terra_rust_api/keys/
private.rs

1use crate::core_types::StdSignature;
2use crate::keys::PublicKey;
3use bitcoin::util::bip32::{ExtendedPrivKey, IntoDerivationPath};
4use bitcoin::Network;
5use crypto::sha2::Sha256;
6use secp256k1::Message;
7use secp256k1::Secp256k1;
8
9use crypto::digest::Digest;
10use hkd32::mnemonic::{Phrase, Seed};
11
12use crate::errors::TerraRustAPIError;
13use rand_core::OsRng;
14
15/// This is the coin type used in most derivations
16pub static LUNA_COIN_TYPE: u32 = 330;
17
18/// The Private key structure that is used to generate signatures and public keys
19/// WARNING: No Security Audit has been performed
20#[derive(Clone)]
21pub struct PrivateKey {
22    #[allow(missing_docs)]
23    pub account: u32,
24    #[allow(missing_docs)]
25    pub index: u32,
26    #[allow(missing_docs)]
27    pub coin_type: u32,
28    /// The 24 words used to generate this private key
29    mnemonic: Option<Phrase>,
30    #[allow(dead_code)]
31    /// This is used for testing
32    root_private_key: ExtendedPrivKey,
33    /// The private key
34    private_key: ExtendedPrivKey,
35}
36impl PrivateKey {
37    /// Generate a new private key
38    pub fn new<C: secp256k1::Signing + secp256k1::Context>(
39        secp: &Secp256k1<C>,
40    ) -> Result<PrivateKey, TerraRustAPIError> {
41        let phrase =
42            hkd32::mnemonic::Phrase::random(&mut OsRng, hkd32::mnemonic::Language::English);
43
44        PrivateKey::gen_private_key_phrase(secp, phrase, 0, 0, LUNA_COIN_TYPE, "")
45    }
46    /// generate a new private key with a seed phrase
47    pub fn new_seed<C: secp256k1::Signing + secp256k1::Context>(
48        secp: &Secp256k1<C>,
49        seed_phrase: &str,
50    ) -> Result<PrivateKey, TerraRustAPIError> {
51        let phrase =
52            hkd32::mnemonic::Phrase::random(&mut OsRng, hkd32::mnemonic::Language::English);
53
54        PrivateKey::gen_private_key_phrase(secp, phrase, 0, 0, LUNA_COIN_TYPE, seed_phrase)
55    }
56    /// for private key recovery. This is also used by wallet routines to re-hydrate the structure
57    pub fn from_words<C: secp256k1::Signing + secp256k1::Context>(
58        secp: &Secp256k1<C>,
59        words: &str,
60        account: u32,
61        index: u32,
62    ) -> Result<PrivateKey, TerraRustAPIError> {
63        match hkd32::mnemonic::Phrase::new(words, hkd32::mnemonic::Language::English) {
64            Ok(phrase) => {
65                PrivateKey::gen_private_key_phrase(secp, phrase, account, index, LUNA_COIN_TYPE, "")
66            }
67            Err(_) => Err(TerraRustAPIError::Phrasing),
68        }
69    }
70
71    /// for private key recovery with seed phrase
72    pub fn from_words_seed<C: secp256k1::Signing + secp256k1::Context>(
73        secp: &Secp256k1<C>,
74        words: &str,
75        seed_pass: &str,
76    ) -> Result<PrivateKey, TerraRustAPIError> {
77        match hkd32::mnemonic::Phrase::new(words, hkd32::mnemonic::Language::English) {
78            Ok(phrase) => {
79                PrivateKey::gen_private_key_phrase(secp, phrase, 0, 0, LUNA_COIN_TYPE, seed_pass)
80            }
81            Err(_) => Err(TerraRustAPIError::Phrasing),
82        }
83    }
84
85    /// generate the public key for this private key
86    pub fn public_key<C: secp256k1::Signing + secp256k1::Context>(
87        &self,
88        secp: &Secp256k1<C>,
89    ) -> PublicKey {
90        let x = &self.private_key.private_key.public_key(secp);
91        PublicKey::from_bitcoin_public_key(x)
92    }
93
94    fn gen_private_key_phrase<C: secp256k1::Signing + secp256k1::Context>(
95        secp: &Secp256k1<C>,
96        phrase: Phrase,
97        account: u32,
98        index: u32,
99        coin_type: u32,
100        seed_phrase: &str,
101    ) -> Result<PrivateKey, TerraRustAPIError> {
102        let seed = phrase.to_seed(seed_phrase);
103        let root_private_key =
104            ExtendedPrivKey::new_master(Network::Bitcoin, seed.as_bytes()).unwrap();
105        let path = format!("m/44'/{}'/{}'/0/{}", coin_type, account, index);
106        let derivation_path = path.into_derivation_path()?;
107
108        let private_key = root_private_key.derive_priv(secp, &derivation_path)?;
109        Ok(PrivateKey {
110            account,
111            index,
112            coin_type,
113            mnemonic: Some(phrase),
114            root_private_key,
115            private_key,
116        })
117    }
118
119    /// the words used to generate this private key
120    pub fn words(&self) -> Option<&str> {
121        self.mnemonic.as_ref().map(|phrase| phrase.phrase())
122    }
123    /// signs a blob of data and returns a [StdSignature]
124    pub fn sign<C: secp256k1::Signing + secp256k1::Context>(
125        &self,
126        secp: &Secp256k1<C>,
127        blob: &str,
128    ) -> Result<StdSignature, TerraRustAPIError> {
129        let pub_k = &self.private_key.private_key.public_key(secp);
130
131        let priv_k = self.private_key.private_key.key;
132        let mut sha = Sha256::new();
133        let mut sha_result: [u8; 32] = [0; 32];
134        sha.input_str(blob);
135        sha.result(&mut sha_result);
136
137        let message: Message = Message::from_slice(&sha_result)?;
138        let signature = secp.sign(&message, &priv_k);
139
140        //eprintln!("SIG:{}", hex::encode(&signature.serialize_compact()));
141        let sig: StdSignature = StdSignature::create(&signature.serialize_compact(), pub_k);
142        Ok(sig)
143    }
144    /// used for testing
145    /// could potentially be used to recreate the private key instead of words
146    #[allow(dead_code)]
147    pub(crate) fn seed(&self, passwd: &str) -> Option<Seed> {
148        self.mnemonic.as_ref().map(|phrase| phrase.to_seed(passwd))
149    }
150}
151
152#[cfg(test)]
153mod tst {
154    use super::*;
155
156    #[test]
157    pub fn tst_gen_mnemonic() -> Result<(), TerraRustAPIError> {
158        // this test just makes sure the default will call it.
159        let s = Secp256k1::new();
160        PrivateKey::new(&s).map(|_| ())
161    }
162    #[test]
163    pub fn tst_words() -> anyhow::Result<()> {
164        let str_1 = "notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius";
165        let seed_1 = "a2ae8846397b55d266af35acdbb18ba1d005f7ddbdd4ca7a804df83352eaf373f274ba0dc8ac1b2b25f19dfcb7fa8b30a240d2c6039d88963defc2f626003b2f";
166        let s = Secp256k1::new();
167        let pk = PrivateKey::from_words(&s, str_1, 0, 0)?;
168        assert_eq!(hex::encode(pk.seed("").unwrap().as_bytes()), seed_1);
169        match pk.words() {
170            Some(words) => {
171                assert_eq!(words, str_1);
172                Ok(())
173            }
174            None => Err(TerraRustAPIError::MissingPhrase.into()),
175        }
176    }
177    #[test]
178    pub fn tst_root_priv_key() -> anyhow::Result<()> {
179        let str_1 = "wonder caution square unveil april art add hover spend smile proud admit modify old copper throw crew happy nature luggage reopen exhibit ordinary napkin";
180        let secp = Secp256k1::new();
181        let pk = PrivateKey::from_words(&secp, str_1, 0, 0)?;
182        let root_key = "xprv9s21ZrQH143K2ep3BpYRRMjSqjLHZAPAzxfVVS3NBuGKBVtCrK3C8mE8TcmTjYnLm7SJxdLigDFWGAMnctKxc3p5QKNWXdprcFSQzGzQqTW";
183        assert_eq!(pk.root_private_key.to_string(), root_key);
184
185        let derived_key = "4804e2bdce36d413206ccf47cc4c64db2eff924e7cc9e90339fa7579d2bd9d5b";
186        assert_eq!(pk.private_key.private_key.key.to_string(), derived_key);
187
188        Ok(())
189    }
190    #[test]
191    pub fn tst_words_to_pub() -> anyhow::Result<()> {
192        let str_1 = "wonder caution square unveil april art add hover spend smile proud admit modify old copper throw crew happy nature luggage reopen exhibit ordinary napkin";
193        let secp = Secp256k1::new();
194        let pk = PrivateKey::from_words(&secp, str_1, 0, 0)?;
195        let pub_k = pk.public_key(&secp);
196
197        let account = pub_k.account()?;
198        assert_eq!(&account, "terra1jnzv225hwl3uxc5wtnlgr8mwy6nlt0vztv3qqm");
199        assert_eq!(
200            &pub_k.operator_address_public_key()?,
201            "terravaloperpub1addwnpepqt8ha594svjn3nvfk4ggfn5n8xd3sm3cz6ztxyugwcuqzsuuhhfq5y7accr"
202        );
203        assert_eq!(
204            &pub_k.application_public_key()?,
205            "terrapub1addwnpepqt8ha594svjn3nvfk4ggfn5n8xd3sm3cz6ztxyugwcuqzsuuhhfq5nwzrf9"
206        );
207
208        Ok(())
209    }
210    #[test]
211    pub fn test_sign() -> anyhow::Result<()> {
212        // This test is using message from python SDK.. so these keys generate same sigs as they do.
213        let str_1 =  "island relax shop such yellow opinion find know caught erode blue dolphin behind coach tattoo light focus snake common size analyst imitate employ walnut";
214        let secp = Secp256k1::new();
215        let pk = PrivateKey::from_words(&secp, str_1, 0, 0)?;
216        let _pub_k = pk.public_key(&secp);
217        let to_sign = r#"{"account_number":"45","chain_id":"columbus-3-testnet","fee":{"amount":[{"amount":"698","denom":"uluna"}],"gas":"46467"},"memo":"","msgs":[{"type":"bank/MsgSend","value":{"amount":[{"amount":"100000000","denom":"uluna"}],"from_address":"terra1n3g37dsdlv7ryqftlkef8mhgqj4ny7p8v78lg7","to_address":"terra1wg2mlrxdmnnkkykgqg4znky86nyrtc45q336yv"}}],"sequence":"0"}"#;
218
219        let sig = pk.sign(&secp, to_sign)?;
220
221        assert_eq!(
222            sig.pub_key.value,
223            "AiMzHaA2bvnDXfHzkjMM+vkSE/p0ymBtAFKUnUtQAeXe"
224        );
225        assert_eq!(sig.signature, "FJKAXRxNB5ruqukhVqZf3S/muZEUmZD10fVmWycdVIxVWiCXXFsUy2VY2jINEOUGNwfrqEZsT2dUfAvWj8obLg==");
226
227        Ok(())
228    }
229}