• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ergoplatform / sigma-rust / 16405540612

20 Jul 2025 11:50PM UTC coverage: 78.438% (-0.01%) from 78.451%
16405540612

Pull #790

github

web-flow
Merge 7bd76aff4 into 2725f402c
Pull Request #790: Use precomputed tables

62 of 69 new or added lines in 16 files covered. (89.86%)

10 existing lines in 5 files now uncovered.

11961 of 15249 relevant lines covered (78.44%)

2.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.67
/ergo-lib/src/wallet/ext_secret_key.rs
1
//! Extended private key operations according to BIP-32
2
use super::{
3
    derivation_path::{ChildIndex, ChildIndexError, DerivationPath},
4
    ext_pub_key::ExtPubKey,
5
    mnemonic::MnemonicSeed,
6
    secret_key::SecretKey,
7
};
8
use crate::ArrLength;
9
use alloc::{string::String, vec::Vec};
10
use ergotree_interpreter::sigma_protocol::{private_input::DlogProverInput, wscalar::Wscalar};
11
use ergotree_ir::{
12
    serialization::{SigmaParsingError, SigmaSerializable, SigmaSerializationError},
13
    sigma_protocol::sigma_boolean::ProveDlog,
14
};
15
use hmac::{Hmac, Mac};
16

17
use sha2::Sha512;
18
use thiserror::Error;
19

20
/// Private key (serialized Scalar) bytes
21
pub type SecretKeyBytes = [u8; 32];
22
/// Chain code bytes
23
pub type ChainCode = [u8; 32];
24

25
type HmacSha512 = Hmac<Sha512>;
26

27
/// Extended secret key
28
/// implemented according to BIP-32
29
#[derive(PartialEq, Eq, Clone)]
30
pub struct ExtSecretKey {
31
    private_input: Wscalar,
32
    chain_code: ChainCode,
33
    derivation_path: DerivationPath,
34
}
35

36
impl core::fmt::Debug for ExtSecretKey {
NEW
37
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
×
NEW
38
        f.debug_struct("ExtSecretKey")
×
39
            .field("private_input", &"*****") // disable debug output for secret key to prevent key leakage in logs
NEW
40
            .field("chain_code", &self.chain_code)
×
41
            .field("derivation_path", &self.derivation_path)
42
            .finish()
43
    }
44
}
45

46
/// Extended secret key errors
47
#[derive(Error, PartialEq, Eq, Debug, Clone)]
48
pub enum ExtSecretKeyError {
49
    /// Parsing error
50
    #[error("parsing error: {0}")]
51
    SigmaParsingError(#[from] SigmaParsingError),
52
    #[error("serialization error: {0}")]
53
    /// Serializing error
54
    SigmaSerializationError(#[from] SigmaSerializationError),
55
    /// Error encoding bytes as SEC-1-encoded scalar
56
    #[error("scalar encoding error")]
57
    ScalarEncodingError,
58
    /// Derivation path child index error
59
    /// For example trying to use a u32 value for a private index (31 bit size)
60
    #[error("child index error: {0}")]
61
    ChildIndexError(#[from] ChildIndexError),
62
    /// Incompatible derivation paths when trying to derive a new key
63
    #[error("incompatible paths: {0}")]
64
    IncompatibleDerivation(String),
65
}
66

67
impl ExtSecretKey {
68
    const BITCOIN_SEED: &'static [u8; 12] = b"Bitcoin seed";
69

70
    /// Create a new extended secret key instance
71
    pub fn new(
5✔
72
        secret_key_bytes: SecretKeyBytes,
73
        chain_code: ChainCode,
74
        derivation_path: DerivationPath,
75
    ) -> Result<Self, ExtSecretKeyError> {
76
        let private_input =
10✔
77
            Wscalar::from_bytes(&secret_key_bytes).ok_or(ExtSecretKeyError::ScalarEncodingError)?;
78
        Ok(Self {
5✔
79
            private_input,
80
            chain_code,
81
            derivation_path,
5✔
82
        })
83
    }
84

85
    /// Derivation path associated with the ext secret key
86
    pub fn path(&self) -> DerivationPath {
×
87
        self.derivation_path.clone()
×
88
    }
89

90
    /// Returns secret key
91
    pub fn secret_key(&self) -> SecretKey {
×
NEW
92
        DlogProverInput::new(self.private_input.clone()).into()
×
93
    }
94

95
    /// Byte representation of the underlying scalar
96
    pub fn secret_key_bytes(&self) -> SecretKeyBytes {
5✔
97
        self.private_input.to_bytes()
5✔
98
    }
99

100
    /// Public image associated with the private input
101
    pub fn public_image(&self) -> ProveDlog {
5✔
102
        DlogProverInput::new(self.private_input.clone()).public_image()
5✔
103
    }
104

105
    /// Public image bytes in SEC-1 encoded & compressed format
106
    pub fn public_image_bytes(&self) -> Result<Vec<u8>, ExtSecretKeyError> {
5✔
107
        Ok(self.public_image().h.sigma_serialize_bytes()?)
7✔
108
    }
109

110
    /// The extended public key associated with this secret key
111
    pub fn public_key(&self) -> Result<ExtPubKey, ExtSecretKeyError> {
3✔
112
        #[allow(clippy::unwrap_used)]
113
        Ok(ExtPubKey {
4✔
114
            public_key: *self.public_image().h,
3✔
115
            chain_code: self.chain_code,
3✔
116
            derivation_path: self.derivation_path.clone(),
2✔
117
        })
118
    }
119

120
    /// Derive a child extended secret key using the provided index
121
    pub fn child(&self, index: ChildIndex) -> Result<ExtSecretKey, ExtSecretKeyError> {
1✔
122
        // Unwrap is fine due to `ChainCode` type having fixed length of 32.
123
        #[allow(clippy::unwrap_used)]
124
        let mut mac = HmacSha512::new_from_slice(&self.chain_code).unwrap();
1✔
125
        match index {
1✔
126
            ChildIndex::Hardened(_) => {
127
                mac.update(&[0u8]);
1✔
128
                mac.update(&self.secret_key_bytes());
1✔
129
            }
130
            ChildIndex::Normal(_) => mac.update(&self.public_image_bytes()?),
1✔
131
        }
132
        mac.update(&index.to_bits().to_be_bytes());
1✔
133
        let mac_bytes = mac.finalize().into_bytes();
1✔
134
        let mut secret_key_bytes = [0; SecretKeyBytes::LEN];
1✔
135
        secret_key_bytes.copy_from_slice(&mac_bytes[..32]);
1✔
136
        if let Some(wscalar) = Wscalar::from_bytes(&secret_key_bytes) {
2✔
137
            // parse256(IL) + kpar (mod n).
138
            // via https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#child-key-derivation-ckd-functions
139
            let child_secret_key = Wscalar::from(
140
                wscalar
2✔
141
                    .as_scalar_ref()
142
                    .add(self.private_input.as_scalar_ref()),
1✔
143
            );
144
            if child_secret_key.is_zero() {
1✔
145
                // ki == 0 case of:
146
                // > In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid, and one
147
                // > should proceed with the next value for i
148
                // via https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#child-key-derivation-ckd-functions
149
                self.child(index.next()?)
×
150
            } else {
151
                let mut chain_code = [0; ChainCode::LEN];
1✔
152
                chain_code.copy_from_slice(&mac_bytes[32..]);
1✔
153
                ExtSecretKey::new(
154
                    child_secret_key.to_bytes(),
1✔
155
                    chain_code,
1✔
156
                    self.derivation_path.extend(index),
1✔
157
                )
158
            }
159
        } else {
160
            // not in range [0, modulus), thus repeat with next index value (BIP-32)
161
            // This is the 'parse256(IL) ≥ n' case of:
162
            // > In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid, and one
163
            // > should proceed with the next value for i
164
            // via https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#child-key-derivation-ckd-functions
165
            self.child(index.next()?)
×
166
        }
167
    }
168

169
    /// Derive a new extended secret key based on the provided derivation path
170
    pub fn derive(&self, up_path: DerivationPath) -> Result<ExtSecretKey, ExtSecretKeyError> {
1✔
171
        // TODO: branch visibility must also be equal
172
        let is_matching_path = up_path.0[..self.derivation_path.depth()]
3✔
173
            .iter()
174
            .zip(self.derivation_path.0.iter())
1✔
175
            .all(|(a, b)| a == b);
×
176

177
        if up_path.depth() >= self.derivation_path.depth() && is_matching_path {
2✔
178
            up_path.0[self.derivation_path.depth()..]
3✔
179
                .iter()
180
                .try_fold(self.clone(), |parent, i| parent.child(*i))
3✔
181
        } else {
182
            Err(ExtSecretKeyError::IncompatibleDerivation(format!(
×
183
                "{}, {}",
184
                up_path, self.derivation_path
185
            )))
186
        }
187
    }
188

189
    /// Derive a root master key from the provided mnemonic seed
190
    pub fn derive_master(seed: MnemonicSeed) -> Result<ExtSecretKey, ExtSecretKeyError> {
5✔
191
        // Unwrap is safe, we are using a valid static length slice
192
        #[allow(clippy::unwrap_used)]
193
        let mut mac = HmacSha512::new_from_slice(ExtSecretKey::BITCOIN_SEED).unwrap();
5✔
194
        mac.update(&seed);
5✔
195
        let hash = mac.finalize().into_bytes();
5✔
196
        let mut secret_key_bytes = [0; SecretKeyBytes::LEN];
5✔
197
        secret_key_bytes.copy_from_slice(&hash[..32]);
5✔
198
        let mut chain_code = [0; ChainCode::LEN];
5✔
199
        chain_code.copy_from_slice(&hash[32..]);
5✔
200

201
        ExtSecretKey::new(secret_key_bytes, chain_code, DerivationPath::master_path())
5✔
202
    }
203
}
204

205
#[cfg(test)]
206
#[allow(clippy::unwrap_used)]
207
mod tests {
208
    use ergotree_ir::chain::address::{Address, NetworkAddress};
209

210
    use crate::wallet::{
211
        derivation_path::{ChildIndexHardened, ChildIndexNormal},
212
        mnemonic::Mnemonic,
213
    };
214

215
    use super::*;
216
    // Covers the test cases found here: https://en.bitcoin.it/wiki/BIP_0032_TestVectors
217
    // Only tests secret key derivation, pub key derivation is tested in `ext_pub_key.rs`
218

219
    struct Bip32Vector {
220
        next_index: ChildIndex,
221
        expected_secret_key: [u8; 32],
222
    }
223

224
    impl Bip32Vector {
225
        pub fn new(next_index: &str, expected_secret_key: &str) -> Self {
226
            Bip32Vector {
227
                next_index: next_index.parse::<ChildIndex>().unwrap(),
228
                expected_secret_key: base16::decode(expected_secret_key)
229
                    .unwrap()
230
                    .try_into()
231
                    .unwrap(),
232
            }
233
        }
234
    }
235

236
    #[test]
237
    fn bip32_test_vector1() {
238
        let vectors = vec![
239
            // m/0'
240
            Bip32Vector::new(
241
                "0'",
242
                "edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea",
243
            ),
244
            // m/0'/1
245
            Bip32Vector::new(
246
                "1",
247
                "3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368",
248
            ),
249
            // m/0'/1/2'
250
            Bip32Vector::new(
251
                "2'",
252
                "cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca",
253
            ),
254
            // m/0'/1/2'/2
255
            Bip32Vector::new(
256
                "2",
257
                "0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4",
258
            ),
259
            // m/0'/1/2'/2/1000000000
260
            Bip32Vector::new(
261
                "1000000000",
262
                "471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8",
263
            ),
264
        ];
265
        let secret_key =
266
            base16::decode(b"e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35")
267
                .unwrap();
268
        let chain_code =
269
            base16::decode(b"873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508")
270
                .unwrap();
271
        let mut ext_secret_key = ExtSecretKey::new(
272
            secret_key.try_into().unwrap(),
273
            chain_code.try_into().unwrap(),
274
            DerivationPath::master_path(),
275
        )
276
        .unwrap();
277

278
        for v in vectors {
279
            ext_secret_key = ext_secret_key.child(v.next_index).unwrap();
280
            assert_eq!(ext_secret_key.secret_key_bytes(), v.expected_secret_key);
281
        }
282
    }
283

284
    #[test]
285
    fn bip32_test_vector2() {
286
        let vectors = vec![
287
            // m/0
288
            Bip32Vector::new(
289
                "0",
290
                "abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e",
291
            ),
292
            // m/0/2147483647'
293
            Bip32Vector::new(
294
                "2147483647'",
295
                "877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93",
296
            ),
297
            // m/0/2147483647'/1
298
            Bip32Vector::new(
299
                "1",
300
                "704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7",
301
            ),
302
            // m/0/2147483647'/1/2147483646'
303
            Bip32Vector::new(
304
                "2147483646'",
305
                "f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d",
306
            ),
307
            // m/0/2147483647'/1/2147483646'/2
308
            Bip32Vector::new(
309
                "2",
310
                "bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23",
311
            ),
312
        ];
313
        let secret_key =
314
            base16::decode(b"4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e")
315
                .unwrap();
316
        let chain_code =
317
            base16::decode(b"60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689")
318
                .unwrap();
319
        let mut ext_secret_key = ExtSecretKey::new(
320
            secret_key.try_into().unwrap(),
321
            chain_code.try_into().unwrap(),
322
            DerivationPath::master_path(),
323
        )
324
        .unwrap();
325

326
        for v in vectors {
327
            ext_secret_key = ext_secret_key.child(v.next_index).unwrap();
328
            assert_eq!(ext_secret_key.secret_key_bytes(), v.expected_secret_key);
329
        }
330
    }
331

332
    #[test]
333
    fn ergo_node_key_tree_derivation_from_seed() {
334
        // Tests against the following ergo node test vector:
335
        // https://github.com/ergoplatform/ergo/blob/c320810c498bca25a44197840c7c5a86440c5906/ergo-wallet/src/test/scala/org/ergoplatform/wallet/secrets/ExtendedSecretKeySpec.scala#L18-L35
336
        let seed_str = "edge talent poet tortoise trumpet dose";
337
        let seed = Mnemonic::to_seed(seed_str, "");
338
        let expected_root = "4rEDKLd17LX4xNR8ss4ithdqFRc3iFnTiTtQbanWJbCT";
339
        let cases: Vec<(&str, ChildIndex)> = vec![
340
            (
341
                "CLdMMHxNtiPzDnWrVuZQr22VyUx8deUG7vMqMNW7as7M",
342
                ChildIndexNormal::normal(1).unwrap().into(),
343
            ),
344
            (
345
                "9icjp3TuTpRaTn6JK6AHw2nVJQaUnwmkXVdBdQSS98xD",
346
                ChildIndexNormal::normal(2).unwrap().into(),
347
            ),
348
            (
349
                "DWMp3L9JZiywxSb5gSjc5dYxPwEZ6KkmasNiHD6VRcpJ",
350
                ChildIndexHardened::from_31_bit(2).unwrap().into(),
351
            ),
352
        ];
353

354
        let mut ext_secret_key = ExtSecretKey::derive_master(seed).unwrap();
355
        let ext_secret_key_b58 = bs58::encode(ext_secret_key.secret_key_bytes()).into_string();
356

357
        assert_eq!(expected_root, ext_secret_key_b58);
358

359
        for (expected_key, idx) in cases {
360
            ext_secret_key = ext_secret_key.child(idx).unwrap();
361
            let ext_secret_key_b58 = bs58::encode(ext_secret_key.secret_key_bytes()).into_string();
362

363
            assert_eq!(expected_key, ext_secret_key_b58);
364
        }
365
    }
366

367
    #[test]
368
    fn ergo_node_path_derivation() {
369
        // Tests against the following ergo node test vector:
370
        // https://github.com/ergoplatform/ergo/blob/c320810c498bca25a44197840c7c5a86440c5906/ergo-wallet/src/test/scala/org/ergoplatform/wallet/secrets/ExtendedSecretKeySpec.scala#L37-L50
371
        let seed_str = "edge talent poet tortoise trumpet dose";
372
        let seed = Mnemonic::to_seed(seed_str, "");
373
        let cases: Vec<(&str, &str)> = vec![
374
            ("CLdMMHxNtiPzDnWrVuZQr22VyUx8deUG7vMqMNW7as7M", "m/1"),
375
            ("9icjp3TuTpRaTn6JK6AHw2nVJQaUnwmkXVdBdQSS98xD", "m/1/2"),
376
            ("DWMp3L9JZiywxSb5gSjc5dYxPwEZ6KkmasNiHD6VRcpJ", "m/1/2/2'"),
377
        ];
378

379
        let root = ExtSecretKey::derive_master(seed).unwrap();
380

381
        for (expected_key, path) in cases {
382
            let derived = root.derive(path.parse().unwrap()).unwrap();
383
            let ext_secret_key_b58 = bs58::encode(derived.secret_key_bytes()).into_string();
384

385
            assert_eq!(expected_key, ext_secret_key_b58);
386
        }
387
    }
388

389
    #[test]
390
    fn ergo_wallet_incorrect_bip32_derivation() {
391
        // test vector triggering ergo-wallet's incorrect BIP32 key derivation
392
        // see https://github.com/ergoplatform/ergo/issues/1627
393
        let seed_str = "race relax argue hair sorry riot there spirit ready fetch food hedgehog hybrid mobile pretty";
394
        let seed = Mnemonic::to_seed(seed_str, "");
395

396
        // in ergo-wallet the above mnemonic produces "9ewv8sxJ1jfr6j3WUSbGPMTVx3TZgcJKdnjKCbJWhiJp5U62uhP";
397
        let expected_p2pk = "9eYMpbGgBf42bCcnB2nG3wQdqPzpCCw5eB1YaWUUen9uCaW3wwm";
398
        let path = "m/44'/429'/0'/0/0";
399

400
        let root = ExtSecretKey::derive_master(seed).unwrap();
401

402
        let derived = root.derive(path.parse().unwrap()).unwrap();
403
        let p2pk: Address = derived.public_key().unwrap().into();
404
        let mainnet_p2pk =
405
            NetworkAddress::new(ergotree_ir::chain::address::NetworkPrefix::Mainnet, &p2pk);
406

407
        assert_eq!(expected_p2pk, mainnet_p2pk.to_base58());
408
    }
409

410
    #[test]
411
    fn appkit_test_vector() {
412
        // from https://github.com/ergoplatform/ergo-appkit/blob/b77b6910bb36a26d5d46d41ae3af8ae1167c902c/common/src/test/scala/org/ergoplatform/appkit/AppkitTestingCommon.scala#L4-L21
413
        let seed_str = "slow silly start wash bundle suffer bulb ancient height spin express remind today effort helmet";
414
        let seed = Mnemonic::to_seed(seed_str, "");
415
        let root = ExtSecretKey::derive_master(seed).unwrap();
416

417
        let mainnet_p2pk0 = NetworkAddress::new(
418
            ergotree_ir::chain::address::NetworkPrefix::Mainnet,
419
            &root
420
                .derive("m/44'/429'/0'/0/0".parse().unwrap())
421
                .unwrap()
422
                .public_key()
423
                .unwrap()
424
                .into(),
425
        );
426
        let expected_p2pk0 = "9eatpGQdYNjTi5ZZLK7Bo7C3ms6oECPnxbQTRn6sDcBNLMYSCa8";
427

428
        assert_eq!(expected_p2pk0, mainnet_p2pk0.to_base58());
429

430
        let mainnet_p2pk1 = NetworkAddress::new(
431
            ergotree_ir::chain::address::NetworkPrefix::Mainnet,
432
            &root
433
                .derive("m/44'/429'/0'/0/1".parse().unwrap())
434
                .unwrap()
435
                .public_key()
436
                .unwrap()
437
                .into(),
438
        );
439
        let expected_p2pk1 = "9iBhwkjzUAVBkdxWvKmk7ab7nFgZRFbGpXA9gP6TAoakFnLNomk";
440

441
        assert_eq!(expected_p2pk1, mainnet_p2pk1.to_base58());
442
    }
443
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc