From 74fe0322a2f34dea165c4abeb0aba2ff96dc4596 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Fri, 17 Jan 2025 08:05:55 +1300 Subject: [PATCH 01/15] pullpush: return 0 if pulling a varint from an exhausted buffer --- src/pullpush.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pullpush.c b/src/pullpush.c index e45e8031a..50db25fe6 100644 --- a/src/pullpush.c +++ b/src/pullpush.c @@ -149,8 +149,12 @@ uint64_t pull_varint(const unsigned char **cursor, size_t *max) uint64_t v; pull_bytes(buf, 1, cursor, max); + if (!*cursor) + return 0; if ((len = varint_length_from_bytes(buf) - 1)) pull_bytes(buf + 1, len, cursor, max); + if (!*cursor) + return 0; varint_from_bytes(buf, &v); return v; } From 05d5e0adc8363a522cb087cb9982168663ee5695 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 23 Jan 2025 11:15:39 +1300 Subject: [PATCH 02/15] python: contrib: fix linter issues --- src/swig_python/contrib/elements_tx.py | 4 ++-- src/swig_python/contrib/mnemonic.py | 4 ++-- src/swig_python/contrib/psbt.py | 9 ++++----- src/swig_python/contrib/reconcile_sigs.py | 8 +++----- src/swig_python/contrib/signmessage.py | 3 ++- src/swig_python/contrib/tx.py | 8 ++++---- 6 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/swig_python/contrib/elements_tx.py b/src/swig_python/contrib/elements_tx.py index c6ca2c297..a9e3d69b3 100644 --- a/src/swig_python/contrib/elements_tx.py +++ b/src/swig_python/contrib/elements_tx.py @@ -86,8 +86,8 @@ def test_tx(self): tx_add_output(tx, tx_output) ct_value = tx_confidential_value_from_satoshi(20000) tx_add_elements_raw_output(tx, script, None, ct_value, None, None, None, 0) - size = tx_get_length(tx, 0) - vsize = tx_vsize_from_weight(tx_get_weight(tx)) + tx_get_length(tx, 0) + tx_vsize_from_weight(tx_get_weight(tx)) for extra_flags in (0, WALLY_TX_FLAG_USE_ELEMENTS, WALLY_TX_FLAG_ALLOW_PARTIAL): tx_hex = tx_to_hex(tx, WALLY_TX_FLAG_USE_WITNESS | extra_flags) tx_bytes = tx_to_bytes(tx, WALLY_TX_FLAG_USE_WITNESS | extra_flags) diff --git a/src/swig_python/contrib/mnemonic.py b/src/swig_python/contrib/mnemonic.py index 58709eaa7..ab74b326e 100644 --- a/src/swig_python/contrib/mnemonic.py +++ b/src/swig_python/contrib/mnemonic.py @@ -50,12 +50,12 @@ def to_seed(self, mnemonic, passphrase = ''): try: m.generate(BIP39_ENTROPY_LEN_256 - 1) assert False - except: + except Exception as _: pass try: m.check(phrase + ' foo') assert False - except: + except Exception as _: pass assert m.to_entropy(phrase) == m.to_entropy(phrase.split()) assert m.to_mnemonic(m.to_entropy(phrase)) == phrase diff --git a/src/swig_python/contrib/psbt.py b/src/swig_python/contrib/psbt.py index 3b8d669d4..9c8dd9155 100644 --- a/src/swig_python/contrib/psbt.py +++ b/src/swig_python/contrib/psbt.py @@ -150,7 +150,7 @@ def test_add_remove_tx_items(self): asset = hex_to_bytes('77' * 32) explicit_asset = hex_to_bytes('01' + '77' * 32) blinded_asset = hex_to_bytes('0a' + '77' * 32) # Dummy value - explicit_value = tx_confidential_value_from_satoshi(1234) + explicit_value = tx_confidential_value_from_satoshi(value) blinded_value = hex_to_bytes('08' + '55' * 32) # Dummy value # Inputs: BTC @@ -166,9 +166,9 @@ def test_add_remove_tx_items(self): # Outputs: BTC psbt2 = psbt_init(2, 0, 0, 0, 0) - tx_output = tx_output_init(1234, script) + tx_output = tx_output_init(value, script) psbt_add_tx_output_at(psbt2, 0, 0, tx_output) - self.assertEqual(psbt_get_output_amount(psbt2, 0), 1234) + self.assertEqual(psbt_get_output_amount(psbt2, 0), value) self.assertEqual(psbt_get_output_script(psbt2, 0), script) if is_elements_build(): @@ -180,7 +180,7 @@ def test_add_remove_tx_items(self): # txout has explicit value/asset: Expect the values # set and no commitments in the PSET self.assertEqual(psbt_has_output_amount(pset2, 0), 1) - self.assertEqual(psbt_get_output_amount(pset2, 0), 1234) + self.assertEqual(psbt_get_output_amount(pset2, 0), value) self.assertEqual(psbt_get_output_value_commitment_len(pset2, 0), 0) self.assertTrue(not psbt_get_output_value_commitment(pset2, 0)) self.assertEqual(psbt_get_output_script(pset2, 0), script) @@ -339,7 +339,6 @@ def test_psbt(self): dummy_tap_internal_key = bytearray(b'\x01' * 32) # Untweaked x-only pubkey if is_elements_build(): dummy_nonce = bytearray(b'\x00' * WALLY_TX_ASSET_CT_NONCE_LEN) - dummy_bf = bytearray(b'\x00' * BLINDING_FACTOR_LEN) dummy_blind_asset = bytearray(b'\x0a' * ASSET_COMMITMENT_LEN) dummy_blind_value = bytearray(b'\x08' * WALLY_TX_ASSET_CT_VALUE_UNBLIND_LEN) dummy_nonce = bytearray(b'\x02' * ASSET_COMMITMENT_LEN) diff --git a/src/swig_python/contrib/reconcile_sigs.py b/src/swig_python/contrib/reconcile_sigs.py index b99ebde56..5d046f298 100644 --- a/src/swig_python/contrib/reconcile_sigs.py +++ b/src/swig_python/contrib/reconcile_sigs.py @@ -8,12 +8,11 @@ except ImportError: have_pycoin = False -USE_WITNESS = 1 class TxTests(unittest.TestCase): def do_test_tx(self, sighash, index_, flags): - txhash, seq, script, witness_script = b'0' * 32, 0xffffffff, b'\x51', b'000000' + txhash, seq, script = b'0' * 32, 0xffffffff, b'\x51' out_script, spend_script, locktime = b'\x00\x00\x51', b'\x00\x51', 999999 txs_in = [TxIn(txhash, 0, script, seq), TxIn(txhash, 1, script+b'\x51', seq-1), @@ -29,7 +28,7 @@ def do_test_tx(self, sighash, index_, flags): 3: TxOut(5003, spend_script)} unspent = pytx.unspents[index_] pytx_hex = pytx.as_hex() - if flags & USE_WITNESS: + if flags & WALLY_TX_FLAG_USE_WITNESS: pytx_hash = pytx.signature_for_hash_type_segwit(unspent.script, index_, sighash) else: pytx_hash = pytx.signature_hash(spend_script, index_, sighash) @@ -44,7 +43,6 @@ def do_test_tx(self, sighash, index_, flags): tx_add_raw_output(tx, 54, out_script+b'\x51', 0) tx_add_raw_output(tx, 53, out_script+b'\x51\x51', 0) tx_hex = tx_to_hex(tx, 0) - amount = (index_ + 1) * 5000 tx_hash = tx_get_btc_signature_hash(tx, index_, unspent.script, unspent.coin_value, sighash, flags) @@ -57,7 +55,7 @@ def test_tx(self): for sighash in [WALLY_SIGHASH_ALL, WALLY_SIGHASH_NONE, WALLY_SIGHASH_SINGLE]: for index_ in [0, 1, 2, 3]: for anyonecanpay in [0, WALLY_SIGHASH_ANYONECANPAY]: - for flags in [0, USE_WITNESS]: + for flags in [0, WALLY_TX_FLAG_USE_WITNESS]: self.do_test_tx(sighash | anyonecanpay, index_, flags) diff --git a/src/swig_python/contrib/signmessage.py b/src/swig_python/contrib/signmessage.py index 45badd087..70ec40790 100644 --- a/src/swig_python/contrib/signmessage.py +++ b/src/swig_python/contrib/signmessage.py @@ -27,9 +27,10 @@ def test_signmessage(self): message = 'This is just a test message'.encode('ascii') priv_key_wif = 'cUeKHd5orzT3mz8P9pxyREHfsWtVfgsfDjiZZBcjUBAaGk1BTj7N' address = 'mpLQjfK79b7CCV4VMJWEWAj5Mpx8Up5zxB' - expected_signature = 'INbVnW4e6PeRmsv2Qgu8NuopvrVjkcxob+sX8OcZG0SALhWybUjzMLPdAsXI46YZGb0KQTRii+wWIQzRpG/U+S0=' + expected_signature = b'INbVnW4e6PeRmsv2Qgu8NuopvrVjkcxob+sX8OcZG0SALhWybUjzMLPdAsXI46YZGb0KQTRii+wWIQzRpG/U+S0=' signature = self.signmessage(priv_key_wif, message) + self.assertEqual(signature, expected_signature) self.assertTrue(self.verifymessage(address, signature, message)) diff --git a/src/swig_python/contrib/tx.py b/src/swig_python/contrib/tx.py index 833657064..1665d6974 100644 --- a/src/swig_python/contrib/tx.py +++ b/src/swig_python/contrib/tx.py @@ -19,7 +19,7 @@ def test_tx_witness(self): with self.assertRaises(ValueError): tx_witness_stack_clone(None) - cloned = tx_witness_stack_clone(witness) + tx_witness_stack_clone(witness) def test_tx_input(self): # Test invalid inputs @@ -101,9 +101,9 @@ def test_tx(self): self.assertEqual(tx_get_total_output_satoshi(tx), 10000) tx_add_raw_output(tx, 20000, script, 0) self.assertEqual(tx_get_total_output_satoshi(tx), 30000) - size = tx_get_length(tx, 0) - vsize = tx_vsize_from_weight(tx_get_weight(tx)) - tx_hex = tx_to_hex(tx, FLAG_USE_WITNESS) + tx_get_length(tx, 0) + tx_vsize_from_weight(tx_get_weight(tx)) + tx_to_hex(tx, FLAG_USE_WITNESS) with self.assertRaises(ValueError): tx_add_raw_output(tx, WALLY_SATOSHI_MAX + 1, script, 0) From 19042d10051a0fedb338307debac7a8226e5be28 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 23 Jan 2025 11:17:15 +1300 Subject: [PATCH 03/15] python: tests: fix linter issues --- src/test/test_aes.py | 2 +- src/test/test_anti_exfil.py | 2 +- src/test/test_elements.py | 2 +- src/test/test_map.py | 5 ++--- src/test/test_sign.py | 2 +- src/test/test_transaction.py | 1 - src/test/util.py | 1 - 7 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/test/test_aes.py b/src/test/test_aes.py index dd75de47c..ff09b5a5a 100755 --- a/src/test/test_aes.py +++ b/src/test/test_aes.py @@ -93,7 +93,7 @@ def test_aes_cbc(self): self.assertEqual(h(out_buf), h(o)) def test_aes_cbc_with_ecdh_key(self): - ENCRYPT, DECRYPT, PUBKEY_LEN, _ = 1, 2, 33, True + ENCRYPT, DECRYPT, _ = 1, 2, True a_priv = make_cbuffer('1c6a837d1ac663fdc7f1002327ca38452766eaf4fe3b80ce620bf7cd3f584cf6')[0] a_pub = make_cbuffer('03e581be89d1ef8ce11d60746d08e4f8aedf934d1d861dd436042ee2e3b16db918')[0] b_priv = make_cbuffer('0b6b3dc90d203d854100110788ac87d43aa00620c9cdb361b281b09022ef4b53')[0] diff --git a/src/test/test_anti_exfil.py b/src/test/test_anti_exfil.py index e339792c4..36bbecac6 100755 --- a/src/test/test_anti_exfil.py +++ b/src/test/test_anti_exfil.py @@ -16,7 +16,7 @@ def test_anti_exfil(self): flags = FLAG_ECDSA - ret = wally_ec_public_key_from_private_key(priv_key, 32, pub_key, 33); + ret = wally_ec_public_key_from_private_key(priv_key, 32, pub_key, 33) self.assertEqual(WALLY_OK, ret) ret = wally_ae_host_commit_from_bytes(entropy, 32, flags, host_commitment, 32) diff --git a/src/test/test_elements.py b/src/test/test_elements.py index 8012aa8b7..198f84699 100755 --- a/src/test/test_elements.py +++ b/src/test/test_elements.py @@ -202,7 +202,7 @@ def test_blinding(self): SCALAR_OFFSET_LEN = 32 offset, offset_len = make_cbuffer('00' * SCALAR_OFFSET_LEN) ret = wally_asset_scalar_offset(value, UNBLINDED_ABF, UNBLINDED_ABF_LEN, - UNBLINDED_VBF, UNBLINDED_VBF_LEN, offset, offset_len); + UNBLINDED_VBF, UNBLINDED_VBF_LEN, offset, offset_len) self.assertEqual(ret, WALLY_OK) self.assertEqual(h(offset), utf8('4e5f3ca8aa2048eeacc8c300e3d63ca92048f407264352bee2fb15bd44349c45')) diff --git a/src/test/test_map.py b/src/test/test_map.py index f74e721e8..4fc45e0ec 100644 --- a/src/test/test_map.py +++ b/src/test/test_map.py @@ -162,7 +162,7 @@ def test_map(self): self.assertEqual(wally_map_find(clone, new_key, new_key_len), (WALLY_OK, 0)) # Re-create clone to test combining - self.assertEqual(wally_map_free(clone), WALLY_OK); + self.assertEqual(wally_map_free(clone), WALLY_OK) self.assertEqual(wally_map_init_alloc(0, None, clone), WALLY_OK) self.assertEqual(wally_map_add(clone, new_key, new_key_len, v, vl), WALLY_OK) @@ -246,7 +246,7 @@ def test_keypath_map(self): self.assertEqual(wally_map_add(m, bip32, bip32_len, kp_path, len(kp_path)), WALLY_OK) # Fingerprint/Path - out, out_len = (c_ubyte * (FP_LEN + 5 * 4))(), 24 + out = (c_ubyte * (FP_LEN + 5 * 4))() cases = [ (None, 0, out, FP_LEN), # NULL key (m, 1, out, FP_LEN), # Bad index @@ -255,7 +255,6 @@ def test_keypath_map(self): ] for args in cases: self.assertEqual(wally_map_keypath_get_item_fingerprint(*args), WALLY_EINVAL) - plen = out_len - 4 if args[3] == FP_LEN else out_len - 5 if args[0] is None or args[1] != 0: ret = wally_map_keypath_get_item_path_len(args[0], args[1]) self.assertEqual(ret, (WALLY_EINVAL, 0)) diff --git a/src/test/test_sign.py b/src/test/test_sign.py index 193393d69..aadd38de2 100755 --- a/src/test/test_sign.py +++ b/src/test/test_sign.py @@ -188,7 +188,7 @@ def test_invalid_inputs(self): (priv_key, len(priv_key), out_buf, 10)] # Wrong out len for pk, pk_len, o, o_len in cases: - ret = wally_ec_public_key_from_private_key(pk, pk_len, o, o_len); + ret = wally_ec_public_key_from_private_key(pk, pk_len, o, o_len) self.assertEqual(ret, WALLY_EINVAL) diff --git a/src/test/test_transaction.py b/src/test/test_transaction.py index fb8920602..85338caef 100644 --- a/src/test/test_transaction.py +++ b/src/test/test_transaction.py @@ -484,7 +484,6 @@ def test_get_taproot_signature_hash(self): annex = None annex_len = 0 - fn = wally_tx_get_btc_taproot_signature_hash args = [tx, index, scripts, values, num_values, tapleaf_script, tapleaf_script_len, key_version, codesep_pos, annex, annex_len, sighash, flags, bytes_out, out_len] diff --git a/src/test/util.py b/src/test/util.py index 5b37098c3..19c7c1de6 100755 --- a/src/test/util.py +++ b/src/test/util.py @@ -3,7 +3,6 @@ from os.path import isfile, abspath from os import urandom import platform -import sys # Allow to run from any sub dir SO_EXT = 'dylib' if platform.system() == 'Darwin' else 'dll' if platform.system() == 'Windows' else 'so' From c3bea88435fca690a43478e7c680436c509abc22 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 23 Jan 2025 11:17:39 +1300 Subject: [PATCH 04/15] python: tools: fix linter issues --- tools/build_wrappers.py | 11 ++++++----- tools/wordlist_cc.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tools/build_wrappers.py b/tools/build_wrappers.py index f024feaf5..81277e67c 100755 --- a/tools/build_wrappers.py +++ b/tools/build_wrappers.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import os import subprocess -import sys # Structs with no definition in the public header files OPAQUE_STRUCTS = [u'words', u'wally_descriptor'] @@ -338,9 +337,9 @@ def gen_wally_hpp(funcs, all_funcs): prev = func.args[-3] impl.append(f' if (ret == WALLY_OK && n != static_cast({prev.name}.size())) ret = WALLY_EINVAL;') if is_verify_function: - impl.append(f' return ret == WALLY_OK;') + impl.append(' return ret == WALLY_OK;') else: - impl.append(f' return detail::check_ret(__FUNCTION__, ret);') + impl.append(' return detail::check_ret(__FUNCTION__, ret);') impl.extend([u'}', u'']) (cpp_elements if func.is_elements else cpp)[func.name] = impl @@ -490,8 +489,10 @@ def map_args(func): js_args.append(js_arg_type) - if ts_arg_type: ts_args.append(f'{arg.name}: {ts_arg_type}') - if ts_return_type: ts_returns.append(f'{arg.name}: {ts_return_type}') + if ts_arg_type: + ts_args.append(f'{arg.name}: {ts_arg_type}') + if ts_return_type: + ts_returns.append(f'{arg.name}: {ts_return_type}') continue diff --git a/tools/wordlist_cc.py b/tools/wordlist_cc.py index c74fc18ac..8ad8ed108 100755 --- a/tools/wordlist_cc.py +++ b/tools/wordlist_cc.py @@ -17,7 +17,7 @@ def as_hex(s): assert len(words) >= 2 assert len(words) in bits - lengths = [ 0 ]; + lengths = [ 0 ] for w in words: lengths.append(lengths[-1] + len(w.encode('utf-8')) + 1) idxs = ['{0}+{1}'.format(string_name, n) for n in lengths] From 2d8d18fee803f183444747f19514bdef44da0b3d Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 23 Jan 2025 11:18:05 +1300 Subject: [PATCH 05/15] python: setup/docs: fix linter issues --- docs/source/conf.py | 3 ++- setup.py | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 6e85d0994..3e92aedbf 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # # libwally-core documentation build configuration file +from os import getenv + SCANNING, DOCS, FUNC = 1, 2, 3 -from os import getenv # DUMP_FUNCS/DUMP_INTERNAL are used by tools/build_wrappers.py to auto-generate wrapper code DUMP_FUNCS = getenv("WALLY_DOC_DUMP_FUNCS") is not None DUMP_INTERNAL = DUMP_FUNCS and getenv("WALLY_DOC_DUMP_INTERNAL") is not None diff --git a/setup.py b/setup.py index ab1a0e9e1..96122c6e9 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,11 @@ """setuptools config for wallycore """ from setuptools import setup, Extension -import copy, os, platform, shutil, subprocess, sys +import copy import distutils.sysconfig +import os +import platform +import subprocess +import sys def _msg(s): print(s + '\n', file=sys.stderr) From 0361a77fe0f2afc47898aa5906d70cba36a4556e Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 23 Jan 2025 23:22:22 +1300 Subject: [PATCH 06/15] descriptor: simplify analyze_pubkey_hex args before refactoring for taproot --- src/descriptor.c | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/descriptor.c b/src/descriptor.c index 1550b809b..f63e53360 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -2022,8 +2022,11 @@ static int analyze_address(ms_ctx *ctx, const char *str, size_t str_len, return ret; } -static int analyze_pubkey_hex(ms_ctx *ctx, const char *str, size_t str_len, - uint32_t flags, ms_node *node, bool *is_hex) +/* take the possible hex data in node->data, if it is a valid pubkey + * convert it to an allocated binary buffer and make this node a key node + */ +static int analyze_pubkey_hex(ms_ctx *ctx, ms_node *node, + uint32_t flags, bool *is_hex) { unsigned char pubkey[EC_PUBLIC_KEY_UNCOMPRESSED_LEN + 1]; size_t offset = flags & WALLY_MINISCRIPT_TAPSCRIPT ? 1 : 0; @@ -2031,26 +2034,28 @@ static int analyze_pubkey_hex(ms_ctx *ctx, const char *str, size_t str_len, *is_hex = false; if (offset) { - if (str_len != EC_XONLY_PUBLIC_KEY_LEN * 2) + if (node->data_len != EC_XONLY_PUBLIC_KEY_LEN * 2) return WALLY_OK; /* Only X-only pubkeys allowed under tapscript */ pubkey[0] = 2; /* Non-X-only pubkey prefix, for validation below */ } else { - if (str_len != EC_PUBLIC_KEY_LEN * 2 && str_len != EC_PUBLIC_KEY_UNCOMPRESSED_LEN * 2) + if (node->data_len != EC_PUBLIC_KEY_LEN * 2 && + node->data_len != EC_PUBLIC_KEY_UNCOMPRESSED_LEN * 2) return WALLY_OK; /* Unknown public key size */ } - if (wally_hex_n_to_bytes(str, str_len, pubkey + offset, sizeof(pubkey) - offset, &written) != WALLY_OK || + if (wally_hex_n_to_bytes(node->data, node->data_len, + pubkey + offset, sizeof(pubkey) - offset, &written) != WALLY_OK || wally_ec_public_key_verify(pubkey, written + offset) != WALLY_OK) return WALLY_OK; /* Not hex, or not a pubkey */ if (!clone_bytes((unsigned char **)&node->data, pubkey + offset, written)) return WALLY_ENOMEM; - node->data_len = str_len / 2; - if (str_len == EC_PUBLIC_KEY_UNCOMPRESSED_LEN * 2) { + node->data_len = node->data_len / 2; + if (node->data_len == EC_PUBLIC_KEY_UNCOMPRESSED_LEN) { node->flags |= WALLY_MS_IS_UNCOMPRESSED; ctx->features |= WALLY_MS_IS_UNCOMPRESSED; } - if (str_len == EC_XONLY_PUBLIC_KEY_LEN * 2) { + if (node->data_len == EC_XONLY_PUBLIC_KEY_LEN) { node->flags |= WALLY_MS_IS_X_ONLY; ctx->features |= WALLY_MS_IS_X_ONLY; } @@ -2108,7 +2113,7 @@ static int analyze_miniscript_key(ms_ctx *ctx, uint32_t flags, } /* check key (public key) */ - ret = analyze_pubkey_hex(ctx, node->data, node->data_len, flags, node, &is_hex); + ret = analyze_pubkey_hex(ctx, node, flags, &is_hex); if (ret == WALLY_OK && is_hex) return WALLY_OK; From 323948ff579d0635de2d951cebd06ed4226407f9 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Fri, 24 Jan 2025 00:22:24 +1300 Subject: [PATCH 07/15] descriptor: add rawtr constant, add support for x-only hex keys We must differentiate between x-only being allowed, mandatory, and not allowed, so the logic is a little more complex. If we get a compressed (non-x-only) hex key for a rawtr() expression, convert it in-place to an x-only key to make future handling simpler. --- src/descriptor.c | 54 ++++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/src/descriptor.c b/src/descriptor.c index f63e53360..42524aaa3 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -86,6 +86,7 @@ #define KIND_DESCRIPTOR_COMBO (0x00030000 | KIND_DESCRIPTOR) #define KIND_DESCRIPTOR_ADDR (0x00040000 | KIND_DESCRIPTOR) #define KIND_DESCRIPTOR_RAW (0x00050000 | KIND_DESCRIPTOR) +#define KIND_DESCRIPTOR_RAW_TR (0x00100000 | KIND_DESCRIPTOR) /* miniscript */ #define KIND_MINISCRIPT_PK (0x00000100 | KIND_MINISCRIPT) @@ -2028,41 +2029,50 @@ static int analyze_address(ms_ctx *ctx, const char *str, size_t str_len, static int analyze_pubkey_hex(ms_ctx *ctx, ms_node *node, uint32_t flags, bool *is_hex) { - unsigned char pubkey[EC_PUBLIC_KEY_UNCOMPRESSED_LEN + 1]; - size_t offset = flags & WALLY_MINISCRIPT_TAPSCRIPT ? 1 : 0; - size_t written; - - *is_hex = false; - if (offset) { - if (node->data_len != EC_XONLY_PUBLIC_KEY_LEN * 2) + unsigned char pubkey[EC_PUBLIC_KEY_UNCOMPRESSED_LEN]; + size_t pubkey_len; + bool allow_xonly, make_xonly = false; + + *is_hex = wally_hex_n_to_bytes(node->data, node->data_len, + pubkey, sizeof(pubkey), &pubkey_len) == WALLY_OK; + if (!is_hex || pubkey_len > sizeof(pubkey)) + return WALLY_OK; /* Not hex, or too long */ + + if (wally_ec_public_key_verify(pubkey, pubkey_len) != WALLY_OK && + wally_ec_xonly_public_key_verify(pubkey, pubkey_len) != WALLY_OK) + return WALLY_OK; /* Not a valid pubkey */ + + make_xonly = node->parent && (node->parent->kind == KIND_DESCRIPTOR_RAW_TR); + allow_xonly = make_xonly || flags & WALLY_MINISCRIPT_TAPSCRIPT; + if (pubkey_len == EC_PUBLIC_KEY_UNCOMPRESSED_LEN && allow_xonly) + return WALLY_OK; /* Uncompressed key not allowed here */ + if (pubkey_len == EC_XONLY_PUBLIC_KEY_LEN && !allow_xonly) + return WALLY_OK; /* X-only not allowed here */ + if (pubkey_len != EC_XONLY_PUBLIC_KEY_LEN) { + if (flags & WALLY_MINISCRIPT_TAPSCRIPT) return WALLY_OK; /* Only X-only pubkeys allowed under tapscript */ - pubkey[0] = 2; /* Non-X-only pubkey prefix, for validation below */ - } else { - if (node->data_len != EC_PUBLIC_KEY_LEN * 2 && - node->data_len != EC_PUBLIC_KEY_UNCOMPRESSED_LEN * 2) - return WALLY_OK; /* Unknown public key size */ + if (make_xonly) { + /* Convert to x-only */ + --pubkey_len; + memmove(pubkey, pubkey + 1, pubkey_len); + } } - if (wally_hex_n_to_bytes(node->data, node->data_len, - pubkey + offset, sizeof(pubkey) - offset, &written) != WALLY_OK || - wally_ec_public_key_verify(pubkey, written + offset) != WALLY_OK) - return WALLY_OK; /* Not hex, or not a pubkey */ - - if (!clone_bytes((unsigned char **)&node->data, pubkey + offset, written)) + if (!clone_bytes((unsigned char **)&node->data, pubkey, pubkey_len)) return WALLY_ENOMEM; - node->data_len = node->data_len / 2; - if (node->data_len == EC_PUBLIC_KEY_UNCOMPRESSED_LEN) { + node->data_len = pubkey_len; + + if (pubkey_len == EC_PUBLIC_KEY_UNCOMPRESSED_LEN) { node->flags |= WALLY_MS_IS_UNCOMPRESSED; ctx->features |= WALLY_MS_IS_UNCOMPRESSED; } - if (node->data_len == EC_XONLY_PUBLIC_KEY_LEN) { + if (pubkey_len == EC_XONLY_PUBLIC_KEY_LEN) { node->flags |= WALLY_MS_IS_X_ONLY; ctx->features |= WALLY_MS_IS_X_ONLY; } ctx->features |= WALLY_MS_IS_RAW; node->kind = KIND_PUBLIC_KEY; node->flags |= WALLY_MS_IS_RAW; - *is_hex = true; return ctx_add_key_node(ctx, node); } From e7e6e35c015459107f5717e09ab71e7ddad7a21e Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Fri, 24 Jan 2025 00:32:50 +1300 Subject: [PATCH 08/15] descriptor: support forcing a key to be generated as x-only This is to support rawtr() which can be given either an x-only or compressed (non-x-only) key. --- src/descriptor.c | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/descriptor.c b/src/descriptor.c index 42524aaa3..a608011c4 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -1112,8 +1112,9 @@ static int generate_number(int64_t number, ms_node *parent, return WALLY_OK; } -static int generate_pk_k(ms_ctx *ctx, ms_node *node, - unsigned char *script, size_t script_len, size_t *written) +static int generate_pk_k_impl(ms_ctx *ctx, ms_node *node, + unsigned char *script, size_t script_len, + bool force_xonly, size_t *written) { unsigned char buff[EC_PUBLIC_KEY_UNCOMPRESSED_LEN]; int ret; @@ -1126,15 +1127,30 @@ static int generate_pk_k(ms_ctx *ctx, ms_node *node, if (*written != EC_PUBLIC_KEY_LEN && *written != EC_XONLY_PUBLIC_KEY_LEN && *written != EC_PUBLIC_KEY_UNCOMPRESSED_LEN) return WALLY_EINVAL; /* Invalid pubkey length */ - if (*written <= script_len) { + if (force_xonly) { + if (*written == EC_PUBLIC_KEY_UNCOMPRESSED_LEN) + return WALLY_EINVAL; /* Can't make x-only from uncompressed key */ + if (*written == EC_XONLY_PUBLIC_KEY_LEN) + force_xonly = false; /* Already x-only */ + else + *written -= 1; /* Account for stripping the lead byte below */ + } + if (*written + 1 <= script_len) { script[0] = *written & 0xff; /* push opcode */ - memcpy(script + 1, buff, *written); + memcpy(script + 1, buff + (force_xonly ? 1 : 0), *written); } *written += 1; } return ret; } +static int generate_pk_k(ms_ctx *ctx, ms_node *node, + unsigned char *script, size_t script_len, size_t *written) +{ + const bool force_xonly = false; + return generate_pk_k_impl(ctx, node, script, script_len, force_xonly, written); +} + static int generate_pk_h(ms_ctx *ctx, ms_node *node, unsigned char *script, size_t script_len, size_t *written) { From aa7b8b85f74464beb52eddf1e6487b09e95387c6 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Fri, 24 Jan 2025 00:36:08 +1300 Subject: [PATCH 09/15] descriptor: add support for rawtr() --- src/descriptor.c | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/descriptor.c b/src/descriptor.c index a608011c4..02ad58e26 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -693,6 +693,15 @@ static int verify_raw(ms_ctx *ctx, ms_node *node) return WALLY_OK; } +static int verify_raw_tr(ms_ctx *ctx, ms_node *node) +{ + if (node->child->builtin || !(node->child->kind & KIND_KEY) || + node_has_uncompressed_key(ctx, node)) + return WALLY_EINVAL; + node->type_properties = builtin_get(node)->type_properties; + return WALLY_OK; +} + static int verify_delay(ms_ctx *ctx, ms_node *node) { (void)ctx; @@ -1380,6 +1389,21 @@ static int generate_raw(ms_ctx *ctx, ms_node *node, return *written > REDEEM_SCRIPT_MAX_SIZE ? WALLY_EINVAL : ret; } +static int generate_raw_tr(ms_ctx *ctx, ms_node *node, + unsigned char *script, size_t script_len, size_t *written) +{ + int ret = WALLY_OK; + + if (script_len >= WALLY_SCRIPTPUBKEY_P2TR_LEN) { + script[0] = OP_1; + const bool force_xonly = true; + ret = generate_pk_k_impl(ctx, node, script + 1, script_len - 1, + force_xonly, written); + } + *written = WALLY_SCRIPTPUBKEY_P2TR_LEN; + return ret; +} + static int generate_delay(ms_ctx *ctx, ms_node *node, unsigned char *script, size_t script_len, size_t *written) { @@ -1788,6 +1812,11 @@ static const struct ms_builtin_t g_builtins[] = { KIND_DESCRIPTOR_RAW, TYPE_NONE, 0xffffffff, verify_raw, generate_raw + }, { + I_NAME("rawtr"), + KIND_DESCRIPTOR_RAW_TR, + TYPE_NONE, + 1, verify_raw_tr, generate_raw_tr }, /* miniscript */ { @@ -2472,6 +2501,9 @@ static int node_generation_size(const ms_node *node, size_t *total) case KIND_DESCRIPTOR_RAW: /* No-op */ break; + case KIND_DESCRIPTOR_RAW_TR: + *total += WALLY_SCRIPTPUBKEY_P2TR_LEN; + break; case KIND_MINISCRIPT_PK_K: *total += 1; break; From 1e039227de81a40961b856e295b039d4655e5ae8 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Fri, 24 Jan 2025 09:31:51 +1300 Subject: [PATCH 10/15] descriptor: add test cases for invalid rawtr() inputs Also extend the pk() test cases to cover the new x-only behaviour for non-taproot descriptors. --- src/ctest/test_descriptor.c | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c index 7c7847086..374f432ff 100644 --- a/src/ctest/test_descriptor.c +++ b/src/ctest/test_descriptor.c @@ -42,6 +42,8 @@ static struct wally_map_item g_key_map_items[] = { { B("mainnet_xpub"), B("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL") }, { B("mainnet_xpriv"), B("xprvA2YKGLieCs6cWCiczALiH1jzk3VCCS5M1pGQfWPkamCdR9UpBgE2Gb8AKAyVjKHkz8v37avcfRjdcnP19dVAmZrvZQfvTcXXSAiFNQ6tTtU") }, { B("uncompressed"), B("0414fc03b8df87cd7b872996810db8458d61da8448e531569c8517b469a119d267be5645686309c6e6736dbd93940707cc9143d3cf29f1b877ff340e2cb2d259cf") }, + { B("x_only"), B("b71aa79cab0ae2d83b82d44cbdc23f5dcca3797e8ba622c4e45a8f7dce28ba0e") }, + { B("non_x_only"), B("03b71aa79cab0ae2d83b82d44cbdc23f5dcca3797e8ba622c4e45a8f7dce28ba0e") } }; static const struct wally_map g_key_map = { @@ -897,7 +899,7 @@ static const struct descriptor_test { "d959hk4q" }, /* - * Taproot cases + * Miniscript taproot cases */ { "miniscript - taproot raw pubkey", @@ -1139,6 +1141,14 @@ static const struct descriptor_test { "descriptor - pk - non-key child", "pk(1)", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" + },{ + "descriptor - pk - invalid public key", + "pk(uncompresseduncompressed)", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" + },{ + "descriptor - pk - x-only child", + "pk(x_only)", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" },{ "descriptor - pk - multi-child", "pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)", @@ -1251,6 +1261,26 @@ static const struct descriptor_test { "descriptor - raw - any parent", "pk(raw(000102030405060708090a0b0c0d0e0f))", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" + },{ + "descriptor - empty rawtr", + "rawtr()", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" + },{ + "descriptor - rawtr - multi-child", + "rawtr(x_only,x_only)", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" + },{ + "descriptor - rawtr - any parent", + "pk(rawtr(x_only))", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" + },{ + "descriptor - rawtr - uncompressed key", + "rawtr(uncompressed)", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" + },{ + "descriptor - rawtr - invalid public key", + "rawtr(uncompresseduncompressed)", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" },{ "descriptor - after - non number child", "wsh(after(key_1))", From cc9dd7f4eaea86a7dacb26653213908caf31ae00 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Fri, 24 Jan 2025 10:45:39 +1300 Subject: [PATCH 11/15] descriptor: add script/address test cases for rawtr() --- src/ctest/test_descriptor.c | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c index 374f432ff..636cd6ee9 100644 --- a/src/ctest/test_descriptor.c +++ b/src/ctest/test_descriptor.c @@ -371,6 +371,18 @@ static const struct descriptor_test { WALLY_NETWORK_NONE, 0, 0, 0, NULL, 0, "76a91477b6f27ac523d8b9aa8abcfc94fd536493202ae088ac", "9gv5p2gj" + },{ + "descriptor - rawtr - x-only", + "rawtr(x_only)", + WALLY_NETWORK_BITCOIN_REGTEST, 0, 0, 0, NULL, 0, + "5120b71aa79cab0ae2d83b82d44cbdc23f5dcca3797e8ba622c4e45a8f7dce28ba0e", + "nsnjmrf4" + },{ + "descriptor - rawtr - non-x-only returns the same script as x-only", + "rawtr(non_x_only)", + WALLY_NETWORK_BITCOIN_REGTEST, 0, 0, 0, NULL, 0, + "5120b71aa79cab0ae2d83b82d44cbdc23f5dcca3797e8ba622c4e45a8f7dce28ba0e", + "ha989syu" },{ "descriptor - A single key", "wsh(c:pk_k(key_1))", @@ -1759,6 +1771,28 @@ static const struct address_test { 1, 0, 0, ADDR("mn9rm3FtHUHANae2p5jURy9GXJGDM1ox43") }, + /* + * Taproot + */ + { + "address - rawtr (x-only)", + "rawtr(x_only)", + WALLY_NETWORK_BITCOIN_REGTEST, + 0, 0, 0, + ADDR("bcrt1pkud2089tpt3dswuz63xtms3lthx2x7t73wnz938yt28hmn3ghg8qvxhwkz") + }, { + "address list - rawtr (0-4)", + "rawtr([59d1f3b0/86'/1'/0']tpubDC2Q4xK4XH72Gow34bNSZpx7uPcg1gfu6hPACSSieETYzpWgywMLmi2Yz9STA2Nrif3Yav7jvkzSj8q3nDKjjQrEfRYckUj5jsadYCdCw1C/0/*)#2k4we0vv", + WALLY_NETWORK_BITCOIN_REGTEST, + 0, 0, 0, 5, + { + "bcrt1p7evqsttltmzxd4sjyzhdzs5nj8tmahu0qdpge8t7gp3dsr3hx3dq403ahy", + "bcrt1pa76letfw4eskpz9wap84ha0vcajul7xhhnhktmpp0mhgzlcdr6gswjcq3u", + "bcrt1phmq09p8y6efk06fp5hvf6dkvjsq2vfmph2uuy7y6ya6jvmrfxq3s44cksv", + "bcrt1p9s6clznyy4enaplm2ak9fa0t3d66s5q5m4khpg7w00w9wuerp93q88e9ql", + "bcrt1p220y7newnya8fk04079hcvd3cupx6t8ta659nayh8kaav39f3scsf63y8h", + } + }, /* * Multi-path */ From dc8165eaee3fb1a3d61e9163d891b04b10a7a971 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Fri, 24 Jan 2025 15:28:20 +1300 Subject: [PATCH 12/15] descriptor: add support for tr(k) --- src/descriptor.c | 52 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/descriptor.c b/src/descriptor.c index 02ad58e26..86fb7ffe2 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -87,6 +87,7 @@ #define KIND_DESCRIPTOR_ADDR (0x00040000 | KIND_DESCRIPTOR) #define KIND_DESCRIPTOR_RAW (0x00050000 | KIND_DESCRIPTOR) #define KIND_DESCRIPTOR_RAW_TR (0x00100000 | KIND_DESCRIPTOR) +#define KIND_DESCRIPTOR_TR (0x00200000 | KIND_DESCRIPTOR) /* miniscript */ #define KIND_MINISCRIPT_PK (0x00000100 | KIND_MINISCRIPT) @@ -702,6 +703,18 @@ static int verify_raw_tr(ms_ctx *ctx, ms_node *node) return WALLY_OK; } +static int verify_tr(ms_ctx *ctx, ms_node *node) +{ + const uint32_t child_count = node_get_child_count(node); + if (child_count != 1u) + return WALLY_EINVAL; /* FIXME: Support script paths */ + if (node->child->builtin || !(node->child->kind & KIND_KEY) || + node_has_uncompressed_key(ctx, node)) + return WALLY_EINVAL; + node->type_properties = builtin_get(node)->type_properties; + return WALLY_OK; +} + static int verify_delay(ms_ctx *ctx, ms_node *node) { (void)ctx; @@ -1404,6 +1417,35 @@ static int generate_raw_tr(ms_ctx *ctx, ms_node *node, return ret; } +static int generate_tr(ms_ctx *ctx, ms_node *node, + unsigned char *script, size_t script_len, size_t *written) +{ + unsigned char tweaked[EC_PUBLIC_KEY_LEN]; + unsigned char pubkey[EC_PUBLIC_KEY_UNCOMPRESSED_LEN + 1]; + size_t pubkey_len; + int ret; + + /* Generate a push of the x-only public key of our child */ + const bool force_xonly = true; + ret = generate_pk_k_impl(ctx, node, pubkey, sizeof(pubkey), force_xonly, &pubkey_len); + if (pubkey_len != EC_XONLY_PUBLIC_KEY_LEN + 1) + return WALLY_EINVAL; /* Should be PUSH_32 [x-only pubkey] */ + + /* Tweak it into a compressed pubkey */ + ret = wally_ec_public_key_bip341_tweak(pubkey + 1, pubkey_len - 1, + NULL, 0, 0, /* FIXME: Support script path */ + tweaked, sizeof(tweaked)); + + if (ret == WALLY_OK && script_len >= WALLY_SCRIPTPUBKEY_P2TR_LEN) { + /* Generate the script using the x-only part of the tweaked key */ + script[0] = OP_1; + script[1] = sizeof(tweaked) - 1; + memcpy(script + 2, tweaked + 1, sizeof(tweaked) - 1); + } + *written = WALLY_SCRIPTPUBKEY_P2TR_LEN; + return ret; +} + static int generate_delay(ms_ctx *ctx, ms_node *node, unsigned char *script, size_t script_len, size_t *written) { @@ -1817,6 +1859,11 @@ static const struct ms_builtin_t g_builtins[] = { KIND_DESCRIPTOR_RAW_TR, TYPE_NONE, 1, verify_raw_tr, generate_raw_tr + }, { + I_NAME("tr"), + KIND_DESCRIPTOR_TR, + TYPE_NONE, + 0xffffffff, verify_tr, generate_tr }, /* miniscript */ { @@ -2087,7 +2134,9 @@ static int analyze_pubkey_hex(ms_ctx *ctx, ms_node *node, wally_ec_xonly_public_key_verify(pubkey, pubkey_len) != WALLY_OK) return WALLY_OK; /* Not a valid pubkey */ - make_xonly = node->parent && (node->parent->kind == KIND_DESCRIPTOR_RAW_TR); + make_xonly = node->parent && + (node->parent->kind == KIND_DESCRIPTOR_RAW_TR || + node->parent->kind == KIND_DESCRIPTOR_TR); allow_xonly = make_xonly || flags & WALLY_MINISCRIPT_TAPSCRIPT; if (pubkey_len == EC_PUBLIC_KEY_UNCOMPRESSED_LEN && allow_xonly) return WALLY_OK; /* Uncompressed key not allowed here */ @@ -2502,6 +2551,7 @@ static int node_generation_size(const ms_node *node, size_t *total) /* No-op */ break; case KIND_DESCRIPTOR_RAW_TR: + case KIND_DESCRIPTOR_TR: *total += WALLY_SCRIPTPUBKEY_P2TR_LEN; break; case KIND_MINISCRIPT_PK_K: From 475cb0cd4e1f0917d93be6d37397ad04eb169628 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Fri, 24 Jan 2025 15:42:41 +1300 Subject: [PATCH 13/15] descriptor: add script/addresses/invalid tr(k) test cases --- src/ctest/test_descriptor.c | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c index 636cd6ee9..f87cd7c67 100644 --- a/src/ctest/test_descriptor.c +++ b/src/ctest/test_descriptor.c @@ -383,6 +383,18 @@ static const struct descriptor_test { WALLY_NETWORK_BITCOIN_REGTEST, 0, 0, 0, NULL, 0, "5120b71aa79cab0ae2d83b82d44cbdc23f5dcca3797e8ba622c4e45a8f7dce28ba0e", "ha989syu" + },{ + "descriptor - tr - x-only", + "tr(x_only)", + WALLY_NETWORK_BITCOIN_REGTEST, 0, 0, 0, NULL, 0, + "51205fb8e39dbbdc7c831af59e44a9b2997f9daaf72c3e965b30982f3c731539e1db", + "axny68jy" + },{ + "descriptor - tr - non-x-only returns the same script as x-only", + "tr(non_x_only)", + WALLY_NETWORK_BITCOIN_REGTEST, 0, 0, 0, NULL, 0, + "51205fb8e39dbbdc7c831af59e44a9b2997f9daaf72c3e965b30982f3c731539e1db", + "tp2ky708" },{ "descriptor - A single key", "wsh(c:pk_k(key_1))", @@ -1293,6 +1305,26 @@ static const struct descriptor_test { "descriptor - rawtr - invalid public key", "rawtr(uncompresseduncompressed)", WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" + },{ + "descriptor - empty tr", + "tr()", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" + },{ + "descriptor - tr - multi-child", + "tr(x_only,x_only)", /* FIXME: delete this case when script path is supported */ + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" + },{ + "descriptor - tr - any parent", + "pk(tr(x_only))", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" + },{ + "descriptor - tr - uncompressed key", + "tr(uncompressed)", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" + },{ + "descriptor - tr - invalid public key", + "tr(uncompresseduncompressed)", + WALLY_NETWORK_BITCOIN_MAINNET, 0, 0, 0, NULL, 0, NULL, "" },{ "descriptor - after - non number child", "wsh(after(key_1))", @@ -1792,6 +1824,24 @@ static const struct address_test { "bcrt1p9s6clznyy4enaplm2ak9fa0t3d66s5q5m4khpg7w00w9wuerp93q88e9ql", "bcrt1p220y7newnya8fk04079hcvd3cupx6t8ta659nayh8kaav39f3scsf63y8h", } + }, { + "address - tr (x-only)", + "tr(x_only)", + WALLY_NETWORK_BITCOIN_REGTEST, + 0, 0, 0, + ADDR("bcrt1pt7uw88dmm37gxxh4nez2nv5e07w64aev86t9kvyc9u78x9feu8dsp5hh3s") + }, { + "address list - tr (5-9)", + "tr([59d1f3b0/86'/1'/0']tpubDC2Q4xK4XH72Gow34bNSZpx7uPcg1gfu6hPACSSieETYzpWgywMLmi2Yz9STA2Nrif3Yav7jvkzSj8q3nDKjjQrEfRYckUj5jsadYCdCw1C/0/*)#9wpuzyss", + WALLY_NETWORK_BITCOIN_REGTEST, + 0, 0, 5, 5, + { + "bcrt1p3hf8e9tczepujy3fe66wgq9ez5tllqkschupy08pfzukjtye5wksgtj5k2", + "bcrt1p9wut0rvmq347rldeyktdn9qmrj9qk9ynefykruxrp4yr9u27pzvqfd6ktq", + "bcrt1p3kjmw33umzaylvdmglk489vd4ph2te67ged3c4fmk20adn0pfknsm5jmvu", + "bcrt1pz6t2y0qg69594u60qmnlq23rezxseyf65qk0xl2kt7l02y8rcs8qndm2sm", + "bcrt1p2nunl5trs2hfrlu5fp6kg3hu3tffpzg0w5m56mpmx8m96pl0qxnszgdnaw", + } }, /* * Multi-path From 7fb7b1e55521ff7ac32a8826706545d372d5e957 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Mon, 27 Jan 2025 15:25:15 +1300 Subject: [PATCH 14/15] descriptor: require an extra byte to generate raw expressions raw() with no arguments generates a zero length script. Ensure we tell the caller they need at least one byte of generation space for this case, otherwise they would be passing an invalid (empty) output buffer. --- src/descriptor.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/descriptor.c b/src/descriptor.c index 86fb7ffe2..d5b341bbf 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -2546,8 +2546,12 @@ static int node_generation_size(const ms_node *node, size_t *total) /* max of p2pk, p2pkh, p2wpkh, or p2sh-p2wpkh */ *total += WALLY_SCRIPTPUBKEY_P2PKH_LEN; break; - case KIND_DESCRIPTOR_ADDR: case KIND_DESCRIPTOR_RAW: + /* Add an extra byte to handle 'raw()' which results in nothing, + * as empty output buffers cannot be passed to descriptor calls. + */ + *total += 1; + case KIND_DESCRIPTOR_ADDR: /* No-op */ break; case KIND_DESCRIPTOR_RAW_TR: From 6da0029e0b9dd309e54397cbe81fb16b99d5fa1a Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Mon, 27 Jan 2025 15:33:59 +1300 Subject: [PATCH 15/15] descriptor: add tests for calling with a too-short buffer Use a freshly allocated buffer of the computed size to allocate into, so valgrind can warn us of any errant writes. Also add a testcase for a simple key push without CHECKSIG. --- src/ctest/test_descriptor.c | 54 +++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/src/ctest/test_descriptor.c b/src/ctest/test_descriptor.c index f87cd7c67..812e229fd 100644 --- a/src/ctest/test_descriptor.c +++ b/src/ctest/test_descriptor.c @@ -503,6 +503,12 @@ static const struct descriptor_test { * Miniscript: Randomly generated test set that covers the majority of type and node type combinations */ { + "miniscript - A single key", + "pk_k(key_1)", + WALLY_NETWORK_NONE, 0, 0, 0, NULL, WALLY_MINISCRIPT_ONLY, + "21038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048", + "jyazm5lc" + },{ "miniscript - random 1", "lltvln:after(1231488000)", WALLY_NETWORK_NONE, 0, 0, 0, NULL, WALLY_MINISCRIPT_ONLY, @@ -787,13 +793,13 @@ static const struct descriptor_test { * Miniscript: BOLT examples */ { - "miniscript - A single key", + "miniscript - A single key + CHECKSIG", "c:pk_k(key_1)", WALLY_NETWORK_NONE, 0, 0, 0, NULL, WALLY_MINISCRIPT_ONLY, "21038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048ac", "" }, { - "miniscript - A single key (2)", + "miniscript - A single key + CHECKSIG (2)", "pk(key_1)", WALLY_NETWORK_NONE, 0, 0, 0, NULL, WALLY_MINISCRIPT_ONLY, "21038bc7431d9285a064b0328b6333f3a20b86664437b6de8f4e26e6bbdee258f048ac", @@ -2062,9 +2068,8 @@ static const struct address_test { static bool check_descriptor_to_script(const struct descriptor_test* test) { struct wally_descriptor *descriptor; - size_t written, max_written; - const size_t script_len = 520; - unsigned char *script = malloc(script_len); + size_t written, computed_written, max_written; + const size_t default_script_len = 520; char *checksum, *canonical; int expected_ret, ret, len_ret; uint32_t multi_index = 0; @@ -2094,25 +2099,53 @@ static bool check_descriptor_to_script(const struct descriptor_test* test) if (!check_ret("descriptor_parse", ret, expected_ret)) return false; - if (expected_ret != WALLY_OK) { - free(script); + if (expected_ret != WALLY_OK) return true; - } } + computed_written = default_script_len; + if (expected_ret == WALLY_OK) { + /* Try the call with a too-short buffer. + * This returns a more exact required size for generation, although + * it may still overestimate by a few bytes for some descriptors. + */ + unsigned char *short_script = malloc(1); + ret = wally_descriptor_to_script(descriptor, + test->depth, test->index, + test->variant, multi_index, + child_num, 0, + short_script, 1, &computed_written); + free(short_script); + if (!check_ret("descriptor_to_script(short buffer)\n", ret, expected_ret)) + return false; + } + + const size_t script_len = computed_written ? computed_written : 1; + unsigned char *script = malloc(script_len); ret = wally_descriptor_to_script(descriptor, test->depth, test->index, test->variant, multi_index, child_num, 0, script, script_len, &written); + if (ret == WALLY_OK && written > script_len) { + printf("descriptor_to_script: wrote more than computed length!\n"); + return false; + } if (!check_ret("descriptor_to_script", ret, expected_ret)) return false; + if (expected_ret != WALLY_OK) { + /* Failure case: stop testing here */ wally_descriptor_free(descriptor); free(script); return true; } + if (computed_written < written) { + printf("descriptor_to_script: computed < written\n"); + return false; + } + ret = wally_descriptor_get_features(descriptor, &features); if (!check_ret("descriptor_get_features", ret, WALLY_OK)) return false; @@ -2124,6 +2157,11 @@ static bool check_descriptor_to_script(const struct descriptor_test* test) max_written < written) return false; + if (computed_written > max_written) { + printf("descriptor_to_script: computed > max written\n"); + return false; + } + ret = wally_descriptor_get_checksum(descriptor, 0, &checksum); if (!check_ret("descriptor_get_checksum", ret, WALLY_OK)) return false;