Skip to content

feat: limit pool tx size #317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/scroll/chainspec/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
18 changes: 16 additions & 2 deletions crates/scroll/chainspec/src/genesis.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Address>,
/// 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,
Expand All @@ -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,
}
}
Expand All @@ -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,
}
}
}

Expand Down Expand Up @@ -209,6 +221,7 @@ mod tests {
"feynmanTime": 100,
"scroll": {
"feeVaultAddress": "0x5300000000000000000000000000000000000005",
"maxTxPayloadBytesPerBlock": 122880,
"l1Config": {
"l1ChainId": 1,
"l1MessageQueueAddress": "0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B",
Expand Down Expand Up @@ -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"),
Expand Down
48 changes: 25 additions & 23 deletions crates/scroll/chainspec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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");
Expand All @@ -659,6 +659,7 @@ mod tests {
scroll_object,
&serde_json::json!({
"feeVaultAddress": "0x5300000000000000000000000000000000000005",
"maxTxPayloadBytesPerBlock": 122880,
"l1Config": {
"l1ChainId": 1,
"l1MessageQueueAddress": "0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B",
Expand Down Expand Up @@ -712,6 +713,7 @@ mod tests {
String::from("scroll"),
serde_json::json!({
"feeVaultAddress": "0x5300000000000000000000000000000000000005",
"maxTxPayloadBytesPerBlock": 122880,
"l1Config": {
"l1ChainId": 1,
"l1MessageQueueAddress": "0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B",
Expand Down
156 changes: 156 additions & 0 deletions crates/scroll/node/src/builder/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few notes:

  • Here we only check tx.input, but the overall transaction could be much larger (e.g. by including a long authorization list).
  • Also, if tx.input == max_payload_size, then we'll admin it into the txpool, but we must not include it in the block (the full serialized block must respect the same limit).

That said, this is not very critical, and the current impl seems idential to l2geth. The critical part is ensuring these limits during block building.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was not aware of the second point, can always add this to the block builder, it's not much. For the first point, I think we check tx.rlp_encoding_length so this should cover the authorization list.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is here: https://github.com/scroll-tech/go-ethereum/blob/develop/miner/scroll_worker.go#L836. Yes we should add it to the block builder.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a check, this was implemented by Morty here.

.with_additional_tasks(
pool_config_overrides
.additional_validation_tasks
Expand Down Expand Up @@ -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<NoopProvider<ScrollChainSpec, ScrollPrimitives>, 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::<ScrollPooledTransaction>::default();
let ctx = BuilderContext::<
FullNodeTypesAdapter<
ScrollNode,
DatabaseMock,
NoopProvider<ScrollChainSpec, ScrollPrimitives>,
>,
>::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::<ScrollPrimitives, _>::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::<ScrollPooledTransaction>::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::<u8>().take(100).collect::<Vec<_>>()),
..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);
}
}
12 changes: 7 additions & 5 deletions crates/scroll/node/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(),
Expand Down
Loading
Loading