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    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    /// 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 as u8) >= (SpecId::LONDON as u8)
297    }
298
299    pub fn is_eip4844(&self) -> bool {
300        (self.state.read().rules.spec_id as u8) >= (SpecId::CANCUN as u8)
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 as u8) >= (SpecId::CANCUN as u8) {
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 as u8) >= (SpecId::LONDON as u8) { 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 as u8) >= (SpecId::CANCUN as u8))
330            .then_some(state.blob_excess_gas_and_price)
331    }
332
333    pub fn base_fee_per_blob_gas(&self) -> u128 {
334        let state = self.state.read();
335        if (state.rules.spec_id as u8) >= (SpecId::CANCUN as u8) {
336            state.blob_excess_gas_and_price.blob_gasprice
337        } else {
338            0
339        }
340    }
341
342    /// Returns the current gas price
343    pub fn set_gas_price(&self, price: u128) {
344        self.state.write().gas_price = price;
345    }
346
347    /// Returns the current base fee
348    pub fn set_base_fee(&self, fee: u64) {
349        trace!(target: "backend::fees", "updated base fee {:?}", fee);
350        self.state.write().base_fee = fee;
351    }
352
353    /// Sets the current blob excess gas and price
354    pub fn set_blob_excess_gas_and_price(&self, blob_excess_gas_and_price: BlobExcessGasAndPrice) {
355        trace!(target: "backend::fees", "updated blob base fee {:?}", blob_excess_gas_and_price);
356        self.state.write().blob_excess_gas_and_price = blob_excess_gas_and_price;
357    }
358
359    /// Calculates the base fee for the next block
360    pub fn get_next_block_base_fee_per_gas(
361        &self,
362        gas_used: u64,
363        gas_limit: u64,
364        last_fee_per_gas: u64,
365    ) -> u64 {
366        let state = self.state.read();
367        // It's naturally impossible for base fee to be 0;
368        // It means it was set by the user deliberately and therefore we treat it as a constant.
369        // Therefore, we skip the base fee calculation altogether and we return 0.
370        if (state.rules.spec_id as u8) < (SpecId::LONDON as u8) || state.base_fee == 0 {
371            return 0;
372        }
373        calculate_next_block_base_fee_per_gas(state.rules, gas_used, gas_limit, last_fee_per_gas)
374    }
375
376    /// Calculates the next block base fee from the parent block without applying the configured
377    /// zero-fee sentinel.
378    #[cfg(test)]
379    pub(crate) fn calculate_next_block_base_fee_per_gas(
380        &self,
381        gas_used: u64,
382        gas_limit: u64,
383        last_fee_per_gas: u64,
384    ) -> u64 {
385        let rules = self.state.read().rules;
386        if (rules.spec_id as u8) < (SpecId::LONDON as u8) {
387            return 0;
388        }
389        calculate_next_block_base_fee_per_gas(rules, gas_used, gas_limit, last_fee_per_gas)
390    }
391
392    /// Calculates the next block base fee from a complete parent header.
393    pub(crate) fn get_next_block_base_fee_from_header<H: BlockHeader>(&self, header: &H) -> u64 {
394        let state = self.state.read();
395        if (state.rules.spec_id as u8) < (SpecId::LONDON as u8) || state.base_fee == 0 {
396            return 0;
397        }
398        calculate_parent_header_fees(state.rules, header).base_fee
399    }
400
401    /// Returns all fee metadata inherited from a parent header, honoring the configured zero-fee
402    /// sentinel.
403    pub(crate) fn get_parent_header_fees<H: BlockHeader>(&self, header: &H) -> ParentHeaderFees {
404        let state = self.state.read();
405        let mut fees = calculate_parent_header_fees(state.rules, header);
406        if (state.rules.spec_id as u8) < (SpecId::LONDON as u8) || state.base_fee == 0 {
407            fees.base_fee = 0;
408        }
409        fees
410    }
411
412    /// Calculates the next block base fee from a complete parent header without applying the
413    /// configured zero-fee sentinel.
414    pub(crate) fn calculate_next_block_base_fee_from_header<H: BlockHeader>(
415        &self,
416        header: &H,
417    ) -> u64 {
418        let rules = self.state.read().rules;
419        if (rules.spec_id as u8) < (SpecId::LONDON as u8) {
420            return 0;
421        }
422        calculate_parent_header_fees(rules, header).base_fee
423    }
424
425    /// Returns all fee metadata inherited from a parent header without applying the configured
426    /// zero-fee sentinel.
427    pub(crate) fn calculate_parent_header_fees<H: BlockHeader>(
428        &self,
429        header: &H,
430    ) -> ParentHeaderFees {
431        let rules = self.state.read().rules;
432        let mut fees = calculate_parent_header_fees(rules, header);
433        if (rules.spec_id as u8) < (SpecId::LONDON as u8) {
434            fees.base_fee = 0;
435        }
436        fees
437    }
438
439    /// Calculates the next block blob base fee.
440    pub fn get_next_block_blob_base_fee_per_gas(&self) -> u128 {
441        let state = self.state.read();
442        state.blob_params.calc_blob_fee(state.blob_excess_gas_and_price.excess_blob_gas)
443    }
444
445    /// Configures the blob params
446    pub fn set_blob_params(&self, blob_params: BlobParams) {
447        self.state.write().blob_params = blob_params;
448    }
449
450    /// Returns the active [`BlobParams`]
451    pub fn blob_params(&self) -> BlobParams {
452        self.state.read().blob_params
453    }
454}
455
456fn calculate_next_block_base_fee_per_gas(
457    rules: FeeRules,
458    gas_used: u64,
459    gas_limit: u64,
460    last_fee_per_gas: u64,
461) -> u64 {
462    // Tempo replaces EIP-1559 with its own hardfork-specific base fee rules.
463    if let Some(hardfork) = rules.tempo_hardfork {
464        return tempo_next_block_base_fee(hardfork, gas_used, last_fee_per_gas);
465    }
466    calc_next_block_base_fee(gas_used, gas_limit, last_fee_per_gas, rules.base_fee.params())
467}
468
469fn calculate_parent_header_fees<H: BlockHeader>(rules: FeeRules, header: &H) -> ParentHeaderFees {
470    if let Some(hardfork) = rules.tempo_hardfork {
471        return ParentHeaderFees {
472            base_fee: tempo_next_block_base_fee(
473                hardfork,
474                header.gas_used(),
475                header.base_fee_per_gas().unwrap_or_default(),
476            ),
477            ..Default::default()
478        };
479    }
480    rules.base_fee.parent_header_fees(header)
481}
482
483/// Computes the next block's base fee for a Tempo chain.
484///
485/// - T7+: the TIP-1067 dynamic controller, an EIP-1559 update against a fixed 10M gas target
486///   clamped to `[floor, cap]`.
487/// - Pre-T7: the fixed hardfork base fee (10 gwei pre-T1, 20 gwei T1+).
488fn tempo_next_block_base_fee(hardfork: TempoHardfork, gas_used: u64, parent_base_fee: u64) -> u64 {
489    if hardfork.is_t7() {
490        return tempo_t7_next_block_base_fee(parent_base_fee, gas_used);
491    }
492    crate::config::tempo_default_base_fee(hardfork)
493}
494
495/// An async service that takes care of the `FeeHistory` cache
496pub struct FeeHistoryService<N: Network>
497where
498    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
499{
500    /// Live fee rules, including blob parameters replaced by fork resets.
501    fees: FeeManager,
502    /// incoming notifications about new blocks
503    new_blocks: ChainNotifications,
504    /// contains all fee history related entries
505    cache: FeeHistoryCache,
506    /// number of items to consider
507    fee_history_limit: u64,
508    /// a type that can fetch ethereum-storage data
509    storage_info: StorageInfo<N>,
510}
511
512impl<N: Network> FeeHistoryService<N>
513where
514    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
515{
516    pub const fn new(
517        fees: FeeManager,
518        new_blocks: ChainNotifications,
519        cache: FeeHistoryCache,
520        storage_info: StorageInfo<N>,
521    ) -> Self {
522        Self {
523            fees,
524            new_blocks,
525            cache,
526            fee_history_limit: MAX_FEE_HISTORY_CACHE_SIZE,
527            storage_info,
528        }
529    }
530
531    /// Returns the configured history limit
532    pub const fn fee_history_limit(&self) -> u64 {
533        self.fee_history_limit
534    }
535
536    /// Inserts a new cache entry for the given block
537    pub(crate) fn insert_cache_entry_for_block(&self, hash: B256, header: &impl BlockHeader) {
538        let (result, block_number) = self.create_cache_entry(hash, header);
539        self.insert_cache_entry(result, block_number);
540    }
541
542    /// Create a new history entry for the block
543    fn create_cache_entry(
544        &self,
545        hash: B256,
546        header: &impl BlockHeader,
547    ) -> (FeeHistoryCacheItem, Option<u64>) {
548        create_fee_history_cache_item(hash, header, &self.storage_info, self.fees.blob_params())
549    }
550
551    fn insert_cache_entry(&self, item: FeeHistoryCacheItem, block_number: Option<u64>) {
552        insert_fee_history_cache_item(&self.cache, item, block_number, self.fee_history_limit);
553    }
554}
555
556/// Inserts an entry into the fee history cache and trims it back to `fee_history_limit`.
557///
558/// Used by the async [`FeeHistoryService`]. The `eth_feeHistory` fallback applies the same bounded
559/// insertion policy to a batch under one lock.
560pub(crate) fn insert_fee_history_cache_item(
561    cache: &FeeHistoryCache,
562    item: FeeHistoryCacheItem,
563    block_number: Option<u64>,
564    fee_history_limit: u64,
565) {
566    if let Some(block_number) = block_number {
567        trace!(target: "fees", "insert new history item={:?} for {}", item, block_number);
568        let mut cache = cache.lock();
569        cache.insert(block_number, item);
570
571        // Trim to the cache limit by dropping the oldest entries (smallest block numbers).
572        // `pop_first` is saturating and correct regardless of insertion order, unlike the
573        // previous index math which could underflow when the `eth_feeHistory` fallback inserts
574        // entries out of order.
575        while cache.len() as u64 > fee_history_limit {
576            cache.pop_first();
577        }
578    }
579}
580
581/// Calculates percentile rewards from transactions sorted by effective reward.
582///
583/// [`REWARD_PERCENTILES`] must remain ascending because the transaction cursor never rewinds.
584fn reward_percentiles(transactions: &[(u64, u128)], block_gas_used: f64) -> Vec<u128> {
585    let mut rewards = Vec::with_capacity(REWARD_PERCENTILES.len());
586    let mut transactions = transactions.iter().copied();
587    let Some((mut cumulative_gas, mut current_reward)) = transactions.next() else {
588        return rewards;
589    };
590
591    for &percentile in REWARD_PERCENTILES.iter() {
592        let target_gas = (percentile * block_gas_used / 100f64) as u64;
593        while target_gas > cumulative_gas {
594            let Some((tx_gas_used, effective_reward)) = transactions.next() else { return rewards };
595            cumulative_gas += tx_gas_used;
596            current_reward = effective_reward;
597        }
598        rewards.push(current_reward);
599    }
600
601    rewards
602}
603
604/// Builds the [`FeeHistoryCacheItem`] for a single block.
605///
606/// Shared by the async [`FeeHistoryService`] and by `eth_feeHistory` itself: the service can lag
607/// the chain head (it only runs when the node task is polled), so the RPC handler computes any
608/// missing entry on demand with the same logic instead of returning a short response.
609pub(crate) fn create_fee_history_cache_item<N: Network>(
610    hash: B256,
611    header: &impl BlockHeader,
612    storage_info: &StorageInfo<N>,
613    blob_params: BlobParams,
614) -> (FeeHistoryCacheItem, Option<u64>)
615where
616    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
617{
618    let mut block_number: Option<u64> = None;
619    let base_fee = header.base_fee_per_gas().unwrap_or_default();
620    let excess_blob_gas = header.excess_blob_gas().map(|g| g as u128);
621    let blob_gas_used = header.blob_gas_used().map(|g| g as u128);
622    let base_fee_per_blob_gas = header.blob_fee(blob_params);
623
624    let mut item = FeeHistoryCacheItem {
625        block_hash: hash,
626        base_fee: base_fee as u128,
627        gas_used_ratio: 0f64,
628        blob_gas_used_ratio: 0f64,
629        rewards: Vec::new(),
630        excess_blob_gas,
631        base_fee_per_blob_gas,
632        blob_gas_used,
633    };
634
635    let current_block = storage_info.block(hash);
636    let current_receipts = storage_info.receipts(hash);
637
638    if let (Some(block), Some(receipts)) = (current_block, current_receipts) {
639        block_number = Some(block.header.number());
640
641        let gas_used = block.header.gas_used() as f64;
642        let blob_gas_used = block.header.blob_gas_used().map(|g| g as f64);
643        let gas_limit = block.header.gas_limit() as f64;
644        item.gas_used_ratio = if gas_limit == 0.0 { 0.0 } else { gas_used / gas_limit };
645        item.blob_gas_used_ratio = blob_gas_used
646            .map(|g| {
647                let max = blob_params.max_blob_gas_per_block() as f64;
648                if max == 0.0 { 0.0 } else { g / max }
649            })
650            .unwrap_or(0.0);
651
652        // extract useful tx info (gas_used, effective_reward)
653        let mut transactions: Vec<(_, _)> = receipts
654            .iter()
655            .enumerate()
656            .map(|(i, receipt)| {
657                let cumulative = receipt.cumulative_gas_used();
658                let prev_cumulative = if i > 0 { receipts[i - 1].cumulative_gas_used() } else { 0 };
659                let gas_used = cumulative - prev_cumulative;
660                let effective_reward = block
661                    .body
662                    .transactions
663                    .get(i)
664                    .map(|tx| tx.as_ref().effective_tip_per_gas(base_fee).unwrap_or(0))
665                    .unwrap_or(0);
666
667                (gas_used, effective_reward)
668            })
669            .collect();
670
671        // sort by effective reward asc
672        transactions.sort_by_key(|(_, reward)| *reward);
673
674        item.rewards = reward_percentiles(&transactions, gas_used);
675    } else {
676        item.rewards = vec![0; REWARD_PERCENTILES.len()];
677    }
678    (item, block_number)
679}
680
681// An endless future that listens for new blocks and updates the cache
682impl<N: Network> Future for FeeHistoryService<N>
683where
684    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
685{
686    type Output = ();
687
688    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
689        let pin = self.get_mut();
690
691        while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) {
692            // add the imported block.
693            if let Some(block) = notification.as_new_block() {
694                pin.insert_cache_entry_for_block(block.hash, block.header.as_ref());
695            }
696        }
697
698        Poll::Pending
699    }
700}
701
702pub type FeeHistoryCache = Arc<Mutex<BTreeMap<u64, FeeHistoryCacheItem>>>;
703
704/// A single item in the whole fee history cache
705#[derive(Clone, Debug)]
706pub struct FeeHistoryCacheItem {
707    pub block_hash: B256,
708    pub base_fee: u128,
709    pub gas_used_ratio: f64,
710    pub base_fee_per_blob_gas: Option<u128>,
711    pub blob_gas_used_ratio: f64,
712    pub excess_blob_gas: Option<u128>,
713    pub blob_gas_used: Option<u128>,
714    pub rewards: Vec<u128>,
715}
716
717#[derive(Clone, Default)]
718pub struct FeeDetails {
719    pub gas_price: Option<u128>,
720    pub max_fee_per_gas: Option<u128>,
721    pub max_priority_fee_per_gas: Option<u128>,
722    pub max_fee_per_blob_gas: Option<u128>,
723}
724
725impl FeeDetails {
726    /// All values zero
727    pub const fn zero() -> Self {
728        Self {
729            gas_price: Some(0),
730            max_fee_per_gas: Some(0),
731            max_priority_fee_per_gas: Some(0),
732            max_fee_per_blob_gas: None,
733        }
734    }
735
736    /// If neither `gas_price` nor `max_fee_per_gas` is `Some`, this will set both to `0`
737    pub const fn or_zero_fees(self) -> Self {
738        let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
739            self;
740
741        let no_fees = gas_price.is_none() && max_fee_per_gas.is_none();
742        let gas_price = if no_fees { Some(0) } else { gas_price };
743        let max_fee_per_gas = if no_fees { Some(0) } else { max_fee_per_gas };
744
745        Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas }
746    }
747
748    /// Turns this type into a tuple
749    pub const fn split(self) -> (Option<u128>, Option<u128>, Option<u128>, Option<u128>) {
750        let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
751            self;
752        (gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
753    }
754
755    /// Creates a new instance from the request's gas related values
756    pub fn new(
757        request_gas_price: Option<u128>,
758        request_max_fee: Option<u128>,
759        request_priority: Option<u128>,
760        max_fee_per_blob_gas: Option<u128>,
761    ) -> Result<Self, BlockchainError> {
762        match (request_gas_price, request_max_fee, request_priority, max_fee_per_blob_gas) {
763            (Some(_), Some(_), _, _) | (Some(_), _, Some(_), _) => {
764                Err(BlockchainError::ConflictingFeeFields)
765            }
766            (gas_price, None, None, max_fee_per_blob_gas) => {
767                // Legacy request, all default to gas price. A blob fee cap is priced separately.
768                Ok(Self {
769                    gas_price,
770                    max_fee_per_gas: gas_price,
771                    max_priority_fee_per_gas: gas_price,
772                    max_fee_per_blob_gas,
773                })
774            }
775            (_, max_fee, max_priority, max_fee_per_blob_gas) => {
776                // eip-1559
777                // Ensure `max_priority_fee_per_gas` is less or equal to `max_fee_per_gas`.
778                if let Some(max_priority) = max_priority {
779                    let max_fee = max_fee.unwrap_or_default();
780                    if max_priority > max_fee {
781                        return Err(BlockchainError::InvalidFeeInput);
782                    }
783                }
784                Ok(Self {
785                    gas_price: max_fee,
786                    max_fee_per_gas: max_fee,
787                    // A fee cap without a tip pays the base fee only.
788                    max_priority_fee_per_gas: max_priority.or(max_fee.map(|_| 0)),
789                    max_fee_per_blob_gas,
790                })
791            }
792        }
793    }
794}
795
796impl fmt::Debug for FeeDetails {
797    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
798        write!(fmt, "Fees {{ ")?;
799        write!(fmt, "gas_price: {:?}, ", self.gas_price)?;
800        write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?;
801        write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?;
802        write!(fmt, "}}")?;
803        Ok(())
804    }
805}
806
807#[cfg(test)]
808mod tests {
809    use super::*;
810
811    fn reward_percentiles_reference(
812        transactions: &[(u64, u128)],
813        block_gas_used: f64,
814    ) -> Vec<u128> {
815        REWARD_PERCENTILES
816            .iter()
817            .filter_map(|&percentile| {
818                let target_gas = (percentile * block_gas_used / 100f64) as u64;
819                let mut cumulative_gas = 0;
820                for (tx_gas_used, effective_reward) in transactions.iter().copied() {
821                    cumulative_gas += tx_gas_used;
822                    if target_gas <= cumulative_gas {
823                        return Some(effective_reward);
824                    }
825                }
826                None
827            })
828            .collect()
829    }
830
831    fn assert_reward_percentiles_match(transactions: &mut [(u64, u128)], gas_used: u64) {
832        transactions.sort_by_key(|(_, reward)| *reward);
833        assert_eq!(
834            reward_percentiles(transactions, gas_used as f64),
835            reward_percentiles_reference(transactions, gas_used as f64)
836        );
837    }
838
839    fn fee_manager(spec_id: SpecId) -> FeeManager {
840        FeeManager::new(
841            spec_id,
842            INITIAL_BASE_FEE,
843            true,
844            INITIAL_GAS_PRICE,
845            BlobExcessGasAndPrice::new_with_spec(0, SpecId::CANCUN),
846            BlobParams::cancun(),
847            BaseFeeParams::ethereum(),
848            None,
849        )
850    }
851
852    #[test]
853    fn raw_next_base_fee_respects_london_activation() {
854        let berlin = fee_manager(SpecId::BERLIN);
855        assert_eq!(
856            berlin.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
857            0
858        );
859
860        let london = fee_manager(SpecId::LONDON);
861        assert_ne!(
862            london.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
863            0
864        );
865    }
866
867    #[cfg(feature = "optimism")]
868    #[test]
869    fn pre_london_parent_fees_preserve_optimism_metadata() {
870        let fees = fee_manager(SpecId::BERLIN);
871        let jovian = [1, 0, 0, 0, 250, 0, 0, 0, 2, 0, 0, 0, 0, 0, 76, 75, 64];
872        fees.set_optimism_base_fee_rules(&jovian);
873        let header = alloy_consensus::Header { extra_data: jovian.into(), ..Default::default() };
874
875        let parent_fees = fees.get_parent_header_fees(&header);
876        assert_eq!(parent_fees.base_fee, 0);
877        assert_eq!(parent_fees.extra_data.as_ref(), jovian);
878        assert_eq!(parent_fees.optimism_jovian, Some(true));
879    }
880
881    #[test]
882    fn fee_details_default_missing_tip_to_zero() {
883        let fees = FeeDetails::new(None, Some(5), None, None).unwrap();
884        assert_eq!(fees.split(), (Some(5), Some(5), Some(0), None));
885    }
886
887    #[test]
888    fn reward_percentile_sweep_preserves_boundaries_and_empty_results() {
889        assert_reward_percentiles_match(&mut [], 0);
890
891        let mut transactions = [(5, 1), (0, 2), (5, 3)];
892        assert_reward_percentiles_match(&mut transactions, 1_000);
893        assert_eq!(reward_percentiles(&transactions, 1_000f64), [1, 1, 3]);
894
895        let mut transactions = [(0, 10), (1, 20)];
896        assert_reward_percentiles_match(&mut transactions, 1);
897        let rewards = reward_percentiles(&transactions, 1f64);
898        assert_eq!(&rewards[..200], &[10; 200]);
899        assert_eq!(rewards[200], 20);
900    }
901
902    #[test]
903    fn reward_percentile_sweep_matches_reference_for_randomized_inputs() {
904        let mut state = 0x4d59_5df4_d0f3_3173u64;
905        for _ in 0..2_000 {
906            let len = (next_random(&mut state) % 129) as usize;
907            let mut transactions = (0..len)
908                .map(|_| {
909                    let gas_used = next_random(&mut state) % 100;
910                    let effective_reward = (next_random(&mut state) % 16) as u128;
911                    (gas_used, effective_reward)
912                })
913                .collect::<Vec<_>>();
914            let total_gas = transactions.iter().map(|(gas_used, _)| gas_used).sum::<u64>();
915            let header_gas_used = match next_random(&mut state) % 4 {
916                0 => total_gas,
917                1 => next_random(&mut state) % (total_gas.saturating_add(1)),
918                2 => total_gas.saturating_add(next_random(&mut state) % 1_000),
919                _ => 0,
920            };
921
922            assert_reward_percentiles_match(&mut transactions, header_gas_used);
923        }
924    }
925
926    fn next_random(state: &mut u64) -> u64 {
927        *state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
928        *state
929    }
930
931    #[test]
932    fn fee_details_reject_gas_price_with_dynamic_fees() {
933        for (max_fee, priority) in [(Some(2), None), (None, Some(1)), (Some(2), Some(1))] {
934            assert!(matches!(
935                FeeDetails::new(Some(2), max_fee, priority, None),
936                Err(BlockchainError::ConflictingFeeFields)
937            ));
938        }
939    }
940
941    #[test]
942    fn fee_details_price_legacy_blob_calls_by_gas_price() {
943        let fees = FeeDetails::new(Some(2), None, None, Some(1)).unwrap();
944        assert_eq!(fees.split(), (Some(2), Some(2), Some(2), Some(1)));
945    }
946}