fix: update consuming code for spec-aligned ExecutionProof types#9109
fix: update consuming code for spec-aligned ExecutionProof types#9109lodekeeper wants to merge 6 commits intoChainSafe:optional-proofsfrom
Conversation
## Description
Implements EIP-8025 optional execution proofs for Lodestar — validated
with 3-client interop (Lodestar + Lighthouse + Prysm) in a kurtosis
devnet with assertoor.
### What's included
| Phase | Description | Commit |
|-------|-------------|--------|
| A | SSZ types & constants (`ExecutionProof`, proof IDs, size limits) |
`5ee6cf3` |
| F | CLI: `--activateZkvm`, `--chain.minProofsRequired`,
`--chain.zkvmGenerationProofTypes` | `50cb096` |
| B.1 | Gossip: `execution_proof` topic wiring (parse, queue, scoring,
handler, zkvm-gated subscription) | `e33b004` |
| B.2 | Req/Resp: `ExecutionProofsByRoot` + `ExecutionProofsByRange`
protocols | `ae6f0f4` |
| B.4 | ENR `zkvm` key signaling | `3e8fae0` |
| C | `ExecutionProofPool` (in-memory, indexed by
slot→blockRoot→proofId, 8-slot retention) | `da648a6` |
| D | REST API: `GET/POST /eth/v1/beacon/pool/execution_proofs` |
`08b4871` |
| G | Kurtosis devnet config + dummy prover script | `6f59221` |
| fix | Best-effort gossip publish on API submit, SSE timeout fix |
`0f5c734` |
| fix | Bridge `activateZkvm` CLI flag to network options | `5b1afeb` |
| fix | Far-future slot rejection in gossip handler | `acefaea` |
| test | ExecutionProofPool unit tests (15 tests) | `1bd02bb` |
### Interop validation (3-client, assertoor)
6-node kurtosis devnet: 3 supernodes (Lodestar+Lighthouse+Prysm, 96
validators each) + 3 zkvm observer nodes. Fulu fork epoch 1, 6s slots.
**Assertoor results — all passed:**
- ✅ Chain stability (finality, no reorgs, no forks)
- ✅ Block proposals from all client pairs
- ✅ Finality reached (finalized epoch 3, justified 4 at slot 171)
- ✅ 0 missed slots (130 blocks: Lodestar 46, Lighthouse 40, Prysm 44)
**Proof gossip — 100% delivery:**
- Prysm generates 2 proofs per block (proofId 0 and 1)
- Lodestar zkvm node received 278 proofs (2 × 139 slots), all
`insertOutcome=NewData`
- Lighthouse zkvm node received 156+ proofs
- SSZ wire format compatible across all 3 clients (uint8 ProofId, fixed
part = 77 bytes)
**Errors:** 1 harmless startup warning ("discv5 has no boot enr"), zero
errors across all 6 CL + 3 VC nodes.
### Not included (future work)
- **Phase E (sync integration)**: Range sync proof fetching + DA gating
— not needed for all-start-at-genesis devnets
- **Proof verification**: Gossip handler accepts all well-formed proofs
without cryptographic verification (TODO)
- **Cross-client req/resp**: Only gossip tested; req/resp protocol
handlers are stubbed
### How to test
```bash
# Build image
docker build -t lodestar:eip8025 -f Dockerfile.dev .
# Start mixed devnet
kurtosis run github.com/ethpandaops/ethereum-package \
--enclave eip8025-devnet \
--args-file scripts/eip8025-devnet/network_params.yaml
# Run dummy prover against Lodestar zkvm node
node scripts/dummy-prover.mjs --beacon-node http://127.0.0.1:33015
```
---
*This PR was authored with AI assistance (Claude Opus 4.6 + sub-agents
for review).*
---------
Co-authored-by: lodekeeper <lodekeeper@users.noreply.github.com>
…th) (ChainSafe#8923) ## Summary Implements **Phase E** of EIP-8025 Optional Execution Proofs: proof-driven execution payload validation for Lodestar. When `--activateZkvm` is enabled, beacon nodes can import/validate execution payloads using execution proofs from `ExecutionProofPool` instead of requiring `engine_newPayload` responses from EL. ## What this PR adds ### 1) Proof-driven execution verification path - Adds `verifyBlockExecutionPayloadByProof()` in `verifyBlocksExecutionPayloads.ts` - In zkvm mode: - loads proofs from `executionProofPool` keyed by beacon `blockRoot` - validates proofs with configurable verifier (structural checks via dummy verifier) - returns `ExecutionStatus.Valid` when threshold met - otherwise imports block optimistically (`ExecutionStatus.Syncing`) ### 2) Dummy zkVM proof verifier (dependency-injectable) - New file: `packages/beacon-node/src/chain/validation/executionProofVerifier.ts` - `IZkvmExecutionProofVerifier` interface — swap for real prover via `IChainOptions.executionProofVerifier` - `DummyZkvmExecutionProofVerifier` checks: - `proofData` is non-empty - proof `blockRoot` matches expected beacon block root - proof `blockHash` matches expected execution payload block hash - distinct `proofId` count meets `minProofsRequired` ### 3) Chain wiring - Exposes zkvm runtime knobs on chain: - `activateZkvm`, `minProofsRequired`, `executionProofVerifier` - Added to `IBeaconChain` + `BeaconChain` initialization - Verifier is a chain class property, defaulting to `DummyZkvmExecutionProofVerifier` ### 4) Fork-choice transition on proof arrival - `maybeTransitionToValidOnProofArrival()` on `BeaconChain` — shared logic for both gossip and API paths - Uses fork choice `getBlockHex()` to look up canonical execution payload block hash (avoids cross-client SSZ mismatch) - Calls `recomputeForkChoiceHead()` after `validateLatestHash()` to refresh cached head - Transitions blocks from optimistic/syncing to valid when proof threshold is met ### 5) Tests - `executionProofVerifier.test.ts` (9 tests) + `executionProofPool.test.ts` (15 tests) = 24 total - Covers: valid path, empty proof data, blockRoot/blockHash mismatch, insufficient proof types, pool operations ## Key design decisions - **Optimistic import**: Missing proofs do NOT reject block import — blocks are imported optimistically and become valid once sufficient proofs arrive. Matches Lighthouse approach. - **Fork choice as source of truth**: Uses `block.executionPayloadBlockHash` from fork choice proto-array, NOT `proof.blockHash`, to avoid cross-client SSZ encoding mismatches. - **DI for verifier**: `IZkvmExecutionProofVerifier` is injected via chain options — a real zkvm prover just implements the interface. No code changes needed beyond configuration. ## Kurtosis interop validation Validated in 3-client devnet (Lodestar + Lighthouse + Prysm, 6 nodes): - 130 blocks proposed (Lodestar 46, Lighthouse 40, Prysm 44), 0 missed - 278 proofs delivered, 100% delivery rate - Assertoor: chain stability ✅, finality ✅, attestation stats ✅, no reorgs ✅ 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com> --------- Signed-off-by: lodekeeper <lodekeeper@users.noreply.github.com> Co-authored-by: lodekeeper <lodekeeper@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: gemini-code-assist[bot] <gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Nico Flaig <nflaig@users.noreply.github.com>
This includes `ExecutionProof`, `SignedExecutionProof`, and `PublicInput`
The previous commit (7f52198) updated the ExecutionProof SSZ type to match the EIP-8025 spec but didn't update the ~10 files that consume it. Changes: - ExecutionProofPool: reindex by newPayloadRequestRoot + proofType (replaces slot + blockRoot + proofId indexing) - DummyZkvmExecutionProofVerifier: remove blockRoot/blockHash matching (no longer in proof type) - chain.ts: fix duplicate fork-choice imports, add newPayloadRequestRoot → blockRoot mapping for proof → block lookups - gossipHandlers/API/reqresp: use new type field names - Tests: rewrite pool + verifier tests for new type structure TODOs left for follow-up: - Populate newPayloadRequestRoot → blockRoot mapping during block import - Update reqresp ByRoot handler to use the mapping - Full gossip validation per spec (signature check, validator active, etc.) Co-authored-by: lodekeeper <lodekeeper@users.noreply.github.com>
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request addresses a build-breaking issue caused by a previous update to the Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 83881aa2fc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const proofs = chain.executionProofPool?.getAllProofs() ?? []; | ||
| const verifier = chain.executionProofVerifier ?? defaultZkvmExecutionProofVerifier; | ||
| const verification = verifier.verifyProofs({ | ||
| proofs, | ||
| expectedBlockRootHex: blockRootHex, | ||
| expectedExecBlockHashHex: execBlockHash, | ||
| minProofsRequired: minRequired, |
There was a problem hiding this comment.
Scope proof checks to the block being imported
verifyBlockExecutionPayloadByProof now validates chain.executionProofPool.getAllProofs() and the updated verifier only checks non-empty proofData plus distinct proofType, so proofs from unrelated payloads can satisfy minProofsRequired for the current block. In zkVM mode, once the pool contains enough proof types for any prior request, subsequent blocks can be marked ExecutionStatus.Valid without block-specific evidence, which breaks execution validity gating.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid finding — this is a known limitation tracked as a TODO in the PR description. The newPayloadRequestRoot → blockRoot mapping needs to be populated during block import to scope proof verification to the correct block. Will be addressed when EIP-8025 work resumes.
| if (this.proofsByRoot.size >= MAX_ROOT_ENTRIES && !this.proofsByRoot.has(rootHex)) { | ||
| return InsertOutcome.Old; |
There was a problem hiding this comment.
Evict old roots instead of rejecting all new ones at cap
When the pool reaches MAX_ROOT_ENTRIES, add() immediately returns InsertOutcome.Old for unseen roots, but prune() only deletes entries when size > MAX_ROOT_ENTRIES. Because insertion never allows size to exceed the cap, pruning cannot make room, so after 64 distinct roots the pool permanently rejects new payload roots (unless an external caller invokes pruneByRoot, which this commit does not wire up).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — real bug. add() rejects at >= MAX_ROOT_ENTRIES but prune() only evicts at > MAX_ROOT_ENTRIES, so the pool becomes permanently full after 64 distinct roots since prune can never make room. Fix: change prune() threshold to >= or switch to LRU-style eviction. Will fix when EIP-8025 work resumes.
| const blockRootHex = this.newPayloadRequestRootToBlockRoot.get(newPayloadRequestRootHex); | ||
| if (blockRootHex == null) { | ||
| this.logger.debug("Cannot transition block to valid: no blockRoot mapping for newPayloadRequestRoot", { |
There was a problem hiding this comment.
Populate proof-root mapping before late-valid transition
maybeTransitionToValidOnProofArrival now depends on newPayloadRequestRootToBlockRoot, but this change set only adds the map and lookup path; it does not add any callsite that populates the mapping during block import. As a result, proofs that arrive after optimistic import hit the early return here (no blockRoot mapping) and cannot flip the block from Syncing to Valid via this path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct — also a known TODO from the PR description: "Populate newPayloadRequestRoot → blockRoot mapping during block import". The mapping and lookup path are wired up but the population callsite during block import is intentionally deferred. Will be completed when EIP-8025 work resumes.
There was a problem hiding this comment.
Code Review
This pull request implements significant changes related to EIP-8025 execution proofs. The ExecutionProof structure is updated to remove slot, blockRoot, and blockHash fields, replacing them with publicInput.newPayloadRequestRoot and proofType. The ExecutionProofPool is refactored to index proofs by newPayloadRequestRoot and proofType and introduces a new size-based pruning mechanism instead of slot-based. The BeaconChain now includes a mapping from newPayloadRequestRoot to blockRoot and updates its proof handling logic accordingly. The dummy proof verifier and network handlers are also adjusted to align with these new proof semantics. A review comment highlights that the registerNewPayloadRequestRoot function is missing its JSDoc, and its previous documentation block has been orphaned by the refactoring.
| } | ||
|
|
||
| maybeTransitionToValidOnProofArrival(proof: ExecutionProof): void { | ||
| if (!this.activateZkvm) { |
There was a problem hiding this comment.
Good catch on the orphaned JSDoc. Will fix when EIP-8025 work resumes — this PR is currently on hold.
7f52198 to
4ed194b
Compare
498caac to
cb064c6
Compare
Problem
Commit
7f5219861d(feat: implement eip-8025 spec aligned ssz types) updated theExecutionProofSSZ type to match the EIP-8025 spec but didn't update the ~10 consuming files inbeacon-node, breaking the build.Old type:
{ proofId, slot, blockHash, blockRoot, proofData }New type:
{ proofData, proofType, publicInput: { newPayloadRequestRoot } }+ SignedExecutionProof = { message: ExecutionProof, validatorIndex, signature }Changes
Pool (
executionProofPool.ts)(newPayloadRequestRoot, proofType)instead of(slot, blockRoot, proofId)getAllProofs()method for API listing and reqresp fallbackVerifier (
executionProofVerifier.ts)expectedBlockRootHex/expectedExecBlockHashHexfrom input (not in new type)Chain (
chain.ts)@lodestar/fork-choiceimports (merge artifact)newPayloadRequestRootToBlockRootmapping for proof → block lookupsmaybeTransitionToValidOnProofArrivaltakesExecutionProofdirectlyInterface (
interface.ts)IBeaconChain.maybeTransitionToValidOnProofArrivalsignatureNetwork/Gossip/API/ReqResp
proof.proofType,proof.publicInput.newPayloadRequestRoot.slot/.blockRoot/.blockHash/.proofIdreferencesTypes (
eip8025/)SignedExecutionProof,PublicInput,ProofTypetypesSlotimport from sszTypesTests
TODOs (follow-up)
newPayloadRequestRoot → blockRootmapping during block importNote
This PR was authored with AI assistance (Lodekeeper 🌟)