Execution proofs enable consensus layer (CL) nodes to perform stateless validation of execution payloads. Altruistic proof-generating nodes generate execution proofs and broadcast them over the CL p2p network. Proof-verifying nodes consume gossiped execution proofs and check payload validity by verifying those proofs. Verification is stateless and constant time with respect to the payload’s gas limit and EL state size, so a validator’s payload-verification requirements are decoupled from the linear cost of re-execution. The mechanism is fully opt-in and does not change consensus validity rules, allowing the proving stack to mature in production; a separate, future EIP could subsequently propose making execution proofs mandatory once they have matured. This EIP does not introduce incentives for proof-generating nodes; for that reason, they are considered altruistic and the mechanism is opt-in.
Motivation
Today, verifying a beacon block requires re-execution of its execution payload against the execution layer (EL) state — the full set of account and storage trie data. The cost of re-execution scales linearly with the gas limit. This couples a node’s resource requirements to the state size and the chain’s throughput.
This EIP introduces an opt-in path with the following properties:
Constant-time payload verification. Once an execution proof is available, a proof-verifying node checks payload validity by verifying the proof. Proof verification is constant time with respect to the payload’s gas limit and EL state size.
Stateless verification. Verifying an execution proof requires only the proof itself and a public commitment to the payload — not the EL state.
Broader verifier participation. Constant-time, stateless payload verification lowers the resource floor for proof-verifying validators, keeping payload-validity checks tractable for participants with modest hardware and expertise even as the gas limit and state grow.
Optional by design. Deploying execution proofs as opt-in lets clients and operators build operational experience, collect performance metrics, and fine-tune the proving stack against live-network conditions without enshrining untested mechanics in the protocol or impacting validators that have not opted in.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Terminology
This EIP uses the following proof-system terms:
Proof system. The cryptographic system used to prove that a computation was performed correctly. Different proof systems may have different proof formats, verifier logic, proving costs, and security assumptions.
Prover. A node that runs the execution-validation computation and generates an execution proof for the result. In this EIP, provers are opt-in, altruistic proof-generating validators.
Verifier. A node that checks an execution proof instead of re-executing payload validation.
Guest. The program whose execution is being proven. Here, the guest program performs stateless validation of an execution payload.
Host. The off-chain logic that prepares the guest input, runs the guest inside the proof system, and packages the resulting proof.
Private input. Data supplied to the guest by the prover but not revealed to verifiers. In this EIP, the serialised StatelessInput is private input: it contains the data needed to run stateless payload validation.
Execution Witness. The execution data within the private input that lets the guest validate the payload without the verifier holding the full EL state.
Public input. The data committed to by the proof and visible to verifiers. In this EIP, the public input binds the proof to a specific NewPayloadRequest, chain configuration, and validation result.
Proof node. An external component that performs proof-system-specific work such as proof generation and proof verification.
Proof engine. The implementation-dependent consensus-client protocol that encapsulates proof-subsystem state, generation, and verification. PROOF_ENGINE is the reference used in the consensus specifications for the client-provided instance of ProofEngine.
Proof-aware peer. A consensus-layer peer that advertises optional execution-proof support and participates in the execution-proof networking protocols for its supported proof types.
Server-Sent Events (SSE). An HTML standard for one-way, long-lived HTTP event streams from server to client. In this EIP, SSE is used in two places: by the beacon node to push Block events to the prover, and by the proof node to push proof-completion events to the prover.
Specification references
The normative specifications for this EIP are maintained alongside the rest of the protocol in the consensus-specs and execution-specs repositories. The sections below summarise the changes; the canonical sources are:
This EIP introduces a new execution_proof gossip topic on the
consensus-layer p2p network carrying SignedExecutionProof messages,
and a ProofEngine interface that beacon nodes use to generate and
verify execution proofs. Proof generation and
verification are delegated to an external proof node; the ProofEngine
is the in-client component that mediates communication with it. A node
MAY enable either or both of:
Proof-generating mode. An active validator that opts in to act as
a prover — generates execution proofs and broadcasts them on the
execution_proof gossip topic.
Proof-verifying mode. A node that consumes gossiped proofs and
treats a verified proof as a supplementary validity signal for the
corresponding payload’s validity. Proof verification does not require local
EL state. Proof-verifying nodes still re-execute payloads themselves;
verified proofs are an additional signal, not a replacement for
re-execution. This separation lets clients deploy and measure the
proving stack against live-network conditions without making
fork-choice dependent on it. A subsequent EIP could propose promoting
verified proofs to a load-bearing role and dropping re-execution once
the stack has matured.
The following diagram summarises how proofs move between the two roles:
At a high level, the proof lifecycle is:
A prover reacts to a new beacon block, constructs the corresponding
NewPayloadRequest, and calls
proof_engine.request_proofs(new_payload_request, ProofAttributes(proof_types))
to initiate proof generation for its configured proof_types on the proof node.
The proof node produces an ExecutionProof for each requested type.
The prover signs each proof, wraps it in a SignedExecutionProof, and
broadcasts it on the execution_proof topic.
Proof-verifying peers receive the gossip message, run the gossip
validation rules, then call proof_engine.verify_execution_proof on
the inner ExecutionProof to confirm payload validity.
This lifecycle uses the execution-validation window introduced by
EIP-7732, which defers execution validation until the next
beacon block and provides additional time for proof generation.
Sync support requirement. Independent of the request/produce/verify
loop above, every proof-aware node MUST retain valid proofs for
canonical-chain blocks back to the finalised checkpoint so that peers
syncing or filling gaps can backfill them via ExecutionProofsByRange
and ExecutionProofsByRoot (see Req/resp domain for
the protocol details).
Proof types and containers
The consensus layer introduces a small set of new types and constants to
identify proofs, bound their size, and route gossip signatures:
Name
Value
ProofType
uint8
MAX_PROOF_SIZE
409600 (= 400 KiB)
MAX_EXECUTION_PROOFS_PER_PAYLOAD
uint64(4)
DOMAIN_EXECUTION_PROOF
DomainType('0x0D000000')
MAX_PROOF_SIZE is set at 400 KiB as a network-bandwidth budget: at the
gossip target of one proof per (payload, proof_type) and the cap of
MAX_EXECUTION_PROOFS_PER_PAYLOAD = 4 proof types per payload, the
worst-case in-flight proof bandwidth per slot is 4 × 400 KiB = 1.6 MiB
— of a similar order to existing per-slot blob-sidecar traffic. This
bounds steady-state traffic. Provers using systems that naturally
produce larger proofs are expected to apply proof compression
(recursion, succinct wrappers) before gossiping.
Peers advertise their supported proof types via
ExecutionProofStatus (see Req/resp domain). The set
of proof_types a node speaks is a per-node, dynamic configuration; new
proof systems can be added by socialising a new ProofType value
out-of-band and rolling it out to consenting operators. Proof-system
support is local, opt-in configuration and does not change consensus
validity rules. Peers discover which proof types overlap during their
ExecutionProofStatus handshake and route requests accordingly.
The top-level gossip message is the SignedExecutionProof:
Its inner ExecutionProof couples a serialised proof blob to the proof
system and guest program it was generated against, and to the public
input that identifies the payload it certifies:
proof_data is the proof system’s serialised output. It is the only field
whose size scales with the prover output and is bounded by
MAX_PROOF_SIZE.
proof_type selects which proof system and guest program the verifier dispatches to.
public_input.new_payload_request_root is the SSZ hash_tree_root of the
Engine API NewPayloadRequest whose execution this proof certifies. It is
the link between a proof and a specific execution payload; verifiers use
it to bind the proof to the payload they are verifying.
public_input.successful_validation is True if the guest program
accepted the payload as valid. Verifiers MUST check this field is
True before treating the proof as a positive validity signal.
public_input.chain_config pins the chain ID and fork configuration the
guest was run against, preventing cross-chain replay and fork
mismatch. ChainConfig is defined in the
Execution Layer section.
validator_index and signature identify the prover and prove that they
signed the proof under DOMAIN_EXECUTION_PROOF, allowing peers to
attribute invalid proofs to a specific active validator.
Proof engine interface
The ProofEngine interface encapsulates the proof-system-specific logic
so that the consensus layer stays agnostic of which proof system is in
use. It acts as a payload-validity oracle for the beacon node, and its
API shape is modelled on the Engine API to allow for seamless
integration into the protocol. The ProofEngine is realised as an
in-client component that maintains its own proof state and delegates
the cryptographic work of proof generation and verification to an
external proof node, mirroring the way the ExecutionEngine delegates
payload execution to the EL client. The
interface comprises four operations and a ProofAttributes type:
Verification (verify_execution_proof) is called on every incoming
proof that passes gossip validation. The engine performs the
proof-system-specific cryptographic verification and binds the result
to the specific payload identified by
proof.public_input.new_payload_request_root. A payload is considered
proof-verified once k valid SignedExecutionProofs for it have been
verified. The value of k will be pinned before this EIP transitions
to Review; it is intentionally left open while the network gathers
real-world data.
Peers exchange their current proof-verification view via
ExecutionProofStatus.
Lifecycle notifications (notify_new_payload,
notify_forkchoice_updated) keep the engine in sync with the state
of the beacon node. notify_new_payload lets the engine associate
incoming proofs with the payloads they certify;
notify_forkchoice_updated lets it track the canonical chain to
drive proof retention and pruning.
Generation (request_proofs) is used by proof-generating nodes to
trigger asynchronous proof generation for a given payload and set of
proof types. The return value is new_payload_request.hash_tree_root(),
which the prover uses as a request id to correlate the proof-completion
SSE event the proof node emits when a proof is ready to be fetched.
Beacon-chain state transition
EIP-8025 extends process_block to forward execution payload processing to
both the execution engine and the proof engine, and introduces a new
process_execution_proof handler for incoming proofs.
process_block is modified to pass PROOF_ENGINE to
process_execution_payload:
defprocess_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)
process_execution_payload is modified to additionally notify the proof
engine of the new payload after the execution engine has accepted it.
This hook lets the proof engine learn of each new payload so it
can associate it with the proofs that certify it as they arrive over
gossip:
defprocess_execution_payload(state:BeaconState,body:BeaconBlockBody,execution_engine:ExecutionEngine,proof_engine:ProofEngine,)->None:...# Verify the execution payload is valid via ExecutionEngine
assertexecution_engine.verify_and_notify_new_payload(NewPayloadRequest(...))# [New in EIP8025]
# Notify ProofEngine of the new execution payload
proof_engine.notify_new_payload(NewPayloadRequest(...))...
A new process_execution_proof handles a SignedExecutionProof once it has
passed gossip validation. It re-checks that the prover is active in the
current state, verifies the BLS signature under DOMAIN_EXECUTION_PROOF,
and delegates cryptographic verification to the proof engine:
defprocess_execution_proof(state:BeaconState,signed_proof:SignedExecutionProof,proof_engine:ProofEngine,)->None:proof_message=signed_proof.message# Verify prover is an active validator
validator=state.validators[signed_proof.validator_index]assertis_active_validator(validator,get_current_epoch(state))domain=get_domain(state,DOMAIN_EXECUTION_PROOF,compute_epoch_at_slot(state.slot))signing_root=compute_signing_root(proof_message,domain)assertbls.Verify(validator.pubkey,signing_root,signed_proof.signature)# Verify the execution proof
assertproof_engine.verify_execution_proof(proof_message)
process_execution_proof is invoked outside the beacon-block state-transition
function and produces no on-chain state change: it is the operational hook
that lets a node decide whether to treat a gossiped proof as a valid
validity signal for an execution payload. Proof storage is managed by the
ProofEngine; retention is bounded by proof_serve_range (see
Req/resp domain): clients MUST retain proofs for
canonical-chain blocks
back to the finalised checkpoint so they can serve ExecutionProofsByRange
and ExecutionProofsByRoot requests.
Proof gossip
Execution proofs are gossiped over a new global topic, execution_proof,
carrying SignedExecutionProof messages. Each rule below is a libp2p
gossipsub validation rule, following the same [IGNORE] / [REJECT]
conventions used by the other beacon-node gossip topics. With
proof = signed_execution_proof.message:
[IGNORE] the proof’s new_payload_request_root has been seen via
gossip or non-gossip sources (clients MAY queue proofs until the
corresponding payload arrives).
[IGNORE] no valid proof has already been received for the tuple
(new_payload_request_root, proof_type) — i.e. only the first valid
proof of each type per payload is forwarded.
[IGNORE] this is the first proof received for the tuple
(new_payload_request_root, proof_type, validator_index) — i.e. each
prover gets one shot per (payload, proof_type).
[REJECT] the validator at validator_index is an active validator.
[REJECT] the BLS signature is valid for the prover’s public key under
DOMAIN_EXECUTION_PROOF.
[REJECT]proof.proof_data is non-empty and no larger than
MAX_PROOF_SIZE.
[REJECT] all of the checks in process_execution_proof pass.
These rules compose with the existing gossipsub peer-scoring machinery: a
peer relaying messages that hit [REJECT] is downscored by the same
mechanism that scores misbehaviour on any other CL gossip topic. The
[IGNORE] rules bound steady-state bandwidth — once a node has a valid
proof for a given (new_payload_request_root, proof_type), additional
proofs for the same tuple are dropped silently, regardless of who signs
them.
Req/resp domain
Three req/resp protocols let peers backfill, target, and coordinate proof
state:
ExecutionProofsByRange — request (start_slot, count, proof_types),
response List[SignedExecutionProof]. Mirrors BlobSidecarsByRange and
DataColumnSidecarsByRange: range-based, root-unaware, filtered by proof
type. Used for bulk backfill of proofs after a syncing window.
ExecutionProofsByRoot — request List[ProofByRootIdentifier] where
each identifier carries (block_root, List[proof_type]), response
List[SignedExecutionProof]. Used for targeted retrieval when a node
knows the specific block roots whose proofs it is missing.
ExecutionProofStatus — request and response share the same
container: (block_root, slot, proof_types). Peers exchange the most
recent block they consider proof-verified, plus their dynamically
advertised set of supported proof types. The dialing peer MUST send a
status on first connecting to any proof-aware peer. This is similar in
nature to the beacon-chain
Status v2
handshake.
Clients MUST serve both range and root requests for any block on the
canonical chain whose slot is in proof_serve_range — defined as
[finalized_checkpoint.slot, current_slot]. Peers unable to reply respond
with 3: ResourceUnavailable and MAY be descored. The shape of these
protocols composes with existing sync flows: a node syncing forward over
blocks pulls proofs by range; a node patching a specific gap pulls them by
root.
Discovery
Discovery is extended with a new Ethereum Node Record (ENR) key, eproof, encoded as a uint8. A
node is considered proof-aware if the field is present and non-zero,
allowing other peers to filter for proof-aware peers during discovery. This
mirrors the existing pattern used to advertise data-column awareness in
Fulu via the
cgc ENR field
and is intentionally cheap: no fork or fork-version bump is required for a
node to start (or stop) advertising proof support.
Prover lifecycle
The prover lifecycle is illustrated below:
A prover’s flow, as detailed in the prover guide referenced above, is
fully event-driven:
Subscribe to two SSE event streams at startup:
Block events from the beacon node, signalling that a new valid beacon
block has been received.
Proof-completion events from the proof node.
On a Block event, fetch the full BeaconBlock via RPC, construct
the corresponding NewPayloadRequest, choose the ProofAttributes
(set of proof_types the prover wants to generate), and call
proof_engine.request_proofs(...). The returned root is used to track
the request.
On a proof-completion event matching a tracked root, fetch the
completed ExecutionProof from the proof node, sign it under
DOMAIN_EXECUTION_PROOF, wrap it in a SignedExecutionProof, and
broadcast on the execution_proof gossip topic.
The prover role is scoped to the optional-proof phase. If a future EIP
were to make execution proofs mandatory, proof generation would
plausibly move into block production, at which point the prover role
and the optional gossip topic could be deprecated in favour of
in-block proof commitments.
Execution Layer
This EIP introduces a stateless execution guest and prover side logic to construct the input used by
that guest. The prover runs the guest over private StatelessInput bytes. The
proof exposes StatelessValidationResult as public output, allowing a verifier
to bind the proof to the payload request being checked.
A proof-verifying node accepts the execution-layer result only if:
the proof verifies for the expected guest program;
successful_validation is true;
new_payload_request_root equals the SSZ hash_tree_root of the
NewPayloadRequest associated with the payload being validated; and
chain_config matches the chain ID and fork configuration expected by the
verifier.
At a high level, proof generation follows this flow:
A host, usually a stateful execution-layer client, executes or constructs
the block while recording the account, storage, code, and ancestor-header
data used during execution.
The host builds an ExecutionWitness, wraps it with the Engine API
NewPayloadRequest, chain configuration, and transaction public keys into a
StatelessInput, and serialises that input as schema-prefixed SSZ bytes.
A prover runs the guest program on the serialised StatelessInput. The
guest validates the chain configuration and witness scaffolding, executes the
payload against witness-backed state, and returns StatelessValidationResult.
Stateless input and output
The top-level guest input is private prover input:
Note this struct is the EL-counterpart of PublicInput described in the Proof types and containers
section.
At this level:
new_payload_request is the Engine API payload data supplied by the
consensus layer.
witness is the account, storage, code, and ancestor-header data needed to
execute the payload without local EL state.
chain_config tells the guest which chain ID and fork configuration to
validate against.
public_keys contains one 65-byte uncompressed secp256k1 public key for
each transaction signer, in the same order as the transactions in the payload.
Optimised guests can use these keys to avoid public-key recovery, but
transaction signatures and supplied public keys MUST still be verified.
The NewPayloadRequest commits to the payload, blob versioned hashes,
parent_beacon_block_root, and typed execution requests.
The versioned_hashes field carries the versioned hashes introduced by
EIP-4844. The typed deposit, withdrawal, and consolidation
requests are specified by EIP-6110,
EIP-7002, and EIP-7251, respectively.
The block_access_list field in ExecutionPayload is the block-level access
list defined by EIP-7928.
The ExecutionWitness fields have the following roles:
state: RLP-encoded account and storage trie-node preimages needed during
execution and state-root recomputation.
codes: bytecodes required for code reads from the pre-state. Code created
in the block being executed is not included because the guest observes it
during re-execution.
headers: RLP-encoded parent and ancestor headers, ordered by block number
and ending at the payload parent. These headers provide the parent state root
and the recent block hashes used by BLOCKHASH and system-contract logic.
The guest checks that the headers form a contiguous chain.
Guest validation
The guest entry point decodes schema-prefixed SSZ input, runs stateless payload
validation, and serialises the result.
The main validation path first computes the public payload-request commitment.
It then validates fork configuration, checks the header witness, creates a
WitnessState, and delegates payload execution to the same new_payload path
used by the execution engine:
defcompute_new_payload_request_root(stateless_input:StatelessInput,)->Hash32:ssz_npr=_new_payload_request_to_ssz(stateless_input.new_payload_request)returnHash32(ssz_npr.hash_tree_root())defvalidate_chain_config(chain_config:ChainConfig,new_payload_request:NewPayloadRequest,)->ForkConfig:active_fork=chain_config.active_forkexecution_payload=new_payload_request.execution_payloadifnot_is_activation_active(active_fork.activation,execution_payload):raiseInactiveForkConfigError(...)ifactive_fork.forknotinguest_supported_forks():raiseUnsupportedForkConfigError(...)ifactive_fork.blob_schedule!=_expected_amsterdam_blob_schedule():raiseUnsupportedForkConfigError(...)returnactive_forkdefvalidate_headers(encoded_headers:Tuple[Bytes,...],)->Tuple[List[Header|PreviousForkHeader],List[Hash32]]:assertlen(encoded_headers)<=256headers=[_decode_header(header)forheaderinencoded_headers]block_hashes=[keccak256(header)forheaderinencoded_headers]foriinrange(1,len(headers)):ifheaders[i].parent_hash!=block_hashes[i-1]:raiseException("Witness headers are not contiguous")returnheaders,block_hashesdefverify_stateless_new_payload(stateless_input:StatelessInput,)->StatelessValidationResult:new_payload_request_root=compute_new_payload_request_root(stateless_input)witness=stateless_input.witnesstry:validate_chain_config(stateless_input.chain_config,stateless_input.new_payload_request,)decoded_headers,block_hashes=validate_headers(witness.headers)parent_header=decoded_headers[-1]chain_context=ChainContext(chain_id=stateless_input.chain_config.chain_id,block_hashes=block_hashes,parent_header=parent_header,)pre_state=WitnessState(_node_db=build_node_db(witness.state),_state_root=parent_header.state_root,_code_db=build_code_db(witness.codes),)execute_new_payload_request(stateless_input.new_payload_request,pre_state,chain_context,transaction_public_keys=stateless_input.public_keys,)successful_validation=TrueexceptException:successful_validation=FalsereturnStatelessValidationResult(new_payload_request_root=new_payload_request_root,successful_validation=successful_validation,chain_config=stateless_input.chain_config,)
WitnessState is the stateless replacement for a local pre-state database. It
serves account, storage, and code reads from the witness and computes the
post-state root from the execution diff:
Optimised guests may avoid full public-key recovery, but they must still verify
that the supplied key validates the transaction signature and is consistent with
the recovery id or y-parity bit.
Host-side input construction
On the host side, build_stateless_input receives the artefacts gathered during
block execution or construction and packages them for the guest:
The blob schedule fields and their target, max, and
base_fee_update_fraction parameters are defined by
EIP-7892; this EIP introduces no new blob-schedule
parameters.
The execution witness is built from the block-level read/write tracker and
pre-state trie data. The read/write tracker is a component already used in ELs
to track which data is touched during execution to assist for block access lists (BALs)
construction. Conceptually, witness construction:
collects the parent header and any older ancestors touched by BLOCKHASH;
collects pre-state bytecode reads;
captures account and storage trie nodes needed for all reads and writes;
captures any potential sibling requires because of branch compression
during post-state root calculation.
The host serialises a StatelessInput by prefixing the SSZ bytes with
STATELESS_INPUT_SCHEMA_ID. This lets future guest programs select the correct
input schema as forks or encoding rules evolve.
This step moves the network measurably closer to low-resource validation. Stateless, constant-time payload verification breaks the coupling between a node’s hardware requirements and the gas limit or state size, lowering the floor for who can meaningfully run a node and improving decentralisation.
Build operational experience
The central design choice is to deploy execution proofs as an opt-in feature. Stateless validation, the witness format, and the proof system itself are maturing, but we need operational experience before making execution proofs load-bearing. Treating execution proofs as a non-critical artefact lets the stack mature on a live network — proof sizes, generation latency, verifier throughput, gossip behaviour, prover diversity — without placing any of that on the path of fork choice or attestation. A bug, outage, or poor parameter choice is contained to the nodes that opted in; it cannot fork the chain or affect validators that did not subscribe.
Backwards Compatibility
This EIP is fully opt-in and does not change consensus validity rules. Validators that do not enable either mode see no change to their behaviour, their bandwidth, or their attestation duties. Nodes that opt in additionally subscribe to the proof gossip topic, advertise themselves via the eproof ENR field, and may run a prover; this affects only their local resource profile.
Test Cases
Conformance tests are maintained in the canonical specification repositories:
Gossip surface. Proofs are carried over a new gossipsub topic with payload size bounded by MAX_PROOF_SIZE. Validation, anti-DoS rate limits, and peer scoring for invalid proofs follow the same patterns as other CL gossip topics; a misbehaving peer is bounded and can be downscored.
Soundness and consensus implications. This EIP does not wire proof verification into fork choice or any other consensus rule: process_execution_proof runs outside the beacon-block state-transition function. A forged proof therefore cannot fork the chain, slash a validator, or otherwise affect consensus state — its effect is bounded to a verifying node’s local view of payload validity.
Liveness and critical-path latency. Proof generation and verification are off the attestation hot path. Validators must not delay block validation or attestation production while waiting for a proof; if a proof is missing or late, the node attests using the fork choice determined by the execution engine’s re-execution of payloads.
Verifier–prover decentralisation asymmetry. Stateless, constant-time payload verification lowers the resource floor for validators, but proof generation itself remains demanding: a prover must hold the full EL state used during execution and run a non-trivial proving stack whose cost scales as O(n log² n) in the size n of the witnessed computation. If the proportion of verifying-only nodes grows faster than the proportion of nodes able to generate proofs, the set of actors who maintain the full EL state and produce proofs may become more concentrated even as overall validator participation broadens.