Skip to main content

mk_codec/bytecode/
encode.rs

1//! Top-level bytecode encoder: `KeyCard` → canonical `Vec<u8>`.
2//!
3//! Per `design/SPEC_mk_v0_1.md` §3.2 payload field order (closure Q-6):
4//!
5//! ```text
6//! [bytecode_header   : 1 B]
7//! [stub_count        : 1 B; MUST be ≥ 1]
8//! [policy_id_stubs   : 4 × N B]
9//! [origin_fingerprint: 4 B]   ← present iff bytecode_header bit 2 set
10//! [origin_path       : variable]
11//! [xpub_compact      : 73 B]
12//! ```
13
14use crate::bytecode::header::BytecodeHeader;
15use crate::bytecode::path::encode_path;
16use crate::bytecode::xpub_compact::{XpubCompact, encode_xpub_compact};
17use crate::error::{Error, Result};
18use crate::key_card::KeyCard;
19
20/// Encode a `KeyCard` to its canonical bytecode form (pre-chunking).
21pub fn encode_bytecode(card: &KeyCard) -> Result<Vec<u8>> {
22    if card.policy_id_stubs.is_empty() {
23        return Err(Error::InvalidPolicyIdStubCount);
24    }
25    if card.policy_id_stubs.len() > u8::MAX as usize {
26        return Err(Error::InvalidPolicyIdStubCount);
27    }
28
29    let header = BytecodeHeader {
30        version: 0,
31        fingerprint_flag: card.origin_fingerprint.is_some(),
32    };
33
34    let mut out: Vec<u8> = Vec::new();
35    out.push(header.to_byte());
36    out.push(card.policy_id_stubs.len() as u8);
37    for stub in &card.policy_id_stubs {
38        out.extend_from_slice(stub);
39    }
40    if let Some(fp) = &card.origin_fingerprint {
41        out.extend_from_slice(fp.as_bytes());
42    }
43    out.extend_from_slice(&encode_path(&card.origin_path));
44    let compact = XpubCompact::from_xpub(&card.xpub);
45    encode_xpub_compact(&compact, &mut out);
46    Ok(out)
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use crate::bytecode::test_helpers::synthetic_xpub;
53    use bitcoin::bip32::{DerivationPath, Fingerprint};
54    use std::str::FromStr;
55
56    fn fixture_card_1stub_with_fp() -> KeyCard {
57        let path = DerivationPath::from_str("m/48'/0'/0'/2'").unwrap();
58        KeyCard {
59            policy_id_stubs: vec![[0xAA; 4]],
60            origin_fingerprint: Some(Fingerprint::from([0xD3, 0x4D, 0xB3, 0x3F])),
61            xpub: synthetic_xpub(&path),
62            origin_path: path,
63        }
64    }
65
66    #[test]
67    fn encodes_typical_1stub_card_to_84_bytes() {
68        let card = fixture_card_1stub_with_fp();
69        let wire = encode_bytecode(&card).unwrap();
70        // header(1) + stub_count(1) + 1*stub(4) + fp(4) + std-table indicator(1) + xpub_compact(73) = 84
71        assert_eq!(wire.len(), 84);
72        assert_eq!(wire[0], 0x04, "fingerprint flag set");
73        assert_eq!(wire[1], 1, "stub_count = 1");
74        assert_eq!(&wire[2..6], &[0xAA; 4], "stub bytes");
75        assert_eq!(&wire[6..10], &[0xD3, 0x4D, 0xB3, 0x3F], "fp bytes");
76        assert_eq!(wire[10], 0x05, "std-table indicator for m/48'/0'/0'/2'");
77    }
78
79    #[test]
80    fn encodes_card_without_fingerprint_to_80_bytes() {
81        let mut card = fixture_card_1stub_with_fp();
82        card.origin_fingerprint = None;
83        let wire = encode_bytecode(&card).unwrap();
84        // 84 - 4 (omitted fp) = 80
85        assert_eq!(wire.len(), 80);
86        assert_eq!(wire[0], 0x00, "fingerprint flag unset");
87    }
88
89    #[test]
90    fn rejects_zero_stubs() {
91        let mut card = fixture_card_1stub_with_fp();
92        card.policy_id_stubs.clear();
93        assert!(matches!(
94            encode_bytecode(&card),
95            Err(Error::InvalidPolicyIdStubCount),
96        ));
97    }
98
99    #[test]
100    fn deterministic_output() {
101        let card = fixture_card_1stub_with_fp();
102        let a = encode_bytecode(&card).unwrap();
103        let b = encode_bytecode(&card).unwrap();
104        assert_eq!(a, b, "encoder must be byte-deterministic");
105    }
106}