Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f45e6ce
initial eip 8025 proposal
frisitano Jan 11, 2026
18bff2d
add new files
frisitano Jan 11, 2026
d046541
remove engine API and introdue prover
frisitano Jan 12, 2026
d833689
update proof ordering logic
frisitano Jan 12, 2026
a7136ef
update proof store
frisitano Jan 12, 2026
ab94ca9
chore: run lints
frisitano Jan 12, 2026
e9a8f8e
refactor: introduce proof engine
frisitano Jan 13, 2026
27514cb
split specs by fork base
frisitano Jan 13, 2026
d5e64ff
clean up
frisitano Jan 13, 2026
089b17d
add proof engine
frisitano Jan 13, 2026
401b25f
remove eip8025 gloas
frisitano Jan 13, 2026
b11df60
fix typo
frisitano Jan 13, 2026
aacb0e1
lint
frisitano Jan 14, 2026
35bf664
lint
frisitano Jan 15, 2026
30efa02
remove get_proofs proof engine method
frisitano Jan 15, 2026
4d45977
lint
frisitano Jan 15, 2026
4885119
address PR comments
frisitano Jan 17, 2026
9bf6076
Fix table of contents (my mistake)
jtraglia Jan 22, 2026
b08d0ce
Fix various nits
jtraglia Jan 22, 2026
1d8e7e3
Rename ProverSignedExecutionProof to SignedExecutionProof
jtraglia Jan 22, 2026
23b0334
Reorganize a bit
jtraglia Jan 22, 2026
6bdb7d0
Nits
jihoonsong Jan 23, 2026
7fd6b51
Update specs/_features/eip8025/beacon-chain.md
frisitano Jan 23, 2026
eaa9c23
Remove prover whitelist
jtraglia Jan 23, 2026
aae1f78
Fix nit
jtraglia Jan 23, 2026
614679c
Clarify gossip conditions
jtraglia Jan 23, 2026
b746a88
Trim ToC
jihoonsong Jan 24, 2026
f29f71d
Merge branch 'master' into feat/eip-8025-design
jtraglia Jan 30, 2026
be1a7e0
Run make lint
jtraglia Jan 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 135 additions & 105 deletions specs/_features/eip8025/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,49 @@

- [Table of contents](#table-of-contents)
- [Introduction](#introduction)
- [Types](#types)
- [Constants](#constants)
- [Execution](#execution)
- [Domains](#domains)
- [Configuration](#configuration)
- [Containers](#containers)
- [New containers](#new-containers)
- [`ExecutionProof`](#executionproof)
- [`SignedExecutionProof`](#signedexecutionproof)
- [Extended containers](#extended-containers)
- [Helpers](#helpers)
- [Execution proof functions](#execution-proof-functions)
- [`verify_execution_proof`](#verify_execution_proof)
- [`verify_execution_proofs`](#verify_execution_proofs)
- [New `PublicInput`](#new-publicinput)
- [New `ExecutionProof`](#new-executionproof)
- [New `SignedExecutionProof`](#new-signedexecutionproof)
- [Beacon chain state transition function](#beacon-chain-state-transition-function)
- [Execution payload processing](#execution-payload-processing)
- [Modified `process_execution_payload`](#modified-process_execution_payload)
- [Block processing](#block-processing)
- [Modified `process_block`](#modified-process_block)
- [Execution payload](#execution-payload)
- [New `NewPayloadRequestHeader`](#new-newpayloadrequestheader)
- [Modified `process_execution_payload`](#modified-process_execution_payload)
- [Execution proof](#execution-proof)
- [New `process_execution_proof`](#new-process_execution_proof)

<!-- mdformat-toc end -->

## Introduction

These are the beacon-chain specifications to add EIP-8025. This enables
stateless validation of execution payloads through cryptographic proofs.
These are the beacon-chain specifications to add EIP-8025, enabling stateless
validation of execution payloads through execution proofs.

*Note*: This specification is built upon [Fulu](../../fulu/beacon-chain.md).
*Note*: This specification is built upon [Fulu](../../fulu/beacon-chain.md) and
imports proof types from [proof-engine.md](./proof-engine.md).

*Note*: This specification assumes the reader is familiar with the
[public zkEVM methods exposed](./zkevm.md).
## Types

| Name | SSZ equivalent | Description |
| ----------- | -------------- | --------------------------------- |
| `ProofType` | `uint8` | The type identifier for the proof |

## Constants

### Execution

| Name | Value |
| ---------------------------------- | -------------------------------------- |
| `MAX_EXECUTION_PROOFS_PER_PAYLOAD` | `uint64(4)` |
| `PROGRAM` | `ProgramBytecode(b"DEFAULT__PROGRAM")` |
*Note*: The execution values are not definitive.

| Name | Value |
| ---------------- | ------------------- |
| `MAX_PROOF_SIZE` | `307200` (= 300KiB) |

### Domains

Expand All @@ -56,110 +62,83 @@ stateless validation of execution payloads through cryptographic proofs.

*Note*: The configuration values are not definitive.

| Name | Value |
| ------------------------------- | ----------- |
| `MIN_REQUIRED_EXECUTION_PROOFS` | `uint64(1)` |
| Name | Value |
| ------------------------- | ------------- |
| `MAX_WHITELISTED_PROVERS` | `uint64(256)` |

## Containers

### New containers
### New `PublicInput`

```python
class PublicInput(Container):
new_payload_request_root: Root
```

#### `ExecutionProof`
### New `ExecutionProof`

```python
class ExecutionProof(Container):
beacon_root: Root
zk_proof: ZKEVMProof
validator_index: ValidatorIndex
proof_data: ByteList[MAX_PROOF_SIZE]
proof_type: ProofType
public_input: PublicInput
```

#### `SignedExecutionProof`
### New `SignedExecutionProof`

```python
class SignedExecutionProof(Container):
message: ExecutionProof
prover_pubkey: BLSPubkey
signature: BLSSignature
```

### Extended containers

*Note*: `BeaconState` and `BeaconBlockBody` remain the same. No modifications
are required for execution proofs since they are handled externally.
## Beacon chain state transition function

## Helpers
### Block processing

### Execution proof functions
#### Modified `process_block`

#### `verify_execution_proof`
*Note*: `process_block` is modified in EIP-8025 to pass `PROOF_ENGINE` to
`process_execution_payload`.

```python
def verify_execution_proof(
signed_proof: SignedExecutionProof,
parent_hash: Hash32,
block_hash: Hash32,
state: BeaconState,
el_program: ProgramBytecode,
) -> bool:
"""
Verify an execution proof against a payload header using zkEVM verification.
"""

# Note: signed_proof.message.beacon_root verification will be done at a higher level

# Verify the validator signature
proof_message = signed_proof.message
validator = state.validators[proof_message.validator_index]
signing_root = compute_signing_root(proof_message, get_domain(state, DOMAIN_EXECUTION_PROOF))
if not bls.Verify(validator.pubkey, signing_root, signed_proof.signature):
return False

# Derive program bytecode from the EL program identifier and proof type
program_bytecode = ProgramBytecode(
el_program + proof_message.zk_proof.proof_type.to_bytes(1, "little")
)

return verify_zkevm_proof(proof_message.zk_proof, parent_hash, block_hash, program_bytecode)
def process_block(state: BeaconState, block: BeaconBlock) -> None:
process_block_header(state, block)
process_withdrawals(state, block.body.execution_payload)
# [Modified in EIP8025]
process_execution_payload(state, block.body, EXECUTION_ENGINE, PROOF_ENGINE)
process_randao(state, block.body)
process_eth1_data(state, block.body)
process_operations(state, block.body)
process_sync_aggregate(state, block.body.sync_aggregate)
```

#### `verify_execution_proofs`
#### Execution payload

##### New `NewPayloadRequestHeader`

```python
def verify_execution_proofs(parent_hash: Hash32, block_hash: Hash32, state: BeaconState) -> bool:
"""
Verify that execution proofs are available and valid for an execution payload.
"""
# `retrieve_execution_proofs` is implementation and context dependent.
# It returns all execution proofs for the given payload block hash.
signed_execution_proofs = retrieve_execution_proofs(block_hash)

# Verify there are sufficient proofs
if len(signed_execution_proofs) < MIN_REQUIRED_EXECUTION_PROOFS:
return False

# Verify all execution proofs
for signed_proof in signed_execution_proofs:
if not verify_execution_proof(signed_proof, parent_hash, block_hash, state, PROGRAM):
return False

return True
@dataclass
class NewPayloadRequestHeader(object):
execution_payload_header: ExecutionPayloadHeader
versioned_hashes: Sequence[VersionedHash]
parent_beacon_block_root: Root
execution_requests: ExecutionRequests
```

## Beacon chain state transition function

### Execution payload processing
##### Modified `process_execution_payload`

#### Modified `process_execution_payload`
*Note*: `process_execution_payload` is modified in EIP-8025 to require both
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.

Wasn't the whole purpose of execution proofs to let lightweight nodes run without an execution client (or at least with a stateless one)?

If so, why require both an ExecutionEngine and a ProofEngine for a lightweight client?
Conversely, EIP-8025 is titled "Optional Execution Proofs."
Yet with this PR, all nodes, including stateful, classic ones, will need execution proofs to import a block.

The EIP states:

Optional execution proofs allow beacon nodes to verify the validity of the execution payload within a beacon block without running an execution layer client.

This PR seems to make an EL mandatory for all node types.

Copy link
Copy Markdown
Member

@jtraglia jtraglia Jan 23, 2026

Choose a reason for hiding this comment

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

I believe these specs represent what full nodes & validators would implement. There could be another node type for lightweight verifiers which are not connected to an EL client.

Edit: Yeah, see the "Proof Verification Flow" diagram in the PR's description.

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.

I wonder if we should add back a stateless validation flag like on master here to either verify using the execution engine or veify a proof using the proof engine @frisitano ?

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.

The high-level idea was that a node can be spun up with a ProofEngine, an ExecutionEngine, or both. If we only have a ProofEngine, then the ExecutionEngine invocation would be a no-op. Similarly, if we only have an ExecutionEngine, then the ProofEngine call would be a no-op.

I agree that this spec doesn't capture this construct well and isn't clear. In regard to options as I see them:

  1. Introduce the stateless validation flag as @kevaundray suggested (I think this exposes implementation details to the spec, which isn't ideal)
  2. Make this clear in the specs via comments
  3. Revert process_execution_payload back to default, and instead add a comment to ExecutionEngine::verify_and_notify_new_payload that it should invoke the ProofEngine (if configured) as well as an external re-execution node (if configured).
  4. Instead of introducing the ProofEngine, we extend the ExecutionEngine with methods for proofs and leave the method implementations as an implementation detail.

I tend to be in favour of 3 or 4 but keen to hear others' opinions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think replacing ExecutionEngine with ProofEngine would be the ideal especially in terms of forward-compatibility. process_execution_payload can receive proof engine instead of execution engine, and replace the call to execution engine with proof engine.

What do people feel about this?

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.

Yes, that sounds like a good approach. I agree.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(Bridging my comment from Discord)

On the actual implementation, clients can expose a flag to switch to EIP-8025. But on the spec side, we can remove payload execution (and maybe other execution engine stuff, if it makes sense)

`ExecutionEngine` and `ProofEngine` for validation.

```python
def process_execution_payload(
state: BeaconState,
body: BeaconBlockBody,
execution_engine: ExecutionEngine,
stateless_validation: bool = False,
proof_engine: ProofEngine,
) -> None:
"""
Note: This function is modified to support optional stateless validation with execution proofs.
"""
payload = body.execution_payload

# Verify consistency of the parent hash with respect to the previous execution payload header
Expand All @@ -174,24 +153,48 @@ def process_execution_payload(
<= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block
)

if stateless_validation:
# Stateless validation using execution proofs
assert verify_execution_proofs(payload.parent_hash, payload.block_hash, state)
else:
# Compute list of versioned hashes
versioned_hashes = [
kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments
]

# Verify the execution payload is valid
assert execution_engine.verify_and_notify_new_payload(
NewPayloadRequest(
execution_payload=payload,
versioned_hashes=versioned_hashes,
parent_beacon_block_root=state.latest_block_header.parent_root,
execution_requests=body.execution_requests,
)
# Compute list of versioned hashes
versioned_hashes = [
kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments
]

# Verify the execution payload is valid via ExecutionEngine
assert execution_engine.verify_and_notify_new_payload(
NewPayloadRequest(
execution_payload=payload,
versioned_hashes=versioned_hashes,
parent_beacon_block_root=state.latest_block_header.parent_root,
execution_requests=body.execution_requests,
)
)

# [New in EIP8025]
# Verify via ProofEngine
new_payload_request_header = NewPayloadRequestHeader(
execution_payload_header=ExecutionPayloadHeader(
parent_hash=payload.parent_hash,
fee_recipient=payload.fee_recipient,
state_root=payload.state_root,
receipts_root=payload.receipts_root,
logs_bloom=payload.logs_bloom,
prev_randao=payload.prev_randao,
block_number=payload.block_number,
gas_limit=payload.gas_limit,
gas_used=payload.gas_used,
timestamp=payload.timestamp,
extra_data=payload.extra_data,
base_fee_per_gas=payload.base_fee_per_gas,
block_hash=payload.block_hash,
transactions_root=hash_tree_root(payload.transactions),
withdrawals_root=hash_tree_root(payload.withdrawals),
blob_gas_used=payload.blob_gas_used,
excess_blob_gas=payload.excess_blob_gas,
),
versioned_hashes=versioned_hashes,
parent_beacon_block_root=state.latest_block_header.parent_root,
execution_requests=body.execution_requests,
)
assert proof_engine.verify_new_payload_request_header(new_payload_request_header)
Copy link
Copy Markdown
Member

@jihoonsong jihoonsong Jan 23, 2026

Choose a reason for hiding this comment

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

If I'm not wrong, the right modifications here at the high-level are:

  • pass proofs received so far (i.e., change process_execution_payload's signature)
  • extract NewPayloadRequestHeader
  • assert hash_tree_root(new_payload_request_header) == proof.new_payload_request_header_root
  • call proof engine's verify_execution_proof.

This basically merges process_prover_signed_execution_proof into process_execution_payload. But the tricky part is that you should have enough proofs before calling this. So we would need some assertion like this assert len(proofs) >= N (I'm thinking out loud here).

One easy solution comes to me is, we introduce a deadline for proofs, which is set before the block's deadline. e.g., proofs deadline is 7s while block deadline is 8s on top of Fulu.

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.

I was thinking this could be done async and left as an implementation detail (left unspecified). If proofs do not arrive before proof_engine.verify_new_payload_request_header is invoked, then it should return SYNCING, and the block should be added to the forkchoice store optimistically. Then, upon proof arrival when len(proofs) >= N is satisfied, the proof engine will return something like Result { valid: Valid, valid_root: Some(root) } and there is a call on the fork choice store to update the payload status associated with the valid_root. This allows async processing of blocks and proofs. I believe this somewhat mirrors the pattern in ePBS.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you explain what does verify_new_payload_request_header do and why do we need it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It must be specified, especially if it touches fork choice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe this somewhat mirrors the pattern in ePBS.

I don't follow this at all. ePBS is not underspecified with regard to block process and fork choice from my understanding.

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.

verify_new_payload_request_header just checks len(execution_proofs) > threshold in the ProofEngine.

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.

By the way I've written up some considerations on consensus for fulu and gloas here - https://hackmd.io/@frisitano/BJ9P2IpQ-e#Consensus-Considerations

Copy link
Copy Markdown
Member

@jihoonsong jihoonsong Jan 23, 2026

Choose a reason for hiding this comment

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

Ah, is it because proofs are stored in ProofEngine? Then, the storing function should also be specified. But to me, I'd prefer to store proofs in the CL, rather than making ProofEngine stateful. You can store it in an implementation dependent cache and still avoid hard fork. You can refer to InclusionListStore.

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.

This makes sense. I will introduce this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry, multiple factors came up to my mind. Please wait a bit. Let me think more about this.


# Cache execution payload header
state.latest_execution_payload_header = ExecutionPayloadHeader(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can reuse ExecutionPayloadHeader that we constructed when calling verify_new_payload_request_header.

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.

Expand All @@ -214,3 +217,30 @@ def process_execution_payload(
excess_blob_gas=payload.excess_blob_gas,
)
```

### Execution proof

*Note*: Proof storage is implementation-dependent, managed by the `ProofEngine`.

#### New `process_execution_proof`

```python
def process_execution_proof(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I also think k-out-of-n policy should be described somewhere in the spec.

state: BeaconState,
signed_proof: SignedExecutionProof,
proof_engine: ProofEngine,
) -> None:
proof_message = signed_proof.message
prover_pubkey = signed_proof.prover_pubkey

# Verify prover is whitelisted
validator_pubkeys = [v.pubkey for v in state.validators]
assert prover_pubkey in validator_pubkeys

domain = get_domain(state, DOMAIN_EXECUTION_PROOF, compute_epoch_at_slot(state.slot))
signing_root = compute_signing_root(proof_message, domain)
assert bls.Verify(prover_pubkey, signing_root, signed_proof.signature)

# Verify the execution proof
assert proof_engine.verify_execution_proof(proof_message)
```
Loading