Skip to main content

nym_validator_client/signing/
direct_wallet.rs

1// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::signing::signer::{OfflineSigner, SigningError};
5use crate::signing::{
6    derive_extended_private_key, derive_keypair, AccountData, Secp256k1Derivation, Secp256k1Keypair,
7};
8use bip32::XPrv;
9use cosmrs::bip32::DerivationPath;
10use cosmrs::tx;
11use cosmrs::tx::SignDoc;
12use nym_config::defaults;
13use std::borrow::Cow;
14use thiserror::Error;
15use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
16
17#[derive(Debug, Error)]
18pub enum DirectSecp256k1HdWalletError {
19    #[error(transparent)]
20    SigningFailure(#[from] SigningError),
21
22    #[error("failed to derive child key: {source}")]
23    Bip32KeyDerivationFailure {
24        #[from]
25        source: bip32::Error,
26    },
27
28    #[error("There was an issue with bip39: {source}")]
29    Bip39Error {
30        #[from]
31        source: bip39::Error,
32    },
33
34    #[error("failed to derive accounts: {source}")]
35    AccountDerivationError { source: eyre::Report },
36}
37
38// TODO: maybe lock this one behind feature flag?
39#[derive(Zeroize, ZeroizeOnDrop)]
40pub struct DirectSecp256k1HdWallet {
41    /// Base secret
42    secret: bip39::Mnemonic,
43
44    /// Derived accounts
45    #[zeroize(skip)]
46    // unfortunately `dyn EcdsaSigner` does not guarantee Zeroize
47    accounts: Vec<AccountData>,
48}
49
50impl OfflineSigner for DirectSecp256k1HdWallet {
51    type Error = DirectSecp256k1HdWalletError;
52
53    fn get_accounts(&self) -> &[AccountData] {
54        &self.accounts
55    }
56
57    fn sign_direct_with_account(
58        &self,
59        signer: &AccountData,
60        sign_doc: SignDoc,
61    ) -> Result<tx::Raw, Self::Error> {
62        sign_doc
63            .sign(&signer.private_key)
64            .map_err(|source| SigningError::SigningFailure { source }.into())
65    }
66}
67
68impl DirectSecp256k1HdWallet {
69    pub fn builder(prefix: &str) -> DirectSecp256k1HdWalletBuilder {
70        DirectSecp256k1HdWalletBuilder::new(prefix)
71    }
72
73    /// Restores a wallet from the given BIP39 mnemonic using default options.
74    #[deprecated(
75        note = "this function can potentially panic if accounts can't be derived correctly. please use .checked_from_mnemonic() instead"
76    )]
77    pub fn from_mnemonic(prefix: &str, mnemonic: bip39::Mnemonic) -> Self {
78        // unfortunately due to backwards compatibility requirements,
79        // we can't change signature of this method
80        #[allow(deprecated)]
81        DirectSecp256k1HdWalletBuilder::new(prefix).build(mnemonic)
82    }
83
84    /// Restores a wallet from the given BIP39 mnemonic using default options.
85    pub fn checked_from_mnemonic(
86        prefix: &str,
87        mnemonic: bip39::Mnemonic,
88    ) -> Result<Self, DirectSecp256k1HdWalletError> {
89        DirectSecp256k1HdWalletBuilder::new(prefix).try_build(mnemonic)
90    }
91
92    pub fn generate(prefix: &str, word_count: usize) -> Result<Self, DirectSecp256k1HdWalletError> {
93        let mneomonic = bip39::Mnemonic::generate(word_count)?;
94        Self::checked_from_mnemonic(prefix, mneomonic)
95    }
96
97    pub fn secret(&self) -> &bip39::Mnemonic {
98        &self.secret
99    }
100
101    #[deprecated(
102        note = "use either .secret() for obtaining &bip39::Mnemonic or .mnemonic_string() for Zeroizing wrapper around the String"
103    )]
104    pub fn mnemonic(&self) -> String {
105        self.secret.to_string()
106    }
107
108    pub fn mnemonic_string(&self) -> Zeroizing<String> {
109        Zeroizing::new(self.secret.to_string())
110    }
111
112    pub fn account_seed<'a, P: Into<Cow<'a, str>>>(
113        &self,
114        bip39_password: P,
115    ) -> Zeroizing<[u8; 64]> {
116        Zeroizing::new(self.secret.to_seed(bip39_password))
117    }
118
119    /// Derive an extended private key from the stored account secret assuming no bip39 password
120    #[deprecated(
121        note = "use derive_extended_private_key_with_password to ensure correct derivation if used bip39 password"
122    )]
123    pub fn derive_extended_private_key(
124        &self,
125        hd_path: &DerivationPath,
126    ) -> Result<XPrv, DirectSecp256k1HdWalletError> {
127        let seed = self.account_seed("");
128        derive_extended_private_key(seed, hd_path)
129    }
130
131    pub fn derive_keypair<'a, P: Into<Cow<'a, str>>>(
132        &self,
133        hd_path: &DerivationPath,
134        bip39_password: P,
135    ) -> Result<Secp256k1Keypair, DirectSecp256k1HdWalletError> {
136        let seed = self.account_seed(bip39_password);
137        derive_keypair(seed, hd_path)
138    }
139
140    pub fn derive_extended_private_key_with_password<'a, P: Into<Cow<'a, str>>>(
141        &self,
142        hd_path: &DerivationPath,
143        bip39_password: P,
144    ) -> Result<XPrv, DirectSecp256k1HdWalletError> {
145        let seed = self.account_seed(bip39_password);
146        derive_extended_private_key(seed, hd_path)
147    }
148}
149
150#[must_use]
151#[derive(Zeroize)]
152#[zeroize(drop)]
153pub struct DirectSecp256k1HdWalletBuilder {
154    /// The password to use when deriving a BIP39 seed from a mnemonic.
155    bip39_password: String,
156
157    /// The BIP-32/SLIP-10 derivation paths
158    #[zeroize(skip)]
159    hd_paths: Vec<DerivationPath>,
160
161    /// The bech32 address prefix (human readable part)
162    prefix: String,
163}
164
165impl DirectSecp256k1HdWalletBuilder {
166    pub fn new(prefix: &str) -> Self {
167        DirectSecp256k1HdWalletBuilder {
168            bip39_password: String::new(),
169            hd_paths: vec![defaults::COSMOS_DERIVATION_PATH.parse().unwrap()],
170            prefix: prefix.into(),
171        }
172    }
173
174    pub fn with_bip39_password<S: Into<String>>(mut self, password: S) -> Self {
175        self.bip39_password = password.into();
176        self
177    }
178
179    pub fn with_hd_path(mut self, path: DerivationPath) -> Self {
180        self.hd_paths.push(path);
181        self
182    }
183
184    pub fn with_hd_paths(mut self, hd_paths: Vec<DerivationPath>) -> Self {
185        self.hd_paths = hd_paths;
186        self
187    }
188
189    pub fn with_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
190        self.prefix = prefix.into();
191        self
192    }
193
194    #[deprecated(
195        note = "this function can potentially panic if accounts can't be derived correctly. please use .try_build() instead"
196    )]
197    pub fn build(self, mnemonic: bip39::Mnemonic) -> DirectSecp256k1HdWallet {
198        // unfortunately due to backwards compatibility requirements,
199        // we can't change signature of this method
200        #[allow(clippy::expect_used)]
201        self.try_build(mnemonic)
202            .expect("account derivation failure")
203    }
204
205    pub fn try_build(
206        self,
207        mnemonic: bip39::Mnemonic,
208    ) -> Result<DirectSecp256k1HdWallet, DirectSecp256k1HdWalletError> {
209        let seed = Zeroizing::new(mnemonic.to_seed(&self.bip39_password));
210        let prefix = self.prefix.clone();
211        let accounts = self
212            .hd_paths
213            .iter()
214            .map(|hd_path| {
215                Secp256k1Derivation {
216                    hd_path: hd_path.clone(),
217                    prefix: prefix.clone(),
218                }
219                .try_derive_account(&seed)
220            })
221            .collect::<Result<_, _>>()?;
222
223        Ok(DirectSecp256k1HdWallet {
224            accounts,
225            secret: mnemonic,
226        })
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use nym_network_defaults::NymNetworkDetails;
233
234    use super::*;
235
236    #[test]
237    fn generating_account_addresses() -> anyhow::Result<()> {
238        // test vectors produced from our js wallet
239        let mnemonics = ["crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove",
240            "acquire rebel spot skin gun such erupt pull swear must define ill chief turtle today flower chunk truth battle claw rigid detail gym feel",
241            "step income throw wheat mobile ship wave drink pool sudden upset jaguar bar globe rifle spice frost bless glimpse size regular carry aspect ball"];
242        let prefix = NymNetworkDetails::new_mainnet()
243            .chain_details
244            .bech32_account_prefix;
245
246        let addrs = [
247            "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
248            "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es",
249            "n17n9flp6jflljg6fp05dsy07wcprf2uuu8g40rf",
250        ];
251        for (idx, mnemonic) in mnemonics.iter().enumerate() {
252            let wallet =
253                DirectSecp256k1HdWallet::checked_from_mnemonic(&prefix, mnemonic.parse()?)?;
254            assert_eq!(wallet.signer_addresses()[0], addrs[idx].parse().unwrap());
255        }
256        Ok(())
257    }
258}