Skip to content

Commit d6f52ad

Browse files
yiweichigreged93
andauthored
feat: suggest gas price based on latest block's transactions (#291)
* feat: simplify gas price oracle * fix: api * add max_payload_size to ethApi * feat: suggest gas price based on latest block's transactions * feat: only update cache when it's latest block * Update crates/scroll/rpc/src/eth/fee.rs Co-authored-by: greg <[email protected]> * fix: ci * address comments * feat: use ScrollBaseFeeProvider in fee history * fix update reward logic * fix calculate_suggest_tip_cap logic * fix ci * address comment * fix typo --------- Co-authored-by: greg <[email protected]>
1 parent 357655f commit d6f52ad

File tree

5 files changed

+481
-24
lines changed

5 files changed

+481
-24
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rpc/rpc-eth-types/src/gas_oracle.rs

Lines changed: 145 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
use super::{EthApiError, EthResult, EthStateCache, RpcInvalidTransactionError};
55
use alloy_consensus::{constants::GWEI_TO_WEI, BlockHeader, Transaction, TxReceipt};
6-
use alloy_eips::BlockNumberOrTag;
6+
use alloy_eips::{BlockNumberOrTag, Encodable2718};
77
use alloy_primitives::{B256, U256};
88
use alloy_rpc_types_eth::BlockId;
99
use derive_more::{Deref, DerefMut, From, Into};
@@ -205,7 +205,8 @@ where
205205
}
206206
}
207207

208-
inner.last_price = GasPriceOracleResult { block_hash: header.hash(), price };
208+
inner.last_price =
209+
GasPriceOracleResult { block_hash: header.hash(), price, ..Default::default() };
209210

210211
Ok(price)
211212
}
@@ -340,11 +341,149 @@ where
340341
}
341342
}
342343

343-
inner.last_price = GasPriceOracleResult { block_hash: header.hash(), price: suggestion };
344+
inner.last_price = GasPriceOracleResult {
345+
block_hash: header.hash(),
346+
price: suggestion,
347+
..Default::default()
348+
};
344349

345350
Ok(suggestion)
346351
}
347352

353+
/// Suggests a max priority fee value using a simplified and more predictable algorithm
354+
/// appropriate for chains like Scroll with a single known block builder.
355+
///
356+
/// It returns either:
357+
/// - The minimum suggested priority fee when blocks have capacity
358+
/// - 10% above the median effective priority fee from the last block when at capacity
359+
///
360+
/// A block is considered at capacity if its total gas used plus the maximum single transaction
361+
/// gas would exceed the block's gas limit, or the total block payload size plus the maximum
362+
/// single transaction would exceed the block's payload size limit.
363+
pub async fn scroll_suggest_tip_cap(
364+
&self,
365+
min_suggested_priority_fee: U256,
366+
payload_size_limit: u64,
367+
) -> EthResult<U256> {
368+
let (result, _) = self
369+
.calculate_suggest_tip_cap(
370+
BlockNumberOrTag::Latest,
371+
min_suggested_priority_fee,
372+
payload_size_limit,
373+
)
374+
.await;
375+
result
376+
}
377+
378+
/// Calculates a gas price suggestion and returns whether the block is at capacity.
379+
///
380+
/// This method implements the core logic for suggesting gas prices based on block capacity.
381+
/// It returns a tuple containing the suggested gas price and a boolean indicating
382+
/// whether the latest block is at capacity (gas limit or payload size limit).
383+
pub async fn calculate_suggest_tip_cap(
384+
&self,
385+
block_number_or_tag: BlockNumberOrTag,
386+
min_suggested_priority_fee: U256,
387+
payload_size_limit: u64,
388+
) -> (EthResult<U256>, bool) {
389+
let header = match self.provider.sealed_header_by_number_or_tag(block_number_or_tag) {
390+
Ok(Some(header)) => header,
391+
Ok(None) => return (Err(EthApiError::HeaderNotFound(BlockId::latest())), false),
392+
Err(e) => return (Err(e.into()), false),
393+
};
394+
395+
let mut inner = self.inner.lock().await;
396+
397+
// if we have stored a last price, then we check whether or not it was for the same head
398+
if inner.last_price.block_hash == header.hash() {
399+
return (Ok(inner.last_price.price), inner.last_price.is_at_capacity);
400+
}
401+
402+
let mut suggestion = min_suggested_priority_fee;
403+
let mut is_at_capacity = false;
404+
405+
// find the maximum gas used by any of the transactions in the block and
406+
// the maximum and total payload size used by the transactions in the block to use as
407+
// the capacity margin for the block, if no receipts or block are found return the
408+
// suggested_min_priority_fee
409+
let (block, receipts) = match self.cache.get_block_and_receipts(header.hash()).await {
410+
Ok(Some((block, receipts))) => (block, receipts),
411+
Ok(None) => return (Ok(suggestion), false),
412+
Err(e) => return (Err(e.into()), false),
413+
};
414+
415+
let mut max_tx_gas_used = 0u64;
416+
let mut last_cumulative_gas = 0;
417+
for receipt in receipts.as_ref() {
418+
let cumulative_gas = receipt.cumulative_gas_used();
419+
// get the gas used by each transaction in the block, by subtracting the
420+
// cumulative gas used of the previous transaction from the cumulative gas used of
421+
// the current transaction. This is because there is no gas_used()
422+
// method on the Receipt trait.
423+
let gas_used = cumulative_gas - last_cumulative_gas;
424+
max_tx_gas_used = max_tx_gas_used.max(gas_used);
425+
last_cumulative_gas = cumulative_gas;
426+
}
427+
428+
let transactions = block.transactions_recovered();
429+
430+
// Calculate payload sizes for all transactions
431+
let mut max_tx_payload_size = 0u64;
432+
let mut total_payload_size = 0u64;
433+
434+
for tx in transactions {
435+
// Get the EIP-2718 encoded length as payload size
436+
let payload_size = tx.encode_2718_len() as u64;
437+
max_tx_payload_size = max_tx_payload_size.max(payload_size);
438+
total_payload_size += payload_size;
439+
}
440+
441+
// sanity check the max gas used and transaction size value
442+
if max_tx_gas_used > header.gas_limit() {
443+
warn!(target: "scroll::gas_price_oracle", ?max_tx_gas_used, "Found tx consuming more gas than the block limit");
444+
return (Ok(suggestion), is_at_capacity);
445+
}
446+
if max_tx_payload_size > payload_size_limit {
447+
warn!(target: "scroll::gas_price_oracle", ?max_tx_payload_size, "Found tx consuming more size than the block size limit");
448+
return (Ok(suggestion), is_at_capacity);
449+
}
450+
451+
// if the block is at capacity, the suggestion must be increased
452+
if header.gas_used() + max_tx_gas_used > header.gas_limit() ||
453+
total_payload_size + max_tx_payload_size > payload_size_limit
454+
{
455+
let median_tip = match self.get_block_median_tip(header.hash()).await {
456+
Ok(Some(median_tip)) => median_tip,
457+
Ok(None) => return (Ok(suggestion), is_at_capacity),
458+
Err(e) => return (Err(e), is_at_capacity),
459+
};
460+
461+
let new_suggestion = median_tip + median_tip / U256::from(10);
462+
463+
if new_suggestion > suggestion {
464+
suggestion = new_suggestion;
465+
}
466+
is_at_capacity = true;
467+
}
468+
469+
// constrain to the max price
470+
if let Some(max_price) = self.oracle_config.max_price {
471+
if suggestion > max_price {
472+
suggestion = max_price;
473+
}
474+
}
475+
476+
// update the cache only if it's latest block header
477+
if block_number_or_tag == BlockNumberOrTag::Latest {
478+
inner.last_price = GasPriceOracleResult {
479+
block_hash: header.hash(),
480+
price: suggestion,
481+
is_at_capacity,
482+
};
483+
}
484+
(Ok(suggestion), is_at_capacity)
485+
}
486+
348487
/// Get the median tip value for the given block. This is useful for determining
349488
/// tips when a block is at capacity.
350489
///
@@ -409,11 +548,13 @@ pub struct GasPriceOracleResult {
409548
pub block_hash: B256,
410549
/// The price that the oracle calculated
411550
pub price: U256,
551+
/// Whether the latest block is at capacity
552+
pub is_at_capacity: bool,
412553
}
413554

414555
impl Default for GasPriceOracleResult {
415556
fn default() -> Self {
416-
Self { block_hash: B256::ZERO, price: U256::from(GWEI_TO_WEI) }
557+
Self { block_hash: B256::ZERO, price: U256::from(GWEI_TO_WEI), is_at_capacity: false }
417558
}
418559
}
419560

crates/scroll/rpc/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ scroll-alloy-rpc-types.workspace = true
4242
alloy-primitives.workspace = true
4343
alloy-rpc-types-eth.workspace = true
4444
alloy-consensus.workspace = true
45+
alloy-eips.workspace = true
4546
revm.workspace = true
4647
alloy-transport.workspace = true
4748
alloy-json-rpc.workspace = true

0 commit comments

Comments
 (0)