mk_codec/bytecode/
encode.rs1use bitcoin::bip32::ChildNumber;
15
16use crate::bytecode::header::BytecodeHeader;
17use crate::bytecode::path::encode_path;
18use crate::bytecode::xpub_compact::{XpubCompact, encode_xpub_compact};
19use crate::error::{Error, Result};
20use crate::key_card::KeyCard;
21
22pub fn encode_bytecode(card: &KeyCard) -> Result<Vec<u8>> {
24 if card.policy_id_stubs.is_empty() {
25 return Err(Error::InvalidPolicyIdStubCount);
26 }
27 if card.policy_id_stubs.len() > u8::MAX as usize {
28 return Err(Error::InvalidPolicyIdStubCount);
29 }
30
31 let path_depth = card.origin_path.into_iter().count();
39 let path_child = card.origin_path.into_iter().last().copied();
40 let expected_child = path_child.unwrap_or(ChildNumber::Normal { index: 0 });
41 if card.xpub.depth as usize != path_depth || card.xpub.child_number != expected_child {
42 return Err(Error::XpubOriginPathMismatch {
43 xpub_depth: card.xpub.depth,
44 path_depth: path_depth as u8,
45 xpub_child: card.xpub.child_number,
46 path_child,
47 });
48 }
49
50 let header = BytecodeHeader {
51 version: 0,
52 fingerprint_flag: card.origin_fingerprint.is_some(),
53 };
54
55 let mut out: Vec<u8> = Vec::new();
56 out.push(header.to_byte());
57 out.push(card.policy_id_stubs.len() as u8);
58 for stub in &card.policy_id_stubs {
59 out.extend_from_slice(stub);
60 }
61 if let Some(fp) = &card.origin_fingerprint {
62 out.extend_from_slice(fp.as_bytes());
63 }
64 out.extend_from_slice(&encode_path(&card.origin_path));
65 let compact = XpubCompact::from_xpub(&card.xpub);
66 encode_xpub_compact(&compact, &mut out);
67 Ok(out)
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use crate::bytecode::test_helpers::synthetic_xpub;
74 use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint};
75 use std::str::FromStr;
76
77 fn fixture_card_1stub_with_fp() -> KeyCard {
78 let path = DerivationPath::from_str("m/48'/0'/0'/2'").unwrap();
79 KeyCard {
80 policy_id_stubs: vec![[0xAA; 4]],
81 origin_fingerprint: Some(Fingerprint::from([0xD3, 0x4D, 0xB3, 0x3F])),
82 xpub: synthetic_xpub(&path),
83 origin_path: path,
84 }
85 }
86
87 #[test]
88 fn encodes_typical_1stub_card_to_84_bytes() {
89 let card = fixture_card_1stub_with_fp();
90 let wire = encode_bytecode(&card).unwrap();
91 assert_eq!(wire.len(), 84);
93 assert_eq!(wire[0], 0x04, "fingerprint flag set");
94 assert_eq!(wire[1], 1, "stub_count = 1");
95 assert_eq!(&wire[2..6], &[0xAA; 4], "stub bytes");
96 assert_eq!(&wire[6..10], &[0xD3, 0x4D, 0xB3, 0x3F], "fp bytes");
97 assert_eq!(wire[10], 0x05, "std-table indicator for m/48'/0'/0'/2'");
98 }
99
100 #[test]
101 fn encodes_card_without_fingerprint_to_80_bytes() {
102 let mut card = fixture_card_1stub_with_fp();
103 card.origin_fingerprint = None;
104 let wire = encode_bytecode(&card).unwrap();
105 assert_eq!(wire.len(), 80);
107 assert_eq!(wire[0], 0x00, "fingerprint flag unset");
108 }
109
110 #[test]
111 fn rejects_zero_stubs() {
112 let mut card = fixture_card_1stub_with_fp();
113 card.policy_id_stubs.clear();
114 assert!(matches!(
115 encode_bytecode(&card),
116 Err(Error::InvalidPolicyIdStubCount),
117 ));
118 }
119
120 #[test]
121 fn deterministic_output() {
122 let card = fixture_card_1stub_with_fp();
123 let a = encode_bytecode(&card).unwrap();
124 let b = encode_bytecode(&card).unwrap();
125 assert_eq!(a, b, "encoder must be byte-deterministic");
126 }
127
128 #[test]
132 fn rejects_xpub_depth_mismatch() {
133 let mut card = fixture_card_1stub_with_fp(); card.xpub.depth = 3;
135 assert!(matches!(
136 encode_bytecode(&card),
137 Err(Error::XpubOriginPathMismatch {
138 xpub_depth: 3,
139 path_depth: 4,
140 ..
141 }),
142 ));
143 }
144
145 #[test]
149 fn rejects_xpub_child_mismatch_same_depth() {
150 let mut card = fixture_card_1stub_with_fp(); card.xpub.child_number = ChildNumber::Hardened { index: 1 }; assert!(matches!(
153 encode_bytecode(&card),
154 Err(Error::XpubOriginPathMismatch { .. }),
155 ));
156 }
157
158 #[test]
161 fn accepts_consistent_depth0_card() {
162 let path = DerivationPath::from_str("m").unwrap(); let card = KeyCard {
164 policy_id_stubs: vec![[0xAA; 4]],
165 origin_fingerprint: None,
166 xpub: synthetic_xpub(&path), origin_path: path,
168 };
169 assert!(
170 encode_bytecode(&card).is_ok(),
171 "consistent depth-0 no-path card must encode"
172 );
173 }
174
175 #[test]
178 fn rejects_depth0_noncanonical_child() {
179 let path = DerivationPath::from_str("m").unwrap(); let mut card = KeyCard {
181 policy_id_stubs: vec![[0xAA; 4]],
182 origin_fingerprint: None,
183 xpub: synthetic_xpub(&path), origin_path: path,
185 };
186 card.xpub.child_number = ChildNumber::Normal { index: 5 };
187 assert!(matches!(
188 encode_bytecode(&card),
189 Err(Error::XpubOriginPathMismatch {
190 xpub_depth: 0,
191 path_depth: 0,
192 path_child: None,
193 ..
194 }),
195 ));
196 }
197
198 #[test]
201 fn depth0_card_round_trips() {
202 use crate::bytecode::decode::decode_bytecode;
203 let path = DerivationPath::from_str("m").unwrap();
204 let card = KeyCard {
205 policy_id_stubs: vec![[0xAA; 4]],
206 origin_fingerprint: None,
207 xpub: synthetic_xpub(&path),
208 origin_path: path.clone(),
209 };
210 let wire = encode_bytecode(&card).unwrap();
211 let decoded = decode_bytecode(&wire).unwrap();
212 assert_eq!(decoded.origin_path, path);
213 assert_eq!(decoded.xpub.depth, 0);
214 assert_eq!(decoded.xpub.child_number, ChildNumber::Normal { index: 0 });
215 assert_eq!(decoded.xpub.public_key, card.xpub.public_key);
216 assert_eq!(decoded.xpub.chain_code, card.xpub.chain_code);
217 }
218
219 #[test]
225 fn aligned_explicit_path_card_encodes() {
226 let path = DerivationPath::from_str("m/44'/0'/0'/0/5").unwrap(); let card = KeyCard {
228 policy_id_stubs: vec![[0xAA; 4]],
229 origin_fingerprint: None,
230 xpub: synthetic_xpub(&path), origin_path: path,
232 };
233 assert!(
234 encode_bytecode(&card).is_ok(),
235 "aligned explicit-path card must encode"
236 );
237 }
238}