ergo_lib/wallet/
ext_pub_key.rs

1//! Extended public key operations according to BIP-32
2use std::convert::TryInto;
3
4use ergo_chain_types::EcPoint;
5use ergotree_interpreter::sigma_protocol::private_input::DlogProverInput;
6use ergotree_ir::chain::address::Address;
7use ergotree_ir::serialization::SigmaParsingError;
8use ergotree_ir::serialization::SigmaSerializable;
9use hmac::{Hmac, Mac};
10use sha2::Sha512;
11use thiserror::Error;
12
13use super::derivation_path::ChildIndex;
14use super::derivation_path::ChildIndexNormal;
15use super::derivation_path::DerivationPath;
16
17/// Public key (serialized EcPoint) bytes
18pub type PubKeyBytes = [u8; EcPoint::GROUP_SIZE];
19/// Chain code bytes
20pub type ChainCode = [u8; 32];
21
22type HmacSha512 = Hmac<Sha512>;
23
24/// Extended public key
25/// implemented according to BIP-32
26#[derive(PartialEq, Eq, Debug, Clone)]
27pub struct ExtPubKey {
28    /// Parsed public key (EcPoint)
29    pub public_key: EcPoint,
30    chain_code: ChainCode,
31    /// Derivation path for this extended public key
32    pub derivation_path: DerivationPath,
33}
34
35/// Extended secret key errors
36#[derive(Error, PartialEq, Eq, Debug, Clone)]
37pub enum ExtPubKeyError {
38    /// Incompatible derivation paths when trying to derive a new key
39    #[error("incompatible paths: {0}")]
40    IncompatibleDerivation(String),
41}
42
43impl ExtPubKey {
44    /// Create ExtPubKey from public key bytes (from SEC1 compressed), chain code and derivation
45    /// path
46    pub fn new(
47        public_key_bytes: PubKeyBytes,
48        chain_code: ChainCode,
49        derivation_path: DerivationPath,
50    ) -> Result<Self, SigmaParsingError> {
51        let public_key = EcPoint::sigma_parse_bytes(&public_key_bytes)?;
52        Ok(Self {
53            public_key,
54            chain_code,
55            derivation_path,
56        })
57    }
58
59    /// Public key bytes of the `ExtPubKey`
60    #[allow(clippy::unwrap_used)]
61    pub fn pub_key_bytes(&self) -> PubKeyBytes {
62        // Unwraps are fine here since `self.public_key` is valid through the checking constructor
63        // above.
64        self.public_key
65            .sigma_serialize_bytes()
66            .unwrap()
67            .as_slice()
68            .try_into()
69            .unwrap()
70    }
71
72    /// Chain code of the `ExtPubKey`
73    pub fn chain_code(&self) -> ChainCode {
74        self.chain_code
75    }
76
77    /// Soft derivation of the child public key with a given index
78    #[allow(clippy::unwrap_used)]
79    pub fn child(&self, index: ChildIndexNormal) -> Self {
80        // Unwrap is fine due to `ChainCode` type having fixed length of 32.
81        let mut mac = HmacSha512::new_from_slice(&self.chain_code).unwrap();
82        mac.update(&self.pub_key_bytes());
83        mac.update(ChildIndex::Normal(index).to_bits().to_be_bytes().as_ref());
84        let mac_bytes = mac.finalize().into_bytes();
85        let mut secret_key_bytes = [0; 32];
86        secret_key_bytes.copy_from_slice(&mac_bytes[..32]);
87        if let Some(child_secret_key) = DlogProverInput::from_bytes(&secret_key_bytes) {
88            let child_pub_key = *child_secret_key.public_image().h * &self.public_key;
89            if ergo_chain_types::ec_point::is_identity(&child_pub_key) {
90                // point is infinity element, thus repeat with next index value (see BIP-32)
91                self.child(index.next())
92            } else {
93                let mut chain_code = [0; 32];
94                chain_code.copy_from_slice(&mac_bytes[32..]);
95                ExtPubKey {
96                    public_key: child_pub_key,
97                    chain_code,
98                    derivation_path: self.derivation_path.extend(index.into()),
99                }
100            }
101        } else {
102            // not in range [0, modulus), thus repeat with next index value (BIP-32)
103            self.child(index.next())
104        }
105    }
106
107    /// Derive a new extended pub key based on the provided derivation path
108    pub fn derive(&self, up_path: DerivationPath) -> Result<Self, ExtPubKeyError> {
109        // TODO: branch visibility must also be equal
110        let is_matching_path = up_path.0[..self.derivation_path.depth()]
111            .iter()
112            .zip(self.derivation_path.0.iter())
113            .all(|(a, b)| a == b);
114
115        if up_path.depth() >= self.derivation_path.depth() && is_matching_path {
116            up_path.0[self.derivation_path.depth()..]
117                .iter()
118                .try_fold(self.clone(), |parent, i| match i {
119                    ChildIndex::Hardened(_) => Err(ExtPubKeyError::IncompatibleDerivation(
120                        format!("pub keys can't use hardened paths: {}", i),
121                    )),
122                    ChildIndex::Normal(i) => Ok(parent.child(*i)),
123                })
124        } else {
125            Err(ExtPubKeyError::IncompatibleDerivation(format!(
126                "{}, {}",
127                up_path, self.derivation_path
128            )))
129        }
130    }
131}
132
133impl From<ExtPubKey> for Address {
134    fn from(epk: ExtPubKey) -> Self {
135        Address::P2Pk(epk.public_key.into())
136    }
137}
138
139#[cfg(test)]
140#[allow(clippy::unwrap_used)]
141mod tests {
142    use crate::wallet::{
143        derivation_path::ChildIndexHardened, ext_secret_key::ExtSecretKey, mnemonic::Mnemonic,
144    };
145
146    use super::*;
147
148    #[test]
149    fn bip32_test_vector0() {
150        // from https://en.bitcoin.it/wiki/BIP_0032_TestVectors
151        // Chain m/0' from Test vector 1
152        // The difference between path "m/0'" and our "m/44'/429'/0" does not matter
153        // since we only testing soft derivation for children
154        let derivation_path =
155            DerivationPath::new(ChildIndexHardened::from_31_bit(0).unwrap(), vec![]);
156        let pub_key_bytes =
157            base16::decode(b"035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56")
158                .unwrap();
159        let chain_code =
160            base16::decode(b"47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141")
161                .unwrap();
162        let ext_pub_key = ExtPubKey::new(
163            pub_key_bytes.try_into().unwrap(),
164            chain_code.try_into().unwrap(),
165            derivation_path,
166        )
167        .unwrap();
168
169        // Chain m/0'/1
170        let child = ext_pub_key.child(ChildIndexNormal::normal(1).unwrap());
171        let expected_child_pub_key_bytes: PubKeyBytes =
172            base16::decode(b"03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c")
173                .unwrap()
174                .try_into()
175                .unwrap();
176        assert_eq!(child.pub_key_bytes(), expected_child_pub_key_bytes);
177    }
178
179    #[test]
180    fn bip32_test_vector1() {
181        // from https://en.bitcoin.it/wiki/BIP_0032_TestVectors
182        // Chain m/0'/1/2' from Test vector 1
183        // The difference between path "m/0'/1/2'" and our "m/44'/429'/0" does not matter
184        // since we only testing soft derivation for children
185        let derivation_path =
186            DerivationPath::new(ChildIndexHardened::from_31_bit(0).unwrap(), vec![]);
187        let pub_key_bytes =
188            base16::decode(b"0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2")
189                .unwrap();
190        let chain_code =
191            base16::decode(b"04466b9cc8e161e966409ca52986c584f07e9dc81f735db683c3ff6ec7b1503f")
192                .unwrap();
193        let ext_pub_key = ExtPubKey::new(
194            pub_key_bytes.try_into().unwrap(),
195            chain_code.try_into().unwrap(),
196            derivation_path,
197        )
198        .unwrap();
199
200        // Chain m/0'/1/2'/2
201        let child = ext_pub_key.child(ChildIndexNormal::normal(2).unwrap());
202        let expected_child_pub_key_bytes: PubKeyBytes =
203            base16::decode(b"02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29")
204                .unwrap()
205                .try_into()
206                .unwrap();
207        assert_eq!(child.pub_key_bytes(), expected_child_pub_key_bytes);
208
209        // Chain m/0'/1/2'/2/1000000000
210        let child2 = child.child(ChildIndexNormal::normal(1000000000).unwrap());
211        let expected_child2_pub_key_bytes: PubKeyBytes =
212            base16::decode(b"022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011")
213                .unwrap()
214                .try_into()
215                .unwrap();
216        assert_eq!(child2.pub_key_bytes(), expected_child2_pub_key_bytes);
217    }
218
219    #[test]
220    fn bip32_test_vector2() {
221        // from https://en.bitcoin.it/wiki/BIP_0032_TestVectors
222        // Chain m from Test vector 2
223        // The difference between path "m" and our "m/44'/429'/0" does not matter
224        // since we only testing soft derivation for children
225        let derivation_path =
226            DerivationPath::new(ChildIndexHardened::from_31_bit(0).unwrap(), vec![]);
227        let pub_key_bytes =
228            base16::decode(b"03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7")
229                .unwrap();
230        let chain_code =
231            base16::decode(b"60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689")
232                .unwrap();
233        let ext_pub_key = ExtPubKey::new(
234            pub_key_bytes.try_into().unwrap(),
235            chain_code.try_into().unwrap(),
236            derivation_path,
237        )
238        .unwrap();
239
240        // Chain m/0
241        let child = ext_pub_key.child(ChildIndexNormal::normal(0).unwrap());
242        let expected_child_pub_key_bytes: PubKeyBytes =
243            base16::decode(b"02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea")
244                .unwrap()
245                .try_into()
246                .unwrap();
247        assert_eq!(child.pub_key_bytes(), expected_child_pub_key_bytes);
248    }
249
250    #[test]
251    fn ergo_node_key_tree_derivation_from_seed() {
252        // Tests against the following ergo node test vector:
253        // https://github.com/ergoplatform/ergo/blob/c320810c498bca25a44197840c7c5a86440c5906/ergo-wallet/src/test/scala/org/ergoplatform/wallet/secrets/ExtendedPublicKeySpec.scala#L13-L30
254        let seed_str = "edge talent poet tortoise trumpet dose";
255        let seed = Mnemonic::to_seed(seed_str, "");
256        let root_secret = ExtSecretKey::derive_master(seed).unwrap();
257        let expected_root = "kTV6HY41wXZVSqdpoe1heA8pBZFEN2oq5T59ZCMpqKKJ";
258        let cases: Vec<(&str, ChildIndexNormal)> = vec![
259            (
260                "uRg1eWWRkhghMxhcZEy2rRjfbc3MqWCJ1oVSP4dNmBAW",
261                ChildIndexNormal::normal(1).unwrap(),
262            ),
263            (
264                "xfhJ6aCQUodzhw1J4NcD7iJFvGVc3iPk3pBARCTncYcE",
265                ChildIndexNormal::normal(1).unwrap(),
266            ),
267            (
268                "2282dj5QqC7SM7G2ndp4pzaMZwT7vGgUAUZLCKhmXQFxG",
269                ChildIndexNormal::normal(1).unwrap(),
270            ),
271        ];
272
273        let mut ext_pub_key = root_secret.public_key().unwrap();
274        let ext_pub_key_b58 = bs58::encode(ext_pub_key.pub_key_bytes()).into_string();
275
276        assert_eq!(expected_root, ext_pub_key_b58);
277
278        for (expected_key, idx) in cases {
279            ext_pub_key = ext_pub_key.child(idx);
280            let ext_pub_key_b58 = bs58::encode(ext_pub_key.pub_key_bytes()).into_string();
281
282            assert_eq!(expected_key, ext_pub_key_b58);
283        }
284    }
285}