Skip to main content

anvil/eth/
fees.rs

1use std::{
2    collections::BTreeMap,
3    fmt,
4    pin::Pin,
5    sync::{Arc, LazyLock},
6    task::{Context, Poll},
7};
8
9use alloy_consensus::{BlockHeader, Transaction, TxReceipt};
10use alloy_eips::{calc_next_block_base_fee, eip1559::BaseFeeParams, eip7840::BlobParams};
11use alloy_network::Network;
12use alloy_primitives::{B256, Bytes};
13#[cfg(feature = "optimism")]
14use foundry_evm::hardfork::FoundryHardfork;
15use futures::StreamExt;
16use parking_lot::{Mutex, RwLock};
17use revm::{context_interface::block::BlobExcessGasAndPrice, primitives::hardfork::SpecId};
18use tempo_hardfork::{TempoHardfork, constants::gas::tempo_t7_next_block_base_fee};
19
20use crate::eth::{
21    backend::{info::StorageInfo, notifications::ChainNotifications},
22    error::BlockchainError,
23};
24
25#[cfg(feature = "optimism")]
26mod optimism;
27
28/// Maximum number of entries in the fee history cache
29pub const MAX_FEE_HISTORY_CACHE_SIZE: u64 = 2048u64;
30
31/// Number of cached reward samples per percentile.
32pub(crate) const REWARD_PERCENTILE_RESOLUTION: f64 = 2.0;
33
34/// Percentile list from 0.0 to 100.0 with a 0.5 resolution (201 points).
35///
36/// Constant across blocks, so it is computed once instead of being rebuilt on every
37/// `create_fee_history_cache_item` call.
38static REWARD_PERCENTILES: LazyLock<Vec<f64>> =
39    LazyLock::new(|| (0..=200).map(|index| index as f64 / REWARD_PERCENTILE_RESOLUTION).collect());
40
41/// Initial base fee for EIP-1559 blocks.
42pub const INITIAL_BASE_FEE: u64 = 1_000_000_000;
43
44/// Initial default gas price for the first block
45pub const INITIAL_GAS_PRICE: u128 = 1_875_000_000;
46
47/// Bounds the amount the base fee can change between blocks.
48pub const BASE_FEE_CHANGE_DENOMINATOR: u128 = 8;
49
50/// Minimum suggested priority fee
51pub const MIN_SUGGESTED_PRIORITY_FEE: u128 = 1e9 as u128;
52
53/// Stores the fee related information
54#[derive(Clone, Debug)]
55pub struct FeeManager {
56    /// Fee state published as one coherent execution context.
57    state: Arc<RwLock<FeeState>>,
58    /// Whether the minimum suggested priority fee is enforced
59    is_min_priority_fee_enforced: bool,
60}
61
62#[derive(Clone, Copy, Debug)]
63struct FeeRules {
64    spec_id: SpecId,
65    base_fee: BaseFeeRules,
66    /// The active Tempo hardfork, set only when running a Tempo chain.
67    tempo_hardfork: Option<TempoHardfork>,
68}
69
70#[derive(Clone, Copy, Debug)]
71enum BaseFeeRules {
72    Standard(BaseFeeParams),
73    #[cfg(feature = "optimism")]
74    Optimism {
75        inherited: Option<optimism::OptimismBaseFeeRules>,
76        fallback: BaseFeeParams,
77    },
78}
79
80impl BaseFeeRules {
81    const fn params(self) -> BaseFeeParams {
82        match self {
83            Self::Standard(params) => params,
84            #[cfg(feature = "optimism")]
85            Self::Optimism { inherited, fallback } => {
86                if let Some(rules) = inherited {
87                    rules.params()
88                } else {
89                    fallback
90                }
91            }
92        }
93    }
94
95    fn extra_data(self) -> Bytes {
96        match self {
97            Self::Standard(_) => Bytes::new(),
98            #[cfg(feature = "optimism")]
99            Self::Optimism { inherited, .. } => {
100                inherited.map_or_else(Bytes::new, optimism::OptimismBaseFeeRules::extra_data)
101            }
102        }
103    }
104
105    fn parent_header_fees<H: BlockHeader>(self, header: &H) -> ParentHeaderFees {
106        match self {
107            Self::Standard(params) => ParentHeaderFees {
108                base_fee: calc_next_block_base_fee(
109                    header.gas_used(),
110                    header.gas_limit(),
111                    header.base_fee_per_gas().unwrap_or_default(),
112                    params,
113                ),
114                ..Default::default()
115            },
116            #[cfg(feature = "optimism")]
117            Self::Optimism { fallback, .. } => {
118                let inherited = optimism::OptimismBaseFeeRules::decode(header.extra_data());
119                ParentHeaderFees {
120                    base_fee: inherited.map_or_else(
121                        || {
122                            calc_next_block_base_fee(
123                                header.gas_used(),
124                                header.gas_limit(),
125                                header.base_fee_per_gas().unwrap_or_default(),
126                                fallback,
127                            )
128                        },
129                        |rules| rules.next_block_base_fee(header),
130                    ),
131                    extra_data: inherited
132                        .map_or_else(Bytes::new, optimism::OptimismBaseFeeRules::extra_data),
133                    optimism_jovian: inherited.map(optimism::OptimismBaseFeeRules::is_jovian),
134                }
135            }
136        }
137    }
138}
139
140#[derive(Clone, Debug, Default)]
141pub(crate) struct ParentHeaderFees {
142    /// Base fee inherited by the child block.
143    pub(crate) base_fee: u64,
144    /// Dynamic fee parameters inherited by the child block.
145    pub(crate) extra_data: Bytes,
146    /// Whether the decoded Optimism fee parameters activate Jovian.
147    pub(crate) optimism_jovian: Option<bool>,
148}
149
150#[derive(Clone, Copy, Debug)]
151struct FeeState {
152    rules: FeeRules,
153    blob_params: BlobParams,
154    /// Base fee for the next block.
155    base_fee: u64,
156    /// Excess blob gas and price for the next block.
157    blob_excess_gas_and_price: BlobExcessGasAndPrice,
158    /// Legacy gas price.
159    gas_price: u128,
160}
161
162/// Chain-derived fee state for the next block.
163#[derive(Clone, Copy, Debug)]
164pub(crate) struct FeeSnapshot {
165    base_fee: u64,
166    blob_excess_gas_and_price: BlobExcessGasAndPrice,
167}
168
169impl FeeManager {
170    #[allow(clippy::too_many_arguments)]
171    pub fn new(
172        spec_id: SpecId,
173        base_fee: u64,
174        is_min_priority_fee_enforced: bool,
175        gas_price: u128,
176        blob_excess_gas_and_price: BlobExcessGasAndPrice,
177        blob_params: BlobParams,
178        base_fee_params: BaseFeeParams,
179        tempo_hardfork: Option<TempoHardfork>,
180    ) -> Self {
181        Self {
182            state: Arc::new(RwLock::new(FeeState {
183                rules: FeeRules {
184                    spec_id,
185                    base_fee: BaseFeeRules::Standard(base_fee_params),
186                    tempo_hardfork,
187                },
188                blob_params,
189                base_fee,
190                blob_excess_gas_and_price,
191                gas_price,
192            })),
193            is_min_priority_fee_enforced,
194        }
195    }
196
197    /// Creates an independent copy suitable for staging a fork reset.
198    pub(crate) fn detached(&self) -> Self {
199        Self {
200            state: Arc::new(RwLock::new(*self.state.read())),
201            is_min_priority_fee_enforced: self.is_min_priority_fee_enforced,
202        }
203    }
204
205    /// Replaces all mutable fee state with a staged manager's values.
206    pub(crate) fn replace_from(&self, other: &Self) {
207        *self.state.write() = *other.state.read();
208    }
209
210    /// Captures the chain-derived fee state for the next block.
211    pub(crate) fn snapshot(&self) -> FeeSnapshot {
212        let state = self.state.read();
213        FeeSnapshot {
214            base_fee: state.base_fee,
215            blob_excess_gas_and_price: state.blob_excess_gas_and_price,
216        }
217    }
218
219    /// Restores the chain-derived fee state for the next block.
220    pub(crate) fn restore(&self, snapshot: FeeSnapshot) {
221        let mut state = self.state.write();
222        state.base_fee = snapshot.base_fee;
223        state.blob_excess_gas_and_price = snapshot.blob_excess_gas_and_price;
224    }
225
226    /// Returns the active Tempo hardfork, if running a Tempo chain.
227    pub fn tempo_hardfork(&self) -> Option<TempoHardfork> {
228        self.state.read().rules.tempo_hardfork
229    }
230
231    /// Atomically replaces all execution-dependent fee rules.
232    pub fn set_execution_rules(
233        &self,
234        spec_id: SpecId,
235        base_fee_params: BaseFeeParams,
236        tempo_hardfork: Option<TempoHardfork>,
237    ) {
238        self.state.write().rules =
239            FeeRules { spec_id, base_fee: BaseFeeRules::Standard(base_fee_params), tempo_hardfork };
240    }
241
242    /// Applies the dynamic EIP-1559 parameters encoded in an Optimism-family parent header.
243    #[cfg(feature = "optimism")]
244    pub(crate) fn set_optimism_base_fee_rules(&self, extra_data: &[u8]) {
245        let mut state = self.state.write();
246        let fallback = match state.rules.base_fee {
247            BaseFeeRules::Standard(params) | BaseFeeRules::Optimism { fallback: params, .. } => {
248                params
249            }
250        };
251        state.rules.base_fee = BaseFeeRules::Optimism {
252            inherited: optimism::OptimismBaseFeeRules::decode(extra_data),
253            fallback,
254        };
255    }
256
257    /// Initializes Optimism-family fee rules for a node that is not inheriting a fork header.
258    #[cfg(feature = "optimism")]
259    pub(crate) fn set_optimism_hardfork(&self, hardfork: FoundryHardfork) {
260        let mut state = self.state.write();
261        let fallback = state.rules.base_fee.params();
262        state.rules.base_fee = BaseFeeRules::Optimism {
263            inherited: optimism::OptimismBaseFeeRules::for_hardfork(hardfork, fallback),
264            fallback,
265        };
266    }
267
268    /// Returns the Optimism-family EIP-1559 parameters inherited by locally built blocks.
269    pub(crate) fn base_fee_extra_data(&self) -> Bytes {
270        self.state.read().rules.base_fee.extra_data()
271    }
272
273    pub fn elasticity(&self) -> f64 {
274        1f64 / self.state.read().rules.base_fee.params().elasticity_multiplier as f64
275    }
276
277    /// Returns true for post London
278    pub fn is_eip1559(&self) -> bool {
279        (self.state.read().rules.spec_id as u8) >= (SpecId::LONDON as u8)
280    }
281
282    pub fn is_eip4844(&self) -> bool {
283        (self.state.read().rules.spec_id as u8) >= (SpecId::CANCUN as u8)
284    }
285
286    /// Calculates the current blob gas price
287    pub fn blob_gas_price(&self) -> u128 {
288        let state = self.state.read();
289        if (state.rules.spec_id as u8) >= (SpecId::CANCUN as u8) {
290            state.blob_excess_gas_and_price.blob_gasprice
291        } else {
292            0
293        }
294    }
295
296    pub fn base_fee(&self) -> u64 {
297        let state = self.state.read();
298        if (state.rules.spec_id as u8) >= (SpecId::LONDON as u8) { state.base_fee } else { 0 }
299    }
300
301    pub const fn is_min_priority_fee_enforced(&self) -> bool {
302        self.is_min_priority_fee_enforced
303    }
304
305    /// Raw base gas price
306    pub fn raw_gas_price(&self) -> u128 {
307        self.state.read().gas_price
308    }
309
310    pub fn excess_blob_gas_and_price(&self) -> Option<BlobExcessGasAndPrice> {
311        let state = self.state.read();
312        ((state.rules.spec_id as u8) >= (SpecId::CANCUN as u8))
313            .then_some(state.blob_excess_gas_and_price)
314    }
315
316    pub fn base_fee_per_blob_gas(&self) -> u128 {
317        let state = self.state.read();
318        if (state.rules.spec_id as u8) >= (SpecId::CANCUN as u8) {
319            state.blob_excess_gas_and_price.blob_gasprice
320        } else {
321            0
322        }
323    }
324
325    /// Returns the current gas price
326    pub fn set_gas_price(&self, price: u128) {
327        self.state.write().gas_price = price;
328    }
329
330    /// Returns the current base fee
331    pub fn set_base_fee(&self, fee: u64) {
332        trace!(target: "backend::fees", "updated base fee {:?}", fee);
333        self.state.write().base_fee = fee;
334    }
335
336    /// Sets the current blob excess gas and price
337    pub fn set_blob_excess_gas_and_price(&self, blob_excess_gas_and_price: BlobExcessGasAndPrice) {
338        trace!(target: "backend::fees", "updated blob base fee {:?}", blob_excess_gas_and_price);
339        self.state.write().blob_excess_gas_and_price = blob_excess_gas_and_price;
340    }
341
342    /// Calculates the base fee for the next block
343    pub fn get_next_block_base_fee_per_gas(
344        &self,
345        gas_used: u64,
346        gas_limit: u64,
347        last_fee_per_gas: u64,
348    ) -> u64 {
349        let state = self.state.read();
350        // It's naturally impossible for base fee to be 0;
351        // It means it was set by the user deliberately and therefore we treat it as a constant.
352        // Therefore, we skip the base fee calculation altogether and we return 0.
353        if (state.rules.spec_id as u8) < (SpecId::LONDON as u8) || state.base_fee == 0 {
354            return 0;
355        }
356        calculate_next_block_base_fee_per_gas(state.rules, gas_used, gas_limit, last_fee_per_gas)
357    }
358
359    /// Calculates the next block base fee from the parent block without applying the configured
360    /// zero-fee sentinel.
361    #[cfg(test)]
362    pub(crate) fn calculate_next_block_base_fee_per_gas(
363        &self,
364        gas_used: u64,
365        gas_limit: u64,
366        last_fee_per_gas: u64,
367    ) -> u64 {
368        let rules = self.state.read().rules;
369        if (rules.spec_id as u8) < (SpecId::LONDON as u8) {
370            return 0;
371        }
372        calculate_next_block_base_fee_per_gas(rules, gas_used, gas_limit, last_fee_per_gas)
373    }
374
375    /// Calculates the next block base fee from a complete parent header.
376    pub(crate) fn get_next_block_base_fee_from_header<H: BlockHeader>(&self, header: &H) -> u64 {
377        let state = self.state.read();
378        if (state.rules.spec_id as u8) < (SpecId::LONDON as u8) || state.base_fee == 0 {
379            return 0;
380        }
381        calculate_parent_header_fees(state.rules, header).base_fee
382    }
383
384    /// Returns all fee metadata inherited from a parent header, honoring the configured zero-fee
385    /// sentinel.
386    pub(crate) fn get_parent_header_fees<H: BlockHeader>(&self, header: &H) -> ParentHeaderFees {
387        let state = self.state.read();
388        let mut fees = calculate_parent_header_fees(state.rules, header);
389        if (state.rules.spec_id as u8) < (SpecId::LONDON as u8) || state.base_fee == 0 {
390            fees.base_fee = 0;
391        }
392        fees
393    }
394
395    /// Calculates the next block base fee from a complete parent header without applying the
396    /// configured zero-fee sentinel.
397    pub(crate) fn calculate_next_block_base_fee_from_header<H: BlockHeader>(
398        &self,
399        header: &H,
400    ) -> u64 {
401        let rules = self.state.read().rules;
402        if (rules.spec_id as u8) < (SpecId::LONDON as u8) {
403            return 0;
404        }
405        calculate_parent_header_fees(rules, header).base_fee
406    }
407
408    /// Returns all fee metadata inherited from a parent header without applying the configured
409    /// zero-fee sentinel.
410    pub(crate) fn calculate_parent_header_fees<H: BlockHeader>(
411        &self,
412        header: &H,
413    ) -> ParentHeaderFees {
414        let rules = self.state.read().rules;
415        let mut fees = calculate_parent_header_fees(rules, header);
416        if (rules.spec_id as u8) < (SpecId::LONDON as u8) {
417            fees.base_fee = 0;
418        }
419        fees
420    }
421
422    /// Calculates the next block blob base fee.
423    pub fn get_next_block_blob_base_fee_per_gas(&self) -> u128 {
424        let state = self.state.read();
425        state.blob_params.calc_blob_fee(state.blob_excess_gas_and_price.excess_blob_gas)
426    }
427
428    /// Configures the blob params
429    pub fn set_blob_params(&self, blob_params: BlobParams) {
430        self.state.write().blob_params = blob_params;
431    }
432
433    /// Returns the active [`BlobParams`]
434    pub fn blob_params(&self) -> BlobParams {
435        self.state.read().blob_params
436    }
437}
438
439fn calculate_next_block_base_fee_per_gas(
440    rules: FeeRules,
441    gas_used: u64,
442    gas_limit: u64,
443    last_fee_per_gas: u64,
444) -> u64 {
445    // Tempo replaces EIP-1559 with its own hardfork-specific base fee rules.
446    if let Some(hardfork) = rules.tempo_hardfork {
447        return tempo_next_block_base_fee(hardfork, gas_used, last_fee_per_gas);
448    }
449    calc_next_block_base_fee(gas_used, gas_limit, last_fee_per_gas, rules.base_fee.params())
450}
451
452fn calculate_parent_header_fees<H: BlockHeader>(rules: FeeRules, header: &H) -> ParentHeaderFees {
453    if let Some(hardfork) = rules.tempo_hardfork {
454        return ParentHeaderFees {
455            base_fee: tempo_next_block_base_fee(
456                hardfork,
457                header.gas_used(),
458                header.base_fee_per_gas().unwrap_or_default(),
459            ),
460            ..Default::default()
461        };
462    }
463    rules.base_fee.parent_header_fees(header)
464}
465
466/// Computes the next block's base fee for a Tempo chain.
467///
468/// - T7+: the TIP-1067 dynamic controller, an EIP-1559 update against a fixed 10M gas target
469///   clamped to `[floor, cap]`.
470/// - Pre-T7: the fixed hardfork base fee (10 gwei pre-T1, 20 gwei T1+).
471fn tempo_next_block_base_fee(hardfork: TempoHardfork, gas_used: u64, parent_base_fee: u64) -> u64 {
472    if hardfork.is_t7() {
473        return tempo_t7_next_block_base_fee(parent_base_fee, gas_used);
474    }
475    crate::config::tempo_default_base_fee(hardfork)
476}
477
478/// An async service that takes care of the `FeeHistory` cache
479pub struct FeeHistoryService<N: Network>
480where
481    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
482{
483    /// Live fee rules, including blob parameters replaced by fork resets.
484    fees: FeeManager,
485    /// incoming notifications about new blocks
486    new_blocks: ChainNotifications,
487    /// contains all fee history related entries
488    cache: FeeHistoryCache,
489    /// number of items to consider
490    fee_history_limit: u64,
491    /// a type that can fetch ethereum-storage data
492    storage_info: StorageInfo<N>,
493}
494
495impl<N: Network> FeeHistoryService<N>
496where
497    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
498{
499    pub const fn new(
500        fees: FeeManager,
501        new_blocks: ChainNotifications,
502        cache: FeeHistoryCache,
503        storage_info: StorageInfo<N>,
504    ) -> Self {
505        Self {
506            fees,
507            new_blocks,
508            cache,
509            fee_history_limit: MAX_FEE_HISTORY_CACHE_SIZE,
510            storage_info,
511        }
512    }
513
514    /// Returns the configured history limit
515    pub const fn fee_history_limit(&self) -> u64 {
516        self.fee_history_limit
517    }
518
519    /// Inserts a new cache entry for the given block
520    pub(crate) fn insert_cache_entry_for_block(&self, hash: B256, header: &impl BlockHeader) {
521        let (result, block_number) = self.create_cache_entry(hash, header);
522        self.insert_cache_entry(result, block_number);
523    }
524
525    /// Create a new history entry for the block
526    fn create_cache_entry(
527        &self,
528        hash: B256,
529        header: &impl BlockHeader,
530    ) -> (FeeHistoryCacheItem, Option<u64>) {
531        create_fee_history_cache_item(hash, header, &self.storage_info, self.fees.blob_params())
532    }
533
534    fn insert_cache_entry(&self, item: FeeHistoryCacheItem, block_number: Option<u64>) {
535        insert_fee_history_cache_item(&self.cache, item, block_number, self.fee_history_limit);
536    }
537}
538
539/// Inserts an entry into the fee history cache and trims it back to `fee_history_limit`.
540///
541/// Used by the async [`FeeHistoryService`]. The `eth_feeHistory` fallback applies the same bounded
542/// insertion policy to a batch under one lock.
543pub(crate) fn insert_fee_history_cache_item(
544    cache: &FeeHistoryCache,
545    item: FeeHistoryCacheItem,
546    block_number: Option<u64>,
547    fee_history_limit: u64,
548) {
549    if let Some(block_number) = block_number {
550        trace!(target: "fees", "insert new history item={:?} for {}", item, block_number);
551        let mut cache = cache.lock();
552        cache.insert(block_number, item);
553
554        // Trim to the cache limit by dropping the oldest entries (smallest block numbers).
555        // `pop_first` is saturating and correct regardless of insertion order, unlike the
556        // previous index math which could underflow when the `eth_feeHistory` fallback inserts
557        // entries out of order.
558        while cache.len() as u64 > fee_history_limit {
559            cache.pop_first();
560        }
561    }
562}
563
564/// Calculates percentile rewards from transactions sorted by effective reward.
565///
566/// [`REWARD_PERCENTILES`] must remain ascending because the transaction cursor never rewinds.
567fn reward_percentiles(transactions: &[(u64, u128)], block_gas_used: f64) -> Vec<u128> {
568    let mut rewards = Vec::with_capacity(REWARD_PERCENTILES.len());
569    let mut transactions = transactions.iter().copied();
570    let Some((mut cumulative_gas, mut current_reward)) = transactions.next() else {
571        return rewards;
572    };
573
574    for &percentile in REWARD_PERCENTILES.iter() {
575        let target_gas = (percentile * block_gas_used / 100f64) as u64;
576        while target_gas > cumulative_gas {
577            let Some((tx_gas_used, effective_reward)) = transactions.next() else { return rewards };
578            cumulative_gas += tx_gas_used;
579            current_reward = effective_reward;
580        }
581        rewards.push(current_reward);
582    }
583
584    rewards
585}
586
587/// Builds the [`FeeHistoryCacheItem`] for a single block.
588///
589/// Shared by the async [`FeeHistoryService`] and by `eth_feeHistory` itself: the service can lag
590/// the chain head (it only runs when the node task is polled), so the RPC handler computes any
591/// missing entry on demand with the same logic instead of returning a short response.
592pub(crate) fn create_fee_history_cache_item<N: Network>(
593    hash: B256,
594    header: &impl BlockHeader,
595    storage_info: &StorageInfo<N>,
596    blob_params: BlobParams,
597) -> (FeeHistoryCacheItem, Option<u64>)
598where
599    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
600{
601    let mut block_number: Option<u64> = None;
602    let base_fee = header.base_fee_per_gas().unwrap_or_default();
603    let excess_blob_gas = header.excess_blob_gas().map(|g| g as u128);
604    let blob_gas_used = header.blob_gas_used().map(|g| g as u128);
605    let base_fee_per_blob_gas = header.blob_fee(blob_params);
606
607    let mut item = FeeHistoryCacheItem {
608        block_hash: hash,
609        base_fee: base_fee as u128,
610        gas_used_ratio: 0f64,
611        blob_gas_used_ratio: 0f64,
612        rewards: Vec::new(),
613        excess_blob_gas,
614        base_fee_per_blob_gas,
615        blob_gas_used,
616    };
617
618    let current_block = storage_info.block(hash);
619    let current_receipts = storage_info.receipts(hash);
620
621    if let (Some(block), Some(receipts)) = (current_block, current_receipts) {
622        block_number = Some(block.header.number());
623
624        let gas_used = block.header.gas_used() as f64;
625        let blob_gas_used = block.header.blob_gas_used().map(|g| g as f64);
626        item.gas_used_ratio = gas_used / block.header.gas_limit() as f64;
627        item.blob_gas_used_ratio = blob_gas_used
628            .map(|g| {
629                let max = blob_params.max_blob_gas_per_block() as f64;
630                if max == 0.0 { 0.0 } else { g / max }
631            })
632            .unwrap_or(0.0);
633
634        // extract useful tx info (gas_used, effective_reward)
635        let mut transactions: Vec<(_, _)> = receipts
636            .iter()
637            .enumerate()
638            .map(|(i, receipt)| {
639                let cumulative = receipt.cumulative_gas_used();
640                let prev_cumulative = if i > 0 { receipts[i - 1].cumulative_gas_used() } else { 0 };
641                let gas_used = cumulative - prev_cumulative;
642                let effective_reward = block
643                    .body
644                    .transactions
645                    .get(i)
646                    .map(|tx| tx.as_ref().effective_tip_per_gas(base_fee).unwrap_or(0))
647                    .unwrap_or(0);
648
649                (gas_used, effective_reward)
650            })
651            .collect();
652
653        // sort by effective reward asc
654        transactions.sort_by_key(|(_, reward)| *reward);
655
656        item.rewards = reward_percentiles(&transactions, gas_used);
657    } else {
658        item.rewards = vec![0; REWARD_PERCENTILES.len()];
659    }
660    (item, block_number)
661}
662
663// An endless future that listens for new blocks and updates the cache
664impl<N: Network> Future for FeeHistoryService<N>
665where
666    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
667{
668    type Output = ();
669
670    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
671        let pin = self.get_mut();
672
673        while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) {
674            // add the imported block.
675            if let Some(block) = notification.as_new_block() {
676                pin.insert_cache_entry_for_block(block.hash, block.header.as_ref());
677            }
678        }
679
680        Poll::Pending
681    }
682}
683
684pub type FeeHistoryCache = Arc<Mutex<BTreeMap<u64, FeeHistoryCacheItem>>>;
685
686/// A single item in the whole fee history cache
687#[derive(Clone, Debug)]
688pub struct FeeHistoryCacheItem {
689    pub block_hash: B256,
690    pub base_fee: u128,
691    pub gas_used_ratio: f64,
692    pub base_fee_per_blob_gas: Option<u128>,
693    pub blob_gas_used_ratio: f64,
694    pub excess_blob_gas: Option<u128>,
695    pub blob_gas_used: Option<u128>,
696    pub rewards: Vec<u128>,
697}
698
699#[derive(Clone, Default)]
700pub struct FeeDetails {
701    pub gas_price: Option<u128>,
702    pub max_fee_per_gas: Option<u128>,
703    pub max_priority_fee_per_gas: Option<u128>,
704    pub max_fee_per_blob_gas: Option<u128>,
705}
706
707impl FeeDetails {
708    /// All values zero
709    pub const fn zero() -> Self {
710        Self {
711            gas_price: Some(0),
712            max_fee_per_gas: Some(0),
713            max_priority_fee_per_gas: Some(0),
714            max_fee_per_blob_gas: None,
715        }
716    }
717
718    /// If neither `gas_price` nor `max_fee_per_gas` is `Some`, this will set both to `0`
719    pub const fn or_zero_fees(self) -> Self {
720        let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
721            self;
722
723        let no_fees = gas_price.is_none() && max_fee_per_gas.is_none();
724        let gas_price = if no_fees { Some(0) } else { gas_price };
725        let max_fee_per_gas = if no_fees { Some(0) } else { max_fee_per_gas };
726        let max_fee_per_blob_gas = if no_fees { None } else { max_fee_per_blob_gas };
727
728        Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas }
729    }
730
731    /// Turns this type into a tuple
732    pub const fn split(self) -> (Option<u128>, Option<u128>, Option<u128>, Option<u128>) {
733        let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
734            self;
735        (gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
736    }
737
738    /// Creates a new instance from the request's gas related values
739    pub fn new(
740        request_gas_price: Option<u128>,
741        request_max_fee: Option<u128>,
742        request_priority: Option<u128>,
743        max_fee_per_blob_gas: Option<u128>,
744    ) -> Result<Self, BlockchainError> {
745        match (request_gas_price, request_max_fee, request_priority, max_fee_per_blob_gas) {
746            (gas_price, None, None, None) => {
747                // Legacy request, all default to gas price.
748                Ok(Self {
749                    gas_price,
750                    max_fee_per_gas: gas_price,
751                    max_priority_fee_per_gas: gas_price,
752                    max_fee_per_blob_gas: None,
753                })
754            }
755            (_, max_fee, max_priority, max_fee_per_blob_gas) => {
756                // eip-1559
757                // Ensure `max_priority_fee_per_gas` is less or equal to `max_fee_per_gas`.
758                if let Some(max_priority) = max_priority {
759                    let max_fee = max_fee.unwrap_or_default();
760                    if max_priority > max_fee {
761                        return Err(BlockchainError::InvalidFeeInput);
762                    }
763                }
764                Ok(Self {
765                    gas_price: max_fee,
766                    max_fee_per_gas: max_fee,
767                    max_priority_fee_per_gas: max_priority,
768                    max_fee_per_blob_gas,
769                })
770            }
771        }
772    }
773}
774
775impl fmt::Debug for FeeDetails {
776    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
777        write!(fmt, "Fees {{ ")?;
778        write!(fmt, "gas_price: {:?}, ", self.gas_price)?;
779        write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?;
780        write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?;
781        write!(fmt, "}}")?;
782        Ok(())
783    }
784}
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789
790    fn reward_percentiles_reference(
791        transactions: &[(u64, u128)],
792        block_gas_used: f64,
793    ) -> Vec<u128> {
794        REWARD_PERCENTILES
795            .iter()
796            .filter_map(|&percentile| {
797                let target_gas = (percentile * block_gas_used / 100f64) as u64;
798                let mut cumulative_gas = 0;
799                for (tx_gas_used, effective_reward) in transactions.iter().copied() {
800                    cumulative_gas += tx_gas_used;
801                    if target_gas <= cumulative_gas {
802                        return Some(effective_reward);
803                    }
804                }
805                None
806            })
807            .collect()
808    }
809
810    fn assert_reward_percentiles_match(transactions: &mut [(u64, u128)], gas_used: u64) {
811        transactions.sort_by_key(|(_, reward)| *reward);
812        assert_eq!(
813            reward_percentiles(transactions, gas_used as f64),
814            reward_percentiles_reference(transactions, gas_used as f64)
815        );
816    }
817
818    fn fee_manager(spec_id: SpecId) -> FeeManager {
819        FeeManager::new(
820            spec_id,
821            INITIAL_BASE_FEE,
822            true,
823            INITIAL_GAS_PRICE,
824            BlobExcessGasAndPrice::new_with_spec(0, SpecId::CANCUN),
825            BlobParams::cancun(),
826            BaseFeeParams::ethereum(),
827            None,
828        )
829    }
830
831    #[test]
832    fn raw_next_base_fee_respects_london_activation() {
833        let berlin = fee_manager(SpecId::BERLIN);
834        assert_eq!(
835            berlin.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
836            0
837        );
838
839        let london = fee_manager(SpecId::LONDON);
840        assert_ne!(
841            london.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
842            0
843        );
844    }
845
846    #[cfg(feature = "optimism")]
847    #[test]
848    fn pre_london_parent_fees_preserve_optimism_metadata() {
849        let fees = fee_manager(SpecId::BERLIN);
850        let jovian = [1, 0, 0, 0, 250, 0, 0, 0, 2, 0, 0, 0, 0, 0, 76, 75, 64];
851        fees.set_optimism_base_fee_rules(&jovian);
852        let header = alloy_consensus::Header {
853            extra_data: Bytes::copy_from_slice(&jovian),
854            ..Default::default()
855        };
856
857        let parent_fees = fees.get_parent_header_fees(&header);
858        assert_eq!(parent_fees.base_fee, 0);
859        assert_eq!(parent_fees.extra_data.as_ref(), jovian);
860        assert_eq!(parent_fees.optimism_jovian, Some(true));
861    }
862
863    #[test]
864    fn reward_percentile_sweep_preserves_boundaries_and_empty_results() {
865        assert_reward_percentiles_match(&mut [], 0);
866
867        let mut transactions = [(5, 1), (0, 2), (5, 3)];
868        assert_reward_percentiles_match(&mut transactions, 1_000);
869        assert_eq!(reward_percentiles(&transactions, 1_000f64), [1, 1, 3]);
870
871        let mut transactions = [(0, 10), (1, 20)];
872        assert_reward_percentiles_match(&mut transactions, 1);
873        let rewards = reward_percentiles(&transactions, 1f64);
874        assert_eq!(&rewards[..200], &[10; 200]);
875        assert_eq!(rewards[200], 20);
876    }
877
878    #[test]
879    fn reward_percentile_sweep_matches_reference_for_randomized_inputs() {
880        let mut state = 0x4d59_5df4_d0f3_3173u64;
881        for _ in 0..2_000 {
882            let len = (next_random(&mut state) % 129) as usize;
883            let mut transactions = (0..len)
884                .map(|_| {
885                    let gas_used = next_random(&mut state) % 100;
886                    let effective_reward = (next_random(&mut state) % 16) as u128;
887                    (gas_used, effective_reward)
888                })
889                .collect::<Vec<_>>();
890            let total_gas = transactions.iter().map(|(gas_used, _)| gas_used).sum::<u64>();
891            let header_gas_used = match next_random(&mut state) % 4 {
892                0 => total_gas,
893                1 => next_random(&mut state) % (total_gas.saturating_add(1)),
894                2 => total_gas.saturating_add(next_random(&mut state) % 1_000),
895                _ => 0,
896            };
897
898            assert_reward_percentiles_match(&mut transactions, header_gas_used);
899        }
900    }
901
902    fn next_random(state: &mut u64) -> u64 {
903        *state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
904        *state
905    }
906}