Skip to content

feat(l2): add native rollup L2 PoC — block production and L1 commitment via EXECUTE precompile#6248

Merged
avilagaston9 merged 10 commits into
feat/native-rollups-execute-precompilefrom
feat/native-rollups-l2-poc
Feb 27, 2026
Merged

feat(l2): add native rollup L2 PoC — block production and L1 commitment via EXECUTE precompile#6248
avilagaston9 merged 10 commits into
feat/native-rollups-execute-precompilefrom
feat/native-rollups-l2-poc

Conversation

@avilagaston9
Copy link
Copy Markdown
Contributor

Motivation

Phase 2 of the native rollups PoC. Phase 1 (the base branch) added the EXECUTE precompile on L1 and the NativeRollup.sol contract. This PR adds the L2 side: three GenServer actors that produce blocks matching the EXECUTE precompile's semantics and commit them to L1.

Description

Three GenServer actors implement the native rollup L2 lifecycle, communicating through Arc<Mutex<VecDeque>> shared queues:

  • NativeL1Watcher — polls L1 for L1MessageRecorded events from NativeRollup.sol and queues L1Message structs for the block producer
  • NativeBlockProducer — drains pending messages, computes the Merkle root (L1 anchor), writes the L1Anchor system slot, builds signed EIP-1559 relayer transactions calling L2Bridge.processL1Message(), executes them via LEVM with VMType::L1, computes post-state root and receipts root, stores the block, and pushes it to the committer queue
  • NativeL1Committer — pops produced blocks and submits them to L1 via advance() with the witness JSON, transactions RLP, and block params

The block producer mirrors the EXECUTE precompile exactly:

  • Failed transactions get empty receipt logs
  • Gas price computed from actual transaction fields: min(max_priority + base_fee, max_fee)
  • Pre-state root verified against parent header's state_root
  • initialize_block_header_hashes called before execution
  • L1Anchor written as system storage before transaction execution

All code gated behind the native-rollups feature flag — zero impact on the default build.

New files:

  • crates/l2/sequencer/native_rollup/types.rs — shared types
  • crates/l2/sequencer/native_rollup/l1_watcher.rs — L1 event watcher
  • crates/l2/sequencer/native_rollup/block_producer.rs — block producer (~700 lines)
  • crates/l2/sequencer/native_rollup/l1_committer.rs — L1 committer
  • crates/l2/sequencer/native_rollup/mod.rs — config and start_native_rollup_l2() entry point

Modified files:

  • crates/l2/Cargo.toml — added optional deps (k256, ethrex-crypto, rustc-hash) and native-rollups feature
  • cmd/ethrex/Cargo.toml — propagated native-rollups feature to ethrex-l2
  • crates/l2/l2.rs — re-exported NativeRollupConfig and start function
  • crates/l2/sequencer/mod.rs — added native_rollup module

How to Test

# Verify compilation with feature
cargo check -p ethrex-l2 --features native-rollups
cargo clippy -p ethrex-l2 --features native-rollups -- -D warnings

# Verify no impact without feature
cargo check -p ethrex-l2

# Full binary with both features
cargo check -p ethrex --features "l2,native-rollups"

Limitations (PoC)

  • Only relayer transactions included (no mempool/user transactions)
  • State trie changes not persisted back to the Store (single-block production works; multi-block requires trie persistence)
  • Not yet wired into the CLI startup path — start_native_rollup_l2() is exported and ready to be connected
  • No proof sender (not needed for native rollups — L1 re-executes)

compatible with the EXECUTE precompile and commit them to L1 via advance().

NativeL1Watcher polls L1 for L1MessageRecorded events from NativeRollup.sol
and queues them for the block producer. NativeBlockProducer drains pending L1
messages, computes the Merkle root, writes the L1Anchor system slot, builds
signed relayer transactions calling L2Bridge.processL1Message(), executes them
through LEVM with VMType::L1 semantics, and stores the resulting block in the
Store. NativeL1Committer picks produced blocks from the shared queue and
submits them to L1 via the advance() function with the witness, transactions
RLP, and block params.

The block producer mirrors the EXECUTE precompile exactly: failed transactions
get empty receipt logs, gas price is computed from actual transaction fields
(min(max_priority + base_fee, max_fee)), pre-state root is verified against
the parent header, and initialize_block_header_hashes is called before
execution.

All three actors communicate through Arc<Mutex<VecDeque>> shared queues and
are gated behind the native-rollups feature flag. The feature adds k256,
ethrex-crypto, and rustc-hash as optional dependencies to ethrex-l2.
@avilagaston9 avilagaston9 changed the title Add native rollup L2 PoC — block production and L1 commitment via EXECUTE precompile feat(l2): add native rollup L2 PoC — block production and L1 commitment via EXECUTE precompile Feb 23, 2026
@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

Review Summary

This PR introduces a native rollup L2 implementation with three main components: block producer, L1 committer, and L1 watcher. The code is well-structured and follows good patterns, but there are several security and correctness issues that need attention.

Critical Issues

1. Private Key Handling (Security)

  • File: crates/l2/sequencer/native_rollup/block_producer.rs
  • Lines: 81-82, 201-202, 331-332
  • Issue: Private keys are stored as raw byte arrays ([u8; 32]) and loaded directly into memory. This is a major security vulnerability for production use.
  • Fix: Use secure key management (e.g., encrypted keystore, HSM, or at minimum environment variables with proper secret management).

2. Integer Overflow in Message Count

  • File: crates/l2/sequencer/native_rollup/block_producer.rs
  • Line: 295
  • Issue: l1_messages.len().try_into() could overflow if there are more than 2^64-1 messages, though this is unlikely in practice.
  • Fix: Add explicit bounds checking or use saturating conversion.

3. Missing Input Validation

  • File: crates/l2/sequencer/native_rollup/block_producer.rs
  • Line: 285-290
  • Issue: No validation that l1_messages.len() matches message_hashes.len() after computing hashes.
  • Fix: Add assertion or return error if lengths mismatch.

Medium Issues

4. Error Handling in Async Context

  • File: crates/l2/sequencer/native_rollup/block_producer.rs
  • Lines: 154-159, 743-748
  • Issue: Errors in drain_l1_messages() are logged but silently ignored, potentially leading to lost messages.
  • Fix: Return the error instead of converting to empty Vec, or implement proper retry logic.

5. Gas Limit Hardcoding

  • File: crates/l2/sequencer/native_rollup/block_producer.rs
  • Line: 249
  • Issue: Gas limit for processL1Message is hardcoded to 200,000 without validation.
  • Fix: Make this configurable or add runtime estimation.

6. Race Condition in Nonce Management

  • File: crates/l2/sequencer/native_rollup/block_producer.rs
  • Lines: 321-330
  • Issue: Relayer nonce is fetched from store but there's no guarantee it won't change between fetch and transaction submission.
  • Fix: Implement proper nonce management with retry logic on nonce conflicts.

Minor Issues

7. Code Duplication

  • Files: Multiple files
  • Issue: compute_message_hash function is duplicated between block_producer.rs and test files.
  • Fix: Extract to a shared utility module.

8. Inefficient Data Structures

  • File: crates/l2/sequencer/native_rollup/block_producer.rs
  • Line: 343
  • Issue: Using Vec::with_capacity(messages.len()) when messages could be empty.
  • Fix: Use Vec::new() or check if empty first.

9. Missing Constants

  • File: crates/l2/sequencer/native_rollup/block_producer.rs
  • Lines: 247, 248
  • Issue: Magic numbers for gas prices (1 gwei) should be constants.
  • Fix: Define as const MAX_PRIORITY_FEE_PER_GAS: u64 = 1_000_000_000;

10. Incomplete Error Context

  • File: crates/l2/sequencer/native_rollup/l1_watcher.rs
  • Line: 168-172
  • Issue: Error messages lack context about which log failed to parse.
  • Fix: Include log index or transaction hash in error messages.

Recommendations

  1. Add comprehensive tests for edge cases in message parsing and Merkle proof generation
  2. Implement proper logging with structured fields for debugging
  3. Add metrics collection for monitoring L1/L2 synchronization
  4. Consider adding circuit breakers for L1 communication failures
  5. Document security assumptions clearly in the module documentation

The overall architecture is sound, but these security and correctness issues should be addressed before production use.


Automated review by Kimi (Moonshot AI) · custom prompt

@github-actions github-actions Bot added the L2 Rollup client label Feb 23, 2026
@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

Summary
Good structure for a PoC, but there are a few correctness and consensus‑critical gaps that will cause invalid blocks or failed message processing in non‑trivial scenarios.

Findings

  1. Block production will fail on non‑trivial state roots. You build the pre‑state trie from only “touched” addresses, then compare it to the parent header’s full state_root. This will almost always mismatch on a real chain. Either build the full trie from storage, or skip the check and set state_trie_root in the witness directly to the parent header root for the PoC.
    crates/l2/sequencer/native_rollup/block_producer.rs:299-339

  2. L1 message data is discarded, likely causing all non‑empty messages to revert. The watcher only captures dataHash, and the relayer txs always pass empty data. If processL1Message verifies dataHash == keccak(data), any message with non‑empty data will fail. You need to ingest the actual data or gate on dataHash == keccak("") and skip otherwise.
    crates/l2/sequencer/native_rollup/l1_watcher.rs:140-219
    crates/l2/sequencer/native_rollup/block_producer.rs:180-201

  3. No block gas limit enforcement. total_gas_used accumulates without checking block_gas_limit. That can yield blocks exceeding the configured limit (invalid under consensus rules). Add a pre‑execution check or stop inclusion once total_gas_used + tx.gas_limit > block_gas_limit, or ensure the VM enforces the limit per block.
    crates/l2/sequencer/native_rollup/block_producer.rs:405-474
    crates/l2/sequencer/native_rollup/block_producer.rs:510-520

  4. Reorg/duplication risk in L1 watcher. last_block_fetched is in‑memory only and there is no handling for log.removed. On restart or reorgs you can enqueue duplicate messages and produce inconsistent blocks. Persist last processed height and ignore removed logs (or wait for finality).
    crates/l2/sequencer/native_rollup/l1_watcher.rs:65-138
    crates/l2/sequencer/native_rollup/l1_watcher.rs:153-213

If you want, I can draft targeted fixes for Items 1–4.

Suggested next steps

  1. Decide whether to keep the strict pre‑state root check or make it PoC‑tolerant.
  2. Confirm the processL1Message ABI semantics for data/dataHash so we can fix message ingestion correctly.

Automated review by OpenAI Codex · custom prompt

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 23, 2026

Lines of code report

Total lines added: 947
Total lines removed: 0
Total lines changed: 947

Detailed view
+------------------------------------------------------------+-------+------+
| File                                                       | Lines | Diff |
+------------------------------------------------------------+-------+------+
| ethrex/crates/l2/l2.rs                                     | 13    | +2   |
+------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/mod.rs                          | 273   | +2   |
+------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/native_rollup/block_producer.rs | 389   | +389 |
+------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/native_rollup/l1_committer.rs   | 231   | +231 |
+------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/native_rollup/l1_watcher.rs     | 208   | +208 |
+------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/native_rollup/mod.rs            | 89    | +89  |
+------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/native_rollup/types.rs          | 26    | +26  |
+------------------------------------------------------------+-------+------+

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Based on my full review of the PR, here is the analysis:


PR Review: Native Rollup L2 PoC

This is a well-structured PoC that faithfully mirrors the EXECUTE precompile's logic. The module gating and actor isolation are clean. The following findings range from bugs-that-will-bite-immediately to security concerns.


Critical / Blocking

1. Partial state trie will always fail pre-state root verification

block_producer.rs, around line 390:

let mut touched_addresses = vec![coinbase, relayer, L2_BRIDGE, L1_ANCHOR];
for msg in &l1_messages {
    if !touched_addresses.contains(&msg.to) {
        touched_addresses.push(msg.to);
    }
}
// ...
let pre_state_root = state_trie.hash_no_commit();
if pre_state_root != parent_header.state_root {
    return Err(...);
}

A Merkle Patricia Trie root hash is computed over all accounts in the state, not a subset. Building a trie from a handful of known-touched addresses and then comparing it against the full world state root will always produce a mismatch on any non-trivial chain. The verification will unconditionally fail unless the only accounts in the entire chain happen to be the four pre-seeded ones (e.g., a freshly initialised devnet with no other accounts). Either populate the full state trie or drop the verification check for the PoC and add a comment explaining why.

2. State updates not persisted back to the Store

The account updates returned by gen_db.get_state_transitions() are applied only to the in-memory GuestProgramStateDb. The self.store is never updated. On the next block production call, get_account_state will return stale pre-execution balances and nonces. Concretely:

  • The relayer's nonce will be fetched as 0 on every block, so every relayer transaction will use nonce 0 and be rejected by the VM on block 2+.
  • Any ETH transferred in processL1Message won't be reflected in subsequent blocks.

This is acknowledged in the PR description as a limitation, but it means the code as written only produces a correct first block. A // FIXME: comment at the point of gen_db.get_state_transitions() would make this explicit and prevent future developers from missing it.

3. Private key exposed via Debug derive

block_producer.rs line 136, mod.rs line 1352:

#[derive(Clone, Debug)]
pub struct NativeBlockProducerConfig {
    ...
    pub relayer_key: [u8; 32],
}

Both NativeBlockProducerConfig and NativeRollupConfig derive Debug while containing the raw 32-byte private key. Any {:?} log of these structs (which is a natural debugging move) leaks the key. Use a newtype with a redacted Debug impl, or at minimum add #[debug(skip)] / a manual Debug that prints "[REDACTED]".


High Severity

4. Double-fetch of current_block_number introduces a potential inconsistency

block_producer.rs lines 324 and 356:

let current_block_number = self.store.get_latest_block_number().await?;
// ... 30+ lines later ...
let current_block_number_for_nonce = self.store.get_latest_block_number().await?;

Two separate async calls. In the single-actor model this is unlikely to race, but if a background process updates the store between calls the relayer nonce would be read from a different block than the one used as parent. Use current_block_number for both queries.

5. Integer overflow in max_fee construction

block_producer.rs line 297:

let max_priority_fee = 1_000_000_000u64; // 1 gwei
let max_fee = base_fee + max_priority_fee;  // unchecked u64 addition

If base_fee is close to u64::MAX (possible on a misconfigured or adversarial chain), this panics in debug or silently wraps in release. Use base_fee.saturating_add(max_priority_fee) or checked_add with a proper error.

The same pattern at line 521 (max_priority_fee + base_fee inside cmp::min) shares the risk.

6. last_block_fetched not persisted — duplicate message replay on restart

l1_watcher.rs line 1104:

last_block_fetched: U256::zero(),

Every time the process restarts, the watcher re-scans from block 1. Any L1 messages already included in prior L2 blocks will be re-queued and re-executed, leading to duplicate transactions with failing nonces or duplicate fund transfers. The last processed block must be persisted (to the Store or a file) and restored on startup.

7. Silent gas price fallback in committer

l1_committer.rs line 969:

let gas_price = self.eth_client.get_gas_price_with_extra(20)
    .await?
    .try_into()
    .unwrap_or(20_000_000_000u64);

If the actual gas price exceeds u64::MAX the try_into fails and the call proceeds silently with a hardcoded 20 Gwei. During L1 congestion this guarantees the transaction will never be included. At minimum log an error before falling back; ideally propagate it.

8. Unbounded ProducedBlocks queue

types.rs: ProducedBlocks = Arc<Mutex<VecDeque<ProducedBlockInfo>>>.

If the L1 committer falls behind (RPC down, L1 congestion), the block producer keeps appending to the queue indefinitely. Each ProducedBlockInfo contains witness_json: Vec<u8> and transactions_rlp: Vec<u8> which can be large. Add a maximum queue depth and stall block production (or drop) when the limit is reached.


Medium

9. Dead code: unused code-hash bindings

block_producer.rs lines 385–386:

let _bridge_code_hash = H256(keccak_hash(&self.bridge_runtime));
let _anchor_code_hash = H256(keccak_hash(&self.anchor_runtime));

These are computed and immediately discarded. Remove them or put them to use.

10. l1_message_recorded_topic() recomputed on every poll

l1_watcher.rs line 1064: the function is called inside poll_l1_messages on every tick. Use once_cell::sync::Lazy or std::sync::LazyLock to compute the keccak hash once:

static L1_MESSAGE_RECORDED_TOPIC: LazyLock<H256> = LazyLock::new(|| {
    keccak(b"L1MessageRecorded(address,address,uint256,uint256,bytes32,uint256)")
});

11. relayer_address() and build_relayer_transactions() both independently parse the signing key

The SigningKey is constructed twice per block: once in relayer_address() and once in build_relayer_transactions(). Derive the address once at construction time and cache it, or pass the SigningKey through instead of re-parsing the byte array.

12. touched_addresses uses O(n) linear search

if !touched_addresses.contains(&msg.to) {
    touched_addresses.push(msg.to);
}

Use a HashSet<Address> for dedup tracking and collect into a Vec afterwards. Negligible at small counts but semantically clearer.

13. Two separate Mutex lock acquisitions for consecutive state operations

block_producer.rs around lines 580–594: apply_account_updates and state_trie_root() each acquire and release the lock separately. Combine them into a single lock scope to make the atomicity explicit and avoid a potential interleave if the struct ever becomes multi-threaded.


Minor / Style

14. L1Message.data_hash vs. actual message data

The processL1Message calldata always passes Bytes::new() as data (line ~281), even though the event carries a data_hash. The L1Bridge on L2 presumably validates the hash; passing empty bytes will fail that check for any message whose data is non-empty. This is a known PoC limitation but should be called out with a // TODO: comment explaining that the actual data needs to be recovered (e.g., from L1 calldata/blob) rather than silently sending empty bytes.

15. parent_base_fee / parent_gas_limit / parent_gas_used fields in ProducedBlockInfo are stored but never read

types.rs lines 1505–1507. Unused fields add confusion. Remove them or add a comment explaining their intended consumer.

16. NativeL1Watcher starts scanning from block 0

last_block_fetched: U256::zero() means the watcher scans from genesis. If the contract was deployed at block N, blocks 1..N-1 are scanned wastefully on every startup. Accept the deployment block as a config parameter and use it as the initial last_block_fetched.


Correctness Notes (Positive)

  • EIP-1559 signing (sign_eip1559_tx) is correctly structured: 0x02 || RLP(chain_id, nonce, …) hashed and signed.
  • effective_gas_price = min(max_priority + base_fee, max_fee) matches the EIP-1559 spec and the EXECUTE precompile.
  • compute_message_hash correctly uses encodePacked semantics (20-byte addresses, 32-byte uints/hashes), matching what _recordL1Message in Solidity would produce.
  • Event log parsing correctly strips the 12-byte address padding from indexed topics.
  • Cumulative gas accounting in receipts is correct.
  • Feature-flagging is clean — zero impact on default builds is confirmed by the Cargo.toml structure.

Automated review by Claude (Anthropic) · custom prompt

avilagaston9 added 2 commits February 24, 2026 13:46
…r.rs

pattern (create_payload, PayloadBuildContext, apply_plain_transaction,
finalize_payload, store_block, apply_fork_choice) instead of manual LEVM
execution. Relayer txs for L1 messages are built as HeadTransactions and
executed first in the fill_transactions loop before mempool txs, ensuring
L1 messages always get priority. If a relayer tx fails any validation
(gas, nonce, replay protection, execution), the block producer returns an
error immediately. The committer now fetches blocks from the Store and
calls generate_witness_for_blocks instead of receiving pre-computed
witnesses, and counts relayer txs directly from the block body instead of
using a shared L1MessagesCounts map. Removes ProducedBlocks,
NativeBlockWitness, and L1MessagesCounts shared state — only
PendingL1Messages remains for watcher-to-producer communication.
the Merkle root in L1Anchor before execution.

take_l1_messages_for_block replaces drain_l1_messages: pops messages one
by one from the shared queue, summing each message's gas_limit. Once the
next message would exceed block_gas_limit the remaining messages stay in
the queue for the next block. The relayer tx gas_limit now uses the
message's own gas_limit instead of a hardcoded 200k constant.

anchor_l1_messages writes the L1 messages Merkle root to the L1Anchor
predeploy's storage slot 0 in the VM cache before any transaction
executes, mirroring what the EXECUTE precompile does as a system
transaction. This lets L2Bridge.processL1Message verify individual
messages via Merkle proofs. The anchor runs every block — when there are
no messages it writes the zero hash, matching NativeRollup.sol's
_computeMerkleRoot(_, 0) == bytes32(0).

Also fixes a pre-existing bug: let is_relayer_tx was immutable but
reassigned inside the if-let arm (now let mut).
avilagaston9 pushed a commit that referenced this pull request Feb 24, 2026
…nces.

The standalone native rollups integration guide is removed since the
implementation details now live in the open PRs (#6186, #6248). The L2
roadmap is updated to reflect current progress: distributed proving is
done (#6158), SP1 Hypercube upgrade is in progress (#6188), native
rollups EXECUTE precompile and L2 PoC are in progress (#6186, #6248),
and custom contract errors are in progress (#6206).
@avilagaston9 avilagaston9 mentioned this pull request Feb 24, 2026
1 task
avilagaston9 added 7 commits February 25, 2026 11:45
…n test,

and fix base_fee_per_gas mismatch between EXECUTE precompile and L2 block producer.

Deployment: deploy_native_rollup_contracts() in deployer.rs handles the full
flow — builds L2 genesis with L2Bridge/L1Anchor predeploys, computes the genesis
state root via a temp in-memory store, deploys NativeRollup.sol to L1 with
CREATE2, funds the contract with 100 ETH for withdrawal claims, and writes the
contract address to .env. The build system (build_l2.rs) now compiles
NativeRollup.sol, L2Bridge.sol, and L1Anchor.sol from levm/contracts. CLI
routing in command.rs dispatches to native rollup deploy/init when the flag is set.

Withdrawal proof RPC: new eth_getWithdrawalProof endpoint in
native_withdrawal_proof.rs returns MPT account + storage proofs against the
committed L2 state root, enabling users to claim withdrawals on L1 via
claimWithdrawal() in NativeRollup.sol.

Base fee fix: NativeRollup.sol constructor was setting lastGasUsed to
blockGasLimit/2 (the EIP-1559 gas target), which caused the base fee to stay
at 1 gwei for block 1. The L2 block producer correctly computed 875M (12.5%
decrease from target) because the actual genesis has gas_used=0. Changed to
lastGasUsed = 0 so both paths agree.

Bridge integration test: native_rollup_bridge_roundtrip exercises the full
deposit → L2 withdrawal → proof generation → L1 claim flow end-to-end.
…nfo! to

debug!, and consolidating the committer's two info! per block into a single
message emitted after the L1 transaction is sent. The aggregate-level info!
messages (watcher "found N events", producer "produced block", committer
"committed block") remain as the only info-level output per cycle.
required_unless_present = "native_rollups" (via cfg_attr) to the 7
fields that had required_unless_present = "dev", so that running
`ethrex l2 --native-rollups` no longer demands --l1.bridge-address,
--block-producer.coinbase-address, committer keys, proposer address,
or proof-coordinator keys. The native rollup code path returns early
before these fields are ever read. Also adds id = "native_rollups"
to the --native-rollups flag for a stable clap argument ID.
demo to the integration test.

The L1MessageRecorded event in NativeRollup.sol now emits the raw
bytes data instead of bytes32 dataHash, so the L1 watcher can read
the actual calldata from the event log. The watcher's ABI decoder
is updated for the dynamic bytes encoding (offset + length + content),
and the block producer forwards msg.data to L2Bridge.processL1Message()
instead of passing empty bytes.

The integration test gains a Phase 6 that deploys a Counter contract
on L2, sends sendL1Message from L1 with the increment() selector as
calldata, and polls counter.count() until it confirms the relay landed.
Replace Makefile-based setup with direct binary commands to avoid stale
database issues. Fix rex CLI syntax throughout: use positional RPC_URL for
balance/block-number, -k for private key, wei for --value, hex without 0x
prefix for bytes args, drop unsupported return type annotations from call
signatures. Add Step 4 (Counter contract deploy on L2 + increment via L1
message) demonstrating L1→L2 calldata relay. Add rex CLI syntax quirks
reference note.
…sh demo guide.

The deployer's ETH transfer to NativeRollup.sol used gas_limit=21000 which is
only enough for EOA transfers — sending to a contract needs extra gas to run the
receive() dispatcher. Increased to 30000.

Changed default native rollup intervals so the committer keeps pace with the
block producer: block_time_ms 5000→10000, commit_interval_ms 10000→3000.

Updated the demo guide to add --no-monitor (required for the L2 to start without
the TUI monitor), and replaced the raw Counter bytecode deploy with
rex deploy --contract-path which compiles and deploys in one step.
the actor calls advance() on NativeRollup.sol, which re-executes the
block and advances L2 state on L1 — not just committing data.

Renames: l1_committer.rs → l1_advancer.rs, NativeL1Committer →
NativeL1Advancer, commit_next_block → advance_next_block,
CastMsg::Commit → CastMsg::Advance, commit_interval_ms →
advance_interval_ms, CLI flag --native-rollups.commit-interval →
--native-rollups.advance-interval, env var
ETHREX_NATIVE_ROLLUPS_COMMIT_INTERVAL →
ETHREX_NATIVE_ROLLUPS_ADVANCE_INTERVAL. Updates docs, logs, and test
assertions accordingly. Only native rollup code is affected; the
standard L2 committer is untouched.
@avilagaston9 avilagaston9 merged commit 88fe57c into feat/native-rollups-execute-precompile Feb 27, 2026
1 check passed
@avilagaston9 avilagaston9 deleted the feat/native-rollups-l2-poc branch February 27, 2026 18:01
@avilagaston9
Copy link
Copy Markdown
Contributor Author

Merged into #6186 (feat/native-rollups-execute-precompile) to keep all native rollup work in a single PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L2 Rollup client

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant