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/// Options controlling decode-time validation strictness (P0 pathless/
14/// dead-card partial-decode).
15///
16/// The default (`allow_unresolved_origin: false`) is BYTE-IDENTICAL to
17/// pre-P0 decode behavior: `Error::MissingExplicitOrigin` is raised for
18/// any `@N` whose origin cannot be resolved (no canonical default AND no
19/// explicit `path_decl`/override). When `true`, that ONE reject is
20/// swallowed and the decode succeeds; the caller MUST query
21/// [`crate::encode::Descriptor::unresolved_origin_indices`] on the
22/// returned descriptor to learn which `@N` are unresolved (nothing extra
23/// is recorded on the `Descriptor` itself).
24///
25/// INVARIANT (funds-load-bearing): this flag relaxes ONLY the
26/// `validate_explicit_origin_required` outcome. No other decode check is
27/// affected — placeholder-usage, multipath consistency, tap-script-tree
28/// leaf validity, xpub-bytes validity, and (via
29/// [`crate::chunk::reassemble_with_opts`]) per-chunk BCH, chunk-header
30/// consistency, index-gap, and the derived-chunk-set-id / content-id
31/// check all stay enforced regardless of this flag. The
32/// `Error::EmptyOriginOverride` reject (P0.3) is likewise a DISTINCT,
33/// always-fatal error class — never swallowed by this opt-in, even when
34/// `allow_unresolved_origin` is `true` (fatal-in-partial).
35///
36/// `#[non_exhaustive]` (API-freeze, M-2): a future decode-relaxation option
37/// can be added as a new field without a SemVer-major bump. Downstream
38/// crates therefore cannot construct this with a struct literal — use
39/// [`DecodeOpts::default`] (strict) or [`DecodeOpts::partial`]
40/// (partial-allowing) instead.
41#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
42#[non_exhaustive]
43pub struct DecodeOpts {
44    /// When `true`, `Error::MissingExplicitOrigin` is not raised at
45    /// decode time. Default `false` (strict — today's behavior).
46    pub allow_unresolved_origin: bool,
47}
48
49impl DecodeOpts {
50    /// Partial-allowing decode options (`allow_unresolved_origin: true`),
51    /// all other (future) options at their `Default`. The stable
52    /// constructor for the render-path opt-in (`md decode`/`md inspect`,
53    /// toolkit `verify-bundle` gate) — preferred over a struct literal
54    /// since `DecodeOpts` is `#[non_exhaustive]`.
55    pub fn partial() -> Self {
56        Self {
57            allow_unresolved_origin: true,
58        }
59    }
60}
61
62/// Decode a Descriptor from the canonical payload bit stream (strict:
63/// byte-identical to pre-P0 behavior). Delegates to
64/// [`decode_payload_with_opts`] with the default (strict) options.
65/// `bytes` may be zero-padded; `total_bits` is the exact payload bit count.
66pub fn decode_payload(bytes: &[u8], total_bits: usize) -> Result<Descriptor, Error> {
67    decode_payload_with_opts(bytes, total_bits, DecodeOpts::default())
68}
69
70/// Decode a Descriptor from the canonical payload bit stream, honoring
71/// `opts` (P0 partial-decode; see [`DecodeOpts`] for the contract).
72/// `bytes` may be zero-padded; `total_bits` is the exact payload bit count.
73pub fn decode_payload_with_opts(
74    bytes: &[u8],
75    total_bits: usize,
76    opts: DecodeOpts,
77) -> Result<Descriptor, Error> {
78    let mut r = BitReader::with_bit_limit(bytes, total_bits);
79
80    let header = Header::read(&mut r)?;
81    let path_decl = PathDecl::read(&mut r, header.divergent_paths)?;
82    let use_site_path = UseSitePath::read(&mut r)?;
83    // SPEC v0.30 §7 width formula: ⌈log₂(n)⌉. v0.30 drops the +1 v0.18 used
84    // to reserve the NUMS sentinel slot — NUMS is now signalled by an
85    // explicit `is_nums` bit on Body::Tr. MUST mirror
86    // `Descriptor::key_index_width` exactly; a stale formula silently
87    // desyncs the bitstream.
88    let key_index_width = (32 - (path_decl.n as u32).saturating_sub(1).leading_zeros()) as u8;
89    let tree = read_node(&mut r, key_index_width)?;
90
91    // SPEC §11: root tag MUST be in {Sh, Wsh, Wpkh, Pkh, Tr} (the wrapper-tag
92    // allow-list — structural body validation for `Sh`/`Wsh` is separate).
93    // Decoder-side hardening (defense in depth) — the parser-side enforces this
94    // for CLI/template inputs; this catches malformed wires that bypass the
95    // parser via direct bitstream construction. Note: `Sh` covers both
96    // `sh(multi)` and `sh(wsh(multi))` which are distinct BIP-388 shapes sharing
97    // the same root tag; per-shape validation happens at the policy layer.
98    if !matches!(
99        tree.tag,
100        Tag::Sh | Tag::Wsh | Tag::Wpkh | Tag::Pkh | Tag::Tr
101    ) {
102        return Err(Error::OperatorContextViolation {
103            tag: tree.tag,
104            context: ContextKind::TopLevel,
105        });
106    }
107
108    let tlv = TlvSection::read(&mut r, key_index_width, path_decl.n)?;
109
110    let descriptor = Descriptor {
111        n: path_decl.n,
112        path_decl,
113        use_site_path,
114        tree,
115        tlv,
116    };
117
118    crate::validate::validate_placeholder_usage(&descriptor.tree, descriptor.n)?;
119    if let Some(overrides) = &descriptor.tlv.use_site_path_overrides {
120        crate::validate::validate_multipath_consistency(&descriptor.use_site_path, overrides)?;
121        // D5(a): reject non-canonical override shapes (an `@0` override, or a
122        // redundant override equal to the baseline) — never emitted by our
123        // encoders; defense-in-depth against hand-crafted wire.
124        crate::validate::validate_use_site_overrides_canonical(
125            &descriptor.use_site_path,
126            overrides,
127        )?;
128    }
129    if matches!(descriptor.tree.tag, crate::tag::Tag::Tr) {
130        if let crate::tree::Body::Tr { tree: Some(t), .. } = &descriptor.tree.body {
131            crate::validate::validate_tap_script_tree(t)?;
132        }
133    }
134    // Spec v0.13 §6.3 + §6.4: enforce explicit-origin and xpub-validity
135    // after the v0.11 ordering / multipath / taptree checks. Order matters:
136    // ordering must run first so subsequent checks see canonical indices.
137    //
138    // P0.3 (I-1): the empty-origin-override reject is UNCONDITIONAL and a
139    // DISTINCT error from `MissingExplicitOrigin` — it runs regardless of
140    // `opts` and regardless of canonical-shape status (I-1a), so it is
141    // never swallowed by partial-allowing decode below (I-1b,
142    // fatal-in-partial).
143    crate::validate::validate_no_empty_origin_overrides(&descriptor)?;
144    match crate::validate::validate_explicit_origin_required(&descriptor) {
145        Ok(()) => {}
146        Err(Error::MissingExplicitOrigin { .. }) if opts.allow_unresolved_origin => {
147            // P0.2: partial-allowing decode swallows ONLY this reject.
148            // The caller queries `Descriptor::unresolved_origin_indices()`
149            // on the returned descriptor to learn which `@N` are
150            // unresolved.
151        }
152        Err(e) => return Err(e),
153    }
154    crate::validate::validate_xpub_bytes(&descriptor)?;
155
156    Ok(descriptor)
157}
158
159/// Decode a Descriptor from a complete codex32 md1 string.
160///
161/// Uses the symbol-aligned bit count returned by `unwrap_string` (5 × symbol_count),
162/// which is exact at the codex32 layer with ≤4 bits of trailing zero-padding —
163/// well within the v11 decoder's TLV-rollback tolerance.
164///
165/// F-A2: in-band auto-dispatch per SPEC v0.30 §2.3. The chunked-flag lives in
166/// bit 0 (LSB) of the first 5-bit symbol (`[v3][v2][v1][v0][chunked]` for a
167/// chunk header vs `[divergent][v3][v2][v1][v0]` for a single payload). When
168/// set, the string is a chunk-form md1 and MUST route through the
169/// chunk-reassembly path (a 1-element set) rather than the single-payload
170/// primitive `decode_payload` — mirroring `decode_with_correction`'s
171/// single-string auto-dispatch. The usable single-payload version set {4,8,12}
172/// is all-even ⇒ every currently-valid single-payload string has first-symbol
173/// LSB = 0, so this dispatch never diverts an input that decodes today. No
174/// recursion cycle: `reassemble` → `decode_payload`, never back to here.
175///
176/// Strict (byte-identical to pre-P0 behavior). Delegates to
177/// [`decode_md1_string_with_opts`] with the default (strict) options.
178pub fn decode_md1_string(s: &str) -> Result<Descriptor, Error> {
179    decode_md1_string_with_opts(s, DecodeOpts::default())
180}
181
182/// Decode a Descriptor from a complete codex32 md1 string, honoring
183/// `opts` (P0 partial-decode; see [`DecodeOpts`]). Routes chunk-form
184/// strings through [`crate::chunk::reassemble_with_opts`] and
185/// single-payload strings through [`decode_payload_with_opts`], so
186/// `opts` reaches whichever layer actually performs the origin check.
187pub fn decode_md1_string_with_opts(s: &str, opts: DecodeOpts) -> Result<Descriptor, Error> {
188    let (bytes, symbol_aligned_bit_count) = crate::codex32::unwrap_string(s)?;
189    // The first symbol occupies the top 5 bits of byte 0 (MSB-first packing),
190    // so its LSB (the chunked-flag) is bit 3 of byte 0.
191    let chunked_flag = bytes.first().map(|b| (b >> 3) & 0x01).unwrap_or(0);
192    if chunked_flag == 1 {
193        return crate::chunk::reassemble_with_opts(&[s], opts);
194    }
195    decode_payload_with_opts(&bytes, symbol_aligned_bit_count, opts)
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::encode::encode_payload;
202    use crate::origin_path::{OriginPath, PathComponent, PathDeclPaths};
203    use crate::tlv::TlvSection;
204    use crate::tree::{Body, Node};
205
206    /// SPEC §11 TopLevel check: a wire payload whose root tag is outside the
207    /// BIP-388 allow-list `{Sh, Wsh, Wpkh, Pkh, Tr}` must be rejected with
208    /// `Error::OperatorContextViolation { context: ContextKind::TopLevel }`.
209    /// The encoder has no root-tag gate (only placeholder/multipath/taptree
210    /// validators run), so `encode_payload` of an AndV-rooted descriptor
211    /// succeeds and round-trips through `decode_payload` exposes the gap.
212    #[test]
213    fn decode_rejects_non_canonical_root_tag() {
214        // The TopLevel check fires in `decode_payload` before any downstream
215        // validator runs, so this test reaches the rejection regardless of
216        // whether path_decl would satisfy `validate_explicit_origin_required`
217        // (it does, but the check is short-circuited above). path_decl is
218        // populated here to mirror a realistic descriptor shape.
219        let d = Descriptor {
220            n: 1,
221            path_decl: PathDecl {
222                n: 1,
223                paths: PathDeclPaths::Shared(OriginPath {
224                    components: vec![PathComponent {
225                        hardened: true,
226                        value: 84,
227                    }],
228                }),
229            },
230            use_site_path: UseSitePath::standard_multipath(),
231            tree: Node {
232                tag: Tag::AndV,
233                body: Body::Children(vec![
234                    Node {
235                        tag: Tag::PkK,
236                        body: Body::KeyArg { index: 0 },
237                    },
238                    Node {
239                        tag: Tag::PkK,
240                        body: Body::KeyArg { index: 0 },
241                    },
242                ]),
243            },
244            tlv: TlvSection::new_empty(),
245        };
246        let (bytes, total_bits) = encode_payload(&d).expect("encode AndV-rooted ok");
247        let err = decode_payload(&bytes, total_bits).expect_err("decode must reject");
248        assert!(
249            matches!(
250                err,
251                Error::OperatorContextViolation {
252                    tag: Tag::AndV,
253                    context: ContextKind::TopLevel,
254                }
255            ),
256            "expected OperatorContextViolation{{TopLevel}}, got {err:?}"
257        );
258    }
259}