taproot: add bip341_control_block_verify
What changed, and why it matters
This commit adds a new public helper function that checks whether a chunk of taproot-related data (a BIP-341 control block) is well-formed. It is purely a validation/verification addition and does not change existing behavior or fix a bug. There is no indication in the commit that it addresses a security vulnerability.
No security action required. Treat as routine feature/API addition. If auditing, confirm the new verify function is called appropriately by consumers and that errors are not silently ignored.
Security signals we found
New validation API only; no existing logic altered
No mention of vulnerability, CVE, bug fix, or security issue in commit title/message
Bindings added across C++, Java/SWIG, Python, WASM/JS/TypeScript consistently
Evidence from the diff
The change introduces wally_bip341_control_block_verify and language bindings. The C implementation delegates to the existing wally_merkle_path_xonly_public_key_verify after stripping the leading parity byte. It enforces a minimum length and validates the x-only pubkey and optional merkle path elements. No existing code paths are modified; this is an additive API surface.
Changed components
include/wally.hppinclude/wally_map.hsrc/map.csrc/swig_java/swig.isrc/swig_python/contrib/psbt.pysrc/test/util.pysrc/wasm_package/src/functions.jssrc/wasm_package/src/index.d.tstools/wasm_exports.shInspect captured patch +48 / −0
diff --git a/include/wally.hpp b/include/wally.hpp
index 1c60acc..4e699a4 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 <class BYTES>
+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 <class BYTES>
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 0b09f65..27fa91d 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 6eaa02c..332cf0a 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 5ce5d2a..8b310ec 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 9c8dd91..2a12a73 100644
--- a/src/swig_python/contrib/psbt.py
+++ b/src/swig_python/contrib/psbt.py
@@ -224,6 +224,23 @@ class PSBTTests(unittest.TestCase):
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 @@ class PSBTTests(unittest.TestCase):
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 cdca1d9..ddbc5ca 100755
--- a/src/test/util.py
+++ b/src/test/util.py
@@ -310,6 +310,7 @@ for f in (
('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 41ae6e7..5c2ccda 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 de2fed7..b34ed0c 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 6af7044..9d8ab1d 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' \
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.