From 1c482b85641f801d3e5065849219a05f2b69247f Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 13 Nov 2025 21:52:43 +1300 Subject: [PATCH 1/5] build: fix clear sizes and index type --- src/psbt.c | 4 ++-- src/sign.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/psbt.c b/src/psbt.c index d09800010..5f47472c8 100644 --- a/src/psbt.c +++ b/src/psbt.c @@ -1196,7 +1196,7 @@ static int psbt_init(uint32_t version, size_t num_inputs, size_t num_outputs, wally_free(psbt_out->inputs); wally_free(psbt_out->outputs); wally_map_clear(&psbt_out->unknowns); - wally_clear(psbt_out, sizeof(psbt_out)); + wally_clear(psbt_out, sizeof(*psbt_out)); return ret != WALLY_OK ? ret : WALLY_ENOMEM; } @@ -3961,7 +3961,7 @@ static int psbt_build_input(const struct wally_psbt_input *src, BUILD_ITEM(inflation_keys_rangeproof, PSET_IN_ISSUANCE_INFLATION_KEYS_RANGEPROOF); struct wally_map_item issuance_amount_item = { NULL, 0, issuance_amount, sizeof(issuance_amount) }; struct wally_map_item inflation_keys_item = { NULL, 0, inflation_keys, sizeof(inflation_keys) }; - int src_index = src->index; + uint32_t src_index = src->index; if (src->issuance_amount || src->inflation_keys || issuance_amount_commitment || inflation_keys_commitment) src_index |= WALLY_TX_ISSUANCE_FLAG; diff --git a/src/sign.c b/src/sign.c index e94547a78..e803b0906 100644 --- a/src/sign.c +++ b/src/sign.c @@ -359,7 +359,7 @@ int wally_ec_sig_from_bytes_aux(const unsigned char *priv_key, size_t priv_key_l ret = WALLY_EINVAL; else if (!secp256k1_schnorrsig_sign32(ctx, bytes_out, bytes, &keypair, aux_rand)) ret = WALLY_ERROR; - wally_clear(&keypair, sizeof(&keypair)); + wally_clear(&keypair, sizeof(keypair)); return ret; } else { unsigned char extra_entropy[32] = {0}, *entropy_p = (unsigned char *)aux_rand; From 7e483c049b0a4405801f010e60c9f0335d2a617f Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 13 Nov 2025 22:14:34 +1300 Subject: [PATCH 2/5] map_merkle_path_add: ignore duplicates as per the other _add functions --- src/map.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/map.c b/src/map.c index 79703b168..d496c7991 100644 --- a/src/map.c +++ b/src/map.c @@ -727,7 +727,7 @@ int wally_map_merkle_path_add(struct wally_map *map_in, /* Add map for tap leaves */ return map_add(map_in, pub_key, pub_key_len, - merkle_hashes, merkle_hashes_len, false, false); + merkle_hashes, merkle_hashes_len, false, true); } int wally_keypath_get_fingerprint(const unsigned char *val, size_t val_len, From 54eaca220b5713c0f63c55a58f1bc4705fa13492 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 13 Nov 2025 22:21:56 +1300 Subject: [PATCH 3/5] taproot: fix merkle path length check Per BIP-0341 path lengths are limited to 128 or less. --- src/map.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/map.c b/src/map.c index d496c7991..6eaa02cad 100644 --- a/src/map.c +++ b/src/map.c @@ -667,7 +667,9 @@ int wally_merkle_path_xonly_public_key_verify(const unsigned char *key, size_t k if (key_len != EC_XONLY_PUBLIC_KEY_LEN || keypath_key_verify(key, key_len, &extkey) != WALLY_OK || - extkey.version || BYTES_INVALID(val, val_len) || val_len % SHA256_LEN != 0) + extkey.version || BYTES_INVALID(val, val_len)) + return WALLY_EINVAL; + if (val_len && (val_len % SHA256_LEN || val_len % SHA256_LEN > 128u)) return WALLY_EINVAL; return WALLY_OK; } From 7ecf1fa4bfe64cb96bdd3adc3016ac29b3906990 Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 13 Nov 2025 23:45:30 +1300 Subject: [PATCH 4/5] taproot: add bip341_control_block_verify --- include/wally.hpp | 6 ++++++ include/wally_map.h | 10 ++++++++++ src/map.c | 9 +++++++++ src/swig_java/swig.i | 1 + src/swig_python/contrib/psbt.py | 18 ++++++++++++++++++ src/test/util.py | 1 + src/wasm_package/src/functions.js | 1 + src/wasm_package/src/index.d.ts | 1 + tools/wasm_exports.sh | 1 + 9 files changed, 48 insertions(+) diff --git a/include/wally.hpp b/include/wally.hpp index 1c60acc0e..4e699a454 100644 --- a/include/wally.hpp +++ b/include/wally.hpp @@ -511,6 +511,12 @@ inline int bip340_tagged_hash(const BYTES& bytes, const TAG& tag, BYTES_OUT& byt return detail::check_ret(__FUNCTION__, ret); } +template +inline bool bip341_control_block_verify(const BYTES& bytes) { + int ret = ::wally_bip341_control_block_verify(bytes.data(), bytes.size()); + return ret == WALLY_OK; +} + template inline int bzero(BYTES& bytes) { int ret = ::wally_bzero(bytes.data(), bytes.size()); diff --git a/include/wally_map.h b/include/wally_map.h index 0b09f6582..27fa91d14 100644 --- a/include/wally_map.h +++ b/include/wally_map.h @@ -485,6 +485,16 @@ WALLY_CORE_API int wally_merkle_path_xonly_public_key_verify( const unsigned char *val, size_t val_len); +/** + * Verify a taproot control block as specified in BIP-0341. + * + * :param bytes: Control block bytes. + * :param bytes_len: Length of ``bytes`` in bytes. Must be at least `EC_XONLY_PUBLIC_KEY_LEN` + 1. + */ +WALLY_CORE_API int wally_bip341_control_block_verify( + const unsigned char *bytes, + size_t bytes_len); + /** * Allocate and initialize a new BIP32 keypath map. * diff --git a/src/map.c b/src/map.c index 6eaa02cad..332cf0a75 100644 --- a/src/map.c +++ b/src/map.c @@ -674,6 +674,15 @@ int wally_merkle_path_xonly_public_key_verify(const unsigned char *key, size_t k return WALLY_OK; } +int wally_bip341_control_block_verify(const unsigned char *bytes, size_t bytes_len) +{ + const size_t min_len = 1 + EC_XONLY_PUBLIC_KEY_LEN; + if (bytes_len < min_len) + return WALLY_EINVAL; /* Missing parity byte and/or x-only pubkey */ + return wally_merkle_path_xonly_public_key_verify(bytes + 1, EC_XONLY_PUBLIC_KEY_LEN, + bytes_len == min_len ? NULL : bytes + min_len, bytes_len - min_len); +} + int wally_map_keypath_bip32_init_alloc(size_t allocation_len, struct wally_map **output) { return wally_map_init_alloc(allocation_len, wally_keypath_bip32_verify, output); diff --git a/src/swig_java/swig.i b/src/swig_java/swig.i index 5ce5d2ae7..8b310ecbd 100644 --- a/src/swig_java/swig.i +++ b/src/swig_java/swig.i @@ -568,6 +568,7 @@ static jobjectArray create_jstringArray(JNIEnv *jenv, char **p, size_t len) { %returns_string(wally_bip32_key_to_address); %returns_string(wally_bip32_key_to_addr_segwit); %returns_array_(wally_bip340_tagged_hash, 4, 5, SHA256_LEN); +%returns_void__(wally_bip341_control_block_verify) %returns_size_t(wally_coinselect_assets); %returns_string(wally_confidential_addr_to_addr); %returns_array_(wally_confidential_addr_to_ec_public_key, 3, 4, EC_PUBLIC_KEY_LEN); diff --git a/src/swig_python/contrib/psbt.py b/src/swig_python/contrib/psbt.py index 9c8dd9155..2a12a7315 100644 --- a/src/swig_python/contrib/psbt.py +++ b/src/swig_python/contrib/psbt.py @@ -224,6 +224,23 @@ def check_keypath(self, keypaths, master, derived, pubkey, fingerprint, path): key = map_keypath_get_bip32_key_from(keypaths, 0, master) self.assertEqual(bip32_key_serialize(key, 0), bip32_key_serialize(derived, 0)) + def check_taproot_keypath(self): + # TODO: add in-situ checks on the PSBT fields + # BIP-0341 control block + parity = hex_to_bytes('55') + xonly = hex_to_bytes('22' * 32) + bad_xonly = hex_to_bytes('04' + '22' * 32) + path_elem = hex_to_bytes('00' * 32) + bip341_control_block_verify(parity + xonly) # No path, OK + bip341_control_block_verify(parity + xonly + path_elem) # 1 path element, OK + for args in [ + None, # Null control block + parity + bad_xonly + path_elem, # Bad x-only pubkey + parity + bad_xonly + path_elem[:-1], # Path length not modulo 32 + parity + bad_xonly + path_elem * 129, # Path length too long + ]: + self.assertRaises(ValueError, lambda: bip341_control_block_verify(args)) + def check_txout(self, lhs, rhs): self.assertEqual(tx_output_get_satoshi(lhs), tx_output_get_satoshi(rhs)) self.assertEqual(tx_output_get_script(lhs), tx_output_get_script(rhs)) @@ -350,6 +367,7 @@ def test_psbt(self): map_keypath_add(dummy_keypaths, dummy_pubkey, dummy_fingerprint, dummy_path) self.check_keypath(dummy_keypaths, master, derived, dummy_pubkey, dummy_fingerprint, dummy_path) + self.check_taproot_keypath() empty_signatures = map_init(0, None) dummy_signatures = map_init(0, None) # TODO: pubkey to sig map init diff --git a/src/test/util.py b/src/test/util.py index cdca1d9c4..ddbc5ca2d 100755 --- a/src/test/util.py +++ b/src/test/util.py @@ -310,6 +310,7 @@ class wally_psbt(Structure): ('wally_bip32_key_to_addr_segwit', c_int, [POINTER(ext_key), c_char_p, c_uint32, c_char_p_p]), ('wally_bip32_key_to_address', c_int, [POINTER(ext_key), c_uint32, c_uint32, c_char_p_p]), ('wally_bip340_tagged_hash', c_int, [c_void_p, c_size_t, c_char_p, c_void_p, c_size_t]), + ('wally_bip341_control_block_verify', c_int, [c_void_p, c_size_t]), ('wally_bzero', c_int, [c_void_p, c_size_t]), ('wally_cleanup', c_int, [c_uint32]), ('wally_coinselect_assets', c_int, [POINTER(c_uint64), c_size_t, c_uint64, c_uint64, c_uint32, POINTER(c_uint32), c_size_t, c_size_t_p]), diff --git a/src/wasm_package/src/functions.js b/src/wasm_package/src/functions.js index 41ae6e7e3..5c2ccda0e 100644 --- a/src/wasm_package/src/functions.js +++ b/src/wasm_package/src/functions.js @@ -141,6 +141,7 @@ export const bip32_path_from_str_n_len = wrap('bip32_path_from_str_n_len', [T.St export const bip32_path_str_get_features = wrap('bip32_path_str_get_features', [T.String, T.DestPtr(T.Int32)]); export const bip32_path_str_n_get_features = wrap('bip32_path_str_n_get_features', [T.String, T.Int32, T.DestPtr(T.Int32)]); export const bip340_tagged_hash = wrap('wally_bip340_tagged_hash', [T.Bytes, T.String, T.DestPtrSized(T.Bytes, C.SHA256_LEN)]); +export const bip341_control_block_verify = wrap('wally_bip341_control_block_verify', [T.Bytes]); export const bip38_get_flags = wrap('bip38_get_flags', [T.String, T.DestPtr(T.Int32)]); export const bip38_raw_get_flags = wrap('bip38_raw_get_flags', [T.Bytes, T.DestPtr(T.Int32)]); export const bip39_get_languages = wrap('bip39_get_languages', [T.DestPtrPtr(T.String)]); diff --git a/src/wasm_package/src/index.d.ts b/src/wasm_package/src/index.d.ts index de2fed74c..b34ed0c7a 100644 --- a/src/wasm_package/src/index.d.ts +++ b/src/wasm_package/src/index.d.ts @@ -101,6 +101,7 @@ export function bip32_path_from_str_n_len(path_str: string, path_str_len: number export function bip32_path_str_get_features(path_str: string): number; export function bip32_path_str_n_get_features(path_str: string, path_str_len: number): number; export function bip340_tagged_hash(bytes: Buffer|Uint8Array, tag: string): Buffer; +export function bip341_control_block_verify(bytes: Buffer|Uint8Array): void; export function bip38_get_flags(bip38: string): number; export function bip38_raw_get_flags(bytes: Buffer|Uint8Array): number; export function bip39_get_languages(): string; diff --git a/tools/wasm_exports.sh b/tools/wasm_exports.sh index 6af704488..9d8ab1d17 100644 --- a/tools/wasm_exports.sh +++ b/tools/wasm_exports.sh @@ -83,6 +83,7 @@ EXPORTED_FUNCTIONS="['_malloc','_free','_bip32_key_free' \ ,'_wally_bip32_key_to_addr_segwit' \ ,'_wally_bip32_key_to_address' \ ,'_wally_bip340_tagged_hash' \ +,'_wally_bip341_control_block_verify' \ ,'_wally_bzero' \ ,'_wally_cleanup' \ ,'_wally_descriptor_canonicalize' \ From 944f26608f70cb4ddc89c0d0261543498737e50e Mon Sep 17 00:00:00 2001 From: Jon Griffiths Date: Thu, 13 Nov 2025 23:53:56 +1300 Subject: [PATCH 5/5] psbt: use control block verification call internally --- src/psbt.c | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/psbt.c b/src/psbt.c index 5f47472c8..74b46a549 100644 --- a/src/psbt.c +++ b/src/psbt.c @@ -2217,22 +2217,15 @@ static int pull_taproot_leaf_signature(const unsigned char **cursor, size_t *max return map_add(leaf_sigs, xonly_hash, 64u, val, val_len, false, false); } -static bool is_valid_control_block_len(size_t ctrl_len) -{ - return ctrl_len >= 33u && ctrl_len <= 33u + 128u * 32u && - ((ctrl_len - 33u) % 32u) == 0; -} - static int pull_taproot_leaf_script(const unsigned char **cursor, size_t *max, const unsigned char **key, size_t *key_len, struct wally_map *leaf_scripts) { - /* TODO: use taproot constants here */ const unsigned char *ctrl, *val; size_t ctrl_len = *key_len, val_len; ctrl = pull_skip(key, key_len, ctrl_len); - if (!ctrl || !is_valid_control_block_len(ctrl_len)) + if (wally_bip341_control_block_verify(ctrl, ctrl_len) != WALLY_OK) return WALLY_EINVAL; subfield_nomore_end(cursor, max, *key, *key_len); @@ -2987,7 +2980,8 @@ static int push_taproot_leaf_scripts(unsigned char **cursor, size_t *max, size_t for (i = 0; i < leaf_scripts->num_items; ++i) { const struct wally_map_item *item = leaf_scripts->items + i; - if (!is_valid_control_block_len(item->key_len) || !item->value_len) + if (wally_bip341_control_block_verify(item->key, item->key_len) != WALLY_OK || + !item->value_len) return WALLY_EINVAL; push_key(cursor, max, ft, false, item->key, item->key_len);