Newer
Older
// Copyright (C) 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 <http://www.gnu.org/licenses/>.
//! Substrate GRANDPA Pallet
//! This pallet is an on-chain GRANDPA light client for Substrate based chains.
//! This pallet achieves this by trustlessly verifying GRANDPA finality proofs on-chain. Once
//! verified, finalized headers are stored in the pallet, thereby creating a sparse header chain.
//! This sparse header chain can be used as a source of truth for other higher-level applications.
//! The pallet is responsible for tracking GRANDPA validator set hand-offs. We only import headers
//! with justifications signed by the current validator set we know of. The header is inspected for
//! a `ScheduledChanges` digest item, which is then used to update to next validator set.
//!
//! Since this pallet only tracks finalized headers it does not deal with forks. Forks can only
//! occur if the GRANDPA validator set on the bridged chain is either colluding or there is a severe
//! bug causing resulting in an equivocation. Such events are outside the scope of this pallet.
//! Shall the fork occur on the bridged chain governance intervention will be required to
//! re-initialize the bridge and track the right fork.
#![warn(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
pub use storage_types::StoredAuthoritySet;
Svyatoslav Nikolsky
committed
use bp_header_chain::{
justification::GrandpaJustification, AuthoritySet, ChainWithGrandpa, GrandpaConsensusLogReader,
HeaderChain, InitializationData, StoredHeaderData, StoredHeaderDataBuilder,
StoredHeaderGrandpaInfo,
use bp_runtime::{BlockNumberOf, HashOf, HasherOf, HeaderId, HeaderOf, OwnedBridgeModule};
use frame_support::{dispatch::PostDispatchInfo, ensure, DefaultNoBound};
use sp_consensus_grandpa::SetId;
use sp_runtime::{
traits::{Header as HeaderT, Zero},
SaturatedConversion,
};
use sp_std::{boxed::Box, convert::TryInto, prelude::*};
Svyatoslav Nikolsky
committed
mod storage_types;
Svyatoslav Nikolsky
committed
/// Module, containing weights for this pallet.
pub mod weights;
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
// Re-export in crate namespace for `construct_runtime!`
pub use call_ext::*;
pub use weights::WeightInfo;
/// The target that will be used when publishing logs related to this pallet.
Svyatoslav Nikolsky
committed
pub const LOG_TARGET: &str = "runtime::bridge-grandpa";
/// Bridged chain from the pallet configuration.
pub type BridgedChain<T, I> = <T as Config<I>>::BridgedChain;
/// Block number of the bridged chain.
pub type BridgedBlockNumber<T, I> = BlockNumberOf<<T as Config<I>>::BridgedChain>;
/// Block hash of the bridged chain.
pub type BridgedBlockHash<T, I> = HashOf<<T as Config<I>>::BridgedChain>;
/// Block id of the bridged chain.
pub type BridgedBlockId<T, I> = HeaderId<BridgedBlockHash<T, I>, BridgedBlockNumber<T, I>>;
/// Hasher of the bridged chain.
pub type BridgedBlockHasher<T, I> = HasherOf<<T as Config<I>>::BridgedChain>;
/// Header of the bridged chain.
pub type BridgedHeader<T, I> = HeaderOf<<T as Config<I>>::BridgedChain>;
/// Header data of the bridged chain that is stored at this chain by this pallet.
pub type BridgedStoredHeaderData<T, I> =
StoredHeaderData<BridgedBlockNumber<T, I>, BridgedBlockHash<T, I>>;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use bp_runtime::BasicOperatingMode;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[pallet::config]
pub trait Config<I: 'static = ()>: frame_system::Config {
Svyatoslav Nikolsky
committed
/// The overarching event type.
type RuntimeEvent: From<Event<Self, I>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// The chain we are bridging to here.
type BridgedChain: ChainWithGrandpa;
/// Maximal number of "free" header transactions per block.
Svyatoslav Nikolsky
committed
/// To be able to track the bridged chain, the pallet requires all headers that are
/// changing GRANDPA authorities set at the bridged chain (we call them mandatory).
/// So it is a common good deed to submit mandatory headers to the pallet.
///
/// The pallet may be configured (see `[Self::FreeHeadersInterval]`) to import some
/// non-mandatory headers for free as well. It also may be treated as a common good
/// deed, because it may help to reduce bridge fees - this cost may be deducted from
/// bridge fees, paid by message senders.
///
/// However, if the bridged chain gets compromised, its validators may generate as many
/// "free" headers as they want. And they may fill the whole block (at this chain) for
/// free. This constants limits number of calls that we may refund in a single block.
/// All calls above this limit are accepted, but are not refunded.
type MaxFreeHeadersPerBlock: Get<u32>;
/// The distance between bridged chain headers, that may be submitted for free. The
/// first free header is header number zero, the next one is header number
/// `FreeHeadersInterval::get()`. In other words, header with number that
/// is divisible by `FreeHeadersInterval` may be submitted for free.
#[pallet::constant]
type FreeHeadersInterval: Get<Option<u32>>;
/// Maximal number of finalized headers to keep in the storage.
///
/// The setting is there to prevent growing the on-chain state indefinitely. Note
/// the setting does not relate to block numbers - we will simply keep as much items
/// in the storage, so it doesn't guarantee any fixed timeframe for finality headers.
///
/// Incautious change of this constant may lead to orphan entries in the runtime storage.
#[pallet::constant]
type HeadersToKeep: Get<u32>;
/// Weights gathered through benchmarking.
type WeightInfo: WeightInfo;
#[pallet::pallet]
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
#[pallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
Svyatoslav Nikolsky
committed
fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
FreeHeadersRemaining::<T, I>::put(T::MaxFreeHeadersPerBlock::get());
Svyatoslav Nikolsky
committed
Weight::zero()
}
Svyatoslav Nikolsky
committed
fn on_finalize(_n: BlockNumberFor<T>) {
FreeHeadersRemaining::<T, I>::kill();
impl<T: Config<I>, I: 'static> OwnedBridgeModule<T> for Pallet<T, I> {
const LOG_TARGET: &'static str = LOG_TARGET;
type OwnerStorage = PalletOwner<T, I>;
type OperatingMode = BasicOperatingMode;
type OperatingModeStorage = PalletOperatingMode<T, I>;
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// This call is deprecated and will be removed around May 2024. Use the
/// `submit_finality_proof_ex` instead. Semantically, this call is an equivalent of the
/// `submit_finality_proof_ex` call without current authority set id check.
#[pallet::call_index(0)]
#[pallet::weight(<T::WeightInfo as WeightInfo>::submit_finality_proof(
justification.commit.precommits.len().saturated_into(),
justification.votes_ancestries.len().saturated_into(),
))]
#[allow(deprecated)]
#[deprecated(
note = "`submit_finality_proof` will be removed in May 2024. Use `submit_finality_proof_ex` instead."
)]
pub fn submit_finality_proof(
origin: OriginFor<T>,
finality_target: Box<BridgedHeader<T, I>>,
justification: GrandpaJustification<BridgedHeader<T, I>>,
) -> DispatchResultWithPostInfo {
Self::submit_finality_proof_ex(
origin,
finality_target,
justification,
// the `submit_finality_proof_ex` also reads this value, but it is done from the
// cache, so we don't treat it as an additional db access
<CurrentAuthoritySet<T, I>>::get().set_id,
Svyatoslav Nikolsky
committed
// cannot enforce free execution using this call
false,
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
)
}
/// 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 arbitrary 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::call_index(1)]
#[pallet::weight((T::DbWeight::get().reads_writes(2, 5), DispatchClass::Operational))]
pub fn initialize(
origin: OriginFor<T>,
init_data: super::InitializationData<BridgedHeader<T, I>>,
) -> DispatchResultWithPostInfo {
Self::ensure_owner_or_root(origin)?;
let init_allowed = !<BestFinalized<T, I>>::exists();
ensure!(init_allowed, <Error<T, I>>::AlreadyInitialized);
initialize_bridge::<T, I>(init_data.clone())?;
log::info!(
target: LOG_TARGET,
"Pallet has been initialized with the following parameters: {:?}",
init_data
);
Ok(().into())
}
/// Change `PalletOwner`.
///
/// May only be called either by root, or by `PalletOwner`.
#[pallet::call_index(2)]
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
pub fn set_owner(origin: OriginFor<T>, new_owner: Option<T::AccountId>) -> DispatchResult {
<Self as OwnedBridgeModule<_>>::set_owner(origin, new_owner)
}
/// Halt or resume all pallet operations.
///
/// May only be called either by root, or by `PalletOwner`.
#[pallet::call_index(3)]
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
pub fn set_operating_mode(
origin: OriginFor<T>,
operating_mode: BasicOperatingMode,
) -> DispatchResult {
<Self as OwnedBridgeModule<_>>::set_operating_mode(origin, operating_mode)
}
/// Verify a target header is finalized according to the given finality proof. The proof
/// is assumed to be signed by GRANDPA authorities set with `current_set_id` id.
/// 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.
Svyatoslav Nikolsky
committed
///
/// The call fails if:
///
/// - the pallet is halted;
///
/// - the pallet knows better header than the `finality_target`;
///
/// - the id of best GRANDPA authority set, known to the pallet is not equal to the
/// `current_set_id`;
///
Svyatoslav Nikolsky
committed
/// - verification is not optimized or invalid;
///
/// - header contains forced authorities set change or change with non-zero delay.
Svyatoslav Nikolsky
committed
///
/// The `is_free_execution_expected` parameter is not really used inside the call. It is
/// used by the transaction extension, which should be registered at the runtime level. If
/// this parameter is `true`, the transaction will be treated as invalid, if the call won't
/// be executed for free. If transaction extension is not used by the runtime, this
/// parameter is not used at all.
#[pallet::call_index(4)]
#[pallet::weight(<T::WeightInfo as WeightInfo>::submit_finality_proof(
justification.commit.precommits.len().saturated_into(),
justification.votes_ancestries.len().saturated_into(),
pub fn submit_finality_proof_ex(
Svyatoslav Nikolsky
committed
origin: OriginFor<T>,
finality_target: Box<BridgedHeader<T, I>>,
justification: GrandpaJustification<BridgedHeader<T, I>>,
current_set_id: sp_consensus_grandpa::SetId,
Svyatoslav Nikolsky
committed
_is_free_execution_expected: bool,
) -> DispatchResultWithPostInfo {
Self::ensure_not_halted().map_err(Error::<T, I>::BridgeModule)?;
Svyatoslav Nikolsky
committed
ensure_signed(origin)?;
Svyatoslav Nikolsky
committed
let (hash, number) = (finality_target.hash(), *finality_target.number());
log::trace!(
target: LOG_TARGET,
"Going to try and finalize header {:?}",
finality_target
);
// it checks whether the `number` is better than the current best block number
// and whether the `current_set_id` matches the best known set id
SubmitFinalityProofHelper::<T, I>::check_obsolete(number, Some(current_set_id))?;
let authority_set = <CurrentAuthoritySet<T, I>>::get();
let unused_proof_size = authority_set.unused_proof_size();
let set_id = authority_set.set_id;
let authority_set: AuthoritySet = authority_set.into();
verify_justification::<T, I>(&justification, hash, number, authority_set)?;
let maybe_new_authority_set =
try_enact_authority_change::<T, I>(&finality_target, set_id)?;
let may_refund_call_fee = may_refund_call_fee::<T, I>(
maybe_new_authority_set.is_some(),
&finality_target,
&justification,
current_set_id,
);
Svyatoslav Nikolsky
committed
if may_refund_call_fee {
FreeHeadersRemaining::<T, I>::mutate(|count| *count = count.saturating_sub(1));
Svyatoslav Nikolsky
committed
}
insert_header::<T, I>(*finality_target, hash);
log::info!(
target: LOG_TARGET,
"Successfully imported finalized header with hash {:?}!",
hash
);
// mandatory header is a header that changes authorities set. The pallet can't go
// further without importing this header. So every bridge MUST import mandatory headers.
// We don't want to charge extra costs for mandatory operations. So relayer is not
// paying fee for mandatory headers import transactions.
//
// If size/weight of the call is exceeds our estimated limits, the relayer still needs
// to pay for the transaction.
let pays_fee = if may_refund_call_fee { Pays::No } else { Pays::Yes };
// the proof size component of the call weight assumes that there are
// `MaxBridgedAuthorities` in the `CurrentAuthoritySet` (we use `MaxEncodedLen`
// estimation). But if their number is lower, then we may "refund" some `proof_size`,
// making proof smaller and leaving block space to other useful transactions
let pre_dispatch_weight = T::WeightInfo::submit_finality_proof(
justification.commit.precommits.len().saturated_into(),
justification.votes_ancestries.len().saturated_into(),
);
let actual_weight = pre_dispatch_weight
.set_proof_size(pre_dispatch_weight.proof_size().saturating_sub(unused_proof_size));
Self::deposit_event(Event::UpdatedBestFinalizedHeader {
number,
hash,
grandpa_info: StoredHeaderGrandpaInfo {
finality_proof: justification,
new_verification_context: maybe_new_authority_set,
Svyatoslav Nikolsky
committed
Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee })
/// Number of free header submissions that we may yet accept in the current block.
/// If the `FreeHeadersRemaining` hits zero, all following mandatory headers in the
Svyatoslav Nikolsky
committed
/// current block are accepted with fee (`Pays::Yes` is returned).
/// The `FreeHeadersRemaining` is an ephemeral value that is set to
/// `MaxFreeHeadersPerBlock` at each block initialization and is killed on block
Svyatoslav Nikolsky
committed
/// finalization. So it never ends up in the storage trie.
Svyatoslav Nikolsky
committed
#[pallet::whitelist_storage]
#[pallet::getter(fn free_mandatory_headers_remaining)]
pub(super) type FreeHeadersRemaining<T: Config<I>, I: 'static = ()> =
Svyatoslav Nikolsky
committed
StorageValue<_, u32, ValueQuery>;
/// Hash of the header used to bootstrap the pallet.
#[pallet::storage]
pub(super) type InitialHash<T: Config<I>, I: 'static = ()> =
StorageValue<_, BridgedBlockHash<T, I>, ValueQuery>;
/// Hash of the best finalized header.
#[pallet::storage]
#[pallet::getter(fn best_finalized)]
Svyatoslav Nikolsky
committed
pub type BestFinalized<T: Config<I>, I: 'static = ()> =
StorageValue<_, BridgedBlockId<T, I>, OptionQuery>;
/// A ring buffer of imported hashes. Ordered by the insertion time.
#[pallet::storage]
pub(super) type ImportedHashes<T: Config<I>, I: 'static = ()> = StorageMap<
Hasher = Identity,
Key = u32,
Value = BridgedBlockHash<T, I>,
QueryKind = OptionQuery,
OnEmpty = GetDefault,
MaxValues = MaybeHeadersToKeep<T, I>,
>;
/// Current ring buffer position.
#[pallet::storage]
pub(super) type ImportedHashesPointer<T: Config<I>, I: 'static = ()> =
StorageValue<_, u32, ValueQuery>;
/// Relevant fields of imported headers.
#[pallet::storage]
pub type ImportedHeaders<T: Config<I>, I: 'static = ()> = StorageMap<
Hasher = Identity,
Key = BridgedBlockHash<T, I>,
Value = BridgedStoredHeaderData<T, I>,
QueryKind = OptionQuery,
OnEmpty = GetDefault,
MaxValues = MaybeHeadersToKeep<T, I>,
>;
/// The current GRANDPA Authority set.
#[pallet::storage]
pub type CurrentAuthoritySet<T: Config<I>, I: 'static = ()> =
Svyatoslav Nikolsky
committed
StorageValue<_, StoredAuthoritySet<T, I>, 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]
Svyatoslav Nikolsky
committed
pub type PalletOwner<T: Config<I>, I: 'static = ()> =
/// The current operating mode of the pallet.
///
/// Depending on the mode either all, or no transactions will be allowed.
#[pallet::storage]
pub type PalletOperatingMode<T: Config<I>, I: 'static = ()> =
StorageValue<_, BasicOperatingMode, ValueQuery>;
#[pallet::genesis_config]
#[derive(DefaultNoBound)]
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
/// Optional module owner account.
pub owner: Option<T::AccountId>,
/// Optional module initialization data.
pub init_data: Option<super::InitializationData<BridgedHeader<T, I>>>,
}
#[pallet::genesis_build]
impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
fn build(&self) {
if let Some(ref owner) = self.owner {
}
if let Some(init_data) = self.init_data.clone() {
Svyatoslav Nikolsky
committed
initialize_bridge::<T, I>(init_data).expect("genesis config is correct; qed");
} else {
// Since the bridge hasn't been initialized we shouldn't allow anyone to perform
// transactions.
<PalletOperatingMode<T, I>>::put(BasicOperatingMode::Halted);
}
}
}
Svyatoslav Nikolsky
committed
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
/// Best finalized chain header has been updated to the header with given number and hash.
UpdatedBestFinalizedHeader {
/// Number of the new best finalized header.
Svyatoslav Nikolsky
committed
number: BridgedBlockNumber<T, I>,
/// Hash of the new best finalized header.
Svyatoslav Nikolsky
committed
hash: BridgedBlockHash<T, I>,
/// The Grandpa info associated to the new best finalized header.
grandpa_info: StoredHeaderGrandpaInfo<BridgedHeader<T, I>>,
Svyatoslav Nikolsky
committed
},
}
pub enum Error<T, I = ()> {
/// The given justification is invalid for the given header.
InvalidJustification,
/// The authority set from the underlying header chain is invalid.
InvalidAuthoritySet,
/// 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 is not yet initialized.
NotInitialized,
/// The pallet has already been initialized.
AlreadyInitialized,
Svyatoslav Nikolsky
committed
/// Too many authorities in the set.
TooManyAuthoritiesInSet,
/// Error generated by the `OwnedBridgeModule` trait.
BridgeModule(bp_runtime::OwnedBridgeModuleError),
/// The `current_set_id` argument of the `submit_finality_proof_ex` doesn't match
/// the id of the current set, known to the pallet.
InvalidAuthoritySetId,
Svyatoslav Nikolsky
committed
/// The submitter wanted free execution, but we can't fit more free transactions
/// to the block.
CannotAcceptMoreFreeHeaders,
Svyatoslav Nikolsky
committed
/// Return true if we may refund transaction cost to the submitter. In other words,
/// this transaction is considered as common good deed w.r.t to pallet configuration.
fn may_refund_call_fee<T: Config<I>, I: 'static>(
is_mandatory_header: bool,
finality_target: &BridgedHeader<T, I>,
justification: &GrandpaJustification<BridgedHeader<T, I>>,
current_set_id: SetId,
) -> bool {
// if we have refunded too much at this block => not refunding
if FreeHeadersRemaining::<T, I>::get() == 0 {
return false;
}
// if size/weight of call is larger than expected => not refunding
let call_info = submit_finality_proof_info_from_args::<T, I>(
&finality_target,
&justification,
Some(current_set_id),
Svyatoslav Nikolsky
committed
// this function is called from the transaction body and we do not want
// to do MAY-be-free-executed checks here - they had to be done in the
// transaction extension before
false,
);
if !call_info.fits_limits() {
return false;
}
// if that's a mandatory header => refund
if is_mandatory_header {
return true;
}
// if configuration allows free non-mandatory headers and the header
// matches criteria => refund
if let Some(free_headers_interval) = T::FreeHeadersInterval::get() {
if *finality_target.number() % free_headers_interval.into() == Zero::zero() {
return true;
}
}
false
}
/// Check the given header for a GRANDPA scheduled authority set change. If a change
/// is found it will be enacted immediately.
/// This function does not support forced changes, or scheduled changes with delays
/// since these types of changes are indicative of abnormal behavior from GRANDPA.
///
/// Returned value will indicate if a change was enacted or not.
pub(crate) fn try_enact_authority_change<T: Config<I>, I: 'static>(
header: &BridgedHeader<T, I>,
current_set_id: sp_consensus_grandpa::SetId,
) -> Result<Option<AuthoritySet>, DispatchError> {
// We don't support forced changes - at that point governance intervention is required.
ensure!(
GrandpaConsensusLogReader::<BridgedBlockNumber<T, I>>::find_forced_change(
header.digest()
)
.is_none(),
<Error<T, I>>::UnsupportedScheduledChange
if let Some(change) =
GrandpaConsensusLogReader::<BridgedBlockNumber<T, I>>::find_scheduled_change(
header.digest(),
) {
// GRANDPA only includes a `delay` for forced changes, so this isn't valid.
ensure!(change.delay == Zero::zero(), <Error<T, I>>::UnsupportedScheduledChange);
// TODO [#788]: Stop manually increasing the `set_id` here.
Svyatoslav Nikolsky
committed
let next_authorities = StoredAuthoritySet::<T, I> {
authorities: change
.next_authorities
.try_into()
.map_err(|_| Error::<T, I>::TooManyAuthoritiesInSet)?,
};
// Since our header schedules a change and we know the delay is 0, it must also enact
// the change.
<CurrentAuthoritySet<T, I>>::put(&next_authorities);
"Transitioned from authority set {} to {}! New authorities are: {:?}",
current_set_id,
current_set_id + 1,
next_authorities,
);
return Ok(Some(next_authorities.into()))
/// Verify a GRANDPA justification (finality proof) for a given header.
///
/// Will use the GRANDPA current authorities known to the pallet.
/// If successful it returns the decoded GRANDPA justification so we can refund any weight which
/// was overcharged in the initial call.
pub(crate) fn verify_justification<T: Config<I>, I: 'static>(
justification: &GrandpaJustification<BridgedHeader<T, I>>,
hash: BridgedBlockHash<T, I>,
number: BridgedBlockNumber<T, I>,
authority_set: bp_header_chain::AuthoritySet,
) -> Result<(), sp_runtime::DispatchError> {
use bp_header_chain::justification::verify_justification;
Ok(verify_justification::<BridgedHeader<T, I>>(
(hash, number),
&authority_set.try_into().map_err(|_| <Error<T, I>>::InvalidAuthoritySet)?,
"Received invalid justification for {:?}: {:?}",
hash,
e,
);
<Error<T, I>>::InvalidJustification
})?)
/// Import a previously verified header to the storage.
///
/// Note this function solely takes care of updating the storage and pruning old entries,
/// but does not verify the validity of such import.
pub(crate) fn insert_header<T: Config<I>, I: 'static>(
header: BridgedHeader<T, I>,
let index = <ImportedHashesPointer<T, I>>::get();
let pruning = <ImportedHashes<T, I>>::try_get(index);
<BestFinalized<T, I>>::put(HeaderId(*header.number(), hash));
<ImportedHeaders<T, I>>::insert(hash, header.build());
<ImportedHashes<T, I>>::insert(index, hash);
// Update ring buffer pointer and remove old header.
<ImportedHashesPointer<T, I>>::put((index + 1) % T::HeadersToKeep::get());
if let Ok(hash) = pruning {
log::debug!(target: LOG_TARGET, "Pruning old header: {:?}.", hash);
<ImportedHeaders<T, I>>::remove(hash);
}
}
/// Since this writes to storage with no real checks this should only be used in functions that
/// were called by a trusted origin.
pub(crate) fn initialize_bridge<T: Config<I>, I: 'static>(
init_params: super::InitializationData<BridgedHeader<T, I>>,
Svyatoslav Nikolsky
committed
) -> Result<(), Error<T, I>> {
let super::InitializationData { header, authority_list, set_id, operating_mode } =
init_params;
let authority_set_length = authority_list.len();
Svyatoslav Nikolsky
committed
let authority_set = StoredAuthoritySet::<T, I>::try_new(authority_list, set_id)
.map_err(|e| {
log::error!(
target: LOG_TARGET,
"Failed to initialize bridge. Number of authorities in the set {} is larger than the configured value {}",
authority_set_length,
T::BridgedChain::MAX_AUTHORITIES_COUNT,
);
})?;
let initial_hash = header.hash();
<InitialHash<T, I>>::put(initial_hash);
<ImportedHashesPointer<T, I>>::put(0);
insert_header::<T, I>(*header, initial_hash);
<CurrentAuthoritySet<T, I>>::put(authority_set);
<PalletOperatingMode<T, I>>::put(operating_mode);
Svyatoslav Nikolsky
committed
Ok(())
/// Adapter for using `Config::HeadersToKeep` as `MaxValues` bound in our storage maps.
pub struct MaybeHeadersToKeep<T, I>(PhantomData<(T, I)>);
// this implementation is required to use the struct as `MaxValues`
impl<T: Config<I>, I: 'static> Get<Option<u32>> for MaybeHeadersToKeep<T, I> {
fn get() -> Option<u32> {
Some(T::HeadersToKeep::get())
}
}
/// Initialize pallet so that it is ready for inserting new header.
///
/// The function makes sure that the new insertion will cause the pruning of some old header.
///
/// Returns parent header for the new header.
#[cfg(feature = "runtime-benchmarks")]
pub(crate) fn bootstrap_bridge<T: Config<I>, I: 'static>(
init_params: super::InitializationData<BridgedHeader<T, I>>,
) -> BridgedHeader<T, I> {
let start_header = init_params.header.clone();
Svyatoslav Nikolsky
committed
initialize_bridge::<T, I>(init_params).expect("benchmarks are correct");
// the most obvious way to cause pruning during next insertion would be to insert
// `HeadersToKeep` headers. But it'll make our benchmarks slow. So we will just play with
// our pruning ring-buffer.
assert_eq!(ImportedHashesPointer::<T, I>::get(), 1);
ImportedHashesPointer::<T, I>::put(0);
*start_header
impl<T: Config<I>, I: 'static> Pallet<T, I>
where
<T as frame_system::Config>::RuntimeEvent: TryInto<Event<T, I>>,
{
/// Get the GRANDPA justifications accepted in the current block.
pub fn synced_headers_grandpa_info() -> Vec<StoredHeaderGrandpaInfo<BridgedHeader<T, I>>> {
frame_system::Pallet::<T>::read_events_no_consensus()
.filter_map(|event| {
if let Event::<T, I>::UpdatedBestFinalizedHeader { grandpa_info, .. } =
event.event.try_into().ok()?
{
return Some(grandpa_info)
}
None
})
.collect()
}
}
/// Bridge GRANDPA pallet as header chain.
pub type GrandpaChainHeaders<T, I> = Pallet<T, I>;
impl<T: Config<I>, I: 'static> HeaderChain<BridgedChain<T, I>> for GrandpaChainHeaders<T, I> {
fn finalized_header_state_root(
header_hash: HashOf<BridgedChain<T, I>>,
) -> Option<HashOf<BridgedChain<T, I>>> {
ImportedHeaders::<T, I>::get(header_hash).map(|h| h.state_root)
/// (Re)initialize bridge with given header for using it in `pallet-bridge-messages` benchmarks.
#[cfg(feature = "runtime-benchmarks")]
pub fn initialize_for_benchmarks<T: Config<I>, I: 'static>(header: BridgedHeader<T, I>) {
initialize_bridge::<T, I>(InitializationData {
header: Box::new(header),
authority_list: sp_std::vec::Vec::new(), /* we don't verify any proofs in external
* benchmarks */
operating_mode: bp_runtime::BasicOperatingMode::Normal,
Svyatoslav Nikolsky
committed
})
.expect("only used from benchmarks; benchmarks are correct; qed");
#[cfg(test)]
mod tests {
use super::*;
Svyatoslav Nikolsky
committed
use crate::mock::{
run_test, test_header, FreeHeadersInterval, RuntimeEvent as TestEvent, RuntimeOrigin,
System, TestBridgedChain, TestHeader, TestNumber, TestRuntime, MAX_BRIDGED_AUTHORITIES,
Svyatoslav Nikolsky
committed
};
use bp_header_chain::BridgeGrandpaCall;
use bp_runtime::BasicOperatingMode;
authority_list, generate_owned_bridge_module_tests, make_default_justification,
make_justification_for_header, JustificationGeneratorParams, ALICE, BOB,
TEST_GRANDPA_SET_ID,
Svyatoslav Nikolsky
committed
use frame_support::{
Svyatoslav Nikolsky
committed
assert_err, assert_noop, assert_ok,
dispatch::{Pays, PostDispatchInfo},
storage::generator::StorageValue,
Svyatoslav Nikolsky
committed
};
Svyatoslav Nikolsky
committed
use frame_system::{EventRecord, Phase};
use sp_consensus_grandpa::{ConsensusLog, GRANDPA_ENGINE_ID};
use sp_runtime::{Digest, DigestItem, DispatchError};
fn initialize_substrate_bridge() {
Svyatoslav Nikolsky
committed
System::set_block_number(1);
System::reset_events();
assert_ok!(init_with_origin(RuntimeOrigin::root()));
}
fn init_with_origin(
origin: RuntimeOrigin,
) -> Result<
InitializationData<TestHeader>,
sp_runtime::DispatchErrorWithPostInfo<PostDispatchInfo>,
> {
let init_data = InitializationData {
header: Box::new(genesis),
set_id: TEST_GRANDPA_SET_ID,
operating_mode: BasicOperatingMode::Normal,
Pallet::<TestRuntime>::initialize(origin, init_data.clone()).map(|_| init_data)
fn submit_finality_proof(header: u8) -> frame_support::dispatch::DispatchResultWithPostInfo {
let header = test_header(header.into());
let justification = make_default_justification(&header);
Pallet::<TestRuntime>::submit_finality_proof_ex(
RuntimeOrigin::signed(1),
TEST_GRANDPA_SET_ID,
Svyatoslav Nikolsky
committed
false,
Svyatoslav Nikolsky
committed
fn submit_finality_proof_with_set_id(
header: u8,
set_id: u64,
) -> frame_support::dispatch::DispatchResultWithPostInfo {
let header = test_header(header.into());
let justification = make_justification_for_header(JustificationGeneratorParams {
header: header.clone(),
set_id,
..Default::default()
});
Pallet::<TestRuntime>::submit_finality_proof_ex(
Svyatoslav Nikolsky
committed
RuntimeOrigin::signed(1),
Box::new(header),
justification,
set_id,
Svyatoslav Nikolsky
committed
false,
Svyatoslav Nikolsky
committed
)
}
fn submit_mandatory_finality_proof(
number: u8,
set_id: u64,
) -> frame_support::dispatch::DispatchResultWithPostInfo {
let mut header = test_header(number.into());
// to ease tests that are using `submit_mandatory_finality_proof`, we'll be using the
// same set for all sessions
let consensus_log =
ConsensusLog::<TestNumber>::ScheduledChange(sp_consensus_grandpa::ScheduledChange {
next_authorities: authority_list(),
delay: 0,
});
header.digest =
Digest { logs: vec![DigestItem::Consensus(GRANDPA_ENGINE_ID, consensus_log.encode())] };
let justification = make_justification_for_header(JustificationGeneratorParams {
header: header.clone(),
set_id,
..Default::default()
});
Pallet::<TestRuntime>::submit_finality_proof_ex(
Svyatoslav Nikolsky
committed
RuntimeOrigin::signed(1),
Box::new(header),
justification,
set_id,
Svyatoslav Nikolsky
committed
false,
Svyatoslav Nikolsky
committed
)
}
fn next_block() {
use frame_support::traits::OnInitialize;
let current_number = frame_system::Pallet::<TestRuntime>::block_number();
frame_system::Pallet::<TestRuntime>::set_block_number(current_number + 1);
let _ = Pallet::<TestRuntime>::on_initialize(current_number);
ConsensusLog::<TestNumber>::ScheduledChange(sp_consensus_grandpa::ScheduledChange {
next_authorities: vec![(ALICE.into(), 1), (BOB.into(), 1)],
delay,
});
Digest { logs: vec![DigestItem::Consensus(GRANDPA_ENGINE_ID, consensus_log.encode())] }
fn forced_change_log(delay: u64) -> Digest {
let consensus_log = ConsensusLog::<TestNumber>::ForcedChange(
delay,
sp_consensus_grandpa::ScheduledChange {
next_authorities: vec![(ALICE.into(), 1), (BOB.into(), 1)],
delay,
},
);
Digest { logs: vec![DigestItem::Consensus(GRANDPA_ENGINE_ID, consensus_log.encode())] }
Svyatoslav Nikolsky
committed
fn many_authorities_log() -> Digest {
let consensus_log =
ConsensusLog::<TestNumber>::ScheduledChange(sp_consensus_grandpa::ScheduledChange {
Svyatoslav Nikolsky
committed
next_authorities: std::iter::repeat((ALICE.into(), 1))
.take(MAX_BRIDGED_AUTHORITIES as usize + 1)
.collect(),
delay: 0,
});
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(RuntimeOrigin::signed(1)), DispatchError::BadOrigin);
assert_ok!(init_with_origin(RuntimeOrigin::root()));
// Reset storage so we can initialize the pallet again
BestFinalized::<TestRuntime>::kill();
assert_ok!(init_with_origin(RuntimeOrigin::signed(2)));
})
}
#[test]
fn init_storage_entries_are_correctly_initialized() {
run_test(|| {
Svyatoslav Nikolsky
committed
assert_eq!(BestFinalized::<TestRuntime>::get(), None,);
assert_eq!(Pallet::<TestRuntime>::best_finalized(), None);
assert_eq!(PalletOperatingMode::<TestRuntime>::try_get(), Err(()));
let init_data = init_with_origin(RuntimeOrigin::root()).unwrap();
assert!(<ImportedHeaders<TestRuntime>>::contains_key(init_data.header.hash()));
Svyatoslav Nikolsky
committed
assert_eq!(BestFinalized::<TestRuntime>::get().unwrap().1, init_data.header.hash());
assert_eq!(
CurrentAuthoritySet::<TestRuntime>::get().authorities,
init_data.authority_list
);
assert_eq!(
PalletOperatingMode::<TestRuntime>::try_get(),
Ok(BasicOperatingMode::Normal)
);
})
}
#[test]
fn init_can_only_initialize_pallet_once() {
run_test(|| {
initialize_substrate_bridge();
assert_noop!(
init_with_origin(RuntimeOrigin::root()),
<Error<TestRuntime>>::AlreadyInitialized
);
})
}
Svyatoslav Nikolsky
committed
#[test]
fn init_fails_if_there_are_too_many_authorities_in_the_set() {
run_test(|| {
let genesis = test_header(0);
let init_data = InitializationData {
header: Box::new(genesis),
authority_list: std::iter::repeat(authority_list().remove(0))
.take(MAX_BRIDGED_AUTHORITIES as usize + 1)
.collect(),
set_id: 1,
operating_mode: BasicOperatingMode::Normal,
};
assert_noop!(
Pallet::<TestRuntime>::initialize(RuntimeOrigin::root(), init_data),
Svyatoslav Nikolsky
committed
Error::<TestRuntime>::TooManyAuthoritiesInSet,
);
});
}
#[test]
fn pallet_rejects_transactions_if_halted() {
run_test(|| {
initialize_substrate_bridge();
assert_ok!(Pallet::<TestRuntime>::set_operating_mode(
RuntimeOrigin::root(),
BasicOperatingMode::Halted
));
assert_noop!(
submit_finality_proof(1),
Error::<TestRuntime>::BridgeModule(bp_runtime::OwnedBridgeModuleError::Halted)
);
assert_ok!(Pallet::<TestRuntime>::set_operating_mode(
RuntimeOrigin::root(),
BasicOperatingMode::Normal
));
assert_ok!(submit_finality_proof(1));
})
}
#[test]
fn pallet_rejects_header_if_not_initialized_yet() {