fix: Complete snowbridge pezpallet rebrand and critical bug fixes

- snowbridge-pezpallet-* → pezsnowbridge-pezpallet-* (201 refs)
- pallet/ directories → pezpallet/ (4 locations)
- Fixed pezpallet.rs self-include recursion bug
- Fixed sc-chain-spec hardcoded crate name in derive macro
- Reverted .pezpallet_by_name() to .pallet_by_name() (subxt API)
- Added BizinikiwiConfig type alias for zombienet tests
- Deleted obsolete session state files

Verified: pezsnowbridge-pezpallet-*, pezpallet-staking,
pezpallet-staking-async, pezframe-benchmarking-cli all pass cargo check
This commit is contained in:
2025-12-16 09:57:23 +03:00
parent eea003e14d
commit 3139ffa25e
3022 changed files with 42157 additions and 23579 deletions
+75
View File
@@ -0,0 +1,75 @@
[package]
name = "pezpallet-bridge-beefy"
version = "0.1.0"
description = "Module implementing BEEFY on-chain light client used for bridging consensus of bizinikiwi-based chains."
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
repository.workspace = true
publish = false
documentation = "https://docs.rs/pezpallet-bridge-beefy"
homepage = { workspace = true }
[lints]
workspace = true
[dependencies]
codec = { workspace = true }
scale-info = { features = ["derive"], workspace = true }
serde = { optional = true, workspace = true }
tracing = { workspace = true }
# Bridge Dependencies
bp-beefy = { workspace = true }
pezbp-runtime = { workspace = true }
# Bizinikiwi Dependencies
pezframe-support = { workspace = true }
pezframe-system = { workspace = true }
pezsp-core = { workspace = true }
pezsp-runtime = { workspace = true }
pezsp-std = { workspace = true }
[dev-dependencies]
bp-test-utils = { workspace = true, default-features = true }
mmr-lib = { workspace = true, default-features = true }
pezpallet-beefy-mmr = { workspace = true, default-features = true }
pezpallet-mmr = { workspace = true, default-features = true }
rand = { workspace = true, default-features = true }
pezsp-consensus-beefy = { workspace = true, default-features = true }
pezsp-io = { workspace = true, default-features = true }
[features]
default = ["std"]
std = [
"bp-beefy/std",
"pezbp-runtime/std",
"codec/std",
"pezframe-support/std",
"pezframe-system/std",
"scale-info/std",
"serde/std",
"pezsp-core/std",
"pezsp-runtime/std",
"pezsp-std/std",
"tracing/std",
]
try-runtime = [
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezpallet-beefy-mmr/try-runtime",
"pezpallet-mmr/try-runtime",
"pezsp-runtime/try-runtime",
]
runtime-benchmarks = [
"bp-beefy/runtime-benchmarks",
"pezbp-runtime/runtime-benchmarks",
"bp-test-utils/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezpallet-beefy-mmr/runtime-benchmarks",
"pezpallet-mmr/runtime-benchmarks",
"pezsp-consensus-beefy/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
+657
View File
@@ -0,0 +1,657 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! BEEFY bridge pezpallet.
//!
//! This pezpallet is an on-chain BEEFY light client for Bizinikiwi-based chains that are using the
//! following pallets bundle: `pezpallet-mmr`, `pezpallet-beefy` and `pezpallet-beefy-mmr`.
//!
//! The pezpallet is able to verify MMR leaf proofs and BEEFY commitments, so it has access
//! to the following data of the bridged chain:
//!
//! - header hashes
//! - changes of BEEFY authorities
//! - extra data of MMR leafs
//!
//! Given the header hash, other pallets are able to verify header-based proofs
//! (e.g. storage proofs, transaction inclusion proofs, etc.).
#![warn(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
use bp_beefy::{ChainWithBeefy, InitializationData};
use pezsp_std::{boxed::Box, prelude::*};
// Re-export in crate namespace for `construct_runtime!`
pub use pezpallet::*;
mod utils;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod mock_chain;
/// The target that will be used when publishing logs related to this pezpallet.
pub const LOG_TARGET: &str = "runtime::bridge-beefy";
/// Configured bridged chain.
pub type BridgedChain<T, I> = <T as Config<I>>::BridgedChain;
/// Block number, used by configured bridged chain.
pub type BridgedBlockNumber<T, I> = pezbp_runtime::BlockNumberOf<BridgedChain<T, I>>;
/// Block hash, used by configured bridged chain.
pub type BridgedBlockHash<T, I> = pezbp_runtime::HashOf<BridgedChain<T, I>>;
/// Pezpallet initialization data.
pub type InitializationDataOf<T, I> =
InitializationData<BridgedBlockNumber<T, I>, bp_beefy::MmrHashOf<BridgedChain<T, I>>>;
/// BEEFY commitment hasher, used by configured bridged chain.
pub type BridgedBeefyCommitmentHasher<T, I> = bp_beefy::BeefyCommitmentHasher<BridgedChain<T, I>>;
/// BEEFY validator id, used by configured bridged chain.
pub type BridgedBeefyAuthorityId<T, I> = bp_beefy::BeefyAuthorityIdOf<BridgedChain<T, I>>;
/// BEEFY validator set, used by configured bridged chain.
pub type BridgedBeefyAuthoritySet<T, I> = bp_beefy::BeefyAuthoritySetOf<BridgedChain<T, I>>;
/// BEEFY authority set, used by configured bridged chain.
pub type BridgedBeefyAuthoritySetInfo<T, I> = bp_beefy::BeefyAuthoritySetInfoOf<BridgedChain<T, I>>;
/// BEEFY signed commitment, used by configured bridged chain.
pub type BridgedBeefySignedCommitment<T, I> = bp_beefy::BeefySignedCommitmentOf<BridgedChain<T, I>>;
/// MMR hashing algorithm, used by configured bridged chain.
pub type BridgedMmrHashing<T, I> = bp_beefy::MmrHashingOf<BridgedChain<T, I>>;
/// MMR hashing output type of `BridgedMmrHashing<T, I>`.
pub type BridgedMmrHash<T, I> = bp_beefy::MmrHashOf<BridgedChain<T, I>>;
/// The type of the MMR leaf extra data used by the configured bridged chain.
pub type BridgedBeefyMmrLeafExtra<T, I> = bp_beefy::BeefyMmrLeafExtraOf<BridgedChain<T, I>>;
/// BEEFY MMR proof type used by the pezpallet
pub type BridgedMmrProof<T, I> = bp_beefy::MmrProofOf<BridgedChain<T, I>>;
/// MMR leaf type, used by configured bridged chain.
pub type BridgedBeefyMmrLeaf<T, I> = bp_beefy::BeefyMmrLeafOf<BridgedChain<T, I>>;
/// Imported commitment data, stored by the pezpallet.
pub type ImportedCommitment<T, I> = bp_beefy::ImportedCommitment<
BridgedBlockNumber<T, I>,
BridgedBlockHash<T, I>,
BridgedMmrHash<T, I>,
>;
/// Some high level info about the imported commitments.
#[derive(codec::Encode, codec::Decode, scale_info::TypeInfo)]
pub struct ImportedCommitmentsInfoData<BlockNumber> {
/// Best known block number, provided in a BEEFY commitment. However this is not
/// the best proven block. The best proven block is this block's parent.
best_block_number: BlockNumber,
/// The head of the `ImportedBlockNumbers` ring buffer.
next_block_number_index: u32,
}
#[pezframe_support::pezpallet(dev_mode)]
pub mod pezpallet {
use super::*;
use pezbp_runtime::{BasicOperatingMode, OwnedBridgeModule};
use pezframe_support::pezpallet_prelude::*;
use pezframe_system::pezpallet_prelude::*;
#[pezpallet::config]
pub trait Config<I: 'static = ()>: pezframe_system::Config {
/// The upper bound on the number of requests allowed by the pezpallet.
///
/// A request refers to an action which writes a header to storage.
///
/// Once this bound is reached the pezpallet will reject all commitments
/// until the request count has decreased.
#[pezpallet::constant]
type MaxRequests: Get<u32>;
/// Maximal number of imported commitments 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 imported commitments.
#[pezpallet::constant]
type CommitmentsToKeep: Get<u32>;
/// The chain we are bridging to here.
type BridgedChain: ChainWithBeefy;
}
#[pezpallet::pezpallet]
#[pezpallet::without_storage_info]
pub struct Pezpallet<T, I = ()>(PhantomData<(T, I)>);
#[pezpallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pezpallet<T, I> {
fn on_initialize(_n: BlockNumberFor<T>) -> pezframe_support::weights::Weight {
<RequestCount<T, I>>::mutate(|count| *count = count.saturating_sub(1));
Weight::from_parts(0, 0)
.saturating_add(T::DbWeight::get().reads(1))
.saturating_add(T::DbWeight::get().writes(1))
}
}
impl<T: Config<I>, I: 'static> OwnedBridgeModule<T> for Pezpallet<T, I> {
const LOG_TARGET: &'static str = LOG_TARGET;
type OwnerStorage = PalletOwner<T, I>;
type OperatingMode = BasicOperatingMode;
type OperatingModeStorage = PalletOperatingMode<T, I>;
}
#[pezpallet::call]
impl<T: Config<I>, I: 'static> Pezpallet<T, I>
where
BridgedMmrHashing<T, I>: 'static + Send + Sync,
{
/// Initialize pezpallet with BEEFY authority set and best known finalized block number.
#[pezpallet::call_index(0)]
#[pezpallet::weight((T::DbWeight::get().reads_writes(2, 3), DispatchClass::Operational))]
pub fn initialize(
origin: OriginFor<T>,
init_data: InitializationDataOf<T, I>,
) -> DispatchResult {
Self::ensure_owner_or_root(origin)?;
let is_initialized = <ImportedCommitmentsInfo<T, I>>::exists();
ensure!(!is_initialized, <Error<T, I>>::AlreadyInitialized);
tracing::info!(target: LOG_TARGET, ?init_data, "Initializing bridge BEEFY pezpallet");
Ok(initialize::<T, I>(init_data)?)
}
/// 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<T>, new_owner: Option<T::AccountId>) -> DispatchResult {
<Self as OwnedBridgeModule<_>>::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<T>,
operating_mode: BasicOperatingMode,
) -> DispatchResult {
<Self as OwnedBridgeModule<_>>::set_operating_mode(origin, operating_mode)
}
/// Submit a commitment generated by BEEFY authority set.
///
/// It will use the underlying storage pezpallet to fetch information about the current
/// authority set and best finalized block number in order to verify that the commitment
/// is valid.
///
/// If successful in verification, it will update the underlying storage with the data
/// provided in the newly submitted commitment.
#[pezpallet::call_index(3)]
#[pezpallet::weight(0)]
pub fn submit_commitment(
origin: OriginFor<T>,
commitment: BridgedBeefySignedCommitment<T, I>,
validator_set: BridgedBeefyAuthoritySet<T, I>,
mmr_leaf: Box<BridgedBeefyMmrLeaf<T, I>>,
mmr_proof: BridgedMmrProof<T, I>,
) -> DispatchResult
where
BridgedBeefySignedCommitment<T, I>: Clone,
{
Self::ensure_not_halted().map_err(Error::<T, I>::BridgeModule)?;
ensure_signed(origin)?;
ensure!(Self::request_count() < T::MaxRequests::get(), <Error<T, I>>::TooManyRequests);
// Ensure that the commitment is for a better block.
let commitments_info =
ImportedCommitmentsInfo::<T, I>::get().ok_or(Error::<T, I>::NotInitialized)?;
ensure!(
commitment.commitment.block_number > commitments_info.best_block_number,
Error::<T, I>::OldCommitment
);
// Verify commitment and mmr leaf.
let current_authority_set_info = CurrentAuthoritySetInfo::<T, I>::get();
let mmr_root = utils::verify_commitment::<T, I>(
&commitment,
&current_authority_set_info,
&validator_set,
)?;
utils::verify_beefy_mmr_leaf::<T, I>(&mmr_leaf, mmr_proof, mmr_root)?;
// Update request count.
RequestCount::<T, I>::mutate(|count| *count += 1);
// Update authority set if needed.
if mmr_leaf.beefy_next_authority_set.id > current_authority_set_info.id {
CurrentAuthoritySetInfo::<T, I>::put(mmr_leaf.beefy_next_authority_set);
}
// Import commitment.
let block_number_index = commitments_info.next_block_number_index;
let to_prune = ImportedBlockNumbers::<T, I>::try_get(block_number_index);
ImportedCommitments::<T, I>::insert(
commitment.commitment.block_number,
ImportedCommitment::<T, I> {
parent_number_and_hash: mmr_leaf.parent_number_and_hash,
mmr_root,
},
);
ImportedBlockNumbers::<T, I>::insert(
block_number_index,
commitment.commitment.block_number,
);
ImportedCommitmentsInfo::<T, I>::put(ImportedCommitmentsInfoData {
best_block_number: commitment.commitment.block_number,
next_block_number_index: (block_number_index + 1) % T::CommitmentsToKeep::get(),
});
if let Ok(old_block_number) = to_prune {
tracing::debug!(
target: LOG_TARGET,
?old_block_number,
"Pruning commitment for old block."
);
ImportedCommitments::<T, I>::remove(old_block_number);
}
tracing::info!(
target: LOG_TARGET,
block=?commitment.commitment.block_number,
"Successfully imported commitment for block",
);
Ok(())
}
}
/// The current number of requests which have written to storage.
///
/// If the `RequestCount` hits `MaxRequests`, no more calls will be allowed to the pezpallet until
/// the request capacity is increased.
///
/// The `RequestCount` is decreased by one at the beginning of every block. This is to ensure
/// that the pezpallet can always make progress.
#[pezpallet::storage]
pub type RequestCount<T: Config<I>, I: 'static = ()> = StorageValue<_, u32, ValueQuery>;
/// High level info about the imported commitments.
///
/// Contains the following info:
/// - best known block number of the bridged chain, finalized by BEEFY
/// - the head of the `ImportedBlockNumbers` ring buffer
#[pezpallet::storage]
pub type ImportedCommitmentsInfo<T: Config<I>, I: 'static = ()> =
StorageValue<_, ImportedCommitmentsInfoData<BridgedBlockNumber<T, I>>>;
/// A ring buffer containing the block numbers of the commitments that we have imported,
/// ordered by the insertion time.
#[pezpallet::storage]
pub(super) type ImportedBlockNumbers<T: Config<I>, I: 'static = ()> =
StorageMap<_, Identity, u32, BridgedBlockNumber<T, I>>;
/// All the commitments that we have imported and haven't been pruned yet.
#[pezpallet::storage]
pub type ImportedCommitments<T: Config<I>, I: 'static = ()> =
StorageMap<_, Blake2_128Concat, BridgedBlockNumber<T, I>, ImportedCommitment<T, I>>;
/// The current BEEFY authority set at the bridged chain.
#[pezpallet::storage]
pub type CurrentAuthoritySetInfo<T: Config<I>, I: 'static = ()> =
StorageValue<_, BridgedBeefyAuthoritySetInfo<T, I>, ValueQuery>;
/// Optional pezpallet owner.
///
/// Pezpallet owner has the right to halt all pezpallet operations and then resume it. 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 calling `set_operating_mode`).
#[pezpallet::storage]
pub type PalletOwner<T: Config<I>, 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<T: Config<I>, I: 'static = ()> =
StorageValue<_, BasicOperatingMode, ValueQuery>;
#[pezpallet::genesis_config]
#[derive(pezframe_support::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<InitializationDataOf<T, I>>,
}
#[pezpallet::genesis_build]
impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
fn build(&self) {
if let Some(ref owner) = self.owner {
<PalletOwner<T, I>>::put(owner);
}
if let Some(init_data) = self.init_data.clone() {
initialize::<T, I>(init_data)
.expect("invalid initialization data of BEEFY bridge pezpallet");
} else {
// Since the bridge hasn't been initialized we shouldn't allow anyone to perform
// transactions.
<PalletOperatingMode<T, I>>::put(BasicOperatingMode::Halted);
}
}
}
#[pezpallet::error]
pub enum Error<T, I = ()> {
/// The pezpallet has not been initialized yet.
NotInitialized,
/// The pezpallet has already been initialized.
AlreadyInitialized,
/// Invalid initial authority set.
InvalidInitialAuthoritySet,
/// There are too many requests for the current window to handle.
TooManyRequests,
/// The imported commitment is older than the best commitment known to the pezpallet.
OldCommitment,
/// The commitment is signed by unknown validator set.
InvalidCommitmentValidatorSetId,
/// The id of the provided validator set is invalid.
InvalidValidatorSetId,
/// The number of signatures in the commitment is invalid.
InvalidCommitmentSignaturesLen,
/// The number of validator ids provided is invalid.
InvalidValidatorSetLen,
/// There aren't enough correct signatures in the commitment to finalize the block.
NotEnoughCorrectSignatures,
/// MMR root is missing from the commitment.
MmrRootMissingFromCommitment,
/// MMR proof verification has failed.
MmrProofVerificationFailed,
/// The validators are not matching the merkle tree root of the authority set.
InvalidValidatorSetRoot,
/// Error generated by the `OwnedBridgeModule` trait.
BridgeModule(pezbp_runtime::OwnedBridgeModuleError),
}
/// Initialize pezpallet with given parameters.
pub(super) fn initialize<T: Config<I>, I: 'static>(
init_data: InitializationDataOf<T, I>,
) -> Result<(), Error<T, I>> {
if init_data.authority_set.len == 0 {
return Err(Error::<T, I>::InvalidInitialAuthoritySet);
}
CurrentAuthoritySetInfo::<T, I>::put(init_data.authority_set);
<PalletOperatingMode<T, I>>::put(init_data.operating_mode);
ImportedCommitmentsInfo::<T, I>::put(ImportedCommitmentsInfoData {
best_block_number: init_data.best_block_number,
next_block_number_index: 0,
});
Ok(())
}
impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
/// The current number of requests which have written to storage.
pub fn request_count() -> u32 {
RequestCount::<T, I>::get()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pezbp_runtime::{BasicOperatingMode, OwnedBridgeModuleError};
use bp_test_utils::generate_owned_bridge_module_tests;
use pezframe_support::{assert_noop, assert_ok, traits::Get};
use mock::*;
use mock_chain::*;
use pezsp_consensus_beefy::mmr::BeefyAuthoritySet;
use pezsp_runtime::DispatchError;
fn next_block() {
use pezframe_support::traits::OnInitialize;
let current_number = pezframe_system::Pezpallet::<TestRuntime>::block_number();
pezframe_system::Pezpallet::<TestRuntime>::set_block_number(current_number + 1);
let _ = Pezpallet::<TestRuntime>::on_initialize(current_number);
}
fn import_header_chain(headers: Vec<HeaderAndCommitment>) {
for header in headers {
if header.commitment.is_some() {
assert_ok!(import_commitment(header));
}
}
}
#[test]
fn fails_to_initialize_if_already_initialized() {
run_test_with_initialize(32, || {
assert_noop!(
Pezpallet::<TestRuntime>::initialize(
RuntimeOrigin::root(),
InitializationData {
operating_mode: BasicOperatingMode::Normal,
best_block_number: 0,
authority_set: BeefyAuthoritySet {
id: 0,
len: 1,
keyset_commitment: [0u8; 32].into()
}
}
),
Error::<TestRuntime, ()>::AlreadyInitialized,
);
});
}
#[test]
fn fails_to_initialize_if_authority_set_is_empty() {
run_test(|| {
assert_noop!(
Pezpallet::<TestRuntime>::initialize(
RuntimeOrigin::root(),
InitializationData {
operating_mode: BasicOperatingMode::Normal,
best_block_number: 0,
authority_set: BeefyAuthoritySet {
id: 0,
len: 0,
keyset_commitment: [0u8; 32].into()
}
}
),
Error::<TestRuntime, ()>::InvalidInitialAuthoritySet,
);
});
}
#[test]
fn fails_to_import_commitment_if_halted() {
run_test_with_initialize(1, || {
assert_ok!(Pezpallet::<TestRuntime>::set_operating_mode(
RuntimeOrigin::root(),
BasicOperatingMode::Halted
));
assert_noop!(
import_commitment(ChainBuilder::new(1).append_finalized_header().to_header()),
Error::<TestRuntime, ()>::BridgeModule(OwnedBridgeModuleError::Halted),
);
})
}
#[test]
fn fails_to_import_commitment_if_too_many_requests() {
run_test_with_initialize(1, || {
let max_requests = <<TestRuntime as Config>::MaxRequests as Get<u32>>::get() as u64;
let mut chain = ChainBuilder::new(1);
for _ in 0..max_requests + 2 {
chain = chain.append_finalized_header();
}
// import `max_request` headers
for i in 0..max_requests {
assert_ok!(import_commitment(chain.header(i + 1)));
}
// try to import next header: it fails because we are no longer accepting commitments
assert_noop!(
import_commitment(chain.header(max_requests + 1)),
Error::<TestRuntime, ()>::TooManyRequests,
);
// when next block is "started", we allow import of next header
next_block();
assert_ok!(import_commitment(chain.header(max_requests + 1)));
// but we can't import two headers until next block and so on
assert_noop!(
import_commitment(chain.header(max_requests + 2)),
Error::<TestRuntime, ()>::TooManyRequests,
);
})
}
#[test]
fn fails_to_import_commitment_if_not_initialized() {
run_test(|| {
assert_noop!(
import_commitment(ChainBuilder::new(1).append_finalized_header().to_header()),
Error::<TestRuntime, ()>::NotInitialized,
);
})
}
#[test]
fn submit_commitment_works_with_long_chain_with_handoffs() {
run_test_with_initialize(3, || {
let chain = ChainBuilder::new(3)
.append_finalized_header()
.append_default_headers(16) // 2..17
.append_finalized_header() // 18
.append_default_headers(16) // 19..34
.append_handoff_header(9) // 35
.append_default_headers(8) // 36..43
.append_finalized_header() // 44
.append_default_headers(8) // 45..52
.append_handoff_header(17) // 53
.append_default_headers(4) // 54..57
.append_finalized_header() // 58
.append_default_headers(4); // 59..63
import_header_chain(chain.to_chain());
assert_eq!(
ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().best_block_number,
58
);
assert_eq!(CurrentAuthoritySetInfo::<TestRuntime>::get().id, 2);
assert_eq!(CurrentAuthoritySetInfo::<TestRuntime>::get().len, 17);
let imported_commitment = ImportedCommitments::<TestRuntime>::get(58).unwrap();
assert_eq!(
imported_commitment,
bp_beefy::ImportedCommitment {
parent_number_and_hash: (57, chain.header(57).header.hash()),
mmr_root: chain.header(58).mmr_root,
},
);
})
}
#[test]
fn commitment_pruning_works() {
run_test_with_initialize(3, || {
let commitments_to_keep = <TestRuntime as Config<()>>::CommitmentsToKeep::get();
let commitments_to_import: Vec<HeaderAndCommitment> = ChainBuilder::new(3)
.append_finalized_headers(commitments_to_keep as usize + 2)
.to_chain();
// import exactly `CommitmentsToKeep` commitments
for index in 0..commitments_to_keep {
next_block();
import_commitment(commitments_to_import[index as usize].clone())
.expect("must succeed");
assert_eq!(
ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().next_block_number_index,
(index + 1) % commitments_to_keep
);
}
// ensure that all commitments are in the storage
assert_eq!(
ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().best_block_number,
commitments_to_keep as TestBridgedBlockNumber
);
assert_eq!(
ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().next_block_number_index,
0
);
for index in 0..commitments_to_keep {
assert!(ImportedCommitments::<TestRuntime>::get(
index as TestBridgedBlockNumber + 1
)
.is_some());
assert_eq!(
ImportedBlockNumbers::<TestRuntime>::get(index),
Some(Into::into(index + 1)),
);
}
// import next commitment
next_block();
import_commitment(commitments_to_import[commitments_to_keep as usize].clone())
.expect("must succeed");
assert_eq!(
ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().next_block_number_index,
1
);
assert!(ImportedCommitments::<TestRuntime>::get(
commitments_to_keep as TestBridgedBlockNumber + 1
)
.is_some());
assert_eq!(
ImportedBlockNumbers::<TestRuntime>::get(0),
Some(Into::into(commitments_to_keep + 1)),
);
// the side effect of the import is that the commitment#1 is pruned
assert!(ImportedCommitments::<TestRuntime>::get(1).is_none());
// import next commitment
next_block();
import_commitment(commitments_to_import[commitments_to_keep as usize + 1].clone())
.expect("must succeed");
assert_eq!(
ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().next_block_number_index,
2
);
assert!(ImportedCommitments::<TestRuntime>::get(
commitments_to_keep as TestBridgedBlockNumber + 2
)
.is_some());
assert_eq!(
ImportedBlockNumbers::<TestRuntime>::get(1),
Some(Into::into(commitments_to_keep + 2)),
);
// the side effect of the import is that the commitment#2 is pruned
assert!(ImportedCommitments::<TestRuntime>::get(1).is_none());
assert!(ImportedCommitments::<TestRuntime>::get(2).is_none());
});
}
generate_owned_bridge_module_tests!(BasicOperatingMode::Normal, BasicOperatingMode::Halted);
}
+196
View File
@@ -0,0 +1,196 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
use crate as beefy;
use crate::{
utils::get_authorities_mmr_root, BridgedBeefyAuthoritySet, BridgedBeefyAuthoritySetInfo,
BridgedBeefyCommitmentHasher, BridgedBeefyMmrLeafExtra, BridgedBeefySignedCommitment,
BridgedMmrHash, BridgedMmrHashing, BridgedMmrProof,
};
use bp_beefy::{BeefyValidatorSignatureOf, ChainWithBeefy, Commitment, MmrDataOrHash};
use pezbp_runtime::{BasicOperatingMode, Chain, ChainId};
use codec::Encode;
use pezframe_support::{construct_runtime, derive_impl, weights::Weight};
use pezsp_core::{sr25519::Signature, Pair};
use pezsp_runtime::{
testing::{Header, H256},
traits::{BlakeTwo256, Hash},
StateVersion,
};
pub use pezsp_consensus_beefy::ecdsa_crypto::{AuthorityId as BeefyId, Pair as BeefyPair};
use pezsp_core::crypto::Wraps;
use pezsp_runtime::traits::Keccak256;
pub type TestAccountId = u64;
pub type TestBridgedBlockNumber = u64;
pub type TestBridgedBlockHash = H256;
pub type TestBridgedHeader = Header;
pub type TestBridgedAuthoritySetInfo = BridgedBeefyAuthoritySetInfo<TestRuntime, ()>;
pub type TestBridgedValidatorSet = BridgedBeefyAuthoritySet<TestRuntime, ()>;
pub type TestBridgedCommitment = BridgedBeefySignedCommitment<TestRuntime, ()>;
pub type TestBridgedValidatorSignature = BeefyValidatorSignatureOf<TestBridgedChain>;
pub type TestBridgedCommitmentHasher = BridgedBeefyCommitmentHasher<TestRuntime, ()>;
pub type TestBridgedMmrHashing = BridgedMmrHashing<TestRuntime, ()>;
pub type TestBridgedMmrHash = BridgedMmrHash<TestRuntime, ()>;
pub type TestBridgedBeefyMmrLeafExtra = BridgedBeefyMmrLeafExtra<TestRuntime, ()>;
pub type TestBridgedMmrProof = BridgedMmrProof<TestRuntime, ()>;
pub type TestBridgedRawMmrLeaf = pezsp_consensus_beefy::mmr::MmrLeaf<
TestBridgedBlockNumber,
TestBridgedBlockHash,
TestBridgedMmrHash,
TestBridgedBeefyMmrLeafExtra,
>;
pub type TestBridgedMmrNode = MmrDataOrHash<Keccak256, TestBridgedRawMmrLeaf>;
type Block = pezframe_system::mocking::MockBlock<TestRuntime>;
construct_runtime! {
pub enum TestRuntime
{
System: pezframe_system::{Pezpallet, Call, Config<T>, Storage, Event<T>},
Beefy: beefy::{Pezpallet},
}
}
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
impl pezframe_system::Config for TestRuntime {
type Block = Block;
}
impl beefy::Config for TestRuntime {
type MaxRequests = pezframe_support::traits::ConstU32<16>;
type BridgedChain = TestBridgedChain;
type CommitmentsToKeep = pezframe_support::traits::ConstU32<16>;
}
#[derive(Debug)]
pub struct TestBridgedChain;
impl Chain for TestBridgedChain {
const ID: ChainId = *b"tbch";
type BlockNumber = TestBridgedBlockNumber;
type Hash = H256;
type Hasher = BlakeTwo256;
type Header = pezsp_runtime::testing::Header;
type AccountId = TestAccountId;
type Balance = u64;
type Nonce = u64;
type Signature = Signature;
const STATE_VERSION: StateVersion = StateVersion::V1;
fn max_extrinsic_size() -> u32 {
unreachable!()
}
fn max_extrinsic_weight() -> Weight {
unreachable!()
}
}
impl ChainWithBeefy for TestBridgedChain {
type CommitmentHasher = Keccak256;
type MmrHashing = Keccak256;
type MmrHash = <Keccak256 as Hash>::Output;
type BeefyMmrLeafExtra = ();
type AuthorityId = BeefyId;
type AuthorityIdToMerkleLeaf = pezpallet_beefy_mmr::BeefyEcdsaToEthereum;
}
/// Run test within test runtime.
pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
pezsp_io::TestExternalities::new(Default::default()).execute_with(test)
}
/// Initialize pezpallet and run test.
pub fn run_test_with_initialize<T>(initial_validators_count: u32, test: impl FnOnce() -> T) -> T {
run_test(|| {
let validators = validator_ids(0, initial_validators_count);
let authority_set = authority_set_info(0, &validators);
crate::Pezpallet::<TestRuntime>::initialize(
RuntimeOrigin::root(),
bp_beefy::InitializationData {
operating_mode: BasicOperatingMode::Normal,
best_block_number: 0,
authority_set,
},
)
.expect("initialization data is correct");
test()
})
}
/// Import given commitment.
pub fn import_commitment(
header: crate::mock_chain::HeaderAndCommitment,
) -> pezsp_runtime::DispatchResult {
crate::Pezpallet::<TestRuntime>::submit_commitment(
RuntimeOrigin::signed(1),
header
.commitment
.expect("thou shall not call import_commitment on header without commitment"),
header.validator_set,
Box::new(header.leaf),
header.leaf_proof,
)
}
pub fn validator_pairs(index: u32, count: u32) -> Vec<BeefyPair> {
(index..index + count)
.map(|index| {
let mut seed = [1u8; 32];
seed[0..8].copy_from_slice(&(index as u64).encode());
BeefyPair::from_seed(&seed)
})
.collect()
}
/// Return identifiers of validators, starting at given index.
pub fn validator_ids(index: u32, count: u32) -> Vec<BeefyId> {
validator_pairs(index, count).into_iter().map(|pair| pair.public()).collect()
}
pub fn authority_set_info(id: u64, validators: &[BeefyId]) -> TestBridgedAuthoritySetInfo {
let merkle_root = get_authorities_mmr_root::<TestRuntime, (), _>(validators.iter());
TestBridgedAuthoritySetInfo { id, len: validators.len() as u32, keyset_commitment: merkle_root }
}
/// Sign BEEFY commitment.
pub fn sign_commitment(
commitment: Commitment<TestBridgedBlockNumber>,
validator_pairs: &[BeefyPair],
signature_count: usize,
) -> TestBridgedCommitment {
let total_validators = validator_pairs.len();
let random_validators =
rand::seq::index::sample(&mut rand::thread_rng(), total_validators, signature_count);
let commitment_hash = TestBridgedCommitmentHasher::hash(&commitment.encode());
let mut signatures = vec![None; total_validators];
for validator_idx in random_validators.iter() {
let validator = &validator_pairs[validator_idx];
signatures[validator_idx] =
Some(validator.as_inner_ref().sign_prehashed(commitment_hash.as_fixed_bytes()).into());
}
TestBridgedCommitment { commitment, signatures }
}
+301
View File
@@ -0,0 +1,301 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Utilities to build bridged chain and BEEFY+MMR structures.
use crate::{
mock::{
sign_commitment, validator_pairs, BeefyPair, TestBridgedBlockNumber, TestBridgedCommitment,
TestBridgedHeader, TestBridgedMmrHash, TestBridgedMmrHashing, TestBridgedMmrNode,
TestBridgedMmrProof, TestBridgedRawMmrLeaf, TestBridgedValidatorSet,
TestBridgedValidatorSignature, TestRuntime,
},
utils::get_authorities_mmr_root,
};
use bp_beefy::{BeefyPayload, Commitment, ValidatorSetId, MMR_ROOT_PAYLOAD_ID};
use codec::Encode;
use pezpallet_mmr::NodeIndex;
use rand::Rng;
use pezsp_consensus_beefy::mmr::{BeefyNextAuthoritySet, MmrLeafVersion};
use pezsp_core::Pair;
use pezsp_runtime::traits::{Hash, Header as HeaderT};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct HeaderAndCommitment {
pub header: TestBridgedHeader,
pub commitment: Option<TestBridgedCommitment>,
pub validator_set: TestBridgedValidatorSet,
pub leaf: TestBridgedRawMmrLeaf,
pub leaf_proof: TestBridgedMmrProof,
pub mmr_root: TestBridgedMmrHash,
}
impl HeaderAndCommitment {
pub fn customize_signatures(
&mut self,
f: impl FnOnce(&mut Vec<Option<TestBridgedValidatorSignature>>),
) {
if let Some(commitment) = &mut self.commitment {
f(&mut commitment.signatures);
}
}
pub fn customize_commitment(
&mut self,
f: impl FnOnce(&mut Commitment<TestBridgedBlockNumber>),
validator_pairs: &[BeefyPair],
signature_count: usize,
) {
if let Some(mut commitment) = self.commitment.take() {
f(&mut commitment.commitment);
self.commitment =
Some(sign_commitment(commitment.commitment, validator_pairs, signature_count));
}
}
}
pub struct ChainBuilder {
headers: Vec<HeaderAndCommitment>,
validator_set_id: ValidatorSetId,
validator_keys: Vec<BeefyPair>,
mmr: mmr_lib::MMR<TestBridgedMmrNode, BridgedMmrHashMerge, BridgedMmrStorage>,
}
struct BridgedMmrStorage {
nodes: HashMap<NodeIndex, TestBridgedMmrNode>,
}
impl mmr_lib::MMRStoreReadOps<TestBridgedMmrNode> for BridgedMmrStorage {
fn get_elem(&self, pos: NodeIndex) -> mmr_lib::Result<Option<TestBridgedMmrNode>> {
Ok(self.nodes.get(&pos).cloned())
}
}
impl mmr_lib::MMRStoreWriteOps<TestBridgedMmrNode> for BridgedMmrStorage {
fn append(&mut self, pos: NodeIndex, elems: Vec<TestBridgedMmrNode>) -> mmr_lib::Result<()> {
for (i, elem) in elems.into_iter().enumerate() {
self.nodes.insert(pos + i as NodeIndex, elem);
}
Ok(())
}
}
impl ChainBuilder {
/// Creates new chain builder with given validator set size.
pub fn new(initial_validators_count: u32) -> Self {
ChainBuilder {
headers: Vec::new(),
validator_set_id: 0,
validator_keys: validator_pairs(0, initial_validators_count),
mmr: mmr_lib::MMR::new(0, BridgedMmrStorage { nodes: HashMap::new() }),
}
}
/// Get header with given number.
pub fn header(&self, number: TestBridgedBlockNumber) -> HeaderAndCommitment {
self.headers[number as usize - 1].clone()
}
/// Returns single built header.
pub fn to_header(&self) -> HeaderAndCommitment {
assert_eq!(self.headers.len(), 1);
self.headers[0].clone()
}
/// Returns built chain.
pub fn to_chain(&self) -> Vec<HeaderAndCommitment> {
self.headers.clone()
}
/// Appends header, that has been finalized by BEEFY (so it has a linked signed commitment).
pub fn append_finalized_header(self) -> Self {
let next_validator_set_id = self.validator_set_id;
let next_validator_keys = self.validator_keys.clone();
HeaderBuilder::with_chain(self, next_validator_set_id, next_validator_keys).finalize()
}
/// Append multiple finalized headers at once.
pub fn append_finalized_headers(mut self, count: usize) -> Self {
for _ in 0..count {
self = self.append_finalized_header();
}
self
}
/// Appends header, that enacts new validator set.
///
/// Such headers are explicitly finalized by BEEFY.
pub fn append_handoff_header(self, next_validators_len: u32) -> Self {
let new_validator_set_id = self.validator_set_id + 1;
let new_validator_pairs =
validator_pairs(rand::thread_rng().gen::<u32>() % (u32::MAX / 2), next_validators_len);
HeaderBuilder::with_chain(self, new_validator_set_id, new_validator_pairs).finalize()
}
/// Append several default header without commitment.
pub fn append_default_headers(mut self, count: usize) -> Self {
for _ in 0..count {
let next_validator_set_id = self.validator_set_id;
let next_validator_keys = self.validator_keys.clone();
self =
HeaderBuilder::with_chain(self, next_validator_set_id, next_validator_keys).build()
}
self
}
}
/// Custom header builder.
pub struct HeaderBuilder {
chain: ChainBuilder,
header: TestBridgedHeader,
leaf: TestBridgedRawMmrLeaf,
leaf_proof: Option<TestBridgedMmrProof>,
next_validator_set_id: ValidatorSetId,
next_validator_keys: Vec<BeefyPair>,
}
impl HeaderBuilder {
fn with_chain(
chain: ChainBuilder,
next_validator_set_id: ValidatorSetId,
next_validator_keys: Vec<BeefyPair>,
) -> Self {
// we're starting with header#1, since header#0 is always finalized
let header_number = chain.headers.len() as TestBridgedBlockNumber + 1;
let header = TestBridgedHeader::new(
header_number,
Default::default(),
Default::default(),
chain.headers.last().map(|h| h.header.hash()).unwrap_or_default(),
Default::default(),
);
let next_validators =
next_validator_keys.iter().map(|pair| pair.public()).collect::<Vec<_>>();
let next_validators_mmr_root =
get_authorities_mmr_root::<TestRuntime, (), _>(next_validators.iter());
let leaf = pezsp_consensus_beefy::mmr::MmrLeaf {
version: MmrLeafVersion::new(1, 0),
parent_number_and_hash: (header.number().saturating_sub(1), *header.parent_hash()),
beefy_next_authority_set: BeefyNextAuthoritySet {
id: next_validator_set_id,
len: next_validators.len() as u32,
keyset_commitment: next_validators_mmr_root,
},
leaf_extra: (),
};
HeaderBuilder {
chain,
header,
leaf,
leaf_proof: None,
next_validator_keys,
next_validator_set_id,
}
}
/// Customize generated proof of header MMR leaf.
///
/// Can only be called once.
pub fn customize_proof(
mut self,
f: impl FnOnce(TestBridgedMmrProof) -> TestBridgedMmrProof,
) -> Self {
assert!(self.leaf_proof.is_none());
let leaf_hash = TestBridgedMmrHashing::hash(&self.leaf.encode());
let node = TestBridgedMmrNode::Hash(leaf_hash);
let leaf_position = self.chain.mmr.push(node).unwrap();
let proof = self.chain.mmr.gen_proof(vec![leaf_position]).unwrap();
// genesis has no leaf => leaf index is header number minus 1
let leaf_index = *self.header.number() - 1;
let leaf_count = *self.header.number();
self.leaf_proof = Some(f(TestBridgedMmrProof {
leaf_indices: vec![leaf_index],
leaf_count,
items: proof.proof_items().iter().map(|i| i.hash()).collect(),
}));
self
}
/// Build header without commitment.
pub fn build(mut self) -> ChainBuilder {
if self.leaf_proof.is_none() {
self = self.customize_proof(|proof| proof);
}
let validators =
self.chain.validator_keys.iter().map(|pair| pair.public()).collect::<Vec<_>>();
self.chain.headers.push(HeaderAndCommitment {
header: self.header,
commitment: None,
validator_set: TestBridgedValidatorSet::new(validators, self.chain.validator_set_id)
.unwrap(),
leaf: self.leaf,
leaf_proof: self.leaf_proof.expect("guaranteed by the customize_proof call above; qed"),
mmr_root: self.chain.mmr.get_root().unwrap().hash(),
});
self.chain.validator_set_id = self.next_validator_set_id;
self.chain.validator_keys = self.next_validator_keys;
self.chain
}
/// Build header with commitment.
pub fn finalize(self) -> ChainBuilder {
let validator_count = self.chain.validator_keys.len();
let current_validator_set_id = self.chain.validator_set_id;
let current_validator_set_keys = self.chain.validator_keys.clone();
let mut chain = self.build();
let last_header = chain.headers.last_mut().expect("added by append_header; qed");
last_header.commitment = Some(sign_commitment(
Commitment {
payload: BeefyPayload::from_single_entry(
MMR_ROOT_PAYLOAD_ID,
chain.mmr.get_root().unwrap().hash().encode(),
),
block_number: *last_header.header.number(),
validator_set_id: current_validator_set_id,
},
&current_validator_set_keys,
validator_count * 2 / 3 + 1,
));
chain
}
}
/// Default Merging & Hashing behavior for MMR.
pub struct BridgedMmrHashMerge;
impl mmr_lib::Merge for BridgedMmrHashMerge {
type Item = TestBridgedMmrNode;
fn merge(left: &Self::Item, right: &Self::Item) -> mmr_lib::Result<Self::Item> {
let mut concat = left.hash().as_ref().to_vec();
concat.extend_from_slice(right.hash().as_ref());
Ok(TestBridgedMmrNode::Hash(TestBridgedMmrHashing::hash(&concat)))
}
}
+360
View File
@@ -0,0 +1,360 @@
use crate::{
BridgedBeefyAuthorityId, BridgedBeefyAuthoritySet, BridgedBeefyAuthoritySetInfo,
BridgedBeefyMmrLeaf, BridgedBeefySignedCommitment, BridgedChain, BridgedMmrHash,
BridgedMmrHashing, BridgedMmrProof, Config, Error, LOG_TARGET,
};
use bp_beefy::{merkle_root, verify_mmr_leaves_proof, BeefyAuthorityId, MmrDataOrHash};
use codec::Encode;
use pezframe_support::ensure;
use pezsp_runtime::traits::{Convert, Hash};
use pezsp_std::{vec, vec::Vec};
type BridgedMmrDataOrHash<T, I> = MmrDataOrHash<BridgedMmrHashing<T, I>, BridgedBeefyMmrLeaf<T, I>>;
/// A way to encode validator id to the BEEFY merkle tree leaf.
type BridgedBeefyAuthorityIdToMerkleLeaf<T, I> =
bp_beefy::BeefyAuthorityIdToMerkleLeafOf<BridgedChain<T, I>>;
/// Get the MMR root for a collection of validators.
pub(crate) fn get_authorities_mmr_root<
'a,
T: Config<I>,
I: 'static,
V: Iterator<Item = &'a BridgedBeefyAuthorityId<T, I>>,
>(
authorities: V,
) -> BridgedMmrHash<T, I> {
let merkle_leafs = authorities
.cloned()
.map(BridgedBeefyAuthorityIdToMerkleLeaf::<T, I>::convert)
.collect::<Vec<_>>();
merkle_root::<BridgedMmrHashing<T, I>, _>(merkle_leafs)
}
fn verify_authority_set<T: Config<I>, I: 'static>(
authority_set_info: &BridgedBeefyAuthoritySetInfo<T, I>,
authority_set: &BridgedBeefyAuthoritySet<T, I>,
) -> Result<(), Error<T, I>> {
ensure!(authority_set.id() == authority_set_info.id, Error::<T, I>::InvalidValidatorSetId);
ensure!(
authority_set.len() == authority_set_info.len as usize,
Error::<T, I>::InvalidValidatorSetLen
);
// Ensure that the authority set that signed the commitment is the expected one.
let root = get_authorities_mmr_root::<T, I, _>(authority_set.validators().iter());
ensure!(root == authority_set_info.keyset_commitment, Error::<T, I>::InvalidValidatorSetRoot);
Ok(())
}
/// Number of correct signatures, required from given validators set to accept signed
/// commitment.
///
/// We're using 'conservative' approach here, where signatures of `2/3+1` validators are
/// required..
pub(crate) fn signatures_required(validators_len: usize) -> usize {
validators_len - validators_len.saturating_sub(1) / 3
}
fn verify_signatures<T: Config<I>, I: 'static>(
commitment: &BridgedBeefySignedCommitment<T, I>,
authority_set: &BridgedBeefyAuthoritySet<T, I>,
) -> Result<(), Error<T, I>> {
ensure!(
commitment.signatures.len() == authority_set.len(),
Error::<T, I>::InvalidCommitmentSignaturesLen
);
// Ensure that the commitment was signed by enough authorities.
let msg = commitment.commitment.encode();
let mut missing_signatures = signatures_required(authority_set.len());
for (idx, (authority, maybe_sig)) in
authority_set.validators().iter().zip(commitment.signatures.iter()).enumerate()
{
if let Some(sig) = maybe_sig {
if authority.verify(sig, &msg) {
missing_signatures = missing_signatures.saturating_sub(1);
if missing_signatures == 0 {
break;
}
} else {
tracing::debug!(
target: LOG_TARGET,
%idx,
?authority,
?sig,
"Signed commitment contains incorrect signature of validator"
);
}
}
}
ensure!(missing_signatures == 0, Error::<T, I>::NotEnoughCorrectSignatures);
Ok(())
}
/// Extract MMR root from commitment payload.
fn extract_mmr_root<T: Config<I>, I: 'static>(
commitment: &BridgedBeefySignedCommitment<T, I>,
) -> Result<BridgedMmrHash<T, I>, Error<T, I>> {
commitment
.commitment
.payload
.get_decoded(&bp_beefy::MMR_ROOT_PAYLOAD_ID)
.ok_or(Error::MmrRootMissingFromCommitment)
}
pub(crate) fn verify_commitment<T: Config<I>, I: 'static>(
commitment: &BridgedBeefySignedCommitment<T, I>,
authority_set_info: &BridgedBeefyAuthoritySetInfo<T, I>,
authority_set: &BridgedBeefyAuthoritySet<T, I>,
) -> Result<BridgedMmrHash<T, I>, Error<T, I>> {
// Ensure that the commitment is signed by the best known BEEFY validator set.
ensure!(
commitment.commitment.validator_set_id == authority_set_info.id,
Error::<T, I>::InvalidCommitmentValidatorSetId
);
ensure!(
commitment.signatures.len() == authority_set_info.len as usize,
Error::<T, I>::InvalidCommitmentSignaturesLen
);
verify_authority_set(authority_set_info, authority_set)?;
verify_signatures(commitment, authority_set)?;
extract_mmr_root(commitment)
}
/// Verify MMR proof of given leaf.
pub(crate) fn verify_beefy_mmr_leaf<T: Config<I>, I: 'static>(
mmr_leaf: &BridgedBeefyMmrLeaf<T, I>,
mmr_proof: BridgedMmrProof<T, I>,
mmr_root: BridgedMmrHash<T, I>,
) -> Result<(), Error<T, I>> {
let mmr_proof_leaf_count = mmr_proof.leaf_count;
let mmr_proof_length = mmr_proof.items.len();
// Verify the mmr proof for the provided leaf.
let mmr_leaf_hash = BridgedMmrHashing::<T, I>::hash(&mmr_leaf.encode());
verify_mmr_leaves_proof(
mmr_root,
vec![BridgedMmrDataOrHash::<T, I>::Hash(mmr_leaf_hash)],
mmr_proof,
)
.map_err(|e| {
tracing::error!(
target: LOG_TARGET,
error=?e,
?mmr_leaf_hash,
root=?mmr_root,
leaf_count=%mmr_proof_leaf_count,
len=%mmr_proof_length,
"MMR proof of leaf verification has failed"
);
Error::<T, I>::MmrProofVerificationFailed
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{mock::*, mock_chain::*, *};
use bp_beefy::{BeefyPayload, MMR_ROOT_PAYLOAD_ID};
use pezframe_support::{assert_noop, assert_ok};
use pezsp_consensus_beefy::ValidatorSet;
#[test]
fn submit_commitment_checks_metadata() {
run_test_with_initialize(8, || {
// Fails if `commitment.commitment.validator_set_id` differs.
let mut header = ChainBuilder::new(8).append_finalized_header().to_header();
header.customize_commitment(
|commitment| {
commitment.validator_set_id += 1;
},
&validator_pairs(0, 8),
6,
);
assert_noop!(
import_commitment(header),
Error::<TestRuntime, ()>::InvalidCommitmentValidatorSetId,
);
// Fails if `commitment.signatures.len()` differs.
let mut header = ChainBuilder::new(8).append_finalized_header().to_header();
header.customize_signatures(|signatures| {
signatures.pop();
});
assert_noop!(
import_commitment(header),
Error::<TestRuntime, ()>::InvalidCommitmentSignaturesLen,
);
});
}
#[test]
fn submit_commitment_checks_validator_set() {
run_test_with_initialize(8, || {
// Fails if `ValidatorSet::id` differs.
let mut header = ChainBuilder::new(8).append_finalized_header().to_header();
header.validator_set = ValidatorSet::new(validator_ids(0, 8), 1).unwrap();
assert_noop!(
import_commitment(header),
Error::<TestRuntime, ()>::InvalidValidatorSetId,
);
// Fails if `ValidatorSet::len()` differs.
let mut header = ChainBuilder::new(8).append_finalized_header().to_header();
header.validator_set = ValidatorSet::new(validator_ids(0, 5), 0).unwrap();
assert_noop!(
import_commitment(header),
Error::<TestRuntime, ()>::InvalidValidatorSetLen,
);
// Fails if the validators differ.
let mut header = ChainBuilder::new(8).append_finalized_header().to_header();
header.validator_set = ValidatorSet::new(validator_ids(3, 8), 0).unwrap();
assert_noop!(
import_commitment(header),
Error::<TestRuntime, ()>::InvalidValidatorSetRoot,
);
});
}
#[test]
fn submit_commitment_checks_signatures() {
run_test_with_initialize(20, || {
// Fails when there aren't enough signatures.
let mut header = ChainBuilder::new(20).append_finalized_header().to_header();
header.customize_signatures(|signatures| {
let first_signature_idx = signatures.iter().position(Option::is_some).unwrap();
signatures[first_signature_idx] = None;
});
assert_noop!(
import_commitment(header),
Error::<TestRuntime, ()>::NotEnoughCorrectSignatures,
);
// Fails when there aren't enough correct signatures.
let mut header = ChainBuilder::new(20).append_finalized_header().to_header();
header.customize_signatures(|signatures| {
let first_signature_idx = signatures.iter().position(Option::is_some).unwrap();
let last_signature_idx = signatures.len() -
signatures.iter().rev().position(Option::is_some).unwrap() -
1;
signatures[first_signature_idx] = signatures[last_signature_idx].clone();
});
assert_noop!(
import_commitment(header),
Error::<TestRuntime, ()>::NotEnoughCorrectSignatures,
);
// Returns Ok(()) when there are enough signatures, even if some are incorrect.
let mut header = ChainBuilder::new(20).append_finalized_header().to_header();
header.customize_signatures(|signatures| {
let first_signature_idx = signatures.iter().position(Option::is_some).unwrap();
let first_missing_signature_idx =
signatures.iter().position(Option::is_none).unwrap();
signatures[first_missing_signature_idx] = signatures[first_signature_idx].clone();
});
assert_ok!(import_commitment(header));
});
}
#[test]
fn submit_commitment_checks_mmr_proof() {
run_test_with_initialize(1, || {
let validators = validator_pairs(0, 1);
// Fails if leaf is not for parent.
let mut header = ChainBuilder::new(1).append_finalized_header().to_header();
header.leaf.parent_number_and_hash.0 += 1;
assert_noop!(
import_commitment(header),
Error::<TestRuntime, ()>::MmrProofVerificationFailed,
);
// Fails if mmr proof is incorrect.
let mut header = ChainBuilder::new(1).append_finalized_header().to_header();
header.leaf_proof.leaf_indices[0] += 1;
assert_noop!(
import_commitment(header),
Error::<TestRuntime, ()>::MmrProofVerificationFailed,
);
// Fails if mmr root is incorrect.
let mut header = ChainBuilder::new(1).append_finalized_header().to_header();
// Replace MMR root with zeroes.
header.customize_commitment(
|commitment| {
commitment.payload =
BeefyPayload::from_single_entry(MMR_ROOT_PAYLOAD_ID, [0u8; 32].encode());
},
&validators,
1,
);
assert_noop!(
import_commitment(header),
Error::<TestRuntime, ()>::MmrProofVerificationFailed,
);
});
}
#[test]
fn submit_commitment_extracts_mmr_root() {
run_test_with_initialize(1, || {
let validators = validator_pairs(0, 1);
// Fails if there is no mmr root in the payload.
let mut header = ChainBuilder::new(1).append_finalized_header().to_header();
// Remove MMR root from the payload.
header.customize_commitment(
|commitment| {
commitment.payload = BeefyPayload::from_single_entry(*b"xy", vec![]);
},
&validators,
1,
);
assert_noop!(
import_commitment(header),
Error::<TestRuntime, ()>::MmrRootMissingFromCommitment,
);
// Fails if mmr root can't be decoded.
let mut header = ChainBuilder::new(1).append_finalized_header().to_header();
// MMR root is a 32-byte array and we have replaced it with single byte
header.customize_commitment(
|commitment| {
commitment.payload =
BeefyPayload::from_single_entry(MMR_ROOT_PAYLOAD_ID, vec![42]);
},
&validators,
1,
);
assert_noop!(
import_commitment(header),
Error::<TestRuntime, ()>::MmrRootMissingFromCommitment,
);
});
}
#[test]
fn submit_commitment_stores_valid_data() {
run_test_with_initialize(20, || {
let header = ChainBuilder::new(20).append_handoff_header(30).to_header();
assert_ok!(import_commitment(header.clone()));
assert_eq!(ImportedCommitmentsInfo::<TestRuntime>::get().unwrap().best_block_number, 1);
assert_eq!(CurrentAuthoritySetInfo::<TestRuntime>::get().id, 1);
assert_eq!(CurrentAuthoritySetInfo::<TestRuntime>::get().len, 30);
assert_eq!(
ImportedCommitments::<TestRuntime>::get(1).unwrap(),
bp_beefy::ImportedCommitment {
parent_number_and_hash: (0, [0; 32].into()),
mmr_root: header.mmr_root,
},
);
});
}
}
+74
View File
@@ -0,0 +1,74 @@
[package]
name = "pezpallet-bridge-grandpa"
version = "0.7.0"
description = "Module implementing GRANDPA on-chain light client used for bridging consensus of bizinikiwi-based chains."
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
repository.workspace = true
documentation = "https://docs.rs/pezpallet-bridge-grandpa"
homepage = { workspace = true }
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
codec = { workspace = true }
scale-info = { features = ["derive"], workspace = true }
tracing = { workspace = true }
# Bridge Dependencies
bp-header-pez-chain = { workspace = true }
pezbp-runtime = { workspace = true }
# Bizinikiwi Dependencies
pezframe-support = { workspace = true }
pezframe-system = { workspace = true }
pezsp-consensus-grandpa = { features = ["serde"], workspace = true }
pezsp-runtime = { features = ["serde"], workspace = true }
pezsp-std = { workspace = true }
# Optional Benchmarking Dependencies
bp-test-utils = { optional = true, workspace = true }
pezframe-benchmarking = { optional = true, workspace = true }
[dev-dependencies]
pezbp-runtime = { features = ["test-helpers"], workspace = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-io = { workspace = true, default-features = true }
[features]
default = ["std"]
std = [
"bp-header-pez-chain/std",
"pezbp-runtime/std",
"bp-test-utils/std",
"codec/std",
"pezframe-benchmarking/std",
"pezframe-support/std",
"pezframe-system/std",
"scale-info/std",
"pezsp-consensus-grandpa/std",
"pezsp-runtime/std",
"pezsp-std/std",
"tracing/std",
]
runtime-benchmarks = [
"bp-header-pez-chain/runtime-benchmarks",
"pezbp-runtime/runtime-benchmarks",
"bp-test-utils",
"bp-test-utils?/runtime-benchmarks",
"pezframe-benchmarking/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezsp-consensus-grandpa/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
try-runtime = [
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezsp-runtime/try-runtime",
]
+101
View File
@@ -0,0 +1,101 @@
# Bridge GRANDPA Pezpallet
The bridge GRANDPA pezpallet is a light client for the GRANDPA finality gadget, running at the bridged chain.
It may import headers and their GRANDPA finality proofs (justifications) of the bridged chain. Imported
headers then may be used to verify storage proofs by other pallets. This makes the bridge GRANDPA pezpallet
a basic pezpallet of all bridges with Bizinikiwi-based chains. It is used by all bridge types (bridge between
standalone chains, between teyrchains and any combination of those) and is used by other bridge pallets.
It is used by the teyrchains light client (bridge teyrchains pezpallet) and by messages pezpallet.
## A Brief Introduction into GRANDPA Finality
You can find detailed information on GRANDPA, by exploring its [repository](https://github.com/pezkuwichain/finality-grandpa).
Here is the minimal required GRANDPA information to understand how pezpallet works.
Any Bizinikiwi chain may use different block authorship algorithms (like BABE or Aura) to determine block producers and
generate blocks. This has nothing common with finality, though - the task of block authorship is to coordinate
blocks generation. Any block may be reverted (if there's a fork) if it is not finalized. The finality solution
for (standalone) Bizinikiwi-based chains is the GRANDPA finality gadget. If some block is finalized by the gadget, it
can't be reverted.
In GRANDPA, there are validators, identified by their public keys. They select some generated block and produce
signatures on this block hash. If there are enough (more than `2 / 3 * N`, where `N` is number of validators)
signatures, then the block is considered finalized. The set of signatures for the block is called justification.
Anyone who knows the public keys of validators is able to verify GRANDPA justification and that it is generated
for provided header.
There are two main things in GRANDPA that help building light clients:
- there's no need to import all headers of the bridged chain. Light client may import finalized headers or just
some of finalized headers that it consider useful. While the validators set stays the same, the client may
import any header that is finalized by this set;
- when validators set changes, the GRANDPA gadget adds next set to the header. So light client doesn't need to
verify storage proofs when this happens - it only needs to look at the header and see if it changes the set.
Once set is changed, all following justifications are generated by the new set. Header that is changing the
set is called "mandatory" in the pezpallet. As the name says, the light client need to import all such headers
to be able to operate properly.
## Pezpallet Operations
The main entrypoint of the pezpallet is the `submit_finality_proof_ex` call. It has three arguments - the finalized
headers, associated GRANDPA justification and ID of the authority set that has generated this justification. The
call simply verifies the justification using current validators set and checks if header is better than the
previous best header. If both checks are passed, the header (only its useful fields) is inserted into the runtime
storage and may be used by other pallets to verify storage proofs.
The submitter pays regular fee for submitting all headers, except for the mandatory header. Since it is
required for the pezpallet operations, submitting such header is free. So if you're ok with session-length
lags (meaning that there's exactly 1 mandatory header per session), the cost of pezpallet calls is zero.
When the pezpallet sees mandatory header, it updates the validators set with the set from the header. All
following justifications (until next mandatory header) must be generated by this new set.
## Pezpallet Initialization
As the previous section states, there are two things that are mandatory for pezpallet operations: best finalized
header and the current validators set. Without it the pezpallet can't import any headers. But how to provide
initial values for these fields? There are two options.
First option, while it is easier, doesn't work in all cases. It is to start chain with initial header and
validators set specified in the chain specification. This won't work, however, if we want to add bridge
to already started chain.
For the latter case we have the `initialize` call. It accepts the initial header and initial validators set.
The call may be called by the governance, root or by the pezpallet owner (if it is set).
## Non-Essential Functionality
There may be a special account in every runtime where the bridge GRANDPA module is deployed. This
account, named 'module owner', is like a module-level sudo account - he's able to halt and
resume all module operations without requiring runtime upgrade. Calls that are related to this
account are:
- `fn set_owner()`: current module owner may call it to transfer "ownership" to another account;
- `fn set_operating_mode()`: the module owner (or sudo account) may call this function to stop all
module operations. After this call, all finality proofs will be rejected until further `set_operating_mode` call'.
This call may be used when something extraordinary happens with the bridge;
- `fn initialize()`: module owner may call this function to initialize the bridge.
If pezpallet owner is not defined, the governance may be used to make those calls.
## Signed Extension to Reject Obsolete Headers
It'd be better for anyone (for chain and for submitters) to reject all transactions that are submitting
already known headers to the pezpallet. This way, we leave block space to other useful transactions and
we don't charge concurrent submitters for their honest actions.
To deal with that, we have a [signed extension](./src/call_ext.rs) that may be added to the runtime.
It does exactly what is required - rejects all transactions with already known headers. The submitter
pays nothing for such transactions - they're simply removed from the transaction pool, when the block
is built.
You may also take a look at the [`generate_bridge_reject_obsolete_headers_and_messages`](../../bin/runtime-common/src/lib.rs)
macro that bundles several similar signed extensions in a single one.
## GRANDPA Finality Relay
We have an offchain actor, who is watching for GRANDPA justifications and submits them to the bridged chain.
It is the finality relay - you may look at the [crate level documentation and the code](../../relays/finality/).
@@ -0,0 +1,157 @@
// 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/>.
//! Benchmarks for the GRANDPA Pezpallet.
//!
//! The main dispatchable for the GRANDPA pezpallet is `submit_finality_proof_ex`. Our benchmarks
//! are based around `submit_finality_proof`, though - from weight PoV they are the same calls.
//! There are to main factors which affect finality proof verification:
//!
//! 1. The number of `votes-ancestries` in the justification
//! 2. The number of `pre-commits` in the justification
//!
//! Vote ancestries are the headers between (`finality_target`, `head_of_chain`], where
//! `header_of_chain` is a descendant of `finality_target`.
//!
//! Pre-commits are messages which are signed by validators at the head of the chain they think is
//! the best.
//!
//! Consider the following:
//!
//! / B <- C'
//! A <- B <- C
//!
//! The common ancestor of both forks is block A, so this is what GRANDPA will finalize. In order to
//! verify this we will have vote ancestries of `[B, C, B', C']` and pre-commits `[C, C']`.
//!
//! Note that the worst case scenario here would be a justification where each validator has it's
//! own fork which is `SESSION_LENGTH` blocks long.
use crate::*;
use bp_header_pez_chain::justification::required_justification_precommits;
use pezbp_runtime::BasicOperatingMode;
use bp_test_utils::{
accounts, make_justification_for_header, JustificationGeneratorParams, TEST_GRANDPA_ROUND,
TEST_GRANDPA_SET_ID,
};
use pezframe_benchmarking::{benchmarks_instance_pallet, whitelisted_caller};
use pezframe_system::RawOrigin;
use pezsp_consensus_grandpa::AuthorityId;
use pezsp_runtime::traits::{One, Zero};
use pezsp_std::vec::Vec;
/// The maximum number of vote ancestries to include in a justification.
///
/// In practice this would be limited by the session length (number of blocks a single authority set
/// can produce) of a given chain.
const MAX_VOTE_ANCESTRIES: u32 = 1000;
// `1..MAX_VOTE_ANCESTRIES` is too large && benchmarks are running for almost 40m (steps=50,
// repeat=20) on a decent laptop, which is too much. Since we're building linear function here,
// let's just select some limited subrange for benchmarking.
const MAX_VOTE_ANCESTRIES_RANGE_BEGIN: u32 = MAX_VOTE_ANCESTRIES / 20;
const MAX_VOTE_ANCESTRIES_RANGE_END: u32 =
MAX_VOTE_ANCESTRIES_RANGE_BEGIN + MAX_VOTE_ANCESTRIES_RANGE_BEGIN;
// the same with validators - if there are too much validators, let's run benchmarks on subrange
fn precommits_range_end<T: Config<I>, I: 'static>() -> u32 {
let max_bridged_authorities = T::BridgedChain::MAX_AUTHORITIES_COUNT;
let max_bridged_authorities = if max_bridged_authorities > 128 {
pezsp_std::cmp::max(128, max_bridged_authorities / 5)
} else {
max_bridged_authorities
};
required_justification_precommits(max_bridged_authorities)
}
/// Prepare header and its justification to submit using `submit_finality_proof`.
fn prepare_benchmark_data<T: Config<I>, I: 'static>(
precommits: u32,
ancestors: u32,
) -> (BridgedHeader<T, I>, GrandpaJustification<BridgedHeader<T, I>>) {
// going from precommits to total authorities count
let total_authorities_count = (3 * precommits - 1) / 2;
let authority_list = accounts(total_authorities_count as u16)
.iter()
.map(|id| (AuthorityId::from(*id), 1))
.collect::<Vec<_>>();
let genesis_header: BridgedHeader<T, I> = bp_test_utils::test_header(Zero::zero());
let genesis_hash = genesis_header.hash();
let init_data = InitializationData {
header: Box::new(genesis_header),
authority_list,
set_id: TEST_GRANDPA_SET_ID,
operating_mode: BasicOperatingMode::Normal,
};
bootstrap_bridge::<T, I>(init_data);
assert!(<ImportedHeaders<T, I>>::contains_key(genesis_hash));
let header: BridgedHeader<T, I> = bp_test_utils::test_header(One::one());
let params = JustificationGeneratorParams {
header: header.clone(),
round: TEST_GRANDPA_ROUND,
set_id: TEST_GRANDPA_SET_ID,
authorities: accounts(precommits as u16).iter().map(|k| (*k, 1)).collect::<Vec<_>>(),
ancestors,
forks: 1,
};
let justification = make_justification_for_header(params);
(header, justification)
}
benchmarks_instance_pallet! {
// This is the "gold standard" benchmark for this extrinsic, and it's what should be used to
// annotate the weight in the pezpallet.
submit_finality_proof {
let p in 1 .. precommits_range_end::<T, I>();
let v in MAX_VOTE_ANCESTRIES_RANGE_BEGIN..MAX_VOTE_ANCESTRIES_RANGE_END;
let caller: T::AccountId = whitelisted_caller();
let (header, justification) = prepare_benchmark_data::<T, I>(p, v);
}: submit_finality_proof(RawOrigin::Signed(caller), Box::new(header), justification)
verify {
let genesis_header: BridgedHeader<T, I> = bp_test_utils::test_header(Zero::zero());
let header: BridgedHeader<T, I> = bp_test_utils::test_header(One::one());
let expected_hash = header.hash();
// check that the header#1 has been inserted
assert_eq!(<BestFinalized<T, I>>::get().unwrap().1, expected_hash);
assert!(<ImportedHeaders<T, I>>::contains_key(expected_hash));
// check that the header#0 has been pruned
assert!(!<ImportedHeaders<T, I>>::contains_key(genesis_header.hash()));
}
force_set_pallet_state {
let set_id = 100;
let authorities = accounts(T::BridgedChain::MAX_AUTHORITIES_COUNT as u16)
.iter()
.map(|id| (AuthorityId::from(*id), 1))
.collect::<Vec<_>>();
let (header, _) = prepare_benchmark_data::<T, I>(1, 1);
let expected_hash = header.hash();
}: force_set_pallet_state(RawOrigin::Root, set_id, authorities, Box::new(header))
verify {
assert_eq!(<BestFinalized<T, I>>::get().unwrap().1, expected_hash);
assert_eq!(<CurrentAuthoritySet<T, I>>::get().set_id, set_id);
}
impl_benchmark_test_suite!(Pezpallet, crate::mock::new_test_ext(), crate::mock::TestRuntime)
}
+720
View File
@@ -0,0 +1,720 @@
// 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/>.
use crate::{
weights::WeightInfo, BestFinalized, BridgedBlockNumber, BridgedHeader, Config,
CurrentAuthoritySet, Error, FreeHeadersRemaining, Pezpallet,
};
use bp_header_pez_chain::{
justification::GrandpaJustification, submit_finality_proof_limits_extras,
SubmitFinalityProofInfo,
};
use pezbp_runtime::{BlockNumberOf, Chain, OwnedBridgeModule};
use pezframe_support::{
dispatch::CallableCallFor,
traits::{Get, IsSubType},
weights::Weight,
};
use pezsp_consensus_grandpa::SetId;
use pezsp_runtime::{
traits::{CheckedSub, Header, Zero},
transaction_validity::{InvalidTransaction, TransactionValidityError},
RuntimeDebug, SaturatedConversion,
};
use pezsp_std::fmt::Debug;
/// Verified `SubmitFinalityProofInfo<N>`.
#[derive(Copy, Clone, PartialEq, RuntimeDebug)]
pub struct VerifiedSubmitFinalityProofInfo<N: Debug> {
/// Base call information.
pub base: SubmitFinalityProofInfo<N>,
/// A difference between bundled bridged header and best bridged header known to us
/// before the call.
pub improved_by: N,
}
/// Helper struct that provides methods for working with the `SubmitFinalityProof` call.
pub struct SubmitFinalityProofHelper<T: Config<I>, I: 'static> {
_phantom_data: pezsp_std::marker::PhantomData<(T, I)>,
}
impl<T: Config<I>, I: 'static> SubmitFinalityProofHelper<T, I> {
/// Returns `true` if we may fit more free headers into the current block. If `false` is
/// returned, the call will be paid even if `is_free_execution_expected` has been set
/// to `true`.
pub fn has_free_header_slots() -> bool {
// `unwrap_or(u32::MAX)` means that if `FreeHeadersRemaining` is `None`, we may accept
// this header for free. That is a small cheat - it is `None` if executed outside of
// transaction (e.g. during block initialization). Normal relayer would never submit
// such calls, but if he did, that is not our problem. During normal transactions,
// the `FreeHeadersRemaining` is always `Some(_)`.
let free_headers_remaining = FreeHeadersRemaining::<T, I>::get().unwrap_or(u32::MAX);
free_headers_remaining > 0
}
/// Check that the: (1) GRANDPA head provided by the `SubmitFinalityProof` is better than the
/// best one we know (2) if `current_set_id` matches the current authority set id, if specified
/// and (3) whether transaction MAY be free for the submitter if `is_free_execution_expected`
/// is `true`.
///
/// Returns number of headers between the current best finalized header, known to the pezpallet
/// and the bundled header.
pub fn check_obsolete_from_extension(
call_info: &SubmitFinalityProofInfo<BlockNumberOf<T::BridgedChain>>,
) -> Result<BlockNumberOf<T::BridgedChain>, Error<T, I>> {
// do basic checks first
let improved_by = Self::check_obsolete(call_info.block_number, call_info.current_set_id)?;
// if submitter has NOT specified that it wants free execution, then we are done
if !call_info.is_free_execution_expected {
return Ok(improved_by);
}
// else - if we can not accept more free headers, "reject" the transaction
if !Self::has_free_header_slots() {
tracing::trace!(
target: crate::LOG_TARGET,
chain_id=?T::BridgedChain::ID,
block_number=?call_info.block_number,
"Cannot accept free header. No more free slots remaining"
);
return Err(Error::<T, I>::FreeHeadersLimitExceded);
}
// ensure that the `improved_by` is larger than the configured free interval
if !call_info.is_mandatory {
if let Some(free_headers_interval) = T::FreeHeadersInterval::get() {
if improved_by < free_headers_interval.into() {
tracing::trace!(
target: crate::LOG_TARGET,
chain_id=?T::BridgedChain::ID,
block_number=?call_info.block_number,
?improved_by,
%free_headers_interval,
"Cannot accept free header. Too small difference between submitted headers"
);
return Err(Error::<T, I>::BelowFreeHeaderInterval);
}
}
}
// let's also check whether the header submission fits the hardcoded limits. A normal
// relayer would check that before submitting a transaction (since limits are constants
// and do not depend on a volatile runtime state), but the ckeck itself is cheap, so
// let's do it here too
if !call_info.fits_limits() {
return Err(Error::<T, I>::HeaderOverflowLimits);
}
Ok(improved_by)
}
/// Check that the GRANDPA head provided by the `SubmitFinalityProof` is better than the best
/// one we know. Additionally, checks if `current_set_id` matches the current authority set
/// id, if specified. This method is called by the call code and the transaction extension,
/// so it does not check the free execution.
///
/// Returns number of headers between the current best finalized header, known to the pezpallet
/// and the bundled header.
pub fn check_obsolete(
finality_target: BlockNumberOf<T::BridgedChain>,
current_set_id: Option<SetId>,
) -> Result<BlockNumberOf<T::BridgedChain>, Error<T, I>> {
let best_finalized = BestFinalized::<T, I>::get().ok_or_else(|| {
tracing::trace!(
target: crate::LOG_TARGET,
header=?finality_target,
"Cannot finalize header because pezpallet is not yet initialized"
);
<Error<T, I>>::NotInitialized
})?;
let improved_by = match finality_target.checked_sub(&best_finalized.number()) {
Some(improved_by) if improved_by > Zero::zero() => improved_by,
_ => {
tracing::trace!(
target: crate::LOG_TARGET,
bundled=?finality_target,
best=?best_finalized,
"Cannot finalize obsolete header"
);
return Err(Error::<T, I>::OldHeader);
},
};
if let Some(current_set_id) = current_set_id {
let actual_set_id = <CurrentAuthoritySet<T, I>>::get().set_id;
if current_set_id != actual_set_id {
tracing::trace!(
target: crate::LOG_TARGET,
bundled=?current_set_id,
best=?actual_set_id,
"Cannot finalize header signed by unknown authority set"
);
return Err(Error::<T, I>::InvalidAuthoritySetId);
}
}
Ok(improved_by)
}
/// Check if the `SubmitFinalityProof` was successfully executed.
pub fn was_successful(finality_target: BlockNumberOf<T::BridgedChain>) -> bool {
match BestFinalized::<T, I>::get() {
Some(best_finalized) => best_finalized.number() == finality_target,
None => false,
}
}
}
/// Trait representing a call that is a sub type of this pezpallet's call.
pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
IsSubType<CallableCallFor<Pezpallet<T, I>, T>>
{
/// Extract finality proof info from a runtime call.
fn submit_finality_proof_info(
&self,
) -> Option<SubmitFinalityProofInfo<BridgedBlockNumber<T, I>>> {
if let Some(crate::Call::<T, I>::submit_finality_proof { finality_target, justification }) =
self.is_sub_type()
{
return Some(submit_finality_proof_info_from_args::<T, I>(
finality_target,
justification,
None,
false,
));
} else if let Some(crate::Call::<T, I>::submit_finality_proof_ex {
finality_target,
justification,
current_set_id,
is_free_execution_expected,
}) = self.is_sub_type()
{
return Some(submit_finality_proof_info_from_args::<T, I>(
finality_target,
justification,
Some(*current_set_id),
*is_free_execution_expected,
));
}
None
}
/// Validate Grandpa headers in order to avoid "mining" transactions that provide outdated
/// bridged chain headers. Without this validation, even honest relayers may lose their funds
/// if there are multiple relays running and submitting the same information.
///
/// Returns `Ok(None)` if the call is not the `submit_finality_proof` call of our pezpallet.
/// Returns `Ok(Some(_))` if the call is the `submit_finality_proof` call of our pezpallet and
/// we believe the call brings header that improves the pezpallet state.
/// Returns `Err(_)` if the call is the `submit_finality_proof` call of our pezpallet and we
/// believe that the call will fail.
fn check_obsolete_submit_finality_proof(
&self,
) -> Result<
Option<VerifiedSubmitFinalityProofInfo<BridgedBlockNumber<T, I>>>,
TransactionValidityError,
>
where
Self: Sized,
{
let call_info = match self.submit_finality_proof_info() {
Some(finality_proof) => finality_proof,
_ => return Ok(None),
};
if Pezpallet::<T, I>::ensure_not_halted().is_err() {
return Err(InvalidTransaction::Call.into());
}
let result = SubmitFinalityProofHelper::<T, I>::check_obsolete_from_extension(&call_info);
match result {
Ok(improved_by) =>
Ok(Some(VerifiedSubmitFinalityProofInfo { base: call_info, improved_by })),
Err(Error::<T, I>::OldHeader) => Err(InvalidTransaction::Stale.into()),
Err(_) => Err(InvalidTransaction::Call.into()),
}
}
}
impl<T: Config<I>, I: 'static> CallSubType<T, I> for T::RuntimeCall where
T::RuntimeCall: IsSubType<CallableCallFor<Pezpallet<T, I>, T>>
{
}
/// Extract finality proof info from the submitted header and justification.
pub(crate) fn submit_finality_proof_info_from_args<T: Config<I>, I: 'static>(
finality_target: &BridgedHeader<T, I>,
justification: &GrandpaJustification<BridgedHeader<T, I>>,
current_set_id: Option<SetId>,
is_free_execution_expected: bool,
) -> SubmitFinalityProofInfo<BridgedBlockNumber<T, I>> {
// check if call exceeds limits. In other words - whether some size or weight is included
// in the call
let extras =
submit_finality_proof_limits_extras::<T::BridgedChain>(finality_target, justification);
// We do care about extra weight because of more-than-expected headers in the votes
// ancestries. But we have problems computing extra weight for additional headers (weight of
// additional header is too small, so that our benchmarks aren't detecting that). So if there
// are more than expected headers in votes ancestries, we will treat the whole call weight
// as an extra weight.
let extra_weight = if extras.is_weight_limit_exceeded {
let precommits_len = justification.commit.precommits.len().saturated_into();
let votes_ancestries_len = justification.votes_ancestries.len().saturated_into();
T::WeightInfo::submit_finality_proof(precommits_len, votes_ancestries_len)
} else {
Weight::zero()
};
SubmitFinalityProofInfo {
block_number: *finality_target.number(),
current_set_id,
is_mandatory: extras.is_mandatory_finality_target,
is_free_execution_expected,
extra_weight,
extra_size: extras.extra_size,
}
}
#[cfg(test)]
mod tests {
use crate::{
call_ext::CallSubType,
mock::{
run_test, test_header, FreeHeadersInterval, RuntimeCall, TestBridgedChain, TestNumber,
TestRuntime,
},
BestFinalized, Config, CurrentAuthoritySet, FreeHeadersRemaining, PalletOperatingMode,
StoredAuthoritySet, WeightInfo,
};
use bp_header_pez_chain::{ChainWithGrandpa, SubmitFinalityProofInfo};
use pezbp_runtime::{BasicOperatingMode, HeaderId};
use bp_test_utils::{
make_default_justification, make_justification_for_header, JustificationGeneratorParams,
TEST_GRANDPA_SET_ID,
};
use codec::Encode;
use pezframe_support::weights::Weight;
use pezsp_runtime::{testing::DigestItem, traits::Header as _, SaturatedConversion};
fn validate_block_submit(num: TestNumber) -> bool {
let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
finality_target: Box::new(test_header(num)),
justification: make_default_justification(&test_header(num)),
// not initialized => zero
current_set_id: 0,
is_free_execution_expected: false,
};
RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call,
))
.is_ok()
}
fn sync_to_header_10() {
let header10_hash = pezsp_core::H256::default();
BestFinalized::<TestRuntime, ()>::put(HeaderId(10, header10_hash));
}
#[test]
fn extension_rejects_obsolete_header() {
run_test(|| {
// when current best finalized is #10 and we're trying to import header#5 => tx is
// rejected
sync_to_header_10();
assert!(!validate_block_submit(5));
});
}
#[test]
fn extension_rejects_same_header() {
run_test(|| {
// when current best finalized is #10 and we're trying to import header#10 => tx is
// rejected
sync_to_header_10();
assert!(!validate_block_submit(10));
});
}
#[test]
fn extension_rejects_new_header_if_pallet_is_halted() {
run_test(|| {
// when pezpallet is halted => tx is rejected
sync_to_header_10();
PalletOperatingMode::<TestRuntime, ()>::put(BasicOperatingMode::Halted);
assert!(!validate_block_submit(15));
});
}
#[test]
fn extension_rejects_new_header_if_set_id_is_invalid() {
run_test(|| {
// when set id is different from the passed one => tx is rejected
sync_to_header_10();
let next_set = StoredAuthoritySet::<TestRuntime, ()>::try_new(vec![], 0x42).unwrap();
CurrentAuthoritySet::<TestRuntime, ()>::put(next_set);
assert!(!validate_block_submit(15));
});
}
#[test]
fn extension_rejects_new_header_if_free_execution_is_requested_and_free_submissions_are_not_accepted(
) {
run_test(|| {
let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
finality_target: Box::new(test_header(10 + FreeHeadersInterval::get() as u64)),
justification: make_default_justification(&test_header(
10 + FreeHeadersInterval::get() as u64,
)),
current_set_id: 0,
is_free_execution_expected: true,
};
sync_to_header_10();
// when we can accept free headers => Ok
FreeHeadersRemaining::<TestRuntime, ()>::put(2);
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_ok());
// when we can NOT accept free headers => Err
FreeHeadersRemaining::<TestRuntime, ()>::put(0);
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_err());
// when called outside of transaction => Ok
FreeHeadersRemaining::<TestRuntime, ()>::kill();
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call,
),)
.is_ok());
})
}
#[test]
fn extension_rejects_new_header_if_it_overflow_size_limits() {
run_test(|| {
let mut large_finality_target = test_header(10 + FreeHeadersInterval::get() as u64);
large_finality_target
.digest_mut()
.push(DigestItem::Other(vec![42u8; 1024 * 1024]));
let justification_params = JustificationGeneratorParams {
header: large_finality_target.clone(),
..Default::default()
};
let large_justification = make_justification_for_header(justification_params);
let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
finality_target: Box::new(large_finality_target),
justification: large_justification,
current_set_id: 0,
is_free_execution_expected: true,
};
sync_to_header_10();
// if overflow size limits => Err
FreeHeadersRemaining::<TestRuntime, ()>::put(2);
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_err());
})
}
#[test]
fn extension_rejects_new_header_if_it_overflow_weight_limits() {
run_test(|| {
let finality_target = test_header(10 + FreeHeadersInterval::get() as u64);
let justification_params = JustificationGeneratorParams {
header: finality_target.clone(),
ancestors: TestBridgedChain::REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY,
..Default::default()
};
let justification = make_justification_for_header(justification_params);
let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
finality_target: Box::new(finality_target),
justification,
current_set_id: 0,
is_free_execution_expected: true,
};
sync_to_header_10();
// if overflow weight limits => Err
FreeHeadersRemaining::<TestRuntime, ()>::put(2);
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_err());
})
}
#[test]
fn extension_rejects_new_header_if_free_execution_is_requested_and_improved_by_is_below_expected(
) {
run_test(|| {
let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
finality_target: Box::new(test_header(100)),
justification: make_default_justification(&test_header(100)),
current_set_id: 0,
is_free_execution_expected: true,
};
sync_to_header_10();
// when `improved_by` is less than the free interval
BestFinalized::<TestRuntime, ()>::put(HeaderId(
100 - FreeHeadersInterval::get() as u64 + 1,
pezsp_core::H256::default(),
));
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_err());
// when `improved_by` is equal to the free interval
BestFinalized::<TestRuntime, ()>::put(HeaderId(
100 - FreeHeadersInterval::get() as u64,
pezsp_core::H256::default(),
));
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_ok());
// when `improved_by` is larger than the free interval
BestFinalized::<TestRuntime, ()>::put(HeaderId(
100 - FreeHeadersInterval::get() as u64 - 1,
pezsp_core::H256::default(),
));
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_ok());
// when `improved_by` is less than the free interval BUT it is a mandatory header
let mut mandatory_header = test_header(100);
let consensus_log = pezsp_consensus_grandpa::ConsensusLog::<TestNumber>::ScheduledChange(
pezsp_consensus_grandpa::ScheduledChange {
next_authorities: bp_test_utils::authority_list(),
delay: 0,
},
);
mandatory_header.digest = pezsp_runtime::Digest {
logs: vec![DigestItem::Consensus(
pezsp_consensus_grandpa::GRANDPA_ENGINE_ID,
consensus_log.encode(),
)],
};
let justification = make_justification_for_header(JustificationGeneratorParams {
header: mandatory_header.clone(),
set_id: 1,
..Default::default()
});
let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
finality_target: Box::new(mandatory_header),
justification,
current_set_id: 0,
is_free_execution_expected: true,
};
BestFinalized::<TestRuntime, ()>::put(HeaderId(
100 - FreeHeadersInterval::get() as u64 + 1,
pezsp_core::H256::default(),
));
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_ok());
})
}
#[test]
fn extension_accepts_new_header() {
run_test(|| {
// when current best finalized is #10 and we're trying to import header#15 => tx is
// accepted
sync_to_header_10();
assert!(validate_block_submit(15));
});
}
#[test]
fn submit_finality_proof_info_is_parsed() {
// when `submit_finality_proof` is used, `current_set_id` is set to `None`
let deprecated_call =
RuntimeCall::Grandpa(crate::Call::<TestRuntime, ()>::submit_finality_proof {
finality_target: Box::new(test_header(42)),
justification: make_default_justification(&test_header(42)),
});
assert_eq!(
deprecated_call.submit_finality_proof_info(),
Some(SubmitFinalityProofInfo {
block_number: 42,
current_set_id: None,
extra_weight: Weight::zero(),
extra_size: 0,
is_mandatory: false,
is_free_execution_expected: false,
})
);
// when `submit_finality_proof_ex` is used, `current_set_id` is set to `Some`
let deprecated_call =
RuntimeCall::Grandpa(crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
finality_target: Box::new(test_header(42)),
justification: make_default_justification(&test_header(42)),
current_set_id: 777,
is_free_execution_expected: false,
});
assert_eq!(
deprecated_call.submit_finality_proof_info(),
Some(SubmitFinalityProofInfo {
block_number: 42,
current_set_id: Some(777),
extra_weight: Weight::zero(),
extra_size: 0,
is_mandatory: false,
is_free_execution_expected: false,
})
);
}
#[test]
fn extension_returns_correct_extra_size_if_call_arguments_are_too_large() {
// when call arguments are below our limit => no refund
let small_finality_target = test_header(1);
let justification_params = JustificationGeneratorParams {
header: small_finality_target.clone(),
..Default::default()
};
let small_justification = make_justification_for_header(justification_params);
let small_call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof_ex {
finality_target: Box::new(small_finality_target),
justification: small_justification,
current_set_id: TEST_GRANDPA_SET_ID,
is_free_execution_expected: false,
});
assert_eq!(small_call.submit_finality_proof_info().unwrap().extra_size, 0);
// when call arguments are too large => partial refund
let mut large_finality_target = test_header(1);
large_finality_target
.digest_mut()
.push(DigestItem::Other(vec![42u8; 1024 * 1024]));
let justification_params = JustificationGeneratorParams {
header: large_finality_target.clone(),
..Default::default()
};
let large_justification = make_justification_for_header(justification_params);
let large_call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof_ex {
finality_target: Box::new(large_finality_target),
justification: large_justification,
current_set_id: TEST_GRANDPA_SET_ID,
is_free_execution_expected: false,
});
assert_ne!(large_call.submit_finality_proof_info().unwrap().extra_size, 0);
}
#[test]
fn extension_returns_correct_extra_weight_if_there_are_too_many_headers_in_votes_ancestry() {
let finality_target = test_header(1);
let mut justification_params = JustificationGeneratorParams {
header: finality_target.clone(),
ancestors: TestBridgedChain::REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY,
..Default::default()
};
// when there are `REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY` headers => no refund
let justification = make_justification_for_header(justification_params.clone());
let call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof_ex {
finality_target: Box::new(finality_target.clone()),
justification,
current_set_id: TEST_GRANDPA_SET_ID,
is_free_execution_expected: false,
});
assert_eq!(call.submit_finality_proof_info().unwrap().extra_weight, Weight::zero());
// when there are `REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY + 1` headers => full refund
justification_params.ancestors += 1;
let justification = make_justification_for_header(justification_params);
let call_weight = <TestRuntime as Config>::WeightInfo::submit_finality_proof(
justification.commit.precommits.len().saturated_into(),
justification.votes_ancestries.len().saturated_into(),
);
let call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof_ex {
finality_target: Box::new(finality_target),
justification,
current_set_id: TEST_GRANDPA_SET_ID,
is_free_execution_expected: false,
});
assert_eq!(call.submit_finality_proof_info().unwrap().extra_weight, call_weight);
}
#[test]
fn check_obsolete_submit_finality_proof_returns_correct_improved_by() {
run_test(|| {
fn make_call(number: u64) -> RuntimeCall {
RuntimeCall::Grandpa(crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
finality_target: Box::new(test_header(number)),
justification: make_default_justification(&test_header(number)),
current_set_id: 0,
is_free_execution_expected: false,
})
}
sync_to_header_10();
// when the difference between headers is 1
assert_eq!(
RuntimeCall::check_obsolete_submit_finality_proof(&make_call(11))
.unwrap()
.unwrap()
.improved_by,
1,
);
// when the difference between headers is 2
assert_eq!(
RuntimeCall::check_obsolete_submit_finality_proof(&make_call(12))
.unwrap()
.unwrap()
.improved_by,
2,
);
})
}
#[test]
fn check_obsolete_submit_finality_proof_ignores_other_calls() {
run_test(|| {
let call =
RuntimeCall::System(pezframe_system::Call::<TestRuntime>::remark { remark: vec![42] });
assert_eq!(RuntimeCall::check_obsolete_submit_finality_proof(&call), Ok(None));
})
}
}
File diff suppressed because it is too large Load Diff
+117
View File
@@ -0,0 +1,117 @@
// 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/>.
// From construct_runtime macro
#![allow(clippy::from_over_into)]
use bp_header_pez_chain::ChainWithGrandpa;
use pezbp_runtime::{Chain, ChainId};
use pezframe_support::{
construct_runtime, derive_impl, parameter_types, pezsp_runtime::StateVersion, traits::Hooks,
weights::Weight,
};
use pezsp_core::sr25519::Signature;
pub type AccountId = u64;
pub type TestHeader = pezsp_runtime::testing::Header;
pub type TestNumber = u64;
type Block = pezframe_system::mocking::MockBlock<TestRuntime>;
pub const MAX_BRIDGED_AUTHORITIES: u32 = 5;
use crate as grandpa;
construct_runtime! {
pub enum TestRuntime
{
System: pezframe_system::{Pezpallet, Call, Config<T>, Storage, Event<T>},
Grandpa: grandpa::{Pezpallet, Call, Event<T>},
}
}
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
impl pezframe_system::Config for TestRuntime {
type Block = Block;
}
parameter_types! {
pub const MaxFreeHeadersPerBlock: u32 = 2;
pub const FreeHeadersInterval: u32 = 32;
pub const HeadersToKeep: u32 = 5;
}
impl grandpa::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type BridgedChain = TestBridgedChain;
type MaxFreeHeadersPerBlock = MaxFreeHeadersPerBlock;
type FreeHeadersInterval = FreeHeadersInterval;
type HeadersToKeep = HeadersToKeep;
type WeightInfo = ();
}
#[derive(Debug)]
pub struct TestBridgedChain;
impl Chain for TestBridgedChain {
const ID: ChainId = *b"tbch";
type BlockNumber = pezframe_system::pezpallet_prelude::BlockNumberFor<TestRuntime>;
type Hash = <TestRuntime as pezframe_system::Config>::Hash;
type Hasher = <TestRuntime as pezframe_system::Config>::Hashing;
type Header = TestHeader;
type AccountId = AccountId;
type Balance = u64;
type Nonce = u64;
type Signature = Signature;
const STATE_VERSION: StateVersion = StateVersion::V1;
fn max_extrinsic_size() -> u32 {
unreachable!()
}
fn max_extrinsic_weight() -> Weight {
unreachable!()
}
}
impl ChainWithGrandpa for TestBridgedChain {
const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str = "";
const MAX_AUTHORITIES_COUNT: u32 = MAX_BRIDGED_AUTHORITIES;
const REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY: u32 = 8;
const MAX_MANDATORY_HEADER_SIZE: u32 = 256;
const AVERAGE_HEADER_SIZE: u32 = 64;
}
/// Return test externalities to use in tests.
pub fn new_test_ext() -> pezsp_io::TestExternalities {
pezsp_io::TestExternalities::new(Default::default())
}
/// Return test within default test externalities context.
pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
new_test_ext().execute_with(|| {
let _ = Grandpa::on_initialize(0);
test()
})
}
/// Return test header with given number.
pub fn test_header(num: TestNumber) -> TestHeader {
// We wrap the call to avoid explicit type annotations in our tests
bp_test_utils::test_header(num)
}
@@ -0,0 +1,136 @@
// 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/>.
//! Wrappers for public types that are implementing `MaxEncodedLen`
use crate::{Config, Error};
use bp_header_pez_chain::{AuthoritySet, ChainWithGrandpa};
use codec::{Decode, Encode, MaxEncodedLen};
use pezframe_support::{traits::Get, BoundedVec, CloneNoBound, RuntimeDebugNoBound};
use scale_info::TypeInfo;
use pezsp_consensus_grandpa::{AuthorityId, AuthorityList, AuthorityWeight, SetId};
use pezsp_std::marker::PhantomData;
/// A bounded list of Grandpa authorities with associated weights.
pub type StoredAuthorityList<MaxBridgedAuthorities> =
BoundedVec<(AuthorityId, AuthorityWeight), MaxBridgedAuthorities>;
/// Adapter for using `T::BridgedChain::MAX_BRIDGED_AUTHORITIES` in `BoundedVec`.
pub struct StoredAuthorityListLimit<T, I>(PhantomData<(T, I)>);
impl<T: Config<I>, I: 'static> Get<u32> for StoredAuthorityListLimit<T, I> {
fn get() -> u32 {
T::BridgedChain::MAX_AUTHORITIES_COUNT
}
}
/// A bounded GRANDPA Authority List and ID.
#[derive(CloneNoBound, Decode, Encode, Eq, TypeInfo, MaxEncodedLen, RuntimeDebugNoBound)]
#[scale_info(skip_type_params(T, I))]
pub struct StoredAuthoritySet<T: Config<I>, I: 'static> {
/// List of GRANDPA authorities for the current round.
pub authorities: StoredAuthorityList<StoredAuthorityListLimit<T, I>>,
/// Monotonic identifier of the current GRANDPA authority set.
pub set_id: SetId,
}
impl<T: Config<I>, I: 'static> StoredAuthoritySet<T, I> {
/// Try to create a new bounded GRANDPA Authority Set from unbounded list.
///
/// Returns error if number of authorities in the provided list is too large.
pub fn try_new(authorities: AuthorityList, set_id: SetId) -> Result<Self, Error<T, I>> {
Ok(Self {
authorities: TryFrom::try_from(authorities)
.map_err(|_| Error::TooManyAuthoritiesInSet)?,
set_id,
})
}
/// Returns number of bytes that may be subtracted from the PoV component of
/// `submit_finality_proof` call, because the actual authorities set is smaller than the maximal
/// configured.
///
/// Maximal authorities set size is configured by the `MaxBridgedAuthorities` constant from
/// the pezpallet configuration. The PoV of the call includes the size of maximal authorities
/// count. If the actual size is smaller, we may subtract extra bytes from this component.
pub fn unused_proof_size(&self) -> u64 {
// we can only safely estimate bytes that are occupied by the authority data itself. We have
// no means here to compute PoV bytes, occupied by extra trie nodes or extra bytes in the
// whole set encoding
let single_authority_max_encoded_len =
<(AuthorityId, AuthorityWeight)>::max_encoded_len() as u64;
let extra_authorities =
T::BridgedChain::MAX_AUTHORITIES_COUNT.saturating_sub(self.authorities.len() as _);
single_authority_max_encoded_len.saturating_mul(extra_authorities as u64)
}
}
impl<T: Config<I>, I: 'static> PartialEq for StoredAuthoritySet<T, I> {
fn eq(&self, other: &Self) -> bool {
self.set_id == other.set_id && self.authorities == other.authorities
}
}
impl<T: Config<I>, I: 'static> Default for StoredAuthoritySet<T, I> {
fn default() -> Self {
StoredAuthoritySet { authorities: BoundedVec::default(), set_id: 0 }
}
}
impl<T: Config<I>, I: 'static> From<StoredAuthoritySet<T, I>> for AuthoritySet {
fn from(t: StoredAuthoritySet<T, I>) -> Self {
AuthoritySet { authorities: t.authorities.into(), set_id: t.set_id }
}
}
#[cfg(test)]
mod tests {
use crate::mock::{TestRuntime, MAX_BRIDGED_AUTHORITIES};
use bp_test_utils::authority_list;
type StoredAuthoritySet = super::StoredAuthoritySet<TestRuntime, ()>;
#[test]
fn unused_proof_size_works() {
let authority_entry = authority_list().pop().unwrap();
// when we have exactly `MaxBridgedAuthorities` authorities
assert_eq!(
StoredAuthoritySet::try_new(
vec![authority_entry.clone(); MAX_BRIDGED_AUTHORITIES as usize],
0,
)
.unwrap()
.unused_proof_size(),
0,
);
// when we have less than `MaxBridgedAuthorities` authorities
assert_eq!(
StoredAuthoritySet::try_new(
vec![authority_entry; MAX_BRIDGED_AUTHORITIES as usize - 1],
0,
)
.unwrap()
.unused_proof_size(),
40,
);
// and we can't have more than `MaxBridgedAuthorities` authorities in the bounded vec, so
// no test for this case
}
}
+216
View File
@@ -0,0 +1,216 @@
// 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/>.
//! Autogenerated weights for pezpallet_bridge_grandpa
//!
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2023-03-02, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `covid`, CPU: `11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz`
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
// Executed Command:
// target/release/unknown-bridge-node
// benchmark
// pezpallet
// --chain=dev
// --steps=50
// --repeat=20
// --pezpallet=pezpallet_bridge_grandpa
// --extrinsic=*
// --execution=wasm
// --wasm-execution=Compiled
// --heap-pages=4096
// --output=./modules/grandpa/src/weights.rs
// --template=./.maintain/bridge-weight-template.hbs
#![allow(clippy::all)]
#![allow(unused_parens)]
#![allow(unused_imports)]
#![allow(missing_docs)]
use pezframe_support::{
traits::Get,
weights::{constants::RocksDbWeight, Weight},
};
use pezsp_std::marker::PhantomData;
/// Weight functions needed for pezpallet_bridge_grandpa.
pub trait WeightInfo {
fn submit_finality_proof(p: u32, v: u32) -> Weight;
fn force_set_pallet_state() -> Weight;
}
/// Weights for `pezpallet_bridge_grandpa` that are generated using one of the Bridge testnets.
///
/// Those weights are test only and must never be used in production.
pub struct BridgeWeight<T>(PhantomData<T>);
impl<T: pezframe_system::Config> WeightInfo for BridgeWeight<T> {
/// Storage: BridgeUnknownGrandpa PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa PalletOperatingMode (max_values: Some(1), max_size: Some(1),
/// added: 496, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa RequestCount (r:1 w:1)
///
/// Proof: BridgeUnknownGrandpa RequestCount (max_values: Some(1), max_size: Some(4), added:
/// 499, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa BestFinalized (r:1 w:1)
///
/// Proof: BridgeUnknownGrandpa BestFinalized (max_values: Some(1), max_size: Some(36), added:
/// 531, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa CurrentAuthoritySet (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa CurrentAuthoritySet (max_values: Some(1), max_size: Some(209),
/// added: 704, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHashesPointer (r:1 w:1)
///
/// Proof: BridgeUnknownGrandpa ImportedHashesPointer (max_values: Some(1), max_size: Some(4),
/// added: 499, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHashes (r:1 w:1)
///
/// Proof: BridgeUnknownGrandpa ImportedHashes (max_values: Some(14400), max_size: Some(36),
/// added: 2016, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:0 w:2)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// The range of component `p` is `[1, 4]`.
///
/// The range of component `v` is `[50, 100]`.
fn submit_finality_proof(p: u32, v: u32) -> Weight {
// Proof Size summary in bytes:
// Measured: `394 + p * (60 ±0)`
// Estimated: `4745`
// Minimum execution time: 228_072 nanoseconds.
Weight::from_parts(57_853_228, 4745)
// Standard Error: 149_421
.saturating_add(Weight::from_parts(36_708_702, 0).saturating_mul(p.into()))
// Standard Error: 10_625
.saturating_add(Weight::from_parts(1_469_032, 0).saturating_mul(v.into()))
.saturating_add(T::DbWeight::get().reads(6_u64))
.saturating_add(T::DbWeight::get().writes(6_u64))
}
/// Storage: `BridgeZagrosGrandpa::CurrentAuthoritySet` (r:1 w:1)
/// Proof: `BridgeZagrosGrandpa::CurrentAuthoritySet` (`max_values`: Some(1), `max_size`:
/// Some(50250), added: 50745, mode: `MaxEncodedLen`)
/// Storage: `BridgeZagrosGrandpa::ImportedHashesPointer` (r:1 w:1)
/// Proof: `BridgeZagrosGrandpa::ImportedHashesPointer` (`max_values`: Some(1), `max_size`:
/// Some(4), added: 499, mode: `MaxEncodedLen`) Storage: `BridgeZagrosGrandpa::ImportedHashes`
/// (r:1 w:1) Proof: `BridgeZagrosGrandpa::ImportedHashes` (`max_values`: Some(1024),
/// `max_size`: Some(36), added: 1521, mode: `MaxEncodedLen`)
/// Storage: `BridgeZagrosGrandpa::BestFinalized` (r:0 w:1)
/// Proof: `BridgeZagrosGrandpa::BestFinalized` (`max_values`: Some(1), `max_size`: Some(36),
/// added: 531, mode: `MaxEncodedLen`) Storage: `BridgeZagrosGrandpa::ImportedHeaders` (r:0
/// w:2) Proof: `BridgeZagrosGrandpa::ImportedHeaders` (`max_values`: Some(1024), `max_size`:
/// Some(68), added: 1553, mode: `MaxEncodedLen`)
fn force_set_pallet_state() -> Weight {
// Proof Size summary in bytes:
// Measured: `452`
// Estimated: `51735`
// Minimum execution time: 62_232_000 picoseconds.
Weight::from_parts(78_755_000, 0)
.saturating_add(Weight::from_parts(0, 51735))
.saturating_add(RocksDbWeight::get().reads(3))
.saturating_add(RocksDbWeight::get().writes(6))
}
}
// For backwards compatibility and tests
impl WeightInfo for () {
/// Storage: BridgeUnknownGrandpa PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa PalletOperatingMode (max_values: Some(1), max_size: Some(1),
/// added: 496, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa RequestCount (r:1 w:1)
///
/// Proof: BridgeUnknownGrandpa RequestCount (max_values: Some(1), max_size: Some(4), added:
/// 499, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa BestFinalized (r:1 w:1)
///
/// Proof: BridgeUnknownGrandpa BestFinalized (max_values: Some(1), max_size: Some(36), added:
/// 531, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa CurrentAuthoritySet (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa CurrentAuthoritySet (max_values: Some(1), max_size: Some(209),
/// added: 704, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHashesPointer (r:1 w:1)
///
/// Proof: BridgeUnknownGrandpa ImportedHashesPointer (max_values: Some(1), max_size: Some(4),
/// added: 499, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHashes (r:1 w:1)
///
/// Proof: BridgeUnknownGrandpa ImportedHashes (max_values: Some(14400), max_size: Some(36),
/// added: 2016, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:0 w:2)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// The range of component `p` is `[1, 4]`.
///
/// The range of component `v` is `[50, 100]`.
fn submit_finality_proof(p: u32, v: u32) -> Weight {
// Proof Size summary in bytes:
// Measured: `394 + p * (60 ±0)`
// Estimated: `4745`
// Minimum execution time: 228_072 nanoseconds.
Weight::from_parts(57_853_228, 4745)
// Standard Error: 149_421
.saturating_add(Weight::from_parts(36_708_702, 0).saturating_mul(p.into()))
// Standard Error: 10_625
.saturating_add(Weight::from_parts(1_469_032, 0).saturating_mul(v.into()))
.saturating_add(RocksDbWeight::get().reads(6_u64))
.saturating_add(RocksDbWeight::get().writes(6_u64))
}
/// Storage: `BridgeZagrosGrandpa::CurrentAuthoritySet` (r:1 w:1)
/// Proof: `BridgeZagrosGrandpa::CurrentAuthoritySet` (`max_values`: Some(1), `max_size`:
/// Some(50250), added: 50745, mode: `MaxEncodedLen`)
/// Storage: `BridgeZagrosGrandpa::ImportedHashesPointer` (r:1 w:1)
/// Proof: `BridgeZagrosGrandpa::ImportedHashesPointer` (`max_values`: Some(1), `max_size`:
/// Some(4), added: 499, mode: `MaxEncodedLen`) Storage: `BridgeZagrosGrandpa::ImportedHashes`
/// (r:1 w:1) Proof: `BridgeZagrosGrandpa::ImportedHashes` (`max_values`: Some(1024),
/// `max_size`: Some(36), added: 1521, mode: `MaxEncodedLen`)
/// Storage: `BridgeZagrosGrandpa::BestFinalized` (r:0 w:1)
/// Proof: `BridgeZagrosGrandpa::BestFinalized` (`max_values`: Some(1), `max_size`: Some(36),
/// added: 531, mode: `MaxEncodedLen`) Storage: `BridgeZagrosGrandpa::ImportedHeaders` (r:0
/// w:2) Proof: `BridgeZagrosGrandpa::ImportedHeaders` (`max_values`: Some(1024), `max_size`:
/// Some(68), added: 1553, mode: `MaxEncodedLen`)
fn force_set_pallet_state() -> Weight {
// Proof Size summary in bytes:
// Measured: `452`
// Estimated: `51735`
// Minimum execution time: 62_232_000 picoseconds.
Weight::from_parts(78_755_000, 0)
.saturating_add(Weight::from_parts(0, 51735))
.saturating_add(RocksDbWeight::get().reads(3))
.saturating_add(RocksDbWeight::get().writes(6))
}
}
@@ -0,0 +1,58 @@
// 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/>.
//! Weight-related utilities.
use crate::weights::{BridgeWeight, WeightInfo};
use pezframe_support::weights::Weight;
/// Extended weight info.
pub trait WeightInfoExt: WeightInfo {
// Our configuration assumes that the runtime has special signed extensions used to:
//
// 1) boost priority of `submit_finality_proof` transactions;
//
// 2) slash relayer if he submits an invalid transaction.
//
// We read and update storage values of other pallets (`pezpallet-bridge-relayers` and
// balances/assets pezpallet). So we need to add this weight to the weight of our call.
// Hence two following methods.
/// Extra weight that is added to the `submit_finality_proof` call weight by signed extensions
/// that are declared at runtime level.
fn submit_finality_proof_overhead_from_runtime() -> Weight;
// Functions that are directly mapped to extrinsics weights.
/// Weight of message delivery extrinsic.
fn submit_finality_proof_weight(precommits_len: u32, votes_ancestries_len: u32) -> Weight {
let base_weight = Self::submit_finality_proof(precommits_len, votes_ancestries_len);
base_weight.saturating_add(Self::submit_finality_proof_overhead_from_runtime())
}
}
impl<T: pezframe_system::Config> WeightInfoExt for BridgeWeight<T> {
fn submit_finality_proof_overhead_from_runtime() -> Weight {
Weight::zero()
}
}
impl WeightInfoExt for () {
fn submit_finality_proof_overhead_from_runtime() -> Weight {
Weight::zero()
}
}
+84
View File
@@ -0,0 +1,84 @@
[package]
name = "pezpallet-bridge-messages"
description = "Module that allows bridged chains to exchange messages using lane concept."
version = "0.7.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
repository.workspace = true
documentation = "https://docs.rs/pezpallet-bridge-messages"
homepage = { workspace = true }
[lints]
workspace = true
[dependencies]
codec = { workspace = true }
scale-info = { features = ["derive"], workspace = true }
tracing = { workspace = true }
# Bridge dependencies
bp-header-pez-chain = { workspace = true }
bp-messages = { workspace = true }
pezbp-runtime = { workspace = true }
# Bizinikiwi Dependencies
pezframe-benchmarking = { optional = true, workspace = true }
pezframe-support = { workspace = true }
pezframe-system = { workspace = true }
pezsp-runtime = { workspace = true }
pezsp-std = { workspace = true }
pezsp-trie = { optional = true, workspace = true }
[dev-dependencies]
pezbp-runtime = { features = ["test-helpers"], workspace = true }
bp-test-utils = { workspace = true }
pezpallet-balances = { workspace = true }
pezpallet-bridge-grandpa = { workspace = true }
pezsp-core = { workspace = true }
pezsp-io = { workspace = true }
[features]
default = ["std"]
std = [
"bp-header-pez-chain/std",
"bp-messages/std",
"pezbp-runtime/std",
"bp-test-utils/std",
"codec/std",
"pezframe-benchmarking/std",
"pezframe-support/std",
"pezframe-system/std",
"pezpallet-balances/std",
"pezpallet-bridge-grandpa/std",
"scale-info/std",
"pezsp-core/std",
"pezsp-io/std",
"pezsp-runtime/std",
"pezsp-std/std",
"pezsp-trie/std",
"tracing/std",
]
runtime-benchmarks = [
"bp-header-pez-chain/runtime-benchmarks",
"bp-messages/runtime-benchmarks",
"pezbp-runtime/runtime-benchmarks",
"pezbp-runtime/test-helpers",
"bp-test-utils/runtime-benchmarks",
"pezframe-benchmarking/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezpallet-balances/runtime-benchmarks",
"pezpallet-bridge-grandpa/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"pezsp-trie?/runtime-benchmarks",
]
try-runtime = [
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezpallet-balances/try-runtime",
"pezpallet-bridge-grandpa/try-runtime",
"pezsp-runtime/try-runtime",
]
test-helpers = ["pezbp-runtime/test-helpers", "pezsp-trie"]
+202
View File
@@ -0,0 +1,202 @@
# Bridge Messages Pezpallet
The messages pezpallet is used to deliver messages from source chain to target chain. Message is (almost) opaque to the
module and the final goal is to hand message to the message dispatch mechanism.
## Contents
- [Overview](#overview)
- [Message Workflow](#message-workflow)
- [Integrating Message Lane Module into Runtime](#integrating-messages-module-into-runtime)
- [Non-Essential Functionality](#non-essential-functionality)
- [Weights of Module Extrinsics](#weights-of-module-extrinsics)
## Overview
Message lane is a unidirectional channel, where messages are sent from source chain to the target chain. At the same
time, a single instance of messages module supports both outbound lanes and inbound lanes. So the chain where the module
is deployed (this chain), may act as a source chain for outbound messages (heading to a bridged chain) and as a target
chain for inbound messages (coming from a bridged chain).
Messages module supports multiple message lanes. Every message lane is identified with a 4-byte identifier. Messages
sent through the lane are assigned unique (for this lane) increasing integer value that is known as nonce ("number that
can only be used once"). Messages that are sent over the same lane are guaranteed to be delivered to the target chain in
the same order they're sent from the source chain. In other words, message with nonce `N` will be delivered right before
delivering a message with nonce `N+1`.
Single message lane may be seen as a transport channel for single application (onchain, offchain or mixed). At the same
time the module itself never dictates any lane or message rules. In the end, it is the runtime developer who defines
what message lane and message mean for this runtime.
In our [Kusama<>PezkuwiChain bridge](../../docs/pezkuwi-kusama-bridge-overview.md) we are using lane
as a channel of communication between two teyrchains of different relay chains. For example, lane
`[0, 0, 0, 0]` is used for PezkuwiChain <> Kusama Asset Hub communications. Other lanes may be used to
bridge other teyrchains.
## Message Workflow
The pezpallet is not intended to be used by end users and provides no public calls to send the message. Instead, it
provides runtime-internal method that allows other pallets (or other runtime code) to queue outbound messages.
The message "appears" when some runtime code calls the `send_message()` method of the pezpallet. The submitter specifies
the lane that they're willing to use and the message itself. If some fee must be paid for sending the message, it must
be paid outside of the pezpallet. If a message passes all checks (that include, for example, message size check, disabled
lane check, ...), the nonce is assigned and the message is stored in the module storage. The message is in an
"undelivered" state now.
We assume that there are external, offchain actors, called relayers, that are submitting module related transactions to
both target and source chains. The pezpallet itself has no assumptions about relayers incentivization scheme, but it has
some callbacks for paying rewards. See [Integrating Messages Module into
runtime](#Integrating-Messages-Module-into-runtime) for details.
Eventually, some relayer would notice this message in the "undelivered" state and it would decide to deliver this
message. Relayer then crafts `receive_messages_proof()` transaction (aka delivery transaction) for the messages module
instance, deployed at the target chain. Relayer provides its account id at the source chain, the proof of message (or
several messages), the number of messages in the transaction and their cumulative dispatch weight. Once a transaction is
mined, the message is considered "delivered".
Once a message is delivered, the relayer may want to confirm delivery back to the source chain. There are two reasons
why it would want to do that. The first is that we intentionally limit number of "delivered", but not yet "confirmed"
messages at inbound lanes (see [What about other Constants in the Messages Module Configuration
Trait](#What-about-other-Constants-in-the-Messages-Module-Configuration-Trait) for explanation). So at some point, the
target chain may stop accepting new messages until relayers confirm some of these. The second is that if the relayer
wants to be rewarded for delivery, it must prove the fact that it has actually delivered the message. And this proof may
only be generated after the delivery transaction is mined. So relayer crafts the `receive_messages_delivery_proof()`
transaction (aka confirmation transaction) for the messages module instance, deployed at the source chain. Once this
transaction is mined, the message is considered "confirmed".
The "confirmed" state is the final state of the message. But there's one last thing related to the message - the fact
that it is now "confirmed" and reward has been paid to the relayer (or at least callback for this has been called), must
be confirmed to the target chain. Otherwise, we may reach the limit of "unconfirmed" messages at the target chain and it
will stop accepting new messages. So relayer sometimes includes a nonce of the latest "confirmed" message in the next
`receive_messages_proof()` transaction, proving that some messages have been confirmed.
## Integrating Messages Module into Runtime
As it has been said above, the messages module supports both outbound and inbound message lanes. So if we will integrate
a module in some runtime, it may act as the source chain runtime for outbound messages and as the target chain runtime
for inbound messages. In this section, we'll sometimes refer to the chain we're currently integrating with, as "this
chain" and the other chain as "bridged chain".
Messages module doesn't simply accept transactions that are claiming that the bridged chain has some updated data for
us. Instead of this, the module assumes that the bridged chain is able to prove that updated data in some way. The proof
is abstracted from the module and may be of any kind. In our Bizinikiwi-to-Bizinikiwi bridge we're using runtime storage
proofs. Other bridges may use transaction proofs, Bizinikiwi header digests or anything else that may be proved.
**IMPORTANT NOTE**: everything below in this chapter describes details of the messages module configuration. But if
you're interested in well-probed and relatively easy integration of two Bizinikiwi-based chains, you may want to look at
the [pezbridge-runtime-common](../../bin/runtime-common/) crate. This crate is providing a lot of helpers for integration,
which may be directly used from within your runtime. Then if you'll decide to change something in this scheme, get back
here for detailed information.
### General Information
The messages module supports instances. Every module instance is supposed to bridge this chain and some bridged chain.
To bridge with another chain, using another instance is suggested (this isn't forced anywhere in the code, though). Keep
in mind, that the pezpallet may be used to build virtual channels between multiple chains, as we do in our [PezkuwiChain <>
Kusama bridge](../../docs/pezkuwi-kusama-bridge-overview.md). There, the pezpallet actually bridges only two teyrchains -
Kusama Bridge Hub and PezkuwiChain Bridge Hub. However, other Kusama and PezkuwiChain teyrchains are able to send (XCM) messages
to their Bridge Hubs. The messages will be delivered to the other side of the bridge and routed to the proper
destination teyrchain within the bridged chain consensus.
Message submitters may track message progress by inspecting module events. When Message is accepted, the
`MessageAccepted` event is emitted. The event contains both message lane identifier and nonce that has been assigned to
the message. When a message is delivered to the target chain, the `MessagesDelivered` event is emitted from the
`receive_messages_delivery_proof()` transaction. The `MessagesDelivered` contains the message lane identifier and
inclusive range of delivered message nonces.
The pezpallet provides no means to get the result of message dispatch at the target chain. If that is
required, it must be done outside of the pezpallet. For example, XCM messages, when dispatched, have
special instructions to send some data back to the sender. Other dispatchers may use similar
mechanism for that.
### How to plug-in Messages Module to Send and Receive Messages from the Bridged Chain?
The `pallet_bridge_messages::Config` trait has 2 main associated types that are used to work with
inbound messages. The `pallet_bridge_messages::BridgedChain` defines basic primitives of the bridged
chain. The `pallet_bridge_messages::BridgedHeaderChain` defines the way we access the bridged chain
headers in our runtime. You may use `pallet_bridge_grandpa` if you're bridging with chain that uses
GRANDPA finality or `pallet_bridge_teyrchains::TeyrchainHeaders` if you're bridging with teyrchain.
The `pallet_bridge_messages::Config::MessageDispatch` defines a way on how to dispatch delivered
messages. Apart from actually dispatching the message, the implementation must return the correct
dispatch weight of the message before dispatch is called.
The last type is the `pallet_bridge_messages::Config::DeliveryConfirmationPayments`. When confirmation
transaction is received, we call the `pay_reward()` method, passing the range of delivered messages.
You may use the [`pezpallet-bridge-relayers`](../relayers/) pezpallet and its
[`DeliveryConfirmationPaymentsAdapter`](../relayers/src/payment_adapter.rs) adapter as a possible
implementation. It allows you to pay fixed reward for relaying the message and some of its portion
for confirming delivery.
### I have a Messages Module in my Runtime, but I Want to Reject all Outbound Messages. What shall I do?
You should be looking at the `bp_messages::source_chain::ForbidOutboundMessages` structure
[`bp_messages::source_chain`](../../primitives/messages/src/source_chain.rs). It implements all required traits and will
simply reject all transactions, related to outbound messages.
### I have a Messages Module in my Runtime, but I Want to Reject all Inbound Messages. What shall I do?
You should be looking at the `bp_messages::target_chain::ForbidInboundMessages` structure from the
[`bp_messages::target_chain`](../../primitives/messages/src/target_chain.rs) module. It implements all required traits
and will simply reject all transactions, related to inbound messages.
### What about other Constants in the Messages Module Configuration Trait?
`pallet_bridge_messages::Config::MaximalOutboundPayloadSize` constant defines the maximal size
of outbound message that may be sent. If the message size is above this limit, the message is
rejected.
To be able to reward the relayer for delivering messages, we store a map of message nonces range =>
identifier of the relayer that has delivered this range at the target chain runtime storage. If a
relayer delivers multiple consequent ranges, they're merged into single entry. So there may be more
than one entry for the same relayer. Eventually, this whole map must be delivered back to the source
chain to confirm delivery and pay rewards. So to make sure we are able to craft this confirmation
transaction, we need to: (1) keep the size of this map below a certain limit and (2) make sure that
the weight of processing this map is below a certain limit. Both size and processing weight mostly
depend on the number of entries. The number of entries is limited with the
`pallet_bridge_messages::Config::BridgedChain::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX` parameter.
Processing weight also depends on the total number of messages that are being confirmed, because every
confirmed message needs to be read. So there's another
`pallet_bridge_messages::Config::BridgedChain::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX` parameter
for that.
When choosing values for these parameters, you must also keep in mind that if proof in your scheme
is based on finality of headers (and it is the most obvious option for Bizinikiwi-based chains with
finality notion), then choosing too small values for these parameters may cause significant delays
in message delivery. That's because there are too many actors involved in this scheme: 1) authorities
that are finalizing headers of the target chain need to finalize header with non-empty map; 2) the
headers relayer then needs to submit this header and its finality proof to the source chain; 3) the
messages relayer must then send confirmation transaction (storage proof of this map) to the source
chain; 4) when the confirmation transaction will be mined at some header, source chain authorities
must finalize this header; 5) the headers relay then needs to submit this header and its finality
proof to the target chain; 6) only now the messages relayer may submit new messages from the source
to target chain and prune the entry from the map.
Delivery transaction requires the relayer to provide both number of entries and total number of
messages in the map. This means that the module never charges an extra cost for delivering a map -
the relayer would need to pay exactly for the number of entries+messages it has delivered. So the
best guess for values of these parameters would be the pair that would occupy `N` percent of the
maximal transaction size and weight of the source chain. The `N` should be large enough to process
large maps, at the same time keeping reserve for future source chain upgrades.
## Non-Essential Functionality
There may be a special account in every runtime where the messages module is deployed. This account, named 'module
owner', is like a module-level sudo account - he's able to halt and resume all module operations without requiring
runtime upgrade. Calls that are related to this account are:
- `fn set_owner()`: current module owner may call it to transfer "ownership" to another account;
- `fn set_operating_mode()`: the module owner (or sudo account) may call this function to pause/resume
pezpallet operations. Owner may halt the pezpallet by calling this method with
`MessagesOperatingMode::Basic(BasicOperatingMode::Halted)` argument - all message-related
transactions will be rejected. Owner may then resume pezpallet operations by passing the
`MessagesOperatingMode::Basic(BasicOperatingMode::Normal)` argument. There's also
`MessagesOperatingMode::RejectingOutboundMessages` pezpallet mode, where it still accepts all incoming
messages, but all outbound messages are rejected.
If pezpallet owner is not defined, the governance may be used to make those calls.
## Messages Relay
We have an offchain actor, who is watching for new messages and submits them to the bridged chain. It is the messages
relay - you may look at the [crate level documentation and the code](../../relays/messages/).
@@ -0,0 +1,552 @@
// 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/>.
//! Messages pezpallet benchmarking.
#![cfg(feature = "runtime-benchmarks")]
use crate::{
active_outbound_lane, weights_ext::EXPECTED_DEFAULT_MESSAGE_LENGTH, BridgedChainOf, Call,
InboundLanes, OutboundLanes,
};
use bp_messages::{
source_chain::FromBridgedChainMessagesDeliveryProof,
target_chain::FromBridgedChainMessagesProof, ChainWithMessages, DeliveredMessages,
InboundLaneData, LaneState, MessageNonce, OutboundLaneData, UnrewardedRelayer,
UnrewardedRelayersState,
};
use pezbp_runtime::{AccountIdOf, HashOf, UnverifiedStorageProofParams};
use codec::Decode;
use pezframe_benchmarking::{account, v2::*};
use pezframe_support::weights::Weight;
use pezframe_system::RawOrigin;
use pezsp_runtime::{traits::TrailingZeroInput, BoundedVec};
use pezsp_std::{ops::RangeInclusive, prelude::*};
const SEED: u32 = 0;
/// Pezpallet we're benchmarking here.
pub struct Pezpallet<T: Config<I>, I: 'static = ()>(crate::Pezpallet<T, I>);
/// Benchmark-specific message proof parameters.
#[derive(Debug)]
pub struct MessageProofParams<LaneId> {
/// Id of the lane.
pub lane: LaneId,
/// Range of messages to include in the proof.
pub message_nonces: RangeInclusive<MessageNonce>,
/// If `Some`, the proof needs to include this outbound lane data.
pub outbound_lane_data: Option<OutboundLaneData>,
/// If `true`, the caller expects that the proof will contain correct messages that will
/// be successfully dispatched. This is only called from the "optional"
/// `receive_single_message_proof_with_dispatch` benchmark. If you don't need it, just
/// return `true` from the `is_message_successfully_dispatched`.
pub is_successful_dispatch_expected: bool,
/// Proof size requirements.
pub proof_params: UnverifiedStorageProofParams,
}
/// Benchmark-specific message delivery proof parameters.
#[derive(Debug)]
pub struct MessageDeliveryProofParams<ThisChainAccountId, LaneId> {
/// Id of the lane.
pub lane: LaneId,
/// The proof needs to include this inbound lane data.
pub inbound_lane_data: InboundLaneData<ThisChainAccountId>,
/// Proof size requirements.
pub proof_params: UnverifiedStorageProofParams,
}
/// Trait that must be implemented by runtime.
pub trait Config<I: 'static>: crate::Config<I> {
/// Lane id to use in benchmarks.
fn bench_lane_id() -> Self::LaneId {
Self::LaneId::default()
}
/// Return id of relayer account at the bridged chain.
///
/// By default, zero account is returned.
fn bridged_relayer_id() -> AccountIdOf<BridgedChainOf<Self, I>> {
Decode::decode(&mut TrailingZeroInput::zeroes()).unwrap()
}
/// Create given account and give it enough balance for test purposes. Used to create
/// relayer account at the target chain. Is strictly necessary when your rewards scheme
/// assumes that the relayer account must exist.
///
/// Does nothing by default.
fn endow_account(_account: &Self::AccountId) {}
/// Prepare messages proof to receive by the module.
fn prepare_message_proof(
params: MessageProofParams<Self::LaneId>,
) -> (FromBridgedChainMessagesProof<HashOf<BridgedChainOf<Self, I>>, Self::LaneId>, Weight);
/// Prepare messages delivery proof to receive by the module.
fn prepare_message_delivery_proof(
params: MessageDeliveryProofParams<Self::AccountId, Self::LaneId>,
) -> FromBridgedChainMessagesDeliveryProof<HashOf<BridgedChainOf<Self, I>>, Self::LaneId>;
/// Returns true if message has been successfully dispatched or not.
fn is_message_successfully_dispatched(_nonce: MessageNonce) -> bool {
true
}
/// Returns true if given relayer has been rewarded for some of its actions.
fn is_relayer_rewarded(relayer: &Self::AccountId) -> bool;
}
fn send_regular_message<T: Config<I>, I: 'static>() {
OutboundLanes::<T, I>::insert(
T::bench_lane_id(),
OutboundLaneData {
state: LaneState::Opened,
latest_generated_nonce: 1,
..Default::default()
},
);
let mut outbound_lane = active_outbound_lane::<T, I>(T::bench_lane_id()).unwrap();
outbound_lane.send_message(BoundedVec::try_from(vec![]).expect("We craft valid messages"));
}
fn receive_messages<T: Config<I>, I: 'static>(nonce: MessageNonce) {
InboundLanes::<T, I>::insert(
T::bench_lane_id(),
InboundLaneData {
state: LaneState::Opened,
relayers: vec![UnrewardedRelayer {
relayer: T::bridged_relayer_id(),
messages: DeliveredMessages::new(nonce),
}]
.into(),
last_confirmed_nonce: 0,
},
);
}
struct ReceiveMessagesProofSetup<T: Config<I>, I: 'static> {
relayer_id_on_src: AccountIdOf<BridgedChainOf<T, I>>,
relayer_id_on_tgt: T::AccountId,
msgs_count: u32,
_phantom_data: pezsp_std::marker::PhantomData<I>,
}
impl<T: Config<I>, I: 'static> ReceiveMessagesProofSetup<T, I> {
const LATEST_RECEIVED_NONCE: MessageNonce = 20;
fn new(msgs_count: u32) -> Self {
let setup = Self {
relayer_id_on_src: T::bridged_relayer_id(),
relayer_id_on_tgt: account("relayer", 0, SEED),
msgs_count,
_phantom_data: Default::default(),
};
T::endow_account(&setup.relayer_id_on_tgt);
// mark messages 1..=latest_recvd_nonce as delivered
receive_messages::<T, I>(Self::LATEST_RECEIVED_NONCE);
setup
}
fn relayer_id_on_src(&self) -> AccountIdOf<BridgedChainOf<T, I>> {
self.relayer_id_on_src.clone()
}
fn relayer_id_on_tgt(&self) -> T::AccountId {
self.relayer_id_on_tgt.clone()
}
fn last_nonce(&self) -> MessageNonce {
Self::LATEST_RECEIVED_NONCE + self.msgs_count as u64
}
fn nonces(&self) -> RangeInclusive<MessageNonce> {
(Self::LATEST_RECEIVED_NONCE + 1)..=self.last_nonce()
}
fn check_last_nonce(&self) {
assert_eq!(
crate::InboundLanes::<T, I>::get(&T::bench_lane_id()).map(|d| d.last_delivered_nonce()),
Some(self.last_nonce()),
);
}
}
#[instance_benchmarks]
mod benchmarks {
use super::*;
//
// Benchmarks that are used directly by the runtime calls weight formulae.
//
fn max_msgs<T: Config<I>, I: 'static>() -> u32 {
T::BridgedChain::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX as u32 -
ReceiveMessagesProofSetup::<T, I>::LATEST_RECEIVED_NONCE as u32
}
// Benchmark `receive_messages_proof` extrinsic with single minimal-weight message and following
// conditions:
// * proof does not include outbound lane state proof;
// * inbound lane already has state, so it needs to be read and decoded;
// * message is dispatched (reminder: dispatch weight should be minimal);
// * message requires all heavy checks done by dispatcher.
#[benchmark]
fn receive_single_message_proof() {
// setup code
let setup = ReceiveMessagesProofSetup::<T, I>::new(1);
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
lane: T::bench_lane_id(),
message_nonces: setup.nonces(),
outbound_lane_data: None,
is_successful_dispatch_expected: false,
proof_params: UnverifiedStorageProofParams::from_db_size(
EXPECTED_DEFAULT_MESSAGE_LENGTH,
),
});
#[extrinsic_call]
receive_messages_proof(
RawOrigin::Signed(setup.relayer_id_on_tgt()),
setup.relayer_id_on_src(),
Box::new(proof),
setup.msgs_count,
dispatch_weight,
);
// verification code
setup.check_last_nonce();
}
// Benchmark `receive_messages_proof` extrinsic with `n` minimal-weight messages and following
// conditions:
// * proof does not include outbound lane state proof;
// * inbound lane already has state, so it needs to be read and decoded;
// * message is dispatched (reminder: dispatch weight should be minimal);
// * message requires all heavy checks done by dispatcher.
#[benchmark]
fn receive_n_messages_proof(n: Linear<1, { max_msgs::<T, I>() }>) {
// setup code
let setup = ReceiveMessagesProofSetup::<T, I>::new(n);
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
lane: T::bench_lane_id(),
message_nonces: setup.nonces(),
outbound_lane_data: None,
is_successful_dispatch_expected: false,
proof_params: UnverifiedStorageProofParams::from_db_size(
EXPECTED_DEFAULT_MESSAGE_LENGTH,
),
});
#[extrinsic_call]
receive_messages_proof(
RawOrigin::Signed(setup.relayer_id_on_tgt()),
setup.relayer_id_on_src(),
Box::new(proof),
setup.msgs_count,
dispatch_weight,
);
// verification code
setup.check_last_nonce();
}
// Benchmark `receive_messages_proof` extrinsic with single minimal-weight message and following
// conditions:
// * proof includes outbound lane state proof;
// * inbound lane already has state, so it needs to be read and decoded;
// * message is successfully dispatched (reminder: dispatch weight should be minimal);
// * message requires all heavy checks done by dispatcher.
//
// The weight of outbound lane state delivery would be
// `weight(receive_single_message_proof_with_outbound_lane_state) -
// weight(receive_single_message_proof)`. This won't be super-accurate if message has non-zero
// dispatch weight, but estimation should be close enough to real weight.
#[benchmark]
fn receive_single_message_proof_with_outbound_lane_state() {
// setup code
let setup = ReceiveMessagesProofSetup::<T, I>::new(1);
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
lane: T::bench_lane_id(),
message_nonces: setup.nonces(),
outbound_lane_data: Some(OutboundLaneData {
state: LaneState::Opened,
oldest_unpruned_nonce: setup.last_nonce(),
latest_received_nonce: ReceiveMessagesProofSetup::<T, I>::LATEST_RECEIVED_NONCE,
latest_generated_nonce: setup.last_nonce(),
}),
is_successful_dispatch_expected: false,
proof_params: UnverifiedStorageProofParams::from_db_size(
EXPECTED_DEFAULT_MESSAGE_LENGTH,
),
});
#[extrinsic_call]
receive_messages_proof(
RawOrigin::Signed(setup.relayer_id_on_tgt()),
setup.relayer_id_on_src(),
Box::new(proof),
setup.msgs_count,
dispatch_weight,
);
// verification code
setup.check_last_nonce();
}
// Benchmark `receive_messages_proof` extrinsic with single minimal-weight message and following
// conditions:
// * the proof has large leaf with total size ranging between 1KB and 16KB;
// * proof does not include outbound lane state proof;
// * inbound lane already has state, so it needs to be read and decoded;
// * message is dispatched (reminder: dispatch weight should be minimal);
// * message requires all heavy checks done by dispatcher.
#[benchmark]
fn receive_single_n_bytes_message_proof(
/// Proof size in KB
n: Linear<1, { 16 * 1024 }>,
) {
// setup code
let setup = ReceiveMessagesProofSetup::<T, I>::new(1);
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
lane: T::bench_lane_id(),
message_nonces: setup.nonces(),
outbound_lane_data: None,
is_successful_dispatch_expected: false,
proof_params: UnverifiedStorageProofParams::from_db_size(n),
});
#[extrinsic_call]
receive_messages_proof(
RawOrigin::Signed(setup.relayer_id_on_tgt()),
setup.relayer_id_on_src(),
Box::new(proof),
setup.msgs_count,
dispatch_weight,
);
// verification code
setup.check_last_nonce();
}
// Benchmark `receive_messages_delivery_proof` extrinsic with following conditions:
// * single relayer is rewarded for relaying single message;
// * relayer account does not exist (in practice it needs to exist in production environment).
//
// This is base benchmark for all other confirmations delivery benchmarks.
#[benchmark]
fn receive_delivery_proof_for_single_message() {
let relayer_id: T::AccountId = account("relayer", 0, SEED);
// send message that we're going to confirm
send_regular_message::<T, I>();
let relayers_state = UnrewardedRelayersState {
unrewarded_relayer_entries: 1,
messages_in_oldest_entry: 1,
total_messages: 1,
last_delivered_nonce: 1,
};
let proof = T::prepare_message_delivery_proof(MessageDeliveryProofParams {
lane: T::bench_lane_id(),
inbound_lane_data: InboundLaneData {
state: LaneState::Opened,
relayers: vec![UnrewardedRelayer {
relayer: relayer_id.clone(),
messages: DeliveredMessages::new(1),
}]
.into_iter()
.collect(),
last_confirmed_nonce: 0,
},
proof_params: UnverifiedStorageProofParams::default(),
});
#[extrinsic_call]
receive_messages_delivery_proof(
RawOrigin::Signed(relayer_id.clone()),
proof,
relayers_state,
);
assert_eq!(
OutboundLanes::<T, I>::get(T::bench_lane_id()).map(|s| s.latest_received_nonce),
Some(1)
);
assert!(T::is_relayer_rewarded(&relayer_id));
}
// Benchmark `receive_messages_delivery_proof` extrinsic with following conditions:
// * single relayer is rewarded for relaying two messages;
// * relayer account does not exist (in practice it needs to exist in production environment).
//
// Additional weight for paying single-message reward to the same relayer could be computed
// as `weight(receive_delivery_proof_for_two_messages_by_single_relayer)
// - weight(receive_delivery_proof_for_single_message)`.
#[benchmark]
fn receive_delivery_proof_for_two_messages_by_single_relayer() {
let relayer_id: T::AccountId = account("relayer", 0, SEED);
// send message that we're going to confirm
send_regular_message::<T, I>();
send_regular_message::<T, I>();
let relayers_state = UnrewardedRelayersState {
unrewarded_relayer_entries: 1,
messages_in_oldest_entry: 2,
total_messages: 2,
last_delivered_nonce: 2,
};
let mut delivered_messages = DeliveredMessages::new(1);
delivered_messages.note_dispatched_message();
let proof = T::prepare_message_delivery_proof(MessageDeliveryProofParams {
lane: T::bench_lane_id(),
inbound_lane_data: InboundLaneData {
state: LaneState::Opened,
relayers: vec![UnrewardedRelayer {
relayer: relayer_id.clone(),
messages: delivered_messages,
}]
.into_iter()
.collect(),
last_confirmed_nonce: 0,
},
proof_params: UnverifiedStorageProofParams::default(),
});
#[extrinsic_call]
receive_messages_delivery_proof(
RawOrigin::Signed(relayer_id.clone()),
proof,
relayers_state,
);
assert_eq!(
OutboundLanes::<T, I>::get(T::bench_lane_id()).map(|s| s.latest_received_nonce),
Some(2)
);
assert!(T::is_relayer_rewarded(&relayer_id));
}
// Benchmark `receive_messages_delivery_proof` extrinsic with following conditions:
// * two relayers are rewarded for relaying single message each;
// * relayer account does not exist (in practice it needs to exist in production environment).
//
// Additional weight for paying reward to the next relayer could be computed
// as `weight(receive_delivery_proof_for_two_messages_by_two_relayers)
// - weight(receive_delivery_proof_for_two_messages_by_single_relayer)`.
#[benchmark]
fn receive_delivery_proof_for_two_messages_by_two_relayers() {
let relayer1_id: T::AccountId = account("relayer1", 1, SEED);
let relayer2_id: T::AccountId = account("relayer2", 2, SEED);
// send message that we're going to confirm
send_regular_message::<T, I>();
send_regular_message::<T, I>();
let relayers_state = UnrewardedRelayersState {
unrewarded_relayer_entries: 2,
messages_in_oldest_entry: 1,
total_messages: 2,
last_delivered_nonce: 2,
};
let proof = T::prepare_message_delivery_proof(MessageDeliveryProofParams {
lane: T::bench_lane_id(),
inbound_lane_data: InboundLaneData {
state: LaneState::Opened,
relayers: vec![
UnrewardedRelayer {
relayer: relayer1_id.clone(),
messages: DeliveredMessages::new(1),
},
UnrewardedRelayer {
relayer: relayer2_id.clone(),
messages: DeliveredMessages::new(2),
},
]
.into_iter()
.collect(),
last_confirmed_nonce: 0,
},
proof_params: UnverifiedStorageProofParams::default(),
});
#[extrinsic_call]
receive_messages_delivery_proof(
RawOrigin::Signed(relayer1_id.clone()),
proof,
relayers_state,
);
assert_eq!(
OutboundLanes::<T, I>::get(T::bench_lane_id()).map(|s| s.latest_received_nonce),
Some(2)
);
assert!(T::is_relayer_rewarded(&relayer1_id));
assert!(T::is_relayer_rewarded(&relayer2_id));
}
//
// Benchmarks that the runtime developers may use for proper pezpallet configuration.
//
// This benchmark is optional and may be used when runtime developer need a way to compute
// message dispatch weight. In this case, he needs to provide messages that can go the whole
// dispatch
//
// Benchmark `receive_messages_proof` extrinsic with single message and following conditions:
//
// * proof does not include outbound lane state proof;
// * inbound lane already has state, so it needs to be read and decoded;
// * message is **SUCCESSFULLY** dispatched;
// * message requires all heavy checks done by dispatcher.
#[benchmark]
fn receive_single_n_bytes_message_proof_with_dispatch(
/// Proof size in KB
n: Linear<1, { 16 * 1024 }>,
) {
// setup code
let setup = ReceiveMessagesProofSetup::<T, I>::new(1);
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
lane: T::bench_lane_id(),
message_nonces: setup.nonces(),
outbound_lane_data: None,
is_successful_dispatch_expected: true,
proof_params: UnverifiedStorageProofParams::from_db_size(n),
});
#[extrinsic_call]
receive_messages_proof(
RawOrigin::Signed(setup.relayer_id_on_tgt()),
setup.relayer_id_on_src(),
Box::new(proof),
setup.msgs_count,
dispatch_weight,
);
// verification code
setup.check_last_nonce();
assert!(T::is_message_successfully_dispatched(setup.last_nonce()));
}
impl_benchmark_test_suite!(
Pezpallet,
crate::tests::mock::new_test_ext(),
crate::tests::mock::TestRuntime
);
}
+576
View File
@@ -0,0 +1,576 @@
// 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/>.
//! Helpers for easier manipulation of call processing with signed extensions.
use crate::{BridgedChainOf, Config, InboundLanes, OutboundLanes, Pezpallet, LOG_TARGET};
use bp_messages::{
target_chain::MessageDispatch, BaseMessagesProofInfo, ChainWithMessages, InboundLaneData,
MessageNonce, MessagesCallInfo, ReceiveMessagesDeliveryProofInfo, ReceiveMessagesProofInfo,
UnrewardedRelayerOccupation,
};
use pezbp_runtime::{AccountIdOf, OwnedBridgeModule};
use pezframe_support::{dispatch::CallableCallFor, traits::IsSubType};
use pezsp_runtime::transaction_validity::TransactionValidity;
/// Helper struct that provides methods for working with a call supported by `MessagesCallInfo`.
pub struct CallHelper<T: Config<I>, I: 'static> {
_phantom_data: pezsp_std::marker::PhantomData<(T, I)>,
}
impl<T: Config<I>, I: 'static> CallHelper<T, I> {
/// Returns true if:
///
/// - call is `receive_messages_proof` and all messages have been delivered;
///
/// - call is `receive_messages_delivery_proof` and all messages confirmations have been
/// received.
pub fn was_successful(info: &MessagesCallInfo<T::LaneId>) -> bool {
match info {
MessagesCallInfo::ReceiveMessagesProof(info) => {
let inbound_lane_data = match InboundLanes::<T, I>::get(info.base.lane_id) {
Some(inbound_lane_data) => inbound_lane_data,
None => return false,
};
if info.base.bundled_range.is_empty() {
let post_occupation =
unrewarded_relayers_occupation::<T, I>(&inbound_lane_data);
// we don't care about `free_relayer_slots` here - it is checked in
// `is_obsolete` and every relayer has delivered at least one message,
// so if relayer slots are released, then message slots are also
// released
return post_occupation.free_message_slots >
info.unrewarded_relayers.free_message_slots;
}
inbound_lane_data.last_delivered_nonce() == *info.base.bundled_range.end()
},
MessagesCallInfo::ReceiveMessagesDeliveryProof(info) => {
let outbound_lane_data = match OutboundLanes::<T, I>::get(info.0.lane_id) {
Some(outbound_lane_data) => outbound_lane_data,
None => return false,
};
outbound_lane_data.latest_received_nonce == *info.0.bundled_range.end()
},
}
}
}
/// Trait representing a call that is a sub type of `pezpallet_bridge_messages::Call`.
pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
IsSubType<CallableCallFor<Pezpallet<T, I>, T>>
{
/// Create a new instance of `ReceiveMessagesProofInfo` from a `ReceiveMessagesProof` call.
fn receive_messages_proof_info(&self) -> Option<ReceiveMessagesProofInfo<T::LaneId>>;
/// Create a new instance of `ReceiveMessagesDeliveryProofInfo` from
/// a `ReceiveMessagesDeliveryProof` call.
fn receive_messages_delivery_proof_info(
&self,
) -> Option<ReceiveMessagesDeliveryProofInfo<T::LaneId>>;
/// Create a new instance of `MessagesCallInfo` from a `ReceiveMessagesProof`
/// or a `ReceiveMessagesDeliveryProof` call.
fn call_info(&self) -> Option<MessagesCallInfo<T::LaneId>>;
/// Create a new instance of `MessagesCallInfo` from a `ReceiveMessagesProof`
/// or a `ReceiveMessagesDeliveryProof` call, if the call is for the provided lane.
fn call_info_for(&self, lane_id: T::LaneId) -> Option<MessagesCallInfo<T::LaneId>>;
/// Ensures that a `ReceiveMessagesProof` or a `ReceiveMessagesDeliveryProof` call:
///
/// - does not deliver already delivered messages. We require all messages in the
/// `ReceiveMessagesProof` call to be undelivered;
///
/// - does not submit empty `ReceiveMessagesProof` call with zero messages, unless the lane
/// needs to be unblocked by providing relayer rewards proof;
///
/// - brings no new delivery confirmations in a `ReceiveMessagesDeliveryProof` call. We require
/// at least one new delivery confirmation in the unrewarded relayers set;
///
/// - does not violate some basic (easy verifiable) messages pezpallet rules obsolete (like
/// submitting a call when a pezpallet is halted or delivering messages when a dispatcher is
/// inactive).
///
/// If one of above rules is violated, the transaction is treated as invalid.
fn check_obsolete_call(&self) -> TransactionValidity;
}
impl<
Call: IsSubType<CallableCallFor<Pezpallet<T, I>, T>>,
T: pezframe_system::Config<RuntimeCall = Call> + Config<I>,
I: 'static,
> CallSubType<T, I> for T::RuntimeCall
{
fn receive_messages_proof_info(&self) -> Option<ReceiveMessagesProofInfo<T::LaneId>> {
if let Some(crate::Call::<T, I>::receive_messages_proof { ref proof, .. }) =
self.is_sub_type()
{
let inbound_lane_data = InboundLanes::<T, I>::get(proof.lane)?;
return Some(ReceiveMessagesProofInfo {
base: BaseMessagesProofInfo {
lane_id: proof.lane,
// we want all messages in this range to be new for us. Otherwise transaction
// will be considered obsolete.
bundled_range: proof.nonces_start..=proof.nonces_end,
best_stored_nonce: inbound_lane_data.last_delivered_nonce(),
},
unrewarded_relayers: unrewarded_relayers_occupation::<T, I>(&inbound_lane_data),
});
}
None
}
fn receive_messages_delivery_proof_info(
&self,
) -> Option<ReceiveMessagesDeliveryProofInfo<T::LaneId>> {
if let Some(crate::Call::<T, I>::receive_messages_delivery_proof {
ref proof,
ref relayers_state,
..
}) = self.is_sub_type()
{
let outbound_lane_data = OutboundLanes::<T, I>::get(proof.lane)?;
return Some(ReceiveMessagesDeliveryProofInfo(BaseMessagesProofInfo {
lane_id: proof.lane,
// there's a time frame between message delivery, message confirmation and reward
// confirmation. Because of that, we can't assume that our state has been confirmed
// to the bridged chain. So we are accepting any proof that brings new
// confirmations.
bundled_range: outbound_lane_data.latest_received_nonce + 1..=
relayers_state.last_delivered_nonce,
best_stored_nonce: outbound_lane_data.latest_received_nonce,
}));
}
None
}
fn call_info(&self) -> Option<MessagesCallInfo<T::LaneId>> {
if let Some(info) = self.receive_messages_proof_info() {
return Some(MessagesCallInfo::ReceiveMessagesProof(info));
}
if let Some(info) = self.receive_messages_delivery_proof_info() {
return Some(MessagesCallInfo::ReceiveMessagesDeliveryProof(info));
}
None
}
fn call_info_for(&self, lane_id: T::LaneId) -> Option<MessagesCallInfo<T::LaneId>> {
self.call_info().filter(|info| {
let actual_lane_id = match info {
MessagesCallInfo::ReceiveMessagesProof(info) => info.base.lane_id,
MessagesCallInfo::ReceiveMessagesDeliveryProof(info) => info.0.lane_id,
};
actual_lane_id == lane_id
})
}
fn check_obsolete_call(&self) -> TransactionValidity {
let is_pallet_halted = Pezpallet::<T, I>::ensure_not_halted().is_err();
match self.call_info() {
Some(proof_info) if is_pallet_halted => {
tracing::trace!(
target: LOG_TARGET,
?proof_info,
"Rejecting messages transaction on halted pezpallet"
);
return pezsp_runtime::transaction_validity::InvalidTransaction::Call.into();
},
Some(MessagesCallInfo::ReceiveMessagesProof(proof_info))
if proof_info
.is_obsolete(T::MessageDispatch::is_active(proof_info.base.lane_id)) =>
{
tracing::trace!(
target: LOG_TARGET,
?proof_info,
"Rejecting obsolete messages delivery transaction"
);
return pezsp_runtime::transaction_validity::InvalidTransaction::Stale.into();
},
Some(MessagesCallInfo::ReceiveMessagesDeliveryProof(proof_info))
if proof_info.is_obsolete() =>
{
tracing::trace!(
target: LOG_TARGET,
?proof_info,
"Rejecting obsolete messages confirmation transaction"
);
return pezsp_runtime::transaction_validity::InvalidTransaction::Stale.into();
},
_ => {},
}
Ok(pezsp_runtime::transaction_validity::ValidTransaction::default())
}
}
/// Returns occupation state of unrewarded relayers vector.
fn unrewarded_relayers_occupation<T: Config<I>, I: 'static>(
inbound_lane_data: &InboundLaneData<AccountIdOf<BridgedChainOf<T, I>>>,
) -> UnrewardedRelayerOccupation {
UnrewardedRelayerOccupation {
free_relayer_slots: T::BridgedChain::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX
.saturating_sub(inbound_lane_data.relayers.len() as MessageNonce),
free_message_slots: {
let unconfirmed_messages = inbound_lane_data
.last_delivered_nonce()
.saturating_sub(inbound_lane_data.last_confirmed_nonce);
T::BridgedChain::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX
.saturating_sub(unconfirmed_messages)
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::mock::*;
use bp_messages::{
source_chain::FromBridgedChainMessagesDeliveryProof,
target_chain::FromBridgedChainMessagesProof, DeliveredMessages, InboundLaneData, LaneState,
OutboundLaneData, UnrewardedRelayer, UnrewardedRelayersState,
};
use pezsp_std::ops::RangeInclusive;
fn fill_unrewarded_relayers() {
let mut inbound_lane_state = InboundLanes::<TestRuntime>::get(test_lane_id()).unwrap();
for n in 0..BridgedChain::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX {
inbound_lane_state.relayers.push_back(UnrewardedRelayer {
relayer: Default::default(),
messages: DeliveredMessages { begin: n + 1, end: n + 1 },
});
}
InboundLanes::<TestRuntime>::insert(test_lane_id(), inbound_lane_state);
}
fn fill_unrewarded_messages() {
let mut inbound_lane_state = InboundLanes::<TestRuntime>::get(test_lane_id()).unwrap();
inbound_lane_state.relayers.push_back(UnrewardedRelayer {
relayer: Default::default(),
messages: DeliveredMessages {
begin: 1,
end: BridgedChain::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX,
},
});
InboundLanes::<TestRuntime>::insert(test_lane_id(), inbound_lane_state);
}
fn deliver_message_10() {
InboundLanes::<TestRuntime>::insert(
test_lane_id(),
bp_messages::InboundLaneData {
state: LaneState::Opened,
relayers: Default::default(),
last_confirmed_nonce: 10,
},
);
}
fn validate_message_delivery(
nonces_start: bp_messages::MessageNonce,
nonces_end: bp_messages::MessageNonce,
) -> bool {
RuntimeCall::Messages(crate::Call::<TestRuntime, ()>::receive_messages_proof {
relayer_id_at_bridged_chain: 42,
messages_count: nonces_end.checked_sub(nonces_start).map(|x| x + 1).unwrap_or(0) as u32,
dispatch_weight: pezframe_support::weights::Weight::zero(),
proof: Box::new(FromBridgedChainMessagesProof {
bridged_header_hash: Default::default(),
storage_proof: Default::default(),
lane: test_lane_id(),
nonces_start,
nonces_end,
}),
})
.check_obsolete_call()
.is_ok()
}
fn run_test<T>(test: impl Fn() -> T) -> T {
pezsp_io::TestExternalities::new(Default::default()).execute_with(|| {
InboundLanes::<TestRuntime>::insert(test_lane_id(), InboundLaneData::opened());
OutboundLanes::<TestRuntime>::insert(test_lane_id(), OutboundLaneData::opened());
test()
})
}
#[test]
fn extension_rejects_obsolete_messages() {
run_test(|| {
// when current best delivered is message#10 and we're trying to deliver messages 8..=9
// => tx is rejected
deliver_message_10();
assert!(!validate_message_delivery(8, 9));
});
}
#[test]
fn extension_rejects_same_message() {
run_test(|| {
// when current best delivered is message#10 and we're trying to import messages 10..=10
// => tx is rejected
deliver_message_10();
assert!(!validate_message_delivery(8, 10));
});
}
#[test]
fn extension_rejects_call_with_some_obsolete_messages() {
run_test(|| {
// when current best delivered is message#10 and we're trying to deliver messages
// 10..=15 => tx is rejected
deliver_message_10();
assert!(!validate_message_delivery(10, 15));
});
}
#[test]
fn extension_rejects_call_with_future_messages() {
run_test(|| {
// when current best delivered is message#10 and we're trying to deliver messages
// 13..=15 => tx is rejected
deliver_message_10();
assert!(!validate_message_delivery(13, 15));
});
}
#[test]
fn extension_reject_call_when_dispatcher_is_inactive() {
run_test(|| {
// when current best delivered is message#10 and we're trying to deliver message 11..=15
// => tx is accepted, but we have inactive dispatcher, so...
deliver_message_10();
TestMessageDispatch::deactivate(test_lane_id());
assert!(!validate_message_delivery(11, 15));
});
}
#[test]
fn extension_rejects_empty_delivery_with_rewards_confirmations_if_there_are_free_relayer_and_message_slots(
) {
run_test(|| {
deliver_message_10();
assert!(!validate_message_delivery(10, 9));
});
}
#[test]
fn extension_accepts_empty_delivery_with_rewards_confirmations_if_there_are_no_free_relayer_slots(
) {
run_test(|| {
deliver_message_10();
fill_unrewarded_relayers();
assert!(validate_message_delivery(10, 9));
});
}
#[test]
fn extension_accepts_empty_delivery_with_rewards_confirmations_if_there_are_no_free_message_slots(
) {
run_test(|| {
fill_unrewarded_messages();
assert!(validate_message_delivery(
BridgedChain::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX,
BridgedChain::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX - 1
));
});
}
#[test]
fn extension_accepts_new_messages() {
run_test(|| {
// when current best delivered is message#10 and we're trying to deliver message 11..=15
// => tx is accepted
deliver_message_10();
assert!(validate_message_delivery(11, 15));
});
}
fn confirm_message_10() {
OutboundLanes::<TestRuntime>::insert(
test_lane_id(),
bp_messages::OutboundLaneData {
state: LaneState::Opened,
oldest_unpruned_nonce: 0,
latest_received_nonce: 10,
latest_generated_nonce: 10,
},
);
}
fn validate_message_confirmation(last_delivered_nonce: bp_messages::MessageNonce) -> bool {
RuntimeCall::Messages(crate::Call::<TestRuntime>::receive_messages_delivery_proof {
proof: FromBridgedChainMessagesDeliveryProof {
bridged_header_hash: Default::default(),
storage_proof: Default::default(),
lane: test_lane_id(),
},
relayers_state: UnrewardedRelayersState { last_delivered_nonce, ..Default::default() },
})
.check_obsolete_call()
.is_ok()
}
#[test]
fn extension_rejects_obsolete_confirmations() {
run_test(|| {
// when current best confirmed is message#10 and we're trying to confirm message#5 => tx
// is rejected
confirm_message_10();
assert!(!validate_message_confirmation(5));
});
}
#[test]
fn extension_rejects_same_confirmation() {
run_test(|| {
// when current best confirmed is message#10 and we're trying to confirm message#10 =>
// tx is rejected
confirm_message_10();
assert!(!validate_message_confirmation(10));
});
}
#[test]
fn extension_rejects_empty_confirmation_even_if_there_are_no_free_unrewarded_entries() {
run_test(|| {
confirm_message_10();
fill_unrewarded_relayers();
assert!(!validate_message_confirmation(10));
});
}
#[test]
fn extension_accepts_new_confirmation() {
run_test(|| {
// when current best confirmed is message#10 and we're trying to confirm message#15 =>
// tx is accepted
confirm_message_10();
assert!(validate_message_confirmation(15));
});
}
fn was_message_delivery_successful(
bundled_range: RangeInclusive<MessageNonce>,
is_empty: bool,
) -> bool {
CallHelper::<TestRuntime, ()>::was_successful(&MessagesCallInfo::ReceiveMessagesProof(
ReceiveMessagesProofInfo {
base: BaseMessagesProofInfo {
lane_id: test_lane_id(),
bundled_range,
best_stored_nonce: 0, // doesn't matter for `was_successful`
},
unrewarded_relayers: UnrewardedRelayerOccupation {
free_relayer_slots: 0, // doesn't matter for `was_successful`
free_message_slots: if is_empty {
0
} else {
BridgedChain::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX
},
},
},
))
}
#[test]
#[allow(clippy::reversed_empty_ranges)]
fn was_successful_returns_false_for_failed_reward_confirmation_transaction() {
run_test(|| {
fill_unrewarded_messages();
assert!(!was_message_delivery_successful(10..=9, true));
});
}
#[test]
#[allow(clippy::reversed_empty_ranges)]
fn was_successful_returns_true_for_successful_reward_confirmation_transaction() {
run_test(|| {
assert!(was_message_delivery_successful(10..=9, true));
});
}
#[test]
fn was_successful_returns_false_for_failed_delivery() {
run_test(|| {
deliver_message_10();
assert!(!was_message_delivery_successful(10..=12, false));
});
}
#[test]
fn was_successful_returns_false_for_partially_successful_delivery() {
run_test(|| {
deliver_message_10();
assert!(!was_message_delivery_successful(9..=12, false));
});
}
#[test]
fn was_successful_returns_true_for_successful_delivery() {
run_test(|| {
deliver_message_10();
assert!(was_message_delivery_successful(9..=10, false));
});
}
fn was_message_confirmation_successful(bundled_range: RangeInclusive<MessageNonce>) -> bool {
CallHelper::<TestRuntime, ()>::was_successful(
&MessagesCallInfo::ReceiveMessagesDeliveryProof(ReceiveMessagesDeliveryProofInfo(
BaseMessagesProofInfo {
lane_id: test_lane_id(),
bundled_range,
best_stored_nonce: 0, // doesn't matter for `was_successful`
},
)),
)
}
#[test]
fn was_successful_returns_false_for_failed_confirmation() {
run_test(|| {
confirm_message_10();
assert!(!was_message_confirmation_successful(10..=12));
});
}
#[test]
fn was_successful_returns_false_for_partially_successful_confirmation() {
run_test(|| {
confirm_message_10();
assert!(!was_message_confirmation_successful(9..=12));
});
}
#[test]
fn was_successful_returns_true_for_successful_confirmation() {
run_test(|| {
confirm_message_10();
assert!(was_message_confirmation_successful(9..=10));
});
}
}
@@ -0,0 +1,570 @@
// 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/>.
//! Everything about incoming messages receival.
use crate::{BridgedChainOf, Config};
use bp_messages::{
target_chain::{DispatchMessage, DispatchMessageData, MessageDispatch},
ChainWithMessages, DeliveredMessages, InboundLaneData, LaneState, MessageKey, MessageNonce,
OutboundLaneData, ReceptionResult, UnrewardedRelayer,
};
use pezbp_runtime::AccountIdOf;
use codec::{Decode, Encode, EncodeLike, MaxEncodedLen};
use scale_info::{Type, TypeInfo};
use pezsp_runtime::RuntimeDebug;
use pezsp_std::prelude::PartialEq;
/// Inbound lane storage.
pub trait InboundLaneStorage {
/// Id of relayer on source chain.
type Relayer: Clone + PartialEq;
/// Lane identifier type.
type LaneId: Encode;
/// Lane id.
fn id(&self) -> Self::LaneId;
/// Return maximal number of unrewarded relayer entries in inbound lane.
fn max_unrewarded_relayer_entries(&self) -> MessageNonce;
/// Return maximal number of unconfirmed messages in inbound lane.
fn max_unconfirmed_messages(&self) -> MessageNonce;
/// Get lane data from the storage.
fn data(&self) -> InboundLaneData<Self::Relayer>;
/// Update lane data in the storage.
fn set_data(&mut self, data: InboundLaneData<Self::Relayer>);
/// Purge lane data from the storage.
fn purge(self);
}
/// Inbound lane data wrapper that implements `MaxEncodedLen`.
///
/// We have already had `MaxEncodedLen`-like functionality before, but its usage has
/// been localized and we haven't been passing bounds (maximal count of unrewarded relayer entries,
/// maximal count of unconfirmed messages) everywhere. This wrapper allows us to avoid passing
/// these generic bounds all over the code.
///
/// The encoding of this type matches encoding of the corresponding `MessageData`.
#[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, Eq)]
pub struct StoredInboundLaneData<T: Config<I>, I: 'static>(
pub InboundLaneData<AccountIdOf<BridgedChainOf<T, I>>>,
);
impl<T: Config<I>, I: 'static> pezsp_std::ops::Deref for StoredInboundLaneData<T, I> {
type Target = InboundLaneData<AccountIdOf<BridgedChainOf<T, I>>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: Config<I>, I: 'static> pezsp_std::ops::DerefMut for StoredInboundLaneData<T, I> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T: Config<I>, I: 'static> Default for StoredInboundLaneData<T, I> {
fn default() -> Self {
StoredInboundLaneData(Default::default())
}
}
impl<T: Config<I>, I: 'static> From<StoredInboundLaneData<T, I>>
for InboundLaneData<AccountIdOf<BridgedChainOf<T, I>>>
{
fn from(data: StoredInboundLaneData<T, I>) -> Self {
data.0
}
}
impl<T: Config<I>, I: 'static> EncodeLike<StoredInboundLaneData<T, I>>
for InboundLaneData<AccountIdOf<BridgedChainOf<T, I>>>
{
}
impl<T: Config<I>, I: 'static> TypeInfo for StoredInboundLaneData<T, I> {
type Identity = Self;
fn type_info() -> Type {
InboundLaneData::<AccountIdOf<BridgedChainOf<T, I>>>::type_info()
}
}
impl<T: Config<I>, I: 'static> MaxEncodedLen for StoredInboundLaneData<T, I> {
fn max_encoded_len() -> usize {
InboundLaneData::<AccountIdOf<BridgedChainOf<T, I>>>::encoded_size_hint(
BridgedChainOf::<T, I>::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX as usize,
)
.unwrap_or(usize::MAX)
}
}
/// Inbound messages lane.
pub struct InboundLane<S> {
storage: S,
}
impl<S: InboundLaneStorage> InboundLane<S> {
/// Create new inbound lane backed by given storage.
pub fn new(storage: S) -> Self {
InboundLane { storage }
}
/// Get lane state.
pub fn state(&self) -> LaneState {
self.storage.data().state
}
/// Returns storage reference.
pub fn storage(&self) -> &S {
&self.storage
}
/// Set lane state.
pub fn set_state(&mut self, state: LaneState) {
let mut data = self.storage.data();
data.state = state;
self.storage.set_data(data);
}
/// Receive state of the corresponding outbound lane.
pub fn receive_state_update(
&mut self,
outbound_lane_data: OutboundLaneData,
) -> Option<MessageNonce> {
let mut data = self.storage.data();
let last_delivered_nonce = data.last_delivered_nonce();
if outbound_lane_data.latest_received_nonce > last_delivered_nonce {
// this is something that should never happen if proofs are correct
return None;
}
if outbound_lane_data.latest_received_nonce <= data.last_confirmed_nonce {
return None;
}
let new_confirmed_nonce = outbound_lane_data.latest_received_nonce;
data.last_confirmed_nonce = new_confirmed_nonce;
// Firstly, remove all of the records where higher nonce <= new confirmed nonce
while data
.relayers
.front()
.map(|entry| entry.messages.end <= new_confirmed_nonce)
.unwrap_or(false)
{
data.relayers.pop_front();
}
// Secondly, update the next record with lower nonce equal to new confirmed nonce if needed.
// Note: There will be max. 1 record to update as we don't allow messages from relayers to
// overlap.
match data.relayers.front_mut() {
Some(entry) if entry.messages.begin <= new_confirmed_nonce => {
entry.messages.begin = new_confirmed_nonce + 1;
},
_ => {},
}
self.storage.set_data(data);
Some(outbound_lane_data.latest_received_nonce)
}
/// Receive new message.
pub fn receive_message<Dispatch: MessageDispatch<LaneId = S::LaneId>>(
&mut self,
relayer_at_bridged_chain: &S::Relayer,
nonce: MessageNonce,
message_data: DispatchMessageData<Dispatch::DispatchPayload>,
) -> ReceptionResult<Dispatch::DispatchLevelResult> {
let mut data = self.storage.data();
if Some(nonce) != data.last_delivered_nonce().checked_add(1) {
return ReceptionResult::InvalidNonce;
}
// if there are more unrewarded relayer entries than we may accept, reject this message
if data.relayers.len() as MessageNonce >= self.storage.max_unrewarded_relayer_entries() {
return ReceptionResult::TooManyUnrewardedRelayers;
}
// if there are more unconfirmed messages than we may accept, reject this message
let unconfirmed_messages_count = nonce.saturating_sub(data.last_confirmed_nonce);
if unconfirmed_messages_count > self.storage.max_unconfirmed_messages() {
return ReceptionResult::TooManyUnconfirmedMessages;
}
// then, dispatch message
let dispatch_result = Dispatch::dispatch(DispatchMessage {
key: MessageKey { lane_id: self.storage.id(), nonce },
data: message_data,
});
// now let's update inbound lane storage
match data.relayers.back_mut() {
Some(entry) if entry.relayer == *relayer_at_bridged_chain => {
entry.messages.note_dispatched_message();
},
_ => {
data.relayers.push_back(UnrewardedRelayer {
relayer: relayer_at_bridged_chain.clone(),
messages: DeliveredMessages::new(nonce),
});
},
};
self.storage.set_data(data);
ReceptionResult::Dispatched(dispatch_result)
}
/// Purge lane state from the storage.
pub fn purge(self) {
self.storage.purge()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{active_inbound_lane, lanes_manager::RuntimeInboundLaneStorage, tests::mock::*};
use bp_messages::UnrewardedRelayersState;
fn receive_regular_message(
lane: &mut InboundLane<RuntimeInboundLaneStorage<TestRuntime, ()>>,
nonce: MessageNonce,
) {
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&TEST_RELAYER_A,
nonce,
inbound_message_data(REGULAR_PAYLOAD)
),
ReceptionResult::Dispatched(dispatch_result(0))
);
}
#[test]
fn receive_status_update_ignores_status_from_the_future() {
run_test(|| {
let mut lane = active_inbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
receive_regular_message(&mut lane, 1);
assert_eq!(
lane.receive_state_update(OutboundLaneData {
latest_received_nonce: 10,
..Default::default()
}),
None,
);
assert_eq!(lane.storage.data().last_confirmed_nonce, 0);
});
}
#[test]
fn receive_status_update_ignores_obsolete_status() {
run_test(|| {
let mut lane = active_inbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
receive_regular_message(&mut lane, 1);
receive_regular_message(&mut lane, 2);
receive_regular_message(&mut lane, 3);
assert_eq!(
lane.receive_state_update(OutboundLaneData {
latest_received_nonce: 3,
..Default::default()
}),
Some(3),
);
assert_eq!(lane.storage.data().last_confirmed_nonce, 3);
assert_eq!(
lane.receive_state_update(OutboundLaneData {
latest_received_nonce: 3,
..Default::default()
}),
None,
);
assert_eq!(lane.storage.data().last_confirmed_nonce, 3);
});
}
#[test]
fn receive_status_update_works() {
run_test(|| {
let mut lane = active_inbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
receive_regular_message(&mut lane, 1);
receive_regular_message(&mut lane, 2);
receive_regular_message(&mut lane, 3);
assert_eq!(lane.storage.data().last_confirmed_nonce, 0);
assert_eq!(
lane.storage.data().relayers,
vec![unrewarded_relayer(1, 3, TEST_RELAYER_A)]
);
assert_eq!(
lane.receive_state_update(OutboundLaneData {
latest_received_nonce: 2,
..Default::default()
}),
Some(2),
);
assert_eq!(lane.storage.data().last_confirmed_nonce, 2);
assert_eq!(
lane.storage.data().relayers,
vec![unrewarded_relayer(3, 3, TEST_RELAYER_A)]
);
assert_eq!(
lane.receive_state_update(OutboundLaneData {
latest_received_nonce: 3,
..Default::default()
}),
Some(3),
);
assert_eq!(lane.storage.data().last_confirmed_nonce, 3);
assert_eq!(lane.storage.data().relayers, vec![]);
});
}
#[test]
fn receive_status_update_works_with_batches_from_relayers() {
run_test(|| {
let mut lane = active_inbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
let mut seed_storage_data = lane.storage.data();
// Prepare data
seed_storage_data.last_confirmed_nonce = 0;
seed_storage_data.relayers.push_back(unrewarded_relayer(1, 1, TEST_RELAYER_A));
// Simulate messages batch (2, 3, 4) from relayer #2
seed_storage_data.relayers.push_back(unrewarded_relayer(2, 4, TEST_RELAYER_B));
seed_storage_data.relayers.push_back(unrewarded_relayer(5, 5, TEST_RELAYER_C));
lane.storage.set_data(seed_storage_data);
// Check
assert_eq!(
lane.receive_state_update(OutboundLaneData {
latest_received_nonce: 3,
..Default::default()
}),
Some(3),
);
assert_eq!(lane.storage.data().last_confirmed_nonce, 3);
assert_eq!(
lane.storage.data().relayers,
vec![
unrewarded_relayer(4, 4, TEST_RELAYER_B),
unrewarded_relayer(5, 5, TEST_RELAYER_C)
]
);
});
}
#[test]
fn fails_to_receive_message_with_incorrect_nonce() {
run_test(|| {
let mut lane = active_inbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&TEST_RELAYER_A,
10,
inbound_message_data(REGULAR_PAYLOAD)
),
ReceptionResult::InvalidNonce
);
assert_eq!(lane.storage.data().last_delivered_nonce(), 0);
});
}
#[test]
fn fails_to_receive_messages_above_unrewarded_relayer_entries_limit_per_lane() {
run_test(|| {
let mut lane = active_inbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
let max_nonce = BridgedChain::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX;
for current_nonce in 1..max_nonce + 1 {
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&(TEST_RELAYER_A + current_nonce),
current_nonce,
inbound_message_data(REGULAR_PAYLOAD)
),
ReceptionResult::Dispatched(dispatch_result(0))
);
}
// Fails to dispatch new message from different than latest relayer.
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&(TEST_RELAYER_A + max_nonce + 1),
max_nonce + 1,
inbound_message_data(REGULAR_PAYLOAD)
),
ReceptionResult::TooManyUnrewardedRelayers,
);
// Fails to dispatch new messages from latest relayer. Prevents griefing attacks.
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&(TEST_RELAYER_A + max_nonce),
max_nonce + 1,
inbound_message_data(REGULAR_PAYLOAD)
),
ReceptionResult::TooManyUnrewardedRelayers,
);
});
}
#[test]
fn fails_to_receive_messages_above_unconfirmed_messages_limit_per_lane() {
run_test(|| {
let mut lane = active_inbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
let max_nonce = BridgedChain::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
for current_nonce in 1..=max_nonce {
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&TEST_RELAYER_A,
current_nonce,
inbound_message_data(REGULAR_PAYLOAD)
),
ReceptionResult::Dispatched(dispatch_result(0))
);
}
// Fails to dispatch new message from different than latest relayer.
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&TEST_RELAYER_B,
max_nonce + 1,
inbound_message_data(REGULAR_PAYLOAD)
),
ReceptionResult::TooManyUnconfirmedMessages,
);
// Fails to dispatch new messages from latest relayer.
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&TEST_RELAYER_A,
max_nonce + 1,
inbound_message_data(REGULAR_PAYLOAD)
),
ReceptionResult::TooManyUnconfirmedMessages,
);
});
}
#[test]
fn correctly_receives_following_messages_from_two_relayers_alternately() {
run_test(|| {
let mut lane = active_inbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&TEST_RELAYER_A,
1,
inbound_message_data(REGULAR_PAYLOAD)
),
ReceptionResult::Dispatched(dispatch_result(0))
);
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&TEST_RELAYER_B,
2,
inbound_message_data(REGULAR_PAYLOAD)
),
ReceptionResult::Dispatched(dispatch_result(0))
);
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&TEST_RELAYER_A,
3,
inbound_message_data(REGULAR_PAYLOAD)
),
ReceptionResult::Dispatched(dispatch_result(0))
);
assert_eq!(
lane.storage.data().relayers,
vec![
unrewarded_relayer(1, 1, TEST_RELAYER_A),
unrewarded_relayer(2, 2, TEST_RELAYER_B),
unrewarded_relayer(3, 3, TEST_RELAYER_A)
]
);
});
}
#[test]
fn rejects_same_message_from_two_different_relayers() {
run_test(|| {
let mut lane = active_inbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&TEST_RELAYER_A,
1,
inbound_message_data(REGULAR_PAYLOAD)
),
ReceptionResult::Dispatched(dispatch_result(0))
);
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&TEST_RELAYER_B,
1,
inbound_message_data(REGULAR_PAYLOAD)
),
ReceptionResult::InvalidNonce,
);
});
}
#[test]
fn correct_message_is_processed_instantly() {
run_test(|| {
let mut lane = active_inbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
receive_regular_message(&mut lane, 1);
assert_eq!(lane.storage.data().last_delivered_nonce(), 1);
});
}
#[test]
fn unspent_weight_is_returned_by_receive_message() {
run_test(|| {
let mut lane = active_inbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
let mut payload = REGULAR_PAYLOAD;
*payload.dispatch_result.unspent_weight.ref_time_mut() = 1;
assert_eq!(
lane.receive_message::<TestMessageDispatch>(
&TEST_RELAYER_A,
1,
inbound_message_data(payload)
),
ReceptionResult::Dispatched(dispatch_result(1))
);
});
}
#[test]
fn first_message_is_confirmed_correctly() {
run_test(|| {
let mut lane = active_inbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
receive_regular_message(&mut lane, 1);
receive_regular_message(&mut lane, 2);
assert_eq!(
lane.receive_state_update(OutboundLaneData {
latest_received_nonce: 1,
..Default::default()
}),
Some(1),
);
assert_eq!(
inbound_unrewarded_relayers_state(test_lane_id()),
UnrewardedRelayersState {
unrewarded_relayer_entries: 1,
messages_in_oldest_entry: 1,
total_messages: 1,
last_delivered_nonce: 2,
},
);
});
}
}
@@ -0,0 +1,287 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
use crate::{
BridgedChainOf, Config, InboundLane, InboundLaneStorage, InboundLanes, OutboundLane,
OutboundLaneStorage, OutboundLanes, OutboundMessages, StoredInboundLaneData,
StoredMessagePayload,
};
use bp_messages::{
target_chain::MessageDispatch, ChainWithMessages, InboundLaneData, LaneState, MessageKey,
MessageNonce, OutboundLaneData,
};
use pezbp_runtime::AccountIdOf;
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use pezframe_support::{ensure, pezsp_runtime::RuntimeDebug, PalletError};
use scale_info::TypeInfo;
use pezsp_std::marker::PhantomData;
/// Lanes manager errors.
#[derive(
Encode, Decode, DecodeWithMemTracking, RuntimeDebug, PartialEq, Eq, PalletError, TypeInfo,
)]
pub enum LanesManagerError {
/// Inbound lane already exists.
InboundLaneAlreadyExists,
/// Outbound lane already exists.
OutboundLaneAlreadyExists,
/// No inbound lane with given id.
UnknownInboundLane,
/// No outbound lane with given id.
UnknownOutboundLane,
/// Inbound lane with given id is closed.
ClosedInboundLane,
/// Outbound lane with given id is closed.
ClosedOutboundLane,
/// Message dispatcher is inactive at given inbound lane. This is logical equivalent
/// of the [`Self::ClosedInboundLane`] variant.
LaneDispatcherInactive,
}
/// Message lanes manager.
pub struct LanesManager<T, I>(PhantomData<(T, I)>);
impl<T: Config<I>, I: 'static> Default for LanesManager<T, I> {
fn default() -> Self {
Self::new()
}
}
impl<T: Config<I>, I: 'static> LanesManager<T, I> {
/// Create new lanes manager.
pub fn new() -> Self {
Self(PhantomData)
}
/// Create new inbound lane in `Opened` state.
pub fn create_inbound_lane(
&self,
lane_id: T::LaneId,
) -> Result<InboundLane<RuntimeInboundLaneStorage<T, I>>, LanesManagerError> {
InboundLanes::<T, I>::try_mutate(lane_id, |lane| match lane {
Some(_) => Err(LanesManagerError::InboundLaneAlreadyExists),
None => {
*lane = Some(StoredInboundLaneData(InboundLaneData {
state: LaneState::Opened,
..Default::default()
}));
Ok(())
},
})?;
self.active_inbound_lane(lane_id)
}
/// Create new outbound lane in `Opened` state.
pub fn create_outbound_lane(
&self,
lane_id: T::LaneId,
) -> Result<OutboundLane<RuntimeOutboundLaneStorage<T, I>>, LanesManagerError> {
OutboundLanes::<T, I>::try_mutate(lane_id, |lane| match lane {
Some(_) => Err(LanesManagerError::OutboundLaneAlreadyExists),
None => {
*lane = Some(OutboundLaneData { state: LaneState::Opened, ..Default::default() });
Ok(())
},
})?;
self.active_outbound_lane(lane_id)
}
/// Get existing inbound lane, checking that it is in usable state.
pub fn active_inbound_lane(
&self,
lane_id: T::LaneId,
) -> Result<InboundLane<RuntimeInboundLaneStorage<T, I>>, LanesManagerError> {
Ok(InboundLane::new(RuntimeInboundLaneStorage::from_lane_id(lane_id, true)?))
}
/// Get existing outbound lane, checking that it is in usable state.
pub fn active_outbound_lane(
&self,
lane_id: T::LaneId,
) -> Result<OutboundLane<RuntimeOutboundLaneStorage<T, I>>, LanesManagerError> {
Ok(OutboundLane::new(RuntimeOutboundLaneStorage::from_lane_id(lane_id, true)?))
}
/// Get existing inbound lane without any additional state checks.
pub fn any_state_inbound_lane(
&self,
lane_id: T::LaneId,
) -> Result<InboundLane<RuntimeInboundLaneStorage<T, I>>, LanesManagerError> {
Ok(InboundLane::new(RuntimeInboundLaneStorage::from_lane_id(lane_id, false)?))
}
/// Get existing outbound lane without any additional state checks.
pub fn any_state_outbound_lane(
&self,
lane_id: T::LaneId,
) -> Result<OutboundLane<RuntimeOutboundLaneStorage<T, I>>, LanesManagerError> {
Ok(OutboundLane::new(RuntimeOutboundLaneStorage::from_lane_id(lane_id, false)?))
}
}
/// Runtime inbound lane storage.
pub struct RuntimeInboundLaneStorage<T: Config<I>, I: 'static = ()> {
pub(crate) lane_id: T::LaneId,
pub(crate) cached_data: InboundLaneData<AccountIdOf<BridgedChainOf<T, I>>>,
}
impl<T: Config<I>, I: 'static> RuntimeInboundLaneStorage<T, I> {
/// Creates new runtime inbound lane storage for given **existing** lane.
fn from_lane_id(
lane_id: T::LaneId,
check_active: bool,
) -> Result<RuntimeInboundLaneStorage<T, I>, LanesManagerError> {
let cached_data =
InboundLanes::<T, I>::get(lane_id).ok_or(LanesManagerError::UnknownInboundLane)?;
if check_active {
// check that the lane is not explicitly closed
ensure!(cached_data.state.is_active(), LanesManagerError::ClosedInboundLane);
// apart from the explicit closure, the lane may be unable to receive any messages.
// Right now we do an additional check here, but it may be done later (e.g. by
// explicitly closing the lane and reopening it from
// `pezpallet-xcm-bridge-hub::on-initialize`)
//
// The fact that we only check it here, means that the `MessageDispatch` may switch
// to inactive state during some message dispatch in the middle of message delivery
// transaction. But we treat result of `MessageDispatch::is_active()` as a hint, so
// we know that it won't drop messages - just it experiences problems with processing.
// This would allow us to check that in our signed extensions, and invalidate
// transaction early, thus avoiding losing honest relayers funds. This problem should
// gone with relayers coordination protocol.
//
// There's a limit on number of messages in the message delivery transaction, so even
// if we dispatch (enqueue) some additional messages, we'll know the maximal queue
// length;
ensure!(
T::MessageDispatch::is_active(lane_id),
LanesManagerError::LaneDispatcherInactive
);
}
Ok(RuntimeInboundLaneStorage { lane_id, cached_data: cached_data.into() })
}
/// Returns number of bytes that may be subtracted from the PoV component of
/// `receive_messages_proof` call, because the actual inbound lane state is smaller than the
/// maximal configured.
///
/// Maximal inbound lane state set size is configured by the
/// `MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX` constant from the pezpallet configuration. The PoV
/// of the call includes the maximal size of inbound lane state. If the actual size is smaller,
/// we may subtract extra bytes from this component.
pub fn extra_proof_size_bytes(&self) -> u64 {
let max_encoded_len = StoredInboundLaneData::<T, I>::max_encoded_len();
let relayers_count = self.data().relayers.len();
let actual_encoded_len =
InboundLaneData::<AccountIdOf<BridgedChainOf<T, I>>>::encoded_size_hint(relayers_count)
.unwrap_or(usize::MAX);
max_encoded_len.saturating_sub(actual_encoded_len) as _
}
}
impl<T: Config<I>, I: 'static> InboundLaneStorage for RuntimeInboundLaneStorage<T, I> {
type Relayer = AccountIdOf<BridgedChainOf<T, I>>;
type LaneId = T::LaneId;
fn id(&self) -> Self::LaneId {
self.lane_id
}
fn max_unrewarded_relayer_entries(&self) -> MessageNonce {
BridgedChainOf::<T, I>::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX
}
fn max_unconfirmed_messages(&self) -> MessageNonce {
BridgedChainOf::<T, I>::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX
}
fn data(&self) -> InboundLaneData<AccountIdOf<BridgedChainOf<T, I>>> {
self.cached_data.clone()
}
fn set_data(&mut self, data: InboundLaneData<AccountIdOf<BridgedChainOf<T, I>>>) {
self.cached_data = data.clone();
InboundLanes::<T, I>::insert(self.lane_id, StoredInboundLaneData::<T, I>(data))
}
fn purge(self) {
InboundLanes::<T, I>::remove(self.lane_id)
}
}
/// Runtime outbound lane storage.
#[derive(Debug, PartialEq, Eq)]
pub struct RuntimeOutboundLaneStorage<T: Config<I>, I: 'static> {
pub(crate) lane_id: T::LaneId,
pub(crate) cached_data: OutboundLaneData,
pub(crate) _phantom: PhantomData<(T, I)>,
}
impl<T: Config<I>, I: 'static> RuntimeOutboundLaneStorage<T, I> {
/// Creates new runtime outbound lane storage for given **existing** lane.
fn from_lane_id(lane_id: T::LaneId, check_active: bool) -> Result<Self, LanesManagerError> {
let cached_data =
OutboundLanes::<T, I>::get(lane_id).ok_or(LanesManagerError::UnknownOutboundLane)?;
ensure!(
!check_active || cached_data.state.is_active(),
LanesManagerError::ClosedOutboundLane
);
Ok(Self { lane_id, cached_data, _phantom: PhantomData })
}
}
impl<T: Config<I>, I: 'static> OutboundLaneStorage for RuntimeOutboundLaneStorage<T, I> {
type StoredMessagePayload = StoredMessagePayload<T, I>;
type LaneId = T::LaneId;
fn id(&self) -> Self::LaneId {
self.lane_id
}
fn data(&self) -> OutboundLaneData {
self.cached_data.clone()
}
fn set_data(&mut self, data: OutboundLaneData) {
self.cached_data = data.clone();
OutboundLanes::<T, I>::insert(self.lane_id, data)
}
#[cfg(test)]
fn message(&self, nonce: &MessageNonce) -> Option<Self::StoredMessagePayload> {
OutboundMessages::<T, I>::get(MessageKey { lane_id: self.lane_id, nonce: *nonce })
.map(Into::into)
}
fn save_message(&mut self, nonce: MessageNonce, message_payload: Self::StoredMessagePayload) {
OutboundMessages::<T, I>::insert(
MessageKey { lane_id: self.lane_id, nonce },
message_payload,
);
}
fn remove_message(&mut self, nonce: &MessageNonce) {
OutboundMessages::<T, I>::remove(MessageKey { lane_id: self.lane_id, nonce: *nonce });
}
fn purge(self) {
OutboundLanes::<T, I>::remove(self.lane_id)
}
}
+791
View File
@@ -0,0 +1,791 @@
// 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/>.
//! Runtime module that allows sending and receiving messages using lane concept:
//!
//! 1) the message is sent using `send_message()` call;
//! 2) every outbound message is assigned nonce;
//! 3) the messages are stored in the storage;
//! 4) external component (relay) delivers messages to bridged chain;
//! 5) messages are processed in order (ordered by assigned nonce);
//! 6) relay may send proof-of-delivery back to this chain.
//!
//! Once message is sent, its progress can be tracked by looking at module events.
//! The assigned nonce is reported using `MessageAccepted` event. When message is
//! delivered to the the bridged chain, it is reported using `MessagesDelivered` event.
//!
//! **IMPORTANT NOTE**: after generating weights (custom `WeighInfo` implementation) for
//! your runtime (where this module is plugged to), please add test for these weights.
//! The test should call the `ensure_weights_are_correct` function from this module.
//! If this test fails with your weights, then either weights are computed incorrectly,
//! or some benchmarks assumptions are broken for your runtime.
#![warn(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
pub use inbound_lane::{InboundLane, InboundLaneStorage, StoredInboundLaneData};
pub use lanes_manager::{
LanesManager, LanesManagerError, RuntimeInboundLaneStorage, RuntimeOutboundLaneStorage,
};
pub use outbound_lane::{
OutboundLane, OutboundLaneStorage, ReceptionConfirmationError, StoredMessagePayload,
};
pub use weights::WeightInfo;
pub use weights_ext::{
ensure_able_to_receive_confirmation, ensure_able_to_receive_message,
ensure_maximal_message_dispatch, ensure_weights_are_correct, WeightInfoExt,
EXPECTED_DEFAULT_MESSAGE_LENGTH, EXTRA_STORAGE_PROOF_SIZE,
};
use bp_header_pez_chain::HeaderChain;
use bp_messages::{
source_chain::{
DeliveryConfirmationPayments, FromBridgedChainMessagesDeliveryProof, OnMessagesDelivered,
SendMessageArtifacts,
},
target_chain::{
DeliveryPayments, DispatchMessage, FromBridgedChainMessagesProof, MessageDispatch,
ProvedLaneMessages, ProvedMessages,
},
ChainWithMessages, DeliveredMessages, InboundLaneData, InboundMessageDetails, MessageKey,
MessageNonce, MessagePayload, MessagesOperatingMode, OutboundLaneData, OutboundMessageDetails,
UnrewardedRelayersState, VerificationError,
};
use pezbp_runtime::{
AccountIdOf, BasicOperatingMode, HashOf, OwnedBridgeModule, PreComputedSize, RangeInclusiveExt,
Size,
};
use codec::{Decode, Encode};
use pezframe_support::{dispatch::PostDispatchInfo, ensure, fail, traits::Get, DefaultNoBound};
use pezsp_std::{marker::PhantomData, prelude::*};
mod call_ext;
mod inbound_lane;
mod lanes_manager;
mod outbound_lane;
mod proofs;
mod tests;
mod weights_ext;
pub mod weights;
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
pub mod migration;
pub use call_ext::*;
pub use pezpallet::*;
#[cfg(feature = "test-helpers")]
pub use tests::*;
/// The target that will be used when publishing logs related to this pezpallet.
pub const LOG_TARGET: &str = "runtime::bridge-messages";
#[pezframe_support::pezpallet]
pub mod pezpallet {
use super::*;
use bp_messages::{LaneIdType, ReceivedMessages, ReceptionResult};
use pezbp_runtime::RangeInclusiveExt;
use pezframe_support::pezpallet_prelude::*;
use pezframe_system::pezpallet_prelude::*;
#[pezpallet::config]
pub trait Config<I: 'static = ()>: pezframe_system::Config {
// General types
/// The overarching event type.
#[allow(deprecated)]
type RuntimeEvent: From<Event<Self, I>>
+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
/// Benchmarks results from runtime we're plugged into.
type WeightInfo: WeightInfoExt;
/// This chain type.
type ThisChain: ChainWithMessages;
/// Bridged chain type.
type BridgedChain: ChainWithMessages;
/// Bridged chain headers provider.
type BridgedHeaderChain: HeaderChain<Self::BridgedChain>;
/// Payload type of outbound messages. This payload is dispatched on the bridged chain.
type OutboundPayload: Parameter + Size;
/// Payload type of inbound messages. This payload is dispatched on this chain.
type InboundPayload: Decode;
/// Lane identifier type.
type LaneId: LaneIdType;
/// Handler for relayer payments that happen during message delivery transaction.
type DeliveryPayments: DeliveryPayments<Self::AccountId>;
/// Handler for relayer payments that happen during message delivery confirmation
/// transaction.
type DeliveryConfirmationPayments: DeliveryConfirmationPayments<
Self::AccountId,
Self::LaneId,
>;
/// Delivery confirmation callback.
type OnMessagesDelivered: OnMessagesDelivered<Self::LaneId>;
/// Message dispatch handler.
type MessageDispatch: MessageDispatch<
DispatchPayload = Self::InboundPayload,
LaneId = Self::LaneId,
>;
}
/// Shortcut to this chain type for Config.
pub type ThisChainOf<T, I> = <T as Config<I>>::ThisChain;
/// Shortcut to bridged chain type for Config.
pub type BridgedChainOf<T, I> = <T as Config<I>>::BridgedChain;
/// Shortcut to bridged header chain type for Config.
pub type BridgedHeaderChainOf<T, I> = <T as Config<I>>::BridgedHeaderChain;
/// Shortcut to lane identifier type for Config.
pub type LaneIdOf<T, I> = <T as Config<I>>::LaneId;
#[pezpallet::pezpallet]
#[pezpallet::storage_version(migration::STORAGE_VERSION)]
pub struct Pezpallet<T, I = ()>(PhantomData<(T, I)>);
impl<T: Config<I>, I: 'static> OwnedBridgeModule<T> for Pezpallet<T, I> {
const LOG_TARGET: &'static str = LOG_TARGET;
type OwnerStorage = PalletOwner<T, I>;
type OperatingMode = MessagesOperatingMode;
type OperatingModeStorage = PalletOperatingMode<T, I>;
}
#[pezpallet::call]
impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
/// Change `PalletOwner`.
///
/// May only be called either by root, or by `PalletOwner`.
#[pezpallet::call_index(0)]
#[pezpallet::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/some pezpallet operations.
///
/// 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_operating_mode(
origin: OriginFor<T>,
operating_mode: MessagesOperatingMode,
) -> DispatchResult {
<Self as OwnedBridgeModule<_>>::set_operating_mode(origin, operating_mode)
}
/// Receive messages proof from bridged chain.
///
/// The weight of the call assumes that the transaction always brings outbound lane
/// state update. Because of that, the submitter (relayer) has no benefit of not including
/// this data in the transaction, so reward confirmations lags should be minimal.
///
/// The call fails if:
///
/// - the pezpallet is halted;
///
/// - the call origin is not `Signed(_)`;
///
/// - there are too many messages in the proof;
///
/// - the proof verification procedure returns an error - e.g. because header used to craft
/// proof is not imported by the associated finality pezpallet;
///
/// - the `dispatch_weight` argument is not sufficient to dispatch all bundled messages.
///
/// The call may succeed, but some messages may not be delivered e.g. if they are not fit
/// into the unrewarded relayers vector.
#[pezpallet::call_index(2)]
#[pezpallet::weight(T::WeightInfo::receive_messages_proof_weight(&**proof, *messages_count, *dispatch_weight))]
pub fn receive_messages_proof(
origin: OriginFor<T>,
relayer_id_at_bridged_chain: AccountIdOf<BridgedChainOf<T, I>>,
proof: Box<FromBridgedChainMessagesProof<HashOf<BridgedChainOf<T, I>>, T::LaneId>>,
messages_count: u32,
dispatch_weight: Weight,
) -> DispatchResultWithPostInfo {
Self::ensure_not_halted().map_err(Error::<T, I>::BridgeModule)?;
let relayer_id_at_this_chain = ensure_signed(origin)?;
// reject transactions that are declaring too many messages
ensure!(
MessageNonce::from(messages_count) <=
BridgedChainOf::<T, I>::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX,
Error::<T, I>::TooManyMessagesInTheProof
);
// why do we need to know the weight of this (`receive_messages_proof`) call? Because
// we may want to return some funds for not-dispatching (or partially dispatching) some
// messages to the call origin (relayer). And this is done by returning actual weight
// from the call. But we only know dispatch weight of every message. So to refund
// relayer because we have not dispatched message, we need to:
//
// ActualWeight = DeclaredWeight - Message.DispatchWeight
//
// The DeclaredWeight is exactly what's computed here. Unfortunately it is impossible
// to get pre-computed value (and it has been already computed by the executive).
let declared_weight = T::WeightInfo::receive_messages_proof_weight(
&*proof,
messages_count,
dispatch_weight,
);
let mut actual_weight = declared_weight;
// verify messages proof && convert proof into messages
let (lane_id, lane_data) =
verify_and_decode_messages_proof::<T, I>(*proof, messages_count).map_err(
|err| {
tracing::trace!(target: LOG_TARGET, error=?err, "Rejecting invalid messages proof");
Error::<T, I>::InvalidMessagesProof
},
)?;
// dispatch messages and (optionally) update lane(s) state(s)
let mut total_messages = 0;
let mut valid_messages = 0;
let mut dispatch_weight_left = dispatch_weight;
let mut lane = active_inbound_lane::<T, I>(lane_id)?;
// subtract extra storage proof bytes from the actual PoV size - there may be
// less unrewarded relayers than the maximal configured value
let lane_extra_proof_size_bytes = lane.storage().extra_proof_size_bytes();
actual_weight = actual_weight.set_proof_size(
actual_weight.proof_size().saturating_sub(lane_extra_proof_size_bytes),
);
if let Some(lane_state) = lane_data.lane_state {
let updated_latest_confirmed_nonce = lane.receive_state_update(lane_state);
if let Some(updated_latest_confirmed_nonce) = updated_latest_confirmed_nonce {
tracing::trace!(
target: LOG_TARGET,
?lane_id,
latest_confirmed_nonce=%updated_latest_confirmed_nonce,
unrewarded_relayers=?UnrewardedRelayersState::from(&lane.storage().data()),
"Received state update"
);
}
}
let mut messages_received_status =
ReceivedMessages::new(lane_id, Vec::with_capacity(lane_data.messages.len()));
for mut message in lane_data.messages {
debug_assert_eq!(message.key.lane_id, lane_id);
total_messages += 1;
// ensure that relayer has declared enough weight for dispatching next message
// on this lane. We can't dispatch lane messages out-of-order, so if declared
// weight is not enough, let's move to next lane
let message_dispatch_weight = T::MessageDispatch::dispatch_weight(&mut message);
if message_dispatch_weight.any_gt(dispatch_weight_left) {
tracing::trace!(
target: LOG_TARGET,
?lane_id,
declared=%message_dispatch_weight,
left=%dispatch_weight_left,
"Cannot dispatch any more messages"
);
fail!(Error::<T, I>::InsufficientDispatchWeight);
}
let receival_result = lane.receive_message::<T::MessageDispatch>(
&relayer_id_at_bridged_chain,
message.key.nonce,
message.data,
);
// note that we're returning unspent weight to relayer even if message has been
// rejected by the lane. This allows relayers to submit spam transactions with
// e.g. the same set of already delivered messages over and over again, without
// losing funds for messages dispatch. But keep in mind that relayer pays base
// delivery transaction cost anyway. And base cost covers everything except
// dispatch, so we have a balance here.
let unspent_weight = match &receival_result {
ReceptionResult::Dispatched(dispatch_result) => {
valid_messages += 1;
dispatch_result.unspent_weight
},
ReceptionResult::InvalidNonce |
ReceptionResult::TooManyUnrewardedRelayers |
ReceptionResult::TooManyUnconfirmedMessages => message_dispatch_weight,
};
messages_received_status.push(message.key.nonce, receival_result);
let unspent_weight = unspent_weight.min(message_dispatch_weight);
dispatch_weight_left -= message_dispatch_weight - unspent_weight;
actual_weight = actual_weight.saturating_sub(unspent_weight);
}
// let's now deal with relayer payments
T::DeliveryPayments::pay_reward(
relayer_id_at_this_chain,
total_messages,
valid_messages,
actual_weight,
);
tracing::debug!(
target: LOG_TARGET,
total=%total_messages,
valid=%valid_messages,
%actual_weight,
%declared_weight,
"Received messages."
);
Self::deposit_event(Event::MessagesReceived(messages_received_status));
Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee: Pays::Yes })
}
/// Receive messages delivery proof from bridged chain.
#[pezpallet::call_index(3)]
#[pezpallet::weight(T::WeightInfo::receive_messages_delivery_proof_weight(
proof,
relayers_state,
))]
pub fn receive_messages_delivery_proof(
origin: OriginFor<T>,
proof: FromBridgedChainMessagesDeliveryProof<HashOf<BridgedChainOf<T, I>>, T::LaneId>,
mut relayers_state: UnrewardedRelayersState,
) -> DispatchResultWithPostInfo {
Self::ensure_not_halted().map_err(Error::<T, I>::BridgeModule)?;
let proof_size = proof.size();
let confirmation_relayer = ensure_signed(origin)?;
let (lane_id, lane_data) = proofs::verify_messages_delivery_proof::<T, I>(proof)
.map_err(|err| {
tracing::trace!(
target: LOG_TARGET,
error=?err,
"Rejecting invalid messages delivery proof"
);
Error::<T, I>::InvalidMessagesDeliveryProof
})?;
ensure!(
relayers_state.is_valid(&lane_data),
Error::<T, I>::InvalidUnrewardedRelayersState
);
// mark messages as delivered
let mut lane = any_state_outbound_lane::<T, I>(lane_id)?;
let last_delivered_nonce = lane_data.last_delivered_nonce();
let confirmed_messages = lane
.confirm_delivery(
relayers_state.total_messages,
last_delivered_nonce,
&lane_data.relayers,
)
.map_err(Error::<T, I>::ReceptionConfirmation)?;
if let Some(confirmed_messages) = confirmed_messages {
// emit 'delivered' event
let received_range = confirmed_messages.begin..=confirmed_messages.end;
Self::deposit_event(Event::MessagesDelivered {
lane_id: lane_id.into(),
messages: confirmed_messages,
});
// if some new messages have been confirmed, reward relayers
let actually_rewarded_relayers = T::DeliveryConfirmationPayments::pay_reward(
lane_id,
lane_data.relayers,
&confirmation_relayer,
&received_range,
);
// update relayers state with actual numbers to compute actual weight below
relayers_state.unrewarded_relayer_entries = pezsp_std::cmp::min(
relayers_state.unrewarded_relayer_entries,
actually_rewarded_relayers,
);
relayers_state.total_messages = pezsp_std::cmp::min(
relayers_state.total_messages,
received_range.checked_len().unwrap_or(MessageNonce::MAX),
);
};
tracing::trace!(
target: LOG_TARGET,
?lane_id,
%last_delivered_nonce,
"Received messages delivery proof up to (and including)"
);
// notify others about messages delivery
T::OnMessagesDelivered::on_messages_delivered(
lane_id,
lane.data().queued_messages().saturating_len(),
);
// because of lags, the inbound lane state (`lane_data`) may have entries for
// already rewarded relayers and messages (if all entries are duplicated, then
// this transaction must be filtered out by our signed extension)
let actual_weight = T::WeightInfo::receive_messages_delivery_proof_weight(
&PreComputedSize(proof_size as usize),
&relayers_state,
);
Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee: Pays::Yes })
}
}
#[pezpallet::event]
#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
/// Message has been accepted and is waiting to be delivered.
MessageAccepted {
/// Lane, which has accepted the message.
lane_id: T::LaneId,
/// Nonce of accepted message.
nonce: MessageNonce,
},
/// Messages have been received from the bridged chain.
MessagesReceived(
/// Result of received messages dispatch.
ReceivedMessages<
<T::MessageDispatch as MessageDispatch>::DispatchLevelResult,
T::LaneId,
>,
),
/// Messages in the inclusive range have been delivered to the bridged chain.
MessagesDelivered {
/// Lane for which the delivery has been confirmed.
lane_id: T::LaneId,
/// Delivered messages.
messages: DeliveredMessages,
},
}
#[pezpallet::error]
#[derive(PartialEq, Eq)]
pub enum Error<T, I = ()> {
/// Pezpallet is not in Normal operating mode.
NotOperatingNormally,
/// Error that is reported by the lanes manager.
LanesManager(LanesManagerError),
/// Message has been treated as invalid by the pezpallet logic.
MessageRejectedByPallet(VerificationError),
/// The transaction brings too many messages.
TooManyMessagesInTheProof,
/// Invalid messages has been submitted.
InvalidMessagesProof,
/// Invalid messages delivery proof has been submitted.
InvalidMessagesDeliveryProof,
/// The relayer has declared invalid unrewarded relayers state in the
/// `receive_messages_delivery_proof` call.
InvalidUnrewardedRelayersState,
/// The cumulative dispatch weight, passed by relayer is not enough to cover dispatch
/// of all bundled messages.
InsufficientDispatchWeight,
/// Error confirming messages receival.
ReceptionConfirmation(ReceptionConfirmationError),
/// Error generated by the `OwnedBridgeModule` trait.
BridgeModule(pezbp_runtime::OwnedBridgeModuleError),
}
/// Optional pezpallet owner.
///
/// Pezpallet owner has a right to halt all pezpallet operations and then resume it. 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<T: Config<I>, I: 'static = ()> = StorageValue<_, T::AccountId>;
/// The current operating mode of the pezpallet.
///
/// Depending on the mode either all, some, or no transactions will be allowed.
#[pezpallet::storage]
pub type PalletOperatingMode<T: Config<I>, I: 'static = ()> =
StorageValue<_, MessagesOperatingMode, ValueQuery>;
// TODO: https://github.com/pezkuwichain/kurdistan-sdk/issues/89: let's limit number of
// possible opened lanes && use it to constraint maps below
/// Map of lane id => inbound lane data.
#[pezpallet::storage]
pub type InboundLanes<T: Config<I>, I: 'static = ()> =
StorageMap<_, Blake2_128Concat, T::LaneId, StoredInboundLaneData<T, I>, OptionQuery>;
/// Map of lane id => outbound lane data.
#[pezpallet::storage]
pub type OutboundLanes<T: Config<I>, I: 'static = ()> = StorageMap<
Hasher = Blake2_128Concat,
Key = T::LaneId,
Value = OutboundLaneData,
QueryKind = OptionQuery,
>;
/// All queued outbound messages.
#[pezpallet::storage]
pub type OutboundMessages<T: Config<I>, I: 'static = ()> =
StorageMap<_, Blake2_128Concat, MessageKey<T::LaneId>, StoredMessagePayload<T, I>>;
#[pezpallet::genesis_config]
#[derive(DefaultNoBound)]
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
/// Initial pezpallet operating mode.
pub operating_mode: MessagesOperatingMode,
/// Initial pezpallet owner.
pub owner: Option<T::AccountId>,
/// Opened lanes.
pub opened_lanes: Vec<T::LaneId>,
/// Dummy marker.
#[serde(skip)]
pub _phantom: pezsp_std::marker::PhantomData<I>,
}
#[pezpallet::genesis_build]
impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
fn build(&self) {
PalletOperatingMode::<T, I>::put(self.operating_mode);
if let Some(ref owner) = self.owner {
PalletOwner::<T, I>::put(owner);
}
for lane_id in &self.opened_lanes {
InboundLanes::<T, I>::insert(lane_id, InboundLaneData::opened());
OutboundLanes::<T, I>::insert(lane_id, OutboundLaneData::opened());
}
}
}
#[pezpallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pezpallet<T, I> {
#[cfg(feature = "try-runtime")]
fn try_state(_n: BlockNumberFor<T>) -> Result<(), pezsp_runtime::TryRuntimeError> {
Self::do_try_state()
}
}
impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
/// Get stored data of the outbound message with given nonce.
pub fn outbound_message_data(
lane: T::LaneId,
nonce: MessageNonce,
) -> Option<MessagePayload> {
OutboundMessages::<T, I>::get(MessageKey { lane_id: lane, nonce }).map(Into::into)
}
/// Prepare data, related to given inbound message.
pub fn inbound_message_data(
lane: T::LaneId,
payload: MessagePayload,
outbound_details: OutboundMessageDetails,
) -> InboundMessageDetails {
let mut dispatch_message = DispatchMessage {
key: MessageKey { lane_id: lane, nonce: outbound_details.nonce },
data: payload.into(),
};
InboundMessageDetails {
dispatch_weight: T::MessageDispatch::dispatch_weight(&mut dispatch_message),
}
}
/// Return outbound lane data.
pub fn outbound_lane_data(lane: T::LaneId) -> Option<OutboundLaneData> {
OutboundLanes::<T, I>::get(lane)
}
/// Return inbound lane data.
pub fn inbound_lane_data(
lane: T::LaneId,
) -> Option<InboundLaneData<AccountIdOf<BridgedChainOf<T, I>>>> {
InboundLanes::<T, I>::get(lane).map(|lane| lane.0)
}
}
#[cfg(any(feature = "try-runtime", test))]
impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
/// Ensure the correctness of the state of this pezpallet.
pub fn do_try_state() -> Result<(), pezsp_runtime::TryRuntimeError> {
Self::do_try_state_for_outbound_lanes()
}
/// Ensure the correctness of the state of outbound lanes.
pub fn do_try_state_for_outbound_lanes() -> Result<(), pezsp_runtime::TryRuntimeError> {
use pezsp_runtime::traits::One;
use pezsp_std::vec::Vec;
// collect unpruned lanes
let mut unpruned_lanes = Vec::new();
for (lane_id, lane_data) in OutboundLanes::<T, I>::iter() {
let Some(expected_last_prunned_nonce) =
lane_data.oldest_unpruned_nonce.checked_sub(One::one())
else {
continue;
};
// collect message_nonces that were supposed to be pruned
let mut unpruned_message_nonces = Vec::new();
const MAX_MESSAGES_ITERATION: u64 = 16;
let start_nonce =
expected_last_prunned_nonce.checked_sub(MAX_MESSAGES_ITERATION).unwrap_or(0);
for current_nonce in start_nonce..=expected_last_prunned_nonce {
// check a message for current_nonce
if OutboundMessages::<T, I>::contains_key(MessageKey {
lane_id,
nonce: current_nonce,
}) {
unpruned_message_nonces.push(current_nonce);
}
}
if !unpruned_message_nonces.is_empty() {
tracing::warn!(
target: LOG_TARGET,
?lane_id,
?lane_data,
?unpruned_message_nonces,
"do_try_state_for_outbound_lanes found",
);
unpruned_lanes.push((lane_id, lane_data, unpruned_message_nonces));
}
}
// ensure messages before `oldest_unpruned_nonce` are really pruned.
ensure!(unpruned_lanes.is_empty(), "Found unpruned lanes!");
Ok(())
}
}
}
/// Structure, containing a validated message payload and all the info required
/// to send it on the bridge.
#[derive(Debug, PartialEq, Eq)]
pub struct SendMessageArgs<T: Config<I>, I: 'static> {
lane_id: T::LaneId,
lane: OutboundLane<RuntimeOutboundLaneStorage<T, I>>,
payload: StoredMessagePayload<T, I>,
}
impl<T, I> bp_messages::source_chain::MessagesBridge<T::OutboundPayload, T::LaneId> for Pezpallet<T, I>
where
T: Config<I>,
I: 'static,
{
type Error = Error<T, I>;
type SendMessageArgs = SendMessageArgs<T, I>;
fn validate_message(
lane_id: T::LaneId,
message: &T::OutboundPayload,
) -> Result<SendMessageArgs<T, I>, Self::Error> {
// we can't accept any messages if the pezpallet is halted
ensure_normal_operating_mode::<T, I>()?;
// check lane
let lane = active_outbound_lane::<T, I>(lane_id)?;
Ok(SendMessageArgs {
lane_id,
lane,
payload: StoredMessagePayload::<T, I>::try_from(message.encode()).map_err(|_| {
Error::<T, I>::MessageRejectedByPallet(VerificationError::MessageTooLarge)
})?,
})
}
fn send_message(args: SendMessageArgs<T, I>) -> SendMessageArtifacts {
// save message in outbound storage and emit event
let mut lane = args.lane;
let message_len = args.payload.len();
let nonce = lane.send_message(args.payload);
// return number of messages in the queue to let sender know about its state
let enqueued_messages = lane.data().queued_messages().saturating_len();
tracing::trace!(
target: LOG_TARGET,
lane_id=?args.lane_id,
%nonce,
message_size=?message_len,
"Accepted message"
);
Pezpallet::<T, I>::deposit_event(Event::MessageAccepted {
lane_id: args.lane_id.into(),
nonce,
});
SendMessageArtifacts { nonce, enqueued_messages }
}
}
/// Ensure that the pezpallet is in normal operational mode.
fn ensure_normal_operating_mode<T: Config<I>, I: 'static>() -> Result<(), Error<T, I>> {
if PalletOperatingMode::<T, I>::get() ==
MessagesOperatingMode::Basic(BasicOperatingMode::Normal)
{
return Ok(());
}
Err(Error::<T, I>::NotOperatingNormally)
}
/// Creates new inbound lane object, backed by runtime storage. Lane must be active.
fn active_inbound_lane<T: Config<I>, I: 'static>(
lane_id: T::LaneId,
) -> Result<InboundLane<RuntimeInboundLaneStorage<T, I>>, Error<T, I>> {
LanesManager::<T, I>::new()
.active_inbound_lane(lane_id)
.map_err(Error::LanesManager)
}
/// Creates new outbound lane object, backed by runtime storage. Lane must be active.
fn active_outbound_lane<T: Config<I>, I: 'static>(
lane_id: T::LaneId,
) -> Result<OutboundLane<RuntimeOutboundLaneStorage<T, I>>, Error<T, I>> {
LanesManager::<T, I>::new()
.active_outbound_lane(lane_id)
.map_err(Error::LanesManager)
}
/// Creates new outbound lane object, backed by runtime storage.
fn any_state_outbound_lane<T: Config<I>, I: 'static>(
lane_id: T::LaneId,
) -> Result<OutboundLane<RuntimeOutboundLaneStorage<T, I>>, Error<T, I>> {
LanesManager::<T, I>::new()
.any_state_outbound_lane(lane_id)
.map_err(Error::LanesManager)
}
/// Verify messages proof and return proved messages with decoded payload.
fn verify_and_decode_messages_proof<T: Config<I>, I: 'static>(
proof: FromBridgedChainMessagesProof<HashOf<BridgedChainOf<T, I>>, T::LaneId>,
messages_count: u32,
) -> Result<
ProvedMessages<T::LaneId, DispatchMessage<T::InboundPayload, T::LaneId>>,
VerificationError,
> {
// `receive_messages_proof` weight formula and `MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX`
// check guarantees that the `message_count` is sane and Vec<Message> may be allocated.
// (tx with too many messages will either be rejected from the pool, or will fail earlier)
proofs::verify_messages_proof::<T, I>(proof, messages_count).map(|(lane, lane_data)| {
(
lane,
ProvedLaneMessages {
lane_state: lane_data.lane_state,
messages: lane_data.messages.into_iter().map(Into::into).collect(),
},
)
})
}
@@ -0,0 +1,146 @@
// 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/>.
//! A module that is responsible for migration of storage.
use crate::{Config, Pezpallet};
use pezframe_support::{
traits::{Get, StorageVersion},
weights::Weight,
};
/// The in-code storage version.
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
/// This module contains data structures that are valid for the initial state of `0`.
/// (used with v1 migration).
pub mod v0 {
use super::Config;
use crate::BridgedChainOf;
use bp_messages::{MessageNonce, UnrewardedRelayer};
use pezbp_runtime::AccountIdOf;
use codec::{Decode, Encode};
use pezsp_std::collections::vec_deque::VecDeque;
#[derive(Encode, Decode, Clone, PartialEq, Eq)]
pub(crate) struct StoredInboundLaneData<T: Config<I>, I: 'static>(
pub(crate) InboundLaneData<AccountIdOf<BridgedChainOf<T, I>>>,
);
#[derive(Encode, Decode, Clone, PartialEq, Eq)]
pub(crate) struct InboundLaneData<RelayerId> {
pub(crate) relayers: VecDeque<UnrewardedRelayer<RelayerId>>,
pub(crate) last_confirmed_nonce: MessageNonce,
}
#[derive(Encode, Decode, Clone, PartialEq, Eq)]
pub(crate) struct OutboundLaneData {
pub(crate) oldest_unpruned_nonce: MessageNonce,
pub(crate) latest_received_nonce: MessageNonce,
pub(crate) latest_generated_nonce: MessageNonce,
}
}
/// This migration to `1` updates the metadata of `InboundLanes` and `OutboundLanes` to the new
/// structures.
pub mod v1 {
use super::*;
use crate::{
InboundLaneData, InboundLanes, OutboundLaneData, OutboundLanes, StoredInboundLaneData,
};
use bp_messages::LaneState;
use pezframe_support::traits::UncheckedOnRuntimeUpgrade;
use pezsp_std::marker::PhantomData;
/// Migrates the pezpallet storage to v1.
pub struct UncheckedMigrationV0ToV1<T, I>(PhantomData<(T, I)>);
impl<T: Config<I>, I: 'static> UncheckedOnRuntimeUpgrade for UncheckedMigrationV0ToV1<T, I> {
fn on_runtime_upgrade() -> Weight {
let mut weight = T::DbWeight::get().reads(1);
// `InboundLanes` - add state to the old structs
let translate_inbound =
|pre: v0::StoredInboundLaneData<T, I>| -> Option<v1::StoredInboundLaneData<T, I>> {
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
Some(v1::StoredInboundLaneData(v1::InboundLaneData {
state: LaneState::Opened,
relayers: pre.0.relayers,
last_confirmed_nonce: pre.0.last_confirmed_nonce,
}))
};
InboundLanes::<T, I>::translate_values(translate_inbound);
// `OutboundLanes` - add state to the old structs
let translate_outbound = |pre: v0::OutboundLaneData| -> Option<v1::OutboundLaneData> {
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
Some(v1::OutboundLaneData {
state: LaneState::Opened,
oldest_unpruned_nonce: pre.oldest_unpruned_nonce,
latest_received_nonce: pre.latest_received_nonce,
latest_generated_nonce: pre.latest_generated_nonce,
})
};
OutboundLanes::<T, I>::translate_values(translate_outbound);
weight
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<pezsp_std::vec::Vec<u8>, pezsp_runtime::DispatchError> {
use codec::Encode;
let number_of_inbound_to_migrate = InboundLanes::<T, I>::iter_keys().count();
let number_of_outbound_to_migrate = OutboundLanes::<T, I>::iter_keys().count();
Ok((number_of_inbound_to_migrate as u32, number_of_outbound_to_migrate as u32).encode())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: pezsp_std::vec::Vec<u8>) -> Result<(), pezsp_runtime::DispatchError> {
use codec::Decode;
const LOG_TARGET: &str = "runtime::bridge-messages-migration";
let (number_of_inbound_to_migrate, number_of_outbound_to_migrate): (u32, u32) =
Decode::decode(&mut &state[..]).unwrap();
let number_of_inbound = InboundLanes::<T, I>::iter_keys().count();
let number_of_outbound = OutboundLanes::<T, I>::iter_keys().count();
tracing::info!(target: LOG_TARGET, %number_of_inbound_to_migrate, "post-upgrade expects inbound lanes to have been migrated.");
tracing::info!(target: LOG_TARGET, %number_of_outbound_to_migrate, "post-upgrade expects outbound lanes to have been migrated.");
pezframe_support::ensure!(
number_of_inbound_to_migrate as usize == number_of_inbound,
"must migrate all `InboundLanes`."
);
pezframe_support::ensure!(
number_of_outbound_to_migrate as usize == number_of_outbound,
"must migrate all `OutboundLanes`."
);
tracing::info!(target: LOG_TARGET, "migrated all.");
Ok(())
}
}
/// [`UncheckedMigrationV0ToV1`] wrapped in a
/// [`VersionedMigration`](pezframe_support::migrations::VersionedMigration), ensuring the
/// migration is only performed when on-chain version is 0.
pub type MigrationToV1<T, I> = pezframe_support::migrations::VersionedMigration<
0,
1,
UncheckedMigrationV0ToV1<T, I>,
Pezpallet<T, I>,
<T as pezframe_system::Config>::DbWeight,
>;
}
@@ -0,0 +1,429 @@
// 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/>.
//! Everything about outgoing messages sending.
use crate::{Config, LOG_TARGET};
use bp_messages::{
ChainWithMessages, DeliveredMessages, LaneState, MessageNonce, OutboundLaneData,
UnrewardedRelayer,
};
use codec::{Decode, DecodeWithMemTracking, Encode};
use pezframe_support::{traits::Get, BoundedVec, PalletError};
use scale_info::TypeInfo;
use pezsp_runtime::RuntimeDebug;
use pezsp_std::{collections::vec_deque::VecDeque, marker::PhantomData, ops::RangeInclusive};
/// Outbound lane storage.
pub trait OutboundLaneStorage {
/// Stored message payload type.
type StoredMessagePayload;
/// Lane identifier type.
type LaneId: Encode;
/// Lane id.
fn id(&self) -> Self::LaneId;
/// Get lane data from the storage.
fn data(&self) -> OutboundLaneData;
/// Update lane data in the storage.
fn set_data(&mut self, data: OutboundLaneData);
/// Returns saved outbound message payload.
#[cfg(test)]
fn message(&self, nonce: &MessageNonce) -> Option<Self::StoredMessagePayload>;
/// Save outbound message in the storage.
fn save_message(&mut self, nonce: MessageNonce, message_payload: Self::StoredMessagePayload);
/// Remove outbound message from the storage.
fn remove_message(&mut self, nonce: &MessageNonce);
/// Purge lane data from the storage.
fn purge(self);
}
/// Limit for the `StoredMessagePayload` vector.
pub struct StoredMessagePayloadLimit<T, I>(PhantomData<(T, I)>);
impl<T: Config<I>, I: 'static> Get<u32> for StoredMessagePayloadLimit<T, I> {
fn get() -> u32 {
T::BridgedChain::maximal_incoming_message_size()
}
}
/// Outbound message data wrapper that implements `MaxEncodedLen`.
pub type StoredMessagePayload<T, I> = BoundedVec<u8, StoredMessagePayloadLimit<T, I>>;
/// Result of messages receival confirmation.
#[derive(
Encode, Decode, DecodeWithMemTracking, RuntimeDebug, PartialEq, Eq, PalletError, TypeInfo,
)]
pub enum ReceptionConfirmationError {
/// Bridged chain is trying to confirm more messages than we have generated. May be a result
/// of invalid bridged chain storage.
FailedToConfirmFutureMessages,
/// The unrewarded relayers vec contains an empty entry. May be a result of invalid bridged
/// chain storage.
EmptyUnrewardedRelayerEntry,
/// The unrewarded relayers vec contains non-consecutive entries. May be a result of invalid
/// bridged chain storage.
NonConsecutiveUnrewardedRelayerEntries,
/// The chain has more messages that need to be confirmed than there is in the proof.
TryingToConfirmMoreMessagesThanExpected,
}
/// Outbound messages lane.
#[derive(Debug, PartialEq, Eq)]
pub struct OutboundLane<S> {
storage: S,
}
impl<S: OutboundLaneStorage> OutboundLane<S> {
/// Create new outbound lane backed by given storage.
pub fn new(storage: S) -> Self {
OutboundLane { storage }
}
/// Get this lane data.
pub fn data(&self) -> OutboundLaneData {
self.storage.data()
}
/// Get lane state.
pub fn state(&self) -> LaneState {
self.storage.data().state
}
/// Set lane state.
pub fn set_state(&mut self, state: LaneState) {
let mut data = self.storage.data();
data.state = state;
self.storage.set_data(data);
}
/// Return nonces of all currently queued messages.
pub fn queued_messages(&self) -> RangeInclusive<MessageNonce> {
let data = self.storage.data();
data.oldest_unpruned_nonce..=data.latest_generated_nonce
}
/// Send message over lane.
///
/// Returns new message nonce.
pub fn send_message(&mut self, message_payload: S::StoredMessagePayload) -> MessageNonce {
let mut data = self.storage.data();
let nonce = data.latest_generated_nonce + 1;
data.latest_generated_nonce = nonce;
self.storage.save_message(nonce, message_payload);
self.storage.set_data(data);
nonce
}
/// Confirm messages delivery.
pub fn confirm_delivery<RelayerId>(
&mut self,
max_allowed_messages: MessageNonce,
latest_delivered_nonce: MessageNonce,
relayers: &VecDeque<UnrewardedRelayer<RelayerId>>,
) -> Result<Option<DeliveredMessages>, ReceptionConfirmationError> {
let mut data = self.storage.data();
let confirmed_messages = DeliveredMessages {
begin: data.latest_received_nonce.saturating_add(1),
end: latest_delivered_nonce,
};
if confirmed_messages.total_messages() == 0 {
return Ok(None);
}
if confirmed_messages.end > data.latest_generated_nonce {
return Err(ReceptionConfirmationError::FailedToConfirmFutureMessages);
}
if confirmed_messages.total_messages() > max_allowed_messages {
// that the relayer has declared correct number of messages that the proof contains (it
// is checked outside of the function). But it may happen (but only if this/bridged
// chain storage is corrupted, though) that the actual number of confirmed messages if
// larger than declared. This would mean that 'reward loop' will take more time than the
// weight formula accounts, so we can't allow that.
tracing::trace!(
target: LOG_TARGET,
confirmed=%confirmed_messages.total_messages(),
max_allowed=%max_allowed_messages,
"Messages delivery proof contains too many messages to confirm"
);
return Err(ReceptionConfirmationError::TryingToConfirmMoreMessagesThanExpected);
}
ensure_unrewarded_relayers_are_correct(confirmed_messages.end, relayers)?;
// prune all confirmed messages
for nonce in confirmed_messages.begin..=confirmed_messages.end {
self.storage.remove_message(&nonce);
}
data.latest_received_nonce = confirmed_messages.end;
data.oldest_unpruned_nonce = data.latest_received_nonce.saturating_add(1);
self.storage.set_data(data);
Ok(Some(confirmed_messages))
}
/// Remove message from the storage. Doesn't perform any checks.
pub fn remove_oldest_unpruned_message(&mut self) {
let mut data = self.storage.data();
self.storage.remove_message(&data.oldest_unpruned_nonce);
data.oldest_unpruned_nonce += 1;
self.storage.set_data(data);
}
/// Purge lane state from the storage.
pub fn purge(self) {
self.storage.purge()
}
}
/// Verifies unrewarded relayers vec.
///
/// Returns `Err(_)` if unrewarded relayers vec contains invalid data, meaning that the bridged
/// chain has invalid runtime storage.
fn ensure_unrewarded_relayers_are_correct<RelayerId>(
latest_received_nonce: MessageNonce,
relayers: &VecDeque<UnrewardedRelayer<RelayerId>>,
) -> Result<(), ReceptionConfirmationError> {
let mut expected_entry_begin = relayers.front().map(|entry| entry.messages.begin);
for entry in relayers {
// unrewarded relayer entry must have at least 1 unconfirmed message
// (guaranteed by the `InboundLane::receive_message()`)
if entry.messages.end < entry.messages.begin {
return Err(ReceptionConfirmationError::EmptyUnrewardedRelayerEntry);
}
// every entry must confirm range of messages that follows previous entry range
// (guaranteed by the `InboundLane::receive_message()`)
if expected_entry_begin != Some(entry.messages.begin) {
return Err(ReceptionConfirmationError::NonConsecutiveUnrewardedRelayerEntries);
}
expected_entry_begin = entry.messages.end.checked_add(1);
// entry can't confirm messages larger than `inbound_lane_data.latest_received_nonce()`
// (guaranteed by the `InboundLane::receive_message()`)
if entry.messages.end > latest_received_nonce {
return Err(ReceptionConfirmationError::FailedToConfirmFutureMessages);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
active_outbound_lane,
tests::mock::{
outbound_message_data, run_test, test_lane_id, unrewarded_relayer, TestRelayer,
TestRuntime, REGULAR_PAYLOAD,
},
};
use pezsp_std::ops::RangeInclusive;
fn unrewarded_relayers(
nonces: RangeInclusive<MessageNonce>,
) -> VecDeque<UnrewardedRelayer<TestRelayer>> {
vec![unrewarded_relayer(*nonces.start(), *nonces.end(), 0)]
.into_iter()
.collect()
}
fn delivered_messages(nonces: RangeInclusive<MessageNonce>) -> DeliveredMessages {
DeliveredMessages { begin: *nonces.start(), end: *nonces.end() }
}
fn assert_3_messages_confirmation_fails(
latest_received_nonce: MessageNonce,
relayers: &VecDeque<UnrewardedRelayer<TestRelayer>>,
) -> Result<Option<DeliveredMessages>, ReceptionConfirmationError> {
run_test(|| {
let mut lane = active_outbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
lane.send_message(outbound_message_data(REGULAR_PAYLOAD));
lane.send_message(outbound_message_data(REGULAR_PAYLOAD));
lane.send_message(outbound_message_data(REGULAR_PAYLOAD));
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
assert_eq!(lane.storage.data().latest_received_nonce, 0);
let result = lane.confirm_delivery(3, latest_received_nonce, relayers);
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
assert_eq!(lane.storage.data().latest_received_nonce, 0);
result
})
}
#[test]
fn send_message_works() {
run_test(|| {
let mut lane = active_outbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
assert_eq!(lane.storage.data().latest_generated_nonce, 0);
assert_eq!(lane.send_message(outbound_message_data(REGULAR_PAYLOAD)), 1);
assert!(lane.storage.message(&1).is_some());
assert_eq!(lane.storage.data().latest_generated_nonce, 1);
});
}
#[test]
fn confirm_delivery_works() {
run_test(|| {
let mut lane = active_outbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
assert_eq!(lane.send_message(outbound_message_data(REGULAR_PAYLOAD)), 1);
assert_eq!(lane.send_message(outbound_message_data(REGULAR_PAYLOAD)), 2);
assert_eq!(lane.send_message(outbound_message_data(REGULAR_PAYLOAD)), 3);
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
assert_eq!(lane.storage.data().latest_received_nonce, 0);
assert_eq!(lane.storage.data().oldest_unpruned_nonce, 1);
assert_eq!(
lane.confirm_delivery(3, 3, &unrewarded_relayers(1..=3)),
Ok(Some(delivered_messages(1..=3))),
);
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
assert_eq!(lane.storage.data().latest_received_nonce, 3);
assert_eq!(lane.storage.data().oldest_unpruned_nonce, 4);
});
}
#[test]
fn confirm_partial_delivery_works() {
run_test(|| {
let mut lane = active_outbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
assert_eq!(lane.send_message(outbound_message_data(REGULAR_PAYLOAD)), 1);
assert_eq!(lane.send_message(outbound_message_data(REGULAR_PAYLOAD)), 2);
assert_eq!(lane.send_message(outbound_message_data(REGULAR_PAYLOAD)), 3);
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
assert_eq!(lane.storage.data().latest_received_nonce, 0);
assert_eq!(lane.storage.data().oldest_unpruned_nonce, 1);
assert_eq!(
lane.confirm_delivery(3, 2, &unrewarded_relayers(1..=2)),
Ok(Some(delivered_messages(1..=2))),
);
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
assert_eq!(lane.storage.data().latest_received_nonce, 2);
assert_eq!(lane.storage.data().oldest_unpruned_nonce, 3);
assert_eq!(
lane.confirm_delivery(3, 3, &unrewarded_relayers(3..=3)),
Ok(Some(delivered_messages(3..=3))),
);
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
assert_eq!(lane.storage.data().latest_received_nonce, 3);
assert_eq!(lane.storage.data().oldest_unpruned_nonce, 4);
});
}
#[test]
fn confirm_delivery_rejects_nonce_lesser_than_latest_received() {
run_test(|| {
let mut lane = active_outbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
lane.send_message(outbound_message_data(REGULAR_PAYLOAD));
lane.send_message(outbound_message_data(REGULAR_PAYLOAD));
lane.send_message(outbound_message_data(REGULAR_PAYLOAD));
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
assert_eq!(lane.storage.data().latest_received_nonce, 0);
assert_eq!(lane.storage.data().oldest_unpruned_nonce, 1);
assert_eq!(
lane.confirm_delivery(3, 3, &unrewarded_relayers(1..=3)),
Ok(Some(delivered_messages(1..=3))),
);
assert_eq!(lane.confirm_delivery(3, 3, &unrewarded_relayers(1..=3)), Ok(None),);
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
assert_eq!(lane.storage.data().latest_received_nonce, 3);
assert_eq!(lane.storage.data().oldest_unpruned_nonce, 4);
assert_eq!(lane.confirm_delivery(1, 2, &unrewarded_relayers(1..=1)), Ok(None),);
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
assert_eq!(lane.storage.data().latest_received_nonce, 3);
assert_eq!(lane.storage.data().oldest_unpruned_nonce, 4);
});
}
#[test]
fn confirm_delivery_rejects_nonce_larger_than_last_generated() {
assert_eq!(
assert_3_messages_confirmation_fails(10, &unrewarded_relayers(1..=10),),
Err(ReceptionConfirmationError::FailedToConfirmFutureMessages),
);
}
#[test]
fn confirm_delivery_fails_if_entry_confirms_future_messages() {
assert_eq!(
assert_3_messages_confirmation_fails(
3,
&unrewarded_relayers(1..=1)
.into_iter()
.chain(unrewarded_relayers(2..=30))
.chain(unrewarded_relayers(3..=3))
.collect(),
),
Err(ReceptionConfirmationError::FailedToConfirmFutureMessages),
);
}
#[test]
#[allow(clippy::reversed_empty_ranges)]
fn confirm_delivery_fails_if_entry_is_empty() {
assert_eq!(
assert_3_messages_confirmation_fails(
3,
&unrewarded_relayers(1..=1)
.into_iter()
.chain(unrewarded_relayers(2..=1))
.chain(unrewarded_relayers(2..=3))
.collect(),
),
Err(ReceptionConfirmationError::EmptyUnrewardedRelayerEntry),
);
}
#[test]
fn confirm_delivery_fails_if_entries_are_non_consecutive() {
assert_eq!(
assert_3_messages_confirmation_fails(
3,
&unrewarded_relayers(1..=1)
.into_iter()
.chain(unrewarded_relayers(3..=3))
.chain(unrewarded_relayers(2..=2))
.collect(),
),
Err(ReceptionConfirmationError::NonConsecutiveUnrewardedRelayerEntries),
);
}
#[test]
fn confirm_delivery_detects_when_more_than_expected_messages_are_confirmed() {
run_test(|| {
let mut lane = active_outbound_lane::<TestRuntime, _>(test_lane_id()).unwrap();
lane.send_message(outbound_message_data(REGULAR_PAYLOAD));
lane.send_message(outbound_message_data(REGULAR_PAYLOAD));
lane.send_message(outbound_message_data(REGULAR_PAYLOAD));
assert_eq!(
lane.confirm_delivery(0, 3, &unrewarded_relayers(1..=3)),
Err(ReceptionConfirmationError::TryingToConfirmMoreMessagesThanExpected),
);
assert_eq!(
lane.confirm_delivery(2, 3, &unrewarded_relayers(1..=3)),
Err(ReceptionConfirmationError::TryingToConfirmMoreMessagesThanExpected),
);
assert_eq!(
lane.confirm_delivery(3, 3, &unrewarded_relayers(1..=3)),
Ok(Some(delivered_messages(1..=3))),
);
});
}
}
+561
View File
@@ -0,0 +1,561 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Tools for messages and delivery proof verification.
use crate::{BridgedChainOf, BridgedHeaderChainOf, Config};
use bp_header_pez_chain::{HeaderChain, HeaderChainError};
use bp_messages::{
source_chain::FromBridgedChainMessagesDeliveryProof,
target_chain::{FromBridgedChainMessagesProof, ProvedLaneMessages, ProvedMessages},
ChainWithMessages, InboundLaneData, Message, MessageKey, MessageNonce, MessagePayload,
OutboundLaneData, VerificationError,
};
use pezbp_runtime::{
HashOf, HasherOf, RangeInclusiveExt, RawStorageProof, StorageProofChecker, StorageProofError,
};
use codec::Decode;
use pezsp_std::vec::Vec;
/// 'Parsed' message delivery proof - inbound lane id and its state.
pub(crate) type ParsedMessagesDeliveryProofFromBridgedChain<T, I> =
(<T as Config<I>>::LaneId, InboundLaneData<<T as pezframe_system::Config>::AccountId>);
/// Verify proof of Bridged -> This chain messages.
///
/// This function is used when Bridged chain is directly using GRANDPA finality. For Bridged
/// teyrchains, please use the `verify_messages_proof_from_teyrchain`.
///
/// The `messages_count` argument verification (sane limits) is supposed to be made
/// outside of this function. This function only verifies that the proof declares exactly
/// `messages_count` messages.
pub fn verify_messages_proof<T: Config<I>, I: 'static>(
proof: FromBridgedChainMessagesProof<HashOf<BridgedChainOf<T, I>>, T::LaneId>,
messages_count: u32,
) -> Result<ProvedMessages<T::LaneId, Message<T::LaneId>>, VerificationError> {
let FromBridgedChainMessagesProof {
bridged_header_hash,
storage_proof,
lane,
nonces_start,
nonces_end,
} = proof;
let mut parser: MessagesStorageProofAdapter<T, I> =
MessagesStorageProofAdapter::try_new_with_verified_storage_proof(
bridged_header_hash,
storage_proof,
)
.map_err(VerificationError::HeaderChain)?;
let nonces_range = nonces_start..=nonces_end;
// receiving proofs where end < begin is ok (if proof includes outbound lane state)
let messages_in_the_proof = nonces_range.saturating_len();
if messages_in_the_proof != MessageNonce::from(messages_count) {
return Err(VerificationError::MessagesCountMismatch);
}
// Read messages first. All messages that are claimed to be in the proof must
// be in the proof. So any error in `read_value`, or even missing value is fatal.
//
// Mind that we allow proofs with no messages if outbound lane state is proved.
let mut messages = Vec::with_capacity(messages_in_the_proof as _);
for nonce in nonces_range {
let message_key = MessageKey { lane_id: lane, nonce };
let message_payload = parser
.read_and_decode_message_payload(&message_key)
.map_err(VerificationError::MessageStorage)?;
messages.push(Message { key: message_key, payload: message_payload });
}
// Now let's check if proof contains outbound lane state proof. It is optional, so
// we simply ignore `read_value` errors and missing value.
let proved_lane_messages = ProvedLaneMessages {
lane_state: parser
.read_and_decode_outbound_lane_data(&lane)
.map_err(VerificationError::OutboundLaneStorage)?,
messages,
};
// Now we may actually check if the proof is empty or not.
if proved_lane_messages.lane_state.is_none() && proved_lane_messages.messages.is_empty() {
return Err(VerificationError::EmptyMessageProof);
}
// Check that the storage proof doesn't have any untouched keys.
parser.ensure_no_unused_keys().map_err(VerificationError::StorageProof)?;
Ok((lane, proved_lane_messages))
}
/// Verify proof of This -> Bridged chain messages delivery.
pub fn verify_messages_delivery_proof<T: Config<I>, I: 'static>(
proof: FromBridgedChainMessagesDeliveryProof<HashOf<BridgedChainOf<T, I>>, T::LaneId>,
) -> Result<ParsedMessagesDeliveryProofFromBridgedChain<T, I>, VerificationError> {
let FromBridgedChainMessagesDeliveryProof { bridged_header_hash, storage_proof, lane } = proof;
let mut parser: MessagesStorageProofAdapter<T, I> =
MessagesStorageProofAdapter::try_new_with_verified_storage_proof(
bridged_header_hash,
storage_proof,
)
.map_err(VerificationError::HeaderChain)?;
// Messages delivery proof is just proof of single storage key read => any error
// is fatal.
let storage_inbound_lane_data_key = bp_messages::storage_keys::inbound_lane_data_key(
T::ThisChain::WITH_CHAIN_MESSAGES_PALLET_NAME,
&lane,
);
let inbound_lane_data = parser
.read_and_decode_mandatory_value(&storage_inbound_lane_data_key)
.map_err(VerificationError::InboundLaneStorage)?;
// check that the storage proof doesn't have any untouched trie nodes
parser.ensure_no_unused_keys().map_err(VerificationError::StorageProof)?;
Ok((lane, inbound_lane_data))
}
/// Abstraction over storage proof manipulation, hiding implementation details of actual storage
/// proofs.
trait StorageProofAdapter<T: Config<I>, I: 'static> {
fn read_and_decode_mandatory_value<D: Decode>(
&mut self,
key: &impl AsRef<[u8]>,
) -> Result<D, StorageProofError>;
fn read_and_decode_optional_value<D: Decode>(
&mut self,
key: &impl AsRef<[u8]>,
) -> Result<Option<D>, StorageProofError>;
fn ensure_no_unused_keys(self) -> Result<(), StorageProofError>;
fn read_and_decode_outbound_lane_data(
&mut self,
lane_id: &T::LaneId,
) -> Result<Option<OutboundLaneData>, StorageProofError> {
let storage_outbound_lane_data_key = bp_messages::storage_keys::outbound_lane_data_key(
T::ThisChain::WITH_CHAIN_MESSAGES_PALLET_NAME,
lane_id,
);
self.read_and_decode_optional_value(&storage_outbound_lane_data_key)
}
fn read_and_decode_message_payload(
&mut self,
message_key: &MessageKey<T::LaneId>,
) -> Result<MessagePayload, StorageProofError> {
let storage_message_key = bp_messages::storage_keys::message_key(
T::ThisChain::WITH_CHAIN_MESSAGES_PALLET_NAME,
&message_key.lane_id,
message_key.nonce,
);
self.read_and_decode_mandatory_value(&storage_message_key)
}
}
/// Actual storage proof adapter for messages proofs.
type MessagesStorageProofAdapter<T, I> = StorageProofCheckerAdapter<T, I>;
/// A `StorageProofAdapter` implementation for raw storage proofs.
struct StorageProofCheckerAdapter<T: Config<I>, I: 'static> {
storage: StorageProofChecker<HasherOf<BridgedChainOf<T, I>>>,
_dummy: pezsp_std::marker::PhantomData<(T, I)>,
}
impl<T: Config<I>, I: 'static> StorageProofCheckerAdapter<T, I> {
fn try_new_with_verified_storage_proof(
bridged_header_hash: HashOf<BridgedChainOf<T, I>>,
storage_proof: RawStorageProof,
) -> Result<Self, HeaderChainError> {
BridgedHeaderChainOf::<T, I>::verify_storage_proof(bridged_header_hash, storage_proof).map(
|storage| StorageProofCheckerAdapter::<T, I> { storage, _dummy: Default::default() },
)
}
}
impl<T: Config<I>, I: 'static> StorageProofAdapter<T, I> for StorageProofCheckerAdapter<T, I> {
fn read_and_decode_optional_value<D: Decode>(
&mut self,
key: &impl AsRef<[u8]>,
) -> Result<Option<D>, StorageProofError> {
self.storage.read_and_decode_opt_value(key.as_ref())
}
fn read_and_decode_mandatory_value<D: Decode>(
&mut self,
key: &impl AsRef<[u8]>,
) -> Result<D, StorageProofError> {
self.storage.read_and_decode_mandatory_value(key.as_ref())
}
fn ensure_no_unused_keys(self) -> Result<(), StorageProofError> {
self.storage.ensure_no_unused_nodes()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{
messages_generation::{
encode_all_messages, encode_lane_data, generate_dummy_message,
prepare_messages_storage_proof,
},
mock::*,
};
use bp_header_pez_chain::{HeaderChainError, StoredHeaderDataBuilder};
use bp_messages::LaneState;
use pezbp_runtime::{HeaderId, StorageProofError};
use codec::Encode;
use pezsp_runtime::traits::Header;
fn using_messages_proof<R>(
nonces_end: MessageNonce,
outbound_lane_data: Option<OutboundLaneData>,
encode_message: impl Fn(MessageNonce, &MessagePayload) -> Option<Vec<u8>>,
encode_outbound_lane_data: impl Fn(&OutboundLaneData) -> Vec<u8>,
add_duplicate_key: bool,
add_unused_key: bool,
test: impl Fn(FromBridgedChainMessagesProof<BridgedHeaderHash, TestLaneIdType>) -> R,
) -> R {
let (state_root, storage_proof) =
prepare_messages_storage_proof::<BridgedChain, ThisChain, TestLaneIdType>(
test_lane_id(),
1..=nonces_end,
outbound_lane_data,
pezbp_runtime::UnverifiedStorageProofParams::default(),
generate_dummy_message,
encode_message,
encode_outbound_lane_data,
add_duplicate_key,
add_unused_key,
);
pezsp_io::TestExternalities::new(Default::default()).execute_with(move || {
let bridged_header = BridgedChainHeader::new(
0,
Default::default(),
state_root,
Default::default(),
Default::default(),
);
let bridged_header_hash = bridged_header.hash();
pezpallet_bridge_grandpa::BestFinalized::<TestRuntime>::put(HeaderId(
0,
bridged_header_hash,
));
pezpallet_bridge_grandpa::ImportedHeaders::<TestRuntime>::insert(
bridged_header_hash,
bridged_header.build(),
);
test(FromBridgedChainMessagesProof {
bridged_header_hash,
storage_proof,
lane: test_lane_id(),
nonces_start: 1,
nonces_end,
})
})
}
#[test]
fn messages_proof_is_rejected_if_declared_less_than_actual_number_of_messages() {
assert_eq!(
using_messages_proof(
10,
None,
encode_all_messages,
encode_lane_data,
false,
false,
|proof| { verify_messages_proof::<TestRuntime, ()>(proof, 5) }
),
Err(VerificationError::MessagesCountMismatch),
);
}
#[test]
fn messages_proof_is_rejected_if_declared_more_than_actual_number_of_messages() {
assert_eq!(
using_messages_proof(
10,
None,
encode_all_messages,
encode_lane_data,
false,
false,
|proof| { verify_messages_proof::<TestRuntime, ()>(proof, 15) }
),
Err(VerificationError::MessagesCountMismatch),
);
}
#[test]
fn message_proof_is_rejected_if_header_is_missing_from_the_chain() {
assert_eq!(
using_messages_proof(
10,
None,
encode_all_messages,
encode_lane_data,
false,
false,
|proof| {
let bridged_header_hash =
pezpallet_bridge_grandpa::BestFinalized::<TestRuntime>::get().unwrap().1;
pezpallet_bridge_grandpa::ImportedHeaders::<TestRuntime>::remove(
bridged_header_hash,
);
verify_messages_proof::<TestRuntime, ()>(proof, 10)
}
),
Err(VerificationError::HeaderChain(HeaderChainError::UnknownHeader)),
);
}
#[test]
fn message_proof_is_rejected_if_header_state_root_mismatches() {
assert_eq!(
using_messages_proof(
10,
None,
encode_all_messages,
encode_lane_data,
false,
false,
|proof| {
let bridged_header_hash =
pezpallet_bridge_grandpa::BestFinalized::<TestRuntime>::get().unwrap().1;
pezpallet_bridge_grandpa::ImportedHeaders::<TestRuntime>::insert(
bridged_header_hash,
BridgedChainHeader::new(
0,
Default::default(),
Default::default(),
Default::default(),
Default::default(),
)
.build(),
);
verify_messages_proof::<TestRuntime, ()>(proof, 10)
}
),
Err(VerificationError::HeaderChain(HeaderChainError::StorageProof(
StorageProofError::StorageRootMismatch
))),
);
}
#[test]
fn message_proof_is_rejected_if_it_has_duplicate_trie_nodes() {
assert_eq!(
using_messages_proof(
10,
None,
encode_all_messages,
encode_lane_data,
true,
false,
|proof| { verify_messages_proof::<TestRuntime, ()>(proof, 10) },
),
Err(VerificationError::HeaderChain(HeaderChainError::StorageProof(
StorageProofError::DuplicateNodes
))),
);
}
#[test]
fn message_proof_is_rejected_if_it_has_unused_trie_nodes() {
assert_eq!(
using_messages_proof(
10,
None,
encode_all_messages,
encode_lane_data,
false,
true,
|proof| { verify_messages_proof::<TestRuntime, ()>(proof, 10) },
),
Err(VerificationError::StorageProof(StorageProofError::UnusedKey)),
);
}
#[test]
fn message_proof_is_rejected_if_required_message_is_missing() {
matches!(
using_messages_proof(
10,
None,
|n, m| if n != 5 { Some(m.encode()) } else { None },
encode_lane_data,
false,
false,
|proof| verify_messages_proof::<TestRuntime, ()>(proof, 10)
),
Err(VerificationError::MessageStorage(StorageProofError::EmptyVal)),
);
}
#[test]
fn message_proof_is_rejected_if_message_decode_fails() {
matches!(
using_messages_proof(
10,
None,
|n, m| {
let mut m = m.encode();
if n == 5 {
m = vec![42]
}
Some(m)
},
encode_lane_data,
false,
false,
|proof| verify_messages_proof::<TestRuntime, ()>(proof, 10),
),
Err(VerificationError::MessageStorage(StorageProofError::DecodeError)),
);
}
#[test]
fn message_proof_is_rejected_if_outbound_lane_state_decode_fails() {
matches!(
using_messages_proof(
10,
Some(OutboundLaneData {
state: LaneState::Opened,
oldest_unpruned_nonce: 1,
latest_received_nonce: 1,
latest_generated_nonce: 1,
}),
encode_all_messages,
|d| {
let mut d = d.encode();
d.truncate(1);
d
},
false,
false,
|proof| verify_messages_proof::<TestRuntime, ()>(proof, 10),
),
Err(VerificationError::OutboundLaneStorage(StorageProofError::DecodeError)),
);
}
#[test]
fn message_proof_is_rejected_if_it_is_empty() {
assert_eq!(
using_messages_proof(
0,
None,
encode_all_messages,
encode_lane_data,
false,
false,
|proof| { verify_messages_proof::<TestRuntime, ()>(proof, 0) },
),
Err(VerificationError::EmptyMessageProof),
);
}
#[test]
fn non_empty_message_proof_without_messages_is_accepted() {
assert_eq!(
using_messages_proof(
0,
Some(OutboundLaneData {
state: LaneState::Opened,
oldest_unpruned_nonce: 1,
latest_received_nonce: 1,
latest_generated_nonce: 1,
}),
encode_all_messages,
encode_lane_data,
false,
false,
|proof| verify_messages_proof::<TestRuntime, ()>(proof, 0),
),
Ok((
test_lane_id(),
ProvedLaneMessages {
lane_state: Some(OutboundLaneData {
state: LaneState::Opened,
oldest_unpruned_nonce: 1,
latest_received_nonce: 1,
latest_generated_nonce: 1,
}),
messages: Vec::new(),
},
)),
);
}
#[test]
fn non_empty_message_proof_is_accepted() {
assert_eq!(
using_messages_proof(
1,
Some(OutboundLaneData {
state: LaneState::Opened,
oldest_unpruned_nonce: 1,
latest_received_nonce: 1,
latest_generated_nonce: 1,
}),
encode_all_messages,
encode_lane_data,
false,
false,
|proof| verify_messages_proof::<TestRuntime, ()>(proof, 1),
),
Ok((
test_lane_id(),
ProvedLaneMessages {
lane_state: Some(OutboundLaneData {
state: LaneState::Opened,
oldest_unpruned_nonce: 1,
latest_received_nonce: 1,
latest_generated_nonce: 1,
}),
messages: vec![Message {
key: MessageKey { lane_id: test_lane_id(), nonce: 1 },
payload: vec![42],
}],
},
))
);
}
#[test]
fn verify_messages_proof_does_not_panic_if_messages_count_mismatches() {
assert_eq!(
using_messages_proof(
1,
None,
encode_all_messages,
encode_lane_data,
false,
false,
|mut proof| {
proof.nonces_end = u64::MAX;
verify_messages_proof::<TestRuntime, ()>(proof, u32::MAX)
},
),
Err(VerificationError::MessagesCountMismatch),
);
}
}
@@ -0,0 +1,171 @@
// 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/>.
//! Helpers for generating message storage proofs, that are used by tests and by benchmarks.
use bp_messages::{
storage_keys, ChainWithMessages, InboundLaneData, MessageKey, MessageNonce, MessagePayload,
OutboundLaneData,
};
use pezbp_runtime::{
grow_storage_value, record_all_trie_keys, AccountIdOf, Chain, HashOf, HasherOf,
RawStorageProof, UnverifiedStorageProofParams,
};
use codec::Encode;
use pezsp_std::{ops::RangeInclusive, prelude::*};
use pezsp_trie::{trie_types::TrieDBMutBuilderV1, LayoutV1, MemoryDB, TrieMut};
/// Dummy message generation function.
pub fn generate_dummy_message(_: MessageNonce) -> MessagePayload {
vec![42]
}
/// Simple and correct message data encode function.
pub fn encode_all_messages(_: MessageNonce, m: &MessagePayload) -> Option<Vec<u8>> {
Some(m.encode())
}
/// Simple and correct outbound lane data encode function.
pub fn encode_lane_data(d: &OutboundLaneData) -> Vec<u8> {
d.encode()
}
/// Prepare storage proof of given messages.
///
/// Returns state trie root and nodes with prepared messages.
#[allow(clippy::too_many_arguments)]
pub fn prepare_messages_storage_proof<
BridgedChain: Chain,
ThisChain: ChainWithMessages,
LaneId: Encode + Copy,
>(
lane: LaneId,
message_nonces: RangeInclusive<MessageNonce>,
outbound_lane_data: Option<OutboundLaneData>,
proof_params: UnverifiedStorageProofParams,
generate_message: impl Fn(MessageNonce) -> MessagePayload,
encode_message: impl Fn(MessageNonce, &MessagePayload) -> Option<Vec<u8>>,
encode_outbound_lane_data: impl Fn(&OutboundLaneData) -> Vec<u8>,
add_duplicate_key: bool,
add_unused_key: bool,
) -> (HashOf<BridgedChain>, RawStorageProof)
where
HashOf<BridgedChain>: Copy + Default,
{
// prepare Bridged chain storage with messages and (optionally) outbound lane state
let message_count = message_nonces.end().saturating_sub(*message_nonces.start()) + 1;
let mut storage_keys = Vec::with_capacity(message_count as usize + 1);
let mut root = Default::default();
let mut mdb = MemoryDB::default();
{
let mut trie =
TrieDBMutBuilderV1::<HasherOf<BridgedChain>>::new(&mut mdb, &mut root).build();
// insert messages
for (i, nonce) in message_nonces.into_iter().enumerate() {
let message_key = MessageKey { lane_id: lane, nonce };
let message_payload = match encode_message(nonce, &generate_message(nonce)) {
Some(message_payload) =>
if i == 0 {
grow_storage_value(message_payload, &proof_params)
} else {
message_payload
},
None => continue,
};
let storage_key = storage_keys::message_key(
ThisChain::WITH_CHAIN_MESSAGES_PALLET_NAME,
&message_key.lane_id,
message_key.nonce,
)
.0;
trie.insert(&storage_key, &message_payload)
.map_err(|_| "TrieMut::insert has failed")
.expect("TrieMut::insert should not fail in benchmarks");
storage_keys.push(storage_key);
}
// insert outbound lane state
if let Some(outbound_lane_data) = outbound_lane_data.as_ref().map(encode_outbound_lane_data)
{
let storage_key = storage_keys::outbound_lane_data_key(
ThisChain::WITH_CHAIN_MESSAGES_PALLET_NAME,
&lane,
)
.0;
trie.insert(&storage_key, &outbound_lane_data)
.map_err(|_| "TrieMut::insert has failed")
.expect("TrieMut::insert should not fail in benchmarks");
storage_keys.push(storage_key);
}
}
// generate storage proof to be delivered to This chain
let mut storage_proof =
record_all_trie_keys::<LayoutV1<HasherOf<BridgedChain>>, _>(&mdb, &root)
.map_err(|_| "record_all_trie_keys has failed")
.expect("record_all_trie_keys should not fail in benchmarks");
if add_duplicate_key {
assert!(!storage_proof.is_empty());
let node = storage_proof.pop().unwrap();
storage_proof.push(node.clone());
storage_proof.push(node);
}
if add_unused_key {
storage_proof.push(b"unused_value".to_vec());
}
(root, storage_proof)
}
/// Prepare storage proof of given messages delivery.
///
/// Returns state trie root and nodes with prepared messages.
pub fn prepare_message_delivery_storage_proof<
BridgedChain: Chain,
ThisChain: ChainWithMessages,
LaneId: Encode,
>(
lane: LaneId,
inbound_lane_data: InboundLaneData<AccountIdOf<ThisChain>>,
proof_params: UnverifiedStorageProofParams,
) -> (HashOf<BridgedChain>, RawStorageProof)
where
HashOf<BridgedChain>: Copy + Default,
{
// prepare Bridged chain storage with inbound lane state
let storage_key =
storage_keys::inbound_lane_data_key(ThisChain::WITH_CHAIN_MESSAGES_PALLET_NAME, &lane).0;
let mut root = Default::default();
let mut mdb = MemoryDB::default();
{
let mut trie =
TrieDBMutBuilderV1::<HasherOf<BridgedChain>>::new(&mut mdb, &mut root).build();
let inbound_lane_data = grow_storage_value(inbound_lane_data.encode(), &proof_params);
trie.insert(&storage_key, &inbound_lane_data)
.map_err(|_| "TrieMut::insert has failed")
.expect("TrieMut::insert should not fail in benchmarks");
}
// generate storage proof to be delivered to This chain
let storage_proof = record_all_trie_keys::<LayoutV1<HasherOf<BridgedChain>>, _>(&mdb, &root)
.map_err(|_| "record_all_trie_keys has failed")
.expect("record_all_trie_keys should not fail in benchmarks");
(root, storage_proof)
}
@@ -0,0 +1,561 @@
// 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/>.
// From construct_runtime macro
#![allow(clippy::from_over_into)]
use crate::{
tests::messages_generation::{
encode_all_messages, encode_lane_data, prepare_message_delivery_storage_proof,
prepare_messages_storage_proof,
},
Config, StoredMessagePayload,
};
use bp_header_pez_chain::{ChainWithGrandpa, StoredHeaderData};
use bp_messages::{
calc_relayers_rewards,
source_chain::{
DeliveryConfirmationPayments, FromBridgedChainMessagesDeliveryProof, OnMessagesDelivered,
},
target_chain::{
DeliveryPayments, DispatchMessage, DispatchMessageData, FromBridgedChainMessagesProof,
MessageDispatch,
},
ChainWithMessages, DeliveredMessages, HashedLaneId, InboundLaneData, LaneIdType, LaneState,
Message, MessageKey, MessageNonce, OutboundLaneData, UnrewardedRelayer,
UnrewardedRelayersState,
};
use pezbp_runtime::{
messages::MessageDispatchResult, Chain, ChainId, Size, UnverifiedStorageProofParams,
};
use codec::{Decode, DecodeWithMemTracking, Encode};
use pezframe_support::{
derive_impl,
weights::{constants::RocksDbWeight, Weight},
};
use scale_info::TypeInfo;
use pezsp_core::H256;
use pezsp_runtime::{
testing::Header as BizinikiwiHeader,
traits::{BlakeTwo256, ConstU32},
BuildStorage, StateVersion,
};
use std::{collections::VecDeque, ops::RangeInclusive};
pub type AccountId = u64;
pub type Balance = u64;
#[derive(Decode, DecodeWithMemTracking, Encode, Clone, Debug, PartialEq, Eq, TypeInfo)]
pub struct TestPayload {
/// Field that may be used to identify messages.
pub id: u64,
/// Dispatch weight that is declared by the message sender.
pub declared_weight: Weight,
/// Message dispatch result.
///
/// Note: in correct code `dispatch_result.unspent_weight` will always be <= `declared_weight`,
/// but for test purposes we'll be making it larger than `declared_weight` sometimes.
pub dispatch_result: MessageDispatchResult<TestDispatchLevelResult>,
/// Extra bytes that affect payload size.
pub extra: Vec<u8>,
}
pub type TestMessageFee = u64;
pub type TestRelayer = u64;
pub type TestDispatchLevelResult = ();
pub struct ThisChain;
impl Chain for ThisChain {
const ID: ChainId = *b"ttch";
type BlockNumber = u64;
type Hash = H256;
type Hasher = BlakeTwo256;
type Header = BizinikiwiHeader;
type AccountId = AccountId;
type Balance = Balance;
type Nonce = u64;
type Signature = pezsp_runtime::MultiSignature;
const STATE_VERSION: StateVersion = StateVersion::V1;
fn max_extrinsic_size() -> u32 {
u32::MAX
}
fn max_extrinsic_weight() -> Weight {
Weight::MAX
}
}
impl ChainWithMessages for ThisChain {
const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str = "WithThisChainBridgeMessages";
const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 16;
const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 128;
}
pub struct BridgedChain;
pub type BridgedHeaderHash = H256;
pub type BridgedChainHeader = BizinikiwiHeader;
impl Chain for BridgedChain {
const ID: ChainId = *b"tbch";
type BlockNumber = u64;
type Hash = BridgedHeaderHash;
type Hasher = BlakeTwo256;
type Header = BridgedChainHeader;
type AccountId = TestRelayer;
type Balance = Balance;
type Nonce = u64;
type Signature = pezsp_runtime::MultiSignature;
const STATE_VERSION: StateVersion = StateVersion::V1;
fn max_extrinsic_size() -> u32 {
4096
}
fn max_extrinsic_weight() -> Weight {
Weight::MAX
}
}
impl ChainWithGrandpa for BridgedChain {
const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str = "WithBridgedChainBridgeGrandpa";
const MAX_AUTHORITIES_COUNT: u32 = 16;
const REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY: u32 = 4;
const MAX_MANDATORY_HEADER_SIZE: u32 = 4096;
const AVERAGE_HEADER_SIZE: u32 = 4096;
}
impl ChainWithMessages for BridgedChain {
const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str = "WithBridgedChainBridgeMessages";
const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 16;
const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 128;
}
type Block = pezframe_system::mocking::MockBlock<TestRuntime>;
use crate as pezpallet_bridge_messages;
pezframe_support::construct_runtime! {
pub enum TestRuntime
{
System: pezframe_system::{Pezpallet, Call, Config<T>, Storage, Event<T>},
Balances: pezpallet_balances::{Pezpallet, Call, Event<T>},
BridgedChainGrandpa: pezpallet_bridge_grandpa::{Pezpallet, Call, Event<T>},
Messages: pezpallet_bridge_messages::{Pezpallet, Call, Event<T>},
}
}
pub type DbWeight = RocksDbWeight;
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
impl pezframe_system::Config for TestRuntime {
type Block = Block;
type AccountData = pezpallet_balances::AccountData<Balance>;
type DbWeight = DbWeight;
}
#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
impl pezpallet_balances::Config for TestRuntime {
type AccountStore = System;
}
impl pezpallet_bridge_grandpa::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type BridgedChain = BridgedChain;
type MaxFreeHeadersPerBlock = ConstU32<4>;
type FreeHeadersInterval = ConstU32<1_024>;
type HeadersToKeep = ConstU32<8>;
type WeightInfo = pezpallet_bridge_grandpa::weights::BridgeWeight<TestRuntime>;
}
/// weights of messages pezpallet calls we use in tests.
pub type TestWeightInfo = ();
impl Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = TestWeightInfo;
type ThisChain = ThisChain;
type BridgedChain = BridgedChain;
type BridgedHeaderChain = BridgedChainGrandpa;
type OutboundPayload = TestPayload;
type InboundPayload = TestPayload;
type LaneId = TestLaneIdType;
type DeliveryPayments = TestDeliveryPayments;
type DeliveryConfirmationPayments = TestDeliveryConfirmationPayments;
type OnMessagesDelivered = TestOnMessagesDelivered;
type MessageDispatch = TestMessageDispatch;
}
#[cfg(feature = "runtime-benchmarks")]
impl crate::benchmarking::Config<()> for TestRuntime {
fn bench_lane_id() -> Self::LaneId {
test_lane_id()
}
fn prepare_message_proof(
params: crate::benchmarking::MessageProofParams<Self::LaneId>,
) -> (FromBridgedChainMessagesProof<BridgedHeaderHash, Self::LaneId>, Weight) {
use pezbp_runtime::RangeInclusiveExt;
let dispatch_weight =
REGULAR_PAYLOAD.declared_weight * params.message_nonces.saturating_len();
(
*prepare_messages_proof(
params.message_nonces.into_iter().map(|n| message(n, REGULAR_PAYLOAD)).collect(),
params.outbound_lane_data,
),
dispatch_weight,
)
}
fn prepare_message_delivery_proof(
params: crate::benchmarking::MessageDeliveryProofParams<AccountId, Self::LaneId>,
) -> FromBridgedChainMessagesDeliveryProof<BridgedHeaderHash, Self::LaneId> {
// in mock run we only care about benchmarks correctness, not the benchmark results
// => ignore size related arguments
prepare_messages_delivery_proof(params.lane, params.inbound_lane_data)
}
fn is_relayer_rewarded(_relayer: &AccountId) -> bool {
true
}
}
impl Size for TestPayload {
fn size(&self) -> u32 {
16 + self.extra.len() as u32
}
}
/// Account that has balance to use in tests.
pub const ENDOWED_ACCOUNT: AccountId = 0xDEAD;
/// Account id of test relayer.
pub const TEST_RELAYER_A: AccountId = 100;
/// Account id of additional test relayer - B.
pub const TEST_RELAYER_B: AccountId = 101;
/// Account id of additional test relayer - C.
pub const TEST_RELAYER_C: AccountId = 102;
/// Lane identifier type used for tests.
pub type TestLaneIdType = HashedLaneId;
/// Lane that we're using in tests.
pub fn test_lane_id() -> TestLaneIdType {
TestLaneIdType::try_new(1, 2).unwrap()
}
/// Lane that is completely unknown to our runtime.
pub fn unknown_lane_id() -> TestLaneIdType {
TestLaneIdType::try_new(1, 3).unwrap()
}
/// Lane that is registered, but it is closed.
pub fn closed_lane_id() -> TestLaneIdType {
TestLaneIdType::try_new(1, 4).unwrap()
}
/// Regular message payload.
pub const REGULAR_PAYLOAD: TestPayload = message_payload(0, 50);
/// Reward payments at the target chain during delivery transaction.
#[derive(Debug, Default)]
pub struct TestDeliveryPayments;
impl TestDeliveryPayments {
/// Returns true if given relayer has been rewarded with given balance. The reward-paid flag is
/// cleared after the call.
pub fn is_reward_paid(relayer: AccountId) -> bool {
let key = (b":delivery-relayer-reward:", relayer).encode();
pezframe_support::storage::unhashed::take::<bool>(&key).is_some()
}
}
impl DeliveryPayments<AccountId> for TestDeliveryPayments {
type Error = &'static str;
fn pay_reward(
relayer: AccountId,
_total_messages: MessageNonce,
_valid_messages: MessageNonce,
_actual_weight: Weight,
) {
let key = (b":delivery-relayer-reward:", relayer).encode();
pezframe_support::storage::unhashed::put(&key, &true);
}
}
/// Reward payments at the source chain during delivery confirmation transaction.
#[derive(Debug, Default)]
pub struct TestDeliveryConfirmationPayments;
impl TestDeliveryConfirmationPayments {
/// Returns true if given relayer has been rewarded with given balance. The reward-paid flag is
/// cleared after the call.
pub fn is_reward_paid(relayer: AccountId, fee: TestMessageFee) -> bool {
let key = (b":relayer-reward:", relayer, fee).encode();
pezframe_support::storage::unhashed::take::<bool>(&key).is_some()
}
}
impl DeliveryConfirmationPayments<AccountId, TestLaneIdType> for TestDeliveryConfirmationPayments {
type Error = &'static str;
fn pay_reward(
_lane_id: TestLaneIdType,
pez_messages_relayers: VecDeque<UnrewardedRelayer<AccountId>>,
_confirmation_relayer: &AccountId,
received_range: &RangeInclusive<MessageNonce>,
) -> MessageNonce {
let relayers_rewards = calc_relayers_rewards(pez_messages_relayers, received_range);
let rewarded_relayers = relayers_rewards.len();
for (relayer, reward) in &relayers_rewards {
let key = (b":relayer-reward:", relayer, reward).encode();
pezframe_support::storage::unhashed::put(&key, &true);
}
rewarded_relayers as _
}
}
/// Test message dispatcher.
#[derive(Debug)]
pub struct TestMessageDispatch;
impl TestMessageDispatch {
pub fn deactivate(lane: TestLaneIdType) {
// "enqueue" enough (to deactivate dispatcher) messages at dispatcher
let latest_received_nonce = BridgedChain::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX + 1;
for _ in 1..=latest_received_nonce {
Self::emulate_enqueued_message(lane);
}
}
pub fn emulate_enqueued_message(lane: TestLaneIdType) {
let key = (b"dispatched", lane).encode();
let dispatched = pezframe_support::storage::unhashed::get_or_default::<MessageNonce>(&key[..]);
pezframe_support::storage::unhashed::put(&key[..], &(dispatched + 1));
}
}
impl MessageDispatch for TestMessageDispatch {
type DispatchPayload = TestPayload;
type DispatchLevelResult = TestDispatchLevelResult;
type LaneId = TestLaneIdType;
fn is_active(lane: Self::LaneId) -> bool {
pezframe_support::storage::unhashed::get_or_default::<MessageNonce>(
&(b"dispatched", lane).encode()[..],
) <= BridgedChain::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX
}
fn dispatch_weight(message: &mut DispatchMessage<TestPayload, Self::LaneId>) -> Weight {
match message.data.payload.as_ref() {
Ok(payload) => payload.declared_weight,
Err(_) => Weight::zero(),
}
}
fn dispatch(
message: DispatchMessage<TestPayload, Self::LaneId>,
) -> MessageDispatchResult<TestDispatchLevelResult> {
match message.data.payload.as_ref() {
Ok(payload) => {
Self::emulate_enqueued_message(message.key.lane_id);
payload.dispatch_result.clone()
},
Err(_) => dispatch_result(0),
}
}
}
/// Test callback, called during message delivery confirmation transaction.
pub struct TestOnMessagesDelivered;
impl TestOnMessagesDelivered {
pub fn call_arguments() -> Option<(TestLaneIdType, MessageNonce)> {
pezframe_support::storage::unhashed::get(b"TestOnMessagesDelivered.OnMessagesDelivered")
}
}
impl OnMessagesDelivered<TestLaneIdType> for TestOnMessagesDelivered {
fn on_messages_delivered(lane: TestLaneIdType, enqueued_messages: MessageNonce) {
pezframe_support::storage::unhashed::put(
b"TestOnMessagesDelivered.OnMessagesDelivered",
&(lane, enqueued_messages),
);
}
}
/// Return test lane message with given nonce and payload.
pub fn message(nonce: MessageNonce, payload: TestPayload) -> Message<TestLaneIdType> {
Message { key: MessageKey { lane_id: test_lane_id(), nonce }, payload: payload.encode() }
}
/// Return valid outbound message data, constructed from given payload.
pub fn outbound_message_data(payload: TestPayload) -> StoredMessagePayload<TestRuntime, ()> {
StoredMessagePayload::<TestRuntime, ()>::try_from(payload.encode()).expect("payload too large")
}
/// Return valid inbound (dispatch) message data, constructed from given payload.
pub fn inbound_message_data(payload: TestPayload) -> DispatchMessageData<TestPayload> {
DispatchMessageData { payload: Ok(payload) }
}
/// Constructs message payload using given arguments and zero unspent weight.
pub const fn message_payload(id: u64, declared_weight: u64) -> TestPayload {
TestPayload {
id,
declared_weight: Weight::from_parts(declared_weight, 0),
dispatch_result: dispatch_result(0),
extra: Vec::new(),
}
}
/// Returns message dispatch result with given unspent weight.
pub const fn dispatch_result(
unspent_weight: u64,
) -> MessageDispatchResult<TestDispatchLevelResult> {
MessageDispatchResult {
unspent_weight: Weight::from_parts(unspent_weight, 0),
dispatch_level_result: (),
}
}
/// Constructs unrewarded relayer entry from nonces range and relayer id.
pub fn unrewarded_relayer(
begin: MessageNonce,
end: MessageNonce,
relayer: TestRelayer,
) -> UnrewardedRelayer<TestRelayer> {
UnrewardedRelayer { relayer, messages: DeliveredMessages { begin, end } }
}
/// Returns unrewarded relayers state at given lane.
pub fn inbound_unrewarded_relayers_state(lane: TestLaneIdType) -> UnrewardedRelayersState {
let inbound_lane_data = crate::InboundLanes::<TestRuntime, ()>::get(lane).unwrap().0;
UnrewardedRelayersState::from(&inbound_lane_data)
}
/// Return test externalities to use in tests.
pub fn new_test_ext() -> pezsp_io::TestExternalities {
let mut t = pezframe_system::GenesisConfig::<TestRuntime>::default().build_storage().unwrap();
pezpallet_balances::GenesisConfig::<TestRuntime> {
balances: vec![(ENDOWED_ACCOUNT, 1_000_000)],
..Default::default()
}
.assimilate_storage(&mut t)
.unwrap();
pezsp_io::TestExternalities::new(t)
}
/// Run pezpallet test.
pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
new_test_ext().execute_with(|| {
crate::InboundLanes::<TestRuntime, ()>::insert(test_lane_id(), InboundLaneData::opened());
crate::OutboundLanes::<TestRuntime, ()>::insert(test_lane_id(), OutboundLaneData::opened());
crate::InboundLanes::<TestRuntime, ()>::insert(
closed_lane_id(),
InboundLaneData { state: LaneState::Closed, ..Default::default() },
);
crate::OutboundLanes::<TestRuntime, ()>::insert(
closed_lane_id(),
OutboundLaneData { state: LaneState::Closed, ..Default::default() },
);
test()
})
}
/// Prepare valid storage proof for given messages and insert appropriate header to the
/// bridged header chain.
///
/// Since this function changes the runtime storage, you can't "inline" it in the
/// `asset_noop` macro calls.
pub fn prepare_messages_proof(
messages: Vec<Message<TestLaneIdType>>,
outbound_lane_data: Option<OutboundLaneData>,
) -> Box<FromBridgedChainMessagesProof<BridgedHeaderHash, TestLaneIdType>> {
// first - let's generate storage proof
let lane = messages.first().unwrap().key.lane_id;
let nonces_start = messages.first().unwrap().key.nonce;
let nonces_end = messages.last().unwrap().key.nonce;
let (storage_root, storage_proof) =
prepare_messages_storage_proof::<BridgedChain, ThisChain, TestLaneIdType>(
lane,
nonces_start..=nonces_end,
outbound_lane_data,
UnverifiedStorageProofParams::default(),
|nonce| messages[(nonce - nonces_start) as usize].payload.clone(),
encode_all_messages,
encode_lane_data,
false,
false,
);
// let's now insert bridged chain header into the storage
let bridged_header_hash = Default::default();
pezpallet_bridge_grandpa::ImportedHeaders::<TestRuntime>::insert(
bridged_header_hash,
StoredHeaderData { number: 0, state_root: storage_root },
);
Box::new(FromBridgedChainMessagesProof::<BridgedHeaderHash, TestLaneIdType> {
bridged_header_hash,
storage_proof,
lane,
nonces_start,
nonces_end,
})
}
/// Prepare valid storage proof for given messages and insert appropriate header to the
/// bridged header chain.
///
/// Since this function changes the runtime storage, you can't "inline" it in the
/// `asset_noop` macro calls.
pub fn prepare_messages_delivery_proof(
lane: TestLaneIdType,
inbound_lane_data: InboundLaneData<AccountId>,
) -> FromBridgedChainMessagesDeliveryProof<BridgedHeaderHash, TestLaneIdType> {
// first - let's generate storage proof
let (storage_root, storage_proof) =
prepare_message_delivery_storage_proof::<BridgedChain, ThisChain, TestLaneIdType>(
lane,
inbound_lane_data,
UnverifiedStorageProofParams::default(),
);
// let's now insert bridged chain header into the storage
let bridged_header_hash = Default::default();
pezpallet_bridge_grandpa::ImportedHeaders::<TestRuntime>::insert(
bridged_header_hash,
StoredHeaderData { number: 0, state_root: storage_root },
);
FromBridgedChainMessagesDeliveryProof::<BridgedHeaderHash, TestLaneIdType> {
bridged_header_hash,
storage_proof,
lane,
}
}
@@ -0,0 +1,26 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Tests and test helpers for messages pezpallet.
#![cfg(any(feature = "test-helpers", test))]
#[cfg(test)]
pub(crate) mod mock;
#[cfg(test)]
mod pezpallet_tests;
pub mod messages_generation;
File diff suppressed because it is too large Load Diff
+530
View File
@@ -0,0 +1,530 @@
// 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/>.
//! Autogenerated weights for pezpallet_bridge_messages
//!
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2023-06-22, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `serban-ROG-Zephyrus`, CPU: `12th Gen Intel(R) Core(TM) i7-12700H`
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
// Executed Command:
// target/release/unknown-bridge-node
// benchmark
// pezpallet
// --chain=dev
// --steps=50
// --repeat=20
// --pezpallet=pezpallet_bridge_messages
// --extrinsic=*
// --execution=wasm
// --wasm-execution=Compiled
// --heap-pages=4096
// --output=./modules/messages/src/weights.rs
// --template=./.maintain/bridge-weight-template.hbs
#![allow(clippy::all)]
#![allow(unused_parens)]
#![allow(unused_imports)]
#![allow(missing_docs)]
use pezframe_support::{
traits::Get,
weights::{constants::RocksDbWeight, Weight},
};
use pezsp_std::marker::PhantomData;
/// Weight functions needed for pezpallet_bridge_messages.
pub trait WeightInfo {
fn receive_single_message_proof() -> Weight;
fn receive_n_messages_proof(n: u32) -> Weight;
fn receive_single_message_proof_with_outbound_lane_state() -> Weight;
fn receive_single_n_bytes_message_proof(n: u32) -> Weight;
fn receive_delivery_proof_for_single_message() -> Weight;
fn receive_delivery_proof_for_two_messages_by_single_relayer() -> Weight;
fn receive_delivery_proof_for_two_messages_by_two_relayers() -> Weight;
fn receive_single_n_bytes_message_proof_with_dispatch(n: u32) -> Weight;
}
/// Weights for `pezpallet_bridge_messages` that are generated using one of the Bridge testnets.
///
/// Those weights are test only and must never be used in production.
pub struct BridgeWeight<T>(PhantomData<T>);
impl<T: pezframe_system::Config> WeightInfo for BridgeWeight<T> {
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages InboundLanes (r:1 w:1)
///
/// Proof: BridgeUnknownMessages InboundLanes (max_values: None, max_size: Some(49180), added:
/// 51655, mode: MaxEncodedLen)
fn receive_single_message_proof() -> Weight {
// Proof Size summary in bytes:
// Measured: `653`
// Estimated: `52673`
// Minimum execution time: 38_724 nanoseconds.
Weight::from_parts(40_650_000, 52673)
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: BridgeRialtoMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeRialtoMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeRialtoGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeRialtoGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeRialtoMessages InboundLanes (r:1 w:1)
///
/// Proof: BridgeRialtoMessages InboundLanes (max_values: None, max_size: Some(49208), added:
/// 51683, mode: MaxEncodedLen)
///
/// The range of component `n` is `[1, 1004]`.
///
/// The range of component `n` is `[1, 1004]`.
fn receive_n_messages_proof(n: u32) -> Weight {
// Proof Size summary in bytes:
// Measured: `653`
// Estimated: `52673`
// Minimum execution time: 39_354 nanoseconds.
Weight::from_parts(29_708_543, 52673)
// Standard Error: 1_185
.saturating_add(Weight::from_parts(7_648_787, 0).saturating_mul(n.into()))
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages InboundLanes (r:1 w:1)
///
/// Proof: BridgeUnknownMessages InboundLanes (max_values: None, max_size: Some(49180), added:
/// 51655, mode: MaxEncodedLen)
fn receive_single_message_proof_with_outbound_lane_state() -> Weight {
// Proof Size summary in bytes:
// Measured: `653`
// Estimated: `52673`
// Minimum execution time: 45_578 nanoseconds.
Weight::from_parts(47_161_000, 52673)
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages InboundLanes (r:1 w:1)
///
/// Proof: BridgeUnknownMessages InboundLanes (max_values: None, max_size: Some(49180), added:
/// 51655, mode: MaxEncodedLen)
///
/// The range of component `n` is `[1, 16384]`.
fn receive_single_n_bytes_message_proof(n: u32) -> Weight {
// Proof Size summary in bytes:
// Measured: `653`
// Estimated: `52673`
// Minimum execution time: 38_702 nanoseconds.
Weight::from_parts(41_040_143, 52673)
// Standard Error: 5
.saturating_add(Weight::from_parts(1_174, 0).saturating_mul(n.into()))
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages OutboundLanes (r:1 w:1)
///
/// Proof: BridgeUnknownMessages OutboundLanes (max_values: Some(1), max_size: Some(44), added:
/// 539, mode: MaxEncodedLen)
///
/// Storage: BridgeRelayers RelayerRewards (r:1 w:1)
///
/// Proof: BridgeRelayers RelayerRewards (max_values: None, max_size: Some(93), added: 2568,
/// mode: MaxEncodedLen)
///
/// Storage: BridgeRialtoMessages OutboundMessages (r:0 w:1)
///
/// Proof: BridgeRialtoMessages OutboundMessages (max_values: None, max_size: Some(65596),
/// added: 68071, mode: MaxEncodedLen)
fn receive_delivery_proof_for_single_message() -> Weight {
// Proof Size summary in bytes:
// Measured: `701`
// Estimated: `3558`
// Minimum execution time: 37_197 nanoseconds.
Weight::from_parts(38_371_000, 3558)
.saturating_add(T::DbWeight::get().reads(4_u64))
.saturating_add(T::DbWeight::get().writes(3_u64))
}
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages OutboundLanes (r:1 w:1)
///
/// Proof: BridgeUnknownMessages OutboundLanes (max_values: Some(1), max_size: Some(44), added:
/// 539, mode: MaxEncodedLen)
///
/// Storage: BridgeRelayers RelayerRewards (r:1 w:1)
///
/// Proof: BridgeRelayers RelayerRewards (max_values: None, max_size: Some(93), added: 2568,
/// mode: MaxEncodedLen)
///
/// Storage: BridgeRialtoMessages OutboundMessages (r:0 w:2)
///
/// Proof: BridgeRialtoMessages OutboundMessages (max_values: None, max_size: Some(65596),
/// added: 68071, mode: MaxEncodedLen)
fn receive_delivery_proof_for_two_messages_by_single_relayer() -> Weight {
// Proof Size summary in bytes:
// Measured: `701`
// Estimated: `3558`
// Minimum execution time: 38_684 nanoseconds.
Weight::from_parts(39_929_000, 3558)
.saturating_add(T::DbWeight::get().reads(4_u64))
.saturating_add(T::DbWeight::get().writes(4_u64))
}
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages OutboundLanes (r:1 w:1)
///
/// Proof: BridgeUnknownMessages OutboundLanes (max_values: Some(1), max_size: Some(44), added:
/// 539, mode: MaxEncodedLen)
///
/// Storage: BridgeRelayers RelayerRewards (r:2 w:2)
///
/// Proof: BridgeRelayers RelayerRewards (max_values: None, max_size: Some(93), added: 2568,
/// mode: MaxEncodedLen)
///
/// Storage: BridgeRialtoMessages OutboundMessages (r:0 w:2)
///
/// Proof: BridgeRialtoMessages OutboundMessages (max_values: None, max_size: Some(65596),
/// added: 68071, mode: MaxEncodedLen)
fn receive_delivery_proof_for_two_messages_by_two_relayers() -> Weight {
// Proof Size summary in bytes:
// Measured: `701`
// Estimated: `6126`
// Minimum execution time: 41_363 nanoseconds.
Weight::from_parts(42_621_000, 6126)
.saturating_add(T::DbWeight::get().reads(5_u64))
.saturating_add(T::DbWeight::get().writes(5_u64))
}
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages InboundLanes (r:1 w:1)
///
/// Proof: BridgeUnknownMessages InboundLanes (max_values: None, max_size: Some(49180), added:
/// 51655, mode: MaxEncodedLen)
///
/// The range of component `n` is `[1, 16384]`.
fn receive_single_n_bytes_message_proof_with_dispatch(n: u32) -> Weight {
// Proof Size summary in bytes:
// Measured: `653`
// Estimated: `52673`
// Minimum execution time: 38_925 nanoseconds.
Weight::from_parts(39_617_000, 52673)
// Standard Error: 612
.saturating_add(Weight::from_parts(372_813, 0).saturating_mul(n.into()))
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
}
// For backwards compatibility and tests
impl WeightInfo for () {
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages InboundLanes (r:1 w:1)
///
/// Proof: BridgeUnknownMessages InboundLanes (max_values: None, max_size: Some(49180), added:
/// 51655, mode: MaxEncodedLen)
fn receive_single_message_proof() -> Weight {
// Proof Size summary in bytes:
// Measured: `653`
// Estimated: `52673`
// Minimum execution time: 38_724 nanoseconds.
Weight::from_parts(40_650_000, 52673)
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: BridgeRialtoMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeRialtoMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeRialtoGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeRialtoGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeRialtoMessages InboundLanes (r:1 w:1)
///
/// Proof: BridgeRialtoMessages InboundLanes (max_values: None, max_size: Some(49208), added:
/// 51683, mode: MaxEncodedLen)
///
/// The range of component `n` is `[1, 1004]`.
///
/// The range of component `n` is `[1, 1004]`.
fn receive_n_messages_proof(n: u32) -> Weight {
// Proof Size summary in bytes:
// Measured: `653`
// Estimated: `52673`
// Minimum execution time: 39_354 nanoseconds.
Weight::from_parts(29_708_543, 52673)
// Standard Error: 1_185
.saturating_add(Weight::from_parts(7_648_787, 0).saturating_mul(n.into()))
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages InboundLanes (r:1 w:1)
///
/// Proof: BridgeUnknownMessages InboundLanes (max_values: None, max_size: Some(49180), added:
/// 51655, mode: MaxEncodedLen)
fn receive_single_message_proof_with_outbound_lane_state() -> Weight {
// Proof Size summary in bytes:
// Measured: `653`
// Estimated: `52673`
// Minimum execution time: 45_578 nanoseconds.
Weight::from_parts(47_161_000, 52673)
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages InboundLanes (r:1 w:1)
///
/// Proof: BridgeRialtoMessages InboundLanes (max_values: None, max_size: Some(49208), added:
/// 51683, mode: MaxEncodedLen)
///
/// The range of component `n` is `[1, 16384]`.
///
/// The range of component `n` is `[1, 16384]`.
fn receive_single_n_bytes_message_proof(n: u32) -> Weight {
// Proof Size summary in bytes:
// Measured: `653`
// Estimated: `52673`
// Minimum execution time: 38_702 nanoseconds.
Weight::from_parts(41_040_143, 52673)
// Standard Error: 5
.saturating_add(Weight::from_parts(1_174, 0).saturating_mul(n.into()))
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages OutboundLanes (r:1 w:1)
///
/// Proof: BridgeUnknownMessages OutboundLanes (max_values: Some(1), max_size: Some(44), added:
/// 539, mode: MaxEncodedLen)
///
/// Storage: BridgeRelayers RelayerRewards (r:1 w:1)
///
/// Proof: BridgeRelayers RelayerRewards (max_values: None, max_size: Some(93), added: 2568,
/// mode: MaxEncodedLen)
///
/// Storage: BridgeRialtoMessages OutboundMessages (r:0 w:1)
///
/// Proof: BridgeRialtoMessages OutboundMessages (max_values: None, max_size: Some(65596),
/// added: 68071, mode: MaxEncodedLen)
fn receive_delivery_proof_for_single_message() -> Weight {
// Proof Size summary in bytes:
// Measured: `701`
// Estimated: `3558`
// Minimum execution time: 37_197 nanoseconds.
Weight::from_parts(38_371_000, 3558)
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages OutboundLanes (r:1 w:1)
///
/// Proof: BridgeUnknownMessages OutboundLanes (max_values: Some(1), max_size: Some(44), added:
/// 539, mode: MaxEncodedLen)
///
/// Storage: BridgeRelayers RelayerRewards (r:1 w:1)
///
/// Proof: BridgeRelayers RelayerRewards (max_values: None, max_size: Some(93), added: 2568,
/// mode: MaxEncodedLen)
///
/// Storage: BridgeRialtoMessages OutboundMessages (r:0 w:2)
///
/// Proof: BridgeRialtoMessages OutboundMessages (max_values: None, max_size: Some(65596),
/// added: 68071, mode: MaxEncodedLen)
fn receive_delivery_proof_for_two_messages_by_single_relayer() -> Weight {
// Proof Size summary in bytes:
// Measured: `701`
// Estimated: `3558`
// Minimum execution time: 38_684 nanoseconds.
Weight::from_parts(39_929_000, 3558)
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(4_u64))
}
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages OutboundLanes (r:1 w:1)
///
/// Proof: BridgeUnknownMessages OutboundLanes (max_values: Some(1), max_size: Some(44), added:
/// 539, mode: MaxEncodedLen)
///
/// Storage: BridgeRelayers RelayerRewards (r:2 w:2)
///
/// Proof: BridgeRelayers RelayerRewards (max_values: None, max_size: Some(93), added: 2568,
/// mode: MaxEncodedLen)
///
/// Storage: BridgeRialtoMessages OutboundMessages (r:0 w:2)
///
/// Proof: BridgeRialtoMessages OutboundMessages (max_values: None, max_size: Some(65596),
/// added: 68071, mode: MaxEncodedLen)
fn receive_delivery_proof_for_two_messages_by_two_relayers() -> Weight {
// Proof Size summary in bytes:
// Measured: `701`
// Estimated: `6126`
// Minimum execution time: 41_363 nanoseconds.
Weight::from_parts(42_621_000, 6126)
.saturating_add(RocksDbWeight::get().reads(5_u64))
.saturating_add(RocksDbWeight::get().writes(5_u64))
}
/// Storage: BridgeUnknownMessages PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownMessages PalletOperatingMode (max_values: Some(1), max_size: Some(2),
/// added: 497, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownMessages InboundLanes (r:1 w:1)
///
/// Proof: BridgeUnknownMessages InboundLanes (max_values: None, max_size: Some(49180), added:
/// 51655, mode: MaxEncodedLen)
///
/// The range of component `n` is `[1, 16384]`.
fn receive_single_n_bytes_message_proof_with_dispatch(n: u32) -> Weight {
// Proof Size summary in bytes:
// Measured: `653`
// Estimated: `52673`
// Minimum execution time: 38_925 nanoseconds.
Weight::from_parts(39_617_000, 52673)
// Standard Error: 612
.saturating_add(Weight::from_parts(372_813, 0).saturating_mul(n.into()))
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
}
@@ -0,0 +1,470 @@
// 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/>.
//! Weight-related utilities.
use crate::weights::WeightInfo;
use bp_messages::{MessageNonce, UnrewardedRelayersState};
use pezbp_runtime::{PreComputedSize, Size};
use pezframe_support::weights::Weight;
/// Size of the message being delivered in benchmarks.
pub const EXPECTED_DEFAULT_MESSAGE_LENGTH: u32 = 128;
/// We assume that size of signed extensions on all our chains and size of all 'small' arguments of
/// calls we're checking here would fit 1KB.
const SIGNED_EXTENSIONS_SIZE: u32 = 1024;
/// Number of extra bytes (excluding size of storage value itself) of storage proof.
/// This mostly depends on number of entries (and their density) in the storage trie.
/// Some reserve is reserved to account future chain growth.
pub const EXTRA_STORAGE_PROOF_SIZE: u32 = 1024;
/// Ensure that weights from `WeightInfoExt` implementation are looking correct.
pub fn ensure_weights_are_correct<W: WeightInfoExt>() {
// all components of weight formulae must have zero `proof_size`, because the `proof_size` is
// benchmarked using `MaxEncodedLen` approach and there are no components that cause additional
// db reads
// W::receive_messages_proof_outbound_lane_state_overhead().ref_time() may be zero because:
// the outbound lane state processing code (`InboundLane::receive_state_update`) is minimal and
// may not be accounted by our benchmarks
assert_eq!(W::receive_messages_proof_outbound_lane_state_overhead().proof_size(), 0);
assert_ne!(W::storage_proof_size_overhead(1).ref_time(), 0);
assert_eq!(W::storage_proof_size_overhead(1).proof_size(), 0);
// verify `receive_messages_delivery_proof` weight components
assert_ne!(W::receive_messages_delivery_proof_overhead().ref_time(), 0);
assert_ne!(W::receive_messages_delivery_proof_overhead().proof_size(), 0);
// W::receive_messages_delivery_proof_messages_overhead(1).ref_time() may be zero because:
// there's no code that iterates over confirmed messages in confirmation transaction
assert_eq!(W::receive_messages_delivery_proof_messages_overhead(1).proof_size(), 0);
// W::receive_messages_delivery_proof_relayers_overhead(1).ref_time() may be zero because:
// runtime **can** choose not to pay any rewards to relayers
// W::receive_messages_delivery_proof_relayers_overhead(1).proof_size() is an exception
// it may or may not cause additional db reads, so proof size may vary
assert_ne!(W::storage_proof_size_overhead(1).ref_time(), 0);
assert_eq!(W::storage_proof_size_overhead(1).proof_size(), 0);
// verify `receive_message_proof` weight
let receive_messages_proof_weight =
W::receive_messages_proof_weight(&PreComputedSize(1), 10, Weight::zero());
assert_ne!(receive_messages_proof_weight.ref_time(), 0);
assert_ne!(receive_messages_proof_weight.proof_size(), 0);
messages_proof_size_does_not_affect_proof_size::<W>();
messages_count_does_not_affect_proof_size::<W>();
// verify `receive_message_proof` weight
let receive_messages_delivery_proof_weight = W::receive_messages_delivery_proof_weight(
&PreComputedSize(1),
&UnrewardedRelayersState::default(),
);
assert_ne!(receive_messages_delivery_proof_weight.ref_time(), 0);
assert_ne!(receive_messages_delivery_proof_weight.proof_size(), 0);
messages_delivery_proof_size_does_not_affect_proof_size::<W>();
total_messages_in_delivery_proof_does_not_affect_proof_size::<W>();
}
/// Ensure that we are able to dispatch maximal size messages.
pub fn ensure_maximal_message_dispatch<W: WeightInfoExt>(
max_incoming_message_size: u32,
max_incoming_message_dispatch_weight: Weight,
) {
let message_dispatch_weight = W::message_dispatch_weight(max_incoming_message_size);
assert!(
message_dispatch_weight.all_lte(max_incoming_message_dispatch_weight),
"Dispatch weight of maximal message {message_dispatch_weight:?} must be lower \
than the hardcoded {max_incoming_message_dispatch_weight:?}",
);
}
/// Ensure that we're able to receive maximal (by-size and by-weight) message from other chain.
pub fn ensure_able_to_receive_message<W: WeightInfoExt>(
max_extrinsic_size: u32,
max_extrinsic_weight: Weight,
max_incoming_message_proof_size: u32,
max_incoming_message_dispatch_weight: Weight,
) {
// verify that we're able to receive proof of maximal-size message
let max_delivery_transaction_size =
max_incoming_message_proof_size.saturating_add(SIGNED_EXTENSIONS_SIZE);
assert!(
max_delivery_transaction_size <= max_extrinsic_size,
"Size of maximal message delivery transaction {max_incoming_message_proof_size} + \
{SIGNED_EXTENSIONS_SIZE} is larger than maximal possible transaction size {max_extrinsic_size}",
);
// verify that we're able to receive proof of maximal-size message with maximal dispatch weight
let max_delivery_transaction_dispatch_weight = W::receive_messages_proof_weight(
&PreComputedSize(
(max_incoming_message_proof_size + W::expected_extra_storage_proof_size()) as usize,
),
1,
max_incoming_message_dispatch_weight,
);
assert!(
max_delivery_transaction_dispatch_weight.all_lte(max_extrinsic_weight),
"Weight of maximal message delivery transaction + {max_delivery_transaction_dispatch_weight} is larger than maximal possible transaction weight {max_extrinsic_weight}",
);
}
/// Ensure that we're able to receive maximal confirmation from other chain.
pub fn ensure_able_to_receive_confirmation<W: WeightInfoExt>(
max_extrinsic_size: u32,
max_extrinsic_weight: Weight,
max_inbound_lane_data_proof_size_from_peer_chain: u32,
max_unrewarded_relayer_entries_at_peer_inbound_lane: MessageNonce,
max_unconfirmed_messages_at_inbound_lane: MessageNonce,
) {
// verify that we're able to receive confirmation of maximal-size
let max_confirmation_transaction_size =
max_inbound_lane_data_proof_size_from_peer_chain.saturating_add(SIGNED_EXTENSIONS_SIZE);
assert!(
max_confirmation_transaction_size <= max_extrinsic_size,
"Size of maximal message delivery confirmation transaction {max_inbound_lane_data_proof_size_from_peer_chain} + {SIGNED_EXTENSIONS_SIZE} is larger than maximal possible transaction size {max_extrinsic_size}",
);
// verify that we're able to reward maximal number of relayers that have delivered maximal
// number of messages
let max_confirmation_transaction_dispatch_weight = W::receive_messages_delivery_proof_weight(
&PreComputedSize(max_inbound_lane_data_proof_size_from_peer_chain as usize),
&UnrewardedRelayersState {
unrewarded_relayer_entries: max_unrewarded_relayer_entries_at_peer_inbound_lane,
total_messages: max_unconfirmed_messages_at_inbound_lane,
..Default::default()
},
);
assert!(
max_confirmation_transaction_dispatch_weight.all_lte(max_extrinsic_weight),
"Weight of maximal confirmation transaction {max_confirmation_transaction_dispatch_weight} is larger than maximal possible transaction weight {max_extrinsic_weight}",
);
}
/// Panics if `proof_size` of message delivery call depends on the message proof size.
fn messages_proof_size_does_not_affect_proof_size<W: WeightInfoExt>() {
let dispatch_weight = Weight::zero();
let weight_when_proof_size_is_8k =
W::receive_messages_proof_weight(&PreComputedSize(8 * 1024), 1, dispatch_weight);
let weight_when_proof_size_is_16k =
W::receive_messages_proof_weight(&PreComputedSize(16 * 1024), 1, dispatch_weight);
ensure_weight_components_are_not_zero(weight_when_proof_size_is_8k);
ensure_weight_components_are_not_zero(weight_when_proof_size_is_16k);
ensure_proof_size_is_the_same(
weight_when_proof_size_is_8k,
weight_when_proof_size_is_16k,
"Messages proof size does not affect values that we read from our storage",
);
}
/// Panics if `proof_size` of message delivery call depends on the messages count.
///
/// In practice, it will depend on the messages count, because most probably every
/// message will read something from db during dispatch. But this must be accounted
/// by the `dispatch_weight`.
fn messages_count_does_not_affect_proof_size<W: WeightInfoExt>() {
let messages_proof_size = PreComputedSize(8 * 1024);
let dispatch_weight = Weight::zero();
let weight_of_one_incoming_message =
W::receive_messages_proof_weight(&messages_proof_size, 1, dispatch_weight);
let weight_of_two_incoming_messages =
W::receive_messages_proof_weight(&messages_proof_size, 2, dispatch_weight);
ensure_weight_components_are_not_zero(weight_of_one_incoming_message);
ensure_weight_components_are_not_zero(weight_of_two_incoming_messages);
ensure_proof_size_is_the_same(
weight_of_one_incoming_message,
weight_of_two_incoming_messages,
"Number of same-lane incoming messages does not affect values that we read from our storage",
);
}
/// Panics if `proof_size` of delivery confirmation call depends on the delivery proof size.
fn messages_delivery_proof_size_does_not_affect_proof_size<W: WeightInfoExt>() {
let relayers_state = UnrewardedRelayersState {
unrewarded_relayer_entries: 1,
messages_in_oldest_entry: 1,
total_messages: 1,
last_delivered_nonce: 1,
};
let weight_when_proof_size_is_8k =
W::receive_messages_delivery_proof_weight(&PreComputedSize(8 * 1024), &relayers_state);
let weight_when_proof_size_is_16k =
W::receive_messages_delivery_proof_weight(&PreComputedSize(16 * 1024), &relayers_state);
ensure_weight_components_are_not_zero(weight_when_proof_size_is_8k);
ensure_weight_components_are_not_zero(weight_when_proof_size_is_16k);
ensure_proof_size_is_the_same(
weight_when_proof_size_is_8k,
weight_when_proof_size_is_16k,
"Messages delivery proof size does not affect values that we read from our storage",
);
}
/// Panics if `proof_size` of delivery confirmation call depends on the number of confirmed
/// messages.
fn total_messages_in_delivery_proof_does_not_affect_proof_size<W: WeightInfoExt>() {
let proof_size = PreComputedSize(8 * 1024);
let weight_when_1k_messages_confirmed = W::receive_messages_delivery_proof_weight(
&proof_size,
&UnrewardedRelayersState {
unrewarded_relayer_entries: 1,
messages_in_oldest_entry: 1,
total_messages: 1024,
last_delivered_nonce: 1,
},
);
let weight_when_2k_messages_confirmed = W::receive_messages_delivery_proof_weight(
&proof_size,
&UnrewardedRelayersState {
unrewarded_relayer_entries: 1,
messages_in_oldest_entry: 1,
total_messages: 2048,
last_delivered_nonce: 1,
},
);
ensure_weight_components_are_not_zero(weight_when_1k_messages_confirmed);
ensure_weight_components_are_not_zero(weight_when_2k_messages_confirmed);
ensure_proof_size_is_the_same(
weight_when_1k_messages_confirmed,
weight_when_2k_messages_confirmed,
"More messages in delivery proof does not affect values that we read from our storage",
);
}
/// Panics if either Weight' `proof_size` or `ref_time` are zero.
fn ensure_weight_components_are_not_zero(weight: Weight) {
assert_ne!(weight.ref_time(), 0);
assert_ne!(weight.proof_size(), 0);
}
/// Panics if `proof_size` of `weight1` is not equal to `proof_size` of `weight2`.
fn ensure_proof_size_is_the_same(weight1: Weight, weight2: Weight, msg: &str) {
assert_eq!(
weight1.proof_size(),
weight2.proof_size(),
"{msg}: {} must be equal to {}",
weight1.proof_size(),
weight2.proof_size(),
);
}
/// Extended weight info.
pub trait WeightInfoExt: WeightInfo {
/// Size of proof that is already included in the single message delivery weight.
///
/// The message submitter (at source chain) has already covered this cost. But there are two
/// factors that may increase proof size: (1) the message size may be larger than predefined
/// and (2) relayer may add extra trie nodes to the proof. So if proof size is larger than
/// this value, we're going to charge relayer for that.
fn expected_extra_storage_proof_size() -> u32;
// Our configuration assumes that the runtime has special signed extensions used to:
//
// 1) reject obsolete delivery and confirmation transactions;
//
// 2) refund transaction cost to relayer and register his rewards.
//
// The checks in (1) are trivial, so its computation weight may be ignored. And we only touch
// storage values that are read during the call. So we may ignore the weight of this check.
//
// However, during (2) we read and update storage values of other pallets
// (`pezpallet-bridge-relayers` and balances/assets pezpallet). So we need to add this weight to the
// weight of our call. Hence two following methods.
/// Extra weight that is added to the `receive_messages_proof` call weight by signed extensions
/// that are declared at runtime level.
fn receive_messages_proof_overhead_from_runtime() -> Weight;
/// Extra weight that is added to the `receive_messages_delivery_proof` call weight by signed
/// extensions that are declared at runtime level.
fn receive_messages_delivery_proof_overhead_from_runtime() -> Weight;
// Functions that are directly mapped to extrinsics weights.
/// Weight of message delivery extrinsic.
fn receive_messages_proof_weight(
proof: &impl Size,
messages_count: u32,
dispatch_weight: Weight,
) -> Weight {
// basic components of extrinsic weight
let base_weight = Self::receive_n_messages_proof(messages_count);
let transaction_overhead_from_runtime =
Self::receive_messages_proof_overhead_from_runtime();
let outbound_state_delivery_weight =
Self::receive_messages_proof_outbound_lane_state_overhead();
let messages_dispatch_weight = dispatch_weight;
// proof size overhead weight
let expected_proof_size = EXPECTED_DEFAULT_MESSAGE_LENGTH
.saturating_mul(messages_count.saturating_sub(1))
.saturating_add(Self::expected_extra_storage_proof_size());
let actual_proof_size = proof.size();
let proof_size_overhead = Self::storage_proof_size_overhead(
actual_proof_size.saturating_sub(expected_proof_size),
);
base_weight
.saturating_add(transaction_overhead_from_runtime)
.saturating_add(outbound_state_delivery_weight)
.saturating_add(messages_dispatch_weight)
.saturating_add(proof_size_overhead)
}
/// Weight of confirmation delivery extrinsic.
fn receive_messages_delivery_proof_weight(
proof: &impl Size,
relayers_state: &UnrewardedRelayersState,
) -> Weight {
// basic components of extrinsic weight
let transaction_overhead = Self::receive_messages_delivery_proof_overhead();
let transaction_overhead_from_runtime =
Self::receive_messages_delivery_proof_overhead_from_runtime();
let messages_overhead =
Self::receive_messages_delivery_proof_messages_overhead(relayers_state.total_messages);
let relayers_overhead = Self::receive_messages_delivery_proof_relayers_overhead(
relayers_state.unrewarded_relayer_entries,
);
// proof size overhead weight
let expected_proof_size = Self::expected_extra_storage_proof_size();
let actual_proof_size = proof.size();
let proof_size_overhead = Self::storage_proof_size_overhead(
actual_proof_size.saturating_sub(expected_proof_size),
);
transaction_overhead
.saturating_add(transaction_overhead_from_runtime)
.saturating_add(messages_overhead)
.saturating_add(relayers_overhead)
.saturating_add(proof_size_overhead)
}
// Functions that are used by extrinsics weights formulas.
/// Returns weight that needs to be accounted when message delivery transaction
/// (`receive_messages_proof`) is carrying outbound lane state proof.
fn receive_messages_proof_outbound_lane_state_overhead() -> Weight {
let weight_of_single_message_and_lane_state =
Self::receive_single_message_proof_with_outbound_lane_state();
let weight_of_single_message = Self::receive_single_message_proof();
weight_of_single_message_and_lane_state.saturating_sub(weight_of_single_message)
}
/// Returns weight overhead of delivery confirmation transaction
/// (`receive_messages_delivery_proof`).
fn receive_messages_delivery_proof_overhead() -> Weight {
let weight_of_two_messages_and_two_tx_overheads =
Self::receive_delivery_proof_for_single_message().saturating_mul(2);
let weight_of_two_messages_and_single_tx_overhead =
Self::receive_delivery_proof_for_two_messages_by_single_relayer();
weight_of_two_messages_and_two_tx_overheads
.saturating_sub(weight_of_two_messages_and_single_tx_overhead)
}
/// Returns weight that needs to be accounted when receiving confirmations for given a number of
/// messages with delivery confirmation transaction (`receive_messages_delivery_proof`).
fn receive_messages_delivery_proof_messages_overhead(messages: MessageNonce) -> Weight {
let weight_of_two_messages =
Self::receive_delivery_proof_for_two_messages_by_single_relayer();
let weight_of_single_message = Self::receive_delivery_proof_for_single_message();
weight_of_two_messages
.saturating_sub(weight_of_single_message)
.saturating_mul(messages as _)
}
/// Returns weight that needs to be accounted when receiving confirmations for given a number of
/// relayers entries with delivery confirmation transaction (`receive_messages_delivery_proof`).
fn receive_messages_delivery_proof_relayers_overhead(relayers: MessageNonce) -> Weight {
let weight_of_two_messages_by_two_relayers =
Self::receive_delivery_proof_for_two_messages_by_two_relayers();
let weight_of_two_messages_by_single_relayer =
Self::receive_delivery_proof_for_two_messages_by_single_relayer();
weight_of_two_messages_by_two_relayers
.saturating_sub(weight_of_two_messages_by_single_relayer)
.saturating_mul(relayers as _)
}
/// Returns weight that needs to be accounted when storage proof of given size is received
/// (either in `receive_messages_proof` or `receive_messages_delivery_proof`).
///
/// **IMPORTANT**: this overhead is already included in the 'base' transaction cost - e.g. proof
/// size depends on messages count or number of entries in the unrewarded relayers set. So this
/// shouldn't be added to cost of transaction, but instead should act as a minimal cost that the
/// relayer must pay when it relays proof of given size (even if cost based on other parameters
/// is less than that cost).
fn storage_proof_size_overhead(proof_size: u32) -> Weight {
let proof_size_in_bytes = proof_size;
let byte_weight = Self::receive_single_n_bytes_message_proof(2) -
Self::receive_single_n_bytes_message_proof(1);
proof_size_in_bytes * byte_weight
}
// Functions that may be used by runtime developers.
/// Returns dispatch weight of message of given size.
///
/// This function would return correct value only if your runtime is configured to run
/// `receive_single_message_proof_with_dispatch` benchmark. See its requirements for
/// details.
fn message_dispatch_weight(message_size: u32) -> Weight {
let message_size_in_bytes = message_size;
Self::receive_single_n_bytes_message_proof_with_dispatch(message_size_in_bytes)
.saturating_sub(Self::receive_single_n_bytes_message_proof(message_size_in_bytes))
}
}
impl WeightInfoExt for () {
fn expected_extra_storage_proof_size() -> u32 {
EXTRA_STORAGE_PROOF_SIZE
}
fn receive_messages_proof_overhead_from_runtime() -> Weight {
Weight::zero()
}
fn receive_messages_delivery_proof_overhead_from_runtime() -> Weight {
Weight::zero()
}
}
impl<T: pezframe_system::Config> WeightInfoExt for crate::weights::BridgeWeight<T> {
fn expected_extra_storage_proof_size() -> u32 {
EXTRA_STORAGE_PROOF_SIZE
}
fn receive_messages_proof_overhead_from_runtime() -> Weight {
Weight::zero()
}
fn receive_messages_delivery_proof_overhead_from_runtime() -> Weight {
Weight::zero()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{tests::mock::TestRuntime, weights::BridgeWeight};
#[test]
fn ensure_default_weights_are_correct() {
ensure_weights_are_correct::<BridgeWeight<TestRuntime>>();
}
}
+103
View File
@@ -0,0 +1,103 @@
[package]
name = "pezpallet-bridge-relayers"
description = "Module used to store relayer rewards and coordinate relayers set."
version = "0.7.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
repository.workspace = true
documentation = "https://docs.rs/pezpallet-bridge-relayers"
homepage = { workspace = true }
[lints]
workspace = true
[dependencies]
codec = { workspace = true }
scale-info = { features = ["derive"], workspace = true }
tracing = { workspace = true }
# Bridge dependencies
bp-header-pez-chain = { workspace = true }
bp-messages = { workspace = true }
bp-relayers = { workspace = true }
pezbp-runtime = { workspace = true }
pezpallet-bridge-grandpa = { workspace = true }
pezpallet-bridge-messages = { workspace = true }
pezpallet-bridge-teyrchains = { workspace = true }
# Bizinikiwi Dependencies
pezframe-benchmarking = { optional = true, workspace = true }
pezframe-support = { workspace = true }
pezframe-system = { workspace = true }
pezpallet-transaction-payment = { workspace = true }
pezsp-arithmetic = { workspace = true }
pezsp-runtime = { workspace = true }
[dev-dependencies]
bp-pezkuwi-core = { workspace = true }
bp-test-utils = { workspace = true }
bp-teyrchains = { workspace = true }
pezpallet-balances = { workspace = true, default-features = true }
pezpallet-utility = { workspace = true }
pezsp-core = { workspace = true }
pezsp-io = { workspace = true }
[features]
default = ["std"]
std = [
"bp-header-pez-chain/std",
"bp-messages/std",
"bp-pezkuwi-core/std",
"bp-relayers/std",
"pezbp-runtime/std",
"bp-test-utils/std",
"bp-teyrchains/std",
"codec/std",
"pezframe-benchmarking/std",
"pezframe-support/std",
"pezframe-system/std",
"pezpallet-bridge-grandpa/std",
"pezpallet-bridge-messages/std",
"pezpallet-bridge-teyrchains/std",
"pezpallet-transaction-payment/std",
"pezpallet-utility/std",
"scale-info/std",
"pezsp-arithmetic/std",
"pezsp-core/std",
"pezsp-io/std",
"pezsp-runtime/std",
"tracing/std",
]
runtime-benchmarks = [
"bp-header-pez-chain/runtime-benchmarks",
"bp-messages/runtime-benchmarks",
"bp-pezkuwi-core/runtime-benchmarks",
"bp-relayers/runtime-benchmarks",
"pezbp-runtime/runtime-benchmarks",
"bp-test-utils/runtime-benchmarks",
"bp-teyrchains/runtime-benchmarks",
"pezframe-benchmarking/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezpallet-balances/runtime-benchmarks",
"pezpallet-bridge-grandpa/runtime-benchmarks",
"pezpallet-bridge-messages/runtime-benchmarks",
"pezpallet-bridge-teyrchains/runtime-benchmarks",
"pezpallet-transaction-payment/runtime-benchmarks",
"pezpallet-utility/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
try-runtime = [
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezpallet-balances/try-runtime",
"pezpallet-bridge-grandpa/try-runtime",
"pezpallet-bridge-messages/try-runtime",
"pezpallet-bridge-teyrchains/try-runtime",
"pezpallet-transaction-payment/try-runtime",
"pezpallet-utility/try-runtime",
"pezsp-runtime/try-runtime",
]
integrity-test = []
+14
View File
@@ -0,0 +1,14 @@
# Bridge Relayers Pezpallet
The pezpallet serves as a storage for pending bridge relayer rewards. Any runtime component may register reward
to some relayer for doing some useful job at some messages lane. Later, the relayer may claim its rewards
using the `claim_rewards` call.
The reward payment procedure is abstracted from the pezpallet code. One of possible implementations, is the
[`PayLaneRewardFromAccount`](../../primitives/relayers/src/lib.rs), which just does a `Currency::transfer`
call to relayer account from the relayer-rewards account, determined by the message lane id.
We have two examples of how this pezpallet is used in production. Rewards are registered at the target chain to
compensate fees of message delivery transactions (and linked finality delivery calls). At the source chain, rewards
are registered during delivery confirmation transactions. You may find more information about that in the
[Kusama <> PezkuwiChain bridge](../../docs/pezkuwi-kusama-bridge-overview.md) documentation.
@@ -0,0 +1,198 @@
// 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/>.
//! Benchmarks for the relayers Pezpallet.
#![cfg(feature = "runtime-benchmarks")]
use crate::*;
use pezframe_benchmarking::v2::*;
use pezframe_support::{assert_ok, weights::Weight};
use pezframe_system::RawOrigin;
use pezsp_runtime::traits::One;
/// Reward amount that is (hopefully) is larger than existential deposit across all chains.
const REWARD_AMOUNT: u32 = u32::MAX;
/// Pezpallet we're benchmarking here.
pub struct Pezpallet<T: Config<I>, I: 'static = ()>(crate::Pezpallet<T, I>);
/// Trait that must be implemented by runtime.
pub trait Config<I: 'static = ()>: crate::Config<I> {
/// `T::Reward` to use in benchmarks.
fn bench_reward() -> Self::Reward;
/// Prepare environment for paying given reward for serving given lane.
fn prepare_rewards_account(
reward_kind: Self::Reward,
reward: Self::RewardBalance,
) -> Option<BeneficiaryOf<Self, I>>;
/// Give enough balance to given account.
fn deposit_account(account: Self::AccountId, balance: Self::Balance);
}
fn assert_last_event<T: Config<I>, I: 'static>(
generic_event: <T as pezpallet::Config<I>>::RuntimeEvent,
) {
pezframe_system::Pezpallet::<T>::assert_last_event(generic_event.into());
}
#[instance_benchmarks(
where
BeneficiaryOf<T, I>: From<<T as pezframe_system::Config>::AccountId>,
)]
mod benchmarks {
use super::*;
#[benchmark]
fn claim_rewards() {
let relayer: T::AccountId = whitelisted_caller();
let reward_kind = T::bench_reward();
let reward_balance = T::RewardBalance::from(REWARD_AMOUNT);
let _ = T::prepare_rewards_account(reward_kind, reward_balance);
RelayerRewards::<T, I>::insert(&relayer, reward_kind, reward_balance);
#[extrinsic_call]
_(RawOrigin::Signed(relayer.clone()), reward_kind);
// we can't check anything here, because `PaymentProcedure` is responsible for
// payment logic, so we assume that if call has succeeded, the procedure has
// also completed successfully
assert_last_event::<T, I>(
Event::RewardPaid {
relayer: relayer.clone(),
reward_kind,
reward_balance,
beneficiary: relayer.into(),
}
.into(),
);
}
#[benchmark]
fn claim_rewards_to() -> Result<(), BenchmarkError> {
let relayer: T::AccountId = whitelisted_caller();
let reward_kind = T::bench_reward();
let reward_balance = T::RewardBalance::from(REWARD_AMOUNT);
let Some(alternative_beneficiary) = T::prepare_rewards_account(reward_kind, reward_balance)
else {
return Err(BenchmarkError::Override(BenchmarkResult::from_weight(Weight::MAX)));
};
RelayerRewards::<T, I>::insert(&relayer, reward_kind, reward_balance);
#[extrinsic_call]
_(RawOrigin::Signed(relayer.clone()), reward_kind, alternative_beneficiary.clone());
// we can't check anything here, because `PaymentProcedure` is responsible for
// payment logic, so we assume that if call has succeeded, the procedure has
// also completed successfully
assert_last_event::<T, I>(
Event::RewardPaid {
relayer: relayer.clone(),
reward_kind,
reward_balance,
beneficiary: alternative_beneficiary,
}
.into(),
);
Ok(())
}
#[benchmark]
fn register() {
let relayer: T::AccountId = whitelisted_caller();
let valid_till = pezframe_system::Pezpallet::<T>::block_number()
.saturating_add(crate::Pezpallet::<T, I>::required_registration_lease())
.saturating_add(One::one())
.saturating_add(One::one());
T::deposit_account(relayer.clone(), crate::Pezpallet::<T, I>::required_stake());
#[extrinsic_call]
_(RawOrigin::Signed(relayer.clone()), valid_till);
assert!(crate::Pezpallet::<T, I>::is_registration_active(&relayer));
}
#[benchmark]
fn deregister() {
let relayer: T::AccountId = whitelisted_caller();
let valid_till = pezframe_system::Pezpallet::<T>::block_number()
.saturating_add(crate::Pezpallet::<T, I>::required_registration_lease())
.saturating_add(One::one())
.saturating_add(One::one());
T::deposit_account(relayer.clone(), crate::Pezpallet::<T, I>::required_stake());
crate::Pezpallet::<T, I>::register(RawOrigin::Signed(relayer.clone()).into(), valid_till)
.unwrap();
pezframe_system::Pezpallet::<T>::set_block_number(valid_till.saturating_add(One::one()));
#[extrinsic_call]
_(RawOrigin::Signed(relayer.clone()));
assert!(!crate::Pezpallet::<T, I>::is_registration_active(&relayer));
}
// Benchmark `slash_and_deregister` method of the pezpallet. We are adding this weight to
// the weight of message delivery call if `BridgeRelayersTransactionExtension` signed extension
// is deployed at runtime level.
#[benchmark]
fn slash_and_deregister() {
// prepare and register relayer account
let relayer: T::AccountId = whitelisted_caller();
let valid_till = pezframe_system::Pezpallet::<T>::block_number()
.saturating_add(crate::Pezpallet::<T, I>::required_registration_lease())
.saturating_add(One::one())
.saturating_add(One::one());
T::deposit_account(relayer.clone(), crate::Pezpallet::<T, I>::required_stake());
assert_ok!(crate::Pezpallet::<T, I>::register(
RawOrigin::Signed(relayer.clone()).into(),
valid_till
));
// create slash destination account
let slash_destination: T::AccountId = whitelisted_caller();
T::deposit_account(slash_destination.clone(), Zero::zero());
#[block]
{
crate::Pezpallet::<T, I>::slash_and_deregister(
&relayer,
bp_relayers::ExplicitOrAccountParams::Explicit::<_, ()>(slash_destination),
);
}
assert!(!crate::Pezpallet::<T, I>::is_registration_active(&relayer));
}
// Benchmark `register_relayer_reward` method of the pezpallet. We are adding this weight to
// the weight of message delivery call if `BridgeRelayersTransactionExtension` signed extension
// is deployed at runtime level.
#[benchmark]
fn register_relayer_reward() {
let reward_kind = T::bench_reward();
let relayer: T::AccountId = whitelisted_caller();
#[block]
{
crate::Pezpallet::<T, I>::register_relayer_reward(reward_kind, &relayer, One::one());
}
assert_eq!(RelayerRewards::<T, I>::get(relayer, &reward_kind), Some(One::one()));
}
impl_benchmark_test_suite!(Pezpallet, crate::mock::new_test_ext(), crate::mock::TestRuntime);
}
@@ -0,0 +1,182 @@
// 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/>.
//! Adapter that allows using `pezpallet-bridge-relayers` as a signed extension in the
//! bridge with remote GRANDPA chain.
use crate::{
extension::verify_messages_call_succeeded, Config as BridgeRelayersConfig, LOG_TARGET,
};
use bp_relayers::{BatchCallUnpacker, ExtensionCallData, ExtensionCallInfo, ExtensionConfig};
use pezbp_runtime::{Chain, StaticStrProvider};
use core::marker::PhantomData;
use pezframe_support::dispatch::{DispatchInfo, PostDispatchInfo};
use pezframe_system::Config as SystemConfig;
use pezpallet_bridge_grandpa::{
CallSubType as BridgeGrandpaCallSubtype, Config as BridgeGrandpaConfig,
SubmitFinalityProofHelper,
};
use pezpallet_bridge_messages::{
CallSubType as BridgeMessagesCallSubType, Config as BridgeMessagesConfig, LaneIdOf,
};
use pezsp_runtime::{
traits::{Dispatchable, Get},
transaction_validity::{TransactionPriority, TransactionValidityError},
Saturating,
};
/// Adapter to be used in signed extension configuration, when bridging with remote
/// chains that are using GRANDPA finality.
pub struct WithGrandpaChainExtensionConfig<
// signed extension identifier
IdProvider,
// runtime that implements `BridgeMessagesConfig<BridgeMessagesPalletInstance>`, which
// uses `BridgeGrandpaConfig<BridgeGrandpaPalletInstance>` to receive messages and
// confirmations from the remote chain.
Runtime,
// batch call unpacker
BatchCallUnpacker,
// instance of the `pezpallet-bridge-grandpa`, tracked by this extension
BridgeGrandpaPalletInstance,
// instance of BridgedChain `pezpallet-bridge-messages`, tracked by this extension
BridgeMessagesPalletInstance,
// instance of `pezpallet-bridge-relayers`, tracked by this extension
BridgeRelayersPalletInstance,
// message delivery transaction priority boost for every additional message
PriorityBoostPerMessage,
>(
PhantomData<(
IdProvider,
Runtime,
BatchCallUnpacker,
BridgeGrandpaPalletInstance,
BridgeMessagesPalletInstance,
BridgeRelayersPalletInstance,
PriorityBoostPerMessage,
)>,
);
impl<ID, R, BCU, GI, MI, RI, P> ExtensionConfig
for WithGrandpaChainExtensionConfig<ID, R, BCU, GI, MI, RI, P>
where
ID: StaticStrProvider,
R: BridgeRelayersConfig<RI>
+ BridgeMessagesConfig<MI, BridgedChain = pezpallet_bridge_grandpa::BridgedChain<R, GI>>
+ BridgeGrandpaConfig<GI>,
BCU: BatchCallUnpacker<R>,
GI: 'static,
MI: 'static,
RI: 'static,
P: Get<TransactionPriority>,
R::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
+ BridgeGrandpaCallSubtype<R, GI>
+ BridgeMessagesCallSubType<R, MI>,
{
type IdProvider = ID;
type Runtime = R;
type BridgeMessagesPalletInstance = MI;
type BridgeRelayersPalletInstance = RI;
type PriorityBoostPerMessage = P;
type RemoteGrandpaChainBlockNumber = pezpallet_bridge_grandpa::BridgedBlockNumber<R, GI>;
type LaneId = LaneIdOf<R, Self::BridgeMessagesPalletInstance>;
fn parse_and_check_for_obsolete_call(
call: &R::RuntimeCall,
) -> Result<
Option<ExtensionCallInfo<Self::RemoteGrandpaChainBlockNumber, Self::LaneId>>,
TransactionValidityError,
> {
let calls = BCU::unpack(call, 2);
let total_calls = calls.len();
let mut calls = calls.into_iter().map(Self::check_obsolete_parsed_call).rev();
let msgs_call = calls.next().transpose()?.and_then(|c| c.call_info());
let relay_finality_call =
calls.next().transpose()?.and_then(|c| c.submit_finality_proof_info());
Ok(match (total_calls, relay_finality_call, msgs_call) {
(2, Some(relay_finality_call), Some(msgs_call)) =>
Some(ExtensionCallInfo::RelayFinalityAndMsgs(relay_finality_call, msgs_call)),
(1, None, Some(msgs_call)) => Some(ExtensionCallInfo::Msgs(msgs_call)),
_ => None,
})
}
fn check_obsolete_parsed_call(
call: &R::RuntimeCall,
) -> Result<&R::RuntimeCall, TransactionValidityError> {
call.check_obsolete_submit_finality_proof()?;
call.check_obsolete_call()?;
Ok(call)
}
fn check_call_result(
call_info: &ExtensionCallInfo<Self::RemoteGrandpaChainBlockNumber, Self::LaneId>,
call_data: &mut ExtensionCallData,
relayer: &R::AccountId,
) -> bool {
verify_submit_finality_proof_succeeded::<Self, GI>(call_info, call_data, relayer) &&
verify_messages_call_succeeded::<Self>(call_info, call_data, relayer)
}
}
/// If the batch call contains the GRANDPA chain state update call, verify that it
/// has been successful.
///
/// Only returns false when GRANDPA chain state update call has failed.
pub(crate) fn verify_submit_finality_proof_succeeded<C, GI>(
call_info: &ExtensionCallInfo<C::RemoteGrandpaChainBlockNumber, C::LaneId>,
call_data: &mut ExtensionCallData,
relayer: &<C::Runtime as SystemConfig>::AccountId,
) -> bool
where
C: ExtensionConfig,
GI: 'static,
C::Runtime: BridgeGrandpaConfig<GI>,
<C::Runtime as BridgeGrandpaConfig<GI>>::BridgedChain:
Chain<BlockNumber = C::RemoteGrandpaChainBlockNumber>,
{
let Some(finality_proof_info) = call_info.submit_finality_proof_info() else { return true };
if !SubmitFinalityProofHelper::<C::Runtime, GI>::was_successful(
finality_proof_info.block_number,
) {
// we only refund relayer if all calls have updated chain state
tracing::trace!(
target: LOG_TARGET,
id_provider=%C::IdProvider::STR,
lane_id=?call_info.messages_call_info().lane_id(),
?relayer,
"Relayer has submitted invalid GRANDPA chain finality proof"
);
return false;
}
// there's a conflict between how bridge GRANDPA pezpallet works and a `utility.batchAll`
// transaction. If relay chain header is mandatory, the GRANDPA pezpallet returns
// `Pays::No`, because such transaction is mandatory for operating the bridge. But
// `utility.batchAll` transaction always requires payment. But in both cases we'll
// refund relayer - either explicitly here, or using `Pays::No` if he's choosing
// to submit dedicated transaction.
// submitter has means to include extra weight/bytes in the `submit_finality_proof`
// call, so let's subtract extra weight/size to avoid refunding for this extra stuff
call_data.extra_weight.saturating_accrue(finality_proof_info.extra_weight);
call_data.extra_size.saturating_accrue(finality_proof_info.extra_size);
true
}
@@ -0,0 +1,99 @@
// 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/>.
//! Adapter that allows using `pezpallet-bridge-relayers` as a signed extension in the
//! bridge with any remote chain. This adapter does not refund any finality transactions.
use crate::{extension::verify_messages_call_succeeded, Config as BridgeRelayersConfig};
use bp_relayers::{ExtensionCallData, ExtensionCallInfo, ExtensionConfig};
use pezbp_runtime::StaticStrProvider;
use core::marker::PhantomData;
use pezframe_support::dispatch::{DispatchInfo, PostDispatchInfo};
use pezpallet_bridge_messages::{
CallSubType as BridgeMessagesCallSubType, Config as BridgeMessagesConfig, LaneIdOf,
};
use pezsp_runtime::{
traits::{Dispatchable, Get},
transaction_validity::{TransactionPriority, TransactionValidityError},
};
/// Transaction extension that refunds a relayer for standalone messages delivery and confirmation
/// transactions. Finality transactions are not refunded.
pub struct WithMessagesExtensionConfig<
IdProvider,
Runtime,
BridgeMessagesPalletInstance,
BridgeRelayersPalletInstance,
PriorityBoostPerMessage,
>(
PhantomData<(
// signed extension identifier
IdProvider,
// runtime with `pezpallet-bridge-messages` pezpallet deployed
Runtime,
// instance of BridgedChain `pezpallet-bridge-messages`, tracked by this extension
BridgeMessagesPalletInstance,
// instance of `pezpallet-bridge-relayers`, tracked by this extension
BridgeRelayersPalletInstance,
// message delivery transaction priority boost for every additional message
PriorityBoostPerMessage,
)>,
);
impl<ID, R, MI, RI, P> ExtensionConfig for WithMessagesExtensionConfig<ID, R, MI, RI, P>
where
ID: StaticStrProvider,
R: BridgeRelayersConfig<RI> + BridgeMessagesConfig<MI>,
MI: 'static,
RI: 'static,
P: Get<TransactionPriority>,
R::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
+ BridgeMessagesCallSubType<R, MI>,
{
type IdProvider = ID;
type Runtime = R;
type BridgeMessagesPalletInstance = MI;
type BridgeRelayersPalletInstance = RI;
type PriorityBoostPerMessage = P;
type RemoteGrandpaChainBlockNumber = ();
type LaneId = LaneIdOf<R, Self::BridgeMessagesPalletInstance>;
fn parse_and_check_for_obsolete_call(
call: &R::RuntimeCall,
) -> Result<
Option<ExtensionCallInfo<Self::RemoteGrandpaChainBlockNumber, Self::LaneId>>,
TransactionValidityError,
> {
let call = Self::check_obsolete_parsed_call(call)?;
Ok(call.call_info().map(ExtensionCallInfo::Msgs))
}
fn check_obsolete_parsed_call(
call: &R::RuntimeCall,
) -> Result<&R::RuntimeCall, TransactionValidityError> {
call.check_obsolete_call()?;
Ok(call)
}
fn check_call_result(
call_info: &ExtensionCallInfo<Self::RemoteGrandpaChainBlockNumber, Self::LaneId>,
call_data: &mut ExtensionCallData,
relayer: &R::AccountId,
) -> bool {
verify_messages_call_succeeded::<Self>(call_info, call_data, relayer)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,431 @@
// 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/>.
//! Bridge transaction priority calculator.
//!
//! We want to prioritize message delivery transactions with more messages over
//! transactions with less messages. That's because we reject delivery transactions
//! if it contains already delivered message. And if some transaction delivers
//! single message with nonce `N`, then the transaction with nonces `N..=N+100` will
//! be rejected. This can lower bridge throughput down to one message per block.
use pezframe_support::traits::Get;
use pezsp_runtime::transaction_validity::TransactionPriority;
// reexport everything from `integrity_tests` module
#[allow(unused_imports)]
pub use integrity_tests::*;
/// We'll deal with different bridge items here - messages, headers, ...
/// To avoid being too verbose with generic code, let's just define a separate alias.
pub type ItemCount = u64;
/// Compute priority boost for transaction that brings given number of bridge
/// items (messages, headers, ...), when every additional item adds `PriorityBoostPerItem`
/// to transaction priority.
pub fn compute_priority_boost<PriorityBoostPerItem>(n_items: ItemCount) -> TransactionPriority
where
PriorityBoostPerItem: Get<TransactionPriority>,
{
// we don't want any boost for transaction with single (additional) item => minus one
PriorityBoostPerItem::get().saturating_mul(n_items.saturating_sub(1))
}
#[cfg(not(feature = "integrity-test"))]
mod integrity_tests {}
#[cfg(feature = "integrity-test")]
mod integrity_tests {
use super::{compute_priority_boost, ItemCount};
use bp_messages::MessageNonce;
use pezbp_runtime::PreComputedSize;
use pezframe_support::{
dispatch::{DispatchClass, DispatchInfo, Pays, PostDispatchInfo},
traits::Get,
};
use pezpallet_transaction_payment::OnChargeTransaction;
use pezsp_runtime::{
traits::{Dispatchable, UniqueSaturatedInto, Zero},
transaction_validity::TransactionPriority,
FixedPointOperand, SaturatedConversion, Saturating,
};
type BalanceOf<T> =
<<T as pezpallet_transaction_payment::Config>::OnChargeTransaction as OnChargeTransaction<
T,
>>::Balance;
/// Ensures that the value of `PriorityBoostPerItem` matches the value of
/// `tip_boost_per_item`.
///
/// We want two transactions, `TX1` with `N` items and `TX2` with `N+1` items, have almost
/// the same priority if we'll add `tip_boost_per_item` tip to the `TX1`. We want to be sure
/// that if we add plain `PriorityBoostPerItem` priority to `TX1`, the priority will be close
/// to `TX2` as well.
fn ensure_priority_boost_is_sane<PriorityBoostPerItem, Balance>(
param_name: &str,
max_items: ItemCount,
tip_boost_per_item: Balance,
estimate_priority: impl Fn(ItemCount, Balance) -> TransactionPriority,
) where
PriorityBoostPerItem: Get<TransactionPriority>,
ItemCount: UniqueSaturatedInto<Balance>,
Balance: FixedPointOperand + Zero,
{
let priority_boost_per_item = PriorityBoostPerItem::get();
for n_items in 1..=max_items {
let base_priority = estimate_priority(n_items, Zero::zero());
let priority_boost = compute_priority_boost::<PriorityBoostPerItem>(n_items);
let priority_with_boost = base_priority
.checked_add(priority_boost)
.expect("priority overflow: try lowering `max_items` or `tip_boost_per_item`?");
let tip = tip_boost_per_item.saturating_mul((n_items - 1).unique_saturated_into());
let priority_with_tip = estimate_priority(1, tip);
const ERROR_MARGIN: TransactionPriority = 5; // 5%
if priority_with_boost.abs_diff(priority_with_tip).saturating_mul(100) /
priority_with_tip >
ERROR_MARGIN
{
panic!(
"The {param_name} value ({}) must be fixed to: {}",
priority_boost_per_item,
compute_priority_boost_per_item(
max_items,
tip_boost_per_item,
estimate_priority
),
);
}
}
}
/// Compute priority boost that we give to bridge transaction for every
/// additional bridge item.
#[cfg(feature = "integrity-test")]
fn compute_priority_boost_per_item<Balance>(
max_items: ItemCount,
tip_boost_per_item: Balance,
estimate_priority: impl Fn(ItemCount, Balance) -> TransactionPriority,
) -> TransactionPriority
where
ItemCount: UniqueSaturatedInto<Balance>,
Balance: FixedPointOperand + Zero,
{
// estimate priority of transaction that delivers one item and has large tip
let small_with_tip_priority =
estimate_priority(1, tip_boost_per_item.saturating_mul(max_items.saturated_into()));
// estimate priority of transaction that delivers maximal number of items, but has no tip
let large_without_tip_priority = estimate_priority(max_items, Zero::zero());
small_with_tip_priority
.saturating_sub(large_without_tip_priority)
.saturating_div(max_items - 1)
}
/// Computations, specific to bridge relay chains transactions.
pub mod per_relay_header {
use super::*;
use bp_header_pez_chain::{
max_expected_submit_finality_proof_arguments_size, ChainWithGrandpa,
};
use pezpallet_bridge_grandpa::WeightInfoExt;
/// Ensures that the value of `PriorityBoostPerHeader` matches the value of
/// `tip_boost_per_header`.
///
/// We want two transactions, `TX1` with `N` headers and `TX2` with `N+1` headers, have
/// almost the same priority if we'll add `tip_boost_per_header` tip to the `TX1`. We want
/// to be sure that if we add plain `PriorityBoostPerHeader` priority to `TX1`, the priority
/// will be close to `TX2` as well.
pub fn ensure_priority_boost_is_sane<Runtime, GrandpaInstance, PriorityBoostPerHeader>(
tip_boost_per_header: BalanceOf<Runtime>,
) where
Runtime:
pezpallet_transaction_payment::Config + pezpallet_bridge_grandpa::Config<GrandpaInstance>,
GrandpaInstance: 'static,
PriorityBoostPerHeader: Get<TransactionPriority>,
Runtime::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
BalanceOf<Runtime>: Send + Sync + FixedPointOperand,
{
// the meaning of `max_items` here is different when comparing with message
// transactions - with messages we have a strict limit on maximal number of
// messages we can fit into a single transaction. With headers, current best
// header may be improved by any "number of items". But this number is only
// used to verify priority boost, so it should be fine to select this arbitrary
// value - it SHALL NOT affect any value, it just adds more tests for the value.
let maximal_improved_by = 4_096;
super::ensure_priority_boost_is_sane::<PriorityBoostPerHeader, BalanceOf<Runtime>>(
"PriorityBoostPerRelayHeader",
maximal_improved_by,
tip_boost_per_header,
|_n_headers, tip| {
estimate_relay_header_submit_transaction_priority::<Runtime, GrandpaInstance>(
tip,
)
},
);
}
/// Estimate relay header delivery transaction priority.
#[cfg(feature = "integrity-test")]
fn estimate_relay_header_submit_transaction_priority<Runtime, GrandpaInstance>(
tip: BalanceOf<Runtime>,
) -> TransactionPriority
where
Runtime:
pezpallet_transaction_payment::Config + pezpallet_bridge_grandpa::Config<GrandpaInstance>,
GrandpaInstance: 'static,
Runtime::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
BalanceOf<Runtime>: Send + Sync + FixedPointOperand,
{
// just an estimation of extra transaction bytes that are added to every transaction
// (including signature, signed extensions extra and etc + in our case it includes
// all call arguments except the proof itself)
let base_tx_size = 512;
// let's say we are relaying largest relay chain headers
let tx_call_size = max_expected_submit_finality_proof_arguments_size::<
Runtime::BridgedChain,
>(true, Runtime::BridgedChain::MAX_AUTHORITIES_COUNT * 2 / 3 + 1);
// finally we are able to estimate transaction size and weight
let transaction_size = base_tx_size.saturating_add(tx_call_size);
let transaction_weight = <Runtime as ::pezpallet_bridge_grandpa::Config<
GrandpaInstance,
>>::WeightInfo::submit_finality_proof_weight(
Runtime::BridgedChain::MAX_AUTHORITIES_COUNT * 2 / 3 + 1,
Runtime::BridgedChain::REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY,
);
pezpallet_transaction_payment::ChargeTransactionPayment::<Runtime>::get_priority(
&DispatchInfo {
call_weight: transaction_weight,
extension_weight: Default::default(),
class: DispatchClass::Normal,
pays_fee: Pays::Yes,
},
transaction_size as _,
tip,
Zero::zero(),
)
}
}
/// Computations, specific to bridge teyrchains transactions.
pub mod per_teyrchain_header {
use super::*;
use pezbp_runtime::Teyrchain;
use pezpallet_bridge_teyrchains::WeightInfoExt;
/// Ensures that the value of `PriorityBoostPerHeader` matches the value of
/// `tip_boost_per_header`.
///
/// We want two transactions, `TX1` with `N` headers and `TX2` with `N+1` headers, have
/// almost the same priority if we'll add `tip_boost_per_header` tip to the `TX1`. We want
/// to be sure that if we add plain `PriorityBoostPerHeader` priority to `TX1`, the priority
/// will be close to `TX2` as well.
pub fn ensure_priority_boost_is_sane<
Runtime,
TeyrchainsInstance,
Para,
PriorityBoostPerHeader,
>(
tip_boost_per_header: BalanceOf<Runtime>,
) where
Runtime: pezpallet_transaction_payment::Config
+ pezpallet_bridge_teyrchains::Config<TeyrchainsInstance>,
TeyrchainsInstance: 'static,
Para: Teyrchain,
PriorityBoostPerHeader: Get<TransactionPriority>,
Runtime::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
BalanceOf<Runtime>: Send + Sync + FixedPointOperand,
{
// the meaning of `max_items` here is different when comparing with message
// transactions - with messages we have a strict limit on maximal number of
// messages we can fit into a single transaction. With headers, current best
// header may be improved by any "number of items". But this number is only
// used to verify priority boost, so it should be fine to select this arbitrary
// value - it SHALL NOT affect any value, it just adds more tests for the value.
let maximal_improved_by = 4_096;
super::ensure_priority_boost_is_sane::<PriorityBoostPerHeader, BalanceOf<Runtime>>(
"PriorityBoostPerTeyrchainHeader",
maximal_improved_by,
tip_boost_per_header,
|_n_headers, tip| {
estimate_teyrchain_header_submit_transaction_priority::<
Runtime,
TeyrchainsInstance,
Para,
>(tip)
},
);
}
/// Estimate teyrchain header delivery transaction priority.
#[cfg(feature = "integrity-test")]
fn estimate_teyrchain_header_submit_transaction_priority<
Runtime,
TeyrchainsInstance,
Para,
>(
tip: BalanceOf<Runtime>,
) -> TransactionPriority
where
Runtime: pezpallet_transaction_payment::Config
+ pezpallet_bridge_teyrchains::Config<TeyrchainsInstance>,
TeyrchainsInstance: 'static,
Para: Teyrchain,
Runtime::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
BalanceOf<Runtime>: Send + Sync + FixedPointOperand,
{
// just an estimation of extra transaction bytes that are added to every transaction
// (including signature, signed extensions extra and etc + in our case it includes
// all call arguments except the proof itself)
let base_tx_size = 512;
// let's say we are relaying largest teyrchain headers and proof takes some more bytes
let tx_call_size = <Runtime as pezpallet_bridge_teyrchains::Config<
TeyrchainsInstance,
>>::WeightInfo::expected_extra_storage_proof_size()
.saturating_add(Para::MAX_HEADER_SIZE);
// finally we are able to estimate transaction size and weight
let transaction_size = base_tx_size.saturating_add(tx_call_size);
let transaction_weight = <Runtime as pezpallet_bridge_teyrchains::Config<
TeyrchainsInstance,
>>::WeightInfo::submit_teyrchain_heads_weight(
Runtime::DbWeight::get(),
&PreComputedSize(transaction_size as _),
// just one teyrchain - all other submissions won't receive any boost
1,
);
pezpallet_transaction_payment::ChargeTransactionPayment::<Runtime>::get_priority(
&DispatchInfo {
call_weight: transaction_weight,
extension_weight: Default::default(),
class: DispatchClass::Normal,
pays_fee: Pays::Yes,
},
transaction_size as _,
tip,
Zero::zero(),
)
}
}
/// Computations, specific to bridge messages transactions.
pub mod per_message {
use super::*;
use bp_messages::ChainWithMessages;
use pezpallet_bridge_messages::WeightInfoExt;
/// Ensures that the value of `PriorityBoostPerMessage` matches the value of
/// `tip_boost_per_message`.
///
/// We want two transactions, `TX1` with `N` messages and `TX2` with `N+1` messages, have
/// almost the same priority if we'll add `tip_boost_per_message` tip to the `TX1`. We want
/// to be sure that if we add plain `PriorityBoostPerMessage` priority to `TX1`, the
/// priority will be close to `TX2` as well.
pub fn ensure_priority_boost_is_sane<Runtime, MessagesInstance, PriorityBoostPerMessage>(
tip_boost_per_message: BalanceOf<Runtime>,
) where
Runtime: pezpallet_transaction_payment::Config
+ pezpallet_bridge_messages::Config<MessagesInstance>,
MessagesInstance: 'static,
PriorityBoostPerMessage: Get<TransactionPriority>,
Runtime::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
BalanceOf<Runtime>: Send + Sync + FixedPointOperand,
{
let maximal_messages_in_delivery_transaction =
Runtime::BridgedChain::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
super::ensure_priority_boost_is_sane::<PriorityBoostPerMessage, BalanceOf<Runtime>>(
"PriorityBoostPerMessage",
maximal_messages_in_delivery_transaction,
tip_boost_per_message,
|n_messages, tip| {
estimate_message_delivery_transaction_priority::<Runtime, MessagesInstance>(
n_messages, tip,
)
},
);
}
/// Estimate message delivery transaction priority.
#[cfg(feature = "integrity-test")]
fn estimate_message_delivery_transaction_priority<Runtime, MessagesInstance>(
messages: MessageNonce,
tip: BalanceOf<Runtime>,
) -> TransactionPriority
where
Runtime: pezpallet_transaction_payment::Config
+ pezpallet_bridge_messages::Config<MessagesInstance>,
MessagesInstance: 'static,
Runtime::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
BalanceOf<Runtime>: Send + Sync + FixedPointOperand,
{
// just an estimation of extra transaction bytes that are added to every transaction
// (including signature, signed extensions extra and etc + in our case it includes
// all call arguments except the proof itself)
let base_tx_size = 512;
// let's say we are relaying similar small messages and for every message we add more
// trie nodes to the proof (x0.5 because we expect some nodes to be reused)
let estimated_message_size = 512;
// let's say all our messages have the same dispatch weight
let estimated_message_dispatch_weight = <Runtime as pezpallet_bridge_messages::Config<
MessagesInstance,
>>::WeightInfo::message_dispatch_weight(
estimated_message_size
);
// messages proof argument size is (for every message) messages size + some additional
// trie nodes. Some of them are reused by different messages, so let's take 2/3 of
// default "overhead" constant
let messages_proof_size = <Runtime as pezpallet_bridge_messages::Config<
MessagesInstance,
>>::WeightInfo::expected_extra_storage_proof_size()
.saturating_mul(2)
.saturating_div(3)
.saturating_add(estimated_message_size)
.saturating_mul(messages as _);
// finally we are able to estimate transaction size and weight
let transaction_size = base_tx_size.saturating_add(messages_proof_size);
let transaction_weight = <Runtime as pezpallet_bridge_messages::Config<
MessagesInstance,
>>::WeightInfo::receive_messages_proof_weight(
&PreComputedSize(transaction_size as _),
messages as _,
estimated_message_dispatch_weight.saturating_mul(messages),
);
pezpallet_transaction_payment::ChargeTransactionPayment::<Runtime>::get_priority(
&DispatchInfo {
call_weight: transaction_weight,
extension_weight: Default::default(),
class: DispatchClass::Normal,
pays_fee: Pays::Yes,
},
transaction_size as _,
tip,
Zero::zero(),
)
}
}
}
@@ -0,0 +1,188 @@
// 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/>.
//! Adapter that allows using `pezpallet-bridge-relayers` as a signed extension in the
//! bridge with remote teyrchain.
use crate::{
extension::{
grandpa_adapter::verify_submit_finality_proof_succeeded, verify_messages_call_succeeded,
},
Config as BridgeRelayersConfig, LOG_TARGET,
};
use bp_relayers::{BatchCallUnpacker, ExtensionCallData, ExtensionCallInfo, ExtensionConfig};
use pezbp_runtime::{StaticStrProvider, Teyrchain};
use core::marker::PhantomData;
use pezframe_support::dispatch::{DispatchInfo, PostDispatchInfo};
use pezframe_system::Config as SystemConfig;
use pezpallet_bridge_grandpa::{
CallSubType as BridgeGrandpaCallSubtype, Config as BridgeGrandpaConfig,
};
use pezpallet_bridge_messages::{
CallSubType as BridgeMessagesCallSubType, Config as BridgeMessagesConfig, LaneIdOf,
};
use pezpallet_bridge_teyrchains::{
CallSubType as BridgeTeyrchainsCallSubtype, Config as BridgeTeyrchainsConfig,
SubmitTeyrchainHeadsHelper,
};
use pezsp_runtime::{
traits::{Dispatchable, Get},
transaction_validity::{TransactionPriority, TransactionValidityError},
};
/// Adapter to be used in signed extension configuration, when bridging with remote teyrchains.
pub struct WithTeyrchainExtensionConfig<
// signed extension identifier
IdProvider,
// runtime that implements `BridgeMessagesConfig<BridgeMessagesPalletInstance>`, which
// uses `BridgeTeyrchainsConfig<BridgeTeyrchainsPalletInstance>` to receive messages and
// confirmations from the remote chain.
Runtime,
// batch call unpacker
BatchCallUnpacker,
// instance of the `pezpallet-bridge-teyrchains`, tracked by this extension
BridgeTeyrchainsPalletInstance,
// instance of BridgedChain `pezpallet-bridge-messages`, tracked by this extension
BridgeMessagesPalletInstance,
// instance of `pezpallet-bridge-relayers`, tracked by this extension
BridgeRelayersPalletInstance,
// message delivery transaction priority boost for every additional message
PriorityBoostPerMessage,
>(
PhantomData<(
IdProvider,
Runtime,
BatchCallUnpacker,
BridgeTeyrchainsPalletInstance,
BridgeMessagesPalletInstance,
BridgeRelayersPalletInstance,
PriorityBoostPerMessage,
)>,
);
impl<ID, R, BCU, PI, MI, RI, P> ExtensionConfig
for WithTeyrchainExtensionConfig<ID, R, BCU, PI, MI, RI, P>
where
ID: StaticStrProvider,
R: BridgeRelayersConfig<RI>
+ BridgeMessagesConfig<MI>
+ BridgeTeyrchainsConfig<PI>
+ BridgeGrandpaConfig<R::BridgesGrandpaPalletInstance>,
BCU: BatchCallUnpacker<R>,
PI: 'static,
MI: 'static,
RI: 'static,
P: Get<TransactionPriority>,
R::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
+ BridgeGrandpaCallSubtype<R, R::BridgesGrandpaPalletInstance>
+ BridgeTeyrchainsCallSubtype<R, PI>
+ BridgeMessagesCallSubType<R, MI>,
<R as BridgeMessagesConfig<MI>>::BridgedChain: Teyrchain,
{
type IdProvider = ID;
type Runtime = R;
type BridgeMessagesPalletInstance = MI;
type BridgeRelayersPalletInstance = RI;
type PriorityBoostPerMessage = P;
type RemoteGrandpaChainBlockNumber =
pezpallet_bridge_grandpa::BridgedBlockNumber<R, R::BridgesGrandpaPalletInstance>;
type LaneId = LaneIdOf<R, Self::BridgeMessagesPalletInstance>;
fn parse_and_check_for_obsolete_call(
call: &R::RuntimeCall,
) -> Result<
Option<ExtensionCallInfo<Self::RemoteGrandpaChainBlockNumber, Self::LaneId>>,
TransactionValidityError,
> {
let calls = BCU::unpack(call, 3);
let total_calls = calls.len();
let mut calls = calls.into_iter().map(Self::check_obsolete_parsed_call).rev();
let msgs_call = calls.next().transpose()?.and_then(|c| c.call_info());
let para_finality_call = calls.next().transpose()?.and_then(|c| {
let r = c.submit_teyrchain_heads_info_for(
<R as BridgeMessagesConfig<Self::BridgeMessagesPalletInstance>>::BridgedChain::TEYRCHAIN_ID,
);
r
});
let relay_finality_call =
calls.next().transpose()?.and_then(|c| c.submit_finality_proof_info());
Ok(match (total_calls, relay_finality_call, para_finality_call, msgs_call) {
(3, Some(relay_finality_call), Some(para_finality_call), Some(msgs_call)) =>
Some(ExtensionCallInfo::AllFinalityAndMsgs(
relay_finality_call,
para_finality_call,
msgs_call,
)),
(2, None, Some(para_finality_call), Some(msgs_call)) =>
Some(ExtensionCallInfo::TeyrchainFinalityAndMsgs(para_finality_call, msgs_call)),
(1, None, None, Some(msgs_call)) => Some(ExtensionCallInfo::Msgs(msgs_call)),
_ => None,
})
}
fn check_obsolete_parsed_call(
call: &R::RuntimeCall,
) -> Result<&R::RuntimeCall, TransactionValidityError> {
call.check_obsolete_submit_finality_proof()?;
call.check_obsolete_submit_teyrchain_heads()?;
call.check_obsolete_call()?;
Ok(call)
}
fn check_call_result(
call_info: &ExtensionCallInfo<Self::RemoteGrandpaChainBlockNumber, Self::LaneId>,
call_data: &mut ExtensionCallData,
relayer: &R::AccountId,
) -> bool {
verify_submit_finality_proof_succeeded::<Self, R::BridgesGrandpaPalletInstance>(
call_info, call_data, relayer,
) && verify_submit_teyrchain_head_succeeded::<Self, PI>(call_info, call_data, relayer) &&
verify_messages_call_succeeded::<Self>(call_info, call_data, relayer)
}
}
/// If the batch call contains the teyrchain state update call, verify that it
/// has been successful.
///
/// Only returns false when teyrchain state update call has failed.
pub(crate) fn verify_submit_teyrchain_head_succeeded<C, PI>(
call_info: &ExtensionCallInfo<C::RemoteGrandpaChainBlockNumber, C::LaneId>,
_call_data: &mut ExtensionCallData,
relayer: &<C::Runtime as SystemConfig>::AccountId,
) -> bool
where
C: ExtensionConfig,
PI: 'static,
C::Runtime: BridgeTeyrchainsConfig<PI>,
{
let Some(para_proof_info) = call_info.submit_teyrchain_heads_info() else { return true };
if !SubmitTeyrchainHeadsHelper::<C::Runtime, PI>::was_successful(para_proof_info) {
// we only refund relayer if all calls have updated chain state
tracing::trace!(
target: LOG_TARGET,
id_provider=%C::IdProvider::STR,
lane_id=?call_info.messages_call_info().lane_id(),
?relayer,
"Relayer has submitted invalid teyrchain finality proof"
);
return false;
}
true
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,451 @@
// 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/>.
//! A module that is responsible for migration of storage.
use alloc::vec::Vec;
use pezframe_support::{
traits::{Get, StorageVersion},
weights::Weight,
};
/// The in-code storage version.
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
/// This module contains data structures that are valid for the initial state of `0`.
/// (used with v1 migration).
pub mod v0 {
use crate::{Config, Pezpallet};
use bp_relayers::RewardsAccountOwner;
use pezbp_runtime::{ChainId, StorageDoubleMapKeyProvider};
use codec::{Codec, Decode, Encode, EncodeLike, MaxEncodedLen};
use core::marker::PhantomData;
use pezframe_support::{pezpallet_prelude::OptionQuery, Blake2_128Concat, Identity};
use scale_info::TypeInfo;
use pezsp_runtime::traits::AccountIdConversion;
/// Structure used to identify the account that pays a reward to the relayer.
#[derive(Copy, Clone, Debug, Decode, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen)]
pub struct RewardsAccountParams<LaneId> {
/// lane_id
pub lane_id: LaneId,
/// bridged_chain_id
pub bridged_chain_id: ChainId,
/// owner
pub owner: RewardsAccountOwner,
}
impl<LaneId: Decode + Encode> RewardsAccountParams<LaneId> {
/// Create a new instance of `RewardsAccountParams`.
pub const fn new(
lane_id: LaneId,
bridged_chain_id: ChainId,
owner: RewardsAccountOwner,
) -> Self {
Self { lane_id, bridged_chain_id, owner }
}
}
impl<LaneId> pezsp_runtime::TypeId for RewardsAccountParams<LaneId> {
const TYPE_ID: [u8; 4] = *b"brap";
}
pub(crate) struct RelayerRewardsKeyProvider<AccountId, RewardBalance, LaneId>(
PhantomData<(AccountId, RewardBalance, LaneId)>,
);
impl<AccountId, RewardBalance, LaneId> StorageDoubleMapKeyProvider
for RelayerRewardsKeyProvider<AccountId, RewardBalance, LaneId>
where
AccountId: 'static + Codec + EncodeLike + Send + Sync,
RewardBalance: 'static + Codec + EncodeLike + Send + Sync,
LaneId: Codec + EncodeLike + Send + Sync,
{
const MAP_NAME: &'static str = "RelayerRewards";
type Hasher1 = Blake2_128Concat;
type Key1 = AccountId;
type Hasher2 = Identity;
type Key2 = RewardsAccountParams<LaneId>;
type Value = RewardBalance;
}
pub(crate) type RelayerRewardsKeyProviderOf<T, I, LaneId> = RelayerRewardsKeyProvider<
<T as pezframe_system::Config>::AccountId,
<T as Config<I>>::RewardBalance,
LaneId,
>;
#[pezframe_support::storage_alias]
pub(crate) type RelayerRewards<T: Config<I>, I: 'static, LaneId> = StorageDoubleMap<
Pezpallet<T, I>,
<RelayerRewardsKeyProviderOf<T, I, LaneId> as StorageDoubleMapKeyProvider>::Hasher1,
<RelayerRewardsKeyProviderOf<T, I, LaneId> as StorageDoubleMapKeyProvider>::Key1,
<RelayerRewardsKeyProviderOf<T, I, LaneId> as StorageDoubleMapKeyProvider>::Hasher2,
<RelayerRewardsKeyProviderOf<T, I, LaneId> as StorageDoubleMapKeyProvider>::Key2,
<RelayerRewardsKeyProviderOf<T, I, LaneId> as StorageDoubleMapKeyProvider>::Value,
OptionQuery,
>;
/// Reward account generator for `v0`.
pub struct PayRewardFromAccount<Account, LaneId>(PhantomData<(Account, LaneId)>);
impl<Account, LaneId> PayRewardFromAccount<Account, LaneId>
where
Account: Decode + Encode,
LaneId: Decode + Encode,
{
/// Return account that pays rewards based on the provided parameters.
pub fn rewards_account(params: RewardsAccountParams<LaneId>) -> Account {
params.into_sub_account_truncating(b"rewards-account")
}
}
}
/// This migration updates `RelayerRewards` where `RewardsAccountParams` was used as the key with
/// `lane_id` as the first attribute, which affects `into_sub_account_truncating`. We are migrating
/// this key to use the new `RewardsAccountParams` where `lane_id` is the last attribute.
pub mod v1 {
use super::*;
use crate::{Config, Pezpallet};
use bp_messages::LaneIdType;
use bp_relayers::RewardsAccountParams;
use pezbp_runtime::StorageDoubleMapKeyProvider;
use codec::{Codec, EncodeLike};
use core::marker::PhantomData;
use pezframe_support::{
pezpallet_prelude::OptionQuery, traits::UncheckedOnRuntimeUpgrade, Blake2_128Concat, Identity,
};
use pezsp_arithmetic::traits::Zero;
pub(crate) struct RelayerRewardsKeyProvider<AccountId, RewardBalance, LaneId>(
PhantomData<(AccountId, RewardBalance, LaneId)>,
);
impl<AccountId, RewardBalance, LaneId> StorageDoubleMapKeyProvider
for RelayerRewardsKeyProvider<AccountId, RewardBalance, LaneId>
where
AccountId: 'static + Codec + EncodeLike + Send + Sync,
RewardBalance: 'static + Codec + EncodeLike + Send + Sync,
LaneId: Codec + EncodeLike + Send + Sync,
{
const MAP_NAME: &'static str = "RelayerRewards";
type Hasher1 = Blake2_128Concat;
type Key1 = AccountId;
type Hasher2 = Identity;
type Key2 = v1::RewardsAccountParams<LaneId>;
type Value = RewardBalance;
}
pub(crate) type RelayerRewardsKeyProviderOf<T, I, LaneId> = RelayerRewardsKeyProvider<
<T as pezframe_system::Config>::AccountId,
<T as Config<I>>::RewardBalance,
LaneId,
>;
#[pezframe_support::storage_alias]
pub(crate) type RelayerRewards<T: Config<I>, I: 'static, LaneId> = StorageDoubleMap<
Pezpallet<T, I>,
<RelayerRewardsKeyProviderOf<T, I, LaneId> as StorageDoubleMapKeyProvider>::Hasher1,
<RelayerRewardsKeyProviderOf<T, I, LaneId> as StorageDoubleMapKeyProvider>::Key1,
<RelayerRewardsKeyProviderOf<T, I, LaneId> as StorageDoubleMapKeyProvider>::Hasher2,
<RelayerRewardsKeyProviderOf<T, I, LaneId> as StorageDoubleMapKeyProvider>::Key2,
<RelayerRewardsKeyProviderOf<T, I, LaneId> as StorageDoubleMapKeyProvider>::Value,
OptionQuery,
>;
// Copy of `Pezpallet::<T, I>::register_relayer_reward` compatible with v1.
fn register_relayer_reward_for_v1<
T: Config<I>,
I: 'static,
LaneId: LaneIdType + Send + Sync,
>(
rewards_account_params: v1::RewardsAccountParams<LaneId>,
relayer: &T::AccountId,
reward_balance: T::RewardBalance,
) {
use pezsp_runtime::Saturating;
if reward_balance.is_zero() {
return;
}
v1::RelayerRewards::<T, I, LaneId>::mutate(
relayer,
rewards_account_params,
|old_reward: &mut Option<T::RewardBalance>| {
let new_reward =
old_reward.unwrap_or_else(Zero::zero).saturating_add(reward_balance);
*old_reward = Some(new_reward);
tracing::trace!(
target: crate::LOG_TARGET,
?relayer,
?rewards_account_params,
?new_reward,
"Relayer can now claim reward"
);
},
);
}
/// Migrates the pezpallet storage to v1.
pub struct UncheckedMigrationV0ToV1<T, I, LaneId>(PhantomData<(T, I, LaneId)>);
#[cfg(feature = "try-runtime")]
const LOG_TARGET: &str = "runtime::bridge-relayers-migration";
impl<T: Config<I>, I: 'static, LaneId: LaneIdType + Send + Sync> UncheckedOnRuntimeUpgrade
for UncheckedMigrationV0ToV1<T, I, LaneId>
{
fn on_runtime_upgrade() -> Weight {
let mut weight = T::DbWeight::get().reads(1);
// list all rewards (we cannot do this as one step because of `drain` limitation)
let mut rewards_to_migrate =
Vec::with_capacity(v0::RelayerRewards::<T, I, LaneId>::iter().count());
for (key1, key2, reward) in v0::RelayerRewards::<T, I, LaneId>::drain() {
rewards_to_migrate.push((key1, key2, reward));
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
}
// re-register rewards with new format of `RewardsAccountParams`.
for (key1, key2, reward) in rewards_to_migrate {
// expand old key
let v0::RewardsAccountParams { owner, lane_id, bridged_chain_id } = key2;
// re-register reward
register_relayer_reward_for_v1::<T, I, LaneId>(
v1::RewardsAccountParams::new(lane_id, bridged_chain_id, owner),
&key1,
reward,
);
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
}
weight
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, pezsp_runtime::DispatchError> {
use codec::Encode;
use pezframe_support::BoundedBTreeMap;
use pezsp_runtime::traits::ConstU32;
// collect actual rewards
let mut rewards: BoundedBTreeMap<
(T::AccountId, LaneId),
T::RewardBalance,
ConstU32<{ u32::MAX }>,
> = BoundedBTreeMap::new();
for (key1, key2, reward) in v0::RelayerRewards::<T, I, LaneId>::iter() {
tracing::info!(target: LOG_TARGET, ?key1, ?key2, ?reward, "Reward to migrate");
rewards = rewards
.try_mutate(|inner| {
inner
.entry((key1.clone(), key2.lane_id))
.and_modify(|value| *value += reward)
.or_insert(reward);
})
.unwrap();
}
tracing::info!(target: LOG_TARGET, ?rewards, "Found total rewards to migrate");
Ok(rewards.encode())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), pezsp_runtime::DispatchError> {
use codec::Decode;
use pezframe_support::BoundedBTreeMap;
use pezsp_runtime::traits::ConstU32;
let rewards_before: BoundedBTreeMap<
(T::AccountId, LaneId),
T::RewardBalance,
ConstU32<{ u32::MAX }>,
> = Decode::decode(&mut &state[..]).unwrap();
// collect migrated rewards
let mut rewards_after: BoundedBTreeMap<
(T::AccountId, LaneId),
T::RewardBalance,
ConstU32<{ u32::MAX }>,
> = BoundedBTreeMap::new();
for (key1, key2, reward) in v1::RelayerRewards::<T, I, LaneId>::iter() {
tracing::info!(target: LOG_TARGET, ?key1, ?key2, ?reward, "Migrated rewards");
rewards_after = rewards_after
.try_mutate(|inner| {
inner
.entry((key1.clone(), *key2.lane_id()))
.and_modify(|value| *value += reward)
.or_insert(reward);
})
.unwrap();
}
tracing::info!(target: LOG_TARGET, ?rewards_after, "Found total migrated rewards");
pezframe_support::ensure!(
rewards_before == rewards_after,
"The rewards were not migrated correctly!."
);
tracing::info!(target: LOG_TARGET, "migrated all.");
Ok(())
}
}
/// [`UncheckedMigrationV0ToV1`] wrapped in a
/// [`VersionedMigration`](pezframe_support::migrations::VersionedMigration), ensuring the
/// migration is only performed when on-chain version is 0.
pub type MigrationToV1<T, I, LaneId> = pezframe_support::migrations::VersionedMigration<
0,
1,
UncheckedMigrationV0ToV1<T, I, LaneId>,
Pezpallet<T, I>,
<T as pezframe_system::Config>::DbWeight,
>;
}
/// The pezpallet in version 1 only supported rewards collected under the key of
/// `RewardsAccountParams`. This migration essentially converts existing `RewardsAccountParams` keys
/// to the generic type `T::Reward`.
pub mod v2 {
use super::*;
#[cfg(feature = "try-runtime")]
use crate::RelayerRewards;
use crate::{Config, Pezpallet};
use bp_messages::LaneIdType;
use bp_relayers::RewardsAccountParams;
use core::marker::PhantomData;
use pezframe_support::traits::UncheckedOnRuntimeUpgrade;
/// Migrates the pezpallet storage to v2.
pub struct UncheckedMigrationV1ToV2<T, I, LaneId>(PhantomData<(T, I, LaneId)>);
#[cfg(feature = "try-runtime")]
const LOG_TARGET: &str = "runtime::bridge-relayers-migration";
impl<T: Config<I>, I: 'static, LaneId: LaneIdType + Send + Sync> UncheckedOnRuntimeUpgrade
for UncheckedMigrationV1ToV2<T, I, LaneId>
where
<T as Config<I>>::Reward: From<RewardsAccountParams<LaneId>>,
{
fn on_runtime_upgrade() -> Weight {
let mut weight = T::DbWeight::get().reads(1);
// list all rewards (we cannot do this as one step because of `drain` limitation)
let mut rewards_to_migrate =
Vec::with_capacity(v1::RelayerRewards::<T, I, LaneId>::iter().count());
for (key1, key2, reward) in v1::RelayerRewards::<T, I, LaneId>::drain() {
rewards_to_migrate.push((key1, key2, reward));
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
}
// re-register rewards with new format.
for (key1, key2, reward) in rewards_to_migrate {
// convert old key to the new
let new_key2: T::Reward = key2.into();
// re-register reward (drained above)
Pezpallet::<T, I>::register_relayer_reward(new_key2, &key1, reward);
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
}
weight
}
#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, pezsp_runtime::DispatchError> {
use codec::Encode;
use pezframe_support::BoundedBTreeMap;
use pezsp_runtime::traits::ConstU32;
// collect actual rewards
let mut rewards: BoundedBTreeMap<
(T::AccountId, Vec<u8>),
T::RewardBalance,
ConstU32<{ u32::MAX }>,
> = BoundedBTreeMap::new();
for (key1, key2, reward) in v1::RelayerRewards::<T, I, LaneId>::iter() {
let new_key2: T::Reward = key2.into();
tracing::info!(target: LOG_TARGET, ?key1, ?key2, ?new_key2, ?reward, "Reward to migrate");
rewards = rewards
.try_mutate(|inner| {
inner
.entry((key1.clone(), new_key2.encode()))
.and_modify(|value| *value += reward)
.or_insert(reward);
})
.unwrap();
}
tracing::info!(target: LOG_TARGET, ?rewards, "Found total rewards to migrate");
Ok(rewards.encode())
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(state: Vec<u8>) -> Result<(), pezsp_runtime::DispatchError> {
use codec::{Decode, Encode};
use pezframe_support::BoundedBTreeMap;
use pezsp_runtime::traits::ConstU32;
let rewards_before: BoundedBTreeMap<
(T::AccountId, Vec<u8>),
T::RewardBalance,
ConstU32<{ u32::MAX }>,
> = Decode::decode(&mut &state[..]).unwrap();
// collect migrated rewards
let mut rewards_after: BoundedBTreeMap<
(T::AccountId, Vec<u8>),
T::RewardBalance,
ConstU32<{ u32::MAX }>,
> = BoundedBTreeMap::new();
for (key1, key2, reward) in v2::RelayerRewards::<T, I>::iter() {
tracing::info!(target: LOG_TARGET, ?key1, ?key2, ?reward, "Migrated rewards");
rewards_after = rewards_after
.try_mutate(|inner| {
inner
.entry((key1.clone(), key2.encode()))
.and_modify(|value| *value += reward)
.or_insert(reward);
})
.unwrap();
}
tracing::info!(target: LOG_TARGET, ?rewards_after, "Found total migrated rewards");
pezframe_support::ensure!(
rewards_before == rewards_after,
"The rewards were not migrated correctly!."
);
tracing::info!(target: LOG_TARGET, "migrated all.");
Ok(())
}
}
/// [`UncheckedMigrationV1ToV2`] wrapped in a
/// [`VersionedMigration`](pezframe_support::migrations::VersionedMigration), ensuring the
/// migration is only performed when on-chain version is 1.
pub type MigrationToV2<T, I, LaneId> = pezframe_support::migrations::VersionedMigration<
1,
2,
UncheckedMigrationV1ToV2<T, I, LaneId>,
Pezpallet<T, I>,
<T as pezframe_system::Config>::DbWeight,
>;
}
+423
View File
@@ -0,0 +1,423 @@
// 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/>.
#![cfg(test)]
use crate as pezpallet_bridge_relayers;
use bp_header_pez_chain::ChainWithGrandpa;
use bp_messages::{
target_chain::{DispatchMessage, MessageDispatch},
ChainWithMessages, HashedLaneId, LaneIdType, MessageNonce,
};
use bp_relayers::{
PayRewardFromAccount, PaymentProcedure, RewardsAccountOwner, RewardsAccountParams,
};
use pezbp_runtime::{messages::MessageDispatchResult, Chain, ChainId, Teyrchain};
use bp_teyrchains::SingleParaStoredHeaderDataBuilder;
use codec::Encode;
use pezframe_support::{
derive_impl, parameter_types,
traits::fungible::Mutate,
weights::{ConstantMultiplier, IdentityFee, RuntimeDbWeight, Weight},
};
use pezpallet_transaction_payment::Multiplier;
use pezsp_core::H256;
use pezsp_runtime::{
traits::{BlakeTwo256, ConstU32, ConstU64, ConstU8},
BuildStorage, FixedPointNumber, Perquintill, StateVersion,
};
/// Account identifier at `ThisChain`.
pub type ThisChainAccountId = u64;
/// Balance at `ThisChain`.
pub type ThisChainBalance = u64;
/// Block number at `ThisChain`.
pub type ThisChainBlockNumber = u32;
/// Hash at `ThisChain`.
pub type ThisChainHash = H256;
/// Hasher at `ThisChain`.
pub type ThisChainHasher = BlakeTwo256;
/// Header of `ThisChain`.
pub type ThisChainHeader = pezsp_runtime::generic::Header<ThisChainBlockNumber, ThisChainHasher>;
/// Block of `ThisChain`.
pub type ThisChainBlock = pezframe_system::mocking::MockBlockU32<TestRuntime>;
/// Account identifier at the `BridgedChain`.
pub type BridgedChainAccountId = u128;
/// Balance at the `BridgedChain`.
pub type BridgedChainBalance = u128;
/// Block number at the `BridgedChain`.
pub type BridgedChainBlockNumber = u32;
/// Hash at the `BridgedChain`.
pub type BridgedChainHash = H256;
/// Hasher at the `BridgedChain`.
pub type BridgedChainHasher = BlakeTwo256;
/// Header of the `BridgedChain`.
pub type BridgedChainHeader =
pezsp_runtime::generic::Header<BridgedChainBlockNumber, BridgedChainHasher>;
/// Bridged chain id used in tests.
pub const TEST_BRIDGED_CHAIN_ID: ChainId = *b"brdg";
/// Maximal extrinsic size at the `BridgedChain`.
pub const BRIDGED_CHAIN_MAX_EXTRINSIC_SIZE: u32 = 1024;
/// Lane identifier type used for tests.
pub type TestLaneIdType = HashedLaneId;
/// Lane that we're using in tests.
pub fn test_lane_id() -> TestLaneIdType {
TestLaneIdType::try_new(1, 2).unwrap()
}
/// Reward measurement type.
pub type RewardBalance = u64;
/// Underlying chain of `ThisChain`.
pub struct ThisUnderlyingChain;
impl Chain for ThisUnderlyingChain {
const ID: ChainId = *b"tuch";
type BlockNumber = ThisChainBlockNumber;
type Hash = ThisChainHash;
type Hasher = ThisChainHasher;
type Header = ThisChainHeader;
type AccountId = ThisChainAccountId;
type Balance = ThisChainBalance;
type Nonce = u32;
type Signature = pezsp_runtime::MultiSignature;
const STATE_VERSION: StateVersion = StateVersion::V1;
fn max_extrinsic_size() -> u32 {
BRIDGED_CHAIN_MAX_EXTRINSIC_SIZE
}
fn max_extrinsic_weight() -> Weight {
Weight::zero()
}
}
impl ChainWithMessages for ThisUnderlyingChain {
const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str = "";
const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 16;
const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 1000;
}
/// Underlying chain of `BridgedChain`.
pub struct BridgedUnderlyingTeyrchain;
impl Chain for BridgedUnderlyingTeyrchain {
const ID: ChainId = TEST_BRIDGED_CHAIN_ID;
type BlockNumber = BridgedChainBlockNumber;
type Hash = BridgedChainHash;
type Hasher = BridgedChainHasher;
type Header = BridgedChainHeader;
type AccountId = BridgedChainAccountId;
type Balance = BridgedChainBalance;
type Nonce = u32;
type Signature = pezsp_runtime::MultiSignature;
const STATE_VERSION: StateVersion = StateVersion::V1;
fn max_extrinsic_size() -> u32 {
BRIDGED_CHAIN_MAX_EXTRINSIC_SIZE
}
fn max_extrinsic_weight() -> Weight {
Weight::zero()
}
}
impl ChainWithGrandpa for BridgedUnderlyingTeyrchain {
const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str = "";
const MAX_AUTHORITIES_COUNT: u32 = 16;
const REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY: u32 = 8;
const MAX_MANDATORY_HEADER_SIZE: u32 = 256;
const AVERAGE_HEADER_SIZE: u32 = 64;
}
impl ChainWithMessages for BridgedUnderlyingTeyrchain {
const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str = "";
const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 16;
const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 1000;
}
impl Teyrchain for BridgedUnderlyingTeyrchain {
const TEYRCHAIN_ID: u32 = 42;
const MAX_HEADER_SIZE: u32 = 1_024;
}
pub type TestStakeAndSlash = pezpallet_bridge_relayers::StakeAndSlashNamed<
ThisChainAccountId,
ThisChainBlockNumber,
Balances,
ReserveId,
Stake,
Lease,
>;
pezframe_support::construct_runtime! {
pub enum TestRuntime
{
System: pezframe_system,
Utility: pezpallet_utility,
Balances: pezpallet_balances,
TransactionPayment: pezpallet_transaction_payment,
BridgeRelayers: pezpallet_bridge_relayers,
BridgeGrandpa: pezpallet_bridge_grandpa,
BridgeTeyrchains: pezpallet_bridge_teyrchains,
BridgeMessages: pezpallet_bridge_messages,
}
}
parameter_types! {
pub const BridgedParasPalletName: &'static str = "Paras";
pub const DbWeight: RuntimeDbWeight = RuntimeDbWeight { read: 1, write: 2 };
pub const ExistentialDeposit: ThisChainBalance = 1;
pub const ReserveId: [u8; 8] = *b"brdgrlrs";
pub const Stake: ThisChainBalance = 1_000;
pub const Lease: ThisChainBlockNumber = 8;
pub const TargetBlockFullness: Perquintill = Perquintill::from_percent(25);
pub const TransactionBaseFee: ThisChainBalance = 0;
pub const TransactionByteFee: ThisChainBalance = 1;
pub AdjustmentVariable: Multiplier = Multiplier::saturating_from_rational(3, 100_000);
pub MinimumMultiplier: Multiplier = Multiplier::saturating_from_rational(1, 1_000_000u128);
pub MaximumMultiplier: Multiplier = pezsp_runtime::traits::Bounded::max_value();
}
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
impl pezframe_system::Config for TestRuntime {
type Block = ThisChainBlock;
// TODO: remove when https://github.com/pezkuwichain/kurdistan-sdk/issues/120 merged
type BlockHashCount = ConstU32<10>;
type AccountData = pezpallet_balances::AccountData<ThisChainBalance>;
type DbWeight = DbWeight;
}
#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
impl pezpallet_balances::Config for TestRuntime {
type ReserveIdentifier = [u8; 8];
type AccountStore = System;
}
impl pezpallet_utility::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type RuntimeCall = RuntimeCall;
type PalletsOrigin = OriginCaller;
type WeightInfo = ();
}
#[derive_impl(pezpallet_transaction_payment::config_preludes::TestDefaultConfig)]
impl pezpallet_transaction_payment::Config for TestRuntime {
type OnChargeTransaction = pezpallet_transaction_payment::FungibleAdapter<Balances, ()>;
type OperationalFeeMultiplier = ConstU8<5>;
type WeightToFee = IdentityFee<ThisChainBalance>;
type LengthToFee = ConstantMultiplier<ThisChainBalance, TransactionByteFee>;
type FeeMultiplierUpdate = pezpallet_transaction_payment::TargetedFeeAdjustment<
TestRuntime,
TargetBlockFullness,
AdjustmentVariable,
MinimumMultiplier,
MaximumMultiplier,
>;
type RuntimeEvent = RuntimeEvent;
}
impl pezpallet_bridge_grandpa::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type BridgedChain = BridgedUnderlyingTeyrchain;
type MaxFreeHeadersPerBlock = ConstU32<4>;
type FreeHeadersInterval = ConstU32<1_024>;
type HeadersToKeep = ConstU32<8>;
type WeightInfo = pezpallet_bridge_grandpa::weights::BridgeWeight<TestRuntime>;
}
impl pezpallet_bridge_teyrchains::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type BridgesGrandpaPalletInstance = ();
type ParasPalletName = BridgedParasPalletName;
type ParaStoredHeaderDataBuilder =
SingleParaStoredHeaderDataBuilder<BridgedUnderlyingTeyrchain>;
type HeadsToKeep = ConstU32<8>;
type MaxParaHeadDataSize = ConstU32<1024>;
type WeightInfo = pezpallet_bridge_teyrchains::weights::BridgeWeight<TestRuntime>;
type OnNewHead = ();
}
impl pezpallet_bridge_messages::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = pezpallet_bridge_messages::weights::BridgeWeight<TestRuntime>;
type OutboundPayload = Vec<u8>;
type InboundPayload = Vec<u8>;
type LaneId = TestLaneIdType;
type DeliveryPayments = ();
type DeliveryConfirmationPayments = pezpallet_bridge_relayers::DeliveryConfirmationPaymentsAdapter<
TestRuntime,
(),
(),
ConstU64<100_000>,
>;
type OnMessagesDelivered = ();
type MessageDispatch = DummyMessageDispatch;
type ThisChain = ThisUnderlyingChain;
type BridgedChain = BridgedUnderlyingTeyrchain;
type BridgedHeaderChain = BridgeGrandpa;
}
impl pezpallet_bridge_relayers::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type RewardBalance = RewardBalance;
type Reward = RewardsAccountParams<pezpallet_bridge_messages::LaneIdOf<TestRuntime, ()>>;
type PaymentProcedure = TestPaymentProcedure;
type StakeAndSlash = TestStakeAndSlash;
type Balance = ThisChainBalance;
type WeightInfo = ();
}
#[cfg(feature = "runtime-benchmarks")]
impl pezpallet_bridge_relayers::benchmarking::Config for TestRuntime {
fn bench_reward() -> Self::Reward {
RewardsAccountParams::new(
TestLaneIdType::default(),
*b"test",
RewardsAccountOwner::ThisChain,
)
}
fn prepare_rewards_account(
account_params: RewardsAccountParams<TestLaneIdType>,
reward: Self::RewardBalance,
) -> Option<ThisChainAccountId> {
let rewards_account = PayRewardFromAccount::<
Balances,
ThisChainAccountId,
TestLaneIdType,
RewardBalance,
>::rewards_account(account_params);
Self::deposit_account(rewards_account, reward.into());
Some(REGULAR_RELAYER2)
}
fn deposit_account(account: Self::AccountId, balance: Self::Balance) {
pezframe_support::assert_ok!(Balances::mint_into(
&account,
balance.saturating_add(ExistentialDeposit::get())
));
}
}
/// Regular relayer that may receive rewards.
pub const REGULAR_RELAYER: ThisChainAccountId = 1;
/// Regular relayer that may receive rewards.
pub const REGULAR_RELAYER2: ThisChainAccountId = 3;
/// Relayer that can't receive rewards.
pub const FAILING_RELAYER: ThisChainAccountId = 2;
/// Relayer that is able to register.
pub const REGISTER_RELAYER: ThisChainAccountId = 42;
/// Payment procedure that rejects payments to the `FAILING_RELAYER`.
pub struct TestPaymentProcedure;
impl TestPaymentProcedure {
pub fn rewards_account(params: RewardsAccountParams<TestLaneIdType>) -> ThisChainAccountId {
PayRewardFromAccount::<(), ThisChainAccountId, TestLaneIdType, RewardBalance>::rewards_account(
params,
)
}
}
impl PaymentProcedure<ThisChainAccountId, RewardsAccountParams<TestLaneIdType>, RewardBalance>
for TestPaymentProcedure
{
type Error = ();
type Beneficiary = ThisChainAccountId;
fn pay_reward(
relayer: &ThisChainAccountId,
_reward_kind: RewardsAccountParams<TestLaneIdType>,
_reward: RewardBalance,
_beneficiary: Self::Beneficiary,
) -> Result<(), Self::Error> {
match *relayer {
FAILING_RELAYER => Err(()),
_ => Ok(()),
}
}
}
/// Dummy message dispatcher.
pub struct DummyMessageDispatch;
impl DummyMessageDispatch {
pub fn deactivate(lane: TestLaneIdType) {
pezframe_support::storage::unhashed::put(&(b"inactive", lane).encode()[..], &false);
}
}
impl MessageDispatch for DummyMessageDispatch {
type DispatchPayload = Vec<u8>;
type DispatchLevelResult = ();
type LaneId = TestLaneIdType;
fn is_active(lane: Self::LaneId) -> bool {
pezframe_support::storage::unhashed::take::<bool>(&(b"inactive", lane).encode()[..]) !=
Some(false)
}
fn dispatch_weight(
_message: &mut DispatchMessage<Self::DispatchPayload, Self::LaneId>,
) -> Weight {
Weight::zero()
}
fn dispatch(
_: DispatchMessage<Self::DispatchPayload, Self::LaneId>,
) -> MessageDispatchResult<Self::DispatchLevelResult> {
MessageDispatchResult { unspent_weight: Weight::zero(), dispatch_level_result: () }
}
}
/// Reward account params that we are using in tests.
pub fn test_reward_account_param() -> RewardsAccountParams<TestLaneIdType> {
RewardsAccountParams::new(
TestLaneIdType::try_new(1, 2).unwrap(),
*b"test",
RewardsAccountOwner::ThisChain,
)
}
/// Return test externalities to use in tests.
pub fn new_test_ext() -> pezsp_io::TestExternalities {
let t = pezframe_system::GenesisConfig::<TestRuntime>::default().build_storage().unwrap();
pezsp_io::TestExternalities::new(t)
}
/// Run pezpallet test.
pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
new_test_ext().execute_with(|| {
Balances::mint_into(&REGISTER_RELAYER, ExistentialDeposit::get() + 10 * Stake::get())
.unwrap();
test()
})
}
@@ -0,0 +1,226 @@
// 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/>.
//! Code that allows relayers pezpallet to be used as a payment mechanism for
//! the `pezpallet-bridge-messages` pezpallet using `RewardsAccountParams`.
use crate::{Config, Pezpallet};
use alloc::collections::vec_deque::VecDeque;
use bp_messages::{
source_chain::{DeliveryConfirmationPayments, RelayersRewards},
MessageNonce,
};
pub use bp_relayers::PayRewardFromAccount;
use bp_relayers::{RewardsAccountOwner, RewardsAccountParams};
use pezbp_runtime::Chain;
use core::{marker::PhantomData, ops::RangeInclusive};
use pezframe_support::{pezsp_runtime::SaturatedConversion, traits::Get};
use pezpallet_bridge_messages::LaneIdOf;
use pezsp_arithmetic::traits::{Saturating, Zero};
/// Adapter that allows relayers pezpallet to be used as a delivery+dispatch payment mechanism
/// for the `pezpallet-bridge-messages` pezpallet and using `RewardsAccountParams`.
pub struct DeliveryConfirmationPaymentsAdapter<T, MI, RI, DeliveryReward>(
PhantomData<(T, MI, RI, DeliveryReward)>,
);
impl<T, MI, RI, DeliveryReward> DeliveryConfirmationPayments<T::AccountId, LaneIdOf<T, MI>>
for DeliveryConfirmationPaymentsAdapter<T, MI, RI, DeliveryReward>
where
T: Config<RI> + pezpallet_bridge_messages::Config<MI>,
MI: 'static,
RI: 'static,
DeliveryReward: Get<T::RewardBalance>,
<T as Config<RI>>::Reward: From<RewardsAccountParams<LaneIdOf<T, MI>>>,
{
type Error = &'static str;
fn pay_reward(
lane_id: LaneIdOf<T, MI>,
pez_messages_relayers: VecDeque<bp_messages::UnrewardedRelayer<T::AccountId>>,
confirmation_relayer: &T::AccountId,
received_range: &RangeInclusive<bp_messages::MessageNonce>,
) -> MessageNonce {
let relayers_rewards =
bp_messages::calc_relayers_rewards::<T::AccountId>(pez_messages_relayers, received_range);
let rewarded_relayers = relayers_rewards.len();
register_relayers_rewards::<T, RI, MI>(
confirmation_relayer,
relayers_rewards,
RewardsAccountParams::new(
lane_id,
T::BridgedChain::ID,
RewardsAccountOwner::BridgedChain,
),
DeliveryReward::get(),
);
rewarded_relayers as _
}
}
// Update rewards to given relayers, optionally rewarding confirmation relayer.
fn register_relayers_rewards<
T: Config<RI> + pezpallet_bridge_messages::Config<MI>,
RI: 'static,
MI: 'static,
>(
confirmation_relayer: &T::AccountId,
relayers_rewards: RelayersRewards<T::AccountId>,
lane_id: RewardsAccountParams<LaneIdOf<T, MI>>,
delivery_fee: T::RewardBalance,
) where
<T as Config<RI>>::Reward: From<RewardsAccountParams<LaneIdOf<T, MI>>>,
{
// reward every relayer except `confirmation_relayer`
let mut confirmation_relayer_reward = T::RewardBalance::zero();
for (relayer, messages) in relayers_rewards {
// sane runtime configurations guarantee that the number of messages will be below
// `u32::MAX`
let relayer_reward =
T::RewardBalance::saturated_from(messages).saturating_mul(delivery_fee);
if relayer != *confirmation_relayer {
Pezpallet::<T, RI>::register_relayer_reward(lane_id.into(), &relayer, relayer_reward);
} else {
confirmation_relayer_reward =
confirmation_relayer_reward.saturating_add(relayer_reward);
}
}
// finally - pay reward to confirmation relayer
Pezpallet::<T, RI>::register_relayer_reward(
lane_id.into(),
confirmation_relayer,
confirmation_relayer_reward,
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{mock::*, RelayerRewards};
use bp_messages::LaneIdType;
use bp_relayers::PaymentProcedure;
use pezframe_support::{
assert_ok,
traits::fungible::{Inspect, Mutate},
};
const RELAYER_1: ThisChainAccountId = 1;
const RELAYER_2: ThisChainAccountId = 2;
const RELAYER_3: ThisChainAccountId = 3;
fn relayers_rewards() -> RelayersRewards<ThisChainAccountId> {
vec![(RELAYER_1, 2), (RELAYER_2, 3)].into_iter().collect()
}
#[test]
fn confirmation_relayer_is_rewarded_if_it_has_also_delivered_messages() {
run_test(|| {
register_relayers_rewards::<TestRuntime, (), ()>(
&RELAYER_2,
relayers_rewards(),
test_reward_account_param(),
50,
);
assert_eq!(
RelayerRewards::<TestRuntime>::get(RELAYER_1, test_reward_account_param()),
Some(100)
);
assert_eq!(
RelayerRewards::<TestRuntime>::get(RELAYER_2, test_reward_account_param()),
Some(150)
);
});
}
#[test]
fn confirmation_relayer_is_not_rewarded_if_it_has_not_delivered_any_messages() {
run_test(|| {
register_relayers_rewards::<TestRuntime, (), ()>(
&RELAYER_3,
relayers_rewards(),
test_reward_account_param(),
50,
);
assert_eq!(
RelayerRewards::<TestRuntime>::get(RELAYER_1, test_reward_account_param()),
Some(100)
);
assert_eq!(
RelayerRewards::<TestRuntime>::get(RELAYER_2, test_reward_account_param()),
Some(150)
);
assert_eq!(
RelayerRewards::<TestRuntime>::get(RELAYER_3, test_reward_account_param()),
None
);
});
}
#[test]
fn pay_reward_from_account_actually_pays_reward() {
type Balances = pezpallet_balances::Pezpallet<TestRuntime>;
type PayLaneRewardFromAccount =
PayRewardFromAccount<Balances, ThisChainAccountId, TestLaneIdType, RewardBalance>;
run_test(|| {
let in_lane_0 = RewardsAccountParams::new(
TestLaneIdType::try_new(1, 2).unwrap(),
*b"test",
RewardsAccountOwner::ThisChain,
);
let out_lane_1 = RewardsAccountParams::new(
TestLaneIdType::try_new(1, 3).unwrap(),
*b"test",
RewardsAccountOwner::BridgedChain,
);
let in_lane0_rewards_account = PayLaneRewardFromAccount::rewards_account(in_lane_0);
let out_lane1_rewards_account = PayLaneRewardFromAccount::rewards_account(out_lane_1);
assert_ok!(Balances::mint_into(&in_lane0_rewards_account, 200));
assert_ok!(Balances::mint_into(&out_lane1_rewards_account, 100));
assert_eq!(Balances::balance(&in_lane0_rewards_account), 200);
assert_eq!(Balances::balance(&out_lane1_rewards_account), 100);
assert_eq!(Balances::balance(&1), 0);
assert_eq!(Balances::balance(&2), 0);
assert_ok!(PayLaneRewardFromAccount::pay_reward(&1, in_lane_0, 100, 1_u64));
assert_eq!(Balances::balance(&in_lane0_rewards_account), 100);
assert_eq!(Balances::balance(&out_lane1_rewards_account), 100);
assert_eq!(Balances::balance(&1), 100);
assert_eq!(Balances::balance(&2), 0);
assert_ok!(PayLaneRewardFromAccount::pay_reward(&1, out_lane_1, 100, 1_u64));
assert_eq!(Balances::balance(&in_lane0_rewards_account), 100);
assert_eq!(Balances::balance(&out_lane1_rewards_account), 0);
assert_eq!(Balances::balance(&1), 200);
assert_eq!(Balances::balance(&2), 0);
assert_ok!(PayLaneRewardFromAccount::pay_reward(&1, in_lane_0, 100, 2_u64));
assert_eq!(Balances::balance(&in_lane0_rewards_account), 0);
assert_eq!(Balances::balance(&out_lane1_rewards_account), 0);
assert_eq!(Balances::balance(&1), 200);
assert_eq!(Balances::balance(&2), 100);
});
}
}
@@ -0,0 +1,203 @@
// 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/>.
//! Code that allows `NamedReservableCurrency` to be used as a `StakeAndSlash`
//! mechanism of the relayers pezpallet.
use bp_relayers::StakeAndSlash;
use codec::Codec;
use core::{fmt::Debug, marker::PhantomData};
use pezframe_support::traits::{tokens::BalanceStatus, NamedReservableCurrency};
use pezsp_runtime::{traits::Get, DispatchError, DispatchResult};
/// `StakeAndSlash` that works with `NamedReservableCurrency` and uses named
/// reservations.
///
/// **WARNING**: this implementation assumes that the relayers pezpallet is configured to
/// use the [`bp_relayers::PayRewardFromAccount`] as its relayers payment scheme.
pub struct StakeAndSlashNamed<AccountId, BlockNumber, Currency, ReserveId, Stake, Lease>(
PhantomData<(AccountId, BlockNumber, Currency, ReserveId, Stake, Lease)>,
);
impl<AccountId, BlockNumber, Currency, ReserveId, Stake, Lease>
StakeAndSlash<AccountId, BlockNumber, Currency::Balance>
for StakeAndSlashNamed<AccountId, BlockNumber, Currency, ReserveId, Stake, Lease>
where
AccountId: Codec + Debug,
Currency: NamedReservableCurrency<AccountId>,
ReserveId: Get<Currency::ReserveIdentifier>,
Stake: Get<Currency::Balance>,
Lease: Get<BlockNumber>,
{
type RequiredStake = Stake;
type RequiredRegistrationLease = Lease;
fn reserve(relayer: &AccountId, amount: Currency::Balance) -> DispatchResult {
Currency::reserve_named(&ReserveId::get(), relayer, amount)
}
fn unreserve(relayer: &AccountId, amount: Currency::Balance) -> Currency::Balance {
Currency::unreserve_named(&ReserveId::get(), relayer, amount)
}
fn repatriate_reserved(
relayer: &AccountId,
beneficiary: &AccountId,
amount: Currency::Balance,
) -> Result<Currency::Balance, DispatchError> {
Currency::repatriate_reserved_named(
&ReserveId::get(),
relayer,
&beneficiary,
amount,
BalanceStatus::Free,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::*;
use bp_relayers::ExplicitOrAccountParams;
use pezframe_support::traits::fungible::Mutate;
use pezsp_runtime::traits::IdentifyAccount;
fn test_stake() -> ThisChainBalance {
Stake::get()
}
#[test]
fn reserve_works() {
run_test(|| {
assert!(TestStakeAndSlash::reserve(&1, test_stake()).is_err());
assert_eq!(Balances::free_balance(1), 0);
assert_eq!(Balances::reserved_balance(1), 0);
Balances::mint_into(&2, test_stake() - 1).unwrap();
assert!(TestStakeAndSlash::reserve(&2, test_stake()).is_err());
assert_eq!(Balances::free_balance(2), test_stake() - 1);
assert_eq!(Balances::reserved_balance(2), 0);
Balances::mint_into(&3, test_stake() * 2).unwrap();
assert_eq!(TestStakeAndSlash::reserve(&3, test_stake()), Ok(()));
assert_eq!(Balances::free_balance(3), test_stake());
assert_eq!(Balances::reserved_balance(3), test_stake());
})
}
#[test]
fn unreserve_works() {
run_test(|| {
assert_eq!(TestStakeAndSlash::unreserve(&1, test_stake()), test_stake());
assert_eq!(Balances::free_balance(1), 0);
assert_eq!(Balances::reserved_balance(1), 0);
Balances::mint_into(&2, test_stake() * 2).unwrap();
TestStakeAndSlash::reserve(&2, test_stake() / 3).unwrap();
assert_eq!(
TestStakeAndSlash::unreserve(&2, test_stake()),
test_stake() - test_stake() / 3
);
assert_eq!(Balances::free_balance(2), test_stake() * 2);
assert_eq!(Balances::reserved_balance(2), 0);
Balances::mint_into(&3, test_stake() * 2).unwrap();
TestStakeAndSlash::reserve(&3, test_stake()).unwrap();
assert_eq!(TestStakeAndSlash::unreserve(&3, test_stake()), 0);
assert_eq!(Balances::free_balance(3), test_stake() * 2);
assert_eq!(Balances::reserved_balance(3), 0);
})
}
#[test]
fn repatriate_reserved_works() {
run_test(|| {
let beneficiary = test_reward_account_param();
let beneficiary_account = TestPaymentProcedure::rewards_account(beneficiary);
let mut expected_balance = ExistentialDeposit::get();
Balances::mint_into(&beneficiary_account, expected_balance).unwrap();
assert_eq!(
TestStakeAndSlash::repatriate_reserved(
&1,
&(ExplicitOrAccountParams::Params(beneficiary).into_account()),
test_stake()
),
Ok(test_stake())
);
assert_eq!(Balances::free_balance(1), 0);
assert_eq!(Balances::reserved_balance(1), 0);
assert_eq!(Balances::free_balance(beneficiary_account), expected_balance);
assert_eq!(Balances::reserved_balance(beneficiary_account), 0);
expected_balance += test_stake() / 3;
Balances::mint_into(&2, test_stake() * 2).unwrap();
TestStakeAndSlash::reserve(&2, test_stake() / 3).unwrap();
assert_eq!(
TestStakeAndSlash::repatriate_reserved(
&2,
&(ExplicitOrAccountParams::Params(beneficiary).into_account()),
test_stake()
),
Ok(test_stake() - test_stake() / 3)
);
assert_eq!(Balances::free_balance(2), test_stake() * 2 - test_stake() / 3);
assert_eq!(Balances::reserved_balance(2), 0);
assert_eq!(Balances::free_balance(beneficiary_account), expected_balance);
assert_eq!(Balances::reserved_balance(beneficiary_account), 0);
expected_balance += test_stake();
Balances::mint_into(&3, test_stake() * 2).unwrap();
TestStakeAndSlash::reserve(&3, test_stake()).unwrap();
assert_eq!(
TestStakeAndSlash::repatriate_reserved(
&3,
&(ExplicitOrAccountParams::Params(beneficiary).into_account()),
test_stake()
),
Ok(0)
);
assert_eq!(Balances::free_balance(3), test_stake());
assert_eq!(Balances::reserved_balance(3), 0);
assert_eq!(Balances::free_balance(beneficiary_account), expected_balance);
assert_eq!(Balances::reserved_balance(beneficiary_account), 0);
})
}
#[test]
fn repatriate_reserved_doesnt_work_when_beneficiary_account_is_missing() {
run_test(|| {
let beneficiary = test_reward_account_param();
let beneficiary_account = TestPaymentProcedure::rewards_account(beneficiary);
Balances::mint_into(&3, test_stake() * 2).unwrap();
TestStakeAndSlash::reserve(&3, test_stake()).unwrap();
assert!(TestStakeAndSlash::repatriate_reserved(
&3,
&(ExplicitOrAccountParams::Params(beneficiary).into_account()),
test_stake()
)
.is_err());
assert_eq!(Balances::free_balance(3), test_stake());
assert_eq!(Balances::reserved_balance(3), test_stake());
assert_eq!(Balances::free_balance(beneficiary_account), 0);
assert_eq!(Balances::reserved_balance(beneficiary_account), 0);
});
}
}
+306
View File
@@ -0,0 +1,306 @@
// 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/>.
//! Autogenerated weights for pezpallet_bridge_relayers
//!
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2023-04-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `covid`, CPU: `11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz`
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
// Executed Command:
// target/release/rip-bridge-node
// benchmark
// pezpallet
// --chain=dev
// --steps=50
// --repeat=20
// --pezpallet=pezpallet_bridge_relayers
// --extrinsic=*
// --execution=wasm
// --wasm-execution=Compiled
// --heap-pages=4096
// --output=./modules/relayers/src/weights.rs
// --template=./.maintain/bridge-weight-template.hbs
#![allow(clippy::all)]
#![allow(unused_parens)]
#![allow(unused_imports)]
#![allow(missing_docs)]
use core::marker::PhantomData;
use pezframe_support::{
traits::Get,
weights::{constants::RocksDbWeight, Weight},
};
/// Weight functions needed for pezpallet_bridge_relayers.
pub trait WeightInfo {
fn claim_rewards() -> Weight;
fn claim_rewards_to() -> Weight;
fn register() -> Weight;
fn deregister() -> Weight;
fn slash_and_deregister() -> Weight;
fn register_relayer_reward() -> Weight;
}
/// Weights for `pezpallet_bridge_relayers` that are generated using one of the Bridge testnets.
///
/// Those weights are test only and must never be used in production.
pub struct BridgeWeight<T>(PhantomData<T>);
impl<T: pezframe_system::Config> WeightInfo for BridgeWeight<T> {
/// Storage: BridgeRelayers RelayerRewards (r:1 w:1)
///
/// Proof: BridgeRelayers RelayerRewards (max_values: None, max_size: Some(65), added: 2540,
/// mode: MaxEncodedLen)
///
/// Storage: Balances TotalIssuance (r:1 w:0)
///
/// Proof: Balances TotalIssuance (max_values: Some(1), max_size: Some(8), added: 503, mode:
/// MaxEncodedLen)
///
/// Storage: System Account (r:1 w:1)
///
/// Proof: System Account (max_values: None, max_size: Some(104), added: 2579, mode:
/// MaxEncodedLen)
fn claim_rewards() -> Weight {
// Proof Size summary in bytes:
// Measured: `294`
// Estimated: `8592`
// Minimum execution time: 77_614 nanoseconds.
Weight::from_parts(79_987_000, 8592)
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(2_u64))
}
/// Storage: BridgeRelayers RelayerRewards (r:1 w:1)
///
/// Proof: BridgeRelayers RelayerRewards (max_values: None, max_size: Some(65), added: 2540,
/// mode: MaxEncodedLen)
///
/// Storage: Balances TotalIssuance (r:1 w:0)
///
/// Proof: Balances TotalIssuance (max_values: Some(1), max_size: Some(8), added: 503, mode:
/// MaxEncodedLen)
///
/// Storage: System Account (r:1 w:1)
///
/// Proof: System Account (max_values: None, max_size: Some(104), added: 2579, mode:
/// MaxEncodedLen)
fn claim_rewards_to() -> Weight {
// Proof Size summary in bytes:
// Measured: `294`
// Estimated: `8592`
// Minimum execution time: 77_614 nanoseconds.
Weight::from_parts(79_987_000, 8592)
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(2_u64))
}
/// Storage: BridgeRelayers RegisteredRelayers (r:1 w:1)
///
/// Proof: BridgeRelayers RegisteredRelayers (max_values: None, max_size: Some(64), added: 2539,
/// mode: MaxEncodedLen)
///
/// Storage: Balances Reserves (r:1 w:1)
///
/// Proof: Balances Reserves (max_values: None, max_size: Some(849), added: 3324, mode:
/// MaxEncodedLen)
fn register() -> Weight {
// Proof Size summary in bytes:
// Measured: `87`
// Estimated: `7843`
// Minimum execution time: 39_590 nanoseconds.
Weight::from_parts(40_546_000, 7843)
.saturating_add(T::DbWeight::get().reads(2_u64))
.saturating_add(T::DbWeight::get().writes(2_u64))
}
/// Storage: BridgeRelayers RegisteredRelayers (r:1 w:1)
///
/// Proof: BridgeRelayers RegisteredRelayers (max_values: None, max_size: Some(64), added: 2539,
/// mode: MaxEncodedLen)
///
/// Storage: Balances Reserves (r:1 w:1)
///
/// Proof: Balances Reserves (max_values: None, max_size: Some(849), added: 3324, mode:
/// MaxEncodedLen)
fn deregister() -> Weight {
// Proof Size summary in bytes:
// Measured: `264`
// Estimated: `7843`
// Minimum execution time: 43_332 nanoseconds.
Weight::from_parts(45_087_000, 7843)
.saturating_add(T::DbWeight::get().reads(2_u64))
.saturating_add(T::DbWeight::get().writes(2_u64))
}
/// Storage: BridgeRelayers RegisteredRelayers (r:1 w:1)
///
/// Proof: BridgeRelayers RegisteredRelayers (max_values: None, max_size: Some(64), added: 2539,
/// mode: MaxEncodedLen)
///
/// Storage: Balances Reserves (r:1 w:1)
///
/// Proof: Balances Reserves (max_values: None, max_size: Some(849), added: 3324, mode:
/// MaxEncodedLen)
///
/// Storage: System Account (r:1 w:1)
///
/// Proof: System Account (max_values: None, max_size: Some(104), added: 2579, mode:
/// MaxEncodedLen)
fn slash_and_deregister() -> Weight {
// Proof Size summary in bytes:
// Measured: `380`
// Estimated: `11412`
// Minimum execution time: 42_358 nanoseconds.
Weight::from_parts(43_539_000, 11412)
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(3_u64))
}
/// Storage: BridgeRelayers RelayerRewards (r:1 w:1)
///
/// Proof: BridgeRelayers RelayerRewards (max_values: None, max_size: Some(65), added: 2540,
/// mode: MaxEncodedLen)
fn register_relayer_reward() -> Weight {
// Proof Size summary in bytes:
// Measured: `12`
// Estimated: `3530`
// Minimum execution time: 6_338 nanoseconds.
Weight::from_parts(6_526_000, 3530)
.saturating_add(T::DbWeight::get().reads(1_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
}
// For backwards compatibility and tests
impl WeightInfo for () {
/// Storage: BridgeRelayers RelayerRewards (r:1 w:1)
///
/// Proof: BridgeRelayers RelayerRewards (max_values: None, max_size: Some(65), added: 2540,
/// mode: MaxEncodedLen)
///
/// Storage: Balances TotalIssuance (r:1 w:0)
///
/// Proof: Balances TotalIssuance (max_values: Some(1), max_size: Some(8), added: 503, mode:
/// MaxEncodedLen)
///
/// Storage: System Account (r:1 w:1)
///
/// Proof: System Account (max_values: None, max_size: Some(104), added: 2579, mode:
/// MaxEncodedLen)
fn claim_rewards() -> Weight {
// Proof Size summary in bytes:
// Measured: `294`
// Estimated: `8592`
// Minimum execution time: 77_614 nanoseconds.
Weight::from_parts(79_987_000, 8592)
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
/// Storage: BridgeRelayers RelayerRewards (r:1 w:1)
///
/// Proof: BridgeRelayers RelayerRewards (max_values: None, max_size: Some(65), added: 2540,
/// mode: MaxEncodedLen)
///
/// Storage: Balances TotalIssuance (r:1 w:0)
///
/// Proof: Balances TotalIssuance (max_values: Some(1), max_size: Some(8), added: 503, mode:
/// MaxEncodedLen)
///
/// Storage: System Account (r:1 w:1)
///
/// Proof: System Account (max_values: None, max_size: Some(104), added: 2579, mode:
/// MaxEncodedLen)
fn claim_rewards_to() -> Weight {
// Proof Size summary in bytes:
// Measured: `294`
// Estimated: `8592`
// Minimum execution time: 77_614 nanoseconds.
Weight::from_parts(79_987_000, 8592)
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
/// Storage: BridgeRelayers RegisteredRelayers (r:1 w:1)
///
/// Proof: BridgeRelayers RegisteredRelayers (max_values: None, max_size: Some(64), added: 2539,
/// mode: MaxEncodedLen)
///
/// Storage: Balances Reserves (r:1 w:1)
///
/// Proof: Balances Reserves (max_values: None, max_size: Some(849), added: 3324, mode:
/// MaxEncodedLen)
fn register() -> Weight {
// Proof Size summary in bytes:
// Measured: `87`
// Estimated: `7843`
// Minimum execution time: 39_590 nanoseconds.
Weight::from_parts(40_546_000, 7843)
.saturating_add(RocksDbWeight::get().reads(2_u64))
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
/// Storage: BridgeRelayers RegisteredRelayers (r:1 w:1)
///
/// Proof: BridgeRelayers RegisteredRelayers (max_values: None, max_size: Some(64), added: 2539,
/// mode: MaxEncodedLen)
///
/// Storage: Balances Reserves (r:1 w:1)
///
/// Proof: Balances Reserves (max_values: None, max_size: Some(849), added: 3324, mode:
/// MaxEncodedLen)
fn deregister() -> Weight {
// Proof Size summary in bytes:
// Measured: `264`
// Estimated: `7843`
// Minimum execution time: 43_332 nanoseconds.
Weight::from_parts(45_087_000, 7843)
.saturating_add(RocksDbWeight::get().reads(2_u64))
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
/// Storage: BridgeRelayers RegisteredRelayers (r:1 w:1)
///
/// Proof: BridgeRelayers RegisteredRelayers (max_values: None, max_size: Some(64), added: 2539,
/// mode: MaxEncodedLen)
///
/// Storage: Balances Reserves (r:1 w:1)
///
/// Proof: Balances Reserves (max_values: None, max_size: Some(849), added: 3324, mode:
/// MaxEncodedLen)
///
/// Storage: System Account (r:1 w:1)
///
/// Proof: System Account (max_values: None, max_size: Some(104), added: 2579, mode:
/// MaxEncodedLen)
fn slash_and_deregister() -> Weight {
// Proof Size summary in bytes:
// Measured: `380`
// Estimated: `11412`
// Minimum execution time: 42_358 nanoseconds.
Weight::from_parts(43_539_000, 11412)
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: BridgeRelayers RelayerRewards (r:1 w:1)
///
/// Proof: BridgeRelayers RelayerRewards (max_values: None, max_size: Some(65), added: 2540,
/// mode: MaxEncodedLen)
fn register_relayer_reward() -> Weight {
// Proof Size summary in bytes:
// Measured: `12`
// Estimated: `3530`
// Minimum execution time: 6_338 nanoseconds.
Weight::from_parts(6_526_000, 3530)
.saturating_add(RocksDbWeight::get().reads(1_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
}
@@ -0,0 +1,49 @@
// 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/>.
//! Weight-related utilities.
use crate::weights::WeightInfo;
use pezframe_support::pezpallet_prelude::Weight;
/// Extended weight info.
pub trait WeightInfoExt: WeightInfo {
/// Returns weight, that needs to be added to the pre-dispatch weight of message delivery call,
/// if `BridgeRelayersTransactionExtension` signed extension is deployed at runtime level.
fn receive_messages_proof_overhead_from_runtime() -> Weight {
Self::slash_and_deregister().max(Self::register_relayer_reward())
}
/// Returns weight, that needs to be added to the pre-dispatch weight of message delivery
/// confirmation call, if `BridgeRelayersTransactionExtension` signed extension is deployed at
/// runtime level.
fn receive_messages_delivery_proof_overhead_from_runtime() -> Weight {
Self::register_relayer_reward()
}
/// Returns weight that we need to deduct from the message delivery call weight that has
/// completed successfully.
///
/// Usually, the weight of `slash_and_deregister` is larger than the weight of the
/// `register_relayer_reward`. So if relayer has been rewarded, we want to deduct the difference
/// to get the actual post-dispatch weight.
fn extra_weight_of_successful_receive_messages_proof_call() -> Weight {
Self::slash_and_deregister().saturating_sub(Self::register_relayer_reward())
}
}
impl<T: WeightInfo> WeightInfoExt for T {}
+75
View File
@@ -0,0 +1,75 @@
[package]
name = "pezpallet-bridge-teyrchains"
version = "0.7.0"
description = "Module that allows bridged relay chains to exchange information on their teyrchains' heads."
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
repository.workspace = true
documentation = "https://docs.rs/pezpallet-bridge-teyrchains"
homepage = { workspace = true }
[lints]
workspace = true
[dependencies]
codec = { workspace = true }
scale-info = { features = ["derive"], workspace = true }
tracing = { workspace = true }
# Bridge Dependencies
bp-header-pez-chain = { workspace = true }
bp-pezkuwi-core = { workspace = true }
pezbp-runtime = { workspace = true }
bp-teyrchains = { workspace = true }
pezpallet-bridge-grandpa = { workspace = true }
# Bizinikiwi Dependencies
pezframe-benchmarking = { optional = true, workspace = true }
pezframe-support = { workspace = true }
pezframe-system = { workspace = true }
pezsp-runtime = { workspace = true }
pezsp-std = { workspace = true }
[dev-dependencies]
bp-header-pez-chain = { workspace = true, default-features = true }
bp-test-utils = { workspace = true, default-features = true }
pezsp-core = { workspace = true, default-features = true }
pezsp-io = { workspace = true, default-features = true }
[features]
default = ["std"]
std = [
"bp-header-pez-chain/std",
"bp-pezkuwi-core/std",
"pezbp-runtime/std",
"bp-teyrchains/std",
"codec/std",
"pezframe-benchmarking/std",
"pezframe-support/std",
"pezframe-system/std",
"pezpallet-bridge-grandpa/std",
"scale-info/std",
"pezsp-runtime/std",
"pezsp-std/std",
"tracing/std",
]
runtime-benchmarks = [
"bp-header-pez-chain/runtime-benchmarks",
"bp-pezkuwi-core/runtime-benchmarks",
"pezbp-runtime/runtime-benchmarks",
"bp-test-utils/runtime-benchmarks",
"bp-teyrchains/runtime-benchmarks",
"pezframe-benchmarking/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezpallet-bridge-grandpa/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
try-runtime = [
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezpallet-bridge-grandpa/try-runtime",
"pezsp-runtime/try-runtime",
]
+90
View File
@@ -0,0 +1,90 @@
# Bridge Teyrchains Pezpallet
The bridge teyrchains pezpallet is a light client for one or several teyrchains of the bridged relay chain.
It serves as a source of finalized teyrchain headers and is used when you need to build a bridge with
a teyrchain.
The pezpallet requires [bridge GRANDPA pezpallet](../grandpa/) to be deployed at the same chain - it is used
to verify storage proofs, generated at the bridged relay chain.
## A Brief Introduction into Teyrchains Finality
You can find detailed information on teyrchains finality in the
[PezkuwiChain-SDK](https://github.com/pezkuwichain/pezkuwi-sdk) repository. This section gives a brief overview of how the
teyrchain finality works and how to build a light client for a teyrchain.
The main thing there is that the teyrchain generates blocks on its own, but it can't achieve finality without
help of its relay chain. Instead, the teyrchain collators create a block and hand it over to the relay chain
validators. Validators validate the block and register the new teyrchain head in the
[`Heads` map](https://github.com/pezkuwichain/pezkuwi-sdk/blob/bc5005217a8c2e7c95b9011c96d7e619879b1200/polkadot/runtime/parachains/src/paras/mod.rs#L683-L686)
of the [`paras`](https://github.com/paritytech/polkadot-sdk/tree/master/polkadot/runtime/parachains/src/paras) pezpallet,
deployed at the relay chain. Keep in mind that this pezpallet, deployed at a relay chain, is **NOT** a bridge pezpallet,
even though the names are similar.
And what the bridge teyrchains pezpallet does, is simply verifying storage proofs of teyrchain heads within that
`Heads` map. It does that using relay chain header, that has been previously imported by the
[bridge GRANDPA pezpallet](../grandpa/). Once the proof is verified, the pezpallet knows that the given teyrchain
header has been finalized by the relay chain. The teyrchain header fields may then be used to verify storage
proofs, coming from the teyrchain. This allows the pezpallet to be used e.g. as a source of finality for the messages
pezpallet.
## Pezpallet Operations
The main entrypoint of the pezpallet is the `submit_teyrchain_heads` call. It has three arguments:
- storage proof of teyrchain heads from the `Heads` map;
- teyrchain identifiers and hashes of their heads from the storage proof;
- the relay block, at which the storage proof has been generated.
The pezpallet may track multiple teyrchains. And the teyrchains may use different primitives - one may use 128-bit block
numbers, other - 32-bit. To avoid extra decode operations, the pezpallet is using relay chain block number to order
teyrchain headers. Any finalized descendant of finalized relay block `RB`, which has teyrchain block `PB` in
its `Heads` map, is guaranteed to have either `PB`, or its descendant. So teyrchain block number grows with relay
block number.
The pezpallet may reject teyrchain head if it already knows better (or the same) head. In addition, pezpallet rejects
heads of untracked teyrchains.
The pezpallet doesn't track anything behind teyrchain heads. So it requires no initialization - it is ready to accept
headers right after deployment.
## Non-Essential Functionality
There may be a special account in every runtime where the bridge teyrchains module is deployed. This
account, named 'module owner', is like a module-level sudo account - he's able to halt and
resume all module operations without requiring runtime upgrade. Calls that are related to this
account are:
- `fn set_owner()`: current module owner may call it to transfer "ownership" to another account;
- `fn set_operating_mode()`: the module owner (or sudo account) may call this function to stop all
module operations. After this call, all finality proofs will be rejected until further `set_operating_mode` call'.
This call may be used when something extraordinary happens with the bridge.
If pezpallet owner is not defined, the governance may be used to make those calls.
## Signed Extension to Reject Obsolete Headers
It'd be better for anyone (for chain and for submitters) to reject all transactions that are submitting
already known teyrchain heads to the pezpallet. This way, we leave block space to other useful transactions and
we don't charge concurrent submitters for their honest actions.
To deal with that, we have a [signed extension](./src/call_ext.rs) that may be added to the runtime.
It does exactly what is required - rejects all transactions with already known heads. The submitter
pays nothing for such transactions - they're simply removed from the transaction pool, when the block
is built.
The signed extension, however, is a bit limited - it only works with transactions that provide single
teyrchain head. So it won't work with multiple teyrchain heads transactions. This fits our needs
for [Kusama <> PezkuwiChain bridge](../../docs/pezkuwi-kusama-bridge-overview.md). If you need to deal
with other transaction formats, you may implement similar extension for your runtime.
You may also take a look at the [`generate_bridge_reject_obsolete_headers_and_messages`](../../bin/runtime-common/src/lib.rs)
macro that bundles several similar signed extensions in a single one.
## Teyrchains Finality Relay
We have an offchain actor, who is watching for new teyrchain heads and submits them to the bridged chain.
It is the teyrchains relay - you may look at the [crate level documentation and the code](../../relays/teyrchains/).
@@ -0,0 +1,116 @@
// 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/>.
//! Teyrchains finality pezpallet benchmarking.
use crate::{
weights_ext::DEFAULT_TEYRCHAIN_HEAD_SIZE, Call, RelayBlockHash, RelayBlockHasher,
RelayBlockNumber,
};
use bp_pezkuwi_core::teyrchains::{ParaHash, ParaHeadsProof, ParaId};
use pezbp_runtime::UnverifiedStorageProofParams;
use pezframe_benchmarking::{account, benchmarks_instance_pallet};
use pezframe_system::RawOrigin;
use pezsp_std::prelude::*;
/// Pezpallet we're benchmarking here.
pub struct Pezpallet<T: Config<I>, I: 'static = ()>(crate::Pezpallet<T, I>);
/// Trait that must be implemented by runtime to benchmark the teyrchains finality pezpallet.
pub trait Config<I: 'static>: crate::Config<I> {
/// Returns vector of supported teyrchains.
fn teyrchains() -> Vec<ParaId>;
/// Generate teyrchain heads proof and prepare environment for verifying this proof.
fn prepare_teyrchain_heads_proof(
teyrchains: &[ParaId],
teyrchain_head_size: u32,
proof_params: UnverifiedStorageProofParams,
) -> (RelayBlockNumber, RelayBlockHash, ParaHeadsProof, Vec<(ParaId, ParaHash)>);
}
benchmarks_instance_pallet! {
where_clause {
where
<T as pezpallet_bridge_grandpa::Config<T::BridgesGrandpaPalletInstance>>::BridgedChain:
pezbp_runtime::Chain<
BlockNumber = RelayBlockNumber,
Hash = RelayBlockHash,
Hasher = RelayBlockHasher,
>,
}
// Benchmark `submit_teyrchain_heads` extrinsic with different number of teyrchains.
submit_teyrchain_heads_with_n_teyrchains {
let p in 1..(T::teyrchains().len() + 1) as u32;
let sender = account("sender", 0, 0);
let mut teyrchains = T::teyrchains();
let _ = if p <= teyrchains.len() as u32 {
teyrchains.split_off(p as usize)
} else {
Default::default()
};
tracing::trace!(target: crate::LOG_TARGET, "=== {:?}", teyrchains.len());
let (relay_block_number, relay_block_hash, teyrchain_heads_proof, teyrchains_heads) = T::prepare_teyrchain_heads_proof(
&teyrchains,
DEFAULT_TEYRCHAIN_HEAD_SIZE,
UnverifiedStorageProofParams::default(),
);
let at_relay_block = (relay_block_number, relay_block_hash);
}: submit_teyrchain_heads(RawOrigin::Signed(sender), at_relay_block, teyrchains_heads, teyrchain_heads_proof)
verify {
for teyrchain in teyrchains {
assert!(crate::Pezpallet::<T, I>::best_teyrchain_head(teyrchain).is_some());
}
}
// Benchmark `submit_teyrchain_heads` extrinsic with 1kb proof size.
submit_teyrchain_heads_with_1kb_proof {
let sender = account("sender", 0, 0);
let teyrchains = vec![T::teyrchains()[0]];
let (relay_block_number, relay_block_hash, teyrchain_heads_proof, teyrchains_heads) = T::prepare_teyrchain_heads_proof(
&teyrchains,
DEFAULT_TEYRCHAIN_HEAD_SIZE,
UnverifiedStorageProofParams::from_db_size(1024),
);
let at_relay_block = (relay_block_number, relay_block_hash);
}: submit_teyrchain_heads(RawOrigin::Signed(sender), at_relay_block, teyrchains_heads, teyrchain_heads_proof)
verify {
for teyrchain in teyrchains {
assert!(crate::Pezpallet::<T, I>::best_teyrchain_head(teyrchain).is_some());
}
}
// Benchmark `submit_teyrchain_heads` extrinsic with 16kb proof size.
submit_teyrchain_heads_with_16kb_proof {
let sender = account("sender", 0, 0);
let teyrchains = vec![T::teyrchains()[0]];
let (relay_block_number, relay_block_hash, teyrchain_heads_proof, teyrchains_heads) = T::prepare_teyrchain_heads_proof(
&teyrchains,
DEFAULT_TEYRCHAIN_HEAD_SIZE,
UnverifiedStorageProofParams::from_db_size(16 * 1024),
);
let at_relay_block = (relay_block_number, relay_block_hash);
}: submit_teyrchain_heads(RawOrigin::Signed(sender), at_relay_block, teyrchains_heads, teyrchain_heads_proof)
verify {
for teyrchain in teyrchains {
assert!(crate::Pezpallet::<T, I>::best_teyrchain_head(teyrchain).is_some());
}
}
impl_benchmark_test_suite!(Pezpallet, crate::mock::new_test_ext(), crate::mock::TestRuntime)
}
@@ -0,0 +1,439 @@
// 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/>.
use crate::{Config, GrandpaPalletOf, Pezpallet, RelayBlockNumber};
use bp_header_pez_chain::HeaderChain;
use pezbp_runtime::{HeaderId, OwnedBridgeModule};
use bp_teyrchains::{BestParaHeadHash, SubmitTeyrchainHeadsInfo};
use pezframe_support::{
dispatch::CallableCallFor,
traits::{Get, IsSubType},
};
use pezpallet_bridge_grandpa::SubmitFinalityProofHelper;
use pezsp_runtime::{
traits::Zero,
transaction_validity::{InvalidTransaction, TransactionValidityError},
RuntimeDebug,
};
/// Verified `SubmitTeyrchainHeadsInfo`.
#[derive(PartialEq, RuntimeDebug)]
pub struct VerifiedSubmitTeyrchainHeadsInfo {
/// Base call information.
pub base: SubmitTeyrchainHeadsInfo,
/// A difference between bundled bridged relay chain header and relay chain header number
/// used to prove best bridged teyrchain header, known to us before the call.
pub improved_by: RelayBlockNumber,
}
/// Helper struct that provides methods for working with the `SubmitTeyrchainHeads` call.
pub struct SubmitTeyrchainHeadsHelper<T: Config<I>, I: 'static> {
_phantom_data: pezsp_std::marker::PhantomData<(T, I)>,
}
impl<T: Config<I>, I: 'static> SubmitTeyrchainHeadsHelper<T, I> {
/// Check that is called from signed extension and takes the `is_free_execution_expected`
/// into account.
pub fn check_obsolete_from_extension(
update: &SubmitTeyrchainHeadsInfo,
) -> Result<RelayBlockNumber, TransactionValidityError> {
// first do all base checks
let improved_by = Self::check_obsolete(update)?;
// if we don't expect free execution - no more checks
if !update.is_free_execution_expected {
return Ok(improved_by);
}
// reject if no more free slots remaining in the block
if !SubmitFinalityProofHelper::<T, T::BridgesGrandpaPalletInstance>::has_free_header_slots()
{
tracing::trace!(
target: crate::LOG_TARGET,
para_id=?update.para_id,
"The free teyrchain head can't be updated: no more free slots left in the block."
);
return Err(InvalidTransaction::Call.into());
}
// if free headers interval is not configured and call is expected to execute
// for free => it is a relayer error, it should've been able to detect that.
let free_headers_interval = match T::FreeHeadersInterval::get() {
Some(free_headers_interval) => free_headers_interval,
None => return Ok(improved_by),
};
// reject if we are importing teyrchain headers too often
if improved_by < free_headers_interval {
tracing::trace!(
target: crate::LOG_TARGET,
para_id=?update.para_id,
%improved_by,
"The free teyrchain head can't be updated: it improves previous
best head while at least {free_headers_interval} is expected."
);
return Err(InvalidTransaction::Stale.into());
}
Ok(improved_by)
}
/// Check if the para head provided by the `SubmitTeyrchainHeads` is better than the best one
/// we know.
pub fn check_obsolete(
update: &SubmitTeyrchainHeadsInfo,
) -> Result<RelayBlockNumber, TransactionValidityError> {
// check if we know better teyrchain head already
let improved_by = match crate::ParasInfo::<T, I>::get(update.para_id) {
Some(stored_best_head) => {
let improved_by = match update
.at_relay_block
.0
.checked_sub(stored_best_head.best_head_hash.at_relay_block_number)
{
Some(improved_by) if improved_by > Zero::zero() => improved_by,
_ => {
tracing::trace!(
target: crate::LOG_TARGET,
para_id=?update.para_id,
"The teyrchain head can't be updated. The teyrchain head \
was already updated at better relay chain block {} >= {}.",
stored_best_head.best_head_hash.at_relay_block_number,
update.at_relay_block.0
);
return Err(InvalidTransaction::Stale.into());
},
};
if stored_best_head.best_head_hash.head_hash == update.para_head_hash {
tracing::trace!(
target: crate::LOG_TARGET,
para_id=?update.para_id,
para_head_hash=%update.para_head_hash,
"The teyrchain head can't be updated. The teyrchain head hash \
was already updated at block {} < {}.",
stored_best_head.best_head_hash.at_relay_block_number,
update.at_relay_block.0
);
return Err(InvalidTransaction::Stale.into());
}
improved_by
},
None => RelayBlockNumber::MAX,
};
// let's check if our chain had no reorgs and we still know the relay chain header
// used to craft the proof
if GrandpaPalletOf::<T, I>::finalized_header_state_root(update.at_relay_block.1).is_none() {
tracing::trace!(
target: crate::LOG_TARGET,
para_id=?update.para_id,
at_relay_block=?update.at_relay_block,
"The teyrchain head can't be updated. Relay chain header used to create \
teyrchain proof is missing from the storage."
);
return Err(InvalidTransaction::Call.into());
}
Ok(improved_by)
}
/// Check if the `SubmitTeyrchainHeads` was successfully executed.
pub fn was_successful(update: &SubmitTeyrchainHeadsInfo) -> bool {
match crate::ParasInfo::<T, I>::get(update.para_id) {
Some(stored_best_head) =>
stored_best_head.best_head_hash ==
BestParaHeadHash {
at_relay_block_number: update.at_relay_block.0,
head_hash: update.para_head_hash,
},
None => false,
}
}
}
/// Trait representing a call that is a sub type of this pezpallet's call.
pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
IsSubType<CallableCallFor<Pezpallet<T, I>, T>>
{
/// Create a new instance of `SubmitTeyrchainHeadsInfo` from a `SubmitTeyrchainHeads` call with
/// one single teyrchain entry.
fn one_entry_submit_teyrchain_heads_info(&self) -> Option<SubmitTeyrchainHeadsInfo> {
match self.is_sub_type() {
Some(crate::Call::<T, I>::submit_teyrchain_heads {
ref at_relay_block,
ref teyrchains,
..
}) => match &teyrchains[..] {
&[(para_id, para_head_hash)] => Some(SubmitTeyrchainHeadsInfo {
at_relay_block: HeaderId(at_relay_block.0, at_relay_block.1),
para_id,
para_head_hash,
is_free_execution_expected: false,
}),
_ => None,
},
Some(crate::Call::<T, I>::submit_teyrchain_heads_ex {
ref at_relay_block,
ref teyrchains,
is_free_execution_expected,
..
}) => match &teyrchains[..] {
&[(para_id, para_head_hash)] => Some(SubmitTeyrchainHeadsInfo {
at_relay_block: HeaderId(at_relay_block.0, at_relay_block.1),
para_id,
para_head_hash,
is_free_execution_expected: *is_free_execution_expected,
}),
_ => None,
},
_ => None,
}
}
/// Create a new instance of `SubmitTeyrchainHeadsInfo` from a `SubmitTeyrchainHeads` call with
/// one single teyrchain entry, if the entry is for the provided teyrchain id.
fn submit_teyrchain_heads_info_for(&self, para_id: u32) -> Option<SubmitTeyrchainHeadsInfo> {
self.one_entry_submit_teyrchain_heads_info()
.filter(|update| update.para_id.0 == para_id)
}
/// Validate teyrchain heads in order to avoid "mining" transactions that provide
/// outdated bridged teyrchain heads. Without this validation, even honest relayers
/// may lose their funds if there are multiple relays running and submitting the
/// same information.
///
/// This validation only works with transactions that are updating single teyrchain
/// head. We can't use unbounded validation - it may take too long and either break
/// block production, or "eat" significant portion of block production time literally
/// for nothing. In addition, the single-teyrchain-head-per-transaction is how the
/// pezpallet will be used in our environment.
fn check_obsolete_submit_teyrchain_heads(
&self,
) -> Result<Option<VerifiedSubmitTeyrchainHeadsInfo>, TransactionValidityError>
where
Self: Sized,
{
let update = match self.one_entry_submit_teyrchain_heads_info() {
Some(update) => update,
None => return Ok(None),
};
if Pezpallet::<T, I>::ensure_not_halted().is_err() {
return Err(InvalidTransaction::Call.into());
}
SubmitTeyrchainHeadsHelper::<T, I>::check_obsolete_from_extension(&update)
.map(|improved_by| Some(VerifiedSubmitTeyrchainHeadsInfo { base: update, improved_by }))
}
}
impl<T, I: 'static> CallSubType<T, I> for T::RuntimeCall
where
T: Config<I>,
T::RuntimeCall: IsSubType<CallableCallFor<Pezpallet<T, I>, T>>,
{
}
#[cfg(test)]
mod tests {
use crate::{
mock::{run_test, FreeHeadersInterval, RuntimeCall, TestRuntime},
CallSubType, PalletOperatingMode, ParaInfo, ParasInfo, RelayBlockHash, RelayBlockNumber,
};
use bp_header_pez_chain::StoredHeaderData;
use bp_pezkuwi_core::teyrchains::{ParaHash, ParaHeadsProof, ParaId};
use pezbp_runtime::BasicOperatingMode;
use bp_teyrchains::BestParaHeadHash;
fn validate_submit_teyrchain_heads(
num: RelayBlockNumber,
teyrchains: Vec<(ParaId, ParaHash)>,
) -> bool {
RuntimeCall::Teyrchains(crate::Call::<TestRuntime, ()>::submit_teyrchain_heads_ex {
at_relay_block: (num, [num as u8; 32].into()),
teyrchains,
teyrchain_heads_proof: ParaHeadsProof { storage_proof: Default::default() },
is_free_execution_expected: false,
})
.check_obsolete_submit_teyrchain_heads()
.is_ok()
}
fn validate_free_submit_teyrchain_heads(
num: RelayBlockNumber,
teyrchains: Vec<(ParaId, ParaHash)>,
) -> bool {
RuntimeCall::Teyrchains(crate::Call::<TestRuntime, ()>::submit_teyrchain_heads_ex {
at_relay_block: (num, [num as u8; 32].into()),
teyrchains,
teyrchain_heads_proof: ParaHeadsProof { storage_proof: Default::default() },
is_free_execution_expected: true,
})
.check_obsolete_submit_teyrchain_heads()
.is_ok()
}
fn insert_relay_block(num: RelayBlockNumber) {
pezpallet_bridge_grandpa::ImportedHeaders::<TestRuntime, crate::Instance1>::insert(
RelayBlockHash::from([num as u8; 32]),
StoredHeaderData { number: num, state_root: RelayBlockHash::from([10u8; 32]) },
);
}
fn sync_to_relay_header_10() {
ParasInfo::<TestRuntime, ()>::insert(
ParaId(1),
ParaInfo {
best_head_hash: BestParaHeadHash {
at_relay_block_number: 10,
head_hash: [1u8; 32].into(),
},
next_imported_hash_position: 0,
},
);
}
#[test]
fn extension_rejects_header_from_the_obsolete_relay_block() {
run_test(|| {
// when current best finalized is #10 and we're trying to import header#5 => tx is
// rejected
sync_to_relay_header_10();
assert!(!validate_submit_teyrchain_heads(5, vec![(ParaId(1), [1u8; 32].into())]));
});
}
#[test]
fn extension_rejects_header_from_the_same_relay_block() {
run_test(|| {
// when current best finalized is #10 and we're trying to import header#10 => tx is
// rejected
sync_to_relay_header_10();
assert!(!validate_submit_teyrchain_heads(10, vec![(ParaId(1), [1u8; 32].into())]));
});
}
#[test]
fn extension_rejects_header_from_new_relay_block_with_same_hash() {
run_test(|| {
// when current best finalized is #10 and we're trying to import header#10 => tx is
// rejected
sync_to_relay_header_10();
assert!(!validate_submit_teyrchain_heads(20, vec![(ParaId(1), [1u8; 32].into())]));
});
}
#[test]
fn extension_rejects_header_if_pallet_is_halted() {
run_test(|| {
// when pezpallet is halted => tx is rejected
sync_to_relay_header_10();
PalletOperatingMode::<TestRuntime, ()>::put(BasicOperatingMode::Halted);
assert!(!validate_submit_teyrchain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
});
}
#[test]
fn extension_accepts_new_header() {
run_test(|| {
// when current best finalized is #10 and we're trying to import header#15 => tx is
// accepted
sync_to_relay_header_10();
insert_relay_block(15);
assert!(validate_submit_teyrchain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
});
}
#[test]
fn extension_accepts_if_more_than_one_teyrchain_is_submitted() {
run_test(|| {
// when current best finalized is #10 and we're trying to import header#5, but another
// teyrchain head is also supplied => tx is accepted
sync_to_relay_header_10();
assert!(validate_submit_teyrchain_heads(
5,
vec![(ParaId(1), [1u8; 32].into()), (ParaId(2), [1u8; 32].into())]
));
});
}
#[test]
fn extension_rejects_initial_teyrchain_head_if_missing_relay_chain_header() {
run_test(|| {
// when relay chain header is unknown => "obsolete"
assert!(!validate_submit_teyrchain_heads(10, vec![(ParaId(1), [1u8; 32].into())]));
// when relay chain header is unknown => "ok"
insert_relay_block(10);
assert!(validate_submit_teyrchain_heads(10, vec![(ParaId(1), [1u8; 32].into())]));
});
}
#[test]
fn extension_rejects_free_teyrchain_head_if_missing_relay_chain_header() {
run_test(|| {
sync_to_relay_header_10();
// when relay chain header is unknown => "obsolete"
assert!(!validate_submit_teyrchain_heads(15, vec![(ParaId(2), [15u8; 32].into())]));
// when relay chain header is unknown => "ok"
insert_relay_block(15);
assert!(validate_submit_teyrchain_heads(15, vec![(ParaId(2), [15u8; 32].into())]));
});
}
#[test]
fn extension_rejects_free_teyrchain_head_if_no_free_slots_remaining() {
run_test(|| {
// when current best finalized is #10 and we're trying to import header#15 => tx should
// be accepted
sync_to_relay_header_10();
insert_relay_block(15);
// ... but since we have specified `is_free_execution_expected = true`, it'll be
// rejected
assert!(!validate_free_submit_teyrchain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
// ... if we have specify `is_free_execution_expected = false`, it'll be accepted
assert!(validate_submit_teyrchain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
});
}
#[test]
fn extension_rejects_free_teyrchain_head_if_improves_by_is_below_expected() {
run_test(|| {
// when current best finalized is #10 and we're trying to import header#15 => tx should
// be accepted
sync_to_relay_header_10();
insert_relay_block(10 + FreeHeadersInterval::get() - 1);
insert_relay_block(10 + FreeHeadersInterval::get());
// try to submit at 10 + FreeHeadersInterval::get() - 1 => failure
let relay_header = 10 + FreeHeadersInterval::get() - 1;
assert!(!validate_free_submit_teyrchain_heads(
relay_header,
vec![(ParaId(1), [2u8; 32].into())]
));
// try to submit at 10 + FreeHeadersInterval::get() => ok
let relay_header = 10 + FreeHeadersInterval::get();
assert!(validate_free_submit_teyrchain_heads(
relay_header,
vec![(ParaId(1), [2u8; 32].into())]
));
});
}
}
File diff suppressed because it is too large Load Diff
+313
View File
@@ -0,0 +1,313 @@
// 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/>.
use bp_header_pez_chain::ChainWithGrandpa;
use bp_pezkuwi_core::teyrchains::ParaId;
use pezbp_runtime::{Chain, ChainId, Teyrchain};
use pezframe_support::{
construct_runtime, derive_impl, parameter_types, traits::ConstU32, weights::Weight,
};
use pezsp_runtime::{
testing::H256,
traits::{BlakeTwo256, Header as HeaderT},
MultiSignature, StateVersion,
};
use crate as pezpallet_bridge_teyrchains;
pub type AccountId = u64;
pub type RelayBlockHeader =
pezsp_runtime::generic::Header<crate::RelayBlockNumber, crate::RelayBlockHasher>;
type Block = pezframe_system::mocking::MockBlock<TestRuntime>;
pub const PARAS_PALLET_NAME: &str = "Paras";
pub const UNTRACKED_TEYRCHAIN_ID: u32 = 10;
// use exact expected encoded size: `vec_len_size + header_number_size + state_root_hash_size`
pub const MAXIMAL_TEYRCHAIN_HEAD_DATA_SIZE: u32 = 1 + 8 + 32;
// total teyrchains that we use in tests
pub const TOTAL_TEYRCHAINS: u32 = 4;
pub type RegularTeyrchainHeader = pezsp_runtime::testing::Header;
pub type RegularTeyrchainHasher = BlakeTwo256;
pub type BigTeyrchainHeader = pezsp_runtime::generic::Header<u128, BlakeTwo256>;
pub struct Teyrchain1;
impl Chain for Teyrchain1 {
const ID: ChainId = *b"pch1";
type BlockNumber = u64;
type Hash = H256;
type Hasher = RegularTeyrchainHasher;
type Header = RegularTeyrchainHeader;
type AccountId = u64;
type Balance = u64;
type Nonce = u64;
type Signature = MultiSignature;
const STATE_VERSION: StateVersion = StateVersion::V1;
fn max_extrinsic_size() -> u32 {
0
}
fn max_extrinsic_weight() -> Weight {
Weight::zero()
}
}
impl Teyrchain for Teyrchain1 {
const TEYRCHAIN_ID: u32 = 1;
const MAX_HEADER_SIZE: u32 = 1_024;
}
pub struct Teyrchain2;
impl Chain for Teyrchain2 {
const ID: ChainId = *b"pch2";
type BlockNumber = u64;
type Hash = H256;
type Hasher = RegularTeyrchainHasher;
type Header = RegularTeyrchainHeader;
type AccountId = u64;
type Balance = u64;
type Nonce = u64;
type Signature = MultiSignature;
const STATE_VERSION: StateVersion = StateVersion::V1;
fn max_extrinsic_size() -> u32 {
0
}
fn max_extrinsic_weight() -> Weight {
Weight::zero()
}
}
impl Teyrchain for Teyrchain2 {
const TEYRCHAIN_ID: u32 = 2;
const MAX_HEADER_SIZE: u32 = 1_024;
}
pub struct Teyrchain3;
impl Chain for Teyrchain3 {
const ID: ChainId = *b"pch3";
type BlockNumber = u64;
type Hash = H256;
type Hasher = RegularTeyrchainHasher;
type Header = RegularTeyrchainHeader;
type AccountId = u64;
type Balance = u64;
type Nonce = u64;
type Signature = MultiSignature;
const STATE_VERSION: StateVersion = StateVersion::V1;
fn max_extrinsic_size() -> u32 {
0
}
fn max_extrinsic_weight() -> Weight {
Weight::zero()
}
}
impl Teyrchain for Teyrchain3 {
const TEYRCHAIN_ID: u32 = 3;
const MAX_HEADER_SIZE: u32 = 1_024;
}
// this teyrchain is using u128 as block number and stored head data size exceeds limit
pub struct BigTeyrchain;
impl Chain for BigTeyrchain {
const ID: ChainId = *b"bpch";
type BlockNumber = u128;
type Hash = H256;
type Hasher = RegularTeyrchainHasher;
type Header = BigTeyrchainHeader;
type AccountId = u64;
type Balance = u64;
type Nonce = u64;
type Signature = MultiSignature;
const STATE_VERSION: StateVersion = StateVersion::V1;
fn max_extrinsic_size() -> u32 {
0
}
fn max_extrinsic_weight() -> Weight {
Weight::zero()
}
}
impl Teyrchain for BigTeyrchain {
const TEYRCHAIN_ID: u32 = 4;
const MAX_HEADER_SIZE: u32 = 2_048;
}
construct_runtime! {
pub enum TestRuntime
{
System: pezframe_system::{Pezpallet, Call, Config<T>, Storage, Event<T>},
Grandpa1: pezpallet_bridge_grandpa::<Instance1>::{Pezpallet, Event<T>},
Grandpa2: pezpallet_bridge_grandpa::<Instance2>::{Pezpallet, Event<T>},
Teyrchains: pezpallet_bridge_teyrchains::{Call, Pezpallet, Event<T>},
}
}
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
impl pezframe_system::Config for TestRuntime {
type Block = Block;
}
parameter_types! {
pub const HeadersToKeep: u32 = 5;
pub const FreeHeadersInterval: u32 = 15;
}
impl pezpallet_bridge_grandpa::Config<pezpallet_bridge_grandpa::Instance1> for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type BridgedChain = TestBridgedChain;
type MaxFreeHeadersPerBlock = ConstU32<2>;
type FreeHeadersInterval = FreeHeadersInterval;
type HeadersToKeep = HeadersToKeep;
type WeightInfo = ();
}
impl pezpallet_bridge_grandpa::Config<pezpallet_bridge_grandpa::Instance2> for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type BridgedChain = TestBridgedChain;
type MaxFreeHeadersPerBlock = ConstU32<2>;
type FreeHeadersInterval = FreeHeadersInterval;
type HeadersToKeep = HeadersToKeep;
type WeightInfo = ();
}
parameter_types! {
pub const HeadsToKeep: u32 = 4;
pub const ParasPalletName: &'static str = PARAS_PALLET_NAME;
pub GetTenFirstTeyrchains: Vec<ParaId> = (0..10).map(ParaId).collect();
}
impl pezpallet_bridge_teyrchains::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
type BridgesGrandpaPalletInstance = pezpallet_bridge_grandpa::Instance1;
type ParasPalletName = ParasPalletName;
type ParaStoredHeaderDataBuilder = (Teyrchain1, Teyrchain2, Teyrchain3, BigTeyrchain);
type HeadsToKeep = HeadsToKeep;
type MaxParaHeadDataSize = ConstU32<MAXIMAL_TEYRCHAIN_HEAD_DATA_SIZE>;
type OnNewHead = ();
}
#[cfg(feature = "runtime-benchmarks")]
impl pezpallet_bridge_teyrchains::benchmarking::Config<()> for TestRuntime {
fn teyrchains() -> Vec<ParaId> {
vec![
ParaId(Teyrchain1::TEYRCHAIN_ID),
ParaId(Teyrchain2::TEYRCHAIN_ID),
ParaId(Teyrchain3::TEYRCHAIN_ID),
]
}
fn prepare_teyrchain_heads_proof(
teyrchains: &[ParaId],
_teyrchain_head_size: u32,
_proof_params: pezbp_runtime::UnverifiedStorageProofParams,
) -> (
crate::RelayBlockNumber,
crate::RelayBlockHash,
bp_pezkuwi_core::teyrchains::ParaHeadsProof,
Vec<(ParaId, bp_pezkuwi_core::teyrchains::ParaHash)>,
) {
// in mock run we only care about benchmarks correctness, not the benchmark results
// => ignore size related arguments
let (state_root, proof, teyrchains) =
bp_test_utils::prepare_teyrchain_heads_proof::<RegularTeyrchainHeader>(
teyrchains.iter().map(|p| (p.0, crate::tests::head_data(p.0, 1))).collect(),
);
let relay_genesis_hash = crate::tests::initialize(state_root);
(0, relay_genesis_hash, proof, teyrchains)
}
}
#[derive(Debug)]
pub struct TestBridgedChain;
impl Chain for TestBridgedChain {
const ID: ChainId = *b"tbch";
type BlockNumber = crate::RelayBlockNumber;
type Hash = crate::RelayBlockHash;
type Hasher = crate::RelayBlockHasher;
type Header = RelayBlockHeader;
type AccountId = AccountId;
type Balance = u32;
type Nonce = u32;
type Signature = pezsp_runtime::testing::TestSignature;
const STATE_VERSION: StateVersion = StateVersion::V1;
fn max_extrinsic_size() -> u32 {
unreachable!()
}
fn max_extrinsic_weight() -> Weight {
unreachable!()
}
}
impl ChainWithGrandpa for TestBridgedChain {
const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str = "";
const MAX_AUTHORITIES_COUNT: u32 = 16;
const REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY: u32 = 8;
const MAX_MANDATORY_HEADER_SIZE: u32 = 256;
const AVERAGE_HEADER_SIZE: u32 = 64;
}
/// Return test externalities to use in tests.
pub fn new_test_ext() -> pezsp_io::TestExternalities {
pezsp_io::TestExternalities::new(Default::default())
}
/// Run pezpallet test.
pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
new_test_ext().execute_with(|| {
System::set_block_number(1);
System::reset_events();
test()
})
}
/// Return test relay chain header with given number.
pub fn test_relay_header(
num: crate::RelayBlockNumber,
state_root: crate::RelayBlockHash,
) -> RelayBlockHeader {
RelayBlockHeader::new(
num,
Default::default(),
state_root,
Default::default(),
Default::default(),
)
}
@@ -0,0 +1,81 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Tools for teyrchain head proof verification.
use crate::{Config, GrandpaPalletOf, RelayBlockHash, RelayBlockHasher};
use bp_header_pez_chain::{HeaderChain, HeaderChainError};
use bp_pezkuwi_core::teyrchains::{ParaHead, ParaId};
use pezbp_runtime::{RawStorageProof, StorageProofChecker, StorageProofError};
use bp_teyrchains::teyrchain_head_storage_key_at_source;
use codec::Decode;
use pezframe_support::traits::Get;
/// Abstraction over storage proof manipulation, hiding implementation details of actual storage
/// proofs.
pub trait StorageProofAdapter<T: Config<I>, I: 'static> {
/// Read and decode optional value from the proof.
fn read_and_decode_optional_value<D: Decode>(
&mut self,
key: &impl AsRef<[u8]>,
) -> Result<Option<D>, StorageProofError>;
/// Checks if each key was read.
fn ensure_no_unused_keys(self) -> Result<(), StorageProofError>;
/// Read teyrchain head from storage proof.
fn read_teyrchain_head(
&mut self,
teyrchain: ParaId,
) -> Result<Option<ParaHead>, StorageProofError> {
let teyrchain_head_key =
teyrchain_head_storage_key_at_source(T::ParasPalletName::get(), teyrchain);
self.read_and_decode_optional_value(&teyrchain_head_key)
}
}
/// Actual storage proof adapter for teyrchain proofs.
pub type TeyrchainsStorageProofAdapter<T, I> = RawStorageProofAdapter<T, I>;
/// A `StorageProofAdapter` implementation for raw storage proofs.
pub struct RawStorageProofAdapter<T: Config<I>, I: 'static> {
storage: StorageProofChecker<RelayBlockHasher>,
_dummy: pezsp_std::marker::PhantomData<(T, I)>,
}
impl<T: Config<I>, I: 'static> RawStorageProofAdapter<T, I> {
/// Try to create a new instance of `RawStorageProofAdapter`.
pub fn try_new_with_verified_storage_proof(
relay_block_hash: RelayBlockHash,
storage_proof: RawStorageProof,
) -> Result<Self, HeaderChainError> {
GrandpaPalletOf::<T, I>::verify_storage_proof(relay_block_hash, storage_proof)
.map(|storage| RawStorageProofAdapter::<T, I> { storage, _dummy: Default::default() })
}
}
impl<T: Config<I>, I: 'static> StorageProofAdapter<T, I> for RawStorageProofAdapter<T, I> {
fn read_and_decode_optional_value<D: Decode>(
&mut self,
key: &impl AsRef<[u8]>,
) -> Result<Option<D>, StorageProofError> {
self.storage.read_and_decode_opt_value(key.as_ref())
}
fn ensure_no_unused_keys(self) -> Result<(), StorageProofError> {
self.storage.ensure_no_unused_nodes()
}
}
@@ -0,0 +1,269 @@
// 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/>.
//! Autogenerated weights for pezpallet_bridge_teyrchains
//!
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2023-06-22, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `serban-ROG-Zephyrus`, CPU: `12th Gen Intel(R) Core(TM) i7-12700H`
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
// Executed Command:
// target/release/unknown-bridge-node
// benchmark
// pezpallet
// --chain=dev
// --steps=50
// --repeat=20
// --pezpallet=pezpallet_bridge_teyrchains
// --extrinsic=*
// --execution=wasm
// --wasm-execution=Compiled
// --heap-pages=4096
// --output=./modules/teyrchains/src/weights.rs
// --template=./.maintain/bridge-weight-template.hbs
#![allow(clippy::all)]
#![allow(unused_parens)]
#![allow(unused_imports)]
#![allow(missing_docs)]
use pezframe_support::{
traits::Get,
weights::{constants::RocksDbWeight, Weight},
};
use pezsp_std::marker::PhantomData;
/// Weight functions needed for pezpallet_bridge_teyrchains.
pub trait WeightInfo {
fn submit_teyrchain_heads_with_n_teyrchains(p: u32) -> Weight;
fn submit_teyrchain_heads_with_1kb_proof() -> Weight;
fn submit_teyrchain_heads_with_16kb_proof() -> Weight;
}
/// Weights for `pezpallet_bridge_teyrchains` that are generated using one of the Bridge testnets.
///
/// Those weights are test only and must never be used in production.
pub struct BridgeWeight<T>(PhantomData<T>);
impl<T: pezframe_system::Config> WeightInfo for BridgeWeight<T> {
/// Storage: BridgeUnknownTeyrchains PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownTeyrchains PalletOperatingMode (max_values: Some(1), max_size: Some(1),
/// added: 496, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ParasInfo (r:1 w:1)
///
/// Proof: BridgeUnknownTeyrchains ParasInfo (max_values: Some(1), max_size: Some(60), added:
/// 555, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ImportedParaHashes (r:1 w:1)
///
/// Proof: BridgeUnknownTeyrchains ImportedParaHashes (max_values: Some(1024), max_size:
/// Some(64), added: 1549, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ImportedParaHeads (r:0 w:1)
///
/// Proof: BridgeUnknownTeyrchains ImportedParaHeads (max_values: Some(1024), max_size:
/// Some(196), added: 1681, mode: MaxEncodedLen)
///
/// The range of component `p` is `[1, 2]`.
fn submit_teyrchain_heads_with_n_teyrchains(_p: u32) -> Weight {
// Proof Size summary in bytes:
// Measured: `302`
// Estimated: `3038`
// Minimum execution time: 30_211 nanoseconds.
Weight::from_parts(32_633_893, 3038)
.saturating_add(T::DbWeight::get().reads(4_u64))
.saturating_add(T::DbWeight::get().writes(3_u64))
}
/// Storage: BridgeUnknownTeyrchains PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownTeyrchains PalletOperatingMode (max_values: Some(1), max_size: Some(1),
/// added: 496, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ParasInfo (r:1 w:1)
///
/// Proof: BridgeUnknownTeyrchains ParasInfo (max_values: Some(1), max_size: Some(60), added:
/// 555, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ImportedParaHashes (r:1 w:1)
///
/// Proof: BridgeUnknownTeyrchains ImportedParaHashes (max_values: Some(1024), max_size:
/// Some(64), added: 1549, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ImportedParaHeads (r:0 w:1)
///
/// Proof: BridgeUnknownTeyrchains ImportedParaHeads (max_values: Some(1024), max_size:
/// Some(196), added: 1681, mode: MaxEncodedLen)
fn submit_teyrchain_heads_with_1kb_proof() -> Weight {
// Proof Size summary in bytes:
// Measured: `302`
// Estimated: `3038`
// Minimum execution time: 30_830 nanoseconds.
Weight::from_parts(31_801_000, 3038)
.saturating_add(T::DbWeight::get().reads(4_u64))
.saturating_add(T::DbWeight::get().writes(3_u64))
}
/// Storage: BridgeUnknownTeyrchains PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownTeyrchains PalletOperatingMode (max_values: Some(1), max_size: Some(1),
/// added: 496, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ParasInfo (r:1 w:1)
///
/// Proof: BridgeUnknownTeyrchains ParasInfo (max_values: Some(1), max_size: Some(60), added:
/// 555, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ImportedParaHashes (r:1 w:1)
///
/// Proof: BridgeUnknownTeyrchains ImportedParaHashes (max_values: Some(1024), max_size:
/// Some(64), added: 1549, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ImportedParaHeads (r:0 w:1)
///
/// Proof: BridgeUnknownTeyrchains ImportedParaHeads (max_values: Some(1024), max_size:
/// Some(196), added: 1681, mode: MaxEncodedLen)
fn submit_teyrchain_heads_with_16kb_proof() -> Weight {
// Proof Size summary in bytes:
// Measured: `302`
// Estimated: `3038`
// Minimum execution time: 44_736 nanoseconds.
Weight::from_parts(45_296_000, 3038)
.saturating_add(T::DbWeight::get().reads(4_u64))
.saturating_add(T::DbWeight::get().writes(3_u64))
}
}
// For backwards compatibility and tests
impl WeightInfo for () {
/// Storage: BridgeUnknownTeyrchains PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownTeyrchains PalletOperatingMode (max_values: Some(1), max_size: Some(1),
/// added: 496, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ParasInfo (r:1 w:1)
///
/// Proof: BridgeUnknownTeyrchains ParasInfo (max_values: Some(1), max_size: Some(60), added:
/// 555, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ImportedParaHashes (r:1 w:1)
///
/// Proof: BridgeUnknownTeyrchains ImportedParaHashes (max_values: Some(1024), max_size:
/// Some(64), added: 1549, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ImportedParaHeads (r:0 w:1)
///
/// Proof: BridgeUnknownTeyrchains ImportedParaHeads (max_values: Some(1024), max_size:
/// Some(196), added: 1681, mode: MaxEncodedLen)
///
/// The range of component `p` is `[1, 2]`.
fn submit_teyrchain_heads_with_n_teyrchains(_p: u32) -> Weight {
// Proof Size summary in bytes:
// Measured: `302`
// Estimated: `3038`
// Minimum execution time: 30_211 nanoseconds.
Weight::from_parts(32_633_893, 3038)
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: BridgeUnknownTeyrchains PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownTeyrchains PalletOperatingMode (max_values: Some(1), max_size: Some(1),
/// added: 496, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ParasInfo (r:1 w:1)
///
/// Proof: BridgeUnknownTeyrchains ParasInfo (max_values: Some(1), max_size: Some(60), added:
/// 555, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ImportedParaHashes (r:1 w:1)
///
/// Proof: BridgeUnknownTeyrchains ImportedParaHashes (max_values: Some(1024), max_size:
/// Some(64), added: 1549, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ImportedParaHeads (r:0 w:1)
///
/// Proof: BridgeUnknownTeyrchains ImportedParaHeads (max_values: Some(1024), max_size:
/// Some(196), added: 1681, mode: MaxEncodedLen)
fn submit_teyrchain_heads_with_1kb_proof() -> Weight {
// Proof Size summary in bytes:
// Measured: `302`
// Estimated: `3038`
// Minimum execution time: 30_830 nanoseconds.
Weight::from_parts(31_801_000, 3038)
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: BridgeUnknownTeyrchains PalletOperatingMode (r:1 w:0)
///
/// Proof: BridgeUnknownTeyrchains PalletOperatingMode (max_values: Some(1), max_size: Some(1),
/// added: 496, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownGrandpa ImportedHeaders (r:1 w:0)
///
/// Proof: BridgeUnknownGrandpa ImportedHeaders (max_values: Some(14400), max_size: Some(68),
/// added: 2048, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ParasInfo (r:1 w:1)
///
/// Proof: BridgeUnknownTeyrchains ParasInfo (max_values: Some(1), max_size: Some(60), added:
/// 555, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ImportedParaHashes (r:1 w:1)
///
/// Proof: BridgeUnknownTeyrchains ImportedParaHashes (max_values: Some(1024), max_size:
/// Some(64), added: 1549, mode: MaxEncodedLen)
///
/// Storage: BridgeUnknownTeyrchains ImportedParaHeads (r:0 w:1)
///
/// Proof: BridgeUnknownTeyrchains ImportedParaHeads (max_values: Some(1024), max_size:
/// Some(196), added: 1681, mode: MaxEncodedLen)
fn submit_teyrchain_heads_with_16kb_proof() -> Weight {
// Proof Size summary in bytes:
// Measured: `302`
// Estimated: `3038`
// Minimum execution time: 44_736 nanoseconds.
Weight::from_parts(45_296_000, 3038)
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
}
@@ -0,0 +1,132 @@
// 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/>.
//! Weight-related utilities.
use crate::weights::{BridgeWeight, WeightInfo};
use pezbp_runtime::Size;
use pezframe_support::weights::{RuntimeDbWeight, Weight};
/// Size of the regular teyrchain head.
///
/// It's not that we are expecting all teyrchain heads to share the same size or that we would
/// reject all heads that have larger/lesser size. It is about head size that we use in benchmarks.
/// Relayer would need to pay additional fee for extra bytes.
///
/// 384 is a bit larger (1.3 times) than the size of the randomly chosen Pezkuwi block.
pub const DEFAULT_TEYRCHAIN_HEAD_SIZE: u32 = 384;
/// Number of extra bytes (excluding size of storage value itself) of storage proof, built at
/// some generic chain.
pub const EXTRA_STORAGE_PROOF_SIZE: u32 = 1024;
/// Extended weight info.
pub trait WeightInfoExt: WeightInfo {
// Our configuration assumes that the runtime has special signed extensions used to:
//
// 1) boost priority of `submit_teyrchain_heads` transactions;
//
// 2) slash relayer if he submits an invalid transaction.
//
// We read and update storage values of other pallets (`pezpallet-bridge-relayers` and
// balances/assets pezpallet). So we need to add this weight to the weight of our call.
// Hence two following methods.
/// Extra weight that is added to the `submit_finality_proof` call weight by signed extensions
/// that are declared at runtime level.
fn submit_teyrchain_heads_overhead_from_runtime() -> Weight;
/// Storage proof overhead, that is included in every storage proof.
///
/// The relayer would pay some extra fee for additional proof bytes, since they mean
/// more hashing operations.
fn expected_extra_storage_proof_size() -> u32;
/// Weight of the teyrchain heads delivery extrinsic.
fn submit_teyrchain_heads_weight(
db_weight: RuntimeDbWeight,
proof: &impl Size,
teyrchains_count: u32,
) -> Weight {
// weight of the `submit_teyrchain_heads` with exactly `teyrchains_count` teyrchain
// heads of the default size (`DEFAULT_TEYRCHAIN_HEAD_SIZE`)
let base_weight = Self::submit_teyrchain_heads_with_n_teyrchains(teyrchains_count);
// overhead because of extra storage proof bytes
let expected_proof_size = teyrchains_count
.saturating_mul(DEFAULT_TEYRCHAIN_HEAD_SIZE)
.saturating_add(Self::expected_extra_storage_proof_size());
let actual_proof_size = proof.size();
let proof_size_overhead = Self::storage_proof_size_overhead(
actual_proof_size.saturating_sub(expected_proof_size),
);
// potential pruning weight (refunded if hasn't happened)
let pruning_weight =
Self::teyrchain_head_pruning_weight(db_weight).saturating_mul(teyrchains_count as u64);
base_weight
.saturating_add(proof_size_overhead)
.saturating_add(pruning_weight)
.saturating_add(Self::submit_teyrchain_heads_overhead_from_runtime())
}
/// Returns weight of single teyrchain head storage update.
///
/// This weight only includes db write operations that happens if teyrchain head is actually
/// updated. All extra weights (weight of storage proof validation, additional checks, ...) is
/// not included.
fn teyrchain_head_storage_write_weight(db_weight: RuntimeDbWeight) -> Weight {
// it's just a couple of operations - we need to write the hash (`ImportedParaHashes`) and
// the head itself (`ImportedParaHeads`. Pruning is not included here
db_weight.writes(2)
}
/// Returns weight of single teyrchain head pruning.
fn teyrchain_head_pruning_weight(db_weight: RuntimeDbWeight) -> Weight {
// it's just one write operation, we don't want any benchmarks for that
db_weight.writes(1)
}
/// Returns weight that needs to be accounted when storage proof of given size is received.
fn storage_proof_size_overhead(extra_proof_bytes: u32) -> Weight {
let extra_byte_weight = (Self::submit_teyrchain_heads_with_16kb_proof() -
Self::submit_teyrchain_heads_with_1kb_proof()) /
(15 * 1024);
extra_byte_weight.saturating_mul(extra_proof_bytes as u64)
}
}
impl WeightInfoExt for () {
fn submit_teyrchain_heads_overhead_from_runtime() -> Weight {
Weight::zero()
}
fn expected_extra_storage_proof_size() -> u32 {
EXTRA_STORAGE_PROOF_SIZE
}
}
impl<T: pezframe_system::Config> WeightInfoExt for BridgeWeight<T> {
fn submit_teyrchain_heads_overhead_from_runtime() -> Weight {
Weight::zero()
}
fn expected_extra_storage_proof_size() -> u32 {
EXTRA_STORAGE_PROOF_SIZE
}
}
@@ -0,0 +1,72 @@
[package]
name = "pezpallet-xcm-bridge-hub-router"
description = "Bridge hub interface for sibling/parent chains with dynamic fees support."
version = "0.5.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
repository.workspace = true
documentation = "https://docs.rs/pezpallet-xcm-bridge-hub-router"
homepage = { workspace = true }
[lints]
workspace = true
[dependencies]
codec = { workspace = true }
scale-info = { features = ["bit-vec", "derive", "serde"], workspace = true }
tracing = { workspace = true }
# Bridge dependencies
bp-xcm-bridge-hub-router = { workspace = true }
# Bizinikiwi Dependencies
pezframe-benchmarking = { optional = true, workspace = true }
pezframe-support = { workspace = true }
pezframe-system = { workspace = true }
pezsp-core = { workspace = true }
pezsp-runtime = { workspace = true }
pezsp-std = { workspace = true }
# Pezkuwi Dependencies
pezkuwi-runtime-teyrchains = { workspace = true }
xcm = { workspace = true }
xcm-builder = { workspace = true }
[dev-dependencies]
pezsp-io = { workspace = true, default-features = true }
[features]
default = ["std"]
std = [
"bp-xcm-bridge-hub-router/std",
"codec/std",
"pezframe-benchmarking/std",
"pezframe-support/std",
"pezframe-system/std",
"pezkuwi-runtime-teyrchains/std",
"scale-info/std",
"pezsp-core/std",
"pezsp-runtime/std",
"pezsp-std/std",
"tracing/std",
"xcm-builder/std",
"xcm/std",
]
runtime-benchmarks = [
"bp-xcm-bridge-hub-router/runtime-benchmarks",
"pezframe-benchmarking/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezkuwi-runtime-teyrchains/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"xcm-builder/runtime-benchmarks",
"xcm/runtime-benchmarks",
]
try-runtime = [
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezkuwi-runtime-teyrchains/try-runtime",
"pezsp-runtime/try-runtime",
]
@@ -0,0 +1,80 @@
// 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/>.
//! XCM bridge hub router pezpallet benchmarks.
#![cfg(feature = "runtime-benchmarks")]
use crate::{Bridge, BridgeState, Call};
use pezframe_benchmarking::{benchmarks_instance_pallet, BenchmarkError};
use pezframe_support::traits::{EnsureOrigin, Get, Hooks, UnfilteredDispatchable};
use pezkuwi_runtime_teyrchains::FeeTracker;
use pezsp_runtime::{traits::Zero, Saturating};
use xcm::prelude::*;
/// Pezpallet we're benchmarking here.
pub struct Pezpallet<T: Config<I>, I: 'static = ()>(crate::Pezpallet<T, I>);
/// Trait that must be implemented by runtime to be able to benchmark pezpallet properly.
pub trait Config<I: 'static>: crate::Config<I> {
/// Fill up queue so it becomes congested.
fn make_congested();
/// Returns destination which is valid for this router instance.
/// (Needs to pass `T::Bridges`)
/// Make sure that `SendXcm` will pass.
fn ensure_bridged_target_destination() -> Result<Location, BenchmarkError> {
Ok(Location::new(
Self::UniversalLocation::get().len() as u8,
[GlobalConsensus(Self::BridgedNetworkId::get().unwrap())],
))
}
}
benchmarks_instance_pallet! {
on_initialize_when_non_congested {
Bridge::<T, I>::put(BridgeState {
is_congested: false,
delivery_fee_factor: crate::Pezpallet::<T, I>::MIN_FEE_FACTOR.saturating_mul(2.into()),
});
}: {
crate::Pezpallet::<T, I>::on_initialize(Zero::zero())
}
on_initialize_when_congested {
Bridge::<T, I>::put(BridgeState {
is_congested: false,
delivery_fee_factor: crate::Pezpallet::<T, I>::MIN_FEE_FACTOR.saturating_mul(2.into()),
});
let _ = T::ensure_bridged_target_destination()?;
T::make_congested();
}: {
crate::Pezpallet::<T, I>::on_initialize(Zero::zero())
}
report_bridge_status {
Bridge::<T, I>::put(BridgeState::default());
let origin: T::RuntimeOrigin = T::BridgeHubOrigin::try_successful_origin().expect("expected valid BridgeHubOrigin");
let bridge_id = Default::default();
let is_congested = true;
let call = Call::<T, I>::report_bridge_status { bridge_id, is_congested };
}: { call.dispatch_bypass_filter(origin)? }
verify {
assert!(Bridge::<T, I>::get().is_congested);
}
}
@@ -0,0 +1,747 @@
// 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/>.
//! Pezpallet that may be used instead of `SovereignPaidRemoteExporter` in the XCM router
//! configuration. The main thing that the pezpallet offers is the dynamic message fee,
//! that is computed based on the bridge queues state. It starts exponentially increasing
//! if the queue between this chain and the sibling/child bridge hub is congested.
//!
//! All other bridge hub queues offer some backpressure mechanisms. So if at least one
//! of all queues is congested, it will eventually lead to the growth of the queue at
//! this chain.
//!
//! **A note on terminology**: when we mention the bridge hub here, we mean the chain that
//! has the messages pezpallet deployed (`pezpallet-bridge-grandpa`, `pezpallet-bridge-messages`,
//! `pezpallet-xcm-bridge-hub`, ...). It may be the system bridge hub teyrchain or any other
//! chain.
#![cfg_attr(not(feature = "std"), no_std)]
use bp_xcm_bridge_hub_router::MINIMAL_DELIVERY_FEE_FACTOR;
pub use bp_xcm_bridge_hub_router::{BridgeState, XcmChannelStatusProvider};
use codec::Encode;
use pezframe_support::traits::Get;
use pezkuwi_runtime_teyrchains::FeeTracker;
use pezsp_core::H256;
use pezsp_runtime::{FixedPointNumber, FixedU128};
use pezsp_std::vec::Vec;
use xcm::prelude::*;
use xcm_builder::{ExporterFor, InspectMessageQueues, SovereignPaidRemoteExporter};
pub use pezpallet::*;
pub use weights::WeightInfo;
pub mod benchmarking;
pub mod weights;
mod mock;
/// Maximal size of the XCM message that may be sent over bridge.
///
/// This should be less than the maximal size, allowed by the messages pezpallet, because
/// the message itself is wrapped in other structs and is double encoded.
pub const HARD_MESSAGE_SIZE_LIMIT: u32 = 32 * 1024;
/// The target that will be used when publishing logs related to this pezpallet.
///
/// This doesn't match the pattern used by other bridge pallets (`runtime::bridge-*`). But this
/// pezpallet has significant differences with those pallets. The main one is that is intended to
/// be deployed at sending chains. Other bridge pallets are likely to be deployed at the separate
/// bridge hub teyrchain.
pub const LOG_TARGET: &str = "xcm::bridge-hub-router";
#[pezframe_support::pezpallet]
pub mod pezpallet {
use super::*;
use pezframe_support::pezpallet_prelude::*;
use pezframe_system::pezpallet_prelude::*;
#[pezpallet::config]
pub trait Config<I: 'static = ()>: pezframe_system::Config {
/// The overarching event type.
#[allow(deprecated)]
type RuntimeEvent: From<Event<Self, I>>
+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
/// Benchmarks results from runtime we're plugged into.
type WeightInfo: WeightInfo;
/// Universal location of this runtime.
type UniversalLocation: Get<InteriorLocation>;
/// Relative location of the supported sibling bridge hub.
type SiblingBridgeHubLocation: Get<Location>;
/// The bridged network that this config is for if specified.
/// Also used for filtering `Bridges` by `BridgedNetworkId`.
/// If not specified, allows all networks pass through.
type BridgedNetworkId: Get<Option<NetworkId>>;
/// Configuration for supported **bridged networks/locations** with **bridge location** and
/// **possible fee**. Allows to externalize better control over allowed **bridged
/// networks/locations**.
type Bridges: ExporterFor;
/// Checks the XCM version for the destination.
type DestinationVersion: GetVersion;
/// Origin of the sibling bridge hub that is allowed to report bridge status.
type BridgeHubOrigin: EnsureOrigin<Self::RuntimeOrigin>;
/// Actual message sender (`HRMP` or `DMP`) to the sibling bridge hub location.
type ToBridgeHubSender: SendXcm;
/// Local XCM channel manager.
type LocalXcmChannelManager: XcmChannelStatusProvider;
/// Additional fee that is paid for every byte of the outbound message.
type ByteFee: Get<u128>;
/// Asset that is used to paid bridge fee.
type FeeAsset: Get<AssetId>;
}
#[pezpallet::pezpallet]
pub struct Pezpallet<T, I = ()>(PhantomData<(T, I)>);
#[pezpallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pezpallet<T, I> {
fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
// if XCM channel is still congested, we don't change anything
if T::LocalXcmChannelManager::is_congested(&T::SiblingBridgeHubLocation::get()) {
return T::WeightInfo::on_initialize_when_congested();
}
// if bridge has reported congestion, we don't change anything
let mut bridge = Self::bridge();
if bridge.is_congested {
return T::WeightInfo::on_initialize_when_congested();
}
let previous_factor = Self::get_fee_factor(());
// if we can't decrease the delivery fee factor anymore, we don't change anything
if !Self::do_decrease_fee_factor(&mut bridge.delivery_fee_factor) {
return T::WeightInfo::on_initialize_when_congested();
}
tracing::info!(
target: LOG_TARGET,
from=%previous_factor,
to=%bridge.delivery_fee_factor,
"Bridge channel is uncongested. Decreased fee factor"
);
Self::deposit_event(Event::DeliveryFeeFactorDecreased {
new_value: bridge.delivery_fee_factor,
});
Bridge::<T, I>::put(bridge);
T::WeightInfo::on_initialize_when_non_congested()
}
}
#[pezpallet::call]
impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
/// Notification about congested bridge queue.
#[pezpallet::call_index(0)]
#[pezpallet::weight(T::WeightInfo::report_bridge_status())]
pub fn report_bridge_status(
origin: OriginFor<T>,
// this argument is not currently used, but to ease future migration, we'll keep it
// here
bridge_id: H256,
is_congested: bool,
) -> DispatchResult {
T::BridgeHubOrigin::ensure_origin(origin)?;
tracing::info!(
target: LOG_TARGET,
from=?bridge_id,
congested=%is_congested,
"Received bridge status"
);
Bridge::<T, I>::mutate(|bridge| {
bridge.is_congested = is_congested;
});
Ok(())
}
}
/// Bridge that we are using.
///
/// **bridges-v1** assumptions: all outbound messages through this router are using single lane
/// and to single remote consensus. If there is some other remote consensus that uses the same
/// bridge hub, the separate pezpallet instance shall be used, In `v2` we'll have all required
/// primitives (lane-id aka bridge-id, derived from XCM locations) to support multiple bridges
/// by the same pezpallet instance.
#[pezpallet::storage]
pub type Bridge<T: Config<I>, I: 'static = ()> = StorageValue<_, BridgeState, ValueQuery>;
impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
/// Bridge that we are using.
pub fn bridge() -> BridgeState {
Bridge::<T, I>::get()
}
/// Called when new message is sent (queued to local outbound XCM queue) over the bridge.
pub(crate) fn on_message_sent_to_bridge(message_size: u32) {
tracing::trace!(
target: LOG_TARGET,
?message_size, "on_message_sent_to_bridge"
);
let _ = Bridge::<T, I>::try_mutate(|bridge| {
let is_channel_with_bridge_hub_congested =
T::LocalXcmChannelManager::is_congested(&T::SiblingBridgeHubLocation::get());
let is_bridge_congested = bridge.is_congested;
// if outbound queue is not congested AND bridge has not reported congestion, do
// nothing
if !is_channel_with_bridge_hub_congested && !is_bridge_congested {
return Err(());
}
let previous_factor = Self::get_fee_factor(());
// ok - we need to increase the fee factor, let's do that
<Self as FeeTracker>::do_increase_fee_factor(
&mut bridge.delivery_fee_factor,
message_size as u128,
);
tracing::info!(
target: LOG_TARGET,
from=%previous_factor,
to=%bridge.delivery_fee_factor,
"Bridge channel is congested. Increased fee factor"
);
Self::deposit_event(Event::DeliveryFeeFactorIncreased {
new_value: bridge.delivery_fee_factor,
});
Ok(())
});
}
}
#[pezpallet::event]
#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
/// Delivery fee factor has been decreased.
DeliveryFeeFactorDecreased {
/// New value of the `DeliveryFeeFactor`.
new_value: FixedU128,
},
/// Delivery fee factor has been increased.
DeliveryFeeFactorIncreased {
/// New value of the `DeliveryFeeFactor`.
new_value: FixedU128,
},
}
}
/// We'll be using `SovereignPaidRemoteExporter` to send remote messages over the sibling/child
/// bridge hub.
type ViaBridgeHubExporter<T, I> = SovereignPaidRemoteExporter<
Pezpallet<T, I>,
<T as Config<I>>::ToBridgeHubSender,
<T as Config<I>>::UniversalLocation,
>;
// This pezpallet acts as the `ExporterFor` for the `SovereignPaidRemoteExporter` to compute
// message fee using fee factor.
impl<T: Config<I>, I: 'static> ExporterFor for Pezpallet<T, I> {
fn exporter_for(
network: &NetworkId,
remote_location: &InteriorLocation,
message: &Xcm<()>,
) -> Option<(Location, Option<Asset>)> {
tracing::trace!(
target: LOG_TARGET,
?network, ?remote_location, msg=?message, "exporter_for"
);
// ensure that the message is sent to the expected bridged network (if specified).
if let Some(bridged_network) = T::BridgedNetworkId::get() {
if *network != bridged_network {
tracing::trace!(
target: LOG_TARGET,
bridged_network_id=?bridged_network, ?network, "Router does not support bridging!"
);
return None;
}
}
// ensure that the message is sent to the expected bridged network and location.
let (bridge_hub_location, maybe_payment) =
match T::Bridges::exporter_for(network, remote_location, message) {
Some((bridge_hub_location, maybe_payment))
if bridge_hub_location.eq(&T::SiblingBridgeHubLocation::get()) =>
(bridge_hub_location, maybe_payment),
_ => {
tracing::trace!(
target: LOG_TARGET,
bridged_network_id=?T::BridgedNetworkId::get(),
sibling_bridge_hub_location=?T::SiblingBridgeHubLocation::get(),
?network,
?remote_location,
"Router configured does not support bridging!"
);
return None;
},
};
// take `base_fee` from `T::Brides`, but it has to be the same `T::FeeAsset`
let base_fee = match maybe_payment {
Some(payment) => match payment {
Asset { fun: Fungible(amount), id } if id.eq(&T::FeeAsset::get()) => amount,
invalid_asset => {
tracing::error!(
target: LOG_TARGET,
bridged_network_id=?T::BridgedNetworkId::get(),
fee_asset=?T::FeeAsset::get(),
with=?invalid_asset,
?bridge_hub_location,
?network,
?remote_location,
"Router is configured for `T::FeeAsset` which is not compatible for bridging!"
);
return None;
},
},
None => 0,
};
// compute fee amount. Keep in mind that this is only the bridge fee. The fee for sending
// message from this chain to child/sibling bridge hub is determined by the
// `Config::ToBridgeHubSender`
let message_size = message.encoded_size();
let message_fee = (message_size as u128).saturating_mul(T::ByteFee::get());
let fee_sum = base_fee.saturating_add(message_fee);
let fee_factor = Self::get_fee_factor(());
let fee = fee_factor.saturating_mul_int(fee_sum);
let fee = if fee > 0 { Some((T::FeeAsset::get(), fee).into()) } else { None };
tracing::info!(
target: LOG_TARGET,
to=?(network, remote_location),
bridge_fee=?fee,
%fee_factor,
"Going to send message ({message_size} bytes) over bridge."
);
Some((bridge_hub_location, fee))
}
}
// This pezpallet acts as the `SendXcm` to the sibling/child bridge hub instead of regular
// XCMP/DMP transport. This allows injecting dynamic message fees into XCM programs that
// are going to the bridged network.
impl<T: Config<I>, I: 'static> SendXcm for Pezpallet<T, I> {
type Ticket = (u32, <T::ToBridgeHubSender as SendXcm>::Ticket);
fn validate(
dest: &mut Option<Location>,
xcm: &mut Option<Xcm<()>>,
) -> SendResult<Self::Ticket> {
tracing::trace!(target: LOG_TARGET, msg=?xcm, destination=?dest, "validate");
// In case of success, the `ViaBridgeHubExporter` can modify XCM instructions and consume
// `dest` / `xcm`, so we retain the clone of original message and the destination for later
// `DestinationVersion` validation.
let xcm_to_dest_clone = xcm.clone();
let dest_clone = dest.clone();
// First, use the inner exporter to validate the destination to determine if it is even
// routable. If it is not, return an error. If it is, then the XCM is extended with
// instructions to pay the message fee at the sibling/child bridge hub. The cost will
// include both the cost of (1) delivery to the sibling bridge hub (returned by
// `Config::ToBridgeHubSender`) and (2) delivery to the bridged bridge hub (returned by
// `Self::exporter_for`).
match ViaBridgeHubExporter::<T, I>::validate(dest, xcm) {
Ok((ticket, cost)) => {
// If the ticket is ok, it means we are routing with this router, so we need to
// apply more validations to the cloned `dest` and `xcm`, which are required here.
let xcm_to_dest_clone = xcm_to_dest_clone.ok_or(SendError::MissingArgument)?;
let dest_clone = dest_clone.ok_or(SendError::MissingArgument)?;
// We won't have access to `dest` and `xcm` in the `deliver` method, so we need to
// precompute everything required here. However, `dest` and `xcm` were consumed by
// `ViaBridgeHubExporter`, so we need to use their clones.
let message_size = xcm_to_dest_clone.encoded_size() as _;
// The bridge doesn't support oversized or overweight messages. Therefore, it's
// better to drop such messages here rather than at the bridge hub. Let's check the
// message size."
if message_size > HARD_MESSAGE_SIZE_LIMIT {
return Err(SendError::ExceedsMaxMessageSize);
}
// We need to ensure that the known `dest`'s XCM version can comprehend the current
// `xcm` program. This may seem like an additional, unnecessary check, but it is
// not. A similar check is probably performed by the `ViaBridgeHubExporter`, which
// attempts to send a versioned message to the sibling bridge hub. However, the
// local bridge hub may have a higher XCM version than the remote `dest`. Once
// again, it is better to discard such messages here than at the bridge hub (e.g.,
// to avoid losing funds).
let destination_version = T::DestinationVersion::get_version_for(&dest_clone)
.ok_or(SendError::DestinationUnsupported)?;
VersionedXcm::from(xcm_to_dest_clone)
.into_version(destination_version)
.map_err(|()| SendError::DestinationUnsupported)?;
Ok(((message_size, ticket), cost))
},
Err(e) => {
tracing::trace!(target: LOG_TARGET, error=?e, "validate - ViaBridgeHubExporter");
Err(e)
},
}
}
fn deliver(ticket: Self::Ticket) -> Result<XcmHash, SendError> {
// use router to enqueue message to the sibling/child bridge hub. This also should handle
// payment for passing through this queue.
let (message_size, ticket) = ticket;
let xcm_hash = ViaBridgeHubExporter::<T, I>::deliver(ticket)?;
// increase delivery fee factor if required
Self::on_message_sent_to_bridge(message_size);
tracing::trace!(target: LOG_TARGET, ?xcm_hash, "deliver - message sent");
Ok(xcm_hash)
}
}
impl<T: Config<I>, I: 'static> InspectMessageQueues for Pezpallet<T, I> {
fn clear_messages() {}
/// This router needs to implement `InspectMessageQueues` but doesn't have to
/// return any messages, since it just reuses the `XcmpQueue` router.
fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
Vec::new()
}
}
impl<T: Config<I>, I: 'static> FeeTracker for Pezpallet<T, I> {
type Id = ();
const MIN_FEE_FACTOR: FixedU128 = MINIMAL_DELIVERY_FEE_FACTOR;
fn get_fee_factor(_id: Self::Id) -> FixedU128 {
Self::bridge().delivery_fee_factor
}
fn set_fee_factor(_id: Self::Id, val: FixedU128) {
let mut bridge = Self::bridge();
bridge.delivery_fee_factor = val;
Bridge::<T, I>::put(bridge);
}
}
#[cfg(test)]
mod tests {
use super::*;
use pezframe_support::assert_ok;
use mock::*;
use pezframe_support::traits::Hooks;
use pezframe_system::{EventRecord, Phase};
use pezsp_runtime::traits::One;
fn congested_bridge(delivery_fee_factor: FixedU128) -> BridgeState {
BridgeState { is_congested: true, delivery_fee_factor }
}
fn uncongested_bridge(delivery_fee_factor: FixedU128) -> BridgeState {
BridgeState { is_congested: false, delivery_fee_factor }
}
#[test]
fn initial_fee_factor_is_one() {
run_test(|| {
assert_eq!(
Bridge::<TestRuntime, ()>::get(),
uncongested_bridge(Pezpallet::<TestRuntime, ()>::MIN_FEE_FACTOR),
);
})
}
#[test]
fn fee_factor_is_not_decreased_from_on_initialize_when_xcm_channel_is_congested() {
run_test(|| {
Bridge::<TestRuntime, ()>::put(uncongested_bridge(FixedU128::from_rational(125, 100)));
TestLocalXcmChannelManager::make_congested(&SiblingBridgeHubLocation::get());
// it should not decrease, because queue is congested
let old_delivery = XcmBridgeHubRouter::bridge();
XcmBridgeHubRouter::on_initialize(One::one());
assert_eq!(XcmBridgeHubRouter::bridge(), old_delivery);
assert_eq!(System::events(), vec![]);
})
}
#[test]
fn fee_factor_is_not_decreased_from_on_initialize_when_bridge_has_reported_congestion() {
run_test(|| {
Bridge::<TestRuntime, ()>::put(congested_bridge(FixedU128::from_rational(125, 100)));
// it should not decrease, because bridge congested
let old_bridge = XcmBridgeHubRouter::bridge();
XcmBridgeHubRouter::on_initialize(One::one());
assert_eq!(XcmBridgeHubRouter::bridge(), old_bridge);
assert_eq!(System::events(), vec![]);
})
}
#[test]
fn fee_factor_is_decreased_from_on_initialize_when_xcm_channel_is_uncongested() {
run_test(|| {
let initial_fee_factor = FixedU128::from_rational(125, 100);
Bridge::<TestRuntime, ()>::put(uncongested_bridge(initial_fee_factor));
// it should eventually decrease to one
while XcmBridgeHubRouter::bridge().delivery_fee_factor >
Pezpallet::<TestRuntime, ()>::MIN_FEE_FACTOR
{
XcmBridgeHubRouter::on_initialize(One::one());
}
// verify that it doesn't decrease anymore
XcmBridgeHubRouter::on_initialize(One::one());
assert_eq!(
XcmBridgeHubRouter::bridge(),
uncongested_bridge(Pezpallet::<TestRuntime, ()>::MIN_FEE_FACTOR)
);
// check emitted event
let first_system_event = System::events().first().cloned();
assert_eq!(
first_system_event,
Some(EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::XcmBridgeHubRouter(Event::DeliveryFeeFactorDecreased {
new_value: initial_fee_factor / XcmBridgeHubRouter::EXPONENTIAL_FEE_BASE,
}),
topics: vec![],
})
);
})
}
#[test]
fn not_applicable_if_destination_is_within_other_network() {
run_test(|| {
// unroutable dest
let dest = Location::new(2, [GlobalConsensus(ByGenesis([0; 32])), Teyrchain(1000)]);
let xcm: Xcm<()> = vec![ClearOrigin].into();
// check that router does not consume when `NotApplicable`
let mut xcm_wrapper = Some(xcm.clone());
assert_eq!(
XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper),
Err(SendError::NotApplicable),
);
// XCM is NOT consumed and untouched
assert_eq!(Some(xcm.clone()), xcm_wrapper);
// check the full `send_xcm`
assert_eq!(send_xcm::<XcmBridgeHubRouter>(dest, xcm,), Err(SendError::NotApplicable),);
});
}
#[test]
fn exceeds_max_message_size_if_size_is_above_hard_limit() {
run_test(|| {
// routable dest with XCM version
let dest =
Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Teyrchain(1000)]);
// oversized XCM
let xcm: Xcm<()> = vec![ClearOrigin; HARD_MESSAGE_SIZE_LIMIT as usize].into();
// dest is routable with the inner router
assert_ok!(ViaBridgeHubExporter::<TestRuntime, ()>::validate(
&mut Some(dest.clone()),
&mut Some(xcm.clone())
));
// check for oversized message
let mut xcm_wrapper = Some(xcm.clone());
assert_eq!(
XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper),
Err(SendError::ExceedsMaxMessageSize),
);
// XCM is consumed by the inner router
assert!(xcm_wrapper.is_none());
// check the full `send_xcm`
assert_eq!(
send_xcm::<XcmBridgeHubRouter>(dest, xcm,),
Err(SendError::ExceedsMaxMessageSize),
);
});
}
#[test]
fn destination_unsupported_if_wrap_version_fails() {
run_test(|| {
// routable dest but we don't know XCM version
let dest = UnknownXcmVersionForRoutableLocation::get();
let xcm: Xcm<()> = vec![ClearOrigin].into();
// dest is routable with the inner router
assert_ok!(ViaBridgeHubExporter::<TestRuntime, ()>::validate(
&mut Some(dest.clone()),
&mut Some(xcm.clone())
));
// check that it does not pass XCM version check
let mut xcm_wrapper = Some(xcm.clone());
assert_eq!(
XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper),
Err(SendError::DestinationUnsupported),
);
// XCM is consumed by the inner router
assert!(xcm_wrapper.is_none());
// check the full `send_xcm`
assert_eq!(
send_xcm::<XcmBridgeHubRouter>(dest, xcm,),
Err(SendError::DestinationUnsupported),
);
});
}
#[test]
fn returns_proper_delivery_price() {
run_test(|| {
let dest = Location::new(2, [GlobalConsensus(BridgedNetworkId::get())]);
let xcm: Xcm<()> = vec![ClearOrigin].into();
let msg_size = xcm.encoded_size();
// initially the base fee is used: `BASE_FEE + BYTE_FEE * msg_size + HRMP_FEE`
let expected_fee = BASE_FEE + BYTE_FEE * (msg_size as u128) + HRMP_FEE;
assert_eq!(
XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut Some(xcm.clone()))
.unwrap()
.1
.get(0),
Some(&(BridgeFeeAsset::get(), expected_fee).into()),
);
// but when factor is larger than one, it increases the fee, so it becomes:
// `(BASE_FEE + BYTE_FEE * msg_size) * F + HRMP_FEE`
let factor = FixedU128::from_rational(125, 100);
Bridge::<TestRuntime, ()>::put(uncongested_bridge(factor));
let expected_fee =
(FixedU128::saturating_from_integer(BASE_FEE + BYTE_FEE * (msg_size as u128)) *
factor)
.into_inner() / FixedU128::DIV +
HRMP_FEE;
assert_eq!(
XcmBridgeHubRouter::validate(&mut Some(dest), &mut Some(xcm)).unwrap().1.get(0),
Some(&(BridgeFeeAsset::get(), expected_fee).into()),
);
});
}
#[test]
fn sent_message_doesnt_increase_factor_if_queue_is_uncongested() {
run_test(|| {
let old_bridge = XcmBridgeHubRouter::bridge();
assert_eq!(
send_xcm::<XcmBridgeHubRouter>(
Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Teyrchain(1000)]),
vec![ClearOrigin].into(),
)
.map(drop),
Ok(()),
);
assert!(TestToBridgeHubSender::is_message_sent());
assert_eq!(old_bridge, XcmBridgeHubRouter::bridge());
assert_eq!(System::events(), vec![]);
});
}
#[test]
fn sent_message_increases_factor_if_xcm_channel_is_congested() {
run_test(|| {
TestLocalXcmChannelManager::make_congested(&SiblingBridgeHubLocation::get());
let old_bridge = XcmBridgeHubRouter::bridge();
assert_ok!(send_xcm::<XcmBridgeHubRouter>(
Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Teyrchain(1000)]),
vec![ClearOrigin].into(),
)
.map(drop));
assert!(TestToBridgeHubSender::is_message_sent());
assert!(
old_bridge.delivery_fee_factor < XcmBridgeHubRouter::bridge().delivery_fee_factor
);
// check emitted event
let first_system_event = System::events().first().cloned();
assert!(matches!(
first_system_event,
Some(EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::XcmBridgeHubRouter(
Event::DeliveryFeeFactorIncreased { .. }
),
..
})
));
});
}
#[test]
fn sent_message_increases_factor_if_bridge_has_reported_congestion() {
run_test(|| {
Bridge::<TestRuntime, ()>::put(congested_bridge(
Pezpallet::<TestRuntime, ()>::MIN_FEE_FACTOR,
));
let old_bridge = XcmBridgeHubRouter::bridge();
assert_ok!(send_xcm::<XcmBridgeHubRouter>(
Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Teyrchain(1000)]),
vec![ClearOrigin].into(),
)
.map(drop));
assert!(TestToBridgeHubSender::is_message_sent());
assert!(
old_bridge.delivery_fee_factor < XcmBridgeHubRouter::bridge().delivery_fee_factor
);
// check emitted event
let first_system_event = System::events().first().cloned();
assert!(matches!(
first_system_event,
Some(EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::XcmBridgeHubRouter(
Event::DeliveryFeeFactorIncreased { .. }
),
..
})
));
});
}
#[test]
fn get_messages_does_not_return_anything() {
run_test(|| {
assert_ok!(send_xcm::<XcmBridgeHubRouter>(
(Parent, Parent, GlobalConsensus(BridgedNetworkId::get()), Teyrchain(1000)).into(),
vec![ClearOrigin].into()
));
assert_eq!(XcmBridgeHubRouter::get_messages(), vec![]);
});
}
}
@@ -0,0 +1,191 @@
// 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/>.
#![cfg(test)]
use crate as pezpallet_xcm_bridge_hub_router;
use bp_xcm_bridge_hub_router::XcmChannelStatusProvider;
use codec::Encode;
use pezframe_support::{
construct_runtime, derive_impl, parameter_types,
traits::{Contains, Equals},
};
use pezsp_runtime::{traits::ConstU128, BuildStorage};
use pezsp_std::cell::RefCell;
use xcm::prelude::*;
use xcm_builder::{InspectMessageQueues, NetworkExportTable, NetworkExportTableItem};
type Block = pezframe_system::mocking::MockBlock<TestRuntime>;
/// HRMP fee.
pub const HRMP_FEE: u128 = 500;
/// Base bridge fee.
pub const BASE_FEE: u128 = 1_000_000;
/// Byte bridge fee.
pub const BYTE_FEE: u128 = 1_000;
construct_runtime! {
pub enum TestRuntime
{
System: pezframe_system::{Pezpallet, Call, Config<T>, Storage, Event<T>},
XcmBridgeHubRouter: pezpallet_xcm_bridge_hub_router::{Pezpallet, Storage, Event<T>},
}
}
parameter_types! {
pub ThisNetworkId: NetworkId = Pezkuwi;
pub BridgedNetworkId: NetworkId = Kusama;
pub UniversalLocation: InteriorLocation = [GlobalConsensus(ThisNetworkId::get()), Teyrchain(1000)].into();
pub SiblingBridgeHubLocation: Location = ParentThen([Teyrchain(1002)].into()).into();
pub BridgeFeeAsset: AssetId = Location::parent().into();
pub BridgeTable: Vec<NetworkExportTableItem>
= vec![
NetworkExportTableItem::new(
BridgedNetworkId::get(),
None,
SiblingBridgeHubLocation::get(),
Some((BridgeFeeAsset::get(), BASE_FEE).into())
)
];
pub UnknownXcmVersionForRoutableLocation: Location = Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Teyrchain(9999)]);
}
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
impl pezframe_system::Config for TestRuntime {
type Block = Block;
}
impl pezpallet_xcm_bridge_hub_router::Config<()> for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
type UniversalLocation = UniversalLocation;
type SiblingBridgeHubLocation = SiblingBridgeHubLocation;
type BridgedNetworkId = BridgedNetworkId;
type Bridges = NetworkExportTable<BridgeTable>;
type DestinationVersion =
LatestOrNoneForLocationVersionChecker<Equals<UnknownXcmVersionForRoutableLocation>>;
type BridgeHubOrigin = pezframe_system::EnsureRoot<u64>;
type ToBridgeHubSender = TestToBridgeHubSender;
type LocalXcmChannelManager = TestLocalXcmChannelManager;
type ByteFee = ConstU128<BYTE_FEE>;
type FeeAsset = BridgeFeeAsset;
}
pub struct LatestOrNoneForLocationVersionChecker<Location>(pezsp_std::marker::PhantomData<Location>);
impl<LocationValue: Contains<Location>> GetVersion
for LatestOrNoneForLocationVersionChecker<LocationValue>
{
fn get_version_for(dest: &Location) -> Option<XcmVersion> {
if LocationValue::contains(dest) {
return None;
}
Some(XCM_VERSION)
}
}
pub struct TestToBridgeHubSender;
impl TestToBridgeHubSender {
pub fn is_message_sent() -> bool {
!Self::get_messages().is_empty()
}
}
thread_local! {
pub static SENT_XCM: RefCell<Vec<(Location, Xcm<()>)>> = RefCell::new(Vec::new());
}
impl SendXcm for TestToBridgeHubSender {
type Ticket = (Location, Xcm<()>);
fn validate(
destination: &mut Option<Location>,
message: &mut Option<Xcm<()>>,
) -> SendResult<Self::Ticket> {
let pair = (destination.take().unwrap(), message.take().unwrap());
Ok((pair, (BridgeFeeAsset::get(), HRMP_FEE).into()))
}
fn deliver(pair: Self::Ticket) -> Result<XcmHash, SendError> {
let hash = fake_message_hash(&pair.1);
SENT_XCM.with(|q| q.borrow_mut().push(pair));
Ok(hash)
}
}
impl InspectMessageQueues for TestToBridgeHubSender {
fn clear_messages() {
SENT_XCM.with(|q| q.borrow_mut().clear());
}
fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
SENT_XCM.with(|q| {
(*q.borrow())
.clone()
.iter()
.map(|(location, message)| {
(
VersionedLocation::from(location.clone()),
vec![VersionedXcm::from(message.clone())],
)
})
.collect()
})
}
}
pub struct TestLocalXcmChannelManager;
impl TestLocalXcmChannelManager {
pub fn make_congested(with: &Location) {
pezframe_support::storage::unhashed::put(
&(b"TestLocalXcmChannelManager.Congested", with).encode()[..],
&true,
);
}
}
impl XcmChannelStatusProvider for TestLocalXcmChannelManager {
fn is_congested(with: &Location) -> bool {
pezframe_support::storage::unhashed::get_or_default(
&(b"TestLocalXcmChannelManager.Congested", with).encode()[..],
)
}
}
/// Return test externalities to use in tests.
pub fn new_test_ext() -> pezsp_io::TestExternalities {
let t = pezframe_system::GenesisConfig::<TestRuntime>::default().build_storage().unwrap();
pezsp_io::TestExternalities::new(t)
}
/// Run pezpallet test.
pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
new_test_ext().execute_with(|| {
System::set_block_number(1);
System::reset_events();
test()
})
}
pub(crate) fn fake_message_hash<T>(message: &Xcm<T>) -> XcmHash {
message.using_encoded(pezsp_io::hashing::blake2_256)
}
@@ -0,0 +1,150 @@
// 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/>.
//! Autogenerated weights for pezpallet_xcm_bridge_hub_router
//!
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2023-08-03, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `covid`, CPU: `11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz`
//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
// Executed Command:
// target/release/rip-bridge-node
// benchmark
// pezpallet
// --chain=dev
// --steps=50
// --repeat=20
// --pezpallet=pezpallet_xcm_bridge_hub_router
// --extrinsic=*
// --execution=wasm
// --wasm-execution=Compiled
// --heap-pages=4096
// --output=./modules/xcm-bridge-hub-router/src/weights.rs
// --template=./.maintain/bridge-weight-template.hbs
#![allow(clippy::all)]
#![allow(unused_parens)]
#![allow(unused_imports)]
#![allow(missing_docs)]
use pezframe_support::{
traits::Get,
weights::{constants::RocksDbWeight, Weight},
};
use pezsp_std::marker::PhantomData;
/// Weight functions needed for pezpallet_xcm_bridge_hub_router.
pub trait WeightInfo {
fn on_initialize_when_non_congested() -> Weight;
fn on_initialize_when_congested() -> Weight;
fn report_bridge_status() -> Weight;
}
/// Weights for `pezpallet_xcm_bridge_hub_router` that are generated using one of the Bridge testnets.
///
/// Those weights are test only and must never be used in production.
pub struct BridgeWeight<T>(PhantomData<T>);
impl<T: pezframe_system::Config> WeightInfo for BridgeWeight<T> {
///
/// Storage: `XcmBridgeHubRouter::DeliveryFeeFactor` (r:1 w:1)
///
/// Proof: `XcmBridgeHubRouter::DeliveryFeeFactor` (`max_values`: Some(1), `max_size`: Some(16),
/// added: 511, mode: `MaxEncodedLen`)
fn on_initialize_when_non_congested() -> Weight {
// Proof Size summary in bytes:
// Measured: `52`
// Estimated: `3517`
// Minimum execution time: 11_141 nanoseconds.
Weight::from_parts(11_339_000, 3517)
.saturating_add(T::DbWeight::get().reads(2_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765`
/// (r:1 w:0)
///
/// Proof: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765` (r:1
/// w:0)
fn on_initialize_when_congested() -> Weight {
// Proof Size summary in bytes:
// Measured: `82`
// Estimated: `3547`
// Minimum execution time: 4_239 nanoseconds.
Weight::from_parts(4_383_000, 3547).saturating_add(T::DbWeight::get().reads(1_u64))
}
/// Storage: `XcmBridgeHubRouter::Bridge` (r:1 w:1)
///
/// Proof: `XcmBridgeHubRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added:
/// 512, mode: `MaxEncodedLen`)
fn report_bridge_status() -> Weight {
// Proof Size summary in bytes:
// Measured: `53`
// Estimated: `1502`
// Minimum execution time: 10_427 nanoseconds.
Weight::from_parts(10_682_000, 1502)
.saturating_add(T::DbWeight::get().reads(1_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
}
// For backwards compatibility and tests
impl WeightInfo for () {
/// Storage: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765`
/// (r:1 w:0)
///
/// Proof: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765` (r:1
/// w:0)
///
/// Storage: `XcmBridgeHubRouter::DeliveryFeeFactor` (r:1 w:1)
///
/// Proof: `XcmBridgeHubRouter::DeliveryFeeFactor` (`max_values`: Some(1), `max_size`: Some(16),
/// added: 511, mode: `MaxEncodedLen`)
fn on_initialize_when_non_congested() -> Weight {
// Proof Size summary in bytes:
// Measured: `52`
// Estimated: `3517`
// Minimum execution time: 11_141 nanoseconds.
Weight::from_parts(11_339_000, 3517)
.saturating_add(RocksDbWeight::get().reads(2_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765`
/// (r:1 w:0)
///
/// Proof: UNKNOWN KEY `0x456d756c617465645369626c696e6758636d704368616e6e656c2e436f6e6765` (r:1
/// w:0)
fn on_initialize_when_congested() -> Weight {
// Proof Size summary in bytes:
// Measured: `82`
// Estimated: `3547`
// Minimum execution time: 4_239 nanoseconds.
Weight::from_parts(4_383_000, 3547).saturating_add(RocksDbWeight::get().reads(1_u64))
}
/// Storage: `XcmBridgeHubRouter::Bridge` (r:1 w:1)
///
/// Proof: `XcmBridgeHubRouter::Bridge` (`max_values`: Some(1), `max_size`: Some(17), added:
/// 512, mode: `MaxEncodedLen`)
fn report_bridge_status() -> Weight {
// Proof Size summary in bytes:
// Measured: `53`
// Estimated: `1502`
// Minimum execution time: 10_427 nanoseconds.
Weight::from_parts(10_682_000, 1502)
.saturating_add(RocksDbWeight::get().reads(1_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
}
@@ -0,0 +1,96 @@
[package]
name = "pezpallet-xcm-bridge-hub"
description = "Module that adds dynamic bridges/lanes support to XCM infrastructure at the bridge hub."
version = "0.2.0"
authors.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
repository.workspace = true
documentation = "https://docs.rs/pezpallet-xcm-bridge-hub"
homepage = { workspace = true }
[lints]
workspace = true
[dependencies]
codec = { workspace = true }
scale-info = { features = ["derive"], workspace = true }
tracing = { workspace = true }
# Bridge Dependencies
bp-messages = { workspace = true }
pezbp-runtime = { workspace = true }
bp-xcm-bridge-hub = { workspace = true }
pezpallet-bridge-messages = { workspace = true }
# Bizinikiwi Dependencies
pezframe-support = { workspace = true }
pezframe-system = { workspace = true }
pezsp-core = { workspace = true }
pezsp-runtime = { workspace = true }
pezsp-std = { workspace = true }
# Pezkuwi Dependencies
xcm = { workspace = true }
xcm-builder = { workspace = true }
xcm-executor = { workspace = true }
[dev-dependencies]
bp-header-pez-chain = { workspace = true }
bp-xcm-bridge-hub-router = { workspace = true }
pezpallet-balances = { workspace = true }
pezpallet-xcm-bridge-hub-router = { workspace = true }
pezkuwi-teyrchain-primitives = { workspace = true }
pezsp-io = { workspace = true }
[features]
default = ["std"]
std = [
"bp-header-pez-chain/std",
"bp-messages/std",
"pezbp-runtime/std",
"bp-xcm-bridge-hub-router/std",
"bp-xcm-bridge-hub/std",
"codec/std",
"pezframe-support/std",
"pezframe-system/std",
"pezpallet-balances/std",
"pezpallet-bridge-messages/std",
"pezpallet-xcm-bridge-hub-router/std",
"pezkuwi-teyrchain-primitives/std",
"scale-info/std",
"pezsp-core/std",
"pezsp-io/std",
"pezsp-runtime/std",
"pezsp-std/std",
"tracing/std",
"xcm-builder/std",
"xcm-executor/std",
"xcm/std",
]
runtime-benchmarks = [
"bp-header-pez-chain/runtime-benchmarks",
"bp-messages/runtime-benchmarks",
"pezbp-runtime/runtime-benchmarks",
"bp-xcm-bridge-hub-router/runtime-benchmarks",
"bp-xcm-bridge-hub/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezpallet-balances/runtime-benchmarks",
"pezpallet-bridge-messages/runtime-benchmarks",
"pezpallet-xcm-bridge-hub-router/runtime-benchmarks",
"pezkuwi-teyrchain-primitives/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
"xcm-builder/runtime-benchmarks",
"xcm-executor/runtime-benchmarks",
"xcm/runtime-benchmarks",
]
try-runtime = [
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezpallet-balances/try-runtime",
"pezpallet-bridge-messages/try-runtime",
"pezpallet-xcm-bridge-hub-router/try-runtime",
"pezsp-runtime/try-runtime",
]
@@ -0,0 +1,278 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! The code that allows to use the pezpallet (`pezpallet-xcm-bridge-hub`) as inbound
//! bridge messages dispatcher. Internally, it just forwards inbound blob to the
//! XCM-level blob dispatcher, which pushes message to some other queue (e.g.
//! to HRMP queue with the sibling target chain).
//!
//! This code is executed at the target bridge hub.
use crate::{Config, Pezpallet, LOG_TARGET};
use bp_messages::target_chain::{DispatchMessage, MessageDispatch};
use pezbp_runtime::messages::MessageDispatchResult;
use bp_xcm_bridge_hub::{LocalXcmChannelManager, XcmAsPlainPayload};
use codec::{Decode, DecodeWithMemTracking, Encode};
use pezframe_support::{weights::Weight, CloneNoBound, EqNoBound, PartialEqNoBound};
use pezpallet_bridge_messages::{Config as BridgeMessagesConfig, WeightInfoExt};
use scale_info::TypeInfo;
use pezsp_runtime::SaturatedConversion;
use xcm::prelude::*;
use xcm_builder::{DispatchBlob, DispatchBlobError};
/// Message dispatch result type for single message.
#[derive(
CloneNoBound,
EqNoBound,
PartialEqNoBound,
Encode,
Decode,
DecodeWithMemTracking,
Debug,
TypeInfo,
)]
pub enum XcmBlobMessageDispatchResult {
/// We've been unable to decode message payload.
InvalidPayload,
/// Message has been dispatched.
Dispatched,
/// Message has **NOT** been dispatched because of given error.
NotDispatched(#[codec(skip)] Option<DispatchBlobError>),
}
/// An easy way to access associated messages pezpallet weights.
type MessagesPalletWeights<T, I> =
<T as BridgeMessagesConfig<<T as Config<I>>::BridgeMessagesPalletInstance>>::WeightInfo;
impl<T: Config<I>, I: 'static> MessageDispatch for Pezpallet<T, I>
where
T: BridgeMessagesConfig<T::BridgeMessagesPalletInstance, InboundPayload = XcmAsPlainPayload>,
{
type DispatchPayload = XcmAsPlainPayload;
type DispatchLevelResult = XcmBlobMessageDispatchResult;
type LaneId = T::LaneId;
fn is_active(lane: Self::LaneId) -> bool {
Pezpallet::<T, I>::bridge_by_lane_id(&lane)
.and_then(|(_, bridge)| (*bridge.bridge_origin_relative_location).try_into().ok())
.map(|recipient: Location| !T::LocalXcmChannelManager::is_congested(&recipient))
.unwrap_or(false)
}
fn dispatch_weight(
message: &mut DispatchMessage<Self::DispatchPayload, Self::LaneId>,
) -> Weight {
match message.data.payload {
Ok(ref payload) => {
let payload_size = payload.encoded_size().saturated_into();
MessagesPalletWeights::<T, I>::message_dispatch_weight(payload_size)
},
Err(_) => Weight::zero(),
}
}
fn dispatch(
message: DispatchMessage<Self::DispatchPayload, Self::LaneId>,
) -> MessageDispatchResult<Self::DispatchLevelResult> {
let payload = match message.data.payload {
Ok(payload) => payload,
Err(e) => {
tracing::error!(
target: LOG_TARGET,
error=?e,
lane_id=?message.key.lane_id,
message_nonce=?message.key.nonce,
"dispatch - payload error"
);
return MessageDispatchResult {
unspent_weight: Weight::zero(),
dispatch_level_result: XcmBlobMessageDispatchResult::InvalidPayload,
};
},
};
let dispatch_level_result = match T::BlobDispatcher::dispatch_blob(payload) {
Ok(_) => {
tracing::debug!(
target: LOG_TARGET,
lane_id=?message.key.lane_id,
message_nonce=?message.key.nonce,
"dispatch - `DispatchBlob::dispatch_blob` was ok"
);
XcmBlobMessageDispatchResult::Dispatched
},
Err(e) => {
tracing::error!(
target: LOG_TARGET,
error=?e,
lane_id=?message.key.lane_id,
message_nonce=?message.key.nonce,
"dispatch - `DispatchBlob::dispatch_blob` failed"
);
XcmBlobMessageDispatchResult::NotDispatched(Some(e))
},
};
MessageDispatchResult { unspent_weight: Weight::zero(), dispatch_level_result }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{mock::*, Bridges, LaneToBridge, LanesManagerOf};
use bp_messages::{target_chain::DispatchMessageData, LaneIdType, MessageKey};
use bp_xcm_bridge_hub::{Bridge, BridgeLocations, BridgeState};
use pezframe_support::assert_ok;
use pezpallet_bridge_messages::InboundLaneStorage;
use xcm_executor::traits::ConvertLocation;
fn bridge() -> (Box<BridgeLocations>, TestLaneIdType) {
let origin = OpenBridgeOrigin::sibling_teyrchain_origin();
let with = bridged_asset_hub_universal_location();
let locations =
XcmOverBridge::bridge_locations_from_origin(origin, Box::new(with.into())).unwrap();
let lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap();
(locations, lane_id)
}
fn run_test_with_opened_bridge(test: impl FnOnce()) {
run_test(|| {
let (bridge, lane_id) = bridge();
if !Bridges::<TestRuntime, ()>::contains_key(bridge.bridge_id()) {
// insert bridge
Bridges::<TestRuntime, ()>::insert(
bridge.bridge_id(),
Bridge {
bridge_origin_relative_location: Box::new(
bridge.bridge_origin_relative_location().clone().into(),
),
bridge_origin_universal_location: Box::new(
bridge.bridge_origin_universal_location().clone().into(),
),
bridge_destination_universal_location: Box::new(
bridge.bridge_destination_universal_location().clone().into(),
),
state: BridgeState::Opened,
bridge_owner_account: LocationToAccountId::convert_location(
bridge.bridge_origin_relative_location(),
)
.expect("valid accountId"),
deposit: 0,
lane_id,
},
);
LaneToBridge::<TestRuntime, ()>::insert(lane_id, bridge.bridge_id());
// create lanes
let lanes_manager = LanesManagerOf::<TestRuntime, ()>::new();
if lanes_manager.create_inbound_lane(lane_id).is_ok() {
assert_eq!(
0,
lanes_manager
.active_inbound_lane(lane_id)
.unwrap()
.storage()
.data()
.last_confirmed_nonce
);
}
if lanes_manager.create_outbound_lane(lane_id).is_ok() {
assert!(lanes_manager
.active_outbound_lane(lane_id)
.unwrap()
.queued_messages()
.is_empty());
}
}
assert_ok!(XcmOverBridge::do_try_state());
test();
});
}
fn invalid_message() -> DispatchMessage<Vec<u8>, TestLaneIdType> {
DispatchMessage {
key: MessageKey { lane_id: TestLaneIdType::try_new(1, 2).unwrap(), nonce: 1 },
data: DispatchMessageData { payload: Err(codec::Error::from("test")) },
}
}
fn valid_message() -> DispatchMessage<Vec<u8>, TestLaneIdType> {
DispatchMessage {
key: MessageKey { lane_id: TestLaneIdType::try_new(1, 2).unwrap(), nonce: 1 },
data: DispatchMessageData { payload: Ok(vec![42]) },
}
}
#[test]
fn dispatcher_is_inactive_when_channel_with_target_chain_is_congested() {
run_test_with_opened_bridge(|| {
TestLocalXcmChannelManager::make_congested();
assert!(!XcmOverBridge::is_active(bridge().1));
});
}
#[test]
fn dispatcher_is_active_when_channel_with_target_chain_is_not_congested() {
run_test_with_opened_bridge(|| {
assert!(XcmOverBridge::is_active(bridge().1));
});
}
#[test]
fn dispatch_weight_is_zero_if_we_have_failed_to_decode_message() {
run_test(|| {
assert_eq!(XcmOverBridge::dispatch_weight(&mut invalid_message()), Weight::zero());
});
}
#[test]
fn dispatch_weight_is_non_zero_if_we_have_decoded_message() {
run_test(|| {
assert_ne!(XcmOverBridge::dispatch_weight(&mut valid_message()), Weight::zero());
});
}
#[test]
fn message_is_not_dispatched_when_we_have_failed_to_decode_message() {
run_test(|| {
assert_eq!(
XcmOverBridge::dispatch(invalid_message()),
MessageDispatchResult {
unspent_weight: Weight::zero(),
dispatch_level_result: XcmBlobMessageDispatchResult::InvalidPayload,
},
);
assert!(!TestBlobDispatcher::is_dispatched());
});
}
#[test]
fn message_is_dispatched_when_we_have_decoded_message() {
run_test(|| {
assert_eq!(
XcmOverBridge::dispatch(valid_message()),
MessageDispatchResult {
unspent_weight: Weight::zero(),
dispatch_level_result: XcmBlobMessageDispatchResult::Dispatched,
},
);
assert!(TestBlobDispatcher::is_dispatched());
});
}
}
@@ -0,0 +1,873 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! The code that allows to use the pezpallet (`pezpallet-xcm-bridge-hub`) as XCM message
//! exporter at the sending bridge hub. Internally, it just enqueues outbound blob
//! in the messages pezpallet queue.
//!
//! This code is executed at the source bridge hub.
use crate::{Config, Pezpallet, LOG_TARGET};
use crate::{BridgeOf, Bridges};
use bp_messages::{
source_chain::{MessagesBridge, OnMessagesDelivered},
MessageNonce,
};
use bp_xcm_bridge_hub::{BridgeId, BridgeState, LocalXcmChannelManager, XcmAsPlainPayload};
use pezframe_support::{ensure, traits::Get};
use pezpallet_bridge_messages::{
Config as BridgeMessagesConfig, Error, Pezpallet as BridgeMessagesPallet,
};
use xcm::prelude::*;
use xcm_builder::{HaulBlob, HaulBlobError, HaulBlobExporter};
use xcm_executor::traits::ExportXcm;
/// Maximal number of messages in the outbound bridge queue. Once we reach this limit, we
/// suspend a bridge.
const OUTBOUND_LANE_CONGESTED_THRESHOLD: MessageNonce = 8_192;
/// After we have suspended the bridge, we wait until number of messages in the outbound bridge
/// queue drops to this count, before sending resuming the bridge.
const OUTBOUND_LANE_UNCONGESTED_THRESHOLD: MessageNonce = 1_024;
/// An easy way to access `HaulBlobExporter`.
pub type PalletAsHaulBlobExporter<T, I> = HaulBlobExporter<
DummyHaulBlob,
<T as Config<I>>::BridgedNetwork,
<T as Config<I>>::DestinationVersion,
<T as Config<I>>::MessageExportPrice,
>;
/// An easy way to access associated messages pezpallet.
type MessagesPallet<T, I> = BridgeMessagesPallet<T, <T as Config<I>>::BridgeMessagesPalletInstance>;
impl<T: Config<I>, I: 'static> ExportXcm for Pezpallet<T, I>
where
T: BridgeMessagesConfig<T::BridgeMessagesPalletInstance, OutboundPayload = XcmAsPlainPayload>,
{
type Ticket = (
BridgeId,
BridgeOf<T, I>,
<MessagesPallet<T, I> as MessagesBridge<T::OutboundPayload, T::LaneId>>::SendMessageArgs,
XcmHash,
);
fn validate(
network: NetworkId,
channel: u32,
universal_source: &mut Option<InteriorLocation>,
destination: &mut Option<InteriorLocation>,
message: &mut Option<Xcm<()>>,
) -> Result<(Self::Ticket, Assets), SendError> {
tracing::trace!(
target: LOG_TARGET,
?network,
?channel,
?universal_source,
?destination,
"Validate for network"
);
// `HaulBlobExporter` may consume the `universal_source` and `destination` arguments, so
// let's save them before
let bridge_origin_universal_location =
universal_source.clone().ok_or(SendError::MissingArgument)?;
// Note: watch out this is `ExportMessage::destination`, which is relative to the `network`,
// which means it does not contain `GlobalConsensus`, We need to find `BridgeId` with
// `Self::bridge_locations` which requires **universal** location for destination.
let bridge_destination_universal_location = {
let dest = destination.clone().ok_or(SendError::MissingArgument)?;
match dest.global_consensus() {
Ok(dest_network) => {
tracing::trace!(
target: LOG_TARGET,
?dest,
?dest_network,
?network,
"Destination is already universal, checking if matches: {:?}",
dest_network == network
);
ensure!(dest_network == network, SendError::NotApplicable);
// ok, `dest` looks like a universal location, so let's use it
dest
},
Err(_) => {
// `dest` is not a universal location, so we need to prepend it with
// `GlobalConsensus`.
dest.pushed_front_with(GlobalConsensus(network)).map_err(|error_data| {
tracing::error!(
target: LOG_TARGET, error=?error_data,
"Destination is not a universal and prepending failed!"
);
SendError::NotApplicable
})?
},
}
};
// prepare the origin relative location
let bridge_origin_relative_location =
bridge_origin_universal_location.relative_to(&T::UniversalLocation::get());
// then we are able to compute the `BridgeId` and find `LaneId` used to send messages
let locations = Self::bridge_locations(
bridge_origin_relative_location,
bridge_destination_universal_location.into(),
)
.map_err(|e| {
tracing::error!(
target: LOG_TARGET, error=?e,
"Validate `bridge_locations` with error"
);
SendError::NotApplicable
})?;
let bridge = Self::bridge(locations.bridge_id()).ok_or_else(|| {
tracing::error!(
target: LOG_TARGET,
bridge_origin_relative_location=?locations.bridge_origin_relative_location(),
bridge_destination_universal_location=?locations.bridge_destination_universal_location(),
"No opened bridge for requested"
);
SendError::NotApplicable
})?;
// check if we are able to route the message. We use existing `HaulBlobExporter` for that.
// It will make all required changes and will encode message properly, so that the
// `DispatchBlob` at the bridged bridge hub will be able to decode it
let ((blob, id), price) = PalletAsHaulBlobExporter::<T, I>::validate(
network,
channel,
universal_source,
destination,
message,
)?;
let bridge_message = MessagesPallet::<T, I>::validate_message(bridge.lane_id, &blob)
.map_err(|e| {
match e {
Error::LanesManager(ref ei) => {
tracing::error!(target: LOG_TARGET, error=?ei, "LanesManager")
},
Error::MessageRejectedByPallet(ref ei) => {
tracing::error!(target: LOG_TARGET, error=?ei, "MessageRejectedByPallet")
},
Error::ReceptionConfirmation(ref ei) => {
tracing::error!(target: LOG_TARGET, error=?ei, "ReceptionConfirmation")
},
_ => (),
};
tracing::error!(
target: LOG_TARGET,
error=?e,
topic_id=?id,
bridge_id=?locations,
lane_id=?bridge.lane_id,
"XCM message cannot be exported"
);
SendError::Transport("BridgeValidateError")
})?;
Ok(((*locations.bridge_id(), bridge, bridge_message, id), price))
}
fn deliver(
(bridge_id, bridge, bridge_message, id): Self::Ticket,
) -> Result<XcmHash, SendError> {
let artifacts = MessagesPallet::<T, I>::send_message(bridge_message);
tracing::info!(
target: LOG_TARGET,
topic_id=?id,
bridge_id=?bridge_id,
lane_id=?bridge.lane_id,
nonce=%artifacts.nonce,
"XCM message has been enqueued"
);
// maybe we need switch to congested state
Self::on_bridge_message_enqueued(bridge_id, bridge, artifacts.enqueued_messages);
Ok(id)
}
}
impl<T: Config<I>, I: 'static> OnMessagesDelivered<T::LaneId> for Pezpallet<T, I> {
fn on_messages_delivered(lane_id: T::LaneId, enqueued_messages: MessageNonce) {
Self::on_bridge_messages_delivered(lane_id, enqueued_messages);
}
}
impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
/// Called when new message is pushed onto outbound bridge queue.
fn on_bridge_message_enqueued(
bridge_id: BridgeId,
bridge: BridgeOf<T, I>,
enqueued_messages: MessageNonce,
) {
// if the bridge queue is not congested, we don't want to do anything
let is_congested = enqueued_messages > OUTBOUND_LANE_CONGESTED_THRESHOLD;
if !is_congested {
return;
}
// TODO: https://github.com/pezkuwichain/kurdistan-sdk/issues/83 we either need fishermens
// to watch this rule violation (suspended, but keep sending new messages), or we need a
// hard limit for that like other XCM queues have
// check if the lane is already suspended. If it is, do nothing. We still accept new
// messages to the suspended bridge, hoping that it'll be actually resumed soon
if bridge.state == BridgeState::Suspended {
return;
}
// else - suspend the bridge
let result_bridge_origin_relative_location =
(*bridge.bridge_origin_relative_location).clone().try_into();
let bridge_origin_relative_location = match &result_bridge_origin_relative_location {
Ok(bridge_origin_relative_location) => bridge_origin_relative_location,
Err(_) => {
tracing::debug!(
target: LOG_TARGET,
?bridge_id,
origin_location=?bridge.bridge_origin_relative_location,
"Failed to convert"
);
return;
},
};
let suspend_result =
T::LocalXcmChannelManager::suspend_bridge(bridge_origin_relative_location, bridge_id);
match suspend_result {
Ok(_) => {
tracing::debug!(
target: LOG_TARGET,
?bridge_id,
originated_by=?bridge.bridge_origin_relative_location,
"Suspended"
);
},
Err(e) => {
tracing::debug!(
target: LOG_TARGET,
error=?e,
?bridge_id,
originated_by=?bridge.bridge_origin_relative_location,
"Failed to suspended"
);
return;
},
}
// and remember that we have suspended the bridge
Bridges::<T, I>::mutate_extant(bridge_id, |bridge| {
bridge.state = BridgeState::Suspended;
});
}
/// Must be called whenever we receive a message delivery confirmation.
fn on_bridge_messages_delivered(lane_id: T::LaneId, enqueued_messages: MessageNonce) {
// if the bridge queue is still congested, we don't want to do anything
let is_congested = enqueued_messages > OUTBOUND_LANE_UNCONGESTED_THRESHOLD;
if is_congested {
return;
}
// if we have not suspended the bridge before (or it is closed), we don't want to do
// anything
let (bridge_id, bridge) = match Self::bridge_by_lane_id(&lane_id) {
Some(bridge) if bridge.1.state == BridgeState::Suspended => bridge,
_ => {
// if there is no bridge or it has been closed, then we don't need to send resume
// signal to the local origin - it has closed bridge itself, so it should have
// alrady pruned everything else
return;
},
};
// else - resume the bridge
let bridge_origin_relative_location = (*bridge.bridge_origin_relative_location).try_into();
let bridge_origin_relative_location = match bridge_origin_relative_location {
Ok(bridge_origin_relative_location) => bridge_origin_relative_location,
Err(e) => {
tracing::debug!(
target: LOG_TARGET,
error=?e,
?bridge_id,
?lane_id,
"Failed to convert",
);
return;
},
};
let resume_result =
T::LocalXcmChannelManager::resume_bridge(&bridge_origin_relative_location, bridge_id);
match resume_result {
Ok(_) => {
tracing::debug!(
target: LOG_TARGET,
?bridge_id,
?lane_id,
originated_by=?bridge_origin_relative_location,
"Resumed",
);
},
Err(e) => {
tracing::debug!(
target: LOG_TARGET,
error=?e,
?bridge_id,
?lane_id,
originated_by=?bridge_origin_relative_location,
"Failed to resume"
);
return;
},
}
// and forget that we have previously suspended the bridge
Bridges::<T, I>::mutate_extant(bridge_id, |bridge| {
bridge.state = BridgeState::Opened;
});
}
}
/// Dummy implementation of the `HaulBlob` trait that is never called.
///
/// We are using `HaulBlobExporter`, which requires `HaulBlob` implementation. It assumes that
/// there's a single channel between two bridge hubs - `HaulBlob` only accepts the blob and nothing
/// else. But bridge messages pezpallet may have a dedicated channel (lane) for every pair of bridged
/// chains. So we are using our own `ExportXcm` implementation, but to utilize `HaulBlobExporter` we
/// still need this `DummyHaulBlob`.
pub struct DummyHaulBlob;
impl HaulBlob for DummyHaulBlob {
fn haul_blob(_blob: XcmAsPlainPayload) -> Result<(), HaulBlobError> {
Err(HaulBlobError::Transport("DummyHaulBlob"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{mock::*, Bridges, LaneToBridge, LanesManagerOf};
use pezbp_runtime::RangeInclusiveExt;
use bp_xcm_bridge_hub::{Bridge, BridgeLocations, BridgeState};
use pezframe_support::{assert_ok, traits::EnsureOrigin};
use pezpallet_bridge_messages::InboundLaneStorage;
use xcm_builder::{NetworkExportTable, UnpaidRemoteExporter};
use xcm_executor::traits::{export_xcm, ConvertLocation};
fn universal_source() -> InteriorLocation {
SiblingUniversalLocation::get()
}
fn bridged_relative_destination() -> InteriorLocation {
BridgedRelativeDestination::get()
}
fn bridged_universal_destination() -> InteriorLocation {
BridgedUniversalDestination::get()
}
fn open_lane(origin: RuntimeOrigin) -> (BridgeLocations, TestLaneIdType) {
// open expected outbound lane
let with = bridged_asset_hub_universal_location();
let locations =
XcmOverBridge::bridge_locations_from_origin(origin, Box::new(with.into())).unwrap();
let lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap();
if !Bridges::<TestRuntime, ()>::contains_key(locations.bridge_id()) {
// insert bridge
Bridges::<TestRuntime, ()>::insert(
locations.bridge_id(),
Bridge {
bridge_origin_relative_location: Box::new(SiblingLocation::get().into()),
bridge_origin_universal_location: Box::new(
locations.bridge_origin_universal_location().clone().into(),
),
bridge_destination_universal_location: Box::new(
locations.bridge_destination_universal_location().clone().into(),
),
state: BridgeState::Opened,
bridge_owner_account: LocationToAccountId::convert_location(
locations.bridge_origin_relative_location(),
)
.expect("valid accountId"),
deposit: 0,
lane_id,
},
);
LaneToBridge::<TestRuntime, ()>::insert(lane_id, locations.bridge_id());
// create lanes
let lanes_manager = LanesManagerOf::<TestRuntime, ()>::new();
if lanes_manager.create_inbound_lane(lane_id).is_ok() {
assert_eq!(
0,
lanes_manager
.active_inbound_lane(lane_id)
.unwrap()
.storage()
.data()
.last_confirmed_nonce
);
}
if lanes_manager.create_outbound_lane(lane_id).is_ok() {
assert!(lanes_manager
.active_outbound_lane(lane_id)
.unwrap()
.queued_messages()
.is_empty());
}
}
assert_ok!(XcmOverBridge::do_try_state());
(*locations, lane_id)
}
fn open_lane_and_send_regular_message() -> (BridgeId, TestLaneIdType) {
let (locations, lane_id) = open_lane(OpenBridgeOrigin::sibling_teyrchain_origin());
// now let's try to enqueue message using our `ExportXcm` implementation
export_xcm::<XcmOverBridge>(
BridgedRelayNetwork::get(),
0,
locations.bridge_origin_universal_location().clone(),
locations.bridge_destination_universal_location().clone(),
vec![Instruction::ClearOrigin].into(),
)
.unwrap();
(*locations.bridge_id(), lane_id)
}
#[test]
fn exporter_works() {
run_test(|| {
let (_, lane_id) = open_lane_and_send_regular_message();
// double check that the message has been pushed to the expected lane
// (it should already been checked during `send_message` call)
assert!(!LanesManagerOf::<TestRuntime, ()>::new()
.active_outbound_lane(lane_id)
.unwrap()
.queued_messages()
.is_empty());
});
}
#[test]
fn exporter_does_not_suspend_the_bridge_if_outbound_bridge_queue_is_not_congested() {
run_test(|| {
let (bridge_id, _) = open_lane_and_send_regular_message();
assert!(!TestLocalXcmChannelManager::is_bridge_suspended(&bridge_id));
assert_eq!(XcmOverBridge::bridge(&bridge_id).unwrap().state, BridgeState::Opened);
});
}
#[test]
fn exporter_does_not_suspend_the_bridge_if_it_is_already_suspended() {
run_test(|| {
let (bridge_id, _) = open_lane_and_send_regular_message();
Bridges::<TestRuntime, ()>::mutate_extant(bridge_id, |bridge| {
bridge.state = BridgeState::Suspended;
});
for _ in 1..OUTBOUND_LANE_CONGESTED_THRESHOLD {
open_lane_and_send_regular_message();
}
open_lane_and_send_regular_message();
assert!(!TestLocalXcmChannelManager::is_bridge_suspended(&bridge_id));
});
}
#[test]
fn exporter_suspends_the_bridge_if_outbound_bridge_queue_is_congested() {
run_test(|| {
let (bridge_id, _) = open_lane_and_send_regular_message();
for _ in 1..OUTBOUND_LANE_CONGESTED_THRESHOLD {
open_lane_and_send_regular_message();
}
assert!(!TestLocalXcmChannelManager::is_bridge_suspended(&bridge_id));
assert_eq!(XcmOverBridge::bridge(&bridge_id).unwrap().state, BridgeState::Opened);
open_lane_and_send_regular_message();
assert!(TestLocalXcmChannelManager::is_bridge_suspended(&bridge_id));
assert_eq!(XcmOverBridge::bridge(&bridge_id).unwrap().state, BridgeState::Suspended);
});
}
#[test]
fn bridge_is_not_resumed_if_outbound_bridge_queue_is_still_congested() {
run_test(|| {
let (bridge_id, lane_id) = open_lane_and_send_regular_message();
Bridges::<TestRuntime, ()>::mutate_extant(bridge_id, |bridge| {
bridge.state = BridgeState::Suspended;
});
XcmOverBridge::on_bridge_messages_delivered(
lane_id,
OUTBOUND_LANE_UNCONGESTED_THRESHOLD + 1,
);
assert!(!TestLocalXcmChannelManager::is_bridge_resumed(&bridge_id));
assert_eq!(XcmOverBridge::bridge(&bridge_id).unwrap().state, BridgeState::Suspended);
});
}
#[test]
fn bridge_is_not_resumed_if_it_was_not_suspended_before() {
run_test(|| {
let (bridge_id, lane_id) = open_lane_and_send_regular_message();
XcmOverBridge::on_bridge_messages_delivered(
lane_id,
OUTBOUND_LANE_UNCONGESTED_THRESHOLD,
);
assert!(!TestLocalXcmChannelManager::is_bridge_resumed(&bridge_id));
assert_eq!(XcmOverBridge::bridge(&bridge_id).unwrap().state, BridgeState::Opened);
});
}
#[test]
fn bridge_is_resumed_when_enough_messages_are_delivered() {
run_test(|| {
let (bridge_id, lane_id) = open_lane_and_send_regular_message();
Bridges::<TestRuntime, ()>::mutate_extant(bridge_id, |bridge| {
bridge.state = BridgeState::Suspended;
});
XcmOverBridge::on_bridge_messages_delivered(
lane_id,
OUTBOUND_LANE_UNCONGESTED_THRESHOLD,
);
assert!(TestLocalXcmChannelManager::is_bridge_resumed(&bridge_id));
assert_eq!(XcmOverBridge::bridge(&bridge_id).unwrap().state, BridgeState::Opened);
});
}
#[test]
fn export_fails_if_argument_is_missing() {
run_test(|| {
assert_eq!(
XcmOverBridge::validate(
BridgedRelayNetwork::get(),
0,
&mut None,
&mut Some(bridged_relative_destination()),
&mut Some(Vec::new().into()),
),
Err(SendError::MissingArgument),
);
assert_eq!(
XcmOverBridge::validate(
BridgedRelayNetwork::get(),
0,
&mut Some(universal_source()),
&mut None,
&mut Some(Vec::new().into()),
),
Err(SendError::MissingArgument),
);
})
}
#[test]
fn exporter_computes_correct_lane_id() {
run_test(|| {
assert_ne!(bridged_universal_destination(), bridged_relative_destination());
let locations = BridgeLocations::bridge_locations(
UniversalLocation::get(),
SiblingLocation::get(),
bridged_universal_destination(),
BridgedRelayNetwork::get(),
)
.unwrap();
let expected_bridge_id = locations.bridge_id();
let expected_lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap();
if LanesManagerOf::<TestRuntime, ()>::new()
.create_outbound_lane(expected_lane_id)
.is_ok()
{
Bridges::<TestRuntime, ()>::insert(
expected_bridge_id,
Bridge {
bridge_origin_relative_location: Box::new(
locations.bridge_origin_relative_location().clone().into(),
),
bridge_origin_universal_location: Box::new(
locations.bridge_origin_universal_location().clone().into(),
),
bridge_destination_universal_location: Box::new(
locations.bridge_destination_universal_location().clone().into(),
),
state: BridgeState::Opened,
bridge_owner_account: [0u8; 32].into(),
deposit: 0,
lane_id: expected_lane_id,
},
);
}
let ticket = XcmOverBridge::validate(
BridgedRelayNetwork::get(),
0,
&mut Some(universal_source()),
// Note: The `ExportMessage` expects relative `InteriorLocation` in the
// `BridgedRelayNetwork`.
&mut Some(bridged_relative_destination()),
&mut Some(Vec::new().into()),
)
.unwrap()
.0;
assert_eq!(&ticket.0, expected_bridge_id);
assert_eq!(ticket.1.lane_id, expected_lane_id);
});
}
#[test]
fn exporter_is_compatible_with_pallet_xcm_bridge_hub_router() {
run_test(|| {
// valid routable destination
let dest = Location::new(2, BridgedUniversalDestination::get());
// open bridge
let origin = OpenBridgeOrigin::sibling_teyrchain_origin();
let origin_as_location =
OpenBridgeOriginOf::<TestRuntime, ()>::try_origin(origin.clone()).unwrap();
let (_, expected_lane_id) = open_lane(origin);
// check before - no messages
assert_eq!(
pezpallet_bridge_messages::Pezpallet::<TestRuntime, ()>::outbound_lane_data(
expected_lane_id
)
.unwrap()
.queued_messages()
.saturating_len(),
0
);
// send `ExportMessage(message)` by `UnpaidRemoteExporter`.
ExecuteXcmOverSendXcm::set_origin_for_execute(origin_as_location);
assert_ok!(send_xcm::<
UnpaidRemoteExporter<
NetworkExportTable<BridgeTable>,
ExecuteXcmOverSendXcm,
UniversalLocation,
>,
>(dest.clone(), Xcm::<()>::default()));
// we need to set `UniversalLocation` for `sibling_teyrchain_origin` for
// `XcmOverBridgeWrappedWithExportMessageRouterInstance`.
ExportMessageOriginUniversalLocation::set(Some(SiblingUniversalLocation::get()));
// send `ExportMessage(message)` by `pezpallet_xcm_bridge_hub_router`.
ExecuteXcmOverSendXcm::set_origin_for_execute(SiblingLocation::get());
assert_ok!(send_xcm::<XcmOverBridgeWrappedWithExportMessageRouter>(
dest.clone(),
Xcm::<()>::default()
));
// check after - a message ready to be relayed
assert_eq!(
pezpallet_bridge_messages::Pezpallet::<TestRuntime, ()>::outbound_lane_data(
expected_lane_id
)
.unwrap()
.queued_messages()
.saturating_len(),
2
);
})
}
#[test]
fn validate_works() {
run_test(|| {
let xcm: Xcm<()> = vec![ClearOrigin].into();
// check that router does not consume when `NotApplicable`
let mut xcm_wrapper = Some(xcm.clone());
let mut universal_source_wrapper = Some(universal_source());
// wrong `NetworkId`
let mut dest_wrapper = Some(bridged_relative_destination());
assert_eq!(
XcmOverBridge::validate(
NetworkId::ByGenesis([0; 32]),
0,
&mut universal_source_wrapper,
&mut dest_wrapper,
&mut xcm_wrapper,
),
Err(SendError::NotApplicable),
);
// dest and xcm is NOT consumed and untouched
assert_eq!(&Some(xcm.clone()), &xcm_wrapper);
assert_eq!(&Some(universal_source()), &universal_source_wrapper);
assert_eq!(&Some(bridged_relative_destination()), &dest_wrapper);
// dest starts with wrong `NetworkId`
let mut invalid_dest_wrapper = Some(
[GlobalConsensus(NetworkId::ByGenesis([0; 32])), Teyrchain(BRIDGED_ASSET_HUB_ID)]
.into(),
);
assert_eq!(
XcmOverBridge::validate(
BridgedRelayNetwork::get(),
0,
&mut Some(universal_source()),
&mut invalid_dest_wrapper,
&mut xcm_wrapper,
),
Err(SendError::NotApplicable),
);
// dest and xcm is NOT consumed and untouched
assert_eq!(&Some(xcm.clone()), &xcm_wrapper);
assert_eq!(&Some(universal_source()), &universal_source_wrapper);
assert_eq!(
&Some(
[
GlobalConsensus(NetworkId::ByGenesis([0; 32]),),
Teyrchain(BRIDGED_ASSET_HUB_ID)
]
.into()
),
&invalid_dest_wrapper
);
// no opened lane for dest
let mut dest_without_lane_wrapper =
Some([GlobalConsensus(BridgedRelayNetwork::get()), Teyrchain(5679)].into());
assert_eq!(
XcmOverBridge::validate(
BridgedRelayNetwork::get(),
0,
&mut Some(universal_source()),
&mut dest_without_lane_wrapper,
&mut xcm_wrapper,
),
Err(SendError::NotApplicable),
);
// dest and xcm is NOT consumed and untouched
assert_eq!(&Some(xcm.clone()), &xcm_wrapper);
assert_eq!(&Some(universal_source()), &universal_source_wrapper);
assert_eq!(
&Some([GlobalConsensus(BridgedRelayNetwork::get(),), Teyrchain(5679)].into()),
&dest_without_lane_wrapper
);
// ok
let _ = open_lane(OpenBridgeOrigin::sibling_teyrchain_origin());
let mut dest_wrapper = Some(bridged_relative_destination());
assert_ok!(XcmOverBridge::validate(
BridgedRelayNetwork::get(),
0,
&mut Some(universal_source()),
&mut dest_wrapper,
&mut xcm_wrapper,
));
// dest and xcm IS consumed
assert_eq!(None, xcm_wrapper);
assert_eq!(&Some(universal_source()), &universal_source_wrapper);
assert_eq!(None, dest_wrapper);
});
}
#[test]
fn congestion_with_pallet_xcm_bridge_hub_router_works() {
run_test(|| {
// valid routable destination
let dest = Location::new(2, BridgedUniversalDestination::get());
fn router_bridge_state() -> pezpallet_xcm_bridge_hub_router::BridgeState {
pezpallet_xcm_bridge_hub_router::Bridge::<
TestRuntime,
XcmOverBridgeWrappedWithExportMessageRouterInstance,
>::get()
}
// open two bridges
let origin = OpenBridgeOrigin::sibling_teyrchain_origin();
let origin_as_location =
OpenBridgeOriginOf::<TestRuntime, ()>::try_origin(origin.clone()).unwrap();
let (bridge_1, expected_lane_id_1) = open_lane(origin);
// we need to set `UniversalLocation` for `sibling_teyrchain_origin` for
// `XcmOverBridgeWrappedWithExportMessageRouterInstance`.
ExportMessageOriginUniversalLocation::set(Some(SiblingUniversalLocation::get()));
// check before
// bridges are opened
assert_eq!(
XcmOverBridge::bridge(bridge_1.bridge_id()).unwrap().state,
BridgeState::Opened
);
// the router is uncongested
assert!(!router_bridge_state().is_congested);
assert!(!TestLocalXcmChannelManager::is_bridge_suspended(bridge_1.bridge_id()));
assert!(!TestLocalXcmChannelManager::is_bridge_resumed(bridge_1.bridge_id()));
// make bridges congested with sending too much messages
for _ in 1..(OUTBOUND_LANE_CONGESTED_THRESHOLD + 2) {
// send `ExportMessage(message)` by `pezpallet_xcm_bridge_hub_router`.
ExecuteXcmOverSendXcm::set_origin_for_execute(origin_as_location.clone());
assert_ok!(send_xcm::<XcmOverBridgeWrappedWithExportMessageRouter>(
dest.clone(),
Xcm::<()>::default()
));
}
// checks after
// bridges are suspended
assert_eq!(
XcmOverBridge::bridge(bridge_1.bridge_id()).unwrap().state,
BridgeState::Suspended,
);
// the router is congested
assert!(router_bridge_state().is_congested);
assert!(TestLocalXcmChannelManager::is_bridge_suspended(bridge_1.bridge_id()));
assert!(!TestLocalXcmChannelManager::is_bridge_resumed(bridge_1.bridge_id()));
// make bridges uncongested to trigger resume signal
XcmOverBridge::on_bridge_messages_delivered(
expected_lane_id_1,
OUTBOUND_LANE_UNCONGESTED_THRESHOLD,
);
// bridge is again opened
assert_eq!(
XcmOverBridge::bridge(bridge_1.bridge_id()).unwrap().state,
BridgeState::Opened
);
// the router is uncongested
assert!(!router_bridge_state().is_congested);
assert!(TestLocalXcmChannelManager::is_bridge_resumed(bridge_1.bridge_id()));
})
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,154 @@
// 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/>.
//! A module that is responsible for migration of storage.
use crate::{Config, Pezpallet, LOG_TARGET};
use pezframe_support::{
traits::{Get, OnRuntimeUpgrade, StorageVersion},
weights::Weight,
};
use xcm::prelude::{InteriorLocation, Location};
/// The in-code storage version.
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
/// This migration does not modify storage but can be used to open a bridge and link it to the
/// specified LaneId. This is useful when we want to open a bridge and use a custom LaneId instead
/// of the pre-calculated one provided by the `fn open_bridge extrinsic`.
/// Or perhaps if you want to ensure that your runtime (e.g., for testing) always has an open
/// bridge.
pub struct OpenBridgeForLane<
T,
I,
Lane,
CreateLane,
SourceRelativeLocation,
BridgedUniversalLocation,
>(
core::marker::PhantomData<(
T,
I,
Lane,
CreateLane,
SourceRelativeLocation,
BridgedUniversalLocation,
)>,
);
impl<
T: Config<I>,
I: 'static,
Lane: Get<T::LaneId>,
CreateLane: Get<bool>,
SourceRelativeLocation: Get<Location>,
BridgedUniversalLocation: Get<InteriorLocation>,
> OnRuntimeUpgrade
for OpenBridgeForLane<T, I, Lane, CreateLane, SourceRelativeLocation, BridgedUniversalLocation>
{
fn on_runtime_upgrade() -> Weight {
let bridge_origin_relative_location = SourceRelativeLocation::get();
let bridge_destination_universal_location = BridgedUniversalLocation::get();
let lane_id = Lane::get();
let create_lane = CreateLane::get();
tracing::info!(
target: LOG_TARGET,
?lane_id,
?create_lane,
?bridge_origin_relative_location,
?bridge_destination_universal_location,
"OpenBridgeForLane - going to open bridge"
);
let locations = match Pezpallet::<T, I>::bridge_locations(
bridge_origin_relative_location.clone(),
bridge_destination_universal_location.clone(),
) {
Ok(locations) => locations,
Err(e) => {
tracing::error!(
target: LOG_TARGET,
error=?e,
"OpenBridgeForLane - on_runtime_upgrade failed to construct bridge_locations"
);
return T::DbWeight::get().reads(0);
},
};
// check if already exists
if let Some((bridge_id, bridge)) = Pezpallet::<T, I>::bridge_by_lane_id(&lane_id) {
tracing::info!(
target: LOG_TARGET,
?bridge,
?bridge_id,
?lane_id,
"OpenBridgeForLane - already exist!"
);
if &bridge_id != locations.bridge_id() {
tracing::warn!(
target: LOG_TARGET,
?bridge,
?bridge_id,
?lane_id,
?bridge_origin_relative_location,
?bridge_destination_universal_location,
"OpenBridgeForLane - check you parameters, because a different bridge exist for requested!"
);
}
return T::DbWeight::get().reads(2);
}
if let Err(e) = Pezpallet::<T, I>::do_open_bridge(locations, lane_id, create_lane) {
tracing::error!(target: LOG_TARGET, error=?e, "OpenBridgeForLane - do_open_bridge failed");
T::DbWeight::get().reads(6)
} else {
tracing::info!(target: LOG_TARGET, "OpenBridgeForLane - do_open_bridge passed!");
T::DbWeight::get().reads_writes(6, 4)
}
}
#[cfg(feature = "try-runtime")]
fn post_upgrade(_state: pezsp_std::vec::Vec<u8>) -> Result<(), pezsp_runtime::DispatchError> {
let bridge_origin_relative_location = SourceRelativeLocation::get();
let bridge_destination_universal_location = BridgedUniversalLocation::get();
let lane_id = Lane::get();
// check that requested bridge is stored
let Ok(locations) = Pezpallet::<T, I>::bridge_locations(
bridge_origin_relative_location.clone(),
bridge_destination_universal_location.clone(),
) else {
return Err(pezsp_runtime::DispatchError::Other("Invalid locations!"));
};
let Some((bridge_id, _)) = Pezpallet::<T, I>::bridge_by_lane_id(&lane_id) else {
return Err(pezsp_runtime::DispatchError::Other("Missing bridge!"));
};
pezframe_support::ensure!(
locations.bridge_id() == &bridge_id,
"Bridge is not stored correctly!"
);
tracing::info!(
target: LOG_TARGET,
?lane_id,
?bridge_origin_relative_location,
?bridge_destination_universal_location,
"OpenBridgeForLane - post_upgrade found opened bridge"
);
Ok(())
}
}
@@ -0,0 +1,669 @@
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
#![cfg(test)]
use crate as pezpallet_xcm_bridge_hub;
use bp_messages::{
target_chain::{DispatchMessage, MessageDispatch},
ChainWithMessages, HashedLaneId, MessageNonce,
};
use pezbp_runtime::{messages::MessageDispatchResult, Chain, ChainId, HashOf};
use bp_xcm_bridge_hub::{BridgeId, LocalXcmChannelManager};
use codec::{Decode, Encode};
use pezframe_support::{
assert_ok, derive_impl, parameter_types,
traits::{EnsureOrigin, Equals, Everything, Get, OriginTrait},
weights::RuntimeDbWeight,
};
use pezkuwi_teyrchain_primitives::primitives::Sibling;
use pezsp_core::H256;
use pezsp_runtime::{
testing::Header as BizinikiwiHeader,
traits::{BlakeTwo256, ConstU128, ConstU32, IdentityLookup},
AccountId32, BuildStorage, StateVersion,
};
use pezsp_std::cell::RefCell;
use xcm::{latest::PEZKUWICHAIN_GENESIS_HASH, prelude::*};
use xcm_builder::{
AllowUnpaidExecutionFrom, DispatchBlob, DispatchBlobError, FixedWeightBounds,
InspectMessageQueues, NetworkExportTable, NetworkExportTableItem, ParentIsPreset,
SiblingTeyrchainConvertsVia,
};
use xcm_executor::{traits::ConvertOrigin, XcmExecutor};
pub type AccountId = AccountId32;
pub type Balance = u64;
type Block = pezframe_system::mocking::MockBlock<TestRuntime>;
/// Lane identifier type used for tests.
pub type TestLaneIdType = HashedLaneId;
pub const SIBLING_ASSET_HUB_ID: u32 = 2001;
pub const THIS_BRIDGE_HUB_ID: u32 = 2002;
pub const BRIDGED_ASSET_HUB_ID: u32 = 1001;
pezframe_support::construct_runtime! {
pub enum TestRuntime {
System: pezframe_system::{Pezpallet, Call, Config<T>, Storage, Event<T>},
Balances: pezpallet_balances::{Pezpallet, Event<T>},
Messages: pezpallet_bridge_messages::{Pezpallet, Call, Event<T>},
XcmOverBridge: pezpallet_xcm_bridge_hub::{Pezpallet, Call, HoldReason, Event<T>},
XcmOverBridgeWrappedWithExportMessageRouter: pezpallet_xcm_bridge_hub_router = 57,
}
}
parameter_types! {
pub const DbWeight: RuntimeDbWeight = RuntimeDbWeight { read: 1, write: 2 };
pub const ExistentialDeposit: Balance = 1;
}
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
impl pezframe_system::Config for TestRuntime {
type AccountId = AccountId;
type AccountData = pezpallet_balances::AccountData<Balance>;
type Block = Block;
type Lookup = IdentityLookup<Self::AccountId>;
}
#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
impl pezpallet_balances::Config for TestRuntime {
type AccountStore = System;
}
impl pezpallet_bridge_messages::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = TestMessagesWeights;
type ThisChain = ThisUnderlyingChain;
type BridgedChain = BridgedUnderlyingChain;
type BridgedHeaderChain = BridgedHeaderChain;
type OutboundPayload = Vec<u8>;
type InboundPayload = Vec<u8>;
type LaneId = TestLaneIdType;
type DeliveryPayments = ();
type DeliveryConfirmationPayments = ();
type OnMessagesDelivered = ();
type MessageDispatch = TestMessageDispatch;
}
pub struct TestMessagesWeights;
impl pezpallet_bridge_messages::WeightInfo for TestMessagesWeights {
fn receive_single_message_proof() -> Weight {
Weight::zero()
}
fn receive_n_messages_proof(_: u32) -> Weight {
Weight::zero()
}
fn receive_single_message_proof_with_outbound_lane_state() -> Weight {
Weight::zero()
}
fn receive_single_n_bytes_message_proof(_: u32) -> Weight {
Weight::zero()
}
fn receive_delivery_proof_for_single_message() -> Weight {
Weight::zero()
}
fn receive_delivery_proof_for_two_messages_by_single_relayer() -> Weight {
Weight::zero()
}
fn receive_delivery_proof_for_two_messages_by_two_relayers() -> Weight {
Weight::zero()
}
fn receive_single_n_bytes_message_proof_with_dispatch(_n: u32) -> Weight {
Weight::from_parts(1, 0)
}
}
impl pezpallet_bridge_messages::WeightInfoExt for TestMessagesWeights {
fn expected_extra_storage_proof_size() -> u32 {
0
}
fn receive_messages_proof_overhead_from_runtime() -> Weight {
Weight::zero()
}
fn receive_messages_delivery_proof_overhead_from_runtime() -> Weight {
Weight::zero()
}
}
parameter_types! {
pub const RelayNetwork: NetworkId = NetworkId::Kusama;
pub UniversalLocation: InteriorLocation = [
GlobalConsensus(RelayNetwork::get()),
Teyrchain(THIS_BRIDGE_HUB_ID),
].into();
pub SiblingLocation: Location = Location::new(1, [Teyrchain(SIBLING_ASSET_HUB_ID)]);
pub SiblingUniversalLocation: InteriorLocation = [GlobalConsensus(RelayNetwork::get()), Teyrchain(SIBLING_ASSET_HUB_ID)].into();
pub const BridgedRelayNetwork: NetworkId = NetworkId::ByGenesis([1; 32]);
pub BridgedRelayNetworkLocation: Location = (Parent, GlobalConsensus(BridgedRelayNetwork::get())).into();
pub BridgedRelativeDestination: InteriorLocation = [Teyrchain(BRIDGED_ASSET_HUB_ID)].into();
pub BridgedUniversalDestination: InteriorLocation = [GlobalConsensus(BridgedRelayNetwork::get()), Teyrchain(BRIDGED_ASSET_HUB_ID)].into();
pub const NonBridgedRelayNetwork: NetworkId = NetworkId::ByGenesis(PEZKUWICHAIN_GENESIS_HASH);
pub const BridgeDeposit: Balance = 100_000;
// configuration for pezpallet_xcm_bridge_hub_router
pub BridgeHubLocation: Location = Here.into();
pub BridgeFeeAsset: AssetId = Location::here().into();
pub BridgeTable: Vec<NetworkExportTableItem>
= vec![
NetworkExportTableItem::new(
BridgedRelayNetwork::get(),
None,
BridgeHubLocation::get(),
None
)
];
pub UnitWeightCost: Weight = Weight::from_parts(10, 10);
}
/// **Universal** `InteriorLocation` of bridged asset hub.
pub fn bridged_asset_hub_universal_location() -> InteriorLocation {
BridgedUniversalDestination::get()
}
impl pezpallet_xcm_bridge_hub::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type UniversalLocation = UniversalLocation;
type BridgedNetwork = BridgedRelayNetworkLocation;
type BridgeMessagesPalletInstance = ();
type MessageExportPrice = ();
type DestinationVersion = AlwaysLatest;
type ForceOrigin = pezframe_system::EnsureNever<()>;
type OpenBridgeOrigin = OpenBridgeOrigin;
type BridgeOriginAccountIdConverter = LocationToAccountId;
type BridgeDeposit = BridgeDeposit;
type Currency = Balances;
type RuntimeHoldReason = RuntimeHoldReason;
type AllowWithoutBridgeDeposit = Equals<ParentRelayChainLocation>;
type LocalXcmChannelManager = TestLocalXcmChannelManager;
type BlobDispatcher = TestBlobDispatcher;
}
/// A router instance simulates a scenario where the router is deployed on a different chain than
/// the `MessageExporter`. This means that the router sends an `ExportMessage`.
pub type XcmOverBridgeWrappedWithExportMessageRouterInstance = ();
impl pezpallet_xcm_bridge_hub_router::Config<XcmOverBridgeWrappedWithExportMessageRouterInstance>
for TestRuntime
{
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
type UniversalLocation = ExportMessageOriginUniversalLocation;
type SiblingBridgeHubLocation = BridgeHubLocation;
type BridgedNetworkId = BridgedRelayNetwork;
type Bridges = NetworkExportTable<BridgeTable>;
type DestinationVersion = AlwaysLatest;
// We convert to root `here` location with `BridgeHubLocationXcmOriginAsRoot`
type BridgeHubOrigin = pezframe_system::EnsureRoot<AccountId>;
// **Note**: The crucial part is that `ExportMessage` is processed by `XcmExecutor`, which
// calls the `ExportXcm` implementation of `pezpallet_xcm_bridge_hub` as the
// `MessageExporter`.
type ToBridgeHubSender = ExecuteXcmOverSendXcm;
type LocalXcmChannelManager = TestLocalXcmChannelManager;
type ByteFee = ConstU128<0>;
type FeeAsset = BridgeFeeAsset;
}
pub struct XcmConfig;
impl xcm_executor::Config for XcmConfig {
type RuntimeCall = RuntimeCall;
type XcmSender = ();
type XcmEventEmitter = ();
type AssetTransactor = ();
type OriginConverter = BridgeHubLocationXcmOriginAsRoot<RuntimeOrigin>;
type IsReserve = ();
type IsTeleporter = ();
type UniversalLocation = UniversalLocation;
type Barrier = AllowUnpaidExecutionFrom<Everything>;
type Weigher = FixedWeightBounds<UnitWeightCost, RuntimeCall, ConstU32<100>>;
type Trader = ();
type ResponseHandler = ();
type AssetTrap = ();
type AssetClaims = ();
type SubscriptionService = ();
type PalletInstancesInfo = ();
type MaxAssetsIntoHolding = ();
type AssetLocker = ();
type AssetExchanger = ();
type FeeManager = ();
// We just set `MessageExporter` as our `pezpallet_xcm_bridge_hub` instance.
type MessageExporter = (XcmOverBridge,);
type UniversalAliases = ();
type CallDispatcher = RuntimeCall;
type SafeCallFilter = Everything;
type Aliasers = ();
type TransactionalProcessor = ();
type HrmpNewChannelOpenRequestHandler = ();
type HrmpChannelAcceptedHandler = ();
type HrmpChannelClosingHandler = ();
type XcmRecorder = ();
}
thread_local! {
pub static EXECUTE_XCM_ORIGIN: RefCell<Option<Location>> = RefCell::new(None);
}
/// The `SendXcm` implementation directly executes XCM using `XcmExecutor`.
///
/// We ensure that the `ExportMessage` produced by `pezpallet_xcm_bridge_hub_router` is compatible with
/// the `ExportXcm` implementation of `pezpallet_xcm_bridge_hub`.
///
/// Note: The crucial part is that `ExportMessage` is processed by `XcmExecutor`, which calls the
/// `ExportXcm` implementation of `pezpallet_xcm_bridge_hub` as `MessageExporter`.
pub struct ExecuteXcmOverSendXcm;
impl SendXcm for ExecuteXcmOverSendXcm {
type Ticket = Xcm<()>;
fn validate(
_: &mut Option<Location>,
message: &mut Option<Xcm<()>>,
) -> SendResult<Self::Ticket> {
Ok((message.take().unwrap(), Assets::new()))
}
fn deliver(ticket: Self::Ticket) -> Result<XcmHash, SendError> {
let xcm: Xcm<RuntimeCall> = ticket.into();
let origin = EXECUTE_XCM_ORIGIN.with(|o| o.borrow().clone().unwrap());
let mut hash = xcm.using_encoded(pezsp_io::hashing::blake2_256);
let outcome = XcmExecutor::<XcmConfig>::prepare_and_execute(
origin,
xcm,
&mut hash,
Weight::MAX,
Weight::zero(),
);
assert_ok!(outcome.ensure_complete());
Ok(hash)
}
}
impl InspectMessageQueues for ExecuteXcmOverSendXcm {
fn clear_messages() {
todo!()
}
fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
todo!()
}
}
impl ExecuteXcmOverSendXcm {
pub fn set_origin_for_execute(origin: Location) {
EXECUTE_XCM_ORIGIN.with(|o| *o.borrow_mut() = Some(origin));
}
}
/// A dynamic way to set different universal location for the origin which sends `ExportMessage`.
pub struct ExportMessageOriginUniversalLocation;
impl ExportMessageOriginUniversalLocation {
pub(crate) fn set(universal_location: Option<InteriorLocation>) {
EXPORT_MESSAGE_ORIGIN_UNIVERSAL_LOCATION.with(|o| *o.borrow_mut() = universal_location);
}
}
impl Get<InteriorLocation> for ExportMessageOriginUniversalLocation {
fn get() -> InteriorLocation {
EXPORT_MESSAGE_ORIGIN_UNIVERSAL_LOCATION.with(|o| {
o.borrow()
.clone()
.expect("`EXPORT_MESSAGE_ORIGIN_UNIVERSAL_LOCATION` is not set!")
})
}
}
thread_local! {
pub static EXPORT_MESSAGE_ORIGIN_UNIVERSAL_LOCATION: RefCell<Option<InteriorLocation>> = RefCell::new(None);
}
pub struct BridgeHubLocationXcmOriginAsRoot<RuntimeOrigin>(
pezsp_std::marker::PhantomData<RuntimeOrigin>,
);
impl<RuntimeOrigin: OriginTrait> ConvertOrigin<RuntimeOrigin>
for BridgeHubLocationXcmOriginAsRoot<RuntimeOrigin>
{
fn convert_origin(
origin: impl Into<Location>,
kind: OriginKind,
) -> Result<RuntimeOrigin, Location> {
let origin = origin.into();
if kind == OriginKind::Xcm && origin.eq(&BridgeHubLocation::get()) {
Ok(RuntimeOrigin::root())
} else {
Err(origin)
}
}
}
/// Type for specifying how a `Location` can be converted into an `AccountId`. This is used
/// when determining ownership of accounts for asset transacting and when attempting to use XCM
/// `Transact` in order to determine the dispatch Origin.
pub type LocationToAccountId = (
// The parent (Relay-chain) origin converts to the parent `AccountId`.
ParentIsPreset<AccountId>,
// Sibling teyrchain origins convert to AccountId via the `ParaId::into`.
SiblingTeyrchainConvertsVia<Sibling, AccountId>,
);
parameter_types! {
pub ParentRelayChainLocation: Location = Location { parents: 1, interior: Here };
}
pub struct OpenBridgeOrigin;
impl OpenBridgeOrigin {
pub fn parent_relay_chain_origin() -> RuntimeOrigin {
RuntimeOrigin::signed([0u8; 32].into())
}
pub fn parent_relay_chain_universal_origin() -> RuntimeOrigin {
RuntimeOrigin::signed([1u8; 32].into())
}
pub fn sibling_teyrchain_origin() -> RuntimeOrigin {
let mut account = [0u8; 32];
account[..4].copy_from_slice(&SIBLING_ASSET_HUB_ID.encode()[..4]);
RuntimeOrigin::signed(account.into())
}
pub fn sibling_teyrchain_universal_origin() -> RuntimeOrigin {
RuntimeOrigin::signed([2u8; 32].into())
}
pub fn origin_without_sovereign_account() -> RuntimeOrigin {
RuntimeOrigin::signed([3u8; 32].into())
}
pub fn disallowed_origin() -> RuntimeOrigin {
RuntimeOrigin::signed([42u8; 32].into())
}
}
impl EnsureOrigin<RuntimeOrigin> for OpenBridgeOrigin {
type Success = Location;
fn try_origin(o: RuntimeOrigin) -> Result<Self::Success, RuntimeOrigin> {
let signer = o.clone().into_signer();
if signer == Self::parent_relay_chain_origin().into_signer() {
return Ok(ParentRelayChainLocation::get());
} else if signer == Self::parent_relay_chain_universal_origin().into_signer() {
return Ok(Location {
parents: 2,
interior: GlobalConsensus(RelayNetwork::get()).into(),
});
} else if signer == Self::sibling_teyrchain_universal_origin().into_signer() {
return Ok(Location {
parents: 2,
interior: [GlobalConsensus(RelayNetwork::get()), Teyrchain(SIBLING_ASSET_HUB_ID)]
.into(),
});
} else if signer == Self::origin_without_sovereign_account().into_signer() {
return Ok(Location {
parents: 1,
interior: [Teyrchain(SIBLING_ASSET_HUB_ID), OnlyChild].into(),
});
}
let mut sibling_account = [0u8; 32];
sibling_account[..4].copy_from_slice(&SIBLING_ASSET_HUB_ID.encode()[..4]);
if signer == Some(sibling_account.into()) {
return Ok(Location { parents: 1, interior: Teyrchain(SIBLING_ASSET_HUB_ID).into() });
}
Err(o)
}
#[cfg(feature = "runtime-benchmarks")]
fn try_successful_origin() -> Result<RuntimeOrigin, ()> {
Ok(Self::parent_relay_chain_origin())
}
}
pub(crate) type OpenBridgeOriginOf<T, I> =
<T as pezpallet_xcm_bridge_hub::Config<I>>::OpenBridgeOrigin;
pub struct TestLocalXcmChannelManager;
impl TestLocalXcmChannelManager {
pub fn make_congested() {
pezframe_support::storage::unhashed::put(b"TestLocalXcmChannelManager.Congested", &true);
}
fn suspended_key(bridge: &BridgeId) -> Vec<u8> {
[b"TestLocalXcmChannelManager.Suspended", bridge.encode().as_slice()].concat()
}
fn resumed_key(bridge: &BridgeId) -> Vec<u8> {
[b"TestLocalXcmChannelManager.Resumed", bridge.encode().as_slice()].concat()
}
pub fn is_bridge_suspended(bridge: &BridgeId) -> bool {
pezframe_support::storage::unhashed::get_or_default(&Self::suspended_key(bridge))
}
pub fn is_bridge_resumed(bridge: &BridgeId) -> bool {
pezframe_support::storage::unhashed::get_or_default(&Self::resumed_key(bridge))
}
fn build_congestion_message(bridge: &BridgeId, is_congested: bool) -> Vec<Instruction<()>> {
use bp_xcm_bridge_hub_router::XcmBridgeHubRouterCall;
#[allow(clippy::large_enum_variant)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, scale_info::TypeInfo)]
enum Call {
#[codec(index = 57)]
XcmOverBridgeWrappedWithExportMessageRouter(XcmBridgeHubRouterCall),
}
pezsp_std::vec![
UnpaidExecution { weight_limit: Unlimited, check_origin: None },
Transact {
origin_kind: OriginKind::Xcm,
fallback_max_weight: None,
call: Call::XcmOverBridgeWrappedWithExportMessageRouter(
XcmBridgeHubRouterCall::report_bridge_status {
bridge_id: bridge.inner(),
is_congested,
}
)
.encode()
.into(),
},
ExpectTransactStatus(MaybeErrorCode::Success),
]
}
fn report_bridge_status(
local_origin: &Location,
bridge: &BridgeId,
is_congested: bool,
key: Vec<u8>,
) -> Result<(), SendError> {
// send as BridgeHub would send to sibling chain
ExecuteXcmOverSendXcm::set_origin_for_execute(BridgeHubLocation::get());
let result = send_xcm::<ExecuteXcmOverSendXcm>(
local_origin.clone(),
Self::build_congestion_message(&bridge, is_congested).into(),
);
if result.is_ok() {
pezframe_support::storage::unhashed::put(&key, &true);
}
result.map(|_| ())
}
}
impl LocalXcmChannelManager for TestLocalXcmChannelManager {
type Error = SendError;
fn is_congested(_with: &Location) -> bool {
pezframe_support::storage::unhashed::get_or_default(b"TestLocalXcmChannelManager.Congested")
}
fn suspend_bridge(local_origin: &Location, bridge: BridgeId) -> Result<(), Self::Error> {
Self::report_bridge_status(local_origin, &bridge, true, Self::suspended_key(&bridge))
}
fn resume_bridge(local_origin: &Location, bridge: BridgeId) -> Result<(), Self::Error> {
Self::report_bridge_status(local_origin, &bridge, false, Self::resumed_key(&bridge))
}
}
impl pezpallet_xcm_bridge_hub_router::XcmChannelStatusProvider for TestLocalXcmChannelManager {
fn is_congested(with: &Location) -> bool {
<Self as LocalXcmChannelManager>::is_congested(with)
}
}
pub struct TestBlobDispatcher;
impl TestBlobDispatcher {
pub fn is_dispatched() -> bool {
pezframe_support::storage::unhashed::get_or_default(b"TestBlobDispatcher.Dispatched")
}
}
impl DispatchBlob for TestBlobDispatcher {
fn dispatch_blob(_blob: Vec<u8>) -> Result<(), DispatchBlobError> {
pezframe_support::storage::unhashed::put(b"TestBlobDispatcher.Dispatched", &true);
Ok(())
}
}
pub struct ThisUnderlyingChain;
impl Chain for ThisUnderlyingChain {
const ID: ChainId = *b"tuch";
type BlockNumber = u64;
type Hash = H256;
type Hasher = BlakeTwo256;
type Header = BizinikiwiHeader;
type AccountId = AccountId;
type Balance = Balance;
type Nonce = u64;
type Signature = pezsp_runtime::MultiSignature;
const STATE_VERSION: StateVersion = StateVersion::V1;
fn max_extrinsic_size() -> u32 {
u32::MAX
}
fn max_extrinsic_weight() -> Weight {
Weight::MAX
}
}
impl ChainWithMessages for ThisUnderlyingChain {
const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str = "WithThisChainBridgeMessages";
const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 16;
const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 128;
}
pub type BridgedHeaderHash = H256;
pub type BridgedChainHeader = BizinikiwiHeader;
pub struct BridgedUnderlyingChain;
impl Chain for BridgedUnderlyingChain {
const ID: ChainId = *b"bgdc";
type BlockNumber = u64;
type Hash = BridgedHeaderHash;
type Hasher = BlakeTwo256;
type Header = BridgedChainHeader;
type AccountId = AccountId;
type Balance = Balance;
type Nonce = u64;
type Signature = pezsp_runtime::MultiSignature;
const STATE_VERSION: StateVersion = StateVersion::V1;
fn max_extrinsic_size() -> u32 {
4096
}
fn max_extrinsic_weight() -> Weight {
Weight::MAX
}
}
impl ChainWithMessages for BridgedUnderlyingChain {
const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str = "WithBridgedChainBridgeMessages";
const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 16;
const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 128;
}
pub struct BridgedHeaderChain;
impl bp_header_pez_chain::HeaderChain<BridgedUnderlyingChain> for BridgedHeaderChain {
fn finalized_header_state_root(
_hash: HashOf<BridgedUnderlyingChain>,
) -> Option<HashOf<BridgedUnderlyingChain>> {
unreachable!()
}
}
/// Test message dispatcher.
pub struct TestMessageDispatch;
impl TestMessageDispatch {
pub fn deactivate(lane: TestLaneIdType) {
pezframe_support::storage::unhashed::put(&(b"inactive", lane).encode()[..], &false);
}
}
impl MessageDispatch for TestMessageDispatch {
type DispatchPayload = Vec<u8>;
type DispatchLevelResult = ();
type LaneId = TestLaneIdType;
fn is_active(lane: Self::LaneId) -> bool {
pezframe_support::storage::unhashed::take::<bool>(&(b"inactive", lane).encode()[..]) !=
Some(false)
}
fn dispatch_weight(
_message: &mut DispatchMessage<Self::DispatchPayload, Self::LaneId>,
) -> Weight {
Weight::zero()
}
fn dispatch(
_: DispatchMessage<Self::DispatchPayload, Self::LaneId>,
) -> MessageDispatchResult<Self::DispatchLevelResult> {
MessageDispatchResult { unspent_weight: Weight::zero(), dispatch_level_result: () }
}
}
/// Run pezpallet test.
pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
pezsp_io::TestExternalities::new(
pezframe_system::GenesisConfig::<TestRuntime>::default().build_storage().unwrap(),
)
.execute_with(test)
}