Skip to main content

md_codec/
decode.rs

1//! Top-level decoder per spec §13.2.
2
3use crate::bitstream::BitReader;
4use crate::encode::Descriptor;
5use crate::error::{ContextKind, Error};
6use crate::header::Header;
7use crate::origin_path::PathDecl;
8use crate::tag::Tag;
9use crate::tlv::TlvSection;
10use crate::tree::read_node;
11use crate::use_site_path::UseSitePath;
12
13/// Decode a Descriptor from the canonical payload bit stream.
14/// `bytes` may be zero-padded; `total_bits` is the exact payload bit count.
15pub fn decode_payload(bytes: &[u8], total_bits: usize) -> Result<Descriptor, Error> {
16    let mut r = BitReader::with_bit_limit(bytes, total_bits);
17
18    let header = Header::read(&mut r)?;
19    let path_decl = PathDecl::read(&mut r, header.divergent_paths)?;
20    let use_site_path = UseSitePath::read(&mut r)?;
21    // SPEC v0.30 §7 width formula: ⌈log₂(n)⌉. v0.30 drops the +1 v0.18 used
22    // to reserve the NUMS sentinel slot — NUMS is now signalled by an
23    // explicit `is_nums` bit on Body::Tr. MUST mirror
24    // `Descriptor::key_index_width` exactly; a stale formula silently
25    // desyncs the bitstream.
26    let key_index_width = (32 - (path_decl.n as u32).saturating_sub(1).leading_zeros()) as u8;
27    let tree = read_node(&mut r, key_index_width)?;
28
29    // SPEC §11: root tag MUST be in {Sh, Wsh, Wpkh, Pkh, Tr} (the wrapper-tag
30    // allow-list — structural body validation for `Sh`/`Wsh` is separate).
31    // Decoder-side hardening (defense in depth) — the parser-side enforces this
32    // for CLI/template inputs; this catches malformed wires that bypass the
33    // parser via direct bitstream construction. Note: `Sh` covers both
34    // `sh(multi)` and `sh(wsh(multi))` which are distinct BIP-388 shapes sharing
35    // the same root tag; per-shape validation happens at the policy layer.
36    if !matches!(
37        tree.tag,
38        Tag::Sh | Tag::Wsh | Tag::Wpkh | Tag::Pkh | Tag::Tr
39    ) {
40        return Err(Error::OperatorContextViolation {
41            tag: tree.tag,
42            context: ContextKind::TopLevel,
43        });
44    }
45
46    let tlv = TlvSection::read(&mut r, key_index_width, path_decl.n)?;
47
48    let descriptor = Descriptor {
49        n: path_decl.n,
50        path_decl,
51        use_site_path,
52        tree,
53        tlv,
54    };
55
56    crate::validate::validate_placeholder_usage(&descriptor.tree, descriptor.n)?;
57    if let Some(overrides) = &descriptor.tlv.use_site_path_overrides {
58        crate::validate::validate_multipath_consistency(&descriptor.use_site_path, overrides)?;
59        // D5(a): reject non-canonical override shapes (an `@0` override, or a
60        // redundant override equal to the baseline) — never emitted by our
61        // encoders; defense-in-depth against hand-crafted wire.
62        crate::validate::validate_use_site_overrides_canonical(
63            &descriptor.use_site_path,
64            overrides,
65        )?;
66    }
67    if matches!(descriptor.tree.tag, crate::tag::Tag::Tr) {
68        if let crate::tree::Body::Tr { tree: Some(t), .. } = &descriptor.tree.body {
69            crate::validate::validate_tap_script_tree(t)?;
70        }
71    }
72    // Spec v0.13 §6.3 + §6.4: enforce explicit-origin and xpub-validity
73    // after the v0.11 ordering / multipath / taptree checks. Order matters:
74    // ordering must run first so subsequent checks see canonical indices.
75    crate::validate::validate_explicit_origin_required(&descriptor)?;
76    crate::validate::validate_xpub_bytes(&descriptor)?;
77
78    Ok(descriptor)
79}
80
81/// Decode a Descriptor from a complete codex32 md1 string.
82///
83/// Uses the symbol-aligned bit count returned by `unwrap_string` (5 × symbol_count),
84/// which is exact at the codex32 layer with ≤4 bits of trailing zero-padding —
85/// well within the v11 decoder's TLV-rollback tolerance.
86///
87/// F-A2: in-band auto-dispatch per SPEC v0.30 §2.3. The chunked-flag lives in
88/// bit 0 (LSB) of the first 5-bit symbol (`[v3][v2][v1][v0][chunked]` for a
89/// chunk header vs `[divergent][v3][v2][v1][v0]` for a single payload). When
90/// set, the string is a chunk-form md1 and MUST route through the
91/// chunk-reassembly path (a 1-element set) rather than the single-payload
92/// primitive `decode_payload` — mirroring `decode_with_correction`'s
93/// single-string auto-dispatch. The usable single-payload version set {4,8,12}
94/// is all-even ⇒ every currently-valid single-payload string has first-symbol
95/// LSB = 0, so this dispatch never diverts an input that decodes today. No
96/// recursion cycle: `reassemble` → `decode_payload`, never back to here.
97pub fn decode_md1_string(s: &str) -> Result<Descriptor, Error> {
98    let (bytes, symbol_aligned_bit_count) = crate::codex32::unwrap_string(s)?;
99    // The first symbol occupies the top 5 bits of byte 0 (MSB-first packing),
100    // so its LSB (the chunked-flag) is bit 3 of byte 0.
101    let chunked_flag = bytes.first().map(|b| (b >> 3) & 0x01).unwrap_or(0);
102    if chunked_flag == 1 {
103        return crate::chunk::reassemble(&[s]);
104    }
105    decode_payload(&bytes, symbol_aligned_bit_count)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::encode::encode_payload;
112    use crate::origin_path::{OriginPath, PathComponent, PathDeclPaths};
113    use crate::tlv::TlvSection;
114    use crate::tree::{Body, Node};
115
116    /// SPEC §11 TopLevel check: a wire payload whose root tag is outside the
117    /// BIP-388 allow-list `{Sh, Wsh, Wpkh, Pkh, Tr}` must be rejected with
118    /// `Error::OperatorContextViolation { context: ContextKind::TopLevel }`.
119    /// The encoder has no root-tag gate (only placeholder/multipath/taptree
120    /// validators run), so `encode_payload` of an AndV-rooted descriptor
121    /// succeeds and round-trips through `decode_payload` exposes the gap.
122    #[test]
123    fn decode_rejects_non_canonical_root_tag() {
124        // The TopLevel check fires in `decode_payload` before any downstream
125        // validator runs, so this test reaches the rejection regardless of
126        // whether path_decl would satisfy `validate_explicit_origin_required`
127        // (it does, but the check is short-circuited above). path_decl is
128        // populated here to mirror a realistic descriptor shape.
129        let d = Descriptor {
130            n: 1,
131            path_decl: PathDecl {
132                n: 1,
133                paths: PathDeclPaths::Shared(OriginPath {
134                    components: vec![PathComponent {
135                        hardened: true,
136                        value: 84,
137                    }],
138                }),
139            },
140            use_site_path: UseSitePath::standard_multipath(),
141            tree: Node {
142                tag: Tag::AndV,
143                body: Body::Children(vec![
144                    Node {
145                        tag: Tag::PkK,
146                        body: Body::KeyArg { index: 0 },
147                    },
148                    Node {
149                        tag: Tag::PkK,
150                        body: Body::KeyArg { index: 0 },
151                    },
152                ]),
153            },
154            tlv: TlvSection::new_empty(),
155        };
156        let (bytes, total_bits) = encode_payload(&d).expect("encode AndV-rooted ok");
157        let err = decode_payload(&bytes, total_bits).expect_err("decode must reject");
158        assert!(
159            matches!(
160                err,
161                Error::OperatorContextViolation {
162                    tag: Tag::AndV,
163                    context: ContextKind::TopLevel,
164                }
165            ),
166            "expected OperatorContextViolation{{TopLevel}}, got {err:?}"
167        );
168    }
169}