use md_codec::chunk::{derive_chunk_set_id, split};
use md_codec::encode::Descriptor;
use md_codec::identity::compute_md1_encoding_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;
fn small_descriptor() -> Descriptor {
Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 84,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
],
}),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: TlvSection::new_empty(),
}
}
#[test]
fn small_descriptor_splits_into_one_chunk() {
let d = small_descriptor();
let chunks = split(&d).unwrap();
assert_eq!(chunks.len(), 1);
for c in &chunks {
assert!(c.starts_with("md1"));
}
}
#[test]
fn chunk_set_id_matches_md1_encoding_id_top_20_bits() {
let d = small_descriptor();
let md1_id = compute_md1_encoding_id(&d).unwrap();
let derived = derive_chunk_set_id(&md1_id);
let bytes = md1_id.as_bytes();
let expected = ((bytes[0] as u32) << 12) | ((bytes[1] as u32) << 4) | ((bytes[2] as u32) >> 4);
assert_eq!(derived, expected);
}
#[test]
fn small_descriptor_split_then_reassemble() {
use md_codec::chunk::reassemble;
let d = small_descriptor();
let chunks = split(&d).unwrap();
let chunk_refs: Vec<&str> = chunks.iter().map(|s| s.as_str()).collect();
let d2 = reassemble(&chunk_refs).unwrap();
assert_eq!(d, d2);
}
#[test]
fn single_string_payload_bit_limit_matches_regular_form() {
assert_eq!(md_codec::chunk::SINGLE_STRING_PAYLOAD_BIT_LIMIT, 320);
}
fn deep_path_descriptor() -> Descriptor {
let mut components = Vec::new();
for i in 0..15u32 {
components.push(PathComponent {
hardened: true,
value: i + 1,
});
}
Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(OriginPath { components }),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: TlvSection::new_empty(),
}
}
fn multi_chunk_descriptor() -> Descriptor {
let mut paths = Vec::new();
for cosigner in 0..4u32 {
let mut components = Vec::new();
for i in 0..15u32 {
components.push(PathComponent {
hardened: true,
value: cosigner * 100 + i + 1,
});
}
paths.push(OriginPath { components });
}
Descriptor {
n: 4,
path_decl: PathDecl {
n: 4,
paths: PathDeclPaths::Divergent(paths),
},
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: (0..4).collect(),
},
}]),
},
tlv: TlvSection::new_empty(),
}
}
#[test]
fn deep_path_descriptor_still_single_string() {
let d = deep_path_descriptor();
let chunks = split(&d).unwrap();
assert_eq!(chunks.len(), 1, "deep single-sig should fit in one chunk");
}
#[test]
fn multi_chunk_descriptor_splits_and_reassembles() {
use md_codec::chunk::reassemble;
let d = multi_chunk_descriptor();
let chunks = split(&d).unwrap();
assert!(
chunks.len() >= 2,
"expected multi-chunk emission, 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);
}
fn near_cap_descriptor() -> Descriptor {
use md_codec::tlv::TlvSection;
let big_payload: Vec<u8> = (0..2400).map(|i| (i % 251) as u8).collect();
let big_bit_len = big_payload.len() * 8;
let mut tlv = TlvSection::new_empty();
tlv.unknown.push((0x10, big_payload, big_bit_len));
Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(OriginPath {
components: vec![PathComponent {
hardened: true,
value: 84,
}],
}),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv,
}
}
fn over_cap_descriptor() -> Descriptor {
use md_codec::tlv::TlvSection;
let big_payload: Vec<u8> = (0..2700).map(|i| (i % 251) as u8).collect();
let big_bit_len = big_payload.len() * 8;
let mut tlv = TlvSection::new_empty();
tlv.unknown.push((0x10, big_payload, big_bit_len));
Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(OriginPath {
components: vec![PathComponent {
hardened: true,
value: 84,
}],
}),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv,
}
}
#[test]
fn near_cap_descriptor_splits_to_at_most_64_chunks_and_round_trips() {
use md_codec::chunk::reassemble;
let d = near_cap_descriptor();
let chunks = split(&d).unwrap();
assert!(
chunks.len() <= 64,
"near-cap descriptor must produce ≤64 chunks (got {})",
chunks.len()
);
assert!(
chunks.len() >= 8,
"near-cap descriptor should produce many chunks (got {})",
chunks.len()
);
let chunk_refs: Vec<&str> = chunks.iter().map(|s| s.as_str()).collect();
let d2 = reassemble(&chunk_refs).unwrap();
assert_eq!(d, d2);
}
#[test]
fn over_cap_descriptor_rejected_with_chunk_count_exceeds_max() {
use md_codec::error::Error;
let d = over_cap_descriptor();
let err = split(&d).unwrap_err();
assert!(
matches!(err, Error::ChunkCountExceedsMax { needed } if needed > 64),
"expected ChunkCountExceedsMax with needed > 64, got {:?}",
err
);
}
#[test]
fn tampered_chunk_rejected_by_bch_verify() {
use md_codec::chunk::reassemble;
let d = multi_chunk_descriptor();
let chunks = split(&d).unwrap();
let mut tampered = chunks[0].clone().into_bytes();
let pos = "md1".len();
let original = tampered[pos];
tampered[pos] = if original == b'q' { b'p' } else { b'q' };
let tampered_str = String::from_utf8(tampered).unwrap();
let mut chunk_refs: Vec<&str> = chunks.iter().map(|s| s.as_str()).collect();
chunk_refs[0] = tampered_str.as_str();
let result = reassemble(&chunk_refs);
assert!(result.is_err(), "tampered chunk should fail BCH verify");
}