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