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