ergo_lib/wallet/
ext_secret_key.rs

1//! Extended private key operations according to BIP-32
2use std::convert::TryInto;
3
4use super::{
5    derivation_path::{ChildIndex, ChildIndexError, DerivationPath},
6    ext_pub_key::ExtPubKey,
7    mnemonic::MnemonicSeed,
8    secret_key::SecretKey,
9};
10use crate::ArrLength;
11use ergotree_interpreter::sigma_protocol::{private_input::DlogProverInput, wscalar::Wscalar};
12use ergotree_ir::{
13    serialization::{SigmaParsingError, SigmaSerializable, SigmaSerializationError},
14    sigma_protocol::sigma_boolean::ProveDlog,
15};
16use hmac::{Hmac, Mac};
17
18use sha2::Sha512;
19use thiserror::Error;
20
21/// Private key (serialized Scalar) bytes
22pub type SecretKeyBytes = [u8; 32];
23/// Chain code bytes
24pub type ChainCode = [u8; 32];
25
26type HmacSha512 = Hmac<Sha512>;
27
28/// Extended secret key
29/// implemented according to BIP-32
30#[derive(PartialEq, Eq, Debug, Clone)]
31pub struct ExtSecretKey {
32    /// The secret key
33    private_input: DlogProverInput,
34    chain_code: ChainCode,
35    derivation_path: DerivationPath,
36}
37
38/// Extended secret key errors
39#[derive(Error, PartialEq, Eq, Debug, Clone)]
40pub enum ExtSecretKeyError {
41    /// Parsing error
42    #[error("parsing error: {0}")]
43    SigmaParsingError(#[from] SigmaParsingError),
44    #[error("serialization error: {0}")]
45    /// Serializing error
46    SigmaSerializationError(#[from] SigmaSerializationError),
47    /// Error encoding bytes as SEC-1-encoded scalar
48    #[error("scalar encoding error")]
49    ScalarEncodingError,
50    /// Derivation path child index error
51    /// For example trying to use a u32 value for a private index (31 bit size)
52    #[error("child index error: {0}")]
53    ChildIndexError(#[from] ChildIndexError),
54    /// Incompatible derivation paths when trying to derive a new key
55    #[error("incompatible paths: {0}")]
56    IncompatibleDerivation(String),
57}
58
59impl ExtSecretKey {
60    const BITCOIN_SEED: &'static [u8; 12] = b"Bitcoin seed";
61
62    /// Create a new extended secret key instance
63    pub fn new(
64        secret_key_bytes: SecretKeyBytes,
65        chain_code: ChainCode,
66        derivation_path: DerivationPath,
67    ) -> Result<Self, ExtSecretKeyError> {
68        let private_input = DlogProverInput::from_bytes(&secret_key_bytes)
69            .ok_or(ExtSecretKeyError::ScalarEncodingError)?;
70        Ok(Self {
71            private_input,
72            chain_code,
73            derivation_path,
74        })
75    }
76
77    /// Derivation path associated with the ext secret key
78    pub fn path(&self) -> DerivationPath {
79        self.derivation_path.clone()
80    }
81
82    /// Returns secret key
83    pub fn secret_key(&self) -> SecretKey {
84        self.private_input.clone().into()
85    }
86
87    /// Byte representation of the underlying scalar
88    pub fn secret_key_bytes(&self) -> SecretKeyBytes {
89        self.private_input.to_bytes()
90    }
91
92    /// Public image associated with the private input
93    pub fn public_image(&self) -> ProveDlog {
94        self.private_input.public_image()
95    }
96
97    /// Public image bytes in SEC-1 encoded & compressed format
98    pub fn public_image_bytes(&self) -> Result<Vec<u8>, ExtSecretKeyError> {
99        Ok(self.public_image().h.sigma_serialize_bytes()?)
100    }
101
102    /// The extended public key associated with this secret key
103    pub fn public_key(&self) -> Result<ExtPubKey, ExtSecretKeyError> {
104        #[allow(clippy::unwrap_used)]
105        Ok(ExtPubKey::new(
106            // unwrap is safe as it is used on an Infallible result type
107            self.public_image_bytes()?.try_into().unwrap(),
108            self.chain_code,
109            self.derivation_path.clone(),
110        )?)
111    }
112
113    /// Derive a child extended secret key using the provided index
114    pub fn child(&self, index: ChildIndex) -> Result<ExtSecretKey, ExtSecretKeyError> {
115        // Unwrap is fine due to `ChainCode` type having fixed length of 32.
116        #[allow(clippy::unwrap_used)]
117        let mut mac = HmacSha512::new_from_slice(&self.chain_code).unwrap();
118        match index {
119            ChildIndex::Hardened(_) => {
120                mac.update(&[0u8]);
121                mac.update(&self.secret_key_bytes());
122            }
123            ChildIndex::Normal(_) => mac.update(&self.public_image_bytes()?),
124        }
125        mac.update(&index.to_bits().to_be_bytes());
126        let mac_bytes = mac.finalize().into_bytes();
127        let mut secret_key_bytes = [0; SecretKeyBytes::LEN];
128        secret_key_bytes.copy_from_slice(&mac_bytes[..32]);
129        if let Some(dlog_prover) = DlogProverInput::from_bytes(&secret_key_bytes) {
130            // parse256(IL) + kpar (mod n).
131            // via https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#child-key-derivation-ckd-functions
132            let child_secret_key: DlogProverInput = Wscalar::from(
133                dlog_prover
134                    .w
135                    .as_scalar_ref()
136                    .add(self.private_input.w.as_scalar_ref()),
137            )
138            .into();
139            if child_secret_key.is_zero() {
140                // ki == 0 case of:
141                // > In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid, and one
142                // > should proceed with the next value for i
143                // via https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#child-key-derivation-ckd-functions
144                self.child(index.next()?)
145            } else {
146                let mut chain_code = [0; ChainCode::LEN];
147                chain_code.copy_from_slice(&mac_bytes[32..]);
148                ExtSecretKey::new(
149                    child_secret_key.to_bytes(),
150                    chain_code,
151                    self.derivation_path.extend(index),
152                )
153            }
154        } else {
155            // not in range [0, modulus), thus repeat with next index value (BIP-32)
156            // This is the 'parse256(IL) ≥ n' case of:
157            // > In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid, and one
158            // > should proceed with the next value for i
159            // via https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#child-key-derivation-ckd-functions
160            self.child(index.next()?)
161        }
162    }
163
164    /// Derive a new extended secret key based on the provided derivation path
165    pub fn derive(&self, up_path: DerivationPath) -> Result<ExtSecretKey, ExtSecretKeyError> {
166        // TODO: branch visibility must also be equal
167        let is_matching_path = up_path.0[..self.derivation_path.depth()]
168            .iter()
169            .zip(self.derivation_path.0.iter())
170            .all(|(a, b)| a == b);
171
172        if up_path.depth() >= self.derivation_path.depth() && is_matching_path {
173            up_path.0[self.derivation_path.depth()..]
174                .iter()
175                .try_fold(self.clone(), |parent, i| parent.child(*i))
176        } else {
177            Err(ExtSecretKeyError::IncompatibleDerivation(format!(
178                "{}, {}",
179                up_path, self.derivation_path
180            )))
181        }
182    }
183
184    /// Derive a root master key from the provided mnemonic seed
185    pub fn derive_master(seed: MnemonicSeed) -> Result<ExtSecretKey, ExtSecretKeyError> {
186        // Unwrap is safe, we are using a valid static length slice
187        #[allow(clippy::unwrap_used)]
188        let mut mac = HmacSha512::new_from_slice(ExtSecretKey::BITCOIN_SEED).unwrap();
189        mac.update(&seed);
190        let hash = mac.finalize().into_bytes();
191        let mut secret_key_bytes = [0; SecretKeyBytes::LEN];
192        secret_key_bytes.copy_from_slice(&hash[..32]);
193        let mut chain_code = [0; ChainCode::LEN];
194        chain_code.copy_from_slice(&hash[32..]);
195
196        ExtSecretKey::new(secret_key_bytes, chain_code, DerivationPath::master_path())
197    }
198}
199
200#[cfg(test)]
201#[allow(clippy::unwrap_used)]
202mod tests {
203    use ergotree_ir::chain::address::{Address, NetworkAddress};
204
205    use crate::wallet::{
206        derivation_path::{ChildIndexHardened, ChildIndexNormal},
207        mnemonic::Mnemonic,
208    };
209
210    use super::*;
211    // Covers the test cases found here: https://en.bitcoin.it/wiki/BIP_0032_TestVectors
212    // Only tests secret key derivation, pub key derivation is tested in `ext_pub_key.rs`
213
214    struct Bip32Vector {
215        next_index: ChildIndex,
216        expected_secret_key: [u8; 32],
217    }
218
219    impl Bip32Vector {
220        pub fn new(next_index: &str, expected_secret_key: &str) -> Self {
221            Bip32Vector {
222                next_index: next_index.parse::<ChildIndex>().unwrap(),
223                expected_secret_key: base16::decode(expected_secret_key)
224                    .unwrap()
225                    .try_into()
226                    .unwrap(),
227            }
228        }
229    }
230
231    #[test]
232    fn bip32_test_vector1() {
233        let vectors = vec![
234            // m/0'
235            Bip32Vector::new(
236                "0'",
237                "edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea",
238            ),
239            // m/0'/1
240            Bip32Vector::new(
241                "1",
242                "3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368",
243            ),
244            // m/0'/1/2'
245            Bip32Vector::new(
246                "2'",
247                "cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca",
248            ),
249            // m/0'/1/2'/2
250            Bip32Vector::new(
251                "2",
252                "0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4",
253            ),
254            // m/0'/1/2'/2/1000000000
255            Bip32Vector::new(
256                "1000000000",
257                "471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8",
258            ),
259        ];
260        let secret_key =
261            base16::decode(b"e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35")
262                .unwrap();
263        let chain_code =
264            base16::decode(b"873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508")
265                .unwrap();
266        let mut ext_secret_key = ExtSecretKey::new(
267            secret_key.try_into().unwrap(),
268            chain_code.try_into().unwrap(),
269            DerivationPath::master_path(),
270        )
271        .unwrap();
272
273        for v in vectors {
274            ext_secret_key = ext_secret_key.child(v.next_index).unwrap();
275            assert_eq!(ext_secret_key.secret_key_bytes(), v.expected_secret_key);
276        }
277    }
278
279    #[test]
280    fn bip32_test_vector2() {
281        let vectors = vec![
282            // m/0
283            Bip32Vector::new(
284                "0",
285                "abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e",
286            ),
287            // m/0/2147483647'
288            Bip32Vector::new(
289                "2147483647'",
290                "877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93",
291            ),
292            // m/0/2147483647'/1
293            Bip32Vector::new(
294                "1",
295                "704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7",
296            ),
297            // m/0/2147483647'/1/2147483646'
298            Bip32Vector::new(
299                "2147483646'",
300                "f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d",
301            ),
302            // m/0/2147483647'/1/2147483646'/2
303            Bip32Vector::new(
304                "2",
305                "bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23",
306            ),
307        ];
308        let secret_key =
309            base16::decode(b"4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e")
310                .unwrap();
311        let chain_code =
312            base16::decode(b"60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689")
313                .unwrap();
314        let mut ext_secret_key = ExtSecretKey::new(
315            secret_key.try_into().unwrap(),
316            chain_code.try_into().unwrap(),
317            DerivationPath::master_path(),
318        )
319        .unwrap();
320
321        for v in vectors {
322            ext_secret_key = ext_secret_key.child(v.next_index).unwrap();
323            assert_eq!(ext_secret_key.secret_key_bytes(), v.expected_secret_key);
324        }
325    }
326
327    #[test]
328    fn ergo_node_key_tree_derivation_from_seed() {
329        // Tests against the following ergo node test vector:
330        // https://github.com/ergoplatform/ergo/blob/c320810c498bca25a44197840c7c5a86440c5906/ergo-wallet/src/test/scala/org/ergoplatform/wallet/secrets/ExtendedSecretKeySpec.scala#L18-L35
331        let seed_str = "edge talent poet tortoise trumpet dose";
332        let seed = Mnemonic::to_seed(seed_str, "");
333        let expected_root = "4rEDKLd17LX4xNR8ss4ithdqFRc3iFnTiTtQbanWJbCT";
334        let cases: Vec<(&str, ChildIndex)> = vec![
335            (
336                "CLdMMHxNtiPzDnWrVuZQr22VyUx8deUG7vMqMNW7as7M",
337                ChildIndexNormal::normal(1).unwrap().into(),
338            ),
339            (
340                "9icjp3TuTpRaTn6JK6AHw2nVJQaUnwmkXVdBdQSS98xD",
341                ChildIndexNormal::normal(2).unwrap().into(),
342            ),
343            (
344                "DWMp3L9JZiywxSb5gSjc5dYxPwEZ6KkmasNiHD6VRcpJ",
345                ChildIndexHardened::from_31_bit(2).unwrap().into(),
346            ),
347        ];
348
349        let mut ext_secret_key = ExtSecretKey::derive_master(seed).unwrap();
350        let ext_secret_key_b58 = bs58::encode(ext_secret_key.secret_key_bytes()).into_string();
351
352        assert_eq!(expected_root, ext_secret_key_b58);
353
354        for (expected_key, idx) in cases {
355            ext_secret_key = ext_secret_key.child(idx).unwrap();
356            let ext_secret_key_b58 = bs58::encode(ext_secret_key.secret_key_bytes()).into_string();
357
358            assert_eq!(expected_key, ext_secret_key_b58);
359        }
360    }
361
362    #[test]
363    fn ergo_node_path_derivation() {
364        // Tests against the following ergo node test vector:
365        // https://github.com/ergoplatform/ergo/blob/c320810c498bca25a44197840c7c5a86440c5906/ergo-wallet/src/test/scala/org/ergoplatform/wallet/secrets/ExtendedSecretKeySpec.scala#L37-L50
366        let seed_str = "edge talent poet tortoise trumpet dose";
367        let seed = Mnemonic::to_seed(seed_str, "");
368        let cases: Vec<(&str, &str)> = vec![
369            ("CLdMMHxNtiPzDnWrVuZQr22VyUx8deUG7vMqMNW7as7M", "m/1"),
370            ("9icjp3TuTpRaTn6JK6AHw2nVJQaUnwmkXVdBdQSS98xD", "m/1/2"),
371            ("DWMp3L9JZiywxSb5gSjc5dYxPwEZ6KkmasNiHD6VRcpJ", "m/1/2/2'"),
372        ];
373
374        let root = ExtSecretKey::derive_master(seed).unwrap();
375
376        for (expected_key, path) in cases {
377            let derived = root.derive(path.parse().unwrap()).unwrap();
378            let ext_secret_key_b58 = bs58::encode(derived.secret_key_bytes()).into_string();
379
380            assert_eq!(expected_key, ext_secret_key_b58);
381        }
382    }
383
384    #[test]
385    fn ergo_wallet_incorrect_bip32_derivation() {
386        // test vector triggering ergo-wallet's incorrect BIP32 key derivation
387        // see https://github.com/ergoplatform/ergo/issues/1627
388        let seed_str = "race relax argue hair sorry riot there spirit ready fetch food hedgehog hybrid mobile pretty";
389        let seed = Mnemonic::to_seed(seed_str, "");
390
391        // in ergo-wallet the above mnemonic produces "9ewv8sxJ1jfr6j3WUSbGPMTVx3TZgcJKdnjKCbJWhiJp5U62uhP";
392        let expected_p2pk = "9eYMpbGgBf42bCcnB2nG3wQdqPzpCCw5eB1YaWUUen9uCaW3wwm";
393        let path = "m/44'/429'/0'/0/0";
394
395        let root = ExtSecretKey::derive_master(seed).unwrap();
396
397        let derived = root.derive(path.parse().unwrap()).unwrap();
398        let p2pk: Address = derived.public_key().unwrap().into();
399        let mainnet_p2pk =
400            NetworkAddress::new(ergotree_ir::chain::address::NetworkPrefix::Mainnet, &p2pk);
401
402        assert_eq!(expected_p2pk, mainnet_p2pk.to_base58());
403    }
404
405    #[test]
406    fn appkit_test_vector() {
407        // from https://github.com/ergoplatform/ergo-appkit/blob/b77b6910bb36a26d5d46d41ae3af8ae1167c902c/common/src/test/scala/org/ergoplatform/appkit/AppkitTestingCommon.scala#L4-L21
408        let seed_str = "slow silly start wash bundle suffer bulb ancient height spin express remind today effort helmet";
409        let seed = Mnemonic::to_seed(seed_str, "");
410        let root = ExtSecretKey::derive_master(seed).unwrap();
411
412        let mainnet_p2pk0 = NetworkAddress::new(
413            ergotree_ir::chain::address::NetworkPrefix::Mainnet,
414            &root
415                .derive("m/44'/429'/0'/0/0".parse().unwrap())
416                .unwrap()
417                .public_key()
418                .unwrap()
419                .into(),
420        );
421        let expected_p2pk0 = "9eatpGQdYNjTi5ZZLK7Bo7C3ms6oECPnxbQTRn6sDcBNLMYSCa8";
422
423        assert_eq!(expected_p2pk0, mainnet_p2pk0.to_base58());
424
425        let mainnet_p2pk1 = NetworkAddress::new(
426            ergotree_ir::chain::address::NetworkPrefix::Mainnet,
427            &root
428                .derive("m/44'/429'/0'/0/1".parse().unwrap())
429                .unwrap()
430                .public_key()
431                .unwrap()
432                .into(),
433        );
434        let expected_p2pk1 = "9iBhwkjzUAVBkdxWvKmk7ab7nFgZRFbGpXA9gP6TAoakFnLNomk";
435
436        assert_eq!(expected_p2pk1, mainnet_p2pk1.to_base58());
437    }
438}