Unify Substrate Bridge Pallet with Finality Verifier (#783)

* Add relevant storage items from Substrate pallet

* Add function for importing finalized headers to storage

* Remove unused getter functions

* Add GenesisConfig to pallet

* Add initialization extrinsic

* Add operational extrinsic

* Get existing finality verifier tests compiling again

* Add tests for pallet initialization

* Add tests related to pallet's operational status

* Update tests which were using `pallet-substrate-bridge`

* Add tests related to header imports

* Use wrapper function when init-ing some tests

* Add prefix to tests related to rate limiter

* Fix failed compilation related to GenesisConfig

* Add some documentation

* Change some extrinsics to be Operational

* Add public interface to pallet

* Implement runtime APIs for finality-verifier pallet

* Justify use of `expect` when importing headers

* Reject headers with forced changes

* Add weight to initialize extrinsic

* Remove TODO which will be addressed later

* Move succesful import log to correct location

* Expand proof for when `best_finalized` is fetched

* Move check for newer finalized blocks earlier in pipeline

* Rename `ConflictingFork` error to be more generic

* Only compute finality_target's hash once

* Add missing documentation to Runtime APIs

* Add TODO about using `set_id` from `ScheduledChange` digest
This commit is contained in:
Hernando Castano
2021-03-04 14:50:30 -05:00
committed by Bastian Köcher
parent 95f5d2248e
commit 6cfd87783e
7 changed files with 630 additions and 43 deletions
+11
View File
@@ -559,6 +559,17 @@ impl_runtime_apis! {
} }
} }
impl bp_rialto::RialtoFinalityApi<Block> for Runtime {
fn best_finalized() -> (bp_rialto::BlockNumber, bp_rialto::Hash) {
let header = BridgeFinalityVerifier::best_finalized();
(header.number, header.hash())
}
fn is_known_header(hash: bp_rialto::Hash) -> bool {
BridgeFinalityVerifier::is_known_header(hash)
}
}
impl bp_rialto::ToRialtoOutboundLaneApi<Block, Balance, ToRialtoMessagePayload> for Runtime { impl bp_rialto::ToRialtoOutboundLaneApi<Block, Balance, ToRialtoMessagePayload> for Runtime {
fn estimate_message_delivery_and_dispatch_fee( fn estimate_message_delivery_and_dispatch_fee(
_lane_id: bp_message_lane::LaneId, _lane_id: bp_message_lane::LaneId,
+11
View File
@@ -642,6 +642,17 @@ impl_runtime_apis! {
} }
} }
impl bp_millau::MillauFinalityApi<Block> for Runtime {
fn best_finalized() -> (bp_millau::BlockNumber, bp_millau::Hash) {
let header = BridgeFinalityVerifier::best_finalized();
(header.number, header.hash())
}
fn is_known_header(hash: bp_millau::Hash) -> bool {
BridgeFinalityVerifier::is_known_header(hash)
}
}
impl bp_currency_exchange::RialtoCurrencyExchangeApi<Block, exchange::EthereumTransactionInclusionProof> for Runtime { impl bp_currency_exchange::RialtoCurrencyExchangeApi<Block, exchange::EthereumTransactionInclusionProof> for Runtime {
fn filter_transaction_proof(proof: exchange::EthereumTransactionInclusionProof) -> bool { fn filter_transaction_proof(proof: exchange::EthereumTransactionInclusionProof) -> bool {
BridgeRialtoCurrencyExchange::filter_transaction_proof(&proof) BridgeRialtoCurrencyExchange::filter_transaction_proof(&proof)
@@ -21,6 +21,7 @@ bp-header-chain = { path = "../../primitives/header-chain", default-features = f
frame-support = { git = "https://github.com/paritytech/substrate.git", branch = "master" , default-features = false } frame-support = { git = "https://github.com/paritytech/substrate.git", branch = "master" , default-features = false }
frame-system = { git = "https://github.com/paritytech/substrate.git", branch = "master" , default-features = false } frame-system = { git = "https://github.com/paritytech/substrate.git", branch = "master" , default-features = false }
sp-finality-grandpa = { git = "https://github.com/paritytech/substrate.git", branch = "master" , default-features = false }
sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "master" , default-features = false } sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "master" , default-features = false }
sp-std = { git = "https://github.com/paritytech/substrate.git", branch = "master" , default-features = false } sp-std = { git = "https://github.com/paritytech/substrate.git", branch = "master" , default-features = false }
@@ -40,6 +41,7 @@ std = [
"frame-support/std", "frame-support/std",
"frame-system/std", "frame-system/std",
"serde", "serde",
"sp-finality-grandpa/std",
"sp-runtime/std", "sp-runtime/std",
"sp-std/std", "sp-std/std",
] ]
+568 -37
View File
@@ -33,11 +33,16 @@
#![allow(clippy::large_enum_variant)] #![allow(clippy::large_enum_variant)]
use bp_header_chain::{justification::verify_justification, AncestryChecker, HeaderChain}; use bp_header_chain::{justification::verify_justification, AncestryChecker, HeaderChain};
use bp_runtime::{Chain, HeaderOf}; use bp_runtime::{BlockNumberOf, Chain, HashOf, HasherOf, HeaderOf};
use codec::{Decode, Encode};
use finality_grandpa::voter_set::VoterSet; use finality_grandpa::voter_set::VoterSet;
use frame_support::{dispatch::DispatchError, ensure}; use frame_support::{dispatch::DispatchError, ensure};
use frame_system::ensure_signed; use frame_system::{ensure_signed, RawOrigin};
use sp_runtime::traits::Header as HeaderT; #[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; use sp_std::vec::Vec;
#[cfg(test)] #[cfg(test)]
@@ -46,15 +51,21 @@ mod mock;
// Re-export in crate namespace for `construct_runtime!` // Re-export in crate namespace for `construct_runtime!`
pub use pallet::*; pub use pallet::*;
/// Block number of the bridged chain.
pub type BridgedBlockNumber<T> = BlockNumberOf<<T as Config>::BridgedChain>;
/// Block hash of the bridged chain.
pub type BridgedBlockHash<T> = HashOf<<T as Config>::BridgedChain>;
/// Hasher of the bridged chain.
pub type _BridgedBlockHasher<T> = HasherOf<<T as Config>::BridgedChain>;
/// Header of the bridged chain.
pub type BridgedHeader<T> = HeaderOf<<T as Config>::BridgedChain>;
#[frame_support::pallet] #[frame_support::pallet]
pub mod pallet { pub mod pallet {
use super::*; use super::*;
use frame_support::pallet_prelude::*; use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*; use frame_system::pallet_prelude::*;
/// Header of the bridged chain.
pub(crate) type BridgedHeader<T> = HeaderOf<<T as Config>::BridgedChain>;
#[pallet::config] #[pallet::config]
pub trait Config: frame_system::Config { pub trait Config: frame_system::Config {
/// The chain we are bridging to here. /// The chain we are bridging to here.
@@ -113,6 +124,7 @@ pub mod pallet {
justification: Vec<u8>, justification: Vec<u8>,
ancestry_proof: T::AncestryProof, ancestry_proof: T::AncestryProof,
) -> DispatchResultWithPostInfo { ) -> DispatchResultWithPostInfo {
ensure_operational::<T>()?;
let _ = ensure_signed(origin)?; let _ = ensure_signed(origin)?;
ensure!( ensure!(
@@ -120,14 +132,24 @@ pub mod pallet {
<Error<T>>::TooManyRequests <Error<T>>::TooManyRequests
); );
let (hash, number) = (finality_target.hash(), finality_target.number());
frame_support::debug::trace!("Going to try and finalize header {:?}", finality_target); frame_support::debug::trace!("Going to try and finalize header {:?}", finality_target);
let authority_set = T::HeaderChain::authority_set(); let best_finalized = <ImportedHeaders<T>>::get(<BestFinalized<T>>::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, <Error<T>>::OldHeader);
let authority_set = <CurrentAuthoritySet<T>>::get();
let voter_set = VoterSet::new(authority_set.authorities).ok_or(<Error<T>>::InvalidAuthoritySet)?; let voter_set = VoterSet::new(authority_set.authorities).ok_or(<Error<T>>::InvalidAuthoritySet)?;
let set_id = authority_set.set_id; let set_id = authority_set.set_id;
let (hash, number) = (finality_target.hash(), *finality_target.number()); verify_justification::<BridgedHeader<T>>((hash, *number), set_id, voter_set, &justification).map_err(
verify_justification::<BridgedHeader<T>>((hash, number), set_id, voter_set, &justification).map_err(
|e| { |e| {
frame_support::debug::error!("Received invalid justification for {:?}: {:?}", finality_target, e); frame_support::debug::error!("Received invalid justification for {:?}: {:?}", finality_target, e);
<Error<T>>::InvalidJustification <Error<T>>::InvalidJustification
@@ -142,10 +164,84 @@ pub mod pallet {
<Error<T>>::InvalidAncestryProof <Error<T>>::InvalidAncestryProof
); );
let _ = T::HeaderChain::append_header(finality_target)?; let _ = T::HeaderChain::append_header(finality_target.clone())?;
import_header::<T>(hash, finality_target)?;
<RequestCount<T>>::mutate(|count| *count += 1);
frame_support::debug::info!("Succesfully imported finalized header with hash {:?}!", hash); frame_support::debug::info!("Succesfully imported finalized header with hash {:?}!", hash);
<RequestCount<T>>::mutate(|count| *count += 1); 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<T>,
init_data: super::InitializationData<BridgedHeader<T>>,
) -> DispatchResultWithPostInfo {
ensure_owner_or_root::<T>(origin)?;
let init_allowed = !<BestFinalized<T>>::exists();
ensure!(init_allowed, <Error<T>>::AlreadyInitialized);
initialize_bridge::<T>(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<T>, new_owner: Option<T::AccountId>) -> DispatchResultWithPostInfo {
ensure_owner_or_root::<T>(origin)?;
match new_owner {
Some(new_owner) => {
ModuleOwner::<T>::put(&new_owner);
frame_support::debug::info!("Setting pallet Owner to: {:?}", new_owner);
}
None => {
ModuleOwner::<T>::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<T>) -> DispatchResultWithPostInfo {
ensure_owner_or_root::<T>(origin)?;
<IsHalted<T>>::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<T>) -> DispatchResultWithPostInfo {
ensure_owner_or_root::<T>(origin)?;
<IsHalted<T>>::put(false);
frame_support::debug::info!("Resuming pallet operations.");
Ok(().into()) Ok(().into())
} }
@@ -162,6 +258,68 @@ pub mod pallet {
#[pallet::getter(fn request_count)] #[pallet::getter(fn request_count)]
pub(super) type RequestCount<T: Config> = StorageValue<_, u32, ValueQuery>; pub(super) type RequestCount<T: Config> = StorageValue<_, u32, ValueQuery>;
/// Hash of the header used to bootstrap the pallet.
#[pallet::storage]
pub(super) type InitialHash<T: Config> = StorageValue<_, BridgedBlockHash<T>, ValueQuery>;
/// Hash of the best finalized header.
#[pallet::storage]
pub(super) type BestFinalized<T: Config> = StorageValue<_, BridgedBlockHash<T>, ValueQuery>;
/// Headers which have been imported into the pallet.
#[pallet::storage]
pub(super) type ImportedHeaders<T: Config> = StorageMap<_, Identity, BridgedBlockHash<T>, BridgedHeader<T>>;
/// The current GRANDPA Authority set.
#[pallet::storage]
pub(super) type CurrentAuthoritySet<T: Config> = 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<T: Config> = StorageValue<_, T::AccountId, OptionQuery>;
/// If true, all pallet transactions are failed immediately.
#[pallet::storage]
pub(super) type IsHalted<T: Config> = StorageValue<_, bool, ValueQuery>;
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
owner: Option<T::AccountId>,
init_data: Option<super::InitializationData<BridgedHeader<T>>>,
}
#[cfg(feature = "std")]
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
owner: None,
init_data: None,
}
}
}
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
fn build(&self) {
if let Some(ref owner) = self.owner {
<ModuleOwner<T>>::put(owner);
}
if let Some(init_data) = self.init_data.clone() {
initialize_bridge::<T>(init_data);
} else {
// Since the bridge hasn't been initialized we shouldn't allow anyone to perform
// transactions.
<IsHalted<T>>::put(true);
}
}
}
#[pallet::error] #[pallet::error]
pub enum Error<T> { pub enum Error<T> {
/// The given justification is invalid for the given header. /// The given justification is invalid for the given header.
@@ -175,32 +333,199 @@ pub mod pallet {
FailedToWriteHeader, FailedToWriteHeader,
/// There are too many requests for the current window to handle. /// There are too many requests for the current window to handle.
TooManyRequests, 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<T: Config>(
hash: BridgedBlockHash<T>,
header: BridgedHeader<T>,
) -> 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(),
<Error<T>>::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(), <Error<T>>::UnsupportedScheduledChange);
// TODO [#788]: Stop manually increasing the `set_id` here.
let next_authorities = bp_header_chain::AuthoritySet {
authorities: change.next_authorities,
set_id: <CurrentAuthoritySet<T>>::get().set_id + 1,
};
// Since our header schedules a change and we know the delay is 0, it must also enact
// the change.
<CurrentAuthoritySet<T>>::put(next_authorities);
};
<BestFinalized<T>>::put(hash);
<ImportedHeaders<T>>::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<T: Config>(init_params: super::InitializationData<BridgedHeader<T>>) {
let super::InitializationData {
header,
authority_list,
set_id,
is_halted,
} = init_params;
let initial_hash = header.hash();
<InitialHash<T>>::put(initial_hash);
<BestFinalized<T>>::put(initial_hash);
<ImportedHeaders<T>>::insert(initial_hash, header);
let authority_set = bp_header_chain::AuthoritySet::new(authority_list, set_id);
<CurrentAuthoritySet<T>>::put(authority_set);
<IsHalted<T>>::put(is_halted);
}
/// Ensure that the origin is either root, or `ModuleOwner`.
fn ensure_owner_or_root<T: Config>(origin: T::Origin) -> Result<(), BadOrigin> {
match origin.into() {
Ok(RawOrigin::Root) => Ok(()),
Ok(RawOrigin::Signed(ref signer)) if Some(signer) == <ModuleOwner<T>>::get().as_ref() => Ok(()),
_ => Err(BadOrigin),
}
}
/// Ensure that the pallet is in operational mode (not halted).
fn ensure_operational<T: Config>() -> Result<(), Error<T>> {
if <IsHalted<T>>::get() {
Err(<Error<T>>::Halted)
} else {
Ok(())
}
}
}
impl<T: Config> Pallet<T> {
/// 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<T> {
let hash = <BestFinalized<T>>::get();
<ImportedHeaders<T>>::get(hash).unwrap_or_else(|| {
<BridgedHeader<T>>::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<T>) -> bool {
<ImportedHeaders<T>>::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<H: HeaderT> {
/// 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<H: HeaderT>(header: &H) -> Option<sp_finality_grandpa::ScheduledChange<H::Number>> {
use sp_runtime::generic::OpaqueDigestItemId;
let id = OpaqueDigestItemId::Consensus(&GRANDPA_ENGINE_ID);
let filter_log = |log: ConsensusLog<H::Number>| 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<H: HeaderT>(
header: &H,
) -> Option<(H::Number, sp_finality_grandpa::ScheduledChange<H::Number>)> {
use sp_runtime::generic::OpaqueDigestItemId;
let id = OpaqueDigestItemId::Consensus(&GRANDPA_ENGINE_ID);
let filter_log = |log: ConsensusLog<H::Number>| 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::mock::{run_test, test_header, Origin, TestRuntime}; use crate::mock::{run_test, test_header, Origin, TestHash, TestHeader, TestNumber, TestRuntime};
use bp_test_utils::{authority_list, make_justification_for_header}; use bp_test_utils::{alice, authority_list, bob, make_justification_for_header};
use codec::Encode; use codec::Encode;
use frame_support::{assert_err, assert_ok}; use frame_support::weights::PostDispatchInfo;
use frame_support::{assert_err, assert_noop, assert_ok};
use sp_runtime::{Digest, DigestItem, DispatchError};
fn initialize_substrate_bridge() { fn initialize_substrate_bridge() {
assert_ok!(init_with_origin(Origin::root()));
}
fn init_with_origin(
origin: Origin,
) -> Result<InitializationData<TestHeader>, sp_runtime::DispatchErrorWithPostInfo<PostDispatchInfo>> {
let genesis = test_header(0); let genesis = test_header(0);
let init_data = pallet_substrate_bridge::InitializationData { let init_data = InitializationData {
header: genesis, header: genesis,
authority_list: authority_list(), authority_list: authority_list(),
set_id: 1, set_id: 1,
scheduled_change: None,
is_halted: false, is_halted: false,
}; };
assert_ok!(pallet_substrate_bridge::Module::<TestRuntime>::initialize( Module::<TestRuntime>::initialize(origin, init_data.clone()).map(|_| init_data)
Origin::root(),
init_data
));
} }
fn submit_finality_proof(child: u8, header: u8) -> frame_support::dispatch::DispatchResultWithPostInfo { fn submit_finality_proof(child: u8, header: u8) -> frame_support::dispatch::DispatchResultWithPostInfo {
@@ -223,6 +548,146 @@ mod tests {
let _ = Module::<TestRuntime>::on_initialize(current_number); let _ = Module::<TestRuntime>::on_initialize(current_number);
} }
fn change_log(delay: u64) -> Digest<TestHash> {
let consensus_log = ConsensusLog::<TestNumber>::ScheduledChange(sp_finality_grandpa::ScheduledChange {
next_authorities: vec![(alice(), 1), (bob(), 1)],
delay,
});
Digest::<TestHash> {
logs: vec![DigestItem::Consensus(GRANDPA_ENGINE_ID, consensus_log.encode())],
}
}
fn forced_change_log(delay: u64) -> Digest<TestHash> {
let consensus_log = ConsensusLog::<TestNumber>::ForcedChange(
delay,
sp_finality_grandpa::ScheduledChange {
next_authorities: vec![(alice(), 1), (bob(), 1)],
delay,
},
);
Digest::<TestHash> {
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::<TestRuntime>::kill();
ModuleOwner::<TestRuntime>::put(2);
assert_ok!(init_with_origin(Origin::signed(2)));
})
}
#[test]
fn init_storage_entries_are_correctly_initialized() {
run_test(|| {
assert_eq!(
BestFinalized::<TestRuntime>::get(),
BridgedBlockHash::<TestRuntime>::default()
);
assert_eq!(Module::<TestRuntime>::best_finalized(), test_header(0));
let init_data = init_with_origin(Origin::root()).unwrap();
assert!(<ImportedHeaders<TestRuntime>>::contains_key(init_data.header.hash()));
assert_eq!(BestFinalized::<TestRuntime>::get(), init_data.header.hash());
assert_eq!(
CurrentAuthoritySet::<TestRuntime>::get().authorities,
init_data.authority_list
);
assert_eq!(IsHalted::<TestRuntime>::get(), false);
})
}
#[test]
fn init_can_only_initialize_pallet_once() {
run_test(|| {
initialize_substrate_bridge();
assert_noop!(
init_with_origin(Origin::root()),
<Error<TestRuntime>>::AlreadyInitialized
);
})
}
#[test]
fn pallet_owner_may_change_owner() {
run_test(|| {
ModuleOwner::<TestRuntime>::put(2);
assert_ok!(Module::<TestRuntime>::set_owner(Origin::root(), Some(1)));
assert_noop!(
Module::<TestRuntime>::halt_operations(Origin::signed(2)),
DispatchError::BadOrigin,
);
assert_ok!(Module::<TestRuntime>::halt_operations(Origin::root()));
assert_ok!(Module::<TestRuntime>::set_owner(Origin::signed(1), None));
assert_noop!(
Module::<TestRuntime>::resume_operations(Origin::signed(1)),
DispatchError::BadOrigin,
);
assert_noop!(
Module::<TestRuntime>::resume_operations(Origin::signed(2)),
DispatchError::BadOrigin,
);
assert_ok!(Module::<TestRuntime>::resume_operations(Origin::root()));
});
}
#[test]
fn pallet_may_be_halted_by_root() {
run_test(|| {
assert_ok!(Module::<TestRuntime>::halt_operations(Origin::root()));
assert_ok!(Module::<TestRuntime>::resume_operations(Origin::root()));
});
}
#[test]
fn pallet_may_be_halted_by_owner() {
run_test(|| {
ModuleOwner::<TestRuntime>::put(2);
assert_ok!(Module::<TestRuntime>::halt_operations(Origin::signed(2)));
assert_ok!(Module::<TestRuntime>::resume_operations(Origin::signed(2)));
assert_noop!(
Module::<TestRuntime>::halt_operations(Origin::signed(1)),
DispatchError::BadOrigin,
);
assert_noop!(
Module::<TestRuntime>::resume_operations(Origin::signed(1)),
DispatchError::BadOrigin,
);
assert_ok!(Module::<TestRuntime>::halt_operations(Origin::signed(2)));
assert_noop!(
Module::<TestRuntime>::resume_operations(Origin::signed(1)),
DispatchError::BadOrigin,
);
});
}
#[test]
fn pallet_rejects_transactions_if_halted() {
run_test(|| {
<IsHalted<TestRuntime>>::put(true);
assert_noop!(
Module::<TestRuntime>::submit_finality_proof(Origin::signed(1), test_header(1), vec![], vec![]),
Error::<TestRuntime>::Halted,
);
})
}
#[test] #[test]
fn succesfully_imports_header_with_valid_finality_and_ancestry_proofs() { fn succesfully_imports_header_with_valid_finality_and_ancestry_proofs() {
run_test(|| { run_test(|| {
@@ -231,12 +696,8 @@ mod tests {
assert_ok!(submit_finality_proof(1, 2)); assert_ok!(submit_finality_proof(1, 2));
let header = test_header(2); let header = test_header(2);
assert_eq!( assert_eq!(<BestFinalized<TestRuntime>>::get(), header.hash());
pallet_substrate_bridge::Module::<TestRuntime>::best_headers(), assert!(<ImportedHeaders<TestRuntime>>::contains_key(header.hash()));
vec![(*header.number(), header.hash())]
);
assert_eq!(pallet_substrate_bridge::Module::<TestRuntime>::best_finalized(), header);
}) })
} }
@@ -309,18 +770,14 @@ mod tests {
let genesis = test_header(0); let genesis = test_header(0);
let invalid_authority_list = vec![(alice(), u64::MAX), (bob(), u64::MAX)]; let invalid_authority_list = vec![(alice(), u64::MAX), (bob(), u64::MAX)];
let init_data = pallet_substrate_bridge::InitializationData { let init_data = InitializationData {
header: genesis, header: genesis,
authority_list: invalid_authority_list, authority_list: invalid_authority_list,
set_id: 1, set_id: 1,
scheduled_change: None,
is_halted: false, is_halted: false,
}; };
assert_ok!(pallet_substrate_bridge::Module::<TestRuntime>::initialize( assert_ok!(Module::<TestRuntime>::initialize(Origin::root(), init_data));
Origin::root(),
init_data
));
let header = test_header(1); let header = test_header(1);
let justification = [1u8; 32].encode(); let justification = [1u8; 32].encode();
@@ -334,7 +791,81 @@ mod tests {
} }
#[test] #[test]
fn disallows_imports_once_limit_is_hit_in_single_block() { 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::<TestRuntime>::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::<TestRuntime>(header.hash(), header.clone()));
// Make sure that our header is the best finalized
assert_eq!(<BestFinalized<TestRuntime>>::get(), header.hash());
assert!(<ImportedHeaders<TestRuntime>>::contains_key(header.hash()));
// Make sure that the authority set actually changed upon importing our header
assert_eq!(
<CurrentAuthoritySet<TestRuntime>>::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::<TestRuntime>(header.hash(), header),
<Error<TestRuntime>>::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::<TestRuntime>(header.hash(), header),
<Error<TestRuntime>>::UnsupportedScheduledChange
);
})
}
#[test]
fn rate_limiter_disallows_imports_once_limit_is_hit_in_single_block() {
run_test(|| { run_test(|| {
initialize_substrate_bridge(); initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2)); assert_ok!(submit_finality_proof(1, 2));
@@ -344,7 +875,7 @@ mod tests {
} }
#[test] #[test]
fn invalid_requests_do_not_count_towards_request_count() { fn rate_limiter_invalid_requests_do_not_count_towards_request_count() {
run_test(|| { run_test(|| {
let submit_invalid_request = || { let submit_invalid_request = || {
let child = test_header(1); let child = test_header(1);
@@ -376,7 +907,7 @@ mod tests {
} }
#[test] #[test]
fn allows_request_after_new_block_has_started() { fn rate_limiter_allows_request_after_new_block_has_started() {
run_test(|| { run_test(|| {
initialize_substrate_bridge(); initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2)); assert_ok!(submit_finality_proof(1, 2));
@@ -388,7 +919,7 @@ mod tests {
} }
#[test] #[test]
fn disallows_imports_once_limit_is_hit_across_different_blocks() { fn rate_limiter_disallows_imports_once_limit_is_hit_across_different_blocks() {
run_test(|| { run_test(|| {
initialize_substrate_bridge(); initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2)); assert_ok!(submit_finality_proof(1, 2));
@@ -401,7 +932,7 @@ mod tests {
} }
#[test] #[test]
fn allows_max_requests_after_long_time_with_no_activity() { fn rate_limiter_allows_max_requests_after_long_time_with_no_activity() {
run_test(|| { run_test(|| {
initialize_substrate_bridge(); initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2)); assert_ok!(submit_finality_proof(1, 2));
@@ -17,8 +17,7 @@
// From construct_runtime macro // From construct_runtime macro
#![allow(clippy::from_over_into)] #![allow(clippy::from_over_into)]
use crate::pallet::{BridgedHeader, Config}; use bp_runtime::Chain;
use bp_runtime::{BlockNumberOf, Chain};
use frame_support::{construct_runtime, parameter_types, weights::Weight}; use frame_support::{construct_runtime, parameter_types, weights::Weight};
use sp_runtime::{ use sp_runtime::{
testing::{Header, H256}, testing::{Header, H256},
@@ -27,8 +26,9 @@ use sp_runtime::{
}; };
pub type AccountId = u64; pub type AccountId = u64;
pub type TestHeader = BridgedHeader<TestRuntime>; pub type TestHeader = crate::BridgedHeader<TestRuntime>;
pub type TestNumber = BlockNumberOf<<TestRuntime as Config>::BridgedChain>; pub type TestNumber = crate::BridgedBlockNumber<TestRuntime>;
pub type TestHash = crate::BridgedBlockHash<TestRuntime>;
type Block = frame_system::mocking::MockBlock<TestRuntime>; type Block = frame_system::mocking::MockBlock<TestRuntime>;
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<TestRuntime>; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<TestRuntime>;
+17 -1
View File
@@ -238,6 +238,11 @@ pub const IS_KNOWN_MILLAU_BLOCK_METHOD: &str = "MillauHeaderApi_is_known_block";
/// Name of the `MillauHeaderApi::incomplete_headers` runtime method. /// Name of the `MillauHeaderApi::incomplete_headers` runtime method.
pub const INCOMPLETE_MILLAU_HEADERS_METHOD: &str = "MillauHeaderApi_incomplete_headers"; pub const INCOMPLETE_MILLAU_HEADERS_METHOD: &str = "MillauHeaderApi_incomplete_headers";
/// Name of the `RialtoFinalityApi::best_finalized` runtime method.
pub const BEST_FINALIZED_MILLAU_HEADER_METHOD: &str = "MillauFinalityApi_best_finalized";
/// Name of the `RialtoFinalityApi::is_known_header` runtime method.
pub const IS_KNOW_MILLAU_HEADER_METHOD: &str = "MillauFinalityApi_is_known_header";
/// Name of the `ToMillauOutboundLaneApi::estimate_message_delivery_and_dispatch_fee` runtime method. /// Name of the `ToMillauOutboundLaneApi::estimate_message_delivery_and_dispatch_fee` runtime method.
pub const TO_MILLAU_ESTIMATE_MESSAGE_FEE_METHOD: &str = pub const TO_MILLAU_ESTIMATE_MESSAGE_FEE_METHOD: &str =
"ToMillauOutboundLaneApi_estimate_message_delivery_and_dispatch_fee"; "ToMillauOutboundLaneApi_estimate_message_delivery_and_dispatch_fee";
@@ -258,7 +263,7 @@ pub const FROM_MILLAU_UNREWARDED_RELAYERS_STATE: &str = "FromMillauInboundLaneAp
sp_api::decl_runtime_apis! { sp_api::decl_runtime_apis! {
/// API for querying information about Millau headers from the Bridge Pallet instance. /// API for querying information about Millau headers from the Bridge Pallet instance.
/// ///
/// This API is implemented by runtimes that are bridging with Millau chain, not the /// This API is implemented by runtimes that are bridging with the Millau chain, not the
/// Millau runtime itself. /// Millau runtime itself.
pub trait MillauHeaderApi { pub trait MillauHeaderApi {
/// Returns number and hash of the best blocks known to the bridge module. /// Returns number and hash of the best blocks known to the bridge module.
@@ -281,6 +286,17 @@ sp_api::decl_runtime_apis! {
fn is_finalized_block(hash: Hash) -> bool; fn is_finalized_block(hash: Hash) -> bool;
} }
/// API for querying information about the finalized Millau headers.
///
/// This API is implemented by runtimes that are bridging with the Millau chain, not the
/// Millau runtime itself.
pub trait MillauFinalityApi {
/// Returns number and hash of the best finalized header known to the bridge module.
fn best_finalized() -> (BlockNumber, Hash);
/// Returns true if the header is known to the runtime.
fn is_known_header(hash: Hash) -> bool;
}
/// Outbound message lane API for messages that are sent to Millau chain. /// Outbound message lane API for messages that are sent to Millau chain.
/// ///
/// This API is implemented by runtimes that are sending messages to Millau chain, not the /// This API is implemented by runtimes that are sending messages to Millau chain, not the
+17 -1
View File
@@ -199,6 +199,11 @@ pub const IS_KNOWN_RIALTO_BLOCK_METHOD: &str = "RialtoHeaderApi_is_known_block";
/// Name of the `RialtoHeaderApi::incomplete_headers` runtime method. /// Name of the `RialtoHeaderApi::incomplete_headers` runtime method.
pub const INCOMPLETE_RIALTO_HEADERS_METHOD: &str = "RialtoHeaderApi_incomplete_headers"; pub const INCOMPLETE_RIALTO_HEADERS_METHOD: &str = "RialtoHeaderApi_incomplete_headers";
/// Name of the `RialtoFinalityApi::best_finalized` runtime method.
pub const BEST_FINALIZED_RIALTO_HEADER_METHOD: &str = "RialtoFinalityApi_best_finalized";
/// Name of the `RialtoFinalityApi::is_known_header` runtime method.
pub const IS_KNOW_RIALTO_HEADER_METHOD: &str = "RialtoFinalityApi_is_known_header";
/// Name of the `ToRialtoOutboundLaneApi::estimate_message_delivery_and_dispatch_fee` runtime method. /// Name of the `ToRialtoOutboundLaneApi::estimate_message_delivery_and_dispatch_fee` runtime method.
pub const TO_RIALTO_ESTIMATE_MESSAGE_FEE_METHOD: &str = pub const TO_RIALTO_ESTIMATE_MESSAGE_FEE_METHOD: &str =
"ToRialtoOutboundLaneApi_estimate_message_delivery_and_dispatch_fee"; "ToRialtoOutboundLaneApi_estimate_message_delivery_and_dispatch_fee";
@@ -219,7 +224,7 @@ pub const FROM_RIALTO_UNREWARDED_RELAYERS_STATE: &str = "FromRialtoInboundLaneAp
sp_api::decl_runtime_apis! { sp_api::decl_runtime_apis! {
/// API for querying information about Rialto headers from the Bridge Pallet instance. /// API for querying information about Rialto headers from the Bridge Pallet instance.
/// ///
/// This API is implemented by runtimes that are bridging with Rialto chain, not the /// This API is implemented by runtimes that are bridging with the Rialto chain, not the
/// Rialto runtime itself. /// Rialto runtime itself.
pub trait RialtoHeaderApi { pub trait RialtoHeaderApi {
/// Returns number and hash of the best blocks known to the bridge module. /// Returns number and hash of the best blocks known to the bridge module.
@@ -242,6 +247,17 @@ sp_api::decl_runtime_apis! {
fn is_finalized_block(hash: Hash) -> bool; fn is_finalized_block(hash: Hash) -> bool;
} }
/// API for querying information about the finalized Rialto headers.
///
/// This API is implemented by runtimes that are bridging with the Rialto chain, not the
/// Millau runtime itself.
pub trait RialtoFinalityApi {
/// Returns number and hash of the best finalized header known to the bridge module.
fn best_finalized() -> (BlockNumber, Hash);
/// Returns true if the header is known to the runtime.
fn is_known_header(hash: Hash) -> bool;
}
/// Outbound message lane API for messages that are sent to Rialto chain. /// Outbound message lane API for messages that are sent to Rialto chain.
/// ///
/// This API is implemented by runtimes that are sending messages to Rialto chain, not the /// This API is implemented by runtimes that are sending messages to Rialto chain, not the