@@ -14,8 +14,8 @@ use slot_clock::SlotClock;
1414use ssv_types:: {
1515 CommitteeId , IndexSet , OperatorId ,
1616 consensus:: {
17- AggregatorCommitteeConsensusData , BeaconVote , GloasBeaconVote , ProposerConsensusData ,
18- QbftData , QbftDataValidator ,
17+ AggregatorCommitteeConsensusData , BeaconVote , EnvelopeConsensusData , GloasBeaconVote ,
18+ ProposerConsensusData , QbftData , QbftDataValidator ,
1919 } ,
2020 domain_type:: DomainType ,
2121 message:: SignedSSVMessage ,
@@ -30,7 +30,7 @@ use tokio::{
3030 } ,
3131 time:: { Instant , sleep} ,
3232} ;
33- use tracing:: { Instrument , debug , debug_span, error, warn} ;
33+ use tracing:: { Instrument , debug_span, error, warn} ;
3434use types:: { ChainSpec , Epoch , EthSpec , Hash256 , Slot } ;
3535
3636use crate :: instance:: qbft_instance;
@@ -96,6 +96,14 @@ pub enum ValidatorDutyKind {
9696 SyncCommitteeAggregator ,
9797}
9898
99+ /// Unique identifier for an envelope-proposer QBFT instance (SIP-94 §6). Envelope
100+ /// signing is a single per-slot duty, so no `ValidatorDutyKind` discriminator.
101+ #[ derive( Debug , Clone , Hash , PartialEq , Eq ) ]
102+ pub struct EnvelopeProposerInstanceId {
103+ pub validator : PublicKeyBytes ,
104+ pub instance_height : InstanceHeight ,
105+ }
106+
99107// Message that is passed around the QbftManager
100108pub struct QbftMessage < D : QbftData > {
101109 pub kind : QbftMessageKind < D > ,
@@ -148,6 +156,8 @@ pub struct QbftManager<E: EthSpec, S: SlotClock> {
148156 // QBFT instances for AggregatorCommitteeConsensusData
149157 aggregator_committee_instances :
150158 Map < AggregatorCommitteeInstanceId , AggregatorCommitteeConsensusData < E > > ,
159+ // QBFT instances voting on Gloas self-build envelope consensus data (SIP-94 §6)
160+ envelope_consensus_data_instances : Map < EnvelopeProposerInstanceId , EnvelopeConsensusData > ,
151161 // Utility to sign and serialize network messages
152162 message_sender : Arc < dyn MessageSender > ,
153163 // Number of slots per epoch
@@ -178,6 +188,7 @@ impl<E: EthSpec, S: SlotClock + Clone + 'static> QbftManager<E, S> {
178188 beacon_vote_instances : DashMap :: new ( ) ,
179189 gloas_beacon_vote_instances : DashMap :: new ( ) ,
180190 aggregator_committee_instances : DashMap :: new ( ) ,
191+ envelope_consensus_data_instances : DashMap :: new ( ) ,
181192 message_sender,
182193 slots_per_epoch,
183194 fork_schedule,
@@ -201,6 +212,12 @@ impl<E: EthSpec, S: SlotClock + Clone + 'static> QbftManager<E, S> {
201212 self . fork_schedule . active_fork_config ( epoch) . domain_type
202213 }
203214
215+ /// Whether the Ethereum Gloas (ePBS) fork is active at `slot`, per the consensus
216+ /// spec. Distinct from the SSV protocol `fork_schedule`.
217+ fn gloas_enabled_at_slot ( & self , slot : Slot ) -> bool {
218+ self . spec . fork_name_at_slot :: < E > ( slot) . gloas_enabled ( )
219+ }
220+
204221 // Decide a brand new qbft instance
205222 pub async fn decide_instance < D : QbftDecidable < E > > (
206223 & self ,
@@ -283,9 +300,24 @@ impl<E: EthSpec, S: SlotClock + Clone + 'static> QbftManager<E, S> {
283300 Some ( Role :: Aggregator ) => ValidatorDutyKind :: Aggregator ,
284301 Some ( Role :: SyncCommittee ) => ValidatorDutyKind :: SyncCommitteeAggregator ,
285302 Some ( Role :: EnvelopeProposer ) => {
286- // TODO: wire EnvelopeProposer instance routing (#1122)
287- debug ! ( ?msg_id, "EnvelopeProposer routing not yet wired" ) ;
288- return Err ( QbftError :: RoleNotActive ) ;
303+ let slot = types:: Slot :: new ( qbft_message. height ) ;
304+ // Defense in depth behind `validate_role_for_fork`: envelope QBFT
305+ // exists only post-Gloas.
306+ if !self . gloas_enabled_at_slot ( slot) {
307+ warn ! ( %slot, "Ignoring EnvelopeProposer message before Gloas fork" ) ;
308+ return Err ( QbftError :: RoleNotActive ) ;
309+ }
310+ let id = EnvelopeProposerInstanceId {
311+ validator,
312+ instance_height,
313+ } ;
314+ return self . pass_to_instance :: < EnvelopeConsensusData > (
315+ id,
316+ WrappedQbftMessage {
317+ signed_message : full_message,
318+ qbft_message,
319+ } ,
320+ ) ;
289321 }
290322 // Committee roles use DutyExecutor::Committee, not Validator
291323 Some ( Role :: Committee | Role :: AggregatorCommittee )
@@ -324,9 +356,8 @@ impl<E: EthSpec, S: SlotClock + Clone + 'static> QbftManager<E, S> {
324356 qbft_message,
325357 } ;
326358
327- // Gate the Gloas beacon-vote shape on Ethereum's Gloas (ePBS) fork,
328- // read from the consensus spec, rather than an SSV-internal fork.
329- if self . spec . fork_name_at_slot :: < E > ( slot) . gloas_enabled ( ) {
359+ // Gate the Gloas beacon-vote shape on Ethereum's Gloas (ePBS) fork using Ethereum consensus spec.
360+ if self . gloas_enabled_at_slot ( slot) {
330361 self . pass_to_instance :: < GloasBeaconVote > ( id, wrapped)
331362 } else {
332363 self . pass_to_instance :: < BeaconVote > ( id, wrapped)
@@ -416,6 +447,8 @@ impl<E: EthSpec, S: SlotClock + Clone + 'static> QbftManager<E, S> {
416447 . retain ( |k, _| * k. instance_height >= cutoff. as_usize ( ) ) ;
417448 self . aggregator_committee_instances
418449 . retain ( |k, _| * k. instance_height >= cutoff. as_usize ( ) ) ;
450+ self . envelope_consensus_data_instances
451+ . retain ( |k, _| * k. instance_height >= cutoff. as_usize ( ) ) ;
419452 }
420453 }
421454}
@@ -560,6 +593,26 @@ impl<E: EthSpec> QbftDecidable<E> for AggregatorCommitteeConsensusData<E> {
560593 }
561594}
562595
596+ impl < E : EthSpec > QbftDecidable < E > for EnvelopeConsensusData {
597+ type Id = EnvelopeProposerInstanceId ;
598+
599+ fn get_map < S : SlotClock > ( manager : & QbftManager < E , S > ) -> & Map < Self :: Id , Self > {
600+ & manager. envelope_consensus_data_instances
601+ }
602+
603+ fn instance_height ( & self , id : & Self :: Id ) -> InstanceHeight {
604+ id. instance_height
605+ }
606+
607+ fn message_id ( domain : & DomainType , id : & Self :: Id ) -> MessageId {
608+ MessageId :: new (
609+ domain,
610+ Role :: EnvelopeProposer ,
611+ & DutyExecutor :: Validator ( id. validator ) ,
612+ )
613+ }
614+ }
615+
563616#[ derive( Debug , Clone ) ]
564617pub enum QbftError {
565618 QueueClosedError ,
0 commit comments