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