diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 98ebe601fd1..8f3f301d8f6 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}; @@ -72,6 +69,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::{ @@ -2323,6 +2321,20 @@ 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_len() + blobs_bundle.length(); + 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)?; @@ -2352,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); @@ -2432,7 +2456,21 @@ 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 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_len(); + if encoded_len > MAX_TX_SIZE { + return Err(MempoolError::TxSizeExceeded { + actual: encoded_len, + limit: MAX_TX_SIZE, + }); + } + } // Check init code size // [EIP-7954] - Amsterdam increases the limit @@ -2448,10 +2486,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 39436472dd4..b37336d2919 100644 --- a/crates/blockchain/error.rs +++ b/crates/blockchain/error.rs @@ -81,8 +81,8 @@ 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")] 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..0857755357e 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 { diff --git a/test/tests/blockchain/mempool_tests.rs b/test/tests/blockchain/mempool_tests.rs index 9098b2599bd..65e04d88359 100644 --- a/test/tests/blockchain/mempool_tests.rs +++ b/test/tests/blockchain/mempool_tests.rs @@ -357,6 +357,36 @@ 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), + } +} + #[test] fn test_filter_mempool_transactions() { let plain_tx_decoded = Transaction::decode_canonical(&hex::decode("f86d80843baa0c4082f618946177843db3138ae69679a54b95cf345ed759450d870aa87bee538000808360306ba0151ccc02146b9b11adf516e6787b59acae3e76544fdcd75e77e67c6b598ce65da064c5dd5aae2fbb535830ebbdad0234975cd7ece3562013b63ea18cc0df6c97d4").unwrap()).unwrap();