Skip to content

fix(ssv_types): cap Role::Proposer QBFT rounds at 2 - #1173

Merged
mergify[bot] merged 3 commits into
sigp:epbsfrom
jnhsigmap:fix/1119-proposer-max-round-2
Jul 28, 2026
Merged

fix(ssv_types): cap Role::Proposer QBFT rounds at 2#1173
mergify[bot] merged 3 commits into
sigp:epbsfrom
jnhsigmap:fix/1119-proposer-max-round-2

Conversation

@jnhsigmap

@jnhsigmap jnhsigmap commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Problem, Evidence, and Context

  • Role::Proposer capped QBFT at round 6, so Anchor accepted proposer consensus messages at rounds 3-6 that go-ssv drops as ErrRoundTooHigh.
  • go-ssv epbs-gloas message/validation/consensus_validation.go maxRound returns 2 for the proposer-class roles, 6 for sync-committee contribution, 12 for the committee and aggregator roles.
  • A proposer that has not decided by round 2 has already missed its in-slot broadcast deadline, so rounds 3-6 only widen the accepted-message surface. go-ssv's round timer documents the same reasoning: the 2s quick timeout is deliberately not retimed for Gloas, making the proposer effectively round-1-must-succeed.
  • Closes fix: Proposer max_round is 6, should be 2 (match go-ssv) #1119

Change Overview

  • Role::Proposer now shares the round 2 cap with Role::EnvelopeProposer, mirroring how go-ssv groups the two proposer-class roles.
  • Role::SyncCommittee keeps its round 6 cap in its own match arm.
  • Updated max_round comment citing SSV SIP-94 (reference now cannot go stale).
  • Unchanged: Role::EnvelopeProposer, committee and aggregator, the non-QBFT roles, round-timer durations, and timeout-mode selection.

Risks, Trade-offs, and Mitigations

  • Proposer consensus messages at rounds 3-6 are now ignored instead of accepted, and a local proposer instance times out one round-change earlier. Both are the intended go-ssv parity.
  • Anchor uses one value for the gossip gate and the local instance limit, while ssv keeps its local cut-off role-independent at 12. However, rounds above the validation cap cannot reach quorum because peers drop them, so stopping the local instance there forfeits no reachable consensus.
  • Blast radius is one constant. No signature, wire-format, or control-flow change.

Validation

  • New unit test pins the proposer cap at 2 and the sync-committee cap at 6, so a future re-merge of the arms fails loudly.
  • New message-validation tests assert a proposer message at round 3 is rejected with RoundTooHigh and one at round 2 is still accepted, covering both sides of the boundary.
  • cargo test -p ssv_types -p message_validator: 220 passed, 0 failed.
  • make cargo-fmt-check and make lint: clean.

Rollback

  • Revert the commit. The match arms return to a shared round 6 cap for Role::Proposer and Role::SyncCommittee. No config, data, or migration impact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jnhsigmap

Copy link
Copy Markdown
Contributor Author

pretty sure local claude re-pushed that 🙄
@claude, review this PR

@claude-code-actions-sigp

claude-code-actions-sigp Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude finished @jnhsigmap's task in 1m 44s —— View job


Review: cap Role::Proposer QBFT rounds at 2

  • Read the diff against origin/epbs
  • Review msgid.rs changes
  • Review consensus_message.rs test changes
  • Post review feedback

Verdict: LGTM. The change is correct, minimal, and well-tested. One constant moves (Role::Proposer from a shared 6 arm to the 2 arm alongside Role::EnvelopeProposer), with Role::SyncCommittee split into its own 6 arm.

Correctness

  • Role::Proposer | Role::EnvelopeProposer => Some(2) and Role::SyncCommittee => Some(6) are both reachable and exhaustive (msgid.rs:82-89); no arm overlaps. The match still covers every variant, so the compiler enforces completeness.
  • The single max_round() value legitimately feeds both consumers, so proposer parity is applied consistently:
    • Gossip gate: consensus_message.rs:141 rejects round > max_round with RoundTooHigh.
    • Local instance limit: qbft_manager/src/lib.rs:237-241 seeds with_max_rounds(...).
      The PR's note that clamping the local limit forfeits no reachable consensus (peers drop rounds above the validation cap, so quorum is unreachable anyway) is sound.
  • is_qbft_role() / monotonic_slot_role() behavior for Role::Proposer is unchanged — it still returns Some, so no classification regressions.

Tests

  • proposer_and_sync_committee_max_rounds_are_pinned (msgid.rs:526) pins both caps, and role_qbft_classification_is_pinned (msgid.rs:413) still guards the QBFT partition — a future accidental re-merge of the two arms would fail loudly. Good.
  • The two boundary tests (round 3 rejected, round 2 accepted) mirror the existing test_consensus_message_round_too_high pattern exactly and cover both sides of the cap. Nice coverage.

Minor observations (non-blocking)

  • The comment at msgid.rs:79-80 now cites the SIP instead of a pinned go-ssv commit/line. This is more durable, but the reader loses the exact upstream anchor. Since the PR body already carries the go-ssv maxRound mapping (2 = proposer-class, 6 = sync-committee contribution, 12 = committee/aggregator), that context lives outside the code. Optional: a one-line inline note that byte 3 (SyncCommittee, msgid.rs:54) is go-ssv's sync-committee contribution role would preserve the "why 6" rationale at the definition. Not required.
  • No change needed, but worth flagging for reviewers: this is a behavior change on the validation surface (rounds 3–6 for proposers go from accepted → ignored). That is the stated intent and matches go-ssv, so this is correct — just the one line to scrutinize on merge.

Verification note

I was unable to run cargo test / make lint in this environment (sandbox denied the commands). The PR body reports cargo test -p ssv_types -p message_validator (220 passed) and a clean make lint / cargo-fmt-check; I reviewed the code and tests by reading and they are consistent with those results, but I could not independently re-run them here.
· fix/1119-proposer-max-round-2

@codecov-commenter

codecov-commenter commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (epbs@16b64b6). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             epbs    #1173   +/-   ##
=======================================
  Coverage        ?   69.02%           
=======================================
  Files           ?      164           
  Lines           ?    31915           
  Branches        ?        0           
=======================================
  Hits            ?    22029           
  Misses          ?     9886           
  Partials        ?        0           
Flag Coverage Δ
rust 69.02% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jnhsigmap
jnhsigmap marked this pull request as ready for review July 27, 2026 09:00
@jnhsigmap

Copy link
Copy Markdown
Contributor Author

I updated the comment because it was referencing a commit and was stale. the SIP hasn't merged yet and the ssv epbs branch will likely change before it gets merged anywhere so just being conservative.

@shane-moore

shane-moore commented Jul 27, 2026

Copy link
Copy Markdown
Member

Looks great! Small nit: SIP-94 §7 only pins the cut-off for the new roles; Proposer = 2 is pre-existing go-ssv behavior. Simpler to just cite go-ssv:

// Caps both the incoming consensus-message round gate and the local QBFT
// instance round limit. Values mirror go-ssv's maxRound:
// https://github.com/ssvlabs/ssv/blob/d2352a3dba3e7b309ef090b7a23f4cac1d9002d1/message/validation/consensus_validation.go#L434-L443

@jnhsigmap

Copy link
Copy Markdown
Contributor Author

Looks great! Small nit: SIP-94 §7 only pins the cut-off for the new roles; Proposer = 2 is pre-existing go-ssv behavior. Simpler to just cite go-ssv:

// Caps both the incoming consensus-message round gate and the local QBFT
// instance round limit. Values mirror go-ssv's maxRound:
// https://github.com/ssvlabs/ssv/blob/d2352a3dba3e7b309ef090b7a23f4cac1d9002d1/message/validation/consensus_validation.go#L434-L443

yeah good point, fixed

@mergify mergify Bot added the queued label Jul 28, 2026
@mergify

mergify Bot commented Jul 28, 2026

Copy link
Copy Markdown

Merge Queue Status

  • Entered queue2026-07-28 02:46 UTC · Rule: default · triggered by rule Add outdated, ready-to-merge PRs to merge queue
  • Checks passed · on draft merge queue: checking epbs (16b64b6) and #1173 together #1181
  • Merged2026-07-28 04:39 UTC · at 793c0bbc1259f02c1e8bb36ae041aa097637b2fd · squash

This pull request spent 1 hour 53 minutes 20 seconds in the queue, including 1 hour 51 minutes 55 seconds running CI.

Required conditions to merge
  • check-success=cli-reference-check
  • check-success=run-local-testnet
  • check-success=test-suite-success

@mergify
mergify Bot merged commit ee4bcff into sigp:epbs Jul 28, 2026
23 checks passed
@mergify mergify Bot removed the queued label Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants