EIP-8025 Optional execution proofs
Hegota Declined for Inclusion · Networking & nodes · EIP status Draft · created 2025-09-17
An optional feature that lets consensus layer nodes confirm validity through cryptographic proofs, without re-executing the execution payload themselves. Participation is voluntary and the consensus rules do not change.
Why it is needed
Today, verifying a beacon block requires re-executing the execution payload inside it against the full execution layer (EL) state. This needs all account and storage data, and the cost of re-execution grows in proportion to the gas limit. As a result, the hardware a node needs is tied to the chain's throughput and state size. As the gas limit rises or the state grows, the hardware threshold for becoming a validator rises with it. This EIP separates nodes that produce proofs from nodes that check proofs. It opens a path for the checking side to confirm payload validity in constant time without EL state. However, because proof systems are still maturing, the EIP does not make them mandatory from the start. Instead, only nodes that want to turn them on do so, building operating experience and metrics on a real network. There is no reward for proof-generating nodes, so participation is considered voluntary.
What changes
Today
- Verifying a beacon block requires re-executing the execution payload against the full EL state
- Re-execution cost grows in proportion to the gas limit, so node requirements are tied to throughput and state size
- There is no standard path for exchanging execution proofs on the consensus layer network
After EIP-8025
- Active validators who choose to do so act as provers, producing execution proofs and propagating them over the consensus layer p2p network
- Proof-checking nodes confirm payload validity from the proof alone, in constant time, without EL state
- In this phase, checking nodes keep re-executing as well, and verified proofs are used only as a supplementary signal
- Validators that do not participate see no change in behavior, bandwidth, or attestation duties
Who is affected
- Validators and node operators: Participation is optional. Only nodes that turn on proof-generation mode or proof-checking mode subscribe to the new gossip topic and advertise the eproof field in their ENR. The only thing that changes is local resource usage.
- Consensus and execution client teams: Consensus clients must implement the ProofEngine interface, the execution_proof gossip topic, and three req/resp protocols. Execution clients act as the host and produce the execution witness and StatelessInput.
- Proof-generating nodes: A voluntary role with no reward. They must hold the full EL state and proof costs are high, so the barrier to participation is higher than for checking nodes.
- Regular users and app developers: Consensus rules and transaction processing do not change, so there is no change they will notice directly.
In depth
Two optional modes and the supplementary signal principle
A node can turn on proof-generation mode, proof-checking mode, or both. In generation mode, an active validator acts as a prover, produces execution proofs and publishes them on the execution_proof gossip topic. In checking mode, the node receives the propagated proofs and uses them as a supplementary signal for the validity of the corresponding payload. In this EIP phase, checking nodes also keep re-executing payloads, and verified proofs do not replace re-execution. The design keeps fork choice independent of proofs so that the proof stack can be measured under real network conditions. Proof generation and checking use the execution verification window introduced by EIP-7732. Because execution verification is deferred until the next beacon block, there is more time to produce proofs. There is no reward for proof-generating nodes, so the role is considered altruistic. In addition, independent of the request, generation and checking flow, proof-aware nodes must store proofs for canonical chain blocks up to the latest finalized checkpoint so that syncing peers can backfill. The source says only that a follow-up EIP may propose making proofs mandatory and removing re-execution.
Proof containers and constants
The top level of the gossip message is SignedExecutionProof, which consists of three fields: message (ExecutionProof), validator_index, and signature. ExecutionProof is made up of proof_data (ByteList[MAX_PROOF_SIZE]), proof_type (uint8), and public_input. PublicInput holds new_payload_request_root, successful_validation, chain_id (uint64), and schema_id (uint16). new_payload_request_root is the SSZ hash_tree_root of the Engine API NewPayloadRequest, and it is the link that ties a proof to a specific payload. A checker must verify that successful_validation is True. chain_id and schema_id prevent cross-chain replay and fork mismatch. The constants are MAX_PROOF_SIZE = 409600 (400 KiB), MAX_EXECUTION_PROOFS_PER_PAYLOAD = uint64(4), and DOMAIN_EXECUTION_PROOF = DomainType('0x0D000000'). 400 KiB was chosen as a network bandwidth budget. Even if all 4 proof types per payload are filled, the worst case per slot is 4 × 400 KiB = 1.6 MiB, similar in scale to existing blob sidecar traffic. Systems that produce larger proofs must compress them with recursion or a succinct wrapper before propagation. The proof_type value is a per-node dynamic setting, and a new proof system is added by announcing a new ProofType value out of band and deploying it to operators who agree.
ProofEngine interface
ProofEngine is an interface that wraps proof-system-specific logic so the consensus layer does not need to know which proof system is in use. Its API is modeled on the Engine API: just as ExecutionEngine delegates payload execution to the EL client, cryptographic work is delegated to an external proof node. There are four operations. verify_execution_proof is called for every proof that passes gossip validation and binds the result to the payload that public_input.new_payload_request_root points to. notify_new_payload and notify_forkchoice_updated are notifications that let the engine follow beacon node state: they link incoming proofs to payloads and track the canonical chain to drive proof retention and pruning. request_proofs is the operation by which a generating node passes a NewPayloadRequest and ProofAttributes(proof_types) to start asynchronous generation, and it returns new_payload_request.hash_tree_root(). The prover uses this value as the request ID to match the proof node's completion SSE event. A payload is considered proof-verified once k valid SignedExecutionProof messages have been verified for it. The value of k will be fixed before the proposal moves to the Review stage, and it is deliberately left open for now to gather real data.
Beacon chain state transition and process_execution_proof
process_block is modified to pass PROOF_ENGINE along with EXECUTION_ENGINE to process_execution_payload. After the execution engine accepts the payload, process_execution_payload calls proof_engine.notify_new_payload(NewPayloadRequest(...)) to tell the proof engine about the new payload. The newly added process_execution_proof takes a SignedExecutionProof that has passed gossip validation, checks that the prover is an active validator in the current state, verifies the BLS signature under the DOMAIN_EXECUTION_PROOF domain, and then delegates the cryptographic checks to proof_engine.verify_execution_proof. This function runs outside the beacon block state transition function and does not change on-chain state. It is an operational hook for a node to decide whether to accept a propagated proof as a signal of payload validity. Proof retention is handled by the ProofEngine, and the retention range is proof_serve_range, that is, from the finalized checkpoint to the current slot.
Gossip validation rules, req/resp, and discovery
The validation rules for the execution_proof topic follow the same [IGNORE]/[REJECT] convention as other beacon gossip topics. There are three IGNORE conditions. If the node has not yet seen the new_payload_request_root, it ignores the message, though it may queue it until the payload arrives. If a valid proof already exists for the (new_payload_request_root, proof_type) combination, it ignores the message, so only the first valid proof of each type is forwarded. If the (new_payload_request_root, proof_type, validator_index) combination is not the first, it ignores the message, giving each prover only one chance. REJECT applies when validator_index is not an active validator, when the BLS signature is wrong, when proof_data is empty or exceeds MAX_PROOF_SIZE, or when the process_execution_proof check fails, and the sender is penalized through the existing peer score. There are three req/resp messages. ExecutionProofsByRange takes (start_slot, count, proof_types) and is used for range backfill. ExecutionProofsByRoot takes a list of (block_root, List[proof_type]) identifiers and is used to look up proofs for specific blocks. ExecutionProofStatus sends and receives (block_root, slot, proof_types) to exchange the proof verification view and supported proof types, and the side that connects first must send it first. A client must answer both requests for canonical blocks within proof_serve_range, that is, [finalized_checkpoint.slot, current_slot], and if it cannot, it replies with 3: ResourceUnavailable and may be penalized. Discovery adds a uint8 ENR key, eproof, and a peer is treated as proof-aware if the value is present and nonzero. This works like the cgc field in Fulu, so advertising can be turned on and off without a fork or a fork version change.
Execution layer: StatelessInput and the guest program
The prover runs the guest on the private input StatelessInput and produces the public output StatelessValidationResult. StatelessInput consists of new_payload_request, witness, and chain_id (uint64), and uses progressive containers and lists. The fixed bounds are MAX_EXTRA_DATA_BYTES = 32, MAX_WITNESS_HEADERS = 256, MAX_BYTES_PER_CODE = 2**16, MAX_BYTES_PER_HEADER = 2**10, and MAX_BYTES_PER_WITNESS_NODE = 2**10. In ExecutionWitness, state holds the raw RLP-encoded account and storage trie nodes needed for execution and state root recomputation, codes holds the bytecode read from the pre-state (excluding code deployed in the same block), and headers holds the RLP-encoded ancestor headers in block number order, ending at the payload's parent. The parent header is always included, and older headers go back as far as the oldest block returned by BLOCKHASH. The guest checks that the headers form a contiguous chain and uses their hashes to serve BLOCKHASH. The entry point run_stateless_guest decodes the SSZ input with its schema prefix, runs verify_stateless_new_payload, and serializes the result in every case. If decoding itself fails, it outputs a failure result in which the request root, chain_id, and schema_id are all zero, so a schema_id of 0 indicates that case. If validation fails after decoding, the decoded values are kept. WitnessState replaces the local pre-state DB by serving account, storage, and code reads from the witness, and it computes the post-state root from the block diff. The absence of a key is accepted only when the witness contains a trie path that proves the absence, and if a node on the path or a bytecode is missing or corrupted, the whole validation fails. The Amsterdam guest in the reference implementation does not compare the timestamp with fork activation, but production implementations must do so.
Host-side input construction and the SSZ schema ID
The host (usually a stateful EL client) records the account, storage, code, and ancestor header data it uses while executing or building a block. build_stateless_input bundles the ExecutionWitness, ExecutionRequests, the block access list, and chain_id into a StatelessInput. The execution witness is built from the per-block read/write tracker that the EL already uses to build the block access list (BAL), together with pre-state trie data. The host records trie nodes using the same lookups the guest uses when reading, and both sides apply inserts and updates first and deletions afterward in each storage trie. A deletion can collapse a branch, and then the remaining sibling node is needed. Doing deletions last reduces collapses, and because both sides write in the same order, the guest never looks for a sibling node the host did not record. build_execution_witness finally asserts that mpt_root equals the expected post-state root. On serialization, a 2-byte schema ID is prepended to the SSZ bytes. The high byte is the fork (Amsterdam = 0x15) and the low byte is the schema revision, so Amsterdam revision 1 is 0x1501. The guest must accept only the canonical SSZ encoding, and the reference decoder re-encodes the decoded value and rejects the input if the bytes are not exactly the same. This check catches offset gaps and unread trailing bytes, even inside nested containers. Conformance tests must be run with the blockchain_test_engine fixture, not blockchain_test. Because StatelessInput wraps the Engine API NewPayloadRequest, the engine fixture covers more invalid payload variants.
Security considerations and current status
A forged proof is not connected to fork choice or any other consensus rule, so it cannot fork the chain or slash a validator, and its effect is limited to the checking node's local validity view. Proof generation and checking sit outside the attestation hot path. Validators must not delay block validation or attestation production while waiting for a proof, and if a proof is missing or late they follow the fork choice determined by the execution engine's re-execution. An asymmetry between checkers and provers is also pointed out. The bar for checking drops, but a prover must hold the full EL state and run a proving stack whose cost grows as O(n log² n) in the witness operation size n, so if check-only nodes grow faster than proving-capable nodes, the parties that keep state and generate proofs could become more concentrated. On the gossip side, size is bounded by MAX_PROOF_SIZE, and the same validation, DoS-protection rate limits, and peer scoring as other CL topics apply. This EIP is in Draft status, so its content may change, and some items, such as the value of k, are not yet decided. In the fork schedule, it was declined for inclusion (DFI) in the Hegota candidate list at 10.15 ACDC #189. Support from the CL teams fell short of the CFI bar, so the experiment continues on a separate branch. The earlier 09.16 AMA #15 and 10.01 ACDC #188 had discussion only and no decision. The day after the rejection, critics said that ready-made work with no consensus change and therefore no risk had been thrown away, while the counterargument was that early adoption bypasses deliberation and that this belongs in an informational EIP rather than a core EIP for a hard fork.