diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 862c5062a16..e4d873eb481 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -3735,6 +3735,7 @@ dependencies = [ "base64 0.13.1", "borsh 0.9.3", "hyperlane-core", + "hyperlane-sealevel-igp", "hyperlane-sealevel-interchain-security-module-interface", "hyperlane-sealevel-mailbox", "hyperlane-sealevel-message-recipient-interface", diff --git a/rust/chains/hyperlane-sealevel/Cargo.toml b/rust/chains/hyperlane-sealevel/Cargo.toml index 82dbf2446f2..84fe045dc32 100644 --- a/rust/chains/hyperlane-sealevel/Cargo.toml +++ b/rust/chains/hyperlane-sealevel/Cargo.toml @@ -24,6 +24,7 @@ url.workspace = true hyperlane-core = { path = "../../hyperlane-core" } hyperlane-sealevel-mailbox = { path = "../../sealevel/programs/mailbox", features = ["no-entrypoint"] } +hyperlane-sealevel-igp = { path = "../../sealevel/programs/interchain-gas-paymaster", features = ["no-entrypoint"] } hyperlane-sealevel-interchain-security-module-interface = { path = "../../sealevel/libraries/interchain-security-module-interface" } hyperlane-sealevel-message-recipient-interface = { path = "../../sealevel/libraries/message-recipient-interface" } serializable-account-meta = { path = "../../sealevel/libraries/serializable-account-meta" } diff --git a/rust/chains/hyperlane-sealevel/src/client.rs b/rust/chains/hyperlane-sealevel/src/client.rs index 0ecc300817c..0144cb82689 100644 --- a/rust/chains/hyperlane-sealevel/src/client.rs +++ b/rust/chains/hyperlane-sealevel/src/client.rs @@ -1,4 +1,5 @@ use solana_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::commitment_config::CommitmentConfig; /// Kludge to implement Debug for RpcClient. pub(crate) struct RpcClientWithDebug(RpcClient); @@ -7,6 +8,10 @@ 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 { diff --git a/rust/chains/hyperlane-sealevel/src/interchain_gas.rs b/rust/chains/hyperlane-sealevel/src/interchain_gas.rs index 92731fe42cd..5401635a758 100644 --- a/rust/chains/hyperlane-sealevel/src/interchain_gas.rs +++ b/rust/chains/hyperlane-sealevel/src/interchain_gas.rs @@ -1,27 +1,57 @@ +#![allow(warnings)] // FIXME remove + use async_trait::async_trait; use hyperlane_core::{ - ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, HyperlaneDomain, - HyperlaneProvider, IndexRange, Indexer, InterchainGasPaymaster, InterchainGasPayment, LogMeta, - H256, + ChainCommunicationError, ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, + HyperlaneDomain, HyperlaneProvider, + IndexRange::{self, SequenceRange}, + Indexer, InterchainGasPaymaster, InterchainGasPayment, LogMeta, H256, U256, +}; +use hyperlane_sealevel_igp::{ + accounts::ProgramDataAccount, igp_pda_seeds, igp_program_data_pda_seeds, +}; +use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig}; +use solana_client::{ + nonblocking::rpc_client::RpcClient, + rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, + rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, }; -use tracing::{info, instrument}; +use tracing::{debug, info, instrument}; -use crate::{ConnectionConf, SealevelProvider}; -use solana_sdk::pubkey::Pubkey; +use crate::{client::RpcClientWithDebug, ConnectionConf, SealevelProvider}; +use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey}; /// A reference to an IGP contract on some Sealevel chain #[derive(Debug)] pub struct SealevelInterchainGasPaymaster { program_id: Pubkey, + pda: (Pubkey, u8), + data_pda: (Pubkey, u8), domain: HyperlaneDomain, } impl SealevelInterchainGasPaymaster { /// Create a new Sealevel IGP. - pub fn new(_conf: &ConnectionConf, locator: ContractLocator) -> Self { + pub fn new(conf: &ConnectionConf, locator: ContractLocator, seed: u64) -> Self { + // Set the `processed` commitment at rpc level + let rpc_client = + RpcClient::new_with_commitment(conf.url.to_string(), CommitmentConfig::processed()); + let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); + let domain = locator.domain.id(); + // TODO: is this the right way of passing the seed? + let pda = Pubkey::find_program_address(igp_pda_seeds!(seed), &program_id); + let data_pda = Pubkey::find_program_address(igp_program_data_pda_seeds!(), &program_id); + + debug!( + "domain={}\nmailbox={}\npda=({}, {})\ndata_pda=({}, {})", + domain, program_id, pda.0, pda.1, data_pda.0, data_pda.1, + ); + Self { program_id, + pda, + data_pda, domain: locator.domain.clone(), } } @@ -47,12 +77,128 @@ 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, + program_id: Pubkey, +} impl SealevelInterchainGasPaymasterIndexer { /// Create a new Sealevel IGP indexer. - pub fn new(_conf: &ConnectionConf, _locator: ContractLocator) -> Self { - Self {} + pub fn new(conf: &ConnectionConf, locator: ContractLocator) -> Self { + let program_id = Pubkey::from(<[u8; 32]>::from(locator.address)); + // 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, locator)?; + Ok(Self { + program_id, + rpc_client, + igp, + }) + } + + async fn get_payment_with_sequence( + &self, + sequence_number: u64, + ) -> ChainResult<(InterchainGasPayment, LogMeta)> { + let payment_bytes = &[ + &hyperlane_sealevel_igp::accounts::GAS_PAYMENT_DISCRIMINATOR[..], + &sequence_number.to_le_bytes()[..], + ] + .concat(); + 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. + 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: 1 + 8 + 32 + 4 + 32 + 8, // the offset to get the `unique_gas_payment_pubkey` field + 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 message storage PDA. + let mut valid_payment_pda_pubkey = Option::::None; + + for (pubkey, account) in accounts.iter() { + let unique_message_pubkey = Pubkey::new(&account.data); + let (expected_pubkey, _bump) = Pubkey::try_find_program_address( + // TODO: should the bump be passed as a seed? + igp_program_data_pda_seeds!(), + &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 message storage PDA pubkey", + ) + })?; + + // Now that we have the valid message storage 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 dispatched_payment_account = ProgramDataAccount::fetch(&mut account.data.as_ref()) + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + let igp_payment = + // TODO: implement `Decode` for `InterchainGasPayment` + InterchainGasPayment::read_from(&mut &dispatched_payment_account.encoded_message[..])?; + + Ok(( + igp_payment, + LogMeta { + address: self.igp.program_id.to_bytes().into(), + block_number: dispatched_payment_account.slot, + // TODO: get these when building out scraper support. + // It's inconvenient to get these :| + block_hash: H256::zero(), + transaction_hash: H256::zero(), + transaction_index: 0, + log_index: U256::zero(), + }, + )) } } @@ -61,10 +207,24 @@ impl Indexer for SealevelInterchainGasPaymasterIndexer { #[instrument(err, skip(self))] async fn fetch_logs( &self, - _range: IndexRange, + range: IndexRange, ) -> ChainResult> { - info!("Gas payment indexing not implemented for Sealevel"); - Ok(vec![]) + let SequenceRange(range) = range else { + return Err(ChainCommunicationError::from_other_str( + "SealevelMailboxIndexer only supports sequence-based indexing", + )) + }; + + info!( + ?range, + "Fetching SealevelMailboxIndexer HyperlaneMessage logs" + ); + + let mut messages = Vec::with_capacity((range.end() - range.start()) as usize); + for nonce in range { + messages.push(self.get_payment_with_sequence(nonce.into()).await?); + } + Ok(messages) } #[instrument(level = "debug", err, ret, skip(self))]