1use 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
11pub 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 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 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 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 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
112pub 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
150pub 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
179pub 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 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
210pub 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 if let Some((_, op)) = overrides.iter().find(|(i, _)| *i == idx) {
229 if !op.components.is_empty() {
230 continue;
231 }
232 }
233 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
248pub 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
267pub 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 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 if let Some((_, op)) = overrides.iter().find(|(i, _)| *i == idx) {
316 if !op.components.is_empty() {
317 continue;
318 }
319 }
320 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[test]
772 fn unresolved_origin_indices_empty_for_canonical_wpkh() {
773 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 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 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 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 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 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 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 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 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 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 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 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 fn valid_compressed_g() -> [u8; 33] {
1045 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 for (i, b) in xpub[0..32].iter_mut().enumerate() {
1086 *b = i as u8;
1087 }
1088 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 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 let mut xpub = [0u8; 65];
1112 xpub[32] = 0x02;
1113 for b in xpub[33..65].iter_mut() {
1114 *b = 0xFF;
1115 }
1116 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 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; 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}