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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ openssl rand -hex 32 > jwt.hex
| `--morph.max-tx-per-block` | None (unlimited) | Maximum number of transactions per block |
| `--rpc.eth-proof-window` | 0 (disabled) | Max historical blocks for `eth_getProof` (up to 1209600) |

#### Upstream Reth Flags with Morph-Specific Behavior

| Flag | Default | Description |
|------|---------|-------------|
| `--builder.gaslimit` / `--miner.gaslimit` | None (copy parent header) | Sequencer target for the block header `gasLimit`. Each assembled block ramps toward it by at most ~1/1024 of the parent, as morph-geth's `--miner.gaslimit` does. Unset leaves the header copying the parent. Ignored when payload attributes carry an explicit `gasLimit` (derivation import). |

### Running Tests

```bash
Expand Down
14 changes: 14 additions & 0 deletions crates/chainspec/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ pub const L2_MESSAGE_QUEUE_ADDRESS: Address = address!("530000000000000000000000
/// This is slot 33, which stores the Merkle root for L2->L1 messages.
pub const L2_MESSAGE_QUEUE_WITHDRAW_TRIE_ROOT_SLOT: U256 = U256::from_limbs([33, 0, 0, 0]);

// =============================================================================
// Protocol Gas Constants
// =============================================================================

/// Lowest block `gasLimit` the protocol accepts.
///
/// Matches go-ethereum's `params.MinGasLimit` (`params/protocol_params.go`).
/// Both ends of block production read it, which is why it lives here rather than
/// in either crate alone: the sequencer clamps its `gasLimit` target to it before
/// ramping (`morph-payload-builder`), and header validation rejects anything below
/// it (`morph-consensus`). One definition keeps producer and validator from
/// drifting apart.
pub const MINIMUM_GAS_LIMIT: u64 = 5000;

#[cfg(test)]
mod tests {
use super::*;
Expand Down
5 changes: 1 addition & 4 deletions crates/consensus/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use crate::MorphConsensusError;
use alloy_consensus::{BlockHeader as _, EMPTY_OMMER_ROOT_HASH, TxReceipt};
use alloy_evm::block::BlockExecutionResult;
use alloy_primitives::{B256, Bloom};
use morph_chainspec::{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 All @@ -63,9 +63,6 @@ const MORPH_MAXIMUM_BASE_FEE: u64 = 10_000_000_000;
/// Maximum gas limit (2^63 - 1)
const MAX_GAS_LIMIT: u64 = 0x7fffffffffffffff;

/// Minimum gas limit allowed for transactions.
const MINIMUM_GAS_LIMIT: u64 = 5000;

/// The bound divisor of the gas limit, used in update calculations.
const GAS_LIMIT_BOUND_DIVISOR: u64 = 1024;

Expand Down
15 changes: 13 additions & 2 deletions crates/node/src/components/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use morph_evm::MorphEvmConfig;
use morph_payload_builder::{MorphBuilderConfig, MorphPayloadBuilder};
use reth_node_api::FullNodeTypes;
use reth_node_builder::{BuilderContext, components::PayloadBuilderBuilder};
use reth_node_core::cli::config::PayloadBuilderConfig;
use reth_tracing::tracing::info;
use reth_transaction_pool::blobstore::InMemoryBlobStore;

Expand Down Expand Up @@ -60,10 +61,20 @@ where
pool: morph_txpool::MorphTransactionPool<Node::Provider, InMemoryBlobStore>,
evm_config: MorphEvmConfig,
) -> eyre::Result<Self::PayloadBuilder> {
let mut config = self.config;
let desired_gas_limit = ctx.payload_builder_config().gas_limit();
if let Some(desired) = desired_gas_limit {
config = config.with_desired_gas_limit(desired);
}

let builder =
MorphPayloadBuilder::with_config(pool, evm_config, ctx.provider().clone(), self.config);
MorphPayloadBuilder::with_config(pool, evm_config, ctx.provider().clone(), config);

info!(target: "morph::node", "Payload builder initialized");
info!(
target: "morph::node",
?desired_gas_limit,
"Payload builder initialized"
);

Ok(builder)
}
Expand Down
23 changes: 20 additions & 3 deletions crates/node/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ pub struct TestNodeBuilder {
schedule: HardforkSchedule,
num_nodes: usize,
is_dev: bool,
desired_gas_limit: Option<u64>,
}

impl Default for TestNodeBuilder {
Expand All @@ -213,6 +214,7 @@ impl TestNodeBuilder {
schedule: HardforkSchedule::AllActive,
num_nodes: 1,
is_dev: false,
desired_gas_limit: None,
}
}

Expand Down Expand Up @@ -260,6 +262,14 @@ impl TestNodeBuilder {
self
}

/// Set the sequencer's header `gasLimit` target, as `--builder.gaslimit` would.
///
/// Assembled blocks then ramp toward this value instead of copying the parent.
pub fn with_desired_gas_limit(mut self, desired_gas_limit: u64) -> Self {
self.desired_gas_limit = Some(desired_gas_limit);
self
}

/// Build and launch the configured nodes.
///
/// Returns the node handles and a wallet derived from
Expand All @@ -271,13 +281,20 @@ impl TestNodeBuilder {
let genesis: Genesis = serde_json::from_value(self.genesis_json)?;
let chain_spec = morph_chainspec::MorphChainSpec::from_genesis(genesis);

reth_e2e_test_utils::setup_engine(
// Built via `E2ETestSetupBuilder` rather than `setup_engine` so the node config
// can carry `--builder.gaslimit`, which `setup_engine` gives no way to set.
let is_dev = self.is_dev;
let desired_gas_limit = self.desired_gas_limit;
reth_e2e_test_utils::E2ETestSetupBuilder::<MorphNode, _>::new(
self.num_nodes,
Arc::new(chain_spec),
self.is_dev,
Default::default(),
morph_payload_attributes,
)
.with_node_config_modifier(move |mut config| {
config.builder.gas_limit = desired_gas_limit;
config.set_dev(is_dev)
})
.build()
.await
}
}
Expand Down
70 changes: 70 additions & 0 deletions crates/node/tests/it/block_building.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,76 @@ async fn l1_messages_precede_l2_transactions() -> eyre::Result<()> {
Ok(())
}

/// Genesis `gasLimit` in `tests/assets/test-genesis.json`, matching mainnet.
const GENESIS_GAS_LIMIT: u64 = 30_000_000;

/// With `--builder.gaslimit` set, each assembled header must step toward the target
/// by go-ethereum's `CalcGasLimit` delta, and every such block must still import.
///
/// This covers the wiring, not just the arithmetic: the target has to travel from the
/// node config through `MorphBuilderConfig` into the header, and `advance_empty_block`
/// runs `submit_payload` + `update_forkchoice`, so a header the consensus rules reject
/// fails the test rather than passing silently.
#[tokio::test(flavor = "multi_thread")]
async fn header_gas_limit_ramps_toward_builder_target() -> eyre::Result<()> {
reth_tracing::init_test_tracing();

let target = 60_000_000u64;
let (mut nodes, _wallet) = TestNodeBuilder::new()
.with_desired_gas_limit(target)
.build()
.await?;
let mut node = nodes.pop().unwrap();

let mut parent_gas_limit = GENESIS_GAS_LIMIT;
for block_number in 1..=3u64 {
let payload = advance_empty_block(&mut node).await?;
let header = payload.block().header();
assert_eq!(header.inner.number, block_number);

// Derived from geth's rule rather than from the implementation: the step is
// `parent/1024 - 1`, one below the diff header validation starts rejecting.
let expected = parent_gas_limit + parent_gas_limit / 1024 - 1;
assert_eq!(
header.inner.gas_limit, expected,
"block {block_number} should ramp from {parent_gas_limit} to {expected}"
);
assert!(
header.inner.gas_limit < target,
"ramp must not overshoot the target in a single block"
);

parent_gas_limit = header.inner.gas_limit;
}

// First step from mainnet's 30M, pinned so a changed bound divisor is caught.
assert_eq!(GENESIS_GAS_LIMIT + GENESIS_GAS_LIMIT / 1024 - 1, 30_029_295);

Ok(())
}

/// Without `--builder.gaslimit`, headers keep copying the parent `gasLimit`. This is
/// the default every deployment runs, so it gets its own guard.
#[tokio::test(flavor = "multi_thread")]
async fn header_gas_limit_copies_parent_without_builder_target() -> eyre::Result<()> {
reth_tracing::init_test_tracing();

let (mut nodes, _wallet) = TestNodeBuilder::new().build().await?;
let mut node = nodes.pop().unwrap();

for block_number in 1..=3u64 {
let payload = advance_empty_block(&mut node).await?;
let header = payload.block().header();
assert_eq!(header.inner.number, block_number);
assert_eq!(
header.inner.gas_limit, GENESIS_GAS_LIMIT,
"gasLimit must not drift when no target is configured"
);
}

Ok(())
}

/// Multiple L1 messages with strictly sequential queue indices in one block.
#[tokio::test(flavor = "multi_thread")]
async fn multiple_l1_messages_sequential_queue_indices() -> eyre::Result<()> {
Expand Down
4 changes: 3 additions & 1 deletion crates/payload/builder/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,9 @@ where
timestamp: attributes.timestamp,
suggested_fee_recipient: attributes.suggested_fee_recipient,
prev_randao: attributes.prev_randao,
gas_limit: attributes.gas_limit.unwrap_or(ctx.parent().gas_limit()),
gas_limit: ctx
.builder_config
.next_header_gas_limit(ctx.parent().gas_limit(), attributes.gas_limit),
withdrawals: Some(attributes.withdrawals.clone()),
parent_beacon_block_root: attributes.parent_beacon_block_root,
extra_data: Default::default(),
Expand Down
Loading