1use std::{
2 collections::BTreeMap,
3 fmt,
4 pin::Pin,
5 sync::Arc,
6 task::{Context, Poll},
7};
8
9use alloy_consensus::{Header, Transaction};
10use alloy_eips::{calc_next_block_base_fee, eip1559::BaseFeeParams, eip7840::BlobParams};
11use alloy_primitives::B256;
12use futures::StreamExt;
13use parking_lot::{Mutex, RwLock};
14use revm::{context_interface::block::BlobExcessGasAndPrice, primitives::hardfork::SpecId};
15
16use crate::eth::{
17 backend::{info::StorageInfo, notifications::NewBlockNotifications},
18 error::BlockchainError,
19};
20
21pub const MAX_FEE_HISTORY_CACHE_SIZE: u64 = 2048u64;
23
24pub const INITIAL_BASE_FEE: u64 = 1_000_000_000;
26
27pub const INITIAL_GAS_PRICE: u128 = 1_875_000_000;
29
30pub const BASE_FEE_CHANGE_DENOMINATOR: u128 = 8;
32
33pub const MIN_SUGGESTED_PRIORITY_FEE: u128 = 1e9 as u128;
35
36#[derive(Clone, Debug)]
38pub struct FeeManager {
39 spec_id: SpecId,
41 blob_params: Arc<RwLock<BlobParams>>,
43 base_fee: Arc<RwLock<u64>>,
47 is_min_priority_fee_enforced: bool,
49 blob_excess_gas_and_price: Arc<RwLock<BlobExcessGasAndPrice>>,
53 gas_price: Arc<RwLock<u128>>,
57 elasticity: Arc<RwLock<f64>>,
58 base_fee_params: BaseFeeParams,
60}
61
62impl FeeManager {
63 pub fn new(
64 spec_id: SpecId,
65 base_fee: u64,
66 is_min_priority_fee_enforced: bool,
67 gas_price: u128,
68 blob_excess_gas_and_price: BlobExcessGasAndPrice,
69 blob_params: BlobParams,
70 base_fee_params: BaseFeeParams,
71 ) -> Self {
72 let elasticity = 1f64 / base_fee_params.elasticity_multiplier as f64;
73 Self {
74 spec_id,
75 blob_params: Arc::new(RwLock::new(blob_params)),
76 base_fee: Arc::new(RwLock::new(base_fee)),
77 is_min_priority_fee_enforced,
78 gas_price: Arc::new(RwLock::new(gas_price)),
79 blob_excess_gas_and_price: Arc::new(RwLock::new(blob_excess_gas_and_price)),
80 elasticity: Arc::new(RwLock::new(elasticity)),
81 base_fee_params,
82 }
83 }
84
85 pub fn base_fee_params(&self) -> BaseFeeParams {
87 self.base_fee_params
88 }
89
90 pub fn elasticity(&self) -> f64 {
91 *self.elasticity.read()
92 }
93
94 pub fn is_eip1559(&self) -> bool {
96 (self.spec_id as u8) >= (SpecId::LONDON as u8)
97 }
98
99 pub fn is_eip4844(&self) -> bool {
100 (self.spec_id as u8) >= (SpecId::CANCUN as u8)
101 }
102
103 pub fn blob_gas_price(&self) -> u128 {
105 if self.is_eip4844() { self.base_fee_per_blob_gas() } else { 0 }
106 }
107
108 pub fn base_fee(&self) -> u64 {
109 if self.is_eip1559() { *self.base_fee.read() } else { 0 }
110 }
111
112 pub fn is_min_priority_fee_enforced(&self) -> bool {
113 self.is_min_priority_fee_enforced
114 }
115
116 pub fn raw_gas_price(&self) -> u128 {
118 *self.gas_price.read()
119 }
120
121 pub fn excess_blob_gas_and_price(&self) -> Option<BlobExcessGasAndPrice> {
122 if self.is_eip4844() { Some(*self.blob_excess_gas_and_price.read()) } else { None }
123 }
124
125 pub fn base_fee_per_blob_gas(&self) -> u128 {
126 if self.is_eip4844() { self.blob_excess_gas_and_price.read().blob_gasprice } else { 0 }
127 }
128
129 pub fn set_gas_price(&self, price: u128) {
131 let mut gas = self.gas_price.write();
132 *gas = price;
133 }
134
135 pub fn set_base_fee(&self, fee: u64) {
137 trace!(target: "backend::fees", "updated base fee {:?}", fee);
138 let mut base = self.base_fee.write();
139 *base = fee;
140 }
141
142 pub fn set_blob_excess_gas_and_price(&self, blob_excess_gas_and_price: BlobExcessGasAndPrice) {
144 trace!(target: "backend::fees", "updated blob base fee {:?}", blob_excess_gas_and_price);
145 let mut base = self.blob_excess_gas_and_price.write();
146 *base = blob_excess_gas_and_price;
147 }
148
149 pub fn get_next_block_base_fee_per_gas(
151 &self,
152 gas_used: u64,
153 gas_limit: u64,
154 last_fee_per_gas: u64,
155 ) -> u64 {
156 if self.base_fee() == 0 {
160 return 0;
161 }
162 calc_next_block_base_fee(gas_used, gas_limit, last_fee_per_gas, self.base_fee_params)
163 }
164
165 pub fn get_next_block_blob_base_fee_per_gas(&self) -> u128 {
167 self.blob_params().calc_blob_fee(self.blob_excess_gas_and_price.read().excess_blob_gas)
168 }
169
170 pub fn get_next_block_blob_excess_gas(&self, blob_gas_used: u64, blob_excess_gas: u64) -> u64 {
173 self.blob_params().next_block_excess_blob_gas_osaka(
174 blob_excess_gas,
175 blob_gas_used,
176 self.base_fee(),
177 )
178 }
179
180 pub fn set_blob_params(&self, blob_params: BlobParams) {
182 *self.blob_params.write() = blob_params;
183 }
184
185 pub fn blob_params(&self) -> BlobParams {
187 *self.blob_params.read()
188 }
189}
190
191pub struct FeeHistoryService {
193 blob_params: BlobParams,
195 new_blocks: NewBlockNotifications,
197 cache: FeeHistoryCache,
199 fee_history_limit: u64,
201 storage_info: StorageInfo,
203}
204
205impl FeeHistoryService {
206 pub fn new(
207 blob_params: BlobParams,
208 new_blocks: NewBlockNotifications,
209 cache: FeeHistoryCache,
210 storage_info: StorageInfo,
211 ) -> Self {
212 Self {
213 blob_params,
214 new_blocks,
215 cache,
216 fee_history_limit: MAX_FEE_HISTORY_CACHE_SIZE,
217 storage_info,
218 }
219 }
220
221 pub fn fee_history_limit(&self) -> u64 {
223 self.fee_history_limit
224 }
225
226 pub(crate) fn insert_cache_entry_for_block(&self, hash: B256, header: &Header) {
228 let (result, block_number) = self.create_cache_entry(hash, header);
229 self.insert_cache_entry(result, block_number);
230 }
231
232 fn create_cache_entry(
234 &self,
235 hash: B256,
236 header: &Header,
237 ) -> (FeeHistoryCacheItem, Option<u64>) {
238 let reward_percentiles: Vec<f64> = {
241 let mut percentile: f64 = 0.0;
242 (0..=200)
243 .map(|_| {
244 let val = percentile;
245 percentile += 0.5;
246 val
247 })
248 .collect()
249 };
250
251 let mut block_number: Option<u64> = None;
252 let base_fee = header.base_fee_per_gas.unwrap_or_default();
253 let excess_blob_gas = header.excess_blob_gas.map(|g| g as u128);
254 let blob_gas_used = header.blob_gas_used.map(|g| g as u128);
255 let base_fee_per_blob_gas = header.blob_fee(self.blob_params);
256
257 let mut item = FeeHistoryCacheItem {
258 base_fee: base_fee as u128,
259 gas_used_ratio: 0f64,
260 blob_gas_used_ratio: 0f64,
261 rewards: Vec::new(),
262 excess_blob_gas,
263 base_fee_per_blob_gas,
264 blob_gas_used,
265 };
266
267 let current_block = self.storage_info.block(hash);
268 let current_receipts = self.storage_info.receipts(hash);
269
270 if let (Some(block), Some(receipts)) = (current_block, current_receipts) {
271 block_number = Some(block.header.number);
272
273 let gas_used = block.header.gas_used as f64;
274 let blob_gas_used = block.header.blob_gas_used.map(|g| g as f64);
275 item.gas_used_ratio = gas_used / block.header.gas_limit as f64;
276 item.blob_gas_used_ratio = blob_gas_used
277 .map(|g| {
278 let max = self.blob_params.max_blob_gas_per_block() as f64;
279 if max == 0.0 { 0.0 } else { g / max }
280 })
281 .unwrap_or(0.0);
282
283 let mut transactions: Vec<(_, _)> = receipts
285 .iter()
286 .enumerate()
287 .map(|(i, receipt)| {
288 let cumulative = receipt.cumulative_gas_used();
289 let prev_cumulative =
290 if i > 0 { receipts[i - 1].cumulative_gas_used() } else { 0 };
291 let gas_used = cumulative - prev_cumulative;
292 let effective_reward = block
293 .body
294 .transactions
295 .get(i)
296 .map(|tx| tx.as_ref().effective_tip_per_gas(base_fee).unwrap_or(0))
297 .unwrap_or(0);
298
299 (gas_used, effective_reward)
300 })
301 .collect();
302
303 transactions.sort_by_key(|(_, reward)| *reward);
305
306 item.rewards = reward_percentiles
308 .into_iter()
309 .filter_map(|p| {
310 let target_gas = (p * gas_used / 100f64) as u64;
311 let mut sum_gas = 0;
312 for (gas_used, effective_reward) in transactions.iter().copied() {
313 sum_gas += gas_used;
314 if target_gas <= sum_gas {
315 return Some(effective_reward);
316 }
317 }
318 None
319 })
320 .collect();
321 } else {
322 item.rewards = vec![0; reward_percentiles.len()];
323 }
324 (item, block_number)
325 }
326
327 fn insert_cache_entry(&self, item: FeeHistoryCacheItem, block_number: Option<u64>) {
328 if let Some(block_number) = block_number {
329 trace!(target: "fees", "insert new history item={:?} for {}", item, block_number);
330 let mut cache = self.cache.lock();
331 cache.insert(block_number, item);
332
333 let pop_next = block_number.saturating_sub(self.fee_history_limit);
335
336 let num_remove = (cache.len() as u64).saturating_sub(self.fee_history_limit);
337 for num in 0..num_remove {
338 let key = pop_next - num;
339 cache.remove(&key);
340 }
341 }
342 }
343}
344
345impl Future for FeeHistoryService {
347 type Output = ();
348
349 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
350 let pin = self.get_mut();
351
352 while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) {
353 pin.insert_cache_entry_for_block(notification.hash, notification.header.as_ref());
355 }
356
357 Poll::Pending
358 }
359}
360
361pub type FeeHistoryCache = Arc<Mutex<BTreeMap<u64, FeeHistoryCacheItem>>>;
362
363#[derive(Clone, Debug)]
365pub struct FeeHistoryCacheItem {
366 pub base_fee: u128,
367 pub gas_used_ratio: f64,
368 pub base_fee_per_blob_gas: Option<u128>,
369 pub blob_gas_used_ratio: f64,
370 pub excess_blob_gas: Option<u128>,
371 pub blob_gas_used: Option<u128>,
372 pub rewards: Vec<u128>,
373}
374
375#[derive(Clone, Default)]
376pub struct FeeDetails {
377 pub gas_price: Option<u128>,
378 pub max_fee_per_gas: Option<u128>,
379 pub max_priority_fee_per_gas: Option<u128>,
380 pub max_fee_per_blob_gas: Option<u128>,
381}
382
383impl FeeDetails {
384 pub fn zero() -> Self {
386 Self {
387 gas_price: Some(0),
388 max_fee_per_gas: Some(0),
389 max_priority_fee_per_gas: Some(0),
390 max_fee_per_blob_gas: None,
391 }
392 }
393
394 pub fn or_zero_fees(self) -> Self {
396 let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
397 self;
398
399 let no_fees = gas_price.is_none() && max_fee_per_gas.is_none();
400 let gas_price = if no_fees { Some(0) } else { gas_price };
401 let max_fee_per_gas = if no_fees { Some(0) } else { max_fee_per_gas };
402 let max_fee_per_blob_gas = if no_fees { None } else { max_fee_per_blob_gas };
403
404 Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas }
405 }
406
407 pub fn split(self) -> (Option<u128>, Option<u128>, Option<u128>, Option<u128>) {
409 let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
410 self;
411 (gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
412 }
413
414 pub fn new(
416 request_gas_price: Option<u128>,
417 request_max_fee: Option<u128>,
418 request_priority: Option<u128>,
419 max_fee_per_blob_gas: Option<u128>,
420 ) -> Result<Self, BlockchainError> {
421 match (request_gas_price, request_max_fee, request_priority, max_fee_per_blob_gas) {
422 (gas_price, None, None, None) => {
423 Ok(Self {
425 gas_price,
426 max_fee_per_gas: gas_price,
427 max_priority_fee_per_gas: gas_price,
428 max_fee_per_blob_gas: None,
429 })
430 }
431 (_, max_fee, max_priority, None) => {
432 if let Some(max_priority) = max_priority {
435 let max_fee = max_fee.unwrap_or_default();
436 if max_priority > max_fee {
437 return Err(BlockchainError::InvalidFeeInput);
438 }
439 }
440 Ok(Self {
441 gas_price: max_fee,
442 max_fee_per_gas: max_fee,
443 max_priority_fee_per_gas: max_priority,
444 max_fee_per_blob_gas: None,
445 })
446 }
447 (_, max_fee, max_priority, max_fee_per_blob_gas) => {
448 if let Some(max_priority) = max_priority {
451 let max_fee = max_fee.unwrap_or_default();
452 if max_priority > max_fee {
453 return Err(BlockchainError::InvalidFeeInput);
454 }
455 }
456 Ok(Self {
457 gas_price: max_fee,
458 max_fee_per_gas: max_fee,
459 max_priority_fee_per_gas: max_priority,
460 max_fee_per_blob_gas,
461 })
462 }
463 }
464 }
465}
466
467impl fmt::Debug for FeeDetails {
468 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
469 write!(fmt, "Fees {{ ")?;
470 write!(fmt, "gas_price: {:?}, ", self.gas_price)?;
471 write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?;
472 write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?;
473 write!(fmt, "}}")?;
474 Ok(())
475 }
476}