diff --git a/relayer-cli/README.md b/relayer-cli/README.md index 43290889c7..7f9690ab5f 100644 --- a/relayer-cli/README.md +++ b/relayer-cli/README.md @@ -19,6 +19,9 @@ relayer-cli -c config.toml tx raw create-client dest_chain_id src_chain_id dest_ relayer-cli -c config.toml tx raw conn-init dest_chain_id src_chain_id dest_client_id src_client_id dest_connection_id -d src_connection_id -k seed_file.json + +relayer-cli -c config.toml tx raw conn-try dest_chain_id src_chain_id dest_client_id src_client_id dest_connection_id src_connection_id + -k seed_file.json ``` Note: This is work in progress, more commands will be implemented and tested with gaia stargate-4 chains. diff --git a/relayer-cli/src/commands/query/channel.rs b/relayer-cli/src/commands/query/channel.rs index 40193362e8..aee3d1ca09 100644 --- a/relayer-cli/src/commands/query/channel.rs +++ b/relayer-cli/src/commands/query/channel.rs @@ -102,7 +102,9 @@ impl Runnable for QueryChannelEndCmd { opts.proof, ) .map_err(|e| Kind::Query.context(e).into()) - .and_then(|v| ChannelEnd::decode_vec(&v).map_err(|e| Kind::Query.context(e).into())); + .and_then(|v| { + ChannelEnd::decode_vec(&v.value).map_err(|e| Kind::Query.context(e).into()) + }); match res { Ok(cs) => status_info!("Result for channel end query: ", "{:?}", cs), diff --git a/relayer-cli/src/commands/query/client.rs b/relayer-cli/src/commands/query/client.rs index 47139a306a..20ec1dc53d 100644 --- a/relayer-cli/src/commands/query/client.rs +++ b/relayer-cli/src/commands/query/client.rs @@ -85,7 +85,7 @@ impl Runnable for QueryClientStateCmd { ) .map_err(|e| Kind::Query.context(e).into()) .and_then(|v| { - AnyClientState::decode_vec(&v).map_err(|e| Kind::Query.context(e).into()) + AnyClientState::decode_vec(&v.value).map_err(|e| Kind::Query.context(e).into()) }); match res { Ok(cs) => status_info!("client state query result: ", "{:?}", cs), @@ -183,7 +183,7 @@ impl Runnable for QueryClientConsensusCmd { ) .map_err(|e| Kind::Query.context(e).into()) .and_then(|v| { - AnyConsensusState::decode_vec(&v).map_err(|e| Kind::Query.context(e).into()) + AnyConsensusState::decode_vec(&v.value).map_err(|e| Kind::Query.context(e).into()) }); match res { @@ -289,7 +289,9 @@ impl Runnable for QueryClientConnectionsCmd { false, ) .map_err(|e| Kind::Query.context(e).into()) - .and_then(|v| ConnectionIDs::decode_vec(&v).map_err(|e| Kind::Query.context(e).into())); + .and_then(|v| { + ConnectionIDs::decode_vec(&v.value).map_err(|e| Kind::Query.context(e).into()) + }); match res { Ok(cs) => status_info!("client connections query result: ", "{:?}", cs), Err(e) => status_info!("client connections query error", "{}", e), diff --git a/relayer-cli/src/commands/query/connection.rs b/relayer-cli/src/commands/query/connection.rs index 111c803e3b..6ce3ab02de 100644 --- a/relayer-cli/src/commands/query/connection.rs +++ b/relayer-cli/src/commands/query/connection.rs @@ -1,9 +1,7 @@ -use std::convert::TryInto; - use abscissa_core::{Command, Options, Runnable}; use ibc::ics03_connection::connection::ConnectionEnd; use ibc::ics24_host::error::ValidationError; -use ibc::ics24_host::identifier::ConnectionId; +use ibc::ics24_host::identifier::{ChainId as ICSChainId, ConnectionId}; use relayer::chain::{Chain, CosmosSDKChain}; use relayer::config::{ChainConfig, Config}; use tendermint::chain::Id as ChainId; @@ -64,6 +62,7 @@ impl QueryConnectionEndCmd { } } +// cargo run --bin relayer -- -c relayer/tests/config/fixtures/simple_config.toml query connection end ibc-test connectionidone --height 3 impl Runnable for QueryConnectionEndCmd { fn run(&self) { let config = app_config(); @@ -78,14 +77,14 @@ impl Runnable for QueryConnectionEndCmd { status_info!("Options", "{:?}", opts); let chain = CosmosSDKChain::from_config(chain_config).unwrap(); - // run without proof: - // cargo run --bin relayer -- -c relayer/tests/config/fixtures/simple_config.toml query connection end ibc-test connectionidone --height 3 -p false + let height = ibc::Height::new( + ICSChainId::chain_version(chain.id().to_string()), + opts.height, + ); + + // TODO - any value in querying with proof from the CLI? let res: Result = chain - .query_connection( - &opts.connection_id, - opts.height.try_into().unwrap(), - opts.proof, - ) + .query_connection(&opts.connection_id, height) .map_err(|e| Kind::Query.context(e).into()); match res { diff --git a/relayer-cli/src/commands/tx.rs b/relayer-cli/src/commands/tx.rs index 445ad8660e..c347d172c3 100644 --- a/relayer-cli/src/commands/tx.rs +++ b/relayer-cli/src/commands/tx.rs @@ -24,10 +24,6 @@ pub enum TxRawCommands { #[options(help = "get usage information")] Help(Help), - /// The `tx raw conn-init` subcommand - #[options(help = "tx raw conn-init")] - ConnInit(connection::TxRawConnInitCmd), - /// The `tx raw client-create` subcommand submits a MsgCreateClient in a transaction to a chain #[options(help = "tx raw create-client")] CreateClient(TxCreateClientCmd), @@ -35,4 +31,12 @@ pub enum TxRawCommands { /// The `tx raw client-update` subcommand submits a MsgUpdateClient in a transaction to a chain #[options(help = "tx raw update-client")] UpdateClient(TxUpdateClientCmd), + + /// The `tx raw conn-init` subcommand + #[options(help = "tx raw conn-init")] + ConnInit(connection::TxRawConnInitCmd), + + /// The `tx raw conn-try` subcommand + #[options(help = "tx raw conn-try")] + ConnTry(connection::TxRawConnTryCmd), } diff --git a/relayer-cli/src/commands/tx/client.rs b/relayer-cli/src/commands/tx/client.rs index 3b26fc1947..ffb2bdd604 100644 --- a/relayer-cli/src/commands/tx/client.rs +++ b/relayer-cli/src/commands/tx/client.rs @@ -2,7 +2,9 @@ use abscissa_core::{Command, Options, Runnable}; use ibc::ics24_host::identifier::ClientId; -use relayer::tx::client::{create_client, update_client, ClientOptions}; +use relayer::tx::client::{ + build_create_client_and_send, build_update_client_and_send, ClientOptions, +}; use crate::application::app_config; use crate::error::{Error, Kind}; @@ -22,9 +24,6 @@ pub struct TxCreateClientCmd { )] dest_client_id: ClientId, - #[options(help = "account sequence of the signer", short = "s")] - account_sequence: u64, - #[options( help = "json key file for the signer, must include mnemonic", short = "k" @@ -38,7 +37,6 @@ impl Runnable for TxCreateClientCmd { &self.dest_chain_id, &self.src_chain_id, &self.dest_client_id, - self.account_sequence, &self.seed_file, ) { Err(err) => { @@ -49,8 +47,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 = + build_create_client_and_send(opts).map_err(|e| Kind::Tx.context(e).into()); match res { Ok(receipt) => status_ok!("Success", "client created: {:?}", receipt), @@ -73,9 +71,6 @@ pub struct TxUpdateClientCmd { )] dest_client_id: ClientId, - #[options(help = "account sequence of the signer", short = "s")] - account_sequence: u64, - #[options( help = "json key file for the signer, must include mnemonic", short = "k" @@ -89,7 +84,6 @@ impl Runnable for TxUpdateClientCmd { &self.dest_chain_id, &self.src_chain_id, &self.dest_client_id, - self.account_sequence, &self.seed_file, ) { Err(err) => { @@ -100,8 +94,8 @@ impl Runnable for TxUpdateClientCmd { }; status_info!("Message", "{:?}", opts); - let res: Result, Error> = - update_client(opts).map_err(|e| Kind::Tx.context(e).into()); + let res: Result = + build_update_client_and_send(opts).map_err(|e| Kind::Tx.context(e).into()); match res { Ok(receipt) => status_ok!("Success", "client updated: {:?}", receipt), @@ -114,7 +108,6 @@ fn validate_common_options( dest_chain_id: &str, src_chain_id: &str, dest_client_id: &ClientId, - account_sequence: u64, seed_file: &str, ) -> Result { let config = app_config(); @@ -150,6 +143,5 @@ fn validate_common_options( dest_chain_config: dest_chain_config.clone(), src_chain_config: src_chain_config.clone(), signer_seed, - account_sequence, }) } diff --git a/relayer-cli/src/commands/tx/connection.rs b/relayer-cli/src/commands/tx/connection.rs index c7091533c5..61c3a4628d 100644 --- a/relayer-cli/src/commands/tx/connection.rs +++ b/relayer-cli/src/commands/tx/connection.rs @@ -5,7 +5,10 @@ use abscissa_core::{Command, Options, Runnable}; use ibc::ics24_host::identifier::{ClientId, ConnectionId}; use relayer::config::Config; -use relayer::tx::connection::{conn_init, ConnectionOpenInitOptions}; +use relayer::tx::connection::{ + build_conn_init_and_send, build_conn_try_and_send, ConnectionOpenInitOptions, + ConnectionOpenTryOptions, +}; use crate::error::{Error, Kind}; @@ -43,6 +46,7 @@ impl TxRawConnInitCmd { .iter() .find(|c| c.id == self.dest_chain_id.parse().unwrap()) .ok_or_else(|| "missing destination chain configuration".to_string())?; + let src_chain_config = config .chains .iter() @@ -54,12 +58,12 @@ impl TxRawConnInitCmd { })?; let opts = ConnectionOpenInitOptions { + dest_chain_config: dest_chain_config.clone(), + src_chain_config: src_chain_config.clone(), dest_client_id: self.dest_client_id.clone(), src_client_id: self.src_client_id.clone(), dest_connection_id: self.dest_connection_id.clone(), src_connection_id: self.src_connection_id.clone(), - dest_chain_config: dest_chain_config.clone(), - src_chain_config: src_chain_config.clone(), signer_seed, }; @@ -80,7 +84,8 @@ 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 = + build_conn_init_and_send(&opts).map_err(|e| Kind::Tx.context(e).into()); match res { Ok(receipt) => status_info!("conn init, result: ", "{:?}", receipt), @@ -88,3 +93,85 @@ impl Runnable for TxRawConnInitCmd { } } } + +#[derive(Clone, Command, Debug, Options)] +pub struct TxRawConnTryCmd { + #[options(free, help = "identifier of the destination chain")] + dest_chain_id: String, + + #[options(free, help = "identifier of the source chain")] + src_chain_id: String, + + #[options(free, help = "identifier of the destination client")] + dest_client_id: ClientId, + + #[options(free, help = "identifier of the source client")] + src_client_id: ClientId, + + #[options(free, help = "identifier of the destination connection")] + dest_connection_id: ConnectionId, + + #[options(free, help = "identifier of the source connection")] + src_connection_id: ConnectionId, + + #[options( + help = "json key file for the signer, must include mnemonic", + short = "k" + )] + seed_file: String, +} + +impl TxRawConnTryCmd { + fn validate_options(&self, config: &Config) -> Result { + let dest_chain_config = config + .chains + .iter() + .find(|c| c.id == self.dest_chain_id.parse().unwrap()) + .ok_or_else(|| "missing destination chain configuration".to_string())?; + + let src_chain_config = config + .chains + .iter() + .find(|c| c.id == self.src_chain_id.parse().unwrap()) + .ok_or_else(|| "missing src chain configuration".to_string())?; + + let signer_seed = std::fs::read_to_string(&self.seed_file).map_err(|e| { + anomaly::Context::new("invalid signer seed file", Some(e.into())).to_string() + })?; + + let opts = ConnectionOpenTryOptions { + src_chain_config: src_chain_config.clone(), + dest_chain_config: dest_chain_config.clone(), + src_client_id: self.src_client_id.clone(), + dest_client_id: self.dest_client_id.clone(), + src_connection_id: self.src_connection_id.clone(), + dest_connection_id: self.dest_connection_id.clone(), + signer_seed, + }; + + Ok(opts) + } +} + +impl Runnable for TxRawConnTryCmd { + 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, + }; + status_info!("Message", "{:?}", opts); + + let res: Result = + build_conn_try_and_send(opts).map_err(|e| Kind::Tx.context(e).into()); + + match res { + Ok(receipt) => status_info!("conn try, result: ", "{:?}", receipt), + Err(e) => status_info!("conn try failed, error: ", "{}", e), + } + } +} diff --git a/relayer-cli/tests/integration.rs b/relayer-cli/tests/integration.rs index 6084e8708f..b486a29592 100644 --- a/relayer-cli/tests/integration.rs +++ b/relayer-cli/tests/integration.rs @@ -93,7 +93,8 @@ fn query_channel_id() { Height::from(0_u32), false, ) - .unwrap(), + .unwrap() + .value, ) .unwrap(); @@ -117,7 +118,8 @@ fn query_client_id() { Height::from(0_u32), false, ) - .unwrap(), + .unwrap() + .value, ) .unwrap(); diff --git a/relayer/src/chain.rs b/relayer/src/chain.rs index 7d8dc20d13..314dd0f093 100644 --- a/relayer/src/chain.rs +++ b/relayer/src/chain.rs @@ -8,7 +8,8 @@ use serde::{de::DeserializeOwned, Serialize}; use tendermint_proto::DomainType; // TODO - tendermint deps should not be here -use tendermint::account::Id as AccountId; +//use tendermint::account::Id as AccountId; +use ibc_proto::ibc::core::commitment::v1::MerkleProof; use tendermint::block::Height; use tendermint::chain::Id as ChainId; use tendermint_light_client::types::TrustThreshold; @@ -17,27 +18,40 @@ use tendermint_rpc::Client as RpcClient; use ibc::ics02_client::client_def::{AnyClientState, AnyConsensusState, AnyHeader}; use ibc::ics02_client::msgs::create_client::MsgCreateAnyClient; use ibc::ics02_client::msgs::update_client::MsgUpdateAnyClient; +use ibc::Height as ICSHeight; + use ibc::ics02_client::state::{ClientState, ConsensusState}; -use ibc::ics03_connection::connection::{ConnectionEnd, Counterparty}; +use ibc::ics03_connection::connection::{ConnectionEnd, Counterparty, State}; use ibc::ics03_connection::msgs::conn_open_init::MsgConnectionOpenInit; -use ibc::ics23_commitment::commitment::CommitmentPrefix; +use ibc::ics03_connection::msgs::conn_open_try::MsgConnectionOpenTry; +use ibc::ics03_connection::msgs::ConnectionMsgType; +use ibc::ics03_connection::version::get_compatible_versions; +use ibc::ics23_commitment::commitment::{CommitmentPrefix, CommitmentProof}; use ibc::ics24_host::identifier::{ClientId, ConnectionId}; use ibc::ics24_host::Path; +use ibc::ics24_host::Path::ClientConsensusState as ClientConsensusPath; +use ibc::proofs::{ConsensusProof, Proofs}; use ibc::tx_msg::Msg; -use ibc::Height as ICSHeight; use crate::client::LightClient; use crate::config::ChainConfig; use crate::error::{Error, Kind}; use crate::keyring::store::{KeyEntry, KeyRing}; -use crate::tx::connection::ConnectionOpenInitOptions; +use crate::tx::connection::{ConnectionOpenInitOptions, ConnectionOpenTryOptions}; use crate::util::block_on; pub(crate) mod cosmos; pub use cosmos::CosmosSDKChain; - pub mod handle; +/// Generic query response type +/// TODO - will slowly move to GRPC protobuf specs for queries +pub struct QueryResponse { + pub value: Vec, + pub proof: MerkleProof, + pub height: Height, +} + /// Defines a blockchain as understood by the relayer pub trait Chain { /// TODO - Should these be part of the Chain trait? @@ -61,7 +75,16 @@ pub trait Chain { type Error: Into>; /// Perform a generic `query`, and return the corresponding response data. - fn query(&self, data: Path, height: Height, prove: bool) -> Result, Self::Error>; + // TODO - migrate callers to use ics_query() and then remove this + fn query(&self, data: Path, height: Height, prove: bool) -> Result; + + /// Perform a generic ICS `query`, and return the corresponding response data. + fn ics_query( + &self, + data: Path, + height: ICSHeight, + prove: bool, + ) -> Result; /// send a transaction with `msgs` to chain. fn send( @@ -70,9 +93,10 @@ pub trait Chain { key: KeyEntry, memo: String, timeout_height: u64, - ) -> Result, Self::Error>; + ) -> Result; /// Returns the chain's identifier + /// TODO - move to ICS Chain Id fn id(&self) -> &ChainId { &self.config().id } @@ -98,14 +122,56 @@ pub trait Chain { fn query_client_state( &self, client_id: &ClientId, - height: Height, - proof: bool, + height: ICSHeight, ) -> Result; + fn proven_client_state( + &self, + client_id: &ClientId, + height: ICSHeight, + ) -> Result<(AnyClientState, MerkleProof), Error>; + fn build_client_state(&self, height: ICSHeight) -> Result; fn build_consensus_state(&self, height: ICSHeight) -> Result; + fn proven_connection( + &self, + connection_id: &ConnectionId, + height: ICSHeight, + ) -> Result<(ConnectionEnd, MerkleProof), Error> { + let res = self + .ics_query(Path::Connections(connection_id.clone()), height, true) + .map_err(|e| Kind::Query.context(e))?; + let connection_end = + ConnectionEnd::decode_vec(&res.value).map_err(|e| Kind::Query.context(e))?; + + Ok((connection_end, res.proof)) + } + + fn proven_client_consensus( + &self, + client_id: &ClientId, + consensus_height: ICSHeight, + height: ICSHeight, + ) -> Result<(AnyConsensusState, MerkleProof), Error> { + let res = self + .ics_query( + ClientConsensusPath { + client_id: client_id.clone(), + epoch: consensus_height.version_number, + height: consensus_height.version_height, + }, + height, + true, + ) + .map_err(|e| Kind::Query.context(e))?; + let consensus_state = + AnyConsensusState::decode_vec(&res.value).map_err(|e| Kind::Query.context(e))?; + + Ok((consensus_state, res.proof)) + } + fn build_header( &self, trusted_height: ICSHeight, @@ -113,20 +179,85 @@ pub trait Chain { ) -> Result; fn query_commitment_prefix(&self) -> Result { + // TODO - do a real chain query Ok(CommitmentPrefix::from( self.config().store_prefix.as_bytes().to_vec(), )) } + fn query_compatible_versions(&self) -> Result, Error> { + // TODO - do a real chain query + Ok(get_compatible_versions()) + } + fn query_connection( &self, connection_id: &ConnectionId, - height: Height, - proof: bool, + height: ICSHeight, ) -> Result { Ok(self - .query(Path::Connections(connection_id.clone()), height, proof) + .ics_query(Path::Connections(connection_id.clone()), height, false) .map_err(|e| Kind::Query.context(e)) - .and_then(|v| ConnectionEnd::decode_vec(&v).map_err(|e| Kind::Query.context(e)))?) + .and_then(|v| { + ConnectionEnd::decode_vec(&v.value).map_err(|e| Kind::Query.context(e)) + })?) + } + + /// Build the required proofs for connection handshake messages. The proofs are obtained from + /// queries at height - 1 + fn build_connection_proofs( + &self, + message_type: ConnectionMsgType, + connection_id: &ConnectionId, + client_id: &ClientId, + height: ICSHeight, + ) -> Result { + // Set the height of the queries at height - 1 + let query_height = height + .decrement() + .map_err(|e| Kind::InvalidHeight.context(e))?; + + let connection_proof = + CommitmentProof::from(self.proven_connection(&connection_id, query_height)?.1); + + let mut client_proof: Option = None; + let mut consensus_proof = None; + + match message_type { + ConnectionMsgType::OpenTry | ConnectionMsgType::OpenAck => { + let (client_state, client_state_proof) = + self.proven_client_state(&client_id, query_height)?; + + client_proof = Some(CommitmentProof::from(client_state_proof)); + + let consensus_state_proof = self + .proven_client_consensus( + &client_id, + client_state.latest_height(), + query_height, + )? + .1; + + consensus_proof = Some( + ConsensusProof::new( + CommitmentProof::from(consensus_state_proof), + client_state.latest_height(), + ) + .map_err(|e| { + Kind::ConnOpenTry( + connection_id.clone(), + "failed to build consensus proof".to_string(), + ) + .context(e) + })?, + ); + } + _ => {} + } + + Ok( + Proofs::new(connection_proof, client_proof, consensus_proof, height) + .map_err(|e| Kind::MalformedProof)?, + ) } } diff --git a/relayer/src/chain/cosmos.rs b/relayer/src/chain/cosmos.rs index 5a3c624b1e..5fec24365b 100644 --- a/relayer/src/chain/cosmos.rs +++ b/relayer/src/chain/cosmos.rs @@ -10,7 +10,10 @@ use prost_types::Any; use bitcoin::hashes::hex::ToHex; use k256::ecdsa::{SigningKey, VerifyKey}; +use tendermint_proto::crypto::ProofOps; use tendermint_proto::DomainType; +use tendermint_rpc::endpoint::abci_query::AbciQuery; +use tendermint_rpc::endpoint::broadcast; use tendermint::abci::{Path as TendermintABCIPath, Transaction}; use tendermint::account::Id as AccountId; @@ -22,8 +25,17 @@ use tendermint_light_client::types::{LightBlock, SignedHeader, TrustThreshold, V use tendermint_rpc::Client; use tendermint_rpc::HttpClient; +// Support for GRPC +use ibc_proto::cosmos::auth::v1beta1::query_client::QueryClient; +use ibc_proto::cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest}; +use ibc_proto::cosmos::staking::v1beta1::Params as StakingParams; + +use tonic::codegen::http::Uri; + 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 ibc_proto::ibc::core::commitment::v1::MerkleProof; use ibc::ics02_client::client_def::{AnyClientState, AnyConsensusState, AnyHeader}; use ibc::ics02_client::msgs::create_client::MsgCreateAnyClient; @@ -35,24 +47,20 @@ use ibc::ics07_tendermint::consensus_state::ConsensusState; use ibc::ics07_tendermint::header::Header as TendermintHeader; use ibc::ics23_commitment::commitment::CommitmentPrefix; use ibc::ics24_host::identifier::{ChainId, ClientId, ConnectionId}; +use ibc::ics24_host::Path::ClientConsensusState as ClientConsensusPath; use ibc::ics24_host::Path::ClientState as ClientStatePath; use ibc::ics24_host::{Path, IBC_QUERY_PATH}; use ibc::tx_msg::Msg; use ibc::Height as ICSHeight; -use ibc_proto::cosmos::tx::v1beta1::{AuthInfo, Fee, ModeInfo, SignDoc, SignerInfo, TxBody, TxRaw}; use super::Chain; +use crate::chain::QueryResponse; use crate::client::tendermint::LightClient; use crate::config::ChainConfig; use crate::error::{Error, Kind}; use crate::keyring::store::{KeyEntry, KeyRing, KeyRingOperations, StoreBackend}; use crate::util::block_on; -// Support for GRPC -use ibc_proto::cosmos::auth::v1beta1::query_client::QueryClient; -use ibc_proto::cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest}; -use tonic::codegen::http::Uri; - pub struct CosmosSDKChain { config: ChainConfig, rpc_client: HttpClient, @@ -80,9 +88,30 @@ impl CosmosSDKChain { } /// The unbonding period of this chain - fn unbonding_period(&self) -> Duration { - // TODO - query chain - Duration::from_secs(24 * 7 * 3 * 3600) + async fn unbonding_period(&self) -> Result { + // TODO - generalize this + let grpc_addr = + Uri::from_str(&self.config().grpc_addr).map_err(|e| Kind::Grpc.context(e))?; + let mut client = + ibc_proto::cosmos::staking::v1beta1::query_client::QueryClient::connect(grpc_addr) + .await + .map_err(|e| Kind::Grpc.context(e))?; + + let request = + tonic::Request::new(ibc_proto::cosmos::staking::v1beta1::QueryParamsRequest {}); + + let response = client + .params(request) + .await + .map_err(|e| Kind::Grpc.context(e))?; + + let res = response + .into_inner() + .params + .ok_or_else(|| Kind::Grpc.context("none staking params".to_string()))? + .unbonding_time + .ok_or_else(|| Kind::Grpc.context("none unbonding time".to_string()))?; + Ok(Duration::from_secs(res.seconds as u64)) } /// Query the consensus parameters via an RPC query @@ -158,7 +187,18 @@ impl Chain for CosmosSDKChain { type ClientState = ClientState; type Error = Error; - fn query(&self, data: Path, height: Height, prove: bool) -> Result, Self::Error> { + fn ics_query( + &self, + data: Path, + height: ICSHeight, + prove: bool, + ) -> Result { + let height = + Height::try_from(height.version_height).map_err(|e| Kind::InvalidHeight.context(e))?; + self.query(data, height, prove) + } + + fn query(&self, data: Path, height: Height, prove: bool) -> Result { let path = TendermintABCIPath::from_str(IBC_QUERY_PATH).unwrap(); if !data.is_provable() & prove { @@ -178,13 +218,14 @@ impl Chain for CosmosSDKChain { } /// Send a transaction that includes the specified messages + /// TODO - split the messages in multiple Tx-es such that they don't exceed some max size fn send( &mut self, proto_msgs: Vec, key: KeyEntry, memo: String, timeout_height: u64, - ) -> Result, Error> { + ) -> Result { // Create TxBody let body = TxBody { messages: proto_msgs.to_vec(), @@ -230,7 +271,7 @@ impl Chain for CosmosSDKChain { let fee = Some(Fee { amount: vec![coin], - gas_limit: 100000, + gas_limit: 150000, payer: "".to_string(), granter: "".to_string(), }); @@ -266,10 +307,9 @@ impl Chain for CosmosSDKChain { 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)).map_err(|e| Kind::Rpc.context(e))?; + let response = + block_on(broadcast_tx_commit(self, txraw_buf)).map_err(|e| Kind::Rpc.context(e))?; Ok(response) } @@ -312,13 +352,28 @@ impl Chain for CosmosSDKChain { fn query_client_state( &self, client_id: &ClientId, - height: Height, - proof: bool, + height: ICSHeight, ) -> Result { Ok(self - .query(ClientStatePath(client_id.clone()), height, proof) + .ics_query(ClientStatePath(client_id.clone()), height, false) .map_err(|e| Kind::Query.context(e)) - .and_then(|v| AnyClientState::decode_vec(&v).map_err(|e| Kind::Query.context(e)))?) + .and_then(|v| { + AnyClientState::decode_vec(&v.value).map_err(|e| Kind::Query.context(e)) + })?) + } + + fn proven_client_state( + &self, + client_id: &ClientId, + height: ICSHeight, + ) -> Result<(AnyClientState, MerkleProof), Error> { + let res = self + .ics_query(ClientStatePath(client_id.clone()), height, true) + .map_err(|e| Kind::Query.context(e))?; + + let state = AnyClientState::decode_vec(&res.value).map_err(|e| Kind::Query.context(e))?; + + Ok((state, res.proof)) } fn build_client_state(&self, height: ICSHeight) -> Result { @@ -327,7 +382,7 @@ impl Chain for CosmosSDKChain { self.id().to_string(), self.config.trust_threshold, self.config.trusting_period, - self.unbonding_period(), + block_on(self.unbonding_period())?, Duration::from_millis(3000), // TODO - get it from src config when avail height, ICSHeight::zero(), @@ -396,7 +451,7 @@ async fn abci_query( data: String, height: Height, prove: bool, -) -> Result, anomaly::Error> { +) -> Result> { let height = if height.value() == 0 { None } else { @@ -418,16 +473,33 @@ async fn abci_query( // Fail due to empty response value (nothing to decode). return Err(Kind::EmptyResponseValue.into()); } + if prove && response.proof.is_none() { + // Fail due to empty proof + return Err(Kind::EmptyResponseProof.into()); + } + + let raw_proof_ops = response + .proof + .map(ProofOps::try_from) + .transpose() + .map_err(|e| Kind::MalformedProof.context(e))?; + + let response = QueryResponse { + value: response.value, + proof: MerkleProof { + proof: raw_proof_ops, + }, + height: response.height, + }; - Ok(response.value) + Ok(response) } -/// Perform a generic `broadcast_tx`, and return the corresponding deserialized response data. -async fn broadcast_tx( +/// Perform a `broadcast_tx_sync`, and return the corresponding deserialized response data. +async fn broadcast_tx_sync( chain: &CosmosSDKChain, data: Vec, -) -> Result, anomaly::Error> { - // Use the Tendermint-rs RPC client to do the query. +) -> Result> { let response = chain .rpc_client() .broadcast_tx_sync(data.into()) @@ -440,7 +512,22 @@ async fn broadcast_tx( return Err(Kind::Rpc.context(response.log.to_string()).into()); } - Ok(response.data.as_bytes().to_vec()) + Ok(serde_json::to_string_pretty(&response).unwrap()) +} + +/// Perform a `broadcast_tx_commit`, and return the corresponding deserialized response data. +/// TODO - move send() to this once RPC tendermint response is fixed +async fn broadcast_tx_commit( + chain: &CosmosSDKChain, + data: Vec, +) -> Result> { + let response = chain + .rpc_client() + .broadcast_tx_commit(data.into()) + .await + .map_err(|e| Kind::Rpc.context(e))?; + + Ok(serde_json::to_string(&response).unwrap()) } fn fetch_signed_header(client: &HttpClient, height: Height) -> Result { @@ -462,9 +549,10 @@ fn fetch_validator_set(client: &HttpClient, height: Height) -> Result Result { let grpc_addr = Uri::from_str(&chain.config().grpc_addr).map_err(|e| Kind::Grpc.context(e))?; - let mut client = QueryClient::connect(grpc_addr) - .await - .map_err(|e| Kind::Grpc.context(e))?; + let mut client = + ibc_proto::cosmos::auth::v1beta1::query_client::QueryClient::connect(grpc_addr) + .await + .map_err(|e| Kind::Grpc.context(e))?; let request = tonic::Request::new(QueryAccountRequest { address }); diff --git a/relayer/src/error.rs b/relayer/src/error.rs index 1c0a76d85b..e3311fcd80 100644 --- a/relayer/src/error.rs +++ b/relayer/src/error.rs @@ -42,6 +42,14 @@ pub enum Kind { #[error("Empty response value")] EmptyResponseValue, + /// Response does not contain a proof + #[error("Empty response proof")] + EmptyResponseProof, + + /// Response does not contain a proof + #[error("Malformed proof")] + MalformedProof, + /// Invalid height #[error("Invalid height")] InvalidHeight, @@ -58,6 +66,10 @@ pub enum Kind { #[error("Failed to build conn open init {0}: {1}")] ConnOpenInit(ConnectionId, String), + /// Connection open try failure + #[error("Failed to build conn open try {0}: {1}")] + ConnOpenTry(ConnectionId, String), + /// A message transaction failure #[error("Message transaction failure: {0}")] MessageTransaction(String), diff --git a/relayer/src/keyring/store.rs b/relayer/src/keyring/store.rs index 7deee972c6..dce105c7b4 100644 --- a/relayer/src/keyring/store.rs +++ b/relayer/src/keyring/store.rs @@ -68,8 +68,8 @@ impl KeyRingOperations for KeyRing { /// 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 key_json: Value = + serde_json::from_str(key_file_content).map_err(|e| Kind::InvalidKey.context(e))?; let signer: AccountId; let key: KeyEntry; diff --git a/relayer/src/tx/client.rs b/relayer/src/tx/client.rs index 87888d62bd..1fb78cd4c0 100644 --- a/relayer/src/tx/client.rs +++ b/relayer/src/tx/client.rs @@ -1,20 +1,19 @@ -use bitcoin::hashes::hex::ToHex; -use prost_types::Any; use std::convert::TryInto; +use std::str::FromStr; use std::time::Duration; -use ibc::downcast; -use ibc::ics02_client::client_def::{AnyClientState, AnyConsensusState, AnyHeader}; -use std::str::FromStr; +use bitcoin::hashes::hex::ToHex; +use prost_types::Any; use tendermint::account::Id as AccountId; use tendermint_light_client::types::TrustThreshold; +use tendermint_proto::DomainType; use ibc_proto::ibc::core::client::v1::MsgCreateClient as RawMsgCreateClient; use ibc_proto::ibc::core::client::v1::MsgUpdateClient as RawMsgUpdateClient; +use ibc::ics02_client::client_def::{AnyClientState, AnyConsensusState, AnyHeader}; use ibc::ics02_client::client_type::ClientType; -use ibc::ics02_client::height::Height; use ibc::ics02_client::msgs::create_client::MsgCreateAnyClient; use ibc::ics02_client::msgs::update_client::MsgUpdateAnyClient; use ibc::ics07_tendermint::header::Header as TendermintHeader; @@ -22,13 +21,12 @@ use ibc::ics24_host::identifier::{ChainId, ClientId}; use ibc::ics24_host::Path::ClientConsensusState; use ibc::ics24_host::Path::ClientState as ClientStatePath; use ibc::tx_msg::Msg; +use ibc::Height; use crate::chain::{Chain, CosmosSDKChain}; use crate::config::ChainConfig; use crate::error::{Error, Kind}; - use crate::keyring::store::{KeyEntry, KeyRingOperations}; -use tendermint_proto::DomainType; #[derive(Clone, Debug)] pub struct ClientOptions { @@ -36,68 +34,99 @@ pub struct ClientOptions { pub dest_chain_config: ChainConfig, pub src_chain_config: ChainConfig, pub signer_seed: String, - pub account_sequence: u64, } -pub fn create_client(opts: ClientOptions) -> Result, Error> { - // Get the source and destination chains. - let src_chain = CosmosSDKChain::from_config(opts.clone().src_chain_config)?; - let mut dest_chain = CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; - +pub fn build_create_client( + dest_chain: &mut CosmosSDKChain, + src_chain: &CosmosSDKChain, + dest_client_id: ClientId, + signer_seed: &str, +) -> Result { // Verify that the client has not been created already, i.e the destination chain does not // have a state for this client. - let client_state = dest_chain.query_client_state(&opts.dest_client_id, 0_u32.into(), false); + let client_state = dest_chain.query_client_state(&dest_client_id, Height::default()); if client_state.is_ok() { return Err(Into::::into(Kind::CreateClient( - opts.dest_client_id, + dest_client_id, "client already exists".into(), ))); } // Get the key and signer from key seed file. - let (key, signer) = dest_chain.key_and_signer(&opts.signer_seed)?; + let (key, signer) = dest_chain.key_and_signer(signer_seed)?; - // Build client create message with the data from the source chain at latest height. + // Build client create message with the data from source chain at latest height. let latest_height = src_chain.query_latest_height()?; - let new_msg = MsgCreateAnyClient::new( - opts.dest_client_id, + Ok(MsgCreateAnyClient::new( + dest_client_id, src_chain.build_client_state(latest_height)?, src_chain.build_consensus_state(latest_height)?, signer, ) .map_err(|e| { Kind::MessageTransaction("failed to build the create client message".into()).context(e) - })?; + })?) +} - let proto_msgs: Vec = vec![new_msg.to_any::()]; +pub fn build_create_client_and_send(opts: ClientOptions) -> Result { + // Get the source and destination chains. + let src_chain = &CosmosSDKChain::from_config(opts.clone().src_chain_config)?; + let dest_chain = &mut CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; - // Send the transaction to the destination chain - Ok(dest_chain.send(proto_msgs, key, "".to_string(), 0)?) + let new_msg = build_create_client( + dest_chain, + src_chain, + opts.dest_client_id, + &opts.signer_seed, + )?; + let (key, _) = dest_chain.key_and_signer(&opts.signer_seed)?; + + Ok(dest_chain.send( + vec![new_msg.to_any::()], + key, + "".to_string(), + 0, + )?) } -pub fn update_client(opts: ClientOptions) -> Result, Error> { - // Get the source and destination chains - let src_chain = CosmosSDKChain::from_config(opts.clone().src_chain_config)?; - let mut dest_chain = CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; - +pub fn build_update_client( + dest_chain: &mut CosmosSDKChain, + src_chain: &CosmosSDKChain, + dest_client_id: ClientId, + target_height: Height, + signer_seed: &str, +) -> Result, Error> { // Get the latest trusted height from the client state on destination. let trusted_height = dest_chain - .query_client_state(&opts.dest_client_id, 0_u32.into(), false)? + .query_client_state(&dest_client_id, Height::default())? .latest_height(); - // Set the target height to latest. - let target_height = src_chain.query_latest_height()?; - // Get the key and signer from key seed file. - let (key, signer) = dest_chain.key_and_signer(&opts.signer_seed)?; + let (key, signer) = dest_chain.key_and_signer(signer_seed)?; let new_msg = MsgUpdateAnyClient { - client_id: opts.dest_client_id, + client_id: dest_client_id, header: src_chain.build_header(trusted_height, target_height)?, signer, }; - let proto_msgs: Vec = vec![new_msg.to_any::()]; + Ok(vec![new_msg.to_any::()]) +} + +pub fn build_update_client_and_send(opts: ClientOptions) -> Result { + // Get the source and destination chains. + let src_chain = &CosmosSDKChain::from_config(opts.clone().src_chain_config)?; + let dest_chain = &mut CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; + + let target_height = src_chain.query_latest_height()?; + let new_msgs = build_update_client( + dest_chain, + src_chain, + opts.dest_client_id, + target_height, + &opts.signer_seed, + )?; + let (key, _) = dest_chain.key_and_signer(&opts.signer_seed)?; - Ok(dest_chain.send(proto_msgs, key, "".to_string(), 0)?) + Ok(dest_chain.send(new_msgs, key, "".to_string(), 0)?) } diff --git a/relayer/src/tx/connection.rs b/relayer/src/tx/connection.rs index 54297fd98a..821c7af7a7 100644 --- a/relayer/src/tx/connection.rs +++ b/relayer/src/tx/connection.rs @@ -1,23 +1,32 @@ +use std::convert::{TryFrom, TryInto}; +use std::str::FromStr; +use std::thread; +use std::time::Duration; + use prost_types::Any; use serde_json::Value; -use std::str::FromStr; use bitcoin::hashes::hex::ToHex; -use tendermint::account::Id as AccountId; -use tendermint_rpc::Id; +use ibc_proto::ibc::core::client::v1::MsgUpdateClient as RawMsgUpdateClient; +use ibc_proto::ibc::core::connection::v1::MsgConnectionOpenInit as RawMsgConnectionOpenInit; +use ibc_proto::ibc::core::connection::v1::MsgConnectionOpenTry as RawMsgConnectionOpenTry; -use ibc::ics03_connection::connection::Counterparty; +use ibc::ics03_connection::connection::{ConnectionEnd, Counterparty, State}; use ibc::ics03_connection::msgs::conn_open_init::MsgConnectionOpenInit; +use ibc::ics03_connection::msgs::conn_open_try::MsgConnectionOpenTry; +use ibc::ics03_connection::msgs::ConnectionMsgType; +use ibc::ics03_connection::version::get_compatible_versions; use ibc::ics23_commitment::commitment::CommitmentPrefix; use ibc::ics24_host::identifier::{ClientId, ConnectionId}; use ibc::tx_msg::Msg; -use ibc_proto::ibc::core::connection::v1::MsgConnectionOpenInit as RawMsgConnectionOpenInit; +use ibc::Height as ICSHeight; use crate::chain::{Chain, CosmosSDKChain}; use crate::config::ChainConfig; use crate::error::{Error, Kind}; use crate::keyring::store::{KeyEntry, KeyRingOperations}; +use crate::tx::client::{build_update_client, build_update_client_and_send, ClientOptions}; #[derive(Clone, Debug)] pub struct ConnectionOpenInitOptions { @@ -30,14 +39,14 @@ pub struct ConnectionOpenInitOptions { pub signer_seed: String, } -pub fn conn_init(opts: &ConnectionOpenInitOptions) -> Result, Error> { - // Get the source and destination chains - let src_chain = CosmosSDKChain::from_config(opts.src_chain_config.clone())?; - let mut dest_chain = CosmosSDKChain::from_config(opts.dest_chain_config.clone())?; - +pub fn build_conn_init( + dest_chain: &mut CosmosSDKChain, + src_chain: &CosmosSDKChain, + opts: &ConnectionOpenInitOptions, +) -> Result, Error> { // Check that the destination chain will accept the message, i.e. it does not have the connection if dest_chain - .query_connection(&opts.dest_connection_id, 0_u32.into(), false) + .query_connection(&opts.dest_connection_id, ICSHeight::default()) .is_ok() { return Err(Kind::ConnOpenInit( @@ -63,11 +72,162 @@ pub fn conn_init(opts: &ConnectionOpenInitOptions) -> Result, Error> { client_id: opts.dest_client_id.clone(), connection_id: opts.dest_connection_id.clone(), counterparty, - version: "".to_string(), + version: dest_chain.query_compatible_versions()?[0].clone(), + signer, + }; + + Ok(vec![new_msg.to_any::()]) +} + +pub fn build_conn_init_and_send(opts: &ConnectionOpenInitOptions) -> Result { + // Get the source and destination chains. + let src_chain = &CosmosSDKChain::from_config(opts.clone().src_chain_config)?; + let dest_chain = &mut CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; + + let new_msgs = build_conn_init(dest_chain, src_chain, opts)?; + let (key, _) = dest_chain.key_and_signer(&opts.signer_seed)?; + + Ok(dest_chain.send(new_msgs, key, "".to_string(), 0)?) +} + +#[derive(Clone, Debug)] +pub struct ConnectionOpenTryOptions { + pub dest_chain_config: ChainConfig, + pub src_chain_config: ChainConfig, + pub dest_client_id: ClientId, + pub src_client_id: ClientId, + pub dest_connection_id: ConnectionId, + pub src_connection_id: ConnectionId, + pub signer_seed: String, +} + +fn check_connection_state_for_try( + connection_id: ConnectionId, + existing_connection: ConnectionEnd, + expected_connection: ConnectionEnd, +) -> Result<(), Error> { + if existing_connection.client_id() != expected_connection.client_id() + || existing_connection.counterparty().client_id() + != expected_connection.counterparty().client_id() + || existing_connection.counterparty().connection_id().is_some() + && existing_connection.counterparty().connection_id() + != expected_connection.counterparty().connection_id() + { + Err(Kind::ConnOpenTry( + connection_id, + "connection already exist in an incompatible state".into(), + ) + .into()) + } else { + Ok(()) + } +} + +/// Attempts to send a MsgConnOpenTry to the dest_chain. +pub fn build_conn_try( + dest_chain: &mut CosmosSDKChain, + src_chain: &CosmosSDKChain, + opts: &ConnectionOpenTryOptions, +) -> Result, Error> { + // If there is a connection present on the destination chain it should look like this + let counterparty = Counterparty::new( + opts.src_client_id.clone(), + Some(opts.src_connection_id.clone()), + src_chain.query_commitment_prefix()?, + ); + let dest_expected_connection = ConnectionEnd::new( + State::Init, + opts.dest_client_id.clone(), + counterparty.clone(), + src_chain.query_compatible_versions()?, + ) + .unwrap(); + + // Check that if a connection exists on destination it is consistent with the try options + if let Ok(dest_connection) = + dest_chain.query_connection(&opts.dest_connection_id.clone(), ICSHeight::default()) + { + check_connection_state_for_try( + opts.dest_connection_id.clone(), + dest_connection, + dest_expected_connection, + )? + } + + let src_connection = src_chain + .query_connection(&opts.src_connection_id.clone(), ICSHeight::default()) + .map_err(|e| { + Kind::ConnOpenTry( + opts.src_connection_id.clone(), + "missing connection on source chain".to_string(), + ) + })?; + // TODO - check that the src connection is consistent with the try options + + // TODO - Build add send the message(s) for updating client on source (when we don't need the key seed anymore) + // TODO - add check if it is required + // let (key, signer) = src_chain.key_and_signer(&opts.signer_seed)?; + // build_update_client_and_send(ClientOptions { + // dest_client_id: opts.src_client_id.clone(), + // dest_chain_config: src_chain.config().clone(), + // src_chain_config: dest_chain.config().clone(), + // signer_seed: "".to_string(), + // })?; + + // Get the key and signer from key seed file + let (key, signer) = dest_chain.key_and_signer(&opts.signer_seed)?; + + // Build message(s) for updating client on destination + let ics_target_height = src_chain.query_latest_height()?; + + let mut msgs = build_update_client( + dest_chain, + src_chain, + opts.dest_client_id.clone(), + ics_target_height, + &opts.signer_seed, + )?; + + let client_state = src_chain.query_client_state(&opts.src_client_id, ics_target_height)?; + + let proofs = src_chain.build_connection_proofs( + ConnectionMsgType::OpenTry, + &opts.src_connection_id.clone(), + &opts.src_client_id, + ics_target_height, + )?; + + let counterparty_versions = if src_connection.versions().is_empty() { + src_chain.query_compatible_versions()? + } else { + src_connection.versions() + }; + + let new_msg = MsgConnectionOpenTry { + connection_id: opts.dest_connection_id.clone(), + client_id: opts.dest_client_id.clone(), + client_state: Some(client_state), + counterparty_chosen_connection_id: src_connection.counterparty().connection_id().cloned(), + counterparty, + counterparty_versions, + proofs, signer, }; - let proto_msgs: Vec = vec![new_msg.to_any::()]; + let mut new_msgs = vec![new_msg.to_any::()]; + + msgs.append(&mut new_msgs); + + Ok(msgs) +} + +pub fn build_conn_try_and_send(opts: ConnectionOpenTryOptions) -> Result { + // Get the source and destination chains. + let src_chain = &CosmosSDKChain::from_config(opts.src_chain_config.clone())?; + let dest_chain = &mut CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; + + let dest_msgs = build_conn_try(dest_chain, src_chain, &opts)?; + let (key, _) = dest_chain.key_and_signer(&opts.signer_seed)?; - Ok(dest_chain.send(proto_msgs, key, "".to_string(), 0)?) + Ok(dest_chain.send(dest_msgs, key, "".to_string(), 0)?) }