ethrex_rpc/eth/
gas_tip_estimator.rs1use ethrex_common::{H256, U256, types::MIN_GAS_TIP};
2use ethrex_storage::Store;
3use tracing::error;
4
5use crate::utils::RpcErr;
6
7const TXS_SAMPLE_SIZE: usize = 3;
10const BLOCK_RANGE_LOWER_BOUND_DEC: u64 = 20;
12
13#[derive(Debug, Clone)]
14pub struct GasTipEstimator {
16 pub last_hash: H256,
18 pub last_tip: u64,
20}
21
22impl GasTipEstimator {
23 pub fn new() -> GasTipEstimator {
25 Self {
26 last_hash: H256::default(),
27 last_tip: MIN_GAS_TIP,
28 }
29 }
30
31 pub async fn estimate_gas_tip(&mut self, storage: &Store) -> Result<u64, RpcErr> {
50 let latest_block_number = storage.get_latest_block_number().await?;
51 let latest_block_hash = storage
52 .get_canonical_block_hash(latest_block_number)
53 .await?
54 .ok_or(RpcErr::Internal(format!(
55 "Latest Block {latest_block_number} not cannonical"
56 )))?;
57 if self.last_hash == latest_block_hash {
59 return Ok(self.last_tip);
60 }
61 let block_range_lower_bound =
62 latest_block_number.saturating_sub(BLOCK_RANGE_LOWER_BOUND_DEC);
63 let block_range = block_range_lower_bound..=latest_block_number;
65 if block_range.is_empty() {
66 error!(
67 "Calculated block range from block {} \
68 up to block {} for gas price estimation is empty",
69 block_range_lower_bound, latest_block_number
70 );
71 return Err(RpcErr::Internal("Error calculating gas price".to_string()));
72 }
73 let mut results: Vec<U256> = vec![];
74 for block_num in block_range {
79 let Some(block_body) = storage.get_block_body(block_num).await? else {
80 error!(
81 "Block body for block number {block_num} is missing but is below the latest known block!"
82 );
83 return Err(RpcErr::Internal(
84 "Error calculating gas price: missing data".to_string(),
85 ));
86 };
87
88 let base_fee = storage
89 .get_block_header(block_num)
90 .ok()
91 .flatten()
92 .and_then(|header| header.base_fee_per_gas);
93
94 let mut gas_tip_samples = block_body
97 .transactions
98 .into_iter()
99 .filter_map(|tx| tx.effective_gas_tip(base_fee))
100 .collect::<Vec<U256>>();
101
102 gas_tip_samples.sort();
103 results.extend(gas_tip_samples.into_iter().take(TXS_SAMPLE_SIZE));
104 }
105 results.sort();
106
107 let last_tip_u256 = U256::from(self.last_tip);
109 let estimated_tip = *results.get(results.len() / 2).unwrap_or(&last_tip_u256);
110 let estimated_tip = u64::try_from(estimated_tip).unwrap_or(u64::MAX);
112
113 self.last_hash = latest_block_hash;
115 self.last_tip = estimated_tip;
116
117 Ok(estimated_tip)
118 }
119}
120
121impl Default for GasTipEstimator {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::test_utils::{
132 BASE_PRICE_IN_WEI, add_eip1559_tx_blocks, add_empty_blocks, add_legacy_tx_blocks,
133 add_mixed_tx_blocks, setup_store,
134 };
135
136 #[tokio::test]
137 async fn test_for_legacy_txs() {
138 let storage = setup_store().await;
139 add_legacy_tx_blocks(&storage, 20, 10).await;
140 let gas_tip = GasTipEstimator::new()
141 .estimate_gas_tip(&storage)
142 .await
143 .unwrap();
144 assert_eq!(gas_tip, BASE_PRICE_IN_WEI);
145 }
146
147 #[tokio::test]
148 async fn test_for_eip1559_txs() {
149 let storage = setup_store().await;
150 add_eip1559_tx_blocks(&storage, 20, 10).await;
151 let gas_tip = GasTipEstimator::new()
152 .estimate_gas_tip(&storage)
153 .await
154 .unwrap();
155 assert_eq!(gas_tip, BASE_PRICE_IN_WEI);
156 }
157
158 #[tokio::test]
159 async fn test_for_mixed_txs() {
160 let storage = setup_store().await;
161 add_mixed_tx_blocks(&storage, 20, 10).await;
162 let gas_tip = GasTipEstimator::new()
163 .estimate_gas_tip(&storage)
164 .await
165 .unwrap();
166 assert_eq!(gas_tip, BASE_PRICE_IN_WEI);
167 }
168
169 #[tokio::test]
170 async fn test_for_no_blocks() {
171 let storage = setup_store().await;
172 let gas_tip = GasTipEstimator::new()
173 .estimate_gas_tip(&storage)
174 .await
175 .unwrap();
176 assert_eq!(gas_tip, MIN_GAS_TIP);
177 }
178
179 #[tokio::test]
180 async fn test_for_empty_blocks() {
181 let storage = setup_store().await;
182 add_empty_blocks(&storage, 20).await;
183 let gas_tip = GasTipEstimator::new()
184 .estimate_gas_tip(&storage)
185 .await
186 .unwrap();
187 assert_eq!(gas_tip, MIN_GAS_TIP);
188 }
189}