reth_rpc_eth_api/helpers/
fee.rs1use 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
21pub trait EthFees:
24 LoadFee<
25 Provider: ChainSpecProvider<ChainSpec: EthChainSpec<Header = ProviderHeader<Self::Provider>>>,
26>
27{
28 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 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 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 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 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 if let Some(percentiles) = &reward_percentiles &&
81 (percentiles.iter().any(|p| *p < 0.0 || *p > 100.0) ||
82 percentiles.windows(2).any(|w| w[0] > w[1]))
83 {
84 return Err(EthApiError::InvalidRewardPercentiles.into())
85 }
86
87 let max_fee_history = if reward_percentiles.is_none() {
89 self.gas_oracle().config().max_header_history
90 } else {
91 self.gas_oracle().config().max_block_history
92 };
93
94 if block_count > max_fee_history {
95 debug!(
96 requested = block_count,
97 truncated = max_fee_history,
98 "Sanitizing fee history block count"
99 );
100 block_count = max_fee_history
101 }
102
103 if newest_block.is_pending() {
104 newest_block = BlockNumberOrTag::Latest;
106 }
107
108 if let BlockNumberOrTag::Number(requested) = newest_block {
110 let latest_block =
111 self.provider().best_block_number().map_err(Self::Error::from_eth_err)?;
112 if requested > latest_block {
113 return Err(
114 EthApiError::RequestBeyondHead { requested, head: latest_block }.into()
115 )
116 }
117 }
118
119 let end_block = self
120 .provider()
121 .block_number_for_id(newest_block.into())
122 .map_err(Self::Error::from_eth_err)?
123 .ok_or(EthApiError::HeaderNotFound(newest_block.into()))?;
124
125 let end_block_plus = end_block + 1;
127 if end_block_plus < block_count {
129 block_count = end_block_plus;
130 }
131
132 let start_block = end_block_plus - block_count;
138
139 let mut base_fee_per_gas: Vec<u128> = Vec::with_capacity(block_count as usize + 1);
142 let mut gas_used_ratio: Vec<f64> = Vec::with_capacity(block_count as usize);
143
144 let mut base_fee_per_blob_gas: Vec<u128> = Vec::with_capacity(block_count as usize + 1);
145 let mut blob_gas_used_ratio: Vec<f64> = Vec::with_capacity(block_count as usize);
146
147 let mut rewards: Vec<Vec<u128>> = if reward_percentiles.is_some() {
148 Vec::with_capacity(block_count as usize)
149 } else {
150 Vec::new()
151 };
152
153 let fee_entries = self.fee_history_cache().get_history(start_block, end_block).await;
155
156 if let Some(fee_entries) = fee_entries {
157 if fee_entries.len() != block_count as usize {
158 return Err(EthApiError::InvalidBlockRange.into())
159 }
160
161 for entry in &fee_entries {
162 base_fee_per_gas
163 .push(entry.header.base_fee_per_gas().unwrap_or_default() as u128);
164 gas_used_ratio.push(entry.gas_used_ratio);
165 base_fee_per_blob_gas.push(entry.base_fee_per_blob_gas.unwrap_or_default());
166 blob_gas_used_ratio.push(entry.blob_gas_used_ratio);
167
168 if let Some(percentiles) = &reward_percentiles {
169 let mut block_rewards = Vec::with_capacity(percentiles.len());
170 for &percentile in percentiles {
171 block_rewards.push(self.approximate_percentile(entry, percentile));
172 }
173 rewards.push(block_rewards);
174 }
175 }
176 let last_entry = fee_entries.last().expect("is not empty");
177
178 base_fee_per_gas.push(
181 self.provider()
182 .chain_spec()
183 .next_block_base_fee(&last_entry.header, last_entry.header.timestamp())
184 .unwrap_or_default() as u128,
185 );
186
187 base_fee_per_blob_gas.push(last_entry.next_block_blob_fee().unwrap_or_default());
188 } else {
189 let headers = self.provider()
191 .sealed_headers_range(start_block..=end_block)
192 .map_err(Self::Error::from_eth_err)?;
193 if headers.len() != block_count as usize {
194 return Err(EthApiError::InvalidBlockRange.into())
195 }
196
197 let chain_spec = self.provider().chain_spec();
198 for header in &headers {
199 base_fee_per_gas.push(header.base_fee_per_gas().unwrap_or_default() as u128);
200 gas_used_ratio.push(header.gas_used() as f64 / header.gas_limit() as f64);
201
202 let blob_params = chain_spec
203 .blob_params_at_timestamp(header.timestamp())
204 .unwrap_or_else(BlobParams::cancun);
205
206 base_fee_per_blob_gas.push(header.blob_fee(blob_params).unwrap_or_default());
207 blob_gas_used_ratio.push(checked_blob_gas_used_ratio(
208 header.blob_gas_used().unwrap_or_default(),
209 blob_params.max_blob_gas_per_block(),
210 ));
211 }
212
213 if let Some(percentiles) = reward_percentiles.as_ref().filter(|p| !p.is_empty()) {
214 let hashes: Vec<_> = headers.iter().map(|h| h.hash()).collect();
215 let mut stream =
216 futures::stream::iter(hashes)
217 .map(|hash| self.cache().get_block_and_receipts(hash))
218 .buffered(4);
219 let mut header_idx = 0;
220 while let Some(result) = stream.next().await {
221 let header = &headers[header_idx];
222 header_idx += 1;
223 let (block, receipts) = result
224 .map_err(Self::Error::from_eth_err)?
225 .ok_or(EthApiError::InvalidBlockRange)?;
226 rewards.push(
227 calculate_reward_percentiles_for_block(
228 percentiles,
229 header.gas_used(),
230 header.base_fee_per_gas().unwrap_or_default(),
231 block.body().transactions(),
232 &receipts,
233 )
234 .unwrap_or_default(),
235 );
236 }
237 }
238
239 let last_header = headers.last().expect("is present");
245 base_fee_per_gas.push(
246 chain_spec
247 .next_block_base_fee(last_header.header(), last_header.timestamp())
248 .unwrap_or_default() as u128,
249 );
250 base_fee_per_blob_gas.push(
253 last_header
254 .maybe_next_block_blob_fee(
255 chain_spec.blob_params_at_timestamp(last_header.timestamp())
256 ).unwrap_or_default()
257 );
258 };
259
260 Ok(FeeHistory {
261 base_fee_per_gas,
262 gas_used_ratio,
263 base_fee_per_blob_gas,
264 blob_gas_used_ratio,
265 oldest_block: start_block,
266 reward: reward_percentiles.map(|_| rewards),
267 })
268 }
269 }
270
271 fn approximate_percentile(
274 &self,
275 entry: &FeeHistoryEntry<ProviderHeader<Self::Provider>>,
276 requested_percentile: f64,
277 ) -> u128 {
278 let resolution = self.fee_history_cache().resolution();
279 let rounded_percentile =
280 (requested_percentile * resolution as f64).round() / resolution as f64;
281 let clamped_percentile = rounded_percentile.clamp(0.0, 100.0);
282
283 let index = (clamped_percentile / (1.0 / resolution as f64)).round() as usize;
285 entry.rewards.get(index).copied().unwrap_or_default()
287 }
288}
289
290pub trait LoadFee: LoadBlock
294where
295 Self::Provider: BlockReaderIdExt,
296{
297 fn gas_oracle(&self) -> &GasPriceOracle<Self::Provider>;
301
302 fn fee_history_cache(&self) -> &FeeHistoryCache<ProviderHeader<Self::Provider>>;
306
307 fn legacy_gas_price(
310 &self,
311 gas_price: Option<U256>,
312 ) -> impl Future<Output = Result<U256, Self::Error>> + Send {
313 async move {
314 match gas_price {
315 Some(gas_price) => Ok(gas_price),
316 None => {
317 self.gas_price().await
319 }
320 }
321 }
322 }
323
324 fn eip1559_fees(
329 &self,
330 base_fee: Option<U256>,
331 max_priority_fee_per_gas: Option<U256>,
332 ) -> impl Future<Output = Result<(U256, U256), Self::Error>> + Send {
333 async move {
334 let base_fee = match base_fee {
335 Some(base_fee) => base_fee,
336 None => {
337 let latest = self
339 .provider()
340 .latest_header()
341 .map_err(Self::Error::from_eth_err)?
342 .ok_or(EthApiError::HeaderNotFound(BlockNumberOrTag::Latest.into()))?;
343 let pending_base_fee = self
344 .provider()
345 .chain_spec()
346 .next_block_base_fee(&latest, latest.timestamp())
347 .ok_or(EthApiError::InvalidTransaction(
348 RpcInvalidTransactionError::TxTypeNotSupported,
349 ))?;
350 U256::from(pending_base_fee)
351 }
352 };
353
354 let max_priority_fee_per_gas = match max_priority_fee_per_gas {
355 Some(max_priority_fee_per_gas) => max_priority_fee_per_gas,
356 None => self.suggested_priority_fee().await?,
357 };
358 Ok((base_fee, max_priority_fee_per_gas))
359 }
360 }
361
362 fn eip4844_blob_fee(
364 &self,
365 blob_fee: Option<U256>,
366 ) -> impl Future<Output = Result<U256, Self::Error>> + Send {
367 async move {
368 match blob_fee {
369 Some(blob_fee) => Ok(blob_fee),
370 None => self.blob_base_fee().await,
371 }
372 }
373 }
374
375 fn gas_price(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send {
379 async move {
380 let header = self.provider().latest_header().map_err(Self::Error::from_eth_err)?;
381 let suggested_tip = self.suggested_priority_fee().await?;
382 let base_fee = header.and_then(|h| h.base_fee_per_gas()).unwrap_or_default();
383 Ok(suggested_tip + U256::from(base_fee))
384 }
385 }
386
387 fn blob_base_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send {
389 async move {
390 self.provider()
391 .latest_header()
392 .map_err(Self::Error::from_eth_err)?
393 .and_then(|h| {
394 h.maybe_next_block_blob_fee(
395 self.provider().chain_spec().blob_params_at_timestamp(h.timestamp()),
396 )
397 })
398 .ok_or(EthApiError::ExcessBlobGasNotSet.into())
399 .map(U256::from)
400 }
401 }
402
403 fn suggested_priority_fee(&self) -> impl Future<Output = Result<U256, Self::Error>> + Send
405 where
406 Self: 'static,
407 {
408 async move { self.gas_oracle().suggest_tip_cap().await.map_err(Self::Error::from_eth_err) }
409 }
410}