thor_devkit/
hdnode.rs

1//! VeChain-tailored hierarchically deterministic nodes support
2//!
3//! [In-deep explanation](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
4//!
5//! This module glues together several important components involved in key derivation
6//! from different sources. You can construct an [`HDNode`] in multiple ways, allowing,
7//! for example, generating a private key from mnemonic or generating a random key.
8
9use bip32::{
10    ChainCode, ChildNumber, DerivationPath, ExtendedKey, ExtendedKeyAttrs, ExtendedPrivateKey,
11    ExtendedPublicKey, Prefix,
12};
13pub use bip39::{Language, Mnemonic};
14use secp256k1::{PublicKey, SecretKey as PrivateKey};
15
16/// Default HD derivation path for VeChain
17pub const VET_EXTERNAL_PATH: &str = "m/44'/818'/0'/0";
18
19// TODO: add zeroize?
20
21#[derive(Clone, Debug, Eq, PartialEq)]
22enum HDNodeVariant {
23    Full(ExtendedPrivateKey<PrivateKey>),
24    Restricted(ExtendedPublicKey<PublicKey>),
25}
26use HDNodeVariant::{Full, Restricted};
27
28/// Hierarchically deterministic node.
29///
30/// To construct a wallet, use the [`HDNode::build`] method. It exposes access to the builder
31/// that supports multiple construction methods and validates the arguments.
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct HDNode(HDNodeVariant);
34
35impl HDNode {
36    pub fn build<'a>() -> HDNodeBuilder<'a> {
37        //! Build an HDNode from various parameters
38        HDNodeBuilder::default()
39    }
40
41    pub fn derive(&self, index: u32) -> Result<Self, HDNodeError> {
42        //! Derive a child given an index.
43        let child = match &self.0 {
44            Full(privkey) => Self(Full(privkey.derive_child(ChildNumber(index))?)),
45            Restricted(pubkey) => Self(Restricted(pubkey.derive_child(ChildNumber(index))?)),
46        };
47        Ok(child)
48    }
49
50    pub fn public_key(&self) -> ExtendedPublicKey<PublicKey> {
51        //! Get underlying extended public key.
52        match &self.0 {
53            Full(privkey) => privkey.public_key(),
54            Restricted(pubkey) => pubkey.clone(),
55        }
56    }
57    pub fn private_key(&self) -> Result<ExtendedPrivateKey<PrivateKey>, HDNodeError> {
58        //! Get underlying extended private key.
59        match &self.0 {
60            Full(privkey) => Ok(privkey.clone()),
61            Restricted(_) => Err(HDNodeError::Crypto),
62        }
63    }
64    pub fn chain_code(&self) -> ChainCode {
65        //! Get underlying chain code.
66        match &self.0 {
67            Full(privkey) => privkey.attrs().chain_code,
68            Restricted(pubkey) => pubkey.attrs().chain_code,
69        }
70    }
71    pub fn parent_fingerprint(&self) -> [u8; 4] {
72        //! Get underlying chain code.
73        match &self.0 {
74            Full(privkey) => privkey.attrs().parent_fingerprint,
75            Restricted(pubkey) => pubkey.attrs().parent_fingerprint,
76        }
77    }
78    pub fn child_number(&self) -> ChildNumber {
79        //! Get underlying chain code.
80        match &self.0 {
81            Full(privkey) => privkey.attrs().child_number,
82            Restricted(pubkey) => pubkey.attrs().child_number,
83        }
84    }
85    pub fn depth(&self) -> u8 {
86        //! Get underlying chain code.
87        match &self.0 {
88            Full(privkey) => privkey.attrs().depth,
89            Restricted(pubkey) => pubkey.attrs().depth,
90        }
91    }
92    pub fn address(&self) -> crate::address::Address {
93        //! Get the address of current node.
94        use crate::address::AddressConvertible;
95
96        match &self.0 {
97            Full(privkey) => privkey.public_key().public_key().address(),
98            Restricted(pubkey) => pubkey.public_key().address(),
99        }
100    }
101}
102
103/// Errors related to HDNode construction and operation.
104#[derive(Clone, Debug, PartialEq, Eq)]
105pub enum HDNodeError {
106    /// Failure of a cryptographic operation.
107    Crypto,
108    /// Failure of some parsing operation (e.g. wrong bytes length)
109    Parse,
110    /// Incorrect child number (above 2u32.pow(31) for derivation from public key)
111    WrongChildNumber,
112    /// Incompatible parameters
113    Unbuildable(String),
114    /// Other error with message
115    Custom(String),
116}
117
118#[cfg(not(tarpaulin_include))]
119impl std::fmt::Display for HDNodeError {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        match self {
122            Self::Crypto => f.write_str("cryptography error"),
123            Self::Parse => f.write_str("decoding error"),
124            Self::WrongChildNumber => {
125                f.write_str("cannot derive hardened children from public key")
126            }
127            Self::Unbuildable(msg) => {
128                f.write_str("cannot build HDNode:")?;
129                f.write_str(msg)
130            }
131            Self::Custom(msg) => f.write_str(msg),
132        }
133    }
134}
135impl std::error::Error for HDNodeError {}
136
137#[cfg(not(tarpaulin_include))]
138impl From<bip32::Error> for HDNodeError {
139    fn from(err: bip32::Error) -> HDNodeError {
140        match err {
141            bip32::Error::Crypto => HDNodeError::Crypto,
142            bip32::Error::Decode => HDNodeError::Parse,
143            bip32::Error::ChildNumber => HDNodeError::WrongChildNumber,
144            err => HDNodeError::Custom(format!("{:?}", err)),
145        }
146    }
147}
148
149/// Builder for HDNode: use this to construct a node from different sources.
150///
151/// The following sources are supported:
152/// - Binary seed. 64 bytes of raw entropy to use for key generation.
153/// - [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic
154///   with optional password. This method is compatible with derivation in Sync2 wallet.
155/// - Master private key bytes and chain code
156/// - Extended private key
157/// - Master public key bytes and chain code
158/// - Extended public key
159///
160/// First two methods accept a derivation path to use (defaults to VeChain path).
161///
162/// For example, here's what you could do:
163///
164/// ```rust
165/// use thor_devkit::hdnode::{Mnemonic, Language, HDNode};
166/// use rand::RngCore;
167///
168/// let mnemonic = Mnemonic::from_phrase(
169///     "ignore empty bird silly journey junior ripple have guard waste between tenant",
170///     Language::English,
171/// )
172/// .expect("Should be constructible");
173/// let wallet = HDNode::build().mnemonic(mnemonic).build().expect("Must be buildable");
174/// // OR
175/// let mut entropy = [0u8; 64];
176/// rand::rng().fill_bytes(&mut entropy);
177/// let other_wallet = HDNode::build().seed(entropy).build().expect("Must be buildable");
178/// ```
179#[derive(Clone, Default)]
180pub struct HDNodeBuilder<'a> {
181    path: Option<DerivationPath>,
182    seed: Option<[u8; 64]>,
183    mnemonic: Option<Mnemonic>,
184    password: Option<&'a str>,
185    ext_privkey: Option<ExtendedKey>,
186    ext_pubkey: Option<ExtendedKey>,
187}
188
189impl<'a> HDNodeBuilder<'a> {
190    pub fn path(mut self, path: DerivationPath) -> Self {
191        //! Set a derivation path to use.
192        //!
193        //! If not called, defaults to `VET_EXTERNAL_PATH`.
194        self.path = Some(path);
195        self
196    }
197    pub const fn seed(mut self, seed: [u8; 64]) -> Self {
198        //! Set a seed to use.
199        self.seed = Some(seed);
200        self
201    }
202
203    pub fn mnemonic(mut self, mnemonic: Mnemonic) -> Self {
204        //! Set a mnemonic to use. You may optionally provide a password as well.
205        //!
206        //! Derivation from mnemonic is compatible with Sync2 wallet (with empty password).
207        self.mnemonic = Some(mnemonic);
208        self
209    }
210    pub fn mnemonic_with_password(mut self, mnemonic: Mnemonic, password: &'a str) -> Self {
211        //! Set a password for the mnemonic to use.
212        //!
213        //! Replaces previous mnemonic, if any.
214        self.mnemonic = Some(mnemonic);
215        self.password = Some(password);
216        self
217    }
218
219    pub fn master_private_key_bytes<T: Into<ChainCode>>(
220        mut self,
221        key: [u8; 33],
222        chain_code: T,
223    ) -> Self {
224        //! Create an HDNode from private key bytes and chain code.
225        self.ext_privkey = Some(ExtendedKey {
226            prefix: Prefix::XPRV,
227            attrs: ExtendedKeyAttrs {
228                depth: 0,
229                parent_fingerprint: [0; 4],
230                child_number: ChildNumber(0u32),
231                chain_code: chain_code.into(),
232            },
233            key_bytes: key,
234        });
235        self
236    }
237    pub fn private_key(mut self, ext_key: ExtendedKey) -> Self {
238        //! Create an HDNode from extended private key structure.
239        self.ext_privkey = Some(ext_key);
240        self
241    }
242
243    pub fn master_public_key_bytes<T: Into<ChainCode>>(
244        mut self,
245        key: [u8; 33],
246        chain_code: T,
247    ) -> Self {
248        //! Create an HDNode from private key bytes and chain code.
249        //!
250        //! <div class="warning">
251        //! Beware that this node cannot be used to derive new private keys.
252        //! </div>
253        self.ext_pubkey = Some(ExtendedKey {
254            prefix: Prefix::XPUB,
255            attrs: ExtendedKeyAttrs {
256                depth: 0,
257                parent_fingerprint: [0; 4],
258                child_number: ChildNumber(0u32),
259                chain_code: chain_code.into(),
260            },
261            key_bytes: key,
262        });
263        self
264    }
265    pub fn public_key(mut self, ext_key: ExtendedKey) -> Self {
266        //! Create an HDNode from extended public key structure.
267        //!
268        //! <div class="warning">
269        //! Beware that this node cannot be used to derive new private keys.
270        //! </div>
271        self.ext_pubkey = Some(ext_key);
272        self
273    }
274
275    pub fn build(self) -> Result<HDNode, HDNodeError> {
276        //! Create an HDNode from given arguments.
277        match (self.seed, self.mnemonic, self.ext_privkey, self.ext_pubkey) {
278            (Some(seed), None, None, None) => {
279                let path = self.path.unwrap_or_else(|| {
280                    VET_EXTERNAL_PATH
281                        .parse()
282                        .expect("hardcoded path must be valid")
283                });
284                Ok(ExtendedPrivateKey::derive_from_path(seed, &path).map(|k| HDNode(Full(k)))?)
285            }
286            (None, Some(mnemonic), None, None) => {
287                let path = self.path.unwrap_or_else(|| {
288                    VET_EXTERNAL_PATH
289                        .parse()
290                        .expect("hardcoded path must be valid")
291                });
292                Ok(ExtendedPrivateKey::derive_from_path(
293                    bip39::Seed::new(&mnemonic, self.password.unwrap_or("")),
294                    &path,
295                )
296                .map(|k| HDNode(Full(k)))?)
297            }
298            (None, None, Some(ext_key), None) => Ok(HDNode(Full(ext_key.try_into()?))),
299            (None, None, None, Some(ext_key)) => Ok(HDNode(Restricted(ext_key.try_into()?))),
300            (None, None, None, None) => Err(HDNodeError::Unbuildable(
301                "no parameters provided".to_string(),
302            )),
303            _ => Err(HDNodeError::Unbuildable(
304                "incompatible parameters".to_string(),
305            )),
306        }
307    }
308}