1use std::{
2 collections::BTreeMap,
3 fmt,
4 pin::Pin,
5 sync::Arc,
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 if let Some(hardfork) = self.tempo_hardfork() {
179 return tempo_next_block_base_fee(hardfork, gas_used, last_fee_per_gas);
180 }
181 calc_next_block_base_fee(gas_used, gas_limit, last_fee_per_gas, self.base_fee_params)
182 }
183
184 pub fn get_next_block_blob_base_fee_per_gas(&self) -> u128 {
186 self.blob_params().calc_blob_fee(self.blob_excess_gas_and_price.read().excess_blob_gas)
187 }
188
189 pub fn get_next_block_blob_excess_gas(&self, blob_excess_gas: u64, blob_gas_used: u64) -> u64 {
192 self.blob_params().next_block_excess_blob_gas_osaka(
193 blob_excess_gas,
194 blob_gas_used,
195 self.base_fee(),
196 )
197 }
198
199 pub fn set_blob_params(&self, blob_params: BlobParams) {
201 *self.blob_params.write() = blob_params;
202 }
203
204 pub fn blob_params(&self) -> BlobParams {
206 *self.blob_params.read()
207 }
208}
209
210fn tempo_next_block_base_fee(hardfork: TempoHardfork, gas_used: u64, parent_base_fee: u64) -> u64 {
216 if hardfork.is_t7() {
217 return tempo_t7_next_block_base_fee(parent_base_fee, gas_used);
218 }
219 crate::config::tempo_default_base_fee(hardfork)
220}
221
222pub struct FeeHistoryService<N: Network>
224where
225 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
226{
227 blob_params: BlobParams,
229 new_blocks: ChainNotifications,
231 cache: FeeHistoryCache,
233 fee_history_limit: u64,
235 storage_info: StorageInfo<N>,
237}
238
239impl<N: Network> FeeHistoryService<N>
240where
241 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
242{
243 pub const fn new(
244 blob_params: BlobParams,
245 new_blocks: ChainNotifications,
246 cache: FeeHistoryCache,
247 storage_info: StorageInfo<N>,
248 ) -> Self {
249 Self {
250 blob_params,
251 new_blocks,
252 cache,
253 fee_history_limit: MAX_FEE_HISTORY_CACHE_SIZE,
254 storage_info,
255 }
256 }
257
258 pub const fn fee_history_limit(&self) -> u64 {
260 self.fee_history_limit
261 }
262
263 pub(crate) fn insert_cache_entry_for_block(&self, hash: B256, header: &impl BlockHeader) {
265 let (result, block_number) = self.create_cache_entry(hash, header);
266 self.insert_cache_entry(result, block_number);
267 }
268
269 fn create_cache_entry(
271 &self,
272 hash: B256,
273 header: &impl BlockHeader,
274 ) -> (FeeHistoryCacheItem, Option<u64>) {
275 let reward_percentiles: Vec<f64> = {
278 let mut percentile: f64 = 0.0;
279 (0..=200)
280 .map(|_| {
281 let val = percentile;
282 percentile += 0.5;
283 val
284 })
285 .collect()
286 };
287
288 let mut block_number: Option<u64> = None;
289 let base_fee = header.base_fee_per_gas().unwrap_or_default();
290 let excess_blob_gas = header.excess_blob_gas().map(|g| g as u128);
291 let blob_gas_used = header.blob_gas_used().map(|g| g as u128);
292 let base_fee_per_blob_gas = header.blob_fee(self.blob_params);
293
294 let mut item = FeeHistoryCacheItem {
295 base_fee: base_fee as u128,
296 gas_used_ratio: 0f64,
297 blob_gas_used_ratio: 0f64,
298 rewards: Vec::new(),
299 excess_blob_gas,
300 base_fee_per_blob_gas,
301 blob_gas_used,
302 };
303
304 let current_block = self.storage_info.block(hash);
305 let current_receipts = self.storage_info.receipts(hash);
306
307 if let (Some(block), Some(receipts)) = (current_block, current_receipts) {
308 block_number = Some(block.header.number());
309
310 let gas_used = block.header.gas_used() as f64;
311 let blob_gas_used = block.header.blob_gas_used().map(|g| g as f64);
312 item.gas_used_ratio = gas_used / block.header.gas_limit() as f64;
313 item.blob_gas_used_ratio = blob_gas_used
314 .map(|g| {
315 let max = self.blob_params.max_blob_gas_per_block() as f64;
316 if max == 0.0 { 0.0 } else { g / max }
317 })
318 .unwrap_or(0.0);
319
320 let mut transactions: Vec<(_, _)> = receipts
322 .iter()
323 .enumerate()
324 .map(|(i, receipt)| {
325 let cumulative = receipt.cumulative_gas_used();
326 let prev_cumulative =
327 if i > 0 { receipts[i - 1].cumulative_gas_used() } else { 0 };
328 let gas_used = cumulative - prev_cumulative;
329 let effective_reward = block
330 .body
331 .transactions
332 .get(i)
333 .map(|tx| tx.as_ref().effective_tip_per_gas(base_fee).unwrap_or(0))
334 .unwrap_or(0);
335
336 (gas_used, effective_reward)
337 })
338 .collect();
339
340 transactions.sort_by_key(|(_, reward)| *reward);
342
343 item.rewards = reward_percentiles
345 .into_iter()
346 .filter_map(|p| {
347 let target_gas = (p * gas_used / 100f64) as u64;
348 let mut sum_gas = 0;
349 for (gas_used, effective_reward) in transactions.iter().copied() {
350 sum_gas += gas_used;
351 if target_gas <= sum_gas {
352 return Some(effective_reward);
353 }
354 }
355 None
356 })
357 .collect();
358 } else {
359 item.rewards = vec![0; reward_percentiles.len()];
360 }
361 (item, block_number)
362 }
363
364 fn insert_cache_entry(&self, item: FeeHistoryCacheItem, block_number: Option<u64>) {
365 if let Some(block_number) = block_number {
366 trace!(target: "fees", "insert new history item={:?} for {}", item, block_number);
367 let mut cache = self.cache.lock();
368 cache.insert(block_number, item);
369
370 let pop_next = block_number.saturating_sub(self.fee_history_limit);
372
373 let num_remove = (cache.len() as u64).saturating_sub(self.fee_history_limit);
374 for num in 0..num_remove {
375 let key = pop_next - num;
376 cache.remove(&key);
377 }
378 }
379 }
380}
381
382impl<N: Network> Future for FeeHistoryService<N>
384where
385 N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
386{
387 type Output = ();
388
389 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
390 let pin = self.get_mut();
391
392 while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) {
393 if let Some(block) = notification.as_new_block() {
395 pin.insert_cache_entry_for_block(block.hash, block.header.as_ref());
396 }
397 }
398
399 Poll::Pending
400 }
401}
402
403pub type FeeHistoryCache = Arc<Mutex<BTreeMap<u64, FeeHistoryCacheItem>>>;
404
405#[derive(Clone, Debug)]
407pub struct FeeHistoryCacheItem {
408 pub base_fee: u128,
409 pub gas_used_ratio: f64,
410 pub base_fee_per_blob_gas: Option<u128>,
411 pub blob_gas_used_ratio: f64,
412 pub excess_blob_gas: Option<u128>,
413 pub blob_gas_used: Option<u128>,
414 pub rewards: Vec<u128>,
415}
416
417#[derive(Clone, Default)]
418pub struct FeeDetails {
419 pub gas_price: Option<u128>,
420 pub max_fee_per_gas: Option<u128>,
421 pub max_priority_fee_per_gas: Option<u128>,
422 pub max_fee_per_blob_gas: Option<u128>,
423}
424
425impl FeeDetails {
426 pub const fn zero() -> Self {
428 Self {
429 gas_price: Some(0),
430 max_fee_per_gas: Some(0),
431 max_priority_fee_per_gas: Some(0),
432 max_fee_per_blob_gas: None,
433 }
434 }
435
436 pub const fn or_zero_fees(self) -> Self {
438 let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
439 self;
440
441 let no_fees = gas_price.is_none() && max_fee_per_gas.is_none();
442 let gas_price = if no_fees { Some(0) } else { gas_price };
443 let max_fee_per_gas = if no_fees { Some(0) } else { max_fee_per_gas };
444 let max_fee_per_blob_gas = if no_fees { None } else { max_fee_per_blob_gas };
445
446 Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas }
447 }
448
449 pub const fn split(self) -> (Option<u128>, Option<u128>, Option<u128>, Option<u128>) {
451 let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
452 self;
453 (gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
454 }
455
456 pub fn new(
458 request_gas_price: Option<u128>,
459 request_max_fee: Option<u128>,
460 request_priority: Option<u128>,
461 max_fee_per_blob_gas: Option<u128>,
462 ) -> Result<Self, BlockchainError> {
463 match (request_gas_price, request_max_fee, request_priority, max_fee_per_blob_gas) {
464 (gas_price, None, None, None) => {
465 Ok(Self {
467 gas_price,
468 max_fee_per_gas: gas_price,
469 max_priority_fee_per_gas: gas_price,
470 max_fee_per_blob_gas: None,
471 })
472 }
473 (_, max_fee, max_priority, max_fee_per_blob_gas) => {
474 if let Some(max_priority) = max_priority {
477 let max_fee = max_fee.unwrap_or_default();
478 if max_priority > max_fee {
479 return Err(BlockchainError::InvalidFeeInput);
480 }
481 }
482 Ok(Self {
483 gas_price: max_fee,
484 max_fee_per_gas: max_fee,
485 max_priority_fee_per_gas: max_priority,
486 max_fee_per_blob_gas,
487 })
488 }
489 }
490 }
491}
492
493impl fmt::Debug for FeeDetails {
494 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
495 write!(fmt, "Fees {{ ")?;
496 write!(fmt, "gas_price: {:?}, ", self.gas_price)?;
497 write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?;
498 write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?;
499 write!(fmt, "}}")?;
500 Ok(())
501 }
502}