From e1a7c0ab019d8ce206f5274b14bda3ea7cfa2a41 Mon Sep 17 00:00:00 2001 From: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> Date: Tue, 20 Aug 2024 08:15:39 -0700 Subject: [PATCH] eth provider: split provider in multiple traits (#1337) * eth provider: split provider in multiple traits * replace EthereumProvider1 by EthereumProvider --- src/providers/eth_provider/blocks.rs | 116 ++++ src/providers/eth_provider/chain.rs | 43 ++ src/providers/eth_provider/gas.rs | 116 ++++ src/providers/eth_provider/logs.rs | 75 +++ src/providers/eth_provider/mod.rs | 8 + src/providers/eth_provider/provider.rs | 721 ++------------------- src/providers/eth_provider/receipts.rs | 58 ++ src/providers/eth_provider/state.rs | 213 ++++++ src/providers/eth_provider/transactions.rs | 204 ++++++ src/providers/eth_provider/tx_pool.rs | 36 + src/test_utils/eoa.rs | 4 +- src/test_utils/mock_provider.rs | 78 ++- tests/tests/debug_api.rs | 2 +- tests/tests/eth_provider.rs | 7 + tests/tests/kakarot_api.rs | 3 +- tests/tests/trace_api.rs | 2 +- tests/tests/tracer.rs | 2 +- 17 files changed, 992 insertions(+), 696 deletions(-) create mode 100644 src/providers/eth_provider/blocks.rs create mode 100644 src/providers/eth_provider/chain.rs create mode 100644 src/providers/eth_provider/gas.rs create mode 100644 src/providers/eth_provider/logs.rs create mode 100644 src/providers/eth_provider/receipts.rs create mode 100644 src/providers/eth_provider/state.rs create mode 100644 src/providers/eth_provider/transactions.rs create mode 100644 src/providers/eth_provider/tx_pool.rs diff --git a/src/providers/eth_provider/blocks.rs b/src/providers/eth_provider/blocks.rs new file mode 100644 index 000000000..5e7f54a85 --- /dev/null +++ b/src/providers/eth_provider/blocks.rs @@ -0,0 +1,116 @@ +use super::{ + database::ethereum::EthereumBlockStore, + error::{EthApiError, KakarotError}, +}; +use crate::providers::eth_provider::{ + database::ethereum::EthereumTransactionStore, + provider::{EthDataProvider, EthProviderResult}, +}; +use async_trait::async_trait; +use auto_impl::auto_impl; +use mongodb::bson::doc; +use reth_primitives::{BlockId, BlockNumberOrTag, B256, U256, U64}; +use reth_rpc_types::{Header, RichBlock}; +use tracing::Instrument; + +/// Ethereum block provider trait. +#[async_trait] +#[auto_impl(Arc, &)] +pub trait BlockProvider { + /// Get header by block id + async fn header(&self, block_id: &BlockId) -> EthProviderResult>; + + /// Returns the latest block number. + async fn block_number(&self) -> EthProviderResult; + + /// Returns a block by hash. Block can be full or just the hashes of the transactions. + async fn block_by_hash(&self, hash: B256, full: bool) -> EthProviderResult>; + + /// Returns a block by number. Block can be full or just the hashes of the transactions. + async fn block_by_number( + &self, + number_or_tag: BlockNumberOrTag, + full: bool, + ) -> EthProviderResult>; + + /// Returns the transaction count for a block by hash. + async fn block_transaction_count_by_hash(&self, hash: B256) -> EthProviderResult>; + + /// Returns the transaction count for a block by number. + async fn block_transaction_count_by_number( + &self, + number_or_tag: BlockNumberOrTag, + ) -> EthProviderResult>; + + /// Returns the transactions for a block. + async fn block_transactions( + &self, + block_id: Option, + ) -> EthProviderResult>>; +} + +#[async_trait] +impl BlockProvider for EthDataProvider +where + SP: starknet::providers::Provider + Send + Sync, +{ + async fn header(&self, block_id: &BlockId) -> EthProviderResult> { + let block_hash_or_number = self.block_id_into_block_number_or_hash(*block_id).await?; + Ok(self.database().header(block_hash_or_number).await?) + } + + async fn block_number(&self) -> EthProviderResult { + let block_number = match self.database().latest_header().await? { + // In case the database is empty, use the starknet provider + None => { + let span = tracing::span!(tracing::Level::INFO, "sn::block_number"); + U64::from(self.starknet_provider().block_number().instrument(span).await.map_err(KakarotError::from)?) + } + Some(header) => { + let number = header.number.ok_or(EthApiError::UnknownBlockNumber(None))?; + let is_pending_block = header.hash.unwrap_or_default().is_zero(); + U64::from(if is_pending_block { number - 1 } else { number }) + } + }; + Ok(block_number) + } + + async fn block_by_hash(&self, hash: B256, full: bool) -> EthProviderResult> { + Ok(self.database().block(hash.into(), full).await?) + } + + async fn block_by_number( + &self, + number_or_tag: BlockNumberOrTag, + full: bool, + ) -> EthProviderResult> { + let block_number = self.tag_into_block_number(number_or_tag).await?; + Ok(self.database().block(block_number.into(), full).await?) + } + + async fn block_transaction_count_by_hash(&self, hash: B256) -> EthProviderResult> { + self.database().transaction_count(hash.into()).await + } + + async fn block_transaction_count_by_number( + &self, + number_or_tag: BlockNumberOrTag, + ) -> EthProviderResult> { + let block_number = self.tag_into_block_number(number_or_tag).await?; + self.database().transaction_count(block_number.into()).await + } + + async fn block_transactions( + &self, + block_id: Option, + ) -> EthProviderResult>> { + let block_hash_or_number = self + .block_id_into_block_number_or_hash(block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))) + .await?; + if !self.database().block_exists(block_hash_or_number).await? { + return Ok(None); + } + + Ok(Some(self.database().transactions(block_hash_or_number).await?)) + } +} diff --git a/src/providers/eth_provider/chain.rs b/src/providers/eth_provider/chain.rs new file mode 100644 index 000000000..aef96c7fe --- /dev/null +++ b/src/providers/eth_provider/chain.rs @@ -0,0 +1,43 @@ +use crate::providers::eth_provider::{ + error::KakarotError, + provider::{EthDataProvider, EthProviderResult}, +}; +use async_trait::async_trait; +use auto_impl::auto_impl; +use reth_primitives::{U256, U64}; +use reth_rpc_types::{SyncInfo, SyncStatus}; +use starknet::core::types::SyncStatusType; +use tracing::Instrument; + +#[async_trait] +#[auto_impl(Arc, &)] +pub trait ChainProvider { + /// Returns the syncing status. + async fn syncing(&self) -> EthProviderResult; + + /// Returns the chain id. + async fn chain_id(&self) -> EthProviderResult>; +} + +#[async_trait] +impl ChainProvider for EthDataProvider +where + SP: starknet::providers::Provider + Send + Sync, +{ + async fn syncing(&self) -> EthProviderResult { + let span = tracing::span!(tracing::Level::INFO, "sn::syncing"); + Ok(match self.starknet_provider().syncing().instrument(span).await.map_err(KakarotError::from)? { + SyncStatusType::NotSyncing => SyncStatus::None, + SyncStatusType::Syncing(data) => SyncStatus::Info(SyncInfo { + starting_block: U256::from(data.starting_block_num), + current_block: U256::from(data.current_block_num), + highest_block: U256::from(data.highest_block_num), + ..Default::default() + }), + }) + } + + async fn chain_id(&self) -> EthProviderResult> { + Ok(Some(U64::from(self.chain_id))) + } +} diff --git a/src/providers/eth_provider/gas.rs b/src/providers/eth_provider/gas.rs new file mode 100644 index 000000000..42e331032 --- /dev/null +++ b/src/providers/eth_provider/gas.rs @@ -0,0 +1,116 @@ +use super::{ + constant::BLOCK_NUMBER_HEX_STRING_LEN, + error::{ExecutionError, KakarotError}, + starknet::kakarot_core::{core::KakarotCoreReader, KAKAROT_ADDRESS}, +}; +use crate::{ + into_via_wrapper, + models::felt::Felt252Wrapper, + providers::eth_provider::{ + database::{filter::format_hex, types::header::StoredHeader}, + provider::{EthDataProvider, EthProviderResult}, + }, +}; +use async_trait::async_trait; +use auto_impl::auto_impl; +use eyre::eyre; +use mongodb::bson::doc; +use reth_primitives::{BlockId, BlockNumberOrTag, U256, U64}; +use reth_rpc_types::{FeeHistory, TransactionRequest}; +use tracing::Instrument; + +#[async_trait] +#[auto_impl(Arc, &)] +pub trait GasProvider { + /// Returns the result of a estimate gas. + async fn estimate_gas(&self, call: TransactionRequest, block_id: Option) -> EthProviderResult; + + /// Returns the fee history given a block count and a newest block number. + async fn fee_history( + &self, + block_count: U64, + newest_block: BlockNumberOrTag, + reward_percentiles: Option>, + ) -> EthProviderResult; + + /// Returns the current gas price. + async fn gas_price(&self) -> EthProviderResult; +} + +#[async_trait] +impl GasProvider for EthDataProvider +where + SP: starknet::providers::Provider + Send + Sync, +{ + async fn estimate_gas(&self, request: TransactionRequest, block_id: Option) -> EthProviderResult { + // Set a high gas limit to make sure the transaction will not fail due to gas. + let request = TransactionRequest { gas: Some(u128::from(u64::MAX)), ..request }; + + let gas_used = self.estimate_gas_helper(request, block_id).await?; + + // Increase the gas used by 20% to make sure the transaction will not fail due to gas. + // This is a temporary solution until we have a proper gas estimation. + // Does not apply to Hive feature otherwise end2end tests will fail. + let gas_used = if cfg!(feature = "hive") { gas_used } else { gas_used * 120 / 100 }; + Ok(U256::from(gas_used)) + } + + async fn fee_history( + &self, + block_count: U64, + newest_block: BlockNumberOrTag, + _reward_percentiles: Option>, + ) -> EthProviderResult { + if block_count == U64::ZERO { + return Ok(FeeHistory::default()); + } + + let end_block = self.tag_into_block_number(newest_block).await?; + let end_block_plus_one = end_block.saturating_add(1); + + // 0 <= start_block <= end_block + let start_block = end_block_plus_one.saturating_sub(block_count.to()); + + let header_filter = doc! {"$and": [ { "header.number": { "$gte": format_hex(start_block, BLOCK_NUMBER_HEX_STRING_LEN) } }, { "header.number": { "$lte": format_hex(end_block, BLOCK_NUMBER_HEX_STRING_LEN) } } ] }; + let blocks: Vec = self.database().get(header_filter, None).await?; + + if blocks.is_empty() { + return Err( + KakarotError::from(mongodb::error::Error::custom(eyre!("No blocks found in the database"))).into() + ); + } + + let gas_used_ratio = blocks + .iter() + .map(|header| { + let gas_used = header.gas_used as f64; + let mut gas_limit = header.gas_limit as f64; + if gas_limit == 0. { + gas_limit = 1.; + }; + gas_used / gas_limit + }) + .collect(); + + let mut base_fee_per_gas = + blocks.iter().map(|header| header.base_fee_per_gas.unwrap_or_default()).collect::>(); + // TODO(EIP1559): Remove this when proper base fee computation: if gas_ratio > 50%, increase base_fee_per_gas + base_fee_per_gas.extend_from_within((base_fee_per_gas.len() - 1)..); + + Ok(FeeHistory { + base_fee_per_gas, + gas_used_ratio, + oldest_block: start_block, + reward: Some(vec![]), + ..Default::default() + }) + } + + async fn gas_price(&self) -> EthProviderResult { + let kakarot_contract = KakarotCoreReader::new(*KAKAROT_ADDRESS, self.starknet_provider()); + let span = tracing::span!(tracing::Level::INFO, "sn::base_fee"); + let gas_price = + kakarot_contract.get_base_fee().call().instrument(span).await.map_err(ExecutionError::from)?.base_fee; + Ok(into_via_wrapper!(gas_price)) + } +} diff --git a/src/providers/eth_provider/logs.rs b/src/providers/eth_provider/logs.rs new file mode 100644 index 000000000..416e80624 --- /dev/null +++ b/src/providers/eth_provider/logs.rs @@ -0,0 +1,75 @@ +use super::{ + constant::MAX_LOGS, + database::{filter::EthDatabaseFilterBuilder, types::log::StoredLog}, + error::EthApiError, +}; +use crate::providers::eth_provider::{ + blocks::BlockProvider, + database::{ + filter::{self}, + FindOpts, + }, + provider::{EthDataProvider, EthProviderResult}, +}; +use async_trait::async_trait; +use auto_impl::auto_impl; +use reth_rpc_types::{Filter, FilterChanges}; + +#[async_trait] +#[auto_impl(Arc, &)] +pub trait LogProvider: BlockProvider { + async fn get_logs(&self, filter: Filter) -> EthProviderResult; +} + +#[async_trait] +impl LogProvider for EthDataProvider +where + SP: starknet::providers::Provider + Send + Sync, +{ + async fn get_logs(&self, filter: Filter) -> EthProviderResult { + let block_hash = filter.get_block_hash(); + + // Create the database filter. + let mut builder = EthDatabaseFilterBuilder::::default(); + builder = if block_hash.is_some() { + // We filter by block hash on matching the exact block hash. + builder.with_block_hash(&block_hash.unwrap()) + } else { + let current_block = self.block_number().await?; + let current_block = + current_block.try_into().map_err(|_| EthApiError::UnknownBlockNumber(Some(current_block.to())))?; + + let from = filter.get_from_block().unwrap_or_default(); + let to = filter.get_to_block().unwrap_or(current_block); + + let (from, to) = match (from, to) { + (from, to) if from > current_block || to < from => return Ok(FilterChanges::Empty), + (from, to) if to > current_block => (from, current_block), + other => other, + }; + // We filter by block number using $gte and $lte. + builder.with_block_number_range(from, to) + }; + + // TODO: this will work for now but isn't very efficient. Would need to: + // 1. Create the bloom filter from the topics + // 2. Query the database for logs within block range with the bloom filter + // 3. Filter this reduced set of logs by the topics + // 4. Limit the number of logs returned + + // Convert the topics to a MongoDB filter and add it to the database filter + builder = builder.with_topics(&filter.topics); + + // Add the addresses + builder = builder.with_addresses(&filter.address.into_iter().collect::>()); + + Ok(FilterChanges::Logs( + self.database() + .get_and_map_to::<_, StoredLog>( + builder.build(), + (*MAX_LOGS).map(|limit| FindOpts::default().with_limit(limit)), + ) + .await?, + )) + } +} diff --git a/src/providers/eth_provider/mod.rs b/src/providers/eth_provider/mod.rs index 559b22bb3..594c9de6a 100644 --- a/src/providers/eth_provider/mod.rs +++ b/src/providers/eth_provider/mod.rs @@ -1,7 +1,15 @@ +pub mod blocks; +pub mod chain; pub mod constant; pub mod contracts; pub mod database; pub mod error; +pub mod gas; +pub mod logs; pub mod provider; +pub mod receipts; pub mod starknet; +pub mod state; +pub mod transactions; +pub mod tx_pool; pub mod utils; diff --git a/src/providers/eth_provider/provider.rs b/src/providers/eth_provider/provider.rs index 3f3d3ee0b..77f1e5b55 100644 --- a/src/providers/eth_provider/provider.rs +++ b/src/providers/eth_provider/provider.rs @@ -1,170 +1,58 @@ use super::{ - constant::{BLOCK_NUMBER_HEX_STRING_LEN, CALL_REQUEST_GAS_LIMIT, HASH_HEX_STRING_LEN, MAX_LOGS}, - database::{ - ethereum::EthereumBlockStore, - filter::EthDatabaseFilterBuilder, - state::{EthCacheDatabase, EthDatabase}, - types::{ - header::StoredHeader, - log::StoredLog, - receipt::StoredTransactionReceipt, - transaction::{StoredPendingTransaction, StoredTransaction}, - }, - CollectionName, Database, + constant::CALL_REQUEST_GAS_LIMIT, + database::{ethereum::EthereumBlockStore, Database}, + error::{EthApiError, EthereumDataFormatError, EvmError, ExecutionError, TransactionError}, + starknet::kakarot_core::{ + self, + core::{CallInput, KakarotCoreReader, Uint256}, + KAKAROT_ADDRESS, }, - error::{ - EthApiError, EthereumDataFormatError, EvmError, ExecutionError, KakarotError, SignatureError, TransactionError, - }, - starknet::{ - kakarot_core::{ - self, - account_contract::AccountContractReader, - core::{CallInput, KakarotCoreReader, Uint256}, - starknet_address, to_starknet_transaction, KAKAROT_ADDRESS, - }, - ERC20Reader, STARKNET_NATIVE_TOKEN, - }, - utils::{class_hash_not_declared, contract_not_found, entrypoint_not_found, split_u256}, }; use crate::{ into_via_try_wrapper, into_via_wrapper, models::{ block::{EthBlockId, EthBlockNumberOrTag}, felt::Felt252Wrapper, - transaction::validate_transaction, }, - providers::eth_provider::database::{ - ethereum::EthereumTransactionStore, - filter::{self, format_hex}, - FindOpts, + providers::eth_provider::{ + blocks::BlockProvider, gas::GasProvider, logs::LogProvider, receipts::ReceiptProvider, state::StateProvider, + transactions::TransactionProvider, tx_pool::TxPoolProvider, }, }; -use alloy_rlp::Decodable; use async_trait::async_trait; -use auto_impl::auto_impl; use cainome::cairo_serde::CairoArrayLegacy; -use eyre::{eyre, Result}; +use eyre::Result; use itertools::Itertools; use mongodb::bson::doc; use num_traits::cast::ToPrimitive; -use reth_evm_ethereum::EthEvmConfig; -use reth_node_api::ConfigureEvm; -use reth_primitives::{ - Address, BlockId, BlockNumberOrTag, Bytes, TransactionSigned, TransactionSignedEcRecovered, TxKind, B256, U256, U64, -}; -use reth_revm::{ - db::CacheDB, - primitives::{BlockEnv, CfgEnv, CfgEnvWithHandlerCfg, HandlerCfg, SpecId}, -}; -use reth_rpc_eth_types::{error::ensure_success, revm_utils::prepare_call_env}; -use reth_rpc_types::{ - serde_helpers::JsonStorageKey, - state::{EvmOverrides, StateOverride}, - txpool::TxpoolContent, - BlockHashOrNumber, BlockOverrides, FeeHistory, Filter, FilterChanges, Header, Index, RichBlock, SyncInfo, - SyncStatus, Transaction, TransactionReceipt, TransactionRequest, -}; -use reth_rpc_types_compat::transaction::from_recovered; +use reth_primitives::{BlockId, BlockNumberOrTag, TxKind, U256}; + +use reth_rpc_types::{BlockHashOrNumber, TransactionRequest}; +use starknet::core::types::Felt; +use tracing::{instrument, Instrument}; #[cfg(feature = "hive")] -use starknet::core::types::BroadcastedInvokeTransaction; -use starknet::core::{ - types::{Felt, SyncStatusType}, - utils::get_storage_var_address, +use { + crate::providers::eth_provider::error::{KakarotError, SignatureError}, + crate::providers::eth_provider::starknet::kakarot_core::{ + account_contract::AccountContractReader, starknet_address, + }, + crate::providers::eth_provider::utils::contract_not_found, + reth_primitives::Address, + starknet::core::types::BroadcastedInvokeTransaction, }; -use tracing::{instrument, Instrument}; pub type EthProviderResult = Result; -/// Ethereum provider trait. Used to abstract away the database and the network. #[async_trait] -#[auto_impl(Arc, &)] -pub trait EthereumProvider { - /// Get header by block id - async fn header(&self, block_id: &BlockId) -> EthProviderResult>; - /// Returns the latest block number. - async fn block_number(&self) -> EthProviderResult; - /// Returns the syncing status. - async fn syncing(&self) -> EthProviderResult; - /// Returns the chain id. - async fn chain_id(&self) -> EthProviderResult>; - /// Returns a block by hash. Block can be full or just the hashes of the transactions. - async fn block_by_hash(&self, hash: B256, full: bool) -> EthProviderResult>; - /// Returns a block by number. Block can be full or just the hashes of the transactions. - async fn block_by_number( - &self, - number_or_tag: BlockNumberOrTag, - full: bool, - ) -> EthProviderResult>; - /// Returns the transaction count for a block by hash. - async fn block_transaction_count_by_hash(&self, hash: B256) -> EthProviderResult>; - /// Returns the transaction count for a block by number. - async fn block_transaction_count_by_number( - &self, - number_or_tag: BlockNumberOrTag, - ) -> EthProviderResult>; - /// Returns the transaction by hash. - async fn transaction_by_hash(&self, hash: B256) -> EthProviderResult>; - /// Returns the transaction by block hash and index. - async fn transaction_by_block_hash_and_index( - &self, - hash: B256, - index: Index, - ) -> EthProviderResult>; - /// Returns the transaction by block number and index. - async fn transaction_by_block_number_and_index( - &self, - number_or_tag: BlockNumberOrTag, - index: Index, - ) -> EthProviderResult>; - /// Returns the transaction receipt by hash of the transaction. - async fn transaction_receipt(&self, hash: B256) -> EthProviderResult>; - /// Returns the balance of an address in native eth. - async fn balance(&self, address: Address, block_id: Option) -> EthProviderResult; - /// Returns the storage of an address at a certain index. - async fn storage_at( - &self, - address: Address, - index: JsonStorageKey, - block_id: Option, - ) -> EthProviderResult; - /// Returns the nonce for the address at the given block. - async fn transaction_count(&self, address: Address, block_id: Option) -> EthProviderResult; - /// Returns the code for the address at the given block. - async fn get_code(&self, address: Address, block_id: Option) -> EthProviderResult; - /// Returns the logs for the given filter. - async fn get_logs(&self, filter: Filter) -> EthProviderResult; - /// Returns the result of a call. - async fn call( - &self, - request: TransactionRequest, - block_id: Option, - state_overrides: Option, - block_overrides: Option>, - ) -> EthProviderResult; - /// Returns the result of a estimate gas. - async fn estimate_gas(&self, call: TransactionRequest, block_id: Option) -> EthProviderResult; - /// Returns the fee history given a block count and a newest block number. - async fn fee_history( - &self, - block_count: U64, - newest_block: BlockNumberOrTag, - reward_percentiles: Option>, - ) -> EthProviderResult; - /// Send a raw transaction to the network and returns the transactions hash. - async fn send_raw_transaction(&self, transaction: Bytes) -> EthProviderResult; - /// Returns the current gas price. - async fn gas_price(&self) -> EthProviderResult; - /// Returns the block receipts for a block. - async fn block_receipts(&self, block_id: Option) -> EthProviderResult>>; - /// Returns the transactions for a block. - async fn block_transactions( - &self, - block_id: Option, - ) -> EthProviderResult>>; - /// Returns a vec of pending pool transactions. - async fn txpool_transactions(&self) -> EthProviderResult>; - /// Returns the content of the pending pool. - async fn txpool_content(&self) -> EthProviderResult; +pub trait EthereumProvider: + GasProvider + StateProvider + TransactionProvider + ReceiptProvider + LogProvider + TxPoolProvider +{ +} + +#[async_trait] +impl EthereumProvider for T where + T: GasProvider + StateProvider + TransactionProvider + ReceiptProvider + LogProvider + TxPoolProvider +{ } /// Structure that implements the `EthereumProvider` trait. @@ -174,7 +62,7 @@ pub trait EthereumProvider { pub struct EthDataProvider { database: Database, starknet_provider: SP, - chain_id: u64, + pub(crate) chain_id: u64, } impl EthDataProvider @@ -185,527 +73,10 @@ where pub const fn database(&self) -> &Database { &self.database } -} - -#[async_trait] -impl EthereumProvider for EthDataProvider -where - SP: starknet::providers::Provider + Send + Sync, -{ - async fn header(&self, block_id: &BlockId) -> EthProviderResult> { - let block_hash_or_number = self.block_id_into_block_number_or_hash(*block_id).await?; - Ok(self.database.header(block_hash_or_number).await?) - } - - async fn block_number(&self) -> EthProviderResult { - let block_number = match self.database.latest_header().await? { - // In case the database is empty, use the starknet provider - None => { - let span = tracing::span!(tracing::Level::INFO, "sn::block_number"); - U64::from(self.starknet_provider.block_number().instrument(span).await.map_err(KakarotError::from)?) - } - Some(header) => { - let number = header.number.ok_or(EthApiError::UnknownBlockNumber(None))?; - let is_pending_block = header.hash.unwrap_or_default().is_zero(); - U64::from(if is_pending_block { number - 1 } else { number }) - } - }; - Ok(block_number) - } - - async fn syncing(&self) -> EthProviderResult { - let span = tracing::span!(tracing::Level::INFO, "sn::syncing"); - Ok(match self.starknet_provider.syncing().instrument(span).await.map_err(KakarotError::from)? { - SyncStatusType::NotSyncing => SyncStatus::None, - SyncStatusType::Syncing(data) => SyncStatus::Info(SyncInfo { - starting_block: U256::from(data.starting_block_num), - current_block: U256::from(data.current_block_num), - highest_block: U256::from(data.highest_block_num), - ..Default::default() - }), - }) - } - - async fn chain_id(&self) -> EthProviderResult> { - Ok(Some(U64::from(self.chain_id))) - } - - async fn block_by_hash(&self, hash: B256, full: bool) -> EthProviderResult> { - Ok(self.database.block(hash.into(), full).await?) - } - - async fn block_by_number( - &self, - number_or_tag: BlockNumberOrTag, - full: bool, - ) -> EthProviderResult> { - let block_number = self.tag_into_block_number(number_or_tag).await?; - Ok(self.database.block(block_number.into(), full).await?) - } - - async fn block_transaction_count_by_hash(&self, hash: B256) -> EthProviderResult> { - self.database.transaction_count(hash.into()).await - } - - async fn block_transaction_count_by_number( - &self, - number_or_tag: BlockNumberOrTag, - ) -> EthProviderResult> { - let block_number = self.tag_into_block_number(number_or_tag).await?; - self.database.transaction_count(block_number.into()).await - } - - async fn transaction_by_hash(&self, hash: B256) -> EthProviderResult> { - let pipeline = vec![ - doc! { - // Union with pending transactions with only specified hash - "$unionWith": { - "coll": StoredPendingTransaction::collection_name(), - "pipeline": [ - { - "$match": { - "tx.hash": format_hex(hash, HASH_HEX_STRING_LEN) - } - } - ] - }, - }, - // Only specified hash in the transactions collection - doc! { - "$match": { - "tx.hash": format_hex(hash, HASH_HEX_STRING_LEN) - } - }, - // Sort in descending order by block number as pending transactions have null block number - doc! { - "$sort": { "tx.blockNumber" : -1 } - }, - // Only one document in the final result with priority to the final transactions collection if available - doc! { - "$limit": 1 - }, - ]; - - Ok(self.database.get_one_aggregate::(pipeline).await?.map(Into::into)) - } - - async fn transaction_by_block_hash_and_index( - &self, - hash: B256, - index: Index, - ) -> EthProviderResult> { - let filter = EthDatabaseFilterBuilder::::default() - .with_block_hash(&hash) - .with_tx_index(&index) - .build(); - Ok(self.database.get_one::(filter, None).await?.map(Into::into)) - } - - async fn transaction_by_block_number_and_index( - &self, - number_or_tag: BlockNumberOrTag, - index: Index, - ) -> EthProviderResult> { - let block_number = self.tag_into_block_number(number_or_tag).await?; - let filter = EthDatabaseFilterBuilder::::default() - .with_block_number(block_number) - .with_tx_index(&index) - .build(); - Ok(self.database.get_one::(filter, None).await?.map(Into::into)) - } - - async fn transaction_receipt(&self, hash: B256) -> EthProviderResult> { - let filter = EthDatabaseFilterBuilder::::default().with_tx_hash(&hash).build(); - Ok(self.database.get_one::(filter, None).await?.map(Into::into)) - } - - async fn balance(&self, address: Address, block_id: Option) -> EthProviderResult { - // Convert the optional Ethereum block ID to a Starknet block ID. - let starknet_block_id = self.to_starknet_block_id(block_id).await?; - - // Create a new `ERC20Reader` instance for the Starknet native token - let eth_contract = ERC20Reader::new(*STARKNET_NATIVE_TOKEN, &self.starknet_provider); - - // Call the `balanceOf` method on the contract for the given address and block ID, awaiting the result - let span = tracing::span!(tracing::Level::INFO, "sn::balance"); - let res = eth_contract - .balanceOf(&starknet_address(address)) - .block_id(starknet_block_id) - .call() - .instrument(span) - .await; - - // Check if the contract was not found or the class hash not declared, - // returning a default balance of 0 if true. - // The native token contract should be deployed on Kakarot, so this should not happen - // We want to avoid errors in this case and return a default balance of 0 - if contract_not_found(&res) || class_hash_not_declared(&res) { - return Ok(Default::default()); - } - // Otherwise, extract the balance from the result, converting any errors to ExecutionError - let balance = res.map_err(ExecutionError::from)?.balance; - - // Convert the low and high parts of the balance to U256 - let low: U256 = into_via_wrapper!(balance.low); - let high: U256 = into_via_wrapper!(balance.high); - - // Combine the low and high parts to form the final balance and return it - Ok(low + (high << 128)) - } - - async fn storage_at( - &self, - address: Address, - index: JsonStorageKey, - block_id: Option, - ) -> EthProviderResult { - let starknet_block_id = self.to_starknet_block_id(block_id).await?; - - let address = starknet_address(address); - let contract = AccountContractReader::new(address, &self.starknet_provider); - - let keys = split_u256(index.0); - let storage_address = get_storage_var_address("Account_storage", &keys).expect("Storage var name is not ASCII"); - - let span = tracing::span!(tracing::Level::INFO, "sn::storage"); - let maybe_storage = - contract.storage(&storage_address).block_id(starknet_block_id).call().instrument(span).await; - - if contract_not_found(&maybe_storage) || entrypoint_not_found(&maybe_storage) { - return Ok(U256::ZERO.into()); - } - - let storage = maybe_storage.map_err(ExecutionError::from)?.value; - let low: U256 = into_via_wrapper!(storage.low); - let high: U256 = into_via_wrapper!(storage.high); - let storage: U256 = low + (high << 128); - Ok(storage.into()) - } - - async fn transaction_count(&self, address: Address, block_id: Option) -> EthProviderResult { - let starknet_block_id = self.to_starknet_block_id(block_id).await?; - - let address = starknet_address(address); - let account_contract = AccountContractReader::new(address, &self.starknet_provider); - let span = tracing::span!(tracing::Level::INFO, "sn::kkrt_nonce"); - let maybe_nonce = account_contract.get_nonce().block_id(starknet_block_id).call().instrument(span).await; - - if contract_not_found(&maybe_nonce) || entrypoint_not_found(&maybe_nonce) { - return Ok(U256::ZERO); - } - let nonce = maybe_nonce.map_err(ExecutionError::from)?.nonce; - - // Get the protocol nonce as well, in edge cases where the protocol nonce is higher than the account nonce. - // This can happen when an underlying Starknet transaction reverts => Account storage changes are reverted, - // but the protocol nonce is still incremented. - let span = tracing::span!(tracing::Level::INFO, "sn::protocol_nonce"); - let protocol_nonce = - self.starknet_provider.get_nonce(starknet_block_id, address).instrument(span).await.unwrap_or_default(); - let nonce = nonce.max(protocol_nonce); - - Ok(into_via_wrapper!(nonce)) - } - - async fn get_code(&self, address: Address, block_id: Option) -> EthProviderResult { - let starknet_block_id = self.to_starknet_block_id(block_id).await?; - - let address = starknet_address(address); - let account_contract = AccountContractReader::new(address, &self.starknet_provider); - let span = tracing::span!(tracing::Level::INFO, "sn::code"); - let bytecode = account_contract.bytecode().block_id(starknet_block_id).call().instrument(span).await; - - if contract_not_found(&bytecode) || entrypoint_not_found(&bytecode) { - return Ok(Bytes::default()); - } - - let bytecode = bytecode.map_err(ExecutionError::from)?.bytecode.0; - - Ok(Bytes::from(bytecode.into_iter().filter_map(|x| x.to_u8()).collect::>())) - } - - async fn get_logs(&self, filter: Filter) -> EthProviderResult { - let block_hash = filter.get_block_hash(); - - // Create the database filter. - let mut builder = EthDatabaseFilterBuilder::::default(); - builder = if block_hash.is_some() { - // We filter by block hash on matching the exact block hash. - builder.with_block_hash(&block_hash.unwrap()) - } else { - let current_block = self.block_number().await?; - let current_block = - current_block.try_into().map_err(|_| EthApiError::UnknownBlockNumber(Some(current_block.to())))?; - - let from = filter.get_from_block().unwrap_or_default(); - let to = filter.get_to_block().unwrap_or(current_block); - - let (from, to) = match (from, to) { - (from, to) if from > current_block || to < from => return Ok(FilterChanges::Empty), - (from, to) if to > current_block => (from, current_block), - other => other, - }; - // We filter by block number using $gte and $lte. - builder.with_block_number_range(from, to) - }; - - // TODO: this will work for now but isn't very efficient. Would need to: - // 1. Create the bloom filter from the topics - // 2. Query the database for logs within block range with the bloom filter - // 3. Filter this reduced set of logs by the topics - // 4. Limit the number of logs returned - - // Convert the topics to a MongoDB filter and add it to the database filter - builder = builder.with_topics(&filter.topics); - - // Add the addresses - builder = builder.with_addresses(&filter.address.into_iter().collect::>()); - - Ok(FilterChanges::Logs( - self.database - .get_and_map_to::<_, StoredLog>( - builder.build(), - (*MAX_LOGS).map(|limit| FindOpts::default().with_limit(limit)), - ) - .await?, - )) - } - - async fn call( - &self, - request: TransactionRequest, - block_id: Option, - state_overrides: Option, - block_overrides: Option>, - ) -> EthProviderResult { - // Create the EVM overrides from the state and block overrides. - let evm_overrides = EvmOverrides::new(state_overrides, block_overrides); - - // Check if either state_overrides or block_overrides is present. - if evm_overrides.has_state() || evm_overrides.has_block() { - // Create the configuration environment with the chain ID. - let cfg_env = CfgEnv::default().with_chain_id(self.chain_id().await?.unwrap_or_default().to()); - - // Retrieve the block header details. - let Header { number, timestamp, miner, base_fee_per_gas, difficulty, .. } = - self.header(&block_id.unwrap_or_default()).await?.unwrap_or_default(); - - // Create the block environment with the retrieved header details and transaction request. - let block_env = BlockEnv { - number: U256::from(number.unwrap_or_default()), - timestamp: U256::from(timestamp), - gas_limit: U256::from(request.gas.unwrap_or_default()), - coinbase: miner, - basefee: U256::from(base_fee_per_gas.unwrap_or_default()), - prevrandao: Some(B256::from_slice(&difficulty.to_be_bytes::<32>()[..])), - ..Default::default() - }; - - // Combine the configuration environment with the handler configuration. - let cfg_env_with_handler_cfg = - CfgEnvWithHandlerCfg { cfg_env, handler_cfg: HandlerCfg::new(SpecId::CANCUN) }; - - // Create a snapshot of the Ethereum database using the block ID. - let mut db = EthCacheDatabase(CacheDB::new(EthDatabase::new(self, block_id.unwrap_or_default()))); - - // Prepare the call environment with the transaction request, gas limit, and overrides. - let env = prepare_call_env( - cfg_env_with_handler_cfg, - block_env, - request.clone(), - request.gas.unwrap_or_default().try_into().expect("Gas limit is too large"), - &mut db.0, - evm_overrides, - )?; - - // Execute the transaction using the configured EVM asynchronously. - let res = EthEvmConfig::default() - .evm_with_env(db.0, env) - .transact() - .map_err(|err| >::into(TransactionError::Call(err.into())))?; - - // Ensure the transaction was successful and return the result. - return Ok(ensure_success(res.result)?); - } - - // If no state or block overrides are present, call the helper function to execute the call. - let output = self.call_helper(request, block_id).await?; - Ok(Bytes::from(output.0.into_iter().filter_map(|x| x.to_u8()).collect::>())) - } - - async fn estimate_gas(&self, request: TransactionRequest, block_id: Option) -> EthProviderResult { - // Set a high gas limit to make sure the transaction will not fail due to gas. - let request = TransactionRequest { gas: Some(u128::from(u64::MAX)), ..request }; - - let gas_used = self.estimate_gas_helper(request, block_id).await?; - - // Increase the gas used by 20% to make sure the transaction will not fail due to gas. - // This is a temporary solution until we have a proper gas estimation. - // Does not apply to Hive feature otherwise end2end tests will fail. - let gas_used = if cfg!(feature = "hive") { gas_used } else { gas_used * 120 / 100 }; - Ok(U256::from(gas_used)) - } - - async fn fee_history( - &self, - block_count: U64, - newest_block: BlockNumberOrTag, - _reward_percentiles: Option>, - ) -> EthProviderResult { - if block_count == U64::ZERO { - return Ok(FeeHistory::default()); - } - - let end_block = self.tag_into_block_number(newest_block).await?; - let end_block_plus_one = end_block.saturating_add(1); - - // 0 <= start_block <= end_block - let start_block = end_block_plus_one.saturating_sub(block_count.to()); - - let header_filter = doc! {"$and": [ { "header.number": { "$gte": format_hex(start_block, BLOCK_NUMBER_HEX_STRING_LEN) } }, { "header.number": { "$lte": format_hex(end_block, BLOCK_NUMBER_HEX_STRING_LEN) } } ] }; - let blocks: Vec = self.database.get(header_filter, None).await?; - - if blocks.is_empty() { - return Err( - KakarotError::from(mongodb::error::Error::custom(eyre!("No blocks found in the database"))).into() - ); - } - - let gas_used_ratio = blocks - .iter() - .map(|header| { - let gas_used = header.gas_used as f64; - let mut gas_limit = header.gas_limit as f64; - if gas_limit == 0. { - gas_limit = 1.; - }; - gas_used / gas_limit - }) - .collect(); - - let mut base_fee_per_gas = - blocks.iter().map(|header| header.base_fee_per_gas.unwrap_or_default()).collect::>(); - // TODO(EIP1559): Remove this when proper base fee computation: if gas_ratio > 50%, increase base_fee_per_gas - base_fee_per_gas.extend_from_within((base_fee_per_gas.len() - 1)..); - - Ok(FeeHistory { - base_fee_per_gas, - gas_used_ratio, - oldest_block: start_block, - reward: Some(vec![]), - ..Default::default() - }) - } - - async fn send_raw_transaction(&self, transaction: Bytes) -> EthProviderResult { - // Decode the transaction data - let transaction_signed = TransactionSigned::decode(&mut transaction.0.as_ref()) - .map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::TransactionConversion))?; - - let chain_id: u64 = - self.chain_id().await?.unwrap_or_default().try_into().map_err(|_| TransactionError::InvalidChainId)?; - - // Validate the transaction - let latest_block_header = self.database.latest_header().await?.ok_or(EthApiError::UnknownBlockNumber(None))?; - validate_transaction(&transaction_signed, chain_id, &latest_block_header)?; - - // Recover the signer from the transaction - let signer = transaction_signed.recover_signer().ok_or(SignatureError::Recovery)?; - - // Get the number of retries for the transaction - let retries = self.database.pending_transaction_retries(&transaction_signed.hash).await?; - - // Upsert the transaction as pending in the database - let transaction = - from_recovered(TransactionSignedEcRecovered::from_signed_transaction(transaction_signed.clone(), signer)); - self.database.upsert_pending_transaction(transaction, retries).await?; - - // Convert the Ethereum transaction to a Starknet transaction - let starknet_transaction = to_starknet_transaction(&transaction_signed, signer, retries)?; - - // Deploy EVM transaction signer if Hive feature is enabled - #[cfg(feature = "hive")] - self.deploy_evm_transaction_signer(signer).await?; - - // Add the transaction to the Starknet provider - let span = tracing::span!(tracing::Level::INFO, "sn::add_invoke_transaction"); - let res = self - .starknet_provider - .add_invoke_transaction(starknet_transaction) - .instrument(span) - .await - .map_err(KakarotError::from)?; - - // Return transaction hash if testing feature is enabled, otherwise log and return Ethereum hash - if cfg!(feature = "testing") { - return Ok(B256::from_slice(&res.transaction_hash.to_bytes_be()[..])); - } - let hash = transaction_signed.hash(); - tracing::info!( - "Fired a transaction: Starknet Hash: {} --- Ethereum Hash: {}", - B256::from_slice(&res.transaction_hash.to_bytes_be()[..]), - hash - ); - - Ok(hash) - } - - async fn gas_price(&self) -> EthProviderResult { - let kakarot_contract = KakarotCoreReader::new(*KAKAROT_ADDRESS, &self.starknet_provider); - let span = tracing::span!(tracing::Level::INFO, "sn::base_fee"); - let gas_price = - kakarot_contract.get_base_fee().call().instrument(span).await.map_err(ExecutionError::from)?.base_fee; - Ok(into_via_wrapper!(gas_price)) - } - - async fn block_receipts(&self, block_id: Option) -> EthProviderResult>> { - match block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)) { - BlockId::Number(number_or_tag) => { - let block_number = self.tag_into_block_number(number_or_tag).await?; - if !self.database.block_exists(block_number.into()).await? { - return Ok(None); - } - - let filter = - EthDatabaseFilterBuilder::::default().with_block_number(block_number).build(); - let tx: Vec = self.database.get(filter, None).await?; - Ok(Some(tx.into_iter().map(Into::into).collect())) - } - BlockId::Hash(hash) => { - if !self.database.block_exists(hash.block_hash.into()).await? { - return Ok(None); - } - let filter = - EthDatabaseFilterBuilder::::default().with_block_hash(&hash.block_hash).build(); - Ok(Some(self.database.get_and_map_to::<_, StoredTransactionReceipt>(filter, None).await?)) - } - } - } - - async fn block_transactions( - &self, - block_id: Option, - ) -> EthProviderResult>> { - let block_hash_or_number = self - .block_id_into_block_number_or_hash(block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))) - .await?; - if !self.database.block_exists(block_hash_or_number).await? { - return Ok(None); - } - - Ok(Some(self.database.transactions(block_hash_or_number).await?)) - } - - async fn txpool_transactions(&self) -> EthProviderResult> { - let span = tracing::span!(tracing::Level::INFO, "sn::txpool"); - Ok(self.database.get_all_and_map_to::().instrument(span).await?) - } - - async fn txpool_content(&self) -> EthProviderResult { - Ok(self.txpool_transactions().await?.into_iter().fold(TxpoolContent::default(), |mut content, pending| { - content.pending.entry(pending.from).or_default().insert(pending.nonce.to_string(), pending); - content - })) + /// Returns a reference to the Starknet provider. + pub const fn starknet_provider(&self) -> &SP { + &self.starknet_provider } } @@ -723,11 +94,6 @@ where Ok(Self { database, starknet_provider, chain_id }) } - #[cfg(feature = "testing")] - pub const fn starknet_provider(&self) -> &SP { - &self.starknet_provider - } - /// Prepare the call input for an estimate gas or call from a transaction request. #[instrument(skip(self, request), name = "prepare_call")] async fn prepare_call_input( @@ -781,7 +147,7 @@ where } /// Call the Kakarot contract with the given request. - async fn call_helper( + pub(crate) async fn call_helper( &self, request: TransactionRequest, block_id: Option, @@ -820,7 +186,7 @@ where } /// Estimate the gas used in Kakarot for the given request. - async fn estimate_gas_helper( + pub(crate) async fn estimate_gas_helper( &self, request: TransactionRequest, block_id: Option, @@ -896,7 +262,7 @@ where /// Converts the given [`BlockNumberOrTag`] into a block number. #[instrument(skip(self))] - async fn tag_into_block_number(&self, tag: BlockNumberOrTag) -> EthProviderResult { + pub(crate) async fn tag_into_block_number(&self, tag: BlockNumberOrTag) -> EthProviderResult { match tag { // Converts the tag representing the earliest block into block number 0. BlockNumberOrTag::Earliest => Ok(0), @@ -913,7 +279,10 @@ where /// Converts the given [`BlockId`] into a [`BlockHashOrNumber`]. #[instrument(skip_all, ret)] - async fn block_id_into_block_number_or_hash(&self, block_id: BlockId) -> EthProviderResult { + pub(crate) async fn block_id_into_block_number_or_hash( + &self, + block_id: BlockId, + ) -> EthProviderResult { match block_id { BlockId::Hash(hash) => Ok(BlockHashOrNumber::Hash(hash.into())), BlockId::Number(number_or_tag) => Ok(self.tag_into_block_number(number_or_tag).await?.into()), @@ -928,7 +297,7 @@ where { /// Deploy the EVM transaction signer if a corresponding contract is not found on /// Starknet. - async fn deploy_evm_transaction_signer(&self, signer: Address) -> EthProviderResult<()> { + pub(crate) async fn deploy_evm_transaction_signer(&self, signer: Address) -> EthProviderResult<()> { use crate::providers::eth_provider::constant::{DEPLOY_WALLET, DEPLOY_WALLET_NONCE}; use starknet::{ accounts::{Call, ExecutionV1}, diff --git a/src/providers/eth_provider/receipts.rs b/src/providers/eth_provider/receipts.rs new file mode 100644 index 000000000..e65b8db55 --- /dev/null +++ b/src/providers/eth_provider/receipts.rs @@ -0,0 +1,58 @@ +use super::database::{filter::EthDatabaseFilterBuilder, types::receipt::StoredTransactionReceipt}; +use crate::providers::eth_provider::{ + database::{ + ethereum::EthereumBlockStore, + filter::{self}, + }, + provider::{EthDataProvider, EthProviderResult}, +}; +use async_trait::async_trait; +use auto_impl::auto_impl; +use mongodb::bson::doc; +use reth_primitives::{BlockId, BlockNumberOrTag, B256}; +use reth_rpc_types::TransactionReceipt; + +#[async_trait] +#[auto_impl(Arc, &)] +pub trait ReceiptProvider { + /// Returns the transaction receipt by hash of the transaction. + async fn transaction_receipt(&self, hash: B256) -> EthProviderResult>; + + /// Returns the block receipts for a block. + async fn block_receipts(&self, block_id: Option) -> EthProviderResult>>; +} + +#[async_trait] +impl ReceiptProvider for EthDataProvider +where + SP: starknet::providers::Provider + Send + Sync, +{ + async fn transaction_receipt(&self, hash: B256) -> EthProviderResult> { + let filter = EthDatabaseFilterBuilder::::default().with_tx_hash(&hash).build(); + Ok(self.database().get_one::(filter, None).await?.map(Into::into)) + } + + async fn block_receipts(&self, block_id: Option) -> EthProviderResult>> { + match block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)) { + BlockId::Number(number_or_tag) => { + let block_number = self.tag_into_block_number(number_or_tag).await?; + if !self.database().block_exists(block_number.into()).await? { + return Ok(None); + } + + let filter = + EthDatabaseFilterBuilder::::default().with_block_number(block_number).build(); + let tx: Vec = self.database().get(filter, None).await?; + Ok(Some(tx.into_iter().map(Into::into).collect())) + } + BlockId::Hash(hash) => { + if !self.database().block_exists(hash.block_hash.into()).await? { + return Ok(None); + } + let filter = + EthDatabaseFilterBuilder::::default().with_block_hash(&hash.block_hash).build(); + Ok(Some(self.database().get_and_map_to::<_, StoredTransactionReceipt>(filter, None).await?)) + } + } + } +} diff --git a/src/providers/eth_provider/state.rs b/src/providers/eth_provider/state.rs new file mode 100644 index 000000000..f4274f966 --- /dev/null +++ b/src/providers/eth_provider/state.rs @@ -0,0 +1,213 @@ +use super::{ + database::state::{EthCacheDatabase, EthDatabase}, + error::{EthApiError, ExecutionError, TransactionError}, + starknet::{ + kakarot_core::{account_contract::AccountContractReader, starknet_address}, + ERC20Reader, STARKNET_NATIVE_TOKEN, + }, + utils::{class_hash_not_declared, contract_not_found, entrypoint_not_found, split_u256}, +}; +use crate::{ + into_via_wrapper, + models::felt::Felt252Wrapper, + providers::eth_provider::{ + blocks::BlockProvider, + chain::ChainProvider, + provider::{EthDataProvider, EthProviderResult}, + }, +}; +use async_trait::async_trait; +use auto_impl::auto_impl; +use mongodb::bson::doc; +use num_traits::cast::ToPrimitive; +use reth_evm_ethereum::EthEvmConfig; +use reth_node_api::ConfigureEvm; +use reth_primitives::{Address, BlockId, Bytes, B256, U256}; +use reth_revm::{ + db::CacheDB, + primitives::{BlockEnv, CfgEnv, CfgEnvWithHandlerCfg, HandlerCfg, SpecId}, +}; +use reth_rpc_eth_types::{error::ensure_success, revm_utils::prepare_call_env}; +use reth_rpc_types::{ + serde_helpers::JsonStorageKey, + state::{EvmOverrides, StateOverride}, + BlockOverrides, Header, TransactionRequest, +}; +use starknet::core::utils::get_storage_var_address; +use tracing::Instrument; + +#[async_trait] +#[auto_impl(Arc, &)] +pub trait StateProvider: ChainProvider + BlockProvider { + /// Returns the balance of an address in native eth. + async fn balance(&self, address: Address, block_id: Option) -> EthProviderResult; + + /// Returns the storage of an address at a certain index. + async fn storage_at( + &self, + address: Address, + index: JsonStorageKey, + block_id: Option, + ) -> EthProviderResult; + + /// Returns the code for the address at the given block. + async fn get_code(&self, address: Address, block_id: Option) -> EthProviderResult; + + /// Returns the result of a call. + async fn call( + &self, + request: TransactionRequest, + block_id: Option, + state_overrides: Option, + block_overrides: Option>, + ) -> EthProviderResult; +} + +#[async_trait] +impl StateProvider for EthDataProvider +where + SP: starknet::providers::Provider + Send + Sync, +{ + async fn balance(&self, address: Address, block_id: Option) -> EthProviderResult { + // Convert the optional Ethereum block ID to a Starknet block ID. + let starknet_block_id = self.to_starknet_block_id(block_id).await?; + + // Create a new `ERC20Reader` instance for the Starknet native token + let eth_contract = ERC20Reader::new(*STARKNET_NATIVE_TOKEN, self.starknet_provider()); + + // Call the `balanceOf` method on the contract for the given address and block ID, awaiting the result + let span = tracing::span!(tracing::Level::INFO, "sn::balance"); + let res = eth_contract + .balanceOf(&starknet_address(address)) + .block_id(starknet_block_id) + .call() + .instrument(span) + .await; + + // Check if the contract was not found or the class hash not declared, + // returning a default balance of 0 if true. + // The native token contract should be deployed on Kakarot, so this should not happen + // We want to avoid errors in this case and return a default balance of 0 + if contract_not_found(&res) || class_hash_not_declared(&res) { + return Ok(Default::default()); + } + // Otherwise, extract the balance from the result, converting any errors to ExecutionError + let balance = res.map_err(ExecutionError::from)?.balance; + + // Convert the low and high parts of the balance to U256 + let low: U256 = into_via_wrapper!(balance.low); + let high: U256 = into_via_wrapper!(balance.high); + + // Combine the low and high parts to form the final balance and return it + Ok(low + (high << 128)) + } + + async fn storage_at( + &self, + address: Address, + index: JsonStorageKey, + block_id: Option, + ) -> EthProviderResult { + let starknet_block_id = self.to_starknet_block_id(block_id).await?; + + let address = starknet_address(address); + let contract = AccountContractReader::new(address, self.starknet_provider()); + + let keys = split_u256(index.0); + let storage_address = get_storage_var_address("Account_storage", &keys).expect("Storage var name is not ASCII"); + + let span = tracing::span!(tracing::Level::INFO, "sn::storage"); + let maybe_storage = + contract.storage(&storage_address).block_id(starknet_block_id).call().instrument(span).await; + + if contract_not_found(&maybe_storage) || entrypoint_not_found(&maybe_storage) { + return Ok(U256::ZERO.into()); + } + + let storage = maybe_storage.map_err(ExecutionError::from)?.value; + let low: U256 = into_via_wrapper!(storage.low); + let high: U256 = into_via_wrapper!(storage.high); + let storage: U256 = low + (high << 128); + + Ok(storage.into()) + } + + async fn get_code(&self, address: Address, block_id: Option) -> EthProviderResult { + let starknet_block_id = self.to_starknet_block_id(block_id).await?; + + let address = starknet_address(address); + let account_contract = AccountContractReader::new(address, self.starknet_provider()); + let span = tracing::span!(tracing::Level::INFO, "sn::code"); + let bytecode = account_contract.bytecode().block_id(starknet_block_id).call().instrument(span).await; + + if contract_not_found(&bytecode) || entrypoint_not_found(&bytecode) { + return Ok(Bytes::default()); + } + + let bytecode = bytecode.map_err(ExecutionError::from)?.bytecode.0; + + Ok(Bytes::from(bytecode.into_iter().filter_map(|x| x.to_u8()).collect::>())) + } + + async fn call( + &self, + request: TransactionRequest, + block_id: Option, + state_overrides: Option, + block_overrides: Option>, + ) -> EthProviderResult { + // Create the EVM overrides from the state and block overrides. + let evm_overrides = EvmOverrides::new(state_overrides, block_overrides); + + // Check if either state_overrides or block_overrides is present. + if evm_overrides.has_state() || evm_overrides.has_block() { + // Create the configuration environment with the chain ID. + let cfg_env = CfgEnv::default().with_chain_id(self.chain_id().await?.unwrap_or_default().to()); + + // Retrieve the block header details. + let Header { number, timestamp, miner, base_fee_per_gas, difficulty, .. } = + self.header(&block_id.unwrap_or_default()).await?.unwrap_or_default(); + + // Create the block environment with the retrieved header details and transaction request. + let block_env = BlockEnv { + number: U256::from(number.unwrap_or_default()), + timestamp: U256::from(timestamp), + gas_limit: U256::from(request.gas.unwrap_or_default()), + coinbase: miner, + basefee: U256::from(base_fee_per_gas.unwrap_or_default()), + prevrandao: Some(B256::from_slice(&difficulty.to_be_bytes::<32>()[..])), + ..Default::default() + }; + + // Combine the configuration environment with the handler configuration. + let cfg_env_with_handler_cfg = + CfgEnvWithHandlerCfg { cfg_env, handler_cfg: HandlerCfg::new(SpecId::CANCUN) }; + + // Create a snapshot of the Ethereum database using the block ID. + let mut db = EthCacheDatabase(CacheDB::new(EthDatabase::new(self, block_id.unwrap_or_default()))); + + // Prepare the call environment with the transaction request, gas limit, and overrides. + let env = prepare_call_env( + cfg_env_with_handler_cfg, + block_env, + request.clone(), + request.gas.unwrap_or_default().try_into().expect("Gas limit is too large"), + &mut db.0, + evm_overrides, + )?; + + // Execute the transaction using the configured EVM asynchronously. + let res = EthEvmConfig::default() + .evm_with_env(db.0, env) + .transact() + .map_err(|err| >::into(TransactionError::Call(err.into())))?; + + // Ensure the transaction was successful and return the result. + return Ok(ensure_success(res.result)?); + } + + // If no state or block overrides are present, call the helper function to execute the call. + let output = self.call_helper(request, block_id).await?; + Ok(Bytes::from(output.0.into_iter().filter_map(|x| x.to_u8()).collect::>())) + } +} diff --git a/src/providers/eth_provider/transactions.rs b/src/providers/eth_provider/transactions.rs new file mode 100644 index 000000000..c99fdf735 --- /dev/null +++ b/src/providers/eth_provider/transactions.rs @@ -0,0 +1,204 @@ +use super::{ + constant::HASH_HEX_STRING_LEN, + database::{ + ethereum::EthereumBlockStore, + filter::EthDatabaseFilterBuilder, + types::transaction::{StoredPendingTransaction, StoredTransaction}, + CollectionName, + }, + error::{EthApiError, EthereumDataFormatError, ExecutionError, KakarotError, SignatureError, TransactionError}, + starknet::kakarot_core::{account_contract::AccountContractReader, starknet_address, to_starknet_transaction}, + utils::{contract_not_found, entrypoint_not_found}, +}; +use crate::{ + into_via_wrapper, + models::{felt::Felt252Wrapper, transaction::validate_transaction}, + providers::eth_provider::{ + chain::ChainProvider, + database::{ + ethereum::EthereumTransactionStore, + filter::{self, format_hex}, + }, + provider::{EthDataProvider, EthProviderResult}, + }, +}; +use alloy_rlp::Decodable; +use async_trait::async_trait; +use auto_impl::auto_impl; +use mongodb::bson::doc; +use reth_primitives::{ + Address, BlockId, BlockNumberOrTag, Bytes, TransactionSigned, TransactionSignedEcRecovered, B256, U256, +}; +use reth_rpc_types::Index; +use reth_rpc_types_compat::transaction::from_recovered; +use tracing::Instrument; + +#[async_trait] +#[auto_impl(Arc, &)] +pub trait TransactionProvider: ChainProvider { + /// Returns the transaction by hash. + async fn transaction_by_hash(&self, hash: B256) -> EthProviderResult>; + + /// Returns the transaction by block hash and index. + async fn transaction_by_block_hash_and_index( + &self, + hash: B256, + index: Index, + ) -> EthProviderResult>; + + /// Returns the transaction by block number and index. + async fn transaction_by_block_number_and_index( + &self, + number_or_tag: BlockNumberOrTag, + index: Index, + ) -> EthProviderResult>; + + /// Returns the nonce for the address at the given block. + async fn transaction_count(&self, address: Address, block_id: Option) -> EthProviderResult; + + /// Send a raw transaction to the network and returns the transactions hash. + async fn send_raw_transaction(&self, transaction: Bytes) -> EthProviderResult; +} + +#[async_trait] +impl TransactionProvider for EthDataProvider +where + SP: starknet::providers::Provider + Send + Sync, +{ + async fn transaction_by_hash(&self, hash: B256) -> EthProviderResult> { + let pipeline = vec![ + doc! { + // Union with pending transactions with only specified hash + "$unionWith": { + "coll": StoredPendingTransaction::collection_name(), + "pipeline": [ + { + "$match": { + "tx.hash": format_hex(hash, HASH_HEX_STRING_LEN) + } + } + ] + }, + }, + // Only specified hash in the transactions collection + doc! { + "$match": { + "tx.hash": format_hex(hash, HASH_HEX_STRING_LEN) + } + }, + // Sort in descending order by block number as pending transactions have null block number + doc! { + "$sort": { "tx.blockNumber" : -1 } + }, + // Only one document in the final result with priority to the final transactions collection if available + doc! { + "$limit": 1 + }, + ]; + + Ok(self.database().get_one_aggregate::(pipeline).await?.map(Into::into)) + } + + async fn transaction_by_block_hash_and_index( + &self, + hash: B256, + index: Index, + ) -> EthProviderResult> { + let filter = EthDatabaseFilterBuilder::::default() + .with_block_hash(&hash) + .with_tx_index(&index) + .build(); + Ok(self.database().get_one::(filter, None).await?.map(Into::into)) + } + + async fn transaction_by_block_number_and_index( + &self, + number_or_tag: BlockNumberOrTag, + index: Index, + ) -> EthProviderResult> { + let block_number = self.tag_into_block_number(number_or_tag).await?; + let filter = EthDatabaseFilterBuilder::::default() + .with_block_number(block_number) + .with_tx_index(&index) + .build(); + Ok(self.database().get_one::(filter, None).await?.map(Into::into)) + } + + async fn transaction_count(&self, address: Address, block_id: Option) -> EthProviderResult { + let starknet_block_id = self.to_starknet_block_id(block_id).await?; + + let address = starknet_address(address); + let account_contract = AccountContractReader::new(address, self.starknet_provider()); + let span = tracing::span!(tracing::Level::INFO, "sn::kkrt_nonce"); + let maybe_nonce = account_contract.get_nonce().block_id(starknet_block_id).call().instrument(span).await; + + if contract_not_found(&maybe_nonce) || entrypoint_not_found(&maybe_nonce) { + return Ok(U256::ZERO); + } + let nonce = maybe_nonce.map_err(ExecutionError::from)?.nonce; + + // Get the protocol nonce as well, in edge cases where the protocol nonce is higher than the account nonce. + // This can happen when an underlying Starknet transaction reverts => Account storage changes are reverted, + // but the protocol nonce is still incremented. + let span = tracing::span!(tracing::Level::INFO, "sn::protocol_nonce"); + let protocol_nonce = + self.starknet_provider().get_nonce(starknet_block_id, address).instrument(span).await.unwrap_or_default(); + let nonce = nonce.max(protocol_nonce); + + Ok(into_via_wrapper!(nonce)) + } + + async fn send_raw_transaction(&self, transaction: Bytes) -> EthProviderResult { + // Decode the transaction data + let transaction_signed = TransactionSigned::decode(&mut transaction.0.as_ref()) + .map_err(|_| EthApiError::EthereumDataFormat(EthereumDataFormatError::TransactionConversion))?; + + let chain_id: u64 = + self.chain_id().await?.unwrap_or_default().try_into().map_err(|_| TransactionError::InvalidChainId)?; + + // Validate the transaction + let latest_block_header = + self.database().latest_header().await?.ok_or(EthApiError::UnknownBlockNumber(None))?; + validate_transaction(&transaction_signed, chain_id, &latest_block_header)?; + + // Recover the signer from the transaction + let signer = transaction_signed.recover_signer().ok_or(SignatureError::Recovery)?; + + // Get the number of retries for the transaction + let retries = self.database().pending_transaction_retries(&transaction_signed.hash).await?; + + // Upsert the transaction as pending in the database + let transaction = + from_recovered(TransactionSignedEcRecovered::from_signed_transaction(transaction_signed.clone(), signer)); + self.database().upsert_pending_transaction(transaction, retries).await?; + + // Convert the Ethereum transaction to a Starknet transaction + let starknet_transaction = to_starknet_transaction(&transaction_signed, signer, retries)?; + + // Deploy EVM transaction signer if Hive feature is enabled + #[cfg(feature = "hive")] + self.deploy_evm_transaction_signer(signer).await?; + + // Add the transaction to the Starknet provider + let span = tracing::span!(tracing::Level::INFO, "sn::add_invoke_transaction"); + let res = self + .starknet_provider() + .add_invoke_transaction(starknet_transaction) + .instrument(span) + .await + .map_err(KakarotError::from)?; + + // Return transaction hash if testing feature is enabled, otherwise log and return Ethereum hash + if cfg!(feature = "testing") { + return Ok(B256::from_slice(&res.transaction_hash.to_bytes_be()[..])); + } + let hash = transaction_signed.hash(); + tracing::info!( + "Fired a transaction: Starknet Hash: {} --- Ethereum Hash: {}", + B256::from_slice(&res.transaction_hash.to_bytes_be()[..]), + hash + ); + + Ok(hash) + } +} diff --git a/src/providers/eth_provider/tx_pool.rs b/src/providers/eth_provider/tx_pool.rs new file mode 100644 index 000000000..f37c92b6e --- /dev/null +++ b/src/providers/eth_provider/tx_pool.rs @@ -0,0 +1,36 @@ +use super::database::types::transaction::StoredPendingTransaction; +use crate::providers::eth_provider::provider::{EthDataProvider, EthProviderResult}; +use async_trait::async_trait; +use auto_impl::auto_impl; +use mongodb::bson::doc; +use reth_rpc_types::{txpool::TxpoolContent, Transaction}; +use tracing::Instrument; + +/// Ethereum provider trait. Used to abstract away the database and the network. +#[async_trait] +#[auto_impl(Arc, &)] +pub trait TxPoolProvider { + /// Returns a vec of pending pool transactions. + async fn txpool_transactions(&self) -> EthProviderResult>; + + /// Returns the content of the pending pool. + async fn txpool_content(&self) -> EthProviderResult; +} + +#[async_trait] +impl TxPoolProvider for EthDataProvider +where + SP: starknet::providers::Provider + Send + Sync, +{ + async fn txpool_transactions(&self) -> EthProviderResult> { + let span = tracing::span!(tracing::Level::INFO, "sn::txpool"); + Ok(self.database().get_all_and_map_to::().instrument(span).await?) + } + + async fn txpool_content(&self) -> EthProviderResult { + Ok(self.txpool_transactions().await?.into_iter().fold(TxpoolContent::default(), |mut content, pending| { + content.pending.entry(pending.from).or_default().insert(pending.nonce.to_string(), pending); + content + })) + } +} diff --git a/src/test_utils/eoa.rs b/src/test_utils/eoa.rs index 862ba38fb..382f587cb 100644 --- a/src/test_utils/eoa.rs +++ b/src/test_utils/eoa.rs @@ -1,8 +1,8 @@ use crate::{ models::felt::Felt252Wrapper, providers::eth_provider::{ - provider::{EthDataProvider, EthereumProvider}, - starknet::kakarot_core::starknet_address, + chain::ChainProvider, provider::EthDataProvider, starknet::kakarot_core::starknet_address, + transactions::TransactionProvider, }, test_utils::{ evm_contract::{EvmContract, KakarotEvmContract, TransactionInfo, TxCommonInfo, TxFeeMarketInfo}, diff --git a/src/test_utils/mock_provider.rs b/src/test_utils/mock_provider.rs index 69dd2dae8..325409ec4 100644 --- a/src/test_utils/mock_provider.rs +++ b/src/test_utils/mock_provider.rs @@ -1,4 +1,7 @@ -use crate::providers::eth_provider::provider::{EthProviderResult, EthereumProvider}; +use crate::providers::eth_provider::{ + blocks::BlockProvider, chain::ChainProvider, gas::GasProvider, logs::LogProvider, provider::EthProviderResult, + receipts::ReceiptProvider, state::StateProvider, transactions::TransactionProvider, tx_pool::TxPoolProvider, +}; use async_trait::async_trait; use mockall::mock; use reth_primitives::{Address, BlockId, BlockNumberOrTag, Bytes, B256, U256, U64}; @@ -10,33 +13,80 @@ mock! { #[derive(Clone, Debug)] pub EthereumProviderStruct {} + #[async_trait] - impl EthereumProvider for EthereumProviderStruct { + impl BlockProvider for EthereumProviderStruct { async fn header(&self, block_id: &BlockId) -> EthProviderResult>; + async fn block_number(&self) -> EthProviderResult; - async fn syncing(&self) -> EthProviderResult; - async fn chain_id(&self) -> EthProviderResult>; + async fn block_by_hash(&self, hash: B256, full: bool) -> EthProviderResult>; + async fn block_by_number(&self, number_or_tag: BlockNumberOrTag, full: bool) -> EthProviderResult>; + async fn block_transaction_count_by_hash(&self, hash: B256) -> EthProviderResult>; + async fn block_transaction_count_by_number(&self, number_or_tag: BlockNumberOrTag) -> EthProviderResult>; - async fn transaction_by_hash(&self, hash: B256) -> EthProviderResult>; - async fn transaction_by_block_hash_and_index(&self, hash: B256, index: reth_rpc_types::Index) -> EthProviderResult>; - async fn transaction_by_block_number_and_index(&self, number_or_tag: BlockNumberOrTag, index: reth_rpc_types::Index) -> EthProviderResult>; + + async fn block_transactions(&self, block_id: Option) -> EthProviderResult>>; + } + + #[async_trait] + impl ChainProvider for EthereumProviderStruct { + async fn syncing(&self) -> EthProviderResult; + + async fn chain_id(&self) -> EthProviderResult>; + } + + #[async_trait] + impl GasProvider for EthereumProviderStruct { + async fn estimate_gas(&self, call: TransactionRequest, block_id: Option) -> EthProviderResult; + + async fn fee_history(&self, block_count: U64, newest_block: BlockNumberOrTag, reward_percentiles: Option>) -> EthProviderResult; + + async fn gas_price(&self) -> EthProviderResult; + } + + #[async_trait] + impl LogProvider for EthereumProviderStruct { + async fn get_logs(&self, filter: Filter) -> EthProviderResult; + } + + #[async_trait] + impl ReceiptProvider for EthereumProviderStruct { async fn transaction_receipt(&self, hash: B256) -> EthProviderResult>; + + async fn block_receipts(&self, block_id: Option) -> EthProviderResult>>; + } + + #[async_trait] + impl StateProvider for EthereumProviderStruct { async fn balance(&self, address: Address, block_id: Option) -> EthProviderResult; + async fn storage_at(&self, address: Address, index: reth_rpc_types::serde_helpers::JsonStorageKey, block_id: Option) -> EthProviderResult; - async fn transaction_count(&self, address: Address, block_id: Option) -> EthProviderResult; + async fn get_code(&self, address: Address, block_id: Option) -> EthProviderResult; - async fn get_logs(&self, filter: Filter) -> EthProviderResult; + async fn call(&self, request: TransactionRequest, block_id: Option, state_overrides: Option, block_overrides: Option>) -> EthProviderResult; - async fn estimate_gas(&self, call: TransactionRequest, block_id: Option) -> EthProviderResult; - async fn fee_history(&self, block_count: U64, newest_block: BlockNumberOrTag, reward_percentiles: Option>) -> EthProviderResult; + } + + #[async_trait] + impl TransactionProvider for EthereumProviderStruct { + async fn transaction_by_hash(&self, hash: B256) -> EthProviderResult>; + + async fn transaction_by_block_hash_and_index(&self, hash: B256, index: reth_rpc_types::Index) -> EthProviderResult>; + + async fn transaction_by_block_number_and_index(&self, number_or_tag: BlockNumberOrTag, index: reth_rpc_types::Index) -> EthProviderResult>; + + async fn transaction_count(&self, address: Address, block_id: Option) -> EthProviderResult; + async fn send_raw_transaction(&self, transaction: Bytes) -> EthProviderResult; - async fn gas_price(&self) -> EthProviderResult; - async fn block_receipts(&self, block_id: Option) -> EthProviderResult>>; - async fn block_transactions(&self, block_id: Option) -> EthProviderResult>>; + } + + #[async_trait] + impl TxPoolProvider for EthereumProviderStruct { async fn txpool_transactions(&self) -> EthProviderResult>; + async fn txpool_content(&self) -> EthProviderResult; } } diff --git a/tests/tests/debug_api.rs b/tests/tests/debug_api.rs index 327e928e8..cf6d67588 100644 --- a/tests/tests/debug_api.rs +++ b/tests/tests/debug_api.rs @@ -2,7 +2,7 @@ #![cfg(feature = "testing")] use alloy_rlp::Encodable; use kakarot_rpc::{ - providers::eth_provider::provider::EthereumProvider, + providers::eth_provider::{blocks::BlockProvider, receipts::ReceiptProvider, transactions::TransactionProvider}, test_utils::{ fixtures::{katana, setup}, katana::Katana, diff --git a/tests/tests/eth_provider.rs b/tests/tests/eth_provider.rs index 5d9808487..c695dc71d 100644 --- a/tests/tests/eth_provider.rs +++ b/tests/tests/eth_provider.rs @@ -4,9 +4,16 @@ use alloy_sol_types::{sol, SolCall}; use kakarot_rpc::{ models::felt::Felt252Wrapper, providers::eth_provider::{ + blocks::BlockProvider, + chain::ChainProvider, constant::{MAX_LOGS, STARKNET_MODULUS}, database::{ethereum::EthereumTransactionStore, types::transaction::StoredPendingTransaction}, + gas::GasProvider, + logs::LogProvider, provider::EthereumProvider, + receipts::ReceiptProvider, + state::StateProvider, + transactions::TransactionProvider, }, test_utils::{ eoa::Eoa, diff --git a/tests/tests/kakarot_api.rs b/tests/tests/kakarot_api.rs index ca60b4790..09cbfae70 100644 --- a/tests/tests/kakarot_api.rs +++ b/tests/tests/kakarot_api.rs @@ -2,7 +2,8 @@ #![cfg(feature = "testing")] use kakarot_rpc::{ providers::eth_provider::{ - constant::Constant, database::types::transaction::StoredPendingTransaction, provider::EthereumProvider, + chain::ChainProvider, constant::Constant, database::types::transaction::StoredPendingTransaction, + transactions::TransactionProvider, }, test_utils::{ eoa::Eoa, diff --git a/tests/tests/trace_api.rs b/tests/tests/trace_api.rs index 2734987a5..f9a2cb48d 100644 --- a/tests/tests/trace_api.rs +++ b/tests/tests/trace_api.rs @@ -3,7 +3,7 @@ use alloy_dyn_abi::DynSolValue; use alloy_sol_types::{sol, SolCall}; use kakarot_rpc::{ - providers::eth_provider::provider::EthereumProvider, + providers::eth_provider::chain::ChainProvider, test_utils::{ eoa::Eoa, evm_contract::{EvmContract, KakarotEvmContract, TransactionInfo, TxCommonInfo, TxFeeMarketInfo}, diff --git a/tests/tests/tracer.rs b/tests/tests/tracer.rs index 57c226815..d5e2ba96e 100644 --- a/tests/tests/tracer.rs +++ b/tests/tests/tracer.rs @@ -2,7 +2,7 @@ #![cfg(feature = "testing")] use alloy_dyn_abi::DynSolValue; use kakarot_rpc::{ - providers::eth_provider::provider::EthereumProvider, + providers::eth_provider::{blocks::BlockProvider, chain::ChainProvider}, test_utils::{ eoa::Eoa, evm_contract::{EvmContract, KakarotEvmContract, TransactionInfo, TxCommonInfo, TxFeeMarketInfo},