From eaf536e0107d4d0d17f93dab3d9396f373993287 Mon Sep 17 00:00:00 2001 From: JamesC Date: Wed, 14 Aug 2019 16:44:39 -0400 Subject: [PATCH 1/9] [test] Refactor BitcoinTestFramework main function. In order to wrap the BitcoinTestFramework class in a user-defined class, which can be started and shutdown, the code in BitcoinTestFramework main has been encapsulated in respective functions. Additionally, test-run specific state in temp-directory and NetworkThread is now reset after shutdown. --- test/functional/test_framework/mininode.py | 2 +- .../test_framework/test_framework.py | 86 ++++++++++++------- 2 files changed, 57 insertions(+), 31 deletions(-) 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/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') From 2d93ec1d28b24407966d83a7423c4b6912a60df0 Mon Sep 17 00:00:00 2001 From: JamesC Date: Thu, 1 Aug 2019 17:55:14 -0400 Subject: [PATCH 2/9] [test] Add add/multiply methods to ECPubkey and ECKey classes. These methods are required for the multiplication/tweaking of private/public key pairs. Keypairs are multiplied by a challenge factor in musig signatures. Keypairs are tweaked in both discrete log contracts and adaptor signatures. --- test/functional/test_framework/key.py | 65 +++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/functional/test_framework/key.py b/test/functional/test_framework/key.py index b3c283623692..4916d8993230 100644 --- a/test/functional/test_framework/key.py +++ b/test/functional/test_framework/key.py @@ -341,6 +341,26 @@ def verify_schnorr(self, sig, msg): return False return True + def __add__(self, pk): + """Adds two ECPubKey points.""" + assert(self.valid) + assert(pk.valid) + ret = ECPubKey() + ret.p = SECP256K1.add(pk.p, self.p) + ret.valid = True + ret.compressed = self.compressed + return ret + + def __mul__(self, private_key): + """Multiplies ECPubKey point with scalar data.""" + 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 tweak_add(self, tweak): assert(self.valid) assert(len(tweak) == 32) @@ -356,6 +376,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) + key = ECKey() + key.set(data, True) + return self * key + class ECKey(): """A secp256k1 private key""" @@ -380,6 +408,43 @@ def get_bytes(self): assert(self.valid) return self.secret.to_bytes(32, 'big') + def __add__(self, key): + """Add key secrets. Returns compressed key.""" + assert(key.secret > 0 and key.secret < SECP256K1_ORDER) + assert(self.valid == True) + ret_data = ((self.secret + key.secret) % SECP256K1_ORDER).to_bytes(32, 'big') + ret = ECKey() + ret.set(ret_data, True) + return ret + + def __mul__(self, key): + """Multiply private keys. Returns compressed key.""" + if isinstance(key, ECKey): + assert(key.secret > 0 and key.secret < SECP256K1_ORDER) + assert(self.valid == True) + ret_data = ((self.secret * key.secret) % SECP256K1_ORDER).to_bytes(32, 'big') + ret = ECKey() + ret.set(ret_data, True) + return ret + elif isinstance(key, ECPubKey): + return key * self + + def add(self, data): + """Add key to scalar data. Returns compressed key.""" + key = ECKey() + key.set(data, True) + return self + key + + def mul(self, data): + """Multiply key secret with scalar data. Returns compressed key.""" + key = ECKey() + key.set(data, True) + return self * key + + def negate(self): + assert(self.valid) + self.secret = SECP256K1_ORDER - self.secret + @property def is_valid(self): return self.valid From aca314a91006905c071672227150f61c6c94e27c Mon Sep 17 00:00:00 2001 From: John Newbery Date: Sun, 15 Sep 2019 16:30:38 -0400 Subject: [PATCH 3/9] add/mul method nits --- test/functional/test_framework/key.py | 82 +++++++++++++++++---------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/test/functional/test_framework/key.py b/test/functional/test_framework/key.py index 4916d8993230..22e7fc138811 100644 --- a/test/functional/test_framework/key.py +++ b/test/functional/test_framework/key.py @@ -341,20 +341,29 @@ def verify_schnorr(self, sig, msg): return False return True - def __add__(self, pk): + def __add__(self, other): """Adds two ECPubKey points.""" - assert(self.valid) - assert(pk.valid) + assert isinstance(other, ECPubKey) + assert self.valid + assert other.valid ret = ECPubKey() - ret.p = SECP256K1.add(pk.p, self.p) + 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 scalar data.""" - assert(self.valid) - assert(private_key.secret < SECP256K1_ORDER and private_key.secret is not None) + """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 @@ -378,11 +387,11 @@ def tweak_add(self, tweak): def mul(self, data): """Multiplies ECPubKey point with scalar data.""" - assert(self.valid) - assert(len(data) == 32) - key = ECKey() - key.set(data, True) - return self * key + assert self.valid + assert len(data) == 32 + other = ECKey() + other.set(data, True) + return self * other class ECKey(): """A secp256k1 private key""" @@ -408,41 +417,52 @@ def get_bytes(self): assert(self.valid) return self.secret.to_bytes(32, 'big') - def __add__(self, key): + def __add__(self, other): """Add key secrets. Returns compressed key.""" - assert(key.secret > 0 and key.secret < SECP256K1_ORDER) - assert(self.valid == True) - ret_data = ((self.secret + key.secret) % SECP256K1_ORDER).to_bytes(32, 'big') + 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, key): - """Multiply private keys. Returns compressed key.""" - if isinstance(key, ECKey): - assert(key.secret > 0 and key.secret < SECP256K1_ORDER) - assert(self.valid == True) - ret_data = ((self.secret * key.secret) % SECP256K1_ORDER).to_bytes(32, 'big') + def __radd__(self, other): + """Allows this ECKey to be added to 0 for sum()""" + if other == 0: + return self + else: + return self + other + + 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(key, ECPubKey): - return key * self + elif isinstance(other, ECPubKey): + return other * self + else: + raise TypeError def add(self, data): """Add key to scalar data. Returns compressed key.""" - key = ECKey() - key.set(data, True) - return self + key + other = ECKey() + other.set(data, True) + return self + other def mul(self, data): """Multiply key secret with scalar data. Returns compressed key.""" - key = ECKey() - key.set(data, True) - return self * key + other = ECKey() + other.set(data, True) + return self * other def negate(self): - assert(self.valid) + """Negate a private key.""" + assert self.valid self.secret = SECP256K1_ORDER - self.secret @property From 502742ec92b4b0d9670c17087372d5bb82e0bdd6 Mon Sep 17 00:00:00 2001 From: John Newbery Date: Mon, 16 Sep 2019 15:24:40 -0400 Subject: [PATCH 4/9] Add subtract method --- test/functional/test_framework/key.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/functional/test_framework/key.py b/test/functional/test_framework/key.py index 22e7fc138811..d61229af0909 100644 --- a/test/functional/test_framework/key.py +++ b/test/functional/test_framework/key.py @@ -370,6 +370,17 @@ def __mul__(self, private_key): 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) @@ -434,6 +445,16 @@ def __radd__(self, other): 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): From daa4d6deaa82169d906702bc1f538597531e4b2d Mon Sep 17 00:00:00 2001 From: JamesC Date: Thu, 1 Aug 2019 18:03:09 -0400 Subject: [PATCH 5/9] [test] Add schnorr method which allows signing with a valid, predetermined Schnorr nonce. --- test/functional/test_framework/key.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/test/functional/test_framework/key.py b/test/functional/test_framework/key.py index d61229af0909..9a5406f152e0 100644 --- a/test/functional/test_framework/key.py +++ b/test/functional/test_framework/key.py @@ -525,12 +525,15 @@ 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.""" + def sign_schnorr(self, msg, k_key = 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) - kp = int.from_bytes(hashlib.sha256(self.get_bytes() + msg).digest(), 'big') % SECP256K1_ORDER + if k_key is None: + kp = int.from_bytes(hashlib.sha256(self.get_bytes() + msg).digest(), 'big') % SECP256K1_ORDER + else: + kp = int.from_bytes(k_key.get_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 @@ -550,3 +553,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/sipa/bips/blob/bip-schnorr/bip-schnorr.mediawiki. + 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 From f2a2c0910fb3fd90858fe2d75082d718d2f0180e Mon Sep 17 00:00:00 2001 From: John Newbery Date: Sun, 15 Sep 2019 16:40:45 -0400 Subject: [PATCH 6/9] schnorr method nits --- test/functional/test_framework/key.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/test/functional/test_framework/key.py b/test/functional/test_framework/key.py index 9a5406f152e0..48afb0f53bd5 100644 --- a/test/functional/test_framework/key.py +++ b/test/functional/test_framework/key.py @@ -525,20 +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, k_key = None): + 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 k_key is None: - kp = int.from_bytes(hashlib.sha256(self.get_bytes() + msg).digest(), 'big') % SECP256K1_ORDER + assert self.valid + assert self.compressed + assert len(msg) == 32 + if nonce is not None: + nonce_bytes = nonce.get_bytes() else: - kp = int.from_bytes(k_key.get_bytes(), 'big') % SECP256K1_ORDER - assert(kp != 0) + 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.""" @@ -557,10 +558,10 @@ def tweak_add(self, tweak): def generate_schnorr_nonce(): """Generate a random valid bip-schnorr nonce. - See https://github.com/sipa/bips/blob/bip-schnorr/bip-schnorr.mediawiki. + 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) + 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() From 94ebb67df3874051dc051e442de14f9f26a7d393 Mon Sep 17 00:00:00 2001 From: JamesC Date: Thu, 1 Aug 2019 18:07:10 -0400 Subject: [PATCH 7/9] [test] Add MuSig signature support MuSig scheme is based on the interactive MuSig proposal, which is not formalized into a BIP proposal at the time of this writing. --- test/functional/feature_musig.py | 129 +++++++++++++++++++++ test/functional/feature_musig_tweaked.py | 136 +++++++++++++++++++++++ test/functional/test_framework/musig.py | 72 ++++++++++++ test/functional/test_runner.py | 2 + 4 files changed, 339 insertions(+) create mode 100755 test/functional/feature_musig.py create mode 100755 test/functional/feature_musig_tweaked.py create mode 100644 test/functional/test_framework/musig.py 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/test_framework/musig.py b/test/functional/test_framework/musig.py new file mode 100644 index 000000000000..2069154983b0 --- /dev/null +++ b/test/functional/test_framework/musig.py @@ -0,0 +1,72 @@ +# Copyright (c) 2019 James Chiang +# 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. +""" + +import hashlib +from functools import reduce + +from .key import ECPubKey, SECP256K1, SECP256K1_FIELD_SIZE, SECP256K1_ORDER, SECP256K1_G, 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 = {} + tweaked_pks = [] + for key in pubkey_list: + musig_c[key] = hashlib.sha256(Lh + key.get_bytes()[1:]).digest() + tweaked_pks.append(key.mul(musig_c[key])) + return musig_c, reduce(lambda x, y: x + y, tweaked_pks) + +def aggregate_schnorr_nonces(nonce_point_list): + """Construct aggregated musig nonce from individually generated nonces.""" + R_agg = reduce(lambda x, y: x + y, nonce_point_list) + R_agg_affine = SECP256K1.affine(R_agg.p) + 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 + else: + negated = False + 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 != 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 idx, sig in enumerate(sigs): + assert len(sig) is 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 = reduce(lambda x, y: (x + y) % SECP256K1_ORDER, s_list) + 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_runner.py b/test/functional/test_runner.py index f8a35dc68081..78c95751321a 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -131,6 +131,8 @@ 'wallet_createwallet.py --usecli', 'wallet_watchonly.py', 'wallet_watchonly.py --usecli', + 'feature_musig.py', + 'feature_musig_tweaked.py', 'interface_http.py', 'interface_rpc.py', 'rpc_psbt.py', From dac842c1aaf9f961d59e9b4a1c7d2d0ba3136244 Mon Sep 17 00:00:00 2001 From: John Newbery Date: Sun, 15 Sep 2019 17:03:52 -0400 Subject: [PATCH 8/9] musig nits --- test/functional/test_framework/musig.py | 45 ++++++++++++++----------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/test/functional/test_framework/musig.py b/test/functional/test_framework/musig.py index 2069154983b0..179bcd3b2019 100644 --- a/test/functional/test_framework/musig.py +++ b/test/functional/test_framework/musig.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019 James Chiang +# 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. @@ -10,10 +10,16 @@ See https://eprint.iacr.org/2018/068.pdf for the MuSig signature scheme implemented here. """ -import hashlib from functools import reduce +import hashlib -from .key import ECPubKey, SECP256K1, SECP256K1_FIELD_SIZE, SECP256K1_ORDER, SECP256K1_G, jacobi_symbol +from .key import ( + SECP256K1, + SECP256K1_FIELD_SIZE, + SECP256K1_G, + SECP256K1_ORDER, + jacobi_symbol, +) def generate_musig_key(pubkey_list): """Aggregate individually generated public keys. @@ -25,48 +31,47 @@ def generate_musig_key(pubkey_list): L += px.to_bytes(32, 'big') Lh = hashlib.sha256(L).digest() musig_c = {} - tweaked_pks = [] + aggregate_key = 0 for key in pubkey_list: musig_c[key] = hashlib.sha256(Lh + key.get_bytes()[1:]).digest() - tweaked_pks.append(key.mul(musig_c[key])) - return musig_c, reduce(lambda x, y: x + y, tweaked_pks) + 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 = reduce(lambda x, y: x + y, nonce_point_list) + 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 - else: - negated = False 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 != None and k_key.secret != 0) + 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) + 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') + 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) + assert sigs s_list = [] R_list = [] - for idx, sig in enumerate(sigs): - assert len(sig) is 64 + 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 = reduce(lambda x, y: (x + y) % SECP256K1_ORDER, s_list) + 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') + return SECP256K1.affine(R_agg)[0].to_bytes(32, 'big') + s_agg.to_bytes(32, 'big') From 18a2613ea6eaf2108deb0cd11d0948f94fe2de23 Mon Sep 17 00:00:00 2001 From: JamesC Date: Fri, 26 Jul 2019 00:25:37 -0400 Subject: [PATCH 9/9] [test] Add Taptree, Tapleaf classes and generator methods. Added miniscript class and expressions. Added subset of miniscript, which can be used to construct/satisfy scripts in a composable manner. Add miniscript to TapLeaf descriptors. TapLeaf class now deserializes/serializes descriptors with miniscript nodes. This facilitates construction of new tapleaf descriptors due to composability. New TapLeaf descriptors added containing time/hash expressions. Added TapLeaf test for pk descriptor types. Test constructs and spends scriptpaths of taptree which is constructed from various pk tapscript descriptor types containing pk, hash and time expressions. Added TapLeaf test for csa descriptor types. Test constructs and spends scriptpaths of taptree which is constructed from various csa (checksigadd) tapscript descriptor types containing pk, hash and time expressions. --- test/functional/feature_taproot_csa.py | 175 +++++ test/functional/feature_taproot_pk.py | 171 +++++ .../functional/feature_taptree_constructor.py | 142 +++++ test/functional/test_framework/script.py | 601 ++++++++++++++++++ test/functional/test_runner.py | 3 + 5 files changed, 1092 insertions(+) create mode 100755 test/functional/feature_taproot_csa.py create mode 100755 test/functional/feature_taproot_pk.py create mode 100755 test/functional/feature_taptree_constructor.py 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/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_runner.py b/test/functional/test_runner.py index 78c95751321a..383d833fe496 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -133,6 +133,9 @@ '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',