From a43b6d0561d1daa32064b663f29412cb52d79707 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 6 May 2026 14:08:37 -0700 Subject: [PATCH 01/62] feat: epbs sip --- sips/epbs_support.md | 251 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 sips/epbs_support.md diff --git a/sips/epbs_support.md b/sips/epbs_support.md new file mode 100644 index 0000000..99b7748 --- /dev/null +++ b/sips/epbs_support.md @@ -0,0 +1,251 @@ +| Author | Title | Category | Status | Date | +| -------------- | -------------------------- | ---------- | ------------------- | ---------- | +| Shane Moore | ePBS (EIP-7732) Support | Core | draft | 2026-04-18 | + +## Summary + +Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after epbs, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas) (`ethereum/consensus-specs@f1371480c4`, reviewed 2026-04-17). + +Validator client related changes via ePBS: +1. earlier slot deadlines +2. attestation handling changes, including preservation of the Gloas `attestation.data.index` semantics +3. the new Payload Timeliness Committee (PTC) +4. the Gloas proposer flow using `produceBlockV4`, which returns either `Gloas.BlockContents` (self-build: block + inline execution-payload envelope + blobs + KZG proofs) or `Gloas.BeaconBlock` (external-builder: block only, envelope published by the builder) +5. the new `SignedProposerPreferences` message must be submitted if the node operator wants to be able to select block bids received over p2p + +## Motivation + +Gloas changes validator duties in ways that break a few current SSV assumptions: + +- attestation `index` is no longer safely reconstructible from local validator duty data +- proposer post-consensus can require signing more than one beacon object for the same duty +- the new PTC duty has a late in-slot deadline +- SSV validators must broadcast `SignedProposerPreferences` or they cannot accept external-builder bids for their slots + +## Rationale + +Key design choices and why: + +- **`BeaconVote` gains `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. +- **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`; `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. +- **`ProposerConsensusData` carries raw SSZ bytes rather than split fields.** QBFT agrees on the exact `produceBlockV4` response, keeping SSV aligned with the Beacon API wire output and side-stepping ad-hoc field maps for two variants (`BeaconBlock` vs `BlockContents`). + +## Specification + +### 1. Slot Timing Changes + +All existing validator duty deadlines shift earlier in the slot. A new PTC deadline is added. + +Relevant consensus-spec references: + +- [Validator time parameters](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/validator.md#time-parameters) + +| Duty | Pre-ePBS | Post-ePBS (Gloas) | +|------|----------|--------------------| +| Attestation | 1/3 slot (~4s) | 1/4 slot (25%, ~3s) | +| Sync Committee Message | 1/3 slot | 1/4 slot (25%) | +| Aggregation | 2/3 slot (~8s) | 1/2 slot (50%, ~6s) | +| Sync Committee Contribution | 2/3 slot | 1/2 slot (50%) | +| PTC Attestation | - | 3/4 slot (75%, ~9s) | + +### 2. Modified Attestation Duty + +Relevant consensus-spec references: + +- [Validator attestation changes](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/validator.md#attestation) + +#### Consensus-spec change + +Under Gloas, the attestation `index` field no longer carries the beacon committee index: + +- if attesting to a same-slot block, set `index = 0` +- otherwise, for a non-same-slot attestation: + - `index = 0` means payload `EMPTY` + - `index = 1` means payload `FULL` + +This value is fork-choice dependent and is supplied by the beacon node in the `AttestationData` returned to the validator client. + +#### Why the current SSV model is insufficient + +SSV currently omits `AttestationData.Index` from `BeaconVote` and fills it locally at reconstruction (`0` on the current Electra/Fulu path, duty-derived earlier). Post-Gloas this is unsafe: `Index` is BN-supplied and part of the signed attestation root, so if SSV drops it from consensus data, operators can agree on the same `block_root/source/target` and still sign the wrong root. + +#### Required change + +`BeaconVote` gains an `AttestationDataIndex` field for Gloas, matching the `phase0.CommitteeIndex` (a `uint64` alias) type of `AttestationData.Index` in consensus specs so reconstruction is a direct field assignment. The restricted Gloas value space (`0` = `EMPTY`, `1` = `FULL` for non-same-slot attestations; `0` for same-slot) is enforced in the value check below, not at the type level. + +```go +// Current (ssv-spec types/consensus_data.go) +type BeaconVote struct { + BlockRoot phase0.Root `ssz-size:"32"` + Source *phase0.Checkpoint + Target *phase0.Checkpoint +} + +// Gloas-extended +type BeaconVote struct { + BlockRoot phase0.Root `ssz-size:"32"` + Source *phase0.Checkpoint + Target *phase0.Checkpoint + AttestationDataIndex phase0.CommitteeIndex // copied from AttestationData.Index +} +``` + +#### Value check + +`BeaconVoteValueCheckF()` becomes fork-aware: + +- pre-Gloas: reject non-canonical `AttestationDataIndex` values; existing slashability workaround otherwise unchanged. +- Gloas and later: reject `AttestationDataIndex` values other than `0` or `1`; build slashability checks using the real `AttestationDataIndex`. + +#### Implementation note: aggregation path + +The `BNRoleAggregator` duty (handled by the aggregator-committee runner) fetches aggregated attestations from the Beacon API's aggregate-attestation endpoint with `attestation_data_root` as an input. Implementations must compute that root from the BN-supplied `AttestationData` (including its Gloas `index`). + +### 3. New Duty: Payload Timeliness Committee (PTC) Attestation + +Relevant consensus-spec references: + +- [Validator payload timeliness attestation flow](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/validator.md#payload-timeliness-attestation) +- [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/beacon-chain.md#payloadattestationdata) +- [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) + +PTC is a per-slot consensus-layer-selected set of validators that attests to payload and blob availability for the slot's beacon block before the deadline. + +Each validator signs a `PayloadAttestationData` object carrying `beacon_block_root`, `slot`, `payload_present`, and `blob_data_available`, then submits a validator-specific `PayloadAttestationMessage(validator_index, data, signature)` to the beacon node. + +At the start of each epoch, SSV should fetch PTC duties for the next epoch and refresh them on duty-dependent-root changes. Because PTC duty responses may be sparse and incomplete, a changed duty-dependent root for an epoch should replace the cached duties for that epoch rather than being merged. + +`PAYLOAD_ATTESTATION_DUE_BPS = 75%` is the consensus-spec-recommended broadcast time, chosen to leave ~25% of the slot for gossip propagation and aggregation by the next slot's proposer. `PayloadAttestationData` values (`payload_present`, `blob_data_available`, `beacon_block_root`) evolve throughout the slot as envelopes and blobs are observed, so runners should delay fetch and QBFT start as late as the DVT round budget permits to maximize the chance `payload_present` reflects the envelope actually arriving. The consensus specs do not prescribe a start time. Within-slot overruns past 75% still reach fork-choice via the wire path but risk missing block inclusion; past slot end the message is dropped (gossip IGNORE, fork-choice wire REJECT on `data.slot == current_slot`), and each missed vote chips at the `PTC_SIZE/2` threshold that governs whether fork-choice extends the payload. + +There is no pre-consensus phase. Operator QBFT instances agree on a stripped `PayloadAttestationVote`: + +```go +type PayloadAttestationVote struct { + BeaconBlockRoot phase0.Root `ssz-size:"32"` + PayloadPresent bool + BlobDataAvailable bool +} +``` + +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. + +This SIP adds a new beacon role `BNRolePTCAttester` and a matching runner role `RolePTCCommittee`. + +```go +// types/beacon_types.go additions +const ( + // ... existing values ... + BNRolePTCAttester BeaconRole = 7 +) + +// types/runner_role.go additions +const ( + // ... existing values ... + RolePTCCommittee RunnerRole = 7 +) +``` + +`MapDutyToRunnerRole()` must map `BNRolePTCAttester` to `RolePTCCommittee`. A new `PTCCommitteeDuty` is introduced, reusing the existing `ValidatorDuty`: + +```go +type PTCCommitteeDuty struct { + Slot spec.Slot + ValidatorDuties []*ValidatorDuty +} +``` + +`PTCCommitteeDuty` bundles all PTC-selected validators for a given slot under one duty, so one QBFT round on the shared `PayloadAttestationVote` covers all of them rather than running a separate consensus per validator. + +### 4. Modified Proposer Duty + +Relevant consensus-spec references: + +- [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/validator.md#block-and-sidecar-proposal) +- [Builder execution payload envelope construction](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/builder.md#constructing-the-signedexecutionpayloadenvelope) + +Under Gloas, `produceBlockV4` replaces the pre-Gloas proposer flow; blinded blocks are removed. The beacon node now returns one of two variants: + +- `Gloas.BlockContents` (self-build): beacon block plus inline envelope, blobs, and KZG proofs. The validator signs both the block and the `ExecutionPayloadEnvelope`. +- `Gloas.BeaconBlock` (external-builder bid accepted by the beacon node): block only. The builder later signs and publishes its own `SignedExecutionPayloadEnvelope`. + +`ProposerConsensusData` is preserved: its struct shape (`Duty`, `Version`, `DataSSZ []byte`) is unchanged, because it already carries raw SSZ bytes. The Gloas work lives entirely inside `GetBlockData()`, which grows a `DataVersionGloas` case that decodes into either `Gloas.BlockContents` (self-build) or `Gloas.BeaconBlock` (external-builder), exposing the block object and, when the decoded variant is `BlockContents`, the inline envelope plus blobs and KZG proofs. + +Post-consensus object rules: + +- both objects (block + envelope) are signed by the validator's existing BLS share key; the envelope changes the domain, not the signer identity +- each operator's `PostConsensusPartialSig` packet must carry one `PartialSignatureMessage` per required root for the decided variant: + - `Gloas.BeaconBlock`: one entry (block root) + - `Gloas.BlockContents`: two entries (block root + envelope root) + +Pre-consensus RANDAO flow is unchanged. Operators run QBFT on `ProposerConsensusData`, then sign the objects required by the decided variant. + +Publication order and completion: +- publish the signed beacon block first +- if the decided value is `Gloas.BlockContents` and block publication succeeded, publish the envelope wrapper immediately after, before the PTC 75% deadline so it can influence timely payload attestations + +### 5. Proposer Preferences Duty + +Relevant consensus-spec references: + +- [Broadcasting SignedProposerPreferences](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/validator.md#broadcasting-signedproposerpreferences) +- [`SignedProposerPreferences` container](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/p2p-interface.md#new-proposerpreferences) +- [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/p2p-interface.md#proposer_preferences) +- [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/p2p-interface.md#execution_payload_bid) + +Under Gloas, each proposer declares their preferred `fee_recipient` and `gas_limit` for upcoming proposal slots (future slots in the current epoch plus all proposal slots in the next epoch) by broadcasting `SignedProposerPreferences` on the `proposer_preferences` p2p topic. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks. + +Gossip enforces the handshake at the `execution_payload_bid` topic: bids for a slot with no seen `SignedProposerPreferences` are IGNORE'd (not forwarded), and bids whose `fee_recipient` or `gas_limit` disagree with the proposer's preferences are REJECT'd. Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. + +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); `gas_limit` lives in operator config (`DefaultGasLimit = 30_000_000` default with runtime overrides), so operators must configure matching values out-of-band. Divergence on `gas_limit` fails reconstruction, same as `ValidatorRegistration` today. + +Trigger: at each epoch boundary, and on duty-dependent-root changes for the current or next epoch, iterate local validators and emit one duty per slot returned by `get_upcoming_proposal_slots(state, validator_index)` (future slots in the current epoch plus all slots of the next epoch). In the epoch immediately before `GLOAS_FORK_EPOCH`, operators must also emit preferences for any local-validator proposal slots in the first Gloas epoch; otherwise bids for those slots will not propagate during the first post-fork epoch (per the pre-fork subscription note in `p2p-interface.md`). Per the Gloas validator spec, a validator MAY broadcast multiple `SignedProposerPreferences` for the same slot (later messages supersede earlier ones), so this SIP does not hard-cap the number of `SignedProposerPreferences` publications per slot. If the proposer lookahead for an epoch changes, cached duties for that epoch are replaced rather than merged. + +This SIP adds a new beacon role `BNRoleProposerPreferences`, a matching runner role `RoleProposerPreferences`, and a new `PartialSigMsgType` `ProposerPreferencesPartialSig`. + +```go +// types/beacon_types.go additions +const ( + // ... existing values + BNRoleProposerPreferences BeaconRole = 8 +) + +// types/runner_role.go additions +const ( + // ... existing values + RoleProposerPreferences RunnerRole = 8 +) + +// types/partial_sig_message.go additions +const ( + // ... existing values + ProposerPreferencesPartialSig PartialSigMsgType = 7 +) +``` + +`MapDutyToRunnerRole()` must map `BNRoleProposerPreferences` to `RoleProposerPreferences`. + +## Security Considerations + +### `BeaconVoteValueCheckF` must include `AttestationDataIndex` in slashability checks + +Under Gloas, `AttestationData.Index` is part of the attestation data root and therefore part of the double-vote slashing predicate. `BeaconVoteValueCheckF` must reconstruct the full Gloas `AttestationData` with `Index` from the decided `BeaconVote.AttestationDataIndex` before calling `IsAttestationSlashable`; otherwise an operator could sign `index=0` and `index=1` for the same `(source, target)` in the same slot without the predicate tripping. + +### Payload-status fields are trusted from the QBFT leader + +Value checks for Gloas `AttestationData.Index` and PTC `payload_present` / `blob_data_available` do not require the decided value to match each operator's local BN view. Requiring local agreement would fail QBFT rounds whenever operators observe the envelope at slightly different times around the 75% deadline, a normal gossip-lag scenario. Accepted tradeoff: a malicious QBFT leader can push a value contrary to the cluster's majority BN observation. This matches existing ssv-spec treatment of `BeaconVote.BlockRoot`, which is trusted from the leader because BNs legitimately diverge on fork-choice head. + +### Config divergence silently disables trustless external builder bids + +`ProposerPreferences` reconstruction requires cluster-wide agreement on `gas_limit`, which lives in per-operator config rather than `Share`. Divergence produces no reconstructed signature, no gossip publication, and therefore no trustless external builder bids for that slot (the `execution_payload_bid` topic IGNOREs bids with no matching preferences). Same reconstruction failure shape as `ValidatorRegistration` today. + +## Open Questions / Upstream Watchlist + +This section is intentionally limited to upstream items that could still change the normative SSV behavior described above. If any of these settle differently, this SIP should be updated. + +- PTC Beacon API detail drift: the core PTC duty, payload-attestation-data, and pool-submission surfaces are already present in upstream Beacon API `master`, and this SIP assumes those mainline shapes. Watch them for any remaining field, header, or duty-refresh semantic changes. +- Self-build proposer API stabilization: this SIP relies on the current reviewed shape of `produceBlockV4` (`apis/validator/block.v4.yaml`) and stateless envelope publication (`apis/beacon/execution_payload/envelope_post.yaml`), neither of which has been merged to `beacon-APIs/master` yet; both live in [PR #580](https://github.com/ethereum/beacon-APIs/pull/580). Watch for changes to self-build versus block-only response behavior, required submission wrappers, or publication headers. The PR's discussion also covers restructuring `builder_boost_factor` for multi-builder connections; this will reshape SSV node-operator config but does not change SIP-normative behavior (the §4 decode / value-check / post-consensus rules remain variant-agnostic). +- `head_v2` SSE event ([PR #590](https://github.com/ethereum/beacon-APIs/pull/590), Gloas-labeled upstream): adds an ePBS-specific `payload_status` field (`empty` / `full`) that may update mid-slot as the envelope is observed, and renames `{previous,current}_duty_dependent_root` → `{previous,current}_epoch_dependent_root` while adding `next_epoch_dependent_root`. Both §3 (PTC duty refresh) and §5 (proposer-preferences re-emission trigger) key off these dependent-root fields, so implementations will need to consume the renamed fields once the PR lands. `payload_status` is also optionally useful as an early signal for the PTC runner's internal decision cutoff (§3). +- Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. From cfb672e886078fe206f9877d3fd5d444305537ed Mon Sep 17 00:00:00 2001 From: shane-moore Date: Thu, 14 May 2026 09:11:12 -0700 Subject: [PATCH 02/62] chore: remove envelope signing requirements for epbs --- sips/epbs_support.md | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 99b7748..fa2dfb0 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -10,7 +10,7 @@ Validator client related changes via ePBS: 1. earlier slot deadlines 2. attestation handling changes, including preservation of the Gloas `attestation.data.index` semantics 3. the new Payload Timeliness Committee (PTC) -4. the Gloas proposer flow using `produceBlockV4`, which returns either `Gloas.BlockContents` (self-build: block + inline execution-payload envelope + blobs + KZG proofs) or `Gloas.BeaconBlock` (external-builder: block only, envelope published by the builder) +4. the Gloas proposer flow using `produceBlockV4`. The SSV cluster signs only the `Gloas.BeaconBlock`; signing of `SignedExecutionPayloadEnvelope` (the validator-signed envelope used in the self-build path) is intentionally out of scope — see §4. 5. the new `SignedProposerPreferences` message must be submitted if the node operator wants to be able to select block bids received over p2p ## Motivation @@ -18,7 +18,6 @@ Validator client related changes via ePBS: Gloas changes validator duties in ways that break a few current SSV assumptions: - attestation `index` is no longer safely reconstructible from local validator duty data -- proposer post-consensus can require signing more than one beacon object for the same duty - the new PTC duty has a late in-slot deadline - SSV validators must broadcast `SignedProposerPreferences` or they cannot accept external-builder bids for their slots @@ -29,7 +28,7 @@ Key design choices and why: - **`BeaconVote` gains `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. - **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`; `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. -- **`ProposerConsensusData` carries raw SSZ bytes rather than split fields.** QBFT agrees on the exact `produceBlockV4` response, keeping SSV aligned with the Beacon API wire output and side-stepping ad-hoc field maps for two variants (`BeaconBlock` vs `BlockContents`). +- **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` is out of scope; see §4 for the rationale. ## Specification @@ -164,27 +163,20 @@ type PTCCommitteeDuty struct { Relevant consensus-spec references: - [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/validator.md#block-and-sidecar-proposal) -- [Builder execution payload envelope construction](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/builder.md#constructing-the-signedexecutionpayloadenvelope) -Under Gloas, `produceBlockV4` replaces the pre-Gloas proposer flow; blinded blocks are removed. The beacon node now returns one of two variants: +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)). -- `Gloas.BlockContents` (self-build): beacon block plus inline envelope, blobs, and KZG proofs. The validator signs both the block and the `ExecutionPayloadEnvelope`. -- `Gloas.BeaconBlock` (external-builder bid accepted by the beacon node): block only. The builder later signs and publishes its own `SignedExecutionPayloadEnvelope`. +`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. -`ProposerConsensusData` is preserved: its struct shape (`Duty`, `Version`, `DataSSZ []byte`) is unchanged, because it already carries raw SSZ bytes. The Gloas work lives entirely inside `GetBlockData()`, which grows a `DataVersionGloas` case that decodes into either `Gloas.BlockContents` (self-build) or `Gloas.BeaconBlock` (external-builder), exposing the block object and, when the decoded variant is `BlockContents`, the inline envelope plus blobs and KZG proofs. +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. -Post-consensus object rules: +**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: -- both objects (block + envelope) are signed by the validator's existing BLS share key; the envelope changes the domain, not the signer identity -- each operator's `PostConsensusPartialSig` packet must carry one `PartialSignatureMessage` per required root for the decided variant: - - `Gloas.BeaconBlock`: one entry (block root) - - `Gloas.BlockContents`: two entries (block root + envelope root) +- Self-build is a fallback path in ePBS, rare in practice. Mainnet validator behavior continues to migrate away from local block building. +- Gloas introduces a trustless `execution_payload_bid` p2p market that gives proposers a new fallback for trusted block building without depending on their own EL. +- Adding a second QBFT duty solely to cover self-build envelope signing is significant protocol surface for a use case unlikely to materialize at scale among SSV operators. -Pre-consensus RANDAO flow is unchanged. Operators run QBFT on `ProposerConsensusData`, then sign the objects required by the decided variant. - -Publication order and completion: -- publish the signed beacon block first -- if the decided value is `Gloas.BlockContents` and block publication succeeded, publish the envelope wrapper immediately after, before the PTC 75% deadline so it can influence timely payload attestations +Consequence: SSV proposer slots that resolve to self-build (no acceptable external bid arrived at the BN) will see PTC attestations record `payload_present = FALSE` (see §3) and the proposer forfeits the payload reward for that slot. ### 5. Proposer Preferences Duty @@ -241,11 +233,15 @@ Value checks for Gloas `AttestationData.Index` and PTC `payload_present` / `blob `ProposerPreferences` reconstruction requires cluster-wide agreement on `gas_limit`, which lives in per-operator config rather than `Share`. Divergence produces no reconstructed signature, no gossip publication, and therefore no trustless external builder bids for that slot (the `execution_payload_bid` topic IGNOREs bids with no matching preferences). Same reconstruction failure shape as `ValidatorRegistration` today. +### Self-build slots produce `payload_present = FALSE` + +Because this SIP omits distributed envelope signing (§4), slots where the proposer's BN falls back to self-build will see PTC attestations record `payload_present = FALSE` (§3) and the proposer will forfeit the payload reward boost for that slot. If self-build prevalence becomes a material liveness concern post-Gloas mainnet activation, distributed envelope signing can be added in a follow-up SIP without breaking this baseline. + ## Open Questions / Upstream Watchlist This section is intentionally limited to upstream items that could still change the normative SSV behavior described above. If any of these settle differently, this SIP should be updated. - PTC Beacon API detail drift: the core PTC duty, payload-attestation-data, and pool-submission surfaces are already present in upstream Beacon API `master`, and this SIP assumes those mainline shapes. Watch them for any remaining field, header, or duty-refresh semantic changes. -- Self-build proposer API stabilization: this SIP relies on the current reviewed shape of `produceBlockV4` (`apis/validator/block.v4.yaml`) and stateless envelope publication (`apis/beacon/execution_payload/envelope_post.yaml`), neither of which has been merged to `beacon-APIs/master` yet; both live in [PR #580](https://github.com/ethereum/beacon-APIs/pull/580). Watch for changes to self-build versus block-only response behavior, required submission wrappers, or publication headers. The PR's discussion also covers restructuring `builder_boost_factor` for multi-builder connections; this will reshape SSV node-operator config but does not change SIP-normative behavior (the §4 decode / value-check / post-consensus rules remain variant-agnostic). +- `produceBlockV4` shape stabilization: this SIP relies on the current reviewed shape of `produceBlockV4` (`apis/validator/block.v4.yaml`), which has not been merged to `beacon-APIs/master` yet — it lives in [PR #580](https://github.com/ethereum/beacon-APIs/pull/580). Watch for changes to the response variant discriminator (stateful `BeaconBlock` vs stateless `BlockContents`) and the block submission wrapper shape. The PR's discussion also covers restructuring `builder_boost_factor` for multi-builder connections; this will reshape SSV node-operator config but does not change SIP-normative behavior. - `head_v2` SSE event ([PR #590](https://github.com/ethereum/beacon-APIs/pull/590), Gloas-labeled upstream): adds an ePBS-specific `payload_status` field (`empty` / `full`) that may update mid-slot as the envelope is observed, and renames `{previous,current}_duty_dependent_root` → `{previous,current}_epoch_dependent_root` while adding `next_epoch_dependent_root`. Both §3 (PTC duty refresh) and §5 (proposer-preferences re-emission trigger) key off these dependent-root fields, so implementations will need to consume the renamed fields once the PR lands. `payload_status` is also optionally useful as an early signal for the PTC runner's internal decision cutoff (§3). - Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. From e849eebdcaccf69ccc364b24e83dd56b431e2604 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Fri, 15 May 2026 10:20:45 -0700 Subject: [PATCH 03/62] chore: add gloasbeaconvote --- sips/epbs_support.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index fa2dfb0..4495066 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -25,7 +25,7 @@ Gloas changes validator duties in ways that break a few current SSV assumptions: Key design choices and why: -- **`BeaconVote` gains `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. +- **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`; `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. - **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` is out of scope; see §4 for the rationale. @@ -71,18 +71,20 @@ SSV currently omits `AttestationData.Index` from `BeaconVote` and fills it local #### Required change -`BeaconVote` gains an `AttestationDataIndex` field for Gloas, matching the `phase0.CommitteeIndex` (a `uint64` alias) type of `AttestationData.Index` in consensus specs so reconstruction is a direct field assignment. The restricted Gloas value space (`0` = `EMPTY`, `1` = `FULL` for non-same-slot attestations; `0` for same-slot) is enforced in the value check below, not at the type level. +A new `GloasBeaconVote` struct mirrors `BeaconVote` plus an `AttestationDataIndex` field, matching the `phase0.CommitteeIndex` (a `uint64` alias) type of `AttestationData.Index` in consensus specs so reconstruction is a direct field assignment. Gloas-era QBFT instances decide on `GloasBeaconVote`; pre-Gloas instances continue to decide on the existing `BeaconVote`, whose SSZ layout is unchanged. A separate type (rather than extending `BeaconVote` in place) is the cleanest way to make pre-Gloas and Gloas wire bytes mutually-rejecting on length mismatch, since SSZ derives do not support fork-conditional fields. The restricted Gloas value space (`0` = `EMPTY`, `1` = `FULL` for non-same-slot attestations; `0` for same-slot) is enforced in the value check below, not at the type level. + +After the Gloas fork epoch has activated on all networks and pre-Gloas slots are no longer reachable in normal operation, a follow-up SIP can retire `BeaconVote` and rename `GloasBeaconVote` back to `BeaconVote`. ```go -// Current (ssv-spec types/consensus_data.go) +// Existing (ssv-spec types/consensus_data.go); unchanged type BeaconVote struct { BlockRoot phase0.Root `ssz-size:"32"` Source *phase0.Checkpoint Target *phase0.Checkpoint } -// Gloas-extended -type BeaconVote struct { +// New (Gloas only) +type GloasBeaconVote struct { BlockRoot phase0.Root `ssz-size:"32"` Source *phase0.Checkpoint Target *phase0.Checkpoint @@ -92,10 +94,12 @@ type BeaconVote struct { #### Value check -`BeaconVoteValueCheckF()` becomes fork-aware: +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)`. -- pre-Gloas: reject non-canonical `AttestationDataIndex` values; existing slashability workaround otherwise unchanged. -- Gloas and later: reject `AttestationDataIndex` values other than `0` or `1`; build slashability checks using the real `AttestationDataIndex`. +Pre-Gloas slots continue to run `BeaconVoteValueCheckF()` unchanged. #### Implementation note: aggregation path @@ -221,9 +225,9 @@ const ( ## Security Considerations -### `BeaconVoteValueCheckF` must include `AttestationDataIndex` in slashability checks +### `GloasBeaconVoteValueCheckF` must include `AttestationDataIndex` in slashability checks -Under Gloas, `AttestationData.Index` is part of the attestation data root and therefore part of the double-vote slashing predicate. `BeaconVoteValueCheckF` must reconstruct the full Gloas `AttestationData` with `Index` from the decided `BeaconVote.AttestationDataIndex` before calling `IsAttestationSlashable`; otherwise an operator could sign `index=0` and `index=1` for the same `(source, target)` in the same slot without the predicate tripping. +Under Gloas, `AttestationData.Index` is part of the attestation data root and therefore part of the double-vote slashing predicate. `GloasBeaconVoteValueCheckF` must reconstruct the full Gloas `AttestationData` with `Index` from the decided `GloasBeaconVote.AttestationDataIndex` before calling `IsAttestationSlashable`; otherwise an operator could sign `index=0` and `index=1` for the same `(source, target)` in the same slot without the predicate tripping. ### Payload-status fields are trusted from the QBFT leader From f0777a38881c83fa329aa3f0e4863caae3c9ce75 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 18 May 2026 12:11:47 -0700 Subject: [PATCH 04/62] chore: rename epbs --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 4495066..4bd1b8d 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -4,7 +4,7 @@ ## Summary -Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after epbs, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas) (`ethereum/consensus-specs@f1371480c4`, reviewed 2026-04-17). +Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after ePBS, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas) (`ethereum/consensus-specs@f1371480c4`, reviewed 2026-04-17). Validator client related changes via ePBS: 1. earlier slot deadlines From e892b0c350610789eb02ab84f9f84cf16ad1456f Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 18 May 2026 12:50:13 -0700 Subject: [PATCH 05/62] chore: add same slot attestation context --- sips/epbs_support.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 4bd1b8d..825dbc4 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -99,6 +99,8 @@ A new `GloasBeaconVoteValueCheckF()` mirrors today's `BeaconVoteValueCheckF()` a - 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)`. +The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. + Pre-Gloas slots continue to run `BeaconVoteValueCheckF()` unchanged. #### Implementation note: aggregation path From 1f71a2133ea92ee172f7b622942db50f2ade9665 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 18 May 2026 13:04:39 -0700 Subject: [PATCH 06/62] chore: remove ptc hard deadline statement --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 825dbc4..c93ca0e 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -115,7 +115,7 @@ Relevant consensus-spec references: - [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/beacon-chain.md#payloadattestationdata) - [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) -PTC is a per-slot consensus-layer-selected set of validators that attests to payload and blob availability for the slot's beacon block before the deadline. +PTC is a per-slot consensus-layer-selected set of validators that attests to payload and blob availability for the slot's beacon block. Each validator signs a `PayloadAttestationData` object carrying `beacon_block_root`, `slot`, `payload_present`, and `blob_data_available`, then submits a validator-specific `PayloadAttestationMessage(validator_index, data, signature)` to the beacon node. From 2df7b53a14208f9356162abc39b511b500a53948 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 18 May 2026 13:14:45 -0700 Subject: [PATCH 07/62] chore: update fork choice reject comment --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index c93ca0e..e2405b3 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -121,7 +121,7 @@ Each validator signs a `PayloadAttestationData` object carrying `beacon_block_ro At the start of each epoch, SSV should fetch PTC duties for the next epoch and refresh them on duty-dependent-root changes. Because PTC duty responses may be sparse and incomplete, a changed duty-dependent root for an epoch should replace the cached duties for that epoch rather than being merged. -`PAYLOAD_ATTESTATION_DUE_BPS = 75%` is the consensus-spec-recommended broadcast time, chosen to leave ~25% of the slot for gossip propagation and aggregation by the next slot's proposer. `PayloadAttestationData` values (`payload_present`, `blob_data_available`, `beacon_block_root`) evolve throughout the slot as envelopes and blobs are observed, so runners should delay fetch and QBFT start as late as the DVT round budget permits to maximize the chance `payload_present` reflects the envelope actually arriving. The consensus specs do not prescribe a start time. Within-slot overruns past 75% still reach fork-choice via the wire path but risk missing block inclusion; past slot end the message is dropped (gossip IGNORE, fork-choice wire REJECT on `data.slot == current_slot`), and each missed vote chips at the `PTC_SIZE/2` threshold that governs whether fork-choice extends the payload. +`PAYLOAD_ATTESTATION_DUE_BPS = 75%` is the consensus-spec-recommended broadcast time, chosen to leave ~25% of the slot for gossip propagation and aggregation by the next slot's proposer. `PayloadAttestationData` values (`payload_present`, `blob_data_available`, `beacon_block_root`) evolve throughout the slot as envelopes and blobs are observed, so runners should delay fetch and QBFT start as late as the DVT round budget permits to maximize the chance `payload_present` reflects the envelope actually arriving. The consensus specs do not prescribe a start time. Within-slot overruns past 75% still reach fork-choice via the wire path but risk missing block inclusion; past slot end the message is dropped (gossip IGNORE and fork-choice wire REJECT when `data.slot != current_slot`), and each missed vote chips at the `PTC_SIZE/2` threshold that governs whether fork-choice extends the payload. There is no pre-consensus phase. Operator QBFT instances agree on a stripped `PayloadAttestationVote`: From c80cc99698809d0967c54b22c7231b742e30f424 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 18 May 2026 13:18:47 -0700 Subject: [PATCH 08/62] chore: add domains --- sips/epbs_support.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index e2405b3..1020312 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -141,6 +141,11 @@ This SIP adds a new beacon role `BNRolePTCAttester` and a matching runner role ` ```go // types/beacon_types.go additions +var ( + // ... existing values ... + DomainPTCAttester = [4]byte{0x0C, 0x00, 0x00, 0x00} +) + const ( // ... existing values ... BNRolePTCAttester BeaconRole = 7 @@ -205,6 +210,11 @@ This SIP adds a new beacon role `BNRoleProposerPreferences`, a matching runner r ```go // types/beacon_types.go additions +var ( + // ... existing values ... + DomainProposerPreferences = [4]byte{0x0D, 0x00, 0x00, 0x00} +) + const ( // ... existing values BNRoleProposerPreferences BeaconRole = 8 From 4f0fdabafe8fe53234d84b05238c5db24afc0e43 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 18 May 2026 13:30:39 -0700 Subject: [PATCH 09/62] chore: DataVersionGloas needs gloas arm --- sips/epbs_support.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 1020312..362699c 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -179,6 +179,8 @@ Under Gloas, `produceBlockV4` replaces the pre-Gloas proposer flow; blinded bloc `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`. + 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: From 81c6f888a1ce392751dee6231d9750959df47191 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 18 May 2026 13:46:21 -0700 Subject: [PATCH 10/62] chore: update ProposerPreferences field divergence risks --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 362699c..4ee1798 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -204,7 +204,7 @@ Under Gloas, each proposer declares their preferred `fee_recipient` and `gas_lim Gossip enforces the handshake at the `execution_payload_bid` topic: bids for a slot with no seen `SignedProposerPreferences` are IGNORE'd (not forwarded), and bids whose `fee_recipient` or `gas_limit` disagree with the proposer's preferences are REJECT'd. Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. -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); `gas_limit` lives in operator config (`DefaultGasLimit = 30_000_000` default with runtime overrides), so operators must configure matching values out-of-band. Divergence on `gas_limit` fails reconstruction, same as `ValidatorRegistration` today. +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); `gas_limit` lives in operator config (`DefaultGasLimit = 30_000_000` default with runtime overrides), so operators must configure matching values out-of-band. Either field's divergence would fail reconstruction (same as `ValidatorRegistration` today), but only `gas_limit` is the practical risk because `fee_recipient` lives on cluster-consistent `Share`. Trigger: at each epoch boundary, and on duty-dependent-root changes for the current or next epoch, iterate local validators and emit one duty per slot returned by `get_upcoming_proposal_slots(state, validator_index)` (future slots in the current epoch plus all slots of the next epoch). In the epoch immediately before `GLOAS_FORK_EPOCH`, operators must also emit preferences for any local-validator proposal slots in the first Gloas epoch; otherwise bids for those slots will not propagate during the first post-fork epoch (per the pre-fork subscription note in `p2p-interface.md`). Per the Gloas validator spec, a validator MAY broadcast multiple `SignedProposerPreferences` for the same slot (later messages supersede earlier ones), so this SIP does not hard-cap the number of `SignedProposerPreferences` publications per slot. If the proposer lookahead for an epoch changes, cached duties for that epoch are replaced rather than merged. From bd1e3e4f5ac6ce5f0423928a876b8b44e4824297 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 18 May 2026 16:36:44 -0700 Subject: [PATCH 11/62] chore: bump consensus-specs pin to 4a4937be and tighten proposer preferences 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) --- sips/epbs_support.md | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 4ee1798..8f22cba 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -4,7 +4,7 @@ ## Summary -Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after ePBS, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas) (`ethereum/consensus-specs@f1371480c4`, reviewed 2026-04-17). +Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after ePBS, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas) (`ethereum/consensus-specs@4a4937be`, reviewed 2026-05-18). Validator client related changes via ePBS: 1. earlier slot deadlines @@ -27,7 +27,7 @@ Key design choices and why: - **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`; `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. +- **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. - **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` is out of scope; see §4 for the rationale. ## Specification @@ -38,7 +38,7 @@ All existing validator duty deadlines shift earlier in the slot. A new PTC deadl Relevant consensus-spec references: -- [Validator time parameters](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/validator.md#time-parameters) +- [Validator time parameters](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/validator.md#time-parameters) | Duty | Pre-ePBS | Post-ePBS (Gloas) | |------|----------|--------------------| @@ -52,7 +52,7 @@ Relevant consensus-spec references: Relevant consensus-spec references: -- [Validator attestation changes](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/validator.md#attestation) +- [Validator attestation changes](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/validator.md#attestation) #### Consensus-spec change @@ -99,7 +99,7 @@ A new `GloasBeaconVoteValueCheckF()` mirrors today's `BeaconVoteValueCheckF()` a - 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)`. -The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. +The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. Pre-Gloas slots continue to run `BeaconVoteValueCheckF()` unchanged. @@ -111,9 +111,9 @@ The `BNRoleAggregator` duty (handled by the aggregator-committee runner) fetches Relevant consensus-spec references: -- [Validator payload timeliness attestation flow](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/validator.md#payload-timeliness-attestation) -- [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/beacon-chain.md#payloadattestationdata) -- [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) +- [Validator payload timeliness attestation flow](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/validator.md#payload-timeliness-attestation) +- [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/beacon-chain.md#payloadattestationdata) +- [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) PTC is a per-slot consensus-layer-selected set of validators that attests to payload and blob availability for the slot's beacon block. @@ -121,7 +121,7 @@ Each validator signs a `PayloadAttestationData` object carrying `beacon_block_ro At the start of each epoch, SSV should fetch PTC duties for the next epoch and refresh them on duty-dependent-root changes. Because PTC duty responses may be sparse and incomplete, a changed duty-dependent root for an epoch should replace the cached duties for that epoch rather than being merged. -`PAYLOAD_ATTESTATION_DUE_BPS = 75%` is the consensus-spec-recommended broadcast time, chosen to leave ~25% of the slot for gossip propagation and aggregation by the next slot's proposer. `PayloadAttestationData` values (`payload_present`, `blob_data_available`, `beacon_block_root`) evolve throughout the slot as envelopes and blobs are observed, so runners should delay fetch and QBFT start as late as the DVT round budget permits to maximize the chance `payload_present` reflects the envelope actually arriving. The consensus specs do not prescribe a start time. Within-slot overruns past 75% still reach fork-choice via the wire path but risk missing block inclusion; past slot end the message is dropped (gossip IGNORE and fork-choice wire REJECT when `data.slot != current_slot`), and each missed vote chips at the `PTC_SIZE/2` threshold that governs whether fork-choice extends the payload. +Two coincident 75%-of-slot deadlines bound the runner: `PAYLOAD_ATTESTATION_DUE_BPS = 75%` (consensus-spec-recommended broadcast time, leaving ~25% for gossip propagation and aggregation by the next slot's proposer) and `PAYLOAD_DUE_BPS = 75%` (validator-side observation cutoff after which envelopes do not flip `payload_present` to `True` per the Gloas validator spec). `PayloadAttestationData` values (`payload_present`, `blob_data_available`, `beacon_block_root`) evolve throughout the slot as envelopes and blobs are observed, so runners should target fetch and QBFT start near 75%, as late as the DVT round budget permits, to maximize the chance `payload_present` reflects the envelope actually arriving. The consensus specs do not prescribe a start time. QBFT and broadcast may complete after 75% and still propagate; within-slot overruns reach fork-choice via the wire path but risk missing block inclusion. Past slot end the message is dropped (gossip IGNORE and fork-choice wire REJECT when `data.slot != current_slot`), and each missed vote chips at the `PTC_SIZE/2` threshold that governs whether fork-choice extends the payload. There is no pre-consensus phase. Operator QBFT instances agree on a stripped `PayloadAttestationVote`: @@ -173,7 +173,7 @@ type PTCCommitteeDuty struct { Relevant consensus-spec references: -- [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/validator.md#block-and-sidecar-proposal) +- [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/validator.md#block-and-sidecar-proposal) 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)). @@ -195,18 +195,18 @@ Consequence: SSV proposer slots that resolve to self-build (no acceptable extern Relevant consensus-spec references: -- [Broadcasting SignedProposerPreferences](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/validator.md#broadcasting-signedproposerpreferences) -- [`SignedProposerPreferences` container](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/p2p-interface.md#new-proposerpreferences) -- [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/p2p-interface.md#proposer_preferences) -- [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/p2p-interface.md#execution_payload_bid) +- [Broadcasting SignedProposerPreferences](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/validator.md#broadcasting-signedproposerpreferences) +- [`SignedProposerPreferences` container](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/p2p-interface.md#new-proposerpreferences) +- [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/p2p-interface.md#proposer_preferences) +- [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/p2p-interface.md#execution_payload_bid) -Under Gloas, each proposer declares their preferred `fee_recipient` and `gas_limit` for upcoming proposal slots (future slots in the current epoch plus all proposal slots in the next epoch) by broadcasting `SignedProposerPreferences` on the `proposer_preferences` p2p topic. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks. +Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `proposer_preferences` p2p topic for future proposal slots within the proposer lookahead (the current epoch up to `MIN_SEED_LOOKAHEAD` epochs ahead). The signed `ProposerPreferences` carries `dependent_root`, `proposal_slot`, `validator_index`, `fee_recipient`, and `target_gas_limit`. `dependent_root` pins the proposer-lookahead epoch's seed via `get_proposer_dependent_root(state, epoch)`; operators populate it from the `dependent_root` returned by [`GET /eth/v2/validator/duties/proposer/{epoch}`](https://github.com/ethereum/beacon-APIs/pull/563) for the proposal-slot's epoch. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks. -Gossip enforces the handshake at the `execution_payload_bid` topic: bids for a slot with no seen `SignedProposerPreferences` are IGNORE'd (not forwarded), and bids whose `fee_recipient` or `gas_limit` disagree with the proposer's preferences are REJECT'd. Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. +Gossip enforces the handshake at the `execution_payload_bid` topic: each bid requires a matching `SignedProposerPreferences` for its `(proposal_slot, dependent_root)` (otherwise IGNORE'd, not forwarded). The bid `fee_recipient` must match the preference (mismatch is REJECT'd), and the bid `gas_limit` must be EIP-1559-compatible with the proposer's `target_gas_limit` via `is_gas_limit_target_compatible` (incompatible is IGNORE'd). Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. -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); `gas_limit` lives in operator config (`DefaultGasLimit = 30_000_000` default with runtime overrides), so operators must configure matching values out-of-band. Either field's divergence would fail reconstruction (same as `ValidatorRegistration` today), but only `gas_limit` is the practical risk because `fee_recipient` lives on cluster-consistent `Share`. +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 the current or next epoch, iterate local validators and emit one duty per slot returned by `get_upcoming_proposal_slots(state, validator_index)` (future slots in the current epoch plus all slots of the next epoch). In the epoch immediately before `GLOAS_FORK_EPOCH`, operators must also emit preferences for any local-validator proposal slots in the first Gloas epoch; otherwise bids for those slots will not propagate during the first post-fork epoch (per the pre-fork subscription note in `p2p-interface.md`). Per the Gloas validator spec, a validator MAY broadcast multiple `SignedProposerPreferences` for the same slot (later messages supersede earlier ones), so this SIP does not hard-cap the number of `SignedProposerPreferences` publications per slot. If the proposer lookahead for an epoch changes, cached duties for that epoch are replaced rather than merged. +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. This SIP adds a new beacon role `BNRoleProposerPreferences`, a matching runner role `RoleProposerPreferences`, and a new `PartialSigMsgType` `ProposerPreferencesPartialSig`. @@ -249,7 +249,11 @@ Value checks for Gloas `AttestationData.Index` and PTC `payload_present` / `blob ### Config divergence silently disables trustless external builder bids -`ProposerPreferences` reconstruction requires cluster-wide agreement on `gas_limit`, which lives in per-operator config rather than `Share`. Divergence produces no reconstructed signature, no gossip publication, and therefore no trustless external builder bids for that slot (the `execution_payload_bid` topic IGNOREs bids with no matching preferences). Same reconstruction failure shape as `ValidatorRegistration` today. +`ProposerPreferences` reconstruction requires cluster-wide agreement on `target_gas_limit` (per-operator config) and `dependent_root` (per-operator BN observation). Divergence on either produces no reconstructed signature, no gossip publication, and therefore no matching preference on the `execution_payload_bid` topic; bids for the slot are IGNORE'd by gossip (§5), leaving the BN with no trustless external builder options to return. Same reconstruction failure shape as `ValidatorRegistration` today. + +### Too-early `SignedProposerPreferences` publication pins the wrong preference + +Because the `proposer_preferences` gossip topic accepts only the first valid message per `(dependent_root, proposal_slot, validator_index)` tuple (§5), reconstructing and publishing a preference before all tuple inputs are final can durably pin a wrong-input preference: a later corrected message for the same tuple is dropped by gossip rather than treated as a replacement. Builders keep using the stale preference, and bids matching the corrected values fail the §5 handshake. Operators must therefore hold publication until `dependent_root`, `fee_recipient`, and `target_gas_limit` are all final for the tuple, and re-emit only when the tuple itself changes (notably when `dependent_root` shifts due to reorg, or `proposer_lookahead` reassigns the validator to a different slot). Distinct from the config-divergence entry above: there, divergence prevents publication; here, premature publication pins the wrong preference more durably than no publication would. ### Self-build slots produce `payload_present = FALSE` @@ -262,4 +266,4 @@ This section is intentionally limited to upstream items that could still change - PTC Beacon API detail drift: the core PTC duty, payload-attestation-data, and pool-submission surfaces are already present in upstream Beacon API `master`, and this SIP assumes those mainline shapes. Watch them for any remaining field, header, or duty-refresh semantic changes. - `produceBlockV4` shape stabilization: this SIP relies on the current reviewed shape of `produceBlockV4` (`apis/validator/block.v4.yaml`), which has not been merged to `beacon-APIs/master` yet — it lives in [PR #580](https://github.com/ethereum/beacon-APIs/pull/580). Watch for changes to the response variant discriminator (stateful `BeaconBlock` vs stateless `BlockContents`) and the block submission wrapper shape. The PR's discussion also covers restructuring `builder_boost_factor` for multi-builder connections; this will reshape SSV node-operator config but does not change SIP-normative behavior. - `head_v2` SSE event ([PR #590](https://github.com/ethereum/beacon-APIs/pull/590), Gloas-labeled upstream): adds an ePBS-specific `payload_status` field (`empty` / `full`) that may update mid-slot as the envelope is observed, and renames `{previous,current}_duty_dependent_root` → `{previous,current}_epoch_dependent_root` while adding `next_epoch_dependent_root`. Both §3 (PTC duty refresh) and §5 (proposer-preferences re-emission trigger) key off these dependent-root fields, so implementations will need to consume the renamed fields once the PR lands. `payload_status` is also optionally useful as an early signal for the PTC runner's internal decision cutoff (§3). -- Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/f1371480c4da884398e688d81b030f5280a6a578/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. +- Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. From 8721f2147ed31e123cc2fc557363cb2c11edc4f5 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 20 May 2026 15:20:26 -0700 Subject: [PATCH 12/62] chore: update proposer preferences reconstruction depends on signing_root --- sips/epbs_support.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 8f22cba..1e98a6e 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -27,7 +27,7 @@ Key design choices and why: - **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. +- **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. - **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` is out of scope; see §4 for the rationale. ## Specification @@ -204,9 +204,9 @@ Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `propos Gossip enforces the handshake at the `execution_payload_bid` topic: each bid requires a matching `SignedProposerPreferences` for its `(proposal_slot, dependent_root)` (otherwise IGNORE'd, not forwarded). The bid `fee_recipient` must match the preference (mismatch is REJECT'd), and the bid `gas_limit` must be EIP-1559-compatible with the proposer's `target_gas_limit` via `is_gas_limit_target_compatible` (incompatible is IGNORE'd). Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. -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). +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; 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. +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, 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. This SIP adds a new beacon role `BNRoleProposerPreferences`, a matching runner role `RoleProposerPreferences`, and a new `PartialSigMsgType` `ProposerPreferencesPartialSig`. @@ -249,7 +249,7 @@ Value checks for Gloas `AttestationData.Index` and PTC `payload_present` / `blob ### Config divergence silently disables trustless external builder bids -`ProposerPreferences` reconstruction requires cluster-wide agreement on `target_gas_limit` (per-operator config) and `dependent_root` (per-operator BN observation). Divergence on either produces no reconstructed signature, no gossip publication, and therefore no matching preference on the `execution_payload_bid` topic; bids for the slot are IGNORE'd by gossip (§5), leaving the BN with no trustless external builder options to return. Same reconstruction failure shape as `ValidatorRegistration` today. +`ProposerPreferences` reconstruction requires a quorum of operators to derive the same signing root, which depends on `target_gas_limit` (per-operator config) and `dependent_root` (per-operator BN observation). Divergence on either splits signing roots; if no root reaches threshold, there is no reconstructed signature, no gossip publication, and therefore no matching preference on the `execution_payload_bid` topic; bids for the slot are IGNORE'd by gossip (§5), leaving the BN with no trustless external builder options to return. Same reconstruction failure shape as `ValidatorRegistration` today. ### Too-early `SignedProposerPreferences` publication pins the wrong preference From 0feaf57ae75eeaf3aa370d9c3d3637774ddae91c Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 20 May 2026 15:24:17 -0700 Subject: [PATCH 13/62] chore: update ptc committee based wording --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 1e98a6e..f09c5f3 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -133,7 +133,7 @@ type PayloadAttestationVote struct { } ``` -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. +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. 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. From 7e8b5bd6d4007682d8bd75b06a2f2ac7b617e9e5 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 20 May 2026 16:00:18 -0700 Subject: [PATCH 14/62] chore: add reasoning for why emit proposer preferences in fork before gloas --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index f09c5f3..e8b05d5 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -206,7 +206,7 @@ Gossip enforces the handshake at the `execution_payload_bid` topic: each bid req 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; 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, 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. +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. This SIP adds a new beacon role `BNRoleProposerPreferences`, a matching runner role `RoleProposerPreferences`, and a new `PartialSigMsgType` `ProposerPreferencesPartialSig`. From e01800e239a9111d7fed18d433e8188071d65d58 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Thu, 21 May 2026 08:53:02 -0700 Subject: [PATCH 15/62] =?UTF-8?q?chore:=20clarify=20=C2=A74=20envelope-sig?= =?UTF-8?q?ning=20out-of-scope=20consequence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index e8b05d5..4714d35 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -189,7 +189,7 @@ Pre-consensus RANDAO flow is unchanged. Post-consensus is unchanged: each operat - Gloas introduces a trustless `execution_payload_bid` p2p market that gives proposers a new fallback for trusted block building without depending on their own EL. - Adding a second QBFT duty solely to cover self-build envelope signing is significant protocol surface for a use case unlikely to materialize at scale among SSV operators. -Consequence: SSV proposer slots that resolve to self-build (no acceptable external bid arrived at the BN) will see PTC attestations record `payload_present = FALSE` (see §3) and the proposer forfeits the payload reward for that slot. +Consequence: SSV does not complete the Gloas self-build path. When no acceptable external bid is available for a proposer slot, the cluster cannot produce and publish a valid `SignedExecutionPayloadEnvelope`; the slot's payload is treated as absent (PTC attestations record `payload_present = FALSE`, see §3) and the proposer forfeits the payload reward for that slot. ### 5. Proposer Preferences Duty From 84db5da5bead3aba1e999355d7faa6f7135b42cf Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 25 May 2026 20:08:23 -0400 Subject: [PATCH 16/62] chore: note ptc role numbering leaves room for agg-comm refactor --- sips/epbs_support.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 4714d35..a3c093a 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -158,6 +158,8 @@ const ( ) ``` +`RolePTCCommittee = 7` leaves room for `RoleAggregatorCommittee = 6` from the in-flight ssv-spec [`aggregator-committee`](https://github.com/ssvlabs/ssv-spec/tree/aggregator-committee) refactor, which consolidates `RoleAggregator` and `RoleSyncCommitteeContribution` (gaps at `1` and `3`). + `MapDutyToRunnerRole()` must map `BNRolePTCAttester` to `RolePTCCommittee`. A new `PTCCommitteeDuty` is introduced, reusing the existing `ValidatorDuty`: ```go From 7bf2b3e3f07f3c766ea5b3801748222991b96238 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 25 May 2026 20:13:07 -0400 Subject: [PATCH 17/62] chore: add iota 1 3 explanation --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index a3c093a..e3c57dc 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -158,7 +158,7 @@ const ( ) ``` -`RolePTCCommittee = 7` leaves room for `RoleAggregatorCommittee = 6` from the in-flight ssv-spec [`aggregator-committee`](https://github.com/ssvlabs/ssv-spec/tree/aggregator-committee) refactor, which consolidates `RoleAggregator` and `RoleSyncCommitteeContribution` (gaps at `1` and `3`). +`RunnerRole` values `1` and `3` are reserved for backward-compat decoding of pre-consolidation messages. `MapDutyToRunnerRole()` must map `BNRolePTCAttester` to `RolePTCCommittee`. A new `PTCCommitteeDuty` is introduced, reusing the existing `ValidatorDuty`: From 4efbff45b1d9a3c0b689856ed5d5ae0a0e2ead15 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 25 May 2026 20:28:20 -0400 Subject: [PATCH 18/62] chore: pin GetBlockData link to ssv-spec commit --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index e3c57dc..7502dae 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -181,7 +181,7 @@ Under Gloas, `produceBlockV4` replaces the pre-Gloas proposer flow; blinded bloc `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`. +Although the struct shape is unchanged, [`ProposerConsensusData.GetBlockData()`](https://github.com/ssvlabs/ssv-spec/blob/85ee4f32e4fc22bae8aacf837153aab3dcd6620b/types/consensus_data.go#L175-L237)'s per-version switch (Capella → Fulu today) needs a new `DataVersionGloas` arm that unmarshals `DataSSZ` as `Gloas.BeaconBlock`. 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. From 53992936d07113e1e37c9675bf3dc39744b9edaa Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 25 May 2026 20:31:52 -0400 Subject: [PATCH 19/62] chore: explain pre-Gloas MaxUint64 sentinel rationale --- sips/epbs_support.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 7502dae..02eafc9 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -99,6 +99,8 @@ A new `GloasBeaconVoteValueCheckF()` mirrors today's `BeaconVoteValueCheckF()` a - 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)`. +The existing sentinel is in place because pre-Gloas consensus data carries no `CommitteeIndex`; `math.MaxUint64` keeps `IsAttestationSlashable` from flagging legitimate same-`(source, target, slot, BlockRoot)` attestations as double-votes. + The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. Pre-Gloas slots continue to run `BeaconVoteValueCheckF()` unchanged. From c10617fbbb0bddabd57deca5682ce62d76d777f8 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 25 May 2026 20:43:45 -0400 Subject: [PATCH 20/62] chore: declare PostConsensusPartialSig type for PTC partial sigs --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 02eafc9..9d7e616 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -135,7 +135,7 @@ type PayloadAttestationVote struct { } ``` -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. +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 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. From 07a1cdd29b994f5b2f0deda87a0a8a9c7ddb1b72 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 25 May 2026 20:54:52 -0400 Subject: [PATCH 21/62] chore: drop deprecated fee-recipient anchor; require byte-identical target_gas_limit --- sips/epbs_support.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 9d7e616..452aee0 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -27,7 +27,7 @@ Key design choices and why: - **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. +- **Proposer-preferences is validator-scoped and non-QBFT.** The per-validator `fee_recipient` is configured cluster-side and is cluster-consistent in practice; `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000` in `types/beacon_types.go`, with runtime overrides; operators in a cluster must agree byte-for-byte on the value used at signing time, 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. - **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` is out of scope; see §4 for the rationale. ## Specification @@ -208,7 +208,7 @@ Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `propos Gossip enforces the handshake at the `execution_payload_bid` topic: each bid requires a matching `SignedProposerPreferences` for its `(proposal_slot, dependent_root)` (otherwise IGNORE'd, not forwarded). The bid `fee_recipient` must match the preference (mismatch is REJECT'd), and the bid `gas_limit` must be EIP-1559-compatible with the proposer's `target_gas_limit` via `is_gas_limit_target_compatible` (incompatible is IGNORE'd). Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. -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). +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` is configured cluster-side (cluster-consistent in practice); `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000`, with runtime overrides; operators in a cluster must agree byte-for-byte at signing time); `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. From 1753e359f77acf2b95c2cdbc050af6192b4cbe81 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Tue, 26 May 2026 15:14:18 -0700 Subject: [PATCH 22/62] chore: add SC entry on late dependent_root re-emission timing risk --- sips/epbs_support.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 452aee0..5f8e9c2 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -259,6 +259,10 @@ Value checks for Gloas `AttestationData.Index` and PTC `payload_present` / `blob Because the `proposer_preferences` gossip topic accepts only the first valid message per `(dependent_root, proposal_slot, validator_index)` tuple (§5), reconstructing and publishing a preference before all tuple inputs are final can durably pin a wrong-input preference: a later corrected message for the same tuple is dropped by gossip rather than treated as a replacement. Builders keep using the stale preference, and bids matching the corrected values fail the §5 handshake. Operators must therefore hold publication until `dependent_root`, `fee_recipient`, and `target_gas_limit` are all final for the tuple, and re-emit only when the tuple itself changes (notably when `dependent_root` shifts due to reorg, or `proposer_lookahead` reassigns the validator to a different slot). Distinct from the config-divergence entry above: there, divergence prevents publication; here, premature publication pins the wrong preference more durably than no publication would. +### Late `dependent_root` change near the proposal slot may leave the slot with no matching bid + +`dependent_root` is pinned to a block at the last slot of epoch `p-2` relative to proposal epoch `p` (via `get_proposer_dependent_root` with `MIN_SEED_LOOKAHEAD = 1`), so the dependent block is typically near or past finalization. If a `dependent_root` change arrives close to the proposal slot, the re-emission and builder-bid gossip round-trip may not complete before the proposal deadline. The proposer's BN then has no matching bids available; per §4 (envelope signing out of scope), the slot's payload is empty. The risk is bounded by the `p-2` pinning but not eliminated: the realistic scenario is non-finality periods where the dependent block remains volatile. + ### Self-build slots produce `payload_present = FALSE` Because this SIP omits distributed envelope signing (§4), slots where the proposer's BN falls back to self-build will see PTC attestations record `payload_present = FALSE` (§3) and the proposer will forfeit the payload reward boost for that slot. If self-build prevalence becomes a material liveness concern post-Gloas mainnet activation, distributed envelope signing can be added in a follow-up SIP without breaking this baseline. From 1f18c8480b0ee8f32686a89013751a68a30f8c60 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Tue, 26 May 2026 17:38:22 -0700 Subject: [PATCH 23/62] chore: clarify fork-choice asymmetry for payload_present vs blob_data_available False votes --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 5f8e9c2..a3d5e77 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -135,7 +135,7 @@ type PayloadAttestationVote struct { } ``` -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 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. +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. From 66feb71ed615c0b07dd952c76cce3a2b3084b880 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Thu, 28 May 2026 09:35:02 -0700 Subject: [PATCH 24/62] =?UTF-8?q?chore:=20add=20=C2=A76=20distributed=20en?= =?UTF-8?q?velope=20signing=20duty=20for=20self-build=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 90 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index a3d5e77..41d514f 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -10,7 +10,7 @@ Validator client related changes via ePBS: 1. earlier slot deadlines 2. attestation handling changes, including preservation of the Gloas `attestation.data.index` semantics 3. the new Payload Timeliness Committee (PTC) -4. the Gloas proposer flow using `produceBlockV4`. The SSV cluster signs only the `Gloas.BeaconBlock`; signing of `SignedExecutionPayloadEnvelope` (the validator-signed envelope used in the self-build path) is intentionally out of scope — see §4. +4. the Gloas proposer flow using `produceBlockV4`. The SSV cluster signs the `Gloas.BeaconBlock` in §4 and, on the self-build path, signs `SignedExecutionPayloadEnvelope` as a companion QBFT duty (§6). 5. the new `SignedProposerPreferences` message must be submitted if the node operator wants to be able to select block bids received over p2p ## Motivation @@ -28,7 +28,8 @@ Key design choices and why: - **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.** The per-validator `fee_recipient` is configured cluster-side and is cluster-consistent in practice; `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000` in `types/beacon_types.go`, with runtime overrides; operators in a cluster must agree byte-for-byte on the value used at signing time, 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. -- **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` is out of scope; see §4 for the rationale. +- **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` for the self-build path is covered by a separate companion QBFT duty (§6), keyed by the block QBFT's decided block root. +- **Envelope QBFT uses a blinded envelope shape.** §6's duty runs QBFT over `BlindedExecutionPayloadEnvelope` (`payload` → `payload_root: Root`), whose hash tree root equals the full envelope's. Keeps QBFT messages bounded (~few hundred bytes vs hundreds of KB to ~MB). ## Specification @@ -187,13 +188,7 @@ Although the struct shape is unchanged, [`ProposerConsensusData.GetBlockData()`] 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: - -- Self-build is a fallback path in ePBS, rare in practice. Mainnet validator behavior continues to migrate away from local block building. -- Gloas introduces a trustless `execution_payload_bid` p2p market that gives proposers a new fallback for trusted block building without depending on their own EL. -- Adding a second QBFT duty solely to cover self-build envelope signing is significant protocol surface for a use case unlikely to materialize at scale among SSV operators. - -Consequence: SSV does not complete the Gloas self-build path. When no acceptable external bid is available for a proposer slot, the cluster cannot produce and publish a valid `SignedExecutionPayloadEnvelope`; the slot's payload is treated as absent (PTC attestations record `payload_present = FALSE`, see §3) and the proposer forfeits the payload reward for that slot. +**Envelope signing.** 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. Distributed signing of `SignedExecutionPayloadEnvelope` for the self-build path is specified in §6. ### 5. Proposer Preferences Duty @@ -241,6 +236,76 @@ const ( `MapDutyToRunnerRole()` must map `BNRoleProposerPreferences` to `RoleProposerPreferences`. +### 6. New Duty: Envelope Signing (Self-Build Path) + +Relevant consensus-spec references: + +- [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/beacon-chain.md#executionpayloadenvelope) +- [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/p2p-interface.md#execution_payload) +- [`POST /eth/v1/beacon/execution_payload_envelope` endpoint](https://github.com/ethereum/beacon-APIs/pull/580) (beacon-APIs PR #580, not yet merged) + +On the self-build path (`bid.builder_index == BUILDER_INDEX_SELF_BUILD` per [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732)), the proposer signs `SignedExecutionPayloadEnvelope` after block publication. The SSV cluster runs a second QBFT round to produce this signature. + +#### Blinded envelope type + +To bound QBFT message size, the cluster runs QBFT over a blinded form that substitutes `payload` with `payload_root: Root = hash_tree_root(payload)`. By SSZ Container positional merkleization, `hash_tree_root(BlindedExecutionPayloadEnvelope) == hash_tree_root(ExecutionPayloadEnvelope)`, so a BLS sig over the blinded signing root is valid for the full envelope. + +```go +type BlindedExecutionPayloadEnvelope struct { + PayloadRoot phase0.Root // == hash_tree_root(envelope.payload) + ExecutionRequests electra.ExecutionRequests + BuilderIndex uint64 + BeaconBlockRoot phase0.Root + ParentBeaconBlockRoot phase0.Root +} +``` + +#### Trigger and envelope source + +Fires on the self-build path only, after the §4 block is signed and published. No pre-consensus phase. Envelope source by self-build variant: stateless self-build returns the envelope inline in `BlockContents`; stateful self-build requires a `GET /eth/v1/validator/execution_payload_envelope/{slot}/{beacon_block_root}` to the same BN that served the §4 block (envelope held server-side keyed by that call). + +#### Roles and constants + +```go +// types/beacon_types.go +var DomainBeaconBuilder = [4]byte{0x0B, 0x00, 0x00, 0x00} +const BNRoleEnvelopeBuilder BeaconRole = 9 + +// types/runner_role.go +const RoleEnvelopeBuilder RunnerRole = 9 + +type EnvelopeConsensusData struct { + Duty ValidatorDuty + Version spec.DataVersion + DataSSZ []byte // SSZ-encoded BlindedExecutionPayloadEnvelope +} +``` + +`MapDutyToRunnerRole()` must map `BNRoleEnvelopeBuilder` to `RoleEnvelopeBuilder`. Post-consensus reuses `PostConsensusPartialSig`; the runner role discriminates routing. + +#### 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`. Leader: the §4 block-QBFT decided leader (the only operator whose BN built the on-chain block, so the only one whose envelope's `BeaconBlockRoot` matches §4 and can pass value check). + +#### Value check + +A new `EnvelopeValueCheckF()` accepts the decided blinded envelope if all of: + +- SSZ decode of `DataSSZ` into `BlindedExecutionPayloadEnvelope` succeeds; +- `EnvelopeConsensusData.Duty.{Slot, ValidatorIndex, PubKey}` match the duty the runner was started with; +- `BuilderIndex == BUILDER_INDEX_SELF_BUILD` (external builders sign their own envelopes; this duty applies only to the self-build path); +- `BeaconBlockRoot` matches the block root decided by the §4 block QBFT for the slot. + +No envelope-content validation: `PayloadRoot` (and therefore every constituent field of the underlying `ExecutionPayload` such as `transactions`, `withdrawals`, `state_root`, `block_hash`) is leader-decided and trusted by the cluster. This matches the existing blinded-block trust model in `BNRoleProposer`, where operators do no field-level validation against their local BN view (see Security Considerations). + +#### Post-consensus + +Operators sign the decided `BlindedExecutionPayloadEnvelope`'s signing root under `DOMAIN_BEACON_BUILDER` (`0x0B000000`); by SSZ root-equivalence this is the full envelope's signing root. Partial sigs broadcast as `PostConsensusPartialSig`. + +#### Publication + +Each operator's BN built a different full envelope; only the operator whose blinded form matched the QBFT decision holds the matching full bytes. That operator constructs `SignedExecutionPayloadEnvelope(full_envelope, reconstructed_sig)` and POSTs it to `/eth/v1/beacon/execution_payload_envelope` (beacon-APIs PR #580). Other operators complete without publishing. + ## Security Considerations ### `GloasBeaconVoteValueCheckF` must include `AttestationDataIndex` in slashability checks @@ -261,11 +326,11 @@ Because the `proposer_preferences` gossip topic accepts only the first valid mes ### Late `dependent_root` change near the proposal slot may leave the slot with no matching bid -`dependent_root` is pinned to a block at the last slot of epoch `p-2` relative to proposal epoch `p` (via `get_proposer_dependent_root` with `MIN_SEED_LOOKAHEAD = 1`), so the dependent block is typically near or past finalization. If a `dependent_root` change arrives close to the proposal slot, the re-emission and builder-bid gossip round-trip may not complete before the proposal deadline. The proposer's BN then has no matching bids available; per §4 (envelope signing out of scope), the slot's payload is empty. The risk is bounded by the `p-2` pinning but not eliminated: the realistic scenario is non-finality periods where the dependent block remains volatile. +Late `dependent_root` change tightens the re-emission window. Under non-finality, a deep reorg affecting the end-of-p-2 dependent block forces the proposer to re-emit `SignedProposerPreferences` with the new root; if the re-emission + builder-bid gossip round-trip cannot complete before the proposal deadline, the slot falls through to §6 self-build with a compressed envelope-signing window. -### Self-build slots produce `payload_present = FALSE` +### Envelope-QBFT leader failure misses the slot's envelope -Because this SIP omits distributed envelope signing (§4), slots where the proposer's BN falls back to self-build will see PTC attestations record `payload_present = FALSE` (§3) and the proposer will forfeit the payload reward boost for that slot. If self-build prevalence becomes a material liveness concern post-Gloas mainnet activation, distributed envelope signing can be added in a follow-up SIP without breaking this baseline. +If the §6 envelope-QBFT leader fails between decide and publish, the slot's envelope is missed (only the leader holds matching full envelope bytes; see §6 Publication). PTC records `payload_present = FALSE` (§3); proposer forfeits the payload reward. No worse than the no-envelope-signing baseline for the self-build path. ## Open Questions / Upstream Watchlist @@ -275,3 +340,4 @@ This section is intentionally limited to upstream items that could still change - `produceBlockV4` shape stabilization: this SIP relies on the current reviewed shape of `produceBlockV4` (`apis/validator/block.v4.yaml`), which has not been merged to `beacon-APIs/master` yet — it lives in [PR #580](https://github.com/ethereum/beacon-APIs/pull/580). Watch for changes to the response variant discriminator (stateful `BeaconBlock` vs stateless `BlockContents`) and the block submission wrapper shape. The PR's discussion also covers restructuring `builder_boost_factor` for multi-builder connections; this will reshape SSV node-operator config but does not change SIP-normative behavior. - `head_v2` SSE event ([PR #590](https://github.com/ethereum/beacon-APIs/pull/590), Gloas-labeled upstream): adds an ePBS-specific `payload_status` field (`empty` / `full`) that may update mid-slot as the envelope is observed, and renames `{previous,current}_duty_dependent_root` → `{previous,current}_epoch_dependent_root` while adding `next_epoch_dependent_root`. Both §3 (PTC duty refresh) and §5 (proposer-preferences re-emission trigger) key off these dependent-root fields, so implementations will need to consume the renamed fields once the PR lands. `payload_status` is also optionally useful as an early signal for the PTC runner's internal decision cutoff (§3). - Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. +- Envelope publication and fetch endpoints: §6 specifies `POST /eth/v1/beacon/execution_payload_envelope` for the self-build envelope-signing duty (publication path) and `GET /eth/v1/validator/execution_payload_envelope/{slot}/{beacon_block_root}` for the runner-side envelope fetch (retrieval from the same BN that returned the `BlockContents` response). Both live in [beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580), which has not been merged to `beacon-APIs/master`. Watch for endpoint path, request/response body, and SSZ encoding changes. From b6b4a822415b07cd370ae6587c16efcd928659eb Mon Sep 17 00:00:00 2001 From: shane-moore Date: Thu, 28 May 2026 10:04:07 -0700 Subject: [PATCH 25/62] =?UTF-8?q?chore:=20make=20=C2=A74=20BlockContents?= =?UTF-8?q?=20runner=20behavior=20explicit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 41d514f..6342c11 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -182,7 +182,7 @@ Relevant consensus-spec references: 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. +`ProposerConsensusData` is preserved: its struct shape (`Duty`, `Version`, `DataSSZ []byte`) is unchanged. `DataSSZ` carries the SSZ-encoded `Gloas.BeaconBlock`. The stateless `BlockContents` variant is handled identically: the cluster extracts the block into `DataSSZ` for QBFT; the inline envelope, blobs, and KZG proofs are handled by §6. Although the struct shape is unchanged, [`ProposerConsensusData.GetBlockData()`](https://github.com/ssvlabs/ssv-spec/blob/85ee4f32e4fc22bae8aacf837153aab3dcd6620b/types/consensus_data.go#L175-L237)'s per-version switch (Capella → Fulu today) needs a new `DataVersionGloas` arm that unmarshals `DataSSZ` as `Gloas.BeaconBlock`. From 05cd5565da10e0ad386d10237302118a664fc584 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Thu, 28 May 2026 10:53:50 -0700 Subject: [PATCH 26/62] chore: bump consensus-specs pin --- sips/epbs_support.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 6342c11..b8101b1 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -4,7 +4,7 @@ ## Summary -Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after ePBS, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas) (`ethereum/consensus-specs@4a4937be`, reviewed 2026-05-18). +Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after ePBS, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas) (`ethereum/consensus-specs@5898db97e`, reviewed 2026-05-28). Validator client related changes via ePBS: 1. earlier slot deadlines @@ -39,7 +39,7 @@ All existing validator duty deadlines shift earlier in the slot. A new PTC deadl Relevant consensus-spec references: -- [Validator time parameters](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/validator.md#time-parameters) +- [Validator time parameters](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/validator.md#time-parameters) | Duty | Pre-ePBS | Post-ePBS (Gloas) | |------|----------|--------------------| @@ -53,7 +53,7 @@ Relevant consensus-spec references: Relevant consensus-spec references: -- [Validator attestation changes](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/validator.md#attestation) +- [Validator attestation changes](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/validator.md#attestation) #### Consensus-spec change @@ -102,7 +102,7 @@ A new `GloasBeaconVoteValueCheckF()` mirrors today's `BeaconVoteValueCheckF()` a The existing sentinel is in place because pre-Gloas consensus data carries no `CommitteeIndex`; `math.MaxUint64` keeps `IsAttestationSlashable` from flagging legitimate same-`(source, target, slot, BlockRoot)` attestations as double-votes. -The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. +The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. Pre-Gloas slots continue to run `BeaconVoteValueCheckF()` unchanged. @@ -114,9 +114,9 @@ The `BNRoleAggregator` duty (handled by the aggregator-committee runner) fetches Relevant consensus-spec references: -- [Validator payload timeliness attestation flow](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/validator.md#payload-timeliness-attestation) -- [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/beacon-chain.md#payloadattestationdata) -- [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) +- [Validator payload timeliness attestation flow](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/validator.md#payload-timeliness-attestation) +- [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/beacon-chain.md#payloadattestationdata) +- [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) PTC is a per-slot consensus-layer-selected set of validators that attests to payload and blob availability for the slot's beacon block. @@ -178,7 +178,7 @@ type PTCCommitteeDuty struct { Relevant consensus-spec references: -- [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/validator.md#block-and-sidecar-proposal) +- [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/validator.md#block-and-sidecar-proposal) 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)). @@ -194,10 +194,10 @@ Pre-consensus RANDAO flow is unchanged. Post-consensus is unchanged: each operat Relevant consensus-spec references: -- [Broadcasting SignedProposerPreferences](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/validator.md#broadcasting-signedproposerpreferences) -- [`SignedProposerPreferences` container](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/p2p-interface.md#new-proposerpreferences) -- [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/p2p-interface.md#proposer_preferences) -- [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/p2p-interface.md#execution_payload_bid) +- [Broadcasting SignedProposerPreferences](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/validator.md#broadcasting-signedproposerpreferences) +- [`SignedProposerPreferences` container](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/p2p-interface.md#new-proposerpreferences) +- [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/p2p-interface.md#proposer_preferences) +- [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/p2p-interface.md#execution_payload_bid) Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `proposer_preferences` p2p topic for future proposal slots within the proposer lookahead (the current epoch up to `MIN_SEED_LOOKAHEAD` epochs ahead). The signed `ProposerPreferences` carries `dependent_root`, `proposal_slot`, `validator_index`, `fee_recipient`, and `target_gas_limit`. `dependent_root` pins the proposer-lookahead epoch's seed via `get_proposer_dependent_root(state, epoch)`; operators populate it from the `dependent_root` returned by [`GET /eth/v2/validator/duties/proposer/{epoch}`](https://github.com/ethereum/beacon-APIs/pull/563) for the proposal-slot's epoch. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks. @@ -240,8 +240,8 @@ const ( Relevant consensus-spec references: -- [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/beacon-chain.md#executionpayloadenvelope) -- [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/p2p-interface.md#execution_payload) +- [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/beacon-chain.md#executionpayloadenvelope) +- [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/p2p-interface.md#execution_payload) - [`POST /eth/v1/beacon/execution_payload_envelope` endpoint](https://github.com/ethereum/beacon-APIs/pull/580) (beacon-APIs PR #580, not yet merged) On the self-build path (`bid.builder_index == BUILDER_INDEX_SELF_BUILD` per [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732)), the proposer signs `SignedExecutionPayloadEnvelope` after block publication. The SSV cluster runs a second QBFT round to produce this signature. @@ -339,5 +339,5 @@ This section is intentionally limited to upstream items that could still change - PTC Beacon API detail drift: the core PTC duty, payload-attestation-data, and pool-submission surfaces are already present in upstream Beacon API `master`, and this SIP assumes those mainline shapes. Watch them for any remaining field, header, or duty-refresh semantic changes. - `produceBlockV4` shape stabilization: this SIP relies on the current reviewed shape of `produceBlockV4` (`apis/validator/block.v4.yaml`), which has not been merged to `beacon-APIs/master` yet — it lives in [PR #580](https://github.com/ethereum/beacon-APIs/pull/580). Watch for changes to the response variant discriminator (stateful `BeaconBlock` vs stateless `BlockContents`) and the block submission wrapper shape. The PR's discussion also covers restructuring `builder_boost_factor` for multi-builder connections; this will reshape SSV node-operator config but does not change SIP-normative behavior. - `head_v2` SSE event ([PR #590](https://github.com/ethereum/beacon-APIs/pull/590), Gloas-labeled upstream): adds an ePBS-specific `payload_status` field (`empty` / `full`) that may update mid-slot as the envelope is observed, and renames `{previous,current}_duty_dependent_root` → `{previous,current}_epoch_dependent_root` while adding `next_epoch_dependent_root`. Both §3 (PTC duty refresh) and §5 (proposer-preferences re-emission trigger) key off these dependent-root fields, so implementations will need to consume the renamed fields once the PR lands. `payload_status` is also optionally useful as an early signal for the PTC runner's internal decision cutoff (§3). -- Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/4a4937bea332d72a55a76aaebcb97fbcdc189f69/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. +- Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. - Envelope publication and fetch endpoints: §6 specifies `POST /eth/v1/beacon/execution_payload_envelope` for the self-build envelope-signing duty (publication path) and `GET /eth/v1/validator/execution_payload_envelope/{slot}/{beacon_block_root}` for the runner-side envelope fetch (retrieval from the same BN that returned the `BlockContents` response). Both live in [beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580), which has not been merged to `beacon-APIs/master`. Watch for endpoint path, request/response body, and SSZ encoding changes. From c5417cb4a0b6fe6872eaf7a5891e24b8bbc43007 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 1 Jun 2026 10:58:40 -0700 Subject: [PATCH 27/62] chore: note PTC beacon_block_root slot-binding is left to gossip (accept-and-sign) --- sips/epbs_support.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index b8101b1..c57c0f1 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -138,7 +138,7 @@ type PayloadAttestationVote struct { 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. +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, nor for slot-binding to `duty.slot`, matching existing `BeaconVote.BlockRoot` handling (see Security Considerations: the runner accepts that a slot-mismatched root yields a message the network ignores). PTC attestations are not in the beacon chain slashing predicate, so no slashability call is required. This SIP adds a new beacon role `BNRolePTCAttester` and a matching runner role `RolePTCCommittee`. @@ -316,6 +316,10 @@ Under Gloas, `AttestationData.Index` is part of the attestation data root and th Value checks for Gloas `AttestationData.Index` and PTC `payload_present` / `blob_data_available` do not require the decided value to match each operator's local BN view. Requiring local agreement would fail QBFT rounds whenever operators observe the envelope at slightly different times around the 75% deadline, a normal gossip-lag scenario. Accepted tradeoff: a malicious QBFT leader can push a value contrary to the cluster's majority BN observation. This matches existing ssv-spec treatment of `BeaconVote.BlockRoot`, which is trusted from the leader because BNs legitimately diverge on fork-choice head. +### PTC `beacon_block_root` slot-binding is left to gossip, not enforced in the value check + +Gloas gossip adds an IGNORE-level check that the block at `data.beacon_block_root` is at `data.slot` ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/6370819a35e9558822ef024126cc09ee3666827d/specs/gloas/p2p-interface.md#L346-L347), added after this SIP's pinned snapshot). SSV takes `BeaconBlockRoot` from the QBFT-decided value while `data.slot` comes from the duty, so a faulty leader could propose an off-slot root. The runner does not check `block.slot == duty.slot`: it holds no local block, and validating the slot would require an extra beacon-node query on the late-slot hot path. A slot-mismatched root is signed but ignored by peers, wasting that round's PTC votes; this is bounded, non-slashable, and equals a missed vote (the False-vote / missed-vote equivalence above), and arises only under leader fault. + ### Config divergence silently disables trustless external builder bids `ProposerPreferences` reconstruction requires a quorum of operators to derive the same signing root, which depends on `target_gas_limit` (per-operator config) and `dependent_root` (per-operator BN observation). Divergence on either splits signing roots; if no root reaches threshold, there is no reconstructed signature, no gossip publication, and therefore no matching preference on the `execution_payload_bid` topic; bids for the slot are IGNORE'd by gossip (§5), leaving the BN with no trustless external builder options to return. Same reconstruction failure shape as `ValidatorRegistration` today. From 9aa954a97a061c55f1319d20c481bff405ecffe5 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 1 Jun 2026 11:25:07 -0700 Subject: [PATCH 28/62] chore: bump consensus-specs pin to e34dbbb33 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). --- sips/epbs_support.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index c57c0f1..f7249d5 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -4,7 +4,7 @@ ## Summary -Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after ePBS, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas) (`ethereum/consensus-specs@5898db97e`, reviewed 2026-05-28). +Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after ePBS, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas) (`ethereum/consensus-specs@e34dbbb33`, reviewed 2026-06-01). Validator client related changes via ePBS: 1. earlier slot deadlines @@ -39,7 +39,7 @@ All existing validator duty deadlines shift earlier in the slot. A new PTC deadl Relevant consensus-spec references: -- [Validator time parameters](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/validator.md#time-parameters) +- [Validator time parameters](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/validator.md#time-parameters) | Duty | Pre-ePBS | Post-ePBS (Gloas) | |------|----------|--------------------| @@ -53,7 +53,7 @@ Relevant consensus-spec references: Relevant consensus-spec references: -- [Validator attestation changes](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/validator.md#attestation) +- [Validator attestation changes](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/validator.md#attestation) #### Consensus-spec change @@ -102,7 +102,7 @@ A new `GloasBeaconVoteValueCheckF()` mirrors today's `BeaconVoteValueCheckF()` a The existing sentinel is in place because pre-Gloas consensus data carries no `CommitteeIndex`; `math.MaxUint64` keeps `IsAttestationSlashable` from flagging legitimate same-`(source, target, slot, BlockRoot)` attestations as double-votes. -The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. +The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. Pre-Gloas slots continue to run `BeaconVoteValueCheckF()` unchanged. @@ -114,9 +114,9 @@ The `BNRoleAggregator` duty (handled by the aggregator-committee runner) fetches Relevant consensus-spec references: -- [Validator payload timeliness attestation flow](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/validator.md#payload-timeliness-attestation) -- [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/beacon-chain.md#payloadattestationdata) -- [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) +- [Validator payload timeliness attestation flow](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/validator.md#payload-timeliness-attestation) +- [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/beacon-chain.md#payloadattestationdata) +- [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) PTC is a per-slot consensus-layer-selected set of validators that attests to payload and blob availability for the slot's beacon block. @@ -178,7 +178,7 @@ type PTCCommitteeDuty struct { Relevant consensus-spec references: -- [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/validator.md#block-and-sidecar-proposal) +- [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/validator.md#block-and-sidecar-proposal) 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)). @@ -194,10 +194,10 @@ Pre-consensus RANDAO flow is unchanged. Post-consensus is unchanged: each operat Relevant consensus-spec references: -- [Broadcasting SignedProposerPreferences](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/validator.md#broadcasting-signedproposerpreferences) -- [`SignedProposerPreferences` container](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/p2p-interface.md#new-proposerpreferences) -- [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/p2p-interface.md#proposer_preferences) -- [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/p2p-interface.md#execution_payload_bid) +- [Broadcasting SignedProposerPreferences](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/validator.md#broadcasting-signedproposerpreferences) +- [`SignedProposerPreferences` container](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#new-proposerpreferences) +- [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#proposer_preferences) +- [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#execution_payload_bid) Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `proposer_preferences` p2p topic for future proposal slots within the proposer lookahead (the current epoch up to `MIN_SEED_LOOKAHEAD` epochs ahead). The signed `ProposerPreferences` carries `dependent_root`, `proposal_slot`, `validator_index`, `fee_recipient`, and `target_gas_limit`. `dependent_root` pins the proposer-lookahead epoch's seed via `get_proposer_dependent_root(state, epoch)`; operators populate it from the `dependent_root` returned by [`GET /eth/v2/validator/duties/proposer/{epoch}`](https://github.com/ethereum/beacon-APIs/pull/563) for the proposal-slot's epoch. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks. @@ -240,8 +240,8 @@ const ( Relevant consensus-spec references: -- [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/beacon-chain.md#executionpayloadenvelope) -- [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/p2p-interface.md#execution_payload) +- [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/beacon-chain.md#executionpayloadenvelope) +- [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#execution_payload) - [`POST /eth/v1/beacon/execution_payload_envelope` endpoint](https://github.com/ethereum/beacon-APIs/pull/580) (beacon-APIs PR #580, not yet merged) On the self-build path (`bid.builder_index == BUILDER_INDEX_SELF_BUILD` per [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732)), the proposer signs `SignedExecutionPayloadEnvelope` after block publication. The SSV cluster runs a second QBFT round to produce this signature. @@ -318,7 +318,7 @@ Value checks for Gloas `AttestationData.Index` and PTC `payload_present` / `blob ### PTC `beacon_block_root` slot-binding is left to gossip, not enforced in the value check -Gloas gossip adds an IGNORE-level check that the block at `data.beacon_block_root` is at `data.slot` ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/6370819a35e9558822ef024126cc09ee3666827d/specs/gloas/p2p-interface.md#L346-L347), added after this SIP's pinned snapshot). SSV takes `BeaconBlockRoot` from the QBFT-decided value while `data.slot` comes from the duty, so a faulty leader could propose an off-slot root. The runner does not check `block.slot == duty.slot`: it holds no local block, and validating the slot would require an extra beacon-node query on the late-slot hot path. A slot-mismatched root is signed but ignored by peers, wasting that round's PTC votes; this is bounded, non-slashable, and equals a missed vote (the False-vote / missed-vote equivalence above), and arises only under leader fault. +Gloas gossip adds an IGNORE-level check that the block at `data.beacon_block_root` is at `data.slot` ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#L346-L347)). SSV takes `BeaconBlockRoot` from the QBFT-decided value while `data.slot` comes from the duty, so a faulty leader could propose an off-slot root. The runner does not check `block.slot == duty.slot`: it holds no local block, and validating the slot would require an extra beacon-node query on the late-slot hot path. A slot-mismatched root is signed but ignored by peers, wasting that round's PTC votes; this is bounded, non-slashable, and equals a missed vote (the False-vote / missed-vote equivalence above), and arises only under leader fault. ### Config divergence silently disables trustless external builder bids @@ -343,5 +343,5 @@ This section is intentionally limited to upstream items that could still change - PTC Beacon API detail drift: the core PTC duty, payload-attestation-data, and pool-submission surfaces are already present in upstream Beacon API `master`, and this SIP assumes those mainline shapes. Watch them for any remaining field, header, or duty-refresh semantic changes. - `produceBlockV4` shape stabilization: this SIP relies on the current reviewed shape of `produceBlockV4` (`apis/validator/block.v4.yaml`), which has not been merged to `beacon-APIs/master` yet — it lives in [PR #580](https://github.com/ethereum/beacon-APIs/pull/580). Watch for changes to the response variant discriminator (stateful `BeaconBlock` vs stateless `BlockContents`) and the block submission wrapper shape. The PR's discussion also covers restructuring `builder_boost_factor` for multi-builder connections; this will reshape SSV node-operator config but does not change SIP-normative behavior. - `head_v2` SSE event ([PR #590](https://github.com/ethereum/beacon-APIs/pull/590), Gloas-labeled upstream): adds an ePBS-specific `payload_status` field (`empty` / `full`) that may update mid-slot as the envelope is observed, and renames `{previous,current}_duty_dependent_root` → `{previous,current}_epoch_dependent_root` while adding `next_epoch_dependent_root`. Both §3 (PTC duty refresh) and §5 (proposer-preferences re-emission trigger) key off these dependent-root fields, so implementations will need to consume the renamed fields once the PR lands. `payload_status` is also optionally useful as an early signal for the PTC runner's internal decision cutoff (§3). -- Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/5898db97eccadd3407e1216035c831bca559d2fe/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. +- Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. - Envelope publication and fetch endpoints: §6 specifies `POST /eth/v1/beacon/execution_payload_envelope` for the self-build envelope-signing duty (publication path) and `GET /eth/v1/validator/execution_payload_envelope/{slot}/{beacon_block_root}` for the runner-side envelope fetch (retrieval from the same BN that returned the `BlockContents` response). Both live in [beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580), which has not been merged to `beacon-APIs/master`. Watch for endpoint path, request/response body, and SSZ encoding changes. From f7fdee242502957b983279af37a52798cb4edfd1 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 1 Jun 2026 12:12:45 -0700 Subject: [PATCH 29/62] =?UTF-8?q?chore:=20add=20=C2=A76=20envelope=20publi?= =?UTF-8?q?cation=20timing=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- sips/epbs_support.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index f7249d5..32f3b93 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -264,6 +264,8 @@ type BlindedExecutionPayloadEnvelope struct { Fires on the self-build path only, after the §4 block is signed and published. No pre-consensus phase. Envelope source by self-build variant: stateless self-build returns the envelope inline in `BlockContents`; stateful self-build requires a `GET /eth/v1/validator/execution_payload_envelope/{slot}/{beacon_block_root}` to the same BN that served the §4 block (envelope held server-side keyed by that call). +The duty must target publishing the signed envelope before `get_payload_due_ms()` (the `PAYLOAD_DUE_BPS` 75% cutoff, §3), with margin for gossip to reach PTC beacon nodes before then. The §4 block is already out by the attestation deadline (§1), so the round has the intervening block-to-payload gap to work in. An envelope that misses the cutoff makes honest PTC validators vote `payload_present = False`, which can leave the next slot building on the empty parent (§3); this is the bounded missed-envelope degradation in Security Considerations. + #### Roles and constants ```go @@ -332,9 +334,9 @@ Because the `proposer_preferences` gossip topic accepts only the first valid mes Late `dependent_root` change tightens the re-emission window. Under non-finality, a deep reorg affecting the end-of-p-2 dependent block forces the proposer to re-emit `SignedProposerPreferences` with the new root; if the re-emission + builder-bid gossip round-trip cannot complete before the proposal deadline, the slot falls through to §6 self-build with a compressed envelope-signing window. -### Envelope-QBFT leader failure misses the slot's envelope +### Envelope-QBFT leader failure or late publication misses the slot's envelope -If the §6 envelope-QBFT leader fails between decide and publish, the slot's envelope is missed (only the leader holds matching full envelope bytes; see §6 Publication). PTC records `payload_present = FALSE` (§3); proposer forfeits the payload reward. No worse than the no-envelope-signing baseline for the self-build path. +The slot's envelope is missed if the §6 envelope-QBFT leader fails between decide and publish (only the leader holds matching full envelope bytes; see §6 Publication), or if the envelope-QBFT round completes after `get_payload_due_ms()` so the signed envelope reaches PTC validators too late to observe before the cutoff. In either case PTC records `payload_present = FALSE` (§3); proposer forfeits the payload reward. No worse than the no-envelope-signing baseline for the self-build path. ## Open Questions / Upstream Watchlist From c41941e07a5261b4a8eb95ebb1d0712e3c5d528a Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 1 Jun 2026 12:42:38 -0700 Subject: [PATCH 30/62] =?UTF-8?q?chore:=20=C2=A76=20envelope=20publication?= =?UTF-8?q?=20is=20content-match,=20not=20leader=20identity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- sips/epbs_support.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 32f3b93..8c913a5 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -287,7 +287,7 @@ type EnvelopeConsensusData struct { #### 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`. Leader: the §4 block-QBFT decided leader (the only operator whose BN built the on-chain block, so the only one whose envelope's `BeaconBlockRoot` matches §4 and can pass value check). +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). #### Value check @@ -334,9 +334,9 @@ Because the `proposer_preferences` gossip topic accepts only the first valid mes Late `dependent_root` change tightens the re-emission window. Under non-finality, a deep reorg affecting the end-of-p-2 dependent block forces the proposer to re-emit `SignedProposerPreferences` with the new root; if the re-emission + builder-bid gossip round-trip cannot complete before the proposal deadline, the slot falls through to §6 self-build with a compressed envelope-signing window. -### Envelope-QBFT leader failure or late publication misses the slot's envelope +### Matching-envelope operator failure or late publication misses the slot's envelope -The slot's envelope is missed if the §6 envelope-QBFT leader fails between decide and publish (only the leader holds matching full envelope bytes; see §6 Publication), or if the envelope-QBFT round completes after `get_payload_due_ms()` so the signed envelope reaches PTC validators too late to observe before the cutoff. In either case PTC records `payload_present = FALSE` (§3); proposer forfeits the payload reward. No worse than the no-envelope-signing baseline for the self-build path. +The slot's envelope is missed if the operator whose envelope blinds to the §6-decided value (the only one holding the matching full bytes; see §6 Publication) fails before publishing it, or if the envelope-QBFT round completes after `get_payload_due_ms()` so the signed envelope reaches PTC validators too late to observe before the cutoff. In either case PTC records `payload_present = FALSE` (§3); proposer forfeits the payload reward. No worse than the no-envelope-signing baseline for the self-build path. ## Open Questions / Upstream Watchlist From 8de1d6776799f86dffb94107ae1eed6fd7b6f6aa Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 1 Jun 2026 13:06:34 -0700 Subject: [PATCH 31/62] =?UTF-8?q?chore:=20=C2=A76=20publication=20body=20s?= =?UTF-8?q?plit=20by=20self-build=20variant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 8c913a5..cee8fb0 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -306,7 +306,7 @@ Operators sign the decided `BlindedExecutionPayloadEnvelope`'s signing root unde #### Publication -Each operator's BN built a different full envelope; only the operator whose blinded form matched the QBFT decision holds the matching full bytes. That operator constructs `SignedExecutionPayloadEnvelope(full_envelope, reconstructed_sig)` and POSTs it to `/eth/v1/beacon/execution_payload_envelope` (beacon-APIs PR #580). Other operators complete without publishing. +Each operator's BN built a different full envelope; only the operator whose blinded form matched the QBFT decision holds the matching full bytes. That operator reconstructs the signature and POSTs the signed envelope to `/eth/v1/beacon/execution_payload_envelope` ([beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580)); the body depends on the self-build variant (§6 Trigger). For stateless self-build the envelope arrived inline in `BlockContents`, so that operator also holds the matching blobs and KZG proofs and publishes `SignedExecutionPayloadEnvelopeContents` (the `SignedExecutionPayloadEnvelope` plus `blobs` and `kzg_proofs`). For stateful self-build the BN that served the envelope already cached the side data, so a bare `SignedExecutionPayloadEnvelope(full_envelope, reconstructed_sig)` suffices. Other operators complete without publishing. ## Security Considerations From 0744f785303aab6cd6fa3f5c643d11961805e913 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 1 Jun 2026 16:08:34 -0700 Subject: [PATCH 32/62] chore: refresh beacon-API open questions against v5.0.0-alpha.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- sips/epbs_support.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index cee8fb0..57e004d 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -126,6 +126,8 @@ At the start of each epoch, SSV should fetch PTC duties for the next epoch and r Two coincident 75%-of-slot deadlines bound the runner: `PAYLOAD_ATTESTATION_DUE_BPS = 75%` (consensus-spec-recommended broadcast time, leaving ~25% for gossip propagation and aggregation by the next slot's proposer) and `PAYLOAD_DUE_BPS = 75%` (validator-side observation cutoff after which envelopes do not flip `payload_present` to `True` per the Gloas validator spec). `PayloadAttestationData` values (`payload_present`, `blob_data_available`, `beacon_block_root`) evolve throughout the slot as envelopes and blobs are observed, so runners should target fetch and QBFT start near 75%, as late as the DVT round budget permits, to maximize the chance `payload_present` reflects the envelope actually arriving. The consensus specs do not prescribe a start time. QBFT and broadcast may complete after 75% and still propagate; within-slot overruns reach fork-choice via the wire path but risk missing block inclusion. Past slot end the message is dropped (gossip IGNORE and fork-choice wire REJECT when `data.slot != current_slot`), and each missed vote chips at the `PTC_SIZE/2` threshold that governs whether fork-choice extends the payload. +The beacon node also emits SSE events a runner may consume as a push trigger for when to fetch and start QBFT, instead of polling toward the cutoff: `execution_payload_gossip` and `execution_payload` fire when a `SignedExecutionPayloadEnvelope` passes `execution_payload`-topic gossip validation and when it is imported into fork choice, and `execution_payload_available` fires once the node has verified the payload and blobs are available and ready for payload attestation ([beacon-APIs event stream](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/eventstream/index.yaml)). These are timing signals only: `payload_present` and `blob_data_available` still come from the BN-computed `PayloadAttestationData` at fetch time, bounded by the same cutoff. + There is no pre-consensus phase. Operator QBFT instances agree on a stripped `PayloadAttestationVote`: ```go @@ -199,7 +201,7 @@ Relevant consensus-spec references: - [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#proposer_preferences) - [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#execution_payload_bid) -Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `proposer_preferences` p2p topic for future proposal slots within the proposer lookahead (the current epoch up to `MIN_SEED_LOOKAHEAD` epochs ahead). The signed `ProposerPreferences` carries `dependent_root`, `proposal_slot`, `validator_index`, `fee_recipient`, and `target_gas_limit`. `dependent_root` pins the proposer-lookahead epoch's seed via `get_proposer_dependent_root(state, epoch)`; operators populate it from the `dependent_root` returned by [`GET /eth/v2/validator/duties/proposer/{epoch}`](https://github.com/ethereum/beacon-APIs/pull/563) for the proposal-slot's epoch. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks. +Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `proposer_preferences` p2p topic for future proposal slots within the proposer lookahead (the current epoch up to `MIN_SEED_LOOKAHEAD` epochs ahead). The signed `ProposerPreferences` carries `dependent_root`, `proposal_slot`, `validator_index`, `fee_recipient`, and `target_gas_limit`. `dependent_root` pins the proposer-lookahead epoch's seed via `get_proposer_dependent_root(state, epoch)`; operators populate it from the `dependent_root` returned by [`GET /eth/v2/validator/duties/proposer/{epoch}`](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/validator/duties/proposer.v2.yaml) for the proposal-slot's epoch. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks. Gossip enforces the handshake at the `execution_payload_bid` topic: each bid requires a matching `SignedProposerPreferences` for its `(proposal_slot, dependent_root)` (otherwise IGNORE'd, not forwarded). The bid `fee_recipient` must match the preference (mismatch is REJECT'd), and the bid `gas_limit` must be EIP-1559-compatible with the proposer's `target_gas_limit` via `is_gas_limit_target_compatible` (incompatible is IGNORE'd). Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. @@ -242,7 +244,7 @@ Relevant consensus-spec references: - [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/beacon-chain.md#executionpayloadenvelope) - [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#execution_payload) -- [`POST /eth/v1/beacon/execution_payload_envelope` endpoint](https://github.com/ethereum/beacon-APIs/pull/580) (beacon-APIs PR #580, not yet merged) +- [`POST /eth/v1/beacon/execution_payload_envelopes` endpoint](https://github.com/ethereum/beacon-APIs/pull/580) (beacon-APIs PR #580, not yet merged) On the self-build path (`bid.builder_index == BUILDER_INDEX_SELF_BUILD` per [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732)), the proposer signs `SignedExecutionPayloadEnvelope` after block publication. The SSV cluster runs a second QBFT round to produce this signature. @@ -262,7 +264,7 @@ type BlindedExecutionPayloadEnvelope struct { #### Trigger and envelope source -Fires on the self-build path only, after the §4 block is signed and published. No pre-consensus phase. Envelope source by self-build variant: stateless self-build returns the envelope inline in `BlockContents`; stateful self-build requires a `GET /eth/v1/validator/execution_payload_envelope/{slot}/{beacon_block_root}` to the same BN that served the §4 block (envelope held server-side keyed by that call). +Fires on the self-build path only, after the §4 block is signed and published. No pre-consensus phase. Envelope source by self-build variant: stateless self-build returns the envelope inline in `BlockContents`; stateful self-build requires a `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` to the same BN that served the §4 block (envelope held server-side keyed by that call). The duty must target publishing the signed envelope before `get_payload_due_ms()` (the `PAYLOAD_DUE_BPS` 75% cutoff, §3), with margin for gossip to reach PTC beacon nodes before then. The §4 block is already out by the attestation deadline (§1), so the round has the intervening block-to-payload gap to work in. An envelope that misses the cutoff makes honest PTC validators vote `payload_present = False`, which can leave the next slot building on the empty parent (§3); this is the bounded missed-envelope degradation in Security Considerations. @@ -306,7 +308,7 @@ Operators sign the decided `BlindedExecutionPayloadEnvelope`'s signing root unde #### Publication -Each operator's BN built a different full envelope; only the operator whose blinded form matched the QBFT decision holds the matching full bytes. That operator reconstructs the signature and POSTs the signed envelope to `/eth/v1/beacon/execution_payload_envelope` ([beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580)); the body depends on the self-build variant (§6 Trigger). For stateless self-build the envelope arrived inline in `BlockContents`, so that operator also holds the matching blobs and KZG proofs and publishes `SignedExecutionPayloadEnvelopeContents` (the `SignedExecutionPayloadEnvelope` plus `blobs` and `kzg_proofs`). For stateful self-build the BN that served the envelope already cached the side data, so a bare `SignedExecutionPayloadEnvelope(full_envelope, reconstructed_sig)` suffices. Other operators complete without publishing. +Each operator's BN built a different full envelope; only the operator whose blinded form matched the QBFT decision holds the matching full bytes. That operator reconstructs the signature and POSTs the signed envelope to `/eth/v1/beacon/execution_payload_envelopes` ([beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580)); the body depends on the self-build variant (§6 Trigger). For stateless self-build the envelope arrived inline in `BlockContents`, so that operator also holds the matching blobs and KZG proofs and publishes `SignedExecutionPayloadEnvelopeContents` (the `SignedExecutionPayloadEnvelope` plus `blobs` and `kzg_proofs`). For stateful self-build the BN that served the envelope already cached the side data, so a bare `SignedExecutionPayloadEnvelope(full_envelope, reconstructed_sig)` suffices. Other operators complete without publishing. ## Security Considerations @@ -342,8 +344,6 @@ The slot's envelope is missed if the operator whose envelope blinds to the §6-d This section is intentionally limited to upstream items that could still change the normative SSV behavior described above. If any of these settle differently, this SIP should be updated. -- PTC Beacon API detail drift: the core PTC duty, payload-attestation-data, and pool-submission surfaces are already present in upstream Beacon API `master`, and this SIP assumes those mainline shapes. Watch them for any remaining field, header, or duty-refresh semantic changes. -- `produceBlockV4` shape stabilization: this SIP relies on the current reviewed shape of `produceBlockV4` (`apis/validator/block.v4.yaml`), which has not been merged to `beacon-APIs/master` yet — it lives in [PR #580](https://github.com/ethereum/beacon-APIs/pull/580). Watch for changes to the response variant discriminator (stateful `BeaconBlock` vs stateless `BlockContents`) and the block submission wrapper shape. The PR's discussion also covers restructuring `builder_boost_factor` for multi-builder connections; this will reshape SSV node-operator config but does not change SIP-normative behavior. -- `head_v2` SSE event ([PR #590](https://github.com/ethereum/beacon-APIs/pull/590), Gloas-labeled upstream): adds an ePBS-specific `payload_status` field (`empty` / `full`) that may update mid-slot as the envelope is observed, and renames `{previous,current}_duty_dependent_root` → `{previous,current}_epoch_dependent_root` while adding `next_epoch_dependent_root`. Both §3 (PTC duty refresh) and §5 (proposer-preferences re-emission trigger) key off these dependent-root fields, so implementations will need to consume the renamed fields once the PR lands. `payload_status` is also optionally useful as an early signal for the PTC runner's internal decision cutoff (§3). +- `produceBlockV4` shape stabilization: this SIP relies on the current reviewed shape of `produceBlockV4` (`apis/validator/block.v4.yaml`), which has not been merged to `beacon-APIs/master` yet (it lives in [PR #580](https://github.com/ethereum/beacon-APIs/pull/580)). Watch for changes to the response variant discriminator (stateful `BeaconBlock` vs stateless `BlockContents`) and the block submission wrapper shape. - Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. -- Envelope publication and fetch endpoints: §6 specifies `POST /eth/v1/beacon/execution_payload_envelope` for the self-build envelope-signing duty (publication path) and `GET /eth/v1/validator/execution_payload_envelope/{slot}/{beacon_block_root}` for the runner-side envelope fetch (retrieval from the same BN that returned the `BlockContents` response). Both live in [beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580), which has not been merged to `beacon-APIs/master`. Watch for endpoint path, request/response body, and SSZ encoding changes. +- Envelope publication and fetch endpoints: §6 specifies `POST /eth/v1/beacon/execution_payload_envelopes` for the self-build envelope-signing duty (publication path) and `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` for the runner-side envelope fetch (retrieval from the same BN that returned the `BlockContents` response). Both live in [beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580), which has not been merged to `beacon-APIs/master`. Watch for endpoint path, request/response body, and SSZ encoding changes. From d427556a32297a47977e50736534c8e30bffdd13 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 3 Jun 2026 15:26:25 -0700 Subject: [PATCH 33/62] =?UTF-8?q?chore:=20=C2=A73=20PTC=20switches=20to=20?= =?UTF-8?q?validator-scoped=20no-QBFT=20reconstruction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the committee-scoped QBFT PTC runner with the no-QBFT design from the L138 thread: each operator signs its own 75% PayloadAttestationData observation, per-validator scoped like ProposerPreferences (§5), reconstructing when a threshold of operators converge on identical data. Drops PayloadAttestationVote and PTCCommitteeDuty; adds PartialSigMsgType PTCAttesterPartialSig=7 and renames RolePTCCommittee -> RolePTCAttester; bumps ProposerPreferencesPartialSig 7 -> 8 to keep RunnerRole/PartialSigMsgType value-parity. Rescopes the leader-trust security consideration to AttestationData.Index and replaces the slot-binding entry with PTC reconstruction is honest-convergence. --- sips/epbs_support.md | 51 ++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 57e004d..0ad89fe 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -26,7 +26,7 @@ Gloas changes validator duties in ways that break a few current SSV assumptions: Key design choices and why: - **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`. +- **PTC is a validator-scoped, non-QBFT runner.** Each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% cutoff; partial signatures group by signing root and reconstruct when one reaches threshold, the same one-round shape as `ProposerPreferences` (§5). A PTC vote is one beacon node's observation, not a value to negotiate, so QBFT would only add round-trips that risk the late-slot deadline (§3). - **Proposer-preferences is validator-scoped and non-QBFT.** The per-validator `fee_recipient` is configured cluster-side and is cluster-consistent in practice; `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000` in `types/beacon_types.go`, with runtime overrides; operators in a cluster must agree byte-for-byte on the value used at signing time, 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. - **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` for the self-build path is covered by a separate companion QBFT duty (§6), keyed by the block QBFT's decided block root. - **Envelope QBFT uses a blinded envelope shape.** §6's duty runs QBFT over `BlindedExecutionPayloadEnvelope` (`payload` → `payload_root: Root`), whose hash tree root equals the full envelope's. Keeps QBFT messages bounded (~few hundred bytes vs hundreds of KB to ~MB). @@ -124,25 +124,19 @@ Each validator signs a `PayloadAttestationData` object carrying `beacon_block_ro At the start of each epoch, SSV should fetch PTC duties for the next epoch and refresh them on duty-dependent-root changes. Because PTC duty responses may be sparse and incomplete, a changed duty-dependent root for an epoch should replace the cached duties for that epoch rather than being merged. -Two coincident 75%-of-slot deadlines bound the runner: `PAYLOAD_ATTESTATION_DUE_BPS = 75%` (consensus-spec-recommended broadcast time, leaving ~25% for gossip propagation and aggregation by the next slot's proposer) and `PAYLOAD_DUE_BPS = 75%` (validator-side observation cutoff after which envelopes do not flip `payload_present` to `True` per the Gloas validator spec). `PayloadAttestationData` values (`payload_present`, `blob_data_available`, `beacon_block_root`) evolve throughout the slot as envelopes and blobs are observed, so runners should target fetch and QBFT start near 75%, as late as the DVT round budget permits, to maximize the chance `payload_present` reflects the envelope actually arriving. The consensus specs do not prescribe a start time. QBFT and broadcast may complete after 75% and still propagate; within-slot overruns reach fork-choice via the wire path but risk missing block inclusion. Past slot end the message is dropped (gossip IGNORE and fork-choice wire REJECT when `data.slot != current_slot`), and each missed vote chips at the `PTC_SIZE/2` threshold that governs whether fork-choice extends the payload. +Two distinct deadlines bound the runner, both nominally at 75% of the slot. `PAYLOAD_DUE_BPS = 75%` is the validator-side observation cutoff: `payload_present` is the predicate "a `SignedExecutionPayloadEnvelope` for `beacon_block_root` was seen before the cutoff" (a first-seen-time test, not a "present now" query), so an envelope arriving after the cutoff does not flip it to `True`. `PAYLOAD_ATTESTATION_DUE_BPS = 75%` is the consensus-spec-recommended broadcast time, a soft target leaving ~25% of the slot for propagation to the next slot's proposer. The hard deadline is slot end (100%): a slot-N payload attestation is accepted on the wire only while `data.slot == current_slot` and is consulted by fork choice only in slot N+1, so nothing consumes a slot-N vote during the [75%, 100%] window. Each operator therefore evaluates `PayloadAttestationData` (`payload_present`, `blob_data_available`, `beacon_block_root`) from its beacon node at the 75% cutoff and runs its partial-signature round in the otherwise-free [75%, 100%] window. Broadcast may complete after 75% and still propagate; past slot end the message is dropped (gossip IGNORE and fork-choice wire REJECT when `data.slot != current_slot`), and each missed vote chips at the `PTC_SIZE/2` threshold that governs whether fork choice extends the payload. -The beacon node also emits SSE events a runner may consume as a push trigger for when to fetch and start QBFT, instead of polling toward the cutoff: `execution_payload_gossip` and `execution_payload` fire when a `SignedExecutionPayloadEnvelope` passes `execution_payload`-topic gossip validation and when it is imported into fork choice, and `execution_payload_available` fires once the node has verified the payload and blobs are available and ready for payload attestation ([beacon-APIs event stream](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/eventstream/index.yaml)). These are timing signals only: `payload_present` and `blob_data_available` still come from the BN-computed `PayloadAttestationData` at fetch time, bounded by the same cutoff. +The beacon node also emits SSE events a runner may consume as a push trigger for when to evaluate its observation, instead of polling toward the cutoff: `execution_payload_gossip` and `execution_payload` fire when a `SignedExecutionPayloadEnvelope` passes `execution_payload`-topic gossip validation and when it is imported into fork choice, and `execution_payload_available` fires once the node has verified the payload and blobs are available and ready for payload attestation ([beacon-APIs event stream](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/eventstream/index.yaml)). These are timing signals only: `payload_present` and `blob_data_available` still come from the BN-computed `PayloadAttestationData` at fetch time, bounded by the same cutoff. -There is no pre-consensus phase. Operator QBFT instances agree on a stripped `PayloadAttestationVote`: +There is no pre-consensus phase and no QBFT round. Each operator evaluates `PayloadAttestationData` from its own beacon node at the 75% cutoff and signs that observation directly; there is no leader and no negotiated value. Because a per-validator BLS signature reconstructs only from partial signatures over byte-identical data, each operator pins its 75% `PayloadAttestationData` snapshot (with `slot` taken from the duty) and validates and aggregates incoming partial signatures against exactly that signing root. -```go -type PayloadAttestationVote struct { - BeaconBlockRoot phase0.Root `ssz-size:"32"` - PayloadPresent bool - BlobDataAvailable bool -} -``` +An operator that has seen no beacon block for the slot abstains (submits nothing), matching the Gloas validator spec. Otherwise, for each of its local PTC-assigned validators it produces one partial signature over the full `PayloadAttestationData` 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 = PTCAttesterPartialSig` (the runner role `RolePTCAttester` is the dispatch discriminator). Each operator accumulates peers' partial signatures over its own frozen root; when signatures over identical `PayloadAttestationData` reach the reconstruction threshold, it BLS-aggregates and submits one `PayloadAttestationMessage(validator_index, data, signature)` per validator to the beacon node, inside the [75%, 100%] window. Operators on a minority observation never reach threshold and contribute nothing, a non-slashable silent miss (see Security Considerations). The cluster therefore emits, per validator, the observation a threshold of its operators converged on, rather than a single leader's. -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 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 observation carries slightly different fork-choice weight across the two boolean fields. -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, nor for slot-binding to `duty.slot`, matching existing `BeaconVote.BlockRoot` handling (see Security Considerations: the runner accepts that a slot-mismatched root yields a message the network ignores). PTC attestations are not in the beacon chain slashing predicate, so no slashability call is required. +There is no QBFT value check: there is no leader-proposed value to validate, since each operator signs only the observation it made (one that saw no block abstains, as above). The off-slot-root concern that a leader-decided root would raise does not arise, because each operator signs the block it observed for `duty.slot`, on-slot by construction. PTC attestations are not in the beacon chain slashing predicate, so no slashability call is required. -This SIP adds a new beacon role `BNRolePTCAttester` and a matching runner role `RolePTCCommittee`. +This SIP adds a new beacon role `BNRolePTCAttester`, a matching runner role `RolePTCAttester`, and a new `PartialSigMsgType` `PTCAttesterPartialSig`. ```go // types/beacon_types.go additions @@ -159,22 +153,19 @@ const ( // types/runner_role.go additions const ( // ... existing values ... - RolePTCCommittee RunnerRole = 7 + RolePTCAttester RunnerRole = 7 +) + +// types/partial_sig_message.go additions +const ( + // ... existing values ... + PTCAttesterPartialSig PartialSigMsgType = 7 ) ``` `RunnerRole` values `1` and `3` are reserved for backward-compat decoding of pre-consolidation messages. -`MapDutyToRunnerRole()` must map `BNRolePTCAttester` to `RolePTCCommittee`. A new `PTCCommitteeDuty` is introduced, reusing the existing `ValidatorDuty`: - -```go -type PTCCommitteeDuty struct { - Slot spec.Slot - ValidatorDuties []*ValidatorDuty -} -``` - -`PTCCommitteeDuty` bundles all PTC-selected validators for a given slot under one duty, so one QBFT round on the shared `PayloadAttestationVote` covers all of them rather than running a separate consensus per validator. +`MapDutyToRunnerRole()` must map `BNRolePTCAttester` to `RolePTCAttester`. PTC reuses the existing `ValidatorDuty`, with one runner instance scoped per PTC-assigned validator (keyed by validator pubkey), the same validator-scoped shape as `ProposerPreferences` (§5). A slot's PTC is `PTC_SIZE` (512) seats selected from that slot's beacon committees, so a cluster typically holds zero or one PTC seat in a given slot; per-validator scoping reuses the single-validator signing path, and committee-style bundling would save almost nothing. ### 4. Modified Proposer Duty @@ -232,7 +223,7 @@ const ( // types/partial_sig_message.go additions const ( // ... existing values - ProposerPreferencesPartialSig PartialSigMsgType = 7 + ProposerPreferencesPartialSig PartialSigMsgType = 8 ) ``` @@ -316,13 +307,13 @@ Each operator's BN built a different full envelope; only the operator whose blin Under Gloas, `AttestationData.Index` is part of the attestation data root and therefore part of the double-vote slashing predicate. `GloasBeaconVoteValueCheckF` must reconstruct the full Gloas `AttestationData` with `Index` from the decided `GloasBeaconVote.AttestationDataIndex` before calling `IsAttestationSlashable`; otherwise an operator could sign `index=0` and `index=1` for the same `(source, target)` in the same slot without the predicate tripping. -### Payload-status fields are trusted from the QBFT leader +### Gloas `AttestationData.Index` is trusted from the QBFT leader -Value checks for Gloas `AttestationData.Index` and PTC `payload_present` / `blob_data_available` do not require the decided value to match each operator's local BN view. Requiring local agreement would fail QBFT rounds whenever operators observe the envelope at slightly different times around the 75% deadline, a normal gossip-lag scenario. Accepted tradeoff: a malicious QBFT leader can push a value contrary to the cluster's majority BN observation. This matches existing ssv-spec treatment of `BeaconVote.BlockRoot`, which is trusted from the leader because BNs legitimately diverge on fork-choice head. +The Gloas `AttestationData.Index` value check (§2) does not require the QBFT-decided value to match each operator's local BN view. Requiring local agreement would fail QBFT rounds whenever operators observe fork-choice state at slightly different times around the deadline, a normal gossip-lag scenario. Accepted tradeoff: a malicious QBFT leader can push a value contrary to the cluster's majority BN observation. This matches existing ssv-spec treatment of `BeaconVote.BlockRoot`, which is trusted from the leader because BNs legitimately diverge on fork-choice head. -### PTC `beacon_block_root` slot-binding is left to gossip, not enforced in the value check +### PTC reconstruction is honest-convergence, not consensus -Gloas gossip adds an IGNORE-level check that the block at `data.beacon_block_root` is at `data.slot` ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#L346-L347)). SSV takes `BeaconBlockRoot` from the QBFT-decided value while `data.slot` comes from the duty, so a faulty leader could propose an off-slot root. The runner does not check `block.slot == duty.slot`: it holds no local block, and validating the slot would require an extra beacon-node query on the late-slot hot path. A slot-mismatched root is signed but ignored by peers, wasting that round's PTC votes; this is bounded, non-slashable, and equals a missed vote (the False-vote / missed-vote equivalence above), and arises only under leader fault. +PTC runs no QBFT (§3): each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% cutoff, and a per-validator signature reconstructs only when a threshold of operators converged on byte-identical data. There is no leader to push a value contrary to the cluster's observation, and an operator can only ever vote its own honest observation. The cost is liveness rather than safety: when operators' beacon nodes split across observations near the cutoff (envelope-arrival or head jitter at the `MAXIMUM_GOSSIP_CLOCK_DISPARITY` boundary, or operators on diverged forks), no observation may reach threshold and the cluster's vote for that validator is a silent miss. That miss is non-slashable and its only effect is the foregone contribution to the `PTC_SIZE/2` fork-choice tally, bounded by SSV's PTC seat share. The off-slot-root case that Gloas gossip guards with an IGNORE-level `block.slot == data.slot` check ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#L346-L347)) cannot arise here: each operator signs the block it observed for `duty.slot`. ### Config divergence silently disables trustless external builder bids From 4e02e6a41440dd77ce33f57edda560241d26fdd8 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Fri, 5 Jun 2026 14:02:29 -0700 Subject: [PATCH 34/62] chore: add Acknowledgements section --- sips/epbs_support.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 0ad89fe..81a7ee3 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -338,3 +338,7 @@ This section is intentionally limited to upstream items that could still change - `produceBlockV4` shape stabilization: this SIP relies on the current reviewed shape of `produceBlockV4` (`apis/validator/block.v4.yaml`), which has not been merged to `beacon-APIs/master` yet (it lives in [PR #580](https://github.com/ethereum/beacon-APIs/pull/580)). Watch for changes to the response variant discriminator (stateful `BeaconBlock` vs stateless `BlockContents`) and the block submission wrapper shape. - Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. - Envelope publication and fetch endpoints: §6 specifies `POST /eth/v1/beacon/execution_payload_envelopes` for the self-build envelope-signing duty (publication path) and `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` for the runner-side envelope fetch (retrieval from the same BN that returned the `BlockContents` response). Both live in [beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580), which has not been merged to `beacon-APIs/master`. Watch for endpoint path, request/response body, and SSZ encoding changes. + +## 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. From dd72262f7cada0f22b5673bf1326b8c9633fc250 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 10 Jun 2026 16:59:39 -0700 Subject: [PATCH 35/62] =?UTF-8?q?chore:=20clean=20up=20=C2=A73/=C2=A75:=20?= =?UTF-8?q?no-QBFT=20residue,=20PTC=20API=20refs,=20domain=20epochs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 81a7ee3..1601ef1 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -26,8 +26,8 @@ Gloas changes validator duties in ways that break a few current SSV assumptions: Key design choices and why: - **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 validator-scoped, non-QBFT runner.** Each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% cutoff; partial signatures group by signing root and reconstruct when one reaches threshold, the same one-round shape as `ProposerPreferences` (§5). A PTC vote is one beacon node's observation, not a value to negotiate, so QBFT would only add round-trips that risk the late-slot deadline (§3). -- **Proposer-preferences is validator-scoped and non-QBFT.** The per-validator `fee_recipient` is configured cluster-side and is cluster-consistent in practice; `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000` in `types/beacon_types.go`, with runtime overrides; operators in a cluster must agree byte-for-byte on the value used at signing time, 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. +- **PTC is a validator-scoped, non-QBFT runner.** Each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% cutoff, validates incoming partial signatures against its own derived signing root, and reconstructs when that root reaches threshold, the same one-round shape as `ProposerPreferences` (§5). A PTC vote is one beacon node's observation, not a value to negotiate, so QBFT would only add round-trips that risk the late-slot deadline (§3). +- **Proposer-preferences is validator-scoped and non-QBFT.** The per-validator `fee_recipient` is configured cluster-side and is cluster-consistent in practice; `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000` in `types/beacon_types.go`, with runtime overrides; operators in a cluster must agree byte-for-byte on the value used at signing time, same as the existing validator-registration flow). Operators independently derive the full `ProposerPreferences` and validate incoming partial signatures against their own derived signing root; reconstruction succeeds only when a quorum of operators converge on one signing root. The registration-like one-round partial-sig-and-submit flow from `voluntary_exit.md` fits directly. - **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` for the self-build path is covered by a separate companion QBFT duty (§6), keyed by the block QBFT's decided block root. - **Envelope QBFT uses a blinded envelope shape.** §6's duty runs QBFT over `BlindedExecutionPayloadEnvelope` (`payload` → `payload_root: Root`), whose hash tree root equals the full envelope's. Keeps QBFT messages bounded (~few hundred bytes vs hundreds of KB to ~MB). @@ -120,17 +120,17 @@ Relevant consensus-spec references: PTC is a per-slot consensus-layer-selected set of validators that attests to payload and blob availability for the slot's beacon block. -Each validator signs a `PayloadAttestationData` object carrying `beacon_block_root`, `slot`, `payload_present`, and `blob_data_available`, then submits a validator-specific `PayloadAttestationMessage(validator_index, data, signature)` to the beacon node. +Each validator signs a `PayloadAttestationData` object carrying `beacon_block_root`, `slot`, `payload_present`, and `blob_data_available`, then submits a validator-specific `PayloadAttestationMessage(validator_index, data, signature)` to the beacon node via [`POST /eth/v1/beacon/pool/payload_attestations`](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/beacon/pool/payload_attestations.yaml). -At the start of each epoch, SSV should fetch PTC duties for the next epoch and refresh them on duty-dependent-root changes. Because PTC duty responses may be sparse and incomplete, a changed duty-dependent root for an epoch should replace the cached duties for that epoch rather than being merged. +At the start of each epoch, SSV should fetch PTC duties for the current and next epoch via [`POST /eth/v1/validator/duties/ptc/{epoch}`](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/validator/duties/ptc.yaml) (the endpoint serves at most one epoch ahead; the current-epoch fetch covers operators starting mid-epoch) and refresh them on duty-dependent-root changes. On a dependent-root change, the new response is authoritative for that epoch: cached duties for the epoch are replaced rather than merged, since a merge could retain assignments that no longer exist under the new root. -Two distinct deadlines bound the runner, both nominally at 75% of the slot. `PAYLOAD_DUE_BPS = 75%` is the validator-side observation cutoff: `payload_present` is the predicate "a `SignedExecutionPayloadEnvelope` for `beacon_block_root` was seen before the cutoff" (a first-seen-time test, not a "present now" query), so an envelope arriving after the cutoff does not flip it to `True`. `PAYLOAD_ATTESTATION_DUE_BPS = 75%` is the consensus-spec-recommended broadcast time, a soft target leaving ~25% of the slot for propagation to the next slot's proposer. The hard deadline is slot end (100%): a slot-N payload attestation is accepted on the wire only while `data.slot == current_slot` and is consulted by fork choice only in slot N+1, so nothing consumes a slot-N vote during the [75%, 100%] window. Each operator therefore evaluates `PayloadAttestationData` (`payload_present`, `blob_data_available`, `beacon_block_root`) from its beacon node at the 75% cutoff and runs its partial-signature round in the otherwise-free [75%, 100%] window. Broadcast may complete after 75% and still propagate; past slot end the message is dropped (gossip IGNORE and fork-choice wire REJECT when `data.slot != current_slot`), and each missed vote chips at the `PTC_SIZE/2` threshold that governs whether fork choice extends the payload. +Two distinct deadlines bound the runner, both nominally at 75% of the slot. `PAYLOAD_DUE_BPS = 75%` is the validator-side observation cutoff: `payload_present` is the predicate "a `SignedExecutionPayloadEnvelope` for `beacon_block_root` was seen before the cutoff" (a first-seen-time test, not a "present now" query), so an envelope arriving after the cutoff does not flip it to `True`. `blob_data_available` has no such upstream cutoff; it is `is_data_available(beacon_block_root)` at whatever time the data is evaluated. `PAYLOAD_ATTESTATION_DUE_BPS = 75%` is the consensus-spec-recommended broadcast time, a soft target leaving ~25% of the slot for propagation to the next slot's proposer. The hard deadline is slot end (100%): a slot-N payload attestation is accepted on the wire only while `data.slot == current_slot` and is consulted by fork choice only in slot N+1, so nothing consumes a slot-N vote during the [75%, 100%] window. Each operator therefore evaluates `PayloadAttestationData` (`payload_present`, `blob_data_available`, `beacon_block_root`) from its beacon node at the 75% cutoff and runs its partial-signature round in the otherwise-free [75%, 100%] window. Broadcast may complete after 75% and still propagate; past slot end the message is dropped (gossip IGNORE and fork-choice wire REJECT when `data.slot != current_slot`), and each missed vote chips at the `PTC_SIZE/2` threshold that governs whether fork choice extends the payload. -The beacon node also emits SSE events a runner may consume as a push trigger for when to evaluate its observation, instead of polling toward the cutoff: `execution_payload_gossip` and `execution_payload` fire when a `SignedExecutionPayloadEnvelope` passes `execution_payload`-topic gossip validation and when it is imported into fork choice, and `execution_payload_available` fires once the node has verified the payload and blobs are available and ready for payload attestation ([beacon-APIs event stream](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/eventstream/index.yaml)). These are timing signals only: `payload_present` and `blob_data_available` still come from the BN-computed `PayloadAttestationData` at fetch time, bounded by the same cutoff. +The beacon node also emits SSE events a runner may consume as a push trigger for when to evaluate its observation, instead of polling toward the cutoff: `execution_payload_gossip` and `execution_payload` fire when a `SignedExecutionPayloadEnvelope` passes `execution_payload`-topic gossip validation and when it is imported into fork choice, and `execution_payload_available` fires once the node has verified the payload and blobs are available and ready for payload attestation ([beacon-APIs event stream](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/eventstream/index.yaml)). These are timing signals only: `payload_present` and `blob_data_available` still come from the BN-computed `PayloadAttestationData` fetched via [`GET /eth/v1/validator/payload_attestation_data/{slot}`](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/validator/payload_attestation_data.yaml). There is no pre-consensus phase and no QBFT round. Each operator evaluates `PayloadAttestationData` from its own beacon node at the 75% cutoff and signs that observation directly; there is no leader and no negotiated value. Because a per-validator BLS signature reconstructs only from partial signatures over byte-identical data, each operator pins its 75% `PayloadAttestationData` snapshot (with `slot` taken from the duty) and validates and aggregates incoming partial signatures against exactly that signing root. -An operator that has seen no beacon block for the slot abstains (submits nothing), matching the Gloas validator spec. Otherwise, for each of its local PTC-assigned validators it produces one partial signature over the full `PayloadAttestationData` 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 = PTCAttesterPartialSig` (the runner role `RolePTCAttester` is the dispatch discriminator). Each operator accumulates peers' partial signatures over its own frozen root; when signatures over identical `PayloadAttestationData` reach the reconstruction threshold, it BLS-aggregates and submits one `PayloadAttestationMessage(validator_index, data, signature)` per validator to the beacon node, inside the [75%, 100%] window. Operators on a minority observation never reach threshold and contribute nothing, a non-slashable silent miss (see Security Considerations). The cluster therefore emits, per validator, the observation a threshold of its operators converged on, rather than a single leader's. +An operator that has seen no beacon block for the slot abstains (submits nothing), matching the Gloas validator spec. Otherwise, the operator's runner for each of its local PTC-assigned validators produces one partial signature over the full `PayloadAttestationData` 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. Each runner broadcasts its partial signature in its own `PartialSignatureMessages` container with `Type = PTCAttesterPartialSig` (the runner role `RolePTCAttester` is the dispatch discriminator), one single-validator container per PTC-assigned validator, the same per-validator container shape as `ValidatorRegistration` today. Each operator accumulates peers' partial signatures over its own frozen root; when signatures over identical `PayloadAttestationData` reach the reconstruction threshold, it BLS-aggregates and submits one `PayloadAttestationMessage(validator_index, data, signature)` per validator to the beacon node, inside the [75%, 100%] window. Operators on a minority observation never reach threshold and contribute nothing, a non-slashable silent miss (see Security Considerations). The cluster therefore emits, per validator, the observation a threshold of its operators converged on, rather than a single leader's. 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 observation carries slightly different fork-choice weight across the two boolean fields. @@ -196,9 +196,9 @@ Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `propos Gossip enforces the handshake at the `execution_payload_bid` topic: each bid requires a matching `SignedProposerPreferences` for its `(proposal_slot, dependent_root)` (otherwise IGNORE'd, not forwarded). The bid `fee_recipient` must match the preference (mismatch is REJECT'd), and the bid `gas_limit` must be EIP-1559-compatible with the proposer's `target_gas_limit` via `is_gas_limit_target_compatible` (incompatible is IGNORE'd). Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. -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` is configured cluster-side (cluster-consistent in practice); `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000`, with runtime overrides; operators in a cluster must agree byte-for-byte at signing time); `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). +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; the domain epoch is `compute_epoch_at_slot(proposal_slot)` per the spec's `get_proposer_preferences_signature`, so signatures for the pre-fork emission required below are computed under the Gloas fork domain even while the chain is still on the prior fork. `fee_recipient` is configured cluster-side (cluster-consistent in practice); `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000`, with runtime overrides; operators in a cluster must agree byte-for-byte at signing time); `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, and operators validate incoming partial signatures against their own derived root (the existing `ValidatorRegistration` expected-root validation); divergence splits signing roots, and reconstruction succeeds only when one root reaches threshold. `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. +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. Emission during the first Gloas epoch itself would also be gossip-valid for slots later in that epoch, but slots early in the epoch leave effectively no post-fork emission time, and builders need a validator's preference before they can construct and gossip bids for its slot; pre-fork emission covers both, 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. This SIP adds a new beacon role `BNRoleProposerPreferences`, a matching runner role `RoleProposerPreferences`, and a new `PartialSigMsgType` `ProposerPreferencesPartialSig`. @@ -295,7 +295,7 @@ No envelope-content validation: `PayloadRoot` (and therefore every constituent f #### Post-consensus -Operators sign the decided `BlindedExecutionPayloadEnvelope`'s signing root under `DOMAIN_BEACON_BUILDER` (`0x0B000000`); by SSZ root-equivalence this is the full envelope's signing root. Partial sigs broadcast as `PostConsensusPartialSig`. +Operators sign the decided `BlindedExecutionPayloadEnvelope`'s signing root under `DOMAIN_BEACON_BUILDER` (`0x0B000000`), domain epoch = `compute_epoch_at_slot(duty.slot)` (matching `verify_execution_payload_envelope_signature`'s `get_domain(state, DOMAIN_BEACON_BUILDER)` against the proposal-slot state); by SSZ root-equivalence this is the full envelope's signing root. Partial sigs broadcast as `PostConsensusPartialSig`. #### Publication From 8beaa7a31c963adb6938da0eb7793a9b02ad9549 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 10 Jun 2026 16:59:39 -0700 Subject: [PATCH 36/62] chore: deprecate ValidatorRegistration duty at the Gloas fork --- sips/epbs_support.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 1601ef1..8a3b759 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -12,6 +12,7 @@ Validator client related changes via ePBS: 3. the new Payload Timeliness Committee (PTC) 4. the Gloas proposer flow using `produceBlockV4`. The SSV cluster signs the `Gloas.BeaconBlock` in §4 and, on the self-build path, signs `SignedExecutionPayloadEnvelope` as a companion QBFT duty (§6). 5. the new `SignedProposerPreferences` message must be submitted if the node operator wants to be able to select block bids received over p2p +6. the existing validator-registration duty is deprecated at the Gloas fork, its purpose replaced by `SignedProposerPreferences` (§5) ## Motivation @@ -192,7 +193,7 @@ Relevant consensus-spec references: - [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#proposer_preferences) - [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#execution_payload_bid) -Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `proposer_preferences` p2p topic for future proposal slots within the proposer lookahead (the current epoch up to `MIN_SEED_LOOKAHEAD` epochs ahead). The signed `ProposerPreferences` carries `dependent_root`, `proposal_slot`, `validator_index`, `fee_recipient`, and `target_gas_limit`. `dependent_root` pins the proposer-lookahead epoch's seed via `get_proposer_dependent_root(state, epoch)`; operators populate it from the `dependent_root` returned by [`GET /eth/v2/validator/duties/proposer/{epoch}`](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/validator/duties/proposer.v2.yaml) for the proposal-slot's epoch. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks. +Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `proposer_preferences` p2p topic for future proposal slots within the proposer lookahead (the current epoch up to `MIN_SEED_LOOKAHEAD` epochs ahead). The signed `ProposerPreferences` carries `dependent_root`, `proposal_slot`, `validator_index`, `fee_recipient`, and `target_gas_limit`. `dependent_root` pins the proposer-lookahead epoch's seed via `get_proposer_dependent_root(state, epoch)`; operators populate it from the `dependent_root` returned by [`GET /eth/v2/validator/duties/proposer/{epoch}`](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/validator/duties/proposer.v2.yaml) for the proposal-slot's epoch. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks; the `ValidatorRegistration` duty is deprecated accordingly (end of this section). Gossip enforces the handshake at the `execution_payload_bid` topic: each bid requires a matching `SignedProposerPreferences` for its `(proposal_slot, dependent_root)` (otherwise IGNORE'd, not forwarded). The bid `fee_recipient` must match the preference (mismatch is REJECT'd), and the bid `gas_limit` must be EIP-1559-compatible with the proposer's `target_gas_limit` via `is_gas_limit_target_compatible` (incompatible is IGNORE'd). Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. @@ -229,6 +230,10 @@ const ( `MapDutyToRunnerRole()` must map `BNRoleProposerPreferences` to `RoleProposerPreferences`. +#### Deprecation of the `ValidatorRegistration` duty + +Gloas removes every protocol consumer of `SignedValidatorRegistrationV1`: builders source `fee_recipient` and `target_gas_limit` from the `proposer_preferences` topic instead, the relay path that consumed registrations is gone with blinded blocks (§4), and the Gloas builder-API workstream itself deprecates `ValidatorRegistrationV1` in favor of `ProposerPreferences` ([builder-specs PR #138](https://github.com/ethereum/builder-specs/pull/138), in flight; see also [builder-specs issue #150](https://github.com/ethereum/builder-specs/issues/150)). This SIP therefore deprecates the duty at the fork: operators emit no `BNRoleValidatorRegistration` duties for epochs at or after `GLOAS_FORK_EPOCH`, message validation treats `ValidatorRegistrationPartialSig` messages for Gloas-or-later slots as invalid, and pre-Gloas slots are unchanged. `BNRoleValidatorRegistration`, `RoleValidatorRegistration`, and `ValidatorRegistrationPartialSig` keep their numeric values, reserved for pre-Gloas operation and backward-compat decoding, the same treatment as `RunnerRole` values `1` and `3` (§3). Where a beacon node sources self-build `fee_recipient` / `target_gas_limit` after the fork is BN configuration outside the SSV protocol, and it needs no distributed signature: registration signatures existed to authenticate proposers to untrusted relays (`prepare_beacon_proposer` carries only `fee_recipient` today, and a validator-facing preferences submission endpoint is on the watchlist). + ### 6. New Duty: Envelope Signing (Self-Build Path) Relevant consensus-spec references: From 2343ef8591e7fae7fc0ed237821dd3851fa4f502 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 10 Jun 2026 17:06:46 -0700 Subject: [PATCH 37/62] chore: bump consensus-specs pin to 6ebb2216c and refresh should_build_on_full note --- sips/epbs_support.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 8a3b759..3474d54 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -4,7 +4,7 @@ ## Summary -Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after ePBS, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas) (`ethereum/consensus-specs@e34dbbb33`, reviewed 2026-06-01). +Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after ePBS, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas) (`ethereum/consensus-specs@6ebb2216c`, reviewed 2026-06-10). Validator client related changes via ePBS: 1. earlier slot deadlines @@ -40,7 +40,7 @@ All existing validator duty deadlines shift earlier in the slot. A new PTC deadl Relevant consensus-spec references: -- [Validator time parameters](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/validator.md#time-parameters) +- [Validator time parameters](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/validator.md#time-parameters) | Duty | Pre-ePBS | Post-ePBS (Gloas) | |------|----------|--------------------| @@ -54,7 +54,7 @@ Relevant consensus-spec references: Relevant consensus-spec references: -- [Validator attestation changes](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/validator.md#attestation) +- [Validator attestation changes](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/validator.md#attestation) #### Consensus-spec change @@ -103,7 +103,7 @@ A new `GloasBeaconVoteValueCheckF()` mirrors today's `BeaconVoteValueCheckF()` a The existing sentinel is in place because pre-Gloas consensus data carries no `CommitteeIndex`; `math.MaxUint64` keeps `IsAttestationSlashable` from flagging legitimate same-`(source, target, slot, BlockRoot)` attestations as double-votes. -The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. +The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. Pre-Gloas slots continue to run `BeaconVoteValueCheckF()` unchanged. @@ -115,9 +115,9 @@ The `BNRoleAggregator` duty (handled by the aggregator-committee runner) fetches Relevant consensus-spec references: -- [Validator payload timeliness attestation flow](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/validator.md#payload-timeliness-attestation) -- [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/beacon-chain.md#payloadattestationdata) -- [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) +- [Validator payload timeliness attestation flow](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/validator.md#payload-timeliness-attestation) +- [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/beacon-chain.md#payloadattestationdata) +- [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) PTC is a per-slot consensus-layer-selected set of validators that attests to payload and blob availability for the slot's beacon block. @@ -133,7 +133,7 @@ There is no pre-consensus phase and no QBFT round. Each operator evaluates `Payl An operator that has seen no beacon block for the slot abstains (submits nothing), matching the Gloas validator spec. Otherwise, the operator's runner for each of its local PTC-assigned validators produces one partial signature over the full `PayloadAttestationData` 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. Each runner broadcasts its partial signature in its own `PartialSignatureMessages` container with `Type = PTCAttesterPartialSig` (the runner role `RolePTCAttester` is the dispatch discriminator), one single-validator container per PTC-assigned validator, the same per-validator container shape as `ValidatorRegistration` today. Each operator accumulates peers' partial signatures over its own frozen root; when signatures over identical `PayloadAttestationData` reach the reconstruction threshold, it BLS-aggregates and submits one `PayloadAttestationMessage(validator_index, data, signature)` per validator to the beacon node, inside the [75%, 100%] window. Operators on a minority observation never reach threshold and contribute nothing, a non-slashable silent miss (see Security Considerations). The cluster therefore emits, per validator, the observation a threshold of its operators converged on, rather than a single leader's. -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 observation carries slightly different fork-choice weight across the two boolean fields. +False votes and missed votes are equivalent in the payload-extension tally (`should_extend_payload` counts only `True` votes toward the threshold), but not for the next proposer's parent choice: explicit `False` majorities on either field steer the proposer off the full parent in `should_build_on_full` (via `payload_timeliness(..., timely=False)` and `payload_data_availability(..., available=False)`), weight a missed vote does not carry. There is no QBFT value check: there is no leader-proposed value to validate, since each operator signs only the observation it made (one that saw no block abstains, as above). The off-slot-root concern that a leader-decided root would raise does not arise, because each operator signs the block it observed for `duty.slot`, on-slot by construction. PTC attestations are not in the beacon chain slashing predicate, so no slashability call is required. @@ -172,7 +172,7 @@ const ( Relevant consensus-spec references: -- [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/validator.md#block-and-sidecar-proposal) +- [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/validator.md#block-and-sidecar-proposal) 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)). @@ -188,10 +188,10 @@ Pre-consensus RANDAO flow is unchanged. Post-consensus is unchanged: each operat Relevant consensus-spec references: -- [Broadcasting SignedProposerPreferences](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/validator.md#broadcasting-signedproposerpreferences) -- [`SignedProposerPreferences` container](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#new-proposerpreferences) -- [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#proposer_preferences) -- [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#execution_payload_bid) +- [Broadcasting SignedProposerPreferences](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/validator.md#broadcasting-signedproposerpreferences) +- [`SignedProposerPreferences` container](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/p2p-interface.md#new-proposerpreferences) +- [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/p2p-interface.md#proposer_preferences) +- [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/p2p-interface.md#execution_payload_bid) Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `proposer_preferences` p2p topic for future proposal slots within the proposer lookahead (the current epoch up to `MIN_SEED_LOOKAHEAD` epochs ahead). The signed `ProposerPreferences` carries `dependent_root`, `proposal_slot`, `validator_index`, `fee_recipient`, and `target_gas_limit`. `dependent_root` pins the proposer-lookahead epoch's seed via `get_proposer_dependent_root(state, epoch)`; operators populate it from the `dependent_root` returned by [`GET /eth/v2/validator/duties/proposer/{epoch}`](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/validator/duties/proposer.v2.yaml) for the proposal-slot's epoch. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks; the `ValidatorRegistration` duty is deprecated accordingly (end of this section). @@ -238,8 +238,8 @@ Gloas removes every protocol consumer of `SignedValidatorRegistrationV1`: builde Relevant consensus-spec references: -- [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/beacon-chain.md#executionpayloadenvelope) -- [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#execution_payload) +- [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/beacon-chain.md#executionpayloadenvelope) +- [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/p2p-interface.md#execution_payload) - [`POST /eth/v1/beacon/execution_payload_envelopes` endpoint](https://github.com/ethereum/beacon-APIs/pull/580) (beacon-APIs PR #580, not yet merged) On the self-build path (`bid.builder_index == BUILDER_INDEX_SELF_BUILD` per [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732)), the proposer signs `SignedExecutionPayloadEnvelope` after block publication. The SSV cluster runs a second QBFT round to produce this signature. @@ -318,7 +318,7 @@ The Gloas `AttestationData.Index` value check (§2) does not require the QBFT-de ### PTC reconstruction is honest-convergence, not consensus -PTC runs no QBFT (§3): each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% cutoff, and a per-validator signature reconstructs only when a threshold of operators converged on byte-identical data. There is no leader to push a value contrary to the cluster's observation, and an operator can only ever vote its own honest observation. The cost is liveness rather than safety: when operators' beacon nodes split across observations near the cutoff (envelope-arrival or head jitter at the `MAXIMUM_GOSSIP_CLOCK_DISPARITY` boundary, or operators on diverged forks), no observation may reach threshold and the cluster's vote for that validator is a silent miss. That miss is non-slashable and its only effect is the foregone contribution to the `PTC_SIZE/2` fork-choice tally, bounded by SSV's PTC seat share. The off-slot-root case that Gloas gossip guards with an IGNORE-level `block.slot == data.slot` check ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#L346-L347)) cannot arise here: each operator signs the block it observed for `duty.slot`. +PTC runs no QBFT (§3): each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% cutoff, and a per-validator signature reconstructs only when a threshold of operators converged on byte-identical data. There is no leader to push a value contrary to the cluster's observation, and an operator can only ever vote its own honest observation. The cost is liveness rather than safety: when operators' beacon nodes split across observations near the cutoff (envelope-arrival or head jitter at the `MAXIMUM_GOSSIP_CLOCK_DISPARITY` boundary, or operators on diverged forks), no observation may reach threshold and the cluster's vote for that validator is a silent miss. That miss is non-slashable and its only effect is the foregone contribution to the `PTC_SIZE/2` fork-choice tally, bounded by SSV's PTC seat share. The off-slot-root case that Gloas gossip guards with an IGNORE-level `block.slot == data.slot` check ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/p2p-interface.md#L346-L347)) cannot arise here: each operator signs the block it observed for `duty.slot`. ### Config divergence silently disables trustless external builder bids @@ -341,7 +341,7 @@ The slot's envelope is missed if the operator whose envelope blinds to the §6-d This section is intentionally limited to upstream items that could still change the normative SSV behavior described above. If any of these settle differently, this SIP should be updated. - `produceBlockV4` shape stabilization: this SIP relies on the current reviewed shape of `produceBlockV4` (`apis/validator/block.v4.yaml`), which has not been merged to `beacon-APIs/master` yet (it lives in [PR #580](https://github.com/ethereum/beacon-APIs/pull/580)). Watch for changes to the response variant discriminator (stateful `BeaconBlock` vs stateless `BlockContents`) and the block submission wrapper shape. -- Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/e34dbbb330c14cdd6e62b6f78817d70041abd5b5/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. +- Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. - Envelope publication and fetch endpoints: §6 specifies `POST /eth/v1/beacon/execution_payload_envelopes` for the self-build envelope-signing duty (publication path) and `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` for the runner-side envelope fetch (retrieval from the same BN that returned the `BlockContents` response). Both live in [beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580), which has not been merged to `beacon-APIs/master`. Watch for endpoint path, request/response body, and SSZ encoding changes. ## Acknowledgements From 15a9ccd476777c8b635ae6ee6382a34542eba7eb Mon Sep 17 00:00:00 2001 From: shane-moore Date: Sun, 5 Jul 2026 13:41:06 -0700 Subject: [PATCH 38/62] chore: bump consensus-specs pin to bd454cb0a and fold helper renames - get_proposer_dependent_root(state, epoch) -> get_shuffling_dependent_root(store, root, epoch) (#5374) - get_proposer_preferences_signature -> get_signed_proposer_preferences (#5396) - p2p-interface topic anchors gained New prefixes (#5356): proposer_preferences, execution_payload_bid, execution_payload - all pinned links moved to the new SHA; L346-L347 slot-check line pin verified unchanged --- sips/epbs_support.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 3474d54..a00f03c 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -4,7 +4,7 @@ ## Summary -Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after ePBS, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas) (`ethereum/consensus-specs@6ebb2216c`, reviewed 2026-06-10). +Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after ePBS, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas) (`ethereum/consensus-specs@bd454cb0a`, reviewed 2026-07-05). Validator client related changes via ePBS: 1. earlier slot deadlines @@ -40,7 +40,7 @@ All existing validator duty deadlines shift earlier in the slot. A new PTC deadl Relevant consensus-spec references: -- [Validator time parameters](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/validator.md#time-parameters) +- [Validator time parameters](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/validator.md#time-parameters) | Duty | Pre-ePBS | Post-ePBS (Gloas) | |------|----------|--------------------| @@ -54,7 +54,7 @@ Relevant consensus-spec references: Relevant consensus-spec references: -- [Validator attestation changes](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/validator.md#attestation) +- [Validator attestation changes](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/validator.md#attestation) #### Consensus-spec change @@ -103,7 +103,7 @@ A new `GloasBeaconVoteValueCheckF()` mirrors today's `BeaconVoteValueCheckF()` a The existing sentinel is in place because pre-Gloas consensus data carries no `CommitteeIndex`; `math.MaxUint64` keeps `IsAttestationSlashable` from flagging legitimate same-`(source, target, slot, BlockRoot)` attestations as double-votes. -The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. +The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. Pre-Gloas slots continue to run `BeaconVoteValueCheckF()` unchanged. @@ -115,9 +115,9 @@ The `BNRoleAggregator` duty (handled by the aggregator-committee runner) fetches Relevant consensus-spec references: -- [Validator payload timeliness attestation flow](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/validator.md#payload-timeliness-attestation) -- [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/beacon-chain.md#payloadattestationdata) -- [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) +- [Validator payload timeliness attestation flow](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/validator.md#payload-timeliness-attestation) +- [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/beacon-chain.md#payloadattestationdata) +- [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) PTC is a per-slot consensus-layer-selected set of validators that attests to payload and blob availability for the slot's beacon block. @@ -172,7 +172,7 @@ const ( Relevant consensus-spec references: -- [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/validator.md#block-and-sidecar-proposal) +- [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/validator.md#block-and-sidecar-proposal) 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)). @@ -188,16 +188,16 @@ Pre-consensus RANDAO flow is unchanged. Post-consensus is unchanged: each operat Relevant consensus-spec references: -- [Broadcasting SignedProposerPreferences](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/validator.md#broadcasting-signedproposerpreferences) -- [`SignedProposerPreferences` container](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/p2p-interface.md#new-proposerpreferences) -- [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/p2p-interface.md#proposer_preferences) -- [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/p2p-interface.md#execution_payload_bid) +- [Broadcasting SignedProposerPreferences](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/validator.md#broadcasting-signedproposerpreferences) +- [`SignedProposerPreferences` container](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/p2p-interface.md#new-proposerpreferences) +- [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/p2p-interface.md#new-proposer_preferences) +- [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/p2p-interface.md#new-execution_payload_bid) -Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `proposer_preferences` p2p topic for future proposal slots within the proposer lookahead (the current epoch up to `MIN_SEED_LOOKAHEAD` epochs ahead). The signed `ProposerPreferences` carries `dependent_root`, `proposal_slot`, `validator_index`, `fee_recipient`, and `target_gas_limit`. `dependent_root` pins the proposer-lookahead epoch's seed via `get_proposer_dependent_root(state, epoch)`; operators populate it from the `dependent_root` returned by [`GET /eth/v2/validator/duties/proposer/{epoch}`](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/validator/duties/proposer.v2.yaml) for the proposal-slot's epoch. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks; the `ValidatorRegistration` duty is deprecated accordingly (end of this section). +Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `proposer_preferences` p2p topic for future proposal slots within the proposer lookahead (the current epoch up to `MIN_SEED_LOOKAHEAD` epochs ahead). The signed `ProposerPreferences` carries `dependent_root`, `proposal_slot`, `validator_index`, `fee_recipient`, and `target_gas_limit`. `dependent_root` pins the proposer-lookahead epoch's seed via `get_shuffling_dependent_root(store, root, epoch)`; operators populate it from the `dependent_root` returned by [`GET /eth/v2/validator/duties/proposer/{epoch}`](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/validator/duties/proposer.v2.yaml) for the proposal-slot's epoch. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks; the `ValidatorRegistration` duty is deprecated accordingly (end of this section). Gossip enforces the handshake at the `execution_payload_bid` topic: each bid requires a matching `SignedProposerPreferences` for its `(proposal_slot, dependent_root)` (otherwise IGNORE'd, not forwarded). The bid `fee_recipient` must match the preference (mismatch is REJECT'd), and the bid `gas_limit` must be EIP-1559-compatible with the proposer's `target_gas_limit` via `is_gas_limit_target_compatible` (incompatible is IGNORE'd). Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. -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; the domain epoch is `compute_epoch_at_slot(proposal_slot)` per the spec's `get_proposer_preferences_signature`, so signatures for the pre-fork emission required below are computed under the Gloas fork domain even while the chain is still on the prior fork. `fee_recipient` is configured cluster-side (cluster-consistent in practice); `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000`, with runtime overrides; operators in a cluster must agree byte-for-byte at signing time); `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, and operators validate incoming partial signatures against their own derived root (the existing `ValidatorRegistration` expected-root validation); divergence splits signing roots, and reconstruction succeeds only when one root reaches threshold. `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). +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; the domain epoch is `compute_epoch_at_slot(proposal_slot)` per the spec's `get_signed_proposer_preferences`, so signatures for the pre-fork emission required below are computed under the Gloas fork domain even while the chain is still on the prior fork. `fee_recipient` is configured cluster-side (cluster-consistent in practice); `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000`, with runtime overrides; operators in a cluster must agree byte-for-byte at signing time); `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, and operators validate incoming partial signatures against their own derived root (the existing `ValidatorRegistration` expected-root validation); divergence splits signing roots, and reconstruction succeeds only when one root reaches threshold. `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. Emission during the first Gloas epoch itself would also be gossip-valid for slots later in that epoch, but slots early in the epoch leave effectively no post-fork emission time, and builders need a validator's preference before they can construct and gossip bids for its slot; pre-fork emission covers both, 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. @@ -238,8 +238,8 @@ Gloas removes every protocol consumer of `SignedValidatorRegistrationV1`: builde Relevant consensus-spec references: -- [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/beacon-chain.md#executionpayloadenvelope) -- [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/p2p-interface.md#execution_payload) +- [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/beacon-chain.md#executionpayloadenvelope) +- [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/p2p-interface.md#new-execution_payload) - [`POST /eth/v1/beacon/execution_payload_envelopes` endpoint](https://github.com/ethereum/beacon-APIs/pull/580) (beacon-APIs PR #580, not yet merged) On the self-build path (`bid.builder_index == BUILDER_INDEX_SELF_BUILD` per [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732)), the proposer signs `SignedExecutionPayloadEnvelope` after block publication. The SSV cluster runs a second QBFT round to produce this signature. @@ -318,7 +318,7 @@ The Gloas `AttestationData.Index` value check (§2) does not require the QBFT-de ### PTC reconstruction is honest-convergence, not consensus -PTC runs no QBFT (§3): each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% cutoff, and a per-validator signature reconstructs only when a threshold of operators converged on byte-identical data. There is no leader to push a value contrary to the cluster's observation, and an operator can only ever vote its own honest observation. The cost is liveness rather than safety: when operators' beacon nodes split across observations near the cutoff (envelope-arrival or head jitter at the `MAXIMUM_GOSSIP_CLOCK_DISPARITY` boundary, or operators on diverged forks), no observation may reach threshold and the cluster's vote for that validator is a silent miss. That miss is non-slashable and its only effect is the foregone contribution to the `PTC_SIZE/2` fork-choice tally, bounded by SSV's PTC seat share. The off-slot-root case that Gloas gossip guards with an IGNORE-level `block.slot == data.slot` check ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/p2p-interface.md#L346-L347)) cannot arise here: each operator signs the block it observed for `duty.slot`. +PTC runs no QBFT (§3): each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% cutoff, and a per-validator signature reconstructs only when a threshold of operators converged on byte-identical data. There is no leader to push a value contrary to the cluster's observation, and an operator can only ever vote its own honest observation. The cost is liveness rather than safety: when operators' beacon nodes split across observations near the cutoff (envelope-arrival or head jitter at the `MAXIMUM_GOSSIP_CLOCK_DISPARITY` boundary, or operators on diverged forks), no observation may reach threshold and the cluster's vote for that validator is a silent miss. That miss is non-slashable and its only effect is the foregone contribution to the `PTC_SIZE/2` fork-choice tally, bounded by SSV's PTC seat share. The off-slot-root case that Gloas gossip guards with an IGNORE-level `block.slot == data.slot` check ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/p2p-interface.md#L346-L347)) cannot arise here: each operator signs the block it observed for `duty.slot`. ### Config divergence silently disables trustless external builder bids @@ -341,7 +341,7 @@ The slot's envelope is missed if the operator whose envelope blinds to the §6-d This section is intentionally limited to upstream items that could still change the normative SSV behavior described above. If any of these settle differently, this SIP should be updated. - `produceBlockV4` shape stabilization: this SIP relies on the current reviewed shape of `produceBlockV4` (`apis/validator/block.v4.yaml`), which has not been merged to `beacon-APIs/master` yet (it lives in [PR #580](https://github.com/ethereum/beacon-APIs/pull/580)). Watch for changes to the response variant discriminator (stateful `BeaconBlock` vs stateless `BlockContents`) and the block submission wrapper shape. -- Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/6ebb2216ca8d7fdac7c108fcfddb74d0e56c42ab/specs/gloas/p2p-interface.md#proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. +- Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/p2p-interface.md#new-proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. - Envelope publication and fetch endpoints: §6 specifies `POST /eth/v1/beacon/execution_payload_envelopes` for the self-build envelope-signing duty (publication path) and `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` for the runner-side envelope fetch (retrieval from the same BN that returned the `BlockContents` response). Both live in [beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580), which has not been merged to `beacon-APIs/master`. Watch for endpoint path, request/response body, and SSZ encoding changes. ## Acknowledgements From 62c3bdc98b397c4541e18539de123cf1b9cc1078 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 6 Jul 2026 12:23:38 -0700 Subject: [PATCH 39/62] =?UTF-8?q?chore:=20align=20=C2=A74-=C2=A76=20with?= =?UTF-8?q?=20merged=20beacon-APIs=20#580/#608?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - produceBlockV4 merged: cite GET /eth/v4/validator/blocks/{slot} and the execution_payload_included / Eth-Execution-Payload-Included / include_payload variant discriminator; signed block publishes as Gloas.SignedBeaconBlock - §6 stateful flow: validator envelope fetch returns the already-blinded BlindedExecutionPayloadEnvelope; publication body is Contents or SignedBlindedExecutionPayloadEnvelope selected by Eth-Execution-Payload-Blinded (bare full-envelope body does not exist in the merged shape) - §5: cite the merged submitProposerPreferences endpoint (#608) - watchlist: collapse three stale not-yet-merged entries into one release-status entry (merged but unreleased, post-v5.0.0-alpha.2, no client marks yet) --- sips/epbs_support.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index a00f03c..e52b49f 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -174,13 +174,13 @@ Relevant consensus-spec references: - [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/validator.md#block-and-sidecar-proposal) -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)). +Under Gloas, `produceBlockV4` (`GET /eth/v4/validator/blocks/{slot}`, merged via [beacon-APIs #580](https://github.com/ethereum/beacon-APIs/pull/580)) 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. The variant is signaled by the required `execution_payload_included` response field and `Eth-Execution-Payload-Included` header, and driven by the `include_payload` query parameter (`true` requests the stateless `BlockContents` form). `ProposerConsensusData` is preserved: its struct shape (`Duty`, `Version`, `DataSSZ []byte`) is unchanged. `DataSSZ` carries the SSZ-encoded `Gloas.BeaconBlock`. The stateless `BlockContents` variant is handled identically: the cluster extracts the block into `DataSSZ` for QBFT; the inline envelope, blobs, and KZG proofs are handled by §6. Although the struct shape is unchanged, [`ProposerConsensusData.GetBlockData()`](https://github.com/ssvlabs/ssv-spec/blob/85ee4f32e4fc22bae8aacf837153aab3dcd6620b/types/consensus_data.go#L175-L237)'s per-version switch (Capella → Fulu today) needs a new `DataVersionGloas` arm that unmarshals `DataSSZ` as `Gloas.BeaconBlock`. -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. +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 as `Gloas.SignedBeaconBlock` via the existing block-publish endpoint. **Envelope signing.** 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. Distributed signing of `SignedExecutionPayloadEnvelope` for the self-build path is specified in §6. @@ -197,7 +197,7 @@ Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `propos Gossip enforces the handshake at the `execution_payload_bid` topic: each bid requires a matching `SignedProposerPreferences` for its `(proposal_slot, dependent_root)` (otherwise IGNORE'd, not forwarded). The bid `fee_recipient` must match the preference (mismatch is REJECT'd), and the bid `gas_limit` must be EIP-1559-compatible with the proposer's `target_gas_limit` via `is_gas_limit_target_compatible` (incompatible is IGNORE'd). Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. -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; the domain epoch is `compute_epoch_at_slot(proposal_slot)` per the spec's `get_signed_proposer_preferences`, so signatures for the pre-fork emission required below are computed under the Gloas fork domain even while the chain is still on the prior fork. `fee_recipient` is configured cluster-side (cluster-consistent in practice); `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000`, with runtime overrides; operators in a cluster must agree byte-for-byte at signing time); `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, and operators validate incoming partial signatures against their own derived root (the existing `ValidatorRegistration` expected-root validation); divergence splits signing roots, and reconstruction succeeds only when one root reaches threshold. `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). +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; the domain epoch is `compute_epoch_at_slot(proposal_slot)` per the spec's `get_signed_proposer_preferences`, so signatures for the pre-fork emission required below are computed under the Gloas fork domain even while the chain is still on the prior fork. `fee_recipient` is configured cluster-side (cluster-consistent in practice); `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000`, with runtime overrides; operators in a cluster must agree byte-for-byte at signing time); `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, and operators validate incoming partial signatures against their own derived root (the existing `ValidatorRegistration` expected-root validation); divergence splits signing roots, and reconstruction succeeds only when one root reaches threshold. `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). The reconstructed `SignedProposerPreferences` is submitted via `POST /eth/v1/validator/proposer_preferences` ([beacon-APIs #608](https://github.com/ethereum/beacon-APIs/pull/608)), which stores it and broadcasts it to the `proposer_preferences` topic. 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. Emission during the first Gloas epoch itself would also be gossip-valid for slots later in that epoch, but slots early in the epoch leave effectively no post-fork emission time, and builders need a validator's preference before they can construct and gossip bids for its slot; pre-fork emission covers both, 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. @@ -260,7 +260,7 @@ type BlindedExecutionPayloadEnvelope struct { #### Trigger and envelope source -Fires on the self-build path only, after the §4 block is signed and published. No pre-consensus phase. Envelope source by self-build variant: stateless self-build returns the envelope inline in `BlockContents`; stateful self-build requires a `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` to the same BN that served the §4 block (envelope held server-side keyed by that call). +Fires on the self-build path only, after the §4 block is signed and published. No pre-consensus phase. Envelope source by self-build variant: stateless self-build returns the envelope inline in `BlockContents`; stateful self-build requires a `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` to the same BN that served the §4 block, which returns the already-blinded `BlindedExecutionPayloadEnvelope` (cached server-side for the current slot only; keying by block root makes the lookup reorg-resistant). The duty must target publishing the signed envelope before `get_payload_due_ms()` (the `PAYLOAD_DUE_BPS` 75% cutoff, §3), with margin for gossip to reach PTC beacon nodes before then. The §4 block is already out by the attestation deadline (§1), so the round has the intervening block-to-payload gap to work in. An envelope that misses the cutoff makes honest PTC validators vote `payload_present = False`, which can leave the next slot building on the empty parent (§3); this is the bounded missed-envelope degradation in Security Considerations. @@ -285,7 +285,7 @@ type EnvelopeConsensusData struct { #### 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). +On the stateless path each operator blinds its local BN's inline envelope (`PayloadRoot = hash_tree_root(envelope.payload)`, other fields verbatim); on the stateful path it proposes the fetched `BlindedExecutionPayloadEnvelope` as-is. Either way the SSZ-encoded blinded form goes in `EnvelopeConsensusData.DataSSZ`. Only an operator whose BN built the §4-decided block holds (stateless) or can fetch (stateful) an envelope with a matching `BeaconBlockRoot`, 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 it locally; the decided value, and which operator can publish it, are independent of who leads the deciding round (publication is by content-match, see Publication). #### Value check @@ -304,7 +304,7 @@ Operators sign the decided `BlindedExecutionPayloadEnvelope`'s signing root unde #### Publication -Each operator's BN built a different full envelope; only the operator whose blinded form matched the QBFT decision holds the matching full bytes. That operator reconstructs the signature and POSTs the signed envelope to `/eth/v1/beacon/execution_payload_envelopes` ([beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580)); the body depends on the self-build variant (§6 Trigger). For stateless self-build the envelope arrived inline in `BlockContents`, so that operator also holds the matching blobs and KZG proofs and publishes `SignedExecutionPayloadEnvelopeContents` (the `SignedExecutionPayloadEnvelope` plus `blobs` and `kzg_proofs`). For stateful self-build the BN that served the envelope already cached the side data, so a bare `SignedExecutionPayloadEnvelope(full_envelope, reconstructed_sig)` suffices. Other operators complete without publishing. +Each operator's BN built a different envelope; only the operator whose blinded form matched the QBFT decision can publish. That operator reconstructs the signature and POSTs to `/eth/v1/beacon/execution_payload_envelopes` (merged via [beacon-APIs #580](https://github.com/ethereum/beacon-APIs/pull/580)); the body variant is selected by the required `Eth-Execution-Payload-Blinded` header. For stateless self-build the envelope arrived inline in `BlockContents`, so that operator holds the matching full payload, blobs, and KZG proofs and publishes `SignedExecutionPayloadEnvelopeContents` (header `false`). For stateful self-build the operator publishes `SignedBlindedExecutionPayloadEnvelope` (header `true`): the decided blinded envelope plus the reconstructed signature, from which the BN that built the block reconstructs the full envelope out of its production-time cache. Other operators complete without publishing. ## Security Considerations @@ -340,9 +340,7 @@ The slot's envelope is missed if the operator whose envelope blinds to the §6-d This section is intentionally limited to upstream items that could still change the normative SSV behavior described above. If any of these settle differently, this SIP should be updated. -- `produceBlockV4` shape stabilization: this SIP relies on the current reviewed shape of `produceBlockV4` (`apis/validator/block.v4.yaml`), which has not been merged to `beacon-APIs/master` yet (it lives in [PR #580](https://github.com/ethereum/beacon-APIs/pull/580)). Watch for changes to the response variant discriminator (stateful `BeaconBlock` vs stateless `BlockContents`) and the block submission wrapper shape. -- Validator-facing `SignedProposerPreferences` publication endpoint: the Gloas validator spec expects validators to broadcast preferences to the [`proposer_preferences`](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/p2p-interface.md#new-proposer_preferences) gossipsub topic, but `beacon-APIs/master` does not yet expose a validator-facing publication endpoint. §5 is specified against a future `SubmitProposerPreferences(...)` BN abstraction method whose concrete Beacon API shape is TBD. -- Envelope publication and fetch endpoints: §6 specifies `POST /eth/v1/beacon/execution_payload_envelopes` for the self-build envelope-signing duty (publication path) and `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` for the runner-side envelope fetch (retrieval from the same BN that returned the `BlockContents` response). Both live in [beacon-APIs PR #580](https://github.com/ethereum/beacon-APIs/pull/580), which has not been merged to `beacon-APIs/master`. Watch for endpoint path, request/response body, and SSZ encoding changes. +- beacon-APIs release status: `produceBlockV4` (`GET /eth/v4/validator/blocks/{slot}`), the §6 envelope publication and fetch endpoints, and the §5 `SignedProposerPreferences` submission endpoint are merged to `beacon-APIs/master` ([#580](https://github.com/ethereum/beacon-APIs/pull/580), [#608](https://github.com/ethereum/beacon-APIs/pull/608)) but unreleased: the latest release tag (`v5.0.0-alpha.2`) predates them, and no client implementations are marked in the compatibility matrix yet. Watch for pre-release changes to the response variant discriminators, request/response bodies, and SSZ encodings. ## Acknowledgements From bbd51e619ea75c0d64f14ef7ab6de98a15e61334 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 6 Jul 2026 13:36:17 -0700 Subject: [PATCH 40/62] =?UTF-8?q?chore:=20=C2=A76=20blinded=20envelope=20c?= =?UTF-8?q?arries=20the=20Gloas=20ExecutionRequests=20container=20(EIP-828?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At the bumped pin (bd454cb0a), gloas defines its own ExecutionRequests (adds builder_deposits and builder_exits, consensus-specs #5359); the electra 3-list type would merkleize to a different root and break the blinded/full hash_tree_root equivalence §6 relies on. --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index e52b49f..9081ab8 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -251,7 +251,7 @@ To bound QBFT message size, the cluster runs QBFT over a blinded form that subst ```go type BlindedExecutionPayloadEnvelope struct { PayloadRoot phase0.Root // == hash_tree_root(envelope.payload) - ExecutionRequests electra.ExecutionRequests + ExecutionRequests gloas.ExecutionRequests // Gloas 5-list container: adds builder_deposits and builder_exits (EIP-8282) BuilderIndex uint64 BeaconBlockRoot phase0.Root ParentBeaconBlockRoot phase0.Root From 3debfb14b78f4597e481a0006906438e87fafaca Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 6 Jul 2026 13:50:12 -0700 Subject: [PATCH 41/62] =?UTF-8?q?chore:=20drop=20stale=20not-yet-merged=20?= =?UTF-8?q?note=20on=20the=20=C2=A76=20envelope=20POST=20reference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 9081ab8..31a4a0b 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -240,7 +240,7 @@ Relevant consensus-spec references: - [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/beacon-chain.md#executionpayloadenvelope) - [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/p2p-interface.md#new-execution_payload) -- [`POST /eth/v1/beacon/execution_payload_envelopes` endpoint](https://github.com/ethereum/beacon-APIs/pull/580) (beacon-APIs PR #580, not yet merged) +- [`POST /eth/v1/beacon/execution_payload_envelopes` endpoint](https://github.com/ethereum/beacon-APIs/pull/580) On the self-build path (`bid.builder_index == BUILDER_INDEX_SELF_BUILD` per [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732)), the proposer signs `SignedExecutionPayloadEnvelope` after block publication. The SSV cluster runs a second QBFT round to produce this signature. From 62c3ec86f2f360517bf8f301e8423bf205c2950f Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 6 Jul 2026 14:26:16 -0700 Subject: [PATCH 42/62] =?UTF-8?q?chore:=20=C2=A76=20stateful=20publish=20t?= =?UTF-8?q?argets=20the=20BN=20that=20produced=20the=20block=20(production?= =?UTF-8?q?-cache=20affinity)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 31a4a0b..333887e 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -304,7 +304,7 @@ Operators sign the decided `BlindedExecutionPayloadEnvelope`'s signing root unde #### Publication -Each operator's BN built a different envelope; only the operator whose blinded form matched the QBFT decision can publish. That operator reconstructs the signature and POSTs to `/eth/v1/beacon/execution_payload_envelopes` (merged via [beacon-APIs #580](https://github.com/ethereum/beacon-APIs/pull/580)); the body variant is selected by the required `Eth-Execution-Payload-Blinded` header. For stateless self-build the envelope arrived inline in `BlockContents`, so that operator holds the matching full payload, blobs, and KZG proofs and publishes `SignedExecutionPayloadEnvelopeContents` (header `false`). For stateful self-build the operator publishes `SignedBlindedExecutionPayloadEnvelope` (header `true`): the decided blinded envelope plus the reconstructed signature, from which the BN that built the block reconstructs the full envelope out of its production-time cache. Other operators complete without publishing. +Each operator's BN built a different envelope; only the operator whose blinded form matched the QBFT decision can publish. That operator reconstructs the signature and POSTs to `/eth/v1/beacon/execution_payload_envelopes` (merged via [beacon-APIs #580](https://github.com/ethereum/beacon-APIs/pull/580)); the body variant is selected by the required `Eth-Execution-Payload-Blinded` header. For stateless self-build the envelope arrived inline in `BlockContents`, so that operator holds the matching full payload, blobs, and KZG proofs and publishes `SignedExecutionPayloadEnvelopeContents` (header `false`). For stateful self-build the operator publishes `SignedBlindedExecutionPayloadEnvelope` (header `true`) to the same BN that produced the §4 block: the decided blinded envelope plus the reconstructed signature, from which that BN reconstructs the full envelope out of its production-time cache (any other BN lacks the cache and rejects the publish with a 400). Other operators complete without publishing. ## Security Considerations From 04bdd5e5e8b242f6b48b17ec5682ddef3f8e488b Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 6 Jul 2026 14:57:47 -0700 Subject: [PATCH 43/62] =?UTF-8?q?chore:=20=C2=A74/=C2=A76=20external-build?= =?UTF-8?q?=20and=20request-timing=20notes;=20RequestAuthV1=20watchlist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies iurii's two suggestions from the L263 thread (r3472933153): a §6 Trigger sentence making explicit that on the external-build path the builder signs and reveals its own SignedExecutionPayloadEnvelope so the duty does not run, and a §4 non-normative note on produceBlockV4 request timing under the earlier Gloas deadlines (tighter window for a cluster: QBFT, post-consensus signing, and propagation must clear the §1 attestation deadline). Adds an Open Questions bullet for builder-specs #138's SignedRequestAuthV1: validator-key BLS over {builder URL, slot} under fork-agnostic domain 0x0B000001, used for authenticated bid requests and required for per-builder execution payments (submitBuilderPreferences). Deferring costs no liveness (an unsubmitted max_execution_payment cap is zero and gossiped bids MUST carry execution_payment = 0, so every bid pays from staked collateral); a ValidatorRegistration-shaped duty will be specified once the hop between the key holder and block assembly is standardized and the Gloas builder API ships in a builder-specs release. --- sips/epbs_support.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 333887e..adaedcd 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -176,6 +176,8 @@ Relevant consensus-spec references: Under Gloas, `produceBlockV4` (`GET /eth/v4/validator/blocks/{slot}`, merged via [beacon-APIs #580](https://github.com/ethereum/beacon-APIs/pull/580)) 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. The variant is signaled by the required `execution_payload_included` response field and `Eth-Execution-Payload-Included` header, and driven by the `include_payload` query parameter (`true` requests the stateless `BlockContents` form). +*Note (non-normative).* When to issue the `produceBlockV4` request is the residual proposer timing discretion: a later request lets the beacon node observe more bids and can return a higher-value block. The window is tighter for an SSV cluster than for a solo proposer, since the returned block must still clear QBFT, post-consensus signing, and propagation before the earlier attestation deadline (§1), so the request must leave room for them. Operator policy, not a protocol requirement. + `ProposerConsensusData` is preserved: its struct shape (`Duty`, `Version`, `DataSSZ []byte`) is unchanged. `DataSSZ` carries the SSZ-encoded `Gloas.BeaconBlock`. The stateless `BlockContents` variant is handled identically: the cluster extracts the block into `DataSSZ` for QBFT; the inline envelope, blobs, and KZG proofs are handled by §6. Although the struct shape is unchanged, [`ProposerConsensusData.GetBlockData()`](https://github.com/ssvlabs/ssv-spec/blob/85ee4f32e4fc22bae8aacf837153aab3dcd6620b/types/consensus_data.go#L175-L237)'s per-version switch (Capella → Fulu today) needs a new `DataVersionGloas` arm that unmarshals `DataSSZ` as `Gloas.BeaconBlock`. @@ -260,7 +262,7 @@ type BlindedExecutionPayloadEnvelope struct { #### Trigger and envelope source -Fires on the self-build path only, after the §4 block is signed and published. No pre-consensus phase. Envelope source by self-build variant: stateless self-build returns the envelope inline in `BlockContents`; stateful self-build requires a `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` to the same BN that served the §4 block, which returns the already-blinded `BlindedExecutionPayloadEnvelope` (cached server-side for the current slot only; keying by block root makes the lookup reorg-resistant). +Fires on the self-build path only, after the §4 block is signed and published. On the external-build path the builder signs and reveals its own `SignedExecutionPayloadEnvelope`, so this duty does not run. No pre-consensus phase. Envelope source by self-build variant: stateless self-build returns the envelope inline in `BlockContents`; stateful self-build requires a `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` to the same BN that served the §4 block, which returns the already-blinded `BlindedExecutionPayloadEnvelope` (cached server-side for the current slot only; keying by block root makes the lookup reorg-resistant). The duty must target publishing the signed envelope before `get_payload_due_ms()` (the `PAYLOAD_DUE_BPS` 75% cutoff, §3), with margin for gossip to reach PTC beacon nodes before then. The §4 block is already out by the attestation deadline (§1), so the round has the intervening block-to-payload gap to work in. An envelope that misses the cutoff makes honest PTC validators vote `payload_present = False`, which can leave the next slot building on the empty parent (§3); this is the bounded missed-envelope degradation in Security Considerations. @@ -341,6 +343,7 @@ The slot's envelope is missed if the operator whose envelope blinds to the §6-d This section is intentionally limited to upstream items that could still change the normative SSV behavior described above. If any of these settle differently, this SIP should be updated. - beacon-APIs release status: `produceBlockV4` (`GET /eth/v4/validator/blocks/{slot}`), the §6 envelope publication and fetch endpoints, and the §5 `SignedProposerPreferences` submission endpoint are merged to `beacon-APIs/master` ([#580](https://github.com/ethereum/beacon-APIs/pull/580), [#608](https://github.com/ethereum/beacon-APIs/pull/608)) but unreleased: the latest release tag (`v5.0.0-alpha.2`) predates them, and no client implementations are marked in the compatibility matrix yet. Watch for pre-release changes to the response variant discriminators, request/response bodies, and SSZ encodings. +- Builder-API authenticated connections (`SignedRequestAuthV1`): builder-specs [#138](https://github.com/ethereum/builder-specs/pull/138) (merged 2026-06-11, not yet in a release) defines an optional validator-key BLS signature over `{builder URL, slot}` under the fork-agnostic domain `DOMAIN_REQUEST_AUTH` (`0x0B000001`), used to authenticate direct bid requests (`getExecutionPayloadBid`) and required for per-builder execution payments (`submitBuilderPreferences`). A bid pays the proposer via `bid.value`, deducted on-chain from the builder's staked collateral, and optionally via `bid.execution_payment`, a trusted execution-layer payment that the proposer must opt into per builder by submitting a `max_execution_payment` cap through `submitBuilderPreferences`. Deferring the duty costs no liveness in the interim: a cluster that has not yet submitted builder preferences has a zero cap, so builders MUST NOT attach execution payments and every bid it can receive pays from staked collateral only (gossiped bids MUST carry `execution_payment = 0` regardless of channel). Distributed signing of `RequestAuthV1` follows the `ValidatorRegistration` shape (validator-scoped, one partial-signature round, no QBFT) and will be specified once the upstream surface settles, meaning a standardized hop between the key holder and block assembly (a beacon-APIs successor to the now-deprecated `register_validator` forwarding, or a sidecar specification) and a builder-specs release that includes the Gloas builder API. ## Acknowledgements From 83705e24a4c64986c998f1b45c6f457b0302bc23 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 6 Jul 2026 16:21:14 -0700 Subject: [PATCH 44/62] chore: bump consensus-specs pin to 801a38e15 (post-pin PRs #5414/#5429/#4630) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviews all consensus-specs commits merged since the bd454cb0a pin and folds the three SIP-relevant changes: - #5414: PAYLOAD_DUE_BPS lowered 7500 to 5000. §3 deadline paragraph rewritten (50% payload-seen cutoff frozen for the rest of the slot vs 75% broadcast mark; the [75%, 100%] signing window survives), §1 table gains the 50% envelope-reveal row, §6 timing now states the quarter-slot window (~3s) between the 25% attestation deadline and the 50% payload cutoff. - #5429: bid-topic fee-recipient check downgraded REJECT to IGNORE (preferences are not equivocation-checked); §5 handshake sentence updated. - #4630 (EIP-7688): ExecutionPayloadEnvelope is now ProgressiveContainer(active_fields=[1]*5). §6 blinding rationale restated: field-substitution equivalence still holds, but only under the progressive shape; explicit merkleization formula added, plus a note that the blinded shape is beacon-APIs' Gloas.BlindedExecutionPayloadEnvelope (#580). Verified unaffected: PayloadAttestationData is still a plain Container, AttestationData and ProposerPreferences unchanged. - No SIP impact verified for #5355 (index==1 gossip import requirement), #5433 (Heze-only), #5330 (phase0 transport). All 15 pinned links swept to 801a38e15; §3 SC line anchor re-located (L346-L347 to L400-L401); review date bumped. --- sips/epbs_support.md | 45 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index adaedcd..9878f83 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -4,7 +4,7 @@ ## Summary -Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after ePBS, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas) (`ethereum/consensus-specs@bd454cb0a`, reviewed 2026-07-05). +Describes the SSV spec changes needed to keep SSV operators performing validator duties correctly after ePBS, [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732), is implemented in Ethereum's consensus layer Gloas fork. Based on the pinned [Gloas consensus-spec snapshot](https://github.com/ethereum/consensus-specs/tree/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas) (`ethereum/consensus-specs@801a38e15`, reviewed 2026-07-06). Validator client related changes via ePBS: 1. earlier slot deadlines @@ -27,7 +27,7 @@ Gloas changes validator duties in ways that break a few current SSV assumptions: Key design choices and why: - **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 validator-scoped, non-QBFT runner.** Each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% cutoff, validates incoming partial signatures against its own derived signing root, and reconstructs when that root reaches threshold, the same one-round shape as `ProposerPreferences` (§5). A PTC vote is one beacon node's observation, not a value to negotiate, so QBFT would only add round-trips that risk the late-slot deadline (§3). +- **PTC is a validator-scoped, non-QBFT runner.** Each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% broadcast mark, validates incoming partial signatures against its own derived signing root, and reconstructs when that root reaches threshold, the same one-round shape as `ProposerPreferences` (§5). A PTC vote is one beacon node's observation, not a value to negotiate, so QBFT would only add round-trips that risk the late-slot deadline (§3). - **Proposer-preferences is validator-scoped and non-QBFT.** The per-validator `fee_recipient` is configured cluster-side and is cluster-consistent in practice; `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000` in `types/beacon_types.go`, with runtime overrides; operators in a cluster must agree byte-for-byte on the value used at signing time, same as the existing validator-registration flow). Operators independently derive the full `ProposerPreferences` and validate incoming partial signatures against their own derived signing root; reconstruction succeeds only when a quorum of operators converge on one signing root. The registration-like one-round partial-sig-and-submit flow from `voluntary_exit.md` fits directly. - **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` for the self-build path is covered by a separate companion QBFT duty (§6), keyed by the block QBFT's decided block root. - **Envelope QBFT uses a blinded envelope shape.** §6's duty runs QBFT over `BlindedExecutionPayloadEnvelope` (`payload` → `payload_root: Root`), whose hash tree root equals the full envelope's. Keeps QBFT messages bounded (~few hundred bytes vs hundreds of KB to ~MB). @@ -40,7 +40,7 @@ All existing validator duty deadlines shift earlier in the slot. A new PTC deadl Relevant consensus-spec references: -- [Validator time parameters](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/validator.md#time-parameters) +- [Validator time parameters](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/validator.md#time-parameters) | Duty | Pre-ePBS | Post-ePBS (Gloas) | |------|----------|--------------------| @@ -49,12 +49,13 @@ Relevant consensus-spec references: | Aggregation | 2/3 slot (~8s) | 1/2 slot (50%, ~6s) | | Sync Committee Contribution | 2/3 slot | 1/2 slot (50%) | | PTC Attestation | - | 3/4 slot (75%, ~9s) | +| Envelope reveal (self-build, §6) | - | 1/2 slot (50%, ~6s) | ### 2. Modified Attestation Duty Relevant consensus-spec references: -- [Validator attestation changes](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/validator.md#attestation) +- [Validator attestation changes](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/validator.md#attestation) #### Consensus-spec change @@ -103,7 +104,7 @@ A new `GloasBeaconVoteValueCheckF()` mirrors today's `BeaconVoteValueCheckF()` a The existing sentinel is in place because pre-Gloas consensus data carries no `CommitteeIndex`; `math.MaxUint64` keeps `IsAttestationSlashable` from flagging legitimate same-`(source, target, slot, BlockRoot)` attestations as double-votes. -The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. +The [Gloas same-slot rule](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/validator.md#attestation) (`block.slot == data.slot ⇒ data.index = 0`) is not enforced locally: the cluster has only the QBFT-decided `BlockRoot` and trusts `AttestationDataIndex` from the leader. A single bad same-slot `index=1` is rejected by the ethereum network and ignored on chain but is not slashable, while cross-`index` equivocation over the same `(source, target, slot, BlockRoot)` is still caught by `IsAttestationSlashable` per the previous bullet. Pre-Gloas slots continue to run `BeaconVoteValueCheckF()` unchanged. @@ -115,9 +116,9 @@ The `BNRoleAggregator` duty (handled by the aggregator-committee runner) fetches Relevant consensus-spec references: -- [Validator payload timeliness attestation flow](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/validator.md#payload-timeliness-attestation) -- [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/beacon-chain.md#payloadattestationdata) -- [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) +- [Validator payload timeliness attestation flow](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/validator.md#payload-timeliness-attestation) +- [Beacon-chain payload attestation containers](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/beacon-chain.md#payloadattestationdata) +- [Fork-choice payload attestation deadline](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/fork-choice.md#new-get_payload_attestation_due_ms) PTC is a per-slot consensus-layer-selected set of validators that attests to payload and blob availability for the slot's beacon block. @@ -125,11 +126,11 @@ Each validator signs a `PayloadAttestationData` object carrying `beacon_block_ro At the start of each epoch, SSV should fetch PTC duties for the current and next epoch via [`POST /eth/v1/validator/duties/ptc/{epoch}`](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/validator/duties/ptc.yaml) (the endpoint serves at most one epoch ahead; the current-epoch fetch covers operators starting mid-epoch) and refresh them on duty-dependent-root changes. On a dependent-root change, the new response is authoritative for that epoch: cached duties for the epoch are replaced rather than merged, since a merge could retain assignments that no longer exist under the new root. -Two distinct deadlines bound the runner, both nominally at 75% of the slot. `PAYLOAD_DUE_BPS = 75%` is the validator-side observation cutoff: `payload_present` is the predicate "a `SignedExecutionPayloadEnvelope` for `beacon_block_root` was seen before the cutoff" (a first-seen-time test, not a "present now" query), so an envelope arriving after the cutoff does not flip it to `True`. `blob_data_available` has no such upstream cutoff; it is `is_data_available(beacon_block_root)` at whatever time the data is evaluated. `PAYLOAD_ATTESTATION_DUE_BPS = 75%` is the consensus-spec-recommended broadcast time, a soft target leaving ~25% of the slot for propagation to the next slot's proposer. The hard deadline is slot end (100%): a slot-N payload attestation is accepted on the wire only while `data.slot == current_slot` and is consulted by fork choice only in slot N+1, so nothing consumes a slot-N vote during the [75%, 100%] window. Each operator therefore evaluates `PayloadAttestationData` (`payload_present`, `blob_data_available`, `beacon_block_root`) from its beacon node at the 75% cutoff and runs its partial-signature round in the otherwise-free [75%, 100%] window. Broadcast may complete after 75% and still propagate; past slot end the message is dropped (gossip IGNORE and fork-choice wire REJECT when `data.slot != current_slot`), and each missed vote chips at the `PTC_SIZE/2` threshold that governs whether fork choice extends the payload. +Two distinct deadlines bound the runner. `PAYLOAD_DUE_BPS = 50%` (lowered from 75% by [consensus-specs #5414](https://github.com/ethereum/consensus-specs/pull/5414)) is the payload-side observation cutoff: `payload_present` is the predicate "a `SignedExecutionPayloadEnvelope` for `beacon_block_root` was seen before the 50% mark" (a first-seen-time test, not a "present now" query), so an envelope arriving after the cutoff never flips it to `True` and the predicate is frozen for the rest of the slot. `blob_data_available` has no such upstream cutoff; it is `is_data_available(beacon_block_root)` at whatever time the data is evaluated. `PAYLOAD_ATTESTATION_DUE_BPS = 75%` is the consensus-spec-recommended broadcast time, a soft target leaving ~25% of the slot for propagation to the next slot's proposer. The hard deadline is slot end (100%): a slot-N payload attestation is accepted on the wire only while `data.slot == current_slot` and is consulted by fork choice only in slot N+1, so nothing consumes a slot-N vote during the [75%, 100%] window. Each operator therefore evaluates `PayloadAttestationData` (`payload_present`, `blob_data_available`, `beacon_block_root`) from its beacon node at the 75% broadcast mark, by which point `payload_present` has been frozen for a quarter slot, and runs its partial-signature round in the otherwise-free [75%, 100%] window. Broadcast may complete after 75% and still propagate; past slot end the message is dropped (gossip IGNORE and fork-choice wire REJECT when `data.slot != current_slot`), and each missed vote chips at the `PTC_SIZE/2` threshold that governs whether fork choice extends the payload. The beacon node also emits SSE events a runner may consume as a push trigger for when to evaluate its observation, instead of polling toward the cutoff: `execution_payload_gossip` and `execution_payload` fire when a `SignedExecutionPayloadEnvelope` passes `execution_payload`-topic gossip validation and when it is imported into fork choice, and `execution_payload_available` fires once the node has verified the payload and blobs are available and ready for payload attestation ([beacon-APIs event stream](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/eventstream/index.yaml)). These are timing signals only: `payload_present` and `blob_data_available` still come from the BN-computed `PayloadAttestationData` fetched via [`GET /eth/v1/validator/payload_attestation_data/{slot}`](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/validator/payload_attestation_data.yaml). -There is no pre-consensus phase and no QBFT round. Each operator evaluates `PayloadAttestationData` from its own beacon node at the 75% cutoff and signs that observation directly; there is no leader and no negotiated value. Because a per-validator BLS signature reconstructs only from partial signatures over byte-identical data, each operator pins its 75% `PayloadAttestationData` snapshot (with `slot` taken from the duty) and validates and aggregates incoming partial signatures against exactly that signing root. +There is no pre-consensus phase and no QBFT round. Each operator evaluates `PayloadAttestationData` from its own beacon node at the 75% broadcast mark and signs that observation directly; there is no leader and no negotiated value. Because a per-validator BLS signature reconstructs only from partial signatures over byte-identical data, each operator pins its 75% `PayloadAttestationData` snapshot (with `slot` taken from the duty) and validates and aggregates incoming partial signatures against exactly that signing root. An operator that has seen no beacon block for the slot abstains (submits nothing), matching the Gloas validator spec. Otherwise, the operator's runner for each of its local PTC-assigned validators produces one partial signature over the full `PayloadAttestationData` 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. Each runner broadcasts its partial signature in its own `PartialSignatureMessages` container with `Type = PTCAttesterPartialSig` (the runner role `RolePTCAttester` is the dispatch discriminator), one single-validator container per PTC-assigned validator, the same per-validator container shape as `ValidatorRegistration` today. Each operator accumulates peers' partial signatures over its own frozen root; when signatures over identical `PayloadAttestationData` reach the reconstruction threshold, it BLS-aggregates and submits one `PayloadAttestationMessage(validator_index, data, signature)` per validator to the beacon node, inside the [75%, 100%] window. Operators on a minority observation never reach threshold and contribute nothing, a non-slashable silent miss (see Security Considerations). The cluster therefore emits, per validator, the observation a threshold of its operators converged on, rather than a single leader's. @@ -172,7 +173,7 @@ const ( Relevant consensus-spec references: -- [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/validator.md#block-and-sidecar-proposal) +- [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/validator.md#block-and-sidecar-proposal) Under Gloas, `produceBlockV4` (`GET /eth/v4/validator/blocks/{slot}`, merged via [beacon-APIs #580](https://github.com/ethereum/beacon-APIs/pull/580)) 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. The variant is signaled by the required `execution_payload_included` response field and `Eth-Execution-Payload-Included` header, and driven by the `include_payload` query parameter (`true` requests the stateless `BlockContents` form). @@ -190,14 +191,14 @@ Pre-consensus RANDAO flow is unchanged. Post-consensus is unchanged: each operat Relevant consensus-spec references: -- [Broadcasting SignedProposerPreferences](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/validator.md#broadcasting-signedproposerpreferences) -- [`SignedProposerPreferences` container](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/p2p-interface.md#new-proposerpreferences) -- [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/p2p-interface.md#new-proposer_preferences) -- [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/p2p-interface.md#new-execution_payload_bid) +- [Broadcasting SignedProposerPreferences](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/validator.md#broadcasting-signedproposerpreferences) +- [`SignedProposerPreferences` container](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/p2p-interface.md#new-proposerpreferences) +- [`proposer_preferences` gossip topic](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/p2p-interface.md#new-proposer_preferences) +- [`execution_payload_bid` gossip validation](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/p2p-interface.md#new-execution_payload_bid) Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `proposer_preferences` p2p topic for future proposal slots within the proposer lookahead (the current epoch up to `MIN_SEED_LOOKAHEAD` epochs ahead). The signed `ProposerPreferences` carries `dependent_root`, `proposal_slot`, `validator_index`, `fee_recipient`, and `target_gas_limit`. `dependent_root` pins the proposer-lookahead epoch's seed via `get_shuffling_dependent_root(store, root, epoch)`; operators populate it from the `dependent_root` returned by [`GET /eth/v2/validator/duties/proposer/{epoch}`](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/validator/duties/proposer.v2.yaml) for the proposal-slot's epoch. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks; the `ValidatorRegistration` duty is deprecated accordingly (end of this section). -Gossip enforces the handshake at the `execution_payload_bid` topic: each bid requires a matching `SignedProposerPreferences` for its `(proposal_slot, dependent_root)` (otherwise IGNORE'd, not forwarded). The bid `fee_recipient` must match the preference (mismatch is REJECT'd), and the bid `gas_limit` must be EIP-1559-compatible with the proposer's `target_gas_limit` via `is_gas_limit_target_compatible` (incompatible is IGNORE'd). Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. +Gossip enforces the handshake at the `execution_payload_bid` topic: each bid requires a matching `SignedProposerPreferences` for its `(proposal_slot, dependent_root)` (otherwise IGNORE'd, not forwarded). The bid `fee_recipient` must match the preference, and the bid `gas_limit` must be EIP-1559-compatible with the proposer's `target_gas_limit` via `is_gas_limit_target_compatible`; both mismatches are IGNORE'd rather than REJECT'd (the fee-recipient check was downgraded from REJECT by [consensus-specs #5429](https://github.com/ethereum/consensus-specs/pull/5429): preferences are not equivocation-checked, so a mismatch is not provably the bidder's fault). Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. 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; the domain epoch is `compute_epoch_at_slot(proposal_slot)` per the spec's `get_signed_proposer_preferences`, so signatures for the pre-fork emission required below are computed under the Gloas fork domain even while the chain is still on the prior fork. `fee_recipient` is configured cluster-side (cluster-consistent in practice); `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000`, with runtime overrides; operators in a cluster must agree byte-for-byte at signing time); `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, and operators validate incoming partial signatures against their own derived root (the existing `ValidatorRegistration` expected-root validation); divergence splits signing roots, and reconstruction succeeds only when one root reaches threshold. `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). The reconstructed `SignedProposerPreferences` is submitted via `POST /eth/v1/validator/proposer_preferences` ([beacon-APIs #608](https://github.com/ethereum/beacon-APIs/pull/608)), which stores it and broadcasts it to the `proposer_preferences` topic. @@ -240,20 +241,20 @@ Gloas removes every protocol consumer of `SignedValidatorRegistrationV1`: builde Relevant consensus-spec references: -- [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/beacon-chain.md#executionpayloadenvelope) -- [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/p2p-interface.md#new-execution_payload) +- [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/beacon-chain.md#executionpayloadenvelope) +- [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/p2p-interface.md#new-execution_payload) - [`POST /eth/v1/beacon/execution_payload_envelopes` endpoint](https://github.com/ethereum/beacon-APIs/pull/580) On the self-build path (`bid.builder_index == BUILDER_INDEX_SELF_BUILD` per [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732)), the proposer signs `SignedExecutionPayloadEnvelope` after block publication. The SSV cluster runs a second QBFT round to produce this signature. #### Blinded envelope type -To bound QBFT message size, the cluster runs QBFT over a blinded form that substitutes `payload` with `payload_root: Root = hash_tree_root(payload)`. By SSZ Container positional merkleization, `hash_tree_root(BlindedExecutionPayloadEnvelope) == hash_tree_root(ExecutionPayloadEnvelope)`, so a BLS sig over the blinded signing root is valid for the full envelope. +To bound QBFT message size, the cluster runs QBFT over a blinded form that substitutes `payload` with `payload_root: Root = hash_tree_root(payload)`, the same shape as beacon-APIs' `Gloas.BlindedExecutionPayloadEnvelope` ([#580](https://github.com/ethereum/beacon-APIs/pull/580)). In SSZ merkleization every field subtree commits to `hash_tree_root(field)`, so substituting `payload` with its root in the same field position preserves the container root: `hash_tree_root(BlindedExecutionPayloadEnvelope) == hash_tree_root(ExecutionPayloadEnvelope)`, and a BLS sig over the blinded signing root is valid for the full envelope. Since [consensus-specs #4630](https://github.com/ethereum/consensus-specs/pull/4630) (EIP-7688), `ExecutionPayloadEnvelope` is a `ProgressiveContainer(active_fields=[1] * 5)` rather than a plain `Container`; the equivalence holds only if the blinded form merkleizes with that same progressive shape. Concretely: `hash_tree_root = mix_in_active_fields(merkleize_progressive([payload_root, htr(execution_requests), htr(builder_index), htr(beacon_block_root), htr(parent_beacon_block_root)]), [1] * 5)`, per the [SSZ merkleization rules](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/ssz/simple-serialize.md#merkleization). Hashing the blinded struct as an ordinary positional SSZ container yields a different root. ```go type BlindedExecutionPayloadEnvelope struct { PayloadRoot phase0.Root // == hash_tree_root(envelope.payload) - ExecutionRequests gloas.ExecutionRequests // Gloas 5-list container: adds builder_deposits and builder_exits (EIP-8282) + ExecutionRequests gloas.ExecutionRequests // Gloas 5-list container: adds builder_deposits and builder_exits (EIP-8282); itself progressive since EIP-7688 BuilderIndex uint64 BeaconBlockRoot phase0.Root ParentBeaconBlockRoot phase0.Root @@ -264,7 +265,7 @@ type BlindedExecutionPayloadEnvelope struct { Fires on the self-build path only, after the §4 block is signed and published. On the external-build path the builder signs and reveals its own `SignedExecutionPayloadEnvelope`, so this duty does not run. No pre-consensus phase. Envelope source by self-build variant: stateless self-build returns the envelope inline in `BlockContents`; stateful self-build requires a `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` to the same BN that served the §4 block, which returns the already-blinded `BlindedExecutionPayloadEnvelope` (cached server-side for the current slot only; keying by block root makes the lookup reorg-resistant). -The duty must target publishing the signed envelope before `get_payload_due_ms()` (the `PAYLOAD_DUE_BPS` 75% cutoff, §3), with margin for gossip to reach PTC beacon nodes before then. The §4 block is already out by the attestation deadline (§1), so the round has the intervening block-to-payload gap to work in. An envelope that misses the cutoff makes honest PTC validators vote `payload_present = False`, which can leave the next slot building on the empty parent (§3); this is the bounded missed-envelope degradation in Security Considerations. +The duty must target publishing the signed envelope before `get_payload_due_ms()` (the `PAYLOAD_DUE_BPS` cutoff, 50% of the slot since [consensus-specs #5414](https://github.com/ethereum/consensus-specs/pull/5414); §3), with margin for gossip to reach PTC beacon nodes before then. The §4 block is already out by the attestation deadline (§1, 25%), leaving roughly a quarter slot (~3s on mainnet) for the envelope QBFT round, post-consensus signing, and publication; #5414 halved this window from the earlier 75% cutoff. An envelope that misses the cutoff makes honest PTC validators vote `payload_present = False`, which can leave the next slot building on the empty parent (§3); this is the bounded missed-envelope degradation in Security Considerations. #### Roles and constants @@ -320,7 +321,7 @@ The Gloas `AttestationData.Index` value check (§2) does not require the QBFT-de ### PTC reconstruction is honest-convergence, not consensus -PTC runs no QBFT (§3): each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% cutoff, and a per-validator signature reconstructs only when a threshold of operators converged on byte-identical data. There is no leader to push a value contrary to the cluster's observation, and an operator can only ever vote its own honest observation. The cost is liveness rather than safety: when operators' beacon nodes split across observations near the cutoff (envelope-arrival or head jitter at the `MAXIMUM_GOSSIP_CLOCK_DISPARITY` boundary, or operators on diverged forks), no observation may reach threshold and the cluster's vote for that validator is a silent miss. That miss is non-slashable and its only effect is the foregone contribution to the `PTC_SIZE/2` fork-choice tally, bounded by SSV's PTC seat share. The off-slot-root case that Gloas gossip guards with an IGNORE-level `block.slot == data.slot` check ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/bd454cb0a6cff1b210ea9de208803df4d9966655/specs/gloas/p2p-interface.md#L346-L347)) cannot arise here: each operator signs the block it observed for `duty.slot`. +PTC runs no QBFT (§3): each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% broadcast mark, and a per-validator signature reconstructs only when a threshold of operators converged on byte-identical data. There is no leader to push a value contrary to the cluster's observation, and an operator can only ever vote its own honest observation. The cost is liveness rather than safety: when operators' beacon nodes split across observations (envelope-arrival jitter at the 50% `PAYLOAD_DUE_BPS` boundary, head or blob-availability jitter at evaluation time around `MAXIMUM_GOSSIP_CLOCK_DISPARITY`, or operators on diverged forks), no observation may reach threshold and the cluster's vote for that validator is a silent miss. That miss is non-slashable and its only effect is the foregone contribution to the `PTC_SIZE/2` fork-choice tally, bounded by SSV's PTC seat share. The off-slot-root case that Gloas gossip guards with an IGNORE-level `block.slot == data.slot` check ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/p2p-interface.md#L400-L401)) cannot arise here: each operator signs the block it observed for `duty.slot`. ### Config divergence silently disables trustless external builder bids From 677318353fe3a7d149f3145353d415bfae98ed38 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 6 Jul 2026 17:27:48 -0700 Subject: [PATCH 45/62] chore: rename EnvelopeBuilder role identifiers to EnvelopeProposer Matches ssv-spec #632; wire value 9 and DomainBeaconBuilder unchanged. --- sips/epbs_support.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 9878f83..b6a7756 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -272,10 +272,10 @@ The duty must target publishing the signed envelope before `get_payload_due_ms() ```go // types/beacon_types.go var DomainBeaconBuilder = [4]byte{0x0B, 0x00, 0x00, 0x00} -const BNRoleEnvelopeBuilder BeaconRole = 9 +const BNRoleEnvelopeProposer BeaconRole = 9 // types/runner_role.go -const RoleEnvelopeBuilder RunnerRole = 9 +const RoleEnvelopeProposer RunnerRole = 9 type EnvelopeConsensusData struct { Duty ValidatorDuty @@ -284,7 +284,7 @@ type EnvelopeConsensusData struct { } ``` -`MapDutyToRunnerRole()` must map `BNRoleEnvelopeBuilder` to `RoleEnvelopeBuilder`. Post-consensus reuses `PostConsensusPartialSig`; the runner role discriminates routing. +`MapDutyToRunnerRole()` must map `BNRoleEnvelopeProposer` to `RoleEnvelopeProposer`. Post-consensus reuses `PostConsensusPartialSig`; the runner role discriminates routing. #### QBFT proposal From 77b4055dbc9b5e445cb8274480e335ce6679d8b6 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Tue, 7 Jul 2026 12:07:19 -0700 Subject: [PATCH 46/62] =?UTF-8?q?feat:=20add=20=C2=A77=20SSV=20message-val?= =?UTF-8?q?idation=20rules=20for=20Gloas=20roles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Specify the per-role message-validation parameters for the three new roles (PTCAttester 7, ProposerPreferences 8, EnvelopeProposer 9) and the deprecated ValidatorRegistration, as a role-by-stage table plus per-row derivations and an epoch-aware fork-gate table. Add the §5 sentence pinning the ProposerPreferences message slot to proposal_slot. Answers Gal's message-validation request (G11) and iurii's re-emission dedup thread (I7, N=4 by (signer, proposal_slot, signing_root)). --- sips/epbs_support.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index b6a7756..ae2149b 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -204,6 +204,8 @@ The flow matches the existing `ValidatorRegistration` / `VoluntaryExit` shape: v 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. Emission during the first Gloas epoch itself would also be gossip-valid for slots later in that epoch, but slots early in the epoch leave effectively no post-fork emission time, and builders need a validator's preference before they can construct and gossip bids for its slot; pre-fork emission covers both, 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. +The duty's slot, carried in the `PartialSignatureMessages` envelope, is `proposal_slot` itself: one runner per proposal slot, matching ssv-spec's requirement that a partial-signature message's slot equal its duty's slot (`validatePartialSigMsgForSlot`). At emission time that slot is up to the proposer lookahead in the future, and a re-emission for the same slot carries a new signing root; both effects need dedicated message-validation rules, specified in §7. + This SIP adds a new beacon role `BNRoleProposerPreferences`, a matching runner role `RoleProposerPreferences`, and a new `PartialSigMsgType` `ProposerPreferencesPartialSig`. ```go @@ -309,6 +311,38 @@ Operators sign the decided `BlindedExecutionPayloadEnvelope`'s signing root unde Each operator's BN built a different envelope; only the operator whose blinded form matched the QBFT decision can publish. That operator reconstructs the signature and POSTs to `/eth/v1/beacon/execution_payload_envelopes` (merged via [beacon-APIs #580](https://github.com/ethereum/beacon-APIs/pull/580)); the body variant is selected by the required `Eth-Execution-Payload-Blinded` header. For stateless self-build the envelope arrived inline in `BlockContents`, so that operator holds the matching full payload, blobs, and KZG proofs and publishes `SignedExecutionPayloadEnvelopeContents` (header `false`). For stateful self-build the operator publishes `SignedBlindedExecutionPayloadEnvelope` (header `true`) to the same BN that produced the §4 block: the decided blinded envelope plus the reconstructed signature, from which that BN reconstructs the full envelope out of its production-time cache (any other BN lacks the cache and rejects the publish with a 400). Other operators complete without publishing. +### 7. SSV Message Validation + +SSV pubsub message validation is content-agnostic: it never decodes the signed beacon object, so it enforces only metadata. This section pins those rules for the three new roles and the deprecated one; all structural, signature, and topic rules for existing roles apply unchanged. Classifications follow the existing convention: REJECT penalizes the sending peer while IGNORE drops without forwarding. + +All three new roles are validator-scoped, like `RoleValidatorRegistration` and `RoleVoluntaryExit`: the `MessageID` carries the validator public key, routing reuses the cluster's existing subnet (no new topic), and each `PartialSignatureMessages` container carries at most one entry (the non-committee packet rule, REJECT above 1). Partial-signature type must match the role (REJECT on mismatch), and consensus messages are REJECT'd for the two non-QBFT roles, joining the existing registration/exit rule. + +| Role (wire) | Consensus messages | Partial-sig type; count per (signer, slot) | Message slot | Earliness allowance | Lateness TTL | Duty assignment check | Duty limit per epoch | +|---|---|---|---|---|---|---|---| +| `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) | + +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). + +Derivations, per row: + +- **PTC (7).** One partial-signature round, no pre/post split: `PTCAttesterPartialSig` is the only type, once per duty. The vote is consumed within its own slot and included at slot + 1 (§3), so the short non-committee TTL applies. The duty limit follows from `compute_ptc` drawing members exclusively from `get_beacon_committee(state, slot, i)`: a validator belongs to exactly one slot's beacon committees per epoch, so the honest bound is one PTC duty per validator per epoch, plus the +1 reorg margin the existing registration and aggregation limits already use. Multiple PTC seats within one slot are still one duty: the validator signs one `PayloadAttestationData`, and seat multiplicity lives in the aggregation bits. +- **ProposerPreferences (8).** + - *Earliness:* the message slot is `proposal_slot` (§5), up to the proposer lookahead in the future at emission, hence the allowance. + - *Lateness:* the tight TTL drops stale preferences as replays. + - *Slot-advance exempt:* a signer holds its whole lookahead at once, so a lower-slot message is a concurrent duty, not a stale one (lateness is its past bound). + - *Dedup:* the base one-per-(signer, slot) rule gains a bounded exception for distinct signing roots at one proposal slot; a repeat of a seen root is a duplicate (same-peer REJECT, relayed IGNORE, the existing two-tier handling), and a distinct root past the cap is IGNORE'd (rate-limiting, not a provable violation). Those extra roots come from preference-input changes between emissions, notably a `dependent_root` shift under reorg (§5); the cap is policy headroom, and the epoch-boundary batch never touches it since each proposal slot is its own key. + - *Duty limit:* at most one proposal per slot. +- **EnvelopeProposer (9).** Standard QBFT validation applies (leader round-robin, justification rules, one message per type per round); the round cut-off is the proposer-class value, and the §6 timing makes anything higher moot: the duty's window ends at the `PAYLOAD_DUE_BPS` cutoff (~a quarter slot, §6), which fits the first round plus at most one round-change. Post-consensus is the only partial-signature phase (no pre-consensus analog to RANDAO). The duty exists only for the cluster's own proposal slots, so the proposer-assignment check applies, and the duty limit mirrors the preferences bound. + +Epoch-aware fork gates, a new rule class. Both are REJECT because they are slot-scoped rather than wall-clock-scoped: the gated condition is carried in the message itself, so no honest ordering race can produce it. + +| Rule | Classification | Explanation | +|---|---|---| +| Role in {7, 8, 9} and `epoch(msg.slot) < GLOAS_FORK_EPOCH` | REJECT | These duties do not exist before the fork. §5's required pre-fork emission carries post-fork `proposal_slot` values by construction, so it passes this gate with no special case. | +| `RoleValidatorRegistration` (4) and `epoch(msg.slot) >= GLOAS_FORK_EPOCH` | REJECT | §5 deprecates the duty at the fork; honest registrations are always stamped with pre-fork slots. Pre-Gloas slots remain valid and unchanged. | + ## Security Considerations ### `GloasBeaconVoteValueCheckF` must include `AttestationDataIndex` in slashability checks From 018b3b196bdcfad95bf33e0f972d842f1ea887c0 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Tue, 7 Jul 2026 12:52:29 -0700 Subject: [PATCH 47/62] =?UTF-8?q?chore:=20=C2=A75=20cite=20builder-specs?= =?UTF-8?q?=20#138=20as=20merged;=20drop=20stale=20watchlist=20note?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #138 merged 2026-06-11 (L240 still said in flight, inconsistent with the L381 watchlist which already said merged). Drop the stale parenthetical about a validator-facing preferences submission endpoint being on the watchlist: that endpoint exists now via beacon-APIs #608, cited at L203/L380. --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index ae2149b..b58cbbf 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -237,7 +237,7 @@ const ( #### Deprecation of the `ValidatorRegistration` duty -Gloas removes every protocol consumer of `SignedValidatorRegistrationV1`: builders source `fee_recipient` and `target_gas_limit` from the `proposer_preferences` topic instead, the relay path that consumed registrations is gone with blinded blocks (§4), and the Gloas builder-API workstream itself deprecates `ValidatorRegistrationV1` in favor of `ProposerPreferences` ([builder-specs PR #138](https://github.com/ethereum/builder-specs/pull/138), in flight; see also [builder-specs issue #150](https://github.com/ethereum/builder-specs/issues/150)). This SIP therefore deprecates the duty at the fork: operators emit no `BNRoleValidatorRegistration` duties for epochs at or after `GLOAS_FORK_EPOCH`, message validation treats `ValidatorRegistrationPartialSig` messages for Gloas-or-later slots as invalid, and pre-Gloas slots are unchanged. `BNRoleValidatorRegistration`, `RoleValidatorRegistration`, and `ValidatorRegistrationPartialSig` keep their numeric values, reserved for pre-Gloas operation and backward-compat decoding, the same treatment as `RunnerRole` values `1` and `3` (§3). Where a beacon node sources self-build `fee_recipient` / `target_gas_limit` after the fork is BN configuration outside the SSV protocol, and it needs no distributed signature: registration signatures existed to authenticate proposers to untrusted relays (`prepare_beacon_proposer` carries only `fee_recipient` today, and a validator-facing preferences submission endpoint is on the watchlist). +Gloas removes every protocol consumer of `SignedValidatorRegistrationV1`: builders source `fee_recipient` and `target_gas_limit` from the `proposer_preferences` topic instead, the relay path that consumed registrations is gone with blinded blocks (§4), and the Gloas builder-API workstream itself deprecates `ValidatorRegistrationV1` in favor of `ProposerPreferences` ([builder-specs PR #138](https://github.com/ethereum/builder-specs/pull/138), merged 2026-06-11; see also [builder-specs issue #150](https://github.com/ethereum/builder-specs/issues/150)). This SIP therefore deprecates the duty at the fork: operators emit no `BNRoleValidatorRegistration` duties for epochs at or after `GLOAS_FORK_EPOCH`, message validation treats `ValidatorRegistrationPartialSig` messages for Gloas-or-later slots as invalid, and pre-Gloas slots are unchanged. `BNRoleValidatorRegistration`, `RoleValidatorRegistration`, and `ValidatorRegistrationPartialSig` keep their numeric values, reserved for pre-Gloas operation and backward-compat decoding, the same treatment as `RunnerRole` values `1` and `3` (§3). Where a beacon node sources self-build `fee_recipient` / `target_gas_limit` after the fork is BN configuration outside the SSV protocol, and it needs no distributed signature: registration signatures existed to authenticate proposers to untrusted relays (`prepare_beacon_proposer` carries only `fee_recipient` today). ### 6. New Duty: Envelope Signing (Self-Build Path) From 5b1e018cbae9de94083e2e33be4f71e82ded9fba Mon Sep 17 00:00:00 2001 From: shane-moore Date: Tue, 7 Jul 2026 16:37:45 -0700 Subject: [PATCH 48/62] =?UTF-8?q?chore:=20revert=20=C2=A76=20to=20position?= =?UTF-8?q?al=20merkleization;=20note=20EIP-7688=20is=20CFI=20not=20SFI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index b58cbbf..42b5b84 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -251,12 +251,12 @@ On the self-build path (`bid.builder_index == BUILDER_INDEX_SELF_BUILD` per [EIP #### Blinded envelope type -To bound QBFT message size, the cluster runs QBFT over a blinded form that substitutes `payload` with `payload_root: Root = hash_tree_root(payload)`, the same shape as beacon-APIs' `Gloas.BlindedExecutionPayloadEnvelope` ([#580](https://github.com/ethereum/beacon-APIs/pull/580)). In SSZ merkleization every field subtree commits to `hash_tree_root(field)`, so substituting `payload` with its root in the same field position preserves the container root: `hash_tree_root(BlindedExecutionPayloadEnvelope) == hash_tree_root(ExecutionPayloadEnvelope)`, and a BLS sig over the blinded signing root is valid for the full envelope. Since [consensus-specs #4630](https://github.com/ethereum/consensus-specs/pull/4630) (EIP-7688), `ExecutionPayloadEnvelope` is a `ProgressiveContainer(active_fields=[1] * 5)` rather than a plain `Container`; the equivalence holds only if the blinded form merkleizes with that same progressive shape. Concretely: `hash_tree_root = mix_in_active_fields(merkleize_progressive([payload_root, htr(execution_requests), htr(builder_index), htr(beacon_block_root), htr(parent_beacon_block_root)]), [1] * 5)`, per the [SSZ merkleization rules](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/ssz/simple-serialize.md#merkleization). Hashing the blinded struct as an ordinary positional SSZ container yields a different root. +To bound QBFT message size, the cluster runs QBFT over a blinded form that substitutes `payload` with `payload_root: Root = hash_tree_root(payload)`, the same shape as beacon-APIs' `Gloas.BlindedExecutionPayloadEnvelope` ([#580](https://github.com/ethereum/beacon-APIs/pull/580)). In SSZ merkleization every field subtree commits to `hash_tree_root(field)`, so substituting `payload` with its root in the same field position preserves the container root: `hash_tree_root(BlindedExecutionPayloadEnvelope) == hash_tree_root(ExecutionPayloadEnvelope)`, and a BLS sig over the blinded signing root is valid for the full envelope. (If EIP-7688 progressive containers land in Gloas, this note and the blinded struct below must merkleize with the progressive shape; see the watchlist.) ```go type BlindedExecutionPayloadEnvelope struct { PayloadRoot phase0.Root // == hash_tree_root(envelope.payload) - ExecutionRequests gloas.ExecutionRequests // Gloas 5-list container: adds builder_deposits and builder_exits (EIP-8282); itself progressive since EIP-7688 + ExecutionRequests gloas.ExecutionRequests // Gloas 5-list container: adds builder_deposits and builder_exits (EIP-8282) BuilderIndex uint64 BeaconBlockRoot phase0.Root ParentBeaconBlockRoot phase0.Root @@ -378,6 +378,7 @@ The slot's envelope is missed if the operator whose envelope blinds to the §6-d This section is intentionally limited to upstream items that could still change the normative SSV behavior described above. If any of these settle differently, this SIP should be updated. - beacon-APIs release status: `produceBlockV4` (`GET /eth/v4/validator/blocks/{slot}`), the §6 envelope publication and fetch endpoints, and the §5 `SignedProposerPreferences` submission endpoint are merged to `beacon-APIs/master` ([#580](https://github.com/ethereum/beacon-APIs/pull/580), [#608](https://github.com/ethereum/beacon-APIs/pull/608)) but unreleased: the latest release tag (`v5.0.0-alpha.2`) predates them, and no client implementations are marked in the compatibility matrix yet. Watch for pre-release changes to the response variant discriminators, request/response bodies, and SSZ encodings. +- EIP-7688 progressive containers: [EIP-7688](https://eips.ethereum.org/EIPS/eip-7688) (forward-compatible consensus data structures) reshapes core Gloas containers into `ProgressiveContainer`s, changing how `hash_tree_root` is computed for the §4 block signing root and the §6 envelope. It is only Considered for Inclusion (CFI), not Scheduled for Inclusion, for Glamsterdam ([EIP-7773](https://eips.ethereum.org/EIPS/eip-7773); targeted for glamsterdam-devnet-7, inclusion decision pending). The pinned snapshot `801a38e15` has already merged it ([consensus-specs #4630](https://github.com/ethereum/consensus-specs/pull/4630)), but this SIP describes positional merkleization until 7688 is SFI'd: at that point §6's blinded-envelope note adopts `mix_in_active_fields(merkleize_progressive(...))` and §4 gains a matching block-root note. - Builder-API authenticated connections (`SignedRequestAuthV1`): builder-specs [#138](https://github.com/ethereum/builder-specs/pull/138) (merged 2026-06-11, not yet in a release) defines an optional validator-key BLS signature over `{builder URL, slot}` under the fork-agnostic domain `DOMAIN_REQUEST_AUTH` (`0x0B000001`), used to authenticate direct bid requests (`getExecutionPayloadBid`) and required for per-builder execution payments (`submitBuilderPreferences`). A bid pays the proposer via `bid.value`, deducted on-chain from the builder's staked collateral, and optionally via `bid.execution_payment`, a trusted execution-layer payment that the proposer must opt into per builder by submitting a `max_execution_payment` cap through `submitBuilderPreferences`. Deferring the duty costs no liveness in the interim: a cluster that has not yet submitted builder preferences has a zero cap, so builders MUST NOT attach execution payments and every bid it can receive pays from staked collateral only (gossiped bids MUST carry `execution_payment = 0` regardless of channel). Distributed signing of `RequestAuthV1` follows the `ValidatorRegistration` shape (validator-scoped, one partial-signature round, no QBFT) and will be specified once the upstream surface settles, meaning a standardized hop between the key holder and block assembly (a beacon-APIs successor to the now-deprecated `register_validator` forwarding, or a sidecar specification) and a builder-specs release that includes the Gloas builder API. ## Acknowledgements From 5713c401c67f5ed80d88a6b5bcfde3218849f8f1 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Tue, 7 Jul 2026 16:51:09 -0700 Subject: [PATCH 49/62] =?UTF-8?q?chore:=20=C2=A72=20pin=20DataVersionGloas?= =?UTF-8?q?=20on=20aggregator-committee=20consensus=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 42b5b84..823b16b 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -112,6 +112,8 @@ Pre-Gloas slots continue to run `BeaconVoteValueCheckF()` unchanged. The `BNRoleAggregator` duty (handled by the aggregator-committee runner) fetches aggregated attestations from the Beacon API's aggregate-attestation endpoint with `attestation_data_root` as an input. Implementations must compute that root from the BN-supplied `AttestationData` (including its Gloas `index`). +`AggregatorCommitteeConsensusData.Version` is part of the QBFT-decided value, so operators must stamp it identically: use `DataVersionGloas` at Gloas slots (as §4 does for `ProposerConsensusData`). The aggregate stays Electra-shaped; Gloas changes only `AttestationData.Index` semantics, not the aggregate container. + ### 3. New Duty: Payload Timeliness Committee (PTC) Attestation Relevant consensus-spec references: From 6786b80cd91fd836d91f18486b2d062da7895a25 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 8 Jul 2026 10:28:49 -0700 Subject: [PATCH 50/62] =?UTF-8?q?chore:=20=C2=A75=20reframe=20target=5Fgas?= =?UTF-8?q?=5Flimit=20as=20operator-configured;=20drop=20deprecated=20Defa?= =?UTF-8?q?ultGasLimit=20citation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 823b16b..a013dc1 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -28,7 +28,7 @@ Key design choices and why: - **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 validator-scoped, non-QBFT runner.** Each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% broadcast mark, validates incoming partial signatures against its own derived signing root, and reconstructs when that root reaches threshold, the same one-round shape as `ProposerPreferences` (§5). A PTC vote is one beacon node's observation, not a value to negotiate, so QBFT would only add round-trips that risk the late-slot deadline (§3). -- **Proposer-preferences is validator-scoped and non-QBFT.** The per-validator `fee_recipient` is configured cluster-side and is cluster-consistent in practice; `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000` in `types/beacon_types.go`, with runtime overrides; operators in a cluster must agree byte-for-byte on the value used at signing time, same as the existing validator-registration flow). Operators independently derive the full `ProposerPreferences` and validate incoming partial signatures against their own derived signing root; reconstruction succeeds only when a quorum of operators converge on one signing root. The registration-like one-round partial-sig-and-submit flow from `voluntary_exit.md` fits directly. +- **Proposer-preferences is validator-scoped and non-QBFT.** The per-validator `fee_recipient` is configured cluster-side and is cluster-consistent in practice; `target_gas_limit` is operator-configured, with a client default when unset; operators in a cluster must agree byte-for-byte on the value used at signing time, the same convergence requirement as the existing validator-registration flow. Operators independently derive the full `ProposerPreferences` and validate incoming partial signatures against their own derived signing root; reconstruction succeeds only when a quorum of operators converge on one signing root. The registration-like one-round partial-sig-and-submit flow from `voluntary_exit.md` fits directly. - **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` for the self-build path is covered by a separate companion QBFT duty (§6), keyed by the block QBFT's decided block root. - **Envelope QBFT uses a blinded envelope shape.** §6's duty runs QBFT over `BlindedExecutionPayloadEnvelope` (`payload` → `payload_root: Root`), whose hash tree root equals the full envelope's. Keeps QBFT messages bounded (~few hundred bytes vs hundreds of KB to ~MB). @@ -202,7 +202,7 @@ Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `propos Gossip enforces the handshake at the `execution_payload_bid` topic: each bid requires a matching `SignedProposerPreferences` for its `(proposal_slot, dependent_root)` (otherwise IGNORE'd, not forwarded). The bid `fee_recipient` must match the preference, and the bid `gas_limit` must be EIP-1559-compatible with the proposer's `target_gas_limit` via `is_gas_limit_target_compatible`; both mismatches are IGNORE'd rather than REJECT'd (the fee-recipient check was downgraded from REJECT by [consensus-specs #5429](https://github.com/ethereum/consensus-specs/pull/5429): preferences are not equivocation-checked, so a mismatch is not provably the bidder's fault). Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. -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; the domain epoch is `compute_epoch_at_slot(proposal_slot)` per the spec's `get_signed_proposer_preferences`, so signatures for the pre-fork emission required below are computed under the Gloas fork domain even while the chain is still on the prior fork. `fee_recipient` is configured cluster-side (cluster-consistent in practice); `target_gas_limit` lives in operator config (ssv-spec default `DefaultGasLimit = 30_000_000`, with runtime overrides; operators in a cluster must agree byte-for-byte at signing time); `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, and operators validate incoming partial signatures against their own derived root (the existing `ValidatorRegistration` expected-root validation); divergence splits signing roots, and reconstruction succeeds only when one root reaches threshold. `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). The reconstructed `SignedProposerPreferences` is submitted via `POST /eth/v1/validator/proposer_preferences` ([beacon-APIs #608](https://github.com/ethereum/beacon-APIs/pull/608)), which stores it and broadcasts it to the `proposer_preferences` topic. +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; the domain epoch is `compute_epoch_at_slot(proposal_slot)` per the spec's `get_signed_proposer_preferences`, so signatures for the pre-fork emission required below are computed under the Gloas fork domain even while the chain is still on the prior fork. `fee_recipient` is configured cluster-side (cluster-consistent in practice); `target_gas_limit` is operator-configured, with a client default when unset; operators in a cluster must agree byte-for-byte at signing time; `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, and operators validate incoming partial signatures against their own derived root (the existing `ValidatorRegistration` expected-root validation); divergence splits signing roots, and reconstruction succeeds only when one root reaches threshold. `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). The reconstructed `SignedProposerPreferences` is submitted via `POST /eth/v1/validator/proposer_preferences` ([beacon-APIs #608](https://github.com/ethereum/beacon-APIs/pull/608)), which stores it and broadcasts it to the `proposer_preferences` topic. 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. Emission during the first Gloas epoch itself would also be gossip-valid for slots later in that epoch, but slots early in the epoch leave effectively no post-fork emission time, and builders need a validator's preference before they can construct and gossip bids for its slot; pre-fork emission covers both, 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. From a94f04b7acc3f2755d804ba42ac63ff752c7a148 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 8 Jul 2026 12:09:48 -0700 Subject: [PATCH 51/62] =?UTF-8?q?chore:=20=C2=A74=20reject=20decided=20pro?= =?UTF-8?q?poser=20value=20whose=20Version=20!=3D=20fork(duty.Slot)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index a013dc1..53320ec 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -185,6 +185,8 @@ Under Gloas, `produceBlockV4` (`GET /eth/v4/validator/blocks/{slot}`, merged via Although the struct shape is unchanged, [`ProposerConsensusData.GetBlockData()`](https://github.com/ssvlabs/ssv-spec/blob/85ee4f32e4fc22bae8aacf837153aab3dcd6620b/types/consensus_data.go#L175-L237)'s per-version switch (Capella → Fulu today) needs a new `DataVersionGloas` arm that unmarshals `DataSSZ` as `Gloas.BeaconBlock`. +The decided value's `Version` selects how `DataSSZ` is decoded (`Gloas.BeaconBlock` when `Version >= DataVersionGloas`), and `Version` is leader-supplied. An operator MUST reject any decided value whose `Version` does not equal the fork scheduled at `duty.Slot`. Honest proposers always stamp `Version == fork(duty.Slot)`, so this rejects no honest value. + 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 as `Gloas.SignedBeaconBlock` via the existing block-publish endpoint. **Envelope signing.** 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. Distributed signing of `SignedExecutionPayloadEnvelope` for the self-build path is specified in §6. From 2991498ba70df85cd39373d3d694b1bd026f478c Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 8 Jul 2026 12:21:34 -0700 Subject: [PATCH 52/62] =?UTF-8?q?chore:=20=C2=A72=20reframe=20as=20extendi?= =?UTF-8?q?ng=20BeaconVote=20at=20the=20fork;=20drop=20follow-up-SIP=20not?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 53320ec..ce46940 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -26,7 +26,7 @@ Gloas changes validator duties in ways that break a few current SSV assumptions: Key design choices and why: -- **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. +- **`BeaconVote` gains `AttestationDataIndex` at the Gloas fork.** 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. The pre-Gloas encoding stays frozen for pre-Gloas slots, so the two are mutually rejecting on length. - **PTC is a validator-scoped, non-QBFT runner.** Each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% broadcast mark, validates incoming partial signatures against its own derived signing root, and reconstructs when that root reaches threshold, the same one-round shape as `ProposerPreferences` (§5). A PTC vote is one beacon node's observation, not a value to negotiate, so QBFT would only add round-trips that risk the late-slot deadline (§3). - **Proposer-preferences is validator-scoped and non-QBFT.** The per-validator `fee_recipient` is configured cluster-side and is cluster-consistent in practice; `target_gas_limit` is operator-configured, with a client default when unset; operators in a cluster must agree byte-for-byte on the value used at signing time, the same convergence requirement as the existing validator-registration flow. Operators independently derive the full `ProposerPreferences` and validate incoming partial signatures against their own derived signing root; reconstruction succeeds only when a quorum of operators converge on one signing root. The registration-like one-round partial-sig-and-submit flow from `voluntary_exit.md` fits directly. - **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` for the self-build path is covered by a separate companion QBFT duty (§6), keyed by the block QBFT's decided block root. @@ -74,19 +74,17 @@ SSV currently omits `AttestationData.Index` from `BeaconVote` and fills it local #### Required change -A new `GloasBeaconVote` struct mirrors `BeaconVote` plus an `AttestationDataIndex` field, matching the `phase0.CommitteeIndex` (a `uint64` alias) type of `AttestationData.Index` in consensus specs so reconstruction is a direct field assignment. Gloas-era QBFT instances decide on `GloasBeaconVote`; pre-Gloas instances continue to decide on the existing `BeaconVote`, whose SSZ layout is unchanged. A separate type (rather than extending `BeaconVote` in place) is the cleanest way to make pre-Gloas and Gloas wire bytes mutually-rejecting on length mismatch, since SSZ derives do not support fork-conditional fields. The restricted Gloas value space (`0` = `EMPTY`, `1` = `FULL` for non-same-slot attestations; `0` for same-slot) is enforced in the value check below, not at the type level. - -After the Gloas fork epoch has activated on all networks and pre-Gloas slots are no longer reachable in normal operation, a follow-up SIP can retire `BeaconVote` and rename `GloasBeaconVote` back to `BeaconVote`. +At the Gloas fork, `BeaconVote` is extended with an `AttestationDataIndex` field (`phase0.CommitteeIndex`, a `uint64` alias matching `AttestationData.Index`, so reconstruction is a direct field assignment). The pre-Gloas 3-field encoding stays frozen for pre-Gloas slots; decoders select by fork, and the two encodings are mutually rejecting on length (fixed-size SSZ, 112 vs 120 bytes). Implementations MAY realize the transition as two concrete types. The restricted Gloas value space (`0` = `EMPTY`, `1` = `FULL` for non-same-slot attestations; `0` for same-slot) is enforced in the value check below, not at the type level. ```go -// Existing (ssv-spec types/consensus_data.go); unchanged +// Pre-Gloas encoding (ssv-spec types/consensus_data.go); frozen for pre-Gloas slots type BeaconVote struct { BlockRoot phase0.Root `ssz-size:"32"` Source *phase0.Checkpoint Target *phase0.Checkpoint } -// New (Gloas only) +// Gloas encoding; implementations may keep a distinct type through the transition type GloasBeaconVote struct { BlockRoot phase0.Root `ssz-size:"32"` Source *phase0.Checkpoint From 9f7a3b0dbec222a59cf16b17d33a443c72af4d7e Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 8 Jul 2026 12:50:33 -0700 Subject: [PATCH 53/62] =?UTF-8?q?chore:=20=C2=A72=20reword=20aggregation-p?= =?UTF-8?q?ath=20root=20note=20(compute=20from=20decided=20GloasBeaconVote?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index ce46940..a63cd13 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -108,7 +108,7 @@ Pre-Gloas slots continue to run `BeaconVoteValueCheckF()` unchanged. #### Implementation note: aggregation path -The `BNRoleAggregator` duty (handled by the aggregator-committee runner) fetches aggregated attestations from the Beacon API's aggregate-attestation endpoint with `attestation_data_root` as an input. Implementations must compute that root from the BN-supplied `AttestationData` (including its Gloas `index`). +The `BNRoleAggregator` duty (handled by the aggregator-committee runner) fetches aggregated attestations from the Beacon API's aggregate-attestation endpoint with `attestation_data_root` as an input. Implementations must compute that root over the full Gloas `AttestationData` reconstructed from the decided `GloasBeaconVote` (including `AttestationDataIndex`), rather than from a locally reconstructed pre-Gloas shape; a root computed without the decided `index` matches no aggregate. `AggregatorCommitteeConsensusData.Version` is part of the QBFT-decided value, so operators must stamp it identically: use `DataVersionGloas` at Gloas slots (as §4 does for `ProposerConsensusData`). The aggregate stays Electra-shaped; Gloas changes only `AttestationData.Index` semantics, not the aggregate container. From 0ca969064b2e43362dc3db8dd5dc0353a248dc41 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 8 Jul 2026 12:57:30 -0700 Subject: [PATCH 54/62] =?UTF-8?q?chore:=20trim=20=C2=A73=20no-value-check?= =?UTF-8?q?=20paragraph=20to=20the=20two=20facts=20that=20matter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index a63cd13..43d0146 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -136,7 +136,7 @@ An operator that has seen no beacon block for the slot abstains (submits nothing False votes and missed votes are equivalent in the payload-extension tally (`should_extend_payload` counts only `True` votes toward the threshold), but not for the next proposer's parent choice: explicit `False` majorities on either field steer the proposer off the full parent in `should_build_on_full` (via `payload_timeliness(..., timely=False)` and `payload_data_availability(..., available=False)`), weight a missed vote does not carry. -There is no QBFT value check: there is no leader-proposed value to validate, since each operator signs only the observation it made (one that saw no block abstains, as above). The off-slot-root concern that a leader-decided root would raise does not arise, because each operator signs the block it observed for `duty.slot`, on-slot by construction. PTC attestations are not in the beacon chain slashing predicate, so no slashability call is required. +There is no QBFT and therefore no value check. PTC attestations are not in the beacon chain slashing predicate, so no slashability call is required. This SIP adds a new beacon role `BNRolePTCAttester`, a matching runner role `RolePTCAttester`, and a new `PartialSigMsgType` `PTCAttesterPartialSig`. From 9daebe89f0a50e1a7ceb545d4cc09367a27b8eb7 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 8 Jul 2026 13:13:32 -0700 Subject: [PATCH 55/62] =?UTF-8?q?chore:=20link=20all=20=C2=A7N=20section?= =?UTF-8?q?=20references=20to=20their=20heading=20anchors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sips/epbs_support.md | 64 ++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 43d0146..92a056e 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -10,9 +10,9 @@ Validator client related changes via ePBS: 1. earlier slot deadlines 2. attestation handling changes, including preservation of the Gloas `attestation.data.index` semantics 3. the new Payload Timeliness Committee (PTC) -4. the Gloas proposer flow using `produceBlockV4`. The SSV cluster signs the `Gloas.BeaconBlock` in §4 and, on the self-build path, signs `SignedExecutionPayloadEnvelope` as a companion QBFT duty (§6). +4. the Gloas proposer flow using `produceBlockV4`. The SSV cluster signs the `Gloas.BeaconBlock` in [§4](#4-modified-proposer-duty) and, on the self-build path, signs `SignedExecutionPayloadEnvelope` as a companion QBFT duty ([§6](#6-new-duty-envelope-signing-self-build-path)). 5. the new `SignedProposerPreferences` message must be submitted if the node operator wants to be able to select block bids received over p2p -6. the existing validator-registration duty is deprecated at the Gloas fork, its purpose replaced by `SignedProposerPreferences` (§5) +6. the existing validator-registration duty is deprecated at the Gloas fork, its purpose replaced by `SignedProposerPreferences` ([§5](#5-proposer-preferences-duty)) ## Motivation @@ -27,10 +27,10 @@ Gloas changes validator duties in ways that break a few current SSV assumptions: Key design choices and why: - **`BeaconVote` gains `AttestationDataIndex` at the Gloas fork.** 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. The pre-Gloas encoding stays frozen for pre-Gloas slots, so the two are mutually rejecting on length. -- **PTC is a validator-scoped, non-QBFT runner.** Each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% broadcast mark, validates incoming partial signatures against its own derived signing root, and reconstructs when that root reaches threshold, the same one-round shape as `ProposerPreferences` (§5). A PTC vote is one beacon node's observation, not a value to negotiate, so QBFT would only add round-trips that risk the late-slot deadline (§3). +- **PTC is a validator-scoped, non-QBFT runner.** Each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% broadcast mark, validates incoming partial signatures against its own derived signing root, and reconstructs when that root reaches threshold, the same one-round shape as `ProposerPreferences` ([§5](#5-proposer-preferences-duty)). A PTC vote is one beacon node's observation, not a value to negotiate, so QBFT would only add round-trips that risk the late-slot deadline ([§3](#3-new-duty-payload-timeliness-committee-ptc-attestation)). - **Proposer-preferences is validator-scoped and non-QBFT.** The per-validator `fee_recipient` is configured cluster-side and is cluster-consistent in practice; `target_gas_limit` is operator-configured, with a client default when unset; operators in a cluster must agree byte-for-byte on the value used at signing time, the same convergence requirement as the existing validator-registration flow. Operators independently derive the full `ProposerPreferences` and validate incoming partial signatures against their own derived signing root; reconstruction succeeds only when a quorum of operators converge on one signing root. The registration-like one-round partial-sig-and-submit flow from `voluntary_exit.md` fits directly. -- **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` for the self-build path is covered by a separate companion QBFT duty (§6), keyed by the block QBFT's decided block root. -- **Envelope QBFT uses a blinded envelope shape.** §6's duty runs QBFT over `BlindedExecutionPayloadEnvelope` (`payload` → `payload_root: Root`), whose hash tree root equals the full envelope's. Keeps QBFT messages bounded (~few hundred bytes vs hundreds of KB to ~MB). +- **Block QBFT remains scoped to the `Gloas.BeaconBlock`.** `ProposerConsensusData.data_ssz` carries the block SSZ, matching today's shape. Distributed signing of `SignedExecutionPayloadEnvelope` for the self-build path is covered by a separate companion QBFT duty ([§6](#6-new-duty-envelope-signing-self-build-path)), keyed by the block QBFT's decided block root. +- **Envelope QBFT uses a blinded envelope shape.** [§6](#6-new-duty-envelope-signing-self-build-path)'s duty runs QBFT over `BlindedExecutionPayloadEnvelope` (`payload` → `payload_root: Root`), whose hash tree root equals the full envelope's. Keeps QBFT messages bounded (~few hundred bytes vs hundreds of KB to ~MB). ## Specification @@ -49,7 +49,7 @@ Relevant consensus-spec references: | Aggregation | 2/3 slot (~8s) | 1/2 slot (50%, ~6s) | | Sync Committee Contribution | 2/3 slot | 1/2 slot (50%) | | PTC Attestation | - | 3/4 slot (75%, ~9s) | -| Envelope reveal (self-build, §6) | - | 1/2 slot (50%, ~6s) | +| Envelope reveal (self-build, [§6](#6-new-duty-envelope-signing-self-build-path)) | - | 1/2 slot (50%, ~6s) | ### 2. Modified Attestation Duty @@ -110,7 +110,7 @@ Pre-Gloas slots continue to run `BeaconVoteValueCheckF()` unchanged. The `BNRoleAggregator` duty (handled by the aggregator-committee runner) fetches aggregated attestations from the Beacon API's aggregate-attestation endpoint with `attestation_data_root` as an input. Implementations must compute that root over the full Gloas `AttestationData` reconstructed from the decided `GloasBeaconVote` (including `AttestationDataIndex`), rather than from a locally reconstructed pre-Gloas shape; a root computed without the decided `index` matches no aggregate. -`AggregatorCommitteeConsensusData.Version` is part of the QBFT-decided value, so operators must stamp it identically: use `DataVersionGloas` at Gloas slots (as §4 does for `ProposerConsensusData`). The aggregate stays Electra-shaped; Gloas changes only `AttestationData.Index` semantics, not the aggregate container. +`AggregatorCommitteeConsensusData.Version` is part of the QBFT-decided value, so operators must stamp it identically: use `DataVersionGloas` at Gloas slots (as [§4](#4-modified-proposer-duty) does for `ProposerConsensusData`). The aggregate stays Electra-shaped; Gloas changes only `AttestationData.Index` semantics, not the aggregate container. ### 3. New Duty: Payload Timeliness Committee (PTC) Attestation @@ -167,7 +167,7 @@ const ( `RunnerRole` values `1` and `3` are reserved for backward-compat decoding of pre-consolidation messages. -`MapDutyToRunnerRole()` must map `BNRolePTCAttester` to `RolePTCAttester`. PTC reuses the existing `ValidatorDuty`, with one runner instance scoped per PTC-assigned validator (keyed by validator pubkey), the same validator-scoped shape as `ProposerPreferences` (§5). A slot's PTC is `PTC_SIZE` (512) seats selected from that slot's beacon committees, so a cluster typically holds zero or one PTC seat in a given slot; per-validator scoping reuses the single-validator signing path, and committee-style bundling would save almost nothing. +`MapDutyToRunnerRole()` must map `BNRolePTCAttester` to `RolePTCAttester`. PTC reuses the existing `ValidatorDuty`, with one runner instance scoped per PTC-assigned validator (keyed by validator pubkey), the same validator-scoped shape as `ProposerPreferences` ([§5](#5-proposer-preferences-duty)). A slot's PTC is `PTC_SIZE` (512) seats selected from that slot's beacon committees, so a cluster typically holds zero or one PTC seat in a given slot; per-validator scoping reuses the single-validator signing path, and committee-style bundling would save almost nothing. ### 4. Modified Proposer Duty @@ -177,9 +177,9 @@ Relevant consensus-spec references: Under Gloas, `produceBlockV4` (`GET /eth/v4/validator/blocks/{slot}`, merged via [beacon-APIs #580](https://github.com/ethereum/beacon-APIs/pull/580)) 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. The variant is signaled by the required `execution_payload_included` response field and `Eth-Execution-Payload-Included` header, and driven by the `include_payload` query parameter (`true` requests the stateless `BlockContents` form). -*Note (non-normative).* When to issue the `produceBlockV4` request is the residual proposer timing discretion: a later request lets the beacon node observe more bids and can return a higher-value block. The window is tighter for an SSV cluster than for a solo proposer, since the returned block must still clear QBFT, post-consensus signing, and propagation before the earlier attestation deadline (§1), so the request must leave room for them. Operator policy, not a protocol requirement. +*Note (non-normative).* When to issue the `produceBlockV4` request is the residual proposer timing discretion: a later request lets the beacon node observe more bids and can return a higher-value block. The window is tighter for an SSV cluster than for a solo proposer, since the returned block must still clear QBFT, post-consensus signing, and propagation before the earlier attestation deadline ([§1](#1-slot-timing-changes)), so the request must leave room for them. Operator policy, not a protocol requirement. -`ProposerConsensusData` is preserved: its struct shape (`Duty`, `Version`, `DataSSZ []byte`) is unchanged. `DataSSZ` carries the SSZ-encoded `Gloas.BeaconBlock`. The stateless `BlockContents` variant is handled identically: the cluster extracts the block into `DataSSZ` for QBFT; the inline envelope, blobs, and KZG proofs are handled by §6. +`ProposerConsensusData` is preserved: its struct shape (`Duty`, `Version`, `DataSSZ []byte`) is unchanged. `DataSSZ` carries the SSZ-encoded `Gloas.BeaconBlock`. The stateless `BlockContents` variant is handled identically: the cluster extracts the block into `DataSSZ` for QBFT; the inline envelope, blobs, and KZG proofs are handled by [§6](#6-new-duty-envelope-signing-self-build-path). Although the struct shape is unchanged, [`ProposerConsensusData.GetBlockData()`](https://github.com/ssvlabs/ssv-spec/blob/85ee4f32e4fc22bae8aacf837153aab3dcd6620b/types/consensus_data.go#L175-L237)'s per-version switch (Capella → Fulu today) needs a new `DataVersionGloas` arm that unmarshals `DataSSZ` as `Gloas.BeaconBlock`. @@ -187,7 +187,7 @@ The decided value's `Version` selects how `DataSSZ` is decoded (`Gloas.BeaconBlo 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 as `Gloas.SignedBeaconBlock` via the existing block-publish endpoint. -**Envelope signing.** 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. Distributed signing of `SignedExecutionPayloadEnvelope` for the self-build path is specified in §6. +**Envelope signing.** 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. Distributed signing of `SignedExecutionPayloadEnvelope` for the self-build path is specified in [§6](#6-new-duty-envelope-signing-self-build-path). ### 5. Proposer Preferences Duty @@ -206,7 +206,7 @@ The flow matches the existing `ValidatorRegistration` / `VoluntaryExit` shape: v 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. Emission during the first Gloas epoch itself would also be gossip-valid for slots later in that epoch, but slots early in the epoch leave effectively no post-fork emission time, and builders need a validator's preference before they can construct and gossip bids for its slot; pre-fork emission covers both, 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. -The duty's slot, carried in the `PartialSignatureMessages` envelope, is `proposal_slot` itself: one runner per proposal slot, matching ssv-spec's requirement that a partial-signature message's slot equal its duty's slot (`validatePartialSigMsgForSlot`). At emission time that slot is up to the proposer lookahead in the future, and a re-emission for the same slot carries a new signing root; both effects need dedicated message-validation rules, specified in §7. +The duty's slot, carried in the `PartialSignatureMessages` envelope, is `proposal_slot` itself: one runner per proposal slot, matching ssv-spec's requirement that a partial-signature message's slot equal its duty's slot (`validatePartialSigMsgForSlot`). At emission time that slot is up to the proposer lookahead in the future, and a re-emission for the same slot carries a new signing root; both effects need dedicated message-validation rules, specified in [§7](#7-ssv-message-validation). This SIP adds a new beacon role `BNRoleProposerPreferences`, a matching runner role `RoleProposerPreferences`, and a new `PartialSigMsgType` `ProposerPreferencesPartialSig`. @@ -239,7 +239,7 @@ const ( #### Deprecation of the `ValidatorRegistration` duty -Gloas removes every protocol consumer of `SignedValidatorRegistrationV1`: builders source `fee_recipient` and `target_gas_limit` from the `proposer_preferences` topic instead, the relay path that consumed registrations is gone with blinded blocks (§4), and the Gloas builder-API workstream itself deprecates `ValidatorRegistrationV1` in favor of `ProposerPreferences` ([builder-specs PR #138](https://github.com/ethereum/builder-specs/pull/138), merged 2026-06-11; see also [builder-specs issue #150](https://github.com/ethereum/builder-specs/issues/150)). This SIP therefore deprecates the duty at the fork: operators emit no `BNRoleValidatorRegistration` duties for epochs at or after `GLOAS_FORK_EPOCH`, message validation treats `ValidatorRegistrationPartialSig` messages for Gloas-or-later slots as invalid, and pre-Gloas slots are unchanged. `BNRoleValidatorRegistration`, `RoleValidatorRegistration`, and `ValidatorRegistrationPartialSig` keep their numeric values, reserved for pre-Gloas operation and backward-compat decoding, the same treatment as `RunnerRole` values `1` and `3` (§3). Where a beacon node sources self-build `fee_recipient` / `target_gas_limit` after the fork is BN configuration outside the SSV protocol, and it needs no distributed signature: registration signatures existed to authenticate proposers to untrusted relays (`prepare_beacon_proposer` carries only `fee_recipient` today). +Gloas removes every protocol consumer of `SignedValidatorRegistrationV1`: builders source `fee_recipient` and `target_gas_limit` from the `proposer_preferences` topic instead, the relay path that consumed registrations is gone with blinded blocks ([§4](#4-modified-proposer-duty)), and the Gloas builder-API workstream itself deprecates `ValidatorRegistrationV1` in favor of `ProposerPreferences` ([builder-specs PR #138](https://github.com/ethereum/builder-specs/pull/138), merged 2026-06-11; see also [builder-specs issue #150](https://github.com/ethereum/builder-specs/issues/150)). This SIP therefore deprecates the duty at the fork: operators emit no `BNRoleValidatorRegistration` duties for epochs at or after `GLOAS_FORK_EPOCH`, message validation treats `ValidatorRegistrationPartialSig` messages for Gloas-or-later slots as invalid, and pre-Gloas slots are unchanged. `BNRoleValidatorRegistration`, `RoleValidatorRegistration`, and `ValidatorRegistrationPartialSig` keep their numeric values, reserved for pre-Gloas operation and backward-compat decoding, the same treatment as `RunnerRole` values `1` and `3` ([§3](#3-new-duty-payload-timeliness-committee-ptc-attestation)). Where a beacon node sources self-build `fee_recipient` / `target_gas_limit` after the fork is BN configuration outside the SSV protocol, and it needs no distributed signature: registration signatures existed to authenticate proposers to untrusted relays (`prepare_beacon_proposer` carries only `fee_recipient` today). ### 6. New Duty: Envelope Signing (Self-Build Path) @@ -267,9 +267,9 @@ type BlindedExecutionPayloadEnvelope struct { #### Trigger and envelope source -Fires on the self-build path only, after the §4 block is signed and published. On the external-build path the builder signs and reveals its own `SignedExecutionPayloadEnvelope`, so this duty does not run. No pre-consensus phase. Envelope source by self-build variant: stateless self-build returns the envelope inline in `BlockContents`; stateful self-build requires a `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` to the same BN that served the §4 block, which returns the already-blinded `BlindedExecutionPayloadEnvelope` (cached server-side for the current slot only; keying by block root makes the lookup reorg-resistant). +Fires on the self-build path only, after the [§4](#4-modified-proposer-duty) block is signed and published. On the external-build path the builder signs and reveals its own `SignedExecutionPayloadEnvelope`, so this duty does not run. No pre-consensus phase. Envelope source by self-build variant: stateless self-build returns the envelope inline in `BlockContents`; stateful self-build requires a `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` to the same BN that served the [§4](#4-modified-proposer-duty) block, which returns the already-blinded `BlindedExecutionPayloadEnvelope` (cached server-side for the current slot only; keying by block root makes the lookup reorg-resistant). -The duty must target publishing the signed envelope before `get_payload_due_ms()` (the `PAYLOAD_DUE_BPS` cutoff, 50% of the slot since [consensus-specs #5414](https://github.com/ethereum/consensus-specs/pull/5414); §3), with margin for gossip to reach PTC beacon nodes before then. The §4 block is already out by the attestation deadline (§1, 25%), leaving roughly a quarter slot (~3s on mainnet) for the envelope QBFT round, post-consensus signing, and publication; #5414 halved this window from the earlier 75% cutoff. An envelope that misses the cutoff makes honest PTC validators vote `payload_present = False`, which can leave the next slot building on the empty parent (§3); this is the bounded missed-envelope degradation in Security Considerations. +The duty must target publishing the signed envelope before `get_payload_due_ms()` (the `PAYLOAD_DUE_BPS` cutoff, 50% of the slot since [consensus-specs #5414](https://github.com/ethereum/consensus-specs/pull/5414); [§3](#3-new-duty-payload-timeliness-committee-ptc-attestation)), with margin for gossip to reach PTC beacon nodes before then. The [§4](#4-modified-proposer-duty) block is already out by the attestation deadline ([§1](#1-slot-timing-changes), 25%), leaving roughly a quarter slot (~3s on mainnet) for the envelope QBFT round, post-consensus signing, and publication; #5414 halved this window from the earlier 75% cutoff. An envelope that misses the cutoff makes honest PTC validators vote `payload_present = False`, which can leave the next slot building on the empty parent ([§3](#3-new-duty-payload-timeliness-committee-ptc-attestation)); this is the bounded missed-envelope degradation in Security Considerations. #### Roles and constants @@ -292,7 +292,7 @@ type EnvelopeConsensusData struct { #### QBFT proposal -On the stateless path each operator blinds its local BN's inline envelope (`PayloadRoot = hash_tree_root(envelope.payload)`, other fields verbatim); on the stateful path it proposes the fetched `BlindedExecutionPayloadEnvelope` as-is. Either way the SSZ-encoded blinded form goes in `EnvelopeConsensusData.DataSSZ`. Only an operator whose BN built the §4-decided block holds (stateless) or can fetch (stateful) an envelope with a matching `BeaconBlockRoot`, 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 it locally; the decided value, and which operator can publish it, are independent of who leads the deciding round (publication is by content-match, see Publication). +On the stateless path each operator blinds its local BN's inline envelope (`PayloadRoot = hash_tree_root(envelope.payload)`, other fields verbatim); on the stateful path it proposes the fetched `BlindedExecutionPayloadEnvelope` as-is. Either way the SSZ-encoded blinded form goes in `EnvelopeConsensusData.DataSSZ`. Only an operator whose BN built the [§4](#4-modified-proposer-duty)-decided block holds (stateless) or can fetch (stateful) an envelope with a matching `BeaconBlockRoot`, 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 it locally; the decided value, and which operator can publish it, are independent of who leads the deciding round (publication is by content-match, see Publication). #### Value check @@ -301,7 +301,7 @@ A new `EnvelopeValueCheckF()` accepts the decided blinded envelope if all of: - SSZ decode of `DataSSZ` into `BlindedExecutionPayloadEnvelope` succeeds; - `EnvelopeConsensusData.Duty.{Slot, ValidatorIndex, PubKey}` match the duty the runner was started with; - `BuilderIndex == BUILDER_INDEX_SELF_BUILD` (external builders sign their own envelopes; this duty applies only to the self-build path); -- `BeaconBlockRoot` matches the block root decided by the §4 block QBFT for the slot. +- `BeaconBlockRoot` matches the block root decided by the [§4](#4-modified-proposer-duty) block QBFT for the slot. No envelope-content validation: `PayloadRoot` (and therefore every constituent field of the underlying `ExecutionPayload` such as `transactions`, `withdrawals`, `state_root`, `block_hash`) is leader-decided and trusted by the cluster. This matches the existing blinded-block trust model in `BNRoleProposer`, where operators do no field-level validation against their local BN view (see Security Considerations). @@ -311,7 +311,7 @@ Operators sign the decided `BlindedExecutionPayloadEnvelope`'s signing root unde #### Publication -Each operator's BN built a different envelope; only the operator whose blinded form matched the QBFT decision can publish. That operator reconstructs the signature and POSTs to `/eth/v1/beacon/execution_payload_envelopes` (merged via [beacon-APIs #580](https://github.com/ethereum/beacon-APIs/pull/580)); the body variant is selected by the required `Eth-Execution-Payload-Blinded` header. For stateless self-build the envelope arrived inline in `BlockContents`, so that operator holds the matching full payload, blobs, and KZG proofs and publishes `SignedExecutionPayloadEnvelopeContents` (header `false`). For stateful self-build the operator publishes `SignedBlindedExecutionPayloadEnvelope` (header `true`) to the same BN that produced the §4 block: the decided blinded envelope plus the reconstructed signature, from which that BN reconstructs the full envelope out of its production-time cache (any other BN lacks the cache and rejects the publish with a 400). Other operators complete without publishing. +Each operator's BN built a different envelope; only the operator whose blinded form matched the QBFT decision can publish. That operator reconstructs the signature and POSTs to `/eth/v1/beacon/execution_payload_envelopes` (merged via [beacon-APIs #580](https://github.com/ethereum/beacon-APIs/pull/580)); the body variant is selected by the required `Eth-Execution-Payload-Blinded` header. For stateless self-build the envelope arrived inline in `BlockContents`, so that operator holds the matching full payload, blobs, and KZG proofs and publishes `SignedExecutionPayloadEnvelopeContents` (header `false`). For stateful self-build the operator publishes `SignedBlindedExecutionPayloadEnvelope` (header `true`) to the same BN that produced the [§4](#4-modified-proposer-duty) block: the decided blinded envelope plus the reconstructed signature, from which that BN reconstructs the full envelope out of its production-time cache (any other BN lacks the cache and rejects the publish with a 400). Other operators complete without publishing. ### 7. SSV Message Validation @@ -329,21 +329,21 @@ Lateness TTL uses the existing deadline convention: a message is late once recei Derivations, per row: -- **PTC (7).** One partial-signature round, no pre/post split: `PTCAttesterPartialSig` is the only type, once per duty. The vote is consumed within its own slot and included at slot + 1 (§3), so the short non-committee TTL applies. The duty limit follows from `compute_ptc` drawing members exclusively from `get_beacon_committee(state, slot, i)`: a validator belongs to exactly one slot's beacon committees per epoch, so the honest bound is one PTC duty per validator per epoch, plus the +1 reorg margin the existing registration and aggregation limits already use. Multiple PTC seats within one slot are still one duty: the validator signs one `PayloadAttestationData`, and seat multiplicity lives in the aggregation bits. +- **PTC (7).** One partial-signature round, no pre/post split: `PTCAttesterPartialSig` is the only type, once per duty. The vote is consumed within its own slot and included at slot + 1 ([§3](#3-new-duty-payload-timeliness-committee-ptc-attestation)), so the short non-committee TTL applies. The duty limit follows from `compute_ptc` drawing members exclusively from `get_beacon_committee(state, slot, i)`: a validator belongs to exactly one slot's beacon committees per epoch, so the honest bound is one PTC duty per validator per epoch, plus the +1 reorg margin the existing registration and aggregation limits already use. Multiple PTC seats within one slot are still one duty: the validator signs one `PayloadAttestationData`, and seat multiplicity lives in the aggregation bits. - **ProposerPreferences (8).** - - *Earliness:* the message slot is `proposal_slot` (§5), up to the proposer lookahead in the future at emission, hence the allowance. + - *Earliness:* the message slot is `proposal_slot` ([§5](#5-proposer-preferences-duty)), up to the proposer lookahead in the future at emission, hence the allowance. - *Lateness:* the tight TTL drops stale preferences as replays. - *Slot-advance exempt:* a signer holds its whole lookahead at once, so a lower-slot message is a concurrent duty, not a stale one (lateness is its past bound). - - *Dedup:* the base one-per-(signer, slot) rule gains a bounded exception for distinct signing roots at one proposal slot; a repeat of a seen root is a duplicate (same-peer REJECT, relayed IGNORE, the existing two-tier handling), and a distinct root past the cap is IGNORE'd (rate-limiting, not a provable violation). Those extra roots come from preference-input changes between emissions, notably a `dependent_root` shift under reorg (§5); the cap is policy headroom, and the epoch-boundary batch never touches it since each proposal slot is its own key. + - *Dedup:* the base one-per-(signer, slot) rule gains a bounded exception for distinct signing roots at one proposal slot; a repeat of a seen root is a duplicate (same-peer REJECT, relayed IGNORE, the existing two-tier handling), and a distinct root past the cap is IGNORE'd (rate-limiting, not a provable violation). Those extra roots come from preference-input changes between emissions, notably a `dependent_root` shift under reorg ([§5](#5-proposer-preferences-duty)); the cap is policy headroom, and the epoch-boundary batch never touches it since each proposal slot is its own key. - *Duty limit:* at most one proposal per slot. -- **EnvelopeProposer (9).** Standard QBFT validation applies (leader round-robin, justification rules, one message per type per round); the round cut-off is the proposer-class value, and the §6 timing makes anything higher moot: the duty's window ends at the `PAYLOAD_DUE_BPS` cutoff (~a quarter slot, §6), which fits the first round plus at most one round-change. Post-consensus is the only partial-signature phase (no pre-consensus analog to RANDAO). The duty exists only for the cluster's own proposal slots, so the proposer-assignment check applies, and the duty limit mirrors the preferences bound. +- **EnvelopeProposer (9).** Standard QBFT validation applies (leader round-robin, justification rules, one message per type per round); the round cut-off is the proposer-class value, and the [§6](#6-new-duty-envelope-signing-self-build-path) timing makes anything higher moot: the duty's window ends at the `PAYLOAD_DUE_BPS` cutoff (~a quarter slot, [§6](#6-new-duty-envelope-signing-self-build-path)), which fits the first round plus at most one round-change. Post-consensus is the only partial-signature phase (no pre-consensus analog to RANDAO). The duty exists only for the cluster's own proposal slots, so the proposer-assignment check applies, and the duty limit mirrors the preferences bound. Epoch-aware fork gates, a new rule class. Both are REJECT because they are slot-scoped rather than wall-clock-scoped: the gated condition is carried in the message itself, so no honest ordering race can produce it. | Rule | Classification | Explanation | |---|---|---| -| Role in {7, 8, 9} and `epoch(msg.slot) < GLOAS_FORK_EPOCH` | REJECT | These duties do not exist before the fork. §5's required pre-fork emission carries post-fork `proposal_slot` values by construction, so it passes this gate with no special case. | -| `RoleValidatorRegistration` (4) and `epoch(msg.slot) >= GLOAS_FORK_EPOCH` | REJECT | §5 deprecates the duty at the fork; honest registrations are always stamped with pre-fork slots. Pre-Gloas slots remain valid and unchanged. | +| Role in {7, 8, 9} and `epoch(msg.slot) < GLOAS_FORK_EPOCH` | REJECT | These duties do not exist before the fork. [§5](#5-proposer-preferences-duty)'s required pre-fork emission carries post-fork `proposal_slot` values by construction, so it passes this gate with no special case. | +| `RoleValidatorRegistration` (4) and `epoch(msg.slot) >= GLOAS_FORK_EPOCH` | REJECT | [§5](#5-proposer-preferences-duty) deprecates the duty at the fork; honest registrations are always stamped with pre-fork slots. Pre-Gloas slots remain valid and unchanged. | ## Security Considerations @@ -353,34 +353,34 @@ Under Gloas, `AttestationData.Index` is part of the attestation data root and th ### Gloas `AttestationData.Index` is trusted from the QBFT leader -The Gloas `AttestationData.Index` value check (§2) does not require the QBFT-decided value to match each operator's local BN view. Requiring local agreement would fail QBFT rounds whenever operators observe fork-choice state at slightly different times around the deadline, a normal gossip-lag scenario. Accepted tradeoff: a malicious QBFT leader can push a value contrary to the cluster's majority BN observation. This matches existing ssv-spec treatment of `BeaconVote.BlockRoot`, which is trusted from the leader because BNs legitimately diverge on fork-choice head. +The Gloas `AttestationData.Index` value check ([§2](#2-modified-attestation-duty)) does not require the QBFT-decided value to match each operator's local BN view. Requiring local agreement would fail QBFT rounds whenever operators observe fork-choice state at slightly different times around the deadline, a normal gossip-lag scenario. Accepted tradeoff: a malicious QBFT leader can push a value contrary to the cluster's majority BN observation. This matches existing ssv-spec treatment of `BeaconVote.BlockRoot`, which is trusted from the leader because BNs legitimately diverge on fork-choice head. ### PTC reconstruction is honest-convergence, not consensus -PTC runs no QBFT (§3): each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% broadcast mark, and a per-validator signature reconstructs only when a threshold of operators converged on byte-identical data. There is no leader to push a value contrary to the cluster's observation, and an operator can only ever vote its own honest observation. The cost is liveness rather than safety: when operators' beacon nodes split across observations (envelope-arrival jitter at the 50% `PAYLOAD_DUE_BPS` boundary, head or blob-availability jitter at evaluation time around `MAXIMUM_GOSSIP_CLOCK_DISPARITY`, or operators on diverged forks), no observation may reach threshold and the cluster's vote for that validator is a silent miss. That miss is non-slashable and its only effect is the foregone contribution to the `PTC_SIZE/2` fork-choice tally, bounded by SSV's PTC seat share. The off-slot-root case that Gloas gossip guards with an IGNORE-level `block.slot == data.slot` check ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/p2p-interface.md#L400-L401)) cannot arise here: each operator signs the block it observed for `duty.slot`. +PTC runs no QBFT ([§3](#3-new-duty-payload-timeliness-committee-ptc-attestation)): each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% broadcast mark, and a per-validator signature reconstructs only when a threshold of operators converged on byte-identical data. There is no leader to push a value contrary to the cluster's observation, and an operator can only ever vote its own honest observation. The cost is liveness rather than safety: when operators' beacon nodes split across observations (envelope-arrival jitter at the 50% `PAYLOAD_DUE_BPS` boundary, head or blob-availability jitter at evaluation time around `MAXIMUM_GOSSIP_CLOCK_DISPARITY`, or operators on diverged forks), no observation may reach threshold and the cluster's vote for that validator is a silent miss. That miss is non-slashable and its only effect is the foregone contribution to the `PTC_SIZE/2` fork-choice tally, bounded by SSV's PTC seat share. The off-slot-root case that Gloas gossip guards with an IGNORE-level `block.slot == data.slot` check ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/p2p-interface.md#L400-L401)) cannot arise here: each operator signs the block it observed for `duty.slot`. ### Config divergence silently disables trustless external builder bids -`ProposerPreferences` reconstruction requires a quorum of operators to derive the same signing root, which depends on `target_gas_limit` (per-operator config) and `dependent_root` (per-operator BN observation). Divergence on either splits signing roots; if no root reaches threshold, there is no reconstructed signature, no gossip publication, and therefore no matching preference on the `execution_payload_bid` topic; bids for the slot are IGNORE'd by gossip (§5), leaving the BN with no trustless external builder options to return. Same reconstruction failure shape as `ValidatorRegistration` today. +`ProposerPreferences` reconstruction requires a quorum of operators to derive the same signing root, which depends on `target_gas_limit` (per-operator config) and `dependent_root` (per-operator BN observation). Divergence on either splits signing roots; if no root reaches threshold, there is no reconstructed signature, no gossip publication, and therefore no matching preference on the `execution_payload_bid` topic; bids for the slot are IGNORE'd by gossip ([§5](#5-proposer-preferences-duty)), leaving the BN with no trustless external builder options to return. Same reconstruction failure shape as `ValidatorRegistration` today. ### Too-early `SignedProposerPreferences` publication pins the wrong preference -Because the `proposer_preferences` gossip topic accepts only the first valid message per `(dependent_root, proposal_slot, validator_index)` tuple (§5), reconstructing and publishing a preference before all tuple inputs are final can durably pin a wrong-input preference: a later corrected message for the same tuple is dropped by gossip rather than treated as a replacement. Builders keep using the stale preference, and bids matching the corrected values fail the §5 handshake. Operators must therefore hold publication until `dependent_root`, `fee_recipient`, and `target_gas_limit` are all final for the tuple, and re-emit only when the tuple itself changes (notably when `dependent_root` shifts due to reorg, or `proposer_lookahead` reassigns the validator to a different slot). Distinct from the config-divergence entry above: there, divergence prevents publication; here, premature publication pins the wrong preference more durably than no publication would. +Because the `proposer_preferences` gossip topic accepts only the first valid message per `(dependent_root, proposal_slot, validator_index)` tuple ([§5](#5-proposer-preferences-duty)), reconstructing and publishing a preference before all tuple inputs are final can durably pin a wrong-input preference: a later corrected message for the same tuple is dropped by gossip rather than treated as a replacement. Builders keep using the stale preference, and bids matching the corrected values fail the [§5](#5-proposer-preferences-duty) handshake. Operators must therefore hold publication until `dependent_root`, `fee_recipient`, and `target_gas_limit` are all final for the tuple, and re-emit only when the tuple itself changes (notably when `dependent_root` shifts due to reorg, or `proposer_lookahead` reassigns the validator to a different slot). Distinct from the config-divergence entry above: there, divergence prevents publication; here, premature publication pins the wrong preference more durably than no publication would. ### Late `dependent_root` change near the proposal slot may leave the slot with no matching bid -Late `dependent_root` change tightens the re-emission window. Under non-finality, a deep reorg affecting the end-of-p-2 dependent block forces the proposer to re-emit `SignedProposerPreferences` with the new root; if the re-emission + builder-bid gossip round-trip cannot complete before the proposal deadline, the slot falls through to §6 self-build with a compressed envelope-signing window. +Late `dependent_root` change tightens the re-emission window. Under non-finality, a deep reorg affecting the end-of-p-2 dependent block forces the proposer to re-emit `SignedProposerPreferences` with the new root; if the re-emission + builder-bid gossip round-trip cannot complete before the proposal deadline, the slot falls through to [§6](#6-new-duty-envelope-signing-self-build-path) self-build with a compressed envelope-signing window. ### Matching-envelope operator failure or late publication misses the slot's envelope -The slot's envelope is missed if the operator whose envelope blinds to the §6-decided value (the only one holding the matching full bytes; see §6 Publication) fails before publishing it, or if the envelope-QBFT round completes after `get_payload_due_ms()` so the signed envelope reaches PTC validators too late to observe before the cutoff. In either case PTC records `payload_present = FALSE` (§3); proposer forfeits the payload reward. No worse than the no-envelope-signing baseline for the self-build path. +The slot's envelope is missed if the operator whose envelope blinds to the [§6](#6-new-duty-envelope-signing-self-build-path)-decided value (the only one holding the matching full bytes; see [§6](#6-new-duty-envelope-signing-self-build-path) Publication) fails before publishing it, or if the envelope-QBFT round completes after `get_payload_due_ms()` so the signed envelope reaches PTC validators too late to observe before the cutoff. In either case PTC records `payload_present = FALSE` ([§3](#3-new-duty-payload-timeliness-committee-ptc-attestation)); proposer forfeits the payload reward. No worse than the no-envelope-signing baseline for the self-build path. ## Open Questions / Upstream Watchlist This section is intentionally limited to upstream items that could still change the normative SSV behavior described above. If any of these settle differently, this SIP should be updated. -- beacon-APIs release status: `produceBlockV4` (`GET /eth/v4/validator/blocks/{slot}`), the §6 envelope publication and fetch endpoints, and the §5 `SignedProposerPreferences` submission endpoint are merged to `beacon-APIs/master` ([#580](https://github.com/ethereum/beacon-APIs/pull/580), [#608](https://github.com/ethereum/beacon-APIs/pull/608)) but unreleased: the latest release tag (`v5.0.0-alpha.2`) predates them, and no client implementations are marked in the compatibility matrix yet. Watch for pre-release changes to the response variant discriminators, request/response bodies, and SSZ encodings. -- EIP-7688 progressive containers: [EIP-7688](https://eips.ethereum.org/EIPS/eip-7688) (forward-compatible consensus data structures) reshapes core Gloas containers into `ProgressiveContainer`s, changing how `hash_tree_root` is computed for the §4 block signing root and the §6 envelope. It is only Considered for Inclusion (CFI), not Scheduled for Inclusion, for Glamsterdam ([EIP-7773](https://eips.ethereum.org/EIPS/eip-7773); targeted for glamsterdam-devnet-7, inclusion decision pending). The pinned snapshot `801a38e15` has already merged it ([consensus-specs #4630](https://github.com/ethereum/consensus-specs/pull/4630)), but this SIP describes positional merkleization until 7688 is SFI'd: at that point §6's blinded-envelope note adopts `mix_in_active_fields(merkleize_progressive(...))` and §4 gains a matching block-root note. +- beacon-APIs release status: `produceBlockV4` (`GET /eth/v4/validator/blocks/{slot}`), the [§6](#6-new-duty-envelope-signing-self-build-path) envelope publication and fetch endpoints, and the [§5](#5-proposer-preferences-duty) `SignedProposerPreferences` submission endpoint are merged to `beacon-APIs/master` ([#580](https://github.com/ethereum/beacon-APIs/pull/580), [#608](https://github.com/ethereum/beacon-APIs/pull/608)) but unreleased: the latest release tag (`v5.0.0-alpha.2`) predates them, and no client implementations are marked in the compatibility matrix yet. Watch for pre-release changes to the response variant discriminators, request/response bodies, and SSZ encodings. +- EIP-7688 progressive containers: [EIP-7688](https://eips.ethereum.org/EIPS/eip-7688) (forward-compatible consensus data structures) reshapes core Gloas containers into `ProgressiveContainer`s, changing how `hash_tree_root` is computed for the [§4](#4-modified-proposer-duty) block signing root and the [§6](#6-new-duty-envelope-signing-self-build-path) envelope. It is only Considered for Inclusion (CFI), not Scheduled for Inclusion, for Glamsterdam ([EIP-7773](https://eips.ethereum.org/EIPS/eip-7773); targeted for glamsterdam-devnet-7, inclusion decision pending). The pinned snapshot `801a38e15` has already merged it ([consensus-specs #4630](https://github.com/ethereum/consensus-specs/pull/4630)), but this SIP describes positional merkleization until 7688 is SFI'd: at that point [§6](#6-new-duty-envelope-signing-self-build-path)'s blinded-envelope note adopts `mix_in_active_fields(merkleize_progressive(...))` and [§4](#4-modified-proposer-duty) gains a matching block-root note. - Builder-API authenticated connections (`SignedRequestAuthV1`): builder-specs [#138](https://github.com/ethereum/builder-specs/pull/138) (merged 2026-06-11, not yet in a release) defines an optional validator-key BLS signature over `{builder URL, slot}` under the fork-agnostic domain `DOMAIN_REQUEST_AUTH` (`0x0B000001`), used to authenticate direct bid requests (`getExecutionPayloadBid`) and required for per-builder execution payments (`submitBuilderPreferences`). A bid pays the proposer via `bid.value`, deducted on-chain from the builder's staked collateral, and optionally via `bid.execution_payment`, a trusted execution-layer payment that the proposer must opt into per builder by submitting a `max_execution_payment` cap through `submitBuilderPreferences`. Deferring the duty costs no liveness in the interim: a cluster that has not yet submitted builder preferences has a zero cap, so builders MUST NOT attach execution payments and every bid it can receive pays from staked collateral only (gossiped bids MUST carry `execution_payment = 0` regardless of channel). Distributed signing of `RequestAuthV1` follows the `ValidatorRegistration` shape (validator-scoped, one partial-signature round, no QBFT) and will be specified once the upstream surface settles, meaning a standardized hop between the key holder and block assembly (a beacon-APIs successor to the now-deprecated `register_validator` forwarding, or a sidecar specification) and a builder-specs release that includes the Gloas builder API. ## Acknowledgements From 0bbfe6e8e3b75e8ce104328481da50749ef03d8e Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 8 Jul 2026 13:25:16 -0700 Subject: [PATCH 56/62] chore: drop "external" from trustless-builder-bids wording (reads like old PBS) --- sips/epbs_support.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 92a056e..f2dd9a9 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -20,7 +20,7 @@ Gloas changes validator duties in ways that break a few current SSV assumptions: - attestation `index` is no longer safely reconstructible from local validator duty data - the new PTC duty has a late in-slot deadline -- SSV validators must broadcast `SignedProposerPreferences` or they cannot accept external-builder bids for their slots +- SSV validators must broadcast `SignedProposerPreferences` or they cannot accept builder bids for their slots ## Rationale @@ -200,7 +200,7 @@ Relevant consensus-spec references: Under Gloas, each proposer broadcasts `SignedProposerPreferences` on the `proposer_preferences` p2p topic for future proposal slots within the proposer lookahead (the current epoch up to `MIN_SEED_LOOKAHEAD` epochs ahead). The signed `ProposerPreferences` carries `dependent_root`, `proposal_slot`, `validator_index`, `fee_recipient`, and `target_gas_limit`. `dependent_root` pins the proposer-lookahead epoch's seed via `get_shuffling_dependent_root(store, root, epoch)`; operators populate it from the `dependent_root` returned by [`GET /eth/v2/validator/duties/proposer/{epoch}`](https://github.com/ethereum/beacon-APIs/blob/v5.0.0-alpha.2/apis/validator/duties/proposer.v2.yaml) for the proposal-slot's epoch. Builders listen to this topic and use a proposer's preferences to construct `execution_payload_bid` objects for that proposer's slots. This replaces the pre-Gloas out-of-band relay-registration mechanism, which is gone along with blinded blocks; the `ValidatorRegistration` duty is deprecated accordingly (end of this section). -Gossip enforces the handshake at the `execution_payload_bid` topic: each bid requires a matching `SignedProposerPreferences` for its `(proposal_slot, dependent_root)` (otherwise IGNORE'd, not forwarded). The bid `fee_recipient` must match the preference, and the bid `gas_limit` must be EIP-1559-compatible with the proposer's `target_gas_limit` via `is_gas_limit_target_compatible`; both mismatches are IGNORE'd rather than REJECT'd (the fee-recipient check was downgraded from REJECT by [consensus-specs #5429](https://github.com/ethereum/consensus-specs/pull/5429): preferences are not equivocation-checked, so a mismatch is not provably the bidder's fault). Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless external builder options to return. +Gossip enforces the handshake at the `execution_payload_bid` topic: each bid requires a matching `SignedProposerPreferences` for its `(proposal_slot, dependent_root)` (otherwise IGNORE'd, not forwarded). The bid `fee_recipient` must match the preference, and the bid `gas_limit` must be EIP-1559-compatible with the proposer's `target_gas_limit` via `is_gas_limit_target_compatible`; both mismatches are IGNORE'd rather than REJECT'd (the fee-recipient check was downgraded from REJECT by [consensus-specs #5429](https://github.com/ethereum/consensus-specs/pull/5429): preferences are not equivocation-checked, so a mismatch is not provably the bidder's fault). Without this duty broadcast, bids for the validator's slots don't propagate across the network, leaving the BN with no trustless builder options to return. 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; the domain epoch is `compute_epoch_at_slot(proposal_slot)` per the spec's `get_signed_proposer_preferences`, so signatures for the pre-fork emission required below are computed under the Gloas fork domain even while the chain is still on the prior fork. `fee_recipient` is configured cluster-side (cluster-consistent in practice); `target_gas_limit` is operator-configured, with a client default when unset; operators in a cluster must agree byte-for-byte at signing time; `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, and operators validate incoming partial signatures against their own derived root (the existing `ValidatorRegistration` expected-root validation); divergence splits signing roots, and reconstruction succeeds only when one root reaches threshold. `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). The reconstructed `SignedProposerPreferences` is submitted via `POST /eth/v1/validator/proposer_preferences` ([beacon-APIs #608](https://github.com/ethereum/beacon-APIs/pull/608)), which stores it and broadcasts it to the `proposer_preferences` topic. @@ -359,9 +359,9 @@ The Gloas `AttestationData.Index` value check ([§2](#2-modified-attestation-dut PTC runs no QBFT ([§3](#3-new-duty-payload-timeliness-committee-ptc-attestation)): each operator signs the `PayloadAttestationData` its own beacon node observed at the 75% broadcast mark, and a per-validator signature reconstructs only when a threshold of operators converged on byte-identical data. There is no leader to push a value contrary to the cluster's observation, and an operator can only ever vote its own honest observation. The cost is liveness rather than safety: when operators' beacon nodes split across observations (envelope-arrival jitter at the 50% `PAYLOAD_DUE_BPS` boundary, head or blob-availability jitter at evaluation time around `MAXIMUM_GOSSIP_CLOCK_DISPARITY`, or operators on diverged forks), no observation may reach threshold and the cluster's vote for that validator is a silent miss. That miss is non-slashable and its only effect is the foregone contribution to the `PTC_SIZE/2` fork-choice tally, bounded by SSV's PTC seat share. The off-slot-root case that Gloas gossip guards with an IGNORE-level `block.slot == data.slot` check ([`p2p-interface.md`](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/p2p-interface.md#L400-L401)) cannot arise here: each operator signs the block it observed for `duty.slot`. -### Config divergence silently disables trustless external builder bids +### Config divergence silently disables trustless builder bids -`ProposerPreferences` reconstruction requires a quorum of operators to derive the same signing root, which depends on `target_gas_limit` (per-operator config) and `dependent_root` (per-operator BN observation). Divergence on either splits signing roots; if no root reaches threshold, there is no reconstructed signature, no gossip publication, and therefore no matching preference on the `execution_payload_bid` topic; bids for the slot are IGNORE'd by gossip ([§5](#5-proposer-preferences-duty)), leaving the BN with no trustless external builder options to return. Same reconstruction failure shape as `ValidatorRegistration` today. +`ProposerPreferences` reconstruction requires a quorum of operators to derive the same signing root, which depends on `target_gas_limit` (per-operator config) and `dependent_root` (per-operator BN observation). Divergence on either splits signing roots; if no root reaches threshold, there is no reconstructed signature, no gossip publication, and therefore no matching preference on the `execution_payload_bid` topic; bids for the slot are IGNORE'd by gossip ([§5](#5-proposer-preferences-duty)), leaving the BN with no trustless builder options to return. Same reconstruction failure shape as `ValidatorRegistration` today. ### Too-early `SignedProposerPreferences` publication pins the wrong preference From 938648d8b9613d8f29c77d10c07e1ff8d9b70cbc Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 8 Jul 2026 13:29:07 -0700 Subject: [PATCH 57/62] chore: clarify dependent_root reorg re-emission forms a new gossip tuple --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index f2dd9a9..20396d0 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -369,7 +369,7 @@ Because the `proposer_preferences` gossip topic accepts only the first valid mes ### Late `dependent_root` change near the proposal slot may leave the slot with no matching bid -Late `dependent_root` change tightens the re-emission window. Under non-finality, a deep reorg affecting the end-of-p-2 dependent block forces the proposer to re-emit `SignedProposerPreferences` with the new root; if the re-emission + builder-bid gossip round-trip cannot complete before the proposal deadline, the slot falls through to [§6](#6-new-duty-envelope-signing-self-build-path) self-build with a compressed envelope-signing window. +A late `dependent_root` change tightens the re-emission window but does not block it: the changed root forms a new `(dependent_root, proposal_slot, validator_index)` tuple, which gossip accepts (first-message pinning binds a fixed tuple), so the risk here is the remaining time budget, not gossip rejection. Under non-finality, a deep reorg affecting the end-of-p-2 dependent block forces the proposer to re-emit `SignedProposerPreferences` with the new root; if the re-emission + builder-bid gossip round-trip cannot complete before the proposal deadline, the slot falls through to [§6](#6-new-duty-envelope-signing-self-build-path) self-build with a compressed envelope-signing window. ### Matching-envelope operator failure or late publication misses the slot's envelope From 0a8de9e276894706b90e616e93769a481debf340 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 15 Jul 2026 14:28:44 -0700 Subject: [PATCH 58/62] =?UTF-8?q?feat:=20tolerate=20stale=20proposer-duty?= =?UTF-8?q?=20view=20across=20validator-set=20changes=20(=C2=A75/=C2=A77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §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. --- sips/epbs_support.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 20396d0..6ca0578 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -204,7 +204,7 @@ Gossip enforces the handshake at the `execution_payload_bid` topic: each bid req 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; the domain epoch is `compute_epoch_at_slot(proposal_slot)` per the spec's `get_signed_proposer_preferences`, so signatures for the pre-fork emission required below are computed under the Gloas fork domain even while the chain is still on the prior fork. `fee_recipient` is configured cluster-side (cluster-consistent in practice); `target_gas_limit` is operator-configured, with a client default when unset; operators in a cluster must agree byte-for-byte at signing time; `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, and operators validate incoming partial signatures against their own derived root (the existing `ValidatorRegistration` expected-root validation); divergence splits signing roots, and reconstruction succeeds only when one root reaches threshold. `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). The reconstructed `SignedProposerPreferences` is submitted via `POST /eth/v1/validator/proposer_preferences` ([beacon-APIs #608](https://github.com/ethereum/beacon-APIs/pull/608)), which stores it and broadcasts it to the `proposer_preferences` topic. -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. Emission during the first Gloas epoch itself would also be gossip-valid for slots later in that epoch, but slots early in the epoch leave effectively no post-fork emission time, and builders need a validator's preference before they can construct and gossip bids for its slot; pre-fork emission covers both, 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. +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. Emission during the first Gloas epoch itself would also be gossip-valid for slots later in that epoch, but slots early in the epoch leave effectively no post-fork emission time, and builders need a validator's preference before they can construct and gossip bids for its slot; pre-fork emission covers both, 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. After a validator-set change (local validators registering, shifting indices), emitters SHOULD additionally delay (re-)emission by a couple of slots so committee peers converge on the new set before the one-shot partials go out; [§7](#7-ssv-message-validation) specifies the matching receive-side tolerance that keeps honest partials valid during the convergence window. The duty's slot, carried in the `PartialSignatureMessages` envelope, is `proposal_slot` itself: one runner per proposal slot, matching ssv-spec's requirement that a partial-signature message's slot equal its duty's slot (`validatePartialSigMsgForSlot`). At emission time that slot is up to the proposer lookahead in the future, and a re-emission for the same slot carries a new signing root; both effects need dedicated message-validation rules, specified in [§7](#7-ssv-message-validation). @@ -327,6 +327,8 @@ All three new roles are validator-scoped, like `RoleValidatorRegistration` and ` 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. + Derivations, per row: - **PTC (7).** One partial-signature round, no pre/post split: `PTCAttesterPartialSig` is the only type, once per duty. The vote is consumed within its own slot and included at slot + 1 ([§3](#3-new-duty-payload-timeliness-committee-ptc-attestation)), so the short non-committee TTL applies. The duty limit follows from `compute_ptc` drawing members exclusively from `get_beacon_committee(state, slot, i)`: a validator belongs to exactly one slot's beacon committees per epoch, so the honest bound is one PTC duty per validator per epoch, plus the +1 reorg margin the existing registration and aggregation limits already use. Multiple PTC seats within one slot are still one duty: the validator signs one `PayloadAttestationData`, and seat multiplicity lives in the aggregation bits. From d5197bc06c7c4b4a8728f0869a82c9c852579f77 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Fri, 17 Jul 2026 10:17:43 -0700 Subject: [PATCH 59/62] align envelope duty with beacon API changes --- sips/epbs_support.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 6ca0578..b4dbc33 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -175,7 +175,7 @@ Relevant consensus-spec references: - [Validator block and sidecar proposal flow](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/validator.md#block-and-sidecar-proposal) -Under Gloas, `produceBlockV4` (`GET /eth/v4/validator/blocks/{slot}`, merged via [beacon-APIs #580](https://github.com/ethereum/beacon-APIs/pull/580)) 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. The variant is signaled by the required `execution_payload_included` response field and `Eth-Execution-Payload-Included` header, and driven by the `include_payload` query parameter (`true` requests the stateless `BlockContents` form). +Under Gloas, `produceBlockV4` (`GET /eth/v4/validator/blocks/{slot}`, merged via [beacon-APIs #580](https://github.com/ethereum/beacon-APIs/pull/580) and updated by [#624](https://github.com/ethereum/beacon-APIs/pull/624)) replaces the pre-Gloas proposer flow; blinded blocks are removed. The beacon node returns `Gloas.BeaconBlock` when the required `include_payload` query parameter is `false` (and on any external-build response), or `Gloas.BlockContents` when it is `true` on a self-build response. The variant is signaled by the required `execution_payload_included` response field and `Eth-Execution-Payload-Included` header. *Note (non-normative).* When to issue the `produceBlockV4` request is the residual proposer timing discretion: a later request lets the beacon node observe more bids and can return a higher-value block. The window is tighter for an SSV cluster than for a solo proposer, since the returned block must still clear QBFT, post-consensus signing, and propagation before the earlier attestation deadline ([§1](#1-slot-timing-changes)), so the request must leave room for them. Operator policy, not a protocol requirement. @@ -247,13 +247,13 @@ Relevant consensus-spec references: - [`ExecutionPayloadEnvelope` container](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/beacon-chain.md#executionpayloadenvelope) - [`execution_payload` gossip topic (carries `SignedExecutionPayloadEnvelope`)](https://github.com/ethereum/consensus-specs/blob/801a38e1524a4945e30105a281ae693e3355d5ad/specs/gloas/p2p-interface.md#new-execution_payload) -- [`POST /eth/v1/beacon/execution_payload_envelopes` endpoint](https://github.com/ethereum/beacon-APIs/pull/580) +- [`POST /eth/v1/beacon/execution_payload_envelopes` endpoint](https://github.com/ethereum/beacon-APIs/pull/624) On the self-build path (`bid.builder_index == BUILDER_INDEX_SELF_BUILD` per [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732)), the proposer signs `SignedExecutionPayloadEnvelope` after block publication. The SSV cluster runs a second QBFT round to produce this signature. #### Blinded envelope type -To bound QBFT message size, the cluster runs QBFT over a blinded form that substitutes `payload` with `payload_root: Root = hash_tree_root(payload)`, the same shape as beacon-APIs' `Gloas.BlindedExecutionPayloadEnvelope` ([#580](https://github.com/ethereum/beacon-APIs/pull/580)). In SSZ merkleization every field subtree commits to `hash_tree_root(field)`, so substituting `payload` with its root in the same field position preserves the container root: `hash_tree_root(BlindedExecutionPayloadEnvelope) == hash_tree_root(ExecutionPayloadEnvelope)`, and a BLS sig over the blinded signing root is valid for the full envelope. (If EIP-7688 progressive containers land in Gloas, this note and the blinded struct below must merkleize with the progressive shape; see the watchlist.) +To bound QBFT message size, the cluster runs QBFT over a blinded form that substitutes `payload` with `payload_root: Root = hash_tree_root(payload)`. In SSZ merkleization every field subtree commits to `hash_tree_root(field)`, so substituting `payload` with its root in the same field position preserves the container root: `hash_tree_root(BlindedExecutionPayloadEnvelope) == hash_tree_root(ExecutionPayloadEnvelope)`, and a BLS sig over the blinded signing root is valid for the full envelope. (If EIP-7688 progressive containers land in Gloas, this note and the blinded struct below must merkleize with the progressive shape; see the watchlist.) ```go type BlindedExecutionPayloadEnvelope struct { @@ -267,7 +267,9 @@ type BlindedExecutionPayloadEnvelope struct { #### Trigger and envelope source -Fires on the self-build path only, after the [§4](#4-modified-proposer-duty) block is signed and published. On the external-build path the builder signs and reveals its own `SignedExecutionPayloadEnvelope`, so this duty does not run. No pre-consensus phase. Envelope source by self-build variant: stateless self-build returns the envelope inline in `BlockContents`; stateful self-build requires a `GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root}` to the same BN that served the [§4](#4-modified-proposer-duty) block, which returns the already-blinded `BlindedExecutionPayloadEnvelope` (cached server-side for the current slot only; keying by block root makes the lookup reorg-resistant). +Fires after the [§4](#4-modified-proposer-duty) self-build block is signed and published. No pre-consensus phase. + +Each operator blinds its local full `ExecutionPayloadEnvelope` for the SSV consensus value. Blobs and KZG proofs are never part of the SSV QBFT value. The duty must target publishing the signed envelope before `get_payload_due_ms()` (the `PAYLOAD_DUE_BPS` cutoff, 50% of the slot since [consensus-specs #5414](https://github.com/ethereum/consensus-specs/pull/5414); [§3](#3-new-duty-payload-timeliness-committee-ptc-attestation)), with margin for gossip to reach PTC beacon nodes before then. The [§4](#4-modified-proposer-duty) block is already out by the attestation deadline ([§1](#1-slot-timing-changes), 25%), leaving roughly a quarter slot (~3s on mainnet) for the envelope QBFT round, post-consensus signing, and publication; #5414 halved this window from the earlier 75% cutoff. An envelope that misses the cutoff makes honest PTC validators vote `payload_present = False`, which can leave the next slot building on the empty parent ([§3](#3-new-duty-payload-timeliness-committee-ptc-attestation)); this is the bounded missed-envelope degradation in Security Considerations. @@ -292,7 +294,11 @@ type EnvelopeConsensusData struct { #### QBFT proposal -On the stateless path each operator blinds its local BN's inline envelope (`PayloadRoot = hash_tree_root(envelope.payload)`, other fields verbatim); on the stateful path it proposes the fetched `BlindedExecutionPayloadEnvelope` as-is. Either way the SSZ-encoded blinded form goes in `EnvelopeConsensusData.DataSSZ`. Only an operator whose BN built the [§4](#4-modified-proposer-duty)-decided block holds (stateless) or can fetch (stateful) an envelope with a matching `BeaconBlockRoot`, 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 it locally; the decided value, and which operator can publish it, are independent of who leads the deciding round (publication is by content-match, see Publication). +Each operator places the SSZ-encoded blinded form of its local full envelope in `EnvelopeConsensusData.DataSSZ`. + +An operator's local candidate may commit to a block other than the [§4](#4-modified-proposer-duty)-decided block. That candidate is still sufficient to initialize and participate in the envelope QBFT instance, but it is not an admissible proposal: if that operator becomes leader and proposes it, peers reject it through `EnvelopeValueCheckF()` and the duty round-changes. `EnvelopeValueCheckF()` applies to proposed values, not to the local value used to initialize participation. Only an operator whose local envelope commits to the decided block can originate an admissible value without a prior justification. + +Under round-change, a later leader can re-propose a justified matching value without holding the corresponding full envelope. The decided value and the operator capable of publishing it are therefore independent of who leads the deciding round. #### Value check @@ -311,7 +317,9 @@ Operators sign the decided `BlindedExecutionPayloadEnvelope`'s signing root unde #### Publication -Each operator's BN built a different envelope; only the operator whose blinded form matched the QBFT decision can publish. That operator reconstructs the signature and POSTs to `/eth/v1/beacon/execution_payload_envelopes` (merged via [beacon-APIs #580](https://github.com/ethereum/beacon-APIs/pull/580)); the body variant is selected by the required `Eth-Execution-Payload-Blinded` header. For stateless self-build the envelope arrived inline in `BlockContents`, so that operator holds the matching full payload, blobs, and KZG proofs and publishes `SignedExecutionPayloadEnvelopeContents` (header `false`). For stateful self-build the operator publishes `SignedBlindedExecutionPayloadEnvelope` (header `true`) to the same BN that produced the [§4](#4-modified-proposer-duty) block: the decided blinded envelope plus the reconstructed signature, from which that BN reconstructs the full envelope out of its production-time cache (any other BN lacks the cache and rejects the publish with a 400). Other operators complete without publishing. +All operators reconstruct the signature over the decided blinded envelope. Only the operator whose local full envelope blinds to that decided value can form a valid publish body; every other envelope has a different signing root and fails signature validation. + +Publication follows [beacon-APIs #624](https://github.com/ethereum/beacon-APIs/pull/624): `Eth-Blob-Data-Included: false` carries `SignedExecutionPayloadEnvelope`, while `true` carries `SignedExecutionPayloadEnvelopeContents`. No public `SignedBlindedExecutionPayloadEnvelope` is used. ### 7. SSV Message Validation @@ -375,15 +383,14 @@ A late `dependent_root` change tightens the re-emission window but does not bloc ### Matching-envelope operator failure or late publication misses the slot's envelope -The slot's envelope is missed if the operator whose envelope blinds to the [§6](#6-new-duty-envelope-signing-self-build-path)-decided value (the only one holding the matching full bytes; see [§6](#6-new-duty-envelope-signing-self-build-path) Publication) fails before publishing it, or if the envelope-QBFT round completes after `get_payload_due_ms()` so the signed envelope reaches PTC validators too late to observe before the cutoff. In either case PTC records `payload_present = FALSE` ([§3](#3-new-duty-payload-timeliness-committee-ptc-attestation)); proposer forfeits the payload reward. No worse than the no-envelope-signing baseline for the self-build path. +The slot's envelope is missed if the operator whose local full envelope blinds to the [§6](#6-new-duty-envelope-signing-self-build-path)-decided value fails to publish it, or if envelope QBFT completes after `get_payload_due_ms()` so the signed envelope reaches PTC validators too late to observe before the cutoff. In either case PTC records `payload_present = FALSE` ([§3](#3-new-duty-payload-timeliness-committee-ptc-attestation)); the proposer forfeits the payload reward. No worse than the no-envelope-signing baseline for the self-build path. ## Open Questions / Upstream Watchlist This section is intentionally limited to upstream items that could still change the normative SSV behavior described above. If any of these settle differently, this SIP should be updated. -- beacon-APIs release status: `produceBlockV4` (`GET /eth/v4/validator/blocks/{slot}`), the [§6](#6-new-duty-envelope-signing-self-build-path) envelope publication and fetch endpoints, and the [§5](#5-proposer-preferences-duty) `SignedProposerPreferences` submission endpoint are merged to `beacon-APIs/master` ([#580](https://github.com/ethereum/beacon-APIs/pull/580), [#608](https://github.com/ethereum/beacon-APIs/pull/608)) but unreleased: the latest release tag (`v5.0.0-alpha.2`) predates them, and no client implementations are marked in the compatibility matrix yet. Watch for pre-release changes to the response variant discriminators, request/response bodies, and SSZ encodings. +- `produceBlockV4` request shape and builder authentication: [beacon-APIs #625](https://github.com/ethereum/beacon-APIs/pull/625) proposes replacing the GET with a POST carrying per-builder `BuilderPreferences`, including an optional `SignedRequestAuthV1`. If adopted, update [§4](#4-modified-proposer-duty)'s endpoint and specify the distributed request-auth signing duty once [builder-specs #165](https://github.com/ethereum/builder-specs/pull/165) settles the signed object and forwarding contract. - EIP-7688 progressive containers: [EIP-7688](https://eips.ethereum.org/EIPS/eip-7688) (forward-compatible consensus data structures) reshapes core Gloas containers into `ProgressiveContainer`s, changing how `hash_tree_root` is computed for the [§4](#4-modified-proposer-duty) block signing root and the [§6](#6-new-duty-envelope-signing-self-build-path) envelope. It is only Considered for Inclusion (CFI), not Scheduled for Inclusion, for Glamsterdam ([EIP-7773](https://eips.ethereum.org/EIPS/eip-7773); targeted for glamsterdam-devnet-7, inclusion decision pending). The pinned snapshot `801a38e15` has already merged it ([consensus-specs #4630](https://github.com/ethereum/consensus-specs/pull/4630)), but this SIP describes positional merkleization until 7688 is SFI'd: at that point [§6](#6-new-duty-envelope-signing-self-build-path)'s blinded-envelope note adopts `mix_in_active_fields(merkleize_progressive(...))` and [§4](#4-modified-proposer-duty) gains a matching block-root note. -- Builder-API authenticated connections (`SignedRequestAuthV1`): builder-specs [#138](https://github.com/ethereum/builder-specs/pull/138) (merged 2026-06-11, not yet in a release) defines an optional validator-key BLS signature over `{builder URL, slot}` under the fork-agnostic domain `DOMAIN_REQUEST_AUTH` (`0x0B000001`), used to authenticate direct bid requests (`getExecutionPayloadBid`) and required for per-builder execution payments (`submitBuilderPreferences`). A bid pays the proposer via `bid.value`, deducted on-chain from the builder's staked collateral, and optionally via `bid.execution_payment`, a trusted execution-layer payment that the proposer must opt into per builder by submitting a `max_execution_payment` cap through `submitBuilderPreferences`. Deferring the duty costs no liveness in the interim: a cluster that has not yet submitted builder preferences has a zero cap, so builders MUST NOT attach execution payments and every bid it can receive pays from staked collateral only (gossiped bids MUST carry `execution_payment = 0` regardless of channel). Distributed signing of `RequestAuthV1` follows the `ValidatorRegistration` shape (validator-scoped, one partial-signature round, no QBFT) and will be specified once the upstream surface settles, meaning a standardized hop between the key holder and block assembly (a beacon-APIs successor to the now-deprecated `register_validator` forwarding, or a sidecar specification) and a builder-specs release that includes the Gloas builder API. ## Acknowledgements From 9343090f8166d6287072bb193f2409386a6d81f1 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Tue, 28 Jul 2026 09:53:17 -0700 Subject: [PATCH 60/62] clarify MessageID validation keying --- sips/epbs_support.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index b4dbc33..bc37265 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -327,13 +327,13 @@ SSV pubsub message validation is content-agnostic: it never decodes the signed b All three new roles are validator-scoped, like `RoleValidatorRegistration` and `RoleVoluntaryExit`: the `MessageID` carries the validator public key, routing reuses the cluster's existing subnet (no new topic), and each `PartialSignatureMessages` container carries at most one entry (the non-committee packet rule, REJECT above 1). Partial-signature type must match the role (REJECT on mismatch), and consensus messages are REJECT'd for the two non-QBFT roles, joining the existing registration/exit rule. -| Role (wire) | Consensus messages | Partial-sig type; count per (signer, slot) | Message slot | Earliness allowance | Lateness TTL | Duty assignment check | Duty limit per epoch | +| Role (wire) | Consensus messages | Partial-sig type; count per (`MessageID`, signer, slot) | Message slot | Earliness allowance | Lateness TTL | Duty assignment check | Duty limit per epoch | |---|---|---|---|---|---|---|---| | `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) | -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). +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 (`MessageID`, 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. From dee67f5dd85e43e89348776542a1b1218f01876f Mon Sep 17 00:00:00 2001 From: shane-moore Date: Tue, 28 Jul 2026 10:48:14 -0700 Subject: [PATCH 61/62] Clarify dependent-root stale duty handling --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index bc37265..114ed9d 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -335,7 +335,7 @@ All three new roles are validator-scoped, like `RoleValidatorRegistration` and ` 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 (`MessageID`, 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. +A view that predates the node's most recent validator-set change, or whose cached `dependent_root` does not match the node's current local dependent root for that epoch, is treated the same way: skip the check, don't IGNORE, until a successful refresh matching the current validator set and dependent root is installed. This is load-bearing for the one-shot partial-signature checks across these roles: a validator-index or dependent-root change can cause an honest message to be emitted while invalidating 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 can permanently starve the affected duty from quorum. Anti-spam stays bounded by committee membership, the distinct-signing-root cap, and the per-epoch duty-count cap. Derivations, per row: From 9ecb69535ee9695e6fa7313c109ee9c55bad14d2 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Wed, 29 Jul 2026 15:07:37 -0700 Subject: [PATCH 62/62] make stale-view rule duty-refresh-architecture neutral --- sips/epbs_support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sips/epbs_support.md b/sips/epbs_support.md index 114ed9d..13ddaba 100644 --- a/sips/epbs_support.md +++ b/sips/epbs_support.md @@ -335,7 +335,7 @@ All three new roles are validator-scoped, like `RoleValidatorRegistration` and ` 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 (`MessageID`, 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, or whose cached `dependent_root` does not match the node's current local dependent root for that epoch, is treated the same way: skip the check, don't IGNORE, until a successful refresh matching the current validator set and dependent root is installed. This is load-bearing for the one-shot partial-signature checks across these roles: a validator-index or dependent-root change can cause an honest message to be emitted while invalidating 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 can permanently starve the affected duty from quorum. Anti-spam stays bounded by committee membership, the distinct-signing-root cap, and the per-epoch duty-count cap. +A view that predates the node's most recent validator-set change, or its most recently detected dependent-root change for that epoch, is treated the same way: skip the check, don't IGNORE, until a refresh completed after that change is installed. Detection is whatever the client's duty-refresh mechanism provides: a client that re-fetches and wholesale-replaces the view each slot detects and installs in one step, leaving no skip window, while an event-driven client skips between the event and the refetch landing. This is load-bearing for the one-shot partial-signature checks across these roles: a validator-index or dependent-root change can cause an honest message to be emitted while invalidating 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 can permanently starve the affected duty from quorum. Anti-spam stays bounded by committee membership, the distinct-signing-root cap, and the per-epoch duty-count cap. Derivations, per row: