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