ePBS (EIP-7732 / Gloas) — SIP-94 - #2901
Draft
iurii-ssv wants to merge 127 commits into
Draft
Conversation
iurii-ssv
force-pushed
the
epbs-gloas
branch
2 times, most recently
from
June 24, 2026 13:01
bc977ed to
838fe4b
Compare
iurii-ssv
added a commit
that referenced
this pull request
Jun 28, 2026
Promote EPBS_IMPLEMENTATION_PLAN.md from local-only (.git/info/exclude) into the branch so the in-flight ePBS planning context is shared, not local. The file carries an explicit action item: before #2901 is marked ready for review, move all remaining/useful action items into the PR description and delete this file — it must not outlive the PR.
iurii-ssv
marked this pull request as ready for review
June 28, 2026 12:10
Contributor
Greptile SummaryThis PR adds the node-side foundation for ePBS/Gloas. The main changes are:
Confidence Score: 4/5The Gloas message validation and block production paths need fixes before merging.
message/validation/signed_ssv_message.go; beacon/goclient/gloas_proposer.go Important Files Changed
Reviews (1): Last reviewed commit: "gloas: log §2 vote index + §5 proposer-p..." | Re-trigger Greptile |
momosh-ssv
reviewed
Jun 29, 2026
iurii-ssv
added a commit
that referenced
this pull request
Jun 30, 2026
Comment/doc-only response to a #2901 review (most findings were non-issues or over-stated — assessed in the plan); the actionable bits: - beacon_block.go: note blob KZG commitments also leave the body (the payload and blobs ship in the §6 envelope) — they were missing from the drops list. - ptc.go: clarify the hand-rolled client's missing custom-TLS matches the main eth2clienthttp path (system-CA https + basic-auth), so it's no regression. - plan §2: devnet-verify that a Gloas BN accepts the Fulu-tagged attestation submission (BeaconForkAtEpoch caps at Fulu; TODO(gloas) to extend if rejected). - plan §2b: record the remote-signer limitation — Web3Signer has no PTC / proposer-preferences / envelope sign types; bounded by f, local-sign workaround; operator-facing.
Contributor
Author
|
The ePBS validation log that lived in this comment has been moved into the PR description (the Validation section) — so it sits alongside the implementation summary and is easy to find. This comment is kept only as a pointer. |
iurii-ssv
added a commit
that referenced
this pull request
Jun 30, 2026
Promote EPBS_IMPLEMENTATION_PLAN.md from local-only (.git/info/exclude) into the branch so the in-flight ePBS planning context is shared, not local. The file carries an explicit action item: before #2901 is marked ready for review, move all remaining/useful action items into the PR description and delete this file — it must not outlive the PR.
iurii-ssv
added a commit
that referenced
this pull request
Jun 30, 2026
Comment/doc-only response to a #2901 review (most findings were non-issues or over-stated — assessed in the plan); the actionable bits: - beacon_block.go: note blob KZG commitments also leave the body (the payload and blobs ship in the §6 envelope) — they were missing from the drops list. - ptc.go: clarify the hand-rolled client's missing custom-TLS matches the main eth2clienthttp path (system-CA https + basic-auth), so it's no regression. - plan §2: devnet-verify that a Gloas BN accepts the Fulu-tagged attestation submission (BeaconForkAtEpoch caps at Fulu; TODO(gloas) to extend if rejected). - plan §2b: record the remote-signer limitation — Web3Signer has no PTC / proposer-preferences / envelope sign types; bounded by f, local-sign workaround; operator-facing.
iurii-ssv
added a commit
that referenced
this pull request
Jul 1, 2026
The proposer, attester and sync-committee duty-fetch handlers marked an epoch/period intent fulfilled even when no validators were eligible at fetch time, so the duties were never fetched once validators did become eligible (e.g. after a beacon-metadata sync that arrives without an accompanying indices-change event). On the Gloas devnet this surfaced as the proposer missing every block it was assigned. fetchAndProcessDuties now returns (fetched bool, err error); the caller marks the intent fulfilled only when a beacon fetch actually ran. "No eligible validators" returns fetched=false, leaving the intent pending so a later tick retries — the same model the PTC and proposer-preferences handlers already use. Also drop the now-redundant per-fetch bracket log lines, and add a temporary proposer diagnostic (#2901) that dumps the Validators()/ SelfValidators() view on no-eligible to confirm the root cause on devnet. Scheduler tests updated to assert the intent stays pending and that a late indices-change remains the sole re-fetch trigger.
iurii-ssv
added a commit
that referenced
this pull request
Jul 1, 2026
Promote EPBS_IMPLEMENTATION_PLAN.md from local-only (.git/info/exclude) into the branch so the in-flight ePBS planning context is shared, not local. The file carries an explicit action item: before #2901 is marked ready for review, move all remaining/useful action items into the PR description and delete this file — it must not outlive the PR.
iurii-ssv
added a commit
that referenced
this pull request
Jul 1, 2026
Comment/doc-only response to a #2901 review (most findings were non-issues or over-stated — assessed in the plan); the actionable bits: - beacon_block.go: note blob KZG commitments also leave the body (the payload and blobs ship in the §6 envelope) — they were missing from the drops list. - ptc.go: clarify the hand-rolled client's missing custom-TLS matches the main eth2clienthttp path (system-CA https + basic-auth), so it's no regression. - plan §2: devnet-verify that a Gloas BN accepts the Fulu-tagged attestation submission (BeaconForkAtEpoch caps at Fulu; TODO(gloas) to extend if rejected). - plan §2b: record the remote-signer limitation — Web3Signer has no PTC / proposer-preferences / envelope sign types; bounded by f, local-sign workaround; operator-facing.
iurii-ssv
added a commit
that referenced
this pull request
Jul 1, 2026
The proposer, attester and sync-committee duty-fetch handlers marked an epoch/period intent fulfilled even when no validators were eligible at fetch time, so the duties were never fetched once validators did become eligible (e.g. after a beacon-metadata sync that arrives without an accompanying indices-change event). On the Gloas devnet this surfaced as the proposer missing every block it was assigned. fetchAndProcessDuties now returns (fetched bool, err error); the caller marks the intent fulfilled only when a beacon fetch actually ran. "No eligible validators" returns fetched=false, leaving the intent pending so a later tick retries — the same model the PTC and proposer-preferences handlers already use. Also drop the now-redundant per-fetch bracket log lines, and add a temporary proposer diagnostic (#2901) that dumps the Validators()/ SelfValidators() view on no-eligible to confirm the root cause on devnet. Scheduler tests updated to assert the intent stays pending and that a late indices-change remains the sole re-fetch trigger.
5 tasks
iurii-ssv
added a commit
that referenced
this pull request
Jul 1, 2026
Two TEMP logging-only diagnostics to pinpoint where an assigned Gloas proposer duty is lost between fetch and the runner. The existing zero-eligible diagnostic only covers the "never fetched" case; these cover the loaded-epoch case: - logSlotDispatchDiagnostic (processExecution): on any slot carrying a stored proposer duty, reports stored_any / in_committee / executable so a run can separate an InCommittee-flag drop from a one-slot-window miss from a downstream dispatch loss (cross-checked against the existing 🔧 executing validator duty / could not find validator logs). - logFetchDispatchDiagnostic (fetchAndProcessDuties): flags in-committee duties stored for already-passed slots (fetched-too-late), and surfaces the InCommittee split on the fetch-success path. Read-only; no control-flow change. Remove after devnet confirmation.
This was referenced Jul 2, 2026
The SIP #94 §1 retiming made duty deadlines interval-based (intervals of 1/3 of the slot before Gloas, 1/4 from Gloas on), leaving a few comments and one error message still hard-coding pre-Gloas fractions: - aggregator_committee: the aggregation-deadline wait call site said "spec: 2/3 into slot" - goclient fetchVersionedAggregate: "gossip backfill before 2/3 of the slot" is now "before the aggregation deadline" - goclient GetSyncCommitteeContribution: the "wait for 1/3 of slot" error is now "wait for sync message deadline", matching the sibling aggregation/contribution deadline errors; test assertion updated - duties scheduler: SlotTicker doc said "1/3 of slot-time past slot start" though the ticker waits IntervalDuration(slot) Also corrects waitIntoSlot's deadline labels: intervals=1 is the attestation/sync-message deadline and intervals=2 the aggregation/contribution deadline (contributions are due at two intervals, as its own callers already say).
…1008) The main-module misspell failures were masking this: make lint runs the ssvsigner module's golangci pass only after the main module's passes, so this one surfaced only once the misspellings were fixed. Full make lint (both golangci passes, deadcode, openapi, ssvsigner-boundary) now passes locally.
The §6 analog of #2922. On the self-build path every operator publishes the identical execution-payload envelope, so the non-winning ones race the canonical publish and Lodestar rejects them with 500 EXECUTION_PAYLOAD_ENVELOPE_ERROR_ALREADY_KNOWN — which surfaced at ERROR (could not submit execution payload envelope), marked the §6 duty failed on those operators, and skewed failed-submission metrics. Generalize the §4 already-known classifier (isBlockAlreadyKnown -> isAlreadyKnown; it already matched the envelope token) and apply it to the envelope publish, so an already-known envelope is treated as success — keeping the all-publish redundancy, matching the §4 handling.
…hange SIP #94 §5 has a proposer re-emit its preference when the proposal slot's dependent_root changes (e.g. after a reorg). Two node-side gaps blocked it: - Message validation capped ProposerPreferences at one pre-consensus partial sig per (slot, signer), so a refresh (a new signing root) was rejected as a duplicate and gossip-penalized. Now track distinct signing roots per (slot, signer) and admit up to 4, treating a repeat root as a duplicate (same-peer REJECT, relayed IGNORE). - The proposer-preferences scheduler re-emitted on every reorg, so a no-op reorg re-broadcast an identical preference and self-inflicted a gossip penalty. Now track the dependent_root last emitted per epoch and, on a reorg, re-emit only the epochs whose dependent_root actually changed (ProposerDutiesDependentRoot added to the scheduler BeaconNode interface). Cleanups: fix a stale filename doc comment, and make ProposerPreferences explicitly root-tracked rather than reusing the shared pre-consensus bit.
SIP #94 §7 classifies a distinct, validly-signed ProposerPreferences root past the per-(slot,signer) cap as rate-limiting (IGNORE), not a provable violation. REJECT now fires only on a same-peer repeat of an already-seen root; a relayed repeat or a distinct root past the world cap is IGNORE'd.
Two impl-vs-spec gaps surfaced by the SIP §7 cross-check of the message-validation / QBFT value-check for the new Gloas roles: - PTC duty-limit: was capped at SlotsPerEpoch (grouped with proposer-preferences and self-build envelopes on a mistaken "one per slot" basis). A PTC member is drawn from a beacon committee and a validator sits on exactly one beacon committee per epoch, so it has at most one PTC duty per epoch. Cap it at 2 (one duty + reorg margin), matching aggregation / validator registration. - Proposer value-check: reject a value on a Gloas slot carrying a pre-Gloas Version. ssv-spec's ProposerValueCheckF branches to Gloas on cd.Version while the node branches on the slot; without the guard a Byzantine leader could split the value-check across a mixed ssv/anchor cluster. Honest proposers always stamp Version == the slot's fork, so the guard only rejects malformed values (the reverse, a Gloas Version on a pre-Gloas slot, is already rejected via GetBlockData's unknown-version error). Also correct the duty-limit rule summary: voluntary exit uses a tracked count (not a fixed 2), and self-build envelopes share the SlotsPerEpoch cap.
Hand-rolled Gloas requests (ptcDo) now surface non-2xx responses as a typed *httpStatusError — same message, but callers can branch on the status code. The §5 proposer-preferences submit uses it to wrap a 404 with a missing-endpoint hint: a beacon node predating the merged beacon-APIs#608 route (e.g. Lodestar releases through v1.44.0, which only serve an earlier draft path) 404s the final POST /eth/v1/validator/proposer_preferences, and the duty-failure log now says so instead of looking like a transient submit failure.
Operators broadcast their §5 partial signature exactly once, at their own emission tick, and those ticks skew across the committee (registration/event-sync timing). A partial arriving before the local duty (re)started was only replayed by the queue for ~1 slot and then dropped — unrecoverably, since the sender never re-broadcasts and message validation dedups a same-root re-broadcast by signing root. Every indices-change re-emission also replaced the sub-runner and discarded the partials it had collected. On a devnet this starved every first-§5-epoch duty (the Gloas fork-boundary epoch) on all operators: each node held fewer than quorum partials and logged 'duty did not complete before slot end'. Runner (dispatcher): - stash every §5 partial per proposal slot (bounded by committee × the wire's distinct-root cap, deduplicated, pruned with past slots) and replay the stash into every (re)started sub-runner, so quorum forms regardless of emission order and container replacement. - conclude a replaced sub-runner as not-required (superseded, not stuck), and carry the already-submitted preference over so an unchanged re-emission is idempotent: no duplicate gossip broadcast (peers would reject it) and no duplicate beacon-node submit; a dependent_root change still re-signs, re-broadcasts and resubmits. Outcome watcher: a §5 duty legitimately keeps converging until its proposal slot, so its stuck horizon is that slot's start rather than the emission slot's end (which produced false 'stuck' warnings for duties that could still complete). Scheduler: - give each §5 duty an execution window running to the end of its own proposal slot (was: end of the emission slot), matching the watcher. - skip assignments whose proposal slot is already reached — their preference is moot and peers would reject the partials as late. - force a one-time lookahead recheck on the first Gloas tick, like a reorg would: if the CL's reported dependent_root for the boundary epoch shifts at the fork transition, the pre-fork emission is refreshed; with an unchanged root the recheck is a no-op.
A §5 partial is broadcast exactly once and its bytes are deterministic, so gossipsub's seen-cache makes any re-broadcast a no-op: a receiver that wrongly drops the first copy can never recover it. The wrong drop we observed: the §5 duty-existence rule evaluated against a proposer duty view fetched before the receiver processed a just-added validator's registration — honest partials rejected with 'no duty for this epoch', quorum starved for that proposal slot (seen on the first §5-eligible epoch right after a registration wave; the stash/replay mechanism can't help since the message dies before the queue). Two complementary changes: Validation freshness: the duty store now tracks per-epoch staleness — the ProposerHandler marks the current and next epoch stale the moment an indices change arrives, and a completed refetch (Set) clears it. The §5 proposer-preferences and §6 self-build-envelope duty-existence checks treat a stale epoch like a not-yet-fetched one (skip, don't reject); data keeps being served to checks that must always enforce assignment. During such windows §5/§6 messages remain bounded by committee membership, the per-signer distinct-root cap and the per-epoch duty-count cap. Emission grace: the §5 handler defers (re-)emission for two slots after a validator-set change, so committee peers learn of the new validators (contract-event sync) and refresh their duty views before the one-shot partials are broadcast. Preferences target future slots, so the delay costs nothing; a pending reorg/fork recheck survives the grace and is consumed by the first post-grace tick.
…next tick The ProposerHandler consumed indicesChangeCh only inside its tick loop, raced against an intra-slot deadline, so the freshness marking §5/§6 message validation relies on could trail the event by up to a slot — eating most of the §5 emission grace. A dedicated top-level case now consumes the event immediately whenever the loop is idle: it marks the current and next epochs stale and declares the refetch intents (the next tick processes intents first thing, before duty execution). A change landing while a tick is being processed is still caught by the tick's own indices-change wait, keeping the same-slot refetch. Eager consumption also relieves the fan-out back-pressure the buffered handler channel was added to mitigate.
…e-emission An indices-change re-emission of a §5 duty that had broadcast its partial but not yet submitted (quorum still converging) re-signed and re-broadcast the identical preference. Peers reject that as a same-peer duplicate by signing root, so the operator self-inflicted a gossip-scoring penalty for a message that was also useless — peers stash the first copy, and the dispatcher replays it into the replacement sub-runner anyway. The slot runner now carries the previously broadcast preference across replacements (alongside the previously submitted one): a re-emission that rebuilds it byte-identically freezes it and keeps converging without re-signing; a dependent_root change still re-signs and broadcasts under the new root. This is the in-flight remainder of issue #2934 — the already-submitted case was covered by the earlier idempotent-re-emission change.
A message routed to a validator with no running local instance (and no matching committee, non-exporter) was discarded silently. That hop was the one place a §5 proposer-preferences partial could die without any log signature: wire validation already passes on the stored share while the validator instance is still starting (the seconds right after a registration wave), and the one-shot broadcast lost there has no redelivery — the §5 duty starves with nothing to grep. Every other hop already logs its drops (validation ignore/reject with role+slot fields, router buffer-full, queue replay exhaustion). The fall-through now logs at debug — but only for validators that belong to this operator: on shared subnets the same branch routinely swallows other operators' validator traffic, which must stay quiet. The line carries role, message type, pubkey and, for partial-signature messages, slot and signer, making a starved duty attributable in one query.
This was referenced Jul 20, 2026
V5 — §2 aggregate index across BNs. The aggregation path re-derives the attestation-data root only when the cache of our own submitted root misses, and that cached root is the QBFT-decided value, so the decided payload-status index was never at risk on the common path. Make the fallback self-correcting instead: on Gloas a 404 under our beacon node's index retries under the flipped bit, the only other value the §2 index can hold. Both roots come from one fetch, so nothing else can differ. The retry is deliberately not applied on a cache hit, whose root is decided by construction — a 404 there is a real miss and must surface. The re-derivation also stops writing Index through the shared per-slot attestation-data cache pointer; that write-through was dead in practice (Electra+ beacon nodes already return Index=0) but live on the pre-Electra branch. V9 — telemetry gauging. PTC non-convergence now reports as its own no_quorum duty outcome rather than hiding inside the generic "likely stuck" every role shares. §3 has no consensus phase and already marks every other terminal path — abstain to not_required, beacon-node, signing or broadcast failure to failed — so a PTC duty still unmarked at the deadline can only be a convergence miss. The classification keys off the immutable runner role, so it stays race-free against the watcher goroutine. On the §6 side, add ssv.runner.envelope.build_match so reconstruction misses are countable: per operator an "other" share is expected, and the signal is cluster-wide — a decided envelope no operator matched means the builder's bytes were lost. V11 — Web3Signer fork_info on Gloas. No code change needed, but the reasoning is now recorded on GetForkInfo so it need not be re-derived: Web3Signer takes fork.current_version whenever epoch >= fork.epoch and hashes it into a ForkData root without consulting any milestone enum, so an unrecognized Gloas version still yields the correct domain. Its AttestationData schema also has no post-Electra index == 0 check, so the §2 payload-status index survives into the signing root. A live-instance confirm remains. Also unify the package's two 404-detection idioms behind one isNotFound helper covering both the typed go-eth2-client error and the httpStatusError the hand-rolled Gloas endpoints return.
A go.mod replace of ssv-spec with a version-less target is a local directory by go.mod semantics, not a module in the cache; GetSpecDir fed it to the module-cache resolver and failed on a malformed module path. Use the directory directly (resolved against the module root when relative), so the spec-test suites keep running while ssv-spec is replaced with a local checkout during spec development.
…elopeProposer rename Both go.mods move to the ssv-spec#632 branch tip, which adds DomainRequestAuth (builder-specs' DOMAIN_REQUEST_AUTH, 0x0b000001) and RequestAuthPartialSig(9) for issue #2962, and renames the §6 role EnvelopeBuilder to EnvelopeProposer to match SIP-94's wire naming. The rename fallout is mechanical — identifiers, the message-validation test file name, role-referring prose — and envelope.go's error-path domain sentinel joins the prevailing spectypes.DomainError convention while touched.
…#2962) RequestAuthV1{data ByteList[4096], slot} / SignedRequestAuthV1 per builder-specs — SSZ via sszgen plus the builder-API JSON form — with hash tree roots pinned against an independent merkleization of the builder-specs wire example. BuilderEntry adopts keymanager-APIs#87's field vocabulary for the cluster's direct-builder list: entry cap, at most one default (empty-URL) entry, (URL, AuthData) identity, and auth data defaulting to the UTF-8 bytes of the URL exactly as configured. The shared distinct-root budgets both the §5 dispatcher stash and message validation size from live here too, including SIP-94 §7's normative cap of 4 preference roots.
goclient computes it genesis-style alongside DomainApplicationBuilder — the application-namespace domains never derive from a fork-versioned state; the local key manager signs it as a plain SSZ root with the other Gloas domains (nothing of it is in the slashing predicate); the remote key manager reports it unsupported like the rest of the Gloas set — Web3Signer has no request-auth type, so remote-signing operators sit out the direct-builder overlay (bounded: the cluster reconstructs while at most f operators are remote-signing).
…cher
For each authenticatable configured builder, the §5 slot sub-runner freezes
RequestAuthV1{data, proposal_slot} ahead of the preference logic, broadcasts
one single-root RequestAuthPartialSig per builder — once per root across
re-emissions, since auth roots don't move with dependent_root — collects
partials in a dedicated container with no succeeded-gate (auth collection
legitimately outlives the §5 preference submit), and reconstructs each quorum
into a per-validator RequestAuthCache shared with the proposer runner for the
produceBlockV4 POST attach (beacon-APIs#625, upstream-gated). Sub-quorum
degrades silently to the enshrined flow — gossiped bids or self-build — and
never blocks the proposal; reconstructions are counted, and the §4
build-source telemetry becomes a typed enum staged for the phase-2 reason
split (no bid / economics / auth unavailable).
Message validation admits the new type under RoleProposerPreferences with its
own distinct-root budget and the §5 two-tier REJECT/IGNORE semantics; every
other §5 wire rule (earliness, lateness, slot-advance exemption, assignment
check, duty limit, fork gates) is role-scoped and applies unchanged.
Config: the Builders YAML list (validated at startup) plumbs through
ControllerOptions and CommonOptions — now built from a struct literal instead
of the 17-argument constructor — to the runner options; documented in
EXTERNAL_BUILDERS.md with the all-operators-identical requirement. Riding
along: the remaining error-path domain sentinels join spectypes.DomainError.
…ken builders, budget = entry cap RequestAuthCache eviction is now measured against the current slot instead of the slot being written: §5 writes land up to a proposer lookahead ahead, so retention anchored to the (arbitrarily future) write slot could evict a sibling lookahead slot that was still ahead. The cache also drops its redundant per-validator key — it is one instance per validator, like ProposedBlockRoots. A frozen request auth now carries every configured builder relationship behind its signing root: the root derives from (data, slot) alone, so distinct (URL, AuthData) entries sharing one pre-agreed token converge on one root and one reconstruction — previously last-write-wins left all but one of them silently unauthenticated. The single broadcast per root is unchanged. The request-auth distinct-root budget drops from entry cap + 4 to the entry cap exactly: the builder list is startup-only and auth roots are dependent_root-independent, so no in-window churn exists for headroom to cover — and wire validation being config-independent, every extra admitted root was pure surface for clusters that never opt in. Also: a runner-layer validator-index guard on auth partials (the auth root, unlike the §5 preference, doesn't bind the index); nodes with Builders configured and a remote signer now warn once at startup and disable the overlay locally instead of warning per builder per emission forever; GetSpecDir walks the module tree once; and a conciseness pass over the new surface's comments, deduplicating rationale to single homes.
…ers; contract and metric wording
A node with no local builder entries (never configured, or disabled for a
remote signer) never freezes auth roots, so peer request-auth partials for a
started duty now fail hard instead of exhausting the retry budget — retrying
could never help there; the duty-start race keeps its retryable path.
Regression-tested.
Wording follow-ups: the cache documents its shallow-copy contract (returned
auths are shared across the cache, the runner's frozen state, and
token-sharing identities — treat as immutable) and no longer implies a
present-day reader; the reconstruction metric states its root granularity
(token-sharing builders count once, unit {root}); the budget comment owns the
restart-with-a-changed-list case as an accepted, self-healing cost; and the
loop-invariant cache nil-check is hoisted.
…s; hard-fail on frozen-nothing The dispatcher's stash replay now gates on duty-ASSIGNED rather than running: an unchanged-preference re-emission concludes not-required immediately (State.Succeeded), which used to skip the replay and leave the replacement's fresh request-auth container permanently short of quorum — peers broadcast their partials exactly once. Preference partials replayed into a concluded duty bounce off the succeeded-gate harmlessly; the auth rounds have no such gate by design. Regression-tested (the new test fails on the old gate). A domain-fetch failure now freezes an empty root set, so peer auth partials hard-fail as unknown roots instead of burning queue retries — the dispatcher stash keeps them for replay should a re-emission freeze successfully. Also: a real AuthDataBytes error is logged instead of silently treated as the default entry; the URL-length check applies only when the URL's bytes are the auth data; the in-memory broadcast markers' restart re-broadcast cost is TODO'd for devnet gauging; message validation's per-type seen-root sets collapse into one seenRootSet type with a shared budget check; and the request-auth round moves to its own file beside the §5 dispatcher.
The public-devnet SSV-cluster target is fully retired (#2920 closed in favor of the split successors): e2e rides the Aetheria local_testnet_gloas net and the Sepolia/Hoodi Gloas forks, so this placeholder config — zero registry address, bootnodes TBD — would never be filled in. The zero-registry-address guard it motivated in SSVConfigByName stays; it protects any future placeholder config the same way.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Node-side ePBS (EIP-7732 / Gloas) support, per SIP ssvlabs/SIPs#94.
Built off
boole-fork(ePBS depends on the Boole baseline: consolidated roles,ProposerConsensusData, the node-side switches). It will rebase ontostageonce Boole lands there.The ePBS wire constants (runner/beacon roles, partial-sig types, beacon domains
0x0B/0C/0D, and the #2962 request-auth pairDomainRequestAuth 0x0B000001/RequestAuthPartialSig(9)) live in ssv-spec via PR #632 — the establishedspectypespattern, and required because ekm/ssvsigner reaches signing domains only throughspectypes. Their golden test lives there; both go.mods are pinned to that branch until it merges.What's here
protocol/v2/types/gloaspackage — node-side Gloas wire types (SSZ + JSON): the PTC containers (PayloadAttestationData/Message,PTCDuty),GloasBeaconVote(fixed 120-byte, cross-fork decode fails cleanly), the §4 block family (BeaconBlock,ExecutionPayloadBid, EIP-8282 five-listExecutionRequests), the §6 envelope family (full + blinded), andProposerPreferences. Layouts pinned by round-trip tests and a devnet golden block.GLOAS_FORK_EPOCHread from the BN spec into the fork map;IsGloas/IsGloasAtSlot, safe on pre-Gloas networks.IntervalDurationbecomes slot-keyed (1/3 of the slot → 1/4 at the fork) and drives duty deadlines, QBFT round-1 head starts, aggregation waits, and attestation fetch budgets.QuickTimeoutis deliberately not retimed — the Gloas proposer is round-1-must-succeed pending real round-trip data.GloasBeaconVote(carrying the BN's payload-status index) on Gloas slots: fork-aware decode across runner, post-consensus validation, committee observer, and duty tracer;NewGloasVoteChecker; index-preserving aggregation; and a hand-rolled attestation-data GET on Gloas slots (go-eth2-client's post-ElectraIndex==0check rejects the healthy FULL status).PTCAttesterRunner(no consensus, partial-signature only, honest convergence over the frozen 75%-cutoff observation, abstains when no block is seen); scheduler handler firing at the cutoff; duty store + message validation; ekm signing.include_payload=false); the bid-only block is the QBFT value directly (no blinding); every operator submits the decided block (BN dedupes by root — to be re-confirmed on a Gloas BN); slashing-protected local signing (check→record→sign under a dedicated lock); build-source telemetry; newProposerDelayEPBSknob (hard-capped at 1s, no dangerous override).EnvelopeProposerRunner, role 9) over the blinded envelope; only the operator whose cached envelope content-matches the decided value publishes the full body; the value check ties the envelope to the §4-decided root. The envelope QBFT window is now[25%, 50%]of the slot — consensus-specs#5414 loweredPAYLOAD_DUE_BPS75%→50% (§3 PTC'sPAYLOAD_ATTESTATION_DUEstays 75%).RequestAuthV1{data, proposal_slot}signing (builder-specsDOMAIN_REQUEST_AUTH, genesis-style) rides the §5 dispatcher: per configured builder — newBuilderscluster config in keymanager-APIs#87 vocabulary, validated at startup, required identical across all n operators — one single-rootRequestAuthPartialSigper proposal slot, collected in a dedicated container with no succeeded-gate, reconstructed into a per-validator auth cache for the produceBlockV4-POST attach (beacon-APIs#625, upstream-gated). Message validation admits the new type under role 8 with its own distinct-root budget (§5 two-tier REJECT/IGNORE). Sub-quorum degrades silently to the enshrined flow — never blocks the proposal; reconstructions are counted and the §4 build-source telemetry is now a typed enum staged for the phase-2 reason split. Design/tracking: #2962 (phases 2–3 stay upstream-gated).SSVConfigByName;GetStateRootnil-state guard; runner-state JSON dedup.vendor/sszgen directive; documented thatexporter/model_encoding.gois hand-maintained.Validation — hermetic Gloas devnet (
local_testnet_gloas)Validated on a hermetic Gloas devnet (geth/lighthouse
glamsterdam-devnet-6, lighthousev8.2.0-36b70da, Gloas fork @ epoch 2), read from Loki node logs + the beacon API. Suites: ssvlabs/aetheria#130 (proposer, §4/§5/§6) + #128 (ptc, §3).POST …/validator/proposer_preferences → 200The node's ePBS implementation is spec-correct — §2–§5 are confirmed working on-chain, and §6 publishes and lands on Lodestar
v1.43.0(the live devnet-6 CL). §6 produce is missing in lighthousev8.2.0(a generic beacon-APIs#580404) but works on Lodestar. #2921 now publishes the full/unblinded envelope (SignedExecutionPayloadEnvelopeContents), which Lodestar accepts — the earlier400 "Offset out of bounds"(blinded body) is gone and the envelope lands on-chain. #2922 makes the all-operators §4 submit handle Lodestar's500 BLOCK_ERROR_ALREADY_KNOWNas success (the "BN dedupes by root" assumption doesn't hold on Lodestar; the block still lands). Residual: §6 has the same all-publish dedup as §4 — on self-build every operator publishes the identical envelope, so the redundant ones hit500 EXECUTION_PAYLOAD_ENVELOPE_ERROR_ALREADY_KNOWN(the §6 analog of #2922, tracked in #2923). E2E-verify context in #2920 (V1/V2/V6 — resolved there; the issue is closed, see the e2e strategy below). (§3 also shows a transient pre-fork500 IncorrectStateVarianton lighthouse's PTC-duties lookahead, self-clearing once the head crosses the fork.)E2E strategy. The hermetic Aetheria
local_testnet_gloasnet above is the ePBS proving ground, and public-network e2e rides the Sepolia/Hoodi Gloas forks on the existing SSV deployments there —GLOAS_FORK_EPOCHis read from the BN at runtime, so no new networkconfig is needed. A dedicated SSV cluster on the public glamsterdam devnet was considered and dropped: standing up a fresh SSV environment (contract deploy, registry bootstrap, operator onboarding) buys little over those two paths. #2920 is closed accordingly, its remaining verify items split across the successors: the manual Sepolia fork log-check (#2953), the node-design verification items V5/V8/V9/V11 (#2954), the Aetheria harness on Hoodi post-fork (ssvlabs/aetheria#139), and automated fork-transition coverage (ssvlabs/aetheria#141). TheGlamsterdamDevnetnetworkconfig stub has been dropped from the branch accordingly (the zero-registry-address guard it motivated inSSVConfigByNamestays).Known limitations / deferred
fork_infonow carries the Gloas fork so every other remote duty signs under the correct domain (whether that alone satisfies a live Web3Signer is an e2e-confirm — #2954 (V11)). Deferred §4 fix direction: pass the block header (HTR-equal to the block, nameable across the module boundary) instead of the Gloas block; whether Web3Signer accepts a Gloas-version block request is unverified.N=4distinct signing roots per (slot, signer) and the scheduler re-emits only on a real dependent_root change (the now-agreed SIP-94 §5 rule, still to be matched by Anchor). Only the pre-publish finality hold stays deferred. Low severity (reorg-gated, §5 is observational).DataVersionGloasplaceholder reconciliation — including the settled SIP §2 decision to stampDataVersionGloas(not the temporary Fulu cap) intoAggregatorCommitteeConsensusData.Versionto byte-match Anchor, which is gated on ssv-spec:GetAggregateAndProofs/GetAggregateAndProofHashRootbothswitchonVersionwith no Gloas arm, and go-eth2-client'sVersionedAggregateAndProofhas no Gloas field (the aggregate itself stays Electra-shaped); eth2-key-manager purpose-named root signer.Merge & follow-up checklist
Merge blockers — to clear before this PR lands on
stage:hasDutyRunning→hasDutyAssigned), domain-fetch hard-fail, and minor polishEnvelopeBuilder→EnvelopeProposerrename and the ePBS/Gloas: external-builder authentication & per-builder bid preferences — design questions #2962 request-auth constants; the tag should also carry the §2AggregatorCommitteeConsensusDataGloas arm noted above)stage→ move the ePBS commits withgit rebase --onto stage <boole-fork-tip> epbs-gloas(prefer a merge/FF over a squash for the Boole landing so the rebase stays mechanical); keep absorbingboole-forkrefreshes until thenlocal_testnet_gloassuites (proposer+ptc) after the rebase and after eachboole-forkrefreshTracked follow-ups (post-merge / external, not in-branch):
local_testnet_gloasrun — gauge the restart re-broadcast cost (a restart mid-lookahead re-broadcasts every auth root, up to entry-cap+1 same-peer REJECTs per pending slot; noted atbroadcastAuthRoots) and the request-auth reconstruction telemetrysigp/anchor) — now alsoRequestAuthPartialSig(9)/DomainRequestAuthand the role-8 dual-type validation rule; reconcile the SIP-94 §4/§6 text to the merged beacon-APIs#580 flow this implementation tracks (include_payload=false+ blinded publish); run a computational SSZ/HTR cross-check (incl. the EIP-8282 five-listExecutionRequests) once canonical Gloas spec vectors existsubmitBuilderPreferences(gated on the AOT-channel answer); tracked on ePBS/Gloas: external-builder authentication & per-builder bid preferences — design questions #2962, not in this PRdocs/MEV_CONSIDERATIONS.mdePBS rewrite (config.example.yamlalready points readers there forProposerDelayEPBS); re-evaluate proposals: mev/commit-boost driven MEV flow #2855 (ProposalSoftDeadline) against the tighter 25% proposal deadline once ePBS lands — decision + rationale recorded on proposals: mev/commit-boost driven MEV flow #2855.Eth-Consensus-Versionresponse header on Gloas produce; unify the hand-rolled JSON/SSZ HTTP helpers; relocateDefaultGasLimit/feeRecipientProviderout ofvalidator_registration.gowhen the VR runner is removed post-fork; reuse the shared Gloas test-block fixture in the goclient proposer tests.