Skip to main content

mk_codec/bytecode/
decode.rs

1//! Top-level bytecode decoder: canonical `Vec<u8>` → `KeyCard`.
2//!
3//! Reverses [`crate::bytecode::encode::encode_bytecode`]; applies the
4//! validity rules of `design/SPEC_mk_v0_1.md` §4.
5
6use bitcoin::bip32::Fingerprint;
7
8use crate::bytecode::header::BytecodeHeader;
9use crate::bytecode::path::decode_path;
10use crate::bytecode::xpub_compact::{decode_xpub_compact, reconstruct_xpub};
11use crate::consts::{ORIGIN_FINGERPRINT_BYTES, POLICY_ID_STUB_BYTES};
12use crate::error::{Error, Result};
13use crate::key_card::KeyCard;
14
15/// Decode canonical bytecode (pre-chunking) into a `KeyCard`.
16///
17/// Surfaces every SPEC §4 bytecode-layer validity rule via a unique
18/// `Error` variant.
19pub fn decode_bytecode(bytes: &[u8]) -> Result<KeyCard> {
20    let mut cursor: &[u8] = bytes;
21
22    let header_byte = read_u8(&mut cursor)?;
23    let header = BytecodeHeader::parse(header_byte)?;
24
25    let stub_count = read_u8(&mut cursor)?;
26    if stub_count == 0 {
27        return Err(Error::InvalidPolicyIdStubCount);
28    }
29    let mut policy_id_stubs: Vec<[u8; 4]> = Vec::with_capacity(stub_count as usize);
30    for _ in 0..stub_count {
31        let stub: [u8; POLICY_ID_STUB_BYTES] = read_array(&mut cursor)?;
32        policy_id_stubs.push(stub);
33    }
34
35    let origin_fingerprint = if header.fingerprint_flag {
36        let fp_bytes: [u8; ORIGIN_FINGERPRINT_BYTES] = read_array(&mut cursor)?;
37        Some(Fingerprint::from(fp_bytes))
38    } else {
39        None
40    };
41
42    let origin_path = decode_path(&mut cursor)?;
43    let compact = decode_xpub_compact(&mut cursor)?;
44    let xpub = reconstruct_xpub(&compact, &origin_path)?;
45
46    if !cursor.is_empty() {
47        return Err(Error::TrailingBytes);
48    }
49
50    Ok(KeyCard {
51        policy_id_stubs,
52        origin_fingerprint,
53        origin_path,
54        xpub,
55    })
56}
57
58fn read_u8(cursor: &mut &[u8]) -> Result<u8> {
59    if cursor.is_empty() {
60        return Err(Error::UnexpectedEnd);
61    }
62    let b = cursor[0];
63    *cursor = &cursor[1..];
64    Ok(b)
65}
66
67fn read_array<const N: usize>(cursor: &mut &[u8]) -> Result<[u8; N]> {
68    if cursor.len() < N {
69        return Err(Error::UnexpectedEnd);
70    }
71    let mut buf = [0u8; N];
72    buf.copy_from_slice(&cursor[..N]);
73    *cursor = &cursor[N..];
74    Ok(buf)
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::bytecode::encode::encode_bytecode;
81    use crate::bytecode::test_helpers::synthetic_xpub;
82    use bitcoin::bip32::DerivationPath;
83    use std::str::FromStr;
84
85    fn fixture_card_1stub_with_fp() -> KeyCard {
86        let path = DerivationPath::from_str("m/48'/0'/0'/2'").unwrap();
87        KeyCard {
88            policy_id_stubs: vec![[0xAA; 4]],
89            origin_fingerprint: Some(Fingerprint::from([0xD3, 0x4D, 0xB3, 0x3F])),
90            xpub: synthetic_xpub(&path),
91            origin_path: path,
92        }
93    }
94
95    fn fixture_card_3stubs_no_fp() -> KeyCard {
96        let path = DerivationPath::from_str("m/48'/0'/0'/2'").unwrap();
97        KeyCard {
98            policy_id_stubs: vec![[0xAA; 4], [0xBB; 4], [0xCC; 4]],
99            origin_fingerprint: None,
100            xpub: synthetic_xpub(&path),
101            origin_path: path,
102        }
103    }
104
105    fn fixture_card_explicit_path() -> KeyCard {
106        let path = DerivationPath::from_str("m/9999'/1234'/56'/7'").unwrap();
107        KeyCard {
108            policy_id_stubs: vec![[0x11, 0x22, 0x33, 0x44]],
109            origin_fingerprint: Some(Fingerprint::from([0xAB, 0xCD, 0xEF, 0x01])),
110            xpub: synthetic_xpub(&path),
111            origin_path: path,
112        }
113    }
114
115    #[test]
116    fn round_trip_1stub_with_fp() {
117        let card = fixture_card_1stub_with_fp();
118        let wire = encode_bytecode(&card).unwrap();
119        let decoded = decode_bytecode(&wire).unwrap();
120        assert_eq!(decoded, card);
121    }
122
123    #[test]
124    fn round_trip_3stubs_no_fp() {
125        let card = fixture_card_3stubs_no_fp();
126        let wire = encode_bytecode(&card).unwrap();
127        let decoded = decode_bytecode(&wire).unwrap();
128        assert_eq!(decoded, card);
129    }
130
131    #[test]
132    fn round_trip_explicit_path() {
133        let card = fixture_card_explicit_path();
134        let wire = encode_bytecode(&card).unwrap();
135        let decoded = decode_bytecode(&wire).unwrap();
136        assert_eq!(decoded, card);
137    }
138
139    #[test]
140    fn rejects_unsupported_version() {
141        let card = fixture_card_1stub_with_fp();
142        let mut wire = encode_bytecode(&card).unwrap();
143        wire[0] = 0x10; // version=1
144        assert!(matches!(
145            decode_bytecode(&wire),
146            Err(Error::UnsupportedVersion(1)),
147        ));
148    }
149
150    #[test]
151    fn rejects_reserved_bits_set() {
152        let card = fixture_card_1stub_with_fp();
153        let mut wire = encode_bytecode(&card).unwrap();
154        wire[0] |= 0b0000_0010; // bit 1 = reserved
155        assert!(matches!(
156            decode_bytecode(&wire),
157            Err(Error::ReservedBitsSet),
158        ));
159    }
160
161    #[test]
162    fn rejects_zero_stub_count() {
163        let card = fixture_card_1stub_with_fp();
164        let mut wire = encode_bytecode(&card).unwrap();
165        wire[1] = 0; // stub_count = 0
166        assert!(matches!(
167            decode_bytecode(&wire),
168            Err(Error::InvalidPolicyIdStubCount),
169        ));
170    }
171
172    #[test]
173    fn rejects_invalid_path_indicator() {
174        let card = fixture_card_1stub_with_fp();
175        let mut wire = encode_bytecode(&card).unwrap();
176        // path indicator is at offset 1+1+4+4 = 10. Use 0x18, the
177        // smallest reserved testnet-range indicator (0x18..=0xFD are
178        // all reserved). 0x16 was the obvious choice in v0.1.x but
179        // graduated to a defined indicator in v0.2.0 — see
180        // `bytecode/path::round_trip_indicator_0x16_added_in_v0_2`.
181        wire[10] = 0x18;
182        assert!(matches!(
183            decode_bytecode(&wire),
184            Err(Error::InvalidPathIndicator(0x18)),
185        ));
186    }
187
188    #[test]
189    fn rejects_invalid_xpub_version() {
190        let card = fixture_card_1stub_with_fp();
191        let mut wire = encode_bytecode(&card).unwrap();
192        // xpub_compact version is at offset 1+1+4+4+1 = 11
193        wire[11] = 0xDE;
194        wire[12] = 0xAD;
195        wire[13] = 0xBE;
196        wire[14] = 0xEF;
197        assert!(matches!(
198            decode_bytecode(&wire),
199            Err(Error::InvalidXpubVersion(0xDEADBEEF)),
200        ));
201    }
202
203    #[test]
204    fn rejects_trailing_bytes() {
205        let card = fixture_card_1stub_with_fp();
206        let mut wire = encode_bytecode(&card).unwrap();
207        wire.push(0xFF); // extra byte after xpub
208        assert!(matches!(decode_bytecode(&wire), Err(Error::TrailingBytes),));
209    }
210
211    #[test]
212    fn rejects_truncated_mid_stub() {
213        let card = fixture_card_1stub_with_fp();
214        let wire = encode_bytecode(&card).unwrap();
215        let truncated = &wire[..4]; // header + count + 2/4 stub bytes
216        assert!(matches!(
217            decode_bytecode(truncated),
218            Err(Error::UnexpectedEnd),
219        ));
220    }
221
222    #[test]
223    fn rejects_path_too_deep_at_top_level() {
224        // Construct a card with a hand-crafted bytecode where the path
225        // is an explicit-path with count = 11 (one over the cap).
226        let card = fixture_card_1stub_with_fp();
227        let wire = encode_bytecode(&card).unwrap();
228        // path indicator at offset 1+1+4+4 = 10. Replace the std-table
229        // indicator + xpub_compact tail with explicit-path +
230        // 11 single-byte LEB128 components + xpub_compact.
231        let header_and_pre_path = &wire[..10]; // header + count + stubs + fp
232        let xpub_compact_tail = &wire[11..]; // skip the 1-byte std-table indicator
233        let mut new_wire: Vec<u8> = header_and_pre_path.to_vec();
234        new_wire.push(0xFE); // explicit-path indicator
235        new_wire.push(11); // count = 11 (one over cap)
236        for i in 0..11 {
237            new_wire.push(i); // single-byte LEB128 component
238        }
239        new_wire.extend_from_slice(xpub_compact_tail);
240        assert!(matches!(
241            decode_bytecode(&new_wire),
242            Err(Error::PathTooDeep(11)),
243        ));
244    }
245
246    #[test]
247    fn rejects_invalid_path_component_at_top_level() {
248        // Construct a card with a hand-crafted bytecode where the
249        // explicit-path LEB128 has a 6-byte continuation (overflow).
250        let card = fixture_card_1stub_with_fp();
251        let wire = encode_bytecode(&card).unwrap();
252        let header_and_pre_path = &wire[..10];
253        let xpub_compact_tail = &wire[11..];
254        let mut new_wire: Vec<u8> = header_and_pre_path.to_vec();
255        new_wire.push(0xFE); // explicit-path indicator
256        new_wire.push(1); // count = 1
257        // 6-byte LEB128 with all continuation bits set: triggers overflow check
258        new_wire.extend_from_slice(&[0x80, 0x80, 0x80, 0x80, 0x80, 0x80]);
259        new_wire.extend_from_slice(xpub_compact_tail);
260        assert!(matches!(
261            decode_bytecode(&new_wire),
262            Err(Error::InvalidPathComponent(_)),
263        ));
264    }
265
266    #[test]
267    fn rejects_invalid_xpub_public_key() {
268        // Perturb the public_key bytes (offset 40 within xpub_compact)
269        // to a value that doesn't parse as a compressed secp256k1 point.
270        let card = fixture_card_1stub_with_fp();
271        let mut wire = encode_bytecode(&card).unwrap();
272        // xpub_compact starts at offset 1+1+4+4+1 = 11; public_key
273        // within xpub_compact starts at +40 (= 51).
274        let pub_key_offset = 11 + 40;
275        // 0x05 is not a valid compressed-point prefix (must be 0x02 or 0x03).
276        wire[pub_key_offset] = 0x05;
277        // Fill the rest with garbage that's almost certainly not on the curve.
278        for i in 1..33 {
279            wire[pub_key_offset + i] = 0xFF;
280        }
281        assert!(matches!(
282            decode_bytecode(&wire),
283            Err(Error::InvalidXpubPublicKey(_)),
284        ));
285    }
286}