Skip to content

Commit

Permalink
Add dynamic gas fees metrics (#3798)
Browse files Browse the repository at this point in the history
* Add simple metric for dynamic gas price after multiplication

* Add max gas price configuration to dynamic gas configuration

* Apply dynamic max gas configuration

* Add 2 new metrics for dynamic gas fees

* Add guide entry for Dynamic gas configuration and metrics

* Fix EIP query parsing

* Use chain id instead of RPC address in emitted metrics

---------

Co-authored-by: Romain Ruetschi <106849+romac@users.noreply.github.com>
  • Loading branch information
ljoss17 and romac authored Jan 19, 2024
1 parent a63d447 commit b0a53f5
Show file tree
Hide file tree
Showing 13 changed files with 223 additions and 31 deletions.
7 changes: 5 additions & 2 deletions crates/relayer/src/chain/cosmos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,11 @@ impl CosmosSdkChain {
pub fn dynamic_gas_price(&self) -> GasPrice {
let gas_config = GasConfig::from(self.config());

self.rt
.block_on(dynamic_gas_price(&gas_config, &self.config.rpc_addr))
self.rt.block_on(dynamic_gas_price(
&gas_config,
&self.config.id,
&self.config.rpc_addr,
))
}

/// The unbonding period of this chain
Expand Down
4 changes: 4 additions & 0 deletions crates/relayer/src/chain/cosmos/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ async fn batch_messages(
let max_fee = gas_amount_to_fee(
&config.gas_config,
config.gas_config.max_gas,
&config.chain_id,
&config.rpc_address,
)
.await;
Expand Down Expand Up @@ -376,6 +377,7 @@ mod tests {
let max_fee = gas_amount_to_fee(
&config.gas_config,
config.gas_config.max_gas,
&config.chain_id,
&config.rpc_address,
)
.await;
Expand Down Expand Up @@ -463,6 +465,7 @@ mod tests {
let max_fee = gas_amount_to_fee(
&config.gas_config,
config.gas_config.max_gas,
&config.chain_id,
&config.rpc_address,
)
.await;
Expand Down Expand Up @@ -593,6 +596,7 @@ mod tests {
let max_fee = gas_amount_to_fee(
&config.gas_config,
config.gas_config.max_gas,
&config.chain_id,
&config.rpc_address,
)
.await;
Expand Down
12 changes: 9 additions & 3 deletions crates/relayer/src/chain/cosmos/eip_base_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ use std::str::FromStr;

use serde::Deserialize;
use subtle_encoding::base64;
use tendermint_rpc::Url;
use tracing::debug;

use ibc_proto::cosmos::base::v1beta1::DecProto;

use crate::error::Error;

pub async fn query_eip_base_fee(rpc_address: &str) -> Result<f64, Error> {
pub async fn query_eip_base_fee(rpc_address: &Url) -> Result<f64, Error> {
debug!("Querying Omosis EIP-1559 base fee from {rpc_address}");

let url =
Expand All @@ -22,6 +23,11 @@ pub async fn query_eip_base_fee(rpc_address: &str) -> Result<f64, Error> {
return Err(Error::http_response(response.status()));
}

#[derive(Deserialize)]
struct EipBaseFeeHTTPResult {
result: EipBaseFeeResult,
}

#[derive(Deserialize)]
struct EipBaseFeeResult {
response: EipBaseFeeResponse,
Expand All @@ -32,9 +38,9 @@ pub async fn query_eip_base_fee(rpc_address: &str) -> Result<f64, Error> {
value: String,
}

let result: EipBaseFeeResult = response.json().await.map_err(Error::http_response_body)?;
let result: EipBaseFeeHTTPResult = response.json().await.map_err(Error::http_response_body)?;

let encoded = result.response.value;
let encoded = result.result.response.value;
let decoded = base64::decode(encoded).map_err(Error::base64_decode)?;

let dec_proto: DecProto = prost::Message::decode(decoded.as_ref())
Expand Down
2 changes: 1 addition & 1 deletion crates/relayer/src/chain/cosmos/estimate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ async fn estimate_fee_with_tx(
));
}

let adjusted_fee = gas_amount_to_fee(gas_config, estimated_gas, rpc_address).await;
let adjusted_fee = gas_amount_to_fee(gas_config, estimated_gas, chain_id, rpc_address).await;

debug!(
id = %chain_id,
Expand Down
55 changes: 49 additions & 6 deletions crates/relayer/src/chain/cosmos/gas.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
use core::cmp::min;
use ibc_proto::cosmos::base::v1beta1::Coin;
use ibc_proto::cosmos::tx::v1beta1::Fee;
use ibc_relayer_types::core::ics24_host::identifier::ChainId;
use num_bigint::BigInt;
use num_rational::BigRational;
use tendermint_rpc::Url;
use tracing::warn;

use crate::chain::cosmos::types::gas::GasConfig;
use crate::config::GasPrice;
use crate::telemetry;

use super::eip_base_fee::query_eip_base_fee;

pub async fn gas_amount_to_fee(config: &GasConfig, gas_amount: u64, rpc_address: &Url) -> Fee {
pub async fn gas_amount_to_fee(
config: &GasConfig,
gas_amount: u64,
chain_id: &ChainId,
rpc_address: &Url,
) -> Fee {
let adjusted_gas_limit = adjust_estimated_gas(AdjustGas {
gas_multiplier: config.gas_multiplier,
max_gas: config.max_gas,
gas_amount,
});

// The fee in coins based on gas amount
let dynamic_gas_price = dynamic_gas_price(config, rpc_address).await;
let dynamic_gas_price = dynamic_gas_price(config, chain_id, rpc_address).await;
let amount = calculate_fee(adjusted_gas_limit, &dynamic_gas_price);

Fee {
Expand All @@ -29,16 +37,51 @@ pub async fn gas_amount_to_fee(config: &GasConfig, gas_amount: u64, rpc_address:
}
}

pub async fn dynamic_gas_price(config: &GasConfig, rpc_address: &Url) -> GasPrice {
pub async fn dynamic_gas_price(
config: &GasConfig,
chain_id: &ChainId,
rpc_address: &Url,
) -> GasPrice {
if let Some(dynamic_gas_price_multiplier) = config.dynamic_gas_price_multiplier {
query_eip_base_fee(&rpc_address.to_string())
let dynamic_gas_price = query_eip_base_fee(rpc_address)
.await
.map(|base_fee| base_fee * dynamic_gas_price_multiplier)
.map(|new_price| GasPrice {
price: new_price,
denom: config.gas_price.denom.clone(),
})
.unwrap_or_else(|_| config.gas_price.clone())
});

let dynamic_gas_price = match dynamic_gas_price {
Ok(dynamic_gas_price) => {
telemetry!(
dynamic_gas_queried_success_fees,
chain_id,
dynamic_gas_price.price
);

dynamic_gas_price
}
Err(e) => {
warn!("failed to query EIP base fee, will fallback to configured `gas_price`: {e}");
config.gas_price.clone()
}
};

telemetry!(dynamic_gas_queried_fees, chain_id, dynamic_gas_price.price);

if dynamic_gas_price.price > config.max_dynamic_gas_price {
warn!(
"queried EIP gas price is higher than configured max gas price, \
will fallback to configured `max_dynamic_gas_price`. Queried: {}, maximum: {}",
dynamic_gas_price.price, config.max_dynamic_gas_price
);

return GasPrice::new(config.max_dynamic_gas_price, dynamic_gas_price.denom);
}

telemetry!(dynamic_gas_paid_fees, chain_id, dynamic_gas_price.price);

dynamic_gas_price
} else {
config.gas_price.clone()
}
Expand Down
2 changes: 2 additions & 0 deletions crates/relayer/src/chain/cosmos/types/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct GasConfig {
pub max_fee: Fee,
pub fee_granter: String,
pub dynamic_gas_price_multiplier: Option<f64>,
pub max_dynamic_gas_price: f64,
}

impl<'a> From<&'a CosmosSdkConfig> for GasConfig {
Expand All @@ -30,6 +31,7 @@ impl<'a> From<&'a CosmosSdkConfig> for GasConfig {
max_fee: max_fee_from_config(config),
fee_granter: fee_granter_from_config(config),
dynamic_gas_price_multiplier: config.dynamic_gas.dynamic_gas_price(),
max_dynamic_gas_price: config.dynamic_gas.max_gas_price,
}
}
}
Expand Down
66 changes: 51 additions & 15 deletions crates/relayer/src/config/dynamic_gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,56 @@ use serde_derive::Serialize;

flex_error::define_error! {
Error {
TooSmall
MultiplierTooSmall
{ value: f64 }
|e| {
format_args!("`gas_price_multiplier` in dynamic_gas configuration must be greater than or equal to {}, found {}",
DynamicGas::MIN_BOUND, e.value)
},

MaxPriceTooSmall
{ value: f64 }
|e| {
format_args!("`max_gas_price` in dynamic_gas configuration must be greater than or equal to {}, found {}",
DynamicGas::CHAIN_MIN_PRICE, e.value)
},
}
}

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
pub struct DynamicGas {
pub enabled: bool,
pub gas_price_multiplier: f64,
pub max_gas_price: f64,
}

impl DynamicGas {
const DEFAULT: f64 = 1.1;
const DEFAULT_MAX_PRICE: f64 = 0.6;
const MIN_BOUND: f64 = 1.0;
// Using Osmosis min https://github.com/osmosis-labs/osmosis/blob/v21.2.1/x/txfees/keeper/mempool-1559/code.go#L45
const CHAIN_MIN_PRICE: f64 = 0.0025;

pub fn new(enabled: bool, value: f64) -> Result<Self, Error> {
if value < Self::MIN_BOUND {
return Err(Error::too_small(value));
pub fn new(enabled: bool, multiplier: f64, max_price: f64) -> Result<Self, Error> {
if multiplier < Self::MIN_BOUND {
return Err(Error::multiplier_too_small(multiplier));
}
if max_price < Self::CHAIN_MIN_PRICE {
return Err(Error::max_price_too_small(max_price));
}
Ok(Self {
enabled,
gas_price_multiplier: value,
gas_price_multiplier: multiplier,
max_gas_price: max_price,
})
}

// Unsafe GasMultiplier used for test cases only.
pub fn unsafe_new(enabled: bool, value: f64) -> Self {
pub fn unsafe_new(enabled: bool, multiplier: f64, max_price: f64) -> Self {
Self {
enabled,
gas_price_multiplier: value,
gas_price_multiplier: multiplier,
max_gas_price: max_price,
}
}

Expand All @@ -57,6 +73,7 @@ impl Default for DynamicGas {
Self {
enabled: false,
gas_price_multiplier: Self::DEFAULT,
max_gas_price: Self::DEFAULT_MAX_PRICE,
}
}
}
Expand All @@ -76,11 +93,29 @@ impl<'de> Deserialize<'de> for DynamicGas {
D::Error::invalid_value(Unexpected::Other("missing field"), &"gas_price_multiplier")
})?;

DynamicGas::new(enabled, gas_price_multiplier).map_err(|e| match e.detail() {
ErrorDetail::TooSmall(_) => D::Error::invalid_value(
Unexpected::Float(gas_price_multiplier),
&format!("a floating-point value less than {}", Self::MIN_BOUND).as_str(),
),
let max_gas_price = value["max_gas_price"].as_f64().ok_or_else(|| {
D::Error::invalid_value(Unexpected::Other("missing field"), &"max_gas_price")
})?;

DynamicGas::new(enabled, gas_price_multiplier, max_gas_price).map_err(|e| {
match e.detail() {
ErrorDetail::MultiplierTooSmall(_) => D::Error::invalid_value(
Unexpected::Float(gas_price_multiplier),
&format!(
"a floating-point value less than {} for multiplier",
Self::MIN_BOUND
)
.as_str(),
),
ErrorDetail::MaxPriceTooSmall(_) => D::Error::invalid_value(
Unexpected::Float(gas_price_multiplier),
&format!(
"a floating-point value less than {} for max gas price",
Self::CHAIN_MIN_PRICE
)
.as_str(),
),
}
})
}
}
Expand All @@ -101,7 +136,7 @@ mod tests {
}

let err = toml::from_str::<DummyConfig>(
"dynamic_gas = { enabled = true, gas_price_multiplier = 0.9 }",
"dynamic_gas = { enabled = true, gas_price_multiplier = 0.9, max_gas_price = 0.6 }",
)
.unwrap_err()
.to_string();
Expand All @@ -113,7 +148,7 @@ mod tests {

#[test]
fn safe_gas_multiplier() {
let dynamic_gas = DynamicGas::new(true, 0.6);
let dynamic_gas = DynamicGas::new(true, 0.6, 0.6);
assert!(
dynamic_gas.is_err(),
"Gas multiplier should be an error if value is lower than 1.0: {dynamic_gas:?}"
Expand All @@ -122,7 +157,8 @@ mod tests {

#[test]
fn unsafe_gas_multiplier() {
let dynamic_gas = DynamicGas::unsafe_new(true, 0.6);
let dynamic_gas = DynamicGas::unsafe_new(true, 0.6, 0.4);
assert_eq!(dynamic_gas.gas_price_multiplier, 0.6);
assert_eq!(dynamic_gas.max_gas_price, 0.4);
}
}
Loading

0 comments on commit b0a53f5

Please sign in to comment.