List view
Covers PTC (Payload Timeliness Committee) attestation duty for the Gloas / EIP-7732 / ePBS fork (SSV-side §3 in [SIP-94](https://github.com/ssvlabs/SIPs/pull/94)). This is the **no-QBFT redesign** of §3 and supersedes the QBFT-era PTC milestone (#4): SIP-94 §3 was rewritten to a validator-scoped, leaderless design. PTC is a new per-slot duty. The consensus layer selects 512 validators per slot to attest to whether the proposer published its `SignedExecutionPayloadEnvelope` on time (`payload_present`) and whether the block's blob data is locally available (`blob_data_available`). The duty fires at the 75% slot mark; validators sign `PayloadAttestationData { beacon_block_root, slot, payload_present, blob_data_available }` under `DOMAIN_PTC_ATTESTER` (`0x0C`). See [consensus-specs Gloas validator.md](https://github.com/ethereum/consensus-specs/blob/dev/specs/gloas/validator.md) and [beacon-chain.md containers](https://github.com/ethereum/consensus-specs/blob/dev/specs/gloas/beacon-chain.md). **No QBFT, validator-scoped.** Unlike §2 attestation, PTC runs no consensus round and has no leader. A PTC vote is one beacon node's observation, not a value to negotiate, and QBFT would only add round-trips that risk the late-slot deadline. SSV models PTC like `ProposerPreferences` (§5): a validator-scoped runner role `Role::PTCAttester` and a dedicated `PartialSignatureKind::PTCAttester = 7` (next free after `AggregatorCommitteePartialSig = 6`), with one runner instance per locally-assigned PTC validator (keyed by pubkey). Each operator evaluates `PayloadAttestationData` from its own beacon node at the 75% cutoff, pins that snapshot, and signs it directly with its BLS share. Partial signatures group by signing root; when a threshold (2f+1) of operators converge on byte-identical data, any operator reconstructs and submits one `PayloadAttestationMessage` per validator to the beacon node within the [75%, 100%] window. Lighthouse's `PayloadAttestationService` owns the data fetch (once at 75%) and the no-block abstain, then calls `sign_payload_attestation(pubkey, data)` per validator; Anchor signs the `data` it is handed and never fetches from the BN or writes slashing protection (CR-1). **Trust model: honest-convergence, not consensus.** There is no QBFT value check because there is no leader-proposed value: each operator signs only the observation its own BN made (one that saw no block abstains, matching the Gloas validator spec). So there is no Byzantine-leader wrong-value surface (the concern §2 accepts). The cost is liveness rather than safety: when operators' beacon nodes split across observations near the cutoff, no signing root reaches threshold and the cluster's vote for that validator is a **non-slashable, penalty-free silent miss**, bounded by SSV's PTC seat share. PTC attestations are not in the beacon-chain slashing predicate, so no slashability call is made. Tracking how often this silent miss happens is useful operational telemetry (the divergence-rate question was raised in the SIP-94 thread; it is not a normative §3 requirement); see the telemetry note in issue 3. **Critical path:** No. PTC sits on the proposer-reward path (75% slot deadline), not the 3-second attestation deadline. Ships after §2 attestation lands. **Migration from the merged QBFT code.** Two QBFT-era PRs merged on the `epbs` branch and are reworked here: PR #1033 (committee-scoped `Role::PTCCommittee` + message-validator wiring + a `qbft_manager` stub) and PR #1047 (`PayloadAttestationVote` + `QbftData` value object, now dead). The QBFT-design milestone (#4) is retained as a fallback but superseded by this one. **Issue arc:** 1. **Remove dead QBFT value object:** delete the merged `PayloadAttestationVote` struct + `QbftData` impl + value checker + tests (reverts PR #1047). Standalone. 2. **Retarget the role:** rename committee-scoped `Role::PTCCommittee` to validator-scoped `Role::PTCAttester` (`is_committee_role = false`, `duty_executor = Validator`, `max_round = None`) and add a dedicated `PartialSignatureKind::PTCAttester = 7` (no longer reusing `PostConsensus`); message-validator fork-gate (`Fork::CStar`), long TTL, duty-limit, per-validator packet bound (one message per packet), partial-sig binding, and message-counts bucketing. No `qbft_manager` routing (the role is non-QBFT; relocate it into the `InconsistentMessageId` arms). Reworks PR #1033. `PTCAttester = 7` keeps value-parity with the SIP's `RunnerRole` / `PartialSigMsgType` / `BeaconRole`; `ProposerPreferences` (§5) renumbers to `8`. 3. **Sign path:** `sign_payload_attestation` runs `collect_signature` in `SingleValidator` mode over the passed-in `data` under `Domain::PTCAttester`, mirroring `collect_voluntary_exit_partial_signatures`. No BN fetch, no slashing-DB writes. Reconstruction-failure telemetry: increment `anchor_ptc_reconstruction_failures_total`, but note that at the current signature-collector granularity a no-threshold miss is indistinguishable from a genuine channel close (both surface as `QueueClosedError`), so the divergence count is a coarse upper bound. A separate `research`-tagged follow-up (collector divergence-error split) can refine it; it is tracked outside this milestone. 4. **Service wiring:** spawn Lighthouse's `PayloadAttestationService` with `AnchorValidatorStore` as the backend (`::new(...)`, no builder); gate the start on `spec.is_gloas_scheduled()`, mirroring LH's own VC. A separate, non-blocking observability follow-up (`signature_collector` divergence-error split, tagged `epbs` + `research`) is tracked outside this milestone: it would let the issue 3 metric report a clean `observation_divergence` rate instead of the coarse `no_signature` upper bound. **Issues are directionally correct, not prescriptive.** The implementer should verify against the current SIP, consensus-specs Gloas `validator.md` / `beacon-chain.md`, and the latest Lighthouse VC behavior, and deviate from the suggested approach if a better one emerges. Acceptance criteria are the contract; surface tables are starting points.
No due date•0/4 issues closedCovers the new ProposerPreferences duty for the Gloas / EIP-7732 / ePBS fork (SSV-side §5 in [SIP-94](https://github.com/ssvlabs/SIPs/pull/94), commit `1f18c84`). Under Gloas, the pre-fork relay-based builder registration is gone along with blinded blocks. Proposers instead broadcast `SignedProposerPreferences` on a new `proposer_preferences` p2p topic; builders listen and use the preferences to construct matching `execution_payload_bid` objects. Without this broadcast, gossip IGNOREs unmatched bids and the proposer's BN has no trustless external builder options for that slot. Each `SignedProposerPreferences` carries `dependent_root`, `proposal_slot`, `validator_index`, `fee_recipient`, and `target_gas_limit`, signed under a new `DOMAIN_PROPOSER_PREFERENCES` with the validator's BLS share. SSV runs this as a validator-scoped, non-QBFT duty: each operator signs locally with its share, partial signatures go through one collection round, and reconstruction succeeds when ≥ 2f+1 operators converge on the same signing root. Same shape as `ValidatorRegistration` / `VoluntaryExit`. LH provides the `ProposerPreferencesService` that drives the per-validator per-epoch loop. **Trust model:** no QBFT and no shared decided value. Each operator's signing root is determined by its `target_gas_limit` (per-operator config) and `dependent_root` (per-operator BN observation); `fee_recipient` is cluster-consistent in practice. Divergence on `target_gas_limit` or `dependent_root` splits signing roots and silently fails reconstruction, leaving the validator without trustless external builder bids for that slot. Anchor surfaces both via metrics + warn-logs with distinguishable reason labels (per SIP-94 §5 Security Considerations). **Critical path:** No. Required for external-builder bid availability under Gloas; self-build still works without it. The cluster's block-signing path (§4) does not require seeing its own preferences before producing a block, because BN-side selection from `execution_payload_bid` gossip filters non-matching bids before the proposer is asked. **Issue arc:** 1. **Wire surface**: introduce `Role::ProposerPreferences` (byte `[8, 0, 0, 0]`) + `PartialSignatureKind::ProposerPreferences = 7` (next free after `AggregatorCommitteePartialSig = 6`); message-validator fork-gate (`Fork::CStar`), TTL (`2 * slots_per_epoch + LATE_SLOT_ALLOWANCE` to cover current + next-epoch lookahead), per-role bounds, partial-sig binding, and message-counts bucketing. Non-QBFT, so no `qbft_manager` stub. 2. **Sign path**: `sign_proposer_preferences` runs `collect_signature` in `SingleValidator` mode, mirroring `sign_validator_registration_data`. No slashing-DB writes (CR-1). 3. **Service wiring**: spawn LH's `ProposerPreferencesService` with `AnchorValidatorStore` as the backend; the service drives the per-validator per-epoch loop. 4. **`RegistrationService` fork-gate**: short-circuit `register_validators` post-Gloas (the relay-builder mechanism is gone). Service stays spawned; pre-Gloas testnets and transition-window operators continue unchanged. **Inherited LH gaps** (documented, not worked around): no mid-epoch dependent-root trigger (SIP-94 wants one; LH publishes once per epoch); pre-fork emission window for first-Gloas-epoch slots from epoch `GLOAS_FORK_EPOCH - 1` is missed by LH's `gloas_enabled` gate; BN endpoint URL pre-`beacon-APIs` lock. All inherited via LH's service; Anchor adapts as LH evolves. **Issues under this milestone are directionally correct, not prescriptive.** The implementer should verify against the current SIP, consensus-specs Gloas `validator.md`, beacon-APIs PR #563 (proposer-duties `dependent_root`), and the latest Lighthouse VC behavior, and deviate from the suggested approach if a better one emerges. Acceptance criteria are the contract; the sketch is a starting point.
No due date•0/4 issues closedCovers attestation duty updates required for the Gloas / EIP-7732 / ePBS fork (SSV-side §2 in SIP-94). Under Gloas, `AttestationData.index` is repurposed as a payload-presence flag set by the beacon node: - **`index = 0`** = payload `EMPTY` (or attesting to a same-slot block, which is unconditionally `0`). - **`index = 1`** = payload `FULL` (non-same-slot, payload envelope observed for the attested block). The BN owns the determination; the validator client carries the value unchanged. SSV must thread it through QBFT consensus so that all cluster operators sign over the same `AttestationData.index` and produce identical signing roots. Wire format: a new `GloasBeaconVote { block_root, source, target, attestation_data_index }` SSZ struct activates on Gloas slots, alongside today's 3-field `BeaconVote` (which stays unchanged for pre-Gloas slots). Cross-fork wire-incompatibility falls out of SSZ length mismatch by design (SIP-94 §2). After Gloas has activated on all networks, a follow-up SIP retires `BeaconVote` and renames `GloasBeaconVote` back to `BeaconVote`. Value-check trust model: per SIP-94 SC-2, the SSV value check does **not** compare the proposed `attestation_data_index` against each operator's local BN view. Operators' BNs may legitimately disagree on payload-arrival timing near the 25% slot deadline; requiring local agreement would fail QBFT rounds. Accepted tradeoff: a malicious QBFT leader can push a payload-status value contrary to the cluster's BN-majority observation; worst case is a missed attestation when the gossip REJECT layer drops it. Issues under this milestone are directionally correct, not prescriptive. The implementer should verify against the current SIP, beacon-APIs `attestation_data` schema, consensus-specs Gloas `validator.md`, and the latest Lighthouse VC behavior, and deviate from the suggested approach if a better one emerges. Acceptance criteria are the contract; the sketch is a starting point.
No due date•1/5 issues closedCovers block proposing updates required for the Gloas / EIP-7732 / ePBS fork (SSV-side §4 in SIP-94). Under Gloas, the `ExecutionPayloadEnvelope` reaches the network through one of three paths: - **External builder:** the builder wins the on-chain bid market and broadcasts a `SignedExecutionPayloadEnvelope` under `DOMAIN_BEACON_BUILDER`. The proposer's `BeaconBlock` commits to the bid via `signed_execution_payload_bid`; the VC never signs the envelope. - **Stateless self-build:** `produceBlockV4` with `include_payload=true` returns `Gloas.BlockContents` (block + envelope + blobs + KZG proofs) as one bundle. Multi-BN friendly: any BN can publish the envelope on behalf of the proposer. - **Stateful self-build:** `produceBlockV4` with `include_payload=false` returns `Gloas.BeaconBlock` only; the BN caches the envelope and blobs internally. The client fetches them later via `GET /eth/v1/validator/execution_payload_envelope/{slot}/{root}` against the same BN, signs the envelope, and publishes in a second step. Single-BN coupling per slot. The response header `execution_payload_included` tells the client whether the BN has an envelope to offer (`true` for self-build, `false` for external-builder slots where the builder will broadcast independently). Lighthouse's VC currently fetches blocks via v4 in stateful self-build mode; PR ethereum/beacon-APIs#580 is still evolving. The SSV cluster runs QBFT over `Gloas.BeaconBlock` only on every path; blobs and KZG proofs never pass through consensus. Distributed signing of `SignedExecutionPayloadEnvelope` is out of scope per SIP-94 (commit `cfb672e`), so on self-build slots the cluster does not republish the envelope and PTC may attest `payload_present = FALSE`. This is a known consequence. External-builder slots are unaffected. Issues under this milestone are directionally correct, not prescriptive. The implementer should verify the proposed solution against the current SIP, beacon-APIs PR #580, consensus-specs Gloas validator doc, and the latest Lighthouse VC behavior, and deviate from the suggested approach if a better one emerges. Acceptance criteria are the contract; the sketch is a starting point.
No due date•1/3 issues closedBuild a proposer-duty observability baseline for Anchor before making further QBFT or proposer-path changes. Why this exists: - Anchor has clear evidence that proposer timing is fragile under a tight slot budget, including `#758` and the broader fetch-before-consensus concern. - We currently have useful `tracing` and Prometheus coverage, but not one coherent proposer-duty view from pre-QBFT fetch/build through QBFT, post-consensus threshold/reconstruction, and publish. - We should measure the duty path end to end before changing consensus behavior. How this relates to SSV: - SSV has already shipped relevant observability and proposer-path work: duty-flow tracing (`ssvlabs/ssv#2076`, `ssvlabs/ssv#2272`), QBFT stage duration logging (`ssvlabs/ssv#2609`), smart proposal selection (`ssvlabs/ssv#2648`), and proposer timing budget documentation (`ssvlabs/ssv#2416`). - SSV still has open proposer-liveness and timing work: fetch-before-consensus / async fetch (`ssvlabs/ssv#1825`, `ssvlabs/ssv-spec#371`) and timeout alignment (`ssvlabs/SIPs#37`, `ssvlabs/ssv-spec#352`). - Anchor does not need a literal port of `ssvlabs/ssv#2076`. It needs a proposer-focused observability slice that lets us decide whether the next optimization belongs in pre-QBFT fetch/build, QBFT round behavior, post-consensus signing, or publish. Desired outcome: - one canonical proposer-duty root span - stable event / checkpoint vocabulary - low-cardinality metrics suitable for dashboards and alerting - enough data to attribute missed or slow proposer duties to the dominant phase Non-goals: - protocol redesign - leaderless consensus work - changing timeout logic in the same PRs as instrumentation - a repo-wide OpenTelemetry rollout on day one
No due date•1/7 issues closed