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
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.