diff --git a/rust/main/agents/scraper/src/chain_scraper/mod.rs b/rust/main/agents/scraper/src/chain_scraper/mod.rs index 71ddd84efb..c23df70a16 100644 --- a/rust/main/agents/scraper/src/chain_scraper/mod.rs +++ b/rust/main/agents/scraper/src/chain_scraper/mod.rs @@ -9,7 +9,7 @@ use async_trait::async_trait; use eyre::Result; use hyperlane_base::settings::IndexSettings; use hyperlane_core::{ - unwrap_or_none_result, BlockInfo, Delivery, HyperlaneDomain, HyperlaneLogStore, + unwrap_or_none_result, BlockId, BlockInfo, Delivery, HyperlaneDomain, HyperlaneLogStore, HyperlaneMessage, HyperlaneProvider, HyperlaneSequenceAwareIndexerStoreReader, HyperlaneWatermarkedLogStore, Indexed, InterchainGasPayment, LogMeta, H256, }; @@ -78,13 +78,13 @@ impl HyperlaneSqlDb { &self, log_meta: impl Iterator, ) -> Result> { - let block_hash_by_txn_hash: HashMap = log_meta + let block_id_by_txn_hash: HashMap = log_meta .map(|meta| { ( meta.transaction_id .try_into() .expect("256-bit transaction ids are the maximum supported at this time"), - meta.block_hash, + BlockId::new(meta.block_hash, meta.block_number), ) }) .collect(); @@ -92,16 +92,16 @@ impl HyperlaneSqlDb { // all blocks we care about // hash of block maps to the block id and timestamp let blocks: HashMap<_, _> = self - .ensure_blocks(block_hash_by_txn_hash.values().copied()) + .ensure_blocks(block_id_by_txn_hash.values().copied()) .await? .map(|block| (block.hash, block)) .collect(); trace!(?blocks, "Ensured blocks"); // We ensure transactions only from blocks which are inserted into database - let txn_hash_with_block_ids = block_hash_by_txn_hash + let txn_hash_with_block_ids = block_id_by_txn_hash .into_iter() - .filter_map(move |(txn, block)| blocks.get(&block).map(|b| (txn, b.id))) + .filter_map(move |(txn, block)| blocks.get(&block.hash).map(|b| (txn, b.id))) .map(|(txn_hash, block_id)| TxnWithBlockId { txn_hash, block_id }); let txns_with_ids = self.ensure_txns(txn_hash_with_block_ids).await?; @@ -195,11 +195,17 @@ impl HyperlaneSqlDb { /// this method. async fn ensure_blocks( &self, - block_hashes: impl Iterator, + block_ids: impl Iterator, ) -> Result> { + // Mapping from block hash to block ids (hash and height) + let block_hash_to_block_id_map: HashMap = + block_ids.map(|b| (b.hash, b)).collect(); + // Mapping of block hash to `BasicBlock` which contains database block id and block hash. - let mut blocks: HashMap> = - block_hashes.map(|b| (b, None)).collect(); + let mut blocks: HashMap> = block_hash_to_block_id_map + .keys() + .map(|hash| (*hash, None)) + .collect(); let db_blocks: Vec = if !blocks.is_empty() { // check database to see which blocks we already know and fetch their IDs @@ -230,10 +236,14 @@ impl HyperlaneSqlDb { for chunk in as_chunks(blocks_to_fetch, CHUNK_SIZE) { debug_assert!(!chunk.is_empty()); for (hash, block_info) in chunk { - let info = match self.provider.get_block_by_hash(hash).await { + // We should have block_id in this map for every hashes + let block_id = block_hash_to_block_id_map[hash]; + let block_height = block_id.height; + + let info = match self.provider.get_block_by_height(block_height).await { Ok(info) => info, Err(e) => { - warn!(?hash, ?e, "error fetching and parsing block"); + warn!(block_hash = ?hash, ?block_height, ?e, "error fetching and parsing block"); continue; } }; diff --git a/rust/main/chains/hyperlane-cosmos/src/providers/cosmos/provider.rs b/rust/main/chains/hyperlane-cosmos/src/providers/cosmos/provider.rs index 491f197765..ef0c153458 100644 --- a/rust/main/chains/hyperlane-cosmos/src/providers/cosmos/provider.rs +++ b/rust/main/chains/hyperlane-cosmos/src/providers/cosmos/provider.rs @@ -18,8 +18,8 @@ use tracing::{error, warn}; use crypto::decompress_public_key; use hyperlane_core::{ AccountAddressType, BlockInfo, ChainCommunicationError, ChainInfo, ChainResult, - ContractLocator, HyperlaneChain, HyperlaneDomain, HyperlaneProvider, TxnInfo, TxnReceiptInfo, - H256, U256, + ContractLocator, HyperlaneChain, HyperlaneDomain, HyperlaneProvider, HyperlaneProviderError, + TxnInfo, TxnReceiptInfo, H256, U256, }; use crate::grpc::{WasmGrpcProvider, WasmProvider}; @@ -367,33 +367,26 @@ impl HyperlaneChain for CosmosProvider { #[async_trait] impl HyperlaneProvider for CosmosProvider { - async fn get_block_by_hash(&self, hash: &H256) -> ChainResult { - let tendermint_hash = Hash::from_bytes(Algorithm::Sha256, hash.as_bytes()) - .expect("block hash should be of correct size"); - - let response = self.rpc_client.get_block_by_hash(tendermint_hash).await?; + async fn get_block_by_height(&self, height: u64) -> ChainResult { + let response = self.rpc_client.get_block(height as u32).await?; - let received_hash = H256::from_slice(response.block_id.hash.as_bytes()); + let block = response.block; + let block_height = block.header.height.value(); - if &received_hash != hash { - return Err(ChainCommunicationError::from_other_str( - &format!("received incorrect block, expected hash: {hash:?}, received hash: {received_hash:?}") - )); + if block_height != height { + Err(HyperlaneProviderError::IncorrectBlockByHeight( + height, + block_height, + ))? } - let block = response.block.ok_or_else(|| { - ChainCommunicationError::from_other_str(&format!( - "empty block info for block: {:?}", - hash - )) - })?; - + let hash = H256::from_slice(response.block_id.hash.as_bytes()); let time: OffsetDateTime = block.header.time.into(); let block_info = BlockInfo { hash: hash.to_owned(), timestamp: time.unix_timestamp() as u64, - number: block.header.height.value(), + number: block_height, }; Ok(block_info) diff --git a/rust/main/chains/hyperlane-ethereum/src/rpc_clients/provider.rs b/rust/main/chains/hyperlane-ethereum/src/rpc_clients/provider.rs index 8434a4de39..c8ef3d78ce 100644 --- a/rust/main/chains/hyperlane-ethereum/src/rpc_clients/provider.rs +++ b/rust/main/chains/hyperlane-ethereum/src/rpc_clients/provider.rs @@ -49,26 +49,49 @@ where { #[instrument(err, skip(self))] #[allow(clippy::blocks_in_conditions)] // TODO: `rustc` 1.80.1 clippy issue - async fn get_block_by_hash(&self, hash: &H256) -> ChainResult { - let block = get_with_retry_on_none(hash, |h| { - let eth_h256: ethers_core_types::H256 = h.into(); - self.provider.get_block(eth_h256) - }) + async fn get_block_by_height(&self, height: u64) -> ChainResult { + let block = get_with_retry_on_none( + &height, + |h| self.provider.get_block(*h), + |h| HyperlaneProviderError::CouldNotFindBlockByHeight(*h), + ) .await?; - Ok(BlockInfo { - hash: *hash, + + let block_height = block + .number + .ok_or(HyperlaneProviderError::CouldNotFindBlockByHeight(height))? + .as_u64(); + + if block_height != height { + Err(HyperlaneProviderError::IncorrectBlockByHeight( + height, + block_height, + ))?; + } + + let block_hash = block + .hash + .ok_or(HyperlaneProviderError::BlockWithoutHash(height))?; + + let block_info = BlockInfo { + hash: block_hash.into(), timestamp: block.timestamp.as_u64(), - number: block - .number - .ok_or(HyperlaneProviderError::BlockIsNotPartOfChainYet(*hash))? - .as_u64(), - }) + number: block_height, + }; + + Ok(block_info) } #[instrument(err, skip(self))] #[allow(clippy::blocks_in_conditions)] // TODO: `rustc` 1.80.1 clippy issue async fn get_txn_by_hash(&self, hash: &H256) -> ChainResult { - let txn = get_with_retry_on_none(hash, |h| self.provider.get_transaction(*h)).await?; + let txn = get_with_retry_on_none( + hash, + |h| self.provider.get_transaction(*h), + |h| HyperlaneProviderError::CouldNotFindTransactionByHash(*h), + ) + .await?; + let receipt = self .provider .get_transaction_receipt(*hash) @@ -193,22 +216,24 @@ impl BuildableWithProvider for HyperlaneProviderBuilder { /// Call a get function that returns a Result> and retry if the inner /// option is None. This can happen because the provider has not discovered the /// object we are looking for yet. -async fn get_with_retry_on_none(hash: &H256, get: F) -> ChainResult +async fn get_with_retry_on_none( + id: &I, + get: F, + not_found_error: N, +) -> ChainResult where - F: Fn(&H256) -> O, + F: Fn(&I) -> O, O: Future, E>>, E: std::error::Error + Send + Sync + 'static, + N: Fn(&I) -> HyperlaneProviderError, { for _ in 0..3 { - if let Some(t) = get(hash) - .await - .map_err(ChainCommunicationError::from_other)? - { + if let Some(t) = get(id).await.map_err(ChainCommunicationError::from_other)? { return Ok(t); } else { sleep(Duration::from_secs(5)).await; continue; }; } - Err(HyperlaneProviderError::CouldNotFindObjectByHash(*hash).into()) + Err(not_found_error(id).into()) } diff --git a/rust/main/chains/hyperlane-fuel/src/provider.rs b/rust/main/chains/hyperlane-fuel/src/provider.rs index e3836f141a..a72f8a6c5f 100644 --- a/rust/main/chains/hyperlane-fuel/src/provider.rs +++ b/rust/main/chains/hyperlane-fuel/src/provider.rs @@ -1,7 +1,6 @@ use std::{collections::HashMap, ops::Deref}; use async_trait::async_trait; - use fuels::{ client::{FuelClient, PageDirection, PaginationRequest}, prelude::Provider, @@ -13,13 +12,14 @@ use fuels::{ transaction::{Transaction, TransactionType}, transaction_response::TransactionResponse, tx_status::TxStatus, - Address, Bytes32, ContractId, + Address, BlockHeight, Bytes32, ContractId, }, }; use futures::future::join_all; use hyperlane_core::{ BlockInfo, ChainCommunicationError, ChainInfo, ChainResult, HyperlaneChain, HyperlaneDomain, - HyperlaneMessage, HyperlaneProvider, Indexed, LogMeta, TxnInfo, H256, H512, U256, + HyperlaneMessage, HyperlaneProvider, HyperlaneProviderError, Indexed, LogMeta, TxnInfo, H256, + H512, U256, }; use crate::{make_client, make_provider, prelude::FuelIntoH256, ConnectionConf}; @@ -285,19 +285,30 @@ impl HyperlaneChain for FuelProvider { impl HyperlaneProvider for FuelProvider { /// Used by scraper #[allow(clippy::clone_on_copy)] // TODO: `rustc` 1.80.1 clippy issue - async fn get_block_by_hash(&self, hash: &H256) -> ChainResult { - let block_res = self.provider.block(&hash.0.into()).await.map_err(|e| { - ChainCommunicationError::CustomError(format!("Failed to get block: {}", e)) - })?; + async fn get_block_by_height(&self, height: u64) -> ChainResult { + let block_res = self + .provider + .block_by_height(BlockHeight::new(height as u32)) + .await + .map_err(|e| HyperlaneProviderError::CouldNotFindBlockByHeight(height))?; - match block_res { - Some(block) => Ok(BlockInfo { + let block_info = match block_res { + Some(block) => BlockInfo { hash: H256::from_slice(block.id.as_slice()), - number: block.header.height.into(), timestamp: block.header.time.map_or(0, |t| t.timestamp() as u64), - }), - None => Err(ChainCommunicationError::BlockNotFound(hash.clone())), + number: block.header.height.into(), + }, + None => Err(HyperlaneProviderError::CouldNotFindBlockByHeight(height))?, + }; + + if block_info.number != height { + Err(HyperlaneProviderError::IncorrectBlockByHeight( + height, + block_info.number, + ))?; } + + Ok(block_info) } /// Used by scraper diff --git a/rust/main/chains/hyperlane-sealevel/src/error.rs b/rust/main/chains/hyperlane-sealevel/src/error.rs index 55b81b4167..ff0142c393 100644 --- a/rust/main/chains/hyperlane-sealevel/src/error.rs +++ b/rust/main/chains/hyperlane-sealevel/src/error.rs @@ -14,6 +14,9 @@ pub enum HyperlaneSealevelError { /// ClientError error #[error("{0}")] ClientError(#[from] ClientError), + /// Decoding error + #[error("{0}")] + Decoding(#[from] solana_sdk::bs58::decode::Error), } impl From for ChainCommunicationError { diff --git a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs index 5453360e0b..5e348d9995 100644 --- a/rust/main/chains/hyperlane-sealevel/src/mailbox.rs +++ b/rust/main/chains/hyperlane-sealevel/src/mailbox.rs @@ -430,7 +430,7 @@ impl Mailbox for SealevelMailbox { let account = self .rpc() - .get_possible_account_with_finalized_commitment(&processed_message_account_key) + .get_account_option_with_finalized_commitment(&processed_message_account_key) .await?; Ok(account.is_some()) diff --git a/rust/main/chains/hyperlane-sealevel/src/provider.rs b/rust/main/chains/hyperlane-sealevel/src/provider.rs index b292d95941..aaf32ac593 100644 --- a/rust/main/chains/hyperlane-sealevel/src/provider.rs +++ b/rust/main/chains/hyperlane-sealevel/src/provider.rs @@ -1,11 +1,11 @@ use std::{str::FromStr, sync::Arc}; use async_trait::async_trait; - use hyperlane_core::{ - BlockInfo, ChainInfo, ChainResult, HyperlaneChain, HyperlaneDomain, HyperlaneProvider, TxnInfo, - H256, U256, + BlockInfo, ChainInfo, ChainResult, HyperlaneChain, HyperlaneDomain, HyperlaneProvider, + HyperlaneProviderError, TxnInfo, H256, U256, }; +use solana_sdk::bs58; use solana_sdk::pubkey::Pubkey; use crate::{error::HyperlaneSealevelError, ConnectionConf, SealevelRpcClient}; @@ -47,8 +47,25 @@ impl HyperlaneChain for SealevelProvider { #[async_trait] impl HyperlaneProvider for SealevelProvider { - async fn get_block_by_hash(&self, _hash: &H256) -> ChainResult { - todo!() // FIXME + async fn get_block_by_height(&self, slot: u64) -> ChainResult { + let confirmed_block = self.rpc_client.get_block(slot).await?; + + let hash_binary = bs58::decode(confirmed_block.blockhash) + .into_vec() + .map_err(HyperlaneSealevelError::Decoding)?; + let block_hash = H256::from_slice(&hash_binary); + + let block_time = confirmed_block + .block_time + .ok_or(HyperlaneProviderError::CouldNotFindBlockByHeight(slot))?; + + let block_info = BlockInfo { + hash: block_hash, + timestamp: block_time as u64, + number: slot, + }; + + Ok(block_info) } async fn get_txn_by_hash(&self, _hash: &H256) -> ChainResult { diff --git a/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs b/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs index 77f21ee1f0..44ba2c8d59 100644 --- a/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs +++ b/rust/main/chains/hyperlane-sealevel/src/rpc/client.rs @@ -3,8 +3,8 @@ use borsh::{BorshDeserialize, BorshSerialize}; use hyperlane_core::{ChainCommunicationError, ChainResult, U256}; use serializable_account_meta::{SerializableAccountMeta, SimulationReturnData}; use solana_client::{ - nonblocking::rpc_client::RpcClient, rpc_config::RpcProgramAccountsConfig, - rpc_response::Response, + nonblocking::rpc_client::RpcClient, rpc_config::RpcBlockConfig, + rpc_config::RpcProgramAccountsConfig, rpc_response::Response, }; use solana_sdk::{ account::Account, @@ -16,7 +16,9 @@ use solana_sdk::{ signature::{Keypair, Signature, Signer}, transaction::Transaction, }; -use solana_transaction_status::{TransactionStatus, UiReturnDataEncoding, UiTransactionReturnData}; +use solana_transaction_status::{ + TransactionStatus, UiConfirmedBlock, UiReturnDataEncoding, UiTransactionReturnData, +}; use crate::error::HyperlaneSealevelError; @@ -79,12 +81,12 @@ impl SealevelRpcClient { &self, pubkey: &Pubkey, ) -> ChainResult { - self.get_possible_account_with_finalized_commitment(pubkey) + self.get_account_option_with_finalized_commitment(pubkey) .await? .ok_or_else(|| ChainCommunicationError::from_other_str("Could not find account data")) } - pub async fn get_possible_account_with_finalized_commitment( + pub async fn get_account_option_with_finalized_commitment( &self, pubkey: &Pubkey, ) -> ChainResult> { @@ -97,6 +99,19 @@ impl SealevelRpcClient { Ok(account) } + pub async fn get_block(&self, height: u64) -> ChainResult { + let config = RpcBlockConfig { + commitment: Some(CommitmentConfig::finalized()), + max_supported_transaction_version: Some(0), + ..Default::default() + }; + self.0 + .get_block_with_config(height, config) + .await + .map_err(HyperlaneSealevelError::ClientError) + .map_err(Into::into) + } + pub async fn get_block_height(&self) -> ChainResult { let height = self .0 diff --git a/rust/main/hyperlane-core/src/error.rs b/rust/main/hyperlane-core/src/error.rs index fe1385c0ed..63c0f36558 100644 --- a/rust/main/hyperlane-core/src/error.rs +++ b/rust/main/hyperlane-core/src/error.rs @@ -93,9 +93,6 @@ pub enum ChainCommunicationError { /// Failed to parse strings or integers #[error("Data parsing error {0:?}")] StrOrIntParseError(#[from] StrOrIntParseError), - /// BlockNotFoundError - #[error("Block not found: {0:?}")] - BlockNotFound(H256), /// utf8 error #[error("{0}")] Utf8(#[from] FromUtf8Error), diff --git a/rust/main/hyperlane-core/src/traits/provider.rs b/rust/main/hyperlane-core/src/traits/provider.rs index 654e80218f..47070bc2ab 100644 --- a/rust/main/hyperlane-core/src/traits/provider.rs +++ b/rust/main/hyperlane-core/src/traits/provider.rs @@ -16,8 +16,8 @@ use crate::{BlockInfo, ChainInfo, ChainResult, HyperlaneChain, TxnInfo, H256, U2 #[async_trait] #[auto_impl(&, Box, Arc)] pub trait HyperlaneProvider: HyperlaneChain + Send + Sync + Debug { - /// Get block info for a given block hash - async fn get_block_by_hash(&self, hash: &H256) -> ChainResult; + /// Get block info for a given block height + async fn get_block_by_height(&self, height: u64) -> ChainResult; /// Get txn info for a given txn hash async fn get_txn_by_hash(&self, hash: &H256) -> ChainResult; @@ -35,13 +35,19 @@ pub trait HyperlaneProvider: HyperlaneChain + Send + Sync + Debug { /// Errors when querying for provider information. #[derive(Error, Debug)] pub enum HyperlaneProviderError { - /// The requested block hash is not yet known by the provider - #[error("Block is not part of chain yet {0:?}")] - BlockIsNotPartOfChainYet(H256), /// The provider did not return the gas which was used #[error("Provider did not return gas used")] NoGasUsed, - /// Could not find a transaction, block, or other object - #[error("Could not find object from provider with hash {0:?}")] - CouldNotFindObjectByHash(H256), + /// Could not find a transaction by hash + #[error("Could not find transaction from provider with hash {0:?}")] + CouldNotFindTransactionByHash(H256), + /// Could not find a block by height + #[error("Could not find block from provider with height {0:?}")] + CouldNotFindBlockByHeight(u64), + /// The requested block does not have its hash + #[error("Block with height {0:?} does not contain its hash")] + BlockWithoutHash(u64), + /// Incorrect block is received + #[error("Requested block with height {0:?}, received block with height {1:?}")] + IncorrectBlockByHeight(u64, u64), } diff --git a/rust/main/hyperlane-core/src/types/block_id.rs b/rust/main/hyperlane-core/src/types/block_id.rs new file mode 100644 index 0000000000..57f69045ed --- /dev/null +++ b/rust/main/hyperlane-core/src/types/block_id.rs @@ -0,0 +1,17 @@ +use crate::H256; + +/// Struct `BlockId` contains two types of identifiers for the same block: hash and height. +#[derive(Debug, Default, Copy, Clone)] +pub struct BlockId { + /// Block hash + pub hash: H256, + /// Block height + pub height: u64, +} + +impl BlockId { + /// Creates instance of `BlockId` struct + pub fn new(hash: H256, height: u64) -> Self { + Self { hash, height } + } +} diff --git a/rust/main/hyperlane-core/src/types/mod.rs b/rust/main/hyperlane-core/src/types/mod.rs index 07989e83ef..a8973a72b3 100644 --- a/rust/main/hyperlane-core/src/types/mod.rs +++ b/rust/main/hyperlane-core/src/types/mod.rs @@ -9,6 +9,7 @@ pub use self::primitive_types::*; pub use ::primitive_types as ethers_core_types; pub use account_address_type::AccountAddressType; pub use announcement::*; +pub use block_id::BlockId; pub use chain_data::*; pub use checkpoint::*; pub use indexing::*; @@ -23,6 +24,7 @@ use crate::{Decode, Encode, HyperlaneProtocolError}; /// This module contains enum for account address type mod account_address_type; mod announcement; +mod block_id; mod chain_data; mod checkpoint; mod indexing;