Skip to content

feat(ssv_types): add Role::EnvelopeProposer with message-validator fork gate #1120

Description

@shane-moore

Goal

Add validator-scoped QBFT Role::EnvelopeProposer with wire byte [9, 0, 0, 0], matching SIP-94 RoleEnvelopeProposer = 9, and bind it to PartialSignatureKind::PostConsensus.

Wire the compile-coupled Role match sites in ssv_types, message_validator, and qbft_manager, gate the role on the Ethereum Gloas fork, and leave a transient qbft_manager reject for the follow-up QBFT routing issue to replace.

Context / motivation

SIP-94 §6 models self-build envelope signing as a second proposer-scoped QBFT duty after the §4 block is published. SIP-94 §7 pins the message-validation contract for this role:

  • QBFT role, round cut-off 2.
  • PostConsensusPartialSig only, one partial signature per (signer, slot).
  • Message slot is the proposal slot.
  • No earliness allowance, 3-slot lateness TTL.
  • Proposer assignment check at the slot, but only once the epoch's proposer duties are known.
  • Duty cap is SLOTS_PER_EPOCH.

Live SIP-94 and ssv-spec #632 name the role RoleEnvelopeProposer. The current go-ssv PR #2901 still names the same wire role RoleEnvelopeBuilder, but the relevant parity target is the shared wire byte 9 plus validation behavior.

Adding this role breaks exhaustive matches. This issue intentionally bundles the compile-coupled validation and temporary routing edits so the workspace remains buildable.

Suggested approach

  • ssv_types/src/msgid.rs: add Role::EnvelopeProposer, encode/decode it as [9, 0, 0, 0], keep it out of is_committee_role(), set max_round() to Some(2), and map its MessageId executor to DutyExecutor::Validator.
  • ssv_types/src/partial_sig.rs: no new kind. Reuse PartialSignatureKind::PostConsensus.
  • message_validator/src/lib.rs: add EnvelopeProposer to the validator-role committee lookup, the Gloas fork gate, the short TTL bucket, create_message_id_for_test, and duty_limit.
  • message_validator/src/lib.rs: extend validate_beacon_duty with an EnvelopeProposer arm that checks proposer duties only when is_epoch_known_for_proposers(epoch) is true: is_epoch_known_for_proposers(epoch) && !is_validator_proposer_at_slot(slot, validator_index) returns NoDuty.
  • message_validator/src/partial_signature.rs: accept only PostConsensus for EnvelopeProposer, reject other kinds with PartialSignatureTypeRoleMismatch, and include the role in the per-validator message_count > 1 packet bound.
  • message_validator/src/message_counts.rs: no new arm should be needed. PostConsensus already uses the generic post-consensus seen-message counter. Add a regression test for a repeated PostConsensus packet from the same signer/slot.
  • message_validator: do not add envelope-specific stale-slot branches. Because EnvelopeProposer is non-committee, inherit the existing monotonic-slot checks in both consensus and partial-signature validation. Consensus and post-consensus messages must use the same role-specific MessageId state; ordinary Role::Proposer state remains isolated by its different role byte.
  • qbft_manager/src/lib.rs: in the DutyExecutor::Validator branch, handle EnvelopeProposer with a transient warn! + Err(QbftError::RoleNotActive) and a //todo(epbs): wire EnvelopeProposer instance routing marker. Do not classify it as InconsistentMessageId in this branch, since the executor is correct and routing is just not wired yet.
  • qbft_manager/src/lib.rs: in the DutyExecutor::Committee branch, reject EnvelopeProposer with InconsistentMessageId.

Acceptance criteria

  • Role::EnvelopeProposer round-trips through From<Role> / TryFrom<&[u8]> as [9, 0, 0, 0].
  • is_committee_role() == false; MessageId::duty_executor() resolves to DutyExecutor::Validator.
  • max_round() == Some(2) and is_qbft_role() == true.
  • PartialSignatureKind::PostConsensus is accepted for EnvelopeProposer; any other kind is rejected with PartialSignatureTypeRoleMismatch.
  • Same-packet message_count > 1 is rejected, and a repeated PostConsensus packet for the same signer/slot is rejected by the inherited post-consensus seen-message guard.
  • EnvelopeProposer remains a monotonic-slot validator role. After accepting envelope consensus or PostConsensus at slot N, an envelope message from the same signer at a lower slot returns SlotAlreadyAdvanced and maps to MessageAcceptance::Ignore. Consensus and post-consensus share the same envelope MessageId state, while ordinary proposer state does not advance envelope state.
  • Pre-Gloas messages are rejected with RoleNotActiveBeforeEthFork { minimum_fork: ForkName::Gloas }.
  • TTL uses the short bucket, 1 + LATE_SLOT_ALLOWANCE.
  • validate_beacon_duty rejects a known-epoch non-proposer with NoDuty, and tolerates a not-yet-fetched proposer epoch.
  • duty_limit(EnvelopeProposer) == Ok(Some(slots_per_epoch)).
  • Gossip classification matches SIP-94 §7 once fix: duty-limit breach is classified REJECT, should be IGNORE #1118 lands: NoDuty and duty-limit overflow map to Ignore; fork-gate failure, kind mismatch, and packet count overflow map to Reject.
  • qbft_manager validator-executor EnvelopeProposer returns RoleNotActive with the temporary TODO marker; committee-executor EnvelopeProposer returns InconsistentMessageId.

Tests

  • cargo test -p ssv_types -p message_validator
  • cargo check -p qbft_manager
  • Cover role byte round-trip, validator duty executor, QBFT classification, partial-signature kind binding, repeated PostConsensus packet rejection, pre-Gloas fork gate, TTL bucket, proposer-duty known/unknown epoch behavior, duty cap, and qbft_manager transient/permanent reject behavior.
  • Cover monotonic-slot behavior through the production validator state lookup: envelope consensus at slot N followed by envelope PostConsensus at N succeeds; accepted consensus at N causes lower-slot post-consensus from the same signer to be ignored; accepted post-consensus at N causes lower-slot consensus from the same signer to be ignored; ordinary proposer state does not cause an envelope stale-slot result.

Notes

Depends on #1118 for the duty-limit overflow gossip classification. This issue should not duplicate that fix unless #1118 is still open and the implementer intentionally folds it in.

Do not add a new partial-signature kind. SIP-94 §6 reuses PostConsensusPartialSig; the runner role discriminates routing.

Do not bucket EnvelopeProposer with Anchor's current Role::Proposer max_round() == Some(6). SIP-94 §7 pins the envelope cut-off to 2.

Issues are directionally correct, not prescriptive; verify symbols at PR time.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

epbsePBS / EIP-7732 / Gloas implementation

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions