Skip to content

Add execution proof network test crate (optional-proofs-gloas-pr-5) - #44

Open
frisitano wants to merge 11 commits into
optional-proofs-gloasfrom
optional-proofs-gloas-pr-5
Open

frisitano wants to merge 11 commits into
optional-proofs-gloasfrom
optional-proofs-gloas-pr-5

Conversation

@frisitano

@frisitano frisitano commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds testing/execution_proof_network_tests, a reusable deterministic harness for multi-node EIP-8025 execution-proof integration tests, built on the simulator's LocalNetwork rather than a second node orchestrator. The branch is merged with optional-proofs-gloas after #42 and #43, and uses both: proof-only nodes and the execution_proofs Beacon API endpoints.

  • Node modes (NodeSpec): execution-layer nodes (carry the validators, import payloads once executed), verifiers (execution layer plus proof engine: verify, cache and serve proofs without gating import), and proof-only nodes (no execution layer: hold every payload until two proof types verify). Validators only ever sit on execution-layer nodes.
  • Proof-engine injection: each node gets its own MockProofEngine through ProofEngineConfig::with_engine, gated behind a test-utils feature on the proof_engine crate and never serialized, so valid and invalid proof data are chosen per scenario. ProofEngineConfig::build_engine returns the injected engine or builds the ERE verifiers, so the client builder no longer carries the ere-verifier branches and ClientConfig has no test-only field. Production builds of lighthouse do not enable the feature.
  • Proof generation on the network (ProvingExecutionLayer, attached with NodeSpec::proving): a JSON-RPC proxy in front of a node's mock execution layer. It forwards every engine call; when engine_newPayload returns VALID it resolves the beacon block carrying the payload through the node's Beacon API (headers by parent root, matched on the bid's block hash), signs one envelope per configured proof type with a deterministic interop validator key, and posts them to POST /eth/v1/beacon/execution_proofs. Different proving nodes can supply different proof types.
  • Submission and retrieval: submit_execution_proof posts to a node's Beacon API; publish_execution_proof injects an unverified proof onto gossip, as a faulty peer would; retrieved_proof_types reads GET /eth/v1/beacon/execution_proofs/{block_id}.
  • Observation: ProofStatus snapshots, per node, whether the block is known, a valid proof of a type was verified, which proof types the pending payload cache holds, whether the envelope is pending or stored, whether fork choice has the payload, and how many proofs the node requires. Waits are bounded and print every node's mode, head, payload status and peer count on timeout.

Production and simulator changes are small and additive: the simulator gains a library target and add_beacon_node_without_mock_execution_layer, Client exposes network_senders() and network_globals(), and ProofEngine implements Debug so it can live in ClientConfig.

Scenarios

Scenario Topology Shows
execution_layer_nodes_import_payloads_while_proof_only_nodes_wait verifier, 2 validator nodes, proof-only execution-layer nodes import at once; the proof-only node holds the envelope pending, unstored, and stops at the first full block
proofs_posted_to_the_beacon_api_reach_every_verifier_and_unlock_proof_only_import same two types posted to node 0 propagate over gossip, are verified, cached and served on both engines; the second type unlocks the proof-only import
proving_execution_layer_keeps_a_proof_only_node_in_sync proving verifier with validators, validator node, proof-only the middleware proves every executed payload; the proof-only node follows the chain with no test code submitting proofs
proofs_from_two_provers_combine_to_unlock_import two proving verifiers (one type each, distinct validators), proof-only the proof-only node imports only once it holds both types
larger_network_delivers_proofs_to_every_verifier 2 verifiers, 2 validator nodes, 2 proof-only proofs reach all four engines and unlock both proof-only nodes
invalid_proof_data_is_rejected_by_verifiers_that_do_not_accept_it proof-only node accepts only the type 1 bytes API rejection with InvalidProof, gossip rejection with peer penalty, an honest prover of the same type still lands, and a type node 3 does not accept leaves its payload locked
unsupported_proof_type_is_rejected_before_verification standard UnsupportedProofType at the API and on gossip, with peer penalty
proof_only_node_can_submit_its_own_proofs validator node, proof-only a two-node network where the proof-only node is the only verifier
syncing_execution_layer_stalls_a_node_at_the_head_until_it_recovers 3 execution-layer nodes, one switched to a syncing execution layer at runtime an envelope with an optimistic status is rejected at the head, the node stalls while the chain moves on, and it catches up once the execution layer answers VALID again
range_sync_imports_payloads_a_syncing_execution_layer_never_verified 2 validator nodes; an execution-layer node with a syncing execution layer joins 40 slots behind range sync imports every historical payload despite SYNCING answers, and the node reports those blocks as not optimistic
late_joining_proof_only_node_stalls_on_proofs_it_missed proving verifier with validators, validator node; proof-only node added at runtime a node joining within the sync tolerance lookup-syncs envelopes through the gate, misses the proofs gossiped before it joined, stalls at the first full block, and drops later proofs as unknown blocks while the prover still serves the missed ones over its API

Findings on the base (for the EL-optional owners, not changed here)

  1. The proof gate is per payload and ignores recursion. PendingComponents::make_available requires the configured proof types for that payload; a verified proof for a descendant does not mark ancestors received (on_valid_payload_envelope_received touches one root). A proof for payload N should attest to N and its ancestry.
  2. A gated node cannot admit the next block. While payload N is pending, fork choice rejects block N+1 with ParentPayloadNotVerified, so a later proof is dropped as UnknownBlockRoot. Without a prover a proof-only node stops at the first full block (baseline scenario).
  3. Nothing re-serves proofs, and sync paths differ. Lookup sync (within SLOT_IMPORT_TOLERANCE, 32 slots) routes envelopes through process_execution_payload_envelope and the gate, so a late joiner stalls on proofs gossiped before it joined (late-joiner scenario). By inspection, range sync uses process_range_sync_envelope, which gives a proof-only node PayloadVerificationStatus::Irrelevant and imports without proofs; that path is not exercised here. Recursion-aware gating (finding 1) would let the near joiner recover from the proofs it does receive; proof retrieval from peers is the alternative.
  4. Gloas has no provisional payload state, so a syncing execution layer stalls the head too. into_executed_payload_envelope rejects an optimistic status with OptimisticSyncNotSupported (the TODO(gloas) above it notes optimistic sync was not re-added), while range sync passes the optimistic status to import, where only events and metrics see it; fork choice records the payload as received and the API reports the block as not optimistic (syncing-execution-layer scenarios). Pre-Gloas, propagate_execution_payload_validation settled such payloads later; restoring that state for Gloas, with an execution-layer verdict or a verified proof as the settling event, would fix findings 1 to 4 together.

Validation

Base: a62a9709da55d98664f4d903e75041f71aae23d8 (origin/optional-proofs-gloas, merged into this branch).

Check Result
cargo fmt --all -- --check pass
cargo clippy with the Makefile lint flags: proof_engine, client, beacon_node without test-utils; the test crate, proof_engine, simulator with it pass
cargo nextest run -p execution_proof_network_tests --release --test-threads 1 11 passed, 0 ignored, 555s total (34s to 139s per scenario)

CI runs the suite in a dedicated execution-proof-network-tests-ubuntu job (required by test-suite-success). Locally, run it with make test-execution-proof-network (cargo nextest run -p execution_proof_network_tests --release --test-threads 1). Tests bind the simulator's fixed ports, so they are serialised in-process and ignored in debug builds.

Reviewer focus

  1. testing/execution_proof_network_tests/src/proving_execution_layer.rs: the proxy, block-root resolution and proof submission.
  2. testing/simulator/src/local_network.rs: nodes without a mock execution layer keep the caller's execution_layer config.
  3. beacon_node/proof_engine/src/config.rs: the test-utils-gated engine on ProofEngineConfig, build_engine, and the manual PartialEq that ignores it.
  4. testing/execution_proof_network_tests/src/tests.rs: topologies and assertions.

Move the module tree into `lib.rs` so other testing crates can reuse
`LocalNetwork`, the checks and the retry helper instead of duplicating
node orchestration. The `simulator` binary keeps its subcommands and now
builds on the library.
Let tests drive a production beacon node's proof pipeline without an
`ere-verifier` build or a Beacon API:

- `ClientConfig::proof_engine_override` injects a ready-built
  `ProofEngine` (never serialized) ahead of the `proof_engine` config, so
  each node can run its own mock verifier.
- `Client::network_senders()` and `Client::network_globals()` expose the
  network channels and peer state for publishing proofs and reading peer
  scores.
- `PendingPayloadCache::cached_execution_proof_types()` reports which
  proof types are cached for a block.
- `ProofEngine` implements `Debug` so it can live in `ClientConfig`.
Add `testing/execution_proof_network_tests`, a deterministic multi-node
harness for EIP-8025 execution proofs built on the simulator's
`LocalNetwork`. Tests choose per node whether it runs a mock proof
engine, which proof bytes it accepts and whether it carries validators;
sign proof envelopes with the interop validator keys; submit them through
a node (verify, cache, publish) or inject them unverified onto gossip;
and observe verification, caching, storage and payload import on every
node with bounded waits that print a per-node snapshot on timeout.

Four scenarios cover the Gloas baseline (verifiers hold a payload until
two proof types arrive), the end-to-end submit, propagate, verify and
import path, invalid proof data rejected by a verifier that does not
accept it, and an unsupported proof type rejected before verification.
The two scenarios that need proof verification are ignored on this base:
gossip verification loads the envelope from the store, which a
proof-engine node fills only after two proofs, so submission fails with
`PayloadUnavailable`. They carry the full assertions for when that is
fixed.

Tests bind the simulator's fixed ports, so `make
test-execution-proof-network` runs them sequentially in release and the
workspace test targets exclude the crate.
Keep `ClientConfig::proof_engine_override` out of production builds: the
field, its default and the builder branch now exist only with the new
`client/test-utils` feature, and `Config::proof_engine_override()`
returns `None` without it. The execution proof network test crate
enables the feature, and feature unification makes the field available
to `beacon_node` and `node_test_rig` in that build.
…er/build-lighthouse-execution-proof-network-test-crate
`add_beacon_node_without_mock_execution_layer` starts a beacon node
without pairing it with a mock execution node and keeps the caller's
`execution_layer` config: `None` for a node that validates payloads with
execution proofs alone, or an endpoint the caller runs itself, such as a
proxy in front of its own mock. `add_beacon_node` is unchanged.
Adopt the merged base in the execution proof network test crate:

- Node modes follow the base's semantics. Execution-layer nodes carry the
  validators and import payloads once executed; verifiers also verify,
  cache and serve proofs; proof-only nodes have no execution layer and
  hold every payload until two proof types verify.
- Proofs are submitted through `POST /eth/v1/beacon/execution_proofs`
  and observed through `GET /eth/v1/beacon/execution_proofs/{block_id}`
  as well as the chain's caches, and `ProofStatus` reports how many
  proofs a node requires.
- `ProvingExecutionLayer` generates proofs on the network: a JSON-RPC
  proxy in front of a node's mock execution layer that, when
  `engine_newPayload` returns `VALID`, resolves the beacon block carrying
  the payload through the Beacon API, signs one envelope per configured
  proof type and posts them to the node's API. `NodeSpec::proving`
  attaches it; different nodes can prove different types.

Eight scenarios cover the baseline, Beacon API submission and retrieval,
a proving node keeping a proof-only node in sync, two provers combining
one type each, a six-node network, invalid proof data, an unsupported
proof type and a two-node network where the proof-only node is the only
verifier. All run on this base; none are ignored.
Add an `execution-proof-network-tests-ubuntu` job next to the simulator
jobs. It installs `cargo-nextest` and runs `make
test-execution-proof-network`, which executes the crate's scenarios one
at a time in release because each starts several beacon nodes on fixed
ports. The job is required by `test-suite-success`.
Let the `proof_engine` crate own how an engine is built from its config.
`ProofEngineConfig::build_engine` returns an injected test engine when
one was supplied through the `test-utils`-gated `with_engine`, and
otherwise builds the ERE verifiers (or fails without `ere-verifier`).
The client builder calls it instead of carrying both feature branches,
and `ClientConfig` loses its test-only field and `test-utils` feature.
Equality on `ProofEngineConfig` covers the configured verifiers only.

The execution proof network test crate enables `proof_engine/test-utils`
and injects each node's mock engine with `with_engine`. Its network
start-up awaits are boxed so `ProofNetwork::build` keeps a small stack
frame under `clippy::large_stack_frames`.
`ProofNetwork::add_node` starts another beacon node once the network is
running, so scenarios can observe how a node that joins late behaves.

The new scenario shows what this base does for a proof-only node that
joins a few slots behind: lookup sync routes its envelopes through the
proof gate, the proofs for those payloads were gossiped before it
existed and nothing re-serves them, so it stops at the first full
block. Blocks after it are never admitted, so the proofs it does
receive for later payloads are dropped as unknown blocks, while the
prover still serves the missed proofs over its Beacon API.

The crate docs no longer claim that this waits on RPC proof retrieval;
they record the per-payload gate, the refused successor block and the
difference between the lookup and range sync paths instead.
`NodeSpec::with_syncing_execution_layer` starts a node whose mock
execution layer answers `SYNCING`, and
`ProofNetwork::set_execution_layer_syncing` flips that at runtime.
`ProofStatus` documents that its fork choice fields read false once a
block is finalized and pruned, and the harness can read a block's
`execution_optimistic` flag and look up roots by slot.

Two scenarios record how Gloas handles payloads the execution layer has
not validated. At the head, an envelope with an optimistic status is
rejected (`OptimisticSyncNotSupported`), so the node stalls while the
chain moves on and catches up once the execution layer answers `VALID`
again. Far behind, range sync imports every such envelope, fork choice
records the payload as received, and the block is reported as not
optimistic. Pre-Gloas optimistic sync settled such payloads later;
Gloas has no provisional payload state, which is the same gap that
keeps a proof-only node from using a later proof to cover its ancestry.
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.

1 participant