Skip to main content

avalanche_types/key/secp256k1/
mod.rs

1//! Modules for secp256k1 key management in Avalanche.
2pub mod address;
3pub mod keychain;
4pub mod kms;
5pub mod private_key;
6pub mod public_key;
7pub mod signature;
8pub mod txs;
9
10#[cfg(feature = "libsecp256k1")]
11#[cfg_attr(docsrs, doc(cfg(feature = "libsecp256k1")))]
12pub mod libsecp256k1;
13
14#[cfg(feature = "mnemonic")]
15#[cfg_attr(docsrs, doc(cfg(feature = "mnemonic")))]
16pub mod mnemonic;
17
18use std::{
19    collections::HashMap,
20    fmt,
21    fs::{self, File},
22    io::Write,
23    path::Path,
24};
25
26use crate::{
27    codec::serde::hex_0x_primitive_types_h160::Hex0xH160,
28    errors::{Error, Result},
29    ids::short,
30};
31use async_trait::async_trait;
32use lazy_static::lazy_static;
33use rust_embed::RustEmbed;
34use serde::{Deserialize, Serialize};
35use serde_with::{serde_as, DisplayFromStr};
36
37/// Key interface that "only" allows "sign" operations.
38/// Trait is used here to limit access to the underlying private/secret key.
39/// or to enable secure remote key management service integration (e.g., KMS ECC_SECG_P256K1).
40#[async_trait]
41pub trait SignOnly {
42    fn signing_key(&self) -> Result<k256::ecdsa::SigningKey>;
43
44    /// Signs the 32-byte SHA256 output message with the ECDSA private key and the recoverable code.
45    /// "github.com/decred/dcrd/dcrec/secp256k1/v3/ecdsa.SignCompact" outputs 65-byte signature.
46    /// ref. "avalanchego/utils/crypto.PrivateKeySECP256K1R.SignHash"
47    /// ref. <https://github.com/rust-bitcoin/rust-secp256k1/blob/master/src/ecdsa/recovery.rs>
48    /// ref. <https://docs.rs/secp256k1/latest/secp256k1/struct.SecretKey.html#method.sign_ecdsa>
49    /// ref. <https://docs.rs/secp256k1/latest/secp256k1/struct.Message.html>
50    /// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/utils/crypto#PrivateKeyED25519.SignHash>
51    async fn sign_digest(&self, digest: &[u8]) -> Result<[u8; 65]>;
52}
53
54/// Key interface that "only" allows "read" operations.
55pub trait ReadOnly {
56    fn key_type(&self) -> KeyType;
57    /// Implements "crypto.PublicKeySECP256K1R.Address()" and "formatting.FormatAddress".
58    /// "human readable part" (hrp) must be valid output from "constants.GetHRP(networkID)".
59    /// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/utils/constants>
60    fn hrp_address(&self, network_id: u32, chain_id_alias: &str) -> Result<String>;
61    fn short_address(&self) -> Result<short::Id>;
62    fn short_address_bytes(&self) -> Result<Vec<u8>>;
63    fn eth_address(&self) -> String;
64    fn h160_address(&self) -> primitive_types::H160;
65}
66
67lazy_static! {
68    /// Test keys generated by "avalanchego/utils/crypto.FactorySECP256K1R".
69    pub static ref TEST_KEYS: Vec<crate::key::secp256k1::private_key::Key> = {
70        #[derive(RustEmbed)]
71        #[folder = "artifacts/"]
72        #[prefix = "artifacts/"]
73        struct Asset;
74
75        let key_file = Asset::get("artifacts/test.insecure.secp256k1.key.infos.json").unwrap();
76
77        let key_infos: Vec<Info> = serde_json::from_slice(&key_file.data).unwrap();
78        let mut keys: Vec<crate::key::secp256k1::private_key::Key> = Vec::new();
79        for ki in key_infos.iter() {
80            keys.push(ki.to_private_key());
81        }
82        keys
83    };
84
85    /// Test key infos in the same order of "TEST_KEYS".
86    pub static ref TEST_INFOS: Vec<Info> = {
87        #[derive(RustEmbed)]
88        #[folder = "artifacts/"]
89        #[prefix = "artifacts/"]
90        struct Asset;
91
92        let key_file = Asset::get("artifacts/test.insecure.secp256k1.key.infos.json").unwrap();
93        serde_json::from_slice(&key_file.data).unwrap()
94    };
95}
96
97/// RUST_LOG=debug cargo test --package avalanche-types --lib -- key::secp256k1::test_keys --exact --show-output
98#[test]
99fn test_keys() {
100    let _ = env_logger::builder()
101        .filter_level(log::LevelFilter::Info)
102        .is_test(true)
103        .try_init();
104
105    for k in TEST_KEYS.iter() {
106        log::info!(
107            "[KEY] test key eth address {:?}",
108            k.to_public_key().to_eth_address()
109        );
110    }
111    for ki in TEST_INFOS.iter() {
112        log::info!("[INFO] test key eth address {:?}", ki.eth_address);
113    }
114    assert_eq!(TEST_KEYS.len(), TEST_INFOS.len());
115
116    log::info!("total {} test keys are found", TEST_KEYS.len());
117}
118
119// test random keys generated by "avalanchego/utils/crypto.FactorySECP256K1R"
120// and make sure both generate the same addresses
121// use "avalanche-rs/avalanchego-conformance/key/secp256k1"
122// to generate keys and addresses with "avalanchego"
123#[serde_as]
124#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
125#[serde(rename_all = "snake_case")]
126pub struct Info {
127    /// Optional key identifier (e.g., name, AWS KMS Id/Arn).
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub id: Option<String>,
130    #[serde_as(as = "DisplayFromStr")]
131    pub key_type: KeyType,
132
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub mnemonic_phrase: Option<String>,
135    /// CB58-encoded private key with the prefix "PrivateKey-" (e.g., Avalanche).
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub private_key_cb58: Option<String>,
138    /// Hex-encoded private key without the prefix "0x" (e.g., Ethereum).
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub private_key_hex: Option<String>,
141
142    #[serde(default)]
143    pub addresses: HashMap<u32, ChainAddresses>,
144    #[serde(default)]
145    pub short_address: short::Id,
146    #[serde(default)]
147    pub eth_address: String,
148    #[serde_as(as = "Hex0xH160")]
149    pub h160_address: primitive_types::H160,
150}
151
152impl Default for Info {
153    fn default() -> Self {
154        Info {
155            id: None,
156            key_type: KeyType::Unknown(String::new()),
157            mnemonic_phrase: None,
158            private_key_cb58: None,
159            private_key_hex: None,
160            addresses: HashMap::new(),
161            short_address: short::Id::empty(),
162            eth_address: String::new(),
163            h160_address: primitive_types::H160::zero(),
164        }
165    }
166}
167
168impl From<&crate::key::secp256k1::private_key::Key> for Info {
169    fn from(sk: &crate::key::secp256k1::private_key::Key) -> Self {
170        sk.to_info(1).unwrap()
171    }
172}
173
174impl Info {
175    pub fn load(file_path: &str) -> Result<Self> {
176        log::info!("loading Info from {}", file_path);
177
178        if !Path::new(file_path).exists() {
179            return Err(Error::Other {
180                message: format!("file {} does not exists", file_path),
181                retryable: false,
182            });
183        }
184
185        let f = File::open(file_path).map_err(|e| Error::Other {
186            message: format!("failed to open {} ({})", file_path, e),
187            retryable: false,
188        })?;
189        serde_yaml::from_reader(f).map_err(|e| Error::Other {
190            message: format!("failed serde_yaml::from_reader {}", e),
191            retryable: false,
192        })
193    }
194
195    pub fn sync(&self, file_path: String) -> std::io::Result<()> {
196        log::info!("syncing key info to '{}'", file_path);
197        let path = Path::new(&file_path);
198        let parent_dir = path.parent().unwrap();
199        fs::create_dir_all(parent_dir)?;
200
201        let d = serde_json::to_vec(self).map_err(|e| {
202            std::io::Error::new(
203                std::io::ErrorKind::Other,
204                format!("failed to serialize JSON {}", e),
205            )
206        })?;
207
208        let mut f = File::create(&file_path)?;
209        f.write_all(&d)?;
210
211        Ok(())
212    }
213
214    pub fn to_private_key(&self) -> crate::key::secp256k1::private_key::Key {
215        crate::key::secp256k1::private_key::Key::from_cb58(self.private_key_cb58.clone().unwrap())
216            .unwrap()
217    }
218}
219
220/// ref. <https://doc.rust-lang.org/std/string/trait.ToString.html>
221/// ref. <https://doc.rust-lang.org/std/fmt/trait.Display.html>
222/// Use "Self.to_string()" to directly invoke this.
223impl fmt::Display for Info {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        let s = serde_yaml::to_string(&self).unwrap();
226        write!(f, "{}", s)
227    }
228}
229
230/// Defines the key type.
231#[derive(
232    Deserialize,
233    Serialize,
234    std::clone::Clone,
235    std::cmp::Eq,
236    std::cmp::Ord,
237    std::cmp::PartialEq,
238    std::cmp::PartialOrd,
239    std::fmt::Debug,
240    std::hash::Hash,
241)]
242pub enum KeyType {
243    #[serde(rename = "hot")]
244    Hot,
245    #[serde(rename = "aws-kms")]
246    AwsKms,
247    Unknown(String),
248}
249
250impl std::convert::From<&str> for KeyType {
251    fn from(s: &str) -> Self {
252        match s {
253            "hot" => KeyType::Hot,
254            "aws-kms" => KeyType::AwsKms,
255            "aws_kms" => KeyType::AwsKms,
256
257            other => KeyType::Unknown(other.to_owned()),
258        }
259    }
260}
261
262impl std::str::FromStr for KeyType {
263    type Err = std::convert::Infallible;
264
265    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
266        Ok(KeyType::from(s))
267    }
268}
269
270/// ref. <https://doc.rust-lang.org/std/string/trait.ToString.html>
271/// ref. <https://doc.rust-lang.org/std/fmt/trait.Display.html>
272/// Use "Self.to_string()" to directly invoke this.
273impl std::fmt::Display for KeyType {
274    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275        write!(f, "{}", self.as_str())
276    }
277}
278
279impl KeyType {
280    /// Returns the `&str` value of the enum member.
281    pub fn as_str(&self) -> &str {
282        match self {
283            KeyType::Hot => "hot",
284            KeyType::AwsKms => "aws-kms",
285
286            KeyType::Unknown(s) => s.as_ref(),
287        }
288    }
289
290    /// Returns all the `&str` values of the enum members.
291    pub fn values() -> &'static [&'static str] {
292        &[
293            "hot",     //
294            "aws-kms", //
295        ]
296    }
297}
298
299impl AsRef<str> for KeyType {
300    fn as_ref(&self) -> &str {
301        self.as_str()
302    }
303}
304
305#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
306#[serde(rename_all = "snake_case")]
307pub struct ChainAddresses {
308    pub x: String,
309    pub p: String,
310}
311
312/// RUST_LOG=debug cargo test --package avalanche-types --lib -- key::secp256k1::test_keys_address --exact --show-output
313#[test]
314fn test_keys_address() {
315    let _ = env_logger::builder()
316        .filter_level(log::LevelFilter::Info)
317        .is_test(true)
318        .try_init();
319
320    #[derive(RustEmbed)]
321    #[folder = "artifacts/"]
322    #[prefix = "artifacts/"]
323    struct Asset;
324
325    for asset in ["artifacts/test.insecure.secp256k1.key.infos.json"] {
326        let key_file = Asset::get(asset).unwrap();
327        let key_contents = std::str::from_utf8(key_file.data.as_ref()).unwrap();
328        let key_infos: Vec<Info> = serde_json::from_slice(key_contents.as_bytes()).unwrap();
329        log::info!("loaded {}", asset);
330
331        for (pos, ki) in key_infos.iter().enumerate() {
332            log::info!("checking the key info at {}", pos);
333
334            let sk = crate::key::secp256k1::private_key::Key::from_cb58(
335                &ki.private_key_cb58.clone().unwrap(),
336            )
337            .unwrap();
338            assert_eq!(
339                sk,
340                crate::key::secp256k1::private_key::Key::from_hex(
341                    ki.private_key_hex.clone().unwrap()
342                )
343                .unwrap(),
344            );
345            let pubkey = sk.to_public_key();
346
347            assert_eq!(
348                pubkey.to_hrp_address(1, "X").unwrap(),
349                ki.addresses.get(&1).unwrap().x
350            );
351            assert_eq!(
352                pubkey.to_hrp_address(1, "P").unwrap(),
353                ki.addresses.get(&1).unwrap().p
354            );
355
356            assert_eq!(
357                pubkey.to_hrp_address(9999, "X").unwrap(),
358                ki.addresses.get(&9999).unwrap().x
359            );
360            assert_eq!(
361                pubkey.to_hrp_address(9999, "P").unwrap(),
362                ki.addresses.get(&9999).unwrap().p
363            );
364
365            assert_eq!(pubkey.to_short_id().unwrap(), ki.short_address);
366            assert_eq!(pubkey.to_eth_address(), ki.eth_address);
367        }
368    }
369}