What changed, and why it matters
This commit only adds new unit tests and test-support code for Bitcoin address generation. It does not change the app that runs on the Ledger device, nor does it fix or introduce any security-relevant behavior. It is a testing/infrastructure change with no direct security impact.
No security action required. Treat as normal test-coverage improvement. If reviewing the broader PR, verify that the vendored tomlc17 parser is an acceptable dependency and that its MIT license is compatible with the project.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces a new data-driven unit-test suite (test_get_wallet_address.c) plus a TOML test-vector file, a vendored TOML parser build rule, and additional speculos bridge stubs (HMAC-SHA256, modular arithmetic, secure memcmp) so that more of the wallet/address code can be exercised in the unit-test build. The production source files are only referenced for compilation; no functional modifications are made to device code. The vectors cover singlesig and multisig descriptors across P2PKH, P2WPKH, P2SH-P2WPKH, P2TR, and MuSig variants.
Changed components
unit-tests/CMakeLists.txtunit-tests/libs/speculos_bridge.cunit-tests/test_get_wallet_address.cunit-tests/test_get_wallet_address_vectors.tomlInspect captured patch +631 / −0
diff --git a/unit-tests/CMakeLists.txt b/unit-tests/CMakeLists.txt
index 4986dc8..bac23dd 100644
--- a/unit-tests/CMakeLists.txt
+++ b/unit-tests/CMakeLists.txt
@@ -108,6 +108,14 @@ add_library(mock_dispatcher SHARED libs/mock_dispatcher.c)
add_library(psbt_parse SHARED libs/psbt_parse.c)
add_library(xpub SHARED libs/xpub.c)
+# Vendored TOML parser (cktan/tomlc17, MIT). Used by the data-driven
+# unit tests that load test vectors from a .toml file.
+add_library(tomlc17 STATIC libs/tomlc17.c)
+target_include_directories(tomlc17 PUBLIC libs)
+# tomlc17 is a third-party source we don't intend to maintain locally —
+# silence warnings on it without affecting the rest of the tree.
+target_compile_options(tomlc17 PRIVATE -w)
+
# App's libraries
add_library(base58 SHARED $ENV{BOLOS_SDK}/lib_standard_app/base58.c)
add_library(bip32 SHARED $ENV{BOLOS_SDK}/lib_standard_app/bip32.c)
@@ -278,6 +286,15 @@ if(SPECULOS AND SPECULOS_SRC)
../src/common/wallet.c
../src/common/script.c
../src/common/segwit_addr.c
+ ../src/common/merkle.c
+ ../src/handler/lib/policy.c
+ ../src/handler/lib/get_merkle_leaf_element.c
+ ../src/handler/lib/get_merkle_leaf_hash.c
+ ../src/handler/lib/get_merkle_preimage.c
+ ../src/handler/lib/stream_preimage.c
+ ../src/handler/lib/get_preimage.c
+ ../src/handler/sign_psbt/sign_psbt_cache.c
+ ../src/musig/musig.c
)
target_compile_options(app_crypto PRIVATE -fno-stack-protector)
target_compile_definitions(app_crypto PRIVATE
@@ -296,6 +313,10 @@ if(SPECULOS AND SPECULOS_SRC)
../src/common
../src/debug-helpers
../src/boilerplate
+ ../src/handler
+ ../src/handler/lib
+ ../src/handler/sign_psbt
+ ../src/musig
$ENV{BOLOS_SDK}/target/nanox/include
$ENV{BOLOS_SDK}/include
$ENV{BOLOS_SDK}/io/include
@@ -325,4 +346,19 @@ if(SPECULOS AND SPECULOS_SRC)
add_executable(test_wallet test_wallet.c)
target_link_libraries(test_wallet PRIVATE cmocka gcov app_crypto buffer buffer_ext)
add_test(test_wallet test_wallet)
+
+ # test_get_wallet_address exercises get_wallet_script + get_script_address
+ # end-to-end against a mock dispatcher; vectors come from a separate
+ # text file (see test_get_wallet_address_vectors.txt for the format).
+ add_executable(test_get_wallet_address test_get_wallet_address.c)
+ target_include_directories(test_get_wallet_address PRIVATE
+ ../src/common ../src/handler ../src/handler/lib ../src/handler/sign_psbt
+ )
+ target_compile_definitions(test_get_wallet_address PRIVATE
+ TEST_VECTORS_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/test_get_wallet_address_vectors.toml\"
+ )
+ target_link_libraries(test_get_wallet_address PRIVATE
+ cmocka gcov app_crypto mock_dispatcher buffer buffer_ext sha256 tomlc17
+ )
+ add_test(test_get_wallet_address test_get_wallet_address)
endif()
diff --git a/unit-tests/libs/speculos_bridge.c b/unit-tests/libs/speculos_bridge.c
index 500337e..91672d6 100644
--- a/unit-tests/libs/speculos_bridge.c
+++ b/unit-tests/libs/speculos_bridge.c
@@ -166,6 +166,28 @@ end:
return error;
}
+/* HMAC-SHA256 is implemented directly by speculos under a spec_ prefix. */
+extern int spec_cx_hmac_sha256(const unsigned char *key,
+ unsigned int key_len,
+ const unsigned char *in,
+ unsigned int len,
+ unsigned char *out,
+ unsigned int out_len);
+
+size_t cx_hmac_sha256(const uint8_t *key,
+ size_t key_len,
+ const uint8_t *in,
+ size_t len,
+ uint8_t *out,
+ size_t out_len) {
+ return (size_t) spec_cx_hmac_sha256(key,
+ (unsigned int) key_len,
+ in,
+ (unsigned int) len,
+ out,
+ (unsigned int) out_len);
+}
+
/* HMAC-SHA512 is implemented directly by speculos under a spec_ prefix. */
extern int spec_cx_hmac_sha512(const unsigned char *key,
unsigned int key_len,
@@ -324,6 +346,48 @@ end:
return error;
}
+extern int sys_cx_math_multm(uint8_t *r,
+ const uint8_t *a,
+ const uint8_t *b,
+ const uint8_t *m,
+ unsigned int len);
+
+cx_err_t cx_math_multm_no_throw(uint8_t *r,
+ const uint8_t *a,
+ const uint8_t *b,
+ const uint8_t *m,
+ size_t len) {
+ /* sys_cx_math_multm has a useless return value (a leftover marker
+ * value, see the speculos source). Treat any path through it as
+ * success. */
+ (void) sys_cx_math_multm(r, a, b, m, (unsigned int) len);
+ return 0; /* CX_OK */
+}
+
+extern int sys_cx_math_modm(uint8_t *v,
+ unsigned int len_v,
+ const uint8_t *m,
+ unsigned int len_m);
+
+cx_err_t cx_math_modm_no_throw(uint8_t *v, size_t len_v, const uint8_t *m, size_t len_m) {
+ (void) sys_cx_math_modm(v, (unsigned int) len_v, m, (unsigned int) len_m);
+ return 0; /* CX_OK */
+}
+
+/* Constant-time memcmp, matching the SDK char convention (0 on equal,
+ * nonzero otherwise). XOR-accumulate every byte pair so the loop's data
+ * access pattern and branch behavior don't depend on where the first
+ * mismatch occurs. */
+char os_secure_memcmp(const void *src1, const void *src2, size_t length) {
+ const uint8_t *a = (const uint8_t *) src1;
+ const uint8_t *b = (const uint8_t *) src2;
+ uint8_t diff = 0;
+ for (size_t i = 0; i < length; i++) {
+ diff |= a[i] ^ b[i];
+ }
+ return diff == 0 ? 0 : 1;
+}
+
cx_err_t cx_math_sub_no_throw(uint8_t *r, const uint8_t *a, const uint8_t *b, size_t len) {
cx_bn_t bn_r, bn_a, bn_b;
cx_err_t error;
@@ -663,6 +727,12 @@ void *pic(void *link_address) {
return link_address;
}
+/* mock_dispatcher.c resets cx_hash_mock's pool counter on every init. When
+ * the dispatcher is wired to the speculos-backed cx_ primitives instead,
+ * this global is never actually consulted — but it still has to be defined
+ * for the linker. */
+int g_sha256_pool_next = 0;
+
void speculos_bridge_init(void) {
/* Deterministic OpenSSL RNG seed for reproducible test runs. */
static const uint8_t seed[32] = {
diff --git a/unit-tests/test_get_wallet_address.c b/unit-tests/test_get_wallet_address.c
new file mode 100644
index 0000000..a90414a
--- /dev/null
+++ b/unit-tests/test_get_wallet_address.c
@@ -0,0 +1,299 @@
+/**
+ * Unit tests for get_wallet_script + get_script_address.
+ *
+ * Test cases are loaded from TEST_VECTORS_PATH (a TOML file) at runtime;
+ * see the file's header comment for the schema. For each vector we:
+ * 1. Register the keys-info list with the mock dispatcher (this also
+ * builds the keys Merkle tree and gives us its root).
+ * 2. Parse the descriptor template into the policy AST.
+ * 3. Call get_wallet_script to compute the scriptPubKey.
+ * 4. Call get_script_address on the resulting script.
+ * 5. Compare with the expected address string.
+ */
+#include <limits.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <setjmp.h>
+
+#include <cmocka.h>
+
+#include "mock_dispatcher.h"
+#include "tomlc17.h"
+
+#include "common/wallet.h"
+#include "handler/lib/policy.h"
+#include "common/script.h"
+#include "constants.h"
+
+#ifndef TEST_VECTORS_PATH
+#error "TEST_VECTORS_PATH must be defined to point at the vectors file"
+#endif
+
+#define MAX_KEYS_PER_CASE 8
+#define MAX_KEY_INFO_LEN (MAX_POLICY_KEY_INFO_LEN + 1)
+#define MAX_TPL_LEN (MAX_DESCRIPTOR_TEMPLATE_LENGTH_V2 + 1)
+#define MAX_ADDR_LEN (MAX_ADDRESS_LENGTH_STR + 1)
+#define MAX_NAME_LEN 64
+
+typedef struct {
+ char name[MAX_NAME_LEN];
+ char descriptor_template[MAX_TPL_LEN];
+ char keys_info[MAX_KEYS_PER_CASE][MAX_KEY_INFO_LEN];
+ size_t n_keys;
+ unsigned int change;
+ unsigned int address_index;
+ char expected_address[MAX_ADDR_LEN];
+} testcase_t;
+
+static testcase_t *g_cases = NULL;
+static size_t g_n_cases = 0;
+
+/* ===========================================================================
+ * TOML loader. Copies the relevant fields out of the parsed document into
+ * g_cases, so the document lifetime doesn't need to outlive parsing.
+ * =========================================================================== */
+
+/* The TOML loader runs from main() before cmocka_run_group_tests_name(),
+ * so there is no active test (and no setjmp buffer) for cmocka's fail()
+ * / assert_* macros to unwind to. On malformed input we abort() the
+ * process instead, which gives a deterministic, non-zero exit. */
+
+/* Copy a required string field from a TOML table into a fixed-size buffer.
+ * Aborts the process if the field is missing or doesn't fit. */
+static void copy_required_string(toml_datum_t table,
+ const char *key,
+ char *dst,
+ size_t dst_size) {
+ toml_datum_t d = toml_get(table, key);
+ if (d.type != TOML_STRING) {
+ fprintf(stderr, "missing or non-string field: %s\n", key);
+ abort();
+ }
+ if ((size_t) d.u.str.len >= dst_size) {
+ fprintf(stderr, "field %s too long (%d >= %zu)\n", key, d.u.str.len, dst_size);
+ abort();
+ }
+ memcpy(dst, d.u.str.ptr, (size_t) d.u.str.len);
+ dst[d.u.str.len] = '\0';
+}
+
+/* Copy a required non-negative integer field. */
+static unsigned int copy_required_uint(toml_datum_t table, const char *key) {
+ toml_datum_t d = toml_get(table, key);
+ if (d.type != TOML_INT64) {
+ fprintf(stderr, "missing or non-integer field: %s\n", key);
+ abort();
+ }
+ if (d.u.int64 < 0) {
+ fprintf(stderr, "negative integer in field: %s\n", key);
+ abort();
+ }
+ if (d.u.int64 > UINT_MAX) {
+ fprintf(stderr, "integer in field %s too large\n", key);
+ abort();
+ }
+ return (unsigned int) d.u.int64;
+}
+
+static void parse_vectors(const char *path) {
+ toml_result_t r = toml_parse_file_ex(path);
+ if (!r.ok) {
+ fprintf(stderr, "failed to parse %s: %s\n", path, r.errmsg);
+ toml_free(r);
+ abort();
+ }
+
+ toml_datum_t cases = toml_get(r.toptab, "case");
+ if (cases.type != TOML_ARRAY) {
+ fprintf(stderr, "vectors file is missing the [[case]] array\n");
+ toml_free(r);
+ abort();
+ }
+
+ if (cases.u.arr.size <= 0) {
+ fprintf(stderr, "vectors file has no [[case]] entries\n");
+ abort();
+ }
+
+ g_n_cases = (size_t) cases.u.arr.size;
+ g_cases = calloc(g_n_cases, sizeof(testcase_t));
+ if (g_cases == NULL) {
+ fprintf(stderr, "out of memory allocating %zu test cases\n", g_n_cases);
+ abort();
+ }
+
+ for (size_t i = 0; i < g_n_cases; i++) {
+ toml_datum_t tc_node = cases.u.arr.elem[i];
+ if (tc_node.type != TOML_TABLE) {
+ fprintf(stderr, "case %zu: not a TOML table\n", i);
+ abort();
+ }
+
+ testcase_t *cur = &g_cases[i];
+
+ copy_required_string(tc_node, "name", cur->name, sizeof(cur->name));
+ copy_required_string(tc_node,
+ "descriptor_template",
+ cur->descriptor_template,
+ sizeof(cur->descriptor_template));
+ copy_required_string(tc_node,
+ "expected_address",
+ cur->expected_address,
+ sizeof(cur->expected_address));
+ cur->change = copy_required_uint(tc_node, "change");
+ cur->address_index = copy_required_uint(tc_node, "address_index");
+
+ toml_datum_t keys = toml_get(tc_node, "keys_info");
+ if (keys.type != TOML_ARRAY) {
+ fprintf(stderr, "%s: keys_info must be an array\n", cur->name);
+ abort();
+ }
+ if (keys.u.arr.size <= 0 || (size_t) keys.u.arr.size > MAX_KEYS_PER_CASE) {
+ fprintf(stderr,
+ "%s: keys_info has %d entries (must be 1..%d)\n",
+ cur->name,
+ keys.u.arr.size,
+ MAX_KEYS_PER_CASE);
+ abort();
+ }
+ cur->n_keys = (size_t) keys.u.arr.size;
+
+ for (size_t k = 0; k < cur->n_keys; k++) {
+ toml_datum_t key_node = keys.u.arr.elem[k];
+ if (key_node.type != TOML_STRING) {
+ fprintf(stderr, "%s: keys_info[%zu] is not a string\n", cur->name, k);
+ abort();
+ }
+ if ((size_t) key_node.u.str.len >= MAX_KEY_INFO_LEN) {
+ fprintf(stderr,
+ "%s: keys_info[%zu] too long (%d >= %d)\n",
+ cur->name,
+ k,
+ key_node.u.str.len,
+ MAX_KEY_INFO_LEN);
+ abort();
+ }
+ memcpy(cur->keys_info[k], key_node.u.str.ptr, (size_t) key_node.u.str.len);
+ cur->keys_info[k][key_node.u.str.len] = '\0';
+ }
+ }
+
+ toml_free(r);
+}
+
+/* ===========================================================================
+ * Test driver
+ * =========================================================================== */
+
+/* Per-case cmocka state: pairs a heap-allocated mock dispatcher with the
+ * testcase we're running. The mock pointer is owned by case_setup/teardown,
+ * which wrap mock_dispatcher_setup/teardown so the global active-mock
+ * pointer is cleared after each case. */
+typedef struct {
+ void *mock_state;
+ const testcase_t *tc;
+} case_state_t;
+
+static int case_setup(void **state) {
+ const testcase_t *tc = (const testcase_t *) *state;
+ case_state_t *cs = calloc(1, sizeof(case_state_t));
+ if (cs == NULL) {
+ return -1;
+ }
+ if (mock_dispatcher_setup(&cs->mock_state) != 0) {
+ free(cs);
+ return -1;
+ }
+ cs->tc = tc;
+ *state = cs;
+ return 0;
+}
+
+static int case_teardown(void **state) {
+ case_state_t *cs = *state;
+ if (cs != NULL) {
+ mock_dispatcher_teardown(&cs->mock_state);
+ free(cs);
+ *state = NULL;
+ }
+ return 0;
+}
+
+/**
+ * Runs a single test case: parses the descriptor template, registers the
+ * keys with the mock dispatcher, derives the script and the address, then
+ * compares the produced address to the expected one.
+ */
+static void test_one_case(void **state) {
+ case_state_t *cs = *state;
+ mock_dispatcher_t *mock = cs->mock_state;
+ const testcase_t *tc = cs->tc;
+
+ const uint8_t *key_ptrs[MAX_KEYS_PER_CASE];
+ size_t key_lens[MAX_KEYS_PER_CASE];
+ for (size_t i = 0; i < tc->n_keys; i++) {
+ key_ptrs[i] = (const uint8_t *) tc->keys_info[i];
+ key_lens[i] = strlen(tc->keys_info[i]);
+ }
+ mock_dispatcher_add_list(mock, key_ptrs, key_lens, tc->n_keys);
+
+ uint8_t keys_merkle_root[32];
+ memcpy(keys_merkle_root, mock->trees[mock->n_trees - 1].root, 32);
+
+ uint8_t policy_buf[MAX_WALLET_POLICY_BYTES];
+ buffer_t tpl_buf = buffer_create((void *) tc->descriptor_template,
+ strlen(tc->descriptor_template));
+ int parse_res = parse_descriptor_template(&tpl_buf,
+ policy_buf,
+ sizeof(policy_buf),
+ WALLET_POLICY_VERSION_V2);
+ assert_true(parse_res >= 0);
+
+ wallet_derivation_info_t wdi = {
+ .wallet_version = WALLET_POLICY_VERSION_V2,
+ .keys_merkle_root = keys_merkle_root,
+ .n_keys = (uint32_t) tc->n_keys,
+ .change = tc->change != 0,
+ .address_index = tc->address_index,
+ .sign_psbt_cache = NULL,
+ };
+
+ uint8_t script[MAX_PREVOUT_SCRIPTPUBKEY_LEN];
+ int script_len = get_wallet_script(mock_dispatcher_get_dc(mock),
+ (const policy_node_t *) policy_buf,
+ &wdi,
+ script);
+ assert_true(script_len > 0);
+
+ char address[MAX_ADDR_LEN];
+ int addr_len = get_script_address(script, (size_t) script_len, address, sizeof(address));
+ assert_true(addr_len > 0);
+
+ assert_string_equal(address, tc->expected_address);
+}
+
+int main(void) {
+ parse_vectors(TEST_VECTORS_PATH);
+
+ /* VLA so that the cmocka_run_group_tests_name macro's sizeof-based
+ * count computes correctly; a calloc'd pointer would not. */
+ struct CMUnitTest tests[g_n_cases];
+ for (size_t i = 0; i < g_n_cases; i++) {
+ tests[i] = (struct CMUnitTest){
+ .name = g_cases[i].name,
+ .test_func = test_one_case,
+ .setup_func = case_setup,
+ .teardown_func = case_teardown,
+ .initial_state = &g_cases[i],
+ };
+ }
+
+ int rc = cmocka_run_group_tests_name("get_wallet_address", tests, NULL, NULL);
+ free(g_cases);
+ return rc;
+}
diff --git a/unit-tests/test_get_wallet_address_vectors.toml b/unit-tests/test_get_wallet_address_vectors.toml
new file mode 100644
index 0000000..28b31d1
--- /dev/null
+++ b/unit-tests/test_get_wallet_address_vectors.toml
@@ -0,0 +1,226 @@
+# Test vectors for get_wallet_script + get_script_address.
+#
+# Each test case is a `[[case]]` entry with the following keys:
+# name - human-readable label
+# descriptor_template - the V2 descriptor template
+# keys_info - list of key info strings, in the order referenced
+# by @0, @1, ...
+# change - 0 (receive) or 1 (change)
+# address_index - non-negative integer
+# expected_address - the address the device must produce
+#
+# Vectors below are taken from tests/test_get_wallet_address.py.
+# All segwit addresses use the "tb" HRP (testnet); base58 versions are
+# COIN_P2PKH_VERSION=111 / COIN_P2SH_VERSION=196 (testnet/regtest).
+
+
+# ---- Singlesig P2PKH (BIP-44) ----
+
+[[case]]
+name = "singlesig_legacy_recv_0"
+descriptor_template = "pkh(@0/**)"
+keys_info = [
+ "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT",
+]
+change = 0
+address_index = 0
+expected_address = "mz5vLWdM1wHVGSmXUkhKVvZbJ2g4epMXSm"
+
+[[case]]
+name = "singlesig_legacy_change_15"
+descriptor_template = "pkh(@0/**)"
+keys_info = [
+ "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT",
+]
+change = 1
+address_index = 15
+expected_address = "myFCUBRCKFjV7292HnZtiHqMzzHrApobpT"
+
+
+# ---- Singlesig P2WPKH (BIP-84) ----
+
+[[case]]
+name = "singlesig_wit_recv_0"
+descriptor_template = "wpkh(@0/**)"
+keys_info = [
+ "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P",
+]
+change = 0
+address_index = 0
+expected_address = "tb1qzdr7s2sr0dwmkwx033r4nujzk86u0cy6fmzfjk"
+
+[[case]]
+name = "singlesig_wit_change_15"
+descriptor_template = "wpkh(@0/**)"
+keys_info = [
+ "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P",
+]
+change = 1
+address_index = 15
+expected_address = "tb1qlrvzyx8jcjfj2xuy69du9trtxnsvjuped7e289"
+
+
+# ---- Singlesig P2SH-P2WPKH (BIP-49) ----
+
+[[case]]
+name = "singlesig_sh_wit_recv_0"
+descriptor_template = "sh(wpkh(@0/**))"
+keys_info = [
+ "[f5acc2fd/49'/1'/0']tpubDC871vGLAiKPcwAw22EjhKVLk5L98UGXBEcGR8gpcigLQVDDfgcYW24QBEyTHTSFEjgJgbaHU8CdRi9vmG4cPm1kPLmZhJEP17FMBdNheh3",
+]
+change = 0
+address_index = 0
+expected_address = "2MyHkbusvLomaarGYMqyq7q9pSBYJRwWcsw"
+
+[[case]]
+name = "singlesig_sh_wit_change_15"
+descriptor_template = "sh(wpkh(@0/**))"
+keys_info = [
+ "[f5acc2fd/49'/1'/0']tpubDC871vGLAiKPcwAw22EjhKVLk5L98UGXBEcGR8gpcigLQVDDfgcYW24QBEyTHTSFEjgJgbaHU8CdRi9vmG4cPm1kPLmZhJEP17FMBdNheh3",
+]
+change = 1
+address_index = 15
+expected_address = "2NAbM4FSeBQG4o85kbXw2YNfKypcnEZS9MR"
+
+
+# ---- Singlesig P2TR (BIP-86) ----
+
+[[case]]
+name = "singlesig_taproot_recv_0"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+change = 0
+address_index = 0
+expected_address = "tb1pws8wvnj99ca6acf8kq7pjk7vyxknah0d9mexckh5s0vu2ccy68js9am6u7"
+
+[[case]]
+name = "singlesig_taproot_recv_9"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+change = 0
+address_index = 9
+expected_address = "tb1psl7eyk2jyjzq6evqvan854fts7a5j65rth25yqahkd2a765yvj0qggs5ne"
+
+[[case]]
+name = "singlesig_taproot_change_0"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+change = 1
+address_index = 0
+expected_address = "tb1pmr60r5vfjmdkrwcu4a2z8h39mzs7a6wf2rfhuml6qgcp940x9cxs7t9pdy"
+
+[[case]]
+name = "singlesig_taproot_change_9"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+change = 1
+address_index = 9
+expected_address = "tb1p98d6s9jkf0la8ras4nnm72zme5r03fexn29e3pgz4qksdy84ndpqgjak72"
+
+
+# ---- 2-of-2 sortedmulti, legacy P2SH ----
+
+[[case]]
+name = "multisig_legacy_2of2"
+descriptor_template = "sh(sortedmulti(2,@0/**,@1/**))"
+keys_info = [
+ "[5c9e228d/48'/1'/0'/0']tpubDEGquuorgFNb8bjh5kNZQMPtABJzoWwNm78FUmeoPkfRtoPF7JLrtoZeT3J3ybq1HmC3Rn1Q8wFQ8J5usanzups5rj7PJoQLNyvq8QbJruW",
+ "[f5acc2fd/48'/1'/0'/0']tpubDFAqEGNyad35WQAZMmPD4vgBXnjH16RGciLdWekPe4f4d5JzoHVu1PS86Sy4Tm63vDf8rfV3UjifhrRuSUDfiZj5KPffTPyZ4ZXBKvjD8jm",
+]
+change = 0
+address_index = 0
+expected_address = "2Mx69MjHC4ViZAH1koVXPvVgaazbBCdr89j"
+
+
+# ---- 2-of-2 sortedmulti, P2SH-P2WSH ----
+
+[[case]]
+name = "multisig_sh_wit_2of2"
+descriptor_template = "sh(wsh(sortedmulti(2,@0/**,@1/**)))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/1']tpubDE7NQymr4AFtcJXi9TaWZtrhAdy8QyKmT4U6b9qYByAxCzoyMJ8zw5d8xVLVpbTRAEqP8pVUxjLE2vDt1rSFjaiS8DSz1QcNZ8D1qxUMx1g",
+ "[f5acc2fd/48'/1'/0'/1']tpubDFAqEGNyad35YgH8zxvxFZqNUoPtr5mDojs7wzbXQBHTZ4xHeVXG6w2HvsKvjBpaRpTmjYDjdPg5w2c6Wvu8QBkyMDrmBWdCyqkDM7reSsY",
+]
+change = 0
+address_index = 0
+expected_address = "2MxAUTJh27foYtyp9dcSxP7RgaSwkkVCHTU"
+
+
+# ---- 2-of-2 sortedmulti, native P2WSH ----
+
+[[case]]
+name = "multisig_wit_2of2"
+descriptor_template = "wsh(sortedmulti(2,@0/**,@1/**))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+change = 0
+address_index = 0
+expected_address = "tb1qmyauyzn08cduzdqweexgna2spwd0rndj55fsrkefry2cpuyt4cpsn2pg28"
+
+
+# ---- Taproot with script path: pk leaf ----
+
+[[case]]
+name = "tr_script_pk"
+descriptor_template = "tr(@0/**,pk(@1/**))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+change = 0
+address_index = 0
+expected_address = "tb1pls9pp5cgcljpkjauxep03lv2c2yc2wcuua26p3ks6j2lq0vl9kjqf5rgm2"
+
+
+# ---- Taproot with script path: sortedmulti_a leaf ----
+
+[[case]]
+name = "tr_script_sortedmulti_a"
+descriptor_template = "tr(@0/**,sortedmulti_a(2,@1/**,@2/**))"
+keys_info = [
+ "[f5acc2fd/48'/1'/0'/1']tpubDFAqEGNyad35YgH8zxvxFZqNUoPtr5mDojs7wzbXQBHTZ4xHeVXG6w2HvsKvjBpaRpTmjYDjdPg5w2c6Wvu8QBkyMDrmBWdCyqkDM7reSsY",
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+change = 0
+address_index = 0
+expected_address = "tb1pdzk72dnvz3246474p4m5a97u43h6ykt2qcjrrhk6y0fkg8hx2mvswwgvv7"
+
+
+# ---- Taproot with musig in the key path ----
+
+[[case]]
+name = "tr_musig_keypath"
+descriptor_template = "tr(musig(@0,@1)/**)"
+keys_info = [
+ "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT",
+ "tpubDCwYjpDhUdPGQWG6wG6hkBJuWFZEtrn7j3xwG3i8XcQabcGC53xWZm1hSXrUPFS5UvZ3QhdPSjXWNfWmFGTioARHuG5J7XguEjgg7p8PxAm",
+]
+change = 0
+address_index = 3
+expected_address = "tb1pc87la0ksvw4pfq6qc3gn9en33kx7s9rx4c4epy578kfjsdjv6mks7u7dgn"
+
+
+# ---- Taproot with musig in a script path leaf ----
+
+[[case]]
+name = "tr_musig_scriptpath"
+descriptor_template = "tr(@0/**,pk(musig(@1,@2)/**))"
+keys_info = [
+ "tpubD6NzVbkrYhZ4WLczPJWReQycCJdd6YVWXubbVUFnJ5KgU5MDQrD998ZJLSmaB7GVcCnJSDWprxmrGkJ6SvgQC6QAffVpqSvonXmeizXcrkN",
+ "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT",
+ "tpubDCwYjpDhUdPGQWG6wG6hkBJuWFZEtrn7j3xwG3i8XcQabcGC53xWZm1hSXrUPFS5UvZ3QhdPSjXWNfWmFGTioARHuG5J7XguEjgg7p8PxAm",
+]
+change = 0
+address_index = 3
+expected_address = "tb1pa423acwcjc8jgt36muavyun8e2hz3t5qwptsr3wr8afmdfk3wchswf9ntp"
Why this scored 15/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.