Sealevel igp indexing (#2585)

### Description
 
Depends on https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/2583

Indexes IGP payments related to the relayer's data pda address. Unless
this address is specified in the config (`sealevel.relayer_account `),
no filtering is applied and all IGP payments are stored in the local
database.

<!--
What's included in this PR?
-->

### Drive-by changes

- Sets `HYP_BASE_GASPAYMENTENFORCEMENT` in `run-locally` for the
relayer, to test that it correctly indexes the IGP payment before
submitting the message
- A new config section (`sealevel`) is added to the relayer
- The `MessageIndexer` trait is replaced with
`SequenceIndexer<HyperlaneMessage>`, renaming `fetch_count_at_tip` to
`sequence_at_tip`. `SequenceIndexer` is now common to both the message
and igp indexers.
- The `parse_addr` macro is modified so it can be reused when parsing
the sealevel relayer address config too
- `rust/utils/sealevel-test.bash` is included because I was using it to
test locally, but I can remove it if the sealevel e2e test already does
all the steps there @mattiecnvr
- Performs a `try_into` conversion that can be removed once
https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2610 is done

### Related issues

- Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2501


### Backward compatibility

<!--
Are these changes backward compatible? Are there any infrastructure
implications, e.g. changes that would prohibit deploying older commits
using this infra tooling?

Yes/No
-->

### Testing

<!--
What kind of testing have these changes undergone?
-->
e2e tests but the pipeline is failing, likely fixed by
https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/2602

---------

Co-authored-by: Trevor Porter <trkporter@ucdavis.edu>
trevor/try-fix-e2e
Daniel Savu 1 year ago committed by GitHub
parent 17a6e79ea5
commit 71e8988ccd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      rust/Cargo.lock
  2. 4
      rust/agents/relayer/src/relayer.rs
  3. 5
      rust/agents/validator/src/validator.rs
  4. 37
      rust/chains/hyperlane-ethereum/src/interchain_gas.rs
  5. 60
      rust/chains/hyperlane-ethereum/src/mailbox.rs
  6. 6
      rust/chains/hyperlane-fuel/src/interchain_gas.rs
  7. 11
      rust/chains/hyperlane-fuel/src/mailbox.rs
  8. 2
      rust/chains/hyperlane-sealevel/Cargo.toml
  9. 7
      rust/chains/hyperlane-sealevel/src/client.rs
  10. 291
      rust/chains/hyperlane-sealevel/src/interchain_gas.rs
  11. 48
      rust/chains/hyperlane-sealevel/src/mailbox.rs
  12. 13
      rust/chains/hyperlane-sealevel/src/utils.rs
  13. 4
      rust/config/test_sealevel_config.json
  14. 378
      rust/hyperlane-base/src/contract_sync/cursor.rs
  15. 20
      rust/hyperlane-base/src/contract_sync/mod.rs
  16. 37
      rust/hyperlane-base/src/settings/chains.rs
  17. 31
      rust/hyperlane-core/src/chain.rs
  18. 4
      rust/hyperlane-core/src/error.rs
  19. 6
      rust/hyperlane-core/src/traits/cursor.rs
  20. 24
      rust/hyperlane-core/src/traits/indexer.rs
  21. 1
      rust/sealevel/client/Cargo.toml
  22. 27
      rust/sealevel/client/src/context.rs
  23. 35
      rust/sealevel/client/src/core.rs
  24. 62
      rust/sealevel/client/src/main.rs
  25. 4
      rust/sealevel/programs/hyperlane-sealevel-igp-test/src/functional.rs
  26. 6
      rust/sealevel/programs/hyperlane-sealevel-igp/src/accounts.rs
  27. 1
      rust/sealevel/programs/hyperlane-sealevel-igp/src/processor.rs
  28. 1
      rust/sealevel/programs/hyperlane-sealevel-token-collateral/tests/functional.rs
  29. 1
      rust/sealevel/programs/hyperlane-sealevel-token-native/tests/functional.rs
  30. 1
      rust/sealevel/programs/hyperlane-sealevel-token/tests/functional.rs
  31. 2
      rust/utils/run-locally/Cargo.toml
  32. 1
      rust/utils/run-locally/src/config.rs
  33. 25
      rust/utils/run-locally/src/invariants.rs
  34. 28
      rust/utils/run-locally/src/main.rs
  35. 4
      rust/utils/run-locally/src/metrics.rs
  36. 30
      rust/utils/run-locally/src/solana.rs

5
rust/Cargo.lock generated

@ -3725,7 +3725,9 @@ dependencies = [
"async-trait",
"base64 0.21.2",
"borsh 0.9.3",
"derive-new",
"hyperlane-core",
"hyperlane-sealevel-igp",
"hyperlane-sealevel-interchain-security-module-interface",
"hyperlane-sealevel-mailbox",
"hyperlane-sealevel-message-recipient-interface",
@ -3772,6 +3774,7 @@ dependencies = [
"solana-client",
"solana-program",
"solana-sdk",
"solana-transaction-status",
]
[[package]]
@ -6249,9 +6252,11 @@ version = "0.1.0"
dependencies = [
"ctrlc",
"eyre",
"hyperlane-core",
"macro_rules_attribute",
"maplit",
"nix 0.26.2",
"regex",
"tempfile",
"ureq",
"which",

@ -260,7 +260,7 @@ impl Relayer {
&self,
origin: &HyperlaneDomain,
) -> Instrumented<JoinHandle<eyre::Result<()>>> {
let index_settings = self.as_ref().settings.chains[origin.name()].index.clone();
let index_settings = self.as_ref().settings.chains[origin.name()].index_settings();
let contract_sync = self.message_syncs.get(origin).unwrap().clone();
let cursor = contract_sync
.forward_backward_message_sync_cursor(index_settings)
@ -278,7 +278,7 @@ impl Relayer {
&self,
origin: &HyperlaneDomain,
) -> Instrumented<JoinHandle<eyre::Result<()>>> {
let index_settings = self.as_ref().settings.chains[origin.name()].index.clone();
let index_settings = self.as_ref().settings.chains[origin.name()].index_settings();
let contract_sync = self
.interchain_gas_payment_syncs
.get(origin)

@ -136,9 +136,8 @@ impl BaseAgent for Validator {
impl Validator {
async fn run_message_sync(&self) -> Instrumented<JoinHandle<Result<()>>> {
let index_settings = self.as_ref().settings.chains[self.origin_chain.name()]
.index
.clone();
let index_settings =
self.as_ref().settings.chains[self.origin_chain.name()].index_settings();
let contract_sync = self.message_sync.clone();
let cursor = contract_sync
.forward_backward_message_sync_cursor(index_settings)

@ -2,17 +2,17 @@
use std::collections::HashMap;
use std::fmt::Display;
use std::ops::RangeInclusive;
use std::sync::Arc;
use async_trait::async_trait;
use ethers::prelude::Middleware;
use tracing::instrument;
use hyperlane_core::{
BlockRange, ChainCommunicationError, ChainResult, ContractLocator, HyperlaneAbi,
HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, IndexRange, Indexer,
InterchainGasPaymaster, InterchainGasPayment, LogMeta, H160, H256,
ChainCommunicationError, ChainResult, ContractLocator, HyperlaneAbi, HyperlaneChain,
HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexer, InterchainGasPaymaster,
InterchainGasPayment, LogMeta, SequenceIndexer, H160, H256,
};
use tracing::instrument;
use crate::contracts::i_interchain_gas_paymaster::{
IInterchainGasPaymaster as EthereumInterchainGasPaymasterInternal, IINTERCHAINGASPAYMASTER_ABI,
@ -36,7 +36,7 @@ pub struct InterchainGasPaymasterIndexerBuilder {
#[async_trait]
impl BuildableWithProvider for InterchainGasPaymasterIndexerBuilder {
type Output = Box<dyn Indexer<InterchainGasPayment>>;
type Output = Box<dyn SequenceIndexer<InterchainGasPayment>>;
async fn build_with_provider<M: Middleware + 'static>(
&self,
@ -87,14 +87,8 @@ where
#[instrument(err, skip(self))]
async fn fetch_logs(
&self,
range: IndexRange,
range: RangeInclusive<u32>,
) -> ChainResult<Vec<(InterchainGasPayment, LogMeta)>> {
let BlockRange(range) = range else {
return Err(ChainCommunicationError::from_other_str(
"EthereumInterchainGasPaymasterIndexer only supports block-based indexing",
));
};
let events = self
.contract
.gas_payment_filter()
@ -130,6 +124,23 @@ where
}
}
#[async_trait]
impl<M> SequenceIndexer<InterchainGasPayment> for EthereumInterchainGasPaymasterIndexer<M>
where
M: Middleware + 'static,
{
async fn sequence_and_tip(&self) -> ChainResult<(Option<u32>, u32)> {
// The InterchainGasPaymasterIndexerBuilder must return a `SequenceIndexer` type.
// It's fine if only a blanket implementation is provided for EVM chains, since their
// indexing only uses the `Index` trait, which is a supertrait of `SequenceIndexer`.
// TODO: if `SequenceIndexer` turns out to not depend on `Indexer` at all, then the supertrait
// dependency could be removed, even if the builder would still need to return a type that is both
// ``SequenceIndexer` and `Indexer`.
let tip = self.get_finalized_block_number().await?;
Ok((None, tip))
}
}
pub struct InterchainGasPaymasterBuilder {}
#[async_trait]

@ -3,6 +3,7 @@
use std::collections::HashMap;
use std::num::NonZeroU64;
use std::ops::RangeInclusive;
use std::sync::Arc;
use async_trait::async_trait;
@ -14,10 +15,10 @@ use tracing::instrument;
use hyperlane_core::accumulator::incremental::IncrementalMerkle;
use hyperlane_core::accumulator::TREE_DEPTH;
use hyperlane_core::{
utils::fmt_bytes, BlockRange, ChainCommunicationError, ChainResult, Checkpoint,
ContractLocator, HyperlaneAbi, HyperlaneChain, HyperlaneContract, HyperlaneDomain,
HyperlaneMessage, HyperlaneProtocolError, HyperlaneProvider, IndexRange, Indexer, LogMeta,
Mailbox, MessageIndexer, RawHyperlaneMessage, TxCostEstimate, TxOutcome, H160, H256, U256,
utils::fmt_bytes, ChainCommunicationError, ChainResult, Checkpoint, ContractLocator,
HyperlaneAbi, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage,
HyperlaneProtocolError, HyperlaneProvider, Indexer, LogMeta, Mailbox, RawHyperlaneMessage,
SequenceIndexer, TxCostEstimate, TxOutcome, H160, H256, U256,
};
use crate::contracts::arbitrum_node_interface::ArbitrumNodeInterface;
@ -38,13 +39,13 @@ where
}
}
pub struct MessageIndexerBuilder {
pub struct SequenceIndexerBuilder {
pub finality_blocks: u32,
}
#[async_trait]
impl BuildableWithProvider for MessageIndexerBuilder {
type Output = Box<dyn MessageIndexer>;
impl BuildableWithProvider for SequenceIndexerBuilder {
type Output = Box<dyn SequenceIndexer<HyperlaneMessage>>;
async fn build_with_provider<M: Middleware + 'static>(
&self,
@ -65,7 +66,7 @@ pub struct DeliveryIndexerBuilder {
#[async_trait]
impl BuildableWithProvider for DeliveryIndexerBuilder {
type Output = Box<dyn Indexer<H256>>;
type Output = Box<dyn SequenceIndexer<H256>>;
async fn build_with_provider<M: Middleware + 'static>(
&self,
@ -130,13 +131,10 @@ where
}
#[instrument(err, skip(self))]
async fn fetch_logs(&self, range: IndexRange) -> ChainResult<Vec<(HyperlaneMessage, LogMeta)>> {
let BlockRange(range) = range else {
return Err(ChainCommunicationError::from_other_str(
"EthereumMailboxIndexer only supports block-based indexing",
))
};
async fn fetch_logs(
&self,
range: RangeInclusive<u32>,
) -> ChainResult<Vec<(HyperlaneMessage, LogMeta)>> {
let mut events: Vec<(HyperlaneMessage, LogMeta)> = self
.contract
.dispatch_filter()
@ -154,17 +152,17 @@ where
}
#[async_trait]
impl<M> MessageIndexer for EthereumMailboxIndexer<M>
impl<M> SequenceIndexer<HyperlaneMessage> for EthereumMailboxIndexer<M>
where
M: Middleware + 'static,
{
#[instrument(err, skip(self))]
async fn fetch_count_at_tip(&self) -> ChainResult<(u32, u32)> {
let tip = Indexer::<HyperlaneMessage>::get_finalized_block_number(self as _).await?;
async fn sequence_and_tip(&self) -> ChainResult<(Option<u32>, u32)> {
let tip = Indexer::<HyperlaneMessage>::get_finalized_block_number(self).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))
let sequence = call_at_tip.call().await?;
Ok((Some(sequence), tip))
}
}
@ -178,13 +176,7 @@ where
}
#[instrument(err, skip(self))]
async fn fetch_logs(&self, range: IndexRange) -> ChainResult<Vec<(H256, LogMeta)>> {
let BlockRange(range) = range else {
return Err(ChainCommunicationError::from_other_str(
"EthereumMailboxIndexer only supports block-based indexing",
))
};
async fn fetch_logs(&self, range: RangeInclusive<u32>) -> ChainResult<Vec<(H256, LogMeta)>> {
Ok(self
.contract
.process_id_filter()
@ -197,6 +189,20 @@ where
.collect())
}
}
#[async_trait]
impl<M> SequenceIndexer<H256> for EthereumMailboxIndexer<M>
where
M: Middleware + 'static,
{
async fn sequence_and_tip(&self) -> ChainResult<(Option<u32>, u32)> {
// A blanket implementation for this trait is fine for the EVM.
// TODO: Consider removing `Indexer` as a supertrait of `SequenceIndexer`
let tip = Indexer::<H256>::get_finalized_block_number(self).await?;
Ok((None, tip))
}
}
pub struct MailboxBuilder {}
#[async_trait]

@ -1,7 +1,9 @@
use std::ops::RangeInclusive;
use async_trait::async_trait;
use hyperlane_core::{
ChainResult, HyperlaneChain, HyperlaneContract, IndexRange, Indexer, InterchainGasPaymaster,
ChainResult, HyperlaneChain, HyperlaneContract, Indexer, InterchainGasPaymaster,
};
use hyperlane_core::{HyperlaneDomain, HyperlaneProvider, InterchainGasPayment, LogMeta, H256};
@ -35,7 +37,7 @@ pub struct FuelInterchainGasPaymasterIndexer {}
impl Indexer<InterchainGasPayment> for FuelInterchainGasPaymasterIndexer {
async fn fetch_logs(
&self,
range: IndexRange,
range: RangeInclusive<u32>,
) -> ChainResult<Vec<(InterchainGasPayment, LogMeta)>> {
todo!()
}

@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
use std::num::NonZeroU64;
use std::ops::RangeInclusive;
use async_trait::async_trait;
use fuels::prelude::{Bech32ContractId, WalletUnlocked};
@ -10,8 +11,7 @@ use tracing::instrument;
use hyperlane_core::{
utils::fmt_bytes, ChainCommunicationError, ChainResult, Checkpoint, ContractLocator,
HyperlaneAbi, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneMessage,
HyperlaneProvider, IndexRange, Indexer, LogMeta, Mailbox, TxCostEstimate, TxOutcome, H256,
U256,
HyperlaneProvider, Indexer, LogMeta, Mailbox, TxCostEstimate, TxOutcome, H256, U256,
};
use crate::{
@ -154,7 +154,10 @@ pub struct FuelMailboxIndexer {}
#[async_trait]
impl Indexer<HyperlaneMessage> for FuelMailboxIndexer {
async fn fetch_logs(&self, range: IndexRange) -> ChainResult<Vec<(HyperlaneMessage, LogMeta)>> {
async fn fetch_logs(
&self,
range: RangeInclusive<u32>,
) -> ChainResult<Vec<(HyperlaneMessage, LogMeta)>> {
todo!()
}
@ -165,7 +168,7 @@ impl Indexer<HyperlaneMessage> for FuelMailboxIndexer {
#[async_trait]
impl Indexer<H256> for FuelMailboxIndexer {
async fn fetch_logs(&self, range: IndexRange) -> ChainResult<Vec<(H256, LogMeta)>> {
async fn fetch_logs(&self, range: RangeInclusive<u32>) -> ChainResult<Vec<(H256, LogMeta)>> {
todo!()
}

@ -10,6 +10,7 @@ anyhow.workspace = true
async-trait.workspace = true
base64.workspace = true
borsh.workspace = true
derive-new.workspace = true
jsonrpc-core.workspace = true
num-traits.workspace = true
serde.workspace = true
@ -26,6 +27,7 @@ account-utils = { path = "../../sealevel/libraries/account-utils" }
hyperlane-core = { path = "../../hyperlane-core", features = ["solana"] }
hyperlane-sealevel-interchain-security-module-interface = { path = "../../sealevel/libraries/interchain-security-module-interface" }
hyperlane-sealevel-mailbox = { path = "../../sealevel/programs/mailbox", features = ["no-entrypoint"] }
hyperlane-sealevel-igp = { path = "../../sealevel/programs/hyperlane-sealevel-igp", features = ["no-entrypoint"] }
hyperlane-sealevel-message-recipient-interface = { path = "../../sealevel/libraries/message-recipient-interface" }
hyperlane-sealevel-multisig-ism-message-id = { path = "../../sealevel/programs/ism/multisig-ism-message-id", features = ["no-entrypoint"] }
hyperlane-sealevel-validator-announce = { path = "../../sealevel/programs/validator-announce", features = ["no-entrypoint"] }

@ -1,12 +1,17 @@
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_sdk::commitment_config::CommitmentConfig;
/// Kludge to implement Debug for RpcClient.
pub(crate) struct RpcClientWithDebug(RpcClient);
pub struct RpcClientWithDebug(RpcClient);
impl RpcClientWithDebug {
pub fn new(rpc_endpoint: String) -> Self {
Self(RpcClient::new(rpc_endpoint))
}
pub fn new_with_commitment(rpc_endpoint: String, commitment: CommitmentConfig) -> Self {
Self(RpcClient::new_with_commitment(rpc_endpoint, commitment))
}
}
impl std::fmt::Debug for RpcClientWithDebug {

@ -1,29 +1,79 @@
use async_trait::async_trait;
use hyperlane_core::{
ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, HyperlaneDomain,
HyperlaneProvider, IndexRange, Indexer, InterchainGasPaymaster, InterchainGasPayment, LogMeta,
H256,
config::StrOrIntParseError, ChainCommunicationError, ChainResult, ContractLocator,
HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexer,
InterchainGasPaymaster, InterchainGasPayment, LogMeta, SequenceIndexer, H256, H512,
};
use hyperlane_sealevel_igp::{
accounts::{GasPaymentAccount, ProgramDataAccount},
igp_gas_payment_pda_seeds, igp_program_data_pda_seeds,
};
use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig};
use solana_client::{
rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType},
};
use std::ops::RangeInclusive;
use tracing::{info, instrument};
use crate::{ConnectionConf, SealevelProvider};
use solana_sdk::pubkey::Pubkey;
use crate::{
client::RpcClientWithDebug, utils::get_finalized_block_number, ConnectionConf, SealevelProvider,
};
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
use derive_new::new;
/// The offset to get the `unique_gas_payment_pubkey` field from the serialized GasPaymentData
const UNIQUE_GAS_PAYMENT_PUBKEY_OFFSET: usize = 1 + 8 + 8 + 32 + 4 + 32 + 8 + 8;
/// A reference to an IGP contract on some Sealevel chain
#[derive(Debug)]
pub struct SealevelInterchainGasPaymaster {
program_id: Pubkey,
data_pda_pubkey: Pubkey,
domain: HyperlaneDomain,
igp_account: H256,
}
impl SealevelInterchainGasPaymaster {
/// Create a new Sealevel IGP.
pub fn new(_conf: &ConnectionConf, locator: ContractLocator) -> Self {
let program_id = Pubkey::from(<[u8; 32]>::from(locator.address));
Self {
pub async fn new(
conf: &ConnectionConf,
igp_account_locator: &ContractLocator<'_>,
) -> ChainResult<Self> {
let rpc_client = RpcClientWithDebug::new_with_commitment(
conf.url.to_string(),
CommitmentConfig::processed(),
);
let program_id =
Self::determine_igp_program_id(&rpc_client, &igp_account_locator.address).await?;
let (data_pda_pubkey, _) =
Pubkey::find_program_address(igp_program_data_pda_seeds!(), &program_id);
Ok(Self {
program_id,
domain: locator.domain.clone(),
}
data_pda_pubkey,
domain: igp_account_locator.domain.clone(),
igp_account: igp_account_locator.address,
})
}
async fn determine_igp_program_id(
rpc_client: &RpcClientWithDebug,
igp_account_pubkey: &H256,
) -> ChainResult<Pubkey> {
let account = rpc_client
.get_account_with_commitment(
&Pubkey::from(<[u8; 32]>::from(*igp_account_pubkey)),
CommitmentConfig::finalized(),
)
.await
.map_err(ChainCommunicationError::from_other)?
.value
.ok_or_else(|| {
ChainCommunicationError::from_other_str("Could not find IGP account for pubkey")
})?;
Ok(account.owner)
}
}
@ -47,12 +97,137 @@ impl InterchainGasPaymaster for SealevelInterchainGasPaymaster {}
/// Struct that retrieves event data for a Sealevel IGP contract
#[derive(Debug)]
pub struct SealevelInterchainGasPaymasterIndexer {}
pub struct SealevelInterchainGasPaymasterIndexer {
rpc_client: RpcClientWithDebug,
igp: SealevelInterchainGasPaymaster,
}
/// IGP payment data on Sealevel
#[derive(Debug, new)]
pub struct SealevelGasPayment {
payment: InterchainGasPayment,
log_meta: LogMeta,
igp_account_pubkey: H256,
}
impl SealevelInterchainGasPaymasterIndexer {
/// Create a new Sealevel IGP indexer.
pub fn new(_conf: &ConnectionConf, _locator: ContractLocator) -> Self {
Self {}
pub async fn new(
conf: &ConnectionConf,
igp_account_locator: ContractLocator<'_>,
) -> ChainResult<Self> {
// Set the `processed` commitment at rpc level
let rpc_client = RpcClientWithDebug::new_with_commitment(
conf.url.to_string(),
CommitmentConfig::processed(),
);
let igp = SealevelInterchainGasPaymaster::new(conf, &igp_account_locator).await?;
Ok(Self { rpc_client, igp })
}
async fn get_payment_with_sequence(
&self,
sequence_number: u64,
) -> ChainResult<SealevelGasPayment> {
let payment_bytes = &[
&hyperlane_sealevel_igp::accounts::GAS_PAYMENT_DISCRIMINATOR[..],
&sequence_number.to_le_bytes()[..],
]
.concat();
#[allow(deprecated)]
let payment_bytes: String = base64::encode(payment_bytes);
// First, find all accounts with the matching gas payment data.
// To keep responses small in case there is ever more than 1
// match, we don't request the full account data, and just request
// the `unique_gas_payment_pubkey` field.
#[allow(deprecated)]
let memcmp = RpcFilterType::Memcmp(Memcmp {
// Ignore the first byte, which is the `initialized` bool flag.
offset: 1,
bytes: MemcmpEncodedBytes::Base64(payment_bytes),
encoding: None,
});
let config = RpcProgramAccountsConfig {
filters: Some(vec![memcmp]),
account_config: RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
// Don't return any data
data_slice: Some(UiDataSliceConfig {
offset: UNIQUE_GAS_PAYMENT_PUBKEY_OFFSET,
length: 32, // the length of the `unique_gas_payment_pubkey` field
}),
commitment: Some(CommitmentConfig::finalized()),
min_context_slot: None,
},
with_context: Some(false),
};
let accounts = self
.rpc_client
.get_program_accounts_with_config(&self.igp.program_id, config)
.await
.map_err(ChainCommunicationError::from_other)?;
// Now loop through matching accounts and find the one with a valid account pubkey
// that proves it's an actual gas payment PDA.
let mut valid_payment_pda_pubkey = Option::<Pubkey>::None;
for (pubkey, account) in accounts {
let unique_gas_payment_pubkey = Pubkey::new(&account.data);
let (expected_pubkey, _bump) = Pubkey::try_find_program_address(
igp_gas_payment_pda_seeds!(unique_gas_payment_pubkey),
&self.igp.program_id,
)
.ok_or_else(|| {
ChainCommunicationError::from_other_str(
"Could not find program address for unique_gas_payment_pubkey",
)
})?;
if expected_pubkey == pubkey {
valid_payment_pda_pubkey = Some(pubkey);
break;
}
}
let valid_payment_pda_pubkey = valid_payment_pda_pubkey.ok_or_else(|| {
ChainCommunicationError::from_other_str("Could not find valid gas payment PDA pubkey")
})?;
// Now that we have the valid gas payment PDA pubkey, we can get the full account data.
let account = self
.rpc_client
.get_account_with_commitment(&valid_payment_pda_pubkey, CommitmentConfig::finalized())
.await
.map_err(ChainCommunicationError::from_other)?
.value
.ok_or_else(|| {
ChainCommunicationError::from_other_str("Could not find account data")
})?;
let gas_payment_account = GasPaymentAccount::fetch(&mut account.data.as_ref())
.map_err(ChainCommunicationError::from_other)?
.into_inner();
let igp_payment = InterchainGasPayment {
message_id: gas_payment_account.message_id,
payment: gas_payment_account.payment.into(),
gas_amount: gas_payment_account.gas_amount.into(),
};
Ok(SealevelGasPayment::new(
igp_payment,
LogMeta {
address: self.igp.program_id.to_bytes().into(),
block_number: gas_payment_account.slot,
// TODO: get these when building out scraper support.
// It's inconvenient to get these :|
block_hash: H256::zero(),
transaction_id: H512::zero(),
transaction_index: 0,
log_index: sequence_number.into(),
},
H256::from(gas_payment_account.igp.to_bytes()),
))
}
}
@ -61,16 +236,94 @@ impl Indexer<InterchainGasPayment> for SealevelInterchainGasPaymasterIndexer {
#[instrument(err, skip(self))]
async fn fetch_logs(
&self,
_range: IndexRange,
range: RangeInclusive<u32>,
) -> ChainResult<Vec<(InterchainGasPayment, LogMeta)>> {
info!("Gas payment indexing not implemented for Sealevel");
Ok(vec![])
info!(
?range,
"Fetching SealevelInterchainGasPaymasterIndexer InterchainGasPayment logs"
);
let payments_capacity = range.end().saturating_sub(*range.start());
let mut payments = Vec::with_capacity(payments_capacity as usize);
for nonce in range {
if let Ok(sealevel_payment) = self.get_payment_with_sequence(nonce.into()).await {
let igp_account_filter = self.igp.igp_account;
if igp_account_filter == sealevel_payment.igp_account_pubkey {
payments.push((sealevel_payment.payment, sealevel_payment.log_meta));
}
}
}
Ok(payments)
}
#[instrument(level = "debug", err, ret, skip(self))]
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
// As a workaround to avoid gas payment indexing on Sealevel,
// we pretend the block number is 1.
Ok(1)
get_finalized_block_number(&self.rpc_client).await
}
}
#[async_trait]
impl SequenceIndexer<InterchainGasPayment> for SealevelInterchainGasPaymasterIndexer {
#[instrument(level = "info", err, ret, skip(self))]
async fn sequence_and_tip(&self) -> ChainResult<(Option<u32>, u32)> {
let program_data_account = self
.rpc_client
.get_account_with_commitment(&self.igp.data_pda_pubkey, CommitmentConfig::finalized())
.await
.map_err(ChainCommunicationError::from_other)?
.value
.ok_or_else(|| {
ChainCommunicationError::from_other_str("Could not find account data")
})?;
let program_data = ProgramDataAccount::fetch(&mut program_data_account.data.as_ref())
.map_err(ChainCommunicationError::from_other)?
.into_inner();
let payment_count = program_data
.payment_count
.try_into()
.map_err(StrOrIntParseError::from)?;
let tip = get_finalized_block_number(&self.rpc_client).await?;
info!(
?payment_count,
?tip,
"Fetched SealevelInterchainGasPaymasterIndexer sequence and tip"
);
Ok((Some(payment_count), tip))
}
}
#[test]
fn test_unique_gas_payment_pubkey_offset() {
use borsh::BorshSerialize;
use hyperlane_sealevel_igp::accounts::GasPaymentData;
let expected_unique_gas_payment_pubkey = Pubkey::new_unique();
let gas_payment = GasPaymentAccount::new(
GasPaymentData {
sequence_number: 123,
igp: Default::default(),
destination_domain: Default::default(),
message_id: Default::default(),
gas_amount: Default::default(),
payment: Default::default(),
unique_gas_payment_pubkey: expected_unique_gas_payment_pubkey,
slot: Default::default(),
}
.into(),
);
let serialized = gas_payment.into_inner().try_to_vec().unwrap();
// Note: although unclear in the docs, the reason for subtracting 1 is as follows.
// The `offset` field of `memcmp` does not add to the offset of the `dataSlice` filtering param in `get_payment_with_sequence`.
// As such, `UNIQUE_GAS_PAYMENT_PUBKEY_OFFSET` has to account for that 1-byte offset of that `offset` field, which represents
// an `is_initialized` boolean.
// Since the dummy `GasPaymentAccount` is not prefixed by an `is_initialized` boolean, we have to subtract 1 from the offset.
let sliced_unique_gas_payment_pubkey = Pubkey::new(
&serialized
[(UNIQUE_GAS_PAYMENT_PUBKEY_OFFSET - 1)..(UNIQUE_GAS_PAYMENT_PUBKEY_OFFSET + 32 - 1)],
);
assert_eq!(
expected_unique_gas_payment_pubkey,
sliced_unique_gas_payment_pubkey
);
}

@ -1,6 +1,6 @@
#![allow(warnings)] // FIXME remove
use std::{collections::HashMap, num::NonZeroU64, str::FromStr as _};
use std::{collections::HashMap, num::NonZeroU64, ops::RangeInclusive, str::FromStr as _};
use async_trait::async_trait;
use borsh::{BorshDeserialize, BorshSerialize};
@ -10,8 +10,8 @@ use tracing::{debug, info, instrument, warn};
use hyperlane_core::{
accumulator::incremental::IncrementalMerkle, ChainCommunicationError, ChainResult, Checkpoint,
ContractLocator, Decode as _, Encode as _, HyperlaneAbi, HyperlaneChain, HyperlaneContract,
HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, IndexRange, Indexer, LogMeta, Mailbox,
MessageIndexer, SequenceRange, TxCostEstimate, TxOutcome, H256, H512, U256,
HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, Indexer, LogMeta, Mailbox,
SequenceIndexer, TxCostEstimate, TxOutcome, H256, H512, U256,
};
use hyperlane_sealevel_interchain_security_module_interface::{
InterchainSecurityModuleInstruction, VerifyInstruction,
@ -53,7 +53,7 @@ use solana_transaction_status::{
use crate::RpcClientWithDebug;
use crate::{
utils::{get_account_metas, simulate_instruction},
utils::{get_account_metas, get_finalized_block_number, simulate_instruction},
ConnectionConf, SealevelProvider,
};
@ -621,7 +621,7 @@ impl SealevelMailboxIndexer {
// that proves it's an actual message storage PDA.
let mut valid_message_storage_pda_pubkey = Option::<Pubkey>::None;
for (pubkey, account) in accounts.iter() {
for (pubkey, account) in accounts {
let unique_message_pubkey = Pubkey::new(&account.data);
let (expected_pubkey, _bump) = Pubkey::try_find_program_address(
mailbox_dispatched_message_pda_seeds!(unique_message_pubkey),
@ -632,8 +632,8 @@ impl SealevelMailboxIndexer {
"Could not find program address for unique_message_pubkey",
)
})?;
if expected_pubkey == *pubkey {
valid_message_storage_pda_pubkey = Some(*pubkey);
if expected_pubkey == pubkey {
valid_message_storage_pda_pubkey = Some(pubkey);
break;
}
}
@ -682,31 +682,29 @@ impl SealevelMailboxIndexer {
}
#[async_trait]
impl MessageIndexer for SealevelMailboxIndexer {
impl SequenceIndexer<HyperlaneMessage> for SealevelMailboxIndexer {
#[instrument(err, skip(self))]
async fn fetch_count_at_tip(&self) -> ChainResult<(u32, u32)> {
async fn sequence_and_tip(&self) -> ChainResult<(Option<u32>, u32)> {
let tip = Indexer::<HyperlaneMessage>::get_finalized_block_number(self as _).await?;
// TODO: need to make sure the call and tip are at the same height?
let count = self.mailbox.count(None).await?;
Ok((count, tip))
Ok((Some(count), tip))
}
}
#[async_trait]
impl Indexer<HyperlaneMessage> for SealevelMailboxIndexer {
async fn fetch_logs(&self, range: IndexRange) -> ChainResult<Vec<(HyperlaneMessage, LogMeta)>> {
let SequenceRange(range) = range else {
return Err(ChainCommunicationError::from_other_str(
"SealevelMailboxIndexer only supports sequence-based indexing",
))
};
async fn fetch_logs(
&self,
range: RangeInclusive<u32>,
) -> ChainResult<Vec<(HyperlaneMessage, LogMeta)>> {
info!(
?range,
"Fetching SealevelMailboxIndexer HyperlaneMessage logs"
);
let mut messages = Vec::with_capacity((range.end() - range.start()) as usize);
let message_capacity = range.end().saturating_sub(*range.start());
let mut messages = Vec::with_capacity(message_capacity as usize);
for nonce in range {
messages.push(self.get_message_with_nonce(nonce).await?);
}
@ -714,13 +712,13 @@ impl Indexer<HyperlaneMessage> for SealevelMailboxIndexer {
}
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
self.get_finalized_block_number().await
get_finalized_block_number(&self.rpc_client).await
}
}
#[async_trait]
impl Indexer<H256> for SealevelMailboxIndexer {
async fn fetch_logs(&self, _range: IndexRange) -> ChainResult<Vec<(H256, LogMeta)>> {
async fn fetch_logs(&self, _range: RangeInclusive<u32>) -> ChainResult<Vec<(H256, LogMeta)>> {
todo!()
}
@ -729,6 +727,16 @@ impl Indexer<H256> for SealevelMailboxIndexer {
}
}
#[async_trait]
impl SequenceIndexer<H256> for SealevelMailboxIndexer {
async fn sequence_and_tip(&self) -> ChainResult<(Option<u32>, u32)> {
// TODO: implement when sealevel scraper support is implemented
info!("Message delivery indexing not implemented");
let tip = Indexer::<H256>::get_finalized_block_number(self).await?;
Ok((Some(1), tip))
}
}
struct SealevelMailboxAbi;
// TODO figure out how this is used and if we can support it for sealevel.

@ -13,6 +13,8 @@ use solana_sdk::{
};
use solana_transaction_status::UiReturnDataEncoding;
use crate::client::RpcClientWithDebug;
/// Simulates an instruction, and attempts to deserialize it into a T.
/// If no return data at all was returned, returns Ok(None).
/// If some return data was returned but deserialization was unsuccessful,
@ -78,3 +80,14 @@ pub async fn get_account_metas(
Ok(account_metas)
}
pub async fn get_finalized_block_number(rpc_client: &RpcClientWithDebug) -> ChainResult<u32> {
let height = rpc_client
.get_block_height()
.await
.map_err(ChainCommunicationError::from_other)?
.try_into()
// FIXME solana block height is u64...
.expect("sealevel block height exceeds u32::MAX");
Ok(height)
}

@ -5,7 +5,7 @@
"domain": 13375,
"addresses": {
"mailbox": "692KZJaoe2KRcD6uhCQDLLXnLNA5ZLnfvdqjE4aX9iu1",
"interchainGasPaymaster": "FixmeFixmeFixmeFixmeFixmeFixmeFixmeFixmeFixm",
"interchainGasPaymaster": "DrFtxirPPsfdY4HQiNZj2A9o4Ux7JaL3gELANgAoihhp",
"validatorAnnounce": "DH43ae1LwemXAboWwSh8zc9pG8j72gKUEXNi57w8fEnn"
},
"protocol": "sealevel",
@ -24,7 +24,7 @@
"domain": 13376,
"addresses": {
"mailbox": "9tCUWNjpqcf3NUSrtp7vquYVCwbEByvLjZUrhG5dgvhj",
"interchainGasPaymaster": "FixmeFixmeFixmeFixmeFixmeFixmeFixmeFixmeFixm",
"interchainGasPaymaster": "G5rGigZBL8NmxCaukK2CAKr9Jq4SUfAhsjzeri7GUraK",
"validatorAnnounce": "3Uo5j2Bti9aZtrDqJmAyuwiFaJFPFoNL5yxTpVCNcUhb"
},
"protocol": "sealevel",

@ -1,5 +1,6 @@
use std::cmp::Ordering;
use std::fmt::Debug;
use std::ops::RangeInclusive;
use std::{
sync::Arc,
time::{Duration, Instant},
@ -12,9 +13,9 @@ use tokio::time::sleep;
use tracing::{debug, warn};
use hyperlane_core::{
BlockRange, ChainResult, ContractSyncCursor, CursorAction, HyperlaneMessage,
HyperlaneMessageStore, HyperlaneWatermarkedLogStore, IndexMode, IndexRange, Indexer, LogMeta,
MessageIndexer, SequenceRange,
ChainCommunicationError, ChainResult, ContractSyncCursor, CursorAction, HyperlaneMessage,
HyperlaneMessageStore, HyperlaneWatermarkedLogStore, IndexMode, Indexer, LogMeta,
SequenceIndexer,
};
use crate::contract_sync::eta_calculator::SyncerEtaCalculator;
@ -28,15 +29,110 @@ const MAX_SEQUENCE_RANGE: u32 = 100;
/// message sync cursors.
#[derive(Debug, new)]
pub(crate) struct MessageSyncCursor {
indexer: Arc<dyn MessageIndexer>,
indexer: Arc<dyn SequenceIndexer<HyperlaneMessage>>,
db: Arc<dyn HyperlaneMessageStore>,
sync_state: SyncState,
}
#[derive(Debug, new)]
pub(crate) struct SyncState {
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,
mode: IndexMode,
/// The next sequence index that the cursor is looking for.
/// In the EVM, this is used for optimizing indexing,
/// because it's cheaper to make read calls for the sequence index than
/// to call `eth_getLogs` with a block range.
/// In Sealevel, historic queries aren't supported, so the sequence field
/// is used to query storage in sequence.
next_sequence: u32,
direction: SyncDirection,
}
impl SyncState {
async fn get_next_range(
&mut self,
max_sequence: Option<u32>,
tip: u32,
) -> ChainResult<Option<RangeInclusive<u32>>> {
// We attempt to index a range of blocks that is as large as possible.
let range = match self.mode {
IndexMode::Block => self.block_range(tip),
IndexMode::Sequence => {
let max_sequence = max_sequence.ok_or_else(|| {
ChainCommunicationError::from_other_str(
"Sequence indexing requires a max sequence",
)
})?;
if let Some(range) = self.sequence_range(tip, max_sequence)? {
range
} else {
return Ok(None);
}
}
};
if range.is_empty() {
return Ok(None);
}
Ok(Some(range))
}
fn block_range(&mut self, tip: u32) -> RangeInclusive<u32> {
let (from, to) = match self.direction {
SyncDirection::Forward => {
let from = self.next_block;
let mut to = from + self.chunk_size;
to = u32::min(to, tip);
self.next_block = to + 1;
(from, to)
}
SyncDirection::Backward => {
let to = self.next_block;
let from = to.saturating_sub(self.chunk_size);
self.next_block = from.saturating_sub(1);
(from, to)
}
};
from..=to
}
/// Returns the next sequence range to index.
///
/// # Arguments
///
/// * `tip` - The current tip of the chain.
/// * `max_sequence` - The maximum sequence that should be indexed.
/// `max_sequence` is the exclusive upper bound of the range to be indexed.
/// (e.g. `0..max_sequence`)
fn sequence_range(
&mut self,
tip: u32,
max_sequence: u32,
) -> ChainResult<Option<RangeInclusive<u32>>> {
let (from, to) = match self.direction {
SyncDirection::Forward => {
let sequence_start = self.next_sequence;
let mut sequence_end = sequence_start + MAX_SEQUENCE_RANGE;
if self.next_sequence >= max_sequence {
return Ok(None);
}
sequence_end = u32::min(sequence_end, max_sequence.saturating_sub(1));
self.next_block = tip;
self.next_sequence = sequence_end + 1;
(sequence_start, sequence_end)
}
SyncDirection::Backward => {
let sequence_end = self.next_sequence;
let sequence_start = sequence_end.saturating_sub(MAX_SEQUENCE_RANGE);
self.next_sequence = sequence_start.saturating_sub(1);
(sequence_start, sequence_end)
}
};
Ok(Some(from..=to))
}
}
impl MessageSyncCursor {
@ -59,19 +155,23 @@ impl MessageSyncCursor {
async fn update(
&mut self,
logs: Vec<(HyperlaneMessage, LogMeta)>,
prev_nonce: u32,
prev_sequence: u32,
) -> 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) {
warn!(next_nonce=?self.next_nonce, "Target nonce not found, rewinding");
if !logs.is_empty()
&& !logs
.iter()
.any(|m| m.0.nonce == self.sync_state.next_sequence)
{
warn!(next_nonce=?self.sync_state.next_sequence, "Target nonce not found, rewinding");
// 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;
if let Some(block_number) = self.retrieve_dispatched_block_number(prev_sequence).await {
self.sync_state.next_block = block_number;
warn!(block_number, "Rewound to previous known message");
} else {
self.next_block = self.start_block;
self.sync_state.next_block = self.sync_state.start_block;
}
Ok(())
} else {
@ -81,75 +181,87 @@ impl MessageSyncCursor {
}
/// A MessageSyncCursor that syncs forwards in perpetuity.
#[derive(new)]
pub(crate) struct ForwardMessageSyncCursor {
cursor: MessageSyncCursor,
mode: IndexMode,
}
impl ForwardMessageSyncCursor {
async fn get_next_range(&mut self) -> ChainResult<Option<IndexRange>> {
pub fn new(
indexer: Arc<dyn SequenceIndexer<HyperlaneMessage>>,
db: Arc<dyn HyperlaneMessageStore>,
chunk_size: u32,
start_block: u32,
next_block: u32,
mode: IndexMode,
next_sequence: u32,
) -> Self {
Self {
cursor: MessageSyncCursor::new(
indexer,
db,
SyncState::new(
chunk_size,
start_block,
next_block,
mode,
next_sequence,
SyncDirection::Forward,
),
),
}
}
async fn get_next_range(&mut self) -> ChainResult<Option<RangeInclusive<u32>>> {
// Check if any new messages have been inserted into the DB,
// and update the cursor accordingly.
while self
.cursor
.retrieve_message_by_nonce(self.cursor.next_nonce)
.retrieve_message_by_nonce(self.cursor.sync_state.next_sequence)
.await
.is_some()
{
if let Some(block_number) = self
.cursor
.retrieve_dispatched_block_number(self.cursor.next_nonce)
.retrieve_dispatched_block_number(self.cursor.sync_state.next_sequence)
.await
{
debug!(next_block = block_number, "Fast forwarding next block");
// It's possible that eth_getLogs dropped logs from this block, therefore we cannot do block_number + 1.
self.cursor.next_block = block_number;
self.cursor.sync_state.next_block = block_number;
}
debug!(
next_nonce = self.cursor.next_nonce + 1,
next_nonce = self.cursor.sync_state.next_sequence + 1,
"Fast forwarding next nonce"
);
self.cursor.next_nonce += 1;
self.cursor.sync_state.next_sequence += 1;
}
let (mailbox_count, tip) = self.cursor.indexer.fetch_count_at_tip().await?;
let cursor_count = self.cursor.next_nonce;
let cmp = cursor_count.cmp(&mailbox_count);
match cmp {
let (Some(mailbox_count), tip) = self.cursor.indexer.sequence_and_tip().await?
else {
return Ok(None);
};
let cursor_count = self.cursor.sync_state.next_sequence;
Ok(match cursor_count.cmp(&mailbox_count) {
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.cursor.next_block = tip;
Ok(None)
self.cursor.sync_state.next_block = tip;
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.cursor.next_block;
let to = u32::min(tip, from + self.cursor.chunk_size);
self.cursor.next_block = to + 1;
let range = match self.mode {
IndexMode::Block => BlockRange(from..=to),
IndexMode::Sequence => SequenceRange(
cursor_count
..=u32::min(
mailbox_count.saturating_sub(1),
cursor_count + MAX_SEQUENCE_RANGE,
),
),
};
Ok(Some(range))
self.cursor
.sync_state
.get_next_range(Some(mailbox_count), tip)
.await?
}
Ordering::Greater => {
// Providers may be internally inconsistent, e.g. RPC request A could hit a node
// whose tip is N and subsequent RPC request B could hit a node whose tip is < N.
debug!("Cursor count is greater than Mailbox count");
Ok(None)
None
}
}
})
}
}
@ -167,99 +279,115 @@ impl ContractSyncCursor<HyperlaneMessage> for ForwardMessageSyncCursor {
}
fn latest_block(&self) -> u32 {
self.cursor.next_block.saturating_sub(1)
self.cursor.sync_state.next_block.saturating_sub(1)
}
/// 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)>) -> Result<()> {
let prev_nonce = self.cursor.next_nonce.saturating_sub(1);
let prev_nonce = self.cursor.sync_state.next_sequence.saturating_sub(1);
// We may wind up having re-indexed messages that are previous to the nonce that we are looking for.
// We should not consider these messages when checking for continuity errors.
let filtered_logs = logs
.into_iter()
.filter(|m| m.0.nonce >= self.cursor.next_nonce)
.filter(|m| m.0.nonce >= self.cursor.sync_state.next_sequence)
.collect();
self.cursor.update(filtered_logs, prev_nonce).await
}
}
/// A MessageSyncCursor that syncs backwards to nonce zero.
#[derive(new)]
/// A MessageSyncCursor that syncs backwards to sequence (nonce) zero.
pub(crate) struct BackwardMessageSyncCursor {
cursor: MessageSyncCursor,
synced: bool,
mode: IndexMode,
}
impl BackwardMessageSyncCursor {
async fn get_next_range(&mut self) -> Option<IndexRange> {
#[allow(clippy::too_many_arguments)]
pub fn new(
indexer: Arc<dyn SequenceIndexer<HyperlaneMessage>>,
db: Arc<dyn HyperlaneMessageStore>,
chunk_size: u32,
start_block: u32,
next_block: u32,
mode: IndexMode,
next_sequence: u32,
synced: bool,
) -> Self {
Self {
cursor: MessageSyncCursor::new(
indexer,
db,
SyncState::new(
chunk_size,
start_block,
next_block,
mode,
next_sequence,
SyncDirection::Backward,
),
),
synced,
}
}
async fn get_next_range(&mut self) -> ChainResult<Option<RangeInclusive<u32>>> {
// 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)
.retrieve_message_by_nonce(self.cursor.sync_state.next_sequence)
.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 {
// If we found sequence zero or hit block zero, we are done rewinding.
if self.cursor.sync_state.next_sequence == 0 || self.cursor.sync_state.next_block == 0 {
self.synced = true;
break;
}
if let Some(block_number) = self
.cursor
.retrieve_dispatched_block_number(self.cursor.next_nonce)
.retrieve_dispatched_block_number(self.cursor.sync_state.next_sequence)
.await
{
// It's possible that eth_getLogs dropped logs from this block, therefore we cannot do block_number - 1.
self.cursor.next_block = block_number;
self.cursor.sync_state.next_block = block_number;
}
self.cursor.next_nonce = self.cursor.next_nonce.saturating_sub(1);
self.cursor.sync_state.next_sequence =
self.cursor.sync_state.next_sequence.saturating_sub(1);
}
if self.synced {
return None;
return Ok(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);
let next_nonce = self.cursor.next_nonce;
let range = match self.mode {
IndexMode::Block => BlockRange(from..=to),
IndexMode::Sequence => {
SequenceRange(next_nonce.saturating_sub(MAX_SEQUENCE_RANGE)..=next_nonce)
}
};
Some(range)
let (count, tip) = self.cursor.indexer.sequence_and_tip().await?;
self.cursor.sync_state.get_next_range(count, tip).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)>) -> Result<()> {
let prev_nonce = self.cursor.next_nonce.saturating_add(1);
// We may wind up having re-indexed messages that are previous to the nonce that we are looking for.
let prev_sequence = self.cursor.sync_state.next_sequence.saturating_add(1);
// We may wind up having re-indexed messages that are previous to the sequence (nonce) that we are looking for.
// We should not consider these messages when checking for continuity errors.
let filtered_logs = logs
.into_iter()
.filter(|m| m.0.nonce <= self.cursor.next_nonce)
.filter(|m| m.0.nonce <= self.cursor.sync_state.next_sequence)
.collect();
self.cursor.update(filtered_logs, prev_nonce).await
self.cursor.update(filtered_logs, prev_sequence).await
}
}
enum SyncDirection {
#[derive(Debug)]
pub enum SyncDirection {
Forward,
Backward,
}
@ -274,28 +402,33 @@ pub(crate) struct ForwardBackwardMessageSyncCursor {
impl ForwardBackwardMessageSyncCursor {
/// Construct a new contract sync helper.
pub async fn new(
indexer: Arc<dyn MessageIndexer>,
indexer: Arc<dyn SequenceIndexer<HyperlaneMessage>>,
db: Arc<dyn HyperlaneMessageStore>,
chunk_size: u32,
mode: IndexMode,
) -> Result<Self> {
let (count, tip) = indexer.fetch_count_at_tip().await?;
let (count, tip) = indexer.sequence_and_tip().await?;
let count = count.ok_or(ChainCommunicationError::from_other_str(
"Failed to query message count",
))?;
let forward_cursor = ForwardMessageSyncCursor::new(
MessageSyncCursor::new(indexer.clone(), db.clone(), chunk_size, tip, tip, count),
indexer.clone(),
db.clone(),
chunk_size,
tip,
tip,
mode,
count,
);
let backward_cursor = BackwardMessageSyncCursor::new(
MessageSyncCursor::new(
indexer.clone(),
db.clone(),
chunk_size,
tip,
tip,
count.saturating_sub(1),
),
count == 0,
indexer.clone(),
db.clone(),
chunk_size,
tip,
tip,
mode,
count.saturating_sub(1),
count == 0,
);
Ok(Self {
forward: forward_cursor,
@ -316,7 +449,7 @@ impl ContractSyncCursor<HyperlaneMessage> for ForwardBackwardMessageSyncCursor {
return Ok((CursorAction::Query(forward_range), eta));
}
if let Some(backward_range) = self.backward.get_next_range().await {
if let Some(backward_range) = self.backward.get_next_range().await? {
self.direction = SyncDirection::Backward;
return Ok((CursorAction::Query(backward_range), eta));
}
@ -325,7 +458,7 @@ impl ContractSyncCursor<HyperlaneMessage> for ForwardBackwardMessageSyncCursor {
}
fn latest_block(&self) -> u32 {
self.forward.cursor.next_block.saturating_sub(1)
self.forward.cursor.sync_state.next_block.saturating_sub(1)
}
async fn update(&mut self, logs: Vec<(HyperlaneMessage, LogMeta)>) -> Result<()> {
@ -340,41 +473,46 @@ impl ContractSyncCursor<HyperlaneMessage> for ForwardBackwardMessageSyncCursor {
/// queried is and also handling rate limiting. Rate limiting is automatically
/// performed by `next_action`.
pub(crate) struct RateLimitedContractSyncCursor<T> {
indexer: Arc<dyn Indexer<T>>,
indexer: Arc<dyn SequenceIndexer<T>>,
db: Arc<dyn HyperlaneWatermarkedLogStore<T>>,
tip: u32,
last_tip_update: Instant,
chunk_size: u32,
from: u32,
eta_calculator: SyncerEtaCalculator,
initial_height: u32,
sync_state: SyncState,
}
impl<T> RateLimitedContractSyncCursor<T> {
/// Construct a new contract sync helper.
pub async fn new(
indexer: Arc<dyn Indexer<T>>,
indexer: Arc<dyn SequenceIndexer<T>>,
db: Arc<dyn HyperlaneWatermarkedLogStore<T>>,
chunk_size: u32,
initial_height: u32,
mode: IndexMode,
) -> 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),
sync_state: SyncState::new(
chunk_size,
initial_height,
initial_height,
mode,
Default::default(),
// The rate limited cursor currently only syncs in the forward direction.
SyncDirection::Forward,
),
})
}
/// Wait based on how close we are to the tip and update the tip,
/// i.e. the highest block we may scrape.
async fn get_rate_limit(&mut self) -> ChainResult<Option<Duration>> {
if self.from + self.chunk_size < self.tip {
if self.sync_state.next_block + self.sync_state.chunk_size < self.tip {
// If doing the full chunk wouldn't exceed the already known tip we do not need to rate limit.
Ok(None)
} else {
@ -409,8 +547,11 @@ where
T: Send + Debug + 'static,
{
async fn next_action(&mut self) -> ChainResult<(CursorAction, Duration)> {
let to = u32::min(self.tip, self.from + self.chunk_size);
let from = to.saturating_sub(self.chunk_size);
let to = u32::min(
self.tip,
self.sync_state.next_block + self.sync_state.chunk_size,
);
let from = to.saturating_sub(self.sync_state.chunk_size);
let eta = if to < self.tip {
self.eta_calculator.calculate(from, self.tip)
} else {
@ -418,21 +559,20 @@ where
};
let rate_limit = self.get_rate_limit().await?;
let action = if let Some(rate_limit) = rate_limit {
CursorAction::Sleep(rate_limit)
} else {
self.from = to + 1;
// TODO: note at the moment IndexModes are not considered here, and
// block-based indexing is always used.
// This should be changed when Sealevel IGP indexing is implemented,
// along with a refactor to better accommodate indexing modes.
CursorAction::Query(BlockRange(from..=to))
};
Ok((action, eta))
if let Some(rate_limit) = rate_limit {
return Ok((CursorAction::Sleep(rate_limit), eta));
}
let (count, tip) = self.indexer.sequence_and_tip().await?;
if let Some(range) = self.sync_state.get_next_range(count, tip).await? {
return Ok((CursorAction::Query(range), eta));
}
// TODO: Define the sleep time from interval flag
Ok((CursorAction::Sleep(Duration::from_secs(5)), eta))
}
fn latest_block(&self) -> u32 {
self.from.saturating_sub(1)
self.sync_state.next_block.saturating_sub(1)
}
async fn update(&mut self, _: Vec<(T, LogMeta)>) -> Result<()> {
@ -440,8 +580,10 @@ where
// 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),
self.sync_state.start_block,
self.sync_state
.next_block
.saturating_sub(self.sync_state.chunk_size),
))
.await?;
Ok(())

@ -4,7 +4,8 @@ use cursor::*;
use derive_new::new;
use hyperlane_core::{
utils::fmt_sync_time, ContractSyncCursor, CursorAction, HyperlaneDomain, HyperlaneLogStore,
HyperlaneMessage, HyperlaneMessageStore, HyperlaneWatermarkedLogStore, Indexer, MessageIndexer,
HyperlaneMessage, HyperlaneMessageStore, HyperlaneWatermarkedLogStore, Indexer,
SequenceIndexer,
};
pub use metrics::ContractSyncMetrics;
use tokio::time::sleep;
@ -88,7 +89,7 @@ where
/// A ContractSync for syncing events using a RateLimitedContractSyncCursor
pub type WatermarkContractSync<T> =
ContractSync<T, Arc<dyn HyperlaneWatermarkedLogStore<T>>, Arc<dyn Indexer<T>>>;
ContractSync<T, Arc<dyn HyperlaneWatermarkedLogStore<T>>, Arc<dyn SequenceIndexer<T>>>;
impl<T> WatermarkContractSync<T>
where
T: Debug + Send + Sync + Clone + 'static,
@ -110,6 +111,7 @@ where
self.db.clone(),
index_settings.chunk_size,
index_settings.from,
index_settings.mode,
)
.await
.unwrap(),
@ -118,8 +120,11 @@ where
}
/// A ContractSync for syncing messages using a MessageSyncCursor
pub type MessageContractSync =
ContractSync<HyperlaneMessage, Arc<dyn HyperlaneMessageStore>, Arc<dyn MessageIndexer>>;
pub type MessageContractSync = ContractSync<
HyperlaneMessage,
Arc<dyn HyperlaneMessageStore>,
Arc<dyn SequenceIndexer<HyperlaneMessage>>,
>;
impl MessageContractSync {
/// Returns a new cursor to be used for syncing dispatched messages from the indexer
pub async fn forward_message_sync_cursor(
@ -127,17 +132,14 @@ impl MessageContractSync {
index_settings: IndexSettings,
next_nonce: u32,
) -> Box<dyn ContractSyncCursor<HyperlaneMessage>> {
let forward_data = MessageSyncCursor::new(
Box::new(ForwardMessageSyncCursor::new(
self.indexer.clone(),
self.db.clone(),
index_settings.chunk_size,
index_settings.from,
index_settings.from,
next_nonce,
);
Box::new(ForwardMessageSyncCursor::new(
forward_data,
index_settings.mode,
next_nonce,
))
}

@ -7,9 +7,9 @@ use ethers_prometheus::middleware::{
use eyre::{eyre, Context, Result};
use hyperlane_core::{
AggregationIsm, CcipReadIsm, ContractLocator, HyperlaneAbi, HyperlaneDomain,
HyperlaneDomainProtocol, HyperlaneProvider, HyperlaneSigner, IndexMode, Indexer,
InterchainGasPaymaster, InterchainGasPayment, InterchainSecurityModule, Mailbox,
MessageIndexer, MultisigIsm, RoutingIsm, ValidatorAnnounce, H256,
HyperlaneDomainProtocol, HyperlaneMessage, HyperlaneProvider, HyperlaneSigner, IndexMode,
InterchainGasPaymaster, InterchainGasPayment, InterchainSecurityModule, Mailbox, MultisigIsm,
RoutingIsm, SequenceIndexer, ValidatorAnnounce, H256,
};
use hyperlane_ethereum::{
self as h_eth, BuildableWithProvider, EthereumInterchainGasPaymasterAbi, EthereumMailboxAbi,
@ -90,6 +90,11 @@ pub struct IndexSettings {
}
impl ChainConf {
/// Fetch the index settings and index mode, since they are often used together.
pub fn index_settings(&self) -> IndexSettings {
self.index.clone()
}
/// Try to convert the chain settings into an HyperlaneProvider.
pub async fn build_provider(
&self,
@ -139,7 +144,7 @@ impl ChainConf {
pub async fn build_message_indexer(
&self,
metrics: &CoreMetrics,
) -> Result<Box<dyn MessageIndexer>> {
) -> Result<Box<dyn SequenceIndexer<HyperlaneMessage>>> {
let ctx = "Building delivery indexer";
let locator = self.locator(self.addresses.mailbox);
@ -149,7 +154,7 @@ impl ChainConf {
conf,
&locator,
metrics,
h_eth::MessageIndexerBuilder {
h_eth::SequenceIndexerBuilder {
finality_blocks: self.finality_blocks,
},
)
@ -159,7 +164,7 @@ impl ChainConf {
ChainConnectionConf::Fuel(_) => todo!(),
ChainConnectionConf::Sealevel(conf) => {
let indexer = Box::new(h_sealevel::SealevelMailboxIndexer::new(conf, locator)?);
Ok(indexer as Box<dyn MessageIndexer>)
Ok(indexer as Box<dyn SequenceIndexer<HyperlaneMessage>>)
}
}
.context(ctx)
@ -169,7 +174,7 @@ impl ChainConf {
pub async fn build_delivery_indexer(
&self,
metrics: &CoreMetrics,
) -> Result<Box<dyn Indexer<H256>>> {
) -> Result<Box<dyn SequenceIndexer<H256>>> {
let ctx = "Building delivery indexer";
let locator = self.locator(self.addresses.mailbox);
@ -189,7 +194,7 @@ impl ChainConf {
ChainConnectionConf::Fuel(_) => todo!(),
ChainConnectionConf::Sealevel(conf) => {
let indexer = Box::new(h_sealevel::SealevelMailboxIndexer::new(conf, locator)?);
Ok(indexer as Box<dyn Indexer<H256>>)
Ok(indexer as Box<dyn SequenceIndexer<H256>>)
}
}
.context(ctx)
@ -217,9 +222,9 @@ impl ChainConf {
ChainConnectionConf::Fuel(_) => todo!(),
ChainConnectionConf::Sealevel(conf) => {
let paymaster = Box::new(h_sealevel::SealevelInterchainGasPaymaster::new(
conf, locator,
));
let paymaster = Box::new(
h_sealevel::SealevelInterchainGasPaymaster::new(conf, &locator).await?,
);
Ok(paymaster as Box<dyn InterchainGasPaymaster>)
}
}
@ -230,7 +235,7 @@ impl ChainConf {
pub async fn build_interchain_gas_payment_indexer(
&self,
metrics: &CoreMetrics,
) -> Result<Box<dyn Indexer<InterchainGasPayment>>> {
) -> Result<Box<dyn SequenceIndexer<InterchainGasPayment>>> {
let ctx = "Building IGP indexer";
let locator = self.locator(self.addresses.interchain_gas_paymaster);
@ -250,10 +255,10 @@ impl ChainConf {
ChainConnectionConf::Fuel(_) => todo!(),
ChainConnectionConf::Sealevel(conf) => {
let indexer = Box::new(h_sealevel::SealevelInterchainGasPaymasterIndexer::new(
conf, locator,
));
Ok(indexer as Box<dyn Indexer<InterchainGasPayment>>)
let indexer = Box::new(
h_sealevel::SealevelInterchainGasPaymasterIndexer::new(conf, locator).await?,
);
Ok(indexer as Box<dyn SequenceIndexer<InterchainGasPayment>>)
}
}
.context(ctx)

@ -10,7 +10,7 @@ use num_traits::FromPrimitive;
#[cfg(feature = "strum")]
use strum::{EnumIter, EnumString, IntoStaticStr};
use crate::{utils::many_to_one, HyperlaneProtocolError, H160, H256};
use crate::{utils::many_to_one, HyperlaneProtocolError, IndexMode, H160, H256};
#[derive(Debug, Clone)]
pub struct Address(pub bytes::Bytes);
@ -107,17 +107,6 @@ pub enum HyperlaneDomain {
},
}
impl HyperlaneDomain {
pub fn is_arbitrum_nitro(&self) -> bool {
matches!(
self,
HyperlaneDomain::Known(
KnownHyperlaneDomain::Arbitrum | KnownHyperlaneDomain::ArbitrumGoerli,
)
)
}
}
#[cfg(any(test, feature = "test-utils"))]
impl HyperlaneDomain {
pub fn new_test_domain(name: &str) -> Self {
@ -360,6 +349,24 @@ impl HyperlaneDomain {
} => *domain_protocol,
}
}
pub fn is_arbitrum_nitro(&self) -> bool {
matches!(
self,
HyperlaneDomain::Known(
KnownHyperlaneDomain::Arbitrum | KnownHyperlaneDomain::ArbitrumGoerli,
)
)
}
pub const fn index_mode(&self) -> IndexMode {
use HyperlaneDomainProtocol::*;
let protocol = self.domain_protocol();
many_to_one!(match protocol {
IndexMode::Block: [Ethereum],
IndexMode::Sequence : [Sealevel, Fuel],
})
}
}
#[cfg(test)]

@ -3,6 +3,7 @@ use std::error::Error as StdError;
use std::fmt::{Debug, Display, Formatter};
use std::ops::Deref;
use crate::config::StrOrIntParseError;
use crate::HyperlaneProviderError;
use crate::H256;
@ -74,6 +75,9 @@ pub enum ChainCommunicationError {
/// No signer is available and was required for the operation
#[error("Signer unavailable")]
SignerUnavailable,
/// Failed to parse strings or integers
#[error("Data parsing error {0:?}")]
StrOrIntParseError(#[from] StrOrIntParseError),
}
impl ChainCommunicationError {

@ -1,9 +1,9 @@
use std::time::Duration;
use std::{ops::RangeInclusive, time::Duration};
use async_trait::async_trait;
use auto_impl::auto_impl;
use crate::{ChainResult, IndexRange, LogMeta};
use crate::{ChainResult, LogMeta};
/// A cursor governs event indexing for a contract.
#[async_trait]
@ -23,7 +23,7 @@ pub trait ContractSyncCursor<T>: Send + Sync + 'static {
/// The action that should be taken by the contract sync loop
pub enum CursorAction {
/// Direct the contract_sync task to query a block range (inclusive)
Query(IndexRange),
Query(RangeInclusive<u32>),
/// Direct the contract_sync task to sleep for a duration
Sleep(Duration),
}

@ -11,7 +11,7 @@ use async_trait::async_trait;
use auto_impl::auto_impl;
use serde::Deserialize;
use crate::{ChainResult, HyperlaneMessage, LogMeta};
use crate::{ChainResult, LogMeta};
/// Indexing mode.
#[derive(Copy, Debug, Default, Deserialize, Clone)]
@ -24,33 +24,21 @@ pub enum IndexMode {
Sequence,
}
/// An indexing range.
#[derive(Debug, Clone)]
pub enum IndexRange {
/// For block-based indexers
BlockRange(RangeInclusive<u32>),
/// For indexers that look for specific sequences, e.g. message nonces.
SequenceRange(RangeInclusive<u32>),
}
pub use IndexRange::*;
/// Interface for an indexer.
#[async_trait]
#[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, range: IndexRange) -> ChainResult<Vec<(T, LogMeta)>>;
async fn fetch_logs(&self, range: RangeInclusive<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>;
}
/// Interface for Mailbox contract indexer. Interface for allowing other
/// entities to retrieve chain-specific data from a mailbox.
/// Interface for indexing data in sequence.
#[async_trait]
#[auto_impl(&, Box, Arc)]
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)>;
pub trait SequenceIndexer<T>: Indexer<T> + 'static {
/// Return the latest finalized sequence (if any) and block number
async fn sequence_and_tip(&self) -> ChainResult<(Option<u32>, u32)>;
}

@ -17,6 +17,7 @@ solana-cli-config.workspace = true
solana-client.workspace = true
solana-program.workspace = true
solana-sdk.workspace = true
solana-transaction-status.workspace = true
account-utils = { path = "../libraries/account-utils" }
hyperlane-core = { path = "../../hyperlane-core" }

@ -1,10 +1,11 @@
use solana_client::rpc_client::RpcClient;
use solana_client::rpc_config::RpcSendTransactionConfig;
use solana_client::rpc_config::{RpcSendTransactionConfig, RpcTransactionConfig};
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::instruction::Instruction;
use solana_sdk::signature::{Keypair, Signer};
use solana_sdk::signers::Signers;
use solana_sdk::transaction::Transaction;
use solana_transaction_status::{EncodedConfirmedTransactionWithStatusMeta, UiTransactionEncoding};
use std::cell::RefCell;
pub(crate) struct Context {
@ -43,12 +44,15 @@ impl<'ctx, 'rpc> TxnBuilder<'ctx, 'rpc> {
self
}
pub(crate) fn send_with_payer(self) {
pub(crate) fn send_with_payer(self) -> Option<EncodedConfirmedTransactionWithStatusMeta> {
let payer = &self.ctx.payer;
self.send(&[payer])
}
pub(crate) fn send<T: Signers>(self, signers: &T) {
pub(crate) fn send<T: Signers>(
self,
signers: &T,
) -> Option<EncodedConfirmedTransactionWithStatusMeta> {
let client = self.client.unwrap_or(&self.ctx.client);
let recent_blockhash = client.get_latest_blockhash().unwrap();
@ -59,7 +63,7 @@ impl<'ctx, 'rpc> TxnBuilder<'ctx, 'rpc> {
recent_blockhash,
);
let _signature = client
let signature = client
.send_and_confirm_transaction_with_spinner_and_config(
&txn,
self.ctx.commitment,
@ -73,5 +77,20 @@ impl<'ctx, 'rpc> TxnBuilder<'ctx, 'rpc> {
err
})
.unwrap();
// If the commitment level set in the client is less than `finalized`,
// the only way to reliably read the tx is to use the deprecated
// `CommitmentConfig::single()` commitment...
#[allow(deprecated)]
client
.get_transaction_with_config(
&signature,
RpcTransactionConfig {
encoding: Some(UiTransactionEncoding::Base64),
commitment: Some(CommitmentConfig::single()),
..RpcTransactionConfig::default()
},
)
.ok()
}
}

@ -1,10 +1,10 @@
use serde::{Deserialize, Serialize};
use solana_program::pubkey::Pubkey;
use solana_sdk::{signature::Signer, signer::keypair::Keypair};
use solana_sdk::signature::Signer;
use std::collections::HashMap;
use std::{fs::File, io::Write, path::Path, str::FromStr};
use std::{fs::File, io::Write, path::Path};
use crate::{
cmd_utils::{create_and_write_keypair, create_new_directory, deploy_program},
@ -321,37 +321,6 @@ fn deploy_igp(ctx: &mut Context, core: &CoreDeploy, key_dir: &Path) -> (Pubkey,
println!("Skipping setting gas overheads");
}
// TODO: this payment logic should be in the transfer remote and this block of code needs to be
// removed after that
if core.remote_domains.contains(&13376) {
// Now make a gas payment for a message ID
let message_id =
H256::from_str("0x7b8ba684e5ce44f898c5fa81785c83a00e32b5bef3412e648eb7a17bec497685")
.unwrap();
let unique_gas_payment_keypair = Keypair::new();
let (instruction, gas_payment_data_account) =
hyperlane_sealevel_igp::instruction::pay_for_gas_instruction(
program_id,
ctx.payer.pubkey(),
igp_account,
Some(overhead_igp_account),
unique_gas_payment_keypair.pubkey(),
message_id,
13376,
100000,
)
.unwrap();
ctx.new_txn()
.add(instruction)
.send(&[&ctx.payer, &unique_gas_payment_keypair]);
println!(
"Made a payment for message {} with gas payment data account {}",
message_id, gas_payment_data_account
);
}
(program_id, overhead_igp_account, igp_account)
}

@ -103,6 +103,7 @@ enum HyperlaneSealevelCmd {
Core(CoreCmd),
Mailbox(MailboxCmd),
Token(TokenCmd),
Igp(IgpCmd),
ValidatorAnnounce(ValidatorAnnounceCmd),
MultisigIsmMessageId(MultisigIsmMessageIdCmd),
WarpRoute(WarpRouteCmd),
@ -323,6 +324,23 @@ struct TokenEnrollRemoteRouter {
router: H256,
}
#[derive(Args)]
struct IgpCmd {
#[command(subcommand)]
cmd: IgpSubCmd,
}
#[derive(Subcommand)]
enum IgpSubCmd {
PayForGas(PayForGasArgs),
}
#[derive(Args)]
struct PayForGasArgs {
program_id: Pubkey,
message_id: String,
}
#[derive(Args)]
struct ValidatorAnnounceCmd {
#[command(subcommand)]
@ -443,6 +461,7 @@ fn main() {
}
HyperlaneSealevelCmd::Core(cmd) => process_core_cmd(ctx, cmd),
HyperlaneSealevelCmd::WarpRoute(cmd) => process_warp_route_cmd(ctx, cmd),
HyperlaneSealevelCmd::Igp(cmd) => process_igp_cmd(ctx, cmd),
}
}
@ -914,11 +933,13 @@ fn process_token_cmd(ctx: Context, cmd: TokenCmd) {
data: ixn.encode().unwrap(),
accounts,
};
ctx.new_txn().add(xfer_instruction).send(&[
let tx_result = ctx.new_txn().add(xfer_instruction).send(&[
&ctx.payer,
&sender,
&unique_message_account_keypair,
]);
// Print the output so it can be used in e2e tests
println!("{:?}", tx_result);
}
TokenSubCmd::EnrollRemoteRouter(enroll) => {
let enroll_instruction = HtInstruction::EnrollRemoteRouter(RemoteRouterConfig {
@ -1103,3 +1124,42 @@ fn process_multisig_ism_message_id_cmd(ctx: Context, cmd: MultisigIsmMessageIdCm
}
}
}
fn process_igp_cmd(ctx: Context, cmd: IgpCmd) {
match cmd.cmd {
IgpSubCmd::PayForGas(payment_details) => {
let unique_gas_payment_keypair = Keypair::new();
let salt = H256::zero();
let (igp_account, _igp_account_bump) = Pubkey::find_program_address(
hyperlane_sealevel_igp::igp_pda_seeds!(salt),
&payment_details.program_id,
);
let (overhead_igp_account, _) = Pubkey::find_program_address(
hyperlane_sealevel_igp::overhead_igp_pda_seeds!(salt),
&payment_details.program_id,
);
let (ixn, gas_payment_data_account) =
hyperlane_sealevel_igp::instruction::pay_for_gas_instruction(
payment_details.program_id,
ctx.payer.pubkey(),
igp_account,
Some(overhead_igp_account),
unique_gas_payment_keypair.pubkey(),
H256::from_str(&payment_details.message_id).unwrap(),
13376,
100000,
)
.unwrap();
ctx.new_txn()
.add(ixn)
.send(&[&ctx.payer, &unique_gas_payment_keypair]);
println!(
"Made a payment for message {} with gas payment data account {}",
payment_details.message_id, gas_payment_data_account
);
}
}
}

@ -1035,6 +1035,7 @@ async fn assert_gas_payment(
gas_payment_account_key: Pubkey,
destination_domain: u32,
gas_amount: u64,
payment: u64,
message_id: H256,
sequence_number: u64,
) {
@ -1063,6 +1064,7 @@ async fn assert_gas_payment(
destination_domain,
message_id,
gas_amount,
payment,
unique_gas_payment_pubkey,
slot,
}
@ -1130,6 +1132,7 @@ async fn run_pay_for_gas_tests(gas_amount: u64, overhead_gas_amount: Option<u64>
gas_payment_pda_key,
TEST_DESTINATION_DOMAIN,
gas_amount + overhead_gas_amount.unwrap_or_default(),
quote,
message_id,
0,
)
@ -1157,6 +1160,7 @@ async fn run_pay_for_gas_tests(gas_amount: u64, overhead_gas_amount: Option<u64>
gas_payment_pda_key,
TEST_DESTINATION_DOMAIN,
gas_amount + overhead_gas_amount.unwrap_or_default(),
quote,
message_id,
1,
)

@ -269,8 +269,10 @@ pub struct GasPaymentData {
pub destination_domain: u32,
/// The message ID of the gas payment.
pub message_id: H256,
/// The amount of gas paid for.
/// The amount of gas provided.
pub gas_amount: u64,
/// The amount of lamports quoted and paid.
pub payment: u64,
/// The unique gas payment pubkey.
pub unique_gas_payment_pubkey: Pubkey,
/// The slot of the gas payment.
@ -286,7 +288,7 @@ impl SizedData for GasPaymentData {
// 8 for gas_amount
// 32 for unique_gas_payment_pubkey
// 8 for slot
8 + 32 + 4 + 32 + 8 + 32 + 8
8 + 32 + 4 + 32 + 8 + 8 + 32 + 8
}
}

@ -367,6 +367,7 @@ fn pay_for_gas(program_id: &Pubkey, accounts: &[AccountInfo], payment: PayForGas
destination_domain: payment.destination_domain,
message_id: payment.message_id,
gas_amount,
payment: required_payment,
unique_gas_payment_pubkey: *unique_gas_payment_account_info.key,
slot: Clock::get()?.slot,
}

@ -766,6 +766,7 @@ async fn test_transfer_remote(spl_token_program_id: Pubkey) {
gas_amount: REMOTE_GAS_AMOUNT,
unique_gas_payment_pubkey: unique_message_account_keypair.pubkey(),
slot: transfer_remote_tx_status.slot,
payment: REMOTE_GAS_AMOUNT
}
.into(),
);

@ -551,6 +551,7 @@ async fn test_transfer_remote() {
gas_amount: REMOTE_GAS_AMOUNT,
unique_gas_payment_pubkey: unique_message_account_keypair.pubkey(),
slot: transfer_remote_tx_status.slot,
payment: REMOTE_GAS_AMOUNT
}
.into(),
);

@ -824,6 +824,7 @@ async fn test_transfer_remote() {
gas_amount: REMOTE_GAS_AMOUNT,
unique_gas_payment_pubkey: unique_message_account_keypair.pubkey(),
slot: transfer_remote_tx_status.slot,
payment: REMOTE_GAS_AMOUNT
}
.into(),
);

@ -18,3 +18,5 @@ tempfile.workspace = true
ureq = { workspace = true, default-features = false }
which.workspace = true
macro_rules_attribute.workspace = true
regex.workspace = true
hyperlane-core = { path = "../../hyperlane-core" }

@ -6,6 +6,7 @@ pub struct Config {
pub ci_mode: bool,
pub ci_mode_timeout: u64,
pub kathy_messages: u64,
// TODO: Include count of sealevel messages in a field separate from `kathy_messages`?
}
impl Config {

@ -7,6 +7,10 @@ use crate::fetch_metric;
use crate::logging::log;
use crate::solana::solana_termination_invariants_met;
// This number should be even, so the messages can be split into two equal halves
// sent before and after the relayer spins up, to avoid rounding errors.
pub const SOL_MESSAGES_EXPECTED: u32 = 20;
/// Use the metrics to check if the relayer queues are empty and the expected
/// number of messages have been sent.
pub fn termination_invariants_met(
@ -15,8 +19,7 @@ pub fn termination_invariants_met(
solana_config_path: &Path,
) -> eyre::Result<bool> {
let eth_messages_expected = (config.kathy_messages / 2) as u32 * 2;
let sol_messages_expected = 1;
let total_messages_expected = eth_messages_expected + sol_messages_expected;
let total_messages_expected = eth_messages_expected + SOL_MESSAGES_EXPECTED;
let lengths = fetch_metric("9092", "hyperlane_submitter_queue_length", &hashmap! {})?;
assert!(!lengths.is_empty(), "Could not find queue length metric");
@ -47,6 +50,17 @@ pub fn termination_invariants_met(
)?
.iter()
.sum::<u32>();
let gas_payment_sealevel_events_count = fetch_metric(
"9092",
"hyperlane_contract_sync_stored_events",
&hashmap! {
"data_type" => "gas_payments",
"chain" => "sealeveltest",
},
)?
.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 < total_messages_expected {
@ -87,11 +101,14 @@ pub fn termination_invariants_met(
.iter()
.sum::<u32>();
// The relayer and scraper should have the same number of gas payments.
if gas_payments_scraped != gas_payment_events_count {
// TODO: Sealevel gas payments are not yet included in the event count.
// For now, treat as an exception in the invariants.
let expected_gas_payments = gas_payment_events_count - gas_payment_sealevel_events_count;
if gas_payments_scraped != expected_gas_payments {
log!(
"Scraper has scraped {} gas payments, expected {}",
gas_payments_scraped,
eth_messages_expected
expected_gas_payments
);
return Ok(false);
}

@ -29,7 +29,7 @@ use program::Program;
use crate::config::Config;
use crate::ethereum::start_anvil;
use crate::invariants::termination_invariants_met;
use crate::invariants::{termination_invariants_met, SOL_MESSAGES_EXPECTED};
use crate::solana::*;
use crate::utils::{concat_path, make_static, stop_child, AgentHandles, ArbitraryData, TaskHandle};
@ -171,6 +171,22 @@ fn main() -> ExitCode {
.hyp_env("CHAINS_SEALEVELTEST2_SIGNER_KEY", RELAYER_KEYS[4])
.hyp_env("RELAYCHAINS", "invalidchain,otherinvalid")
.hyp_env("ALLOWLOCALCHECKPOINTSYNCERS", "true")
.hyp_env(
"GASPAYMENTENFORCEMENT",
r#"[{
"type": "minimum",
"payment": "1",
"matchingList": [
{
"originDomain": ["13375","13376"],
"destinationDomain": ["13375","13376"]
}
]
},
{
"type": "none"
}]"#,
)
.arg(
"chains.test1.connection.urls",
"http://127.0.0.1:8545,http://127.0.0.1:8545,http://127.0.0.1:8545",
@ -319,9 +335,17 @@ fn main() -> ExitCode {
state.push_agent(validator);
}
// Send some sealevel messages before spinning up the relayer, to test the backward indexing cursor
for _i in 0..(SOL_MESSAGES_EXPECTED / 2) {
initiate_solana_hyperlane_transfer(solana_path.clone(), solana_config_path.clone()).join();
}
state.push_agent(relayer_env.spawn("RLY"));
initiate_solana_hyperlane_transfer(solana_path.clone(), solana_config_path.clone()).join();
// Send some sealevel messages after spinning up the relayer, to test the forward indexing cursor
for _i in 0..(SOL_MESSAGES_EXPECTED / 2) {
initiate_solana_hyperlane_transfer(solana_path.clone(), solana_config_path.clone()).join();
}
log!("Setup complete! Agents running in background...");
log!("Ctrl+C to end execution...");

@ -11,7 +11,9 @@ pub fn fetch_metric(port: &str, metric: &str, labels: &HashMap<&str, &str>) -> R
.filter(|l| {
labels
.iter()
.all(|(k, v)| l.contains(&format!("{k}=\"{v}\"")))
// Do no check for the closing quotation mark when matching, to allow for
// only matching a label value prefix.
.all(|(k, v)| l.contains(&format!("{k}=\"{v}")))
})
.map(|l| {
Ok(l.rsplit_once(' ')

@ -4,6 +4,7 @@ use std::thread::sleep;
use std::time::Duration;
use macro_rules_attribute::apply;
use regex::Regex;
use tempfile::{tempdir, NamedTempFile};
use crate::logging::log;
@ -296,7 +297,7 @@ pub fn initiate_solana_hyperlane_transfer(
.trim()
.to_owned();
sealevel_client(&solana_cli_tools_path, &solana_config_path)
let output = sealevel_client(&solana_cli_tools_path, &solana_config_path)
.cmd("token")
.cmd("transfer-remote")
.cmd(SOLANA_KEYPAIR)
@ -305,8 +306,33 @@ pub fn initiate_solana_hyperlane_transfer(
.cmd(sender) // send to self
.cmd("native")
.arg("program-id", "CGn8yNtSD3aTTqJfYhUb6s1aVTN75NzwtsFKo1e83aga")
.run()
.run_with_output()
.join();
let message_id = get_message_id_from_logs(output);
if let Some(message_id) = message_id {
sealevel_client(&solana_cli_tools_path, &solana_config_path)
.cmd("igp")
.cmd("pay-for-gas")
.cmd("GwHaw8ewMyzZn9vvrZEnTEAAYpLdkGYs195XWcLDCN4U")
.cmd(message_id)
.run()
.join();
}
}
fn get_message_id_from_logs(logs: Vec<String>) -> Option<String> {
let message_id_regex = Regex::new(r"Dispatched message to \d+, ID 0x([0-9a-fA-F]+)").unwrap();
for log in logs {
// Use the regular expression to capture the ID
if let Some(captures) = message_id_regex.captures(&log) {
if let Some(id_match) = captures.get(1) {
let id = id_match.as_str();
return Some(format!("0x{}", id));
}
}
}
None
}
pub fn solana_termination_invariants_met(

Loading…
Cancel
Save