diff --git a/relayer/cli/Cargo.toml b/relayer/cli/Cargo.toml index 2ab37e9e83..82930288e6 100644 --- a/relayer/cli/Cargo.toml +++ b/relayer/cli/Cargo.toml @@ -9,7 +9,8 @@ authors = [ [dependencies] relayer = { path = "../relay" } -tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git" } +relayer-modules = { path = "../../modules" } +tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "tendermint/v0.33" } anomaly = "0.2.0" gumdrop = "0.7" diff --git a/relayer/cli/src/commands.rs b/relayer/cli/src/commands.rs index f1f7374a75..808ebc9d9e 100644 --- a/relayer/cli/src/commands.rs +++ b/relayer/cli/src/commands.rs @@ -7,10 +7,14 @@ mod config; mod light; +mod query; mod start; +mod utils; mod version; -use self::{config::ConfigCmd, light::LightCmd, start::StartCmd, version::VersionCmd}; +use self::{ + config::ConfigCmd, light::LightCmd, query::QueryCmd, start::StartCmd, version::VersionCmd, +}; use crate::config::Config; use abscissa_core::{Command, Configurable, FrameworkError, Help, Options, Runnable}; @@ -26,10 +30,6 @@ pub enum CliCmd { #[options(help = "get usage information")] Help(Help), - /// The `version` subcommand - #[options(help = "display version information")] - Version(VersionCmd), - /// The `start` subcommand #[options(help = "start the relayer")] Start(StartCmd), @@ -38,6 +38,14 @@ pub enum CliCmd { #[options(help = "manipulate the relayer configuration")] Config(ConfigCmd), + /// The `version` subcommand + #[options(help = "display version information")] + Version(VersionCmd), + + /// The `query` subcommand + #[options(help = "query state from chain")] + Query(QueryCmd), + /// The `light` subcommand #[options(help = "basic functionality for managing the lite clients")] Light(LightCmd), diff --git a/relayer/cli/src/commands/query.rs b/relayer/cli/src/commands/query.rs new file mode 100644 index 0000000000..5ecb5cf5d1 --- /dev/null +++ b/relayer/cli/src/commands/query.rs @@ -0,0 +1,23 @@ +//! `query` subcommand + +use abscissa_core::{Command, Options, Runnable}; + +mod query_client; + +/// `query` subcommand +#[derive(Command, Debug, Options, Runnable)] +pub enum QueryCmd { + /// The `query client` subcommand + #[options(help = "query client")] + Client(QueryClientCmds), +} + +#[derive(Command, Debug, Options, Runnable)] +pub enum QueryClientCmds { + /// The `query client` subcommand + #[options(help = "query client state")] + State(query_client::QueryClientStateCmd), + + #[options(help = "query client consensus")] + Consensus(query_client::QueryClientConsensusCmd), +} diff --git a/relayer/cli/src/commands/query/query_client.rs b/relayer/cli/src/commands/query/query_client.rs new file mode 100644 index 0000000000..ad16087b96 --- /dev/null +++ b/relayer/cli/src/commands/query/query_client.rs @@ -0,0 +1,216 @@ +use crate::prelude::*; + +use abscissa_core::{Command, Options, Runnable}; +use relayer::config::{ChainConfig, Config}; +use relayer::query::client_consensus_state::query_client_consensus_state; +use relayer::query::client_state::query_client_full_state; + +use relayer_modules::ics24_host::client::ClientId; + +use crate::commands::utils::block_on; +use relayer::chain::tendermint::TendermintChain; +use tendermint::chain::Id as ChainId; + +#[derive(Command, Debug, Options)] +pub struct QueryClientStateCmd { + #[options(free, help = "identifier of the chain to query")] + chain_id: Option, + + #[options(free, help = "identifier of the client to query")] + client_id: Option, + + #[options(help = "height of the state to query", short = "h")] + height: Option, + + #[options(help = "whether proof is required", short = "p")] + proof: Option, +} + +#[derive(Debug)] +struct QueryClientStateOptions { + client_id: ClientId, + height: u64, + proof: bool, +} + +impl QueryClientStateCmd { + fn validate_options( + &self, + config: &Config, + ) -> Result<(ChainConfig, QueryClientStateOptions), String> { + let (chain_config, client_id) = + validate_common_options(&self.chain_id, &self.client_id, config)?; + let opts = QueryClientStateOptions { + client_id: client_id.parse().unwrap(), + height: match self.height { + Some(h) => h, + None => 0 as u64, + }, + proof: match self.proof { + Some(proof) => proof, + None => true, + }, + }; + Ok((chain_config.clone(), opts)) + } +} + +impl Runnable for QueryClientStateCmd { + fn run(&self) { + let config = app_config(); + + let (chain_config, opts) = match self.validate_options(&config) { + Err(err) => { + status_err!("invalid options: {}", err); + return; + } + Ok(result) => result, + }; + status_info!("Options", "{:?}", opts); + + // run with proof: + // cargo run --bin relayer -- -c simple_config.toml query client state ibc0 ibconeclient + // + // run without proof: + // cargo run --bin relayer -- -c simple_config.toml query client state ibc0 ibconeclient -p false + // + // Note: currently both fail in amino_unmarshal_binary_length_prefixed(). + // To test this start a Gaia node and configure a client using the go relayer. + let chain = TendermintChain::from_config(chain_config).unwrap(); + let res = block_on(query_client_full_state( + &chain, + opts.height, + opts.client_id.clone(), + opts.proof, + )); + match res { + Ok(cs) => status_info!("client state query result: ", "{:?}", cs.client_state), + Err(e) => status_info!("client state query error: ", "{:?}", e), + } + } +} + +#[derive(Command, Debug, Options)] +pub struct QueryClientConsensusCmd { + #[options(free, help = "identifier of the chain to query")] + chain_id: Option, + + #[options(free, help = "identifier of the client to query")] + client_id: Option, + + #[options(free, help = "height of the consensus state to query")] + consensus_height: Option, + + #[options(help = "height of the consensus state to query", short = "h")] + height: Option, + + #[options(help = "whether proof is required", short = "p")] + proof: Option, +} + +#[derive(Debug)] +struct QueryClientConsensusOptions { + client_id: ClientId, + consensus_height: u64, + height: u64, + proof: bool, +} + +impl QueryClientConsensusCmd { + fn validate_options( + &self, + config: &Config, + ) -> Result<(ChainConfig, QueryClientConsensusOptions), String> { + let (chain_config, client_id) = + validate_common_options(&self.chain_id, &self.client_id, config)?; + + match self.consensus_height { + Some(consensus_height) => { + let opts = QueryClientConsensusOptions { + client_id: client_id.parse().unwrap(), + consensus_height, + height: match self.height { + Some(h) => h, + None => 0 as u64, + }, + proof: match self.proof { + Some(proof) => proof, + None => true, + }, + }; + Ok((chain_config.clone(), opts)) + } + None => Err("missing client consensus height".to_string()), + } + } +} + +impl Runnable for QueryClientConsensusCmd { + fn run(&self) { + let config = app_config(); + + let (chain_config, opts) = match self.validate_options(&config) { + Err(err) => { + status_err!("invalid options: {}", err); + return; + } + Ok(result) => result, + }; + status_info!("Options", "{:?}", opts); + + // run with proof: + // cargo run --bin relayer -- -c simple_config.toml query client consensus ibc0 ibconeclient 22 + // + // run without proof: + // cargo run --bin relayer -- -c simple_config.toml query client consensus ibc0 ibconeclient 22 -p false + // + // Note: currently both fail in amino_unmarshal_binary_length_prefixed(). + // To test this start a Gaia node and configure a client using the go relayer. + let chain = TendermintChain::from_config(chain_config).unwrap(); + let res = block_on(query_client_consensus_state( + &chain, + opts.height, + opts.client_id, + opts.consensus_height, + opts.proof, + )); + match res { + Ok(cs) => status_info!( + "client consensus state query result: ", + "{:?}", + cs.consensus_state + ), + Err(e) => status_info!("client consensus state query error: ", "{:?}", e), + } + } +} + +fn validate_common_options( + chain_id: &Option, + client_id: &Option, + config: &Config, +) -> Result<(ChainConfig, String), String> { + match (&chain_id, &client_id) { + (Some(chain_id), Some(client_id)) => { + let chain_config = config.chains.iter().find(|c| c.id == *chain_id); + + match chain_config { + Some(chain_config) => { + // check that the client_id is specified in one of the chain configurations + match config + .chains + .iter() + .find(|c| c.client_ids.contains(client_id)) + { + Some(_) => Ok((chain_config.clone(), client_id.parse().unwrap())), + None => Err(format!("cannot find client {} in config", client_id)), + } + } + None => Err(format!("cannot find chain {} in config", chain_id)), + } + } + + (None, _) => Err("missing chain identifier".to_string()), + (_, None) => Err("missing client identifier".to_string()), + } +} diff --git a/relayer/cli/src/commands/start.rs b/relayer/cli/src/commands/start.rs index 275da47160..59d7b07497 100644 --- a/relayer/cli/src/commands/start.rs +++ b/relayer/cli/src/commands/start.rs @@ -1,4 +1,3 @@ -use std::future::Future; use std::time::{Duration, SystemTime}; // use crate::application::APPLICATION; @@ -9,6 +8,7 @@ use abscissa_core::{Command, Options, Runnable}; use tendermint::lite::types::Header; +use crate::commands::utils::block_on; use relayer::chain::tendermint::TendermintChain; use relayer::chain::Chain; use relayer::client::Client; @@ -98,12 +98,3 @@ async fn create_client( Client::new(chain, store, trust_options).await.unwrap() } - -fn block_on(future: F) -> F::Output { - tokio::runtime::Builder::new() - .basic_scheduler() - .enable_all() - .build() - .unwrap() - .block_on(future) -} diff --git a/relayer/cli/src/commands/utils.rs b/relayer/cli/src/commands/utils.rs new file mode 100644 index 0000000000..c2c0d29130 --- /dev/null +++ b/relayer/cli/src/commands/utils.rs @@ -0,0 +1,11 @@ +use core::future::Future; + +/// block on future +pub fn block_on(future: F) -> F::Output { + tokio::runtime::Builder::new() + .basic_scheduler() + .enable_all() + .build() + .unwrap() + .block_on(future) +} diff --git a/relayer/relay/Cargo.toml b/relayer/relay/Cargo.toml index bb9d1f32af..ce46d31f04 100644 --- a/relayer/relay/Cargo.toml +++ b/relayer/relay/Cargo.toml @@ -9,8 +9,7 @@ authors = [ [dependencies] relayer-modules = { path = "../../modules" } -tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git" } - +tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "tendermint/v0.33" } anomaly = "0.2.0" async-trait = "0.1.24" humantime-serde = "1.0.0" diff --git a/relayer/relay/src/chain.rs b/relayer/relay/src/chain.rs index 4909a0791a..58bab625f9 100644 --- a/relayer/relay/src/chain.rs +++ b/relayer/relay/src/chain.rs @@ -8,7 +8,7 @@ use ::tendermint::lite::types as tmlite; use ::tendermint::lite::{self, Height, TrustThresholdFraction}; use ::tendermint::rpc::Client as RpcClient; -use relayer_modules::ics02_client::state::ConsensusState; +use relayer_modules::ics02_client::state::{ClientState, ConsensusState}; use crate::config::ChainConfig; use crate::error; @@ -28,6 +28,8 @@ pub trait Chain { /// Type of consensus state for this chain type ConsensusState: ConsensusState + Serialize + DeserializeOwned; + type Type; + type ClientState: ClientState; /// Type of RPC requester (wrapper around low-level RPC client) for this chain type Requester: tmlite::Requester; diff --git a/relayer/relay/src/chain/tendermint.rs b/relayer/relay/src/chain/tendermint.rs index 4484e4833c..7a23e862e7 100644 --- a/relayer/relay/src/chain/tendermint.rs +++ b/relayer/relay/src/chain/tendermint.rs @@ -5,6 +5,8 @@ use tendermint::block::Header as TMHeader; use tendermint::lite::TrustThresholdFraction; use tendermint::rpc::Client as RpcClient; +use relayer_modules::ics02_client::client_type::ClientType; +use relayer_modules::ics07_tendermint::client_state::ClientState; use relayer_modules::ics07_tendermint::consensus_state::ConsensusState; use crate::client::rpc_requester::RpcRequester; @@ -34,10 +36,12 @@ impl TendermintChain { } impl Chain for TendermintChain { + type Type = ClientType; type Header = TMHeader; type Commit = TMCommit; type ConsensusState = ConsensusState; type Requester = RpcRequester; + type ClientState = ClientState; fn config(&self) -> &ChainConfig { &self.config diff --git a/relayer/relay/src/query.rs b/relayer/relay/src/query.rs index e9cdf62057..16fd19236b 100644 --- a/relayer/relay/src/query.rs +++ b/relayer/relay/src/query.rs @@ -7,6 +7,7 @@ use crate::chain::Chain; use crate::error; pub mod client_consensus_state; +pub mod client_state; /// The type of IBC response sent back for a given IBC `Query`. pub trait IbcResponse: Sized { @@ -72,5 +73,5 @@ where /// is_query_store_with_proofxpects a format like ///, /// where queryType must be "store" and subpath must be "key" to require a proof. fn is_query_store_with_proof(_path: &abci::Path) -> bool { - todo!() + false } diff --git a/relayer/relay/src/query/client_consensus_state.rs b/relayer/relay/src/query/client_consensus_state.rs index c2209a24b5..3a2a2c6ee9 100644 --- a/relayer/relay/src/query/client_consensus_state.rs +++ b/relayer/relay/src/query/client_consensus_state.rs @@ -18,16 +18,23 @@ pub struct QueryClientConsensusState { pub client_id: ClientId, pub consensus_height: Height, pub consensus_state_path: ConsensusStatePath, + pub prove: bool, marker: PhantomData, } impl QueryClientConsensusState { - pub fn new(chain_height: Height, client_id: ClientId, consensus_height: Height) -> Self { + pub fn new( + chain_height: Height, + client_id: ClientId, + consensus_height: Height, + prove: bool, + ) -> Self { Self { chain_height, client_id: client_id.clone(), consensus_height, consensus_state_path: ConsensusStatePath::new(client_id, consensus_height), + prove, marker: PhantomData, } } @@ -48,7 +55,7 @@ where } fn prove(&self) -> bool { - true + self.prove } fn data(&self) -> Vec { @@ -58,7 +65,7 @@ where pub struct ConsensusStateResponse { pub consensus_state: CS, - pub proof: CommitmentProof, + pub proof: Option, pub proof_path: CommitmentPath, pub proof_height: Height, } @@ -67,17 +74,15 @@ impl ConsensusStateResponse { pub fn new( client_id: ClientId, consensus_state: CS, - abci_proof: abci::Proof, + abci_proof: Option, proof_height: Height, ) -> Self { - let proof = CommitmentProof::from_bytes(abci_proof.as_ref()); - let proof_path = CommitmentPath::from_path(ConsensusStatePath::new(client_id, proof_height)); ConsensusStateResponse { consensus_state, - proof, + proof: abci_proof, proof_path, proof_height, } @@ -92,18 +97,18 @@ where query: QueryClientConsensusState, response: AbciQuery, ) -> Result { - match (response.value, response.proof) { - (Some(value), Some(proof)) => { + match (response.value, &response.proof) { + (Some(value), _) => { let consensus_state = amino_unmarshal_binary_length_prefixed(&value)?; Ok(ConsensusStateResponse::new( query.client_id, consensus_state, - proof, + response.proof, response.height.into(), )) } - _ => todo!(), + (None, _) => Err(error::Kind::Rpc.context("Bad response").into()), } } } @@ -113,12 +118,12 @@ pub async fn query_client_consensus_state( chain_height: Height, client_id: ClientId, consensus_height: Height, + prove: bool, ) -> Result, error::Error> where C: Chain, { - let query = QueryClientConsensusState::new(chain_height, client_id, consensus_height); - + let query = QueryClientConsensusState::new(chain_height, client_id, consensus_height, prove); ibc_query(chain, query).await } diff --git a/relayer/relay/src/query/client_state.rs b/relayer/relay/src/query/client_state.rs new file mode 100644 index 0000000000..f0f3d4b5af --- /dev/null +++ b/relayer/relay/src/query/client_state.rs @@ -0,0 +1,123 @@ +use std::marker::PhantomData; +use tendermint::rpc::endpoint::abci_query::AbciQuery; + +use tendermint::abci; + +use relayer_modules::ics02_client::state::ClientState; +use relayer_modules::ics23_commitment::{CommitmentPath, CommitmentProof}; +use relayer_modules::ics24_host::client::ClientId; +use relayer_modules::path::{ClientStatePath, Path}; +use relayer_modules::Height; + +use super::{ibc_query, IbcQuery, IbcResponse}; +use crate::chain::Chain; +use crate::error; + +pub struct QueryClientFullState { + pub chain_height: Height, + pub client_id: ClientId, + pub client_state_path: ClientStatePath, + pub prove: bool, + marker: PhantomData, +} + +impl QueryClientFullState { + pub fn new(chain_height: Height, client_id: ClientId, prove: bool) -> Self { + Self { + chain_height, + client_id: client_id.clone(), + client_state_path: ClientStatePath::new(client_id), + prove, + marker: PhantomData, + } + } +} + +impl IbcQuery for QueryClientFullState +where + CLS: ClientState, +{ + type Response = ClientFullStateResponse; + + fn path(&self) -> abci::Path { + "/store/ibc/key".parse().unwrap() + } + + fn height(&self) -> Height { + self.chain_height + } + + fn prove(&self) -> bool { + self.prove + } + + fn data(&self) -> Vec { + self.client_state_path.to_key().into() + } +} + +pub struct ClientFullStateResponse { + pub client_state: CLS, + pub proof: Option, + pub proof_path: CommitmentPath, + pub proof_height: Height, +} + +impl ClientFullStateResponse { + pub fn new( + client_id: ClientId, + client_state: CLS, + abci_proof: Option, + proof_height: Height, + ) -> Self { + let proof_path = CommitmentPath::from_path(ClientStatePath::new(client_id)); + + ClientFullStateResponse { + client_state, + proof: abci_proof, + proof_path, + proof_height, + } + } +} + +impl IbcResponse> for ClientFullStateResponse +where + CLS: ClientState, +{ + fn from_abci_response( + query: QueryClientFullState, + response: AbciQuery, + ) -> Result { + match (response.value, &response.proof) { + (Some(value), _) => { + let client_state = amino_unmarshal_binary_length_prefixed(&value)?; + + Ok(ClientFullStateResponse::new( + query.client_id, + client_state, + response.proof, + response.height.into(), + )) + } + (None, _) => Err(error::Kind::Rpc.context("Bad response").into()), + } + } +} + +pub async fn query_client_full_state( + chain: &C, + chain_height: Height, + client_id: ClientId, + prove: bool, +) -> Result, error::Error> +where + C: Chain, +{ + let query = QueryClientFullState::new(chain_height, client_id, prove); + ibc_query(chain, query).await +} + +fn amino_unmarshal_binary_length_prefixed(_bytes: &[u8]) -> Result { + todo!() +}