What changed, and why it matters
This commit is a test-only refactoring change. It moves existing test data from a private unit-test file into shared TOML files in a new test_vectors/ directory so the same test cases can be reused by Python, C, and future Rust test suites. No application code that runs on the Ledger device was changed, and no security behavior of the app was modified.
No security action needed. Treat as normal test-infrastructure maintenance.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change extracts deterministic test vectors for get_extended_pubkey, get_wallet_address, register_wallet, sign_message, and sign_psbt into language-neutral TOML files. It updates unit-tests/CMakeLists.txt to point the existing C unit test at the new shared file location and deletes the old unit-tests/test_get_wallet_address_vectors.toml. The README documents the schema and conventions. No firmware source code, APDU handlers, cryptography, or wallet logic was altered.
Changed components
test_vectors/ directory (new)unit-tests/CMakeLists.txt (path update)unit-tests/test_get_wallet_address_vectors.toml (deleted)Inspect captured patch +1299 / −229
diff --git a/test_vectors/README.md b/test_vectors/README.md
new file mode 100644
index 0000000..db61cab
--- /dev/null
+++ b/test_vectors/README.md
@@ -0,0 +1,130 @@
+# Language-neutral test vectors
+
+This directory holds **TOML test assets** that capture the deterministic core of
+the app's behavioral tests as plain data: inputs and expected outputs/errors,
+with no framework-specific code. The same vectors can be replayed from the Python
+suite (`tests/`), the C unit-tests (`unit-tests/`, via the bundled `tomlc17`
+parser), and — eventually — the Rust client (`bitcoin_client_rs/`).
+
+The goal is to keep the *what the device must produce* separate from the *how a
+given framework drives the device*. UX-only behaviors (screen-by-screen
+approve/reject navigation), randomized PSBTs, multi-round MuSig2, and
+bitcoind-integration end-to-end flows are intentionally **not** captured here;
+they remain as bespoke code in each framework.
+
+All vectors were generated against the Speculos test seed (see
+[`fixtures.toml`](fixtures.toml)) on the **testnet** network.
+
+## Files
+
+| File | Operation |
+|------|-----------|
+| [`fixtures.toml`](fixtures.toml) | Global constants (seed, master fingerprint, network). |
+| [`get_extended_pubkey.toml`](get_extended_pubkey.toml) | `get_extended_pubkey`. |
+| [`sign_message.toml`](sign_message.toml) | `sign_message`. |
+| [`register_wallet.toml`](register_wallet.toml) | `register_wallet`. |
+| [`get_wallet_address.toml`](get_wallet_address.toml) | `get_wallet_address`. |
+| [`sign_psbt.toml`](sign_psbt.toml) | `sign_psbt`. |
+
+## Common conventions
+
+Each file contains an array of `[[case]]` tables.
+
+- **`name`** (string, required, unique): per-case test label.
+- **`description`** (string, optional): human note.
+
+### Wallet policy block
+
+Operations that take a wallet policy (`register_wallet`, `get_wallet_address`,
+`sign_psbt`) embed it directly in the case:
+
+- **`descriptor_template`** (string): a V2 descriptor template, e.g.
+ `"wpkh(@0/**)"`, `"sh(sortedmulti(2,@0/**,@1/**))"`, `"tr(@0/**,pk(@1/**))"`.
+- **`keys_info`** (array of strings): key-info strings in `@0,@1,…` order, e.g.
+ `"[f5acc2fd/44'/1'/0']tpub..."`.
+- **`wallet_name`** (string, optional, default `""`): the registered wallet name.
+ Default (empty) is used for standard single-key policies.
+
+Only **V2** policies are represented (V1 is out of scope). The Python
+`MultisigWallet(...)` helper is just sugar over a `descriptor_template` +
+`keys_info`; vectors always store this canonical form, which both the C parser
+(`parse_descriptor_template`) and the Python `WalletPolicy` accept directly.
+
+### Error encoding
+
+A case that expects the device to reject the request carries an **`error`** string
+instead of the `expected_*` field(s). The value is a symbolic status-word name
+that each framework maps to the numeric status word (see `src/boilerplate/sw.h`
+and `DeviceException.exc` in the Python client):
+
+| Name | Status word |
+|------|-------------|
+| `OK` | `0x9000` |
+| `DENY` | `0x6985` |
+| `SECURITY_STATUS_NOT_SATISFIED` | `0x6982` |
+| `INCORRECT_DATA` | `0x6A80` |
+| `NOT_SUPPORTED` | `0x6A82` |
+| `WRONG_P1P2` | `0x6A86` |
+| `WRONG_DATA_LENGTH` | `0x6A87` |
+| `BAD_STATE` | `0xB007` |
+| `SIGNATURE_FAIL` | `0xB008` |
+
+Only **deterministic** errors (those derivable from the inputs) are encoded here.
+`DENY` errors that arise purely from UI rejection are *not* captured — they depend
+on navigation, not on the request, and stay as framework-specific code.
+
+## Per-operation fields
+
+### `get_extended_pubkey.toml`
+- `path` (string): BIP-32 path, e.g. "m/44'/1'/0'".
+- `standard` (bool): whether the path is exported without user confirmation.
+ - `true`: exported regardless of the display flag.
+ - `false`: exported only when the UI confirmation is shown; otherwise `NOT_SUPPORTED`.
+- `expected_pubkey` (string, optional) on success — *or* `error` (e.g. `NOT_SUPPORTED`,
+ `INCORRECT_DATA`).
+
+### `sign_message.toml`
+- `message` (string) *or* `message_hex` (hex string) for raw/binary input.
+- `path` (string).
+- `expected_signature` (base64 string) on success — *or* `error`.
+
+### `register_wallet.toml`
+- wallet policy block (`descriptor_template`, `keys_info`, `wallet_name`).
+- `expected_wallet_id` (hex, 32 bytes): the policy id (sha256 of the registration);
+ secret-free, checkable by any framework.
+- `expected_wallet_hmac` (hex, 32 bytes, optional): reproducible only with the
+ Speculos registration key.
+- *or* `error` (e.g. `INCORRECT_DATA` for a bad name, `NOT_SUPPORTED` for a
+ non-sane policy).
+
+### `get_wallet_address.toml`
+- wallet policy block.
+- `change` (int): `0` (receive) or `1` (change).
+- `address_index` (int, non-negative).
+- `expected_address` (string) on success — *or* `error`.
+
+### `sign_psbt.toml`
+- `psbt_file` (string, relative to `tests/`) *or* `psbt` (base64 string).
+- wallet policy block.
+- `wallet_hmac` (hex, optional): required for non-default (registered) policies.
+- one or more `[[case.expected_signatures]]` sub-tables:
+ - `input_index` (int).
+ - `pubkey` (hex string): 33-byte compressed (legacy/segwit) or 32-byte x-only
+ (taproot).
+ - `sighash_type` (int, **required**): the SIGHASH flag the input is signed with
+ (`ALL=1`, `NONE=2`, `SINGLE=3`, `+ANYONECANPAY=0x80`; taproot `DEFAULT=0`).
+ - exactly one of:
+ - `signature` (hex string): exact expected bytes. Present for deterministic
+ ECDSA/DER signatures — assert byte-equality. The trailing sighash byte is
+ included for non-default sighash types.
+ - `sighash` (hex string, 32 bytes): the signed message **digest**. Present for
+ taproot Schnorr signatures, which are *not* byte-stable; the framework
+ verifies the produced signature against `pubkey` over this digest (BIP-340).
+ Storing the digest directly keeps verification portable — a framework need
+ not reimplement the sighash algorithm. (For a 65-byte taproot signature, drop
+ the trailing sighash-type byte before verifying.)
+ - `tapleaf_hash` (hex string, optional): for tapscript spends.
+
+The top-level case may also carry `error` (instead of `expected_signatures`) for
+deterministic rejections, e.g. `NOT_SUPPORTED` for `SIGHASH_SINGLE` with no
+matching output, or an unsupported sighash type.
diff --git a/test_vectors/fixtures.toml b/test_vectors/fixtures.toml
new file mode 100644
index 0000000..dbb9a23
--- /dev/null
+++ b/test_vectors/fixtures.toml
@@ -0,0 +1,14 @@
+# Global constants the vectors in this directory were generated against.
+#
+# Frameworks that re-derive keys/signatures from the seed read these once.
+# These match tests/conftest.py (the Speculos test setup).
+
+# The Speculos test seed (BIP-39 mnemonic).
+mnemonic = "glory promote mansion idle axis finger extra february uncover one trip resource lawn turtle enact monster seven myth punch hobby comfort wild raise skin"
+
+# Master key fingerprint derived from the seed above (BIP-32, 4 bytes, hex).
+master_key_fingerprint = "f5acc2fd"
+
+# Network all expected addresses/keys are encoded for.
+# testnet: base58 P2PKH version 111, P2SH version 196; bech32/bech32m HRP "tb".
+network = "test"
diff --git a/test_vectors/get_extended_pubkey.toml b/test_vectors/get_extended_pubkey.toml
new file mode 100644
index 0000000..b47ab96
--- /dev/null
+++ b/test_vectors/get_extended_pubkey.toml
@@ -0,0 +1,211 @@
+# Test vectors for get_extended_pubkey.
+#
+# Source: tests/test_get_extended_pubkey.py
+#
+# Schema (per [[case]]):
+# name human-readable identifier.
+# path BIP32 derivation path ("m", "m/44'/1'/0'", ...).
+# standard whether the path is exported without user confirmation:
+# * standard = true -> exported regardless of the display flag.
+# * standard = false -> only exported with display = true; with
+# display = false it is rejected with
+# NOT_SUPPORTED.
+# error (optional) expected status word name. Orthogonal to `standard`:
+# when set, that error is expected instead of a successful export
+# (e.g. paths over the max depth give INCORRECT_DATA regardless of
+# the display flag).
+# expected_pubkey
+# (optional) expected base58check pubkey. Only pinned where an
+# independent source exists (the functional tests). When omitted,
+# the test only checks that the export succeeds; the encoding
+# itself is covered by the pinned cases.
+#
+# Pure UI approve/reject flows and raw-APDU parse errors are not captured here.
+
+
+# ---- Standard paths (exported with or without display) ----
+
+[[case]]
+name = "std_pkh_account0"
+path = "m/44'/1'/0'"
+standard = true
+expected_pubkey = "tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT"
+
+[[case]]
+name = "std_pkh_account10"
+path = "m/44'/1'/10'"
+standard = true
+expected_pubkey = "tpubDCwYjpDhUdPGp21gSpVay2QPJVh6WNySWMXPhbcu1DsxH31dF7mY18oibbu5RxCLBc1Szerjscuc3D5HyvfYqfRvc9mesewnFqGmPjney4d"
+
+[[case]]
+name = "std_pkh_account2_recv_42"
+path = "m/44'/1'/2'/1/42"
+standard = true
+expected_pubkey = "tpubDGF9YgHKv6qh777rcqVhpmDrbNzgophJM9ec7nHiSfrbss7fVBXoqhmZfohmJSvhNakDHAspPHjVVNL657tLbmTXvSeGev2vj5kzjMaeupT"
+
+[[case]]
+name = "std_bip48_account4"
+path = "m/48'/1'/4'/1'/0/7"
+standard = true
+expected_pubkey = "tpubDK8WPFx4WJo1R9mEL7Wq325wBiXvkAe8ipgb9Q1QBDTDUD2YeCfutWtzY88NPokZqJyRPKHLGwTNLT7jBG59aC6VH8q47LDGQitPB6tX2d7"
+
+[[case]]
+name = "std_sh_wpkh_account1"
+path = "m/49'/1'/1'/1/3"
+standard = true
+expected_pubkey = "tpubDGnetmJDCL18TyaaoyRAYbkSE9wbHktSdTS4mfsR6inC8c2r6TjdBt3wkqEQhHYPtXpa46xpxDaCXU2PRNUGVvDzAHPG6hHRavYbwAGfnFr"
+
+[[case]]
+name = "std_wpkh_account2"
+path = "m/84'/1'/2'/0/10"
+standard = true
+expected_pubkey = "tpubDG9YpSUwScWJBBSrhnAT47NcT4NZGLcY18cpkaiWHnkUCi19EtCh8Heeox268NaFF6o56nVeSXuTyK6jpzTvV1h68Kr3edA8AZp27MiLUNt"
+
+[[case]]
+name = "std_tr_account4"
+path = "m/86'/1'/4'/1/12"
+standard = true
+expected_pubkey = "tpubDHTZ815MvTaRmo6Qg1rnU6TEU4ZkWyA56jA1UgpmMcBGomnSsyo34EZLoctzZY9MTJ6j7bhccceUeXZZLxZj5vgkVMYfcZ7DNPsyRdFpS3f"
+
+[[case]]
+name = "std_tr_8_steps"
+path = "m/86'/1'/4'/1/2/3/4/5"
+standard = true
+expected_pubkey = "tpubDNcjumrTe1BBYEc1FmMaJZQw47mbvb4LfX4YwqC6GQ18PfMfuH3BEYREfdHm2gWXkSJ3JiXHF11iKnbJxzxp5qkgo8BBy2L48FRvrLhpTuh"
+
+[[case]]
+name = "std_bip45_unchained_account0"
+path = "m/45'/1'/0'"
+standard = true
+expected_pubkey = "tpubDCy2BKyxJFzACNgThkunvdnkHNotREK9LQDw8L9J1gx26SyzfoeJynJgWekzkramggmahVAgeHPxfpnvFtJ7hcYADrsVUnsPSei2tY9fBLL"
+
+[[case]]
+name = "std_bip45_unchained_account0_chain1"
+path = "m/45'/1'/0'/1"
+standard = true
+expected_pubkey = "tpubDFGDxRGdGFKekUtPuta4p9Kw2a2PSeyyhSTa7KNENJfBuJ78EEsL1LxwAA8ddSxZFWBT9gYRuLDoa2rwdix56WRsq77vAJ2iqeyPw6UBeyt"
+
+# Electrum historically used "m/4541509h/1112098098h" to derive encryption
+# keys, so the app whitelists it: non-standard in the BIP sense, but exported
+# without confirmation.
+[[case]]
+name = "electrum_path"
+path = "m/4541509'/1112098098'"
+standard = true
+expected_pubkey = "tpubDAs3mrkQXkGyzp7Yo9SXiZNW7Tmia5EmdUpXjuBQvBDDGGr9CnVHehSB6P5RZFY3bwkYBweXir8MhmvPXqYHHVxKrFkm3mfZ5UkjG5ZH8Ui"
+
+# Boundary: account == MAX_BIP44_ACCOUNT_RECOMMENDED (100) is still standard.
+[[case]]
+name = "std_account_max"
+path = "m/44'/1'/100'"
+standard = true
+
+# Boundary: BIP48 script type 2' is a standardized value, so this is standard.
+[[case]]
+name = "std_bip48_script_type_2"
+path = "m/48'/1'/0'/2'"
+standard = true
+
+
+# ---- Non-standard paths (exported only with display = true) ----
+
+# Unchained Capital multisig setup. Non-standard for export-without-display:
+# is_path_safe accepts BIP-45 only in the canonical m/45'/coin_type'/account'
+# shape (3 hardened steps, extras unhardened, coin_type == BIP44_COIN_TYPE).
+# This path has a 4th hardened step (1') and coin_type 2', so it is only
+# exported with display = true. (Derivation itself is still permitted; the
+# Makefile whitelists the whole 45' tree via PATH_APP_LOAD_PARAMS.)
+[[case]]
+name = "nonstd_bip45_unchained"
+path = "m/45'/2'/0'/1'"
+standard = false
+expected_pubkey = "tpubDFL11pFAgsKed5bv9Tkxe51xyB4qo1cPDwK6c8WZ4wiVEjtGDg5YuMXNk9yZcB6b47k2oaSWADJF3CRmk97qAwnaiRieT2ocWzh4rq2b3F3"
+
+[[case]]
+name = "nonstandard_bip45_root_display"
+path = "m/45'"
+standard = false
+expected_pubkey = "tpubDA4runSouWhEn4C36dL1PwpMhWi1LCgc7EkYJXLExjVr3eWNE5p5ZRUe7LN7cY8YwCroQsLECcv9ufij8EUpDHM2WQM9Cba8Ztf6Zo8jpFf"
+
+# Maximum supported depth (10 steps). Non-standard: it has hardened steps
+# beyond the BIP48 prefix, so it is only exported when display = true.
+[[case]]
+name = "bip48_max_depth_display"
+path = "m/48'/1'/0'/2'/0'/1'/2'/3'/0/1"
+standard = false
+expected_pubkey = "tpubDSegeM6ezY6VYgNjYiUxk94ZwLYeuUzHpUYff4LLdEEUVUd8VpgGUxyxZ9oRiEepZzmFZogVWFEWznRj3oJgEuWjcg6BERCxTCYdaxwtShk"
+
+[[case]]
+name = "nonstd_root"
+path = "m"
+standard = false
+
+[[case]]
+name = "nonstd_purse_only"
+path = "m/44'"
+standard = false
+
+[[case]]
+name = "nonstd_purpose_coin"
+path = "m/44'/1'"
+standard = false
+
+[[case]]
+name = "nonstd_wrong_coin_type"
+path = "m/44'/10'/0'"
+standard = false
+
+[[case]]
+name = "nonstd_account_not_hardened"
+path = "m/44'/1'/0"
+standard = false
+
+[[case]]
+name = "nonstd_cointype_not_hardened"
+path = "m/44'/1/0'"
+standard = false
+
+[[case]]
+name = "nonstd_purpose_not_hardened"
+path = "m/44/1'/0'"
+standard = false
+
+[[case]]
+name = "nonstd_bip48_script_type_0"
+path = "m/48'/1'/0'/0'"
+standard = false
+
+[[case]]
+name = "nonstd_bip48_script_type_3"
+path = "m/48'/1'/0'/3'"
+standard = false
+
+[[case]]
+name = "nonstd_unknown_purpose"
+path = "m/999'/1'/0'"
+standard = false
+
+[[case]]
+name = "nonstd_bip45_wrong_coin_type"
+path = "m/45'/2'/0'"
+standard = false
+
+[[case]]
+name = "nonstd_bip45_too_short"
+path = "m/45'/1'"
+standard = false
+
+# Boundary: account == MAX_BIP44_ACCOUNT_RECOMMENDED + 1 (101) is non-standard.
+[[case]]
+name = "nonstd_account_over_max"
+path = "m/44'/1'/101'"
+standard = false
+
+
+# ---- Path exceeding the maximum depth: INCORRECT_DATA (regardless of display) ----
+
+[[case]]
+name = "too_long_path"
+path = "m/48'/1'/0'/2'/0'/1'/2'/3'/0/1/2"
+standard = true
+error = "INCORRECT_DATA"
diff --git a/test_vectors/get_wallet_address.toml b/test_vectors/get_wallet_address.toml
new file mode 100644
index 0000000..7cf9ee0
--- /dev/null
+++ b/test_vectors/get_wallet_address.toml
@@ -0,0 +1,222 @@
+# Test vectors for get_wallet_address (get_wallet_script + get_script_address).
+#
+# Schema and conventions are documented in test_vectors/README.md. In short, each
+# `[[case]]` carries a wallet policy block (descriptor_template, keys_info,
+# optional wallet_name), a `change` flag (0 receive / 1 change), an
+# `address_index`, and the `expected_address` (or an `error` name).
+#
+# 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"
diff --git a/test_vectors/register_wallet.toml b/test_vectors/register_wallet.toml
new file mode 100644
index 0000000..68d55f6
--- /dev/null
+++ b/test_vectors/register_wallet.toml
@@ -0,0 +1,377 @@
+# Test vectors for register_wallet.
+#
+# Schema and conventions are documented in test_vectors/README.md.
+# Source: tests/test_register_wallet.py
+#
+# `expected_wallet_id` is the policy id (sha256 of the registration) and can be
+# checked by any framework with no secrets. `expected_wallet_hmac` is the device
+# HMAC over that id, reproducible only with the Speculos registration key
+# (see fixtures.toml). Pure UI reject flows (DENY) are not captured here.
+
+[[case]]
+name = "multisig_legacy_2of2"
+description = "2-of-2 sortedmulti, legacy P2SH"
+descriptor_template = "sh(sortedmulti(2,@0/**,@1/**))"
+keys_info = [
+ "[5c9e228d/48'/1'/0'/0']tpubDEGquuorgFNb8bjh5kNZQMPtABJzoWwNm78FUmeoPkfRtoPF7JLrtoZeT3J3ybq1HmC3Rn1Q8wFQ8J5usanzups5rj7PJoQLNyvq8QbJruW",
+ "[f5acc2fd/48'/1'/0'/0']tpubDFAqEGNyad35WQAZMmPD4vgBXnjH16RGciLdWekPe4f4d5JzoHVu1PS86Sy4Tm63vDf8rfV3UjifhrRuSUDfiZj5KPffTPyZ4ZXBKvjD8jm",
+]
+wallet_name = "Cold storage"
+expected_wallet_id = "1d150ed425a871a5ca7e2c55db1b3295c1fd97147fd0a7b188d4327f4ba7402a"
+expected_wallet_hmac = "fa73e36119324fbe4cc1ca94aa842c6261526d44112a22164bc57c3335102b04"
+
+[[case]]
+name = "multisig_sh_wit_2of2"
+description = "2-of-2 sortedmulti, wrapped segwit P2SH-P2WSH"
+descriptor_template = "sh(wsh(sortedmulti(2,@0/**,@1/**)))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/1']tpubDE7NQymr4AFtcJXi9TaWZtrhAdy8QyKmT4U6b9qYByAxCzoyMJ8zw5d8xVLVpbTRAEqP8pVUxjLE2vDt1rSFjaiS8DSz1QcNZ8D1qxUMx1g",
+ "[f5acc2fd/48'/1'/0'/1']tpubDFAqEGNyad35YgH8zxvxFZqNUoPtr5mDojs7wzbXQBHTZ4xHeVXG6w2HvsKvjBpaRpTmjYDjdPg5w2c6Wvu8QBkyMDrmBWdCyqkDM7reSsY",
+]
+wallet_name = "Cold storage"
+expected_wallet_id = "763926f53be53ad89a9248dc15bc2f3ed577a59a87d81cd88f14279b263b31f6"
+expected_wallet_hmac = "1f498e7444841b883c4a63e2b88a5cad297c289d235794f8e3e17cf559ed0654"
+
+[[case]]
+name = "multisig_wit_2of2"
+description = "2-of-2 sortedmulti, native segwit P2WSH"
+descriptor_template = "wsh(sortedmulti(2,@0/**,@1/**))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+wallet_name = "Cold storage"
+expected_wallet_id = "cd9474ae9e74403128477789789db43a215e996af80d60120f0d844f8404ac64"
+expected_wallet_hmac = "d7c7a60b4ab4a14c1bf8901ba627d72140b2fb907f2b4e35d2e693bce9fbb371"
+
+[[case]]
+name = "multisig_wit_2of2_long_name"
+description = "2-of-2 native segwit with a 64-char name"
+descriptor_template = "wsh(sortedmulti(2,@0/**,@1/**))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+wallet_name = "Cold storage with a pretty long name that requires 64 characters"
+expected_wallet_id = "57f64b36153b819c624dedd0ba3ba491000c41652087b5b265b2460508b09620"
+expected_wallet_hmac = "42ea7900175227ee3ea259a0a061dda232dce3e93707d0940f9dc63bab50d35a"
+
+[[case]]
+name = "unusual_singlesig_legacy"
+description = "single-key legacy on an unusual account path (44'/1'/3')"
+descriptor_template = "pkh(@0/**)"
+keys_info = [
+ "[f5acc2fd/44'/1'/3']tpubDCwYjpDhUdPGW815u9VExvLRXDAHehcnkNfjqiwSvJp7NizpAGsX3Qfq9A173rES9vF17HgeqXBUvodRTyeTHCeAvg7gVgDE19mudggheN1",
+]
+wallet_name = "Unusual legacy"
+expected_wallet_id = "409d4fb1e165e94fb7a822f82ce648e3cc263f06311b790d7f6299ee7fcb5987"
+expected_wallet_hmac = "5d4a77a905b1be578a638f5c037a9a646af5ad71c16f813913bb9aba7abac795"
+
+[[case]]
+name = "unusual_singlesig_nested_segwit"
+description = "single-key nested_segwit on an unusual account path (44'/1'/3')"
+descriptor_template = "sh(wpkh(@0/**))"
+keys_info = [
+ "[f5acc2fd/44'/1'/3']tpubDCwYjpDhUdPGW815u9VExvLRXDAHehcnkNfjqiwSvJp7NizpAGsX3Qfq9A173rES9vF17HgeqXBUvodRTyeTHCeAvg7gVgDE19mudggheN1",
+]
+wallet_name = "Unusual nested_segwit"
+expected_wallet_id = "70e97d1e974a5cf46cd13b8c3358850c51873a1a1ab8b60647ed35520a76e870"
+expected_wallet_hmac = "f7e49a379e71cfe6645a564c641f1b5232aeb6696c862f4f3df581236bd451c4"
+
+[[case]]
+name = "unusual_singlesig_native_segwit"
+description = "single-key native_segwit on an unusual account path (44'/1'/3')"
+descriptor_template = "wpkh(@0/**)"
+keys_info = [
+ "[f5acc2fd/44'/1'/3']tpubDCwYjpDhUdPGW815u9VExvLRXDAHehcnkNfjqiwSvJp7NizpAGsX3Qfq9A173rES9vF17HgeqXBUvodRTyeTHCeAvg7gVgDE19mudggheN1",
+]
+wallet_name = "Unusual native_segwit"
+expected_wallet_id = "d5de0973b368d69f67166391d66b8ead31478a6e7dda5c9acbeff185523e1f64"
+expected_wallet_hmac = "b9c66ce0adb1684c2f2cf51e280c3a95746393ddfe32ebfb6938922a37b438d9"
+
+[[case]]
+name = "unusual_singlesig_taproot"
+description = "single-key taproot on an unusual account path (44'/1'/3')"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/44'/1'/3']tpubDCwYjpDhUdPGW815u9VExvLRXDAHehcnkNfjqiwSvJp7NizpAGsX3Qfq9A173rES9vF17HgeqXBUvodRTyeTHCeAvg7gVgDE19mudggheN1",
+]
+wallet_name = "Unusual taproot"
+expected_wallet_id = "4e9679b3f4aaf54a3ffdef160963bc5b4d6ac706499d2317e8514819a9777921"
+expected_wallet_hmac = "9006e41fe8ba29dd93aeae79dc6b3513db42d2fbcf18dbda5df860342e137fec"
+
+[[case]]
+name = "miniscript_long_policy"
+description = "miniscript policy longer than 256 bytes"
+descriptor_template = "wsh(and_v(and_v(v:pk(@0/**),or_c(pk(@1/**),or_c(pk(@2/**),v:older(1000)))),and_v(v:hash256(0563fb3e85cbc61b134941ad6610a2b0dfd77543dfb77a5433ff3cb538213807),and_v(v:hash256(ad3391a00bad00a6a03f907b3fcc2f369a88be038c63c7db7f43b01e097efbbe),hash256(137dfa9b54a538200c94e3c9dd1a59b431e3b89aef8093fc910df48a98cb06d9)))))"
+keys_info = [
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ "tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "tpubDDV6FDLcCieWUeN7R3vZK2Qs3KuQed3ScTY9EiwMXvyCkLjDbCb8RXaAgWDbkG4tW1BMKVF1zERHnyt78QKd4ZaAYGMJMpvHPwgSSU1AxZ3",
+]
+wallet_name = "Long policy"
+expected_wallet_id = "71b806a6ef8102626f8c9126a0bac51efca19fadd295c42245be240138bb0b57"
+expected_wallet_hmac = "c018966ba0416569d3510c93f5c6cd88c4338a9192e3de3a94db9d27f58615b3"
+
+[[case]]
+name = "miniscript_minmax_relative_timelocks"
+description = "min/max relative timelocks in a taptree"
+descriptor_template = "tr(@0/<0;1>/*,{thresh(3,pk(@0/<2;3>/*),s:pk(@1/<2;3>/*),s:pk(@2/<2;3>/*),sln:older(1),sln:older(65535)),thresh(3,pk(@0/<4;5>/*),s:pk(@1/<4;5>/*),s:pk(@2/<4;5>/*),sln:older(4194305),sln:older(4259839))})"
+keys_info = [
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ "tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "tpubDDV6FDLcCieWUeN7R3vZK2Qs3KuQed3ScTY9EiwMXvyCkLjDbCb8RXaAgWDbkG4tW1BMKVF1zERHnyt78QKd4ZaAYGMJMpvHPwgSSU1AxZ3",
+]
+wallet_name = "Relative timelocks"
+expected_wallet_id = "3d9900113d87d80c72ca9d036d1f7aa163cea194629afa4c4946409e05b00fcb"
+expected_wallet_hmac = "65fda496184f9f720c2d3eb47169b94d2390fe2a085e6d2c43eb6d12d7608af3"
+
+[[case]]
+name = "tr_script_pk"
+description = "taproot, foreign internal key and our script key"
+descriptor_template = "tr(@0/**,pk(@1/**))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+wallet_name = "Taproot foreign internal key, and our script key"
+expected_wallet_id = "ba1a89c470ab0c36c38beaebb577b754c33d90b51df53977e2b01018fbe25f33"
+expected_wallet_hmac = "dae925660e20859ed8833025d46444483ce264fdb77e34569aabe9d590da8fb7"
+
+[[case]]
+name = "tr_nums_keypath"
+description = "taproot with an unspendable (NUMS) internal key"
+descriptor_template = "tr(@0/**,pk(@1/**))"
+keys_info = [
+ "tpubD6NzVbkrYhZ4WLczPJWReQycCJdd6YVWXubbVUFnJ5KgU5MDQrD998ZJLSmaB7GVcCnJSDWprxmrGkJ6SvgQC6QAffVpqSvonXmeizXcrkN",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+wallet_name = "Taproot unspendable keypath"
+expected_wallet_id = "ba6c11196b604c062a27e195bcbdfe6c1fe1fa61e92256974b5c53c2fc2e2172"
+expected_wallet_hmac = "7af8844fb3f176337acd66fb76113b5e250e1be75a0845964587219b297fda59"
+
+[[case]]
+name = "tr_script_sortedmulti_a"
+description = "taproot single-key or 2-of-2 sortedmulti_a script"
+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",
+]
+wallet_name = "Taproot single-key or multisig 2-of-2"
+expected_wallet_id = "46c498a049e164ec7feaf167c3ae0f4282217645a338f66f5377bfe67f6a8aa0"
+expected_wallet_hmac = "a3f31e9d7b70d1d967413488bae136a8b6c7afd1de0524deb6cf74f5c509b9ab"
+
+[[case]]
+name = "max_derivation_steps"
+description = "key with the maximum (8) BIP-388 derivation steps"
+descriptor_template = "wpkh(@0/**)"
+keys_info = [
+ "[f5acc2fd/48'/1'/0'/2'/0'/1'/2'/3']tpubDNTK3WZ6g6eZBbCYCdFMpkCNvEqMxYexWRyqrGDkC5WDsNJdUk95z9wHWm6rBZeeMRxKafnuJzT8TqUs94A11KKkW9wT1pcFQShC9p3Vcxi",
+]
+wallet_name = "Max derivation"
+expected_wallet_id = "71b58446a17ebc98e654560f4f1fb9003bad49ddde1d3ee9384221f7f2d3dda2"
+expected_wallet_hmac = "e3bb8a13e19d74079ee209a99438da0b47f3f430a88130ba18b1c05e626d22c8"
+
+# ---- Deterministic rejections ----
+
+[[case]]
+name = "invalid_pubkey_version"
+description = "external key uses a mainnet xpub version instead of testnet tpub"
+descriptor_template = "wsh(sortedmulti(2,@0/**,@1/**))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']xpub6DjjtjxALtJSP9dKRKuhejeTpZc711gUGZyS9nCM5GAtrNTDuMBZD2FcndJoHst6LYNbJktm4NmJyKqspLi5uRmtnDMAdcPAf2jiSj9gFTX",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+wallet_name = "Cold storage"
+error = "INCORRECT_DATA"
+
+[[case]]
+name = "invalid_name_empty"
+description = "unacceptable wallet name: empty"
+descriptor_template = "wsh(sortedmulti(2,@0/**,@1/**))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+error = "INCORRECT_DATA"
+
+[[case]]
+name = "invalid_name_too_long_65"
+description = "unacceptable wallet name: too_long_65"
+descriptor_template = "wsh(sortedmulti(2,@0/**,@1/**))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+wallet_name = "This wallet name is much too long since it requires 65 characters"
+error = "INCORRECT_DATA"
+
+[[case]]
+name = "invalid_name_leading_space"
+description = "unacceptable wallet name: leading_space"
+descriptor_template = "wsh(sortedmulti(2,@0/**,@1/**))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+wallet_name = " Test"
+error = "INCORRECT_DATA"
+
+[[case]]
+name = "invalid_name_trailing_space"
+description = "unacceptable wallet name: trailing_space"
+descriptor_template = "wsh(sortedmulti(2,@0/**,@1/**))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+wallet_name = "Test "
+error = "INCORRECT_DATA"
+
+[[case]]
+name = "invalid_name_out_of_range_char"
+description = "unacceptable wallet name: out_of_range_char"
+descriptor_template = "wsh(sortedmulti(2,@0/**,@1/**))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+wallet_name = "Tæst"
+error = "INCORRECT_DATA"
+
+[[case]]
+name = "missing_key"
+description = "policy references @1 but only one key is provided"
+descriptor_template = "wsh(multi(2,@0/**,@1/**))"
+keys_info = [
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+wallet_name = "Missing a key"
+error = "INCORRECT_DATA"
+
+[[case]]
+name = "unsupported_bare_pk"
+description = "bare pk() top-level descriptor is not supported"
+descriptor_template = "pk(@0/**)"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+]
+wallet_name = "Unsupported"
+error = "NOT_SUPPORTED"
+
+[[case]]
+name = "not_sane_duplicate_key"
+description = "the same key appears twice in the keys vector"
+descriptor_template = "wsh(c:andor(pk(@0/<0;1>/*),pk_k(@1/**),and_v(v:pk(@2/<2;3>/*),pk_k(@3/**))))"
+keys_info = [
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ "tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ "tpubDDV6FDLcCieWUeN7R3vZK2Qs3KuQed3ScTY9EiwMXvyCkLjDbCb8RXaAgWDbkG4tW1BMKVF1zERHnyt78QKd4ZaAYGMJMpvHPwgSSU1AxZ3",
+]
+wallet_name = "Unsupported policy"
+error = "NOT_SUPPORTED"
+
+[[case]]
+name = "not_sane_overlapping_derivations"
+description = "same key with overlapping derivations"
+descriptor_template = "wsh(thresh(3,pk(@0/**),s:pk(@1/**),s:pk(@0/**),sln:older(12960)))"
+keys_info = [
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ "tpubDDV6FDLcCieWUeN7R3vZK2Qs3KuQed3ScTY9EiwMXvyCkLjDbCb8RXaAgWDbkG4tW1BMKVF1zERHnyt78QKd4ZaAYGMJMpvHPwgSSU1AxZ3",
+]
+wallet_name = "Unsupported policy"
+error = "NOT_SUPPORTED"
+
+[[case]]
+name = "not_sane_timelock_mixing"
+description = "miniscript mixing time- and height-based absolute timelocks"
+descriptor_template = "wsh(thresh(2,c:pk_k(@0/**),ac:pk_k(@1/**),altv:after(1000000000),altv:after(100)))"
+keys_info = [
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ "tpubDDV6FDLcCieWUeN7R3vZK2Qs3KuQed3ScTY9EiwMXvyCkLjDbCb8RXaAgWDbkG4tW1BMKVF1zERHnyt78QKd4ZaAYGMJMpvHPwgSSU1AxZ3",
+]
+wallet_name = "Timelock mixing is bad"
+error = "NOT_SUPPORTED"
+
+[[case]]
+name = "not_sane_no_signature_required"
+description = "miniscript that does not always require a signature"
+descriptor_template = "wsh(or_d(multi(1,@0/**),or_b(multi(3,@1/**,@2/**,@3/**),su:after(500000))))"
+keys_info = [
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ "tpubDDV6FDLcCieWUeN7R3vZK2Qs3KuQed3ScTY9EiwMXvyCkLjDbCb8RXaAgWDbkG4tW1BMKVF1zERHnyt78QKd4ZaAYGMJMpvHPwgSSU1AxZ3",
+ "tpubDF6JT5K4izwALMpFv7fQrpWr5bGUMEoWphkzTVJH8jTfgirNEgGZnxsWJDCCxhg2UnW5RcD9Tx8aVAdoM734X5bnRGmJUujz26uQ5gAC1nE",
+ "tpubDF4kujkh5dAhC1pFgBToZybXdvJFXXGX4BWdDxWqP7EUpG8gxkfMQeDjGPDnTr9e4NrkFmDM1ocav3Jz6x79CRZbxGr9dzFokJLuvDDnyRh",
+]
+wallet_name = "No need for sig"
+error = "NOT_SUPPORTED"
+
+[[case]]
+name = "not_sane_malleable"
+description = "malleable miniscript policy"
+descriptor_template = "wsh(c:andor(ripemd160(6ad07d21fd5dfc646f0b30577045ce201616b9ba),pk_h(@0/**),and_v(v:hash256(8a35d9ca92a48eaade6f53a64985e9e2afeb74dcf8acb4c3721e0dc7e4294b25),pk_h(@1/**))))"
+keys_info = [
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ "tpubDDV6FDLcCieWUeN7R3vZK2Qs3KuQed3ScTY9EiwMXvyCkLjDbCb8RXaAgWDbkG4tW1BMKVF1zERHnyt78QKd4ZaAYGMJMpvHPwgSSU1AxZ3",
+]
+wallet_name = "Malleable"
+error = "NOT_SUPPORTED"
+
+[[case]]
+name = "too_many_derivation_steps"
+description = "key with 9 derivation steps exceeds the maximum (8)"
+descriptor_template = "wpkh(@0/**)"
+keys_info = [
+ "[f5acc2fd/48'/1'/0'/2'/0'/1'/2'/3'/4']tpubDR1MRwnrfYuCbZ5dvozygap7EkR1pRXfwKhfCEpMTHtzrtN78MnFeipS2Vq9Nr3WHSurdqmrPfm5yQH5ikxJhYUTK12VxmqfTKR8hUF8mty",
+]
+wallet_name = "Too many steps"
+error = "INCORRECT_DATA"
+
+[[case]]
+name = "too_many_keys"
+description = "16 key expressions exceed the maximum (15) supported in a wallet policy"
+descriptor_template = "wsh(multi(1,@0/**,@1/**,@2/**,@3/**,@4/**,@5/**,@6/**,@7/**,@8/**,@9/**,@10/**,@11/**,@12/**,@13/**,@14/**,@15/**))"
+keys_info = [
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+ "tpubDD814oESVM3uG2yZZDnJDex6gLeeahu2cmK2AL26ozTtDouQCY4NA8xcTW5oiJLp1cydFtDPdWxWDpLPGyLo6sGbemykJm4ocjjvt5cwvtv",
+ "tpubDCz2AbmcM5cVir5UiGjdPgtY6Tugwg44VZYJjpPxyZAg9vMsztGVjcgW9wkQF3oDF4mK2vRxzeXY7d31KYStsRQtuhPNpLFJgz3A3Np9Vc8",
+ "tpubDCbN8YbwHgkEVHc3kYfJirFiHaQaR9brzgF6KNhnceLe58hWiHMDXrovq3SBbHBcHHhNiXNSY7wKWRUGb3pYDTeEnVJBqthwG3FhgeuUQjb",
+ "tpubDD3GzZF6koScghQdMUZ1VT6UnNyvvhVGwwEaReiHSPrKxFguBhp6K2kpbKUwudodaQy44EDbUz4rLgGGN5XJmqCTrydxwY9sRmErUGmK8XA",
+ "tpubDDi6fvvXrjwNvm9tRrAowv5MmxajeptXACMAoVTGSS7dCfabntWJk6eaxFYTy3SLnphoLDs8wzkuQF54x5fiT63dYQ8FBQZWxVSVgNqZUCb",
+ "tpubDDtawMw95R6dzwnW6ptNwAHJVHQRS8vYCyXBVrycd6hnqDfPjBpyMxBbgFgZCdp5oX5L34Z42tvetavdvzcRubV7Yc6mh639uDjRvHDFTrB",
+ "tpubDCDtoPWjoKyu8BurUA6erB4hDBtN18xybX2mFSY1AWXy5qem2j79MbzGiJiNUZsqzLXUuoxjDhwCvACr8NSTukFLWPpgLZSS7KGawZSkTKQ",
+ "tpubDCXuqAmWrb4Zv2wRsjW3iaCN4EddaxFwMDnrfBaonLKDKvnkAzR5yar6CpAU6XiQpXJtirSfjJ1UjTs4ZnUXFpCM3Jr6Y8WgbPGhRgo7vXW",
+ "tpubDDpVtgEAwc9QGnxi6froduQcPHUFQKfz8XDRvjnwqXPTYQYzjTmPkepYQkD5vymqQanH2sMZsu6GYvfpzrPgvBiUUmpuYuWnotukE1h1zBb",
+ "tpubDDCK19rtBJsMt8Jb5eoJZKwF8C46HDY6qGrfuDSmtzb3nXA3X7h8DgQrTRbdaRMPziwo9CfS9xK8s8mDqAztqC2MTp1ZuK51Jr2DKweqkKY",
+ "tpubDCFwnN6jcEGzVsxTe7yX72zModdxsdAXi8C6t3xEqGKwkZd2ke2cExXgTNSbTM25FdzUxZXj6EhzLGZdPYtDk1hQbUgkak7HLJppn3erid2",
+ "tpubDDCaqm7sT9mSE1tQFcDnxrGvS7wJkVZ2UyL2daVVUVuWSSkco3u8pKyJSH7uRJKu48gudpXzWa8XqGzSPmbZN2SwPpkKE3GbXvpQB9euuNS",
+ "tpubDDXidKmnHQnn7XNBfV6ngDqyporatWXdUGSayXBTm1WQwQGrtcPb38zaZGo3y3K6KnD37dAnU66JWcLEY53F8zRKeBT7J5Y27ER5TTcDhr6",
+ "tpubDCyakepujvAQzi2S6eVYrC6ky5mBfHe3xE4fxj3FaT4Nt7TvmGW7w22kJ5EF4XR33oLc2PAGveNymx8tubaSZ4wA6ja7cnE3t4maND5ehep",
+ "tpubDDExGXv7gGXC7aXxQoajPixpn6o5BPLPYL3DFouZd3GmNwSi8cKBLn51y6Deh2jHcCDB88e4R51oqn5iK9pvsyUKV6BPieMCbCn8Qzd6GPs",
+]
+wallet_name = "Too many keys"
+error = "NOT_SUPPORTED"
+
+[[case]]
+name = "no_internal_key"
+description = "policy with only a foreign key (fingerprint never matches ours)"
+descriptor_template = "wpkh(@0/**)"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']tpubDD814oESVM3uG2yZZDnJDex6gLeeahu2cmK2AL26ozTtDouQCY4NA8xcTW5oiJLp1cydFtDPdWxWDpLPGyLo6sGbemykJm4ocjjvt5cwvtv",
+]
+wallet_name = "No internal key"
+error = "INCORRECT_DATA"
+
+[[case]]
+name = "no_internal_key_fp_collision"
+description = "key has our fingerprint but a foreign xpub; the re-derivation mismatch must keep it external (no internal key)"
+descriptor_template = "wpkh(@0/**)"
+keys_info = [
+ "[f5acc2fd/44'/1'/3']tpubDD814oESVM3uG2yZZDnJDex6gLeeahu2cmK2AL26ozTtDouQCY4NA8xcTW5oiJLp1cydFtDPdWxWDpLPGyLo6sGbemykJm4ocjjvt5cwvtv",
+]
+wallet_name = "Fingerprint collision"
+error = "INCORRECT_DATA"
+
diff --git a/test_vectors/sign_message.toml b/test_vectors/sign_message.toml
new file mode 100644
index 0000000..9a36ba2
--- /dev/null
+++ b/test_vectors/sign_message.toml
@@ -0,0 +1,49 @@
+# Test vectors for sign_message.
+#
+# Source: tests/test_sign_message.py
+#
+# `message` is the UTF-8 text to sign; `expected_signature` is the base64 result.
+# Pure UI reject flows (DENY) are not captured here.
+
+[[case]]
+name = "times_headline"
+message = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks."
+path = "m/44'/1'/0'/0/0"
+expected_signature = "IOR4YRVlmJGMx+H7PgQvHzWAF0HAgrUggQeRdnoWKpypfaAberpvF+XbOCM5Cd/ljogNyU3w2OIL8eYCyZ6Ru2k="
+
+[[case]]
+name = "times_headline_with_eol"
+message = "\nThe Times 03/Jan/2009 Chancellor\non brink of second bailout for banks."
+path = "m/44'/1'/0'/0/0"
+expected_signature = "ILMgIpFxqZwrB5bgR/seis2N48jS7pmHViKcRYisIPBdDjhAzGDGP3HiLPcrYMlEOXJvdJ9/Mud/+fslM2KtKaM="
+
+[[case]]
+name = "hello_world"
+message = "Hello world!"
+path = "m/84'/1'/0'/0/0"
+expected_signature = "IEOK4+JMK7FToR7XMzFCoAYh1nud1IKm9Wq3vXLSVk/lBay8rHCRp9bP6riyR5NDqXYyYf7cXgMQTHNz3SemwZI="
+
+[[case]]
+name = "long_multileaf"
+message = "The root problem with conventional currency is all the trust that's required to make it work. The central bank must be trusted not to debase the currency, but the history of fiat currencies is full of breaches of that trust. Banks must be trusted to hold our money and transfer it electronically, but they lend it out in waves of credit bubbles with barely a fraction in reserve. We have to trust them with our privacy, trust them not to let identity thieves drain our accounts. Their massive overhead costs make micropayments impossible."
+path = "m/84'/1'/0'/0/8"
+expected_signature = "H4frM6TYm5ty1MAf9o/Zz9Qiy3VEldAYFY91SJ/5nYMAZY1UUB97fiRjKW8mJit2+V4OCa1YCqjDqyFnD9Fw75k="
+
+[[case]]
+name = "non_ascii_carriage_return"
+message = "Hello\rworld!"
+path = "m/84'/1'/0'/0/8"
+expected_signature = "ILXQPJapQ/OEy9f/ggRouI6HuAleQveIOBphNUCLlWNVQAml2Yx+885tsU0DKIxggsVq53o/fQRlosTPTQE/keE="
+
+[[case]]
+name = "too_long_to_display"
+message = "The root problem with conventional currency is all the trust that's required to make it work. The central bank must be trusted not to debase the currency, but the history of fiat currencies is full of breaches of that trust. Banks must be trusted to hold our money and transfer it electronically, but they lend it out in waves of credit bubbles with barely a fraction in reserve. We have to trust them with our privacy, trust them not to let identity thieves drain our accounts. Their massive overhead costs make micropayments impossible. The root problem with conventional currency is all the trust that's required to make it work. The central bank must be trusted not to debase the currency, but the history of fiat currencies is full of breaches of that trust. Banks must be trusted to hold our money and transfer it electronically, but they lend it out in waves of credit bubbles with barely a fraction in reserve. We have to trust them with our privacy, trust them not to let identity thieves drain our accounts. Their massive overhead costs make micropayments impossible. The root problem with conventional currency is all the trust that's required to make it work. The central bank must be trusted not to debase the currency, but the history of fiat currencies is full of breaches of that trust. Banks must be trusted to hold our money and transfer it electronically, but they lend it out in waves of credit bubbles with barely a fraction in reserve. We have to trust them with our privacy, trust them not to let identity thieves drain our accounts. Their massive overhead costs make micropayments impossible."
+path = "m/84'/1'/0'/0/8"
+expected_signature = "IDAl9RThAyunmYuol9DaDs/CScUpiol3FDSjIjyK9y0tc/x1HWrbT/ufdkPFY1Bmi+L9hc3ip1me2RmufprVuNk="
+
+# Path with 11 derivation steps exceeds the maximum (10): WRONG_DATA_LENGTH.
+[[case]]
+name = "too_long_bip32_path"
+message = "Too long derivation path test"
+path = "m/44'/1'/0'/0'/1'/2'/3'/4'/5/6/7"
+error = "WRONG_DATA_LENGTH"
diff --git a/test_vectors/sign_psbt.toml b/test_vectors/sign_psbt.toml
new file mode 100644
index 0000000..4ff48c1
--- /dev/null
+++ b/test_vectors/sign_psbt.toml
@@ -0,0 +1,293 @@
+# Test vectors for sign_psbt.
+#
+# Schema and conventions are documented in test_vectors/README.md.
+# Sources: tests/test_sign_psbt.py and tests/test_sign_psbt_with_sighash_types.py.
+#
+# Each case references a PSBT fixture (psbt_file, relative to tests/) plus a wallet
+# policy block, and lists the expected signatures per input. For each signature:
+# - `sighash_type` is the SIGHASH flag the input is signed with (always present);
+# - deterministic ECDSA signatures carry exact `signature` bytes;
+# - taproot Schnorr signatures (non-deterministic) instead carry the `sighash`
+# digest the signature must verify against (BIP-340), since the produced bytes
+# are not byte-stable.
+#
+# Randomized PSBTs (txmaker), PSBT-mutation tests, and pure UI flows stay as
+# bespoke code in each framework.
+
+[[case]]
+name = "singlesig_pkh_1to1"
+description = "legacy P2PKH, 1 input 1 output"
+psbt_file = "psbt/singlesig/pkh-1to1.psbt"
+descriptor_template = "pkh(@0/**)"
+keys_info = [
+ "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT",
+]
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "02ee8608207e21028426f69e76447d7e3d5e077049f5e683c3136c2314762a4718"
+sighash_type = 1
+signature = "3045022100e55b3ca788721aae8def2eadff710e524ffe8c9dec1764fdaa89584f9726e196022012a30fbcf9e1a24df31a1010356b794ab8de438b4250684757ed5772402540f401"
+
+[[case]]
+name = "singlesig_sh_wpkh_1to2"
+description = "wrapped segwit P2SH-P2WPKH, 1 input 2 outputs"
+psbt_file = "psbt/singlesig/sh-wpkh-1to2.psbt"
+descriptor_template = "sh(wpkh(@0/**))"
+keys_info = [
+ "[f5acc2fd/49'/1'/0']tpubDC871vGLAiKPcwAw22EjhKVLk5L98UGXBEcGR8gpcigLQVDDfgcYW24QBEyTHTSFEjgJgbaHU8CdRi9vmG4cPm1kPLmZhJEP17FMBdNheh3",
+]
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "024ba3b77d933de9fa3f9583348c40f3caaf2effad5b6e244ece8abbfcc7244f67"
+sighash_type = 1
+signature = "30440220720722b08489c2a50d10edea8e21880086c8e8f22889a16815e306daeea4665b02203fcf453fa490b76cf4f929714065fc90a519b7b97ab18914f9451b5a4b45241201"
+
+[[case]]
+name = "singlesig_wpkh_1to2"
+description = "native segwit P2WPKH, 1 input 2 outputs"
+psbt_file = "psbt/singlesig/wpkh-1to2.psbt"
+descriptor_template = "wpkh(@0/**)"
+keys_info = [
+ "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P",
+]
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "03ee2c3d98eb1f93c0a1aa8e5a4009b70eb7b44ead15f1666f136b012ad58d3068"
+sighash_type = 1
+signature = "3045022100ab44f34dd7e87c9054591297a101e8500a0641d1d591878d0d23cf8096fa79e802205d12d1062d925e27b57bdcf994ecf332ad0a8e67b8fe407bab2101255da632aa01"
+
+[[case]]
+name = "singlesig_wpkh_2to2"
+description = "native segwit P2WPKH, 2 inputs 2 outputs"
+psbt_file = "psbt/singlesig/wpkh-2to2.psbt"
+descriptor_template = "wpkh(@0/**)"
+keys_info = [
+ "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P",
+]
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "03455ee7cedc97b0ba435b80066fc92c963a34c600317981d135330c4ee43ac7a3"
+sighash_type = 1
+signature = "304402206b3e877655f08c6e7b1b74d6d893a82cdf799f68a5ae7cecae63a71b0339e5ce022019b94aa3fb6635956e109f3d89c996b1bfbbaf3c619134b5a302badfaf52180e01"
+
+[[case.expected_signatures]]
+input_index = 1
+pubkey = "0271b5b779ad870838587797bcf6f0c7aec5abe76a709d724f48d2e26cf874f0a0"
+sighash_type = 1
+signature = "3045022100e2e98e4f8c70274f10145c89a5d86e216d0376bdf9f42f829e4315ea67d79d210220743589fd4f55e540540a976a5af58acd610fa5e188a5096dfe7d36baf3afb94001"
+
+[[case]]
+name = "multisig_wsh_2of2"
+description = "native segwit 2-of-2 sortedmulti"
+psbt_file = "psbt/multisig/wsh-2of2.psbt"
+descriptor_template = "wsh(sortedmulti(2,@0/**,@1/**))"
+keys_info = [
+ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF",
+ "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK",
+]
+wallet_name = "Cold storage"
+wallet_hmac = "d7c7a60b4ab4a14c1bf8901ba627d72140b2fb907f2b4e35d2e693bce9fbb371"
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "036b16e8c1f979fa4cc0f05b6a300affff941459b6f20de77de55b0160ef8e4cac"
+sighash_type = 1
+signature = "304402206ab297c83ab66e573723892061d827c5ac0150e2044fed7ed34742fedbcfb26e0220319cdf4eaddff63fc308cdf53e225ea034024ef96de03fd0939b6deeea1e8bd301"
+
+[[case]]
+name = "singlesig_tr_1to2_sighash_all"
+description = "taproot keypath, SIGHASH_ALL (verify against digest)"
+psbt_file = "psbt/singlesig/tr-1to2-sighash-all.psbt"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "d8f4f1d18996db61bb1caf5423de25d8a1eee9c950d37e6ffa023012d5e62e0d"
+sighash_type = 1
+sighash = "7A999E5AD6F53EA6448E7026061D3B4523F957999C430A5A492DFACE74AE31B6"
+
+[[case]]
+name = "singlesig_tr_1to2_sighash_default"
+description = "taproot keypath, SIGHASH_DEFAULT (64-byte sig, verify against digest)"
+psbt_file = "psbt/singlesig/tr-1to2-sighash-default.psbt"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "89904a6348d7ff9a44d49db8b6a807c622e868364c547da426f30b5a6213186a"
+sighash_type = 0
+sighash = "75C96FB06A12DB4CD011D8C95A5995DB758A4F2837A22F30F0F579619A4466F3"
+
+[[case]]
+name = "singlesig_tr_1to2_sighash_omitted"
+description = "taproot keypath, SIGHASH_DEFAULT (64-byte sig, verify against digest)"
+psbt_file = "psbt/singlesig/tr-1to2-sighash-omitted.psbt"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "89904a6348d7ff9a44d49db8b6a807c622e868364c547da426f30b5a6213186a"
+sighash_type = 0
+sighash = "75C96FB06A12DB4CD011D8C95A5995DB758A4F2837A22F30F0F579619A4466F3"
+
+[[case]]
+name = "sighash_all"
+description = "taproot keypath, sighash flag 0x01, 2 inputs (verify against digests)"
+psbt_file = "psbt/sighash/sighash-all-sign.psbt"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "0b8e7486bc306a0aefb7a7c00af0405542db1acd73eecca82771ccf5fe5eb60f"
+sighash_type = 1
+sighash = "2221AA462110C77A8E2DD34C3681BAA9BFFF6553B4C609EC7E3D8FF9B1D18D69"
+
+[[case.expected_signatures]]
+input_index = 1
+pubkey = "41461489a43cb67e2c64f39427278edd30c03d6d72a4b89986147c428f0cc5f1"
+sighash_type = 1
+sighash = "D47D3FA22B4F6C50521C49E1A42E8CB10689540A227491A8FC5AD0A6E413063E"
+
+[[case]]
+name = "sighash_none"
+description = "taproot keypath, sighash flag 0x02, 2 inputs (verify against digests)"
+psbt_file = "psbt/sighash/sighash-none-sign.psbt"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "0b8e7486bc306a0aefb7a7c00af0405542db1acd73eecca82771ccf5fe5eb60f"
+sighash_type = 2
+sighash = "965976D58A387369D970F0B6560B144E1B721D41E04675592C41AC35D30D2A56"
+
+[[case.expected_signatures]]
+input_index = 1
+pubkey = "41461489a43cb67e2c64f39427278edd30c03d6d72a4b89986147c428f0cc5f1"
+sighash_type = 2
+sighash = "67E85534A12E4054F4AFAA434D7A7C38123DA6909DF7E45DDB9945F7B8D832D0"
+
+[[case]]
+name = "sighash_single"
+description = "taproot keypath, sighash flag 0x03, 2 inputs (verify against digests)"
+psbt_file = "psbt/sighash/sighash-single-sign.psbt"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "0b8e7486bc306a0aefb7a7c00af0405542db1acd73eecca82771ccf5fe5eb60f"
+sighash_type = 3
+sighash = "F9B834D7FE272F9EACE2FC5F7A97468B024438EF5D55338FC243D5273534A6B5"
+
+[[case.expected_signatures]]
+input_index = 1
+pubkey = "41461489a43cb67e2c64f39427278edd30c03d6d72a4b89986147c428f0cc5f1"
+sighash_type = 3
+sighash = "9A4DDC13C6D0EE10A41D33C6595C63F51AF4C9314387685304F515F790260F78"
+
+[[case]]
+name = "sighash_all_anyonecanpay"
+description = "taproot keypath, sighash flag 0x81, 2 inputs (verify against digests)"
+psbt_file = "psbt/sighash/sighash-all-anyone-can-pay-sign.psbt"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "0b8e7486bc306a0aefb7a7c00af0405542db1acd73eecca82771ccf5fe5eb60f"
+sighash_type = 129
+sighash = "09A6559AF84C48C8D5A7984C5A72E53ED88D160AABAE99C18F00E78A55E7EDC7"
+
+[[case.expected_signatures]]
+input_index = 1
+pubkey = "41461489a43cb67e2c64f39427278edd30c03d6d72a4b89986147c428f0cc5f1"
+sighash_type = 129
+sighash = "9B25C319E12F4755D8A43F3295B8C61B861FB23D7EBF7F9A25E6E8CE3242F939"
+
+[[case]]
+name = "sighash_none_anyonecanpay"
+description = "taproot keypath, sighash flag 0x82, 2 inputs (verify against digests)"
+psbt_file = "psbt/sighash/sighash-none-anyone-can-pay-sign.psbt"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "0b8e7486bc306a0aefb7a7c00af0405542db1acd73eecca82771ccf5fe5eb60f"
+sighash_type = 130
+sighash = "8FCEFFAE04D320E05DE04034069FE6AF8C7CBCC93CDE3F187AB0DEC202692735"
+
+[[case.expected_signatures]]
+input_index = 1
+pubkey = "41461489a43cb67e2c64f39427278edd30c03d6d72a4b89986147c428f0cc5f1"
+sighash_type = 130
+sighash = "A06D37C1C8EEE7EA145F9D8A98CBE79F6BB1691B37F8F26F49F8318F9443B766"
+
+[[case]]
+name = "sighash_single_anyonecanpay"
+description = "taproot keypath, sighash flag 0x83, 2 inputs (verify against digests)"
+psbt_file = "psbt/sighash/sighash-single-anyone-can-pay-sign.psbt"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+
+[[case.expected_signatures]]
+input_index = 0
+pubkey = "0b8e7486bc306a0aefb7a7c00af0405542db1acd73eecca82771ccf5fe5eb60f"
+sighash_type = 131
+sighash = "971886B247797E0A616489B449B5E78AE8EC63E54B55727AF626B964DD8F329D"
+
+[[case.expected_signatures]]
+input_index = 1
+pubkey = "41461489a43cb67e2c64f39427278edd30c03d6d72a4b89986147c428f0cc5f1"
+sighash_type = 131
+sighash = "6B130F2BE5467A8BC36227B8C2A082B46CA24F91A6A6A54AA5EFA4901BE5ADBB"
+
+# ---- Deterministic rejections ----
+
+[[case]]
+name = "sighash_single_unallowed_3ins_2outs"
+description = "SIGHASH_SINGLE with input index >= number of outputs is rejected"
+psbt_file = "psbt/sighash/sighash-single-3-ins-2-outs.psbt"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+error = "NOT_SUPPORTED"
+
+[[case]]
+name = "sighash_unsupported_type"
+description = "unsupported sighash type is rejected"
+psbt_file = "psbt/sighash/sighash-unsupported.psbt"
+descriptor_template = "tr(@0/**)"
+keys_info = [
+ "[f5acc2fd/86'/1'/0']tpubDDKYE6BREvDsSWMazgHoyQWiJwYaDDYPbCFjYxN3HFXJP5fokeiK4hwK5tTLBNEDBwrDXn8cQ4v9b2xdW62Xr5yxoQdMu1v6c7UDXYVH27U",
+]
+error = "NOT_SUPPORTED"
+
diff --git a/unit-tests/CMakeLists.txt b/unit-tests/CMakeLists.txt
index 6d1e477..a727efa 100644
--- a/unit-tests/CMakeLists.txt
+++ b/unit-tests/CMakeLists.txt
@@ -332,12 +332,12 @@ if(SPECULOS AND SPECULOS_SRC)
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
- # TOML file (see test_get_wallet_address_vectors.toml).
+ # end-to-end against a mock dispatcher; vectors come from the test assets
+ # in the repository root (see ../test_vectors/get_wallet_address.toml).
add_executable(test_get_wallet_address test_get_wallet_address.c)
app_apply_real_sdk_config(test_get_wallet_address)
target_compile_definitions(test_get_wallet_address PRIVATE
- TEST_VECTORS_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/test_get_wallet_address_vectors.toml\"
+ TEST_VECTORS_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/../test_vectors/get_wallet_address.toml\"
)
target_link_libraries(test_get_wallet_address PRIVATE
cmocka mock_dispatcher app_crypto buffer buffer_ext tomlc17
diff --git a/unit-tests/test_get_wallet_address_vectors.toml b/unit-tests/test_get_wallet_address_vectors.toml
deleted file mode 100644
index 28b31d1..0000000
--- a/unit-tests/test_get_wallet_address_vectors.toml
+++ /dev/null
@@ -1,226 +0,0 @@
-# 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.