Skip to content

Commit

Permalink
feat: Scrape Sealevel dispatched messages (#4776)
Browse files Browse the repository at this point in the history
### Description

Scraper is able to index dispatch messages:
1. Blocks are stored into database
2. Transactions are stored into database (need population of all fields)
3. Dispatched messages are stored into database

### Drive-by changes

Initial indexing of delivered messages (so that Scraper does not crush)

### Related issues

- Contributes into
#4272

### Backward compatibility

Yes (Solana-like chains should not be enabled for Scraper)

### Testing

Manual run of Scraper
E2E Tests

---------

Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com>
  • Loading branch information
ameten and ameten authored Oct 30, 2024
1 parent 7e9e248 commit c87cfbd
Show file tree
Hide file tree
Showing 10 changed files with 916 additions and 180 deletions.
68 changes: 68 additions & 0 deletions rust/main/chains/hyperlane-sealevel/src/account.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use base64::{engine::general_purpose::STANDARD as Base64, Engine};
use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig};
use solana_client::{
rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType},
};
use solana_sdk::{account::Account, commitment_config::CommitmentConfig, pubkey::Pubkey};

use hyperlane_core::{ChainCommunicationError, ChainResult};

use crate::rpc::SealevelRpcClient;

pub async fn search_accounts_by_discriminator(
client: &SealevelRpcClient,
program_id: &Pubkey,
discriminator: &[u8; 8],
nonce_bytes: &[u8],
offset: usize,
length: usize,
) -> ChainResult<Vec<(Pubkey, Account)>> {
let target_message_account_bytes = &[discriminator, nonce_bytes].concat();
let target_message_account_bytes = Base64.encode(target_message_account_bytes);

// First, find all accounts with the matching account 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 field which was used to generate account id
#[allow(deprecated)]
let memcmp = RpcFilterType::Memcmp(Memcmp {
// Ignore the first byte, which is the `initialized` bool flag.
offset: 1,
bytes: MemcmpEncodedBytes::Base64(target_message_account_bytes),
encoding: None,
});
let config = RpcProgramAccountsConfig {
filters: Some(vec![memcmp]),
account_config: RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
data_slice: Some(UiDataSliceConfig { offset, length }),
commitment: Some(CommitmentConfig::finalized()),
min_context_slot: None,
},
with_context: Some(false),
};
let accounts = client
.get_program_accounts_with_config(program_id, config)
.await?;
Ok(accounts)
}

pub fn search_and_validate_account<F>(
accounts: Vec<(Pubkey, Account)>,
message_account: F,
) -> ChainResult<Pubkey>
where
F: Fn(&Account) -> ChainResult<Pubkey>,
{
for (pubkey, account) in accounts {
let expected_pubkey = message_account(&account)?;
if expected_pubkey == pubkey {
return Ok(pubkey);
}
}

Err(ChainCommunicationError::from_other_str(
"Could not find valid storage PDA pubkey",
))
}
18 changes: 17 additions & 1 deletion rust/main/chains/hyperlane-sealevel/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use hyperlane_core::ChainCommunicationError;
use hyperlane_core::{ChainCommunicationError, H512};
use solana_client::client_error::ClientError;
use solana_sdk::pubkey::ParsePubkeyError;
use solana_transaction_status::EncodedTransaction;

/// Errors from the crates specific to the hyperlane-sealevel
/// implementation.
Expand All @@ -17,6 +18,21 @@ pub enum HyperlaneSealevelError {
/// Decoding error
#[error("{0}")]
Decoding(#[from] solana_sdk::bs58::decode::Error),
/// No transaction in block error
#[error("{0}")]
NoTransactions(String),
/// Too many transactions of particular content in block
#[error("{0}")]
TooManyTransactions(String),
/// Unsupported transaction encoding
#[error("{0:?}")]
UnsupportedTransactionEncoding(EncodedTransaction),
/// Unsigned transaction
#[error("{0}")]
UnsignedTransaction(H512),
/// Incorrect transaction
#[error("received incorrect transaction, expected hash: {0:?}, received hash: {1:?}")]
IncorrectTransaction(Box<H512>, Box<H512>),
}

impl From<HyperlaneSealevelError> for ChainCommunicationError {
Expand Down
114 changes: 39 additions & 75 deletions rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
use std::ops::RangeInclusive;

use async_trait::async_trait;
use hyperlane_core::{
config::StrOrIntParseError, ChainCommunicationError, ChainResult, ContractLocator,
HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexed, Indexer,
InterchainGasPaymaster, InterchainGasPayment, LogMeta, SequenceAwareIndexer, H256, H512,
};
use derive_new::new;
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 solana_sdk::{account::Account, pubkey::Pubkey};
use tracing::{info, instrument};

use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient};
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
use hyperlane_core::{
config::StrOrIntParseError, ChainCommunicationError, ChainResult, ContractLocator,
HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexed, Indexer,
InterchainGasPaymaster, InterchainGasPayment, LogMeta, SequenceAwareIndexer, H256, H512,
};

use derive_new::new;
use crate::account::{search_accounts_by_discriminator, search_and_validate_account};
use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient};

/// The offset to get the `unique_gas_payment_pubkey` field from the serialized GasPaymentData.
/// The account data includes prefixes that are accounted for here: a 1 byte initialized flag
Expand Down Expand Up @@ -121,70 +118,23 @@ impl SealevelInterchainGasPaymasterIndexer {
&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),
};
tracing::debug!(config=?config, "Fetching program accounts");
let accounts = self
.rpc_client
.get_program_accounts_with_config(&self.igp.program_id, config)
.await?;
let discriminator = hyperlane_sealevel_igp::accounts::GAS_PAYMENT_DISCRIMINATOR;
let sequence_number_bytes = sequence_number.to_le_bytes();
let unique_gas_payment_pubkey_length = 32; // the length of the `unique_gas_payment_pubkey` field
let accounts = search_accounts_by_discriminator(
&self.rpc_client,
&self.igp.program_id,
discriminator,
&sequence_number_bytes,
UNIQUE_GAS_PAYMENT_PUBKEY_OFFSET,
unique_gas_payment_pubkey_length,
)
.await?;

tracing::debug!(accounts=?accounts, "Fetched program accounts");

// 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")
let valid_payment_pda_pubkey = search_and_validate_account(accounts, |account| {
self.interchain_payment_account(account)
})?;

// Now that we have the valid gas payment PDA pubkey, we can get the full account data.
Expand Down Expand Up @@ -224,6 +174,20 @@ impl SealevelInterchainGasPaymasterIndexer {
H256::from(gas_payment_account.igp.to_bytes()),
))
}

fn interchain_payment_account(&self, account: &Account) -> ChainResult<Pubkey> {
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",
)
})?;
Ok(expected_pubkey)
}
}

#[async_trait]
Expand Down
3 changes: 3 additions & 0 deletions rust/main/chains/hyperlane-sealevel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub use solana_sdk::signer::keypair::Keypair;
pub use trait_builder::*;
pub use validator_announce::*;

mod account;
mod error;
mod interchain_gas;
mod interchain_security_module;
Expand All @@ -24,4 +25,6 @@ mod multisig_ism;
mod provider;
mod rpc;
mod trait_builder;
mod transaction;
mod utils;
mod validator_announce;
Loading

0 comments on commit c87cfbd

Please sign in to comment.