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, Bytes};
13#[cfg(feature = "optimism")]
14use foundry_evm::hardfork::FoundryHardfork;
15use futures::StreamExt;
16use parking_lot::{Mutex, RwLock};
17use revm::{context_interface::block::BlobExcessGasAndPrice, primitives::hardfork::SpecId};
18use tempo_hardfork::{TempoHardfork, constants::gas::tempo_t7_next_block_base_fee};
19
20use crate::eth::{
21 backend::{info::StorageInfo, notifications::ChainNotifications},
22 error::BlockchainError,
23};
24
25#[cfg(feature = "optimism")]
26mod optimism;
27
28pub const MAX_FEE_HISTORY_CACHE_SIZE: u64 = 2048u64;
30
31pub(crate) const REWARD_PERCENTILE_RESOLUTION: f64 = 2.0;
33
34static REWARD_PERCENTILES: LazyLock<Vec<f64>> =
39 LazyLock::new(|| (0..=200).map(|index| index as f64 / REWARD_PERCENTILE_RESOLUTION).collect());
40
41pub const INITIAL_BASE_FEE: u64 = 1_000_000_000;
43
44pub const INITIAL_GAS_PRICE: u128 = 1_875_000_000;
46
47pub const BASE_FEE_CHANGE_DENOMINATOR: u128 = 8;
49
50pub const MIN_SUGGESTED_PRIORITY_FEE: u128 = 1e9 as u128;
52
53#[derive(Clone, Debug)]
55pub struct FeeManager {
56 state: Arc<RwLock<FeeState>>,
58 is_min_priority_fee_enforced: bool,
60}
61
62#[derive(Clone, Copy, Debug)]
63struct FeeRules {
64 spec_id: SpecId,
65 base_fee: BaseFeeRules,
66 tempo_hardfork: Option<TempoHardfork>,
68}
69
70#[derive(Clone, Copy, Debug)]
71enum BaseFeeRules {
72 Standard(BaseFeeParams),
73 #[cfg(feature = "optimism")]
74 Optimism {
75 inherited: Option<optimism::OptimismBaseFeeRules>,
76 fallback: BaseFeeParams,
77 },
78}
79
80impl BaseFeeRules {
81 const fn params(self) -> BaseFeeParams {
82 match self {
83 Self::Standard(params) => params,
84 #[cfg(feature = "optimism")]
85 Self::Optimism { inherited, fallback } => {
86 if let Some(rules) = inherited {
87 rules.params()
88 } else {
89 fallback
90 }
91 }
92 }
93 }
94
95 fn extra_data(self) -> Bytes {
96 match self {
97 Self::Standard(_) => Bytes::new(),
98 #[cfg(feature = "optimism")]
99 Self::Optimism { inherited, .. } => {
100 inherited.map_or_else(Bytes::new, optimism::OptimismBaseFeeRules::extra_data)
101 }
102 }
103 }
104
105 fn parent_header_fees<H: BlockHeader>(self, header: &H) -> ParentHeaderFees {
106 match self {
107 Self::Standard(params) => ParentHeaderFees {
108 base_fee: calc_next_block_base_fee(
109 header.gas_used(),
110 header.gas_limit(),
111 header.base_fee_per_gas().unwrap_or_default(),
112 params,
113 ),
114 ..Default::default()
115 },
116 #[cfg(feature = "optimism")]
117 Self::Optimism { fallback, .. } => {
118 let inherited = optimism::OptimismBaseFeeRules::decode(header.extra_data());
119 ParentHeaderFees {
120 base_fee: inherited.map_or_else(
121 || {
122 calc_next_block_base_fee(
123 header.gas_used(),
124 header.gas_limit(),
125 header.base_fee_per_gas().unwrap_or_default(),
126 fallback,
127 )
128 },
129 |rules| rules.next_block_base_fee(header),
130 ),
131 extra_data: inherited
132 .map_or_else(Bytes::new, optimism::OptimismBaseFeeRules::extra_data),
133 optimism_jovian: inherited.map(optimism::OptimismBaseFeeRules::is_jovian),
134 }
135 }
136 }
137 }
138}
139
140#[derive(Clone, Debug, Default)]
141pub(crate) struct ParentHeaderFees {
142 pub(crate) base_fee: u64,
144 pub(crate) extra_data: Bytes,
146 pub(crate) optimism_jovian: Option<bool>,
148}
149
150#[derive(Clone, Copy, Debug)]
151struct FeeState {
152 rules: FeeRules,
153 blob_params: BlobParams,
154 base_fee: u64,
156 blob_excess_gas_and_price: BlobExcessGasAndPrice,
158 gas_price: u128,
160}
161
162#[derive(Clone, Copy, Debug)]
164pub(crate) struct FeeSnapshot {
165 base_fee: u64,
166 blob_excess_gas_and_price: BlobExcessGasAndPrice,
167}
168
169impl FeeManager {
170 #[allow(clippy::too_many_arguments)]
171 pub fn new(
172 spec_id: SpecId,
173 base_fee: u64,
174 is_min_priority_fee_enforced: bool,
175 gas_price: u128,
176 blob_excess_gas_and_price: BlobExcessGasAndPrice,
177 blob_params: BlobParams,
178 base_fee_params: BaseFeeParams,
179 tempo_hardfork: Option<TempoHardfork>,
180 ) -> Self {
181 Self {
182 state: Arc::new(RwLock::new(FeeState {
183 rules: FeeRules {
184 spec_id,
185 base_fee: BaseFeeRules::Standard(base_fee_params),
186 tempo_hardfork,
187 },
188 blob_params,
189 base_fee,
190 blob_excess_gas_and_price,
191 gas_price,
192 })),
193 is_min_priority_fee_enforced,
194 }
195 }
196
197 pub(crate) fn detached(&self) -> Self {
199 Self {
200 state: Arc::new(RwLock::new(*self.state.read())),
201 is_min_priority_fee_enforced: self.is_min_priority_fee_enforced,
202 }
203 }
204
205 pub(crate) fn replace_from(&self, other: &Self) {
207 *self.state.write() = *other.state.read();
208 }
209
210 pub(crate) fn snapshot(&self) -> FeeSnapshot {
212 let state = self.state.read();
213 FeeSnapshot {
214 base_fee: state.base_fee,
215 blob_excess_gas_and_price: state.blob_excess_gas_and_price,
216 }
217 }
218
219 pub(crate) fn restore(&self, snapshot: FeeSnapshot) {
221 let mut state = self.state.write();
222 state.base_fee = snapshot.base_fee;
223 state.blob_excess_gas_and_price = snapshot.blob_excess_gas_and_price;
224 }
225
226 pub fn tempo_hardfork(&self) -> Option<TempoHardfork> {
228 self.state.read().rules.tempo_hardfork
229 }
230
231 pub fn set_execution_rules(
233 &self,
234 spec_id: SpecId,
235 base_fee_params: BaseFeeParams,
236 tempo_hardfork: Option<TempoHardfork>,
237 ) {
238 self.state.write().rules =
239 FeeRules { spec_id, base_fee: BaseFeeRules::Standard(base_fee_params), tempo_hardfork };
240 }
241
242 #[cfg(feature = "optimism")]
244 pub(crate) fn set_optimism_base_fee_rules(&self, extra_data: &[u8]) {
245 let mut state = self.state.write();
246 let fallback = match state.rules.base_fee {
247 BaseFeeRules::Standard(params) | BaseFeeRules::Optimism { fallback: params, .. } => {
248 params
249 }
250 };
251 state.rules.base_fee = BaseFeeRules::Optimism {
252 inherited: optimism::OptimismBaseFeeRules::decode(extra_data),
253 fallback,
254 };
255 }
256
257 #[cfg(feature = "optimism")]
259 pub(crate) fn set_optimism_hardfork(&self, hardfork: FoundryHardfork) {
260 let mut state = self.state.write();
261 let fallback = state.rules.base_fee.params();
262 state.rules.base_fee = BaseFeeRules::Optimism {
263 inherited: optimism::OptimismBaseFeeRules::for_hardfork(hardfork, fallback),
264 fallback,
265 };
266 }
267
268 pub(crate) fn base_fee_extra_data(&self) -> Bytes {
270 self.state.read().rules.base_fee.extra_data()
271 }
272
273 pub fn elasticity(&self) -> f64 {
274 1f64 / self.state.read().rules.base_fee.params().elasticity_multiplier as f64
275 }
276
277 pub fn is_eip1559(&self) -> bool {
279 (self.state.read().rules.spec_id as u8) >= (SpecId::LONDON as u8)
280 }
281
282 pub fn is_eip4844(&self) -> bool {
283 (self.state.read().rules.spec_id as u8) >= (SpecId::CANCUN as u8)
284 }
285
286 pub fn blob_gas_price(&self) -> u128 {
288 let state = self.state.read();
289 if (state.rules.spec_id as u8) >= (SpecId::CANCUN as u8) {
290 state.blob_excess_gas_and_price.blob_gasprice
291 } else {
292 0
293 }
294 }
295
296 pub fn base_fee(&self) -> u64 {
297 let state = self.state.read();
298 if (state.rules.spec_id as u8) >= (SpecId::LONDON as u8) { state.base_fee } else { 0 }
299 }
300
301 pub const fn is_min_priority_fee_enforced(&self) -> bool {
302 self.is_min_priority_fee_enforced
303 }
304
305 pub fn raw_gas_price(&self) -> u128 {
307 self.state.read().gas_price
308 }
309
310 pub fn excess_blob_gas_and_price(&self) -> Option<BlobExcessGasAndPrice> {
311 let state = self.state.read();
312 ((state.rules.spec_id as u8) >= (SpecId::CANCUN as u8))
313 .then_some(state.blob_excess_gas_and_price)
314 }
315
316 pub fn base_fee_per_blob_gas(&self) -> u128 {
317 let state = self.state.read();
318 if (state.rules.spec_id as u8) >= (SpecId::CANCUN as u8) {
319 state.blob_excess_gas_and_price.blob_gasprice
320 } else {
321 0
322 }
323 }
324
325 pub fn set_gas_price(&self, price: u128) {
327 self.state.write().gas_price = price;
328 }
329
330 pub fn set_base_fee(&self, fee: u64) {
332 trace!(target: "backend::fees", "updated base fee {:?}", fee);
333 self.state.write().base_fee = fee;
334 }
335
336 pub fn set_blob_excess_gas_and_price(&self, blob_excess_gas_and_price: BlobExcessGasAndPrice) {
338 trace!(target: "backend::fees", "updated blob base fee {:?}", blob_excess_gas_and_price);
339 self.state.write().blob_excess_gas_and_price = blob_excess_gas_and_price;
340 }
341
342 pub fn get_next_block_base_fee_per_gas(
344 &self,
345 gas_used: u64,
346 gas_limit: u64,
347 last_fee_per_gas: u64,
348 ) -> u64 {
349 let state = self.state.read();
350 if (state.rules.spec_id as u8) < (SpecId::LONDON as u8) || state.base_fee == 0 {
354 return 0;
355 }
356 calculate_next_block_base_fee_per_gas(state.rules, gas_used, gas_limit, last_fee_per_gas)
357 }
358
359 #[cfg(test)]
362 pub(crate) fn calculate_next_block_base_fee_per_gas(
363 &self,
364 gas_used: u64,
365 gas_limit: u64,
366 last_fee_per_gas: u64,
367 ) -> u64 {
368 let rules = self.state.read().rules;
369 if (rules.spec_id as u8) < (SpecId::LONDON as u8) {
370 return 0;
371 }
372 calculate_next_block_base_fee_per_gas(rules, gas_used, gas_limit, last_fee_per_gas)
373 }
374
375 pub(crate) fn get_next_block_base_fee_from_header<H: BlockHeader>(&self, header: &H) -> u64 {
377 let state = self.state.read();
378 if (state.rules.spec_id as u8) < (SpecId::LONDON as u8) || state.base_fee == 0 {
379 return 0;
380 }
381 calculate_parent_header_fees(state.rules, header).base_fee
382 }
383
384 pub(crate) fn get_parent_header_fees<H: BlockHeader>(&self, header: &H) -> ParentHeaderFees {
387 let state = self.state.read();
388 let mut fees = calculate_parent_header_fees(state.rules, header);
389 if (state.rules.spec_id as u8) < (SpecId::LONDON as u8) || state.base_fee == 0 {
390 fees.base_fee = 0;
391 }
392 fees
393 }
394
395 pub(crate) fn calculate_next_block_base_fee_from_header<H: BlockHeader>(
398 &self,
399 header: &H,
400 ) -> u64 {
401 let rules = self.state.read().rules;
402 if (rules.spec_id as u8) < (SpecId::LONDON as u8) {
403 return 0;
404 }
405 calculate_parent_header_fees(rules, header).base_fee
406 }
407
408 pub(crate) fn calculate_parent_header_fees<H: BlockHeader>(
411 &self,
412 header: &H,
413 ) -> ParentHeaderFees {
414 let rules = self.state.read().rules;
415 let mut fees = calculate_parent_header_fees(rules, header);
416 if (rules.spec_id as u8) < (SpecId::LONDON as u8) {
417 fees.base_fee = 0;
418 }
419 fees
420 }
421
422 pub fn get_next_block_blob_base_fee_per_gas(&self) -> u128 {
424 let state = self.state.read();
425 state.blob_params.calc_blob_fee(state.blob_excess_gas_and_price.excess_blob_gas)
426 }
427
428 pub fn set_blob_params(&self, blob_params: BlobParams) {
430 self.state.write().blob_params = blob_params;
431 }
432
433 pub fn blob_params(&self) -> BlobParams {
435 self.state.read().blob_params
436 }
437}
438
439fn calculate_next_block_base_fee_per_gas(
440 rules: FeeRules,
441 gas_used: u64,
442 gas_limit: u64,
443 last_fee_per_gas: u64,
444) -> u64 {
445 if let Some(hardfork) = rules.tempo_hardfork {
447 return tempo_next_block_base_fee(hardfork, gas_used, last_fee_per_gas);
448 }
449 calc_next_block_base_fee(gas_used, gas_limit, last_fee_per_gas, rules.base_fee.params())
450}
451
452fn calculate_parent_header_fees<H: BlockHeader>(rules: FeeRules, header: &H) -> ParentHeaderFees {
453 if let Some(hardfork) = rules.tempo_hardfork {
454 return ParentHeaderFees {
455 base_fee: tempo_next_block_base_fee(
456 hardfork,
457 header.gas_used(),
458 header.base_fee_per_gas().unwrap_or_default(),
459 ),
460 ..Default::default()
461 };
462 }
463 rules.base_fee.parent_header_fees(header)
464}
465
466fn tempo_next_block_base_fee(hardfork: TempoHardfork, gas_used: u64, parent_base_fee: u64) -> u64 {
472 if hardfork.is_t7() {
473 return tempo_t7_next_block_base_fee(parent_base_fee, gas_used);
474 }
475 crate::config::tempo_default_base_fee(hardfork)
476}
477
478pub struct FeeHistoryService<N: Network>
480where
481 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
482{
483 fees: FeeManager,
485 new_blocks: ChainNotifications,
487 cache: FeeHistoryCache,
489 fee_history_limit: u64,
491 storage_info: StorageInfo<N>,
493}
494
495impl<N: Network> FeeHistoryService<N>
496where
497 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
498{
499 pub const fn new(
500 fees: FeeManager,
501 new_blocks: ChainNotifications,
502 cache: FeeHistoryCache,
503 storage_info: StorageInfo<N>,
504 ) -> Self {
505 Self {
506 fees,
507 new_blocks,
508 cache,
509 fee_history_limit: MAX_FEE_HISTORY_CACHE_SIZE,
510 storage_info,
511 }
512 }
513
514 pub const fn fee_history_limit(&self) -> u64 {
516 self.fee_history_limit
517 }
518
519 pub(crate) fn insert_cache_entry_for_block(&self, hash: B256, header: &impl BlockHeader) {
521 let (result, block_number) = self.create_cache_entry(hash, header);
522 self.insert_cache_entry(result, block_number);
523 }
524
525 fn create_cache_entry(
527 &self,
528 hash: B256,
529 header: &impl BlockHeader,
530 ) -> (FeeHistoryCacheItem, Option<u64>) {
531 create_fee_history_cache_item(hash, header, &self.storage_info, self.fees.blob_params())
532 }
533
534 fn insert_cache_entry(&self, item: FeeHistoryCacheItem, block_number: Option<u64>) {
535 insert_fee_history_cache_item(&self.cache, item, block_number, self.fee_history_limit);
536 }
537}
538
539pub(crate) fn insert_fee_history_cache_item(
544 cache: &FeeHistoryCache,
545 item: FeeHistoryCacheItem,
546 block_number: Option<u64>,
547 fee_history_limit: u64,
548) {
549 if let Some(block_number) = block_number {
550 trace!(target: "fees", "insert new history item={:?} for {}", item, block_number);
551 let mut cache = cache.lock();
552 cache.insert(block_number, item);
553
554 while cache.len() as u64 > fee_history_limit {
559 cache.pop_first();
560 }
561 }
562}
563
564fn reward_percentiles(transactions: &[(u64, u128)], block_gas_used: f64) -> Vec<u128> {
568 let mut rewards = Vec::with_capacity(REWARD_PERCENTILES.len());
569 let mut transactions = transactions.iter().copied();
570 let Some((mut cumulative_gas, mut current_reward)) = transactions.next() else {
571 return rewards;
572 };
573
574 for &percentile in REWARD_PERCENTILES.iter() {
575 let target_gas = (percentile * block_gas_used / 100f64) as u64;
576 while target_gas > cumulative_gas {
577 let Some((tx_gas_used, effective_reward)) = transactions.next() else { return rewards };
578 cumulative_gas += tx_gas_used;
579 current_reward = effective_reward;
580 }
581 rewards.push(current_reward);
582 }
583
584 rewards
585}
586
587pub(crate) fn create_fee_history_cache_item<N: Network>(
593 hash: B256,
594 header: &impl BlockHeader,
595 storage_info: &StorageInfo<N>,
596 blob_params: BlobParams,
597) -> (FeeHistoryCacheItem, Option<u64>)
598where
599 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
600{
601 let mut block_number: Option<u64> = None;
602 let base_fee = header.base_fee_per_gas().unwrap_or_default();
603 let excess_blob_gas = header.excess_blob_gas().map(|g| g as u128);
604 let blob_gas_used = header.blob_gas_used().map(|g| g as u128);
605 let base_fee_per_blob_gas = header.blob_fee(blob_params);
606
607 let mut item = FeeHistoryCacheItem {
608 block_hash: hash,
609 base_fee: base_fee as u128,
610 gas_used_ratio: 0f64,
611 blob_gas_used_ratio: 0f64,
612 rewards: Vec::new(),
613 excess_blob_gas,
614 base_fee_per_blob_gas,
615 blob_gas_used,
616 };
617
618 let current_block = storage_info.block(hash);
619 let current_receipts = storage_info.receipts(hash);
620
621 if let (Some(block), Some(receipts)) = (current_block, current_receipts) {
622 block_number = Some(block.header.number());
623
624 let gas_used = block.header.gas_used() as f64;
625 let blob_gas_used = block.header.blob_gas_used().map(|g| g as f64);
626 item.gas_used_ratio = gas_used / block.header.gas_limit() as f64;
627 item.blob_gas_used_ratio = blob_gas_used
628 .map(|g| {
629 let max = blob_params.max_blob_gas_per_block() as f64;
630 if max == 0.0 { 0.0 } else { g / max }
631 })
632 .unwrap_or(0.0);
633
634 let mut transactions: Vec<(_, _)> = receipts
636 .iter()
637 .enumerate()
638 .map(|(i, receipt)| {
639 let cumulative = receipt.cumulative_gas_used();
640 let prev_cumulative = if i > 0 { receipts[i - 1].cumulative_gas_used() } else { 0 };
641 let gas_used = cumulative - prev_cumulative;
642 let effective_reward = block
643 .body
644 .transactions
645 .get(i)
646 .map(|tx| tx.as_ref().effective_tip_per_gas(base_fee).unwrap_or(0))
647 .unwrap_or(0);
648
649 (gas_used, effective_reward)
650 })
651 .collect();
652
653 transactions.sort_by_key(|(_, reward)| *reward);
655
656 item.rewards = reward_percentiles(&transactions, gas_used);
657 } else {
658 item.rewards = vec![0; REWARD_PERCENTILES.len()];
659 }
660 (item, block_number)
661}
662
663impl<N: Network> Future for FeeHistoryService<N>
665where
666 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
667{
668 type Output = ();
669
670 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
671 let pin = self.get_mut();
672
673 while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) {
674 if let Some(block) = notification.as_new_block() {
676 pin.insert_cache_entry_for_block(block.hash, block.header.as_ref());
677 }
678 }
679
680 Poll::Pending
681 }
682}
683
684pub type FeeHistoryCache = Arc<Mutex<BTreeMap<u64, FeeHistoryCacheItem>>>;
685
686#[derive(Clone, Debug)]
688pub struct FeeHistoryCacheItem {
689 pub block_hash: B256,
690 pub base_fee: u128,
691 pub gas_used_ratio: f64,
692 pub base_fee_per_blob_gas: Option<u128>,
693 pub blob_gas_used_ratio: f64,
694 pub excess_blob_gas: Option<u128>,
695 pub blob_gas_used: Option<u128>,
696 pub rewards: Vec<u128>,
697}
698
699#[derive(Clone, Default)]
700pub struct FeeDetails {
701 pub gas_price: Option<u128>,
702 pub max_fee_per_gas: Option<u128>,
703 pub max_priority_fee_per_gas: Option<u128>,
704 pub max_fee_per_blob_gas: Option<u128>,
705}
706
707impl FeeDetails {
708 pub const fn zero() -> Self {
710 Self {
711 gas_price: Some(0),
712 max_fee_per_gas: Some(0),
713 max_priority_fee_per_gas: Some(0),
714 max_fee_per_blob_gas: None,
715 }
716 }
717
718 pub const fn or_zero_fees(self) -> Self {
720 let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
721 self;
722
723 let no_fees = gas_price.is_none() && max_fee_per_gas.is_none();
724 let gas_price = if no_fees { Some(0) } else { gas_price };
725 let max_fee_per_gas = if no_fees { Some(0) } else { max_fee_per_gas };
726 let max_fee_per_blob_gas = if no_fees { None } else { max_fee_per_blob_gas };
727
728 Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas }
729 }
730
731 pub const fn split(self) -> (Option<u128>, Option<u128>, Option<u128>, Option<u128>) {
733 let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
734 self;
735 (gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
736 }
737
738 pub fn new(
740 request_gas_price: Option<u128>,
741 request_max_fee: Option<u128>,
742 request_priority: Option<u128>,
743 max_fee_per_blob_gas: Option<u128>,
744 ) -> Result<Self, BlockchainError> {
745 match (request_gas_price, request_max_fee, request_priority, max_fee_per_blob_gas) {
746 (gas_price, None, None, None) => {
747 Ok(Self {
749 gas_price,
750 max_fee_per_gas: gas_price,
751 max_priority_fee_per_gas: gas_price,
752 max_fee_per_blob_gas: None,
753 })
754 }
755 (_, max_fee, max_priority, max_fee_per_blob_gas) => {
756 if let Some(max_priority) = max_priority {
759 let max_fee = max_fee.unwrap_or_default();
760 if max_priority > max_fee {
761 return Err(BlockchainError::InvalidFeeInput);
762 }
763 }
764 Ok(Self {
765 gas_price: max_fee,
766 max_fee_per_gas: max_fee,
767 max_priority_fee_per_gas: max_priority,
768 max_fee_per_blob_gas,
769 })
770 }
771 }
772 }
773}
774
775impl fmt::Debug for FeeDetails {
776 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
777 write!(fmt, "Fees {{ ")?;
778 write!(fmt, "gas_price: {:?}, ", self.gas_price)?;
779 write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?;
780 write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?;
781 write!(fmt, "}}")?;
782 Ok(())
783 }
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789
790 fn reward_percentiles_reference(
791 transactions: &[(u64, u128)],
792 block_gas_used: f64,
793 ) -> Vec<u128> {
794 REWARD_PERCENTILES
795 .iter()
796 .filter_map(|&percentile| {
797 let target_gas = (percentile * block_gas_used / 100f64) as u64;
798 let mut cumulative_gas = 0;
799 for (tx_gas_used, effective_reward) in transactions.iter().copied() {
800 cumulative_gas += tx_gas_used;
801 if target_gas <= cumulative_gas {
802 return Some(effective_reward);
803 }
804 }
805 None
806 })
807 .collect()
808 }
809
810 fn assert_reward_percentiles_match(transactions: &mut [(u64, u128)], gas_used: u64) {
811 transactions.sort_by_key(|(_, reward)| *reward);
812 assert_eq!(
813 reward_percentiles(transactions, gas_used as f64),
814 reward_percentiles_reference(transactions, gas_used as f64)
815 );
816 }
817
818 fn fee_manager(spec_id: SpecId) -> FeeManager {
819 FeeManager::new(
820 spec_id,
821 INITIAL_BASE_FEE,
822 true,
823 INITIAL_GAS_PRICE,
824 BlobExcessGasAndPrice::new_with_spec(0, SpecId::CANCUN),
825 BlobParams::cancun(),
826 BaseFeeParams::ethereum(),
827 None,
828 )
829 }
830
831 #[test]
832 fn raw_next_base_fee_respects_london_activation() {
833 let berlin = fee_manager(SpecId::BERLIN);
834 assert_eq!(
835 berlin.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
836 0
837 );
838
839 let london = fee_manager(SpecId::LONDON);
840 assert_ne!(
841 london.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
842 0
843 );
844 }
845
846 #[cfg(feature = "optimism")]
847 #[test]
848 fn pre_london_parent_fees_preserve_optimism_metadata() {
849 let fees = fee_manager(SpecId::BERLIN);
850 let jovian = [1, 0, 0, 0, 250, 0, 0, 0, 2, 0, 0, 0, 0, 0, 76, 75, 64];
851 fees.set_optimism_base_fee_rules(&jovian);
852 let header = alloy_consensus::Header {
853 extra_data: Bytes::copy_from_slice(&jovian),
854 ..Default::default()
855 };
856
857 let parent_fees = fees.get_parent_header_fees(&header);
858 assert_eq!(parent_fees.base_fee, 0);
859 assert_eq!(parent_fees.extra_data.as_ref(), jovian);
860 assert_eq!(parent_fees.optimism_jovian, Some(true));
861 }
862
863 #[test]
864 fn reward_percentile_sweep_preserves_boundaries_and_empty_results() {
865 assert_reward_percentiles_match(&mut [], 0);
866
867 let mut transactions = [(5, 1), (0, 2), (5, 3)];
868 assert_reward_percentiles_match(&mut transactions, 1_000);
869 assert_eq!(reward_percentiles(&transactions, 1_000f64), [1, 1, 3]);
870
871 let mut transactions = [(0, 10), (1, 20)];
872 assert_reward_percentiles_match(&mut transactions, 1);
873 let rewards = reward_percentiles(&transactions, 1f64);
874 assert_eq!(&rewards[..200], &[10; 200]);
875 assert_eq!(rewards[200], 20);
876 }
877
878 #[test]
879 fn reward_percentile_sweep_matches_reference_for_randomized_inputs() {
880 let mut state = 0x4d59_5df4_d0f3_3173u64;
881 for _ in 0..2_000 {
882 let len = (next_random(&mut state) % 129) as usize;
883 let mut transactions = (0..len)
884 .map(|_| {
885 let gas_used = next_random(&mut state) % 100;
886 let effective_reward = (next_random(&mut state) % 16) as u128;
887 (gas_used, effective_reward)
888 })
889 .collect::<Vec<_>>();
890 let total_gas = transactions.iter().map(|(gas_used, _)| gas_used).sum::<u64>();
891 let header_gas_used = match next_random(&mut state) % 4 {
892 0 => total_gas,
893 1 => next_random(&mut state) % (total_gas.saturating_add(1)),
894 2 => total_gas.saturating_add(next_random(&mut state) % 1_000),
895 _ => 0,
896 };
897
898 assert_reward_percentiles_match(&mut transactions, header_gas_used);
899 }
900 }
901
902 fn next_random(state: &mut u64) -> u64 {
903 *state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
904 *state
905 }
906}