use crate::encode::Descriptor;
use crate::error::Error;
use crate::origin_path::{OriginPath, PathDeclPaths};
use crate::tree::{Body, Node};
use crate::use_site_path::UseSitePath;
fn walk_collect_first(node: &Node, seen: &mut [bool], first_occurrences: &mut Vec<u8>) {
match &node.body {
Body::KeyArg { index } => {
if let Some(slot) = seen.get_mut(*index as usize) {
if !*slot {
*slot = true;
first_occurrences.push(*index);
}
}
}
Body::Tr {
is_nums,
key_index,
tree,
} => {
if !*is_nums {
if let Some(slot) = seen.get_mut(*key_index as usize) {
if !*slot {
*slot = true;
first_occurrences.push(*key_index);
}
}
}
if let Some(t) = tree {
walk_collect_first(t, seen, first_occurrences);
}
}
Body::Children(children) => {
for c in children {
walk_collect_first(c, seen, first_occurrences);
}
}
Body::Variable { children, .. } => {
for c in children {
walk_collect_first(c, seen, first_occurrences);
}
}
Body::MultiKeys { indices, .. } => {
for idx in indices {
if let Some(slot) = seen.get_mut(*idx as usize) {
if !*slot {
*slot = true;
first_occurrences.push(*idx);
}
}
}
}
Body::Hash256Body(_) | Body::Hash160Body(_) | Body::Timelock(_) | Body::Empty => {}
}
}
fn remap_indices(node: &mut Node, perm: &[u8]) {
match &mut node.body {
Body::KeyArg { index } => {
*index = perm[*index as usize];
}
Body::Tr {
is_nums,
key_index,
tree,
} => {
if !*is_nums {
*key_index = perm[*key_index as usize];
}
if let Some(t) = tree {
remap_indices(t, perm);
}
}
Body::Children(children) => {
for c in children {
remap_indices(c, perm);
}
}
Body::Variable { children, .. } => {
for c in children {
remap_indices(c, perm);
}
}
Body::MultiKeys { indices, .. } => {
for idx in indices.iter_mut() {
*idx = perm[*idx as usize];
}
}
Body::Hash256Body(_) | Body::Hash160Body(_) | Body::Timelock(_) | Body::Empty => {}
}
}
fn remap_tlv_vec<T>(entries: &mut [(u8, T)], perm: &[u8]) {
for (idx, _) in entries.iter_mut() {
*idx = perm[*idx as usize];
}
entries.sort_by_key(|(idx, _)| *idx);
}
pub fn canonicalize_placeholder_indices(d: &mut Descriptor) -> Result<(), Error> {
let n = d.n as usize;
check_placeholder_bounds(&d.tree, d.n)?;
let mut seen = vec![false; n];
let mut first_occurrences: Vec<u8> = Vec::with_capacity(n);
walk_collect_first(&d.tree, &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: d.n,
});
}
}
let mut perm = vec![0u8; n];
for (new_idx, &old_idx) in first_occurrences.iter().enumerate() {
perm[old_idx as usize] = new_idx as u8;
}
if perm.iter().enumerate().all(|(i, p)| i as u8 == *p) {
return Ok(());
}
remap_indices(&mut d.tree, &perm);
if let PathDeclPaths::Divergent(paths) = &mut d.path_decl.paths {
let mut inverse = vec![0u8; n];
for (old, &new) in perm.iter().enumerate() {
inverse[new as usize] = old as u8;
}
let old_paths = std::mem::take(paths);
let mut new_paths = Vec::with_capacity(n);
for new_idx in 0..n {
new_paths.push(old_paths[inverse[new_idx] as usize].clone());
}
*paths = new_paths;
}
if let Some(v) = d.tlv.use_site_path_overrides.as_mut() {
remap_tlv_vec(v, &perm);
}
if let Some(v) = d.tlv.fingerprints.as_mut() {
remap_tlv_vec(v, &perm);
}
if let Some(v) = d.tlv.pubkeys.as_mut() {
remap_tlv_vec(v, &perm);
}
if let Some(v) = d.tlv.origin_path_overrides.as_mut() {
remap_tlv_vec(v, &perm);
}
debug_assert!(
crate::validate::validate_placeholder_usage(&d.tree, d.n).is_ok(),
"post-condition: tree first-occurrence must be canonical after canonicalize_placeholder_indices",
);
debug_assert!(
tlv_indices_strictly_ascending_and_in_range(d),
"post-condition: every TLV's idx column must be strictly ascending and < n",
);
Ok(())
}
fn check_placeholder_bounds(node: &Node, n: u8) -> Result<(), Error> {
match &node.body {
Body::KeyArg { index } => {
if *index >= n {
return Err(Error::PlaceholderIndexOutOfRange { idx: *index, n });
}
}
Body::Tr {
is_nums,
key_index,
tree,
} => {
if !*is_nums && *key_index >= n {
return Err(Error::NUMSSentinelConflict);
}
if let Some(t) = tree {
check_placeholder_bounds(t, n)?;
}
}
Body::Children(children) => {
for c in children {
check_placeholder_bounds(c, n)?;
}
}
Body::Variable { children, .. } => {
for c in children {
check_placeholder_bounds(c, n)?;
}
}
Body::MultiKeys { indices, .. } => {
for idx in indices {
if *idx >= n {
return Err(Error::PlaceholderIndexOutOfRange { idx: *idx, n });
}
}
}
Body::Hash256Body(_) | Body::Hash160Body(_) | Body::Timelock(_) | Body::Empty => {}
}
Ok(())
}
fn tlv_indices_strictly_ascending_and_in_range(d: &Descriptor) -> bool {
fn check<T>(v: &Option<Vec<(u8, T)>>, n: u8) -> bool {
let Some(v) = v else {
return true;
};
let mut prev: Option<u8> = None;
for (idx, _) in v {
if *idx >= n {
return false;
}
if let Some(p) = prev {
if *idx <= p {
return false;
}
}
prev = Some(*idx);
}
true
}
check(&d.tlv.use_site_path_overrides, d.n)
&& check(&d.tlv.fingerprints, d.n)
&& check(&d.tlv.pubkeys, d.n)
&& check(&d.tlv.origin_path_overrides, d.n)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExpandedKey {
pub idx: u8,
pub origin_path: OriginPath,
pub use_site_path: UseSitePath,
pub fingerprint: Option<[u8; 4]>,
pub xpub: Option<[u8; 65]>,
}
fn sparse_lookup<T>(v: &Option<Vec<(u8, T)>>, idx: u8) -> Option<&T> {
v.as_ref()
.and_then(|entries| entries.iter().find(|(i, _)| *i == idx).map(|(_, t)| t))
}
pub fn expand_per_at_n(d: &Descriptor) -> Result<Vec<ExpandedKey>, Error> {
if let PathDeclPaths::Divergent(paths) = &d.path_decl.paths {
if paths.len() != d.n as usize {
return Err(Error::DivergentPathCountMismatch {
n: d.n,
got: paths.len(),
});
}
}
let mut out = Vec::with_capacity(d.n as usize);
for idx in 0..d.n {
let origin_path = if let Some(p) = sparse_lookup(&d.tlv.origin_path_overrides, idx) {
p.clone()
} else {
match &d.path_decl.paths {
PathDeclPaths::Shared(p) => p.clone(),
PathDeclPaths::Divergent(v) => v[idx as usize].clone(),
}
};
if origin_path.components.is_empty()
&& sparse_lookup(&d.tlv.origin_path_overrides, idx).is_none()
&& crate::canonical_origin::canonical_origin(&d.tree).is_none()
{
return Err(Error::MissingExplicitOrigin { idx });
}
let use_site_path = sparse_lookup(&d.tlv.use_site_path_overrides, idx)
.cloned()
.unwrap_or_else(|| d.use_site_path.clone());
let fingerprint = sparse_lookup(&d.tlv.fingerprints, idx).copied();
let xpub = sparse_lookup(&d.tlv.pubkeys, idx).copied();
out.push(ExpandedKey {
idx,
origin_path,
use_site_path,
fingerprint,
xpub,
});
}
Ok(out)
}
#[cfg(test)]
mod 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 shared_bip84() -> PathDecl {
PathDecl {
n: 1,
paths: PathDeclPaths::Shared(OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 84,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
],
}),
}
}
fn shared_path_decl(n: u8) -> PathDecl {
PathDecl {
n,
paths: PathDeclPaths::Shared(OriginPath {
components: vec![PathComponent {
hardened: true,
value: 48,
}],
}),
}
}
fn no_multipath() -> UseSitePath {
UseSitePath {
multipath: None,
wildcard_hardened: false,
}
}
#[test]
fn identity_permutation_no_op() {
let d = Descriptor {
n: 1,
path_decl: shared_bip84(),
use_site_path: no_multipath(),
tree: Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: false,
key_index: 0,
tree: None,
},
},
tlv: TlvSection::new_empty(),
};
let mut d2 = d.clone();
canonicalize_placeholder_indices(&mut d2).unwrap();
assert_eq!(d, d2);
}
#[test]
fn swap_two_placeholders_in_multi() {
let mut d = Descriptor {
n: 2,
path_decl: shared_path_decl(2),
use_site_path: no_multipath(),
tree: Node {
tag: Tag::Multi,
body: Body::MultiKeys {
k: 2,
indices: vec![1, 0],
},
},
tlv: TlvSection::new_empty(),
};
canonicalize_placeholder_indices(&mut d).unwrap();
let expected_tree = Node {
tag: Tag::Multi,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1],
},
};
assert_eq!(d.tree, expected_tree);
}
#[test]
fn permute_three_placeholders_in_sortedmulti() {
let xpub_a = [0xaa; 65];
let xpub_b = [0xbb; 65];
let xpub_c = [0xcc; 65];
let mut d = Descriptor {
n: 3,
path_decl: shared_path_decl(3),
use_site_path: no_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![2, 0, 1],
},
}]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0, xpub_a), (1, xpub_b), (2, xpub_c)]);
t
},
};
canonicalize_placeholder_indices(&mut d).unwrap();
let expected_tree = Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1, 2],
},
}]),
};
assert_eq!(d.tree, expected_tree);
assert_eq!(
d.tlv.pubkeys.unwrap(),
vec![(0, xpub_c), (1, xpub_a), (2, xpub_b)],
);
}
#[test]
fn permute_with_divergent_path_decl() {
let path_for_at_0 = OriginPath {
components: vec![PathComponent {
hardened: true,
value: 84,
}],
};
let path_for_at_1 = OriginPath {
components: vec![PathComponent {
hardened: true,
value: 86,
}],
};
let mut d = Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
paths: PathDeclPaths::Divergent(vec![path_for_at_0.clone(), path_for_at_1.clone()]),
},
use_site_path: no_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::Multi,
body: Body::MultiKeys {
k: 2,
indices: vec![1, 0],
},
}]),
},
tlv: TlvSection::new_empty(),
};
canonicalize_placeholder_indices(&mut d).unwrap();
match &d.path_decl.paths {
PathDeclPaths::Divergent(paths) => {
assert_eq!(paths[0], path_for_at_1);
assert_eq!(paths[1], path_for_at_0);
}
_ => panic!("expected divergent paths"),
}
}
#[test]
fn permute_with_use_site_path_overrides() {
let custom = UseSitePath::standard_multipath();
let mut d = Descriptor {
n: 2,
path_decl: shared_path_decl(2),
use_site_path: no_multipath(),
tree: Node {
tag: Tag::Multi,
body: Body::MultiKeys {
k: 2,
indices: vec![1, 0],
},
},
tlv: {
let mut t = TlvSection::new_empty();
t.use_site_path_overrides = Some(vec![(1, custom.clone())]);
t
},
};
canonicalize_placeholder_indices(&mut d).unwrap();
assert_eq!(d.tlv.use_site_path_overrides.unwrap(), vec![(0, custom)],);
}
#[test]
fn permute_with_fingerprints_and_pubkeys() {
let fp_a = [0x11, 0x11, 0x11, 0x11];
let fp_b = [0x22, 0x22, 0x22, 0x22];
let fp_c = [0x33, 0x33, 0x33, 0x33];
let xpub_a = [0xaa; 65];
let xpub_b = [0xbb; 65];
let xpub_c = [0xcc; 65];
let mut d = Descriptor {
n: 3,
path_decl: shared_path_decl(3),
use_site_path: no_multipath(),
tree: Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![2, 0, 1],
},
},
tlv: {
let mut t = TlvSection::new_empty();
t.fingerprints = Some(vec![(0, fp_a), (1, fp_b), (2, fp_c)]);
t.pubkeys = Some(vec![(0, xpub_a), (1, xpub_b), (2, xpub_c)]);
t
},
};
canonicalize_placeholder_indices(&mut d).unwrap();
assert_eq!(
d.tlv.fingerprints.unwrap(),
vec![(0, fp_c), (1, fp_a), (2, fp_b)],
);
assert_eq!(
d.tlv.pubkeys.unwrap(),
vec![(0, xpub_c), (1, xpub_a), (2, xpub_b)],
);
}
#[test]
fn permute_with_origin_path_overrides() {
let path_for_at_2 = OriginPath {
components: vec![PathComponent {
hardened: true,
value: 99,
}],
};
let mut d = Descriptor {
n: 3,
path_decl: shared_path_decl(3),
use_site_path: no_multipath(),
tree: Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![2, 0, 1],
},
},
tlv: {
let mut t = TlvSection::new_empty();
t.origin_path_overrides = Some(vec![(2, path_for_at_2.clone())]);
t
},
};
canonicalize_placeholder_indices(&mut d).unwrap();
assert_eq!(
d.tlv.origin_path_overrides.unwrap(),
vec![(0, path_for_at_2)],
);
}
#[test]
fn unreferenced_placeholder_returns_error() {
let mut d = Descriptor {
n: 3,
path_decl: shared_path_decl(3),
use_site_path: no_multipath(),
tree: Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: false,
key_index: 0,
tree: None,
},
},
tlv: TlvSection::new_empty(),
};
let err = canonicalize_placeholder_indices(&mut d).unwrap_err();
assert!(matches!(
err,
Error::PlaceholderNotReferenced { idx: 1, n: 3 }
));
}
#[test]
fn out_of_range_placeholder_returns_error() {
let mut d = Descriptor {
n: 2,
path_decl: shared_path_decl(2),
use_site_path: no_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 5 },
},
tlv: TlvSection::new_empty(),
};
let err = canonicalize_placeholder_indices(&mut d).unwrap_err();
assert!(matches!(
err,
Error::PlaceholderIndexOutOfRange { idx: 5, n: 2 }
));
}
#[test]
fn idempotence() {
let mut d = Descriptor {
n: 3,
path_decl: shared_path_decl(3),
use_site_path: no_multipath(),
tree: Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![2, 0, 1],
},
},
tlv: {
let mut t = TlvSection::new_empty();
t.fingerprints = Some(vec![(0, [1; 4]), (1, [2; 4]), (2, [3; 4])]);
t
},
};
canonicalize_placeholder_indices(&mut d).unwrap();
let after_first = d.clone();
canonicalize_placeholder_indices(&mut d).unwrap();
assert_eq!(d, after_first);
}
#[test]
fn tlv_idx_post_condition() {
let mut d = Descriptor {
n: 3,
path_decl: shared_path_decl(3),
use_site_path: no_multipath(),
tree: Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![2, 0, 1],
},
},
tlv: {
let mut t = TlvSection::new_empty();
t.fingerprints = Some(vec![(0, [1; 4]), (1, [2; 4]), (2, [3; 4])]);
t.pubkeys = Some(vec![(0, [0xaa; 65]), (1, [0xbb; 65]), (2, [0xcc; 65])]);
t
},
};
canonicalize_placeholder_indices(&mut d).unwrap();
assert!(tlv_indices_strictly_ascending_and_in_range(&d));
}
#[test]
fn tree_first_occurrence_post_condition() {
let mut d = Descriptor {
n: 3,
path_decl: shared_path_decl(3),
use_site_path: no_multipath(),
tree: Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![2, 0, 1],
},
},
tlv: TlvSection::new_empty(),
};
canonicalize_placeholder_indices(&mut d).unwrap();
crate::validate::validate_placeholder_usage(&d.tree, d.n).unwrap();
let mut seen = vec![false; d.n as usize];
let mut first = Vec::new();
walk_collect_first(&d.tree, &mut seen, &mut first);
assert_eq!(first, vec![0, 1, 2]);
}
#[test]
fn encoder_canonicalizes_non_canonical_input() {
let d = Descriptor {
n: 2,
path_decl: shared_path_decl(2),
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::Multi,
body: Body::MultiKeys {
k: 2,
indices: vec![1, 0],
},
}]),
},
tlv: TlvSection::new_empty(),
};
let (bytes, total_bits) =
crate::encode::encode_payload(&d).expect("encoder must canonicalize and succeed");
let decoded = crate::decode::decode_payload(&bytes, total_bits).expect("decode");
let mut seen = vec![false; decoded.n as usize];
let mut first = Vec::new();
walk_collect_first(&decoded.tree, &mut seen, &mut first);
assert_eq!(first, vec![0, 1]);
}
#[test]
fn round_trip_canonicalize_encode_decode_canonicalize() {
let permutations: Vec<Vec<u8>> = vec![
vec![0, 1, 2],
vec![0, 2, 1],
vec![1, 0, 2],
vec![1, 2, 0],
vec![2, 0, 1],
vec![2, 1, 0],
vec![1, 0, 1], vec![2, 1, 0], ];
for perm in permutations {
let mut distinct: Vec<u8> = perm.clone();
distinct.sort_unstable();
distinct.dedup();
let n = distinct.len() as u8;
assert!(n >= 2, "test fixture expects ≥2 distinct placeholders");
let mut renumbered = perm.clone();
let mut mapping = std::collections::HashMap::new();
for (i, v) in distinct.iter().enumerate() {
mapping.insert(*v, i as u8);
}
for v in renumbered.iter_mut() {
*v = mapping[v];
}
let indices: Vec<u8> = renumbered.clone();
let n_children = indices.len();
let k_value = std::cmp::min(2u8, n_children as u8);
let mut d = Descriptor {
n,
path_decl: shared_path_decl(n),
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: k_value,
indices,
},
}]),
},
tlv: TlvSection::new_empty(),
};
canonicalize_placeholder_indices(&mut d).unwrap();
let canonical = d.clone();
let (bytes, total_bits) = crate::encode::encode_payload(&d).expect("encode");
let decoded = crate::decode::decode_payload(&bytes, total_bits).expect("decode");
let mut decoded_mut = decoded;
canonicalize_placeholder_indices(&mut decoded_mut).unwrap();
assert_eq!(canonical, decoded_mut);
}
}
}
#[cfg(test)]
mod expand_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 bip84() -> OriginPath {
OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 84,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
],
}
}
fn bip48_type_2() -> OriginPath {
OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 48,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 2,
},
],
}
}
#[test]
fn expand_full_elision_canonical_wpkh() {
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(bip84()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: TlvSection::new_empty(),
};
let expanded = expand_per_at_n(&d).unwrap();
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0].idx, 0);
assert_eq!(expanded[0].origin_path, bip84());
assert_eq!(expanded[0].use_site_path, UseSitePath::standard_multipath());
assert!(expanded[0].fingerprint.is_none());
assert!(expanded[0].xpub.is_none());
}
#[test]
fn expand_full_elision_canonical_wsh_multi() {
let d = Descriptor {
n: 3,
path_decl: PathDecl {
n: 3,
paths: PathDeclPaths::Shared(bip48_type_2()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1, 2],
},
}]),
},
tlv: TlvSection::new_empty(),
};
let expanded = expand_per_at_n(&d).unwrap();
assert_eq!(expanded.len(), 3);
for ek in &expanded {
assert_eq!(ek.origin_path, bip48_type_2());
assert_eq!(ek.use_site_path, UseSitePath::standard_multipath());
assert!(ek.fingerprint.is_none());
assert!(ek.xpub.is_none());
}
}
#[test]
fn expand_per_idx_override_mix() {
let custom_path = OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 84,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 5,
},
],
};
let d = Descriptor {
n: 3,
path_decl: PathDecl {
n: 3,
paths: PathDeclPaths::Shared(bip48_type_2()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1, 2],
},
}]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.origin_path_overrides = Some(vec![(1, custom_path.clone())]);
t
},
};
let expanded = expand_per_at_n(&d).unwrap();
assert_eq!(expanded[0].origin_path, bip48_type_2());
assert_eq!(expanded[1].origin_path, custom_path);
assert_eq!(expanded[2].origin_path, bip48_type_2());
}
#[test]
fn expand_divergent_paths() {
let path_a = OriginPath {
components: vec![PathComponent {
hardened: true,
value: 84,
}],
};
let path_b = OriginPath {
components: vec![PathComponent {
hardened: true,
value: 86,
}],
};
let d = Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
paths: PathDeclPaths::Divergent(vec![path_a.clone(), path_b.clone()]),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::Multi,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1],
},
}]),
},
tlv: TlvSection::new_empty(),
};
let expanded = expand_per_at_n(&d).unwrap();
assert_eq!(expanded[0].origin_path, path_a);
assert_eq!(expanded[1].origin_path, path_b);
}
#[test]
fn expand_use_site_path_overrides() {
let baseline = UseSitePath::standard_multipath();
let custom = UseSitePath {
multipath: None,
wildcard_hardened: true,
};
let d = Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
paths: PathDeclPaths::Shared(bip48_type_2()),
},
use_site_path: baseline.clone(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::Multi,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1],
},
}]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.use_site_path_overrides = Some(vec![(0, custom.clone())]);
t
},
};
let expanded = expand_per_at_n(&d).unwrap();
assert_eq!(expanded[0].use_site_path, custom);
assert_eq!(expanded[1].use_site_path, baseline);
}
#[test]
fn expand_fingerprints_and_pubkeys() {
let fp = [0xaa, 0xbb, 0xcc, 0xdd];
let mut xpub = [0u8; 65];
for (i, b) in xpub.iter_mut().enumerate() {
*b = i as u8;
}
let d = Descriptor {
n: 3,
path_decl: PathDecl {
n: 3,
paths: PathDeclPaths::Shared(bip48_type_2()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1, 2],
},
}]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.fingerprints = Some(vec![(0, fp)]);
t.pubkeys = Some(vec![(2, xpub)]);
t
},
};
let expanded = expand_per_at_n(&d).unwrap();
assert_eq!(expanded[0].fingerprint, Some(fp));
assert!(expanded[1].fingerprint.is_none());
assert!(expanded[2].fingerprint.is_none());
assert!(expanded[0].xpub.is_none());
assert!(expanded[1].xpub.is_none());
assert_eq!(expanded[2].xpub, Some(xpub));
}
#[test]
fn expand_non_canonical_wrapper_without_overrides_errors() {
let empty_path = OriginPath { components: vec![] };
let d = Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
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],
},
}]),
},
tlv: TlvSection::new_empty(),
};
let err = expand_per_at_n(&d).unwrap_err();
assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
}
#[test]
fn expand_determinism_across_elision() {
let d_elided = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(bip84()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: TlvSection::new_empty(),
};
let d_explicit = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(bip84()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: TlvSection::new_empty(),
};
assert_eq!(
expand_per_at_n(&d_elided).unwrap(),
expand_per_at_n(&d_explicit).unwrap()
);
}
#[test]
fn expand_after_canonicalize_uses_canonical_indices() {
let xpub_a = [0xaa; 65];
let xpub_b = [0xbb; 65];
let mut d = Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
paths: PathDeclPaths::Shared(bip48_type_2()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Multi,
body: Body::MultiKeys {
k: 2,
indices: vec![1, 0],
},
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0, xpub_a), (1, xpub_b)]);
t
},
};
canonicalize_placeholder_indices(&mut d).unwrap();
let expanded = expand_per_at_n(&d).unwrap();
assert_eq!(expanded[0].xpub, Some(xpub_b));
assert_eq!(expanded[1].xpub, Some(xpub_a));
}
}