@@ -36,6 +36,11 @@ pub use pallet::*;
3636// which guarantees that at least one full session has passed before any changes are applied.
3737pub ( crate ) const SESSION_DELAY : SessionIndex = 2 ;
3838
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+
3944#[ frame_support:: pallet]
4045pub mod pallet {
4146 use super :: * ;
@@ -79,6 +84,15 @@ pub mod pallet {
7984 ( T :: BlockNumber , CoreIndex ) ,
8085 > ;
8186
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+
8296 #[ pallet:: call]
8397 impl < T : Config > Pallet < T > { }
8498}
@@ -129,6 +143,55 @@ impl<T: Config> Pallet<T> {
129143 Self :: session_index ( ) . saturating_add ( SESSION_DELAY )
130144 }
131145
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+
132195 /// Records an included candidate, returning the block height that should be reverted to if the
133196 /// block is found to be invalid. This method will return `None` if and only if `included_in`
134197 /// is zero.
@@ -164,9 +227,9 @@ impl<T: Config> Pallet<T> {
164227 <IncludedCandidates < T > >:: get ( session, candidate_hash)
165228 }
166229
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 )
170233 }
171234
172235 #[ cfg( test) ]
@@ -327,34 +390,174 @@ mod tests {
327390 }
328391
329392 #[ test]
330- fn prune_included_candidate_removes_all_candidates_with_same_session ( ) {
393+ fn prune_ancient_sessions_no_incomplete_session ( ) {
331394 new_test_ext ( MockGenesisConfig :: default ( ) ) . execute_with ( || {
395+ let session = 1 ;
332396 let candidate_hash1 = CandidateHash ( sp_core:: H256 :: repeat_byte ( 1 ) ) ;
333397 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 ) ;
335400
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 ) ,
338409 ) ;
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 ) ,
341460 ) ;
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 ) ,
344469 ) ;
470+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash1) ) ;
471+ assert ! ( ParasShared :: is_included_candidate( & session, & candidate_hash2) ) ;
345472
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 ) ;
348475
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) ) ;
350479
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) ) ;
353483
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 ) ;
355487
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+ } )
359562 }
360563}
0 commit comments