From 8247868abd6a11b92c83a0d7bd00956096ec50f4 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 23 Mar 2021 21:19:50 +0100 Subject: [PATCH] Hermes CLI for upgrading client (#723) * Added domain type def. Also cleaned-up the documentation, which looked very sloppy. https://docs.rs/ibc/0.1.1/ibc/ics02_client/msgs/index.html * Added partial handler & command * Added upgrade proto files. Added Cargo.lock for proto-compiler * Prep for query_upgraded_client_state * Method & support for querying ugpraded client state * Support for querying the upgraded consensus state * Minor dev scripts enhancements & bugs * Added guide. Uses patched Go relayer * Proto conversion for upgrade msg. Refactored upgrade() impl * Fix missing signer bug * Update msg bf. upgrade. Event parsing * Changelog. Revised guide * Aesthetic nits based on file review * Clarifications in the test instructions * Documented Go relayer version in testing instructions * Possible fix for #734 * changelog & method documentation * Apply suggestions from code review Co-authored-by: Romain Ruetschi * Added more derived trait bounds on module events * Adapt to newer Ics02 structure. * Added Protobuf impl for MsgUpgradeAnyClient * FMT * Added upgrade-chain CLI and updated instructions * Remove a clone and turn zero_custom_fields into a static method * Whitespace and nitpick * Remove obsolete TODO Co-authored-by: Romain Ruetschi Co-authored-by: Anca Zamfir --- CHANGELOG.md | 5 +- guide/src/upgrade_test.md | 166 +++++++++++++++++++++++++ relayer-cli/src/commands/tx.rs | 11 +- relayer-cli/src/commands/tx/client.rs | 52 +++++++- relayer-cli/src/commands/tx/upgrade.rs | 101 +++++++++++++++ relayer/src/chain.rs | 10 ++ relayer/src/chain/cosmos.rs | 132 +++++++++++++++++++- relayer/src/chain/handle.rs | 20 +++ relayer/src/chain/handle/prod.rs | 14 +++ relayer/src/chain/mock.rs | 14 +++ relayer/src/chain/runtime.rs | 42 +++++++ relayer/src/error.rs | 4 + relayer/src/foreign_client.rs | 106 +++++++++++++++- relayer/src/lib.rs | 1 + relayer/src/upgrade_chain.rs | 120 ++++++++++++++++++ scripts/dev-env | 4 +- scripts/init-clients | 4 +- scripts/one-chain | 4 + scripts/setup-chains | 2 - 19 files changed, 801 insertions(+), 11 deletions(-) create mode 100644 guide/src/upgrade_test.md create mode 100644 relayer-cli/src/commands/tx/upgrade.rs create mode 100644 relayer/src/upgrade_chain.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 40aa2552a7..afb5cf1979 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - [ibc-relayer-cli] - Added `create connection` CLI ([#630]) - Proposed ADR 006 to describe Hermes v0.2.0 use-cases ([#637]) + - Added `client-upgrade` CLI ([#357]) - Update gaia to version 4.1.0 for e2e tests on CI ([#702]) ### IMPROVEMENTS @@ -33,7 +34,7 @@ - [nothing yet] - [ibc-relayer-cli] - - [nothing yet] + - Clarified success path for updating a client that is already up-to-date ([#734]) ### BUG FIXES @@ -61,6 +62,7 @@ - [nothing yet] [#352]: https://github.com/informalsystems/ibc-rs/issues/352 +[#357]: https://github.com/informalsystems/ibc-rs/issues/357 [#416]: https://github.com/informalsystems/ibc-rs/issues/416 [#561]: https://github.com/informalsystems/ibc-rs/issues/561 [#599]: https://github.com/informalsystems/ibc-rs/issues/599 @@ -72,6 +74,7 @@ [#699]: https://github.com/informalsystems/ibc-rs/issues/699 [#700]: https://github.com/informalsystems/ibc-rs/pull/700 [#702]: https://github.com/informalsystems/ibc-rs/issues/702 +[#734]: https://github.com/informalsystems/ibc-rs/issues/734 [#736]: https://github.com/informalsystems/ibc-rs/issues/736 [#740]: https://github.com/informalsystems/ibc-rs/issues/740 [#752]: https://github.com/informalsystems/ibc-rs/issues/752 diff --git a/guide/src/upgrade_test.md b/guide/src/upgrade_test.md new file mode 100644 index 0000000000..aaf06fbb50 --- /dev/null +++ b/guide/src/upgrade_test.md @@ -0,0 +1,166 @@ +## Prerequisites + +- gaiad `(v4.1.*)`, for example: + +```shell +$ gaiad version --long | head -n4 +name: gaia +server_name: gaiad +version: 4.1.2 +commit: 95b07e641d1f69ee12dd911e92b1679f2c64d385 +``` + +## Testing procedure + +1. Start two gaia instances and initialize hermes: + + ```shell + $ ./scripts/dev-env ~/.hermes/config.toml ibc-0 ibc-1 + ``` + The `one-chain` script is invoked for each chain and modifies the `genesis.json` file to use a short window for governance proposals (`200s` for `max_deposit_period` and `voting_period`). Therefore, an upgrade proposal can be submitted, voted on and accepted within a short time. + +2. Create one client on `ibc-1` for `ibc-0`: + + ```shell + $ hermes tx raw create-client ibc-1 ibc-0 + ``` + +3. Create and submit an upgrade plan for chain `ibc-0`: + + Use the hermes test command to make an upgrade proposal. In the example below a software upgrade proposal is made for `ibc-0`, for the height `300` blocks from latest height. `10000000stake` is deposited. + The proposal includes the upgraded client state constructed from the state of `07-tendermint-0` client on `ibc-1` that was created in the previous step. In addition, the `unbonding_period` of the client is set to some new value (`400h`) + + ```shell + $ hermes tx raw upgrade-chain ibc-0 ibc-1 07-tendermint-0 10000000 300 + ``` + + Note that the height offset should be picked such that the proposal plan height is reached after the `200s` voting period. + + 4. Verify that the proposal was accepted: + + Query the upgrade plan to check that it was submitted correctly. Note the `height` at which the proposal will take effect (chain halts). Also `status: PROPOSAL_STATUS_VOTING_PERIOD`. + + ```shell + $ gaiad query gov proposal 1 --home data/ibc-0/ + + content: + '@type': /cosmos.upgrade.v1beta1.SoftwareUpgradeProposal + description: upgrade the chain software and unbonding period + plan: + height: "382" + info: upgrade the chain software and unbonding period + name: test + time: "0001-01-01T00:00:00Z" + upgraded_client_state: + '@type': /ibc.lightclients.tendermint.v1.ClientState + allow_update_after_expiry: false + allow_update_after_misbehaviour: false + chain_id: ibc-0 + frozen_height: + revision_height: "0" + revision_number: "0" + latest_height: + revision_height: "383" + revision_number: "0" + max_clock_drift: 0s + proof_specs: + ... + trust_level: + denominator: "0" + numerator: "0" + trusting_period: 0s + unbonding_period: 1440000s + upgrade_path: + - upgrade + - upgradedIBCState + title: upgrade_ibc_clients + deposit_end_time: "2021-03-23T17:25:42.543572Z" + final_tally_result: + abstain: "0" + "no": "0" + no_with_veto: "0" + "yes": "0" + proposal_id: "1" + status: PROPOSAL_STATUS_VOTING_PERIOD + submit_time: "2021-03-23T17:22:22.543572Z" + total_deposit: + - amount: "10000000" + denom: stake + voting_end_time: "2021-03-23T17:25:42.543572Z" + voting_start_time: "2021-03-23T17:22:22.543572Z" + ``` + + 5. Vote on the proposal + + The parameter `1` should match the `proposal_id:` from the upgrade proposal submitted at step 3. This command must be issued while the proposal status is `PROPOSAL_STATUS_VOTING_PERIOD`. + + ```shell + gaiad tx gov vote 1 yes --home data/ibc-0/data/ --keyring-backend test --keyring-dir data/ibc-0/ --chain-id ibc-0 --from validator + ``` + + Wait approximately 200 seconds until the proposal changes status to `PROPOSAL_STATUS_PASSED`. Note the `final tally_result` that includes the vote submitted in previous step. + + ```shell + $ gaiad query gov proposal 1 --home data/ibc-0/ + + content: + '@type': /cosmos.upgrade.v1beta1.SoftwareUpgradeProposal + description: upgrade the chain software and unbonding period + plan: + ... + final_tally_result: + abstain: "0" + "no": "0" + no_with_veto: "0" + "yes": "100000000000" + proposal_id: "1" + status: PROPOSAL_STATUS_PASSED + submit_time: "2021-03-23T17:22:22.543572Z" + total_deposit: + - amount: "10000000" + denom: stake + voting_end_time: "2021-03-23T17:25:42.543572Z" + voting_start_time: "2021-03-23T17:22:22.543572Z" + ``` + +6. Test the `upgrade-client` CLI + + The following command performs the upgrade for client `07-tendermint-0`. It outputs two events, one for the updated client state, and another for the upgraded state. + + ```shell + $ hermes tx raw upgrade-client ibc-1 ibc-0 07-tendermint-0 + + { + "status": "success", + "result": [ + { + "UpdateClient": { + "client_id": "07-tendermint-0", + "client_type": "Tendermint", + "consensus_height": { + "revision_height": 332, + "revision_number": 0 + }, + "height": { + "revision_height": 404, + "revision_number": 1 + } + } + }, + { + "UpgradeClient": { + "client_id": "07-tendermint-0", + "client_type": "Tendermint", + "consensus_height": { + "revision_height": 333, + "revision_number": 0 + }, + "height": { + "revision_height": 404, + "revision_number": 1 + } + } + } + ] + } + ``` diff --git a/relayer-cli/src/commands/tx.rs b/relayer-cli/src/commands/tx.rs index 5f1304266f..b0008a7fe0 100644 --- a/relayer-cli/src/commands/tx.rs +++ b/relayer-cli/src/commands/tx.rs @@ -1,13 +1,14 @@ //! `tx` subcommand use abscissa_core::{Command, Help, Options, Runnable}; -use crate::commands::tx::client::{TxCreateClientCmd, TxUpdateClientCmd}; +use crate::commands::tx::client::{TxCreateClientCmd, TxUpdateClientCmd, TxUpgradeClientCmd}; mod channel; mod client; mod connection; mod packet; mod transfer; +mod upgrade; /// `tx` subcommand #[derive(Command, Debug, Options, Runnable)] @@ -35,6 +36,10 @@ pub enum TxRawCommands { #[options(help = "Update the specified client on destination chain")] UpdateClient(TxUpdateClientCmd), + /// The `tx raw upgrade-client` subcommand. Submits a MsgUpgradeClient in a transaction to a chain. + #[options(help = "Upgrade the specified client on destination chain")] + UpgradeClient(TxUpgradeClientCmd), + /// The `tx raw conn-init` subcommand #[options(help = "Initialize a connection (ConnectionOpenInit)")] ConnInit(connection::TxRawConnInitCmd), @@ -86,4 +91,8 @@ pub enum TxRawCommands { /// The `tx raw packet-ack` subcommand #[options(help = "Relay acknowledgment packets")] PacketAck(packet::TxRawPacketAckCmd), + + /// The `tx raw upgrade-chain` subcommand + #[options(help = "Send an upgrade plan")] + UpgradeChain(upgrade::TxUpgradeChainCmd), } diff --git a/relayer-cli/src/commands/tx/client.rs b/relayer-cli/src/commands/tx/client.rs index d2e3098bdf..5f8a6363a8 100644 --- a/relayer-cli/src/commands/tx/client.rs +++ b/relayer-cli/src/commands/tx/client.rs @@ -1,4 +1,5 @@ use abscissa_core::{Command, Options, Runnable}; +use tracing::info; use ibc::events::IbcEvent; use ibc::ics24_host::identifier::{ChainId, ClientId}; @@ -7,7 +8,7 @@ use ibc_relayer::foreign_client::ForeignClient; use crate::application::app_config; use crate::commands::cli_utils::{ChainHandlePair, SpawnOptions}; -use crate::conclude::Output; +use crate::conclude::{exit_with_unrecoverable_error, Output}; use crate::error::{Error, Kind}; #[derive(Clone, Command, Debug, Options)] @@ -101,3 +102,52 @@ impl Runnable for TxUpdateClientCmd { } } } + +#[derive(Clone, Command, Debug, Options)] +pub struct TxUpgradeClientCmd { + #[options(free, required, help = "identifier of the destination chain")] + dst_chain_id: ChainId, + + #[options( + free, + required, + help = "identifier of the chain which underwent upgrade (source chain)" + )] + src_chain_id: ChainId, + + #[options( + free, + required, + help = "identifier of the client to be upgraded on destination chain" + )] + dst_client_id: ClientId, +} + +impl Runnable for TxUpgradeClientCmd { + fn run(&self) { + let config = app_config(); + + let spawn_options = SpawnOptions::override_store_config(StoreConfig::memory()); + let chains = ChainHandlePair::spawn_with( + spawn_options, + &config, + &self.src_chain_id, + &self.dst_chain_id, + ) + .unwrap_or_else(exit_with_unrecoverable_error); + + info!("Started the chain runtimes"); + + // Instantiate the client hosted on the destination chain, which is targeting headers for + // the source chain. + let client = ForeignClient::find(chains.src, chains.dst, &self.dst_client_id) + .unwrap_or_else(exit_with_unrecoverable_error); + + let outcome = client.upgrade(); + + match outcome { + Ok(receipt) => Output::success(receipt).exit(), + Err(e) => Output::error(format!("{}", e)).exit(), + } + } +} diff --git a/relayer-cli/src/commands/tx/upgrade.rs b/relayer-cli/src/commands/tx/upgrade.rs new file mode 100644 index 0000000000..7f2d890677 --- /dev/null +++ b/relayer-cli/src/commands/tx/upgrade.rs @@ -0,0 +1,101 @@ +use std::sync::Arc; + +use abscissa_core::{Command, Options, Runnable}; +use tokio::runtime::Runtime as TokioRuntime; + +use ibc::events::IbcEvent; +use ibc::ics24_host::identifier::{ChainId, ClientId}; +use ibc_relayer::upgrade_chain::{build_and_send_upgrade_chain_message, UpdatePlanOptions}; +use ibc_relayer::{ + chain::{Chain, CosmosSdkChain}, + config::Config, +}; + +use crate::conclude::Output; +use crate::error::{Error, Kind}; +use crate::prelude::*; + +#[derive(Clone, Command, Debug, Options)] +pub struct TxUpgradeChainCmd { + #[options(free, required, help = "identifier of the chain to upgrade")] + dst_chain_id: ChainId, + + #[options(free, required, help = "identifier of the source chain")] + src_chain_id: ChainId, + + #[options( + free, + required, + help = "identifier of the client on source chain from which the plan is created" + )] + src_client_id: ClientId, + + #[options(free, required, help = "amount of stake")] + amount: u64, + + #[options( + free, + required, + help = "upgrade height offset in number of blocks since current" + )] + height_offset: u64, +} + +impl TxUpgradeChainCmd { + fn validate_options(&self, config: &Config) -> Result { + let src_chain_config = config + .find_chain(&self.src_chain_id) + .ok_or_else(|| "missing src chain configuration".to_string())?; + + let dst_chain_config = config + .find_chain(&self.dst_chain_id) + .ok_or_else(|| "missing destination chain configuration".to_string())?; + + let opts = UpdatePlanOptions { + dst_chain_config: dst_chain_config.clone(), + src_chain_config: src_chain_config.clone(), + src_client_id: self.src_client_id.clone(), + amount: self.amount, + height_offset: self.height_offset, + }; + + Ok(opts) + } +} + +impl Runnable for TxUpgradeChainCmd { + fn run(&self) { + let config = app_config(); + + let opts = match self.validate_options(&config) { + Err(err) => return Output::error(err).exit(), + Ok(result) => result, + }; + info!("Message {:?}", opts); + + let rt = Arc::new(TokioRuntime::new().unwrap()); + + let src_chain_res = CosmosSdkChain::bootstrap(opts.src_chain_config.clone(), rt.clone()) + .map_err(|e| Kind::Runtime.context(e)); + let src_chain = match src_chain_res { + Ok(chain) => chain, + Err(e) => return Output::error(format!("{}", e)).exit(), + }; + + let dst_chain_res = CosmosSdkChain::bootstrap(opts.dst_chain_config.clone(), rt) + .map_err(|e| Kind::Runtime.context(e)); + let dst_chain = match dst_chain_res { + Ok(chain) => chain, + Err(e) => return Output::error(format!("{}", e)).exit(), + }; + + let res: Result, Error> = + build_and_send_upgrade_chain_message(dst_chain, src_chain, &opts) + .map_err(|e| Kind::Tx.context(e).into()); + + match res { + Ok(ev) => Output::success(ev).exit(), + Err(e) => Output::error(format!("{}", e)).exit(), + } + } +} diff --git a/relayer/src/chain.rs b/relayer/src/chain.rs index 545242c71a..f09d02f0a5 100644 --- a/relayer/src/chain.rs +++ b/relayer/src/chain.rs @@ -130,6 +130,16 @@ pub trait Chain: Sized { height: ICSHeight, ) -> Result; + fn query_upgraded_client_state( + &self, + height: ICSHeight, + ) -> Result<(Self::ClientState, MerkleProof), Error>; + + fn query_upgraded_consensus_state( + &self, + height: ICSHeight, + ) -> Result<(Self::ConsensusState, MerkleProof), Error>; + /// Performs a query to retrieve the identifiers of all connections. fn query_connections( &self, diff --git a/relayer/src/chain/cosmos.rs b/relayer/src/chain/cosmos.rs index 49d267f8c8..6f8ef9d6ff 100644 --- a/relayer/src/chain/cosmos.rs +++ b/relayer/src/chain/cosmos.rs @@ -35,7 +35,7 @@ use ibc::ics23_commitment::merkle::convert_tm_to_ics_merkle_proof; use ibc::ics24_host::identifier::{ChainId, ChannelId, ClientId, ConnectionId, PortId}; 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::ics24_host::{ClientUpgradePath, Path, IBC_QUERY_PATH, SDK_UPGRADE_QUERY_PATH}; use ibc::signer::Signer; use ibc::Height as ICSHeight; // Support for GRPC @@ -43,6 +43,9 @@ use ibc_proto::cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest}; 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::cosmos::upgrade::v1beta1::{ + QueryCurrentPlanRequest, QueryUpgradedConsensusStateRequest, +}; use ibc_proto::ibc::core::channel::v1::{ PacketState, QueryChannelsRequest, QueryConnectionChannelsRequest, QueryNextSequenceReceiveRequest, QueryPacketAcknowledgementsRequest, @@ -269,6 +272,34 @@ impl CosmosSdkChain { Ok(response) } + + // Perform an ABCI query against the client upgrade sub-store to fetch a proof. + fn query_client_upgrade_proof( + &self, + data: ClientUpgradePath, + height: Height, + ) -> Result<(MerkleProof, ICSHeight), Error> { + let prev_height = + Height::try_from(height.value() - 1).map_err(|e| Kind::InvalidHeight.context(e))?; + + let path = TendermintABCIPath::from_str(SDK_UPGRADE_QUERY_PATH).unwrap(); + let response = self.block_on(abci_query( + &self, + path, + Path::Upgrade(data).to_string(), + prev_height, + true, + ))?; + + let proof = response.proof.ok_or(Kind::EmptyResponseProof)?; + + let height = ICSHeight::new( + self.config.id.version(), + response.height.increment().value(), + ); + + Ok((proof, height)) + } } impl Chain for CosmosSdkChain { @@ -477,6 +508,105 @@ impl Chain for CosmosSdkChain { Ok(client_state) } + fn query_upgraded_client_state( + &self, + height: ICSHeight, + ) -> Result<(Self::ClientState, MerkleProof), Error> { + crate::time!("query_upgraded_client_state"); + + let grpc_address = + Uri::from_str(&self.config.grpc_addr).map_err(|e| Kind::Grpc.context(e))?; + + let mut client = self + .block_on( + ibc_proto::cosmos::upgrade::v1beta1::query_client::QueryClient::connect( + grpc_address, + ), + ) + .map_err(|e| Kind::Grpc.context(e))?; + + let req = tonic::Request::new(QueryCurrentPlanRequest {}); + let response = self + .block_on(client.current_plan(req)) + .map_err(|e| Kind::Grpc.context(e))?; + + let upgraded_client_state_raw = response + .into_inner() + .plan + .ok_or(Kind::EmptyResponseValue)? + .upgraded_client_state + .ok_or(Kind::EmptyUpgradedClientState)?; + let client_state = AnyClientState::try_from(upgraded_client_state_raw) + .map_err(|e| Kind::Grpc.context(e))?; + + // TODO: Better error kinds here. + let tm_client_state = + downcast!(client_state => AnyClientState::Tendermint).ok_or_else(|| { + Kind::Query("upgraded client state".into()).context("unexpected client state type") + })?; + + // Query for the proof. + let tm_height = + Height::try_from(height.revision_height).map_err(|e| Kind::InvalidHeight.context(e))?; + let (proof, _proof_height) = self.query_client_upgrade_proof( + ClientUpgradePath::UpgradedClientState(height.revision_height), + tm_height, + )?; + + Ok((tm_client_state, proof)) + } + + fn query_upgraded_consensus_state( + &self, + height: ICSHeight, + ) -> Result<(Self::ConsensusState, MerkleProof), Error> { + crate::time!("query_upgraded_consensus_state"); + + let tm_height = + Height::try_from(height.revision_height).map_err(|e| Kind::InvalidHeight.context(e))?; + + let grpc_address = + Uri::from_str(&self.config.grpc_addr).map_err(|e| Kind::Grpc.context(e))?; + + let mut client = self + .block_on( + ibc_proto::cosmos::upgrade::v1beta1::query_client::QueryClient::connect( + grpc_address, + ), + ) + .map_err(|e| Kind::Grpc.context(e))?; + + let req = tonic::Request::new(QueryUpgradedConsensusStateRequest { + last_height: tm_height.into(), + }); + let response = self + .block_on(client.upgraded_consensus_state(req)) + .map_err(|e| Kind::Grpc.context(e))?; + + let upgraded_consensus_state_raw = response + .into_inner() + .upgraded_consensus_state + .ok_or(Kind::EmptyResponseValue)?; + + // TODO: More explicit error kinds (should not reuse Grpc all over the place) + let consensus_state = AnyConsensusState::try_from(upgraded_consensus_state_raw) + .map_err(|e| Kind::Grpc.context(e))?; + + let tm_consensus_state = downcast!(consensus_state => AnyConsensusState::Tendermint) + .ok_or_else(|| { + Kind::Query("upgraded consensus state".into()) + .context("unexpected consensus state type") + })?; + + // Fetch the proof. + let (proof, _proof_height) = self.query_client_upgrade_proof( + ClientUpgradePath::UpgradedClientConsensusState(height.revision_height), + tm_height, + )?; + + Ok((tm_consensus_state, proof)) + } + /// Performs a query to retrieve the identifiers of all connections. fn query_client_connections( &self, diff --git a/relayer/src/chain/handle.rs b/relayer/src/chain/handle.rs index 36cc9af29d..ef5c92e6c4 100644 --- a/relayer/src/chain/handle.rs +++ b/relayer/src/chain/handle.rs @@ -114,6 +114,16 @@ pub enum ChainRequest { reply_to: ReplyTo, }, + QueryUpgradedClientState { + height: Height, + reply_to: ReplyTo<(AnyClientState, MerkleProof)>, + }, + + QueryUpgradedConsensusState { + height: Height, + reply_to: ReplyTo<(AnyConsensusState, MerkleProof)>, + }, + QueryCommitmentPrefix { reply_to: ReplyTo, }, @@ -228,6 +238,16 @@ pub trait ChainHandle: DynClone + Send + Sync + Debug { height: Height, ) -> Result; + fn query_upgraded_client_state( + &self, + height: Height, + ) -> Result<(AnyClientState, MerkleProof), Error>; + + fn query_upgraded_consensus_state( + &self, + height: Height, + ) -> Result<(AnyConsensusState, MerkleProof), Error>; + fn query_commitment_prefix(&self) -> Result; fn query_compatible_versions(&self) -> Result, Error>; diff --git a/relayer/src/chain/handle/prod.rs b/relayer/src/chain/handle/prod.rs index 231720814a..c9b047d0c3 100644 --- a/relayer/src/chain/handle/prod.rs +++ b/relayer/src/chain/handle/prod.rs @@ -117,6 +117,20 @@ impl ChainHandle for ProdChainHandle { }) } + fn query_upgraded_client_state( + &self, + height: Height, + ) -> Result<(AnyClientState, MerkleProof), Error> { + self.send(|reply_to| ChainRequest::QueryUpgradedClientState { height, reply_to }) + } + + fn query_upgraded_consensus_state( + &self, + height: Height, + ) -> Result<(AnyConsensusState, MerkleProof), Error> { + self.send(|reply_to| ChainRequest::QueryUpgradedConsensusState { height, reply_to }) + } + fn query_commitment_prefix(&self) -> Result { self.send(|reply_to| ChainRequest::QueryCommitmentPrefix { reply_to }) } diff --git a/relayer/src/chain/mock.rs b/relayer/src/chain/mock.rs index 669d6374ba..cd53871ea7 100644 --- a/relayer/src/chain/mock.rs +++ b/relayer/src/chain/mock.rs @@ -147,6 +147,13 @@ impl Chain for MockChain { Ok(client_state) } + fn query_upgraded_client_state( + &self, + _height: Height, + ) -> Result<(Self::ClientState, MerkleProof), Error> { + unimplemented!() + } + fn query_connection( &self, _connection_id: &ConnectionId, @@ -313,6 +320,13 @@ impl Chain for MockChain { trusted_validator_set: trusted_light_block.validators, }) } + + fn query_upgraded_consensus_state( + &self, + _height: Height, + ) -> Result<(Self::ConsensusState, MerkleProof), Error> { + unimplemented!() + } } // For integration tests with the modules diff --git a/relayer/src/chain/runtime.rs b/relayer/src/chain/runtime.rs index 2dbf59d9bf..69ce5f2162 100644 --- a/relayer/src/chain/runtime.rs +++ b/relayer/src/chain/runtime.rs @@ -217,6 +217,14 @@ impl ChainRuntime { self.query_client_state(client_id, height, reply_to)? }, + Ok(ChainRequest::QueryUpgradedClientState { height, reply_to }) => { + self.query_upgraded_client_state(height, reply_to)? + } + + Ok(ChainRequest::QueryUpgradedConsensusState { height, reply_to }) => { + self.query_upgraded_consensus_state(height, reply_to)? + } + Ok(ChainRequest::QueryCommitmentPrefix { reply_to }) => { self.query_commitment_prefix(reply_to)? }, @@ -466,6 +474,40 @@ impl ChainRuntime { Ok(()) } + fn query_upgraded_client_state( + &self, + height: Height, + reply_to: ReplyTo<(AnyClientState, MerkleProof)>, + ) -> Result<(), Error> { + let result = self + .chain + .query_upgraded_client_state(height) + .map(|(cl, proof)| (cl.wrap_any(), proof)); + + reply_to + .send(result) + .map_err(|e| Kind::Channel.context(e))?; + + Ok(()) + } + + fn query_upgraded_consensus_state( + &self, + height: Height, + reply_to: ReplyTo<(AnyConsensusState, MerkleProof)>, + ) -> Result<(), Error> { + let result = self + .chain + .query_upgraded_consensus_state(height) + .map(|(cs, proof)| (cs.wrap_any(), proof)); + + reply_to + .send(result) + .map_err(|e| Kind::Channel.context(e))?; + + Ok(()) + } + fn query_commitment_prefix(&self, reply_to: ReplyTo) -> Result<(), Error> { let prefix = self.chain.query_commitment_prefix(); diff --git a/relayer/src/error.rs b/relayer/src/error.rs index 176fa619f6..f2a1b87793 100644 --- a/relayer/src/error.rs +++ b/relayer/src/error.rs @@ -48,6 +48,10 @@ pub enum Kind { #[error("Bad Notification")] Event, + /// Missing ClientState in the upgrade CurrentPlan + #[error("The upgrade plan specifies no upgraded client state")] + EmptyUpgradedClientState, + /// Response does not contain data #[error("Empty response value")] EmptyResponseValue, diff --git a/relayer/src/foreign_client.rs b/relayer/src/foreign_client.rs index 4e3b3b8393..83de334202 100644 --- a/relayer/src/foreign_client.rs +++ b/relayer/src/foreign_client.rs @@ -2,7 +2,7 @@ use std::{thread, time::Duration}; use prost_types::Any; use thiserror::Error; -use tracing::{error, info}; +use tracing::{debug, error, info, warn}; use ibc::events::IbcEvent; use ibc::ics02_client::client_consensus::ConsensusState; @@ -10,6 +10,7 @@ use ibc::ics02_client::client_state::ClientState; use ibc::ics02_client::header::Header; use ibc::ics02_client::msgs::create_client::MsgCreateAnyClient; use ibc::ics02_client::msgs::update_client::MsgUpdateAnyClient; +use ibc::ics02_client::msgs::upgrade_client::MsgUpgradeAnyClient; use ibc::ics24_host::identifier::{ChainId, ClientId}; use ibc::tx_msg::Msg; use ibc::Height; @@ -29,6 +30,9 @@ pub enum ForeignClientError { #[error("failed while finding client {0}: expected chain_id in client state: {1}; actual chain_id: {2}")] ClientFind(ClientId, ChainId, ChainId), + + #[error("failed while trying to upgrade client id {0} with error: {1}")] + ClientUpgrade(ClientId, String), } #[derive(Clone, Debug)] @@ -109,6 +113,87 @@ impl ForeignClient { } } + pub fn upgrade(&self) -> Result, ForeignClientError> { + // Fetch the latest height of the source chain. + let src_height = self.src_chain.query_latest_height().map_err(|e| { + ForeignClientError::ClientUpgrade( + self.id.clone(), + format!( + "failed while querying src chain ({}) for latest height: {}", + self.src_chain.id(), + e + ), + ) + })?; + + info!("Upgrade Height: {}", src_height); + + let mut msgs = self.build_update_client(src_height)?; + + // Query the host chain for the upgraded client state, consensus state & their proofs. + let (client_state, proof_upgrade_client) = self + .src_chain + .query_upgraded_client_state(src_height) + .map_err(|e| { + ForeignClientError::ClientUpgrade( + self.id.clone(), + format!( + "failed while fetching from chain {} the upgraded client state: {}", + self.src_chain.id(), + e + ), + ) + })?; + + debug!("Upgraded client state {:?}", client_state); + + let (consensus_state, proof_upgrade_consensus_state) = self + .src_chain + .query_upgraded_consensus_state(src_height) + .map_err(|e| ForeignClientError::ClientUpgrade(self.id.clone(), format!( + "failed while fetching from chain {} the upgraded client consensus state: {}", self.src_chain.id(), e))) + ?; + + debug!("Upgraded client consensus state {:?}", consensus_state); + + // Get signer + let signer = self.dst_chain.get_signer().map_err(|e| { + ForeignClientError::ClientUpgrade( + self.id.clone(), + format!( + "failed while fetching the destination chain ({}) signer: {}", + self.dst_chain.id(), + e + ), + ) + })?; + + let msg_upgrade = MsgUpgradeAnyClient { + client_id: self.id.clone(), + client_state, + consensus_state, + proof_upgrade_client, + proof_upgrade_consensus_state, + signer, + } + .to_any(); + + msgs.push(msg_upgrade); + + let res = self.dst_chain.send_msgs(msgs).map_err(|e| { + ForeignClientError::ClientUpgrade( + self.id.clone(), + format!( + "failed while sending message to destination chain {} with err: {}", + self.dst_chain.id(), + e + ), + ) + })?; + + Ok(res) + } + /// Returns a handle to the chain hosting this client. pub fn dst_chain(&self) -> Box { self.dst_chain.clone() @@ -210,6 +295,8 @@ impl ForeignClient { Ok(()) } + /// Returns a vector with a message for updating the client to height `target_height`. + /// If the client already stores consensus states for this height, returns an empty vector. pub fn build_update_client( &self, target_height: Height, @@ -237,6 +324,14 @@ impl ForeignClient { })? .latest_height(); + if trusted_height >= target_height { + warn!( + "Client height ({}) >= chain target height ({}). Cannot build update message.", + trusted_height, target_height + ); + return Ok(vec![]); + } + let header = self .src_chain() .build_header(trusted_height, target_height) @@ -272,7 +367,16 @@ impl ForeignClient { e )) })?; + let new_msgs = self.build_update_client(h)?; + if new_msgs.is_empty() { + return Err(ForeignClientError::ClientUpdate(format!( + "Client {} is already up-to-date with chain {}@{}", + self.id, + self.src_chain.id(), + h + ))); + } let mut events = self.dst_chain().send_msgs(new_msgs).map_err(|e| { ForeignClientError::ClientUpdate(format!( diff --git a/relayer/src/lib.rs b/relayer/src/lib.rs index 472f9496fd..e14841c1fb 100644 --- a/relayer/src/lib.rs +++ b/relayer/src/lib.rs @@ -27,4 +27,5 @@ pub mod macros; pub mod relay; pub mod supervisor; pub mod transfer; +pub mod upgrade_chain; pub mod util; diff --git a/relayer/src/upgrade_chain.rs b/relayer/src/upgrade_chain.rs new file mode 100644 index 0000000000..bb214597f3 --- /dev/null +++ b/relayer/src/upgrade_chain.rs @@ -0,0 +1,120 @@ +use bitcoin::hashes::core::time::Duration; +use prost_types::Any; +use thiserror::Error; +use tracing::error; + +use ibc::ics02_client::client_state::AnyClientState; +use ibc::ics02_client::height::Height; +use ibc::ics24_host::identifier::{ChainId, ClientId}; +use ibc::{events::IbcEvent, ics07_tendermint::client_state::ClientState}; +use ibc_proto::cosmos::gov::v1beta1::MsgSubmitProposal; +use ibc_proto::cosmos::upgrade::v1beta1::{Plan, SoftwareUpgradeProposal}; + +use crate::chain::{Chain, CosmosSdkChain}; +use crate::config::ChainConfig; +use crate::error::Error; + +#[derive(Debug, Error)] +pub enum UpgradeChainError { + #[error("failed with underlying cause: {0}")] + Failed(String), + + #[error("key error with underlying cause: {0}")] + KeyError(Error), + + #[error( + "failed during a transaction submission step to chain id {0} with underlying error: {1}" + )] + SubmitError(ChainId, Error), +} + +#[derive(Clone, Debug)] +pub struct UpdatePlanOptions { + pub src_chain_config: ChainConfig, + pub dst_chain_config: ChainConfig, + pub src_client_id: ClientId, + pub amount: u64, + pub height_offset: u64, +} + +pub fn build_and_send_upgrade_chain_message( + mut dst_chain: CosmosSdkChain, // the chain whose account is debited + src_chain: CosmosSdkChain, // the chain where the transfer is sent + opts: &UpdatePlanOptions, +) -> Result, UpgradeChainError> { + // build a proposal Plan + let upgrade_height = dst_chain + .query_latest_height() + .unwrap() + .add(opts.height_offset); + + let client_state = src_chain + .query_client_state(&opts.src_client_id, Height::zero()) + .unwrap(); + + let mut upgraded_client_state = ClientState::zero_custom_fields(client_state); + upgraded_client_state.latest_height = upgrade_height.increment(); + upgraded_client_state.unbonding_period = Duration::from_secs(400 * 3600); + + let raw_client_state = AnyClientState::Tendermint(upgraded_client_state); + let plan = Plan { + name: "test".to_string(), + time: None, + height: upgrade_height.revision_height as i64, + info: "upgrade the chain software and unbonding period".to_string(), + upgraded_client_state: Some(Any::from(raw_client_state)), + }; + + // build the proposal + let proposal = SoftwareUpgradeProposal { + title: "upgrade_ibc_clients".to_string(), + description: "upgrade the chain software and unbonding period".to_string(), + plan: Some(plan), + }; + + let mut buf_proposal = Vec::new(); + prost::Message::encode(&proposal, &mut buf_proposal).unwrap(); + + let any_proposal = Any { + type_url: "/cosmos.upgrade.v1beta1.SoftwareUpgradeProposal".to_string(), + value: buf_proposal, + }; + + // build the msg submit proposal + let proposer = dst_chain + .get_signer() + .map_err(UpgradeChainError::KeyError)?; + + let coins = ibc_proto::cosmos::base::v1beta1::Coin { + denom: "stake".to_string(), + amount: opts.amount.to_string(), + }; + + let msg = MsgSubmitProposal { + content: Some(any_proposal), + initial_deposit: vec![coins], + proposer: proposer.to_string(), + }; + + let mut buf_msg = Vec::new(); + prost::Message::encode(&msg, &mut buf_msg).unwrap(); + let any_msg = Any { + type_url: "/cosmos.gov.v1beta1.MsgSubmitProposal".to_string(), + value: buf_msg, + }; + + let events = dst_chain + .send_msgs(vec![any_msg]) + .map_err(|e| UpgradeChainError::SubmitError(dst_chain.id().clone(), e))?; + + // Check if the chain rejected the transaction + let result = events.iter().find_map(|event| match event { + IbcEvent::ChainError(reason) => Some(reason.clone()), + _ => None, + }); + + match result { + None => Ok(events), + Some(reason) => Err(UpgradeChainError::Failed(reason)), + } +} diff --git a/scripts/dev-env b/scripts/dev-env index a5631a1c6f..1bda9bac2e 100755 --- a/scripts/dev-env +++ b/scripts/dev-env @@ -11,8 +11,8 @@ missing() { usage } -if [ -z "$1" ]; then - missing "CONFIG_FILE" +if [ ! -r "$1" ]; then + missing "CONFIG_FILE ($1)" fi if [ -z "$2" ]; then diff --git a/scripts/init-clients b/scripts/init-clients index 6cf1a674ac..333779e9ef 100755 --- a/scripts/init-clients +++ b/scripts/init-clients @@ -66,8 +66,8 @@ cargo run --bin hermes -- -c "$CONFIG_FILE" light rm -c "$CHAIN_1_ID" --all -y & # set the primary peers for clients on each chain echo "Adding primary peers to light client configuration..." -cargo run --bin hermes -- -c "$CONFIG_FILE" light add $CHAIN_0_RPC_ADDR -c "$CHAIN_0_ID" -f -p -s "$GAIA_DATA/$CHAIN_0_ID/data" -y &>/dev/null -cargo run --bin hermes -- -c "$CONFIG_FILE" light add $CHAIN_1_RPC_ADDR -c "$CHAIN_1_ID" -f -p -s "$GAIA_DATA/$CHAIN_1_ID/data" -y &>/dev/null +cargo run --bin hermes -- -c "$CONFIG_FILE" light add $CHAIN_0_RPC_ADDR -c "$CHAIN_0_ID" -f -p -s "$GAIA_DATA/$CHAIN_0_ID/data" -y &>/dev/null || printf "\tError adding primary peer to %s\n" "$CHAIN_0_RPC_ADDR" +cargo run --bin hermes -- -c "$CONFIG_FILE" light add $CHAIN_1_RPC_ADDR -c "$CHAIN_1_ID" -f -p -s "$GAIA_DATA/$CHAIN_1_ID/data" -y &>/dev/null || printf "\tError adding primary peer to %s\n" "$CHAIN_1_RPC_ADDR" # set the secondary peers for clients on each chain echo "Adding secondary peers to light client configuration..." diff --git a/scripts/one-chain b/scripts/one-chain index 6d59e857e6..e7e0fed600 100755 --- a/scripts/one-chain +++ b/scripts/one-chain @@ -105,6 +105,7 @@ fi # Set proper defaults and change ports (use a different sed for Mac or Linux) echo "Change settings in config.toml file..." if [ $platform = 'linux' ]; then + sed -i 's#"172800s"#"200s"#g' $CHAIN_DIR/$CHAIN_ID/config/genesis.json sed -i 's#"tcp://127.0.0.1:26657"#"tcp://0.0.0.0:'"$RPC_PORT"'"#g' $CHAIN_DIR/$CHAIN_ID/config/config.toml sed -i 's#"tcp://0.0.0.0:26656"#"tcp://0.0.0.0:'"$P2P_PORT"'"#g' $CHAIN_DIR/$CHAIN_ID/config/config.toml sed -i 's#"localhost:6060"#"localhost:'"$PROF_PORT"'"#g' $CHAIN_DIR/$CHAIN_ID/config/config.toml @@ -113,6 +114,7 @@ if [ $platform = 'linux' ]; then sed -i 's/index_all_keys = false/index_all_keys = true/g' $CHAIN_DIR/$CHAIN_ID/config/config.toml # sed -i '' 's#index-events = \[\]#index-events = \["message.action","send_packet.packet_src_channel","send_packet.packet_sequence"\]#g' $CHAIN_DIR/$CHAIN_ID/config/app.toml else + sed -i '' 's#"172800s"#"200s"#g' $CHAIN_DIR/$CHAIN_ID/config/genesis.json sed -i '' 's#"tcp://127.0.0.1:26657"#"tcp://0.0.0.0:'"$RPC_PORT"'"#g' $CHAIN_DIR/$CHAIN_ID/config/config.toml sed -i '' 's#"tcp://0.0.0.0:26656"#"tcp://0.0.0.0:'"$P2P_PORT"'"#g' $CHAIN_DIR/$CHAIN_ID/config/config.toml sed -i '' 's#"localhost:6060"#"localhost:'"$PROF_PORT"'"#g' $CHAIN_DIR/$CHAIN_ID/config/config.toml @@ -131,5 +133,7 @@ $BINARY --home $CHAIN_DIR/$CHAIN_ID start --pruning=nothing --grpc.address="0.0. # Show validator's and user's balance sleep 3 RPC_ADDR="tcp://localhost:$RPC_PORT" +echo "Balances for validator '$VALIDATOR' @ '$RPC_ADDR'" $BINARY --node "$RPC_ADDR" query bank balances $VALIDATOR --log_level error +echo "Balances for user '$USER' @ '$RPC_ADDR'" $BINARY --node "$RPC_ADDR" query bank balances $USER --log_level error diff --git a/scripts/setup-chains b/scripts/setup-chains index b440582f35..f6f6a14a17 100755 --- a/scripts/setup-chains +++ b/scripts/setup-chains @@ -63,9 +63,7 @@ mkdir -p "$GAIA_DATA" && cd "$GAIA_DATA" && cd ../ ONE_CHAIN="$(dirname "$0")/one-chain" CHAIN_0_RPC_PORT=26657 -CHAIN_0_RPC_ADDR="localhost:$CHAIN_0_RPC_PORT" CHAIN_1_RPC_PORT=26557 -CHAIN_1_RPC_ADDR="localhost:$CHAIN_1_RPC_PORT" CHAIN_0_SAMOLEANS=100000000000 CHAIN_1_SAMOLEANS=100000000000