Skip to content

Add L1 precompile witness verification#72

Open
jmadibekov wants to merge 13 commits into
taikoxyz:mainfrom
jmadibekov:jmadibekov/l1-precompiles
Open

Add L1 precompile witness verification#72
jmadibekov wants to merge 13 commits into
taikoxyz:mainfrom
jmadibekov:jmadibekov/l1-precompiles

Conversation

@jmadibekov

@jmadibekov jmadibekov commented Jun 1, 2026

Copy link
Copy Markdown

Prover-side support for the L1SLOAD and L1STATICCALL precompiles. Companion alethia-reth PR: taikoxyz/alethia-reth#195.

flowchart LR
  L1["L1 EL<br/>Nethermind"]
  L2["L2 EL<br/>alethia-reth<br/>#195"]
  P["Prover<br/>raiko2<br/>(this PR)"]

  L2 -- "live RPC" --> L1
  P  -- "preflight RPC" --> L1
  P -. "imports as library" .-> L2
Loading

The trust chain hangs off one on-chain commitment — originBlockHash, written by the L1 EVM during Inbox.propose():

flowchart TD
  A["L1 Inbox.propose commits<br/>originBlockHash = blockhash(block.number − 1)"]
  B["originBlockHash is part of the proposal hash<br/>protocol-locked, can't be tampered"]
  C["L1 origin header<br/>+ up to 256 ancestor headers in the witness"]
  D["Walk parent_hash backward<br/>builds trusted block → state_root map"]
  E["L1SLOAD: MPT-verify storage proof<br/>against trusted state_root"]
  F["L1STATICCALL: bind witness headers,<br/>rebuild MPT, re-execute in revm,<br/>assert (output, gas, halt) match"]

  A --> B --> C --> D
  D --> E
  D --> F
Loading

256 is the cap because Ethereum's BLOCKHASH opcode only sees that far back.

Live and proving paths reach the same answer because both reuse the same precompile binary, the same cache key, and the same [origin − 256, origin] window check. The two precompiles differ in the RPC used for discovery, the proof shape fetched, and the guest verification step:

L1SLOAD (0x10001) L1STATICCALL (0x10002)
Live + discovery RPC eth_getStorageAt debug_traceCall
Preflight proof RPC eth_getProof proof_call
Proof shape Merkle storage proof Execution witness
GuestInput field L1StorageProof[] L1StaticCallWitness[]
Guest verification MPT-verify against trusted state_root rebuild MPT + revm re-exec + assert (output, gas, halt) match
End-to-end: L1SLOAD — live + proving
sequenceDiagram
  participant T as L2 tx (live)
  participant EVM as alethia-reth EVM
  participant Hf as Live fetcher
  participant Pf as Prover fetcher
  participant L1 as L1 EL
  participant H as Host preflight
  participant G as Guest

  par Live (sequencer)
    T->>EVM: CALL 0x10001
    EVM->>Hf: cache miss
    Hf->>L1: eth_getStorageAt
    L1-->>Hf: value (B256)
    Hf-->>EVM: cache, return
  and Proving (raiko2)
    H->>EVM: install Pf, re-run blocks (discovery)
    EVM->>Pf: cache miss
    Pf->>L1: eth_getStorageAt
    L1-->>Pf: value
    Pf-->>EVM: cache, record served call
    H->>L1: eth_getProof per record → Merkle proof
    H->>H: verify each proof against trusted state_root (S7)
    H->>G: GuestInput with L1StorageProof[]
    G->>EVM: MPT-verify proof against trusted state_root
    G->>EVM: populate cache, re-run blocks
  end
Loading
End-to-end: L1STATICCALL — live + proving
sequenceDiagram
  participant T as L2 tx (live)
  participant EVM as alethia-reth EVM
  participant Hf as Live fetcher
  participant Pf as Prover fetcher
  participant L1 as L1 EL
  participant H as Host preflight
  participant G as Guest

  par Live (sequencer)
    T->>EVM: CALL 0x10002
    EVM->>Hf: cache miss
    Hf->>L1: debug_traceCall
    L1-->>Hf: gas, output, reverted
    Hf-->>EVM: cache, return
  and Proving (raiko2)
    H->>EVM: install Pf, re-run blocks (discovery)
    EVM->>Pf: cache miss
    Pf->>L1: debug_traceCall
    L1-->>Pf: gas, output
    Pf-->>EVM: cache, record served call
    H->>L1: proof_call per non-reverted record → execution witness
    H->>H: extend ancestor window for witness BLOCKHASH reads (S2)
    H->>G: GuestInput with L1StaticCallWitness[]
    G->>EVM: verify witness headers against trusted chain
    G->>EVM: rebuild MPT, populate full block env from header
    G->>EVM: revm re-exec, assert (output, gas, halt) match
    G->>EVM: populate cache, re-run blocks
  end
Loading
RPC reference
RPC Namespace Served by Called by Purpose
eth_getStorageAt eth any L1 EL alethia live; raiko2 discovery L1SLOAD live value
eth_getProof eth any L1 EL raiko2 preflight L1SLOAD Merkle proof
debug_traceCall debug any L1 EL alethia live; raiko2 discovery L1STATICCALL gas + output
proof_call proof Nethermind only raiko2 preflight L1STATICCALL execution witness

Caveats

The single place for the operational and behavioral notes that span both PRs. The EL PR taikoxyz/alethia-reth#195 points here.

Running this on a devnet

Two things on top of a normal raiko2 + Taiko devnet:

  1. Rebuild the guest ELFs for your chain spec. The guest bakes config/chain_spec_list_default.json in at compile time and checks it byte-for-byte at runtime, so the committed placeholder taiko_dev.l1_contract (0xb432…) won't match a real devnet. Set taiko_dev's l1_contract and verifier_address_forks (including the SGXGETH slot) to your devnet's addresses, then cargo run -p xtask-build-guest -- <risc0|sp1|all>. Skipping this panics the guest with unexpected l1_contract. Rebuild the proposal and aggregation ELFs together — never binary-patch one, that changes its image id and breaks aggregation. If the alethia-reth fork rev moves, re-sync each guest workspace's own Cargo.lock with cargo metadata (not generate-lockfile, which over-resolves).

  2. The L1 EL must be Nethermind. L1STATICCALL witnesses come from the proof_call RPC (proof namespace), which only Nethermind serves today (feat(rpc): implement proof_call for stateless call verification NethermindEth/nethermind#11732, merged 2026-06-11). Make sure your devnet's L1 EL exposes it. Standardizing it across Geth/Reth is future work.

How the precompiles behave (EL side, from #195)

  • Fork-gated, not always-on. They exist only from TaikoSpecId::UNZEN. Your chain spec must activate Unzen; otherwise 0x10001/0x10002 are unregistered and a call hits the normal "no code at address" path (no error). There is no runtime toggle.
  • An L1 RPC is required. Start the node with --l1-rpc-url; with Unzen on and the flag missing, it refuses to start. The endpoint must serve debug_traceCall (for L1STATICCALL; L1SLOAD only needs eth_getStorageAt), and the fetchers need a multi-thread tokio runtime.
  • Reads are windowed to [origin − 256, origin] (256 is the BLOCKHASH reach). With an origin set (block build/import) an out-of-window read halts; without one (preconf, eth_call, stateless re-exec) the check is skipped and the prover re-enforces it. Consequence: a call that succeeds under eth_call for an out-of-window block is rejected in proving — test against a real built block, not just eth_call.
  • Shared constants prevent sequencer↔prover drift. Gas caps, size limits, caller, and lookback are pub constants in alethia-reth-evm, read by both sides, so they can't disagree.
  • L1STATICCALL limits. Calldata capped at 64 KiB (rejected before any gas is charged), return data at 24 KiB, caller pinned to Address::ZERO. A reverting L1 target returns (gas 0, empty); the revert reason is not propagated.

How the prover handles them (raiko2 side)

  • Temporary fork pin. The alethia-reth-* deps are redirected via [patch] to a personal fork (jmadibekov/alethia-reth @ 5333f16, the #195 / 1.2.0 line) in all three workspaces: root, guests/risc0, and guests/sp1. The base revs still point at taikoxyz/alethia-reth @ 1ed7c19. When #195 merges, drop the [patch] and bump the base revs to the merge SHA.
  • L1SLOAD proofs are verified in host preflight against the trusted state root, so an L1 reorg between discovery and proof-fetch fails cleanly instead of deep in the guest.
  • BLOCKHASH below the window works. A call at the lower window edge can read block hashes below origin − 256; the host pulls in the extra ancestor headers and the guest verifies their hash chain (without granting them state roots, so the trust window stays 256 blocks).
  • The guest rebuilds the full L1 block env (timestamp, basefee, prevrandao, gas limit, …) from the verified header, so L1 contracts reading those opcodes prove correctly.
  • Tunable + robust. Preflight L1-RPC concurrency is set by L1_PRECOMPILE_CONCURRENCY (default 16); a discovery panic surfaces as a clean preflight error.

Known follow-ups (none block the devnet)

  • zk-gas multipliers are uncalibrated placeholders. UNZEN_PRECOMPILE_MULTIPLIERS (alethia-reth crates/evm/src/zk_gas/unzen.rs) sets both precompiles to 50, flagged TODO(calibrate). Native gas is fixed and correct, but the multiplier must be measured before any non-devnet use — without an entry a precompile falls through to FAILSAFE_MULTIPLIER (u16::MAX), which alone exceeds the block zk-gas budget. Treat devnet gas/throughput numbers as provisional.
  • A little more L1 context is threaded later, deferred together to avoid churning guest fixtures now: aligning the proof_call/guest gas budget with discovery's (only matters for a callee that branches on gasleft()), and setting the guest's chain_id and fork from the L1 chain spec (only matters for a non-mainnet L1 whose callee reads CHAINID, or a block before a gas-changing fork boundary). Both are annotated in-code; neither affects the current devnet.

@jmadibekov jmadibekov force-pushed the jmadibekov/l1-precompiles branch 3 times, most recently from 633075b to c4dc9f8 Compare June 1, 2026 14:49
@jmadibekov jmadibekov marked this pull request as ready for review June 2, 2026 08:52
@jmadibekov jmadibekov requested a review from a team June 2, 2026 08:52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4dc9f85f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Cargo.toml Outdated
Comment on lines +220 to +225
[patch."https://github.com/taikoxyz/alethia-reth"]
alethia-reth-block = { git = "https://github.com/jmadibekov/alethia-reth", rev = "83659a1a2442c071cff0c276146f9459de7bb42f" }
alethia-reth-chainspec = { git = "https://github.com/jmadibekov/alethia-reth", rev = "83659a1a2442c071cff0c276146f9459de7bb42f" }
alethia-reth-consensus = { git = "https://github.com/jmadibekov/alethia-reth", rev = "83659a1a2442c071cff0c276146f9459de7bb42f" }
alethia-reth-evm = { git = "https://github.com/jmadibekov/alethia-reth", rev = "83659a1a2442c071cff0c276146f9459de7bb42f" }
alethia-reth-primitives = { git = "https://github.com/jmadibekov/alethia-reth", rev = "83659a1a2442c071cff0c276146f9459de7bb42f" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace alethia-reth fork patches with upstream pins

The repository instructions require alethia-reth dependencies to come from https://github.com/taikoxyz/alethia-reth origin/main with explicit reviewed rev pins, and explicitly forbid keeping required fixes only in one-off PR branches. This patch redirects every alethia-reth crate to a personal fork, so fresh builds and lockfile resolution depend on an uncanonical source instead of the reviewed upstream commit.

Useful? React with 👍 / 👎.

Comment thread crates/provider/src/network/l1_precompiles.rs Outdated
Comment thread crates/provider/src/network/l1_precompiles.rs Outdated
Comment thread crates/primitives-shasta/src/l1_precompiles/mod.rs
@jmadibekov jmadibekov force-pushed the jmadibekov/l1-precompiles branch from 29e7901 to f0aff5a Compare June 4, 2026 10:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0aff5abee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/pipeline/src/forks/shasta/spec.rs Outdated
Comment thread crates/pipeline/src/forks/shasta/spec.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 182c65ab16

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/primitives-shasta/src/l1_precompiles/witness_db.rs Outdated
"from": format!("{L1_PRECOMPILE_CALLER:?}"),
"to": format!("{target:?}"),
"data": format!("0x{}", hex::encode(calldata)),
"gas": format!("0x{:x}", L1STATICCALL_GAS_CAP),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the discovered gas limit for proof_call

For L1STATICCALLs where the L2 frame supplies less than L1STATICCALL_GAS_CAP gas, discovery runs debug_traceCall with min(frame_gas, cap) but the witness fetch here always runs proof_call at the full cap. Any L1 view that depends on gasleft() or only succeeds with the larger cap can produce a witness for a different execution than the recorded return/gas, causing honest preflight/proving to fail; carry the discovered gas limit in the served record and use the same value here and in guest re-execution.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 820a56ba45

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/pipeline/src/forks/shasta/spec.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d422421743

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// The zero-address caller has no balance, so `gas_price >= basefee` would normally
// fail validation. `cfg.disable_base_fee` lifts that check (we're already running
// `gas_price = 0`).
let mut evm = revm::Context::mainnet()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Set the L1 chain id for staticcall replay

When the request is proving against a non-mainnet L1 (for example the Hoodi configuration), this builds a mainnet revm context and never overrides cfg.chain_id. Any L1STATICCALL target that executes the CHAINID opcode is traced by the live L1 with that network's chain id but re-executed here with mainnet's id, so the return-data/gas assertion rejects otherwise valid proposals; thread the resolved L1 chain id into this verifier and set it on the cfg before replay.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3a254142ef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/primitives-shasta/src/l1_precompiles/l1staticcall.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 94f7f20067

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/primitives-shasta/Cargo.toml

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3121797052

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let missing: Vec<u64> = (min_served..current_floor).collect();
let headers = provider.batch_l1_headers(&missing).await?;
// D15: single splice instead of take + extend + reassign — one allocation.
input.taiko.l1_ancestor_headers.splice(0..0, headers);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the stalled-anchor sentinel when extending L1 headers

When hydrate_shasta_l1_headers takes the stalled-anchor bypass it deliberately leaves taiko.l1_ancestor_headers empty, and validate_l1_anchor_linkage later uses that emptiness to decide whether to skip anchor progression. This splice makes the vector non-empty whenever a stalled-anchor proposal also needs an ancestor for L1SLOAD/L1STATICCALL (for example a read at origin - 1, or an origin staticcall whose witness includes BLOCKHASH(origin - 1)), so the guest no longer takes the bypass and rejects an otherwise valid proposal during anchor-linkage validation.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant