From 4570a8ae26571ae6fb08b444f3c96caf6c0cb16a Mon Sep 17 00:00:00 2001 From: ilitteri Date: Mon, 11 May 2026 16:18:30 -0300 Subject: [PATCH 1/6] feat(l1): enforce per-transaction wire-size cap in mempool admission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the peer-policy size cap that every major Ethereum execution client (geth `txMaxSize`, reth `DEFAULT_MAX_TX_INPUT_BYTES`, nethermind `MaxTxSize` / `MaxBlobTxSize`, erigon size enforcement) ships by default. Without it a single oversized transaction can chew bandwidth and pool capacity at near-zero attacker cost. - New constants `MAX_TX_SIZE = 128 KiB` and `MAX_BLOB_TX_SIZE = 1 MiB` in `crates/common/types/constants.rs`. - `Blockchain::validate_transaction` now rejects transactions whose `Transaction`-canonical RLP encoding exceeds the cap. For EIP-4844 the cap applies to the core tx only — the blob sidecar lives separately in `BlobsBundle` and is already bounded by per-blob byte count plus per-block blob count. - New error `MempoolError::TxSizeExceeded { actual, limit }`. - Two unit tests in `transaction.rs` confirming the wire-size signal matches the cap. --- crates/blockchain/blockchain.rs | 19 ++++++++++++++++++- crates/blockchain/error.rs | 2 ++ crates/common/types/constants.rs | 10 ++++++++++ crates/common/types/transaction.rs | 27 +++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 98ebe601fd1..5aab45c7df5 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -72,6 +72,7 @@ use ethrex_common::types::{ }; use ethrex_common::types::{ELASTICITY_MULTIPLIER, P2PTransaction}; use ethrex_common::types::{Fork, MempoolTransaction}; +use ethrex_common::types::{MAX_BLOB_TX_SIZE, MAX_TX_SIZE}; use ethrex_common::utils::keccak; use ethrex_common::{Address, H256, TrieLogger, U256}; pub use ethrex_common::{ @@ -2432,7 +2433,23 @@ impl Blockchain { .ok_or(MempoolError::NoBlockHeaderError)?; let config = self.storage.get_chain_config(); - // NOTE: We could add a tx size limit here, but it's not in the actual spec + // Wire size cap: peer-policy default, not consensus. Matches geth + // `txMaxSize` (legacypool / blobpool), reth `DEFAULT_MAX_TX_INPUT_BYTES`, + // nethermind `MaxTxSize` / `MaxBlobTxSize`. For EIP-4844 the canonical + // encoding of `Transaction` excludes the sidecar (which lives in the + // adjacent `BlobsBundle`), so this caps the core tx only. + let encoded_len = tx.encode_canonical_to_vec().len(); + let size_limit = if matches!(tx, Transaction::EIP4844Transaction(_)) { + MAX_BLOB_TX_SIZE + } else { + MAX_TX_SIZE + }; + if encoded_len > size_limit { + return Err(MempoolError::TxSizeExceeded { + actual: encoded_len, + limit: size_limit, + }); + } // Check init code size // [EIP-7954] - Amsterdam increases the limit diff --git a/crates/blockchain/error.rs b/crates/blockchain/error.rs index 39436472dd4..c8668b882a6 100644 --- a/crates/blockchain/error.rs +++ b/crates/blockchain/error.rs @@ -83,6 +83,8 @@ pub enum MempoolError { TxMaxInitCodeSizeError, #[error("Transaction max data size exceeded")] TxMaxDataSizeError, + #[error("Transaction encoded size ({actual} bytes) exceeds the {limit}-byte limit")] + TxSizeExceeded { actual: usize, limit: usize }, #[error("Transaction gas limit exceeded")] TxGasLimitExceededError, #[error( diff --git a/crates/common/types/constants.rs b/crates/common/types/constants.rs index 76d912531a8..3d5c78500d6 100644 --- a/crates/common/types/constants.rs +++ b/crates/common/types/constants.rs @@ -32,3 +32,13 @@ pub const FIELD_ELEMENTS_PER_EXT_BLOB: usize = 2 * FIELD_ELEMENTS_PER_BLOB; pub const FIELD_ELEMENTS_PER_CELL: usize = 64; pub const BYTES_PER_CELL: usize = FIELD_ELEMENTS_PER_CELL * BYTES_PER_FIELD_ELEMENT; pub const CELLS_PER_EXT_BLOB: usize = FIELD_ELEMENTS_PER_EXT_BLOB / FIELD_ELEMENTS_PER_CELL; + +// Mempool admission size caps — peer-policy defaults, not consensus. +// Matches geth `txMaxSize` (legacypool) and `txMaxSize` (blobpool), reth +// `DEFAULT_MAX_TX_INPUT_BYTES`, nethermind `MaxTxSize` / `MaxBlobTxSize`. +/// Maximum RLP-encoded wire size for a non-blob transaction (128 KiB). +pub const MAX_TX_SIZE: usize = 131_072; +/// Maximum RLP-encoded core size for an EIP-4844 blob transaction (1 MiB), +/// excluding the blob sidecar. Sidecar size is bounded separately by the +/// per-blob byte count and the fork's max blob count. +pub const MAX_BLOB_TX_SIZE: usize = 1_048_576; diff --git a/crates/common/types/transaction.rs b/crates/common/types/transaction.rs index 59c026c39b9..128397d9b97 100644 --- a/crates/common/types/transaction.rs +++ b/crates/common/types/transaction.rs @@ -3764,4 +3764,31 @@ mod tests { "blob-gas term missing from cost_without_base_fee() for EIP-4844" ); } + + #[test] + fn test_encoded_size_exceeds_max_tx_size() { + // Confirms the wire-size signal used by the mempool admission cap: + // an EIP-1559 tx with > 128 KiB of `data` encodes to > MAX_TX_SIZE bytes. + use crate::types::MAX_TX_SIZE; + + let payload = vec![0u8; MAX_TX_SIZE + 1]; + let tx = Transaction::EIP1559Transaction(EIP1559Transaction { + data: Bytes::from(payload), + ..Default::default() + }); + + assert!( + tx.encode_canonical_to_vec().len() > MAX_TX_SIZE, + "tx with > 128 KiB calldata must encode larger than MAX_TX_SIZE" + ); + } + + #[test] + fn test_encoded_size_below_max_tx_size() { + // A small tx encodes well below the cap. + use crate::types::MAX_TX_SIZE; + + let tx = Transaction::EIP1559Transaction(EIP1559Transaction::default()); + assert!(tx.encode_canonical_to_vec().len() <= MAX_TX_SIZE); + } } From b2054ac26c0344444d7c2386ef88cccf1a4717bf Mon Sep 17 00:00:00 2001 From: ilitteri Date: Mon, 11 May 2026 19:25:34 -0300 Subject: [PATCH 2/6] fix(l1): remove redundant MAX_TRANSACTION_DATA_SIZE; add size-cap integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes from review: 1. Drop the now-redundant calldata-only size check `MAX_TRANSACTION_DATA_SIZE = 128 KiB` (in `crates/blockchain/constants.rs`) was byte-identical to the new `MAX_TX_SIZE`, and the new wire-size check at the top of `validate_transaction` runs first and rejects for all the same inputs (the encoded envelope is strictly larger than `data().len()`). Delete the constant, the `MempoolError::TxMaxDataSizeError` variant, and the dead check block. No behavior change. 2. Replace the two tautological encoding tests in `transaction.rs` (still landing — they only asserted that RLP doesn't compress) with real integration tests in `test/tests/blockchain/mempool_tests.rs` that exercise `validate_transaction` directly: - `validate_transaction_rejects_oversize_non_blob` against `MAX_TX_SIZE` - `validate_transaction_rejects_oversize_blob_core` against `MAX_BLOB_TX_SIZE` for an EIP-4844 tx Both assert the `TxSizeExceeded { actual, limit }` shape so a regression in the limit dispatch (non-blob vs blob) is also caught. --- crates/blockchain/blockchain.rs | 9 +--- crates/blockchain/constants.rs | 3 -- crates/blockchain/error.rs | 2 - test/tests/blockchain/mempool_tests.rs | 58 ++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 13 deletions(-) diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 5aab45c7df5..512d5e94291 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -51,10 +51,7 @@ pub mod tracing; pub mod vm; use ::tracing::{debug, error, info, instrument, warn}; -use constants::{ - AMSTERDAM_MAX_INITCODE_SIZE, MAX_INITCODE_SIZE, MAX_TRANSACTION_DATA_SIZE, - POST_OSAKA_GAS_LIMIT_CAP, -}; +use constants::{AMSTERDAM_MAX_INITCODE_SIZE, MAX_INITCODE_SIZE, POST_OSAKA_GAS_LIMIT_CAP}; use error::MempoolError; use error::{ChainError, InvalidBlockError}; use ethrex_common::constants::{EMPTY_TRIE_HASH, MIN_BASE_FEE_PER_BLOB_GAS}; @@ -2465,10 +2462,6 @@ impl Blockchain { return Err(MempoolError::TxMaxInitCodeSizeError); } - if !tx.is_contract_creation() && tx.data().len() >= MAX_TRANSACTION_DATA_SIZE as usize { - return Err(MempoolError::TxMaxDataSizeError); - } - if config.is_osaka_activated(header.timestamp) && tx.gas_limit() > POST_OSAKA_GAS_LIMIT_CAP { // https://eips.ethereum.org/EIPS/eip-7825 diff --git a/crates/blockchain/constants.rs b/crates/blockchain/constants.rs index 733cd7f06be..569fd874753 100644 --- a/crates/blockchain/constants.rs +++ b/crates/blockchain/constants.rs @@ -35,9 +35,6 @@ pub const MAX_INITCODE_SIZE: u32 = 2 * MAX_CODE_SIZE; // EIP-7954 (Amsterdam): increased max initcode size pub const AMSTERDAM_MAX_INITCODE_SIZE: u32 = 2 * AMSTERDAM_MAX_CODE_SIZE; -// Max non-contract creation bytecode size -pub const MAX_TRANSACTION_DATA_SIZE: u32 = 4 * 32 * 1024; // 128 Kb - // === EIP-2028 constants === // Gas cost for each non zero byte on transaction data diff --git a/crates/blockchain/error.rs b/crates/blockchain/error.rs index c8668b882a6..b37336d2919 100644 --- a/crates/blockchain/error.rs +++ b/crates/blockchain/error.rs @@ -81,8 +81,6 @@ pub enum MempoolError { BlobsBundleError(#[from] BlobsBundleError), #[error("Transaction max init code size exceeded")] TxMaxInitCodeSizeError, - #[error("Transaction max data size exceeded")] - TxMaxDataSizeError, #[error("Transaction encoded size ({actual} bytes) exceeds the {limit}-byte limit")] TxSizeExceeded { actual: usize, limit: usize }, #[error("Transaction gas limit exceeded")] diff --git a/test/tests/blockchain/mempool_tests.rs b/test/tests/blockchain/mempool_tests.rs index 9098b2599bd..690aa35eb21 100644 --- a/test/tests/blockchain/mempool_tests.rs +++ b/test/tests/blockchain/mempool_tests.rs @@ -357,6 +357,64 @@ async fn transaction_with_blob_base_fee_below_min_should_fail() { )); } +#[tokio::test] +async fn validate_transaction_rejects_oversize_non_blob() { + // EIP-1559 tx with serialized RLP > MAX_TX_SIZE must be rejected at + // admission with `TxSizeExceeded`. The size cap is the first + // size-themed check; it runs before init-code, intrinsic gas, and + // balance lookups, so an unsigned tx with no sender state is enough. + use ethrex_common::types::MAX_TX_SIZE; + + let (config, header) = build_basic_config_and_header(false, false); + let store = setup_storage(config, header).await.expect("Storage setup"); + let blockchain = Blockchain::default_with_store(store); + + // Pad calldata above MAX_TX_SIZE so the *encoded* tx is also oversized. + let tx = Transaction::EIP1559Transaction(EIP1559Transaction { + data: Bytes::from(vec![0u8; MAX_TX_SIZE + 1]), + ..Default::default() + }); + + let res = blockchain + .validate_transaction(&tx, Address::random()) + .await; + match res { + Err(MempoolError::TxSizeExceeded { actual, limit }) => { + assert!(actual > limit); + assert_eq!(limit, MAX_TX_SIZE); + } + other => panic!("expected TxSizeExceeded, got {:?}", other), + } +} + +#[tokio::test] +async fn validate_transaction_rejects_oversize_blob_core() { + // EIP-4844 tx whose *core* encoding exceeds MAX_BLOB_TX_SIZE. The blob + // sidecar is bounded separately and is not encoded into the canonical + // tx, so to trigger the cap we have to grow the access-list / data. + use ethrex_common::types::MAX_BLOB_TX_SIZE; + + let (config, header) = build_basic_config_and_header(false, false); + let store = setup_storage(config, header).await.expect("Storage setup"); + let blockchain = Blockchain::default_with_store(store); + + let tx = Transaction::EIP4844Transaction(EIP4844Transaction { + data: Bytes::from(vec![0u8; MAX_BLOB_TX_SIZE + 1]), + ..Default::default() + }); + + let res = blockchain + .validate_transaction(&tx, Address::random()) + .await; + match res { + Err(MempoolError::TxSizeExceeded { actual, limit }) => { + assert!(actual > limit); + assert_eq!(limit, MAX_BLOB_TX_SIZE); + } + other => panic!("expected TxSizeExceeded, got {:?}", other), + } +} + #[test] fn test_filter_mempool_transactions() { let plain_tx_decoded = Transaction::decode_canonical(&hex::decode("f86d80843baa0c4082f618946177843db3138ae69679a54b95cf345ed759450d870aa87bee538000808360306ba0151ccc02146b9b11adf516e6787b59acae3e76544fdcd75e77e67c6b598ce65da064c5dd5aae2fbb535830ebbdad0234975cd7ece3562013b63ea18cc0df6c97d4").unwrap()).unwrap(); From 88338535c1266aa364701895eda4c6d4a2bd1f26 Mon Sep 17 00:00:00 2001 From: ilitteri Date: Mon, 11 May 2026 20:59:05 -0300 Subject: [PATCH 3/6] fix(l1): apply blob-tx size cap to wire-wrapper (core + sidecar) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-client audit flagged that geth (`core/types/transaction.go::Size`), nethermind (`MaxBlobTxSize`), and erigon (`ValidateSerializedTxn`) all compare their 1 MiB blob-tx cap against the wire form that **includes the sidecar** (blobs + commitments + proofs). The previous ethrex check in `validate_transaction` compared `Transaction::encode_canonical_to_vec` which only covers the core tx — the sidecar lives in the adjacent `BlobsBundle`. With 6 blobs (~786 KB blob data + ~100 KB commitments/proofs ≈ 900 KB) the worst-case wire wrapper can reach ~1.9 MiB while still passing the 1 MiB core-only check. Peers reject that on the wire so ethrex would be admitting txs nobody else will relay. Changes: - `validate_transaction` now only enforces `MAX_TX_SIZE` for non-blob txs. - `add_blob_transaction_to_pool_inner` runs a new wire-wrapper check before bundle validation: `core_tx_encoded + bundle_encoded <= MAX_BLOB_TX_SIZE`. Summing the two encoded sizes matches geth's `tx.Size()` semantic to within the ±few bytes of outer list framing, which is rounding error at this scale. - Drop `validate_transaction_rejects_oversize_blob_core` — the function it covered no longer applies to blob txs. Integration test for the new wrapper check deferred (same pattern as PRs #6603/#6576: the `c-kzg`-gated `add_blob_transaction_to_pool` isn't currently exercised by `mempool_tests`). --- crates/blockchain/blockchain.rs | 45 +++++++++++++++++--------- test/tests/blockchain/mempool_tests.rs | 28 ---------------- 2 files changed, 29 insertions(+), 44 deletions(-) diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 512d5e94291..01e6f848566 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -2321,6 +2321,21 @@ impl Blockchain { return Ok(hash); } + // Wire-wrapper size cap for blob txs. Matches geth `txMaxSize = 1 MiB` + // (blobpool) and nethermind `MaxBlobTxSize`, which both bound the + // wire-wrapper form including the sidecar. ethrex stores the core tx + // and the bundle in separate structs, so sum the two encoded sizes + // (the ±few bytes of outer list framing are rounding error at this + // scale). + let wrapper_len = + transaction.encode_canonical_to_vec().len() + blobs_bundle.encode_to_vec().len(); + if wrapper_len > MAX_BLOB_TX_SIZE { + return Err(MempoolError::TxSizeExceeded { + actual: wrapper_len, + limit: MAX_BLOB_TX_SIZE, + }); + } + // Validate blobs bundle after checking if it's already added. if let Transaction::EIP4844Transaction(transaction) = &transaction { blobs_bundle.validate(transaction, fork)?; @@ -2430,22 +2445,20 @@ impl Blockchain { .ok_or(MempoolError::NoBlockHeaderError)?; let config = self.storage.get_chain_config(); - // Wire size cap: peer-policy default, not consensus. Matches geth - // `txMaxSize` (legacypool / blobpool), reth `DEFAULT_MAX_TX_INPUT_BYTES`, - // nethermind `MaxTxSize` / `MaxBlobTxSize`. For EIP-4844 the canonical - // encoding of `Transaction` excludes the sidecar (which lives in the - // adjacent `BlobsBundle`), so this caps the core tx only. - let encoded_len = tx.encode_canonical_to_vec().len(); - let size_limit = if matches!(tx, Transaction::EIP4844Transaction(_)) { - MAX_BLOB_TX_SIZE - } else { - MAX_TX_SIZE - }; - if encoded_len > size_limit { - return Err(MempoolError::TxSizeExceeded { - actual: encoded_len, - limit: size_limit, - }); + // Wire size cap for non-blob txs: peer-policy default, not consensus. + // Matches geth `txMaxSize` (legacypool), reth `DEFAULT_MAX_TX_INPUT_BYTES`, + // nethermind `MaxTxSize`. Blob txs are bounded by their own + // wire-wrapper cap (`MAX_BLOB_TX_SIZE`) in `add_blob_transaction_to_pool`, + // which sums the core tx and the sidecar to match geth/nethermind/erigon + // scope. + if !matches!(tx, Transaction::EIP4844Transaction(_)) { + let encoded_len = tx.encode_canonical_to_vec().len(); + if encoded_len > MAX_TX_SIZE { + return Err(MempoolError::TxSizeExceeded { + actual: encoded_len, + limit: MAX_TX_SIZE, + }); + } } // Check init code size diff --git a/test/tests/blockchain/mempool_tests.rs b/test/tests/blockchain/mempool_tests.rs index 690aa35eb21..65e04d88359 100644 --- a/test/tests/blockchain/mempool_tests.rs +++ b/test/tests/blockchain/mempool_tests.rs @@ -387,34 +387,6 @@ async fn validate_transaction_rejects_oversize_non_blob() { } } -#[tokio::test] -async fn validate_transaction_rejects_oversize_blob_core() { - // EIP-4844 tx whose *core* encoding exceeds MAX_BLOB_TX_SIZE. The blob - // sidecar is bounded separately and is not encoded into the canonical - // tx, so to trigger the cap we have to grow the access-list / data. - use ethrex_common::types::MAX_BLOB_TX_SIZE; - - let (config, header) = build_basic_config_and_header(false, false); - let store = setup_storage(config, header).await.expect("Storage setup"); - let blockchain = Blockchain::default_with_store(store); - - let tx = Transaction::EIP4844Transaction(EIP4844Transaction { - data: Bytes::from(vec![0u8; MAX_BLOB_TX_SIZE + 1]), - ..Default::default() - }); - - let res = blockchain - .validate_transaction(&tx, Address::random()) - .await; - match res { - Err(MempoolError::TxSizeExceeded { actual, limit }) => { - assert!(actual > limit); - assert_eq!(limit, MAX_BLOB_TX_SIZE); - } - other => panic!("expected TxSizeExceeded, got {:?}", other), - } -} - #[test] fn test_filter_mempool_transactions() { let plain_tx_decoded = Transaction::decode_canonical(&hex::decode("f86d80843baa0c4082f618946177843db3138ae69679a54b95cf345ed759450d870aa87bee538000808360306ba0151ccc02146b9b11adf516e6787b59acae3e76544fdcd75e77e67c6b598ce65da064c5dd5aae2fbb535830ebbdad0234975cd7ece3562013b63ea18cc0df6c97d4").unwrap()).unwrap(); From a7317901481278933bfdfc83db1ddb35db58e87d Mon Sep 17 00:00:00 2001 From: ilitteri Date: Mon, 11 May 2026 21:30:15 -0300 Subject: [PATCH 4/6] perf(l1): add Transaction::encode_canonical_len to avoid allocating just to measure Both reviewers (Copilot + @MegaRedHand) flagged that `tx.encode_canonical_to_vec().len()` allocates a `Vec` purely to read its length on the mempool-admission hot path, and for typed txs also clones the cached canonical bytes. `encode_canonical_len()` counts the 1-byte EIP-2718 type prefix plus the inner tx's RLP `length()` (which `RLPEncode::length` provides allocation-free via the existing `ByteCounter`). The two admission size-check call sites (non-blob in `validate_transaction`, blob wire-wrapper in `add_blob_transaction_to_pool_inner`) now use this helper; the blob path also switches `BlobsBundle::encode_to_vec().len()` to `BlobsBundle::length()` for the same reason. --- crates/blockchain/blockchain.rs | 5 ++--- crates/common/types/transaction.rs | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 01e6f848566..7e84e8cb065 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -2327,8 +2327,7 @@ impl Blockchain { // and the bundle in separate structs, so sum the two encoded sizes // (the ±few bytes of outer list framing are rounding error at this // scale). - let wrapper_len = - transaction.encode_canonical_to_vec().len() + blobs_bundle.encode_to_vec().len(); + let wrapper_len = transaction.encode_canonical_len() + blobs_bundle.length(); if wrapper_len > MAX_BLOB_TX_SIZE { return Err(MempoolError::TxSizeExceeded { actual: wrapper_len, @@ -2452,7 +2451,7 @@ impl Blockchain { // which sums the core tx and the sidecar to match geth/nethermind/erigon // scope. if !matches!(tx, Transaction::EIP4844Transaction(_)) { - let encoded_len = tx.encode_canonical_to_vec().len(); + let encoded_len = tx.encode_canonical_len(); if encoded_len > MAX_TX_SIZE { return Err(MempoolError::TxSizeExceeded { actual: encoded_len, diff --git a/crates/common/types/transaction.rs b/crates/common/types/transaction.rs index 128397d9b97..905f821f522 100644 --- a/crates/common/types/transaction.rs +++ b/crates/common/types/transaction.rs @@ -1706,6 +1706,27 @@ mod canonic_encoding { self.encode_canonical(&mut buf); buf } + + /// Canonical-encoded length without allocating a buffer. Counts the + /// 1-byte type prefix for typed txs (EIP-2718) plus the inner RLP + /// payload length. Use this when only the size is needed (e.g. + /// admission-time size caps) to avoid `encode_canonical_to_vec().len()`. + pub fn encode_canonical_len(&self) -> usize { + let prefix_len = match self { + Transaction::LegacyTransaction(_) => 0, + _ => 1, + }; + let inner_len = match self { + Transaction::LegacyTransaction(t) => t.length(), + Transaction::EIP2930Transaction(t) => t.length(), + Transaction::EIP1559Transaction(t) => t.length(), + Transaction::EIP4844Transaction(t) => t.length(), + Transaction::EIP7702Transaction(t) => t.length(), + Transaction::FeeTokenTransaction(t) => t.length(), + Transaction::PrivilegedL2Transaction(t) => t.length(), + }; + prefix_len + inner_len + } } impl P2PTransaction { From 3ee3f72b590b2c5822cdb470da346cc3495323b6 Mon Sep 17 00:00:00 2001 From: ilitteri Date: Mon, 11 May 2026 21:30:43 -0300 Subject: [PATCH 5/6] test(l1): drop encode-size sanity tests in transaction.rs Per @MegaRedHand's review (approved with note: "I think we can remove the unit tests"). The two tests in `transaction.rs` (`test_encoded_size_ exceeds_max_tx_size` and `test_encoded_size_below_max_tx_size`) only re-derived the wire-size signal; the admission gate itself is covered by `validate_transaction_rejects_oversize_non_blob` in the integration test suite. --- crates/common/types/transaction.rs | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/crates/common/types/transaction.rs b/crates/common/types/transaction.rs index 905f821f522..0857755357e 100644 --- a/crates/common/types/transaction.rs +++ b/crates/common/types/transaction.rs @@ -3785,31 +3785,4 @@ mod tests { "blob-gas term missing from cost_without_base_fee() for EIP-4844" ); } - - #[test] - fn test_encoded_size_exceeds_max_tx_size() { - // Confirms the wire-size signal used by the mempool admission cap: - // an EIP-1559 tx with > 128 KiB of `data` encodes to > MAX_TX_SIZE bytes. - use crate::types::MAX_TX_SIZE; - - let payload = vec![0u8; MAX_TX_SIZE + 1]; - let tx = Transaction::EIP1559Transaction(EIP1559Transaction { - data: Bytes::from(payload), - ..Default::default() - }); - - assert!( - tx.encode_canonical_to_vec().len() > MAX_TX_SIZE, - "tx with > 128 KiB calldata must encode larger than MAX_TX_SIZE" - ); - } - - #[test] - fn test_encoded_size_below_max_tx_size() { - // A small tx encodes well below the cap. - use crate::types::MAX_TX_SIZE; - - let tx = Transaction::EIP1559Transaction(EIP1559Transaction::default()); - assert!(tx.encode_canonical_to_vec().len() <= MAX_TX_SIZE); - } } From 9d21d06d5794fcf1e4299f73eec24fce8fc4094a Mon Sep 17 00:00:00 2001 From: ilitteri Date: Mon, 11 May 2026 21:32:07 -0300 Subject: [PATCH 6/6] perf(l1): run mempool admission size cap before sender recovery Codex + Claude review both flagged that the wire-size gate runs after secp256k1 sender recovery in `add_transaction_to_pool`. For a tx that fails the size check, the work was wasted; for a tx that passes, this is just ordering. Matches geth's order: `ValidateTransaction` runs the size check before any crypto. `add_transaction_to_pool` now applies the cap right after the `BlobTxNoBlobsBundle` rejection, before `contains_tx` and sender recovery. The same check remains inside `validate_transaction` so direct callers (integration tests, L2 paths) keep the guarantee unchanged. The blob wrapper-size check is already first in `add_blob_transaction_to_pool_inner` from a prior commit. --- crates/blockchain/blockchain.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 7e84e8cb065..8f3f301d8f6 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -2364,6 +2364,18 @@ impl Blockchain { if matches!(transaction, Transaction::EIP4844Transaction(_)) { return Err(MempoolError::BlobTxNoBlobsBundle); } + // Wire size cap: run before sender recovery so oversized txs don't + // force secp256k1 work. Matches geth's `txMaxSize` admission order + // (size-checked at `ValidateTransaction` entry, well before any + // crypto). The same check sits in `validate_transaction` so direct + // callers (tests, L2 paths) keep the guarantee. + let encoded_len = transaction.encode_canonical_len(); + if encoded_len > MAX_TX_SIZE { + return Err(MempoolError::TxSizeExceeded { + actual: encoded_len, + limit: MAX_TX_SIZE, + }); + } let hash = transaction.hash(); if self.mempool.contains_tx(hash)? { return Ok(hash);