|
3 | 3 |
|
4 | 4 | use super::{EthApiError, EthResult, EthStateCache, RpcInvalidTransactionError}; |
5 | 5 | use alloy_consensus::{constants::GWEI_TO_WEI, BlockHeader, Transaction, TxReceipt}; |
6 | | -use alloy_eips::BlockNumberOrTag; |
| 6 | +use alloy_eips::{BlockNumberOrTag, Encodable2718}; |
7 | 7 | use alloy_primitives::{B256, U256}; |
8 | 8 | use alloy_rpc_types_eth::BlockId; |
9 | 9 | use derive_more::{Deref, DerefMut, From, Into}; |
@@ -205,7 +205,8 @@ where |
205 | 205 | } |
206 | 206 | } |
207 | 207 |
|
208 | | - inner.last_price = GasPriceOracleResult { block_hash: header.hash(), price }; |
| 208 | + inner.last_price = |
| 209 | + GasPriceOracleResult { block_hash: header.hash(), price, ..Default::default() }; |
209 | 210 |
|
210 | 211 | Ok(price) |
211 | 212 | } |
@@ -340,11 +341,149 @@ where |
340 | 341 | } |
341 | 342 | } |
342 | 343 |
|
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 | + }; |
344 | 349 |
|
345 | 350 | Ok(suggestion) |
346 | 351 | } |
347 | 352 |
|
| 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 | + |
348 | 487 | /// Get the median tip value for the given block. This is useful for determining |
349 | 488 | /// tips when a block is at capacity. |
350 | 489 | /// |
@@ -409,11 +548,13 @@ pub struct GasPriceOracleResult { |
409 | 548 | pub block_hash: B256, |
410 | 549 | /// The price that the oracle calculated |
411 | 550 | pub price: U256, |
| 551 | + /// Whether the latest block is at capacity |
| 552 | + pub is_at_capacity: bool, |
412 | 553 | } |
413 | 554 |
|
414 | 555 | impl Default for GasPriceOracleResult { |
415 | 556 | 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 } |
417 | 558 | } |
418 | 559 | } |
419 | 560 |
|
|
0 commit comments