feat: add validator-scoped QBFT Role::EnvelopeProposer (#1120) - #1149
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## epbs #1149 +/- ##
=======================================
Coverage ? 68.53%
=======================================
Files ? 164
Lines ? 31434
Branches ? 0
=======================================
Hits ? 21544
Misses ? 9890
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, review this PR |
|
Claude finished @jnhsigmap's task in 6m 7s —— View job PR Review —
|
|
Queued — the merge queue status continues in this comment ↓. |
Merge Queue Status
This pull request spent 29 minutes 20 seconds in the queue, including 27 minutes 10 seconds running CI. Required conditions to merge
|
Problem, Evidence, and Context (Required)
SIP-94 introduces ePBS self-build envelope signing (§6) as a second, proposer-scoped QBFT duty that runs after the §4 block is published. Anchor has no
Rolefor it, so envelope messages cannot be classified, fork-gated, or validated. This PR adds that role and wires the message-validation contract SIP-94 §7 pins for it.Because
Roleis a plain enum (not#[non_exhaustive]), adding a variant breaks every exhaustive match acrossssv_types,message_validator, andqbft_manager. This change intentionally bundles those compile-coupled edits so the workspace stays buildable in one step, and leaves QBFT instance routing as a transient reject for the follow-up (#1122).Change Overview (Required)
Role::EnvelopeProposer(wire byte[9, 0, 0, 0], matching SIP-94RoleEnvelopeProposer = 9) is a validator-scoped, QBFT, monotonic-slot role bound toPartialSignatureKind::PostConsensus. It reuses the existing post-consensus machinery rather than adding a new partial-signature kind; the runner role discriminates routing. Validation is gated on the Ethereum Gloas fork.The production surface is small (~42 lines): the new variant plus the arms every compile-coupled match site needs. The bulk of the diff is tests (~950 lines) that pin each SIP-94 §7 rule against the real validation entry points.
Issue Criteria Addressed:
Role::EnvelopeProposerencodes/decodes as[9, 0, 0, 0]viaFrom<Role>/TryFrom<&[u8]>. (msgid.rs)is_committee_role() == false,is_qbft_role() == true,max_round() == Some(2)(its own arm — deliberately not grouped withRole::Proposer'sSome(6), per §7's cut-off of 2). (msgid.rs)MessageId::duty_executor()resolves toDutyExecutor::Validator. (msgid.rs)PostConsensusis accepted; any other kind is rejected withPartialSignatureTypeRoleMismatch. No new kind added. (partial_signature.rs)message_count > 1is rejected via the per-validator bound. (partial_signature.rs / lib.rs)PostConsensuspacket for the same signer/slot is rejected by the inherited post-consensus seen-message counter. (message_counts.rs path; no new arm needed)MessageIdstate; after accepting at slot N, a lower-slot envelope message from the same signer returnsSlotAlreadyAdvanced(→Ignore). OrdinaryRole::Proposerstate stays isolated by its different role byte. (inherited non-committee checks; no envelope-specific stale branch)RoleNotActiveBeforeEthFork { minimum_fork: ForkName::Gloas }. (lib.rsvalidate_role_for_fork)1 + LATE_SLOT_ALLOWANCE(3 slots), not the committee bucket. (lib.rs)validate_beacon_dutyrejects a known-epoch non-proposer withNoDuty, and tolerates a not-yet-fetched proposer epoch (accepts). (lib.rs)duty_limit(EnvelopeProposer) == Ok(Some(slots_per_epoch)). (lib.rs)NoDutyand duty-limit overflow →Ignore; fork-gate, kind mismatch, packet-count overflow →Reject. (lib.rs classification test)EnvelopeProposerreturnsRoleNotActivewith aTODO(#1122)marker (executor is correct, routing not yet wired — deliberately notInconsistentMessageId); committee-executor path handles it for exhaustiveness. (qbft_manager/lib.rs)Intentionally unchanged: no new
PartialSignatureKind; no envelope-specific stale-slot branch (the role inherits the existing monotonic checks);Role::Proposerbehaviour and itsmax_round() == Some(6); QBFT instance routing (deferred to #1122).Risks, Trade-offs, and Mitigations (Required)
Rolematch sites, so a missed arm would be a compile error, not a silent runtime bug — the type system is the safety net here. All new arms are additive.RoleNotActivereject rather than a working instance. This is deliberate and markedTODO(#1122); envelope QBFT duties are inert until that lands, matching the issue's staged plan.EnvelopeProposercase cannot produceInconsistentMessageIdas literally worded, becauseduty_executor()always decodes bytes 8–55 as aValidatorpubkey — a committee-executorEnvelopeProposerMessageIdis unconstructable. The role is instead handled in the committee arm for exhaustiveness, and a test documents this encoding constraint. Called out for reviewer awareness.Validation (Required)
cargo test -p ssv_types— 111 passedcargo test -p message_validator— 100 passed (all new EnvelopeProposer coverage + sibling roles)cargo test -p qbft_manager— 36 passedcargo fmt --check— clean;cargo clippy --tests -- -D warnings— cleanMessageIdstate, and the qbft_manager transient/permanent reject behaviour.Rollback (Required for behavior or runtime changes; optional otherwise)
Revert the single commit. The role is fork-gated to Gloas (not yet active) and QBFT routing is a no-op reject, so there is no runtime or data impact on current networks. No config or migration involved.
Additional Info / Next Steps (Optional)
Reviewer guide — production vs test. The diff is ~4% production, ~96% test. Read the production arms first (small, mechanical), then spot-check the tests.
Production (~42 lines) — read these first:
ssv_types/src/msgid.rs(+6) — the variant,[9,0,0,0]codec,max_round() = Some(2),Validatorexecutor.message_validator/src/lib.rs(+22) — committee lookup, fork gate, short TTL,duty_limit, and thevalidate_beacon_dutyproposer-assignment arm.qbft_manager/src/lib.rs(+11) — transientRoleNotActivereject +TODO(#1122); exhaustiveness in the committee arm.message_validator/src/partial_signature.rs(+3 production) — the role added to thePostConsensusbind and the per-validator packet bound.Tests (~950 lines) — verify the contract:
message_validator/src/partial_signature.rs(+577) — the §7 partial-signature contract: kind bind, packet cap, short TTL (accept + reject), proposer-assignment (3 cases), monotonic equality boundary + both stale-slot directions, replay rejection. A sharedfour_node_committee_and_keypairfixture removes repeated setup across these.message_validator/src/consensus_message.rs(+111) — duty-cap and the pre-Gloas fork-gate rejection via the consensus entry point.message_validator/src/lib.rs(+46) — gossipMessageAcceptanceclassification.qbft_manager/src/tests.rs(+168) — transient/permanent reject behaviour and the committee-executor encoding constraint noted above.ssv_types/src/msgid.rs(+48) — role byte round-trip and classification pinning.The single commit is
feat: add validator-scoped QBFT Role::EnvelopeProposer (#1120).