ergo_lib/wallet/
ext_pub_key.rs1use 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
17pub type PubKeyBytes = [u8; EcPoint::GROUP_SIZE];
19pub type ChainCode = [u8; 32];
21
22type HmacSha512 = Hmac<Sha512>;
23
24#[derive(PartialEq, Eq, Debug, Clone)]
27pub struct ExtPubKey {
28 pub public_key: EcPoint,
30 chain_code: ChainCode,
31 pub derivation_path: DerivationPath,
33}
34
35#[derive(Error, PartialEq, Eq, Debug, Clone)]
37pub enum ExtPubKeyError {
38 #[error("incompatible paths: {0}")]
40 IncompatibleDerivation(String),
41}
42
43impl ExtPubKey {
44 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 #[allow(clippy::unwrap_used)]
61 pub fn pub_key_bytes(&self) -> PubKeyBytes {
62 self.public_key
65 .sigma_serialize_bytes()
66 .unwrap()
67 .as_slice()
68 .try_into()
69 .unwrap()
70 }
71
72 pub fn chain_code(&self) -> ChainCode {
74 self.chain_code
75 }
76
77 #[allow(clippy::unwrap_used)]
79 pub fn child(&self, index: ChildIndexNormal) -> Self {
80 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 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 self.child(index.next())
104 }
105 }
106
107 pub fn derive(&self, up_path: DerivationPath) -> Result<Self, ExtPubKeyError> {
109 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 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 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 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 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 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 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 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 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}