Skip to content

feat(levm): native rollups EXECUTE precompile + L2 PoC (EIP-8079)#6186

Closed
avilagaston9 wants to merge 90 commits into
mainfrom
feat/native-rollups-execute-precompile
Closed

feat(levm): native rollups EXECUTE precompile + L2 PoC (EIP-8079)#6186
avilagaston9 wants to merge 90 commits into
mainfrom
feat/native-rollups-execute-precompile

Conversation

@avilagaston9

@avilagaston9 avilagaston9 commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

Motivation

EIP-8079 proposes "native rollups" where L1 verifies L2 state transitions by re-executing them via an EXECUTE precompile, replacing zkVM/fraud proofs with direct execution. This PR implements a Phase 1 PoC: the EXECUTE precompile on L1 and a working L2 that settles blocks through it.

Description

EXECUTE precompile (execute_precompile.rs) — implements the apply_body variant from the native rollups spec. Receives 15 ABI-encoded parameters (13 static block fields + 2 dynamic byte arrays for RLP transactions and JSON execution witness), re-executes the L2 block using LEVM, verifies state root and receipts root, and returns abi.encode(postStateRoot, blockNumber, gasUsed, burnedFees, baseFeePerGas) (160 bytes). Registered at address 0x0101 behind the native-rollups feature flag.

Contracts:

  • NativeRollup.sol — L1 contract managing L2 state: tracks state root, block number, parent gas parameters on-chain; records L1 messages via sendL1Message(); calls EXECUTE via advance(); supports claimWithdrawal() with MPT state root proofs and configurable finality delay
  • L2Bridge.sol — L2 predeploy at 0x...fffd handling L1→L2 message processing (Merkle proof verification) and L2→L1 withdrawals (state root provable)
  • L1Anchor.sol — L2 predeploy at 0x...fffe storing L1 messages Merkle root anchored by EXECUTE
  • MPTProof.sol — Solidity library for MPT trie traversal and account/storage proof verification

L2 GenServer actors:

  • NativeL1Watcher — polls L1 for L1MessageRecorded events, queues messages
  • NativeBlockProducer — produces L2 blocks with relayer transactions for L1 messages + mempool transactions
  • NativeL1Advancer — submits produced blocks to L1 via advance()

Deployer and CLI--native-rollups flag generates L2 genesis with predeploys, deploys NativeRollup.sol to L1, and boots the L2 node.

How to Test

The integration test requires a live L1 + L2 (see docs/l2/deployment/native_rollups.md for full setup):

# Terminal 1: start L1
make -C crates/l2 init-l1

# Terminal 2: deploy contracts and start L2
make -C crates/l2 native-rollup

# Terminal 3: run integration test
cargo test -p ethrex-test --features native-rollups -- l2::native_rollup --nocapture

Checklist

  • Updated STORE_SCHEMA_VERSION (crates/storage/lib.rs) if the PR includes breaking changes to the Store requiring a re-sync.

Add a minimal EXECUTE precompile to LEVM that verifies L2 state
transitions by re-executing them inside the L1 EVM. This is a Phase 1
proof-of-concept behind the `native-rollups` feature flag.

The precompile receives an execution witness, blocks, and deposit data,
re-executes the blocks using LEVM, applies deposits via a simple anchor
mechanism, and verifies the resulting state root matches the expected
post-state root.

New files:
- execute_precompile.rs: core precompile logic (execute_inner, block
  execution, deposit application, gas price calculation, withdrawals)
- guest_program_state_db.rs: thin Database adapter bridging
  GuestProgramState to LEVM's Database trait
- tests/native_rollups.rs: integration test demonstrating a transfer
  (Alice sends 1 ETH to Bob) + deposit (Charlie receives 5 ETH)
- docs/vm/levm/native_rollups.md: architecture and usage documentation

The precompile is registered at address 0x0101 and dispatched at runtime
before the standard const precompile table. Calldata deserialization is
stubbed out — tests call execute_inner() directly with structured input.
@github-actions github-actions Bot added the levm Lambda EVM implementation label Feb 11, 2026
@github-actions

Copy link
Copy Markdown

🤖 Kimi Code Review

Review Summary

This PR implements a proof-of-concept for the Native Rollups EXECUTE precompile (EIP-8079). The implementation is well-structured and follows good practices, but there are several areas that need attention.

Critical Issues

1. Security: Mutex Poisoning Handling (Critical)

File: crates/vm/levm/src/db/guest_program_state_db.rs
Lines: 34-82

All database methods use Mutex::lock() and convert PoisonError to a custom database error. However, this could lead to inconsistent state if the mutex is poisoned. Consider using Mutex::lock().unwrap_or_else(|e| e.into_inner()) to recover from poisoned state, or document why poisoning should be treated as fatal.

2. Overflow Handling (High)

File: crates/vm/levm/src/execute_precompile.rs
Lines: 195-198, 201-204, 250-253, 290-294

Multiple unchecked arithmetic operations could overflow:

  • cumulative_gas_used and block_gas_used additions
  • amount_in_wei multiplication in process_withdrawals
  • Balance additions in apply_deposit

Use checked_add consistently and handle overflow errors properly.

3. State Consistency Risk (High)

File: crates/vm/levm/src/execute_precompile.rs
Lines: 140-150

The state transitions are applied back to the GuestProgramState after each block, but there's no atomicity guarantee. If execution fails mid-way, the state could be partially updated. Consider batching all state updates at the end.

Code Quality Issues

4. Error Handling Inconsistency (Medium)

File: crates/vm/levm/src/execute_precompile.rs
Lines: 45-50, 58-63

The custom_err helper creates VMError::Internal, but some errors (like state root mismatches) should probably be VMError::PrecompileError to distinguish between internal failures and verification failures.

5. Gas Cost Underestimation (Medium)

File: crates/vm/levm/src/execute_precompile.rs
Line: 45

EXECUTE_GAS_COST = 100_000 is likely too low for re-executing entire blocks. This should be dynamically calculated based on the actual work performed.

6. Unnecessary Mutex Usage (Low)

File: crates/vm/levm/src/db/guest_program_state_db.rs

Consider if GuestProgramState could be modified to use interior mutability (e.g., RefCell) instead of Mutex, since this is single-threaded execution.

Minor Issues

7. Code Duplication

File: crates/vm/levm/src/execute_precompile.rs
Lines: 58-63

The custom_err function is simple enough to inline, or could be a macro to reduce boilerplate.

8. Feature Flag Inconsistency

File: crates/vm/levm/src/precompiles.rs
Lines: 259, 346-350

The EXECUTE precompile is marked as active since Paris, but this seems inconsistent with it being a new EIP. Consider using a more appropriate fork.

9. Documentation

File: docs/vm/levm/native_rollups.md

The documentation is excellent, but should mention the security considerations around mutex poisoning and overflow handling.

Recommendations

  1. Fix overflow handling throughout the codebase
  2. Improve error categorization to distinguish between verification failures and internal errors
  3. Add proper gas metering based on actual execution cost
  4. Consider atomic state updates to prevent partial failures
  5. Add fuzz testing for the arithmetic operations
  6. Document security assumptions around mutex usage

The implementation is solid for a PoC, but these issues should be addressed before production use.


Automated review by Kimi (Moonshot AI) · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

Here are the key findings from the diff, ordered by severity, with file/line references.

Findings

  • Consensus/validation gap: execute_block does not validate transactions_root, receipts_root, withdrawals_root, or ommers against the block body. Only gas_used is checked. This allows malformed blocks to pass as long as state root matches, which is not sufficient for L1/L2 block validity. Consider validating all header roots before executing or after executing with computed roots. crates/vm/levm/src/execute_precompile.rs:118
  • Incorrect missing-account semantics: GuestProgramStateDb::get_account_state errors on missing accounts. EVM semantics treat missing accounts as empty; this can cause valid executions to fail unless the witness includes explicit empty accounts. The test works around this by inserting Bob/Charlie, which hides the issue. Return AccountState::default() instead of error. crates/vm/levm/src/db/guest_program_state_db.rs:25
  • Precompile always reverts: execute_precompile charges gas and then always returns an error because calldata parsing is unimplemented. Once native-rollups is enabled, calls to address 0x0101 will always revert and consume gas, which is surprising for a “registered” precompile. Consider gating is_precompile/dispatch behind a runtime flag or returning a structured “not implemented” success code only in tests. crates/vm/levm/src/execute_precompile.rs:40, crates/vm/levm/src/precompiles.rs:333
  • Gas accounting ambiguity: Both cumulative_gas_used (using report.gas_spent) and block_gas_used (using report.gas_used) are tracked, but only block_gas_used is validated and cumulative_gas_used is unused. If gas_used vs gas_spent differ (refunds), ensure the correct one matches header.gas_used and remove the unused accumulator to avoid future mistakes. crates/vm/levm/src/execute_precompile.rs:154

Lower‑risk / maintainability

  • is_precompile returns true for EXECUTE regardless of fork; if this should be fork‑gated (or EIP‑specific), include an explicit fork check to avoid accidental activation across chains. crates/vm/levm/src/precompiles.rs:302

If you want, I can suggest concrete patch changes for the missing‑account behavior and header root validation.


Automated review by OpenAI Codex · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Based on my review of PR #6186, here is my comprehensive code review:


PR Review: EXECUTE Precompile for Native Rollups (EIP-8079 PoC)

Summary

This PR implements a proof-of-concept EXECUTE precompile for Native Rollups (EIP-8079) that verifies L2 state transitions by re-executing them inside L1's EVM. The implementation is well-structured for a PoC and demonstrates the core concept effectively.

Critical Issues

1. Mutex Poisoning Not Properly Handled ⚠️ HIGH PRIORITY

File: crates/vm/levm/src/execute_precompile.rs:126-131

The code locks the mutex multiple times in a loop without recovering from poison errors:

db.state
    .lock()
    .map_err(|e| custom_err(format!("Lock poisoned: {e}")))?
    .apply_account_updates(&account_updates)

Issue: If apply_account_updates() panics, the mutex becomes poisoned and all subsequent blocks will fail. This breaks atomicity across block execution.

Recommendation: Either:

  • Use a single lock acquisition for all blocks (refactor to avoid multiple lock/unlock cycles)
  • Or use PoisonError::into_inner() to recover the mutex guard and handle poisoning gracefully
  • Consider if Mutex is the right choice here - RefCell might be more appropriate since execution should be single-threaded

2. State Root Verification Race Condition ⚠️ MEDIUM PRIORITY

File: crates/vm/levm/src/execute_precompile.rs:133-139

let final_root = db
    .state
    .lock()
    .map_err(|e| custom_err(format!("Lock poisoned: {e}")))?
    .state_trie_root()
    .map_err(|e| custom_err(format!("Failed to compute final state root: {e}")))?;

Issue: The state is accessed through Arc<GuestProgramStateDb> which is shared. Between line 131 (releasing lock) and line 134 (acquiring lock again), there's no guarantee state hasn't been modified.

Recommendation: Hold the lock for the entire verification sequence or ensure exclusive access to the state during execution.

3. Gas Validation Semantics Unclear 🔍 MEDIUM PRIORITY

File: crates/vm/levm/src/execute_precompile.rs:217-219

ethrex_common::validate_gas_used(block_gas_used, &block.header)
    .map_err(|e| custom_err(format!("Gas validation failed: {e}")))?;

Issue: The precompile validates that block_gas_used matches the block header's gas_used field. However:

  • For L2 verification on L1, should the precompile re-calculate gas or trust the header?
  • What happens if the provided block header has an incorrect gas_used value?

Recommendation: Add a comment clarifying whether this is validating the L2 block's claimed gas usage or just checking internal consistency.

Security Concerns

4. No Gas Limit Enforcement on Precompile Execution ⚠️ HIGH PRIORITY

File: crates/vm/levm/src/execute_precompile.rs:59

increase_precompile_consumed_gas(EXECUTE_GAS_COST, gas_remaining)?;

Issue: The precompile charges a flat 100k gas upfront but then executes entire blocks with potentially millions of gas worth of transactions. A malicious actor could submit a block that takes very long to execute (DoS attack) for only 100k gas.

Recommendation: For production:

  • Implement metered gas consumption during block execution
  • Consider a gas-per-transaction multiplier
  • Set maximum block gas limits for the precompile
  • Document this as a known limitation in the PoC

5. Account State Not Found Returns Generic Error 🔍 MEDIUM PRIORITY

File: crates/vm/levm/src/db/guest_program_state_db.rs:36-38

.ok_or(DatabaseError::Custom(format!(
    "Account {address} not found in witness"
)))

Issue: Missing accounts cause immediate failure. In standard EVM execution, accessing a non-existent account should return an empty/default account, not an error.

Recommendation: Return Ok(AccountState::default()) for missing accounts instead of erroring. This matches EVM semantics where all addresses implicitly exist as empty accounts.

6. Deposit Balance Overflow Uses String Error 🔍 LOW PRIORITY

File: crates/vm/levm/src/execute_precompile.rs:295

.ok_or("Deposit would overflow balance")?;

Issue: Inconsistent error handling - other overflows use VMError::Internal(InternalError::Overflow).

Recommendation: Use consistent error types: ok_or(Box::new(VMError::Internal(InternalError::Overflow)) as Box<dyn std::error::Error>)?

Code Quality Issues

7. Inefficient State Trie Hashing on Deposit Application 🔍 LOW PRIORITY

File: crates/vm/levm/src/execute_precompile.rs:103-110

for deposit in &deposits {
    apply_deposit(&mut guest_state, deposit).map_err(|e| {
        custom_err(format!(
            "Failed to apply deposit to {}: {e}",
            deposit.address
        ))
    })?;
}

Issue: Each apply_deposit call modifies the state trie, potentially triggering trie recalculations. The trie is then hashed again at line 94 (initial root) and 134 (final root).

Recommendation: Batch trie operations if possible, or document that trie caching prevents performance issues.

8. Unused Variable cumulative_gas_used ⚠️ MEDIUM PRIORITY

File: crates/vm/levm/src/execute_precompile.rs:165, 204-206

let mut cumulative_gas_used = 0_u64;
// ...
cumulative_gas_used = cumulative_gas_used
    .checked_add(report.gas_spent)
    .ok_or(VMError::Internal(InternalError::Overflow))?;

Issue: The variable is tracked but never used. This could indicate:

  • Missing logic that should validate cumulative gas
  • Confusion between gas_spent vs gas_used semantics

Recommendation: Either use this value for validation or remove it. If keeping it, document why both cumulative_gas_used and block_gas_used are needed.

9. execute_block Mutates GeneralizedDatabase But Doesn't Return New State 🔍 MEDIUM PRIORITY

File: crates/vm/levm/src/execute_precompile.rs:152-222

fn execute_block(block: &Block, db: &mut GeneralizedDatabase) -> Result<(), VMError>

Issue: The function modifies GeneralizedDatabase state, but the caller also needs to extract state transitions via get_state_transitions() and manually apply them. This is error-prone.

Recommendation: Consider returning the AccountUpdates directly from execute_block to make the data flow more explicit.

Rust Best Practices

10. Unnecessary Clone in Arc Pattern 🔍 LOW PRIORITY

File: crates/vm/levm/src/execute_precompile.rs:116

let db_dyn: Arc<dyn crate::db::Database> = db.clone();

Issue: Creating a trait object Arc from a concrete Arc requires clarification. Is this intentional coercion?

Recommendation: Add a comment explaining why db (which is Arc<GuestProgramStateDb>) is cloned and cast to Arc<dyn Database>, or use Arc::clone(&db) for explicitness.

11. Custom Error Helper Should Use impl Into<String> 🔍 LOW PRIORITY

File: crates/vm/levm/src/execute_precompile.rs:68-70

fn custom_err(msg: String) -> VMError {
    VMError::Internal(InternalError::Custom(msg))
}

Recommendation: Make it more ergonomic:

fn custom_err(msg: impl Into<String>) -> VMError {
    VMError::Internal(InternalError::Custom(msg.into()))
}

This would allow passing both String and &str without requiring .to_string() at call sites.

12. Magic Number for Success Return Value 🔍 LOW PRIORITY

File: crates/vm/levm/src/execute_precompile.rs:148

Ok(Bytes::from(vec![0x01]))

Recommendation: Define a constant:

const EXECUTE_SUCCESS_BYTE: u8 = 0x01;
// ...
Ok(Bytes::from(vec![EXECUTE_SUCCESS_BYTE]))

Testing Concerns

13. Test Only Covers Happy Path 🔍 MEDIUM PRIORITY

File: crates/vm/levm/tests/native_rollups.rs

Issue: The test only validates successful execution. Missing test cases:

  • Invalid pre-state root (should fail early)
  • Invalid post-state root (should fail after execution)
  • Transaction that reverts
  • Out-of-gas transaction
  • Invalid signature
  • Withdrawal processing
  • Empty block
  • Multiple blocks
  • Deposit overflow scenarios

Recommendation: Add negative test cases and edge cases before merging.

Documentation

14. Missing Safety Documentation for Mutex 🔍 LOW PRIORITY

File: crates/vm/levm/src/db/guest_program_state_db.rs:17-19

/// Uses a `Mutex` for interior mutability because `GuestProgramState` methods
/// require `&mut self` (they lazily populate caches like `account_hashes_by_address`).

Recommendation: Document thread-safety guarantees. Since LEVM execution is single-threaded, clarify whether concurrent access is a concern or if RefCell would be more appropriate.

Minor Issues

  1. Inconsistent Fork Activation - execute_precompile.rs:266: EXECUTE is listed as active_since_fork: Paris but EIP-8079 is not part of any fork yet. Consider using a more appropriate marker or comment.

  2. Missing Blob Gas Validation - The code processes blob_gas_used and excess_blob_gas but doesn't validate blob transaction constraints for type-3 transactions.

  3. Code Format - execute_precompile.rs:15: Using std::cmp::min instead of importing min from the prelude is less idiomatic.

Positive Observations ✅

  • Good separation of concerns between the precompile logic, DB adapter, and registration
  • Comprehensive overflow checking with checked_add, checked_mul throughout
  • Good test documentation - the test clearly explains what it's doing
  • Feature flag isolation - properly gates experimental code
  • Clear error messages with context
  • Well-structured documentation in native_rollups.md

Recommendations Summary

Before merging:

  1. ✅ Fix mutex poisoning handling (Issue docs: add milestones #1)
  2. ✅ Clarify/fix state access race condition (Issue chore: create project structure #2)
  3. ✅ Document gas validation semantics (Issue build: add Github actions boilerplate #3)
  4. ✅ Document DoS risk from flat gas cost (Issue docs: update milestones. #4)
  5. ✅ Fix account not found behavior (Issue feat: add basic RPC api. #5)
  6. ✅ Add negative test cases (Issue Add jwt authentication to engine api #13)

Future improvements:

  • Implement proper gas metering
  • Add calldata deserialization
  • Consider using RefCell instead of Mutex if execution is single-threaded
  • Add more comprehensive test coverage

Overall, this is a solid PoC implementation that demonstrates the concept well. The main concerns are around thread safety, gas accounting, and test coverage.


Automated review by Claude (Anthropic) · custom prompt

@github-actions

github-actions Bot commented Feb 11, 2026

Copy link
Copy Markdown

Lines of code report

Total lines added: 2245
Total lines removed: 44
Total lines changed: 2289

Detailed view
+---------------------------------------------------------------+-------+------+
| File                                                          | Lines | Diff |
+---------------------------------------------------------------+-------+------+
| ethrex/cmd/ethrex/build_l2.rs                                 | 478   | +31  |
+---------------------------------------------------------------+-------+------+
| ethrex/cmd/ethrex/l2/command.rs                               | 669   | +22  |
+---------------------------------------------------------------+-------+------+
| ethrex/cmd/ethrex/l2/deployer.rs                              | 1833  | +312 |
+---------------------------------------------------------------+-------+------+
| ethrex/cmd/ethrex/l2/initializers.rs                          | 432   | +78  |
+---------------------------------------------------------------+-------+------+
| ethrex/cmd/ethrex/l2/mod.rs                                   | 12    | +2   |
+---------------------------------------------------------------+-------+------+
| ethrex/cmd/ethrex/l2/options.rs                               | 1232  | +128 |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/blockchain/blockchain.rs                        | 2512  | +28  |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/common/common.rs                                | 23    | +1   |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/common/merkle_tree.rs                           | 61    | +61  |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/common/src/merkle_tree.rs                    | 1     | -42  |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/common/src/messages.rs                       | 206   | +4   |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/l2.rs                                        | 13    | +2   |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/networking/rpc/l2/messages.rs                | 87    | -2   |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/networking/rpc/l2/mod.rs                     | 6     | +1   |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/networking/rpc/l2/native_withdrawal_proof.rs | 129   | +129 |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/networking/rpc/rpc.rs                        | 222   | +2   |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sdk/src/sdk.rs                               | 1234  | +6   |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/mod.rs                             | 274   | +2   |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/native_rollup/block_producer.rs    | 382   | +382 |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/native_rollup/l1_advancer.rs       | 218   | +218 |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/native_rollup/l1_watcher.rs        | 222   | +222 |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/native_rollup/mod.rs               | 89    | +89  |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/native_rollup/types.rs             | 28    | +28  |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/tracing.rs                       | 178   | +20  |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/db/guest_program_state_db.rs        | 68    | +68  |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/db/mod.rs                           | 150   | +2   |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/execute_precompile.rs               | 383   | +383 |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/lib.rs                              | 22    | +2   |
+---------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/precompiles.rs                      | 1202  | +22  |
+---------------------------------------------------------------+-------+------+

@github-actions

github-actions Bot commented Feb 11, 2026

Copy link
Copy Markdown

Benchmark Results Comparison

No significant difference was registered for any benchmark run.

Detailed Results

Benchmark Results: BubbleSort

Command Mean [s] Min [s] Max [s] Relative
main_revm_BubbleSort 3.011 ± 0.025 2.972 3.051 1.12 ± 0.01
main_levm_BubbleSort 2.700 ± 0.018 2.680 2.732 1.00
pr_revm_BubbleSort 2.966 ± 0.021 2.944 3.013 1.10 ± 0.01
pr_levm_BubbleSort 2.711 ± 0.037 2.681 2.806 1.00 ± 0.02

Benchmark Results: ERC20Approval

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Approval 976.9 ± 6.5 969.7 992.9 1.00 ± 0.01
main_levm_ERC20Approval 1029.7 ± 10.0 1019.1 1051.7 1.06 ± 0.01
pr_revm_ERC20Approval 972.4 ± 8.2 966.3 993.7 1.00
pr_levm_ERC20Approval 1025.3 ± 6.6 1015.9 1035.8 1.05 ± 0.01

Benchmark Results: ERC20Mint

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Mint 131.6 ± 1.4 130.2 133.8 1.00
main_levm_ERC20Mint 148.4 ± 0.4 147.7 149.0 1.13 ± 0.01
pr_revm_ERC20Mint 131.6 ± 2.3 129.5 137.5 1.00 ± 0.02
pr_levm_ERC20Mint 149.2 ± 1.6 147.7 152.8 1.13 ± 0.02

Benchmark Results: ERC20Transfer

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Transfer 233.1 ± 3.2 230.3 240.1 1.01 ± 0.02
main_levm_ERC20Transfer 253.1 ± 1.5 250.9 255.5 1.10 ± 0.02
pr_revm_ERC20Transfer 231.0 ± 3.2 228.8 239.2 1.00
pr_levm_ERC20Transfer 252.5 ± 2.6 250.1 258.1 1.09 ± 0.02

Benchmark Results: Factorial

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Factorial 224.0 ± 1.2 222.1 225.8 1.00 ± 0.01
main_levm_Factorial 262.3 ± 10.3 255.1 290.7 1.18 ± 0.05
pr_revm_Factorial 223.0 ± 2.2 218.2 225.3 1.00
pr_levm_Factorial 256.0 ± 1.8 253.8 259.3 1.15 ± 0.01

Benchmark Results: FactorialRecursive

Command Mean [s] Min [s] Max [s] Relative
main_revm_FactorialRecursive 1.630 ± 0.024 1.603 1.669 1.06 ± 0.02
main_levm_FactorialRecursive 1.579 ± 0.013 1.563 1.606 1.03 ± 0.01
pr_revm_FactorialRecursive 1.596 ± 0.029 1.557 1.659 1.04 ± 0.02
pr_levm_FactorialRecursive 1.540 ± 0.010 1.520 1.551 1.00

Benchmark Results: Fibonacci

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Fibonacci 204.9 ± 2.1 199.6 207.6 1.00
main_levm_Fibonacci 231.8 ± 6.9 224.7 248.9 1.13 ± 0.04
pr_revm_Fibonacci 205.5 ± 5.4 201.6 220.7 1.00 ± 0.03
pr_levm_Fibonacci 238.5 ± 27.9 225.6 317.2 1.16 ± 0.14

Benchmark Results: FibonacciRecursive

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_FibonacciRecursive 840.2 ± 29.1 819.4 918.7 1.22 ± 0.05
main_levm_FibonacciRecursive 689.1 ± 9.9 680.9 716.1 1.00
pr_revm_FibonacciRecursive 837.0 ± 15.9 824.8 878.2 1.21 ± 0.03
pr_levm_FibonacciRecursive 692.8 ± 6.0 683.2 701.8 1.01 ± 0.02

Benchmark Results: ManyHashes

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ManyHashes 8.3 ± 0.0 8.3 8.4 1.00
main_levm_ManyHashes 9.9 ± 0.2 9.8 10.4 1.19 ± 0.02
pr_revm_ManyHashes 8.4 ± 0.1 8.3 8.5 1.01 ± 0.01
pr_levm_ManyHashes 9.9 ± 0.1 9.8 10.0 1.18 ± 0.01

Benchmark Results: MstoreBench

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_MstoreBench 262.8 ± 3.9 258.9 270.5 1.13 ± 0.02
main_levm_MstoreBench 233.6 ± 0.8 232.4 234.8 1.00
pr_revm_MstoreBench 272.6 ± 17.4 260.8 318.8 1.17 ± 0.07
pr_levm_MstoreBench 233.8 ± 0.8 232.9 235.4 1.00 ± 0.00

Benchmark Results: Push

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Push 289.8 ± 4.3 284.7 295.9 1.05 ± 0.02
main_levm_Push 275.9 ± 0.7 274.6 277.0 1.00 ± 0.01
pr_revm_Push 287.8 ± 1.9 285.7 290.4 1.04 ± 0.01
pr_levm_Push 275.9 ± 1.9 274.5 280.2 1.00

Benchmark Results: SstoreBench_no_opt

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_SstoreBench_no_opt 166.2 ± 5.6 159.9 178.5 1.66 ± 0.07
main_levm_SstoreBench_no_opt 99.8 ± 2.4 98.5 106.5 1.00
pr_revm_SstoreBench_no_opt 164.4 ± 2.0 161.2 167.8 1.65 ± 0.04
pr_levm_SstoreBench_no_opt 100.2 ± 1.7 98.7 103.5 1.00 ± 0.03

github-merge-queue Bot pushed a commit that referenced this pull request Feb 12, 2026
…ub auto-links as issue references (#6187)

## Motivation

The Claude AI code reviewer (`.github/workflows/pr_ai_review.yaml`)
enumerates findings using `#1`, `#2`, etc., which GitHub auto-links as
references to issues/PRs. This clutters PR activity feeds and confuses
readers. Example:
#6186 (comment)

## Description

Add formatting rules to the AI review prompt
(`.github/prompts/ai-review.md`) instructing the model to use `1.`, `2.`
or bullet points instead of `#N`, and to refer back to items as "Item
1", "Point 2" rather than "Issue #1".

## Checklist

- [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR
includes breaking changes to the `Store` requiring a re-sync.
avilagaston9 and others added 19 commits February 12, 2026 12:09
…calldata

deserialization in the EXECUTE precompile. Blob transactions (EIP-4844),
privileged L2 transactions, and fee token transactions are now rejected before
sender recovery (cheap check first). The withdrawal processing was replaced
with validation that rejects non-empty withdrawals since native rollup L2
blocks don't have validator withdrawals. The execute_precompile() entrypoint
now deserializes structured input from JSON calldata via serde_json instead
of returning a stub error.
…ompile.

The demo test (test_execute_precompile_via_contract) deploys a proxy contract
on L1 that forwards calldata to the EXECUTE precompile at 0x0101, serializing
a full L2 state transition (Alice→Bob transfer + Charlie deposit) as JSON.
This demonstrates the end-to-end flow: L1 tx → contract → CALL → precompile
→ re-execute L2 block → verify state roots.

Also adds rejection tests for blob transactions (EIP-4844) and non-empty
withdrawals, plus a build_l2_state_transition() helper that extracts the
common L2 state setup into a reusable function.
…ation,

withdrawal rejection, JSON calldata deserialization, and the new contract
demo and rejection tests
…lock>,

add NativeRollup.sol contract that simulates an L2 on-chain (maintains stateRoot
and blockNumber in storage, calls EXECUTE precompile via advance()), and update
the in-process test to use the NativeRollup contract instead of a dumb proxy.

The single-block API simplifies the interface — callers advance one block at a time.
The NativeRollup contract demonstrates how an L1 contract would track and verify
L2 state transitions by calling the EXECUTE precompile at 0x0101.
advance() updates contract storage correctly. Propagate native-rollups feature
flag to ethrex binary (cmd/ethrex/Cargo.toml) and test crate (test/Cargo.toml),
update Makefile init-l1 to support NATIVE_ROLLUPS=1 env var, and update
documentation to reflect single-block API, NativeRollup contract, and
integration test instructions.
…native-rollups,

matching the same pattern used for the GPU variable in prover targets.
…d add a

deposit mechanism to NativeRollup.sol. The precompile now parses a hybrid binary
format: state roots (32+32 bytes), deposit count (4 bytes), deposits (20 address
+ 32 amount each), block RLP length (4 bytes), RLP-encoded block, and remaining
JSON-serialized ExecutionWitness. Block uses RLP (already implemented), while
ExecutionWitness stays JSON because it lacks RLP support (uses serde/rkyv).

NativeRollup.sol gains deposit(address) payable to record pending deposits and
advance() now takes disaggregated parameters (newStateRoot, newBlockNumber,
depositsCount, block, witness) instead of an opaque precompileInput blob. The
contract builds the binary calldata on-chain from its stored deposits and the
opaque block/witness bytes passed by the caller.

Tests updated: the direct precompile test now exercises the binary parsing path
via execute_precompile(), and the contract test runs a deposit TX before advance
to verify the full deposit-to-advance-to-precompile flow including depositIndex.
EXECUTE precompile and simplify the NativeRollup contract interface.

The precompile now parses abi.encode(bytes32 preStateRoot, bytes blockRlp,
bytes witnessJson, bytes deposits) and returns abi.encode(bytes32 postStateRoot,
uint256 blockNumber) instead of a single 0x01 byte. The post-state root and
block number are extracted from the block header rather than being passed
as separate parameters.

NativeRollup.sol advance() signature changes from
advance(bytes32, uint256, uint256, bytes, bytes) to advance(uint256, bytes, bytes),
removing the redundant _newStateRoot and _newBlockNumber params that are now
decoded from the precompile return value. Also adds receive() for convenient
ETH deposits.
… native

rollups tests to the shared test crate. Since blob transactions are explicitly
rejected, blob gas fields are set to defaults instead of being computed. Unit
tests (precompile transfer, blob rejection, withdrawal rejection) and the
contract-based test move to test/tests/levm/native_rollups.rs. The L1
integration test moves to test/tests/l2/native_rollups.rs and now uses SDK
helpers (compile_contract, encode_calldata) instead of duplicated calldata
encoding and hardcoded deploy bytecode.
…move

of tests from crates/vm/levm/tests/ and test/tests/levm/ to the shared
test crate (test/tests/levm/native_rollups.rs and test/tests/l2/native_rollups.rs).
… Test

guide with real expected output, explanations of what each test does
(precompile flow, contract interaction diagram, integration test steps),
prerequisites, and the exact commands to run at each level.
…on test

instead of hardcoding a gas limit that may exceed the block gas limit. Add the
integration test expected output to the How to Test guide.
…ontract.

The EXECUTE precompile now extracts WithdrawalInitiated events from L2 block
execution logs, computes a commutative Keccak256 Merkle root (OpenZeppelin-
compatible), and returns it as a third field: abi.encode(postStateRoot,
blockNumber, withdrawalRoot) — 96 bytes instead of the previous 64.

New L2WithdrawalBridge.sol contract at address 0x00...fffd where L2 users
call withdraw(receiver) with ETH to initiate withdrawals. The contract emits
WithdrawalInitiated(from, receiver, amount, messageId) events that the
precompile scans for.

NativeRollup.sol now stores withdrawalRoots[blockNumber] after each advance()
and exposes claimWithdrawal() where users verify a Merkle proof against the
stored root to claim their ETH on L1. Includes reentrancy guard and
checks-effects-interactions pattern.

Tests updated to expect 96-byte precompile return with zero withdrawal root
(existing test blocks have no withdrawal events). Integration test verifies
withdrawalRoots mapping storage. Documentation updated with withdrawal flow
diagram, new contract descriptions, and updated return value format.
advances two L2 blocks: block 1 (transfer + deposit) and block 2 (withdrawal
via L2WithdrawalBridge), then calls claimWithdrawal() on L1 with a Merkle
proof and verifies the receiver gets the ETH.

The L2 genesis state now includes the L2WithdrawalBridge contract at the
canonical address (0x00...fffd). A new build_l2_withdrawal_block() helper
uses LEVM to execute the withdrawal transaction and compute exact gas_used
and post-state root for the block, matching what the EXECUTE precompile
will verify on L1.
…w in the

integration test. The test now covers two L2 blocks (transfer+deposit and
withdrawal) plus claimWithdrawal on L1. Updated expected output, test
description, and the limitation about L2 bridge deployment.
instead of leaving it in the contract. This matches the existing
CommonBridgeL2 pattern and ensures the ETH actually leaves circulation
on L2 when a withdrawal is initiated.
feature flag already gates compilation. Move contract compilation
before L2 state building so the test doesn't depend on pre-compiled
binaries. Compile L2WithdrawalBridge with --bin-runtime (needed for L2
genesis state). Add address(0) to test state tries since the bridge
now burns ETH by sending to address(0). Simplify the docs limitations
section to a single line about the L2 production stack being unchanged.
…ayer-based

flow instead of direct state manipulation. Replace L2WithdrawalBridge with a
unified L2Bridge predeploy at 0x00...fffd that handles both deposits
(processDeposit) and withdrawals (withdraw). The sequencer/relayer now sends
real L2 transactions to process deposits, pulling ETH from the preminted bridge
contract. NativeRollup.sol on L1 computes a rolling hash over deposit hashes
and passes it to the EXECUTE precompile, which verifies it by scanning
DepositProcessed events from the L2Bridge after block re-execution. Simplify
the precompile ABI to receive depositsRollingHash as a static bytes32 slot
instead of encoding deposit data in the block. Rewrite both unit and integration
tests to use LEVM-based execution with the relayer pattern. Add gap analysis
document tracking remaining spec gaps (burned fees, finality delay, individual
field inputs). Gitignore solc_out/ under levm and clean up compiled artifacts
at the end of the integration test.
@avilagaston9 avilagaston9 force-pushed the feat/native-rollups-execute-precompile branch from 01fc945 to d3285c8 Compare March 2, 2026 18:16
avilagaston9 and others added 16 commits March 2, 2026 15:47
…nt doc

Fix the EIP-8079 link (was pointing to a random PR, now points to the EIP page).
Remove the undefined "Phase 1" qualifier from the PoC note.
Update the block producer log example to match the actual format.
…rollups.md.

The Merkle tree uses commutative Keccak256 hashing that matches OpenZeppelin's
_hashPair but is our own implementation with no dependency on OpenZeppelin.
Remove "OpenZeppelin-compatible" from merkle_tree.rs module/function docs and
all references in native_rollups.md.

Also remove from native_rollups.md:
- "We're ahead on the first deposit problem" (self-congratulatory, not documentation)
- "Configurable finality delay" from the limitations list (it is not a limitation)
- "These are all Phase 2+ concerns." (vague, adds nothing)
- The files table at the end (stale and redundant with the code)
- Incorrect receive() description: the fallback just accepts ETH silently,
  it does not record an L1 message
…both from deployer.

Two separate actors now have explicit addresses in the contract:
- relayer: the account that submits L1→L2 message transactions on L2
- advancer: the account that calls advance() on L1 and receives burned fees

Previously burned fees were sent to msg.sender (whoever called advance()). Now they
go to the stored advancer address, making both roles configurable independently.
In the current deployment both are set from the same keys, but the contract supports
them being different accounts.

Constructor changes from (bytes32, uint256, uint256, uint64, uint256) to
(bytes32, uint256, uint256, address, address) — CHAIN_ID and FINALITY_DELAY were
never passed by the deployer anyway (defaulted to 0) and are now hardcoded to 0.
…t instead of embedding hardcoded initcode hex.

Follows the same pattern used by the shared_bridge integration test.
…atching the integration_tests cleanup pattern.
The mdbook linkcheck requires all docs files to be listed in SUMMARY.md.
The ZisK Cargo.lock was out of date (lambdaworks-crypto was removed as a dependency).
… from lychee link check.

peer_table.rs moved from crates/networking/p2p/discv4/ to crates/networking/p2p/.
native-rollup.l2beat.com is unreachable from CI (connection refused).
… lychee link check.

SP1, RISC0, and OpenVM Cargo.lock files had lambdaworks-crypto as a stale
dependency (same issue as ZisK, fixed previously). Regenerated all three.

Accept 5xx HTTP responses in lychee to avoid failing on transient server errors
(e.g. the 502 from github.com/ethereum/execution-specs/issues/1940). Genuine
broken links (4xx) still fail.
…lup.rs

to fix CI failures (Cargo.lock check and fmt check).
…behind

feature flags to fix dead-code lint error in CI. The callers of
read_env_file_by_config are all behind #[cfg(feature = "l2")] or
#[cfg(feature = "native-rollup")], so the function must be gated too.
…g 0.

The EXECUTE precompile validates that the chain_id in the calldata matches
the execution witness, but the constructor was setting CHAIN_ID = 0 while
the L2 genesis uses chainId = 1. This caused every advance() call to fail
with "Chain ID mismatch: input=0, witness=1". Now the deployer reads the
chain_id from the L2 genesis config and passes it as a constructor argument.
@avilagaston9 avilagaston9 force-pushed the feat/native-rollups-execute-precompile branch from 9b07ff2 to 7d415f9 Compare March 5, 2026 14:40
avilagaston9 added 9 commits March 5, 2026 11:59
… counter, claim)

in the native rollups deployment doc. The deployer key (0x385c...) is also used by
the L1 advancer for advance() calls, so sending transactions from the same account
causes nonce conflicts ("underpriced transaction" error). Now the demo uses key
0xbcdf... (address 0x8943...), which is pre-funded in the L1 genesis and not used
by any system component.
…e* RPC

methods for Blockscout compatibility, add Step 6 with Blockscout setup/verification
instructions, and add a contract verification script.

RPC fixes in tracing.rs:
- debug_traceTransaction now returns a single CallTraceFrame instead of an
  array, matching geth's callTracer format that Blockscout expects for internal
  transaction indexing.
- debug_traceBlockByNumber now parses hex block numbers (e.g. "0x13E") since
  Blockscout sends them in hex format.
- Increase DEFAULT_REEXEC from 128 to 10000 to handle longer chain histories.

Other changes:
- Add scripts/blockscout_verify_native_rollup.py for verifying the NativeRollup
  contract via Blockscout's V2 standard-input API.
- Fix terminalTotalDifficulty format in native_l2.json (0 → "0x0").
- Document correct Blockscout cleanup: rm -rf services/blockscout-db-data
  (the bind mount lives under services/, not the docker-compose root).
…xecute-precompile

# Conflicts:
#	crates/vm/levm/src/precompiles.rs
…ch: restore DEFAULT_REEXEC to 128 (was bumped to 10000), remove native-rollup.l2beat.com link checker exclusions from lychee CI action and book.toml to match main.
…er merging main to pass the check-cargo-lock CI check.
…the Crypto trait refactor — sender() now requires a &dyn Crypto argument for signature recovery.
…xecute-precompile

Resolve conflict in merkle_tree.rs (kept re-export approach since logic
was moved to ethrex-common). Thread &dyn Crypto through GuestProgramStateDb
and execute_precompile.rs to match main's Crypto trait refactor.
@ilitteri

Copy link
Copy Markdown
Collaborator

Superseded by #6418

@ilitteri ilitteri closed this Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

levm Lambda EVM implementation

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants