1use 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
21pub type SecretKeyBytes = [u8; 32];
23pub type ChainCode = [u8; 32];
25
26type HmacSha512 = Hmac<Sha512>;
27
28#[derive(PartialEq, Eq, Debug, Clone)]
31pub struct ExtSecretKey {
32 private_input: DlogProverInput,
34 chain_code: ChainCode,
35 derivation_path: DerivationPath,
36}
37
38#[derive(Error, PartialEq, Eq, Debug, Clone)]
40pub enum ExtSecretKeyError {
41 #[error("parsing error: {0}")]
43 SigmaParsingError(#[from] SigmaParsingError),
44 #[error("serialization error: {0}")]
45 SigmaSerializationError(#[from] SigmaSerializationError),
47 #[error("scalar encoding error")]
49 ScalarEncodingError,
50 #[error("child index error: {0}")]
53 ChildIndexError(#[from] ChildIndexError),
54 #[error("incompatible paths: {0}")]
56 IncompatibleDerivation(String),
57}
58
59impl ExtSecretKey {
60 const BITCOIN_SEED: &'static [u8; 12] = b"Bitcoin seed";
61
62 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 pub fn path(&self) -> DerivationPath {
79 self.derivation_path.clone()
80 }
81
82 pub fn secret_key(&self) -> SecretKey {
84 self.private_input.clone().into()
85 }
86
87 pub fn secret_key_bytes(&self) -> SecretKeyBytes {
89 self.private_input.to_bytes()
90 }
91
92 pub fn public_image(&self) -> ProveDlog {
94 self.private_input.public_image()
95 }
96
97 pub fn public_image_bytes(&self) -> Result<Vec<u8>, ExtSecretKeyError> {
99 Ok(self.public_image().h.sigma_serialize_bytes()?)
100 }
101
102 pub fn public_key(&self) -> Result<ExtPubKey, ExtSecretKeyError> {
104 #[allow(clippy::unwrap_used)]
105 Ok(ExtPubKey::new(
106 self.public_image_bytes()?.try_into().unwrap(),
108 self.chain_code,
109 self.derivation_path.clone(),
110 )?)
111 }
112
113 pub fn child(&self, index: ChildIndex) -> Result<ExtSecretKey, ExtSecretKeyError> {
115 #[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 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 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 self.child(index.next()?)
161 }
162 }
163
164 pub fn derive(&self, up_path: DerivationPath) -> Result<ExtSecretKey, ExtSecretKeyError> {
166 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 pub fn derive_master(seed: MnemonicSeed) -> Result<ExtSecretKey, ExtSecretKeyError> {
186 #[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 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 Bip32Vector::new(
236 "0'",
237 "edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea",
238 ),
239 Bip32Vector::new(
241 "1",
242 "3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368",
243 ),
244 Bip32Vector::new(
246 "2'",
247 "cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca",
248 ),
249 Bip32Vector::new(
251 "2",
252 "0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4",
253 ),
254 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 Bip32Vector::new(
284 "0",
285 "abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e",
286 ),
287 Bip32Vector::new(
289 "2147483647'",
290 "877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93",
291 ),
292 Bip32Vector::new(
294 "1",
295 "704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7",
296 ),
297 Bip32Vector::new(
299 "2147483646'",
300 "f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d",
301 ),
302 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 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 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 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 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 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}