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