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