diff --git a/CHANGELOG.md b/CHANGELOG.md index ef71d2dfa7..c82e95359e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,25 +18,34 @@ - [ibc-relayer] - Update the on-chain IBC client with supporting headers when light client verification - does bisection when verifying a header for a client update or a misbehaviour detection ([#673]) + performs bisection when verifying a header for a client update or a misbehaviour detection ([#673]) + - Add mitigation for chain impersonation attacks ([#1038]) + - Determine gas fee dynamically per transaction ([#930]) + - Submit transactions with `broadcast_tx_sync` and keep track of account sequences ([#986]) ### BUG FIXES - [gaiad-manager] - - Removed the testnet command as not all networks support it. ([#1050]) - - Update to compatibility with hermes's new `--hd-path` option. + - Removed the testnet command as not all networks support it ([#1050]) + - Update for compatibility with Hermes's new `--hd-path` option -- [ibc-relayer-cli] - - Fix for chain impersonation bug. ([#1038]) - - Fix for partially open handshake bug of `channel create` CLI. ([#1064]) +- [ibc-relayer] + - Fix bug where channels were left partially open after `channel create` ([#1064]) + - Prevent account sequence mismatch errors in many cases ([#919], [#978]) + - Prevent timeouts when submitting transactins ([#977]) ### BREAKING CHANGES - [ibc-relayer-cli] - - Removed `--coin-type` option from `keys restore` command. Use `--hd-path` instead. ([#1049]) - + - Removed `--coin-type` option from `keys restore` command. Use `--hd-path` instead ([#1049]) + [#673]: https://github.com/informalsystems/ibc-rs/issues/673 [#877]: https://github.com/informalsystems/ibc-rs/issues/877 +[#919]: https://github.com/informalsystems/ibc-rs/issues/919 +[#930]: https://github.com/informalsystems/ibc-rs/issues/930 +[#977]: https://github.com/informalsystems/ibc-rs/issues/977 +[#978]: https://github.com/informalsystems/ibc-rs/issues/978 +[#986]: https://github.com/informalsystems/ibc-rs/issues/986 [#1038]: https://github.com/informalsystems/ibc-rs/issues/1038 [#1049]: https://github.com/informalsystems/ibc-rs/issues/1049 [#1050]: https://github.com/informalsystems/ibc-rs/issues/1050 diff --git a/Cargo.lock b/Cargo.lock index aab804bca0..87f07439e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -887,6 +887,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78d24d088325f939faaf806483fbfac0548e43018606bfe7a44abc83f6dc75ea" +dependencies = [ + "num", +] + [[package]] name = "fs2" version = "0.4.3" @@ -1409,6 +1418,7 @@ dependencies = [ "dyn-clonable", "dyn-clone", "env_logger", + "fraction", "futures", "hdpath", "hex", @@ -1824,6 +1834,29 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg 1.0.1", + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -1845,6 +1878,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" diff --git a/ci/simple_config.toml b/ci/simple_config.toml index 3e0c4c4bb2..cc721983ff 100644 --- a/ci/simple_config.toml +++ b/ci/simple_config.toml @@ -11,9 +11,8 @@ rpc_timeout = '10s' account_prefix = 'cosmos' key_name = 'testkey' store_prefix = 'ibc' -fee_denom = 'stake' -fee_amount = 10 -gas = 400000 +max_gas = 400000 +gas_price = { price = 0.001, denom = 'stake' } clock_drift = '5s' trusting_period = '14days' @@ -26,8 +25,7 @@ rpc_timeout = '10s' account_prefix = 'cosmos' key_name = 'testkey' store_prefix = 'ibc' -fee_denom = 'stake' -fee_amount = 10 -gas = 400000 +max_gas = 400000 +gas_price = { price = 0.001, denom = 'stake' } clock_drift = '5s' trusting_period = '14days' diff --git a/config.toml b/config.toml index ae996198e9..64bcc64ae3 100644 --- a/config.toml +++ b/config.toml @@ -19,15 +19,11 @@ rpc_timeout = '10s' account_prefix = 'cosmos' key_name = 'testkey' store_prefix = 'ibc' -gas = 3000000 -fee_denom = 'stake' -fee_amount = 10 +max_gas = 3000000 +gas_price = { price = 0.001, denom = 'stake' } clock_drift = '5s' trusting_period = '14days' - -[chains.trust_threshold] -numerator = '1' -denominator = '3' +trust_threshold = { numerator = '1', denominator = '3' } [[chains]] id = 'ibc-1' @@ -38,13 +34,8 @@ rpc_timeout = '10s' account_prefix = 'cosmos' key_name = 'testkey' store_prefix = 'ibc' -gas = 3000000 -fee_denom = 'stake' -fee_amount = 1000 +max_gas = 3000000 +gas_price = { price = 0.001, denom = 'stake' } clock_drift = '5s' trusting_period = '14days' - -[chains.trust_threshold] -numerator = '1' -denominator = '3' - +trust_threshold = { numerator = '1', denominator = '3' } diff --git a/e2e/e2e/cmd.py b/e2e/e2e/cmd.py index 957e0c31f7..6750aab58b 100644 --- a/e2e/e2e/cmd.py +++ b/e2e/e2e/cmd.py @@ -13,7 +13,7 @@ class Config: config_file: Path relayer_cmd: str log_level: str - max_retries: int = 5 + max_retries: int = 10 T = TypeVar('T') diff --git a/e2e/e2e/packet.py b/e2e/e2e/packet.py index b8b6ee68a0..278aaf1f04 100644 --- a/e2e/e2e/packet.py +++ b/e2e/e2e/packet.py @@ -194,7 +194,7 @@ def query_unreceived_acks( def packet_send(c: Config, src: ChainId, dst: ChainId, src_port: PortId, src_channel: ChannelId, amount: int, height_offset: int, number_msgs: Optional[int] = None, - key: Optional[str] = None) -> Packet: + key: Optional[str] = 'user2') -> Packet: cmd = TxPacketSend(dst_chain_id=dst, src_chain_id=src, src_port=src_port, src_channel=src_channel, diff --git a/modules/src/query.rs b/modules/src/query.rs index b2923c98bf..68393ea38c 100644 --- a/modules/src/query.rs +++ b/modules/src/query.rs @@ -1,3 +1,5 @@ +use tendermint::abci::transaction::Hash; + use crate::ics02_client::client_consensus::QueryClientEventRequest; use crate::ics04_channel::channel::QueryPacketEventDataRequest; @@ -6,4 +8,8 @@ use crate::ics04_channel::channel::QueryPacketEventDataRequest; pub enum QueryTxRequest { Packet(QueryPacketEventDataRequest), Client(QueryClientEventRequest), + Transaction(QueryTxHash), } + +#[derive(Clone, Debug)] +pub struct QueryTxHash(pub Hash); diff --git a/relayer-cli/src/commands/query.rs b/relayer-cli/src/commands/query.rs index 10cdfa4103..a7c4a6e9a4 100644 --- a/relayer-cli/src/commands/query.rs +++ b/relayer-cli/src/commands/query.rs @@ -11,6 +11,7 @@ mod clients; mod connection; mod connections; mod packet; +mod tx; /// `query` subcommand #[derive(Command, Debug, Options, Runnable)] @@ -41,6 +42,10 @@ pub enum QueryCmd { /// The `query packet` subcommand #[options(help = "Query information about packets")] Packet(QueryPacketCmds), + + /// The `query tx` subcommand + #[options(help = "Query information about transactions")] + Tx(tx::QueryTxCmd), } #[derive(Command, Debug, Options, Runnable)] diff --git a/relayer-cli/src/commands/query/packet.rs b/relayer-cli/src/commands/query/packet.rs index abe40ebe57..f9d53cc5af 100644 --- a/relayer-cli/src/commands/query/packet.rs +++ b/relayer-cli/src/commands/query/packet.rs @@ -5,22 +5,20 @@ use serde::Serialize; use subtle_encoding::{Encoding, Hex}; use tokio::runtime::Runtime as TokioRuntime; +use ibc::ics02_client::client_state::ClientState; use ibc::ics04_channel::packet::{PacketMsgType, Sequence}; use ibc::ics24_host::identifier::{ChainId, ChannelId, PortId}; use ibc::Height; - use ibc_proto::ibc::core::channel::v1::{ PacketState, QueryPacketAcknowledgementsRequest, QueryPacketCommitmentsRequest, QueryUnreceivedAcksRequest, QueryUnreceivedPacketsRequest, }; - +use ibc_relayer::chain::counterparty::channel_connection_client; use ibc_relayer::chain::{runtime::ChainRuntime, CosmosSdkChain}; use crate::conclude::Output; use crate::error::{Error, Kind}; use crate::prelude::*; -use ibc::ics02_client::client_state::ClientState; -use ibc_relayer::chain::counterparty::channel_connection_client; #[derive(Serialize, Debug)] struct PacketSeqs { diff --git a/relayer-cli/src/commands/query/tx.rs b/relayer-cli/src/commands/query/tx.rs new file mode 100644 index 0000000000..1bd72845b9 --- /dev/null +++ b/relayer-cli/src/commands/query/tx.rs @@ -0,0 +1,13 @@ +//! `query tx` subcommand + +use abscissa_core::{Command, Options, Runnable}; + +mod events; + +/// `query tx` subcommand +#[derive(Command, Debug, Options, Runnable)] +pub enum QueryTxCmd { + /// The `query tx events` subcommand + #[options(help = "Query the events emitted by transaction")] + Events(events::QueryTxEventsCmd), +} diff --git a/relayer-cli/src/commands/query/tx/events.rs b/relayer-cli/src/commands/query/tx/events.rs new file mode 100644 index 0000000000..adfbfaed9d --- /dev/null +++ b/relayer-cli/src/commands/query/tx/events.rs @@ -0,0 +1,56 @@ +use std::str::FromStr; +use std::sync::Arc; + +use abscissa_core::{Command, Options, Runnable}; +use tokio::runtime::Runtime as TokioRuntime; +use tracing::debug; + +use tendermint::abci::transaction::Hash; + +use ibc::ics24_host::identifier::ChainId; +use ibc::query::{QueryTxHash, QueryTxRequest}; + +use ibc_relayer::chain::runtime::ChainRuntime; +use ibc_relayer::chain::CosmosSdkChain; + +use crate::conclude::Output; +use crate::prelude::app_config; + +/// Query the events emitted by transaction +#[derive(Clone, Command, Debug, Options)] +pub struct QueryTxEventsCmd { + #[options(free, required, help = "identifier of the chain to query")] + chain_id: ChainId, + + #[options(free, required, help = "transaction hash to query")] + hash: String, +} + +// cargo run --bin hermes -- query tx events ibc-0 B8E78AD83810239E21863AC7B5FC4F99396ABB39EB534F721EEF43A4979C2821 +impl Runnable for QueryTxEventsCmd { + fn run(&self) { + let config = app_config(); + + debug!("Options: {:?}", self); + + let chain_config = match config + .find_chain(&self.chain_id) + .ok_or_else(|| format!("chain '{}' not found in configuration file", self.chain_id)) + { + Err(err) => return Output::error(err).exit(), + Ok(result) => result, + }; + + let rt = Arc::new(TokioRuntime::new().unwrap()); + let (chain, _) = ChainRuntime::::spawn(chain_config.clone(), rt).unwrap(); + + let res = chain.query_txs(QueryTxRequest::Transaction(QueryTxHash( + Hash::from_str(self.hash.as_str()).unwrap(), + ))); + + match res { + Ok(res) => Output::success(res).exit(), + Err(e) => Output::error(format!("{}", e)).exit(), + } + } +} diff --git a/relayer-cli/src/commands/start.rs b/relayer-cli/src/commands/start.rs index 72a780f4f8..41cce404e7 100644 --- a/relayer-cli/src/commands/start.rs +++ b/relayer-cli/src/commands/start.rs @@ -15,7 +15,10 @@ impl Runnable for StartCmd { fn run(&self) { let config = app_config(); - match spawn_supervisor(config.clone()).and_then(|s| s.run()) { + match spawn_supervisor(config.clone()).and_then(|s| { + info!("Hermes has started"); + s.run() + }) { Ok(()) => Output::success_msg("done").exit(), Err(e) => Output::error(format!("Hermes failed to start, last error: {}", e)).exit(), } diff --git a/relayer-cli/tests/fixtures/two_chains.toml b/relayer-cli/tests/fixtures/two_chains.toml index 0e4548b42a..2c04d671c8 100644 --- a/relayer-cli/tests/fixtures/two_chains.toml +++ b/relayer-cli/tests/fixtures/two_chains.toml @@ -12,7 +12,6 @@ account_prefix = 'cosmos' key_name = 'testkey' store_prefix = 'ibc' client_ids = ['cla1', 'cla2'] -gas = 200000 clock_drift = '5s' trusting_period = '14days' @@ -30,7 +29,6 @@ account_prefix = 'cosmos' key_name = 'testkey' store_prefix = 'ibc' client_ids = ['clb1'] -gas = 200000 clock_drift = '5s' trusting_period = '14days' diff --git a/relayer/Cargo.toml b/relayer/Cargo.toml index f611c12571..e4d38d3bad 100644 --- a/relayer/Cargo.toml +++ b/relayer/Cargo.toml @@ -54,6 +54,7 @@ dirs-next = "2.0.0" dyn-clone = "1.0.3" retry = { version = "1.2.1", default-features = false } async-stream = "0.3.2" +fraction = {version = "0.8.0", default-features = false } [dependencies.tendermint] version = "=0.19.0" diff --git a/relayer/src/chain/cosmos.rs b/relayer/src/chain/cosmos.rs index f43e39eb41..7116e56663 100644 --- a/relayer/src/chain/cosmos.rs +++ b/relayer/src/chain/cosmos.rs @@ -1,6 +1,10 @@ use std::{ - convert::TryFrom, convert::TryInto, future::Future, str::FromStr, sync::Arc, thread, - time::Duration, + convert::{TryFrom, TryInto}, + future::Future, + str::FromStr, + sync::Arc, + thread, + time::{Duration, Instant}, }; use anomaly::fail; @@ -14,11 +18,12 @@ use tendermint::block::Height; use tendermint::consensus::Params; use tendermint_light_client::types::LightBlock as TMLightBlock; use tendermint_proto::Protobuf; +use tendermint_rpc::endpoint::tx_search::ResultTx; use tendermint_rpc::query::Query; -use tendermint_rpc::{endpoint::broadcast::tx_commit::Response, Client, HttpClient, Order}; +use tendermint_rpc::{endpoint::broadcast::tx_sync::Response, Client, HttpClient, Order}; use tokio::runtime::Runtime as TokioRuntime; use tonic::codegen::http::Uri; -use tonic::Code; +use tracing::{debug, trace}; use ibc::downcast; use ibc::events::{from_tx_response_event, IbcEvent}; @@ -40,14 +45,16 @@ use ibc::ics24_host::identifier::{ChainId, ChannelId, ClientId, ConnectionId, Po use ibc::ics24_host::Path::ClientConsensusState as ClientConsensusPath; use ibc::ics24_host::Path::ClientState as ClientStatePath; use ibc::ics24_host::{ClientUpgradePath, Path, IBC_QUERY_PATH, SDK_UPGRADE_QUERY_PATH}; -use ibc::query::QueryTxRequest; +use ibc::query::{QueryTxHash, QueryTxRequest}; use ibc::signer::Signer; use ibc::Height as ICSHeight; -// Support for GRPC 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::tx::v1beta1::{ + AuthInfo, Fee, ModeInfo, SignDoc, SignerInfo, SimulateRequest, SimulateResponse, Tx, TxBody, + TxRaw, +}; use ibc_proto::cosmos::upgrade::v1beta1::{ QueryCurrentPlanRequest, QueryUpgradedConsensusStateRequest, }; @@ -64,7 +71,7 @@ use ibc_proto::ibc::core::connection::v1::{ }; use crate::chain::QueryResponse; -use crate::config::ChainConfig; +use crate::config::{ChainConfig, GasPrice}; use crate::error::{Error, Kind}; use crate::event::monitor::{EventMonitor, EventReceiver}; use crate::keyring::{KeyEntry, KeyRing, Store}; @@ -73,13 +80,25 @@ use crate::light_client::LightClient; use crate::light_client::Verified; use super::Chain; -use tendermint_rpc::endpoint::tx_search::ResultTx; +use std::cmp::min; + +const DEFAULT_MAX_GAS: u64 = 300_000; +const DEFAULT_GAS_PRICE_PRICE: f64 = 0.001; +const DEFAULT_GAS_PRICE_DENOM: &str = "uatom"; +const DEFAULT_GAS_PRICE_ADJUSTMENT: f64 = 0.1; -// TODO size this properly -const DEFAULT_MAX_GAS: u64 = 300000; const DEFAULT_MAX_MSG_NUM: usize = 30; const DEFAULT_MAX_TX_SIZE: usize = 2 * 1048576; // 2 MBytes -const DEFAULT_GAS_FEE_AMOUNT: u64 = 1000; + +mod retry_strategy { + use crate::util::retry::Fixed; + use std::time::Duration; + + pub fn wait_for_block_commits() -> impl Iterator { + // The total time should be higher than the full node timeout which defaults to 10sec. + Fixed::from_millis(300).take(40) // 12 seconds + } +} pub struct CosmosSdkChain { config: ChainConfig, @@ -87,6 +106,9 @@ pub struct CosmosSdkChain { grpc_addr: Uri, rt: Arc, keybase: KeyRing, + + /// A cached copy of the account information + account: Option, } impl CosmosSdkChain { @@ -144,125 +166,135 @@ impl CosmosSdkChain { self.rt.block_on(f) } - fn send_tx(&self, proto_msgs: Vec) -> Result, Error> { + fn send_tx(&mut self, proto_msgs: Vec) -> Result { crate::time!("send_tx"); - let key = self - .keybase() - .get_key(&self.config.key_name) - .map_err(|e| Kind::KeyBase.context(e))?; - - // Create TxBody - let body = TxBody { - messages: proto_msgs.to_vec(), - memo: "".to_string(), - timeout_height: 0_u64, - extension_options: Vec::::new(), - non_critical_extension_options: Vec::::new(), - }; - - // A protobuf serialization of a TxBody - let mut body_buf = Vec::new(); - prost::Message::encode(&body, &mut body_buf).unwrap(); + let account_seq = self.account_sequence()?; - let mut pk_buf = Vec::new(); - prost::Message::encode(&key.public_key.public_key.to_bytes(), &mut pk_buf).unwrap(); - - crate::time!("PK {:?}", hex::encode(key.public_key.public_key.to_bytes())); - - // Create a MsgSend proto Any message - let pk_any = Any { - type_url: "/cosmos.crypto.secp256k1.PubKey".to_string(), - value: pk_buf, - }; - - let acct_response = self - .block_on(query_account(self, key.account)) - .map_err(|e| Kind::Grpc.context(e))?; + debug!( + "send_tx: sending {} messages to {} using nonce {}", + proto_msgs.len(), + self.id(), + account_seq, + ); - let single = Single { mode: 1 }; - let sum_single = Some(Sum::Single(single)); - let mode = Some(ModeInfo { sum: sum_single }); - let signer_info = SignerInfo { - public_key: Some(pk_any), - mode_info: mode, - sequence: acct_response.sequence, - }; + let signer_info = self.signer(account_seq)?; + let fee = self.default_fee(); + let (body, body_buf) = tx_body_and_bytes(proto_msgs)?; + + let (auth_info, auth_buf) = auth_info_and_bytes(signer_info.clone(), fee.clone())?; + let signed_doc = self.signed_doc(body_buf.clone(), auth_buf, account_seq)?; + + // Try to simulate the Tx. + // It is possible that a batch of messages are fragmented by the caller (`send_msgs`) such that + // they do not individually verify. For example for the following batch + // [`MsgUpdateClient`, `MsgRecvPacket`, ..., `MsgRecvPacket`] + // if the batch is split in two TX-es, the second one will fail the simulation in `deliverTx` check + // In this case we just leave the gas un-adjusted, i.e. use `self.max_gas()` + let estimated_gas = self + .send_tx_simulate(SimulateRequest { + tx: Some(Tx { + body: Some(body), + auth_info: Some(auth_info), + signatures: vec![signed_doc], + }), + }) + .map_or(self.max_gas(), |sr| { + sr.gas_info.map_or(self.max_gas(), |g| g.gas_used) + }); + + if estimated_gas > self.max_gas() { + return Err(Kind::TxSimulateGasEstimateExceeded { + chain_id: self.id().clone(), + estimated_gas, + max_gas: self.max_gas(), + } + .into()); + } - let fee = Some(Fee { - amount: vec![self.fee()], - gas_limit: self.gas(), - payer: "".to_string(), - granter: "".to_string(), - }); + let adjusted_fee = self.fee_with_gas(estimated_gas); - let auth_info = AuthInfo { - signer_infos: vec![signer_info], + trace!( + "send_tx: {} based on the estimated gas, adjusting fee from {:?} to {:?}", + self.id(), fee, - }; - - // A protobuf serialization of a AuthInfo - let mut auth_buf = Vec::new(); - prost::Message::encode(&auth_info, &mut auth_buf).unwrap(); - - let sign_doc = SignDoc { - body_bytes: body_buf.clone(), - auth_info_bytes: auth_buf.clone(), - chain_id: self.config.clone().id.to_string(), - account_number: acct_response.account_number, - }; + adjusted_fee + ); - // A protobuf serialization of a SignDoc - let mut signdoc_buf = Vec::new(); - prost::Message::encode(&sign_doc, &mut signdoc_buf).unwrap(); - - // Sign doc and broadcast - let signed = self - .keybase - .sign_msg(&self.config.key_name, signdoc_buf) - .map_err(|e| Kind::KeyBase.context(e))?; + let (_auth_adjusted, auth_buf_adjusted) = auth_info_and_bytes(signer_info, adjusted_fee)?; + let account_number = self.account_number()?; + let signed_doc = + self.signed_doc(body_buf.clone(), auth_buf_adjusted.clone(), account_number)?; let tx_raw = TxRaw { body_bytes: body_buf, - auth_info_bytes: auth_buf, - signatures: vec![signed], + auth_info_bytes: auth_buf_adjusted, + signatures: vec![signed_doc], }; - let mut txraw_buf = Vec::new(); - prost::Message::encode(&tx_raw, &mut txraw_buf).unwrap(); - - crate::time!("TxRAW {:?}", hex::encode(txraw_buf.clone())); + let mut tx_bytes = Vec::new(); + prost::Message::encode(&tx_raw, &mut tx_bytes).unwrap(); let response = self - .block_on(broadcast_tx_commit(self, txraw_buf)) + .block_on(broadcast_tx_sync(self, tx_bytes)) .map_err(|e| Kind::Rpc(self.config.rpc_addr.clone()).context(e))?; - let res = tx_result_to_event(&self.config.id, response)?; + debug!( + "send_tx: broadcast_tx_sync to {}: {:?}", + self.id(), + response + ); + + self.incr_account_sequence()?; - Ok(res) + Ok(response) } - fn gas(&self) -> u64 { - self.config.gas.unwrap_or(DEFAULT_MAX_GAS) + /// The maximum amount of gas the relayer is willing to pay for a transaction + fn max_gas(&self) -> u64 { + self.config.max_gas.unwrap_or(DEFAULT_MAX_GAS) } - fn fee(&self) -> Coin { - let amount = self - .config - .clone() - .fee_amount - .unwrap_or(DEFAULT_GAS_FEE_AMOUNT); + /// The gas price + fn gas_price(&self) -> GasPrice { + self.config.gas_price.clone().unwrap_or_else(|| { + GasPrice::new(DEFAULT_GAS_PRICE_PRICE, DEFAULT_GAS_PRICE_DENOM.to_string()) + }) + } - Coin { - denom: self.config.fee_denom.clone(), - amount: amount.to_string(), - } + /// The gas price adjustment + fn gas_adjustment(&self) -> f64 { + self.config + .gas_adjustment + .unwrap_or(DEFAULT_GAS_PRICE_ADJUSTMENT) + } + + /// Adjusts the fee based on the configured `gas_adjustment` to prevent out of gas errors. + /// The actual gas cost, when a transaction is executed, may be slightly higher than the + /// one returned by the simulation. + fn apply_adjustment_to_gas(&self, gas_amount: u64) -> u64 { + min( + gas_amount + mul_ceil(gas_amount, self.gas_adjustment()), + self.max_gas(), + ) + } + + /// The maximum fee the relayer pays for a transaction + fn max_fee_in_coins(&self) -> Coin { + calculate_fee(self.max_gas(), self.gas_price()) } + /// The fee in coins based on gas amount + fn fee_from_gas_in_coins(&self, gas: u64) -> Coin { + calculate_fee(gas, self.gas_price()) + } + + /// The maximum number of messages included in a transaction fn max_msg_num(&self) -> usize { self.config.max_msg_num.unwrap_or(DEFAULT_MAX_MSG_NUM) } + /// The maximum size of any transaction sent by the relayer to this chain fn max_tx_size(&self) -> usize { self.config.max_tx_size.unwrap_or(DEFAULT_MAX_TX_SIZE) } @@ -316,6 +348,218 @@ impl CosmosSdkChain { Ok((proof, height)) } + + fn send_tx_simulate(&self, request: SimulateRequest) -> Result { + crate::time!("tx simulate"); + + let mut client = self + .block_on( + ibc_proto::cosmos::tx::v1beta1::service_client::ServiceClient::connect( + self.grpc_addr.clone(), + ), + ) + .map_err(|e| Kind::Grpc.context(e))?; + + let request = tonic::Request::new(request); + let response = self + .block_on(client.simulate(request)) + .map_err(|e| Kind::Grpc.context(e))? + .into_inner(); + + Ok(response) + } + + fn key(&self) -> Result { + Ok(self + .keybase() + .get_key(&self.config.key_name) + .map_err(|e| Kind::KeyBase.context(e))?) + } + + fn key_bytes(&self, key: &KeyEntry) -> Result, Error> { + let mut pk_buf = Vec::new(); + prost::Message::encode(&key.public_key.public_key.to_bytes(), &mut pk_buf).unwrap(); + Ok(pk_buf) + } + + fn key_and_bytes(&self) -> Result<(KeyEntry, Vec), Error> { + let key = self.key()?; + let key_bytes = self.key_bytes(&key)?; + Ok((key, key_bytes)) + } + + fn account(&mut self) -> Result<&mut BaseAccount, Error> { + if self.account == None { + let account = self + .block_on(query_account(self, self.key()?.account)) + .map_err(|e| Kind::Grpc.context(e))?; + + debug!( + sequence = %account.sequence, + number = %account.account_number, + "send_tx: retrieved account for {}", + self.id() + ); + + self.account = Some(account); + } + + Ok(self + .account + .as_mut() + .expect("account was supposedly just cached")) + } + + fn account_number(&mut self) -> Result { + Ok(self.account()?.account_number) + } + + fn account_sequence(&mut self) -> Result { + Ok(self.account()?.sequence) + } + + fn incr_account_sequence(&mut self) -> Result<(), Error> { + self.account()?.sequence += 1; + Ok(()) + } + + fn signer(&self, sequence: u64) -> Result { + let (_key, pk_buf) = self.key_and_bytes()?; + // Create a MsgSend proto Any message + let pk_any = Any { + type_url: "/cosmos.crypto.secp256k1.PubKey".to_string(), + value: pk_buf, + }; + + let single = Single { mode: 1 }; + let sum_single = Some(Sum::Single(single)); + let mode = Some(ModeInfo { sum: sum_single }); + let signer_info = SignerInfo { + public_key: Some(pk_any), + mode_info: mode, + sequence, + }; + Ok(signer_info) + } + + fn default_fee(&self) -> Fee { + Fee { + amount: vec![self.max_fee_in_coins()], + gas_limit: self.max_gas(), + payer: "".to_string(), + granter: "".to_string(), + } + } + + fn fee_with_gas(&self, gas_limit: u64) -> Fee { + let adjusted_gas_limit = self.apply_adjustment_to_gas(gas_limit); + Fee { + amount: vec![self.fee_from_gas_in_coins(adjusted_gas_limit)], + gas_limit: adjusted_gas_limit, + ..self.default_fee() + } + } + + fn signed_doc( + &self, + body_bytes: Vec, + auth_info_bytes: Vec, + account_number: u64, + ) -> Result, Error> { + let sign_doc = SignDoc { + body_bytes, + auth_info_bytes, + chain_id: self.config.clone().id.to_string(), + account_number, + }; + + // A protobuf serialization of a SignDoc + let mut signdoc_buf = Vec::new(); + prost::Message::encode(&sign_doc, &mut signdoc_buf).unwrap(); + + // Sign doc + let signed = self + .keybase + .sign_msg(&self.config.key_name, signdoc_buf) + .map_err(|e| Kind::KeyBase.context(e))?; + + Ok(signed) + } + + /// Given a vector of `TxSyncResult` elements, + /// each including a transaction response hash for one or more messages, periodically queries the chain + /// with the transaction hashes to get the list of IbcEvents included in those transactions. + pub fn wait_for_block_commits( + &self, + mut tx_sync_results: Vec, + ) -> Result, Error> { + use crate::util::retry::{retry_with_index, RetryResult}; + + trace!("waiting for commit of block(s)"); + + // Wait a little bit initially + thread::sleep(Duration::from_millis(200)); + + let start = Instant::now(); + + let result = retry_with_index(retry_strategy::wait_for_block_commits(), |index| { + if all_tx_results_found(&tx_sync_results) { + trace!( + "wait_for_block_commits: retrieved {} tx results after {} tries ({}ms)", + tx_sync_results.len(), + index, + start.elapsed().as_millis() + ); + + // All transactions confirmed + return RetryResult::Ok(()); + } + + for TxSyncResult { response, events } in tx_sync_results.iter_mut() { + // If this transaction was not committed, determine whether it was because it failed + // or because it hasn't been committed yet. + if empty_event_present(&events) { + // If the transaction failed, replace the events with an error, + // so that we don't attempt to resolve the transaction later on. + if response.code.value() != 0 { + *events = vec![IbcEvent::ChainError(format!( + "deliver_tx for Tx hash {} reports error: code={:?}, log={:?}", + response.hash, response.code, response.log + ))]; + + // Otherwise, try to resolve transaction hash to the corresponding events. + } else if let Ok(events_per_tx) = + self.query_txs(QueryTxRequest::Transaction(QueryTxHash(response.hash))) + { + // If we get events back, progress was made, so we replace the events + // with the new ones. in both cases we will check in the next iteration + // whether or not the transaction was fully committed. + if !events_per_tx.is_empty() { + *events = events_per_tx; + } + } + } + } + RetryResult::Retry(index) + }); + + match result { + // All transactions confirmed + Ok(()) => Ok(tx_sync_results), + // Did not find confirmation + Err(_) => Err(Kind::TxNoConfirmation.into()), + } + } +} + +fn empty_event_present(events: &[IbcEvent]) -> bool { + events.iter().any(|ev| matches!(ev, IbcEvent::Empty(_))) +} + +fn all_tx_results_found(tx_sync_results: &[TxSyncResult]) -> bool { + tx_sync_results + .iter() + .all(|r| !empty_event_present(&r.events)) } impl Chain for CosmosSdkChain { @@ -341,6 +585,7 @@ impl Chain for CosmosSdkChain { grpc_addr, rt, keybase, + account: None, }) } @@ -392,38 +637,61 @@ impl Chain for CosmosSdkChain { &mut self.keybase } - /// Send one or more transactions that include all the specified messages + /// Send one or more transactions that include all the specified messages. + /// The `proto_msgs` are split in transactions such they don't exceed the configured maximum + /// number of messages per transaction and the maximum transaction size. + /// Then `send_tx()` is called with each Tx. `send_tx()` determines the fee based on the + /// on-chain simulation and if this exceeds the maximum gas specified in the configuration file + /// then it returns error. + /// TODO - more work is required here for a smarter split maybe iteratively accumulating/ evaluating + /// msgs in a Tx until any of the max size, max num msgs, max fee are exceeded. fn send_msgs(&mut self, proto_msgs: Vec) -> Result, Error> { crate::time!("send_msgs"); if proto_msgs.is_empty() { - return Ok(vec![IbcEvent::Empty("No messages to send".to_string())]); + return Ok(vec![]); } - let mut res = vec![]; + let mut tx_sync_results = vec![]; let mut n = 0; let mut size = 0; let mut msg_batch = vec![]; for msg in proto_msgs.iter() { - msg_batch.append(&mut vec![msg.clone()]); + msg_batch.push(msg.clone()); let mut buf = Vec::new(); prost::Message::encode(msg, &mut buf).unwrap(); n += 1; size += buf.len(); if n >= self.max_msg_num() || size >= self.max_tx_size() { - let mut result = self.send_tx(msg_batch)?; - res.append(&mut result); + let events_per_tx = vec![IbcEvent::Empty("".to_string()); msg_batch.len()]; + let tx_sync_result = self.send_tx(msg_batch)?; + tx_sync_results.push(TxSyncResult { + response: tx_sync_result, + events: events_per_tx, + }); n = 0; size = 0; msg_batch = vec![]; } } if !msg_batch.is_empty() { - let mut result = self.send_tx(msg_batch)?; - res.append(&mut result); + let events_per_tx = vec![IbcEvent::Empty("".to_string()); msg_batch.len()]; + let tx_sync_result = self.send_tx(msg_batch)?; + tx_sync_results.push(TxSyncResult { + response: tx_sync_result, + events: events_per_tx, + }); } - Ok(res) + let tx_sync_results = self.wait_for_block_commits(tx_sync_results)?; + + let events = tx_sync_results + .into_iter() + .map(|el| el.events) + .flatten() + .collect(); + + Ok(events) } /// Get the account for the signer @@ -697,10 +965,11 @@ impl Chain for CosmosSdkChain { let request = tonic::Request::new(request); - let response = self - .block_on(client.client_connections(request)) - .map_err(|e| Kind::Grpc.context(e))? - .into_inner(); + let response = match self.block_on(client.client_connections(request)) { + Ok(res) => res.into_inner(), + Err(e) if e.code() == tonic::Code::NotFound => return Ok(vec![]), + Err(e) => return Err(Kind::Grpc.context(e).into()), + }; // TODO: add warnings for any identifiers that fail to parse (below). // similar to the parsing in `query_connection_channels`. @@ -778,7 +1047,7 @@ impl Chain for CosmosSdkChain { .insert("x-cosmos-block-height", height_param); let response = client.connection(request).await.map_err(|e| { - if e.code() == Code::NotFound { + if e.code() == tonic::Code::NotFound { Kind::ConnectionNotFound(connection_id.clone()).into() } else { Kind::Grpc.context(e) @@ -1142,6 +1411,25 @@ impl Chain for CosmosSdkChain { Ok(event.into_iter().collect()) } + + QueryTxRequest::Transaction(tx) => { + let mut response = self + .block_on(self.rpc_client.tx_search( + tx_hash_query(&tx), + false, + 1, + 1, // get only the first Tx matching the query + Order::Ascending, + )) + .map_err(|e| Kind::Grpc.context(e))?; + + if response.txs.is_empty() { + Ok(vec![]) + } else { + let tx = response.txs.remove(0); + Ok(all_ibc_events_from_tx_search_response(self.id(), tx)) + } + } } } @@ -1380,6 +1668,10 @@ fn header_query(request: &QueryClientEventRequest) -> Query { ) } +fn tx_hash_query(request: &QueryTxHash) -> Query { + tendermint_rpc::query::Query::eq("tx.hash", request.0.to_string()) +} + // Extract the packet events from the query_txs RPC response. For any given // packet query, there is at most one Tx matching such query. Moreover, a Tx may // contain several events, but a single one must match the packet query. @@ -1456,6 +1748,25 @@ fn update_client_from_tx_search_response( .map(IbcEvent::UpdateClient) } +fn all_ibc_events_from_tx_search_response(chain_id: &ChainId, response: ResultTx) -> Vec { + let height = ICSHeight::new(chain_id.version(), u64::from(response.height)); + let deliver_tx_result = response.tx_result; + if deliver_tx_result.code.is_err() { + return vec![IbcEvent::ChainError(format!( + "deliver_tx for {} reports error: code={:?}, log={:?}", + response.hash, deliver_tx_result.code, deliver_tx_result.log + ))]; + } + + let mut result = vec![]; + for event in deliver_tx_result.events { + if let Some(ibc_ev) = from_tx_response_event(height, &event) { + result.push(ibc_ev); + } + } + result +} + /// Perform a generic `abci_query`, and return the corresponding deserialized response data. async fn abci_query( chain: &CosmosSdkChain, @@ -1504,14 +1815,14 @@ async fn abci_query( Ok(response) } -/// Perform a `broadcast_tx_commit`, and return the corresponding deserialized response data. -async fn broadcast_tx_commit( +/// Perform a `broadcast_tx_sync`, and return the corresponding deserialized response data. +async fn broadcast_tx_sync( chain: &CosmosSdkChain, data: Vec, ) -> Result> { let response = chain .rpc_client() - .broadcast_tx_commit(data.into()) + .broadcast_tx_sync(data.into()) .await .map_err(|e| Kind::Rpc(chain.config.rpc_addr.clone()).context(e))?; @@ -1544,35 +1855,6 @@ async fn query_account(chain: &CosmosSdkChain, address: String) -> Result Result, anomaly::Error> { - let mut result = vec![]; - - // Verify the return codes from check_tx and deliver_tx - if response.check_tx.code.is_err() { - return Ok(vec![IbcEvent::ChainError(format!( - "check_tx reports error: log={:?}", - response.check_tx.log - ))]); - } - if response.deliver_tx.code.is_err() { - return Ok(vec![IbcEvent::ChainError(format!( - "deliver_tx reports error: log={:?}", - response.deliver_tx.log - ))]); - } - - let height = ICSHeight::new(chain_id.version(), u64::from(response.height)); - for event in response.deliver_tx.events { - if let Some(ibc_ev) = from_tx_response_event(height, &event) { - result.push(ibc_ev); - } - } - Ok(result) -} - fn encode_to_bech32(address: &str, account_prefix: &str) -> Result { let account = AccountId::from_str(address).map_err(|_| Kind::InvalidKeyAddress(address.to_string()))?; @@ -1586,7 +1868,7 @@ fn encode_to_bech32(address: &str, account_prefix: &str) -> Result Option { +fn client_id_suffix(client_id: &ClientId) -> Option { client_id .as_str() .split('-') @@ -1594,3 +1876,69 @@ pub fn client_id_suffix(client_id: &ClientId) -> Option { .map(|e| e.parse::().ok()) .flatten() } + +pub struct TxSyncResult { + // the broadcast_tx_sync response + response: Response, + // the events generated by a Tx once executed + events: Vec, +} + +fn auth_info_and_bytes(signer_info: SignerInfo, fee: Fee) -> Result<(AuthInfo, Vec), Error> { + let auth_info = AuthInfo { + signer_infos: vec![signer_info], + fee: Some(fee), + }; + + // A protobuf serialization of a AuthInfo + let mut auth_buf = Vec::new(); + prost::Message::encode(&auth_info, &mut auth_buf).unwrap(); + Ok((auth_info, auth_buf)) +} + +fn tx_body_and_bytes(proto_msgs: Vec) -> Result<(TxBody, Vec), Error> { + // Create TxBody + let body = TxBody { + messages: proto_msgs.to_vec(), + memo: "".to_string(), + timeout_height: 0_u64, + extension_options: Vec::::new(), + non_critical_extension_options: Vec::::new(), + }; + // A protobuf serialization of a TxBody + let mut body_buf = Vec::new(); + prost::Message::encode(&body, &mut body_buf).unwrap(); + Ok((body, body_buf)) +} + +fn calculate_fee(adjusted_gas_amount: u64, gas_price: GasPrice) -> Coin { + let fee_amount = mul_ceil(adjusted_gas_amount, gas_price.price); + + Coin { + denom: gas_price.denom, + amount: fee_amount.to_string(), + } +} + +fn mul_ceil(a: u64, f: f64) -> u64 { + use fraction::Fraction as F; + + // Safe to unwrap below as are multiplying two finite fractions + // together, and rounding them to the nearest integer. + let n = (F::from(a) * F::from(f)).ceil(); + n.numer().unwrap() / n.denom().unwrap() +} + +#[cfg(test)] +mod tests { + #[test] + fn mul_ceil() { + assert_eq!(super::mul_ceil(300_000, 0.001), 300); + assert_eq!(super::mul_ceil(300_004, 0.001), 301); + assert_eq!(super::mul_ceil(300_040, 0.001), 301); + assert_eq!(super::mul_ceil(300_400, 0.001), 301); + assert_eq!(super::mul_ceil(304_000, 0.001), 304); + assert_eq!(super::mul_ceil(340_000, 0.001), 340); + assert_eq!(super::mul_ceil(340_001, 0.001), 341); + } +} diff --git a/relayer/src/chain/counterparty.rs b/relayer/src/chain/counterparty.rs index 370444079a..bd35ce068a 100644 --- a/relayer/src/chain/counterparty.rs +++ b/relayer/src/chain/counterparty.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use tracing::{error, trace}; +use tracing::error; use ibc::{ ics02_client::client_state::{ClientState, IdentifiedAnyClientState}, @@ -43,12 +43,12 @@ pub fn channel_connection_client( port_id: &PortId, channel_id: &ChannelId, ) -> Result { - trace!( - chain_id = %chain.id(), - port_id = %port_id, - channel_id = %channel_id, - "getting counterparty chain" - ); + // trace!( + // chain_id = %chain.id(), + // port_id = %port_id, + // channel_id = %channel_id, + // "getting counterparty chain" + // ); let channel_end = chain .query_channel(port_id, channel_id, Height::zero()) @@ -80,10 +80,10 @@ pub fn channel_connection_client( .query_client_state(client_id, Height::zero()) .map_err(|e| Error::QueryFailed(format!("{}", e)))?; - trace!( - chain_id=%chain.id(), port_id=%port_id, channel_id=%channel_id, - "counterparty chain: {}", client_state.chain_id() - ); + // trace!( + // chain_id=%chain.id(), port_id=%port_id, channel_id=%channel_id, + // "counterparty chain: {}", client_state.chain_id() + // ); let client = IdentifiedAnyClientState::new(client_id.clone(), client_state); let connection = IdentifiedConnectionEnd::new(connection_id.clone(), connection_end); diff --git a/relayer/src/chain/mock.rs b/relayer/src/chain/mock.rs index 96078d5fb0..defe908a02 100644 --- a/relayer/src/chain/mock.rs +++ b/relayer/src/chain/mock.rs @@ -403,9 +403,9 @@ pub mod test_utils { account_prefix: "".to_string(), key_name: "".to_string(), store_prefix: "".to_string(), - gas: None, - fee_denom: "stake".to_string(), - fee_amount: Some(1000), + max_gas: None, + gas_price: None, + gas_adjustment: None, max_msg_num: None, max_tx_size: None, clock_drift: Duration::from_secs(5), diff --git a/relayer/src/config.rs b/relayer/src/config.rs index 80c4837ef8..7658ade2c4 100644 --- a/relayer/src/config.rs +++ b/relayer/src/config.rs @@ -1,6 +1,6 @@ //! Relayer configuration -use std::{fs, fs::File, io::Write, path::Path, time::Duration}; +use std::{fmt, fs, fs::File, io::Write, path::Path, time::Duration}; use serde_derive::{Deserialize, Serialize}; use tendermint_light_client::types::TrustThreshold; @@ -10,6 +10,24 @@ use ibc::timestamp::ZERO_DURATION; use crate::error; +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct GasPrice { + pub price: f64, + pub denom: String, +} + +impl GasPrice { + pub const fn new(price: f64, denom: String) -> Self { + Self { price, denom } + } +} + +impl fmt::Display for GasPrice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", self.price, self.denom) + } +} + /// Defaults for various fields pub mod default { use super::*; @@ -112,17 +130,19 @@ pub struct ChainConfig { pub account_prefix: String, pub key_name: String, pub store_prefix: String, - pub gas: Option, - pub fee_denom: String, - pub fee_amount: Option, + pub max_gas: Option, + pub gas_adjustment: Option, pub max_msg_num: Option, pub max_tx_size: Option, #[serde(default = "default::clock_drift", with = "humantime_serde")] pub clock_drift: Duration, #[serde(default = "default::trusting_period", with = "humantime_serde")] pub trusting_period: Duration, + + // these two need to be last otherwise we run into `ValueAfterTable` error when serializing to TOML #[serde(default)] pub trust_threshold: TrustThreshold, + pub gas_price: Option, } /// Attempt to load and parse the TOML config file as a `Config`. @@ -183,9 +203,8 @@ mod tests { ); let config = parse(path).expect("could not parse config"); - let mut buffer = Vec::new(); - let result = store_writer(&config, &mut buffer); - assert!(result.is_ok()); + let mut buffer = Vec::new(); + store_writer(&config, &mut buffer).unwrap(); } } diff --git a/relayer/src/error.rs b/relayer/src/error.rs index 26c6103275..b87e4c712d 100644 --- a/relayer/src/error.rs +++ b/relayer/src/error.rs @@ -5,7 +5,7 @@ use thiserror::Error; use ibc::{ ics02_client::client_type::ClientType, - ics24_host::identifier::{ChannelId, ConnectionId}, + ics24_host::identifier::{ChainId, ChannelId, ConnectionId}, }; /// An error that can be raised by the relayer. @@ -78,6 +78,18 @@ pub enum Kind { #[error("Failed to create client state")] BuildClientStateFailure, + /// Gas estimate from simulated Tx exceeds the maximum configured + #[error("{chain_id} gas estimate {estimated_gas} from simulated Tx exceeds the maximum configured {max_gas}")] + TxSimulateGasEstimateExceeded { + chain_id: ChainId, + estimated_gas: u64, + max_gas: u64, + }, + + /// Tx failure for lack of confirmation + #[error("Failed Tx: no confirmation")] + TxNoConfirmation, + /// Create client failure #[error("Failed to create client {0}")] CreateClient(String), diff --git a/relayer/src/foreign_client.rs b/relayer/src/foreign_client.rs index 2ebe482a08..093bccd8dd 100644 --- a/relayer/src/foreign_client.rs +++ b/relayer/src/foreign_client.rs @@ -1,6 +1,7 @@ use std::time::Instant; use std::{fmt, thread, time::Duration}; +use itertools::Itertools; use prost_types::Any; use thiserror::Error; use tracing::{debug, error, info, trace, warn}; @@ -756,15 +757,16 @@ impl ForeignClient { }; debug!( - "[{}] checking misbehaviour at {}, number of consensus states {}", + "[{}] checking misbehaviour at {}, number of consensus states: {}", self, ch, consensus_state_heights.len() ); + trace!( - "[{}] checking misbehaviour for consensus state heights {:?}", + "[{}] checking misbehaviour for consensus state heights (first 50 shown here): {}", self, - consensus_state_heights + consensus_state_heights.iter().take(50).join(", ") ); let check_once = update.is_some(); diff --git a/relayer/src/link.rs b/relayer/src/link.rs index b991495c92..c7e69f3d7c 100644 --- a/relayer/src/link.rs +++ b/relayer/src/link.rs @@ -616,11 +616,21 @@ impl RelayPath { return Ok(summary); } Err(LinkError::SendError(ev)) => { - // This error means we can retry + // This error means we could retry error!("[{}] error {}", self, ev); - match self.regenerate_operational_data(odata.clone()) { - None => return Ok(RelaySummary::empty()), // Nothing to retry - Some(new_od) => odata = new_od, + if i + 1 == MAX_RETRIES { + error!( + "[{}] {}/{} retries exhausted. giving up", + self, + i + 1, + MAX_RETRIES + ) + } else { + // If we haven't exhausted all retries, regenerate the op. data & retry + match self.regenerate_operational_data(odata.clone()) { + None => return Ok(RelaySummary::empty()), // Nothing to retry + Some(new_od) => odata = new_od, + } } } Err(e) => { diff --git a/relayer/src/transfer.rs b/relayer/src/transfer.rs index 8d1830d65c..aa3329fa5b 100644 --- a/relayer/src/transfer.rs +++ b/relayer/src/transfer.rs @@ -23,7 +23,7 @@ pub enum PacketError { Key(Error), #[error( - "failed during a transaction submission step to chain id {0} with underlying error: {1}" + "failed while submitting the Transfer message to chain {0} with underlying error: {1}" )] Submit(ChainId, Error), diff --git a/relayer/tests/config/fixtures/relayer_conf_example.toml b/relayer/tests/config/fixtures/relayer_conf_example.toml index 344f714884..bd3d322a39 100644 --- a/relayer/tests/config/fixtures/relayer_conf_example.toml +++ b/relayer/tests/config/fixtures/relayer_conf_example.toml @@ -11,17 +11,13 @@ rpc_timeout = '10s' account_prefix = 'cosmos' key_name = 'testkey' store_prefix = 'ibc' -gas = 200000 -fee_denom = 'stake' -fee_amount = 10 +max_gas = 200000 +gas_price = { price = 0.001, denom = 'stake' } max_msg_num = 4 max_tx_size = 1048576 clock_drift = '5s' trusting_period = '14days' - -[chains.trust_threshold] -numerator = '1' -denominator = '3' +trust_threshold = { numerator = '1', denominator = '3' } [[chains]] id = 'chain_B' @@ -32,12 +28,8 @@ rpc_timeout = '10s' account_prefix = 'cosmos' key_name = 'testkey' store_prefix = 'ibc' -fee_denom = 'stake' -fee_amount = 10 +gas_price = { price = 0.001, denom = 'stake' } clock_drift = '5s' trusting_period = '14days' - -[chains.trust_threshold] -numerator = '1' -denominator = '3' +trust_threshold = { numerator = '1', denominator = '3' } diff --git a/scripts/gm/bin/lib-gm b/scripts/gm/bin/lib-gm index d29256f4cb..0628480e91 100644 --- a/scripts/gm/bin/lib-gm +++ b/scripts/gm/bin/lib-gm @@ -765,23 +765,19 @@ EOF DENOM="$(get_staking_denom "$i")" cat <> "$GLOBAL_HERMES_CONFIG" [[chains]] -id='${ID}' -rpc_addr='http://localhost:${RPC}' -grpc_addr='http://localhost:${GRPC}' -websocket_addr='ws://localhost:${RPC}/websocket' -rpc_timeout='1s' -account_prefix='${ACCOUNT_PREFIX}' -key_name='wallet' -store_prefix='ibc' -fee_denom='${DENOM}' -fee_amount=1000 -gas=500000 -clock_drift='5s' -trusting_period='14days' - -[chains.trust_threshold] -numerator='1' -denominator='3' +id = '${ID}' +rpc_addr = 'http://localhost:${RPC}' +grpc_addr = 'http://localhost:${GRPC}' +websocket_addr = 'ws://localhost:${RPC}/websocket' +rpc_timeout = '1s' +account_prefix = '${ACCOUNT_PREFIX}' +key_name = 'wallet' +store_prefix = 'ibc' +gas_price = { price = 0.001, denom = '${DENOM}' } +max_gas = 500000 +clock_drift = '5s' +trusting_period = '14days' +trust_threshold = { numerator = '1', denominator = '3' } EOF done