use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use bitcoin::NetworkKind;
use bitcoin::bip32::{DerivationPath, Fingerprint, Xpub};
use mk_codec::{KeyCard, bytecode::encode_bytecode, decode, encode_with_chunk_set_id};
use serde_json::Value;
use sha2::{Digest, Sha256};
const V0_1_SHA256: &str = "ebd8f34d8d52896e07e1faef995f18ffa61d42e2a048fb2a8c11e67f120d78ff";
const VECTOR_FILE: &str = "src/test_vectors/v0.1.json";
fn vector_file_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(VECTOR_FILE)
}
fn read_vector_file() -> Vec<u8> {
fs::read(vector_file_path()).expect("read src/test_vectors/v0.1.json")
}
fn parse_hex(s: &str) -> Vec<u8> {
hex::decode(s).expect("vector hex must decode")
}
fn vec4(bytes: &[u8]) -> [u8; 4] {
bytes.try_into().expect("4-byte hex slice")
}
fn build_card_from_input(input: &Value) -> KeyCard {
let stubs: Vec<[u8; 4]> = input["policy_id_stubs"]
.as_array()
.expect("policy_id_stubs is array")
.iter()
.map(|v| vec4(&parse_hex(v.as_str().expect("hex string"))))
.collect();
let fp: Option<Fingerprint> = match &input["origin_fingerprint"] {
Value::Null => None,
Value::String(s) => Some(Fingerprint::from(vec4(&parse_hex(s)))),
other => panic!("unexpected origin_fingerprint value: {other:?}"),
};
let path_str = input["origin_path"]
.as_str()
.expect("origin_path is string");
let path = DerivationPath::from_str(path_str).expect("origin_path parses");
let xpub: Xpub = input["xpub"]
.as_str()
.expect("xpub is string")
.parse()
.expect("xpub parses");
let declared = input["network"].as_str().expect("network is string");
let actual = match xpub.network {
NetworkKind::Main => "mainnet",
NetworkKind::Test => "testnet",
};
assert_eq!(
actual, declared,
"vector network mismatch: xpub says {actual}, fixture declares {declared}"
);
KeyCard::new(stubs, fp, path, xpub)
}
#[test]
fn vector_file_sha256_matches_pin() {
let bytes = read_vector_file();
let digest = Sha256::digest(&bytes);
let actual = hex::encode(digest);
assert_eq!(
actual, V0_1_SHA256,
"src/test_vectors/v0.1.json SHA-256 drifted; if intended, regenerate via \
`cargo run --bin gen_mk_vectors --features gen-vectors` and update \
`V0_1_SHA256` in tests/vectors.rs"
);
}
#[test]
fn schema_metadata_pinned() {
let bytes = read_vector_file();
let doc: Value = serde_json::from_slice(&bytes).expect("parse vectors JSON");
assert_eq!(doc["schema"], Value::from(2u64), "schema version drift");
assert_eq!(
doc["family_token"].as_str().unwrap_or(""),
"mk-codec 0.2",
"family_token drift — see consts.rs::GENERATOR_FAMILY"
);
}
#[test]
fn every_vector_round_trips() {
let bytes = read_vector_file();
let doc: Value = serde_json::from_slice(&bytes).expect("parse vectors JSON");
let vectors = doc["vectors"].as_array().expect("vectors is array");
assert!(!vectors.is_empty(), "vector corpus must not be empty");
let mut clean_count = 0usize;
let mut negative_count = 0usize;
for vector in vectors {
let name = vector["name"]
.as_str()
.expect("vector.name is string")
.to_string();
match &vector["expected_error"] {
Value::Null => {
clean_count += 1;
exercise_clean_vector(&name, vector);
}
Value::String(expected_err) => {
negative_count += 1;
exercise_negative_vector(&name, vector, expected_err);
}
other => panic!("[{name}] expected_error must be null or string; got {other:?}"),
}
}
assert!(clean_count >= 18, "clean-vector count regressed");
assert!(negative_count >= 22, "negative-vector count regressed");
}
fn exercise_clean_vector(name: &str, vector: &Value) {
let input = &vector["input"];
let expected = &vector["expected"];
let card = build_card_from_input(input);
let actual_bytecode =
encode_bytecode(&card).unwrap_or_else(|e| panic!("[{name}] encode_bytecode failed: {e}"));
let expected_bytecode = parse_hex(
expected["canonical_bytecode_hex"]
.as_str()
.expect("canonical_bytecode_hex is string"),
);
assert_eq!(
actual_bytecode, expected_bytecode,
"[{name}] canonical_bytecode_hex drifted from encoder output"
);
let chunk_set_id = u32::try_from(input["chunk_set_id"].as_u64().expect("chunk_set_id is u64"))
.expect("chunk_set_id fits in u32");
let actual_strings = encode_with_chunk_set_id(&card, chunk_set_id)
.unwrap_or_else(|e| panic!("[{name}] encode_with_chunk_set_id failed: {e}"));
let expected_strings: Vec<String> = expected["strings"]
.as_array()
.expect("strings is array")
.iter()
.map(|v| v.as_str().expect("string").to_string())
.collect();
assert_eq!(
actual_strings, expected_strings,
"[{name}] mk1 string set drifted from encoder output"
);
let expected_total = u64::try_from(actual_strings.len()).unwrap();
assert_eq!(
expected["total_chunks"].as_u64().unwrap_or(0),
expected_total,
"[{name}] total_chunks metadata disagrees with strings.len()"
);
let recovered_strs: Vec<&str> = actual_strings.iter().map(|s| s.as_str()).collect();
let recovered_card =
decode(&recovered_strs).unwrap_or_else(|e| panic!("[{name}] decode failed: {e}"));
assert_eq!(
recovered_card, card,
"[{name}] decoded KeyCard differs from original"
);
assert_eq!(
expected["decoder_correction"].as_str().unwrap_or(""),
"clean",
"[{name}] decoder_correction is not 'clean' for a clean vector"
);
}
fn exercise_negative_vector(name: &str, vector: &Value, expected_err: &str) {
let strings: Vec<String> = vector["input"]["strings"]
.as_array()
.expect("[name] negative.input.strings is array")
.iter()
.map(|v| v.as_str().expect("string").to_string())
.collect();
let parts: Vec<&str> = strings.iter().map(|s| s.as_str()).collect();
match decode(&parts) {
Err(e) => {
let actual = format!("{e}");
assert_eq!(
actual, expected_err,
"[{name}] decoder error rendering drifted from pinned `expected_error`"
);
}
Ok(card) => panic!(
"[{name}] expected `Err({expected_err})`; decoder accepted input as KeyCard {card:?}"
),
}
}