diff --git a/relayer-cli/Cargo.toml b/relayer-cli/Cargo.toml index 2e12a87db8..babea3d2e5 100644 --- a/relayer-cli/Cargo.toml +++ b/relayer-cli/Cargo.toml @@ -21,6 +21,8 @@ tokio = { version = "0.2.13", features = ["rt-util", "sync"] } tracing = "0.1.13" tracing-subscriber = "0.2.3" futures = "0.3.5" +prost = "0.6.1" +prost-types = { version = "0.6.1" } [dependencies.abscissa_core] version = "0.5.2" diff --git a/relayer-cli/src/commands.rs b/relayer-cli/src/commands.rs index 4fe335c3ed..8394fef90f 100644 --- a/relayer-cli/src/commands.rs +++ b/relayer-cli/src/commands.rs @@ -10,6 +10,7 @@ mod light; mod listen; mod query; mod start; +mod tx; mod utils; mod version; @@ -18,6 +19,7 @@ use self::{ version::VersionCmd, }; +use crate::commands::tx::TxCmd; use crate::config::Config; use abscissa_core::{Command, Configurable, FrameworkError, Help, Options, Runnable}; use std::path::PathBuf; @@ -52,6 +54,10 @@ pub enum CliCmd { #[options(help = "query state from chain")] Query(QueryCmd), + /// The `tx` subcommand + #[options(help = "create IBC transactions on configured chains")] + Tx(TxCmd), + /// The `light` subcommand #[options(help = "basic functionality for managing the lite clients")] Light(LightCmd), diff --git a/relayer-cli/src/commands/tx.rs b/relayer-cli/src/commands/tx.rs new file mode 100644 index 0000000000..d766abf169 --- /dev/null +++ b/relayer-cli/src/commands/tx.rs @@ -0,0 +1,20 @@ +//! `tx` subcommand +use crate::commands::tx::client::TxCreateClientCmd; +use abscissa_core::{Command, Options, Runnable}; + +mod client; + +/// `tx` subcommand +#[derive(Command, Debug, Options, Runnable)] +pub enum TxCmd { + /// The `tx raw` subcommand submits IBC transactions to a chain + #[options(help = "tx raw")] + Raw(TxRawCommands), +} + +#[derive(Command, Debug, Options, Runnable)] +pub enum TxRawCommands { + /// The `tx raw client-create` subcommand submits a MsgCreateClient in a transaction to a chain + #[options(help = "tx raw create-client")] + CreateClient(TxCreateClientCmd), +} diff --git a/relayer-cli/src/commands/tx/client.rs b/relayer-cli/src/commands/tx/client.rs new file mode 100644 index 0000000000..9f3fdf77f6 --- /dev/null +++ b/relayer-cli/src/commands/tx/client.rs @@ -0,0 +1,83 @@ +use abscissa_core::{Command, Options, Runnable}; +use relayer::config::Config; +use relayer::tx::client::{create_client, CreateClientOptions}; + +use crate::application::app_config; +use crate::error::{Error, Kind}; +use crate::prelude::*; + +#[derive(Clone, Command, Debug, Options)] +pub struct TxCreateClientCmd { + #[options(free, help = "identifier of the destination chain")] + dest_chain_id: Option, + + #[options(free, help = "identifier of the source chain")] + src_chain_id: Option, + + #[options( + free, + help = "identifier of the client to be created on destination chain" + )] + dest_client_id: Option, +} + +impl TxCreateClientCmd { + fn validate_options(&self, config: &Config) -> Result { + let dest_chain_id = self + .dest_chain_id + .clone() + .ok_or_else(|| "missing destination chain identifier".to_string())?; + + let dest_chain_config = config + .chains + .iter() + .find(|c| c.id == dest_chain_id.parse().unwrap()) + .ok_or_else(|| "missing destination chain configuration".to_string())?; + + let src_chain_id = self + .src_chain_id + .clone() + .ok_or_else(|| "missing source chain identifier".to_string())?; + + let src_chain_config = config + .chains + .iter() + .find(|c| c.id == src_chain_id.parse().unwrap()) + .ok_or_else(|| "missing source chain configuration".to_string())?; + + let dest_client_id = self + .dest_client_id + .as_ref() + .ok_or_else(|| "missing destination client identifier".to_string())? + .parse() + .map_err(|_| "bad client identifier".to_string())?; + + Ok(CreateClientOptions { + dest_client_id, + dest_chain_config: dest_chain_config.clone(), + src_chain_config: src_chain_config.clone(), + }) + } +} + +impl Runnable for TxCreateClientCmd { + 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<(), Error> = create_client(opts).map_err(|e| Kind::Tx.context(e).into()); + + match res { + Ok(receipt) => status_info!("client created, result: ", "{:?}", receipt), + Err(e) => status_info!("client create failed, error: ", "{}", e), + } + } +} diff --git a/relayer-cli/src/error.rs b/relayer-cli/src/error.rs index 870541bf8a..1ca66be639 100644 --- a/relayer-cli/src/error.rs +++ b/relayer-cli/src/error.rs @@ -20,6 +20,10 @@ pub enum Kind { /// Error during network query #[error("query error")] Query, + + /// Error during transaction submission + #[error("tx error")] + Tx, } impl Kind { diff --git a/relayer/Cargo.toml b/relayer/Cargo.toml index 1365a12d80..9ec48ecfdc 100644 --- a/relayer/Cargo.toml +++ b/relayer/Cargo.toml @@ -25,5 +25,6 @@ tokio = "0.2" serde_json = { version = "1" } bytes = "0.5.6" prost = "0.6.1" +prost-types = { version = "0.6.1" } [dev-dependencies] diff --git a/relayer/src/chain.rs b/relayer/src/chain.rs index cb3f633722..c666176ba0 100644 --- a/relayer/src/chain.rs +++ b/relayer/src/chain.rs @@ -15,8 +15,9 @@ use crate::config::ChainConfig; use crate::error; use std::error::Error; -mod cosmos; +pub(crate) mod cosmos; pub use cosmos::CosmosSDKChain; +use prost_types::Any; /// Handy type alias for the type of validator set associated with a chain pub type ValidatorSet = <::Commit as tmlite::Commit>::ValidatorSet; @@ -44,6 +45,9 @@ pub trait Chain { /// Perform a generic `query`, and return the corresponding response data. fn query(&self, data: Path, height: u64, prove: bool) -> Result, Self::Error>; + /// send a transaction with `msgs` to chain. + fn send(&self, _msgs: &[Any]) -> Result<(), Self::Error>; + /// Returns the chain's identifier fn id(&self) -> &ChainId { &self.config().id @@ -61,6 +65,10 @@ pub trait Chain { /// The trusting period configured for this chain fn trusting_period(&self) -> Duration; + /// The unbonding period of this chain + /// TODO - this is a GRPC query, needs to be implemented + fn unbonding_period(&self) -> Duration; + /// The trust threshold configured for this chain fn trust_threshold(&self) -> TrustThresholdFraction; } @@ -85,6 +93,17 @@ pub async fn query_latest_height(chain: &impl Chain) -> Result( + chain: &C, +) -> Result, error::Error> +where + C: Chain, +{ + let h = query_latest_height(chain).await?; + Ok(query_header_at_height::(chain, h).await?) +} + /// Query a header at the given height via the RPC requester pub async fn query_header_at_height( chain: &C, diff --git a/relayer/src/chain/cosmos.rs b/relayer/src/chain/cosmos.rs index 08c82a259e..a6a2301b53 100644 --- a/relayer/src/chain/cosmos.rs +++ b/relayer/src/chain/cosmos.rs @@ -1,3 +1,5 @@ +use prost_types::Any; +use std::str::FromStr; use std::time::Duration; use tendermint::abci::Path as TendermintABCIPath; @@ -17,7 +19,6 @@ use crate::config::ChainConfig; use crate::error::{Error, Kind}; use super::Chain; -use std::str::FromStr; pub struct CosmosSDKChain { config: ChainConfig, @@ -64,6 +65,12 @@ impl Chain for CosmosSDKChain { Ok(response) } + /// Send a transaction that includes the specified messages + fn send(&self, _msgs: &[Any]) -> Result<(), Error> { + // TODO sign and broadcast_tx + Ok(()) + } + fn config(&self) -> &ChainConfig { &self.config } @@ -80,6 +87,11 @@ impl Chain for CosmosSDKChain { self.config.trusting_period } + fn unbonding_period(&self) -> Duration { + // TODO - query chain + Duration::from_secs(24 * 7 * 3) + } + fn trust_threshold(&self) -> TrustThresholdFraction { TrustThresholdFraction::default() } diff --git a/relayer/src/error.rs b/relayer/src/error.rs index 1f3ac6a191..6a14c71086 100644 --- a/relayer/src/error.rs +++ b/relayer/src/error.rs @@ -1,6 +1,7 @@ //! This module defines the various errors that be raised in the relayer. use anomaly::{BoxError, Context}; +use ibc::ics24_host::identifier::ClientId; use thiserror::Error; /// An error that can be raised by the relayer. @@ -40,6 +41,10 @@ pub enum Kind { /// Response does not contain data #[error("Empty response value")] EmptyResponseValue, + + /// Create client failure + #[error("Failed to create client {0}: {1}")] + CreateClient(ClientId, String), } impl Kind { diff --git a/relayer/src/lib.rs b/relayer/src/lib.rs index ea2e5c7797..e6f035adec 100644 --- a/relayer/src/lib.rs +++ b/relayer/src/lib.rs @@ -18,4 +18,5 @@ pub mod error; pub mod event_handler; pub mod event_monitor; pub mod store; +pub mod tx; pub mod util; diff --git a/relayer/src/tx.rs b/relayer/src/tx.rs new file mode 100644 index 0000000000..b9babe5bc1 --- /dev/null +++ b/relayer/src/tx.rs @@ -0,0 +1 @@ +pub mod client; diff --git a/relayer/src/tx/client.rs b/relayer/src/tx/client.rs new file mode 100644 index 0000000000..90d4a2ed4b --- /dev/null +++ b/relayer/src/tx/client.rs @@ -0,0 +1,95 @@ +use prost_types::Any; +use std::time::Duration; + +use ibc::ics02_client::client_def::{AnyClientState, AnyConsensusState}; +use ibc::ics02_client::client_type::ClientType; +use ibc::ics02_client::msgs::MsgCreateAnyClient; +use ibc::ics24_host::identifier::ClientId; +use ibc::ics24_host::Path::ClientState as ClientStatePath; +use ibc::tx_msg::Msg; + +use tendermint::block::Height; + +use crate::chain::cosmos::block_on; +use crate::chain::{query_latest_header, Chain, CosmosSDKChain}; +use crate::config::ChainConfig; +use crate::error::{Error, Kind}; + +#[derive(Clone, Debug)] +pub struct CreateClientOptions { + pub dest_client_id: ClientId, + pub dest_chain_config: ChainConfig, + pub src_chain_config: ChainConfig, +} + +pub fn create_client(opts: CreateClientOptions) -> Result<(), Error> { + // Ger the destination + let dest_chain = CosmosSDKChain::from_config(opts.clone().dest_chain_config)?; + + // Query the client state on destination chain. + if dest_chain + .query(ClientStatePath(opts.clone().dest_client_id), 0, false) + .is_ok() + { + return Err(Into::::into(Kind::CreateClient( + opts.dest_client_id, + "client already exists".into(), + ))); + } + + // Get the latest header from the source chain and build the consensus state. + let src_chain = CosmosSDKChain::from_config(opts.clone().src_chain_config)?; + let tm_consensus_state = block_on(query_latest_header::(&src_chain)) + .map_err(|e| { + Kind::CreateClient( + opts.dest_client_id.clone(), + "failed to get the latest header".into(), + ) + .context(e) + }) + .map(ibc::ics07_tendermint::consensus_state::ConsensusState::from)?; + + let any_consensus_state = AnyConsensusState::Tendermint(tm_consensus_state.clone()); + + // Build the client state. + let any_client_state = ibc::ics07_tendermint::client_state::ClientState::new( + src_chain.id().to_string(), + src_chain.trusting_period(), + src_chain.unbonding_period(), + Duration::from_millis(3000), + tm_consensus_state.height, + Height(0), + ) + .map_err(|e| { + Kind::CreateClient( + opts.dest_client_id.clone(), + "failed to build the client state".into(), + ) + .context(e) + }) + .map(AnyClientState::Tendermint)?; + + let signer = dest_chain.config().account_prefix.parse().map_err(|e| { + Kind::CreateClient(opts.dest_client_id.clone(), "bad signer".into()).context(e) + })?; + + // Build the domain type message + let new_msg = MsgCreateAnyClient::new( + opts.dest_client_id, + ClientType::Tendermint, + any_client_state, + any_consensus_state, + signer, + ); + + // 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(), + }; + + proto_msgs.push(any_msg); + dest_chain.send(&proto_msgs) +}