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