diff --git a/Cargo.lock b/Cargo.lock index 6d8e731b52..ab30c41bee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3510,15 +3510,6 @@ dependencies = [ "hashbrown 0.12.3", ] -[[package]] -name = "lru" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e7d46de488603ffdd5f30afbc64fbba2378214a2c3a2fb83abf3d33126df17" -dependencies = [ - "hashbrown 0.13.2", -] - [[package]] name = "lru" version = "0.10.0" @@ -5663,7 +5654,6 @@ version = "0.1.0-alpha.1" dependencies = [ "assert_matches", "jsonrpsee-types", - "lru 0.9.0", "rand 0.8.5", "reth-interfaces", "reth-primitives", @@ -5672,7 +5662,6 @@ dependencies = [ "serde_json", "similar-asserts", "thiserror", - "tokio", ] [[package]] diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index 4a682f8c71..cd53334a32 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -437,6 +437,7 @@ impl Transaction { pub fn effective_gas_tip(&self, base_fee: Option) -> Option { if let Some(base_fee) = base_fee { let max_fee_per_gas = self.max_fee_per_gas(); + if max_fee_per_gas < base_fee as u128 { None } else { diff --git a/crates/rpc/rpc-api/src/eth.rs b/crates/rpc/rpc-api/src/eth.rs index 3dad163553..9a66557c83 100644 --- a/crates/rpc/rpc-api/src/eth.rs +++ b/crates/rpc/rpc-api/src/eth.rs @@ -1,7 +1,7 @@ use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use reth_primitives::{ - serde_helper::JsonStorageKey, AccessListWithGasUsed, Address, BlockId, BlockNumberOrTag, Bytes, - H256, H64, U256, U64, + serde_helper::{num::U64HexOrNumber, JsonStorageKey}, + AccessListWithGasUsed, Address, BlockId, BlockNumberOrTag, Bytes, H256, H64, U256, U64, }; use reth_rpc_types::{ state::StateOverride, BlockOverrides, CallRequest, EIP1186AccountProofResponse, FeeHistory, @@ -194,8 +194,8 @@ pub trait EthApi { #[method(name = "feeHistory")] async fn fee_history( &self, - block_count: U64, - newest_block: BlockId, + block_count: U64HexOrNumber, + newest_block: BlockNumberOrTag, reward_percentiles: Option>, ) -> RpcResult; diff --git a/crates/rpc/rpc-builder/tests/it/http.rs b/crates/rpc/rpc-builder/tests/it/http.rs index baab8a3e57..221b2913dd 100644 --- a/crates/rpc/rpc-builder/tests/it/http.rs +++ b/crates/rpc/rpc-builder/tests/it/http.rs @@ -65,7 +65,7 @@ where EthApiClient::block_number(client).await.unwrap(); EthApiClient::get_code(client, address, None).await.unwrap(); EthApiClient::send_raw_transaction(client, tx).await.unwrap(); - EthApiClient::fee_history(client, 0.into(), block_number.into(), None).await.unwrap(); + EthApiClient::fee_history(client, 0.into(), block_number, None).await.unwrap(); EthApiClient::balance(client, address, None).await.unwrap(); EthApiClient::transaction_count(client, address, None).await.unwrap(); EthApiClient::storage_at(client, address, U256::default().into(), None).await.unwrap(); diff --git a/crates/rpc/rpc-types/Cargo.toml b/crates/rpc/rpc-types/Cargo.toml index 037cf873bb..5db05498ba 100644 --- a/crates/rpc/rpc-types/Cargo.toml +++ b/crates/rpc/rpc-types/Cargo.toml @@ -18,14 +18,10 @@ reth-rlp = { workspace = true } # errors thiserror = { workspace = true } -# async -tokio = { workspace = true, features = ["sync"] } - # misc serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } jsonrpsee-types = { version = "0.18" } -lru = "0.9" [dev-dependencies] # reth diff --git a/crates/rpc/rpc-types/src/eth/fee.rs b/crates/rpc/rpc-types/src/eth/fee.rs index 4ace0e0c5c..6463e7b433 100644 --- a/crates/rpc/rpc-types/src/eth/fee.rs +++ b/crates/rpc/rpc-types/src/eth/fee.rs @@ -1,15 +1,12 @@ -use lru::LruCache; -use reth_primitives::{BlockNumber, H256, U256}; +use reth_primitives::U256; use serde::{Deserialize, Serialize}; -use std::{num::NonZeroUsize, sync::Arc}; -use tokio::sync::Mutex; /// Internal struct to calculate reward percentiles #[derive(Clone, Debug, PartialEq, Eq)] pub struct TxGasAndReward { - /// gas used by a block - pub gas_used: u128, - /// minimum between max_priority_fee_per_gas or max_fee_per_gas - base_fee_for_block + /// Gas used by the transaction + pub gas_used: u64, + /// The effective gas tip by the transaction pub reward: u128, } @@ -32,16 +29,28 @@ impl Ord for TxGasAndReward { } /// Response type for `eth_feeHistory` -#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct FeeHistory { /// An array of block base fees per gas. /// This includes the next block after the newest of the returned range, /// because this value can be derived from the newest block. Zeroes are /// returned for pre-EIP-1559 blocks. + /// + /// # Note + /// + /// The `Option` is only for compatability with Erigon and Geth. + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] pub base_fee_per_gas: Vec, /// An array of block gas used ratios. These are calculated as the ratio /// of `gasUsed` and `gasLimit`. + /// + /// # Note + /// + /// The `Option` is only for compatability with Erigon and Geth. + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] pub gas_used_ratio: Vec, /// Lowest number block of the returned range. pub oldest_block: U256, @@ -50,29 +59,3 @@ pub struct FeeHistory { #[serde(default)] pub reward: Option>>, } - -/// LRU cache for `eth_feeHistory` RPC method. Block Number => Fee History. -#[derive(Clone, Debug)] -pub struct FeeHistoryCache(pub Arc>>); - -impl FeeHistoryCache { - /// Creates a new LRU Cache that holds at most cap items. - pub fn new(cap: NonZeroUsize) -> Self { - Self(Arc::new(Mutex::new(LruCache::new(cap)))) - } -} - -/// [FeeHistoryCache] item. -#[derive(Clone, Debug)] -pub struct FeeHistoryCacheItem { - /// Block hash (`None` if it wasn't the oldest block in `eth_feeHistory` response where - /// cache is populated) - pub hash: Option, - /// Block base fee per gas. Zero for pre-EIP-1559 blocks. - pub base_fee_per_gas: U256, - /// Block gas used ratio. Calculated as the ratio of `gasUsed` and `gasLimit`. - pub gas_used_ratio: f64, - /// An (optional) array of effective priority fee per gas data points for a - /// block. All zeroes are returned if the block is empty. - pub reward: Option>, -} diff --git a/crates/rpc/rpc-types/src/eth/mod.rs b/crates/rpc/rpc-types/src/eth/mod.rs index 2592998534..04b2da6214 100644 --- a/crates/rpc/rpc-types/src/eth/mod.rs +++ b/crates/rpc/rpc-types/src/eth/mod.rs @@ -20,7 +20,7 @@ mod work; pub use account::*; pub use block::*; pub use call::CallRequest; -pub use fee::{FeeHistory, FeeHistoryCache, FeeHistoryCacheItem, TxGasAndReward}; +pub use fee::{FeeHistory, TxGasAndReward}; pub use filter::*; pub use index::Index; pub use log::Log; diff --git a/crates/rpc/rpc/Cargo.toml b/crates/rpc/rpc/Cargo.toml index 9fdf2c913f..796c5ded0e 100644 --- a/crates/rpc/rpc/Cargo.toml +++ b/crates/rpc/rpc/Cargo.toml @@ -62,4 +62,5 @@ futures = { workspace = true } [dev-dependencies] jsonrpsee = { version = "0.18", features = ["client"] } assert_matches = "1.5.0" -tempfile = "3.5.0" \ No newline at end of file +tempfile = "3.5.0" +reth-interfaces = { workspace = true, features = ["test-utils"] } \ No newline at end of file diff --git a/crates/rpc/rpc/src/eth/api/fees.rs b/crates/rpc/rpc/src/eth/api/fees.rs index 6077475fce..630c0c2305 100644 --- a/crates/rpc/rpc/src/eth/api/fees.rs +++ b/crates/rpc/rpc/src/eth/api/fees.rs @@ -1,15 +1,17 @@ //! Contains RPC handler implementations for fee history. use crate::{ - eth::error::{EthApiError, EthResult, RpcInvalidTransactionError}, + eth::error::{EthApiError, EthResult}, EthApi, }; use reth_network_api::NetworkInfo; -use reth_primitives::{BlockId, BlockNumberOrTag, U256}; +use reth_primitives::{ + basefee::calculate_next_block_base_fee, BlockNumberOrTag, SealedHeader, U256, +}; use reth_provider::{BlockReaderIdExt, EvmEnvProvider, StateProviderFactory}; -use reth_rpc_types::{FeeHistory, FeeHistoryCacheItem, TxGasAndReward}; +use reth_rpc_types::{FeeHistory, TxGasAndReward}; use reth_transaction_pool::TransactionPool; -use std::collections::BTreeMap; +use tracing::debug; impl EthApi where @@ -37,168 +39,155 @@ where /// provided. pub(crate) async fn fee_history( &self, - block_count: u64, - newest_block: BlockId, + mut block_count: u64, + newest_block: BlockNumberOrTag, reward_percentiles: Option>, ) -> EthResult { if block_count == 0 { return Ok(FeeHistory::default()) } - let Some(previous_to_end_block) = self.inner.provider.block_number_for_id(newest_block)? else { return Err(EthApiError::UnknownBlockNumber)}; - let end_block = previous_to_end_block + 1; + // See https://github.com/ethereum/go-ethereum/blob/2754b197c935ee63101cbbca2752338246384fec/eth/gasprice/feehistory.go#L218C8-L225 + let max_fee_history = if reward_percentiles.is_none() { + self.gas_oracle().config().max_header_history + } else { + self.gas_oracle().config().max_block_history + }; + if block_count > max_fee_history { + debug!( + requested = block_count, + truncated = max_fee_history, + "Sanitizing fee history block count" + ); + block_count = max_fee_history + } + + let Some(end_block) = self.provider().block_number_for_id(newest_block.into())? else { + return Err(EthApiError::UnknownBlockNumber) }; + + // Check that we would not be querying outside of genesis if end_block < block_count { return Err(EthApiError::InvalidBlockRange) } - let mut start_block = end_block - block_count; - - if block_count == 1 { - start_block = previous_to_end_block; - } - - // if not provided the percentiles are [] - let reward_percentiles = reward_percentiles.unwrap_or_default(); - - // checks for rewardPercentile's sorted-ness - // check if any of rewardPercentile is greater than 100 - // pre 1559 blocks, return 0 for baseFeePerGas - for window in reward_percentiles.windows(2) { - if window[0] >= window[1] { - return Err(EthApiError::InvalidRewardPercentile(window[1])) - } - - if window[0] < 0.0 || window[0] > 100.0 { - return Err(EthApiError::InvalidRewardPercentile(window[0])) + // If reward percentiles were specified, we need to validate that they are monotonically + // increasing and 0 <= p <= 100 + // + // Note: The types used ensure that the percentiles are never < 0 + if let Some(percentiles) = &reward_percentiles { + if percentiles.windows(2).any(|w| w[0] > w[1] || w[0] > 100.) { + return Err(EthApiError::InvalidRewardPercentiles) } } - let mut fee_history_cache = self.inner.fee_history_cache.0.lock().await; + // Fetch the headers and ensure we got all of them + // + // Treat a request for 1 block as a request for `newest_block..=newest_block`, + // otherwise `newest_block - 2 + let start_block = end_block - block_count + 1; + let headers = self.provider().sealed_headers_range(start_block..=end_block)?; + if headers.len() != block_count as usize { + return Err(EthApiError::InvalidBlockRange) + } - // Sorted map that's populated in two rounds: - // 1. Cache entries until first non-cached block - // 2. Database query from the first non-cached block - let mut fee_history_cache_items = BTreeMap::new(); + // Collect base fees, gas usage ratios and (optionally) reward percentile data + let mut base_fee_per_gas: Vec = Vec::new(); + let mut gas_used_ratio: Vec = Vec::new(); + let mut rewards: Vec> = Vec::new(); + for header in &headers { + base_fee_per_gas + .push(U256::try_from(header.base_fee_per_gas.unwrap_or_default()).unwrap()); + gas_used_ratio.push(header.gas_used as f64 / header.gas_limit as f64); - let mut first_non_cached_block = None; - let mut last_non_cached_block = None; - for block in start_block..=end_block { - // Check if block exists in cache, and move it to the head of the list if so - if let Some(fee_history_cache_item) = fee_history_cache.get(&block) { - fee_history_cache_items.insert(block, fee_history_cache_item.clone()); - } else { - // If block doesn't exist in cache, set it as a first non-cached block to query it - // from the database - first_non_cached_block.get_or_insert(block); - // And last non-cached block, so we could query the database until we reach it - last_non_cached_block = Some(block); + // Percentiles were specified, so we need to collect reward percentile ino + if let Some(percentiles) = &reward_percentiles { + rewards.push(self.calculate_reward_percentiles(percentiles, header).await?); } } - // If we had any cache misses, query the database starting with the first non-cached block - // and ending with the last - if let (Some(start_block), Some(end_block)) = - (first_non_cached_block, last_non_cached_block) - { - let header_range = start_block..=end_block; - - let headers = self.inner.provider.headers_range(header_range.clone())?; - let transactions_by_block = - self.inner.provider.transactions_by_block_range(header_range)?; - - let header_tx = headers.iter().zip(&transactions_by_block); - - // We should receive exactly the amount of blocks missing from the cache - if headers.len() != (end_block - start_block + 1) as usize { - return Err(EthApiError::InvalidBlockRange) - } - - // We should receive exactly the amount of blocks missing from the cache - if transactions_by_block.len() != (end_block - start_block + 1) as usize { - return Err(EthApiError::InvalidBlockRange) - } - - for (header, transactions) in header_tx { - let base_fee_per_gas: U256 = header.base_fee_per_gas. - unwrap_or_default(). // Zero for pre-EIP-1559 blocks - try_into().unwrap(); // u64 -> U256 won't fail - let gas_used_ratio = header.gas_used as f64 / header.gas_limit as f64; - - let mut sorter = Vec::with_capacity(transactions.len()); - for transaction in transactions.iter() { - let reward = transaction - .effective_gas_tip(header.base_fee_per_gas) - .ok_or(RpcInvalidTransactionError::FeeCapTooLow)?; - - sorter.push(TxGasAndReward { gas_used: header.gas_used as u128, reward }) - } - - sorter.sort(); - - let mut rewards = Vec::with_capacity(reward_percentiles.len()); - let mut sum_gas_used = sorter.first().map(|tx| tx.gas_used).unwrap_or_default(); - let mut tx_index = 0; - - for percentile in reward_percentiles.iter() { - let threshold_gas_used = (header.gas_used as f64) * percentile / 100_f64; - while sum_gas_used < threshold_gas_used as u128 && tx_index < transactions.len() - { - tx_index += 1; - sum_gas_used += sorter[tx_index].gas_used; - } - - rewards.push(U256::from(sorter[tx_index].reward)); - } - - let fee_history_cache_item = FeeHistoryCacheItem { - hash: None, - base_fee_per_gas, - gas_used_ratio, - reward: Some(rewards), - }; - - // Insert missing cache entries in the map for further response composition from - // it - fee_history_cache_items.insert(header.number, fee_history_cache_item.clone()); - // And populate the cache with new entries - fee_history_cache.push(header.number, fee_history_cache_item); - } - } - - // get the first block in the range from the db - let oldest_block_hash = - self.inner.provider.block_hash(start_block)?.ok_or(EthApiError::UnknownBlockNumber)?; - - // Set the hash in cache items if the block is present in the cache - if let Some(cache_item) = fee_history_cache_items.get_mut(&start_block) { - cache_item.hash = Some(oldest_block_hash); - } - - if let Some(cache_item) = fee_history_cache.get_mut(&start_block) { - cache_item.hash = Some(oldest_block_hash); - } - - // `fee_history_cache_items` now contains full requested block range (populated from both - // cache and database), so we can iterate over it in order and populate the response fields - let base_fee_per_gas = - fee_history_cache_items.values().map(|item| item.base_fee_per_gas).collect(); - - let mut gas_used_ratio: Vec = - fee_history_cache_items.values().map(|item| item.gas_used_ratio).collect(); - - let mut rewards: Vec> = - fee_history_cache_items.values().filter_map(|item| item.reward.clone()).collect(); - - // gasUsedRatio doesn't have data for next block in this case the last block - gas_used_ratio.pop(); - rewards.pop(); + // The spec states that `base_fee_per_gas` "[..] includes the next block after the newest of + // the returned range, because this value can be derived from the newest block" + // + // The unwrap is safe since we checked earlier that we got at least 1 header. + let last_header = headers.last().unwrap(); + base_fee_per_gas.push(U256::from(calculate_next_block_base_fee( + last_header.gas_used, + last_header.gas_limit, + last_header.base_fee_per_gas.unwrap_or_default(), + ))); Ok(FeeHistory { base_fee_per_gas, gas_used_ratio, oldest_block: U256::from(start_block), - reward: Some(rewards), + reward: reward_percentiles.map(|_| rewards), }) } + + // todo: docs + async fn calculate_reward_percentiles( + &self, + percentiles: &[f64], + header: &SealedHeader, + ) -> Result, EthApiError> { + let Some(receipts) = + self.cache().get_receipts(header.hash).await? else { + // If there are no receipts, then we do not have all info on the block + return Err(EthApiError::InvalidBlockRange) + }; + let Some(mut transactions): Option> = self + .cache() + .get_block_transactions(header.hash).await? + .map(|txs|txs + .into_iter() + .zip(receipts.into_iter()) + .scan(0, |previous_gas, (tx, receipt)| { + // Convert the cumulative gas used in the receipts + // to the gas usage by the transaction + // + // While we will sum up the gas again later, it is worth + // noting that the order of the transactions will be different, + // so the sum will also be different for each receipt. + let gas_used = receipt.cumulative_gas_used - *previous_gas; + *previous_gas = receipt.cumulative_gas_used; + + Some(TxGasAndReward { + gas_used, + reward: tx.effective_gas_tip(header.base_fee_per_gas).unwrap_or_default(), + }) + }) + .collect()) else { + // If there are no transactions, then we do not have all info on the block + return Err(EthApiError::InvalidBlockRange) + }; + + // Sort the transactions by their rewards in ascending order + transactions.sort_by_key(|tx| tx.reward); + + // Find the transaction that corresponds to the given percentile + // + // We use a `tx_index` here that is shared across all percentiles, since we know + // the percentiles are monotonically increasing. + let mut tx_index = 0; + let mut cumulative_gas_used = + transactions.first().map(|tx| tx.gas_used).unwrap_or_default(); + let mut rewards_in_block = Vec::new(); + for percentile in percentiles { + // Empty blocks should return in a zero row + if transactions.is_empty() { + rewards_in_block.push(U256::ZERO); + continue + } + + let threshold = (header.gas_used as f64 * percentile / 100.) as u64; + while cumulative_gas_used < threshold && tx_index < transactions.len() - 1 { + tx_index += 1; + cumulative_gas_used += transactions[tx_index].gas_used; + } + rewards_in_block.push(U256::from(transactions[tx_index].reward)); + } + + Ok(rewards_in_block) + } } diff --git a/crates/rpc/rpc/src/eth/api/mod.rs b/crates/rpc/rpc/src/eth/api/mod.rs index f611034fd5..227e14cefb 100644 --- a/crates/rpc/rpc/src/eth/api/mod.rs +++ b/crates/rpc/rpc/src/eth/api/mod.rs @@ -14,10 +14,10 @@ use reth_interfaces::Result; use reth_network_api::NetworkInfo; use reth_primitives::{Address, BlockId, BlockNumberOrTag, ChainInfo, H256, U256, U64}; use reth_provider::{BlockReaderIdExt, EvmEnvProvider, StateProviderBox, StateProviderFactory}; -use reth_rpc_types::{FeeHistoryCache, SyncInfo, SyncStatus}; +use reth_rpc_types::{SyncInfo, SyncStatus}; use reth_tasks::{TaskSpawner, TokioTaskExecutor}; use reth_transaction_pool::TransactionPool; -use std::{future::Future, num::NonZeroUsize, sync::Arc}; +use std::{future::Future, sync::Arc}; use tokio::sync::oneshot; mod block; @@ -30,9 +30,6 @@ mod transactions; pub use transactions::{EthTransactions, TransactionSource}; -/// Cache limit of block-level fee history for `eth_feeHistory` RPC method. -const FEE_HISTORY_CACHE_LIMIT: usize = 2048; - /// `Eth` API trait. /// /// Defines core functionality of the `eth` API implementation. @@ -118,9 +115,6 @@ where gas_oracle, starting_block: U256::from(latest_block), task_spawner, - fee_history_cache: FeeHistoryCache::new( - NonZeroUsize::new(FEE_HISTORY_CACHE_LIMIT).unwrap(), - ), }; Self { inner: Arc::new(inner) } } @@ -290,6 +284,4 @@ struct EthApiInner { starting_block: U256, /// The type that can spawn tasks which would otherwise block. task_spawner: Box, - /// The cache for fee history entries, - fee_history_cache: FeeHistoryCache, } diff --git a/crates/rpc/rpc/src/eth/api/server.rs b/crates/rpc/rpc/src/eth/api/server.rs index f8244dcf53..90965b8a86 100644 --- a/crates/rpc/rpc/src/eth/api/server.rs +++ b/crates/rpc/rpc/src/eth/api/server.rs @@ -12,8 +12,8 @@ use crate::{ use jsonrpsee::core::RpcResult as Result; use reth_network_api::NetworkInfo; use reth_primitives::{ - serde_helper::JsonStorageKey, AccessListWithGasUsed, Address, BlockId, BlockNumberOrTag, Bytes, - H256, H64, U256, U64, + serde_helper::{num::U64HexOrNumber, JsonStorageKey}, + AccessListWithGasUsed, Address, BlockId, BlockNumberOrTag, Bytes, H256, H64, U256, U64, }; use reth_provider::{ BlockIdReader, BlockReader, BlockReaderIdExt, EvmEnvProvider, HeaderProvider, @@ -295,8 +295,8 @@ where /// Handler for: `eth_feeHistory` async fn fee_history( &self, - block_count: U64, - newest_block: BlockId, + block_count: U64HexOrNumber, + newest_block: BlockNumberOrTag, reward_percentiles: Option>, ) -> Result { trace!(target: "rpc::eth", ?block_count, ?newest_block, ?reward_percentiles, "Serving eth_feeHistory"); @@ -386,50 +386,78 @@ mod tests { EthApi, }; use jsonrpsee::types::error::INVALID_PARAMS_CODE; - use rand::random; + use reth_interfaces::test_utils::{generators, generators::Rng}; use reth_network_api::test_utils::NoopNetwork; - use reth_primitives::{Block, BlockNumberOrTag, Header, TransactionSigned, H256, U256}; - use reth_provider::test_utils::{MockEthProvider, NoopProvider}; + use reth_primitives::{ + basefee::calculate_next_block_base_fee, Block, BlockNumberOrTag, Header, TransactionSigned, + H256, U256, + }; + use reth_provider::{ + test_utils::{MockEthProvider, NoopProvider}, + BlockReader, BlockReaderIdExt, EvmEnvProvider, StateProviderFactory, + }; use reth_rpc_api::EthApiServer; - use reth_transaction_pool::test_utils::testing_pool; + use reth_rpc_types::FeeHistory; + use reth_transaction_pool::test_utils::{testing_pool, TestPool}; - #[tokio::test] - /// Handler for: `eth_test_fee_history` - async fn test_fee_history() { - let cache = EthStateCache::spawn(NoopProvider::default(), Default::default()); - let eth_api = EthApi::new( - NoopProvider::default(), + fn build_test_eth_api< + P: BlockReaderIdExt + + BlockReader + + EvmEnvProvider + + StateProviderFactory + + Unpin + + Clone + + 'static, + >( + provider: P, + ) -> EthApi { + let cache = EthStateCache::spawn(provider.clone(), Default::default()); + EthApi::new( + provider.clone(), testing_pool(), NoopNetwork, cache.clone(), - GasPriceOracle::new(NoopProvider::default(), Default::default(), cache), - ); + GasPriceOracle::new(provider, Default::default(), cache), + ) + } + /// Invalid block range + #[tokio::test] + async fn test_fee_history_empty() { let response = as EthApiServer>::fee_history( - ð_api, + &build_test_eth_api(NoopProvider::default()), 1.into(), - BlockNumberOrTag::Latest.into(), + BlockNumberOrTag::Latest, None, ) .await; assert!(response.is_err()); let error_object = response.unwrap_err(); assert_eq!(error_object.code(), INVALID_PARAMS_CODE); + } + + /// Handler for: `eth_test_fee_history` + // TODO: Split this into multiple tests, and add tests for percentiles. + #[tokio::test] + async fn test_fee_history() { + let mut rng = generators::rng(); let block_count = 10; let newest_block = 1337; + // Build mock data let mut oldest_block = None; let mut gas_used_ratios = Vec::new(); let mut base_fees_per_gas = Vec::new(); - + let mut last_header = None; let mock_provider = MockEthProvider::default(); - for i in (0..=block_count).rev() { + for i in (0..block_count).rev() { let hash = H256::random(); - let gas_limit: u64 = random(); - let gas_used: u64 = random(); - let base_fee_per_gas: Option = random::().then(random); + let gas_limit: u64 = rng.gen(); + let gas_used: u64 = rng.gen(); + // Note: Generates a u32 to avoid overflows later + let base_fee_per_gas: Option = rng.gen::().then(|| rng.gen::() as u64); let header = Header { number: newest_block - i, @@ -438,10 +466,11 @@ mod tests { base_fee_per_gas, ..Default::default() }; + last_header = Some(header.clone()); let mut transactions = vec![]; for _ in 0..100 { - let random_fee: u128 = random(); + let random_fee: u128 = rng.gen(); if let Some(base_fee_per_gas) = header.base_fee_per_gas { let transaction = TransactionSigned { @@ -480,17 +509,17 @@ mod tests { .push(base_fee_per_gas.map(|fee| U256::try_from(fee).unwrap()).unwrap_or_default()); } - gas_used_ratios.pop(); + // Add final base fee (for the next block outside of the request) + let last_header = last_header.unwrap(); + base_fees_per_gas.push(U256::from(calculate_next_block_base_fee( + last_header.gas_used, + last_header.gas_limit, + last_header.base_fee_per_gas.unwrap_or_default(), + ))); - let cache = EthStateCache::spawn(mock_provider.clone(), Default::default()); - let eth_api = EthApi::new( - mock_provider.clone(), - testing_pool(), - NoopNetwork, - cache.clone(), - GasPriceOracle::new(mock_provider, Default::default(), cache.clone()), - ); + let eth_api = build_test_eth_api(mock_provider); + // Invalid block range (request is before genesis) let response = as EthApiServer>::fee_history( ð_api, (newest_block + 1).into(), @@ -502,20 +531,85 @@ mod tests { let error_object = response.unwrap_err(); assert_eq!(error_object.code(), INVALID_PARAMS_CODE); - // newest_block is finalized + // Invalid block range (request is in in the future) + let response = as EthApiServer>::fee_history( + ð_api, + (1).into(), + (newest_block + 1000).into(), + Some(vec![10.0]), + ) + .await; + assert!(response.is_err()); + let error_object = response.unwrap_err(); + assert_eq!(error_object.code(), INVALID_PARAMS_CODE); + + // Requesting no block should result in a default response + let response = as EthApiServer>::fee_history( + ð_api, + (0).into(), + (newest_block).into(), + None, + ) + .await + .unwrap(); + assert_eq!( + response, + FeeHistory::default(), + "none: requesting no block should yield a default response" + ); + + // Requesting a single block should return 1 block (+ base fee for the next block over) + let fee_history = eth_api.fee_history(1, (newest_block).into(), None).await.unwrap(); + assert_eq!( + &fee_history.base_fee_per_gas, + &base_fees_per_gas[base_fees_per_gas.len() - 2..], + "one: base fee per gas is incorrect" + ); + assert_eq!( + fee_history.base_fee_per_gas.len(), + 2, + "one: should return base fee of the next block as well" + ); + assert_eq!( + &fee_history.gas_used_ratio, + &gas_used_ratios[gas_used_ratios.len() - 1..], + "one: gas used ratio is incorrect" + ); + assert_eq!( + fee_history.oldest_block, + U256::from(newest_block), + "one: oldest block is incorrect" + ); + assert!( + fee_history.reward.is_none(), + "one: no percentiles were requested, so there should be no rewards result" + ); + + // Requesting all blocks should be ok let fee_history = - eth_api.fee_history(block_count, (newest_block - 1).into(), None).await.unwrap(); + eth_api.fee_history(block_count, (newest_block).into(), None).await.unwrap(); - assert_eq!(fee_history.base_fee_per_gas, base_fees_per_gas); - assert_eq!(fee_history.gas_used_ratio, gas_used_ratios); - assert_eq!(fee_history.oldest_block, U256::from(newest_block - block_count)); - - // newest_block is pending - let fee_history = - eth_api.fee_history(block_count, (newest_block - 1).into(), None).await.unwrap(); - - assert_eq!(fee_history.base_fee_per_gas, base_fees_per_gas); - assert_eq!(fee_history.gas_used_ratio, gas_used_ratios); - assert_eq!(fee_history.oldest_block, U256::from(newest_block - block_count)); + assert_eq!( + &fee_history.base_fee_per_gas, &base_fees_per_gas, + "all: base fee per gas is incorrect" + ); + assert_eq!( + fee_history.base_fee_per_gas.len() as u64, + block_count + 1, + "all: should return base fee of the next block as well" + ); + assert_eq!( + &fee_history.gas_used_ratio, &gas_used_ratios, + "all: gas used ratio is incorrect" + ); + assert_eq!( + fee_history.oldest_block, + U256::from(newest_block - block_count + 1), + "all: oldest block is incorrect" + ); + assert!( + fee_history.reward.is_none(), + "all: no percentiles were requested, so there should be no rewards result" + ); } } diff --git a/crates/rpc/rpc/src/eth/error.rs b/crates/rpc/rpc/src/eth/error.rs index 625c0e2bd5..f7bf257007 100644 --- a/crates/rpc/rpc/src/eth/error.rs +++ b/crates/rpc/rpc/src/eth/error.rs @@ -65,8 +65,8 @@ pub enum EthApiError { #[error("invalid tracer config")] InvalidTracerConfig, /// Percentile array is invalid - #[error("invalid reward percentile")] - InvalidRewardPercentile(f64), + #[error("invalid reward percentiles")] + InvalidRewardPercentiles, /// Error thrown when a spawned tracing task failed to deliver an anticipated response. #[error("internal error while tracing")] InternalTracingError, @@ -101,7 +101,7 @@ impl From for ErrorObject<'static> { EthApiError::Unsupported(msg) => internal_rpc_err(msg), EthApiError::InternalJsTracerError(msg) => internal_rpc_err(msg), EthApiError::InvalidParams(msg) => invalid_params_rpc_err(msg), - EthApiError::InvalidRewardPercentile(msg) => internal_rpc_err(msg.to_string()), + EthApiError::InvalidRewardPercentiles => internal_rpc_err(error.to_string()), err @ EthApiError::InternalTracingError => internal_rpc_err(err.to_string()), err @ EthApiError::InternalEthError => internal_rpc_err(err.to_string()), } diff --git a/crates/rpc/rpc/src/eth/gas_oracle.rs b/crates/rpc/rpc/src/eth/gas_oracle.rs index 8a364088d1..2448ad9a2e 100644 --- a/crates/rpc/rpc/src/eth/gas_oracle.rs +++ b/crates/rpc/rpc/src/eth/gas_oracle.rs @@ -111,6 +111,11 @@ where Self { provider, oracle_config, last_price: Default::default(), cache } } + /// Returns the configuration of the gas price oracle. + pub fn config(&self) -> &GasPriceOracleConfig { + &self.oracle_config + } + /// Suggests a gas price estimate based on recent blocks, using the configured percentile. pub async fn suggest_tip_cap(&self) -> EthResult { let header = self