// 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 .
//! Teyrchains finality module.
//!
//! This module needs to be deployed with GRANDPA module, which is syncing relay
//! chain blocks. The main entry point of this module is `submit_teyrchain_heads`, which
//! accepts storage proof of some teyrchain `Heads` entries from bridged relay chain.
//! It requires corresponding relay headers to be already synced.
#![warn(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
pub use weights::WeightInfo;
pub use weights_ext::WeightInfoExt;
use bp_header_pez_chain::{HeaderChain, HeaderChainError};
use bp_pezkuwi_core::teyrchains::{ParaHash, ParaHead, ParaHeadsProof, ParaId};
use bp_teyrchains::{
ParaInfo, ParaStoredHeaderData, RelayBlockHash, RelayBlockHasher, RelayBlockNumber,
SubmitTeyrchainHeadsInfo,
};
use pezbp_runtime::{Chain, HashOf, HeaderId, HeaderIdOf, Teyrchain};
use pezframe_support::{dispatch::PostDispatchInfo, DefaultNoBound};
use pezpallet_bridge_grandpa::SubmitFinalityProofHelper;
use pezsp_std::{marker::PhantomData, vec::Vec};
use proofs::{StorageProofAdapter, TeyrchainsStorageProofAdapter};
#[cfg(feature = "runtime-benchmarks")]
use bp_teyrchains::ParaStoredHeaderDataBuilder;
#[cfg(feature = "runtime-benchmarks")]
use codec::Encode;
#[cfg(feature = "runtime-benchmarks")]
use pezbp_runtime::HeaderOf;
// Re-export in crate namespace for `construct_runtime!`.
pub use call_ext::*;
pub use pezpallet::*;
pub mod weights;
pub mod weights_ext;
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
mod call_ext;
#[cfg(test)]
mod mock;
mod proofs;
/// The target that will be used when publishing logs related to this pezpallet.
pub const LOG_TARGET: &str = "runtime::bridge-teyrchains";
/// Artifacts of the teyrchains head update.
struct UpdateTeyrchainHeadArtifacts {
/// New best head of the teyrchain.
pub best_head: ParaInfo,
/// If `true`, some old teyrchain head has been pruned during update.
pub prune_happened: bool,
}
#[pezframe_support::pezpallet]
pub mod pezpallet {
use super::*;
use bp_teyrchains::{
BestParaHeadHash, ImportedParaHeadsKeyProvider, OnNewHead, ParaStoredHeaderDataBuilder,
ParasInfoKeyProvider,
};
use pezbp_runtime::{
BasicOperatingMode, BoundedStorageValue, OwnedBridgeModule, StorageDoubleMapKeyProvider,
StorageMapKeyProvider,
};
use pezframe_support::pezpallet_prelude::*;
use pezframe_system::pezpallet_prelude::*;
/// Stored teyrchain head data of given teyrchains pezpallet.
pub type StoredParaHeadDataOf =
BoundedStorageValue<>::MaxParaHeadDataSize, ParaStoredHeaderData>;
/// Weight info of the given teyrchains pezpallet.
pub type WeightInfoOf = >::WeightInfo;
/// Bridge GRANDPA pezpallet that is used to verify teyrchain proofs.
pub type GrandpaPalletOf =
pezpallet_bridge_grandpa::Pezpallet>::BridgesGrandpaPalletInstance>;
#[pezpallet::event]
#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event, I: 'static = ()> {
/// The caller has provided head of teyrchain that the pezpallet is not configured to
/// track.
UntrackedTeyrchainRejected {
/// Identifier of the teyrchain that is not tracked by the pezpallet.
teyrchain: ParaId,
},
/// The caller has declared that he has provided given teyrchain head, but it is missing
/// from the storage proof.
MissingTeyrchainHead {
/// Identifier of the teyrchain with missing head.
teyrchain: ParaId,
},
/// The caller has provided teyrchain head hash that is not matching the hash read from the
/// storage proof.
IncorrectTeyrchainHeadHash {
/// Identifier of the teyrchain with incorrect head hast.
teyrchain: ParaId,
/// Specified teyrchain head hash.
teyrchain_head_hash: ParaHash,
/// Actual teyrchain head hash.
actual_teyrchain_head_hash: ParaHash,
},
/// The caller has provided obsolete teyrchain head, which is already known to the
/// pezpallet.
RejectedObsoleteTeyrchainHead {
/// Identifier of the teyrchain with obsolete head.
teyrchain: ParaId,
/// Obsolete teyrchain head hash.
teyrchain_head_hash: ParaHash,
},
/// The caller has provided teyrchain head that exceeds the maximal configured head size.
RejectedLargeTeyrchainHead {
/// Identifier of the teyrchain with rejected head.
teyrchain: ParaId,
/// Teyrchain head hash.
teyrchain_head_hash: ParaHash,
/// Teyrchain head size.
teyrchain_head_size: u32,
},
/// Teyrchain head has been updated.
UpdatedTeyrchainHead {
/// Identifier of the teyrchain that has been updated.
teyrchain: ParaId,
/// Teyrchain head hash.
teyrchain_head_hash: ParaHash,
},
}
#[pezpallet::error]
pub enum Error {
/// Relay chain block hash is unknown to us.
UnknownRelayChainBlock,
/// The number of stored relay block is different from what the relayer has provided.
InvalidRelayChainBlockNumber,
/// Teyrchain heads storage proof is invalid.
HeaderChainStorageProof(HeaderChainError),
/// Error generated by the `OwnedBridgeModule` trait.
BridgeModule(pezbp_runtime::OwnedBridgeModuleError),
}
/// Convenience trait for defining `BridgedChain` bounds.
pub trait BoundedBridgeGrandpaConfig:
pezpallet_bridge_grandpa::Config
{
/// Type of the bridged relay chain.
type BridgedRelayChain: Chain<
BlockNumber = RelayBlockNumber,
Hash = RelayBlockHash,
Hasher = RelayBlockHasher,
>;
}
impl BoundedBridgeGrandpaConfig for T
where
T: pezpallet_bridge_grandpa::Config,
T::BridgedChain:
Chain,
{
type BridgedRelayChain = T::BridgedChain;
}
#[pezpallet::config]
#[pezpallet::disable_pezframe_system_supertrait_check]
pub trait Config:
BoundedBridgeGrandpaConfig
{
/// The overarching event type.
#[allow(deprecated)]
type RuntimeEvent: From>
+ IsType<::RuntimeEvent>;
/// Benchmarks results from runtime we're plugged into.
type WeightInfo: WeightInfoExt;
/// Instance of bridges GRANDPA pezpallet (within this runtime) that this pezpallet is
/// linked to.
///
/// The GRANDPA pezpallet instance must be configured to import headers of relay chain that
/// we're interested in.
///
/// The associated GRANDPA pezpallet is also used to configure free teyrchain heads
/// submissions. The teyrchain head submission will be free if:
///
/// 1) the submission contains exactly one teyrchain head update that succeeds;
///
/// 2) the difference between relay chain block numbers, used to prove new teyrchain head
/// and previous best teyrchain head is larger than the `FreeHeadersInterval`, configured
/// at the associated GRANDPA pezpallet;
///
/// 3) there are slots for free submissions, remaining at the block. This is also configured
/// at the associated GRANDPA pezpallet using `MaxFreeHeadersPerBlock` parameter.
///
/// First teyrchain head submission is also free for the submitted, if free submissions
/// are yet accepted to this block.
type BridgesGrandpaPalletInstance: 'static;
/// Name of the original `paras` pezpallet in the `construct_runtime!()` call at the bridged
/// chain.
///
/// Please keep in mind that this should be the name of the `runtime_teyrchains::paras`
/// pezpallet from pezkuwi repository, not the `pezpallet-bridge-teyrchains`.
#[pezpallet::constant]
type ParasPalletName: Get<&'static str>;
/// Teyrchain head data builder.
///
/// We never store teyrchain heads here, since they may be too big (e.g. because of large
/// digest items). Instead we're using the same approach as `pezpallet-bridge-grandpa`
/// pezpallet - we are only storing `bp_messages::StoredHeaderData` (number and state root),
/// which is enough for our applications. However, we work with different teyrchains here
/// and they can use different primitives (for block numbers and hash). So we can't store
/// it directly. Instead, we're storing `bp_messages::StoredHeaderData` in SCALE-encoded
/// form, wrapping it into `bp_teyrchains::ParaStoredHeaderData`.
///
/// This builder helps to convert from `HeadData` to `bp_teyrchains::ParaStoredHeaderData`.
type ParaStoredHeaderDataBuilder: ParaStoredHeaderDataBuilder;
/// Maximal number of single teyrchain heads to keep in the storage.
///
/// The setting is there to prevent growing the on-chain state indefinitely. Note
/// the setting does not relate to teyrchain block numbers - we will simply keep as much
/// items in the storage, so it doesn't guarantee any fixed timeframe for heads.
///
/// Incautious change of this constant may lead to orphan entries in the runtime storage.
#[pezpallet::constant]
type HeadsToKeep: Get;
/// Maximal size (in bytes) of the SCALE-encoded teyrchain head data
/// (`bp_teyrchains::ParaStoredHeaderData`).
///
/// Keep in mind that the size of any tracked teyrchain header data must not exceed this
/// value. So if you're going to track multiple teyrchains, one of which is using large
/// hashes, you shall choose this maximal value.
///
/// There's no mandatory headers in this pezpallet, so it can't stall if there's some header
/// that exceeds this bound.
#[pezpallet::constant]
type MaxParaHeadDataSize: Get;
/// Runtime hook for when a teyrchain head is updated.
type OnNewHead: OnNewHead;
}
/// Optional pezpallet owner.
///
/// Pezpallet owner has a right to halt all pezpallet operations and then resume them. If it is
/// `None`, then there are no direct ways to halt/resume pezpallet operations, but other
/// runtime methods may still be used to do that (i.e. democracy::referendum to update halt
/// flag directly or call the `set_operating_mode`).
#[pezpallet::storage]
pub type PalletOwner, I: 'static = ()> =
StorageValue<_, T::AccountId, OptionQuery>;
/// The current operating mode of the pezpallet.
///
/// Depending on the mode either all, or no transactions will be allowed.
#[pezpallet::storage]
pub type PalletOperatingMode, I: 'static = ()> =
StorageValue<_, BasicOperatingMode, ValueQuery>;
/// Teyrchains info.
///
/// Contains the following info:
/// - best teyrchain head hash
/// - the head of the `ImportedParaHashes` ring buffer
#[pezpallet::storage]
pub type ParasInfo, I: 'static = ()> = StorageMap<
Hasher = ::Hasher,
Key = ::Key,
Value = ::Value,
QueryKind = OptionQuery,
OnEmpty = GetDefault,
MaxValues = MaybeMaxTeyrchains,
>;
/// State roots of teyrchain heads which have been imported into the pezpallet.
#[pezpallet::storage]
pub type ImportedParaHeads, I: 'static = ()> = StorageDoubleMap<
Hasher1 = ::Hasher1,
Key1 = ::Key1,
Hasher2 = ::Hasher2,
Key2 = ::Key2,
Value = StoredParaHeadDataOf,
QueryKind = OptionQuery,
OnEmpty = GetDefault,
MaxValues = MaybeMaxTotalTeyrchainHashes,
>;
/// A ring buffer of imported teyrchain head hashes. Ordered by the insertion time.
#[pezpallet::storage]
pub(super) type ImportedParaHashes, I: 'static = ()> = StorageDoubleMap<
Hasher1 = Blake2_128Concat,
Key1 = ParaId,
Hasher2 = Twox64Concat,
Key2 = u32,
Value = ParaHash,
QueryKind = OptionQuery,
OnEmpty = GetDefault,
MaxValues = MaybeMaxTotalTeyrchainHashes,
>;
#[pezpallet::pezpallet]
pub struct Pezpallet(PhantomData<(T, I)>);
impl, I: 'static> OwnedBridgeModule for Pezpallet {
const LOG_TARGET: &'static str = LOG_TARGET;
type OwnerStorage = PalletOwner;
type OperatingMode = BasicOperatingMode;
type OperatingModeStorage = PalletOperatingMode;
}
#[pezpallet::call]
impl, I: 'static> Pezpallet {
/// Submit proof of one or several teyrchain heads.
///
/// The proof is supposed to be proof of some `Heads` entries from the
/// `pezkuwi-runtime-teyrchains::paras` pezpallet instance, deployed at the bridged chain.
/// The proof is supposed to be crafted at the `relay_header_hash` that must already be
/// imported by corresponding GRANDPA pezpallet at this chain.
///
/// The call fails if:
///
/// - the pezpallet is halted;
///
/// - the relay chain block `at_relay_block` is not imported by the associated bridge
/// GRANDPA pezpallet.
///
/// The call may succeed, but some heads may not be updated e.g. because pezpallet knows
/// better head or it isn't tracked by the pezpallet.
#[pezpallet::call_index(0)]
#[pezpallet::weight(WeightInfoOf::::submit_teyrchain_heads_weight(
T::DbWeight::get(),
teyrchain_heads_proof,
teyrchains.len() as _,
))]
pub fn submit_teyrchain_heads(
origin: OriginFor,
at_relay_block: (RelayBlockNumber, RelayBlockHash),
teyrchains: Vec<(ParaId, ParaHash)>,
teyrchain_heads_proof: ParaHeadsProof,
) -> DispatchResultWithPostInfo {
Self::submit_teyrchain_heads_ex(
origin,
at_relay_block,
teyrchains,
teyrchain_heads_proof,
false,
)
}
/// Change `PalletOwner`.
///
/// May only be called either by root, or by `PalletOwner`.
#[pezpallet::call_index(1)]
#[pezpallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
pub fn set_owner(origin: OriginFor, new_owner: Option) -> DispatchResult {
>::set_owner(origin, new_owner)
}
/// Halt or resume all pezpallet operations.
///
/// May only be called either by root, or by `PalletOwner`.
#[pezpallet::call_index(2)]
#[pezpallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
pub fn set_operating_mode(
origin: OriginFor,
operating_mode: BasicOperatingMode,
) -> DispatchResult {
>::set_operating_mode(origin, operating_mode)
}
/// Submit proof of one or several teyrchain heads.
///
/// The proof is supposed to be proof of some `Heads` entries from the
/// `pezkuwi-runtime-teyrchains::paras` pezpallet instance, deployed at the bridged chain.
/// The proof is supposed to be crafted at the `relay_header_hash` that must already be
/// imported by corresponding GRANDPA pezpallet at this chain.
///
/// The call fails if:
///
/// - the pezpallet is halted;
///
/// - the relay chain block `at_relay_block` is not imported by the associated bridge
/// GRANDPA pezpallet.
///
/// The call may succeed, but some heads may not be updated e.g. because pezpallet knows
/// better head or it isn't tracked by the pezpallet.
///
/// 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.
#[pezpallet::call_index(3)]
#[pezpallet::weight(WeightInfoOf::::submit_teyrchain_heads_weight(
T::DbWeight::get(),
teyrchain_heads_proof,
teyrchains.len() as _,
))]
pub fn submit_teyrchain_heads_ex(
origin: OriginFor,
at_relay_block: (RelayBlockNumber, RelayBlockHash),
teyrchains: Vec<(ParaId, ParaHash)>,
teyrchain_heads_proof: ParaHeadsProof,
_is_free_execution_expected: bool,
) -> DispatchResultWithPostInfo {
Self::ensure_not_halted().map_err(Error::::BridgeModule)?;
ensure_signed(origin)?;
let total_teyrchains = teyrchains.len();
let free_headers_interval =
T::FreeHeadersInterval::get().unwrap_or(RelayBlockNumber::MAX);
// the pezpallet allows two kind of free submissions
// 1) if distance between all teyrchain heads is gte than the [`T::FreeHeadersInterval`]
// 2) if all heads are the first heads of their teyrchains
let mut free_teyrchain_heads = 0;
// we'll need relay chain header to verify that teyrchains heads are always increasing.
let (relay_block_number, relay_block_hash) = at_relay_block;
let relay_block = pezpallet_bridge_grandpa::ImportedHeaders::<
T,
T::BridgesGrandpaPalletInstance,
>::get(relay_block_hash)
.ok_or(Error::::UnknownRelayChainBlock)?;
ensure!(
relay_block.number == relay_block_number,
Error::::InvalidRelayChainBlockNumber,
);
// now parse storage proof and read teyrchain heads
let mut actual_weight = WeightInfoOf::::submit_teyrchain_heads_weight(
T::DbWeight::get(),
&teyrchain_heads_proof,
teyrchains.len() as _,
);
let mut storage: TeyrchainsStorageProofAdapter =
TeyrchainsStorageProofAdapter::try_new_with_verified_storage_proof(
relay_block_hash,
teyrchain_heads_proof.storage_proof,
)
.map_err(Error::::HeaderChainStorageProof)?;
for (teyrchain, teyrchain_head_hash) in teyrchains {
let teyrchain_head = match storage.read_teyrchain_head(teyrchain) {
Ok(Some(teyrchain_head)) => teyrchain_head,
Ok(None) => {
tracing::trace!(
target: LOG_TARGET,
?teyrchain,
"The head of teyrchain is None. {}",
if ParasInfo::::contains_key(teyrchain) {
"Looks like it is not yet registered at the source relay chain"
} else {
"Looks like it has been deregistered from the source relay chain"
},
);
Self::deposit_event(Event::MissingTeyrchainHead { teyrchain });
continue;
},
Err(e) => {
tracing::trace!(
target: LOG_TARGET,
error=?e,
?teyrchain,
"The read of head of teyrchain has failed"
);
Self::deposit_event(Event::MissingTeyrchainHead { teyrchain });
continue;
},
};
// if relayer has specified invalid teyrchain head hash, ignore the head
// (this isn't strictly necessary, but better safe than sorry)
let actual_teyrchain_head_hash = teyrchain_head.hash();
if teyrchain_head_hash != actual_teyrchain_head_hash {
tracing::trace!(
target: LOG_TARGET,
?teyrchain,
?teyrchain_head_hash,
?actual_teyrchain_head_hash,
"The submitter has specified invalid teyrchain head hash"
);
Self::deposit_event(Event::IncorrectTeyrchainHeadHash {
teyrchain,
teyrchain_head_hash,
actual_teyrchain_head_hash,
});
continue;
}
// convert from teyrchain head into stored teyrchain head data
let teyrchain_head_size = teyrchain_head.0.len();
let teyrchain_head_data =
match T::ParaStoredHeaderDataBuilder::try_build(teyrchain, &teyrchain_head) {
Some(teyrchain_head_data) => teyrchain_head_data,
None => {
tracing::trace!(
target: LOG_TARGET,
?teyrchain,
"The head of teyrchain has been provided, but it is not tracked by the pezpallet"
);
Self::deposit_event(Event::UntrackedTeyrchainRejected { teyrchain });
continue;
},
};
let update_result: Result<_, ()> =
ParasInfo::::try_mutate(teyrchain, |stored_best_head| {
let is_free = teyrchain_head_size
< T::ParaStoredHeaderDataBuilder::max_free_head_size() as usize
&& match stored_best_head {
Some(ref best_head)
if at_relay_block.0.saturating_sub(
best_head.best_head_hash.at_relay_block_number,
) >= free_headers_interval =>
{
true
},
Some(_) => false,
None => true,
};
let artifacts = Pezpallet::::update_teyrchain_head(
teyrchain,
stored_best_head.take(),
HeaderId(relay_block_number, relay_block_hash),
teyrchain_head_data,
teyrchain_head_hash,
teyrchain_head,
)?;
if is_free {
free_teyrchain_heads = free_teyrchain_heads + 1;
}
*stored_best_head = Some(artifacts.best_head);
Ok(artifacts.prune_happened)
});
// we're refunding weight if update has not happened and if pruning has not happened
let is_update_happened = update_result.is_ok();
if !is_update_happened {
actual_weight = actual_weight.saturating_sub(
WeightInfoOf::::teyrchain_head_storage_write_weight(
T::DbWeight::get(),
),
);
}
let is_prune_happened = matches!(update_result, Ok(true));
if !is_prune_happened {
actual_weight = actual_weight.saturating_sub(
WeightInfoOf::::teyrchain_head_pruning_weight(T::DbWeight::get()),
);
}
}
// even though we may have accepted some teyrchain heads, we can't allow relayers to
// submit proof with unused trie nodes
// => treat this as an error
//
// (we can throw error here, because now all our calls are transactional)
storage.ensure_no_unused_keys().map_err(|e| {
Error::::HeaderChainStorageProof(HeaderChainError::StorageProof(e))
})?;
// check if we allow this submission for free
let is_free = total_teyrchains == 1
&& free_teyrchain_heads == total_teyrchains
&& SubmitFinalityProofHelper::::has_free_header_slots();
let pays_fee = if is_free {
tracing::trace!(target: LOG_TARGET, "Teyrchain heads update transaction is free");
pezpallet_bridge_grandpa::on_free_header_imported::<
T,
T::BridgesGrandpaPalletInstance,
>();
Pays::No
} else {
tracing::trace!(target: LOG_TARGET, "Teyrchain heads update transaction is paid");
Pays::Yes
};
Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee })
}
}
impl, I: 'static> Pezpallet {
/// Get stored teyrchain info.
pub fn best_teyrchain_info(teyrchain: ParaId) -> Option {
ParasInfo::::get(teyrchain)
}
/// Get best finalized head data of the given teyrchain.
pub fn best_teyrchain_head(teyrchain: ParaId) -> Option {
let best_para_head_hash = ParasInfo::::get(teyrchain)?.best_head_hash.head_hash;
ImportedParaHeads::::get(teyrchain, best_para_head_hash).map(|h| h.into_inner())
}
/// Get best finalized head hash of the given teyrchain.
pub fn best_teyrchain_head_hash(teyrchain: ParaId) -> Option {
Some(ParasInfo::::get(teyrchain)?.best_head_hash.head_hash)
}
/// Get best finalized head id of the given teyrchain.
pub fn best_teyrchain_head_id + Teyrchain>(
) -> Result