Skip to main content

mk_codec/bytecode/
path.rs

1//! Origin-path codec — standard-table dictionary + `0xFE` explicit-path
2//! escape hatch.
3//!
4//! Per `design/SPEC_mk_v0_1.md` §3.5 (closure Q-3: cap = 10).
5//!
6//! mk1-internal indicator-byte path dictionary. md1 v0.11+ encodes paths
7//! explicitly via `OriginPath` and does not carry a path-dictionary
8//! table; mk1's dictionary is therefore standalone, not a sibling
9//! mirror. Historically (md-codec v0.10.x and earlier), md1 carried a
10//! compatible table via `Tag::SharedPath` / `Tag::OriginPaths`; the
11//! v0.11 architectural cleanup retired that table per
12//! `descriptor-mnemonic/design/SPEC_v0_11_wire_format.md` §1.4. The
13//! testnet companion `0x16` to mainnet `0x06` (BIP 48 nested-segwit
14//! multisig) was added in mk-codec v0.2.0; the addition is wire-additive
15//! (v0.1.x decoders reject `0x16` as `Error::InvalidPathIndicator(0x16)`,
16//! v0.2+ decoders accept and resolve to `m/48'/1'/0'/1'`).
17//!
18//! Explicit-path encoding: indicator `0xFE`, 1-byte component count
19//! (1..=10), then each component as LEB128-encoded u32 with the BIP 32
20//! hardened-bit in the high bit.
21
22use bitcoin::bip32::{ChildNumber, DerivationPath};
23
24use crate::consts::MAX_PATH_COMPONENTS;
25use crate::error::{Error, Result};
26
27/// Indicator byte for an explicit (non-standard-table) path.
28pub const EXPLICIT_PATH_INDICATOR: u8 = 0xFE;
29
30/// Standard-table dictionary entries — `(indicator_byte, path_string)`.
31///
32/// mk1-internal table — not a sibling mirror. md1 v0.11+ does not carry
33/// a path-dictionary table (per
34/// `descriptor-mnemonic/design/SPEC_v0_11_wire_format.md` §1.4). The 14
35/// entries are: 7 mainnet (`0x01`..=`0x07`) and 7 testnet
36/// (`0x11`..=`0x17`). `0x16` (BIP 48 testnet nested-segwit multisig) was
37/// added in v0.2.0.
38pub const STANDARD_PATHS: &[(u8, &str)] = &[
39    // Mainnet
40    (0x01, "m/44'/0'/0'"),    // BIP 44 mainnet
41    (0x02, "m/49'/0'/0'"),    // BIP 49 mainnet
42    (0x03, "m/84'/0'/0'"),    // BIP 84 mainnet
43    (0x04, "m/86'/0'/0'"),    // BIP 86 mainnet
44    (0x05, "m/48'/0'/0'/2'"), // BIP 48 segwit-v0 multisig mainnet
45    (0x06, "m/48'/0'/0'/1'"), // BIP 48 nested-segwit multisig mainnet
46    (0x07, "m/87'/0'/0'"),    // BIP 87 multisig mainnet
47    // Testnet
48    (0x11, "m/44'/1'/0'"),
49    (0x12, "m/49'/1'/0'"),
50    (0x13, "m/84'/1'/0'"),
51    (0x14, "m/86'/1'/0'"),
52    (0x15, "m/48'/1'/0'/2'"),
53    (0x16, "m/48'/1'/0'/1'"), // v0.2.0+; was reserved-pending in v0.1.x
54    (0x17, "m/87'/1'/0'"),
55];
56
57/// Look up a standard-table indicator → `DerivationPath`. Returns
58/// `None` for indicators outside the dictionary (reserved values
59/// `0x00`, `0x08`..=`0x10`, `0x18`..=`0xFD`, `0xFF`).
60pub fn lookup_indicator(indicator: u8) -> Option<DerivationPath> {
61    STANDARD_PATHS
62        .iter()
63        .find(|(b, _)| *b == indicator)
64        .and_then(|(_, p)| p.parse().ok())
65}
66
67/// Look up `DerivationPath` → standard-table indicator. Returns `None`
68/// if the path is not in the dictionary (encoder falls through to
69/// explicit-path encoding). Comparison is structural (parses each
70/// table entry to a `DerivationPath`); this avoids the `m/`-prefix
71/// pitfall in `bitcoin::bip32::DerivationPath`'s Display.
72pub fn lookup_path(path: &DerivationPath) -> Option<u8> {
73    STANDARD_PATHS
74        .iter()
75        .find(|(_, p)| {
76            p.parse::<DerivationPath>()
77                .map(|table_path| &table_path == path)
78                .unwrap_or(false)
79        })
80        .map(|(b, _)| *b)
81}
82
83/// Encode a path: 1-byte standard-table indicator if available, else
84/// explicit-path escape hatch (`0xFE` + count + LEB128 components).
85pub fn encode_path(path: &DerivationPath) -> Vec<u8> {
86    if let Some(indicator) = lookup_path(path) {
87        return vec![indicator];
88    }
89    let mut out = Vec::with_capacity(2 + 5 * MAX_PATH_COMPONENTS as usize);
90    out.push(EXPLICIT_PATH_INDICATOR);
91    let components: Vec<ChildNumber> = path.into_iter().copied().collect();
92    out.push(components.len() as u8);
93    for cn in components {
94        let raw: u32 = u32::from(cn);
95        leb128_encode(raw, &mut out);
96    }
97    out
98}
99
100/// Decode a path field starting at `*cursor` (advances the cursor).
101pub fn decode_path(cursor: &mut &[u8]) -> Result<DerivationPath> {
102    let indicator = read_u8(cursor)?;
103    if indicator == EXPLICIT_PATH_INDICATOR {
104        return decode_explicit_path(cursor);
105    }
106    if let Some(path) = lookup_indicator(indicator) {
107        return Ok(path);
108    }
109    Err(Error::InvalidPathIndicator(indicator))
110}
111
112fn decode_explicit_path(cursor: &mut &[u8]) -> Result<DerivationPath> {
113    let count = read_u8(cursor)?;
114    if count == 0 || count > MAX_PATH_COMPONENTS {
115        return Err(Error::PathTooDeep(count));
116    }
117    let mut components: Vec<ChildNumber> = Vec::with_capacity(count as usize);
118    for _ in 0..count {
119        let raw = leb128_decode_u32(cursor)?;
120        let cn = if raw & 0x8000_0000 != 0 {
121            ChildNumber::from_hardened_idx(raw & 0x7FFF_FFFF)
122                .map_err(|e| Error::InvalidPathComponent(format!("{e}")))?
123        } else {
124            ChildNumber::from_normal_idx(raw)
125                .map_err(|e| Error::InvalidPathComponent(format!("{e}")))?
126        };
127        components.push(cn);
128    }
129    Ok(DerivationPath::from(components))
130}
131
132fn leb128_encode(mut value: u32, out: &mut Vec<u8>) {
133    loop {
134        let mut byte = (value & 0x7F) as u8;
135        value >>= 7;
136        if value != 0 {
137            byte |= 0x80;
138            out.push(byte);
139        } else {
140            out.push(byte);
141            break;
142        }
143    }
144}
145
146fn leb128_decode_u32(cursor: &mut &[u8]) -> Result<u32> {
147    let mut result: u64 = 0;
148    let mut shift: u32 = 0;
149    loop {
150        let byte = read_u8(cursor)?;
151        result |= ((byte & 0x7F) as u64) << shift;
152        if byte & 0x80 == 0 {
153            break;
154        }
155        shift += 7;
156        // u32 max needs ⌈32/7⌉ = 5 bytes; bail at the 6th byte (shift=35).
157        if shift >= 35 {
158            return Err(Error::InvalidPathComponent(format!(
159                "LEB128 overflow at shift {shift}"
160            )));
161        }
162    }
163    if result > u32::MAX as u64 {
164        return Err(Error::InvalidPathComponent(format!(
165            "LEB128 value {result} > u32::MAX"
166        )));
167    }
168    Ok(result as u32)
169}
170
171fn read_u8(cursor: &mut &[u8]) -> Result<u8> {
172    if cursor.is_empty() {
173        return Err(Error::UnexpectedEnd);
174    }
175    let b = cursor[0];
176    *cursor = &cursor[1..];
177    Ok(b)
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use std::str::FromStr;
184
185    #[test]
186    fn round_trip_all_standard_paths() {
187        for (indicator, path_str) in STANDARD_PATHS {
188            let path = DerivationPath::from_str(path_str).unwrap();
189            let encoded = encode_path(&path);
190            assert_eq!(encoded, vec![*indicator], "round-trip {path_str}");
191            let mut cursor: &[u8] = &encoded;
192            let decoded = decode_path(&mut cursor).unwrap();
193            assert_eq!(decoded, path, "round-trip parsed {path_str}");
194            assert!(cursor.is_empty());
195        }
196    }
197
198    #[test]
199    fn round_trip_explicit_path_simple() {
200        let path = DerivationPath::from_str("m/0/1/2").unwrap();
201        let encoded = encode_path(&path);
202        // 0xFE + count(3) + leb128(0,1,2) = each fits in 1 byte
203        assert_eq!(encoded[0], 0xFE);
204        assert_eq!(encoded[1], 3);
205        let mut cursor: &[u8] = &encoded;
206        let decoded = decode_path(&mut cursor).unwrap();
207        assert_eq!(decoded, path);
208    }
209
210    #[test]
211    fn round_trip_explicit_path_all_hardened() {
212        // m/9999'/1234'/56'/7' — hardened, requires 5 LEB128 bytes per component
213        let path = DerivationPath::from_str("m/9999'/1234'/56'/7'").unwrap();
214        let encoded = encode_path(&path);
215        assert_eq!(encoded[0], 0xFE);
216        assert_eq!(encoded[1], 4);
217        // 0xFE + 1 (count) + 4 * 5 = 22 bytes
218        assert_eq!(encoded.len(), 1 + 1 + 4 * 5);
219        let mut cursor: &[u8] = &encoded;
220        let decoded = decode_path(&mut cursor).unwrap();
221        assert_eq!(decoded, path);
222    }
223
224    #[test]
225    fn round_trip_explicit_path_at_cap() {
226        // 10 components — cap exact
227        let path = DerivationPath::from_str("m/0'/1'/2'/3'/4'/5'/6'/7'/8'/9'").unwrap();
228        let encoded = encode_path(&path);
229        let mut cursor: &[u8] = &encoded;
230        let decoded = decode_path(&mut cursor).unwrap();
231        assert_eq!(decoded, path);
232    }
233
234    #[test]
235    fn rejects_path_too_deep() {
236        // Construct an explicit-path encoding with count = 11
237        let mut bytes = vec![0xFE, 11u8];
238        for i in 0..11 {
239            bytes.push(i); // single-byte LEB128
240        }
241        let mut cursor: &[u8] = &bytes;
242        assert!(matches!(
243            decode_path(&mut cursor),
244            Err(Error::PathTooDeep(11)),
245        ));
246    }
247
248    #[test]
249    fn rejects_path_count_zero() {
250        // count = 0 isn't a real path; rejected as PathTooDeep(0) per
251        // spec MUST be 1..=10
252        let bytes = vec![0xFE, 0u8];
253        let mut cursor: &[u8] = &bytes;
254        assert!(matches!(
255            decode_path(&mut cursor),
256            Err(Error::PathTooDeep(0)),
257        ));
258    }
259
260    #[test]
261    fn rejects_reserved_indicator_zero() {
262        let bytes = vec![0x00];
263        let mut cursor: &[u8] = &bytes;
264        assert!(matches!(
265            decode_path(&mut cursor),
266            Err(Error::InvalidPathIndicator(0x00)),
267        ));
268    }
269
270    #[test]
271    fn round_trip_indicator_0x16_added_in_v0_2() {
272        // 0x16 was reserved-pending in v0.1.x; added to STANDARD_PATHS
273        // in v0.2.0. Resolves to BIP 48 testnet nested-segwit multisig
274        // (`m/48'/1'/0'/1'`). Historical context: this entry tracked an
275        // md1-side gap at the time the mk-codec v0.2.0 cycle ran;
276        // md1 v0.11+ has since dropped path dictionaries entirely (the
277        // mirror invariant is retired — see this module's rustdoc).
278        let path = DerivationPath::from_str("m/48'/1'/0'/1'").unwrap();
279        let encoded = encode_path(&path);
280        assert_eq!(encoded, vec![0x16]);
281        let mut cursor: &[u8] = &encoded;
282        let decoded = decode_path(&mut cursor).unwrap();
283        assert_eq!(decoded, path);
284        assert!(cursor.is_empty());
285    }
286
287    #[test]
288    fn rejects_reserved_indicator_high_range() {
289        // 0xFD (just below 0xFE explicit) is reserved
290        let bytes = vec![0xFD];
291        let mut cursor: &[u8] = &bytes;
292        assert!(matches!(
293            decode_path(&mut cursor),
294            Err(Error::InvalidPathIndicator(0xFD)),
295        ));
296        // 0xFF is reserved
297        let bytes = vec![0xFF];
298        let mut cursor: &[u8] = &bytes;
299        assert!(matches!(
300            decode_path(&mut cursor),
301            Err(Error::InvalidPathIndicator(0xFF)),
302        ));
303    }
304
305    #[test]
306    fn rejects_truncated_explicit_path() {
307        // 0xFE indicator + count(2) + only one component byte
308        let bytes = vec![0xFE, 2u8, 0u8];
309        let mut cursor: &[u8] = &bytes;
310        assert!(matches!(
311            decode_path(&mut cursor),
312            Err(Error::UnexpectedEnd),
313        ));
314    }
315
316    #[test]
317    fn leb128_encode_examples() {
318        // 0 → [0]
319        let mut out = Vec::new();
320        leb128_encode(0, &mut out);
321        assert_eq!(out, vec![0]);
322        // 127 → [0x7F]
323        let mut out = Vec::new();
324        leb128_encode(127, &mut out);
325        assert_eq!(out, vec![0x7F]);
326        // 128 → [0x80, 0x01]
327        let mut out = Vec::new();
328        leb128_encode(128, &mut out);
329        assert_eq!(out, vec![0x80, 0x01]);
330        // 0x80000000 (hardened bit set) → [0x80, 0x80, 0x80, 0x80, 0x08]
331        let mut out = Vec::new();
332        leb128_encode(0x8000_0000, &mut out);
333        assert_eq!(out, vec![0x80, 0x80, 0x80, 0x80, 0x08]);
334    }
335}