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
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 fn valid_compressed_g() -> [u8; 33] {
707 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 for (i, b) in xpub[0..32].iter_mut().enumerate() {
748 *b = i as u8;
749 }
750 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 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 let mut xpub = [0u8; 65];
774 xpub[32] = 0x02;
775 for b in xpub[33..65].iter_mut() {
776 *b = 0xFF;
777 }
778 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 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; 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}