Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

grandpa: cleanup stale entries in set id session mapping #13237

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/node-template/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ impl pallet_grandpa::Config for Runtime {

type WeightInfo = ();
type MaxAuthorities = ConstU32<32>;
type MaxSetIdSessionEntries = ConstU64<0>;
}

impl pallet_timestamp::Config for Runtime {
Expand Down
5 changes: 5 additions & 0 deletions bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1313,6 +1313,10 @@ impl pallet_authority_discovery::Config for Runtime {
type MaxAuthorities = MaxAuthorities;
}

parameter_types! {
pub const MaxSetIdSessionEntries: u32 = BondingDuration::get() * SessionsPerEra::get();
}

impl pallet_grandpa::Config for Runtime {
type RuntimeEvent = RuntimeEvent;

Expand All @@ -1334,6 +1338,7 @@ impl pallet_grandpa::Config for Runtime {

type WeightInfo = ();
type MaxAuthorities = MaxAuthorities;
type MaxSetIdSessionEntries = MaxSetIdSessionEntries;
}

parameter_types! {
Expand Down
30 changes: 26 additions & 4 deletions frame/grandpa/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ pub mod pallet {
/// Max Authorities in use
#[pallet::constant]
type MaxAuthorities: Get<u32>;

/// The maximum number of entries to keep in the set id to session index mapping.
///
/// Since the `SetIdSession` map is only used for validating equivocations this
/// value should relate to the bonding duration of whatever staking system is
/// being used (if any). If equivocation handling is not enabled then this value
/// can be zero.
#[pallet::constant]
type MaxSetIdSessionEntries: Get<u64>;
}

#[pallet::hooks]
Expand Down Expand Up @@ -323,6 +332,12 @@ pub mod pallet {
/// A mapping from grandpa set ID to the index of the *most recent* session for which its
/// members were responsible.
///
/// This is only used for validating equivocation proofs. An equivocation proof must
/// contains a key-ownership proof for a given session, therefore we need a way to tie
/// together sessions and GRANDPA set ids, i.e. we need to validate that a validator
/// was the owner of a given key on a given session, and what the active set ID was
/// during that session.
///
/// TWOX-NOTE: `SetId` is not under user control.
#[pallet::storage]
#[pallet::getter(fn session_for_set)]
Expand Down Expand Up @@ -643,10 +658,17 @@ where
};

if res.is_ok() {
CurrentSetId::<T>::mutate(|s| {
let current_set_id = CurrentSetId::<T>::mutate(|s| {
*s += 1;
*s
})
});

let max_set_id_session_entries = T::MaxSetIdSessionEntries::get().max(1);
if current_set_id >= max_set_id_session_entries {
SetIdSession::<T>::remove(current_set_id - max_set_id_session_entries);
}

current_set_id
} else {
// either the session module signalled that the validators have changed
// or the set was stalled. but since we didn't successfully schedule
Expand All @@ -659,8 +681,8 @@ where
Self::current_set_id()
};

// if we didn't issue a change, we update the mapping to note that the current
// set corresponds to the latest equivalent session (i.e. now).
// update the mapping to note that the current set corresponds to the
// latest equivalent session (i.e. now).
let session_index = <pallet_session::Pallet<T>>::current_index();
SetIdSession::<T>::insert(current_set_id, &session_index);
}
Expand Down
46 changes: 46 additions & 0 deletions frame/grandpa/src/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,51 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use frame_support::{
traits::{Get, OnRuntimeUpgrade},
weights::Weight,
};

use crate::{Config, CurrentSetId, SetIdSession, LOG_TARGET};

/// Version 4.
pub mod v4;

/// This migration will clean up all stale set id -> session entries from the
/// `SetIdSession` storage map, only the latest `max_set_id_session_entries`
/// will be kept.
///
/// This migration should be added with a runtime upgrade that introduces the
/// `MaxSetIdSessionEntries` constant to the pallet (although it could also be
/// done later on).
pub struct CleanupSetIdSessionMap<T>(sp_std::marker::PhantomData<T>);
impl<T: Config> OnRuntimeUpgrade for CleanupSetIdSessionMap<T> {
fn on_runtime_upgrade() -> Weight {
// NOTE: since this migration will loop over all stale entries in the
// map we need to set some cutoff value, otherwise the migration might
// take too long to run. for scenarios where there are that many entries
// to cleanup a multiblock migration will be needed instead.
if CurrentSetId::<T>::get() > 25_000 {
Comment on lines +38 to +42
Copy link
Member

@davxy davxy Jan 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious.
In case we have more than X entries this will bail out entirely.

In this case what is the strategy to cleanup the leftovers?
The note talks about a multi-block migration. How this can be implemented? And who is in charge to implement it? Just asking to prevent it from being forgotten forever 😃

Also at this point can't we at least use a best effort approach and cleanup the first X entries?

Another idea/question (not a super expert of runtime upgrades... but I assume it can work)
If we save in the state a temporary variable to keep track of where we arrived (with the cleanup) so far and we leave this migration code here for the next runtime, then in the worst case after a bunch of upgrades we cleaned everything and at that point we also remove the temporary state variable.

Copy link
Contributor Author

@andresilva andresilva Jan 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just put this (random) cut-off here to prevent any users from shooting themselves in the foot. By "multi-block" migration I mean exactly what you suggested, just keep track of the current state of cleanup and remove N entries every block until it's finished. The reason I didn't implement this is because we don't need it for any of our networks and I figured it was unnecessary to add the extra complexity. I put this note here in case any other user of substrate needs to implement this (TBH even 25000 entries might be fast enough to do in a single block, I didn't test this as our limits are way lower than this ~6500).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! Thank you

log::warn!(
target: LOG_TARGET,
"CleanupSetIdSessionMap migration was aborted since there are too many entries to cleanup."
);

return T::DbWeight::get().reads(1)
}

cleanup_set_id_sesion_map::<T>()
}
}

fn cleanup_set_id_sesion_map<T: Config>() -> Weight {
let until_set_id = CurrentSetId::<T>::get().saturating_sub(T::MaxSetIdSessionEntries::get());

for set_id in 0..=until_set_id {
SetIdSession::<T>::remove(set_id);
}

T::DbWeight::get()
.reads(1)
.saturating_add(T::DbWeight::get().writes(until_set_id + 1))
}
2 changes: 2 additions & 0 deletions frame/grandpa/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ impl pallet_offences::Config for Test {
parameter_types! {
pub const ReportLongevity: u64 =
BondingDuration::get() as u64 * SessionsPerEra::get() as u64 * Period::get();
pub const MaxSetIdSessionEntries: u32 = BondingDuration::get() * SessionsPerEra::get();
}

impl Config for Test {
Expand All @@ -241,6 +242,7 @@ impl Config for Test {

type WeightInfo = ();
type MaxAuthorities = ConstU32<100>;
type MaxSetIdSessionEntries = MaxSetIdSessionEntries;
}

pub fn grandpa_log(log: ConsensusLog<u64>) -> DigestItem {
Expand Down
27 changes: 27 additions & 0 deletions frame/grandpa/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,33 @@ fn on_new_session_doesnt_start_new_set_if_schedule_change_failed() {
});
}

#[test]
fn cleans_up_old_set_id_session_mappings() {
new_test_ext(vec![(1, 1), (2, 1), (3, 1)]).execute_with(|| {
let max_set_id_session_entries = MaxSetIdSessionEntries::get();

start_era(max_set_id_session_entries);

// we should have a session id mapping for all the set ids from
// `max_set_id_session_entries` eras we have observed
for i in 1..=max_set_id_session_entries {
assert!(Grandpa::session_for_set(i as u64).is_some());
}

start_era(max_set_id_session_entries * 2);

// we should keep tracking the new mappings for new eras
for i in max_set_id_session_entries + 1..=max_set_id_session_entries * 2 {
assert!(Grandpa::session_for_set(i as u64).is_some());
}

// but the old ones should have been pruned by now
for i in 1..=max_set_id_session_entries {
assert!(Grandpa::session_for_set(i as u64).is_none());
}
});
}

#[test]
fn always_schedules_a_change_on_new_session_when_stalled() {
new_test_ext(vec![(1, 1), (2, 1), (3, 1)]).execute_with(|| {
Expand Down