Repository navigation
feat(beacon): gloas validator duties on the beacon node and the validator client - #662
MegaRedHand wants to merge 30 commits into
Conversation
The gloas validator duties touch the network actor, the Beacon API and block production at once. Fixing the seams first lets each part be built against them independently: two RpcToP2P methods for the gossip a validator client's envelope and payload votes need, a payload attestation pool that gossip and the API fill and block production reads, empty route modules for the new endpoints, JSON decoding for the gloas containers the API accepts, and the electra/gloas attestation conversions a pool holding electra's shape needs to serve gloas.
…tPayloadV6 A self-built gloas block needs the execution client to start a build with PayloadAttributesV4 (slot number and target gas limit) and to hand back an ExecutionPayloadV4 with its block access list and BlobsBundleV2. Both methods are advertised in the capabilities handshake.
Gloas moves the attester and aggregator offsets earlier and adds a payload timeliness committee deadline. The clock now keeps the whole Config and picks the basis points by the fork of the slot's epoch, so a client running across the boundary is on the right offsets on both sides of it. The spec response parser reads the four new keys and keeps the specification defaults when a node omits them.
…dator The placeholder vector scanned every held message on each query. Keying by slot first makes pruning a split and the per-slot reads touch only their own slot; dedup per (validator, data) is a map lookup.
From gloas the block commits to a bid and the payload travels in an envelope, so a proposal is now two publications. The client asks produceBlockV4 with include_payload so the envelope arrives with the block, decodes the body by Eth-Execution-Payload-Included, signs and publishes the bare block, then signs the envelope under the builder domain with the proposer's own key and publishes it inside the same budget. When the node returned no payload the envelope is fetched by slot and block root and published bare. The envelope is checked to be for this block and this self-build before it is signed, since the signature covers it as it stands. A bid naming a builder is left to that builder. A failed envelope does not fail the proposal, whose block is already out, and has its own failure counter because it is the failure that withholds the payload. The proposal guard is untouched: envelopes are not slashable. The same change widens BeaconNodeApi (and its http, mock and fallback implementations) with the gloas aggregate, payload timeliness committee and envelope calls, so that the services built on top of them in the following commits do not each reopen the trait.
Gloas repurposes AttestationData.index as the payload signal, so the client must neither ask about committee 0 nor reset the answer to zero: the query now omits committee_index from the first gloas slot on (the fork is passed from the client's own schedule), and the index the node answers is submitted and signed unchanged. A test pins that an index of 1 reaches both the submission and the signature, and that a zeroed copy would be a different message. Aggregates are fork-tagged: the node's answer is parsed into gloas's own Attestation, the wrapper is the gloas AggregateAndProof signed over its own root (the two attestation layouts hash differently), and the batch goes out as gloas JSON under the gloas header. The doc claims that the index is always zero are corrected.
From gloas a small committee is drawn each slot to attest whether the payload arrived. DutiesService now keeps that schedule for the current and next epoch on the attester schedule's terms (replaced when the dependent root changes, narrowed to this client's indices), and refresh_epoch fetches it for gloas epochs only, gating each epoch on its own fork since the lookahead can be the first gloas epoch. PayloadAttestationService asks the node for the slot's data, signs it under the PTC domain at the data's epoch and submits it. A 204 means no block is known and is not a vote against the payload. One vote per validator and slot is recorded at signing time, so a duty dropped at its deadline is lost rather than re-signed. The duty loop no longer skips a slot that has no attester duty: committee seats are drawn independently, so the old early continue would have dropped exactly those votes. The per-slot work moves into serve_slot_duties, which sleeps to the payload deadline (three quarters in) and is bounded by the end of the slot like the other duties.
…lopes Gloas moves the payload out of the block: the body commits to a bid and the payload is revealed in an envelope. Block production therefore needs its own assembly: the payload inputs that depend on whether the parent's payload is built on, the five-kind request list, gloas attestation packing (payload-availability index rules), payload attestation aggregation by PTC seat, the self-build bid, the state root, and the unsigned envelope. The envelope is checked against the block's post-state before it is handed out, so a malformed build fails at the producer. Column sidecars for the blobs are built from the cells and the execution client's cell proofs.
…eacon API Gossip-accepted payload_attestation_message votes now land in the pool block production packs from, and the Beacon API router gets the same pool as an Extension for its pool endpoints.
Gossipsub never delivers a node its own message, so the publish path also hands the vote to the chain actor and marks the seen cache, which keeps a peer echo from being validated and forwarded a second time.
A validator client needs proposer and attester duties and attestation data from the first gloas epoch on, and all three were refused by name. Proposer duties read the lookahead of a fulu or a gloas state: gloas keeps fulu's field and `upgrade_to_gloas` carries it over, so a fulu head can already answer the first gloas epoch. Attestation data at a gloas slot sets `index` as the payload-present flag: 0 when the head block is from the slot itself, else 1 iff fork choice holds the head's payload as FULL (a pre-gloas head counts as FULL, the boundary rule fork choice applies). `committee_index` is optional in the query, as gloas's beacon-APIs have it.
…ol endpoints The submit endpoints refused the gloas header and the aggregate query refused gloas slots. A gloas slot now needs the gloas header (and an older one an electra or fulu header), a vote's `data.index` is 0 or 1 and passes the gossip payload-status rule (reused, not reimplemented), and aggregates are served and accepted in gloas's container. Submitted and gossip gloas aggregates are pooled by converting them to the pool's electra shape, so block production keeps one pool. The refusal helper has no caller left and is removed.
The committee votes payload_present when an envelope was seen before get_payload_due_ms() into the slot, and the store only knew whether a payload was verified, not when it arrived. Record the arrival time (not the verification time, since envelopes can be held for columns or the engine) in an in-memory map pruned with the el-hash cache, and add get_payload_due_ms.
A validator client needs three things from the node to propose at gloas: a self-built block (produceBlockV4), a way to publish it, and a way to publish the payload envelope that reveals it together with the data columns of its blobs, which a self-building proposer owes the network. produceBlockV4 builds on the head's recorded payload branch, asks the execution client through forkchoiceUpdatedV4 and getPayloadV6, and caches the envelope, blobs and cell proofs for the current and previous slot so include_payload=false and the stateful publish form work. publishBlockV2 takes a bare gloas block. The envelope endpoint waits briefly for a block the chain actor has not imported yet, since a validator client publishes the envelope as soon as the block is handed to the network. The node's custody set is handed to the RPC server for the build request.
A gloas validator client needs three things from its beacon node to sit on
the payload timeliness committee: its seat (POST duties/ptc/{epoch}), the
data to sign (GET payload_attestation_data) and somewhere to submit the vote
(POST pool/payload_attestations). Submitted votes go through the same
stateful checks gossip applies to a peer's, then into the pool block
production packs from, and out on gossip.
The first gloas epoch is answered from a copy of a fulu head state advanced
across the upgrade on a blocking thread, since get_ptc only reads the cached
window of a gloas state.
The Beacon API hands the node a signed envelope and the columns of its blobs, and gossipsub never delivers a node its own messages, so the publish path also has to give them to the chain actor. The envelope goes to the actor as it is, which is safe before its block is imported: the actor holds envelopes for blocks it has not imported or has stored but not imported. The sidecars go through the chain checks, which park one whose block is not imported yet.
…elope publication Both this branch and the PTC branch gave the Beacon API's handles the node's custody set; the merge keeps the PTC branch's `CustodyColumns` handle, which block production now reads under an alias that keeps it apart from the engine's bitfield of the same name.
…nt API The client grew a fork argument on attestation_data, AggregateKind, a fork on BlockRequest and ProducedBlock accessors. The router also layers the payload attestation pool and custody columns as the real server does, so the harness keeps matching what start_beacon_rpc_server builds.
The beacon node and validator client now serve and perform gloas duties, but the docs still said the node refuses a gloas epoch and never builds a payload. Document the new endpoints (produceBlockV4, the envelope endpoints, PTC duties, payload attestation data and pool, gloas on the existing pool and block endpoints), the departures from the specification that came with them (self-build only, target gas limit, ignored skip_randao_verification, the pre-gloas FULL rule for attestation data, arrival-time payload_present), the validator client's slashing-protection scope for envelopes and PTC votes, the four new validator metrics, and the engine calls used to build a gloas payload.
pack_payload_attestations grouped and verified votes inline, tied to a state exactly one slot past the parent. The pool listing endpoint needs the same grouping for any slot the head state can read a committee for, so the grouping moves into aggregate_payload_attestations and packing calls it. Packing behavior is unchanged.
beacon-APIs getPoolPayloadAttestations returns Gloas.PayloadAttestation (a bitvector over the slot's committee plus one aggregated signature), not raw messages, so a consumer built to the spec could not parse our answer. The handler now aggregates the pooled votes per slot and data with the logic block production uses, against the head state. The optional slot filter is kept.
Adds gloas counterparts to the validator-client tests, each running the real client (HttpBeaconNode, ProposalService, PayloadAttestationService) against the real router over a socket, with a signature check against the state's own domains: - attest and aggregate: no committee index in the request, the payload signal in the answer (0, then 1 once fork choice holds the head's payload as full), the gloas header on submit, and the gloas aggregate container. - payload timeliness committee: duties equal the state's committees, 204 for a slot with no block, and votes signed by the client are gossiped and pooled. - proposal: produceBlockV4 with the payload, a signed block, a signed builder-domain envelope matching the bid, with and without a real blob (all data columns go out with the envelope). The fake execution client now also answers forkchoiceUpdatedV4 and getPayloadV6. The test state needs its fork set to the schedule's gloas version, which the builders leave zeroed, for the client's signatures to verify against it.
The listing now follows the specification's PayloadAttestation shape; the doc still described the raw messages it returned before.
Axum caps request bodies at 2 MiB by default, so POST /eth/v2/beacon/blocks with a fulu SignedBlockContents of 18-21 blobs (131072 bytes each, plus 128 cell proofs of 48 bytes per blob) answered 413. The gloas envelope POST with Eth-Blob-Data-Included carries the whole execution payload plus blobs and would hit the same limit. Raise the limit to MAX_PUBLISH_BODY_BYTES (64 MiB) with a route-level DefaultBodyLimit on exactly those two POST routes, not router-wide. The preset's MAX_BLOB_COMMITMENTS_PER_BLOCK is no practical bound (~550 MB), so the cap is sized from a generous blob count plus a maximal payload and margin.
🤖 Claude Code ReviewReview of PR 662Scope: The checkout I can see has one uncommitted change, to Blocking: the edit makes
Suggested fix:
No code, consensus-logic or security changes are in what I could review. Automated review by Claude (Anthropic) · sonnet · custom prompt |
🤖 Codex Code Review
Found one material correctness bug; otherwise the PR looks thoughtfully implemented and well-tested for a large consensus/API addition.
Overall, the design choices around earliest-envelope tracking, bounded duty execution, and keeping PTC duties independent from attester duties are solid. If you want, I can turn the two stronger points above into ready-to-post GitHub review comments. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Kimi Code Review
I'll review this PR focusing on the critical areas for a consensus client. This is a large change introducing Gloas (EIP-7732) support with payload envelopes, payload timeliness committees (PTC), and self-built block production. Critical Issues1. Missing slashing protection for PTC votes and envelopes (Security)
// It still keeps no slashing-protection record; PTC votes have an in-memory
// `(validator, slot)` dedup only, and envelopes are unguarded.This is documented in
Recommendation: Add persistent slashing protection before mainnet deployment, or at minimum document this as a known limitation with risk assessment. 2. Race condition in envelope seen timestamp (
|
| Priority | Issue | Location |
|---|---|---|
| Critical | No slashing protection for PTC/envelopes | CLAUDE.md, ptc.rs |
| Critical | Race condition in envelope timestamp | beacon_envelope.rs:565 |
| High | Mutex poisoning panics |
Multiple files |
| High | Cell proof verification skipped | gloas_block_production.rs:296 |
| Medium | Busy-polling for block arrival | gloas_proposal.rs:1034 |
| Medium | SystemTime::now() instead of slot clock |
ptc.rs:431 |
| Low | O(n×m) aggregation | gloas_block_production.rs:245 |
The PR is generally well-structured for a major fork addition, but the missing slashing protection and mutex poisoning issues should be addressed before production use.
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
…63-64-633-636-638-gloas-live" This reverts merge 1fef238, keeping its first parent's side. #653 serves v2 proposer duties too, through its own handler, so tmp takes that one instead of carrying both. #662 had since taught proposer_duties to read a gloas head's lookahead; that stays, only the dependent-slot parameter, the v2 route and its test go.
…64-633-636-638-gloas-live #653 sits on the squashed #643, whose liveness change tmp already holds from the pre-squash branches, adapted to gloas and the shared attestation pool. Every conflict in the files only #643 touches (verdict.rs, pool.rs, store.rs) therefore keeps tmp's side. validator.rs keeps tmp's imports and takes #653's v2 proposer duties handler; proposer_duties still reads a gloas head's lookahead (#662). The new validators/{validator_id} handler reads the registry through tmp's iter_validators and balance accessors (validators() and balances() are gone here), and reports execution_optimistic through block_is_optimistic, as its siblings in states.rs do on tmp. docs/rpc.md keeps tmp's endpoint rows and both #653's v2 dependent root and #662's gloas sentence.
Motivation
#639 makes the node follow gloas, but every validator duty is refused from the gloas epoch on (
refuse_validator_duties_from_gloas,require_fulu_slot), so neither our beacon node norethlambda validatorcan validate past the fork. This PR implements the gloas validator duties on both sides, per consensus-specs v1.7.0-beta.2 (gloasvalidator.md,builder.md) and the current beacon-APIs (the produceBlockV4 shape lighthouse v8.3.0-rc.0 implements).Beacon node
POST /eth/v4/validator/blocks/{slot}BUILDER_INDEX_SELF_BUILD),include_payloadreturns gloasBlockContents(block, envelope, cell proofs, blobs) or the bare block; JSON or SSZPOST /eth/v2/beacon/blocksSignedBeaconBlockGET /eth/v1/validator/execution_payload_envelopes/{slot}/{root}POST /eth/v1/beacon/execution_payload_envelopesEth-Blob-Data-Included: true) or the cached ones; checks bid match, signature and column KZG proofs, builds every gloas column sidecar, gossips envelope + columns and imports them; waits up to 4 s for the block to be importedPOST /eth/v1/validator/duties/ptc/{epoch}get_ptc_assignment), crossingupgrade_to_gloasfrom a fulu headGET /eth/v1/validator/payload_attestation_data?slot=payload_presentfrom the envelope's arrival time vsPAYLOAD_DUE_BPS,blob_data_availablefrom the custody columns; 204 when the slot has no blockGET/POST /eth/v1/beacon/pool/payload_attestationsattestation_data, pool attestations, aggregatesdata.indexcarries the head's payload status (0 same-slot, else 1 iff FULL)Under the hood:
PayloadAttributesV4(slotNumber,targetGasLimit),engine_forkchoiceUpdatedV4with attributes,engine_getPayloadV6.gloas_block_production.rs: payload inputs perprepare_execution_payload(should_build_on_full,apply_parent_execution_payload), the 5-type request list, gloas attestation packing, payload attestation packing (one bit per PTC seat), block + envelope assembly with a self-check of the envelope against the post-state.SharedPayloadAttestationPoolfilled by gossip and the API, read by block production.Storeforpayload_present.DefaultBodyLimitraised to 64 MiB on the two publish routes: a blob-heavy block or envelope (21 blobs) was a 413 at axum's 2 MiB default (found on the devnet).Validator client
*_DUE_BPS_GLOAS); loop at gloas: propose (block, then envelope), attest at 25%, aggregate at 50%, PTC at 75%.DOMAIN_BEACON_BUILDERwith the proposer key and publishes it with its blobs.committee_indexomitted,indexpassed through) and gloas aggregates.payload_attestation_data,DOMAIN_PTC_ATTESTERsigning and submission; votes even with no attester duty in the slot.Decisions and deviations (documented in
docs/spec_deviations.md)SignedProposerPreferences. TheBuilderConfigbody is decoded and ignored.targetGasLimitis the parent bid's gas limit.skip_randao_verificationis not honored; an invalid reveal is a 400 up front.attestation_data.index(the decided boundary rule).payload_presentreads the envelope's arrival time, which a restart forgets.Validation
validator_client_tests.rsdriving the real validator client against the real router over a socket at a gloas slot: attest + aggregate, PTC duties/data/submission, and a full proposal (with and without a blob) against a fake execution client answeringforkchoiceUpdatedV4/getPayloadV6.cargo test --profile release-fast --lib: rpc 170, validator 269, engine 26, storage 152, state-transitiongloas87 /block_production19; clippy-D warningsand fmt clean. Spec fixture suites not run locally.gloas_fork_epoch: 5, 12 nodes x 128 keys: ethlambda CL + VC on 6 nodes (50% of the stake; geth, ethrex 29.0.1, nethermind, besu, reth, erigon), the other half lighthouse v8.3.0-rc.0, teku 26.9.1, nimbus v26.10.0, prysm v7.2.0, lodestar v1.49.0, grandine 3.0.0-rc.0 (Platåberget releases). Image built from this branch merged intotmp/bci-626-63-64-633-636-638-gloas-live./eth/v1/beacon/rewards/attestations).Known, not addressed here
POST /eth/v2/beacon/blockswith blobs (feat(beacon): blob-carrying blocks and the operation pool #646) needs the same body-limit raise; thetmpmerge carries it.