use std::sync::OnceLock;
use md_codec::canonicalize::canonicalize_placeholder_indices;
use md_codec::chunk::{reassemble, split};
use md_codec::decode::{decode_md1_string, decode_payload};
use md_codec::encode::{Descriptor, encode_md1_string, encode_payload};
use md_codec::error::Error;
use md_codec::identity::compute_wallet_policy_id;
use md_codec::origin_path::{OriginPath, PathComponent, PathDecl, PathDeclPaths};
use md_codec::tag::Tag;
use md_codec::tlv::TlvSection;
use md_codec::tree::{Body, Node};
use md_codec::use_site_path::UseSitePath;
use md_codec::validate::{validate_explicit_origin_required, validate_placeholder_usage};
fn bip84_path() -> OriginPath {
OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 84,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
],
}
}
fn bip48_type_2_path() -> OriginPath {
OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 48,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 2,
},
],
}
}
fn bip86_path() -> OriginPath {
OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 86,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
],
}
}
fn bip49_path() -> OriginPath {
OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 49,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
],
}
}
fn empty_path() -> OriginPath {
OriginPath { components: vec![] }
}
fn pkk(index: u8) -> Node {
Node {
tag: Tag::PkK,
body: Body::KeyArg { index },
}
}
fn valid_compressed_pubkey() -> [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 make_xpub(seed: u8) -> [u8; 65] {
let mut x = [0u8; 65];
for b in x[0..32].iter_mut() {
*b = seed;
}
x[32..65].copy_from_slice(&valid_compressed_pubkey());
x
}
fn wpkh_at_0() -> Node {
Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
}
}
fn tr_keypath_at_0() -> Node {
Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: false,
key_index: 0,
tree: None,
},
}
}
fn wsh_sortedmulti_2of3() -> Node {
Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1, 2],
},
}]),
}
}
fn wsh_sortedmulti_2of2() -> Node {
Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1],
},
}]),
}
}
fn cell_7_wpkh_full() -> Descriptor {
let mut d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(bip84_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: wpkh_at_0(),
tlv: TlvSection::new_empty(),
};
d.tlv.fingerprints = Some(vec![(0u8, [0xDE, 0xAD, 0xBE, 0xEF])]);
d.tlv.pubkeys = Some(vec![(0u8, make_xpub(0x11))]);
d
}
fn cell_7_wsh_2of3_full() -> Descriptor {
let mut d = Descriptor {
n: 3,
path_decl: PathDecl {
n: 3,
paths: PathDeclPaths::Shared(bip48_type_2_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: wsh_sortedmulti_2of3(),
tlv: TlvSection::new_empty(),
};
d.tlv.fingerprints = Some(vec![(0u8, [0x11; 4]), (1u8, [0x22; 4]), (2u8, [0x33; 4])]);
d.tlv.pubkeys = Some(vec![
(0u8, make_xpub(0x10)),
(1u8, make_xpub(0x20)),
(2u8, make_xpub(0x30)),
]);
d
}
fn cell_1_wpkh_template_only() -> Descriptor {
Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(bip84_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: wpkh_at_0(),
tlv: TlvSection::new_empty(),
}
}
#[test]
fn smoke_1of1_cell_7_wpkh_round_trip() {
let d = cell_7_wpkh_full();
let s = encode_md1_string(&d).unwrap();
let d2 = decode_md1_string(&s).unwrap();
assert_eq!(d, d2);
assert!(d2.is_wallet_policy(), "cell-7 must be wallet-policy mode");
}
#[test]
fn smoke_2of3_cell_7_wsh_sortedmulti_round_trip() {
let d = cell_7_wsh_2of3_full();
let s = encode_md1_string(&d).unwrap();
let d2 = decode_md1_string(&s).unwrap();
assert_eq!(d, d2);
assert!(d2.is_wallet_policy(), "cell-7 must be wallet-policy mode");
}
#[test]
fn smoke_1of1_cell_1_wpkh_template_only_round_trip() {
let d = cell_1_wpkh_template_only();
let s = encode_md1_string(&d).unwrap();
let d2 = decode_md1_string(&s).unwrap();
assert_eq!(d, d2);
assert!(
!d2.is_wallet_policy(),
"template-only must NOT be wallet-policy mode"
);
}
#[test]
fn canonicalization_stability_wpkh_explicit_vs_redundant_override() {
let d_a = cell_7_wpkh_full();
let mut d_b = cell_7_wpkh_full();
d_b.tlv.origin_path_overrides = Some(vec![(0u8, bip84_path())]);
let id_a = compute_wallet_policy_id(&d_a).unwrap();
let id_b = compute_wallet_policy_id(&d_b).unwrap();
assert_eq!(id_a, id_b);
let s_b = encode_md1_string(&d_b).unwrap();
let d_b_decoded = decode_md1_string(&s_b).unwrap();
assert_eq!(d_b, d_b_decoded);
}
#[test]
fn partial_keys_2of2_at0_cell7_at1_cell1() {
let mut d_partial = Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
paths: PathDeclPaths::Shared(bip48_type_2_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: wsh_sortedmulti_2of2(),
tlv: TlvSection::new_empty(),
};
d_partial.tlv.fingerprints = Some(vec![(0u8, [0xAA; 4])]);
d_partial.tlv.pubkeys = Some(vec![(0u8, make_xpub(0x55))]);
let s = encode_md1_string(&d_partial).unwrap();
let d2 = decode_md1_string(&s).unwrap();
assert_eq!(d_partial, d2);
assert!(
d2.is_wallet_policy(),
"any populated Pubkeys → wallet-policy"
);
let mut d_full = d_partial.clone();
d_full.tlv.fingerprints = Some(vec![(0u8, [0xAA; 4]), (1u8, [0xBB; 4])]);
d_full.tlv.pubkeys = Some(vec![(0u8, make_xpub(0x55)), (1u8, make_xpub(0x66))]);
let id_partial = compute_wallet_policy_id(&d_partial).unwrap();
let id_full = compute_wallet_policy_id(&d_full).unwrap();
assert_ne!(id_partial, id_full);
}
#[test]
fn forced_explicit_sh_sortedmulti_rejected_at_decoder() {
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 = validate_explicit_origin_required(&d).unwrap_err();
assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
let (bytes, total_bits) = encode_payload(&d).unwrap();
let dec_err = decode_payload(&bytes, total_bits).unwrap_err();
assert!(matches!(dec_err, Error::MissingExplicitOrigin { idx: 0 }));
}
fn fixture_v011_template_only() -> &'static (Vec<u8>, usize) {
static F: OnceLock<(Vec<u8>, usize)> = OnceLock::new();
F.get_or_init(|| encode_payload(&cell_1_wpkh_template_only()).unwrap())
}
fn fixture_v013_same_policy() -> &'static (Vec<u8>, usize) {
static F: OnceLock<(Vec<u8>, usize)> = OnceLock::new();
F.get_or_init(|| encode_payload(&cell_7_wpkh_full()).unwrap())
}
#[test]
fn forward_compat_v011_template_only_decodes_under_v013() {
let (bytes, total_bits) = fixture_v011_template_only();
let d = decode_payload(bytes, *total_bits).unwrap();
assert!(d.tlv.pubkeys.is_none(), "template-only → pubkeys = None");
assert!(
d.tlv.origin_path_overrides.is_none(),
"template-only → origin_path_overrides = None"
);
assert!(
d.tlv.unknown.is_empty(),
"template-only fixture must not carry unknown TLVs"
);
}
#[test]
fn forward_compat_v011_template_only_byte_identical_re_encode() {
let (bytes, total_bits) = fixture_v011_template_only();
let d = decode_payload(bytes, *total_bits).unwrap();
let (re_bytes, re_total_bits) = encode_payload(&d).unwrap();
assert_eq!(re_total_bits, *total_bits);
assert_eq!(&re_bytes, bytes);
}
#[test]
fn forward_compat_v013_same_policy_byte_identical_re_encode() {
let (bytes, total_bits) = fixture_v013_same_policy();
let d = decode_payload(bytes, *total_bits).unwrap();
assert!(d.is_wallet_policy(), "fixture is wallet-policy mode");
let (re_bytes, re_total_bits) = encode_payload(&d).unwrap();
assert_eq!(re_total_bits, *total_bits);
assert_eq!(&re_bytes, bytes);
}
#[test]
fn placeholder_ordering_rejected_by_validator() {
let non_canonical_tree = Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::Multi,
body: Body::MultiKeys {
k: 2,
indices: vec![1, 0],
},
}]),
};
let err = validate_placeholder_usage(&non_canonical_tree, 2).unwrap_err();
assert!(matches!(
err,
Error::PlaceholderFirstOccurrenceOutOfOrder { .. }
));
let mut d_non_canonical = Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
paths: PathDeclPaths::Shared(bip48_type_2_path()),
},
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(),
};
canonicalize_placeholder_indices(&mut d_non_canonical).unwrap();
let (bytes, total_bits) = encode_payload(&d_non_canonical).unwrap();
decode_payload(&bytes, total_bits).expect("canonical wire decodes cleanly");
}
#[test]
fn divergent_paths_wallet_policy_2of2_round_trip() {
let path_a = OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 48,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 2,
},
],
};
let path_b = OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 48,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 1,
},
PathComponent {
hardened: true,
value: 2,
},
],
};
let mut 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(),
};
d.tlv.fingerprints = Some(vec![(0u8, [0xAA; 4]), (1u8, [0xBB; 4])]);
d.tlv.pubkeys = Some(vec![(0u8, make_xpub(0x77)), (1u8, make_xpub(0x88))]);
let s = encode_md1_string(&d).unwrap();
let d2 = decode_md1_string(&s).unwrap();
assert_eq!(d, d2);
assert!(d2.is_wallet_policy());
let id_1 = compute_wallet_policy_id(&d).unwrap();
let id_2 = compute_wallet_policy_id(&d2).unwrap();
assert_eq!(id_1, id_2);
}
#[test]
fn multi_chunk_2of3_cell_7_split_reassemble_round_trip() {
let d = cell_7_wsh_2of3_full();
let chunks = split(&d).unwrap();
assert!(
chunks.len() >= 5,
"2-of-3 with full xpubs should require ~5–7 chunks (got {})",
chunks.len()
);
for c in &chunks {
assert!(c.starts_with("md1"));
}
let chunk_refs: Vec<&str> = chunks.iter().map(|s| s.as_str()).collect();
let d2 = reassemble(&chunk_refs).unwrap();
assert_eq!(d, d2);
assert!(d2.is_wallet_policy());
let chunks_2 = split(&d2).unwrap();
assert_eq!(chunks.len(), chunks_2.len());
}
#[test]
fn bare_wsh_at_n_forced_explicit_rejected_with_empty_path() {
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(empty_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![pkk(0)]),
},
tlv: TlvSection::new_empty(),
};
let err = validate_explicit_origin_required(&d).unwrap_err();
assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
let (bytes, total_bits) = encode_payload(&d).unwrap();
let dec_err = decode_payload(&bytes, total_bits).unwrap_err();
assert!(matches!(dec_err, Error::MissingExplicitOrigin { idx: 0 }));
}
#[test]
fn bare_wsh_at_n_accepts_with_populated_path_decl() {
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(bip84_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![pkk(0)]),
},
tlv: TlvSection::new_empty(),
};
validate_explicit_origin_required(&d).unwrap();
let s = encode_md1_string(&d).unwrap();
let d2 = decode_md1_string(&s).unwrap();
assert_eq!(d, d2);
}
#[test]
fn bare_sh_at_n_forced_explicit_rejected_with_empty_path() {
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(empty_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Sh,
body: Body::Children(vec![pkk(0)]),
},
tlv: TlvSection::new_empty(),
};
let err = validate_explicit_origin_required(&d).unwrap_err();
assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
let (bytes, total_bits) = encode_payload(&d).unwrap();
let dec_err = decode_payload(&bytes, total_bits).unwrap_err();
assert!(matches!(dec_err, Error::MissingExplicitOrigin { idx: 0 }));
}
#[test]
fn bare_sh_at_n_accepts_with_populated_path_decl() {
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(bip84_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Sh,
body: Body::Children(vec![pkk(0)]),
},
tlv: TlvSection::new_empty(),
};
validate_explicit_origin_required(&d).unwrap();
let s = encode_md1_string(&d).unwrap();
let d2 = decode_md1_string(&s).unwrap();
assert_eq!(d, d2);
}
#[test]
fn tr_keypath_only_accepts_with_empty_path_decl() {
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(empty_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: tr_keypath_at_0(),
tlv: TlvSection::new_empty(),
};
validate_explicit_origin_required(&d).unwrap();
let s = encode_md1_string(&d).unwrap();
let d2 = decode_md1_string(&s).unwrap();
assert_eq!(d, d2);
}
#[test]
fn tr_with_taptree_rejects_empty_path_decl() {
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(empty_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: false,
key_index: 0,
tree: Some(Box::new(pkk(0))),
},
},
tlv: TlvSection::new_empty(),
};
let err = validate_explicit_origin_required(&d).unwrap_err();
assert!(matches!(err, Error::MissingExplicitOrigin { idx: 0 }));
let (bytes, total_bits) = encode_payload(&d).unwrap();
let dec_err = decode_payload(&bytes, total_bits).unwrap_err();
assert!(matches!(dec_err, Error::MissingExplicitOrigin { idx: 0 }));
}
#[test]
fn tr_with_taptree_accepts_with_populated_path_decl() {
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(bip86_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Tr,
body: Body::Tr {
is_nums: false,
key_index: 0,
tree: Some(Box::new(pkk(0))),
},
},
tlv: TlvSection::new_empty(),
};
validate_explicit_origin_required(&d).unwrap();
let s = encode_md1_string(&d).unwrap();
let d2 = decode_md1_string(&s).unwrap();
assert_eq!(d, d2);
}
#[test]
fn bip388_388_2_sh_wpkh_bip49_template_shape_round_trip() {
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(bip49_path()),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Sh,
body: Body::Children(vec![Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
}]),
},
tlv: TlvSection::new_empty(),
};
validate_explicit_origin_required(&d).unwrap();
validate_placeholder_usage(&d.tree, d.n).unwrap();
let s = encode_md1_string(&d).unwrap();
let d2 = decode_md1_string(&s).unwrap();
assert_eq!(d, d2);
assert!(
!d2.is_wallet_policy(),
"template-only (no Pubkeys) must NOT be wallet-policy mode"
);
}
#[test]
fn encoder_determinism_2of3_cell_7_byte_identical_emit() {
let d = cell_7_wsh_2of3_full();
let (bytes_1, bits_1) = encode_payload(&d).unwrap();
let (bytes_2, bits_2) = encode_payload(&d).unwrap();
assert_eq!(bits_1, bits_2);
assert_eq!(bytes_1, bytes_2);
let s_1 = encode_md1_string(&d).unwrap();
let s_2 = encode_md1_string(&d).unwrap();
assert_eq!(s_1, s_2);
}