Skip to content

ePBS (EIP-7732) Support#94

Open
shane-moore wants to merge 15 commits into
ssvlabs:mainfrom
shane-moore:feat/epbs
Open

ePBS (EIP-7732) Support#94
shane-moore wants to merge 15 commits into
ssvlabs:mainfrom
shane-moore:feat/epbs

Conversation

@shane-moore
Copy link
Copy Markdown
Collaborator

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.Index propagation through BeaconVote, the new PTC committee duty, the produceBlockV4 proposer flow (self-build vs external-builder variants), and the new SignedProposerPreferences broadcast.

Copy link
Copy Markdown

@iurii-ssv iurii-ssv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
@shane-moore
Copy link
Copy Markdown
Collaborator Author

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 😃

Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
Comment thread sips/epbs_support.md Outdated
@diegomrsantos
Copy link
Copy Markdown
Collaborator

I opened ethereum/EIPs#11684 to update the EIP-7732 Gloas summary against the current consensus-specs Gloas files:

ethereum/EIPs#11684

Since this SIP depends on those EIP-7732 details, I would appreciate review there as well.

@shane-moore
Copy link
Copy Markdown
Collaborator Author

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

the current top-level order mirrors upstream's grouping in gloas/validator.md: Attestation → Sync → Block proposal (with Broadcasting SignedProposerPreferences nested) → PTC. §1 Slot Timing already gives the temporal table. That said, I don't feel strongly here; happy to reorder timeline-first if SSV prefers.

…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>
Comment thread sips/epbs_support.md

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. 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. 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.
Copy link
Copy Markdown
Collaborator

@diegomrsantos diegomrsantos May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the PTC value check may be too weak here.

As written, PayloadPresent and BlobDataAvailable are decided by QBFT, but operators do not compare those booleans against their own BN observation. That means the cluster gets agreement on the observation proposed by the leader, not a BFT view derived from the operators.

The boolean shape makes this more consequential than trusting an arbitrary field. If the QBFT leader did not observe the envelope before the cutoff, but the rest of the cluster did, the cluster can still sign payload_present = false for every local PTC validator covered by this round. That is an explicit no vote, not just a missed yes vote. The same issue applies to blob_data_available = false.

This is especially important because the SIP uses one QBFT round for all local PTC validators in the slot. A slow or faulty leader can cause several validators in the same SSV cluster to emit the same false observation, even if the other operators observed the payload or blob data in time.

I think we should either make honest operators reject a PayloadAttestationVote whose booleans contradict their local observation at the cutoff, or state more explicitly that this model uses the leader observation for PTC and chooses liveness over a cluster view. My preference is the first option, since PTC is an availability observation duty.

Curious what @GalRogozinski and @iurii-ssv think.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can see the safety angle, don't feel too strongly either way. agree would be good to get @iurii-ssv and @GalRogozinski input here

a couple bits of color in case useful: not voting and voting payload_present=false actually land in the same bucket for fork-choice, should_extend_payload is sum(vote is True) > PTC_SIZE // 2, so a missed vote contributes 0 toward the YES threshold the same as an explicit false. and PTC isn't slashable, so no slashing-side cost either way

a counter-example to the leader-trust failure case: honest leader sees the envelope before the cutoff and proposes payload_present=true, but two operators' BNs haven't received it yet (envelope still in flight to them). strict equality blocks QBFT so neither side reaches quorum, and the cluster's local PTC validators all miss. so the "10 wrong votes" amplification can flip to "10 missed votes" under strict-equality, which per the tally is the same network impact.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point that for payload_present, false and missing both contribute zero toward the True threshold in should_extend_payload. I agree that weakens the “false is worse than missed” part of my concern.

I think the remaining issue is slightly different: the SIP uses one shared PayloadAttestationVote for all local PTC validators in the slot. Since PayloadAttestationData is validator-independent, that matches the container shape, but it also means one leader observation can drive several validator votes if the cluster has multiple validators assigned to PTC in the same slot.

So the concern is less about one validator emitting false instead of missing, and more about correlated observations. If the leader’s BN misses the envelope, the cluster can emit payload_present = false for every local PTC validator covered by that QBFT round, even if the other operators saw the envelope. Conversely, with strict local equality, the cluster may miss all of those votes when honest operators observe different states. Both are bad outcomes, but they are different failure modes.

Maybe the SIP should make this tradeoff explicit: the committee runner design treats PTC data as shared per slot and optimizes for one round of QBFT, but that also means the cluster emits a single observation across all local PTC validators. If we keep this model, I think we should say plainly that this is a liveness choice and that local PTC validators from the same SSV cluster are not independent observations.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in 0feaf57. added a sentence at §3 calling out that a cluster's local PTC validators in a slot contribute one shared observation to the network-wide tally rather than independent ones, framed as a deliberate liveness choice of the committee-scoped design.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On PTC value check, in my view, if we use QBFT for it - "per-validator QBFT" for PTC would be an overkill:

  • the committee-scoped pattern is already established for CommitteeRunner, so PTC should just naturally follow the same design
  • the "bad leader choice" amplification doesn't seem to be much of an issue in practice, for total SSV stake share and given an average SSV cluster manages ~100-500 validators today

One framing nuance I'd still want surfaced in §3 (or Security Considerations) for accuracy. The current "shared observation as a deliberate liveness choice" framing implicitly relies on the equivalence "False vote ≈ missed vote" for fork-choice. That equivalence is one-sided:

  • payload_present: only True votes count, via payload_timeliness(..., timely=True) in should_extend_payload. Missed ≈ False on this axis ✓
  • blob_data_available: should_build_on_full calls payload_data_availability(store, head.root, available=False), which counts False votes. A cluster's shared blob_data_available=False observation carries fork-choice weight that abstention doesn't.

Not a design issue — the QBFT runner is well-defined on both axes. Just a framing-accuracy point worth one sentence. Suggested wording (paraphrasable):

"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). The cluster's shared observation therefore carries slightly different fork-choice weight across the two boolean fields."


But I'm also not sure if QBFT for PTC is necessarily the right choice. WDYT of no-QBFT approach (could be 1 or 2 phased, depending on how simple/complex we want it to be) ?

Comment thread sips/epbs_support.md Outdated

- **New `GloasBeaconVote` carries `AttestationDataIndex`.** In Gloas, `AttestationData.Index` is BN-supplied and part of the signed attestation root, so it must travel through QBFT consensus data rather than being reconstructed locally. A dedicated Gloas-only type keeps pre-Gloas `BeaconVote` wire bytes unchanged.
- **PTC is a committee-scoped runner.** `PayloadAttestationData` is validator-independent (like `BeaconVote`), while each PTC-assigned validator still needs its own BLS signature and submission object. This matches the existing committee-runner pattern from `committee_consensus.md`.
- **Proposer-preferences is validator-scoped and non-QBFT.** `fee_recipient` already lives per-validator on `Share.FeeRecipientAddress`; `target_gas_limit` lives in operator config (currently `DefaultGasLimit = 30_000_000` in `types/beacon_types.go`, with runtime overrides, same as the existing validator-registration flow). The signed object is therefore agreed off-chain, so there is nothing to reach consensus over. The registration-like one-round partial-sig-and-submit flow from `voluntary_exit.md` fits directly.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this rationale no longer holds with the current ProposerPreferences shape.

The signed object now includes dependent_root, which is derived from the BN view for the proposer duties epoch. Unlike fee_recipient, this is not stable cluster configuration. Operators can see different dependent_root values around reorgs, epoch boundaries, or delayed head updates.

Without QBFT, operators do not actually agree on dependent_root; they independently derive a ProposerPreferences object and sign it. Reconstruction only succeeds if enough operators happened to sign the same signing root. If the cluster splits across two dependent roots, the partial signatures do not combine, no preference is published, and builder bids for that slot will not propagate.

So I think the SIP needs to choose one of two designs:

1. Run QBFT over the full ProposerPreferences object, then collect partial signatures for the agreed value.
2. Keep the flow without QBFT, but state explicitly that it relies on enough operators having the same BN view. dependent_root divergence causes reconstruction failure and loss of trustless builder bids for that slot.

My preference is option 1. ProposerPreferences is for future slots within the proposer lookahead, so the latency cost of QBFT seems much less concerning here than it would be for PTC. It also gives the cluster explicit agreement on the exact signing root before collecting partial signatures.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can see the concern about dependent_root being BN-derived rather than off-chain config. don't feel too strongly either way and would be good to get @iurii-ssv and @GalRogozinski input here as well

one thing worth flagging on the divergence-risk side: dependent_root for a proposal slot in epoch e resolves to the block at the last slot of epoch e-2, which is 32+ slots before the proposal slot itself. by the time operators emit, that block is generally well past any plausible reorg depth on mainnet (typical reorgs are 1-2 slots; 32+ slot reorgs are essentially nonexistent absent a finality break). so the chain-side divergence cases (reorgs at the epoch-boundary block) are real in principle but very small in practice.

what remains is operator-side BN behavior: a restarting BN, a peer issue, a late-head-update. these can cause one operator to transiently compute a different dependent_root, but tend to be single-operator issues rather than cluster-wide. with f=1 the existing non-QBFT reconstruction handles this.

if we did want explicit cluster agreement via qbft, the targeted scope could just be dependent_root itself since the other fields are either deterministic (proposal_slot, validator_index), cluster-consistent on Share (fee_recipient), or already follow the existing ValidatorRegistration silent-fail pattern (target_gas_limit)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this more, I think my first comment overstated the practical risk, and I agree with your read that the dependent_root should usually be old enough to be stable by the time the proposal slot arrives.

A sensible design may be to emit preferences for proposal slots in epoch e + 1 during epoch e. In that case, the dependent_root is the block at the last slot of epoch e - 1. More generally, for a proposal in epoch p, the root is the last slot of epoch p - 2. By the proposal time, that block should normally be far beyond mainnet reorg depth.

The remaining design question is emission timing inside epoch e.

If SSV emits early in epoch e, builders get more time to receive preferences and prepare bids, and SSV has more time to retry if partial signatures do not reconstruct. This matters especially for early proposal slots in epoch e + 1. The downside is that the root from the last slot of epoch e - 1 is still fresh, so a shallow reorg or delayed BN update can make operators briefly derive different dependent_root values.

If SSV emits later in epoch e, the root is more settled and operators are more likely to sign the same ProposerPreferences root. The downside is that builders have less time, and if reconstruction fails there may be little room to retry before early slots in epoch e + 1.

So I no longer think this necessarily implies QBFT is required. But I do think the SIP should avoid saying there is “nothing to reach consensus over.” A more precise design would be: operators independently derive the full ProposerPreferences; partial signatures are grouped by signing root; reconstruction succeeds only if one signing root reaches threshold.

It would also help to state the retry behavior explicitly: if the dependent root changes for an epoch in the proposer lookahead, cached duties are replaced and a new preference is emitted for the new tuple. Since dependent_root is part of the gossip identity, the new preference is not replacing the same tuple, it is a preference for a different fork context.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in 8721f21. reframed L30 with the signing-root / threshold wording you suggested, and extended §5 to make the dep-root retry case explicit: cached duties replaced and a new preference emitted as a distinct gossip-identity tuple rather than a same-tuple replacement. propagated the same framing through the rest of §5 and the config-divergence security consideration for internal consistency.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Diego's "early vs late" framing — the choice resolves cleanly because the gossip rule (first-valid per (dependent_root, proposal_slot, validator_index) tuple) treats emissions with different dependent_root as distinct messages, not conflicts. So:

  • Emit early in epoch e for all proposal slots in e+1 (already in the SIP at L209: "at each epoch boundary")
  • If dependent_root changes during e, re-emit (already in the SIP: "on duty-dependent-root changes")
  • Both the original R1 preference and the new R2 preference live on the network; builders match whichever fits the canonical chain

Early emission gives maximum builder runway. Re-emission handles any subsequent dependent_root change. The "wait until settled" concern doesn't apply because reconstruction failures from early-epoch BN disagreement are just retry opportunities, not permanent misses. What "emit early" means ? Maybe SIP could be more explicit about it:

Trigger: emit preferences for all upcoming proposal slots in the lookahead at
multiple checkpoint slots within each epoch `e` (e.g., slots 25, 27, 29, 31).
Subsequent emissions over the same `(dependent_root, proposal_slot,
validator_index)` tuple are absorbed by the gossip first-valid-per-tuple
rule, so the cost is bounded to ~600 bytes/validator/epoch of additional
gossip — negligible.

Multiple emissions serve two purposes:
1. In normal conditions: redundancy against transient signing/gossip
   failures (extra emissions are deduplicated by gossip).
2. During non-finality periods: catches "convergent moments" when operators'
   BN views align even briefly. Without this, sustained BN disagreement
   during non-finality could cause sustained emission failures.

Additionally re-emit immediately on any duty-dependent-root change for an
epoch in the lookahead (these are distinct gossip tuples and propagate as
new messages, not blocked by the first-valid rule).

Comment thread sips/epbs_support.md Outdated

The flow matches the existing `ValidatorRegistration` / `VoluntaryExit` shape: validator-scoped, non-QBFT, one round of partial signatures, reconstruct, submit. Each operator signs `ProposerPreferences` under `DOMAIN_PROPOSER_PREFERENCES` with the validator's BLS share key. `fee_recipient` lives on `Share` (cluster-consistent); `target_gas_limit` lives in operator config (`DefaultGasLimit = 30_000_000` default with runtime overrides); `dependent_root` is observed per-operator from the BN's v2 proposer-duties endpoint. Any of the three diverging across operators would fail reconstruction (same as `ValidatorRegistration` today). `fee_recipient` is cluster-consistent in practice; the practical divergence risks are `target_gas_limit` (operator config) and `dependent_root` (observation timing around reorgs and epoch boundaries).

Trigger: at each epoch boundary, and on duty-dependent-root changes for any epoch in the proposer lookahead, iterate local validators and emit one duty per slot returned by `get_upcoming_proposal_slots(state, validator_index)`. In the `MIN_SEED_LOOKAHEAD` epochs immediately before `GLOAS_FORK_EPOCH`, this SIP requires operators to emit preferences for any local-validator proposal slots in the first Gloas epoch. The semantics of `get_upcoming_proposal_slots` plus the gossip rule that `preferences.proposal_slot` must be within the proposer lookahead leave no other emission window for those slots; this aligns with the spec's *"Proposers SHOULD broadcast their preferences in the epoch before the fork"* recommendation in `p2p-interface.md`. The `proposer_preferences` gossip topic accepts only the first valid message per `(dependent_root, proposal_slot, validator_index)` tuple; emission-timing implications are covered in Security Considerations. If the proposer lookahead for an epoch changes, cached duties for that epoch are replaced rather than merged.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be framed as a SIP operational requirement that follows the Gloas recommendation to broadcast preferences in the epoch before the fork.

The reason is not that preferences become invalid after fork activation. Preferences for later slots in the first Gloas epoch are still valid while those slots are in the future. The reason to emit before the fork is to give builders enough time to see preferences and produce bids, especially for early slots in the first Gloas epoch.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in 7e8b5bd. added the builder-time motivation to the pre-fork emission paragraph at §5, alongside the lookahead-semantics reason and the spec's "broadcast in the epoch before the fork" recommendation.

Comment thread sips/epbs_support.md

Pre-consensus RANDAO flow is unchanged. Post-consensus is unchanged: each operator's `PostConsensusPartialSig` packet carries one `PartialSignatureMessage` over the block root under `DOMAIN_BEACON_PROPOSER`. Publish the signed block via the existing beacon API.

**Envelope signing out of scope.** Under Gloas, the validator signs `SignedExecutionPayloadEnvelope` only in the self-build path (`bid.builder_index == BUILDER_INDEX_SELF_BUILD`, per [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732)); in the external-build path the builder signs and publishes its own envelope. This SIP does not specify distributed signing of `SignedExecutionPayloadEnvelope`, on the following grounds:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think rarity in practice is enough by itself to leave this undefined if the SIP is intended to cover full Gloas proposer support.

Self build is still a valid Gloas path, and in that path the proposer validator signs SignedExecutionPayloadEnvelope. For SSV, that means the cluster would need to produce another distributed signature, separate from the Gloas.BeaconBlock signature. This SIP intentionally omits that flow, so it does not fully support the self build path.

I think the SIP should do one of two things:

  1. define distributed signing and publication for SignedExecutionPayloadEnvelope; or
  2. explicitly narrow the scope and say this SIP only supports the external builder path, while self build payload publication is unsupported until a follow up SIP.

The current text explains why self build may be lower priority, but I do not think rarity is enough to make the valid Gloas path unspecified.

Copy link
Copy Markdown
Collaborator Author

@shane-moore shane-moore May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair point that "rare in practice" alone doesn't quite carry the weight for omitting a valid gloas path. don't think a follow-up SIP is the right vehicle for it though; happy to expand this one if the ssv team wants, cc @iurii-ssv @GalRogozinski . two paths:

(a) tighten the §4 carve-out in this SIP, keep self-build out of scope. beyond rarity, stronger justifications worth surfacing in the text:

  • ssv's operator base skews institutional rather than home-staker; local block building is already uncommon in this population
  • gloas's execution_payload_bid p2p market gives even local-leaning proposers a trustless external fallback, so self-build is no longer the only non-MEV-relay option
  • adding a second qbft duty materially expands protocol surface for a use case unlikely to materialize at scale on ssv

(b) specify it in this SIP, but as a blinded-envelope qbft duty rather than full-payload qbft. putting the full execution payload through qbft is heavy and gets worse as gas limit trends up. cleaner shape mirrors today's BlindedBeaconBlock flow: qbft decides on a blinded form that carries an ExecutionPayloadHeader-style commitment instead of the full payload, whose hash tree root equals the full envelope's. the reconstructed bls sig is then valid over the full envelope at publication.

caveats worth flagging if we go (b):

  • the blinded form would be an ssv-internal type; consensus-specs doesn't define a BlindedExecutionPayloadEnvelope (and builder-specs has no reason to either, since self-build has no relay)
  • value-check would match today's blinded-block trust model: ssz decode + duty match + slot. operators trust the leader on envelope contents the same way they trust the leader on BlindedBeaconBlock contents today
  • residual liveness gap: bls sig commits to the leader's specific envelope hash, so the leader is sole custodian of the signable bytes; if the leader fails post-sign, the slot misses its payload (no worse than a missed self-build slot today)

no strong preference between (a) and (b); interested in the ssv team's take.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not treat the execution_payload_bid market as enough to make self build optional. It gives proposers an external path when an acceptable bid exists, but there is no guarantee that one exists for every slot.

If this SIP leaves envelope signing out, I think the limitation needs to be stated very explicitly: when no acceptable external bid is available, SSV does not complete the Gloas self build path because it cannot produce and publish a valid SignedExecutionPayloadEnvelope. The expected result is that the block is treated as payload absent and the proposer gets no payload reward.

My preference would be to specify the envelope signing flow in this SIP rather than leave that valid Gloas path unsupported.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

acknowledged on your preference for (b). made a small tightening to §4 now to state the limitation explicitly per your framing (e01800e). leaving the bigger call (specify envelope signing now vs. accept the explicit unsupported-path declaration) to the ssv team; tagging @iurii-ssv and @GalRogozinski. happy to build out (b) if that's the direction they pick.

Copy link
Copy Markdown

@iurii-ssv iurii-ssv May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we should do (b) unless we have a concrete evidence that local-block-building is "no longer a thing" - in which case option (a) is a reasonable choice. I'm not sure though whether we can get a conclusive data on this (and if we measure it for the current PBS flow, it still might be different for Gloas).

Plus the failure to produce SubmitProposerPreferences for builders to get it in time (mostly relevant for deep reorgs / stalled finality periods) is also a residual risk ... depending on how robust we can make it to be.

I'd say - the closer SIP stays to the actual implementation(s) we end up with, the better (less room to make a mistake) - meaning, I wouldn't spec out (b) unless we actually plan to implement it.

Copy link
Copy Markdown

@iurii-ssv iurii-ssv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 PartialSigMsgType not declared

Important:

  • I3 + I4 (L30) — Share.FeeRecipientAddress deprecated in SSV node; DefaultGasLimit differs between ssv-spec (30M) and node (36M)
  • I5 (L209) — §5 re-emission timing risk near proposal slot (§5 × §4 interaction)

Nits:

  • M2 (L100) — math.MaxUint64 sentinel rationale not explained
  • N1 (L182) — line-range link to GetBlockData() against main will drift
  • N2 (L157) — RunnerRole numbering 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-False against 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_root phrasing & 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.

Comment thread sips/epbs_support.md

- **New `GloasBeaconVote` carries `AttestationDataIndex`.** In Gloas, `AttestationData.Index` is BN-supplied and part of the signed attestation root, so it must travel through QBFT consensus data rather than being reconstructed locally. A dedicated Gloas-only type keeps pre-Gloas `BeaconVote` wire bytes unchanged.
- **PTC is a committee-scoped runner.** `PayloadAttestationData` is validator-independent (like `BeaconVote`), while each PTC-assigned validator still needs its own BLS signature and submission object. This matches the existing committee-runner pattern from `committee_consensus.md`.
- **Proposer-preferences is validator-scoped and non-QBFT.** `fee_recipient` already lives per-validator on `Share.FeeRecipientAddress`; `target_gas_limit` lives in operator config (currently `DefaultGasLimit = 30_000_000` in `types/beacon_types.go`, with runtime overrides, same as the existing validator-registration flow). Operators independently derive the full `ProposerPreferences`, partial signatures are grouped by signing root, and reconstruction succeeds only when one signing root reaches threshold. The registration-like one-round partial-sig-and-submit flow from `voluntary_exit.md` fits directly.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two staleness issues with this Rationale paragraph (also appear in §5 at L207):

(a) Share.FeeRecipientAddress: This field is marked DEPRECATED in the SSV node — see registry/storage/shares.go:102-105. Live fee-recipient lookup goes through feeRecipientProvider.GetFeeRecipient(...) against Recipients storage (see validator_registration.go:234). The cluster-consistency argument still holds (whatever source is used, all operators in a cluster derive the same value), but the SIP shouldn't anchor on a deprecated field.

Suggested phrasing: "the per-validator fee recipient configured cluster-side (today via Recipients storage; Share.FeeRecipientAddress is retained at the ssv-spec type level for wire-compat)."

(b) DefaultGasLimit: The SIP cites DefaultGasLimit = 30_000_000 from ssv-spec types/beacon_types.go:37, but the SSV node has already overridden this to 36_000_000 in validator_registration.go:35 (with an unimplemented 30↔36 epoch-conditional transition).

Because ProposerPreferences reconstruction depends on byte-for-byte agreement on target_gas_limit, the SIP should add a sentence that whatever default the implementation picks must be byte-identical across operators in a cluster. Either point at the implementation default (36M) or note that the SIP citation tracks ssv-spec and the implementation may diverge.

Comment thread sips/epbs_support.md
A new `GloasBeaconVoteValueCheckF()` mirrors today's `BeaconVoteValueCheckF()` and additionally:

- rejects `AttestationDataIndex` values other than `0` or `1`;
- builds the `AttestationData` passed to `IsAttestationSlashable` using the decided `AttestationDataIndex` rather than the existing `math.MaxUint64` sentinel, so the Gloas double-vote predicate trips correctly when an operator is asked to sign both `index=0` and `index=1` for the same `(source, target, slot)`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: worth one sentence on why the existing sentinel is there, so the diff lands cold for a future reader.

Per ssv-spec value_check.go:72-75, pre-Gloas the cluster has no CommitteeIndex in consensus data, and math.MaxUint64 is used so the BeaconSigner's duplicate-value slashing check doesn't false-positive on attestations that legitimately share (source, target, slot, BlockRoot). Gloas tightens this because AttestationDataIndex is now part of consensus data and can drive the slashability check directly.

Comment thread sips/epbs_support.md
}
```

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. 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. After reconstruction, one `PayloadAttestationMessage` per validator is submitted to the beacon node.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

§5 explicitly adds a new ProposerPreferencesPartialSig = 7. Here in §3, "All partial signatures broadcast together in a single PartialSignatureMessages container" doesn't say what value goes in PartialSignatureMessages.Type. The existing ssv-spec partial-sig types don't include a PTC case.

Either reuse PostConsensusPartialSig = 0 (since PTC has no pre-consensus and just produces one BLS sig per validator at the end — structurally similar to existing post-consensus paths) or add a dedicated PTCAttesterPartialSig. Whichever you pick, please declare it explicitly the way §5 does.

Comment thread sips/epbs_support.md
// types/runner_role.go additions
const (
// ... existing values ...
RolePTCCommittee RunnerRole = 7
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: RunnerRole uses explicit values with gaps at 1 and 3 (from the pre-merge Attester/SyncCommittee → Committee consolidation). Picking 7 (and 8 for RoleProposerPreferences in §5) is fine, but a one-line note on why the gaps aren't filled (e.g., "gaps at 1, 3 are reserved for backward-compat decoding of pre-consolidation messages") would make the choice explicit instead of inferred.

Comment thread sips/epbs_support.md

Under Gloas, `produceBlockV4` replaces the pre-Gloas proposer flow; blinded blocks are removed. The beacon node returns `Gloas.BeaconBlock` on the stateful path (and on any external-build response) or `Gloas.BlockContents` on the stateless self-build path ([beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580)).

`ProposerConsensusData` is preserved: its struct shape (`Duty`, `Version`, `DataSSZ []byte`) is unchanged. `DataSSZ` carries the SSZ-encoded `Gloas.BeaconBlock`. For the stateless `BlockContents` variant, the inline envelope, blobs, and KZG proofs returned by the BN are not put through QBFT.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SIP commits the cluster to running QBFT on the extracted Gloas.BeaconBlock (via DataSSZ), but doesn't say what happens after QBFT decides on the BlockContents (self-build) variant. Two reasonable behaviors:

  • (1) Sign + publish the block. Chain gets a block with bid.builder_index = BUILDER_INDEX_SELF_BUILD, SSV fails the envelope → PAYLOAD_STATUS_EMPTY, next slot's proposer can't extend the payload via is_parent_node_full.
  • (2) Skip the duty once BN returns BlockContents. Treated as a missed slot; next proposer extends the previous payload normally.

(1) has a globally-visible cost (every user tx for that slot is delayed); (2) just costs the cluster the block reward. Whichever the team picks, the SIP should specify it explicitly — especially important if §4 ultimately ships as "explicit unsupported-path declaration" (option (a) from the L186 thread), since "explicit unsupported" is incomplete without specifying the runner's actual behavior.

Comment thread sips/epbs_support.md

`ProposerConsensusData` is preserved: its struct shape (`Duty`, `Version`, `DataSSZ []byte`) is unchanged. `DataSSZ` carries the SSZ-encoded `Gloas.BeaconBlock`. For the stateless `BlockContents` variant, the inline envelope, blobs, and KZG proofs returned by the BN are not put through QBFT.

Although the struct shape is unchanged, [`ProposerConsensusData.GetBlockData()`](https://github.com/ssvlabs/ssv-spec/blob/main/types/consensus_data.go#L191-L237)'s per-version switch (Capella → Fulu today) needs a new `DataVersionGloas` arm that unmarshals `DataSSZ` as `Gloas.BeaconBlock`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this link is against main with line numbers (#L191-L237), which will drift as ssv-spec changes. At current ssv-spec HEAD the function is at L175-L237. Suggest either pinning to a specific commit (matching the Gloas consensus-specs pin you do at the top of the SIP) or dropping the line numbers and linking only to the file.

Comment thread sips/epbs_support.md

The flow matches the existing `ValidatorRegistration` / `VoluntaryExit` shape: validator-scoped, non-QBFT, one round of partial signatures, reconstruct, submit. Each operator signs `ProposerPreferences` under `DOMAIN_PROPOSER_PREFERENCES` with the validator's BLS share key. `fee_recipient` lives on `Share` (cluster-consistent); `target_gas_limit` lives in operator config (`DefaultGasLimit = 30_000_000` default with runtime overrides); `dependent_root` is observed per-operator from the BN's v2 proposer-duties endpoint. Each operator's choice of these three inputs determines its signing root; divergence splits signing roots, and reconstruction succeeds only when one root reaches threshold (same shape as `ValidatorRegistration` today). `fee_recipient` is cluster-consistent in practice; the practical divergence risks are `target_gas_limit` (operator config) and `dependent_root` (observation timing around reorgs and epoch boundaries).

Trigger: at each epoch boundary, and on duty-dependent-root changes for any epoch in the proposer lookahead, iterate local validators and emit one duty per slot returned by `get_upcoming_proposal_slots(state, validator_index)`. In the `MIN_SEED_LOOKAHEAD` epochs immediately before `GLOAS_FORK_EPOCH`, this SIP requires operators to emit preferences for any local-validator proposal slots in the first Gloas epoch. The semantics of `get_upcoming_proposal_slots` plus the gossip rule that `preferences.proposal_slot` must be within the proposer lookahead leave no other emission window for those slots; pre-fork emission also gives builders enough time to receive preferences and produce bids for early Gloas slots, aligning with the spec's *"Proposers SHOULD broadcast their preferences in the epoch before the fork"* recommendation in `p2p-interface.md`. The `proposer_preferences` gossip topic accepts only the first valid message per `(dependent_root, proposal_slot, validator_index)` tuple; emission-timing implications are covered in Security Considerations. If the proposer lookahead for an epoch changes, or `dependent_root` changes for an epoch already in the lookahead, cached duties for that epoch are replaced and a new preference is emitted. Because `dependent_root` is part of the gossip identity above, the new preference is a distinct tuple rather than a replacement of the prior one.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding a Security Considerations entry on the re-emission timing risk near the proposal slot:

Dependent-root changes that occur close to the proposal slot may not allow enough time for re-emission + builder bid round-trip before the proposal deadline. In such cases, the proposer's BN will have no matching bids available, and per §4 (self-build out of scope) the slot's payload will be empty. This risk is bounded — dependent_root is pinned in epoch p-2 relative to proposal epoch p, so the dependent block is typically near or past finalization. The realistic scenario is non-finality periods where the dependent block remains volatile.

Connects to the §5/§4 interaction noted in the L186 thread — any §5 reconstruction failure or timing miss routes into §4's unsupported BlockContents path. The multi-checkpoint emission timing being discussed in the L30 thread is the structural mitigation; this Security Considerations entry surfaces what happens when the mitigation isn't enough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants