mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
356 lines
15 KiB
Rust
356 lines
15 KiB
Rust
//! Loads fee history from database. Helper trait for `eth_` fee and transaction RPC methods.
|
|
|
|
use alloy_primitives::U256;
|
|
use alloy_rpc_types::{BlockNumberOrTag, FeeHistory};
|
|
use futures::Future;
|
|
use reth_chainspec::EthChainSpec;
|
|
use reth_provider::{BlockIdReader, ChainSpecProvider, HeaderProvider};
|
|
use reth_rpc_eth_types::{
|
|
fee_history::calculate_reward_percentiles_for_block, EthApiError, EthStateCache,
|
|
FeeHistoryCache, FeeHistoryEntry, GasPriceOracle, RpcInvalidTransactionError,
|
|
};
|
|
use tracing::debug;
|
|
|
|
use crate::FromEthApiError;
|
|
|
|
use super::LoadBlock;
|
|
|
|
/// Fee related functions for the [`EthApiServer`](crate::EthApiServer) trait in the
|
|
/// `eth_` namespace.
|
|
pub trait EthFees: LoadFee {
|
|
/// Returns a suggestion for a gas price for legacy transactions.
|
|
///
|
|
/// See also: <https://github.com/ethereum/pm/issues/328#issuecomment-853234014>
|
|
fn gas_price(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
|
|
where
|
|
Self: LoadBlock,
|
|
{
|
|
LoadFee::gas_price(self)
|
|
}
|
|
|
|
/// Returns a suggestion for a base fee for blob transactions.
|
|
fn blob_base_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
|
|
where
|
|
Self: LoadBlock,
|
|
{
|
|
LoadFee::blob_base_fee(self)
|
|
}
|
|
|
|
/// Returns a suggestion for the priority fee (the tip)
|
|
fn suggested_priority_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
|
|
where
|
|
Self: 'static,
|
|
{
|
|
LoadFee::suggested_priority_fee(self)
|
|
}
|
|
|
|
/// Reports the fee history, for the given amount of blocks, up until the given newest block.
|
|
///
|
|
/// If `reward_percentiles` are provided the [`FeeHistory`] will include the _approximated_
|
|
/// rewards for the requested range.
|
|
fn fee_history(
|
|
&self,
|
|
mut block_count: u64,
|
|
mut newest_block: BlockNumberOrTag,
|
|
reward_percentiles: Option<Vec<f64>>,
|
|
) -> impl Future<Output = Result<FeeHistory, Self::Error>> + Send {
|
|
async move {
|
|
if block_count == 0 {
|
|
return Ok(FeeHistory::default())
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
if newest_block.is_pending() {
|
|
// cap the target block since we don't have fee history for the pending block
|
|
newest_block = BlockNumberOrTag::Latest;
|
|
// account for missing pending block
|
|
block_count = block_count.saturating_sub(1);
|
|
}
|
|
|
|
let end_block = self
|
|
.provider()
|
|
.block_number_for_id(newest_block.into())
|
|
.map_err(Self::Error::from_eth_err)?
|
|
.ok_or(EthApiError::HeaderNotFound(newest_block.into()))?;
|
|
|
|
// need to add 1 to the end block to get the correct (inclusive) range
|
|
let end_block_plus = end_block + 1;
|
|
// Ensure that we would not be querying outside of genesis
|
|
if end_block_plus < block_count {
|
|
block_count = end_block_plus;
|
|
}
|
|
|
|
// 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.into())
|
|
}
|
|
}
|
|
|
|
// 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
|
|
// NOTE: We ensured that block count is capped
|
|
let start_block = end_block_plus - block_count;
|
|
|
|
// Collect base fees, gas usage ratios and (optionally) reward percentile data
|
|
let mut base_fee_per_gas: Vec<u128> = Vec::new();
|
|
let mut gas_used_ratio: Vec<f64> = Vec::new();
|
|
|
|
let mut base_fee_per_blob_gas: Vec<u128> = Vec::new();
|
|
let mut blob_gas_used_ratio: Vec<f64> = Vec::new();
|
|
|
|
let mut rewards: Vec<Vec<u128>> = Vec::new();
|
|
|
|
// Check if the requested range is within the cache bounds
|
|
let fee_entries = self.fee_history_cache().get_history(start_block, end_block).await;
|
|
|
|
if let Some(fee_entries) = fee_entries {
|
|
if fee_entries.len() != block_count as usize {
|
|
return Err(EthApiError::InvalidBlockRange.into())
|
|
}
|
|
|
|
for entry in &fee_entries {
|
|
base_fee_per_gas.push(entry.base_fee_per_gas as u128);
|
|
gas_used_ratio.push(entry.gas_used_ratio);
|
|
base_fee_per_blob_gas.push(entry.base_fee_per_blob_gas.unwrap_or_default());
|
|
blob_gas_used_ratio.push(entry.blob_gas_used_ratio);
|
|
|
|
if let Some(percentiles) = &reward_percentiles {
|
|
let mut block_rewards = Vec::with_capacity(percentiles.len());
|
|
for &percentile in percentiles {
|
|
block_rewards.push(self.approximate_percentile(entry, percentile));
|
|
}
|
|
rewards.push(block_rewards);
|
|
}
|
|
}
|
|
let last_entry = fee_entries.last().expect("is not empty");
|
|
|
|
// Also need to include the `base_fee_per_gas` and `base_fee_per_blob_gas` for the
|
|
// next block
|
|
base_fee_per_gas
|
|
.push(last_entry.next_block_base_fee(self.provider().chain_spec()) as u128);
|
|
|
|
base_fee_per_blob_gas.push(last_entry.next_block_blob_fee().unwrap_or_default());
|
|
} else {
|
|
// read the requested header range
|
|
let headers = self.provider()
|
|
.sealed_headers_range(start_block..=end_block)
|
|
.map_err(Self::Error::from_eth_err)?;
|
|
if headers.len() != block_count as usize {
|
|
return Err(EthApiError::InvalidBlockRange.into())
|
|
}
|
|
|
|
|
|
for header in &headers {
|
|
base_fee_per_gas.push(header.base_fee_per_gas.unwrap_or_default() as u128);
|
|
gas_used_ratio.push(header.gas_used as f64 / header.gas_limit as f64);
|
|
base_fee_per_blob_gas.push(header.blob_fee().unwrap_or_default());
|
|
blob_gas_used_ratio.push(
|
|
header.blob_gas_used.unwrap_or_default() as f64
|
|
/ alloy_eips::eip4844::MAX_DATA_GAS_PER_BLOCK as f64,
|
|
);
|
|
|
|
// Percentiles were specified, so we need to collect reward percentile ino
|
|
if let Some(percentiles) = &reward_percentiles {
|
|
let (block, receipts) = LoadFee::cache(self)
|
|
.get_block_and_receipts(header.hash())
|
|
.await
|
|
.map_err(Self::Error::from_eth_err)?
|
|
.ok_or(EthApiError::InvalidBlockRange)?;
|
|
rewards.push(
|
|
calculate_reward_percentiles_for_block(
|
|
percentiles,
|
|
header.gas_used,
|
|
header.base_fee_per_gas.unwrap_or_default(),
|
|
&block.body.transactions,
|
|
&receipts,
|
|
)
|
|
.unwrap_or_default(),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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().expect("is present");
|
|
base_fee_per_gas.push(
|
|
self.provider()
|
|
.chain_spec()
|
|
.base_fee_params_at_timestamp(last_header.timestamp)
|
|
.next_block_base_fee(
|
|
last_header.gas_used ,
|
|
last_header.gas_limit,
|
|
last_header.base_fee_per_gas.unwrap_or_default() ,
|
|
) as u128,
|
|
);
|
|
|
|
// Same goes for the `base_fee_per_blob_gas`:
|
|
// > "[..] includes the next block after the newest of the returned range, because this value can be derived from the newest block.
|
|
base_fee_per_blob_gas.push(last_header.next_block_blob_fee().unwrap_or_default());
|
|
};
|
|
|
|
Ok(FeeHistory {
|
|
base_fee_per_gas,
|
|
gas_used_ratio,
|
|
base_fee_per_blob_gas,
|
|
blob_gas_used_ratio,
|
|
oldest_block: start_block,
|
|
reward: reward_percentiles.map(|_| rewards),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Approximates reward at a given percentile for a specific block
|
|
/// Based on the configured resolution
|
|
fn approximate_percentile(&self, entry: &FeeHistoryEntry, requested_percentile: f64) -> u128 {
|
|
let resolution = self.fee_history_cache().resolution();
|
|
let rounded_percentile =
|
|
(requested_percentile * resolution as f64).round() / resolution as f64;
|
|
let clamped_percentile = rounded_percentile.clamp(0.0, 100.0);
|
|
|
|
// Calculate the index in the precomputed rewards array
|
|
let index = (clamped_percentile / (1.0 / resolution as f64)).round() as usize;
|
|
// Fetch the reward from the FeeHistoryEntry
|
|
entry.rewards.get(index).copied().unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
/// Loads fee from database.
|
|
///
|
|
/// Behaviour shared by several `eth_` RPC methods, not exclusive to `eth_` fees RPC methods.
|
|
pub trait LoadFee: LoadBlock {
|
|
/// Returns a handle for reading data from memory.
|
|
///
|
|
/// Data access in default (L1) trait method implementations.
|
|
fn cache(&self) -> &EthStateCache;
|
|
|
|
/// Returns a handle for reading gas price.
|
|
///
|
|
/// Data access in default (L1) trait method implementations.
|
|
fn gas_oracle(&self) -> &GasPriceOracle<Self::Provider>;
|
|
|
|
/// Returns a handle for reading fee history data from memory.
|
|
///
|
|
/// Data access in default (L1) trait method implementations.
|
|
fn fee_history_cache(&self) -> &FeeHistoryCache;
|
|
|
|
/// Returns the gas price if it is set, otherwise fetches a suggested gas price for legacy
|
|
/// transactions.
|
|
fn legacy_gas_price(
|
|
&self,
|
|
gas_price: Option<U256>,
|
|
) -> impl Future<Output = Result<U256, Self::Error>> + Send {
|
|
async move {
|
|
match gas_price {
|
|
Some(gas_price) => Ok(gas_price),
|
|
None => {
|
|
// fetch a suggested gas price
|
|
self.gas_price().await
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the EIP-1559 fees if they are set, otherwise fetches a suggested gas price for
|
|
/// EIP-1559 transactions.
|
|
///
|
|
/// Returns (`base_fee`, `priority_fee`)
|
|
fn eip1559_fees(
|
|
&self,
|
|
base_fee: Option<U256>,
|
|
max_priority_fee_per_gas: Option<U256>,
|
|
) -> impl Future<Output = Result<(U256, U256), Self::Error>> + Send {
|
|
async move {
|
|
let base_fee = match base_fee {
|
|
Some(base_fee) => base_fee,
|
|
None => {
|
|
// fetch pending base fee
|
|
let base_fee = self
|
|
.block_with_senders(BlockNumberOrTag::Pending.into())
|
|
.await?
|
|
.ok_or(EthApiError::HeaderNotFound(BlockNumberOrTag::Pending.into()))?
|
|
.base_fee_per_gas
|
|
.ok_or(EthApiError::InvalidTransaction(
|
|
RpcInvalidTransactionError::TxTypeNotSupported,
|
|
))?;
|
|
U256::from(base_fee)
|
|
}
|
|
};
|
|
|
|
let max_priority_fee_per_gas = match max_priority_fee_per_gas {
|
|
Some(max_priority_fee_per_gas) => max_priority_fee_per_gas,
|
|
None => self.suggested_priority_fee().await?,
|
|
};
|
|
Ok((base_fee, max_priority_fee_per_gas))
|
|
}
|
|
}
|
|
|
|
/// Returns the EIP-4844 blob fee if it is set, otherwise fetches a blob fee.
|
|
fn eip4844_blob_fee(
|
|
&self,
|
|
blob_fee: Option<U256>,
|
|
) -> impl Future<Output = Result<U256, Self::Error>> + Send {
|
|
async move {
|
|
match blob_fee {
|
|
Some(blob_fee) => Ok(blob_fee),
|
|
None => self.blob_base_fee().await,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns a suggestion for a gas price for legacy transactions.
|
|
///
|
|
/// See also: <https://github.com/ethereum/pm/issues/328#issuecomment-853234014>
|
|
fn gas_price(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send {
|
|
let header = self.block_with_senders(BlockNumberOrTag::Latest.into());
|
|
let suggested_tip = self.suggested_priority_fee();
|
|
async move {
|
|
let (header, suggested_tip) = futures::try_join!(header, suggested_tip)?;
|
|
let base_fee = header.and_then(|h| h.base_fee_per_gas).unwrap_or_default();
|
|
Ok(suggested_tip + U256::from(base_fee))
|
|
}
|
|
}
|
|
|
|
/// Returns a suggestion for a base fee for blob transactions.
|
|
fn blob_base_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send {
|
|
async move {
|
|
self.block_with_senders(BlockNumberOrTag::Latest.into())
|
|
.await?
|
|
.and_then(|h| h.next_block_blob_fee())
|
|
.ok_or(EthApiError::ExcessBlobGasNotSet.into())
|
|
.map(U256::from)
|
|
}
|
|
}
|
|
|
|
/// Returns a suggestion for the priority fee (the tip)
|
|
fn suggested_priority_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
|
|
where
|
|
Self: 'static,
|
|
{
|
|
async move { self.gas_oracle().suggest_tip_cap().await.map_err(Self::Error::from_eth_err) }
|
|
}
|
|
}
|