diff --git a/substrate/core/finality-grandpa/primitives/src/lib.rs b/substrate/core/finality-grandpa/primitives/src/lib.rs index f3721d5dcc7ea93f8f180511d99a4f9ac54a4c7c..77527820251837b49b996dfbf2928ee4ef7867da 100644 --- a/substrate/core/finality-grandpa/primitives/src/lib.rs +++ b/substrate/core/finality-grandpa/primitives/src/lib.rs @@ -93,6 +93,14 @@ pub enum ConsensusLog<N: Codec> { /// Note that the authority with given index is disabled until the next change. #[codec(index = "3")] OnDisabled(AuthorityIndex), + /// A signal to pause the current authority set after the given delay. + /// After finalizing the block at _delay_ the authorities should stop voting. + #[codec(index = "4")] + Pause(N), + /// A signal to resume the current authority set after the given delay. + /// After authoring the block at _delay_ the authorities should resume voting. + #[codec(index = "5")] + Resume(N), } impl<N: Codec> ConsensusLog<N> { @@ -100,7 +108,7 @@ impl<N: Codec> ConsensusLog<N> { pub fn try_into_change(self) -> Option<ScheduledChange<N>> { match self { ConsensusLog::ScheduledChange(change) => Some(change), - ConsensusLog::ForcedChange(_, _) | ConsensusLog::OnDisabled(_) => None, + _ => None, } } @@ -108,7 +116,23 @@ impl<N: Codec> ConsensusLog<N> { pub fn try_into_forced_change(self) -> Option<(N, ScheduledChange<N>)> { match self { ConsensusLog::ForcedChange(median, change) => Some((median, change)), - ConsensusLog::ScheduledChange(_) | ConsensusLog::OnDisabled(_) => None, + _ => None, + } + } + + /// Try to cast the log entry as a contained pause signal. + pub fn try_into_pause(self) -> Option<N> { + match self { + ConsensusLog::Pause(delay) => Some(delay), + _ => None, + } + } + + /// Try to cast the log entry as a contained resume signal. + pub fn try_into_resume(self) -> Option<N> { + match self { + ConsensusLog::Resume(delay) => Some(delay), + _ => None, } } } diff --git a/substrate/node/runtime/src/lib.rs b/substrate/node/runtime/src/lib.rs index 3bfb86c8cf4e7388f12266bbf88033d4e8f71a92..eab18591832db936a66ac5e6ae4686470531d801 100644 --- a/substrate/node/runtime/src/lib.rs +++ b/substrate/node/runtime/src/lib.rs @@ -69,7 +69,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // and set impl_version to equal spec_version. If only runtime // implementation changes and behavior does not, then leave spec_version as // is and increment impl_version. - spec_version: 113, + spec_version: 114, impl_version: 114, apis: RUNTIME_API_VERSIONS, }; diff --git a/substrate/srml/grandpa/src/lib.rs b/substrate/srml/grandpa/src/lib.rs index ba60128a897cf583112d8ee0bc0b0e70dd78608b..a62f5652d4e6707f7675ff9b47482b883111398c 100644 --- a/substrate/srml/grandpa/src/lib.rs +++ b/substrate/srml/grandpa/src/lib.rs @@ -91,10 +91,42 @@ impl<N: Decode> Decode for StoredPendingChange<N> { } } +/// Current state of the GRANDPA authority set. State transitions must happen in +/// the same order of states defined below, e.g. `Paused` implies a prior +/// `PendingPause`. +#[derive(Decode, Encode)] +#[cfg_attr(test, derive(Debug, PartialEq))] +pub enum StoredState<N> { + /// The current authority set is live, and GRANDPA is enabled. + Live, + /// There is a pending pause event which will be enacted at the given block + /// height. + PendingPause { + /// Block at which the intention to pause was scheduled. + scheduled_at: N, + /// Number of blocks after which the change will be enacted. + delay: N + }, + /// The current GRANDPA authority set is paused. + Paused, + /// There is a pending resume event which will be enacted at the given block + /// height. + PendingResume { + /// Block at which the intention to resume was scheduled. + scheduled_at: N, + /// Number of blocks after which the change will be enacted. + delay: N, + }, +} + decl_event!( pub enum Event { /// New authority set has been applied. NewAuthorities(Vec<(AuthorityId, u64)>), + /// Current authority set has been paused. + Paused, + /// Current authority set has been resumed. + Resumed, } ); @@ -103,6 +135,9 @@ decl_storage! { /// The current authority set. Authorities get(authorities) config(): Vec<(AuthorityId, AuthorityWeight)>; + /// State of the current authority set. + State get(state): StoredState<T::BlockNumber> = StoredState::Live; + /// Pending change: (signaled at, scheduled change). PendingChange: Option<StoredPendingChange<T::BlockNumber>>; @@ -125,12 +160,14 @@ decl_module! { } fn on_finalize(block_number: T::BlockNumber) { + // check for scheduled pending authority set changes if let Some(pending_change) = <PendingChange<T>>::get() { + // emit signal if we're at the block that scheduled the change if block_number == pending_change.scheduled_at { if let Some(median) = pending_change.forced { Self::deposit_log(ConsensusLog::ForcedChange( median, - ScheduledChange{ + ScheduledChange { delay: pending_change.delay, next_authorities: pending_change.next_authorities.clone(), } @@ -145,6 +182,7 @@ decl_module! { } } + // enact the change if we've reached the enacting block if block_number == pending_change.scheduled_at + pending_change.delay { Authorities::put(&pending_change.next_authorities); Self::deposit_event( @@ -153,6 +191,35 @@ decl_module! { <PendingChange<T>>::kill(); } } + + // check for scheduled pending state changes + match <State<T>>::get() { + StoredState::PendingPause { scheduled_at, delay } => { + // signal change to pause + if block_number == scheduled_at { + Self::deposit_log(ConsensusLog::Pause(delay)); + } + + // enact change to paused state + if block_number == scheduled_at + delay { + <State<T>>::put(StoredState::Paused); + Self::deposit_event(Event::Paused); + } + }, + StoredState::PendingResume { scheduled_at, delay } => { + // signal change to resume + if block_number == scheduled_at { + Self::deposit_log(ConsensusLog::Resume(delay)); + } + + // enact change to live state + if block_number == scheduled_at + delay { + <State<T>>::put(StoredState::Live); + Self::deposit_event(Event::Resumed); + } + }, + _ => {}, + } } } } @@ -163,6 +230,36 @@ impl<T: Trait> Module<T> { Authorities::get() } + pub fn schedule_pause(in_blocks: T::BlockNumber) -> Result { + if let StoredState::Live = <State<T>>::get() { + let scheduled_at = system::ChainContext::<T>::default().current_height(); + <State<T>>::put(StoredState::PendingPause { + delay: in_blocks, + scheduled_at, + }); + + Ok(()) + } else { + Err("Attempt to signal GRANDPA pause when the authority set isn't live \ + (either paused or already pending pause).") + } + } + + pub fn schedule_resume(in_blocks: T::BlockNumber) -> Result { + if let StoredState::Paused = <State<T>>::get() { + let scheduled_at = system::ChainContext::<T>::default().current_height(); + <State<T>>::put(StoredState::PendingResume { + delay: in_blocks, + scheduled_at, + }); + + Ok(()) + } else { + Err("Attempt to signal GRANDPA resume when the authority set isn't paused \ + (either live or already pending resume).") + } + } + /// Schedule a change in the authorities. /// /// The change will be applied at the end of execution of the block @@ -232,6 +329,18 @@ impl<T: Trait> Module<T> { { Self::grandpa_log(digest).and_then(|signal| signal.try_into_forced_change()) } + + pub fn pending_pause(digest: &DigestOf<T>) + -> Option<T::BlockNumber> + { + Self::grandpa_log(digest).and_then(|signal| signal.try_into_pause()) + } + + pub fn pending_resume(digest: &DigestOf<T>) + -> Option<T::BlockNumber> + { + Self::grandpa_log(digest).and_then(|signal| signal.try_into_resume()) + } } impl<T: Trait> session::OneSessionHandler<T::AccountId> for Module<T> { @@ -254,6 +363,7 @@ impl<T: Trait> session::OneSessionHandler<T::AccountId> for Module<T> { } } } + fn on_disabled(i: usize) { Self::deposit_log(ConsensusLog::OnDisabled(i as u64)) } diff --git a/substrate/srml/grandpa/src/tests.rs b/substrate/srml/grandpa/src/tests.rs index 763c4fce89cca0b271f16cf92052e60ff4266dba..11700fa99fa78618cf27a189d4847e6301d45f10 100644 --- a/substrate/srml/grandpa/src/tests.rs +++ b/substrate/srml/grandpa/src/tests.rs @@ -202,3 +202,83 @@ fn dispatch_forced_change() { let _ = header; }); } + +#[test] +fn schedule_pause_only_when_live() { + with_externalities(&mut new_test_ext(vec![(1, 1), (2, 1), (3, 1)]), || { + // we schedule a pause at block 1 with delay of 1 + System::initialize(&1, &Default::default(), &Default::default(), &Default::default()); + Grandpa::schedule_pause(1).unwrap(); + + // we've switched to the pending pause state + assert_eq!( + Grandpa::state(), + StoredState::PendingPause { + scheduled_at: 1u64, + delay: 1, + }, + ); + + Grandpa::on_finalize(1); + let _ = System::finalize(); + + System::initialize(&2, &Default::default(), &Default::default(), &Default::default()); + + // signaling a pause now should fail + assert!(Grandpa::schedule_pause(1).is_err()); + + Grandpa::on_finalize(2); + let _ = System::finalize(); + + // after finalizing block 2 the set should have switched to paused state + assert_eq!( + Grandpa::state(), + StoredState::Paused, + ); + }); +} + +#[test] +fn schedule_resume_only_when_paused() { + with_externalities(&mut new_test_ext(vec![(1, 1), (2, 1), (3, 1)]), || { + System::initialize(&1, &Default::default(), &Default::default(), &Default::default()); + + // the set is currently live, resuming it is an error + assert!(Grandpa::schedule_resume(1).is_err()); + + assert_eq!( + Grandpa::state(), + StoredState::Live, + ); + + // we schedule a pause to be applied instantly + Grandpa::schedule_pause(0).unwrap(); + Grandpa::on_finalize(1); + let _ = System::finalize(); + + assert_eq!( + Grandpa::state(), + StoredState::Paused, + ); + + // we schedule the set to go back live in 2 blocks + System::initialize(&2, &Default::default(), &Default::default(), &Default::default()); + Grandpa::schedule_resume(2).unwrap(); + Grandpa::on_finalize(2); + let _ = System::finalize(); + + System::initialize(&3, &Default::default(), &Default::default(), &Default::default()); + Grandpa::on_finalize(3); + let _ = System::finalize(); + + System::initialize(&4, &Default::default(), &Default::default(), &Default::default()); + Grandpa::on_finalize(4); + let _ = System::finalize(); + + // it should be live at block 4 + assert_eq!( + Grandpa::state(), + StoredState::Live, + ); + }); +}