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#[cfg(test)]
268mod tests {
269    use super::*;
270    use crate::tag::Tag;
271    use crate::tree::{Body, Node};
272
273    #[test]
274    fn placeholder_usage_ok_for_2_of_3() {
275        let root = Node {
276            tag: Tag::SortedMulti,
277            body: Body::MultiKeys {
278                k: 2,
279                indices: vec![0, 1, 2],
280            },
281        };
282        validate_placeholder_usage(&root, 3).unwrap();
283    }
284
285    #[test]
286    fn placeholder_usage_rejects_unreferenced() {
287        let root = Node {
288            tag: Tag::SortedMulti,
289            body: Body::MultiKeys {
290                k: 1,
291                indices: vec![0, 1],
292            },
293        };
294        assert!(matches!(
295            validate_placeholder_usage(&root, 3),
296            Err(Error::PlaceholderNotReferenced { idx: 2, n: 3 })
297        ));
298    }
299
300    #[test]
301    fn placeholder_usage_rejects_out_of_order_first_occurrences() {
302        let root = Node {
303            tag: Tag::SortedMulti,
304            body: Body::MultiKeys {
305                k: 1,
306                indices: vec![1, 0],
307            },
308        };
309        assert!(matches!(
310            validate_placeholder_usage(&root, 2),
311            Err(Error::PlaceholderFirstOccurrenceOutOfOrder { .. })
312        ));
313    }
314
315    #[test]
316    fn multipath_consistency_ok_when_all_match() {
317        let shared = UseSitePath::standard_multipath();
318        let overrides = vec![(1u8, UseSitePath::standard_multipath())];
319        validate_multipath_consistency(&shared, &overrides).unwrap();
320    }
321
322    #[test]
323    fn multipath_consistency_rejects_mismatched_alt_counts() {
324        use crate::use_site_path::Alternative;
325        let shared = UseSitePath::standard_multipath();
326        let overrides = vec![(
327            1u8,
328            UseSitePath {
329                multipath: Some(vec![
330                    Alternative {
331                        hardened: false,
332                        value: 0,
333                    },
334                    Alternative {
335                        hardened: false,
336                        value: 1,
337                    },
338                    Alternative {
339                        hardened: false,
340                        value: 2,
341                    },
342                ]),
343                wildcard_hardened: false,
344            },
345        )];
346        assert!(matches!(
347            validate_multipath_consistency(&shared, &overrides),
348            Err(Error::MultipathAltCountMismatch {
349                expected: 2,
350                got: 3
351            })
352        ));
353    }
354
355    #[test]
356    fn tap_tree_leaf_rejects_wsh() {
357        let leaf = Node {
358            tag: Tag::Wsh,
359            body: Body::Children(vec![]),
360        };
361        assert!(matches!(
362            validate_tap_script_tree(&leaf),
363            Err(Error::ForbiddenTapTreeLeaf { .. })
364        ));
365    }
366
367    #[test]
368    fn tap_tree_leaf_accepts_pk_k() {
369        let leaf = Node {
370            tag: Tag::PkK,
371            body: Body::KeyArg { index: 0 },
372        };
373        validate_tap_script_tree(&leaf).unwrap();
374    }
375
376    #[test]
377    fn placeholder_usage_rejects_index_out_of_range_n3() {
378        // n=3 → key_index_width=2 admits 0..=3 structurally. @3 is out of range.
379        let root = Node {
380            tag: Tag::Wpkh,
381            body: Body::KeyArg { index: 3 },
382        };
383        let err = validate_placeholder_usage(&root, 3).unwrap_err();
384        assert!(matches!(
385            err,
386            Error::PlaceholderIndexOutOfRange { idx: 3, n: 3 }
387        ));
388    }
389
390    #[test]
391    fn placeholder_usage_rejects_index_out_of_range_n5() {
392        // n=5 → key_index_width=3 admits 0..=7. @5..=7 are out of range.
393        let root = Node {
394            tag: Tag::SortedMulti,
395            body: Body::MultiKeys {
396                k: 1,
397                indices: vec![5],
398            },
399        };
400        let err = validate_placeholder_usage(&root, 5).unwrap_err();
401        assert!(matches!(
402            err,
403            Error::PlaceholderIndexOutOfRange { idx: 5, n: 5 }
404        ));
405    }
406
407    #[test]
408    fn placeholder_usage_rejects_index_out_of_range_n15() {
409        // n=15 → key_index_width=4 admits 0..=15. @15 just out of range.
410        let root = Node {
411            tag: Tag::SortedMulti,
412            body: Body::MultiKeys {
413                k: 1,
414                indices: vec![15],
415            },
416        };
417        let err = validate_placeholder_usage(&root, 15).unwrap_err();
418        assert!(matches!(
419            err,
420            Error::PlaceholderIndexOutOfRange { idx: 15, n: 15 }
421        ));
422    }
423
424    #[test]
425    fn placeholder_usage_rejects_out_of_range_in_tr_key_index() {
426        // SPEC v0.30 §7 + §11: `is_nums = false` with `key_index >= n` is a
427        // `NUMSSentinelConflict` (distinct from KeyArg's
428        // `PlaceholderIndexOutOfRange`; NUMS is signalled by `is_nums = true`
429        // with `key_index` unused on wire).
430        let root = Node {
431            tag: Tag::Tr,
432            body: Body::Tr {
433                is_nums: false,
434                key_index: 3,
435                tree: None,
436            },
437        };
438        let err = validate_placeholder_usage(&root, 3).unwrap_err();
439        assert!(matches!(err, Error::NUMSSentinelConflict));
440    }
441
442    #[test]
443    fn placeholder_usage_accepts_nums_flag_in_tr() {
444        // SPEC v0.30 §7: `is_nums = true` is the NUMS-H-point signal and
445        // MUST pass validation. validate_placeholder_usage requires every
446        // @i in 0..n to be referenced; the @0 reference here satisfies that
447        // for n=1.
448        let root = Node {
449            tag: Tag::Tr,
450            body: Body::Tr {
451                is_nums: true,
452                key_index: 0,
453                tree: Some(Box::new(Node {
454                    tag: Tag::PkK,
455                    body: Body::KeyArg { index: 0 },
456                })),
457            },
458        };
459        validate_placeholder_usage(&root, 1)
460            .expect("is_nums flag + @0 reference must validate under v0.30");
461    }
462}
463
464#[cfg(test)]
465mod explicit_origin_required_tests {
466    use super::*;
467    use crate::origin_path::{OriginPath, PathComponent, PathDecl, PathDeclPaths};
468    use crate::tag::Tag;
469    use crate::tlv::TlvSection;
470    use crate::tree::{Body, Node};
471    use crate::use_site_path::UseSitePath;
472
473    fn empty_path() -> OriginPath {
474        OriginPath { components: vec![] }
475    }
476
477    fn bip84_path() -> OriginPath {
478        OriginPath {
479            components: vec![
480                PathComponent {
481                    hardened: true,
482                    value: 84,
483                },
484                PathComponent {
485                    hardened: true,
486                    value: 0,
487                },
488                PathComponent {
489                    hardened: true,
490                    value: 0,
491                },
492            ],
493        }
494    }
495
496    /// Build a single-key descriptor with `n=1`, the given tree root, an
497    /// empty shared path_decl (origin elided on wire), and an empty TLV
498    /// section.
499    fn single_key_descriptor(tree: Node) -> Descriptor {
500        Descriptor {
501            n: 1,
502            path_decl: PathDecl {
503                n: 1,
504                paths: PathDeclPaths::Shared(empty_path()),
505            },
506            use_site_path: UseSitePath::standard_multipath(),
507            tree,
508            tlv: TlvSection::new_empty(),
509        }
510    }
511
512    #[test]
513    fn validate_explicit_origin_required_passes_canonical_wpkh() {
514        // wpkh(@0) has canonical BIP-84 origin → empty path_decl OK.
515        let d = single_key_descriptor(Node {
516            tag: Tag::Wpkh,
517            body: Body::KeyArg { index: 0 },
518        });
519        validate_explicit_origin_required(&d).unwrap();
520    }
521
522    #[test]
523    fn validate_explicit_origin_required_passes_with_overrides_for_non_canonical() {
524        // sh(sortedmulti(@0,@1,@2)) — non-canonical. Must have explicit
525        // origin per @N. Provide overrides for all three.
526        let mut d = Descriptor {
527            n: 3,
528            path_decl: PathDecl {
529                n: 3,
530                paths: PathDeclPaths::Shared(empty_path()),
531            },
532            use_site_path: UseSitePath::standard_multipath(),
533            tree: Node {
534                tag: Tag::Sh,
535                body: Body::Children(vec![Node {
536                    tag: Tag::SortedMulti,
537                    body: Body::MultiKeys {
538                        k: 2,
539                        indices: vec![0, 1, 2],
540                    },
541                }]),
542            },
543            tlv: TlvSection::new_empty(),
544        };
545        d.tlv.origin_path_overrides = Some(vec![
546            (0u8, bip84_path()),
547            (1u8, bip84_path()),
548            (2u8, bip84_path()),
549        ]);
550        validate_explicit_origin_required(&d).unwrap();
551    }
552
553    #[test]
554    fn validate_explicit_origin_required_fails_sh_sortedmulti_with_empty_path_decl() {
555        // sh(sortedmulti(@0,@1,@2)) — non-canonical. Empty path_decl, no
556        // overrides → fails on idx=0.
557        let d = Descriptor {
558            n: 3,
559            path_decl: PathDecl {
560                n: 3,
561                paths: PathDeclPaths::Shared(empty_path()),
562            },
563            use_site_path: UseSitePath::standard_multipath(),
564            tree: Node {
565                tag: Tag::Sh,
566                body: Body::Children(vec![Node {
567                    tag: Tag::SortedMulti,
568                    body: Body::MultiKeys {
569                        k: 2,
570                        indices: vec![0, 1, 2],
571                    },
572                }]),
573            },
574            tlv: TlvSection::new_empty(),
575        };
576        let err = validate_explicit_origin_required(&d).unwrap_err();
577        assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
578    }
579
580    #[test]
581    fn validate_explicit_origin_required_fails_bare_wsh_with_empty_path_decl() {
582        // bare wsh(@0) — non-canonical (no `multi`/`sortedmulti` inner).
583        let d = single_key_descriptor(Node {
584            tag: Tag::Wsh,
585            body: Body::Children(vec![Node {
586                tag: Tag::PkK,
587                body: Body::KeyArg { index: 0 },
588            }]),
589        });
590        let err = validate_explicit_origin_required(&d).unwrap_err();
591        assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
592    }
593
594    #[test]
595    fn validate_explicit_origin_required_passes_tr_keypath_only_with_empty_path_decl() {
596        // tr(@0) key-path only → BIP 86 canonical exists → empty path_decl OK.
597        let d = single_key_descriptor(Node {
598            tag: Tag::Tr,
599            body: Body::Tr {
600                is_nums: false,
601                key_index: 0,
602                tree: None,
603            },
604        });
605        validate_explicit_origin_required(&d).unwrap();
606    }
607
608    #[test]
609    fn validate_explicit_origin_required_fails_tr_with_taptree_with_empty_path_decl() {
610        // tr(@0, TapTree) → no canonical → must be explicit.
611        let d = single_key_descriptor(Node {
612            tag: Tag::Tr,
613            body: Body::Tr {
614                is_nums: false,
615                key_index: 0,
616                tree: Some(Box::new(Node {
617                    tag: Tag::PkK,
618                    body: Body::KeyArg { index: 0 },
619                })),
620            },
621        });
622        let err = validate_explicit_origin_required(&d).unwrap_err();
623        assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
624    }
625
626    #[test]
627    fn validate_explicit_origin_required_passes_with_populated_shared_path_decl() {
628        // Bare wsh(@0) with a populated shared path_decl — explicit origin
629        // is on the wire via path_decl, so the validator is satisfied even
630        // without an OriginPathOverrides entry.
631        let mut d = single_key_descriptor(Node {
632            tag: Tag::Wsh,
633            body: Body::Children(vec![Node {
634                tag: Tag::PkK,
635                body: Body::KeyArg { index: 0 },
636            }]),
637        });
638        d.path_decl.paths = PathDeclPaths::Shared(bip84_path());
639        validate_explicit_origin_required(&d).unwrap();
640    }
641
642    #[test]
643    fn validate_explicit_origin_required_passes_divergent_when_all_populated() {
644        // sh(sortedmulti(...)) with divergent path_decl, all entries populated.
645        let d = Descriptor {
646            n: 2,
647            path_decl: PathDecl {
648                n: 2,
649                paths: PathDeclPaths::Divergent(vec![bip84_path(), bip84_path()]),
650            },
651            use_site_path: UseSitePath::standard_multipath(),
652            tree: Node {
653                tag: Tag::Sh,
654                body: Body::Children(vec![Node {
655                    tag: Tag::SortedMulti,
656                    body: Body::MultiKeys {
657                        k: 1,
658                        indices: vec![0, 1],
659                    },
660                }]),
661            },
662            tlv: TlvSection::new_empty(),
663        };
664        validate_explicit_origin_required(&d).unwrap();
665    }
666
667    #[test]
668    fn validate_explicit_origin_required_fails_divergent_when_one_idx_empty() {
669        // sh(sortedmulti(...)) with divergent path_decl; @1 has empty path,
670        // no override → fails on idx=1.
671        let d = Descriptor {
672            n: 2,
673            path_decl: PathDecl {
674                n: 2,
675                paths: PathDeclPaths::Divergent(vec![bip84_path(), empty_path()]),
676            },
677            use_site_path: UseSitePath::standard_multipath(),
678            tree: Node {
679                tag: Tag::Sh,
680                body: Body::Children(vec![Node {
681                    tag: Tag::SortedMulti,
682                    body: Body::MultiKeys {
683                        k: 1,
684                        indices: vec![0, 1],
685                    },
686                }]),
687            },
688            tlv: TlvSection::new_empty(),
689        };
690        let err = validate_explicit_origin_required(&d).unwrap_err();
691        assert!(matches!(err, Error::MissingExplicitOrigin { idx: 1 }));
692    }
693}
694
695#[cfg(test)]
696mod xpub_bytes_tests {
697    use super::*;
698    use crate::origin_path::{OriginPath, PathDecl, PathDeclPaths};
699    use crate::tag::Tag;
700    use crate::tlv::TlvSection;
701    use crate::tree::{Body, Node};
702    use crate::use_site_path::UseSitePath;
703
704    /// G (the secp256k1 generator) compressed: 0x02 || x(G).
705    /// Used for "valid pubkey" tests.
706    fn valid_compressed_g() -> [u8; 33] {
707        // x(G) = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
708        let mut out = [0u8; 33];
709        out[0] = 0x02;
710        let x: [u8; 32] = [
711            0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87,
712            0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B,
713            0x16, 0xF8, 0x17, 0x98,
714        ];
715        out[1..].copy_from_slice(&x);
716        out
717    }
718
719    fn descriptor_with_pubkeys(pks: Option<Vec<(u8, [u8; 65])>>) -> Descriptor {
720        let mut d = Descriptor {
721            n: 1,
722            path_decl: PathDecl {
723                n: 1,
724                paths: PathDeclPaths::Shared(OriginPath { components: vec![] }),
725            },
726            use_site_path: UseSitePath::standard_multipath(),
727            tree: Node {
728                tag: Tag::Wpkh,
729                body: Body::KeyArg { index: 0 },
730            },
731            tlv: TlvSection::new_empty(),
732        };
733        d.tlv.pubkeys = pks;
734        d
735    }
736
737    #[test]
738    fn validate_xpub_bytes_template_only_no_op() {
739        let d = descriptor_with_pubkeys(None);
740        validate_xpub_bytes(&d).unwrap();
741    }
742
743    #[test]
744    fn validate_xpub_bytes_passes_for_valid_compressed_pubkey() {
745        let mut xpub = [0u8; 65];
746        // Chain code 0..32 — arbitrary 32 bytes are valid.
747        for (i, b) in xpub[0..32].iter_mut().enumerate() {
748            *b = i as u8;
749        }
750        // Compressed pubkey 32..65 = G.
751        xpub[32..65].copy_from_slice(&valid_compressed_g());
752        let d = descriptor_with_pubkeys(Some(vec![(0u8, xpub)]));
753        validate_xpub_bytes(&d).unwrap();
754    }
755
756    #[test]
757    fn validate_xpub_bytes_fails_for_invalid_pubkey_prefix() {
758        // Prefix 0x04 is uncompressed-marker; not a valid 33-byte compressed
759        // pubkey prefix (only 0x02 / 0x03 are).
760        let mut xpub = [0u8; 65];
761        xpub[32] = 0x04;
762        let d = descriptor_with_pubkeys(Some(vec![(0u8, xpub)]));
763        let err = validate_xpub_bytes(&d).unwrap_err();
764        assert!(matches!(err, Error::InvalidXpubBytes { idx: 0 }));
765    }
766
767    #[test]
768    fn validate_xpub_bytes_fails_for_off_curve_x_coordinate() {
769        // 0x02 || all-0xFF x-coord. x = p-1 wraps to a non-curve x in
770        // most cases; in particular this exact value fails to lift in
771        // libsecp256k1's compressed-point parser. Verify via the same
772        // routine the validator uses.
773        let mut xpub = [0u8; 65];
774        xpub[32] = 0x02;
775        for b in xpub[33..65].iter_mut() {
776            *b = 0xFF;
777        }
778        // Sanity: confirm bitcoin's parser actually rejects this, so the
779        // test exercises the failure path in our validator.
780        assert!(bitcoin::secp256k1::PublicKey::from_slice(&xpub[32..65]).is_err());
781        let d = descriptor_with_pubkeys(Some(vec![(0u8, xpub)]));
782        let err = validate_xpub_bytes(&d).unwrap_err();
783        assert!(matches!(err, Error::InvalidXpubBytes { idx: 0 }));
784    }
785
786    #[test]
787    fn validate_xpub_bytes_reports_first_failing_idx() {
788        // Two entries: idx=0 valid, idx=2 invalid → error reports idx=2.
789        let mut good = [0u8; 65];
790        good[32..65].copy_from_slice(&valid_compressed_g());
791        let mut bad = [0u8; 65];
792        bad[32] = 0x04; // invalid prefix
793        let d = descriptor_with_pubkeys(Some(vec![(0u8, good), (2u8, bad)]));
794        let err = validate_xpub_bytes(&d).unwrap_err();
795        assert!(matches!(err, Error::InvalidXpubBytes { idx: 2 }));
796    }
797}