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