// Copyright 2020 Parity Technologies (UK) Ltd. // This file is part of Polkadot. // Polkadot is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Polkadot is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . //! The session info pallet provides information about validator sets //! from prior sessions needed for approvals and disputes. //! //! See https://w3f.github.io/parachain-implementers-guide/runtime/session_info.html. use crate::{ configuration, paras, scheduler, shared, util::{take_active_subset, take_active_subset_and_inactive}, }; use frame_support::{pallet_prelude::*, traits::OneSessionHandler}; use primitives::{ v1::{AssignmentId, AuthorityDiscoveryId, SessionIndex}, v2::SessionInfo, }; use sp_std::vec::Vec; pub use pallet::*; pub mod migration; #[frame_support::pallet] pub mod pallet { use super::*; use frame_system::pallet_prelude::BlockNumberFor; #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] #[pallet::storage_version(migration::STORAGE_VERSION)] pub struct Pallet(_); #[pallet::config] pub trait Config: frame_system::Config + configuration::Config + shared::Config + paras::Config + scheduler::Config + AuthorityDiscoveryConfig { } /// Assignment keys for the current session. /// Note that this API is private due to it being prone to 'off-by-one' at session boundaries. /// When in doubt, use `Sessions` API instead. #[pallet::storage] pub(super) type AssignmentKeysUnsafe = StorageValue<_, Vec, ValueQuery>; /// The earliest session for which previous session info is stored. #[pallet::storage] #[pallet::getter(fn earliest_stored_session)] pub(crate) type EarliestStoredSession = StorageValue<_, SessionIndex, ValueQuery>; /// Session information in a rolling window. /// Should have an entry in range `EarliestStoredSession..=CurrentSessionIndex`. /// Does not have any entries before the session index in the first session change notification. #[pallet::storage] #[pallet::getter(fn session_info)] pub(crate) type Sessions = StorageMap<_, Identity, SessionIndex, SessionInfo>; #[pallet::hooks] impl Hooks> for Pallet { fn on_runtime_upgrade() -> Weight { migration::migrate_to_latest::() } } } /// An abstraction for the authority discovery pallet /// to help with mock testing. pub trait AuthorityDiscoveryConfig { /// Retrieve authority identifiers of the current authority set in canonical ordering. fn authorities() -> Vec; } impl AuthorityDiscoveryConfig for T { fn authorities() -> Vec { >::current_authorities().to_vec() } } impl Pallet { /// Handle an incoming session change. pub(crate) fn initializer_on_new_session( notification: &crate::initializer::SessionChangeNotification, ) { let config = >::config(); let dispute_period = config.dispute_period; let validators = notification.validators.clone(); let discovery_keys = ::authorities(); let assignment_keys = AssignmentKeysUnsafe::::get(); let active_set = >::active_validator_indices(); let validator_groups = >::validator_groups(); let n_cores = >::availability_cores().len() as u32; let zeroth_delay_tranche_width = config.zeroth_delay_tranche_width; let relay_vrf_modulo_samples = config.relay_vrf_modulo_samples; let n_delay_tranches = config.n_delay_tranches; let no_show_slots = config.no_show_slots; let needed_approvals = config.needed_approvals; let new_session_index = notification.session_index; let random_seed = notification.random_seed; let old_earliest_stored_session = EarliestStoredSession::::get(); let new_earliest_stored_session = new_session_index.saturating_sub(dispute_period); let new_earliest_stored_session = core::cmp::max(new_earliest_stored_session, old_earliest_stored_session); // remove all entries from `Sessions` from the previous value up to the new value // avoid a potentially heavy loop when introduced on a live chain if old_earliest_stored_session != 0 || Sessions::::get(0).is_some() { for idx in old_earliest_stored_session..new_earliest_stored_session { Sessions::::remove(&idx); } // update `EarliestStoredSession` based on `config.dispute_period` EarliestStoredSession::::set(new_earliest_stored_session); } else { // just introduced on a live chain EarliestStoredSession::::set(new_session_index); } // create a new entry in `Sessions` with information about the current session let new_session_info = SessionInfo { validators, // these are from the notification and are thus already correct. discovery_keys: take_active_subset_and_inactive(&active_set, &discovery_keys), assignment_keys: take_active_subset(&active_set, &assignment_keys), validator_groups, n_cores, zeroth_delay_tranche_width, relay_vrf_modulo_samples, n_delay_tranches, no_show_slots, needed_approvals, active_validator_indices: active_set, random_seed, dispute_period, }; Sessions::::insert(&new_session_index, &new_session_info); } /// Called by the initializer to initialize the session info pallet. pub(crate) fn initializer_initialize(_now: T::BlockNumber) -> Weight { 0 } /// Called by the initializer to finalize the session info pallet. pub(crate) fn initializer_finalize() {} } impl sp_runtime::BoundToRuntimeAppPublic for Pallet { type Public = AssignmentId; } impl OneSessionHandler for Pallet { type Key = AssignmentId; fn on_genesis_session<'a, I: 'a>(_validators: I) where I: Iterator, { } fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, _queued: I) where I: Iterator, { let assignment_keys: Vec<_> = validators.map(|(_, v)| v).collect(); AssignmentKeysUnsafe::::set(assignment_keys); } fn on_disabled(_i: u32) {} } #[cfg(test)] mod tests { use super::*; use crate::{ configuration::HostConfiguration, initializer::SessionChangeNotification, mock::{ new_test_ext, Configuration, MockGenesisConfig, Origin, ParasShared, SessionInfo, System, Test, }, util::take_active_subset, }; use keyring::Sr25519Keyring; use primitives::v1::{BlockNumber, ValidatorId, ValidatorIndex}; fn run_to_block( to: BlockNumber, new_session: impl Fn(BlockNumber) -> Option>, ) { while System::block_number() < to { let b = System::block_number(); SessionInfo::initializer_finalize(); ParasShared::initializer_finalize(); Configuration::initializer_finalize(); if let Some(notification) = new_session(b + 1) { Configuration::initializer_on_new_session(¬ification.session_index); ParasShared::initializer_on_new_session( notification.session_index, notification.random_seed, ¬ification.new_config, notification.validators.clone(), ); SessionInfo::initializer_on_new_session(¬ification); } System::on_finalize(b); System::on_initialize(b + 1); System::set_block_number(b + 1); Configuration::initializer_initialize(b + 1); ParasShared::initializer_initialize(b + 1); SessionInfo::initializer_initialize(b + 1); } } fn default_config() -> HostConfiguration { HostConfiguration { parathread_cores: 1, dispute_period: 2, needed_approvals: 3, ..Default::default() } } fn genesis_config() -> MockGenesisConfig { MockGenesisConfig { configuration: configuration::GenesisConfig { config: default_config(), ..Default::default() }, ..Default::default() } } fn session_changes(n: BlockNumber) -> Option> { if n % 10 == 0 { Some(SessionChangeNotification { session_index: n / 10, ..Default::default() }) } else { None } } fn new_session_every_block(n: BlockNumber) -> Option> { Some(SessionChangeNotification { session_index: n, ..Default::default() }) } #[test] fn session_pruning_is_based_on_dispute_period() { new_test_ext(genesis_config()).execute_with(|| { // Dispute period starts at 2 let config = Configuration::config(); assert_eq!(config.dispute_period, 2); // Move to session 10 run_to_block(100, session_changes); // Earliest stored session is 10 - 2 = 8 assert_eq!(EarliestStoredSession::::get(), 8); // Pruning works as expected assert!(Sessions::::get(7).is_none()); assert!(Sessions::::get(8).is_some()); assert!(Sessions::::get(9).is_some()); // changing `dispute_period` works let dispute_period = 5; Configuration::set_dispute_period(Origin::root(), dispute_period).unwrap(); // Dispute period does not automatically change let config = Configuration::config(); assert_eq!(config.dispute_period, 2); // Two sessions later it will though run_to_block(120, session_changes); let config = Configuration::config(); assert_eq!(config.dispute_period, 5); run_to_block(200, session_changes); assert_eq!(EarliestStoredSession::::get(), 20 - dispute_period); // Increase dispute period even more let new_dispute_period = 16; Configuration::set_dispute_period(Origin::root(), new_dispute_period).unwrap(); run_to_block(210, session_changes); assert_eq!(EarliestStoredSession::::get(), 21 - dispute_period); // Two sessions later it kicks in run_to_block(220, session_changes); let config = Configuration::config(); assert_eq!(config.dispute_period, 16); // Earliest session stays the same assert_eq!(EarliestStoredSession::::get(), 21 - dispute_period); // We still don't have enough stored sessions to start pruning run_to_block(300, session_changes); assert_eq!(EarliestStoredSession::::get(), 21 - dispute_period); // now we do run_to_block(420, session_changes); assert_eq!(EarliestStoredSession::::get(), 42 - new_dispute_period); }) } #[test] fn session_info_is_based_on_config() { new_test_ext(genesis_config()).execute_with(|| { run_to_block(1, new_session_every_block); let session = Sessions::::get(&1).unwrap(); assert_eq!(session.needed_approvals, 3); // change some param Configuration::set_needed_approvals(Origin::root(), 42).unwrap(); // 2 sessions later run_to_block(3, new_session_every_block); let session = Sessions::::get(&3).unwrap(); assert_eq!(session.needed_approvals, 42); }) } #[test] fn session_info_active_subsets() { let unscrambled = vec![ Sr25519Keyring::Alice, Sr25519Keyring::Bob, Sr25519Keyring::Charlie, Sr25519Keyring::Dave, Sr25519Keyring::Eve, ]; let active_set = vec![ValidatorIndex(4), ValidatorIndex(0), ValidatorIndex(2)]; let unscrambled_validators: Vec = unscrambled.iter().map(|v| v.public().into()).collect(); let unscrambled_discovery: Vec = unscrambled.iter().map(|v| v.public().into()).collect(); let unscrambled_assignment: Vec = unscrambled.iter().map(|v| v.public().into()).collect(); let validators = take_active_subset(&active_set, &unscrambled_validators); new_test_ext(genesis_config()).execute_with(|| { ParasShared::set_active_validators_with_indices(active_set.clone(), validators.clone()); assert_eq!(ParasShared::active_validator_indices(), active_set); AssignmentKeysUnsafe::::set(unscrambled_assignment.clone()); crate::mock::set_discovery_authorities(unscrambled_discovery.clone()); assert_eq!(::authorities(), unscrambled_discovery); // invoke directly, because `run_to_block` will invoke `Shared` and clobber our // values. SessionInfo::initializer_on_new_session(&SessionChangeNotification { session_index: 1, validators: validators.clone(), ..Default::default() }); let session = Sessions::::get(&1).unwrap(); assert_eq!(session.validators, validators); assert_eq!( session.discovery_keys, take_active_subset_and_inactive(&active_set, &unscrambled_discovery), ); assert_eq!( session.assignment_keys, take_active_subset(&active_set, &unscrambled_assignment), ); }) } }