1use super::{EthApiError, EthResult, EthStateCache, RpcInvalidTransactionError};
5use alloy_consensus::{constants::GWEI_TO_WEI, BlockHeader, Transaction, TxReceipt};
6use alloy_eips::BlockNumberOrTag;
7use alloy_primitives::{B256, U256};
8use alloy_rpc_types_eth::BlockId;
9use derive_more::{Deref, DerefMut, From, Into};
10use itertools::Itertools;
11use reth_rpc_server_types::{
12 constants,
13 constants::gas_oracle::{
14 DEFAULT_GAS_PRICE_BLOCKS, DEFAULT_GAS_PRICE_PERCENTILE, DEFAULT_IGNORE_GAS_PRICE,
15 DEFAULT_MAX_GAS_PRICE, MAX_HEADER_HISTORY, MAX_REWARD_PERCENTILE_COUNT, SAMPLE_NUMBER,
16 },
17};
18use reth_storage_api::{BlockReaderIdExt, NodePrimitivesProvider};
19use schnellru::{ByLength, LruMap};
20use serde::{Deserialize, Serialize};
21use std::fmt::{self, Debug, Formatter};
22use tokio::sync::Mutex;
23use tracing::warn;
24
25pub const RPC_DEFAULT_GAS_CAP: GasCap = GasCap(constants::gas_oracle::RPC_DEFAULT_GAS_CAP);
28
29#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct GasPriceOracleConfig {
33 pub blocks: u32,
35
36 pub percentile: u32,
38
39 pub max_header_history: u64,
41
42 pub max_block_history: u64,
44
45 pub max_reward_percentile_count: u64,
50
51 pub default_suggested_fee: Option<U256>,
53
54 pub max_price: Option<U256>,
56
57 pub ignore_price: Option<U256>,
59}
60
61impl Default for GasPriceOracleConfig {
62 fn default() -> Self {
63 Self {
64 blocks: DEFAULT_GAS_PRICE_BLOCKS,
65 percentile: DEFAULT_GAS_PRICE_PERCENTILE,
66 max_header_history: MAX_HEADER_HISTORY,
67 max_block_history: MAX_HEADER_HISTORY,
68 max_reward_percentile_count: MAX_REWARD_PERCENTILE_COUNT,
69 default_suggested_fee: None,
70 max_price: Some(DEFAULT_MAX_GAS_PRICE),
71 ignore_price: Some(DEFAULT_IGNORE_GAS_PRICE),
72 }
73 }
74}
75
76#[derive(Debug)]
78pub struct GasPriceOracle<Provider>
79where
80 Provider: NodePrimitivesProvider,
81{
82 provider: Provider,
84 cache: EthStateCache<Provider::Primitives>,
86 oracle_config: GasPriceOracleConfig,
88 ignore_price: Option<u128>,
90 inner: Mutex<GasPriceOracleInner>,
93}
94
95impl<Provider> GasPriceOracle<Provider>
96where
97 Provider: BlockReaderIdExt + NodePrimitivesProvider,
98{
99 pub fn new(
101 provider: Provider,
102 mut oracle_config: GasPriceOracleConfig,
103 cache: EthStateCache<Provider::Primitives>,
104 ) -> Self {
105 if oracle_config.percentile > 100 {
107 warn!(prev_percentile = ?oracle_config.percentile, "Invalid configured gas price percentile, assuming 100.");
108 oracle_config.percentile = 100;
109 }
110 let ignore_price = oracle_config.ignore_price.map(|price| price.saturating_to());
111
112 let cached_values = (oracle_config.blocks * 5).max(oracle_config.max_block_history as u32);
114 let inner = Mutex::new(GasPriceOracleInner {
115 last_price: GasPriceOracleResult {
116 block_hash: B256::ZERO,
117 price: oracle_config
118 .default_suggested_fee
119 .unwrap_or_else(|| GasPriceOracleResult::default().price),
120 },
121 lowest_effective_tip_cache: EffectiveTipLruCache(LruMap::new(ByLength::new(
122 cached_values,
123 ))),
124 });
125
126 Self { provider, oracle_config, cache, ignore_price, inner }
127 }
128
129 pub const fn config(&self) -> &GasPriceOracleConfig {
131 &self.oracle_config
132 }
133
134 pub async fn suggest_tip_cap(&self) -> EthResult<U256> {
136 let header = self
137 .provider
138 .sealed_header_by_number_or_tag(BlockNumberOrTag::Latest)?
139 .ok_or(EthApiError::HeaderNotFound(BlockId::latest()))?;
140
141 let mut inner = self.inner.lock().await;
142
143 if inner.last_price.block_hash == header.hash() {
145 return Ok(inner.last_price.price)
146 }
147
148 let mut current_hash = header.hash();
154 let mut results = Vec::new();
155 let mut populated_blocks = 0;
156
157 let max_blocks = header.number().min(self.oracle_config.max_block_history * 2);
159
160 for _ in 0..max_blocks {
161 let (parent_hash, block_values) =
163 if let Some(vals) = inner.lowest_effective_tip_cache.get(¤t_hash) {
164 vals.to_owned()
165 } else {
166 let (parent_hash, block_values) = self
168 .get_block_values(current_hash, SAMPLE_NUMBER)
169 .await?
170 .ok_or(EthApiError::HeaderNotFound(current_hash.into()))?;
171 inner
172 .lowest_effective_tip_cache
173 .insert(current_hash, (parent_hash, block_values.clone()));
174 (parent_hash, block_values)
175 };
176
177 if block_values.is_empty() {
178 results.push(U256::from(inner.last_price.price));
179 } else {
180 results.extend(block_values);
181 populated_blocks += 1;
182 }
183
184 if populated_blocks >= self.oracle_config.blocks {
186 break
187 }
188
189 current_hash = parent_hash;
190 }
191
192 let mut price = if results.is_empty() {
194 inner.last_price.price
195 } else {
196 results.sort_unstable();
197 *results.get((results.len() - 1) * self.oracle_config.percentile as usize / 100).expect(
198 "gas price index is a percent of nonzero array length, so a value always exists",
199 )
200 };
201
202 if let Some(max_price) = self.oracle_config.max_price &&
204 price > max_price
205 {
206 price = max_price;
207 }
208
209 inner.last_price = GasPriceOracleResult { block_hash: header.hash(), price };
210
211 Ok(price)
212 }
213
214 async fn get_block_values(
222 &self,
223 block_hash: B256,
224 limit: usize,
225 ) -> EthResult<Option<(B256, Vec<U256>)>> {
226 let Some(block) = self.cache.get_recovered_block(block_hash).await? else {
228 return Ok(None)
229 };
230
231 let base_fee_per_gas = block.base_fee_per_gas();
232 let parent_hash = block.parent_hash();
233
234 let sorted_transactions = block.transactions_recovered().sorted_by_cached_key(|tx| {
236 if let Some(base_fee) = base_fee_per_gas {
237 (*tx).effective_tip_per_gas(base_fee)
238 } else {
239 Some((*tx).priority_fee_or_price())
240 }
241 });
242
243 let mut prices = Vec::with_capacity(limit);
244
245 for tx in sorted_transactions {
246 let effective_tip = if let Some(base_fee) = base_fee_per_gas {
247 tx.effective_tip_per_gas(base_fee)
248 } else {
249 Some(tx.priority_fee_or_price())
250 };
251
252 if let Some(ignore_under) = self.ignore_price &&
254 effective_tip < Some(ignore_under)
255 {
256 continue
257 }
258
259 if tx.signer() == block.beneficiary() {
261 continue
262 }
263
264 prices.push(U256::from(effective_tip.ok_or(RpcInvalidTransactionError::FeeCapTooLow)?));
267
268 if prices.len() >= limit {
270 break
271 }
272 }
273
274 Ok(Some((parent_hash, prices)))
275 }
276
277 pub async fn op_suggest_tip_cap(&self, min_suggested_priority_fee: U256) -> EthResult<U256> {
287 let header = self
288 .provider
289 .sealed_header_by_number_or_tag(BlockNumberOrTag::Latest)?
290 .ok_or(EthApiError::HeaderNotFound(BlockId::latest()))?;
291
292 let mut inner = self.inner.lock().await;
293
294 if inner.last_price.block_hash == header.hash() {
296 return Ok(inner.last_price.price);
297 }
298
299 let mut suggestion = min_suggested_priority_fee;
300
301 let receipts = self
305 .cache
306 .get_receipts(header.hash())
307 .await?
308 .ok_or(EthApiError::ReceiptsNotFound(BlockId::latest()))?;
309
310 let mut max_tx_gas_used = 0u64;
311 let mut last_cumulative_gas = 0;
312 for receipt in receipts.as_ref() {
313 let cumulative_gas = receipt.cumulative_gas_used();
314 let gas_used = cumulative_gas - last_cumulative_gas;
319 max_tx_gas_used = max_tx_gas_used.max(gas_used);
320 last_cumulative_gas = cumulative_gas;
321 }
322
323 if header.gas_used() + max_tx_gas_used > header.gas_limit() {
325 let Some(median_tip) = self.get_block_median_tip(header.hash()).await? else {
326 return Ok(suggestion);
327 };
328
329 let new_suggestion = median_tip + median_tip / U256::from(10);
330
331 if new_suggestion > suggestion {
332 suggestion = new_suggestion;
333 }
334 }
335
336 if let Some(max_price) = self.oracle_config.max_price &&
338 suggestion > max_price
339 {
340 suggestion = max_price;
341 }
342
343 inner.last_price = GasPriceOracleResult { block_hash: header.hash(), price: suggestion };
344
345 Ok(suggestion)
346 }
347
348 pub async fn get_block_median_tip(&self, block_hash: B256) -> EthResult<Option<U256>> {
353 let Some(block) = self.cache.get_recovered_block(block_hash).await? else {
355 return Ok(None)
356 };
357
358 let base_fee_per_gas = block.base_fee_per_gas();
359
360 let prices = block
362 .transactions_recovered()
363 .filter_map(|tx| {
364 if let Some(base_fee) = base_fee_per_gas {
365 (*tx).effective_tip_per_gas(base_fee)
366 } else {
367 Some((*tx).priority_fee_or_price())
368 }
369 })
370 .sorted()
371 .collect::<Vec<_>>();
372
373 let median = if prices.is_empty() {
374 None
376 } else if prices.len() % 2 == 1 {
377 Some(U256::from(prices[prices.len() / 2]))
378 } else {
379 Some(U256::from((prices[prices.len() / 2 - 1] + prices[prices.len() / 2]) / 2))
380 };
381
382 Ok(median)
383 }
384}
385#[derive(Debug)]
387struct GasPriceOracleInner {
388 last_price: GasPriceOracleResult,
389 lowest_effective_tip_cache: EffectiveTipLruCache,
390}
391
392#[derive(Deref, DerefMut)]
394pub struct EffectiveTipLruCache(LruMap<B256, (B256, Vec<U256>), ByLength>);
395
396impl Debug for EffectiveTipLruCache {
397 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
398 f.debug_struct("EffectiveTipLruCache")
399 .field("cache_length", &self.len())
400 .field("cache_memory_usage", &self.memory_usage())
401 .finish()
402 }
403}
404
405#[derive(Debug, Clone)]
407pub struct GasPriceOracleResult {
408 pub block_hash: B256,
410 pub price: U256,
412}
413
414impl Default for GasPriceOracleResult {
415 fn default() -> Self {
416 Self { block_hash: B256::ZERO, price: U256::from(GWEI_TO_WEI) }
417 }
418}
419
420#[derive(Debug, Clone, Copy, From, Into)]
422pub struct GasCap(pub u64);
423
424impl Default for GasCap {
425 fn default() -> Self {
426 RPC_DEFAULT_GAS_CAP
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn max_price_sanity() {
436 assert_eq!(DEFAULT_MAX_GAS_PRICE, U256::from(500_000_000_000u64));
437 assert_eq!(DEFAULT_MAX_GAS_PRICE, U256::from(500 * GWEI_TO_WEI))
438 }
439
440 #[test]
441 fn ignore_price_sanity() {
442 assert_eq!(DEFAULT_IGNORE_GAS_PRICE, U256::from(2u64));
443 }
444}