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 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(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 as u8) >= (SpecId::LONDON as u8)
297 }
298
299 pub fn is_eip4844(&self) -> bool {
300 (self.state.read().rules.spec_id as u8) >= (SpecId::CANCUN as u8)
301 }
302
303 pub fn blob_gas_price(&self) -> u128 {
305 let state = self.state.read();
306 if (state.rules.spec_id as u8) >= (SpecId::CANCUN as u8) {
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 as u8) >= (SpecId::LONDON as u8) { 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 as u8) >= (SpecId::CANCUN as u8))
330 .then_some(state.blob_excess_gas_and_price)
331 }
332
333 pub fn base_fee_per_blob_gas(&self) -> u128 {
334 let state = self.state.read();
335 if (state.rules.spec_id as u8) >= (SpecId::CANCUN as u8) {
336 state.blob_excess_gas_and_price.blob_gasprice
337 } else {
338 0
339 }
340 }
341
342 pub fn set_gas_price(&self, price: u128) {
344 self.state.write().gas_price = price;
345 }
346
347 pub fn set_base_fee(&self, fee: u64) {
349 trace!(target: "backend::fees", "updated base fee {:?}", fee);
350 self.state.write().base_fee = fee;
351 }
352
353 pub fn set_blob_excess_gas_and_price(&self, blob_excess_gas_and_price: BlobExcessGasAndPrice) {
355 trace!(target: "backend::fees", "updated blob base fee {:?}", blob_excess_gas_and_price);
356 self.state.write().blob_excess_gas_and_price = blob_excess_gas_and_price;
357 }
358
359 pub fn get_next_block_base_fee_per_gas(
361 &self,
362 gas_used: u64,
363 gas_limit: u64,
364 last_fee_per_gas: u64,
365 ) -> u64 {
366 let state = self.state.read();
367 if (state.rules.spec_id as u8) < (SpecId::LONDON as u8) || state.base_fee == 0 {
371 return 0;
372 }
373 calculate_next_block_base_fee_per_gas(state.rules, gas_used, gas_limit, last_fee_per_gas)
374 }
375
376 #[cfg(test)]
379 pub(crate) fn calculate_next_block_base_fee_per_gas(
380 &self,
381 gas_used: u64,
382 gas_limit: u64,
383 last_fee_per_gas: u64,
384 ) -> u64 {
385 let rules = self.state.read().rules;
386 if (rules.spec_id as u8) < (SpecId::LONDON as u8) {
387 return 0;
388 }
389 calculate_next_block_base_fee_per_gas(rules, gas_used, gas_limit, last_fee_per_gas)
390 }
391
392 pub(crate) fn get_next_block_base_fee_from_header<H: BlockHeader>(&self, header: &H) -> u64 {
394 let state = self.state.read();
395 if (state.rules.spec_id as u8) < (SpecId::LONDON as u8) || state.base_fee == 0 {
396 return 0;
397 }
398 calculate_parent_header_fees(state.rules, header).base_fee
399 }
400
401 pub(crate) fn get_parent_header_fees<H: BlockHeader>(&self, header: &H) -> ParentHeaderFees {
404 let state = self.state.read();
405 let mut fees = calculate_parent_header_fees(state.rules, header);
406 if (state.rules.spec_id as u8) < (SpecId::LONDON as u8) || state.base_fee == 0 {
407 fees.base_fee = 0;
408 }
409 fees
410 }
411
412 pub(crate) fn calculate_next_block_base_fee_from_header<H: BlockHeader>(
415 &self,
416 header: &H,
417 ) -> u64 {
418 let rules = self.state.read().rules;
419 if (rules.spec_id as u8) < (SpecId::LONDON as u8) {
420 return 0;
421 }
422 calculate_parent_header_fees(rules, header).base_fee
423 }
424
425 pub(crate) fn calculate_parent_header_fees<H: BlockHeader>(
428 &self,
429 header: &H,
430 ) -> ParentHeaderFees {
431 let rules = self.state.read().rules;
432 let mut fees = calculate_parent_header_fees(rules, header);
433 if (rules.spec_id as u8) < (SpecId::LONDON as u8) {
434 fees.base_fee = 0;
435 }
436 fees
437 }
438
439 pub fn get_next_block_blob_base_fee_per_gas(&self) -> u128 {
441 let state = self.state.read();
442 state.blob_params.calc_blob_fee(state.blob_excess_gas_and_price.excess_blob_gas)
443 }
444
445 pub fn set_blob_params(&self, blob_params: BlobParams) {
447 self.state.write().blob_params = blob_params;
448 }
449
450 pub fn blob_params(&self) -> BlobParams {
452 self.state.read().blob_params
453 }
454}
455
456fn calculate_next_block_base_fee_per_gas(
457 rules: FeeRules,
458 gas_used: u64,
459 gas_limit: u64,
460 last_fee_per_gas: u64,
461) -> u64 {
462 if let Some(hardfork) = rules.tempo_hardfork {
464 return tempo_next_block_base_fee(hardfork, gas_used, last_fee_per_gas);
465 }
466 calc_next_block_base_fee(gas_used, gas_limit, last_fee_per_gas, rules.base_fee.params())
467}
468
469fn calculate_parent_header_fees<H: BlockHeader>(rules: FeeRules, header: &H) -> ParentHeaderFees {
470 if let Some(hardfork) = rules.tempo_hardfork {
471 return ParentHeaderFees {
472 base_fee: tempo_next_block_base_fee(
473 hardfork,
474 header.gas_used(),
475 header.base_fee_per_gas().unwrap_or_default(),
476 ),
477 ..Default::default()
478 };
479 }
480 rules.base_fee.parent_header_fees(header)
481}
482
483fn tempo_next_block_base_fee(hardfork: TempoHardfork, gas_used: u64, parent_base_fee: u64) -> u64 {
489 if hardfork.is_t7() {
490 return tempo_t7_next_block_base_fee(parent_base_fee, gas_used);
491 }
492 crate::config::tempo_default_base_fee(hardfork)
493}
494
495pub struct FeeHistoryService<N: Network>
497where
498 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
499{
500 fees: FeeManager,
502 new_blocks: ChainNotifications,
504 cache: FeeHistoryCache,
506 fee_history_limit: u64,
508 storage_info: StorageInfo<N>,
510}
511
512impl<N: Network> FeeHistoryService<N>
513where
514 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
515{
516 pub const fn new(
517 fees: FeeManager,
518 new_blocks: ChainNotifications,
519 cache: FeeHistoryCache,
520 storage_info: StorageInfo<N>,
521 ) -> Self {
522 Self {
523 fees,
524 new_blocks,
525 cache,
526 fee_history_limit: MAX_FEE_HISTORY_CACHE_SIZE,
527 storage_info,
528 }
529 }
530
531 pub const fn fee_history_limit(&self) -> u64 {
533 self.fee_history_limit
534 }
535
536 pub(crate) fn insert_cache_entry_for_block(&self, hash: B256, header: &impl BlockHeader) {
538 let (result, block_number) = self.create_cache_entry(hash, header);
539 self.insert_cache_entry(result, block_number);
540 }
541
542 fn create_cache_entry(
544 &self,
545 hash: B256,
546 header: &impl BlockHeader,
547 ) -> (FeeHistoryCacheItem, Option<u64>) {
548 create_fee_history_cache_item(hash, header, &self.storage_info, self.fees.blob_params())
549 }
550
551 fn insert_cache_entry(&self, item: FeeHistoryCacheItem, block_number: Option<u64>) {
552 insert_fee_history_cache_item(&self.cache, item, block_number, self.fee_history_limit);
553 }
554}
555
556pub(crate) fn insert_fee_history_cache_item(
561 cache: &FeeHistoryCache,
562 item: FeeHistoryCacheItem,
563 block_number: Option<u64>,
564 fee_history_limit: u64,
565) {
566 if let Some(block_number) = block_number {
567 trace!(target: "fees", "insert new history item={:?} for {}", item, block_number);
568 let mut cache = cache.lock();
569 cache.insert(block_number, item);
570
571 while cache.len() as u64 > fee_history_limit {
576 cache.pop_first();
577 }
578 }
579}
580
581fn reward_percentiles(transactions: &[(u64, u128)], block_gas_used: f64) -> Vec<u128> {
585 let mut rewards = Vec::with_capacity(REWARD_PERCENTILES.len());
586 let mut transactions = transactions.iter().copied();
587 let Some((mut cumulative_gas, mut current_reward)) = transactions.next() else {
588 return rewards;
589 };
590
591 for &percentile in REWARD_PERCENTILES.iter() {
592 let target_gas = (percentile * block_gas_used / 100f64) as u64;
593 while target_gas > cumulative_gas {
594 let Some((tx_gas_used, effective_reward)) = transactions.next() else { return rewards };
595 cumulative_gas += tx_gas_used;
596 current_reward = effective_reward;
597 }
598 rewards.push(current_reward);
599 }
600
601 rewards
602}
603
604pub(crate) fn create_fee_history_cache_item<N: Network>(
610 hash: B256,
611 header: &impl BlockHeader,
612 storage_info: &StorageInfo<N>,
613 blob_params: BlobParams,
614) -> (FeeHistoryCacheItem, Option<u64>)
615where
616 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
617{
618 let mut block_number: Option<u64> = None;
619 let base_fee = header.base_fee_per_gas().unwrap_or_default();
620 let excess_blob_gas = header.excess_blob_gas().map(|g| g as u128);
621 let blob_gas_used = header.blob_gas_used().map(|g| g as u128);
622 let base_fee_per_blob_gas = header.blob_fee(blob_params);
623
624 let mut item = FeeHistoryCacheItem {
625 block_hash: hash,
626 base_fee: base_fee as u128,
627 gas_used_ratio: 0f64,
628 blob_gas_used_ratio: 0f64,
629 rewards: Vec::new(),
630 excess_blob_gas,
631 base_fee_per_blob_gas,
632 blob_gas_used,
633 };
634
635 let current_block = storage_info.block(hash);
636 let current_receipts = storage_info.receipts(hash);
637
638 if let (Some(block), Some(receipts)) = (current_block, current_receipts) {
639 block_number = Some(block.header.number());
640
641 let gas_used = block.header.gas_used() as f64;
642 let blob_gas_used = block.header.blob_gas_used().map(|g| g as f64);
643 let gas_limit = block.header.gas_limit() as f64;
644 item.gas_used_ratio = if gas_limit == 0.0 { 0.0 } else { gas_used / gas_limit };
645 item.blob_gas_used_ratio = blob_gas_used
646 .map(|g| {
647 let max = blob_params.max_blob_gas_per_block() as f64;
648 if max == 0.0 { 0.0 } else { g / max }
649 })
650 .unwrap_or(0.0);
651
652 let mut transactions: Vec<(_, _)> = receipts
654 .iter()
655 .enumerate()
656 .map(|(i, receipt)| {
657 let cumulative = receipt.cumulative_gas_used();
658 let prev_cumulative = if i > 0 { receipts[i - 1].cumulative_gas_used() } else { 0 };
659 let gas_used = cumulative - prev_cumulative;
660 let effective_reward = block
661 .body
662 .transactions
663 .get(i)
664 .map(|tx| tx.as_ref().effective_tip_per_gas(base_fee).unwrap_or(0))
665 .unwrap_or(0);
666
667 (gas_used, effective_reward)
668 })
669 .collect();
670
671 transactions.sort_by_key(|(_, reward)| *reward);
673
674 item.rewards = reward_percentiles(&transactions, gas_used);
675 } else {
676 item.rewards = vec![0; REWARD_PERCENTILES.len()];
677 }
678 (item, block_number)
679}
680
681impl<N: Network> Future for FeeHistoryService<N>
683where
684 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
685{
686 type Output = ();
687
688 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
689 let pin = self.get_mut();
690
691 while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) {
692 if let Some(block) = notification.as_new_block() {
694 pin.insert_cache_entry_for_block(block.hash, block.header.as_ref());
695 }
696 }
697
698 Poll::Pending
699 }
700}
701
702pub type FeeHistoryCache = Arc<Mutex<BTreeMap<u64, FeeHistoryCacheItem>>>;
703
704#[derive(Clone, Debug)]
706pub struct FeeHistoryCacheItem {
707 pub block_hash: B256,
708 pub base_fee: u128,
709 pub gas_used_ratio: f64,
710 pub base_fee_per_blob_gas: Option<u128>,
711 pub blob_gas_used_ratio: f64,
712 pub excess_blob_gas: Option<u128>,
713 pub blob_gas_used: Option<u128>,
714 pub rewards: Vec<u128>,
715}
716
717#[derive(Clone, Default)]
718pub struct FeeDetails {
719 pub gas_price: Option<u128>,
720 pub max_fee_per_gas: Option<u128>,
721 pub max_priority_fee_per_gas: Option<u128>,
722 pub max_fee_per_blob_gas: Option<u128>,
723}
724
725impl FeeDetails {
726 pub const fn zero() -> Self {
728 Self {
729 gas_price: Some(0),
730 max_fee_per_gas: Some(0),
731 max_priority_fee_per_gas: Some(0),
732 max_fee_per_blob_gas: None,
733 }
734 }
735
736 pub const fn or_zero_fees(self) -> Self {
738 let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
739 self;
740
741 let no_fees = gas_price.is_none() && max_fee_per_gas.is_none();
742 let gas_price = if no_fees { Some(0) } else { gas_price };
743 let max_fee_per_gas = if no_fees { Some(0) } else { max_fee_per_gas };
744
745 Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas }
746 }
747
748 pub const fn split(self) -> (Option<u128>, Option<u128>, Option<u128>, Option<u128>) {
750 let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
751 self;
752 (gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
753 }
754
755 pub fn new(
757 request_gas_price: Option<u128>,
758 request_max_fee: Option<u128>,
759 request_priority: Option<u128>,
760 max_fee_per_blob_gas: Option<u128>,
761 ) -> Result<Self, BlockchainError> {
762 match (request_gas_price, request_max_fee, request_priority, max_fee_per_blob_gas) {
763 (Some(_), Some(_), _, _) | (Some(_), _, Some(_), _) => {
764 Err(BlockchainError::ConflictingFeeFields)
765 }
766 (gas_price, None, None, max_fee_per_blob_gas) => {
767 Ok(Self {
769 gas_price,
770 max_fee_per_gas: gas_price,
771 max_priority_fee_per_gas: gas_price,
772 max_fee_per_blob_gas,
773 })
774 }
775 (_, max_fee, max_priority, max_fee_per_blob_gas) => {
776 if let Some(max_priority) = max_priority {
779 let max_fee = max_fee.unwrap_or_default();
780 if max_priority > max_fee {
781 return Err(BlockchainError::InvalidFeeInput);
782 }
783 }
784 Ok(Self {
785 gas_price: max_fee,
786 max_fee_per_gas: max_fee,
787 max_priority_fee_per_gas: max_priority.or(max_fee.map(|_| 0)),
789 max_fee_per_blob_gas,
790 })
791 }
792 }
793 }
794}
795
796impl fmt::Debug for FeeDetails {
797 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
798 write!(fmt, "Fees {{ ")?;
799 write!(fmt, "gas_price: {:?}, ", self.gas_price)?;
800 write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?;
801 write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?;
802 write!(fmt, "}}")?;
803 Ok(())
804 }
805}
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810
811 fn reward_percentiles_reference(
812 transactions: &[(u64, u128)],
813 block_gas_used: f64,
814 ) -> Vec<u128> {
815 REWARD_PERCENTILES
816 .iter()
817 .filter_map(|&percentile| {
818 let target_gas = (percentile * block_gas_used / 100f64) as u64;
819 let mut cumulative_gas = 0;
820 for (tx_gas_used, effective_reward) in transactions.iter().copied() {
821 cumulative_gas += tx_gas_used;
822 if target_gas <= cumulative_gas {
823 return Some(effective_reward);
824 }
825 }
826 None
827 })
828 .collect()
829 }
830
831 fn assert_reward_percentiles_match(transactions: &mut [(u64, u128)], gas_used: u64) {
832 transactions.sort_by_key(|(_, reward)| *reward);
833 assert_eq!(
834 reward_percentiles(transactions, gas_used as f64),
835 reward_percentiles_reference(transactions, gas_used as f64)
836 );
837 }
838
839 fn fee_manager(spec_id: SpecId) -> FeeManager {
840 FeeManager::new(
841 spec_id,
842 INITIAL_BASE_FEE,
843 true,
844 INITIAL_GAS_PRICE,
845 BlobExcessGasAndPrice::new_with_spec(0, SpecId::CANCUN),
846 BlobParams::cancun(),
847 BaseFeeParams::ethereum(),
848 None,
849 )
850 }
851
852 #[test]
853 fn raw_next_base_fee_respects_london_activation() {
854 let berlin = fee_manager(SpecId::BERLIN);
855 assert_eq!(
856 berlin.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
857 0
858 );
859
860 let london = fee_manager(SpecId::LONDON);
861 assert_ne!(
862 london.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
863 0
864 );
865 }
866
867 #[cfg(feature = "optimism")]
868 #[test]
869 fn pre_london_parent_fees_preserve_optimism_metadata() {
870 let fees = fee_manager(SpecId::BERLIN);
871 let jovian = [1, 0, 0, 0, 250, 0, 0, 0, 2, 0, 0, 0, 0, 0, 76, 75, 64];
872 fees.set_optimism_base_fee_rules(&jovian);
873 let header = alloy_consensus::Header { extra_data: jovian.into(), ..Default::default() };
874
875 let parent_fees = fees.get_parent_header_fees(&header);
876 assert_eq!(parent_fees.base_fee, 0);
877 assert_eq!(parent_fees.extra_data.as_ref(), jovian);
878 assert_eq!(parent_fees.optimism_jovian, Some(true));
879 }
880
881 #[test]
882 fn fee_details_default_missing_tip_to_zero() {
883 let fees = FeeDetails::new(None, Some(5), None, None).unwrap();
884 assert_eq!(fees.split(), (Some(5), Some(5), Some(0), None));
885 }
886
887 #[test]
888 fn reward_percentile_sweep_preserves_boundaries_and_empty_results() {
889 assert_reward_percentiles_match(&mut [], 0);
890
891 let mut transactions = [(5, 1), (0, 2), (5, 3)];
892 assert_reward_percentiles_match(&mut transactions, 1_000);
893 assert_eq!(reward_percentiles(&transactions, 1_000f64), [1, 1, 3]);
894
895 let mut transactions = [(0, 10), (1, 20)];
896 assert_reward_percentiles_match(&mut transactions, 1);
897 let rewards = reward_percentiles(&transactions, 1f64);
898 assert_eq!(&rewards[..200], &[10; 200]);
899 assert_eq!(rewards[200], 20);
900 }
901
902 #[test]
903 fn reward_percentile_sweep_matches_reference_for_randomized_inputs() {
904 let mut state = 0x4d59_5df4_d0f3_3173u64;
905 for _ in 0..2_000 {
906 let len = (next_random(&mut state) % 129) as usize;
907 let mut transactions = (0..len)
908 .map(|_| {
909 let gas_used = next_random(&mut state) % 100;
910 let effective_reward = (next_random(&mut state) % 16) as u128;
911 (gas_used, effective_reward)
912 })
913 .collect::<Vec<_>>();
914 let total_gas = transactions.iter().map(|(gas_used, _)| gas_used).sum::<u64>();
915 let header_gas_used = match next_random(&mut state) % 4 {
916 0 => total_gas,
917 1 => next_random(&mut state) % (total_gas.saturating_add(1)),
918 2 => total_gas.saturating_add(next_random(&mut state) % 1_000),
919 _ => 0,
920 };
921
922 assert_reward_percentiles_match(&mut transactions, header_gas_used);
923 }
924 }
925
926 fn next_random(state: &mut u64) -> u64 {
927 *state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
928 *state
929 }
930
931 #[test]
932 fn fee_details_reject_gas_price_with_dynamic_fees() {
933 for (max_fee, priority) in [(Some(2), None), (None, Some(1)), (Some(2), Some(1))] {
934 assert!(matches!(
935 FeeDetails::new(Some(2), max_fee, priority, None),
936 Err(BlockchainError::ConflictingFeeFields)
937 ));
938 }
939 }
940
941 #[test]
942 fn fee_details_price_legacy_blob_calls_by_gas_price() {
943 let fees = FeeDetails::new(Some(2), None, None, Some(1)).unwrap();
944 assert_eq!(fees.split(), (Some(2), Some(2), Some(2), Some(1)));
945 }
946}