@@ -39,8 +39,25 @@ const QBFT_INSTANCE_NAME: &str = "qbft_instance";
3939const QBFT_MESSAGE_NAME : & str = "qbft_message" ;
4040const QBFT_CLEANER_NAME : & str = "qbft_cleaner" ;
4141
42- /// Number of slots to keep before the current slot
43- const QBFT_RETAIN_SLOTS : u64 = 1 ;
42+ /// Calculate the beacon chain inclusion deadline for a duty
43+ fn calculate_deadline ( role : Role , slot : types:: Slot , slots_per_epoch : u64 ) -> types:: Slot {
44+ match role {
45+ Role :: Committee | Role :: Aggregator => {
46+ // Attestations can be included until end of next epoch (epoch E+1)
47+ // Per EIP-7045: attestation from epoch E valid until end of epoch E+1
48+ let epoch = slot. epoch ( slots_per_epoch) ;
49+ types:: Slot :: new ( ( epoch. as_u64 ( ) + 2 ) * slots_per_epoch - 1 )
50+ }
51+ Role :: Proposer | Role :: SyncCommittee => {
52+ // Must be in the same slot
53+ slot
54+ }
55+ Role :: VoluntaryExit | Role :: ValidatorRegistration => {
56+ // One epoch to complete
57+ types:: Slot :: new ( slot. as_u64 ( ) + slots_per_epoch)
58+ }
59+ }
60+ }
4461
4562// Unique Identifier for a committee and its corresponding QBFT instance
4663#[ derive( Debug , Clone , Hash , PartialEq , Eq ) ]
@@ -98,8 +115,20 @@ pub struct QbftInitialization<D: QbftData> {
98115 on_completed : oneshot:: Sender < Completed < D > > ,
99116}
100117
101- // Map from an identifier to a sender for the instance
102- type Map < I , D > = DashMap < I , UnboundedSender < QbftMessage < D > > > ;
118+ // Manager's bookkeeping for an instance
119+ pub struct ManagedInstance < D : QbftData > {
120+ sender : UnboundedSender < QbftMessage < D > > ,
121+ deadline : types:: Slot ,
122+ }
123+
124+ // Map from an identifier to managed instance data
125+ type Map < I , D > = DashMap < I , ManagedInstance < D > > ;
126+
127+ // Enum to identify which instance completed
128+ pub enum InstanceId {
129+ BeaconVote ( CommitteeInstanceId ) ,
130+ ValidatorConsensus ( ValidatorInstanceId ) ,
131+ }
103132
104133// Top level QBFTManager structure
105134pub struct QbftManager {
@@ -115,6 +144,10 @@ pub struct QbftManager {
115144 message_sender : Arc < dyn MessageSender > ,
116145 // Network domain to embed into messages
117146 domain : DomainType ,
147+ // Channel to notify cleaner when instance completes
148+ completion_tx : mpsc:: UnboundedSender < InstanceId > ,
149+ // Slots per epoch for deadline calculations
150+ slots_per_epoch : u64 ,
118151}
119152
120153impl QbftManager {
@@ -125,21 +158,26 @@ impl QbftManager {
125158 slot_clock : impl SlotClock + ' static ,
126159 message_sender : Arc < dyn MessageSender > ,
127160 domain : DomainType ,
161+ slots_per_epoch : u64 ,
128162 ) -> Result < Arc < Self > , QbftError > {
163+ let ( completion_tx, completion_rx) = mpsc:: unbounded_channel ( ) ;
164+
129165 let manager = Arc :: new ( QbftManager {
130166 processor,
131167 operator_id,
132168 validator_consensus_data_instances : DashMap :: new ( ) ,
133169 beacon_vote_instances : DashMap :: new ( ) ,
134170 message_sender,
135171 domain,
172+ completion_tx,
173+ slots_per_epoch,
136174 } ) ;
137175
138176 // Start a long running task that will clean up old instances
139- manager
140- . processor
141- . permitless
142- . send_async ( Arc :: clone ( & manager ) . cleaner ( slot_clock ) , QBFT_CLEANER_NAME ) ?;
177+ manager. processor . permitless . send_async (
178+ Arc :: clone ( & manager ) . cleaner ( slot_clock , completion_rx ) ,
179+ QBFT_CLEANER_NAME ,
180+ ) ?;
143181
144182 Ok ( manager)
145183 }
@@ -161,6 +199,11 @@ impl QbftManager {
161199 let ( result_sender, result_receiver) = oneshot:: channel ( ) ;
162200 let message_id = D :: message_id ( & self . domain , & id) ;
163201
202+ // Calculate deadline for this instance
203+ let role = message_id. role ( ) . ok_or ( QbftError :: InconsistentMessageId ) ?;
204+ let slot = types:: Slot :: new ( * initial. instance_height ( & id) as u64 ) ;
205+ let deadline = calculate_deadline ( role, slot, self . slots_per_epoch ) ;
206+
164207 // General the qbft configuration
165208 let config = ConfigBuilder :: new (
166209 operator_id,
@@ -169,17 +212,12 @@ impl QbftManager {
169212 ) ;
170213 let config = config
171214 . with_quorum_size ( committee. cluster_members . len ( ) - committee. get_f ( ) as usize )
172- . with_max_rounds (
173- message_id
174- . role ( )
175- . and_then ( |r| r. max_round ( ) )
176- . ok_or ( QbftError :: InconsistentMessageId ) ? as usize ,
177- )
215+ . with_max_rounds ( role. max_round ( ) . ok_or ( QbftError :: InconsistentMessageId ) ? as usize )
178216 . build ( ) ?;
179217
180218 // Get or spawn a new qbft instance. This will return the sender that we can use to send
181219 // new messages to the specific instance
182- let sender = D :: get_or_spawn_instance ( self , id) ;
220+ let sender = D :: get_or_spawn_instance ( self , id. clone ( ) , deadline ) ;
183221 self . processor . urgent_consensus . send_immediate (
184222 move |drop_on_finish : DropOnFinish | {
185223 // A message to initialize this instance
@@ -261,7 +299,19 @@ impl QbftManager {
261299 id : D :: Id ,
262300 data : WrappedQbftMessage ,
263301 ) -> Result < ( ) , QbftError > {
264- let sender = D :: get_or_spawn_instance ( self , id) ;
302+ // Get the map for this data type
303+ let map = D :: get_map ( self ) ;
304+
305+ // Look up existing instance - network messages should only go to existing instances
306+ let Some ( managed) = map. get ( & id) else {
307+ // Instance doesn't exist yet - this message arrived before decide_instance was called
308+ // This is normal during startup, just ignore it
309+ return Ok ( ( ) ) ;
310+ } ;
311+
312+ let sender = managed. sender . clone ( ) ;
313+ drop ( managed) ; // Release the lock before sending
314+
265315 self . processor . urgent_consensus . send_immediate (
266316 move |drop_on_finish : DropOnFinish | {
267317 let _ = sender. send ( QbftMessage {
@@ -274,51 +324,84 @@ impl QbftManager {
274324 Ok ( ( ) )
275325 }
276326
277- // Long running cleaner that will remove instances that are no longer relevant
278- async fn cleaner ( self : Arc < Self > , slot_clock : impl SlotClock ) {
279- while !self . processor . permitless . is_closed ( ) {
280- sleep (
281- slot_clock
282- . duration_to_next_slot ( )
283- . unwrap_or ( slot_clock. slot_duration ( ) ) ,
284- )
285- . await ;
286- let Some ( slot) = slot_clock. now ( ) else {
287- continue ;
288- } ;
289- let cutoff = slot. saturating_sub ( QBFT_RETAIN_SLOTS ) ;
290- self . beacon_vote_instances
291- . retain ( |k, _| * k. instance_height >= cutoff. as_usize ( ) ) ;
292- self . validator_consensus_data_instances
293- . retain ( |k, _| * k. instance_height >= cutoff. as_usize ( ) ) ;
327+ /// Long running cleaner that removes instances based on completion or deadline
328+ async fn cleaner (
329+ self : Arc < Self > ,
330+ slot_clock : impl SlotClock ,
331+ mut completion_rx : mpsc:: UnboundedReceiver < InstanceId > ,
332+ ) {
333+ loop {
334+ tokio:: select! {
335+ // Branch 1: Instance completed - clean immediately
336+ Some ( id) = completion_rx. recv( ) => {
337+ match id {
338+ InstanceId :: BeaconVote ( id) => {
339+ self . beacon_vote_instances. remove( & id) ;
340+ }
341+ InstanceId :: ValidatorConsensus ( id) => {
342+ self . validator_consensus_data_instances. remove( & id) ;
343+ }
344+ }
345+ }
346+ // Branch 2: Slot timeout - clean expired instances
347+ _ = sleep(
348+ slot_clock
349+ . duration_to_next_slot( )
350+ . unwrap_or( slot_clock. slot_duration( ) )
351+ ) => {
352+ let Some ( current_slot) = slot_clock. now( ) else {
353+ continue ;
354+ } ;
355+ self . beacon_vote_instances
356+ . retain( |_, managed| managed. deadline >= current_slot) ;
357+ self . validator_consensus_data_instances
358+ . retain( |_, managed| managed. deadline >= current_slot) ;
359+ }
360+ }
361+
362+ if self . processor . permitless . is_closed ( ) {
363+ break ;
364+ }
294365 }
295366 }
296367}
297368
298- // Trait that describes any data that is able to be decided upon during a qbft instance
299369pub trait QbftDecidable : QbftData < Hash = Hash256 > + Send + Sync + ' static {
300- type Id : Hash + Eq + Send + Debug ;
370+ type Id : Hash + Eq + Send + Debug + Clone ;
301371
302372 fn get_map ( manager : & QbftManager ) -> & Map < Self :: Id , Self > ;
303373
374+ fn wrap_id ( id : Self :: Id ) -> InstanceId ;
375+
304376 fn get_or_spawn_instance (
305377 manager : & QbftManager ,
306378 id : Self :: Id ,
379+ deadline : types:: Slot ,
307380 ) -> UnboundedSender < QbftMessage < Self > > {
308381 let map = Self :: get_map ( manager) ;
309- match map. entry ( id) {
310- dashmap:: Entry :: Occupied ( entry) => entry. get ( ) . clone ( ) ,
382+ match map. entry ( id. clone ( ) ) {
383+ dashmap:: Entry :: Occupied ( entry) => entry. get ( ) . sender . clone ( ) ,
311384 dashmap:: Entry :: Vacant ( entry) => {
312385 // There is not an instance running yet, store the sender and spawn a new instance
313- // with the reeiver
386+ // with the receiver
314387 let ( tx, rx) = mpsc:: unbounded_channel ( ) ;
315388 let span = debug_span ! ( "qbft_instance" , instance_id = ?entry. key( ) ) ;
316- let tx = entry. insert ( tx) ;
389+ let managed = ManagedInstance {
390+ sender : tx,
391+ deadline,
392+ } ;
393+ let sender = entry. insert ( managed) . sender . clone ( ) ;
394+ let instance_id = Self :: wrap_id ( id) ;
395+ let completion_tx = manager. completion_tx . clone ( ) ;
396+ let message_sender = manager. message_sender . clone ( ) ;
317397 let _ = manager. processor . permitless . send_async (
318- Box :: pin ( qbft_instance ( rx, manager. message_sender . clone ( ) ) . instrument ( span) ) ,
398+ Box :: pin (
399+ qbft_instance ( rx, message_sender, completion_tx, instance_id)
400+ . instrument ( span) ,
401+ ) ,
319402 QBFT_INSTANCE_NAME ,
320403 ) ;
321- tx . clone ( )
404+ sender
322405 }
323406 }
324407 }
@@ -334,6 +417,10 @@ impl QbftDecidable for ValidatorConsensusData {
334417 & manager. validator_consensus_data_instances
335418 }
336419
420+ fn wrap_id ( id : Self :: Id ) -> InstanceId {
421+ InstanceId :: ValidatorConsensus ( id)
422+ }
423+
337424 fn instance_height ( & self , id : & Self :: Id ) -> InstanceHeight {
338425 id. instance_height
339426 }
@@ -354,6 +441,10 @@ impl QbftDecidable for BeaconVote {
354441 & manager. beacon_vote_instances
355442 }
356443
444+ fn wrap_id ( id : Self :: Id ) -> InstanceId {
445+ InstanceId :: BeaconVote ( id)
446+ }
447+
357448 fn instance_height ( & self , id : & Self :: Id ) -> InstanceHeight {
358449 id. instance_height
359450 }
0 commit comments