use crate::canonical_origin::canonical_origin;
use crate::encode::Descriptor;
use crate::error::Error;
use crate::origin_path::PathDeclPaths;
use crate::tag::Tag;
use crate::tree::{Body, Node};
use crate::use_site_path::UseSitePath;
pub fn validate_placeholder_usage(root: &Node, n: u8) -> Result<(), Error> {
let mut seen = vec![false; n as usize];
let mut first_occurrences: Vec<u8> = Vec::new();
walk_for_placeholders(root, &mut seen, &mut first_occurrences)?;
for (i, was_seen) in seen.iter().enumerate() {
if !was_seen {
return Err(Error::PlaceholderNotReferenced { idx: i as u8, n });
}
}
for (pos, idx) in first_occurrences.iter().enumerate() {
if *idx as usize != pos {
return Err(Error::PlaceholderFirstOccurrenceOutOfOrder {
expected_first: pos as u8,
got_first: *idx,
});
}
}
Ok(())
}
fn walk_for_placeholders(
node: &Node,
seen: &mut [bool],
first_occurrences: &mut Vec<u8>,
) -> Result<(), Error> {
match &node.body {
Body::KeyArg { index } => {
if (*index as usize) >= seen.len() {
return Err(Error::PlaceholderIndexOutOfRange {
idx: *index,
n: seen.len() as u8,
});
}
if !seen[*index as usize] {
seen[*index as usize] = true;
first_occurrences.push(*index);
}
}
Body::Children(children) => {
for c in children {
walk_for_placeholders(c, seen, first_occurrences)?;
}
}
Body::Variable { children, .. } => {
for c in children {
walk_for_placeholders(c, seen, first_occurrences)?;
}
}
Body::MultiKeys { indices, .. } => {
for index in indices {
if (*index as usize) >= seen.len() {
return Err(Error::PlaceholderIndexOutOfRange {
idx: *index,
n: seen.len() as u8,
});
}
if !seen[*index as usize] {
seen[*index as usize] = true;
first_occurrences.push(*index);
}
}
}
Body::Tr {
is_nums,
key_index,
tree,
} => {
if !*is_nums {
if (*key_index as usize) >= seen.len() {
return Err(Error::NUMSSentinelConflict);
}
if !seen[*key_index as usize] {
seen[*key_index as usize] = true;
first_occurrences.push(*key_index);
}
}
if let Some(t) = tree {
walk_for_placeholders(t, seen, first_occurrences)?;
}
}
Body::Hash256Body(_) | Body::Hash160Body(_) | Body::Timelock(_) | Body::Empty => {}
}
Ok(())
}
pub fn validate_multipath_consistency(
shared: &UseSitePath,
overrides: &[(u8, UseSitePath)],
) -> Result<(), Error> {
let mut seen_alt_count: Option<usize> = None;
let candidates = std::iter::once(shared).chain(overrides.iter().map(|(_, p)| p));
for path in candidates {
if let Some(alts) = &path.multipath {
match seen_alt_count {
None => seen_alt_count = Some(alts.len()),
Some(prev) if prev == alts.len() => {}
Some(prev) => {
return Err(Error::MultipathAltCountMismatch {
expected: prev,
got: alts.len(),
});
}
}
}
}
Ok(())
}
pub fn validate_tap_script_tree(node: &Node) -> Result<(), Error> {
walk_tap_tree_leaves(node)
}
fn walk_tap_tree_leaves(node: &Node) -> Result<(), Error> {
if matches!(node.tag, Tag::TapTree) {
if let Body::Children(children) = &node.body {
for c in children {
walk_tap_tree_leaves(c)?;
}
}
Ok(())
} else {
if is_forbidden_leaf_tag(node.tag) {
return Err(Error::ForbiddenTapTreeLeaf {
tag: node.tag.codes().0,
});
}
Ok(())
}
}
fn is_forbidden_leaf_tag(tag: Tag) -> bool {
matches!(
tag,
Tag::Wpkh | Tag::Tr | Tag::Wsh | Tag::Sh | Tag::Pkh | Tag::Multi | Tag::SortedMulti
)
}
pub fn validate_explicit_origin_required(d: &Descriptor) -> Result<(), Error> {
if canonical_origin(&d.tree).is_some() {
return Ok(());
}
let overrides = d.tlv.origin_path_overrides.as_deref().unwrap_or(&[]);
for idx in 0..d.n {
if let Some((_, op)) = overrides.iter().find(|(i, _)| *i == idx) {
if !op.components.is_empty() {
continue;
}
}
let decl_components_empty = match &d.path_decl.paths {
PathDeclPaths::Shared(p) => p.components.is_empty(),
PathDeclPaths::Divergent(v) => v
.get(idx as usize)
.map(|p| p.components.is_empty())
.unwrap_or(true),
};
if decl_components_empty {
return Err(Error::MissingExplicitOrigin { idx });
}
}
Ok(())
}
pub fn validate_xpub_bytes(d: &Descriptor) -> Result<(), Error> {
let Some(entries) = d.tlv.pubkeys.as_deref() else {
return Ok(());
};
for (idx, xpub) in entries {
if bitcoin::secp256k1::PublicKey::from_slice(&xpub[32..65]).is_err() {
return Err(Error::InvalidXpubBytes { idx: *idx });
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tag::Tag;
use crate::tree::{Body, Node};
#[test]
fn placeholder_usage_ok_for_2_of_3() {
let root = Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1, 2],
},
};
validate_placeholder_usage(&root, 3).unwrap();
}
#[test]
fn placeholder_usage_rejects_unreferenced() {
let root = Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 1,
indices: vec![0, 1],
},
};
assert!(matches!(
validate_placeholder_usage(&root, 3),
Err(Error::PlaceholderNotReferenced { idx: 2, n: 3 })
));
}
#[test]
fn placeholder_usage_rejects_out_of_order_first_occurrences() {
let root = Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 1,
indices: vec![1, 0],
},
};
assert!(matches!(
validate_placeholder_usage(&root, 2),
Err(Error::PlaceholderFirstOccurrenceOutOfOrder { .. })
));
}
#[test]
fn multipath_consistency_ok_when_all_match() {
let shared = UseSitePath::standard_multipath();
let overrides = vec![(1u8, UseSitePath::standard_multipath())];
validate_multipath_consistency(&shared, &overrides).unwrap();
}
#[test]
fn multipath_consistency_rejects_mismatched_alt_counts() {
use crate::use_site_path::Alternative;
let shared = UseSitePath::standard_multipath();
let overrides = vec![(
1u8,
UseSitePath {
multipath: Some(vec![
Alternative {
hardened: false,
value: 0,
},
Alternative {
hardened: false,
value: 1,
},
Alternative {
hardened: false,
value: 2,
},
]),
wildcard_hardened: false,
},
)];
assert!(matches!(
validate_multipath_consistency(&shared, &overrides),
Err(Error::MultipathAltCountMismatch {
expected: 2,
got: 3
})
));
}
#[test]
fn tap_tree_leaf_rejects_wsh() {
let leaf = Node {
tag: Tag::Wsh,
body: Body::Children(vec![]),
};
assert!(matches!(
validate_tap_script_tree(&leaf),
Err(Error::ForbiddenTapTreeLeaf { .. })
));
}
#[test]
fn tap_tree_leaf_accepts_pk_k() {
let leaf = Node {
tag: Tag::PkK,
body: Body::KeyArg { index: 0 },
};
validate_tap_script_tree(&leaf).unwrap();
}
#[test]
fn placeholder_usage_rejects_index_out_of_range_n3() {
let root = Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 3 },
};
let err = validate_placeholder_usage(&root, 3).unwrap_err();
assert!(matches!(
err,
Error::PlaceholderIndexOutOfRange { idx: 3, n: 3 }
));
}
#[test]
fn placeholder_usage_rejects_index_out_of_range_n5() {
let root = Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 1,
indices: vec![5],
},
};
let err = validate_placeholder_usage(&root, 5).unwrap_err();
assert!(matches!(
err,
Error::PlaceholderIndexOutOfRange { idx: 5, n: 5 }
));
}
#[test]
fn placeholder_usage_rejects_index_out_of_range_n15() {
let root = Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 1,
indices: vec![15],
},
};
let err = validate_placeholder_usage(&root, 15).unwrap_err();
assert!(matches!(
err,
Error::PlaceholderIndexOutOfRange { idx: 15, n: 15 }
));
}
#[test]
fn placeholder_usage_rejects_out_of_range_in_tr_key_index() {
let root = Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: false,
key_index: 3,
tree: None,
},
};
let err = validate_placeholder_usage(&root, 3).unwrap_err();
assert!(matches!(err, Error::NUMSSentinelConflict));
}
#[test]
fn placeholder_usage_accepts_nums_flag_in_tr() {
let root = Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: true,
key_index: 0,
tree: Some(Box::new(Node {
tag: Tag::PkK,
body: Body::KeyArg { index: 0 },
})),
},
};
validate_placeholder_usage(&root, 1)
.expect("is_nums flag + @0 reference must validate under v0.30");
}
}
#[cfg(test)]
mod explicit_origin_required_tests {
use super::*;
use crate::origin_path::{OriginPath, PathComponent, PathDecl, PathDeclPaths};
use crate::tag::Tag;
use crate::tlv::TlvSection;
use crate::tree::{Body, Node};
use crate::use_site_path::UseSitePath;
fn empty_path() -> OriginPath {
OriginPath { components: vec![] }
}
fn bip84_path() -> OriginPath {
OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 84,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
],
}
}
fn single_key_descriptor(tree: Node) -> Descriptor {
Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(empty_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree,
tlv: TlvSection::new_empty(),
}
}
#[test]
fn validate_explicit_origin_required_passes_canonical_wpkh() {
let d = single_key_descriptor(Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
});
validate_explicit_origin_required(&d).unwrap();
}
#[test]
fn validate_explicit_origin_required_passes_with_overrides_for_non_canonical() {
let mut d = Descriptor {
n: 3,
path_decl: PathDecl {
n: 3,
paths: PathDeclPaths::Shared(empty_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Sh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1, 2],
},
}]),
},
tlv: TlvSection::new_empty(),
};
d.tlv.origin_path_overrides = Some(vec![
(0u8, bip84_path()),
(1u8, bip84_path()),
(2u8, bip84_path()),
]);
validate_explicit_origin_required(&d).unwrap();
}
#[test]
fn validate_explicit_origin_required_fails_sh_sortedmulti_with_empty_path_decl() {
let d = Descriptor {
n: 3,
path_decl: PathDecl {
n: 3,
paths: PathDeclPaths::Shared(empty_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Sh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1, 2],
},
}]),
},
tlv: TlvSection::new_empty(),
};
let err = validate_explicit_origin_required(&d).unwrap_err();
assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
}
#[test]
fn validate_explicit_origin_required_fails_bare_wsh_with_empty_path_decl() {
let d = single_key_descriptor(Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::PkK,
body: Body::KeyArg { index: 0 },
}]),
});
let err = validate_explicit_origin_required(&d).unwrap_err();
assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
}
#[test]
fn validate_explicit_origin_required_passes_tr_keypath_only_with_empty_path_decl() {
let d = single_key_descriptor(Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: false,
key_index: 0,
tree: None,
},
});
validate_explicit_origin_required(&d).unwrap();
}
#[test]
fn validate_explicit_origin_required_fails_tr_with_taptree_with_empty_path_decl() {
let d = single_key_descriptor(Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: false,
key_index: 0,
tree: Some(Box::new(Node {
tag: Tag::PkK,
body: Body::KeyArg { index: 0 },
})),
},
});
let err = validate_explicit_origin_required(&d).unwrap_err();
assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
}
#[test]
fn validate_explicit_origin_required_passes_with_populated_shared_path_decl() {
let mut d = single_key_descriptor(Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::PkK,
body: Body::KeyArg { index: 0 },
}]),
});
d.path_decl.paths = PathDeclPaths::Shared(bip84_path());
validate_explicit_origin_required(&d).unwrap();
}
#[test]
fn validate_explicit_origin_required_passes_divergent_when_all_populated() {
let d = Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
paths: PathDeclPaths::Divergent(vec![bip84_path(), bip84_path()]),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Sh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 1,
indices: vec![0, 1],
},
}]),
},
tlv: TlvSection::new_empty(),
};
validate_explicit_origin_required(&d).unwrap();
}
#[test]
fn validate_explicit_origin_required_fails_divergent_when_one_idx_empty() {
let d = Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
paths: PathDeclPaths::Divergent(vec![bip84_path(), empty_path()]),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Sh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 1,
indices: vec![0, 1],
},
}]),
},
tlv: TlvSection::new_empty(),
};
let err = validate_explicit_origin_required(&d).unwrap_err();
assert!(matches!(err, Error::MissingExplicitOrigin { idx: 1 }));
}
}
#[cfg(test)]
mod xpub_bytes_tests {
use super::*;
use crate::origin_path::{OriginPath, PathDecl, PathDeclPaths};
use crate::tag::Tag;
use crate::tlv::TlvSection;
use crate::tree::{Body, Node};
use crate::use_site_path::UseSitePath;
fn valid_compressed_g() -> [u8; 33] {
let mut out = [0u8; 33];
out[0] = 0x02;
let x: [u8; 32] = [
0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87,
0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B,
0x16, 0xF8, 0x17, 0x98,
];
out[1..].copy_from_slice(&x);
out
}
fn descriptor_with_pubkeys(pks: Option<Vec<(u8, [u8; 65])>>) -> Descriptor {
let mut d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(OriginPath { components: vec![] }),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: TlvSection::new_empty(),
};
d.tlv.pubkeys = pks;
d
}
#[test]
fn validate_xpub_bytes_template_only_no_op() {
let d = descriptor_with_pubkeys(None);
validate_xpub_bytes(&d).unwrap();
}
#[test]
fn validate_xpub_bytes_passes_for_valid_compressed_pubkey() {
let mut xpub = [0u8; 65];
for (i, b) in xpub[0..32].iter_mut().enumerate() {
*b = i as u8;
}
xpub[32..65].copy_from_slice(&valid_compressed_g());
let d = descriptor_with_pubkeys(Some(vec![(0u8, xpub)]));
validate_xpub_bytes(&d).unwrap();
}
#[test]
fn validate_xpub_bytes_fails_for_invalid_pubkey_prefix() {
let mut xpub = [0u8; 65];
xpub[32] = 0x04;
let d = descriptor_with_pubkeys(Some(vec![(0u8, xpub)]));
let err = validate_xpub_bytes(&d).unwrap_err();
assert!(matches!(err, Error::InvalidXpubBytes { idx: 0 }));
}
#[test]
fn validate_xpub_bytes_fails_for_off_curve_x_coordinate() {
let mut xpub = [0u8; 65];
xpub[32] = 0x02;
for b in xpub[33..65].iter_mut() {
*b = 0xFF;
}
assert!(bitcoin::secp256k1::PublicKey::from_slice(&xpub[32..65]).is_err());
let d = descriptor_with_pubkeys(Some(vec![(0u8, xpub)]));
let err = validate_xpub_bytes(&d).unwrap_err();
assert!(matches!(err, Error::InvalidXpubBytes { idx: 0 }));
}
#[test]
fn validate_xpub_bytes_reports_first_failing_idx() {
let mut good = [0u8; 65];
good[32..65].copy_from_slice(&valid_compressed_g());
let mut bad = [0u8; 65];
bad[32] = 0x04; let d = descriptor_with_pubkeys(Some(vec![(0u8, good), (2u8, bad)]));
let err = validate_xpub_bytes(&d).unwrap_err();
assert!(matches!(err, Error::InvalidXpubBytes { idx: 2 }));
}
}