feat(ssv_types): retarget PTC role to validator-scoped PTCAttester - #1080
Conversation
Retargets the merged committee-scoped Role::PTCCommittee to a validator-scoped, leaderless Role::PTCAttester per SIP-94 §3, and gives PTC its own PartialSignatureKind::PTCAttester = 7 instead of reusing PostConsensus. Wire bytes are unchanged (Role byte 7; kind value 7). - msgid: rename the role, drop it from is_committee_role, max_round is None (non-QBFT), validator-scoped duty executor; add is_non_qbft_role. - partial_sig: add PartialSignatureKind::PTCAttester = 7. - message_validator: bucket PTC as pre-consensus, bind the PTCAttester role to the PTCAttester kind, enforce the per-validator one-message-per-packet bound (removing the dead committee occurrence helpers), flat duty_limit of Some(2), and reject consensus messages for non-QBFT roles via is_non_qbft_role. - qbft_manager: drop the PTC routing stub; PTCAttester rejects QBFT messages as an inconsistent message id in both executor branches.
|
@claude, please take a look |
|
Claude encountered an error after 12m 9s —— View job Review in progress
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## epbs #1080 +/- ##
=======================================
Coverage ? 62.52%
=======================================
Files ? 156
Lines ? 26547
Branches ? 0
=======================================
Hits ? 16598
Misses ? 9949
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Claude finished @shane-moore's task in 11m 45s —— View job Claude Code Review
SummaryThe retargeting from I have a few questions / observations below — none are merge blockers, but two are worth either confirming intent in the PR description or following up on. Observations1. Two behavior changes that are correct, but maybe worth surfacing. Because
Both are aligned with the validator‑scoped redesign (one validator per packet, monotonic per‑validator slot progression). I would just call this out explicitly in the PR description's "Behavior change" bullet, so reviewers understand the bucketing flip is two observable rule changes, not just the duty‑limit/packet‑bound change already mentioned. 2. Possible orphaned QBFT‑PTC types in
If it's the former, a one‑line 3. pub fn is_non_qbft_role(self) -> bool {
self.max_round().is_none()
}This is concise and currently correct, but it implicitly couples "non‑QBFT" to "has no max round". If a future role legitimately has pub fn is_non_qbft_role(self) -> bool {
matches!(
self,
Role::ValidatorRegistration | Role::VoluntaryExit | Role::PTCAttester,
)
}The compiler then forces a deliberate decision when a new 4. error!(?msg_id, "Unexpected role/executor combination in msg id");For 5. Test for I traced the path: Things I checked and are fine
Fix is_non_qbft_role to match variants explicitly → |
|
@claude, thanks for the careful pass. Dispositions: 1. Two newly-enforced checks. Agreed, surfaced in the description: added a "bucketing flip" bullet noting that 2. Orphaned QBFT-PTC types. Already handled by the open #1076 (issue #1074), which removes exactly these and touches only 3. 4. Log wording at 5. Confirmed, thanks for tracing it. |
|
Claude finished @shane-moore's task in 25s —— View job Dispositions acknowledgedThanks for the thorough responses — all five reasoned, no further pushback from me. 1. Bucketing flip surfaced in description. 2. Orphaned types handled by #1076. Makes sense — disjoint-symbols rule keeps 3. 4. Log wording. Fair — pre-existing line, keeping diff scope tight is the right call. Worth a separate small cleanup if anyone hits it in production. 5. 👍 No remaining concerns from my side. LGTM modulo your in-flight description tweak. --- · Branch: |
Add classification asserts for PTCAttester (not committee-scoped, no max round, non-QBFT) so a silent revert of the retarget fails a test, and extend the non-consensus-role rejection test to cover PTCAttester. Also regroup PTCAttester into the non-QBFT arm of qbft_manager's committee-executor match; same InconsistentMessageId outcome, clearer intent.
Payload attestations are gossip-valid only for their own slot and block-includable only at slot + 1, so PTC partials older than that are useless. Move PTCAttester from the epoch-long TTL bucket (34 slots, inherited from the committee-scoped design) to the Proposer/SyncCommittee slot-bound bucket (3 slots).
For # 3: This would trip me up without the help of a coding agent in the distant future if making mods to this domain and still learning my way around here. Valid to use the max round if its tied semantically, but would suggest a test to ensure all roles follow the rules. Something like technically non-blocking but would probably help maintainers later on. I see Less important - could be followed up later or ignored but I see otherwise lgtm 💯 |
Tasks
View job run · Branch: |
|
ha ha, cancelled that run ^. |
|
Great, looks good! |
Review feedback on sigp#1080: - Rename is_non_qbft_role to is_qbft_role and negate at the consensus-message guard, mirroring how is_committee_role is named positively and negated at call sites (petarjuki7). - Pin every role's QBFT classification in a test so a new role landing in the wrong max_round arm fails loudly instead of silently flipping validation behavior (jnhsigmap). - Drop the unreachable FailedToGetMaxRound variant: the guard already rejects every role without a max round, so the fallback now returns UnexpectedConsensusMessage, which is also what the branch would mean if it ever fired (jnhsigmap).
|
@jnhsigmap Both addressed in b50aaf63. Classification test: added
|
|
@petarjuki7 Good call, done in b50aaf63. |
Merge Queue Status
This pull request spent 10 minutes 16 seconds in the queue, including 7 minutes 43 seconds running CI. Required conditions to merge
|

Problem, Evidence, and Context
SIP-94 §3 was rewritten to a validator-scoped, leaderless PTC design (modeled on
the ProposerPreferences flow in §5): a payload-timeliness attestation is a
standalone single-validator signature with no QBFT consensus round. The role
that merged earlier (#1033) modeled PTC as a committee-scoped, QBFT-bearing role,
which no longer matches the spec.
This PR retargets that role to a validator-scoped
PTCAttesterand gives PTC itsown partial-signature kind instead of overloading
PostConsensus.Fork::CStar.The later PTC sign path and client spawn build on this PR's new symbols.
Change Overview
PTC moves out of the committee role-class into the validator-scoped, non-QBFT
class at every dispatch site: validator-scoped duty executor (not a committee
id), no max QBFT round, the pre-consensus message bucket, and a per-validator
one-signature-per-packet bound. It also gets a dedicated
PartialSignatureKind,so a PTC attestation is no longer indistinguishable on the wire from a
post-consensus signature.
Suggested reading order:
ssv_types(msgid,partial_sig): the role rename + reclassification andthe new partial-signature kind. A small
is_qbft_rolepredicate isintroduced (positively named, mirroring
is_committee_role) and negatedat the consensus-message guard.
message_validator: the validation-path consequences (kind/role binding,packet bound, duty limit, and rejection of consensus messages for non-QBFT
roles). Two committee-only helpers are deleted, subsumed by the shared
per-validator bound.
qbft_manager: the PTC routing stub is removed; PTC now rejects QBFTmessages as an inconsistent message id in both executor branches.
What intentionally did not change:
7; the dedicated kind takes value7.Fork::CStaractivation gate.Risks, Trade-offs, and Mitigations
Role/PartialSignatureKindmatch in thetouched crates is exhaustive (no
_wildcard), so the compiler flagged eachsite that needed updating.
cargo check --workspaceis the gate.routed (matching the leaderless design), and PTC partial-sig packets are
capped at one signature per validator per packet (the old committee occurrence
cap is subsumed by this bound). The rejection is a
Rejectgossip verdict(peer penalty on synced nodes), which is intentional: post-
CStara PTCconsensus message is a protocol violation under the leaderless design.
Previously such a message was accepted and propagated, then dropped
internally by the
qbft_managerstub.PTCAttesterleaves the committeerole-class, two checks previously skipped for
PTCCommitteenow apply to PTCpackets: the validator-index-in-committee check (
ValidatorIndexMismatch) andthe per-signer slot-advancement check (
SlotAlreadyAdvanced). Both follow fromvalidator-scoping (one validator per packet, monotonic per-validator slot).
PTCAttestermoves from the epoch-longlateness/TTL bucket (
slots_per_epoch + 2= 34 slots, inherited from thecommittee-scoped design) to the slot-bound Proposer/SyncCommittee bucket
(3 slots). Per the Gloas spec, a payload attestation is gossip-valid only
for its own slot and block-includable only at slot + 1, so older partials
are useless and accepting them only widens the replay/relay surface.
partial-signature kind flips from
PostConsensus(0) toPTCAttester(
7), and the byte-7MessageIdlayout changes fromCommitteeId(
[24..56]) to validator pubkey ([8..]). No deployed impact: no produceremits PTC partial signatures yet and
CStarhas never been fork-active, soonly an epbs devnet mixing pre- and post-retarget nodes would need a
lockstep upgrade.
Validation
cargo test -p ssv_types -p message_validator -p qbft_manager: 98 / 63 / 19pass, 0 failures (plus doc-tests). Existing PTC tests were retargeted to the
validator-scoped role; coverage asserts the validator duty executor, the
non-QBFT classification (not committee-scoped, no max round), the dedicated
kind's SSZ round-trip, the kind-to-role binding, the per-validator packet
bound, the consensus-message rejection for
PTCAttester, the pre-CStarfork reject, the flat duty limit, and the slot-bound TTL (accepted 2 slots
late, rejected 20 slots late).
cargo +nightly fmt --all -- --check: clean.git grep PTCCommittee anchor/: empty.Rollback
Revert the branch's four commits. No config, data, or migration impact; wire encodings
are unchanged, so there is no cross-version compatibility concern.
Blockers / Dependencies
None for merge. Follow-ups that build on this PR's new symbols: the PTC sign path
and the client spawn wiring.