Skip to content
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
52 changes: 43 additions & 9 deletions crates/blockchain/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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::{
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 0 additions & 3 deletions crates/blockchain/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions crates/blockchain/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 10 additions & 0 deletions crates/common/types/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
21 changes: 21 additions & 0 deletions crates/common/types/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
30 changes: 30 additions & 0 deletions test/tests/blockchain/mempool_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading