You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
qbft_manager validator-executor EnvelopeProposer returns RoleNotActive with the temporary TODO marker; committee-executor EnvelopeProposer returns InconsistentMessageId.
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::Proposermax_round() == Some(6). SIP-94 §7 pins the envelope cut-off to 2.
Issues are directionally correct, not prescriptive; verify symbols at PR time.
Goal
Add validator-scoped QBFT
Role::EnvelopeProposerwith wire byte[9, 0, 0, 0], matching SIP-94RoleEnvelopeProposer = 9, and bind it toPartialSignatureKind::PostConsensus.Wire the compile-coupled
Rolematch sites inssv_types,message_validator, andqbft_manager, gate the role on the Ethereum Gloas fork, and leave a transientqbft_managerreject 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:
2.PostConsensusPartialSigonly, one partial signature per(signer, slot).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 roleRoleEnvelopeBuilder, but the relevant parity target is the shared wire byte9plus 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: addRole::EnvelopeProposer, encode/decode it as[9, 0, 0, 0], keep it out ofis_committee_role(), setmax_round()toSome(2), and map itsMessageIdexecutor toDutyExecutor::Validator.ssv_types/src/partial_sig.rs: no new kind. ReusePartialSignatureKind::PostConsensus.message_validator/src/lib.rs: addEnvelopeProposerto the validator-role committee lookup, the Gloas fork gate, the short TTL bucket,create_message_id_for_test, andduty_limit.message_validator/src/lib.rs: extendvalidate_beacon_dutywith anEnvelopeProposerarm that checks proposer duties only whenis_epoch_known_for_proposers(epoch)is true:is_epoch_known_for_proposers(epoch) && !is_validator_proposer_at_slot(slot, validator_index)returnsNoDuty.message_validator/src/partial_signature.rs: accept onlyPostConsensusforEnvelopeProposer, reject other kinds withPartialSignatureTypeRoleMismatch, and include the role in the per-validatormessage_count > 1packet bound.message_validator/src/message_counts.rs: no new arm should be needed.PostConsensusalready uses the generic post-consensus seen-message counter. Add a regression test for a repeatedPostConsensuspacket from the same signer/slot.message_validator: do not add envelope-specific stale-slot branches. BecauseEnvelopeProposeris 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-specificMessageIdstate; ordinaryRole::Proposerstate remains isolated by its different role byte.qbft_manager/src/lib.rs: in theDutyExecutor::Validatorbranch, handleEnvelopeProposerwith a transientwarn!+Err(QbftError::RoleNotActive)and a//todo(epbs): wire EnvelopeProposer instance routingmarker. Do not classify it asInconsistentMessageIdin this branch, since the executor is correct and routing is just not wired yet.qbft_manager/src/lib.rs: in theDutyExecutor::Committeebranch, rejectEnvelopeProposerwithInconsistentMessageId.Acceptance criteria
Role::EnvelopeProposerround-trips throughFrom<Role>/TryFrom<&[u8]>as[9, 0, 0, 0].is_committee_role() == false;MessageId::duty_executor()resolves toDutyExecutor::Validator.max_round() == Some(2)andis_qbft_role() == true.PartialSignatureKind::PostConsensusis accepted forEnvelopeProposer; any other kind is rejected withPartialSignatureTypeRoleMismatch.message_count > 1is rejected, and a repeatedPostConsensuspacket for the same signer/slot is rejected by the inherited post-consensus seen-message guard.EnvelopeProposerremains a monotonic-slot validator role. After accepting envelope consensus orPostConsensusat slot N, an envelope message from the same signer at a lower slot returnsSlotAlreadyAdvancedand maps toMessageAcceptance::Ignore. Consensus and post-consensus share the same envelopeMessageIdstate, while ordinary proposer state does not advance envelope state.RoleNotActiveBeforeEthFork { minimum_fork: ForkName::Gloas }.1 + LATE_SLOT_ALLOWANCE.validate_beacon_dutyrejects a known-epoch non-proposer withNoDuty, and tolerates a not-yet-fetched proposer epoch.duty_limit(EnvelopeProposer) == Ok(Some(slots_per_epoch)).NoDutyand duty-limit overflow map to Ignore; fork-gate failure, kind mismatch, and packet count overflow map to Reject.qbft_managervalidator-executorEnvelopeProposerreturnsRoleNotActivewith the temporary TODO marker; committee-executorEnvelopeProposerreturnsInconsistentMessageId.Tests
cargo test -p ssv_types -p message_validatorcargo check -p qbft_managerPostConsensuspacket rejection, pre-Gloas fork gate, TTL bucket, proposer-duty known/unknown epoch behavior, duty cap, andqbft_managertransient/permanent reject behavior.PostConsensusat 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
EnvelopeProposerwith Anchor's currentRole::Proposermax_round() == Some(6). SIP-94 §7 pins the envelope cut-off to2.Issues are directionally correct, not prescriptive; verify symbols at PR time.