use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use bitcoin::bip32::{DerivationPath, Fingerprint, Xpub};
use mk_codec::{KeyCard, decode, encode_with_chunk_set_id};
use serde_json::Value;
const VECTOR_FILE: &str = "src/test_vectors/v0.1.json";
fn vector_doc() -> Value {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(VECTOR_FILE);
let bytes = fs::read(path).expect("read src/test_vectors/v0.1.json");
serde_json::from_slice(&bytes).expect("parse vectors 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 = DerivationPath::from_str(
input["origin_path"]
.as_str()
.expect("origin_path is string"),
)
.expect("origin_path parses");
let xpub: Xpub = input["xpub"]
.as_str()
.expect("xpub is string")
.parse()
.expect("xpub parses");
KeyCard::new(stubs, fp, path, xpub)
}
fn clean_vector<'a>(doc: &'a Value, name: &str) -> (&'a Value, &'a Value) {
let v = doc["vectors"]
.as_array()
.expect("vectors is array")
.iter()
.find(|v| v["name"].as_str() == Some(name))
.unwrap_or_else(|| panic!("clean vector {name} not found"));
assert!(
v["expected_error"].is_null(),
"{name} is a negative vector, not clean"
);
(&v["input"], &v["expected"])
}
fn decode_vector(expected: &Value) -> KeyCard {
let strings: Vec<String> = expected["strings"]
.as_array()
.expect("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();
decode(&parts).expect("vector strings decode")
}
const REPRESENTATIVE: &[&str] = &[
"V1_bip48_mainnet_1_stub_with_fp",
"V4_bip84_mainnet_1_stub_no_fp",
"V5_explicit_path_4_components_with_fp", "V7_max_path_components_no_fp", ];
#[test]
fn canonical_payload_matches_corpus_hex() {
let doc = vector_doc();
for name in REPRESENTATIVE {
let (_input, expected) = clean_vector(&doc, name);
let card = decode_vector(expected);
let actual = card
.canonical_payload_bytes()
.unwrap_or_else(|e| panic!("[{name}] canonical_payload_bytes failed: {e}"));
let want = parse_hex(
expected["canonical_bytecode_hex"]
.as_str()
.expect("canonical_bytecode_hex is string"),
);
assert_eq!(
actual, want,
"[{name}] canonical_payload_bytes != pinned canonical_bytecode_hex"
);
}
}
#[test]
fn canonical_payload_round_trips() {
let doc = vector_doc();
for name in REPRESENTATIVE {
let (input, _expected) = clean_vector(&doc, name);
let card = build_card_from_input(input);
let bytes = card
.canonical_payload_bytes()
.unwrap_or_else(|e| panic!("[{name}] canonical_payload_bytes failed: {e}"));
let recovered = KeyCard::from_canonical_payload_bytes(&bytes)
.unwrap_or_else(|e| panic!("[{name}] from_canonical_payload_bytes failed: {e}"));
assert_eq!(
recovered, card,
"[{name}] from_canonical_payload_bytes(canonical_payload_bytes) != original"
);
}
}
#[test]
fn canonical_payload_is_chunk_set_id_invariant() {
let doc = vector_doc();
let (input, _expected) = clean_vector(&doc, "V5_explicit_path_4_components_with_fp");
let card = build_card_from_input(input);
let id_a: u32 = 0x0_00AA;
let id_b: u32 = 0x0_FF55;
let strings_a = encode_with_chunk_set_id(&card, id_a).expect("encode A");
let strings_b = encode_with_chunk_set_id(&card, id_b).expect("encode B");
assert!(
strings_a.len() > 1 && strings_b.len() > 1,
"test card must be multi-chunk for chunk_set_id to appear on the wire"
);
assert_ne!(
strings_a, strings_b,
"distinct chunk_set_id must produce distinct mk1 strings"
);
let parts_a: Vec<&str> = strings_a.iter().map(|s| s.as_str()).collect();
let parts_b: Vec<&str> = strings_b.iter().map(|s| s.as_str()).collect();
let payload_a = decode(&parts_a)
.expect("decode A")
.canonical_payload_bytes()
.expect("payload A");
let payload_b = decode(&parts_b)
.expect("decode B")
.canonical_payload_bytes()
.expect("payload B");
assert_eq!(
payload_a, payload_b,
"canonical payload must be invariant to the random chunk_set_id framing"
);
}
#[test]
fn from_canonical_payload_bytes_rejects_garbage() {
assert!(
KeyCard::from_canonical_payload_bytes(&[]).is_err(),
"empty input must be rejected"
);
assert!(
KeyCard::from_canonical_payload_bytes(&[0xFF]).is_err(),
"1-byte garbage must be rejected"
);
assert!(
KeyCard::from_canonical_payload_bytes(&[0u8; 8]).is_err(),
"short zero blob must be rejected"
);
let doc = vector_doc();
let (input, _expected) = clean_vector(&doc, "V1_bip48_mainnet_1_stub_with_fp");
let card = build_card_from_input(input);
let mut bytes = card.canonical_payload_bytes().expect("payload");
bytes.push(0xAB); assert!(
KeyCard::from_canonical_payload_bytes(&bytes).is_err(),
"trailing-byte corruption must be rejected"
);
}