@@ -74,6 +74,12 @@ impl RewardCycleInfo {
74
74
self . reward_cycle == reward_cycle
75
75
}
76
76
77
+ /// Get the reward cycle for a specific burnchain block height
78
+ pub const fn get_reward_cycle ( & self , burnchain_block_height : u64 ) -> u64 {
79
+ let blocks_mined = burnchain_block_height. saturating_sub ( self . first_burnchain_block_height ) ;
80
+ blocks_mined / self . reward_cycle_length
81
+ }
82
+
77
83
/// Check if the provided burnchain block height is in the prepare phase
78
84
pub fn is_in_prepare_phase ( & self , burnchain_block_height : u64 ) -> bool {
79
85
PoxConstants :: static_is_in_prepare_phase (
@@ -83,6 +89,15 @@ impl RewardCycleInfo {
83
89
burnchain_block_height,
84
90
)
85
91
}
92
+
93
+ /// Check if the provided burnchain block height is in the prepare phase of the next cycle
94
+ pub fn is_in_next_prepare_phase ( & self , burnchain_block_height : u64 ) -> bool {
95
+ let effective_height = burnchain_block_height - self . first_burnchain_block_height ;
96
+ let reward_index = effective_height % self . reward_cycle_length ;
97
+
98
+ reward_index >= u64:: from ( self . reward_cycle_length - self . prepare_phase_block_length )
99
+ && self . get_reward_cycle ( burnchain_block_height) == self . reward_cycle
100
+ }
86
101
}
87
102
88
103
/// The runloop for the stacks signer
@@ -253,7 +268,8 @@ impl RunLoop {
253
268
let current_reward_cycle = reward_cycle_info. reward_cycle ;
254
269
self . refresh_signer_config ( current_reward_cycle) ;
255
270
// We should only attempt to initialize the next reward cycle signer if we are in the prepare phase of the next reward cycle
256
- if reward_cycle_info. is_in_prepare_phase ( reward_cycle_info. last_burnchain_block_height ) {
271
+ if reward_cycle_info. is_in_next_prepare_phase ( reward_cycle_info. last_burnchain_block_height )
272
+ {
257
273
self . refresh_signer_config ( current_reward_cycle. saturating_add ( 1 ) ) ;
258
274
}
259
275
self . current_reward_cycle_info = Some ( reward_cycle_info) ;
@@ -270,18 +286,34 @@ impl RunLoop {
270
286
. current_reward_cycle_info
271
287
. as_mut ( )
272
288
. expect ( "FATAL: cannot be an initialized signer with no reward cycle info." ) ;
289
+ let current_reward_cycle = reward_cycle_info. reward_cycle ;
290
+ let block_reward_cycle = reward_cycle_info. get_reward_cycle ( current_burn_block_height) ;
291
+
273
292
// First ensure we refresh our view of the current reward cycle information
274
- if !reward_cycle_info . is_in_reward_cycle ( current_burn_block_height ) {
293
+ if block_reward_cycle != current_reward_cycle {
275
294
let new_reward_cycle_info = retry_with_exponential_backoff ( || {
276
- self . stacks_client
295
+ let info = self
296
+ . stacks_client
277
297
. get_current_reward_cycle_info ( )
278
- . map_err ( backoff:: Error :: transient)
298
+ . map_err ( backoff:: Error :: transient) ?;
299
+ if info. reward_cycle < block_reward_cycle {
300
+ // If the stacks-node is still processing the burn block, the /v2/pox endpoint
301
+ // may return the previous reward cycle. In this case, we should retry.
302
+ return Err ( backoff:: Error :: transient ( ClientError :: InvalidResponse (
303
+ format ! ( "Received reward cycle ({}) does not match the expected reward cycle ({}) for block {}." ,
304
+ info. reward_cycle,
305
+ block_reward_cycle,
306
+ current_burn_block_height
307
+ ) ,
308
+ ) ) ) ;
309
+ }
310
+ Ok ( info)
279
311
} ) ?;
280
312
* reward_cycle_info = new_reward_cycle_info;
281
313
}
282
314
let current_reward_cycle = reward_cycle_info. reward_cycle ;
283
315
// We should only attempt to refresh the signer if we are not configured for the next reward cycle yet and we received a new burn block for its prepare phase
284
- if reward_cycle_info. is_in_prepare_phase ( current_burn_block_height) {
316
+ if reward_cycle_info. is_in_next_prepare_phase ( current_burn_block_height) {
285
317
let next_reward_cycle = current_reward_cycle. saturating_add ( 1 ) ;
286
318
if self
287
319
. stacks_signers
@@ -533,4 +565,67 @@ mod tests {
533
565
. wrapping_add( 1 )
534
566
) ) ;
535
567
}
568
+
569
+ #[ test]
570
+ fn is_in_next_prepare_phase ( ) {
571
+ let reward_cycle_info = RewardCycleInfo {
572
+ reward_cycle : 5 ,
573
+ reward_cycle_length : 10 ,
574
+ prepare_phase_block_length : 5 ,
575
+ first_burnchain_block_height : 0 ,
576
+ last_burnchain_block_height : 50 ,
577
+ } ;
578
+
579
+ assert ! ( !reward_cycle_info. is_in_next_prepare_phase( 49 ) ) ;
580
+ assert ! ( !reward_cycle_info. is_in_next_prepare_phase( 50 ) ) ;
581
+ assert ! ( !reward_cycle_info. is_in_next_prepare_phase( 51 ) ) ;
582
+ assert ! ( !reward_cycle_info. is_in_next_prepare_phase( 52 ) ) ;
583
+ assert ! ( !reward_cycle_info. is_in_next_prepare_phase( 53 ) ) ;
584
+ assert ! ( !reward_cycle_info. is_in_next_prepare_phase( 54 ) ) ;
585
+ assert ! ( reward_cycle_info. is_in_next_prepare_phase( 55 ) ) ;
586
+ assert ! ( reward_cycle_info. is_in_next_prepare_phase( 56 ) ) ;
587
+ assert ! ( reward_cycle_info. is_in_next_prepare_phase( 57 ) ) ;
588
+ assert ! ( reward_cycle_info. is_in_next_prepare_phase( 58 ) ) ;
589
+ assert ! ( reward_cycle_info. is_in_next_prepare_phase( 59 ) ) ;
590
+ assert ! ( !reward_cycle_info. is_in_next_prepare_phase( 60 ) ) ;
591
+ assert ! ( !reward_cycle_info. is_in_next_prepare_phase( 61 ) ) ;
592
+
593
+ let rand_byte: u8 = std:: cmp:: max ( 1 , thread_rng ( ) . gen ( ) ) ;
594
+ let prepare_phase_block_length = rand_byte as u64 ;
595
+ // Ensure the reward cycle is not close to u64 Max to prevent overflow when adding prepare phase len
596
+ let reward_cycle_length = ( std:: cmp:: max (
597
+ prepare_phase_block_length. wrapping_add ( 1 ) ,
598
+ thread_rng ( ) . next_u32 ( ) as u64 ,
599
+ ) )
600
+ . wrapping_add ( prepare_phase_block_length) ;
601
+ let reward_cycle_phase_block_length =
602
+ reward_cycle_length. wrapping_sub ( prepare_phase_block_length) ;
603
+ let first_burnchain_block_height = std:: cmp:: max ( 1u8 , thread_rng ( ) . gen ( ) ) as u64 ;
604
+ let last_burnchain_block_height = thread_rng ( ) . gen_range (
605
+ first_burnchain_block_height
606
+ ..first_burnchain_block_height
607
+ . wrapping_add ( reward_cycle_length)
608
+ . wrapping_sub ( prepare_phase_block_length) ,
609
+ ) ;
610
+ let blocks_mined = last_burnchain_block_height. wrapping_sub ( first_burnchain_block_height) ;
611
+ let reward_cycle = blocks_mined / reward_cycle_length;
612
+
613
+ let reward_cycle_info = RewardCycleInfo {
614
+ reward_cycle,
615
+ reward_cycle_length,
616
+ prepare_phase_block_length,
617
+ first_burnchain_block_height,
618
+ last_burnchain_block_height,
619
+ } ;
620
+
621
+ for i in 0 ..reward_cycle_length {
622
+ if i < reward_cycle_phase_block_length {
623
+ assert ! ( !reward_cycle_info
624
+ . is_in_next_prepare_phase( first_burnchain_block_height. wrapping_add( i) ) ) ;
625
+ } else {
626
+ assert ! ( reward_cycle_info
627
+ . is_in_next_prepare_phase( first_burnchain_block_height. wrapping_add( i) ) ) ;
628
+ }
629
+ }
630
+ }
536
631
}
0 commit comments