Skip to content

fix: update consuming code for spec-aligned ExecutionProof types#9109

Draft
lodekeeper wants to merge 6 commits intoChainSafe:optional-proofsfrom
lodekeeper:fix/spec-aligned-execution-proof-types
Draft

fix: update consuming code for spec-aligned ExecutionProof types#9109
lodekeeper wants to merge 6 commits intoChainSafe:optional-proofsfrom
lodekeeper:fix/spec-aligned-execution-proof-types

Conversation

@lodekeeper
Copy link
Copy Markdown
Contributor

Problem

Commit 7f5219861d (feat: implement eip-8025 spec aligned ssz types) updated the ExecutionProof SSZ type to match the EIP-8025 spec but didn't update the ~10 consuming files in beacon-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)

  • Reindexed by (newPayloadRequestRoot, proofType) instead of (slot, blockRoot, proofId)
  • Size-based pruning replaces slot-based pruning (proofs no longer carry slot)
  • New getAllProofs() method for API listing and reqresp fallback

Verifier (executionProofVerifier.ts)

  • Removed expectedBlockRootHex / expectedExecBlockHashHex from input (not in new type)
  • Simplified to check proofData non-empty + count distinct proofTypes

Chain (chain.ts)

  • Fixed duplicate @lodestar/fork-choice imports (merge artifact)
  • Added newPayloadRequestRootToBlockRoot mapping for proof → block lookups
  • maybeTransitionToValidOnProofArrival takes ExecutionProof directly

Interface (interface.ts)

  • Updated IBeaconChain.maybeTransitionToValidOnProofArrival signature

Network/Gossip/API/ReqResp

  • All handlers updated to use proof.proofType, proof.publicInput.newPayloadRequestRoot
  • Removed all .slot / .blockRoot / .blockHash / .proofId references

Types (eip8025/)

  • Exported SignedExecutionProof, PublicInput, ProofType types
  • Removed unused Slot import from sszTypes

Tests

  • Rewrote pool + verifier tests for new type structure

TODOs (follow-up)

  • Populate newPayloadRequestRoot → blockRoot mapping during block import
  • Update reqresp ByRoot handler to use the mapping (currently returns all proofs as workaround)
  • Full gossip validation per spec (signature check, validator active check, etc.)

Note

This PR was authored with AI assistance (Lodekeeper 🌟)

lodekeeper and others added 6 commits March 24, 2026 19:42
## 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>
@lodekeeper lodekeeper requested a review from a team as a code owner March 25, 2026 13:40
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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 ExecutionProof SSZ type. The changes involve a comprehensive refactoring across the beacon node to correctly integrate the new EIP-8025 spec-aligned proof structure. The core impact is a modernized and more compliant handling of execution proofs, ensuring the system correctly processes and validates these critical components of the consensus mechanism.

Highlights

  • ExecutionProof Type Alignment: Updated all consuming code to align with the new EIP-8025 spec-aligned ExecutionProof SSZ type, which changed from { proofId, slot, blockHash, blockRoot, proofData } to { proofData, proofType, publicInput: { newPayloadRequestRoot } }.
  • Execution Proof Pool Refactor: Reindexed the ExecutionProofPool by (newPayloadRequestRoot, proofType) instead of (slot, blockRoot, proofId), and replaced slot-based pruning with size-based pruning, adding a new getAllProofs() method.
  • Execution Proof Verifier Simplification: Simplified the ExecutionProofVerifier by removing expectedBlockRootHex and expectedExecBlockHashHex from its input, now focusing on checking proofData non-emptiness and distinct proofType count.
  • Chain Logic Updates: Introduced a newPayloadRequestRootToBlockRoot mapping in the BeaconChain for proof-to-block lookups and updated maybeTransitionToValidOnProofArrival to directly accept the new ExecutionProof type.
  • Network and API Adaptations: Adjusted all network, gossip, and API handlers to utilize the new proof.proofType and proof.publicInput.newPayloadRequestRoot fields, removing references to deprecated slot, blockRoot, blockHash, and proofId.
  • Test Suite Modernization: Rewrote the pool and verifier test suites to reflect the updated ExecutionProof type structure and associated logic changes.

🧠 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 Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

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

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: 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".

Comment on lines +289 to 293
const proofs = chain.executionProofPool?.getAllProofs() ?? [];
const verifier = chain.executionProofVerifier ?? defaultZkvmExecutionProofVerifier;
const verification = verifier.verifyProofs({
proofs,
expectedBlockRootHex: blockRootHex,
expectedExecBlockHashHex: execBlockHash,
minProofsRequired: minRequired,
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 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment on lines +55 to +56
if (this.proofsByRoot.size >= MAX_ROOT_ENTRIES && !this.proofsByRoot.has(rootHex)) {
return InsertOutcome.Old;
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 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment on lines +1565 to +1567
const blockRootHex = this.newPayloadRequestRootToBlockRoot.get(newPayloadRequestRootHex);
if (blockRootHex == null) {
this.logger.debug("Cannot transition block to valid: no blockRoot mapping for newPayloadRequestRoot", {
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 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This function is missing its JSDoc. The documentation block that was previously for maybeTransitionToValidOnProofArrival (now at lines 1542-1545) has been orphaned by the refactoring. Please move it here to properly document this function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch on the orphaned JSDoc. Will fix when EIP-8025 work resumes — this PR is currently on hold.

@nflaig nflaig marked this pull request as draft March 26, 2026 16:40
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.

3 participants