Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle some gas corner-cases #2537

Merged
merged 35 commits into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7ad5084
A stab at #2487
adizere Aug 4, 2022
634fb08
A stab at #2487
adizere Aug 4, 2022
ac7a9aa
Merge branch '2487_gas_fixes' of https://github.com/informalsystems/i…
ljoss17 Aug 4, 2022
aae6868
Updated interval between client refresh from 1 second to 5 seconds
ljoss17 Aug 5, 2022
3137a64
Updated refresh in order to return ignorable error if there is a chai…
ljoss17 Aug 8, 2022
32c1f5f
Clippy and fmt
ljoss17 Aug 8, 2022
0c5a177
Updated interval between client refresh to depend on the trusting period
ljoss17 Aug 8, 2022
b225c71
fmt
ljoss17 Aug 8, 2022
00ad6e7
Merge branch 'master' into 2487_gas_fixes
ljoss17 Aug 9, 2022
0dd3fe1
Added changelog entry
ljoss17 Aug 10, 2022
9c9040f
Apply suggestions from code review
ljoss17 Aug 15, 2022
af17334
Merge branch '2487_gas_fixes' of https://github.com/informalsystems/i…
ljoss17 Aug 15, 2022
7d4314e
Merge branch 'master' into 2487_gas_fixes
ljoss17 Aug 15, 2022
b77ec3e
Added new 'GasMultiplier' type for configuration. Fixed clippy errors
ljoss17 Aug 15, 2022
17b3840
cargo fmt
ljoss17 Aug 15, 2022
49050ae
Fixed error in relayer-cli 'gas_multiplier' validation
ljoss17 Aug 15, 2022
e47ad36
Added integration test for client 'refresh()'
ljoss17 Aug 15, 2022
06cfdcd
Refactor override code to override_connected_chains
soareschen Aug 15, 2022
2b3cbab
Documented client refresh integration test and moved related methods …
ljoss17 Aug 15, 2022
59c357d
Add timeout limit to CI tests
soareschen Aug 16, 2022
ca7ca04
Merge branch 'master' into 2487_gas_fixes
ljoss17 Aug 16, 2022
f59e9b9
Merge branch 'soares/ci-timeout' into 2487_gas_fixes
ljoss17 Aug 16, 2022
8aa2877
Improved GasMultiplier type. Updated 'spawn_refresh_client' to increm…
ljoss17 Aug 16, 2022
d8a831a
Clippy fix
ljoss17 Aug 16, 2022
8014545
Merge branch 'master' into 2487_gas_fixes
ljoss17 Aug 16, 2022
2dcb204
Removed 'gas_multiplier' validation, as this is done during deseriali…
ljoss17 Aug 16, 2022
ddfec74
Removed outdated comment
ljoss17 Aug 16, 2022
5c522c4
Moved GasMultiplier to 'gas_multiplier.rs' file. Removed unused Error…
ljoss17 Aug 19, 2022
64b16d6
Merge branch 'master' into 2487_gas_fixes
ljoss17 Aug 24, 2022
47a49b9
Removed cloning the vector of 'IbcEvent' in 'refresh()'
ljoss17 Aug 24, 2022
ffa0bbd
Cleanup 'refresh()' method
ljoss17 Aug 26, 2022
f949690
Merge branch 'master' into 2487_gas_fixes
ljoss17 Aug 26, 2022
6cf2fc7
Fixed issue with new GasMultiplier struct in chain registry
ljoss17 Aug 26, 2022
b596d66
Cargo fmt
ljoss17 Aug 26, 2022
e82c9d9
Updated 'refresh()' method following comment
ljoss17 Aug 26, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Added health-check to warn user if 'gas_multiplier' is smaller than 1.1.
Improved client refresh frequency to depend on 'trusting_period'.
([#2487](https://github.com/informalsystems/ibc-rs/issues/2487))
2 changes: 1 addition & 1 deletion modules/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const TIMEOUT_EVENT: &str = "timeout_packet";
const TIMEOUT_ON_CLOSE_EVENT: &str = "timeout_packet_on_close";

/// Events types
#[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub enum IbcEventType {
NewBlock,
CreateClient,
Expand Down
10 changes: 0 additions & 10 deletions relayer-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,16 +170,6 @@ fn validate_trust_threshold(
}

fn validate_gas_settings(id: &ChainId, config: &ChainConfig) -> Result<(), Diagnostic<Error>> {
// Check that the gas_multiplier is greater than or equal to 1.0
if let Some(gas_multiplier) = config.gas_multiplier {
if gas_multiplier < 1.0 {
return Err(Diagnostic::Error(Error::invalid_gas_multiplier(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take care to also remove the Error enum variant if it is no longer in use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh thank you, I forgot to remove it.

gas_multiplier,
id.clone(),
)));
}
}

// Check that the gas_adjustment option is not set
if let Some(gas_adjustment) = config.gas_adjustment {
let gas_multiplier = gas_adjustment + 1.0;
Expand Down
13 changes: 12 additions & 1 deletion relayer/src/chain/cosmos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ use crate::chain::cosmos::query::tx::query_txs;
use crate::chain::cosmos::query::{abci_query, fetch_version_specs, packet_query, QueryResponse};
use crate::chain::cosmos::types::account::Account;
use crate::chain::cosmos::types::config::TxConfig;
use crate::chain::cosmos::types::gas::{default_gas_from_config, max_gas_from_config};
use crate::chain::cosmos::types::gas::{
default_gas_from_config, gas_multiplier_from_config, max_gas_from_config,
};
use crate::chain::endpoint::{ChainEndpoint, ChainStatus, HealthCheck};
use crate::chain::tracking::TrackedMsgs;
use crate::config::ChainConfig;
Expand Down Expand Up @@ -223,6 +225,15 @@ impl CosmosSdkChain {
}
}

let gas_multiplier = gas_multiplier_from_config(&self.config);

if gas_multiplier < 1.1 {
return Err(Error::config_validation_gas_multiplier_low(
self.id().clone(),
gas_multiplier,
));
}

Ok(())
}

Expand Down
2 changes: 0 additions & 2 deletions relayer/src/chain/cosmos/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ fn adjust_estimated_gas(
gas_amount,
}: AdjustGas,
) -> u64 {
assert!(gas_multiplier >= 1.0);

Comment on lines -74 to -75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather keep it here, just in case someone removes the checks in a refactoring.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assert! was replaced so that the test, in client_refresh.rs, using a gas_multiplier = 0.8 to trigger a failed refresh() runs correctly. The checks are now in the new GasMultiplier struct:
https://github.com/informalsystems/ibc-rs/blob/5c522c4d72659ceec7b81ee31ecc3dddf9460700/relayer/src/config/gas_multiplier.rs#L18-L27
I am not sure if there is a way to easily reproduce the failure while keeping a gas_multiplier >= 1.0.

// No need to compute anything if the gas amount is zero
if gas_amount == 0 {
return 0;
Expand Down
7 changes: 2 additions & 5 deletions relayer/src/chain/cosmos/types/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ use crate::config::{ChainConfig, GasPrice};
/// Default gas limit when submitting a transaction.
const DEFAULT_MAX_GAS: u64 = 400_000;

/// By default, we do not increase the estimated gas to compute the fee.
const DEFAULT_GAS_MULTIPLIER: f64 = 1.1;

const DEFAULT_FEE_GRANTER: &str = "";

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -48,8 +45,8 @@ pub fn max_gas_from_config(config: &ChainConfig) -> u64 {
}

/// The gas multiplier
fn gas_multiplier_from_config(config: &ChainConfig) -> f64 {
config.gas_multiplier.unwrap_or(DEFAULT_GAS_MULTIPLIER)
pub fn gas_multiplier_from_config(config: &ChainConfig) -> f64 {
config.gas_multiplier.unwrap_or_default().to_f64()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

/// Get the fee granter address
Expand Down
4 changes: 2 additions & 2 deletions relayer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use ibc::core::ics24_host::identifier::{ChainId, ChannelId, PortId};
use ibc::timestamp::ZERO_DURATION;

use crate::chain::ChainType;
use crate::config::types::{MaxMsgNum, MaxTxSize, Memo};
use crate::config::types::{GasMultiplier, MaxMsgNum, MaxTxSize, Memo};
use crate::keyring::Store;

pub use error::Error;
Expand Down Expand Up @@ -335,7 +335,7 @@ pub struct ChainConfig {

// This field is deprecated, use `gas_multiplier` instead
pub gas_adjustment: Option<f64>,
pub gas_multiplier: Option<f64>,
pub gas_multiplier: Option<GasMultiplier>,

pub fee_granter: Option<String>,
#[serde(default)]
Expand Down
110 changes: 110 additions & 0 deletions relayer/src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,86 @@ pub mod memo {
}
}

pub use gas_multiplier::GasMultiplier;

pub mod gas_multiplier {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to create a separate module file, this is quite long already for an inline module.


flex_error::define_error! {
Error {
TooSmall
{ value: f64 }
|e| {
format_args!("`gas_multiplier` must be greater than or equal to {}, found {}",
GasMultiplier::MIN_BOUND, e.value)
},
}
}

#[derive(Debug, Clone, Copy)]
pub struct GasMultiplier(f64);

impl GasMultiplier {
const DEFAULT: f64 = 1.1;
const MIN_BOUND: f64 = 1.0;

pub fn new(value: f64) -> Result<Self, Error> {
if value < Self::MIN_BOUND {
return Err(Error::too_small(value));
}
Ok(Self(value))
}

// Unsafe GasMultiplier used for test cases only.
pub fn unsafe_new(value: f64) -> Self {
Self(value)
}

pub fn to_f64(self) -> f64 {
self.0
}
}

impl Default for GasMultiplier {
fn default() -> Self {
Self(Self::DEFAULT)
}
}

use serde::de::Unexpected;
use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
ljoss17 marked this conversation as resolved.
Show resolved Hide resolved

impl<'de> Deserialize<'de> for GasMultiplier {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = f64::deserialize(deserializer)?;

GasMultiplier::new(value).map_err(|e| match e.detail() {
ErrorDetail::TooSmall(_) => D::Error::invalid_value(
Unexpected::Float(value),
&format!("a f64 less than {}", Self::MIN_BOUND).as_str(),
ljoss17 marked this conversation as resolved.
Show resolved Hide resolved
),
})
}
}

impl Serialize for GasMultiplier {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}

impl From<GasMultiplier> for f64 {
fn from(m: GasMultiplier) -> Self {
m.0
}
}
}

#[cfg(test)]
#[allow(dead_code)] // the fields of the structs defined below are never accessed
mod tests {
Expand Down Expand Up @@ -323,4 +403,34 @@ mod tests {

assert!(err.contains("a string length of at most"));
}

#[test]
fn parse_invalid_gas_multiplier() {
#[derive(Debug, Deserialize)]
struct DummyConfig {
gas_multiplier: GasMultiplier,
}

let err = toml::from_str::<DummyConfig>("gas_multiplier = 0.9")
.unwrap_err()
.to_string();

assert!(err.contains("expected a f64 less than"));
}

#[test]
fn safe_gas_multiplier() {
let gas_multiplier = GasMultiplier::new(0.6);
assert!(
gas_multiplier.is_err(),
"Gas multiplier should be an error if value is lower than 1.0: {:?}",
gas_multiplier
);
}

#[test]
fn unsafe_gas_multiplier() {
let gas_multiplier = GasMultiplier::unsafe_new(0.6);
assert_eq!(gas_multiplier.to_f64(), 0.6);
}
}
9 changes: 9 additions & 0 deletions relayer/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,15 @@ define_error! {
e.chain_id, e.default_gas, e.max_gas)
},

ConfigValidationGasMultiplierLow
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many ConfigValidationThisAndThat variants in this enum. Prefixing in Rust usually means we should refactor things in a more elegant and concise way.

I wonder if there is a ConfigValidationError enum hiding here, which would make one variant of relayer::Error and be also convertible to that with a From impl. Maybe not for this PR, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will open an issue for this

{
chain_id: ChainId,
gas_multiplier: f64,
}
|e| {
format!("semantic config validation failed for option `gas_multiplier` of chain '{}', reason: gas multiplier ({}) is smaller than `1.1`, which could trigger gas fee errors in production", e.chain_id, e.gas_multiplier)
},

SdkModuleVersion
{
chain_id: ChainId,
Expand Down
32 changes: 31 additions & 1 deletion relayer/src/foreign_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use ibc::core::ics02_client::msgs::upgrade_client::MsgUpgradeAnyClient;
use ibc::core::ics02_client::trust_threshold::TrustThreshold;
use ibc::core::ics24_host::identifier::{ChainId, ClientId};
use ibc::downcast;
use ibc::events::{IbcEvent, WithBlockDataType};
use ibc::events::{IbcEvent, IbcEventType, WithBlockDataType};
use ibc::timestamp::{Timestamp, TimestampOverflowError};
use ibc::tx_msg::Msg;
use ibc::Height;
Expand Down Expand Up @@ -783,6 +783,36 @@ impl<DstChain: ChainHandle, SrcChain: ChainHandle> ForeignClient<DstChain, SrcCh
}

pub fn refresh(&mut self) -> Result<Option<Vec<IbcEvent>>, ForeignClientError> {
match self.try_refresh() {
Ok(res) => {
// If elapsed < refresh_window for the client, `try_refresh()` will
// be successful with an empty vector.
if let Some(ibc_events) = res.clone() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could avoid cloning the whole collection here: if it destructures to Some(ibc_events), you already own it, if it's None, you don't need the res variable to return Ok(None). Can also use convenience methods here like Option::map.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, so I removed the clone() in commit 47a49b9, but I am not sure I understand how Option::map could be used here. Would the idea be to apply the .map() to res in order to avoid the Ok(None) when res is None?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, see if res.map(|ibc_events| { ... }) could be more elegant here.

// The assumption is that only one IbcEventType::ChainError will be
// in the resulting Vec<IbcEvent> if an error occurred.
match ibc_events
.into_iter()
.find(|e| e.event_type() == IbcEventType::ChainError)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should document the assumption we're making.

Suggested change
.find(|e| e.event_type() == IbcEventType::ChainError)
// We make the assumption that we push a single `ChainError` variant
.find(|e| e.event_type() == IbcEventType::ChainError)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, alternatively, we could have strong type guarantees on this. CC @soareschen @romac

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like @soareschen, I think we should get rid of these ChainError events altogether and figure out a better way to bubble up such errors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked in #2565

{
Some(ev) => {
return Err(ForeignClientError::chain_error_event(
self.dst_chain().id(),
ev,
));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many different execution branches and it's not clear what are the post-conditions of the refresh method. Can we briefly describe that?

Let's maybe also put a comment to describe what is this match arm for. There is also an Ok(res) and a Err(e branches.

None => {
return Ok(res);
}
}
}
// Return the successful empty vector.
Ok(res)
}
Err(e) => Err(e),
}
}

fn try_refresh(&mut self) -> Result<Option<Vec<IbcEvent>>, ForeignClientError> {
let (client_state, elapsed) = self.validated_client_state()?;

// The refresh_window is the maximum duration
Expand Down
2 changes: 1 addition & 1 deletion relayer/src/link/relay_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ impl<ChainA: ChainHandle, ChainB: ChainHandle> RelayPath<ChainA, ChainB> {
match self.send_from_operational_data::<S>(&odata) {
Ok(reply) => {
// Done with this op. data
info!("success");
info!("submitted");
telemetry!({
let (chain, counterparty, channel_id, port_id) =
self.target_info(odata.target);
Expand Down
6 changes: 5 additions & 1 deletion relayer/src/link/relay_sender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ impl Submit for AsyncSender {
.send_messages_and_wait_check_tx(msgs)
.map_err(LinkError::relayer)?;
let reply = AsyncReply { responses: a };
info!("[Async~>{}] {}\n", target.id(), reply);

// Note: There may be errors in the reply, for example:
// `Response { code: Err(11), data: Data([]), log: Log("Too much gas wanted: 35000000, maximum is 25000000: out of gas")`
// The runtime deliberately did not catch or retry on such errors.
info!(target_chain = %target.id(), "{}", reply);

Ok(reply)
}
Expand Down
48 changes: 39 additions & 9 deletions relayer/src/worker/client.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
use core::convert::Infallible;
use core::time::Duration;
use crossbeam_channel::Receiver;
use std::time::Instant;
use tracing::{debug, span, trace, warn};

use ibc::events::IbcEvent;
use retry::delay::Fibonacci;
use retry::retry_with_index;

use crate::util::retry::clamp_total;
use crate::util::task::{spawn_background_task, Next, TaskError, TaskHandle};
use crate::{
chain::handle::ChainHandle,
foreign_client::{ForeignClient, HasExpiredOrFrozenError, MisbehaviourResults},
foreign_client::{ForeignClient, MisbehaviourResults},
};

use super::WorkerCmd;

const REFRESH_INTERVAL_SECONDS: u64 = 2;
const INITIAL_FIBONACCI_VALUE: u64 = 1;
const MAX_REFRESH_DELAY_SECONDS: u64 = 60 * 60; // 1 hour
const MAX_REFRESH_TOTAL_DELAY_SECONDS: u64 = 60 * 60 * 24; // 1 day

pub fn spawn_refresh_client<ChainA: ChainHandle, ChainB: ChainHandle>(
mut client: ForeignClient<ChainA, ChainB>,
) -> Option<TaskHandle> {
Expand All @@ -23,6 +32,9 @@ pub fn spawn_refresh_client<ChainA: ChainHandle, ChainB: ChainHandle>(
);
None
} else {
// Compute the refresh interval as a fraction of the client's trusting period
// If the trusting period or the client state is not retrieved, fallback to a default value.
let mut next_refresh = Instant::now() + Duration::from_secs(REFRESH_INTERVAL_SECONDS);
Some(spawn_background_task(
span!(
tracing::Level::ERROR,
Expand All @@ -33,15 +45,33 @@ pub fn spawn_refresh_client<ChainA: ChainHandle, ChainB: ChainHandle>(
),
Some(Duration::from_secs(1)),
move || {
let _res = client.refresh().map_err(|e| {
if e.is_expired_or_frozen_error() {
TaskError::Fatal(e)
} else {
TaskError::Ignore(e)
}
})?;
// This is used for integration tests until `spawn_background_task`
// uses async instead of threads
if Instant::now() < next_refresh {
return Ok(Next::Continue);
}

Ok(Next::Continue)
// Use retry mechanism only if `client.refresh()` fails.
let res = retry_with_index(
clamp_total(
Fibonacci::from(Duration::from_secs(INITIAL_FIBONACCI_VALUE)),
Duration::from_secs(MAX_REFRESH_DELAY_SECONDS),
Duration::from_secs(MAX_REFRESH_TOTAL_DELAY_SECONDS),
),
|_| client.refresh(),
);

match res {
// If `client.refresh()` was successful, update the `next_refresh` call.
Ok(_) => {
next_refresh =
Instant::now() + Duration::from_secs(REFRESH_INTERVAL_SECONDS);
Ok(Next::Continue)
}
// If `client.refresh()` failed and the retry mechanism
// exceeded the maximum delay, return a fatal error.
Err(e) => Err(TaskError::Fatal(e)),
}
},
))
}
Expand Down
Loading