1use crate::eth::{
2 backend::{info::StorageInfo, notifications::ChainNotifications},
3 error::BlockchainError,
4};
5use alloy_consensus::{BlockHeader, Transaction, TxReceipt};
6use alloy_eips::{calc_next_block_base_fee, eip1559::BaseFeeParams, eip7840::BlobParams};
7use alloy_network::Network;
8use alloy_primitives::{B256, Bytes};
9use futures::StreamExt;
10use parking_lot::{Mutex, RwLock};
11use revm::{context_interface::block::BlobExcessGasAndPrice, primitives::hardfork::SpecId};
12use std::{
13 collections::BTreeMap,
14 fmt,
15 pin::Pin,
16 sync::{Arc, LazyLock},
17 task::{Context, Poll},
18};
19use tempo_hardfork::{TempoHardfork, constants::gas::tempo_t7_next_block_base_fee};
20
21#[cfg(feature = "optimism")]
22use foundry_evm::hardfork::OpHardfork;
23
24#[cfg(feature = "optimism")]
25mod optimism;
26
27pub const MAX_FEE_HISTORY_CACHE_SIZE: u64 = 2048u64;
29
30pub(crate) const REWARD_PERCENTILE_RESOLUTION: f64 = 2.0;
32
33static REWARD_PERCENTILES: LazyLock<Vec<f64>> =
38 LazyLock::new(|| (0..=200).map(|index| index as f64 / REWARD_PERCENTILE_RESOLUTION).collect());
39
40pub const INITIAL_BASE_FEE: u64 = 1_000_000_000;
42
43pub const INITIAL_GAS_PRICE: u128 = 1_875_000_000;
45
46pub const BASE_FEE_CHANGE_DENOMINATOR: u128 = 8;
48
49pub const MIN_SUGGESTED_PRIORITY_FEE: u128 = 1e9 as u128;
51
52#[derive(Clone, Debug)]
54pub struct FeeManager {
55 state: Arc<RwLock<FeeState>>,
57 is_min_priority_fee_enforced: bool,
59}
60
61#[derive(Clone, Copy, Debug)]
62struct FeeRules {
63 spec_id: SpecId,
64 base_fee: BaseFeeRules,
65 tempo_hardfork: Option<TempoHardfork>,
67}
68
69#[derive(Clone, Copy, Debug)]
70enum BaseFeeRules {
71 Standard(BaseFeeParams),
72 #[cfg(feature = "optimism")]
73 Optimism {
74 inherited: Option<optimism::OptimismBaseFeeRules>,
75 fallback: BaseFeeParams,
76 },
77}
78
79impl BaseFeeRules {
80 const fn params(self) -> BaseFeeParams {
81 match self {
82 Self::Standard(params) => params,
83 #[cfg(feature = "optimism")]
84 Self::Optimism { inherited, fallback } => {
85 if let Some(rules) = inherited {
86 rules.params()
87 } else {
88 fallback
89 }
90 }
91 }
92 }
93
94 #[allow(clippy::missing_const_for_fn)] 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 pub(crate) base_fee: u64,
166 pub(crate) 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(crate) fn set_next_block_fees(
228 &self,
229 base_fee: u64,
230 blob_excess_gas_and_price: BlobExcessGasAndPrice,
231 ) {
232 trace!(
233 target: "backend::fees",
234 ?base_fee,
235 ?blob_excess_gas_and_price,
236 "updated next block fees"
237 );
238 let mut state = self.state.write();
239 state.base_fee = base_fee;
240 state.blob_excess_gas_and_price = blob_excess_gas_and_price;
241 }
242
243 pub fn tempo_hardfork(&self) -> Option<TempoHardfork> {
245 self.state.read().rules.tempo_hardfork
246 }
247
248 pub fn set_execution_rules(
250 &self,
251 spec_id: SpecId,
252 base_fee_params: BaseFeeParams,
253 tempo_hardfork: Option<TempoHardfork>,
254 ) {
255 self.state.write().rules =
256 FeeRules { spec_id, base_fee: BaseFeeRules::Standard(base_fee_params), tempo_hardfork };
257 }
258
259 #[cfg(feature = "optimism")]
261 pub(crate) fn set_optimism_base_fee_rules(&self, extra_data: &[u8]) {
262 let mut state = self.state.write();
263 let fallback = match state.rules.base_fee {
264 BaseFeeRules::Standard(params) | BaseFeeRules::Optimism { fallback: params, .. } => {
265 params
266 }
267 };
268 state.rules.base_fee = BaseFeeRules::Optimism {
269 inherited: optimism::OptimismBaseFeeRules::decode(extra_data),
270 fallback,
271 };
272 }
273
274 #[cfg(feature = "optimism")]
276 pub(crate) fn set_optimism_hardfork(&self, hardfork: OpHardfork) {
277 let mut state = self.state.write();
278 let fallback = state.rules.base_fee.params();
279 state.rules.base_fee = BaseFeeRules::Optimism {
280 inherited: optimism::OptimismBaseFeeRules::for_hardfork(hardfork, fallback),
281 fallback,
282 };
283 }
284
285 pub(crate) fn base_fee_extra_data(&self) -> Bytes {
287 self.state.read().rules.base_fee.extra_data()
288 }
289
290 pub fn elasticity(&self) -> f64 {
291 1f64 / self.state.read().rules.base_fee.params().elasticity_multiplier as f64
292 }
293
294 pub fn is_eip1559(&self) -> bool {
296 self.state.read().rules.spec_id.is_enabled_in(SpecId::LONDON)
297 }
298
299 pub fn is_eip4844(&self) -> bool {
300 self.state.read().rules.spec_id.is_enabled_in(SpecId::CANCUN)
301 }
302
303 pub fn blob_gas_price(&self) -> u128 {
305 let state = self.state.read();
306 if state.rules.spec_id.is_enabled_in(SpecId::CANCUN) {
307 state.blob_excess_gas_and_price.blob_gasprice
308 } else {
309 0
310 }
311 }
312
313 pub fn base_fee(&self) -> u64 {
314 let state = self.state.read();
315 if state.rules.spec_id.is_enabled_in(SpecId::LONDON) { state.base_fee } else { 0 }
316 }
317
318 pub const fn is_min_priority_fee_enforced(&self) -> bool {
319 self.is_min_priority_fee_enforced
320 }
321
322 pub fn raw_gas_price(&self) -> u128 {
324 self.state.read().gas_price
325 }
326
327 pub fn excess_blob_gas_and_price(&self) -> Option<BlobExcessGasAndPrice> {
328 let state = self.state.read();
329 state.rules.spec_id.is_enabled_in(SpecId::CANCUN).then_some(state.blob_excess_gas_and_price)
330 }
331
332 pub fn base_fee_per_blob_gas(&self) -> u128 {
333 let state = self.state.read();
334 if state.rules.spec_id.is_enabled_in(SpecId::CANCUN) {
335 state.blob_excess_gas_and_price.blob_gasprice
336 } else {
337 0
338 }
339 }
340
341 pub fn set_gas_price(&self, price: u128) {
343 self.state.write().gas_price = price;
344 }
345
346 pub fn set_base_fee(&self, fee: u64) {
348 trace!(target: "backend::fees", "updated base fee {:?}", fee);
349 self.state.write().base_fee = fee;
350 }
351
352 pub fn set_blob_excess_gas_and_price(&self, blob_excess_gas_and_price: BlobExcessGasAndPrice) {
354 trace!(target: "backend::fees", "updated blob base fee {:?}", blob_excess_gas_and_price);
355 self.state.write().blob_excess_gas_and_price = blob_excess_gas_and_price;
356 }
357
358 pub fn get_next_block_base_fee_per_gas(
360 &self,
361 gas_used: u64,
362 gas_limit: u64,
363 last_fee_per_gas: u64,
364 ) -> u64 {
365 let state = self.state.read();
366 if !state.rules.spec_id.is_enabled_in(SpecId::LONDON) || state.base_fee == 0 {
370 return 0;
371 }
372 calculate_next_block_base_fee_per_gas(state.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.is_enabled_in(SpecId::LONDON) || 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.is_enabled_in(SpecId::LONDON) || 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.is_enabled_in(SpecId::LONDON) {
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.is_enabled_in(SpecId::LONDON) {
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 let gas_limit = block.header.gas_limit() as f64;
627 item.gas_used_ratio = if gas_limit == 0.0 { 0.0 } else { gas_used / gas_limit };
628 item.blob_gas_used_ratio = blob_gas_used
629 .map(|g| {
630 let max = blob_params.max_blob_gas_per_block() as f64;
631 if max == 0.0 { 0.0 } else { g / max }
632 })
633 .unwrap_or(0.0);
634
635 let mut transactions: Vec<(_, _)> = receipts
637 .iter()
638 .enumerate()
639 .map(|(i, receipt)| {
640 let cumulative = receipt.cumulative_gas_used();
641 let prev_cumulative = if i > 0 { receipts[i - 1].cumulative_gas_used() } else { 0 };
642 let gas_used = cumulative - prev_cumulative;
643 let effective_reward = block
644 .body
645 .transactions
646 .get(i)
647 .map(|tx| tx.as_ref().effective_tip_per_gas(base_fee).unwrap_or(0))
648 .unwrap_or(0);
649
650 (gas_used, effective_reward)
651 })
652 .collect();
653
654 transactions.sort_by_key(|(_, reward)| *reward);
656
657 item.rewards = reward_percentiles(&transactions, gas_used);
658 } else {
659 item.rewards = vec![0; REWARD_PERCENTILES.len()];
660 }
661 (item, block_number)
662}
663
664impl<N: Network> Future for FeeHistoryService<N>
666where
667 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
668{
669 type Output = ();
670
671 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
672 let pin = self.get_mut();
673
674 while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) {
675 if let Some(block) = notification.as_new_block() {
677 pin.insert_cache_entry_for_block(block.hash, block.header.as_ref());
678 }
679 }
680
681 Poll::Pending
682 }
683}
684
685pub type FeeHistoryCache = Arc<Mutex<BTreeMap<u64, FeeHistoryCacheItem>>>;
686
687#[derive(Clone, Debug)]
689pub struct FeeHistoryCacheItem {
690 pub block_hash: B256,
691 pub base_fee: u128,
692 pub gas_used_ratio: f64,
693 pub base_fee_per_blob_gas: Option<u128>,
694 pub blob_gas_used_ratio: f64,
695 pub excess_blob_gas: Option<u128>,
696 pub blob_gas_used: Option<u128>,
697 pub rewards: Vec<u128>,
698}
699
700#[derive(Clone, Default)]
701pub struct FeeDetails {
702 pub gas_price: Option<u128>,
703 pub max_fee_per_gas: Option<u128>,
704 pub max_priority_fee_per_gas: Option<u128>,
705 pub max_fee_per_blob_gas: Option<u128>,
706}
707
708impl FeeDetails {
709 pub const fn zero() -> Self {
711 Self {
712 gas_price: Some(0),
713 max_fee_per_gas: Some(0),
714 max_priority_fee_per_gas: Some(0),
715 max_fee_per_blob_gas: None,
716 }
717 }
718
719 pub const fn or_zero_fees(self) -> Self {
721 let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
722 self;
723
724 let no_fees = gas_price.is_none() && max_fee_per_gas.is_none();
725 let gas_price = if no_fees { Some(0) } else { gas_price };
726 let max_fee_per_gas = if no_fees { Some(0) } else { max_fee_per_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 (Some(_), Some(_), _, _) | (Some(_), _, Some(_), _) => {
747 Err(BlockchainError::ConflictingFeeFields)
748 }
749 (gas_price, None, None, max_fee_per_blob_gas) => {
750 Ok(Self {
752 gas_price,
753 max_fee_per_gas: gas_price,
754 max_priority_fee_per_gas: gas_price,
755 max_fee_per_blob_gas,
756 })
757 }
758 (_, max_fee, max_priority, max_fee_per_blob_gas) => {
759 if let Some(max_priority) = max_priority {
762 let max_fee = max_fee.unwrap_or_default();
763 if max_priority > max_fee {
764 return Err(BlockchainError::InvalidFeeInput);
765 }
766 }
767 Ok(Self {
768 gas_price: max_fee,
769 max_fee_per_gas: max_fee,
770 max_priority_fee_per_gas: max_priority.or(max_fee.map(|_| 0)),
772 max_fee_per_blob_gas,
773 })
774 }
775 }
776 }
777}
778
779impl fmt::Debug for FeeDetails {
780 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
781 write!(fmt, "Fees {{ ")?;
782 write!(fmt, "gas_price: {:?}, ", self.gas_price)?;
783 write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?;
784 write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?;
785 write!(fmt, "}}")?;
786 Ok(())
787 }
788}
789
790#[cfg(test)]
791mod tests {
792 use super::*;
793
794 fn reward_percentiles_reference(
795 transactions: &[(u64, u128)],
796 block_gas_used: f64,
797 ) -> Vec<u128> {
798 REWARD_PERCENTILES
799 .iter()
800 .filter_map(|&percentile| {
801 let target_gas = (percentile * block_gas_used / 100f64) as u64;
802 let mut cumulative_gas = 0;
803 for (tx_gas_used, effective_reward) in transactions.iter().copied() {
804 cumulative_gas += tx_gas_used;
805 if target_gas <= cumulative_gas {
806 return Some(effective_reward);
807 }
808 }
809 None
810 })
811 .collect()
812 }
813
814 fn assert_reward_percentiles_match(transactions: &mut [(u64, u128)], gas_used: u64) {
815 transactions.sort_by_key(|(_, reward)| *reward);
816 assert_eq!(
817 reward_percentiles(transactions, gas_used as f64),
818 reward_percentiles_reference(transactions, gas_used as f64)
819 );
820 }
821
822 fn fee_manager(spec_id: SpecId) -> FeeManager {
823 FeeManager::new(
824 spec_id,
825 INITIAL_BASE_FEE,
826 true,
827 INITIAL_GAS_PRICE,
828 BlobExcessGasAndPrice::new_with_spec(0, SpecId::CANCUN),
829 BlobParams::cancun(),
830 BaseFeeParams::ethereum(),
831 None,
832 )
833 }
834
835 #[test]
836 fn raw_next_base_fee_respects_london_activation() {
837 let parent = alloy_consensus::Header {
838 gas_used: 30_000_000,
839 gas_limit: 30_000_000,
840 base_fee_per_gas: Some(INITIAL_BASE_FEE),
841 ..Default::default()
842 };
843
844 let berlin = fee_manager(SpecId::BERLIN);
845 assert_eq!(berlin.calculate_next_block_base_fee_from_header(&parent), 0);
846
847 let london = fee_manager(SpecId::LONDON);
848 assert_ne!(london.calculate_next_block_base_fee_from_header(&parent), 0);
849 }
850
851 #[cfg(feature = "optimism")]
852 #[test]
853 fn pre_london_parent_fees_preserve_optimism_metadata() {
854 let fees = fee_manager(SpecId::BERLIN);
855 let jovian = [1, 0, 0, 0, 250, 0, 0, 0, 2, 0, 0, 0, 0, 0, 76, 75, 64];
856 fees.set_optimism_base_fee_rules(&jovian);
857 let header = alloy_consensus::Header { extra_data: jovian.into(), ..Default::default() };
858
859 let parent_fees = fees.get_parent_header_fees(&header);
860 assert_eq!(parent_fees.base_fee, 0);
861 assert_eq!(parent_fees.extra_data.as_ref(), jovian);
862 assert_eq!(parent_fees.optimism_jovian, Some(true));
863 }
864
865 #[test]
866 fn fee_details_default_missing_tip_to_zero() {
867 let fees = FeeDetails::new(None, Some(5), None, None).unwrap();
868 assert_eq!(fees.split(), (Some(5), Some(5), Some(0), None));
869 }
870
871 #[test]
872 fn reward_percentile_sweep_preserves_boundaries_and_empty_results() {
873 assert_reward_percentiles_match(&mut [], 0);
874
875 let mut transactions = [(5, 1), (0, 2), (5, 3)];
876 assert_reward_percentiles_match(&mut transactions, 1_000);
877 assert_eq!(reward_percentiles(&transactions, 1_000f64), [1, 1, 3]);
878
879 let mut transactions = [(0, 10), (1, 20)];
880 assert_reward_percentiles_match(&mut transactions, 1);
881 let rewards = reward_percentiles(&transactions, 1f64);
882 assert_eq!(&rewards[..200], &[10; 200]);
883 assert_eq!(rewards[200], 20);
884 }
885
886 #[test]
887 fn reward_percentile_sweep_matches_reference_for_randomized_inputs() {
888 let mut state = 0x4d59_5df4_d0f3_3173u64;
889 for _ in 0..2_000 {
890 let len = (next_random(&mut state) % 129) as usize;
891 let mut transactions = (0..len)
892 .map(|_| {
893 let gas_used = next_random(&mut state) % 100;
894 let effective_reward = (next_random(&mut state) % 16) as u128;
895 (gas_used, effective_reward)
896 })
897 .collect::<Vec<_>>();
898 let total_gas = transactions.iter().map(|(gas_used, _)| gas_used).sum::<u64>();
899 let header_gas_used = match next_random(&mut state) % 4 {
900 0 => total_gas,
901 1 => next_random(&mut state) % (total_gas.saturating_add(1)),
902 2 => total_gas.saturating_add(next_random(&mut state) % 1_000),
903 _ => 0,
904 };
905
906 assert_reward_percentiles_match(&mut transactions, header_gas_used);
907 }
908 }
909
910 fn next_random(state: &mut u64) -> u64 {
911 *state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
912 *state
913 }
914
915 #[test]
916 fn fee_details_reject_gas_price_with_dynamic_fees() {
917 for (max_fee, priority) in [(Some(2), None), (None, Some(1)), (Some(2), Some(1))] {
918 assert!(matches!(
919 FeeDetails::new(Some(2), max_fee, priority, None),
920 Err(BlockchainError::ConflictingFeeFields)
921 ));
922 }
923 }
924
925 #[test]
926 fn fee_details_price_legacy_blob_calls_by_gas_price() {
927 let fees = FeeDetails::new(Some(2), None, None, Some(1)).unwrap();
928 assert_eq!(fees.split(), (Some(2), Some(2), Some(2), Some(1)));
929 }
930}