diff --git a/Cargo.lock b/Cargo.lock
index 0a3b9dae..7c7cdfc8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4939,6 +4939,7 @@ name = "morph-consensus"
version = "1.1.0"
dependencies = [
"alloy-consensus",
+ "alloy-eips",
"alloy-evm",
"alloy-genesis",
"alloy-primitives",
diff --git a/README.md b/README.md
index a2239452..620173bd 100644
--- a/README.md
+++ b/README.md
@@ -112,8 +112,7 @@ openssl rand -hex 32 > jwt.hex
| Flag | Default | Description |
|------|---------|-------------|
-| `--morph.max-tx-payload-bytes` | 122880 (120KB) | Maximum transaction payload bytes per block |
-| `--morph.max-tx-per-block` | None (unlimited) | Maximum number of transactions per block |
+| `--morph.max-tx-payload-bytes` | 737280 (720 KiB) | Maximum L2 tx payload bytes per block (fits one uncompressed 6-blob batch) |
| `--rpc.eth-proof-window` | 0 (disabled) | Max historical blocks for `eth_getProof` (up to 1209600) |
### Running Tests
diff --git a/crates/chainspec/src/constants.rs b/crates/chainspec/src/constants.rs
index 56b98755..f24a18fa 100644
--- a/crates/chainspec/src/constants.rs
+++ b/crates/chainspec/src/constants.rs
@@ -12,6 +12,14 @@ pub const MORPH_HOODI_CHAIN_ID: u64 = 2910;
/// The sequencer has the right to set any base fee below `MORPH_MAX_BASE_FEE`.
pub const MORPH_BASE_FEE: u64 = 1_000_000;
+/// Maximum L2 transaction payload bytes per block (L1 messages excluded).
+///
+/// Matches morph-geth `params.MorphMaxTxPayloadBytesPerBlock` (`720 * 1024`).
+/// `720 KiB = 120 KiB × 6`, sized so one uncompressed L2 block fits in a 6-blob
+/// batch (`6 × 4096 × 31 = 761_856` usable bytes). Enforced on import by Morph
+/// consensus and used as the sequencer packing default.
+pub const MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK: u64 = 720 * 1024;
+
/// Default priority fee returned by `eth_maxPriorityFeePerGas` when the gas
/// price oracle has no usable block samples (cold start or empty/zero-tip
/// blocks, the common case on Morph L2).
@@ -110,4 +118,10 @@ mod tests {
fn test_base_fee() {
assert_eq!(MORPH_BASE_FEE, 1_000_000);
}
+
+ #[test]
+ fn test_max_tx_payload_bytes_per_block() {
+ assert_eq!(MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK, 720 * 1024);
+ assert_eq!(MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK, 737_280);
+ }
}
diff --git a/crates/chainspec/src/genesis.rs b/crates/chainspec/src/genesis.rs
index b296b1b5..98a2b93e 100644
--- a/crates/chainspec/src/genesis.rs
+++ b/crates/chainspec/src/genesis.rs
@@ -80,15 +80,22 @@ impl TryFrom<&OtherFields> for MorphHardforkInfo {
}
/// The configuration for the Morph chain.
+///
+/// The genesis keys `maxTxPayloadBytesPerBlock` (122880 on mainnet/hoodi) and
+/// `maxTxPerBlock` are still present in the genesis JSON but are deliberately not
+/// read here. Sequencer packing uses `--morph.max-tx-payload-bytes` (default
+/// [`crate::MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK`]) rather than the leftover
+/// zkEVM genesis field.
+///
+/// Import-time body validation in morph-geth (`IsValidBlockSize`) and morph-reth
+/// (`MorphConsensus::validate_block_pre_execution`) both enforce that same
+/// 720 KiB binary constant, not the stored genesis 122880.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MorphChainConfig {
/// The address of the L2 transaction fee vault.
#[serde(skip_serializing_if = "Option::is_none")]
pub fee_vault_address: Option
,
- /// The maximum tx payload size per block in bytes.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub max_tx_payload_bytes_per_block: Option,
}
impl MorphChainConfig {
@@ -101,13 +108,6 @@ impl MorphChainConfig {
pub const fn is_fee_vault_enabled(&self) -> bool {
self.fee_vault_address.is_some()
}
-
- /// Checks if the given block size (in bytes) is valid for this chain.
- pub fn is_valid_block_size(&self, size: usize) -> bool {
- self.max_tx_payload_bytes_per_block
- .map(|max| size <= max)
- .unwrap_or(true)
- }
}
impl TryFrom<&OtherFields> for MorphChainConfig {
@@ -174,10 +174,7 @@ mod tests {
config.fee_vault_address,
Some(address!("530000000000000000000000000000000000000a"))
);
- assert_eq!(config.max_tx_payload_bytes_per_block, Some(122880));
assert!(config.is_fee_vault_enabled());
- assert!(config.is_valid_block_size(100000));
- assert!(!config.is_valid_block_size(200000));
}
#[test]
@@ -185,8 +182,26 @@ mod tests {
let config = MorphChainConfig::default();
assert!(!config.is_fee_vault_enabled());
assert_eq!(config.fee_vault_address, None);
- assert_eq!(config.max_tx_payload_bytes_per_block, None);
- // Without max size limit, any size is valid
- assert!(config.is_valid_block_size(usize::MAX));
+ }
+
+ #[test]
+ fn test_ignores_unused_packing_fields() {
+ let config_str = r#"
+ {
+ "morph": {
+ "feeVaultAddress": "0x530000000000000000000000000000000000000a",
+ "maxTxPayloadBytesPerBlock": 122880,
+ "maxTxPerBlock": 100
+ }
+ }
+ "#;
+
+ let others: OtherFields = serde_json::from_str(config_str).unwrap();
+ let config = MorphChainConfig::extract_from(&others).unwrap();
+
+ assert_eq!(
+ config.fee_vault_address,
+ Some(address!("530000000000000000000000000000000000000a"))
+ );
}
}
diff --git a/crates/chainspec/src/spec.rs b/crates/chainspec/src/spec.rs
index 39e83e93..6477747c 100644
--- a/crates/chainspec/src/spec.rs
+++ b/crates/chainspec/src/spec.rs
@@ -301,16 +301,6 @@ impl MorphChainSpec {
pub fn fee_vault_address(&self) -> Option {
self.info.morph_chain_info.fee_vault_address
}
-
- /// Returns the maximum tx payload size per block in bytes.
- pub fn max_tx_payload_bytes_per_block(&self) -> Option {
- self.info.morph_chain_info.max_tx_payload_bytes_per_block
- }
-
- /// Checks if the given block size (in bytes) is valid for this chain.
- pub fn is_valid_block_size(&self, size: usize) -> bool {
- self.info.morph_chain_info.is_valid_block_size(size)
- }
}
impl From for MorphChainSpec {
@@ -716,9 +706,10 @@ mod tests {
let chainspec = MorphChainSpec::from(genesis);
assert!(chainspec.is_fee_vault_enabled());
- assert_eq!(chainspec.max_tx_payload_bytes_per_block(), Some(122880));
- assert!(chainspec.is_valid_block_size(100000));
- assert!(!chainspec.is_valid_block_size(200000));
+ assert_eq!(
+ chainspec.fee_vault_address(),
+ Some(address!("530000000000000000000000000000000000000a"))
+ );
}
#[test]
diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml
index 880abd21..5f40f368 100644
--- a/crates/consensus/Cargo.toml
+++ b/crates/consensus/Cargo.toml
@@ -23,6 +23,7 @@ reth-primitives-traits.workspace = true
# Alloy
alloy-consensus.workspace = true
+alloy-eips.workspace = true
alloy-evm.workspace = true
alloy-primitives.workspace = true
alloy-rlp.workspace = true
diff --git a/crates/consensus/src/error.rs b/crates/consensus/src/error.rs
index 280f7e09..42896bd8 100644
--- a/crates/consensus/src/error.rs
+++ b/crates/consensus/src/error.rs
@@ -64,6 +64,18 @@ pub enum MorphConsensusError {
/// Withdrawals are not empty.
#[error("Withdrawals are not empty")]
WithdrawalsNonEmpty,
+
+ /// L2 transaction payload exceeds the per-block DA cap.
+ ///
+ /// Matches go-ethereum `ErrInvalidBlockPayloadSize`. L1 messages are
+ /// excluded from `size`.
+ #[error("invalid block payload size: {size} exceeds limit {limit}")]
+ InvalidBlockPayloadSize {
+ /// Encoded L2 payload bytes (EIP-2718, L1 messages excluded).
+ size: u64,
+ /// Maximum allowed payload bytes.
+ limit: u64,
+ },
}
impl From for MorphConsensusError {
diff --git a/crates/consensus/src/validation.rs b/crates/consensus/src/validation.rs
index cd12183c..44cdc161 100644
--- a/crates/consensus/src/validation.rs
+++ b/crates/consensus/src/validation.rs
@@ -27,6 +27,8 @@
//! - No uncle blocks allowed
//! - Withdrawals field must not be present
//! - Transaction root must be valid
+//! - L2 transaction payload (EIP-2718 encoded, L1 messages excluded) must not
+//! exceed [`morph_chainspec::MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK`]
//!
//! ## Post-Execution Validation
//!
@@ -36,9 +38,10 @@
//!
use crate::MorphConsensusError;
use alloy_consensus::{BlockHeader as _, EMPTY_OMMER_ROOT_HASH, TxReceipt};
+use alloy_eips::eip2718::Encodable2718;
use alloy_evm::block::BlockExecutionResult;
use alloy_primitives::{B256, Bloom};
-use morph_chainspec::{MorphChainSpec, MorphHardforks};
+use morph_chainspec::{MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK, MorphChainSpec, MorphHardforks};
use morph_primitives::{
Block, BlockBody, MorphHeader, MorphReceipt, MorphTxEnvelope,
transaction::morph_transaction::MORPH_TX_VERSION_1,
@@ -271,7 +274,9 @@ impl Consensus for MorphConsensus {
/// 2. **Ommers Hash**: Must be the empty ommer root hash
/// 3. **Transaction Root**: Must be valid
/// 4. **Withdrawals**: Must be empty (Morph L2 doesn't support withdrawals)
- /// 5. **L1 Messages**: Must be ordered correctly (sequential queue indices, L1 before L2)
+ /// 5. **L2 Payload Size**: Encoded L2 txs (L1 messages excluded) must not
+ /// exceed [`MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK`]
+ /// 6. **L1 Messages**: Must be ordered correctly (sequential queue indices, L1 before L2)
fn validate_block_pre_execution(
&self,
block: &SealedBlock,
@@ -305,6 +310,9 @@ impl Consensus for MorphConsensus {
));
}
+ // Matches go-ethereum's BlockValidator.ValidateBody() → IsValidBlockSize().
+ validate_l2_tx_payload_size(&block.body().transactions)?;
+
// Validate MorphTx activation, version and field constraints.
// Matches go-ethereum's BlockValidator.ValidateBody() → ValidateMorphTxVersion().
let is_emerald = self
@@ -467,6 +475,37 @@ fn validate_against_parent_gas_limit(
Ok(())
}
+// ============================================================================
+// L2 Payload Size Validation
+// ============================================================================
+
+/// Sum of EIP-2718 encoded L2 transaction bytes.
+///
+/// Matches go-ethereum `Block.PayloadSize()`: L1 messages are excluded because
+/// their calldata is already published on L1.
+fn l2_tx_payload_bytes(txs: &[MorphTxEnvelope]) -> u64 {
+ txs.iter()
+ .filter(|tx| !tx.is_l1_msg())
+ .map(|tx| tx.encode_2718_len() as u64)
+ .fold(0, u64::saturating_add)
+}
+
+/// Rejects blocks whose L2 payload exceeds [`MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK`].
+///
+/// Matches go-ethereum `MorphConfig.IsValidBlockSize` (`size <= limit`).
+fn validate_l2_tx_payload_size(txs: &[MorphTxEnvelope]) -> Result<(), ConsensusError> {
+ let size = l2_tx_payload_bytes(txs);
+ if size > MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK {
+ return Err(ConsensusError::other(
+ MorphConsensusError::InvalidBlockPayloadSize {
+ size,
+ limit: MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK,
+ },
+ ));
+ }
+ Ok(())
+}
+
// ============================================================================
// L1 Message Validation
// ============================================================================
@@ -766,13 +805,17 @@ mod tests {
}
fn create_l1_msg_tx(queue_index: u64) -> MorphTxEnvelope {
+ create_l1_msg_tx_with_input(queue_index, Bytes::default())
+ }
+
+ fn create_l1_msg_tx_with_input(queue_index: u64, input: Bytes) -> MorphTxEnvelope {
use alloy_consensus::Sealed;
let tx = TxL1Msg {
queue_index,
gas_limit: 21000,
to: Address::ZERO,
value: U256::ZERO,
- input: Bytes::default(),
+ input,
sender: Address::ZERO,
};
// L1 messages have no signature - use Sealed instead of Signed
@@ -780,8 +823,16 @@ mod tests {
}
fn create_regular_tx() -> MorphTxEnvelope {
+ create_legacy_tx_with_input(Bytes::default())
+ }
+
+ fn create_legacy_tx_with_input(input: Bytes) -> MorphTxEnvelope {
use alloy_consensus::TxLegacy;
- let tx = TxLegacy::default();
+ let tx = TxLegacy {
+ gas_limit: 1_000_000,
+ input,
+ ..Default::default()
+ };
let sig = Signature::new(U256::ZERO, U256::ZERO, false);
MorphTxEnvelope::Legacy(Signed::new_unchecked(tx, sig, B256::ZERO))
}
@@ -789,17 +840,26 @@ mod tests {
fn create_sealed_block(
timestamp: u64,
transactions: Vec,
+ ) -> SealedBlock {
+ create_sealed_block_with_next_l1(timestamp, transactions, 0)
+ }
+
+ fn create_sealed_block_with_next_l1(
+ timestamp: u64,
+ transactions: Vec,
+ next_l1_msg_index: u64,
) -> SealedBlock {
use alloy_consensus::proofs::calculate_transaction_root;
use reth_primitives_traits::Block as _;
let transactions_root = calculate_transaction_root(&transactions);
- let header = create_morph_header(Header {
+ let mut header = create_morph_header(Header {
timestamp,
transactions_root,
ommers_hash: EMPTY_OMMER_ROOT_HASH,
..Default::default()
});
+ header.next_l1_msg_index = next_l1_msg_index;
Block::new(
header,
BlockBody {
@@ -1865,6 +1925,70 @@ mod tests {
);
}
+ #[test]
+ fn test_l2_tx_payload_bytes_excludes_l1_messages() {
+ let l1 = create_l1_msg_tx_with_input(
+ 0,
+ Bytes::from(vec![1u8; MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK as usize + 1]),
+ );
+ let l2 = create_regular_tx();
+
+ assert_eq!(l2_tx_payload_bytes(std::slice::from_ref(&l1)), 0);
+ assert_eq!(
+ l2_tx_payload_bytes(&[l1, l2.clone()]),
+ l2_tx_payload_bytes(std::slice::from_ref(&l2))
+ );
+ }
+
+ #[test]
+ fn test_validate_block_pre_execution_accepts_payload_at_limit() {
+ let consensus = MorphConsensus::new(create_test_chainspec());
+ // A default legacy tx is well under the 720 KiB cap.
+ let block = create_sealed_block(0, vec![create_regular_tx()]);
+
+ assert!(consensus.validate_block_pre_execution(&block).is_ok());
+ assert!(
+ l2_tx_payload_bytes(&block.body().transactions) <= MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK
+ );
+ }
+
+ #[test]
+ fn test_validate_block_pre_execution_rejects_oversized_l2_payload() {
+ let consensus = MorphConsensus::new(create_test_chainspec());
+ let oversized = create_legacy_tx_with_input(Bytes::from(vec![
+ 0u8;
+ MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK
+ as usize
+ ]));
+ let size = l2_tx_payload_bytes(std::slice::from_ref(&oversized));
+ assert!(size > MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK);
+
+ let block = create_sealed_block(0, vec![oversized]);
+ let result = consensus.validate_block_pre_execution(&block);
+
+ assert!(result.is_err());
+ let err = result.unwrap_err().to_string();
+ assert!(
+ err.contains("invalid block payload size"),
+ "unexpected error: {err}"
+ );
+ }
+
+ #[test]
+ fn test_validate_block_pre_execution_ignores_large_l1_messages() {
+ let consensus = MorphConsensus::new(create_test_chainspec());
+ let huge_l1 = create_l1_msg_tx_with_input(
+ 0,
+ Bytes::from(vec![
+ 1u8;
+ MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK as usize + 1024
+ ]),
+ );
+ let block = create_sealed_block_with_next_l1(0, vec![huge_l1, create_regular_tx()], 1);
+
+ assert!(consensus.validate_block_pre_execution(&block).is_ok());
+ }
+
#[test]
fn test_validate_morph_tx_v1_fee_token_0_with_fee_limit_rejected() {
use alloy_consensus::Signed;
diff --git a/crates/node/src/args.rs b/crates/node/src/args.rs
index 7e5e9233..95282e61 100644
--- a/crates/node/src/args.rs
+++ b/crates/node/src/args.rs
@@ -1,45 +1,48 @@
//! Morph node CLI arguments.
use clap::Args;
+use morph_chainspec::MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK;
-/// Default maximum transaction payload bytes per block (120KB).
+/// Default maximum L2 transaction payload bytes per block (720 KiB).
///
-/// This matches Morph's go-ethereum configuration.
-pub const MORPH_DEFAULT_MAX_TX_PAYLOAD_BYTES: u64 = 122_880;
+/// `720 KiB = 120 KiB × 6`. A Morph batch can carry up to 6 EIP-4844 blobs.
+/// Each blob's usable payload is `4096 × 31 = 126_976` bytes (~124 KiB), so
+/// six blobs hold 761_856 bytes uncompressed. 120 KiB per blob is the
+/// historical headroom under that usable size; six of them stay under the
+/// uncompressed 6-blob budget and do not require the submitter to split a
+/// single L2 block.
+pub const MORPH_DEFAULT_MAX_TX_PAYLOAD_BYTES: u64 = MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK;
/// Morph-specific CLI arguments.
///
-/// These arguments extend the standard reth CLI with Morph-specific options
-/// for block building and transaction limits.
+/// Block packing is bounded by header `gasLimit`, the payload builder time
+/// budget, and `--morph.max-tx-payload-bytes` (the uncompressed L2 payload
+/// that must fit in one 6-blob batch).
///
/// Note: Block building deadline is configured via reth's built-in `--builder.deadline` flag.
#[derive(Debug, Clone, Args)]
#[command(next_help_heading = "Morph")]
pub struct MorphArgs {
- /// Maximum transaction payload bytes per block.
+ /// Maximum L2 transaction payload bytes per block (L1 messages excluded).
///
- /// Limits the total size of transactions included in a single block.
- /// Default: 122880 bytes (120KB), matching Morph's go-ethereum configuration.
+ /// Default: 737280 bytes (720 KiB), sized so one L2 block fits in a
+ /// 6-blob batch even without compression.
+ ///
+ /// Import-time consensus always enforces
+ /// [`morph_chainspec::MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK`], independent of
+ /// this flag. Packing above that value produces blocks other nodes reject.
#[arg(
long = "morph.max-tx-payload-bytes",
value_name = "BYTES",
default_value_t = MORPH_DEFAULT_MAX_TX_PAYLOAD_BYTES
)]
pub max_tx_payload_bytes: u64,
-
- /// Maximum number of transactions per block.
- ///
- /// If not set, there is no limit on the number of transactions.
- /// Morph Holesky testnet uses 1000 as the default limit.
- #[arg(long = "morph.max-tx-per-block", value_name = "COUNT")]
- pub max_tx_per_block: Option,
}
impl Default for MorphArgs {
fn default() -> Self {
Self {
max_tx_payload_bytes: MORPH_DEFAULT_MAX_TX_PAYLOAD_BYTES,
- max_tx_per_block: None,
}
}
}
@@ -62,35 +65,18 @@ mod tests {
args.max_tx_payload_bytes,
MORPH_DEFAULT_MAX_TX_PAYLOAD_BYTES
);
- assert_eq!(args.max_tx_per_block, None);
+ assert_eq!(args.max_tx_payload_bytes, 720 * 1024);
}
#[test]
- fn test_custom_args() {
+ fn test_custom_payload_bytes() {
let args = CommandParser::::parse_from([
"test",
"--morph.max-tx-payload-bytes",
"100000",
- "--morph.max-tx-per-block",
- "500",
])
.args;
assert_eq!(args.max_tx_payload_bytes, 100000);
- assert_eq!(args.max_tx_per_block, Some(500));
- }
-
- #[test]
- fn test_all_args_combined() {
- let args = CommandParser::::parse_from([
- "test",
- "--morph.max-tx-payload-bytes",
- "200000",
- "--morph.max-tx-per-block",
- "1000",
- ])
- .args;
- assert_eq!(args.max_tx_payload_bytes, 200000);
- assert_eq!(args.max_tx_per_block, Some(1000));
}
#[test]
@@ -105,6 +91,14 @@ mod tests {
);
}
+ #[test]
+ fn unused_tx_count_flag_is_not_supported() {
+ assert!(
+ CommandParser::::try_parse_from(["test", "--morph.max-tx-per-block", "1"])
+ .is_err()
+ );
+ }
+
#[test]
fn test_default_trait_impl() {
let args = MorphArgs::default();
@@ -112,6 +106,5 @@ mod tests {
args.max_tx_payload_bytes,
MORPH_DEFAULT_MAX_TX_PAYLOAD_BYTES
);
- assert!(args.max_tx_per_block.is_none());
}
}
diff --git a/crates/node/src/components/payload.rs b/crates/node/src/components/payload.rs
index 2035c676..37436ca5 100644
--- a/crates/node/src/components/payload.rs
+++ b/crates/node/src/components/payload.rs
@@ -32,12 +32,6 @@ impl MorphPayloadBuilderBuilder {
self.config = self.config.with_max_da_block_size(max_da_block_size);
self
}
-
- /// Sets the maximum number of transactions per block.
- pub fn with_max_tx_per_block(mut self, max_tx_per_block: u64) -> Self {
- self.config = self.config.with_max_tx_per_block(max_tx_per_block);
- self
- }
}
impl
diff --git a/crates/node/src/components/pool.rs b/crates/node/src/components/pool.rs
index 47a196dc..d3ec9799 100644
--- a/crates/node/src/components/pool.rs
+++ b/crates/node/src/components/pool.rs
@@ -106,15 +106,16 @@ mod tests {
use alloy_primitives::{B256, Sealed, Signature, U256};
use morph_primitives::{MorphTxEnvelope, TxL1Msg};
use morph_txpool::MorphPooledTransaction;
- use reth_transaction_pool::PoolTransaction;
+ use reth_transaction_pool::{PoolTransaction, validate::DEFAULT_MAX_TX_INPUT_BYTES};
#[tokio::test]
async fn test_validate_oversized_transaction() {
- // Test that transactions exceeding max_tx_input_bytes are rejected
- // The default max_tx_input_bytes in reth is 120KB (122,880 bytes)
-
- // For this test, we create a mock pool that would reject oversized transactions
- // The actual validation happens in the validator when checking encoded size
+ // `DEFAULT_MAX_TX_INPUT_BYTES` is 4 * TX_SLOT_BYTE_SIZE = 128 KiB. Despite the
+ // "input" in the name, for non-blob transactions the validator compares it against
+ // the full 2718-encoded length, which matches go-ethereum's `txMaxSize`.
+ //
+ // This test only covers the size the pool transaction reports; the rejection itself
+ // lives in the validator, which needs a full provider to exercise.
// Create a legacy transaction
let tx = MorphTxEnvelope::Legacy(Signed::new_unchecked(
@@ -126,15 +127,13 @@ mod tests {
Default::default(),
));
- // Create a pool transaction with an encoded length exceeding the limit (121KB > 120KB)
+ // Create a pool transaction one byte over the limit
let pool_tx = MorphPooledTransaction::new(
Recovered::new_unchecked(tx, Default::default()),
- 121 * 1024,
+ DEFAULT_MAX_TX_INPUT_BYTES + 1,
);
- // Verify the encoded length is larger than the limit
- assert_eq!(pool_tx.encoded_length(), 121 * 1024);
- assert!(pool_tx.encoded_length() > 120 * 1024);
+ assert!(pool_tx.encoded_length() > DEFAULT_MAX_TX_INPUT_BYTES);
}
#[tokio::test]
diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs
index 347cef93..b73f5073 100644
--- a/crates/node/src/node.rs
+++ b/crates/node/src/node.rs
@@ -118,17 +118,9 @@ where
type AddOns = MorphAddOns>;
fn components_builder(&self) -> Self::ComponentsBuilder {
- // Build payload config from args
- let payload_config =
- MorphBuilderConfig::default().with_max_da_block_size(self.args.max_tx_payload_bytes);
-
- let payload_config = if let Some(max_tx) = self.args.max_tx_per_block {
- payload_config.with_max_tx_per_block(max_tx)
- } else {
- payload_config
- };
-
- Self::components(payload_config)
+ Self::components(
+ MorphBuilderConfig::default().with_max_da_block_size(self.args.max_tx_payload_bytes),
+ )
}
fn add_ons(&self) -> Self::AddOns {
@@ -237,27 +229,6 @@ mod tests {
use morph_chainspec::MORPH_HOODI;
use reth_payload_primitives::PayloadAttributesBuilder;
- #[test]
- fn morph_node_default() {
- let node = MorphNode::default();
- assert_eq!(
- node.args.max_tx_payload_bytes,
- super::super::args::MORPH_DEFAULT_MAX_TX_PAYLOAD_BYTES
- );
- assert!(node.args.max_tx_per_block.is_none());
- }
-
- #[test]
- fn morph_node_new_with_args() {
- let args = super::super::args::MorphArgs {
- max_tx_payload_bytes: 200_000,
- max_tx_per_block: Some(500),
- };
- let node = MorphNode::new(args);
- assert_eq!(node.args.max_tx_payload_bytes, 200_000);
- assert_eq!(node.args.max_tx_per_block, Some(500));
- }
-
#[test]
fn payload_attributes_builder_produces_valid_attributes() {
let chain_spec = MORPH_HOODI.clone();
diff --git a/crates/payload/builder/src/builder.rs b/crates/payload/builder/src/builder.rs
index e2dc38b4..c0d0c13a 100644
--- a/crates/payload/builder/src/builder.rs
+++ b/crates/payload/builder/src/builder.rs
@@ -5,7 +5,6 @@ use crate::{MorphBuilderConfig, MorphPayloadBuilderError, config::PayloadBuildin
use alloy_consensus::{BlockHeader, Transaction, Typed2718};
use alloy_eips::eip2718::Encodable2718;
use alloy_primitives::{B256, Bytes, U256};
-use alloy_rlp::Encodable;
use morph_chainspec::MorphChainSpec;
use morph_chainspec::{L2_MESSAGE_QUEUE_ADDRESS, L2_MESSAGE_QUEUE_WITHDRAW_TRIE_ROOT_SLOT};
use morph_evm::{MorphEvmConfig, MorphNextBlockEnvAttributes};
@@ -361,8 +360,9 @@ impl MorphPayloadBuilderCtx {
let tx_gas = recovered_tx.gas_limit();
- // Check if adding this transaction would exceed block gas limit
- if info.cumulative_gas_used + tx_gas > block_gas_limit {
+ // Check if adding this transaction would exceed block gas limit.
+ // L1 messages are excluded from DA payload size (prepaid on L1).
+ if info.is_tx_over_limits(tx_gas, 0, block_gas_limit) {
tracing::warn!(
target: "payload_builder",
tx_index = tx_idx,
@@ -499,12 +499,8 @@ impl MorphPayloadBuilderCtx {
return Ok(Some(()));
}
- // Check if the breaker triggers (time/gas/DA/tx count limits)
- if breaker.should_break(
- info.cumulative_gas_used,
- info.cumulative_da_bytes_used,
- info.transaction_count,
- ) {
+ // Check if the breaker triggers (time, gas, or DA limits)
+ if breaker.should_break(info.cumulative_gas_used, info.cumulative_da_bytes_used) {
tracing::debug!(
target: "payload_builder",
cumulative_gas_used = info.cumulative_gas_used,
@@ -536,23 +532,18 @@ impl MorphPayloadBuilderCtx {
continue;
}
- // Check if the transaction exceeds block limits (gas or DA size).
- // Rare in practice; logged at debug to avoid pool-skip noise.
- if info.is_tx_over_limits(
- tx.gas_limit(),
- tx.length() as u64,
- block_gas_limit,
- self.builder_config.max_da_block_size,
- ) {
+ // Skip transactions that cannot fit in remaining block gas or DA size.
+ let tx_size = tx.encode_2718_len() as u64;
+ if info.is_tx_over_limits(tx.gas_limit(), tx_size, block_gas_limit) {
tracing::debug!(
target: "payload_builder",
signer = %tx.signer(),
nonce = tx.nonce(),
tx_gas_limit = tx.gas_limit(),
- tx_size = tx.length(),
+ tx_size,
block_gas_limit,
max_da_block_size = self.builder_config.max_da_block_size,
- "pool transaction exceeds block limits; skipping"
+ "pool transaction exceeds remaining block gas or DA size; skipping"
);
best_txs.mark_invalid(tx.signer(), tx.nonce());
continue;
@@ -621,7 +612,7 @@ impl MorphPayloadBuilderCtx {
// Update execution info
info.cumulative_gas_used += gas_used;
- info.cumulative_da_bytes_used += tx.length() as u64;
+ info.cumulative_da_bytes_used += tx_size;
info.transaction_count += 1;
// Calculate fees: effective_tip * gas_used
@@ -643,7 +634,8 @@ impl MorphPayloadBuilderCtx {
struct ExecutionInfo {
/// Cumulative gas used by all executed transactions.
cumulative_gas_used: u64,
- /// Cumulative DA bytes used (for L2 data availability).
+ /// Cumulative encoded L2 transaction bytes counted toward the DA packing cap.
+ /// L1 messages are not included.
cumulative_da_bytes_used: u64,
/// Total fees collected from executed transactions.
total_fees: U256,
@@ -651,36 +643,44 @@ struct ExecutionInfo {
next_l1_message_index: u64,
/// Number of transactions executed (including both sequencer and pool transactions).
transaction_count: u64,
+ /// Maximum DA block size from the builder config.
+ max_da_block_size: Option,
}
impl ExecutionInfo {
/// Creates a new [`ExecutionInfo`] with the initial next L1 message index from parent.
- const fn new(next_l1_message_index: u64) -> Self {
+ const fn new(next_l1_message_index: u64, max_da_block_size: Option) -> Self {
Self {
cumulative_gas_used: 0,
cumulative_da_bytes_used: 0,
total_fees: U256::ZERO,
next_l1_message_index,
transaction_count: 0,
+ max_da_block_size,
}
}
- /// Returns true if the transaction would exceed the block limits.
- fn is_tx_over_limits(
- &self,
- tx_gas_limit: u64,
- tx_size: u64,
- block_gas_limit: u64,
- block_da_limit: Option,
- ) -> bool {
- // Check DA limit if configured
- if block_da_limit.is_some_and(|da_limit| self.cumulative_da_bytes_used + tx_size > da_limit)
+ /// Returns true if the transaction would exceed remaining block gas or DA size.
+ ///
+ /// An overflowing sum counts as over the limit: wrapping would otherwise let a
+ /// transaction with an absurd gas limit through and produce an invalid block.
+ fn is_tx_over_limits(&self, tx_gas_limit: u64, tx_size: u64, block_gas_limit: u64) -> bool {
+ if self
+ .cumulative_gas_used
+ .checked_add(tx_gas_limit)
+ .is_none_or(|total_gas| total_gas > block_gas_limit)
{
return true;
}
- // Check gas limit
- self.cumulative_gas_used + tx_gas_limit > block_gas_limit
+ if let Some(da_limit) = self.max_da_block_size {
+ return self
+ .cumulative_da_bytes_used
+ .checked_add(tx_size)
+ .is_none_or(|total_da| total_da > da_limit);
+ }
+
+ false
}
}
@@ -752,7 +752,10 @@ where
})?;
// Initialize next_l1_message_index from parent header
- let mut info = ExecutionInfo::new(ctx.parent().next_l1_msg_index);
+ let mut info = ExecutionInfo::new(
+ ctx.parent().next_l1_msg_index,
+ ctx.builder_config.max_da_block_size,
+ );
let base_fee = builder.evm().block().basefee();
let block_gas_limit = builder.evm().block().gas_limit();
@@ -936,27 +939,29 @@ mod tests {
assert_eq!(info.total_fees, U256::ZERO);
assert_eq!(info.next_l1_message_index, 0);
assert_eq!(info.transaction_count, 0);
+ assert_eq!(info.max_da_block_size, None);
}
#[test]
fn test_execution_info_new_with_l1_index() {
- let info = ExecutionInfo::new(42);
+ let info = ExecutionInfo::new(42, Some(720 * 1024));
assert_eq!(info.next_l1_message_index, 42);
assert_eq!(info.cumulative_gas_used, 0);
assert_eq!(info.cumulative_da_bytes_used, 0);
assert_eq!(info.total_fees, U256::ZERO);
assert_eq!(info.transaction_count, 0);
+ assert_eq!(info.max_da_block_size, Some(720 * 1024));
}
#[test]
fn test_execution_info_new_with_zero_index() {
- let info = ExecutionInfo::new(0);
+ let info = ExecutionInfo::new(0, None);
assert_eq!(info.next_l1_message_index, 0);
}
#[test]
fn test_execution_info_new_with_max_index() {
- let info = ExecutionInfo::new(u64::MAX);
+ let info = ExecutionInfo::new(u64::MAX, None);
assert_eq!(info.next_l1_message_index, u64::MAX);
}
@@ -965,13 +970,13 @@ mod tests {
// =========================================================================
#[test]
- fn test_is_tx_over_limits_within_gas_no_da() {
+ fn test_is_tx_over_limits_within_gas() {
let info = ExecutionInfo {
cumulative_gas_used: 100_000,
..Default::default()
};
// tx_gas + cumulative = 100_000 + 21_000 = 121_000, block limit = 30_000_000
- assert!(!info.is_tx_over_limits(21_000, 100, 30_000_000, None));
+ assert!(!info.is_tx_over_limits(21_000, 100, 30_000_000));
}
#[test]
@@ -981,7 +986,7 @@ mod tests {
..Default::default()
};
// tx_gas + cumulative = 29_990_000 + 21_000 = 30_011_000 > 30_000_000
- assert!(info.is_tx_over_limits(21_000, 100, 30_000_000, None));
+ assert!(info.is_tx_over_limits(21_000, 100, 30_000_000));
}
#[test]
@@ -992,7 +997,7 @@ mod tests {
};
// tx_gas + cumulative = 29_979_000 + 21_000 = 30_000_000 == block limit
// Uses > comparison, so exactly at limit is NOT over
- assert!(!info.is_tx_over_limits(21_000, 100, 30_000_000, None));
+ assert!(!info.is_tx_over_limits(21_000, 100, 30_000_000));
}
#[test]
@@ -1002,77 +1007,64 @@ mod tests {
..Default::default()
};
// tx_gas + cumulative = 29_979_001 + 21_000 = 30_000_001 > 30_000_000
- assert!(info.is_tx_over_limits(21_000, 100, 30_000_000, None));
+ assert!(info.is_tx_over_limits(21_000, 100, 30_000_000));
}
#[test]
- fn test_is_tx_over_limits_exceeds_da_limit() {
- let info = ExecutionInfo {
- cumulative_da_bytes_used: 120_000,
- ..Default::default()
- };
- // da_used + tx_size = 120_000 + 10_000 = 130_000 < 131_072, NOT over
- assert!(!info.is_tx_over_limits(21_000, 10_000, 30_000_000, Some(128 * 1024)));
+ fn test_is_tx_over_limits_zero_gas_tx() {
+ let info = ExecutionInfo::default();
+ assert!(!info.is_tx_over_limits(0, 0, 30_000_000));
+ }
- // da_used + tx_size = 120_000 + 12_000 = 132_000 > 131_072
- assert!(info.is_tx_over_limits(21_000, 12_000, 30_000_000, Some(128 * 1024)));
+ #[test]
+ fn test_is_tx_over_limits_zero_block_gas_limit() {
+ let info = ExecutionInfo::default();
+ assert!(info.is_tx_over_limits(1, 0, 0));
+ // 0 > 0 is false
+ assert!(!info.is_tx_over_limits(0, 0, 0));
}
#[test]
- fn test_is_tx_over_limits_da_limit_none_ignores_da() {
+ fn test_is_tx_over_limits_gas_sum_overflow() {
let info = ExecutionInfo {
- cumulative_da_bytes_used: u64::MAX,
+ cumulative_gas_used: 1,
..Default::default()
};
- // Even with max DA usage, no DA limit means it's not over
- assert!(!info.is_tx_over_limits(21_000, 1_000, 30_000_000, None));
+ // Wrapping would yield 0 and wrongly report "fits"; overflow must count as over.
+ assert!(info.is_tx_over_limits(u64::MAX, 0, 30_000_000));
}
#[test]
- fn test_is_tx_over_limits_da_limit_exactly_at_boundary() {
+ fn test_is_tx_over_limits_exceeds_da_limit() {
let info = ExecutionInfo {
- cumulative_da_bytes_used: 100,
+ cumulative_da_bytes_used: 700_000,
+ max_da_block_size: Some(720 * 1024),
..Default::default()
};
- // da_used + tx_size = 100 + 100 = 200 == da_limit, NOT over (uses > not >=)
- assert!(!info.is_tx_over_limits(21_000, 100, 30_000_000, Some(200)));
-
- // da_used + tx_size = 100 + 101 = 201 > 200
- assert!(info.is_tx_over_limits(21_000, 101, 30_000_000, Some(200)));
+ // 700_000 + 40_000 = 740_000 > 737_280
+ assert!(info.is_tx_over_limits(21_000, 40_000, 30_000_000));
+ // 700_000 + 10_000 = 710_000 < 737_280
+ assert!(!info.is_tx_over_limits(21_000, 10_000, 30_000_000));
}
#[test]
- fn test_is_tx_over_limits_gas_ok_but_da_exceeded() {
+ fn test_is_tx_over_limits_da_limit_none_ignores_da() {
let info = ExecutionInfo {
- cumulative_gas_used: 100_000,
- cumulative_da_bytes_used: 500,
+ cumulative_da_bytes_used: u64::MAX,
+ max_da_block_size: None,
..Default::default()
};
- assert!(info.is_tx_over_limits(21_000, 600, 30_000_000, Some(1000)));
+ assert!(!info.is_tx_over_limits(21_000, 1_000, 30_000_000));
}
#[test]
- fn test_is_tx_over_limits_da_ok_but_gas_exceeded() {
+ fn test_is_tx_over_limits_da_sum_overflow() {
let info = ExecutionInfo {
- cumulative_gas_used: 29_990_000,
- cumulative_da_bytes_used: 100,
+ cumulative_da_bytes_used: 1,
+ max_da_block_size: Some(720 * 1024),
..Default::default()
};
- assert!(info.is_tx_over_limits(21_000, 100, 30_000_000, Some(1_000_000)));
- }
-
- #[test]
- fn test_is_tx_over_limits_zero_gas_tx() {
- let info = ExecutionInfo::default();
- assert!(!info.is_tx_over_limits(0, 0, 30_000_000, None));
- }
-
- #[test]
- fn test_is_tx_over_limits_zero_block_gas_limit() {
- let info = ExecutionInfo::default();
- assert!(info.is_tx_over_limits(1, 0, 0, None));
- // 0 > 0 is false
- assert!(!info.is_tx_over_limits(0, 0, 0, None));
+ assert!(info.is_tx_over_limits(21_000, u64::MAX, 30_000_000));
}
// =========================================================================
@@ -1100,9 +1092,7 @@ mod tests {
#[test]
fn test_morph_payload_builder_set_config() {
let builder = MorphPayloadBuilder::<(), ()>::new((), test_evm_config(), ());
- let config = MorphBuilderConfig::default()
- .with_gas_limit(5_000_000)
- .with_max_tx_per_block(500);
+ let config = MorphBuilderConfig::default().with_gas_limit(5_000_000);
let builder = builder.set_config(config.clone());
assert_eq!(builder.config, config);
}
diff --git a/crates/payload/builder/src/config.rs b/crates/payload/builder/src/config.rs
index 7a618b5b..c0a997eb 100644
--- a/crates/payload/builder/src/config.rs
+++ b/crates/payload/builder/src/config.rs
@@ -3,7 +3,6 @@
use core::time::Duration;
use reth_chainspec::MIN_TRANSACTION_GAS;
use reth_primitives_traits::FastInstant as Instant;
-use std::fmt::Debug;
/// Minimal data bytes size per transaction.
/// This is a conservative estimate for the minimum encoded transaction size.
@@ -33,20 +32,13 @@ pub struct MorphBuilderConfig {
/// Maximum total data availability size for a block.
///
- /// L2 transactions need to be published to L1 for data availability.
- /// This limit controls the maximum size of transaction data in a single block.
+ /// L2 transactions are published to L1 in EIP-4844 blobs. This limit is the
+ /// uncompressed L2 tx payload (L1 messages excluded) that must fit in one
+ /// 6-blob batch so the submitter never has to split a single L2 block.
/// If `None`, no DA limit is enforced.
///
/// This corresponds to the `--morph.max-tx-payload-bytes` CLI flag.
pub max_da_block_size: Option,
-
- /// Maximum number of transactions per block.
- ///
- /// If set, the builder will stop adding transactions once this limit is reached.
- /// If `None`, no transaction count limit is enforced.
- ///
- /// This corresponds to the `--morph.max-tx-per-block` CLI flag.
- pub max_tx_per_block: Option,
}
impl Default for MorphBuilderConfig {
@@ -55,30 +47,13 @@ impl Default for MorphBuilderConfig {
gas_limit: None,
// Default to 1 second - leaves time for consensus
time_limit: Duration::from_secs(1),
- // No DA limit by default
+ // No DA limit by default; the node wires the CLI default.
max_da_block_size: None,
- // No transaction count limit by default
- max_tx_per_block: None,
}
}
}
impl MorphBuilderConfig {
- /// Creates a new [`MorphBuilderConfig`] with the specified parameters.
- pub const fn new(
- gas_limit: Option,
- time_limit: Duration,
- max_da_block_size: Option,
- max_tx_per_block: Option,
- ) -> Self {
- Self {
- gas_limit,
- time_limit,
- max_da_block_size,
- max_tx_per_block,
- }
- }
-
/// Sets the gas limit.
pub const fn with_gas_limit(mut self, gas_limit: u64) -> Self {
self.gas_limit = Some(gas_limit);
@@ -97,32 +72,20 @@ impl MorphBuilderConfig {
self
}
- /// Sets the maximum number of transactions per block.
- pub const fn with_max_tx_per_block(mut self, max_tx_per_block: u64) -> Self {
- self.max_tx_per_block = Some(max_tx_per_block);
- self
- }
-
/// Creates a [`PayloadBuildingBreaker`] for this configuration.
pub(crate) fn breaker(&self, block_gas_limit: u64) -> PayloadBuildingBreaker {
// Use configured gas limit or fall back to block gas limit
let effective_gas_limit = self.gas_limit.unwrap_or(block_gas_limit);
- PayloadBuildingBreaker::new(
- self.time_limit,
- effective_gas_limit,
- self.max_da_block_size,
- self.max_tx_per_block,
- )
+ PayloadBuildingBreaker::new(self.time_limit, effective_gas_limit, self.max_da_block_size)
}
}
-/// Used in the payload builder to exit the transactions execution loop early.
+/// Used in the [`super::MorphPayloadBuilder`] to exit the transactions execution loop early.
///
-/// The breaker checks four conditions:
+/// The breaker checks three conditions:
/// 1. Time limit - stop if building takes too long
/// 2. Gas limit - stop if remaining gas is insufficient for any transaction
/// 3. DA limit - stop if data availability size limit is reached
-/// 4. Transaction count limit - stop if maximum transactions per block is reached
#[derive(Debug, Clone)]
pub struct PayloadBuildingBreaker {
/// When the payload building started.
@@ -133,24 +96,16 @@ pub struct PayloadBuildingBreaker {
gas_limit: u64,
/// Maximum DA block size.
max_da_block_size: Option,
- /// Maximum number of transactions per block.
- max_tx_per_block: Option,
}
impl PayloadBuildingBreaker {
/// Creates a new [`PayloadBuildingBreaker`].
- fn new(
- time_limit: Duration,
- gas_limit: u64,
- max_da_block_size: Option,
- max_tx_per_block: Option,
- ) -> Self {
+ fn new(time_limit: Duration, gas_limit: u64, max_da_block_size: Option) -> Self {
Self {
start: Instant::now(),
time_limit,
gas_limit,
max_da_block_size,
- max_tx_per_block,
}
}
@@ -160,13 +115,7 @@ impl PayloadBuildingBreaker {
/// - Time limit has been exceeded
/// - Gas limit has been reached (leaving room for at least one minimal transaction)
/// - DA size limit has been reached (leaving room for at least one minimal transaction)
- /// - Transaction count limit has been reached
- pub fn should_break(
- &self,
- cumulative_gas_used: u64,
- cumulative_da_size_used: u64,
- transaction_count: u64,
- ) -> bool {
+ pub fn should_break(&self, cumulative_gas_used: u64, cumulative_da_size_used: u64) -> bool {
// Check time limit
if self.start.elapsed() >= self.time_limit {
tracing::trace!(
@@ -202,19 +151,6 @@ impl PayloadBuildingBreaker {
return true;
}
- // Check transaction count limit if configured
- if let Some(max_count) = self.max_tx_per_block
- && transaction_count >= max_count
- {
- tracing::trace!(
- target: "payload_builder",
- transaction_count,
- max_tx_per_block = max_count,
- "transaction count limit reached"
- );
- return true;
- }
-
false
}
@@ -234,7 +170,6 @@ mod tests {
assert_eq!(config.gas_limit, None);
assert_eq!(config.time_limit, Duration::from_secs(1));
assert_eq!(config.max_da_block_size, None);
- assert_eq!(config.max_tx_per_block, None);
}
#[test]
@@ -242,32 +177,26 @@ mod tests {
let config = MorphBuilderConfig::default()
.with_gas_limit(20_000_000)
.with_time_limit(Duration::from_millis(500))
- .with_max_da_block_size(128 * 1024)
- .with_max_tx_per_block(1000);
+ .with_max_da_block_size(720 * 1024);
assert_eq!(config.gas_limit, Some(20_000_000));
assert_eq!(config.time_limit, Duration::from_millis(500));
- assert_eq!(config.max_da_block_size, Some(128 * 1024));
- assert_eq!(config.max_tx_per_block, Some(1000));
+ assert_eq!(config.max_da_block_size, Some(720 * 1024));
}
#[test]
fn test_breaker_should_break_on_time_limit() {
- let breaker = PayloadBuildingBreaker::new(
- Duration::from_millis(100),
- 30_000_000,
- Some(128 * 1024),
- None,
- );
+ let breaker =
+ PayloadBuildingBreaker::new(Duration::from_millis(100), 30_000_000, Some(720 * 1024));
// Should not break immediately
- assert!(!breaker.should_break(0, 0, 0));
+ assert!(!breaker.should_break(0, 0));
// Wait for time limit
std::thread::sleep(Duration::from_millis(150));
// Should break now
- assert!(breaker.should_break(0, 0, 0));
+ assert!(breaker.should_break(0, 0));
}
#[test]
@@ -276,13 +205,13 @@ mod tests {
// Threshold = 42000 - 21000 = 21000
// should_break returns true when cumulative_gas_used > threshold
let gas_limit = 2 * MIN_TRANSACTION_GAS;
- let breaker = PayloadBuildingBreaker::new(Duration::from_secs(10), gas_limit, None, None);
+ let breaker = PayloadBuildingBreaker::new(Duration::from_secs(10), gas_limit, None);
// At threshold (21000), should NOT break (21000 > 21000 is false)
- assert!(!breaker.should_break(MIN_TRANSACTION_GAS, 0, 0));
+ assert!(!breaker.should_break(MIN_TRANSACTION_GAS, 0));
// Just over threshold, should break (21001 > 21000 is true)
- assert!(breaker.should_break(MIN_TRANSACTION_GAS + 1, 0, 0));
+ assert!(breaker.should_break(MIN_TRANSACTION_GAS + 1, 0));
}
#[test]
@@ -290,49 +219,22 @@ mod tests {
// Set max_da = 2 * MIN_TRANSACTION_DATA_SIZE = 230
// Threshold = 230 - 115 = 115
let max_da_size = 2 * MIN_TRANSACTION_DATA_SIZE;
- let breaker = PayloadBuildingBreaker::new(
- Duration::from_secs(10),
- 30_000_000,
- Some(max_da_size),
- None,
- );
+ let breaker =
+ PayloadBuildingBreaker::new(Duration::from_secs(10), 30_000_000, Some(max_da_size));
// At threshold (115), should NOT break (115 > 115 is false)
- assert!(!breaker.should_break(0, MIN_TRANSACTION_DATA_SIZE, 0));
+ assert!(!breaker.should_break(0, MIN_TRANSACTION_DATA_SIZE));
// Just over threshold, should break (116 > 115 is true)
- assert!(breaker.should_break(0, MIN_TRANSACTION_DATA_SIZE + 1, 0));
- }
-
- #[test]
- fn test_breaker_should_break_on_tx_count_limit() {
- let breaker =
- PayloadBuildingBreaker::new(Duration::from_secs(10), 30_000_000, None, Some(100));
-
- // Below limit, should NOT break
- assert!(!breaker.should_break(0, 0, 99));
-
- // At limit, should break (>= comparison)
- assert!(breaker.should_break(0, 0, 100));
-
- // Above limit, should break
- assert!(breaker.should_break(0, 0, 101));
+ assert!(breaker.should_break(0, MIN_TRANSACTION_DATA_SIZE + 1));
}
#[test]
fn test_breaker_no_da_limit() {
- let breaker = PayloadBuildingBreaker::new(Duration::from_secs(10), 30_000_000, None, None);
+ let breaker = PayloadBuildingBreaker::new(Duration::from_secs(10), 30_000_000, None);
// Should not break even with huge DA size when no limit is set
- assert!(!breaker.should_break(0, u64::MAX, 0));
- }
-
- #[test]
- fn test_breaker_no_tx_count_limit() {
- let breaker = PayloadBuildingBreaker::new(Duration::from_secs(10), 30_000_000, None, None);
-
- // Should not break even with huge tx count when no limit is set
- assert!(!breaker.should_break(0, 0, u64::MAX));
+ assert!(!breaker.should_break(0, u64::MAX));
}
#[test]
@@ -345,9 +247,9 @@ mod tests {
// Threshold = 42000 - 21000 = 21000
// At threshold, should NOT break
- assert!(!breaker.should_break(MIN_TRANSACTION_GAS, 0, 0));
+ assert!(!breaker.should_break(MIN_TRANSACTION_GAS, 0));
// Just over, should break
- assert!(breaker.should_break(MIN_TRANSACTION_GAS + 1, 0, 0));
+ assert!(breaker.should_break(MIN_TRANSACTION_GAS + 1, 0));
}
#[test]
@@ -361,8 +263,8 @@ mod tests {
// Should use configured_limit, not block_gas_limit
// Threshold = 42000 - 21000 = 21000
- assert!(!breaker.should_break(MIN_TRANSACTION_GAS, 0, 0));
- assert!(breaker.should_break(MIN_TRANSACTION_GAS + 1, 0, 0));
+ assert!(!breaker.should_break(MIN_TRANSACTION_GAS, 0));
+ assert!(breaker.should_break(MIN_TRANSACTION_GAS + 1, 0));
// Verify it's not using block_gas_limit
// If using block_gas_limit, threshold would be ~29,979,000