Skip to content

Commit

Permalink
feat: Scraper populates delivered messages for Sealevel (#4817)
Browse files Browse the repository at this point in the history
### Description

Scraper populates delivered messages for Sealevel

### Related issues

- Contributes into
#4301

### Backward compatibility

Yes

### Testing

Manual run of Scraper

Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com>
  • Loading branch information
ameten and ameten authored Nov 8, 2024
1 parent f248354 commit 5bebd6d
Show file tree
Hide file tree
Showing 3 changed files with 391 additions and 78 deletions.
174 changes: 124 additions & 50 deletions rust/main/chains/hyperlane-sealevel/src/mailbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ use solana_client::{
use solana_sdk::{
account::Account,
bs58,
clock::Slot,
commitment_config::CommitmentConfig,
compute_budget::ComputeBudgetInstruction,
hash::Hash,
Expand Down Expand Up @@ -61,7 +62,9 @@ use hyperlane_core::{

use crate::account::{search_accounts_by_discriminator, search_and_validate_account};
use crate::error::HyperlaneSealevelError;
use crate::transaction::search_dispatched_message_transactions;
use crate::transaction::{
is_message_delivery_instruction, is_message_dispatch_instruction, search_message_transactions,
};
use crate::utils::{decode_h256, decode_h512, from_base58};
use crate::{ConnectionConf, SealevelProvider, SealevelRpcClient};

Expand Down Expand Up @@ -694,44 +697,15 @@ impl SealevelMailboxIndexer {
let hyperlane_message =
HyperlaneMessage::read_from(&mut &dispatched_message_account.encoded_message[..])?;

let block = self
.mailbox
.provider
.rpc()
.get_block(dispatched_message_account.slot)
let log_meta = self
.dispatch_message_log_meta(
U256::from(nonce),
&valid_message_storage_pda_pubkey,
&dispatched_message_account.slot,
)
.await?;
let block_hash = decode_h256(&block.blockhash)?;

let transactions =
block.transactions.ok_or(HyperlaneSealevelError::NoTransactions("block which should contain message dispatch transaction does not contain any transaction".to_owned()))?;

let transaction_hashes = search_dispatched_message_transactions(
&self.mailbox.program_id,
&valid_message_storage_pda_pubkey,
transactions,
);

// We expect to see that there is only one message dispatch transaction
if transaction_hashes.len() > 1 {
Err(HyperlaneSealevelError::TooManyTransactions("Block contains more than one dispatch message transaction operating on the same dispatch message store PDA".to_owned()))?
}

let (transaction_index, transaction_hash) = transaction_hashes
.into_iter()
.next()
.ok_or(HyperlaneSealevelError::NoTransactions("block which should contain message dispatch transaction does not contain any after filtering".to_owned()))?;

Ok((
hyperlane_message.into(),
LogMeta {
address: self.mailbox.program_id.to_bytes().into(),
block_number: dispatched_message_account.slot,
block_hash,
transaction_id: transaction_hash,
transaction_index: transaction_index as u64,
log_index: U256::from(nonce),
},
))
Ok((hyperlane_message.into(), log_meta))
}

fn dispatched_message_account(&self, account: &Account) -> ChainResult<Pubkey> {
Expand All @@ -748,6 +722,28 @@ impl SealevelMailboxIndexer {
Ok(expected_pubkey)
}

async fn dispatch_message_log_meta(
&self,
log_index: U256,
message_storage_pda_pubkey: &Pubkey,
message_account_slot: &Slot,
) -> ChainResult<LogMeta> {
let error_msg_no_txn = "block which should contain message dispatch transaction does not contain any transaction".to_owned();
let error_msg_too_many_txns = "block contains more than one dispatch message transaction operating on the same dispatch message store PDA".to_owned();
let error_msg_no_txn_after_filtering = "block which should contain message dispatch transaction does not contain any after filtering".to_owned();

self.log_meta(
log_index,
message_storage_pda_pubkey,
message_account_slot,
&is_message_dispatch_instruction,
error_msg_no_txn,
error_msg_too_many_txns,
error_msg_no_txn_after_filtering,
)
.await
}

async fn get_delivered_message_with_nonce(
&self,
nonce: u32,
Expand Down Expand Up @@ -782,19 +778,15 @@ impl SealevelMailboxIndexer {
.into_inner();
let message_id = delivered_message_account.message_id;

Ok((
message_id.into(),
LogMeta {
address: self.mailbox.program_id.to_bytes().into(),
block_number: delivered_message_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: U256::zero(),
},
))
let log_meta = self
.delivered_message_log_meta(
U256::from(nonce),
&valid_message_storage_pda_pubkey,
&delivered_message_account.slot,
)
.await?;

Ok((message_id.into(), log_meta))
}

fn delivered_message_account(&self, account: &Account) -> ChainResult<Pubkey> {
Expand All @@ -808,6 +800,88 @@ impl SealevelMailboxIndexer {
})?;
Ok(expected_pubkey)
}

async fn delivered_message_log_meta(
&self,
log_index: U256,
message_storage_pda_pubkey: &Pubkey,
message_account_slot: &Slot,
) -> ChainResult<LogMeta> {
let error_msg_no_txn = "block which should contain message delivery transaction does not contain any transaction".to_owned();
let error_msg_too_many_txns = "block contains more than one deliver message transaction operating on the same delivery message store PDA".to_owned();
let error_msg_no_txn_after_filtering = "block which should contain message delivery transaction does not contain any after filtering".to_owned();

self.log_meta(
log_index,
message_storage_pda_pubkey,
message_account_slot,
&is_message_delivery_instruction,
error_msg_no_txn,
error_msg_too_many_txns,
error_msg_no_txn_after_filtering,
)
.await
}

async fn log_meta<F>(
&self,
log_index: U256,
message_storage_pda_pubkey: &Pubkey,
message_account_slot: &Slot,
is_message_instruction: &F,
error_msg_no_txn: String,
error_msg_too_many_txns: String,
error_msg_no_txn_after_filtering: String,
) -> ChainResult<LogMeta>
where
F: Fn(instruction::Instruction) -> bool,
{
let block = self
.mailbox
.provider
.rpc()
.get_block(*message_account_slot)
.await?;

let block_hash = decode_h256(&block.blockhash)?;

let transactions = block
.transactions
.ok_or(HyperlaneSealevelError::NoTransactions(error_msg_no_txn))?;

let transaction_hashes = search_message_transactions(
&self.mailbox.program_id,
&message_storage_pda_pubkey,
transactions,
&is_message_instruction,
);

// We expect to see that there is only one message dispatch transaction
if transaction_hashes.len() > 1 {
Err(HyperlaneSealevelError::TooManyTransactions(
error_msg_too_many_txns,
))?
}

let (transaction_index, transaction_hash) =
transaction_hashes
.into_iter()
.next()
.ok_or(HyperlaneSealevelError::NoTransactions(
error_msg_no_txn_after_filtering,
))?;

let log_meta = LogMeta {
address: self.mailbox.program_id.to_bytes().into(),
block_number: *message_account_slot,
block_hash,
transaction_id: transaction_hash,
transaction_index: transaction_index as u64,
log_index,
};

Ok(log_meta)
}
}

#[async_trait]
Expand Down
65 changes: 43 additions & 22 deletions rust/main/chains/hyperlane-sealevel/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,35 @@ use hyperlane_core::H512;

use crate::utils::{decode_h512, from_base58};

/// This function searches for a transaction which dispatches Hyperlane message and returns
/// list of hashes of such transactions.
/// This function searches for a transaction which specified instruction on Hyperlane message and
/// returns list of hashes of such transactions.
///
/// This function takes the mailbox program identifier and the identifier for PDA for storing
/// a dispatched message and searches a message dispatch transaction in a list of transaction.
/// a dispatched or delivered message and searches a message dispatch or delivery transaction
/// in a list of transactions.
///
/// The list of transaction is usually comes from a block. The function returns list of hashes
/// of such transactions.
/// of such transactions and their relative index in the block.
///
/// The transaction will be searched with the following criteria:
/// 1. Transaction contains Mailbox program id in the list of accounts.
/// 2. Transaction contains dispatched message PDA in the list of accounts.
/// 3. Transaction is performing message dispatch (OutboxDispatch).
/// 2. Transaction contains dispatched/delivered message PDA in the list of accounts.
/// 3. Transaction is performing the specified message instruction.
///
/// * `mailbox_program_id` - Identifier of Mailbox program
/// * `message_storage_pda_pubkey` - Identifier for dispatch message store PDA
/// * `message_storage_pda_pubkey` - Identifier for message store PDA
/// * `transactions` - List of transactions
pub fn search_dispatched_message_transactions(
/// * `is_specified_message_instruction` - Function which returns `true` for specified message
/// instruction
pub fn search_message_transactions<F>(
mailbox_program_id: &Pubkey,
message_storage_pda_pubkey: &Pubkey,
transactions: Vec<EncodedTransactionWithStatusMeta>,
) -> Vec<(usize, H512)> {
is_specified_message_instruction: &F,
) -> Vec<(usize, H512)>
where
F: Fn(Instruction) -> bool,
{
transactions
.into_iter()
.enumerate()
Expand All @@ -43,38 +51,43 @@ pub fn search_dispatched_message_transactions(
.map(|(hash, account_keys, instructions)| (index, hash, account_keys, instructions))
})
.filter_map(|(index, hash, account_keys, instructions)| {
filter_not_relevant(
filter_by_relevancy(
mailbox_program_id,
message_storage_pda_pubkey,
hash,
account_keys,
instructions,
is_specified_message_instruction,
)
.map(|hash| (index, hash))
})
.collect::<Vec<(usize, H512)>>()
}

fn filter_not_relevant(
fn filter_by_relevancy<F>(
mailbox_program_id: &Pubkey,
message_storage_pda_pubkey: &Pubkey,
hash: H512,
account_keys: Vec<String>,
instructions: Vec<UiCompiledInstruction>,
) -> Option<H512> {
is_specified_message_instruction: &F,
) -> Option<H512>
where
F: Fn(Instruction) -> bool,
{
let account_index_map = account_index_map(account_keys);

let mailbox_program_id_str = mailbox_program_id.to_string();
let mailbox_program_index = match account_index_map.get(&mailbox_program_id_str) {
Some(i) => *i as u8,
None => return None, // If account keys do not contain Mailbox program, transaction is not message dispatch.
None => return None, // If account keys do not contain Mailbox program, transaction is not message dispatch/delivery.
};

let message_storage_pda_pubkey_str = message_storage_pda_pubkey.to_string();
let dispatch_message_pda_account_index =
let message_storage_pda_account_index =
match account_index_map.get(&message_storage_pda_pubkey_str) {
Some(i) => *i as u8,
None => return None, // If account keys do not contain dispatch message store PDA account, transaction is not message dispatch.
None => return None, // If account keys do not contain dispatch/delivery message store PDA account, transaction is not message dispatch/delivery.
};

let mailbox_program_maybe = instructions
Expand All @@ -83,35 +96,43 @@ fn filter_not_relevant(

let mailbox_program = match mailbox_program_maybe {
Some(p) => p,
None => return None, // If transaction does not contain call into Mailbox, transaction is not message dispatch.
None => return None, // If transaction does not contain call into Mailbox, transaction is not message dispatch/delivery.
};

// If Mailbox program does not operate on dispatch message store PDA account, transaction is not message dispatch.
// If Mailbox program does not operate on dispatch/delivery message store PDA account, transaction is not message dispatch/delivery.
if !mailbox_program
.accounts
.contains(&dispatch_message_pda_account_index)
.contains(&message_storage_pda_account_index)
{
return None;
}

let instruction_data = match from_base58(&mailbox_program.data) {
Ok(d) => d,
Err(_) => return None, // If we cannot decode instruction data, transaction is not message dispatch.
Err(_) => return None, // If we cannot decode instruction data, transaction is not message dispatch/delivery.
};

let instruction = match Instruction::from_instruction_data(&instruction_data) {
Ok(ii) => ii,
Err(_) => return None, // If we cannot parse instruction data, transaction is not message dispatch.
Err(_) => return None, // If we cannot parse instruction data, transaction is not message dispatch/delivery.
};

// If the call into Mailbox program is not OutboxDispatch, transaction is not message dispatch.
if !matches!(instruction, Instruction::OutboxDispatch(_)) {
// If the call into Mailbox program is not OutboxDispatch/InboxProcess, transaction is not message dispatch/delivery.
if is_specified_message_instruction(instruction) {
return None;
}

Some(hash)
}

pub fn is_message_dispatch_instruction(instruction: Instruction) -> bool {
!matches!(instruction, Instruction::OutboxDispatch(_))
}

pub fn is_message_delivery_instruction(instruction: Instruction) -> bool {
!matches!(instruction, Instruction::InboxProcess(_))
}

fn filter_by_validity(
tx: UiTransaction,
meta: UiTransactionStatusMeta,
Expand Down
Loading

0 comments on commit 5bebd6d

Please sign in to comment.