reth_rpc_eth_api/helpers/
fee.rs

1//! Loads fee history from database. Helper trait for `eth_` fee and transaction RPC methods.
2
3use super::LoadBlock;
4use crate::FromEthApiError;
5use alloy_consensus::BlockHeader;
6use alloy_eips::eip7840::BlobParams;
7use alloy_primitives::U256;
8use alloy_rpc_types_eth::{BlockNumberOrTag, FeeHistory};
9use futures::Future;
10use reth_chainspec::{ChainSpecProvider, EthChainSpec};
11use reth_primitives_traits::BlockBody;
12use reth_rpc_eth_types::{
13    fee_history::calculate_reward_percentiles_for_block, utils::checked_blob_gas_used_ratio,
14    EthApiError, FeeHistoryCache, FeeHistoryEntry, GasPriceOracle, RpcInvalidTransactionError,
15};
16use reth_storage_api::{BlockIdReader, BlockReaderIdExt, HeaderProvider, ProviderHeader};
17use tracing::debug;
18
19/// Fee related functions for the [`EthApiServer`](crate::EthApiServer) trait in the
20/// `eth_` namespace.
21pub trait EthFees:
22    LoadFee<
23    Provider: ChainSpecProvider<ChainSpec: EthChainSpec<Header = ProviderHeader<Self::Provider>>>,
24>
25{
26    /// Returns a suggestion for a gas price for legacy transactions.
27    ///
28    /// See also: <https://github.com/ethereum/pm/issues/328#issuecomment-853234014>
29    fn gas_price(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
30    where
31        Self: LoadBlock,
32    {
33        LoadFee::gas_price(self)
34    }
35
36    /// Returns a suggestion for a base fee for blob transactions.
37    fn blob_base_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
38    where
39        Self: LoadBlock,
40    {
41        LoadFee::blob_base_fee(self)
42    }
43
44    /// Returns a suggestion for the priority fee (the tip)
45    fn suggested_priority_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
46    where
47        Self: 'static,
48    {
49        LoadFee::suggested_priority_fee(self)
50    }
51
52    /// Reports the fee history, for the given amount of blocks, up until the given newest block.
53    ///
54    /// If `reward_percentiles` are provided the [`FeeHistory`] will include the _approximated_
55    /// rewards for the requested range.
56    fn fee_history(
57        &self,
58        mut block_count: u64,
59        mut newest_block: BlockNumberOrTag,
60        reward_percentiles: Option<Vec<f64>>,
61    ) -> impl Future<Output = Result<FeeHistory, Self::Error>> + Send {
62        async move {
63            if block_count == 0 {
64                return Ok(FeeHistory::default())
65            }
66
67            // ensure the given reward percentiles aren't excessive
68            if reward_percentiles.as_ref().map(|perc| perc.len() as u64) >
69                Some(self.gas_oracle().config().max_reward_percentile_count)
70            {
71                return Err(EthApiError::InvalidRewardPercentiles.into())
72            }
73
74            // See https://github.com/ethereum/go-ethereum/blob/2754b197c935ee63101cbbca2752338246384fec/eth/gasprice/feehistory.go#L218C8-L225
75            let max_fee_history = if reward_percentiles.is_none() {
76                self.gas_oracle().config().max_header_history
77            } else {
78                self.gas_oracle().config().max_block_history
79            };
80
81            if block_count > max_fee_history {
82                debug!(
83                    requested = block_count,
84                    truncated = max_fee_history,
85                    "Sanitizing fee history block count"
86                );
87                block_count = max_fee_history
88            }
89
90            if newest_block.is_pending() {
91                // cap the target block since we don't have fee history for the pending block
92                newest_block = BlockNumberOrTag::Latest;
93            }
94
95            let end_block = self
96                .provider()
97                .block_number_for_id(newest_block.into())
98                .map_err(Self::Error::from_eth_err)?
99                .ok_or(EthApiError::HeaderNotFound(newest_block.into()))?;
100
101            // need to add 1 to the end block to get the correct (inclusive) range
102            let end_block_plus = end_block + 1;
103            // Ensure that we would not be querying outside of genesis
104            if end_block_plus < block_count {
105                block_count = end_block_plus;
106            }
107
108            // If reward percentiles were specified, we
109            // need to validate that they are monotonically
110            // increasing and 0 <= p <= 100
111            // Note: The types used ensure that the percentiles are never < 0
112            if let Some(percentiles) = &reward_percentiles &&
113                (percentiles.iter().any(|p| *p < 0.0 || *p > 100.0) ||
114                    percentiles.windows(2).any(|w| w[0] > w[1]))
115            {
116                return Err(EthApiError::InvalidRewardPercentiles.into())
117            }
118
119            // Fetch the headers and ensure we got all of them
120            //
121            // Treat a request for 1 block as a request for `newest_block..=newest_block`,
122            // otherwise `newest_block - 2`
123            // NOTE: We ensured that block count is capped
124            let start_block = end_block_plus - block_count;
125
126            // Collect base fees, gas usage ratios and (optionally) reward percentile data
127            let mut base_fee_per_gas: Vec<u128> = Vec::new();
128            let mut gas_used_ratio: Vec<f64> = Vec::new();
129
130            let mut base_fee_per_blob_gas: Vec<u128> = Vec::new();
131            let mut blob_gas_used_ratio: Vec<f64> = Vec::new();
132
133            let mut rewards: Vec<Vec<u128>> = Vec::new();
134
135            // Check if the requested range is within the cache bounds
136            let fee_entries = self.fee_history_cache().get_history(start_block, end_block).await;
137
138            if let Some(fee_entries) = fee_entries {
139                if fee_entries.len() != block_count as usize {
140                    return Err(EthApiError::InvalidBlockRange.into())
141                }
142
143                for entry in &fee_entries {
144                    base_fee_per_gas
145                        .push(entry.header.base_fee_per_gas().unwrap_or_default() as u128);
146                    gas_used_ratio.push(entry.gas_used_ratio);
147                    base_fee_per_blob_gas.push(entry.base_fee_per_blob_gas.unwrap_or_default());
148                    blob_gas_used_ratio.push(entry.blob_gas_used_ratio);
149
150                    if let Some(percentiles) = &reward_percentiles {
151                        let mut block_rewards = Vec::with_capacity(percentiles.len());
152                        for &percentile in percentiles {
153                            block_rewards.push(self.approximate_percentile(entry, percentile));
154                        }
155                        rewards.push(block_rewards);
156                    }
157                }
158                let last_entry = fee_entries.last().expect("is not empty");
159
160                // Also need to include the `base_fee_per_gas` and `base_fee_per_blob_gas` for the
161                // next block
162                base_fee_per_gas.push(
163                    self.provider()
164                        .chain_spec()
165                        .next_block_base_fee(&last_entry.header, last_entry.header.timestamp())
166                        .unwrap_or_default() as u128,
167                );
168
169                base_fee_per_blob_gas.push(last_entry.next_block_blob_fee().unwrap_or_default());
170            } else {
171                // read the requested header range
172                let headers = self.provider()
173                    .sealed_headers_range(start_block..=end_block)
174                    .map_err(Self::Error::from_eth_err)?;
175                if headers.len() != block_count as usize {
176                    return Err(EthApiError::InvalidBlockRange.into())
177                }
178
179                let chain_spec = self.provider().chain_spec();
180                for header in &headers {
181                    base_fee_per_gas.push(header.base_fee_per_gas().unwrap_or_default() as u128);
182                    gas_used_ratio.push(header.gas_used() as f64 / header.gas_limit() as f64);
183
184                    let blob_params = chain_spec
185                        .blob_params_at_timestamp(header.timestamp())
186                        .unwrap_or_else(BlobParams::cancun);
187
188                    base_fee_per_blob_gas.push(header.blob_fee(blob_params).unwrap_or_default());
189                    blob_gas_used_ratio.push(
190                        checked_blob_gas_used_ratio(
191                            header.blob_gas_used().unwrap_or_default(),
192                            blob_params.max_blob_gas_per_block(),
193                        )
194                    );
195
196                    // Percentiles were specified, so we need to collect reward percentile info
197                    if let Some(percentiles) = &reward_percentiles {
198                        let (block, receipts) = self.cache()
199                            .get_block_and_receipts(header.hash())
200                            .await
201                            .map_err(Self::Error::from_eth_err)?
202                            .ok_or(EthApiError::InvalidBlockRange)?;
203                        rewards.push(
204                            calculate_reward_percentiles_for_block(
205                                percentiles,
206                                header.gas_used(),
207                                header.base_fee_per_gas().unwrap_or_default(),
208                                block.body().transactions(),
209                                &receipts,
210                            )
211                            .unwrap_or_default(),
212                        );
213                    }
214                }
215
216                // The spec states that `base_fee_per_gas` "[..] includes the next block after the
217                // newest of the returned range, because this value can be derived from the
218                // newest block"
219                //
220                // The unwrap is safe since we checked earlier that we got at least 1 header.
221                let last_header = headers.last().expect("is present");
222                base_fee_per_gas.push(
223                    chain_spec
224                        .next_block_base_fee(last_header.header(), last_header.timestamp())
225                        .unwrap_or_default() as u128,
226                );
227                // Same goes for the `base_fee_per_blob_gas`:
228                // > "[..] includes the next block after the newest of the returned range, because this value can be derived from the newest block.
229                base_fee_per_blob_gas.push(
230                    last_header
231                    .maybe_next_block_blob_fee(
232                        chain_spec.blob_params_at_timestamp(last_header.timestamp())
233                    ).unwrap_or_default()
234                );
235            };
236
237            Ok(FeeHistory {
238                base_fee_per_gas,
239                gas_used_ratio,
240                base_fee_per_blob_gas,
241                blob_gas_used_ratio,
242                oldest_block: start_block,
243                reward: reward_percentiles.map(|_| rewards),
244            })
245        }
246    }
247
248    /// Approximates reward at a given percentile for a specific block
249    /// Based on the configured resolution
250    fn approximate_percentile(
251        &self,
252        entry: &FeeHistoryEntry<ProviderHeader<Self::Provider>>,
253        requested_percentile: f64,
254    ) -> u128 {
255        let resolution = self.fee_history_cache().resolution();
256        let rounded_percentile =
257            (requested_percentile * resolution as f64).round() / resolution as f64;
258        let clamped_percentile = rounded_percentile.clamp(0.0, 100.0);
259
260        // Calculate the index in the precomputed rewards array
261        let index = (clamped_percentile / (1.0 / resolution as f64)).round() as usize;
262        // Fetch the reward from the FeeHistoryEntry
263        entry.rewards.get(index).copied().unwrap_or_default()
264    }
265}
266
267/// Loads fee from database.
268///
269/// Behaviour shared by several `eth_` RPC methods, not exclusive to `eth_` fees RPC methods.
270pub trait LoadFee: LoadBlock
271where
272    Self::Provider: BlockReaderIdExt,
273{
274    /// Returns a handle for reading gas price.
275    ///
276    /// Data access in default (L1) trait method implementations.
277    fn gas_oracle(&self) -> &GasPriceOracle<Self::Provider>;
278
279    /// Returns a handle for reading fee history data from memory.
280    ///
281    /// Data access in default (L1) trait method implementations.
282    fn fee_history_cache(&self) -> &FeeHistoryCache<ProviderHeader<Self::Provider>>;
283
284    /// Returns the gas price if it is set, otherwise fetches a suggested gas price for legacy
285    /// transactions.
286    fn legacy_gas_price(
287        &self,
288        gas_price: Option<U256>,
289    ) -> impl Future<Output = Result<U256, Self::Error>> + Send {
290        async move {
291            match gas_price {
292                Some(gas_price) => Ok(gas_price),
293                None => {
294                    // fetch a suggested gas price
295                    self.gas_price().await
296                }
297            }
298        }
299    }
300
301    /// Returns the EIP-1559 fees if they are set, otherwise fetches a suggested gas price for
302    /// EIP-1559 transactions.
303    ///
304    /// Returns (`base_fee`, `priority_fee`)
305    fn eip1559_fees(
306        &self,
307        base_fee: Option<U256>,
308        max_priority_fee_per_gas: Option<U256>,
309    ) -> impl Future<Output = Result<(U256, U256), Self::Error>> + Send {
310        async move {
311            let base_fee = match base_fee {
312                Some(base_fee) => base_fee,
313                None => {
314                    // fetch pending base fee
315                    let base_fee = self
316                        .recovered_block(BlockNumberOrTag::Pending.into())
317                        .await?
318                        .ok_or(EthApiError::HeaderNotFound(BlockNumberOrTag::Pending.into()))?
319                        .base_fee_per_gas()
320                        .ok_or(EthApiError::InvalidTransaction(
321                            RpcInvalidTransactionError::TxTypeNotSupported,
322                        ))?;
323                    U256::from(base_fee)
324                }
325            };
326
327            let max_priority_fee_per_gas = match max_priority_fee_per_gas {
328                Some(max_priority_fee_per_gas) => max_priority_fee_per_gas,
329                None => self.suggested_priority_fee().await?,
330            };
331            Ok((base_fee, max_priority_fee_per_gas))
332        }
333    }
334
335    /// Returns the EIP-4844 blob fee if it is set, otherwise fetches a blob fee.
336    fn eip4844_blob_fee(
337        &self,
338        blob_fee: Option<U256>,
339    ) -> impl Future<Output = Result<U256, Self::Error>> + Send {
340        async move {
341            match blob_fee {
342                Some(blob_fee) => Ok(blob_fee),
343                None => self.blob_base_fee().await,
344            }
345        }
346    }
347
348    /// Returns a suggestion for a gas price for legacy transactions.
349    ///
350    /// See also: <https://github.com/ethereum/pm/issues/328#issuecomment-853234014>
351    fn gas_price(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send {
352        async move {
353            let header = self.provider().latest_header().map_err(Self::Error::from_eth_err)?;
354            let suggested_tip = self.suggested_priority_fee().await?;
355            let base_fee = header.and_then(|h| h.base_fee_per_gas()).unwrap_or_default();
356            Ok(suggested_tip + U256::from(base_fee))
357        }
358    }
359
360    /// Returns a suggestion for a base fee for blob transactions.
361    fn blob_base_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send {
362        async move {
363            self.provider()
364                .latest_header()
365                .map_err(Self::Error::from_eth_err)?
366                .and_then(|h| {
367                    h.maybe_next_block_blob_fee(
368                        self.provider().chain_spec().blob_params_at_timestamp(h.timestamp()),
369                    )
370                })
371                .ok_or(EthApiError::ExcessBlobGasNotSet.into())
372                .map(U256::from)
373        }
374    }
375
376    /// Returns a suggestion for the priority fee (the tip)
377    fn suggested_priority_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
378    where
379        Self: 'static,
380    {
381        async move { self.gas_oracle().suggest_tip_cap().await.map_err(Self::Error::from_eth_err) }
382    }
383}