Skip to main content

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