Skip to main content

anvil/eth/
fees.rs

1use std::{
2    collections::BTreeMap,
3    fmt,
4    pin::Pin,
5    sync::Arc,
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;
13use futures::StreamExt;
14use parking_lot::{Mutex, RwLock};
15use revm::{context_interface::block::BlobExcessGasAndPrice, primitives::hardfork::SpecId};
16use tempo_hardfork::{TempoHardfork, constants::gas::tempo_t7_next_block_base_fee};
17
18use crate::eth::{
19    backend::{info::StorageInfo, notifications::ChainNotifications},
20    error::BlockchainError,
21};
22
23/// Maximum number of entries in the fee history cache
24pub const MAX_FEE_HISTORY_CACHE_SIZE: u64 = 2048u64;
25
26/// Initial base fee for EIP-1559 blocks.
27pub const INITIAL_BASE_FEE: u64 = 1_000_000_000;
28
29/// Initial default gas price for the first block
30pub const INITIAL_GAS_PRICE: u128 = 1_875_000_000;
31
32/// Bounds the amount the base fee can change between blocks.
33pub const BASE_FEE_CHANGE_DENOMINATOR: u128 = 8;
34
35/// Minimum suggested priority fee
36pub const MIN_SUGGESTED_PRIORITY_FEE: u128 = 1e9 as u128;
37
38/// Stores the fee related information
39#[derive(Clone, Debug)]
40pub struct FeeManager {
41    /// Hardfork identifier
42    spec_id: SpecId,
43    /// The blob params that determine blob fees
44    blob_params: Arc<RwLock<BlobParams>>,
45    /// Tracks the base fee for the next block post London
46    ///
47    /// This value will be updated after a new block was mined
48    base_fee: Arc<RwLock<u64>>,
49    /// Whether the minimum suggested priority fee is enforced
50    is_min_priority_fee_enforced: bool,
51    /// Tracks the excess blob gas, and the base fee, for the next block post Cancun
52    ///
53    /// This value will be updated after a new block was mined
54    blob_excess_gas_and_price: Arc<RwLock<BlobExcessGasAndPrice>>,
55    /// The base price to use Pre London
56    ///
57    /// This will be constant value unless changed manually
58    gas_price: Arc<RwLock<u128>>,
59    elasticity: Arc<RwLock<f64>>,
60    /// Network-specific base fee params for EIP-1559 calculations
61    base_fee_params: BaseFeeParams,
62    /// The active Tempo hardfork, set only when running a Tempo chain.
63    ///
64    /// Tempo replaces EIP-1559: pre-T7 the base fee is fixed, and T7+ uses the TIP-1067 dynamic
65    /// controller. It is updated once after a fork's hardfork is auto-detected.
66    tempo_hardfork: Arc<RwLock<Option<TempoHardfork>>>,
67}
68
69impl FeeManager {
70    #[allow(clippy::too_many_arguments)]
71    pub fn new(
72        spec_id: SpecId,
73        base_fee: u64,
74        is_min_priority_fee_enforced: bool,
75        gas_price: u128,
76        blob_excess_gas_and_price: BlobExcessGasAndPrice,
77        blob_params: BlobParams,
78        base_fee_params: BaseFeeParams,
79        tempo_hardfork: Option<TempoHardfork>,
80    ) -> Self {
81        let elasticity = 1f64 / base_fee_params.elasticity_multiplier as f64;
82        Self {
83            spec_id,
84            blob_params: Arc::new(RwLock::new(blob_params)),
85            base_fee: Arc::new(RwLock::new(base_fee)),
86            is_min_priority_fee_enforced,
87            gas_price: Arc::new(RwLock::new(gas_price)),
88            blob_excess_gas_and_price: Arc::new(RwLock::new(blob_excess_gas_and_price)),
89            elasticity: Arc::new(RwLock::new(elasticity)),
90            base_fee_params,
91            tempo_hardfork: Arc::new(RwLock::new(tempo_hardfork)),
92        }
93    }
94
95    /// Returns the active Tempo hardfork, if running a Tempo chain.
96    pub fn tempo_hardfork(&self) -> Option<TempoHardfork> {
97        *self.tempo_hardfork.read()
98    }
99
100    /// Sets the active Tempo hardfork, used after a fork's hardfork is auto-detected.
101    pub fn set_tempo_hardfork(&self, hardfork: Option<TempoHardfork>) {
102        *self.tempo_hardfork.write() = hardfork;
103    }
104
105    pub fn elasticity(&self) -> f64 {
106        *self.elasticity.read()
107    }
108
109    /// Returns true for post London
110    pub const fn is_eip1559(&self) -> bool {
111        (self.spec_id as u8) >= (SpecId::LONDON as u8)
112    }
113
114    pub const fn is_eip4844(&self) -> bool {
115        (self.spec_id as u8) >= (SpecId::CANCUN as u8)
116    }
117
118    /// Calculates the current blob gas price
119    pub fn blob_gas_price(&self) -> u128 {
120        if self.is_eip4844() { self.base_fee_per_blob_gas() } else { 0 }
121    }
122
123    pub fn base_fee(&self) -> u64 {
124        if self.is_eip1559() { *self.base_fee.read() } else { 0 }
125    }
126
127    pub const fn is_min_priority_fee_enforced(&self) -> bool {
128        self.is_min_priority_fee_enforced
129    }
130
131    /// Raw base gas price
132    pub fn raw_gas_price(&self) -> u128 {
133        *self.gas_price.read()
134    }
135
136    pub fn excess_blob_gas_and_price(&self) -> Option<BlobExcessGasAndPrice> {
137        self.is_eip4844().then(|| *self.blob_excess_gas_and_price.read())
138    }
139
140    pub fn base_fee_per_blob_gas(&self) -> u128 {
141        if self.is_eip4844() { self.blob_excess_gas_and_price.read().blob_gasprice } else { 0 }
142    }
143
144    /// Returns the current gas price
145    pub fn set_gas_price(&self, price: u128) {
146        let mut gas = self.gas_price.write();
147        *gas = price;
148    }
149
150    /// Returns the current base fee
151    pub fn set_base_fee(&self, fee: u64) {
152        trace!(target: "backend::fees", "updated base fee {:?}", fee);
153        let mut base = self.base_fee.write();
154        *base = fee;
155    }
156
157    /// Sets the current blob excess gas and price
158    pub fn set_blob_excess_gas_and_price(&self, blob_excess_gas_and_price: BlobExcessGasAndPrice) {
159        trace!(target: "backend::fees", "updated blob base fee {:?}", blob_excess_gas_and_price);
160        let mut base = self.blob_excess_gas_and_price.write();
161        *base = blob_excess_gas_and_price;
162    }
163
164    /// Calculates the base fee for the next block
165    pub fn get_next_block_base_fee_per_gas(
166        &self,
167        gas_used: u64,
168        gas_limit: u64,
169        last_fee_per_gas: u64,
170    ) -> u64 {
171        // It's naturally impossible for base fee to be 0;
172        // It means it was set by the user deliberately and therefore we treat it as a constant.
173        // Therefore, we skip the base fee calculation altogether and we return 0.
174        if self.base_fee() == 0 {
175            return 0;
176        }
177        // Tempo replaces EIP-1559 with its own hardfork-specific base fee rules.
178        if let Some(hardfork) = self.tempo_hardfork() {
179            return tempo_next_block_base_fee(hardfork, gas_used, last_fee_per_gas);
180        }
181        calc_next_block_base_fee(gas_used, gas_limit, last_fee_per_gas, self.base_fee_params)
182    }
183
184    /// Calculates the next block blob base fee.
185    pub fn get_next_block_blob_base_fee_per_gas(&self) -> u128 {
186        self.blob_params().calc_blob_fee(self.blob_excess_gas_and_price.read().excess_blob_gas)
187    }
188
189    /// Calculates the next block blob excess gas, using the provided parent blob excess gas and
190    /// parent blob gas used
191    pub fn get_next_block_blob_excess_gas(&self, blob_excess_gas: u64, blob_gas_used: u64) -> u64 {
192        self.blob_params().next_block_excess_blob_gas_osaka(
193            blob_excess_gas,
194            blob_gas_used,
195            self.base_fee(),
196        )
197    }
198
199    /// Configures the blob params
200    pub fn set_blob_params(&self, blob_params: BlobParams) {
201        *self.blob_params.write() = blob_params;
202    }
203
204    /// Returns the active [`BlobParams`]
205    pub fn blob_params(&self) -> BlobParams {
206        *self.blob_params.read()
207    }
208}
209
210/// Computes the next block's base fee for a Tempo chain.
211///
212/// - T7+: the TIP-1067 dynamic controller, an EIP-1559 update against a fixed 10M gas target
213///   clamped to `[floor, cap]`.
214/// - Pre-T7: the fixed hardfork base fee (10 gwei pre-T1, 20 gwei T1+).
215fn tempo_next_block_base_fee(hardfork: TempoHardfork, gas_used: u64, parent_base_fee: u64) -> u64 {
216    if hardfork.is_t7() {
217        return tempo_t7_next_block_base_fee(parent_base_fee, gas_used);
218    }
219    crate::config::tempo_default_base_fee(hardfork)
220}
221
222/// An async service that takes care of the `FeeHistory` cache
223pub struct FeeHistoryService<N: Network>
224where
225    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
226{
227    /// blob parameters for the current spec
228    blob_params: BlobParams,
229    /// incoming notifications about new blocks
230    new_blocks: ChainNotifications,
231    /// contains all fee history related entries
232    cache: FeeHistoryCache,
233    /// number of items to consider
234    fee_history_limit: u64,
235    /// a type that can fetch ethereum-storage data
236    storage_info: StorageInfo<N>,
237}
238
239impl<N: Network> FeeHistoryService<N>
240where
241    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
242{
243    pub const fn new(
244        blob_params: BlobParams,
245        new_blocks: ChainNotifications,
246        cache: FeeHistoryCache,
247        storage_info: StorageInfo<N>,
248    ) -> Self {
249        Self {
250            blob_params,
251            new_blocks,
252            cache,
253            fee_history_limit: MAX_FEE_HISTORY_CACHE_SIZE,
254            storage_info,
255        }
256    }
257
258    /// Returns the configured history limit
259    pub const fn fee_history_limit(&self) -> u64 {
260        self.fee_history_limit
261    }
262
263    /// Inserts a new cache entry for the given block
264    pub(crate) fn insert_cache_entry_for_block(&self, hash: B256, header: &impl BlockHeader) {
265        let (result, block_number) = self.create_cache_entry(hash, header);
266        self.insert_cache_entry(result, block_number);
267    }
268
269    /// Create a new history entry for the block
270    fn create_cache_entry(
271        &self,
272        hash: B256,
273        header: &impl BlockHeader,
274    ) -> (FeeHistoryCacheItem, Option<u64>) {
275        // percentile list from 0.0 to 100.0 with a 0.5 resolution.
276        // this will create 200 percentile points
277        let reward_percentiles: Vec<f64> = {
278            let mut percentile: f64 = 0.0;
279            (0..=200)
280                .map(|_| {
281                    let val = percentile;
282                    percentile += 0.5;
283                    val
284                })
285                .collect()
286        };
287
288        let mut block_number: Option<u64> = None;
289        let base_fee = header.base_fee_per_gas().unwrap_or_default();
290        let excess_blob_gas = header.excess_blob_gas().map(|g| g as u128);
291        let blob_gas_used = header.blob_gas_used().map(|g| g as u128);
292        let base_fee_per_blob_gas = header.blob_fee(self.blob_params);
293
294        let mut item = FeeHistoryCacheItem {
295            base_fee: base_fee as u128,
296            gas_used_ratio: 0f64,
297            blob_gas_used_ratio: 0f64,
298            rewards: Vec::new(),
299            excess_blob_gas,
300            base_fee_per_blob_gas,
301            blob_gas_used,
302        };
303
304        let current_block = self.storage_info.block(hash);
305        let current_receipts = self.storage_info.receipts(hash);
306
307        if let (Some(block), Some(receipts)) = (current_block, current_receipts) {
308            block_number = Some(block.header.number());
309
310            let gas_used = block.header.gas_used() as f64;
311            let blob_gas_used = block.header.blob_gas_used().map(|g| g as f64);
312            item.gas_used_ratio = gas_used / block.header.gas_limit() as f64;
313            item.blob_gas_used_ratio = blob_gas_used
314                .map(|g| {
315                    let max = self.blob_params.max_blob_gas_per_block() as f64;
316                    if max == 0.0 { 0.0 } else { g / max }
317                })
318                .unwrap_or(0.0);
319
320            // extract useful tx info (gas_used, effective_reward)
321            let mut transactions: Vec<(_, _)> = receipts
322                .iter()
323                .enumerate()
324                .map(|(i, receipt)| {
325                    let cumulative = receipt.cumulative_gas_used();
326                    let prev_cumulative =
327                        if i > 0 { receipts[i - 1].cumulative_gas_used() } else { 0 };
328                    let gas_used = cumulative - prev_cumulative;
329                    let effective_reward = block
330                        .body
331                        .transactions
332                        .get(i)
333                        .map(|tx| tx.as_ref().effective_tip_per_gas(base_fee).unwrap_or(0))
334                        .unwrap_or(0);
335
336                    (gas_used, effective_reward)
337                })
338                .collect();
339
340            // sort by effective reward asc
341            transactions.sort_by_key(|(_, reward)| *reward);
342
343            // calculate percentile rewards
344            item.rewards = reward_percentiles
345                .into_iter()
346                .filter_map(|p| {
347                    let target_gas = (p * gas_used / 100f64) as u64;
348                    let mut sum_gas = 0;
349                    for (gas_used, effective_reward) in transactions.iter().copied() {
350                        sum_gas += gas_used;
351                        if target_gas <= sum_gas {
352                            return Some(effective_reward);
353                        }
354                    }
355                    None
356                })
357                .collect();
358        } else {
359            item.rewards = vec![0; reward_percentiles.len()];
360        }
361        (item, block_number)
362    }
363
364    fn insert_cache_entry(&self, item: FeeHistoryCacheItem, block_number: Option<u64>) {
365        if let Some(block_number) = block_number {
366            trace!(target: "fees", "insert new history item={:?} for {}", item, block_number);
367            let mut cache = self.cache.lock();
368            cache.insert(block_number, item);
369
370            // adhere to cache limit
371            let pop_next = block_number.saturating_sub(self.fee_history_limit);
372
373            let num_remove = (cache.len() as u64).saturating_sub(self.fee_history_limit);
374            for num in 0..num_remove {
375                let key = pop_next - num;
376                cache.remove(&key);
377            }
378        }
379    }
380}
381
382// An endless future that listens for new blocks and updates the cache
383impl<N: Network> Future for FeeHistoryService<N>
384where
385    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
386{
387    type Output = ();
388
389    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
390        let pin = self.get_mut();
391
392        while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) {
393            // add the imported block.
394            if let Some(block) = notification.as_new_block() {
395                pin.insert_cache_entry_for_block(block.hash, block.header.as_ref());
396            }
397        }
398
399        Poll::Pending
400    }
401}
402
403pub type FeeHistoryCache = Arc<Mutex<BTreeMap<u64, FeeHistoryCacheItem>>>;
404
405/// A single item in the whole fee history cache
406#[derive(Clone, Debug)]
407pub struct FeeHistoryCacheItem {
408    pub base_fee: u128,
409    pub gas_used_ratio: f64,
410    pub base_fee_per_blob_gas: Option<u128>,
411    pub blob_gas_used_ratio: f64,
412    pub excess_blob_gas: Option<u128>,
413    pub blob_gas_used: Option<u128>,
414    pub rewards: Vec<u128>,
415}
416
417#[derive(Clone, Default)]
418pub struct FeeDetails {
419    pub gas_price: Option<u128>,
420    pub max_fee_per_gas: Option<u128>,
421    pub max_priority_fee_per_gas: Option<u128>,
422    pub max_fee_per_blob_gas: Option<u128>,
423}
424
425impl FeeDetails {
426    /// All values zero
427    pub const fn zero() -> Self {
428        Self {
429            gas_price: Some(0),
430            max_fee_per_gas: Some(0),
431            max_priority_fee_per_gas: Some(0),
432            max_fee_per_blob_gas: None,
433        }
434    }
435
436    /// If neither `gas_price` nor `max_fee_per_gas` is `Some`, this will set both to `0`
437    pub const fn or_zero_fees(self) -> Self {
438        let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
439            self;
440
441        let no_fees = gas_price.is_none() && max_fee_per_gas.is_none();
442        let gas_price = if no_fees { Some(0) } else { gas_price };
443        let max_fee_per_gas = if no_fees { Some(0) } else { max_fee_per_gas };
444        let max_fee_per_blob_gas = if no_fees { None } else { max_fee_per_blob_gas };
445
446        Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas }
447    }
448
449    /// Turns this type into a tuple
450    pub const fn split(self) -> (Option<u128>, Option<u128>, Option<u128>, Option<u128>) {
451        let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
452            self;
453        (gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
454    }
455
456    /// Creates a new instance from the request's gas related values
457    pub fn new(
458        request_gas_price: Option<u128>,
459        request_max_fee: Option<u128>,
460        request_priority: Option<u128>,
461        max_fee_per_blob_gas: Option<u128>,
462    ) -> Result<Self, BlockchainError> {
463        match (request_gas_price, request_max_fee, request_priority, max_fee_per_blob_gas) {
464            (gas_price, None, None, None) => {
465                // Legacy request, all default to gas price.
466                Ok(Self {
467                    gas_price,
468                    max_fee_per_gas: gas_price,
469                    max_priority_fee_per_gas: gas_price,
470                    max_fee_per_blob_gas: None,
471                })
472            }
473            (_, max_fee, max_priority, max_fee_per_blob_gas) => {
474                // eip-1559
475                // Ensure `max_priority_fee_per_gas` is less or equal to `max_fee_per_gas`.
476                if let Some(max_priority) = max_priority {
477                    let max_fee = max_fee.unwrap_or_default();
478                    if max_priority > max_fee {
479                        return Err(BlockchainError::InvalidFeeInput);
480                    }
481                }
482                Ok(Self {
483                    gas_price: max_fee,
484                    max_fee_per_gas: max_fee,
485                    max_priority_fee_per_gas: max_priority,
486                    max_fee_per_blob_gas,
487                })
488            }
489        }
490    }
491}
492
493impl fmt::Debug for FeeDetails {
494    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
495        write!(fmt, "Fees {{ ")?;
496        write!(fmt, "gas_price: {:?}, ", self.gas_price)?;
497        write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?;
498        write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?;
499        write!(fmt, "}}")?;
500        Ok(())
501    }
502}