From c32393a4f344877e34957d13a768e6623486c587 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 28 Apr 2021 17:47:52 +0200 Subject: [PATCH] Add `--coin-type` option to `keys restore` command (#856) * fix hardcoded coin-type * Introduce CoinType type * Make `coin_type` field in key file optional, default to Atom * Properly handle invalid HD path format Co-authored-by: allthatjazzleo --- Cargo.lock | 21 ++++---- relayer-cli/Cargo.toml | 2 +- relayer-cli/src/commands/keys/restore.rs | 23 +++++++-- relayer/src/keyring.rs | 66 ++++++++++++++++++++++-- relayer/src/keyring/errors.rs | 3 ++ 5 files changed, 94 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 46970d72e..0d63cbaa8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,11 +173,12 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "backtrace" -version = "0.3.57" +version = "0.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ed203b9ba68b242c62b3fb7480f589dd49829be1edb3fe8fc8b4ffda2dcb8d" +checksum = "88fb5a785d6b44fd9d6700935608639af1b8356de1e55d5f7c2740f4faa15d82" dependencies = [ "addr2line", + "cc", "cfg-if 1.0.0", "libc", "miniz_oxide", @@ -1112,9 +1113,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f006b8784cfb01fe7aa9c46f5f5cd4cf5c85a8c612a0653ec97642979062665" +checksum = "1e5f105c494081baa3bf9e200b279e27ec1623895cd504c7dbef8d0b080fcf54" dependencies = [ "bytes", "futures-channel", @@ -1434,9 +1435,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.93" +version = "0.2.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" +checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" [[package]] name = "lock_api" @@ -2452,9 +2453,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883" +checksum = "ad184cc9470f9117b2ac6817bfe297307418819ba40552f9b3846f05c33d5373" dependencies = [ "proc-macro2", "quote", @@ -2820,9 +2821,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f715efe02c0862926eb463e49368d38ddb119383475686178e32e26d15d06a66" +checksum = "bf0aa6dfc29148c3826708dabbfa83c121eeb84df4d1468220825e3a33651687" dependencies = [ "futures-core", "futures-util", diff --git a/relayer-cli/Cargo.toml b/relayer-cli/Cargo.toml index 1d58af1cf..f34140072 100644 --- a/relayer-cli/Cargo.toml +++ b/relayer-cli/Cargo.toml @@ -25,7 +25,7 @@ ibc-relayer = { version = "0.2.0", path = "../relayer" } ibc-proto = { version = "0.8.0", path = "../proto" } anomaly = "0.2.0" -gumdrop = "0.7" +gumdrop = { version = "0.7", features = ["default_expr"] } serde = { version = "1", features = ["serde_derive"] } thiserror = "1" tokio = { version = "1.0", features = ["full"] } diff --git a/relayer-cli/src/commands/keys/restore.rs b/relayer-cli/src/commands/keys/restore.rs index ef0562998..c7bdce38c 100644 --- a/relayer-cli/src/commands/keys/restore.rs +++ b/relayer-cli/src/commands/keys/restore.rs @@ -1,10 +1,10 @@ use abscissa_core::{Command, Options, Runnable}; - use anomaly::BoxError; + use ibc::ics24_host::identifier::ChainId; use ibc_relayer::{ config::{ChainConfig, Config}, - keyring::{KeyEntry, KeyRing, Store}, + keyring::{CoinType, KeyEntry, KeyRing, Store}, }; use crate::application::app_config; @@ -17,12 +17,20 @@ pub struct KeyRestoreCmd { #[options(short = "m", required, help = "mnemonic to restore the key from")] mnemonic: String, + + #[options( + short = "t", + help = "coin type of the key to restore, default: 118 (Atom)", + default_expr = "CoinType::ATOM" + )] + coin_type: CoinType, } #[derive(Clone, Debug)] pub struct KeysRestoreOptions { pub mnemonic: String, pub config: ChainConfig, + pub coin_type: CoinType, } impl KeyRestoreCmd { @@ -34,6 +42,7 @@ impl KeyRestoreCmd { Ok(KeysRestoreOptions { mnemonic: self.mnemonic.clone(), config: chain_config.clone(), + coin_type: self.coin_type, }) } } @@ -49,7 +58,7 @@ impl Runnable for KeyRestoreCmd { let key_name = opts.config.key_name.clone(); let chain_id = opts.config.id.clone(); - let key = restore_key(&opts.mnemonic, opts.config); + let key = restore_key(&opts.mnemonic, opts.coin_type, opts.config); match key { Ok(key) => Output::success_msg(format!( @@ -62,9 +71,13 @@ impl Runnable for KeyRestoreCmd { } } -pub fn restore_key(mnemonic: &str, config: ChainConfig) -> Result { +pub fn restore_key( + mnemonic: &str, + coin_type: CoinType, + config: ChainConfig, +) -> Result { let mut keyring = KeyRing::new(Store::Test, config)?; - let key_entry = keyring.key_from_mnemonic(mnemonic)?; + let key_entry = keyring.key_from_mnemonic(mnemonic, coin_type)?; keyring.add_key(key_entry.clone())?; Ok(key_entry) diff --git a/relayer/src/keyring.rs b/relayer/src/keyring.rs index b7a919220..db6e4a91b 100644 --- a/relayer/src/keyring.rs +++ b/relayer/src/keyring.rs @@ -3,6 +3,7 @@ pub mod errors; use std::convert::{TryFrom, TryInto}; use std::fs::{self, File}; use std::path::{Path, PathBuf}; +use std::str::FromStr; use bech32::{ToBase32, Variant}; use bip39::{Language, Mnemonic, Seed}; @@ -25,6 +26,42 @@ pub const KEYSTORE_DEFAULT_FOLDER: &str = ".hermes/keys/"; pub const KEYSTORE_DISK_BACKEND: &str = "keyring-test"; // TODO: Change to "keyring" pub const KEYSTORE_FILE_EXTENSION: &str = "json"; +/// [Coin type][coin-type] associated with a key. +/// +/// See [the list of all registered coin types][coin-types]. +/// +/// [coin-type]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#Coin_type +/// [coin-types]: https://github.com/satoshilabs/slips/blob/master/slip-0044.md +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct CoinType(u32); + +impl CoinType { + /// Atom (Cosmos) coin type with number 118. + pub const ATOM: CoinType = CoinType(118); + + pub fn new(coin_type: u32) -> Self { + Self(coin_type) + } + + pub fn num(&self) -> u32 { + self.0 + } +} + +impl Default for CoinType { + fn default() -> Self { + Self::ATOM + } +} + +impl FromStr for CoinType { + type Err = std::num::ParseIntError; + + fn from_str(s: &str) -> Result { + s.parse::().map(Self::new) + } +} + /// Key entry stores the Private Key and Public Key as well the address #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct KeyEntry { @@ -39,6 +76,9 @@ pub struct KeyEntry { /// Address pub address: Vec, + + /// Coin type + pub coin_type: CoinType, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -48,6 +88,7 @@ pub struct KeyFile { pub address: String, pub pubkey: String, pub mnemonic: String, + pub coin_type: Option, } impl TryFrom for KeyEntry { @@ -60,8 +101,11 @@ impl TryFrom for KeyEntry { // Decode the Bech32-encoded public key from the key file let mut keyfile_pubkey_bytes = decode_bech32(&key_file.pubkey)?; + // Use coin type if present or default coin type (ie. Atom). + let coin_type = key_file.coin_type.unwrap_or_default(); + // Decode the private key from the mnemonic - let private_key = private_key_from_mnemonic(&key_file.mnemonic)?; + let private_key = private_key_from_mnemonic(&key_file.mnemonic, coin_type)?; let public_key = ExtendedPubKey::from_private(&Secp256k1::new(), &private_key); let public_key_bytes = public_key.public_key.to_bytes(); @@ -88,6 +132,7 @@ impl TryFrom for KeyEntry { private_key, account: key_file.address, address: keyfile_address_bytes, + coin_type, }) } } @@ -238,9 +283,13 @@ impl KeyRing { } /// Add a key entry in the store using a mnemonic. - pub fn key_from_mnemonic(&self, mnemonic_words: &str) -> Result { + pub fn key_from_mnemonic( + &self, + mnemonic_words: &str, + coin_type: CoinType, + ) -> Result { // Get the private key from the mnemonic - let private_key = private_key_from_mnemonic(mnemonic_words)?; + let private_key = private_key_from_mnemonic(mnemonic_words, coin_type)?; // Get the public Key from the private key let public_key = ExtendedPubKey::from_private(&Secp256k1::new(), &private_key); @@ -257,6 +306,7 @@ impl KeyRing { private_key, address, account, + coin_type, }) } @@ -282,14 +332,20 @@ impl KeyRing { } /// Decode an extended private key from a mnemonic -fn private_key_from_mnemonic(mnemonic_words: &str) -> Result { +fn private_key_from_mnemonic( + mnemonic_words: &str, + coin_type: CoinType, +) -> Result { let mnemonic = Mnemonic::from_phrase(mnemonic_words, Language::English) .map_err(|e| Kind::InvalidMnemonic.context(e))?; let seed = Seed::new(&mnemonic, ""); // Get Private Key from seed and standard derivation path - let hd_path = StandardHDPath::try_from("m/44'/118'/0'/0/0").unwrap(); + let hd_path_format = format!("m/44'/{}'/0'/0/0", coin_type.num()); + let hd_path = StandardHDPath::try_from(hd_path_format.as_str()) + .map_err(|_| Kind::InvalidHdPath(hd_path_format))?; + let private_key = ExtendedPrivKey::new_master(Network::Bitcoin, seed.as_bytes()) .and_then(|k| k.derive_priv(&Secp256k1::new(), &DerivationPath::from(hd_path))) .map_err(|e| Kind::PrivateKey.context(e))?; diff --git a/relayer/src/keyring/errors.rs b/relayer/src/keyring/errors.rs index 4aaf7801f..9e8103ce7 100644 --- a/relayer/src/keyring/errors.rs +++ b/relayer/src/keyring/errors.rs @@ -31,6 +31,9 @@ pub enum Kind { #[error("key store error")] KeyStore, + + #[error("invalid HD path: {0}")] + InvalidHdPath(String), } impl Kind {