Skip to main content

md_codec/
origin_path.rs

1//! Origin-path-decl block per spec §3.4.
2//!
3//! Block format:
4//! - shared mode (bit 4 = 0): `[n: 5 bits, encoded n-1][origin-path-encoding]`
5//! - divergent mode (bit 4 = 1): `[n: 5 bits, encoded n-1][origin-path-encoding × n]`
6//!
7//! origin-path-encoding (explicit-only per D19′):
8//!   `[depth: 4 bits][component × depth]`
9//!
10//! component:
11//!   `[hardened: 1 bit][value: LP4-ext varint]`
12
13use crate::bitstream::{BitReader, BitWriter};
14use crate::error::Error;
15use crate::varint::{read_varint, write_varint};
16
17/// A single BIP-32 path component (e.g. `84'` or `0`).
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct PathComponent {
20    /// Whether this component is hardened (apostrophe in BIP-32 notation).
21    pub hardened: bool,
22    /// Index value (u31 effective range, encoded as LP4-ext varint).
23    pub value: u32,
24}
25
26impl PathComponent {
27    /// Encode this component into `w`: 1 hardened bit + LP4-ext varint value.
28    pub fn write(&self, w: &mut BitWriter) -> Result<(), Error> {
29        w.write_bits(u64::from(self.hardened), 1);
30        write_varint(w, self.value)?;
31        Ok(())
32    }
33
34    /// Decode a `PathComponent` from `r`.
35    pub fn read(r: &mut BitReader) -> Result<Self, Error> {
36        let hardened = r.read_bits(1)? != 0;
37        let value = read_varint(r)?;
38        Ok(Self { hardened, value })
39    }
40}
41
42/// Maximum number of components in a single origin path (4-bit depth field).
43pub const MAX_PATH_COMPONENTS: usize = 15;
44
45/// An explicit BIP-32 origin path (a sequence of `PathComponent`s).
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct OriginPath {
48    /// Ordered components from root toward leaf.
49    pub components: Vec<PathComponent>,
50}
51
52impl OriginPath {
53    /// Encode the path: 4-bit depth followed by each component.
54    pub fn write(&self, w: &mut BitWriter) -> Result<(), Error> {
55        if self.components.len() > MAX_PATH_COMPONENTS {
56            return Err(Error::PathDepthExceeded {
57                got: self.components.len(),
58                max: MAX_PATH_COMPONENTS,
59            });
60        }
61        w.write_bits(self.components.len() as u64, 4);
62        for c in &self.components {
63            c.write(w)?;
64        }
65        Ok(())
66    }
67
68    /// Decode an `OriginPath` from `r`.
69    pub fn read(r: &mut BitReader) -> Result<Self, Error> {
70        let depth = r.read_bits(4)? as usize;
71        let mut components = Vec::with_capacity(depth);
72        for _ in 0..depth {
73            components.push(PathComponent::read(r)?);
74        }
75        Ok(Self { components })
76    }
77}
78
79/// A path declaration: key count `n` plus either a shared origin path or
80/// `n` divergent origin paths (mode selected by header bit 4).
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct PathDecl {
83    /// Key count, 1..=32 (encoded on the wire as `n - 1` in 5 bits).
84    pub n: u8,
85    /// Path payload — shared (single path) or divergent (one per key).
86    pub paths: PathDeclPaths,
87}
88
89/// Path payload for a [`PathDecl`].
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum PathDeclPaths {
92    /// Single origin path shared by all `n` keys (header bit 4 = 0).
93    Shared(OriginPath),
94    /// `n` distinct origin paths, one per key (header bit 4 = 1).
95    Divergent(Vec<OriginPath>),
96}
97
98impl PathDecl {
99    /// Encode this `PathDecl` into `w`. The mode (shared vs divergent) is
100    /// determined by `self.paths`; the caller is responsible for setting
101    /// header bit 4 to match.
102    ///
103    /// # Errors
104    ///
105    /// - [`Error::KeyCountOutOfRange`] if `self.n` is outside `1..=32`.
106    /// - [`Error::DivergentPathCountMismatch`] if `self.paths` is `Divergent`
107    ///   and the vector length does not equal `self.n`.
108    /// - [`Error::PathDepthExceeded`] propagated from a component's path encoder
109    ///   if any contained path exceeds [`MAX_PATH_COMPONENTS`].
110    pub fn write(&self, w: &mut BitWriter) -> Result<(), Error> {
111        if !(1..=32).contains(&(self.n as u32)) {
112            return Err(Error::KeyCountOutOfRange { n: self.n });
113        }
114        // Encode n-1 in 5 bits per spec §4.2 (count - 1 offset).
115        w.write_bits((self.n - 1) as u64, 5);
116        match &self.paths {
117            PathDeclPaths::Shared(p) => p.write(w)?,
118            PathDeclPaths::Divergent(paths) => {
119                if paths.len() != self.n as usize {
120                    return Err(Error::DivergentPathCountMismatch {
121                        n: self.n,
122                        got: paths.len(),
123                    });
124                }
125                for p in paths {
126                    p.write(w)?;
127                }
128            }
129        }
130        Ok(())
131    }
132
133    /// Decode a `PathDecl` from `r` using `divergent_mode` (header bit 4).
134    pub fn read(r: &mut BitReader, divergent_mode: bool) -> Result<Self, Error> {
135        let n = (r.read_bits(5)? + 1) as u8;
136        let paths = if divergent_mode {
137            let mut paths = Vec::with_capacity(n as usize);
138            for _ in 0..n {
139                paths.push(OriginPath::read(r)?);
140            }
141            PathDeclPaths::Divergent(paths)
142        } else {
143            PathDeclPaths::Shared(OriginPath::read(r)?)
144        };
145        Ok(Self { n, paths })
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    fn bip84() -> OriginPath {
154        // m/84'/0'/0'
155        OriginPath {
156            components: vec![
157                PathComponent {
158                    hardened: true,
159                    value: 84,
160                },
161                PathComponent {
162                    hardened: true,
163                    value: 0,
164                },
165                PathComponent {
166                    hardened: true,
167                    value: 0,
168                },
169            ],
170        }
171    }
172
173    #[test]
174    fn origin_path_round_trip_bip84() {
175        let p = bip84();
176        let mut w = BitWriter::new();
177        p.write(&mut w).unwrap();
178        let bytes = w.into_bytes();
179        let mut r = BitReader::new(&bytes);
180        assert_eq!(OriginPath::read(&mut r).unwrap(), p);
181    }
182
183    #[test]
184    fn origin_path_bit_cost_bip84() {
185        // depth(4) + 84' (1+11) + 0' (1+4) + 0' (1+4) = 26 bits
186        let p = bip84();
187        let mut w = BitWriter::new();
188        p.write(&mut w).unwrap();
189        assert_eq!(w.bit_len(), 26);
190    }
191
192    #[test]
193    fn origin_path_rejects_depth_too_large() {
194        let p = OriginPath {
195            components: (0..16)
196                .map(|_| PathComponent {
197                    hardened: false,
198                    value: 0,
199                })
200                .collect(),
201        };
202        let mut w = BitWriter::new();
203        assert!(matches!(
204            p.write(&mut w),
205            Err(Error::PathDepthExceeded { got: 16, max: 15 })
206        ));
207    }
208}
209
210#[cfg(test)]
211mod path_decl_tests {
212    use super::*;
213
214    #[test]
215    fn path_decl_shared_round_trip() {
216        let p = PathDecl {
217            n: 1,
218            paths: PathDeclPaths::Shared(OriginPath {
219                components: vec![
220                    PathComponent {
221                        hardened: true,
222                        value: 84,
223                    },
224                    PathComponent {
225                        hardened: true,
226                        value: 0,
227                    },
228                    PathComponent {
229                        hardened: true,
230                        value: 0,
231                    },
232                ],
233            }),
234        };
235        let mut w = BitWriter::new();
236        p.write(&mut w).unwrap();
237        let bytes = w.into_bytes();
238        let mut r = BitReader::new(&bytes);
239        assert_eq!(PathDecl::read(&mut r, false).unwrap(), p);
240    }
241
242    #[test]
243    fn path_decl_shared_bit_cost_bip84() {
244        // n(5) + depth(4) + 84' (1+11) + 0' (1+4) + 0' (1+4) = 31 bits
245        let p = PathDecl {
246            n: 1,
247            paths: PathDeclPaths::Shared(OriginPath {
248                components: vec![
249                    PathComponent {
250                        hardened: true,
251                        value: 84,
252                    },
253                    PathComponent {
254                        hardened: true,
255                        value: 0,
256                    },
257                    PathComponent {
258                        hardened: true,
259                        value: 0,
260                    },
261                ],
262            }),
263        };
264        let mut w = BitWriter::new();
265        p.write(&mut w).unwrap();
266        assert_eq!(w.bit_len(), 31);
267    }
268
269    #[test]
270    fn path_decl_divergent_round_trip() {
271        let p = PathDecl {
272            n: 2,
273            paths: PathDeclPaths::Divergent(vec![
274                OriginPath {
275                    components: vec![PathComponent {
276                        hardened: true,
277                        value: 84,
278                    }],
279                },
280                OriginPath {
281                    components: vec![PathComponent {
282                        hardened: true,
283                        value: 86,
284                    }],
285                },
286            ]),
287        };
288        let mut w = BitWriter::new();
289        p.write(&mut w).unwrap();
290        let bytes = w.into_bytes();
291        let mut r = BitReader::new(&bytes);
292        assert_eq!(PathDecl::read(&mut r, true).unwrap(), p);
293    }
294
295    #[test]
296    fn path_decl_n_zero_rejected() {
297        let p = PathDecl {
298            n: 0,
299            paths: PathDeclPaths::Shared(OriginPath { components: vec![] }),
300        };
301        let mut w = BitWriter::new();
302        assert!(matches!(
303            p.write(&mut w),
304            Err(Error::KeyCountOutOfRange { n: 0 })
305        ));
306    }
307}