@@ -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,55 @@ 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) if incomplete_session != session =>
188
+ PruneableSessions :: < T > :: remove ( session) ,
189
+ None => PruneableSessions :: < T > :: remove ( session) ,
190
+ _ => { } ,
191
+ }
192
+ }
193
+ }
194
+
132
195
/// Records an included candidate, returning the block height that should be reverted to if the
133
196
/// block is found to be invalid. This method will return `None` if and only if `included_in`
134
197
/// is zero.
@@ -164,9 +227,9 @@ impl<T: Config> Pallet<T> {
164
227
<IncludedCandidates < T > >:: get ( session, candidate_hash)
165
228
}
166
229
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 ) ;
230
+ # [ cfg ( test ) ]
231
+ pub ( crate ) fn is_pruneable_session ( session : & SessionIndex ) -> bool {
232
+ <PruneableSessions < T > >:: contains_key ( session )
170
233
}
171
234
172
235
#[ cfg( test) ]
@@ -327,34 +390,174 @@ mod tests {
327
390
}
328
391
329
392
#[ test]
330
- fn prune_included_candidate_removes_all_candidates_with_same_session ( ) {
393
+ fn prune_ancient_sessions_no_incomplete_session ( ) {
331
394
new_test_ext ( MockGenesisConfig :: default ( ) ) . execute_with ( || {
395
+ let session = 1 ;
332
396
let candidate_hash1 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 1 ) ) ;
333
397
let candidate_hash2 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 2 ) ) ;
334
- let candidate_hash3 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 3 ) ) ;
398
+ let block_number = 1 ;
399
+ let core_index = CoreIndex ( 0 ) ;
335
400
336
- assert ! (
337
- ParasShared :: note_included_candidate( 1 , candidate_hash1, 1 , CoreIndex ( 0 ) ) . is_some( )
401
+ assert_eq ! (
402
+ ParasShared :: note_included_candidate(
403
+ session,
404
+ candidate_hash1,
405
+ block_number,
406
+ core_index,
407
+ ) ,
408
+ Some ( block_number - 1 ) ,
338
409
) ;
339
- assert ! (
340
- ParasShared :: note_included_candidate( 1 , candidate_hash2, 1 , CoreIndex ( 0 ) ) . is_some( )
410
+ assert_eq ! (
411
+ ParasShared :: note_included_candidate(
412
+ session,
413
+ candidate_hash2,
414
+ block_number,
415
+ core_index,
416
+ ) ,
417
+ Some ( block_number - 1 ) ,
418
+ ) ;
419
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash1) ) ;
420
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash2) ) ;
421
+
422
+ // Prune before any sessions are marked pruneable.
423
+ ParasShared :: prune_ancient_sessions ( 2 ) ;
424
+
425
+ // Both candidates should still exist.
426
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash1) ) ;
427
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash2) ) ;
428
+
429
+ // Mark the candidates' session as pruneable.
430
+ ParasShared :: mark_session_pruneable ( session) ;
431
+ assert ! ( ParasShared :: is_pruneable_session( & session) ) ;
432
+
433
+ // Prune the sessions, which should remove both candidates. The session should not be
434
+ // marked as incomplete since there are exactly two candidates.
435
+ ParasShared :: prune_ancient_sessions ( 2 ) ;
436
+
437
+ assert ! ( !ParasShared :: is_pruneable_session( & session) ) ;
438
+ assert ! ( !ParasShared :: is_included_candidate( & session, & candidate_hash1) ) ;
439
+ assert ! ( !ParasShared :: is_included_candidate( & session, & candidate_hash2) ) ;
440
+ } )
441
+ }
442
+
443
+ #[ test]
444
+ fn prune_ancient_sessions_incomplete_session ( ) {
445
+ new_test_ext ( MockGenesisConfig :: default ( ) ) . execute_with ( || {
446
+ let session = 1 ;
447
+ let candidate_hash1 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 1 ) ) ;
448
+ let candidate_hash2 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 2 ) ) ;
449
+ let block_number = 1 ;
450
+ let core_index = CoreIndex ( 0 ) ;
451
+
452
+ assert_eq ! (
453
+ ParasShared :: note_included_candidate(
454
+ session,
455
+ candidate_hash1,
456
+ block_number,
457
+ core_index,
458
+ ) ,
459
+ Some ( block_number - 1 ) ,
341
460
) ;
342
- assert ! (
343
- ParasShared :: note_included_candidate( 2 , candidate_hash3, 2 , CoreIndex ( 0 ) ) . is_some( )
461
+ assert_eq ! (
462
+ ParasShared :: note_included_candidate(
463
+ session,
464
+ candidate_hash2,
465
+ block_number,
466
+ core_index,
467
+ ) ,
468
+ Some ( block_number - 1 ) ,
344
469
) ;
470
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash1) ) ;
471
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash2) ) ;
345
472
346
- assert_eq ! ( ParasShared :: included_candidates_iter_prefix ( 1 ) . count ( ) , 2 ) ;
347
- assert_eq ! ( ParasShared :: included_candidates_iter_prefix ( 2 ) . count ( ) , 1 ) ;
473
+ // Prune before any sessions are marked pruneable.
474
+ ParasShared :: prune_ancient_sessions ( 1 ) ;
348
475
349
- ParasShared :: prune_included_candidates ( 1 ) ;
476
+ // Both candidates should still exist.
477
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash1) ) ;
478
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash2) ) ;
350
479
351
- assert_eq ! ( ParasShared :: included_candidates_iter_prefix( 1 ) . count( ) , 0 ) ;
352
- assert_eq ! ( ParasShared :: included_candidates_iter_prefix( 2 ) . count( ) , 1 ) ;
480
+ // Mark the candidates' session as pruneable.
481
+ ParasShared :: mark_session_pruneable ( session) ;
482
+ assert ! ( ParasShared :: is_pruneable_session( & session) ) ;
353
483
354
- ParasShared :: prune_included_candidates ( 2 ) ;
484
+ // Prune the sessions, which should remove one of the candidates. The session will be
485
+ // marked as incomplete so the session should remain unpruned.
486
+ ParasShared :: prune_ancient_sessions ( 1 ) ;
355
487
356
- assert_eq ! ( ParasShared :: included_candidates_iter_prefix( 1 ) . count( ) , 0 ) ;
357
- assert_eq ! ( ParasShared :: included_candidates_iter_prefix( 2 ) . count( ) , 0 ) ;
358
- } ) ;
488
+ assert ! ( ParasShared :: is_pruneable_session( & session) ) ;
489
+ assert ! ( !ParasShared :: is_included_candidate( & session, & candidate_hash1) ) ;
490
+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash2) ) ;
491
+ } )
492
+ }
493
+
494
+ #[ test]
495
+ fn prune_ancient_sessions_complete_and_incomplete_sessions ( ) {
496
+ new_test_ext ( MockGenesisConfig :: default ( ) ) . execute_with ( || {
497
+ let session1 = 1 ;
498
+ let session2 = 2 ;
499
+ let candidate_hash1 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 1 ) ) ;
500
+ let candidate_hash2 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 2 ) ) ;
501
+ let candidate_hash3 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 3 ) ) ;
502
+ let block_number1 = 1 ;
503
+ let block_number2 = 2 ;
504
+ let core_index = CoreIndex ( 0 ) ;
505
+
506
+ assert_eq ! (
507
+ ParasShared :: note_included_candidate(
508
+ session1,
509
+ candidate_hash1,
510
+ block_number1,
511
+ core_index,
512
+ ) ,
513
+ Some ( block_number1 - 1 ) ,
514
+ ) ;
515
+ assert_eq ! (
516
+ ParasShared :: note_included_candidate(
517
+ session2,
518
+ candidate_hash2,
519
+ block_number2,
520
+ core_index,
521
+ ) ,
522
+ Some ( block_number2 - 1 ) ,
523
+ ) ;
524
+ assert_eq ! (
525
+ ParasShared :: note_included_candidate(
526
+ session2,
527
+ candidate_hash3,
528
+ block_number2,
529
+ core_index,
530
+ ) ,
531
+ Some ( block_number2 - 1 ) ,
532
+ ) ;
533
+ assert ! ( ParasShared :: is_included_candidate( & session1, & candidate_hash1) ) ;
534
+ assert ! ( ParasShared :: is_included_candidate( & session2, & candidate_hash2) ) ;
535
+ assert ! ( ParasShared :: is_included_candidate( & session2, & candidate_hash3) ) ;
536
+
537
+ // Prune before any sessions are marked pruneable.
538
+ ParasShared :: prune_ancient_sessions ( 2 ) ;
539
+
540
+ // Both candidates should still exist.
541
+ assert ! ( ParasShared :: is_included_candidate( & session1, & candidate_hash1) ) ;
542
+ assert ! ( ParasShared :: is_included_candidate( & session2, & candidate_hash2) ) ;
543
+ assert ! ( ParasShared :: is_included_candidate( & session2, & candidate_hash3) ) ;
544
+
545
+ // Mark the candidates' session as pruneable.
546
+ ParasShared :: mark_session_pruneable ( session1) ;
547
+ ParasShared :: mark_session_pruneable ( session2) ;
548
+ assert ! ( ParasShared :: is_pruneable_session( & session1) ) ;
549
+ assert ! ( ParasShared :: is_pruneable_session( & session2) ) ;
550
+
551
+ // Prune the sessions, which should remove one candidate from each session. The first
552
+ // session should be pruned while the second session will be marked as incomplete, and
553
+ // so should remain in the set of pruneable sessions.
554
+ ParasShared :: prune_ancient_sessions ( 2 ) ;
555
+
556
+ assert ! ( !ParasShared :: is_pruneable_session( & session1) ) ;
557
+ assert ! ( ParasShared :: is_pruneable_session( & session2) ) ;
558
+ assert ! ( !ParasShared :: is_included_candidate( & session1, & candidate_hash1) ) ;
559
+ assert ! ( ParasShared :: is_included_candidate( & session2, & candidate_hash2) ) ;
560
+ assert ! ( !ParasShared :: is_included_candidate( & session2, & candidate_hash3) ) ;
561
+ } )
359
562
}
360
563
}
0 commit comments