srml: grandpa: Pause/resume current authority set (#3068)

* grandpa: add pause/resume signals to runtime module

* grandpa: add tests for srml pause/resume transitions

* node: bump spec_version

* Apply suggestions from code review

* Update core/finality-grandpa/primitives/src/lib.rs

* Update core/finality-grandpa/primitives/src/lib.rs
This commit is contained in:
André Silva
2019-07-19 10:30:59 +01:00
committed by Gavin Wood
parent 1e77717b26
commit a313935947
4 changed files with 218 additions and 4 deletions
@@ -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,
}
}
}
+1 -1
View File
@@ -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,
};
+111 -1
View File
@@ -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))
}
+80
View File
@@ -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,
);
});
}