Skip to main content

md_codec/
validate.rs

1//! Decoder-side validation per spec §7.
2
3use crate::canonical_origin::canonical_origin;
4use crate::encode::Descriptor;
5use crate::error::Error;
6use crate::origin_path::PathDeclPaths;
7use crate::tag::Tag;
8use crate::tree::{Body, Node};
9use crate::use_site_path::UseSitePath;
10
11/// Validate the BIP 388 well-formedness of placeholder usage in the tree.
12///
13/// Enforces two invariants:
14/// 1. Every `@i` for `0 ≤ i < n` appears at least once in the tree.
15/// 2. The first occurrences (in pre-order traversal) of distinct placeholder
16///    indices appear in canonical ascending order: `@0` before `@1` before `@2`, etc.
17pub fn validate_placeholder_usage(root: &Node, n: u8) -> Result<(), Error> {
18    let mut seen = vec![false; n as usize];
19    let mut first_occurrences: Vec<u8> = Vec::new();
20    walk_for_placeholders(root, &mut seen, &mut first_occurrences)?;
21    // Each @i for 0 ≤ i < n must appear at least once.
22    for (i, was_seen) in seen.iter().enumerate() {
23        if !was_seen {
24            return Err(Error::PlaceholderNotReferenced { idx: i as u8, n });
25        }
26    }
27    // First occurrences must be in canonical ascending order.
28    for (pos, idx) in first_occurrences.iter().enumerate() {
29        if *idx as usize != pos {
30            return Err(Error::PlaceholderFirstOccurrenceOutOfOrder {
31                expected_first: pos as u8,
32                got_first: *idx,
33            });
34        }
35    }
36    Ok(())
37}
38
39fn walk_for_placeholders(
40    node: &Node,
41    seen: &mut [bool],
42    first_occurrences: &mut Vec<u8>,
43) -> Result<(), Error> {
44    match &node.body {
45        Body::KeyArg { index } => {
46            if (*index as usize) >= seen.len() {
47                return Err(Error::PlaceholderIndexOutOfRange {
48                    idx: *index,
49                    n: seen.len() as u8,
50                });
51            }
52            if !seen[*index as usize] {
53                seen[*index as usize] = true;
54                first_occurrences.push(*index);
55            }
56        }
57        Body::Children(children) => {
58            for c in children {
59                walk_for_placeholders(c, seen, first_occurrences)?;
60            }
61        }
62        Body::Variable { children, .. } => {
63            for c in children {
64                walk_for_placeholders(c, seen, first_occurrences)?;
65            }
66        }
67        Body::MultiKeys { indices, .. } => {
68            // v0.30 Phase C: multi-family bodies carry raw key indices instead
69            // of child Nodes. Same placeholder-usage semantics as KeyArg, per
70            // index.
71            for index in indices {
72                if (*index as usize) >= seen.len() {
73                    return Err(Error::PlaceholderIndexOutOfRange {
74                        idx: *index,
75                        n: seen.len() as u8,
76                    });
77                }
78                if !seen[*index as usize] {
79                    seen[*index as usize] = true;
80                    first_occurrences.push(*index);
81                }
82            }
83        }
84        Body::Tr {
85            is_nums,
86            key_index,
87            tree,
88        } => {
89            // SPEC v0.30 §7 + §11: when `is_nums = true` the internal key is
90            // the BIP-341 NUMS H-point (not a placeholder reference); skip
91            // registration. Otherwise `key_index` must be in `0..n`; out-of-
92            // range raises `NUMSSentinelConflict` per SPEC §11 (Phase G
93            // finalizes the variant's full doc-comment).
94            if !*is_nums {
95                if (*key_index as usize) >= seen.len() {
96                    return Err(Error::NUMSSentinelConflict);
97                }
98                if !seen[*key_index as usize] {
99                    seen[*key_index as usize] = true;
100                    first_occurrences.push(*key_index);
101                }
102            }
103            if let Some(t) = tree {
104                walk_for_placeholders(t, seen, first_occurrences)?;
105            }
106        }
107        Body::Hash256Body(_) | Body::Hash160Body(_) | Body::Timelock(_) | Body::Empty => {}
108    }
109    Ok(())
110}
111
112/// Validate that all multipaths in shared default + overrides share the same alt-count.
113///
114/// Per spec §7, when multiple `UseSitePath` entries (the shared default plus any
115/// per-`@N` overrides) carry a multipath group, all groups MUST have the same
116/// number of alternatives.
117///
118/// D5(b): a `Some`-multipath baseline mixed with a `None`-multipath override
119/// (or vice-versa) is a **legal divergent STRUCTURE** (e.g. `@0/<0;1>/*` +
120/// `@1/*`), NOT a reject — a `None` entry simply carries no multipath group,
121/// so it is skipped by the alt-count check below (the `if let Some(alts)`
122/// guard). The C2 faithful reconstruction
123/// (`crate::to_miniscript::to_miniscript_descriptor_multipath`) handles the
124/// `None`-override by emitting a single-path `XPub` for that key while sibling
125/// keys stay `MultiXPub`. Only two multipath groups with DIFFERENT alt-counts
126/// are rejected.
127pub fn validate_multipath_consistency(
128    shared: &UseSitePath,
129    overrides: &[(u8, UseSitePath)],
130) -> Result<(), Error> {
131    let mut seen_alt_count: Option<usize> = None;
132    let candidates = std::iter::once(shared).chain(overrides.iter().map(|(_, p)| p));
133    for path in candidates {
134        if let Some(alts) = &path.multipath {
135            match seen_alt_count {
136                None => seen_alt_count = Some(alts.len()),
137                Some(prev) if prev == alts.len() => {}
138                Some(prev) => {
139                    return Err(Error::MultipathAltCountMismatch {
140                        expected: prev,
141                        got: alts.len(),
142                    });
143                }
144            }
145        }
146    }
147    Ok(())
148}
149
150/// D5(a) decode canonical-form check for `use_site_path_overrides`.
151///
152/// Our encoders only push an override entry for `i ≥ 1` and only when it
153/// DIFFERS from the resolved baseline (`Descriptor::use_site_path`). Two
154/// non-canonical / adversarial wire shapes are therefore rejected at decode
155/// (defense in depth — they are never emitted, only hand-crafted):
156///
157/// 1. An entry keyed on `@0` — the baseline cannot be overridden →
158///    [`Error::BaselineUseSiteOverride`].
159/// 2. An entry whose `UseSitePath` equals `baseline` — a redundant
160///    (non-canonical) override → [`Error::RedundantUseSiteOverride`].
161///
162/// The `@0` check runs first so an adversarial `@0` entry that ALSO happens
163/// to equal the baseline surfaces as the more-specific `BaselineUseSiteOverride`.
164pub fn validate_use_site_overrides_canonical(
165    baseline: &UseSitePath,
166    overrides: &[(u8, UseSitePath)],
167) -> Result<(), Error> {
168    for (idx, usp) in overrides {
169        if *idx == 0 {
170            return Err(Error::BaselineUseSiteOverride { idx: *idx });
171        }
172        if usp == baseline {
173            return Err(Error::RedundantUseSiteOverride { idx: *idx });
174        }
175    }
176    Ok(())
177}
178
179/// Validate that all leaves in a tap-script-tree are permitted-leaf tags per §6.3.1.
180pub fn validate_tap_script_tree(node: &Node) -> Result<(), Error> {
181    walk_tap_tree_leaves(node)
182}
183
184fn walk_tap_tree_leaves(node: &Node) -> Result<(), Error> {
185    if matches!(node.tag, Tag::TapTree) {
186        if let Body::Children(children) = &node.body {
187            for c in children {
188                walk_tap_tree_leaves(c)?;
189            }
190        }
191        Ok(())
192    } else {
193        // This is a leaf — validate per §6.3.1.
194        if is_forbidden_leaf_tag(node.tag) {
195            return Err(Error::ForbiddenTapTreeLeaf {
196                tag: node.tag.codes().0,
197            });
198        }
199        Ok(())
200    }
201}
202
203fn is_forbidden_leaf_tag(tag: Tag) -> bool {
204    matches!(
205        tag,
206        Tag::Wpkh | Tag::Tr | Tag::Wsh | Tag::Sh | Tag::Pkh | Tag::Multi | Tag::SortedMulti
207    )
208}
209
210/// Validate that every `@N` in a non-canonical wrapper has an explicit
211/// origin path on the wire — either via `OriginPathOverrides[idx]` or
212/// via a non-empty entry in the `path_decl` (shared or divergent).
213///
214/// Per spec v0.13 §6.3: when `canonical_origin(&d.tree)` is `None`, the
215/// wrapper is "non-canonical" and the encoder must emit an explicit
216/// origin for every `@N`. The decoder enforces the same as defense in
217/// depth: failure → `Error::MissingExplicitOrigin { idx }`.
218///
219/// If `canonical_origin(&d.tree)` is `Some(_)`, this validator is a
220/// no-op — any origin spec (elided or explicit) is allowed.
221pub fn validate_explicit_origin_required(d: &Descriptor) -> Result<(), Error> {
222    if canonical_origin(&d.tree).is_some() {
223        return Ok(());
224    }
225    let overrides = d.tlv.origin_path_overrides.as_deref().unwrap_or(&[]);
226    for idx in 0..d.n {
227        // Override path takes precedence — if present and non-empty, OK.
228        if let Some((_, op)) = overrides.iter().find(|(i, _)| *i == idx) {
229            if !op.components.is_empty() {
230                continue;
231            }
232        }
233        // Otherwise consult the path_decl for this idx.
234        let decl_components_empty = match &d.path_decl.paths {
235            PathDeclPaths::Shared(p) => p.components.is_empty(),
236            PathDeclPaths::Divergent(v) => v
237                .get(idx as usize)
238                .map(|p| p.components.is_empty())
239                .unwrap_or(true),
240        };
241        if decl_components_empty {
242            return Err(Error::MissingExplicitOrigin { idx });
243        }
244    }
245    Ok(())
246}
247
248/// Validate that every `Pubkeys` TLV entry's 33-byte compressed pubkey
249/// field (bytes 32..65 of the 65-byte payload) parses as a valid
250/// secp256k1 point. The 32-byte chain code prefix is unvalidated (any
251/// 32 bytes are a structurally valid BIP 32 chain code).
252///
253/// Per spec v0.13 §6.4: failure → `Error::InvalidXpubBytes { idx }`.
254/// When `d.tlv.pubkeys` is `None` (template-only mode), this is a no-op.
255pub fn validate_xpub_bytes(d: &Descriptor) -> Result<(), Error> {
256    let Some(entries) = d.tlv.pubkeys.as_deref() else {
257        return Ok(());
258    };
259    for (idx, xpub) in entries {
260        if bitcoin::secp256k1::PublicKey::from_slice(&xpub[32..65]).is_err() {
261            return Err(Error::InvalidXpubBytes { idx: *idx });
262        }
263    }
264    Ok(())
265}
266
267/// Validate that no `OriginPathOverrides[idx]` entry is present-but-empty
268/// (zero path components). Per spec v0.13 §6.3 (I-1 hardening, P0
269/// pathless/dead-card partial-decode).
270///
271/// Runs UNCONDITIONALLY — regardless of `canonical_origin(&d.tree)` — so
272/// a CANONICAL-shape wire (e.g. `wpkh(@0)`) carrying an empty override is
273/// ALSO rejected (I-1a). This is a DISTINCT error variant from
274/// `Error::MissingExplicitOrigin` so partial-allowing decode (P0.2, which
275/// swallows ONLY `MissingExplicitOrigin`) never swallows this: a
276/// present-but-empty override is a MALFORMED wire, not a dead card, and
277/// must not partial-render (I-1b, fatal-in-partial).
278///
279/// Converges with [`crate::canonicalize::expand_per_at_n`], which runs
280/// the same check independently (defense in depth for a hand-built
281/// `Descriptor` that bypasses decode).
282pub fn validate_no_empty_origin_overrides(d: &Descriptor) -> Result<(), Error> {
283    let overrides = d.tlv.origin_path_overrides.as_deref().unwrap_or(&[]);
284    for (idx, op) in overrides {
285        if op.components.is_empty() {
286            return Err(Error::EmptyOriginOverride { idx: *idx });
287        }
288    }
289    Ok(())
290}
291
292impl Descriptor {
293    /// The ascending `@N` indices whose origin cannot be resolved: a pure
294    /// query mirroring [`validate_explicit_origin_required`]'s SEMANTICS
295    /// (P0.1, pathless/dead-card partial-decode).
296    ///
297    /// Returns the ascending indices where `canonical_origin(&self.tree)`
298    /// is `None` AND the per-idx origin (override-or-`path_decl`) is
299    /// empty. Returns `[]` when `canonical_origin` is `Some` OR every idx
300    /// has a non-empty origin (i.e. exactly the set of shapes that decode
301    /// cleanly today under the strict default). Does NOT call
302    /// [`crate::canonicalize::expand_per_at_n`] — this is a
303    /// non-erroring, side-effect-free query, not an expansion; callers
304    /// needing per-`@N` origin/use-site/fp/xpub records still use
305    /// `expand_per_at_n` (which stays strict and fail-closed
306    /// unconditionally — see its doc comment).
307    pub fn unresolved_origin_indices(&self) -> Vec<u8> {
308        if canonical_origin(&self.tree).is_some() {
309            return Vec::new();
310        }
311        let overrides = self.tlv.origin_path_overrides.as_deref().unwrap_or(&[]);
312        let mut out = Vec::new();
313        for idx in 0..self.n {
314            // Override path takes precedence — if present and non-empty, resolved.
315            if let Some((_, op)) = overrides.iter().find(|(i, _)| *i == idx) {
316                if !op.components.is_empty() {
317                    continue;
318                }
319            }
320            // Otherwise consult the path_decl for this idx.
321            let decl_components_empty = match &self.path_decl.paths {
322                PathDeclPaths::Shared(p) => p.components.is_empty(),
323                PathDeclPaths::Divergent(v) => v
324                    .get(idx as usize)
325                    .map(|p| p.components.is_empty())
326                    .unwrap_or(true),
327            };
328            if decl_components_empty {
329                out.push(idx);
330            }
331        }
332        out
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use crate::tag::Tag;
340    use crate::tree::{Body, Node};
341
342    #[test]
343    fn placeholder_usage_ok_for_2_of_3() {
344        let root = Node {
345            tag: Tag::SortedMulti,
346            body: Body::MultiKeys {
347                k: 2,
348                indices: vec![0, 1, 2],
349            },
350        };
351        validate_placeholder_usage(&root, 3).unwrap();
352    }
353
354    #[test]
355    fn placeholder_usage_rejects_unreferenced() {
356        let root = Node {
357            tag: Tag::SortedMulti,
358            body: Body::MultiKeys {
359                k: 1,
360                indices: vec![0, 1],
361            },
362        };
363        assert!(matches!(
364            validate_placeholder_usage(&root, 3),
365            Err(Error::PlaceholderNotReferenced { idx: 2, n: 3 })
366        ));
367    }
368
369    #[test]
370    fn placeholder_usage_rejects_out_of_order_first_occurrences() {
371        let root = Node {
372            tag: Tag::SortedMulti,
373            body: Body::MultiKeys {
374                k: 1,
375                indices: vec![1, 0],
376            },
377        };
378        assert!(matches!(
379            validate_placeholder_usage(&root, 2),
380            Err(Error::PlaceholderFirstOccurrenceOutOfOrder { .. })
381        ));
382    }
383
384    #[test]
385    fn multipath_consistency_ok_when_all_match() {
386        let shared = UseSitePath::standard_multipath();
387        let overrides = vec![(1u8, UseSitePath::standard_multipath())];
388        validate_multipath_consistency(&shared, &overrides).unwrap();
389    }
390
391    #[test]
392    fn multipath_consistency_rejects_mismatched_alt_counts() {
393        use crate::use_site_path::Alternative;
394        let shared = UseSitePath::standard_multipath();
395        let overrides = vec![(
396            1u8,
397            UseSitePath {
398                multipath: Some(vec![
399                    Alternative {
400                        hardened: false,
401                        value: 0,
402                    },
403                    Alternative {
404                        hardened: false,
405                        value: 1,
406                    },
407                    Alternative {
408                        hardened: false,
409                        value: 2,
410                    },
411                ]),
412                wildcard_hardened: false,
413            },
414        )];
415        assert!(matches!(
416            validate_multipath_consistency(&shared, &overrides),
417            Err(Error::MultipathAltCountMismatch {
418                expected: 2,
419                got: 3
420            })
421        ));
422    }
423
424    #[test]
425    fn tap_tree_leaf_rejects_wsh() {
426        let leaf = Node {
427            tag: Tag::Wsh,
428            body: Body::Children(vec![]),
429        };
430        assert!(matches!(
431            validate_tap_script_tree(&leaf),
432            Err(Error::ForbiddenTapTreeLeaf { .. })
433        ));
434    }
435
436    #[test]
437    fn tap_tree_leaf_accepts_pk_k() {
438        let leaf = Node {
439            tag: Tag::PkK,
440            body: Body::KeyArg { index: 0 },
441        };
442        validate_tap_script_tree(&leaf).unwrap();
443    }
444
445    #[test]
446    fn placeholder_usage_rejects_index_out_of_range_n3() {
447        // n=3 → key_index_width=2 admits 0..=3 structurally. @3 is out of range.
448        let root = Node {
449            tag: Tag::Wpkh,
450            body: Body::KeyArg { index: 3 },
451        };
452        let err = validate_placeholder_usage(&root, 3).unwrap_err();
453        assert!(matches!(
454            err,
455            Error::PlaceholderIndexOutOfRange { idx: 3, n: 3 }
456        ));
457    }
458
459    #[test]
460    fn placeholder_usage_rejects_index_out_of_range_n5() {
461        // n=5 → key_index_width=3 admits 0..=7. @5..=7 are out of range.
462        let root = Node {
463            tag: Tag::SortedMulti,
464            body: Body::MultiKeys {
465                k: 1,
466                indices: vec![5],
467            },
468        };
469        let err = validate_placeholder_usage(&root, 5).unwrap_err();
470        assert!(matches!(
471            err,
472            Error::PlaceholderIndexOutOfRange { idx: 5, n: 5 }
473        ));
474    }
475
476    #[test]
477    fn placeholder_usage_rejects_index_out_of_range_n15() {
478        // n=15 → key_index_width=4 admits 0..=15. @15 just out of range.
479        let root = Node {
480            tag: Tag::SortedMulti,
481            body: Body::MultiKeys {
482                k: 1,
483                indices: vec![15],
484            },
485        };
486        let err = validate_placeholder_usage(&root, 15).unwrap_err();
487        assert!(matches!(
488            err,
489            Error::PlaceholderIndexOutOfRange { idx: 15, n: 15 }
490        ));
491    }
492
493    #[test]
494    fn placeholder_usage_rejects_out_of_range_in_tr_key_index() {
495        // SPEC v0.30 §7 + §11: `is_nums = false` with `key_index >= n` is a
496        // `NUMSSentinelConflict` (distinct from KeyArg's
497        // `PlaceholderIndexOutOfRange`; NUMS is signalled by `is_nums = true`
498        // with `key_index` unused on wire).
499        let root = Node {
500            tag: Tag::Tr,
501            body: Body::Tr {
502                is_nums: false,
503                key_index: 3,
504                tree: None,
505            },
506        };
507        let err = validate_placeholder_usage(&root, 3).unwrap_err();
508        assert!(matches!(err, Error::NUMSSentinelConflict));
509    }
510
511    #[test]
512    fn placeholder_usage_accepts_nums_flag_in_tr() {
513        // SPEC v0.30 §7: `is_nums = true` is the NUMS-H-point signal and
514        // MUST pass validation. validate_placeholder_usage requires every
515        // @i in 0..n to be referenced; the @0 reference here satisfies that
516        // for n=1.
517        let root = Node {
518            tag: Tag::Tr,
519            body: Body::Tr {
520                is_nums: true,
521                key_index: 0,
522                tree: Some(Box::new(Node {
523                    tag: Tag::PkK,
524                    body: Body::KeyArg { index: 0 },
525                })),
526            },
527        };
528        validate_placeholder_usage(&root, 1)
529            .expect("is_nums flag + @0 reference must validate under v0.30");
530    }
531}
532
533#[cfg(test)]
534mod explicit_origin_required_tests {
535    use super::*;
536    use crate::origin_path::{OriginPath, PathComponent, PathDecl, PathDeclPaths};
537    use crate::tag::Tag;
538    use crate::tlv::TlvSection;
539    use crate::tree::{Body, Node};
540    use crate::use_site_path::UseSitePath;
541
542    fn empty_path() -> OriginPath {
543        OriginPath { components: vec![] }
544    }
545
546    fn bip84_path() -> OriginPath {
547        OriginPath {
548            components: vec![
549                PathComponent {
550                    hardened: true,
551                    value: 84,
552                },
553                PathComponent {
554                    hardened: true,
555                    value: 0,
556                },
557                PathComponent {
558                    hardened: true,
559                    value: 0,
560                },
561            ],
562        }
563    }
564
565    /// Build a single-key descriptor with `n=1`, the given tree root, an
566    /// empty shared path_decl (origin elided on wire), and an empty TLV
567    /// section.
568    fn single_key_descriptor(tree: Node) -> Descriptor {
569        Descriptor {
570            n: 1,
571            path_decl: PathDecl {
572                n: 1,
573                paths: PathDeclPaths::Shared(empty_path()),
574            },
575            use_site_path: UseSitePath::standard_multipath(),
576            tree,
577            tlv: TlvSection::new_empty(),
578        }
579    }
580
581    #[test]
582    fn validate_explicit_origin_required_passes_canonical_wpkh() {
583        // wpkh(@0) has canonical BIP-84 origin → empty path_decl OK.
584        let d = single_key_descriptor(Node {
585            tag: Tag::Wpkh,
586            body: Body::KeyArg { index: 0 },
587        });
588        validate_explicit_origin_required(&d).unwrap();
589    }
590
591    #[test]
592    fn validate_explicit_origin_required_passes_with_overrides_for_non_canonical() {
593        // sh(sortedmulti(@0,@1,@2)) — non-canonical. Must have explicit
594        // origin per @N. Provide overrides for all three.
595        let mut d = Descriptor {
596            n: 3,
597            path_decl: PathDecl {
598                n: 3,
599                paths: PathDeclPaths::Shared(empty_path()),
600            },
601            use_site_path: UseSitePath::standard_multipath(),
602            tree: Node {
603                tag: Tag::Sh,
604                body: Body::Children(vec![Node {
605                    tag: Tag::SortedMulti,
606                    body: Body::MultiKeys {
607                        k: 2,
608                        indices: vec![0, 1, 2],
609                    },
610                }]),
611            },
612            tlv: TlvSection::new_empty(),
613        };
614        d.tlv.origin_path_overrides = Some(vec![
615            (0u8, bip84_path()),
616            (1u8, bip84_path()),
617            (2u8, bip84_path()),
618        ]);
619        validate_explicit_origin_required(&d).unwrap();
620    }
621
622    #[test]
623    fn validate_explicit_origin_required_fails_sh_sortedmulti_with_empty_path_decl() {
624        // sh(sortedmulti(@0,@1,@2)) — non-canonical. Empty path_decl, no
625        // overrides → fails on idx=0.
626        let d = Descriptor {
627            n: 3,
628            path_decl: PathDecl {
629                n: 3,
630                paths: PathDeclPaths::Shared(empty_path()),
631            },
632            use_site_path: UseSitePath::standard_multipath(),
633            tree: Node {
634                tag: Tag::Sh,
635                body: Body::Children(vec![Node {
636                    tag: Tag::SortedMulti,
637                    body: Body::MultiKeys {
638                        k: 2,
639                        indices: vec![0, 1, 2],
640                    },
641                }]),
642            },
643            tlv: TlvSection::new_empty(),
644        };
645        let err = validate_explicit_origin_required(&d).unwrap_err();
646        assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
647    }
648
649    #[test]
650    fn validate_explicit_origin_required_fails_bare_wsh_with_empty_path_decl() {
651        // bare wsh(@0) — non-canonical (no `multi`/`sortedmulti` inner).
652        let d = single_key_descriptor(Node {
653            tag: Tag::Wsh,
654            body: Body::Children(vec![Node {
655                tag: Tag::PkK,
656                body: Body::KeyArg { index: 0 },
657            }]),
658        });
659        let err = validate_explicit_origin_required(&d).unwrap_err();
660        assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
661    }
662
663    #[test]
664    fn validate_explicit_origin_required_passes_tr_keypath_only_with_empty_path_decl() {
665        // tr(@0) key-path only → BIP 86 canonical exists → empty path_decl OK.
666        let d = single_key_descriptor(Node {
667            tag: Tag::Tr,
668            body: Body::Tr {
669                is_nums: false,
670                key_index: 0,
671                tree: None,
672            },
673        });
674        validate_explicit_origin_required(&d).unwrap();
675    }
676
677    #[test]
678    fn validate_explicit_origin_required_fails_tr_with_taptree_with_empty_path_decl() {
679        // tr(@0, TapTree) → no canonical → must be explicit.
680        let d = single_key_descriptor(Node {
681            tag: Tag::Tr,
682            body: Body::Tr {
683                is_nums: false,
684                key_index: 0,
685                tree: Some(Box::new(Node {
686                    tag: Tag::PkK,
687                    body: Body::KeyArg { index: 0 },
688                })),
689            },
690        });
691        let err = validate_explicit_origin_required(&d).unwrap_err();
692        assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
693    }
694
695    #[test]
696    fn validate_explicit_origin_required_passes_with_populated_shared_path_decl() {
697        // Bare wsh(@0) with a populated shared path_decl — explicit origin
698        // is on the wire via path_decl, so the validator is satisfied even
699        // without an OriginPathOverrides entry.
700        let mut d = single_key_descriptor(Node {
701            tag: Tag::Wsh,
702            body: Body::Children(vec![Node {
703                tag: Tag::PkK,
704                body: Body::KeyArg { index: 0 },
705            }]),
706        });
707        d.path_decl.paths = PathDeclPaths::Shared(bip84_path());
708        validate_explicit_origin_required(&d).unwrap();
709    }
710
711    #[test]
712    fn validate_explicit_origin_required_passes_divergent_when_all_populated() {
713        // sh(sortedmulti(...)) with divergent path_decl, all entries populated.
714        let d = Descriptor {
715            n: 2,
716            path_decl: PathDecl {
717                n: 2,
718                paths: PathDeclPaths::Divergent(vec![bip84_path(), bip84_path()]),
719            },
720            use_site_path: UseSitePath::standard_multipath(),
721            tree: Node {
722                tag: Tag::Sh,
723                body: Body::Children(vec![Node {
724                    tag: Tag::SortedMulti,
725                    body: Body::MultiKeys {
726                        k: 1,
727                        indices: vec![0, 1],
728                    },
729                }]),
730            },
731            tlv: TlvSection::new_empty(),
732        };
733        validate_explicit_origin_required(&d).unwrap();
734    }
735
736    #[test]
737    fn validate_explicit_origin_required_fails_divergent_when_one_idx_empty() {
738        // sh(sortedmulti(...)) with divergent path_decl; @1 has empty path,
739        // no override → fails on idx=1.
740        let d = Descriptor {
741            n: 2,
742            path_decl: PathDecl {
743                n: 2,
744                paths: PathDeclPaths::Divergent(vec![bip84_path(), empty_path()]),
745            },
746            use_site_path: UseSitePath::standard_multipath(),
747            tree: Node {
748                tag: Tag::Sh,
749                body: Body::Children(vec![Node {
750                    tag: Tag::SortedMulti,
751                    body: Body::MultiKeys {
752                        k: 1,
753                        indices: vec![0, 1],
754                    },
755                }]),
756            },
757            tlv: TlvSection::new_empty(),
758        };
759        let err = validate_explicit_origin_required(&d).unwrap_err();
760        assert!(matches!(err, Error::MissingExplicitOrigin { idx: 1 }));
761    }
762
763    // ─── P0.1: Descriptor::unresolved_origin_indices ─────────────────────
764    //
765    // Pure query mirroring `validate_explicit_origin_required`'s SEMANTICS
766    // (does NOT call `expand_per_at_n`). Every case below has a sibling
767    // `validate_explicit_origin_required_*` test above/below asserting the
768    // same shape's Ok/Err verdict; these pin the parallel non-erroring
769    // query's ascending-index-vec verdict.
770
771    #[test]
772    fn unresolved_origin_indices_empty_for_canonical_wpkh() {
773        // wpkh(@0) has canonical BIP-84 origin → empty path_decl still []
774        let d = single_key_descriptor(Node {
775            tag: Tag::Wpkh,
776            body: Body::KeyArg { index: 0 },
777        });
778        assert_eq!(d.unresolved_origin_indices(), Vec::<u8>::new());
779    }
780
781    #[test]
782    fn unresolved_origin_indices_empty_for_canonical_tr_keypath() {
783        // tr(@0) key-path only → BIP-86 canonical → [].
784        let d = single_key_descriptor(Node {
785            tag: Tag::Tr,
786            body: Body::Tr {
787                is_nums: false,
788                key_index: 0,
789                tree: None,
790            },
791        });
792        assert_eq!(d.unresolved_origin_indices(), Vec::<u8>::new());
793    }
794
795    #[test]
796    fn unresolved_origin_indices_empty_for_canonical_sh_wpkh() {
797        // sh(wpkh(@0)) → BIP-49 canonical (F-A1) → [].
798        let d = single_key_descriptor(Node {
799            tag: Tag::Sh,
800            body: Body::Children(vec![Node {
801                tag: Tag::Wpkh,
802                body: Body::KeyArg { index: 0 },
803            }]),
804        });
805        assert_eq!(d.unresolved_origin_indices(), Vec::<u8>::new());
806    }
807
808    #[test]
809    fn unresolved_origin_indices_empty_for_canonical_wsh_multi() {
810        // wsh(multi(2,@0,@1)) → BIP-48 type-2 canonical → [].
811        let d = Descriptor {
812            n: 2,
813            path_decl: PathDecl {
814                n: 2,
815                paths: PathDeclPaths::Shared(empty_path()),
816            },
817            use_site_path: UseSitePath::standard_multipath(),
818            tree: Node {
819                tag: Tag::Wsh,
820                body: Body::Children(vec![Node {
821                    tag: Tag::Multi,
822                    body: Body::MultiKeys {
823                        k: 2,
824                        indices: vec![0, 1],
825                    },
826                }]),
827            },
828            tlv: TlvSection::new_empty(),
829        };
830        assert_eq!(d.unresolved_origin_indices(), Vec::<u8>::new());
831    }
832
833    #[test]
834    fn unresolved_origin_indices_empty_for_explicit_origin_dead_shape() {
835        // sh(sortedmulti(2,@0,@1)) is a dead shape (canonical_origin ==
836        // None) but the shared path_decl is EXPLICITLY populated → every
837        // idx resolves → [].
838        let d = Descriptor {
839            n: 2,
840            path_decl: PathDecl {
841                n: 2,
842                paths: PathDeclPaths::Shared(bip84_path()),
843            },
844            use_site_path: UseSitePath::standard_multipath(),
845            tree: Node {
846                tag: Tag::Sh,
847                body: Body::Children(vec![Node {
848                    tag: Tag::SortedMulti,
849                    body: Body::MultiKeys {
850                        k: 2,
851                        indices: vec![0, 1],
852                    },
853                }]),
854            },
855            tlv: TlvSection::new_empty(),
856        };
857        assert_eq!(d.unresolved_origin_indices(), Vec::<u8>::new());
858    }
859
860    #[test]
861    fn unresolved_origin_indices_single_for_tr_with_taptree() {
862        // tr(@0, TapTree) → no canonical default → [0].
863        let d = single_key_descriptor(Node {
864            tag: Tag::Tr,
865            body: Body::Tr {
866                is_nums: false,
867                key_index: 0,
868                tree: Some(Box::new(Node {
869                    tag: Tag::PkK,
870                    body: Body::KeyArg { index: 0 },
871                })),
872            },
873        });
874        assert_eq!(d.unresolved_origin_indices(), vec![0u8]);
875    }
876
877    #[test]
878    fn unresolved_origin_indices_both_for_tr_with_taptree_two_keys() {
879        // tr(@0, pk(@1)) — key-path @0 + tap leaf pk(@1). canonical_origin
880        // is None (Tr with Some(tree)), so BOTH indices are unresolved
881        // with an empty shared path_decl.
882        let d = Descriptor {
883            n: 2,
884            path_decl: PathDecl {
885                n: 2,
886                paths: PathDeclPaths::Shared(empty_path()),
887            },
888            use_site_path: UseSitePath::standard_multipath(),
889            tree: Node {
890                tag: Tag::Tr,
891                body: Body::Tr {
892                    is_nums: false,
893                    key_index: 0,
894                    tree: Some(Box::new(Node {
895                        tag: Tag::PkK,
896                        body: Body::KeyArg { index: 1 },
897                    })),
898                },
899            },
900            tlv: TlvSection::new_empty(),
901        };
902        assert_eq!(d.unresolved_origin_indices(), vec![0u8, 1u8]);
903    }
904
905    #[test]
906    fn unresolved_origin_indices_both_for_sh_sortedmulti_dead() {
907        // sh(sortedmulti(2,@0,@1)) — legacy P2SH multi, dead shape, empty
908        // shared path_decl, no overrides → [0, 1].
909        let d = Descriptor {
910            n: 2,
911            path_decl: PathDecl {
912                n: 2,
913                paths: PathDeclPaths::Shared(empty_path()),
914            },
915            use_site_path: UseSitePath::standard_multipath(),
916            tree: Node {
917                tag: Tag::Sh,
918                body: Body::Children(vec![Node {
919                    tag: Tag::SortedMulti,
920                    body: Body::MultiKeys {
921                        k: 2,
922                        indices: vec![0, 1],
923                    },
924                }]),
925            },
926            tlv: TlvSection::new_empty(),
927        };
928        assert_eq!(d.unresolved_origin_indices(), vec![0u8, 1u8]);
929    }
930
931    #[test]
932    fn unresolved_origin_indices_single_for_bare_wsh() {
933        // bare wsh(@0) — non-canonical (no multi/sortedmulti inner) → [0].
934        let d = single_key_descriptor(Node {
935            tag: Tag::Wsh,
936            body: Body::Children(vec![Node {
937                tag: Tag::PkK,
938                body: Body::KeyArg { index: 0 },
939            }]),
940        });
941        assert_eq!(d.unresolved_origin_indices(), vec![0u8]);
942    }
943
944    #[test]
945    fn unresolved_origin_indices_both_for_raw_miniscript_body() {
946        // wsh(or_d(pk_k(@0), pk_h(@1))) — raw miniscript body, dead shape
947        // → [0, 1].
948        let d = Descriptor {
949            n: 2,
950            path_decl: PathDecl {
951                n: 2,
952                paths: PathDeclPaths::Shared(empty_path()),
953            },
954            use_site_path: UseSitePath::standard_multipath(),
955            tree: Node {
956                tag: Tag::Wsh,
957                body: Body::Children(vec![Node {
958                    tag: Tag::OrD,
959                    body: Body::Children(vec![
960                        Node {
961                            tag: Tag::PkK,
962                            body: Body::KeyArg { index: 0 },
963                        },
964                        Node {
965                            tag: Tag::PkH,
966                            body: Body::KeyArg { index: 1 },
967                        },
968                    ]),
969                }]),
970            },
971            tlv: TlvSection::new_empty(),
972        };
973        assert_eq!(d.unresolved_origin_indices(), vec![0u8, 1u8]);
974    }
975
976    #[test]
977    fn unresolved_origin_indices_partial_divergent() {
978        // sh(sortedmulti(...)) with divergent path_decl; @0 populated, @1
979        // empty, no overrides → [1] only.
980        let d = Descriptor {
981            n: 2,
982            path_decl: PathDecl {
983                n: 2,
984                paths: PathDeclPaths::Divergent(vec![bip84_path(), empty_path()]),
985            },
986            use_site_path: UseSitePath::standard_multipath(),
987            tree: Node {
988                tag: Tag::Sh,
989                body: Body::Children(vec![Node {
990                    tag: Tag::SortedMulti,
991                    body: Body::MultiKeys {
992                        k: 1,
993                        indices: vec![0, 1],
994                    },
995                }]),
996            },
997            tlv: TlvSection::new_empty(),
998        };
999        assert_eq!(d.unresolved_origin_indices(), vec![1u8]);
1000    }
1001
1002    #[test]
1003    fn unresolved_origin_indices_empty_when_override_resolves_dead_shape() {
1004        // sh(sortedmulti(2,@0,@1)) dead shape, empty shared path_decl, but
1005        // a NON-EMPTY override resolves @0 → only @1 unresolved.
1006        let d = Descriptor {
1007            n: 2,
1008            path_decl: PathDecl {
1009                n: 2,
1010                paths: PathDeclPaths::Shared(empty_path()),
1011            },
1012            use_site_path: UseSitePath::standard_multipath(),
1013            tree: Node {
1014                tag: Tag::Sh,
1015                body: Body::Children(vec![Node {
1016                    tag: Tag::SortedMulti,
1017                    body: Body::MultiKeys {
1018                        k: 2,
1019                        indices: vec![0, 1],
1020                    },
1021                }]),
1022            },
1023            tlv: {
1024                let mut t = TlvSection::new_empty();
1025                t.origin_path_overrides = Some(vec![(0u8, bip84_path())]);
1026                t
1027            },
1028        };
1029        assert_eq!(d.unresolved_origin_indices(), vec![1u8]);
1030    }
1031}
1032
1033#[cfg(test)]
1034mod xpub_bytes_tests {
1035    use super::*;
1036    use crate::origin_path::{OriginPath, PathDecl, PathDeclPaths};
1037    use crate::tag::Tag;
1038    use crate::tlv::TlvSection;
1039    use crate::tree::{Body, Node};
1040    use crate::use_site_path::UseSitePath;
1041
1042    /// G (the secp256k1 generator) compressed: 0x02 || x(G).
1043    /// Used for "valid pubkey" tests.
1044    fn valid_compressed_g() -> [u8; 33] {
1045        // x(G) = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
1046        let mut out = [0u8; 33];
1047        out[0] = 0x02;
1048        let x: [u8; 32] = [
1049            0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87,
1050            0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B,
1051            0x16, 0xF8, 0x17, 0x98,
1052        ];
1053        out[1..].copy_from_slice(&x);
1054        out
1055    }
1056
1057    fn descriptor_with_pubkeys(pks: Option<Vec<(u8, [u8; 65])>>) -> Descriptor {
1058        let mut d = Descriptor {
1059            n: 1,
1060            path_decl: PathDecl {
1061                n: 1,
1062                paths: PathDeclPaths::Shared(OriginPath { components: vec![] }),
1063            },
1064            use_site_path: UseSitePath::standard_multipath(),
1065            tree: Node {
1066                tag: Tag::Wpkh,
1067                body: Body::KeyArg { index: 0 },
1068            },
1069            tlv: TlvSection::new_empty(),
1070        };
1071        d.tlv.pubkeys = pks;
1072        d
1073    }
1074
1075    #[test]
1076    fn validate_xpub_bytes_template_only_no_op() {
1077        let d = descriptor_with_pubkeys(None);
1078        validate_xpub_bytes(&d).unwrap();
1079    }
1080
1081    #[test]
1082    fn validate_xpub_bytes_passes_for_valid_compressed_pubkey() {
1083        let mut xpub = [0u8; 65];
1084        // Chain code 0..32 — arbitrary 32 bytes are valid.
1085        for (i, b) in xpub[0..32].iter_mut().enumerate() {
1086            *b = i as u8;
1087        }
1088        // Compressed pubkey 32..65 = G.
1089        xpub[32..65].copy_from_slice(&valid_compressed_g());
1090        let d = descriptor_with_pubkeys(Some(vec![(0u8, xpub)]));
1091        validate_xpub_bytes(&d).unwrap();
1092    }
1093
1094    #[test]
1095    fn validate_xpub_bytes_fails_for_invalid_pubkey_prefix() {
1096        // Prefix 0x04 is uncompressed-marker; not a valid 33-byte compressed
1097        // pubkey prefix (only 0x02 / 0x03 are).
1098        let mut xpub = [0u8; 65];
1099        xpub[32] = 0x04;
1100        let d = descriptor_with_pubkeys(Some(vec![(0u8, xpub)]));
1101        let err = validate_xpub_bytes(&d).unwrap_err();
1102        assert!(matches!(err, Error::InvalidXpubBytes { idx: 0 }));
1103    }
1104
1105    #[test]
1106    fn validate_xpub_bytes_fails_for_off_curve_x_coordinate() {
1107        // 0x02 || all-0xFF x-coord. x = p-1 wraps to a non-curve x in
1108        // most cases; in particular this exact value fails to lift in
1109        // libsecp256k1's compressed-point parser. Verify via the same
1110        // routine the validator uses.
1111        let mut xpub = [0u8; 65];
1112        xpub[32] = 0x02;
1113        for b in xpub[33..65].iter_mut() {
1114            *b = 0xFF;
1115        }
1116        // Sanity: confirm bitcoin's parser actually rejects this, so the
1117        // test exercises the failure path in our validator.
1118        assert!(bitcoin::secp256k1::PublicKey::from_slice(&xpub[32..65]).is_err());
1119        let d = descriptor_with_pubkeys(Some(vec![(0u8, xpub)]));
1120        let err = validate_xpub_bytes(&d).unwrap_err();
1121        assert!(matches!(err, Error::InvalidXpubBytes { idx: 0 }));
1122    }
1123
1124    #[test]
1125    fn validate_xpub_bytes_reports_first_failing_idx() {
1126        // Two entries: idx=0 valid, idx=2 invalid → error reports idx=2.
1127        let mut good = [0u8; 65];
1128        good[32..65].copy_from_slice(&valid_compressed_g());
1129        let mut bad = [0u8; 65];
1130        bad[32] = 0x04; // invalid prefix
1131        let d = descriptor_with_pubkeys(Some(vec![(0u8, good), (2u8, bad)]));
1132        let err = validate_xpub_bytes(&d).unwrap_err();
1133        assert!(matches!(err, Error::InvalidXpubBytes { idx: 2 }));
1134    }
1135}