diff --git a/.github/scripts/run_consensus.sh b/.github/scripts/run_consensus.sh index 738b9bfd82..a7c39c0b07 100755 --- a/.github/scripts/run_consensus.sh +++ b/.github/scripts/run_consensus.sh @@ -7,6 +7,9 @@ set -euo pipefail NODE_COUNT=5 MIN_VALIDATOR_COUNT=4 +# default minimum validator count +MIN_VALIDATOR_COUNT=4 + export NODE_IMAGE=aleph-node:latest mkdir -p docker/data/ diff --git a/bin/runtime/src/lib.rs b/bin/runtime/src/lib.rs index 70b1c12455..84b55a9c34 100644 --- a/bin/runtime/src/lib.rs +++ b/bin/runtime/src/lib.rs @@ -35,7 +35,7 @@ use pallet_transaction_payment::{CurrencyAdapter, Multiplier, TargetedFeeAdjustm pub use primitives::Balance; use primitives::{ staking::MAX_NOMINATORS_REWARDED_PER_VALIDATOR, wrap_methods, ApiError as AlephApiError, - AuthorityId as AlephId, SessionAuthorityData, ADDRESSES_ENCODING, + AuthorityId as AlephId, SessionAuthorityData, Version as FinalityVersion, ADDRESSES_ENCODING, DEFAULT_KICK_OUT_REASON_LENGTH, DEFAULT_SESSIONS_PER_ERA, DEFAULT_SESSION_PERIOD, MILLISECS_PER_BLOCK, TOKEN, }; @@ -108,10 +108,10 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("aleph-node"), impl_name: create_runtime_str!("aleph-node"), authoring_version: 1, - spec_version: 35, + spec_version: 36, impl_version: 1, apis: RUNTIME_API_VERSIONS, - transaction_version: 10, + transaction_version: 11, state_version: 0, }; @@ -313,6 +313,8 @@ impl pallet_sudo::Config for Runtime { impl pallet_aleph::Config for Runtime { type AuthorityId = AlephId; type Event = Event; + type SessionInfoProvider = Session; + type SessionManager = Elections; } impl_opaque_keys! { @@ -350,7 +352,7 @@ impl pallet_session::Config for Runtime { type ValidatorIdOf = pallet_staking::StashOf; type ShouldEndSession = pallet_session::PeriodicSessions; type NextSessionRotation = pallet_session::PeriodicSessions; - type SessionManager = Elections; + type SessionManager = Aleph; type SessionHandler = ::KeyTypeIdProviders; type Keys = SessionKeys; type WeightInfo = pallet_session::weights::SubstrateWeight; @@ -898,6 +900,14 @@ impl_runtime_apis! { Aleph::queued_emergency_finalizer(), )) } + + fn finality_version() -> FinalityVersion { + Aleph::finality_version() + } + + fn next_session_finality_version() -> FinalityVersion { + Aleph::next_session_finality_version() + } } impl pallet_contracts_rpc_runtime_api::ContractsApi for Runtime { diff --git a/pallets/aleph/src/impls.rs b/pallets/aleph/src/impls.rs new file mode 100644 index 0000000000..4a1cba814d --- /dev/null +++ b/pallets/aleph/src/impls.rs @@ -0,0 +1,52 @@ +use primitives::SessionIndex; +use sp_std::vec::Vec; + +use crate::{Config, Event, FinalityScheduledVersionChange, FinalityVersion, Pallet}; + +impl pallet_session::SessionManager for Pallet +where + T: Config, +{ + fn new_session(new_index: SessionIndex) -> Option> { + ::SessionManager::new_session(new_index) + } + + fn new_session_genesis(new_index: SessionIndex) -> Option> { + ::SessionManager::new_session_genesis(new_index) + } + + fn end_session(end_index: SessionIndex) { + ::SessionManager::end_session(end_index); + } + + fn start_session(start_index: SessionIndex) { + ::SessionManager::start_session(start_index); + Self::update_version_change_history(); + } +} + +impl Pallet +where + T: Config, +{ + // Check if a schedule version change has moved into the past. Update history, even if there is + // no change. Resets the scheduled version. + fn update_version_change_history() { + let current_session = Self::current_session(); + + if let Some(scheduled_version_change) = >::get() { + let scheduled_session = scheduled_version_change.session; + let scheduled_version = scheduled_version_change.version_incoming; + + // Record the scheduled version as the current version as it moves into the past. + if scheduled_session == current_session { + >::put(scheduled_version); + + // Reset the scheduled version. + >::kill(); + + Self::deposit_event(Event::FinalityVersionChange(scheduled_version_change)); + } + } + } +} diff --git a/pallets/aleph/src/lib.rs b/pallets/aleph/src/lib.rs index 6f41fe00af..eed1fc3e6e 100644 --- a/pallets/aleph/src/lib.rs +++ b/pallets/aleph/src/lib.rs @@ -1,7 +1,18 @@ -//! This pallet is a runtime companion of Aleph finality gadget. +//! This pallet is the runtime companion of the Aleph finality gadget. //! //! Currently, it only provides support for changing sessions but in the future //! it will allow reporting equivocation in AlephBFT. +//! +//! This pallet relies on an extension of the `AlephSessionApi` Runtime API to handle the finality +//! version. The scheduled version change is persisted as `FinalityScheduledVersionChange`. This +//! value stores the information about a scheduled finality version change, where `version_incoming` +//! is the version to be set and `session` is the session on which the new version will be set. +//! A `pallet_session::Session_Manager` checks whether a scheduled version change has moved into +//! the past and, if so, records it as the current version represented as `FinalityVersion`, +//! and clears `FinalityScheduledVersionChange`. +//! It is always possible to reschedule a version change. In order to cancel a scheduled version +//! change rather than reschedule it, a new version change should be scheduled with +//! `version_incoming` set to the current value of `FinalityVersion`. #![cfg_attr(not(feature = "std"), no_std)] @@ -10,7 +21,9 @@ mod mock; #[cfg(test)] mod tests; +mod impls; mod migrations; +mod traits; use frame_support::{ log, @@ -18,11 +31,14 @@ use frame_support::{ traits::{OneSessionHandler, StorageVersion}, }; pub use pallet::*; +use primitives::{SessionIndex, Version, VersionChange}; use sp_std::prelude::*; /// The current storage version. const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); +const DEFAULT_FINALITY_VERSION: Version = 0; + #[frame_support::pallet] pub mod pallet { use frame_support::{pallet_prelude::*, sp_runtime::RuntimeAppPublic}; @@ -30,20 +46,26 @@ pub mod pallet { ensure_root, pallet_prelude::{BlockNumberFor, OriginFor}, }; + use pallet_session::SessionManager; use pallets_support::StorageMigration; use super::*; + use crate::traits::SessionInfoProvider; #[pallet::config] pub trait Config: frame_system::Config { type AuthorityId: Member + Parameter + RuntimeAppPublic + MaybeSerializeDeserialize; type Event: From> + IsType<::Event>; + type SessionInfoProvider: SessionInfoProvider; + type SessionManager: SessionManager<::AccountId>; } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { ChangeEmergencyFinalizer(T::AuthorityId), + ScheduleFinalityVersionChange(VersionChange), + FinalityVersionChange(VersionChange), } #[pallet::pallet] @@ -77,6 +99,12 @@ pub mod pallet { } } + /// Default finality version. Relevant for sessions before the first version change occurs. + #[pallet::type_value] + pub(crate) fn DefaultFinalityVersion() -> Version { + DEFAULT_FINALITY_VERSION + } + #[pallet::storage] #[pallet::getter(fn authorities)] pub(super) type Authorities = StorageValue<_, Vec, ValueQuery>; @@ -93,6 +121,18 @@ pub mod pallet { #[pallet::storage] type NextEmergencyFinalizer = StorageValue<_, T::AuthorityId, OptionQuery>; + /// Current finality version. + #[pallet::storage] + #[pallet::getter(fn finality_version)] + pub(super) type FinalityVersion = + StorageValue<_, Version, ValueQuery, DefaultFinalityVersion>; + + /// Scheduled finality version change. + #[pallet::storage] + #[pallet::getter(fn finality_version_change)] + pub(super) type FinalityScheduledVersionChange = + StorageValue<_, VersionChange, OptionQuery>; + impl Pallet { pub(crate) fn initialize_authorities(authorities: &[T::AuthorityId]) { if !authorities.is_empty() { @@ -121,6 +161,49 @@ pub mod pallet { pub(crate) fn set_next_emergency_finalizer(emergency_finalizer: T::AuthorityId) { >::put(emergency_finalizer); } + + pub(crate) fn current_session() -> u32 { + T::SessionInfoProvider::current_session() + } + + // If a scheduled future version change is rescheduled to a different session, + // it is possible to reschedule it with the same version as initially. + // To cancel a future version change, reschedule it with the current version. + // If a scheduled version change has moved into the past, `SessionManager` records it + // as the current version. + pub(crate) fn do_schedule_finality_version_change( + version_change: VersionChange, + ) -> Result<(), &'static str> { + let current_session = Self::current_session(); + + let session_to_schedule = version_change.session; + + if session_to_schedule < current_session { + return Err("Cannot schedule finality version changes for sessions in the past!"); + } else if session_to_schedule < current_session + 2 { + return Err( + "Tried to schedule an finality version change less than 2 sessions in advance!", + ); + } + + // Update the scheduled version change with the supplied version change. + >::put(version_change); + + Ok(()) + } + + pub fn next_session_finality_version() -> Version { + let next_session = Self::current_session() + 1; + let scheduled_version_change = Self::finality_version_change(); + + if let Some(version_change) = scheduled_version_change { + if next_session == version_change.session { + return version_change.version_incoming; + } + } + + Self::finality_version() + } } #[pallet::call] @@ -137,6 +220,33 @@ pub mod pallet { Self::deposit_event(Event::ChangeEmergencyFinalizer(emergency_finalizer)); Ok(()) } + + /// Schedules a finality version change for a future session. If such a scheduled future + /// version is already set, it is replaced with the provided one. + /// Any rescheduling of a future version change needs to occur at least 2 sessions in + /// advance of the provided session of the version change. + /// In order to cancel a scheduled version change, a new version change should be scheduled + /// with the same version as the current one. + #[pallet::weight((T::BlockWeights::get().max_block, DispatchClass::Operational))] + pub fn schedule_finality_version_change( + origin: OriginFor, + version_incoming: Version, + session: SessionIndex, + ) -> DispatchResult { + ensure_root(origin)?; + + let version_change = VersionChange { + version_incoming, + session, + }; + + if let Err(e) = Self::do_schedule_finality_version_change(version_change.clone()) { + return Err(DispatchError::Other(e)); + } + + Self::deposit_event(Event::ScheduleFinalityVersionChange(version_change)); + Ok(()) + } } impl BoundToRuntimeAppPublic for Pallet { diff --git a/pallets/aleph/src/mock.rs b/pallets/aleph/src/mock.rs index f4119d5c25..2fc82e96e4 100644 --- a/pallets/aleph/src/mock.rs +++ b/pallets/aleph/src/mock.rs @@ -105,7 +105,7 @@ impl pallet_session::Config for Test { type ValidatorIdOf = ConvertInto; type ShouldEndSession = pallet_session::PeriodicSessions; type NextSessionRotation = pallet_session::PeriodicSessions; - type SessionManager = (); + type SessionManager = Aleph; type SessionHandler = ::KeyTypeIdProviders; type Keys = TestSessionKeys; type WeightInfo = (); @@ -133,6 +133,8 @@ impl pallet_timestamp::Config for Test { impl Config for Test { type AuthorityId = AuthorityId; type Event = Event; + type SessionInfoProvider = Session; + type SessionManager = (); } pub fn to_authority(id: &u64) -> AuthorityId { diff --git a/pallets/aleph/src/tests.rs b/pallets/aleph/src/tests.rs index 7db90914a3..74eb4b1aca 100644 --- a/pallets/aleph/src/tests.rs +++ b/pallets/aleph/src/tests.rs @@ -1,6 +1,7 @@ #![cfg(test)] use frame_support::{storage_alias, traits::OneSessionHandler}; +use primitives::VersionChange; use crate::mock::*; @@ -130,3 +131,40 @@ fn test_emergency_signer() { assert_eq!(Aleph::queued_emergency_finalizer(), Some(to_authority(&37))); }) } + +#[test] +fn test_finality_version_scheduling() { + new_test_ext(&[(1u64, 1u64), (2u64, 2u64)]).execute_with(|| { + initialize_session(); + + run_session(1); + + let version_to_schedule = VersionChange { + version_incoming: 1, + session: 4, + }; + + let scheduling_result = + Aleph::do_schedule_finality_version_change(version_to_schedule.clone()); + assert_eq!(scheduling_result, Ok(())); + + let scheduled_version_change = Aleph::finality_version_change(); + assert_eq!(scheduled_version_change, Some(version_to_schedule.clone())); + + run_session(4); + + let current_version = Aleph::finality_version(); + assert_eq!(current_version, version_to_schedule.version_incoming); + + let scheduled_version_change = Aleph::finality_version_change(); + assert_eq!(scheduled_version_change, None); + + let version_to_schedule = VersionChange { + version_incoming: 1, + session: 5, + }; + + let scheduling_result = Aleph::do_schedule_finality_version_change(version_to_schedule); + assert!(scheduling_result.is_err()); + }) +} diff --git a/pallets/aleph/src/traits.rs b/pallets/aleph/src/traits.rs new file mode 100644 index 0000000000..92de3efd4b --- /dev/null +++ b/pallets/aleph/src/traits.rs @@ -0,0 +1,15 @@ +use primitives::SessionIndex; + +/// Information provider from `pallet_session`. Loose pallet coupling via traits. +pub trait SessionInfoProvider { + fn current_session() -> SessionIndex; +} + +impl SessionInfoProvider for pallet_session::Pallet +where + T: pallet_session::Config, +{ + fn current_session() -> SessionIndex { + pallet_session::CurrentIndex::::get() + } +} diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index 710dbc7d47..c21bd631d8 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -171,6 +171,14 @@ impl SessionAuthorityData { } } +pub type Version = u32; + +#[derive(Clone, Debug, Decode, Encode, PartialEq, Eq, TypeInfo)] +pub struct VersionChange { + pub version_incoming: Version, + pub session: SessionIndex, +} + sp_api::decl_runtime_apis! { pub trait AlephSessionApi { @@ -180,6 +188,8 @@ sp_api::decl_runtime_apis! { fn authority_data() -> SessionAuthorityData; fn session_period() -> u32; fn millisecs_per_block() -> u64; + fn finality_version() -> Version; + fn next_session_finality_version() -> Version; } }