Refactor agent event indexing (#2246)

### Description

This PR refactors event indexing in the agents, allowing similar logic
to be shared across multiple event types (i.e. messages, deliveries, gas
payments) and database types (i.e. the Relayer rocks DB and Scraper SQL
DB).

Furthermore, it adds new syncing modes by way of `MessageSyncCursors`
that take advantage of the monotonically increasing dispatched message
nonce to sync more intelligently.

### Drive-by changes

- Fixes a bug in the existing cursor that caused the same block range to
be indexed three times
- Modifies kathy to get rid of the idea of "rounds", just sends messages
with a sleep in between
- Minor modifications to the e2e test for performance
- Expand macros in settings
- Add scraper to e2e test

### Opportunities for improvement

- We can further reduce RPC usage (or improve latency) by sharing the
view of the latest finalized block number between cursors
- We can speed up the effective time for (a relayer to start deliving
messages | the scraper to scraper recent events) by creating
forward/backward cursors for gas payments and deliveries where the
backwards cursor terminates at index_settings.from
- We can remove the need for index_settings.from by terminating
backwards cursors based on the block number that the first message was
dispatched at

### Related issues

- Fixes #[issue number here]

### Backward compatibility

_Are these changes backward compatible?_

Yes

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

None


### Testing

_What kind of testing have these changes undergone?_

E2E tests

---------

Co-authored-by: Mattie Conover <git@mconover.dev>
pull/2257/head
Asa Oines 2 years ago committed by GitHub
parent 3711b64de3
commit 63562c7211
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/e2e.yml
  2. 1
      package.json
  3. 6
      rust/agents/relayer/src/merkle_tree_builder.rs
  4. 14
      rust/agents/relayer/src/msg/gas_payment/mod.rs
  5. 7
      rust/agents/relayer/src/msg/pending_message.rs
  6. 6
      rust/agents/relayer/src/msg/processor.rs
  7. 2
      rust/agents/relayer/src/msg/serial_submitter.rs
  8. 113
      rust/agents/relayer/src/relayer.rs
  9. 191
      rust/agents/scraper/src/agent.rs
  10. 292
      rust/agents/scraper/src/chain_scraper/mod.rs
  11. 326
      rust/agents/scraper/src/chain_scraper/sync.rs
  12. 16
      rust/agents/scraper/src/conversions.rs
  13. 19
      rust/agents/scraper/src/db/block.rs
  14. 112
      rust/agents/scraper/src/db/message.rs
  15. 16
      rust/agents/scraper/src/db/payment.rs
  16. 15
      rust/agents/scraper/src/db/txn.rs
  17. 2
      rust/agents/validator/src/validator.rs
  18. 36
      rust/chains/hyperlane-ethereum/src/interchain_gas.rs
  19. 77
      rust/chains/hyperlane-ethereum/src/mailbox.rs
  20. 16
      rust/chains/hyperlane-fuel/src/interchain_gas.rs
  21. 27
      rust/chains/hyperlane-fuel/src/mailbox.rs
  22. 358
      rust/hyperlane-base/src/contract_sync/cursor.rs
  23. 78
      rust/hyperlane-base/src/contract_sync/interchain_gas.rs
  24. 40
      rust/hyperlane-base/src/contract_sync/last_message.rs
  25. 486
      rust/hyperlane-base/src/contract_sync/mailbox.rs
  26. 23
      rust/hyperlane-base/src/contract_sync/metrics.rs
  27. 154
      rust/hyperlane-base/src/contract_sync/mod.rs
  28. 43
      rust/hyperlane-base/src/contract_sync/schema.rs
  29. 108
      rust/hyperlane-base/src/db/mod.rs
  30. 210
      rust/hyperlane-base/src/db/rocks/hyperlane_db.rs
  31. 0
      rust/hyperlane-base/src/db/rocks/iterator.rs
  32. 106
      rust/hyperlane-base/src/db/rocks/mod.rs
  33. 0
      rust/hyperlane-base/src/db/rocks/storage_types.rs
  34. 19
      rust/hyperlane-base/src/db/rocks/test_utils.rs
  35. 0
      rust/hyperlane-base/src/db/rocks/typed_db.rs
  36. 51
      rust/hyperlane-base/src/interchain_gas.rs
  37. 7
      rust/hyperlane-base/src/lib.rs
  38. 120
      rust/hyperlane-base/src/mailbox.rs
  39. 188
      rust/hyperlane-base/src/settings/base.rs
  40. 46
      rust/hyperlane-base/src/settings/chains.rs
  41. 42
      rust/hyperlane-core/src/traits/cursor.rs
  42. 36
      rust/hyperlane-core/src/traits/db.rs
  43. 40
      rust/hyperlane-core/src/traits/indexer.rs
  44. 2
      rust/hyperlane-core/src/traits/mod.rs
  45. 3
      rust/hyperlane-core/src/types/message.rs
  46. 40
      rust/hyperlane-test/src/mocks/cursor.rs
  47. 60
      rust/hyperlane-test/src/mocks/indexer.rs
  48. 7
      rust/hyperlane-test/src/mocks/mod.rs
  49. 485
      rust/utils/run-locally/src/main.rs
  50. 8
      typescript/infra/config/environments/test/multisigIsm.ts
  51. 62
      typescript/infra/hardhat.config.ts

@ -60,4 +60,4 @@ jobs:
env:
E2E_CI_MODE: 'true'
E2E_CI_TIMEOUT_SEC: '600'
E2E_KATHY_ROUNDS: '2'
E2E_KATHY_MESSAGES: '20'

@ -16,6 +16,7 @@
"private": true,
"scripts": {
"build": "yarn workspaces foreach --verbose --parallel --topological run build",
"build:e2e": "yarn workspaces foreach --verbose --parallel --exclude @hyperlane-xyz/monorepo --exclude @hyperlane-xyz/hyperlane-token --topological run build",
"clean": "yarn workspaces foreach --verbose --parallel run clean",
"sync-submodules": "./scripts/sync-submodules.sh",
"postinstall": "husky install",

@ -3,7 +3,7 @@ use std::fmt::Display;
use eyre::Result;
use tracing::{debug, error, instrument};
use hyperlane_base::db::{DbError, HyperlaneDB};
use hyperlane_base::db::{DbError, HyperlaneRocksDB};
use hyperlane_core::{
accumulator::{incremental::IncrementalMerkle, merkle::Proof},
ChainCommunicationError, H256,
@ -14,7 +14,7 @@ use crate::prover::{Prover, ProverError};
/// Struct to sync prover.
#[derive(Debug)]
pub struct MerkleTreeBuilder {
db: HyperlaneDB,
db: HyperlaneRocksDB,
prover: Prover,
incremental: IncrementalMerkle,
}
@ -68,7 +68,7 @@ pub enum MerkleTreeBuilderError {
}
impl MerkleTreeBuilder {
pub fn new(db: HyperlaneDB) -> Self {
pub fn new(db: HyperlaneRocksDB) -> Self {
let prover = Prover::default();
let incremental = IncrementalMerkle::default();
Self {

@ -4,7 +4,7 @@ use async_trait::async_trait;
use eyre::Result;
use tracing::{debug, error, trace};
use hyperlane_base::db::HyperlaneDB;
use hyperlane_base::db::HyperlaneRocksDB;
use hyperlane_core::{
HyperlaneMessage, InterchainGasExpenditure, InterchainGasPayment, TxCostEstimate, TxOutcome,
U256,
@ -40,13 +40,13 @@ pub struct GasPaymentEnforcer {
/// policy or another. If a message matches multiple policies'
/// whitelists, then whichever is first in the list will be used.
policies: Vec<(Box<dyn GasPaymentPolicy>, MatchingList)>,
db: HyperlaneDB,
db: HyperlaneRocksDB,
}
impl GasPaymentEnforcer {
pub fn new(
policy_configs: impl IntoIterator<Item = GasPaymentEnforcementConf>,
db: HyperlaneDB,
db: HyperlaneRocksDB,
) -> Self {
let policies = policy_configs
.into_iter()
@ -136,7 +136,7 @@ impl GasPaymentEnforcer {
mod test {
use std::str::FromStr;
use hyperlane_base::db::{test_utils, HyperlaneDB};
use hyperlane_base::db::{test_utils, HyperlaneRocksDB};
use hyperlane_core::{HyperlaneDomain, HyperlaneMessage, TxCostEstimate, H160, H256, U256};
use crate::settings::{
@ -148,7 +148,7 @@ mod test {
#[tokio::test]
async fn test_empty_whitelist() {
test_utils::run_test_db(|db| async move {
let hyperlane_db = HyperlaneDB::new(
let hyperlane_db = HyperlaneRocksDB::new(
&HyperlaneDomain::new_test_domain("test_empty_whitelist"),
db,
);
@ -185,7 +185,7 @@ mod test {
#[allow(unused_must_use)]
test_utils::run_test_db(|db| async move {
let hyperlane_db =
HyperlaneDB::new(&HyperlaneDomain::new_test_domain("test_no_match"), db);
HyperlaneRocksDB::new(&HyperlaneDomain::new_test_domain("test_no_match"), db);
let matching_list = serde_json::from_str(r#"[{"originDomain": 234}]"#).unwrap();
let enforcer = GasPaymentEnforcer::new(
// Require a payment
@ -212,7 +212,7 @@ mod test {
#[tokio::test]
async fn test_non_empty_matching_list() {
test_utils::run_test_db(|db| async move {
let hyperlane_db = HyperlaneDB::new(&HyperlaneDomain::new_test_domain("test_non_empty_matching_list"), db);
let hyperlane_db = HyperlaneRocksDB::new(&HyperlaneDomain::new_test_domain("test_non_empty_matching_list"), db);
let sender_address = "0xaa000000000000000000000000000000000000aa";
let recipient_address = "0xbb000000000000000000000000000000000000bb";

@ -5,10 +5,11 @@ use std::time::{Duration, Instant};
use async_trait::async_trait;
use derive_new::new;
use eyre::{Context, Result};
use hyperlane_base::db::HyperlaneRocksDB;
use prometheus::{IntCounter, IntGauge};
use tracing::{debug, error, info, instrument, trace, warn};
use hyperlane_base::{db::HyperlaneDB, CachingMailbox, CoreMetrics};
use hyperlane_base::CoreMetrics;
use hyperlane_core::{HyperlaneChain, HyperlaneDomain, HyperlaneMessage, Mailbox, U256};
use super::{
@ -29,9 +30,9 @@ const CONFIRM_DELAY: Duration = if cfg!(any(test, feature = "test-utils")) {
/// instance is for a unique origin -> destination pairing.
pub struct MessageContext {
/// Mailbox on the destination chain.
pub destination_mailbox: CachingMailbox,
pub destination_mailbox: Arc<dyn Mailbox>,
/// Origin chain database to verify gas payments.
pub origin_db: HyperlaneDB,
pub origin_db: HyperlaneRocksDB,
/// Used to construct the ISM metadata needed to verify a message from the
/// origin.
pub metadata_builder: BaseMetadataBuilder,

@ -10,7 +10,7 @@ use tokio::{
};
use tracing::{debug, info_span, instrument, instrument::Instrumented, trace, Instrument};
use hyperlane_base::{db::HyperlaneDB, CoreMetrics};
use hyperlane_base::{db::HyperlaneRocksDB, CoreMetrics};
use hyperlane_core::{HyperlaneDomain, HyperlaneMessage};
use crate::msg::pending_operation::DynPendingOperation;
@ -22,7 +22,7 @@ use super::pending_message::*;
/// for to the appropriate destination.
#[derive(new)]
pub struct MessageProcessor {
db: HyperlaneDB,
db: HyperlaneRocksDB,
whitelist: Arc<MatchingList>,
blacklist: Arc<MatchingList>,
metrics: MessageProcessorMetrics,
@ -62,7 +62,7 @@ impl MessageProcessor {
#[instrument(ret, err, skip(self), level = "info")]
async fn main_loop(mut self) -> Result<()> {
// Forever, scan HyperlaneDB looking for new messages to send. When criteria are
// Forever, scan HyperlaneRocksDB looking for new messages to send. When criteria are
// satisfied or the message is disqualified, push the message onto
// self.tx_msg and then continue the scan at the next highest
// nonce.

@ -44,7 +44,7 @@ type OpQueue = Arc<Mutex<BinaryHeap<Reverse<Box<DynPendingOperation>>>>>;
///
/// Finally, the SerialSubmitter ensures that message delivery is robust to
/// destination chain reorgs prior to committing delivery status to
/// HyperlaneDB.
/// HyperlaneRocksDB.
///
///
/// Objectives

@ -6,6 +6,7 @@ use std::{
use async_trait::async_trait;
use eyre::Result;
use hyperlane_base::{MessageContractSync, WatermarkContractSync};
use tokio::sync::mpsc::UnboundedSender;
use tokio::{
sync::{
@ -17,11 +18,10 @@ use tokio::{
use tracing::{info, info_span, instrument::Instrumented, Instrument};
use hyperlane_base::{
db::{HyperlaneDB, DB},
run_all, BaseAgent, CachingInterchainGasPaymaster, CachingMailbox, ContractSyncMetrics,
CoreMetrics, HyperlaneAgentCore,
db::{HyperlaneRocksDB, DB},
run_all, BaseAgent, ContractSyncMetrics, CoreMetrics, HyperlaneAgentCore,
};
use hyperlane_core::{HyperlaneDomain, U256};
use hyperlane_core::{HyperlaneDomain, InterchainGasPayment, U256};
use crate::msg::pending_message::MessageSubmissionMetrics;
use crate::{
@ -48,15 +48,14 @@ pub struct Relayer {
origin_chains: HashSet<HyperlaneDomain>,
destination_chains: HashSet<HyperlaneDomain>,
core: HyperlaneAgentCore,
message_syncs: HashMap<HyperlaneDomain, Arc<MessageContractSync>>,
interchain_gas_payment_syncs:
HashMap<HyperlaneDomain, Arc<WatermarkContractSync<InterchainGasPayment>>>,
/// Context data for each (origin, destination) chain pair a message can be
/// sent between
msg_ctxs: HashMap<ContextKey, Arc<MessageContext>>,
/// Mailboxes for all chains (technically only need caching mailbox for
/// origin chains)
mailboxes: HashMap<HyperlaneDomain, CachingMailbox>,
/// Interchain gas paymaster for each origin chain
interchain_gas_paymasters: HashMap<HyperlaneDomain, CachingInterchainGasPaymaster>,
prover_syncs: HashMap<HyperlaneDomain, Arc<RwLock<MerkleTreeBuilder>>>,
dbs: HashMap<HyperlaneDomain, HyperlaneRocksDB>,
whitelist: Arc<MatchingList>,
blacklist: Arc<MatchingList>,
transaction_gas_limit: Option<U256>,
@ -99,26 +98,40 @@ impl BaseAgent for Relayer {
{
let core = settings.build_hyperlane_core(metrics.clone());
let db = DB::from_path(&settings.db)?;
// Use defined origin chains and remote chains
let domains = settings
let dbs = settings
.origin_chains
.iter()
.chain(&settings.destination_chains)
.collect::<HashSet<_>>();
.map(|origin| (origin.clone(), HyperlaneRocksDB::new(origin, db.clone())))
.collect::<HashMap<_, _>>();
let mailboxes = settings
.build_all_mailboxes(domains.into_iter(), &metrics, db.clone())
.build_mailboxes(settings.destination_chains.iter(), &metrics)
.await?;
let interchain_gas_paymasters = settings
.build_all_interchain_gas_paymasters(
let validator_announces = settings
.build_validator_announces(settings.origin_chains.iter(), &metrics)
.await?;
let contract_sync_metrics = Arc::new(ContractSyncMetrics::new(&metrics));
let message_syncs = settings
.build_message_indexers(
settings.origin_chains.iter(),
&metrics,
db.clone(),
&contract_sync_metrics,
dbs.iter()
.map(|(d, db)| (d.clone(), Arc::new(db.clone()) as _))
.collect(),
)
.await?;
let validator_announces = settings
.build_all_validator_announces(settings.origin_chains.iter(), &metrics)
let interchain_gas_payment_syncs = settings
.build_interchain_gas_payment_indexers(
settings.origin_chains.iter(),
&metrics,
&contract_sync_metrics,
dbs.iter()
.map(|(d, db)| (d.clone(), Arc::new(db.clone()) as _))
.collect(),
)
.await?;
let whitelist = Arc::new(settings.whitelist);
@ -139,7 +152,7 @@ impl BaseAgent for Relayer {
.origin_chains
.iter()
.map(|origin| {
let db = HyperlaneDB::new(origin, db.clone());
let db = dbs.get(origin).unwrap().clone();
(
origin.clone(),
Arc::new(RwLock::new(MerkleTreeBuilder::new(db))),
@ -148,6 +161,7 @@ impl BaseAgent for Relayer {
.collect::<HashMap<_, _>>();
info!(gas_enforcement_policies=?settings.gas_payment_enforcement, "Gas enforcement configuration");
// need one of these per origin chain due to the database scoping even though
// the config itself is the same
let gas_payment_enforcers: HashMap<_, _> = settings
@ -158,7 +172,7 @@ impl BaseAgent for Relayer {
domain.clone(),
Arc::new(GasPaymentEnforcer::new(
settings.gas_payment_enforcement.clone(),
HyperlaneDB::new(domain, db.clone()),
dbs.get(domain).unwrap().clone(),
)),
)
})
@ -192,7 +206,7 @@ impl BaseAgent for Relayer {
},
Arc::new(MessageContext {
destination_mailbox: mailboxes[destination].clone(),
origin_db: HyperlaneDB::new(origin, db.clone()),
origin_db: dbs.get(origin).unwrap().clone(),
metadata_builder,
origin_gas_payment_enforcer: gas_payment_enforcers[origin].clone(),
transaction_gas_limit,
@ -203,12 +217,13 @@ impl BaseAgent for Relayer {
}
Ok(Self {
dbs,
origin_chains: settings.origin_chains,
destination_chains: settings.destination_chains,
msg_ctxs,
core,
mailboxes,
interchain_gas_paymasters,
message_syncs,
interchain_gas_payment_syncs,
prover_syncs,
whitelist,
blacklist,
@ -232,10 +247,9 @@ impl BaseAgent for Relayer {
tasks.push(self.run_destination_submitter(destination, receive_channel));
}
let sync_metrics = ContractSyncMetrics::new(self.core.metrics.clone());
for origin in &self.origin_chains {
tasks.push(self.run_origin_mailbox_sync(origin, sync_metrics.clone()));
tasks.push(self.run_interchain_gas_paymaster_sync(origin, sync_metrics.clone()));
tasks.push(self.run_message_sync(origin).await);
tasks.push(self.run_interchain_gas_payment_sync(origin).await);
}
// each message process attempts to send messages from a chain
@ -248,28 +262,37 @@ impl BaseAgent for Relayer {
}
impl Relayer {
fn run_origin_mailbox_sync(
async fn run_message_sync(
&self,
origin: &HyperlaneDomain,
sync_metrics: ContractSyncMetrics,
) -> Instrumented<JoinHandle<Result<()>>> {
let sync = self.mailboxes[origin].sync(
self.as_ref().settings.chains[origin.name()].index.clone(),
sync_metrics,
);
sync
) -> Instrumented<JoinHandle<eyre::Result<()>>> {
let index_settings = self.as_ref().settings.chains[origin.name()].index.clone();
let contract_sync = self.message_syncs.get(origin).unwrap().clone();
let cursor = contract_sync
.forward_backward_message_sync_cursor(index_settings)
.await;
tokio::spawn(async move {
contract_sync
.clone()
.sync("dispatched_messages", cursor)
.await
})
.instrument(info_span!("ContractSync"))
}
fn run_interchain_gas_paymaster_sync(
async fn run_interchain_gas_payment_sync(
&self,
origin: &HyperlaneDomain,
sync_metrics: ContractSyncMetrics,
) -> Instrumented<JoinHandle<Result<()>>> {
let sync = self.interchain_gas_paymasters[origin].sync(
self.as_ref().settings.chains[origin.name()].index.clone(),
sync_metrics,
);
sync
) -> Instrumented<JoinHandle<eyre::Result<()>>> {
let index_settings = self.as_ref().settings.chains[origin.name()].index.clone();
let contract_sync = self
.interchain_gas_payment_syncs
.get(origin)
.unwrap()
.clone();
let cursor = contract_sync.rate_limited_cursor(index_settings).await;
tokio::spawn(async move { contract_sync.clone().sync("gas_payments", cursor).await })
.instrument(info_span!("ContractSync"))
}
fn run_message_processor(
@ -298,7 +321,7 @@ impl Relayer {
})
.collect();
let message_processor = MessageProcessor::new(
self.mailboxes[origin].db().clone(),
self.dbs.get(origin).unwrap().clone(),
self.whitelist.clone(),
self.blacklist.clone(),
metrics,

@ -3,9 +3,11 @@ use std::sync::Arc;
use async_trait::async_trait;
use eyre::{eyre, WrapErr};
use hyperlane_base::chains::IndexSettings;
use itertools::Itertools;
use tokio::task::JoinHandle;
use tracing::{info_span, instrument::Instrumented, trace, Instrument};
use tracing::info_span;
use tracing::{instrument::Instrumented, trace, Instrument};
use hyperlane_base::{
decl_settings, run_all, BaseAgent, ContractSyncMetrics, CoreMetrics, HyperlaneAgentCore,
@ -14,7 +16,7 @@ use hyperlane_base::{
use hyperlane_core::config::*;
use hyperlane_core::HyperlaneDomain;
use crate::chain_scraper::{Contracts, SqlChainScraper};
use crate::chain_scraper::HyperlaneSqlDb;
use crate::db::ScraperDb;
/// A message explorer scraper agent
@ -22,9 +24,16 @@ use crate::db::ScraperDb;
#[allow(unused)]
pub struct Scraper {
core: HyperlaneAgentCore,
db: ScraperDb,
/// A map of scrapers by domain.
scrapers: HashMap<u32, SqlChainScraper>,
contract_sync_metrics: Arc<ContractSyncMetrics>,
metrics: Arc<CoreMetrics>,
scrapers: HashMap<u32, ChainScraper>,
}
#[derive(Debug)]
struct ChainScraper {
index_settings: IndexSettings,
db: HyperlaneSqlDb,
domain: HyperlaneDomain,
}
decl_settings!(Scraper,
@ -106,69 +115,93 @@ impl BaseAgent for Scraper {
let db = ScraperDb::connect(&settings.db).await?;
let core = settings.build_hyperlane_core(metrics.clone());
let contract_sync_metrics = ContractSyncMetrics::new(metrics.clone());
let mut scrapers: HashMap<u32, SqlChainScraper> = HashMap::new();
let contract_sync_metrics = Arc::new(ContractSyncMetrics::new(&metrics));
let mut scrapers: HashMap<u32, ChainScraper> = HashMap::new();
for domain in settings.chains_to_scrape.iter() {
let chain_setup = settings.chain_setup(domain).expect("Missing chain config");
let ctx = || format!("Loading chain {domain}");
let local = Self::load_chain(&settings, domain, &metrics)
.await
.with_context(ctx)?;
{
trace!(%domain, "Created mailbox and indexer");
let scraper = SqlChainScraper::new(
db.clone(),
local,
&chain_setup.index,
contract_sync_metrics.clone(),
)
.await?;
scrapers.insert(domain.id(), scraper);
}
let db = HyperlaneSqlDb::new(
db.clone(),
chain_setup.addresses.mailbox,
domain.clone(),
settings
.build_provider(domain, &metrics.clone())
.await?
.into(),
&chain_setup.index.clone(),
)
.await?;
scrapers.insert(
domain.id(),
ChainScraper {
domain: domain.clone(),
db,
index_settings: chain_setup.index.clone(),
},
);
}
trace!(domain_count = scrapers.len(), "Creating scraper");
trace!(domain_count = scrapers.len(), "Created scrapers");
Ok(Self { core, db, scrapers })
Ok(Self {
core,
metrics,
contract_sync_metrics,
scrapers,
})
}
#[allow(clippy::async_yields_async)]
async fn run(&self) -> Instrumented<JoinHandle<eyre::Result<()>>> {
let tasks = self
.scrapers
.iter()
.map(|(name, scraper)| {
let span = info_span!("ChainContractSync", %name, chain=%scraper.domain());
tokio::spawn(scraper.clone().sync()).instrument(span)
})
.collect();
let mut tasks = Vec::with_capacity(self.scrapers.len());
for domain in self.scrapers.keys() {
tasks.push(self.scrape(*domain).await);
}
run_all(tasks)
}
}
impl Scraper {
async fn load_chain(
config: &Settings,
domain: &HyperlaneDomain,
metrics: &Arc<CoreMetrics>,
) -> eyre::Result<Contracts> {
macro_rules! b {
($builder:ident) => {
config
.$builder(domain, metrics)
.await
.with_context(|| format!("Loading chain {domain}"))?
.into()
};
}
Ok(Contracts {
provider: b!(build_provider),
mailbox: b!(build_mailbox),
mailbox_indexer: b!(build_mailbox_indexer),
igp_indexer: b!(build_interchain_gas_paymaster_indexer),
})
/// Sync contract data and other blockchain with the current chain state.
/// This will spawn long-running contract sync tasks
async fn scrape(&self, domain_id: u32) -> Instrumented<JoinHandle<eyre::Result<()>>> {
let scraper = self.scrapers.get(&domain_id).unwrap();
let db = scraper.db.clone();
let index_settings = scraper.index_settings.clone();
let domain = scraper.domain.clone();
let mut tasks = Vec::with_capacity(2);
tasks.push(
self.build_message_indexer(
domain.clone(),
self.metrics.clone(),
self.contract_sync_metrics.clone(),
db.clone(),
index_settings.clone(),
)
.await,
);
tasks.push(
self.build_delivery_indexer(
domain.clone(),
self.metrics.clone(),
self.contract_sync_metrics.clone(),
db.clone(),
index_settings.clone(),
)
.await,
);
tasks.push(
self.build_interchain_gas_payment_indexer(
domain,
self.metrics.clone(),
self.contract_sync_metrics.clone(),
db,
index_settings.clone(),
)
.await,
);
run_all(tasks)
}
}
@ -177,3 +210,55 @@ impl AsRef<HyperlaneAgentCore> for Scraper {
&self.core
}
}
/// Create a function to spawn task that syncs contract events
macro_rules! spawn_sync_task {
($name:ident, $cursor: ident, $label:literal) => {
async fn $name(
&self,
domain: HyperlaneDomain,
metrics: Arc<CoreMetrics>,
contract_sync_metrics: Arc<ContractSyncMetrics>,
db: HyperlaneSqlDb,
index_settings: IndexSettings,
) -> Instrumented<JoinHandle<eyre::Result<()>>> {
let sync = self
.as_ref()
.settings
.$name(
&domain,
&metrics.clone(),
&contract_sync_metrics.clone(),
Arc::new(db.clone()),
)
.await
.unwrap();
let cursor = sync
.$cursor(index_settings.clone())
.await;
tokio::spawn(async move {
sync
.sync($label, cursor)
.await
})
.instrument(info_span!("ChainContractSync", chain=%domain.name(), event=$label))
}
}
}
impl Scraper {
spawn_sync_task!(
build_message_indexer,
forward_message_sync_cursor,
"message_dispatch"
);
spawn_sync_task!(
build_delivery_indexer,
rate_limited_cursor,
"message_delivery"
);
spawn_sync_task!(
build_interchain_gas_payment_indexer,
rate_limited_cursor,
"gas_payment"
);
}

@ -2,170 +2,67 @@
//! keeping things updated.
use std::collections::HashMap;
use std::future::Future;
use std::sync::Arc;
use eyre::{eyre, Result};
use futures::TryFutureExt;
use async_trait::async_trait;
use eyre::Result;
use itertools::Itertools;
use tracing::trace;
use hyperlane_base::{chains::IndexSettings, ContractSyncMetrics};
use hyperlane_base::chains::IndexSettings;
use hyperlane_core::{
BlockInfo, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage,
HyperlaneProvider, InterchainGasPaymasterIndexer, InterchainGasPayment, LogMeta, Mailbox,
MailboxIndexer, H256,
BlockInfo, Delivery, HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage,
HyperlaneMessageStore, HyperlaneProvider, HyperlaneWatermarkedLogStore, InterchainGasPayment,
LogMeta, H256,
};
use crate::db::StorablePayment;
use crate::{
chain_scraper::sync::Syncer,
date_time,
db::{BasicBlock, BlockCursor, ScraperDb, StorableDelivery, StorableMessage, StorableTxn},
};
mod sync;
/// Maximum number of records to query at a time. This came about because when a
/// lot of messages are sent in a short period of time we were ending up with a
/// lot of data to query from the node provider between points when we would
/// actually save it to the database.
const CHUNK_SIZE: usize = 50;
/// Local chain components like the mailbox.
#[derive(Debug, Clone)]
pub struct Contracts {
pub mailbox: Arc<dyn Mailbox>,
pub mailbox_indexer: Arc<dyn MailboxIndexer>,
pub igp_indexer: Arc<dyn InterchainGasPaymasterIndexer>,
pub provider: Arc<dyn HyperlaneProvider>,
}
/// A chain scraper is comprised of all the information and contract/provider
/// connections needed to scrape the contracts on a single blockchain.
#[derive(Clone, Debug)]
pub struct SqlChainScraper {
pub struct HyperlaneSqlDb {
mailbox_address: H256,
domain: HyperlaneDomain,
db: ScraperDb,
/// Contracts on this chain representing this chain (e.g. mailbox)
contracts: Contracts,
chunk_size: u32,
metrics: ContractSyncMetrics,
provider: Arc<dyn HyperlaneProvider>,
cursor: Arc<BlockCursor>,
}
#[allow(unused)]
impl SqlChainScraper {
impl HyperlaneSqlDb {
pub async fn new(
db: ScraperDb,
contracts: Contracts,
mailbox_address: H256,
domain: HyperlaneDomain,
provider: Arc<dyn HyperlaneProvider>,
index_settings: &IndexSettings,
metrics: ContractSyncMetrics,
) -> Result<Self> {
let cursor = Arc::new(
db.block_cursor(contracts.mailbox.domain().id(), index_settings.from as u64)
db.block_cursor(domain.id(), index_settings.from as u64)
.await?,
);
Ok(Self {
db,
contracts,
chunk_size: index_settings.chunk_size,
metrics,
domain,
provider,
mailbox_address,
cursor,
})
}
pub fn domain(&self) -> &HyperlaneDomain {
self.contracts.mailbox.domain()
}
/// Sync contract data and other blockchain with the current chain state.
/// This will create a long-running task that should be spawned.
pub fn sync(self) -> impl Future<Output = Result<()>> + Send + 'static {
let chain = self.contracts.mailbox.domain().name().to_owned();
Syncer::new(self)
.and_then(|syncer| syncer.run())
.and_then(|()| async move { panic!("Sync task for {chain} stopped!") })
}
/// Fetch the highest message nonce we have seen for the local domain.
async fn last_message_nonce(&self) -> Result<Option<u32>> {
self.db
.last_message_nonce(self.domain().id(), &self.contracts.mailbox.address())
.await
}
/// Store messages from the origin mailbox into the database.
///
/// Returns the highest message nonce which was provided to this
/// function.
async fn store_messages(
&self,
messages: &[HyperlaneMessageWithMeta],
txns: &HashMap<H256, TxnWithId>,
) -> Result<u32> {
debug_assert!(!messages.is_empty());
let max_nonce = messages
.iter()
.map(|m| m.message.nonce)
.max()
.ok_or_else(|| eyre!("Received empty list"))?;
self.db
.store_messages(
&self.contracts.mailbox.address(),
messages.iter().map(|m| {
let txn = txns.get(&m.meta.transaction_hash).unwrap();
StorableMessage {
msg: m.message.clone(),
meta: &m.meta,
txn_id: txn.id,
}
}),
)
.await?;
Ok(max_nonce)
}
/// Record that a message was delivered.
async fn store_deliveries(
&self,
deliveries: &[Delivery],
txns: &HashMap<H256, TxnWithId>,
) -> Result<()> {
if deliveries.is_empty() {
return Ok(());
}
let storable = deliveries.iter().map(|delivery| {
let txn_id = txns.get(&delivery.meta.transaction_hash).unwrap().id;
delivery.as_storable(txn_id)
});
self.db
.store_deliveries(
self.domain().id(),
self.contracts.mailbox.address(),
storable,
)
.await
}
async fn store_payments(
&self,
payments: &[Payment],
txns: &HashMap<H256, TxnWithId>,
) -> Result<()> {
if payments.is_empty() {
return Ok(());
}
let storable = payments.iter().map(|payment| {
let txn_id = txns.get(&payment.meta.transaction_hash).unwrap().id;
payment.as_storable(txn_id)
});
self.db.store_payments(self.domain().id(), storable).await
&self.domain
}
/// Takes a list of txn and block hashes and ensure they are all in the
@ -174,9 +71,9 @@ impl SqlChainScraper {
/// Returns the relevant transaction info.
async fn ensure_blocks_and_txns(
&self,
message_metadata: impl Iterator<Item = &LogMeta>,
log_meta: impl Iterator<Item = &LogMeta>,
) -> Result<impl Iterator<Item = TxnWithId>> {
let block_hash_by_txn_hash: HashMap<H256, H256> = message_metadata
let block_hash_by_txn_hash: HashMap<H256, H256> = log_meta
.map(|meta| (meta.transaction_hash, meta.block_hash))
.collect();
@ -242,7 +139,7 @@ impl SqlChainScraper {
let mut txns_to_insert: Vec<StorableTxn> = Vec::with_capacity(CHUNK_SIZE);
for mut chunk in as_chunks::<(&H256, &mut (Option<i64>, i64))>(txns_to_fetch, CHUNK_SIZE) {
for (hash, (_, block_id)) in chunk.iter() {
let info = self.contracts.provider.get_txn_by_hash(hash).await?;
let info = self.provider.get_txn_by_hash(hash).await?;
txns_to_insert.push(StorableTxn {
info,
block_id: *block_id,
@ -309,7 +206,7 @@ impl SqlChainScraper {
for chunk in as_chunks(blocks_to_fetch, CHUNK_SIZE) {
debug_assert!(!chunk.is_empty());
for (hash, block_info) in chunk {
let info = self.contracts.provider.get_block_by_hash(hash).await?;
let info = self.provider.get_block_by_hash(hash).await?;
let basic_info_ref = block_info.insert(BasicBlock {
id: -1,
hash: *hash,
@ -349,35 +246,130 @@ impl SqlChainScraper {
}
}
#[derive(Debug, Clone)]
struct Delivery {
message_id: H256,
meta: LogMeta,
#[async_trait]
impl HyperlaneLogStore<HyperlaneMessage> for HyperlaneSqlDb {
/// Store messages from the origin mailbox into the database.
async fn store_logs(&self, messages: &[(HyperlaneMessage, LogMeta)]) -> Result<u32> {
if messages.is_empty() {
return Ok(0);
}
let txns: HashMap<H256, TxnWithId> = self
.ensure_blocks_and_txns(messages.iter().map(|r| &r.1))
.await?
.map(|t| (t.hash, t))
.collect();
let storable = messages.iter().map(|m| {
let txn = txns.get(&m.1.transaction_hash).unwrap();
StorableMessage {
msg: m.0.clone(),
meta: &m.1,
txn_id: txn.id,
}
});
let stored = self
.db
.store_dispatched_messages(self.domain().id(), &self.mailbox_address, storable)
.await?;
Ok(stored as u32)
}
}
impl Delivery {
fn as_storable(&self, txn_id: i64) -> StorableDelivery {
StorableDelivery {
message_id: self.message_id,
meta: &self.meta,
txn_id,
#[async_trait]
impl HyperlaneLogStore<Delivery> for HyperlaneSqlDb {
async fn store_logs(&self, deliveries: &[(Delivery, LogMeta)]) -> Result<u32> {
if deliveries.is_empty() {
return Ok(0);
}
let txns: HashMap<Delivery, TxnWithId> = self
.ensure_blocks_and_txns(deliveries.iter().map(|r| &r.1))
.await?
.map(|t| (t.hash, t))
.collect();
let storable = deliveries.iter().map(|(message_id, meta)| {
let txn_id = txns.get(&meta.transaction_hash).unwrap().id;
StorableDelivery {
message_id: *message_id,
meta,
txn_id,
}
});
let stored = self
.db
.store_deliveries(self.domain().id(), self.mailbox_address, storable)
.await?;
Ok(stored as u32)
}
}
#[derive(Debug, Clone)]
struct Payment {
payment: InterchainGasPayment,
meta: LogMeta,
#[async_trait]
impl HyperlaneLogStore<InterchainGasPayment> for HyperlaneSqlDb {
async fn store_logs(&self, payments: &[(InterchainGasPayment, LogMeta)]) -> Result<u32> {
if payments.is_empty() {
return Ok(0);
}
let txns: HashMap<H256, TxnWithId> = self
.ensure_blocks_and_txns(payments.iter().map(|r| &r.1))
.await?
.map(|t| (t.hash, t))
.collect();
let storable = payments.iter().map(|(payment, meta)| {
let txn_id = txns.get(&meta.transaction_hash).unwrap().id;
StorablePayment {
payment,
meta,
txn_id,
}
});
let stored = self.db.store_payments(self.domain().id(), storable).await?;
Ok(stored as u32)
}
}
impl Payment {
fn as_storable(&self, txn_id: i64) -> StorablePayment {
StorablePayment {
payment: &self.payment,
meta: &self.meta,
txn_id,
}
#[async_trait]
impl HyperlaneMessageStore for HyperlaneSqlDb {
/// Gets a message by nonce.
async fn retrieve_message_by_nonce(&self, nonce: u32) -> Result<Option<HyperlaneMessage>> {
let message = self
.db
.retrieve_message_by_nonce(self.domain().id(), &self.mailbox_address, nonce)
.await?;
Ok(message)
}
/// Retrieves the block number at which the message with the provided nonce
/// was dispatched.
async fn retrieve_dispatched_block_number(&self, nonce: u32) -> Result<Option<u64>> {
let Some(tx_id) = self
.db
.retrieve_dispatched_tx_id(self.domain().id(), &self.mailbox_address, nonce)
.await?
else {
return Ok(None);
};
let Some(block_id) = self.db.retrieve_block_id(tx_id).await? else {
return Ok(None);
};
Ok(self.db.retrieve_block_number(block_id).await?)
}
}
#[async_trait]
impl<T> HyperlaneWatermarkedLogStore<T> for HyperlaneSqlDb
where
HyperlaneSqlDb: HyperlaneLogStore<T>,
{
/// Gets the block number high watermark
async fn retrieve_high_watermark(&self) -> Result<Option<u32>> {
Ok(Some(self.cursor.height().await.try_into()?))
}
/// Stores the block number high watermark
async fn store_high_watermark(&self, block_number: u32) -> Result<()> {
self.cursor.update(block_number.into()).await;
Ok(())
}
}
@ -393,12 +385,6 @@ struct TxnWithBlockId {
block_id: i64,
}
#[derive(Debug, Clone)]
struct HyperlaneMessageWithMeta {
message: HyperlaneMessage,
meta: LogMeta,
}
fn as_chunks<T>(iter: impl Iterator<Item = T>, chunk_size: usize) -> impl Iterator<Item = Vec<T>> {
// the itertools chunks function uses refcell which cannot be used across an
// await so this stabilizes the result by putting it into a vec of vecs and

@ -1,326 +0,0 @@
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use eyre::Result;
use itertools::Itertools;
use prometheus::{IntCounter, IntGauge, IntGaugeVec};
use time::Instant;
use tracing::{debug, info, instrument, trace, warn};
use hyperlane_base::{last_message::validate_message_continuity, RateLimitedSyncBlockRangeCursor};
use hyperlane_core::{
utils::fmt_sync_time, KnownHyperlaneDomain, ListValidity, MailboxIndexer, SyncBlockRangeCursor,
H256,
};
use crate::chain_scraper::{
Delivery, HyperlaneMessageWithMeta, Payment, SqlChainScraper, TxnWithId,
};
/// Workhorse of synchronization. This consumes a `SqlChainScraper` which has
/// the needed connections and information to work and then adds additional
/// running state that can be modified. This is a fn-like struct which allows us
/// to pass a bunch of state around without having a lot of arguments to
/// functions.
///
/// Conceptually this is *just* sync loop code with initial vars being
/// configured but as a struct + multiple functions.
pub(super) struct Syncer {
scraper: SqlChainScraper,
indexed_height: IntGauge,
stored_messages: IntCounter,
stored_deliveries: IntCounter,
stored_payments: IntCounter,
missed_messages: IntCounter,
message_nonce: IntGaugeVec,
sync_cursor: RateLimitedSyncBlockRangeCursor<Arc<dyn MailboxIndexer>>,
last_valid_range_start_block: u32,
last_nonce: Option<u32>,
}
impl Deref for Syncer {
type Target = SqlChainScraper;
fn deref(&self) -> &Self::Target {
&self.scraper
}
}
impl Syncer {
/// Create a new syncer from the `SqlChainScraper` which holds the needed
/// information and connections to create the running state.
///
/// **Note:** Run must be called for syncing to commence.
#[instrument(skip_all)]
pub async fn new(scraper: SqlChainScraper) -> Result<Self> {
let domain = scraper.domain();
let chain_name = domain.name();
let indexed_height = scraper
.metrics
.indexed_height
.with_label_values(&["all", chain_name]);
let stored_deliveries = scraper
.metrics
.stored_events
.with_label_values(&["deliveries", chain_name]);
let stored_payments = scraper
.metrics
.stored_events
.with_label_values(&["gas_payments", chain_name]);
let stored_messages = scraper
.metrics
.stored_events
.with_label_values(&["messages", chain_name]);
let missed_messages = scraper
.metrics
.missed_events
.with_label_values(&["messages", chain_name]);
let message_nonce = scraper.metrics.message_nonce.clone();
let chunk_size = scraper.chunk_size;
let initial_height = scraper.cursor.height().await as u32;
let last_valid_range_start_block = initial_height;
let last_nonce = scraper.last_message_nonce().await?;
let sync_cursor = RateLimitedSyncBlockRangeCursor::new(
scraper.contracts.mailbox_indexer.clone(),
chunk_size,
initial_height,
)
.await?;
Ok(Self {
scraper,
indexed_height,
stored_messages,
stored_deliveries,
stored_payments,
missed_messages,
message_nonce,
sync_cursor,
last_valid_range_start_block,
last_nonce,
})
}
/// Sync contract and other blockchain data with the current chain state.
#[instrument(skip(self), fields(domain = %self.domain(), chunk_size = self.chunk_size))]
pub async fn run(mut self) -> Result<()> {
let start_block = self.sync_cursor.current_position();
info!(from = start_block, "Resuming chain sync");
self.indexed_height.set(start_block as i64);
let mut last_logged_time: Option<Instant> = None;
let mut should_log_checkpoint_info = || {
if last_logged_time.is_none()
|| last_logged_time.unwrap().elapsed() > Duration::from_secs(30)
{
last_logged_time = Some(Instant::now());
true
} else {
false
}
};
loop {
let start_block = self.sync_cursor.current_position();
let Ok((from, to, eta)) = self.sync_cursor.next_range().await else { continue };
if should_log_checkpoint_info() {
info!(
from,
to,
distance_from_tip = self.sync_cursor.distance_from_tip(),
estimated_time_to_sync = fmt_sync_time(eta),
"Syncing range"
);
} else {
debug!(
from,
to,
distance_from_tip = self.sync_cursor.distance_from_tip(),
estimated_time_to_sync = fmt_sync_time(eta),
"Syncing range"
);
}
let extracted = self.scrape_range(from, to).await?;
let validation = validate_message_continuity(
self.last_nonce,
&extracted
.sorted_messages
.iter()
.map(|r| &r.message)
.collect::<Vec<_>>(),
);
match validation {
ListValidity::Valid => {
let max_nonce_of_batch = self.record_data(extracted).await?;
self.cursor.update(from as u64).await;
self.last_nonce = max_nonce_of_batch;
self.last_valid_range_start_block = from;
self.indexed_height.set(to as i64);
}
ListValidity::Empty => {
let _ = self.record_data(extracted).await?;
self.indexed_height.set(to as i64);
}
ListValidity::InvalidContinuation => {
self.missed_messages.inc();
warn!(
last_nonce = self.last_nonce,
start_block = from,
end_block = to,
last_valid_range_start_block = self.last_valid_range_start_block,
"Found invalid continuation in range. Re-indexing from the start block of the last successful range."
);
self.sync_cursor
.backtrack(self.last_valid_range_start_block);
self.indexed_height
.set(self.last_valid_range_start_block as i64);
}
ListValidity::ContainsGaps => {
self.missed_messages.inc();
self.sync_cursor.backtrack(start_block);
warn!(
last_nonce = self.last_nonce,
start_block = from,
end_block = to,
last_valid_range_start_block = self.last_valid_range_start_block,
"Found gaps in the message in range, re-indexing the same range."
);
}
}
}
}
/// Fetch contract data from a given block range.
#[instrument(skip(self))]
async fn scrape_range(&self, from: u32, to: u32) -> Result<ExtractedData> {
debug!(from, to, "Fetching messages for range");
let sorted_messages = self
.contracts
.mailbox_indexer
.fetch_sorted_messages(from, to)
.await?;
trace!(?sorted_messages, "Fetched messages");
debug!("Fetching deliveries for range");
let deliveries = self
.contracts
.mailbox_indexer
.fetch_delivered_messages(from, to)
.await?
.into_iter()
.map(|(message_id, meta)| Delivery { message_id, meta })
.collect_vec();
trace!(?deliveries, "Fetched deliveries");
debug!("Fetching payments for range");
let payments = self
.contracts
.igp_indexer
.fetch_gas_payments(from, to)
.await?
.into_iter()
.map(|(payment, meta)| Payment { payment, meta })
.collect_vec();
trace!(?payments, "Fetched payments");
info!(
message_count = sorted_messages.len(),
delivery_count = deliveries.len(),
payment_count = payments.len(),
"Indexed block range for chain"
);
let sorted_messages = sorted_messages
.into_iter()
.map(|(message, meta)| HyperlaneMessageWithMeta { message, meta })
.filter(|m| {
self.last_nonce
.map_or(true, |last_nonce| m.message.nonce > last_nonce)
})
.collect::<Vec<_>>();
debug!(
message_count = sorted_messages.len(),
"Filtered any messages already indexed for mailbox"
);
Ok(ExtractedData {
sorted_messages,
deliveries,
payments,
})
}
/// Record messages and deliveries, will fetch any extra data needed to do
/// so. Returns the max nonce or None if no messages were provided.
#[instrument(
skip_all,
fields(
sorted_messages = extracted.sorted_messages.len(),
deliveries = extracted.deliveries.len(),
payments = extracted.payments.len()
)
)]
async fn record_data(&self, extracted: ExtractedData) -> Result<Option<u32>> {
let ExtractedData {
sorted_messages,
deliveries,
payments,
} = extracted;
let txns: HashMap<H256, TxnWithId> = self
.ensure_blocks_and_txns(
sorted_messages
.iter()
.map(|r| &r.meta)
.chain(deliveries.iter().map(|d| &d.meta))
.chain(payments.iter().map(|p| &p.meta)),
)
.await?
.map(|t| (t.hash, t))
.collect();
if !deliveries.is_empty() {
self.store_deliveries(&deliveries, &txns).await?;
self.stored_deliveries.inc_by(deliveries.len() as u64);
}
if !payments.is_empty() {
self.store_payments(&payments, &txns).await?;
self.stored_payments.inc_by(payments.len() as u64);
}
if !sorted_messages.is_empty() {
let max_nonce_of_batch = self.store_messages(&sorted_messages, &txns).await?;
self.stored_messages.inc_by(sorted_messages.len() as u64);
for m in sorted_messages.iter() {
let nonce = m.message.nonce;
let dst = KnownHyperlaneDomain::try_from(m.message.destination)
.map(Into::into)
.unwrap_or("unknown");
self.message_nonce
.with_label_values(&["dispatch", self.domain().name(), dst])
.set(nonce as i64);
}
Ok(Some(max_nonce_of_batch))
} else {
Ok(None)
}
}
}
struct ExtractedData {
sorted_messages: Vec<HyperlaneMessageWithMeta>,
deliveries: Vec<Delivery>,
payments: Vec<Payment>,
}

@ -3,7 +3,7 @@ use sea_orm::prelude::BigDecimal;
use hyperlane_core::{H256, U256};
// Creates a big-endian hex representation of the address hash
// Creates a big-endian hex representation of the address
pub fn address_to_bytes(data: &H256) -> Vec<u8> {
if hex::is_h160(data.as_fixed_bytes()) {
// take the last 20 bytes
@ -13,6 +13,20 @@ pub fn address_to_bytes(data: &H256) -> Vec<u8> {
}
}
// Creates a big-endian hex representation of the address
pub fn bytes_to_address(data: Vec<u8>) -> eyre::Result<H256> {
if (data.len() != 20) && (data.len() != 32) {
return Err(eyre::eyre!("Invalid address length"));
}
if data.len() == 20 {
let mut prefix = vec![0; 12];
prefix.extend(data);
Ok(H256::from_slice(&prefix[..]))
} else {
Ok(H256::from_slice(&data[..]))
}
}
// Creates a big-endian hex representation of the address hash
pub fn h256_to_bytes(data: &H256) -> Vec<u8> {
data.as_fixed_bytes().as_slice().into()

@ -35,6 +35,25 @@ impl FromQueryResult for BasicBlock {
}
impl ScraperDb {
/// Retrieves the block number for a given block database ID
pub async fn retrieve_block_number(&self, block_id: i64) -> Result<Option<u64>> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs {
Height,
}
let block_height = block::Entity::find()
.filter(block::Column::Id.eq(block_id))
.select_only()
.column_as(block::Column::Height, QueryAs::Height)
.into_values::<i64, QueryAs>()
.one(&self.0)
.await?;
match block_height {
Some(height) => Ok(Some(height.try_into()?)),
None => Ok(None),
}
}
/// Get basic block data that can be used to insert a transaction or
/// message. Any blocks which are not found will be excluded from the
/// response.

@ -1,14 +1,12 @@
use eyre::Result;
use itertools::Itertools;
use sea_orm::{
prelude::*, ActiveValue::*, DeriveColumn, EnumIter, Insert, QueryOrder, QuerySelect,
};
use sea_orm::{prelude::*, ActiveValue::*, DeriveColumn, EnumIter, Insert, QuerySelect};
use tracing::{debug, instrument, trace};
use hyperlane_core::{HyperlaneMessage, LogMeta, H256};
use migration::OnConflict;
use crate::conversions::{address_to_bytes, h256_to_bytes};
use crate::conversions::{address_to_bytes, bytes_to_address, h256_to_bytes};
use crate::date_time;
use crate::db::ScraperDb;
@ -45,9 +43,8 @@ impl ScraperDb {
let last_nonce = message::Entity::find()
.filter(message::Column::Origin.eq(origin_domain))
.filter(message::Column::OriginMailbox.eq(address_to_bytes(origin_mailbox)))
.order_by_desc(message::Column::Nonce)
.select_only()
.column_as(message::Column::Nonce, QueryAs::Nonce)
.column_as(message::Column::Nonce.max(), QueryAs::Nonce)
.into_values::<i32, QueryAs>()
.one(&self.0)
.await?
@ -61,6 +58,74 @@ impl ScraperDb {
Ok(last_nonce)
}
/// Get the dispatched message associated with a nonce.
#[instrument(skip(self))]
pub async fn retrieve_message_by_nonce(
&self,
origin_domain: u32,
origin_mailbox: &H256,
nonce: u32,
) -> Result<Option<HyperlaneMessage>> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs {
Nonce,
}
if let Some(message) = message::Entity::find()
.filter(message::Column::Origin.eq(origin_domain))
.filter(message::Column::OriginMailbox.eq(address_to_bytes(origin_mailbox)))
.filter(message::Column::Nonce.eq(nonce))
.one(&self.0)
.await?
{
Ok(Some(HyperlaneMessage {
// We do not write version to the DB.
version: 0,
origin: message.origin.try_into()?,
nonce: message.nonce.try_into()?,
destination: message.destination.try_into()?,
sender: bytes_to_address(message.sender)?,
recipient: bytes_to_address(message.recipient)?,
body: message.msg_body.unwrap_or(Vec::new()),
}))
} else {
Ok(None)
}
}
/// Get the tx id associated with a dispatched message.
#[instrument(skip(self))]
pub async fn retrieve_dispatched_tx_id(
&self,
origin_domain: u32,
origin_mailbox: &H256,
nonce: u32,
) -> Result<Option<i64>> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs {
Nonce,
}
let tx_id = message::Entity::find()
.filter(message::Column::Origin.eq(origin_domain))
.filter(message::Column::OriginMailbox.eq(address_to_bytes(origin_mailbox)))
.filter(message::Column::Nonce.eq(nonce))
.select_only()
.column_as(message::Column::OriginTxId.max(), QueryAs::Nonce)
.group_by(message::Column::Origin)
.into_values::<i64, QueryAs>()
.one(&self.0)
.await?;
Ok(tx_id)
}
async fn deliveries_count(&self, domain: u32, destination_mailbox: Vec<u8>) -> Result<u64> {
Ok(delivered_message::Entity::find()
.filter(delivered_message::Column::Domain.eq(domain))
.filter(delivered_message::Column::DestinationMailbox.eq(destination_mailbox.clone()))
.count(&self.0)
.await?)
}
/// Store deliveries from a mailbox into the database (or update an existing
/// one).
#[instrument(skip_all)]
@ -69,12 +134,15 @@ impl ScraperDb {
domain: u32,
destination_mailbox: H256,
deliveries: impl Iterator<Item = StorableDelivery<'_>>,
) -> Result<()> {
) -> Result<u64> {
let destination_mailbox = address_to_bytes(&destination_mailbox);
let deliveries_count_before = self
.deliveries_count(domain, destination_mailbox.clone())
.await?;
// we have a race condition where a message may not have been scraped yet even
// though we have received news of delivery on this chain, so the
// message IDs are looked up in a separate "thread".
let models = deliveries
let models: Vec<delivered_message::ActiveModel> = deliveries
.map(|delivery| delivered_message::ActiveModel {
id: NotSet,
time_created: Set(date_time::now()),
@ -103,17 +171,32 @@ impl ScraperDb {
)
.exec(&self.0)
.await?;
Ok(())
let deliveries_count_after = self.deliveries_count(domain, destination_mailbox).await?;
Ok(deliveries_count_after - deliveries_count_before)
}
async fn dispatched_messages_count(&self, domain: u32, origin_mailbox: Vec<u8>) -> Result<u64> {
Ok(message::Entity::find()
.filter(message::Column::Origin.eq(domain))
.filter(message::Column::OriginMailbox.eq(origin_mailbox))
.count(&self.0)
.await?)
}
/// Store messages from a mailbox into the database (or update an existing
/// one).
#[instrument(skip_all)]
pub async fn store_messages(
pub async fn store_dispatched_messages(
&self,
domain: u32,
origin_mailbox: &H256,
messages: impl Iterator<Item = StorableMessage<'_>>,
) -> Result<()> {
) -> Result<u64> {
let origin_mailbox = address_to_bytes(origin_mailbox);
let messages_count_before = self
.dispatched_messages_count(domain, origin_mailbox.clone())
.await?;
// we have a race condition where a message may not have been scraped yet even
let models = messages
.map(|storable| message::ActiveModel {
id: NotSet,
@ -129,7 +212,7 @@ impl ScraperDb {
} else {
Some(storable.msg.body)
}),
origin_mailbox: Unchanged(address_to_bytes(origin_mailbox)),
origin_mailbox: Unchanged(origin_mailbox.clone()),
origin_tx_id: Set(storable.txn_id),
})
.collect_vec();
@ -157,6 +240,9 @@ impl ScraperDb {
)
.exec(&self.0)
.await?;
Ok(())
let messages_count_after = self
.dispatched_messages_count(domain, origin_mailbox)
.await?;
Ok(messages_count_after - messages_count_before)
}
}

@ -1,6 +1,6 @@
use eyre::Result;
use itertools::Itertools;
use sea_orm::{ActiveValue::*, Insert};
use sea_orm::{prelude::*, ActiveValue::*, Insert};
use tracing::{debug, instrument, trace};
use hyperlane_core::{InterchainGasPayment, LogMeta};
@ -25,7 +25,9 @@ impl ScraperDb {
&self,
domain: u32,
payments: impl Iterator<Item = StorablePayment<'_>>,
) -> Result<()> {
) -> Result<u64> {
let payment_count_before = self.payments_count(domain).await?;
// we have a race condition where a message may not have been scraped yet even
let models = payments
.map(|storable| gas_payment::ActiveModel {
id: NotSet,
@ -60,6 +62,14 @@ impl ScraperDb {
)
.exec(&self.0)
.await?;
Ok(())
let payment_count_after = self.payments_count(domain).await?;
Ok(payment_count_after - payment_count_before)
}
async fn payments_count(&self, domain: u32) -> Result<u64> {
Ok(gas_payment::Entity::find()
.filter(gas_payment::Column::Domain.eq(domain))
.count(&self.0)
.await?)
}
}

@ -19,6 +19,21 @@ pub struct StorableTxn {
}
impl ScraperDb {
pub async fn retrieve_block_id(&self, tx_id: i64) -> Result<Option<i64>> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs {
BlockId,
}
let block_id = transaction::Entity::find()
.filter(transaction::Column::Id.eq(tx_id))
.select_only()
.column_as(transaction::Column::BlockId, QueryAs::BlockId)
.into_values::<i64, QueryAs>()
.one(&self.0)
.await?;
Ok(block_id)
}
/// Lookup transactions and find their ids. Any transactions which are not
/// found be excluded from the hashmap.
pub async fn get_txn_ids(

@ -64,7 +64,7 @@ impl BaseAgent for Validator {
origin_chain: settings.origin_chain,
core,
mailbox,
validator_announce,
validator_announce: validator_announce.into(),
signer,
reorg_period: settings.reorg_period,
interval: settings.interval,

@ -11,7 +11,7 @@ use tracing::instrument;
use hyperlane_core::{
ChainCommunicationError, ChainResult, ContractLocator, HyperlaneAbi, HyperlaneChain,
HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexer, InterchainGasPaymaster,
InterchainGasPaymasterIndexer, InterchainGasPayment, LogMeta, H160, H256,
InterchainGasPayment, LogMeta, H160, H256,
};
use crate::contracts::i_interchain_gas_paymaster::{
@ -36,7 +36,7 @@ pub struct InterchainGasPaymasterIndexerBuilder {
#[async_trait]
impl BuildableWithProvider for InterchainGasPaymasterIndexerBuilder {
type Output = Box<dyn InterchainGasPaymasterIndexer>;
type Output = Box<dyn Indexer<InterchainGasPayment>>;
async fn build_with_provider<M: Middleware + 'static>(
&self,
@ -80,29 +80,12 @@ where
}
#[async_trait]
impl<M> Indexer for EthereumInterchainGasPaymasterIndexer<M>
where
M: Middleware + 'static,
{
#[instrument(level = "debug", err, ret, skip(self))]
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
Ok(self
.provider
.get_block_number()
.await
.map_err(ChainCommunicationError::from_other)?
.as_u32()
.saturating_sub(self.finality_blocks))
}
}
#[async_trait]
impl<M> InterchainGasPaymasterIndexer for EthereumInterchainGasPaymasterIndexer<M>
impl<M> Indexer<InterchainGasPayment> for EthereumInterchainGasPaymasterIndexer<M>
where
M: Middleware + 'static,
{
#[instrument(err, skip(self))]
async fn fetch_gas_payments(
async fn fetch_logs(
&self,
from_block: u32,
to_block: u32,
@ -129,6 +112,17 @@ where
})
.collect())
}
#[instrument(level = "debug", err, ret, skip(self))]
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
Ok(self
.provider
.get_block_number()
.await
.map_err(ChainCommunicationError::from_other)?
.as_u32()
.saturating_sub(self.finality_blocks))
}
}
pub struct InterchainGasPaymasterBuilder {}

@ -14,7 +14,7 @@ use tracing::instrument;
use hyperlane_core::{
utils::fmt_bytes, ChainCommunicationError, ChainResult, Checkpoint, ContractLocator,
HyperlaneAbi, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage,
HyperlaneProtocolError, HyperlaneProvider, Indexer, LogMeta, Mailbox, MailboxIndexer,
HyperlaneProtocolError, HyperlaneProvider, Indexer, LogMeta, Mailbox, MessageIndexer,
RawHyperlaneMessage, TxCostEstimate, TxOutcome, H160, H256, U256,
};
@ -33,13 +33,13 @@ where
}
}
pub struct MailboxIndexerBuilder {
pub struct MessageIndexerBuilder {
pub finality_blocks: u32,
}
#[async_trait]
impl BuildableWithProvider for MailboxIndexerBuilder {
type Output = Box<dyn MailboxIndexer>;
impl BuildableWithProvider for MessageIndexerBuilder {
type Output = Box<dyn MessageIndexer>;
async fn build_with_provider<M: Middleware + 'static>(
&self,
@ -54,7 +54,28 @@ impl BuildableWithProvider for MailboxIndexerBuilder {
}
}
#[derive(Debug)]
pub struct DeliveryIndexerBuilder {
pub finality_blocks: u32,
}
#[async_trait]
impl BuildableWithProvider for DeliveryIndexerBuilder {
type Output = Box<dyn Indexer<H256>>;
async fn build_with_provider<M: Middleware + 'static>(
&self,
provider: M,
locator: &ContractLocator,
) -> Self::Output {
Box::new(EthereumMailboxIndexer::new(
Arc::new(provider),
locator,
self.finality_blocks,
))
}
}
#[derive(Debug, Clone)]
/// Struct that retrieves event data for an Ethereum mailbox
pub struct EthereumMailboxIndexer<M>
where
@ -81,13 +102,7 @@ where
finality_blocks,
}
}
}
#[async_trait]
impl<M> Indexer for EthereumMailboxIndexer<M>
where
M: Middleware + 'static,
{
#[instrument(level = "debug", err, ret, skip(self))]
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
Ok(self
@ -101,12 +116,16 @@ where
}
#[async_trait]
impl<M> MailboxIndexer for EthereumMailboxIndexer<M>
impl<M> Indexer<HyperlaneMessage> for EthereumMailboxIndexer<M>
where
M: Middleware + 'static,
{
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
self.get_finalized_block_number().await
}
#[instrument(err, skip(self))]
async fn fetch_sorted_messages(
async fn fetch_logs(
&self,
from: u32,
to: u32,
@ -125,13 +144,34 @@ where
events.sort_by(|a, b| a.0.nonce.cmp(&b.0.nonce));
Ok(events)
}
}
#[async_trait]
impl<M> MessageIndexer for EthereumMailboxIndexer<M>
where
M: Middleware + 'static,
{
#[instrument(err, skip(self))]
async fn fetch_delivered_messages(
&self,
from: u32,
to: u32,
) -> ChainResult<Vec<(H256, LogMeta)>> {
async fn fetch_count_at_tip(&self) -> ChainResult<(u32, u32)> {
let tip = Indexer::<HyperlaneMessage>::get_finalized_block_number(self as _).await?;
let base_call = self.contract.count();
let call_at_tip = base_call.block(u64::from(tip));
let count = call_at_tip.call().await?;
Ok((count, tip))
}
}
#[async_trait]
impl<M> Indexer<H256> for EthereumMailboxIndexer<M>
where
M: Middleware + 'static,
{
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
self.get_finalized_block_number().await
}
#[instrument(err, skip(self))]
async fn fetch_logs(&self, from: u32, to: u32) -> ChainResult<Vec<(H256, LogMeta)>> {
Ok(self
.contract
.process_id_filter()
@ -144,7 +184,6 @@ where
.collect())
}
}
pub struct MailboxBuilder {}
#[async_trait]

@ -2,7 +2,6 @@ use async_trait::async_trait;
use hyperlane_core::{
ChainResult, HyperlaneChain, HyperlaneContract, Indexer, InterchainGasPaymaster,
InterchainGasPaymasterIndexer,
};
use hyperlane_core::{HyperlaneDomain, HyperlaneProvider, InterchainGasPayment, LogMeta, H256};
@ -33,19 +32,16 @@ impl InterchainGasPaymaster for FuelInterchainGasPaymaster {}
pub struct FuelInterchainGasPaymasterIndexer {}
#[async_trait]
impl Indexer for FuelInterchainGasPaymasterIndexer {
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
todo!()
}
}
#[async_trait]
impl InterchainGasPaymasterIndexer for FuelInterchainGasPaymasterIndexer {
async fn fetch_gas_payments(
impl Indexer<InterchainGasPayment> for FuelInterchainGasPaymasterIndexer {
async fn fetch_logs(
&self,
from_block: u32,
to_block: u32,
) -> ChainResult<Vec<(InterchainGasPayment, LogMeta)>> {
todo!()
}
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
todo!()
}
}

@ -9,8 +9,7 @@ use tracing::instrument;
use hyperlane_core::{
utils::fmt_bytes, ChainCommunicationError, ChainResult, Checkpoint, ContractLocator,
HyperlaneAbi, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage,
HyperlaneProvider, Indexer, LogMeta, Mailbox, MailboxIndexer, TxCostEstimate, TxOutcome, H256,
U256,
HyperlaneProvider, Indexer, LogMeta, Mailbox, TxCostEstimate, TxOutcome, H256, U256,
};
use crate::{
@ -147,27 +146,27 @@ impl Mailbox for FuelMailbox {
pub struct FuelMailboxIndexer {}
#[async_trait]
impl Indexer for FuelMailboxIndexer {
impl Indexer<HyperlaneMessage> for FuelMailboxIndexer {
async fn fetch_logs(
&self,
from: u32,
to: u32,
) -> ChainResult<Vec<(HyperlaneMessage, LogMeta)>> {
todo!()
}
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
todo!()
}
}
#[async_trait]
impl MailboxIndexer for FuelMailboxIndexer {
async fn fetch_sorted_messages(
&self,
from: u32,
to: u32,
) -> ChainResult<Vec<(HyperlaneMessage, LogMeta)>> {
impl Indexer<H256> for FuelMailboxIndexer {
async fn fetch_logs(&self, from: u32, to: u32) -> ChainResult<Vec<(H256, LogMeta)>> {
todo!()
}
async fn fetch_delivered_messages(
&self,
from: u32,
to: u32,
) -> ChainResult<Vec<(H256, LogMeta)>> {
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
todo!()
}
}

@ -1,42 +1,315 @@
use std::time::{Duration, Instant};
use std::cmp::Ordering;
use std::fmt::Debug;
use std::{
sync::Arc,
time::{Duration, Instant},
};
use async_trait::async_trait;
use derive_new::new;
use eyre::Result;
use tokio::time::sleep;
use tracing::warn;
use hyperlane_core::{ChainResult, Indexer, SyncBlockRangeCursor};
use hyperlane_core::{
ChainResult, ContractSyncCursor, HyperlaneMessage, HyperlaneMessageStore,
HyperlaneWatermarkedLogStore, Indexer, LogMeta, MessageIndexer,
};
use crate::contract_sync::eta_calculator::SyncerEtaCalculator;
/// Time window for the moving average used in the eta calculator in seconds.
const ETA_TIME_WINDOW: f64 = 2. * 60.;
/// A struct that holds the data needed for forwards and backwards
/// message sync cursors.
#[derive(Debug, new)]
pub(crate) struct MessageSyncCursor {
indexer: Arc<dyn MessageIndexer>,
db: Arc<dyn HyperlaneMessageStore>,
chunk_size: u32,
/// The starting block for the cursor
start_block: u32,
/// The next block that should be indexed.
next_block: u32,
/// The next nonce that the cursor is looking for.
next_nonce: u32,
}
impl MessageSyncCursor {
async fn retrieve_message_by_nonce(&self, nonce: u32) -> Option<HyperlaneMessage> {
if let Ok(Some(message)) = self.db.retrieve_message_by_nonce(nonce).await {
Some(message)
} else {
None
}
}
async fn retrieve_dispatched_block_number(&self, nonce: u32) -> Option<u32> {
if let Ok(Some(block_number)) = self.db.retrieve_dispatched_block_number(nonce).await {
Some(u32::try_from(block_number).unwrap())
} else {
None
}
}
async fn update(
&mut self,
logs: Vec<(HyperlaneMessage, LogMeta)>,
prev_nonce: u32,
) -> eyre::Result<()> {
// If we found messages, but did *not* find the message we were looking for,
// we need to rewind to the block at which we found the last message.
if !logs.is_empty() && !logs.iter().any(|m| m.0.nonce == self.next_nonce) {
// If the previous nonce has been synced, rewind to the block number
// at which it was dispatched. Otherwise, rewind all the way back to the start block.
if let Some(block_number) = self.retrieve_dispatched_block_number(prev_nonce).await {
self.next_block = block_number;
} else {
self.next_block = self.start_block;
}
Ok(())
} else {
Ok(())
}
}
}
/// A MessageSyncCursor that syncs forwards in perpetuity.
#[derive(new)]
pub(crate) struct ForwardMessageSyncCursor(MessageSyncCursor);
impl ForwardMessageSyncCursor {
async fn get_next_range(&mut self) -> ChainResult<Option<(u32, u32, Duration)>> {
// Check if any new messages have been inserted into the DB,
// and update the cursor accordingly.
while self
.0
.retrieve_message_by_nonce(self.0.next_nonce)
.await
.is_some()
{
if let Some(block_number) = self
.0
.retrieve_dispatched_block_number(self.0.next_nonce)
.await
{
self.0.next_block = block_number;
}
self.0.next_nonce += 1;
}
let (mailbox_count, tip) = self.0.indexer.fetch_count_at_tip().await?;
let cursor_count = self.0.next_nonce;
let cmp = cursor_count.cmp(&mailbox_count);
match cmp {
Ordering::Equal => {
// We are synced up to the latest nonce so we don't need to index anything.
// We update our next block number accordingly.
self.0.next_block = tip;
Ok(None)
}
Ordering::Less => {
// The cursor is behind the mailbox, so we need to index some blocks.
// We attempt to index a range of blocks that is as large as possible.
let from = self.0.next_block;
let to = u32::min(tip, from + self.0.chunk_size);
self.0.next_block = to + 1;
Ok(Some((from, to, Duration::from_secs(0))))
}
Ordering::Greater => {
panic!("Cursor is ahead of mailbox, this should never happen.");
}
}
}
}
#[async_trait]
impl ContractSyncCursor<HyperlaneMessage> for ForwardMessageSyncCursor {
async fn next_range(&mut self) -> ChainResult<(u32, u32, Duration)> {
loop {
if let Some(range) = self.get_next_range().await? {
return Ok(range);
}
// TODO: Define the sleep time from interval flag
sleep(Duration::from_secs(5)).await;
}
}
/// If the previous block has been synced, rewind to the block number
/// at which it was dispatched.
/// Otherwise, rewind all the way back to the start block.
async fn update(&mut self, logs: Vec<(HyperlaneMessage, LogMeta)>) -> eyre::Result<()> {
let prev_nonce = self.0.next_nonce.saturating_sub(1);
self.0.update(logs, prev_nonce).await
}
}
/// A MessageSyncCursor that syncs backwards to nonce zero.
#[derive(new)]
pub(crate) struct BackwardMessageSyncCursor {
cursor: MessageSyncCursor,
synced: bool,
}
impl BackwardMessageSyncCursor {
async fn get_next_range(&mut self) -> Option<(u32, u32, Duration)> {
// Check if any new messages have been inserted into the DB,
// and update the cursor accordingly.
while !self.synced {
if self
.cursor
.retrieve_message_by_nonce(self.cursor.next_nonce)
.await
.is_none()
{
break;
};
// If we found nonce zero or hit block zero, we are done rewinding.
if self.cursor.next_nonce == 0 || self.cursor.next_block == 0 {
self.synced = true;
break;
}
if let Some(block_number) = self
.cursor
.retrieve_dispatched_block_number(self.cursor.next_nonce)
.await
{
self.cursor.next_block = block_number;
}
self.cursor.next_nonce = self.cursor.next_nonce.saturating_sub(1);
}
if self.synced {
return None;
}
// Just keep going backwards.
let to = self.cursor.next_block;
let from = to.saturating_sub(self.cursor.chunk_size);
self.cursor.next_block = from.saturating_sub(1);
// TODO: Consider returning a proper ETA for the backwards pass
Some((from, to, Duration::from_secs(0)))
}
/// If the previous block has been synced, rewind to the block number
/// at which it was dispatched.
/// Otherwise, rewind all the way back to the start block.
async fn update(&mut self, logs: Vec<(HyperlaneMessage, LogMeta)>) -> eyre::Result<()> {
let prev_nonce = self.cursor.next_nonce.saturating_add(1);
self.cursor.update(logs, prev_nonce).await
}
}
enum SyncDirection {
Forward,
Backward,
}
/// A MessageSyncCursor that syncs forwards in perpetuity.
pub(crate) struct ForwardBackwardMessageSyncCursor {
forward: ForwardMessageSyncCursor,
backward: BackwardMessageSyncCursor,
direction: SyncDirection,
}
impl ForwardBackwardMessageSyncCursor {
/// Construct a new contract sync helper.
pub async fn new(
indexer: Arc<dyn MessageIndexer>,
db: Arc<dyn HyperlaneMessageStore>,
chunk_size: u32,
) -> Result<Self> {
let (count, tip) = indexer.fetch_count_at_tip().await?;
let forward_cursor = ForwardMessageSyncCursor::new(MessageSyncCursor::new(
indexer.clone(),
db.clone(),
chunk_size,
tip,
tip,
count,
));
let backward_cursor = BackwardMessageSyncCursor::new(
MessageSyncCursor::new(
indexer.clone(),
db.clone(),
chunk_size,
tip,
tip,
count.saturating_sub(1),
),
count == 0,
);
Ok(Self {
forward: forward_cursor,
backward: backward_cursor,
direction: SyncDirection::Forward,
})
}
}
#[async_trait]
impl ContractSyncCursor<HyperlaneMessage> for ForwardBackwardMessageSyncCursor {
async fn next_range(&mut self) -> ChainResult<(u32, u32, Duration)> {
loop {
// Prioritize forward syncing over backward syncing.
if let Some(forward_range) = self.forward.get_next_range().await? {
self.direction = SyncDirection::Forward;
return Ok(forward_range);
}
if let Some(backward_range) = self.backward.get_next_range().await {
self.direction = SyncDirection::Backward;
return Ok(backward_range);
}
// TODO: Define the sleep time from interval flag
sleep(Duration::from_secs(5)).await;
}
}
async fn update(&mut self, logs: Vec<(HyperlaneMessage, LogMeta)>) -> eyre::Result<()> {
match self.direction {
SyncDirection::Forward => self.forward.update(logs).await,
SyncDirection::Backward => self.backward.update(logs).await,
}
}
}
/// Tool for handling the logic of what the next block range that should be
/// queried is and also handling rate limiting. Rate limiting is automatically
/// performed by `next_range`.
pub struct RateLimitedSyncBlockRangeCursor<I> {
indexer: I,
pub(crate) struct RateLimitedContractSyncCursor<T> {
indexer: Arc<dyn Indexer<T>>,
db: Arc<dyn HyperlaneWatermarkedLogStore<T>>,
tip: u32,
last_tip_update: Instant,
chunk_size: u32,
from: u32,
eta_calculator: SyncerEtaCalculator,
initial_height: u32,
}
impl<I> RateLimitedSyncBlockRangeCursor<I>
where
I: Indexer,
{
impl<T> RateLimitedContractSyncCursor<T> {
/// Construct a new contract sync helper.
pub async fn new(indexer: I, chunk_size: u32, initial_height: u32) -> Result<Self> {
pub async fn new(
indexer: Arc<dyn Indexer<T>>,
db: Arc<dyn HyperlaneWatermarkedLogStore<T>>,
chunk_size: u32,
initial_height: u32,
) -> Result<Self> {
let tip = indexer.get_finalized_block_number().await?;
Ok(Self {
indexer,
db,
tip,
chunk_size,
last_tip_update: Instant::now(),
from: initial_height,
initial_height,
eta_calculator: SyncerEtaCalculator::new(initial_height, tip, ETA_TIME_WINDOW),
})
}
@ -44,47 +317,42 @@ where
/// Wait based on how close we are to the tip and update the tip,
/// i.e. the highest block we may scrape.
async fn rate_limit(&mut self) -> ChainResult<()> {
let update_tip = self.last_tip_update.elapsed() >= Duration::from_secs(30);
if self.from + self.chunk_size < self.tip {
// If doing the full chunk wouldn't exceed the already known tip sleep a tiny
// bit so that we can catch up relatively quickly.
sleep(Duration::from_millis(100)).await;
} else if !update_tip {
// We are close to the tip.
// Sleep a little longer because we have caught up.
sleep(Duration::from_secs(10)).await;
}
if !update_tip {
return Ok(());
}
match self.indexer.get_finalized_block_number().await {
Ok(tip) => {
// we retrieved a new tip value, go ahead and update.
self.last_tip_update = Instant::now();
self.tip = tip;
Ok(())
Ok(())
} else {
// We are within one chunk size of the known tip.
// If it's been fewer than 30s since the last tip update, sleep for a bit until we're ready to fetch the next tip.
if let Some(sleep_time) =
Duration::from_secs(30).checked_sub(self.last_tip_update.elapsed())
{
sleep(sleep_time).await;
}
Err(e) => {
warn!(error = %e, "Failed to get next block range because we could not get the current tip");
// we are failing to make a basic query, we should wait before retrying.
sleep(Duration::from_secs(10)).await;
Err(e)
match self.indexer.get_finalized_block_number().await {
Ok(tip) => {
// we retrieved a new tip value, go ahead and update.
self.last_tip_update = Instant::now();
self.tip = tip;
Ok(())
}
Err(e) => {
warn!(error = %e, "Failed to get next block range because we could not get the current tip");
// we are failing to make a basic query, we should wait before retrying.
sleep(Duration::from_secs(10)).await;
Err(e)
}
}
}
}
}
#[async_trait]
impl<I: Indexer> SyncBlockRangeCursor for RateLimitedSyncBlockRangeCursor<I> {
fn current_position(&self) -> u32 {
self.from
}
fn tip(&self) -> u32 {
self.tip
}
impl<T> ContractSyncCursor<T> for RateLimitedContractSyncCursor<T>
where
T: Send + Debug + 'static,
{
async fn next_range(&mut self) -> ChainResult<(u32, u32, Duration)> {
self.rate_limit().await?;
let to = u32::min(self.tip, self.from + self.chunk_size);
@ -98,7 +366,15 @@ impl<I: Indexer> SyncBlockRangeCursor for RateLimitedSyncBlockRangeCursor<I> {
Ok((from, to, eta))
}
fn backtrack(&mut self, start_from: u32) {
self.from = u32::min(start_from, self.from);
async fn update(&mut self, _: Vec<(T, LogMeta)>) -> eyre::Result<()> {
// Store a relatively conservative view of the high watermark, which should allow a single watermark to be
// safely shared across multiple cursors, so long as they are running sufficiently in sync
self.db
.store_high_watermark(u32::max(
self.initial_height,
self.from.saturating_sub(self.chunk_size),
))
.await?;
Ok(())
}
}

@ -1,78 +0,0 @@
use tracing::{debug, info, instrument};
use hyperlane_core::{utils::fmt_sync_time, InterchainGasPaymasterIndexer, SyncBlockRangeCursor};
use crate::{
contract_sync::{
cursor::RateLimitedSyncBlockRangeCursor, schema::InterchainGasPaymasterContractSyncDB,
},
ContractSync,
};
const GAS_PAYMENTS_LABEL: &str = "gas_payments";
impl<I> ContractSync<I>
where
I: InterchainGasPaymasterIndexer + Clone + 'static,
{
/// Sync gas payments
#[instrument(name = "GasPaymentContractSync", skip(self))]
pub(crate) async fn sync_gas_payments(&self) -> eyre::Result<()> {
let chain_name = self.domain.as_ref();
let indexed_height = self
.metrics
.indexed_height
.with_label_values(&[GAS_PAYMENTS_LABEL, chain_name]);
let stored_messages = self
.metrics
.stored_events
.with_label_values(&[GAS_PAYMENTS_LABEL, chain_name]);
let cursor = {
let config_initial_height = self.index_settings.from;
let initial_height = self
.db
.retrieve_latest_indexed_gas_payment_block()
.map_or(config_initial_height, |b| b + 1);
RateLimitedSyncBlockRangeCursor::new(
self.indexer.clone(),
self.index_settings.chunk_size,
initial_height,
)
};
let mut cursor = cursor.await?;
let start_block = cursor.current_position();
info!(from = start_block, "Resuming indexer");
indexed_height.set(start_block as i64);
loop {
let Ok((from, to, eta)) = cursor.next_range().await else { continue };
let gas_payments = self.indexer.fetch_gas_payments(from, to).await?;
debug!(
from,
to,
distance_from_tip = cursor.distance_from_tip(),
gas_payments_count = gas_payments.len(),
estimated_time_to_sync = fmt_sync_time(eta),
"Indexed block range"
);
let mut new_payments_processed: u64 = 0;
for (payment, meta) in gas_payments.iter() {
// Attempt to process the gas payment, incrementing new_payments_processed
// if it was processed for the first time.
if self.db.process_gas_payment(*payment, meta)? {
new_payments_processed += 1;
}
}
stored_messages.inc_by(new_payments_processed);
self.db.store_latest_indexed_gas_payment_block(from)?;
indexed_height.set(to as i64);
}
}
}

@ -1,40 +0,0 @@
use hyperlane_core::{HyperlaneMessage, ListValidity};
/// Check if the list of sorted messages is a valid continuation of
/// `latest_message_nonce`. If the latest index is Some, check the validity of
/// the list in continuation of the latest. If the latest index is None, check
/// the validity of just the list.
///
/// Optional latest nonce to account for possibility that ContractSync is
/// still yet to see it's first message. We want to check for validity of new
/// list of messages against a potential previous message (Some case) but also
/// still validate the new messages in the case that we have not seen any
/// previous messages (None case).
pub fn validate_message_continuity(
latest_message_nonce: Option<u32>,
sorted_messages: &[&HyperlaneMessage],
) -> ListValidity {
if sorted_messages.is_empty() {
return ListValidity::Empty;
}
// If we have seen another leaf in a previous block range, ensure
// the batch contains the consecutive next leaf
if let Some(last_seen) = latest_message_nonce {
let has_desired_message = sorted_messages
.iter()
.any(|&message| last_seen == message.nonce - 1);
if !has_desired_message {
return ListValidity::InvalidContinuation;
}
}
// Ensure no gaps in new batch of leaves
for pair in sorted_messages.windows(2) {
if pair[0].nonce != pair[1].nonce - 1 {
return ListValidity::ContainsGaps;
}
}
ListValidity::Valid
}

@ -1,486 +0,0 @@
use std::time::{Duration, Instant};
use tracing::{debug, info, instrument, warn};
use hyperlane_core::{
utils::fmt_sync_time, Indexer, KnownHyperlaneDomain, ListValidity, MailboxIndexer,
SyncBlockRangeCursor,
};
use crate::{
contract_sync::{last_message::validate_message_continuity, schema::MailboxContractSyncDB},
ContractSync,
};
const MESSAGES_LABEL: &str = "messages";
impl<I> ContractSync<I>
where
I: MailboxIndexer + Clone + 'static,
{
/// Sync dispatched messages
#[instrument(name = "MessageContractSync", skip(self))]
pub(crate) async fn sync_dispatched_messages(&self) -> eyre::Result<()> {
let chain_name = self.domain.as_ref();
let indexed_height = self
.metrics
.indexed_height
.with_label_values(&[MESSAGES_LABEL, chain_name]);
let stored_messages = self
.metrics
.stored_events
.with_label_values(&[MESSAGES_LABEL, chain_name]);
let missed_messages = self
.metrics
.missed_events
.with_label_values(&[MESSAGES_LABEL, chain_name]);
let message_nonce = self.metrics.message_nonce.clone();
let cursor = {
let config_initial_height = self.index_settings.from;
let initial_height = self
.db
.retrieve_latest_valid_message_range_start_block()
.map_or(config_initial_height, |b| b + 1);
create_cursor(
self.indexer.clone(),
self.index_settings.chunk_size,
initial_height,
)
};
// Indexes messages by fetching messages in ranges of blocks.
// We've observed occasional flakiness with providers where some events in
// a range will be missing. The leading theories are:
//
// 1. The provider is just flaky and sometimes misses events :(
//
// 2. For outbox chains with low finality times, it's possible that when
// we query the RPC provider for the latest finalized block number,
// we're returned a block number T. However when we attempt to index a range
// where the `to` block is T, the `eth_getLogs` RPC is load balanced by the
// provider to a different node whose latest known block is some block T' <T.
//
// The `eth_getLogs` RPC implementations seem to happily accept
// `to` blocks that exceed the latest known block, so it's possible
// that in our indexer we think that we've indexed up to block T but
// we've only *actually* indexed up to block T'.
//
// It's easy to determine if a provider has skipped any message events by
// looking at the indices of each message and ensuring that we've indexed a
// valid continuation of messages.
//
// There are two classes of invalid continuations:
//
// 1. The latest previously indexed message index is M that was found in a
// previously indexed block range. A new block range [A,B] is indexed, returning
// a list of messages. The lowest message index in that list is `M + 1`,
// but there are some missing messages indices in the list. This is
// likely a flaky provider, and we can simply re-index the range [A,B]
// hoping that the provider will soon return a correct list.
//
// 2. The latest previously indexed message index is M that was found in a
// previously indexed block range, [A,B]. A new block range [C,D] is
// indexed, returning a list of messages. However, the lowest message
// index in that list is M' where M' > M + 1. This missing messages
// could be anywhere in the range [A,D]:
// * It's possible there was an issue when the prior block range [A,B] was
// indexed, where the provider didn't provide some messages with indices >
// M that it should have.
// * It's possible that the range [B,C] that was presumed to be empty when it
// was indexed actually wasn't.
// * And it's possible that this was just a flaky gap, where there are
// messages in the [C,D] range that weren't returned for some reason.
//
// We can handle this by re-indexing starting from block A.
// Note this means we only handle this case upon observing messages in some
// range [C,D] that indicate a previously indexed range may have
// missed some messages.
let mut cursor = cursor.await?;
let start_block = cursor.current_position();
let mut last_valid_range_start_block = start_block;
info!(
from = start_block,
"Resuming indexer from latest valid message range start block"
);
indexed_height.set(start_block as i64);
let mut last_logged_time: Option<Instant> = None;
let mut should_log_checkpoint_info = || {
if last_logged_time.is_none()
|| last_logged_time.unwrap().elapsed() > Duration::from_secs(30)
{
last_logged_time = Some(Instant::now());
true
} else {
false
}
};
loop {
let start_block = cursor.current_position();
let Ok((from, to, eta)) = cursor.next_range().await else { continue };
let mut sorted_messages: Vec<_> = self
.indexer
.fetch_sorted_messages(from, to)
.await?
.into_iter()
.map(|(msg, _)| msg)
.collect();
if should_log_checkpoint_info() {
info!(
from,
to,
distance_from_tip = cursor.distance_from_tip(),
estimated_time_to_sync = fmt_sync_time(eta),
message_count = sorted_messages.len(),
"Indexed block range"
);
} else {
debug!(
from,
to,
distance_from_tip = cursor.distance_from_tip(),
estimated_time_to_sync = fmt_sync_time(eta),
message_count = sorted_messages.len(),
"Indexed block range"
);
}
// Get the latest known nonce. All messages whose indices are <= this index
// have been stored in the DB.
let last_nonce = self.db.retrieve_latest_nonce()?;
// Filter out any messages that have already been successfully indexed and
// stored. This is necessary if we're re-indexing blocks in hope of
// finding missing messages.
if let Some(min_nonce) = last_nonce {
sorted_messages.retain(|m| m.nonce > min_nonce);
}
debug!(
from,
to,
message_count = sorted_messages.len(),
"Filtered any messages already indexed"
);
// Ensure the sorted messages are a valid continuation of last_nonce
match validate_message_continuity(
last_nonce,
&sorted_messages.iter().collect::<Vec<_>>(),
) {
ListValidity::Valid => {
// Store messages
let max_nonce_of_batch = self.db.store_messages(&sorted_messages)?;
// Report amount of messages stored into db
stored_messages.inc_by(sorted_messages.len() as u64);
// Report latest nonce to gauge by dst
for msg in sorted_messages.iter() {
let dst = KnownHyperlaneDomain::try_from(msg.destination)
.map(|d| d.as_str())
.unwrap_or("unknown");
message_nonce
.with_label_values(&["dispatch", chain_name, dst])
.set(max_nonce_of_batch as i64);
}
// Update the latest valid start block.
self.db.store_latest_valid_message_range_start_block(from)?;
last_valid_range_start_block = from;
// Move forward to the next height
indexed_height.set(to as i64);
}
// The index of the first message in sorted_messages is not the
// `last_nonce+1`.
ListValidity::InvalidContinuation => {
missed_messages.inc();
warn!(
last_nonce = ?last_nonce,
start_block = from,
end_block = to,
last_valid_range_start_block,
"Found invalid continuation in range. Re-indexing from the start block of the last successful range.",
);
cursor.backtrack(last_valid_range_start_block);
indexed_height.set(last_valid_range_start_block as i64);
}
ListValidity::ContainsGaps => {
missed_messages.inc();
cursor.backtrack(start_block);
warn!(
last_nonce = ?last_nonce,
start_block = from,
end_block = to,
"Found gaps in the messages in range, re-indexing the same range.",
);
}
ListValidity::Empty => {
// Continue if no messages found.
// We don't update last_valid_range_start_block because we cannot extrapolate
// if the range was correctly indexed if there are no messages to observe their
// indices.
indexed_height.set(to as i64);
}
};
}
}
}
#[cfg(test)]
static mut MOCK_CURSOR: Option<hyperlane_test::mocks::cursor::MockSyncBlockRangeCursor> = None;
/// Create a new cursor. In test mode we should use the mock cursor created by
/// the test.
#[cfg_attr(test, allow(unused_variables))]
async fn create_cursor<I: Indexer>(
indexer: I,
chunk_size: u32,
initial_height: u32,
) -> eyre::Result<impl SyncBlockRangeCursor> {
#[cfg(not(test))]
{
crate::RateLimitedSyncBlockRangeCursor::new(indexer, chunk_size, initial_height).await
}
#[cfg(test)]
{
let cursor = unsafe { MOCK_CURSOR.take() };
Ok(cursor.expect("Mock cursor was not set before it was used"))
}
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use std::time::Duration;
use eyre::eyre;
use mockall::{predicate::eq, *};
use tokio::{
select,
sync::Mutex,
time::{interval, sleep, timeout},
};
use hyperlane_core::{HyperlaneDomain, HyperlaneMessage, KnownHyperlaneDomain, LogMeta, H256};
use hyperlane_test::mocks::{cursor::MockSyncBlockRangeCursor, indexer::MockHyperlaneIndexer};
use crate::{
contract_sync::{mailbox::MOCK_CURSOR, schema::MailboxContractSyncDB, IndexSettings},
db::test_utils,
db::HyperlaneDB,
ContractSync, ContractSyncMetrics, CoreMetrics,
};
// we need a mutex for our tests because of the static cursor object
lazy_static! {
static ref TEST_MTX: Mutex<()> = Mutex::new(());
}
#[tokio::test]
async fn handles_missing_rpc_messages() {
test_utils::run_test_db(|db| async move {
let message_gen = |nonce: u32| -> HyperlaneMessage {
HyperlaneMessage {
version: 0,
nonce,
origin: 1000,
destination: 2000,
sender: H256::from([10; 32]),
recipient: H256::from([11; 32]),
body: [10u8; 5].to_vec(),
}
};
let messages = (0..10).map(message_gen).collect::<Vec<HyperlaneMessage>>();
let m0 = messages[0].clone();
let m1 = messages[1].clone();
let m2 = messages[2].clone();
let m3 = messages[3].clone();
let m4 = messages[4].clone();
let m5 = messages[5].clone();
let meta = || LogMeta {
address: Default::default(),
block_number: 0,
block_hash: Default::default(),
transaction_hash: Default::default(),
transaction_index: 0,
log_index: Default::default(),
};
let latest_valid_message_range_start_block = 100;
let mut mock_indexer = MockHyperlaneIndexer::new();
let mut mock_cursor = MockSyncBlockRangeCursor::new();
{
let mut seq = Sequence::new();
// Some local macros to reduce code-duplication.
macro_rules! expect_current_position {
($return_position:literal) => {
mock_cursor
.expect__current_position()
.times(1)
.in_sequence(&mut seq)
.return_once(|| $return_position);
};
}
macro_rules! expect_backtrack {
($expected_new_from:literal) => {
mock_cursor
.expect__backtrack()
.times(1)
.in_sequence(&mut seq)
.with(eq($expected_new_from))
.return_once(|_| ());
};
}
macro_rules! expect_fetches_range {
($expected_from:literal, $expected_to:literal, $return_messages:expr) => {
let messages: &[&HyperlaneMessage] = $return_messages;
let messages = messages.iter().map(|&msg| (msg.clone(), meta())).collect();
mock_cursor
.expect__next_range()
.times(1)
.in_sequence(&mut seq)
.return_once(|| {
Box::pin(async {
Ok(($expected_from, $expected_to, Duration::from_secs(0)))
})
});
mock_indexer
.expect__fetch_sorted_messages()
.times(1)
.with(eq($expected_from), eq($expected_to))
.in_sequence(&mut seq)
.return_once(move |_, _| Ok(messages));
};
}
expect_current_position!(91);
expect_current_position!(91);
// Return m0.
expect_fetches_range!(91, 110, &[&m0]);
// Return m1, miss m2.
expect_current_position!(111);
expect_fetches_range!(101, 120, &[&m1]);
// Miss m3.
expect_current_position!(121);
expect_fetches_range!(111, 130, &[]);
// Empty range.
expect_current_position!(131);
expect_fetches_range!(121, 140, &[]);
// m1 --> m5 seen as an invalid continuation
expect_current_position!(141);
expect_fetches_range!(131, 150, &[&m5]);
expect_backtrack!(101);
// Indexer goes back to the last valid message range start block
// and indexes the range
// This time it gets m1 and m2 (which was previously skipped)
expect_current_position!(101);
expect_fetches_range!(101, 120, &[&m1, &m2]);
// Indexer continues, this time getting m3 and m5 message, but skipping m4,
// which means this range contains gaps
expect_current_position!(121);
expect_fetches_range!(118, 140, &[&m3, &m5]);
expect_backtrack!(121);
// Indexer retries, the same range in hope of filling the gap,
// which it now does successfully
expect_current_position!(121);
expect_fetches_range!(121, 140, &[&m3, &m4, &m5]);
// Indexer continues with the next block range, which happens to be empty
expect_current_position!(141);
expect_fetches_range!(141, 160, &[]);
// Stay at the same tip, so no other fetch_sorted_messages calls are made
mock_cursor.expect__current_position().returning(|| 161);
mock_cursor.expect__next_range().returning(|| {
Box::pin(async move {
// this sleep should be longer than the test timeout since we don't actually
// want to yield any more values at this point.
sleep(Duration::from_secs(100)).await;
Ok((161, 161, Duration::from_secs(0)))
})
});
}
let hyperlane_db = HyperlaneDB::new(
&HyperlaneDomain::new_test_domain("handles_missing_rpc_messages"),
db,
);
// Set the latest valid message range start block
hyperlane_db
.store_latest_valid_message_range_start_block(
latest_valid_message_range_start_block,
)
.unwrap();
let indexer = Arc::new(mock_indexer);
let metrics = Arc::new(
CoreMetrics::new("contract_sync_test", 9090, prometheus::Registry::new())
.expect("could not make metrics"),
);
unsafe { MOCK_CURSOR = Some(mock_cursor) };
let sync_metrics = ContractSyncMetrics::new(metrics);
let contract_sync = ContractSync::new(
HyperlaneDomain::Known(KnownHyperlaneDomain::Test1),
hyperlane_db.clone(),
indexer,
IndexSettings {
from: 0,
chunk_size: 19,
},
sync_metrics,
);
let sync_task = contract_sync.sync_dispatched_messages();
let test_pass_fut = timeout(Duration::from_secs(5), async move {
let mut interval = interval(Duration::from_millis(20));
loop {
if hyperlane_db.message_by_nonce(0).expect("!db").is_some()
&& hyperlane_db.message_by_nonce(1).expect("!db").is_some()
&& hyperlane_db.message_by_nonce(2).expect("!db").is_some()
&& hyperlane_db.message_by_nonce(3).expect("!db").is_some()
&& hyperlane_db.message_by_nonce(4).expect("!db").is_some()
&& hyperlane_db.message_by_nonce(5).expect("!db").is_some()
{
break;
}
interval.tick().await;
}
});
let test_result = select! {
err = sync_task => Err(eyre!(
"sync task unexpectedly done before test: {:?}", err.unwrap_err())),
tests_result = test_pass_fut =>
if tests_result.is_ok() { Ok(()) } else { Err(eyre!("timed out")) }
};
if let Err(err) = test_result {
panic!("Test failed: {err}")
}
})
.await
}
}

@ -1,6 +1,5 @@
use crate::CoreMetrics;
use prometheus::{IntCounterVec, IntGaugeVec};
use std::sync::Arc;
/// Struct encapsulating prometheus metrics used by the ContractSync.
#[derive(Debug, Clone)]
@ -12,29 +11,20 @@ pub struct ContractSyncMetrics {
/// - `chain`: Chain the indexer is collecting data from.
pub indexed_height: IntGaugeVec,
/// Events stored into HyperlaneDB (label values differentiate checkpoints vs.
/// messages)
/// Events stored into HyperlaneDB (label values differentiate event types)
///
/// Labels:
/// - `data_type`: the data the indexer is recording. E.g. `messages` or `gas_payments`.
/// - `chain`: Chain the indexer is collecting data from.
pub stored_events: IntCounterVec,
/// Unique occasions when agent missed an event (label values
/// differentiate checkpoints vs. messages)
///
/// Labels:
/// - `data_type`: the data the indexer is recording. E.g. `messages` or `gas_payments`.
/// - `chain`: Chain the indexer is collecting data from.
pub missed_events: IntCounterVec,
/// See `last_known_message_nonce` in CoreMetrics.
pub message_nonce: IntGaugeVec,
}
impl ContractSyncMetrics {
/// Instantiate a new ContractSyncMetrics object.
pub fn new(metrics: Arc<CoreMetrics>) -> Self {
pub fn new(metrics: &CoreMetrics) -> Self {
let indexed_height = metrics
.new_int_gauge(
"contract_sync_block_height",
@ -51,20 +41,11 @@ impl ContractSyncMetrics {
)
.expect("failed to register stored_events metric");
let missed_events = metrics
.new_int_counter(
"contract_sync_missed_events",
"Number of unique occasions when agent missed an event",
&["data_type", "chain"],
)
.expect("failed to register missed_events metric");
let message_nonce = metrics.last_known_message_nonce();
ContractSyncMetrics {
indexed_height,
stored_events,
missed_events,
message_nonce,
}
}

@ -1,31 +1,153 @@
use std::{marker::PhantomData, sync::Arc};
use derive_new::new;
pub use cursor::*;
use hyperlane_core::HyperlaneDomain;
pub use interchain_gas::*;
pub use mailbox::*;
use cursor::*;
use hyperlane_core::{
utils::fmt_sync_time, ContractSyncCursor, HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage,
HyperlaneMessageStore, HyperlaneWatermarkedLogStore, Indexer, MessageIndexer,
};
pub use metrics::ContractSyncMetrics;
use std::fmt::Debug;
use tracing::{debug, info};
use crate::{chains::IndexSettings, db::HyperlaneDB};
use crate::chains::IndexSettings;
mod cursor;
mod eta_calculator;
mod interchain_gas;
/// Tools for working with message continuity.
pub mod last_message;
mod mailbox;
mod metrics;
mod schema;
/// Entity that drives the syncing of an agent's db with on-chain data.
/// Extracts chain-specific data (emitted checkpoints, messages, etc) from an
/// `indexer` and fills the agent's db with this data. A CachingMailbox
/// will use a contract sync to spawn syncing tasks to keep the db up-to-date.
#[derive(Debug, new)]
pub(crate) struct ContractSync<I> {
/// `indexer` and fills the agent's db with this data.
#[derive(Debug, new, Clone)]
pub struct ContractSync<T, D: HyperlaneLogStore<T>, I: Indexer<T>> {
domain: HyperlaneDomain,
db: HyperlaneDB,
db: D,
indexer: I,
index_settings: IndexSettings,
metrics: ContractSyncMetrics,
_phantom: PhantomData<T>,
}
impl<T, D, I> ContractSync<T, D, I>
where
T: Debug + Send + Sync + Clone + 'static,
D: HyperlaneLogStore<T> + 'static,
I: Indexer<T> + Clone + 'static,
{
/// The domain that this ContractSync is running on
pub fn domain(&self) -> &HyperlaneDomain {
&self.domain
}
/// Sync logs and write them to the LogStore
#[tracing::instrument(name = "ContractSync", fields(domain=self.domain().name()), skip(self, cursor))]
pub async fn sync(
&self,
label: &'static str,
mut cursor: Box<dyn ContractSyncCursor<T>>,
) -> eyre::Result<()> {
let chain_name = self.domain.as_ref();
let indexed_height = self
.metrics
.indexed_height
.with_label_values(&[label, chain_name]);
let stored_logs = self
.metrics
.stored_events
.with_label_values(&[label, chain_name]);
loop {
let Ok((from, to, eta)) = cursor.next_range().await else { continue };
debug!(from, to, "Looking for for events in block range");
let logs = self.indexer.fetch_logs(from, to).await?;
info!(
from,
to,
num_logs = logs.len(),
estimated_time_to_sync = fmt_sync_time(eta),
"Found log(s) in block range"
);
// Store deliveries
let stored = self.db.store_logs(&logs).await?;
// Report amount of deliveries stored into db
stored_logs.inc_by(stored as u64);
// We check the value of the current gauge to avoid overwriting a higher value
// when using a ForwardBackwardMessageSyncCursor
if to as i64 > indexed_height.get() {
indexed_height.set(to as i64);
}
// Update cursor
cursor.update(logs).await?;
}
}
}
/// A ContractSync for syncing events using a RateLimitedContractSyncCursor
pub type WatermarkContractSync<T> =
ContractSync<T, Arc<dyn HyperlaneWatermarkedLogStore<T>>, Arc<dyn Indexer<T>>>;
impl<T> WatermarkContractSync<T>
where
T: Debug + Send + Sync + Clone + 'static,
{
/// Returns a new cursor to be used for syncing events from the indexer based on time
pub async fn rate_limited_cursor(
&self,
index_settings: IndexSettings,
) -> Box<dyn ContractSyncCursor<T>> {
let watermark = self.db.retrieve_high_watermark().await.unwrap();
let index_settings = IndexSettings {
from: watermark.unwrap_or(index_settings.from),
chunk_size: index_settings.chunk_size,
};
Box::new(
RateLimitedContractSyncCursor::new(
Arc::new(self.indexer.clone()),
self.db.clone(),
index_settings.chunk_size,
index_settings.from,
)
.await
.unwrap(),
)
}
}
/// A ContractSync for syncing messages using a MessageSyncCursor
pub type MessageContractSync =
ContractSync<HyperlaneMessage, Arc<dyn HyperlaneMessageStore>, Arc<dyn MessageIndexer>>;
impl MessageContractSync {
/// Returns a new cursor to be used for syncing dispatched messages from the indexer
pub async fn forward_message_sync_cursor(
&self,
index_settings: IndexSettings,
) -> Box<dyn ContractSyncCursor<HyperlaneMessage>> {
let forward_data = MessageSyncCursor::new(
self.indexer.clone(),
self.db.clone(),
index_settings.chunk_size,
index_settings.from,
index_settings.from,
0,
);
Box::new(ForwardMessageSyncCursor::new(forward_data))
}
/// Returns a new cursor to be used for syncing dispatched messages from the indexer
pub async fn forward_backward_message_sync_cursor(
&self,
index_settings: IndexSettings,
) -> Box<dyn ContractSyncCursor<HyperlaneMessage>> {
Box::new(
ForwardBackwardMessageSyncCursor::new(
self.indexer.clone(),
self.db.clone(),
index_settings.chunk_size,
)
.await
.unwrap(),
)
}
}

@ -1,43 +0,0 @@
use eyre::Result;
use crate::db::{DbError, HyperlaneDB};
/// The start block number of the latest "valid" message block range.
/// This is an interval of block indexes where > 0 messages were indexed,
/// all of which had a contiguous sequence of messages based off their indices,
/// and the lowest index is the successor to the highest index of the prior
/// valid range.
static LATEST_VALID_MESSAGE_RANGE_START_BLOCK: &str = "latest_valid_message_range_start_block";
static LATEST_INDEXED_GAS_PAYMENT_BLOCK: &str = "latest_indexed_gas_payment_block";
pub(crate) trait MailboxContractSyncDB {
fn store_latest_valid_message_range_start_block(&self, block_num: u32) -> Result<(), DbError>;
fn retrieve_latest_valid_message_range_start_block(&self) -> Option<u32>;
}
impl MailboxContractSyncDB for HyperlaneDB {
fn store_latest_valid_message_range_start_block(&self, block_num: u32) -> Result<(), DbError> {
self.store_encodable("", LATEST_VALID_MESSAGE_RANGE_START_BLOCK, &block_num)
}
fn retrieve_latest_valid_message_range_start_block(&self) -> Option<u32> {
self.retrieve_decodable("", LATEST_VALID_MESSAGE_RANGE_START_BLOCK)
.expect("db failure")
}
}
pub(crate) trait InterchainGasPaymasterContractSyncDB {
fn store_latest_indexed_gas_payment_block(&self, latest_block: u32) -> Result<(), DbError>;
fn retrieve_latest_indexed_gas_payment_block(&self) -> Option<u32>;
}
impl InterchainGasPaymasterContractSyncDB for HyperlaneDB {
fn store_latest_indexed_gas_payment_block(&self, latest_block: u32) -> Result<(), DbError> {
self.store_encodable("", LATEST_INDEXED_GAS_PAYMENT_BLOCK, &latest_block)
}
fn retrieve_latest_indexed_gas_payment_block(&self) -> Option<u32> {
self.retrieve_decodable("", LATEST_INDEXED_GAS_PAYMENT_BLOCK)
.expect("db failure")
}
}

@ -1,106 +1,2 @@
use std::path::PathBuf;
use std::{io, path::Path, sync::Arc};
use hyperlane_core::HyperlaneProtocolError;
use rocksdb::{Options, DB as Rocks};
use tracing::info;
pub use hyperlane_db::*;
pub use typed_db::*;
/// Shared functionality surrounding use of rocksdb
pub mod iterator;
/// DB operations tied to specific Mailbox
mod hyperlane_db;
/// Type-specific db operations
mod typed_db;
/// Internal-use storage types.
mod storage_types;
/// Database test utilities.
#[cfg(any(test, feature = "test-utils"))]
pub mod test_utils;
#[derive(Debug, Clone)]
/// A KV Store
pub struct DB(Arc<Rocks>);
impl From<Rocks> for DB {
fn from(rocks: Rocks) -> Self {
Self(Arc::new(rocks))
}
}
/// DB Error type
#[derive(thiserror::Error, Debug)]
pub enum DbError {
/// Rocks DB Error
#[error("{0}")]
RockError(#[from] rocksdb::Error),
#[error("Failed to open {path}, canonicalized as {canonicalized}: {source}")]
/// Error opening the database
OpeningError {
/// Rocksdb error during opening
#[source]
source: rocksdb::Error,
/// Raw database path provided
path: PathBuf,
/// Parsed path used
canonicalized: PathBuf,
},
/// Could not parse the provided database path string
#[error("Invalid database path supplied {1:?}; {0}")]
InvalidDbPath(#[source] io::Error, String),
/// Hyperlane Error
#[error("{0}")]
HyperlaneError(#[from] HyperlaneProtocolError),
}
type Result<T> = std::result::Result<T, DbError>;
impl DB {
/// Opens db at `db_path` and creates if missing
#[tracing::instrument(err)]
pub fn from_path(db_path: &Path) -> Result<DB> {
let path = {
let mut path = db_path
.parent()
.unwrap_or(Path::new("."))
.canonicalize()
.map_err(|e| DbError::InvalidDbPath(e, db_path.to_string_lossy().into()))?;
if let Some(file_name) = db_path.file_name() {
path.push(file_name);
}
path
};
if path.is_dir() {
info!(path=%path.to_string_lossy(), "Opening existing db")
} else {
info!(path=%path.to_string_lossy(), "Creating db")
}
let mut opts = Options::default();
opts.create_if_missing(true);
Rocks::open(&opts, &path)
.map_err(|e| DbError::OpeningError {
source: e,
path: db_path.into(),
canonicalized: path,
})
.map(Into::into)
}
/// Store a value in the DB
pub fn store(&self, key: &[u8], value: &[u8]) -> Result<()> {
Ok(self.0.put(key, value)?)
}
/// Retrieve a value from the DB
pub fn retrieve(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
Ok(self.0.get(key)?)
}
}
pub use rocks::*;
mod rocks;

@ -1,11 +1,14 @@
use std::future::Future;
use std::time::Duration;
use async_trait::async_trait;
use eyre::Result;
use tokio::time::sleep;
use tracing::{debug, trace};
use hyperlane_core::{
HyperlaneDomain, HyperlaneMessage, InterchainGasExpenditure, InterchainGasPayment,
HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage, HyperlaneMessageStore,
HyperlaneWatermarkedLogStore, InterchainGasExpenditure, InterchainGasPayment,
InterchainGasPaymentMeta, LogMeta, H256, U256,
};
@ -18,21 +21,21 @@ use super::{
// started with the same database and domain.
const MESSAGE_ID: &str = "message_id_";
const MESSAGE_DISPATCHED_BLOCK_NUMBER: &str = "message_dispatched_block_number_";
const MESSAGE: &str = "message_";
const LATEST_NONCE: &str = "latest_known_nonce_";
const LATEST_NONCE_FOR_DESTINATION: &str = "latest_known_nonce_for_destination_";
const NONCE_PROCESSED: &str = "nonce_processed_";
const GAS_PAYMENT_FOR_MESSAGE_ID: &str = "gas_payment_for_message_id_v2_";
const GAS_PAYMENT_META_PROCESSED: &str = "gas_payment_meta_processed_v2_";
const GAS_EXPENDITURE_FOR_MESSAGE_ID: &str = "gas_expenditure_for_message_id_";
const LATEST_INDEXED_GAS_PAYMENT_BLOCK: &str = "latest_indexed_gas_payment_block";
type Result<T> = std::result::Result<T, DbError>;
type DbResult<T> = std::result::Result<T, DbError>;
/// DB handle for storing data tied to a specific Mailbox.
#[derive(Debug, Clone)]
pub struct HyperlaneDB(HyperlaneDomain, TypedDB);
pub struct HyperlaneRocksDB(HyperlaneDomain, TypedDB);
impl std::ops::Deref for HyperlaneDB {
impl std::ops::Deref for HyperlaneRocksDB {
type Target = TypedDB;
fn deref(&self) -> &Self::Target {
@ -40,20 +43,20 @@ impl std::ops::Deref for HyperlaneDB {
}
}
impl AsRef<TypedDB> for HyperlaneDB {
impl AsRef<TypedDB> for HyperlaneRocksDB {
fn as_ref(&self) -> &TypedDB {
&self.1
}
}
impl AsRef<DB> for HyperlaneDB {
impl AsRef<DB> for HyperlaneRocksDB {
fn as_ref(&self) -> &DB {
self.1.as_ref()
}
}
impl HyperlaneDB {
/// Instantiated new `HyperlaneDB`
impl HyperlaneRocksDB {
/// Instantiated new `HyperlaneRocksDB`
pub fn new(domain: &HyperlaneDomain, db: DB) -> Self {
Self(domain.clone(), TypedDB::new(domain, db))
}
@ -63,98 +66,50 @@ impl HyperlaneDB {
&self.0
}
/// Store list of messages
pub fn store_messages(&self, messages: &[HyperlaneMessage]) -> Result<u32> {
let mut latest_nonce: u32 = 0;
for message in messages {
self.store_latest_message(message)?;
latest_nonce = message.nonce;
}
Ok(latest_nonce)
}
/// Store a raw committed message building off of the latest nonce
pub fn store_latest_message(&self, message: &HyperlaneMessage) -> Result<()> {
// If this message is not building off the latest nonce, log it.
if let Some(nonce) = self.retrieve_latest_nonce()? {
if nonce != message.nonce - 1 {
debug!(msg=%message, "Attempted to store message not building off latest nonce")
}
}
self.store_message(message)
}
/// Store a raw committed message
///
/// Keys --> Values:
/// - `nonce` --> `id`
/// - `id` --> `message`
pub fn store_message(&self, message: &HyperlaneMessage) -> Result<()> {
let id = message.id();
debug!(msg=?message, "Storing new message in db",);
self.store_message_id(message.nonce, message.destination, id)?;
self.store_keyed_encodable(MESSAGE, &id, message)?;
Ok(())
}
/// Store the latest known nonce
///
/// Key --> value: `LATEST_NONCE` --> `nonce`
pub fn update_latest_nonce(&self, nonce: u32) -> Result<()> {
if let Ok(Some(n)) = self.retrieve_latest_nonce() {
if nonce <= n {
return Ok(());
}
}
self.store_encodable("", LATEST_NONCE, &nonce)
}
/// Retrieve the highest known nonce
pub fn retrieve_latest_nonce(&self) -> Result<Option<u32>> {
self.retrieve_decodable("", LATEST_NONCE)
}
/// Store the latest known nonce for a destination
///
/// Key --> value: `destination` --> `nonce`
pub fn update_latest_nonce_for_destination(&self, destination: u32, nonce: u32) -> Result<()> {
if let Ok(Some(n)) = self.retrieve_latest_nonce_for_destination(destination) {
if nonce <= n {
return Ok(());
}
/// - `nonce` --> `dispatched block number`
fn store_message(
&self,
message: &HyperlaneMessage,
dispatched_block_number: u64,
) -> DbResult<bool> {
if let Ok(Some(_)) = self.message_id_by_nonce(message.nonce) {
trace!(msg=?message, "Message already stored in db");
return Ok(false);
}
self.store_keyed_encodable(LATEST_NONCE_FOR_DESTINATION, &destination, &nonce)
}
/// Retrieve the highest known nonce for a destination
pub fn retrieve_latest_nonce_for_destination(&self, destination: u32) -> Result<Option<u32>> {
self.retrieve_keyed_decodable(LATEST_NONCE_FOR_DESTINATION, &destination)
}
let id = message.id();
debug!(msg=?message, "Storing new message in db",);
/// Store the message id keyed by nonce
fn store_message_id(&self, nonce: u32, destination: u32, id: H256) -> Result<()> {
debug!(nonce, ?id, "storing leaf hash keyed by index");
self.store_keyed_encodable(MESSAGE_ID, &nonce, &id)?;
self.update_latest_nonce(nonce)?;
self.update_latest_nonce_for_destination(destination, nonce)
// - `id` --> `message`
self.store_keyed_encodable(MESSAGE, &id, message)?;
// - `nonce` --> `id`
self.store_keyed_encodable(MESSAGE_ID, &message.nonce, &id)?;
// - `nonce` --> `dispatched block number`
self.store_keyed_encodable(
MESSAGE_DISPATCHED_BLOCK_NUMBER,
&message.nonce,
&dispatched_block_number,
)?;
Ok(true)
}
/// Retrieve a message by its id
pub fn message_by_id(&self, id: H256) -> Result<Option<HyperlaneMessage>> {
pub fn message_by_id(&self, id: H256) -> DbResult<Option<HyperlaneMessage>> {
self.retrieve_keyed_decodable(MESSAGE, &id)
}
/// Retrieve the message id keyed by nonce
pub fn message_id_by_nonce(&self, nonce: u32) -> Result<Option<H256>> {
pub fn message_id_by_nonce(&self, nonce: u32) -> DbResult<Option<H256>> {
self.retrieve_keyed_decodable(MESSAGE_ID, &nonce)
}
/// Retrieve a message by its nonce
pub fn message_by_nonce(&self, nonce: u32) -> Result<Option<HyperlaneMessage>> {
pub fn message_by_nonce(&self, nonce: u32) -> DbResult<Option<HyperlaneMessage>> {
let id: Option<H256> = self.message_id_by_nonce(nonce)?;
match id {
None => Ok(None),
@ -164,7 +119,7 @@ impl HyperlaneDB {
// TODO(james): this is a quick-fix for the prover_sync and I don't like it
/// poll db ever 100 milliseconds waiting for a leaf.
pub fn wait_for_message_nonce(&self, nonce: u32) -> impl Future<Output = Result<H256>> {
pub fn wait_for_message_nonce(&self, nonce: u32) -> impl Future<Output = DbResult<H256>> {
let slf = self.clone();
async move {
loop {
@ -177,7 +132,7 @@ impl HyperlaneDB {
}
/// Mark nonce as processed
pub fn mark_nonce_as_processed(&self, nonce: u32) -> Result<()> {
pub fn mark_nonce_as_processed(&self, nonce: u32) -> DbResult<()> {
debug!(?nonce, "mark nonce as processed");
self.store_keyed_encodable(NONCE_PROCESSED, &nonce, &true)
}
@ -196,7 +151,7 @@ impl HyperlaneDB {
&self,
payment: InterchainGasPayment,
log_meta: &LogMeta,
) -> Result<bool> {
) -> DbResult<bool> {
let payment_meta = log_meta.into();
// If the gas payment has already been processed, do nothing
if self.retrieve_gas_payment_meta_processed(&payment_meta)? {
@ -220,26 +175,29 @@ impl HyperlaneDB {
/// Processes the gas expenditure and store the total expenditure for the
/// message.
pub fn process_gas_expenditure(&self, expenditure: InterchainGasExpenditure) -> Result<()> {
pub fn process_gas_expenditure(&self, expenditure: InterchainGasExpenditure) -> DbResult<()> {
// Update the total gas expenditure for the message to include the payment
self.update_gas_expenditure_for_message_id(expenditure)
}
/// Record a gas payment, identified by its metadata, as processed
fn store_gas_payment_meta_processed(&self, meta: &InterchainGasPaymentMeta) -> Result<()> {
fn store_gas_payment_meta_processed(&self, meta: &InterchainGasPaymentMeta) -> DbResult<()> {
self.store_keyed_encodable(GAS_PAYMENT_META_PROCESSED, meta, &true)
}
/// Get whether a gas payment, identified by its metadata, has been
/// processed already
fn retrieve_gas_payment_meta_processed(&self, meta: &InterchainGasPaymentMeta) -> Result<bool> {
fn retrieve_gas_payment_meta_processed(
&self,
meta: &InterchainGasPaymentMeta,
) -> DbResult<bool> {
Ok(self
.retrieve_keyed_decodable(GAS_PAYMENT_META_PROCESSED, meta)?
.unwrap_or(false))
}
/// Update the total gas payment for a message to include gas_payment
fn update_gas_payment_for_message_id(&self, event: InterchainGasPayment) -> Result<()> {
fn update_gas_payment_for_message_id(&self, event: InterchainGasPayment) -> DbResult<()> {
let existing_payment = self.retrieve_gas_payment_for_message_id(event.message_id)?;
let total = existing_payment + event;
@ -254,7 +212,10 @@ impl HyperlaneDB {
}
/// Update the total gas spent for a message
fn update_gas_expenditure_for_message_id(&self, event: InterchainGasExpenditure) -> Result<()> {
fn update_gas_expenditure_for_message_id(
&self,
event: InterchainGasExpenditure,
) -> DbResult<()> {
let existing_payment = self.retrieve_gas_expenditure_for_message_id(event.message_id)?;
let total = existing_payment + event;
@ -272,7 +233,7 @@ impl HyperlaneDB {
pub fn retrieve_gas_payment_for_message_id(
&self,
message_id: H256,
) -> Result<InterchainGasPayment> {
) -> DbResult<InterchainGasPayment> {
Ok(self
.retrieve_keyed_decodable::<_, InterchainGasPaymentData>(
GAS_PAYMENT_FOR_MESSAGE_ID,
@ -286,7 +247,7 @@ impl HyperlaneDB {
pub fn retrieve_gas_expenditure_for_message_id(
&self,
message_id: H256,
) -> Result<InterchainGasExpenditure> {
) -> DbResult<InterchainGasExpenditure> {
Ok(self
.retrieve_keyed_decodable::<_, InterchainGasExpenditureData>(
GAS_EXPENDITURE_FOR_MESSAGE_ID,
@ -296,3 +257,66 @@ impl HyperlaneDB {
.complete(message_id))
}
}
#[async_trait]
impl HyperlaneLogStore<HyperlaneMessage> for HyperlaneRocksDB {
/// Store a list of dispatched messages and their associated metadata.
async fn store_logs(&self, messages: &[(HyperlaneMessage, LogMeta)]) -> Result<u32> {
let mut stored = 0;
for (message, meta) in messages {
let stored_message = self.store_message(message, meta.block_number)?;
if stored_message {
stored += 1;
}
}
Ok(stored)
}
}
#[async_trait]
impl HyperlaneLogStore<InterchainGasPayment> for HyperlaneRocksDB {
/// Store a list of interchain gas payments and their associated metadata.
async fn store_logs(&self, payments: &[(InterchainGasPayment, LogMeta)]) -> Result<u32> {
let mut new = 0;
for (payment, meta) in payments {
if self.process_gas_payment(*payment, meta)? {
new += 1;
}
}
Ok(new)
}
}
#[async_trait]
impl HyperlaneMessageStore for HyperlaneRocksDB {
/// Gets a message by nonce.
async fn retrieve_message_by_nonce(&self, nonce: u32) -> Result<Option<HyperlaneMessage>> {
let message = self.message_by_nonce(nonce)?;
Ok(message)
}
/// Retrieve dispatched block number by message nonce
async fn retrieve_dispatched_block_number(&self, nonce: u32) -> Result<Option<u64>> {
let number = self.retrieve_keyed_decodable(MESSAGE_DISPATCHED_BLOCK_NUMBER, &nonce)?;
Ok(number)
}
}
/// Note that for legacy reasons this watermark may be shared across multiple cursors, some of which may not have anything to do with gas payments
/// The high watermark cursor is relatively conservative in writing block numbers, so this shouldn't result in any events being missed.
#[async_trait]
impl<T> HyperlaneWatermarkedLogStore<T> for HyperlaneRocksDB
where
HyperlaneRocksDB: HyperlaneLogStore<T>,
{
/// Gets the block number high watermark
async fn retrieve_high_watermark(&self) -> Result<Option<u32>> {
let watermark = self.retrieve_decodable("", LATEST_INDEXED_GAS_PAYMENT_BLOCK)?;
Ok(watermark)
}
/// Stores the block number high watermark
async fn store_high_watermark(&self, block_number: u32) -> Result<()> {
let result = self.store_encodable("", LATEST_INDEXED_GAS_PAYMENT_BLOCK, &block_number)?;
Ok(result)
}
}

@ -0,0 +1,106 @@
use std::path::PathBuf;
use std::{io, path::Path, sync::Arc};
use hyperlane_core::HyperlaneProtocolError;
use rocksdb::{Options, DB as Rocks};
use tracing::info;
pub use hyperlane_db::*;
pub use typed_db::*;
/// Shared functionality surrounding use of rocksdb
pub mod iterator;
/// DB operations tied to specific Mailbox
mod hyperlane_db;
/// Type-specific db operations
mod typed_db;
/// Internal-use storage types.
mod storage_types;
/// Database test utilities.
#[cfg(any(test, feature = "test-utils"))]
pub mod test_utils;
#[derive(Debug, Clone)]
/// A KV Store
pub struct DB(Arc<Rocks>);
impl From<Rocks> for DB {
fn from(rocks: Rocks) -> Self {
Self(Arc::new(rocks))
}
}
/// DB Error type
#[derive(thiserror::Error, Debug)]
pub enum DbError {
/// Rocks DB Error
#[error("{0}")]
RockError(#[from] rocksdb::Error),
#[error("Failed to open {path}, canonicalized as {canonicalized}: {source}")]
/// Error opening the database
OpeningError {
/// Rocksdb error during opening
#[source]
source: rocksdb::Error,
/// Raw database path provided
path: PathBuf,
/// Parsed path used
canonicalized: PathBuf,
},
/// Could not parse the provided database path string
#[error("Invalid database path supplied {1:?}; {0}")]
InvalidDbPath(#[source] io::Error, String),
/// Hyperlane Error
#[error("{0}")]
HyperlaneError(#[from] HyperlaneProtocolError),
}
type Result<T> = std::result::Result<T, DbError>;
impl DB {
/// Opens db at `db_path` and creates if missing
#[tracing::instrument(err)]
pub fn from_path(db_path: &Path) -> Result<DB> {
let path = {
let mut path = db_path
.parent()
.unwrap_or(Path::new("."))
.canonicalize()
.map_err(|e| DbError::InvalidDbPath(e, db_path.to_string_lossy().into()))?;
if let Some(file_name) = db_path.file_name() {
path.push(file_name);
}
path
};
if path.is_dir() {
info!(path=%path.to_string_lossy(), "Opening existing db")
} else {
info!(path=%path.to_string_lossy(), "Creating db")
}
let mut opts = Options::default();
opts.create_if_missing(true);
Rocks::open(&opts, &path)
.map_err(|e| DbError::OpeningError {
source: e,
path: db_path.into(),
canonicalized: path,
})
.map(Into::into)
}
/// Store a value in the DB
pub fn store(&self, key: &[u8], value: &[u8]) -> Result<()> {
Ok(self.0.put(key, value)?)
}
/// Retrieve a value from the DB
pub fn retrieve(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
Ok(self.0.get(key)?)
}
}

@ -30,16 +30,19 @@ where
#[cfg(test)]
mod test {
use hyperlane_core::{HyperlaneDomain, HyperlaneMessage, RawHyperlaneMessage, H256};
use hyperlane_core::{
HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage, LogMeta, RawHyperlaneMessage, H256,
U256,
};
use crate::db::HyperlaneDB;
use crate::db::HyperlaneRocksDB;
use super::*;
#[tokio::test]
async fn db_stores_and_retrieves_messages() {
run_test_db(|db| async move {
let db = HyperlaneDB::new(
let db = HyperlaneRocksDB::new(
&HyperlaneDomain::new_test_domain("db_stores_and_retrieves_messages"),
db,
);
@ -53,8 +56,16 @@ mod test {
recipient: H256::from_low_u64_be(5),
body: vec![1, 2, 3],
};
let meta = LogMeta {
address: H256::from_low_u64_be(1),
block_number: 1,
block_hash: H256::from_low_u64_be(1),
transaction_hash: H256::from_low_u64_be(1),
transaction_index: 0,
log_index: U256::from(0),
};
db.store_message(&m).unwrap();
db.store_logs(&vec![(m.clone(), meta)]).await.unwrap();
let by_id = db.message_by_id(m.id()).unwrap().unwrap();
assert_eq!(

@ -1,51 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
use derive_new::new;
use eyre::Result;
use tokio::task::JoinHandle;
use tracing::{info_span, instrument::Instrumented, Instrument};
use hyperlane_core::{InterchainGasPaymaster, InterchainGasPaymasterIndexer};
use crate::{chains::IndexSettings, db::HyperlaneDB, ContractSync, ContractSyncMetrics};
/// Caching InterchainGasPaymaster type
#[derive(Debug, Clone, new)]
pub struct CachingInterchainGasPaymaster {
paymaster: Arc<dyn InterchainGasPaymaster>,
db: HyperlaneDB,
indexer: Arc<dyn InterchainGasPaymasterIndexer>,
}
impl CachingInterchainGasPaymaster {
/// Return handle on paymaster object
pub fn paymaster(&self) -> &Arc<dyn InterchainGasPaymaster> {
&self.paymaster
}
/// Return handle on HyperlaneDB
pub fn db(&self) -> &HyperlaneDB {
&self.db
}
/// Spawn a task that syncs the CachingInterchainGasPaymaster's db with the
/// on-chain event data
pub fn sync(
&self,
index_settings: IndexSettings,
metrics: ContractSyncMetrics,
) -> Instrumented<JoinHandle<Result<()>>> {
let sync = ContractSync::new(
self.paymaster.domain().clone(),
self.db.clone(),
self.indexer.clone(),
index_settings,
metrics,
);
tokio::spawn(async move { sync.sync_gas_payments().await }).instrument(
info_span!("InterchainGasPaymasterContractSync", self=%self.paymaster.domain()),
)
}
}

@ -21,19 +21,12 @@ pub use agent::*;
#[macro_use]
pub mod macros;
/// mailbox type
mod mailbox;
pub use mailbox::*;
mod metrics;
pub use metrics::*;
mod contract_sync;
pub use contract_sync::*;
mod interchain_gas;
pub use interchain_gas::*;
mod traits;
pub use traits::*;

@ -1,120 +0,0 @@
use std::fmt::Debug;
use std::num::NonZeroU64;
use std::sync::Arc;
use async_trait::async_trait;
use derive_new::new;
use tokio::task::JoinHandle;
use tracing::{info_span, instrument::Instrumented, Instrument};
use hyperlane_core::{
ChainResult, Checkpoint, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage,
HyperlaneProvider, Mailbox, MailboxIndexer, TxCostEstimate, TxOutcome, H256, U256,
};
use crate::{chains::IndexSettings, db::HyperlaneDB, ContractSync, ContractSyncMetrics};
/// Caching Mailbox type
#[derive(Debug, Clone, new)]
pub struct CachingMailbox {
mailbox: Arc<dyn Mailbox>,
db: HyperlaneDB,
indexer: Arc<dyn MailboxIndexer>,
}
impl CachingMailbox {
/// Return handle on mailbox object
pub fn mailbox(&self) -> &Arc<dyn Mailbox> {
&self.mailbox
}
/// Return handle on HyperlaneDB
pub fn db(&self) -> &HyperlaneDB {
&self.db
}
/// Spawn a task that syncs the CachingMailbox's db with the on-chain event
/// data
pub fn sync(
&self,
index_settings: IndexSettings,
metrics: ContractSyncMetrics,
) -> Instrumented<JoinHandle<eyre::Result<()>>> {
let sync = ContractSync::new(
self.mailbox.domain().clone(),
self.db.clone(),
self.indexer.clone(),
index_settings,
metrics,
);
tokio::spawn(async move { sync.sync_dispatched_messages().await })
.instrument(info_span!("MailboxContractSync", domain=%self.mailbox.domain()))
}
}
#[async_trait]
impl Mailbox for CachingMailbox {
fn domain_hash(&self) -> H256 {
self.mailbox.domain_hash()
}
async fn count(&self, maybe_lag: Option<NonZeroU64>) -> ChainResult<u32> {
self.mailbox.count(maybe_lag).await
}
/// Fetch the status of a message
async fn delivered(&self, id: H256) -> ChainResult<bool> {
self.mailbox.delivered(id).await
}
async fn latest_checkpoint(&self, maybe_lag: Option<NonZeroU64>) -> ChainResult<Checkpoint> {
self.mailbox.latest_checkpoint(maybe_lag).await
}
/// Fetch the current default interchain security module value
async fn default_ism(&self) -> ChainResult<H256> {
self.mailbox.default_ism().await
}
async fn recipient_ism(&self, recipient: H256) -> ChainResult<H256> {
self.mailbox.recipient_ism(recipient).await
}
async fn process(
&self,
message: &HyperlaneMessage,
metadata: &[u8],
tx_gas_limit: Option<U256>,
) -> ChainResult<TxOutcome> {
self.mailbox.process(message, metadata, tx_gas_limit).await
}
async fn process_estimate_costs(
&self,
message: &HyperlaneMessage,
metadata: &[u8],
) -> ChainResult<TxCostEstimate> {
self.mailbox.process_estimate_costs(message, metadata).await
}
fn process_calldata(&self, message: &HyperlaneMessage, metadata: &[u8]) -> Vec<u8> {
self.mailbox.process_calldata(message, metadata)
}
}
impl HyperlaneChain for CachingMailbox {
fn domain(&self) -> &HyperlaneDomain {
self.mailbox.domain()
}
fn provider(&self) -> Box<dyn HyperlaneProvider> {
self.mailbox.provider()
}
}
impl HyperlaneContract for CachingMailbox {
fn address(&self) -> H256 {
self.mailbox.address()
}
}

@ -3,23 +3,24 @@ use std::fmt::Debug;
use std::{collections::HashMap, sync::Arc};
use eyre::{eyre, Context, Result};
use futures_util::{future::try_join_all, TryFutureExt};
use futures_util::future::try_join_all;
use serde::Deserialize;
use hyperlane_core::{
config::*, HyperlaneChain, HyperlaneDomain, HyperlaneProvider, InterchainGasPaymaster,
InterchainGasPaymasterIndexer, Mailbox, MailboxIndexer, MultisigIsm, ValidatorAnnounce, H256,
config::*, Delivery, HyperlaneChain, HyperlaneDomain, HyperlaneMessageStore, HyperlaneProvider,
HyperlaneWatermarkedLogStore, InterchainGasPaymaster, InterchainGasPayment, Mailbox,
MultisigIsm, ValidatorAnnounce, H256,
};
use crate::db::{HyperlaneDB, DB};
use crate::{
settings::{
chains::{ChainConf, RawChainConf},
signers::SignerConf,
trace::TracingConfig,
},
CachingInterchainGasPaymaster, CachingMailbox, CoreMetrics, HyperlaneAgentCore, RawSignerConf,
CoreMetrics, HyperlaneAgentCore, RawSignerConf,
};
use crate::{ContractSync, ContractSyncMetrics, MessageContractSync, WatermarkContractSync};
/// Settings. Usually this should be treated as a base config and used as
/// follows:
@ -125,93 +126,6 @@ impl Settings {
}
}
/// Try to get a map of chain name -> mailbox contract
pub async fn build_all_mailboxes(
&self,
domains: impl Iterator<Item = &HyperlaneDomain>,
metrics: &CoreMetrics,
db: DB,
) -> Result<HashMap<HyperlaneDomain, CachingMailbox>> {
try_join_all(domains.map(|d| {
self.build_caching_mailbox(d, db.clone(), metrics)
.map_ok(|m| (m.domain().clone(), m))
}))
.await
.map(|vec| vec.into_iter().collect())
}
/// Try to get a map of chain name -> interchain gas paymaster contract
pub async fn build_all_interchain_gas_paymasters(
&self,
domains: impl Iterator<Item = &HyperlaneDomain>,
metrics: &CoreMetrics,
db: DB,
) -> Result<HashMap<HyperlaneDomain, CachingInterchainGasPaymaster>> {
try_join_all(domains.map(|d| {
self.build_caching_interchain_gas_paymaster(d, db.clone(), metrics)
.map_ok(|m| (m.paymaster().domain().clone(), m))
}))
.await
.map(|vec| vec.into_iter().collect())
}
/// Try to get a map of chain name -> validator announce contract
pub async fn build_all_validator_announces(
&self,
domains: impl Iterator<Item = &HyperlaneDomain>,
metrics: &CoreMetrics,
) -> Result<HashMap<HyperlaneDomain, Arc<dyn ValidatorAnnounce>>> {
Ok(
try_join_all(domains.map(|d| self.build_validator_announce(d, metrics)))
.await?
.into_iter()
.map(|va| (va.domain().clone(), va))
.collect(),
)
}
/// Try to get a CachingMailbox
async fn build_caching_mailbox(
&self,
domain: &HyperlaneDomain,
db: DB,
metrics: &CoreMetrics,
) -> Result<CachingMailbox> {
let mailbox = self
.build_mailbox(domain, metrics)
.await
.with_context(|| format!("Building mailbox for {domain}"))?;
let indexer = self
.build_mailbox_indexer(domain, metrics)
.await
.with_context(|| format!("Building mailbox indexer for {domain}"))?;
let hyperlane_db = HyperlaneDB::new(domain, db);
Ok(CachingMailbox::new(
mailbox.into(),
hyperlane_db,
indexer.into(),
))
}
/// Try to get a CachingInterchainGasPaymaster
async fn build_caching_interchain_gas_paymaster(
&self,
domain: &HyperlaneDomain,
db: DB,
metrics: &CoreMetrics,
) -> Result<CachingInterchainGasPaymaster> {
let interchain_gas_paymaster = self.build_interchain_gas_paymaster(domain, metrics).await?;
let indexer = self
.build_interchain_gas_paymaster_indexer(domain, metrics)
.await?;
let hyperlane_db = HyperlaneDB::new(domain, db);
Ok(CachingInterchainGasPaymaster::new(
interchain_gas_paymaster.into(),
hyperlane_db,
indexer.into(),
))
}
/// Try to get a MultisigIsm
pub async fn build_multisig_ism(
&self,
@ -225,20 +139,6 @@ impl Settings {
setup.build_multisig_ism(address, metrics).await
}
/// Try to get a ValidatorAnnounce
pub async fn build_validator_announce(
&self,
domain: &HyperlaneDomain,
metrics: &CoreMetrics,
) -> Result<Arc<dyn ValidatorAnnounce>> {
let setup = self.chain_setup(domain)?;
let announce = setup
.build_validator_announce(metrics)
.await
.with_context(|| format!("Building validator announce for {domain}"))?;
Ok(announce.into())
}
/// Try to get the chain configuration for the given domain.
pub fn chain_setup(&self, domain: &HyperlaneDomain) -> eyre::Result<&ChainConf> {
self.chains
@ -275,24 +175,82 @@ impl Settings {
}
/// Generate a call to ChainSetup for the given builder
macro_rules! delegate_fn {
($name:ident -> $ret:ty) => {
macro_rules! build_contract_fns {
($singular:ident, $plural:ident -> $ret:ty) => {
/// Delegates building to ChainSetup
pub async fn $singular(
&self,
domain: &HyperlaneDomain,
metrics: &CoreMetrics,
) -> eyre::Result<Box<$ret>> {
let setup = self.chain_setup(domain)?;
setup.$singular(metrics).await
}
/// Builds a contract for each domain
pub async fn $plural(
&self,
domains: impl Iterator<Item = &HyperlaneDomain>,
metrics: &CoreMetrics,
) -> Result<HashMap<HyperlaneDomain, Arc<$ret>>> {
try_join_all(domains.map(|d| self.$singular(d, metrics)))
.await?
.into_iter()
.map(|i| Ok((i.domain().clone(), Arc::from(i))))
.collect()
}
};
}
/// Generate a call to ChainSetup for the given builder
macro_rules! build_indexer_fns {
($singular:ident, $plural:ident -> $db:ty, $ret:ty) => {
/// Delegates building to ChainSetup
pub async fn $name(
pub async fn $singular(
&self,
domain: &HyperlaneDomain,
metrics: &CoreMetrics,
sync_metrics: &ContractSyncMetrics,
db: Arc<$db>,
) -> eyre::Result<Box<$ret>> {
let setup = self.chain_setup(domain)?;
setup.$name(metrics).await
let indexer = setup.$singular(metrics).await?;
let sync: $ret = ContractSync::new(
domain.clone(),
db.clone(),
indexer.into(),
sync_metrics.clone(),
);
Ok(Box::new(sync))
}
/// Builds a contract for each domain
pub async fn $plural(
&self,
domains: impl Iterator<Item = &HyperlaneDomain>,
metrics: &CoreMetrics,
sync_metrics: &ContractSyncMetrics,
dbs: HashMap<HyperlaneDomain, Arc<$db>>,
) -> Result<HashMap<HyperlaneDomain, Arc<$ret>>> {
try_join_all(
domains
.map(|d| self.$singular(d, metrics, sync_metrics, dbs.get(d).unwrap().clone())),
)
.await?
.into_iter()
.map(|i| Ok((i.domain().clone(), Arc::from(i))))
.collect()
}
};
}
impl Settings {
delegate_fn!(build_interchain_gas_paymaster -> dyn InterchainGasPaymaster);
delegate_fn!(build_interchain_gas_paymaster_indexer -> dyn InterchainGasPaymasterIndexer);
delegate_fn!(build_mailbox -> dyn Mailbox);
delegate_fn!(build_mailbox_indexer -> dyn MailboxIndexer);
delegate_fn!(build_provider -> dyn HyperlaneProvider);
build_contract_fns!(build_interchain_gas_paymaster, build_interchain_gas_paymasters -> dyn InterchainGasPaymaster);
build_contract_fns!(build_mailbox, build_mailboxes -> dyn Mailbox);
build_contract_fns!(build_validator_announce, build_validator_announces -> dyn ValidatorAnnounce);
build_contract_fns!(build_provider, build_providers -> dyn HyperlaneProvider);
build_indexer_fns!(build_delivery_indexer, build_delivery_indexers -> dyn HyperlaneWatermarkedLogStore<Delivery>, WatermarkContractSync<Delivery>);
build_indexer_fns!(build_message_indexer, build_message_indexers -> dyn HyperlaneMessageStore, MessageContractSync);
build_indexer_fns!(build_interchain_gas_payment_indexer, build_interchain_gas_payment_indexers -> dyn HyperlaneWatermarkedLogStore<InterchainGasPayment>, WatermarkContractSync<InterchainGasPayment>);
}

@ -9,8 +9,8 @@ use ethers_prometheus::middleware::{
};
use hyperlane_core::{
config::*, ContractLocator, HyperlaneAbi, HyperlaneDomain, HyperlaneDomainProtocol,
HyperlaneProvider, HyperlaneSigner, InterchainGasPaymaster, InterchainGasPaymasterIndexer,
InterchainSecurityModule, Mailbox, MailboxIndexer, MultisigIsm, RoutingIsm, ValidatorAnnounce,
HyperlaneProvider, HyperlaneSigner, Indexer, InterchainGasPaymaster, InterchainGasPayment,
InterchainSecurityModule, Mailbox, MessageIndexer, MultisigIsm, RoutingIsm, ValidatorAnnounce,
H160, H256,
};
use hyperlane_ethereum::{
@ -339,12 +339,12 @@ impl ChainConf {
.context(ctx)
}
/// Try to convert the chain settings into a mailbox indexer
pub async fn build_mailbox_indexer(
/// Try to convert the chain settings into a message indexer
pub async fn build_message_indexer(
&self,
metrics: &CoreMetrics,
) -> Result<Box<dyn MailboxIndexer>> {
let ctx = "Building mailbox indexer";
) -> Result<Box<dyn MessageIndexer>> {
let ctx = "Building delivery indexer";
let locator = self.locator(self.addresses.mailbox);
match &self.connection()? {
@ -353,7 +353,33 @@ impl ChainConf {
conf,
&locator,
metrics,
h_eth::MailboxIndexerBuilder {
h_eth::MessageIndexerBuilder {
finality_blocks: self.finality_blocks,
},
)
.await
}
ChainConnectionConf::Fuel(_) => todo!(),
}
.context(ctx)
}
/// Try to convert the chain settings into a delivery indexer
pub async fn build_delivery_indexer(
&self,
metrics: &CoreMetrics,
) -> Result<Box<dyn Indexer<H256>>> {
let ctx = "Building delivery indexer";
let locator = self.locator(self.addresses.mailbox);
match &self.connection()? {
ChainConnectionConf::Ethereum(conf) => {
self.build_ethereum(
conf,
&locator,
metrics,
h_eth::DeliveryIndexerBuilder {
finality_blocks: self.finality_blocks,
},
)
@ -390,11 +416,11 @@ impl ChainConf {
.context(ctx)
}
/// Try to convert the chain settings into a IGP indexer
pub async fn build_interchain_gas_paymaster_indexer(
/// Try to convert the chain settings into a gas payment indexer
pub async fn build_interchain_gas_payment_indexer(
&self,
metrics: &CoreMetrics,
) -> Result<Box<dyn InterchainGasPaymasterIndexer>> {
) -> Result<Box<dyn Indexer<InterchainGasPayment>>> {
let ctx = "Building IGP indexer";
let locator = self.locator(self.addresses.interchain_gas_paymaster);

@ -3,44 +3,16 @@ use std::time::Duration;
use async_trait::async_trait;
use auto_impl::auto_impl;
use crate::ChainResult;
use crate::{ChainResult, LogMeta};
/// Tool for handling the logic of what the next block range that should be
/// queried and may perform rate limiting on `next_range` queries.
/// A cursor governs event indexing for a contract.
#[async_trait]
#[auto_impl(Box)]
pub trait SyncBlockRangeCursor {
/// Returns the current `from` position of the indexer. Note that
/// `next_range` may return a `from` value that is lower than this in order
/// to have some overlap.
fn current_position(&self) -> u32;
/// Returns the current `tip` of the blockchain. This is the highest block
/// we know of.
fn tip(&self) -> u32;
/// Returns the current distance from the tip of the blockchain.
fn distance_from_tip(&self) -> u32 {
self.tip().saturating_sub(self.current_position())
}
/// Get the next block range `(from, to)` which should be fetched (this
/// returns an inclusive range such as (0,50), (51,100), ...). This
/// will automatically rate limit based on how far we are from the
/// highest block we can scrape according to
/// `get_finalized_block_number`.
///
/// In reality this will often return a from value that overlaps with the
/// previous range to help ensure that we scrape everything even if the
/// provider failed to respond in full previously.
///
/// This assumes the caller will call next_range again automatically on Err,
/// but it returns the error to allow for tailored logging or different end
/// cases.
pub trait ContractSyncCursor<T>: Send + Sync + 'static {
/// The next block range that should be queried.
async fn next_range(&mut self) -> ChainResult<(u32, u32, Duration)>;
/// If there was an issue when a range of data was fetched, this rolls back
/// so the next range fetched will be from `start_from`. Note that it is a
/// no-op if a later block value is specified.
fn backtrack(&mut self, start_from: u32);
/// Ingests the logs that were fetched from the chain, and adjusts the cursor
/// accordingly.
async fn update(&mut self, logs: Vec<(T, LogMeta)>) -> eyre::Result<()>;
}

@ -0,0 +1,36 @@
use std::fmt::Debug;
use async_trait::async_trait;
use auto_impl::auto_impl;
use eyre::Result;
use crate::{HyperlaneMessage, LogMeta};
/// Interface for a HyperlaneLogStore that ingests logs.
#[async_trait]
#[auto_impl(&, Box, Arc)]
pub trait HyperlaneLogStore<T>: Send + Sync + Debug {
/// Store a list of logs and their associated metadata
/// Returns the number of elements that were stored.
async fn store_logs(&self, logs: &[(T, LogMeta)]) -> Result<u32>;
}
/// Extension of HyperlaneLogStore trait that supports getting the block number at which a known message was dispatched.
#[async_trait]
#[auto_impl(&, Box, Arc)]
pub trait HyperlaneMessageStore: HyperlaneLogStore<HyperlaneMessage> {
/// Gets a message by nonce.
async fn retrieve_message_by_nonce(&self, nonce: u32) -> Result<Option<HyperlaneMessage>>;
/// Gets the block number at which a message was dispatched.
async fn retrieve_dispatched_block_number(&self, nonce: u32) -> Result<Option<u64>>;
}
/// Extension of HyperlaneLogStore trait that supports a high watermark for the highest indexed block number.
#[async_trait]
#[auto_impl(&, Box, Arc)]
pub trait HyperlaneWatermarkedLogStore<T>: HyperlaneLogStore<T> {
/// Gets the block number high watermark
async fn retrieve_high_watermark(&self) -> Result<Option<u32>>;
/// Stores the block number high watermark
async fn store_high_watermark(&self, block_number: u32) -> Result<()>;
}

@ -9,12 +9,15 @@ use std::fmt::Debug;
use async_trait::async_trait;
use auto_impl::auto_impl;
use crate::{ChainResult, HyperlaneMessage, InterchainGasPayment, LogMeta, H256};
use crate::{ChainResult, HyperlaneMessage, LogMeta};
/// Interface for an indexer.
#[async_trait]
#[auto_impl(&, Box, Arc)]
pub trait Indexer: Send + Sync + Debug {
#[auto_impl(&, Box, Arc,)]
pub trait Indexer<T: Sized>: Send + Sync + Debug {
/// Fetch list of logs between blocks `from` and `to`, inclusive.
async fn fetch_logs(&self, from: u32, to: u32) -> ChainResult<Vec<(T, LogMeta)>>;
/// Get the chain's latest block number that has reached finality
async fn get_finalized_block_number(&self) -> ChainResult<u32>;
}
@ -23,32 +26,7 @@ pub trait Indexer: Send + Sync + Debug {
/// entities to retrieve chain-specific data from a mailbox.
#[async_trait]
#[auto_impl(&, Box, Arc)]
pub trait MailboxIndexer: Indexer {
/// Fetch list of outbound messages between blocks `from` and `to`, sorted
/// by nonce.
async fn fetch_sorted_messages(
&self,
from: u32,
to: u32,
) -> ChainResult<Vec<(HyperlaneMessage, LogMeta)>>;
/// Fetch a list of delivered message IDs between blocks `from` and `to`.
async fn fetch_delivered_messages(
&self,
from: u32,
to: u32,
) -> ChainResult<Vec<(H256, LogMeta)>>;
}
/// Interface for InterchainGasPaymaster contract indexer.
#[async_trait]
#[auto_impl(&, Box, Arc)]
pub trait InterchainGasPaymasterIndexer: Indexer {
/// Fetch list of gas payments between `from_block` and `to_block`,
/// inclusive
async fn fetch_gas_payments(
&self,
from_block: u32,
to_block: u32,
) -> ChainResult<Vec<(InterchainGasPayment, LogMeta)>>;
pub trait MessageIndexer: Indexer<HyperlaneMessage> + 'static {
/// Return the latest finalized mailbox count and block number
async fn fetch_count_at_tip(&self) -> ChainResult<(u32, u32)>;
}

@ -1,4 +1,5 @@
pub use cursor::*;
pub use db::*;
pub use deployed::*;
pub use encode::*;
pub use indexer::*;
@ -12,6 +13,7 @@ pub use signing::*;
pub use validator_announce::*;
mod cursor;
mod db;
mod deployed;
mod encode;
mod indexer;

@ -6,6 +6,9 @@ use crate::{Decode, Encode, HyperlaneProtocolError, H256};
const HYPERLANE_MESSAGE_PREFIX_LEN: usize = 77;
/// A message ID that has been delivered to the destination
pub type Delivery = H256;
/// A Stamped message that has been committed at some nonce
pub type RawHyperlaneMessage = Vec<u8>;

@ -1,40 +0,0 @@
#![allow(non_snake_case)]
use std::future::Future;
use std::time::Duration;
use async_trait::async_trait;
use mockall::mock;
use hyperlane_core::{ChainResult, SyncBlockRangeCursor};
mock! {
pub SyncBlockRangeCursor {
pub fn _next_range(&mut self) -> impl Future<Output=ChainResult<(u32, u32, Duration)>> + Send {}
pub fn _current_position(&self) -> u32 {}
pub fn _tip(&self) -> u32 {}
pub fn _backtrack(&mut self, start_from: u32) {}
}
}
#[async_trait]
impl SyncBlockRangeCursor for MockSyncBlockRangeCursor {
fn current_position(&self) -> u32 {
self._current_position()
}
fn tip(&self) -> u32 {
self._tip()
}
async fn next_range(&mut self) -> ChainResult<(u32, u32, Duration)> {
self._next_range().await
}
fn backtrack(&mut self, start_from: u32) {
self._backtrack(start_from)
}
}

@ -1,60 +0,0 @@
#![allow(non_snake_case)]
use async_trait::async_trait;
use mockall::*;
use hyperlane_core::{ChainResult, HyperlaneMessage, Indexer, LogMeta, MailboxIndexer, H256};
mock! {
pub Indexer {
pub fn _get_finalized_block_number(&self) -> ChainResult<u32> {}
pub fn _fetch_sorted_messages(&self, from: u32, to: u32) -> ChainResult<Vec<(HyperlaneMessage, LogMeta)>> {}
}
}
impl std::fmt::Debug for MockIndexer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "MockIndexer")
}
}
mock! {
pub HyperlaneIndexer {
pub fn _get_finalized_block_number(&self) -> ChainResult<u32> {}
pub fn _fetch_sorted_messages(&self, from: u32, to: u32) -> ChainResult<Vec<(HyperlaneMessage, LogMeta)>> {}
pub fn _fetch_delivered_messages(&self, from: u32, to: u32) -> ChainResult<Vec<(H256, LogMeta)>> {}
}
}
impl std::fmt::Debug for MockHyperlaneIndexer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "MockHyperlaneIndexer")
}
}
#[async_trait]
impl Indexer for MockHyperlaneIndexer {
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
self._get_finalized_block_number()
}
}
#[async_trait]
impl MailboxIndexer for MockHyperlaneIndexer {
async fn fetch_sorted_messages(
&self,
from: u32,
to: u32,
) -> ChainResult<Vec<(HyperlaneMessage, LogMeta)>> {
self._fetch_sorted_messages(from, to)
}
async fn fetch_delivered_messages(
&self,
from: u32,
to: u32,
) -> ChainResult<Vec<(H256, LogMeta)>> {
self._fetch_delivered_messages(from, to)
}
}

@ -1,11 +1,4 @@
/// Mock mailbox contract
pub mod mailbox;
/// Mock indexer
pub mod indexer;
/// Mock SyncBlockRangeCursor
pub mod cursor;
pub use indexer::MockIndexer;
pub use mailbox::MockMailboxContract;

@ -9,13 +9,13 @@
//! the test for. This
//! does not include the initial setup time. If this timeout is reached before
//! the end conditions are met, the test is a failure. Defaults to 10 min.
//! - `E2E_KATHY_ROUNDS`: Number of rounds to run kathy for. Defaults to 4 if CI
//! mode is enabled.
//! - `E2E_KATHY_MESSAGES`: Number of kathy messages to dispatch. Defaults to 16 if CI mode is enabled.
//! - `E2E_LOG_ALL`: Log all output instead of writing to log files. Defaults to
//! true if CI mode,
//! else false.
use std::{
collections::HashMap,
env,
fs::{self, File},
io::{BufRead, BufReader, BufWriter, Read, Write},
@ -36,16 +36,16 @@ use tempfile::tempdir;
/// These private keys are from hardhat/anvil's testing accounts.
const RELAYER_KEYS: &[&str] = &[
"8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61",
"f214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897",
"701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82",
"0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6",
"0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97",
"0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356",
];
/// These private keys are from hardhat/anvil's testing accounts.
/// These must be consistent with the ISM config for the test.
const VALIDATOR_KEYS: &[&str] = &[
"59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
"5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a",
"7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6",
"0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a",
"0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba",
"0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e",
];
static RUNNING: AtomicBool = AtomicBool::new(true);
@ -54,14 +54,36 @@ static RUNNING: AtomicBool = AtomicBool::new(true);
/// cleanup purposes at this time.
#[derive(Default)]
struct State {
build_log: PathBuf,
log_all: bool,
kathy: Option<Child>,
node: Option<Child>,
relayer: Option<Child>,
validators: Vec<Child>,
scraper: Option<Child>,
watchers: Vec<JoinHandle<()>>,
}
fn kill_scraper_postgres(build_log: &PathBuf, log_all: bool) {
build_cmd(
&["docker", "stop", "scraper-testnet-postgres"],
build_log,
log_all,
None,
None,
false,
);
build_cmd(
&["docker", "rm", "scraper-testnet-postgres"],
build_log,
log_all,
None,
None,
false,
);
}
impl Drop for State {
fn drop(&mut self) {
println!("Signaling children to stop...");
@ -71,6 +93,10 @@ impl Drop for State {
if let Some(mut c) = self.relayer.take() {
stop_child(&mut c);
}
if let Some(mut c) = self.scraper.take() {
stop_child(&mut c);
kill_scraper_postgres(&self.build_log, self.log_all);
}
for mut c in self.validators.drain(..) {
stop_child(&mut c);
}
@ -102,21 +128,13 @@ fn main() -> ExitCode {
.map(|k| k.parse::<u64>().unwrap())
.unwrap_or(60 * 10);
let kathy_rounds = {
let r = env::var("E2E_KATHY_ROUNDS")
let kathy_messages = {
let r = env::var("E2E_KATHY_MESSAGES")
.ok()
.map(|r| r.parse::<u64>().unwrap());
if ci_mode && r.is_none() {
Some(4)
} else {
r
}
r.unwrap_or(16)
};
// NOTE: This is defined within the Kathy script and could potentially drift.
// TODO: Plumb via environment variable or something.
let kathy_messages_per_round = 10;
let log_all = env::var("E2E_LOG_ALL")
.map(|k| k.parse::<bool>().unwrap())
.unwrap_or(ci_mode);
@ -132,15 +150,6 @@ fn main() -> ExitCode {
}
let build_log = concat_path(&log_dir, "build.log");
let hardhat_log = concat_path(&log_dir, "hardhat.stdout.log");
let relayer_stdout_log = concat_path(&log_dir, "relayer.stdout.log");
let relayer_stderr_log = concat_path(&log_dir, "relayer.stderr.log");
let validator_stdout_logs = (1..=3)
.map(|i| concat_path(&log_dir, format!("validator{i}.stdout.log")))
.collect::<Vec<_>>();
let validator_stderr_logs = (1..=3)
.map(|i| concat_path(&log_dir, format!("validator{i}.stderr.log")))
.collect::<Vec<_>>();
let kathy_log = concat_path(&log_dir, "kathy.stdout.log");
let checkpoints_dirs = (0..3).map(|_| tempdir().unwrap()).collect::<Vec<_>>();
let rocks_db_dir = tempdir().unwrap();
@ -176,7 +185,7 @@ fn main() -> ExitCode {
];
let validator_envs: Vec<_> = (0..3).map(|i| {
let metrics_port = make_static((9093 + i).to_string());
let metrics_port = make_static((9094 + i).to_string());
let originchainname = make_static(format!("test{}", 1 + i));
hashmap! {
"HYP_BASE_CHAINS_TEST1_CONNECTION_URLS" => "http://127.0.0.1:8545,http://127.0.0.1:8545,http://127.0.0.1:8545",
@ -194,6 +203,18 @@ fn main() -> ExitCode {
}
}).collect();
let scraper_env = hashmap! {
"HYP_BASE_CHAINS_TEST1_CONNECTION_TYPE" => "httpQuorum",
"HYP_BASE_CHAINS_TEST1_CONNECTION_URL" => "http://127.0.0.1:8545",
"HYP_BASE_CHAINS_TEST2_CONNECTION_TYPE" => "httpQuorum",
"HYP_BASE_CHAINS_TEST2_CONNECTION_URL" => "http://127.0.0.1:8545",
"HYP_BASE_CHAINS_TEST3_CONNECTION_TYPE" => "httpQuorum",
"HYP_BASE_CHAINS_TEST3_CONNECTION_URL" => "http://127.0.0.1:8545",
"HYP_BASE_CHAINSTOSCRAPE" => "test1,test2,test3",
"HYP_BASE_METRICS" => "9093",
"HYP_BASE_DB"=>"postgresql://postgres:47221c18c610@localhost:5432/postgres",
};
if !log_all {
println!("Logs in {}", log_dir.display());
}
@ -209,7 +230,7 @@ fn main() -> ExitCode {
let build_cmd = {
let build_log = make_static(build_log.to_str().unwrap().into());
move |cmd, path| build_cmd(cmd, build_log, log_all, path)
move |cmd, path, env| build_cmd(cmd, build_log, log_all, path, env, true)
};
// this task takes a long time in the CI so run it in parallel
@ -226,21 +247,51 @@ fn main() -> ExitCode {
"relayer",
"--bin",
"validator",
"--bin",
"scraper",
"--bin",
"init-db",
],
None,
None,
);
})
};
println!("Running postgres db...");
let postgres_env = hashmap! {
"DATABASE_URL"=>"postgresql://postgres:47221c18c610@localhost:5432/postgres",
};
kill_scraper_postgres(&build_log, log_all);
build_cmd(
&[
"docker",
"run",
"--name",
"scraper-testnet-postgres",
"-e",
"POSTGRES_PASSWORD=47221c18c610",
"-p",
"5432:5432",
"-d",
"postgres:14",
],
None,
Some(&postgres_env),
);
println!("Installing typescript dependencies...");
build_cmd(&["yarn", "install"], Some("../"));
build_cmd(&["yarn", "install"], Some("../"), None);
if !is_ci_env {
// don't need to clean in the CI
build_cmd(&["yarn", "clean"], Some("../"));
build_cmd(&["yarn", "clean"], Some("../"), None);
}
build_cmd(&["yarn", "build"], Some("../"));
build_cmd(&["yarn", "build:e2e"], Some("../"), None);
let mut state = State::default();
state.build_log = build_log;
state.log_all = log_all;
println!("Launching hardhat...");
let mut node = Command::new("yarn");
node.args(["hardhat", "node"])
@ -253,124 +304,122 @@ fn main() -> ExitCode {
node.stdout(append_to(hardhat_log));
}
let node = node.spawn().expect("Failed to start node");
// if log_all {
// let output = node.stdout.take().unwrap();
// state
// .watchers
// .push(spawn(move || prefix_log(output, "ETH")))
// }
state.node = Some(node);
sleep(Duration::from_secs(10));
println!("Deploying hyperlane ism contracts...");
build_cmd(&["yarn", "deploy-ism"], Some("../typescript/infra"));
build_cmd(&["yarn", "deploy-ism"], Some("../typescript/infra"), None);
println!("Rebuilding sdk...");
build_cmd(&["yarn", "build"], Some("../typescript/sdk"));
build_cmd(&["yarn", "build"], Some("../typescript/sdk"), None);
println!("Deploying hyperlane core contracts...");
build_cmd(&["yarn", "deploy-core"], Some("../typescript/infra"));
build_cmd(&["yarn", "deploy-core"], Some("../typescript/infra"), None);
println!("Deploying hyperlane igp contracts...");
build_cmd(&["yarn", "deploy-igp"], Some("../typescript/infra"));
build_cmd(&["yarn", "deploy-igp"], Some("../typescript/infra"), None);
if !is_ci_env {
// Follow-up 'yarn hardhat node' invocation with 'yarn prettier' to fixup
// formatting on any autogenerated json config files to avoid any diff creation.
build_cmd(&["yarn", "prettier"], Some("../"));
build_cmd(&["yarn", "prettier"], Some("../"), None);
}
// Rebuild the SDK to pick up the deployed contracts
println!("Rebuilding sdk...");
build_cmd(&["yarn", "build"], Some("../typescript/sdk"));
build_cmd(&["yarn", "build"], Some("../typescript/sdk"), None);
build_rust.join().unwrap();
println!("Spawning relayer...");
let mut relayer = Command::new("target/debug/relayer")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.envs(&common_env)
.envs(&relayer_env)
.args(relayer_args)
.spawn()
.expect("Failed to start relayer");
let relayer_stdout = relayer.stdout.take().unwrap();
state.watchers.push(spawn(move || {
if log_all {
prefix_log(relayer_stdout, "RLY")
} else {
inspect_and_write_to_file(
relayer_stdout,
relayer_stdout_log,
&["ERROR", "message successfully processed"],
)
}
}));
let relayer_stderr = relayer.stderr.take().unwrap();
state.watchers.push(spawn(move || {
if log_all {
prefix_log(relayer_stderr, "RLY")
} else {
inspect_and_write_to_file(relayer_stderr, relayer_stderr_log, &[])
}
}));
state.relayer = Some(relayer);
println!("Init postgres db...");
build_cmd(
&["cargo", "run", "-r", "-p", "migration", "--bin", "init-db"],
None,
None,
);
let (scraper, scraper_stdout, scraper_stderr) = run_agent(
"scraper",
&scraper_env.into_iter().chain(common_env.clone()).collect(),
&[],
"SCR",
log_all,
&log_dir,
);
state.watchers.push(scraper_stdout);
state.watchers.push(scraper_stderr);
state.scraper = Some(scraper);
for (i, validator_env) in validator_envs.iter().enumerate() {
println!("Spawning validator for test{}", 1 + i);
let mut validator = Command::new("target/debug/validator")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.envs(&common_env)
.envs(validator_env)
.spawn()
.expect("Failed to start validator");
let validator_stdout = validator.stdout.take().unwrap();
let validator_stdout_log = validator_stdout_logs[i].clone();
let log_prefix = make_static(format!("VAL{}", 1 + i));
state.watchers.push(spawn(move || {
if log_all {
prefix_log(validator_stdout, log_prefix)
} else {
inspect_and_write_to_file(validator_stdout, validator_stdout_log, &["ERROR"])
}
}));
let validator_stderr = validator.stderr.take().unwrap();
let validator_stderr_log = validator_stderr_logs[i].clone();
state.watchers.push(spawn(move || {
if log_all {
prefix_log(validator_stderr, log_prefix)
} else {
inspect_and_write_to_file(validator_stderr, &validator_stderr_log, &[])
}
}));
let (validator, validator_stdout, validator_stderr) = run_agent(
"validator",
&common_env
.clone()
.into_iter()
.chain(validator_env.clone())
.collect(),
&[],
make_static(format!("VAL{}", 1 + i)),
log_all,
&log_dir,
);
state.watchers.push(validator_stdout);
state.watchers.push(validator_stderr);
state.validators.push(validator);
}
// Send half the kathy messages before the relayer comes up
let mut kathy = Command::new("yarn");
kathy
.arg("kathy")
.args([
"--messages",
&(kathy_messages / 2).to_string(),
"--timeout",
"1000",
])
.current_dir("../typescript/infra")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let (mut kathy, kathy_stdout, kathy_stderr) =
spawn_cmd_with_logging("kathy", kathy, "KTY", log_all, &log_dir);
state.watchers.push(kathy_stdout);
state.watchers.push(kathy_stderr);
kathy.wait().unwrap();
let (relayer, relayer_stdout, relayer_stderr) = run_agent(
"relayer",
&relayer_env.into_iter().chain(common_env.clone()).collect(),
&relayer_args,
"RLY",
log_all,
&log_dir,
);
state.watchers.push(relayer_stdout);
state.watchers.push(relayer_stderr);
state.relayer = Some(relayer);
println!("Setup complete! Agents running in background...");
println!("Ctrl+C to end execution...");
println!("Spawning Kathy to send Hyperlane message traffic...");
// Send half the kathy messages after the relayer comes up
let mut kathy = Command::new("yarn");
kathy.arg("kathy");
if let Some(r) = kathy_rounds {
kathy.args(["--rounds", &r.to_string()]);
}
let mut kathy = kathy
kathy
.arg("kathy")
.args([
"--messages",
&(kathy_messages / 2).to_string(),
"--timeout",
"1000",
])
.current_dir("../typescript/infra")
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start kathy");
let kathy_stdout = kathy.stdout.take().unwrap();
state.watchers.push(spawn(move || {
if log_all {
prefix_log(kathy_stdout, "KTY")
} else {
inspect_and_write_to_file(kathy_stdout, kathy_log, &["send"])
}
}));
.stderr(Stdio::piped());
let (kathy, kathy_stdout, kathy_stderr) =
spawn_cmd_with_logging("kathy", kathy, "KTY", log_all, &log_dir);
state.watchers.push(kathy_stdout);
state.watchers.push(kathy_stderr);
state.kathy = Some(kathy);
let loop_start = Instant::now();
@ -392,10 +441,10 @@ fn main() -> ExitCode {
}
if ci_mode {
// for CI we have to look for the end condition.
let num_messages_expected = kathy_rounds.unwrap() as u32 * kathy_messages_per_round;
let num_messages_expected = (kathy_messages / 2) as u32 * 2;
if kathy_done && termination_invariants_met(num_messages_expected) {
// end condition reached successfully
println!("Kathy completed successfully and the retry queues are empty");
println!("Kathy completed successfully and agent metrics look healthy");
break;
} else if (Instant::now() - loop_start).as_secs() > ci_mode_timeout {
// we ran out of time
@ -413,19 +462,27 @@ fn main() -> ExitCode {
ExitCode::from(0)
}
/// Use the metrics to check if the relayer queues are empty and the expected
/// number of messages have been sent.
fn termination_invariants_met(num_expected_messages_processed: u32) -> bool {
let lengths: Vec<_> = ureq::get("http://127.0.0.1:9092/metrics")
.call()
fn fetch_metric(port: &str, metric: &str, labels: &HashMap<&str, &str>) -> Vec<u32> {
let resp = ureq::get(&format!("http://127.0.0.1:{}/metrics", port));
resp.call()
.unwrap()
.into_string()
.unwrap()
.lines()
.filter(|l| l.starts_with("hyperlane_submitter_queue_length"))
.filter(|l| !l.contains("queue_name=\"confirm_queue\""))
.filter(|l| l.starts_with(metric))
.filter(|l| {
labels
.iter()
.all(|(k, v)| l.contains(&format!("{}=\"{}\"", k, v)))
})
.map(|l| l.rsplit_once(' ').unwrap().1.parse::<u32>().unwrap())
.collect();
.collect()
}
/// Use the metrics to check if the relayer queues are empty and the expected
/// number of messages have been sent.
fn termination_invariants_met(num_expected_messages: u32) -> bool {
let lengths = fetch_metric("9092", "hyperlane_submitter_queue_length", &hashmap! {});
assert!(!lengths.is_empty(), "Could not find queue length metric");
if lengths.into_iter().any(|n| n != 0) {
println!("<E2E> Relayer queues not empty");
@ -434,37 +491,78 @@ fn termination_invariants_met(num_expected_messages_processed: u32) -> bool {
// Also ensure the counter is as expected (total number of messages), summed
// across all mailboxes.
let msg_processed_count: Vec<_> = ureq::get("http://127.0.0.1:9092/metrics")
.call()
.unwrap()
.into_string()
.unwrap()
.lines()
.filter(|l| l.starts_with("hyperlane_messages_processed_count"))
.map(|l| l.rsplit_once(' ').unwrap().1.parse::<u32>().unwrap())
.collect();
assert!(
!msg_processed_count.is_empty(),
"Could not find message_processed phase metric"
);
if msg_processed_count.into_iter().sum::<u32>() < num_expected_messages_processed {
println!("<E2E> Not all messages have been processed");
let msg_processed_count =
fetch_metric("9092", "hyperlane_messages_processed_count", &hashmap! {})
.iter()
.sum::<u32>();
if msg_processed_count != num_expected_messages {
println!(
"<E2E> Relayer has {} processed messages, expected {}",
msg_processed_count, num_expected_messages
);
return false;
}
let gas_payment_events_count = ureq::get("http://127.0.0.1:9092/metrics")
.call()
.unwrap()
.into_string()
.unwrap()
.lines()
.filter(|l| l.starts_with("hyperlane_contract_sync_stored_events"))
.filter(|l| l.contains(r#"data_type="gas_payments""#))
.map(|l| l.rsplit_once(' ').unwrap().1.parse::<u32>().unwrap())
.sum::<u32>();
let gas_payment_events_count = fetch_metric(
"9092",
"hyperlane_contract_sync_stored_events",
&hashmap! {"data_type" => "gas_payments"},
)
.iter()
.sum::<u32>();
// TestSendReceiver randomly breaks gas payments up into
// two. So we expect at least as many gas payments as messages.
if gas_payment_events_count < num_expected_messages {
println!(
"<E2E> Relayer has {} gas payment events, expected at least {}",
gas_payment_events_count, num_expected_messages
);
return false;
}
let dispatched_messages_scraped = fetch_metric(
"9093",
"hyperlane_contract_sync_stored_events",
&hashmap! {"data_type" => "message_dispatch"},
)
.iter()
.sum::<u32>();
if dispatched_messages_scraped != num_expected_messages {
println!(
"<E2E> Scraper has scraped {} dispatched messages, expected {}",
dispatched_messages_scraped, num_expected_messages
);
return false;
}
let delivered_messages_scraped = fetch_metric(
"9093",
"hyperlane_contract_sync_stored_events",
&hashmap! {"data_type" => "message_delivery"},
)
.iter()
.sum::<u32>();
if delivered_messages_scraped != num_expected_messages {
println!(
"<E2E> Scraper has scraped {} delivered messages, expected {}",
delivered_messages_scraped, num_expected_messages
);
return false;
}
if gas_payment_events_count < num_expected_messages_processed {
println!("<E2E> Missing gas payment events");
let gas_payments_scraped = fetch_metric(
"9093",
"hyperlane_contract_sync_stored_events",
&hashmap! {"data_type" => "gas_payment"},
)
.iter()
.sum::<u32>();
// The relayer and scraper should have the same number of gas payments.
if gas_payments_scraped != gas_payment_events_count {
println!(
"<E2E> Scraper has scraped {} gas payments, expected {}",
gas_payments_scraped, num_expected_messages
);
false
} else {
true
@ -559,7 +657,14 @@ fn append_to(p: impl AsRef<Path>) -> File {
.expect("Failed to open file")
}
fn build_cmd(cmd: &[&str], log: impl AsRef<Path>, log_all: bool, wd: Option<&str>) {
fn build_cmd(
cmd: &[&str],
log: impl AsRef<Path>,
log_all: bool,
wd: Option<&str>,
env: Option<&HashMap<&str, &str>>,
assert_success: bool,
) {
assert!(!cmd.is_empty(), "Must specify a command!");
let mut c = Command::new(cmd[0]);
c.args(&cmd[1..]);
@ -571,14 +676,72 @@ fn build_cmd(cmd: &[&str], log: impl AsRef<Path>, log_all: bool, wd: Option<&str
if let Some(wd) = wd {
c.current_dir(wd);
}
if let Some(env) = env {
c.envs(env);
}
let status = c.status().expect("Failed to run command");
assert!(
status.success(),
"Command returned non-zero exit code: {}",
cmd.join(" ")
);
if assert_success {
assert!(
status.success(),
"Command returned non-zero exit code: {}",
cmd.join(" ")
);
}
}
fn make_static(s: String) -> &'static str {
Box::leak(s.into_boxed_str())
}
fn spawn_cmd_with_logging(
name: &str,
mut command: Command,
log_prefix: &'static str,
log_all: bool,
log_dir: &PathBuf,
) -> (std::process::Child, JoinHandle<()>, JoinHandle<()>) {
println!("Spawning {}...", name);
let mut child = command
.spawn()
.unwrap_or_else(|_| panic!("Failed to start {}", name));
let stdout_path = concat_path(log_dir, format!("{}.stdout.log", log_prefix));
let child_stdout = child.stdout.take().unwrap();
let stdout = spawn(move || {
if log_all {
prefix_log(child_stdout, log_prefix)
} else {
inspect_and_write_to_file(
child_stdout,
stdout_path,
&["ERROR", "message successfully processed"],
)
}
});
let stderr_path = concat_path(log_dir, format!("{}.stderr.log", log_prefix));
let child_stderr = child.stderr.take().unwrap();
let stderr = spawn(move || {
if log_all {
prefix_log(child_stderr, log_prefix)
} else {
inspect_and_write_to_file(child_stderr, stderr_path, &[])
}
});
(child, stdout, stderr)
}
fn run_agent(
name: &str,
env: &HashMap<&str, &str>,
args: &[&str],
log_prefix: &'static str,
log_all: bool,
log_dir: &PathBuf,
) -> (std::process::Child, JoinHandle<()>, JoinHandle<()>) {
let mut command = Command::new(format!("target/debug/{}", name));
command
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.envs(env)
.args(args);
spawn_cmd_with_logging(name, command, log_prefix, log_all, log_dir)
}

@ -2,20 +2,20 @@ import { ChainMap, ModuleType, MultisigIsmConfig } from '@hyperlane-xyz/sdk';
// the addresses here must line up with the e2e test's validator addresses
export const multisigIsm: ChainMap<MultisigIsmConfig> = {
// Validators are hardhat accounts 1-3
// Validators are anvil accounts 4-6
test1: {
type: ModuleType.MULTISIG,
validators: ['0x70997970c51812dc3a010c7d01b50e0d17dc79c8'],
validators: ['0x15d34aaf54267db7d7c367839aaf71a00a2c6a65'],
threshold: 1,
},
test2: {
type: ModuleType.MULTISIG,
validators: ['0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc'],
validators: ['0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc'],
threshold: 1,
},
test3: {
type: ModuleType.MULTISIG,
validators: ['0x90f79bf6eb2c4f870365e785982e1f101e93b906'],
validators: ['0x976ea74026e726554db657fa54763abd0c3a0aa9'],
threshold: 1,
},
};

@ -33,14 +33,14 @@ const chainSummary = async (core: HyperlaneCore, chain: ChainName) => {
task('kathy', 'Dispatches random hyperlane messages')
.addParam(
'rounds',
'Number of message sending rounds to perform; defaults to having no limit',
'messages',
'Number of messages to send; defaults to having no limit',
'0',
)
.addParam('timeout', 'Time to wait between rounds in ms.', '5000')
.addParam('timeout', 'Time to wait between messages in ms.', '5000')
.setAction(
async (
taskArgs: { rounds: string; timeout: string },
taskArgs: { messages: string; timeout: string },
hre: HardhatRuntimeEnvironment,
) => {
const timeout = Number.parseInt(taskArgs.timeout);
@ -60,38 +60,36 @@ task('kathy', 'Dispatches random hyperlane messages')
await recipient.deployTransaction.wait();
// Generate artificial traffic
let rounds = Number.parseInt(taskArgs.rounds) || 0;
const run_forever = rounds === 0;
while (run_forever || rounds-- > 0) {
const local = core.chains()[rounds % core.chains().length];
let messages = Number.parseInt(taskArgs.messages) || 0;
const run_forever = messages === 0;
while (run_forever || messages-- > 0) {
// Round robin origin chain
const local = core.chains()[messages % core.chains().length];
// Random remote chain
const remote: ChainName = randomElement(core.remoteChains(local));
const remoteId = multiProvider.getDomainId(remote);
const mailbox = core.getContracts(local).mailbox;
const igp = igps.getContracts(local).interchainGasPaymaster;
// Send a batch of messages to the destination chain to test
// the relayer submitting only greedily
for (let i = 0; i < 10; i++) {
await recipient.dispatchToSelf(
mailbox.address,
igp.address,
remoteId,
'0x1234',
{
value: interchainGasPayment,
// Some behavior is dependent upon the previous block hash
// so gas estimation may sometimes be incorrect. Just avoid
// estimation to avoid this.
gasLimit: 150_000,
},
);
console.log(
`send to ${recipient.address} on ${remote} via mailbox ${
mailbox.address
} on ${local} with nonce ${(await mailbox.count()) - 1}`,
);
console.log(await chainSummary(core, local));
await sleep(timeout);
}
await recipient.dispatchToSelf(
mailbox.address,
igp.address,
remoteId,
'0x1234',
{
value: interchainGasPayment,
// Some behavior is dependent upon the previous block hash
// so gas estimation may sometimes be incorrect. Just avoid
// estimation to avoid this.
gasLimit: 150_000,
},
);
console.log(
`send to ${recipient.address} on ${remote} via mailbox ${
mailbox.address
} on ${local} with nonce ${(await mailbox.count()) - 1}`,
);
console.log(await chainSummary(core, local));
await sleep(timeout);
}
},
);

Loading…
Cancel
Save