feat: add Role::PTCCommittee with message-validator fork-gate - #1033
Conversation
|
@claude, please take a look. focus on the production code changes first and then the tests in another pass |
|
Claude finished @shane-moore's task in 4m 20s —— View job Review
Overall the production diff is tight and well-reasoned. The Pass 1 — production code
|
| # | Severity | Item |
|---|---|---|
| T1 | nit | test_ptc_committee_message_count_exceeds_validator_count comment overstates the ordering it verifies |
| T2 | nit | Large test boilerplate (helper extraction would shrink) |
No test concerns block merge. Coverage is proportional to the production changes.
Summary
Overall verdict: approve (informally — I can't submit formal reviews). Production decisions are correct, tests target the right invariants, and the WHY-comments at non-obvious sites are unusually well done. The qbft_manager warn-log and the duplicated occurrence-counting loop are the only things I'd consider addressing in this PR; everything else is fine as follow-up.
· branch: feat/ptc-role
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## epbs #1033 +/- ##
=======================================
Coverage ? 61.91%
=======================================
Files ? 156
Lines ? 26141
Branches ? 0
=======================================
Hits ? 16185
Misses ? 9956
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@claude, thanks for the thorough pass. Walking through: Table-driven Role encoding (msgid.rs): Not pursuing. Two properties matter for
Occurrence-counting helper extraction: Agreed, filed as #1039. The triplication pre-dates this PR; PTC is the third instance but extracting in this PR expands scope outside the wire surface. Test boilerplate (your Pass 2 #2) bundled into the same issue.
Fork gate on qbft_manager stub: Already addressed by the expanded TODO in Test comment overstates ordering (Pass 2 #1): Good catch, applied in Follow-up issue: #1039. |
|
Claude finished @shane-moore's task in 1m 27s —— View job AcknowledgedAll dispositions verified against the tree. Table-driven
Helper extraction → #1039. Right call to keep the wire-surface PR scoped. Bundling the test boilerplate into the same issue makes sense — the helper shrinks both production and test code in one pass.
Fork gate on stub. Verified T1 — test comment. Verified No further blockers from my side. Production and test diff stand as-is for merge.
|
| match self { | ||
| Role::Committee | Role::Aggregator | Role::AggregatorCommittee => Some(12), | ||
| Role::Proposer | Role::SyncCommittee => Some(6), | ||
| // PTC duty starts at 75% slot with ~3s remaining; QUICK_TIMEOUT = 2s |
There was a problem hiding this comment.
I don’t think this comment explains the Some(4) choice clearly enough. Starting PTC near 75% of a 12s slot leaves about 3s, so only round 1 is likely to complete before slot end with a 2s quick timeout. Rounds 2-4 are a local grace window rather than something derived from the protocol, and the current wording makes that sound more precise than it is.
Could we soften it to something like this?
// PTC is expected near 75% of a 12s slot, leaving about 3s.
// With 2s quick round timeouts, only round 1 is likely to finish
// before slot end. Allow up to 4 rounds as a small local grace
// window for delayed starts or message loss; this is not a
// consensus-spec requirement.|
I think these are the right behaviors to cover, but the new validation tests are much heavier than the rules they assert. Most of the PTC cases rebuild a full signed message, RSA key pair, fork schedule, clock, committee info, and operator key map just to exercise a small branch like Could we factor the repeated setup into test-only helpers and keep only one or two full-pipeline PTC smoke tests? The count and duplicate-index cases should ideally read mostly as "build these partial sig messages, validate PTC bounds, expect this error." That would keep the coverage while making the rule under test much easier to review. The TTL coverage could also be more targeted: use a receive time that would fail under the short proposer/sync TTL but pass under the long committee TTL, so the test proves PTC is intentionally in the committee-style TTL bucket rather than only proving one accepted and one rejected timestamp. |
|
Pushed 4d98a50. The three small branches you flagged now read as pure-function asserts:
TTL is now bucket-specific via Remaining full-pipeline smoke tests: pre-CStar fork gate + bucket-specific TTL accepted. Two total, all using existing helpers rather than raw signing/key construction. What I deliberately did not do: migrate the Committee and AggregatorCommittee inline arms (and the 152-LOC AggregatorCommittee occurrence test) to use the new helpers. That migration is a clean standalone refactor; doing it here mixes refactor with new code. The helpers ship with their first consumer in this PR and the rest moves in #1039. Inline |
Merge Queue Status
This pull request spent 11 minutes 16 seconds in the queue, including 9 minutes 46 seconds running CI. Required conditions to merge
|
Problem, Evidence, and Context
Role::PTCCommitteewire surface before any other PTC work can land.matchsites inmessage_validatorandqbft_manager; those edits ship together so the workspace stays green between PRs. Theqbft_managerchange is a one-line stub that the PTC instance routing PR replaces.Err(NoMatchingVariant)). The message-validator fork-gate is defense-in-depth for CStar-capable nodes that might receive a PTC message before their localactive_fork >= Fork::CStar.compute_ptcChange Overview
Diff shape: +553 / -9 across 5 files. ~69 lines of production code, ~484 lines of tests (≈7:1). Most of the test volume is
ValidationContextconstruction boilerplate (matches the existing house style for full-pipeline integration tests inpartial_signature.rs).ssv_types— wire surface:Role::PTCCommitteevariant: byte[7, 0, 0, 0](matches SIP-94BNRolePTCAttester = 7),is_committee_role = true,duty_executor = Committee,max_round = Some(4).PartialSignatureKindvariant. PTC is structurally a post-consensus signature (signed after QBFT decidesPayloadAttestationData, same shape asRole::Committee's post-consensus path). ReusingPostConsensuseliminates a wire-discriminant slot and ~30 LOC of test boilerplate. The role-to-kind binding lives inpartial_signature_type_matches_role.message_validator— fork-gate + bounds:validate_role_for_fork: pre-CStar reject mirroring the AggregatorCommittee/pre-Boole safety net.message_lateness: PTC bucketed in the long-TTL arm (committee-scoped messages need cross-cluster propagation time, even with mid-slot deadlines).duty_limit:Some(min(slots_per_epoch, V))whereV= local validator count. PTC pool for slot S = union of beacon committees for slot S, so each validator is PTC-eligible in at most one slot per epoch. Distinct from Committee/AggregatorCommittee'smin(slots_per_epoch, 2*V)— PTC has no sync-committee component and no attest+sync multiplier.partial_signature_type_matches_role: binds PTC toPostConsensus(folded into the existing Committee arm).Role::PTCCommitteearm withmessage_count <= Vand validator-index occurrence cap1. Structural maximum — PTC produces exactly one partial-sig per locally-assigned validator per slot. Distinct from Committee's caps (2V, 2) and AggregatorCommittee's (5V, 5).qbft_manager— transient stub:Role::PTCCommitteeto theSome(DutyExecutor::Validator(_))invalid-combination group.Err(QbftError::RoleNotActive)with a warn-log, marked//todo(cstar). The PTC instance routing PR replaces both.Tests delivered (10): 3 in
ssv_types, 7 inmessage_validator. Each maps one-to-one to a design decision documented in the local milestone plan.Intentionally not changed.
partial_sig.rsandmessage_counts.rs(no newPartialSignatureKindvariant). Slot-advancement-skip and validator-index-mismatch-skip behavior is inherited viais_committee_role() == trueand structurally covered by existing AggregatorCommittee skip tests; adding PTC-specific copies would duplicate ~150 LOC with no new signal. Theptc_is_committee_roleunit test asserts the boolean that drives both skips.Risks, Trade-offs, and Mitigations
Role::AggregatorCommittee = [6, 0, 0, 0]is Anchor-internal; PTC = 7 is upstream SIP-94. Manual audit for upstream go-ssv role conflict is required before merge. Byte 7 matches the SIP-assigned value and Anchor's byte 6 sits below the SIP allocation.qbft_managerstub is transient. PTC messages reachingreceive_dataare rejected withRoleNotActive+ warn-log. Not a safety issue (no PTC QBFT instance exists yet — rejection is the correct response). The//todo(cstar)marker and warn-log must not survive the PTC instance routing PR.max_round = Some(4)is timing-derived, not measured. Follows from the 75% slot start andQUICK_TIMEOUT = 2s: round 1 finishes within slot, rounds 2-4 cover operator clock skew, rounds 5+ are dead weight. Retune only if Lighthouse moves the PTC service start time off 75% slot.Validation
cargo test -p ssv_types -p message_validator— 64 + 83 tests pass (10 new PTC tests; 0 regressions).make cargo-fmt-check,make lint— green.duty_limitformula, per-role bounds, committee-role semantics,max_roundvalue, committee-style duty-executor).lib.rs::message_lateness(long-TTL rationale),lib.rs::duty_limit(PTC selection mechanics + consensus-specs permalink tocompute_ptc),partial_signature.rs::partial_signature_type_matches_role(PostConsensusreuse).Rollback
Blockers / Dependencies
qbft_manager(replaces//todo(cstar)stub),PayloadAttestationVoteSSZ container + value checker,metadata_servicePTC voting-context phase,sign_payload_attestationimpl, LighthousePayloadAttestationServicespawn.Additional Info
Base:
epbs. The milestone bundles original M3.1 (ssv_types) + M3.7 (message-validator) because the exhaustive-match cascade made splitting them produce double-touched files. The follow-up issues land independently against this base.