mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 21:01:02 +00:00
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:
committed by
Bastian Köcher
parent
95f5d2248e
commit
6cfd87783e
@@ -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 {
|
||||
fn estimate_message_delivery_and_dispatch_fee(
|
||||
_lane_id: bp_message_lane::LaneId,
|
||||
|
||||
@@ -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 {
|
||||
fn filter_transaction_proof(proof: exchange::EthereumTransactionInclusionProof) -> bool {
|
||||
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-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-std = { git = "https://github.com/paritytech/substrate.git", branch = "master" , default-features = false }
|
||||
|
||||
@@ -40,6 +41,7 @@ std = [
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"serde",
|
||||
"sp-finality-grandpa/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
]
|
||||
|
||||
@@ -33,11 +33,16 @@
|
||||
#![allow(clippy::large_enum_variant)]
|
||||
|
||||
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 frame_support::{dispatch::DispatchError, ensure};
|
||||
use frame_system::ensure_signed;
|
||||
use sp_runtime::traits::Header as HeaderT;
|
||||
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)]
|
||||
@@ -46,15 +51,21 @@ mod mock;
|
||||
// Re-export in crate namespace for `construct_runtime!`
|
||||
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]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::*;
|
||||
|
||||
/// Header of the bridged chain.
|
||||
pub(crate) type BridgedHeader<T> = HeaderOf<<T as Config>::BridgedChain>;
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config {
|
||||
/// The chain we are bridging to here.
|
||||
@@ -113,6 +124,7 @@ pub mod pallet {
|
||||
justification: Vec<u8>,
|
||||
ancestry_proof: T::AncestryProof,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
ensure_operational::<T>()?;
|
||||
let _ = ensure_signed(origin)?;
|
||||
|
||||
ensure!(
|
||||
@@ -120,14 +132,24 @@ pub mod pallet {
|
||||
<Error<T>>::TooManyRequests
|
||||
);
|
||||
|
||||
let (hash, number) = (finality_target.hash(), finality_target.number());
|
||||
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 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| {
|
||||
frame_support::debug::error!("Received invalid justification for {:?}: {:?}", finality_target, e);
|
||||
<Error<T>>::InvalidJustification
|
||||
@@ -142,10 +164,84 @@ pub mod pallet {
|
||||
<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);
|
||||
|
||||
<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())
|
||||
}
|
||||
@@ -162,6 +258,68 @@ pub mod pallet {
|
||||
#[pallet::getter(fn request_count)]
|
||||
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]
|
||||
pub enum Error<T> {
|
||||
/// The given justification is invalid for the given header.
|
||||
@@ -175,32 +333,199 @@ pub mod pallet {
|
||||
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<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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{run_test, test_header, Origin, TestRuntime};
|
||||
use bp_test_utils::{authority_list, make_justification_for_header};
|
||||
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::{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() {
|
||||
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 init_data = pallet_substrate_bridge::InitializationData {
|
||||
let init_data = InitializationData {
|
||||
header: genesis,
|
||||
authority_list: authority_list(),
|
||||
set_id: 1,
|
||||
scheduled_change: None,
|
||||
is_halted: false,
|
||||
};
|
||||
|
||||
assert_ok!(pallet_substrate_bridge::Module::<TestRuntime>::initialize(
|
||||
Origin::root(),
|
||||
init_data
|
||||
));
|
||||
Module::<TestRuntime>::initialize(origin, init_data.clone()).map(|_| init_data)
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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]
|
||||
fn succesfully_imports_header_with_valid_finality_and_ancestry_proofs() {
|
||||
run_test(|| {
|
||||
@@ -231,12 +696,8 @@ mod tests {
|
||||
assert_ok!(submit_finality_proof(1, 2));
|
||||
|
||||
let header = test_header(2);
|
||||
assert_eq!(
|
||||
pallet_substrate_bridge::Module::<TestRuntime>::best_headers(),
|
||||
vec![(*header.number(), header.hash())]
|
||||
);
|
||||
|
||||
assert_eq!(pallet_substrate_bridge::Module::<TestRuntime>::best_finalized(), header);
|
||||
assert_eq!(<BestFinalized<TestRuntime>>::get(), header.hash());
|
||||
assert!(<ImportedHeaders<TestRuntime>>::contains_key(header.hash()));
|
||||
})
|
||||
}
|
||||
|
||||
@@ -309,18 +770,14 @@ mod tests {
|
||||
let genesis = test_header(0);
|
||||
|
||||
let invalid_authority_list = vec![(alice(), u64::MAX), (bob(), u64::MAX)];
|
||||
let init_data = pallet_substrate_bridge::InitializationData {
|
||||
let init_data = InitializationData {
|
||||
header: genesis,
|
||||
authority_list: invalid_authority_list,
|
||||
set_id: 1,
|
||||
scheduled_change: None,
|
||||
is_halted: false,
|
||||
};
|
||||
|
||||
assert_ok!(pallet_substrate_bridge::Module::<TestRuntime>::initialize(
|
||||
Origin::root(),
|
||||
init_data
|
||||
));
|
||||
assert_ok!(Module::<TestRuntime>::initialize(Origin::root(), init_data));
|
||||
|
||||
let header = test_header(1);
|
||||
let justification = [1u8; 32].encode();
|
||||
@@ -334,7 +791,81 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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(|| {
|
||||
initialize_substrate_bridge();
|
||||
assert_ok!(submit_finality_proof(1, 2));
|
||||
@@ -344,7 +875,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_requests_do_not_count_towards_request_count() {
|
||||
fn rate_limiter_invalid_requests_do_not_count_towards_request_count() {
|
||||
run_test(|| {
|
||||
let submit_invalid_request = || {
|
||||
let child = test_header(1);
|
||||
@@ -376,7 +907,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_request_after_new_block_has_started() {
|
||||
fn rate_limiter_allows_request_after_new_block_has_started() {
|
||||
run_test(|| {
|
||||
initialize_substrate_bridge();
|
||||
assert_ok!(submit_finality_proof(1, 2));
|
||||
@@ -388,7 +919,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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(|| {
|
||||
initialize_substrate_bridge();
|
||||
assert_ok!(submit_finality_proof(1, 2));
|
||||
@@ -401,7 +932,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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(|| {
|
||||
initialize_substrate_bridge();
|
||||
assert_ok!(submit_finality_proof(1, 2));
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
// From construct_runtime macro
|
||||
#![allow(clippy::from_over_into)]
|
||||
|
||||
use crate::pallet::{BridgedHeader, Config};
|
||||
use bp_runtime::{BlockNumberOf, Chain};
|
||||
use bp_runtime::Chain;
|
||||
use frame_support::{construct_runtime, parameter_types, weights::Weight};
|
||||
use sp_runtime::{
|
||||
testing::{Header, H256},
|
||||
@@ -27,8 +26,9 @@ use sp_runtime::{
|
||||
};
|
||||
|
||||
pub type AccountId = u64;
|
||||
pub type TestHeader = BridgedHeader<TestRuntime>;
|
||||
pub type TestNumber = BlockNumberOf<<TestRuntime as Config>::BridgedChain>;
|
||||
pub type TestHeader = crate::BridgedHeader<TestRuntime>;
|
||||
pub type TestNumber = crate::BridgedBlockNumber<TestRuntime>;
|
||||
pub type TestHash = crate::BridgedBlockHash<TestRuntime>;
|
||||
|
||||
type Block = frame_system::mocking::MockBlock<TestRuntime>;
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<TestRuntime>;
|
||||
|
||||
@@ -238,6 +238,11 @@ pub const IS_KNOWN_MILLAU_BLOCK_METHOD: &str = "MillauHeaderApi_is_known_block";
|
||||
/// Name of the `MillauHeaderApi::incomplete_headers` runtime method.
|
||||
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.
|
||||
pub const TO_MILLAU_ESTIMATE_MESSAGE_FEE_METHOD: &str =
|
||||
"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! {
|
||||
/// 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.
|
||||
pub trait MillauHeaderApi {
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// This API is implemented by runtimes that are sending messages to Millau chain, not the
|
||||
|
||||
@@ -199,6 +199,11 @@ pub const IS_KNOWN_RIALTO_BLOCK_METHOD: &str = "RialtoHeaderApi_is_known_block";
|
||||
/// Name of the `RialtoHeaderApi::incomplete_headers` runtime method.
|
||||
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.
|
||||
pub const TO_RIALTO_ESTIMATE_MESSAGE_FEE_METHOD: &str =
|
||||
"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! {
|
||||
/// 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.
|
||||
pub trait RialtoHeaderApi {
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// This API is implemented by runtimes that are sending messages to Rialto chain, not the
|
||||
|
||||
Reference in New Issue
Block a user