Skip to main content

md_codec/
test_vectors.rs

1//! Canonical `md` test-vector corpus.
2//!
3//! Used by `md-codec`'s own integration tests, by `md-cli`'s `vectors`
4//! subcommand, and by `md-cli`'s `tests/json_snapshots.rs` /
5//! `tests/template_roundtrip.rs`. Single source of truth: any vector
6//! addition / removal / rename happens here.
7//!
8//! `Vector` is `#[non_exhaustive]` so future fields can be added without a
9//! breaking-change bump: external consumers construct nothing — they only
10//! read `MANIFEST` entries.
11
12/// One entry of the canonical test-vector corpus.
13#[non_exhaustive]
14pub struct Vector {
15    /// Vector identifier — used in test failure messages and as a stable
16    /// handle for cross-suite filtering. Convention: snake_case mirroring
17    /// the wallet-policy template's distinguishing structure.
18    pub name: &'static str,
19    /// BIP-388 wallet-policy template string the vector encodes. Parsed
20    /// by `parse::template`; round-tripped through `encode` and `decode`.
21    pub template: &'static str,
22    /// `(@N, xpub)` pairs binding each `@N` placeholder in `template`. Empty
23    /// when the vector exercises template-only paths (no key binding).
24    pub keys: &'static [(u8, &'static str)],
25    /// `(@N, 4-byte master fingerprint)` pairs aligned with `keys`. Empty
26    /// when the vector does not exercise fingerprint round-tripping.
27    pub fingerprints: &'static [(u8, [u8; 4])],
28    /// When true, force the encoder onto the chunked wire path even if the
29    /// payload would fit in a single chunk. Exercises chunk-boundary logic
30    /// without padding the template artificially.
31    pub force_chunked: bool,
32    /// Explicit shared origin path applied via the encoder's `--path`
33    /// override (`m/...` literal, or a named `bip44|48|49|84|86` form).
34    /// `None` = elided origin — the encoder infers the canonical origin via
35    /// `canonical_origin`. `Some(..)` is REQUIRED for non-canonical shapes
36    /// (`tr()` + TapTree, NUMS-taproot) whose `canonical_origin` returns
37    /// `None`: without an explicit origin they mint a card the decoder
38    /// rejects with `MissingExplicitOrigin`. Carried into the emitted
39    /// `.descriptor.json` via `path_decl` (the `.template` file alone does
40    /// not determine it), so the BIP §Test Vectors table pins the
41    /// template+path pair for path-carrying rows.
42    pub path: Option<&'static str>,
43}
44
45/// The canonical 15-entry corpus.
46///
47/// Part-3 additions (BIP-alignment cycle): `sh_wpkh`, `tr_with_leaf`,
48/// `nums_taproot`, `wsh_sortedmulti_2chunk`, and `single_string_boundary`.
49///
50/// * `sh_wpkh` — un-omitted: since F-A1, `sh(wpkh)` round-trips symmetrically
51///   in ELIDED form (`canonical_origin(sh(wpkh))` = `m/49'/0'/0'`), so it is a
52///   corpus ADDITION (`path: None`), not an asymmetric omission.
53/// * `tr_with_leaf` / `nums_taproot` — non-canonical `tr()` shapes
54///   (`canonical_origin` = `None`); expressible now via the `path` field,
55///   which supplies the explicit origin the decoder requires.
56/// * `wsh_sortedmulti_2chunk` — a genuine 2-member chunk set: a 2-of-8
57///   sortedmulti with a master fingerprint on every cosigner makes a 376-bit
58///   payload that still FITS a single string (below the 400-bit / 80-symbol
59///   regular-code cap), so it is `force_chunked` to route it through `split`,
60///   whose 320-bit per-chunk budget then yields two chunks. (Contrast
61///   `wsh_multi_chunked`, also `force_chunked` but a chunk-set-of-one.)
62/// * `single_string_boundary` (F-V2) — a 2-of-9 sortedmulti with fingerprints
63///   on 8 of the 9 cosigners, sized so its single-string regular-code emit
64///   lands at 79 of the 80 max data symbols (95 chars total = `md1` plus 79
65///   data plus 13 checksum). Proves the regular-only single-string boundary
66///   holds right at the codex32 BCH(93,80) cap — NOT chunked, NOT long-code.
67#[rustfmt::skip]
68pub const MANIFEST: &[Vector] = &[
69    Vector { name: "wpkh_basic",         template: "wpkh(@0/<0;1>/*)",                                   keys: &[], fingerprints: &[], force_chunked: false, path: None },
70    Vector { name: "pkh_basic",          template: "pkh(@0/<0;1>/*)",                                    keys: &[], fingerprints: &[], force_chunked: false, path: None },
71    Vector { name: "wsh_multi_2of2",     template: "wsh(multi(2,@0/<0;1>/*,@1/<0;1>/*))",                keys: &[], fingerprints: &[], force_chunked: false, path: None },
72    Vector { name: "wsh_multi_2of3",     template: "wsh(multi(2,@0/<0;1>/*,@1/<0;1>/*,@2/<0;1>/*))",     keys: &[], fingerprints: &[], force_chunked: false, path: None },
73    Vector { name: "wsh_sortedmulti",    template: "wsh(sortedmulti(2,@0/<0;1>/*,@1/<0;1>/*,@2/<0;1>/*))", keys: &[], fingerprints: &[], force_chunked: false, path: None },
74    Vector { name: "tr_keyonly",         template: "tr(@0/<0;1>/*)",                                     keys: &[], fingerprints: &[], force_chunked: false, path: None },
75    Vector { name: "sh_wsh_multi",       template: "sh(wsh(multi(2,@0/<0;1>/*,@1/<0;1>/*)))",            keys: &[], fingerprints: &[], force_chunked: false, path: None },
76    Vector { name: "wsh_divergent_paths", template: "wsh(multi(2,@0/<0;1>/*,@1/<2;3>/*))",               keys: &[], fingerprints: &[], force_chunked: false, path: None },
77    Vector { name: "wsh_with_fingerprints", template: "wsh(multi(2,@0/<0;1>/*,@1/<0;1>/*))",
78        keys: &[],
79        fingerprints: &[(0, [0xDE,0xAD,0xBE,0xEF]), (1, [0xCA,0xFE,0xBA,0xBE])],
80        force_chunked: false, path: None },
81    Vector { name: "wsh_multi_chunked",  template: "wsh(multi(3,@0/<0;1>/*,@1/<0;1>/*,@2/<0;1>/*))",     keys: &[], fingerprints: &[], force_chunked: true, path: None },
82    // --- Part-3 additions ---
83    // F-A1: elided sh(wpkh) now round-trips (canonical origin m/49'/0'/0').
84    Vector { name: "sh_wpkh",            template: "sh(wpkh(@0/<0;1>/*))",                               keys: &[], fingerprints: &[], force_chunked: false, path: None },
85    // Non-canonical tr()+leaf — explicit origin via the new `path` field.
86    Vector { name: "tr_with_leaf",       template: "tr(@0/<0;1>/*,pk(@1/<0;1>/*))",                      keys: &[], fingerprints: &[], force_chunked: false, path: Some("48'/0'/0'/2'") },
87    // NUMS-taproot (`is_nums = 1` wire path) — script-path-only tr, explicit origin.
88    Vector { name: "nums_taproot",       template: "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,multi_a(2,@0/<0;1>/*,@1/<0;1>/*,@2/<0;1>/*))",
89        keys: &[], fingerprints: &[], force_chunked: false, path: Some("48'/0'/0'/2'") },
90    // 2-of-8 sortedmulti + fingerprint on every cosigner: a 376-bit payload
91    // that FITS a single string (< the 400-bit regular-code cap), so
92    // `force_chunked` routes it through `split`, whose 320-bit per-chunk budget
93    // yields a genuine 2-member chunk set.
94    Vector { name: "wsh_sortedmulti_2chunk",
95        template: "wsh(sortedmulti(2,@0/<0;1>/*,@1/<0;1>/*,@2/<0;1>/*,@3/<0;1>/*,@4/<0;1>/*,@5/<0;1>/*,@6/<0;1>/*,@7/<0;1>/*))",
96        keys: &[],
97        fingerprints: &[
98            (0, [0x01,0x02,0x03,0x04]), (1, [0x02,0x03,0x04,0x05]),
99            (2, [0x03,0x04,0x05,0x06]), (3, [0x04,0x05,0x06,0x07]),
100            (4, [0x05,0x06,0x07,0x08]), (5, [0x06,0x07,0x08,0x09]),
101            (6, [0x07,0x08,0x09,0x0A]), (7, [0x08,0x09,0x0A,0x0B]),
102        ],
103        force_chunked: true, path: None },
104    // F-V2: single-string regular-code boundary. 2-of-9 sortedmulti with a
105    // fingerprint on 8 of the 9 cosigners sizes the payload to 79 of the 80
106    // max data symbols → a single 95-char md1 string (NOT chunked, NOT long).
107    Vector { name: "single_string_boundary",
108        template: "wsh(sortedmulti(2,@0/<0;1>/*,@1/<0;1>/*,@2/<0;1>/*,@3/<0;1>/*,@4/<0;1>/*,@5/<0;1>/*,@6/<0;1>/*,@7/<0;1>/*,@8/<0;1>/*))",
109        keys: &[],
110        fingerprints: &[
111            (0, [0x01,0x02,0x03,0x04]), (1, [0x02,0x03,0x04,0x05]),
112            (2, [0x03,0x04,0x05,0x06]), (3, [0x04,0x05,0x06,0x07]),
113            (4, [0x05,0x06,0x07,0x08]), (5, [0x06,0x07,0x08,0x09]),
114            (6, [0x07,0x08,0x09,0x0A]), (7, [0x08,0x09,0x0A,0x0B]),
115        ],
116        force_chunked: false, path: None },
117];