Merge branch 'main' into dan/gas-escalator-middleware

pull/3852/head
Daniel Savu 6 months ago
commit f3e015b167
No known key found for this signature in database
GPG Key ID: 795E587829AF7E08
  1. 7
      .changeset/many-rice-wave.md
  2. 5
      .changeset/neat-ducks-own.md
  3. 7
      .changeset/nice-rivers-own.md
  4. 7
      .changeset/perfect-seahorses-add.md
  5. 5
      .changeset/sweet-pandas-brush.md
  6. 7
      .changeset/witty-vans-return.md
  7. 4
      .github/workflows/rust-skipped.yml
  8. 4
      .github/workflows/rust.yml
  9. 109
      .github/workflows/test-skipped.yml
  10. 8
      .github/workflows/test.yml
  11. 6
      CODE_OF_CONDUCT.md
  12. 6
      README.md
  13. 5
      funding.json
  14. 2
      rust/.vscode/extensions.json
  15. 2
      rust/Cargo.lock
  16. 2
      rust/agents/relayer/Cargo.toml
  17. 10
      rust/agents/relayer/src/lib.rs
  18. 10
      rust/agents/relayer/src/main.rs
  19. 9
      rust/agents/relayer/src/msg/gas_payment/mod.rs
  20. 3
      rust/agents/relayer/src/msg/mod.rs
  21. 65
      rust/agents/relayer/src/msg/op_queue.rs
  22. 26
      rust/agents/relayer/src/msg/op_submitter.rs
  23. 68
      rust/agents/relayer/src/msg/pending_message.rs
  24. 4
      rust/agents/relayer/src/msg/processor.rs
  25. 53
      rust/agents/relayer/src/relayer.rs
  26. 18
      rust/agents/relayer/src/server.rs
  27. 6
      rust/agents/relayer/src/settings/matching_list.rs
  28. 2
      rust/agents/relayer/src/settings/mod.rs
  29. 40
      rust/agents/scraper/src/agent.rs
  30. 2
      rust/agents/scraper/src/settings.rs
  31. 2
      rust/agents/validator/src/settings.rs
  32. 5
      rust/agents/validator/src/validator.rs
  33. 2
      rust/chains/hyperlane-cosmos/src/interchain_gas.rs
  34. 4
      rust/chains/hyperlane-cosmos/src/mailbox.rs
  35. 2
      rust/chains/hyperlane-cosmos/src/merkle_tree_hook.rs
  36. 34
      rust/chains/hyperlane-ethereum/src/contracts/interchain_gas.rs
  37. 28
      rust/chains/hyperlane-ethereum/src/contracts/mailbox.rs
  38. 31
      rust/chains/hyperlane-ethereum/src/contracts/merkle_tree_hook.rs
  39. 5
      rust/chains/hyperlane-ethereum/src/contracts/mod.rs
  40. 48
      rust/chains/hyperlane-ethereum/src/contracts/utils.rs
  41. 2
      rust/chains/hyperlane-fuel/src/interchain_gas.rs
  42. 4
      rust/chains/hyperlane-fuel/src/mailbox.rs
  43. 2
      rust/chains/hyperlane-sealevel/src/interchain_gas.rs
  44. 4
      rust/chains/hyperlane-sealevel/src/mailbox.rs
  45. 4
      rust/chains/hyperlane-sealevel/src/merkle_tree_hook.rs
  46. 53
      rust/config/mainnet_config.json
  47. 50
      rust/config/testnet_config.json
  48. 15
      rust/hyperlane-base/src/contract_sync/cursors/mod.rs
  49. 12
      rust/hyperlane-base/src/contract_sync/cursors/rate_limited.rs
  50. 31
      rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/backward.rs
  51. 26
      rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs
  52. 1
      rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/mod.rs
  53. 205
      rust/hyperlane-base/src/contract_sync/mod.rs
  54. 6
      rust/hyperlane-base/src/db/rocks/hyperlane_db.rs
  55. 4
      rust/hyperlane-base/src/settings/base.rs
  56. 2
      rust/hyperlane-base/src/settings/mod.rs
  57. 2
      rust/hyperlane-base/src/settings/parser/mod.rs
  58. 2
      rust/hyperlane-core/Cargo.toml
  59. 7
      rust/hyperlane-core/src/chain.rs
  60. 8
      rust/hyperlane-core/src/traits/cursor.rs
  61. 12
      rust/hyperlane-core/src/traits/indexer.rs
  62. 2
      rust/hyperlane-core/src/traits/mod.rs
  63. 56
      rust/hyperlane-core/src/traits/pending_operation.rs
  64. 50
      rust/hyperlane-core/src/types/channel.rs
  65. 4
      rust/hyperlane-core/src/types/mod.rs
  66. 27
      rust/hyperlane-core/src/types/primitive_types.rs
  67. 2
      rust/utils/backtrace-oneline/src/lib.rs
  68. 2
      rust/utils/run-locally/Cargo.toml
  69. 4
      rust/utils/run-locally/src/config.rs
  70. 2
      rust/utils/run-locally/src/cosmos/cli.rs
  71. 4
      rust/utils/run-locally/src/cosmos/mod.rs
  72. 2
      rust/utils/run-locally/src/ethereum/mod.rs
  73. 35
      rust/utils/run-locally/src/invariants.rs
  74. 153
      rust/utils/run-locally/src/main.rs
  75. 55
      rust/utils/run-locally/src/program.rs
  76. 2
      rust/utils/run-locally/src/solana.rs
  77. 18
      rust/utils/run-locally/src/utils.rs
  78. 12
      solidity/CHANGELOG.md
  79. 148
      solidity/contracts/avs/ECDSAStakeRegistry.sol
  80. 6
      solidity/contracts/avs/ECDSAStakeRegistryStorage.sol
  81. 17
      solidity/contracts/interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol
  82. 50
      solidity/contracts/test/ERC20Test.sol
  83. 4
      solidity/contracts/token/README.md
  84. 27
      solidity/contracts/token/extensions/HypXERC20Lockbox.sol
  85. 2
      solidity/coverage.sh
  86. 6
      solidity/foundry.toml
  87. 2
      solidity/lib/forge-std
  88. 4
      solidity/package.json
  89. 30
      solidity/script/avs/DeployAVS.s.sol
  90. 2
      solidity/script/avs/eigenlayer_addresses.json
  91. 4
      solidity/script/xerc20/.env.blast
  92. 5
      solidity/script/xerc20/.env.ethereum
  93. 50
      solidity/script/xerc20/ApproveLockbox.s.sol
  94. 37
      solidity/script/xerc20/GrantLimits.s.sol
  95. 127
      solidity/script/xerc20/ezETH.s.sol
  96. 107
      solidity/test/AnvilRPC.sol
  97. 17
      solidity/test/avs/HyperlaneServiceManager.t.sol
  98. 77
      solidity/test/token/HypERC20.t.sol
  99. 436
      tools/grafana/easy-relayer-dashboard-external.json
  100. 2
      typescript/ccip-server/CHANGELOG.md
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,7 +0,0 @@
---
"@hyperlane-xyz/cli": patch
"@hyperlane-xyz/helloworld": patch
"@hyperlane-xyz/infra": patch
---
fix: minor change was breaking in registry export

@ -1,5 +0,0 @@
---
'@hyperlane-xyz/cli': minor
---
Add hyperlane validator address command to retrieve validator address from AWS

@ -1,7 +0,0 @@
---
'@hyperlane-xyz/infra': minor
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---
Implement multi collateral warp routes

@ -1,7 +0,0 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
'@hyperlane-xyz/core': minor
---
Support xERC20 and xERC20 Lockbox in SDK and CLI

@ -0,0 +1,5 @@
---
"@hyperlane-xyz/core": patch
---
fix: make XERC20 and XERC20 Lockbox proxy-able

@ -1,7 +0,0 @@
---
'@hyperlane-xyz/infra': minor
'@hyperlane-xyz/utils': minor
'@hyperlane-xyz/sdk': minor
---
Implement metadata builder fetching from message

@ -9,6 +9,8 @@ on:
paths-ignore: paths-ignore:
- 'rust/**' - 'rust/**'
- .github/workflows/rust.yml - .github/workflows/rust.yml
# Support for merge queues
merge_group:
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@ -16,12 +18,10 @@ env:
jobs: jobs:
test-rs: test-rs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- run: 'echo "No test required" ' - run: 'echo "No test required" '
lint-rs: lint-rs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- run: 'echo "No lint required" ' - run: 'echo "No lint required" '

@ -6,7 +6,9 @@ on:
paths: paths:
- 'rust/**' - 'rust/**'
- .github/workflows/rust.yml - .github/workflows/rust.yml
- '!*.md'
# Support for merge queues
merge_group:
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:

@ -0,0 +1,109 @@
name: test
on:
push:
branches: [main]
pull_request:
branches:
- '*'
paths:
- '*.md'
- '!**/*'
merge_group:
concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: ${{ github.ref_name != 'main' }}
jobs:
yarn-install:
runs-on: ubuntu-latest
steps:
- name: Instant pass
run: echo "yarn-install job passed"
yarn-build:
runs-on: ubuntu-latest
steps:
- name: Instant pass
run: echo "yarn-build job passed"
checkout-registry:
runs-on: ubuntu-latest
steps:
- name: Instant pass
run: echo "checkout-registry job passed"
lint-prettier:
runs-on: ubuntu-latest
steps:
- name: Instant pass
run: echo "lint-prettier job passed"
yarn-test:
runs-on: ubuntu-latest
steps:
- name: Instant pass
run: echo "yarn-test job passed"
agent-configs:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
environment: [mainnet3, testnet4]
steps:
- name: Instant pass
run: echo "agent-configs job passed"
e2e-matrix:
runs-on: ubuntu-latest
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') || github.event_name == 'merge_group'
strategy:
matrix:
e2e-type: [cosmwasm, non-cosmwasm]
steps:
- name: Instant pass
run: echo "e2e-matrix job passed"
e2e:
runs-on: ubuntu-latest
if: always()
steps:
- name: Instant pass
run: echo "e2e job passed"
cli-e2e:
runs-on: ubuntu-latest
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') || github.event_name == 'merge_group'
strategy:
matrix:
include:
- test-type: preset_hook_enabled
- test-type: configure_hook_enabled
- test-type: pi_with_core_chain
steps:
- name: Instant pass
run: echo "cli-e2e job passed"
env-test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
environment: [mainnet3]
chain: [ethereum, arbitrum, optimism, inevm, viction]
module: [core, igp]
include:
- environment: testnet4
chain: sepolia
module: core
steps:
- name: Instant pass
run: echo "env-test job passed"
coverage:
runs-on: ubuntu-latest
steps:
- name: Instant pass
run: echo "coverage job passed"

@ -7,6 +7,10 @@ on:
pull_request: pull_request:
branches: branches:
- '*' # run against all branches - '*' # run against all branches
paths-ignore:
- '*.md'
# Support for merge queues
merge_group:
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
@ -224,7 +228,7 @@ jobs:
e2e-matrix: e2e-matrix:
runs-on: larger-runner runs-on: larger-runner
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') || github.event_name == 'merge_group'
needs: [yarn-build, checkout-registry] needs: [yarn-build, checkout-registry]
strategy: strategy:
matrix: matrix:
@ -325,7 +329,7 @@ jobs:
cli-e2e: cli-e2e:
runs-on: larger-runner runs-on: larger-runner
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') || github.event_name == 'merge_group'
needs: [yarn-build, checkout-registry] needs: [yarn-build, checkout-registry]
strategy: strategy:
matrix: matrix:

@ -9,10 +9,10 @@ This CoC applies to all members of the Hyperlane Network's community including,
**Code** **Code**
1. Never harass or bully anyone. Not verbally, not physically, not sexually. Harassment will not be tolerated. 1. Never harass or bully anyone. Not verbally, not physically, not sexually. Harassment will not be tolerated.
2. Never discrimnate on the basis of personal characteristics or group membership. 2. Never discriminate on the basis of personal characteristics or group membership.
3. Treat your fellow contributors with respect, fairness, and professionalism, especially in situations of high pressure. 3. Treat your fellow contributors with respect, fairness, and professionalism, especially in situations of high pressure.
4. Seek, offer, and accept objective critism of yours and others work, strive to acknowledge the contributions of others. 4. Seek, offer, and accept objective criticism of yours and others work, strive to acknowledge the contributions of others.
5. Be transparent and honest about your qualifications and any potential conflicts of interest. Transparency is a key tenant of the Hyperlane project and we expect it from all contributors. 5. Be transparent and honest about your qualifications and any potential conflicts of interest. Transparency is a key tenet of the Hyperlane project and we expect it from all contributors.
6. Bring an open and curious mind, the Hyperlane project is designed to enable developers to express their curiosity, experiment, and build things we couldn't have imagined ourselves. 6. Bring an open and curious mind, the Hyperlane project is designed to enable developers to express their curiosity, experiment, and build things we couldn't have imagined ourselves.
7. Stay on track - Do your best to avoid off-topic discussion and make sure you are posting to the correct channel and repositories. Distractions are costly and it is far too easy for work to go off track. 7. Stay on track - Do your best to avoid off-topic discussion and make sure you are posting to the correct channel and repositories. Distractions are costly and it is far too easy for work to go off track.
8. Step down properly - Think of your fellow contributors when you step down from the project. Contributors of open-source projects come and go. It is crucial that when you leave the project or reduce your contribution significantly you do so in a way that minimizes disruption and keeps continuity in mind. Concretely this means telling your fellow contributors you are leaving and taking the proper steps to enable a smooth transition for other contributors to pick up where you left off. 8. Step down properly - Think of your fellow contributors when you step down from the project. Contributors of open-source projects come and go. It is crucial that when you leave the project or reduce your contribution significantly you do so in a way that minimizes disruption and keeps continuity in mind. Concretely this means telling your fellow contributors you are leaving and taking the proper steps to enable a smooth transition for other contributors to pick up where you left off.

@ -103,3 +103,9 @@ See [`rust/README.md`](rust/README.md)
- Create a summary of change highlights - Create a summary of change highlights
- Create a "breaking changes" section with any changes required - Create a "breaking changes" section with any changes required
- Deploy agents with the new image tag (if it makes sense to) - Deploy agents with the new image tag (if it makes sense to)
### Releasing packages to NPM
We use [changesets](https://github.com/changesets/changesets) to release to NPM. You can use the `release` script in `package.json` to publish.
For an alpha or beta version, follow the directions [here](https://github.com/changesets/changesets/blob/main/docs/prereleases.md).

@ -0,0 +1,5 @@
{
"opRetro": {
"projectId": "0xa47182d330bd0c5c69b1418462f3f742099138f09bff057189cdd19676a6acd1"
}
}

@ -4,7 +4,7 @@
// List of extensions which should be recommended for users of this workspace. // List of extensions which should be recommended for users of this workspace.
"recommendations": [ "recommendations": [
"panicbit.cargo", "rust-lang.rust-analyzer",
"tamasfe.even-better-toml", "tamasfe.even-better-toml",
"rust-lang.rust-analyzer", "rust-lang.rust-analyzer",
], ],

2
rust/Cargo.lock generated

@ -7275,7 +7275,9 @@ dependencies = [
"macro_rules_attribute", "macro_rules_attribute",
"maplit", "maplit",
"nix 0.26.4", "nix 0.26.4",
"once_cell",
"regex", "regex",
"relayer",
"ripemd", "ripemd",
"serde", "serde",
"serde_json", "serde_json",

@ -38,7 +38,7 @@ tracing-futures.workspace = true
tracing.workspace = true tracing.workspace = true
hyperlane-core = { path = "../../hyperlane-core", features = ["agent", "async"] } hyperlane-core = { path = "../../hyperlane-core", features = ["agent", "async"] }
hyperlane-base = { path = "../../hyperlane-base" } hyperlane-base = { path = "../../hyperlane-base", features = ["test-utils"] }
hyperlane-ethereum = { path = "../../chains/hyperlane-ethereum" } hyperlane-ethereum = { path = "../../chains/hyperlane-ethereum" }
[dev-dependencies] [dev-dependencies]

@ -0,0 +1,10 @@
mod merkle_tree;
mod msg;
mod processor;
mod prover;
mod relayer;
mod server;
mod settings;
pub use msg::GAS_EXPENDITURE_LOG_MESSAGE;
pub use relayer::*;

@ -11,15 +11,7 @@ use eyre::Result;
use hyperlane_base::agent_main; use hyperlane_base::agent_main;
use crate::relayer::Relayer; use relayer::Relayer;
mod merkle_tree;
mod msg;
mod processor;
mod prover;
mod relayer;
mod server;
mod settings;
#[tokio::main(flavor = "multi_thread", worker_threads = 20)] #[tokio::main(flavor = "multi_thread", worker_threads = 20)]
async fn main() -> Result<()> { async fn main() -> Result<()> {

@ -19,6 +19,8 @@ use crate::{
mod policies; mod policies;
pub const GAS_EXPENDITURE_LOG_MESSAGE: &str = "Recording gas expenditure for message";
#[async_trait] #[async_trait]
pub trait GasPaymentPolicy: Debug + Send + Sync { pub trait GasPaymentPolicy: Debug + Send + Sync {
/// Returns Some(gas_limit) if the policy has approved the transaction or /// Returns Some(gas_limit) if the policy has approved the transaction or
@ -132,6 +134,13 @@ impl GasPaymentEnforcer {
} }
pub fn record_tx_outcome(&self, message: &HyperlaneMessage, outcome: TxOutcome) -> Result<()> { pub fn record_tx_outcome(&self, message: &HyperlaneMessage, outcome: TxOutcome) -> Result<()> {
// This log is required in E2E, hence the use of a `const`
debug!(
msg=%message,
?outcome,
"{}",
GAS_EXPENDITURE_LOG_MESSAGE,
);
self.db.process_gas_expenditure(InterchainGasExpenditure { self.db.process_gas_expenditure(InterchainGasExpenditure {
message_id: message.id(), message_id: message.id(),
gas_used: outcome.gas_used, gas_used: outcome.gas_used,

@ -30,5 +30,6 @@ pub(crate) mod metadata;
pub(crate) mod op_queue; pub(crate) mod op_queue;
pub(crate) mod op_submitter; pub(crate) mod op_submitter;
pub(crate) mod pending_message; pub(crate) mod pending_message;
pub(crate) mod pending_operation;
pub(crate) mod processor; pub(crate) mod processor;
pub use gas_payment::GAS_EXPENDITURE_LOG_MESSAGE;

@ -1,24 +1,20 @@
use std::{cmp::Reverse, collections::BinaryHeap, sync::Arc}; use std::{cmp::Reverse, collections::BinaryHeap, sync::Arc};
use derive_new::new; use derive_new::new;
use hyperlane_core::MpmcReceiver; use hyperlane_core::{PendingOperation, QueueOperation};
use prometheus::{IntGauge, IntGaugeVec}; use prometheus::{IntGauge, IntGaugeVec};
use tokio::sync::Mutex; use tokio::sync::{broadcast::Receiver, Mutex};
use tracing::{info, instrument}; use tracing::{debug, info, instrument};
use crate::server::MessageRetryRequest; use crate::server::MessageRetryRequest;
use super::pending_operation::PendingOperation;
pub type QueueOperation = Box<dyn PendingOperation>;
/// Queue of generic operations that can be submitted to a destination chain. /// Queue of generic operations that can be submitted to a destination chain.
/// Includes logic for maintaining queue metrics by the destination and `app_context` of an operation /// Includes logic for maintaining queue metrics by the destination and `app_context` of an operation
#[derive(Debug, Clone, new)] #[derive(Debug, Clone, new)]
pub struct OpQueue { pub struct OpQueue {
metrics: IntGaugeVec, metrics: IntGaugeVec,
queue_metrics_label: String, queue_metrics_label: String,
retry_rx: MpmcReceiver<MessageRetryRequest>, retry_rx: Arc<Mutex<Receiver<MessageRetryRequest>>>,
#[new(default)] #[new(default)]
queue: Arc<Mutex<BinaryHeap<Reverse<QueueOperation>>>>, queue: Arc<Mutex<BinaryHeap<Reverse<QueueOperation>>>>,
} }
@ -41,7 +37,7 @@ impl OpQueue {
} }
/// Pop multiple elements at once from the queue and update metrics /// Pop multiple elements at once from the queue and update metrics
#[instrument(skip(self), ret, fields(queue_label=%self.queue_metrics_label), level = "debug")] #[instrument(skip(self), fields(queue_label=%self.queue_metrics_label), level = "debug")]
pub async fn pop_many(&mut self, limit: usize) -> Vec<QueueOperation> { pub async fn pop_many(&mut self, limit: usize) -> Vec<QueueOperation> {
self.process_retry_requests().await; self.process_retry_requests().await;
let mut queue = self.queue.lock().await; let mut queue = self.queue.lock().await;
@ -55,6 +51,15 @@ impl OpQueue {
break; break;
} }
} }
// This function is called very often by the op_submitter tasks, so only log when there are operations to pop
// to avoid spamming the logs
if !popped.is_empty() {
debug!(
queue_label = %self.queue_metrics_label,
operations = ?popped,
"Popped OpQueue operations"
);
}
popped popped
} }
@ -64,7 +69,7 @@ impl OpQueue {
// The other consideration is whether to put the channel receiver in the OpQueue or in a dedicated task // The other consideration is whether to put the channel receiver in the OpQueue or in a dedicated task
// that also holds an Arc to the Mutex. For simplicity, we'll put it in the OpQueue for now. // that also holds an Arc to the Mutex. For simplicity, we'll put it in the OpQueue for now.
let mut message_retry_requests = vec![]; let mut message_retry_requests = vec![];
while let Ok(message_id) = self.retry_rx.receiver.try_recv() { while let Ok(message_id) = self.retry_rx.lock().await.try_recv() {
message_retry_requests.push(message_id); message_retry_requests.push(message_id);
} }
if message_retry_requests.is_empty() { if message_retry_requests.is_empty() {
@ -101,15 +106,15 @@ impl OpQueue {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::msg::pending_operation::PendingOperationResult;
use hyperlane_core::{ use hyperlane_core::{
HyperlaneDomain, HyperlaneMessage, KnownHyperlaneDomain, MpmcChannel, TryBatchAs, HyperlaneDomain, HyperlaneMessage, KnownHyperlaneDomain, PendingOperationResult,
TxOutcome, H256, TryBatchAs, TxOutcome, H256, U256,
}; };
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use tokio::sync;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct MockPendingOperation { struct MockPendingOperation {
@ -174,6 +179,10 @@ mod test {
todo!() todo!()
} }
fn get_tx_cost_estimate(&self) -> Option<U256> {
todo!()
}
/// This will be called after the operation has been submitted and is /// This will be called after the operation has been submitted and is
/// responsible for checking if the operation has reached a point at /// responsible for checking if the operation has reached a point at
/// which we consider it safe from reorgs. /// which we consider it safe from reorgs.
@ -181,6 +190,14 @@ mod test {
todo!() todo!()
} }
fn set_operation_outcome(
&mut self,
_submission_outcome: TxOutcome,
_submission_estimated_cost: U256,
) {
todo!()
}
fn next_attempt_after(&self) -> Option<Instant> { fn next_attempt_after(&self) -> Option<Instant> {
Some( Some(
Instant::now() Instant::now()
@ -212,13 +229,17 @@ mod test {
#[tokio::test] #[tokio::test]
async fn test_multiple_op_queues_message_id() { async fn test_multiple_op_queues_message_id() {
let (metrics, queue_metrics_label) = dummy_metrics_and_label(); let (metrics, queue_metrics_label) = dummy_metrics_and_label();
let mpmc_channel = MpmcChannel::new(100); let broadcaster = sync::broadcast::Sender::new(100);
let mut op_queue_1 = OpQueue::new( let mut op_queue_1 = OpQueue::new(
metrics.clone(), metrics.clone(),
queue_metrics_label.clone(), queue_metrics_label.clone(),
mpmc_channel.receiver(), Arc::new(Mutex::new(broadcaster.subscribe())),
);
let mut op_queue_2 = OpQueue::new(
metrics,
queue_metrics_label,
Arc::new(Mutex::new(broadcaster.subscribe())),
); );
let mut op_queue_2 = OpQueue::new(metrics, queue_metrics_label, mpmc_channel.receiver());
// Add some operations to the queue with increasing `next_attempt_after` values // Add some operations to the queue with increasing `next_attempt_after` values
let destination_domain: HyperlaneDomain = KnownHyperlaneDomain::Injective.into(); let destination_domain: HyperlaneDomain = KnownHyperlaneDomain::Injective.into();
@ -244,11 +265,10 @@ mod test {
} }
// Retry by message ids // Retry by message ids
let mpmc_tx = mpmc_channel.sender(); broadcaster
mpmc_tx
.send(MessageRetryRequest::MessageId(op_ids[1])) .send(MessageRetryRequest::MessageId(op_ids[1]))
.unwrap(); .unwrap();
mpmc_tx broadcaster
.send(MessageRetryRequest::MessageId(op_ids[2])) .send(MessageRetryRequest::MessageId(op_ids[2]))
.unwrap(); .unwrap();
@ -278,11 +298,11 @@ mod test {
#[tokio::test] #[tokio::test]
async fn test_destination_domain() { async fn test_destination_domain() {
let (metrics, queue_metrics_label) = dummy_metrics_and_label(); let (metrics, queue_metrics_label) = dummy_metrics_and_label();
let mpmc_channel = MpmcChannel::new(100); let broadcaster = sync::broadcast::Sender::new(100);
let mut op_queue = OpQueue::new( let mut op_queue = OpQueue::new(
metrics.clone(), metrics.clone(),
queue_metrics_label.clone(), queue_metrics_label.clone(),
mpmc_channel.receiver(), Arc::new(Mutex::new(broadcaster.subscribe())),
); );
// Add some operations to the queue with increasing `next_attempt_after` values // Add some operations to the queue with increasing `next_attempt_after` values
@ -304,8 +324,7 @@ mod test {
} }
// Retry by domain // Retry by domain
let mpmc_tx = mpmc_channel.sender(); broadcaster
mpmc_tx
.send(MessageRetryRequest::DestinationDomain( .send(MessageRetryRequest::DestinationDomain(
destination_domain_2.id(), destination_domain_2.id(),
)) ))

@ -1,10 +1,14 @@
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use derive_new::new; use derive_new::new;
use futures::future::join_all; use futures::future::join_all;
use futures_util::future::try_join_all; use futures_util::future::try_join_all;
use hyperlane_core::total_estimated_cost;
use prometheus::{IntCounter, IntGaugeVec}; use prometheus::{IntCounter, IntGaugeVec};
use tokio::sync::broadcast::Sender;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::Mutex;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tokio::time::sleep; use tokio::time::sleep;
use tokio_metrics::TaskMonitor; use tokio_metrics::TaskMonitor;
@ -14,14 +18,13 @@ use tracing::{info, warn};
use hyperlane_base::CoreMetrics; use hyperlane_base::CoreMetrics;
use hyperlane_core::{ use hyperlane_core::{
BatchItem, ChainCommunicationError, ChainResult, HyperlaneDomain, HyperlaneDomainProtocol, BatchItem, ChainCommunicationError, ChainResult, HyperlaneDomain, HyperlaneDomainProtocol,
HyperlaneMessage, MpmcReceiver, TxOutcome, HyperlaneMessage, PendingOperationResult, QueueOperation, TxOutcome,
}; };
use crate::msg::pending_message::CONFIRM_DELAY; use crate::msg::pending_message::CONFIRM_DELAY;
use crate::server::MessageRetryRequest; use crate::server::MessageRetryRequest;
use super::op_queue::{OpQueue, QueueOperation}; use super::op_queue::OpQueue;
use super::pending_operation::*;
/// SerialSubmitter accepts operations over a channel. It is responsible for /// SerialSubmitter accepts operations over a channel. It is responsible for
/// executing the right strategy to deliver those messages to the destination /// executing the right strategy to deliver those messages to the destination
@ -77,7 +80,7 @@ pub struct SerialSubmitter {
/// Receiver for new messages to submit. /// Receiver for new messages to submit.
rx: mpsc::UnboundedReceiver<QueueOperation>, rx: mpsc::UnboundedReceiver<QueueOperation>,
/// Receiver for retry requests. /// Receiver for retry requests.
retry_rx: MpmcReceiver<MessageRetryRequest>, retry_tx: Sender<MessageRetryRequest>,
/// Metrics for serial submitter. /// Metrics for serial submitter.
metrics: SerialSubmitterMetrics, metrics: SerialSubmitterMetrics,
/// Max batch size for submitting messages /// Max batch size for submitting messages
@ -101,24 +104,24 @@ impl SerialSubmitter {
domain, domain,
metrics, metrics,
rx: rx_prepare, rx: rx_prepare,
retry_rx, retry_tx,
max_batch_size, max_batch_size,
task_monitor, task_monitor,
} = self; } = self;
let prepare_queue = OpQueue::new( let prepare_queue = OpQueue::new(
metrics.submitter_queue_length.clone(), metrics.submitter_queue_length.clone(),
"prepare_queue".to_string(), "prepare_queue".to_string(),
retry_rx.clone(), Arc::new(Mutex::new(retry_tx.subscribe())),
); );
let submit_queue = OpQueue::new( let submit_queue = OpQueue::new(
metrics.submitter_queue_length.clone(), metrics.submitter_queue_length.clone(),
"submit_queue".to_string(), "submit_queue".to_string(),
retry_rx.clone(), Arc::new(Mutex::new(retry_tx.subscribe())),
); );
let confirm_queue = OpQueue::new( let confirm_queue = OpQueue::new(
metrics.submitter_queue_length.clone(), metrics.submitter_queue_length.clone(),
"confirm_queue".to_string(), "confirm_queue".to_string(),
retry_rx, Arc::new(Mutex::new(retry_tx.subscribe())),
); );
let tasks = [ let tasks = [
@ -425,11 +428,10 @@ impl OperationBatch {
async fn submit(self, confirm_queue: &mut OpQueue, metrics: &SerialSubmitterMetrics) { async fn submit(self, confirm_queue: &mut OpQueue, metrics: &SerialSubmitterMetrics) {
match self.try_submit_as_batch(metrics).await { match self.try_submit_as_batch(metrics).await {
Ok(outcome) => { Ok(outcome) => {
// TODO: use the `tx_outcome` with the total gas expenditure
// We'll need to proportionally set `used_gas` based on the tx_outcome, so it can be updated in the confirm step
// which means we need to add a `set_transaction_outcome` fn to `PendingOperation`
info!(outcome=?outcome, batch_size=self.operations.len(), batch=?self.operations, "Submitted transaction batch"); info!(outcome=?outcome, batch_size=self.operations.len(), batch=?self.operations, "Submitted transaction batch");
let total_estimated_cost = total_estimated_cost(&self.operations);
for mut op in self.operations { for mut op in self.operations {
op.set_operation_outcome(outcome.clone(), total_estimated_cost);
op.set_next_attempt_after(CONFIRM_DELAY); op.set_next_attempt_after(CONFIRM_DELAY);
confirm_queue.push(op).await; confirm_queue.push(op).await;
} }
@ -459,8 +461,6 @@ impl OperationBatch {
return Err(ChainCommunicationError::BatchIsEmpty); return Err(ChainCommunicationError::BatchIsEmpty);
}; };
// We use the estimated gas limit from the prior call to
// `process_estimate_costs` to avoid a second gas estimation.
let outcome = first_item.mailbox.process_batch(&batch).await?; let outcome = first_item.mailbox.process_batch(&batch).await?;
metrics.ops_submitted.inc_by(self.operations.len() as u64); metrics.ops_submitted.inc_by(self.operations.len() as u64);
Ok(outcome) Ok(outcome)

@ -9,8 +9,9 @@ use derive_new::new;
use eyre::Result; use eyre::Result;
use hyperlane_base::{db::HyperlaneRocksDB, CoreMetrics}; use hyperlane_base::{db::HyperlaneRocksDB, CoreMetrics};
use hyperlane_core::{ use hyperlane_core::{
BatchItem, ChainCommunicationError, ChainResult, HyperlaneChain, HyperlaneDomain, gas_used_by_operation, make_op_try, BatchItem, ChainCommunicationError, ChainResult,
HyperlaneMessage, Mailbox, MessageSubmissionData, TryBatchAs, TxOutcome, H256, U256, HyperlaneChain, HyperlaneDomain, HyperlaneMessage, Mailbox, MessageSubmissionData,
PendingOperation, PendingOperationResult, TryBatchAs, TxOutcome, H256, U256,
}; };
use prometheus::{IntCounter, IntGauge}; use prometheus::{IntCounter, IntGauge};
use tracing::{debug, error, info, instrument, trace, warn}; use tracing::{debug, error, info, instrument, trace, warn};
@ -18,7 +19,6 @@ use tracing::{debug, error, info, instrument, trace, warn};
use super::{ use super::{
gas_payment::GasPaymentEnforcer, gas_payment::GasPaymentEnforcer,
metadata::{BaseMetadataBuilder, MessageMetadataBuilder, MetadataBuilder}, metadata::{BaseMetadataBuilder, MessageMetadataBuilder, MetadataBuilder},
pending_operation::*,
}; };
pub const CONFIRM_DELAY: Duration = if cfg!(any(test, feature = "test-utils")) { pub const CONFIRM_DELAY: Duration = if cfg!(any(test, feature = "test-utils")) {
@ -259,7 +259,7 @@ impl PendingOperation for PendingMessage {
let state = self let state = self
.submission_data .submission_data
.take() .clone()
.expect("Pending message must be prepared before it can be submitted"); .expect("Pending message must be prepared before it can be submitted");
// We use the estimated gas limit from the prior call to // We use the estimated gas limit from the prior call to
@ -271,7 +271,7 @@ impl PendingOperation for PendingMessage {
.await; .await;
match tx_outcome { match tx_outcome {
Ok(outcome) => { Ok(outcome) => {
self.set_submission_outcome(outcome); self.set_operation_outcome(outcome, state.gas_limit);
} }
Err(e) => { Err(e) => {
error!(error=?e, "Error when processing message"); error!(error=?e, "Error when processing message");
@ -283,6 +283,10 @@ impl PendingOperation for PendingMessage {
self.submission_outcome = Some(outcome); self.submission_outcome = Some(outcome);
} }
fn get_tx_cost_estimate(&self) -> Option<U256> {
self.submission_data.as_ref().map(|d| d.gas_limit)
}
async fn confirm(&mut self) -> PendingOperationResult { async fn confirm(&mut self) -> PendingOperationResult {
make_op_try!(|| { make_op_try!(|| {
// Provider error; just try again later // Provider error; just try again later
@ -313,15 +317,6 @@ impl PendingOperation for PendingMessage {
); );
PendingOperationResult::Success PendingOperationResult::Success
} else { } else {
if let Some(outcome) = &self.submission_outcome {
if let Err(e) = self
.ctx
.origin_gas_payment_enforcer
.record_tx_outcome(&self.message, outcome.clone())
{
error!(error=?e, "Error when recording tx outcome");
}
}
warn!( warn!(
tx_outcome=?self.submission_outcome, tx_outcome=?self.submission_outcome,
message_id=?self.message.id(), message_id=?self.message.id(),
@ -331,6 +326,50 @@ impl PendingOperation for PendingMessage {
} }
} }
fn set_operation_outcome(
&mut self,
submission_outcome: TxOutcome,
submission_estimated_cost: U256,
) {
let Some(operation_estimate) = self.get_tx_cost_estimate() else {
warn!("Cannot set operation outcome without a cost estimate set previously");
return;
};
// calculate the gas used by the operation
let gas_used_by_operation = match gas_used_by_operation(
&submission_outcome,
submission_estimated_cost,
operation_estimate,
) {
Ok(gas_used_by_operation) => gas_used_by_operation,
Err(e) => {
warn!(error = %e, "Error when calculating gas used by operation, falling back to charging the full cost of the tx. Are gas estimates enabled for this chain?");
submission_outcome.gas_used
}
};
let operation_outcome = TxOutcome {
gas_used: gas_used_by_operation,
..submission_outcome
};
// record it in the db, to subtract from the sender's igp allowance
if let Err(e) = self
.ctx
.origin_gas_payment_enforcer
.record_tx_outcome(&self.message, operation_outcome.clone())
{
error!(error=?e, "Error when recording tx outcome");
}
// set the outcome in `Self` as well, for later logging
self.set_submission_outcome(operation_outcome);
debug!(
actual_gas_for_message = ?gas_used_by_operation,
message_gas_estimate = ?operation_estimate,
submission_gas_estimate = ?submission_estimated_cost,
message = ?self.message,
"Gas used by message submission"
);
}
fn next_attempt_after(&self) -> Option<Instant> { fn next_attempt_after(&self) -> Option<Instant> {
self.next_attempt_after self.next_attempt_after
} }
@ -343,7 +382,6 @@ impl PendingOperation for PendingMessage {
self.reset_attempts(); self.reset_attempts();
} }
#[cfg(test)]
fn set_retries(&mut self, retries: u32) { fn set_retries(&mut self, retries: u32) {
self.set_retries(retries); self.set_retries(retries);
} }

@ -13,12 +13,12 @@ use hyperlane_base::{
db::{HyperlaneRocksDB, ProcessMessage}, db::{HyperlaneRocksDB, ProcessMessage},
CoreMetrics, CoreMetrics,
}; };
use hyperlane_core::{HyperlaneDomain, HyperlaneMessage}; use hyperlane_core::{HyperlaneDomain, HyperlaneMessage, QueueOperation};
use prometheus::IntGauge; use prometheus::IntGauge;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use tracing::{debug, instrument, trace}; use tracing::{debug, instrument, trace};
use super::{metadata::AppContextClassifier, op_queue::QueueOperation, pending_message::*}; use super::{metadata::AppContextClassifier, pending_message::*};
use crate::{processor::ProcessorExt, settings::matching_list::MatchingList}; use crate::{processor::ProcessorExt, settings::matching_list::MatchingList};
/// Finds unprocessed messages from an origin and submits then through a channel /// Finds unprocessed messages from an origin and submits then through a channel

@ -13,13 +13,15 @@ use hyperlane_base::{
metrics::{AgentMetrics, MetricsUpdater}, metrics::{AgentMetrics, MetricsUpdater},
settings::ChainConf, settings::ChainConf,
BaseAgent, ChainMetrics, ContractSyncMetrics, ContractSyncer, CoreMetrics, HyperlaneAgentCore, BaseAgent, ChainMetrics, ContractSyncMetrics, ContractSyncer, CoreMetrics, HyperlaneAgentCore,
SyncOptions,
}; };
use hyperlane_core::{ use hyperlane_core::{
HyperlaneDomain, HyperlaneMessage, InterchainGasPayment, MerkleTreeInsertion, MpmcChannel, HyperlaneDomain, HyperlaneMessage, InterchainGasPayment, MerkleTreeInsertion, QueueOperation,
MpmcReceiver, U256, H512, U256,
}; };
use tokio::{ use tokio::{
sync::{ sync::{
broadcast::{Receiver, Sender},
mpsc::{self, UnboundedReceiver, UnboundedSender}, mpsc::{self, UnboundedReceiver, UnboundedSender},
RwLock, RwLock,
}, },
@ -33,7 +35,6 @@ use crate::{
msg::{ msg::{
gas_payment::GasPaymentEnforcer, gas_payment::GasPaymentEnforcer,
metadata::{BaseMetadataBuilder, IsmAwareAppContextClassifier}, metadata::{BaseMetadataBuilder, IsmAwareAppContextClassifier},
op_queue::QueueOperation,
op_submitter::{SerialSubmitter, SerialSubmitterMetrics}, op_submitter::{SerialSubmitter, SerialSubmitterMetrics},
pending_message::{MessageContext, MessageSubmissionMetrics}, pending_message::{MessageContext, MessageSubmissionMetrics},
processor::{MessageProcessor, MessageProcessorMetrics}, processor::{MessageProcessor, MessageProcessorMetrics},
@ -134,7 +135,7 @@ impl BaseAgent for Relayer {
let contract_sync_metrics = Arc::new(ContractSyncMetrics::new(&core_metrics)); let contract_sync_metrics = Arc::new(ContractSyncMetrics::new(&core_metrics));
let message_syncs = settings let message_syncs: HashMap<_, Arc<dyn ContractSyncer<HyperlaneMessage>>> = settings
.contract_syncs::<HyperlaneMessage, _>( .contract_syncs::<HyperlaneMessage, _>(
settings.origin_chains.iter(), settings.origin_chains.iter(),
&core_metrics, &core_metrics,
@ -305,8 +306,8 @@ impl BaseAgent for Relayer {
} }
// run server // run server
let mpmc_channel = MpmcChannel::<MessageRetryRequest>::new(ENDPOINT_MESSAGES_QUEUE_SIZE); let sender = Sender::<MessageRetryRequest>::new(ENDPOINT_MESSAGES_QUEUE_SIZE);
let custom_routes = relayer_server::routes(mpmc_channel.sender()); let custom_routes = relayer_server::routes(sender.clone());
let server = self let server = self
.core .core
@ -328,7 +329,7 @@ impl BaseAgent for Relayer {
self.run_destination_submitter( self.run_destination_submitter(
dest_domain, dest_domain,
receive_channel, receive_channel,
mpmc_channel.receiver(), sender.clone(),
// Default to submitting one message at a time if there is no batch config // Default to submitting one message at a time if there is no batch config
self.core.settings.chains[dest_domain.name()] self.core.settings.chains[dest_domain.name()]
.connection .connection
@ -352,13 +353,25 @@ impl BaseAgent for Relayer {
} }
for origin in &self.origin_chains { for origin in &self.origin_chains {
let maybe_broadcaster = self
.message_syncs
.get(origin)
.and_then(|sync| sync.get_broadcaster());
tasks.push(self.run_message_sync(origin, task_monitor.clone()).await); tasks.push(self.run_message_sync(origin, task_monitor.clone()).await);
tasks.push( tasks.push(
self.run_interchain_gas_payment_sync(origin, task_monitor.clone()) self.run_interchain_gas_payment_sync(
origin,
maybe_broadcaster.clone().map(|b| b.subscribe()),
task_monitor.clone(),
)
.await, .await,
); );
tasks.push( tasks.push(
self.run_merkle_tree_hook_syncs(origin, task_monitor.clone()) self.run_merkle_tree_hook_syncs(
origin,
maybe_broadcaster.map(|b| b.subscribe()),
task_monitor.clone(),
)
.await, .await,
); );
} }
@ -394,7 +407,7 @@ impl Relayer {
tokio::spawn(TaskMonitor::instrument(&task_monitor, async move { tokio::spawn(TaskMonitor::instrument(&task_monitor, async move {
contract_sync contract_sync
.clone() .clone()
.sync("dispatched_messages", cursor) .sync("dispatched_messages", cursor.into())
.await .await
})) }))
.instrument(info_span!("MessageSync")) .instrument(info_span!("MessageSync"))
@ -403,6 +416,7 @@ impl Relayer {
async fn run_interchain_gas_payment_sync( async fn run_interchain_gas_payment_sync(
&self, &self,
origin: &HyperlaneDomain, origin: &HyperlaneDomain,
tx_id_receiver: Option<Receiver<H512>>,
task_monitor: TaskMonitor, task_monitor: TaskMonitor,
) -> Instrumented<JoinHandle<()>> { ) -> Instrumented<JoinHandle<()>> {
let index_settings = self.as_ref().settings.chains[origin.name()].index_settings(); let index_settings = self.as_ref().settings.chains[origin.name()].index_settings();
@ -413,7 +427,13 @@ impl Relayer {
.clone(); .clone();
let cursor = contract_sync.cursor(index_settings).await; let cursor = contract_sync.cursor(index_settings).await;
tokio::spawn(TaskMonitor::instrument(&task_monitor, async move { tokio::spawn(TaskMonitor::instrument(&task_monitor, async move {
contract_sync.clone().sync("gas_payments", cursor).await contract_sync
.clone()
.sync(
"gas_payments",
SyncOptions::new(Some(cursor), tx_id_receiver),
)
.await
})) }))
.instrument(info_span!("IgpSync")) .instrument(info_span!("IgpSync"))
} }
@ -421,13 +441,20 @@ impl Relayer {
async fn run_merkle_tree_hook_syncs( async fn run_merkle_tree_hook_syncs(
&self, &self,
origin: &HyperlaneDomain, origin: &HyperlaneDomain,
tx_id_receiver: Option<Receiver<H512>>,
task_monitor: TaskMonitor, task_monitor: TaskMonitor,
) -> Instrumented<JoinHandle<()>> { ) -> Instrumented<JoinHandle<()>> {
let index_settings = self.as_ref().settings.chains[origin.name()].index.clone(); let index_settings = self.as_ref().settings.chains[origin.name()].index.clone();
let contract_sync = self.merkle_tree_hook_syncs.get(origin).unwrap().clone(); let contract_sync = self.merkle_tree_hook_syncs.get(origin).unwrap().clone();
let cursor = contract_sync.cursor(index_settings).await; let cursor = contract_sync.cursor(index_settings).await;
tokio::spawn(TaskMonitor::instrument(&task_monitor, async move { tokio::spawn(TaskMonitor::instrument(&task_monitor, async move {
contract_sync.clone().sync("merkle_tree_hook", cursor).await contract_sync
.clone()
.sync(
"merkle_tree_hook",
SyncOptions::new(Some(cursor), tx_id_receiver),
)
.await
})) }))
.instrument(info_span!("MerkleTreeHookSync")) .instrument(info_span!("MerkleTreeHookSync"))
} }
@ -498,7 +525,7 @@ impl Relayer {
&self, &self,
destination: &HyperlaneDomain, destination: &HyperlaneDomain,
receiver: UnboundedReceiver<QueueOperation>, receiver: UnboundedReceiver<QueueOperation>,
retry_receiver_channel: MpmcReceiver<MessageRetryRequest>, retry_receiver_channel: Sender<MessageRetryRequest>,
batch_size: u32, batch_size: u32,
task_monitor: TaskMonitor, task_monitor: TaskMonitor,
) -> Instrumented<JoinHandle<()>> { ) -> Instrumented<JoinHandle<()>> {

@ -3,13 +3,11 @@ use axum::{
routing, Router, routing, Router,
}; };
use derive_new::new; use derive_new::new;
use hyperlane_core::{ChainCommunicationError, H256}; use hyperlane_core::{ChainCommunicationError, QueueOperation, H256};
use serde::Deserialize; use serde::Deserialize;
use std::str::FromStr; use std::str::FromStr;
use tokio::sync::broadcast::Sender; use tokio::sync::broadcast::Sender;
use crate::msg::op_queue::QueueOperation;
const MESSAGE_RETRY_API_BASE: &str = "/message_retry"; const MESSAGE_RETRY_API_BASE: &str = "/message_retry";
pub const ENDPOINT_MESSAGES_QUEUE_SIZE: usize = 1_000; pub const ENDPOINT_MESSAGES_QUEUE_SIZE: usize = 1_000;
@ -109,12 +107,12 @@ mod tests {
use super::*; use super::*;
use axum::http::StatusCode; use axum::http::StatusCode;
use ethers::utils::hex::ToHex; use ethers::utils::hex::ToHex;
use hyperlane_core::{MpmcChannel, MpmcReceiver};
use std::net::SocketAddr; use std::net::SocketAddr;
use tokio::sync::broadcast::{Receiver, Sender};
fn setup_test_server() -> (SocketAddr, MpmcReceiver<MessageRetryRequest>) { fn setup_test_server() -> (SocketAddr, Receiver<MessageRetryRequest>) {
let mpmc_channel = MpmcChannel::<MessageRetryRequest>::new(ENDPOINT_MESSAGES_QUEUE_SIZE); let broadcast_tx = Sender::<MessageRetryRequest>::new(ENDPOINT_MESSAGES_QUEUE_SIZE);
let message_retry_api = MessageRetryApi::new(mpmc_channel.sender()); let message_retry_api = MessageRetryApi::new(broadcast_tx.clone());
let (path, retry_router) = message_retry_api.get_route(); let (path, retry_router) = message_retry_api.get_route();
let app = Router::new().nest(path, retry_router); let app = Router::new().nest(path, retry_router);
@ -124,7 +122,7 @@ mod tests {
let addr = server.local_addr(); let addr = server.local_addr();
tokio::spawn(server); tokio::spawn(server);
(addr, mpmc_channel.receiver()) (addr, broadcast_tx.subscribe())
} }
#[tokio::test] #[tokio::test]
@ -148,7 +146,7 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
assert_eq!( assert_eq!(
rx.receiver.try_recv().unwrap(), rx.try_recv().unwrap(),
MessageRetryRequest::MessageId(message_id) MessageRetryRequest::MessageId(message_id)
); );
} }
@ -172,7 +170,7 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
assert_eq!( assert_eq!(
rx.receiver.try_recv().unwrap(), rx.try_recv().unwrap(),
MessageRetryRequest::DestinationDomain(destination_domain) MessageRetryRequest::DestinationDomain(destination_domain)
); );
} }

@ -1,4 +1,4 @@
//! The correct settings shape is defined in the TypeScript SDK metadata. While the the exact shape //! The correct settings shape is defined in the TypeScript SDK metadata. While the exact shape
//! and validations it defines are not applied here, we should mirror them. //! and validations it defines are not applied here, we should mirror them.
//! ANY CHANGES HERE NEED TO BE REFLECTED IN THE TYPESCRIPT SDK. //! ANY CHANGES HERE NEED TO BE REFLECTED IN THE TYPESCRIPT SDK.
@ -267,13 +267,13 @@ impl<'a> From<&'a HyperlaneMessage> for MatchInfo<'a> {
impl MatchingList { impl MatchingList {
/// Check if a message matches any of the rules. /// Check if a message matches any of the rules.
/// - `default`: What to return if the the matching list is empty. /// - `default`: What to return if the matching list is empty.
pub fn msg_matches(&self, msg: &HyperlaneMessage, default: bool) -> bool { pub fn msg_matches(&self, msg: &HyperlaneMessage, default: bool) -> bool {
self.matches(msg.into(), default) self.matches(msg.into(), default)
} }
/// Check if a message matches any of the rules. /// Check if a message matches any of the rules.
/// - `default`: What to return if the the matching list is empty. /// - `default`: What to return if the matching list is empty.
fn matches(&self, info: MatchInfo, default: bool) -> bool { fn matches(&self, info: MatchInfo, default: bool) -> bool {
if let Some(rules) = &self.0 { if let Some(rules) = &self.0 {
matches_any_rule(rules.iter(), info) matches_any_rule(rules.iter(), info)

@ -1,6 +1,6 @@
//! Relayer configuration //! Relayer configuration
//! //!
//! The correct settings shape is defined in the TypeScript SDK metadata. While the the exact shape //! The correct settings shape is defined in the TypeScript SDK metadata. While the exact shape
//! and validations it defines are not applied here, we should mirror them. //! and validations it defines are not applied here, we should mirror them.
//! ANY CHANGES HERE NEED TO BE REFLECTED IN THE TYPESCRIPT SDK. //! ANY CHANGES HERE NEED TO BE REFLECTED IN THE TYPESCRIPT SDK.

@ -5,10 +5,13 @@ use derive_more::AsRef;
use futures::future::try_join_all; use futures::future::try_join_all;
use hyperlane_base::{ use hyperlane_base::{
metrics::AgentMetrics, settings::IndexSettings, BaseAgent, ChainMetrics, ContractSyncMetrics, metrics::AgentMetrics, settings::IndexSettings, BaseAgent, ChainMetrics, ContractSyncMetrics,
ContractSyncer, CoreMetrics, HyperlaneAgentCore, MetricsUpdater, ContractSyncer, CoreMetrics, HyperlaneAgentCore, MetricsUpdater, SyncOptions,
};
use hyperlane_core::{Delivery, HyperlaneDomain, HyperlaneMessage, InterchainGasPayment, H512};
use tokio::{
sync::broadcast::{Receiver, Sender},
task::JoinHandle,
}; };
use hyperlane_core::{Delivery, HyperlaneDomain, HyperlaneMessage, InterchainGasPayment};
use tokio::task::JoinHandle;
use tracing::{info_span, instrument::Instrumented, trace, Instrument}; use tracing::{info_span, instrument::Instrumented, trace, Instrument};
use crate::{chain_scraper::HyperlaneSqlDb, db::ScraperDb, settings::ScraperSettings}; use crate::{chain_scraper::HyperlaneSqlDb, db::ScraperDb, settings::ScraperSettings};
@ -135,16 +138,16 @@ impl Scraper {
let domain = scraper.domain.clone(); let domain = scraper.domain.clone();
let mut tasks = Vec::with_capacity(2); let mut tasks = Vec::with_capacity(2);
tasks.push( let (message_indexer, maybe_broadcaster) = self
self.build_message_indexer( .build_message_indexer(
domain.clone(), domain.clone(),
self.core_metrics.clone(), self.core_metrics.clone(),
self.contract_sync_metrics.clone(), self.contract_sync_metrics.clone(),
db.clone(), db.clone(),
index_settings.clone(), index_settings.clone(),
) )
.await, .await;
); tasks.push(message_indexer);
tasks.push( tasks.push(
self.build_delivery_indexer( self.build_delivery_indexer(
domain.clone(), domain.clone(),
@ -152,6 +155,7 @@ impl Scraper {
self.contract_sync_metrics.clone(), self.contract_sync_metrics.clone(),
db.clone(), db.clone(),
index_settings.clone(), index_settings.clone(),
maybe_broadcaster.clone().map(|b| b.subscribe()),
) )
.await, .await,
); );
@ -162,6 +166,7 @@ impl Scraper {
self.contract_sync_metrics.clone(), self.contract_sync_metrics.clone(),
db, db,
index_settings.clone(), index_settings.clone(),
maybe_broadcaster.map(|b| b.subscribe()),
) )
.await, .await,
); );
@ -182,7 +187,7 @@ impl Scraper {
contract_sync_metrics: Arc<ContractSyncMetrics>, contract_sync_metrics: Arc<ContractSyncMetrics>,
db: HyperlaneSqlDb, db: HyperlaneSqlDb,
index_settings: IndexSettings, index_settings: IndexSettings,
) -> Instrumented<JoinHandle<()>> { ) -> (Instrumented<JoinHandle<()>>, Option<Sender<H512>>) {
let sync = self let sync = self
.as_ref() .as_ref()
.settings .settings
@ -195,9 +200,12 @@ impl Scraper {
.await .await
.unwrap(); .unwrap();
let cursor = sync.cursor(index_settings.clone()).await; let cursor = sync.cursor(index_settings.clone()).await;
tokio::spawn(async move { sync.sync("message_dispatch", cursor).await }).instrument( let maybe_broadcaser = sync.get_broadcaster();
let task = tokio::spawn(async move { sync.sync("message_dispatch", cursor.into()).await })
.instrument(
info_span!("ChainContractSync", chain=%domain.name(), event="message_dispatch"), info_span!("ChainContractSync", chain=%domain.name(), event="message_dispatch"),
) );
(task, maybe_broadcaser)
} }
async fn build_delivery_indexer( async fn build_delivery_indexer(
@ -207,6 +215,7 @@ impl Scraper {
contract_sync_metrics: Arc<ContractSyncMetrics>, contract_sync_metrics: Arc<ContractSyncMetrics>,
db: HyperlaneSqlDb, db: HyperlaneSqlDb,
index_settings: IndexSettings, index_settings: IndexSettings,
tx_id_receiver: Option<Receiver<H512>>,
) -> Instrumented<JoinHandle<()>> { ) -> Instrumented<JoinHandle<()>> {
let sync = self let sync = self
.as_ref() .as_ref()
@ -222,7 +231,10 @@ impl Scraper {
let label = "message_delivery"; let label = "message_delivery";
let cursor = sync.cursor(index_settings.clone()).await; let cursor = sync.cursor(index_settings.clone()).await;
tokio::spawn(async move { sync.sync(label, cursor).await }) tokio::spawn(async move {
sync.sync(label, SyncOptions::new(Some(cursor), tx_id_receiver))
.await
})
.instrument(info_span!("ChainContractSync", chain=%domain.name(), event=label)) .instrument(info_span!("ChainContractSync", chain=%domain.name(), event=label))
} }
@ -233,6 +245,7 @@ impl Scraper {
contract_sync_metrics: Arc<ContractSyncMetrics>, contract_sync_metrics: Arc<ContractSyncMetrics>,
db: HyperlaneSqlDb, db: HyperlaneSqlDb,
index_settings: IndexSettings, index_settings: IndexSettings,
tx_id_receiver: Option<Receiver<H512>>,
) -> Instrumented<JoinHandle<()>> { ) -> Instrumented<JoinHandle<()>> {
let sync = self let sync = self
.as_ref() .as_ref()
@ -248,7 +261,10 @@ impl Scraper {
let label = "gas_payment"; let label = "gas_payment";
let cursor = sync.cursor(index_settings.clone()).await; let cursor = sync.cursor(index_settings.clone()).await;
tokio::spawn(async move { sync.sync(label, cursor).await }) tokio::spawn(async move {
sync.sync(label, SyncOptions::new(Some(cursor), tx_id_receiver))
.await
})
.instrument(info_span!("ChainContractSync", chain=%domain.name(), event=label)) .instrument(info_span!("ChainContractSync", chain=%domain.name(), event=label))
} }
} }

@ -1,6 +1,6 @@
//! Scraper configuration. //! Scraper configuration.
//! //!
//! The correct settings shape is defined in the TypeScript SDK metadata. While the the exact shape //! The correct settings shape is defined in the TypeScript SDK metadata. While the exact shape
//! and validations it defines are not applied here, we should mirror them. //! and validations it defines are not applied here, we should mirror them.
//! ANY CHANGES HERE NEED TO BE REFLECTED IN THE TYPESCRIPT SDK. //! ANY CHANGES HERE NEED TO BE REFLECTED IN THE TYPESCRIPT SDK.

@ -1,6 +1,6 @@
//! Validator configuration. //! Validator configuration.
//! //!
//! The correct settings shape is defined in the TypeScript SDK metadata. While the the exact shape //! The correct settings shape is defined in the TypeScript SDK metadata. While the exact shape
//! and validations it defines are not applied here, we should mirror them. //! and validations it defines are not applied here, we should mirror them.
//! ANY CHANGES HERE NEED TO BE REFLECTED IN THE TYPESCRIPT SDK. //! ANY CHANGES HERE NEED TO BE REFLECTED IN THE TYPESCRIPT SDK.

@ -210,7 +210,10 @@ impl Validator {
let contract_sync = self.merkle_tree_hook_sync.clone(); let contract_sync = self.merkle_tree_hook_sync.clone();
let cursor = contract_sync.cursor(index_settings).await; let cursor = contract_sync.cursor(index_settings).await;
tokio::spawn(async move { tokio::spawn(async move {
contract_sync.clone().sync("merkle_tree_hook", cursor).await; contract_sync
.clone()
.sync("merkle_tree_hook", cursor.into())
.await;
}) })
.instrument(info_span!("MerkleTreeHookSyncer")) .instrument(info_span!("MerkleTreeHookSyncer"))
} }

@ -202,7 +202,7 @@ impl CosmosInterchainGasPaymasterIndexer {
#[async_trait] #[async_trait]
impl Indexer<InterchainGasPayment> for CosmosInterchainGasPaymasterIndexer { impl Indexer<InterchainGasPayment> for CosmosInterchainGasPaymasterIndexer {
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<InterchainGasPayment>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<InterchainGasPayment>, LogMeta)>> {

@ -350,7 +350,7 @@ impl CosmosMailboxIndexer {
#[async_trait] #[async_trait]
impl Indexer<HyperlaneMessage> for CosmosMailboxIndexer { impl Indexer<HyperlaneMessage> for CosmosMailboxIndexer {
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<HyperlaneMessage>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<HyperlaneMessage>, LogMeta)>> {
@ -397,7 +397,7 @@ impl Indexer<HyperlaneMessage> for CosmosMailboxIndexer {
#[async_trait] #[async_trait]
impl Indexer<H256> for CosmosMailboxIndexer { impl Indexer<H256> for CosmosMailboxIndexer {
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<H256>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<H256>, LogMeta)>> {

@ -283,7 +283,7 @@ impl CosmosMerkleTreeHookIndexer {
#[async_trait] #[async_trait]
impl Indexer<MerkleTreeInsertion> for CosmosMerkleTreeHookIndexer { impl Indexer<MerkleTreeInsertion> for CosmosMerkleTreeHookIndexer {
/// Fetch list of logs between `range` of blocks /// Fetch list of logs between `range` of blocks
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<MerkleTreeInsertion>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<MerkleTreeInsertion>, LogMeta)>> {

@ -10,12 +10,14 @@ use ethers::prelude::Middleware;
use hyperlane_core::{ use hyperlane_core::{
ChainCommunicationError, ChainResult, ContractLocator, HyperlaneAbi, HyperlaneChain, ChainCommunicationError, ChainResult, ContractLocator, HyperlaneAbi, HyperlaneChain,
HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexed, Indexer, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexed, Indexer,
InterchainGasPaymaster, InterchainGasPayment, LogMeta, SequenceAwareIndexer, H160, H256, InterchainGasPaymaster, InterchainGasPayment, LogMeta, SequenceAwareIndexer, H160, H256, H512,
}; };
use tracing::instrument; use tracing::instrument;
use super::utils::fetch_raw_logs_and_log_meta;
use crate::interfaces::i_interchain_gas_paymaster::{ use crate::interfaces::i_interchain_gas_paymaster::{
IInterchainGasPaymaster as EthereumInterchainGasPaymasterInternal, IINTERCHAINGASPAYMASTER_ABI, GasPaymentFilter, IInterchainGasPaymaster as EthereumInterchainGasPaymasterInternal,
IINTERCHAINGASPAYMASTER_ABI,
}; };
use crate::{BuildableWithProvider, ConnectionConf, EthereumProvider}; use crate::{BuildableWithProvider, ConnectionConf, EthereumProvider};
@ -86,7 +88,7 @@ where
{ {
/// Note: This call may return duplicates depending on the provider used /// Note: This call may return duplicates depending on the provider used
#[instrument(err, skip(self))] #[instrument(err, skip(self))]
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<InterchainGasPayment>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<InterchainGasPayment>, LogMeta)>> {
@ -124,6 +126,32 @@ where
.as_u32() .as_u32()
.saturating_sub(self.reorg_period)) .saturating_sub(self.reorg_period))
} }
async fn fetch_logs_by_tx_hash(
&self,
tx_hash: H512,
) -> ChainResult<Vec<(Indexed<InterchainGasPayment>, LogMeta)>> {
let logs = fetch_raw_logs_and_log_meta::<GasPaymentFilter, M>(
tx_hash,
self.provider.clone(),
self.contract.address(),
)
.await?
.into_iter()
.map(|(log, log_meta)| {
(
Indexed::new(InterchainGasPayment {
message_id: H256::from(log.message_id),
destination: log.destination_domain,
payment: log.payment.into(),
gas_amount: log.gas_amount.into(),
}),
log_meta,
)
})
.collect();
Ok(logs)
}
} }
#[async_trait] #[async_trait]

@ -11,6 +11,7 @@ use ethers::abi::{AbiEncode, Detokenize};
use ethers::prelude::Middleware; use ethers::prelude::Middleware;
use ethers_contract::builders::ContractCall; use ethers_contract::builders::ContractCall;
use futures_util::future::join_all; use futures_util::future::join_all;
use hyperlane_core::H512;
use tracing::instrument; use tracing::instrument;
use hyperlane_core::{ use hyperlane_core::{
@ -25,10 +26,12 @@ use crate::interfaces::arbitrum_node_interface::ArbitrumNodeInterface;
use crate::interfaces::i_mailbox::{ use crate::interfaces::i_mailbox::{
IMailbox as EthereumMailboxInternal, ProcessCall, IMAILBOX_ABI, IMailbox as EthereumMailboxInternal, ProcessCall, IMAILBOX_ABI,
}; };
use crate::interfaces::mailbox::DispatchFilter;
use crate::tx::{call_with_lag, fill_tx_gas_params, report_tx}; use crate::tx::{call_with_lag, fill_tx_gas_params, report_tx};
use crate::{BuildableWithProvider, ConnectionConf, EthereumProvider, TransactionOverrides}; use crate::{BuildableWithProvider, ConnectionConf, EthereumProvider, TransactionOverrides};
use super::multicall::{self, build_multicall}; use super::multicall::{self, build_multicall};
use super::utils::fetch_raw_logs_and_log_meta;
impl<M> std::fmt::Display for EthereumMailboxInternal<M> impl<M> std::fmt::Display for EthereumMailboxInternal<M>
where where
@ -134,7 +137,7 @@ where
/// Note: This call may return duplicates depending on the provider used /// Note: This call may return duplicates depending on the provider used
#[instrument(err, skip(self))] #[instrument(err, skip(self))]
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<HyperlaneMessage>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<HyperlaneMessage>, LogMeta)>> {
@ -157,6 +160,27 @@ where
events.sort_by(|a, b| a.0.inner().nonce.cmp(&b.0.inner().nonce)); events.sort_by(|a, b| a.0.inner().nonce.cmp(&b.0.inner().nonce));
Ok(events) Ok(events)
} }
async fn fetch_logs_by_tx_hash(
&self,
tx_hash: H512,
) -> ChainResult<Vec<(Indexed<HyperlaneMessage>, LogMeta)>> {
let logs = fetch_raw_logs_and_log_meta::<DispatchFilter, M>(
tx_hash,
self.provider.clone(),
self.contract.address(),
)
.await?
.into_iter()
.map(|(log, log_meta)| {
(
HyperlaneMessage::from(log.message.to_vec()).into(),
log_meta,
)
})
.collect();
Ok(logs)
}
} }
#[async_trait] #[async_trait]
@ -183,7 +207,7 @@ where
/// Note: This call may return duplicates depending on the provider used /// Note: This call may return duplicates depending on the provider used
#[instrument(err, skip(self))] #[instrument(err, skip(self))]
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<H256>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<H256>, LogMeta)>> {

@ -11,13 +11,17 @@ use tracing::instrument;
use hyperlane_core::{ use hyperlane_core::{
ChainCommunicationError, ChainResult, Checkpoint, ContractLocator, HyperlaneChain, ChainCommunicationError, ChainResult, Checkpoint, ContractLocator, HyperlaneChain,
HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexed, Indexer, LogMeta, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexed, Indexer, LogMeta,
MerkleTreeHook, MerkleTreeInsertion, SequenceAwareIndexer, H256, MerkleTreeHook, MerkleTreeInsertion, SequenceAwareIndexer, H256, H512,
}; };
use crate::interfaces::merkle_tree_hook::{MerkleTreeHook as MerkleTreeHookContract, Tree}; use crate::interfaces::merkle_tree_hook::{
InsertedIntoTreeFilter, MerkleTreeHook as MerkleTreeHookContract, Tree,
};
use crate::tx::call_with_lag; use crate::tx::call_with_lag;
use crate::{BuildableWithProvider, ConnectionConf, EthereumProvider}; use crate::{BuildableWithProvider, ConnectionConf, EthereumProvider};
use super::utils::fetch_raw_logs_and_log_meta;
// We don't need the reverse of this impl, so it's ok to disable the clippy lint // We don't need the reverse of this impl, so it's ok to disable the clippy lint
#[allow(clippy::from_over_into)] #[allow(clippy::from_over_into)]
impl Into<IncrementalMerkle> for Tree { impl Into<IncrementalMerkle> for Tree {
@ -108,7 +112,7 @@ where
{ {
/// Note: This call may return duplicates depending on the provider used /// Note: This call may return duplicates depending on the provider used
#[instrument(err, skip(self))] #[instrument(err, skip(self))]
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<MerkleTreeInsertion>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<MerkleTreeInsertion>, LogMeta)>> {
@ -142,6 +146,27 @@ where
.as_u32() .as_u32()
.saturating_sub(self.reorg_period)) .saturating_sub(self.reorg_period))
} }
async fn fetch_logs_by_tx_hash(
&self,
tx_hash: H512,
) -> ChainResult<Vec<(Indexed<MerkleTreeInsertion>, LogMeta)>> {
let logs = fetch_raw_logs_and_log_meta::<InsertedIntoTreeFilter, M>(
tx_hash,
self.provider.clone(),
self.contract.address(),
)
.await?
.into_iter()
.map(|(log, log_meta)| {
(
MerkleTreeInsertion::new(log.index, H256::from(log.message_id)).into(),
log_meta,
)
})
.collect();
Ok(logs)
}
} }
#[async_trait] #[async_trait]

@ -1,11 +1,8 @@
pub use {interchain_gas::*, mailbox::*, merkle_tree_hook::*, validator_announce::*}; pub use {interchain_gas::*, mailbox::*, merkle_tree_hook::*, validator_announce::*};
mod interchain_gas; mod interchain_gas;
mod mailbox; mod mailbox;
mod merkle_tree_hook; mod merkle_tree_hook;
mod multicall; mod multicall;
mod utils;
mod validator_announce; mod validator_announce;

@ -0,0 +1,48 @@
use std::sync::Arc;
use ethers::{
abi::RawLog,
providers::Middleware,
types::{H160 as EthersH160, H256 as EthersH256},
};
use ethers_contract::{ContractError, EthEvent, LogMeta as EthersLogMeta};
use hyperlane_core::{ChainResult, LogMeta, H512};
use tracing::warn;
pub async fn fetch_raw_logs_and_log_meta<T: EthEvent, M>(
tx_hash: H512,
provider: Arc<M>,
contract_address: EthersH160,
) -> ChainResult<Vec<(T, LogMeta)>>
where
M: Middleware + 'static,
{
let ethers_tx_hash: EthersH256 = tx_hash.into();
let receipt = provider
.get_transaction_receipt(ethers_tx_hash)
.await
.map_err(|err| ContractError::<M>::MiddlewareError(err))?;
let Some(receipt) = receipt else {
warn!(%tx_hash, "No receipt found for tx hash");
return Ok(vec![]);
};
let logs: Vec<(T, LogMeta)> = receipt
.logs
.into_iter()
.filter_map(|log| {
// Filter out logs that aren't emitted by this contract
if log.address != contract_address {
return None;
}
let raw_log = RawLog {
topics: log.topics.clone(),
data: log.data.to_vec(),
};
let log_meta: EthersLogMeta = (&log).into();
let event_filter = T::decode_log(&raw_log).ok();
event_filter.map(|log| (log, log_meta.into()))
})
.collect();
Ok(logs)
}

@ -35,7 +35,7 @@ pub struct FuelInterchainGasPaymasterIndexer {}
#[async_trait] #[async_trait]
impl Indexer<InterchainGasPayment> for FuelInterchainGasPaymasterIndexer { impl Indexer<InterchainGasPayment> for FuelInterchainGasPaymasterIndexer {
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<InterchainGasPayment>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<InterchainGasPayment>, LogMeta)>> {

@ -126,7 +126,7 @@ pub struct FuelMailboxIndexer {}
#[async_trait] #[async_trait]
impl Indexer<HyperlaneMessage> for FuelMailboxIndexer { impl Indexer<HyperlaneMessage> for FuelMailboxIndexer {
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<HyperlaneMessage>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<HyperlaneMessage>, LogMeta)>> {
@ -140,7 +140,7 @@ impl Indexer<HyperlaneMessage> for FuelMailboxIndexer {
#[async_trait] #[async_trait]
impl Indexer<H256> for FuelMailboxIndexer { impl Indexer<H256> for FuelMailboxIndexer {
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<H256>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<H256>, LogMeta)>> {

@ -246,7 +246,7 @@ impl SealevelInterchainGasPaymasterIndexer {
#[async_trait] #[async_trait]
impl Indexer<InterchainGasPayment> for SealevelInterchainGasPaymasterIndexer { impl Indexer<InterchainGasPayment> for SealevelInterchainGasPaymasterIndexer {
#[instrument(err, skip(self))] #[instrument(err, skip(self))]
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<InterchainGasPayment>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<InterchainGasPayment>, LogMeta)>> {

@ -646,7 +646,7 @@ impl SequenceAwareIndexer<HyperlaneMessage> for SealevelMailboxIndexer {
#[async_trait] #[async_trait]
impl Indexer<HyperlaneMessage> for SealevelMailboxIndexer { impl Indexer<HyperlaneMessage> for SealevelMailboxIndexer {
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<HyperlaneMessage>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<HyperlaneMessage>, LogMeta)>> {
@ -670,7 +670,7 @@ impl Indexer<HyperlaneMessage> for SealevelMailboxIndexer {
#[async_trait] #[async_trait]
impl Indexer<H256> for SealevelMailboxIndexer { impl Indexer<H256> for SealevelMailboxIndexer {
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
_range: RangeInclusive<u32>, _range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<H256>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<H256>, LogMeta)>> {

@ -83,11 +83,11 @@ pub struct SealevelMerkleTreeHookIndexer(SealevelMailboxIndexer);
#[async_trait] #[async_trait]
impl Indexer<MerkleTreeInsertion> for SealevelMerkleTreeHookIndexer { impl Indexer<MerkleTreeInsertion> for SealevelMerkleTreeHookIndexer {
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<MerkleTreeInsertion>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<MerkleTreeInsertion>, LogMeta)>> {
let messages = Indexer::<HyperlaneMessage>::fetch_logs(&self.0, range).await?; let messages = Indexer::<HyperlaneMessage>::fetch_logs_in_range(&self.0, range).await?;
let merkle_tree_insertions = messages let merkle_tree_insertions = messages
.into_iter() .into_iter()
.map(|(m, meta)| (message_to_merkle_tree_insertion(m.inner()).into(), meta)) .map(|(m, meta)| (message_to_merkle_tree_insertion(m.inner()).into(), meta))

@ -634,13 +634,19 @@
}, },
"injective": { "injective": {
"bech32Prefix": "inj", "bech32Prefix": "inj",
"blockExplorers": [
],
"blocks": { "blocks": {
"confirmations": 1,
"estimateBlockTime": 1,
"reorgPeriod": 10 "reorgPeriod": 10
}, },
"canonicalAsset": "inj", "canonicalAsset": "inj",
"chainId": "injective-1", "chainId": "injective-1",
"contractAddressBytes": 20, "contractAddressBytes": 20,
"domainId": "6909546", "displayName": "Injective",
"domainId": 6909546,
"gasCurrencyCoinGeckoId": "injective-protocol",
"gasPrice": { "gasPrice": {
"amount": "700000000", "amount": "700000000",
"denom": "inj" "denom": "inj"
@ -658,12 +664,24 @@
"mailbox": "0x0f7fb53961d70687e352aa55cb329ca76edc0c19", "mailbox": "0x0f7fb53961d70687e352aa55cb329ca76edc0c19",
"merkleTreeHook": "0x568ad3638447f07def384969f4ea39fae3802962", "merkleTreeHook": "0x568ad3638447f07def384969f4ea39fae3802962",
"name": "injective", "name": "injective",
"nativeToken": {
"decimals": 18,
"denom": "inj",
"name": "Injective",
"symbol": "INJ"
},
"protocol": "cosmos", "protocol": "cosmos",
"restUrls": [
{
"http": "https://sentry.lcd.injective.network:443"
}
],
"rpcUrls": [ "rpcUrls": [
{ {
"http": "https://injective-rpc.polkachu.com" "http": "https://sentry.tm.injective.network:443"
} }
], ],
"slip44": 118,
"validatorAnnounce": "0x1fb225b2fcfbe75e614a1d627de97ff372242eed" "validatorAnnounce": "0x1fb225b2fcfbe75e614a1d627de97ff372242eed"
}, },
"mantapacific": { "mantapacific": {
@ -837,13 +855,25 @@
}, },
"neutron": { "neutron": {
"bech32Prefix": "neutron", "bech32Prefix": "neutron",
"blockExplorers": [
{
"apiUrl": "https://www.mintscan.io/neutron",
"family": "other",
"name": "Mintscan",
"url": "https://www.mintscan.io/neutron"
}
],
"blocks": { "blocks": {
"confirmations": 1,
"estimateBlockTime": 3,
"reorgPeriod": 1 "reorgPeriod": 1
}, },
"canonicalAsset": "untrn", "canonicalAsset": "untrn",
"chainId": "neutron-1", "chainId": "neutron-1",
"contractAddressBytes": 32, "contractAddressBytes": 32,
"domainId": "1853125230", "displayName": "Neutron",
"domainId": 1853125230,
"gasCurrencyCoinGeckoId": "neutron-3",
"gasPrice": { "gasPrice": {
"amount": "0.0053", "amount": "0.0053",
"denom": "untrn" "denom": "untrn"
@ -858,10 +888,22 @@
"from": 4000000 "from": 4000000
}, },
"interchainGasPaymaster": "0x504ee9ac43ec5814e00c7d21869a90ec52becb489636bdf893b7df9d606b5d67", "interchainGasPaymaster": "0x504ee9ac43ec5814e00c7d21869a90ec52becb489636bdf893b7df9d606b5d67",
"isTestnet": false,
"mailbox": "0x848426d50eb2104d5c6381ec63757930b1c14659c40db8b8081e516e7c5238fc", "mailbox": "0x848426d50eb2104d5c6381ec63757930b1c14659c40db8b8081e516e7c5238fc",
"merkleTreeHook": "0xcd30a0001cc1f436c41ef764a712ebabc5a144140e3fd03eafe64a9a24e4e27c", "merkleTreeHook": "0xcd30a0001cc1f436c41ef764a712ebabc5a144140e3fd03eafe64a9a24e4e27c",
"name": "neutron", "name": "neutron",
"nativeToken": {
"decimals": 6,
"denom": "untrn",
"name": "Neutron",
"symbol": "NTRN"
},
"protocol": "cosmos", "protocol": "cosmos",
"restUrls": [
{
"http": "https://rest-lb.neutron.org"
}
],
"rpcUrls": [ "rpcUrls": [
{ {
"http": "https://rpc-kralum.neutron-1.neutron.org" "http": "https://rpc-kralum.neutron-1.neutron.org"
@ -872,6 +914,7 @@
"prefix": "neutron", "prefix": "neutron",
"type": "cosmosKey" "type": "cosmosKey"
}, },
"slip44": 118,
"validatorAnnounce": "0xf3aa0d652226e21ae35cd9035c492ae41725edc9036edf0d6a48701b153b90a0" "validatorAnnounce": "0xf3aa0d652226e21ae35cd9035c492ae41725edc9036edf0d6a48701b153b90a0"
}, },
"optimism": { "optimism": {
@ -998,8 +1041,8 @@
"name": "polygon", "name": "polygon",
"nativeToken": { "nativeToken": {
"decimals": 18, "decimals": 18,
"name": "Ether", "name": "Matic",
"symbol": "ETH" "symbol": "MATIC"
}, },
"pausableHook": "0x748040afB89B8FdBb992799808215419d36A0930", "pausableHook": "0x748040afB89B8FdBb992799808215419d36A0930",
"pausableIsm": "0x6741e91fFDC31c7786E3684427c628dad06299B0", "pausableIsm": "0x6741e91fFDC31c7786E3684427c628dad06299B0",

@ -207,6 +207,56 @@
"timelockController": "0x0000000000000000000000000000000000000000", "timelockController": "0x0000000000000000000000000000000000000000",
"validatorAnnounce": "0x4f7179A691F8a684f56cF7Fed65171877d30739a" "validatorAnnounce": "0x4f7179A691F8a684f56cF7Fed65171877d30739a"
}, },
"holesky": {
"blockExplorers": [
{
"apiUrl": "https://api-holesky.etherscan.io/api",
"family": "etherscan",
"name": "Etherscan",
"url": "https://holesky.etherscan.io"
}
],
"blocks": {
"confirmations": 1,
"estimateBlockTime": 13,
"reorgPeriod": 2
},
"chainId": 17000,
"displayName": "Holesky",
"domainId": 17000,
"domainRoutingIsmFactory": "0xDDcFEcF17586D08A5740B7D91735fcCE3dfe3eeD",
"fallbackRoutingHook": "0x07009DA2249c388aD0f416a235AfE90D784e1aAc",
"index": {
"from": 1543015
},
"interchainGasPaymaster": "0x5CBf4e70448Ed46c2616b04e9ebc72D29FF0cfA9",
"interchainSecurityModule": "0x751f2b684EeBb916dB777767CCb8fd793C8b2956",
"isTestnet": true,
"mailbox": "0x46f7C5D896bbeC89bE1B19e4485e59b4Be49e9Cc",
"merkleTreeHook": "0x98AAE089CaD930C64a76dD2247a2aC5773a4B8cE",
"name": "holesky",
"nativeToken": {
"decimals": 18,
"name": "Ether",
"symbol": "ETH"
},
"pausableHook": "0xF7561c34f17A32D5620583A3397C304e7038a7F6",
"protocol": "ethereum",
"protocolFee": "0x6b1bb4ce664Bb4164AEB4d3D2E7DE7450DD8084C",
"proxyAdmin": "0x33dB966328Ea213b0f76eF96CA368AB37779F065",
"rpcUrls": [
{
"http": "https://ethereum-holesky-rpc.publicnode.com"
}
],
"staticAggregationHookFactory": "0x589C201a07c26b4725A4A829d772f24423da480B",
"staticAggregationIsmFactory": "0x54148470292C24345fb828B003461a9444414517",
"staticMerkleRootMultisigIsmFactory": "0xC2E36cd6e32e194EE11f15D9273B64461A4D49A2",
"staticMessageIdMultisigIsmFactory": "0x6966b0E55883d49BFB24539356a2f8A673E02039",
"storageGasOracle": "0x2b2a158B4059C840c7aC67399B153bb567D06303",
"testRecipient": "0x86fb9F1c124fB20ff130C41a79a432F770f67AFD",
"validatorAnnounce": "0xAb9B273366D794B7F80B4378bc8Aaca75C6178E2"
},
"plumetestnet": { "plumetestnet": {
"aggregationHook": "0x31dF0EEE7Dc7565665468698a0da221225619a1B", "aggregationHook": "0x31dF0EEE7Dc7565665468698a0da221225619a1B",
"blockExplorers": [ "blockExplorers": [

@ -13,8 +13,18 @@ pub enum CursorType {
RateLimited, RateLimited,
} }
// H256 * 1M = 32MB per origin chain worst case
// With one such channel per origin chain.
const TX_ID_CHANNEL_CAPACITY: Option<usize> = Some(1_000_000);
pub trait Indexable { pub trait Indexable {
/// Returns the configured cursor type of this type for the given domain, (e.g. `SequenceAware` or `RateLimited`)
fn indexing_cursor(domain: HyperlaneDomainProtocol) -> CursorType; fn indexing_cursor(domain: HyperlaneDomainProtocol) -> CursorType;
/// Indexing tasks may have channels open between them to share information that improves reliability (such as the txid where a message event was indexed).
/// By default this method is None, and it should return a channel capacity if this indexing task is to broadcast anything to other tasks.
fn broadcast_channel_size() -> Option<usize> {
None
}
} }
impl Indexable for HyperlaneMessage { impl Indexable for HyperlaneMessage {
@ -26,6 +36,11 @@ impl Indexable for HyperlaneMessage {
HyperlaneDomainProtocol::Cosmos => CursorType::SequenceAware, HyperlaneDomainProtocol::Cosmos => CursorType::SequenceAware,
} }
} }
// Only broadcast txids from the message indexing task
fn broadcast_channel_size() -> Option<usize> {
TX_ID_CHANNEL_CAPACITY
}
} }
impl Indexable for InterchainGasPayment { impl Indexable for InterchainGasPayment {

@ -216,6 +216,16 @@ where
} }
} }
impl<T> Debug for RateLimitedContractSyncCursor<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RateLimitedContractSyncCursor")
.field("tip", &self.tip)
.field("last_tip_update", &self.last_tip_update)
.field("sync_state", &self.sync_state)
.finish()
}
}
#[cfg(test)] #[cfg(test)]
pub(crate) mod test { pub(crate) mod test {
use super::*; use super::*;
@ -234,7 +244,7 @@ pub(crate) mod test {
#[async_trait] #[async_trait]
impl Indexer<()> for Indexer { impl Indexer<()> for Indexer {
async fn fetch_logs(&self, range: RangeInclusive<u32>) -> ChainResult<Vec<(hyperlane_core::Indexed<()> , LogMeta)>>; async fn fetch_logs_in_range(&self, range: RangeInclusive<u32>) -> ChainResult<Vec<(hyperlane_core::Indexed<()> , LogMeta)>>;
async fn get_finalized_block_number(&self) -> ChainResult<u32>; async fn get_finalized_block_number(&self) -> ChainResult<u32>;
} }
} }

@ -9,10 +9,13 @@ use hyperlane_core::{
HyperlaneSequenceAwareIndexerStoreReader, IndexMode, Indexed, LogMeta, SequenceIndexed, HyperlaneSequenceAwareIndexerStoreReader, IndexMode, Indexed, LogMeta, SequenceIndexed,
}; };
use itertools::Itertools; use itertools::Itertools;
use tokio::time::sleep;
use tracing::{debug, instrument, warn}; use tracing::{debug, instrument, warn};
use super::{LastIndexedSnapshot, TargetSnapshot}; use super::{LastIndexedSnapshot, TargetSnapshot};
const MAX_BACKWARD_SYNC_BLOCKING_TIME: Duration = Duration::from_secs(5);
/// A sequence-aware cursor that syncs backward until there are no earlier logs to index. /// A sequence-aware cursor that syncs backward until there are no earlier logs to index.
pub(crate) struct BackwardSequenceAwareSyncCursor<T> { pub(crate) struct BackwardSequenceAwareSyncCursor<T> {
/// The max chunk size to query for logs. /// The max chunk size to query for logs.
@ -32,6 +35,17 @@ pub(crate) struct BackwardSequenceAwareSyncCursor<T> {
index_mode: IndexMode, index_mode: IndexMode,
} }
impl<T> Debug for BackwardSequenceAwareSyncCursor<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BackwardSequenceAwareSyncCursor")
.field("chunk_size", &self.chunk_size)
.field("last_indexed_snapshot", &self.last_indexed_snapshot)
.field("current_indexing_snapshot", &self.current_indexing_snapshot)
.field("index_mode", &self.index_mode)
.finish()
}
}
impl<T: Debug> BackwardSequenceAwareSyncCursor<T> { impl<T: Debug> BackwardSequenceAwareSyncCursor<T> {
#[instrument( #[instrument(
skip(db), skip(db),
@ -68,7 +82,11 @@ impl<T: Debug> BackwardSequenceAwareSyncCursor<T> {
#[instrument(ret)] #[instrument(ret)]
pub async fn get_next_range(&mut self) -> Result<Option<RangeInclusive<u32>>> { pub async fn get_next_range(&mut self) -> Result<Option<RangeInclusive<u32>>> {
// Skip any already indexed logs. // Skip any already indexed logs.
self.skip_indexed().await?; tokio::select! {
res = self.skip_indexed() => res?,
// return early to allow the forward cursor to also make progress
_ = sleep(MAX_BACKWARD_SYNC_BLOCKING_TIME) => { return Ok(None); }
};
// If `self.current_indexing_snapshot` is None, we are synced and there are no more ranges to query. // If `self.current_indexing_snapshot` is None, we are synced and there are no more ranges to query.
// Otherwise, we query the next range, searching for logs prior to and including the current indexing snapshot. // Otherwise, we query the next range, searching for logs prior to and including the current indexing snapshot.
@ -309,17 +327,6 @@ impl<T: Debug> BackwardSequenceAwareSyncCursor<T> {
} }
} }
impl<T: Debug> Debug for BackwardSequenceAwareSyncCursor<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BackwardSequenceAwareSyncCursor")
.field("chunk_size", &self.chunk_size)
.field("current_indexing_snapshot", &self.current_indexing_snapshot)
.field("last_indexed_snapshot", &self.last_indexed_snapshot)
.field("index_mode", &self.index_mode)
.finish()
}
}
#[async_trait] #[async_trait]
impl<T: Send + Sync + Clone + Debug + 'static> ContractSyncCursor<T> impl<T: Send + Sync + Clone + Debug + 'static> ContractSyncCursor<T>
for BackwardSequenceAwareSyncCursor<T> for BackwardSequenceAwareSyncCursor<T>

@ -41,6 +41,18 @@ pub(crate) struct ForwardSequenceAwareSyncCursor<T> {
index_mode: IndexMode, index_mode: IndexMode,
} }
impl<T> Debug for ForwardSequenceAwareSyncCursor<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ForwardSequenceAwareSyncCursor")
.field("chunk_size", &self.chunk_size)
.field("last_indexed_snapshot", &self.last_indexed_snapshot)
.field("current_indexing_snapshot", &self.current_indexing_snapshot)
.field("target_snapshot", &self.target_snapshot)
.field("index_mode", &self.index_mode)
.finish()
}
}
impl<T: Debug> ForwardSequenceAwareSyncCursor<T> { impl<T: Debug> ForwardSequenceAwareSyncCursor<T> {
#[instrument( #[instrument(
skip(db, latest_sequence_querier), skip(db, latest_sequence_querier),
@ -391,18 +403,6 @@ impl<T: Debug> ForwardSequenceAwareSyncCursor<T> {
} }
} }
impl<T: Debug> Debug for ForwardSequenceAwareSyncCursor<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ForwardSequenceAwareSyncCursor")
.field("chunk_size", &self.chunk_size)
.field("current_indexing_snapshot", &self.current_indexing_snapshot)
.field("last_indexed_snapshot", &self.last_indexed_snapshot)
.field("target_snapshot", &self.target_snapshot)
.field("index_mode", &self.index_mode)
.finish()
}
}
#[async_trait] #[async_trait]
impl<T: Send + Sync + Clone + Debug + 'static> ContractSyncCursor<T> impl<T: Send + Sync + Clone + Debug + 'static> ContractSyncCursor<T>
for ForwardSequenceAwareSyncCursor<T> for ForwardSequenceAwareSyncCursor<T>
@ -493,7 +493,7 @@ pub(crate) mod test {
where where
T: Sequenced + Debug, T: Sequenced + Debug,
{ {
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
_range: RangeInclusive<u32>, _range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<T>, LogMeta)>> { ) -> ChainResult<Vec<(Indexed<T>, LogMeta)>> {

@ -62,6 +62,7 @@ pub enum SyncDirection {
/// A cursor that prefers to sync forward, but will sync backward if there is nothing to /// A cursor that prefers to sync forward, but will sync backward if there is nothing to
/// sync forward. /// sync forward.
#[derive(Debug)]
pub(crate) struct ForwardBackwardSequenceAwareSyncCursor<T> { pub(crate) struct ForwardBackwardSequenceAwareSyncCursor<T> {
forward: ForwardSequenceAwareSyncCursor<T>, forward: ForwardSequenceAwareSyncCursor<T>,
backward: BackwardSequenceAwareSyncCursor<T>, backward: BackwardSequenceAwareSyncCursor<T>,

@ -10,9 +10,13 @@ use hyperlane_core::{
HyperlaneSequenceAwareIndexerStore, HyperlaneWatermarkedLogStore, Indexer, HyperlaneSequenceAwareIndexerStore, HyperlaneWatermarkedLogStore, Indexer,
SequenceAwareIndexer, SequenceAwareIndexer,
}; };
use hyperlane_core::{Indexed, LogMeta, H512};
pub use metrics::ContractSyncMetrics; pub use metrics::ContractSyncMetrics;
use prometheus::core::{AtomicI64, AtomicU64, GenericCounter, GenericGauge};
use tokio::sync::broadcast::error::TryRecvError;
use tokio::sync::broadcast::{Receiver as BroadcastReceiver, Sender as BroadcastSender};
use tokio::time::sleep; use tokio::time::sleep;
use tracing::{debug, info, warn}; use tracing::{debug, info, instrument, trace, warn};
use crate::settings::IndexSettings; use crate::settings::IndexSettings;
@ -27,17 +31,33 @@ const SLEEP_DURATION: Duration = Duration::from_secs(5);
/// Entity that drives the syncing of an agent's db with on-chain data. /// Entity that drives the syncing of an agent's db with on-chain data.
/// Extracts chain-specific data (emitted checkpoints, messages, etc) from an /// Extracts chain-specific data (emitted checkpoints, messages, etc) from an
/// `indexer` and fills the agent's db with this data. /// `indexer` and fills the agent's db with this data.
#[derive(Debug, new, Clone)] #[derive(Debug)]
pub struct ContractSync<T, D: HyperlaneLogStore<T>, I: Indexer<T>> { pub struct ContractSync<T: Indexable, D: HyperlaneLogStore<T>, I: Indexer<T>> {
domain: HyperlaneDomain, domain: HyperlaneDomain,
db: D, db: D,
indexer: I, indexer: I,
metrics: ContractSyncMetrics, metrics: ContractSyncMetrics,
broadcast_sender: Option<BroadcastSender<H512>>,
_phantom: PhantomData<T>, _phantom: PhantomData<T>,
} }
impl<T: Indexable, D: HyperlaneLogStore<T>, I: Indexer<T>> ContractSync<T, D, I> {
/// Create a new ContractSync
pub fn new(domain: HyperlaneDomain, db: D, indexer: I, metrics: ContractSyncMetrics) -> Self {
Self {
domain,
db,
indexer,
metrics,
broadcast_sender: T::broadcast_channel_size().map(BroadcastSender::new),
_phantom: PhantomData,
}
}
}
impl<T, D, I> ContractSync<T, D, I> impl<T, D, I> ContractSync<T, D, I>
where where
T: Indexable + Debug + Send + Sync + Clone + Eq + Hash + 'static,
D: HyperlaneLogStore<T>, D: HyperlaneLogStore<T>,
I: Indexer<T> + 'static, I: Indexer<T> + 'static,
{ {
@ -45,36 +65,86 @@ where
pub fn domain(&self) -> &HyperlaneDomain { pub fn domain(&self) -> &HyperlaneDomain {
&self.domain &self.domain
} }
fn get_broadcaster(&self) -> Option<BroadcastSender<H512>> {
self.broadcast_sender.clone()
} }
impl<T, D, I> ContractSync<T, D, I>
where
T: Debug + Send + Sync + Clone + Eq + Hash + 'static,
D: HyperlaneLogStore<T>,
I: Indexer<T> + 'static,
{
/// Sync logs and write them to the LogStore /// Sync logs and write them to the LogStore
#[tracing::instrument(name = "ContractSync", fields(domain=self.domain().name()), skip(self, cursor))] #[instrument(name = "ContractSync", fields(domain=self.domain().name()), skip(self, opts))]
pub async fn sync(&self, label: &'static str, mut cursor: Box<dyn ContractSyncCursor<T>>) { pub async fn sync(&self, label: &'static str, mut opts: SyncOptions<T>) {
let chain_name = self.domain.as_ref(); let chain_name = self.domain.as_ref();
let indexed_height = self let indexed_height_metric = self
.metrics .metrics
.indexed_height .indexed_height
.with_label_values(&[label, chain_name]); .with_label_values(&[label, chain_name]);
let stored_logs = self let stored_logs_metric = self
.metrics .metrics
.stored_events .stored_events
.with_label_values(&[label, chain_name]); .with_label_values(&[label, chain_name]);
loop { loop {
indexed_height.set(cursor.latest_queried_block() as i64); if let Some(rx) = opts.tx_id_receiver.as_mut() {
self.fetch_logs_from_receiver(rx, &stored_logs_metric).await;
}
if let Some(cursor) = opts.cursor.as_mut() {
self.fetch_logs_with_cursor(cursor, &stored_logs_metric, &indexed_height_metric)
.await;
}
}
}
#[instrument(fields(domain=self.domain().name()), skip(self, recv, stored_logs_metric))]
async fn fetch_logs_from_receiver(
&self,
recv: &mut BroadcastReceiver<H512>,
stored_logs_metric: &GenericCounter<AtomicU64>,
) {
loop {
match recv.try_recv() {
Ok(tx_id) => {
let logs = match self.indexer.fetch_logs_by_tx_hash(tx_id).await {
Ok(logs) => logs,
Err(err) => {
warn!(?err, ?tx_id, "Error fetching logs for tx id");
continue;
}
};
let logs = self.dedupe_and_store_logs(logs, stored_logs_metric).await;
let num_logs = logs.len() as u64;
info!(
num_logs,
?tx_id,
sequences = ?logs.iter().map(|(log, _)| log.sequence).collect::<Vec<_>>(),
"Found log(s) for tx id"
);
}
Err(TryRecvError::Empty) => {
trace!("No txid received");
break;
}
Err(err) => {
warn!(?err, "Error receiving txid from channel");
break;
}
}
}
}
#[instrument(fields(domain=self.domain().name()), skip(self, stored_logs_metric, indexed_height_metric))]
async fn fetch_logs_with_cursor(
&self,
cursor: &mut Box<dyn ContractSyncCursor<T>>,
stored_logs_metric: &GenericCounter<AtomicU64>,
indexed_height_metric: &GenericGauge<AtomicI64>,
) {
indexed_height_metric.set(cursor.latest_queried_block() as i64);
let (action, eta) = match cursor.next_action().await { let (action, eta) = match cursor.next_action().await {
Ok((action, eta)) => (action, eta), Ok((action, eta)) => (action, eta),
Err(err) => { Err(err) => {
warn!(?err, "Error getting next action"); warn!(?err, "Error getting next action");
sleep(SLEEP_DURATION).await; sleep(SLEEP_DURATION).await;
continue; return;
} }
}; };
let sleep_duration = match action { let sleep_duration = match action {
@ -82,34 +152,35 @@ where
// from the loop (the sleep duration) // from the loop (the sleep duration)
#[allow(clippy::never_loop)] #[allow(clippy::never_loop)]
CursorAction::Query(range) => loop { CursorAction::Query(range) => loop {
debug!(?range, "Looking for for events in index range"); debug!(?range, "Looking for events in index range");
let logs = match self.indexer.fetch_logs(range.clone()).await { let logs = match self.indexer.fetch_logs_in_range(range.clone()).await {
Ok(logs) => logs, Ok(logs) => logs,
Err(err) => { Err(err) => {
warn!(?err, "Error fetching logs"); warn!(?err, ?range, "Error fetching logs in range");
break SLEEP_DURATION; break SLEEP_DURATION;
} }
}; };
let deduped_logs = HashSet::<_>::from_iter(logs);
let logs = Vec::from_iter(deduped_logs);
let logs = self.dedupe_and_store_logs(logs, stored_logs_metric).await;
let logs_found = logs.len() as u64;
info!( info!(
?range, ?range,
num_logs = logs.len(), num_logs = logs_found,
estimated_time_to_sync = fmt_sync_time(eta), estimated_time_to_sync = fmt_sync_time(eta),
sequences = ?logs.iter().map(|(log, _)| log.sequence).collect::<Vec<_>>(),
cursor = ?cursor,
"Found log(s) in index range" "Found log(s) in index range"
); );
// Store deliveries
let stored = match self.db.store_logs(&logs).await { if let Some(tx) = self.broadcast_sender.as_ref() {
Ok(stored) => stored, logs.iter().for_each(|(_, meta)| {
Err(err) => { if let Err(err) = tx.send(meta.transaction_id) {
warn!(?err, "Error storing logs in db"); trace!(?err, "Error sending txid to receiver");
break SLEEP_DURATION;
} }
}; });
// Report amount of deliveries stored into db }
stored_logs.inc_by(stored as u64);
// Update cursor // Update cursor
if let Err(err) = cursor.update(logs, range).await { if let Err(err) = cursor.update(logs, range).await {
warn!(?err, "Error updating cursor"); warn!(?err, "Error updating cursor");
@ -119,8 +190,36 @@ where
}, },
CursorAction::Sleep(duration) => duration, CursorAction::Sleep(duration) => duration,
}; };
sleep(sleep_duration).await; sleep(sleep_duration).await
}
async fn dedupe_and_store_logs(
&self,
logs: Vec<(Indexed<T>, LogMeta)>,
stored_logs_metric: &GenericCounter<AtomicU64>,
) -> Vec<(Indexed<T>, LogMeta)> {
let deduped_logs = HashSet::<_>::from_iter(logs);
let logs = Vec::from_iter(deduped_logs);
// Store deliveries
let stored = match self.db.store_logs(&logs).await {
Ok(stored) => stored,
Err(err) => {
warn!(?err, "Error storing logs in db");
Default::default()
}
};
if stored > 0 {
debug!(
domain = self.domain.as_ref(),
count = stored,
sequences = ?logs.iter().map(|(log, _)| log.sequence).collect::<Vec<_>>(),
"Stored logs in db",
);
} }
// Report amount of deliveries stored into db
stored_logs_metric.inc_by(stored as u64);
logs
} }
} }
@ -141,16 +240,38 @@ pub trait ContractSyncer<T>: Send + Sync {
async fn cursor(&self, index_settings: IndexSettings) -> Box<dyn ContractSyncCursor<T>>; async fn cursor(&self, index_settings: IndexSettings) -> Box<dyn ContractSyncCursor<T>>;
/// Syncs events from the indexer using the provided cursor /// Syncs events from the indexer using the provided cursor
async fn sync(&self, label: &'static str, cursor: Box<dyn ContractSyncCursor<T>>); async fn sync(&self, label: &'static str, opts: SyncOptions<T>);
/// The domain of this syncer /// The domain of this syncer
fn domain(&self) -> &HyperlaneDomain; fn domain(&self) -> &HyperlaneDomain;
/// If this syncer is also a broadcaster, return the channel to receive txids
fn get_broadcaster(&self) -> Option<BroadcastSender<H512>>;
}
#[derive(new)]
/// Options for syncing events
pub struct SyncOptions<T> {
// Keep as optional fields for now to run them simultaneously.
// Might want to refactor into an enum later, where we either index with a cursor or rely on receiving
// txids from a channel to other indexing tasks
cursor: Option<Box<dyn ContractSyncCursor<T>>>,
tx_id_receiver: Option<BroadcastReceiver<H512>>,
}
impl<T> From<Box<dyn ContractSyncCursor<T>>> for SyncOptions<T> {
fn from(cursor: Box<dyn ContractSyncCursor<T>>) -> Self {
Self {
cursor: Some(cursor),
tx_id_receiver: None,
}
}
} }
#[async_trait] #[async_trait]
impl<T> ContractSyncer<T> for WatermarkContractSync<T> impl<T> ContractSyncer<T> for WatermarkContractSync<T>
where where
T: Debug + Send + Sync + Clone + Eq + Hash + 'static, T: Indexable + Debug + Send + Sync + Clone + Eq + Hash + 'static,
{ {
/// Returns a new cursor to be used for syncing events from the indexer based on time /// Returns a new cursor to be used for syncing events from the indexer based on time
async fn cursor(&self, index_settings: IndexSettings) -> Box<dyn ContractSyncCursor<T>> { async fn cursor(&self, index_settings: IndexSettings) -> Box<dyn ContractSyncCursor<T>> {
@ -172,13 +293,17 @@ where
) )
} }
async fn sync(&self, label: &'static str, cursor: Box<dyn ContractSyncCursor<T>>) { async fn sync(&self, label: &'static str, opts: SyncOptions<T>) {
ContractSync::sync(self, label, cursor).await; ContractSync::sync(self, label, opts).await
} }
fn domain(&self) -> &HyperlaneDomain { fn domain(&self) -> &HyperlaneDomain {
ContractSync::domain(self) ContractSync::domain(self)
} }
fn get_broadcaster(&self) -> Option<BroadcastSender<H512>> {
ContractSync::get_broadcaster(self)
}
} }
/// Log store for sequence aware cursors /// Log store for sequence aware cursors
@ -191,7 +316,7 @@ pub type SequencedDataContractSync<T> =
#[async_trait] #[async_trait]
impl<T> ContractSyncer<T> for SequencedDataContractSync<T> impl<T> ContractSyncer<T> for SequencedDataContractSync<T>
where where
T: Send + Sync + Debug + Clone + Eq + Hash + 'static, T: Indexable + Send + Sync + Debug + Clone + Eq + Hash + 'static,
{ {
/// Returns a new cursor to be used for syncing dispatched messages from the indexer /// Returns a new cursor to be used for syncing dispatched messages from the indexer
async fn cursor(&self, index_settings: IndexSettings) -> Box<dyn ContractSyncCursor<T>> { async fn cursor(&self, index_settings: IndexSettings) -> Box<dyn ContractSyncCursor<T>> {
@ -207,11 +332,15 @@ where
) )
} }
async fn sync(&self, label: &'static str, cursor: Box<dyn ContractSyncCursor<T>>) { async fn sync(&self, label: &'static str, opts: SyncOptions<T>) {
ContractSync::sync(self, label, cursor).await; ContractSync::sync(self, label, opts).await;
} }
fn domain(&self) -> &HyperlaneDomain { fn domain(&self) -> &HyperlaneDomain {
ContractSync::domain(self) ContractSync::domain(self)
} }
fn get_broadcaster(&self) -> Option<BroadcastSender<H512>> {
ContractSync::get_broadcaster(self)
}
} }

@ -242,10 +242,10 @@ impl HyperlaneRocksDB {
&self, &self,
event: InterchainGasExpenditure, event: InterchainGasExpenditure,
) -> DbResult<()> { ) -> DbResult<()> {
let existing_payment = self.retrieve_gas_expenditure_by_message_id(event.message_id)?; let existing_expenditure = self.retrieve_gas_expenditure_by_message_id(event.message_id)?;
let total = existing_payment + event; let total = existing_expenditure + event;
debug!(?event, new_total_gas_payment=?total, "Storing gas payment"); debug!(?event, new_total_gas_expenditure=?total, "Storing gas expenditure");
self.store_interchain_gas_expenditure_data_by_message_id( self.store_interchain_gas_expenditure_data_by_message_id(
&total.message_id, &total.message_id,
&InterchainGasExpenditureData { &InterchainGasExpenditureData {

@ -160,7 +160,7 @@ impl Settings {
db: Arc<D>, db: Arc<D>,
) -> eyre::Result<Arc<SequencedDataContractSync<T>>> ) -> eyre::Result<Arc<SequencedDataContractSync<T>>>
where where
T: Debug, T: Indexable + Debug,
SequenceIndexer<T>: TryFromWithMetrics<ChainConf>, SequenceIndexer<T>: TryFromWithMetrics<ChainConf>,
D: HyperlaneLogStore<T> + HyperlaneSequenceAwareIndexerStoreReader<T> + 'static, D: HyperlaneLogStore<T> + HyperlaneSequenceAwareIndexerStoreReader<T> + 'static,
{ {
@ -184,7 +184,7 @@ impl Settings {
db: Arc<D>, db: Arc<D>,
) -> eyre::Result<Arc<WatermarkContractSync<T>>> ) -> eyre::Result<Arc<WatermarkContractSync<T>>>
where where
T: Debug, T: Indexable + Debug,
SequenceIndexer<T>: TryFromWithMetrics<ChainConf>, SequenceIndexer<T>: TryFromWithMetrics<ChainConf>,
D: HyperlaneLogStore<T> + HyperlaneWatermarkedLogStore<T> + 'static, D: HyperlaneLogStore<T> + HyperlaneWatermarkedLogStore<T> + 'static,
{ {

@ -1,6 +1,6 @@
//! Common settings and configuration for Hyperlane agents //! Common settings and configuration for Hyperlane agents
//! //!
//! The correct settings shape is defined in the TypeScript SDK metadata. While the the exact shape //! The correct settings shape is defined in the TypeScript SDK metadata. While the exact shape
//! and validations it defines are not applied here, we should mirror them. //! and validations it defines are not applied here, we should mirror them.
//! ANY CHANGES HERE NEED TO BE REFLECTED IN THE TYPESCRIPT SDK. //! ANY CHANGES HERE NEED TO BE REFLECTED IN THE TYPESCRIPT SDK.
//! //!

@ -1,6 +1,6 @@
//! This module is responsible for parsing the agent's settings. //! This module is responsible for parsing the agent's settings.
//! //!
//! The correct settings shape is defined in the TypeScript SDK metadata. While the the exact shape //! The correct settings shape is defined in the TypeScript SDK metadata. While the exact shape
//! and validations it defines are not applied here, we should mirror them. //! and validations it defines are not applied here, we should mirror them.
//! ANY CHANGES HERE NEED TO BE REFLECTED IN THE TYPESCRIPT SDK. //! ANY CHANGES HERE NEED TO BE REFLECTED IN THE TYPESCRIPT SDK.

@ -49,7 +49,7 @@ uint.workspace = true
tokio = { workspace = true, features = ["rt", "time"] } tokio = { workspace = true, features = ["rt", "time"] }
[features] [features]
default = [] default = ["strum"]
float = [] float = []
test-utils = ["dep:config"] test-utils = ["dep:config"]
agent = ["ethers", "strum"] agent = ["ethers", "strum"]

@ -51,6 +51,7 @@ impl<'a> std::fmt::Display for ContractLocator<'a> {
pub enum KnownHyperlaneDomain { pub enum KnownHyperlaneDomain {
Ethereum = 1, Ethereum = 1,
Sepolia = 11155111, Sepolia = 11155111,
Holesky = 17000,
Polygon = 137, Polygon = 137,
@ -218,7 +219,7 @@ impl KnownHyperlaneDomain {
Moonbeam, Gnosis, MantaPacific, Neutron, Injective, InEvm Moonbeam, Gnosis, MantaPacific, Neutron, Injective, InEvm
], ],
Testnet: [ Testnet: [
Alfajores, MoonbaseAlpha, Sepolia, ScrollSepolia, Chiado, PlumeTestnet, Fuji, BinanceSmartChainTestnet Alfajores, MoonbaseAlpha, Sepolia, ScrollSepolia, Chiado, PlumeTestnet, Fuji, BinanceSmartChainTestnet, Holesky
], ],
LocalTestChain: [Test1, Test2, Test3, FuelTest1, SealevelTest1, SealevelTest2, CosmosTest99990, CosmosTest99991], LocalTestChain: [Test1, Test2, Test3, FuelTest1, SealevelTest1, SealevelTest2, CosmosTest99990, CosmosTest99991],
}) })
@ -229,7 +230,7 @@ impl KnownHyperlaneDomain {
many_to_one!(match self { many_to_one!(match self {
HyperlaneDomainProtocol::Ethereum: [ HyperlaneDomainProtocol::Ethereum: [
Ethereum, Sepolia, Polygon, Avalanche, Fuji, Arbitrum, Ethereum, Sepolia, Holesky, Polygon, Avalanche, Fuji, Arbitrum,
Optimism, BinanceSmartChain, BinanceSmartChainTestnet, Celo, Gnosis, Optimism, BinanceSmartChain, BinanceSmartChainTestnet, Celo, Gnosis,
Alfajores, Moonbeam, InEvm, MoonbaseAlpha, ScrollSepolia, Alfajores, Moonbeam, InEvm, MoonbaseAlpha, ScrollSepolia,
Chiado, MantaPacific, PlumeTestnet, Test1, Test2, Test3 Chiado, MantaPacific, PlumeTestnet, Test1, Test2, Test3
@ -246,7 +247,7 @@ impl KnownHyperlaneDomain {
many_to_one!(match self { many_to_one!(match self {
HyperlaneDomainTechnicalStack::ArbitrumNitro: [Arbitrum, PlumeTestnet], HyperlaneDomainTechnicalStack::ArbitrumNitro: [Arbitrum, PlumeTestnet],
HyperlaneDomainTechnicalStack::Other: [ HyperlaneDomainTechnicalStack::Other: [
Ethereum, Sepolia, Polygon, Avalanche, Fuji, Optimism, Ethereum, Sepolia, Holesky, Polygon, Avalanche, Fuji, Optimism,
BinanceSmartChain, BinanceSmartChainTestnet, Celo, Gnosis, Alfajores, Moonbeam, MoonbaseAlpha, BinanceSmartChain, BinanceSmartChainTestnet, Celo, Gnosis, Alfajores, Moonbeam, MoonbaseAlpha,
ScrollSepolia, Chiado, MantaPacific, Neutron, Injective, InEvm, ScrollSepolia, Chiado, MantaPacific, Neutron, Injective, InEvm,
Test1, Test2, Test3, FuelTest1, SealevelTest1, SealevelTest2, CosmosTest99990, CosmosTest99991 Test1, Test2, Test3, FuelTest1, SealevelTest1, SealevelTest2, CosmosTest99990, CosmosTest99991

@ -1,4 +1,8 @@
use std::{fmt, ops::RangeInclusive, time::Duration}; use std::{
fmt::{self, Debug},
ops::RangeInclusive,
time::Duration,
};
use async_trait::async_trait; use async_trait::async_trait;
use auto_impl::auto_impl; use auto_impl::auto_impl;
@ -9,7 +13,7 @@ use crate::{Indexed, LogMeta};
/// A cursor governs event indexing for a contract. /// A cursor governs event indexing for a contract.
#[async_trait] #[async_trait]
#[auto_impl(Box)] #[auto_impl(Box)]
pub trait ContractSyncCursor<T>: Send + Sync + 'static { pub trait ContractSyncCursor<T>: Debug + Send + Sync + 'static {
/// The next block range that should be queried. /// The next block range that should be queried.
/// This method should be tolerant to being called multiple times in a row /// This method should be tolerant to being called multiple times in a row
/// without any updates in between. /// without any updates in between.

@ -11,7 +11,7 @@ use async_trait::async_trait;
use auto_impl::auto_impl; use auto_impl::auto_impl;
use serde::Deserialize; use serde::Deserialize;
use crate::{ChainResult, Indexed, LogMeta}; use crate::{ChainResult, Indexed, LogMeta, H512};
/// Indexing mode. /// Indexing mode.
#[derive(Copy, Debug, Default, Deserialize, Clone)] #[derive(Copy, Debug, Default, Deserialize, Clone)]
@ -29,13 +29,21 @@ pub enum IndexMode {
#[auto_impl(&, Box, Arc,)] #[auto_impl(&, Box, Arc,)]
pub trait Indexer<T: Sized>: Send + Sync + Debug { pub trait Indexer<T: Sized>: Send + Sync + Debug {
/// Fetch list of logs between blocks `from` and `to`, inclusive. /// Fetch list of logs between blocks `from` and `to`, inclusive.
async fn fetch_logs( async fn fetch_logs_in_range(
&self, &self,
range: RangeInclusive<u32>, range: RangeInclusive<u32>,
) -> ChainResult<Vec<(Indexed<T>, LogMeta)>>; ) -> ChainResult<Vec<(Indexed<T>, LogMeta)>>;
/// Get the chain's latest block number that has reached finality /// Get the chain's latest block number that has reached finality
async fn get_finalized_block_number(&self) -> ChainResult<u32>; async fn get_finalized_block_number(&self) -> ChainResult<u32>;
/// Fetch list of logs emitted in a transaction with the given hash.
async fn fetch_logs_by_tx_hash(
&self,
_tx_hash: H512,
) -> ChainResult<Vec<(Indexed<T>, LogMeta)>> {
Ok(vec![])
}
} }
/// Interface for indexing data in sequence. /// Interface for indexing data in sequence.

@ -10,6 +10,7 @@ pub use interchain_security_module::*;
pub use mailbox::*; pub use mailbox::*;
pub use merkle_tree_hook::*; pub use merkle_tree_hook::*;
pub use multisig_ism::*; pub use multisig_ism::*;
pub use pending_operation::*;
pub use provider::*; pub use provider::*;
pub use routing_ism::*; pub use routing_ism::*;
pub use signing::*; pub use signing::*;
@ -29,6 +30,7 @@ mod interchain_security_module;
mod mailbox; mod mailbox;
mod merkle_tree_hook; mod merkle_tree_hook;
mod multisig_ism; mod multisig_ism;
mod pending_operation;
mod provider; mod provider;
mod routing_ism; mod routing_ism;
mod signing; mod signing;

@ -4,10 +4,16 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use crate::{
ChainResult, FixedPointNumber, HyperlaneDomain, HyperlaneMessage, TryBatchAs, TxOutcome, H256,
U256,
};
use async_trait::async_trait; use async_trait::async_trait;
use hyperlane_core::{HyperlaneDomain, HyperlaneMessage, TryBatchAs, TxOutcome, H256}; use num::CheckedDiv;
use tracing::warn;
use super::op_queue::QueueOperation; /// Boxed operation that can be stored in an operation queue
pub type QueueOperation = Box<dyn PendingOperation>;
/// A pending operation that will be run by the submitter and cause a /// A pending operation that will be run by the submitter and cause a
/// transaction to be sent. /// transaction to be sent.
@ -67,11 +73,21 @@ pub trait PendingOperation: Send + Sync + Debug + TryBatchAs<HyperlaneMessage> {
/// Set the outcome of the `submit` call /// Set the outcome of the `submit` call
fn set_submission_outcome(&mut self, outcome: TxOutcome); fn set_submission_outcome(&mut self, outcome: TxOutcome);
/// Get the estimated the cost of the `submit` call
fn get_tx_cost_estimate(&self) -> Option<U256>;
/// This will be called after the operation has been submitted and is /// This will be called after the operation has been submitted and is
/// responsible for checking if the operation has reached a point at /// responsible for checking if the operation has reached a point at
/// which we consider it safe from reorgs. /// which we consider it safe from reorgs.
async fn confirm(&mut self) -> PendingOperationResult; async fn confirm(&mut self) -> PendingOperationResult;
/// Record the outcome of the operation
fn set_operation_outcome(
&mut self,
submission_outcome: TxOutcome,
submission_estimated_cost: U256,
);
/// Get the earliest instant at which this should next be attempted. /// Get the earliest instant at which this should next be attempted.
/// ///
/// This is only used for sorting, the functions are responsible for /// This is only used for sorting, the functions are responsible for
@ -85,11 +101,41 @@ pub trait PendingOperation: Send + Sync + Debug + TryBatchAs<HyperlaneMessage> {
/// retried immediately. /// retried immediately.
fn reset_attempts(&mut self); fn reset_attempts(&mut self);
#[cfg(test)]
/// Set the number of times this operation has been retried. /// Set the number of times this operation has been retried.
#[cfg(any(test, feature = "test-utils"))]
fn set_retries(&mut self, retries: u32); fn set_retries(&mut self, retries: u32);
} }
/// Utility fn to calculate the total estimated cost of an operation batch
pub fn total_estimated_cost(ops: &[Box<dyn PendingOperation>]) -> U256 {
ops.iter()
.fold(U256::zero(), |acc, op| match op.get_tx_cost_estimate() {
Some(cost_estimate) => acc.saturating_add(cost_estimate),
None => {
warn!(operation=?op, "No cost estimate available for operation, defaulting to 0");
acc
}
})
}
/// Calculate the gas used by an operation (either in a batch or single-submission), by looking at the total cost of the tx,
/// and the estimated cost of the operation compared to the sum of the estimates of all operations in the batch.
/// When using this for single-submission rather than a batch,
/// the `tx_estimated_cost` should be the same as the `tx_estimated_cost`
pub fn gas_used_by_operation(
tx_outcome: &TxOutcome,
tx_estimated_cost: U256,
operation_estimated_cost: U256,
) -> ChainResult<U256> {
let gas_used_by_tx = FixedPointNumber::try_from(tx_outcome.gas_used)?;
let operation_gas_estimate = FixedPointNumber::try_from(operation_estimated_cost)?;
let tx_gas_estimate = FixedPointNumber::try_from(tx_estimated_cost)?;
let gas_used_by_operation = (gas_used_by_tx * operation_gas_estimate)
.checked_div(&tx_gas_estimate)
.ok_or(eyre::eyre!("Division by zero"))?;
gas_used_by_operation.try_into()
}
impl Display for QueueOperation { impl Display for QueueOperation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
@ -138,6 +184,7 @@ impl Ord for QueueOperation {
} }
} }
/// Possible outcomes of performing an action on a pending operation (such as `prepare`, `submit` or `confirm`).
#[derive(Debug)] #[derive(Debug)]
pub enum PendingOperationResult { pub enum PendingOperationResult {
/// Promote to the next step /// Promote to the next step
@ -153,6 +200,7 @@ pub enum PendingOperationResult {
} }
/// create a `op_try!` macro for the `on_retry` handler. /// create a `op_try!` macro for the `on_retry` handler.
#[macro_export]
macro_rules! make_op_try { macro_rules! make_op_try {
($on_retry:expr) => { ($on_retry:expr) => {
/// Handle a result and either return early with retry or a critical failure on /// Handle a result and either return early with retry or a critical failure on
@ -181,5 +229,3 @@ macro_rules! make_op_try {
} }
}; };
} }
pub(super) use make_op_try;

@ -1,50 +0,0 @@
use derive_new::new;
use tokio::sync::broadcast::{Receiver, Sender};
/// Multi-producer, multi-consumer channel
pub struct MpmcChannel<T> {
sender: Sender<T>,
receiver: MpmcReceiver<T>,
}
impl<T: Clone> MpmcChannel<T> {
/// Creates a new `MpmcChannel` with the specified capacity.
///
/// # Arguments
///
/// * `capacity` - The maximum number of messages that can be buffered in the channel.
pub fn new(capacity: usize) -> Self {
let (sender, receiver) = tokio::sync::broadcast::channel(capacity);
Self {
sender: sender.clone(),
receiver: MpmcReceiver::new(sender, receiver),
}
}
/// Returns a clone of the sender end of the channel.
pub fn sender(&self) -> Sender<T> {
self.sender.clone()
}
/// Returns a clone of the receiver end of the channel.
pub fn receiver(&self) -> MpmcReceiver<T> {
self.receiver.clone()
}
}
/// Clonable receiving end of a multi-producer, multi-consumer channel
#[derive(Debug, new)]
pub struct MpmcReceiver<T> {
sender: Sender<T>,
/// The receiving end of the channel.
pub receiver: Receiver<T>,
}
impl<T> Clone for MpmcReceiver<T> {
fn clone(&self) -> Self {
Self {
sender: self.sender.clone(),
receiver: self.sender.subscribe(),
}
}
}

@ -8,8 +8,6 @@ pub use self::primitive_types::*;
pub use ::primitive_types as ethers_core_types; pub use ::primitive_types as ethers_core_types;
pub use announcement::*; pub use announcement::*;
pub use chain_data::*; pub use chain_data::*;
#[cfg(feature = "async")]
pub use channel::*;
pub use checkpoint::*; pub use checkpoint::*;
pub use indexing::*; pub use indexing::*;
pub use log_metadata::*; pub use log_metadata::*;
@ -21,8 +19,6 @@ use crate::{Decode, Encode, HyperlaneProtocolError};
mod announcement; mod announcement;
mod chain_data; mod chain_data;
#[cfg(feature = "async")]
mod channel;
mod checkpoint; mod checkpoint;
mod indexing; mod indexing;
mod log_metadata; mod log_metadata;

@ -3,11 +3,15 @@
#![allow(clippy::assign_op_pattern)] #![allow(clippy::assign_op_pattern)]
#![allow(clippy::reversed_empty_ranges)] #![allow(clippy::reversed_empty_ranges)]
use std::{ops::Mul, str::FromStr}; use std::{
ops::{Div, Mul},
str::FromStr,
};
use bigdecimal::{BigDecimal, RoundingMode}; use bigdecimal::{BigDecimal, RoundingMode};
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{BorshDeserialize, BorshSerialize};
use fixed_hash::impl_fixed_hash_conversions; use fixed_hash::impl_fixed_hash_conversions;
use num::CheckedDiv;
use num_traits::Zero; use num_traits::Zero;
use uint::construct_uint; use uint::construct_uint;
@ -421,6 +425,27 @@ where
} }
} }
impl<T> Div<T> for FixedPointNumber
where
T: Into<FixedPointNumber>,
{
type Output = FixedPointNumber;
fn div(self, rhs: T) -> Self::Output {
let rhs = rhs.into();
Self(self.0 / rhs.0)
}
}
impl CheckedDiv for FixedPointNumber {
fn checked_div(&self, v: &Self) -> Option<Self> {
if v.0.is_zero() {
return None;
}
Some(Self(self.0.clone() / v.0.clone()))
}
}
impl FromStr for FixedPointNumber { impl FromStr for FixedPointNumber {
type Err = ChainCommunicationError; type Err = ChainCommunicationError;

@ -118,7 +118,7 @@ impl BacktraceFrameFmt<'_, '_, '_> {
symbol.name(), symbol.name(),
// TODO: this isn't great that we don't end up printing anything // TODO: this isn't great that we don't end up printing anything
// with non-utf8 filenames. Thankfully almost everything is utf8 so // with non-utf8 filenames. Thankfully almost everything is utf8 so
// this shouldn't be too too bad. // this shouldn't be too bad.
symbol symbol
.filename() .filename()
.and_then(|p| Some(BytesOrWideString::Bytes(p.to_str()?.as_bytes()))), .and_then(|p| Some(BytesOrWideString::Bytes(p.to_str()?.as_bytes()))),

@ -28,11 +28,13 @@ ethers-contract.workspace = true
tokio.workspace = true tokio.workspace = true
maplit.workspace = true maplit.workspace = true
nix = { workspace = true, features = ["signal"], default-features = false } nix = { workspace = true, features = ["signal"], default-features = false }
once_cell.workspace = true
tempfile.workspace = true tempfile.workspace = true
ureq = { workspace = true, default-features = false } ureq = { workspace = true, default-features = false }
which.workspace = true which.workspace = true
macro_rules_attribute.workspace = true macro_rules_attribute.workspace = true
regex.workspace = true regex.workspace = true
relayer = { path = "../../agents/relayer"}
hyperlane-cosmwasm-interface.workspace = true hyperlane-cosmwasm-interface.workspace = true
cosmwasm-schema.workspace = true cosmwasm-schema.workspace = true

@ -6,6 +6,7 @@ pub struct Config {
pub ci_mode: bool, pub ci_mode: bool,
pub ci_mode_timeout: u64, pub ci_mode_timeout: u64,
pub kathy_messages: u64, pub kathy_messages: u64,
pub sealevel_enabled: bool,
// TODO: Include count of sealevel messages in a field separate from `kathy_messages`? // TODO: Include count of sealevel messages in a field separate from `kathy_messages`?
} }
@ -26,6 +27,9 @@ impl Config {
.map(|r| r.parse::<u64>().unwrap()); .map(|r| r.parse::<u64>().unwrap());
r.unwrap_or(16) r.unwrap_or(16)
}, },
sealevel_enabled: env::var("SEALEVEL_ENABLED")
.map(|k| k.parse::<bool>().unwrap())
.unwrap_or(true),
}) })
} }
} }

@ -152,7 +152,7 @@ impl OsmosisCLI {
.arg("grpc.address", &endpoint.grpc_addr) // default is 0.0.0.0:9090 .arg("grpc.address", &endpoint.grpc_addr) // default is 0.0.0.0:9090
.arg("rpc.pprof_laddr", pprof_addr) // default is localhost:6060 .arg("rpc.pprof_laddr", pprof_addr) // default is localhost:6060
.arg("log_level", "panic") .arg("log_level", "panic")
.spawn("COSMOS"); .spawn("COSMOS", None);
endpoint.wait_for_node(); endpoint.wait_for_node();

@ -271,7 +271,7 @@ fn launch_cosmos_validator(
.hyp_env("SIGNER_SIGNER_TYPE", "hexKey") .hyp_env("SIGNER_SIGNER_TYPE", "hexKey")
.hyp_env("SIGNER_KEY", agent_config.signer.key) .hyp_env("SIGNER_KEY", agent_config.signer.key)
.hyp_env("TRACING_LEVEL", if debug { "debug" } else { "info" }) .hyp_env("TRACING_LEVEL", if debug { "debug" } else { "info" })
.spawn("VAL"); .spawn("VAL", None);
validator validator
} }
@ -299,7 +299,7 @@ fn launch_cosmos_relayer(
.hyp_env("TRACING_LEVEL", if debug { "debug" } else { "info" }) .hyp_env("TRACING_LEVEL", if debug { "debug" } else { "info" })
.hyp_env("GASPAYMENTENFORCEMENT", "[{\"type\": \"none\"}]") .hyp_env("GASPAYMENTENFORCEMENT", "[{\"type\": \"none\"}]")
.hyp_env("METRICSPORT", metrics.to_string()) .hyp_env("METRICSPORT", metrics.to_string())
.spawn("RLY"); .spawn("RLY", None);
relayer relayer
} }

@ -36,7 +36,7 @@ pub fn start_anvil(config: Arc<Config>) -> AgentHandles {
} }
log!("Launching anvil..."); log!("Launching anvil...");
let anvil_args = Program::new("anvil").flag("silent").filter_logs(|_| false); // for now do not keep any of the anvil logs let anvil_args = Program::new("anvil").flag("silent").filter_logs(|_| false); // for now do not keep any of the anvil logs
let anvil = anvil_args.spawn("ETH"); let anvil = anvil_args.spawn("ETH", None);
sleep(Duration::from_secs(10)); sleep(Duration::from_secs(10));

@ -1,14 +1,15 @@
// use std::path::Path; use std::fs::File;
use std::path::Path; use std::path::Path;
use crate::config::Config; use crate::config::Config;
use crate::metrics::agent_balance_sum; use crate::metrics::agent_balance_sum;
use crate::utils::get_matching_lines;
use maplit::hashmap; use maplit::hashmap;
use relayer::GAS_EXPENDITURE_LOG_MESSAGE;
use crate::logging::log; use crate::logging::log;
use crate::solana::solana_termination_invariants_met; use crate::solana::solana_termination_invariants_met;
use crate::{fetch_metric, ZERO_MERKLE_INSERTION_KATHY_MESSAGES}; use crate::{fetch_metric, AGENT_LOGGING_DIR, ZERO_MERKLE_INSERTION_KATHY_MESSAGES};
// This number should be even, so the messages can be split into two equal halves // This number should be even, so the messages can be split into two equal halves
// sent before and after the relayer spins up, to avoid rounding errors. // sent before and after the relayer spins up, to avoid rounding errors.
@ -19,11 +20,16 @@ pub const SOL_MESSAGES_EXPECTED: u32 = 20;
pub fn termination_invariants_met( pub fn termination_invariants_met(
config: &Config, config: &Config,
starting_relayer_balance: f64, starting_relayer_balance: f64,
solana_cli_tools_path: &Path, solana_cli_tools_path: Option<&Path>,
solana_config_path: &Path, solana_config_path: Option<&Path>,
) -> eyre::Result<bool> { ) -> eyre::Result<bool> {
let eth_messages_expected = (config.kathy_messages / 2) as u32 * 2; let eth_messages_expected = (config.kathy_messages / 2) as u32 * 2;
let total_messages_expected = eth_messages_expected + SOL_MESSAGES_EXPECTED; let sol_messages_expected = if config.sealevel_enabled {
SOL_MESSAGES_EXPECTED
} else {
0
};
let total_messages_expected = eth_messages_expected + sol_messages_expected;
let lengths = fetch_metric("9092", "hyperlane_submitter_queue_length", &hashmap! {})?; let lengths = fetch_metric("9092", "hyperlane_submitter_queue_length", &hashmap! {})?;
assert!(!lengths.is_empty(), "Could not find queue length metric"); assert!(!lengths.is_empty(), "Could not find queue length metric");
@ -55,6 +61,19 @@ pub fn termination_invariants_met(
.iter() .iter()
.sum::<u32>(); .sum::<u32>();
let log_file_path = AGENT_LOGGING_DIR.join("RLY-output.log");
let relayer_logfile = File::open(log_file_path)?;
let gas_expenditure_log_count =
get_matching_lines(&relayer_logfile, GAS_EXPENDITURE_LOG_MESSAGE)
.unwrap()
.len();
// Zero insertion messages don't reach `submit` stage where gas is spent, so we only expect these logs for the other messages.
assert_eq!(
gas_expenditure_log_count as u32, total_messages_expected,
"Didn't record gas payment for all delivered messages"
);
let gas_payment_sealevel_events_count = fetch_metric( let gas_payment_sealevel_events_count = fetch_metric(
"9092", "9092",
"hyperlane_contract_sync_stored_events", "hyperlane_contract_sync_stored_events",
@ -76,10 +95,14 @@ pub fn termination_invariants_met(
return Ok(false); return Ok(false);
} }
if let Some((solana_cli_tools_path, solana_config_path)) =
solana_cli_tools_path.zip(solana_config_path)
{
if !solana_termination_invariants_met(solana_cli_tools_path, solana_config_path) { if !solana_termination_invariants_met(solana_cli_tools_path, solana_config_path) {
log!("Solana termination invariants not met"); log!("Solana termination invariants not met");
return Ok(false); return Ok(false);
} }
}
let dispatched_messages_scraped = fetch_metric( let dispatched_messages_scraped = fetch_metric(
"9093", "9093",

@ -11,12 +11,17 @@
//! the end conditions are met, the test is a failure. Defaults to 10 min. //! the end conditions are met, the test is a failure. Defaults to 10 min.
//! - `E2E_KATHY_MESSAGES`: Number of kathy messages to dispatch. Defaults to 16 if CI mode is enabled. //! - `E2E_KATHY_MESSAGES`: Number of kathy messages to dispatch. Defaults to 16 if CI mode is enabled.
//! else false. //! else false.
//! - `SEALEVEL_ENABLED`: true/false, enables sealevel testing. Defaults to true.
use std::{ use std::{
fs, collections::HashMap,
fs::{self, File},
path::Path, path::Path,
process::{Child, ExitCode}, process::{Child, ExitCode},
sync::atomic::{AtomicBool, Ordering}, sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
thread::sleep, thread::sleep,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@ -24,6 +29,7 @@ use std::{
use ethers_contract::MULTICALL_ADDRESS; use ethers_contract::MULTICALL_ADDRESS;
use logging::log; use logging::log;
pub use metrics::fetch_metric; pub use metrics::fetch_metric;
use once_cell::sync::Lazy;
use program::Program; use program::Program;
use tempfile::tempdir; use tempfile::tempdir;
@ -46,6 +52,12 @@ mod program;
mod solana; mod solana;
mod utils; mod utils;
pub static AGENT_LOGGING_DIR: Lazy<&Path> = Lazy::new(|| {
let dir = Path::new("/tmp/test_logs");
fs::create_dir_all(dir).unwrap();
dir
});
/// These private keys are from hardhat/anvil's testing accounts. /// These private keys are from hardhat/anvil's testing accounts.
const RELAYER_KEYS: &[&str] = &[ const RELAYER_KEYS: &[&str] = &[
// test1 // test1
@ -61,17 +73,18 @@ const RELAYER_KEYS: &[&str] = &[
]; ];
/// These private keys are from hardhat/anvil's testing accounts. /// These private keys are from hardhat/anvil's testing accounts.
/// These must be consistent with the ISM config for the test. /// These must be consistent with the ISM config for the test.
const VALIDATOR_KEYS: &[&str] = &[ const ETH_VALIDATOR_KEYS: &[&str] = &[
// eth // eth
"0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a",
"0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba", "0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba",
"0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e", "0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e",
];
const SEALEVEL_VALIDATOR_KEYS: &[&str] = &[
// sealevel // sealevel
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
]; ];
const VALIDATOR_ORIGIN_CHAINS: &[&str] = &["test1", "test2", "test3", "sealeveltest1"];
const AGENT_BIN_PATH: &str = "target/debug"; const AGENT_BIN_PATH: &str = "target/debug";
const INFRA_PATH: &str = "../typescript/infra"; const INFRA_PATH: &str = "../typescript/infra";
const MONOREPO_ROOT_PATH: &str = "../"; const MONOREPO_ROOT_PATH: &str = "../";
@ -87,14 +100,15 @@ static SHUTDOWN: AtomicBool = AtomicBool::new(false);
/// cleanup purposes at this time. /// cleanup purposes at this time.
#[derive(Default)] #[derive(Default)]
struct State { struct State {
agents: Vec<(String, Child)>, #[allow(clippy::type_complexity)]
agents: HashMap<String, (Child, Option<Arc<Mutex<File>>>)>,
watchers: Vec<Box<dyn TaskHandle<Output = ()>>>, watchers: Vec<Box<dyn TaskHandle<Output = ()>>>,
data: Vec<Box<dyn ArbitraryData>>, data: Vec<Box<dyn ArbitraryData>>,
} }
impl State { impl State {
fn push_agent(&mut self, handles: AgentHandles) { fn push_agent(&mut self, handles: AgentHandles) {
self.agents.push((handles.0, handles.1)); self.agents.insert(handles.0, (handles.1, handles.5));
self.watchers.push(handles.2); self.watchers.push(handles.2);
self.watchers.push(handles.3); self.watchers.push(handles.3);
self.data.push(handles.4); self.data.push(handles.4);
@ -105,9 +119,7 @@ impl Drop for State {
fn drop(&mut self) { fn drop(&mut self) {
SHUTDOWN.store(true, Ordering::Relaxed); SHUTDOWN.store(true, Ordering::Relaxed);
log!("Signaling children to stop..."); log!("Signaling children to stop...");
// stop children in reverse order for (name, (mut agent, _)) in self.agents.drain() {
self.agents.reverse();
for (name, mut agent) in self.agents.drain(..) {
log!("Stopping child {}", name); log!("Stopping child {}", name);
stop_child(&mut agent); stop_child(&mut agent);
} }
@ -122,6 +134,7 @@ impl Drop for State {
drop(data) drop(data)
} }
fs::remove_dir_all(SOLANA_CHECKPOINT_LOCATION).unwrap_or_default(); fs::remove_dir_all(SOLANA_CHECKPOINT_LOCATION).unwrap_or_default();
fs::remove_dir_all::<&Path>(AGENT_LOGGING_DIR.as_ref()).unwrap_or_default();
} }
} }
@ -133,20 +146,27 @@ fn main() -> ExitCode {
}) })
.unwrap(); .unwrap();
assert_eq!(VALIDATOR_ORIGIN_CHAINS.len(), VALIDATOR_KEYS.len());
const VALIDATOR_COUNT: usize = VALIDATOR_KEYS.len();
let config = Config::load(); let config = Config::load();
let mut validator_origin_chains = ["test1", "test2", "test3"].to_vec();
let solana_checkpoint_path = Path::new(SOLANA_CHECKPOINT_LOCATION); let mut validator_keys = ETH_VALIDATOR_KEYS.to_vec();
fs::remove_dir_all(solana_checkpoint_path).unwrap_or_default(); let mut validator_count: usize = validator_keys.len();
let checkpoints_dirs: Vec<DynPath> = (0..VALIDATOR_COUNT - 1) let mut checkpoints_dirs: Vec<DynPath> = (0..validator_count)
.map(|_| Box::new(tempdir().unwrap()) as DynPath) .map(|_| Box::new(tempdir().unwrap()) as DynPath)
.chain([Box::new(solana_checkpoint_path) as DynPath])
.collect(); .collect();
if config.sealevel_enabled {
validator_origin_chains.push("sealeveltest1");
let mut sealevel_keys = SEALEVEL_VALIDATOR_KEYS.to_vec();
validator_keys.append(&mut sealevel_keys);
let solana_checkpoint_path = Path::new(SOLANA_CHECKPOINT_LOCATION);
fs::remove_dir_all(solana_checkpoint_path).unwrap_or_default();
checkpoints_dirs.push(Box::new(solana_checkpoint_path) as DynPath);
validator_count += 1;
}
assert_eq!(validator_origin_chains.len(), validator_keys.len());
let rocks_db_dir = tempdir().unwrap(); let rocks_db_dir = tempdir().unwrap();
let relayer_db = concat_path(&rocks_db_dir, "relayer"); let relayer_db = concat_path(&rocks_db_dir, "relayer");
let validator_dbs = (0..VALIDATOR_COUNT) let validator_dbs = (0..validator_count)
.map(|i| concat_path(&rocks_db_dir, format!("validator{i}"))) .map(|i| concat_path(&rocks_db_dir, format!("validator{i}")))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -200,15 +220,6 @@ fn main() -> ExitCode {
r#"[{ r#"[{
"type": "minimum", "type": "minimum",
"payment": "1", "payment": "1",
"matchingList": [
{
"originDomain": ["13375","13376"],
"destinationDomain": ["13375","13376"]
}
]
},
{
"type": "none"
}]"#, }]"#,
) )
.arg( .arg(
@ -216,11 +227,15 @@ fn main() -> ExitCode {
"http://127.0.0.1:8545,http://127.0.0.1:8545,http://127.0.0.1:8545", "http://127.0.0.1:8545,http://127.0.0.1:8545,http://127.0.0.1:8545",
) )
// default is used for TEST3 // default is used for TEST3
.arg("defaultSigner.key", RELAYER_KEYS[2]) .arg("defaultSigner.key", RELAYER_KEYS[2]);
.arg( let relayer_env = if config.sealevel_enabled {
relayer_env.arg(
"relayChains", "relayChains",
"test1,test2,test3,sealeveltest1,sealeveltest2", "test1,test2,test3,sealeveltest1,sealeveltest2",
); )
} else {
relayer_env.arg("relayChains", "test1,test2,test3")
};
let base_validator_env = common_agent_env let base_validator_env = common_agent_env
.clone() .clone()
@ -242,14 +257,14 @@ fn main() -> ExitCode {
.hyp_env("INTERVAL", "5") .hyp_env("INTERVAL", "5")
.hyp_env("CHECKPOINTSYNCER_TYPE", "localStorage"); .hyp_env("CHECKPOINTSYNCER_TYPE", "localStorage");
let validator_envs = (0..VALIDATOR_COUNT) let validator_envs = (0..validator_count)
.map(|i| { .map(|i| {
base_validator_env base_validator_env
.clone() .clone()
.hyp_env("METRICSPORT", (9094 + i).to_string()) .hyp_env("METRICSPORT", (9094 + i).to_string())
.hyp_env("DB", validator_dbs[i].to_str().unwrap()) .hyp_env("DB", validator_dbs[i].to_str().unwrap())
.hyp_env("ORIGINCHAINNAME", VALIDATOR_ORIGIN_CHAINS[i]) .hyp_env("ORIGINCHAINNAME", validator_origin_chains[i])
.hyp_env("VALIDATOR_KEY", VALIDATOR_KEYS[i]) .hyp_env("VALIDATOR_KEY", validator_keys[i])
.hyp_env( .hyp_env(
"CHECKPOINTSYNCER_PATH", "CHECKPOINTSYNCER_PATH",
(*checkpoints_dirs[i]).as_ref().to_str().unwrap(), (*checkpoints_dirs[i]).as_ref().to_str().unwrap(),
@ -283,7 +298,7 @@ fn main() -> ExitCode {
.join(", ") .join(", ")
); );
log!("Relayer DB in {}", relayer_db.display()); log!("Relayer DB in {}", relayer_db.display());
(0..VALIDATOR_COUNT).for_each(|i| { (0..validator_count).for_each(|i| {
log!("Validator {} DB in {}", i + 1, validator_dbs[i].display()); log!("Validator {} DB in {}", i + 1, validator_dbs[i].display());
}); });
@ -291,9 +306,14 @@ fn main() -> ExitCode {
// Ready to run... // Ready to run...
// //
let solana_paths = if config.sealevel_enabled {
let (solana_path, solana_path_tempdir) = install_solana_cli_tools().join(); let (solana_path, solana_path_tempdir) = install_solana_cli_tools().join();
state.data.push(Box::new(solana_path_tempdir)); state.data.push(Box::new(solana_path_tempdir));
let solana_program_builder = build_solana_programs(solana_path.clone()); let solana_program_builder = build_solana_programs(solana_path.clone());
Some((solana_program_builder.join(), solana_path))
} else {
None
};
// this task takes a long time in the CI so run it in parallel // this task takes a long time in the CI so run it in parallel
log!("Building rust..."); log!("Building rust...");
@ -303,15 +323,18 @@ fn main() -> ExitCode {
.arg("bin", "relayer") .arg("bin", "relayer")
.arg("bin", "validator") .arg("bin", "validator")
.arg("bin", "scraper") .arg("bin", "scraper")
.arg("bin", "init-db") .arg("bin", "init-db");
.arg("bin", "hyperlane-sealevel-client") let build_rust = if config.sealevel_enabled {
build_rust.arg("bin", "hyperlane-sealevel-client")
} else {
build_rust
};
let build_rust = build_rust
.filter_logs(|l| !l.contains("workspace-inheritance")) .filter_logs(|l| !l.contains("workspace-inheritance"))
.run(); .run();
let start_anvil = start_anvil(config.clone()); let start_anvil = start_anvil(config.clone());
let solana_program_path = solana_program_builder.join();
log!("Running postgres db..."); log!("Running postgres db...");
let postgres = Program::new("docker") let postgres = Program::new("docker")
.cmd("run") .cmd("run")
@ -320,12 +343,14 @@ fn main() -> ExitCode {
.arg("env", "POSTGRES_PASSWORD=47221c18c610") .arg("env", "POSTGRES_PASSWORD=47221c18c610")
.arg("publish", "5432:5432") .arg("publish", "5432:5432")
.cmd("postgres:14") .cmd("postgres:14")
.spawn("SQL"); .spawn("SQL", None);
state.push_agent(postgres); state.push_agent(postgres);
build_rust.join(); build_rust.join();
let solana_ledger_dir = tempdir().unwrap(); let solana_ledger_dir = tempdir().unwrap();
let solana_config_path = if let Some((solana_program_path, solana_path)) = solana_paths.clone()
{
let start_solana_validator = start_solana_test_validator( let start_solana_validator = start_solana_test_validator(
solana_path.clone(), solana_path.clone(),
solana_program_path, solana_program_path,
@ -334,10 +359,15 @@ fn main() -> ExitCode {
let (solana_config_path, solana_validator) = start_solana_validator.join(); let (solana_config_path, solana_validator) = start_solana_validator.join();
state.push_agent(solana_validator); state.push_agent(solana_validator);
Some(solana_config_path)
} else {
None
};
state.push_agent(start_anvil.join()); state.push_agent(start_anvil.join());
// spawn 1st validator before any messages have been sent to test empty mailbox // spawn 1st validator before any messages have been sent to test empty mailbox
state.push_agent(validator_envs.first().unwrap().clone().spawn("VL1")); state.push_agent(validator_envs.first().unwrap().clone().spawn("VL1", None));
sleep(Duration::from_secs(5)); sleep(Duration::from_secs(5));
@ -345,7 +375,7 @@ fn main() -> ExitCode {
Program::new(concat_path(AGENT_BIN_PATH, "init-db")) Program::new(concat_path(AGENT_BIN_PATH, "init-db"))
.run() .run()
.join(); .join();
state.push_agent(scraper_env.spawn("SCR")); state.push_agent(scraper_env.spawn("SCR", None));
// Send half the kathy messages before starting the rest of the agents // Send half the kathy messages before starting the rest of the agents
let kathy_env_single_insertion = Program::new("yarn") let kathy_env_single_insertion = Program::new("yarn")
@ -378,22 +408,35 @@ fn main() -> ExitCode {
.arg("required-hook", "merkleTreeHook"); .arg("required-hook", "merkleTreeHook");
kathy_env_double_insertion.clone().run().join(); kathy_env_double_insertion.clone().run().join();
if let Some((solana_config_path, (_, solana_path))) =
solana_config_path.clone().zip(solana_paths.clone())
{
// Send some sealevel messages before spinning up the agents, to test the backward indexing cursor // Send some sealevel messages before spinning up the agents, to test the backward indexing cursor
for _i in 0..(SOL_MESSAGES_EXPECTED / 2) { for _i in 0..(SOL_MESSAGES_EXPECTED / 2) {
initiate_solana_hyperlane_transfer(solana_path.clone(), solana_config_path.clone()).join(); initiate_solana_hyperlane_transfer(solana_path.clone(), solana_config_path.clone())
.join();
}
} }
// spawn the rest of the validators // spawn the rest of the validators
for (i, validator_env) in validator_envs.into_iter().enumerate().skip(1) { for (i, validator_env) in validator_envs.into_iter().enumerate().skip(1) {
let validator = validator_env.spawn(make_static(format!("VL{}", 1 + i))); let validator = validator_env.spawn(
make_static(format!("VL{}", 1 + i)),
Some(AGENT_LOGGING_DIR.as_ref()),
);
state.push_agent(validator); state.push_agent(validator);
} }
state.push_agent(relayer_env.spawn("RLY")); state.push_agent(relayer_env.spawn("RLY", Some(&AGENT_LOGGING_DIR)));
// Send some sealevel messages after spinning up the relayer, to test the forward indexing cursor if let Some((solana_config_path, (_, solana_path))) =
solana_config_path.clone().zip(solana_paths.clone())
{
// Send some sealevel messages before spinning up the agents, to test the backward indexing cursor
for _i in 0..(SOL_MESSAGES_EXPECTED / 2) { for _i in 0..(SOL_MESSAGES_EXPECTED / 2) {
initiate_solana_hyperlane_transfer(solana_path.clone(), solana_config_path.clone()).join(); initiate_solana_hyperlane_transfer(solana_path.clone(), solana_config_path.clone())
.join();
}
} }
log!("Setup complete! Agents running in background..."); log!("Setup complete! Agents running in background...");
@ -402,7 +445,11 @@ fn main() -> ExitCode {
// Send half the kathy messages after the relayer comes up // Send half the kathy messages after the relayer comes up
kathy_env_double_insertion.clone().run().join(); kathy_env_double_insertion.clone().run().join();
kathy_env_zero_insertion.clone().run().join(); kathy_env_zero_insertion.clone().run().join();
state.push_agent(kathy_env_single_insertion.flag("mineforever").spawn("KTY")); state.push_agent(
kathy_env_single_insertion
.flag("mineforever")
.spawn("KTY", None),
);
let loop_start = Instant::now(); let loop_start = Instant::now();
// give things a chance to fully start. // give things a chance to fully start.
@ -412,12 +459,14 @@ fn main() -> ExitCode {
while !SHUTDOWN.load(Ordering::Relaxed) { while !SHUTDOWN.load(Ordering::Relaxed) {
if config.ci_mode { if config.ci_mode {
// for CI we have to look for the end condition. // for CI we have to look for the end condition.
// if termination_invariants_met(&config, starting_relayer_balance)
if termination_invariants_met( if termination_invariants_met(
&config, &config,
starting_relayer_balance, starting_relayer_balance,
&solana_path, solana_paths
&solana_config_path, .clone()
.map(|(_, solana_path)| solana_path)
.as_deref(),
solana_config_path.as_deref(),
) )
.unwrap_or(false) .unwrap_or(false)
{ {
@ -432,7 +481,7 @@ fn main() -> ExitCode {
} }
// verify long-running tasks are still running // verify long-running tasks are still running
for (name, child) in state.agents.iter_mut() { for (name, (child, _)) in state.agents.iter_mut() {
if let Some(status) = child.try_wait().unwrap() { if let Some(status) = child.try_wait().unwrap() {
if !status.success() { if !status.success() {
log!( log!(

@ -2,14 +2,14 @@ use std::{
collections::BTreeMap, collections::BTreeMap,
ffi::OsStr, ffi::OsStr,
fmt::{Debug, Display, Formatter}, fmt::{Debug, Display, Formatter},
io::{BufRead, BufReader, Read}, fs::{File, OpenOptions},
io::{BufRead, BufReader, Read, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{Command, Stdio}, process::{Command, Stdio},
sync::{ sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
mpsc, mpsc::{self, Sender},
mpsc::Sender, Arc, Mutex,
Arc,
}, },
thread::{sleep, spawn}, thread::{sleep, spawn},
time::Duration, time::Duration,
@ -240,8 +240,18 @@ impl Program {
}) })
} }
pub fn spawn(self, log_prefix: &'static str) -> AgentHandles { pub fn spawn(self, log_prefix: &'static str, logs_dir: Option<&Path>) -> AgentHandles {
let mut command = self.create_command(); let mut command = self.create_command();
let log_file = logs_dir.map(|logs_dir| {
let log_file_name = format!("{}-output.log", log_prefix);
let log_file_path = logs_dir.join(log_file_name);
let log_file = OpenOptions::new()
.append(true)
.create(true)
.open(log_file_path)
.expect("Failed to create a log file");
Arc::new(Mutex::new(log_file))
});
command.stdout(Stdio::piped()).stderr(Stdio::piped()); command.stdout(Stdio::piped()).stderr(Stdio::piped());
log!("Spawning {}...", &self); log!("Spawning {}...", &self);
@ -250,17 +260,35 @@ impl Program {
.unwrap_or_else(|e| panic!("Failed to start {:?} with error: {e}", &self)); .unwrap_or_else(|e| panic!("Failed to start {:?} with error: {e}", &self));
let child_stdout = child.stdout.take().unwrap(); let child_stdout = child.stdout.take().unwrap();
let filter = self.get_filter(); let filter = self.get_filter();
let stdout = let cloned_log_file = log_file.clone();
spawn(move || prefix_log(child_stdout, log_prefix, &RUN_LOG_WATCHERS, filter, None)); let stdout = spawn(move || {
prefix_log(
child_stdout,
log_prefix,
&RUN_LOG_WATCHERS,
filter,
cloned_log_file,
None,
)
});
let child_stderr = child.stderr.take().unwrap(); let child_stderr = child.stderr.take().unwrap();
let stderr = let stderr = spawn(move || {
spawn(move || prefix_log(child_stderr, log_prefix, &RUN_LOG_WATCHERS, filter, None)); prefix_log(
child_stderr,
log_prefix,
&RUN_LOG_WATCHERS,
filter,
None,
None,
)
});
( (
log_prefix.to_owned(), log_prefix.to_owned(),
child, child,
Box::new(SimpleTaskHandle(stdout)), Box::new(SimpleTaskHandle(stdout)),
Box::new(SimpleTaskHandle(stderr)), Box::new(SimpleTaskHandle(stderr)),
self.get_memory(), self.get_memory(),
log_file.clone(),
) )
} }
@ -281,13 +309,13 @@ impl Program {
let stdout = child.stdout.take().unwrap(); let stdout = child.stdout.take().unwrap();
let name = self.get_bin_name(); let name = self.get_bin_name();
let running = running.clone(); let running = running.clone();
spawn(move || prefix_log(stdout, &name, &running, filter, stdout_ch_tx)) spawn(move || prefix_log(stdout, &name, &running, filter, None, stdout_ch_tx))
}; };
let stderr = { let stderr = {
let stderr = child.stderr.take().unwrap(); let stderr = child.stderr.take().unwrap();
let name = self.get_bin_name(); let name = self.get_bin_name();
let running = running.clone(); let running = running.clone();
spawn(move || prefix_log(stderr, &name, &running, filter, None)) spawn(move || prefix_log(stderr, &name, &running, filter, None, None))
}; };
let status = loop { let status = loop {
@ -321,6 +349,7 @@ fn prefix_log(
prefix: &str, prefix: &str,
run_log_watcher: &AtomicBool, run_log_watcher: &AtomicBool,
filter: Option<LogFilter>, filter: Option<LogFilter>,
file: Option<Arc<Mutex<File>>>,
channel: Option<Sender<String>>, channel: Option<Sender<String>>,
) { ) {
let mut reader = BufReader::new(output).lines(); let mut reader = BufReader::new(output).lines();
@ -340,6 +369,10 @@ fn prefix_log(
} }
} }
println!("<{prefix}> {line}"); println!("<{prefix}> {line}");
if let Some(file) = &file {
let mut writer = file.lock().expect("Failed to acquire lock for log file");
writeln!(writer, "{}", line).unwrap_or(());
}
if let Some(channel) = &channel { if let Some(channel) = &channel {
// ignore send errors // ignore send errors
channel.send(line).unwrap_or(()); channel.send(line).unwrap_or(());

@ -202,7 +202,7 @@ pub fn start_solana_test_validator(
concat_path(&solana_programs_path, lib).to_str().unwrap(), concat_path(&solana_programs_path, lib).to_str().unwrap(),
); );
} }
let validator = args.spawn("SOL"); let validator = args.spawn("SOL", None);
sleep(Duration::from_secs(5)); sleep(Duration::from_secs(5));
log!("Deploying the hyperlane programs to solana"); log!("Deploying the hyperlane programs to solana");

@ -1,5 +1,8 @@
use std::fs::File;
use std::io::{self, BufRead};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Child; use std::process::Child;
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle; use std::thread::JoinHandle;
use nix::libc::pid_t; use nix::libc::pid_t;
@ -54,6 +57,8 @@ pub type AgentHandles = (
Box<dyn TaskHandle<Output = ()>>, Box<dyn TaskHandle<Output = ()>>,
// data to drop once program exits // data to drop once program exits
Box<dyn ArbitraryData>, Box<dyn ArbitraryData>,
// file with stdout logs
Option<Arc<Mutex<File>>>,
); );
pub type LogFilter = fn(&str) -> bool; pub type LogFilter = fn(&str) -> bool;
@ -112,3 +117,16 @@ pub fn stop_child(child: &mut Child) {
} }
}; };
} }
pub fn get_matching_lines(file: &File, search_string: &str) -> io::Result<Vec<String>> {
let reader = io::BufReader::new(file);
// Read lines and collect those that contain the search string
let matching_lines: Vec<String> = reader
.lines()
.map_while(Result::ok)
.filter(|line| line.contains(search_string))
.collect();
Ok(matching_lines)
}

@ -1,5 +1,17 @@
# @hyperlane-xyz/core # @hyperlane-xyz/core
## 3.13.0
### Minor Changes
- babe816f8: Support xERC20 and xERC20 Lockbox in SDK and CLI
- b440d98be: Added support for registering/deregistering from the Hyperlane AVS
### Patch Changes
- Updated dependencies [0cf692e73]
- @hyperlane-xyz/utils@3.13.0
## 3.12.0 ## 3.12.0
### Patch Changes ### Patch Changes

@ -44,13 +44,14 @@ contract ECDSAStakeRegistry is
__ECDSAStakeRegistry_init(_serviceManager, _thresholdWeight, _quorum); __ECDSAStakeRegistry_init(_serviceManager, _thresholdWeight, _quorum);
} }
/// @notice Registers a new operator using a provided signature /// @notice Registers a new operator using a provided signature and signing key
/// @param _operatorSignature Contains the operator's signature, salt, and expiry /// @param _operatorSignature Contains the operator's signature, salt, and expiry
/// @param _signingKey The signing key to add to the operator's history
function registerOperatorWithSignature( function registerOperatorWithSignature(
address _operator, ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature,
ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature address _signingKey
) external { ) external {
_registerOperatorWithSig(_operator, _operatorSignature); _registerOperatorWithSig(msg.sender, _operatorSignature, _signingKey);
} }
/// @notice Deregisters an existing operator /// @notice Deregisters an existing operator
@ -58,6 +59,18 @@ contract ECDSAStakeRegistry is
_deregisterOperator(msg.sender); _deregisterOperator(msg.sender);
} }
/**
* @notice Updates the signing key for an operator
* @dev Only callable by the operator themselves
* @param _newSigningKey The new signing key to set for the operator
*/
function updateOperatorSigningKey(address _newSigningKey) external {
if (!_operatorRegistered[msg.sender]) {
revert OperatorNotRegistered();
}
_updateOperatorSigningKey(msg.sender, _newSigningKey);
}
/** /**
* @notice Updates the StakeRegistry's view of one or more operators' stakes adding a new entry in their history of stake checkpoints, * @notice Updates the StakeRegistry's view of one or more operators' stakes adding a new entry in their history of stake checkpoints,
* @dev Queries stakes from the Eigenlayer core DelegationManager contract * @dev Queries stakes from the Eigenlayer core DelegationManager contract
@ -107,18 +120,18 @@ contract ECDSAStakeRegistry is
/// @notice Verifies if the provided signature data is valid for the given data hash. /// @notice Verifies if the provided signature data is valid for the given data hash.
/// @param _dataHash The hash of the data that was signed. /// @param _dataHash The hash of the data that was signed.
/// @param _signatureData Encoded signature data consisting of an array of signers, an array of signatures, and a reference block number. /// @param _signatureData Encoded signature data consisting of an array of operators, an array of signatures, and a reference block number.
/// @return The function selector that indicates the signature is valid according to ERC1271 standard. /// @return The function selector that indicates the signature is valid according to ERC1271 standard.
function isValidSignature( function isValidSignature(
bytes32 _dataHash, bytes32 _dataHash,
bytes memory _signatureData bytes memory _signatureData
) external view returns (bytes4) { ) external view returns (bytes4) {
( (
address[] memory signers, address[] memory operators,
bytes[] memory signatures, bytes[] memory signatures,
uint32 referenceBlock uint32 referenceBlock
) = abi.decode(_signatureData, (address[], bytes[], uint32)); ) = abi.decode(_signatureData, (address[], bytes[], uint32));
_checkSignatures(_dataHash, signers, signatures, referenceBlock); _checkSignatures(_dataHash, operators, signatures, referenceBlock);
return IERC1271Upgradeable.isValidSignature.selector; return IERC1271Upgradeable.isValidSignature.selector;
} }
@ -128,6 +141,37 @@ contract ECDSAStakeRegistry is
return _quorum; return _quorum;
} }
/**
* @notice Retrieves the latest signing key for a given operator.
* @param _operator The address of the operator.
* @return The latest signing key of the operator.
*/
function getLastestOperatorSigningKey(
address _operator
) external view returns (address) {
return address(uint160(_operatorSigningKeyHistory[_operator].latest()));
}
/**
* @notice Retrieves the latest signing key for a given operator at a specific block number.
* @param _operator The address of the operator.
* @param _blockNumber The block number to get the operator's signing key.
* @return The signing key of the operator at the given block.
*/
function getOperatorSigningKeyAtBlock(
address _operator,
uint256 _blockNumber
) external view returns (address) {
return
address(
uint160(
_operatorSigningKeyHistory[_operator].getAtBlock(
_blockNumber
)
)
);
}
/// @notice Retrieves the last recorded weight for a given operator. /// @notice Retrieves the last recorded weight for a given operator.
/// @param _operator The address of the operator. /// @param _operator The address of the operator.
/// @return uint256 - The latest weight of the operator. /// @return uint256 - The latest weight of the operator.
@ -313,9 +357,11 @@ contract ECDSAStakeRegistry is
/// @dev registers an operator through a provided signature /// @dev registers an operator through a provided signature
/// @param _operatorSignature Contains the operator's signature, salt, and expiry /// @param _operatorSignature Contains the operator's signature, salt, and expiry
/// @param _signingKey The signing key to add to the operator's history
function _registerOperatorWithSig( function _registerOperatorWithSig(
address _operator, address _operator,
ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature,
address _signingKey
) internal virtual { ) internal virtual {
if (_operatorRegistered[_operator]) { if (_operatorRegistered[_operator]) {
revert OperatorAlreadyRegistered(); revert OperatorAlreadyRegistered();
@ -324,6 +370,7 @@ contract ECDSAStakeRegistry is
_operatorRegistered[_operator] = true; _operatorRegistered[_operator] = true;
int256 delta = _updateOperatorWeight(_operator); int256 delta = _updateOperatorWeight(_operator);
_updateTotalWeight(delta); _updateTotalWeight(delta);
_updateOperatorSigningKey(_operator, _signingKey);
IServiceManager(_serviceManager).registerOperatorToAVS( IServiceManager(_serviceManager).registerOperatorToAVS(
_operator, _operator,
_operatorSignature _operatorSignature
@ -331,6 +378,28 @@ contract ECDSAStakeRegistry is
emit OperatorRegistered(_operator, _serviceManager); emit OperatorRegistered(_operator, _serviceManager);
} }
/// @dev Internal function to update an operator's signing key
/// @param _operator The address of the operator to update the signing key for
/// @param _newSigningKey The new signing key to set for the operator
function _updateOperatorSigningKey(
address _operator,
address _newSigningKey
) internal {
address oldSigningKey = address(
uint160(_operatorSigningKeyHistory[_operator].latest())
);
if (_newSigningKey == oldSigningKey) {
return;
}
_operatorSigningKeyHistory[_operator].push(uint160(_newSigningKey));
emit SigningKeyUpdate(
_operator,
block.number,
_newSigningKey,
oldSigningKey
);
}
/// @notice Updates the weight of an operator and returns the previous and current weights. /// @notice Updates the weight of an operator and returns the previous and current weights.
/// @param _operator The address of the operator to update the weight of. /// @param _operator The address of the operator to update the weight of.
function _updateOperatorWeight( function _updateOperatorWeight(
@ -401,30 +470,33 @@ contract ECDSAStakeRegistry is
/** /**
* @notice Common logic to verify a batch of ECDSA signatures against a hash, using either last stake weight or at a specific block. * @notice Common logic to verify a batch of ECDSA signatures against a hash, using either last stake weight or at a specific block.
* @param _dataHash The hash of the data the signers endorsed. * @param _dataHash The hash of the data the signers endorsed.
* @param _signers A collection of addresses that endorsed the data hash. * @param _operators A collection of addresses that endorsed the data hash.
* @param _signatures A collection of signatures matching the signers. * @param _signatures A collection of signatures matching the signers.
* @param _referenceBlock The block number for evaluating stake weight; use max uint32 for latest weight. * @param _referenceBlock The block number for evaluating stake weight; use max uint32 for latest weight.
*/ */
function _checkSignatures( function _checkSignatures(
bytes32 _dataHash, bytes32 _dataHash,
address[] memory _signers, address[] memory _operators,
bytes[] memory _signatures, bytes[] memory _signatures,
uint32 _referenceBlock uint32 _referenceBlock
) internal view { ) internal view {
uint256 signersLength = _signers.length; uint256 signersLength = _operators.length;
address lastSigner; address currentOperator;
address lastOperator;
address signer;
uint256 signedWeight; uint256 signedWeight;
_validateSignaturesLength(signersLength, _signatures.length); _validateSignaturesLength(signersLength, _signatures.length);
for (uint256 i; i < signersLength; i++) { for (uint256 i; i < signersLength; i++) {
address currentSigner = _signers[i]; currentOperator = _operators[i];
signer = _getOperatorSigningKey(currentOperator, _referenceBlock);
_validateSortedSigners(lastSigner, currentSigner); _validateSortedSigners(lastOperator, currentOperator);
_validateSignature(currentSigner, _dataHash, _signatures[i]); _validateSignature(signer, _dataHash, _signatures[i]);
lastSigner = currentSigner; lastOperator = currentOperator;
uint256 operatorWeight = _getOperatorWeight( uint256 operatorWeight = _getOperatorWeight(
currentSigner, currentOperator,
_referenceBlock _referenceBlock
); );
signedWeight += operatorWeight; signedWeight += operatorWeight;
@ -474,6 +546,27 @@ contract ECDSAStakeRegistry is
} }
} }
/// @notice Retrieves the operator weight for a signer, either at the last checkpoint or a specified block.
/// @param _operator The operator to query their signing key history for
/// @param _referenceBlock The block number to query the operator's weight at, or the maximum uint32 value for the last checkpoint.
/// @return The weight of the operator.
function _getOperatorSigningKey(
address _operator,
uint32 _referenceBlock
) internal view returns (address) {
if (_referenceBlock >= block.number) {
revert InvalidReferenceBlock();
}
return
address(
uint160(
_operatorSigningKeyHistory[_operator].getAtBlock(
_referenceBlock
)
)
);
}
/// @notice Retrieves the operator weight for a signer, either at the last checkpoint or a specified block. /// @notice Retrieves the operator weight for a signer, either at the last checkpoint or a specified block.
/// @param _signer The address of the signer whose weight is returned. /// @param _signer The address of the signer whose weight is returned.
/// @param _referenceBlock The block number to query the operator's weight at, or the maximum uint32 value for the last checkpoint. /// @param _referenceBlock The block number to query the operator's weight at, or the maximum uint32 value for the last checkpoint.
@ -482,11 +575,10 @@ contract ECDSAStakeRegistry is
address _signer, address _signer,
uint32 _referenceBlock uint32 _referenceBlock
) internal view returns (uint256) { ) internal view returns (uint256) {
if (_referenceBlock == type(uint32).max) { if (_referenceBlock >= block.number) {
return _operatorWeightHistory[_signer].latest(); revert InvalidReferenceBlock();
} else {
return _operatorWeightHistory[_signer].getAtBlock(_referenceBlock);
} }
return _operatorWeightHistory[_signer].getAtBlock(_referenceBlock);
} }
/// @notice Retrieve the total stake weight at a specific block or the latest if not specified. /// @notice Retrieve the total stake weight at a specific block or the latest if not specified.
@ -496,11 +588,10 @@ contract ECDSAStakeRegistry is
function _getTotalWeight( function _getTotalWeight(
uint32 _referenceBlock uint32 _referenceBlock
) internal view returns (uint256) { ) internal view returns (uint256) {
if (_referenceBlock == type(uint32).max) { if (_referenceBlock >= block.number) {
return _totalWeightHistory.latest(); revert InvalidReferenceBlock();
} else {
return _totalWeightHistory.getAtBlock(_referenceBlock);
} }
return _totalWeightHistory.getAtBlock(_referenceBlock);
} }
/// @notice Retrieves the threshold stake for a given reference block. /// @notice Retrieves the threshold stake for a given reference block.
@ -510,11 +601,10 @@ contract ECDSAStakeRegistry is
function _getThresholdStake( function _getThresholdStake(
uint32 _referenceBlock uint32 _referenceBlock
) internal view returns (uint256) { ) internal view returns (uint256) {
if (_referenceBlock == type(uint32).max) { if (_referenceBlock >= block.number) {
return _thresholdWeightHistory.latest(); revert InvalidReferenceBlock();
} else {
return _thresholdWeightHistory.getAtBlock(_referenceBlock);
} }
return _thresholdWeightHistory.getAtBlock(_referenceBlock);
} }
/// @notice Validates that the cumulative stake of signed messages meets or exceeds the required threshold. /// @notice Validates that the cumulative stake of signed messages meets or exceeds the required threshold.

@ -30,6 +30,10 @@ abstract contract ECDSAStakeRegistryStorage is
/// @notice Defines the duration after which the stake's weight expires. /// @notice Defines the duration after which the stake's weight expires.
uint256 internal _stakeExpiry; uint256 internal _stakeExpiry;
/// @notice Maps an operator to their signing key history using checkpoints
mapping(address => CheckpointsUpgradeable.History)
internal _operatorSigningKeyHistory;
/// @notice Tracks the total stake history over time using checkpoints /// @notice Tracks the total stake history over time using checkpoints
CheckpointsUpgradeable.History internal _totalWeightHistory; CheckpointsUpgradeable.History internal _totalWeightHistory;
@ -51,5 +55,5 @@ abstract contract ECDSAStakeRegistryStorage is
// slither-disable-next-line shadowing-state // slither-disable-next-line shadowing-state
/// @dev Reserves storage slots for future upgrades /// @dev Reserves storage slots for future upgrades
// solhint-disable-next-line // solhint-disable-next-line
uint256[42] private __gap; uint256[40] private __gap;
} }

@ -12,8 +12,6 @@ struct Quorum {
StrategyParams[] strategies; // An array of strategy parameters to define the quorum StrategyParams[] strategies; // An array of strategy parameters to define the quorum
} }
/// part of mock interfaces for vendoring necessary Eigenlayer contracts for the hyperlane AVS
/// @author Layr Labs, Inc.
interface IECDSAStakeRegistryEventsAndErrors { interface IECDSAStakeRegistryEventsAndErrors {
/// @notice Emitted when the system registers an operator /// @notice Emitted when the system registers an operator
/// @param _operator The address of the registered operator /// @param _operator The address of the registered operator
@ -61,7 +59,19 @@ interface IECDSAStakeRegistryEventsAndErrors {
/// @notice Emits when setting a new threshold weight. /// @notice Emits when setting a new threshold weight.
event ThresholdWeightUpdated(uint256 _thresholdWeight); event ThresholdWeightUpdated(uint256 _thresholdWeight);
/// @notice Emitted when an operator's signing key is updated
/// @param operator The address of the operator whose signing key was updated
/// @param updateBlock The block number at which the signing key was updated
/// @param newSigningKey The operator's signing key after the update
/// @param oldSigningKey The operator's signing key before the update
event SigningKeyUpdate(
address indexed operator,
uint256 indexed updateBlock,
address indexed newSigningKey,
address oldSigningKey
);
/// @notice Indicates when the lengths of the signers array and signatures array do not match. /// @notice Indicates when the lengths of the signers array and signatures array do not match.
error LengthMismatch(); error LengthMismatch();
/// @notice Indicates encountering an invalid length for the signers or signatures array. /// @notice Indicates encountering an invalid length for the signers or signatures array.
@ -76,6 +86,9 @@ interface IECDSAStakeRegistryEventsAndErrors {
/// @notice Thrown when missing operators in an update /// @notice Thrown when missing operators in an update
error MustUpdateAllOperators(); error MustUpdateAllOperators();
/// @notice Reference blocks must be for blocks that have already been confirmed
error InvalidReferenceBlock();
/// @notice Indicates operator weights were out of sync and the signed weight exceed the total /// @notice Indicates operator weights were out of sync and the signed weight exceed the total
error InvalidSignedWeight(); error InvalidSignedWeight();

@ -3,6 +3,7 @@ pragma solidity >=0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../token/interfaces/IXERC20Lockbox.sol";
import "../token/interfaces/IXERC20.sol"; import "../token/interfaces/IXERC20.sol";
import "../token/interfaces/IFiatToken.sol"; import "../token/interfaces/IFiatToken.sol";
@ -66,15 +67,50 @@ contract XERC20Test is ERC20Test, IXERC20 {
_burn(account, amount); _burn(account, amount);
} }
function setLimits( function setLimits(address, uint256, uint256) external pure {
address _bridge, assert(false);
uint256 _mintingLimit,
uint256 _burningLimit
) external {
require(false);
} }
function owner() external returns (address) { function owner() external pure returns (address) {
return address(0x0); return address(0x0);
} }
} }
contract XERC20LockboxTest is IXERC20Lockbox {
IXERC20 public immutable XERC20;
IERC20 public immutable ERC20;
constructor(
string memory name,
string memory symbol,
uint256 totalSupply,
uint8 __decimals
) {
ERC20Test erc20 = new ERC20Test(name, symbol, totalSupply, __decimals);
erc20.transfer(msg.sender, totalSupply);
ERC20 = erc20;
XERC20 = new XERC20Test(name, symbol, 0, __decimals);
}
function depositTo(address _user, uint256 _amount) public {
ERC20.transferFrom(msg.sender, address(this), _amount);
XERC20.mint(_user, _amount);
}
function deposit(uint256 _amount) external {
depositTo(msg.sender, _amount);
}
function depositNativeTo(address) external payable {
assert(false);
}
function withdrawTo(address _user, uint256 _amount) public {
XERC20.burn(msg.sender, _amount);
ERC20Test(address(ERC20)).mintTo(_user, _amount);
}
function withdraw(uint256 _amount) external {
withdrawTo(msg.sender, _amount);
}
}

@ -6,7 +6,7 @@ For instructions on deploying Warp Routes, see [the deployment documentation](ht
## Warp Route Architecture ## Warp Route Architecture
A Warp Route is a collection of [`TokenRouter`](./contracts/libs/TokenRouter.sol) contracts deployed across a set of Hyperlane chains. These contracts leverage the `Router` pattern to implement access control and routing logic for remote token transfers. These contracts send and receive [`Messages`](./contracts/libs/Message.sol) which encode payloads containing a transfer `amount` and `recipient` address. A Warp Route is a collection of [`TokenRouter`](./libs/TokenRouter.sol) contracts deployed across a set of Hyperlane chains. These contracts leverage the `Router` pattern to implement access control and routing logic for remote token transfers. These contracts send and receive [`Messages`](./libs/TokenMessage.sol) which encode payloads containing a transfer `amount` and `recipient` address.
```mermaid ```mermaid
%%{ init: { %%{ init: {
@ -39,7 +39,7 @@ graph LR
Mailbox_G[(Mailbox)] Mailbox_G[(Mailbox)]
end end
HYP_E -. "router" .- HYP_P -. "router" .- HYP_G HYP_E -. "TokenMessage" .- HYP_P -. "TokenMessage" .- HYP_G
``` ```

@ -17,18 +17,39 @@ contract HypXERC20Lockbox is HypERC20Collateral {
) HypERC20Collateral(address(IXERC20Lockbox(_lockbox).ERC20()), _mailbox) { ) HypERC20Collateral(address(IXERC20Lockbox(_lockbox).ERC20()), _mailbox) {
lockbox = IXERC20Lockbox(_lockbox); lockbox = IXERC20Lockbox(_lockbox);
xERC20 = lockbox.XERC20(); xERC20 = lockbox.XERC20();
approveLockbox();
}
// grant infinite approvals to lockbox /**
* @notice Approve the lockbox to spend the wrapped token and xERC20
* @dev This function is idempotent and need not be access controlled
*/
function approveLockbox() public {
require( require(
IERC20(wrappedToken).approve(_lockbox, MAX_INT), IERC20(wrappedToken).approve(address(lockbox), MAX_INT),
"erc20 lockbox approve failed" "erc20 lockbox approve failed"
); );
require( require(
xERC20.approve(_lockbox, MAX_INT), xERC20.approve(address(lockbox), MAX_INT),
"xerc20 lockbox approve failed" "xerc20 lockbox approve failed"
); );
} }
/**
* @notice Initialize the contract
* @param _hook The address of the hook contract
* @param _ism The address of the interchain security module
* @param _owner The address of the owner
*/
function initialize(
address _hook,
address _ism,
address _owner
) public override initializer {
approveLockbox();
_MailboxClient_initialize(_hook, _ism, _owner);
}
function _transferFromSender( function _transferFromSender(
uint256 _amount uint256 _amount
) internal override returns (bytes memory) { ) internal override returns (bytes memory) {

@ -14,7 +14,7 @@ fi
lcov --version lcov --version
# exclude FastTokenRouter until https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2806 # exclude FastTokenRouter until https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2806
EXCLUDE="*test* *mock* *node_modules* *FastHyp*" EXCLUDE="*test* *mock* *node_modules* *script* *FastHyp*"
lcov \ lcov \
--rc lcov_branch_coverage=1 \ --rc lcov_branch_coverage=1 \
--remove lcov.info $EXCLUDE \ --remove lcov.info $EXCLUDE \

@ -14,7 +14,11 @@ fs_permissions = [
{ access = "read", path = "./script/avs/"}, { access = "read", path = "./script/avs/"},
{ access = "write", path = "./fixtures" } { access = "write", path = "./fixtures" }
] ]
ignored_warnings_from = ['fx-portal'] ignored_warnings_from = [
'lib',
'test',
'contracts/test'
]
[profile.ci] [profile.ci]
verbosity = 4 verbosity = 4

@ -1 +1 @@
Subproject commit e8a047e3f40f13fa37af6fe14e6e06283d9a060e Subproject commit 52715a217dc51d0de15877878ab8213f6cbbbab5

@ -1,10 +1,10 @@
{ {
"name": "@hyperlane-xyz/core", "name": "@hyperlane-xyz/core",
"description": "Core solidity contracts for Hyperlane", "description": "Core solidity contracts for Hyperlane",
"version": "3.12.2", "version": "3.13.0",
"dependencies": { "dependencies": {
"@eth-optimism/contracts": "^0.6.0", "@eth-optimism/contracts": "^0.6.0",
"@hyperlane-xyz/utils": "3.12.2", "@hyperlane-xyz/utils": "3.13.0",
"@layerzerolabs/lz-evm-oapp-v2": "2.0.2", "@layerzerolabs/lz-evm-oapp-v2": "2.0.2",
"@openzeppelin/contracts": "^4.9.3", "@openzeppelin/contracts": "^4.9.3",
"@openzeppelin/contracts-upgradeable": "^v4.9.3", "@openzeppelin/contracts-upgradeable": "^v4.9.3",

@ -12,6 +12,7 @@ import {ProxyAdmin} from "../../contracts/upgrade/ProxyAdmin.sol";
import {TransparentUpgradeableProxy} from "../../contracts/upgrade/TransparentUpgradeableProxy.sol"; import {TransparentUpgradeableProxy} from "../../contracts/upgrade/TransparentUpgradeableProxy.sol";
import {ECDSAStakeRegistry} from "../../contracts/avs/ECDSAStakeRegistry.sol"; import {ECDSAStakeRegistry} from "../../contracts/avs/ECDSAStakeRegistry.sol";
import {Quorum, StrategyParams} from "../../contracts/interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol"; import {Quorum, StrategyParams} from "../../contracts/interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol";
import {ECDSAServiceManagerBase} from "../../contracts/avs/ECDSAServiceManagerBase.sol";
import {HyperlaneServiceManager} from "../../contracts/avs/HyperlaneServiceManager.sol"; import {HyperlaneServiceManager} from "../../contracts/avs/HyperlaneServiceManager.sol";
import {TestPaymentCoordinator} from "../../contracts/test/avs/TestPaymentCoordinator.sol"; import {TestPaymentCoordinator} from "../../contracts/test/avs/TestPaymentCoordinator.sol";
@ -42,6 +43,11 @@ contract DeployAVS is Script {
); );
string memory json = vm.readFile(path); string memory json = vm.readFile(path);
proxyAdmin = ProxyAdmin(
json.readAddress(
string(abi.encodePacked(".", targetEnv, ".proxyAdmin"))
)
);
avsDirectory = IAVSDirectory( avsDirectory = IAVSDirectory(
json.readAddress( json.readAddress(
string(abi.encodePacked(".", targetEnv, ".avsDirectory")) string(abi.encodePacked(".", targetEnv, ".avsDirectory"))
@ -88,15 +94,14 @@ contract DeployAVS is Script {
} }
} }
function run(string memory network) external { function run(string memory network, string memory metadataUri) external {
deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
address deployerAddress = vm.addr(deployerPrivateKey);
_loadEigenlayerAddresses(network); _loadEigenlayerAddresses(network);
vm.startBroadcast(deployerPrivateKey); vm.startBroadcast(deployerPrivateKey);
proxyAdmin = new ProxyAdmin();
ECDSAStakeRegistry stakeRegistryImpl = new ECDSAStakeRegistry( ECDSAStakeRegistry stakeRegistryImpl = new ECDSAStakeRegistry(
delegationManager delegationManager
); );
@ -118,7 +123,7 @@ contract DeployAVS is Script {
address(proxyAdmin), address(proxyAdmin),
abi.encodeWithSelector( abi.encodeWithSelector(
HyperlaneServiceManager.initialize.selector, HyperlaneServiceManager.initialize.selector,
msg.sender address(deployerAddress)
) )
); );
@ -131,7 +136,24 @@ contract DeployAVS is Script {
quorum quorum
) )
); );
HyperlaneServiceManager hsm = HyperlaneServiceManager(
address(hsmProxy)
);
require(success, "Failed to initialize ECDSAStakeRegistry"); require(success, "Failed to initialize ECDSAStakeRegistry");
require(
ECDSAStakeRegistry(address(stakeRegistryProxy)).owner() ==
address(deployerAddress),
"Owner of ECDSAStakeRegistry is not the deployer"
);
require(
HyperlaneServiceManager(address(hsmProxy)).owner() ==
address(deployerAddress),
"Owner of HyperlaneServiceManager is not the deployer"
);
hsm.updateAVSMetadataURI(metadataUri);
console.log( console.log(
"ECDSAStakeRegistry Implementation: ", "ECDSAStakeRegistry Implementation: ",

@ -1,5 +1,6 @@
{ {
"ethereum": { "ethereum": {
"proxyAdmin": "0x75EE15Ee1B4A75Fa3e2fDF5DF3253c25599cc659",
"delegationManager": "0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A", "delegationManager": "0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A",
"avsDirectory": "0x135DDa560e946695d6f155dACaFC6f1F25C1F5AF", "avsDirectory": "0x135DDa560e946695d6f155dACaFC6f1F25C1F5AF",
"paymentCoordinator": "", "paymentCoordinator": "",
@ -19,6 +20,7 @@
] ]
}, },
"holesky": { "holesky": {
"proxyAdmin": "0x33dB966328Ea213b0f76eF96CA368AB37779F065",
"delegationManager": "0xA44151489861Fe9e3055d95adC98FbD462B948e7", "delegationManager": "0xA44151489861Fe9e3055d95adC98FbD462B948e7",
"avsDirectory": "0x055733000064333CaDDbC92763c58BF0192fFeBf", "avsDirectory": "0x055733000064333CaDDbC92763c58BF0192fFeBf",
"paymentCoordinator": "", "paymentCoordinator": "",

@ -0,0 +1,4 @@
export ROUTER_ADDRESS=0xA34ceDf9068C5deE726C67A4e1DCfCc2D6E2A7fD
export ERC20_ADDRESS=0x2416092f143378750bb29b79eD961ab195CcEea5
export XERC20_ADDRESS=0x2416092f143378750bb29b79eD961ab195CcEea5
export RPC_URL="https://rpc.blast.io"

@ -0,0 +1,5 @@
export ROUTER_ADDRESS=0x8dfbEA2582F41c8C4Eb25252BbA392fd3c09449A
export ADMIN_ADDRESS=0xa5B0D537CeBE97f087Dc5FE5732d70719caaEc1D
export ERC20_ADDRESS=0xbf5495Efe5DB9ce00f80364C8B423567e58d2110
export XERC20_ADDRESS=0x2416092f143378750bb29b79eD961ab195CcEea5
export RPC_URL="https://eth.merkle.io"

@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
import "forge-std/Script.sol";
import {AnvilRPC} from "test/AnvilRPC.sol";
import {TypeCasts} from "contracts/libs/TypeCasts.sol";
import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {ProxyAdmin} from "contracts/upgrade/ProxyAdmin.sol";
import {HypXERC20Lockbox} from "contracts/token/extensions/HypXERC20Lockbox.sol";
import {IXERC20Lockbox} from "contracts/token/interfaces/IXERC20Lockbox.sol";
import {IXERC20} from "contracts/token/interfaces/IXERC20.sol";
import {IERC20} from "contracts/token/interfaces/IXERC20.sol";
// source .env.<CHAIN>
// forge script ApproveLockbox.s.sol --broadcast --rpc-url localhost:XXXX
contract ApproveLockbox is Script {
address router = vm.envAddress("ROUTER_ADDRESS");
address admin = vm.envAddress("ADMIN_ADDRESS");
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
ITransparentUpgradeableProxy proxy = ITransparentUpgradeableProxy(router);
HypXERC20Lockbox old = HypXERC20Lockbox(router);
address lockbox = address(old.lockbox());
address mailbox = address(old.mailbox());
ProxyAdmin proxyAdmin = ProxyAdmin(admin);
function run() external {
assert(proxyAdmin.getProxyAdmin(proxy) == admin);
vm.startBroadcast(deployerPrivateKey);
HypXERC20Lockbox logic = new HypXERC20Lockbox(lockbox, mailbox);
proxyAdmin.upgradeAndCall(
proxy,
address(logic),
abi.encodeCall(HypXERC20Lockbox.approveLockbox, ())
);
vm.stopBroadcast();
vm.expectRevert("Initializable: contract is already initialized");
HypXERC20Lockbox(address(proxy)).initialize(
address(0),
address(0),
mailbox
);
}
}

@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
import "forge-std/Script.sol";
import {AnvilRPC} from "test/AnvilRPC.sol";
import {IXERC20Lockbox} from "contracts/token/interfaces/IXERC20Lockbox.sol";
import {IXERC20} from "contracts/token/interfaces/IXERC20.sol";
import {IERC20} from "contracts/token/interfaces/IXERC20.sol";
// source .env.<CHAIN>
// anvil --fork-url $RPC_URL --port XXXX
// forge script GrantLimits.s.sol --broadcast --unlocked --rpc-url localhost:XXXX
contract GrantLimits is Script {
address tester = 0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba;
uint256 amount = 1 gwei;
address router = vm.envAddress("ROUTER_ADDRESS");
IERC20 erc20 = IERC20(vm.envAddress("ERC20_ADDRESS"));
IXERC20 xerc20 = IXERC20(vm.envAddress("XERC20_ADDRESS"));
function runFrom(address account) internal {
AnvilRPC.setBalance(account, 1 ether);
AnvilRPC.impersonateAccount(account);
vm.broadcast(account);
}
function run() external {
address owner = xerc20.owner();
runFrom(owner);
xerc20.setLimits(router, amount, amount);
runFrom(address(erc20));
erc20.transfer(tester, amount);
}
}

@ -0,0 +1,127 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
import "forge-std/Script.sol";
import {IXERC20Lockbox} from "../../contracts/token/interfaces/IXERC20Lockbox.sol";
import {IXERC20} from "../../contracts/token/interfaces/IXERC20.sol";
import {IERC20} from "../../contracts/token/interfaces/IXERC20.sol";
import {HypXERC20Lockbox} from "../../contracts/token/extensions/HypXERC20Lockbox.sol";
import {HypERC20Collateral} from "../../contracts/token/HypERC20Collateral.sol";
import {HypXERC20} from "../../contracts/token/extensions/HypXERC20.sol";
import {TransparentUpgradeableProxy} from "../../contracts/upgrade/TransparentUpgradeableProxy.sol";
import {ProxyAdmin} from "../../contracts/upgrade/ProxyAdmin.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {TokenMessage} from "../../contracts/token/libs/TokenMessage.sol";
contract ezETH is Script {
using TypeCasts for address;
string ETHEREUM_RPC_URL = vm.envString("ETHEREUM_RPC_URL");
string BLAST_RPC_URL = vm.envString("BLAST_RPC_URL");
uint256 ethereumFork;
uint32 ethereumDomainId = 1;
address ethereumMailbox = 0xc005dc82818d67AF737725bD4bf75435d065D239;
address ethereumLockbox = 0xC8140dA31E6bCa19b287cC35531c2212763C2059;
uint256 blastFork;
uint32 blastDomainId = 81457;
address blastXERC20 = 0x2416092f143378750bb29b79eD961ab195CcEea5;
address blastMailbox = 0x3a867fCfFeC2B790970eeBDC9023E75B0a172aa7;
uint256 amount = 100;
function setUp() public {
ethereumFork = vm.createFork(ETHEREUM_RPC_URL);
blastFork = vm.createFork(BLAST_RPC_URL);
}
function run() external {
address deployer = address(this);
bytes32 recipient = deployer.addressToBytes32();
bytes memory tokenMessage = TokenMessage.format(recipient, amount, "");
vm.selectFork(ethereumFork);
HypXERC20Lockbox hypXERC20Lockbox = new HypXERC20Lockbox(
ethereumLockbox,
ethereumMailbox
);
ProxyAdmin ethAdmin = new ProxyAdmin();
TransparentUpgradeableProxy ethProxy = new TransparentUpgradeableProxy(
address(hypXERC20Lockbox),
address(ethAdmin),
abi.encodeCall(
HypXERC20Lockbox.initialize,
(address(0), address(0), deployer)
)
);
hypXERC20Lockbox = HypXERC20Lockbox(address(ethProxy));
vm.selectFork(blastFork);
HypXERC20 hypXERC20 = new HypXERC20(blastXERC20, blastMailbox);
ProxyAdmin blastAdmin = new ProxyAdmin();
TransparentUpgradeableProxy blastProxy = new TransparentUpgradeableProxy(
address(hypXERC20),
address(blastAdmin),
abi.encodeCall(
HypERC20Collateral.initialize,
(address(0), address(0), deployer)
)
);
hypXERC20 = HypXERC20(address(blastProxy));
hypXERC20.enrollRemoteRouter(
ethereumDomainId,
address(hypXERC20Lockbox).addressToBytes32()
);
// grant `amount` mint and burn limit to warp route
vm.prank(IXERC20(blastXERC20).owner());
IXERC20(blastXERC20).setLimits(address(hypXERC20), amount, amount);
// test sending `amount` on warp route
vm.prank(0x7BE481D464CAD7ad99500CE8A637599eB8d0FCDB); // ezETH whale
IXERC20(blastXERC20).transfer(address(this), amount);
IXERC20(blastXERC20).approve(address(hypXERC20), amount);
uint256 value = hypXERC20.quoteGasPayment(ethereumDomainId);
hypXERC20.transferRemote{value: value}(
ethereumDomainId,
recipient,
amount
);
// test receiving `amount` on warp route
vm.prank(blastMailbox);
hypXERC20.handle(
ethereumDomainId,
address(hypXERC20Lockbox).addressToBytes32(),
tokenMessage
);
vm.selectFork(ethereumFork);
hypXERC20Lockbox.enrollRemoteRouter(
blastDomainId,
address(hypXERC20).addressToBytes32()
);
// grant `amount` mint and burn limit to warp route
IXERC20 ethereumXERC20 = hypXERC20Lockbox.xERC20();
vm.prank(ethereumXERC20.owner());
ethereumXERC20.setLimits(address(hypXERC20Lockbox), amount, amount);
// test sending `amount` on warp route
IERC20 erc20 = IXERC20Lockbox(ethereumLockbox).ERC20();
vm.prank(ethereumLockbox);
erc20.transfer(address(this), amount);
erc20.approve(address(hypXERC20Lockbox), amount);
hypXERC20Lockbox.transferRemote(blastDomainId, recipient, amount);
// test receiving `amount` on warp route
vm.prank(ethereumMailbox);
hypXERC20Lockbox.handle(
blastDomainId,
address(hypXERC20).addressToBytes32(),
tokenMessage
);
}
}

@ -0,0 +1,107 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
import "forge-std/Vm.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
// see https://book.getfoundry.sh/reference/anvil/#supported-rpc-methods
library AnvilRPC {
using Strings for address;
using Strings for uint256;
using AnvilRPC for string;
using AnvilRPC for string[1];
using AnvilRPC for string[2];
using AnvilRPC for string[3];
Vm private constant vm =
Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
string private constant OPEN_ARRAY = "[";
string private constant CLOSE_ARRAY = "]";
string private constant COMMA = ",";
string private constant EMPTY_ARRAY = "[]";
function escaped(
string memory value
) internal pure returns (string memory) {
return string.concat(ESCAPED_QUOTE, value, ESCAPED_QUOTE);
}
function toString(
string[1] memory values
) internal pure returns (string memory) {
return string.concat(OPEN_ARRAY, values[0], CLOSE_ARRAY);
}
function toString(
string[2] memory values
) internal pure returns (string memory) {
return
string.concat(OPEN_ARRAY, values[0], COMMA, values[1], CLOSE_ARRAY);
}
function toString(
string[3] memory values
) internal pure returns (string memory) {
return
string.concat(
OPEN_ARRAY,
values[0],
COMMA,
values[1],
COMMA,
values[2],
CLOSE_ARRAY
);
}
function impersonateAccount(address account) internal {
vm.rpc(
"anvil_impersonateAccount",
[account.toHexString().escaped()].toString()
);
}
function setBalance(address account, uint256 balance) internal {
vm.rpc(
"anvil_setBalance",
[account.toHexString().escaped(), balance.toString()].toString()
);
}
function setCode(address account, bytes memory code) internal {
vm.rpc(
"anvil_setCode",
[account.toHexString().escaped(), string(code).escaped()].toString()
);
}
function setStorageAt(
address account,
uint256 slot,
uint256 value
) internal {
vm.rpc(
"anvil_setStorageAt",
[
account.toHexString().escaped(),
slot.toHexString(),
value.toHexString()
].toString()
);
}
function resetFork(string memory rpcUrl) internal {
string memory obj = string.concat(
// solhint-disable-next-line quotes
'{"forking":{"jsonRpcUrl":',
string(rpcUrl).escaped(),
"}}"
);
vm.rpc("anvil_reset", [obj].toString());
}
}
// here to prevent syntax highlighting from breaking
string constant ESCAPED_QUOTE = '"';

@ -29,6 +29,7 @@ contract HyperlaneServiceManagerTest is EigenlayerBase {
// Operator info // Operator info
uint256 operatorPrivateKey = 0xdeadbeef; uint256 operatorPrivateKey = 0xdeadbeef;
address operator; address operator;
address avsSigningKey = address(0xc0ffee);
bytes32 emptySalt; bytes32 emptySalt;
uint256 maxExpiry = type(uint256).max; uint256 maxExpiry = type(uint256).max;
@ -97,9 +98,11 @@ contract HyperlaneServiceManagerTest is EigenlayerBase {
emptySalt, emptySalt,
maxExpiry maxExpiry
); );
vm.prank(operator);
_ecdsaStakeRegistry.registerOperatorWithSignature( _ecdsaStakeRegistry.registerOperatorWithSignature(
operator, operatorSignature,
operatorSignature avsSigningKey
); );
// assert // assert
@ -122,12 +125,13 @@ contract HyperlaneServiceManagerTest is EigenlayerBase {
maxExpiry maxExpiry
); );
vm.prank(operator);
vm.expectRevert( vm.expectRevert(
"EIP1271SignatureUtils.checkSignature_EIP1271: signature not from signer" "EIP1271SignatureUtils.checkSignature_EIP1271: signature not from signer"
); );
_ecdsaStakeRegistry.registerOperatorWithSignature( _ecdsaStakeRegistry.registerOperatorWithSignature(
operator, operatorSignature,
operatorSignature avsSigningKey
); );
// assert // assert
@ -409,9 +413,10 @@ contract HyperlaneServiceManagerTest is EigenlayerBase {
maxExpiry maxExpiry
); );
vm.prank(operator);
_ecdsaStakeRegistry.registerOperatorWithSignature( _ecdsaStakeRegistry.registerOperatorWithSignature(
operator, operatorSignature,
operatorSignature avsSigningKey
); );
} }

@ -19,13 +19,14 @@ import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transpa
import {Mailbox} from "../../contracts/Mailbox.sol"; import {Mailbox} from "../../contracts/Mailbox.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; import {TestMailbox} from "../../contracts/test/TestMailbox.sol";
import {XERC20Test, FiatTokenTest, ERC20Test} from "../../contracts/test/ERC20Test.sol"; import {XERC20LockboxTest, XERC20Test, FiatTokenTest, ERC20Test} from "../../contracts/test/ERC20Test.sol";
import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol";
import {TestInterchainGasPaymaster} from "../../contracts/test/TestInterchainGasPaymaster.sol"; import {TestInterchainGasPaymaster} from "../../contracts/test/TestInterchainGasPaymaster.sol";
import {GasRouter} from "../../contracts/client/GasRouter.sol"; import {GasRouter} from "../../contracts/client/GasRouter.sol";
import {HypERC20} from "../../contracts/token/HypERC20.sol"; import {HypERC20} from "../../contracts/token/HypERC20.sol";
import {HypERC20Collateral} from "../../contracts/token/HypERC20Collateral.sol"; import {HypERC20Collateral} from "../../contracts/token/HypERC20Collateral.sol";
import {HypXERC20Lockbox} from "../../contracts/token/extensions/HypXERC20Lockbox.sol";
import {IXERC20} from "../../contracts/token/interfaces/IXERC20.sol"; import {IXERC20} from "../../contracts/token/interfaces/IXERC20.sol";
import {IFiatToken} from "../../contracts/token/interfaces/IFiatToken.sol"; import {IFiatToken} from "../../contracts/token/interfaces/IFiatToken.sol";
import {HypXERC20} from "../../contracts/token/extensions/HypXERC20.sol"; import {HypXERC20} from "../../contracts/token/extensions/HypXERC20.sol";
@ -442,6 +443,80 @@ contract HypXERC20Test is HypTokenTest {
} }
} }
contract HypXERC20LockboxTest is HypTokenTest {
using TypeCasts for address;
HypXERC20Lockbox internal xerc20Lockbox;
function setUp() public override {
super.setUp();
XERC20LockboxTest lockbox = new XERC20LockboxTest(
NAME,
SYMBOL,
TOTAL_SUPPLY,
DECIMALS
);
primaryToken = ERC20Test(address(lockbox.ERC20()));
localToken = new HypXERC20Lockbox(
address(lockbox),
address(localMailbox)
);
xerc20Lockbox = HypXERC20Lockbox(address(localToken));
xerc20Lockbox.enrollRemoteRouter(
DESTINATION,
address(remoteToken).addressToBytes32()
);
primaryToken.transfer(ALICE, 1000e18);
_enrollRemoteTokenRouter();
}
uint256 constant MAX_INT = 2 ** 256 - 1;
function testApproval() public {
assertEq(
xerc20Lockbox.xERC20().allowance(
address(localToken),
address(xerc20Lockbox.lockbox())
),
MAX_INT
);
assertEq(
xerc20Lockbox.wrappedToken().allowance(
address(localToken),
address(xerc20Lockbox.lockbox())
),
MAX_INT
);
}
function testRemoteTransfer() public {
uint256 balanceBefore = localToken.balanceOf(ALICE);
vm.prank(ALICE);
primaryToken.approve(address(localToken), TRANSFER_AMT);
vm.expectCall(
address(xerc20Lockbox.xERC20()),
abi.encodeCall(IXERC20.burn, (address(localToken), TRANSFER_AMT))
);
_performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMT, 0);
assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMT);
}
function testHandle() public {
uint256 balanceBefore = localToken.balanceOf(ALICE);
vm.expectCall(
address(xerc20Lockbox.xERC20()),
abi.encodeCall(IXERC20.mint, (address(localToken), TRANSFER_AMT))
);
_handleLocalTransfer(TRANSFER_AMT);
assertEq(localToken.balanceOf(ALICE), balanceBefore + TRANSFER_AMT);
}
}
contract HypFiatTokenTest is HypTokenTest { contract HypFiatTokenTest is HypTokenTest {
using TypeCasts for address; using TypeCasts for address;
HypFiatToken internal fiatToken; HypFiatToken internal fiatToken;

@ -0,0 +1,436 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 66,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "grafanacloud-prom"
},
"description": "There shouldn't be abrupt changes, especially for a specific pair",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "bars",
"fillOpacity": 78,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "normal"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"maxHeight": 600,
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "grafanacloud-prom"
},
"editorMode": "code",
"expr": "sum by (origin,remote)(round(increase(hyperlane_messages_processed_count[5m])))",
"hide": false,
"interval": "",
"legendFormat": "{{hyperlane_deployment}}: {{origin}}->{{remote}}",
"range": true,
"refId": "A"
}
],
"title": "Messages Processed",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "grafanacloud-prom"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 9
},
"id": 1,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"maxHeight": 600,
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "grafanacloud-prom"
},
"editorMode": "code",
"expr": "sum by (remote, queue_name)(\n hyperlane_submitter_queue_length{queue_name=\"prepare_queue\"}\n)",
"interval": "",
"legendFormat": "{{hyperlane_deployment }} - {{remote}}",
"range": true,
"refId": "A"
}
],
"title": "Prepare queues (all)",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "grafanacloud-prom"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 18
},
"id": 10,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"maxHeight": 600,
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "grafanacloud-prom"
},
"disableTextWrap": false,
"editorMode": "code",
"expr": "sum by(remote, queue_name) (hyperlane_submitter_queue_length{queue_name=\"submit_queue\"})",
"fullMetaSearch": false,
"includeNullMetadata": true,
"interval": "",
"legendFormat": "{{remote}}",
"range": true,
"refId": "A",
"useBackend": false
}
],
"title": "Submit Queues",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "grafanacloud-prom"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 26
},
"id": 8,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"maxHeight": 600,
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "grafanacloud-prom"
},
"disableTextWrap": false,
"editorMode": "code",
"expr": "sum by(remote, queue_name) (avg_over_time(hyperlane_submitter_queue_length{queue_name=\"confirm_queue\"}[20m]))",
"fullMetaSearch": false,
"includeNullMetadata": true,
"interval": "",
"legendFormat": "{{remote}}",
"range": true,
"refId": "A",
"useBackend": false
}
],
"title": "Confirm Queues",
"type": "timeseries"
}
],
"refresh": "1m",
"schemaVersion": 39,
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-7d",
"to": "now"
},
"timeRangeUpdatedDuringEditOrView": false,
"timepicker": {},
"timezone": "browser",
"title": "Easy Dashboard (External Sharing Template)",
"uid": "afdf6ada6uzvgga",
"version": 5,
"weekStart": ""
}

@ -1,5 +1,7 @@
# @hyperlane-xyz/ccip-server # @hyperlane-xyz/ccip-server
## 3.13.0
## 3.12.0 ## 3.12.0
## 3.11.1 ## 3.11.1

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save