Skip to main content

anvil/eth/
fees.rs

1use crate::eth::{
2    backend::{info::StorageInfo, notifications::ChainNotifications},
3    error::BlockchainError,
4};
5use alloy_consensus::{BlockHeader, Transaction, TxReceipt};
6use alloy_eips::{calc_next_block_base_fee, eip1559::BaseFeeParams, eip7840::BlobParams};
7use alloy_network::Network;
8use alloy_primitives::{B256, Bytes};
9use futures::StreamExt;
10use parking_lot::{Mutex, RwLock};
11use revm::{context_interface::block::BlobExcessGasAndPrice, primitives::hardfork::SpecId};
12use std::{
13    collections::BTreeMap,
14    fmt,
15    pin::Pin,
16    sync::{Arc, LazyLock},
17    task::{Context, Poll},
18};
19use tempo_hardfork::{TempoHardfork, constants::gas::tempo_t7_next_block_base_fee};
20
21#[cfg(feature = "optimism")]
22use foundry_evm::hardfork::OpHardfork;
23
24#[cfg(feature = "optimism")]
25mod optimism;
26
27/// Maximum number of entries in the fee history cache
28pub const MAX_FEE_HISTORY_CACHE_SIZE: u64 = 2048u64;
29
30/// Number of cached reward samples per percentile.
31pub(crate) const REWARD_PERCENTILE_RESOLUTION: f64 = 2.0;
32
33/// Percentile list from 0.0 to 100.0 with a 0.5 resolution (201 points).
34///
35/// Constant across blocks, so it is computed once instead of being rebuilt on every
36/// `create_fee_history_cache_item` call.
37static REWARD_PERCENTILES: LazyLock<Vec<f64>> =
38    LazyLock::new(|| (0..=200).map(|index| index as f64 / REWARD_PERCENTILE_RESOLUTION).collect());
39
40/// Initial base fee for EIP-1559 blocks.
41pub const INITIAL_BASE_FEE: u64 = 1_000_000_000;
42
43/// Initial default gas price for the first block
44pub const INITIAL_GAS_PRICE: u128 = 1_875_000_000;
45
46/// Bounds the amount the base fee can change between blocks.
47pub const BASE_FEE_CHANGE_DENOMINATOR: u128 = 8;
48
49/// Minimum suggested priority fee
50pub const MIN_SUGGESTED_PRIORITY_FEE: u128 = 1e9 as u128;
51
52/// Stores the fee related information
53#[derive(Clone, Debug)]
54pub struct FeeManager {
55    /// Fee state published as one coherent execution context.
56    state: Arc<RwLock<FeeState>>,
57    /// Whether the minimum suggested priority fee is enforced
58    is_min_priority_fee_enforced: bool,
59}
60
61#[derive(Clone, Copy, Debug)]
62struct FeeRules {
63    spec_id: SpecId,
64    base_fee: BaseFeeRules,
65    /// The active Tempo hardfork, set only when running a Tempo chain.
66    tempo_hardfork: Option<TempoHardfork>,
67}
68
69#[derive(Clone, Copy, Debug)]
70enum BaseFeeRules {
71    Standard(BaseFeeParams),
72    #[cfg(feature = "optimism")]
73    Optimism {
74        inherited: Option<optimism::OptimismBaseFeeRules>,
75        fallback: BaseFeeParams,
76    },
77}
78
79impl BaseFeeRules {
80    const fn params(self) -> BaseFeeParams {
81        match self {
82            Self::Standard(params) => params,
83            #[cfg(feature = "optimism")]
84            Self::Optimism { inherited, fallback } => {
85                if let Some(rules) = inherited {
86                    rules.params()
87                } else {
88                    fallback
89                }
90            }
91        }
92    }
93
94    #[allow(clippy::missing_const_for_fn)] // Optimism extra-data encoding is not const.
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    pub(crate) base_fee: u64,
166    pub(crate) 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    /// Atomically publishes the chain-derived fee state for the next block.
227    pub(crate) fn set_next_block_fees(
228        &self,
229        base_fee: u64,
230        blob_excess_gas_and_price: BlobExcessGasAndPrice,
231    ) {
232        trace!(
233            target: "backend::fees",
234            ?base_fee,
235            ?blob_excess_gas_and_price,
236            "updated next block fees"
237        );
238        let mut state = self.state.write();
239        state.base_fee = base_fee;
240        state.blob_excess_gas_and_price = blob_excess_gas_and_price;
241    }
242
243    /// Returns the active Tempo hardfork, if running a Tempo chain.
244    pub fn tempo_hardfork(&self) -> Option<TempoHardfork> {
245        self.state.read().rules.tempo_hardfork
246    }
247
248    /// Atomically replaces all execution-dependent fee rules.
249    pub fn set_execution_rules(
250        &self,
251        spec_id: SpecId,
252        base_fee_params: BaseFeeParams,
253        tempo_hardfork: Option<TempoHardfork>,
254    ) {
255        self.state.write().rules =
256            FeeRules { spec_id, base_fee: BaseFeeRules::Standard(base_fee_params), tempo_hardfork };
257    }
258
259    /// Applies the dynamic EIP-1559 parameters encoded in an Optimism-family parent header.
260    #[cfg(feature = "optimism")]
261    pub(crate) fn set_optimism_base_fee_rules(&self, extra_data: &[u8]) {
262        let mut state = self.state.write();
263        let fallback = match state.rules.base_fee {
264            BaseFeeRules::Standard(params) | BaseFeeRules::Optimism { fallback: params, .. } => {
265                params
266            }
267        };
268        state.rules.base_fee = BaseFeeRules::Optimism {
269            inherited: optimism::OptimismBaseFeeRules::decode(extra_data),
270            fallback,
271        };
272    }
273
274    /// Initializes Optimism-family fee rules for a node that is not inheriting a fork header.
275    #[cfg(feature = "optimism")]
276    pub(crate) fn set_optimism_hardfork(&self, hardfork: OpHardfork) {
277        let mut state = self.state.write();
278        let fallback = state.rules.base_fee.params();
279        state.rules.base_fee = BaseFeeRules::Optimism {
280            inherited: optimism::OptimismBaseFeeRules::for_hardfork(hardfork, fallback),
281            fallback,
282        };
283    }
284
285    /// Returns the Optimism-family EIP-1559 parameters inherited by locally built blocks.
286    pub(crate) fn base_fee_extra_data(&self) -> Bytes {
287        self.state.read().rules.base_fee.extra_data()
288    }
289
290    pub fn elasticity(&self) -> f64 {
291        1f64 / self.state.read().rules.base_fee.params().elasticity_multiplier as f64
292    }
293
294    /// Returns true for post London
295    pub fn is_eip1559(&self) -> bool {
296        self.state.read().rules.spec_id.is_enabled_in(SpecId::LONDON)
297    }
298
299    pub fn is_eip4844(&self) -> bool {
300        self.state.read().rules.spec_id.is_enabled_in(SpecId::CANCUN)
301    }
302
303    /// Calculates the current blob gas price
304    pub fn blob_gas_price(&self) -> u128 {
305        let state = self.state.read();
306        if state.rules.spec_id.is_enabled_in(SpecId::CANCUN) {
307            state.blob_excess_gas_and_price.blob_gasprice
308        } else {
309            0
310        }
311    }
312
313    pub fn base_fee(&self) -> u64 {
314        let state = self.state.read();
315        if state.rules.spec_id.is_enabled_in(SpecId::LONDON) { state.base_fee } else { 0 }
316    }
317
318    pub const fn is_min_priority_fee_enforced(&self) -> bool {
319        self.is_min_priority_fee_enforced
320    }
321
322    /// Raw base gas price
323    pub fn raw_gas_price(&self) -> u128 {
324        self.state.read().gas_price
325    }
326
327    pub fn excess_blob_gas_and_price(&self) -> Option<BlobExcessGasAndPrice> {
328        let state = self.state.read();
329        state.rules.spec_id.is_enabled_in(SpecId::CANCUN).then_some(state.blob_excess_gas_and_price)
330    }
331
332    pub fn base_fee_per_blob_gas(&self) -> u128 {
333        let state = self.state.read();
334        if state.rules.spec_id.is_enabled_in(SpecId::CANCUN) {
335            state.blob_excess_gas_and_price.blob_gasprice
336        } else {
337            0
338        }
339    }
340
341    /// Returns the current gas price
342    pub fn set_gas_price(&self, price: u128) {
343        self.state.write().gas_price = price;
344    }
345
346    /// Returns the current base fee
347    pub fn set_base_fee(&self, fee: u64) {
348        trace!(target: "backend::fees", "updated base fee {:?}", fee);
349        self.state.write().base_fee = fee;
350    }
351
352    /// Sets the current blob excess gas and price
353    pub fn set_blob_excess_gas_and_price(&self, blob_excess_gas_and_price: BlobExcessGasAndPrice) {
354        trace!(target: "backend::fees", "updated blob base fee {:?}", blob_excess_gas_and_price);
355        self.state.write().blob_excess_gas_and_price = blob_excess_gas_and_price;
356    }
357
358    /// Calculates the base fee for the next block
359    pub fn get_next_block_base_fee_per_gas(
360        &self,
361        gas_used: u64,
362        gas_limit: u64,
363        last_fee_per_gas: u64,
364    ) -> u64 {
365        let state = self.state.read();
366        // It's naturally impossible for base fee to be 0;
367        // It means it was set by the user deliberately and therefore we treat it as a constant.
368        // Therefore, we skip the base fee calculation altogether and we return 0.
369        if !state.rules.spec_id.is_enabled_in(SpecId::LONDON) || state.base_fee == 0 {
370            return 0;
371        }
372        calculate_next_block_base_fee_per_gas(state.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.is_enabled_in(SpecId::LONDON) || 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.is_enabled_in(SpecId::LONDON) || 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.is_enabled_in(SpecId::LONDON) {
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.is_enabled_in(SpecId::LONDON) {
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        let gas_limit = block.header.gas_limit() as f64;
627        item.gas_used_ratio = if gas_limit == 0.0 { 0.0 } else { gas_used / gas_limit };
628        item.blob_gas_used_ratio = blob_gas_used
629            .map(|g| {
630                let max = blob_params.max_blob_gas_per_block() as f64;
631                if max == 0.0 { 0.0 } else { g / max }
632            })
633            .unwrap_or(0.0);
634
635        // extract useful tx info (gas_used, effective_reward)
636        let mut transactions: Vec<(_, _)> = receipts
637            .iter()
638            .enumerate()
639            .map(|(i, receipt)| {
640                let cumulative = receipt.cumulative_gas_used();
641                let prev_cumulative = if i > 0 { receipts[i - 1].cumulative_gas_used() } else { 0 };
642                let gas_used = cumulative - prev_cumulative;
643                let effective_reward = block
644                    .body
645                    .transactions
646                    .get(i)
647                    .map(|tx| tx.as_ref().effective_tip_per_gas(base_fee).unwrap_or(0))
648                    .unwrap_or(0);
649
650                (gas_used, effective_reward)
651            })
652            .collect();
653
654        // sort by effective reward asc
655        transactions.sort_by_key(|(_, reward)| *reward);
656
657        item.rewards = reward_percentiles(&transactions, gas_used);
658    } else {
659        item.rewards = vec![0; REWARD_PERCENTILES.len()];
660    }
661    (item, block_number)
662}
663
664// An endless future that listens for new blocks and updates the cache
665impl<N: Network> Future for FeeHistoryService<N>
666where
667    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
668{
669    type Output = ();
670
671    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
672        let pin = self.get_mut();
673
674        while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) {
675            // add the imported block.
676            if let Some(block) = notification.as_new_block() {
677                pin.insert_cache_entry_for_block(block.hash, block.header.as_ref());
678            }
679        }
680
681        Poll::Pending
682    }
683}
684
685pub type FeeHistoryCache = Arc<Mutex<BTreeMap<u64, FeeHistoryCacheItem>>>;
686
687/// A single item in the whole fee history cache
688#[derive(Clone, Debug)]
689pub struct FeeHistoryCacheItem {
690    pub block_hash: B256,
691    pub base_fee: u128,
692    pub gas_used_ratio: f64,
693    pub base_fee_per_blob_gas: Option<u128>,
694    pub blob_gas_used_ratio: f64,
695    pub excess_blob_gas: Option<u128>,
696    pub blob_gas_used: Option<u128>,
697    pub rewards: Vec<u128>,
698}
699
700#[derive(Clone, Default)]
701pub struct FeeDetails {
702    pub gas_price: Option<u128>,
703    pub max_fee_per_gas: Option<u128>,
704    pub max_priority_fee_per_gas: Option<u128>,
705    pub max_fee_per_blob_gas: Option<u128>,
706}
707
708impl FeeDetails {
709    /// All values zero
710    pub const fn zero() -> Self {
711        Self {
712            gas_price: Some(0),
713            max_fee_per_gas: Some(0),
714            max_priority_fee_per_gas: Some(0),
715            max_fee_per_blob_gas: None,
716        }
717    }
718
719    /// If neither `gas_price` nor `max_fee_per_gas` is `Some`, this will set both to `0`
720    pub const fn or_zero_fees(self) -> Self {
721        let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
722            self;
723
724        let no_fees = gas_price.is_none() && max_fee_per_gas.is_none();
725        let gas_price = if no_fees { Some(0) } else { gas_price };
726        let max_fee_per_gas = if no_fees { Some(0) } else { max_fee_per_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            (Some(_), Some(_), _, _) | (Some(_), _, Some(_), _) => {
747                Err(BlockchainError::ConflictingFeeFields)
748            }
749            (gas_price, None, None, max_fee_per_blob_gas) => {
750                // Legacy request, all default to gas price. A blob fee cap is priced separately.
751                Ok(Self {
752                    gas_price,
753                    max_fee_per_gas: gas_price,
754                    max_priority_fee_per_gas: gas_price,
755                    max_fee_per_blob_gas,
756                })
757            }
758            (_, max_fee, max_priority, max_fee_per_blob_gas) => {
759                // eip-1559
760                // Ensure `max_priority_fee_per_gas` is less or equal to `max_fee_per_gas`.
761                if let Some(max_priority) = max_priority {
762                    let max_fee = max_fee.unwrap_or_default();
763                    if max_priority > max_fee {
764                        return Err(BlockchainError::InvalidFeeInput);
765                    }
766                }
767                Ok(Self {
768                    gas_price: max_fee,
769                    max_fee_per_gas: max_fee,
770                    // A fee cap without a tip pays the base fee only.
771                    max_priority_fee_per_gas: max_priority.or(max_fee.map(|_| 0)),
772                    max_fee_per_blob_gas,
773                })
774            }
775        }
776    }
777}
778
779impl fmt::Debug for FeeDetails {
780    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
781        write!(fmt, "Fees {{ ")?;
782        write!(fmt, "gas_price: {:?}, ", self.gas_price)?;
783        write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?;
784        write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?;
785        write!(fmt, "}}")?;
786        Ok(())
787    }
788}
789
790#[cfg(test)]
791mod tests {
792    use super::*;
793
794    fn reward_percentiles_reference(
795        transactions: &[(u64, u128)],
796        block_gas_used: f64,
797    ) -> Vec<u128> {
798        REWARD_PERCENTILES
799            .iter()
800            .filter_map(|&percentile| {
801                let target_gas = (percentile * block_gas_used / 100f64) as u64;
802                let mut cumulative_gas = 0;
803                for (tx_gas_used, effective_reward) in transactions.iter().copied() {
804                    cumulative_gas += tx_gas_used;
805                    if target_gas <= cumulative_gas {
806                        return Some(effective_reward);
807                    }
808                }
809                None
810            })
811            .collect()
812    }
813
814    fn assert_reward_percentiles_match(transactions: &mut [(u64, u128)], gas_used: u64) {
815        transactions.sort_by_key(|(_, reward)| *reward);
816        assert_eq!(
817            reward_percentiles(transactions, gas_used as f64),
818            reward_percentiles_reference(transactions, gas_used as f64)
819        );
820    }
821
822    fn fee_manager(spec_id: SpecId) -> FeeManager {
823        FeeManager::new(
824            spec_id,
825            INITIAL_BASE_FEE,
826            true,
827            INITIAL_GAS_PRICE,
828            BlobExcessGasAndPrice::new_with_spec(0, SpecId::CANCUN),
829            BlobParams::cancun(),
830            BaseFeeParams::ethereum(),
831            None,
832        )
833    }
834
835    #[test]
836    fn raw_next_base_fee_respects_london_activation() {
837        let parent = alloy_consensus::Header {
838            gas_used: 30_000_000,
839            gas_limit: 30_000_000,
840            base_fee_per_gas: Some(INITIAL_BASE_FEE),
841            ..Default::default()
842        };
843
844        let berlin = fee_manager(SpecId::BERLIN);
845        assert_eq!(berlin.calculate_next_block_base_fee_from_header(&parent), 0);
846
847        let london = fee_manager(SpecId::LONDON);
848        assert_ne!(london.calculate_next_block_base_fee_from_header(&parent), 0);
849    }
850
851    #[cfg(feature = "optimism")]
852    #[test]
853    fn pre_london_parent_fees_preserve_optimism_metadata() {
854        let fees = fee_manager(SpecId::BERLIN);
855        let jovian = [1, 0, 0, 0, 250, 0, 0, 0, 2, 0, 0, 0, 0, 0, 76, 75, 64];
856        fees.set_optimism_base_fee_rules(&jovian);
857        let header = alloy_consensus::Header { extra_data: jovian.into(), ..Default::default() };
858
859        let parent_fees = fees.get_parent_header_fees(&header);
860        assert_eq!(parent_fees.base_fee, 0);
861        assert_eq!(parent_fees.extra_data.as_ref(), jovian);
862        assert_eq!(parent_fees.optimism_jovian, Some(true));
863    }
864
865    #[test]
866    fn fee_details_default_missing_tip_to_zero() {
867        let fees = FeeDetails::new(None, Some(5), None, None).unwrap();
868        assert_eq!(fees.split(), (Some(5), Some(5), Some(0), None));
869    }
870
871    #[test]
872    fn reward_percentile_sweep_preserves_boundaries_and_empty_results() {
873        assert_reward_percentiles_match(&mut [], 0);
874
875        let mut transactions = [(5, 1), (0, 2), (5, 3)];
876        assert_reward_percentiles_match(&mut transactions, 1_000);
877        assert_eq!(reward_percentiles(&transactions, 1_000f64), [1, 1, 3]);
878
879        let mut transactions = [(0, 10), (1, 20)];
880        assert_reward_percentiles_match(&mut transactions, 1);
881        let rewards = reward_percentiles(&transactions, 1f64);
882        assert_eq!(&rewards[..200], &[10; 200]);
883        assert_eq!(rewards[200], 20);
884    }
885
886    #[test]
887    fn reward_percentile_sweep_matches_reference_for_randomized_inputs() {
888        let mut state = 0x4d59_5df4_d0f3_3173u64;
889        for _ in 0..2_000 {
890            let len = (next_random(&mut state) % 129) as usize;
891            let mut transactions = (0..len)
892                .map(|_| {
893                    let gas_used = next_random(&mut state) % 100;
894                    let effective_reward = (next_random(&mut state) % 16) as u128;
895                    (gas_used, effective_reward)
896                })
897                .collect::<Vec<_>>();
898            let total_gas = transactions.iter().map(|(gas_used, _)| gas_used).sum::<u64>();
899            let header_gas_used = match next_random(&mut state) % 4 {
900                0 => total_gas,
901                1 => next_random(&mut state) % (total_gas.saturating_add(1)),
902                2 => total_gas.saturating_add(next_random(&mut state) % 1_000),
903                _ => 0,
904            };
905
906            assert_reward_percentiles_match(&mut transactions, header_gas_used);
907        }
908    }
909
910    fn next_random(state: &mut u64) -> u64 {
911        *state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
912        *state
913    }
914
915    #[test]
916    fn fee_details_reject_gas_price_with_dynamic_fees() {
917        for (max_fee, priority) in [(Some(2), None), (None, Some(1)), (Some(2), Some(1))] {
918            assert!(matches!(
919                FeeDetails::new(Some(2), max_fee, priority, None),
920                Err(BlockchainError::ConflictingFeeFields)
921            ));
922        }
923    }
924
925    #[test]
926    fn fee_details_price_legacy_blob_calls_by_gas_price() {
927        let fees = FeeDetails::new(Some(2), None, None, Some(1)).unwrap();
928        assert_eq!(fees.split(), (Some(2), Some(2), Some(2), Some(1)));
929    }
930}