1use std::{
2 collections::BTreeMap,
3 fmt,
4 pin::Pin,
5 sync::{Arc, LazyLock},
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
23pub const MAX_FEE_HISTORY_CACHE_SIZE: u64 = 2048u64;
25
26pub const INITIAL_BASE_FEE: u64 = 1_000_000_000;
28
29pub const INITIAL_GAS_PRICE: u128 = 1_875_000_000;
31
32pub const BASE_FEE_CHANGE_DENOMINATOR: u128 = 8;
34
35pub const MIN_SUGGESTED_PRIORITY_FEE: u128 = 1e9 as u128;
37
38#[derive(Clone, Debug)]
40pub struct FeeManager {
41 spec_id: SpecId,
43 blob_params: Arc<RwLock<BlobParams>>,
45 base_fee: Arc<RwLock<u64>>,
49 is_min_priority_fee_enforced: bool,
51 blob_excess_gas_and_price: Arc<RwLock<BlobExcessGasAndPrice>>,
55 gas_price: Arc<RwLock<u128>>,
59 elasticity: Arc<RwLock<f64>>,
60 base_fee_params: BaseFeeParams,
62 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 pub fn tempo_hardfork(&self) -> Option<TempoHardfork> {
97 *self.tempo_hardfork.read()
98 }
99
100 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 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 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 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 pub fn set_gas_price(&self, price: u128) {
146 let mut gas = self.gas_price.write();
147 *gas = price;
148 }
149
150 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 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 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 if self.base_fee() == 0 {
175 return 0;
176 }
177 self.calculate_next_block_base_fee_per_gas(gas_used, gas_limit, last_fee_per_gas)
178 }
179
180 pub(crate) fn calculate_next_block_base_fee_per_gas(
183 &self,
184 gas_used: u64,
185 gas_limit: u64,
186 last_fee_per_gas: u64,
187 ) -> u64 {
188 if !self.is_eip1559() {
189 return 0;
190 }
191 if let Some(hardfork) = self.tempo_hardfork() {
193 return tempo_next_block_base_fee(hardfork, gas_used, last_fee_per_gas);
194 }
195 calc_next_block_base_fee(gas_used, gas_limit, last_fee_per_gas, self.base_fee_params)
196 }
197
198 pub fn get_next_block_blob_base_fee_per_gas(&self) -> u128 {
200 self.blob_params().calc_blob_fee(self.blob_excess_gas_and_price.read().excess_blob_gas)
201 }
202
203 pub fn get_next_block_blob_excess_gas(&self, blob_excess_gas: u64, blob_gas_used: u64) -> u64 {
206 self.blob_params().next_block_excess_blob_gas_osaka(
207 blob_excess_gas,
208 blob_gas_used,
209 self.base_fee(),
210 )
211 }
212
213 pub fn set_blob_params(&self, blob_params: BlobParams) {
215 *self.blob_params.write() = blob_params;
216 }
217
218 pub fn blob_params(&self) -> BlobParams {
220 *self.blob_params.read()
221 }
222}
223
224fn tempo_next_block_base_fee(hardfork: TempoHardfork, gas_used: u64, parent_base_fee: u64) -> u64 {
230 if hardfork.is_t7() {
231 return tempo_t7_next_block_base_fee(parent_base_fee, gas_used);
232 }
233 crate::config::tempo_default_base_fee(hardfork)
234}
235
236pub struct FeeHistoryService<N: Network>
238where
239 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
240{
241 blob_params: BlobParams,
243 new_blocks: ChainNotifications,
245 cache: FeeHistoryCache,
247 fee_history_limit: u64,
249 storage_info: StorageInfo<N>,
251}
252
253impl<N: Network> FeeHistoryService<N>
254where
255 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
256{
257 pub const fn new(
258 blob_params: BlobParams,
259 new_blocks: ChainNotifications,
260 cache: FeeHistoryCache,
261 storage_info: StorageInfo<N>,
262 ) -> Self {
263 Self {
264 blob_params,
265 new_blocks,
266 cache,
267 fee_history_limit: MAX_FEE_HISTORY_CACHE_SIZE,
268 storage_info,
269 }
270 }
271
272 pub const fn fee_history_limit(&self) -> u64 {
274 self.fee_history_limit
275 }
276
277 pub(crate) fn insert_cache_entry_for_block(&self, hash: B256, header: &impl BlockHeader) {
279 let (result, block_number) = self.create_cache_entry(hash, header);
280 self.insert_cache_entry(result, block_number);
281 }
282
283 fn create_cache_entry(
285 &self,
286 hash: B256,
287 header: &impl BlockHeader,
288 ) -> (FeeHistoryCacheItem, Option<u64>) {
289 create_fee_history_cache_item(hash, header, &self.storage_info, self.blob_params)
290 }
291
292 fn insert_cache_entry(&self, item: FeeHistoryCacheItem, block_number: Option<u64>) {
293 insert_fee_history_cache_item(&self.cache, item, block_number, self.fee_history_limit);
294 }
295}
296
297pub(crate) fn insert_fee_history_cache_item(
302 cache: &FeeHistoryCache,
303 item: FeeHistoryCacheItem,
304 block_number: Option<u64>,
305 fee_history_limit: u64,
306) {
307 if let Some(block_number) = block_number {
308 trace!(target: "fees", "insert new history item={:?} for {}", item, block_number);
309 let mut cache = cache.lock();
310 cache.insert(block_number, item);
311
312 while cache.len() as u64 > fee_history_limit {
317 cache.pop_first();
318 }
319 }
320}
321
322static REWARD_PERCENTILES: LazyLock<Vec<f64>> = LazyLock::new(|| {
327 let mut percentile: f64 = 0.0;
328 (0..=200)
329 .map(|_| {
330 let val = percentile;
331 percentile += 0.5;
332 val
333 })
334 .collect()
335});
336
337fn reward_percentiles(transactions: &[(u64, u128)], block_gas_used: f64) -> Vec<u128> {
341 let mut rewards = Vec::with_capacity(REWARD_PERCENTILES.len());
342 let mut transactions = transactions.iter().copied();
343 let Some((mut cumulative_gas, mut current_reward)) = transactions.next() else {
344 return rewards;
345 };
346
347 for &percentile in REWARD_PERCENTILES.iter() {
348 let target_gas = (percentile * block_gas_used / 100f64) as u64;
349 while target_gas > cumulative_gas {
350 let Some((tx_gas_used, effective_reward)) = transactions.next() else { return rewards };
351 cumulative_gas += tx_gas_used;
352 current_reward = effective_reward;
353 }
354 rewards.push(current_reward);
355 }
356
357 rewards
358}
359
360pub(crate) fn create_fee_history_cache_item<N: Network>(
366 hash: B256,
367 header: &impl BlockHeader,
368 storage_info: &StorageInfo<N>,
369 blob_params: BlobParams,
370) -> (FeeHistoryCacheItem, Option<u64>)
371where
372 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
373{
374 let mut block_number: Option<u64> = None;
375 let base_fee = header.base_fee_per_gas().unwrap_or_default();
376 let excess_blob_gas = header.excess_blob_gas().map(|g| g as u128);
377 let blob_gas_used = header.blob_gas_used().map(|g| g as u128);
378 let base_fee_per_blob_gas = header.blob_fee(blob_params);
379
380 let mut item = FeeHistoryCacheItem {
381 block_hash: hash,
382 base_fee: base_fee as u128,
383 gas_used_ratio: 0f64,
384 blob_gas_used_ratio: 0f64,
385 rewards: Vec::new(),
386 excess_blob_gas,
387 base_fee_per_blob_gas,
388 blob_gas_used,
389 };
390
391 let current_block = storage_info.block(hash);
392 let current_receipts = storage_info.receipts(hash);
393
394 if let (Some(block), Some(receipts)) = (current_block, current_receipts) {
395 block_number = Some(block.header.number());
396
397 let gas_used = block.header.gas_used() as f64;
398 let blob_gas_used = block.header.blob_gas_used().map(|g| g as f64);
399 item.gas_used_ratio = gas_used / block.header.gas_limit() as f64;
400 item.blob_gas_used_ratio = blob_gas_used
401 .map(|g| {
402 let max = blob_params.max_blob_gas_per_block() as f64;
403 if max == 0.0 { 0.0 } else { g / max }
404 })
405 .unwrap_or(0.0);
406
407 let mut transactions: Vec<(_, _)> = receipts
409 .iter()
410 .enumerate()
411 .map(|(i, receipt)| {
412 let cumulative = receipt.cumulative_gas_used();
413 let prev_cumulative = if i > 0 { receipts[i - 1].cumulative_gas_used() } else { 0 };
414 let gas_used = cumulative - prev_cumulative;
415 let effective_reward = block
416 .body
417 .transactions
418 .get(i)
419 .map(|tx| tx.as_ref().effective_tip_per_gas(base_fee).unwrap_or(0))
420 .unwrap_or(0);
421
422 (gas_used, effective_reward)
423 })
424 .collect();
425
426 transactions.sort_by_key(|(_, reward)| *reward);
428
429 item.rewards = reward_percentiles(&transactions, gas_used);
430 } else {
431 item.rewards = vec![0; REWARD_PERCENTILES.len()];
432 }
433 (item, block_number)
434}
435
436impl<N: Network> Future for FeeHistoryService<N>
438where
439 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
440{
441 type Output = ();
442
443 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
444 let pin = self.get_mut();
445
446 while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) {
447 if let Some(block) = notification.as_new_block() {
449 pin.insert_cache_entry_for_block(block.hash, block.header.as_ref());
450 }
451 }
452
453 Poll::Pending
454 }
455}
456
457pub type FeeHistoryCache = Arc<Mutex<BTreeMap<u64, FeeHistoryCacheItem>>>;
458
459#[derive(Clone, Debug)]
461pub struct FeeHistoryCacheItem {
462 pub block_hash: B256,
463 pub base_fee: u128,
464 pub gas_used_ratio: f64,
465 pub base_fee_per_blob_gas: Option<u128>,
466 pub blob_gas_used_ratio: f64,
467 pub excess_blob_gas: Option<u128>,
468 pub blob_gas_used: Option<u128>,
469 pub rewards: Vec<u128>,
470}
471
472#[derive(Clone, Default)]
473pub struct FeeDetails {
474 pub gas_price: Option<u128>,
475 pub max_fee_per_gas: Option<u128>,
476 pub max_priority_fee_per_gas: Option<u128>,
477 pub max_fee_per_blob_gas: Option<u128>,
478}
479
480impl FeeDetails {
481 pub const fn zero() -> Self {
483 Self {
484 gas_price: Some(0),
485 max_fee_per_gas: Some(0),
486 max_priority_fee_per_gas: Some(0),
487 max_fee_per_blob_gas: None,
488 }
489 }
490
491 pub const fn or_zero_fees(self) -> Self {
493 let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
494 self;
495
496 let no_fees = gas_price.is_none() && max_fee_per_gas.is_none();
497 let gas_price = if no_fees { Some(0) } else { gas_price };
498 let max_fee_per_gas = if no_fees { Some(0) } else { max_fee_per_gas };
499 let max_fee_per_blob_gas = if no_fees { None } else { max_fee_per_blob_gas };
500
501 Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas }
502 }
503
504 pub const fn split(self) -> (Option<u128>, Option<u128>, Option<u128>, Option<u128>) {
506 let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
507 self;
508 (gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
509 }
510
511 pub fn new(
513 request_gas_price: Option<u128>,
514 request_max_fee: Option<u128>,
515 request_priority: Option<u128>,
516 max_fee_per_blob_gas: Option<u128>,
517 ) -> Result<Self, BlockchainError> {
518 match (request_gas_price, request_max_fee, request_priority, max_fee_per_blob_gas) {
519 (gas_price, None, None, None) => {
520 Ok(Self {
522 gas_price,
523 max_fee_per_gas: gas_price,
524 max_priority_fee_per_gas: gas_price,
525 max_fee_per_blob_gas: None,
526 })
527 }
528 (_, max_fee, max_priority, max_fee_per_blob_gas) => {
529 if let Some(max_priority) = max_priority {
532 let max_fee = max_fee.unwrap_or_default();
533 if max_priority > max_fee {
534 return Err(BlockchainError::InvalidFeeInput);
535 }
536 }
537 Ok(Self {
538 gas_price: max_fee,
539 max_fee_per_gas: max_fee,
540 max_priority_fee_per_gas: max_priority,
541 max_fee_per_blob_gas,
542 })
543 }
544 }
545 }
546}
547
548impl fmt::Debug for FeeDetails {
549 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
550 write!(fmt, "Fees {{ ")?;
551 write!(fmt, "gas_price: {:?}, ", self.gas_price)?;
552 write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?;
553 write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?;
554 write!(fmt, "}}")?;
555 Ok(())
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562
563 fn reward_percentiles_reference(
564 transactions: &[(u64, u128)],
565 block_gas_used: f64,
566 ) -> Vec<u128> {
567 REWARD_PERCENTILES
568 .iter()
569 .filter_map(|&percentile| {
570 let target_gas = (percentile * block_gas_used / 100f64) as u64;
571 let mut cumulative_gas = 0;
572 for (tx_gas_used, effective_reward) in transactions.iter().copied() {
573 cumulative_gas += tx_gas_used;
574 if target_gas <= cumulative_gas {
575 return Some(effective_reward);
576 }
577 }
578 None
579 })
580 .collect()
581 }
582
583 fn assert_reward_percentiles_match(transactions: &mut [(u64, u128)], gas_used: u64) {
584 transactions.sort_by_key(|(_, reward)| *reward);
585 assert_eq!(
586 reward_percentiles(transactions, gas_used as f64),
587 reward_percentiles_reference(transactions, gas_used as f64)
588 );
589 }
590
591 fn fee_manager(spec_id: SpecId) -> FeeManager {
592 FeeManager::new(
593 spec_id,
594 INITIAL_BASE_FEE,
595 true,
596 INITIAL_GAS_PRICE,
597 BlobExcessGasAndPrice::new_with_spec(0, SpecId::CANCUN),
598 BlobParams::cancun(),
599 BaseFeeParams::ethereum(),
600 None,
601 )
602 }
603
604 #[test]
605 fn raw_next_base_fee_respects_london_activation() {
606 let berlin = fee_manager(SpecId::BERLIN);
607 assert_eq!(
608 berlin.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
609 0
610 );
611
612 let london = fee_manager(SpecId::LONDON);
613 assert_ne!(
614 london.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
615 0
616 );
617 }
618
619 #[test]
620 fn reward_percentile_sweep_preserves_boundaries_and_empty_results() {
621 assert_reward_percentiles_match(&mut [], 0);
622
623 let mut transactions = [(5, 1), (0, 2), (5, 3)];
624 assert_reward_percentiles_match(&mut transactions, 1_000);
625 assert_eq!(reward_percentiles(&transactions, 1_000f64), [1, 1, 3]);
626
627 let mut transactions = [(0, 10), (1, 20)];
628 assert_reward_percentiles_match(&mut transactions, 1);
629 let rewards = reward_percentiles(&transactions, 1f64);
630 assert_eq!(&rewards[..200], &[10; 200]);
631 assert_eq!(rewards[200], 20);
632 }
633
634 #[test]
635 fn reward_percentile_sweep_matches_reference_for_randomized_inputs() {
636 let mut state = 0x4d59_5df4_d0f3_3173u64;
637 for _ in 0..2_000 {
638 let len = (next_random(&mut state) % 129) as usize;
639 let mut transactions = (0..len)
640 .map(|_| {
641 let gas_used = next_random(&mut state) % 100;
642 let effective_reward = (next_random(&mut state) % 16) as u128;
643 (gas_used, effective_reward)
644 })
645 .collect::<Vec<_>>();
646 let total_gas = transactions.iter().map(|(gas_used, _)| gas_used).sum::<u64>();
647 let header_gas_used = match next_random(&mut state) % 4 {
648 0 => total_gas,
649 1 => next_random(&mut state) % (total_gas.saturating_add(1)),
650 2 => total_gas.saturating_add(next_random(&mut state) % 1_000),
651 _ => 0,
652 };
653
654 assert_reward_percentiles_match(&mut transactions, header_gas_used);
655 }
656 }
657
658 fn next_random(state: &mut u64) -> u64 {
659 *state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
660 *state
661 }
662}