// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common 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.
// Parity Bridges Common 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 Parity Bridges Common. If not, see .
//! Substrate Finality Verifier Pallet
//!
//! The goal of this pallet is to provide a safe interface for writing finalized headers to an
//! external pallet which tracks headers and finality proofs. By safe, we mean that only headers
//! whose finality has been verified will be written to the underlying pallet.
//!
//! By verifying the finality of headers before writing them to storage we prevent DoS vectors in
//! which unfinalized headers get written to storage even if they don't have a chance of being
//! finalized in the future (such as in the case where a different fork gets finalized).
//!
//! The underlying pallet used for storage is assumed to be a pallet which tracks headers and
//! GRANDPA authority set changes. This information is used during the verification of GRANDPA
//! finality proofs.
#![cfg_attr(not(feature = "std"), no_std)]
// Runtime-generated enums
#![allow(clippy::large_enum_variant)]
use bp_header_chain::{justification::verify_justification, AncestryChecker, HeaderChain};
use bp_runtime::{BlockNumberOf, Chain, HashOf, HasherOf, HeaderOf};
use codec::{Decode, Encode};
use finality_grandpa::voter_set::VoterSet;
use frame_support::{dispatch::DispatchError, ensure};
use frame_system::{ensure_signed, RawOrigin};
#[cfg(feature = "std")]
use serde::{Deserialize, Serialize};
use sp_finality_grandpa::{ConsensusLog, GRANDPA_ENGINE_ID};
use sp_runtime::traits::{BadOrigin, Header as HeaderT, Zero};
use sp_runtime::RuntimeDebug;
use sp_std::vec::Vec;
#[cfg(test)]
mod mock;
// Re-export in crate namespace for `construct_runtime!`
pub use pallet::*;
/// Block number of the bridged chain.
pub type BridgedBlockNumber = BlockNumberOf<::BridgedChain>;
/// Block hash of the bridged chain.
pub type BridgedBlockHash = HashOf<::BridgedChain>;
/// Hasher of the bridged chain.
pub type _BridgedBlockHasher = HasherOf<::BridgedChain>;
/// Header of the bridged chain.
pub type BridgedHeader = HeaderOf<::BridgedChain>;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[pallet::config]
pub trait Config: frame_system::Config {
/// The chain we are bridging to here.
type BridgedChain: Chain;
/// The pallet which we will use as our underlying storage mechanism.
type HeaderChain: HeaderChain<::Header, DispatchError>;
/// The type of ancestry proof used by the pallet.
///
/// Will be used by the ancestry checker to verify that the header being finalized is
/// related to the best finalized header in storage.
type AncestryProof: Parameter;
/// The type through which we will verify that a given header is related to the last
/// finalized header in our storage pallet.
type AncestryChecker: AncestryChecker<::Header, Self::AncestryProof>;
/// The upper bound on the number of requests allowed by the pallet.
///
/// A request refers to an action which writes a header to storage.
///
/// Once this bound is reached the pallet will not allow any dispatchables to be called
/// until the request count has decreased.
#[pallet::constant]
type MaxRequests: Get;
}
#[pallet::pallet]
pub struct Pallet(PhantomData);
#[pallet::hooks]
impl Hooks> for Pallet {
fn on_initialize(_n: T::BlockNumber) -> frame_support::weights::Weight {
>::mutate(|count| *count = count.saturating_sub(1));
(0_u64)
.saturating_add(T::DbWeight::get().reads(1))
.saturating_add(T::DbWeight::get().writes(1))
}
}
#[pallet::call]
impl Pallet {
/// Verify a target header is finalized according to the given finality proof.
///
/// It will use the underlying storage pallet to fetch information about the current
/// authorities and best finalized header in order to verify that the header is finalized.
///
/// If successful in verification, it will write the target header to the underlying storage
/// pallet.
#[pallet::weight(0)]
pub fn submit_finality_proof(
origin: OriginFor,
finality_target: BridgedHeader,
justification: Vec,
ancestry_proof: T::AncestryProof,
) -> DispatchResultWithPostInfo {
ensure_operational::()?;
let _ = ensure_signed(origin)?;
ensure!(
Self::request_count() < T::MaxRequests::get(),
>::TooManyRequests
);
let (hash, number) = (finality_target.hash(), finality_target.number());
frame_support::debug::trace!("Going to try and finalize header {:?}", finality_target);
let best_finalized = >::get(>::get()).expect(
"In order to reach this point the bridge must have been initialized. Afterwards,
every time `BestFinalized` is updated `ImportedHeaders` is also updated. Therefore
`ImportedHeaders` must contain an entry for `BestFinalized`.",
);
// We do a quick check here to ensure that our header chain is making progress and isn't
// "travelling back in time" (which could be indicative of something bad, e.g a hard-fork).
ensure!(best_finalized.number() < number, >::OldHeader);
let authority_set = >::get();
let voter_set = VoterSet::new(authority_set.authorities).ok_or(>::InvalidAuthoritySet)?;
let set_id = authority_set.set_id;
verify_justification::>((hash, *number), set_id, voter_set, &justification).map_err(
|e| {
frame_support::debug::error!("Received invalid justification for {:?}: {:?}", finality_target, e);
>::InvalidJustification
},
)?;
let best_finalized = T::HeaderChain::best_finalized();
frame_support::debug::trace!("Checking ancestry against best finalized header: {:?}", &best_finalized);
ensure!(
T::AncestryChecker::are_ancestors(&best_finalized, &finality_target, &ancestry_proof),
>::InvalidAncestryProof
);
let _ = T::HeaderChain::append_header(finality_target.clone())?;
import_header::(hash, finality_target)?;
>::mutate(|count| *count += 1);
frame_support::debug::info!("Succesfully imported finalized header with hash {:?}!", hash);
Ok(().into())
}
/// Bootstrap the bridge pallet with an initial header and authority set from which to sync.
///
/// The initial configuration provided does not need to be the genesis header of the bridged
/// chain, it can be any arbirary header. You can also provide the next scheduled set change
/// if it is already know.
///
/// This function is only allowed to be called from a trusted origin and writes to storage
/// with practically no checks in terms of the validity of the data. It is important that
/// you ensure that valid data is being passed in.
#[pallet::weight((T::DbWeight::get().reads_writes(2, 5), DispatchClass::Operational))]
pub fn initialize(
origin: OriginFor,
init_data: super::InitializationData>,
) -> DispatchResultWithPostInfo {
ensure_owner_or_root::(origin)?;
let init_allowed = !>::exists();
ensure!(init_allowed, >::AlreadyInitialized);
initialize_bridge::(init_data.clone());
frame_support::debug::info!(
"Pallet has been initialized with the following parameters: {:?}",
init_data
);
Ok(().into())
}
/// Change `ModuleOwner`.
///
/// May only be called either by root, or by `ModuleOwner`.
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
pub fn set_owner(origin: OriginFor, new_owner: Option) -> DispatchResultWithPostInfo {
ensure_owner_or_root::(origin)?;
match new_owner {
Some(new_owner) => {
ModuleOwner::::put(&new_owner);
frame_support::debug::info!("Setting pallet Owner to: {:?}", new_owner);
}
None => {
ModuleOwner::::kill();
frame_support::debug::info!("Removed Owner of pallet.");
}
}
Ok(().into())
}
/// Halt all pallet operations. Operations may be resumed using `resume_operations` call.
///
/// May only be called either by root, or by `ModuleOwner`.
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
pub fn halt_operations(origin: OriginFor) -> DispatchResultWithPostInfo {
ensure_owner_or_root::(origin)?;
>::put(true);
frame_support::debug::warn!("Stopping pallet operations.");
Ok(().into())
}
/// Resume all pallet operations. May be called even if pallet is halted.
///
/// May only be called either by root, or by `ModuleOwner`.
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
pub fn resume_operations(origin: OriginFor) -> DispatchResultWithPostInfo {
ensure_owner_or_root::(origin)?;
>::put(false);
frame_support::debug::info!("Resuming pallet operations.");
Ok(().into())
}
}
/// The current number of requests which have written to storage.
///
/// If the `RequestCount` hits `MaxRequests`, no more calls will be allowed to the pallet until
/// the request capacity is increased.
///
/// The `RequestCount` is decreased by one at the beginning of every block. This is to ensure
/// that the pallet can always make progress.
#[pallet::storage]
#[pallet::getter(fn request_count)]
pub(super) type RequestCount = StorageValue<_, u32, ValueQuery>;
/// Hash of the header used to bootstrap the pallet.
#[pallet::storage]
pub(super) type InitialHash = StorageValue<_, BridgedBlockHash, ValueQuery>;
/// Hash of the best finalized header.
#[pallet::storage]
pub(super) type BestFinalized = StorageValue<_, BridgedBlockHash, ValueQuery>;
/// Headers which have been imported into the pallet.
#[pallet::storage]
pub(super) type ImportedHeaders = StorageMap<_, Identity, BridgedBlockHash, BridgedHeader>;
/// The current GRANDPA Authority set.
#[pallet::storage]
pub(super) type CurrentAuthoritySet = StorageValue<_, bp_header_chain::AuthoritySet, ValueQuery>;
/// Optional pallet owner.
///
/// Pallet owner has a right to halt all pallet operations and then resume it. If it is
/// `None`, then there are no direct ways to halt/resume pallet operations, but other
/// runtime methods may still be used to do that (i.e. democracy::referendum to update halt
/// flag directly or call the `halt_operations`).
#[pallet::storage]
pub(super) type ModuleOwner = StorageValue<_, T::AccountId, OptionQuery>;
/// If true, all pallet transactions are failed immediately.
#[pallet::storage]
pub(super) type IsHalted = StorageValue<_, bool, ValueQuery>;
#[pallet::genesis_config]
pub struct GenesisConfig {
owner: Option,
init_data: Option>>,
}
#[cfg(feature = "std")]
impl Default for GenesisConfig {
fn default() -> Self {
Self {
owner: None,
init_data: None,
}
}
}
#[pallet::genesis_build]
impl GenesisBuild for GenesisConfig {
fn build(&self) {
if let Some(ref owner) = self.owner {
>::put(owner);
}
if let Some(init_data) = self.init_data.clone() {
initialize_bridge::(init_data);
} else {
// Since the bridge hasn't been initialized we shouldn't allow anyone to perform
// transactions.
>::put(true);
}
}
}
#[pallet::error]
pub enum Error {
/// The given justification is invalid for the given header.
InvalidJustification,
/// The given ancestry proof is unable to verify that the child and ancestor headers are
/// related.
InvalidAncestryProof,
/// The authority set from the underlying header chain is invalid.
InvalidAuthoritySet,
/// Failed to write a header to the underlying header chain.
FailedToWriteHeader,
/// There are too many requests for the current window to handle.
TooManyRequests,
/// The header being imported is older than the best finalized header known to the pallet.
OldHeader,
/// The scheduled authority set change found in the header is unsupported by the pallet.
///
/// This is the case for non-standard (e.g forced) authority set changes.
UnsupportedScheduledChange,
/// The pallet has already been initialized.
AlreadyInitialized,
/// All pallet operations are halted.
Halted,
}
/// Import the given header to the pallet's storage.
///
/// This function will also check if the header schedules and enacts authority set changes,
/// updating the current authority set accordingly.
///
/// Note: This function assumes that the given header has already been proven to be valid and
/// finalized. Using this assumption it will write them to storage with minimal checks. That
/// means it's of great importance that this function *not* called with any headers whose
/// finality has not been checked, otherwise you risk bricking your bridge.
pub(crate) fn import_header(
hash: BridgedBlockHash,
header: BridgedHeader,
) -> Result<(), sp_runtime::DispatchError> {
// We don't support forced changes - at that point governance intervention is required.
ensure!(
super::find_forced_change(&header).is_none(),
>::UnsupportedScheduledChange
);
if let Some(change) = super::find_scheduled_change(&header) {
// GRANDPA only includes a `delay` for forced changes, so this isn't valid.
ensure!(change.delay == Zero::zero(), >::UnsupportedScheduledChange);
// TODO [#788]: Stop manually increasing the `set_id` here.
let next_authorities = bp_header_chain::AuthoritySet {
authorities: change.next_authorities,
set_id: >::get().set_id + 1,
};
// Since our header schedules a change and we know the delay is 0, it must also enact
// the change.
>::put(next_authorities);
};
>::put(hash);
>::insert(hash, header);
Ok(())
}
/// Since this writes to storage with no real checks this should only be used in functions that
/// were called by a trusted origin.
fn initialize_bridge(init_params: super::InitializationData>) {
let super::InitializationData {
header,
authority_list,
set_id,
is_halted,
} = init_params;
let initial_hash = header.hash();
>::put(initial_hash);
>::put(initial_hash);
>::insert(initial_hash, header);
let authority_set = bp_header_chain::AuthoritySet::new(authority_list, set_id);
>::put(authority_set);
>::put(is_halted);
}
/// Ensure that the origin is either root, or `ModuleOwner`.
fn ensure_owner_or_root(origin: T::Origin) -> Result<(), BadOrigin> {
match origin.into() {
Ok(RawOrigin::Root) => Ok(()),
Ok(RawOrigin::Signed(ref signer)) if Some(signer) == >::get().as_ref() => Ok(()),
_ => Err(BadOrigin),
}
}
/// Ensure that the pallet is in operational mode (not halted).
fn ensure_operational() -> Result<(), Error> {
if >::get() {
Err(>::Halted)
} else {
Ok(())
}
}
}
impl Pallet {
/// Get the best finalized header the pallet knows of.
///
/// Returns a dummy header if there is no best header. This can only happen
/// if the pallet has not been initialized yet.
pub fn best_finalized() -> BridgedHeader {
let hash = >::get();
>::get(hash).unwrap_or_else(|| {
>::new(
Default::default(),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
)
})
}
/// Check if a particular header is known to the bridge pallet.
pub fn is_known_header(hash: BridgedBlockHash) -> bool {
>::contains_key(hash)
}
}
/// Data required for initializing the bridge pallet.
///
/// The bridge needs to know where to start its sync from, and this provides that initial context.
#[derive(Default, Encode, Decode, RuntimeDebug, PartialEq, Clone)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct InitializationData {
/// The header from which we should start syncing.
pub header: H,
/// The initial authorities of the pallet.
pub authority_list: sp_finality_grandpa::AuthorityList,
/// The ID of the initial authority set.
pub set_id: sp_finality_grandpa::SetId,
/// Should the pallet block transaction immediately after initialization.
pub is_halted: bool,
}
pub(crate) fn find_scheduled_change(header: &H) -> Option> {
use sp_runtime::generic::OpaqueDigestItemId;
let id = OpaqueDigestItemId::Consensus(&GRANDPA_ENGINE_ID);
let filter_log = |log: ConsensusLog| match log {
ConsensusLog::ScheduledChange(change) => Some(change),
_ => None,
};
// find the first consensus digest with the right ID which converts to
// the right kind of consensus log.
header.digest().convert_first(|l| l.try_to(id).and_then(filter_log))
}
/// Checks the given header for a consensus digest signalling a **forced** scheduled change and
/// extracts it.
pub(crate) fn find_forced_change(
header: &H,
) -> Option<(H::Number, sp_finality_grandpa::ScheduledChange)> {
use sp_runtime::generic::OpaqueDigestItemId;
let id = OpaqueDigestItemId::Consensus(&GRANDPA_ENGINE_ID);
let filter_log = |log: ConsensusLog| match log {
ConsensusLog::ForcedChange(delay, change) => Some((delay, change)),
_ => None,
};
// find the first consensus digest with the right ID which converts to
// the right kind of consensus log.
header.digest().convert_first(|l| l.try_to(id).and_then(filter_log))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::{run_test, test_header, Origin, TestHash, TestHeader, TestNumber, TestRuntime};
use bp_test_utils::{alice, authority_list, bob, make_justification_for_header};
use codec::Encode;
use frame_support::weights::PostDispatchInfo;
use frame_support::{assert_err, assert_noop, assert_ok};
use sp_runtime::{Digest, DigestItem, DispatchError};
fn initialize_substrate_bridge() {
assert_ok!(init_with_origin(Origin::root()));
}
fn init_with_origin(
origin: Origin,
) -> Result, sp_runtime::DispatchErrorWithPostInfo> {
let genesis = test_header(0);
let init_data = InitializationData {
header: genesis,
authority_list: authority_list(),
set_id: 1,
is_halted: false,
};
Module::::initialize(origin, init_data.clone()).map(|_| init_data)
}
fn submit_finality_proof(child: u8, header: u8) -> frame_support::dispatch::DispatchResultWithPostInfo {
let child = test_header(child.into());
let header = test_header(header.into());
let set_id = 1;
let grandpa_round = 1;
let justification = make_justification_for_header(&header, grandpa_round, set_id, &authority_list()).encode();
let ancestry_proof = vec![child, header.clone()];
Module::::submit_finality_proof(Origin::signed(1), header, justification, ancestry_proof)
}
fn next_block() {
use frame_support::traits::OnInitialize;
let current_number = frame_system::Module::::block_number();
frame_system::Module::::set_block_number(current_number + 1);
let _ = Module::::on_initialize(current_number);
}
fn change_log(delay: u64) -> Digest {
let consensus_log = ConsensusLog::::ScheduledChange(sp_finality_grandpa::ScheduledChange {
next_authorities: vec![(alice(), 1), (bob(), 1)],
delay,
});
Digest:: {
logs: vec![DigestItem::Consensus(GRANDPA_ENGINE_ID, consensus_log.encode())],
}
}
fn forced_change_log(delay: u64) -> Digest {
let consensus_log = ConsensusLog::::ForcedChange(
delay,
sp_finality_grandpa::ScheduledChange {
next_authorities: vec![(alice(), 1), (bob(), 1)],
delay,
},
);
Digest:: {
logs: vec![DigestItem::Consensus(GRANDPA_ENGINE_ID, consensus_log.encode())],
}
}
#[test]
fn init_root_or_owner_origin_can_initialize_pallet() {
run_test(|| {
assert_noop!(init_with_origin(Origin::signed(1)), DispatchError::BadOrigin);
assert_ok!(init_with_origin(Origin::root()));
// Reset storage so we can initialize the pallet again
BestFinalized::::kill();
ModuleOwner::::put(2);
assert_ok!(init_with_origin(Origin::signed(2)));
})
}
#[test]
fn init_storage_entries_are_correctly_initialized() {
run_test(|| {
assert_eq!(
BestFinalized::::get(),
BridgedBlockHash::::default()
);
assert_eq!(Module::::best_finalized(), test_header(0));
let init_data = init_with_origin(Origin::root()).unwrap();
assert!(>::contains_key(init_data.header.hash()));
assert_eq!(BestFinalized::::get(), init_data.header.hash());
assert_eq!(
CurrentAuthoritySet::::get().authorities,
init_data.authority_list
);
assert_eq!(IsHalted::::get(), false);
})
}
#[test]
fn init_can_only_initialize_pallet_once() {
run_test(|| {
initialize_substrate_bridge();
assert_noop!(
init_with_origin(Origin::root()),
>::AlreadyInitialized
);
})
}
#[test]
fn pallet_owner_may_change_owner() {
run_test(|| {
ModuleOwner::::put(2);
assert_ok!(Module::::set_owner(Origin::root(), Some(1)));
assert_noop!(
Module::::halt_operations(Origin::signed(2)),
DispatchError::BadOrigin,
);
assert_ok!(Module::::halt_operations(Origin::root()));
assert_ok!(Module::::set_owner(Origin::signed(1), None));
assert_noop!(
Module::::resume_operations(Origin::signed(1)),
DispatchError::BadOrigin,
);
assert_noop!(
Module::::resume_operations(Origin::signed(2)),
DispatchError::BadOrigin,
);
assert_ok!(Module::::resume_operations(Origin::root()));
});
}
#[test]
fn pallet_may_be_halted_by_root() {
run_test(|| {
assert_ok!(Module::::halt_operations(Origin::root()));
assert_ok!(Module::::resume_operations(Origin::root()));
});
}
#[test]
fn pallet_may_be_halted_by_owner() {
run_test(|| {
ModuleOwner::::put(2);
assert_ok!(Module::::halt_operations(Origin::signed(2)));
assert_ok!(Module::::resume_operations(Origin::signed(2)));
assert_noop!(
Module::::halt_operations(Origin::signed(1)),
DispatchError::BadOrigin,
);
assert_noop!(
Module::::resume_operations(Origin::signed(1)),
DispatchError::BadOrigin,
);
assert_ok!(Module::::halt_operations(Origin::signed(2)));
assert_noop!(
Module::::resume_operations(Origin::signed(1)),
DispatchError::BadOrigin,
);
});
}
#[test]
fn pallet_rejects_transactions_if_halted() {
run_test(|| {
>::put(true);
assert_noop!(
Module::::submit_finality_proof(Origin::signed(1), test_header(1), vec![], vec![]),
Error::::Halted,
);
})
}
#[test]
fn succesfully_imports_header_with_valid_finality_and_ancestry_proofs() {
run_test(|| {
initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2));
let header = test_header(2);
assert_eq!(>::get(), header.hash());
assert!(>::contains_key(header.hash()));
})
}
#[test]
fn rejects_justification_that_skips_authority_set_transition() {
run_test(|| {
initialize_substrate_bridge();
let child = test_header(1);
let header = test_header(2);
let set_id = 2;
let grandpa_round = 1;
let justification =
make_justification_for_header(&header, grandpa_round, set_id, &authority_list()).encode();
let ancestry_proof = vec![child, header.clone()];
assert_err!(
Module::::submit_finality_proof(Origin::signed(1), header, justification, ancestry_proof,),
>::InvalidJustification
);
})
}
#[test]
fn does_not_import_header_with_invalid_finality_proof() {
run_test(|| {
initialize_substrate_bridge();
let child = test_header(1);
let header = test_header(2);
let justification = [1u8; 32].encode();
let ancestry_proof = vec![child, header.clone()];
assert_err!(
Module::::submit_finality_proof(Origin::signed(1), header, justification, ancestry_proof,),
>::InvalidJustification
);
})
}
#[test]
fn does_not_import_header_with_invalid_ancestry_proof() {
run_test(|| {
initialize_substrate_bridge();
let header = test_header(2);
let set_id = 1;
let grandpa_round = 1;
let justification =
make_justification_for_header(&header, grandpa_round, set_id, &authority_list()).encode();
// For testing, we've made it so that an empty ancestry proof is invalid
let ancestry_proof = vec![];
assert_err!(
Module::::submit_finality_proof(Origin::signed(1), header, justification, ancestry_proof,),
>::InvalidAncestryProof
);
})
}
#[test]
fn disallows_invalid_authority_set() {
run_test(|| {
use bp_test_utils::{alice, bob};
let genesis = test_header(0);
let invalid_authority_list = vec![(alice(), u64::MAX), (bob(), u64::MAX)];
let init_data = InitializationData {
header: genesis,
authority_list: invalid_authority_list,
set_id: 1,
is_halted: false,
};
assert_ok!(Module::::initialize(Origin::root(), init_data));
let header = test_header(1);
let justification = [1u8; 32].encode();
let ancestry_proof = vec![];
assert_err!(
Module::::submit_finality_proof(Origin::signed(1), header, justification, ancestry_proof,),
>::InvalidAuthoritySet
);
})
}
#[test]
fn importing_header_ensures_that_chain_is_extended() {
run_test(|| {
initialize_substrate_bridge();
assert_ok!(submit_finality_proof(5, 6));
assert_err!(submit_finality_proof(3, 4), Error::::OldHeader);
assert_ok!(submit_finality_proof(7, 8));
})
}
#[test]
fn importing_header_enacts_new_authority_set() {
run_test(|| {
initialize_substrate_bridge();
let next_set_id = 2;
let next_authorities = vec![(alice(), 1), (bob(), 1)];
// Need to update the header digest to indicate that our header signals an authority set
// change. The change will be enacted when we import our header.
let mut header = test_header(2);
header.digest = change_log(0);
// Let's import our test header
assert_ok!(pallet::import_header::(header.hash(), header.clone()));
// Make sure that our header is the best finalized
assert_eq!(>::get(), header.hash());
assert!(>::contains_key(header.hash()));
// Make sure that the authority set actually changed upon importing our header
assert_eq!(
>::get(),
bp_header_chain::AuthoritySet::new(next_authorities, next_set_id),
);
})
}
#[test]
fn importing_header_rejects_header_with_scheduled_change_delay() {
run_test(|| {
initialize_substrate_bridge();
// Need to update the header digest to indicate that our header signals an authority set
// change. However, the change doesn't happen until the next block.
let mut header = test_header(2);
header.digest = change_log(1);
// Should not be allowed to import this header
assert_err!(
pallet::import_header::(header.hash(), header),
>::UnsupportedScheduledChange
);
})
}
#[test]
fn importing_header_rejects_header_with_forced_changes() {
run_test(|| {
initialize_substrate_bridge();
// Need to update the header digest to indicate that it signals a forced authority set
// change.
let mut header = test_header(2);
header.digest = forced_change_log(0);
// Should not be allowed to import this header
assert_err!(
pallet::import_header::(header.hash(), header),
>::UnsupportedScheduledChange
);
})
}
#[test]
fn rate_limiter_disallows_imports_once_limit_is_hit_in_single_block() {
run_test(|| {
initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2));
assert_ok!(submit_finality_proof(3, 4));
assert_err!(submit_finality_proof(5, 6), >::TooManyRequests);
})
}
#[test]
fn rate_limiter_invalid_requests_do_not_count_towards_request_count() {
run_test(|| {
let submit_invalid_request = || {
let child = test_header(1);
let header = test_header(2);
let invalid_justification = vec![4, 2, 4, 2].encode();
let ancestry_proof = vec![child, header.clone()];
Module::::submit_finality_proof(
Origin::signed(1),
header,
invalid_justification,
ancestry_proof,
)
};
initialize_substrate_bridge();
for _ in 0..::MaxRequests::get() + 1 {
// Notice that the error here *isn't* `TooManyRequests`
assert_err!(submit_invalid_request(), >::InvalidJustification);
}
// Can still submit `MaxRequests` requests afterwards
assert_ok!(submit_finality_proof(1, 2));
assert_ok!(submit_finality_proof(3, 4));
assert_err!(submit_finality_proof(5, 6), >::TooManyRequests);
})
}
#[test]
fn rate_limiter_allows_request_after_new_block_has_started() {
run_test(|| {
initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2));
assert_ok!(submit_finality_proof(3, 4));
next_block();
assert_ok!(submit_finality_proof(5, 6));
})
}
#[test]
fn rate_limiter_disallows_imports_once_limit_is_hit_across_different_blocks() {
run_test(|| {
initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2));
assert_ok!(submit_finality_proof(3, 4));
next_block();
assert_ok!(submit_finality_proof(5, 6));
assert_err!(submit_finality_proof(7, 8), >::TooManyRequests);
})
}
#[test]
fn rate_limiter_allows_max_requests_after_long_time_with_no_activity() {
run_test(|| {
initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2));
assert_ok!(submit_finality_proof(3, 4));
next_block();
next_block();
next_block();
assert_ok!(submit_finality_proof(5, 6));
assert_ok!(submit_finality_proof(7, 8));
})
}
}