reth_rpc_eth_api/helpers/
fee.rs

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