1use std::str::FromStr;
7
8use super::*;
9
10#[derive(Clone, Debug)]
13pub struct Bip32;
14
15impl Bip32 {
16 pub fn master<C: KeyDerivationCrypto>(
18 &self, seed: &Seed, subtree: &'static dyn Subtree<Suite = C>,
19 ) -> Bip32Node<C> {
20 let path = Default::default();
21 let xsk = subtree.master(seed);
22 Bip32Node { path, xsk, subtree }
23 }
24}
25
26#[derive(Clone)]
27pub struct Bip32Node<C: KeyDerivationCrypto + 'static> {
30 path: bip32::Path,
31 xsk: <C as KeyDerivationCrypto>::ExtendedPrivateKey,
32 subtree: &'static dyn Subtree<Suite = C>,
33}
34
35impl<C: KeyDerivationCrypto + 'static> fmt::Debug for Bip32Node<C> {
36 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> std::result::Result<(), fmt::Error> {
37 formatter
38 .debug_struct("Bip32Node")
39 .field("path", &self.path)
40 .field("xsk", &"...")
41 .field("subtree", &self.subtree)
42 .finish()
43 }
44}
45
46impl<C: KeyDerivationCrypto + 'static> Bip32Node<C> {
47 pub fn new(
49 path: bip32::Path, xsk: <C as KeyDerivationCrypto>::ExtendedPrivateKey,
50 subtree: &'static dyn Subtree<Suite = C>,
51 ) -> Self {
52 Self { path, xsk, subtree }
53 }
54
55 pub fn path(&self) -> &bip32::Path {
57 &self.path
58 }
59
60 pub fn xsk(&self) -> &<C as KeyDerivationCrypto>::ExtendedPrivateKey {
62 &self.xsk
63 }
64
65 pub fn subtree(&self) -> &'static dyn Subtree<Suite = C> {
67 self.subtree
68 }
69
70 pub fn neuter(&self) -> Bip32PublicNode<C> {
73 let xpk = self.xsk.neuter();
74 Bip32PublicNode::new(self.path.clone(), xpk, self.subtree)
75 }
76
77 pub fn derive_normal(&self, idx: i32) -> Result<Bip32Node<C>> {
79 let path = self.path.append(ChildIndex::Normal(idx));
80 let xsk = self.xsk.derive_normal_child(idx)?;
81 let subtree = self.subtree;
82 Ok(Self { path, xsk, subtree })
83 }
84
85 pub fn derive_hardened(&self, idx: i32) -> Result<Bip32Node<C>> {
87 let path = self.path.append(ChildIndex::Hardened(idx));
88 let xsk = self.xsk.derive_hardened_child(idx)?;
89 let subtree = self.subtree;
90 Ok(Self { path, xsk, subtree })
91 }
92
93 pub fn private_key(&self) -> C::PrivateKey {
95 self.xsk.private_key()
96 }
97}
98
99#[derive(Clone)]
100pub struct Bip32PublicNode<C: KeyDerivationCrypto + 'static> {
104 path: bip32::Path,
105 xpk: <C as KeyDerivationCrypto>::ExtendedPublicKey,
106 subtree: &'static dyn Subtree<Suite = C>,
107}
108
109impl<C: KeyDerivationCrypto + 'static> Bip32PublicNode<C> {
110 pub fn new(
112 path: bip32::Path, xpk: <C as KeyDerivationCrypto>::ExtendedPublicKey,
113 subtree: &'static dyn Subtree<Suite = C>,
114 ) -> Self {
115 Self { path, xpk, subtree }
116 }
117
118 pub fn path(&self) -> &bip32::Path {
120 &self.path
121 }
122
123 pub fn xpk(&self) -> &<C as KeyDerivationCrypto>::ExtendedPublicKey {
125 &self.xpk
126 }
127
128 pub fn subtree(&self) -> &'static dyn Subtree<Suite = C> {
130 self.subtree
131 }
132
133 pub fn derive_normal(&self, idx: i32) -> Result<Bip32PublicNode<C>> {
135 let path = self.path.append(ChildIndex::Normal(idx));
136 let xpk = self.xpk.derive_normal_child(idx)?;
137 let subtree = self.subtree;
138 Ok(Self { path, xpk, subtree })
139 }
140
141 pub fn public_key(&self) -> C::PublicKey {
143 self.xpk.public_key()
144 }
145
146 pub fn key_id(&self) -> C::KeyId {
150 let pk = self.public_key();
151 self.subtree.key_id(&pk)
152 }
153}
154
155impl<C: KeyDerivationCrypto + 'static> fmt::Debug for Bip32PublicNode<C> {
156 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> std::result::Result<(), fmt::Error> {
157 formatter
158 .debug_struct("Bip32PublicNode")
159 .field("path", &self.path)
160 .field("xpk", &"...")
161 .field("subtree", &self.subtree)
162 .finish()
163 }
164}
165
166#[derive(Clone, Debug, Eq, PartialEq)]
170pub enum ChildIndex {
171 Normal(i32),
174 Hardened(i32),
178}
179
180fn is_hardened_suffix_char(c: char) -> bool {
181 ['\'', 'h', 'H'].contains(&c)
182}
183
184impl FromStr for ChildIndex {
185 type Err = anyhow::Error;
186 fn from_str(mut src: &str) -> Result<Self> {
187 let hardened = src.ends_with(is_hardened_suffix_char);
188 if hardened {
189 src = &src[..src.len() - 1];
190 };
191 let idx = src.parse::<i32>()?;
192 if idx < 0 {
193 bail!("BIP32 derivation index cannot be negative");
194 }
195 Ok(if hardened { ChildIndex::Hardened(idx) } else { ChildIndex::Normal(idx) })
196 }
197}
198
199impl fmt::Display for ChildIndex {
200 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), fmt::Error> {
201 match self {
202 ChildIndex::Normal(idx) => formatter.write_fmt(format_args!("{}", idx)),
203 ChildIndex::Hardened(idx) => formatter.write_fmt(format_args!("{}'", idx)),
204 }
205 }
206}
207
208#[derive(Clone, Debug, Default, Eq, PartialEq)]
214pub struct Path {
215 path: Vec<ChildIndex>,
216}
217
218impl Path {
219 pub fn append(&self, child: ChildIndex) -> Self {
221 let mut path = self.path.clone();
222 path.push(child);
223 Self { path }
224 }
225}
226
227impl FromStr for Path {
228 type Err = anyhow::Error;
229 fn from_str(src: &str) -> Result<Self> {
230 let mut pieces = src.split('/');
231
232 let first_opt = pieces.next();
233 if let Some(first) = first_opt {
234 if first != "m" && first != "M" {
235 bail!("BIP32 derivation path needs to start with 'm'");
236 }
237 } else {
238 bail!("BIP32 derivation path cannot be empty");
239 }
240
241 let (mut successes, errors): (Vec<_>, Vec<_>) =
242 pieces.map(|p: &str| (p, p.parse::<ChildIndex>())).partition(|(_p, i)| i.is_ok());
243
244 if !errors.is_empty() {
245 bail!("BIP32 derivation path contains invalid child indices: {:?}", errors);
246 }
247
248 let path = successes.drain(..).map(|(_p, i)| i.unwrap()).collect();
251 Ok(Path { path })
252 }
253}
254
255impl From<Vec<ChildIndex>> for Path {
256 fn from(path: Vec<ChildIndex>) -> Self {
257 Self { path }
258 }
259}
260
261impl fmt::Display for Path {
262 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), fmt::Error> {
263 use fmt::Write as _;
264 formatter.write_char('m')?;
265 for c in &self.path {
266 write!(formatter, "/{}", c)?;
267 }
268 Ok(())
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use crate::*;
275
276 #[test]
277 fn childidx_fromstr() {
278 assert_eq!("0".parse::<ChildIndex>().unwrap(), ChildIndex::Normal(0));
279 assert_eq!("0h".parse::<ChildIndex>().unwrap(), ChildIndex::Hardened(0));
280 assert_eq!("0H".parse::<ChildIndex>().unwrap(), ChildIndex::Hardened(0));
281 assert_eq!("0'".parse::<ChildIndex>().unwrap(), ChildIndex::Hardened(0));
282 assert_eq!("2147483647".parse::<ChildIndex>().unwrap(), ChildIndex::Normal(2_147_483_647));
283 assert_eq!(
284 "2147483647'".parse::<ChildIndex>().unwrap(),
285 ChildIndex::Hardened(2_147_483_647)
286 );
287 assert!("2147483648".parse::<ChildIndex>().is_err());
288 assert!("-1".parse::<ChildIndex>().is_err());
289 assert!("-2147483648".parse::<ChildIndex>().is_err());
290 assert!("522147483648".parse::<ChildIndex>().is_err());
291 assert!("h".parse::<ChildIndex>().is_err());
292 assert!("-h".parse::<ChildIndex>().is_err());
293 assert!("0a".parse::<ChildIndex>().is_err());
294 assert!("a".parse::<ChildIndex>().is_err());
295 }
296
297 #[test]
298 fn path_fromstr() {
299 assert_eq!("m".parse::<Path>().unwrap(), Path { path: Default::default() });
300 assert_eq!("M".parse::<Path>().unwrap(), Path { path: vec![] });
301 assert_eq!("m/0".parse::<Path>().unwrap(), Path { path: vec![ChildIndex::Normal(0)] });
302 assert_eq!("M/44'".parse::<Path>().unwrap(), Path { path: vec![ChildIndex::Hardened(44)] });
303 assert_eq!(
304 "m/44'/0h/0H/0".parse::<Path>().unwrap(),
305 Path {
306 path: vec![
307 ChildIndex::Hardened(44),
308 ChildIndex::Hardened(0),
309 ChildIndex::Hardened(0),
310 ChildIndex::Normal(0)
311 ]
312 }
313 );
314 assert_eq!(
315 "m/2147483647'/2147483647".parse::<Path>().unwrap(),
316 Path {
317 path: vec![ChildIndex::Hardened(2_147_483_647), ChildIndex::Normal(2_147_483_647)]
318 }
319 );
320 assert!("".parse::<Path>().is_err());
321 assert!("m/".parse::<Path>().is_err());
322 assert!("m/m".parse::<Path>().is_err());
323 assert!("m/2147483648".parse::<Path>().is_err());
324 assert!("m/522147483648".parse::<Path>().is_err());
325 }
326
327 #[test]
360 fn derivation() -> Result<()> {
361 let phrase = "blast cargo razor option vote shoe stock cruel mansion boy spot never album crop reflect kangaroo blouse slam empty shoot cable vital crane manual";
362 let seed = Bip39::new().phrase(phrase)?.password(Seed::PASSWORD);
363 let net = &secp256k1::hyd::Mainnet;
364 let master = Bip32.master(&seed, net);
365 let bip44 = master.derive_hardened(44)?;
366 let hyd = bip44.derive_hardened(4_741_444)?;
367 let account = hyd.derive_hardened(0)?;
368 let receives = account.derive_normal(0)?;
369 let key = receives.derive_normal(0)?;
370
371 assert_eq!(master.to_xprv(net), "HYDMVzMRRudwQ4sYtEeUUTuvgSjfFdA4eEEs9tnexVi7wzLh1biJ23KpwowPwpanAcppzuoUuQnDyPG41BV6Haecn1Zzy5fBnEqvqm3EfYsJHfm2");
372 assert_eq!(master.neuter().to_xpub(net), "hydmVzu5j2FCm7zqZBeSU9i6QAU5wQTJCFZyU5nP2w1m7fzrFG9gn3CQhgnchDCuEF5LTsdEgqZsnATTisx8sY5bvf2xqgDbkHtw4o3qxKvjxz5X");
373 assert_eq!(master.path(), &"m".parse()?);
374 assert_eq!(bip44.to_xprv(net), "HYDMVzQELuRPBY1JTQZ1LQD3oJmX9X3atztB57mEaCQEWZMAstsvMp36KGHyPSDR5dfCY1Fc4DLcLk5WwSTwvmwHwioV5dRd6kvqnewQPBBCoWjD");
375 assert_eq!(bip44.neuter().to_xpub(net), "hydmVzwte22eYb8b8MYyL61DX2VwqJLpT2DHPJkxedhsgF1L7ZKK7oug599C8ppMqKWqznGdxp2q28s3TQykNkJE6YFWNjuRc686X6XT9Yojtm9E");
376 assert_eq!(bip44.path(), &"m/44'".parse()?);
377 assert_eq!(hyd.to_xprv(net), "HYDMVzSE83q9tomV2q935JF5mRZXUB37urS6ZpAvi17d5tFNK7K45ddZmjMZJKYQ8yLjKrq4HFewhXuL5AjDzb9Ft5efNT8upEy1ftARyhmxRFZH");
378 assert_eq!(hyd.neuter().to_xpub(net), "hydmVzytRASRFrtmhn914z3FV9Hx9xLMTsmCt1AenSRGFZuXYmkSqdW9XcCn3iAWcCZgEap3diAfhYPXRa5mpHuuxvWehM4j2ZS3V1rZpRF8xTr5");
379 assert_eq!(hyd.path(), &"m/44'/4741444'".parse()?);
380 assert_eq!(account.to_xprv(net), "HYDMVzUVgP7S8GNPrKWhvoFPivfS25QnhfKi1iydA7jbWRwVuMJTVZzBQvBV86zpNJg83rrtvj6SWsftT3nNg5PQ9kwEdzTSpEDH5KbZsjbKBbhs");
381 assert_eq!(account.neuter().to_xpub(net), "hydmW129yVihVKVgXGWfvV3ZSePrhri2FgepKuyMEZ3Eg7bf91jrFZrmAo2hsVcS49dTRP5wZM7A8vudxWk5n8J2Ci12CSNvLy76CsnRaMK4A2b5");
382 assert_eq!(account.path(), &"m/44'/4741444'/0'".parse()?);
383 assert_eq!(receives.to_xprv(net), "HYDMVzVXwQGdZsas8KUKcmTzXtR4Le1fioCn4r8aMFa1CLvCzXJJa28ogEUPdZD1BrSTuBLDGRv5SaPHStH7ZYABhdD6Tf6dCPLd5eaJ9aArC8po");
384 assert_eq!(receives.neuter().to_xpub(net), "hydmW13CEWstvvi9oGUHcTGAFc9V2RJuGpXtP38JRgseN2aNEBjhL21PS7KcNwqbz2jh787bMSPNkd7sYsdVm5rzdeyweau32iL6qzCgpn79R4o2");
385 assert_eq!(receives.path(), &"m/44'/4741444'/0'/0".parse()?);
386 assert_eq!(key.to_xprv(net), "HYDMVzXDVKq5jELcWwpPea9GzS7R8sPmoVo6MjhvGYhenJFBrjzgLw5LPoLzEZY1GyKRqbpPBxZNEvP6TnAq9Qpdi9wo3Bhb9NJNMC79egmr486W");
387 assert_eq!(key.neuter().to_xpub(net), "hydmW14snSSM6HTuBtpMeFwSi9qqpeh1MX8CfvheLz1HwyuM6QS56vwv9gCCyx9GMfhYfMxnn89ynj3y3STp2UxpwT3kFfeGenAkzJRHsuYCLMJW");
388 assert_eq!(key.path(), &"m/44'/4741444'/0'/0/0".parse()?);
389
390 Ok(())
391 }
392
393 #[test]
394 fn parsing() -> Result<()> {
395 let account_xprv = "HYDMVzUVgP7S8GNPrKWhvoFPivfS25QnhfKi1iydA7jbWRwVuMJTVZzBQvBV86zpNJg83rrtvj6SWsftT3nNg5PQ9kwEdzTSpEDH5KbZsjbKBbhs";
396 let account_xpub = "hydmW129yVihVKVgXGWfvV3ZSePrhri2FgepKuyMEZ3Eg7bf91jrFZrmAo2hsVcS49dTRP5wZM7A8vudxWk5n8J2Ci12CSNvLy76CsnRaMK4A2b5";
397 let account_path = "m/44'/4741444'/0'";
398 let net = &secp256k1::hyd::Mainnet;
399
400 let private = Bip32Node::from_xprv(account_path.parse()?, account_xprv, net)?;
401 assert_eq!(private.neuter().to_xpub(net), account_xpub);
402
403 let public = Bip32PublicNode::from_xpub(account_path.parse()?, account_xpub, net)?;
404 let receives = public.derive_normal(0)?;
405 let key = receives.derive_normal(0)?;
406
407 assert_eq!(receives.to_xpub(net), "hydmW13CEWstvvi9oGUHcTGAFc9V2RJuGpXtP38JRgseN2aNEBjhL21PS7KcNwqbz2jh787bMSPNkd7sYsdVm5rzdeyweau32iL6qzCgpn79R4o2");
408 assert_eq!(receives.path(), &"m/44'/4741444'/0'/0".parse()?);
409 assert_eq!(key.to_xpub(net), "hydmW14snSSM6HTuBtpMeFwSi9qqpeh1MX8CfvheLz1HwyuM6QS56vwv9gCCyx9GMfhYfMxnn89ynj3y3STp2UxpwT3kFfeGenAkzJRHsuYCLMJW");
410 assert_eq!(key.path(), &"m/44'/4741444'/0'/0/0".parse()?);
411
412 Ok(())
413 }
414}