@@ -36,6 +36,11 @@ pub use pallet::*;
36
36
// which guarantees that at least one full session has passed before any changes are applied.
37
37
pub ( crate ) const SESSION_DELAY : SessionIndex = 2 ;
38
38
39
+ // `MAX_CANDIDATES_TO_PRUNE` is used to upper bound the number of ancient candidates that can be
40
+ // pruned when a new block is included. This is used to distribute the cost of pruning over
41
+ // multiple blocks rather than pruning all historical candidates upon starting a new session.
42
+ pub ( crate ) const MAX_CANDIDATES_TO_PRUNE : usize = 200 ;
43
+
39
44
#[ frame_support:: pallet]
40
45
pub mod pallet {
41
46
use super :: * ;
@@ -79,6 +84,15 @@ pub mod pallet {
79
84
( T :: BlockNumber , CoreIndex ) ,
80
85
> ;
81
86
87
+ /// The set of all past sessions that have yet to fully pruned. Sessions are added to this set
88
+ /// upon moving to new current session, and removed after all of their included candidates
89
+ /// have been removed. This set is tracked independently so that the cost of removing included
90
+ /// candidates can be amortized over multiple blocks rather than removing all candidates upon
91
+ /// transitioning to a new session.
92
+ #[ pallet:: storage]
93
+ #[ pallet:: getter( fn pruneable_sessions) ]
94
+ pub ( super ) type PruneableSessions < T : Config > = StorageMap < _ , Twox64Concat , SessionIndex , ( ) > ;
95
+
82
96
#[ pallet:: call]
83
97
impl < T : Config > Pallet < T > { }
84
98
}
@@ -129,6 +143,58 @@ impl<T: Config> Pallet<T> {
129
143
Self :: session_index ( ) . saturating_add ( SESSION_DELAY )
130
144
}
131
145
146
+ /// Adds a session that is no longer in the dispute window as pruneable. Subsequent block
147
+ /// inclusions will incrementally remove old candidates to avoid taking the performance hit of
148
+ /// removing all candidates at once.
149
+ pub ( crate ) fn mark_session_pruneable ( session : SessionIndex ) {
150
+ PruneableSessions :: < T > :: insert ( session, ( ) ) ;
151
+ }
152
+
153
+ /// Prunes up to `max_candidates_to_prune` candidates from `IncludedCandidates` that belong to
154
+ /// non-active sessions.
155
+ pub ( crate ) fn prune_ancient_sessions ( max_candidates_to_prune : usize ) {
156
+ let mut to_prune = Vec :: new ( ) ;
157
+ let mut n_candidates = 0 ;
158
+ let mut incomplete_session = None ;
159
+ for session in PruneableSessions :: < T > :: iter_keys ( ) {
160
+ let mut hashes = Vec :: new ( ) ;
161
+ for candidate_hash in IncludedCandidates :: < T > :: iter_key_prefix ( session) {
162
+ // Exit condition when this session still has more candidates to prune; mark the
163
+ // session as incomplete so the remaining candidates can be pruned in a subsequent
164
+ // invocation.
165
+ if n_candidates >= max_candidates_to_prune {
166
+ incomplete_session = Some ( session) ;
167
+ break
168
+ }
169
+ hashes. push ( candidate_hash) ;
170
+ n_candidates += 1 ;
171
+ }
172
+
173
+ to_prune. push ( ( session, hashes) ) ;
174
+ // Exit condition when all candidates from this session were selected for pruning.
175
+ if n_candidates >= max_candidates_to_prune {
176
+ break
177
+ }
178
+ }
179
+
180
+ for ( session, candidate_hashes) in to_prune {
181
+ for candidate_hash in candidate_hashes {
182
+ IncludedCandidates :: < T > :: remove ( session, candidate_hash) ;
183
+ }
184
+
185
+ // Prune the session only if it was not marked as incomplete.
186
+ match incomplete_session {
187
+ Some ( incomplete_session) =>
188
+ if incomplete_session != session {
189
+ PruneableSessions :: < T > :: remove ( session) ;
190
+ } ,
191
+ None => {
192
+ PruneableSessions :: < T > :: remove ( session) ;
193
+ } ,
194
+ }
195
+ }
196
+ }
197
+
132
198
/// Records an included candidate, returning the block height that should be reverted to if the
133
199
/// block is found to be invalid. This method will return `None` if and only if `included_in`
134
200
/// is zero.
@@ -164,9 +230,9 @@ impl<T: Config> Pallet<T> {
164
230
<IncludedCandidates < T > >:: get ( session, candidate_hash)
165
231
}
166
232
167
- /// Prunes all candidates that were included in the `to_prune` session.
168
- pub ( crate ) fn prune_included_candidates ( to_prune : SessionIndex ) {
169
- <IncludedCandidates < T > >:: remove_prefix ( to_prune , None ) ;
233
+ # [ cfg ( test ) ]
234
+ pub ( crate ) fn is_pruneable_session ( session : & SessionIndex ) -> bool {
235
+ <PruneableSessions < T > >:: contains_key ( session )
170
236
}
171
237
172
238
#[ cfg( test) ]
@@ -327,34 +393,174 @@ mod tests {
327
393
}
328
394
329
395
#[ test]
330
- fn prune_included_candidate_removes_all_candidates_with_same_session ( ) {
396
+ fn prune_ancient_sessions_no_incomplete_session ( ) {
331
397
new_test_ext ( MockGenesisConfig :: default ( ) ) . execute_with ( || {
398
+ let session = 1 ;
332
399
let candidate_hash1 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 1 ) ) ;
333
400
let candidate_hash2 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 2 ) ) ;
334
- let candidate_hash3 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 3 ) ) ;
401
+ let block_number = 1 ;
402
+ let core_index = CoreIndex ( 0 ) ;
335
403
336
- assert ! (
337
- ParasShared :: note_included_candidate( 1 , candidate_hash1, 1 , CoreIndex ( 0 ) ) . is_some( )
404
+ assert_eq ! (
405
+ ParasShared :: note_included_candidate(
406
+ session,
407
+ candidate_hash1,
408
+ block_number,
409
+ core_index,
410
+ ) ,
411
+ Some ( block_number - 1 ) ,
338
412
) ;
339
- assert ! (
340
- ParasShared :: note_included_candidate( 1 , candidate_hash2, 1 , CoreIndex ( 0 ) ) . is_some( )
413
+ assert_eq ! (
414
+ ParasShared :: note_included_candidate(
415
+ session,
416
+ candidate_hash2,
417
+ block_number,
418
+ core_index,
419
+ ) ,
420
+ Some ( block_number - 1 ) ,
421
+ ) ;
422
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash1) ) ;
423
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash2) ) ;
424
+
425
+ // Prune before any sessions are marked pruneable.
426
+ ParasShared :: prune_ancient_sessions ( 2 ) ;
427
+
428
+ // Both candidates should still exist.
429
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash1) ) ;
430
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash2) ) ;
431
+
432
+ // Mark the candidates' session as pruneable.
433
+ ParasShared :: mark_session_pruneable ( session) ;
434
+ assert ! ( ParasShared :: is_pruneable_session( & session) ) ;
435
+
436
+ // Prune the sessions, which should remove both candidates. The session should not be
437
+ // marked as incomplete since there are exactly two candidates.
438
+ ParasShared :: prune_ancient_sessions ( 2 ) ;
439
+
440
+ assert ! ( !ParasShared :: is_pruneable_session( & session) ) ;
441
+ assert ! ( !ParasShared :: is_included_candidate( & session, & candidate_hash1) ) ;
442
+ assert ! ( !ParasShared :: is_included_candidate( & session, & candidate_hash2) ) ;
443
+ } )
444
+ }
445
+
446
+ #[ test]
447
+ fn prune_ancient_sessions_incomplete_session ( ) {
448
+ new_test_ext ( MockGenesisConfig :: default ( ) ) . execute_with ( || {
449
+ let session = 1 ;
450
+ let candidate_hash1 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 1 ) ) ;
451
+ let candidate_hash2 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 2 ) ) ;
452
+ let block_number = 1 ;
453
+ let core_index = CoreIndex ( 0 ) ;
454
+
455
+ assert_eq ! (
456
+ ParasShared :: note_included_candidate(
457
+ session,
458
+ candidate_hash1,
459
+ block_number,
460
+ core_index,
461
+ ) ,
462
+ Some ( block_number - 1 ) ,
341
463
) ;
342
- assert ! (
343
- ParasShared :: note_included_candidate( 2 , candidate_hash3, 2 , CoreIndex ( 0 ) ) . is_some( )
464
+ assert_eq ! (
465
+ ParasShared :: note_included_candidate(
466
+ session,
467
+ candidate_hash2,
468
+ block_number,
469
+ core_index,
470
+ ) ,
471
+ Some ( block_number - 1 ) ,
344
472
) ;
473
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash1) ) ;
474
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash2) ) ;
345
475
346
- assert_eq ! ( ParasShared :: included_candidates_iter_prefix ( 1 ) . count ( ) , 2 ) ;
347
- assert_eq ! ( ParasShared :: included_candidates_iter_prefix ( 2 ) . count ( ) , 1 ) ;
476
+ // Prune before any sessions are marked pruneable.
477
+ ParasShared :: prune_ancient_sessions ( 1 ) ;
348
478
349
- ParasShared :: prune_included_candidates ( 1 ) ;
479
+ // Both candidates should still exist.
480
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash1) ) ;
481
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash2) ) ;
350
482
351
- assert_eq ! ( ParasShared :: included_candidates_iter_prefix( 1 ) . count( ) , 0 ) ;
352
- assert_eq ! ( ParasShared :: included_candidates_iter_prefix( 2 ) . count( ) , 1 ) ;
483
+ // Mark the candidates' session as pruneable.
484
+ ParasShared :: mark_session_pruneable ( session) ;
485
+ assert ! ( ParasShared :: is_pruneable_session( & session) ) ;
353
486
354
- ParasShared :: prune_included_candidates ( 2 ) ;
487
+ // Prune the sessions, which should remove one of the candidates. The session will be
488
+ // marked as incomplete so the session should remain unpruned.
489
+ ParasShared :: prune_ancient_sessions ( 1 ) ;
355
490
356
- assert_eq ! ( ParasShared :: included_candidates_iter_prefix( 1 ) . count( ) , 0 ) ;
357
- assert_eq ! ( ParasShared :: included_candidates_iter_prefix( 2 ) . count( ) , 0 ) ;
358
- } ) ;
491
+ assert ! ( ParasShared :: is_pruneable_session( & session) ) ;
492
+ assert ! ( !ParasShared :: is_included_candidate( & session, & candidate_hash1) ) ;
493
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash2) ) ;
494
+ } )
495
+ }
496
+
497
+ #[ test]
498
+ fn prune_ancient_sessions_complete_and_incomplete_sessions ( ) {
499
+ new_test_ext ( MockGenesisConfig :: default ( ) ) . execute_with ( || {
500
+ let session1 = 1 ;
501
+ let session2 = 2 ;
502
+ let candidate_hash1 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 1 ) ) ;
503
+ let candidate_hash2 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 2 ) ) ;
504
+ let candidate_hash3 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 3 ) ) ;
505
+ let block_number1 = 1 ;
506
+ let block_number2 = 2 ;
507
+ let core_index = CoreIndex ( 0 ) ;
508
+
509
+ assert_eq ! (
510
+ ParasShared :: note_included_candidate(
511
+ session1,
512
+ candidate_hash1,
513
+ block_number1,
514
+ core_index,
515
+ ) ,
516
+ Some ( block_number1 - 1 ) ,
517
+ ) ;
518
+ assert_eq ! (
519
+ ParasShared :: note_included_candidate(
520
+ session2,
521
+ candidate_hash2,
522
+ block_number2,
523
+ core_index,
524
+ ) ,
525
+ Some ( block_number2 - 1 ) ,
526
+ ) ;
527
+ assert_eq ! (
528
+ ParasShared :: note_included_candidate(
529
+ session2,
530
+ candidate_hash3,
531
+ block_number2,
532
+ core_index,
533
+ ) ,
534
+ Some ( block_number2 - 1 ) ,
535
+ ) ;
536
+ assert ! ( ParasShared :: is_included_candidate( & session1, & candidate_hash1) ) ;
537
+ assert ! ( ParasShared :: is_included_candidate( & session2, & candidate_hash2) ) ;
538
+ assert ! ( ParasShared :: is_included_candidate( & session2, & candidate_hash3) ) ;
539
+
540
+ // Prune before any sessions are marked pruneable.
541
+ ParasShared :: prune_ancient_sessions ( 2 ) ;
542
+
543
+ // Both candidates should still exist.
544
+ assert ! ( ParasShared :: is_included_candidate( & session1, & candidate_hash1) ) ;
545
+ assert ! ( ParasShared :: is_included_candidate( & session2, & candidate_hash2) ) ;
546
+ assert ! ( ParasShared :: is_included_candidate( & session2, & candidate_hash3) ) ;
547
+
548
+ // Mark the candidates' session as pruneable.
549
+ ParasShared :: mark_session_pruneable ( session1) ;
550
+ ParasShared :: mark_session_pruneable ( session2) ;
551
+ assert ! ( ParasShared :: is_pruneable_session( & session1) ) ;
552
+ assert ! ( ParasShared :: is_pruneable_session( & session2) ) ;
553
+
554
+ // Prune the sessions, which should remove one candidate from each session. The first
555
+ // session should be pruned while the second session will be marked as incomplete, and
556
+ // so should remain in the set of pruneable sessions.
557
+ ParasShared :: prune_ancient_sessions ( 2 ) ;
558
+
559
+ assert ! ( !ParasShared :: is_pruneable_session( & session1) ) ;
560
+ assert ! ( ParasShared :: is_pruneable_session( & session2) ) ;
561
+ assert ! ( !ParasShared :: is_included_candidate( & session1, & candidate_hash1) ) ;
562
+ assert ! ( ParasShared :: is_included_candidate( & session2, & candidate_hash2) ) ;
563
+ assert ! ( !ParasShared :: is_included_candidate( & session2, & candidate_hash3) ) ;
564
+ } )
359
565
}
360
566
}
0 commit comments