Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ openssl rand -hex 32 > jwt.hex

| Flag | Default | Description |
|------|---------|-------------|
| `--morph.max-tx-payload-bytes` | 737280 (720 KiB) | Maximum L2 tx payload bytes per block (fits one uncompressed 6-blob batch) |
| `--morph.max-tx-payload-bytes` | Chain consensus limit | Optional lower sequencer packing limit; cannot exceed genesis `maxTxPayloadBytesPerBlock` |
| `--proofs-history` | false | Enable historical `eth_getProof` / `eth_getMultiProof` and proof-history accumulation |
| `--proofs-history.storage-path` | `<chain-datadir>/historical-proofs` | Override the proof MDBX directory |
| `--proofs-history.window` | 604800 | Number of canonical blocks retained (7 days at 1s/block) |
Expand Down
2 changes: 1 addition & 1 deletion crates/chainspec/res/genesis/hoodi.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"jadeForkTime": 1774418400,
"morph": {
"useZktrie": false,
"maxTxPayloadBytesPerBlock": 122880,
"maxTxPayloadBytesPerBlock": 737280,
"feeVaultAddress": "0x29107cb79ef8f69fe1587f77e283d47e84c5202f"
},
"scroll": {}
Expand Down
2 changes: 1 addition & 1 deletion crates/chainspec/res/genesis/mainnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"morph": {
"useZktrie": false,
"maxTxPerBlock": 100,
"maxTxPayloadBytesPerBlock": 122880,
"maxTxPayloadBytesPerBlock": 737280,
"feeVaultAddress": "0x530000000000000000000000000000000000000a"
}
},
Expand Down
5 changes: 3 additions & 2 deletions crates/chainspec/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ pub const MORPH_BASE_FEE: u64 = 1_000_000;
///
/// 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.
/// batch (`6 × 4096 × 31 = 761_856` usable bytes). The bundled mainnet and
/// Hoodi genesis files carry this value; it is also the default when a custom
/// genesis omits `maxTxPayloadBytesPerBlock`.
pub const MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK: u64 = 720 * 1024;

/// Default priority fee returned by `eth_maxPriorityFeePerGas` when the gas
Expand Down
56 changes: 45 additions & 11 deletions crates/chainspec/src/genesis.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Morph types for genesis data.

use crate::MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK;
use alloy_primitives::Address;
use alloy_serde::OtherFields;
use serde::{Deserialize, Serialize, de::Error as _};
Expand Down Expand Up @@ -81,21 +82,32 @@ 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)]
/// `maxTxPayloadBytesPerBlock` is a consensus rule shared with morph-geth:
/// import validation reads it from the genesis chain configuration, while the
/// sequencer CLI may choose a lower local packing limit.
#[derive(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<Address>,

/// Maximum EIP-2718 encoded L2 transaction bytes accepted in one block.
#[serde(default = "default_max_tx_payload_bytes_per_block")]
pub max_tx_payload_bytes_per_block: u64,
}

const fn default_max_tx_payload_bytes_per_block() -> u64 {
MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK
}

impl Default for MorphChainConfig {
fn default() -> Self {
Self {
fee_vault_address: None,
max_tx_payload_bytes_per_block: default_max_tx_payload_bytes_per_block(),
}
}
}

impl MorphChainConfig {
Expand All @@ -108,6 +120,11 @@ impl MorphChainConfig {
pub const fn is_fee_vault_enabled(&self) -> bool {
self.fee_vault_address.is_some()
}

/// Returns the maximum accepted L2 transaction payload bytes per block.
pub const fn max_tx_payload_bytes_per_block(&self) -> u64 {
self.max_tx_payload_bytes_per_block
}
}

impl TryFrom<&OtherFields> for MorphChainConfig {
Expand Down Expand Up @@ -174,6 +191,7 @@ mod tests {
config.fee_vault_address,
Some(address!("530000000000000000000000000000000000000a"))
);
assert_eq!(config.max_tx_payload_bytes_per_block, 122_880);
assert!(config.is_fee_vault_enabled());
}

Expand All @@ -182,10 +200,25 @@ 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,
MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK
);
}

#[test]
fn test_missing_payload_limit_uses_default() {
let others: OtherFields = serde_json::from_str(r#"{"morph": {}}"#).unwrap();
let config = MorphChainConfig::extract_from(&others).unwrap();

assert_eq!(
config.max_tx_payload_bytes_per_block,
MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK
);
}

#[test]
fn test_ignores_unused_packing_fields() {
fn test_ignores_unused_tx_count_field() {
let config_str = r#"
{
"morph": {
Expand All @@ -203,5 +236,6 @@ mod tests {
config.fee_vault_address,
Some(address!("530000000000000000000000000000000000000a"))
);
assert_eq!(config.max_tx_payload_bytes_per_block, 122_880);
}
}
22 changes: 21 additions & 1 deletion crates/chainspec/src/morph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ pub static MORPH_MAINNET: LazyLock<Arc<MorphChainSpec>> = LazyLock::new(|| {
#[cfg(test)]
mod tests {
use super::*;
use crate::{MORPH_MAINNET_CHAIN_ID, hardfork::MorphHardforks};
use crate::{
MORPH_MAINNET_CHAIN_ID, MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK, hardfork::MorphHardforks,
};
use alloy_primitives::address;
use reth_chainspec::EthChainSpec;

Expand All @@ -46,6 +48,24 @@ mod tests {
);
}

#[test]
fn test_morph_mainnet_payload_limit_matches_genesis() {
// The bundled genesis JSON is the single source of the consensus limit:
// it carries the same 720 KiB value as morph-geth's built-in config and
// the preset uses it unchanged.
let genesis: Genesis = serde_json::from_str(include_str!("../res/genesis/mainnet.json"))
.expect("mainnet genesis should parse");
let genesis_limit = crate::MorphGenesisInfo::extract_from(&genesis.config.extra_fields)
.expect("mainnet morph config should parse")
.morph_chain_info
.max_tx_payload_bytes_per_block;
assert_eq!(genesis_limit, MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK);
assert_eq!(
MORPH_MAINNET.max_tx_payload_bytes_per_block(),
genesis_limit
);
}

#[test]
fn test_morph_mainnet_hardforks() {
// Block-based hardforks: both Bernoulli and Curie active from block 0
Expand Down
19 changes: 18 additions & 1 deletion crates/chainspec/src/morph_hoodi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ pub static MORPH_HOODI: LazyLock<Arc<MorphChainSpec>> = LazyLock::new(|| {
#[cfg(test)]
mod tests {
use super::*;
use crate::{MORPH_HOODI_CHAIN_ID, hardfork::MorphHardforks};
use crate::{
MORPH_HOODI_CHAIN_ID, MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK, hardfork::MorphHardforks,
};
use alloy_primitives::address;
use reth_chainspec::EthChainSpec;

Expand All @@ -45,6 +47,21 @@ mod tests {
);
}

#[test]
fn test_morph_hoodi_payload_limit_matches_genesis() {
// The bundled genesis JSON is the single source of the consensus limit:
// it carries the same 720 KiB value as morph-geth's built-in config and
// the preset uses it unchanged.
let genesis: Genesis = serde_json::from_str(include_str!("../res/genesis/hoodi.json"))
.expect("hoodi genesis should parse");
let genesis_limit = crate::MorphGenesisInfo::extract_from(&genesis.config.extra_fields)
.expect("hoodi morph config should parse")
.morph_chain_info
.max_tx_payload_bytes_per_block;
assert_eq!(genesis_limit, MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK);
assert_eq!(MORPH_HOODI.max_tx_payload_bytes_per_block(), genesis_limit);
}

#[test]
fn test_morph_hoodi_hardforks() {
// Block-based hardforks should be active at block 0
Expand Down
11 changes: 11 additions & 0 deletions crates/chainspec/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,11 @@ impl MorphChainSpec {
pub fn fee_vault_address(&self) -> Option<Address> {
self.info.morph_chain_info.fee_vault_address
}

/// Returns the maximum accepted L2 transaction payload bytes per block.
pub const fn max_tx_payload_bytes_per_block(&self) -> u64 {
self.info.morph_chain_info.max_tx_payload_bytes_per_block()
}
}

impl From<ChainSpec> for MorphChainSpec {
Expand Down Expand Up @@ -710,6 +715,7 @@ mod tests {
chainspec.fee_vault_address(),
Some(address!("530000000000000000000000000000000000000a"))
);
assert_eq!(chainspec.max_tx_payload_bytes_per_block(), 122_880);
}

#[test]
Expand All @@ -720,6 +726,10 @@ mod tests {
let config = chainspec.chain_config();
// Test genesis includes morph config with fee vault address
assert!(config.is_fee_vault_enabled());
assert_eq!(
config.max_tx_payload_bytes_per_block(),
crate::MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK
);
}

#[test]
Expand Down Expand Up @@ -852,5 +862,6 @@ mod tests {
config.fee_vault_address,
Some(address!("530000000000000000000000000000000000000a"))
);
assert_eq!(config.max_tx_payload_bytes_per_block(), 122_880);
}
}
54 changes: 42 additions & 12 deletions crates/consensus/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
//! - 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`]
//! exceed the limit configured in the chain genesis
//!
//! ## Post-Execution Validation
//!
Expand All @@ -41,9 +41,7 @@ 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::{
MINIMUM_GAS_LIMIT, MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK, MorphChainSpec, MorphHardforks,
};
use morph_chainspec::{MINIMUM_GAS_LIMIT, MorphChainSpec, MorphHardforks};
use morph_primitives::{
Block, BlockBody, MorphHeader, MorphReceipt, MorphTxEnvelope,
transaction::morph_transaction::MORPH_TX_VERSION_1,
Expand Down Expand Up @@ -274,7 +272,7 @@ impl Consensus<Block> for MorphConsensus {
/// 3. **Transaction Root**: Must be valid
/// 4. **Withdrawals**: Must be empty (Morph L2 doesn't support withdrawals)
/// 5. **L2 Payload Size**: Encoded L2 txs (L1 messages excluded) must not
/// exceed [`MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK`]
/// exceed the limit configured in the chain genesis
/// 6. **L1 Messages**: Must be ordered correctly (sequential queue indices, L1 before L2)
fn validate_block_pre_execution(
&self,
Expand Down Expand Up @@ -310,7 +308,10 @@ impl Consensus<Block> for MorphConsensus {
}

// Matches go-ethereum's BlockValidator.ValidateBody() → IsValidBlockSize().
validate_l2_tx_payload_size(&block.body().transactions)?;
validate_l2_tx_payload_size(
&block.body().transactions,
self.chain_spec.max_tx_payload_bytes_per_block(),
)?;

// Validate MorphTx activation, version and field constraints.
// Matches go-ethereum's BlockValidator.ValidateBody() → ValidateMorphTxVersion().
Expand Down Expand Up @@ -489,16 +490,19 @@ fn l2_tx_payload_bytes(txs: &[MorphTxEnvelope]) -> u64 {
.fold(0, u64::saturating_add)
}

/// Rejects blocks whose L2 payload exceeds [`MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK`].
/// Rejects blocks whose L2 payload exceeds `max_tx_payload_bytes_per_block`.
///
/// Matches go-ethereum `MorphConfig.IsValidBlockSize` (`size <= limit`).
fn validate_l2_tx_payload_size(txs: &[MorphTxEnvelope]) -> Result<(), ConsensusError> {
fn validate_l2_tx_payload_size(
txs: &[MorphTxEnvelope],
max_tx_payload_bytes_per_block: u64,
) -> Result<(), ConsensusError> {
let size = l2_tx_payload_bytes(txs);
if size > MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK {
if size > max_tx_payload_bytes_per_block {
return Err(ConsensusError::other(
MorphConsensusError::InvalidBlockPayloadSize {
size,
limit: MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK,
limit: max_tx_payload_bytes_per_block,
},
));
}
Expand Down Expand Up @@ -772,9 +776,16 @@ mod tests {
use alloy_consensus::{Header, Signed};
use alloy_genesis::Genesis;
use alloy_primitives::{Address, B64, B256, Bytes, Signature, U256};
use morph_chainspec::MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK;
use morph_primitives::transaction::{MAX_MEMO_LENGTH, MORPH_TX_VERSION_0, TxL1Msg};

fn create_test_chainspec() -> Arc<MorphChainSpec> {
create_test_chainspec_with_payload_limit(MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK)
}

fn create_test_chainspec_with_payload_limit(
max_tx_payload_bytes_per_block: u64,
) -> Arc<MorphChainSpec> {
let genesis_json = serde_json::json!({
"config": {
"chainId": 1337,
Expand All @@ -794,7 +805,9 @@ mod tests {
"viridianTime": 0,
"emeraldTime": 0,
"jadeForkTime": 0,
"morph": {}
"morph": {
"maxTxPayloadBytesPerBlock": max_tx_payload_bytes_per_block
}
},
"alloc": {}
});
Expand Down Expand Up @@ -1940,7 +1953,7 @@ mod tests {
}

#[test]
fn test_validate_block_pre_execution_accepts_payload_at_limit() {
fn test_validate_block_pre_execution_accepts_payload_under_default_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()]);
Expand Down Expand Up @@ -1973,6 +1986,23 @@ mod tests {
);
}

#[test]
fn test_validate_block_pre_execution_uses_genesis_payload_limit() {
let tx = create_legacy_tx_with_input(Bytes::from(vec![0u8; 1024]));
let size = l2_tx_payload_bytes(std::slice::from_ref(&tx));
let block = create_sealed_block(0, vec![tx]);

let at_limit = MorphConsensus::new(create_test_chainspec_with_payload_limit(size));
assert!(at_limit.validate_block_pre_execution(&block).is_ok());

let below_limit = MorphConsensus::new(create_test_chainspec_with_payload_limit(size - 1));
let err = below_limit
.validate_block_pre_execution(&block)
.unwrap_err()
.to_string();
assert!(err.contains(&format!("exceeds limit {}", size - 1)));
}

#[test]
fn test_validate_block_pre_execution_ignores_large_l1_messages() {
let consensus = MorphConsensus::new(create_test_chainspec());
Expand Down
Loading