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

Commit ea4fbcb

Browse files
authored
babe: account for skipped epochs when handling equivocations (#13335)
* babe: account for skipped epochs when handling equivocations * typos * babe: enforce epoch index >= session index
1 parent dfa654d commit ea4fbcb

File tree

2 files changed

+146
-6
lines changed

2 files changed

+146
-6
lines changed

frame/babe/src/lib.rs

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use sp_runtime::{
3939
ConsensusEngineId, KeyTypeId, Permill,
4040
};
4141
use sp_session::{GetSessionNumber, GetValidatorCount};
42+
use sp_staking::SessionIndex;
4243
use sp_std::prelude::*;
4344

4445
use sp_consensus_babe::{
@@ -102,7 +103,7 @@ impl EpochChangeTrigger for SameAuthoritiesForever {
102103
let authorities = <Pallet<T>>::authorities();
103104
let next_authorities = authorities.clone();
104105

105-
<Pallet<T>>::enact_epoch_change(authorities, next_authorities);
106+
<Pallet<T>>::enact_epoch_change(authorities, next_authorities, None);
106107
}
107108
}
108109
}
@@ -316,6 +317,19 @@ pub mod pallet {
316317
#[pallet::storage]
317318
pub(super) type NextEpochConfig<T> = StorageValue<_, BabeEpochConfiguration>;
318319

320+
/// A list of the last 100 skipped epochs and the corresponding session index
321+
/// when the epoch was skipped.
322+
///
323+
/// This is only used for validating equivocation proofs. An equivocation proof
324+
/// must contains a key-ownership proof for a given session, therefore we need a
325+
/// way to tie together sessions and epoch indices, i.e. we need to validate that
326+
/// a validator was the owner of a given key on a given session, and what the
327+
/// active epoch index was during that session.
328+
#[pallet::storage]
329+
#[pallet::getter(fn skipped_epochs)]
330+
pub(super) type SkippedEpochs<T> =
331+
StorageValue<_, BoundedVec<(u64, SessionIndex), ConstU32<100>>, ValueQuery>;
332+
319333
#[cfg_attr(feature = "std", derive(Default))]
320334
#[pallet::genesis_config]
321335
pub struct GenesisConfig {
@@ -577,6 +591,7 @@ impl<T: Config> Pallet<T> {
577591
pub fn enact_epoch_change(
578592
authorities: WeakBoundedVec<(AuthorityId, BabeAuthorityWeight), T::MaxAuthorities>,
579593
next_authorities: WeakBoundedVec<(AuthorityId, BabeAuthorityWeight), T::MaxAuthorities>,
594+
session_index: Option<SessionIndex>,
580595
) {
581596
// PRECONDITION: caller has done initialization and is guaranteed
582597
// by the session module to be called before this.
@@ -602,6 +617,35 @@ impl<T: Config> Pallet<T> {
602617
T::EpochDuration::get(),
603618
);
604619

620+
let current_epoch_index = EpochIndex::<T>::get();
621+
if current_epoch_index.saturating_add(1) != epoch_index {
622+
// we are skipping epochs therefore we need to update the mapping
623+
// of epochs to session
624+
if let Some(session_index) = session_index {
625+
SkippedEpochs::<T>::mutate(|skipped_epochs| {
626+
if epoch_index < session_index as u64 {
627+
log::warn!(
628+
target: LOG_TARGET,
629+
"Current epoch index {} is lower than session index {}.",
630+
epoch_index,
631+
session_index,
632+
);
633+
634+
return
635+
}
636+
637+
if skipped_epochs.is_full() {
638+
// NOTE: this is O(n) but we currently don't have a bounded `VecDeque`.
639+
// this vector is bounded to a small number of elements so performance
640+
// shouldn't be an issue.
641+
skipped_epochs.remove(0);
642+
}
643+
644+
skipped_epochs.force_push((epoch_index, session_index));
645+
})
646+
}
647+
}
648+
605649
EpochIndex::<T>::put(epoch_index);
606650
Authorities::<T>::put(authorities);
607651

@@ -816,6 +860,36 @@ impl<T: Config> Pallet<T> {
816860
this_randomness
817861
}
818862

863+
/// Returns the session index that was live when the given epoch happened,
864+
/// taking into account any skipped epochs.
865+
///
866+
/// This function is only well defined for epochs that actually existed,
867+
/// e.g. if we skipped from epoch 10 to 20 then a call for epoch 15 (which
868+
/// didn't exist) will return an incorrect session index.
869+
fn session_index_for_epoch(epoch_index: u64) -> SessionIndex {
870+
let skipped_epochs = SkippedEpochs::<T>::get();
871+
match skipped_epochs.binary_search_by_key(&epoch_index, |(epoch_index, _)| *epoch_index) {
872+
// we have an exact match so we just return the given session index
873+
Ok(index) => skipped_epochs[index].1,
874+
// we haven't found any skipped epoch before the given epoch,
875+
// so the epoch index and session index should match
876+
Err(0) => epoch_index.saturated_into::<u32>(),
877+
// we have found a skipped epoch before the given epoch
878+
Err(index) => {
879+
// the element before the given index should give us the skipped epoch
880+
// that's closest to the one we're trying to find the session index for
881+
let closest_skipped_epoch = skipped_epochs[index - 1];
882+
883+
// calculate the number of skipped epochs at this point by checking the difference
884+
// between the epoch and session indices. epoch index should always be greater or
885+
// equal to session index, this is because epochs can be skipped whereas sessions
886+
// can't (this is enforced when pushing into `SkippedEpochs`)
887+
let skipped_epochs = closest_skipped_epoch.0 - closest_skipped_epoch.1 as u64;
888+
epoch_index.saturating_sub(skipped_epochs).saturated_into::<u32>()
889+
},
890+
}
891+
}
892+
819893
fn do_report_equivocation(
820894
reporter: Option<T::AccountId>,
821895
equivocation_proof: EquivocationProof<T::Header>,
@@ -832,12 +906,11 @@ impl<T: Config> Pallet<T> {
832906
let validator_set_count = key_owner_proof.validator_count();
833907
let session_index = key_owner_proof.session();
834908

835-
let epoch_index = (*slot.saturating_sub(GenesisSlot::<T>::get()) / T::EpochDuration::get())
836-
.saturated_into::<u32>();
909+
let epoch_index = *slot.saturating_sub(GenesisSlot::<T>::get()) / T::EpochDuration::get();
837910

838911
// check that the slot number is consistent with the session index
839912
// in the key ownership proof (i.e. slot is for that epoch)
840-
if epoch_index != session_index {
913+
if Self::session_index_for_epoch(epoch_index) != session_index {
841914
return Err(Error::<T>::InvalidKeyOwnershipProof.into())
842915
}
843916

@@ -926,7 +999,10 @@ impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> {
926999
type Public = AuthorityId;
9271000
}
9281001

929-
impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
1002+
impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T>
1003+
where
1004+
T: pallet_session::Config,
1005+
{
9301006
type Key = AuthorityId;
9311007

9321008
fn on_genesis_session<'a, I: 'a>(validators: I)
@@ -959,7 +1035,9 @@ impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
9591035
),
9601036
);
9611037

962-
Self::enact_epoch_change(bounded_authorities, next_bounded_authorities)
1038+
let session_index = <pallet_session::Pallet<T>>::current_index();
1039+
1040+
Self::enact_epoch_change(bounded_authorities, next_bounded_authorities, Some(session_index))
9631041
}
9641042

9651043
fn on_disabled(i: u32) {

frame/babe/src/tests.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,56 @@ fn report_equivocation_has_valid_weight() {
826826
.all(|w| w[0].ref_time() < w[1].ref_time()));
827827
}
828828

829+
#[test]
830+
fn report_equivocation_after_skipped_epochs_works() {
831+
let (pairs, mut ext) = new_test_ext_with_pairs(3);
832+
833+
ext.execute_with(|| {
834+
let epoch_duration: u64 = <Test as Config>::EpochDuration::get();
835+
836+
// this sets the genesis slot to 100;
837+
let genesis_slot = 100;
838+
go_to_block(1, genesis_slot);
839+
assert_eq!(EpochIndex::<Test>::get(), 0);
840+
841+
// skip from epoch #0 to epoch #10
842+
go_to_block(System::block_number() + 1, genesis_slot + epoch_duration * 10);
843+
844+
assert_eq!(EpochIndex::<Test>::get(), 10);
845+
assert_eq!(SkippedEpochs::<Test>::get(), vec![(10, 1)]);
846+
847+
// generate an equivocation proof for validator at index 1
848+
let authorities = Babe::authorities();
849+
let offending_validator_index = 1;
850+
let offending_authority_pair = pairs
851+
.into_iter()
852+
.find(|p| p.public() == authorities[offending_validator_index].0)
853+
.unwrap();
854+
855+
let equivocation_proof = generate_equivocation_proof(
856+
offending_validator_index as u32,
857+
&offending_authority_pair,
858+
CurrentSlot::<Test>::get(),
859+
);
860+
861+
// create the key ownership proof
862+
let key = (sp_consensus_babe::KEY_TYPE, &offending_authority_pair.public());
863+
let key_owner_proof = Historical::prove(key).unwrap();
864+
865+
// which is for session index 1 (while current epoch index is 10)
866+
assert_eq!(key_owner_proof.session, 1);
867+
868+
// report the equivocation, in order for the validation to pass the mapping
869+
// between epoch index and session index must be checked.
870+
assert!(Babe::report_equivocation_unsigned(
871+
RuntimeOrigin::none(),
872+
Box::new(equivocation_proof),
873+
key_owner_proof
874+
)
875+
.is_ok());
876+
})
877+
}
878+
829879
#[test]
830880
fn valid_equivocation_reports_dont_pay_fees() {
831881
let (pairs, mut ext) = new_test_ext_with_pairs(3);
@@ -977,5 +1027,17 @@ fn skipping_over_epochs_works() {
9771027

9781028
assert_eq!(EpochIndex::<Test>::get(), 4);
9791029
assert_eq!(Randomness::<Test>::get(), randomness_for_epoch_2);
1030+
1031+
// after skipping epochs the information is registered on-chain so that
1032+
// we can map epochs to sessions
1033+
assert_eq!(SkippedEpochs::<Test>::get(), vec![(4, 2)]);
1034+
1035+
// before epochs are skipped the mapping should be one to one
1036+
assert_eq!(Babe::session_index_for_epoch(0), 0);
1037+
assert_eq!(Babe::session_index_for_epoch(1), 1);
1038+
1039+
// otherwise the session index is offset by the number of skipped epochs
1040+
assert_eq!(Babe::session_index_for_epoch(4), 2);
1041+
assert_eq!(Babe::session_index_for_epoch(5), 3);
9801042
});
9811043
}

0 commit comments

Comments
 (0)