mod common;
use common::corrupt_chunk_at;
use md_codec::bitstream::{BitReader, BitWriter};
use md_codec::chunk::{ChunkHeader, decode_with_correction, reassemble, split};
use md_codec::codex32::{unwrap_string, wrap_payload};
use md_codec::encode::Descriptor;
use md_codec::error::Error;
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 wpkh_descriptor(depth: u8) -> Descriptor {
Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(OriginPath {
components: (0..depth)
.map(|i| PathComponent {
hardened: true,
value: (i as u32) + 1,
})
.collect(),
}),
},
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 paths = (0..6u32)
.map(|c| OriginPath {
components: (0..15u32)
.map(|i| PathComponent {
hardened: true,
value: c * 100 + i + 1,
})
.collect(),
})
.collect();
Descriptor {
n: 6,
path_decl: PathDecl {
n: 6,
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..6).collect(),
},
}]),
},
tlv: TlvSection::new_empty(),
}
}
#[test]
fn t2a_correct_1_to_4_errors_across_lengths() {
for d in [
wpkh_descriptor(3),
wpkh_descriptor(15),
multi_chunk_descriptor(),
] {
let chunks = split(&d).unwrap();
for count in 1..=4usize {
let mut cs = chunks.clone();
for p in 1..=count {
cs[0] = corrupt_chunk_at(&cs[0], p, 0x1F);
}
let refs: Vec<&str> = cs.iter().map(String::as_str).collect();
let (got, details) = decode_with_correction(&refs)
.unwrap_or_else(|e| panic!("t={count} must correct: {e:?}"));
assert_eq!(got, d, "t={count} recovered a different descriptor");
assert!(details.len() >= count, "expected >= {count} corrections");
}
}
}
#[test]
fn t2b_correct_checksum_region_errors() {
let d = wpkh_descriptor(15);
let chunks = split(&d).unwrap();
let dp_len = chunks[0].chars().count() - 3; let mut cs = chunks.clone();
cs[0] = corrupt_chunk_at(&cs[0], dp_len - 1, 0x1F);
cs[0] = corrupt_chunk_at(&cs[0], dp_len - 7, 0x1F);
let refs: Vec<&str> = cs.iter().map(String::as_str).collect();
let (got, _) = decode_with_correction(&refs).expect("checksum-region errors correct");
assert_eq!(got, d);
}
const UNCORRECTABLE_5ERR: [usize; 5] = [1, 4, 7, 10, 13];
#[test]
fn t2c_five_to_eight_errors_never_return_original() {
let d = wpkh_descriptor(15);
let original = d.clone();
let chunks = split(&d).unwrap();
let dp_len = chunks[0].chars().count() - 3;
let mut x: u64 = 0x9E37_79B9_7F4A_7C15;
for trial in 0..300u32 {
for n_err in 5..=8usize {
let mut positions = std::collections::BTreeSet::new();
while positions.len() < n_err {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
positions.insert((x as usize) % dp_len);
}
let mut c0 = chunks[0].clone();
for &p in &positions {
c0 = corrupt_chunk_at(&c0, p, ((x as u8) | 1) & 0x1F);
}
let mut cs = chunks.clone();
cs[0] = c0;
let refs: Vec<&str> = cs.iter().map(String::as_str).collect();
if let Ok((got, _)) = decode_with_correction(&refs) {
assert_ne!(
got, original,
"trial {trial} n_err {n_err}: 5-8 errors silently returned the original"
);
}
}
}
}
#[test]
fn t2d_deterministic_five_error_is_err() {
let d = wpkh_descriptor(15);
let chunks = split(&d).unwrap();
let mut c0 = chunks[0].clone();
for p in UNCORRECTABLE_5ERR {
c0 = corrupt_chunk_at(&c0, p, 0x1F);
}
let mut cs = chunks.clone();
cs[0] = c0;
let refs: Vec<&str> = cs.iter().map(String::as_str).collect();
assert!(
decode_with_correction(&refs).is_err(),
"UNCORRECTABLE_5ERR must be uncorrectable — if this fires, the chunk symbols changed; \
pick another 5-position pattern that errs and update the const (see its doc-comment)"
);
}
#[test]
fn t2h_multi_chunk_two_corrupted_within_t() {
let d = multi_chunk_descriptor();
let chunks = split(&d).unwrap();
assert!(chunks.len() >= 2);
let mut cs = chunks.clone();
cs[0] = corrupt_chunk_at(&cs[0], 2, 0x1F);
let li = cs.len() - 1;
cs[li] = corrupt_chunk_at(&cs[li], 2, 0x1F);
let refs: Vec<&str> = cs.iter().map(String::as_str).collect();
let (got, _) = decode_with_correction(&refs).expect("each chunk within t corrects");
assert_eq!(got, d);
}
#[test]
fn t2i_one_chunk_over_t_never_returns_original() {
let d = multi_chunk_descriptor();
let chunks = split(&d).unwrap();
let mut cs = chunks.clone();
for p in [1usize, 4, 7, 10, 13] {
cs[0] = corrupt_chunk_at(&cs[0], p, 0x1F);
}
let refs: Vec<&str> = cs.iter().map(String::as_str).collect();
if let Ok((got, _)) = decode_with_correction(&refs) {
assert_ne!(
got, d,
"a 5-error chunk-0 corruption must never reassemble to the original"
);
}
}
fn restamp_chunk_header(chunk: &str, mutate: impl FnOnce(&mut ChunkHeader)) -> String {
let (bytes, bit_count) = unwrap_string(chunk).expect("valid chunk");
let mut reader = BitReader::with_bit_limit(&bytes, bit_count);
let mut header = ChunkHeader::read(&mut reader).expect("valid chunk header");
mutate(&mut header);
let mut writer = BitWriter::new();
header.write(&mut writer).expect("mutated header writes");
while reader.remaining_bits() > 0 {
let take = reader.remaining_bits().min(32);
let bits = reader.read_bits(take).expect("read remaining payload bits");
writer.write_bits(bits, take);
}
let new_bytes = writer.into_bytes();
wrap_payload(&new_bytes, bit_count).expect("re-wrap with recomputed BCH")
}
#[test]
fn restamp_identity_round_trips() {
let chunks = split(&multi_chunk_descriptor()).unwrap();
assert!(
chunks.len() >= 3,
"fixture must split into >=3 chunks; got {}",
chunks.len()
);
for c in &chunks {
assert_eq!(
&restamp_chunk_header(c, |_| {}),
c,
"identity restamp must reproduce the chunk"
);
}
}
#[test]
fn t2e_reassemble_rejects_count_mismatch() {
let chunks = split(&multi_chunk_descriptor()).unwrap();
let mut cs = chunks.clone();
cs[0] = restamp_chunk_header(&cs[0], |h| h.count = h.count.wrapping_add(1));
let refs: Vec<&str> = cs.iter().map(String::as_str).collect();
assert!(matches!(
reassemble(&refs),
Err(Error::ChunkSetInconsistent)
));
}
#[test]
fn t2f_reassemble_rejects_index_gap() {
let chunks = split(&multi_chunk_descriptor()).unwrap();
assert!(chunks.len() >= 3, "need >=3 chunks; enlarge the fixture");
let mut cs = chunks.clone();
cs[1] = restamp_chunk_header(&cs[1], |h| h.index = 0); let refs: Vec<&str> = cs.iter().map(String::as_str).collect();
assert!(matches!(
reassemble(&refs),
Err(Error::ChunkIndexGap { .. })
));
}
#[test]
fn t2g_reassemble_rejects_derived_csid_mismatch() {
let chunks = split(&multi_chunk_descriptor()).unwrap();
let foreign: u32 = 0x0_AAAA;
let cs: Vec<String> = chunks
.iter()
.map(|c| restamp_chunk_header(c, |h| h.chunk_set_id = foreign))
.collect();
let refs: Vec<&str> = cs.iter().map(String::as_str).collect();
assert!(matches!(
reassemble(&refs),
Err(Error::ChunkSetIdMismatch { .. })
));
}