diff --git a/crates/node/builder/src/rpc.rs b/crates/node/builder/src/rpc.rs index ea6022a50d..308b0206ae 100644 --- a/crates/node/builder/src/rpc.rs +++ b/crates/node/builder/src/rpc.rs @@ -1090,6 +1090,7 @@ impl<'a, N: FullNodeComponents>> .proof_permits(self.config.proof_permits) .gas_oracle_config(self.config.gas_oracle) .max_batch_size(self.config.max_batch_size) + .pending_block_kind(self.config.pending_block_kind) } } diff --git a/crates/node/core/src/args/rpc_server.rs b/crates/node/core/src/args/rpc_server.rs index 07a0eb9330..afcfd7f726 100644 --- a/crates/node/core/src/args/rpc_server.rs +++ b/crates/node/core/src/args/rpc_server.rs @@ -15,6 +15,7 @@ use clap::{ }; use rand::Rng; use reth_cli_util::parse_ether_value; +use reth_rpc_eth_types::builder::config::PendingBlockKind; use reth_rpc_server_types::{constants, RethRpcModule, RpcModuleSelection}; use crate::args::{ @@ -219,6 +220,13 @@ pub struct RpcServerArgs { #[arg(long = "rpc.proof-permits", alias = "rpc-proof-permits", value_name = "COUNT", default_value_t = constants::DEFAULT_PROOF_PERMITS)] pub rpc_proof_permits: usize, + /// Configures the pending block behavior for RPC responses. + /// + /// Options: full (include all transactions), empty (header only), none (disable pending + /// blocks). + #[arg(long = "rpc.pending-block", default_value = "full", value_name = "KIND")] + pub rpc_pending_block: PendingBlockKind, + /// Path to file containing disallowed addresses, json-encoded list of strings. Block /// validation API will reject blocks containing transactions from these addresses. #[arg(long = "builder.disallow", value_name = "PATH", value_parser = reth_cli_util::parsers::read_json_from_file::>)] @@ -363,6 +371,7 @@ impl Default for RpcServerArgs { rpc_tx_fee_cap: constants::DEFAULT_TX_FEE_CAP_WEI, rpc_max_simulate_blocks: constants::DEFAULT_MAX_SIMULATE_BLOCKS, rpc_eth_proof_window: constants::DEFAULT_ETH_PROOF_WINDOW, + rpc_pending_block: PendingBlockKind::Full, gas_price_oracle: GasPriceOracleArgs::default(), rpc_state_cache: RpcStateCacheArgs::default(), rpc_proof_permits: constants::DEFAULT_PROOF_PERMITS, diff --git a/crates/optimism/rpc/src/eth/pending_block.rs b/crates/optimism/rpc/src/eth/pending_block.rs index 5b50ea68f0..e14f1c332a 100644 --- a/crates/optimism/rpc/src/eth/pending_block.rs +++ b/crates/optimism/rpc/src/eth/pending_block.rs @@ -9,7 +9,7 @@ use reth_rpc_eth_api::{ helpers::{pending_block::PendingEnvBuilder, LoadPendingBlock}, FromEvmError, RpcConvert, RpcNodeCore, }; -use reth_rpc_eth_types::{EthApiError, PendingBlock}; +use reth_rpc_eth_types::{builder::config::PendingBlockKind, EthApiError, PendingBlock}; use reth_storage_api::{ BlockReader, BlockReaderIdExt, ProviderBlock, ProviderReceipt, ReceiptProvider, }; @@ -30,6 +30,11 @@ where self.inner.eth_api.pending_env_builder() } + #[inline] + fn pending_block_kind(&self) -> PendingBlockKind { + self.inner.eth_api.pending_block_kind() + } + /// Returns the locally built pending block async fn local_pending_block( &self, diff --git a/crates/rpc/rpc-builder/src/config.rs b/crates/rpc/rpc-builder/src/config.rs index 602f4e275e..e64a08aa31 100644 --- a/crates/rpc/rpc-builder/src/config.rs +++ b/crates/rpc/rpc-builder/src/config.rs @@ -103,6 +103,7 @@ impl RethRpcServerConfig for RpcServerArgs { .state_cache(self.state_cache_config()) .gpo_config(self.gas_price_oracle_config()) .proof_permits(self.rpc_proof_permits) + .pending_block_kind(self.rpc_pending_block) } fn flashbots_config(&self) -> ValidationApiConfig { diff --git a/crates/rpc/rpc-eth-api/src/helpers/pending_block.rs b/crates/rpc/rpc-eth-api/src/helpers/pending_block.rs index b564cd11b4..deb6883640 100644 --- a/crates/rpc/rpc-eth-api/src/helpers/pending_block.rs +++ b/crates/rpc/rpc-eth-api/src/helpers/pending_block.rs @@ -20,7 +20,10 @@ use reth_primitives_traits::{ }; use reth_revm::{database::StateProviderDatabase, db::State}; use reth_rpc_convert::RpcConvert; -use reth_rpc_eth_types::{EthApiError, PendingBlock, PendingBlockEnv, PendingBlockEnvOrigin}; +use reth_rpc_eth_types::{ + builder::config::PendingBlockKind, EthApiError, PendingBlock, PendingBlockEnv, + PendingBlockEnvOrigin, +}; use reth_storage_api::{ BlockReader, BlockReaderIdExt, ProviderBlock, ProviderHeader, ProviderReceipt, ProviderTx, ReceiptProvider, StateProviderFactory, @@ -54,6 +57,9 @@ pub trait LoadPendingBlock: /// Returns a [`PendingEnvBuilder`] for the pending block. fn pending_env_builder(&self) -> &dyn PendingEnvBuilder; + /// Returns the pending block kind + fn pending_block_kind(&self) -> PendingBlockKind; + /// Configures the [`PendingBlockEnv`] for the pending block /// /// If no pending block is available, this will derive it from the `latest` block @@ -130,6 +136,9 @@ pub trait LoadPendingBlock: TransactionPool>>, { async move { + if self.pending_block_kind() == PendingBlockKind::None { + return Ok(None); + } let pending = self.pending_block_env_and_cfg()?; let parent = match pending.origin { PendingBlockEnvOrigin::ActualPending(block, receipts) => { @@ -227,99 +236,103 @@ pub trait LoadPendingBlock: let mut sum_blob_gas_used = 0; let block_gas_limit: u64 = block_env.gas_limit; - let mut best_txs = - self.pool().best_transactions_with_attributes(BestTransactionsAttributes::new( - block_env.basefee, - block_env.blob_gasprice().map(|gasprice| gasprice as u64), - )); + // Only include transactions if not configured as Empty + if !self.pending_block_kind().is_empty() { + let mut best_txs = + self.pool().best_transactions_with_attributes(BestTransactionsAttributes::new( + block_env.basefee, + block_env.blob_gasprice().map(|gasprice| gasprice as u64), + )); - while let Some(pool_tx) = best_txs.next() { - // ensure we still have capacity for this transaction - if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit { - // we can't fit this transaction into the block, so we need to mark it as invalid - // which also removes all dependent transaction from the iterator before we can - // continue - best_txs.mark_invalid( - &pool_tx, - InvalidPoolTransactionError::ExceedsGasLimit( - pool_tx.gas_limit(), - block_gas_limit, - ), - ); - continue - } - - if pool_tx.origin.is_private() { - // we don't want to leak any state changes made by private transactions, so we mark - // them as invalid here which removes all dependent transactions from the iterator - // before we can continue - best_txs.mark_invalid( - &pool_tx, - InvalidPoolTransactionError::Consensus( - InvalidTransactionError::TxTypeNotSupported, - ), - ); - continue - } - - // convert tx to a signed transaction - let tx = pool_tx.to_consensus(); - - // There's only limited amount of blob space available per block, so we need to check if - // the EIP-4844 can still fit in the block - if let Some(tx_blob_gas) = tx.blob_gas_used() { - if sum_blob_gas_used + tx_blob_gas > blob_params.max_blob_gas_per_block() { - // we can't fit this _blob_ transaction into the block, so we mark it as - // invalid, which removes its dependent transactions from - // the iterator. This is similar to the gas limit condition - // for regular transactions above. + while let Some(pool_tx) = best_txs.next() { + // ensure we still have capacity for this transaction + if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit { + // we can't fit this transaction into the block, so we need to mark it as + // invalid which also removes all dependent transaction from + // the iterator before we can continue best_txs.mark_invalid( &pool_tx, InvalidPoolTransactionError::ExceedsGasLimit( - tx_blob_gas, - blob_params.max_blob_gas_per_block(), + pool_tx.gas_limit(), + block_gas_limit, ), ); continue } - } - let gas_used = match builder.execute_transaction(tx.clone()) { - Ok(gas_used) => gas_used, - Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx { - error, - .. - })) => { - if error.is_nonce_too_low() { - // if the nonce is too low, we can skip this transaction - } else { - // if the transaction is invalid, we can skip it and all of its - // descendants - best_txs.mark_invalid( - &pool_tx, - InvalidPoolTransactionError::Consensus( - InvalidTransactionError::TxTypeNotSupported, - ), - ); - } + if pool_tx.origin.is_private() { + // we don't want to leak any state changes made by private transactions, so we + // mark them as invalid here which removes all dependent + // transactions from the iteratorbefore we can continue + best_txs.mark_invalid( + &pool_tx, + InvalidPoolTransactionError::Consensus( + InvalidTransactionError::TxTypeNotSupported, + ), + ); continue } - // this is an error that we should treat as fatal for this attempt - Err(err) => return Err(Self::Error::from_eth_err(err)), - }; - // add to the total blob gas used if the transaction successfully executed - if let Some(tx_blob_gas) = tx.blob_gas_used() { - sum_blob_gas_used += tx_blob_gas; + // convert tx to a signed transaction + let tx = pool_tx.to_consensus(); - // if we've reached the max data gas per block, we can skip blob txs entirely - if sum_blob_gas_used == blob_params.max_blob_gas_per_block() { - best_txs.skip_blobs(); + // There's only limited amount of blob space available per block, so we need to + // check if the EIP-4844 can still fit in the block + if let Some(tx_blob_gas) = tx.blob_gas_used() { + if sum_blob_gas_used + tx_blob_gas > blob_params.max_blob_gas_per_block() { + // we can't fit this _blob_ transaction into the block, so we mark it as + // invalid, which removes its dependent transactions from + // the iterator. This is similar to the gas limit condition + // for regular transactions above. + best_txs.mark_invalid( + &pool_tx, + InvalidPoolTransactionError::ExceedsGasLimit( + tx_blob_gas, + blob_params.max_blob_gas_per_block(), + ), + ); + continue + } } - } - // add gas used by the transaction to cumulative gas used, before creating the receipt - cumulative_gas_used += gas_used; + let gas_used = match builder.execute_transaction(tx.clone()) { + Ok(gas_used) => gas_used, + Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx { + error, + .. + })) => { + if error.is_nonce_too_low() { + // if the nonce is too low, we can skip this transaction + } else { + // if the transaction is invalid, we can skip it and all of its + // descendants + best_txs.mark_invalid( + &pool_tx, + InvalidPoolTransactionError::Consensus( + InvalidTransactionError::TxTypeNotSupported, + ), + ); + } + continue + } + // this is an error that we should treat as fatal for this attempt + Err(err) => return Err(Self::Error::from_eth_err(err)), + }; + + // add to the total blob gas used if the transaction successfully executed + if let Some(tx_blob_gas) = tx.blob_gas_used() { + sum_blob_gas_used += tx_blob_gas; + + // if we've reached the max data gas per block, we can skip blob txs entirely + if sum_blob_gas_used == blob_params.max_blob_gas_per_block() { + best_txs.skip_blobs(); + } + } + + // add gas used by the transaction to cumulative gas used, before creating the + // receipt + cumulative_gas_used += gas_used; + } } let BlockBuilderOutcome { execution_result, block, hashed_state, .. } = diff --git a/crates/rpc/rpc-eth-types/src/builder/config.rs b/crates/rpc/rpc-eth-types/src/builder/config.rs index 8b2fd229ba..10ab83ae66 100644 --- a/crates/rpc/rpc-eth-types/src/builder/config.rs +++ b/crates/rpc/rpc-eth-types/src/builder/config.rs @@ -15,6 +15,47 @@ use serde::{Deserialize, Serialize}; /// Default value for stale filter ttl pub const DEFAULT_STALE_FILTER_TTL: Duration = Duration::from_secs(5 * 60); +/// Config for the locally built pending block +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum PendingBlockKind { + /// Return a pending block with header only, no transactions included + Empty, + /// Return null/no pending block + None, + /// Return a pending block with all transactions from the mempool (default behavior) + #[default] + Full, +} + +impl std::str::FromStr for PendingBlockKind { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "empty" => Ok(Self::Empty), + "none" => Ok(Self::None), + "full" => Ok(Self::Full), + _ => Err(format!( + "Invalid pending block kind: {}. Valid options are: empty, none, full", + s + )), + } + } +} + +impl PendingBlockKind { + /// Returns true if the pending block kind is `None` + pub const fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + /// Returns true if the pending block kind is `Empty` + pub const fn is_empty(&self) -> bool { + matches!(self, Self::Empty) + } +} + /// Additional config values for the eth namespace. #[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] pub struct EthConfig { @@ -47,6 +88,8 @@ pub struct EthConfig { pub proof_permits: usize, /// Maximum batch size for transaction pool insertions. pub max_batch_size: usize, + /// Controls how pending blocks are built when requested via RPC methods + pub pending_block_kind: PendingBlockKind, } impl EthConfig { @@ -75,6 +118,7 @@ impl Default for EthConfig { fee_history_cache: FeeHistoryCacheConfig::default(), proof_permits: DEFAULT_PROOF_PERMITS, max_batch_size: 1, + pending_block_kind: PendingBlockKind::Full, } } } @@ -145,6 +189,12 @@ impl EthConfig { self.max_batch_size = max_batch_size; self } + + /// Configures the pending block config + pub const fn pending_block_kind(mut self, pending_block_kind: PendingBlockKind) -> Self { + self.pending_block_kind = pending_block_kind; + self + } } /// Config for the filter diff --git a/crates/rpc/rpc/src/eth/builder.rs b/crates/rpc/rpc/src/eth/builder.rs index 6ef68acbe2..fada116afe 100644 --- a/crates/rpc/rpc/src/eth/builder.rs +++ b/crates/rpc/rpc/src/eth/builder.rs @@ -10,9 +10,9 @@ use reth_rpc_eth_api::{ helpers::pending_block::PendingEnvBuilder, node::RpcNodeCoreAdapter, RpcNodeCore, }; use reth_rpc_eth_types::{ - fee_history::fee_history_cache_new_blocks_task, receipt::EthReceiptConverter, EthStateCache, - EthStateCacheConfig, FeeHistoryCache, FeeHistoryCacheConfig, GasCap, GasPriceOracle, - GasPriceOracleConfig, + builder::config::PendingBlockKind, fee_history::fee_history_cache_new_blocks_task, + receipt::EthReceiptConverter, EthStateCache, EthStateCacheConfig, FeeHistoryCache, + FeeHistoryCacheConfig, GasCap, GasPriceOracle, GasPriceOracleConfig, }; use reth_rpc_server_types::constants::{ DEFAULT_ETH_PROOF_WINDOW, DEFAULT_MAX_SIMULATE_BLOCKS, DEFAULT_PROOF_PERMITS, @@ -41,6 +41,7 @@ pub struct EthApiBuilder { task_spawner: Box, next_env: NextEnv, max_batch_size: usize, + pending_block_kind: PendingBlockKind, } impl @@ -80,6 +81,7 @@ impl EthApiBuilder { task_spawner, next_env, max_batch_size, + pending_block_kind, } = self; EthApiBuilder { components, @@ -97,6 +99,7 @@ impl EthApiBuilder { task_spawner, next_env, max_batch_size, + pending_block_kind, } } } @@ -125,6 +128,7 @@ where eth_state_cache_config: Default::default(), next_env: Default::default(), max_batch_size: 1, + pending_block_kind: PendingBlockKind::Full, } } } @@ -160,6 +164,7 @@ where gas_oracle_config, next_env, max_batch_size, + pending_block_kind, } = self; EthApiBuilder { components, @@ -177,6 +182,7 @@ where gas_oracle_config, next_env, max_batch_size, + pending_block_kind, } } @@ -201,6 +207,7 @@ where gas_oracle_config, next_env: _, max_batch_size, + pending_block_kind, } = self; EthApiBuilder { components, @@ -218,6 +225,7 @@ where gas_oracle_config, next_env, max_batch_size, + pending_block_kind, } } @@ -295,6 +303,12 @@ where self } + /// Sets the pending block kind + pub const fn pending_block_kind(mut self, pending_block_kind: PendingBlockKind) -> Self { + self.pending_block_kind = pending_block_kind; + self + } + /// Builds the [`EthApiInner`] instance. /// /// If not configured, this will spawn the cache backend: [`EthStateCache::spawn`]. @@ -324,6 +338,7 @@ where task_spawner, next_env, max_batch_size, + pending_block_kind, } = self; let provider = components.provider().clone(); @@ -361,6 +376,7 @@ where rpc_converter, next_env, max_batch_size, + pending_block_kind, ) } diff --git a/crates/rpc/rpc/src/eth/core.rs b/crates/rpc/rpc/src/eth/core.rs index 6868f15a02..d1b0374da6 100644 --- a/crates/rpc/rpc/src/eth/core.rs +++ b/crates/rpc/rpc/src/eth/core.rs @@ -20,8 +20,8 @@ use reth_rpc_eth_api::{ EthApiTypes, RpcNodeCore, }; use reth_rpc_eth_types::{ - receipt::EthReceiptConverter, EthApiError, EthStateCache, FeeHistoryCache, GasCap, - GasPriceOracle, PendingBlock, + builder::config::PendingBlockKind, receipt::EthReceiptConverter, EthApiError, EthStateCache, + FeeHistoryCache, GasCap, GasPriceOracle, PendingBlock, }; use reth_storage_api::{noop::NoopProvider, BlockReaderIdExt, ProviderHeader}; use reth_tasks::{ @@ -151,6 +151,7 @@ where proof_permits: usize, rpc_converter: Rpc, max_batch_size: usize, + pending_block_kind: PendingBlockKind, ) -> Self { let inner = EthApiInner::new( components, @@ -166,6 +167,7 @@ where rpc_converter, (), max_batch_size, + pending_block_kind, ); Self { inner: Arc::new(inner) } @@ -299,6 +301,9 @@ pub struct EthApiInner { /// Transaction batch sender for batching tx insertions tx_batch_sender: mpsc::UnboundedSender::Transaction>>, + + /// Configuration for pending block construction. + pending_block_kind: PendingBlockKind, } impl EthApiInner @@ -322,6 +327,7 @@ where tx_resp_builder: Rpc, next_env: impl PendingEnvBuilder, max_batch_size: usize, + pending_block_kind: PendingBlockKind, ) -> Self { let signers = parking_lot::RwLock::new(Default::default()); // get the block number of the latest block @@ -360,6 +366,7 @@ where tx_resp_builder, next_env_builder: Box::new(next_env), tx_batch_sender, + pending_block_kind, } } } @@ -513,6 +520,12 @@ where Ok(response_rx.await??) } + + /// Returns the pending block kind + #[inline] + pub const fn pending_block_kind(&self) -> PendingBlockKind { + self.pending_block_kind + } } #[cfg(test)] diff --git a/crates/rpc/rpc/src/eth/helpers/pending_block.rs b/crates/rpc/rpc/src/eth/helpers/pending_block.rs index 5e007c340f..0c08c12e0e 100644 --- a/crates/rpc/rpc/src/eth/helpers/pending_block.rs +++ b/crates/rpc/rpc/src/eth/helpers/pending_block.rs @@ -6,7 +6,7 @@ use reth_rpc_eth_api::{ helpers::{pending_block::PendingEnvBuilder, LoadPendingBlock}, FromEvmError, RpcNodeCore, }; -use reth_rpc_eth_types::{EthApiError, PendingBlock}; +use reth_rpc_eth_types::{builder::config::PendingBlockKind, EthApiError, PendingBlock}; impl LoadPendingBlock for EthApi where @@ -23,4 +23,9 @@ where fn pending_env_builder(&self) -> &dyn PendingEnvBuilder { self.inner.pending_env_builder() } + + #[inline] + fn pending_block_kind(&self) -> PendingBlockKind { + self.inner.pending_block_kind() + } } diff --git a/docs/vocs/docs/pages/cli/reth/node.mdx b/docs/vocs/docs/pages/cli/reth/node.mdx index ed0b012dd8..1ac8338416 100644 --- a/docs/vocs/docs/pages/cli/reth/node.mdx +++ b/docs/vocs/docs/pages/cli/reth/node.mdx @@ -400,6 +400,13 @@ RPC: [default: 25] + --rpc.pending-block + Configures the pending block behavior for RPC responses. + + Options: full (include all transactions), empty (header only), none (disable pending blocks). + + [default: full] + --builder.disallow Path to file containing disallowed addresses, json-encoded list of strings. Block validation API will reject blocks containing transactions from these addresses