From 36d4096824a467cbe288291d7a367de7f751101e Mon Sep 17 00:00:00 2001 From: Andy Nogueira Date: Fri, 30 Oct 2020 09:46:10 -0400 Subject: [PATCH] Transaction signing (#345) * Initial implementation for tx sign * Adding logic to build the tx as part of the chain. Added broadcast tx to chain. #47 * Added keys restore command boileplate for abscissa. Restore key functionality not implemented yet #47 * Implemented changes to support tx signing (#47): * Implemented very basic keybase to store keys (memory store) * Logic to restore key (private/public) from mnemonic * Added keystore to the chain * Implemented working 'keys restore' command on the relayer * Refactoring keybase structure (#47) * Initial logic to send message (#47) * Got the logic to sign but MsgConnectionOpenInit test against stargate-4 not working (#47) * MsgConnectionOpenInit tx working logic! (#47) * Added option to tx raw conn-init to specify and read key file content (#47) * Logic to parse the key_seed.json file passed as tx raw parameter working (#47) * Added support to specify key file and account sequence for the tax raw conn-init command (#47) * Adding instructions on how to submit a transaction (#345) * Fixing format issues (#345) * Fixing tests (#345) * Update relayer/src/tx/client.rs I had this in place so I could test. OK to change it back Co-authored-by: Anca Zamfir * Update relayer/src/tx/connection.rs OK, didn't know where to get it from, thanks. Co-authored-by: Anca Zamfir Co-authored-by: Anca Zamfir --- relayer-cli/Cargo.toml | 1 + relayer-cli/README.md | 88 ++++++- relayer-cli/src/commands.rs | 9 +- relayer-cli/src/commands/keys.rs | 16 ++ relayer-cli/src/commands/keys/restore.rs | 74 ++++++ relayer-cli/src/commands/tx/client.rs | 23 +- relayer-cli/src/commands/tx/connection.rs | 40 +++- relayer-cli/src/error.rs | 4 + relayer/Cargo.toml | 9 +- relayer/src/chain.rs | 17 +- relayer/src/chain/cosmos.rs | 273 ++++++++++++---------- relayer/src/error.rs | 4 + relayer/src/keyring.rs | 2 + relayer/src/keyring/errors.rs | 25 ++ relayer/src/keyring/store.rs | 189 +++++++++++++++ relayer/src/keys.rs | 1 + relayer/src/keys/restore.rs | 24 ++ relayer/src/lib.rs | 2 + relayer/src/tx/client.rs | 40 +++- relayer/src/tx/connection.rs | 57 +++-- 20 files changed, 721 insertions(+), 177 deletions(-) create mode 100644 relayer-cli/src/commands/keys.rs create mode 100644 relayer-cli/src/commands/keys/restore.rs create mode 100644 relayer/src/keyring.rs create mode 100644 relayer/src/keyring/errors.rs create mode 100644 relayer/src/keyring/store.rs create mode 100644 relayer/src/keys.rs create mode 100644 relayer/src/keys/restore.rs diff --git a/relayer-cli/Cargo.toml b/relayer-cli/Cargo.toml index 6158332ab8..886bbcb70e 100644 --- a/relayer-cli/Cargo.toml +++ b/relayer-cli/Cargo.toml @@ -25,6 +25,7 @@ serde_derive = "1.0.116" sled = "0.34.4" prost = "0.6.1" prost-types = { version = "0.6.1" } +hex = "0.4" [dependencies.tendermint] version = "0.17.0-rc1" diff --git a/relayer-cli/README.md b/relayer-cli/README.md index f46b8b372a..1a6536c811 100644 --- a/relayer-cli/README.md +++ b/relayer-cli/README.md @@ -1,14 +1,88 @@ -# relayer-cli +# Relayer (Rust) -`relayer-cli` is an application. +This is the repository for the IBC Relayer built in Rust. + +For updates please check the [releases on the ibc-rs repository](https://github.com/informalsystems/ibc-rs/releases) ## Getting Started -This application is authored using [Abscissa], a Rust application framework. +In order to run the Relayer please ensure you have [Rust installed on your machine](https://www.rust-lang.org/tools/install) + +### Submitting an IBC transaction + +The `tx raw conn-init` command works now. Signing the message is working and the gaia chain (stargate-4) accepts the transaction. + +The command accepts two parameters that allows you to send a transaction: + +* **signer-key** (-k) -> specify a key file (name and location) that will be used by the signer. This key seed file has a mnemonic (seed phrase) that can be used to retrieve the private key (BIP-39) used to sign the transaction. +* **account_sequence** (-s) -> this is the account sequence value, basically every time a tx is committed by the account this number increases. + +#### Steps to testing the transaction: + +* Start two chains using the `dev-env` script from the [ovrclk/relayer](https://github.com/ovrclk/relayer) (make sure to checkout stargate-4 version) +* After you run the script, the Go relayer will create a `data` folder for the chains. Open the key seed file `./data/ibc1/key_seed.json` for chain `ibc-1` and look for the account value + + + { + "name":"user", + "type":"local", + "address":"cosmos1tqzwwr5hrnq2ceg5fg52m720m50xpfy08at7l9", + "pubkey":"cosmospub1addwnpepq08wntxejcla5hd93stgudw02htdpa9vu5a2ds8xkvmgrkrrpwlj6sdhkz6", + "mnemonic":"[MNEMONIC WORDS"} + } + + +* In order to find the account sequence run the command below: + + For the address value after `gaiad query account` use the `address` from the step above. + + `$ gaiad query account cosmos1tqzwwr5hrnq2ceg5fg52m720m50xpfy08at7l9 --home ./data/ibc1 --chain-id ibc1 --node tcp://localhost:26557` + + This will return a JSON with a sequence number at the end. Make a note of that, you will need this number as an argument to the transaction command. + + + '@type': /cosmos.auth.v1beta1.BaseAccount + account_number: "0" + address: cosmos1tqzwwr5hrnq2ceg5fg52m720m50xpfy08at7l9 + pub_key: + '@type': /cosmos.crypto.secp256k1.PubKey + key: A87prNmWP9pdpYwWjjXPVdbQ9KzlOqbA5rM2gdhjC78t + sequence: "12" + + +* Run the transaction command. This will try to initialize an `ibczeroconn2` connection on chain `ibc1` + + `$ cargo run --bin relayer -- -c ./relayer-cli/tests/fixtures/two_chains.toml tx raw conn-init ibc0 ibc1 ibczeroclient ibconeclient ibczeroconn2 ibconeconn -s 12 -k key_seed.json` + + If you get an empty response it means the tx worked + + `conn init, result: []` + +* Check if the connection was created on `ibc-1` using the Golang relayer + + `$ rly query connections ibc1 | jq .` + + If you see an entry in the JSON file that points to the `ibczeroconn2` connection with state `STATE_INIT` it confirms that the transaction worked: + + + { + "id": "ibczeroconn21", + "client_id": "ibczeroclient", + "versions": [ + "\n\u00011\u0012\rORDER_ORDERED\u0012\u000fORDER_UNORDERED" + ], + "state": "STATE_INIT", + "counterparty": { + "client_id": "ibconeclient", + "connection_id": "ibconeconn", + "prefix": { + "key_prefix": "aWJj" + } + } + }, + + + -For more information, see: -[Documentation] -[Abscissa]: https://github.com/iqlusioninc/abscissa -[Documentation]: https://docs.rs/abscissa_core/ diff --git a/relayer-cli/src/commands.rs b/relayer-cli/src/commands.rs index 639952f0a2..b703a14606 100644 --- a/relayer-cli/src/commands.rs +++ b/relayer-cli/src/commands.rs @@ -6,6 +6,7 @@ //! application's configuration file. mod config; +mod keys; mod light; mod listen; mod query; @@ -15,8 +16,8 @@ mod v0; mod version; use self::{ - config::ConfigCmd, light::LightCmd, listen::ListenCmd, query::QueryCmd, start::StartCmd, - tx::TxCmd, v0::V0Cmd, version::VersionCmd, + config::ConfigCmd, keys::KeysCmd, light::LightCmd, listen::ListenCmd, query::QueryCmd, + start::StartCmd, tx::TxCmd, v0::V0Cmd, version::VersionCmd, }; use crate::config::Config; @@ -64,6 +65,10 @@ pub enum CliCmd { /// The `light` subcommand #[options(help = "basic functionality for managing the lite clients")] Light(LightCmd), + + /// The `keys` subcommand + #[options(help = "manage keys in the relayer for each chain")] + Keys(KeysCmd), } /// This trait allows you to define how application configuration is loaded. diff --git a/relayer-cli/src/commands/keys.rs b/relayer-cli/src/commands/keys.rs new file mode 100644 index 0000000000..001dad462c --- /dev/null +++ b/relayer-cli/src/commands/keys.rs @@ -0,0 +1,16 @@ +//! `keys` subcommand +use abscissa_core::{Command, Help, Options, Runnable}; + +mod restore; + +/// `keys` subcommand +#[derive(Command, Debug, Options, Runnable)] +pub enum KeysCmd { + /// The `help` subcommand + #[options(help = "get usage information")] + Help(Help), + + /// The `keys restore` subcommand + #[options(help = "keys restore")] + Restore(restore::KeyRestoreCmd), +} diff --git a/relayer-cli/src/commands/keys/restore.rs b/relayer-cli/src/commands/keys/restore.rs new file mode 100644 index 0000000000..6d9d69ba72 --- /dev/null +++ b/relayer-cli/src/commands/keys/restore.rs @@ -0,0 +1,74 @@ +use crate::application::app_config; +use abscissa_core::{Command, Options, Runnable}; +use relayer::config::Config; + +use crate::error::{Error, Kind}; +use crate::prelude::*; +use relayer::keys::restore::{restore_key, KeysRestoreOptions}; + +#[derive(Clone, Command, Debug, Options)] +pub struct KeyRestoreCmd { + #[options(free, help = "identifier of the chain")] + chain_id: Option, + + #[options(free, help = "the key name")] + name: Option, + + #[options(free, help = "mnemonic to add key")] + mnemonic: Option, +} + +impl KeyRestoreCmd { + fn validate_options(&self, config: &Config) -> Result { + let chain_id = self + .chain_id + .clone() + .ok_or_else(|| "missing chain identifier".to_string())?; + + let chain_config = config + .chains + .iter() + .find(|c| c.id == chain_id.parse().unwrap()) + .ok_or_else(|| { + "Invalid chain identifier. Cannot retrieve the chain configuration".to_string() + })?; + + let key_name = self + .name + .clone() + .ok_or_else(|| "missing key name".to_string())?; + + let mnemonic_words = self + .mnemonic + .clone() + .ok_or_else(|| "missing mnemonic".to_string())?; + + Ok(KeysRestoreOptions { + name: key_name, + mnemonic: mnemonic_words, + chain_config: chain_config.clone(), + }) + } +} + +impl Runnable for KeyRestoreCmd { + fn run(&self) { + let config = app_config(); + + let opts = match self.validate_options(&config) { + Err(err) => { + status_err!("invalid options: {}", err); + return; + } + Ok(result) => result, + }; + + let res: Result, Error> = + restore_key(opts).map_err(|e| Kind::Keys.context(e).into()); + + match res { + Ok(r) => status_info!("key restore result: ", "{:?}", hex::encode(r)), + Err(e) => status_info!("key restore failed: ", "{}", e), + } + } +} diff --git a/relayer-cli/src/commands/tx/client.rs b/relayer-cli/src/commands/tx/client.rs index 9f3fdf77f6..af5ed11c62 100644 --- a/relayer-cli/src/commands/tx/client.rs +++ b/relayer-cli/src/commands/tx/client.rs @@ -5,6 +5,8 @@ use relayer::tx::client::{create_client, CreateClientOptions}; use crate::application::app_config; use crate::error::{Error, Kind}; use crate::prelude::*; +use std::fs; +use std::path::Path; #[derive(Clone, Command, Debug, Options)] pub struct TxCreateClientCmd { @@ -19,10 +21,27 @@ pub struct TxCreateClientCmd { help = "identifier of the client to be created on destination chain" )] dest_client_id: Option, + + #[options(free, help = "key file for the signer")] + signer_key: Option, } impl TxCreateClientCmd { fn validate_options(&self, config: &Config) -> Result { + // Get content of key seed file + let key_filename = self + .signer_key + .clone() + .ok_or_else(|| "missing signer key file".to_string())?; + + let key_file = Path::new(&key_filename).exists(); + if !key_file { + return Err("cannot find key file specified".to_string()); + } + + let key_file_contents = fs::read_to_string(key_filename) + .expect("Something went wrong reading the key seed file"); + let dest_chain_id = self .dest_chain_id .clone() @@ -56,6 +75,7 @@ impl TxCreateClientCmd { dest_client_id, dest_chain_config: dest_chain_config.clone(), src_chain_config: src_chain_config.clone(), + signer_key: key_file_contents, }) } } @@ -73,7 +93,8 @@ impl Runnable for TxCreateClientCmd { }; status_info!("Message", "{:?}", opts); - let res: Result<(), Error> = create_client(opts).map_err(|e| Kind::Tx.context(e).into()); + let res: Result, Error> = + create_client(opts).map_err(|e| Kind::Tx.context(e).into()); match res { Ok(receipt) => status_info!("client created, result: ", "{:?}", receipt), diff --git a/relayer-cli/src/commands/tx/connection.rs b/relayer-cli/src/commands/tx/connection.rs index 1b3d316e24..91e41a0131 100644 --- a/relayer-cli/src/commands/tx/connection.rs +++ b/relayer-cli/src/commands/tx/connection.rs @@ -4,6 +4,8 @@ use crate::error::{Error, Kind}; use abscissa_core::{Command, Options, Runnable}; use relayer::config::Config; use relayer::tx::connection::{conn_init, ConnectionOpenInitOptions}; +use std::fs; +use std::path::Path; #[derive(Clone, Command, Debug, Options)] pub struct TxRawConnInitCmd { @@ -22,12 +24,44 @@ pub struct TxRawConnInitCmd { #[options(free, help = "identifier of the source connection")] src_connection_id: Option, - #[options(help = "identifier of the destination connection", short = "d")] + #[options(free, help = "identifier of the destination connection")] dest_connection_id: Option, + + #[options(help = "account sequence of the signer", short = "s")] + account_sequence: Option, + + #[options(help = "key file for the signer", short = "k")] + signer_key: Option, } impl TxRawConnInitCmd { fn validate_options(&self, config: &Config) -> Result { + // Get the account sequence + let parsed = self + .account_sequence + .clone() + .ok_or_else(|| "missing account sequence".to_string())? + .parse::(); + + let acct_seq = match parsed { + Ok(v) => v, + Err(e) => return Err("invalid account sequence number".to_string()), + }; + + // Get content of key seed file + let key_filename = self + .signer_key + .clone() + .ok_or_else(|| "missing signer key file".to_string())?; + + let key_file = Path::new(&key_filename).exists(); + if !key_file { + return Err("cannot find key file specified".to_string()); + } + + let key_file_contents = fs::read_to_string(key_filename) + .expect("Something went wrong reading the key seed file"); + let src_chain_id = self .src_chain_id .clone() @@ -87,6 +121,8 @@ impl TxRawConnInitCmd { dest_connection_id, src_chain_config: src_chain_config.clone(), dest_chain_config: dest_chain_config.clone(), + signer_key: key_file_contents, + account_sequence: acct_seq, }; Ok(opts) @@ -106,7 +142,7 @@ impl Runnable for TxRawConnInitCmd { }; status_info!("Message", "{:?}", opts); - let res: Result<(), Error> = conn_init(opts).map_err(|e| Kind::Tx.context(e).into()); + let res: Result, Error> = conn_init(opts).map_err(|e| Kind::Tx.context(e).into()); match res { Ok(receipt) => status_info!("conn init, result: ", "{:?}", receipt), diff --git a/relayer-cli/src/error.rs b/relayer-cli/src/error.rs index 1ca66be639..f77b0e2324 100644 --- a/relayer-cli/src/error.rs +++ b/relayer-cli/src/error.rs @@ -24,6 +24,10 @@ pub enum Kind { /// Error during transaction submission #[error("tx error")] Tx, + + /// Error during transaction submission + #[error("keys error")] + Keys, } impl Kind { diff --git a/relayer/Cargo.toml b/relayer/Cargo.toml index d98e7e88b3..d97e80101a 100644 --- a/relayer/Cargo.toml +++ b/relayer/Cargo.toml @@ -8,6 +8,7 @@ authors = [ [dependencies] ibc = { path = "../modules" } +ibc-proto = { version = "0.4.0", path = "../proto" } tendermint-proto = "0.1.0" anomaly = "0.2.0" @@ -28,6 +29,13 @@ prost-types = { version = "0.6.1" } futures = "0.3.5" retry = "1.1.0" crossbeam-channel = "0.5.0" +k256 = { version = "0.5.6", features = ["ecdsa-core", "ecdsa", "sha256"]} +hex = "0.4" +bitcoin = { version= "0.25"} +bitcoin-wallet = "1.1.0" +hdpath = { version = "0.2.0", features = ["with-bitcoin"] } +rust-crypto = "0.2.36" +bech32 = "0.7.2" [dependencies.tendermint] version = "0.17.0-rc1" @@ -40,7 +48,6 @@ features = ["http-client", "websocket-client"] version = "0.17.0-rc1" # Needed for tx sign when ready in tendermint upgrade -#k256 = { version = "0.4", features = ["ecdsa-core", "ecdsa", "sha256"]} #tendermint-rpc = "0.15" #rust-crypto = "0.2.36" #hex = "0.4" diff --git a/relayer/src/chain.rs b/relayer/src/chain.rs index 4132d92d85..57b8b6c8e8 100644 --- a/relayer/src/chain.rs +++ b/relayer/src/chain.rs @@ -12,6 +12,8 @@ use tendermint_rpc::Client as RpcClient; use ibc::ics02_client::state::{ClientState, ConsensusState}; use ibc::ics24_host::Path; +use crate::keyring::store::{KeyEntry, KeyRing}; + use crate::client::LightClient; use crate::config::ChainConfig; use crate::error; @@ -23,6 +25,7 @@ pub(crate) mod cosmos; pub use cosmos::CosmosSDKChain; pub mod handle; +use ibc::tx_msg::Msg; /// Defines a blockchain as understood by the relayer pub trait Chain { @@ -48,7 +51,15 @@ pub trait Chain { fn query(&self, data: Path, height: Height, prove: bool) -> Result, Self::Error>; /// send a transaction with `msgs` to chain. - fn send(&self, _msgs: &[Any]) -> Result<(), Self::Error>; + fn send( + &mut self, + msg_type: String, + msg: Vec, + key: KeyEntry, + acct_seq: u64, + memo: String, + timeout_height: u64, + ) -> Result, Self::Error>; /// Returns the chain's identifier fn id(&self) -> &ChainId { @@ -77,10 +88,6 @@ pub trait Chain { /// The trust threshold configured for this chain fn trust_threshold(&self) -> TrustThreshold; - /// Sign message - /// TODO - waiting for tendermint-rs upgrade to v0.16 - fn sign_tx(&self, _msgs: &[Any]) -> Result, Self::Error>; - /// Query a header at the given height via RPC fn query_header_at_height(&self, height: Height) -> Result; diff --git a/relayer/src/chain/cosmos.rs b/relayer/src/chain/cosmos.rs index 6c4b8d2979..3580d63e71 100644 --- a/relayer/src/chain/cosmos.rs +++ b/relayer/src/chain/cosmos.rs @@ -1,32 +1,41 @@ -use std::future::Future; use std::str::FromStr; use std::time::Duration; -use bytes::Bytes; -use prost::Message; -use prost_types::Any; - -use tendermint::abci::Path as TendermintABCIPath; +use ibc::ics03_connection::msgs::conn_open_init::MsgConnectionOpenInit; +use ibc::ics07_tendermint::client_state::ClientState; +use ibc::ics07_tendermint::consensus_state::ConsensusState; +use ibc::ics24_host::{Path, IBC_QUERY_PATH}; +use tendermint::abci::{Path as TendermintABCIPath, Transaction}; use tendermint::block::Height; use tendermint_light_client::types::{LightBlock, ValidatorSet}; use tendermint_light_client::types::{SignedHeader, TrustThreshold}; use tendermint_rpc::Client; use tendermint_rpc::HttpClient; -use ibc::ics07_tendermint::client_state::ClientState; -use ibc::ics07_tendermint::consensus_state::ConsensusState; -use ibc::ics24_host::{Path, IBC_QUERY_PATH}; - use super::Chain; use crate::client::tendermint::LightClient; use crate::config::ChainConfig; use crate::error::{Error, Kind}; use crate::util::block_on; +use crate::error; +use crate::keyring::store::{KeyEntry, KeyRing, KeyRingOperations, StoreBackend}; +use bytes::Bytes; +use futures::{FutureExt, TryFutureExt}; +use ibc::tx_msg::Msg; +use ibc_proto::cosmos::base::v1beta1::Coin; +use ibc_proto::cosmos::tx::v1beta1::mode_info::{Single, Sum}; +use ibc_proto::cosmos::tx::v1beta1::{AuthInfo, Fee, ModeInfo, SignDoc, SignerInfo, TxBody, TxRaw}; +use k256::ecdsa::{SigningKey, VerifyKey}; +use prost::Message; +use prost_types::Any; +use std::future::Future; + pub struct CosmosSDKChain { config: ChainConfig, rpc_client: HttpClient, light_client: Option, + pub keybase: KeyRing, } impl CosmosSDKChain { @@ -34,8 +43,11 @@ impl CosmosSDKChain { let rpc_client = HttpClient::new(config.rpc_addr.clone()).map_err(|e| Kind::Rpc.context(e))?; + let key_store = KeyRing::init(StoreBackend::Memory); + Ok(Self { config, + keybase: key_store, rpc_client, light_client: None, }) @@ -70,9 +82,111 @@ impl Chain for CosmosSDKChain { } /// Send a transaction that includes the specified messages - fn send(&self, _msgs: &[Any]) -> Result<(), Error> { - // TODO sign and broadcast_tx - Ok(()) + fn send( + &mut self, + msg_type: String, + msg_bytes: Vec, + key: KeyEntry, + acct_seq: u64, + memo: String, + timeout_height: u64, + ) -> Result, Error> { + // Create a proto any message + let mut proto_msgs: Vec = Vec::new(); + + let any_msg = Any { + type_url: msg_type, + value: msg_bytes, + }; + + // Add proto message + proto_msgs.push(any_msg); + + // Create TxBody + let body = TxBody { + messages: proto_msgs.to_vec(), + memo, + timeout_height, + extension_options: Vec::::new(), + non_critical_extension_options: Vec::::new(), + }; + + // A protobuf serialization of a TxBody + let mut body_buf = Vec::new(); + prost::Message::encode(&body, &mut body_buf).unwrap(); + + // let key = self.keybase.get(signer.clone()).map_err(|e| error::Kind::KeyBase.context(e))?; + let pub_key_bytes = key.public_key.public_key.to_bytes(); + + let mut pk_buf = Vec::new(); + prost::Message::encode(&key.public_key.public_key.to_bytes(), &mut pk_buf).unwrap(); + + // Create a MsgSend proto Any message + let pk_any = Any { + type_url: "/cosmos.crypto.secp256k1.PubKey".to_string(), + value: pk_buf, + }; + + let single = Single { mode: 1 }; + let sum_single = Some(Sum::Single(single)); + let mode = Some(ModeInfo { sum: sum_single }); + let signer_info = SignerInfo { + public_key: Some(pk_any), + mode_info: mode, + sequence: acct_seq, + }; + + // Gas Fee + let coin = Coin { + denom: "stake".to_string(), + amount: "1000".to_string(), + }; + + let fee = Some(Fee { + amount: vec![coin], + gas_limit: 100000, + payer: "".to_string(), + granter: "".to_string(), + }); + + let auth_info = AuthInfo { + signer_infos: vec![signer_info], + fee, + }; + + // A protobuf serialization of a AuthInfo + let mut auth_buf = Vec::new(); + prost::Message::encode(&auth_info, &mut auth_buf).unwrap(); + + let sign_doc = SignDoc { + body_bytes: body_buf.clone(), + auth_info_bytes: auth_buf.clone(), + chain_id: self.config.clone().id.to_string(), + account_number: 0, + }; + + // A protobuf serialization of a SignDoc + let mut signdoc_buf = Vec::new(); + prost::Message::encode(&sign_doc, &mut signdoc_buf).unwrap(); + + // Sign doc and broadcast + let signed = self.keybase.sign(key.address, signdoc_buf); + + let tx_raw = TxRaw { + body_bytes: body_buf, + auth_info_bytes: auth_buf, + signatures: vec![signed], + }; + + let mut txraw_buf = Vec::new(); + prost::Message::encode(&tx_raw, &mut txraw_buf).unwrap(); + //println!("TxRAW {:?}", hex::encode(txraw_buf.clone())); + + //let signed = sign(sign_doc); + let response = + block_on(broadcast_tx(self, txraw_buf.clone())).map_err(|e| Kind::Rpc.context(e))?; + + Ok(response) } fn config(&self) -> &ChainConfig { @@ -122,118 +236,6 @@ impl Chain for CosmosSDKChain { Ok(light_block) } - - fn sign_tx(&self, _msgs: &[Any]) -> Result, Error> { - unimplemented!() - - // TODO: Once the tendermint is upgraded and crypto can be imported then work on this build and signing code - // This is a pregenerated private key from running: - // let signing_key = SigningKey::random(&mut OsRng); - // println!("{:?", hex::encode(signing_key.to_bytes())); - // It corresponds to the address: cosmos14kl05amnc3mdyj5d2r27agvwhuqgz7vwfz0wwj - // Add it to your genesis or send coins to it. - // Then query the account number and update account_number here. - // let signing_key_bytes = "cda4e48a1ae228656e483b2f3ae7bca6d04abcef64189ff56d481987259dd2a4"; - // let account_number = 12; - // - // let signing_key = SigningKey::new(&hex::decode(signing_key_bytes).unwrap()).unwrap(); - // let verify_key = VerifyKey::from(&signing_key); - // let pubkey_bytes = verify_key.to_bytes().to_vec(); - // let addr = get_account(pubkey_bytes.clone()); - // msg.signer = addr; // XXX: replace signer - // - // // Build and sign transaction - // //let _signed = chain.build_sign_tx(vec![Box::new(msg)]); - // - // let mut proto_msgs: Vec = Vec::new(); - // let mut buf = Vec::new(); - // - // // Have a loop if new_builder takes more messages - // // for now just encode one message - // prost::Message::encode(&msg, &mut buf).unwrap(); - // - // // Create a proto any message - // let any_msg = prost_types::Any { - // type_url: "/ibc.connection.MsgConnectionOpenInit".to_string(), // "type.googleapis.com/ibc.connection.MsgConnectionOpenInit".to_string(), - // value: buf, - // }; - // - // // Add proto message - // proto_msgs.push(any_msg); - // - // // Create TxBody - // let body = TxBody { - // messages: proto_msgs, - // memo: "".to_string(), - // timeout_height: 0, - // extension_options: Vec::::new(), - // non_critical_extension_options: Vec::::new(), - // }; - // - // let sum = Some(PK_Sum::Secp256k1(pubkey_bytes)); - // - // let pk = Some(PublicKey { sum }); - // - // let single = Single { mode: 1 }; - // let sum_single = Some(Sum::Single(single)); - // let mode = Some(ModeInfo { sum: sum_single }); - // - // let signer_info = SignerInfo { - // public_key: pk, - // mode_info: mode, - // sequence: 0, - // }; - // - // let auth_info = AuthInfo { - // signer_infos: vec![signer_info], - // fee: None, - // }; - // - // // A protobuf serialization of a TxBody - // let mut body_buf = Vec::new(); - // prost::Message::encode(&body, &mut body_buf).unwrap(); - // - // // A protobuf serialization of a AuthInfo - // let mut auth_buf = Vec::new(); - // prost::Message::encode(&auth_info, &mut auth_buf).unwrap(); - // - // let sign_doc = SignDoc { - // body_bytes: body_buf.clone(), - // auth_info_bytes: auth_buf.clone(), - // chain_id: chain_config.clone().id.to_string(), - // account_number: account_number, - // }; - // - // // A protobuf serialization of a AuthInfo - // let mut signdoc_buf = Vec::new(); - // prost::Message::encode(&sign_doc, &mut signdoc_buf).unwrap(); - // - // let signature: Signature = signing_key.sign(&signdoc_buf); - // - // status_info!("Signed Tx", "{:?}", signed_doc); - // - // let tx_raw = TxRaw { - // body_bytes, - // auth_info_bytes: auth_bytes, - // signatures: vec![signature.as_ref().to_vec()], - // }; - // - // let mut txraw_buf = Vec::new(); - // prost::Message::encode(&tx_raw, &mut txraw_buf).unwrap(); - // println!("{:?}", txraw_buf); - - /* - // TODO: get this from the config - let client = Client::new(Address::Tcp{ - peer_id: None, - host: "localhost", - port: 26657, - }); - match client.broadcast_tx_commit(Transaction::new(txraw_buf)); { - Ok(resp) => println!("OK! {:?}", resp), - Err(e) => println!("Err {:?}", e) - };*/ - } } /// Perform a generic `abci_query`, and return the corresponding deserialized response data. @@ -278,6 +280,27 @@ fn fetch_signed_header(client: &HttpClient, height: Height) -> Result, +) -> Result, anomaly::Error> { + // Use the Tendermint-rs RPC client to do the query. + let response = chain + .rpc_client() + .broadcast_tx_sync(Transaction::new(data)) + .await + .map_err(|e| Kind::Rpc.context(e))?; + + if !response.code.is_ok() { + // Fail with response log. + println!("Tx Error Response: {:?}", response.clone()); + return Err(Kind::Rpc.context(response.log.to_string()).into()); + } + + Ok(response.data.as_bytes().to_vec()) +} + fn fetch_validator_set(client: &HttpClient, height: Height) -> Result { let res = block_on(client.validators(height)); diff --git a/relayer/src/error.rs b/relayer/src/error.rs index f50ab92523..287d7694bd 100644 --- a/relayer/src/error.rs +++ b/relayer/src/error.rs @@ -49,6 +49,10 @@ pub enum Kind { /// A message transaction failure #[error("Message transaction failure: {0}")] MessageTransaction(String), + + /// Keybase related error + #[error("Keybase error")] + KeyBase, } impl Kind { diff --git a/relayer/src/keyring.rs b/relayer/src/keyring.rs new file mode 100644 index 0000000000..50c42a7f51 --- /dev/null +++ b/relayer/src/keyring.rs @@ -0,0 +1,2 @@ +pub mod errors; +pub mod store; diff --git a/relayer/src/keyring/errors.rs b/relayer/src/keyring/errors.rs new file mode 100644 index 0000000000..71482459cc --- /dev/null +++ b/relayer/src/keyring/errors.rs @@ -0,0 +1,25 @@ +use anomaly::{BoxError, Context}; +use thiserror::Error; + +pub type Error = anomaly::Error; + +#[derive(Clone, Debug, Error)] +pub enum Kind { + #[error("cannot retrieve key for address")] + InvalidKey, + + #[error("invalid mnemonic")] + InvalidMnemonic, + + #[error("cannot generate private key")] + PrivateKey, + + #[error("cannot generate bech32 account")] + Bech32Account, +} + +impl Kind { + pub fn context(self, source: impl Into) -> Context { + Context::new(self, Some(source.into())) + } +} diff --git a/relayer/src/keyring/store.rs b/relayer/src/keyring/store.rs new file mode 100644 index 0000000000..a88219b200 --- /dev/null +++ b/relayer/src/keyring/store.rs @@ -0,0 +1,189 @@ +use crate::keyring::errors::{Error, Kind}; +use bech32::ToBase32; +use bitcoin::hashes::hex::ToHex; +use bitcoin::secp256k1::{All, Message, Secp256k1}; +use bitcoin::{ + network::constants::Network, + util::bip32::{DerivationPath, ExtendedPrivKey, ExtendedPubKey}, + PrivateKey, +}; +use bitcoin_wallet::account::MasterAccount; +use bitcoin_wallet::mnemonic::Mnemonic; +use hdpath::StandardHDPath; +use k256::{ + ecdsa::{signature::Signer, signature::Verifier, Signature, SigningKey, VerifyKey}, + EncodedPoint, SecretKey, +}; +use serde_json::Value; +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::str::FromStr; +use tendermint::account::Id as AccountId; + +pub type Address = Vec; + +pub enum KeyRing { + MemoryKeyStore { store: BTreeMap }, +} + +pub enum StoreBackend { + Memory, +} + +pub trait KeyRingOperations: Sized { + fn init(backend: StoreBackend) -> KeyRing; + fn key_from_seed_file(&mut self, key_file_content: &str) -> Result; + fn add_from_mnemonic(&mut self, mnemonic_words: &str) -> Result; + fn get(&self, address: Vec) -> Result<&KeyEntry, Error>; + fn insert(&mut self, addr: Vec, key: KeyEntry) -> Option; + fn sign(&self, signer: Vec, msg: Vec) -> Vec; +} + +/// Key entry stores the Private Key and Public Key as well the address +#[derive(Clone, Debug)] +pub struct KeyEntry { + /// Public key + pub public_key: ExtendedPubKey, + + /// Private key + pub private_key: ExtendedPrivKey, + + /// Address + pub address: Vec, + + /// Account Bech32 format - TODO allow hrp + pub account: String, +} + +impl KeyRingOperations for KeyRing { + /// Initialize a in memory key entry store + fn init(backend: StoreBackend) -> KeyRing { + match backend { + StoreBackend::Memory => { + let store: BTreeMap = BTreeMap::new(); + KeyRing::MemoryKeyStore { store } + } + } + } + + /// Get key from seed file + fn key_from_seed_file(&mut self, key_file_content: &str) -> Result { + let key_json: Value = serde_json::from_str(key_file_content) + .map_err(|e| Kind::InvalidKey.context("failed to parse key seed file"))?; + + let signer: AccountId; + let key: KeyEntry; + + let mnemonic: String = "".to_string(); + let mnemonic_value = key_json.get("mnemonic"); + match mnemonic_value { + Some(m) => { + let mnemonic = m.as_str(); + match mnemonic { + Some(v) => { + key = self + .add_from_mnemonic(v) + .map_err(|e| Kind::InvalidMnemonic.context(e))?; + Ok(key) + } + None => { + return Err(Kind::InvalidMnemonic + .context("invalid key file, cannot find mnemonic".to_string()))? + } + } + } + None => { + return Err(Kind::InvalidMnemonic + .context("invalid key file, cannot find mnemonic".to_string()))? + } + } + } + + /// Add a key entry in the store using a mnemonic. + fn add_from_mnemonic(&mut self, mnemonic_words: &str) -> Result { + // Generate seed from mnemonic + let mnemonic = + Mnemonic::from_str(mnemonic_words).map_err(|e| Kind::InvalidMnemonic.context(e))?; + let seed = mnemonic.to_seed(Some("")); + + // Get Private Key from seed and standard derivation path + let hd_path = StandardHDPath::try_from("m/44'/118'/0'/0/0").unwrap(); + let private_key = ExtendedPrivKey::new_master(Network::Bitcoin, &seed.0) + .and_then(|k| k.derive_priv(&Secp256k1::new(), &DerivationPath::from(hd_path))) + .map_err(|e| Kind::PrivateKey.context(e))?; + + // Get Public Key from Private Key + let public_key = ExtendedPubKey::from_private(&Secp256k1::new(), &private_key); + + // Get address from the Public Key + let address = get_address(public_key); + + // Get Bech32 account + let account = bech32::encode("cosmos", address.to_base32()) + .map_err(|e| Kind::Bech32Account.context(e))?; + + let key = KeyEntry { + public_key, + private_key, + address, + account, + }; + + self.insert(key.clone().address, key.clone()); + + Ok(key) + } + + /// Return a key entry from a key name + fn get(&self, address: Vec) -> Result<&KeyEntry, Error> { + match &self { + KeyRing::MemoryKeyStore { store: s } => { + if !s.contains_key(&address) { + return Err(Kind::InvalidKey.into()); + } else { + let key = s.get(&address); + match key { + Some(k) => Ok(k), + None => Err(Kind::InvalidKey.into()), + } + } + } + } + } + + /// Insert an entry in the key store + fn insert(&mut self, addr: Vec, key: KeyEntry) -> Option { + match self { + KeyRing::MemoryKeyStore { store: s } => { + let ke = s.insert(addr, key); + ke + } + } + } + + /// Sign a message + fn sign(&self, signer: Vec, msg: Vec) -> Vec { + let key = self.get(signer).unwrap(); + let private_key_bytes = key.private_key.private_key.to_bytes(); + let signing_key = SigningKey::new(private_key_bytes.as_slice()).unwrap(); + let signature: Signature = signing_key.sign(&msg); + signature.as_ref().to_vec() + } +} + +/// Return an address from a Public Key +fn get_address(pk: ExtendedPubKey) -> Vec { + use crypto::digest::Digest; + use crypto::ripemd160::Ripemd160; + use crypto::sha2::Sha256; + + let mut sha256 = Sha256::new(); + sha256.input(pk.public_key.to_bytes().as_slice()); + let mut bytes = vec![0; sha256.output_bytes()]; + sha256.result(&mut bytes); + let mut hash = Ripemd160::new(); + hash.input(bytes.as_slice()); + let mut acct = vec![0; hash.output_bytes()]; + hash.result(&mut acct); + acct.to_vec() +} diff --git a/relayer/src/keys.rs b/relayer/src/keys.rs new file mode 100644 index 0000000000..a5e662a058 --- /dev/null +++ b/relayer/src/keys.rs @@ -0,0 +1 @@ +pub mod restore; diff --git a/relayer/src/keys/restore.rs b/relayer/src/keys/restore.rs new file mode 100644 index 0000000000..d927b96cd6 --- /dev/null +++ b/relayer/src/keys/restore.rs @@ -0,0 +1,24 @@ +use crate::chain::CosmosSDKChain; +use crate::config::ChainConfig; +use crate::error; +use crate::error::Error; +use crate::keyring::store::{KeyRing, KeyRingOperations}; + +#[derive(Clone, Debug)] +pub struct KeysRestoreOptions { + pub name: String, + pub mnemonic: String, + pub chain_config: ChainConfig, +} + +pub fn restore_key(opts: KeysRestoreOptions) -> Result, Error> { + // Get the destination chain + let mut chain = CosmosSDKChain::from_config(opts.clone().chain_config)?; + + let address = chain + .keybase + .add_from_mnemonic(&opts.mnemonic) + .map_err(|e| error::Kind::KeyBase.context(e))?; + + Ok(address.account.as_bytes().to_vec()) +} diff --git a/relayer/src/lib.rs b/relayer/src/lib.rs index 7deb3e1e0a..44052c3b57 100644 --- a/relayer/src/lib.rs +++ b/relayer/src/lib.rs @@ -22,6 +22,8 @@ pub mod error; pub mod event_handler; pub mod event_monitor; pub mod foreign_client; +pub mod keyring; +pub mod keys; pub mod link; pub mod msgs; pub mod tx; diff --git a/relayer/src/tx/client.rs b/relayer/src/tx/client.rs index 4db1717c4a..8e2f353ef2 100644 --- a/relayer/src/tx/client.rs +++ b/relayer/src/tx/client.rs @@ -11,19 +11,32 @@ use ibc::tx_msg::Msg; use crate::chain::{Chain, CosmosSDKChain}; use crate::config::ChainConfig; use crate::error::{Error, Kind}; +use crate::keyring::store::{KeyEntry, KeyRingOperations}; use crate::util::block_on; +use bitcoin::hashes::hex::ToHex; use ibc::ics02_client::height::Height; +use std::str::FromStr; +use tendermint::account::Id as AccountId; #[derive(Clone, Debug)] pub struct CreateClientOptions { pub dest_client_id: ClientId, pub dest_chain_config: ChainConfig, pub src_chain_config: ChainConfig, + pub signer_key: String, } -pub fn create_client(opts: CreateClientOptions) -> Result<(), Error> { +pub fn create_client(opts: CreateClientOptions) -> Result, Error> { // Get the destination chain - let dest_chain = CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; + let mut dest_chain = CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; + + // Get the key from key seed file + let key = dest_chain + .keybase + .key_from_seed_file(&opts.signer_key) + .map_err(|e| Kind::KeyBase.context(e))?; + let signer: AccountId = + AccountId::from_str(&key.address.to_hex()).map_err(|e| Kind::KeyBase.context(e))?; // Query the client state on destination chain. let response = dest_chain.query( @@ -94,14 +107,19 @@ pub fn create_client(opts: CreateClientOptions) -> Result<(), Error> { Kind::MessageTransaction("failed to build the create client message".into()).context(e) })?; - // Create a proto any message - let mut proto_msgs: Vec = Vec::new(); - let any_msg = Any { - // TODO - add get_url_type() to prepend proper string to get_type() - type_url: "/ibc.client.MsgCreateClient".to_ascii_lowercase(), - value: new_msg.get_sign_bytes(), - }; + let msg_type = "/ibc.client.MsgCreateClient".to_ascii_lowercase(); + + // TODO - Replace logic to fetch the proper account sequence via CLI parameter + let response = dest_chain + .send( + msg_type, + new_msg.get_sign_bytes(), + key, + 0, + "".to_string(), + 0, + ) + .map_err(|e| Kind::MessageTransaction("failed to create client".to_string()).context(e))?; - proto_msgs.push(any_msg); - dest_chain.send(&proto_msgs) + Ok(response) } diff --git a/relayer/src/tx/connection.rs b/relayer/src/tx/connection.rs index efac87bddd..4add9326e6 100644 --- a/relayer/src/tx/connection.rs +++ b/relayer/src/tx/connection.rs @@ -1,14 +1,19 @@ use crate::chain::{Chain, CosmosSDKChain}; use crate::config::ChainConfig; -use crate::error::Error; +use crate::error::{Error, Kind}; +use crate::keyring::store::{KeyEntry, KeyRingOperations}; +use bitcoin::hashes::hex::ToHex; +use hex; use ibc::ics03_connection::connection::Counterparty; use ibc::ics03_connection::msgs::conn_open_init::MsgConnectionOpenInit; use ibc::ics23_commitment::commitment::CommitmentPrefix; use ibc::ics24_host::identifier::{ClientId, ConnectionId}; use ibc::tx_msg::Msg; use prost_types::Any; +use serde_json::Value; use std::str::FromStr; use tendermint::account::Id as AccountId; +use tendermint_rpc::Id; #[derive(Clone, Debug)] pub struct ConnectionOpenInitOptions { @@ -18,45 +23,51 @@ pub struct ConnectionOpenInitOptions { pub dest_connection_id: Option, pub src_chain_config: ChainConfig, pub dest_chain_config: ChainConfig, + pub signer_key: String, + pub account_sequence: u64, } -pub fn conn_init(opts: ConnectionOpenInitOptions) -> Result<(), Error> { +pub fn conn_init(opts: ConnectionOpenInitOptions) -> Result, Error> { // Get the destination chain - let dest_chain = CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; + let mut dest_chain = CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; - let id_hex = "25EF56CA795135E409368E6DB8110F22A4BE05C2"; - let signer = AccountId::from_str(id_hex).unwrap(); - - // let signer = Id::from_str(dest_chain.config().account_prefix.as_str()).map_err(|e| { - // Kind::MessageTransaction("Connection Open Init Error: Bad Signer".into()).context(e) - // })?; + // Get the key from key seed file + let key = dest_chain + .keybase + .key_from_seed_file(&opts.signer_key) + .map_err(|e| Kind::KeyBase.context(e))?; + let signer: AccountId = + AccountId::from_str(&key.address.to_hex()).map_err(|e| Kind::KeyBase.context(e))?; let counterparty = Counterparty::new( opts.dest_client_id, opts.dest_connection_id, - CommitmentPrefix::from(vec![]), + CommitmentPrefix::from(dest_chain.config().store_prefix.as_bytes().to_vec()), ); let msg = MsgConnectionOpenInit { client_id: opts.src_client_id, connection_id: opts.src_connection_id, counterparty: counterparty.unwrap(), - // TODO - add to opts - version: "1.0.0".to_string(), + version: "".to_string(), signer, }; - // Create a proto any message - let mut proto_msgs: Vec = Vec::new(); - - let any_msg = Any { - type_url: "/ibc.connection.MsgConnectionOpenInit".to_string(), // "type.googleapis.com/ibc.connection.MsgConnectionOpenInit".to_string(), - value: msg.get_sign_bytes(), - }; - - // Add proto message - proto_msgs.push(any_msg); + let msg_type = "/ibc.core.connection.v1.MsgConnectionOpenInit".to_string(); // Send message - dest_chain.send(&proto_msgs) + let response = dest_chain + .send( + msg_type, + msg.get_sign_bytes(), + key, + opts.account_sequence, + "".to_string(), + 0, + ) + .map_err(|e| { + Kind::MessageTransaction("failed to initialize open connection".to_string()).context(e) + })?; + + Ok(response) }