1use std::{
4 collections::{BTreeMap, VecDeque},
5 fmt::Debug,
6 sync::{atomic::Ordering::SeqCst, Arc},
7};
8
9use alloy_consensus::{BlockHeader, Header, Transaction, TxReceipt};
10use alloy_eips::eip7840::BlobParams;
11use alloy_rpc_types_eth::TxGasAndReward;
12use futures::{
13 future::{Fuse, FusedFuture},
14 FutureExt, Stream, StreamExt,
15};
16use metrics::atomics::AtomicU64;
17use reth_chain_state::CanonStateNotification;
18use reth_chainspec::{ChainSpecProvider, EthChainSpec};
19use reth_primitives_traits::{Block, BlockBody, NodePrimitives, SealedBlock};
20use reth_rpc_server_types::constants::gas_oracle::MAX_HEADER_HISTORY;
21use reth_storage_api::BlockReaderIdExt;
22use serde::{Deserialize, Serialize};
23use tracing::trace;
24
25use crate::utils::checked_blob_gas_used_ratio;
26
27use super::{EthApiError, EthStateCache};
28
29#[derive(Debug, Clone)]
33pub struct FeeHistoryCache<H> {
34 inner: Arc<FeeHistoryCacheInner<H>>,
35}
36
37impl<H> FeeHistoryCache<H>
38where
39 H: BlockHeader + Clone,
40{
41 pub fn new(config: FeeHistoryCacheConfig) -> Self {
43 let inner = FeeHistoryCacheInner {
44 lower_bound: Default::default(),
45 upper_bound: Default::default(),
46 config,
47 entries: Default::default(),
48 };
49 Self { inner: Arc::new(inner) }
50 }
51
52 #[inline]
54 pub fn config(&self) -> &FeeHistoryCacheConfig {
55 &self.inner.config
56 }
57
58 #[inline]
60 pub fn resolution(&self) -> u64 {
61 self.config().resolution
62 }
63
64 async fn missing_consecutive_blocks(&self) -> VecDeque<u64> {
70 let entries = self.inner.entries.read().await;
71 (self.lower_bound()..self.upper_bound())
72 .rev()
73 .filter(|&block_number| !entries.contains_key(&block_number))
74 .collect()
75 }
76
77 async fn insert_blocks<'a, I, B, R, C>(&self, blocks: I, chain_spec: &C)
79 where
80 B: Block<Header = H> + 'a,
81 R: TxReceipt + 'a,
82 I: IntoIterator<Item = (&'a SealedBlock<B>, &'a [R])>,
83 C: EthChainSpec,
84 {
85 let mut entries = self.inner.entries.write().await;
86
87 let percentiles = self.predefined_percentiles();
88 for (block, receipts) in blocks {
90 let mut fee_history_entry = FeeHistoryEntry::<H>::new(
91 block,
92 chain_spec.blob_params_at_timestamp(block.header().timestamp()),
93 );
94 fee_history_entry.rewards = calculate_reward_percentiles_for_block(
95 &percentiles,
96 fee_history_entry.header.base_fee_per_gas().unwrap_or_default(),
97 block.body().transactions(),
98 receipts,
99 )
100 .unwrap_or_default();
101 entries.insert(block.number(), Arc::new(fee_history_entry));
102 }
103
104 while entries.len() > self.inner.config.max_blocks as usize {
106 entries.pop_first();
107 }
108
109 if entries.is_empty() {
110 self.inner.upper_bound.store(0, SeqCst);
111 self.inner.lower_bound.store(0, SeqCst);
112 return
113 }
114
115 let upper_bound = *entries.last_entry().expect("Contains at least one entry").key();
116
117 let target_lower = upper_bound.saturating_sub(self.inner.config.max_blocks);
119 while entries.len() > 1 && *entries.first_key_value().unwrap().0 < target_lower {
120 entries.pop_first();
121 }
122
123 let lower_bound = *entries.first_entry().expect("Contains at least one entry").key();
124 self.inner.upper_bound.store(upper_bound, SeqCst);
125 self.inner.lower_bound.store(lower_bound, SeqCst);
126 }
127
128 pub fn upper_bound(&self) -> u64 {
130 self.inner.upper_bound.load(SeqCst)
131 }
132
133 pub fn lower_bound(&self) -> u64 {
135 self.inner.lower_bound.load(SeqCst)
136 }
137
138 pub async fn get_history(
145 &self,
146 start_block: u64,
147 end_block: u64,
148 ) -> Option<Vec<Arc<FeeHistoryEntry<H>>>> {
149 if end_block < start_block {
150 return None
152 }
153 let lower_bound = self.lower_bound();
154 let upper_bound = self.upper_bound();
155 if start_block >= lower_bound && end_block <= upper_bound {
156 let entries = self.inner.entries.read().await;
157 let result = entries
158 .range(start_block..=end_block)
159 .map(|(_, fee_entry)| fee_entry.clone())
160 .collect::<Vec<_>>();
161
162 if result.is_empty() {
163 return None
164 }
165
166 Some(result)
167 } else {
168 None
169 }
170 }
171
172 pub fn predefined_percentiles(&self) -> Vec<f64> {
176 let res = self.resolution() as f64;
177 (0..=100 * self.resolution()).map(|p| p as f64 / res).collect()
178 }
179}
180
181#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct FeeHistoryCacheConfig {
185 pub max_blocks: u64,
190 pub resolution: u64,
194}
195
196impl Default for FeeHistoryCacheConfig {
197 fn default() -> Self {
198 Self { max_blocks: MAX_HEADER_HISTORY + 100, resolution: 4 }
199 }
200}
201
202#[derive(Debug)]
204struct FeeHistoryCacheInner<H> {
205 lower_bound: AtomicU64,
207 upper_bound: AtomicU64,
209 config: FeeHistoryCacheConfig,
212 entries: tokio::sync::RwLock<BTreeMap<u64, Arc<FeeHistoryEntry<H>>>>,
214}
215
216pub async fn fee_history_cache_new_blocks_task<St, Provider, N>(
219 fee_history_cache: FeeHistoryCache<N::BlockHeader>,
220 mut events: St,
221 provider: Provider,
222 cache: EthStateCache<N>,
223) where
224 St: Stream<Item = CanonStateNotification<N>> + Unpin + 'static,
225 Provider:
226 BlockReaderIdExt<Block = N::Block, Receipt = N::Receipt> + ChainSpecProvider + 'static,
227 N: NodePrimitives,
228 N::BlockHeader: BlockHeader + Clone,
229{
230 let mut missing_blocks = VecDeque::new();
233 let mut fetch_missing_block = Fuse::terminated();
234
235 loop {
236 if fetch_missing_block.is_terminated() &&
237 let Some(block_number) = missing_blocks.pop_front()
238 {
239 trace!(target: "rpc::fee", ?block_number, "Fetching missing block for fee history cache");
240 if let Ok(Some(hash)) = provider.block_hash(block_number) {
241 fetch_missing_block = cache.get_block_and_receipts(hash).boxed().fuse();
243 }
244 }
245
246 let chain_spec = provider.chain_spec();
247
248 tokio::select! {
249 res = &mut fetch_missing_block => {
250 if let Ok(res) = res {
251 let res = res.as_ref()
252 .map(|(b, r)| (b.sealed_block(), r.as_slice()));
253 fee_history_cache.insert_blocks(res, &chain_spec).await;
254 }
255 }
256 event = events.next() => {
257 let Some(event) = event else {
258 break
260 };
261
262 let committed = event.committed();
263 let blocks_and_receipts = committed
264 .blocks_and_receipts()
265 .map(|(block, receipts)| {
266 (block.sealed_block(), receipts.as_slice())
267 });
268 fee_history_cache.insert_blocks(blocks_and_receipts, &chain_spec).await;
269
270 missing_blocks = fee_history_cache.missing_consecutive_blocks().await;
272 }
273 }
274 }
275}
276
277pub fn calculate_reward_percentiles_for_block<T, R>(
281 percentiles: &[f64],
282 base_fee_per_gas: u64,
283 transactions: &[T],
284 receipts: &[R],
285) -> Result<Vec<u128>, EthApiError>
286where
287 T: Transaction,
288 R: TxReceipt,
289{
290 let mut transactions = transactions
291 .iter()
292 .zip(receipts)
293 .scan(0, |previous_gas, (tx, receipt)| {
294 let gas_used = receipt.cumulative_gas_used() - *previous_gas;
301 *previous_gas = receipt.cumulative_gas_used();
302
303 Some(TxGasAndReward {
304 gas_used,
305 reward: tx.effective_tip_per_gas(base_fee_per_gas).unwrap_or_default(),
306 })
307 })
308 .collect::<Vec<_>>();
309
310 let total_receipt_gas_used =
312 receipts.last().map(|receipt| receipt.cumulative_gas_used()).unwrap_or_default();
313
314 transactions.sort_unstable_by_key(|tx| tx.reward);
316
317 let mut tx_index = 0;
322 let mut cumulative_gas_used = transactions.first().map(|tx| tx.gas_used).unwrap_or_default();
323 let mut rewards_in_block = Vec::with_capacity(percentiles.len());
324 for percentile in percentiles {
325 if transactions.is_empty() {
327 rewards_in_block.push(0);
328 continue
329 }
330
331 let threshold = (total_receipt_gas_used as f64 * percentile / 100.) as u64;
332 while cumulative_gas_used < threshold && tx_index < transactions.len() - 1 {
333 tx_index += 1;
334 cumulative_gas_used += transactions[tx_index].gas_used;
335 }
336 rewards_in_block.push(transactions[tx_index].reward);
337 }
338
339 Ok(rewards_in_block)
340}
341
342#[derive(Debug, Clone)]
344pub struct FeeHistoryEntry<H = Header> {
345 pub header: H,
347 pub gas_used_ratio: f64,
349 pub base_fee_per_blob_gas: Option<u128>,
352 pub blob_gas_used_ratio: f64,
357 pub rewards: Vec<u128>,
359 pub blob_params: Option<BlobParams>,
361}
362
363impl<H> FeeHistoryEntry<H>
364where
365 H: BlockHeader + Clone,
366{
367 pub fn new<B>(block: &SealedBlock<B>, blob_params: Option<BlobParams>) -> Self
371 where
372 B: Block<Header = H>,
373 {
374 let header = block.header();
375 Self {
376 header: block.header().clone(),
377 gas_used_ratio: header.gas_used() as f64 / header.gas_limit() as f64,
378 base_fee_per_blob_gas: header
379 .excess_blob_gas()
380 .and_then(|excess_blob_gas| Some(blob_params?.calc_blob_fee(excess_blob_gas))),
381 blob_gas_used_ratio: checked_blob_gas_used_ratio(
382 block.body().blob_gas_used(),
383 blob_params
384 .as_ref()
385 .map(|params| params.max_blob_gas_per_block())
386 .unwrap_or(alloy_eips::eip4844::MAX_DATA_GAS_PER_BLOCK_DENCUN),
387 ),
388 rewards: Vec::new(),
389 blob_params,
390 }
391 }
392
393 pub fn next_block_blob_fee(&self) -> Option<u128> {
399 self.header.maybe_next_block_blob_fee(self.blob_params)
400 }
401
402 pub fn next_block_excess_blob_gas(&self) -> Option<u64> {
406 self.header.maybe_next_block_excess_blob_gas(self.blob_params)
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use alloy_consensus::{TxEip1559, TxType};
414 use alloy_primitives::Signature;
415 use reth_ethereum_primitives::{Receipt, Transaction as EthTransaction, TransactionSigned};
416
417 #[test]
418 fn reward_percentiles_use_receipt_gas_weight_for_eip8037() {
419 const BASE_FEE: u64 = 1;
420 const LOW_GAS: u64 = 100_000;
421 const HIGH_GAS: u64 = 21_000;
422
423 let low_tip = 1;
424 let high_tip = 100_000_000_000;
425 let transactions =
426 [eip1559_transaction(low_tip, BASE_FEE), eip1559_transaction(high_tip, BASE_FEE)];
427 let receipts = [receipt(LOW_GAS), receipt(LOW_GAS + HIGH_GAS)];
428
429 let rewards =
430 calculate_reward_percentiles_for_block(&[50.0], BASE_FEE, &transactions, &receipts)
431 .unwrap();
432
433 assert_eq!(rewards, vec![low_tip]);
434 }
435
436 fn eip1559_transaction(tip: u128, base_fee: u64) -> TransactionSigned {
437 TransactionSigned::new_unhashed(
438 EthTransaction::Eip1559(TxEip1559 {
439 max_priority_fee_per_gas: tip,
440 max_fee_per_gas: tip + base_fee as u128,
441 ..Default::default()
442 }),
443 Signature::test_signature(),
444 )
445 }
446
447 fn receipt(cumulative_gas_used: u64) -> Receipt {
448 Receipt { tx_type: TxType::Eip1559, success: true, cumulative_gas_used, logs: Vec::new() }
449 }
450}