Add L1 precompile witness verification#72
Conversation
633075b to
c4dc9f8
Compare
There was a problem hiding this comment.
💡 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".
| [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" } |
There was a problem hiding this comment.
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 👍 / 👎.
29e7901 to
f0aff5a
Compare
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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".
| "from": format!("{L1_PRECOMPILE_CALLER:?}"), | ||
| "to": format!("{target:?}"), | ||
| "data": format!("0x{}", hex::encode(calldata)), | ||
| "gas": format!("0x{:x}", L1STATICCALL_GAS_CAP), |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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() |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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); |
There was a problem hiding this comment.
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 👍 / 👎.
Prover-side support for the
L1SLOADandL1STATICCALLprecompiles. Companion alethia-reth PR: taikoxyz/alethia-reth#195.The trust chain hangs off one on-chain commitment —
originBlockHash, written by the L1 EVM duringInbox.propose():256 is the cap because Ethereum's
BLOCKHASHopcode 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:0x10001)0x10002)eth_getStorageAtdebug_traceCalleth_getProofproof_callL1StorageProof[]L1StaticCallWitness[]state_root(output, gas, halt)matchEnd-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 endEnd-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 endRPC reference
eth_getStorageAtetheth_getProofethdebug_traceCalldebugproof_callproofCaveats
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:
Rebuild the guest ELFs for your chain spec. The guest bakes
config/chain_spec_list_default.jsonin at compile time and checks it byte-for-byte at runtime, so the committed placeholdertaiko_dev.l1_contract(0xb432…) won't match a real devnet. Settaiko_dev'sl1_contractandverifier_address_forks(including theSGXGETHslot) to your devnet's addresses, thencargo run -p xtask-build-guest -- <risc0|sp1|all>. Skipping this panics the guest withunexpected 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 ownCargo.lockwithcargo metadata(notgenerate-lockfile, which over-resolves).The L1 EL must be Nethermind. L1STATICCALL witnesses come from the
proof_callRPC (proofnamespace), which only Nethermind serves today (feat(rpc): implementproof_callfor 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)
TaikoSpecId::UNZEN. Your chain spec must activate Unzen; otherwise0x10001/0x10002are unregistered and a call hits the normal "no code at address" path (no error). There is no runtime toggle.--l1-rpc-url; with Unzen on and the flag missing, it refuses to start. The endpoint must servedebug_traceCall(for L1STATICCALL; L1SLOAD only needseth_getStorageAt), and the fetchers need a multi-thread tokio runtime.[origin − 256, origin](256 is theBLOCKHASHreach). 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 undereth_callfor an out-of-window block is rejected in proving — test against a real built block, not justeth_call.pubconstants inalethia-reth-evm, read by both sides, so they can't disagree.Address::ZERO. A reverting L1 target returns(gas 0, empty); the revert reason is not propagated.How the prover handles them (raiko2 side)
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, andguests/sp1. The base revs still point attaikoxyz/alethia-reth@1ed7c19. When #195 merges, drop the[patch]and bump the base revs to the merge SHA.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).L1_PRECOMPILE_CONCURRENCY(default 16); a discovery panic surfaces as a clean preflight error.Known follow-ups (none block the devnet)
UNZEN_PRECOMPILE_MULTIPLIERS(alethia-rethcrates/evm/src/zk_gas/unzen.rs) sets both precompiles to50, flaggedTODO(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 toFAILSAFE_MULTIPLIER(u16::MAX), which alone exceeds the block zk-gas budget. Treat devnet gas/throughput numbers as provisional.proof_call/guest gas budget with discovery's (only matters for a callee that branches ongasleft()), and setting the guest'schain_idand fork from the L1 chain spec (only matters for a non-mainnet L1 whose callee readsCHAINID, or a block before a gas-changing fork boundary). Both are annotated in-code; neither affects the current devnet.