use md_codec::chunk::split;
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;
use md_codec::{CorrectionDetail, decode_with_correction};
const CODEX32_ALPHABET: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
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(),
}
}
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(),
}
}
fn corrupt_chunk_at(chunk: &str, pos: usize, xor_mask: u8) -> String {
let hrp_len = 3; let mut chars: Vec<char> = chunk.chars().collect();
let abs_idx = hrp_len + pos;
let original_char = chars[abs_idx];
let original_sym = CODEX32_ALPHABET
.iter()
.position(|&b| b == original_char.to_ascii_lowercase() as u8)
.expect("char in codex32 alphabet") as u8;
let new_sym = (original_sym ^ (xor_mask & 0x1F)) & 0x1F;
chars[abs_idx] = CODEX32_ALPHABET[new_sym as usize] as char;
chars.iter().collect()
}
fn data_part_len(chunk: &str) -> usize {
chunk.len() - 3 }
#[test]
fn zero_error_passthrough() {
let d = small_descriptor();
let chunks = split(&d).unwrap();
let refs: Vec<&str> = chunks.iter().map(|s| s.as_str()).collect();
let (decoded, details) = decode_with_correction(&refs).expect("clean decode");
assert_eq!(decoded, d, "round-trip preserves descriptor");
assert!(details.is_empty(), "no corrections expected for clean input");
}
#[test]
fn one_error_at_position_0() {
let d = small_descriptor();
let chunks = split(&d).unwrap();
let bad = corrupt_chunk_at(&chunks[0], 0, 0b10101);
let (decoded, details) = decode_with_correction(&[bad.as_str()]).expect("1-error decode");
assert_eq!(decoded, d, "corrected decode matches original");
assert_eq!(details.len(), 1, "exactly 1 correction reported");
assert_eq!(details[0].chunk_index, 0);
assert_eq!(details[0].position, 0);
let original_char = chunks[0].chars().nth(3).unwrap();
assert_eq!(
details[0].now, original_char,
"correction restores the original char"
);
assert_ne!(details[0].was, details[0].now);
}
#[test]
fn one_error_at_last_data_symbol() {
let d = small_descriptor();
let chunks = split(&d).unwrap();
let last_pos = data_part_len(&chunks[0]) - 1;
let bad = corrupt_chunk_at(&chunks[0], last_pos, 0b01110);
let (decoded, details) =
decode_with_correction(&[bad.as_str()]).expect("1-error at last position decodes");
assert_eq!(decoded, d);
assert_eq!(details.len(), 1);
assert_eq!(details[0].position, last_pos);
let original_char = chunks[0].chars().nth(3 + last_pos).unwrap();
assert_eq!(details[0].now, original_char);
}
#[test]
fn four_error_t_boundary() {
let d = small_descriptor();
let chunks = split(&d).unwrap();
let dp_len = data_part_len(&chunks[0]);
let positions: [usize; 4] = [0, dp_len / 4, dp_len / 2, dp_len - 1];
let masks: [u8; 4] = [0b00001, 0b10000, 0b11111, 0b01010];
let mut bad = chunks[0].clone();
for (&p, &m) in positions.iter().zip(&masks) {
bad = corrupt_chunk_at(&bad, p, m);
}
let (decoded, details) =
decode_with_correction(&[bad.as_str()]).expect("4-error t-boundary decodes");
assert_eq!(decoded, d, "corrected decode matches original");
assert_eq!(details.len(), 4, "exactly 4 corrections reported");
let reported_positions: Vec<usize> = details.iter().map(|c| c.position).collect();
let mut expected_positions: Vec<usize> = positions.to_vec();
expected_positions.sort();
assert_eq!(reported_positions, expected_positions);
for det in &details {
assert_eq!(det.chunk_index, 0);
assert_ne!(det.was, det.now, "correction changes the character");
}
}
#[test]
fn five_error_too_many() {
let d = small_descriptor();
let chunks = split(&d).unwrap();
let dp_len = data_part_len(&chunks[0]);
let positions: [usize; 5] = [0, dp_len / 5, 2 * dp_len / 5, 3 * dp_len / 5, dp_len - 1];
let masks: [u8; 5] = [0b00001, 0b00010, 0b00100, 0b01000, 0b10000];
let mut bad = chunks[0].clone();
for (&p, &m) in positions.iter().zip(&masks) {
bad = corrupt_chunk_at(&bad, p, m);
}
let err = decode_with_correction(&[bad.as_str()])
.expect_err("5-error pattern must not decode successfully");
match err {
Error::TooManyErrors { chunk_index, bound } => {
assert_eq!(chunk_index, 0, "the only chunk is index 0");
assert_eq!(bound, 8, "BCH(93,80,8) singleton bound is 8");
}
other => panic!("expected TooManyErrors, got {other:?}"),
}
}
#[test]
fn multi_chunk_one_corrupted() {
let d = multi_chunk_descriptor();
let chunks = split(&d).unwrap();
assert!(
chunks.len() >= 2,
"multi-chunk descriptor must split into 2+ chunks; got {}",
chunks.len()
);
let target_idx = chunks.len() / 2;
let bad_chunk = corrupt_chunk_at(&chunks[target_idx], 4, 0b01101);
let mut input: Vec<String> = chunks.to_vec();
input[target_idx] = bad_chunk;
let refs: Vec<&str> = input.iter().map(|s| s.as_str()).collect();
let (decoded, details) = decode_with_correction(&refs).expect("multi-chunk decode");
assert_eq!(decoded, d, "round-trip restores descriptor");
assert_eq!(details.len(), 1, "exactly 1 correction across the chunk set");
let det: &CorrectionDetail = &details[0];
assert_eq!(
det.chunk_index, target_idx,
"correction reports the corrupted chunk's index"
);
assert_eq!(det.position, 4);
let original_char = chunks[target_idx].chars().nth(3 + 4).unwrap();
assert_eq!(det.now, original_char);
}