mk_codec/bytecode/
encode.rs1use 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
20pub 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 path_depth = card.origin_path.into_iter().count();
34 let path_child = card.origin_path.into_iter().last().copied();
35 if card.xpub.depth as usize != path_depth || Some(card.xpub.child_number) != path_child {
36 return Err(Error::XpubOriginPathMismatch {
37 xpub_depth: card.xpub.depth,
38 path_depth: path_depth as u8,
39 xpub_child: card.xpub.child_number,
40 path_child,
41 });
42 }
43
44 let header = BytecodeHeader {
45 version: 0,
46 fingerprint_flag: card.origin_fingerprint.is_some(),
47 };
48
49 let mut out: Vec<u8> = Vec::new();
50 out.push(header.to_byte());
51 out.push(card.policy_id_stubs.len() as u8);
52 for stub in &card.policy_id_stubs {
53 out.extend_from_slice(stub);
54 }
55 if let Some(fp) = &card.origin_fingerprint {
56 out.extend_from_slice(fp.as_bytes());
57 }
58 out.extend_from_slice(&encode_path(&card.origin_path));
59 let compact = XpubCompact::from_xpub(&card.xpub);
60 encode_xpub_compact(&compact, &mut out);
61 Ok(out)
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::bytecode::test_helpers::synthetic_xpub;
68 use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint};
69 use std::str::FromStr;
70
71 fn fixture_card_1stub_with_fp() -> KeyCard {
72 let path = DerivationPath::from_str("m/48'/0'/0'/2'").unwrap();
73 KeyCard {
74 policy_id_stubs: vec![[0xAA; 4]],
75 origin_fingerprint: Some(Fingerprint::from([0xD3, 0x4D, 0xB3, 0x3F])),
76 xpub: synthetic_xpub(&path),
77 origin_path: path,
78 }
79 }
80
81 #[test]
82 fn encodes_typical_1stub_card_to_84_bytes() {
83 let card = fixture_card_1stub_with_fp();
84 let wire = encode_bytecode(&card).unwrap();
85 assert_eq!(wire.len(), 84);
87 assert_eq!(wire[0], 0x04, "fingerprint flag set");
88 assert_eq!(wire[1], 1, "stub_count = 1");
89 assert_eq!(&wire[2..6], &[0xAA; 4], "stub bytes");
90 assert_eq!(&wire[6..10], &[0xD3, 0x4D, 0xB3, 0x3F], "fp bytes");
91 assert_eq!(wire[10], 0x05, "std-table indicator for m/48'/0'/0'/2'");
92 }
93
94 #[test]
95 fn encodes_card_without_fingerprint_to_80_bytes() {
96 let mut card = fixture_card_1stub_with_fp();
97 card.origin_fingerprint = None;
98 let wire = encode_bytecode(&card).unwrap();
99 assert_eq!(wire.len(), 80);
101 assert_eq!(wire[0], 0x00, "fingerprint flag unset");
102 }
103
104 #[test]
105 fn rejects_zero_stubs() {
106 let mut card = fixture_card_1stub_with_fp();
107 card.policy_id_stubs.clear();
108 assert!(matches!(
109 encode_bytecode(&card),
110 Err(Error::InvalidPolicyIdStubCount),
111 ));
112 }
113
114 #[test]
115 fn deterministic_output() {
116 let card = fixture_card_1stub_with_fp();
117 let a = encode_bytecode(&card).unwrap();
118 let b = encode_bytecode(&card).unwrap();
119 assert_eq!(a, b, "encoder must be byte-deterministic");
120 }
121
122 #[test]
126 fn rejects_xpub_depth_mismatch() {
127 let mut card = fixture_card_1stub_with_fp(); card.xpub.depth = 3;
129 assert!(matches!(
130 encode_bytecode(&card),
131 Err(Error::XpubOriginPathMismatch {
132 xpub_depth: 3,
133 path_depth: 4,
134 ..
135 }),
136 ));
137 }
138
139 #[test]
143 fn rejects_xpub_child_mismatch_same_depth() {
144 let mut card = fixture_card_1stub_with_fp(); card.xpub.child_number = ChildNumber::Hardened { index: 1 }; assert!(matches!(
147 encode_bytecode(&card),
148 Err(Error::XpubOriginPathMismatch { .. }),
149 ));
150 }
151
152 #[test]
155 fn rejects_empty_origin_path() {
156 let path = DerivationPath::from_str("m").unwrap(); let card = KeyCard {
158 policy_id_stubs: vec![[0xAA; 4]],
159 origin_fingerprint: None,
160 xpub: synthetic_xpub(&path), origin_path: path,
162 };
163 assert!(matches!(
164 encode_bytecode(&card),
165 Err(Error::XpubOriginPathMismatch {
166 path_child: None,
167 ..
168 }),
169 ));
170 }
171
172 #[test]
178 fn aligned_explicit_path_card_encodes() {
179 let path = DerivationPath::from_str("m/44'/0'/0'/0/5").unwrap(); let card = KeyCard {
181 policy_id_stubs: vec![[0xAA; 4]],
182 origin_fingerprint: None,
183 xpub: synthetic_xpub(&path), origin_path: path,
185 };
186 assert!(
187 encode_bytecode(&card).is_ok(),
188 "aligned explicit-path card must encode"
189 );
190 }
191}