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(
118 shared: &UseSitePath,
119 overrides: &[(u8, UseSitePath)],
120) -> Result<(), Error> {
121 let mut seen_alt_count: Option<usize> = None;
122 let candidates = std::iter::once(shared).chain(overrides.iter().map(|(_, p)| p));
123 for path in candidates {
124 if let Some(alts) = &path.multipath {
125 match seen_alt_count {
126 None => seen_alt_count = Some(alts.len()),
127 Some(prev) if prev == alts.len() => {}
128 Some(prev) => {
129 return Err(Error::MultipathAltCountMismatch {
130 expected: prev,
131 got: alts.len(),
132 });
133 }
134 }
135 }
136 }
137 Ok(())
138}
139
140pub fn validate_tap_script_tree(node: &Node) -> Result<(), Error> {
142 walk_tap_tree_leaves(node)
143}
144
145fn walk_tap_tree_leaves(node: &Node) -> Result<(), Error> {
146 if matches!(node.tag, Tag::TapTree) {
147 if let Body::Children(children) = &node.body {
148 for c in children {
149 walk_tap_tree_leaves(c)?;
150 }
151 }
152 Ok(())
153 } else {
154 if is_forbidden_leaf_tag(node.tag) {
156 return Err(Error::ForbiddenTapTreeLeaf {
157 tag: node.tag.codes().0,
158 });
159 }
160 Ok(())
161 }
162}
163
164fn is_forbidden_leaf_tag(tag: Tag) -> bool {
165 matches!(
166 tag,
167 Tag::Wpkh | Tag::Tr | Tag::Wsh | Tag::Sh | Tag::Pkh | Tag::Multi | Tag::SortedMulti
168 )
169}
170
171pub fn validate_explicit_origin_required(d: &Descriptor) -> Result<(), Error> {
183 if canonical_origin(&d.tree).is_some() {
184 return Ok(());
185 }
186 let overrides = d.tlv.origin_path_overrides.as_deref().unwrap_or(&[]);
187 for idx in 0..d.n {
188 if let Some((_, op)) = overrides.iter().find(|(i, _)| *i == idx) {
190 if !op.components.is_empty() {
191 continue;
192 }
193 }
194 let decl_components_empty = match &d.path_decl.paths {
196 PathDeclPaths::Shared(p) => p.components.is_empty(),
197 PathDeclPaths::Divergent(v) => v
198 .get(idx as usize)
199 .map(|p| p.components.is_empty())
200 .unwrap_or(true),
201 };
202 if decl_components_empty {
203 return Err(Error::MissingExplicitOrigin { idx });
204 }
205 }
206 Ok(())
207}
208
209pub fn validate_xpub_bytes(d: &Descriptor) -> Result<(), Error> {
217 let Some(entries) = d.tlv.pubkeys.as_deref() else {
218 return Ok(());
219 };
220 for (idx, xpub) in entries {
221 if bitcoin::secp256k1::PublicKey::from_slice(&xpub[32..65]).is_err() {
222 return Err(Error::InvalidXpubBytes { idx: *idx });
223 }
224 }
225 Ok(())
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::tag::Tag;
232 use crate::tree::{Body, Node};
233
234 #[test]
235 fn placeholder_usage_ok_for_2_of_3() {
236 let root = Node {
237 tag: Tag::SortedMulti,
238 body: Body::MultiKeys {
239 k: 2,
240 indices: vec![0, 1, 2],
241 },
242 };
243 validate_placeholder_usage(&root, 3).unwrap();
244 }
245
246 #[test]
247 fn placeholder_usage_rejects_unreferenced() {
248 let root = Node {
249 tag: Tag::SortedMulti,
250 body: Body::MultiKeys {
251 k: 1,
252 indices: vec![0, 1],
253 },
254 };
255 assert!(matches!(
256 validate_placeholder_usage(&root, 3),
257 Err(Error::PlaceholderNotReferenced { idx: 2, n: 3 })
258 ));
259 }
260
261 #[test]
262 fn placeholder_usage_rejects_out_of_order_first_occurrences() {
263 let root = Node {
264 tag: Tag::SortedMulti,
265 body: Body::MultiKeys {
266 k: 1,
267 indices: vec![1, 0],
268 },
269 };
270 assert!(matches!(
271 validate_placeholder_usage(&root, 2),
272 Err(Error::PlaceholderFirstOccurrenceOutOfOrder { .. })
273 ));
274 }
275
276 #[test]
277 fn multipath_consistency_ok_when_all_match() {
278 let shared = UseSitePath::standard_multipath();
279 let overrides = vec![(1u8, UseSitePath::standard_multipath())];
280 validate_multipath_consistency(&shared, &overrides).unwrap();
281 }
282
283 #[test]
284 fn multipath_consistency_rejects_mismatched_alt_counts() {
285 use crate::use_site_path::Alternative;
286 let shared = UseSitePath::standard_multipath();
287 let overrides = vec![(
288 1u8,
289 UseSitePath {
290 multipath: Some(vec![
291 Alternative {
292 hardened: false,
293 value: 0,
294 },
295 Alternative {
296 hardened: false,
297 value: 1,
298 },
299 Alternative {
300 hardened: false,
301 value: 2,
302 },
303 ]),
304 wildcard_hardened: false,
305 },
306 )];
307 assert!(matches!(
308 validate_multipath_consistency(&shared, &overrides),
309 Err(Error::MultipathAltCountMismatch {
310 expected: 2,
311 got: 3
312 })
313 ));
314 }
315
316 #[test]
317 fn tap_tree_leaf_rejects_wsh() {
318 let leaf = Node {
319 tag: Tag::Wsh,
320 body: Body::Children(vec![]),
321 };
322 assert!(matches!(
323 validate_tap_script_tree(&leaf),
324 Err(Error::ForbiddenTapTreeLeaf { .. })
325 ));
326 }
327
328 #[test]
329 fn tap_tree_leaf_accepts_pk_k() {
330 let leaf = Node {
331 tag: Tag::PkK,
332 body: Body::KeyArg { index: 0 },
333 };
334 validate_tap_script_tree(&leaf).unwrap();
335 }
336
337 #[test]
338 fn placeholder_usage_rejects_index_out_of_range_n3() {
339 let root = Node {
341 tag: Tag::Wpkh,
342 body: Body::KeyArg { index: 3 },
343 };
344 let err = validate_placeholder_usage(&root, 3).unwrap_err();
345 assert!(matches!(
346 err,
347 Error::PlaceholderIndexOutOfRange { idx: 3, n: 3 }
348 ));
349 }
350
351 #[test]
352 fn placeholder_usage_rejects_index_out_of_range_n5() {
353 let root = Node {
355 tag: Tag::SortedMulti,
356 body: Body::MultiKeys {
357 k: 1,
358 indices: vec![5],
359 },
360 };
361 let err = validate_placeholder_usage(&root, 5).unwrap_err();
362 assert!(matches!(
363 err,
364 Error::PlaceholderIndexOutOfRange { idx: 5, n: 5 }
365 ));
366 }
367
368 #[test]
369 fn placeholder_usage_rejects_index_out_of_range_n15() {
370 let root = Node {
372 tag: Tag::SortedMulti,
373 body: Body::MultiKeys {
374 k: 1,
375 indices: vec![15],
376 },
377 };
378 let err = validate_placeholder_usage(&root, 15).unwrap_err();
379 assert!(matches!(
380 err,
381 Error::PlaceholderIndexOutOfRange { idx: 15, n: 15 }
382 ));
383 }
384
385 #[test]
386 fn placeholder_usage_rejects_out_of_range_in_tr_key_index() {
387 let root = Node {
392 tag: Tag::Tr,
393 body: Body::Tr {
394 is_nums: false,
395 key_index: 3,
396 tree: None,
397 },
398 };
399 let err = validate_placeholder_usage(&root, 3).unwrap_err();
400 assert!(matches!(err, Error::NUMSSentinelConflict));
401 }
402
403 #[test]
404 fn placeholder_usage_accepts_nums_flag_in_tr() {
405 let root = Node {
410 tag: Tag::Tr,
411 body: Body::Tr {
412 is_nums: true,
413 key_index: 0,
414 tree: Some(Box::new(Node {
415 tag: Tag::PkK,
416 body: Body::KeyArg { index: 0 },
417 })),
418 },
419 };
420 validate_placeholder_usage(&root, 1)
421 .expect("is_nums flag + @0 reference must validate under v0.30");
422 }
423}
424
425#[cfg(test)]
426mod explicit_origin_required_tests {
427 use super::*;
428 use crate::origin_path::{OriginPath, PathComponent, PathDecl, PathDeclPaths};
429 use crate::tag::Tag;
430 use crate::tlv::TlvSection;
431 use crate::tree::{Body, Node};
432 use crate::use_site_path::UseSitePath;
433
434 fn empty_path() -> OriginPath {
435 OriginPath { components: vec![] }
436 }
437
438 fn bip84_path() -> OriginPath {
439 OriginPath {
440 components: vec![
441 PathComponent {
442 hardened: true,
443 value: 84,
444 },
445 PathComponent {
446 hardened: true,
447 value: 0,
448 },
449 PathComponent {
450 hardened: true,
451 value: 0,
452 },
453 ],
454 }
455 }
456
457 fn single_key_descriptor(tree: Node) -> Descriptor {
461 Descriptor {
462 n: 1,
463 path_decl: PathDecl {
464 n: 1,
465 paths: PathDeclPaths::Shared(empty_path()),
466 },
467 use_site_path: UseSitePath::standard_multipath(),
468 tree,
469 tlv: TlvSection::new_empty(),
470 }
471 }
472
473 #[test]
474 fn validate_explicit_origin_required_passes_canonical_wpkh() {
475 let d = single_key_descriptor(Node {
477 tag: Tag::Wpkh,
478 body: Body::KeyArg { index: 0 },
479 });
480 validate_explicit_origin_required(&d).unwrap();
481 }
482
483 #[test]
484 fn validate_explicit_origin_required_passes_with_overrides_for_non_canonical() {
485 let mut d = Descriptor {
488 n: 3,
489 path_decl: PathDecl {
490 n: 3,
491 paths: PathDeclPaths::Shared(empty_path()),
492 },
493 use_site_path: UseSitePath::standard_multipath(),
494 tree: Node {
495 tag: Tag::Sh,
496 body: Body::Children(vec![Node {
497 tag: Tag::SortedMulti,
498 body: Body::MultiKeys {
499 k: 2,
500 indices: vec![0, 1, 2],
501 },
502 }]),
503 },
504 tlv: TlvSection::new_empty(),
505 };
506 d.tlv.origin_path_overrides = Some(vec![
507 (0u8, bip84_path()),
508 (1u8, bip84_path()),
509 (2u8, bip84_path()),
510 ]);
511 validate_explicit_origin_required(&d).unwrap();
512 }
513
514 #[test]
515 fn validate_explicit_origin_required_fails_sh_sortedmulti_with_empty_path_decl() {
516 let d = Descriptor {
519 n: 3,
520 path_decl: PathDecl {
521 n: 3,
522 paths: PathDeclPaths::Shared(empty_path()),
523 },
524 use_site_path: UseSitePath::standard_multipath(),
525 tree: Node {
526 tag: Tag::Sh,
527 body: Body::Children(vec![Node {
528 tag: Tag::SortedMulti,
529 body: Body::MultiKeys {
530 k: 2,
531 indices: vec![0, 1, 2],
532 },
533 }]),
534 },
535 tlv: TlvSection::new_empty(),
536 };
537 let err = validate_explicit_origin_required(&d).unwrap_err();
538 assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
539 }
540
541 #[test]
542 fn validate_explicit_origin_required_fails_bare_wsh_with_empty_path_decl() {
543 let d = single_key_descriptor(Node {
545 tag: Tag::Wsh,
546 body: Body::Children(vec![Node {
547 tag: Tag::PkK,
548 body: Body::KeyArg { index: 0 },
549 }]),
550 });
551 let err = validate_explicit_origin_required(&d).unwrap_err();
552 assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
553 }
554
555 #[test]
556 fn validate_explicit_origin_required_passes_tr_keypath_only_with_empty_path_decl() {
557 let d = single_key_descriptor(Node {
559 tag: Tag::Tr,
560 body: Body::Tr {
561 is_nums: false,
562 key_index: 0,
563 tree: None,
564 },
565 });
566 validate_explicit_origin_required(&d).unwrap();
567 }
568
569 #[test]
570 fn validate_explicit_origin_required_fails_tr_with_taptree_with_empty_path_decl() {
571 let d = single_key_descriptor(Node {
573 tag: Tag::Tr,
574 body: Body::Tr {
575 is_nums: false,
576 key_index: 0,
577 tree: Some(Box::new(Node {
578 tag: Tag::PkK,
579 body: Body::KeyArg { index: 0 },
580 })),
581 },
582 });
583 let err = validate_explicit_origin_required(&d).unwrap_err();
584 assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
585 }
586
587 #[test]
588 fn validate_explicit_origin_required_passes_with_populated_shared_path_decl() {
589 let mut d = single_key_descriptor(Node {
593 tag: Tag::Wsh,
594 body: Body::Children(vec![Node {
595 tag: Tag::PkK,
596 body: Body::KeyArg { index: 0 },
597 }]),
598 });
599 d.path_decl.paths = PathDeclPaths::Shared(bip84_path());
600 validate_explicit_origin_required(&d).unwrap();
601 }
602
603 #[test]
604 fn validate_explicit_origin_required_passes_divergent_when_all_populated() {
605 let d = Descriptor {
607 n: 2,
608 path_decl: PathDecl {
609 n: 2,
610 paths: PathDeclPaths::Divergent(vec![bip84_path(), bip84_path()]),
611 },
612 use_site_path: UseSitePath::standard_multipath(),
613 tree: Node {
614 tag: Tag::Sh,
615 body: Body::Children(vec![Node {
616 tag: Tag::SortedMulti,
617 body: Body::MultiKeys {
618 k: 1,
619 indices: vec![0, 1],
620 },
621 }]),
622 },
623 tlv: TlvSection::new_empty(),
624 };
625 validate_explicit_origin_required(&d).unwrap();
626 }
627
628 #[test]
629 fn validate_explicit_origin_required_fails_divergent_when_one_idx_empty() {
630 let d = Descriptor {
633 n: 2,
634 path_decl: PathDecl {
635 n: 2,
636 paths: PathDeclPaths::Divergent(vec![bip84_path(), empty_path()]),
637 },
638 use_site_path: UseSitePath::standard_multipath(),
639 tree: Node {
640 tag: Tag::Sh,
641 body: Body::Children(vec![Node {
642 tag: Tag::SortedMulti,
643 body: Body::MultiKeys {
644 k: 1,
645 indices: vec![0, 1],
646 },
647 }]),
648 },
649 tlv: TlvSection::new_empty(),
650 };
651 let err = validate_explicit_origin_required(&d).unwrap_err();
652 assert!(matches!(err, Error::MissingExplicitOrigin { idx: 1 }));
653 }
654}
655
656#[cfg(test)]
657mod xpub_bytes_tests {
658 use super::*;
659 use crate::origin_path::{OriginPath, PathDecl, PathDeclPaths};
660 use crate::tag::Tag;
661 use crate::tlv::TlvSection;
662 use crate::tree::{Body, Node};
663 use crate::use_site_path::UseSitePath;
664
665 fn valid_compressed_g() -> [u8; 33] {
668 let mut out = [0u8; 33];
670 out[0] = 0x02;
671 let x: [u8; 32] = [
672 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87,
673 0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B,
674 0x16, 0xF8, 0x17, 0x98,
675 ];
676 out[1..].copy_from_slice(&x);
677 out
678 }
679
680 fn descriptor_with_pubkeys(pks: Option<Vec<(u8, [u8; 65])>>) -> Descriptor {
681 let mut d = Descriptor {
682 n: 1,
683 path_decl: PathDecl {
684 n: 1,
685 paths: PathDeclPaths::Shared(OriginPath { components: vec![] }),
686 },
687 use_site_path: UseSitePath::standard_multipath(),
688 tree: Node {
689 tag: Tag::Wpkh,
690 body: Body::KeyArg { index: 0 },
691 },
692 tlv: TlvSection::new_empty(),
693 };
694 d.tlv.pubkeys = pks;
695 d
696 }
697
698 #[test]
699 fn validate_xpub_bytes_template_only_no_op() {
700 let d = descriptor_with_pubkeys(None);
701 validate_xpub_bytes(&d).unwrap();
702 }
703
704 #[test]
705 fn validate_xpub_bytes_passes_for_valid_compressed_pubkey() {
706 let mut xpub = [0u8; 65];
707 for (i, b) in xpub[0..32].iter_mut().enumerate() {
709 *b = i as u8;
710 }
711 xpub[32..65].copy_from_slice(&valid_compressed_g());
713 let d = descriptor_with_pubkeys(Some(vec![(0u8, xpub)]));
714 validate_xpub_bytes(&d).unwrap();
715 }
716
717 #[test]
718 fn validate_xpub_bytes_fails_for_invalid_pubkey_prefix() {
719 let mut xpub = [0u8; 65];
722 xpub[32] = 0x04;
723 let d = descriptor_with_pubkeys(Some(vec![(0u8, xpub)]));
724 let err = validate_xpub_bytes(&d).unwrap_err();
725 assert!(matches!(err, Error::InvalidXpubBytes { idx: 0 }));
726 }
727
728 #[test]
729 fn validate_xpub_bytes_fails_for_off_curve_x_coordinate() {
730 let mut xpub = [0u8; 65];
735 xpub[32] = 0x02;
736 for b in xpub[33..65].iter_mut() {
737 *b = 0xFF;
738 }
739 assert!(bitcoin::secp256k1::PublicKey::from_slice(&xpub[32..65]).is_err());
742 let d = descriptor_with_pubkeys(Some(vec![(0u8, xpub)]));
743 let err = validate_xpub_bytes(&d).unwrap_err();
744 assert!(matches!(err, Error::InvalidXpubBytes { idx: 0 }));
745 }
746
747 #[test]
748 fn validate_xpub_bytes_reports_first_failing_idx() {
749 let mut good = [0u8; 65];
751 good[32..65].copy_from_slice(&valid_compressed_g());
752 let mut bad = [0u8; 65];
753 bad[32] = 0x04; let d = descriptor_with_pubkeys(Some(vec![(0u8, good), (2u8, bad)]));
755 let err = validate_xpub_bytes(&d).unwrap_err();
756 assert!(matches!(err, Error::InvalidXpubBytes { idx: 2 }));
757 }
758}