ePBS (EIP-7732) Support - #94
Conversation
iurii-ssv
left a comment
There was a problem hiding this comment.
Appreciate this effort! Doing first pass, just started looking into Gloas, forgive AI-heavy commentary (seems relevant though).
Nit: would be nice to list/organize the duties affected/added by the actual slot timeline (eg. "Proposer Preferences Duty", should come first, then "Modified Proposer Duty", then "Modified Attestation Duty", etc.)
thanks for taking a look! will resolve all the comments this week 😃 |
|
I opened ethereum/EIPs#11684 to update the EIP-7732 Gloas summary against the current consensus-specs Gloas files: Since this SIP depends on those EIP-7732 details, I would appreciate review there as well. |
the current top-level order mirrors upstream's grouping in |
…erences Pin updated from f1371480c4 to upstream master HEAD following PR review feedback from iurii-ssv and diegomrsantos. Net changes in the Proposer Preferences section: ProposerPreferences now carries dependent_root, bid handshake matches on (proposal_slot, dependent_root), gossip rule is first-valid-per-tuple, new Security Considerations entry on too-early publication. PTC paragraph also tightened to distinguish PAYLOAD_ATTESTATION_DUE_BPS from PAYLOAD_DUE_BPS. Slot Timing, Attestation Duty, and Proposer Duty sections unchanged at target pin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
§5 message validation: proposer-duty-view staleness tolerance (+ emission grace) — proposal from devnet findings While validating §5 on the Gloas Why §5 is uniquely fragile here. A preference partial is broadcast exactly once per emission, and its bytes are deterministic (BLS partial over a fixed signing root, deterministic operator envelope) — so gossipsub's content-derived message-id + seen-cache make any re-broadcast a no-op. A receiver that wrongly drops the first copy can therefore never recover it; enough wrong drops and the (slot, validator) preference starves quorum permanently. The wrong drop we observed. The duty-existence rule ("a §5 message must reference a real proposer assignment at its slot") is evaluated against the receiver's cached proposer-duty view. §5 emission is triggered by the very event (validator registration → indices change) that invalidates peers' cached views, so during a registration wave a receiver validates a just-added validator's honest partials against a view fetched before it learned of that validator → Proposed §5 validation rule (implemented in ssv, suggesting the SIP §5 message-validation text adopts it):
The same tolerance applies to the §6 self-build envelope's assignment check (identical shape, identical failure mode). Would be good to reflect this in the §5 text so Anchor implements the same semantics from day one — otherwise mixed-implementation committees will disagree about which §5 messages are valid exactly during registration churn. |
…(§5/§7) §5 (re-)emission is triggered by the very validator-index change that invalidates peers' cached proposer-duty views. Because a preference partial is a one-shot deterministic broadcast, a wrongly-dropped first copy is unrecoverable under gossip dedup, starving that slot's quorum. §7: a duty-assignment view predating the node's latest validator-set change is treated as unknown (skip the check, don't IGNORE) until it refreshes, extending the not-yet-fetched-epoch tolerance. Applies to the RoleProposerPreferences (8) and RoleEnvelopeProposer (9) checks. §5: emitters SHOULD delay (re-)emission a couple slots after a validator-set change so peers converge before the one-shot partials go out.
|
Thanks @iurii-ssv, great find! Folded both parts into the SIP (0a8de9e): §7 now skips (doesn't IGNORE) the duty-assignment check when the cached view predates the latest validator-set change, until it refreshes; §5 gets the non-normative "delay re-emission a couple slots after a validator-set change" note. Anti-spam bound (committee membership + distinct-root cap + duty-count cap) kept explicit. Confirmed Anchor would hit the same wrong-drop without it, so we'll match. |
The summary said the SIP activates at the Gloas-aligned SSV fork, but the ePBS SIP (ssvlabs#94, checked at d5197bc) defines no SSV-network fork; its validation rules gate on epoch(msg.slot) >= GLOAS_FORK_EPOCH. This SIP now matches that model explicitly: activation is Ethereum-gated by the epoch(S) >= GLOAS_FORK_EPOCH conjunct, the SSV fork-equality conjunct is window-boundary protection only (it keeps the 2-slot emission window from spanning any scheduled SSV fork), and a new S >= EARLY_RANDAO_LEAD conjunct resolves the genesis underflow normatively, surfaced by the slot-zero unit test in the Anchor implementation. Vectors spell out the boundary outcomes.
| |---|---|---|---|---|---|---|---| | ||
| | `RolePTCAttester` (7) | REJECT | `PTCAttesterPartialSig` (7); 1 | PTC attestation slot | none | 3 slots | PTC assignment at slot (IGNORE) | 2 (IGNORE) | | ||
| | `RoleProposerPreferences` (8) | REJECT | `ProposerPreferencesPartialSig` (8); up to 4 distinct signing roots, repeat of a seen root = duplicate | `proposal_slot` | `(1 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH` slots | 2 slots | proposer assignment at `proposal_slot` (IGNORE) | `SLOTS_PER_EPOCH` (IGNORE) | | ||
| | `RoleEnvelopeProposer` (9) | QBFT: 1 proposal / prepare / commit / round-change per (signer, slot, round); round cut-off 2 | `PostConsensusPartialSig` (0); 1 | proposal slot | none | 3 slots | proposer assignment at slot (IGNORE) | `SLOTS_PER_EPOCH` (IGNORE) | |
There was a problem hiding this comment.
just a reminder that this and other message validation rules should be fixed in https://github.com/ssvlabs/knowledge-base/blob/main/p2p/MessageValidation/Rules.md and spec
There was a problem hiding this comment.
@GalRogozinski not sure what you mean, you want us to update https://github.com/ssvlabs/knowledge-base/blob/main/p2p/MessageValidation/Rules.md to align it with the SIP ? I can create PR for it, I guess. Just maybe lets approve/merge the SIP first, so we know it is final.
And I also think we plan/want to add the "trusted bids flow/duty" to this SIP (once it's finalized on the Ethereum side), right @shane-moore ?
There was a problem hiding this comment.
Yes, we'll add a duty for RequestAuth once the beacon api/builder-spec pr's are a bit more solid:
ethereum/builder-specs#165
ethereum/beacon-APIs#630
ethereum/beacon-APIs#625
similarly, it shows EIP-7688 Progressive Containers as still CFI status in the glamsterdam meta EIP, but they verbally SFI'd it on last ACDC call, so once that's documented, I'll update the SIP accordingly
| | `RoleProposerPreferences` (8) | REJECT | `ProposerPreferencesPartialSig` (8); up to 4 distinct signing roots, repeat of a seen root = duplicate | `proposal_slot` | `(1 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH` slots | 2 slots | proposer assignment at `proposal_slot` (IGNORE) | `SLOTS_PER_EPOCH` (IGNORE) | | ||
| | `RoleEnvelopeProposer` (9) | QBFT: 1 proposal / prepare / commit / round-change per (signer, slot, round); round cut-off 2 | `PostConsensusPartialSig` (0); 1 | proposal slot | none | 3 slots | proposer assignment at slot (IGNORE) | `SLOTS_PER_EPOCH` (IGNORE) | | ||
|
|
||
| Lateness TTL uses the existing deadline convention: a message is late once received after `slot_start(slot + TTL)` plus the clock-error margin. Duty limits count distinct duty slots per (signer, epoch of the message slot). Duty assignment checks apply only once the local node knows the duties for the message slot's epoch (a not-yet-fetched epoch is tolerated rather than IGNORE'd, since the message can legitimately arrive first). |
There was a problem hiding this comment.
§7: duty-limit/count keying drops the MessageID (validator) dimension.
"Duty limits count distinct duty slots per (signer, epoch of the message slot)" — read literally (per signer + role + epoch), the PTC limit of 2 breaks for real clusters: PTC selection gives each validator roughly PTC_SIZE / per-slot-committee-size ≈ 1.6% probability per epoch (at ~1M active validators), so e.g. a 200-validator cluster expects ~3 PTC duties per epoch across its validators, and honest partials past the second get IGNORE'd. The count column has the same issue ("1 per (signer, slot)"): two of a cluster's validators can hold PTC seats in the same slot, which is two containers, not one.
The intent is clearly per-validator — the PTC derivation says "one PTC duty per validator per epoch", and existing message validation keys its state per MessageID, which carries the validator pubkey for validator-scoped roles. Suggest making the keying explicit: "per (signer, MessageID, epoch)" for duty limits and "1 per (signer, MessageID, slot)" for the count column, so an implementer can't read it as a global per-operator budget.
There was a problem hiding this comment.
Good catch, the shorthand was ambiguous. Clarified both keys in 9343090: partial-signature counts are keyed by (MessageID, signer, slot), and duty limits by (MessageID, signer, epoch of the message slot).
|
|
||
| Lateness TTL uses the existing deadline convention: a message is late once received after `slot_start(slot + TTL)` plus the clock-error margin. Duty limits count distinct duty slots per (signer, epoch of the message slot). Duty assignment checks apply only once the local node knows the duties for the message slot's epoch (a not-yet-fetched epoch is tolerated rather than IGNORE'd, since the message can legitimately arrive first). | ||
|
|
||
| A view that predates the node's most recent validator-set change is treated the same way: skip the check, don't IGNORE, until it refreshes. This is load-bearing for the one-shot `RoleProposerPreferences` (8) and `RoleEnvelopeProposer` (9) checks: their (re-)emission is triggered by the very validator-index change that invalidates a peer's cached view, and a deterministic partial dropped once is unrecoverable (gossip's seen-cache suppresses any re-broadcast), so a wrongly-dropped first copy permanently starves that (`proposal_slot`, `validator_index`) from quorum. Anti-spam stays bounded by committee membership, the distinct-signing-root cap, and the per-epoch duty-count cap. |
There was a problem hiding this comment.
§7: the stale-view tolerance covers validator-set changes but not dependent-root (reorg) reshuffles.
§3/§5 make dependent-root changes the canonical duty-refresh and re-emission trigger, and Security Considerations flags the late-reorg race — but the assignment-check tolerance here covers only (a) a not-yet-fetched epoch and (b) a view predating the node's most recent validator-set change. A node that has observed a reorg but hasn't refetched duties holds a fetched, post-validator-set-change view that is simply stale: it will IGNORE honest partials/QBFT messages for newly-assigned slots (roles 7, 8, and 9 all carry assignment checks).
The unrecoverability argument this paragraph already makes — deterministic one-shot partial + gossip seen-cache — applies identically here, so a wrongly-dropped first copy permanently starves that (proposal_slot, validator_index) from quorum.
Suggest the symmetric rule: after a local dependent-root change affecting an epoch, skip assignment checks for that epoch until the duty refetch lands — the same treatment as the validator-set-change window. Anti-spam stays bounded by the same caps cited at the end of this paragraph.
There was a problem hiding this comment.
Good catch, agreed. Updated §7 so a cached duty view whose dependent_root no longer matches the current local root for that epoch is treated like an unfetched or validator-set-stale view. Assignment checks resume only after a successful refresh matching the current validator set and root. This applies to roles 7, 8, and 9: dee67f5
Mechanical re-pin of every consensus-specs permalink from 801a38e15 (reviewed 2026-07-06) to master head 46d3d3513 (reviewed 2026-08-04), plus citation and context accuracy fixes for upstream changes since the old pin. No SSV implementation impact in this commit. - Re-anchor the PTC security citation from a p2p-interface.md line anchor to the stable payload_attestation_message gossip-validation heading (the file was rewritten as executable functions upstream). - Update the proposer_preferences gossip dedup key to (dependent_root, proposal_slot): upstream dropped validator_index, which the proposer check already determines. Matching key-language updates in the two related security entries. - State the REJECT severity of dependent_root sourcing and the network-wide IGNORE of re-emissions that change only fee_recipient or target_gas_limit under an unchanged key. - Flatten PR-history narration (#5414, #5429) into plain statements of current spec behavior.
EIP-7688 was ratified Scheduled for Inclusion for Glamsterdam at ACDC 183, so the SIP drops its describe-positional-until-SFI stance and specifies the progressive Gloas roots. Serialization, the QBFT values, and all SSV containers are unchanged; only signing-root derivation moves. - Summary/Motivation/Rationale: state the same-bytes different-roots boundary and enumerate exactly which SSV signing paths change (aggregate, block, self-build envelope) and which do not. - §2: the aggregate serialization stays Electra-shaped, but the Gloas Attestation is progressive, so the AggregateAndProof signing root requires the Gloas type in both aggregation paths. - §4: the decided block bytes are unchanged while the block root commits to the progressive BeaconBlockBody; positional merkleization is invalid at Gloas slots. - §6: the blinded envelope is a five-field progressive container with an explicit mix_in_active_fields root derivation, progressive payload/request roots, ProgressiveList bounds guidance, and a fixed-expected-root test requirement for blinded-to-full parity. - Security: mixed positional/progressive merkleization splits threshold signatures on identical decided bytes. - Watchlist: track the EIP-7773 status update and the client type implementations instead of waiting on SFI.
- §3: payload_attestation_data moved its slot from path to query parameter after v5.0.0-alpha.2; re-pin that one link to beacon-APIs master 4b4d89a2 and note the 204 no-block response as the explicit abstain signal. - Watchlist: beacon-APIs #625 closed 2026-08-04, superseded by #630 (BuilderEntry preferences, max_execution_payment, submitBuilderPreferences, request_auth types); reserve DOMAIN_REQUEST_AUTH 0x0B000001 and partial-sig type 9 for the request-auth amendment pending builder-specs #165.
…nale - §4: point the GetBlockData permalink at ssv-spec ac6b4233, which contains the roles and partial-sig types this SIP defines; the old 85ee4f32 tree predates them and contradicted the SIP for readers following the link. Same file and line range, verified unchanged. - §6: state the settled design rule that only payload is blinded (the SSV form deviates from the consensus-spec envelope in exactly one field; the request lists are tiny relative to the payload), and link EIP-8282, previously named with no link.
Node-side surface for Gloas (ePBS) Payload Timeliness Committee duties, per SIP ssvlabs/SIPs#94: - types/gloas: PayloadAttestationData (42B SSZ) and PayloadAttestationMessage (146B SSZ), plus JSON-only PTCDuty, matching the beacon-APIs Gloas schemas; SSZ round-trip and golden JSON wire-format tests. - goclient: hand-rolled HTTP for the three PTC endpoints (get duties, produce data, submit messages). go-eth2-client has no Gloas provider yet, so these are direct requests until it is rebased. - beacon: standalone PTCCalls interface (folded into BeaconNode with the PTC runner later) plus regenerated MockPTCCalls; GoClient conformance asserted in tests.
EIP-7773 now lists EIP-7688 as Scheduled for Inclusion, so the watch trigger is gone; the normative progressive-root text stands on its own.
This SIP describes the ssv spec changes needed to keep operators performing validator duties correctly after ePBS (EIP-7732) lands in the consensus layer Gloas fork. Covers earlier slot deadlines,
AttestationData.Indexpropagation throughBeaconVote, the new PTC committee duty, theproduceBlockV4proposer flow (self-build vs external-builder variants), and the newSignedProposerPreferencesbroadcast.