diff --git a/test/functional/feature_musig.py b/test/functional/feature_musig.py new file mode 100755 index 000000000000..0ca1f493b456 --- /dev/null +++ b/test/functional/feature_musig.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test musig signing.""" +import random +from io import BytesIO + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.script import CScript, TaprootSignatureHash, OP_1 +from test_framework.key import ECKey, generate_schnorr_nonce +from test_framework.musig import generate_musig_key, aggregate_schnorr_nonces, sign_musig, aggregate_musig_signatures +from test_framework.messages import CTransaction, COutPoint, CTxIn, CTxOut, CScriptWitness, CTxInWitness +from test_framework.util import assert_equal +from test_framework.address import program_to_witness + +class key_musig(BitcoinTestFramework): + + def set_test_params(self): + self.num_nodes = 1 + + def run_test(self): + + self.nodes[0].generate(101) + balance = self.nodes[0].getbalance() + + # Repeat test for random participants/hashtype + for i in range(0, 10): + + num_participants = random.randint(3, 20) + + # Key Generation. + keys = [] + pubkeys = [] + for _ in range(num_participants): + private_key = ECKey() + private_key.generate() + public_key = private_key.get_pubkey() + keys.append((private_key, public_key)) + pubkeys.append(public_key) + + c_map, pk_musig = generate_musig_key(pubkeys) + + keys_c = [] + for private, public in keys: + private_c = private.mul(c_map[public]) + public_c = public.mul(c_map[public]) + keys_c.append((private_c, public_c)) + + # Create Segwit V1 Output. + pk_musig_data = pk_musig.get_bytes() + pk_musig_data_v1 = bytes([pk_musig_data[0] & 1]) + pk_musig_data[1:] + segwit_address = program_to_witness(1, pk_musig_data_v1) + + # Send funds to musig public key (V1 Segwit Output) + txid = self.nodes[0].sendtoaddress(segwit_address, balance / 100000) + tx_hex = self.nodes[0].getrawtransaction(txid) + tx = CTransaction() + tx.deserialize(BytesIO(bytes.fromhex(tx_hex))) + tx.rehash() + + # Determine Segwit output index sent from wallet. + index = 0 + outputs = tx.vout + output = outputs[index] + while (output.scriptPubKey != CScript([OP_1, pk_musig_data_v1])): + index += 1 + output = outputs[index] + output_value = output.nValue + + tx_schnorr = CTransaction() + tx_schnorr.nVersion = 1 + tx_schnorr.nLockTime = 0 + outpoint = COutPoint(tx.sha256, index) + tx_schnorr_in = CTxIn(outpoint=outpoint) + tx_schnorr.vin = [tx_schnorr_in] + + dest_addr = self.nodes[0].getnewaddress(address_type="bech32") + scriptpubkey = bytes.fromhex(self.nodes[0].getaddressinfo(dest_addr)['scriptPubKey']) + min_fee = int(self.nodes[0].getmempoolinfo()['mempoolminfee'] * 100000000) + dest_output = CTxOut(nValue=output_value - min_fee, scriptPubKey=scriptpubkey) + tx_schnorr.vout = [dest_output] + + # Generate Sighash for signing. + hash_types = [0, 1, 2, 3, 0x81, 0x82, 0x83] + hash_idx = random.randint(0, len(hash_types) - 1) + sighash = TaprootSignatureHash(tx_schnorr, [output], hash_types[hash_idx]) + + # Nonce creation. + nonce_map = {} + nonce_points = [] + for private_c, public_c in keys_c: + nonce_map[public_c] = generate_schnorr_nonce() + nonce_points.append(nonce_map[public_c].get_pubkey()) + + # Aggregate Nonces. + R_agg, negated = aggregate_schnorr_nonces(nonce_points) + + # Negate all individual nonces if R_agg was negated. + if negated: + for pk, _ in nonce_map.items(): + nonce_map[pk].negate() + + # Musig Signing. + sigs = [] + for private_c, public_c in keys_c: + signature = sign_musig(private_c, nonce_map[public_c], R_agg, pk_musig, sighash) + sigs.append(signature) + sig_agg = aggregate_musig_signatures(sigs) + if hash_idx is not 0: + sig_agg += hash_types[hash_idx].to_bytes(1, 'big') + + # Construct transaction witness. + witness = CScriptWitness() + witness.stack.append(sig_agg) + witness_in = CTxInWitness() + witness_in.scriptWitness = witness + tx_schnorr.wit.vtxinwit.append(witness_in) + + # Serialize Schnorr transaction for broadcast. + tx_schnorr_str = tx_schnorr.serialize().hex() + + assert_equal( + [{'txid': tx_schnorr.rehash(), 'allowed': True}], + self.nodes[0].testmempoolaccept([tx_schnorr_str]) + ) + +if __name__ == '__main__': + key_musig().main() diff --git a/test/functional/feature_musig_tweaked.py b/test/functional/feature_musig_tweaked.py new file mode 100755 index 000000000000..26bfbbcb75ef --- /dev/null +++ b/test/functional/feature_musig_tweaked.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test musig signing with a tweaked key.""" +import hashlib +from io import BytesIO +import random + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.script import CScript, TaprootSignatureHash, OP_1 +from test_framework.key import ECKey, generate_schnorr_nonce +from test_framework.musig import generate_musig_key, aggregate_schnorr_nonces, sign_musig, aggregate_musig_signatures +from test_framework.messages import CTransaction, COutPoint, CTxIn, CTxOut, CScriptWitness, CTxInWitness +from test_framework.util import assert_equal +from test_framework.address import program_to_witness + +class key_musig_tweaked(BitcoinTestFramework): + + def set_test_params(self): + self.num_nodes = 1 + + def run_test(self): + + self.nodes[0].generate(101) + balance = self.nodes[0].getbalance() + + # Repeat test for random number of participants/tweaking signer/hashtype + for i in range(0, 10): + + num_participants = random.randint(2, 20) + + # Key Generation. + keys = [] + pubkeys = [] + for _ in range(num_participants): + private_key = ECKey() + private_key.generate() + public_key = private_key.get_pubkey() + keys.append((private_key, public_key)) + pubkeys.append(public_key) + + c_map, pk_musig = generate_musig_key(pubkeys) + + keys_c = [] + for private, public in keys: + private_c = private.mul(c_map[public]) + public_c = public.mul(c_map[public]) + keys_c.append((private_c, public_c)) + + # Tweak Musig public key. + tweak = hashlib.sha256(b'tweak').digest() + pk_musig_tweaked = pk_musig.tweak_add(tweak) + + # Create Segwit V1 Output. + pk_musig_tweaked_data = pk_musig_tweaked.get_bytes() + pk_musig_tweaked_data_v1 = bytes([pk_musig_tweaked_data[0] & 1]) + pk_musig_tweaked_data[1:] + segwit_address = program_to_witness(1, pk_musig_tweaked_data_v1) + + # Send funds to musig public key (V1 Segwit Output) + txid = self.nodes[0].sendtoaddress(segwit_address, balance / 100000) + tx_hex = self.nodes[0].getrawtransaction(txid) + tx = CTransaction() + tx.deserialize(BytesIO(bytes.fromhex(tx_hex))) + tx.rehash() + + # Determine Segwit output sent from wallet. + index = 0 + outputs = tx.vout + output = outputs[index] + while (output.scriptPubKey != CScript([OP_1, pk_musig_tweaked_data_v1])): + index += 1 + output = outputs[index] + output_value = output.nValue + + tx_schnorr = CTransaction() + tx_schnorr.nVersion = 1 + tx_schnorr.nLockTime = 0 + outpoint = COutPoint(tx.sha256, index) + tx_schnorr_in = CTxIn(outpoint=outpoint) + tx_schnorr.vin = [tx_schnorr_in] + + dest_addr = self.nodes[0].getnewaddress(address_type="bech32") + scriptpubkey = bytes.fromhex(self.nodes[0].getaddressinfo(dest_addr)['scriptPubKey']) + min_fee = int(self.nodes[0].getmempoolinfo()['mempoolminfee'] * 100000000) + dest_output = CTxOut(nValue=output_value - min_fee, scriptPubKey=scriptpubkey) + tx_schnorr.vout = [dest_output] + + # Generate Sighash for signing. + hash_types = [0, 1, 2, 3, 0x81, 0x82, 0x83] + hash_idx = random.randint(0, len(hash_types) - 1) + sighash = TaprootSignatureHash(tx_schnorr, [output], hash_types[hash_idx]) + + # Nonce creation. + nonce_map = {} + nonce_points = [] + for private_c, public_c in keys_c: + nonce_map[public_c] = generate_schnorr_nonce() + nonce_points.append(nonce_map[public_c].get_pubkey()) + + R_agg, negated = aggregate_schnorr_nonces(nonce_points) + + # Negate all individual nonces if R_agg was negated. + if negated: + for pk, _ in nonce_map.items(): + nonce_map[pk].negate() + + # Musig Signing. + sigs = [] + tweak_idx = random.randint(0, len(keys_c) - 1) + for idx, (private_c, public_c) in enumerate(keys_c): + # One person must tweak keys. + private_c = private_c.tweak_add(tweak) if idx == tweak_idx else private_c + signature = sign_musig(private_c, nonce_map[public_c], R_agg, pk_musig_tweaked, sighash) + sigs.append(signature) + sig_agg = aggregate_musig_signatures(sigs) + + if hash_idx is not 0: + sig_agg += hash_types[hash_idx].to_bytes(1, 'big') + + # Construct transaction witness. + witness = CScriptWitness() + witness.stack.append(sig_agg) + witness_in = CTxInWitness() + witness_in.scriptWitness = witness + tx_schnorr.wit.vtxinwit.append(witness_in) + + # Serialize transaction for broadcast. + tx_schnorr_str = tx_schnorr.serialize().hex() + assert_equal( + [{'txid': tx_schnorr.rehash(), 'allowed': True}], + self.nodes[0].testmempoolaccept([tx_schnorr_str]) + ) + +if __name__ == '__main__': + key_musig_tweaked().main() diff --git a/test/functional/feature_taproot_csa.py b/test/functional/feature_taproot_csa.py new file mode 100755 index 000000000000..921a8b3d57ac --- /dev/null +++ b/test/functional/feature_taproot_csa.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test CHECKSIGADD OPCODE in taproot outputs.""" +import hashlib +from io import BytesIO +import random + +from test_framework.address import program_to_witness +from test_framework.key import ECKey +from test_framework.messages import COutPoint, CTxIn, CTxOut, CTxInWitness +from test_framework.script import TapLeaf, TapTree, CTransaction, TaprootSignatureHash +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, hex_str_to_bytes + +def tx_from_hex(hexstring): + tx = CTransaction() + f = BytesIO(hex_str_to_bytes(hexstring)) + tx.deserialize(f) + return tx + +class tapleaf_csa_desc(BitcoinTestFramework): + + def set_test_params(self): + self.num_nodes = 1 + + def run_test(self): + + # KeyPairs. + sec_map = {} + pkv = [] + for _ in range(10): + sec = ECKey() + sec.generate() + pkv.append(sec.get_pubkey()) + sec_map[pkv[-1].get_bytes()] = sec + + # Hash & Preimage. + preimage = bytes.fromhex('f6dea1fafb5a58df766747091dd70eafe50119687f5e56137d054b39ce8645fa') + h = hashlib.new('ripemd160') + h.update(preimage) + ripemd160_digest = h.hexdigest() + + # Timedelay in blocks + delay = 200 + + # Threshold. + thresh_n = 2 + + # Construct tapleafs from descriptor strings + tapleafs = [] + + desc1 = 'ts(csa({},{},{},{}))'.format(thresh_n, pkv[0].get_bytes().hex(), pkv[1].get_bytes().hex(), pkv[2].get_bytes().hex()) + tapleaf1 = TapLeaf() + tapleaf1.from_desc(desc1) + tapleafs.append(tapleaf1) + assert(tapleaf1.desc == desc1) + + desc2 = 'ts(csahash({},{},{},{},{}))'.format(thresh_n, pkv[0].get_bytes().hex(), pkv[1].get_bytes().hex(), pkv[2].get_bytes().hex(), ripemd160_digest) + tapleaf2 = TapLeaf() + tapleaf2.from_desc(desc2) + tapleafs.append(tapleaf2) + assert(tapleaf2.desc == desc2) + + # Index to mark beginning of timelocked tapscripts in tapleafs array. + delayed_tapscripts_idx = 2 + + desc3 = 'ts(csaolder({},{},{},{},{}))'.format(thresh_n, pkv[0].get_bytes().hex(), pkv[1].get_bytes().hex(), pkv[2].get_bytes().hex(), delay) + tapleaf3 = TapLeaf() + tapleaf3.from_desc(desc3) + tapleafs.append(tapleaf3) + assert(tapleaf3.desc == desc3) + + desc4 = 'ts(csahasholder({},{},{},{},{},{}))'.format(thresh_n, pkv[0].get_bytes().hex(), pkv[1].get_bytes().hex(), pkv[2].get_bytes().hex(), ripemd160_digest, delay) + tapleaf4 = TapLeaf() + tapleaf4.from_desc(desc4) + tapleafs.append(tapleaf4) + assert(tapleaf4.desc == desc4) + + # Construct Taptree with all tapscripts. + taptree = TapTree() + taptree.key = pkv[1] + tapleaf_weights = [] + for tapleaf in tapleafs: + tapleaf_weights.append((random.randint(1, 10), tapleaf)) + taptree.huffman_constructor(tapleaf_weights) + + # Send to segwit v1 output. + self.nodes[0].generate(101) + bal = self.nodes[0].getbalance() + script, tweak, control_map = taptree.construct() + addr = program_to_witness(1, script[2:]) + outputs = {} + outputs[addr] = bal / 100000 + funding_txid_str = self.nodes[0].sendmany("", outputs) + funding_tx_str = self.nodes[0].getrawtransaction(funding_txid_str) + funding_tx = tx_from_hex(funding_tx_str) + + # Determine which output is taproot output. + taproot_index = 0 + utxos = funding_tx.vout + taproot_output = utxos[taproot_index] + while (taproot_output.scriptPubKey != script): + taproot_index += 1 + taproot_output = utxos[taproot_index] + taproot_value = taproot_output.nValue + + # Generate Transaction for each tapscript spend. + delayed_txns = [] + for idx, tapleaf_to_spend in enumerate(tapleafs): + + taproot_spend_tx = CTransaction() + taproot_spend_tx.nLockTime = 0 + funding_tx.rehash() + taproot_output_point = COutPoint(funding_tx.sha256, taproot_index) + + if idx < delayed_tapscripts_idx: + taproot_spend_tx.nVersion = 1 + tx_input = CTxIn(outpoint=taproot_output_point) + else: + taproot_spend_tx.nVersion = 2 + tx_input = CTxIn(outpoint=taproot_output_point, nSequence=delay) + + taproot_spend_tx.vin = [tx_input] + dest_addr = self.nodes[0].getnewaddress(address_type="bech32") + spk = hex_str_to_bytes(self.nodes[0].getaddressinfo(dest_addr)['scriptPubKey']) + min_fee = 5000 + dest_out = CTxOut(nValue=taproot_value - min_fee, scriptPubKey=spk) + taproot_spend_tx.vout = [dest_out] + + # Construct witness according to required satisfaction elements. + htv = [0, 1, 2, 3, 0x81, 0x82, 0x83] + sighash = TaprootSignatureHash(taproot_spend_tx, [taproot_output], htv[0], 0, scriptpath=True, tapscript=tapleaf_to_spend.script) + witness_elements = [] + thresh_count = 0 + for typ, data in tapleaf_to_spend.sat: + if typ == 'preimage': + witness_elements.append(preimage) + elif typ == 'sig': + if thresh_count < thresh_n: + thresh_count += 1 + sig = sec_map[data].sign_schnorr(sighash) + witness_elements.append(sig) + else: + witness_elements.append(b'') + + taproot_spend_tx.wit.vtxinwit.append(CTxInWitness()) + taproot_spend_tx.wit.vtxinwit[0].scriptWitness.stack = witness_elements + [tapleaf_to_spend.script, control_map[tapleaf_to_spend.script]] + taproot_spend_str = taproot_spend_tx.serialize().hex() + + # Timelocked txns will fail. + if idx < delayed_tapscripts_idx: + assert_equal( + [{'txid': taproot_spend_tx.rehash(), 'allowed': True}], + self.nodes[0].testmempoolaccept([taproot_spend_str]) + ) + else: + assert_equal( + [{'txid': taproot_spend_tx.rehash(), 'allowed': False, 'reject-reason': '64: non-BIP68-final'}], + self.nodes[0].testmempoolaccept([taproot_spend_str]) + ) + delayed_txns.append(taproot_spend_tx) + + # Rebroadcast timelocked txs after delay. + self.nodes[0].generate(delay) + + for tx in delayed_txns: + assert_equal( + [{'txid': tx.rehash(), 'allowed': True}], + self.nodes[0].testmempoolaccept([tx.serialize().hex()]) + ) + +if __name__ == '__main__': + tapleaf_csa_desc().main() diff --git a/test/functional/feature_taproot_pk.py b/test/functional/feature_taproot_pk.py new file mode 100755 index 000000000000..e79af8a77753 --- /dev/null +++ b/test/functional/feature_taproot_pk.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test pubkey descriptors for taproot outputs.""" +import hashlib +from io import BytesIO +import random + +from test_framework.address import program_to_witness +from test_framework.key import ECKey +from test_framework.messages import COutPoint, CTxIn, CTxOut, CTxInWitness +from test_framework.script import TapLeaf, TapTree, CTransaction, TaprootSignatureHash +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, hex_str_to_bytes + +def tx_from_hex(hexstring): + tx = CTransaction() + f = BytesIO(hex_str_to_bytes(hexstring)) + tx.deserialize(f) + return tx + +class tapleaf_pk_desc(BitcoinTestFramework): + + def set_test_params(self): + self.num_nodes = 1 + + def run_test(self): + + self.nodes[0].generate(101) + bal = self.nodes[0].getbalance() + + # Repeat test for random tree structure (huffman weights). + # (Test takes a while to complete.) + for i in range(0, 5): + + # KeyPairs. + sec_map = {} + pkv = [] + for _ in range(10): + sec = ECKey() + sec.generate() + pkv.append(sec.get_pubkey()) + sec_map[pkv[-1].get_bytes()] = sec + + # Hash & Preimage. + preimage = bytes.fromhex('f6dea1fafb5a58df766747091dd70eafe50119687f5e56137d054b39ce8645fa') + h = hashlib.new('ripemd160') + h.update(preimage) + ripemd160_digest = h.hexdigest() + + # Timedelay in blocks + delay = 200 + + tapleafs = [] + + desc1 = 'ts(pk({}))'.format(pkv[0].get_bytes().hex()) + tapleaf1 = TapLeaf() + tapleaf1.from_desc(desc1) + tapleafs.append(tapleaf1) + assert(tapleaf1.desc == desc1) + + desc2 = 'ts(pkhash({},{}))'.format(pkv[0].get_bytes().hex(), ripemd160_digest) + tapleaf2 = TapLeaf() + tapleaf2.from_desc(desc2) + tapleafs.append(tapleaf2) + assert(tapleaf2.desc == desc2) + + # Index to mark beginning of timelocked tapscripts in tapleafs array. + delayed_tapscripts_idx = 2 + + desc3 = 'ts(pkolder({},{}))'.format(pkv[0].get_bytes().hex(), delay) + tapleaf3 = TapLeaf() + tapleaf3.from_desc(desc3) + tapleafs.append(tapleaf3) + assert(tapleaf3.desc == desc3) + + desc4 = 'ts(pkhasholder({},{},{}))'.format(pkv[0].get_bytes().hex(), ripemd160_digest, delay) + tapleaf4 = TapLeaf() + tapleaf4.from_desc(desc4) + tapleafs.append(tapleaf4) + assert(tapleaf4.desc == desc4) + + # Construct Taptree with all tapscripts. + taptree = TapTree() + taptree.key = pkv[1] + policy = [] + for tapleaf in tapleafs: + policy.append((random.randint(1, 10), tapleaf)) + taptree.huffman_constructor(policy) + + # Send to segwit v1 output. + script, tweak, control_map = taptree.construct() + addr = program_to_witness(1, script[2:]) + outputs = {} + outputs[addr] = bal / 100000 + funding_txid_str = self.nodes[0].sendmany("", outputs) + funding_tx_str = self.nodes[0].getrawtransaction(funding_txid_str) + funding_tx = tx_from_hex(funding_tx_str) + + # Determine which output is taproot output. + taproot_index = 0 + utxos = funding_tx.vout + taproot_output = utxos[taproot_index] + while (taproot_output.scriptPubKey != script): + taproot_index += 1 + taproot_output = utxos[taproot_index] + taproot_value = taproot_output.nValue + + # Generate Transaction for each tapscript spend. + delayed_txns = [] + for idx, tapleaf_to_spend in enumerate(tapleafs): + + taproot_spend_tx = CTransaction() + taproot_spend_tx.nLockTime = 0 + funding_tx.rehash() + taproot_output_point = COutPoint(funding_tx.sha256, taproot_index) + + if idx < delayed_tapscripts_idx: + taproot_spend_tx.nVersion = 1 + tx_input = CTxIn(outpoint=taproot_output_point) + else: + taproot_spend_tx.nVersion = 2 + tx_input = CTxIn(outpoint=taproot_output_point, nSequence=delay) + + taproot_spend_tx.vin = [tx_input] + dest_addr = self.nodes[0].getnewaddress(address_type="bech32") + spk = hex_str_to_bytes(self.nodes[0].getaddressinfo(dest_addr)['scriptPubKey']) + min_fee = 5000 + dest_out = CTxOut(nValue=taproot_value - min_fee, scriptPubKey=spk) + taproot_spend_tx.vout = [dest_out] + + # Construct witness according to required satisfaction elements. + htv = [0, 1, 2, 3, 0x81, 0x82, 0x83] + sighash = TaprootSignatureHash(taproot_spend_tx, [taproot_output], htv[0], 0, scriptpath=True, tapscript=tapleaf_to_spend.script) + witness_elements = [] + for typ, data in tapleaf_to_spend.sat: + if typ == 'preimage': + witness_elements.append(preimage) + elif typ == 'sig': + sig = sec_map[data].sign_schnorr(sighash) + witness_elements.append(sig) + + taproot_spend_tx.wit.vtxinwit.append(CTxInWitness()) + taproot_spend_tx.wit.vtxinwit[0].scriptWitness.stack = witness_elements + [tapleaf_to_spend.script, control_map[tapleaf_to_spend.script]] + taproot_spend_str = taproot_spend_tx.serialize().hex() + + # Timelocked txns will fail. + if idx < delayed_tapscripts_idx: + assert_equal( + [{'txid': taproot_spend_tx.rehash(), 'allowed': True}], + self.nodes[0].testmempoolaccept([taproot_spend_str]) + ) + else: + assert_equal( + [{'txid': taproot_spend_tx.rehash(), 'allowed': False, 'reject-reason': '64: non-BIP68-final'}], + self.nodes[0].testmempoolaccept([taproot_spend_str]) + ) + delayed_txns.append(taproot_spend_tx) + + # Rebroadcast timelocked txs after delay. + self.nodes[0].generate(delay) + + for tx in delayed_txns: + assert_equal( + [{'txid': tx.rehash(), 'allowed': True}], + self.nodes[0].testmempoolaccept([tx.serialize().hex()]) + ) + +if __name__ == '__main__': + tapleaf_pk_desc().main() diff --git a/test/functional/feature_taptree_constructor.py b/test/functional/feature_taptree_constructor.py new file mode 100755 index 000000000000..5f70debf6c97 --- /dev/null +++ b/test/functional/feature_taptree_constructor.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test taptree construction.""" +from io import BytesIO +import random + +from test_framework.address import program_to_witness +from test_framework.key import ECKey +from test_framework.messages import CTransaction, COutPoint, CTxIn, CTxOut, CTxInWitness, ser_string +from test_framework.script import TapTree, TapLeaf, TaprootSignatureHash, TaggedHash +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import hex_str_to_bytes, assert_equal + +def tx_from_hex(hexstring): + tx = CTransaction() + f = BytesIO(hex_str_to_bytes(hexstring)) + tx.deserialize(f) + return tx + +class taptree_constructor(BitcoinTestFramework): + + def set_test_params(self): + self.num_nodes = 1 + + def run_test(self): + self.nodes[0].generate(101) + bal = self.nodes[0].getbalance() + + for n in range(1, 10): + sec_map = {} + pkv = [] + for _ in range(n): + sec = ECKey() + sec.generate() + pkv.append(sec.get_pubkey()) + sec_map[pkv[-1]] = sec + + # Generate scripts + pk_tapleafs = [] + tapscript_pk_map = {} + for pk in pkv: + tl = TapLeaf() + tl.from_keys([pk]) + pk_tapleafs.append(tl) + tapscript_pk_map[tl.script] = pk + + # csa_tapleafs, _ = TapLeaf.generate_threshold_csa(n, pkv[:m]) + tapleafs = pk_tapleafs + + # Build Tree. + taptree = TapTree() + int_sec = ECKey() + int_sec.generate() + int_pubkey = int_sec.get_pubkey() + policy = [] + for tapleaf in tapleafs: + policy.append((random.randint(1, 10), tapleaf)) + taptree.huffman_constructor(policy) + taptree.key = int_pubkey + + # Construct output. + script, tweak, control_map = taptree.construct() + addr = program_to_witness(1, script[2:]) + outputs = {} + outputs[addr] = bal / 100000 + + # Verify Controlblock. + for tapleaf in tapleafs: + control = control_map[tapleaf.script] + version = control[0] & 0xfe + int_pubkey_b = bytes([(control[0] & 0x01) + 2]) + control[1:33] + m = len(control[33:]) // 32 + k = TaggedHash("TapLeaf", bytes([version]) + ser_string(tapleaf.script)) + for i in range(m): + e = control[33 + 32 * i:65 + 32 * i] + if k < e: + k = TaggedHash("TapBranch", k + e) + else: + k = TaggedHash("TapBranch", e + k) + t = TaggedHash("TapTweak", int_pubkey_b + k) + assert_equal(t, tweak) + + # Send to taproot output. + funding_txid_str = self.nodes[0].sendmany("", outputs) + funding_tx_str = self.nodes[0].getrawtransaction(funding_txid_str) + funding_tx = tx_from_hex(funding_tx_str) + + # Determine which output is taproot output. + taproot_index = 0 + utxos = funding_tx.vout + taproot_output = utxos[taproot_index] + while (taproot_output.scriptPubKey != script): + taproot_index += 1 + taproot_output = utxos[taproot_index] + taproot_value = taproot_output.nValue + + # Test each Script Path. + for tapleaf_to_spend in tapleafs: + + # Generate spending transaction + # [version][in][][locktime] + taproot_spend_tx = CTransaction() + taproot_spend_tx.nVersion = 1 + taproot_spend_tx.nLockTime = 0 + funding_tx.rehash() + taproot_output_point = COutPoint(funding_tx.sha256, taproot_index) + tx_input = CTxIn(outpoint=taproot_output_point) + taproot_spend_tx.vin = [tx_input] + + # Spend amount back to wallet. + # [version][in][out][locktime] + dest_addr = self.nodes[0].getnewaddress(address_type="bech32") + spk = hex_str_to_bytes(self.nodes[0].getaddressinfo(dest_addr)['scriptPubKey']) + min_fee = 5000 + dest_out = CTxOut(nValue=taproot_value - min_fee, scriptPubKey=spk) + taproot_spend_tx.vout = [dest_out] + + htv = [0, 1, 2, 3, 0x81, 0x82, 0x83] + htv_idx = random.randint(0, len(htv) - 1) + sighash = TaprootSignatureHash(taproot_spend_tx, [taproot_output], htv[htv_idx], 0, scriptpath=True, tapscript=tapleaf_to_spend.script) + + # Determine signatures and publickeys necessary to spend this tapscript. + pk = tapscript_pk_map[tapleaf_to_spend.script] + sec = sec_map[pk] + sig = sec.sign_schnorr(sighash) + taproot_spend_tx.wit.vtxinwit.append(CTxInWitness()) + + # 65B signature required for non-zero hash_type. + if htv_idx is not 0: + sig += htv[htv_idx].to_bytes(1, 'big') + taproot_spend_tx.wit.vtxinwit[0].scriptWitness.stack = [sig] + [tapleaf_to_spend.script, control_map[tapleaf_to_spend.script]] + taproot_spend_str = taproot_spend_tx.serialize().hex() + + assert_equal( + [{'txid': taproot_spend_tx.rehash(), 'allowed': True}], + self.nodes[0].testmempoolaccept([taproot_spend_str]) + ) + +if __name__ == '__main__': + taptree_constructor().main() diff --git a/test/functional/test_framework/key.py b/test/functional/test_framework/key.py index b3c283623692..48afb0f53bd5 100644 --- a/test/functional/test_framework/key.py +++ b/test/functional/test_framework/key.py @@ -341,6 +341,46 @@ def verify_schnorr(self, sig, msg): return False return True + def __add__(self, other): + """Adds two ECPubKey points.""" + assert isinstance(other, ECPubKey) + assert self.valid + assert other.valid + ret = ECPubKey() + ret.p = SECP256K1.add(other.p, self.p) + ret.valid = True + ret.compressed = self.compressed + return ret + + def __radd__(self, other): + """Allows this ECPubKey to be added to 0 for sum()""" + if other == 0: + return self + else: + return self + other + + def __mul__(self, private_key): + """Multiplies ECPubKey point with a ECKey scalar.""" + assert isinstance(private_key, ECKey) + assert self.valid + assert private_key.secret < SECP256K1_ORDER and private_key.secret is not None + ret = ECPubKey() + ret.p = SECP256K1.mul([(self.p, private_key.secret)]) + ret.valid = True + ret.compressed = self.compressed + return ret + + def __sub__(self, other): + """Subtract one point from another""" + assert isinstance(other, ECPubKey) + assert self.valid + assert other.valid + ret = ECPubKey() + ret.p = SECP256K1.add(self.p, SECP256K1.negate(other.p)) + ret.valid = True + ret.compressed = self.compressed + return ret + def tweak_add(self, tweak): assert(self.valid) assert(len(tweak) == 32) @@ -356,6 +396,14 @@ def tweak_add(self, tweak): ret.compressed = self.compressed return ret + def mul(self, data): + """Multiplies ECPubKey point with scalar data.""" + assert self.valid + assert len(data) == 32 + other = ECKey() + other.set(data, True) + return self * other + class ECKey(): """A secp256k1 private key""" @@ -380,6 +428,64 @@ def get_bytes(self): assert(self.valid) return self.secret.to_bytes(32, 'big') + def __add__(self, other): + """Add key secrets. Returns compressed key.""" + assert isinstance(other, ECKey) + assert other.secret > 0 and other.secret < SECP256K1_ORDER + assert self.valid is True + ret_data = ((self.secret + other.secret) % SECP256K1_ORDER).to_bytes(32, 'big') + ret = ECKey() + ret.set(ret_data, True) + return ret + + def __radd__(self, other): + """Allows this ECKey to be added to 0 for sum()""" + if other == 0: + return self + else: + return self + other + + def __sub__(self, other): + """Subtract key secrets. Returns compressed key.""" + assert isinstance(other, ECKey) + assert other.secret > 0 and other.secret < SECP256K1_ORDER + assert self.valid is True + ret_data = ((self.secret - other.secret) % SECP256K1_ORDER).to_bytes(32, 'big') + ret = ECKey() + ret.set(ret_data, True) + return ret + + def __mul__(self, other): + """Multiply a private key by another private key or multiply a public key by a private key. Returns compressed key.""" + if isinstance(other, ECKey): + assert other.secret > 0 and other.secret < SECP256K1_ORDER + assert self.valid is True + ret_data = ((self.secret * other.secret) % SECP256K1_ORDER).to_bytes(32, 'big') + ret = ECKey() + ret.set(ret_data, True) + return ret + elif isinstance(other, ECPubKey): + return other * self + else: + raise TypeError + + def add(self, data): + """Add key to scalar data. Returns compressed key.""" + other = ECKey() + other.set(data, True) + return self + other + + def mul(self, data): + """Multiply key secret with scalar data. Returns compressed key.""" + other = ECKey() + other.set(data, True) + return self * other + + def negate(self): + """Negate a private key.""" + assert self.valid + self.secret = SECP256K1_ORDER - self.secret + @property def is_valid(self): return self.valid @@ -419,17 +525,21 @@ def sign_ecdsa(self, msg, low_s=True): sb = s.to_bytes((s.bit_length() + 8) // 8, 'big') return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb - def sign_schnorr(self, msg): - """Construct a bip-schnorr compatible signature with this key.""" - assert(self.valid) - assert(self.compressed) - assert(len(msg) == 32) - kp = int.from_bytes(hashlib.sha256(self.get_bytes() + msg).digest(), 'big') % SECP256K1_ORDER - assert(kp != 0) + def sign_schnorr(self, msg, nonce=None): + """Construct a bip-schnorr compatible signature with this key and an optional, pre-determined nonce.""" + assert self.valid + assert self.compressed + assert len(msg) == 32 + if nonce is not None: + nonce_bytes = nonce.get_bytes() + else: + nonce_bytes = hashlib.sha256(self.get_bytes() + msg).digest() + kp = int.from_bytes(nonce_bytes, 'big') % SECP256K1_ORDER + assert kp != 0 R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, kp)])) k = kp if jacobi_symbol(R[1], SECP256K1_FIELD_SIZE) == 1 else SECP256K1_ORDER - kp e = int.from_bytes(hashlib.sha256(R[0].to_bytes(32, 'big') + self.get_pubkey().get_bytes() + msg).digest(), 'big') % SECP256K1_ORDER - return R[0].to_bytes(32, 'big') + ((k + e*self.secret) % SECP256K1_ORDER).to_bytes(32, 'big') + return R[0].to_bytes(32, 'big') + ((k + e * self.secret) % SECP256K1_ORDER).to_bytes(32, 'big') def tweak_add(self, tweak): """Return a tweaked version of this private key.""" @@ -444,3 +554,16 @@ def tweak_add(self, tweak): ret = ECKey() ret.set(tweaked.to_bytes(32, 'big'), self.compressed) return ret + +def generate_schnorr_nonce(): + """Generate a random valid bip-schnorr nonce. + + See https://github.com/bitcoinops/bips/blob/v0.1/bip-schnorr.mediawiki#Signing. + This implementation ensures the y-coordinate of the nonce point is a quadratic residue modulo the field size.""" + kp = random.randrange(1, SECP256K1_ORDER) + assert kp != 0 + R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, kp)])) + k = kp if jacobi_symbol(R[1], SECP256K1_FIELD_SIZE) == 1 else SECP256K1_ORDER - kp + k_key = ECKey() + k_key.set(k.to_bytes(32, 'big'), True) + return k_key diff --git a/test/functional/test_framework/mininode.py b/test/functional/test_framework/mininode.py index 779863df7963..abde4418ae58 100755 --- a/test/functional/test_framework/mininode.py +++ b/test/functional/test_framework/mininode.py @@ -480,7 +480,7 @@ def close(self, timeout=10): wait_until(lambda: not self.network_event_loop.is_running(), timeout=timeout) self.network_event_loop.close() self.join(timeout) - + NetworkThread.network_event_loop = None # Safe to remove event loop. class P2PDataStore(P2PInterface): """A P2P data store class. diff --git a/test/functional/test_framework/musig.py b/test/functional/test_framework/musig.py new file mode 100644 index 000000000000..179bcd3b2019 --- /dev/null +++ b/test/functional/test_framework/musig.py @@ -0,0 +1,77 @@ +# Copyright (c) 2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Preliminary MuSig implementation. + +WARNING: This code is slow, uses bad randomness, does not properly protect +keys, and is trivially vulnerable to side channel attacks. Do not use for +anything but tests. + +See https://eprint.iacr.org/2018/068.pdf for the MuSig signature scheme implemented here. +""" + +from functools import reduce +import hashlib + +from .key import ( + SECP256K1, + SECP256K1_FIELD_SIZE, + SECP256K1_G, + SECP256K1_ORDER, + jacobi_symbol, +) + +def generate_musig_key(pubkey_list): + """Aggregate individually generated public keys. + + Returns a MuSig public key as defined in the MuSig paper.""" + pubkey_list_sorted = sorted([int.from_bytes(key.get_bytes()[1:], 'big') for key in pubkey_list]) + L = b'' + for px in pubkey_list_sorted: + L += px.to_bytes(32, 'big') + Lh = hashlib.sha256(L).digest() + musig_c = {} + aggregate_key = 0 + for key in pubkey_list: + musig_c[key] = hashlib.sha256(Lh + key.get_bytes()[1:]).digest() + aggregate_key += key.mul(musig_c[key]) + return musig_c, aggregate_key + +def aggregate_schnorr_nonces(nonce_point_list): + """Construct aggregated musig nonce from individually generated nonces.""" + R_agg = sum(nonce_point_list) + R_agg_affine = SECP256K1.affine(R_agg.p) + negated = False + if jacobi_symbol(R_agg_affine[1], SECP256K1_FIELD_SIZE) != 1: + negated = True + R_agg_negated = SECP256K1.mul([(R_agg.p, SECP256K1_ORDER - 1)]) + R_agg.p = R_agg_negated + return R_agg, negated + +def sign_musig(priv_key, k_key, R_musig, P_musig, msg): + """Construct a musig signature.""" + assert priv_key.valid + assert priv_key.compressed + assert len(msg) == 32 + assert k_key is not None and k_key.secret != 0 + Rm = SECP256K1.affine(R_musig.p) + assert jacobi_symbol(Rm[1], SECP256K1_FIELD_SIZE) == 1 + R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k_key.secret)])) + e = int.from_bytes(hashlib.sha256(Rm[0].to_bytes(32, 'big') + P_musig.get_bytes() + msg).digest(), 'big') % SECP256K1_ORDER + return R[0].to_bytes(32, 'big') + ((k_key.secret + e * priv_key.secret) % SECP256K1_ORDER).to_bytes(32, 'big') + +def aggregate_musig_signatures(sigs): + """Construct valid Schnorr signature from individually generated musig signatures.""" + assert sigs + s_list = [] + R_list = [] + for sig in sigs: + assert len(sig) == 64 + s_list.append(int.from_bytes(sig[32:], 'big')) + R = SECP256K1.lift_x(int.from_bytes(sig[:32], 'big')) + if jacobi_symbol(R[1], SECP256K1_FIELD_SIZE) != 1: + R = SECP256K1.negate(R) + R_list.append(R) + s_agg = sum(s_list) % SECP256K1_ORDER + R_agg = reduce(lambda x, y: SECP256K1.add_mixed(x, y), R_list) + return SECP256K1.affine(R_agg)[0].to_bytes(32, 'big') + s_agg.to_bytes(32, 'big') diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index 962d2687f488..74b3e4c4dcf8 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -10,7 +10,10 @@ from .messages import CTransaction, CTxOut, sha256, hash256, uint256_from_str, ser_uint256, ser_string, CTxInWitness from .key import ECKey, ECPubKey +import binascii import hashlib +import itertools +import queue import struct from .bignum import bn2vch @@ -815,3 +818,601 @@ def taproot_construct(pubkey, scripts=[]): def is_op_success(o): return o == 0x50 or o == 0x62 or o == 0x89 or o == 0x8a or o == 0x8d or o == 0x8e or (o >= 0x7e and o <= 0x81) or (o >= 0x83 and o <= 0x86) or (o >= 0x95 and o <= 0x99) or (o >= 0xbb and o <= 0xfe) + +def IsPayToPubkey(script): + pk = ECPubKey() + pk.set(script[1:34]) + return pk.is_valid and len(script) == 35 and script[-1] == OP_CHECKSIG + +def IsCheckSigAdd(script): + if script[-1] == OP_EQUAL and isinstance(script[-2], int) and script[-3] == OP_CHECKSIGADD: + return True + else: + return False + +def ParseDesc(desc, tag, op, cl): + op_tag = tag+op + assert(desc[:len(op_tag)] == op_tag) + desc = desc[len(op_tag):-1] + depth = 0 + for t in desc: + if t == op: + depth += 1 + if t == cl: + depth -= 1 + if depth == 0: + return desc + else: + # Malformed descriptor. + raise Exception + +class TapLeaf: + def __init__(self, script=CScript(), version=DEFAULT_TAPSCRIPT_VER): + self.version = version + self.script = script + self.miniscript = None + self.sat = None + self.keys = None # TODO: Remove. + + def from_keys(self, keys): + if len(keys) == 1: + if keys[0].is_valid: + self.construct_pk(keys[0]) + else: + raise Exception + elif len(keys) > 1: + self.construct_csa(len(keys), keys) + else: + raise Exception + + # TODO: use raw constructor here. + def from_script(self, script): + if IsPayToPubkey(script): + self.keys = [script[1:34]] + elif IsCheckSigAdd(script): + self.keys = [] + for op in script: + if isinstance(op, bytes): + self.keys.append(ECPubKey()) + self.keys[-1].set(op) + self.script = script + + def construct_pk(self, key): #ECPubKey + pk_node = miniscript.pk(key.get_bytes()) + self._set_miniscript(miniscript.c(pk_node)) + self.desc = TapLeaf._desc_serializer('pk',key.get_bytes().hex()) + + def construct_pkolder(self, key, n): #ECPubKey, int + pk_node = miniscript.pk(key.get_bytes()) + older_node = miniscript.older(n) + v_c_pk_node = miniscript.v(miniscript.c(pk_node)) + self._set_miniscript(miniscript.and_v(v_c_pk_node, older_node)) + self.desc = TapLeaf._desc_serializer('pkolder', key.get_bytes().hex(), str(n)) + + def construct_pkhash(self, key, data): #ECPubKey, 20B, int + pk_node = miniscript.pk(key.get_bytes()) + hash_node = miniscript.ripemd160(data) + v_c_pk_node = miniscript.v(miniscript.c(pk_node)) + self._set_miniscript(miniscript.and_v(v_c_pk_node, hash_node)) + self.desc = TapLeaf._desc_serializer('pkhash', key.get_bytes().hex(), data.hex()) + + def construct_pkhasholder(self, key, data, n): #ECPubKey, 20B, int + pk_node = miniscript.pk(key.get_bytes()) + older_node = miniscript.older(n) + v_hash_node = miniscript.v(miniscript.ripemd160(data)) + v_c_pk_node = miniscript.v(miniscript.c(pk_node)) + self._set_miniscript(miniscript.and_v(v_c_pk_node, miniscript.and_v(v_hash_node, older_node))) + self.desc = TapLeaf._desc_serializer('pkhasholder', key.get_bytes().hex(), data.hex(),str(n)) + + def construct_csa(self, k, *args): #int, ECPubKey ... + keys_data = [key.get_bytes() for key in args] + thresh_csa_node = miniscript.thresh_csa(k, *keys_data) + self._set_miniscript(thresh_csa_node) + keys_string = [data.hex() for data in keys_data] + self.desc = TapLeaf._desc_serializer('csa', str(k), *keys_string) + + def construct_csaolder(self, n, k, *args): #int, int, ECPubKey ... + keys_data = [key.get_bytes() for key in args] + thresh_csa_node = miniscript.thresh_csa(k, *keys_data) + v_thresh_csa_node = miniscript.v(thresh_csa_node) + older_node = miniscript.older(n) + self._set_miniscript(miniscript.and_v(v_thresh_csa_node, older_node)) + keys_string = [data.hex() for data in keys_data] + self.desc = TapLeaf._desc_serializer('csaolder', str(k), *keys_string, str(n)) + + def construct_csahash(self, data, k, *args): #int, 20B, ECPubKey ... + keys_data = [key.get_bytes() for key in args] + thresh_csa_node = miniscript.thresh_csa(k, *keys_data) + v_thresh_csa_node = miniscript.v(thresh_csa_node) + hash_node = miniscript.ripemd160(data) + self._set_miniscript(miniscript.and_v(v_thresh_csa_node, hash_node)) + keys_string = [data.hex() for data in keys_data] + self.desc = TapLeaf._desc_serializer('csahash', str(k), *keys_string, data.hex()) + + def construct_csahasholder(self, n, data, k, *args): #int, 20B, int, ECPubKey ... + keys_data = [key.get_bytes() for key in args] + thresh_csa_node = miniscript.thresh_csa(k, *keys_data) + v_thresh_csa_node = miniscript.v(thresh_csa_node) + hash_node = miniscript.ripemd160(data) + v_hash_node = miniscript.v(hash_node) + older_node = miniscript.older(n) + self._set_miniscript(miniscript.and_v(v_thresh_csa_node, miniscript.and_v(v_hash_node, older_node))) + keys_string = [data.hex() for data in keys_data] + self.desc = TapLeaf._desc_serializer('csahasholder', str(k), *keys_string, data.hex(),str(n)) + + def _set_miniscript(self, miniscript): + self.miniscript = miniscript + self.script = CScript(self.miniscript.script) + self.sat = self.miniscript.sat_xy + + @staticmethod + def _desc_serializer(tag, *args): + desc = 'ts(' + tag + '(' + for arg in args[:-1]: + desc += arg + ',' + desc += args[-1] + '))' + return desc + + def from_desc(self,string): + + string = ''.join(string.split()) + tss = ParseDesc(string, 'ts', '(',')') + + if tss[:3] == 'pk(': + expr_s = ParseDesc(tss, 'pk' ,'(' ,')') + args = self._param_parser(expr_s) + pk = ECPubKey() + pk.set(bytes.fromhex(args[0])) + self.construct_pk(pk) + + elif tss[:8] == 'pkolder(': + expr_s = ParseDesc(tss, 'pkolder' ,'(' ,')') + args = self._param_parser(expr_s) + pk = ECPubKey() + pk.set(bytes.fromhex(args[0])) + self.construct_pkolder(pk, int(args[1])) + + elif tss[:7] == 'pkhash(': + expr_s = ParseDesc(tss, 'pkhash' ,'(' ,')') + args = self._param_parser(expr_s) + pk = ECPubKey() + pk.set(bytes.fromhex(args[0])) + data = bytes.fromhex(args[1]) + self.construct_pkhash(pk, data) + + elif tss[:12] == 'pkhasholder(': + expr_s = ParseDesc(tss, 'pkhasholder' ,'(' ,')') + args = self._param_parser(expr_s) + pk = ECPubKey() + pk.set(bytes.fromhex(args[0])) + data = bytes.fromhex(args[1]) + self.construct_pkhasholder(pk, data, int(args[2])) + + elif tss[:4] == 'csa(': + expr_s = ParseDesc(tss, 'csa' ,'(' ,')') + args = self._param_parser(expr_s) + k = int(args[0]) + pkv = [] + for key_string in args[1:]: + pk = ECPubKey() + pk.set(bytes.fromhex(key_string)) + pkv.append(pk) + self.construct_csa(k, *pkv) + + elif tss[:9] == 'csaolder(': + expr_s = ParseDesc(tss, 'csaolder' ,'(' ,')') + args = self._param_parser(expr_s) + k = int(args[0]) + pkv = [] + for key_string in args[1:-1]: + pk = ECPubKey() + pk.set(bytes.fromhex(key_string)) + pkv.append(pk) + time = int(args[-1]) + self.construct_csaolder(time, k, *pkv) + + elif tss[:8] == 'csahash(': + expr_s = ParseDesc(tss, 'csahash' ,'(' ,')') + args = self._param_parser(expr_s) + k = int(args[0]) + pkv = [] + for key_string in args[1:-1]: + pk = ECPubKey() + pk.set(bytes.fromhex(key_string)) + pkv.append(pk) + data = bytes.fromhex(args[-1]) + self.construct_csahash(data, k, *pkv) + + elif tss[:13] == 'csahasholder(': + expr_s = ParseDesc(tss, 'csahasholder' ,'(' ,')') + args = self._param_parser(expr_s) + k = int(args[0]) + pkv = [] + for key_string in args[1:-2]: + pk = ECPubKey() + pk.set(bytes.fromhex(key_string)) + pkv.append(pk) + data = bytes.fromhex(args[-2]) + time = int(args[-1]) + self.construct_csahasholder(time, data, k, *pkv) + + elif tss[:4] =='raw(': + self.script = CScript(binascii.unhexlify(tss[4:-1])) + + else: + raise Exception('Tapscript descriptor not recognized.') + + @staticmethod + def _param_parser(expr_string): + args = [] + idx_ = 0 + expr_string_ = expr_string + for idx, ch in enumerate(expr_string): + if ch == ',': + args.append(expr_string[idx_:idx]) + idx_ = idx+1 + expr_string_ = expr_string[idx_:] + args.append(expr_string_) + return args + + # TODO: Not nice, necessary for sorting priority queue. + def __lt__(self, other): + return True + + def __gt__(self, other): + return True + + @staticmethod + def generate_threshold_csa(n, pubkeys): + if n == 1 or len(pubkeys) <= n: + raise Exception + pubkeys_b = [pubkey.get_bytes() for pubkey in pubkeys] + pubkeys_b.sort() + pubkey_b_sets = list(itertools.combinations(iter(pubkeys_b), n)) + tapscripts = [] + for pubkey_b_set in pubkey_b_sets: + pubkey_set = [] + for pubkey_b in pubkey_b_set: + pk = ECPubKey() + pk.set(pubkey_b) + pubkey_set.append(pk) + tapscript = TapLeaf() + tapscript.construct_csa(len(pubkey_set), *pubkey_set) + tapscripts.append(tapscript) + return tapscripts + +class TapTree: + def __init__(self): + self.root = Node() + self.key = ECPubKey() + + def from_desc(self, desc): + desc = ''.join(desc.split()) + pk = ECPubKey() + pk.set(binascii.unhexlify(desc[3:69])) + if len(desc)>71 and desc[:3] == 'tp(' and pk.is_valid and desc[69] == ',' and desc[-1] == ')': + self.key = pk + self._decode_tree(desc[70:-1], parent=self.root) + else: + raise Exception + + # Tree construction from list(weight(int), TapScript) + def huffman_constructor(self, tuple_list): + p = queue.PriorityQueue() + for weight_tapleaf in tuple_list: + p.put(weight_tapleaf) + while p.qsize() > 1: + l, r = p.get(), p.get() + node = Node() + node.left, node.right = l[1], r[1] + p.put((l[0]+r[0], node)) + self.root = p.get()[1] + + def set_key(self, data): + self.key.set(data) + + @property + def desc(self): + if self.key.is_valid and self.root!= None: + res = 'tp(' + self.key.get_bytes().hex() + ',' + res += TapTree._encode_tree(self.root) + res += ')' + return res + else: + # TODO: Exception message. + raise Exception + + def construct(self): + ctrl, h = self._constructor(self.root) + tweak = TaggedHash("TapTweak", self.key.get_bytes() + h) + control_map = dict((script, GetVersionTaggedPubKey(self.key, version) + control) for version, script, control in ctrl) + tweaked = self.key.tweak_add(tweak) + return (CScript([OP_1, GetVersionTaggedPubKey(tweaked, TAPROOT_VER)]), tweak, control_map) + + @staticmethod + def _constructor(node): + if isinstance(node, TapLeaf): + h = TaggedHash("TapLeaf", bytes([node.version & 0xfe]) + ser_string(node.script)) + ctrl = [(node.version, node.script, bytes())] + return ctrl, h + if isinstance(node.left, TapLeaf): + h_l = TaggedHash("TapLeaf", bytes([node.left.version & 0xfe]) + ser_string(node.left.script)) + ctrl_l = [(node.left.version, node.left.script, bytes())] + else: + ctrl_l, h_l = TapTree._constructor(node.left) + if isinstance(node.right, TapLeaf): + h_r = TaggedHash("TapLeaf", bytes([node.right.version & 0xfe]) + ser_string(node.right.script)) + ctrl_r = [(node.right.version, node.right.script, bytes())] + else: + ctrl_r, h_r = TapTree._constructor(node.right) + + ctrl_l = [(version, script, ctrl + h_r) for version, script, ctrl in ctrl_l] + ctrl_r = [(version, script, ctrl + h_l) for version, script, ctrl in ctrl_r] + if h_r < h_l: + h_r, h_l = h_l, h_r + h = TaggedHash("TapBranch", h_l + h_r) + return (ctrl_l + ctrl_r , h) + + @staticmethod + def _encode_tree(node): + string = '[' + if isinstance(node, TapLeaf): + string += node.desc + string += ']' + return string + if isinstance(node.left, TapLeaf): + string += node.left.desc + else: + string += TapTree._encode_tree(node.left) + string += ',' + if isinstance(node.right, TapLeaf): + string += node.right.desc + else: + string += TapTree._encode_tree(node.right) + string += ']' + return string + + def _decode_tree(self, string, parent=None): + l, r = TapTree._parse_tuple(string) + if not r: + self.root = TapLeaf() + self.root.from_desc(l) + return + if (l[0] == '[' and l[-1] == ']'): + parent.left = Node() + self._decode_tree(l, parent=parent.left) + else: + parent.left = TapLeaf() + parent.left.from_desc(l) + if (r[0] == '[' and r[-1] == ']'): + parent.right = Node() + self._decode_tree(r, parent=parent.right) + else: + parent.right = TapLeaf() + parent.right.from_desc(r) + + @staticmethod + def _parse_tuple(ts): + ts = ts[1:-1] + depth = 0 + l, r = None, None + for idx, ch in enumerate(ts): + if depth == 0 and ch == ',': + l,r = ts[:idx], ts[idx+1:] + break + if ch == '[' or ch == '(': + depth += 1 + if ch == ']' or ch == ')': + depth -= 1 + if depth == 0 and (l and r): + return l, r + elif depth == 0: + return ts, '' + else: + # Malformed tuple. + raise Exception + +class Node(object): + # Internal Taptree Node. + def __init__(self, left=None, right=None): + self.left = left + self.right = right + + # TODO: Not nice, but seems to be necessary for sorting by priority queue. + def __lt__(self, other): + return True + + def __gt__(self, other): + return True + +# Miniscript Node. +class node_type: + def __init__(self, script=False, nsat=False, sat_xy=False, sat_z=False, typ=False, corr=False, mal=False, children=False, childnum=None): + self._script = script + self._nsat = nsat + self._sat_xy = sat_xy + self._sat_z= sat_z + self._typ = typ + self._corr = corr + self._mal = mal + self.children = children # [x,y,z] + + # Assert all corr/mal/child members are defined. + assert(all (key in corr(children).keys() for key in ('z','o','n','d','u'))) + for _ , value in vars(self).items(): + assert(value != None) + # assert(len(children)==3) # This doesn't hold with threshold. + + def __getattr__(self,name): + attr = getattr(self, '_'+name) + if attr != None: + # All lambda's must accept children argument. + return attr(self.children) + else: + return None + +# Factory class to generate miniscript nodes. +class miniscript: + @staticmethod + def decode(string): + tag, exprs = miniscript._parse(string) + + # Return terminal expressions: + # ['pk','pk_h','older','after','sha256','hash256','ripemd160','hash160','1','0']: + if tag in ['pk','pk_h', 'older', 'ripemd160', 'thresh_csa']: + + if tag in ['pk', 'pk_h']: + key_b = bytes.fromhex(exprs[0]) + return getattr(miniscript, tag)(key_b) + + elif tag in ['older']: + n = int(exprs[0]) + return getattr(miniscript, tag)(n) + + elif tag in ['ripemd160']: + digest = bytes.fromhex(exprs[0]) + return getattr(miniscript, tag)(digest) + + elif tag in ['thresh_csa']: + k = int(exprs[0]) + keys = [] + for key_string in exprs[1:]: + key_data = bytes.fromhex(key_string) + keys.append(key_data) + return getattr(miniscript, tag)(k, *keys) + + child_nodes = [] + for expr in exprs: + child_nodes.append(miniscript.decode(expr)) + return getattr(miniscript, tag)(*child_nodes) + + @staticmethod + def _parse(string): + # TODO: Handle single arg case. + string = ''.join(string.split()) + depth = 0 + tag = '' + exprs = [] + for idx, ch in enumerate(string): + if ch == ':' and depth == 0: + return string[:idx], [string[idx+1:]] + if ch == '(': + depth += 1 + if depth == 1: + tag = string[:idx] + prev_idx = idx + if ch == ')': + depth -= 1 + if depth == 0: + exprs.append(string[prev_idx+1:idx]) + if depth == 1 and ch == ',': + exprs.append(string[prev_idx+1:idx]) + prev_idx = idx + if depth == 0 and bool(tag) and bool(exprs): + return tag, exprs + else: + raise Exception('Malformed miniscript string.') + + @staticmethod + def pk(key): + assert((key[0] in [0x02, 0x03]) or (key[0] not in [0x04, 0x06, 0x07])) + assert(len(key) == 33) + script = lambda x: [key] + nsat = lambda x: [0] + sat_xy = lambda x: [('sig', key)] + sat_z = lambda x: [False] + typ = lambda x: 'K' # Only one possible. + corr = lambda x: {'z': False,'o': True, 'n': True, 'd': True, 'u': True} + mal = lambda x: {'e': True,'f': False, 'm': True, 's': True} + children = [None, None, None] # Terminal. + return node_type(script=script, nsat=nsat, sat_xy=sat_xy, sat_z=sat_z, typ=typ, corr=corr, mal=mal,children=children) + + @staticmethod + def older(n): + assert(n >= 1 and n < 2**32) + script = lambda x: [CScriptNum(n), OP_CHECKSEQUENCEVERIFY] + nsat = lambda x: [False] + sat_xy = lambda x: [] + sat_z = lambda x: [False] + typ = lambda x: 'B' + corr = lambda x: {'z': True,'o': False, 'n': False, 'd': False, 'u': False} + mal = lambda x: {'e': False,'f': True, 'm': True, 's': False} + children = [None, None, None] # Terminal. + return node_type(script=script, nsat=nsat, sat_xy=sat_xy, sat_z=sat_z, typ=typ, corr=corr, mal=mal,children=children) + + @staticmethod + def ripemd160(data): + assert(len(data) == 20) + script = lambda x: [OP_SIZE, CScriptNum(32), OP_EQUALVERIFY, OP_RIPEMD160, data, OP_EQUAL] + nsat = lambda x: [b'\x00'*32] # Not non-malleably. + sat_xy = lambda x: [('preimage', data)] + sat_z = lambda x: [False] + typ = lambda x: 'B' + corr = lambda x: {'z': False,'o': True, 'n': True, 'd': True, 'u': True} + mal = lambda x: {'e': False,'f': False, 'm': True, 's': False} + children = [None, None, None] # Terminal. + return node_type(script=script, nsat=nsat, sat_xy=sat_xy, sat_z=sat_z, typ=typ, corr=corr, mal=mal,children=children) + + @staticmethod + def c(expr): + script = lambda x: x[0].script + [OP_CHECKSIG] + nsat = lambda x: x[0].nsat + sat_xy = lambda x: x[0].sat_xy + sat_z = lambda x: [False] + typ = lambda x: 'B' if x[0].typ == 'K' else False + corr = lambda x: {'z': False,'o': x[0].corr['o'], 'n': x[0].corr['n'], 'd': x[0].corr['d'], 'u': True} + mal = lambda x: {'f': False, 'e': x[0].mal['e'], 'm': x[0].mal['m'], 's': x[0].mal['s']} + children = [expr, None, None] + return node_type(script=script, nsat=nsat, sat_xy=sat_xy, sat_z=sat_z, typ=typ, corr=corr, mal=mal,children=children) + + @staticmethod + def v(expr): + script = lambda x: x[0].script + [OP_VERIFY] + nsat = lambda x: [False] + sat_xy = lambda x: x[0].sat_xy + sat_z = lambda x: [False] + typ = lambda x: 'V' if x[0].typ == 'B' else False + corr = lambda x: {'z': x[0].corr['z'],'o': x[0].corr['o'], 'n': x[0].corr['n'], 'd': False, 'u': False} + mal = lambda x: {'f': True, 'e': False, 'm': x[0].mal['m'], 's': x[0].mal['s']} + children = [expr, None, None] + return node_type(script=script, nsat=nsat, sat_xy=sat_xy, sat_z=sat_z, typ=typ, corr=corr, mal=mal,children=children) + + @staticmethod + def and_v(expr_l, expr_r): + script = lambda x: x[0].script + x[1].script + nsat = lambda x: [False] + sat_xy = lambda x: x[1].sat_xy + x[0].sat_xy + sat_z = lambda x: [False] + typ = lambda x:\ + 'B' if (x[0].typ == 'V' and x[1].typ == 'B') else\ + 'K' if (x[0].typ == 'V' and x[1].typ == 'K') else\ + 'V' if (x[0].typ == 'V' and x[1].typ == 'V') else False + corr = lambda x: {\ + 'z': bool(x[0].corr['z']*x[1].corr['z']),\ + 'o': bool(x[0].corr['z']*x[1].corr['o']+x[0].corr['o']*x[1].corr['z']),\ + 'n': bool(x[0].corr['n']+x[0].corr['z']*x[1].corr['n']),\ + 'd': False,\ + 'u': False} + mal = lambda x: {\ + 'f': bool(x[0].mal['f']*x[1].mal['f']),\ + 'e': False,\ + 'm':bool(x[0].mal['m']*x[1].mal['m']),\ + 's': bool(x[0].mal['s']+x[1].mal['s'])} + children = [expr_l, expr_r, None] + return node_type(script=script, nsat=nsat, sat_xy=sat_xy, sat_z=sat_z, typ=typ, corr=corr, mal=mal,children=children) + + @staticmethod # TODO: + def thresh_csa(k, *args): #arg[0] = k, arg[i>0] = expr_i + assert(k > 0 and k <= len(args) and len(args) > 1) # Requires more than 1 pk. + for key in args: + assert(len(key) == 33) + assert(key[0] in [0x02, 0x03]) or (key[0] not in [0x04, 0x06, 0x07]) + script = lambda x: [args[0], OP_CHECKSIG] + list(itertools.chain.from_iterable([[args[i], OP_CHECKSIGADD] for i in range(1,len(args))])) + [k, OP_NUMEQUAL] + nsat = lambda x: [0x00]*len(args) + sat_xy = lambda x: [('sig', args[i]) for i in range(0,len(args))][::-1] # TODO: ('thresh(n)', [('sig', (0x02../0x00)), ('sig', (0x02../0x00))]) + sat_z = lambda x: [False] + typ = lambda x: 'B' + corr = lambda x: {'z': False,'o': False, 'n': False, 'd': True, 'u': True} + mal = lambda x: {'f': False, 'e': True, 'm': True, 's': True} + children = [None, None, None] # Terminal expression. + return node_type(script=script, nsat=nsat, sat_xy=sat_xy, sat_z=sat_z, typ=typ, corr=corr, mal=mal,children=children) diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 9aff08fdc7da..e054d0da907d 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -105,6 +105,17 @@ def __init__(self): def main(self): """Main function. This should not be overridden by the subclass test scripts.""" + self.parse_args() + + try: + e = None + self.setup() + self.run_test() + except BaseException as exception: + e = exception + self.shutdown(e = e, exit = True) + + def parse_args(self): parser = argparse.ArgumentParser(usage="%(prog)s [options]") parser.add_argument("--nocleanup", dest="nocleanup", default=False, action="store_true", help="Leave bitcoinds and test.* datadir on exit or error") @@ -135,6 +146,10 @@ def main(self): self.add_options(parser) self.options = parser.parse_args() + # Methods to encapsulate setup and shutdown of test. + def setup(self): + """Call this method to startup the test object with options already set.""" + PortSeed.n = self.options.port_seed check_json_precision() @@ -181,33 +196,25 @@ def main(self): self.network_thread = NetworkThread() self.network_thread.start() - success = TestStatus.FAILED + self.success = TestStatus.FAILED - try: - if self.options.usecli: - if not self.supports_cli: - raise SkipTest("--usecli specified but test does not support using CLI") - self.skip_if_no_cli() - self.skip_test_if_missing_module() - self.setup_chain() - self.setup_network() - self.run_test() - success = TestStatus.PASSED - except JSONRPCException: - self.log.exception("JSONRPC error") - except SkipTest as e: - self.log.warning("Test Skipped: %s" % e.message) - success = TestStatus.SKIPPED - except AssertionError: - self.log.exception("Assertion failed") - except KeyError: - self.log.exception("Key error") - except Exception: - self.log.exception("Unexpected exception caught during testing") - except KeyboardInterrupt: - self.log.warning("Exiting after keyboard interrupt") + if self.options.usecli: + if not self.supports_cli: + raise SkipTest("--usecli specified but test does not support using CLI") + self.skip_if_no_cli() + self.skip_test_if_missing_module() + self.setup_chain() + self.setup_network() - if success == TestStatus.FAILED and self.options.pdbonfailure: + def shutdown(self, e=None, exit=False): + """Call this method to shutdown the test object and optionally handle an exception.""" + + if e != None: + self.handle_exception(e) + else: + self.success = TestStatus.PASSED + + if self.success == TestStatus.FAILED and self.options.pdbonfailure: print("Testcase failed. Attaching python debugger. Enter ? for help") pdb.set_trace() @@ -225,7 +232,7 @@ def main(self): should_clean_up = ( not self.options.nocleanup and not self.options.noshutdown and - success != TestStatus.FAILED and + self.success != TestStatus.FAILED and not self.options.perf ) if should_clean_up: @@ -238,10 +245,10 @@ def main(self): self.log.warning("Not cleaning up dir {}".format(self.options.tmpdir)) cleanup_tree_on_exit = False - if success == TestStatus.PASSED: + if self.success == TestStatus.PASSED: self.log.info("Tests successful") exit_code = TEST_EXIT_PASSED - elif success == TestStatus.SKIPPED: + elif self.success == TestStatus.SKIPPED: self.log.info("Test skipped") exit_code = TEST_EXIT_SKIPPED else: @@ -251,7 +258,26 @@ def main(self): logging.shutdown() if cleanup_tree_on_exit: shutil.rmtree(self.options.tmpdir) - sys.exit(exit_code) + + self.nodes.clear() + + if exit: + sys.exit(exit_code) + + def handle_exception(self, e): + if isinstance(e, JSONRPCException): + self.log.exception("JSONRPC error") + elif isinstance(e, SkipTest): + self.log.warning("Test Skipped: %s" % e.message) + self.success = TestStatus.SKIPPED + elif isinstance(e, AssertionError): + self.log.exception("Assertion failed") + elif isinstance(e, KeyError): + self.log.exception("Key error") + elif isinstance(e, Exception): + self.log.exception("Unexpected exception caught during testing") + elif isinstance(e, KeyboardInterrupt): + self.log.warning("Exiting after keyboard interrupt") # Methods to override in subclass test scripts. def set_test_params(self): @@ -440,7 +466,7 @@ def sync_all(self, nodes=None, **kwargs): def _start_logging(self): # Add logger and logging handlers - self.log = logging.getLogger('TestFramework') + self.log = logging.getLogger('TestFramework.'+ self.options.tmpdir) # Assign new logger name to prevent temp path reuse. self.log.setLevel(logging.DEBUG) # Create file handler to log all messages fh = logging.FileHandler(self.options.tmpdir + '/test_framework.log', encoding='utf-8') diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index f8a35dc68081..383d833fe496 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -131,6 +131,11 @@ 'wallet_createwallet.py --usecli', 'wallet_watchonly.py', 'wallet_watchonly.py --usecli', + 'feature_musig.py', + 'feature_musig_tweaked.py', + 'feature_taproot_csa.py', + 'feature_taproot_pk.py', + 'feature_taptree_constructor.py', 'interface_http.py', 'interface_rpc.py', 'rpc_psbt.py',