Support On Chain Fee Quoting (#1658)

### Description

* New gas enforcement policy that uses the on-chain-fee-quoting values.
* Change gas enforcement policies to return the gas limit that should be
used to submit the transaction.
* Change gelato submitter to use the gas limit provided by the gas
enforcement policy.
* Now indexes the gas amounts for the payments

A couple of notes:
* We might want to test this with the e2e test.
* I took a look into the retying provider and it looks like it already
does not retry on failed transaction submissions so I don't think
anything needs to be done there after all.

### Drive-by changes

### Related issues

- Fixes #1535

### Backward compatibility

_Are these changes backward compatible?_

Yes

_Are there any infrastructure implications, e.g. changes that would
prohibit deploying older commits using this infra tooling?_

Yes - changes the internal database format for message payments


### Testing

_What kind of testing have these changes undergone?_

Unit Tests

---------

Co-authored-by: Trevor Porter <trkporter@ucdavis.edu>
pull/1912/head
Mattie Conover 2 years ago committed by GitHub
parent 59a90b1bb6
commit 0049cca1ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 258
      rust/agents/relayer/src/msg/gas_payment/mod.rs
  2. 93
      rust/agents/relayer/src/msg/gas_payment/policies/meets_estimated_cost.rs
  3. 52
      rust/agents/relayer/src/msg/gas_payment/policies/minimum.rs
  4. 2
      rust/agents/relayer/src/msg/gas_payment/policies/mod.rs
  5. 31
      rust/agents/relayer/src/msg/gas_payment/policies/none.rs
  6. 195
      rust/agents/relayer/src/msg/gas_payment/policies/on_chain_fee_quoting.rs
  7. 54
      rust/agents/relayer/src/msg/gelato_submitter/sponsored_call_op.rs
  8. 58
      rust/agents/relayer/src/msg/serial_submitter.rs
  9. 22
      rust/agents/relayer/src/relayer.rs
  10. 33
      rust/agents/relayer/src/settings/mod.rs
  11. 3
      rust/chains/hyperlane-ethereum/src/interchain_gas.rs
  12. 2
      rust/gelato/src/sponsored_call.rs
  13. 6
      rust/helm/hyperlane-agent/templates/relayer-external-secret.yaml
  14. 104
      rust/hyperlane-core/src/db/hyperlane_db.rs
  15. 3
      rust/hyperlane-core/src/db/mod.rs
  16. 122
      rust/hyperlane-core/src/db/storage_types.rs
  17. 6
      rust/hyperlane-core/src/traits/mod.rs
  18. 61
      rust/hyperlane-core/src/types/mod.rs
  19. 4
      rust/utils/run-locally/src/main.rs
  20. 30
      typescript/infra/config/environments/mainnet2/agent.ts
  21. 6
      typescript/infra/config/environments/test/agent.ts
  22. 30
      typescript/infra/config/environments/testnet3/agent.ts
  23. 36
      typescript/infra/src/config/agent.ts

@ -2,13 +2,17 @@ use std::fmt::Debug;
use async_trait::async_trait;
use eyre::Result;
use tracing::{error, trace};
use hyperlane_core::{
db::{DbError, HyperlaneDB},
HyperlaneMessage, TxCostEstimate, H256, U256,
db::HyperlaneDB, HyperlaneMessage, InterchainGasExpenditure, InterchainGasPayment,
TxCostEstimate, TxOutcome, U256,
};
use crate::settings::{matching_list::MatchingList, GasPaymentEnforcementPolicy};
use crate::msg::gas_payment::policies::GasPaymentPolicyOnChainFeeQuoting;
use crate::settings::{
matching_list::MatchingList, GasPaymentEnforcementConfig, GasPaymentEnforcementPolicy,
};
use self::policies::{
GasPaymentPolicyMeetsEstimatedCost, GasPaymentPolicyMinimum, GasPaymentPolicyNone,
@ -18,72 +22,120 @@ mod policies;
#[async_trait]
pub trait GasPaymentPolicy: Debug + Send + Sync {
/// Returns Some(gas_limit) if the policy has approved the transaction or
/// None if the transaction is not approved.
async fn message_meets_gas_payment_requirement(
&self,
message: &HyperlaneMessage,
current_payment: &U256,
current_payment: &InterchainGasPayment,
current_expenditure: &InterchainGasExpenditure,
tx_cost_estimate: &TxCostEstimate,
) -> Result<bool>;
) -> Result<Option<U256>>;
}
#[derive(Debug)]
pub struct GasPaymentEnforcer {
policy: Box<dyn GasPaymentPolicy>,
/// A whitelist, where any matching message is considered
/// as having met the gas payment requirement, even if it doesn't
/// satisfy the policy.
whitelist: MatchingList,
/// List of policies and a whitelist to decide if it should be used for a
/// given transaction. It is highly recommended to have the last policy
/// use a wild-card white list to ensure all messages fall into one
/// policy or another. If a message matches multiple policies'
/// whitelists, then whichever is first in the list will be used.
policies: Vec<(Box<dyn GasPaymentPolicy>, MatchingList)>,
db: HyperlaneDB,
}
impl GasPaymentEnforcer {
pub fn new(
policy_config: GasPaymentEnforcementPolicy,
whitelist: MatchingList,
policy_configs: impl IntoIterator<Item = GasPaymentEnforcementConfig>,
db: HyperlaneDB,
coingecko_api_key: &Option<String>,
) -> Self {
let policy: Box<dyn GasPaymentPolicy> = match policy_config {
GasPaymentEnforcementPolicy::None => Box::new(GasPaymentPolicyNone),
GasPaymentEnforcementPolicy::Minimum { payment } => {
Box::new(GasPaymentPolicyMinimum::new(payment))
}
GasPaymentEnforcementPolicy::MeetsEstimatedCost { coingeckoapikey } => {
Box::new(GasPaymentPolicyMeetsEstimatedCost::new(coingeckoapikey))
}
};
let policies = policy_configs
.into_iter()
.map(|cfg| {
let p: Box<dyn GasPaymentPolicy> = match cfg.policy {
GasPaymentEnforcementPolicy::None => Box::new(GasPaymentPolicyNone),
GasPaymentEnforcementPolicy::Minimum { payment } => {
Box::new(GasPaymentPolicyMinimum::new(payment))
}
GasPaymentEnforcementPolicy::MeetsEstimatedCost => Box::new(
GasPaymentPolicyMeetsEstimatedCost::new(coingecko_api_key.clone()),
),
GasPaymentEnforcementPolicy::OnChainFeeQuoting { gasfraction } => {
let gasfraction = gasfraction.replace(' ', "");
let v: Vec<&str> = gasfraction.split('/').collect();
assert_eq!(
v.len(),
2,
r#"Could not parse gas fraction; expected "`numerator / denominator`""#
);
Box::new(GasPaymentPolicyOnChainFeeQuoting::new(
v[0].parse::<u64>().expect("Invalid integer"),
v[1].parse::<u64>().expect("Invalid integer"),
))
}
};
(p, cfg.matching_list)
})
.collect();
Self {
policy,
whitelist,
db,
}
Self { policies, db }
}
}
impl GasPaymentEnforcer {
/// Returns (gas payment requirement met, current payment according to the DB)
/// Returns Some(gas_limit) if the enforcer has approved the transaction or
/// None if the transaction is not approved.
pub async fn message_meets_gas_payment_requirement(
&self,
message: &HyperlaneMessage,
tx_cost_estimate: &TxCostEstimate,
) -> Result<(bool, U256)> {
let current_payment = self.get_message_gas_payment(message.id())?;
) -> Result<Option<U256>> {
let msg_id = message.id();
let current_payment = self.db.retrieve_gas_payment_for_message_id(msg_id)?;
let current_expenditure = self.db.retrieve_gas_expenditure_for_message_id(msg_id)?;
for (policy, whitelist) in &self.policies {
if !whitelist.msg_matches(message, true) {
trace!(
msg=%message,
?policy,
?whitelist,
"Message did not match whitelist for policy"
);
continue;
}
// If the message matches the whitelist, consider it as meeting the gas payment requirement
if self.whitelist.msg_matches(message, false) {
return Ok((true, current_payment));
trace!(
msg=%message,
?policy,
?whitelist,
"Message matched whitelist for policy"
);
return policy
.message_meets_gas_payment_requirement(
message,
&current_payment,
&current_expenditure,
tx_cost_estimate,
)
.await;
}
let meets_requirement = self
.policy
.message_meets_gas_payment_requirement(message, &current_payment, tx_cost_estimate)
.await?;
Ok((meets_requirement, current_payment))
error!(
msg=%message,
policies=?self.policies,
"No gas payment policy matched for message; consider adding a default policy to the end of the policies array which uses a wildcard whitelist."
);
Ok(None)
}
fn get_message_gas_payment(&self, msg_id: H256) -> Result<U256, DbError> {
self.db.retrieve_gas_payment_for_message_id(msg_id)
pub fn record_tx_outcome(&self, message: &HyperlaneMessage, outcome: TxOutcome) -> Result<()> {
self.db.process_gas_expenditure(InterchainGasExpenditure {
message_id: message.id(),
gas_used: outcome.gas_used,
tokens_used: outcome.gas_used * outcome.gas_price,
})?;
Ok(())
}
}
@ -94,7 +146,9 @@ mod test {
use hyperlane_core::{db::HyperlaneDB, HyperlaneMessage, TxCostEstimate, H160, H256, U256};
use hyperlane_test::test_utils;
use crate::settings::{matching_list::MatchingList, GasPaymentEnforcementPolicy};
use crate::settings::{
matching_list::MatchingList, GasPaymentEnforcementConfig, GasPaymentEnforcementPolicy,
};
use super::GasPaymentEnforcer;
@ -105,16 +159,18 @@ mod test {
let enforcer = GasPaymentEnforcer::new(
// Require a payment
GasPaymentEnforcementPolicy::Minimum {
payment: U256::one(),
},
// Empty whitelist
MatchingList::default().into(),
vec![GasPaymentEnforcementConfig {
policy: GasPaymentEnforcementPolicy::Minimum {
payment: U256::one(),
},
matching_list: Default::default(),
}],
hyperlane_db,
&None,
);
// Ensure that message without any payment is considered as not meeting the requirement
// because it doesn't match the GasPaymentEnforcementPolicy or whitelist
// Ensure that message without any payment is considered as not meeting the
// requirement because it doesn't match the GasPaymentEnforcementPolicy
assert_eq!(
enforcer
.message_meets_gas_payment_requirement(
@ -123,32 +179,70 @@ mod test {
)
.await
.unwrap(),
(false, U256::zero())
None
);
})
.await;
}
#[tokio::test]
async fn test_non_empty_whitelist() {
async fn test_no_match() {
#[allow(unused_must_use)]
test_utils::run_test_db(|db| async move {
let hyperlane_db = HyperlaneDB::new("mailbox", db);
let matching_list = serde_json::from_str(r#"[{"originDomain": 234}]"#).unwrap();
let enforcer = GasPaymentEnforcer::new(
// Require a payment
vec![GasPaymentEnforcementConfig {
policy: GasPaymentEnforcementPolicy::None,
matching_list,
}],
hyperlane_db,
&None,
);
assert!(matches!(
enforcer
.message_meets_gas_payment_requirement(
&HyperlaneMessage::default(),
&TxCostEstimate::default(),
)
.await,
Ok(None)
));
})
.await;
}
#[tokio::test]
async fn test_non_empty_matching_list() {
test_utils::run_test_db(|db| async move {
let hyperlane_db = HyperlaneDB::new("mailbox", db);
let sender_address = "0xaa000000000000000000000000000000000000aa";
let recipient_address = "0xbb000000000000000000000000000000000000bb";
let matching_list = serde_json::from_str(
&format!(r#"[{{"senderAddress": "{sender_address}", "recipientAddress": "{recipient_address}"}}]"#)
).unwrap();
let enforcer = GasPaymentEnforcer::new(
// Require a payment
GasPaymentEnforcementPolicy::Minimum {
payment: U256::one(),
},
// Whitelist
serde_json::from_str(&format!(
r#"[{{"senderAddress": "{}", "recipientAddress": "{}"}}]"#,
sender_address, recipient_address,
))
.unwrap(),
vec![
GasPaymentEnforcementConfig {
// No payment for special cases
policy: GasPaymentEnforcementPolicy::None,
matching_list,
},
GasPaymentEnforcementConfig {
// All other messages must pass a minimum
policy: GasPaymentEnforcementPolicy::Minimum {
payment: U256::one(),
},
matching_list: MatchingList::default(),
},
],
hyperlane_db,
&None,
);
let sender: H256 = H160::from_str(sender_address).unwrap().into();
@ -160,18 +254,16 @@ mod test {
..HyperlaneMessage::default()
};
// The message should meet the requirement because it's on the whitelist, even
// though it has no payment and doesn't satisfy the GasPaymentEnforcementPolicy
assert_eq!(
enforcer
.message_meets_gas_payment_requirement(
&matching_message,
&TxCostEstimate::default(),
)
.await
.unwrap(),
(true, U256::zero())
);
// The message should meet the requirement because it's on the whitelist for the first
// policy, even though it would not pass the second (default) policy.
assert!(enforcer
.message_meets_gas_payment_requirement(
&matching_message,
&TxCostEstimate::default(),
)
.await
.unwrap()
.is_some());
// Switch the sender & recipient
let not_matching_message = HyperlaneMessage {
@ -180,18 +272,16 @@ mod test {
..HyperlaneMessage::default()
};
// The message should not meet the requirement because it's NOT on the whitelist and
// doesn't satisfy the GasPaymentEnforcementPolicy
assert_eq!(
enforcer
.message_meets_gas_payment_requirement(
&not_matching_message,
&TxCostEstimate::default(),
)
.await
.unwrap(),
(false, U256::zero())
);
// The message should not meet the requirement because it's NOT on the first whitelist
// and doesn't satisfy the GasPaymentEnforcementPolicy
assert!(enforcer
.message_meets_gas_payment_requirement(
&not_matching_message,
&TxCostEstimate::default(),
)
.await
.unwrap()
.is_none());
})
.await;
}

@ -9,7 +9,10 @@ use eyre::{eyre, Result};
use tokio::{sync::RwLock, time::timeout};
use tracing::{debug, info};
use hyperlane_core::{HyperlaneMessage, KnownHyperlaneDomain, TxCostEstimate, U256};
use hyperlane_core::{
HyperlaneMessage, InterchainGasExpenditure, InterchainGasPayment, KnownHyperlaneDomain,
TxCostEstimate, U256,
};
use crate::msg::gas_payment::GasPaymentPolicy;
@ -181,16 +184,16 @@ impl GasPaymentPolicyMeetsEstimatedCost {
#[async_trait]
impl GasPaymentPolicy for GasPaymentPolicyMeetsEstimatedCost {
/// Returns (gas payment requirement met, current payment according to the
/// DB)
async fn message_meets_gas_payment_requirement(
&self,
message: &HyperlaneMessage,
current_payment: &U256,
current_payment: &InterchainGasPayment,
current_expenditure: &InterchainGasExpenditure,
tx_cost_estimate: &TxCostEstimate,
) -> Result<bool> {
) -> Result<Option<U256>> {
// Estimated cost of the process tx, quoted in destination native tokens
let destination_token_tx_cost = tx_cost_estimate.gas_limit * tx_cost_estimate.gas_price;
let destination_token_tx_cost = (tx_cost_estimate.gas_limit * tx_cost_estimate.gas_price)
+ current_expenditure.tokens_used;
// Convert the destination token tx cost into origin tokens
let origin_token_tx_cost = self
.convert_native_tokens(
@ -200,7 +203,7 @@ impl GasPaymentPolicy for GasPaymentPolicyMeetsEstimatedCost {
)
.await?;
let meets_requirement = *current_payment >= origin_token_tx_cost;
let meets_requirement = current_payment.payment >= origin_token_tx_cost;
if !meets_requirement {
info!(
msg=%message,
@ -221,7 +224,11 @@ impl GasPaymentPolicy for GasPaymentPolicyMeetsEstimatedCost {
);
}
Ok(meets_requirement)
if meets_requirement {
Ok(Some(tx_cost_estimate.gas_limit))
} else {
Ok(None)
}
}
}
@ -293,20 +300,66 @@ async fn test_gas_payment_policy_meets_estimated_cost() {
let required_celo_payment = ethers::utils::parse_ether("0.03").unwrap();
// Any less than 0.03 CELO as payment, return false.
assert!(!policy
.message_meets_gas_payment_requirement(
&message,
&(required_celo_payment - U256::one()),
&tx_cost_estimate,
)
.await
.unwrap());
let current_payment = InterchainGasPayment {
payment: required_celo_payment - U256::one(),
message_id: H256::zero(),
gas_amount: U256::zero(),
};
let current_expenditure = InterchainGasExpenditure {
message_id: H256::zero(),
tokens_used: U256::zero(),
gas_used: U256::zero(),
};
assert_eq!(
policy
.message_meets_gas_payment_requirement(
&message,
&current_payment,
&current_expenditure,
&tx_cost_estimate,
)
.await
.unwrap(),
None
);
// If the payment is at least 0.03 CELO, return true.
assert!(policy
.message_meets_gas_payment_requirement(&message, &required_celo_payment, &tx_cost_estimate,)
.await
.unwrap());
let current_payment = InterchainGasPayment {
payment: required_celo_payment,
message_id: H256::zero(),
gas_amount: U256::zero(),
};
assert_eq!(
policy
.message_meets_gas_payment_requirement(
&message,
&current_payment,
&current_expenditure,
&tx_cost_estimate,
)
.await
.unwrap(),
Some(tx_cost_estimate.gas_limit)
);
// but not if we have spent tokens already
let current_expenditure = InterchainGasExpenditure {
message_id: H256::zero(),
tokens_used: U256::from(10u32),
gas_used: U256::zero(),
};
assert_eq!(
policy
.message_meets_gas_payment_requirement(
&message,
&current_payment,
&current_expenditure,
&tx_cost_estimate,
)
.await
.unwrap(),
None
);
}
#[test]

@ -2,7 +2,9 @@ use async_trait::async_trait;
use derive_new::new;
use eyre::Result;
use hyperlane_core::{HyperlaneMessage, TxCostEstimate, U256};
use hyperlane_core::{
HyperlaneMessage, InterchainGasExpenditure, InterchainGasPayment, TxCostEstimate, U256,
};
use crate::msg::gas_payment::GasPaymentPolicy;
@ -13,33 +15,47 @@ pub struct GasPaymentPolicyMinimum {
#[async_trait]
impl GasPaymentPolicy for GasPaymentPolicyMinimum {
/// Returns (gas payment requirement met, current payment according to the DB)
async fn message_meets_gas_payment_requirement(
&self,
_message: &HyperlaneMessage,
current_payment: &U256,
_tx_cost_estimate: &TxCostEstimate,
) -> Result<bool> {
Ok(*current_payment >= self.minimum_payment)
current_payment: &InterchainGasPayment,
_current_expenditure: &InterchainGasExpenditure,
tx_cost_estimate: &TxCostEstimate,
) -> Result<Option<U256>> {
if current_payment.payment >= self.minimum_payment {
Ok(Some(tx_cost_estimate.gas_limit))
} else {
Ok(None)
}
}
}
#[tokio::test]
async fn test_gas_payment_policy_none() {
use hyperlane_core::HyperlaneMessage;
async fn test_gas_payment_policy_minimum() {
use hyperlane_core::{HyperlaneMessage, H256};
let min = U256::from(1000u32);
let policy = GasPaymentPolicyMinimum::new(min);
let message = HyperlaneMessage::default();
// If the payment is less than the minimum, returns false
let current_payment = InterchainGasPayment {
message_id: H256::zero(),
payment: U256::from(999u32),
gas_amount: U256::zero(),
};
// expenditure should make no difference
let current_expenditure = InterchainGasExpenditure {
message_id: H256::zero(),
gas_used: U256::from(1000000000u32),
tokens_used: U256::from(1000000000u32),
};
assert_eq!(
policy
.message_meets_gas_payment_requirement(
&message,
&U256::from(999u32),
&current_payment,
&current_expenditure,
&TxCostEstimate {
gas_limit: U256::from(100000u32),
gas_price: U256::from(100000u32),
@ -47,22 +63,28 @@ async fn test_gas_payment_policy_none() {
)
.await
.unwrap(),
false,
None
);
// If the payment is at least the minimum, returns false
let current_payment = InterchainGasPayment {
message_id: H256::zero(),
payment: U256::from(1000u32),
gas_amount: U256::zero(),
};
assert_eq!(
policy
.message_meets_gas_payment_requirement(
&message,
&U256::from(1000u32),
&current_payment,
&current_expenditure,
&TxCostEstimate {
gas_limit: U256::from(100000u32),
gas_price: U256::from(100000u32),
gas_price: U256::from(100001u32),
},
)
.await
.unwrap(),
true,
Some(U256::from(100000u32))
);
}

@ -1,7 +1,9 @@
mod meets_estimated_cost;
mod minimum;
mod none;
mod on_chain_fee_quoting;
pub(crate) use meets_estimated_cost::GasPaymentPolicyMeetsEstimatedCost;
pub(crate) use minimum::GasPaymentPolicyMinimum;
pub(crate) use none::GasPaymentPolicyNone;
pub(crate) use on_chain_fee_quoting::GasPaymentPolicyOnChainFeeQuoting;

@ -1,7 +1,9 @@
use async_trait::async_trait;
use eyre::Result;
use hyperlane_core::{HyperlaneMessage, TxCostEstimate, U256};
use hyperlane_core::{
HyperlaneMessage, InterchainGasExpenditure, InterchainGasPayment, TxCostEstimate, U256,
};
use crate::msg::gas_payment::GasPaymentPolicy;
@ -10,20 +12,20 @@ pub struct GasPaymentPolicyNone;
#[async_trait]
impl GasPaymentPolicy for GasPaymentPolicyNone {
/// Returns (gas payment requirement met, current payment according to the DB)
async fn message_meets_gas_payment_requirement(
&self,
_message: &HyperlaneMessage,
_current_payment: &U256,
_tx_cost_estimate: &TxCostEstimate,
) -> Result<bool> {
Ok(true)
_current_payment: &InterchainGasPayment,
_current_expenditure: &InterchainGasExpenditure,
tx_cost_estimate: &TxCostEstimate,
) -> Result<Option<U256>> {
Ok(Some(tx_cost_estimate.gas_limit))
}
}
#[tokio::test]
async fn test_gas_payment_policy_none() {
use hyperlane_core::HyperlaneMessage;
use hyperlane_core::{HyperlaneMessage, H256, U256};
let policy = GasPaymentPolicyNone;
@ -34,14 +36,23 @@ async fn test_gas_payment_policy_none() {
policy
.message_meets_gas_payment_requirement(
&message,
&U256::zero(),
&InterchainGasPayment {
message_id: H256::zero(),
payment: U256::zero(),
gas_amount: U256::zero(),
},
&InterchainGasExpenditure {
message_id: H256::zero(),
tokens_used: U256::zero(),
gas_used: U256::zero(),
},
&TxCostEstimate {
gas_limit: U256::from(100000u32),
gas_price: U256::from(100000u32),
gas_price: U256::from(100001u32),
},
)
.await
.unwrap(),
true,
Some(U256::from(100000u32))
);
}

@ -0,0 +1,195 @@
use async_trait::async_trait;
use eyre::Result;
use hyperlane_core::{
HyperlaneMessage, InterchainGasExpenditure, InterchainGasPayment, TxCostEstimate, U256,
};
use crate::msg::gas_payment::GasPaymentPolicy;
#[derive(Debug)]
pub struct GasPaymentPolicyOnChainFeeQuoting {
/// Numerator value to modify the estimated gas by. The estimated gas value
/// is multiplied by this value.
fractional_numerator: u64,
/// Denominator value to modify the estimated gas by. The estimated gas
/// value is divided by this value.
fractional_denominator: u64,
}
impl GasPaymentPolicyOnChainFeeQuoting {
pub fn new(fractional_numerator: u64, fractional_denominator: u64) -> Self {
Self {
fractional_numerator,
fractional_denominator,
}
}
}
impl Default for GasPaymentPolicyOnChainFeeQuoting {
fn default() -> Self {
// default to requiring they have paid 1/2 the estimated gas.
Self {
fractional_numerator: 1,
fractional_denominator: 2,
}
}
}
#[async_trait]
impl GasPaymentPolicy for GasPaymentPolicyOnChainFeeQuoting {
async fn message_meets_gas_payment_requirement(
&self,
_message: &HyperlaneMessage,
current_payment: &InterchainGasPayment,
current_expenditure: &InterchainGasExpenditure,
tx_cost_estimate: &TxCostEstimate,
) -> Result<Option<U256>> {
let fractional_gas_estimate =
(tx_cost_estimate.gas_limit * self.fractional_numerator) / self.fractional_denominator;
let gas_amount = current_payment
.gas_amount
.saturating_sub(current_expenditure.gas_used);
// We might want to migrate later to a solution which is a little more
// sophisticated. See https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/1658#discussion_r1093243358
if gas_amount >= fractional_gas_estimate {
Ok(Some(tx_cost_estimate.gas_limit.max(gas_amount)))
} else {
Ok(None)
}
}
}
#[cfg(test)]
mod test {
use hyperlane_core::H256;
use super::*;
fn current_payment(gas_amount: impl Into<U256>) -> InterchainGasPayment {
InterchainGasPayment {
message_id: H256::zero(),
payment: U256::zero(),
gas_amount: gas_amount.into(),
}
}
fn current_expenditure(gas_used: impl Into<U256>) -> InterchainGasExpenditure {
InterchainGasExpenditure {
message_id: H256::zero(),
gas_used: gas_used.into(),
tokens_used: U256::zero(),
}
}
const MIN: U256 = U256([1000, 0, 0, 0]);
const COST_ESTIMATE: TxCostEstimate = TxCostEstimate {
gas_limit: U256([2000, 0, 0, 0]), // MIN * 2
gas_price: U256([100001, 0, 0, 0]),
};
#[test]
fn ensure_little_endian() {
assert_eq!(MIN, U256::from(1000u32));
}
#[tokio::test]
async fn test_payment_less_than_min() {
let policy = GasPaymentPolicyOnChainFeeQuoting::default();
let message = HyperlaneMessage::default();
// If the payment is less than the minimum, returns None
assert_eq!(
policy
.message_meets_gas_payment_requirement(
&message,
&current_payment(MIN - 1),
&current_expenditure(0),
&COST_ESTIMATE,
)
.await
.unwrap(),
None
);
}
#[tokio::test]
async fn test_payment_at_least_min() {
let policy = GasPaymentPolicyOnChainFeeQuoting::default();
let message = HyperlaneMessage::default();
// If the payment is at least the minimum, returns the correct gas amount to use
assert_eq!(
policy
.message_meets_gas_payment_requirement(
&message,
&current_payment(MIN),
&current_expenditure(0),
&COST_ESTIMATE,
)
.await
.unwrap(),
Some(COST_ESTIMATE.gas_limit)
);
}
#[tokio::test]
async fn test_uses_full_paid_amount() {
let policy = GasPaymentPolicyOnChainFeeQuoting::default();
let message = HyperlaneMessage::default();
// Uses the full paid gas amount when it is sufficient
assert_eq!(
policy
.message_meets_gas_payment_requirement(
&message,
&current_payment(MIN * 2 + 300),
&current_expenditure(0),
&COST_ESTIMATE,
)
.await
.unwrap(),
Some(MIN * 2 + 300)
);
}
#[tokio::test]
async fn test_accounts_for_expenditure() {
let policy = GasPaymentPolicyOnChainFeeQuoting::default();
let message = HyperlaneMessage::default();
// Accounts for gas that has already been spent
assert_eq!(
policy
.message_meets_gas_payment_requirement(
&message,
&current_payment(MIN + 300),
&current_expenditure(301),
&COST_ESTIMATE
)
.await
.unwrap(),
None
)
}
#[tokio::test]
async fn test_accounts_for_expenditure_when_giving_full_amount() {
let policy = GasPaymentPolicyOnChainFeeQuoting::default();
let message = HyperlaneMessage::default();
// Accounts for gas that has already been spent
assert_eq!(
policy
.message_meets_gas_payment_requirement(
&message,
&current_payment(MIN * 2 + 300),
&current_expenditure(50),
&COST_ESTIMATE
)
.await
.unwrap(),
Some(MIN * 2 + 250)
)
}
}

@ -18,7 +18,7 @@ use gelato::{
types::Chain,
};
use hyperlane_base::CachingMailbox;
use hyperlane_core::{ChainResult, HyperlaneContract, Mailbox};
use hyperlane_core::{ChainResult, HyperlaneContract, Mailbox, U256};
use crate::msg::{
gas_payment::GasPaymentEnforcer, metadata_builder::MetadataBuilder, SubmitMessageArgs,
@ -120,20 +120,21 @@ impl SponsoredCallOp {
// If the gas payment requirement hasn't been met, sleep briefly and wait for
// the next tick.
let (meets_gas_requirement, gas_payment) = self
let Some(gas_limit) = self
.gas_payment_enforcer
.message_meets_gas_payment_requirement(&self.message.message, &tx_cost_estimate)
.await?;
if !meets_gas_requirement {
info!(?gas_payment, "Gas payment requirement not met yet");
.await?
else {
info!(?tx_cost_estimate, "Gas payment requirement not met yet");
return Ok(false);
}
};
// Send the sponsored call.
let sponsored_call_result = self.send_sponsored_call_api_call(&metadata).await?;
let sponsored_call_result = self
.send_sponsored_call_api_call(&metadata, gas_limit)
.await?;
info!(
msg=?self.message,
message=?self.message,
task_id=sponsored_call_result.task_id,
"Sent sponsored call",
);
@ -205,29 +206,26 @@ impl SponsoredCallOp {
async fn send_sponsored_call_api_call(
&self,
metadata: &[u8],
gas_limit: U256,
) -> Result<SponsoredCallApiCallResult> {
let args = self.create_sponsored_call_args(metadata).await?;
let args = SponsoredCallArgs {
chain_id: self.destination_chain,
target: self.mailbox.address().into(),
data: self
.mailbox
.process_calldata(&self.message.message, metadata)
.into(),
gas_limit: Some(gas_limit),
retries: None, // Use Gelato's default of 5 retries, each ~5 seconds apart
};
let sponsored_call_api_call = SponsoredCallApiCall {
SponsoredCallApiCall {
args: &args,
http: self.http.clone(),
http: &self.http,
sponsor_api_key: &self.sponsor_api_key,
};
sponsored_call_api_call.run().await
}
async fn create_sponsored_call_args(&self, metadata: &[u8]) -> Result<SponsoredCallArgs> {
let calldata = self
.mailbox
.process_calldata(&self.message.message, metadata);
Ok(SponsoredCallArgs {
chain_id: self.destination_chain,
target: self.mailbox.address().into(),
data: calldata.into(),
gas_limit: None, // Gelato will handle gas estimation
retries: None, // Use Gelato's default of 5 retries, each ~5 seconds apart
})
}
.run()
.await
}
async fn message_delivered(&self) -> ChainResult<bool> {

@ -271,21 +271,17 @@ impl SerialSubmitter {
};
// If the gas payment requirement hasn't been met, move to the next tick.
let (meets_gas_requirement, gas_payment) = self
let Some(gas_limit) = self
.gas_payment_enforcer
.message_meets_gas_payment_requirement(&msg.message, &tx_cost_estimate)
.await?;
if !meets_gas_requirement {
info!(
?gas_payment,
?tx_cost_estimate,
"Gas payment requirement not met yet"
);
.await?
else {
info!(?tx_cost_estimate, "Gas payment requirement not met yet");
return Ok(false);
}
};
// Go ahead and attempt processing of message to destination chain.
debug!(?gas_payment, ?tx_cost_estimate, "Ready to process message");
debug!(?gas_limit, "Ready to process message");
// TODO: consider differentiating types of processing errors, and pushing to the front of the
// run queue for intermittent types of errors that can occur even if a message's processing isn't
@ -303,27 +299,31 @@ impl SerialSubmitter {
// We use the estimated gas limit from the prior call to `process_estimate_costs` to
// avoid a second gas estimation.
let process_result = self
let outcome = self
.mailbox
.process(&msg.message, &metadata, Some(gas_limit))
.await;
match process_result {
// TODO(trevor): Instead of immediately marking as processed, move to a verification
// queue, which will wait for finality and indexing by the mailbox indexer and then mark
// as processed (or eventually retry if no confirmation is ever seen).
// Only mark the message as processed if the transaction didn't revert.
Ok(outcome) if outcome.executed => {
info!(hash=?outcome.txid,
wq_sz=?self.wait_queue.len(), rq_sz=?self.run_queue.len(),
"Message successfully processed by transaction");
Ok(true)
}
Ok(outcome) => {
info!(hash=?outcome.txid, "Transaction attempting to process transaction reverted");
Ok(false)
}
Err(e) => Err(e.into()),
.await?;
// TODO(trevor): Instead of immediately marking as processed, move to a verification
// queue, which will wait for finality and indexing by the mailbox indexer and then mark
// as processed (or eventually retry if no confirmation is ever seen).
self.gas_payment_enforcer
.record_tx_outcome(&msg.message, outcome)?;
if outcome.executed {
info!(
hash=?outcome.txid,
wq_sz=?self.wait_queue.len(),
rq_sz=?self.run_queue.len(),
"Message successfully processed by transaction"
);
Ok(true)
} else {
info!(
hash=?outcome.txid,
"Transaction attempting to process transaction reverted"
);
Ok(false)
}
}

@ -28,7 +28,7 @@ use crate::{
serial_submitter::{SerialSubmitter, SerialSubmitterMetrics},
SubmitMessageArgs,
},
settings::{matching_list::MatchingList, RelayerSettings},
settings::{matching_list::MatchingList, GasPaymentEnforcementConfig, RelayerSettings},
};
/// A relayer agent
@ -118,20 +118,14 @@ impl BaseAgent for Relayer {
.context("Relayer must run on a configured chain")?
.domain()?;
let gas_enforcement_policy = settings.gaspaymentenforcement.policy;
let gas_enforcement_whitelist =
parse_matching_list(&settings.gaspaymentenforcement.whitelist);
info!(
?gas_enforcement_policy,
%gas_enforcement_whitelist,
"Gas enforcement configuration"
);
let gas_enforcement_policies =
parse_gas_enforcement_policies(&settings.gaspaymentenforcement);
info!(?gas_enforcement_policies, "Gas enforcement configuration");
let gas_payment_enforcer = Arc::new(GasPaymentEnforcer::new(
gas_enforcement_policy,
gas_enforcement_whitelist,
gas_enforcement_policies,
mailboxes.get(&origin_chain).unwrap().db().clone(),
&settings.coingeckoapikey,
));
let allow_local_checkpoint_syncers = settings.allowlocalcheckpointsyncers.unwrap_or(false);
@ -364,5 +358,9 @@ fn parse_matching_list(list: &Option<String>) -> MatchingList {
.unwrap_or_default()
}
fn parse_gas_enforcement_policies(policies: &str) -> Vec<GasPaymentEnforcementConfig> {
serde_json::from_str(policies).expect("Invalid gas payment enforcement configuration received")
}
#[cfg(test)]
mod test {}

@ -3,6 +3,8 @@
use hyperlane_base::decl_settings;
use hyperlane_core::U256;
use crate::settings::matching_list::MatchingList;
pub mod matching_list;
/// Config for a GasPaymentEnforcementPolicy
@ -15,9 +17,15 @@ pub enum GasPaymentEnforcementPolicy {
Minimum {
payment: U256,
},
MeetsEstimatedCost {
coingeckoapikey: Option<String>,
MeetsEstimatedCost,
/// The required amount of gas on the foreign chain has been paid according
/// to on-chain fee quoting.
OnChainFeeQuoting {
/// Optional fraction of gas which must be paid before attempting to run
/// the transaction. Must be written as `"numerator /
/// denominator"` where both are integers.
#[serde(default = "default_gasfraction")]
gasfraction: String,
},
}
@ -26,11 +34,12 @@ pub enum GasPaymentEnforcementPolicy {
#[serde(tag = "type", rename_all = "camelCase")]
pub struct GasPaymentEnforcementConfig {
/// The gas payment enforcement policy
#[serde(flatten)]
pub policy: GasPaymentEnforcementPolicy,
/// An optional whitelist, where all matching messages will be considered
/// as if they have met the gas payment enforcement policy.
/// If None is provided, all messages will be considered NOT on the whitelist.
pub whitelist: Option<String>,
/// An optional matching list, any message that matches will use this
/// policy. By default all messages will match.
#[serde(default)]
pub matching_list: MatchingList,
}
decl_settings!(Relayer {
@ -40,8 +49,10 @@ decl_settings!(Relayer {
originchainname: String,
// Comma separated list of destination chains.
destinationchainnames: String,
/// The gas payment enforcement configuration
gaspaymentenforcement: GasPaymentEnforcementConfig,
/// The gas payment enforcement configuration as JSON. Expects an ordered array of `GasPaymentEnforcementConfig`.
gaspaymentenforcement: String,
/// API key to be used for the `MeetsEstimatedCost` enforcement policy.
coingeckoapikey: Option<String>,
/// This is optional. If no whitelist is provided ALL messages will be considered on the
/// whitelist.
whitelist: Option<String>,
@ -57,3 +68,7 @@ decl_settings!(Relayer {
/// Not intended for production use. Defaults to false.
allowlocalcheckpointsyncers: Option<bool>,
});
fn default_gasfraction() -> String {
"1/2".into()
}

@ -122,10 +122,11 @@ where
payment: InterchainGasPayment {
message_id: H256::from(log.message_id),
payment: log.payment,
gas_amount: log.gas_amount,
},
meta: InterchainGasPaymentMeta {
transaction_hash: log_meta.transaction_hash,
log_index: log_meta.log_index,
log_index: log_meta.log_index.as_u64(),
},
})
.collect())

@ -30,7 +30,7 @@ pub struct SponsoredCallArgs {
#[derive(Debug, Clone)]
pub struct SponsoredCallApiCall<'a> {
pub http: reqwest::Client,
pub http: &'a reqwest::Client,
pub args: &'a SponsoredCallArgs,
pub sponsor_api_key: &'a str,
}

@ -30,9 +30,7 @@ spec:
AWS_ACCESS_KEY_ID: {{ print "'{{ .aws_access_key_id | toString }}'" }}
AWS_SECRET_ACCESS_KEY: {{ print "'{{ .aws_secret_access_key | toString }}'" }}
{{- end }}
{{- if eq .Values.hyperlane.relayer.config.gasPaymentEnforcement.policy.type "meetsEstimatedCost" }}
HYP_RELAYER_GASPAYMENTENFORCEMENT_POLICY_COINGECKOAPIKEY: {{ print "'{{ .coingecko_api_key | toString }}'" }}
{{- end }}
HYP_RELAYER_COINGECKOAPIKEY: {{ print "'{{ .coingecko_api_key | toString }}'" }}
data:
{{- range .Values.hyperlane.relayerChains }}
{{- if eq .signer.type "hexKey" }}
@ -50,9 +48,7 @@ spec:
remoteRef:
key: {{ printf "%s-%s-%s-relayer-aws-secret-access-key" .Values.hyperlane.context .Values.hyperlane.runEnv .Values.hyperlane.relayer.config.originChainName }}
{{- end }}
{{- if eq .Values.hyperlane.relayer.config.gasPaymentEnforcement.policy.type "meetsEstimatedCost" }}
- secretKey: coingecko_api_key
remoteRef:
key: {{ printf "%s-coingecko-api-key" .Values.hyperlane.runEnv }}
{{- end }}
{{- end }}

@ -4,19 +4,21 @@ use std::time::Duration;
use tokio::time::sleep;
use tracing::{debug, info, trace};
use crate::db::{DbError, TypedDB, DB};
use crate::db::storage_types::InterchainGasExpenditureData;
use crate::db::{storage_types::InterchainGasPaymentData, DbError, TypedDB, DB};
use crate::{
HyperlaneMessage, InterchainGasPayment, InterchainGasPaymentMeta, InterchainGasPaymentWithMeta,
H256, U256,
HyperlaneMessage, InterchainGasExpenditure, InterchainGasPayment, InterchainGasPaymentMeta,
InterchainGasPaymentWithMeta, H256, U256,
};
static MESSAGE_ID: &str = "message_id_";
static MESSAGE: &str = "message_";
static LATEST_NONCE: &str = "latest_known_nonce_";
static LATEST_NONCE_FOR_DESTINATION: &str = "latest_known_nonce_for_destination_";
static NONCE_PROCESSED: &str = "nonce_processed_";
static GAS_PAYMENT_FOR_MESSAGE_ID: &str = "gas_payment_for_message_id_";
static GAS_PAYMENT_META_PROCESSED: &str = "gas_payment_meta_processed_";
const MESSAGE_ID: &str = "message_id_";
const MESSAGE: &str = "message_";
const LATEST_NONCE: &str = "latest_known_nonce_";
const LATEST_NONCE_FOR_DESTINATION: &str = "latest_known_nonce_for_destination_";
const NONCE_PROCESSED: &str = "nonce_processed_";
const GAS_PAYMENT_FOR_MESSAGE_ID: &str = "gas_payment_for_message_id_v2_";
const GAS_PAYMENT_META_PROCESSED: &str = "gas_payment_meta_processed_v2_";
const GAS_EXPENDITURE_FOR_MESSAGE_ID: &str = "gas_expenditure_for_message_id_";
type Result<T> = std::result::Result<T, DbError>;
@ -198,50 +200,86 @@ impl HyperlaneDB {
self.store_gas_payment_meta_processed(meta)?;
// Update the total gas payment for the message to include the payment
self.update_gas_payment_for_message_id(&gas_payment_with_meta.payment)?;
self.update_gas_payment_for_message_id(gas_payment_with_meta.payment)?;
// Return true to indicate the gas payment was processed for the first time
Ok(true)
}
/// Processes the gas expenditure and store the total expenditure for the message.
pub fn process_gas_expenditure(&self, expenditure: InterchainGasExpenditure) -> Result<()> {
// Update the total gas expenditure for the message to include the payment
self.update_gas_expenditure_for_message_id(expenditure)
}
/// Record a gas payment, identified by its metadata, as processed
fn store_gas_payment_meta_processed(
&self,
gas_payment_meta: &InterchainGasPaymentMeta,
) -> Result<()> {
self.store_keyed_encodable(GAS_PAYMENT_META_PROCESSED, gas_payment_meta, &true)
fn store_gas_payment_meta_processed(&self, meta: &InterchainGasPaymentMeta) -> Result<()> {
self.store_keyed_encodable(GAS_PAYMENT_META_PROCESSED, meta, &true)
}
/// Get whether a gas payment, identified by its metadata, has been
/// processed already
fn retrieve_gas_payment_meta_processed(
&self,
gas_payment_meta: &InterchainGasPaymentMeta,
) -> Result<bool> {
fn retrieve_gas_payment_meta_processed(&self, meta: &InterchainGasPaymentMeta) -> Result<bool> {
Ok(self
.retrieve_keyed_decodable(GAS_PAYMENT_META_PROCESSED, gas_payment_meta)?
.retrieve_keyed_decodable(GAS_PAYMENT_META_PROCESSED, meta)?
.unwrap_or(false))
}
/// Update the total gas payment for a message to include gas_payment
fn update_gas_payment_for_message_id(&self, gas_payment: &InterchainGasPayment) -> Result<()> {
let InterchainGasPayment {
message_id,
payment,
} = gas_payment;
let existing_payment = self.retrieve_gas_payment_for_message_id(*message_id)?;
let total = existing_payment + payment;
fn update_gas_payment_for_message_id(&self, event: InterchainGasPayment) -> Result<()> {
let existing_payment = self.retrieve_gas_payment_for_message_id(event.message_id)?;
let total = existing_payment + event;
info!(?event, new_total_gas_payment=?total, "Storing gas payment");
self.store_keyed_encodable::<_, InterchainGasPaymentData>(
GAS_PAYMENT_FOR_MESSAGE_ID,
&total.message_id,
&total.into(),
)?;
Ok(())
}
/// Update the total gas spent for a message
fn update_gas_expenditure_for_message_id(&self, event: InterchainGasExpenditure) -> Result<()> {
let existing_payment = self.retrieve_gas_expenditure_for_message_id(event.message_id)?;
let total = existing_payment + event;
info!(id=?message_id, gas_payment_amount=?payment, new_total_gas_payment=?total, "Storing gas payment");
self.store_keyed_encodable(GAS_PAYMENT_FOR_MESSAGE_ID, &gas_payment.message_id, &total)?;
info!(?event, new_total_gas_payment=?total, "Storing gas payment");
self.store_keyed_encodable::<_, U256>(
GAS_EXPENDITURE_FOR_MESSAGE_ID,
&total.message_id,
&total.tokens_used,
)?;
Ok(())
}
/// Retrieve the total gas payment for a message
pub fn retrieve_gas_payment_for_message_id(&self, message_id: H256) -> Result<U256> {
pub fn retrieve_gas_payment_for_message_id(
&self,
message_id: H256,
) -> Result<InterchainGasPayment> {
Ok(self
.retrieve_keyed_decodable::<_, InterchainGasPaymentData>(
GAS_PAYMENT_FOR_MESSAGE_ID,
&message_id,
)?
.unwrap_or_default()
.complete(message_id))
}
/// Retrieve the total gas payment for a message
pub fn retrieve_gas_expenditure_for_message_id(
&self,
message_id: H256,
) -> Result<InterchainGasExpenditure> {
Ok(self
.retrieve_keyed_decodable(GAS_PAYMENT_FOR_MESSAGE_ID, &message_id)?
.unwrap_or(U256::zero()))
.retrieve_keyed_decodable::<_, InterchainGasExpenditureData>(
GAS_EXPENDITURE_FOR_MESSAGE_ID,
&message_id,
)?
.unwrap_or_default()
.complete(message_id))
}
}

@ -17,6 +17,9 @@ mod hyperlane_db;
/// Type-specific db operations
mod typed_db;
/// Internal-use storage types.
mod storage_types;
#[derive(Debug, Clone)]
/// A KV Store
pub struct DB(Arc<Rocks>);

@ -0,0 +1,122 @@
use std::io::{Read, Write};
use crate::{
Decode, Encode, HyperlaneProtocolError, InterchainGasExpenditure, InterchainGasPayment, H256,
U256,
};
/// Subset of `InterchainGasPayment` excluding the message id which is stored in
/// the key.
#[derive(Debug, Copy, Clone)]
pub(super) struct InterchainGasPaymentData {
pub payment: U256,
pub gas_amount: U256,
}
/// Subset of `InterchainGasExpenditure` excluding the message id which is
/// stored in the key.
#[derive(Debug, Copy, Clone)]
pub(super) struct InterchainGasExpenditureData {
pub tokens_used: U256,
pub gas_used: U256,
}
impl Default for InterchainGasPaymentData {
fn default() -> Self {
Self {
payment: U256::zero(),
gas_amount: U256::zero(),
}
}
}
impl InterchainGasPaymentData {
pub fn complete(self, message_id: H256) -> InterchainGasPayment {
InterchainGasPayment {
message_id,
payment: self.payment,
gas_amount: self.gas_amount,
}
}
}
impl From<InterchainGasPayment> for InterchainGasPaymentData {
fn from(p: InterchainGasPayment) -> Self {
Self {
payment: p.payment,
gas_amount: p.gas_amount,
}
}
}
impl Encode for InterchainGasPaymentData {
fn write_to<W>(&self, writer: &mut W) -> std::io::Result<usize>
where
W: Write,
{
Ok(self.payment.write_to(writer)? + self.gas_amount.write_to(writer)?)
}
}
impl Decode for InterchainGasPaymentData {
fn read_from<R>(reader: &mut R) -> Result<Self, HyperlaneProtocolError>
where
R: Read,
Self: Sized,
{
Ok(Self {
payment: U256::read_from(reader)?,
gas_amount: U256::read_from(reader)?,
})
}
}
impl Default for InterchainGasExpenditureData {
fn default() -> Self {
Self {
tokens_used: U256::zero(),
gas_used: U256::zero(),
}
}
}
impl InterchainGasExpenditureData {
pub fn complete(self, message_id: H256) -> InterchainGasExpenditure {
InterchainGasExpenditure {
message_id,
tokens_used: self.tokens_used,
gas_used: self.gas_used,
}
}
}
impl From<InterchainGasExpenditure> for InterchainGasExpenditureData {
fn from(p: InterchainGasExpenditure) -> Self {
Self {
tokens_used: p.tokens_used,
gas_used: p.gas_used,
}
}
}
impl Encode for InterchainGasExpenditureData {
fn write_to<W>(&self, writer: &mut W) -> std::io::Result<usize>
where
W: Write,
{
Ok(self.tokens_used.write_to(writer)? + self.gas_used.write_to(writer)?)
}
}
impl Decode for InterchainGasExpenditureData {
fn read_from<R>(reader: &mut R) -> Result<Self, HyperlaneProtocolError>
where
R: Read,
Self: Sized,
{
Ok(Self {
tokens_used: U256::read_from(reader)?,
gas_used: U256::read_from(reader)?,
})
}
}

@ -27,6 +27,10 @@ pub struct TxOutcome {
pub txid: crate::H256,
/// True if executed, false otherwise (reverted, etc.)
pub executed: bool,
/// Amount of gas used on this transaction.
pub gas_used: crate::U256,
/// Price paid for the gas
pub gas_price: crate::U256,
// TODO: more? What can be abstracted across all chains?
}
@ -35,6 +39,8 @@ impl From<ethers::prelude::TransactionReceipt> for TxOutcome {
Self {
txid: t.transaction_hash,
executed: t.status.unwrap().low_u32() == 1,
gas_used: t.gas_used.unwrap_or(crate::U256::zero()),
gas_price: t.effective_gas_price.unwrap_or(crate::U256::zero()),
}
}
}

@ -1,4 +1,6 @@
pub use primitive_types::{H128, H160, H256, H512, U128, U256, U512};
use std::io::{Read, Write};
use std::ops::Add;
pub use announcement::*;
pub use chain_data::*;
@ -18,13 +20,58 @@ mod message;
/// 20-byte ids (e.g ethereum addresses)
pub mod identifiers;
/// A payment of native tokens for a message
#[derive(Debug)]
/// A payment of a message's gas costs.
#[derive(Debug, Copy, Clone)]
pub struct InterchainGasPayment {
/// The id of the message
pub message_id: H256,
/// The payment amount, in origin chain native token wei
/// The amount of native tokens paid.
pub payment: U256,
/// The amount of destination gas paid for.
pub gas_amount: U256,
}
/// Amount of gas spent attempting to send the message.
#[derive(Debug, Copy, Clone)]
pub struct InterchainGasExpenditure {
/// The id of the message
pub message_id: H256,
/// Amount of destination tokens used attempting to relay the message
pub tokens_used: U256,
/// Amount of destination gas used attempting to relay the message
pub gas_used: U256,
}
impl Add for InterchainGasPayment {
type Output = Self;
fn add(self, rhs: Self) -> Self {
assert_eq!(
self.message_id, rhs.message_id,
"Cannot add interchain gas payments for different messages"
);
Self {
message_id: self.message_id,
payment: self.payment + rhs.payment,
gas_amount: self.gas_amount + rhs.gas_amount,
}
}
}
impl Add for InterchainGasExpenditure {
type Output = Self;
fn add(self, rhs: Self) -> Self {
assert_eq!(
self.message_id, rhs.message_id,
"Cannot add interchain gas expenditures for different messages"
);
Self {
message_id: self.message_id,
tokens_used: self.tokens_used + rhs.tokens_used,
gas_used: self.gas_used + rhs.gas_used,
}
}
}
/// Uniquely identifying metadata for an InterchainGasPayment
@ -33,13 +80,13 @@ pub struct InterchainGasPaymentMeta {
/// The transaction hash in which the GasPayment log was emitted
pub transaction_hash: H256,
/// The index of the GasPayment log within the transaction's logs
pub log_index: U256,
pub log_index: u64,
}
impl Encode for InterchainGasPaymentMeta {
fn write_to<W>(&self, writer: &mut W) -> std::io::Result<usize>
where
W: std::io::Write,
W: Write,
{
let mut written = 0;
written += self.transaction_hash.write_to(writer)?;
@ -51,12 +98,12 @@ impl Encode for InterchainGasPaymentMeta {
impl Decode for InterchainGasPaymentMeta {
fn read_from<R>(reader: &mut R) -> Result<Self, HyperlaneProtocolError>
where
R: std::io::Read,
R: Read,
Self: Sized,
{
Ok(Self {
transaction_hash: H256::read_from(reader)?,
log_index: U256::read_from(reader)?,
log_index: u64::read_from(reader)?,
})
}
}

@ -156,10 +156,10 @@ fn main() -> ExitCode {
"HYP_BASE_CHAINS_TEST2_SIGNER_TYPE" => "hexKey",
"HYP_BASE_CHAINS_TEST3_SIGNER_KEY" => "701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82",
"HYP_BASE_CHAINS_TEST3_SIGNER_TYPE" => "hexKey",
"HYP_RELAYER_GASPAYMENTENFORCEMENT_POLICY_TYPE" => "none",
"HYP_RELAYER_GASPAYMENTENFORCEMENT" => r#"[{"type": "none"}]"#,
"HYP_RELAYER_ORIGINCHAINNAME" => "test1",
"HYP_RELAYER_DESTINATIONCHAINNAMES" => "test2,test3",
"HYP_RELAYER_WHITELIST" => r#"[{"senderAddress": "*", "destinationDomain": ["13372", "13373"], "recipientAddress": "*"}]"#,
"HYP_RELAYER_WHITELIST" => r#"[{"senderAddress": "*", "destinationDomain": [13372, 13373], "recipientAddress": "*"}]"#,
"HYP_RELAYER_ALLOWLOCALCHECKPOINTSYNCERS" => "true",
};

@ -50,17 +50,20 @@ export const hyperlane: AgentConfig = {
recipientAddress: '0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE',
},
],
gasPaymentEnforcement: {
policy: {
gasPaymentEnforcement: [
{
type: GasPaymentEnforcementPolicyType.None,
// To continue relaying interchain query callbacks, we whitelist
// all messages between interchain query routers.
// This whitelist will become more strict with
// https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/1605
matchingList: interchainQueriesMatchingList,
},
{
type: GasPaymentEnforcementPolicyType.Minimum,
payment: 1,
},
// To continue relaying interchain query callbacks, we whitelist
// all messages between interchain query routers.
// This whitelist will become more strict with
// https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/1605
whitelist: interchainQueriesMatchingList,
},
],
},
},
rolesWithKeys: ALL_KEY_ROLES,
@ -86,13 +89,16 @@ export const releaseCandidate: AgentConfig = {
relayer: {
default: {
whitelist: releaseCandidateHelloworldMatchingList,
gasPaymentEnforcement: {
policy: {
gasPaymentEnforcement: [
{
type: GasPaymentEnforcementPolicyType.None,
matchingList: interchainQueriesMatchingList,
},
{
type: GasPaymentEnforcementPolicyType.Minimum,
payment: 1,
},
whitelist: interchainQueriesMatchingList,
},
],
transactionGasLimit: 750000,
// Skipping arbitrum because the gas price estimates are inclusive of L1
// fees which leads to wildly off predictions.

@ -23,11 +23,11 @@ export const hyperlane: AgentConfig = {
validators,
relayer: {
default: {
gasPaymentEnforcement: {
policy: {
gasPaymentEnforcement: [
{
type: GasPaymentEnforcementPolicyType.None,
},
},
],
},
},
rolesWithKeys: ALL_KEY_ROLES,

@ -44,17 +44,20 @@ export const hyperlane: AgentConfig = {
relayer: {
default: {
blacklist: releaseCandidateHelloworldMatchingList,
gasPaymentEnforcement: {
policy: {
gasPaymentEnforcement: [
{
type: GasPaymentEnforcementPolicyType.None,
// To continue relaying interchain query callbacks, we whitelist
// all messages between interchain query routers.
// This whitelist will become more strict with
// https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/1605
matchingList: interchainQueriesMatchingList,
},
{
type: GasPaymentEnforcementPolicyType.Minimum,
payment: 1,
},
// To continue relaying interchain query callbacks, we whitelist
// all messages between interchain query routers.
// This whitelist will become more strict with
// https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/1605
whitelist: interchainQueriesMatchingList,
},
],
},
},
rolesWithKeys: ALL_KEY_ROLES,
@ -80,13 +83,16 @@ export const releaseCandidate: AgentConfig = {
relayer: {
default: {
whitelist: releaseCandidateHelloworldMatchingList,
gasPaymentEnforcement: {
policy: {
gasPaymentEnforcement: [
{
type: GasPaymentEnforcementPolicyType.None,
matchingList: interchainQueriesMatchingList,
},
{
type: GasPaymentEnforcementPolicyType.Minimum,
payment: 1, // require 1 wei
},
whitelist: interchainQueriesMatchingList,
},
],
transactionGasLimit: 750000,
// Skipping arbitrum because the gas price estimates are inclusive of L1
// fees which leads to wildly off predictions.

@ -62,14 +62,14 @@ export type GasPaymentEnforcementPolicy =
type: GasPaymentEnforcementPolicyType.MeetsEstimatedCost;
};
export interface GasPaymentEnforcementConfig {
policy: GasPaymentEnforcementPolicy;
whitelist?: MatchingList;
}
export type GasPaymentEnforcementConfig = GasPaymentEnforcementPolicy & {
matchingList?: MatchingList;
};
// Incomplete basic relayer agent config
interface BaseRelayerConfig {
gasPaymentEnforcement: GasPaymentEnforcementConfig;
gasPaymentEnforcement: GasPaymentEnforcementConfig[];
coingeckoApiKey?: string;
whitelist?: MatchingList;
blacklist?: MatchingList;
transactionGasLimit?: BigNumberish;
@ -79,11 +79,6 @@ interface BaseRelayerConfig {
// Per-chain relayer agent configs
type ChainRelayerConfigs = ChainOverridableConfig<BaseRelayerConfig>;
interface SerializableGasPaymentEnforcementConfig
extends Omit<GasPaymentEnforcementConfig, 'whitelist'> {
whitelist?: string;
}
// Full relayer agent config for a single chain
interface RelayerConfig
extends Omit<
@ -95,7 +90,7 @@ interface RelayerConfig
| 'gasPaymentEnforcement'
> {
originChainName: ChainName;
gasPaymentEnforcement: SerializableGasPaymentEnforcementConfig;
gasPaymentEnforcement: string;
whitelist?: string;
blacklist?: string;
transactionGasLimit?: string;
@ -401,13 +396,11 @@ export class ChainAgentConfig {
const relayerConfig: RelayerConfig = {
originChainName: this.chainName,
gasPaymentEnforcement: {
...baseConfig.gasPaymentEnforcement,
whitelist: baseConfig.gasPaymentEnforcement.whitelist
? JSON.stringify(baseConfig.gasPaymentEnforcement.whitelist)
: undefined,
},
gasPaymentEnforcement: JSON.stringify(baseConfig.gasPaymentEnforcement),
};
if (baseConfig.coingeckoApiKey) {
relayerConfig.coingeckoApiKey = baseConfig.coingeckoApiKey;
}
if (baseConfig.whitelist) {
relayerConfig.whitelist = JSON.stringify(baseConfig.whitelist);
}
@ -452,10 +445,15 @@ export class ChainAgentConfig {
}
async ensureCoingeckoApiKeySecretExistsIfRequired() {
const baseConfig = getChainOverriddenConfig(
this.agentConfig.relayer!,
this.chainName,
);
// The CoinGecko API Key is only needed when using the "MeetsEstimatedCost" policy.
if (
this.relayerConfig?.gasPaymentEnforcement.policy.type !==
GasPaymentEnforcementPolicyType.MeetsEstimatedCost
baseConfig.gasPaymentEnforcement.every(
(p) => p.type != GasPaymentEnforcementPolicyType.MeetsEstimatedCost,
)
) {
return;
}

Loading…
Cancel
Save