From 2797eb12f5d1c537f1f1b4d67dc73343cf7e3f29 Mon Sep 17 00:00:00 2001 From: Gregory Edison Date: Fri, 8 Aug 2025 17:37:05 +0200 Subject: [PATCH 1/6] feat: limit rollup fee in tx pool Signed-off-by: Gregory Edison --- Cargo.lock | 1 + crates/scroll/consensus/src/constants.rs | 4 ++++ crates/scroll/consensus/src/lib.rs | 4 +++- crates/scroll/txpool/Cargo.toml | 1 + crates/scroll/txpool/src/validator.rs | 9 +++++++++ 5 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 crates/scroll/consensus/src/constants.rs diff --git a/Cargo.lock b/Cargo.lock index 97d81f9d9a6..792f4c3ee8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10618,6 +10618,7 @@ dependencies = [ "reth-provider", "reth-revm", "reth-scroll-chainspec", + "reth-scroll-consensus", "reth-scroll-evm", "reth-scroll-forks", "reth-scroll-primitives", diff --git a/crates/scroll/consensus/src/constants.rs b/crates/scroll/consensus/src/constants.rs new file mode 100644 index 00000000000..88c6f798d0a --- /dev/null +++ b/crates/scroll/consensus/src/constants.rs @@ -0,0 +1,4 @@ +use alloy_primitives::U256; + +/// The maximum value Rollup fee. +pub const MAX_ROLLUP_FEE: U256 = U256::from_limbs([u64::MAX, 0, 0, 0]); diff --git a/crates/scroll/consensus/src/lib.rs b/crates/scroll/consensus/src/lib.rs index 2bf959bc63b..3483ae79067 100644 --- a/crates/scroll/consensus/src/lib.rs +++ b/crates/scroll/consensus/src/lib.rs @@ -2,9 +2,11 @@ extern crate alloc; +mod constants; +pub use constants::MAX_ROLLUP_FEE; + mod error; pub use error::ScrollConsensusError; mod validation; - pub use validation::ScrollBeaconConsensus; diff --git a/crates/scroll/txpool/Cargo.toml b/crates/scroll/txpool/Cargo.toml index 38b29b76bd7..138e7ebb8bf 100644 --- a/crates/scroll/txpool/Cargo.toml +++ b/crates/scroll/txpool/Cargo.toml @@ -28,6 +28,7 @@ reth-transaction-pool.workspace = true revm-scroll.workspace = true # reth-scroll +reth-scroll-consensus.workspace = true reth-scroll-evm.workspace = true reth-scroll-forks.workspace = true reth-scroll-primitives.workspace = true diff --git a/crates/scroll/txpool/src/validator.rs b/crates/scroll/txpool/src/validator.rs index 3b6ae3aac4f..eaa30560cdc 100644 --- a/crates/scroll/txpool/src/validator.rs +++ b/crates/scroll/txpool/src/validator.rs @@ -6,6 +6,7 @@ use reth_primitives_traits::{ transaction::error::InvalidTransactionError, Block, GotExpected, SealedBlock, }; use reth_revm::database::StateProviderDatabase; +use reth_scroll_consensus::MAX_ROLLUP_FEE; use reth_scroll_evm::{ compute_compression_ratio, spec_id_at_timestamp_and_number, RethL1BlockInfo, }; @@ -181,6 +182,14 @@ where return TransactionValidationOutcome::Error(*valid_tx.hash(), Box::new(err)) } }; + // Check rollup fee is under u64::MAX. + if cost_addition > MAX_ROLLUP_FEE { + return TransactionValidationOutcome::Invalid( + valid_tx.into_transaction(), + InvalidTransactionError::GasUintOverflow.into(), + ) + } + let cost = valid_tx.transaction().cost().saturating_add(cost_addition); // Checks for max cost From 6eee35ccf289af58c1af271d841391884bc464cb Mon Sep 17 00:00:00 2001 From: Gregory Edison Date: Mon, 11 Aug 2025 16:42:11 +0200 Subject: [PATCH 2/6] fix: check rollup fee Signed-off-by: Gregory Edison --- crates/scroll/txpool/src/validator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/scroll/txpool/src/validator.rs b/crates/scroll/txpool/src/validator.rs index eaa30560cdc..0f7ea42f582 100644 --- a/crates/scroll/txpool/src/validator.rs +++ b/crates/scroll/txpool/src/validator.rs @@ -183,7 +183,7 @@ where } }; // Check rollup fee is under u64::MAX. - if cost_addition > MAX_ROLLUP_FEE { + if cost_addition >= MAX_ROLLUP_FEE { return TransactionValidationOutcome::Invalid( valid_tx.into_transaction(), InvalidTransactionError::GasUintOverflow.into(), From 2686f6e34e40c8643d81d54ed08c0512ff85160d Mon Sep 17 00:00:00 2001 From: greg <82421016+greged93@users.noreply.github.com> Date: Wed, 13 Aug 2025 15:41:30 +0200 Subject: [PATCH 3/6] feat: limit pool tx size (#317) * feat: add maxTxPayloadBytesPerBlock to ScrollChainConfig Signed-off-by: Gregory Edison * feat: limit pool max tx input bytes Signed-off-by: Gregory Edison * test: set max_tx_payload_bytes_per_block Signed-off-by: Gregory Edison * feat: make MockEthProvider more generic Signed-off-by: Gregory Edison * test: add pool limit tests Signed-off-by: Gregory Edison * fix: lints Signed-off-by: Gregory Edison --------- Signed-off-by: Gregory Edison --- crates/scroll/chainspec/src/constants.rs | 3 + crates/scroll/chainspec/src/genesis.rs | 18 +- crates/scroll/chainspec/src/lib.rs | 48 ++--- crates/scroll/node/src/builder/pool.rs | 156 ++++++++++++++ crates/scroll/node/src/test_utils.rs | 12 +- .../storage/provider/src/test_utils/mock.rs | 193 ++++++++++-------- 6 files changed, 310 insertions(+), 120 deletions(-) diff --git a/crates/scroll/chainspec/src/constants.rs b/crates/scroll/chainspec/src/constants.rs index d41916c5732..a95fba0ee46 100644 --- a/crates/scroll/chainspec/src/constants.rs +++ b/crates/scroll/chainspec/src/constants.rs @@ -5,6 +5,9 @@ use alloy_primitives::{address, b256, Address, B256}; /// The transaction fee recipient on the L2. pub const SCROLL_FEE_VAULT_ADDRESS: Address = address!("5300000000000000000000000000000000000005"); +/// The maximum size in bytes of the payload for a block. +pub const MAX_TX_PAYLOAD_BYTES_PER_BLOCK: usize = 120 * 1024; + /// The system contract on L2 mainnet. pub const SCROLL_MAINNET_L2_SYSTEM_CONFIG_CONTRACT_ADDRESS: Address = address!("331A873a2a85219863d80d248F9e2978fE88D0Ea"); diff --git a/crates/scroll/chainspec/src/genesis.rs b/crates/scroll/chainspec/src/genesis.rs index c82d8600893..c522461ec01 100644 --- a/crates/scroll/chainspec/src/genesis.rs +++ b/crates/scroll/chainspec/src/genesis.rs @@ -1,9 +1,13 @@ //! Scroll types for genesis data. use crate::{ - constants::{SCROLL_FEE_VAULT_ADDRESS, SCROLL_MAINNET_L1_CONFIG, SCROLL_SEPOLIA_L1_CONFIG}, + constants::{ + MAX_TX_PAYLOAD_BYTES_PER_BLOCK, SCROLL_FEE_VAULT_ADDRESS, SCROLL_MAINNET_L1_CONFIG, + SCROLL_SEPOLIA_L1_CONFIG, + }, SCROLL_DEV_L1_CONFIG, }; + use alloy_primitives::Address; use alloy_serde::OtherFields; use serde::de::Error; @@ -113,6 +117,8 @@ pub struct ScrollChainConfig { /// This is an optional field that, when set, specifies where L2 transaction fees /// will be sent or stored. pub fee_vault_address: Option
, + /// The maximum tx payload size of blocks that we produce. + pub max_tx_payload_bytes_per_block: usize, /// The L1 configuration. /// This field encapsulates specific settings and parameters required for L1 pub l1_config: L1Config, @@ -129,6 +135,7 @@ impl ScrollChainConfig { pub const fn mainnet() -> Self { Self { fee_vault_address: Some(SCROLL_FEE_VAULT_ADDRESS), + max_tx_payload_bytes_per_block: MAX_TX_PAYLOAD_BYTES_PER_BLOCK, l1_config: SCROLL_MAINNET_L1_CONFIG, } } @@ -137,13 +144,18 @@ impl ScrollChainConfig { pub const fn sepolia() -> Self { Self { fee_vault_address: Some(SCROLL_FEE_VAULT_ADDRESS), + max_tx_payload_bytes_per_block: MAX_TX_PAYLOAD_BYTES_PER_BLOCK, l1_config: SCROLL_SEPOLIA_L1_CONFIG, } } /// Returns the [`ScrollChainConfig`] for Scroll dev. pub const fn dev() -> Self { - Self { fee_vault_address: Some(SCROLL_FEE_VAULT_ADDRESS), l1_config: SCROLL_DEV_L1_CONFIG } + Self { + fee_vault_address: Some(SCROLL_FEE_VAULT_ADDRESS), + max_tx_payload_bytes_per_block: MAX_TX_PAYLOAD_BYTES_PER_BLOCK, + l1_config: SCROLL_DEV_L1_CONFIG, + } } } @@ -209,6 +221,7 @@ mod tests { "feynmanTime": 100, "scroll": { "feeVaultAddress": "0x5300000000000000000000000000000000000005", + "maxTxPayloadBytesPerBlock": 122880, "l1Config": { "l1ChainId": 1, "l1MessageQueueAddress": "0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B", @@ -237,6 +250,7 @@ mod tests { }), scroll_chain_config: ScrollChainConfig { fee_vault_address: Some(address!("5300000000000000000000000000000000000005")), + max_tx_payload_bytes_per_block: MAX_TX_PAYLOAD_BYTES_PER_BLOCK, l1_config: L1Config { l1_chain_id: 1, l1_message_queue_address: address!("0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B"), diff --git a/crates/scroll/chainspec/src/lib.rs b/crates/scroll/chainspec/src/lib.rs index 7668044ae61..ccd15550e99 100644 --- a/crates/scroll/chainspec/src/lib.rs +++ b/crates/scroll/chainspec/src/lib.rs @@ -34,10 +34,10 @@ extern crate alloc; mod constants; pub use constants::{ - SCROLL_BASE_FEE_PARAMS_FEYNMAN, SCROLL_DEV_L1_CONFIG, SCROLL_DEV_L1_MESSAGE_QUEUE_ADDRESS, - SCROLL_DEV_L1_MESSAGE_QUEUE_V2_ADDRESS, SCROLL_DEV_L1_PROXY_ADDRESS, - SCROLL_DEV_L2_SYSTEM_CONFIG_CONTRACT_ADDRESS, SCROLL_DEV_MAX_L1_MESSAGES, - SCROLL_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_FEYNMAN, + MAX_TX_PAYLOAD_BYTES_PER_BLOCK, SCROLL_BASE_FEE_PARAMS_FEYNMAN, SCROLL_DEV_L1_CONFIG, + SCROLL_DEV_L1_MESSAGE_QUEUE_ADDRESS, SCROLL_DEV_L1_MESSAGE_QUEUE_V2_ADDRESS, + SCROLL_DEV_L1_PROXY_ADDRESS, SCROLL_DEV_L2_SYSTEM_CONFIG_CONTRACT_ADDRESS, + SCROLL_DEV_MAX_L1_MESSAGES, SCROLL_EIP1559_BASE_FEE_MAX_CHANGE_DENOMINATOR_FEYNMAN, SCROLL_EIP1559_DEFAULT_ELASTICITY_MULTIPLIER_FEYNMAN, SCROLL_FEE_VAULT_ADDRESS, SCROLL_MAINNET_GENESIS_HASH, SCROLL_MAINNET_L1_CONFIG, SCROLL_MAINNET_L1_MESSAGE_QUEUE_ADDRESS, SCROLL_MAINNET_L1_MESSAGE_QUEUE_V2_ADDRESS, SCROLL_MAINNET_L1_PROXY_ADDRESS, @@ -624,26 +624,26 @@ mod tests { #[test] fn parse_scroll_hardforks() { let geth_genesis = r#" - { - "config": { - "bernoulliBlock": 10, - "curieBlock": 20, - "darwinTime": 30, - "darwinV2Time": 31, - "scroll": { - "feeVaultAddress": "0x5300000000000000000000000000000000000005", - "l1Config": { - "l1ChainId": 1, - "l1MessageQueueAddress": "0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B", - "l1MessageQueueV2Address": "0x56971da63A3C0205184FEF096E9ddFc7A8C2D18a", - "l2SystemConfigAddress": "0x331A873a2a85219863d80d248F9e2978fE88D0Ea", - "scrollChainAddress": "0xa13BAF47339d63B743e7Da8741db5456DAc1E556", - "numL1MessagesPerBlock": 10 + { + "config": { + "bernoulliBlock": 10, + "curieBlock": 20, + "darwinTime": 30, + "darwinV2Time": 31, + "scroll": { + "feeVaultAddress": "0x5300000000000000000000000000000000000005", + "maxTxPayloadBytesPerBlock": 122880, + "l1Config": { + "l1ChainId": 1, + "l1MessageQueueAddress": "0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B", + "l1MessageQueueV2Address": "0x56971da63A3C0205184FEF096E9ddFc7A8C2D18a", + "l2SystemConfigAddress": "0x331A873a2a85219863d80d248F9e2978fE88D0Ea", + "scrollChainAddress": "0xa13BAF47339d63B743e7Da8741db5456DAc1E556", + "numL1MessagesPerBlock": 10 + } + } } - } - } - } - "#; + }"#; let genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); let actual_bernoulli_block = genesis.config.extra_fields.get("bernoulliBlock"); @@ -659,6 +659,7 @@ mod tests { scroll_object, &serde_json::json!({ "feeVaultAddress": "0x5300000000000000000000000000000000000005", + "maxTxPayloadBytesPerBlock": 122880, "l1Config": { "l1ChainId": 1, "l1MessageQueueAddress": "0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B", @@ -712,6 +713,7 @@ mod tests { String::from("scroll"), serde_json::json!({ "feeVaultAddress": "0x5300000000000000000000000000000000000005", + "maxTxPayloadBytesPerBlock": 122880, "l1Config": { "l1ChainId": 1, "l1MessageQueueAddress": "0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B", diff --git a/crates/scroll/node/src/builder/pool.rs b/crates/scroll/node/src/builder/pool.rs index 8152a5fe5e7..70faed0f901 100644 --- a/crates/scroll/node/src/builder/pool.rs +++ b/crates/scroll/node/src/builder/pool.rs @@ -68,6 +68,7 @@ where .with_local_transactions_config( pool_config_overrides.clone().apply(ctx.pool_config()).local_transactions_config, ) + .with_max_tx_input_bytes(ctx.chain_spec().chain_config().max_tx_payload_bytes_per_block) .with_additional_tasks( pool_config_overrides .additional_validation_tasks @@ -130,3 +131,158 @@ where Ok(transaction_pool) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ScrollNode; + + use alloy_consensus::{transaction::Recovered, Header, Signed, TxLegacy}; + use alloy_primitives::{private::rand::random_iter, Bytes, Signature, B256, U256}; + use reth_chainspec::Head; + use reth_db::mock::DatabaseMock; + use reth_node_api::FullNodeTypesAdapter; + use reth_node_builder::common::WithConfigs; + use reth_node_core::node_config::NodeConfig; + use reth_primitives_traits::{ + transaction::error::InvalidTransactionError, GotExpected, GotExpectedBoxed, + }; + use reth_provider::{ + noop::NoopProvider, + test_utils::{ExtendedAccount, MockEthProvider}, + }; + use reth_scroll_chainspec::{ScrollChainSpec, SCROLL_DEV, SCROLL_MAINNET}; + use reth_scroll_primitives::{ScrollBlock, ScrollPrimitives}; + use reth_scroll_txpool::ScrollPooledTransaction; + use reth_tasks::TaskManager; + use reth_transaction_pool::{ + blobstore::NoopBlobStore, + error::{InvalidPoolTransactionError, PoolErrorKind}, + PoolConfig, TransactionOrigin, TransactionPool, + }; + use scroll_alloy_consensus::ScrollTxEnvelope; + use scroll_alloy_evm::curie::L1_GAS_PRICE_ORACLE_ADDRESS; + + async fn pool() -> ( + ScrollTransactionPool, DiskFileBlobStore>, + TaskManager, + ) { + let handle = tokio::runtime::Handle::current(); + let manager = TaskManager::new(handle); + let config = WithConfigs { + config: NodeConfig::new(SCROLL_MAINNET.clone()), + toml_config: Default::default(), + }; + + let pool_builder = ScrollPoolBuilder::::default(); + let ctx = BuilderContext::< + FullNodeTypesAdapter< + ScrollNode, + DatabaseMock, + NoopProvider, + >, + >::new( + Head::default(), + NoopProvider::new(SCROLL_MAINNET.clone()), + manager.executor(), + config, + ); + (pool_builder.build_pool(&ctx).await.unwrap(), manager) + } + + #[tokio::test] + async fn test_validate_one_oversized_transaction() { + // create the pool. + let (pool, manager) = pool().await; + let tx = ScrollTxEnvelope::Legacy(Signed::new_unchecked( + TxLegacy { gas_limit: 21_000, ..Default::default() }, + Signature::new(U256::ZERO, U256::ZERO, false), + Default::default(), + )); + + // Create a pool transaction with an encoded length of 123,904 bytes. + let pool_tx = ScrollPooledTransaction::new( + Recovered::new_unchecked(tx, Default::default()), + 121 * 1024, + ); + + // add the transaction to the pool and expect an `OversizedData` error. + let err = pool.add_transaction(TransactionOrigin::Local, pool_tx).await.unwrap_err(); + assert!(matches!( + err.kind, + PoolErrorKind::InvalidTransaction( + InvalidPoolTransactionError::OversizedData(x, y,) + ) if x == 121*1024 && y == 120*1024, + )); + + // explicitly drop the manager here otherwise the `TransactionValidationTaskExecutor` will + // drop all validation tasks. + drop(manager); + } + + #[tokio::test] + async fn test_validate_one_rollup_fee_exceeds_balance() { + // create the client. + let handle = tokio::runtime::Handle::current(); + let manager = TaskManager::new(handle); + let blob_store = NoopBlobStore::default(); + let signer = Default::default(); + let client = + MockEthProvider::::new().with_chain_spec(SCROLL_DEV.clone()); + let hash = B256::random(); + + // load a header, block, signer and the L1_GAS_PRICE_ORACLE_ADDRESS storage. + client.add_header(hash, Header::default()); + client.add_block(hash, ScrollBlock::default()); + client.add_account(signer, ExtendedAccount::new(0, U256::from(400_000))); + client.add_account( + L1_GAS_PRICE_ORACLE_ADDRESS, + ExtendedAccount::new(0, U256::from(400_000)).extend_storage( + (0u8..8).map(|k| (B256::from(U256::from(k)), U256::from(u64::MAX))), + ), + ); + + // create the validation task. + let validator = TransactionValidationTaskExecutor::eth_builder(client) + .no_eip4844() + .build_with_tasks(manager.executor(), blob_store) + .map(|validator| { + ScrollTransactionValidator::new(validator).require_l1_data_gas_fee(true) + }); + + // create the pool. + let pool = ScrollTransactionPool::new( + validator, + CoinbaseTipOrdering::::default(), + NoopBlobStore::default(), + PoolConfig::default(), + ); + + // prepare a transaction with random input. + let tx = ScrollTxEnvelope::Legacy(Signed::new_unchecked( + TxLegacy { + gas_limit: 55_000, + gas_price: 7, + input: Bytes::from(random_iter::().take(100).collect::>()), + ..Default::default() + }, + Signature::new(U256::ZERO, U256::ZERO, false), + Default::default(), + )); + let pool_tx = + ScrollPooledTransaction::new(Recovered::new_unchecked(tx, signer), 120 * 1024); + + // add the transaction in the pool and expect to hit `InsufficientFunds` error. + let err = pool.add_transaction(TransactionOrigin::Local, pool_tx).await.unwrap_err(); + assert!(matches!( + err.kind, + PoolErrorKind::InvalidTransaction( + InvalidPoolTransactionError::Consensus(InvalidTransactionError::InsufficientFunds(GotExpectedBoxed(expected))) + ) if *expected == GotExpected{ got: U256::from(400000), expected: U256::from_limbs([384999, 1, 0, 0]) } + )); + + // explicitly drop the manager here otherwise the `TransactionValidationTaskExecutor` will + // drop all validation tasks. + drop(manager); + } +} diff --git a/crates/scroll/node/src/test_utils.rs b/crates/scroll/node/src/test_utils.rs index 1e7141895f5..be464870cbf 100644 --- a/crates/scroll/node/src/test_utils.rs +++ b/crates/scroll/node/src/test_utils.rs @@ -9,7 +9,7 @@ use reth_node_api::NodeTypesWithDBAdapter; use reth_payload_builder::EthPayloadBuilderAttributes; use reth_provider::providers::BlockchainProvider; -use reth_scroll_chainspec::ScrollChainSpecBuilder; +use reth_scroll_chainspec::{ScrollChainConfig, ScrollChainSpecBuilder}; use reth_tasks::TaskManager; use scroll_alloy_rpc_types_engine::BlockDataHint; use std::sync::Arc; @@ -31,10 +31,12 @@ pub async fn setup( reth_e2e_test_utils::setup_engine( num_nodes, Arc::new( - ScrollChainSpecBuilder::scroll_mainnet() - .genesis(genesis) - .euclid_v2_activated() - .build(Default::default()), + ScrollChainSpecBuilder::scroll_mainnet().genesis(genesis).euclid_v2_activated().build( + ScrollChainConfig { + max_tx_payload_bytes_per_block: 120 * 1024, + ..Default::default() + }, + ), ), is_dev, Default::default(), diff --git a/crates/storage/provider/src/test_utils/mock.rs b/crates/storage/provider/src/test_utils/mock.rs index 68f8c38e59d..7636b27db4a 100644 --- a/crates/storage/provider/src/test_utils/mock.rs +++ b/crates/storage/provider/src/test_utils/mock.rs @@ -5,7 +5,7 @@ use crate::{ StateProvider, StateProviderBox, StateProviderFactory, StateReader, StateRootProvider, TransactionVariant, TransactionsProvider, }; -use alloy_consensus::{constants::EMPTY_ROOT_HASH, transaction::TransactionMeta, Header}; +use alloy_consensus::{constants::EMPTY_ROOT_HASH, transaction::TransactionMeta, BlockHeader}; use alloy_eips::{BlockHashOrNumber, BlockId, BlockNumberOrTag}; use alloy_primitives::{ keccak256, map::HashMap, Address, BlockHash, BlockNumber, Bytes, StorageKey, StorageValue, @@ -19,11 +19,12 @@ use reth_db_api::{ models::{AccountBeforeTx, StoredBlockBodyIndices}, }; use reth_ethereum_engine_primitives::EthEngineTypes; -use reth_ethereum_primitives::{EthPrimitives, Receipt}; +use reth_ethereum_primitives::EthPrimitives; use reth_execution_types::ExecutionOutcome; use reth_node_types::NodeTypes; use reth_primitives_traits::{ - Account, Bytecode, GotExpected, NodePrimitives, RecoveredBlock, SealedHeader, SignerRecoverable, + Account, Block, BlockBody, Bytecode, GotExpected, NodePrimitives, RecoveredBlock, SealedHeader, + SignedTransaction, SignerRecoverable, }; use reth_prune_types::PruneModes; use reth_stages_types::{StageCheckpoint, StageId}; @@ -48,16 +49,14 @@ use tokio::sync::broadcast; /// A mock implementation for Provider interfaces. #[derive(Debug)] -pub struct MockEthProvider< - T: NodePrimitives = reth_ethereum_primitives::EthPrimitives, - ChainSpec = reth_chainspec::ChainSpec, -> { +pub struct MockEthProvider +{ ///local block store pub blocks: Arc>>, /// Local header store - pub headers: Arc>>, + pub headers: Arc::Header>>>, /// Local receipt store indexed by block number - pub receipts: Arc>>>, + pub receipts: Arc>>>, /// Local account store pub accounts: Arc>>, /// Local chain spec @@ -106,31 +105,31 @@ impl MockEthProvider { } } -impl MockEthProvider { +impl MockEthProvider { /// Add block to local block store - pub fn add_block(&self, hash: B256, block: reth_ethereum_primitives::Block) { - self.add_header(hash, block.header.clone()); + pub fn add_block(&self, hash: B256, block: T::Block) { + self.add_header(hash, block.header().clone()); self.blocks.lock().insert(hash, block); } /// Add multiple blocks to local block store - pub fn extend_blocks( - &self, - iter: impl IntoIterator, - ) { + pub fn extend_blocks(&self, iter: impl IntoIterator) { for (hash, block) in iter { - self.add_header(hash, block.header.clone()); + self.add_header(hash, block.header().clone()); self.add_block(hash, block) } } /// Add header to local header store - pub fn add_header(&self, hash: B256, header: Header) { + pub fn add_header(&self, hash: B256, header: ::Header) { self.headers.lock().insert(hash, header); } /// Add multiple headers to local header store - pub fn extend_headers(&self, iter: impl IntoIterator) { + pub fn extend_headers( + &self, + iter: impl IntoIterator::Header)>, + ) { for (hash, header) in iter { self.add_header(hash, header) } @@ -149,12 +148,12 @@ impl MockEthProvider) { + pub fn add_receipts(&self, block_number: BlockNumber, receipts: Vec) { self.receipts.lock().insert(block_number, receipts); } /// Add multiple receipts to local receipt store - pub fn extend_receipts(&self, iter: impl IntoIterator)>) { + pub fn extend_receipts(&self, iter: impl IntoIterator)>) { for (block_number, receipts) in iter { self.add_receipts(block_number, receipts); } @@ -175,10 +174,7 @@ impl MockEthProvider( - self, - chain_spec: C, - ) -> MockEthProvider { + pub fn with_chain_spec(self, chain_spec: C) -> MockEthProvider { MockEthProvider { blocks: self.blocks, headers: self.headers, @@ -300,27 +296,27 @@ impl DBProvider } } -impl HeaderProvider - for MockEthProvider +impl HeaderProvider + for MockEthProvider { - type Header = Header; + type Header = ::Header; - fn header(&self, block_hash: &BlockHash) -> ProviderResult> { + fn header(&self, block_hash: &BlockHash) -> ProviderResult> { let lock = self.headers.lock(); Ok(lock.get(block_hash).cloned()) } - fn header_by_number(&self, num: u64) -> ProviderResult> { + fn header_by_number(&self, num: u64) -> ProviderResult> { let lock = self.headers.lock(); - Ok(lock.values().find(|h| h.number == num).cloned()) + Ok(lock.values().find(|h| h.number() == num).cloned()) } fn header_td(&self, hash: &BlockHash) -> ProviderResult> { let lock = self.headers.lock(); Ok(lock.get(hash).map(|target| { lock.values() - .filter(|h| h.number < target.number) - .fold(target.difficulty, |td, h| td + h.difficulty) + .filter(|h| h.number() < target.number()) + .fold(target.difficulty(), |td, h| td + h.difficulty()) })) } @@ -328,30 +324,36 @@ impl HeaderProvider let lock = self.headers.lock(); let sum = lock .values() - .filter(|h| h.number <= number) - .fold(U256::ZERO, |td, h| td + h.difficulty); + .filter(|h| h.number() <= number) + .fold(U256::ZERO, |td, h| td + h.difficulty()); Ok(Some(sum)) } - fn headers_range(&self, range: impl RangeBounds) -> ProviderResult> { + fn headers_range( + &self, + range: impl RangeBounds, + ) -> ProviderResult> { let lock = self.headers.lock(); let mut headers: Vec<_> = - lock.values().filter(|header| range.contains(&header.number)).cloned().collect(); - headers.sort_by_key(|header| header.number); + lock.values().filter(|header| range.contains(&header.number())).cloned().collect(); + headers.sort_by_key(|header| header.number()); Ok(headers) } - fn sealed_header(&self, number: BlockNumber) -> ProviderResult> { + fn sealed_header( + &self, + number: BlockNumber, + ) -> ProviderResult>> { Ok(self.header_by_number(number)?.map(SealedHeader::seal_slow)) } fn sealed_headers_while( &self, range: impl RangeBounds, - mut predicate: impl FnMut(&SealedHeader) -> bool, - ) -> ProviderResult> { + mut predicate: impl FnMut(&SealedHeader) -> bool, + ) -> ProviderResult>> { Ok(self .headers_range(range)? .into_iter() @@ -373,16 +375,16 @@ where } } -impl TransactionsProvider - for MockEthProvider +impl TransactionsProvider + for MockEthProvider { - type Transaction = reth_ethereum_primitives::TransactionSigned; + type Transaction = T::SignedTx; fn transaction_id(&self, tx_hash: TxHash) -> ProviderResult> { let lock = self.blocks.lock(); let tx_number = lock .values() - .flat_map(|block| &block.body.transactions) + .flat_map(|block| block.body().transactions()) .position(|tx| *tx.tx_hash() == tx_hash) .map(|pos| pos as TxNumber); @@ -392,7 +394,7 @@ impl TransactionsProvider fn transaction_by_id(&self, id: TxNumber) -> ProviderResult> { let lock = self.blocks.lock(); let transaction = - lock.values().flat_map(|block| &block.body.transactions).nth(id as usize).cloned(); + lock.values().flat_map(|block| block.body().transactions()).nth(id as usize).cloned(); Ok(transaction) } @@ -403,14 +405,14 @@ impl TransactionsProvider ) -> ProviderResult> { let lock = self.blocks.lock(); let transaction = - lock.values().flat_map(|block| &block.body.transactions).nth(id as usize).cloned(); + lock.values().flat_map(|block| block.body().transactions()).nth(id as usize).cloned(); Ok(transaction) } fn transaction_by_hash(&self, hash: TxHash) -> ProviderResult> { Ok(self.blocks.lock().iter().find_map(|(_, block)| { - block.body.transactions.iter().find(|tx| *tx.tx_hash() == hash).cloned() + block.body().transactions_iter().find(|tx| *tx.tx_hash() == hash).cloned() })) } @@ -420,16 +422,16 @@ impl TransactionsProvider ) -> ProviderResult> { let lock = self.blocks.lock(); for (block_hash, block) in lock.iter() { - for (index, tx) in block.body.transactions.iter().enumerate() { + for (index, tx) in block.body().transactions_iter().enumerate() { if *tx.tx_hash() == hash { let meta = TransactionMeta { tx_hash: hash, index: index as u64, block_hash: *block_hash, - block_number: block.header.number, - base_fee: block.header.base_fee_per_gas, - excess_blob_gas: block.header.excess_blob_gas, - timestamp: block.header.timestamp, + block_number: block.header().number(), + base_fee: block.header().base_fee_per_gas(), + excess_blob_gas: block.header().excess_blob_gas(), + timestamp: block.header().timestamp(), }; return Ok(Some((tx.clone(), meta))) } @@ -442,10 +444,10 @@ impl TransactionsProvider let lock = self.blocks.lock(); let mut current_tx_number: TxNumber = 0; for block in lock.values() { - if current_tx_number + (block.body.transactions.len() as TxNumber) > id { - return Ok(Some(block.header.number)) + if current_tx_number + (block.body().transaction_count() as TxNumber) > id { + return Ok(Some(block.header().number())) } - current_tx_number += block.body.transactions.len() as TxNumber; + current_tx_number += block.body().transaction_count() as TxNumber; } Ok(None) } @@ -454,7 +456,7 @@ impl TransactionsProvider &self, id: BlockHashOrNumber, ) -> ProviderResult>> { - Ok(self.block(id)?.map(|b| b.body.transactions)) + Ok(self.block(id)?.map(|b| b.body().clone_transactions())) } fn transactions_by_block_range( @@ -464,8 +466,8 @@ impl TransactionsProvider // init btreemap so we can return in order let mut map = BTreeMap::new(); for (_, block) in self.blocks.lock().iter() { - if range.contains(&block.number) { - map.insert(block.number, block.body.transactions.clone()); + if range.contains(&block.header().number()) { + map.insert(block.header().number(), block.body().clone_transactions()); } } @@ -479,7 +481,7 @@ impl TransactionsProvider let lock = self.blocks.lock(); let transactions = lock .values() - .flat_map(|block| &block.body.transactions) + .flat_map(|block| block.body().transactions()) .enumerate() .filter(|&(tx_number, _)| range.contains(&(tx_number as TxNumber))) .map(|(_, tx)| tx.clone()) @@ -495,7 +497,7 @@ impl TransactionsProvider let lock = self.blocks.lock(); let transactions = lock .values() - .flat_map(|block| &block.body.transactions) + .flat_map(|block| block.body().transactions()) .enumerate() .filter_map(|(tx_number, tx)| { if range.contains(&(tx_number as TxNumber)) { @@ -519,7 +521,7 @@ where T: NodePrimitives, ChainSpec: Send + Sync + 'static, { - type Receipt = Receipt; + type Receipt = T::Receipt; fn receipt(&self, _id: TxNumber) -> ProviderResult> { Ok(None) @@ -540,7 +542,7 @@ where // Find block number by hash first let headers_lock = self.headers.lock(); if let Some(header) = headers_lock.get(&hash) { - Ok(receipts_lock.get(&header.number).cloned()) + Ok(receipts_lock.get(&header.number()).cloned()) } else { Ok(None) } @@ -566,7 +568,7 @@ where let mut result = Vec::new(); for block_number in block_range { // Only include blocks that exist in headers (i.e., have been added to the provider) - if headers_lock.values().any(|header| header.number == block_number) { + if headers_lock.values().any(|header| header.number() == block_number) { if let Some(block_receipts) = receipts_lock.get(&block_number) { result.push(block_receipts.clone()); } else { @@ -593,7 +595,7 @@ impl BlockHashReader fn block_hash(&self, number: u64) -> ProviderResult> { let lock = self.headers.lock(); let hash = - lock.iter().find_map(|(hash, header)| (header.number == number).then_some(*hash)); + lock.iter().find_map(|(hash, header)| (header.number() == number).then_some(*hash)); Ok(hash) } @@ -604,9 +606,9 @@ impl BlockHashReader ) -> ProviderResult> { let lock = self.headers.lock(); let mut hashes: Vec<_> = - lock.iter().filter(|(_, header)| (start..end).contains(&header.number)).collect(); + lock.iter().filter(|(_, header)| (start..end).contains(&header.number())).collect(); - hashes.sort_by_key(|(_, header)| header.number); + hashes.sort_by_key(|(_, header)| header.number()); Ok(hashes.into_iter().map(|(hash, _)| *hash).collect()) } @@ -621,16 +623,16 @@ impl BlockNumReader Ok(lock .iter() - .find(|(_, header)| header.number == best_block_number) - .map(|(hash, header)| ChainInfo { best_hash: *hash, best_number: header.number }) + .find(|(_, header)| header.number() == best_block_number) + .map(|(hash, header)| ChainInfo { best_hash: *hash, best_number: header.number() }) .unwrap_or_default()) } fn best_block_number(&self) -> ProviderResult { let lock = self.headers.lock(); lock.iter() - .max_by_key(|h| h.1.number) - .map(|(_, header)| header.number) + .max_by_key(|h| h.1.number()) + .map(|(_, header)| header.number()) .ok_or(ProviderError::BestBlockNotFound) } @@ -640,7 +642,7 @@ impl BlockNumReader fn block_number(&self, hash: B256) -> ProviderResult> { let lock = self.headers.lock(); - Ok(lock.get(&hash).map(|header| header.number)) + Ok(lock.get(&hash).map(|header| header.number())) } } @@ -661,10 +663,10 @@ impl BlockId } //look -impl BlockReader - for MockEthProvider +impl BlockReader + for MockEthProvider { - type Block = reth_ethereum_primitives::Block; + type Block = T::Block; fn find_block_by_hash( &self, @@ -678,7 +680,9 @@ impl BlockReader let lock = self.blocks.lock(); match id { BlockHashOrNumber::Hash(hash) => Ok(lock.get(&hash).cloned()), - BlockHashOrNumber::Number(num) => Ok(lock.values().find(|b| b.number == num).cloned()), + BlockHashOrNumber::Number(num) => { + Ok(lock.values().find(|b| b.header().number() == num).cloned()) + } } } @@ -688,7 +692,7 @@ impl BlockReader fn pending_block_and_receipts( &self, - ) -> ProviderResult, Vec)>> { + ) -> ProviderResult, Vec)>> { Ok(None) } @@ -711,9 +715,12 @@ impl BlockReader fn block_range(&self, range: RangeInclusive) -> ProviderResult> { let lock = self.blocks.lock(); - let mut blocks: Vec<_> = - lock.values().filter(|block| range.contains(&block.number)).cloned().collect(); - blocks.sort_by_key(|block| block.number); + let mut blocks: Vec<_> = lock + .values() + .filter(|block| range.contains(&block.header().number())) + .cloned() + .collect(); + blocks.sort_by_key(|block| block.header().number()); Ok(blocks) } @@ -733,26 +740,29 @@ impl BlockReader } } -impl BlockReaderIdExt - for MockEthProvider +impl BlockReaderIdExt for MockEthProvider where ChainSpec: EthChainSpec + Send + Sync + 'static, + T: NodePrimitives, { - fn block_by_id(&self, id: BlockId) -> ProviderResult> { + fn block_by_id(&self, id: BlockId) -> ProviderResult> { match id { BlockId::Number(num) => self.block_by_number_or_tag(num), BlockId::Hash(hash) => self.block_by_hash(hash.block_hash), } } - fn sealed_header_by_id(&self, id: BlockId) -> ProviderResult> { + fn sealed_header_by_id( + &self, + id: BlockId, + ) -> ProviderResult::Header>>> { self.header_by_id(id)?.map_or_else(|| Ok(None), |h| Ok(Some(SealedHeader::seal_slow(h)))) } - fn header_by_id(&self, id: BlockId) -> ProviderResult> { + fn header_by_id(&self, id: BlockId) -> ProviderResult::Header>> { match self.block_by_id(id)? { None => Ok(None), - Some(block) => Ok(Some(block.header)), + Some(block) => Ok(Some(block.into_header())), } } } @@ -989,9 +999,12 @@ impl ChangeSetReader for MockEthProvi } impl StateReader for MockEthProvider { - type Receipt = Receipt; + type Receipt = T::Receipt; - fn get_state(&self, _block: BlockNumber) -> ProviderResult> { + fn get_state( + &self, + _block: BlockNumber, + ) -> ProviderResult>> { Ok(None) } } @@ -1019,7 +1032,7 @@ mod tests { #[test] fn test_mock_provider_receipts() { - let provider = MockEthProvider::new(); + let provider = MockEthProvider::::new(); let block_hash = BlockHash::random(); let block_number = 1u64; @@ -1050,7 +1063,7 @@ mod tests { #[test] fn test_mock_provider_receipts_multiple_blocks() { - let provider = MockEthProvider::new(); + let provider = MockEthProvider::::new(); let block1_hash = BlockHash::random(); let block2_hash = BlockHash::random(); From 3e3c9ca5bcbe0913b4289cdbce482dd6ea1d3f1c Mon Sep 17 00:00:00 2001 From: Gregory Edison Date: Wed, 13 Aug 2025 16:07:54 +0200 Subject: [PATCH 4/6] test: update Signed-off-by: Gregory Edison --- crates/scroll/node/src/builder/pool.rs | 70 +++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/crates/scroll/node/src/builder/pool.rs b/crates/scroll/node/src/builder/pool.rs index 70faed0f901..e40b5a08084 100644 --- a/crates/scroll/node/src/builder/pool.rs +++ b/crates/scroll/node/src/builder/pool.rs @@ -221,7 +221,7 @@ mod tests { } #[tokio::test] - async fn test_validate_one_rollup_fee_exceeds_balance() { + async fn test_validate_one_rollup_fee_exceeds_limit() { // create the client. let handle = tokio::runtime::Handle::current(); let manager = TaskManager::new(handle); @@ -258,6 +258,72 @@ mod tests { PoolConfig::default(), ); + // prepare a transaction with random input. + let tx = ScrollTxEnvelope::Legacy(Signed::new_unchecked( + TxLegacy { + gas_limit: 55_000, + gas_price: 7, + input: Bytes::from(random_iter::().take(100).collect::>()), + ..Default::default() + }, + Signature::new(U256::ZERO, U256::ZERO, false), + Default::default(), + )); + let pool_tx = + ScrollPooledTransaction::new(Recovered::new_unchecked(tx, signer), 120 * 1024); + + // add the transaction in the pool and expect to hit `InsufficientFunds` error. + let err = pool.add_transaction(TransactionOrigin::Local, pool_tx).await.unwrap_err(); + assert!(matches!( + err.kind, + PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::Consensus( + InvalidTransactionError::GasUintOverflow + )) + )); + + // explicitly drop the manager here otherwise the `TransactionValidationTaskExecutor` will + // drop all validation tasks. + drop(manager); + } + + #[tokio::test] + async fn test_validate_one_rollup_fee_exceeds_balance() { + // create the client. + let handle = tokio::runtime::Handle::current(); + let manager = TaskManager::new(handle); + let blob_store = NoopBlobStore::default(); + let signer = Default::default(); + let client = + MockEthProvider::::new().with_chain_spec(SCROLL_DEV.clone()); + let hash = B256::random(); + + // load a header, block, signer and the L1_GAS_PRICE_ORACLE_ADDRESS storage. + client.add_header(hash, Header::default()); + client.add_block(hash, ScrollBlock::default()); + client.add_account(signer, ExtendedAccount::new(0, U256::from(400_000))); + client.add_account( + L1_GAS_PRICE_ORACLE_ADDRESS, + ExtendedAccount::new(0, U256::from(400_000)).extend_storage( + (0u8..8).map(|k| (B256::from(U256::from(k)), U256::from(u32::MAX))), + ), + ); + + // create the validation task. + let validator = TransactionValidationTaskExecutor::eth_builder(client) + .no_eip4844() + .build_with_tasks(manager.executor(), blob_store) + .map(|validator| { + ScrollTransactionValidator::new(validator).require_l1_data_gas_fee(true) + }); + + // create the pool. + let pool = ScrollTransactionPool::new( + validator, + CoinbaseTipOrdering::::default(), + NoopBlobStore::default(), + PoolConfig::default(), + ); + // prepare a transaction with random input. let tx = ScrollTxEnvelope::Legacy(Signed::new_unchecked( TxLegacy { @@ -278,7 +344,7 @@ mod tests { err.kind, PoolErrorKind::InvalidTransaction( InvalidPoolTransactionError::Consensus(InvalidTransactionError::InsufficientFunds(GotExpectedBoxed(expected))) - ) if *expected == GotExpected{ got: U256::from(400000), expected: U256::from_limbs([384999, 1, 0, 0]) } + ) if *expected == GotExpected{ got: U256::from(400000), expected: U256::from(4205858031847u64) } )); // explicitly drop the manager here otherwise the `TransactionValidationTaskExecutor` will From 14e1d5066a6bfecb90c7ea4258ee721c4123d826 Mon Sep 17 00:00:00 2001 From: Gregory Edison Date: Wed, 13 Aug 2025 18:52:43 +0200 Subject: [PATCH 5/6] feat: disallow L1 messages in tx pool Signed-off-by: Gregory Edison --- crates/scroll/alloy/consensus/src/lib.rs | 4 ++-- .../consensus/src/transaction/envelope.rs | 24 +++++++++++++++++++ .../alloy/consensus/src/transaction/mod.rs | 2 +- crates/scroll/node/src/builder/pool.rs | 3 ++- crates/scroll/txpool/src/transaction.rs | 11 +++++++++ crates/scroll/txpool/src/validator.rs | 11 +++++++-- 6 files changed, 49 insertions(+), 6 deletions(-) diff --git a/crates/scroll/alloy/consensus/src/lib.rs b/crates/scroll/alloy/consensus/src/lib.rs index c6eb0dad6ea..061a6d6969f 100644 --- a/crates/scroll/alloy/consensus/src/lib.rs +++ b/crates/scroll/alloy/consensus/src/lib.rs @@ -13,8 +13,8 @@ extern crate alloc as std; mod transaction; pub use transaction::{ ScrollAdditionalInfo, ScrollL1MessageTransactionFields, ScrollPooledTransaction, - ScrollTransactionInfo, ScrollTxEnvelope, ScrollTxType, ScrollTypedTransaction, TxL1Message, - L1_MESSAGE_TRANSACTION_TYPE, L1_MESSAGE_TX_TYPE_ID, + ScrollTransaction, ScrollTransactionInfo, ScrollTxEnvelope, ScrollTxType, + ScrollTypedTransaction, TxL1Message, L1_MESSAGE_TRANSACTION_TYPE, L1_MESSAGE_TX_TYPE_ID, }; mod receipt; diff --git a/crates/scroll/alloy/consensus/src/transaction/envelope.rs b/crates/scroll/alloy/consensus/src/transaction/envelope.rs index 4fd4b8d1e2b..2905667012f 100644 --- a/crates/scroll/alloy/consensus/src/transaction/envelope.rs +++ b/crates/scroll/alloy/consensus/src/transaction/envelope.rs @@ -424,6 +424,30 @@ impl ScrollTxEnvelope { } } +/// A Scroll chain transaction. +pub trait ScrollTransaction { + /// Returns true if the transaction is a L1 message. + fn is_l1_message(&self) -> bool; + /// Returns the queue index if the transaction is a L1 message, None otherwise. + fn queue_index(&self) -> Option; +} + +impl ScrollTransaction for ScrollTxEnvelope { + fn is_l1_message(&self) -> bool { + match self { + Self::Legacy(_) | Self::Eip2930(_) | Self::Eip1559(_) | Self::Eip7702(_) => false, + Self::L1Message(_) => true, + } + } + + fn queue_index(&self) -> Option { + match self { + Self::Legacy(_) | Self::Eip2930(_) | Self::Eip1559(_) | Self::Eip7702(_) => None, + Self::L1Message(tx) => Some(tx.queue_index), + } + } +} + #[cfg(feature = "reth-codec")] impl ToTxCompact for ScrollTxEnvelope { fn to_tx_compact(&self, buf: &mut (impl BufMut + AsMut<[u8]>)) { diff --git a/crates/scroll/alloy/consensus/src/transaction/mod.rs b/crates/scroll/alloy/consensus/src/transaction/mod.rs index c43aab42897..b2f08383ce6 100644 --- a/crates/scroll/alloy/consensus/src/transaction/mod.rs +++ b/crates/scroll/alloy/consensus/src/transaction/mod.rs @@ -4,7 +4,7 @@ mod tx_type; pub use tx_type::{ScrollTxType, L1_MESSAGE_TX_TYPE_ID}; mod envelope; -pub use envelope::ScrollTxEnvelope; +pub use envelope::{ScrollTransaction, ScrollTxEnvelope}; mod l1_message; pub use l1_message::{ScrollL1MessageTransactionFields, TxL1Message, L1_MESSAGE_TRANSACTION_TYPE}; diff --git a/crates/scroll/node/src/builder/pool.rs b/crates/scroll/node/src/builder/pool.rs index e40b5a08084..a20e9bb917f 100644 --- a/crates/scroll/node/src/builder/pool.rs +++ b/crates/scroll/node/src/builder/pool.rs @@ -13,6 +13,7 @@ use reth_transaction_pool::{ blobstore::DiskFileBlobStore, CoinbaseTipOrdering, EthPoolTransaction, TransactionValidationTaskExecutor, }; +use scroll_alloy_consensus::ScrollTransaction; use scroll_alloy_hardforks::ScrollHardforks; /// A basic scroll transaction pool. @@ -52,7 +53,7 @@ where ChainSpec: EthChainSpec + ScrollHardforks + ChainConfig, >, >, - T: EthPoolTransaction>, + T: EthPoolTransaction> + ScrollTransaction, { type Pool = ScrollTransactionPool; diff --git a/crates/scroll/txpool/src/transaction.rs b/crates/scroll/txpool/src/transaction.rs index bee30961949..e0fdf7b66c1 100644 --- a/crates/scroll/txpool/src/transaction.rs +++ b/crates/scroll/txpool/src/transaction.rs @@ -10,6 +10,7 @@ use reth_scroll_primitives::ScrollTransactionSigned; use reth_transaction_pool::{ EthBlobTransactionSidecar, EthPoolTransaction, EthPooledTransaction, PoolTransaction, }; +use scroll_alloy_consensus::ScrollTransaction; use std::sync::{Arc, OnceLock}; /// Pool transaction for Scroll. @@ -209,6 +210,16 @@ where } } +impl ScrollTransaction for ScrollPooledTransaction { + fn is_l1_message(&self) -> bool { + self.transaction.is_l1_message() + } + + fn queue_index(&self) -> Option { + self.transaction.queue_index() + } +} + #[cfg(test)] mod tests { use crate::{ScrollPooledTransaction, ScrollTransactionValidator}; diff --git a/crates/scroll/txpool/src/validator.rs b/crates/scroll/txpool/src/validator.rs index 0f7ea42f582..24a4d76dede 100644 --- a/crates/scroll/txpool/src/validator.rs +++ b/crates/scroll/txpool/src/validator.rs @@ -17,6 +17,7 @@ use reth_transaction_pool::{ TransactionValidator, }; use revm_scroll::l1block::L1BlockInfo; +use scroll_alloy_consensus::ScrollTransaction; use std::sync::{ atomic::{AtomicU64, Ordering}, Arc, @@ -85,7 +86,7 @@ impl ScrollTransactionValidator { impl ScrollTransactionValidator where Client: ChainSpecProvider + StateProviderFactory + BlockReaderIdExt, - Tx: EthPoolTransaction, + Tx: EthPoolTransaction + ScrollTransaction, { /// Create a new [`ScrollTransactionValidator`]. pub fn new(inner: EthTransactionValidator) -> Self { @@ -139,6 +140,12 @@ where transaction: Tx, ) -> TransactionValidationOutcome { if transaction.is_eip4844() { + return TransactionValidationOutcome::Invalid( + transaction, + InvalidTransactionError::Eip4844Disabled.into(), + ) + } + if transaction.is_l1_message() { return TransactionValidationOutcome::Invalid( transaction, InvalidTransactionError::TxTypeNotSupported.into(), @@ -232,7 +239,7 @@ where impl TransactionValidator for ScrollTransactionValidator where Client: ChainSpecProvider + StateProviderFactory + BlockReaderIdExt, - Tx: EthPoolTransaction, + Tx: EthPoolTransaction + ScrollTransaction, { type Transaction = Tx; From 1f6db7b9dc4374c3ae3f3441de29938ca9dcecec Mon Sep 17 00:00:00 2001 From: Gregory Edison Date: Wed, 13 Aug 2025 18:58:31 +0200 Subject: [PATCH 6/6] test: disallow L1 messages in tx pool Signed-off-by: Gregory Edison --- crates/scroll/node/src/builder/pool.rs | 31 ++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/scroll/node/src/builder/pool.rs b/crates/scroll/node/src/builder/pool.rs index a20e9bb917f..8fead3e4c3f 100644 --- a/crates/scroll/node/src/builder/pool.rs +++ b/crates/scroll/node/src/builder/pool.rs @@ -139,7 +139,7 @@ mod tests { use crate::ScrollNode; use alloy_consensus::{transaction::Recovered, Header, Signed, TxLegacy}; - use alloy_primitives::{private::rand::random_iter, Bytes, Signature, B256, U256}; + use alloy_primitives::{private::rand::random_iter, Bytes, Sealed, Signature, B256, U256}; use reth_chainspec::Head; use reth_db::mock::DatabaseMock; use reth_node_api::FullNodeTypesAdapter; @@ -161,7 +161,7 @@ mod tests { error::{InvalidPoolTransactionError, PoolErrorKind}, PoolConfig, TransactionOrigin, TransactionPool, }; - use scroll_alloy_consensus::ScrollTxEnvelope; + use scroll_alloy_consensus::{ScrollTxEnvelope, TxL1Message}; use scroll_alloy_evm::curie::L1_GAS_PRICE_ORACLE_ADDRESS; async fn pool() -> ( @@ -352,4 +352,31 @@ mod tests { // drop all validation tasks. drop(manager); } + + #[tokio::test] + async fn test_validate_one_disallow_l1_messages() { + // create the pool. + let (pool, manager) = pool().await; + let tx = ScrollTxEnvelope::L1Message(Sealed::new_unchecked( + TxL1Message::default(), + B256::default(), + )); + + // Create a pool transaction with the L1 message. + let pool_tx = + ScrollPooledTransaction::new(Recovered::new_unchecked(tx, Default::default()), 0); + + // add the transaction to the pool and expect an `OversizedData` error. + let err = pool.add_transaction(TransactionOrigin::Local, pool_tx).await.unwrap_err(); + assert!(matches!( + err.kind, + PoolErrorKind::InvalidTransaction(InvalidPoolTransactionError::Consensus( + InvalidTransactionError::TxTypeNotSupported + )) + )); + + // explicitly drop the manager here otherwise the `TransactionValidationTaskExecutor` will + // drop all validation tasks. + drop(manager); + } }