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>
There was a problem hiding this comment.
Second pass on e01800e. My first-round items are all resolved — thanks Shane.
Inline comments below cover the remaining structural issues. Summary by severity:
Blockers for v1 ship:
- B1 (L180) — §4 BlockContents-path runner behavior unspecified
- B2 (L136) — §3 PTC
PartialSigMsgTypenot declared
Important:
- I3 + I4 (L30) —
Share.FeeRecipientAddressdeprecated in SSV node;DefaultGasLimitdiffers between ssv-spec (30M) and node (36M) - I5 (L209) — §5 re-emission timing risk near proposal slot (§5 × §4 interaction)
Nits:
- M2 (L100) —
math.MaxUint64sentinel rationale not explained - N1 (L182) — line-range link to
GetBlockData()againstmainwill drift - N2 (L157) —
RunnerRolenumbering gap choice not explained
Already in existing threads (no new inline needed):
- I2 §3 fork-choice asymmetry — see my L138 reply. Linked gist updated to firmly recommend Basic no-QBFT over the Pre-Consensus appendix: the appendix queries BN at ~65% (vs Basic at ~73%) to leave gossip headroom for derivation, but that creates a ~10% slot F-bias window (~1.2s) where late-arriving envelopes are actively signed as wrong-
Falseagainst the Gloas spec's 75%-anchored truth-claim semantics. Basic has only a ~2% F-bias window. SIP §3 (QBFT) has the largest F-bias (~8-25%) plus an active wrong-T attack surface from the Byzantine leader — both alternatives structurally dominate it. - M1 §5
dependent_rootphrasing & multi-checkpoint emission timing — see my L30 reply
Still pending the SSV-team decision: L186 self-build envelope path (a) vs (b) — see my L186 reply for my lean.
…_available False votes
|
|
||
| Slot is omitted because it is already pinned by the QBFT instance (same pattern as `BeaconVote`); only the observation-dependent fields need consensus. One QBFT round covers all of the cluster's local PTC-assigned validators for the slot (committee-scoped, same as `CommitteeRunner` and `AggregatorCommitteeRunner`), rather than one QBFT per validator. As a consequence, a cluster's local PTC validators in a slot contribute one shared observation to the network-wide tally rather than independent ones, which is a deliberate liveness choice of this committee-scoped design. The False-vote / missed-vote equivalence holds for `payload_present` only: `blob_data_available = False` votes are additionally counted in `should_build_on_full` via `payload_data_availability(..., available=False)`, so the cluster's shared observation carries slightly different fork-choice weight across the two boolean fields. At signing time, each operator reconstructs the full `PayloadAttestationData` (slot injected from the duty) and produces one partial signature per local PTC validator under `DOMAIN_PTC_ATTESTER` (domain epoch = `compute_epoch_at_slot(duty.slot)`), because each `PayloadAttestationMessage` on the wire ships a validator-specific signature verified against that validator's pubkey. All partial signatures broadcast together in a single `PartialSignatureMessages` container with `Type = PostConsensusPartialSig` (reused from the existing post-consensus path; the runner role `RolePTCCommittee` is the dispatch discriminator). After reconstruction, one `PayloadAttestationMessage` per validator is submitted to the beacon node. | ||
|
|
||
| The value check should reject zero `BeaconBlockRoot` (a null root cannot refer to a real block). `PayloadPresent` and `BlobDataAvailable` are observation-dependent booleans and are not compared against the local BN view (see Security Considerations); `BeaconBlockRoot` is likewise not checked against the BN's head for the slot, matching existing `BeaconVote.BlockRoot` handling. PTC attestations are not in the beacon chain slashing predicate, so no slashability call is required. |
There was a problem hiding this comment.
The Gloas spec changed after this SIP’s current pin. Current consensus-specs master adds one more PTC gossip check: the block referenced by data.beacon_block_root must be at data.slot.
This is weaker than checking the root against the local BN head. It does not ask whether the operator agrees this root is canonical; it only checks that the root belongs to the slot being signed. Since SSV injects data.slot from the duty but takes BeaconBlockRoot from the QBFT value, a bad leader could otherwise make the cluster sign a PTC message for slot S with a block root from another slot. The message would have a valid SSV signature, but gossip would ignore it under the current Gloas rule, wasting every local PTC vote covered by this QBFT round.
Can we add a value check that, when the block is known locally, verifies block.slot == duty.slot before signing? If the block is not known locally, the SIP should say whether the runner waits, requests the block, or accepts that the network may ignore the resulting message.
There was a problem hiding this comment.
Confirmed the new rule: at 6370819a the payload_attestation_message gossip validations add an IGNORE-level block.slot == data.slot check, absent at our current 5898db97e pin.
Going with accept-and-sign (your third option). The "block known locally" branch doesn't really have a referent at the SSV layer: the runner is a validator client with no block store, it reconstructs PayloadAttestationData from the QBFT-decided value plus the duty, so checking block.slot == duty.slot would mean an extra beacon-node lookup-by-root on the hot path near 75% (and the BeaconNode interface has no by-root method today).
The exposure is small: data.slot is duty-pinned and only BeaconBlockRoot is leader-supplied, so a slot-mismatched root only arises under a faulty leader, and the cost is that round's PTC votes ignored by peers, non-slashable and equivalent to a missed vote on the PTC_SIZE/2 tally. Not worth a per-slot BN round-trip to defend.
Documented in c5417cb: a Security Considerations entry plus a value-check note in §3. Since the rule postdates our pin, it's flagged to fold into §3 when we next bump consensus-specs.
There was a problem hiding this comment.
I see the tradeoff, and I agree we probably do not want every PTC round to depend on a blocking BN header lookup near the 75% cutoff.
One nuance: I do not think the check has to be all or nothing. SSV may already know the root slot from BN block events or a local root to slot cache. In that case, if the operator can determine that the decided BeaconBlockRoot belongs to a different slot than duty.slot, I think the implementation should reject before signing.
Maybe §3 can frame this as a recommended check when the information is available, without making a late BN lookup mandatory. Something like:
The value check should reject `BeaconBlockRoot` when the operator can determine that the referenced block's slot differs from the duty slot, for example from a local root to slot cache or a timely BN header lookup. If the block slot is not known before the PTC signing deadline, the runner may continue without this check; in that case, a slot mismatched root will be signed but ignored by peers.That still preserves the latency tradeoff you described, but avoids signing values that the implementation already knows the network will ignore.
There was a problem hiding this comment.
I think this complexity goes away if we were to go with the Basic (non-QBFT) variant from #94 (comment) ? The problem doesn't fully dissolve, but it doesn't require any extra handling.
PTC beacon_block_root slot-binding gossip check (consensus-specs #5281) is now part of the pinned snapshot; dropped the 'added after this SIP's pinned snapshot' caveat in Security Considerations. All other Gloas commits since the prior pin are below the SIP's abstraction or unrelated (bid construction, should_build_on_full guard, proposer boost, Fulu deposit removal).
State that the self-build envelope duty must target publication before get_payload_due_ms (PAYLOAD_DUE_BPS, 75%) so PTC validators observe it before the cutoff, with the block-to-payload budget window and the payload_present=False / empty-parent consequence. Broaden the envelope-missed Security Considerations entry to cover late publication as a second cause of the same bounded degradation.
Recast the §6 QBFT-proposal paragraph and the envelope-missed Security Considerations entry: only the §4-block builder originates a publishable value, but under QBFT round-change a later leader can carry the justified blinded value to decision without holding the full bytes. Publication is therefore by content-match (the Publication paragraph already states this), and the liveness risk is the matching-envelope operator failing before publication, not the deciding leader.
Distinguish the POST body for the two self-build modes (per Diego's review): stateless publishes SignedExecutionPayloadEnvelopeContents (envelope + blobs + KZG proofs, since the publishing operator holds the side data from BlockContents), stateful publishes a bare SignedExecutionPayloadEnvelope (the BN already cached the side data). Also hyperlink the PR #580 reference for consistency.
- drop settled head_v2 and PTC-surface watchlist items (merged + released) - drop non-normative builder_boost_factor note - add §3 envelope-arrival SSE signals for the PTC fetch/QBFT cutoff - repoint proposer-duties-v2 link to the released spec - fix envelope endpoint paths to #580's current plural form
|
One small suggestion before this is finalized: can we add an The review process changed the SIP quite a bit, and it would be good for the document to reflect the people who helped review and shape it, without making everyone a listed author. You can use my handle there. Something like: ## Acknowledgements
Thanks to @diegomrsantos and @iurii-ssv for review, design discussion, and feedback on the Gloas integration, including PTC handling, proposer preferences, and self build envelope signing. |
| #### QBFT proposal | ||
|
|
||
| Each operator constructs `BlindedExecutionPayloadEnvelope` from its local BN's envelope (`PayloadRoot = hash_tree_root(envelope.payload)`, other fields verbatim) and proposes the SSZ-encoded form in `EnvelopeConsensusData.DataSSZ`. Only an operator whose BN built the §4-decided block holds an envelope with a matching `BeaconBlockRoot` and a `PayloadRoot` backed by real full bytes, so only such an operator originates a publishable value in the first round. The QBFT value is the blinded envelope, so under round-change a later-round leader can re-propose that justified value without holding the full bytes; the decided value, and which operator can publish it, are independent of who leads the deciding round (publication is by content-match, see Publication). |
There was a problem hiding this comment.
Maybe I'm missing something, but I think we might need further adjustments on §6 - here is the concise summary of what I figured (with assertive AI tone mixed in):
Proposal: decide the blinded envelope inside §4 and drop §6's QBFT entirely.
§6's envelope-signing QBFT looks redundant. The value it agrees on is already fully
pinned by the §4 decision, so a second consensus only distributes it — and §4's QBFT
already does distribution.
The envelope is pinned at §4 time. The self-build ExecutionPayloadBid carried in
the §4 block commits to block_hash, blob_kzg_commitments, and
execution_requests_root (gloas/beacon-chain.md).
Once §4 decides and the block is signed, the payload the proposer may later reveal is
fixed (the envelope reveal must satisfy those commitments). So
payload_root = hash_tree_root(payload) is final the moment §4 decides — there is exactly
one valid envelope, and nothing left to agree, only to distribute.
A standalone §6 QBFT is structurally degenerate. Only the operator whose BN built the
§4-decided block (the winning-bid proposer, "A") can originate a valid §6 value: every
other operator's envelope carries a different beacon_block_root and fails the value
check, and only A holds the bytes to compute payload_root. Consequences:
- §6 can only make progress in a round led by A, so we'd have to special-case §6's leader
selection to seed A as round-1 leader (RoundRobinProposerotherwise picks by
height/round, unrelated to who built the block). - Even then it's fragile: a later-round leader can only re-propose A's value if A already
proposed it and it was prepared in an earlier round (carried in the round-change
justification). Non-A leaders can never originate it — strictly weaker than a normal
proposal-type QBFT where any leader proposes its own valid candidate.
§4 has none of this: every operator's BN produces its own valid (block, envelope) pair,
so any §4 leader originates a valid value in any round — a healthy QBFT. The degeneracy
exists only because the §4 decision already fixed which operator holds the valid value;
re-running consensus over it can only ever be seeded by that one operator.
Proposed shape. On the self-build path, A proposes
(block, BlindedExecutionPayloadEnvelope) as the §4 value. The blinded form is small —
that's why it was introduced for §6 in the first place — so it rides in §4 just as well,
including under §4 round-changes via the justified value. §6 then reduces to: sign the
§4-agreed blinded root under DOMAIN_BEACON_BUILDER, reconstruct, and A publishes at the
reveal deadline — no QBFT. (We could even fold the envelope partial-sig into §4's
post-consensus, leaving §6 as pure publication.) Trust model (payload_root
leader-trusted, no content validation) and 1-of-N publication are unchanged.
Cost. §4's decided value grows by the small blinded envelope and becomes
self-build-conditional; stateful self-build fetches the envelope at §4 time (one extra
GET, just moved earlier from §6). In exchange we drop an entire consensus instance on the
tightest deadline in the slot — with no liveness tradeoff, since the value is pinned by the
already-signed §4 block.
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.