mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-18 03:41:02 +00:00
Squashed 'bridges/' content from commit 89a76998f
git-subtree-dir: bridges git-subtree-split: 89a76998f93c8219e9b1f785dcce73d4891e7068
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "pallet-bridge-currency-exchange"
|
||||
description = "A Substrate Runtime module that accepts 'lock funds' transactions from a peer chain and grants an equivalent amount to a the appropriate Substrate account."
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false }
|
||||
log = { version = "0.4.14", default-features = false }
|
||||
serde = { version = "1.0", optional = true }
|
||||
|
||||
# Bridge dependencies
|
||||
|
||||
bp-currency-exchange = { path = "../../primitives/currency-exchange", default-features = false }
|
||||
bp-header-chain = { path = "../../primitives/header-chain", default-features = false }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false, optional = true }
|
||||
frame-support = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
frame-system = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-std = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
sp-io = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"bp-currency-exchange/std",
|
||||
"bp-header-chain/std",
|
||||
"codec/std",
|
||||
"frame-benchmarking/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"log/std",
|
||||
"serde",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"frame-benchmarking",
|
||||
"sp-std",
|
||||
]
|
||||
@@ -0,0 +1,134 @@
|
||||
// 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/>.
|
||||
|
||||
//! Exchange module complexity is mostly determined by callbacks, defined by runtime.
|
||||
//! So we are giving runtime opportunity to prepare environment and construct proof
|
||||
//! before invoking module calls.
|
||||
|
||||
use super::{
|
||||
Call, Config as CurrencyExchangeConfig, InclusionProofVerifier, Instance, Pallet as CurrencyExchangePallet,
|
||||
};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
use frame_benchmarking::{account, benchmarks_instance};
|
||||
use frame_system::RawOrigin;
|
||||
|
||||
const SEED: u32 = 0;
|
||||
const WORST_TX_SIZE_FACTOR: u32 = 1000;
|
||||
const WORST_PROOF_SIZE_FACTOR: u32 = 1000;
|
||||
|
||||
/// Pallet we're benchmarking here.
|
||||
pub struct Pallet<T: Config<I>, I: Instance>(CurrencyExchangePallet<T, I>);
|
||||
|
||||
/// Proof benchmarking parameters.
|
||||
pub struct ProofParams<Recipient> {
|
||||
/// Funds recipient.
|
||||
pub recipient: Recipient,
|
||||
/// When true, recipient must exists before import.
|
||||
pub recipient_exists: bool,
|
||||
/// When 0, transaction should have minimal possible size. When this value has non-zero value n,
|
||||
/// transaction size should be (if possible) near to MIN_SIZE + n * SIZE_FACTOR.
|
||||
pub transaction_size_factor: u32,
|
||||
/// When 0, proof should have minimal possible size. When this value has non-zero value n,
|
||||
/// proof size should be (if possible) near to MIN_SIZE + n * SIZE_FACTOR.
|
||||
pub proof_size_factor: u32,
|
||||
}
|
||||
|
||||
/// Config that must be implemented by runtime.
|
||||
pub trait Config<I: Instance>: CurrencyExchangeConfig<I> {
|
||||
/// Prepare proof for importing exchange transaction.
|
||||
fn make_proof(
|
||||
proof_params: ProofParams<Self::AccountId>,
|
||||
) -> <<Self as CurrencyExchangeConfig<I>>::PeerBlockchain as InclusionProofVerifier>::TransactionInclusionProof;
|
||||
}
|
||||
|
||||
benchmarks_instance! {
|
||||
// Benchmark `import_peer_transaction` extrinsic with the best possible conditions:
|
||||
// * Proof is the transaction itself.
|
||||
// * Transaction has minimal size.
|
||||
// * Recipient account exists.
|
||||
import_peer_transaction_best_case {
|
||||
let i in 1..100;
|
||||
|
||||
let recipient: T::AccountId = account("recipient", i, SEED);
|
||||
let proof = T::make_proof(ProofParams {
|
||||
recipient: recipient.clone(),
|
||||
recipient_exists: true,
|
||||
transaction_size_factor: 0,
|
||||
proof_size_factor: 0,
|
||||
});
|
||||
}: import_peer_transaction(RawOrigin::Signed(recipient), proof)
|
||||
|
||||
// Benchmark `import_peer_transaction` extrinsic when recipient account does not exists.
|
||||
import_peer_transaction_when_recipient_does_not_exists {
|
||||
let i in 1..100;
|
||||
|
||||
let recipient: T::AccountId = account("recipient", i, SEED);
|
||||
let proof = T::make_proof(ProofParams {
|
||||
recipient: recipient.clone(),
|
||||
recipient_exists: false,
|
||||
transaction_size_factor: 0,
|
||||
proof_size_factor: 0,
|
||||
});
|
||||
}: import_peer_transaction(RawOrigin::Signed(recipient), proof)
|
||||
|
||||
// Benchmark `import_peer_transaction` when transaction size increases.
|
||||
import_peer_transaction_when_transaction_size_increases {
|
||||
let i in 1..100;
|
||||
let n in 1..WORST_TX_SIZE_FACTOR;
|
||||
|
||||
let recipient: T::AccountId = account("recipient", i, SEED);
|
||||
let proof = T::make_proof(ProofParams {
|
||||
recipient: recipient.clone(),
|
||||
recipient_exists: true,
|
||||
transaction_size_factor: n,
|
||||
proof_size_factor: 0,
|
||||
});
|
||||
}: import_peer_transaction(RawOrigin::Signed(recipient), proof)
|
||||
|
||||
// Benchmark `import_peer_transaction` when proof size increases.
|
||||
import_peer_transaction_when_proof_size_increases {
|
||||
let i in 1..100;
|
||||
let n in 1..WORST_PROOF_SIZE_FACTOR;
|
||||
|
||||
let recipient: T::AccountId = account("recipient", i, SEED);
|
||||
let proof = T::make_proof(ProofParams {
|
||||
recipient: recipient.clone(),
|
||||
recipient_exists: true,
|
||||
transaction_size_factor: 0,
|
||||
proof_size_factor: n,
|
||||
});
|
||||
}: import_peer_transaction(RawOrigin::Signed(recipient), proof)
|
||||
|
||||
// Benchmark `import_peer_transaction` extrinsic with the worst possible conditions:
|
||||
// * Proof is large.
|
||||
// * Transaction has large size.
|
||||
// * Recipient account does not exists.
|
||||
import_peer_transaction_worst_case {
|
||||
let i in 1..100;
|
||||
let m in WORST_TX_SIZE_FACTOR..WORST_TX_SIZE_FACTOR+1;
|
||||
let n in WORST_PROOF_SIZE_FACTOR..WORST_PROOF_SIZE_FACTOR+1;
|
||||
|
||||
let recipient: T::AccountId = account("recipient", i, SEED);
|
||||
let proof = T::make_proof(ProofParams {
|
||||
recipient: recipient.clone(),
|
||||
recipient_exists: false,
|
||||
transaction_size_factor: m,
|
||||
proof_size_factor: n,
|
||||
});
|
||||
}: import_peer_transaction(RawOrigin::Signed(recipient), proof)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
// 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/>.
|
||||
|
||||
//! Runtime module that allows tokens exchange between two bridged chains.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use bp_currency_exchange::{
|
||||
CurrencyConverter, DepositInto, Error as ExchangeError, MaybeLockFundsTransaction, RecipientsMap,
|
||||
};
|
||||
use bp_header_chain::InclusionProofVerifier;
|
||||
use frame_support::{decl_error, decl_module, decl_storage, ensure};
|
||||
use sp_runtime::DispatchResult;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub mod benchmarking;
|
||||
|
||||
/// Called when transaction is submitted to the exchange module.
|
||||
pub trait OnTransactionSubmitted<AccountId> {
|
||||
/// Called when valid transaction is submitted and accepted by the module.
|
||||
fn on_valid_transaction_submitted(submitter: AccountId);
|
||||
}
|
||||
|
||||
/// The module configuration trait
|
||||
pub trait Config<I = DefaultInstance>: frame_system::Config {
|
||||
/// Handler for transaction submission result.
|
||||
type OnTransactionSubmitted: OnTransactionSubmitted<Self::AccountId>;
|
||||
/// Represents the blockchain that we'll be exchanging currency with.
|
||||
type PeerBlockchain: InclusionProofVerifier;
|
||||
/// Peer blockchain transaction parser.
|
||||
type PeerMaybeLockFundsTransaction: MaybeLockFundsTransaction<
|
||||
Transaction = <Self::PeerBlockchain as InclusionProofVerifier>::Transaction,
|
||||
>;
|
||||
/// Map between blockchains recipients.
|
||||
type RecipientsMap: RecipientsMap<
|
||||
PeerRecipient = <Self::PeerMaybeLockFundsTransaction as MaybeLockFundsTransaction>::Recipient,
|
||||
Recipient = Self::AccountId,
|
||||
>;
|
||||
/// This blockchain currency amount type.
|
||||
type Amount;
|
||||
/// Converter from peer blockchain currency type into current blockchain currency type.
|
||||
type CurrencyConverter: CurrencyConverter<
|
||||
SourceAmount = <Self::PeerMaybeLockFundsTransaction as MaybeLockFundsTransaction>::Amount,
|
||||
TargetAmount = Self::Amount,
|
||||
>;
|
||||
/// Something that could grant money.
|
||||
type DepositInto: DepositInto<Recipient = Self::AccountId, Amount = Self::Amount>;
|
||||
}
|
||||
|
||||
decl_error! {
|
||||
pub enum Error for Pallet<T: Config<I>, I: Instance> {
|
||||
/// Invalid peer blockchain transaction provided.
|
||||
InvalidTransaction,
|
||||
/// Peer transaction has invalid amount.
|
||||
InvalidAmount,
|
||||
/// Peer transaction has invalid recipient.
|
||||
InvalidRecipient,
|
||||
/// Cannot map from peer recipient to this blockchain recipient.
|
||||
FailedToMapRecipients,
|
||||
/// Failed to convert from peer blockchain currency to this blockhain currency.
|
||||
FailedToConvertCurrency,
|
||||
/// Deposit has failed.
|
||||
DepositFailed,
|
||||
/// Deposit has partially failed (changes to recipient account were made).
|
||||
DepositPartiallyFailed,
|
||||
/// Transaction is not finalized.
|
||||
UnfinalizedTransaction,
|
||||
/// Transaction funds are already claimed.
|
||||
AlreadyClaimed,
|
||||
}
|
||||
}
|
||||
|
||||
decl_module! {
|
||||
pub struct Module<T: Config<I>, I: Instance = DefaultInstance> for enum Call where origin: T::Origin {
|
||||
/// Imports lock fund transaction of the peer blockchain.
|
||||
#[weight = 0] // TODO: update me (https://github.com/paritytech/parity-bridges-common/issues/78)
|
||||
pub fn import_peer_transaction(
|
||||
origin,
|
||||
proof: <<T as Config<I>>::PeerBlockchain as InclusionProofVerifier>::TransactionInclusionProof,
|
||||
) -> DispatchResult {
|
||||
let submitter = frame_system::ensure_signed(origin)?;
|
||||
|
||||
// verify and parse transaction proof
|
||||
let deposit = prepare_deposit_details::<T, I>(&proof)?;
|
||||
|
||||
// make sure to update the mapping if we deposit successfully to avoid double spending,
|
||||
// i.e. whenever `deposit_into` is successful we MUST update `Transfers`.
|
||||
{
|
||||
// if any changes were made to the storage, we can't just return error here, because
|
||||
// otherwise the same proof may be imported again
|
||||
let deposit_result = T::DepositInto::deposit_into(deposit.recipient, deposit.amount);
|
||||
match deposit_result {
|
||||
Ok(_) => (),
|
||||
Err(ExchangeError::DepositPartiallyFailed) => (),
|
||||
Err(error) => return Err(Error::<T, I>::from(error).into()),
|
||||
}
|
||||
Transfers::<T, I>::insert(&deposit.transfer_id, ())
|
||||
}
|
||||
|
||||
// reward submitter for providing valid message
|
||||
T::OnTransactionSubmitted::on_valid_transaction_submitted(submitter);
|
||||
|
||||
log::trace!(
|
||||
target: "runtime",
|
||||
"Completed currency exchange: {:?}",
|
||||
deposit.transfer_id,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decl_storage! {
|
||||
trait Store for Pallet<T: Config<I>, I: Instance = DefaultInstance> as Bridge {
|
||||
/// All transfers that have already been claimed.
|
||||
Transfers: map hasher(blake2_128_concat) <T::PeerMaybeLockFundsTransaction as MaybeLockFundsTransaction>::Id => ();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: Instance> Pallet<T, I> {
|
||||
/// Returns true if currency exchange module is able to import given transaction proof in
|
||||
/// its current state.
|
||||
pub fn filter_transaction_proof(
|
||||
proof: &<T::PeerBlockchain as InclusionProofVerifier>::TransactionInclusionProof,
|
||||
) -> bool {
|
||||
if let Err(err) = prepare_deposit_details::<T, I>(proof) {
|
||||
log::trace!(
|
||||
target: "runtime",
|
||||
"Can't accept exchange transaction: {:?}",
|
||||
err,
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: Instance> From<ExchangeError> for Error<T, I> {
|
||||
fn from(error: ExchangeError) -> Self {
|
||||
match error {
|
||||
ExchangeError::InvalidTransaction => Error::InvalidTransaction,
|
||||
ExchangeError::InvalidAmount => Error::InvalidAmount,
|
||||
ExchangeError::InvalidRecipient => Error::InvalidRecipient,
|
||||
ExchangeError::FailedToMapRecipients => Error::FailedToMapRecipients,
|
||||
ExchangeError::FailedToConvertCurrency => Error::FailedToConvertCurrency,
|
||||
ExchangeError::DepositFailed => Error::DepositFailed,
|
||||
ExchangeError::DepositPartiallyFailed => Error::DepositPartiallyFailed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<AccountId> OnTransactionSubmitted<AccountId> for () {
|
||||
fn on_valid_transaction_submitted(_: AccountId) {}
|
||||
}
|
||||
|
||||
/// Exchange deposit details.
|
||||
struct DepositDetails<T: Config<I>, I: Instance> {
|
||||
/// Transfer id.
|
||||
pub transfer_id: <T::PeerMaybeLockFundsTransaction as MaybeLockFundsTransaction>::Id,
|
||||
/// Transfer recipient.
|
||||
pub recipient: <T::RecipientsMap as RecipientsMap>::Recipient,
|
||||
/// Transfer amount.
|
||||
pub amount: <T::CurrencyConverter as CurrencyConverter>::TargetAmount,
|
||||
}
|
||||
|
||||
/// Verify and parse transaction proof, preparing everything required for importing
|
||||
/// this transaction proof.
|
||||
fn prepare_deposit_details<T: Config<I>, I: Instance>(
|
||||
proof: &<<T as Config<I>>::PeerBlockchain as InclusionProofVerifier>::TransactionInclusionProof,
|
||||
) -> Result<DepositDetails<T, I>, Error<T, I>> {
|
||||
// ensure that transaction is included in finalized block that we know of
|
||||
let transaction = <T as Config<I>>::PeerBlockchain::verify_transaction_inclusion_proof(proof)
|
||||
.ok_or(Error::<T, I>::UnfinalizedTransaction)?;
|
||||
|
||||
// parse transaction
|
||||
let transaction =
|
||||
<T as Config<I>>::PeerMaybeLockFundsTransaction::parse(&transaction).map_err(Error::<T, I>::from)?;
|
||||
let transfer_id = transaction.id;
|
||||
ensure!(
|
||||
!Transfers::<T, I>::contains_key(&transfer_id),
|
||||
Error::<T, I>::AlreadyClaimed
|
||||
);
|
||||
|
||||
// grant recipient
|
||||
let recipient = T::RecipientsMap::map(transaction.recipient).map_err(Error::<T, I>::from)?;
|
||||
let amount = T::CurrencyConverter::convert(transaction.amount).map_err(Error::<T, I>::from)?;
|
||||
|
||||
Ok(DepositDetails {
|
||||
transfer_id,
|
||||
recipient,
|
||||
amount,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// From construct_runtime macro
|
||||
#![allow(clippy::from_over_into)]
|
||||
|
||||
use super::*;
|
||||
use bp_currency_exchange::LockFundsTransaction;
|
||||
use frame_support::{assert_noop, assert_ok, construct_runtime, parameter_types, weights::Weight};
|
||||
use sp_core::H256;
|
||||
use sp_runtime::{
|
||||
testing::Header,
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
Perbill,
|
||||
};
|
||||
|
||||
type AccountId = u64;
|
||||
|
||||
const INVALID_TRANSACTION_ID: u64 = 100;
|
||||
const ALREADY_CLAIMED_TRANSACTION_ID: u64 = 101;
|
||||
const UNKNOWN_RECIPIENT_ID: u64 = 0;
|
||||
const INVALID_AMOUNT: u64 = 0;
|
||||
const MAX_DEPOSIT_AMOUNT: u64 = 1000;
|
||||
const SUBMITTER: u64 = 2000;
|
||||
|
||||
type RawTransaction = LockFundsTransaction<u64, u64, u64>;
|
||||
|
||||
pub struct DummyTransactionSubmissionHandler;
|
||||
|
||||
impl OnTransactionSubmitted<AccountId> for DummyTransactionSubmissionHandler {
|
||||
fn on_valid_transaction_submitted(submitter: AccountId) {
|
||||
Transfers::<TestRuntime, DefaultInstance>::insert(submitter, ());
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DummyBlockchain;
|
||||
|
||||
impl InclusionProofVerifier for DummyBlockchain {
|
||||
type Transaction = RawTransaction;
|
||||
type TransactionInclusionProof = (bool, RawTransaction);
|
||||
|
||||
fn verify_transaction_inclusion_proof(proof: &Self::TransactionInclusionProof) -> Option<RawTransaction> {
|
||||
if proof.0 {
|
||||
Some(proof.1.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DummyTransaction;
|
||||
|
||||
impl MaybeLockFundsTransaction for DummyTransaction {
|
||||
type Transaction = RawTransaction;
|
||||
type Id = u64;
|
||||
type Recipient = AccountId;
|
||||
type Amount = u64;
|
||||
|
||||
fn parse(tx: &Self::Transaction) -> bp_currency_exchange::Result<RawTransaction> {
|
||||
match tx.id {
|
||||
INVALID_TRANSACTION_ID => Err(ExchangeError::InvalidTransaction),
|
||||
_ => Ok(tx.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DummyRecipientsMap;
|
||||
|
||||
impl RecipientsMap for DummyRecipientsMap {
|
||||
type PeerRecipient = AccountId;
|
||||
type Recipient = AccountId;
|
||||
|
||||
fn map(peer_recipient: Self::PeerRecipient) -> bp_currency_exchange::Result<Self::Recipient> {
|
||||
match peer_recipient {
|
||||
UNKNOWN_RECIPIENT_ID => Err(ExchangeError::FailedToMapRecipients),
|
||||
_ => Ok(peer_recipient * 10),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DummyCurrencyConverter;
|
||||
|
||||
impl CurrencyConverter for DummyCurrencyConverter {
|
||||
type SourceAmount = u64;
|
||||
type TargetAmount = u64;
|
||||
|
||||
fn convert(amount: Self::SourceAmount) -> bp_currency_exchange::Result<Self::TargetAmount> {
|
||||
match amount {
|
||||
INVALID_AMOUNT => Err(ExchangeError::FailedToConvertCurrency),
|
||||
_ => Ok(amount * 10),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DummyDepositInto;
|
||||
|
||||
impl DepositInto for DummyDepositInto {
|
||||
type Recipient = AccountId;
|
||||
type Amount = u64;
|
||||
|
||||
fn deposit_into(_recipient: Self::Recipient, amount: Self::Amount) -> bp_currency_exchange::Result<()> {
|
||||
match amount {
|
||||
amount if amount < MAX_DEPOSIT_AMOUNT * 10 => Ok(()),
|
||||
amount if amount == MAX_DEPOSIT_AMOUNT * 10 => Err(ExchangeError::DepositPartiallyFailed),
|
||||
_ => Err(ExchangeError::DepositFailed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<TestRuntime>;
|
||||
type Block = frame_system::mocking::MockBlock<TestRuntime>;
|
||||
use crate as pallet_bridge_currency_exchange;
|
||||
|
||||
construct_runtime! {
|
||||
pub enum TestRuntime where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
|
||||
Exchange: pallet_bridge_currency_exchange::{Pallet},
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub const MaximumBlockWeight: Weight = 1024;
|
||||
pub const MaximumBlockLength: u32 = 2 * 1024;
|
||||
pub const AvailableBlockRatio: Perbill = Perbill::one();
|
||||
}
|
||||
|
||||
impl frame_system::Config for TestRuntime {
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type Call = Call;
|
||||
type BlockNumber = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type Event = ();
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = ();
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type BaseCallFilter = ();
|
||||
type SystemWeightInfo = ();
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
}
|
||||
|
||||
impl Config for TestRuntime {
|
||||
type OnTransactionSubmitted = DummyTransactionSubmissionHandler;
|
||||
type PeerBlockchain = DummyBlockchain;
|
||||
type PeerMaybeLockFundsTransaction = DummyTransaction;
|
||||
type RecipientsMap = DummyRecipientsMap;
|
||||
type Amount = u64;
|
||||
type CurrencyConverter = DummyCurrencyConverter;
|
||||
type DepositInto = DummyDepositInto;
|
||||
}
|
||||
|
||||
fn new_test_ext() -> sp_io::TestExternalities {
|
||||
let t = frame_system::GenesisConfig::default()
|
||||
.build_storage::<TestRuntime>()
|
||||
.unwrap();
|
||||
sp_io::TestExternalities::new(t)
|
||||
}
|
||||
|
||||
fn transaction(id: u64) -> RawTransaction {
|
||||
RawTransaction {
|
||||
id,
|
||||
recipient: 1,
|
||||
amount: 2,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfinalized_transaction_rejected() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(
|
||||
Exchange::import_peer_transaction(Origin::signed(SUBMITTER), (false, transaction(0))),
|
||||
Error::<TestRuntime, DefaultInstance>::UnfinalizedTransaction,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_transaction_rejected() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(
|
||||
Exchange::import_peer_transaction(
|
||||
Origin::signed(SUBMITTER),
|
||||
(true, transaction(INVALID_TRANSACTION_ID)),
|
||||
),
|
||||
Error::<TestRuntime, DefaultInstance>::InvalidTransaction,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claimed_transaction_rejected() {
|
||||
new_test_ext().execute_with(|| {
|
||||
<Exchange as crate::Store>::Transfers::insert(ALREADY_CLAIMED_TRANSACTION_ID, ());
|
||||
assert_noop!(
|
||||
Exchange::import_peer_transaction(
|
||||
Origin::signed(SUBMITTER),
|
||||
(true, transaction(ALREADY_CLAIMED_TRANSACTION_ID)),
|
||||
),
|
||||
Error::<TestRuntime, DefaultInstance>::AlreadyClaimed,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_with_unknown_recipient_rejected() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let mut transaction = transaction(0);
|
||||
transaction.recipient = UNKNOWN_RECIPIENT_ID;
|
||||
assert_noop!(
|
||||
Exchange::import_peer_transaction(Origin::signed(SUBMITTER), (true, transaction)),
|
||||
Error::<TestRuntime, DefaultInstance>::FailedToMapRecipients,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_with_invalid_amount_rejected() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let mut transaction = transaction(0);
|
||||
transaction.amount = INVALID_AMOUNT;
|
||||
assert_noop!(
|
||||
Exchange::import_peer_transaction(Origin::signed(SUBMITTER), (true, transaction)),
|
||||
Error::<TestRuntime, DefaultInstance>::FailedToConvertCurrency,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_with_invalid_deposit_rejected() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let mut transaction = transaction(0);
|
||||
transaction.amount = MAX_DEPOSIT_AMOUNT + 1;
|
||||
assert_noop!(
|
||||
Exchange::import_peer_transaction(Origin::signed(SUBMITTER), (true, transaction)),
|
||||
Error::<TestRuntime, DefaultInstance>::DepositFailed,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_transaction_accepted_even_if_deposit_partially_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let mut transaction = transaction(0);
|
||||
transaction.amount = MAX_DEPOSIT_AMOUNT;
|
||||
assert_ok!(Exchange::import_peer_transaction(
|
||||
Origin::signed(SUBMITTER),
|
||||
(true, transaction),
|
||||
),);
|
||||
|
||||
// ensure that the transfer has been marked as completed
|
||||
assert!(<Exchange as crate::Store>::Transfers::contains_key(0u64));
|
||||
// ensure that submitter has been rewarded
|
||||
assert!(<Exchange as crate::Store>::Transfers::contains_key(SUBMITTER));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_transaction_accepted() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(Exchange::import_peer_transaction(
|
||||
Origin::signed(SUBMITTER),
|
||||
(true, transaction(0)),
|
||||
),);
|
||||
|
||||
// ensure that the transfer has been marked as completed
|
||||
assert!(<Exchange as crate::Store>::Transfers::contains_key(0u64));
|
||||
// ensure that submitter has been rewarded
|
||||
assert!(<Exchange as crate::Store>::Transfers::contains_key(SUBMITTER));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "pallet-bridge-dispatch"
|
||||
description = "A Substrate Runtime module that dispatches a bridge message, treating it simply as encoded Call"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false }
|
||||
log = { version = "0.4.14", default-features = false }
|
||||
|
||||
# Bridge dependencies
|
||||
|
||||
bp-message-dispatch = { path = "../../primitives/message-dispatch", default-features = false }
|
||||
bp-runtime = { path = "../../primitives/runtime", default-features = false }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
frame-support = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
frame-system = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-std = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
sp-io = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
serde = "1.0"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"bp-message-dispatch/std",
|
||||
"bp-runtime/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"log/std",
|
||||
"sp-core/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
]
|
||||
@@ -0,0 +1,61 @@
|
||||
# Call Dispatch Module
|
||||
|
||||
The call dispatch module has a single internal (only callable by other runtime modules) entry point
|
||||
for dispatching encoded calls (`pallet_bridge_dispatch::Module::dispatch`). Every dispatch
|
||||
(successful or not) emits a corresponding module event. The module doesn't have any call-related
|
||||
requirements - they may come from the bridged chain over some message lane, or they may be crafted
|
||||
locally. But in this document we'll mostly talk about this module in the context of bridges.
|
||||
|
||||
Every message that is being dispatched has three main characteristics:
|
||||
- `bridge` is the 4-bytes identifier of the bridge where this message comes from. This may be the
|
||||
identifier of the bridged chain (like `b"rlto"` for messages coming from `Rialto`), or the
|
||||
identifier of the bridge itself (`b"rimi"` for `Rialto` <-> `Millau` bridge);
|
||||
- `id` is the unique id of the message within the given bridge. For messages coming from the
|
||||
[messages module](../messages/README.md), it may worth to use a tuple
|
||||
`(LaneId, MessageNonce)` to identify a message;
|
||||
- `message` is the `pallet_bridge_dispatch::MessagePayload` structure. The `call` field is set
|
||||
to the (potentially) encoded `Call` of this chain.
|
||||
|
||||
The easiest way to understand what is happening when a `Call` is being dispatched, is to look at the
|
||||
module events set:
|
||||
|
||||
- `MessageRejected` event is emitted if a message has been rejected even before it has reached the
|
||||
module. Dispatch then is called just to reflect the fact that message has been received, but we
|
||||
have failed to pre-process it (e.g. because we have failed to decode `MessagePayload` structure
|
||||
from the proof);
|
||||
- `MessageVersionSpecMismatch` event is emitted if current runtime specification version differs
|
||||
from the version that has been used to encode the `Call`. The message payload has the
|
||||
`spec_version`, that is filled by the message submitter. If this value differs from the current
|
||||
runtime version, dispatch mechanism rejects to dispatch the message. Without this check, we may
|
||||
decode the wrong `Call` for example if method arguments were changed;
|
||||
- `MessageCallDecodeFailed` event is emitted if we have failed to decode `Call` from the payload.
|
||||
This may happen if the submitter has provided incorrect value in the `call` field, or if source
|
||||
chain storage has been corrupted. The `Call` is decoded after `spec_version` check, so we'll never
|
||||
try to decode `Call` from other runtime version;
|
||||
- `MessageSignatureMismatch` event is emitted if submitter has chose to dispatch message using
|
||||
specified this chain account (`pallet_bridge_dispatch::CallOrigin::TargetAccount` origin),
|
||||
but he has failed to prove that he owns the private key for this account;
|
||||
- `MessageCallRejected` event is emitted if the module has been deployed with some call filter and
|
||||
this filter has rejected the `Call`. In your bridge you may choose to reject all messages except
|
||||
e.g. balance transfer calls;
|
||||
- `MessageWeightMismatch` event is emitted if the message submitter has specified invalid `Call`
|
||||
dispatch weight in the `weight` field of the message payload. The value of this field is compared
|
||||
to the pre-dispatch weight of the decoded `Call`. If it is less than the actual pre-dispatch
|
||||
weight, the dispatch is rejected. Keep in mind, that even if post-dispatch weight will be less
|
||||
than specified, the submitter still have to declare (and pay for) the maximal possible weight
|
||||
(that is the pre-dispatch weight);
|
||||
- `MessageDispatched` event is emitted if the message has passed all checks and we have actually
|
||||
dispatched it. The dispatch may still fail, though - that's why we are including the dispatch
|
||||
result in the event payload.
|
||||
|
||||
When we talk about module in context of bridges, these events are helping in following cases:
|
||||
|
||||
1. when the message submitter has access to the state of both chains and wants to monitor what has
|
||||
happened with his message. Then he could use the message id (that he gets from the
|
||||
[messages module events](../messages/README.md#General-Information)) to filter events of
|
||||
call dispatch module at the target chain and actually see what has happened with his message;
|
||||
|
||||
1. when the message submitter only has access to the source chain state (for example, when sender is
|
||||
the runtime module at the source chain). In this case, your bridge may have additional mechanism
|
||||
to deliver dispatch proofs (which are storage proof of module events) back to the source chain,
|
||||
thus allowing the submitter to see what has happened with his messages.
|
||||
@@ -0,0 +1,865 @@
|
||||
// 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/>.
|
||||
|
||||
//! Runtime module which takes care of dispatching messages received over the bridge.
|
||||
//!
|
||||
//! The messages are interpreted directly as runtime `Call`. We attempt to decode
|
||||
//! them and then dispatch as usual. To prevent compatibility issues, the Calls have
|
||||
//! to include a `spec_version`. This will be checked before dispatch. In the case of
|
||||
//! a succesful dispatch an event is emitted.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use bp_message_dispatch::{MessageDispatch, Weight};
|
||||
use bp_runtime::{derive_account_id, InstanceId, Size, SourceAccount};
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::{
|
||||
decl_event, decl_module, decl_storage,
|
||||
dispatch::{Dispatchable, Parameter},
|
||||
ensure,
|
||||
traits::{Filter, Get},
|
||||
weights::{extract_actual_weight, GetDispatchInfo},
|
||||
RuntimeDebug,
|
||||
};
|
||||
use frame_system::RawOrigin;
|
||||
use sp_runtime::{
|
||||
traits::{BadOrigin, Convert, IdentifyAccount, MaybeDisplay, MaybeSerializeDeserialize, Member, Verify},
|
||||
DispatchResult,
|
||||
};
|
||||
use sp_std::{fmt::Debug, marker::PhantomData, prelude::*};
|
||||
|
||||
/// Spec version type.
|
||||
pub type SpecVersion = u32;
|
||||
|
||||
// TODO [#895] move to primitives
|
||||
/// Origin of a Call when it is dispatched on the target chain.
|
||||
///
|
||||
/// The source chain can (and should) verify that the message can be dispatched on the target chain
|
||||
/// with a particular origin given the source chain's origin. This can be done with the
|
||||
/// `verify_message_origin()` function.
|
||||
#[derive(RuntimeDebug, Encode, Decode, Clone, PartialEq, Eq)]
|
||||
pub enum CallOrigin<SourceChainAccountId, TargetChainAccountPublic, TargetChainSignature> {
|
||||
/// Call is sent by the Root origin on the source chain. On the target chain it is dispatched
|
||||
/// from a derived account.
|
||||
///
|
||||
/// The derived account represents the source Root account on the target chain. This is useful
|
||||
/// if the target chain needs some way of knowing that a call came from a priviledged origin on
|
||||
/// the source chain (maybe to allow a configuration change for example).
|
||||
SourceRoot,
|
||||
|
||||
/// Call is sent by `SourceChainAccountId` on the source chain. On the target chain it is
|
||||
/// dispatched from an account controlled by a private key on the target chain.
|
||||
///
|
||||
/// The account can be identified by `TargetChainAccountPublic`. The proof that the
|
||||
/// `SourceChainAccountId` controls `TargetChainAccountPublic` is the `TargetChainSignature`
|
||||
/// over `(Call, SourceChainAccountId, TargetChainSpecVersion, SourceChainBridgeId).encode()`.
|
||||
///
|
||||
/// NOTE sending messages using this origin (or any other) does not have replay protection!
|
||||
/// The assumption is that both the source account and the target account is controlled by
|
||||
/// the same entity, so source-chain replay protection is sufficient.
|
||||
/// As a consequence, it's extremely important for the target chain user to never produce
|
||||
/// a signature with their target-private key on something that could be sent over the bridge,
|
||||
/// i.e. if the target user signs `(<some-source-account-id>, Call::Transfer(X, 5))`
|
||||
/// The owner of `some-source-account-id` can send that message multiple times, which would
|
||||
/// result with multiple transfer calls being dispatched on the target chain.
|
||||
/// So please, NEVER USE YOUR PRIVATE KEY TO SIGN SOMETHING YOU DON'T FULLY UNDERSTAND!
|
||||
TargetAccount(SourceChainAccountId, TargetChainAccountPublic, TargetChainSignature),
|
||||
|
||||
/// Call is sent by the `SourceChainAccountId` on the source chain. On the target chain it is
|
||||
/// dispatched from a derived account ID.
|
||||
///
|
||||
/// The account ID on the target chain is derived from the source account ID This is useful if
|
||||
/// you need a way to represent foreign accounts on this chain for call dispatch purposes.
|
||||
///
|
||||
/// Note that the derived account does not need to have a private key on the target chain. This
|
||||
/// origin can therefore represent proxies, pallets, etc. as well as "regular" accounts.
|
||||
SourceAccount(SourceChainAccountId),
|
||||
}
|
||||
|
||||
// TODO [#895] move to primitives
|
||||
/// Message payload type used by dispatch module.
|
||||
#[derive(RuntimeDebug, Encode, Decode, Clone, PartialEq, Eq)]
|
||||
pub struct MessagePayload<SourceChainAccountId, TargetChainAccountPublic, TargetChainSignature, Call> {
|
||||
/// Runtime specification version. We only dispatch messages that have the same
|
||||
/// runtime version. Otherwise we risk to misinterpret encoded calls.
|
||||
pub spec_version: SpecVersion,
|
||||
/// Weight of the call, declared by the message sender. If it is less than actual
|
||||
/// static weight, the call is not dispatched.
|
||||
pub weight: Weight,
|
||||
/// Call origin to be used during dispatch.
|
||||
pub origin: CallOrigin<SourceChainAccountId, TargetChainAccountPublic, TargetChainSignature>,
|
||||
/// The call itself.
|
||||
pub call: Call,
|
||||
}
|
||||
|
||||
impl<SourceChainAccountId, TargetChainAccountPublic, TargetChainSignature> Size
|
||||
for MessagePayload<SourceChainAccountId, TargetChainAccountPublic, TargetChainSignature, Vec<u8>>
|
||||
{
|
||||
fn size_hint(&self) -> u32 {
|
||||
self.call.len() as _
|
||||
}
|
||||
}
|
||||
|
||||
/// The module configuration trait.
|
||||
pub trait Config<I = DefaultInstance>: frame_system::Config {
|
||||
/// The overarching event type.
|
||||
type Event: From<Event<Self, I>> + Into<<Self as frame_system::Config>::Event>;
|
||||
/// Id of the message. Whenever message is passed to the dispatch module, it emits
|
||||
/// event with this id + dispatch result. Could be e.g. (LaneId, MessageNonce) if
|
||||
/// it comes from the messages module.
|
||||
type MessageId: Parameter;
|
||||
/// Type of account ID on source chain.
|
||||
type SourceChainAccountId: Parameter + Member + MaybeSerializeDeserialize + Debug + MaybeDisplay + Ord + Default;
|
||||
/// Type of account public key on target chain.
|
||||
type TargetChainAccountPublic: Parameter + IdentifyAccount<AccountId = Self::AccountId>;
|
||||
/// Type of signature that may prove that the message has been signed by
|
||||
/// owner of `TargetChainAccountPublic`.
|
||||
type TargetChainSignature: Parameter + Verify<Signer = Self::TargetChainAccountPublic>;
|
||||
/// The overarching dispatch call type.
|
||||
type Call: Parameter
|
||||
+ GetDispatchInfo
|
||||
+ Dispatchable<
|
||||
Origin = <Self as frame_system::Config>::Origin,
|
||||
PostInfo = frame_support::dispatch::PostDispatchInfo,
|
||||
>;
|
||||
/// Pre-dispatch filter for incoming calls.
|
||||
///
|
||||
/// The pallet will filter all incoming calls right before they're dispatched. If this filter
|
||||
/// rejects the call, special event (`Event::MessageCallRejected`) is emitted.
|
||||
type CallFilter: Filter<<Self as Config<I>>::Call>;
|
||||
/// The type that is used to wrap the `Self::Call` when it is moved over bridge.
|
||||
///
|
||||
/// The idea behind this is to avoid `Call` conversion/decoding until we'll be sure
|
||||
/// that all other stuff (like `spec_version`) is ok. If we would try to decode
|
||||
/// `Call` which has been encoded using previous `spec_version`, then we might end
|
||||
/// up with decoding error, instead of `MessageVersionSpecMismatch`.
|
||||
type EncodedCall: Decode + Encode + Into<Result<<Self as Config<I>>::Call, ()>>;
|
||||
/// A type which can be turned into an AccountId from a 256-bit hash.
|
||||
///
|
||||
/// Used when deriving target chain AccountIds from source chain AccountIds.
|
||||
type AccountIdConverter: sp_runtime::traits::Convert<sp_core::hash::H256, Self::AccountId>;
|
||||
}
|
||||
|
||||
decl_storage! {
|
||||
trait Store for Pallet<T: Config<I>, I: Instance = DefaultInstance> as Dispatch {}
|
||||
}
|
||||
|
||||
decl_event!(
|
||||
pub enum Event<T, I = DefaultInstance> where
|
||||
<T as Config<I>>::MessageId
|
||||
{
|
||||
/// Message has been rejected before reaching dispatch.
|
||||
MessageRejected(InstanceId, MessageId),
|
||||
/// Message has been rejected by dispatcher because of spec version mismatch.
|
||||
/// Last two arguments are: expected and passed spec version.
|
||||
MessageVersionSpecMismatch(InstanceId, MessageId, SpecVersion, SpecVersion),
|
||||
/// Message has been rejected by dispatcher because of weight mismatch.
|
||||
/// Last two arguments are: expected and passed call weight.
|
||||
MessageWeightMismatch(InstanceId, MessageId, Weight, Weight),
|
||||
/// Message signature mismatch.
|
||||
MessageSignatureMismatch(InstanceId, MessageId),
|
||||
/// Message has been dispatched with given result.
|
||||
MessageDispatched(InstanceId, MessageId, DispatchResult),
|
||||
/// We have failed to decode Call from the message.
|
||||
MessageCallDecodeFailed(InstanceId, MessageId),
|
||||
/// The call from the message has been rejected by the call filter.
|
||||
MessageCallRejected(InstanceId, MessageId),
|
||||
/// Phantom member, never used. Needed to handle multiple pallet instances.
|
||||
_Dummy(PhantomData<I>),
|
||||
}
|
||||
);
|
||||
|
||||
decl_module! {
|
||||
/// Call Dispatch FRAME Pallet.
|
||||
pub struct Module<T: Config<I>, I: Instance = DefaultInstance> for enum Call where origin: T::Origin {
|
||||
/// Deposit one of this module's events by using the default implementation.
|
||||
fn deposit_event() = default;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: Instance> MessageDispatch<T::MessageId> for Pallet<T, I> {
|
||||
type Message =
|
||||
MessagePayload<T::SourceChainAccountId, T::TargetChainAccountPublic, T::TargetChainSignature, T::EncodedCall>;
|
||||
|
||||
fn dispatch_weight(message: &Self::Message) -> Weight {
|
||||
message.weight
|
||||
}
|
||||
|
||||
fn dispatch(bridge: InstanceId, id: T::MessageId, message: Result<Self::Message, ()>) {
|
||||
// emit special even if message has been rejected by external component
|
||||
let message = match message {
|
||||
Ok(message) => message,
|
||||
Err(_) => {
|
||||
log::trace!(target: "runtime::bridge-dispatch", "Message {:?}/{:?}: rejected before actual dispatch", bridge, id);
|
||||
Self::deposit_event(RawEvent::MessageRejected(bridge, id));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// verify spec version
|
||||
// (we want it to be the same, because otherwise we may decode Call improperly)
|
||||
let expected_version = <T as frame_system::Config>::Version::get().spec_version;
|
||||
if message.spec_version != expected_version {
|
||||
log::trace!(
|
||||
"Message {:?}/{:?}: spec_version mismatch. Expected {:?}, got {:?}",
|
||||
bridge,
|
||||
id,
|
||||
expected_version,
|
||||
message.spec_version,
|
||||
);
|
||||
Self::deposit_event(RawEvent::MessageVersionSpecMismatch(
|
||||
bridge,
|
||||
id,
|
||||
expected_version,
|
||||
message.spec_version,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// now that we have spec version checked, let's decode the call
|
||||
let call = match message.call.into() {
|
||||
Ok(call) => call,
|
||||
Err(_) => {
|
||||
log::trace!(target: "runtime::bridge-dispatch", "Failed to decode Call from message {:?}/{:?}", bridge, id,);
|
||||
Self::deposit_event(RawEvent::MessageCallDecodeFailed(bridge, id));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// prepare dispatch origin
|
||||
let origin_account = match message.origin {
|
||||
CallOrigin::SourceRoot => {
|
||||
let hex_id = derive_account_id::<T::SourceChainAccountId>(bridge, SourceAccount::Root);
|
||||
let target_id = T::AccountIdConverter::convert(hex_id);
|
||||
log::trace!(target: "runtime::bridge-dispatch", "Root Account: {:?}", &target_id);
|
||||
target_id
|
||||
}
|
||||
CallOrigin::TargetAccount(source_account_id, target_public, target_signature) => {
|
||||
let digest = account_ownership_digest(&call, source_account_id, message.spec_version, bridge);
|
||||
|
||||
let target_account = target_public.into_account();
|
||||
if !target_signature.verify(&digest[..], &target_account) {
|
||||
log::trace!(
|
||||
target: "runtime::bridge-dispatch",
|
||||
"Message {:?}/{:?}: origin proof is invalid. Expected account: {:?} from signature: {:?}",
|
||||
bridge,
|
||||
id,
|
||||
target_account,
|
||||
target_signature,
|
||||
);
|
||||
Self::deposit_event(RawEvent::MessageSignatureMismatch(bridge, id));
|
||||
return;
|
||||
}
|
||||
|
||||
log::trace!(target: "runtime::bridge-dispatch", "Target Account: {:?}", &target_account);
|
||||
target_account
|
||||
}
|
||||
CallOrigin::SourceAccount(source_account_id) => {
|
||||
let hex_id = derive_account_id(bridge, SourceAccount::Account(source_account_id));
|
||||
let target_id = T::AccountIdConverter::convert(hex_id);
|
||||
log::trace!(target: "runtime::bridge-dispatch", "Source Account: {:?}", &target_id);
|
||||
target_id
|
||||
}
|
||||
};
|
||||
|
||||
// filter the call
|
||||
if !T::CallFilter::filter(&call) {
|
||||
log::trace!(
|
||||
target: "runtime::bridge-dispatch",
|
||||
"Message {:?}/{:?}: the call ({:?}) is rejected by filter",
|
||||
bridge,
|
||||
id,
|
||||
call,
|
||||
);
|
||||
Self::deposit_event(RawEvent::MessageCallRejected(bridge, id));
|
||||
return;
|
||||
}
|
||||
|
||||
// verify weight
|
||||
// (we want passed weight to be at least equal to pre-dispatch weight of the call
|
||||
// because otherwise Calls may be dispatched at lower price)
|
||||
let dispatch_info = call.get_dispatch_info();
|
||||
let expected_weight = dispatch_info.weight;
|
||||
if message.weight < expected_weight {
|
||||
log::trace!(
|
||||
target: "runtime::bridge-dispatch",
|
||||
"Message {:?}/{:?}: passed weight is too low. Expected at least {:?}, got {:?}",
|
||||
bridge,
|
||||
id,
|
||||
expected_weight,
|
||||
message.weight,
|
||||
);
|
||||
Self::deposit_event(RawEvent::MessageWeightMismatch(
|
||||
bridge,
|
||||
id,
|
||||
expected_weight,
|
||||
message.weight,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// finally dispatch message
|
||||
let origin = RawOrigin::Signed(origin_account).into();
|
||||
|
||||
log::trace!(target: "runtime::bridge-dispatch", "Message being dispatched is: {:?}", &call);
|
||||
let dispatch_result = call.dispatch(origin);
|
||||
let actual_call_weight = extract_actual_weight(&dispatch_result, &dispatch_info);
|
||||
|
||||
log::trace!(
|
||||
target: "runtime::bridge-dispatch",
|
||||
"Message {:?}/{:?} has been dispatched. Weight: {} of {}. Result: {:?}",
|
||||
bridge,
|
||||
id,
|
||||
actual_call_weight,
|
||||
message.weight,
|
||||
dispatch_result,
|
||||
);
|
||||
|
||||
Self::deposit_event(RawEvent::MessageDispatched(
|
||||
bridge,
|
||||
id,
|
||||
dispatch_result.map(drop).map_err(|e| e.error),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the message is allowed to be dispatched on the target chain given the sender's origin
|
||||
/// on the source chain.
|
||||
///
|
||||
/// For example, if a message is sent from a "regular" account on the source chain it will not be
|
||||
/// allowed to be dispatched as Root on the target chain. This is a useful check to do on the source
|
||||
/// chain _before_ sending a message whose dispatch will be rejected on the target chain.
|
||||
pub fn verify_message_origin<SourceChainAccountId, TargetChainAccountPublic, TargetChainSignature, Call>(
|
||||
sender_origin: &RawOrigin<SourceChainAccountId>,
|
||||
message: &MessagePayload<SourceChainAccountId, TargetChainAccountPublic, TargetChainSignature, Call>,
|
||||
) -> Result<Option<SourceChainAccountId>, BadOrigin>
|
||||
where
|
||||
SourceChainAccountId: PartialEq + Clone,
|
||||
{
|
||||
match message.origin {
|
||||
CallOrigin::SourceRoot => {
|
||||
ensure!(sender_origin == &RawOrigin::Root, BadOrigin);
|
||||
Ok(None)
|
||||
}
|
||||
CallOrigin::TargetAccount(ref source_account_id, _, _) => {
|
||||
ensure!(
|
||||
sender_origin == &RawOrigin::Signed(source_account_id.clone()),
|
||||
BadOrigin
|
||||
);
|
||||
Ok(Some(source_account_id.clone()))
|
||||
}
|
||||
CallOrigin::SourceAccount(ref source_account_id) => {
|
||||
ensure!(
|
||||
sender_origin == &RawOrigin::Signed(source_account_id.clone()),
|
||||
BadOrigin
|
||||
);
|
||||
Ok(Some(source_account_id.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Target account ownership digest from the source chain.
|
||||
///
|
||||
/// The byte vector returned by this function will be signed with a target chain account
|
||||
/// private key. This way, the owner of `source_account_id` on the source chain proves that
|
||||
/// the target chain account private key is also under his control.
|
||||
pub fn account_ownership_digest<Call, AccountId, SpecVersion, BridgeId>(
|
||||
call: &Call,
|
||||
source_account_id: AccountId,
|
||||
target_spec_version: SpecVersion,
|
||||
source_instance_id: BridgeId,
|
||||
) -> Vec<u8>
|
||||
where
|
||||
Call: Encode,
|
||||
AccountId: Encode,
|
||||
SpecVersion: Encode,
|
||||
BridgeId: Encode,
|
||||
{
|
||||
let mut proof = Vec::new();
|
||||
call.encode_to(&mut proof);
|
||||
source_account_id.encode_to(&mut proof);
|
||||
target_spec_version.encode_to(&mut proof);
|
||||
source_instance_id.encode_to(&mut proof);
|
||||
|
||||
proof
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// From construct_runtime macro
|
||||
#![allow(clippy::from_over_into)]
|
||||
|
||||
use super::*;
|
||||
use frame_support::{parameter_types, weights::Weight};
|
||||
use frame_system::{EventRecord, Phase};
|
||||
use sp_core::H256;
|
||||
use sp_runtime::{
|
||||
testing::Header,
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
Perbill,
|
||||
};
|
||||
|
||||
type AccountId = u64;
|
||||
type MessageId = [u8; 4];
|
||||
|
||||
#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq)]
|
||||
pub struct TestAccountPublic(AccountId);
|
||||
|
||||
impl IdentifyAccount for TestAccountPublic {
|
||||
type AccountId = AccountId;
|
||||
|
||||
fn into_account(self) -> AccountId {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq)]
|
||||
pub struct TestSignature(AccountId);
|
||||
|
||||
impl Verify for TestSignature {
|
||||
type Signer = TestAccountPublic;
|
||||
|
||||
fn verify<L: sp_runtime::traits::Lazy<[u8]>>(&self, _msg: L, signer: &AccountId) -> bool {
|
||||
self.0 == *signer
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AccountIdConverter;
|
||||
|
||||
impl sp_runtime::traits::Convert<H256, AccountId> for AccountIdConverter {
|
||||
fn convert(hash: H256) -> AccountId {
|
||||
hash.to_low_u64_ne()
|
||||
}
|
||||
}
|
||||
|
||||
type Block = frame_system::mocking::MockBlock<TestRuntime>;
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<TestRuntime>;
|
||||
|
||||
use crate as call_dispatch;
|
||||
|
||||
frame_support::construct_runtime! {
|
||||
pub enum TestRuntime where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
|
||||
Dispatch: call_dispatch::{Pallet, Call, Event<T>},
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub const MaximumBlockWeight: Weight = 1024;
|
||||
pub const MaximumBlockLength: u32 = 2 * 1024;
|
||||
pub const AvailableBlockRatio: Perbill = Perbill::one();
|
||||
}
|
||||
|
||||
impl frame_system::Config for TestRuntime {
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type Call = Call;
|
||||
type BlockNumber = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type Event = Event;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = ();
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type BaseCallFilter = ();
|
||||
type SystemWeightInfo = ();
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
}
|
||||
|
||||
impl Config for TestRuntime {
|
||||
type Event = Event;
|
||||
type MessageId = MessageId;
|
||||
type SourceChainAccountId = AccountId;
|
||||
type TargetChainAccountPublic = TestAccountPublic;
|
||||
type TargetChainSignature = TestSignature;
|
||||
type Call = Call;
|
||||
type CallFilter = TestCallFilter;
|
||||
type EncodedCall = EncodedCall;
|
||||
type AccountIdConverter = AccountIdConverter;
|
||||
}
|
||||
|
||||
#[derive(Decode, Encode)]
|
||||
pub struct EncodedCall(Vec<u8>);
|
||||
|
||||
impl From<EncodedCall> for Result<Call, ()> {
|
||||
fn from(call: EncodedCall) -> Result<Call, ()> {
|
||||
Call::decode(&mut &call.0[..]).map_err(drop)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestCallFilter;
|
||||
|
||||
impl Filter<Call> for TestCallFilter {
|
||||
fn filter(call: &Call) -> bool {
|
||||
!matches!(*call, Call::System(frame_system::Call::fill_block(_)))
|
||||
}
|
||||
}
|
||||
|
||||
const TEST_SPEC_VERSION: SpecVersion = 0;
|
||||
const TEST_WEIGHT: Weight = 1_000_000_000;
|
||||
|
||||
fn new_test_ext() -> sp_io::TestExternalities {
|
||||
let t = frame_system::GenesisConfig::default()
|
||||
.build_storage::<TestRuntime>()
|
||||
.unwrap();
|
||||
sp_io::TestExternalities::new(t)
|
||||
}
|
||||
|
||||
fn prepare_message(
|
||||
origin: CallOrigin<AccountId, TestAccountPublic, TestSignature>,
|
||||
call: Call,
|
||||
) -> <Pallet<TestRuntime> as MessageDispatch<<TestRuntime as Config>::MessageId>>::Message {
|
||||
MessagePayload {
|
||||
spec_version: TEST_SPEC_VERSION,
|
||||
weight: TEST_WEIGHT,
|
||||
origin,
|
||||
call: EncodedCall(call.encode()),
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_root_message(
|
||||
call: Call,
|
||||
) -> <Pallet<TestRuntime> as MessageDispatch<<TestRuntime as Config>::MessageId>>::Message {
|
||||
prepare_message(CallOrigin::SourceRoot, call)
|
||||
}
|
||||
|
||||
fn prepare_target_message(
|
||||
call: Call,
|
||||
) -> <Pallet<TestRuntime> as MessageDispatch<<TestRuntime as Config>::MessageId>>::Message {
|
||||
let origin = CallOrigin::TargetAccount(1, TestAccountPublic(1), TestSignature(1));
|
||||
prepare_message(origin, call)
|
||||
}
|
||||
|
||||
fn prepare_source_message(
|
||||
call: Call,
|
||||
) -> <Pallet<TestRuntime> as MessageDispatch<<TestRuntime as Config>::MessageId>>::Message {
|
||||
let origin = CallOrigin::SourceAccount(1);
|
||||
prepare_message(origin, call)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_fail_on_spec_version_mismatch() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let bridge = b"ethb".to_owned();
|
||||
let id = [0; 4];
|
||||
|
||||
const BAD_SPEC_VERSION: SpecVersion = 99;
|
||||
let mut message =
|
||||
prepare_root_message(Call::System(<frame_system::Call<TestRuntime>>::remark(vec![1, 2, 3])));
|
||||
message.spec_version = BAD_SPEC_VERSION;
|
||||
|
||||
System::set_block_number(1);
|
||||
Dispatch::dispatch(bridge, id, Ok(message));
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: Event::call_dispatch(call_dispatch::Event::<TestRuntime>::MessageVersionSpecMismatch(
|
||||
bridge,
|
||||
id,
|
||||
TEST_SPEC_VERSION,
|
||||
BAD_SPEC_VERSION
|
||||
)),
|
||||
topics: vec![],
|
||||
}],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_fail_on_weight_mismatch() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let bridge = b"ethb".to_owned();
|
||||
let id = [0; 4];
|
||||
let mut message =
|
||||
prepare_root_message(Call::System(<frame_system::Call<TestRuntime>>::remark(vec![1, 2, 3])));
|
||||
message.weight = 0;
|
||||
|
||||
System::set_block_number(1);
|
||||
Dispatch::dispatch(bridge, id, Ok(message));
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: Event::call_dispatch(call_dispatch::Event::<TestRuntime>::MessageWeightMismatch(
|
||||
bridge, id, 1345000, 0,
|
||||
)),
|
||||
topics: vec![],
|
||||
}],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_fail_on_signature_mismatch() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let bridge = b"ethb".to_owned();
|
||||
let id = [0; 4];
|
||||
|
||||
let call_origin = CallOrigin::TargetAccount(1, TestAccountPublic(1), TestSignature(99));
|
||||
let message = prepare_message(
|
||||
call_origin,
|
||||
Call::System(<frame_system::Call<TestRuntime>>::remark(vec![1, 2, 3])),
|
||||
);
|
||||
|
||||
System::set_block_number(1);
|
||||
Dispatch::dispatch(bridge, id, Ok(message));
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: Event::call_dispatch(call_dispatch::Event::<TestRuntime>::MessageSignatureMismatch(
|
||||
bridge, id
|
||||
)),
|
||||
topics: vec![],
|
||||
}],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_emit_event_for_rejected_messages() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let bridge = b"ethb".to_owned();
|
||||
let id = [0; 4];
|
||||
|
||||
System::set_block_number(1);
|
||||
Dispatch::dispatch(bridge, id, Err(()));
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: Event::call_dispatch(call_dispatch::Event::<TestRuntime>::MessageRejected(bridge, id)),
|
||||
topics: vec![],
|
||||
}],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_fail_on_call_decode() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let bridge = b"ethb".to_owned();
|
||||
let id = [0; 4];
|
||||
|
||||
let mut message =
|
||||
prepare_root_message(Call::System(<frame_system::Call<TestRuntime>>::remark(vec![1, 2, 3])));
|
||||
message.call.0 = vec![];
|
||||
|
||||
System::set_block_number(1);
|
||||
Dispatch::dispatch(bridge, id, Ok(message));
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: Event::call_dispatch(call_dispatch::Event::<TestRuntime>::MessageCallDecodeFailed(
|
||||
bridge, id
|
||||
)),
|
||||
topics: vec![],
|
||||
}],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_emit_event_for_rejected_calls() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let bridge = b"ethb".to_owned();
|
||||
let id = [0; 4];
|
||||
|
||||
let call = Call::System(<frame_system::Call<TestRuntime>>::fill_block(Perbill::from_percent(75)));
|
||||
let weight = call.get_dispatch_info().weight;
|
||||
let mut message = prepare_root_message(call);
|
||||
message.weight = weight;
|
||||
|
||||
System::set_block_number(1);
|
||||
Dispatch::dispatch(bridge, id, Ok(message));
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: Event::call_dispatch(call_dispatch::Event::<TestRuntime>::MessageCallRejected(bridge, id)),
|
||||
topics: vec![],
|
||||
}],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_dispatch_bridge_message_from_root_origin() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let bridge = b"ethb".to_owned();
|
||||
let id = [0; 4];
|
||||
let message = prepare_root_message(Call::System(<frame_system::Call<TestRuntime>>::remark(vec![1, 2, 3])));
|
||||
|
||||
System::set_block_number(1);
|
||||
Dispatch::dispatch(bridge, id, Ok(message));
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: Event::call_dispatch(call_dispatch::Event::<TestRuntime>::MessageDispatched(
|
||||
bridge,
|
||||
id,
|
||||
Ok(())
|
||||
)),
|
||||
topics: vec![],
|
||||
}],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_dispatch_bridge_message_from_target_origin() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let id = [0; 4];
|
||||
let bridge = b"ethb".to_owned();
|
||||
|
||||
let call = Call::System(<frame_system::Call<TestRuntime>>::remark(vec![]));
|
||||
let message = prepare_target_message(call);
|
||||
|
||||
System::set_block_number(1);
|
||||
Dispatch::dispatch(bridge, id, Ok(message));
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: Event::call_dispatch(call_dispatch::Event::<TestRuntime>::MessageDispatched(
|
||||
bridge,
|
||||
id,
|
||||
Ok(())
|
||||
)),
|
||||
topics: vec![],
|
||||
}],
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_dispatch_bridge_message_from_source_origin() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let id = [0; 4];
|
||||
let bridge = b"ethb".to_owned();
|
||||
|
||||
let call = Call::System(<frame_system::Call<TestRuntime>>::remark(vec![]));
|
||||
let message = prepare_source_message(call);
|
||||
|
||||
System::set_block_number(1);
|
||||
Dispatch::dispatch(bridge, id, Ok(message));
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: Event::call_dispatch(call_dispatch::Event::<TestRuntime>::MessageDispatched(
|
||||
bridge,
|
||||
id,
|
||||
Ok(())
|
||||
)),
|
||||
topics: vec![],
|
||||
}],
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn origin_is_checked_when_verifying_sending_message_using_source_root_account() {
|
||||
let call = Call::System(<frame_system::Call<TestRuntime>>::remark(vec![]));
|
||||
let message = prepare_root_message(call);
|
||||
|
||||
// When message is sent by Root, CallOrigin::SourceRoot is allowed
|
||||
assert!(matches!(verify_message_origin(&RawOrigin::Root, &message), Ok(None)));
|
||||
|
||||
// when message is sent by some real account, CallOrigin::SourceRoot is not allowed
|
||||
assert!(matches!(
|
||||
verify_message_origin(&RawOrigin::Signed(1), &message),
|
||||
Err(BadOrigin)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn origin_is_checked_when_verifying_sending_message_using_target_account() {
|
||||
let call = Call::System(<frame_system::Call<TestRuntime>>::remark(vec![]));
|
||||
let message = prepare_target_message(call);
|
||||
|
||||
// When message is sent by Root, CallOrigin::TargetAccount is not allowed
|
||||
assert!(matches!(
|
||||
verify_message_origin(&RawOrigin::Root, &message),
|
||||
Err(BadOrigin)
|
||||
));
|
||||
|
||||
// When message is sent by some other account, it is rejected
|
||||
assert!(matches!(
|
||||
verify_message_origin(&RawOrigin::Signed(2), &message),
|
||||
Err(BadOrigin)
|
||||
));
|
||||
|
||||
// When message is sent by a real account, it is allowed to have origin
|
||||
// CallOrigin::TargetAccount
|
||||
assert!(matches!(
|
||||
verify_message_origin(&RawOrigin::Signed(1), &message),
|
||||
Ok(Some(1))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn origin_is_checked_when_verifying_sending_message_using_source_account() {
|
||||
let call = Call::System(<frame_system::Call<TestRuntime>>::remark(vec![]));
|
||||
let message = prepare_source_message(call);
|
||||
|
||||
// Sending a message from the expected origin account works
|
||||
assert!(matches!(
|
||||
verify_message_origin(&RawOrigin::Signed(1), &message),
|
||||
Ok(Some(1))
|
||||
));
|
||||
|
||||
// If we send a message from a different account, it is rejected
|
||||
assert!(matches!(
|
||||
verify_message_origin(&RawOrigin::Signed(2), &message),
|
||||
Err(BadOrigin)
|
||||
));
|
||||
|
||||
// If we try and send the message from Root, it is also rejected
|
||||
assert!(matches!(
|
||||
verify_message_origin(&RawOrigin::Root, &message),
|
||||
Err(BadOrigin)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "ethereum-contract-builtin"
|
||||
description = "Small crate that helps Solidity contract to verify finality proof."
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0" }
|
||||
ethereum-types = "0.11.0"
|
||||
finality-grandpa = "0.14.0"
|
||||
hex = "0.4"
|
||||
log = "0.4.14"
|
||||
|
||||
# Runtime/chain specific dependencies
|
||||
|
||||
rialto-runtime = { path = "../../bin/rialto/runtime" }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
sc-finality-grandpa = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
sp-blockchain = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
sp-finality-grandpa = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
|
||||
[dev-dependencies]
|
||||
sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
@@ -0,0 +1,374 @@
|
||||
// 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 codec::{Decode, Encode};
|
||||
use ethereum_types::U256;
|
||||
use finality_grandpa::voter_set::VoterSet;
|
||||
use rialto_runtime::{Block, BlockNumber, Hash, Header as RuntimeHeader};
|
||||
use sp_blockchain::Error as ClientError;
|
||||
use sp_finality_grandpa::{AuthorityList, ConsensusLog, GRANDPA_ENGINE_ID};
|
||||
|
||||
/// Builtin errors.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Failed to decode block number.
|
||||
BlockNumberDecode,
|
||||
/// Failed to decode Substrate header.
|
||||
HeaderDecode(codec::Error),
|
||||
/// Failed to decode best voters set.
|
||||
BestSetDecode(codec::Error),
|
||||
/// Best voters set is invalid.
|
||||
InvalidBestSet,
|
||||
/// Failed to decode finality proof.
|
||||
FinalityProofDecode(codec::Error),
|
||||
/// Failed to verify justification.
|
||||
JustificationVerify(Box<ClientError>),
|
||||
}
|
||||
|
||||
/// Substrate header.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Header {
|
||||
/// Header hash.
|
||||
pub hash: Hash,
|
||||
/// Parent header hash.
|
||||
pub parent_hash: Hash,
|
||||
/// Header number.
|
||||
pub number: BlockNumber,
|
||||
/// GRANDPA validators change signal.
|
||||
pub signal: Option<ValidatorsSetSignal>,
|
||||
}
|
||||
|
||||
/// GRANDPA validators set change signal.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ValidatorsSetSignal {
|
||||
/// Signal delay.
|
||||
pub delay: BlockNumber,
|
||||
/// New validators set.
|
||||
pub validators: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Convert from U256 to BlockNumber. Fails if `U256` value isn't fitting within `BlockNumber`
|
||||
/// limits (the runtime referenced by this module uses u32 as `BlockNumber`).
|
||||
pub fn to_substrate_block_number(number: U256) -> Result<BlockNumber, Error> {
|
||||
let substrate_block_number = match number == number.low_u32().into() {
|
||||
true => Ok(number.low_u32()),
|
||||
false => Err(Error::BlockNumberDecode),
|
||||
};
|
||||
|
||||
log::trace!(
|
||||
target: "bridge-builtin",
|
||||
"Parsed Substrate block number from {}: {:?}",
|
||||
number,
|
||||
substrate_block_number,
|
||||
);
|
||||
|
||||
substrate_block_number
|
||||
}
|
||||
|
||||
/// Convert from BlockNumber to U256.
|
||||
pub fn from_substrate_block_number(number: BlockNumber) -> Result<U256, Error> {
|
||||
Ok(U256::from(number as u64))
|
||||
}
|
||||
|
||||
/// Parse Substrate header.
|
||||
pub fn parse_substrate_header(raw_header: &[u8]) -> Result<Header, Error> {
|
||||
let substrate_header = RuntimeHeader::decode(&mut &*raw_header)
|
||||
.map(|header| Header {
|
||||
hash: header.hash(),
|
||||
parent_hash: header.parent_hash,
|
||||
number: header.number,
|
||||
signal: sp_runtime::traits::Header::digest(&header)
|
||||
.log(|log| {
|
||||
log.as_consensus().and_then(|(engine_id, log)| {
|
||||
if engine_id == GRANDPA_ENGINE_ID {
|
||||
Some(log)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.and_then(|log| ConsensusLog::decode(&mut &*log).ok())
|
||||
.and_then(|log| match log {
|
||||
ConsensusLog::ScheduledChange(scheduled_change) => Some(ValidatorsSetSignal {
|
||||
delay: scheduled_change.delay,
|
||||
validators: scheduled_change.next_authorities.encode(),
|
||||
}),
|
||||
_ => None,
|
||||
}),
|
||||
})
|
||||
.map_err(Error::HeaderDecode);
|
||||
|
||||
log::debug!(
|
||||
target: "bridge-builtin",
|
||||
"Parsed Substrate header {}: {:?}",
|
||||
if substrate_header.is_ok() {
|
||||
format!("<{}-bytes-blob>", raw_header.len())
|
||||
} else {
|
||||
hex::encode(raw_header)
|
||||
},
|
||||
substrate_header,
|
||||
);
|
||||
|
||||
substrate_header
|
||||
}
|
||||
|
||||
/// Verify GRANDPA finality proof.
|
||||
pub fn verify_substrate_finality_proof(
|
||||
finality_target_number: BlockNumber,
|
||||
finality_target_hash: Hash,
|
||||
best_set_id: u64,
|
||||
raw_best_set: &[u8],
|
||||
raw_finality_proof: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
let best_set = AuthorityList::decode(&mut &*raw_best_set)
|
||||
.map_err(Error::BestSetDecode)
|
||||
.and_then(|authorities| VoterSet::new(authorities.into_iter()).ok_or(Error::InvalidBestSet));
|
||||
|
||||
log::debug!(
|
||||
target: "bridge-builtin",
|
||||
"Parsed Substrate authorities set {}: {:?}",
|
||||
if best_set.is_ok() {
|
||||
format!("<{}-bytes-blob>", raw_best_set.len())
|
||||
} else {
|
||||
hex::encode(raw_best_set)
|
||||
},
|
||||
best_set,
|
||||
);
|
||||
|
||||
let best_set = best_set?;
|
||||
|
||||
let verify_result = sc_finality_grandpa::GrandpaJustification::<Block>::decode_and_verify_finalizes(
|
||||
&raw_finality_proof,
|
||||
(finality_target_hash, finality_target_number),
|
||||
best_set_id,
|
||||
&best_set,
|
||||
)
|
||||
.map_err(Box::new)
|
||||
.map_err(Error::JustificationVerify)
|
||||
.map(|_| ());
|
||||
|
||||
log::debug!(
|
||||
target: "bridge-builtin",
|
||||
"Verified Substrate finality proof {}: {:?}",
|
||||
if verify_result.is_ok() {
|
||||
format!("<{}-bytes-blob>", raw_finality_proof.len())
|
||||
} else {
|
||||
hex::encode(raw_finality_proof)
|
||||
},
|
||||
verify_result,
|
||||
);
|
||||
|
||||
verify_result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rialto_runtime::DigestItem;
|
||||
use sp_core::crypto::Public;
|
||||
use sp_finality_grandpa::{AuthorityId, ScheduledChange};
|
||||
use sp_runtime::generic::Digest;
|
||||
|
||||
#[test]
|
||||
fn to_substrate_block_number_succeeds() {
|
||||
assert_eq!(to_substrate_block_number(U256::zero()).unwrap(), 0);
|
||||
assert_eq!(
|
||||
to_substrate_block_number(U256::from(std::u32::MAX as u64)).unwrap(),
|
||||
0xFFFFFFFF
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_substrate_block_number_fails() {
|
||||
assert!(matches!(
|
||||
to_substrate_block_number(U256::from(std::u32::MAX as u64 + 1)),
|
||||
Err(Error::BlockNumberDecode)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_substrate_block_number_succeeds() {
|
||||
assert_eq!(from_substrate_block_number(0).unwrap(), U256::zero());
|
||||
assert_eq!(
|
||||
from_substrate_block_number(std::u32::MAX).unwrap(),
|
||||
U256::from(std::u32::MAX)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn substrate_header_without_signal_parsed() {
|
||||
let raw_header = RuntimeHeader {
|
||||
parent_hash: [0u8; 32].into(),
|
||||
number: 0,
|
||||
state_root: "b2fc47904df5e355c6ab476d89fbc0733aeddbe302f0b94ba4eea9283f7e89e7"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
extrinsics_root: "03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
digest: Default::default(),
|
||||
}
|
||||
.encode();
|
||||
assert_eq!(
|
||||
raw_header,
|
||||
hex::decode("000000000000000000000000000000000000000000000000000000000000000000b2fc47904df5e355c6ab476d89fbc0733aeddbe302f0b94ba4eea9283f7e89e703170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c11131400").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_substrate_header(&raw_header).unwrap(),
|
||||
Header {
|
||||
hash: "afbbeb92bf6ff14f60bdef0aa89f043dd403659ae82665238810ace0d761f6d0"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
parent_hash: Default::default(),
|
||||
number: 0,
|
||||
signal: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn substrate_header_with_signal_parsed() {
|
||||
let authorities = vec![
|
||||
(AuthorityId::from_slice(&[1; 32]), 101),
|
||||
(AuthorityId::from_slice(&[3; 32]), 103),
|
||||
];
|
||||
let mut digest = Digest::default();
|
||||
digest.push(DigestItem::Consensus(
|
||||
GRANDPA_ENGINE_ID,
|
||||
ConsensusLog::ScheduledChange(ScheduledChange {
|
||||
next_authorities: authorities.clone(),
|
||||
delay: 8,
|
||||
})
|
||||
.encode(),
|
||||
));
|
||||
|
||||
let raw_header = RuntimeHeader {
|
||||
parent_hash: "c0ac300d4005141ea690f3df593e049739c227316eb7f05052f3ee077388b68b"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
number: 8,
|
||||
state_root: "822d6b412033aa9ac8e1722918eec5f25633529225754b3d4149982f5cacd4aa"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
extrinsics_root: "e7b07c0ce2799416ce7877b9cefc7f596bea5e8813bb2a0abf760414073ca928"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
digest,
|
||||
}
|
||||
.encode();
|
||||
assert_eq!(
|
||||
raw_header,
|
||||
hex::decode("c0ac300d4005141ea690f3df593e049739c227316eb7f05052f3ee077388b68b20822d6b412033aa9ac8e1722918eec5f25633529225754b3d4149982f5cacd4aae7b07c0ce2799416ce7877b9cefc7f596bea5e8813bb2a0abf760414073ca928040446524e4b59010108010101010101010101010101010101010101010101010101010101010101010165000000000000000303030303030303030303030303030303030303030303030303030303030303670000000000000008000000").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_substrate_header(&raw_header).unwrap(),
|
||||
Header {
|
||||
hash: "3dfebb280bd87a4640f89d7f2adecd62b88148747bff5b63af6e1634ee37a56e"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
parent_hash: "c0ac300d4005141ea690f3df593e049739c227316eb7f05052f3ee077388b68b"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
number: 8,
|
||||
signal: Some(ValidatorsSetSignal {
|
||||
delay: 8,
|
||||
validators: authorities.encode(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Number of the example block with justification.
|
||||
const EXAMPLE_JUSTIFIED_BLOCK_NUMBER: u32 = 8;
|
||||
/// Hash of the example block with justification.
|
||||
const EXAMPLE_JUSTIFIED_BLOCK_HASH: &str = "a2f45892db86b2ad133ce57d81b7e4375bb7035ce9883e6b68c358164f343775";
|
||||
/// Id of authorities set that have generated example justification. Could be computed by tracking
|
||||
/// every set change in canonized headers.
|
||||
const EXAMPLE_AUTHORITIES_SET_ID: u64 = 0;
|
||||
/// Encoded authorities set that has generated example justification. Could be fetched from `ScheduledChange`
|
||||
/// digest of the block that has scheduled this set OR by calling `GrandpaApi::grandpa_authorities()` at
|
||||
/// appropriate block.
|
||||
const EXAMPLE_AUTHORITIES_SET: &str = "1488dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0ee0100000000000000d17c2d7823ebf260fd138f2d7e27d114c0145d968b5ff5006125f2414fadae690100000000000000439660b36c6c03afafca027b910b4fecf99801834c62a5e6006f27d978de234f01000000000000005e639b43e0052c47447dac87d6fd2b6ec50bdd4d0f614e4299c665249bbd09d901000000000000001dfe3e22cc0d45c70779c1095f7489a8ef3cf52d62fbd8c2fa38c9f1723502b50100000000000000";
|
||||
/// Example justification. Could be fetched by calling 'chain_getBlock' RPC.
|
||||
const EXAMPLE_JUSTIFICATION: &str = "2600000000000000a2f45892db86b2ad133ce57d81b7e4375bb7035ce9883e6b68c358164f3437750800000010a2f45892db86b2ad133ce57d81b7e4375bb7035ce9883e6b68c358164f34377508000000d66b4ceb57ef8bcbc955071b597c8c5d2adcfdbb009c73f8438d342670fdeca9ac60686cbd58105b10f51d0a64a8e73b2e5829b2eab3248a008c472852130b00439660b36c6c03afafca027b910b4fecf99801834c62a5e6006f27d978de234fa2f45892db86b2ad133ce57d81b7e4375bb7035ce9883e6b68c358164f34377508000000f5730c14d3cd22b7661e2f5fcb3139dd5fef37f946314a441d01b40ce1200ef70d810525f23fd278b588cd67473c200bda83c338c407b479386aa83798e5970b5e639b43e0052c47447dac87d6fd2b6ec50bdd4d0f614e4299c665249bbd09d9a2f45892db86b2ad133ce57d81b7e4375bb7035ce9883e6b68c358164f34377508000000c78d6ec463f476461a695b4791d30e7626d16fdf72d7c252c2cad387495a97e8c2827ed4d5af853d6e05d31cb6fb7438c9481a7e9c6990d60a9bfaf6a6e1930988dc3417d5058ec4b4503e0c12ea1a0a89be200fe98922423d4334014fa6b0eea2f45892db86b2ad133ce57d81b7e4375bb7035ce9883e6b68c358164f3437750800000052b4fc52d430286b3e2d650aa6e01b6ff4fae8b968893a62be789209eb97ee6e23780d3f5af7042d85bb48f1b202890b22724dfebce138826f66a5e00324320fd17c2d7823ebf260fd138f2d7e27d114c0145d968b5ff5006125f2414fadae6900";
|
||||
|
||||
#[test]
|
||||
fn substrate_header_parse_fails() {
|
||||
assert!(matches!(parse_substrate_header(&[]), Err(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_substrate_finality_proof_succeeds() {
|
||||
verify_substrate_finality_proof(
|
||||
EXAMPLE_JUSTIFIED_BLOCK_NUMBER,
|
||||
EXAMPLE_JUSTIFIED_BLOCK_HASH.parse().unwrap(),
|
||||
EXAMPLE_AUTHORITIES_SET_ID,
|
||||
&hex::decode(EXAMPLE_AUTHORITIES_SET).unwrap(),
|
||||
&hex::decode(EXAMPLE_JUSTIFICATION).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_substrate_finality_proof_fails_when_wrong_block_is_finalized() {
|
||||
verify_substrate_finality_proof(
|
||||
4,
|
||||
Default::default(),
|
||||
EXAMPLE_AUTHORITIES_SET_ID,
|
||||
&hex::decode(EXAMPLE_AUTHORITIES_SET).unwrap(),
|
||||
&hex::decode(EXAMPLE_JUSTIFICATION).unwrap(),
|
||||
)
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_substrate_finality_proof_fails_when_wrong_set_is_provided() {
|
||||
verify_substrate_finality_proof(
|
||||
EXAMPLE_JUSTIFIED_BLOCK_NUMBER,
|
||||
EXAMPLE_JUSTIFIED_BLOCK_HASH.parse().unwrap(),
|
||||
EXAMPLE_AUTHORITIES_SET_ID,
|
||||
&hex::decode("deadbeef").unwrap(),
|
||||
&hex::decode(EXAMPLE_JUSTIFICATION).unwrap(),
|
||||
)
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_substrate_finality_proof_fails_when_wrong_set_id_is_provided() {
|
||||
verify_substrate_finality_proof(
|
||||
EXAMPLE_JUSTIFIED_BLOCK_NUMBER,
|
||||
EXAMPLE_JUSTIFIED_BLOCK_HASH.parse().unwrap(),
|
||||
42,
|
||||
&hex::decode(EXAMPLE_AUTHORITIES_SET).unwrap(),
|
||||
&hex::decode(EXAMPLE_JUSTIFICATION).unwrap(),
|
||||
)
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_substrate_finality_proof_fails_when_wrong_proof_is_provided() {
|
||||
verify_substrate_finality_proof(
|
||||
EXAMPLE_JUSTIFIED_BLOCK_NUMBER,
|
||||
EXAMPLE_JUSTIFIED_BLOCK_HASH.parse().unwrap(),
|
||||
0,
|
||||
&hex::decode(EXAMPLE_AUTHORITIES_SET).unwrap(),
|
||||
&hex::decode("deadbeef").unwrap(),
|
||||
)
|
||||
.unwrap_err();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
[package]
|
||||
name = "pallet-bridge-eth-poa"
|
||||
description = "A Substrate Runtime module that is able to verify PoA headers and their finality."
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false }
|
||||
libsecp256k1 = { version = "0.3.4", default-features = false, features = ["hmac"], optional = true }
|
||||
log = { version = "0.4.14", default-features = false }
|
||||
serde = { version = "1.0", optional = true }
|
||||
|
||||
# Bridge dependencies
|
||||
|
||||
bp-eth-poa = { path = "../../primitives/ethereum-poa", default-features = false }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false, optional = true }
|
||||
frame-support = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
frame-system = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-io = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-std = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
libsecp256k1 = { version = "0.3.4", features = ["hmac"] }
|
||||
hex-literal = "0.3"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"bp-eth-poa/std",
|
||||
"codec/std",
|
||||
"frame-benchmarking/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"log/std",
|
||||
"serde",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"frame-benchmarking",
|
||||
"libsecp256k1",
|
||||
]
|
||||
@@ -0,0 +1,270 @@
|
||||
// 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 super::*;
|
||||
|
||||
use crate::test_utils::{
|
||||
build_custom_header, build_genesis_header, insert_header, validator_utils::*, validators_change_receipt,
|
||||
HeaderBuilder,
|
||||
};
|
||||
|
||||
use bp_eth_poa::{compute_merkle_root, U256};
|
||||
use frame_benchmarking::benchmarks_instance;
|
||||
use frame_system::RawOrigin;
|
||||
|
||||
benchmarks_instance! {
|
||||
// Benchmark `import_unsigned_header` extrinsic with the best possible conditions:
|
||||
// * Parent header is finalized.
|
||||
// * New header doesn't require receipts.
|
||||
// * Nothing is finalized by new header.
|
||||
// * Nothing is pruned by new header.
|
||||
import_unsigned_header_best_case {
|
||||
let n in 1..1000;
|
||||
|
||||
let num_validators = 2;
|
||||
let initial_header = initialize_bench::<T, I>(num_validators);
|
||||
|
||||
// prepare header to be inserted
|
||||
let header = build_custom_header(
|
||||
&validator(1),
|
||||
&initial_header,
|
||||
|mut header| {
|
||||
header.gas_limit = header.gas_limit + U256::from(n);
|
||||
header
|
||||
},
|
||||
);
|
||||
}: import_unsigned_header(RawOrigin::None, header, None)
|
||||
verify {
|
||||
let storage = BridgeStorage::<T, I>::new();
|
||||
assert_eq!(storage.best_block().0.number, 1);
|
||||
assert_eq!(storage.finalized_block().number, 0);
|
||||
}
|
||||
|
||||
// Our goal with this bench is to try and see the effect that finalizing difference ranges of
|
||||
// blocks has on our import time. As such we need to make sure that we keep the number of
|
||||
// validators fixed while changing the number blocks finalized (the complexity parameter) by
|
||||
// importing the last header.
|
||||
//
|
||||
// One important thing to keep in mind is that the runtime provides a finality cache in order to
|
||||
// reduce the overhead of header finalization. However, this is only triggered every 16 blocks.
|
||||
import_unsigned_finality {
|
||||
// Our complexity parameter, n, will represent the number of blocks imported before
|
||||
// finalization.
|
||||
let n in 1..7;
|
||||
|
||||
let mut storage = BridgeStorage::<T, I>::new();
|
||||
let num_validators: u32 = 2;
|
||||
let initial_header = initialize_bench::<T, I>(num_validators as usize);
|
||||
|
||||
// Since we only have two validators we need to make sure the number of blocks is even to
|
||||
// make sure the right validator signs the final block
|
||||
let num_blocks = 2 * n;
|
||||
let mut headers = Vec::new();
|
||||
let mut parent = initial_header.clone();
|
||||
|
||||
// Import a bunch of headers without any verification, will ensure that they're not
|
||||
// finalized prematurely
|
||||
for i in 1..=num_blocks {
|
||||
let header = HeaderBuilder::with_parent(&parent).sign_by(&validator(0));
|
||||
let id = header.compute_id();
|
||||
insert_header(&mut storage, header.clone());
|
||||
headers.push(header.clone());
|
||||
parent = header;
|
||||
}
|
||||
|
||||
let last_header = headers.last().unwrap().clone();
|
||||
let last_authority = validator(1);
|
||||
|
||||
// Need to make sure that the header we're going to import hasn't been inserted
|
||||
// into storage already
|
||||
let header = HeaderBuilder::with_parent(&last_header).sign_by(&last_authority);
|
||||
}: import_unsigned_header(RawOrigin::None, header, None)
|
||||
verify {
|
||||
let storage = BridgeStorage::<T, I>::new();
|
||||
assert_eq!(storage.best_block().0.number, (num_blocks + 1) as u64);
|
||||
assert_eq!(storage.finalized_block().number, num_blocks as u64);
|
||||
}
|
||||
|
||||
// Basically the exact same as `import_unsigned_finality` but with a different range for the
|
||||
// complexity parameter. In this bench we use a larger range of blocks to see how performance
|
||||
// changes when the finality cache kicks in (>16 blocks).
|
||||
import_unsigned_finality_with_cache {
|
||||
// Our complexity parameter, n, will represent the number of blocks imported before
|
||||
// finalization.
|
||||
let n in 7..100;
|
||||
|
||||
let mut storage = BridgeStorage::<T, I>::new();
|
||||
let num_validators: u32 = 2;
|
||||
let initial_header = initialize_bench::<T, I>(num_validators as usize);
|
||||
|
||||
// Since we only have two validators we need to make sure the number of blocks is even to
|
||||
// make sure the right validator signs the final block
|
||||
let num_blocks = 2 * n;
|
||||
let mut headers = Vec::new();
|
||||
let mut parent = initial_header.clone();
|
||||
|
||||
// Import a bunch of headers without any verification, will ensure that they're not
|
||||
// finalized prematurely
|
||||
for i in 1..=num_blocks {
|
||||
let header = HeaderBuilder::with_parent(&parent).sign_by(&validator(0));
|
||||
let id = header.compute_id();
|
||||
insert_header(&mut storage, header.clone());
|
||||
headers.push(header.clone());
|
||||
parent = header;
|
||||
}
|
||||
|
||||
let last_header = headers.last().unwrap().clone();
|
||||
let last_authority = validator(1);
|
||||
|
||||
// Need to make sure that the header we're going to import hasn't been inserted
|
||||
// into storage already
|
||||
let header = HeaderBuilder::with_parent(&last_header).sign_by(&last_authority);
|
||||
}: import_unsigned_header(RawOrigin::None, header, None)
|
||||
verify {
|
||||
let storage = BridgeStorage::<T, I>::new();
|
||||
assert_eq!(storage.best_block().0.number, (num_blocks + 1) as u64);
|
||||
assert_eq!(storage.finalized_block().number, num_blocks as u64);
|
||||
}
|
||||
|
||||
// A block import may trigger a pruning event, which adds extra work to the import progress.
|
||||
// In this bench we trigger a pruning event in order to see how much extra time is spent by the
|
||||
// runtime dealing with it. In the Ethereum Pallet, we're limited pruning to eight blocks in a
|
||||
// single import, as dictated by MAX_BLOCKS_TO_PRUNE_IN_SINGLE_IMPORT.
|
||||
import_unsigned_pruning {
|
||||
let n in 1..MAX_BLOCKS_TO_PRUNE_IN_SINGLE_IMPORT as u32;
|
||||
|
||||
let mut storage = BridgeStorage::<T, I>::new();
|
||||
|
||||
let num_validators = 3;
|
||||
let initial_header = initialize_bench::<T, I>(num_validators as usize);
|
||||
let validators = validators(num_validators);
|
||||
|
||||
// Want to prune eligible blocks between [0, n)
|
||||
BlocksToPrune::<I>::put(PruningRange {
|
||||
oldest_unpruned_block: 0,
|
||||
oldest_block_to_keep: n as u64,
|
||||
});
|
||||
|
||||
let mut parent = initial_header;
|
||||
for i in 1..=n {
|
||||
let header = HeaderBuilder::with_parent(&parent).sign_by_set(&validators);
|
||||
let id = header.compute_id();
|
||||
insert_header(&mut storage, header.clone());
|
||||
parent = header;
|
||||
}
|
||||
|
||||
let header = HeaderBuilder::with_parent(&parent).sign_by_set(&validators);
|
||||
}: import_unsigned_header(RawOrigin::None, header, None)
|
||||
verify {
|
||||
let storage = BridgeStorage::<T, I>::new();
|
||||
let max_pruned: u64 = (n - 1) as _;
|
||||
assert_eq!(storage.best_block().0.number, (n + 1) as u64);
|
||||
assert!(HeadersByNumber::<I>::get(&0).is_none());
|
||||
assert!(HeadersByNumber::<I>::get(&max_pruned).is_none());
|
||||
}
|
||||
|
||||
// The goal of this bench is to import a block which contains a transaction receipt. The receipt
|
||||
// will contain a validator set change. Verifying the receipt root is an expensive operation to
|
||||
// do, which is why we're interested in benchmarking it.
|
||||
import_unsigned_with_receipts {
|
||||
let n in 1..100;
|
||||
|
||||
let mut storage = BridgeStorage::<T, I>::new();
|
||||
|
||||
let num_validators = 1;
|
||||
let initial_header = initialize_bench::<T, I>(num_validators as usize);
|
||||
|
||||
let mut receipts = vec![];
|
||||
for i in 1..=n {
|
||||
let receipt = validators_change_receipt(Default::default());
|
||||
receipts.push(receipt)
|
||||
}
|
||||
let encoded_receipts = receipts.iter().map(|r| r.rlp());
|
||||
|
||||
// We need this extra header since this is what signals a validator set transition. This
|
||||
// will ensure that the next header is within the "Contract" window
|
||||
let header1 = HeaderBuilder::with_parent(&initial_header).sign_by(&validator(0));
|
||||
insert_header(&mut storage, header1.clone());
|
||||
|
||||
let header = build_custom_header(
|
||||
&validator(0),
|
||||
&header1,
|
||||
|mut header| {
|
||||
// Logs Bloom signals a change in validator set
|
||||
header.log_bloom = (&[0xff; 256]).into();
|
||||
header.receipts_root = compute_merkle_root(encoded_receipts);
|
||||
header
|
||||
},
|
||||
);
|
||||
}: import_unsigned_header(RawOrigin::None, header, Some(receipts))
|
||||
verify {
|
||||
let storage = BridgeStorage::<T, I>::new();
|
||||
assert_eq!(storage.best_block().0.number, 2);
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_bench<T: Config<I>, I: Instance>(num_validators: usize) -> AuraHeader {
|
||||
// Initialize storage with some initial header
|
||||
let initial_header = build_genesis_header(&validator(0));
|
||||
let initial_difficulty = initial_header.difficulty;
|
||||
let initial_validators = validators_addresses(num_validators as usize);
|
||||
|
||||
initialize_storage::<T, I>(&initial_header, initial_difficulty, &initial_validators);
|
||||
|
||||
initial_header
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{run_test, TestRuntime};
|
||||
use frame_support::assert_ok;
|
||||
|
||||
#[test]
|
||||
fn insert_unsigned_header_best_case() {
|
||||
run_test(1, |_| {
|
||||
assert_ok!(test_benchmark_import_unsigned_header_best_case::<TestRuntime>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_unsigned_header_finality() {
|
||||
run_test(1, |_| {
|
||||
assert_ok!(test_benchmark_import_unsigned_finality::<TestRuntime>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_unsigned_header_finality_with_cache() {
|
||||
run_test(1, |_| {
|
||||
assert_ok!(test_benchmark_import_unsigned_finality_with_cache::<TestRuntime>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_unsigned_header_pruning() {
|
||||
run_test(1, |_| {
|
||||
assert_ok!(test_benchmark_import_unsigned_pruning::<TestRuntime>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_unsigned_header_receipts() {
|
||||
run_test(1, |_| {
|
||||
assert_ok!(test_benchmark_import_unsigned_with_receipts::<TestRuntime>());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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 sp_runtime::RuntimeDebug;
|
||||
|
||||
/// Header import error.
|
||||
#[derive(Clone, Copy, RuntimeDebug)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub enum Error {
|
||||
/// The header is beyond last finalized and can not be imported.
|
||||
AncientHeader = 0,
|
||||
/// The header is already imported.
|
||||
KnownHeader = 1,
|
||||
/// Seal has an incorrect format.
|
||||
InvalidSealArity = 2,
|
||||
/// Block number isn't sensible.
|
||||
RidiculousNumber = 3,
|
||||
/// Block has too much gas used.
|
||||
TooMuchGasUsed = 4,
|
||||
/// Gas limit header field is invalid.
|
||||
InvalidGasLimit = 5,
|
||||
/// Extra data is of an invalid length.
|
||||
ExtraDataOutOfBounds = 6,
|
||||
/// Timestamp header overflowed.
|
||||
TimestampOverflow = 7,
|
||||
/// The parent header is missing from the blockchain.
|
||||
MissingParentBlock = 8,
|
||||
/// The header step is missing from the header.
|
||||
MissingStep = 9,
|
||||
/// The header signature is missing from the header.
|
||||
MissingSignature = 10,
|
||||
/// Empty steps are missing from the header.
|
||||
MissingEmptySteps = 11,
|
||||
/// The same author issued different votes at the same step.
|
||||
DoubleVote = 12,
|
||||
/// Validation proof insufficient.
|
||||
InsufficientProof = 13,
|
||||
/// Difficulty header field is invalid.
|
||||
InvalidDifficulty = 14,
|
||||
/// The received block is from an incorrect proposer.
|
||||
NotValidator = 15,
|
||||
/// Missing transaction receipts for the operation.
|
||||
MissingTransactionsReceipts = 16,
|
||||
/// Redundant transaction receipts are provided.
|
||||
RedundantTransactionsReceipts = 17,
|
||||
/// Provided transactions receipts are not matching the header.
|
||||
TransactionsReceiptsMismatch = 18,
|
||||
/// Can't accept unsigned header from the far future.
|
||||
UnsignedTooFarInTheFuture = 19,
|
||||
/// Trying to finalize sibling of finalized block.
|
||||
TryingToFinalizeSibling = 20,
|
||||
/// Header timestamp is ahead of on-chain timestamp
|
||||
HeaderTimestampIsAhead = 21,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn msg(&self) -> &'static str {
|
||||
match *self {
|
||||
Error::AncientHeader => "Header is beyound last finalized and can not be imported",
|
||||
Error::KnownHeader => "Header is already imported",
|
||||
Error::InvalidSealArity => "Header has an incorrect seal",
|
||||
Error::RidiculousNumber => "Header has too large number",
|
||||
Error::TooMuchGasUsed => "Header has too much gas used",
|
||||
Error::InvalidGasLimit => "Header has invalid gas limit",
|
||||
Error::ExtraDataOutOfBounds => "Header has too large extra data",
|
||||
Error::TimestampOverflow => "Header has too large timestamp",
|
||||
Error::MissingParentBlock => "Header has unknown parent hash",
|
||||
Error::MissingStep => "Header is missing step seal",
|
||||
Error::MissingSignature => "Header is missing signature seal",
|
||||
Error::MissingEmptySteps => "Header is missing empty steps seal",
|
||||
Error::DoubleVote => "Header has invalid step in seal",
|
||||
Error::InsufficientProof => "Header has insufficient proof",
|
||||
Error::InvalidDifficulty => "Header has invalid difficulty",
|
||||
Error::NotValidator => "Header is sealed by unexpected validator",
|
||||
Error::MissingTransactionsReceipts => "The import operation requires transactions receipts",
|
||||
Error::RedundantTransactionsReceipts => "Redundant transactions receipts are provided",
|
||||
Error::TransactionsReceiptsMismatch => "Invalid transactions receipts provided",
|
||||
Error::UnsignedTooFarInTheFuture => "The unsigned header is too far in future",
|
||||
Error::TryingToFinalizeSibling => "Trying to finalize sibling of finalized block",
|
||||
Error::HeaderTimestampIsAhead => "Header timestamp is ahead of on-chain timestamp",
|
||||
}
|
||||
}
|
||||
|
||||
/// Return unique error code.
|
||||
pub fn code(&self) -> u8 {
|
||||
*self as u8
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
// 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::error::Error;
|
||||
use crate::Storage;
|
||||
use bp_eth_poa::{public_to_address, Address, AuraHeader, HeaderId, SealedEmptyStep, H256};
|
||||
use codec::{Decode, Encode};
|
||||
use sp_io::crypto::secp256k1_ecdsa_recover;
|
||||
use sp_runtime::RuntimeDebug;
|
||||
use sp_std::collections::{
|
||||
btree_map::{BTreeMap, Entry},
|
||||
btree_set::BTreeSet,
|
||||
vec_deque::VecDeque,
|
||||
};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
/// Cached finality votes for given block.
|
||||
#[derive(RuntimeDebug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct CachedFinalityVotes<Submitter> {
|
||||
/// True if we have stopped at best finalized block' sibling. This means
|
||||
/// that we are trying to finalize block from fork that has forked before
|
||||
/// best finalized.
|
||||
pub stopped_at_finalized_sibling: bool,
|
||||
/// Header ancestors that were read while we have been searching for
|
||||
/// cached votes entry. Newest header has index 0.
|
||||
pub unaccounted_ancestry: VecDeque<(HeaderId, Option<Submitter>, AuraHeader)>,
|
||||
/// Cached finality votes, if they have been found. The associated
|
||||
/// header is not included into `unaccounted_ancestry`.
|
||||
pub votes: Option<FinalityVotes<Submitter>>,
|
||||
}
|
||||
|
||||
/// Finality effects.
|
||||
#[derive(RuntimeDebug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct FinalityEffects<Submitter> {
|
||||
/// Finalized headers.
|
||||
pub finalized_headers: Vec<(HeaderId, Option<Submitter>)>,
|
||||
/// Finality votes used in computation.
|
||||
pub votes: FinalityVotes<Submitter>,
|
||||
}
|
||||
|
||||
/// Finality votes for given block.
|
||||
#[derive(RuntimeDebug, Decode, Encode)]
|
||||
#[cfg_attr(test, derive(Clone, PartialEq))]
|
||||
pub struct FinalityVotes<Submitter> {
|
||||
/// Number of votes per each validator.
|
||||
pub votes: BTreeMap<Address, u64>,
|
||||
/// Ancestry blocks with oldest ancestors at the beginning and newest at the
|
||||
/// end of the queue.
|
||||
pub ancestry: VecDeque<FinalityAncestor<Submitter>>,
|
||||
}
|
||||
|
||||
/// Information about block ancestor that is used in computations.
|
||||
#[derive(RuntimeDebug, Decode, Encode)]
|
||||
#[cfg_attr(test, derive(Clone, Default, PartialEq))]
|
||||
pub struct FinalityAncestor<Submitter> {
|
||||
/// Bock id.
|
||||
pub id: HeaderId,
|
||||
/// Block submitter.
|
||||
pub submitter: Option<Submitter>,
|
||||
/// Validators that have signed this block and empty steps on top
|
||||
/// of this block.
|
||||
pub signers: BTreeSet<Address>,
|
||||
}
|
||||
|
||||
/// Tries to finalize blocks when given block is imported.
|
||||
///
|
||||
/// Returns numbers and hashes of finalized blocks in ascending order.
|
||||
pub fn finalize_blocks<S: Storage>(
|
||||
storage: &S,
|
||||
best_finalized: HeaderId,
|
||||
header_validators: (HeaderId, &[Address]),
|
||||
id: HeaderId,
|
||||
submitter: Option<&S::Submitter>,
|
||||
header: &AuraHeader,
|
||||
two_thirds_majority_transition: u64,
|
||||
) -> Result<FinalityEffects<S::Submitter>, Error> {
|
||||
// compute count of voters for every unfinalized block in ancestry
|
||||
let validators = header_validators.1.iter().collect();
|
||||
let votes = prepare_votes(
|
||||
header
|
||||
.parent_id()
|
||||
.map(|parent_id| {
|
||||
storage.cached_finality_votes(&parent_id, &best_finalized, |hash| {
|
||||
*hash == header_validators.0.hash || *hash == best_finalized.hash
|
||||
})
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
best_finalized,
|
||||
&validators,
|
||||
id,
|
||||
header,
|
||||
submitter.cloned(),
|
||||
)?;
|
||||
|
||||
// now let's iterate in reverse order && find just finalized blocks
|
||||
let mut finalized_headers = Vec::new();
|
||||
let mut current_votes = votes.votes.clone();
|
||||
for ancestor in &votes.ancestry {
|
||||
if !is_finalized(
|
||||
&validators,
|
||||
¤t_votes,
|
||||
ancestor.id.number >= two_thirds_majority_transition,
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
remove_signers_votes(&ancestor.signers, &mut current_votes);
|
||||
finalized_headers.push((ancestor.id, ancestor.submitter.clone()));
|
||||
}
|
||||
|
||||
Ok(FinalityEffects {
|
||||
finalized_headers,
|
||||
votes,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if there are enough votes to treat this header as finalized.
|
||||
fn is_finalized(
|
||||
validators: &BTreeSet<&Address>,
|
||||
votes: &BTreeMap<Address, u64>,
|
||||
requires_two_thirds_majority: bool,
|
||||
) -> bool {
|
||||
(!requires_two_thirds_majority && votes.len() * 2 > validators.len())
|
||||
|| (requires_two_thirds_majority && votes.len() * 3 > validators.len() * 2)
|
||||
}
|
||||
|
||||
/// Prepare 'votes' of header and its ancestors' signers.
|
||||
pub(crate) fn prepare_votes<Submitter>(
|
||||
mut cached_votes: CachedFinalityVotes<Submitter>,
|
||||
best_finalized: HeaderId,
|
||||
validators: &BTreeSet<&Address>,
|
||||
id: HeaderId,
|
||||
header: &AuraHeader,
|
||||
submitter: Option<Submitter>,
|
||||
) -> Result<FinalityVotes<Submitter>, Error> {
|
||||
// if we have reached finalized block sibling, then we're trying
|
||||
// to switch finalized blocks
|
||||
if cached_votes.stopped_at_finalized_sibling {
|
||||
return Err(Error::TryingToFinalizeSibling);
|
||||
}
|
||||
|
||||
// this fn can only work with single validators set
|
||||
if !validators.contains(&header.author) {
|
||||
return Err(Error::NotValidator);
|
||||
}
|
||||
|
||||
// now we have votes that were valid when some block B has been inserted
|
||||
// things may have changed a bit, but we do not need to read anything else
|
||||
// from the db, because we have ancestry
|
||||
// so the only thing we need to do is:
|
||||
// 1) remove votes from blocks that have been finalized after B has been inserted;
|
||||
// 2) add votes from B descendants
|
||||
let mut votes = cached_votes.votes.unwrap_or_default();
|
||||
|
||||
// remove votes from finalized blocks
|
||||
while let Some(old_ancestor) = votes.ancestry.pop_front() {
|
||||
if old_ancestor.id.number > best_finalized.number {
|
||||
votes.ancestry.push_front(old_ancestor);
|
||||
break;
|
||||
}
|
||||
|
||||
remove_signers_votes(&old_ancestor.signers, &mut votes.votes);
|
||||
}
|
||||
|
||||
// add votes from new blocks
|
||||
let mut parent_empty_step_signers = empty_steps_signers(header);
|
||||
let mut unaccounted_ancestry = VecDeque::new();
|
||||
while let Some((ancestor_id, ancestor_submitter, ancestor)) = cached_votes.unaccounted_ancestry.pop_front() {
|
||||
let mut signers = empty_steps_signers(&ancestor);
|
||||
sp_std::mem::swap(&mut signers, &mut parent_empty_step_signers);
|
||||
signers.insert(ancestor.author);
|
||||
|
||||
add_signers_votes(validators, &signers, &mut votes.votes)?;
|
||||
|
||||
unaccounted_ancestry.push_front(FinalityAncestor {
|
||||
id: ancestor_id,
|
||||
submitter: ancestor_submitter,
|
||||
signers,
|
||||
});
|
||||
}
|
||||
votes.ancestry.extend(unaccounted_ancestry);
|
||||
|
||||
// add votes from block itself
|
||||
let mut header_signers = BTreeSet::new();
|
||||
header_signers.insert(header.author);
|
||||
*votes.votes.entry(header.author).or_insert(0) += 1;
|
||||
votes.ancestry.push_back(FinalityAncestor {
|
||||
id,
|
||||
submitter,
|
||||
signers: header_signers,
|
||||
});
|
||||
|
||||
Ok(votes)
|
||||
}
|
||||
|
||||
/// Increase count of 'votes' for every passed signer.
|
||||
/// Fails if at least one of signers is not in the `validators` set.
|
||||
fn add_signers_votes(
|
||||
validators: &BTreeSet<&Address>,
|
||||
signers_to_add: &BTreeSet<Address>,
|
||||
votes: &mut BTreeMap<Address, u64>,
|
||||
) -> Result<(), Error> {
|
||||
for signer in signers_to_add {
|
||||
if !validators.contains(signer) {
|
||||
return Err(Error::NotValidator);
|
||||
}
|
||||
|
||||
*votes.entry(*signer).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decrease 'votes' count for every passed signer.
|
||||
fn remove_signers_votes(signers_to_remove: &BTreeSet<Address>, votes: &mut BTreeMap<Address, u64>) {
|
||||
for signer in signers_to_remove {
|
||||
match votes.entry(*signer) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if *entry.get() <= 1 {
|
||||
entry.remove();
|
||||
} else {
|
||||
*entry.get_mut() -= 1;
|
||||
}
|
||||
}
|
||||
Entry::Vacant(_) => unreachable!("we only remove signers that have been added; qed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns unique set of empty steps signers.
|
||||
fn empty_steps_signers(header: &AuraHeader) -> BTreeSet<Address> {
|
||||
header
|
||||
.empty_steps()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|step| empty_step_signer(&step, &header.parent_hash))
|
||||
.collect::<BTreeSet<_>>()
|
||||
}
|
||||
|
||||
/// Returns author of empty step signature.
|
||||
fn empty_step_signer(empty_step: &SealedEmptyStep, parent_hash: &H256) -> Option<Address> {
|
||||
let message = empty_step.message(parent_hash);
|
||||
secp256k1_ecdsa_recover(empty_step.signature.as_fixed_bytes(), message.as_fixed_bytes())
|
||||
.ok()
|
||||
.map(|public| public_to_address(&public))
|
||||
}
|
||||
|
||||
impl<Submitter> Default for CachedFinalityVotes<Submitter> {
|
||||
fn default() -> Self {
|
||||
CachedFinalityVotes {
|
||||
stopped_at_finalized_sibling: false,
|
||||
unaccounted_ancestry: VecDeque::new(),
|
||||
votes: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Submitter> Default for FinalityVotes<Submitter> {
|
||||
fn default() -> Self {
|
||||
FinalityVotes {
|
||||
votes: BTreeMap::new(),
|
||||
ancestry: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{insert_header, run_test, validator, validators_addresses, HeaderBuilder, TestRuntime};
|
||||
use crate::{BridgeStorage, FinalityCache, HeaderToImport};
|
||||
use frame_support::StorageMap;
|
||||
|
||||
const TOTAL_VALIDATORS: usize = 5;
|
||||
|
||||
#[test]
|
||||
fn verifies_header_author() {
|
||||
run_test(TOTAL_VALIDATORS, |_| {
|
||||
assert_eq!(
|
||||
finalize_blocks(
|
||||
&BridgeStorage::<TestRuntime>::new(),
|
||||
Default::default(),
|
||||
(Default::default(), &[]),
|
||||
Default::default(),
|
||||
None,
|
||||
&AuraHeader::default(),
|
||||
0,
|
||||
),
|
||||
Err(Error::NotValidator),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_blocks_works() {
|
||||
run_test(TOTAL_VALIDATORS, |ctx| {
|
||||
// let's say we have 5 validators (we need 'votes' from 3 validators to achieve
|
||||
// finality)
|
||||
let mut storage = BridgeStorage::<TestRuntime>::new();
|
||||
|
||||
// when header#1 is inserted, nothing is finalized (1 vote)
|
||||
let header1 = HeaderBuilder::with_parent(&ctx.genesis).sign_by(&validator(0));
|
||||
let id1 = header1.compute_id();
|
||||
let mut header_to_import = HeaderToImport {
|
||||
context: storage.import_context(None, &header1.parent_hash).unwrap(),
|
||||
is_best: true,
|
||||
id: id1,
|
||||
header: header1,
|
||||
total_difficulty: 0.into(),
|
||||
enacted_change: None,
|
||||
scheduled_change: None,
|
||||
finality_votes: Default::default(),
|
||||
};
|
||||
assert_eq!(
|
||||
finalize_blocks(
|
||||
&storage,
|
||||
ctx.genesis.compute_id(),
|
||||
(Default::default(), &ctx.addresses),
|
||||
id1,
|
||||
None,
|
||||
&header_to_import.header,
|
||||
u64::max_value(),
|
||||
)
|
||||
.map(|eff| eff.finalized_headers),
|
||||
Ok(Vec::new()),
|
||||
);
|
||||
storage.insert_header(header_to_import.clone());
|
||||
|
||||
// when header#2 is inserted, nothing is finalized (2 votes)
|
||||
header_to_import.header = HeaderBuilder::with_parent_hash(id1.hash).sign_by(&validator(1));
|
||||
header_to_import.id = header_to_import.header.compute_id();
|
||||
let id2 = header_to_import.header.compute_id();
|
||||
assert_eq!(
|
||||
finalize_blocks(
|
||||
&storage,
|
||||
ctx.genesis.compute_id(),
|
||||
(Default::default(), &ctx.addresses),
|
||||
id2,
|
||||
None,
|
||||
&header_to_import.header,
|
||||
u64::max_value(),
|
||||
)
|
||||
.map(|eff| eff.finalized_headers),
|
||||
Ok(Vec::new()),
|
||||
);
|
||||
storage.insert_header(header_to_import.clone());
|
||||
|
||||
// when header#3 is inserted, header#1 is finalized (3 votes)
|
||||
header_to_import.header = HeaderBuilder::with_parent_hash(id2.hash).sign_by(&validator(2));
|
||||
header_to_import.id = header_to_import.header.compute_id();
|
||||
let id3 = header_to_import.header.compute_id();
|
||||
assert_eq!(
|
||||
finalize_blocks(
|
||||
&storage,
|
||||
ctx.genesis.compute_id(),
|
||||
(Default::default(), &ctx.addresses),
|
||||
id3,
|
||||
None,
|
||||
&header_to_import.header,
|
||||
u64::max_value(),
|
||||
)
|
||||
.map(|eff| eff.finalized_headers),
|
||||
Ok(vec![(id1, None)]),
|
||||
);
|
||||
storage.insert_header(header_to_import);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_votes_are_updated_with_ancestry() {
|
||||
// we're inserting header#5
|
||||
// cached votes are from header#3
|
||||
// header#4 has finalized header#1 and header#2
|
||||
// => when inserting header#5, we need to:
|
||||
// 1) remove votes from header#1 and header#2
|
||||
// 2) add votes from header#4 and header#5
|
||||
let validators = validators_addresses(5);
|
||||
let headers = (1..6)
|
||||
.map(|number| HeaderBuilder::with_number(number).sign_by(&validator(number as usize - 1)))
|
||||
.collect::<Vec<_>>();
|
||||
let ancestry = headers
|
||||
.iter()
|
||||
.map(|header| FinalityAncestor {
|
||||
id: header.compute_id(),
|
||||
signers: vec![header.author].into_iter().collect(),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let header5 = headers[4].clone();
|
||||
assert_eq!(
|
||||
prepare_votes::<()>(
|
||||
CachedFinalityVotes {
|
||||
stopped_at_finalized_sibling: false,
|
||||
unaccounted_ancestry: vec![(headers[3].compute_id(), None, headers[3].clone()),]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
votes: Some(FinalityVotes {
|
||||
votes: vec![(validators[0], 1), (validators[1], 1), (validators[2], 1),]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
ancestry: ancestry[..3].iter().cloned().collect(),
|
||||
}),
|
||||
},
|
||||
headers[1].compute_id(),
|
||||
&validators.iter().collect(),
|
||||
header5.compute_id(),
|
||||
&header5,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
FinalityVotes {
|
||||
votes: vec![(validators[2], 1), (validators[3], 1), (validators[4], 1),]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
ancestry: ancestry[2..].iter().cloned().collect(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_votes_respects_finality_cache() {
|
||||
run_test(TOTAL_VALIDATORS, |ctx| {
|
||||
// we need signatures of 3 validators to finalize block
|
||||
let mut storage = BridgeStorage::<TestRuntime>::new();
|
||||
|
||||
// headers 1..3 are signed by validator#0
|
||||
// headers 4..6 are signed by validator#1
|
||||
// headers 7..9 are signed by validator#2
|
||||
let mut hashes = Vec::new();
|
||||
let mut headers = Vec::new();
|
||||
let mut ancestry = Vec::new();
|
||||
let mut parent_hash = ctx.genesis.compute_hash();
|
||||
for i in 1..10 {
|
||||
let header = HeaderBuilder::with_parent_hash(parent_hash).sign_by(&validator((i - 1) / 3));
|
||||
let id = header.compute_id();
|
||||
insert_header(&mut storage, header.clone());
|
||||
hashes.push(id.hash);
|
||||
ancestry.push(FinalityAncestor {
|
||||
id: header.compute_id(),
|
||||
submitter: None,
|
||||
signers: vec![header.author].into_iter().collect(),
|
||||
});
|
||||
headers.push(header);
|
||||
parent_hash = id.hash;
|
||||
}
|
||||
|
||||
// when we're inserting header#7 and last finalized header is 0:
|
||||
// check that votes at #7 are computed correctly without cache
|
||||
let expected_votes_at_7 = FinalityVotes {
|
||||
votes: vec![(ctx.addresses[0], 3), (ctx.addresses[1], 3), (ctx.addresses[2], 1)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
ancestry: ancestry[..7].iter().cloned().collect(),
|
||||
};
|
||||
let id7 = headers[6].compute_id();
|
||||
assert_eq!(
|
||||
prepare_votes(
|
||||
storage.cached_finality_votes(
|
||||
&headers.get(5).unwrap().compute_id(),
|
||||
&ctx.genesis.compute_id(),
|
||||
|_| false,
|
||||
),
|
||||
Default::default(),
|
||||
&ctx.addresses.iter().collect(),
|
||||
id7,
|
||||
headers.get(6).unwrap(),
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
expected_votes_at_7,
|
||||
);
|
||||
|
||||
// cached votes at #5
|
||||
let expected_votes_at_5 = FinalityVotes {
|
||||
votes: vec![(ctx.addresses[0], 3), (ctx.addresses[1], 2)].into_iter().collect(),
|
||||
ancestry: ancestry[..5].iter().cloned().collect(),
|
||||
};
|
||||
FinalityCache::<TestRuntime>::insert(hashes[4], expected_votes_at_5);
|
||||
|
||||
// when we're inserting header#7 and last finalized header is 0:
|
||||
// check that votes at #7 are computed correctly with cache
|
||||
assert_eq!(
|
||||
prepare_votes(
|
||||
storage.cached_finality_votes(
|
||||
&headers.get(5).unwrap().compute_id(),
|
||||
&ctx.genesis.compute_id(),
|
||||
|_| false,
|
||||
),
|
||||
Default::default(),
|
||||
&ctx.addresses.iter().collect(),
|
||||
id7,
|
||||
headers.get(6).unwrap(),
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
expected_votes_at_7,
|
||||
);
|
||||
|
||||
// when we're inserting header#7 and last finalized header is 3:
|
||||
// check that votes at #7 are computed correctly with cache
|
||||
let expected_votes_at_7 = FinalityVotes {
|
||||
votes: vec![(ctx.addresses[1], 3), (ctx.addresses[2], 1)].into_iter().collect(),
|
||||
ancestry: ancestry[3..7].iter().cloned().collect(),
|
||||
};
|
||||
assert_eq!(
|
||||
prepare_votes(
|
||||
storage.cached_finality_votes(
|
||||
&headers.get(5).unwrap().compute_id(),
|
||||
&headers.get(2).unwrap().compute_id(),
|
||||
|hash| *hash == hashes[2],
|
||||
),
|
||||
headers[2].compute_id(),
|
||||
&ctx.addresses.iter().collect(),
|
||||
id7,
|
||||
headers.get(6).unwrap(),
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
expected_votes_at_7,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_votes_fails_when_finalized_sibling_is_in_ancestry() {
|
||||
assert_eq!(
|
||||
prepare_votes::<()>(
|
||||
CachedFinalityVotes {
|
||||
stopped_at_finalized_sibling: true,
|
||||
..Default::default()
|
||||
},
|
||||
Default::default(),
|
||||
&validators_addresses(3).iter().collect(),
|
||||
Default::default(),
|
||||
&Default::default(),
|
||||
None,
|
||||
),
|
||||
Err(Error::TryingToFinalizeSibling),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,609 @@
|
||||
// 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::error::Error;
|
||||
use crate::finality::finalize_blocks;
|
||||
use crate::validators::{Validators, ValidatorsConfiguration};
|
||||
use crate::verification::{is_importable_header, verify_aura_header};
|
||||
use crate::{AuraConfiguration, ChainTime, ChangeToEnact, PruningStrategy, Storage};
|
||||
use bp_eth_poa::{AuraHeader, HeaderId, Receipt};
|
||||
use sp_std::{collections::btree_map::BTreeMap, prelude::*};
|
||||
|
||||
/// Imports bunch of headers and updates blocks finality.
|
||||
///
|
||||
/// Transactions receipts must be provided if `header_import_requires_receipts()`
|
||||
/// has returned true.
|
||||
/// If successful, returns tuple where first element is the number of useful headers
|
||||
/// we have imported and the second element is the number of useless headers (duplicate)
|
||||
/// we have NOT imported.
|
||||
/// Returns error if fatal error has occured during import. Some valid headers may be
|
||||
/// imported in this case.
|
||||
/// TODO: update me (https://github.com/paritytech/parity-bridges-common/issues/415)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn import_headers<S: Storage, PS: PruningStrategy, CT: ChainTime>(
|
||||
storage: &mut S,
|
||||
pruning_strategy: &mut PS,
|
||||
aura_config: &AuraConfiguration,
|
||||
validators_config: &ValidatorsConfiguration,
|
||||
submitter: Option<S::Submitter>,
|
||||
headers: Vec<(AuraHeader, Option<Vec<Receipt>>)>,
|
||||
chain_time: &CT,
|
||||
finalized_headers: &mut BTreeMap<S::Submitter, u64>,
|
||||
) -> Result<(u64, u64), Error> {
|
||||
let mut useful = 0;
|
||||
let mut useless = 0;
|
||||
for (header, receipts) in headers {
|
||||
let import_result = import_header(
|
||||
storage,
|
||||
pruning_strategy,
|
||||
aura_config,
|
||||
validators_config,
|
||||
submitter.clone(),
|
||||
header,
|
||||
chain_time,
|
||||
receipts,
|
||||
);
|
||||
|
||||
match import_result {
|
||||
Ok((_, finalized)) => {
|
||||
for (_, submitter) in finalized {
|
||||
if let Some(submitter) = submitter {
|
||||
*finalized_headers.entry(submitter).or_default() += 1;
|
||||
}
|
||||
}
|
||||
useful += 1;
|
||||
}
|
||||
Err(Error::AncientHeader) | Err(Error::KnownHeader) => useless += 1,
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
Ok((useful, useless))
|
||||
}
|
||||
|
||||
/// A vector of finalized headers and their submitters.
|
||||
pub type FinalizedHeaders<S> = Vec<(HeaderId, Option<<S as Storage>::Submitter>)>;
|
||||
|
||||
/// Imports given header and updates blocks finality (if required).
|
||||
///
|
||||
/// Transactions receipts must be provided if `header_import_requires_receipts()`
|
||||
/// has returned true.
|
||||
///
|
||||
/// Returns imported block id and list of all finalized headers.
|
||||
/// TODO: update me (https://github.com/paritytech/parity-bridges-common/issues/415)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn import_header<S: Storage, PS: PruningStrategy, CT: ChainTime>(
|
||||
storage: &mut S,
|
||||
pruning_strategy: &mut PS,
|
||||
aura_config: &AuraConfiguration,
|
||||
validators_config: &ValidatorsConfiguration,
|
||||
submitter: Option<S::Submitter>,
|
||||
header: AuraHeader,
|
||||
chain_time: &CT,
|
||||
receipts: Option<Vec<Receipt>>,
|
||||
) -> Result<(HeaderId, FinalizedHeaders<S>), Error> {
|
||||
// first check that we are able to import this header at all
|
||||
let (header_id, finalized_id) = is_importable_header(storage, &header)?;
|
||||
|
||||
// verify header
|
||||
let import_context = verify_aura_header(storage, aura_config, submitter, &header, chain_time)?;
|
||||
|
||||
// check if block schedules new validators
|
||||
let validators = Validators::new(validators_config);
|
||||
let (scheduled_change, enacted_change) = validators.extract_validators_change(&header, receipts)?;
|
||||
|
||||
// check if block finalizes some other blocks and corresponding scheduled validators
|
||||
let validators_set = import_context.validators_set();
|
||||
let finalized_blocks = finalize_blocks(
|
||||
storage,
|
||||
finalized_id,
|
||||
(validators_set.enact_block, &validators_set.validators),
|
||||
header_id,
|
||||
import_context.submitter(),
|
||||
&header,
|
||||
aura_config.two_thirds_majority_transition,
|
||||
)?;
|
||||
let enacted_change = enacted_change
|
||||
.map(|validators| ChangeToEnact {
|
||||
signal_block: None,
|
||||
validators,
|
||||
})
|
||||
.or_else(|| validators.finalize_validators_change(storage, &finalized_blocks.finalized_headers));
|
||||
|
||||
// NOTE: we can't return Err() from anywhere below this line
|
||||
// (because otherwise we'll have inconsistent storage if transaction will fail)
|
||||
|
||||
// and finally insert the block
|
||||
let (best_id, best_total_difficulty) = storage.best_block();
|
||||
let total_difficulty = import_context.total_difficulty() + header.difficulty;
|
||||
let is_best = total_difficulty > best_total_difficulty;
|
||||
storage.insert_header(import_context.into_import_header(
|
||||
is_best,
|
||||
header_id,
|
||||
header,
|
||||
total_difficulty,
|
||||
enacted_change,
|
||||
scheduled_change,
|
||||
finalized_blocks.votes,
|
||||
));
|
||||
|
||||
// compute upper border of updated pruning range
|
||||
let new_best_block_id = if is_best { header_id } else { best_id };
|
||||
let new_best_finalized_block_id = finalized_blocks.finalized_headers.last().map(|(id, _)| *id);
|
||||
let pruning_upper_bound = pruning_strategy.pruning_upper_bound(
|
||||
new_best_block_id.number,
|
||||
new_best_finalized_block_id
|
||||
.map(|id| id.number)
|
||||
.unwrap_or(finalized_id.number),
|
||||
);
|
||||
|
||||
// now mark finalized headers && prune old headers
|
||||
storage.finalize_and_prune_headers(new_best_finalized_block_id, pruning_upper_bound);
|
||||
|
||||
Ok((header_id, finalized_blocks.finalized_headers))
|
||||
}
|
||||
|
||||
/// Returns true if transactions receipts are required to import given header.
|
||||
pub fn header_import_requires_receipts<S: Storage>(
|
||||
storage: &S,
|
||||
validators_config: &ValidatorsConfiguration,
|
||||
header: &AuraHeader,
|
||||
) -> bool {
|
||||
is_importable_header(storage, header)
|
||||
.map(|_| Validators::new(validators_config))
|
||||
.map(|validators| validators.maybe_signals_validators_change(header))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{
|
||||
run_test, secret_to_address, test_aura_config, test_validators_config, validator, validators_addresses,
|
||||
validators_change_receipt, HeaderBuilder, KeepSomeHeadersBehindBest, TestRuntime, GAS_LIMIT,
|
||||
};
|
||||
use crate::validators::ValidatorsSource;
|
||||
use crate::DefaultInstance;
|
||||
use crate::{BlocksToPrune, BridgeStorage, Headers, PruningRange};
|
||||
use frame_support::{StorageMap, StorageValue};
|
||||
use secp256k1::SecretKey;
|
||||
|
||||
const TOTAL_VALIDATORS: usize = 3;
|
||||
|
||||
#[test]
|
||||
fn rejects_finalized_block_competitors() {
|
||||
run_test(TOTAL_VALIDATORS, |_| {
|
||||
let mut storage = BridgeStorage::<TestRuntime>::new();
|
||||
storage.finalize_and_prune_headers(
|
||||
Some(HeaderId {
|
||||
number: 100,
|
||||
..Default::default()
|
||||
}),
|
||||
0,
|
||||
);
|
||||
assert_eq!(
|
||||
import_header(
|
||||
&mut storage,
|
||||
&mut KeepSomeHeadersBehindBest::default(),
|
||||
&test_aura_config(),
|
||||
&test_validators_config(),
|
||||
None,
|
||||
Default::default(),
|
||||
&(),
|
||||
None,
|
||||
),
|
||||
Err(Error::AncientHeader),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_known_header() {
|
||||
run_test(TOTAL_VALIDATORS, |ctx| {
|
||||
let mut storage = BridgeStorage::<TestRuntime>::new();
|
||||
let header = HeaderBuilder::with_parent(&ctx.genesis).sign_by(&validator(1));
|
||||
assert_eq!(
|
||||
import_header(
|
||||
&mut storage,
|
||||
&mut KeepSomeHeadersBehindBest::default(),
|
||||
&test_aura_config(),
|
||||
&test_validators_config(),
|
||||
None,
|
||||
header.clone(),
|
||||
&(),
|
||||
None,
|
||||
)
|
||||
.map(|_| ()),
|
||||
Ok(()),
|
||||
);
|
||||
assert_eq!(
|
||||
import_header(
|
||||
&mut storage,
|
||||
&mut KeepSomeHeadersBehindBest::default(),
|
||||
&test_aura_config(),
|
||||
&test_validators_config(),
|
||||
None,
|
||||
header,
|
||||
&(),
|
||||
None,
|
||||
)
|
||||
.map(|_| ()),
|
||||
Err(Error::KnownHeader),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_header_works() {
|
||||
run_test(TOTAL_VALIDATORS, |ctx| {
|
||||
let validators_config = ValidatorsConfiguration::Multi(vec![
|
||||
(0, ValidatorsSource::List(ctx.addresses.clone())),
|
||||
(1, ValidatorsSource::List(validators_addresses(2))),
|
||||
]);
|
||||
let mut storage = BridgeStorage::<TestRuntime>::new();
|
||||
let header = HeaderBuilder::with_parent(&ctx.genesis).sign_by(&validator(1));
|
||||
let hash = header.compute_hash();
|
||||
assert_eq!(
|
||||
import_header(
|
||||
&mut storage,
|
||||
&mut KeepSomeHeadersBehindBest::default(),
|
||||
&test_aura_config(),
|
||||
&validators_config,
|
||||
None,
|
||||
header,
|
||||
&(),
|
||||
None
|
||||
)
|
||||
.map(|_| ()),
|
||||
Ok(()),
|
||||
);
|
||||
|
||||
// check that new validators will be used for next header
|
||||
let imported_header = Headers::<TestRuntime>::get(&hash).unwrap();
|
||||
assert_eq!(
|
||||
imported_header.next_validators_set_id,
|
||||
1, // new set is enacted from config
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headers_are_pruned_during_import() {
|
||||
run_test(TOTAL_VALIDATORS, |ctx| {
|
||||
let validators_config =
|
||||
ValidatorsConfiguration::Single(ValidatorsSource::Contract([3; 20].into(), ctx.addresses.clone()));
|
||||
let validators = vec![validator(0), validator(1), validator(2)];
|
||||
let mut storage = BridgeStorage::<TestRuntime>::new();
|
||||
|
||||
// header [0..11] are finalizing blocks [0; 9]
|
||||
// => since we want to keep 10 finalized blocks, we aren't pruning anything
|
||||
let mut latest_block_id = Default::default();
|
||||
for i in 1..11 {
|
||||
let header = HeaderBuilder::with_parent_number(i - 1).sign_by_set(&validators);
|
||||
let parent_id = header.parent_id().unwrap();
|
||||
|
||||
let (rolling_last_block_id, finalized_blocks) = import_header(
|
||||
&mut storage,
|
||||
&mut KeepSomeHeadersBehindBest::default(),
|
||||
&test_aura_config(),
|
||||
&validators_config,
|
||||
Some(100),
|
||||
header,
|
||||
&(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
match i {
|
||||
2..=10 => assert_eq!(finalized_blocks, vec![(parent_id, Some(100))], "At {}", i,),
|
||||
_ => assert_eq!(finalized_blocks, vec![], "At {}", i),
|
||||
}
|
||||
latest_block_id = rolling_last_block_id;
|
||||
}
|
||||
assert!(storage.header(&ctx.genesis.compute_hash()).is_some());
|
||||
|
||||
// header 11 finalizes headers [10] AND schedules change
|
||||
// => we prune header#0
|
||||
let header11 = HeaderBuilder::with_parent_number(10)
|
||||
.log_bloom((&[0xff; 256]).into())
|
||||
.receipts_root(
|
||||
"ead6c772ba0083bbff497ba0f4efe47c199a2655401096c21ab7450b6c466d97"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.sign_by_set(&validators);
|
||||
let parent_id = header11.parent_id().unwrap();
|
||||
let (rolling_last_block_id, finalized_blocks) = import_header(
|
||||
&mut storage,
|
||||
&mut KeepSomeHeadersBehindBest::default(),
|
||||
&test_aura_config(),
|
||||
&validators_config,
|
||||
Some(101),
|
||||
header11.clone(),
|
||||
&(),
|
||||
Some(vec![validators_change_receipt(latest_block_id.hash)]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(finalized_blocks, vec![(parent_id, Some(100))],);
|
||||
assert!(storage.header(&ctx.genesis.compute_hash()).is_none());
|
||||
latest_block_id = rolling_last_block_id;
|
||||
|
||||
// and now let's say validators 1 && 2 went offline
|
||||
// => in the range 12-25 no blocks are finalized, but we still continue to prune old headers
|
||||
// until header#11 is met. we can't prune #11, because it schedules change
|
||||
let mut step = 56u64;
|
||||
let mut expected_blocks = vec![(header11.compute_id(), Some(101))];
|
||||
for i in 12..25 {
|
||||
let header = HeaderBuilder::with_parent_hash(latest_block_id.hash)
|
||||
.difficulty(i.into())
|
||||
.step(step)
|
||||
.sign_by_set(&validators);
|
||||
expected_blocks.push((header.compute_id(), Some(102)));
|
||||
let (rolling_last_block_id, finalized_blocks) = import_header(
|
||||
&mut storage,
|
||||
&mut KeepSomeHeadersBehindBest::default(),
|
||||
&test_aura_config(),
|
||||
&validators_config,
|
||||
Some(102),
|
||||
header,
|
||||
&(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(finalized_blocks, vec![],);
|
||||
latest_block_id = rolling_last_block_id;
|
||||
step += 3;
|
||||
}
|
||||
assert_eq!(
|
||||
BlocksToPrune::<DefaultInstance>::get(),
|
||||
PruningRange {
|
||||
oldest_unpruned_block: 11,
|
||||
oldest_block_to_keep: 14,
|
||||
},
|
||||
);
|
||||
|
||||
// now let's insert block signed by validator 1
|
||||
// => blocks 11..24 are finalized and blocks 11..14 are pruned
|
||||
step -= 2;
|
||||
let header = HeaderBuilder::with_parent_hash(latest_block_id.hash)
|
||||
.difficulty(25.into())
|
||||
.step(step)
|
||||
.sign_by_set(&validators);
|
||||
let (_, finalized_blocks) = import_header(
|
||||
&mut storage,
|
||||
&mut KeepSomeHeadersBehindBest::default(),
|
||||
&test_aura_config(),
|
||||
&validators_config,
|
||||
Some(103),
|
||||
header,
|
||||
&(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(finalized_blocks, expected_blocks);
|
||||
assert_eq!(
|
||||
BlocksToPrune::<DefaultInstance>::get(),
|
||||
PruningRange {
|
||||
oldest_unpruned_block: 15,
|
||||
oldest_block_to_keep: 15,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn import_custom_block<S: Storage>(
|
||||
storage: &mut S,
|
||||
validators: &[SecretKey],
|
||||
header: AuraHeader,
|
||||
) -> Result<HeaderId, Error> {
|
||||
let id = header.compute_id();
|
||||
import_header(
|
||||
storage,
|
||||
&mut KeepSomeHeadersBehindBest::default(),
|
||||
&test_aura_config(),
|
||||
&ValidatorsConfiguration::Single(ValidatorsSource::Contract(
|
||||
[0; 20].into(),
|
||||
validators.iter().map(secret_to_address).collect(),
|
||||
)),
|
||||
None,
|
||||
header,
|
||||
&(),
|
||||
None,
|
||||
)
|
||||
.map(|_| id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_of_non_best_block_may_finalize_blocks() {
|
||||
run_test(TOTAL_VALIDATORS, |ctx| {
|
||||
let mut storage = BridgeStorage::<TestRuntime>::new();
|
||||
|
||||
// insert headers (H1, validator1), (H2, validator1), (H3, validator1)
|
||||
// making H3 the best header, without finalizing anything (we need 2 signatures)
|
||||
let mut expected_best_block = Default::default();
|
||||
for i in 1..4 {
|
||||
let step = 1 + i * TOTAL_VALIDATORS as u64;
|
||||
expected_best_block = import_custom_block(
|
||||
&mut storage,
|
||||
&ctx.validators,
|
||||
HeaderBuilder::with_parent_number(i - 1)
|
||||
.step(step)
|
||||
.sign_by_set(&ctx.validators),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let (best_block, best_difficulty) = storage.best_block();
|
||||
assert_eq!(best_block, expected_best_block);
|
||||
assert_eq!(storage.finalized_block(), ctx.genesis.compute_id());
|
||||
|
||||
// insert headers (H1', validator1), (H2', validator2), finalizing H2, even though H3
|
||||
// has better difficulty than H2' (because there are more steps involved)
|
||||
let mut expected_finalized_block = Default::default();
|
||||
let mut parent_hash = ctx.genesis.compute_hash();
|
||||
for i in 1..3 {
|
||||
let step = i;
|
||||
let id = import_custom_block(
|
||||
&mut storage,
|
||||
&ctx.validators,
|
||||
HeaderBuilder::with_parent_hash(parent_hash)
|
||||
.step(step)
|
||||
.gas_limit((GAS_LIMIT + 1).into())
|
||||
.sign_by_set(&ctx.validators),
|
||||
)
|
||||
.unwrap();
|
||||
parent_hash = id.hash;
|
||||
if i == 1 {
|
||||
expected_finalized_block = id;
|
||||
}
|
||||
}
|
||||
let (new_best_block, new_best_difficulty) = storage.best_block();
|
||||
assert_eq!(new_best_block, expected_best_block);
|
||||
assert_eq!(new_best_difficulty, best_difficulty);
|
||||
assert_eq!(storage.finalized_block(), expected_finalized_block);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_to_unfinalized_fork_fails() {
|
||||
const VALIDATORS: u64 = 5;
|
||||
run_test(VALIDATORS as usize, |ctx| {
|
||||
let mut storage = BridgeStorage::<TestRuntime>::new();
|
||||
|
||||
// header1, authored by validator[2] is best common block between two competing forks
|
||||
let header1 = import_custom_block(
|
||||
&mut storage,
|
||||
&ctx.validators,
|
||||
HeaderBuilder::with_parent_number(0)
|
||||
.step(2)
|
||||
.sign_by_set(&ctx.validators),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(storage.best_block().0, header1);
|
||||
assert_eq!(storage.finalized_block().number, 0);
|
||||
|
||||
// validator[3] has authored header2 (nothing is finalized yet)
|
||||
let header2 = import_custom_block(
|
||||
&mut storage,
|
||||
&ctx.validators,
|
||||
HeaderBuilder::with_parent_number(1)
|
||||
.step(3)
|
||||
.sign_by_set(&ctx.validators),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(storage.best_block().0, header2);
|
||||
assert_eq!(storage.finalized_block().number, 0);
|
||||
|
||||
// validator[4] has authored header3 (header1 is finalized)
|
||||
let header3 = import_custom_block(
|
||||
&mut storage,
|
||||
&ctx.validators,
|
||||
HeaderBuilder::with_parent_number(2)
|
||||
.step(4)
|
||||
.sign_by_set(&ctx.validators),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(storage.best_block().0, header3);
|
||||
assert_eq!(storage.finalized_block(), header1);
|
||||
|
||||
// validator[4] has authored 4 blocks: header2'...header5' (header1 is still finalized)
|
||||
let header2_1 = import_custom_block(
|
||||
&mut storage,
|
||||
&ctx.validators,
|
||||
HeaderBuilder::with_parent_number(1)
|
||||
.gas_limit((GAS_LIMIT + 1).into())
|
||||
.step(4)
|
||||
.sign_by_set(&ctx.validators),
|
||||
)
|
||||
.unwrap();
|
||||
let header3_1 = import_custom_block(
|
||||
&mut storage,
|
||||
&ctx.validators,
|
||||
HeaderBuilder::with_parent_hash(header2_1.hash)
|
||||
.step(4 + VALIDATORS)
|
||||
.sign_by_set(&ctx.validators),
|
||||
)
|
||||
.unwrap();
|
||||
let header4_1 = import_custom_block(
|
||||
&mut storage,
|
||||
&ctx.validators,
|
||||
HeaderBuilder::with_parent_hash(header3_1.hash)
|
||||
.step(4 + VALIDATORS * 2)
|
||||
.sign_by_set(&ctx.validators),
|
||||
)
|
||||
.unwrap();
|
||||
let header5_1 = import_custom_block(
|
||||
&mut storage,
|
||||
&ctx.validators,
|
||||
HeaderBuilder::with_parent_hash(header4_1.hash)
|
||||
.step(4 + VALIDATORS * 3)
|
||||
.sign_by_set(&ctx.validators),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(storage.best_block().0, header5_1);
|
||||
assert_eq!(storage.finalized_block(), header1);
|
||||
|
||||
// when we import header4 { parent = header3 }, authored by validator[0], header2 is finalized
|
||||
let header4 = import_custom_block(
|
||||
&mut storage,
|
||||
&ctx.validators,
|
||||
HeaderBuilder::with_parent_number(3)
|
||||
.step(5)
|
||||
.sign_by_set(&ctx.validators),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(storage.best_block().0, header5_1);
|
||||
assert_eq!(storage.finalized_block(), header2);
|
||||
|
||||
// when we import header5 { parent = header4 }, authored by validator[1], header3 is finalized
|
||||
let header5 = import_custom_block(
|
||||
&mut storage,
|
||||
&ctx.validators,
|
||||
HeaderBuilder::with_parent_hash(header4.hash)
|
||||
.step(6)
|
||||
.sign_by_set(&ctx.validators),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(storage.best_block().0, header5);
|
||||
assert_eq!(storage.finalized_block(), header3);
|
||||
|
||||
// import of header2'' { parent = header1 } fails, because it has number < best_finalized
|
||||
assert_eq!(
|
||||
import_custom_block(
|
||||
&mut storage,
|
||||
&ctx.validators,
|
||||
HeaderBuilder::with_parent_number(1)
|
||||
.gas_limit((GAS_LIMIT + 1).into())
|
||||
.step(3)
|
||||
.sign_by_set(&ctx.validators)
|
||||
),
|
||||
Err(Error::AncientHeader),
|
||||
);
|
||||
|
||||
// import of header6' should also fail because we're trying to append to fork thas
|
||||
// has forked before finalized block
|
||||
assert_eq!(
|
||||
import_custom_block(
|
||||
&mut storage,
|
||||
&ctx.validators,
|
||||
HeaderBuilder::with_parent_number(5)
|
||||
.gas_limit((GAS_LIMIT + 1).into())
|
||||
.step(5 + VALIDATORS * 4)
|
||||
.sign_by_set(&ctx.validators),
|
||||
),
|
||||
Err(Error::TryingToFinalizeSibling),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,192 @@
|
||||
// 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/>.
|
||||
|
||||
// From construct_runtime macro
|
||||
#![allow(clippy::from_over_into)]
|
||||
|
||||
pub use crate::test_utils::{insert_header, validator_utils::*, validators_change_receipt, HeaderBuilder, GAS_LIMIT};
|
||||
pub use bp_eth_poa::signatures::secret_to_address;
|
||||
|
||||
use crate::validators::{ValidatorsConfiguration, ValidatorsSource};
|
||||
use crate::{AuraConfiguration, ChainTime, Config, GenesisConfig as CrateGenesisConfig, PruningStrategy};
|
||||
use bp_eth_poa::{Address, AuraHeader, H256, U256};
|
||||
use frame_support::{parameter_types, weights::Weight};
|
||||
use secp256k1::SecretKey;
|
||||
use sp_runtime::{
|
||||
testing::Header as SubstrateHeader,
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
Perbill,
|
||||
};
|
||||
|
||||
pub type AccountId = u64;
|
||||
|
||||
type Block = frame_system::mocking::MockBlock<TestRuntime>;
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<TestRuntime>;
|
||||
|
||||
use crate as pallet_ethereum;
|
||||
|
||||
frame_support::construct_runtime! {
|
||||
pub enum TestRuntime where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
|
||||
Ethereum: pallet_ethereum::{Pallet, Call},
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub const MaximumBlockWeight: Weight = 1024;
|
||||
pub const MaximumBlockLength: u32 = 2 * 1024;
|
||||
pub const AvailableBlockRatio: Perbill = Perbill::one();
|
||||
}
|
||||
|
||||
impl frame_system::Config for TestRuntime {
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type Call = Call;
|
||||
type BlockNumber = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = SubstrateHeader;
|
||||
type Event = Event;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = ();
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type BaseCallFilter = ();
|
||||
type SystemWeightInfo = ();
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const TestFinalityVotesCachingInterval: Option<u64> = Some(16);
|
||||
pub TestAuraConfiguration: AuraConfiguration = test_aura_config();
|
||||
pub TestValidatorsConfiguration: ValidatorsConfiguration = test_validators_config();
|
||||
}
|
||||
|
||||
impl Config for TestRuntime {
|
||||
type AuraConfiguration = TestAuraConfiguration;
|
||||
type ValidatorsConfiguration = TestValidatorsConfiguration;
|
||||
type FinalityVotesCachingInterval = TestFinalityVotesCachingInterval;
|
||||
type PruningStrategy = KeepSomeHeadersBehindBest;
|
||||
type ChainTime = ConstChainTime;
|
||||
type OnHeadersSubmitted = ();
|
||||
}
|
||||
|
||||
/// Test context.
|
||||
pub struct TestContext {
|
||||
/// Initial (genesis) header.
|
||||
pub genesis: AuraHeader,
|
||||
/// Number of initial validators.
|
||||
pub total_validators: usize,
|
||||
/// Secret keys of validators, ordered by validator index.
|
||||
pub validators: Vec<SecretKey>,
|
||||
/// Addresses of validators, ordered by validator index.
|
||||
pub addresses: Vec<Address>,
|
||||
}
|
||||
|
||||
/// Aura configuration that is used in tests by default.
|
||||
pub fn test_aura_config() -> AuraConfiguration {
|
||||
AuraConfiguration {
|
||||
empty_steps_transition: u64::max_value(),
|
||||
strict_empty_steps_transition: 0,
|
||||
validate_step_transition: 0x16e360,
|
||||
validate_score_transition: 0x41a3c4,
|
||||
two_thirds_majority_transition: u64::max_value(),
|
||||
min_gas_limit: 0x1388.into(),
|
||||
max_gas_limit: U256::max_value(),
|
||||
maximum_extra_data_size: 0x20,
|
||||
}
|
||||
}
|
||||
|
||||
/// Validators configuration that is used in tests by default.
|
||||
pub fn test_validators_config() -> ValidatorsConfiguration {
|
||||
ValidatorsConfiguration::Single(ValidatorsSource::List(validators_addresses(3)))
|
||||
}
|
||||
|
||||
/// Genesis header that is used in tests by default.
|
||||
pub fn genesis() -> AuraHeader {
|
||||
HeaderBuilder::genesis().sign_by(&validator(0))
|
||||
}
|
||||
|
||||
/// Run test with default genesis header.
|
||||
pub fn run_test<T>(total_validators: usize, test: impl FnOnce(TestContext) -> T) -> T {
|
||||
run_test_with_genesis(genesis(), total_validators, test)
|
||||
}
|
||||
|
||||
/// Run test with default genesis header.
|
||||
pub fn run_test_with_genesis<T>(
|
||||
genesis: AuraHeader,
|
||||
total_validators: usize,
|
||||
test: impl FnOnce(TestContext) -> T,
|
||||
) -> T {
|
||||
let validators = validators(total_validators);
|
||||
let addresses = validators_addresses(total_validators);
|
||||
sp_io::TestExternalities::new(
|
||||
CrateGenesisConfig {
|
||||
initial_header: genesis.clone(),
|
||||
initial_difficulty: 0.into(),
|
||||
initial_validators: addresses.clone(),
|
||||
}
|
||||
.build_storage::<TestRuntime, crate::DefaultInstance>()
|
||||
.unwrap(),
|
||||
)
|
||||
.execute_with(|| {
|
||||
test(TestContext {
|
||||
genesis,
|
||||
total_validators,
|
||||
validators,
|
||||
addresses,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Pruning strategy that keeps 10 headers behind best block.
|
||||
pub struct KeepSomeHeadersBehindBest(pub u64);
|
||||
|
||||
impl Default for KeepSomeHeadersBehindBest {
|
||||
fn default() -> KeepSomeHeadersBehindBest {
|
||||
KeepSomeHeadersBehindBest(10)
|
||||
}
|
||||
}
|
||||
|
||||
impl PruningStrategy for KeepSomeHeadersBehindBest {
|
||||
fn pruning_upper_bound(&mut self, best_number: u64, _: u64) -> u64 {
|
||||
best_number.saturating_sub(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Constant chain time
|
||||
#[derive(Default)]
|
||||
pub struct ConstChainTime;
|
||||
|
||||
impl ChainTime for ConstChainTime {
|
||||
fn is_timestamp_ahead(&self, timestamp: u64) -> bool {
|
||||
let now = i32::max_value() as u64 / 2;
|
||||
timestamp > now
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
// 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 for testing and benchmarking the Ethereum Bridge Pallet.
|
||||
//!
|
||||
//! Although the name implies that it is used by tests, it shouldn't be be used _directly_ by tests.
|
||||
//! Instead these utilities should be used by the Mock runtime, which in turn is used by tests.
|
||||
//!
|
||||
//! On the other hand, they may be used directly by the bechmarking module.
|
||||
|
||||
// Since this is test code it's fine that not everything is used
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::finality::FinalityVotes;
|
||||
use crate::validators::CHANGE_EVENT_HASH;
|
||||
use crate::verification::calculate_score;
|
||||
use crate::{Config, HeaderToImport, Storage};
|
||||
|
||||
use bp_eth_poa::{
|
||||
rlp_encode,
|
||||
signatures::{secret_to_address, sign, SignHeader},
|
||||
Address, AuraHeader, Bloom, Receipt, SealedEmptyStep, H256, U256,
|
||||
};
|
||||
use secp256k1::SecretKey;
|
||||
use sp_std::prelude::*;
|
||||
|
||||
/// Gas limit valid in test environment.
|
||||
pub const GAS_LIMIT: u64 = 0x2000;
|
||||
|
||||
/// Test header builder.
|
||||
pub struct HeaderBuilder {
|
||||
header: AuraHeader,
|
||||
parent_header: AuraHeader,
|
||||
}
|
||||
|
||||
impl HeaderBuilder {
|
||||
/// Creates default genesis header.
|
||||
pub fn genesis() -> Self {
|
||||
let current_step = 0u64;
|
||||
Self {
|
||||
header: AuraHeader {
|
||||
gas_limit: GAS_LIMIT.into(),
|
||||
seal: vec![bp_eth_poa::rlp_encode(¤t_step).to_vec(), vec![]],
|
||||
..Default::default()
|
||||
},
|
||||
parent_header: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates default header on top of test parent with given hash.
|
||||
#[cfg(test)]
|
||||
pub fn with_parent_hash(parent_hash: H256) -> Self {
|
||||
Self::with_parent_hash_on_runtime::<crate::mock::TestRuntime, crate::DefaultInstance>(parent_hash)
|
||||
}
|
||||
|
||||
/// Creates default header on top of test parent with given number. First parent is selected.
|
||||
#[cfg(test)]
|
||||
pub fn with_parent_number(parent_number: u64) -> Self {
|
||||
Self::with_parent_number_on_runtime::<crate::mock::TestRuntime, crate::DefaultInstance>(parent_number)
|
||||
}
|
||||
|
||||
/// Creates default header on top of parent with given hash.
|
||||
pub fn with_parent_hash_on_runtime<T: Config<I>, I: crate::Instance>(parent_hash: H256) -> Self {
|
||||
use crate::Headers;
|
||||
use frame_support::StorageMap;
|
||||
|
||||
let parent_header = Headers::<T, I>::get(&parent_hash).unwrap().header;
|
||||
Self::with_parent(&parent_header)
|
||||
}
|
||||
|
||||
/// Creates default header on top of parent with given number. First parent is selected.
|
||||
pub fn with_parent_number_on_runtime<T: Config<I>, I: crate::Instance>(parent_number: u64) -> Self {
|
||||
use crate::HeadersByNumber;
|
||||
use frame_support::StorageMap;
|
||||
|
||||
let parent_hash = HeadersByNumber::<I>::get(parent_number).unwrap()[0];
|
||||
Self::with_parent_hash_on_runtime::<T, I>(parent_hash)
|
||||
}
|
||||
|
||||
/// Creates default header on top of non-existent parent.
|
||||
#[cfg(test)]
|
||||
pub fn with_number(number: u64) -> Self {
|
||||
Self::with_parent(&AuraHeader {
|
||||
number: number - 1,
|
||||
seal: vec![bp_eth_poa::rlp_encode(&(number - 1)).to_vec(), vec![]],
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates default header on top of given parent.
|
||||
pub fn with_parent(parent_header: &AuraHeader) -> Self {
|
||||
let parent_step = parent_header.step().unwrap();
|
||||
let current_step = parent_step + 1;
|
||||
Self {
|
||||
header: AuraHeader {
|
||||
parent_hash: parent_header.compute_hash(),
|
||||
number: parent_header.number + 1,
|
||||
gas_limit: GAS_LIMIT.into(),
|
||||
seal: vec![bp_eth_poa::rlp_encode(¤t_step).to_vec(), vec![]],
|
||||
difficulty: calculate_score(parent_step, current_step, 0),
|
||||
..Default::default()
|
||||
},
|
||||
parent_header: parent_header.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update step of this header.
|
||||
pub fn step(mut self, step: u64) -> Self {
|
||||
let parent_step = self.parent_header.step();
|
||||
self.header.seal[0] = rlp_encode(&step).to_vec();
|
||||
self.header.difficulty = parent_step
|
||||
.map(|parent_step| calculate_score(parent_step, step, 0))
|
||||
.unwrap_or_default();
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds empty steps to this header.
|
||||
pub fn empty_steps(mut self, empty_steps: &[(&SecretKey, u64)]) -> Self {
|
||||
let sealed_empty_steps = empty_steps
|
||||
.iter()
|
||||
.map(|(author, step)| {
|
||||
let mut empty_step = SealedEmptyStep {
|
||||
step: *step,
|
||||
signature: Default::default(),
|
||||
};
|
||||
let message = empty_step.message(&self.header.parent_hash);
|
||||
let signature: [u8; 65] = sign(author, message).into();
|
||||
empty_step.signature = signature.into();
|
||||
empty_step
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// by default in test configuration headers are generated without empty steps seal
|
||||
if self.header.seal.len() < 3 {
|
||||
self.header.seal.push(Vec::new());
|
||||
}
|
||||
|
||||
self.header.seal[2] = SealedEmptyStep::rlp_of(&sealed_empty_steps);
|
||||
self
|
||||
}
|
||||
|
||||
/// Update difficulty field of this header.
|
||||
pub fn difficulty(mut self, difficulty: U256) -> Self {
|
||||
self.header.difficulty = difficulty;
|
||||
self
|
||||
}
|
||||
|
||||
/// Update extra data field of this header.
|
||||
pub fn extra_data(mut self, extra_data: Vec<u8>) -> Self {
|
||||
self.header.extra_data = extra_data;
|
||||
self
|
||||
}
|
||||
|
||||
/// Update gas limit field of this header.
|
||||
pub fn gas_limit(mut self, gas_limit: U256) -> Self {
|
||||
self.header.gas_limit = gas_limit;
|
||||
self
|
||||
}
|
||||
|
||||
/// Update gas used field of this header.
|
||||
pub fn gas_used(mut self, gas_used: U256) -> Self {
|
||||
self.header.gas_used = gas_used;
|
||||
self
|
||||
}
|
||||
|
||||
/// Update log bloom field of this header.
|
||||
pub fn log_bloom(mut self, log_bloom: Bloom) -> Self {
|
||||
self.header.log_bloom = log_bloom;
|
||||
self
|
||||
}
|
||||
|
||||
/// Update receipts root field of this header.
|
||||
pub fn receipts_root(mut self, receipts_root: H256) -> Self {
|
||||
self.header.receipts_root = receipts_root;
|
||||
self
|
||||
}
|
||||
|
||||
/// Update timestamp field of this header.
|
||||
pub fn timestamp(mut self, timestamp: u64) -> Self {
|
||||
self.header.timestamp = timestamp;
|
||||
self
|
||||
}
|
||||
|
||||
/// Update transactions root field of this header.
|
||||
pub fn transactions_root(mut self, transactions_root: H256) -> Self {
|
||||
self.header.transactions_root = transactions_root;
|
||||
self
|
||||
}
|
||||
|
||||
/// Signs header by given author.
|
||||
pub fn sign_by(self, author: &SecretKey) -> AuraHeader {
|
||||
self.header.sign_by(author)
|
||||
}
|
||||
|
||||
/// Signs header by given authors set.
|
||||
pub fn sign_by_set(self, authors: &[SecretKey]) -> AuraHeader {
|
||||
self.header.sign_by_set(authors)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function for getting a genesis header which has been signed by an authority.
|
||||
pub fn build_genesis_header(author: &SecretKey) -> AuraHeader {
|
||||
let genesis = HeaderBuilder::genesis();
|
||||
genesis.header.sign_by(&author)
|
||||
}
|
||||
|
||||
/// Helper function for building a custom child header which has been signed by an authority.
|
||||
pub fn build_custom_header<F>(author: &SecretKey, previous: &AuraHeader, customize_header: F) -> AuraHeader
|
||||
where
|
||||
F: FnOnce(AuraHeader) -> AuraHeader,
|
||||
{
|
||||
let new_header = HeaderBuilder::with_parent(&previous);
|
||||
let custom_header = customize_header(new_header.header);
|
||||
custom_header.sign_by(author)
|
||||
}
|
||||
|
||||
/// Insert unverified header into storage.
|
||||
///
|
||||
/// This function assumes that the header is signed by validator from the current set.
|
||||
pub fn insert_header<S: Storage>(storage: &mut S, header: AuraHeader) {
|
||||
let id = header.compute_id();
|
||||
let best_finalized = storage.finalized_block();
|
||||
let import_context = storage.import_context(None, &header.parent_hash).unwrap();
|
||||
let parent_finality_votes = storage.cached_finality_votes(&header.parent_id().unwrap(), &best_finalized, |_| false);
|
||||
let finality_votes = crate::finality::prepare_votes(
|
||||
parent_finality_votes,
|
||||
best_finalized,
|
||||
&import_context.validators_set().validators.iter().collect(),
|
||||
id,
|
||||
&header,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
storage.insert_header(HeaderToImport {
|
||||
context: storage.import_context(None, &header.parent_hash).unwrap(),
|
||||
is_best: true,
|
||||
id,
|
||||
header,
|
||||
total_difficulty: 0.into(),
|
||||
enacted_change: None,
|
||||
scheduled_change: None,
|
||||
finality_votes,
|
||||
});
|
||||
}
|
||||
|
||||
/// Insert unverified header into storage.
|
||||
///
|
||||
/// No assumptions about header author are made. The cost is that finality votes cache
|
||||
/// is filled incorrectly, so this function shall not be used if you're going to insert
|
||||
/// (or import) header descendants.
|
||||
pub fn insert_dummy_header<S: Storage>(storage: &mut S, header: AuraHeader) {
|
||||
storage.insert_header(HeaderToImport {
|
||||
context: storage.import_context(None, &header.parent_hash).unwrap(),
|
||||
is_best: true,
|
||||
id: header.compute_id(),
|
||||
header,
|
||||
total_difficulty: 0.into(),
|
||||
enacted_change: None,
|
||||
scheduled_change: None,
|
||||
finality_votes: FinalityVotes::default(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn validators_change_receipt(parent_hash: H256) -> Receipt {
|
||||
use bp_eth_poa::{LogEntry, TransactionOutcome};
|
||||
|
||||
Receipt {
|
||||
gas_used: 0.into(),
|
||||
log_bloom: (&[0xff; 256]).into(),
|
||||
outcome: TransactionOutcome::Unknown,
|
||||
logs: vec![LogEntry {
|
||||
address: [3; 20].into(),
|
||||
topics: vec![CHANGE_EVENT_HASH.into(), parent_hash],
|
||||
data: vec![
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||
],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
pub mod validator_utils {
|
||||
use super::*;
|
||||
|
||||
/// Return key pair of given test validator.
|
||||
pub fn validator(index: usize) -> SecretKey {
|
||||
let mut raw_secret = [0u8; 32];
|
||||
raw_secret[..8].copy_from_slice(&(index + 1).to_le_bytes());
|
||||
SecretKey::parse(&raw_secret).unwrap()
|
||||
}
|
||||
|
||||
/// Return key pairs of all test validators.
|
||||
pub fn validators(count: usize) -> Vec<SecretKey> {
|
||||
(0..count).map(validator).collect()
|
||||
}
|
||||
|
||||
/// Return address of test validator.
|
||||
pub fn validator_address(index: usize) -> Address {
|
||||
secret_to_address(&validator(index))
|
||||
}
|
||||
|
||||
/// Return addresses of all test validators.
|
||||
pub fn validators_addresses(count: usize) -> Vec<Address> {
|
||||
(0..count).map(validator_address).collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
// 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::error::Error;
|
||||
use crate::{ChangeToEnact, Storage};
|
||||
use bp_eth_poa::{Address, AuraHeader, HeaderId, LogEntry, Receipt, U256};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
/// The hash of InitiateChange event of the validators set contract.
|
||||
pub(crate) const CHANGE_EVENT_HASH: &[u8; 32] = &[
|
||||
0x55, 0x25, 0x2f, 0xa6, 0xee, 0xe4, 0x74, 0x1b, 0x4e, 0x24, 0xa7, 0x4a, 0x70, 0xe9, 0xc1, 0x1f, 0xd2, 0xc2, 0x28,
|
||||
0x1d, 0xf8, 0xd6, 0xea, 0x13, 0x12, 0x6f, 0xf8, 0x45, 0xf7, 0x82, 0x5c, 0x89,
|
||||
];
|
||||
|
||||
/// Where source of validators addresses come from. This covers the chain lifetime.
|
||||
pub enum ValidatorsConfiguration {
|
||||
/// There's a single source for the whole chain lifetime.
|
||||
Single(ValidatorsSource),
|
||||
/// Validators source changes at given blocks. The blocks are ordered
|
||||
/// by the block number.
|
||||
Multi(Vec<(u64, ValidatorsSource)>),
|
||||
}
|
||||
|
||||
/// Where validators addresses come from.
|
||||
///
|
||||
/// This source is valid within some blocks range. The blocks range could
|
||||
/// cover multiple epochs - i.e. the validators that are authoring blocks
|
||||
/// within this range could change, but the source itself can not.
|
||||
#[cfg_attr(any(test, feature = "runtime-benchmarks"), derive(Debug, PartialEq))]
|
||||
pub enum ValidatorsSource {
|
||||
/// The validators addresses are hardcoded and never change.
|
||||
List(Vec<Address>),
|
||||
/// The validators addresses are determined by the validators set contract
|
||||
/// deployed at given address. The contract must implement the `ValidatorSet`
|
||||
/// interface. Additionally, the initial validators set must be provided.
|
||||
Contract(Address, Vec<Address>),
|
||||
}
|
||||
|
||||
/// A short hand for optional validators change.
|
||||
pub type ValidatorsChange = Option<Vec<Address>>;
|
||||
|
||||
/// Validators manager.
|
||||
pub struct Validators<'a> {
|
||||
config: &'a ValidatorsConfiguration,
|
||||
}
|
||||
|
||||
impl<'a> Validators<'a> {
|
||||
/// Creates new validators manager using given configuration.
|
||||
pub fn new(config: &'a ValidatorsConfiguration) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
/// Returns true if header (probabilistically) signals validators change and
|
||||
/// the caller needs to provide transactions receipts to import the header.
|
||||
pub fn maybe_signals_validators_change(&self, header: &AuraHeader) -> bool {
|
||||
let (_, _, source) = self.source_at(header.number);
|
||||
|
||||
// if we are taking validators set from the fixed list, there's always
|
||||
// single epoch
|
||||
// => we never require transactions receipts
|
||||
let contract_address = match source {
|
||||
ValidatorsSource::List(_) => return false,
|
||||
ValidatorsSource::Contract(contract_address, _) => contract_address,
|
||||
};
|
||||
|
||||
// else we need to check logs bloom and if it has required bits set, it means
|
||||
// that the contract has (probably) emitted epoch change event
|
||||
let expected_bloom = LogEntry {
|
||||
address: *contract_address,
|
||||
topics: vec![CHANGE_EVENT_HASH.into(), header.parent_hash],
|
||||
data: Vec::new(), // irrelevant for bloom.
|
||||
}
|
||||
.bloom();
|
||||
|
||||
header.log_bloom.contains(&expected_bloom)
|
||||
}
|
||||
|
||||
/// Extracts validators change signal from the header.
|
||||
///
|
||||
/// Returns tuple where first element is the change scheduled by this header
|
||||
/// (i.e. this change is only applied starting from the block that has finalized
|
||||
/// current block). The second element is the immediately applied change.
|
||||
pub fn extract_validators_change(
|
||||
&self,
|
||||
header: &AuraHeader,
|
||||
receipts: Option<Vec<Receipt>>,
|
||||
) -> Result<(ValidatorsChange, ValidatorsChange), Error> {
|
||||
// let's first check if new source is starting from this header
|
||||
let (source_index, _, source) = self.source_at(header.number);
|
||||
let (next_starts_at, next_source) = self.source_at_next_header(source_index, header.number);
|
||||
if next_starts_at == header.number {
|
||||
match *next_source {
|
||||
ValidatorsSource::List(ref new_list) => return Ok((None, Some(new_list.clone()))),
|
||||
ValidatorsSource::Contract(_, ref new_list) => return Ok((Some(new_list.clone()), None)),
|
||||
}
|
||||
}
|
||||
|
||||
// else deal with previous source
|
||||
//
|
||||
// if we are taking validators set from the fixed list, there's always
|
||||
// single epoch
|
||||
// => we never require transactions receipts
|
||||
let contract_address = match source {
|
||||
ValidatorsSource::List(_) => return Ok((None, None)),
|
||||
ValidatorsSource::Contract(contract_address, _) => contract_address,
|
||||
};
|
||||
|
||||
// else we need to check logs bloom and if it has required bits set, it means
|
||||
// that the contract has (probably) emitted epoch change event
|
||||
let expected_bloom = LogEntry {
|
||||
address: *contract_address,
|
||||
topics: vec![CHANGE_EVENT_HASH.into(), header.parent_hash],
|
||||
data: Vec::new(), // irrelevant for bloom.
|
||||
}
|
||||
.bloom();
|
||||
|
||||
if !header.log_bloom.contains(&expected_bloom) {
|
||||
return Ok((None, None));
|
||||
}
|
||||
|
||||
let receipts = receipts.ok_or(Error::MissingTransactionsReceipts)?;
|
||||
if header.check_receipts_root(&receipts).is_err() {
|
||||
return Err(Error::TransactionsReceiptsMismatch);
|
||||
}
|
||||
|
||||
// iterate in reverse because only the _last_ change in a given
|
||||
// block actually has any effect
|
||||
Ok((
|
||||
receipts
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|r| r.log_bloom.contains(&expected_bloom))
|
||||
.flat_map(|r| r.logs.iter())
|
||||
.filter(|l| {
|
||||
l.address == *contract_address
|
||||
&& l.topics.len() == 2 && l.topics[0].as_fixed_bytes() == CHANGE_EVENT_HASH
|
||||
&& l.topics[1] == header.parent_hash
|
||||
})
|
||||
.filter_map(|l| {
|
||||
let data_len = l.data.len();
|
||||
if data_len < 64 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_validators_len_u256 = U256::from_big_endian(&l.data[32..64]);
|
||||
let new_validators_len = new_validators_len_u256.low_u64();
|
||||
if new_validators_len_u256 != new_validators_len.into() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if (data_len - 64) as u64 != new_validators_len.saturating_mul(32) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
l.data[64..]
|
||||
.chunks(32)
|
||||
.map(|chunk| {
|
||||
let mut new_validator = Address::default();
|
||||
new_validator.as_mut().copy_from_slice(&chunk[12..32]);
|
||||
new_validator
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.next(),
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
/// Finalize changes when blocks are finalized.
|
||||
pub fn finalize_validators_change<S: Storage>(
|
||||
&self,
|
||||
storage: &S,
|
||||
finalized_blocks: &[(HeaderId, Option<S::Submitter>)],
|
||||
) -> Option<ChangeToEnact> {
|
||||
// if we haven't finalized any blocks, no changes may be finalized
|
||||
let newest_finalized_id = match finalized_blocks.last().map(|(id, _)| id) {
|
||||
Some(last_finalized_id) => last_finalized_id,
|
||||
None => return None,
|
||||
};
|
||||
let oldest_finalized_id = finalized_blocks
|
||||
.first()
|
||||
.map(|(id, _)| id)
|
||||
.expect("finalized_blocks is not empty; qed");
|
||||
|
||||
// try to directly go to the header that has scheduled last change
|
||||
//
|
||||
// if we're unable to create import context for some block, it means
|
||||
// that the header has already been pruned => it and its ancestors had
|
||||
// no scheduled changes
|
||||
//
|
||||
// if we're unable to find scheduled changes for some block, it means
|
||||
// that these changes have been finalized already
|
||||
storage
|
||||
.import_context(None, &newest_finalized_id.hash)
|
||||
.and_then(|context| context.last_signal_block())
|
||||
.and_then(|signal_block| {
|
||||
if signal_block.number >= oldest_finalized_id.number {
|
||||
Some(signal_block)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.and_then(|signal_block| {
|
||||
storage
|
||||
.scheduled_change(&signal_block.hash)
|
||||
.map(|change| ChangeToEnact {
|
||||
signal_block: Some(signal_block),
|
||||
validators: change.validators,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns source of validators that should author the header.
|
||||
fn source_at(&self, header_number: u64) -> (usize, u64, &ValidatorsSource) {
|
||||
match self.config {
|
||||
ValidatorsConfiguration::Single(ref source) => (0, 0, source),
|
||||
ValidatorsConfiguration::Multi(ref sources) => sources
|
||||
.iter()
|
||||
.rev()
|
||||
.enumerate()
|
||||
.find(|(_, &(begin, _))| begin < header_number)
|
||||
.map(|(i, (begin, source))| (sources.len() - 1 - i, *begin, source))
|
||||
.expect(
|
||||
"there's always entry for the initial block;\
|
||||
we do not touch any headers with number < initial block number; qed",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns source of validators that should author the next header.
|
||||
fn source_at_next_header(&self, header_source_index: usize, header_number: u64) -> (u64, &ValidatorsSource) {
|
||||
match self.config {
|
||||
ValidatorsConfiguration::Single(ref source) => (0, source),
|
||||
ValidatorsConfiguration::Multi(ref sources) => {
|
||||
let next_source_index = header_source_index + 1;
|
||||
if next_source_index < sources.len() {
|
||||
let next_source = &sources[next_source_index];
|
||||
if next_source.0 < header_number + 1 {
|
||||
return (next_source.0, &next_source.1);
|
||||
}
|
||||
}
|
||||
|
||||
let source = &sources[header_source_index];
|
||||
(source.0, &source.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ValidatorsSource {
|
||||
/// Returns initial validators set.
|
||||
pub fn initial_epoch_validators(&self) -> Vec<Address> {
|
||||
match self {
|
||||
ValidatorsSource::List(ref list) => list.clone(),
|
||||
ValidatorsSource::Contract(_, ref list) => list.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{run_test, validators_addresses, validators_change_receipt, TestRuntime};
|
||||
use crate::DefaultInstance;
|
||||
use crate::{AuraScheduledChange, BridgeStorage, Headers, ScheduledChanges, StoredHeader};
|
||||
use bp_eth_poa::compute_merkle_root;
|
||||
use frame_support::StorageMap;
|
||||
|
||||
const TOTAL_VALIDATORS: usize = 3;
|
||||
|
||||
#[test]
|
||||
fn source_at_works() {
|
||||
let config = ValidatorsConfiguration::Multi(vec![
|
||||
(0, ValidatorsSource::List(vec![[1; 20].into()])),
|
||||
(100, ValidatorsSource::List(vec![[2; 20].into()])),
|
||||
(200, ValidatorsSource::Contract([3; 20].into(), vec![[3; 20].into()])),
|
||||
]);
|
||||
let validators = Validators::new(&config);
|
||||
|
||||
assert_eq!(
|
||||
validators.source_at(99),
|
||||
(0, 0, &ValidatorsSource::List(vec![[1; 20].into()])),
|
||||
);
|
||||
assert_eq!(
|
||||
validators.source_at_next_header(0, 99),
|
||||
(0, &ValidatorsSource::List(vec![[1; 20].into()])),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
validators.source_at(100),
|
||||
(0, 0, &ValidatorsSource::List(vec![[1; 20].into()])),
|
||||
);
|
||||
assert_eq!(
|
||||
validators.source_at_next_header(0, 100),
|
||||
(100, &ValidatorsSource::List(vec![[2; 20].into()])),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
validators.source_at(200),
|
||||
(1, 100, &ValidatorsSource::List(vec![[2; 20].into()])),
|
||||
);
|
||||
assert_eq!(
|
||||
validators.source_at_next_header(1, 200),
|
||||
(200, &ValidatorsSource::Contract([3; 20].into(), vec![[3; 20].into()])),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybe_signals_validators_change_works() {
|
||||
// when contract is active, but bloom has no required bits set
|
||||
let config = ValidatorsConfiguration::Single(ValidatorsSource::Contract(Default::default(), Vec::new()));
|
||||
let validators = Validators::new(&config);
|
||||
let mut header = AuraHeader {
|
||||
number: u64::max_value(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!validators.maybe_signals_validators_change(&header));
|
||||
|
||||
// when contract is active and bloom has required bits set
|
||||
header.log_bloom = (&[0xff; 256]).into();
|
||||
assert!(validators.maybe_signals_validators_change(&header));
|
||||
|
||||
// when list is active and bloom has required bits set
|
||||
let config = ValidatorsConfiguration::Single(ValidatorsSource::List(vec![[42; 20].into()]));
|
||||
let validators = Validators::new(&config);
|
||||
assert!(!validators.maybe_signals_validators_change(&header));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_validators_change_works() {
|
||||
let config = ValidatorsConfiguration::Multi(vec![
|
||||
(0, ValidatorsSource::List(vec![[1; 20].into()])),
|
||||
(100, ValidatorsSource::List(vec![[2; 20].into()])),
|
||||
(200, ValidatorsSource::Contract([3; 20].into(), vec![[3; 20].into()])),
|
||||
]);
|
||||
let validators = Validators::new(&config);
|
||||
let mut header = AuraHeader {
|
||||
number: 100,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// when we're at the block that switches to list source
|
||||
assert_eq!(
|
||||
validators.extract_validators_change(&header, None),
|
||||
Ok((None, Some(vec![[2; 20].into()]))),
|
||||
);
|
||||
|
||||
// when we're inside list range
|
||||
header.number = 150;
|
||||
assert_eq!(validators.extract_validators_change(&header, None), Ok((None, None)),);
|
||||
|
||||
// when we're at the block that switches to contract source
|
||||
header.number = 200;
|
||||
assert_eq!(
|
||||
validators.extract_validators_change(&header, None),
|
||||
Ok((Some(vec![[3; 20].into()]), None)),
|
||||
);
|
||||
|
||||
// when we're inside contract range and logs bloom signals change
|
||||
// but we have no receipts
|
||||
header.number = 250;
|
||||
header.log_bloom = (&[0xff; 256]).into();
|
||||
assert_eq!(
|
||||
validators.extract_validators_change(&header, None),
|
||||
Err(Error::MissingTransactionsReceipts),
|
||||
);
|
||||
|
||||
// when we're inside contract range and logs bloom signals change
|
||||
// but there's no change in receipts
|
||||
header.receipts_root = "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
|
||||
.parse()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
validators.extract_validators_change(&header, Some(Vec::new())),
|
||||
Ok((None, None)),
|
||||
);
|
||||
|
||||
// when we're inside contract range and logs bloom signals change
|
||||
// and there's change in receipts
|
||||
let receipts = vec![validators_change_receipt(Default::default())];
|
||||
header.receipts_root = compute_merkle_root(receipts.iter().map(|r| r.rlp()));
|
||||
assert_eq!(
|
||||
validators.extract_validators_change(&header, Some(receipts)),
|
||||
Ok((Some(vec![[7; 20].into()]), None)),
|
||||
);
|
||||
|
||||
// when incorrect receipts root passed
|
||||
assert_eq!(
|
||||
validators.extract_validators_change(&header, Some(Vec::new())),
|
||||
Err(Error::TransactionsReceiptsMismatch),
|
||||
);
|
||||
}
|
||||
|
||||
fn try_finalize_with_scheduled_change(scheduled_at: Option<HeaderId>) -> Option<ChangeToEnact> {
|
||||
run_test(TOTAL_VALIDATORS, |_| {
|
||||
let config = ValidatorsConfiguration::Single(ValidatorsSource::Contract(Default::default(), Vec::new()));
|
||||
let validators = Validators::new(&config);
|
||||
let storage = BridgeStorage::<TestRuntime>::new();
|
||||
|
||||
// when we're finailizing blocks 10...100
|
||||
let id10 = HeaderId {
|
||||
number: 10,
|
||||
hash: [10; 32].into(),
|
||||
};
|
||||
let id100 = HeaderId {
|
||||
number: 100,
|
||||
hash: [100; 32].into(),
|
||||
};
|
||||
let finalized_blocks = vec![(id10, None), (id100, None)];
|
||||
let header100 = StoredHeader::<u64> {
|
||||
submitter: None,
|
||||
header: AuraHeader {
|
||||
number: 100,
|
||||
..Default::default()
|
||||
},
|
||||
total_difficulty: 0.into(),
|
||||
next_validators_set_id: 0,
|
||||
last_signal_block: scheduled_at,
|
||||
};
|
||||
let scheduled_change = AuraScheduledChange {
|
||||
validators: validators_addresses(1),
|
||||
prev_signal_block: None,
|
||||
};
|
||||
Headers::<TestRuntime>::insert(id100.hash, header100);
|
||||
if let Some(scheduled_at) = scheduled_at {
|
||||
ScheduledChanges::<DefaultInstance>::insert(scheduled_at.hash, scheduled_change);
|
||||
}
|
||||
|
||||
validators.finalize_validators_change(&storage, &finalized_blocks)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_validators_change_finalizes_scheduled_change() {
|
||||
let id50 = HeaderId {
|
||||
number: 50,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
try_finalize_with_scheduled_change(Some(id50)),
|
||||
Some(ChangeToEnact {
|
||||
signal_block: Some(id50),
|
||||
validators: validators_addresses(1),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_validators_change_does_not_finalize_when_changes_are_not_scheduled() {
|
||||
assert_eq!(try_finalize_with_scheduled_change(None), None,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_validators_change_does_not_finalize_changes_when_they_are_outside_of_range() {
|
||||
let id5 = HeaderId {
|
||||
number: 5,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(try_finalize_with_scheduled_change(Some(id5)), None,);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,945 @@
|
||||
// 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::error::Error;
|
||||
use crate::validators::{Validators, ValidatorsConfiguration};
|
||||
use crate::{AuraConfiguration, AuraScheduledChange, ChainTime, ImportContext, PoolConfiguration, Storage};
|
||||
use bp_eth_poa::{
|
||||
public_to_address, step_validator, Address, AuraHeader, HeaderId, Receipt, SealedEmptyStep, H256, H520, U128, U256,
|
||||
};
|
||||
use codec::Encode;
|
||||
use sp_io::crypto::secp256k1_ecdsa_recover;
|
||||
use sp_runtime::transaction_validity::TransactionTag;
|
||||
use sp_std::{vec, vec::Vec};
|
||||
|
||||
/// Pre-check to see if should try and import this header.
|
||||
/// Returns error if we should not try to import this block.
|
||||
/// Returns ID of passed header and best finalized header.
|
||||
pub fn is_importable_header<S: Storage>(storage: &S, header: &AuraHeader) -> Result<(HeaderId, HeaderId), Error> {
|
||||
// we never import any header that competes with finalized header
|
||||
let finalized_id = storage.finalized_block();
|
||||
if header.number <= finalized_id.number {
|
||||
return Err(Error::AncientHeader);
|
||||
}
|
||||
// we never import any header with known hash
|
||||
let id = header.compute_id();
|
||||
if storage.header(&id.hash).is_some() {
|
||||
return Err(Error::KnownHeader);
|
||||
}
|
||||
|
||||
Ok((id, finalized_id))
|
||||
}
|
||||
|
||||
/// Try accept unsigned aura header into transaction pool.
|
||||
///
|
||||
/// Returns required and provided tags.
|
||||
pub fn accept_aura_header_into_pool<S: Storage, CT: ChainTime>(
|
||||
storage: &S,
|
||||
config: &AuraConfiguration,
|
||||
validators_config: &ValidatorsConfiguration,
|
||||
pool_config: &PoolConfiguration,
|
||||
header: &AuraHeader,
|
||||
chain_time: &CT,
|
||||
receipts: Option<&Vec<Receipt>>,
|
||||
) -> Result<(Vec<TransactionTag>, Vec<TransactionTag>), Error> {
|
||||
// check if we can verify further
|
||||
let (header_id, _) = is_importable_header(storage, header)?;
|
||||
|
||||
// we can always do contextless checks
|
||||
contextless_checks(config, header, chain_time)?;
|
||||
|
||||
// we want to avoid having same headers twice in the pool
|
||||
// => we're strict about receipts here - if we need them, we require receipts to be Some,
|
||||
// otherwise we require receipts to be None
|
||||
let receipts_required = Validators::new(validators_config).maybe_signals_validators_change(header);
|
||||
match (receipts_required, receipts.is_some()) {
|
||||
(true, false) => return Err(Error::MissingTransactionsReceipts),
|
||||
(false, true) => return Err(Error::RedundantTransactionsReceipts),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// we do not want to have all future headers in the pool at once
|
||||
// => if we see header with number > maximal ever seen header number + LIMIT,
|
||||
// => we consider this transaction invalid, but only at this moment (we do not want to ban it)
|
||||
// => let's mark it as Unknown transaction
|
||||
let (best_id, _) = storage.best_block();
|
||||
let difference = header.number.saturating_sub(best_id.number);
|
||||
if difference > pool_config.max_future_number_difference {
|
||||
return Err(Error::UnsignedTooFarInTheFuture);
|
||||
}
|
||||
|
||||
// TODO: only accept new headers when we're at the tip of PoA chain
|
||||
// https://github.com/paritytech/parity-bridges-common/issues/38
|
||||
|
||||
// we want to see at most one header with given number from single authority
|
||||
// => every header is providing tag (block_number + authority)
|
||||
// => since only one tx in the pool can provide the same tag, they're auto-deduplicated
|
||||
let provides_number_and_authority_tag = (header.number, header.author).encode();
|
||||
|
||||
// we want to see several 'future' headers in the pool at once, but we may not have access to
|
||||
// previous headers here
|
||||
// => we can at least 'verify' that headers comprise a chain by providing and requiring
|
||||
// tag (header.number, header.hash)
|
||||
let provides_header_number_and_hash_tag = header_id.encode();
|
||||
|
||||
// depending on whether parent header is available, we either perform full or 'shortened' check
|
||||
let context = storage.import_context(None, &header.parent_hash);
|
||||
let tags = match context {
|
||||
Some(context) => {
|
||||
let header_step = contextual_checks(config, &context, None, header)?;
|
||||
validator_checks(config, &context.validators_set().validators, header, header_step)?;
|
||||
|
||||
// since our parent is already in the storage, we do not require it
|
||||
// to be in the transaction pool
|
||||
(
|
||||
vec![],
|
||||
vec![provides_number_and_authority_tag, provides_header_number_and_hash_tag],
|
||||
)
|
||||
}
|
||||
None => {
|
||||
// we know nothing about parent header
|
||||
// => the best thing we can do is to believe that there are no forks in
|
||||
// PoA chain AND that the header is produced either by previous, or next
|
||||
// scheduled validators set change
|
||||
let header_step = header.step().ok_or(Error::MissingStep)?;
|
||||
let best_context = storage.import_context(None, &best_id.hash).expect(
|
||||
"import context is None only when header is missing from the storage;\
|
||||
best header is always in the storage; qed",
|
||||
);
|
||||
let validators_check_result =
|
||||
validator_checks(config, &best_context.validators_set().validators, header, header_step);
|
||||
if let Err(error) = validators_check_result {
|
||||
find_next_validators_signal(storage, &best_context)
|
||||
.ok_or(error)
|
||||
.and_then(|next_validators| validator_checks(config, &next_validators, header, header_step))?;
|
||||
}
|
||||
|
||||
// since our parent is missing from the storage, we **DO** require it
|
||||
// to be in the transaction pool
|
||||
// (- 1 can't underflow because there's always best block in the header)
|
||||
let requires_header_number_and_hash_tag = HeaderId {
|
||||
number: header.number - 1,
|
||||
hash: header.parent_hash,
|
||||
}
|
||||
.encode();
|
||||
(
|
||||
vec![requires_header_number_and_hash_tag],
|
||||
vec![provides_number_and_authority_tag, provides_header_number_and_hash_tag],
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// the heaviest, but rare operation - we do not want invalid receipts in the pool
|
||||
if let Some(receipts) = receipts {
|
||||
log::trace!(target: "runtime", "Got receipts! {:?}", receipts);
|
||||
if header.check_receipts_root(receipts).is_err() {
|
||||
return Err(Error::TransactionsReceiptsMismatch);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
/// Verify header by Aura rules.
|
||||
pub fn verify_aura_header<S: Storage, CT: ChainTime>(
|
||||
storage: &S,
|
||||
config: &AuraConfiguration,
|
||||
submitter: Option<S::Submitter>,
|
||||
header: &AuraHeader,
|
||||
chain_time: &CT,
|
||||
) -> Result<ImportContext<S::Submitter>, Error> {
|
||||
// let's do the lightest check first
|
||||
contextless_checks(config, header, chain_time)?;
|
||||
|
||||
// the rest of checks requires access to the parent header
|
||||
let context = storage.import_context(submitter, &header.parent_hash).ok_or_else(|| {
|
||||
log::warn!(
|
||||
target: "runtime",
|
||||
"Missing parent PoA block: ({:?}, {})",
|
||||
header.number.checked_sub(1),
|
||||
header.parent_hash,
|
||||
);
|
||||
|
||||
Error::MissingParentBlock
|
||||
})?;
|
||||
let header_step = contextual_checks(config, &context, None, header)?;
|
||||
validator_checks(config, &context.validators_set().validators, header, header_step)?;
|
||||
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
/// Perform basic checks that only require header itself.
|
||||
fn contextless_checks<CT: ChainTime>(
|
||||
config: &AuraConfiguration,
|
||||
header: &AuraHeader,
|
||||
chain_time: &CT,
|
||||
) -> Result<(), Error> {
|
||||
let expected_seal_fields = expected_header_seal_fields(config, header);
|
||||
if header.seal.len() != expected_seal_fields {
|
||||
return Err(Error::InvalidSealArity);
|
||||
}
|
||||
if header.number >= u64::max_value() {
|
||||
return Err(Error::RidiculousNumber);
|
||||
}
|
||||
if header.gas_used > header.gas_limit {
|
||||
return Err(Error::TooMuchGasUsed);
|
||||
}
|
||||
if header.gas_limit < config.min_gas_limit {
|
||||
return Err(Error::InvalidGasLimit);
|
||||
}
|
||||
if header.gas_limit > config.max_gas_limit {
|
||||
return Err(Error::InvalidGasLimit);
|
||||
}
|
||||
if header.number != 0 && header.extra_data.len() as u64 > config.maximum_extra_data_size {
|
||||
return Err(Error::ExtraDataOutOfBounds);
|
||||
}
|
||||
|
||||
// we can't detect if block is from future in runtime
|
||||
// => let's only do an overflow check
|
||||
if header.timestamp > i32::max_value() as u64 {
|
||||
return Err(Error::TimestampOverflow);
|
||||
}
|
||||
|
||||
if chain_time.is_timestamp_ahead(header.timestamp) {
|
||||
return Err(Error::HeaderTimestampIsAhead);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform checks that require access to parent header.
|
||||
fn contextual_checks<Submitter>(
|
||||
config: &AuraConfiguration,
|
||||
context: &ImportContext<Submitter>,
|
||||
validators_override: Option<&[Address]>,
|
||||
header: &AuraHeader,
|
||||
) -> Result<u64, Error> {
|
||||
let validators = validators_override.unwrap_or_else(|| &context.validators_set().validators);
|
||||
let header_step = header.step().ok_or(Error::MissingStep)?;
|
||||
let parent_step = context.parent_header().step().ok_or(Error::MissingStep)?;
|
||||
|
||||
// Ensure header is from the step after context.
|
||||
if header_step == parent_step {
|
||||
return Err(Error::DoubleVote);
|
||||
}
|
||||
#[allow(clippy::suspicious_operation_groupings)]
|
||||
if header.number >= config.validate_step_transition && header_step < parent_step {
|
||||
return Err(Error::DoubleVote);
|
||||
}
|
||||
|
||||
// If empty step messages are enabled we will validate the messages in the seal, missing messages are not
|
||||
// reported as there's no way to tell whether the empty step message was never sent or simply not included.
|
||||
let empty_steps_len = match header.number >= config.empty_steps_transition {
|
||||
true => {
|
||||
let strict_empty_steps = header.number >= config.strict_empty_steps_transition;
|
||||
let empty_steps = header.empty_steps().ok_or(Error::MissingEmptySteps)?;
|
||||
let empty_steps_len = empty_steps.len();
|
||||
let mut prev_empty_step = 0;
|
||||
|
||||
for empty_step in empty_steps {
|
||||
if empty_step.step <= parent_step || empty_step.step >= header_step {
|
||||
return Err(Error::InsufficientProof);
|
||||
}
|
||||
|
||||
if !verify_empty_step(&header.parent_hash, &empty_step, validators) {
|
||||
return Err(Error::InsufficientProof);
|
||||
}
|
||||
|
||||
if strict_empty_steps {
|
||||
if empty_step.step <= prev_empty_step {
|
||||
return Err(Error::InsufficientProof);
|
||||
}
|
||||
|
||||
prev_empty_step = empty_step.step;
|
||||
}
|
||||
}
|
||||
|
||||
empty_steps_len
|
||||
}
|
||||
false => 0,
|
||||
};
|
||||
|
||||
// Validate chain score.
|
||||
if header.number >= config.validate_score_transition {
|
||||
let expected_difficulty = calculate_score(parent_step, header_step, empty_steps_len as _);
|
||||
if header.difficulty != expected_difficulty {
|
||||
return Err(Error::InvalidDifficulty);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(header_step)
|
||||
}
|
||||
|
||||
/// Check that block is produced by expected validator.
|
||||
fn validator_checks(
|
||||
config: &AuraConfiguration,
|
||||
validators: &[Address],
|
||||
header: &AuraHeader,
|
||||
header_step: u64,
|
||||
) -> Result<(), Error> {
|
||||
let expected_validator = *step_validator(validators, header_step);
|
||||
if header.author != expected_validator {
|
||||
return Err(Error::NotValidator);
|
||||
}
|
||||
|
||||
let validator_signature = header.signature().ok_or(Error::MissingSignature)?;
|
||||
let header_seal_hash = header
|
||||
.seal_hash(header.number >= config.empty_steps_transition)
|
||||
.ok_or(Error::MissingEmptySteps)?;
|
||||
let is_invalid_proposer = !verify_signature(&expected_validator, &validator_signature, &header_seal_hash);
|
||||
if is_invalid_proposer {
|
||||
return Err(Error::NotValidator);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns expected number of seal fields in the header.
|
||||
fn expected_header_seal_fields(config: &AuraConfiguration, header: &AuraHeader) -> usize {
|
||||
if header.number != u64::max_value() && header.number >= config.empty_steps_transition {
|
||||
3
|
||||
} else {
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify single sealed empty step.
|
||||
fn verify_empty_step(parent_hash: &H256, step: &SealedEmptyStep, validators: &[Address]) -> bool {
|
||||
let expected_validator = *step_validator(validators, step.step);
|
||||
let message = step.message(parent_hash);
|
||||
verify_signature(&expected_validator, &step.signature, &message)
|
||||
}
|
||||
|
||||
/// Chain scoring: total weight is sqrt(U256::max_value())*height - step
|
||||
pub(crate) fn calculate_score(parent_step: u64, current_step: u64, current_empty_steps: usize) -> U256 {
|
||||
U256::from(U128::max_value()) + U256::from(parent_step) - U256::from(current_step) + U256::from(current_empty_steps)
|
||||
}
|
||||
|
||||
/// Verify that the signature over message has been produced by given validator.
|
||||
fn verify_signature(expected_validator: &Address, signature: &H520, message: &H256) -> bool {
|
||||
secp256k1_ecdsa_recover(signature.as_fixed_bytes(), message.as_fixed_bytes())
|
||||
.map(|public| public_to_address(&public))
|
||||
.map(|address| *expected_validator == address)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Find next unfinalized validators set change after finalized set.
|
||||
fn find_next_validators_signal<S: Storage>(storage: &S, context: &ImportContext<S::Submitter>) -> Option<Vec<Address>> {
|
||||
// that's the earliest block number we may met in following loop
|
||||
// it may be None if that's the first set
|
||||
let best_set_signal_block = context.validators_set().signal_block;
|
||||
|
||||
// if parent schedules validators set change, then it may be our set
|
||||
// else we'll start with last known change
|
||||
let mut current_set_signal_block = context.last_signal_block();
|
||||
let mut next_scheduled_set: Option<AuraScheduledChange> = None;
|
||||
|
||||
loop {
|
||||
// if we have reached block that signals finalized change, then
|
||||
// next_current_block_hash points to the block that schedules next
|
||||
// change
|
||||
let current_scheduled_set = match current_set_signal_block {
|
||||
Some(current_set_signal_block) if Some(¤t_set_signal_block) == best_set_signal_block.as_ref() => {
|
||||
return next_scheduled_set.map(|scheduled_set| scheduled_set.validators)
|
||||
}
|
||||
None => return next_scheduled_set.map(|scheduled_set| scheduled_set.validators),
|
||||
Some(current_set_signal_block) => storage.scheduled_change(¤t_set_signal_block.hash).expect(
|
||||
"header that is associated with this change is not pruned;\
|
||||
scheduled changes are only removed when header is pruned; qed",
|
||||
),
|
||||
};
|
||||
|
||||
current_set_signal_block = current_scheduled_set.prev_signal_block;
|
||||
next_scheduled_set = Some(current_scheduled_set);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{
|
||||
insert_header, run_test_with_genesis, test_aura_config, validator, validator_address, validators_addresses,
|
||||
validators_change_receipt, AccountId, ConstChainTime, HeaderBuilder, TestRuntime, GAS_LIMIT,
|
||||
};
|
||||
use crate::validators::ValidatorsSource;
|
||||
use crate::DefaultInstance;
|
||||
use crate::{
|
||||
pool_configuration, BridgeStorage, FinalizedBlock, Headers, HeadersByNumber, NextValidatorsSetId,
|
||||
ScheduledChanges, ValidatorsSet, ValidatorsSets,
|
||||
};
|
||||
use bp_eth_poa::{compute_merkle_root, rlp_encode, TransactionOutcome, H520, U256};
|
||||
use frame_support::{StorageMap, StorageValue};
|
||||
use hex_literal::hex;
|
||||
use secp256k1::SecretKey;
|
||||
use sp_runtime::transaction_validity::TransactionTag;
|
||||
|
||||
const GENESIS_STEP: u64 = 42;
|
||||
const TOTAL_VALIDATORS: usize = 3;
|
||||
|
||||
fn genesis() -> AuraHeader {
|
||||
HeaderBuilder::genesis().step(GENESIS_STEP).sign_by(&validator(0))
|
||||
}
|
||||
|
||||
fn verify_with_config(config: &AuraConfiguration, header: &AuraHeader) -> Result<ImportContext<AccountId>, Error> {
|
||||
run_test_with_genesis(genesis(), TOTAL_VALIDATORS, |_| {
|
||||
let storage = BridgeStorage::<TestRuntime>::new();
|
||||
verify_aura_header(&storage, &config, None, header, &ConstChainTime::default())
|
||||
})
|
||||
}
|
||||
|
||||
fn default_verify(header: &AuraHeader) -> Result<ImportContext<AccountId>, Error> {
|
||||
verify_with_config(&test_aura_config(), header)
|
||||
}
|
||||
|
||||
fn default_accept_into_pool(
|
||||
mut make_header: impl FnMut(&[SecretKey]) -> (AuraHeader, Option<Vec<Receipt>>),
|
||||
) -> Result<(Vec<TransactionTag>, Vec<TransactionTag>), Error> {
|
||||
run_test_with_genesis(genesis(), TOTAL_VALIDATORS, |_| {
|
||||
let validators = vec![validator(0), validator(1), validator(2)];
|
||||
let mut storage = BridgeStorage::<TestRuntime>::new();
|
||||
let block1 = HeaderBuilder::with_parent_number(0).sign_by_set(&validators);
|
||||
insert_header(&mut storage, block1);
|
||||
let block2 = HeaderBuilder::with_parent_number(1).sign_by_set(&validators);
|
||||
let block2_id = block2.compute_id();
|
||||
insert_header(&mut storage, block2);
|
||||
let block3 = HeaderBuilder::with_parent_number(2).sign_by_set(&validators);
|
||||
insert_header(&mut storage, block3);
|
||||
|
||||
FinalizedBlock::<DefaultInstance>::put(block2_id);
|
||||
|
||||
let validators_config =
|
||||
ValidatorsConfiguration::Single(ValidatorsSource::Contract(Default::default(), Vec::new()));
|
||||
let (header, receipts) = make_header(&validators);
|
||||
accept_aura_header_into_pool(
|
||||
&storage,
|
||||
&test_aura_config(),
|
||||
&validators_config,
|
||||
&pool_configuration(),
|
||||
&header,
|
||||
&(),
|
||||
receipts.as_ref(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn change_validators_set_at(number: u64, finalized_set: Vec<Address>, signalled_set: Option<Vec<Address>>) {
|
||||
let set_id = NextValidatorsSetId::<DefaultInstance>::get();
|
||||
NextValidatorsSetId::<DefaultInstance>::put(set_id + 1);
|
||||
ValidatorsSets::<DefaultInstance>::insert(
|
||||
set_id,
|
||||
ValidatorsSet {
|
||||
validators: finalized_set,
|
||||
signal_block: None,
|
||||
enact_block: HeaderId {
|
||||
number: 0,
|
||||
hash: HeadersByNumber::<DefaultInstance>::get(&0).unwrap()[0],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let header_hash = HeadersByNumber::<DefaultInstance>::get(&number).unwrap()[0];
|
||||
let mut header = Headers::<TestRuntime>::get(&header_hash).unwrap();
|
||||
header.next_validators_set_id = set_id;
|
||||
if let Some(signalled_set) = signalled_set {
|
||||
header.last_signal_block = Some(HeaderId {
|
||||
number: header.header.number - 1,
|
||||
hash: header.header.parent_hash,
|
||||
});
|
||||
ScheduledChanges::<DefaultInstance>::insert(
|
||||
header.header.parent_hash,
|
||||
AuraScheduledChange {
|
||||
validators: signalled_set,
|
||||
prev_signal_block: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Headers::<TestRuntime>::insert(header_hash, header);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifies_seal_count() {
|
||||
// when there are no seals at all
|
||||
let mut header = AuraHeader::default();
|
||||
assert_eq!(default_verify(&header), Err(Error::InvalidSealArity));
|
||||
|
||||
// when there's single seal (we expect 2 or 3 seals)
|
||||
header.seal = vec![vec![]];
|
||||
assert_eq!(default_verify(&header), Err(Error::InvalidSealArity));
|
||||
|
||||
// when there's 3 seals (we expect 2 by default)
|
||||
header.seal = vec![vec![], vec![], vec![]];
|
||||
assert_eq!(default_verify(&header), Err(Error::InvalidSealArity));
|
||||
|
||||
// when there's 2 seals
|
||||
header.seal = vec![vec![], vec![]];
|
||||
assert_ne!(default_verify(&header), Err(Error::InvalidSealArity));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifies_header_number() {
|
||||
// when number is u64::max_value()
|
||||
let header = HeaderBuilder::with_number(u64::max_value()).sign_by(&validator(0));
|
||||
assert_eq!(default_verify(&header), Err(Error::RidiculousNumber));
|
||||
|
||||
// when header is < u64::max_value()
|
||||
let header = HeaderBuilder::with_number(u64::max_value() - 1).sign_by(&validator(0));
|
||||
assert_ne!(default_verify(&header), Err(Error::RidiculousNumber));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifies_gas_used() {
|
||||
// when gas used is larger than gas limit
|
||||
let header = HeaderBuilder::with_number(1)
|
||||
.gas_used((GAS_LIMIT + 1).into())
|
||||
.sign_by(&validator(0));
|
||||
assert_eq!(default_verify(&header), Err(Error::TooMuchGasUsed));
|
||||
|
||||
// when gas used is less than gas limit
|
||||
let header = HeaderBuilder::with_number(1)
|
||||
.gas_used((GAS_LIMIT - 1).into())
|
||||
.sign_by(&validator(0));
|
||||
assert_ne!(default_verify(&header), Err(Error::TooMuchGasUsed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifies_gas_limit() {
|
||||
let mut config = test_aura_config();
|
||||
config.min_gas_limit = 100.into();
|
||||
config.max_gas_limit = 200.into();
|
||||
|
||||
// when limit is lower than expected
|
||||
let header = HeaderBuilder::with_number(1)
|
||||
.gas_limit(50.into())
|
||||
.sign_by(&validator(0));
|
||||
assert_eq!(verify_with_config(&config, &header), Err(Error::InvalidGasLimit));
|
||||
|
||||
// when limit is larger than expected
|
||||
let header = HeaderBuilder::with_number(1)
|
||||
.gas_limit(250.into())
|
||||
.sign_by(&validator(0));
|
||||
assert_eq!(verify_with_config(&config, &header), Err(Error::InvalidGasLimit));
|
||||
|
||||
// when limit is within expected range
|
||||
let header = HeaderBuilder::with_number(1)
|
||||
.gas_limit(150.into())
|
||||
.sign_by(&validator(0));
|
||||
assert_ne!(verify_with_config(&config, &header), Err(Error::InvalidGasLimit));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifies_extra_data_len() {
|
||||
// when extra data is too large
|
||||
let header = HeaderBuilder::with_number(1)
|
||||
.extra_data(std::iter::repeat(42).take(1000).collect::<Vec<_>>())
|
||||
.sign_by(&validator(0));
|
||||
assert_eq!(default_verify(&header), Err(Error::ExtraDataOutOfBounds));
|
||||
|
||||
// when extra data size is OK
|
||||
let header = HeaderBuilder::with_number(1)
|
||||
.extra_data(std::iter::repeat(42).take(10).collect::<Vec<_>>())
|
||||
.sign_by(&validator(0));
|
||||
assert_ne!(default_verify(&header), Err(Error::ExtraDataOutOfBounds));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifies_timestamp() {
|
||||
// when timestamp overflows i32
|
||||
let header = HeaderBuilder::with_number(1)
|
||||
.timestamp(i32::max_value() as u64 + 1)
|
||||
.sign_by(&validator(0));
|
||||
assert_eq!(default_verify(&header), Err(Error::TimestampOverflow));
|
||||
|
||||
// when timestamp doesn't overflow i32
|
||||
let header = HeaderBuilder::with_number(1)
|
||||
.timestamp(i32::max_value() as u64)
|
||||
.sign_by(&validator(0));
|
||||
assert_ne!(default_verify(&header), Err(Error::TimestampOverflow));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifies_chain_time() {
|
||||
// expected import context after verification
|
||||
let expect = ImportContext::<AccountId> {
|
||||
submitter: None,
|
||||
parent_hash: hex!("6e41bff05578fc1db17f6816117969b07d2217f1f9039d8116a82764335991d3").into(),
|
||||
parent_header: genesis(),
|
||||
parent_total_difficulty: U256::zero(),
|
||||
parent_scheduled_change: None,
|
||||
validators_set_id: 0,
|
||||
validators_set: ValidatorsSet {
|
||||
validators: vec![
|
||||
hex!("dc5b20847f43d67928f49cd4f85d696b5a7617b5").into(),
|
||||
hex!("897df33a7b3c62ade01e22c13d48f98124b4480f").into(),
|
||||
hex!("05c987b34c6ef74e0c7e69c6e641120c24164c2d").into(),
|
||||
],
|
||||
signal_block: None,
|
||||
enact_block: HeaderId {
|
||||
number: 0,
|
||||
hash: hex!("6e41bff05578fc1db17f6816117969b07d2217f1f9039d8116a82764335991d3").into(),
|
||||
},
|
||||
},
|
||||
last_signal_block: None,
|
||||
};
|
||||
|
||||
// header is behind
|
||||
let header = HeaderBuilder::with_parent(&genesis())
|
||||
.timestamp(i32::max_value() as u64 / 2 - 100)
|
||||
.sign_by(&validator(1));
|
||||
assert_eq!(default_verify(&header).unwrap(), expect);
|
||||
|
||||
// header is ahead
|
||||
let header = HeaderBuilder::with_parent(&genesis())
|
||||
.timestamp(i32::max_value() as u64 / 2 + 100)
|
||||
.sign_by(&validator(1));
|
||||
assert_eq!(default_verify(&header), Err(Error::HeaderTimestampIsAhead));
|
||||
|
||||
// header has same timestamp as ConstChainTime
|
||||
let header = HeaderBuilder::with_parent(&genesis())
|
||||
.timestamp(i32::max_value() as u64 / 2)
|
||||
.sign_by(&validator(1));
|
||||
assert_eq!(default_verify(&header).unwrap(), expect);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifies_parent_existence() {
|
||||
// when there's no parent in the storage
|
||||
let header = HeaderBuilder::with_number(1).sign_by(&validator(0));
|
||||
assert_eq!(default_verify(&header), Err(Error::MissingParentBlock));
|
||||
|
||||
// when parent is in the storage
|
||||
let header = HeaderBuilder::with_parent(&genesis()).sign_by(&validator(0));
|
||||
assert_ne!(default_verify(&header), Err(Error::MissingParentBlock));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifies_step() {
|
||||
// when step is missing from seals
|
||||
let mut header = AuraHeader {
|
||||
seal: vec![vec![], vec![]],
|
||||
gas_limit: test_aura_config().min_gas_limit,
|
||||
parent_hash: genesis().compute_hash(),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(default_verify(&header), Err(Error::MissingStep));
|
||||
|
||||
// when step is the same as for the parent block
|
||||
header.seal[0] = rlp_encode(&42u64).to_vec();
|
||||
assert_eq!(default_verify(&header), Err(Error::DoubleVote));
|
||||
|
||||
// when step is OK
|
||||
header.seal[0] = rlp_encode(&43u64).to_vec();
|
||||
assert_ne!(default_verify(&header), Err(Error::DoubleVote));
|
||||
|
||||
// now check with validate_step check enabled
|
||||
let mut config = test_aura_config();
|
||||
config.validate_step_transition = 0;
|
||||
|
||||
// when step is lesser that for the parent block
|
||||
header.seal[0] = rlp_encode(&40u64).to_vec();
|
||||
header.seal = vec![vec![40], vec![]];
|
||||
assert_eq!(verify_with_config(&config, &header), Err(Error::DoubleVote));
|
||||
|
||||
// when step is OK
|
||||
header.seal[0] = rlp_encode(&44u64).to_vec();
|
||||
assert_ne!(verify_with_config(&config, &header), Err(Error::DoubleVote));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifies_empty_step() {
|
||||
let mut config = test_aura_config();
|
||||
config.empty_steps_transition = 0;
|
||||
|
||||
// when empty step duplicates parent step
|
||||
let header = HeaderBuilder::with_parent(&genesis())
|
||||
.empty_steps(&[(&validator(0), GENESIS_STEP)])
|
||||
.step(GENESIS_STEP + 3)
|
||||
.sign_by(&validator(3));
|
||||
assert_eq!(verify_with_config(&config, &header), Err(Error::InsufficientProof));
|
||||
|
||||
// when empty step signature check fails
|
||||
let header = HeaderBuilder::with_parent(&genesis())
|
||||
.empty_steps(&[(&validator(100), GENESIS_STEP + 1)])
|
||||
.step(GENESIS_STEP + 3)
|
||||
.sign_by(&validator(3));
|
||||
assert_eq!(verify_with_config(&config, &header), Err(Error::InsufficientProof));
|
||||
|
||||
// when we are accepting strict empty steps and they come not in order
|
||||
config.strict_empty_steps_transition = 0;
|
||||
let header = HeaderBuilder::with_parent(&genesis())
|
||||
.empty_steps(&[(&validator(2), GENESIS_STEP + 2), (&validator(1), GENESIS_STEP + 1)])
|
||||
.step(GENESIS_STEP + 3)
|
||||
.sign_by(&validator(3));
|
||||
assert_eq!(verify_with_config(&config, &header), Err(Error::InsufficientProof));
|
||||
|
||||
// when empty steps are OK
|
||||
let header = HeaderBuilder::with_parent(&genesis())
|
||||
.empty_steps(&[(&validator(1), GENESIS_STEP + 1), (&validator(2), GENESIS_STEP + 2)])
|
||||
.step(GENESIS_STEP + 3)
|
||||
.sign_by(&validator(3));
|
||||
assert_ne!(verify_with_config(&config, &header), Err(Error::InsufficientProof));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifies_chain_score() {
|
||||
let mut config = test_aura_config();
|
||||
config.validate_score_transition = 0;
|
||||
|
||||
// when chain score is invalid
|
||||
let header = HeaderBuilder::with_parent(&genesis())
|
||||
.difficulty(100.into())
|
||||
.sign_by(&validator(0));
|
||||
assert_eq!(verify_with_config(&config, &header), Err(Error::InvalidDifficulty));
|
||||
|
||||
// when chain score is accepted
|
||||
let header = HeaderBuilder::with_parent(&genesis()).sign_by(&validator(0));
|
||||
assert_ne!(verify_with_config(&config, &header), Err(Error::InvalidDifficulty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifies_validator() {
|
||||
let good_header = HeaderBuilder::with_parent(&genesis()).sign_by(&validator(1));
|
||||
|
||||
// when header author is invalid
|
||||
let mut header = good_header.clone();
|
||||
header.author = Default::default();
|
||||
assert_eq!(default_verify(&header), Err(Error::NotValidator));
|
||||
|
||||
// when header signature is invalid
|
||||
let mut header = good_header.clone();
|
||||
header.seal[1] = rlp_encode(&H520::default()).to_vec();
|
||||
assert_eq!(default_verify(&header), Err(Error::NotValidator));
|
||||
|
||||
// when everything is OK
|
||||
assert_eq!(default_verify(&good_header).map(|_| ()), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_verifies_known_blocks() {
|
||||
// when header is known
|
||||
assert_eq!(
|
||||
default_accept_into_pool(|validators| (HeaderBuilder::with_parent_number(2).sign_by_set(validators), None)),
|
||||
Err(Error::KnownHeader),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_verifies_ancient_blocks() {
|
||||
// when header number is less than finalized
|
||||
assert_eq!(
|
||||
default_accept_into_pool(|validators| (
|
||||
HeaderBuilder::with_parent_number(1)
|
||||
.gas_limit((GAS_LIMIT + 1).into())
|
||||
.sign_by_set(validators),
|
||||
None,
|
||||
),),
|
||||
Err(Error::AncientHeader),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_rejects_headers_without_required_receipts() {
|
||||
assert_eq!(
|
||||
default_accept_into_pool(|_| (
|
||||
AuraHeader {
|
||||
number: 20_000_000,
|
||||
seal: vec![vec![], vec![]],
|
||||
gas_limit: test_aura_config().min_gas_limit,
|
||||
log_bloom: (&[0xff; 256]).into(),
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
),),
|
||||
Err(Error::MissingTransactionsReceipts),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_rejects_headers_with_redundant_receipts() {
|
||||
assert_eq!(
|
||||
default_accept_into_pool(|validators| (
|
||||
HeaderBuilder::with_parent_number(3).sign_by_set(validators),
|
||||
Some(vec![Receipt {
|
||||
gas_used: 1.into(),
|
||||
log_bloom: (&[0xff; 256]).into(),
|
||||
logs: vec![],
|
||||
outcome: TransactionOutcome::Unknown,
|
||||
}]),
|
||||
),),
|
||||
Err(Error::RedundantTransactionsReceipts),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_verifies_future_block_number() {
|
||||
// when header is too far from the future
|
||||
assert_eq!(
|
||||
default_accept_into_pool(|validators| (HeaderBuilder::with_number(100).sign_by_set(&validators), None),),
|
||||
Err(Error::UnsignedTooFarInTheFuture),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_performs_full_verification_when_parent_is_known() {
|
||||
// if parent is known, then we'll execute contextual_checks, which
|
||||
// checks for DoubleVote
|
||||
assert_eq!(
|
||||
default_accept_into_pool(|validators| (
|
||||
HeaderBuilder::with_parent_number(3)
|
||||
.step(GENESIS_STEP + 3)
|
||||
.sign_by_set(&validators),
|
||||
None,
|
||||
),),
|
||||
Err(Error::DoubleVote),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_performs_validators_checks_when_parent_is_unknown() {
|
||||
// if parent is unknown, then we still need to check if header has required signature
|
||||
// (even if header will be considered invalid/duplicate later, we can use this signature
|
||||
// as a proof of malicious action by this validator)
|
||||
assert_eq!(
|
||||
default_accept_into_pool(|_| (HeaderBuilder::with_number(8).step(8).sign_by(&validator(1)), None,)),
|
||||
Err(Error::NotValidator),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_verifies_header_with_known_parent() {
|
||||
let mut hash = None;
|
||||
assert_eq!(
|
||||
default_accept_into_pool(|validators| {
|
||||
let header = HeaderBuilder::with_parent_number(3).sign_by_set(validators);
|
||||
hash = Some(header.compute_hash());
|
||||
(header, None)
|
||||
}),
|
||||
Ok((
|
||||
// no tags are required
|
||||
vec![],
|
||||
// header provides two tags
|
||||
vec![
|
||||
(4u64, validators_addresses(3)[1]).encode(),
|
||||
(4u64, hash.unwrap()).encode(),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_verifies_header_with_unknown_parent() {
|
||||
let mut id = None;
|
||||
let mut parent_id = None;
|
||||
assert_eq!(
|
||||
default_accept_into_pool(|validators| {
|
||||
let header = HeaderBuilder::with_number(5)
|
||||
.step(GENESIS_STEP + 5)
|
||||
.sign_by_set(validators);
|
||||
id = Some(header.compute_id());
|
||||
parent_id = header.parent_id();
|
||||
(header, None)
|
||||
}),
|
||||
Ok((
|
||||
// parent tag required
|
||||
vec![parent_id.unwrap().encode()],
|
||||
// header provides two tags
|
||||
vec![(5u64, validator_address(2)).encode(), id.unwrap().encode(),],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_uses_next_validators_set_when_finalized_fails() {
|
||||
assert_eq!(
|
||||
default_accept_into_pool(|actual_validators| {
|
||||
// change finalized set at parent header
|
||||
change_validators_set_at(3, validators_addresses(1), None);
|
||||
|
||||
// header is signed using wrong set
|
||||
let header = HeaderBuilder::with_number(5)
|
||||
.step(GENESIS_STEP + 2)
|
||||
.sign_by_set(actual_validators);
|
||||
|
||||
(header, None)
|
||||
}),
|
||||
Err(Error::NotValidator),
|
||||
);
|
||||
|
||||
let mut id = None;
|
||||
let mut parent_id = None;
|
||||
assert_eq!(
|
||||
default_accept_into_pool(|actual_validators| {
|
||||
// change finalized set at parent header + signal valid set at parent block
|
||||
change_validators_set_at(3, validators_addresses(10), Some(validators_addresses(3)));
|
||||
|
||||
// header is signed using wrong set
|
||||
let header = HeaderBuilder::with_number(5)
|
||||
.step(GENESIS_STEP + 2)
|
||||
.sign_by_set(actual_validators);
|
||||
id = Some(header.compute_id());
|
||||
parent_id = header.parent_id();
|
||||
|
||||
(header, None)
|
||||
}),
|
||||
Ok((
|
||||
// parent tag required
|
||||
vec![parent_id.unwrap().encode(),],
|
||||
// header provides two tags
|
||||
vec![(5u64, validator_address(2)).encode(), id.unwrap().encode(),],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_rejects_headers_with_invalid_receipts() {
|
||||
assert_eq!(
|
||||
default_accept_into_pool(|validators| {
|
||||
let header = HeaderBuilder::with_parent_number(3)
|
||||
.log_bloom((&[0xff; 256]).into())
|
||||
.sign_by_set(validators);
|
||||
(header, Some(vec![validators_change_receipt(Default::default())]))
|
||||
}),
|
||||
Err(Error::TransactionsReceiptsMismatch),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_accepts_headers_with_valid_receipts() {
|
||||
let mut hash = None;
|
||||
let receipts = vec![validators_change_receipt(Default::default())];
|
||||
let receipts_root = compute_merkle_root(receipts.iter().map(|r| r.rlp()));
|
||||
|
||||
assert_eq!(
|
||||
default_accept_into_pool(|validators| {
|
||||
let header = HeaderBuilder::with_parent_number(3)
|
||||
.log_bloom((&[0xff; 256]).into())
|
||||
.receipts_root(receipts_root)
|
||||
.sign_by_set(validators);
|
||||
hash = Some(header.compute_hash());
|
||||
(header, Some(receipts.clone()))
|
||||
}),
|
||||
Ok((
|
||||
// no tags are required
|
||||
vec![],
|
||||
// header provides two tags
|
||||
vec![
|
||||
(4u64, validators_addresses(3)[1]).encode(),
|
||||
(4u64, hash.unwrap()).encode(),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
[package]
|
||||
name = "pallet-bridge-grandpa"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false }
|
||||
finality-grandpa = { version = "0.14.0", default-features = false }
|
||||
log = { version = "0.4.14", default-features = false }
|
||||
num-traits = { version = "0.2", default-features = false }
|
||||
serde = { version = "1.0", optional = true }
|
||||
|
||||
# Bridge Dependencies
|
||||
|
||||
bp-runtime = { path = "../../primitives/runtime", default-features = false }
|
||||
bp-header-chain = { path = "../../primitives/header-chain", default-features = false }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
frame-support = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
frame-system = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-finality-grandpa = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-std = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-trie = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
|
||||
# Optional Benchmarking Dependencies
|
||||
bp-test-utils = { path = "../../primitives/test-utils", default-features = false, optional = true }
|
||||
frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
sp-io = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"bp-header-chain/std",
|
||||
"bp-runtime/std",
|
||||
"bp-test-utils/std",
|
||||
"codec/std",
|
||||
"finality-grandpa/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"log/std",
|
||||
"num-traits/std",
|
||||
"serde",
|
||||
"sp-finality-grandpa/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
"sp-trie/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"bp-test-utils",
|
||||
"frame-benchmarking",
|
||||
]
|
||||
@@ -0,0 +1,272 @@
|
||||
// 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/>.
|
||||
|
||||
//! Benchmarks for the GRANDPA Pallet.
|
||||
//!
|
||||
//! The main dispatchable for the GRANDPA pallet is `submit_finality_proof`, so these benchmarks are
|
||||
//! based around that. 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 decendant 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.
|
||||
//!
|
||||
//! As far as benchmarking results go, the only benchmark that should be used in
|
||||
//! `pallet-bridge-grandpa` to annotate weights is the `submit_finality_proof` one. The others are
|
||||
//! looking at the effects of specific code paths and do not actually reflect the overall worst case
|
||||
//! scenario.
|
||||
|
||||
use crate::*;
|
||||
|
||||
use bp_test_utils::{
|
||||
accounts, authority_list, make_justification_for_header, test_keyring, JustificationGeneratorParams, ALICE,
|
||||
TEST_GRANDPA_ROUND, TEST_GRANDPA_SET_ID,
|
||||
};
|
||||
use frame_benchmarking::{benchmarks_instance_pallet, whitelisted_caller};
|
||||
use frame_system::RawOrigin;
|
||||
use sp_finality_grandpa::AuthorityId;
|
||||
use sp_runtime::traits::{One, Zero};
|
||||
use sp_std::{vec, 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;
|
||||
|
||||
// The maximum number of pre-commits to include in a justification. In practice this scales with the
|
||||
// number of validators.
|
||||
const MAX_VALIDATOR_SET_SIZE: u32 = 1024;
|
||||
|
||||
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 pallet.
|
||||
//
|
||||
// The other benchmarks related to `submit_finality_proof` are looking at the effect of specific
|
||||
// parameters and are there mostly for seeing how specific codepaths behave.
|
||||
submit_finality_proof {
|
||||
let v in 1..MAX_VOTE_ANCESTRIES;
|
||||
let p in 1..MAX_VALIDATOR_SET_SIZE;
|
||||
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
|
||||
let authority_list = accounts(p as u16)
|
||||
.iter()
|
||||
.map(|id| (AuthorityId::from(*id), 1))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let init_data = InitializationData {
|
||||
header: bp_test_utils::test_header(Zero::zero()),
|
||||
authority_list,
|
||||
set_id: TEST_GRANDPA_SET_ID,
|
||||
is_halted: false,
|
||||
};
|
||||
|
||||
initialize_bridge::<T, I>(init_data);
|
||||
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(p as u16).iter().map(|k| (*k, 1)).collect::<Vec<_>>(),
|
||||
votes: v,
|
||||
forks: 1,
|
||||
};
|
||||
|
||||
let justification = make_justification_for_header(params);
|
||||
|
||||
}: _(RawOrigin::Signed(caller), header, justification)
|
||||
verify {
|
||||
let header: BridgedHeader<T, I> = bp_test_utils::test_header(One::one());
|
||||
let expected_hash = header.hash();
|
||||
|
||||
assert_eq!(<BestFinalized<T, I>>::get(), expected_hash);
|
||||
assert!(<ImportedHeaders<T, I>>::contains_key(expected_hash));
|
||||
}
|
||||
|
||||
// What we want to check here is the effect of vote ancestries on justification verification
|
||||
// do this by varying the number of headers between `finality_target` and `header_of_chain`.
|
||||
submit_finality_proof_on_single_fork {
|
||||
let v in 1..MAX_VOTE_ANCESTRIES;
|
||||
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
|
||||
let init_data = InitializationData {
|
||||
header: bp_test_utils::test_header(Zero::zero()),
|
||||
authority_list: authority_list(),
|
||||
set_id: TEST_GRANDPA_SET_ID,
|
||||
is_halted: false,
|
||||
};
|
||||
|
||||
initialize_bridge::<T, I>(init_data);
|
||||
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: test_keyring(),
|
||||
votes: v,
|
||||
forks: 1,
|
||||
};
|
||||
|
||||
let justification = make_justification_for_header(params);
|
||||
|
||||
}: submit_finality_proof(RawOrigin::Signed(caller), header, justification)
|
||||
verify {
|
||||
let header: BridgedHeader<T, I> = bp_test_utils::test_header(One::one());
|
||||
let expected_hash = header.hash();
|
||||
|
||||
assert_eq!(<BestFinalized<T, I>>::get(), expected_hash);
|
||||
assert!(<ImportedHeaders<T, I>>::contains_key(expected_hash));
|
||||
}
|
||||
|
||||
// What we want to check here is the effect of many pre-commits on justification verification.
|
||||
// We do this by creating many forks, whose head will be used as a signed pre-commit in the
|
||||
// final justification.
|
||||
submit_finality_proof_on_many_forks {
|
||||
let p in 1..MAX_VALIDATOR_SET_SIZE;
|
||||
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
|
||||
let authority_list = accounts(p as u16)
|
||||
.iter()
|
||||
.map(|id| (AuthorityId::from(*id), 1))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let init_data = InitializationData {
|
||||
header: bp_test_utils::test_header(Zero::zero()),
|
||||
authority_list,
|
||||
set_id: TEST_GRANDPA_SET_ID,
|
||||
is_halted: false,
|
||||
};
|
||||
|
||||
initialize_bridge::<T, I>(init_data);
|
||||
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(p as u16).iter().map(|k| (*k, 1)).collect::<Vec<_>>(),
|
||||
votes: p,
|
||||
forks: p,
|
||||
};
|
||||
|
||||
let justification = make_justification_for_header(params);
|
||||
|
||||
}: submit_finality_proof(RawOrigin::Signed(caller), header, justification)
|
||||
verify {
|
||||
let header: BridgedHeader<T, I> = bp_test_utils::test_header(One::one());
|
||||
let expected_hash = header.hash();
|
||||
|
||||
assert_eq!(<BestFinalized<T, I>>::get(), expected_hash);
|
||||
assert!(<ImportedHeaders<T, I>>::contains_key(expected_hash));
|
||||
}
|
||||
|
||||
// Here we want to find out the overheaded of looking through consensus digests found in a
|
||||
// header. As the number of logs in a header grows, how much more work do we require to look
|
||||
// through them?
|
||||
//
|
||||
// Note that this should be the same for looking through scheduled changes and forces changes,
|
||||
// which is why we only have one benchmark for this.
|
||||
find_scheduled_change {
|
||||
// Not really sure what a good bound for this is.
|
||||
let n in 1..1000;
|
||||
|
||||
let mut logs = vec![];
|
||||
for i in 0..n {
|
||||
// We chose a non-consensus log on purpose since that way we have to look through all
|
||||
// the logs in the header
|
||||
logs.push(sp_runtime::DigestItem::Other(vec![]));
|
||||
}
|
||||
|
||||
let mut header: BridgedHeader<T, I> = bp_test_utils::test_header(Zero::zero());
|
||||
let digest = header.digest_mut();
|
||||
*digest = sp_runtime::Digest {
|
||||
logs,
|
||||
};
|
||||
|
||||
}: {
|
||||
crate::find_scheduled_change(&header)
|
||||
}
|
||||
|
||||
// What we want to check here is how long it takes to read and write the authority set tracked
|
||||
// by the pallet as the number of authorities grows.
|
||||
read_write_authority_sets {
|
||||
// The current max target number of validators on Polkadot/Kusama
|
||||
let n in 1..1000;
|
||||
|
||||
let mut authorities = vec![];
|
||||
for i in 0..n {
|
||||
authorities.push((ALICE, 1));
|
||||
}
|
||||
|
||||
let authority_set = bp_header_chain::AuthoritySet {
|
||||
authorities: authorities.iter().map(|(id, w)| (AuthorityId::from(*id), *w)).collect(),
|
||||
set_id: 0
|
||||
};
|
||||
|
||||
<CurrentAuthoritySet<T, I>>::put(&authority_set);
|
||||
|
||||
}: {
|
||||
let authority_set = <CurrentAuthoritySet<T, I>>::get();
|
||||
<CurrentAuthoritySet<T, I>>::put(&authority_set);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use frame_support::assert_ok;
|
||||
|
||||
#[test]
|
||||
fn finality_proof_is_valid() {
|
||||
mock::run_test(|| {
|
||||
assert_ok!(test_benchmark_submit_finality_proof::<mock::TestRuntime>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_fork_finality_proof_is_valid() {
|
||||
mock::run_test(|| {
|
||||
assert_ok!(test_benchmark_submit_finality_proof_on_single_fork::<mock::TestRuntime>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_fork_finality_proof_is_valid() {
|
||||
mock::run_test(|| {
|
||||
assert_ok!(test_benchmark_submit_finality_proof_on_many_forks::<mock::TestRuntime>());
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
||||
// 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/>.
|
||||
|
||||
// From construct_runtime macro
|
||||
#![allow(clippy::from_over_into)]
|
||||
|
||||
use bp_runtime::Chain;
|
||||
use frame_support::{construct_runtime, parameter_types, weights::Weight};
|
||||
use sp_runtime::{
|
||||
testing::{Header, H256},
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
Perbill,
|
||||
};
|
||||
|
||||
pub type AccountId = u64;
|
||||
pub type TestHeader = crate::BridgedHeader<TestRuntime, ()>;
|
||||
pub type TestNumber = crate::BridgedBlockNumber<TestRuntime, ()>;
|
||||
pub type TestHash = crate::BridgedBlockHash<TestRuntime, ()>;
|
||||
|
||||
type Block = frame_system::mocking::MockBlock<TestRuntime>;
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<TestRuntime>;
|
||||
|
||||
use crate as grandpa;
|
||||
|
||||
construct_runtime! {
|
||||
pub enum TestRuntime where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
|
||||
Grandpa: grandpa::{Pallet},
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub const MaximumBlockWeight: Weight = 1024;
|
||||
pub const MaximumBlockLength: u32 = 2 * 1024;
|
||||
pub const AvailableBlockRatio: Perbill = Perbill::one();
|
||||
}
|
||||
|
||||
impl frame_system::Config for TestRuntime {
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type Call = Call;
|
||||
type BlockNumber = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type Event = ();
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = ();
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type BaseCallFilter = ();
|
||||
type SystemWeightInfo = ();
|
||||
type DbWeight = ();
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const MaxRequests: u32 = 2;
|
||||
pub const HeadersToKeep: u32 = 5;
|
||||
pub const SessionLength: u64 = 5;
|
||||
pub const NumValidators: u32 = 5;
|
||||
}
|
||||
|
||||
impl grandpa::Config for TestRuntime {
|
||||
type BridgedChain = TestBridgedChain;
|
||||
type MaxRequests = MaxRequests;
|
||||
type HeadersToKeep = HeadersToKeep;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TestBridgedChain;
|
||||
|
||||
impl Chain for TestBridgedChain {
|
||||
type BlockNumber = <TestRuntime as frame_system::Config>::BlockNumber;
|
||||
type Hash = <TestRuntime as frame_system::Config>::Hash;
|
||||
type Hasher = <TestRuntime as frame_system::Config>::Hashing;
|
||||
type Header = <TestRuntime as frame_system::Config>::Header;
|
||||
}
|
||||
|
||||
pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
|
||||
sp_io::TestExternalities::new(Default::default()).execute_with(test)
|
||||
}
|
||||
|
||||
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,121 @@
|
||||
// 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/>.
|
||||
|
||||
//! Autogenerated weights for pallet_bridge_grandpa
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0
|
||||
//! DATE: 2021-04-14, STEPS: [50, ], REPEAT: 20
|
||||
//! LOW RANGE: [], HIGH RANGE: []
|
||||
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled
|
||||
//! CHAIN: Some("dev"), DB CACHE: 128
|
||||
|
||||
// Executed Command:
|
||||
// target/release/rialto-bridge-node
|
||||
// benchmark
|
||||
// --chain=dev
|
||||
// --steps=50
|
||||
// --repeat=20
|
||||
// --pallet=pallet_bridge_grandpa
|
||||
// --extrinsic=*
|
||||
// --execution=wasm
|
||||
// --wasm-execution=Compiled
|
||||
// --heap-pages=4096
|
||||
// --output=./modules/grandpa/src/weights.rs
|
||||
// --template=./.maintain/rialto-weight-template.hbs
|
||||
|
||||
#![allow(clippy::all)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
|
||||
use frame_support::{
|
||||
traits::Get,
|
||||
weights::{constants::RocksDbWeight, Weight},
|
||||
};
|
||||
use sp_std::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for pallet_bridge_grandpa.
|
||||
pub trait WeightInfo {
|
||||
fn submit_finality_proof(v: u32, p: u32) -> Weight;
|
||||
fn submit_finality_proof_on_single_fork(v: u32) -> Weight;
|
||||
fn submit_finality_proof_on_many_forks(p: u32) -> Weight;
|
||||
fn find_scheduled_change(n: u32) -> Weight;
|
||||
fn read_write_authority_sets(n: u32) -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for pallet_bridge_grandpa using the Rialto node and recommended hardware.
|
||||
pub struct RialtoWeight<T>(PhantomData<T>);
|
||||
impl<T: frame_system::Config> WeightInfo for RialtoWeight<T> {
|
||||
fn submit_finality_proof(v: u32, p: u32) -> Weight {
|
||||
(0 as Weight)
|
||||
.saturating_add((837_084_000 as Weight).saturating_mul(v as Weight))
|
||||
.saturating_add((874_929_000 as Weight).saturating_mul(p as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(7 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(5 as Weight))
|
||||
}
|
||||
fn submit_finality_proof_on_single_fork(v: u32) -> Weight {
|
||||
(276_463_000 as Weight)
|
||||
.saturating_add((14_149_000 as Weight).saturating_mul(v as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(7 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(5 as Weight))
|
||||
}
|
||||
fn submit_finality_proof_on_many_forks(p: u32) -> Weight {
|
||||
(10_676_019_000 as Weight)
|
||||
.saturating_add((97_598_000 as Weight).saturating_mul(p as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(7 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(5 as Weight))
|
||||
}
|
||||
fn find_scheduled_change(n: u32) -> Weight {
|
||||
(618_000 as Weight).saturating_add((8_000 as Weight).saturating_mul(n as Weight))
|
||||
}
|
||||
fn read_write_authority_sets(n: u32) -> Weight {
|
||||
(8_582_000 as Weight)
|
||||
.saturating_add((234_000 as Weight).saturating_mul(n as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(1 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests
|
||||
impl WeightInfo for () {
|
||||
fn submit_finality_proof(v: u32, p: u32) -> Weight {
|
||||
(0 as Weight)
|
||||
.saturating_add((837_084_000 as Weight).saturating_mul(v as Weight))
|
||||
.saturating_add((874_929_000 as Weight).saturating_mul(p as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(7 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(5 as Weight))
|
||||
}
|
||||
fn submit_finality_proof_on_single_fork(v: u32) -> Weight {
|
||||
(276_463_000 as Weight)
|
||||
.saturating_add((14_149_000 as Weight).saturating_mul(v as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(7 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(5 as Weight))
|
||||
}
|
||||
fn submit_finality_proof_on_many_forks(p: u32) -> Weight {
|
||||
(10_676_019_000 as Weight)
|
||||
.saturating_add((97_598_000 as Weight).saturating_mul(p as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(7 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(5 as Weight))
|
||||
}
|
||||
fn find_scheduled_change(n: u32) -> Weight {
|
||||
(618_000 as Weight).saturating_add((8_000 as Weight).saturating_mul(n as Weight))
|
||||
}
|
||||
fn read_write_authority_sets(n: u32) -> Weight {
|
||||
(8_582_000 as Weight)
|
||||
.saturating_add((234_000 as Weight).saturating_mul(n as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(1 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
[package]
|
||||
name = "pallet-bridge-messages"
|
||||
description = "Module that allows bridged chains to exchange messages using lane concept."
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false }
|
||||
log = { version = "0.4.14", default-features = false }
|
||||
num-traits = { version = "0.2", default-features = false }
|
||||
serde = { version = "1.0.101", optional = true, features = ["derive"] }
|
||||
|
||||
# Bridge dependencies
|
||||
|
||||
bp-messages = { path = "../../primitives/messages", default-features = false }
|
||||
bp-rialto = { path = "../../primitives/chain-rialto", default-features = false }
|
||||
bp-runtime = { path = "../../primitives/runtime", default-features = false }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false, optional = true }
|
||||
frame-support = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
frame-system = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-std = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = "0.4"
|
||||
hex-literal = "0.3"
|
||||
sp-io = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"bp-messages/std",
|
||||
"bp-runtime/std",
|
||||
"bp-rialto/std",
|
||||
"codec/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"log/std",
|
||||
"num-traits/std",
|
||||
"serde",
|
||||
"sp-core/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"frame-benchmarking",
|
||||
]
|
||||
@@ -0,0 +1,391 @@
|
||||
# Messages Module
|
||||
|
||||
The messages module 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 an 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.
|
||||
|
||||
## Message Workflow
|
||||
|
||||
The message "appears" when its submitter calls the `send_message()` function of the module. The
|
||||
submitter specifies the lane that he's willing to use, the message itself and the fee that he's
|
||||
willing to pay for the message delivery and dispatch. If a message passes all checks, 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 pallet 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
|
||||
his 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 he 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, he must prove the fact
|
||||
that he 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 Substrate-to-Substrate bridge we're using runtime storage proofs. Other bridges may use
|
||||
transaction proofs, Substrate 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 interested in well-probed and relatively easy integration of two
|
||||
Substrate-based chains, you may want to look at the
|
||||
[bridge-runtime-common](../../bin/runtime-common/README.md) 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).
|
||||
|
||||
Message submitters may track message progress by inspecting module events. When Message is accepted,
|
||||
the `MessageAccepted` event is emitted in the `send_message()` transaction. 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.
|
||||
|
||||
### How to plug-in Messages Module to Send Messages to the Bridged Chain?
|
||||
|
||||
The `pallet_bridge_messages::Config` trait has 3 main associated types that are used to work with
|
||||
outbound messages. The `pallet_bridge_messages::Config::TargetHeaderChain` defines how we see the
|
||||
bridged chain as the target for our outbound messages. It must be able to check that the bridged
|
||||
chain may accept our message - like that the message has size below maximal possible transaction
|
||||
size of the chain and so on. And when the relayer sends us a confirmation transaction, this
|
||||
implementation must be able to parse and verify the proof of messages delivery. Normally, you would
|
||||
reuse the same (configurable) type on all chains that are sending messages to the same bridged
|
||||
chain.
|
||||
|
||||
The `pallet_bridge_messages::Config::LaneMessageVerifier` defines a single callback to verify outbound
|
||||
messages. The simplest callback may just accept all messages. But in this case you'll need to answer
|
||||
many questions first. Who will pay for the delivery and confirmation transaction? Are we sure that
|
||||
someone will ever deliver this message to the bridged chain? Are we sure that we don't bloat our
|
||||
runtime storage by accepting this message? What if the message is improperly encoded or has some
|
||||
fields set to invalid values? Answering all those (and similar) questions would lead to correct
|
||||
implementation.
|
||||
|
||||
There's another thing to consider when implementing type for use in
|
||||
`pallet_bridge_messages::Config::LaneMessageVerifier`. It is whether we treat all message lanes
|
||||
identically, or they'll have different sets of verification rules? For example, you may reserve
|
||||
lane#1 for messages coming from some 'wrapped-token' pallet - then you may verify in your
|
||||
implementation that the origin is associated with this pallet. Lane#2 may be reserved for 'system'
|
||||
messages and you may charge zero fee for such messages. You may have some rate limiting for messages
|
||||
sent over the lane#3. Or you may just verify the same rules set for all outbound messages - it is
|
||||
all up to the `pallet_bridge_messages::Config::LaneMessageVerifier` implementation.
|
||||
|
||||
The last type is the `pallet_bridge_messages::Config::MessageDeliveryAndDispatchPayment`. When all
|
||||
checks are made and we have decided to accept the message, we're calling the
|
||||
`pay_delivery_and_dispatch_fee()` callback, passing the corresponding argument of the `send_message`
|
||||
function. Later, when message delivery is confirmed, we're calling `pay_relayers_rewards()`
|
||||
callback, passing accounts of relayers and messages that they have delivered. The simplest
|
||||
implementation of this trait is in the [`instant_payments.rs`](./src/instant_payments.rs) module and
|
||||
simply calls `Currency::transfer()` when those callbacks are called. So `Currency` units are
|
||||
transferred between submitter, 'relayers fund' and relayers accounts. Other implementations may use
|
||||
more or less sophisticated techniques - the whole relayers incentivization scheme is not a part of
|
||||
the messages module.
|
||||
|
||||
### 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.
|
||||
|
||||
### How to plug-in Messages Module to 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::Config::SourceHeaderChain` defines how we see the
|
||||
bridged chain as the source or our inbound messages. When relayer sends us a delivery transaction,
|
||||
this implementation must be able to parse and verify the proof of messages wrapped in this
|
||||
transaction. Normally, you would reuse the same (configurable) type on all chains that are sending
|
||||
messages to the same bridged chain.
|
||||
|
||||
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.
|
||||
|
||||
### 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?
|
||||
|
||||
Message is being stored in the source chain storage until its delivery will be confirmed. After
|
||||
that, we may safely remove the message from the storage. Lane messages are removed (pruned) when
|
||||
someone sends a new message using the same lane. So the message submitter pays for that pruning. To
|
||||
avoid pruning too many messages in a single transaction, there's
|
||||
`pallet_bridge_messages::Config::MaxMessagesToPruneAtOnce` configuration parameter. We will never prune
|
||||
more than this number of messages in the single transaction. That said, the value should not be too
|
||||
big to avoid waste of resources when there are no messages to prune.
|
||||
|
||||
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::ConfigMaxUnrewardedRelayerEntriesAtInboundLane` 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::MaxUnconfirmedMessagesAtInboundLane` 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 Substrate-based chains with
|
||||
finality notion), then choosing too small values for these parameters may cause significant delays
|
||||
in message delivery. That's because there 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
|
||||
|
||||
Apart from the message related calls, the module exposes a set of auxiliary calls. They fall in two
|
||||
groups, described in the next two paragraphs.
|
||||
|
||||
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 all and
|
||||
result all module operations without requiring runtime upgrade. The module may have no message
|
||||
owner, but we suggest to use it at least for initial deployment. To calls that are related to this
|
||||
account are:
|
||||
- `fn set_owner()`: current module owner may call it to transfer "ownership" to another account;
|
||||
- `fn halt_operations()`: the module owner (or sudo account) may call this function to stop all
|
||||
module operations. After this call, all message-related transactions will be rejected until
|
||||
further `resume_operations` call'. This call may be used when something extraordinary happens with
|
||||
the bridge;
|
||||
- `fn resume_operations()`: module owner may call this function to resume bridge operations. The
|
||||
module will resume its regular operations after this call.
|
||||
|
||||
Apart from halting and resuming the bridge, the module owner may also tune module configuration
|
||||
parameters without runtime upgrades. The set of parameters needs to be designed in advance, though.
|
||||
The module configuration trait has associated `Parameter` type, which may be e.g. enum and represent
|
||||
a set of parameters that may be updated by the module owner. For example, if your bridge needs to
|
||||
convert sums between different tokens, you may define a 'conversion rate' parameter and let the
|
||||
module owner update this parameter when there are significant changes in the rate. The corresponding
|
||||
module call is `fn update_pallet_parameter()`.
|
||||
|
||||
## Weights of Module Extrinsics
|
||||
|
||||
The main assumptions behind weight formulas is:
|
||||
- all possible costs are paid in advance by the message submitter;
|
||||
- whenever possible, relayer tries to minimize cost of its transactions. So e.g. even though sender
|
||||
always pays for delivering outbound lane state proof, relayer may not include it in the delivery
|
||||
transaction (unless messages module on target chain requires that);
|
||||
- weight formula should incentivize relayer to not to submit any redundant data in the extrinsics
|
||||
arguments;
|
||||
- the extrinsic shall never be executing slower (i.e. has larger actual weight) than defined by the
|
||||
formula.
|
||||
|
||||
### Weight of `send_message` call
|
||||
|
||||
#### Related benchmarks
|
||||
|
||||
| Benchmark | Description |
|
||||
|-----------------------------------|-----------------------------------------------------|
|
||||
`send_minimal_message_worst_case` | Sends 0-size message with worst possible conditions |
|
||||
`send_1_kb_message_worst_case` | Sends 1KB-size message with worst possible conditions |
|
||||
`send_16_kb_message_worst_case` | Sends 16KB-size message with worst possible conditions |
|
||||
|
||||
#### Weight formula
|
||||
|
||||
The weight formula is:
|
||||
```
|
||||
Weight = BaseWeight + MessageSizeInKilobytes * MessageKiloByteSendWeight
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
| Component | How it is computed? | Description |
|
||||
|-----------------------------|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `SendMessageOverhead` | `send_minimal_message_worst_case` | Weight of sending minimal (0 bytes) message |
|
||||
| `MessageKiloByteSendWeight` | `(send_16_kb_message_worst_case - send_1_kb_message_worst_case)/15` | Weight of sending every additional kilobyte of the message |
|
||||
|
||||
### Weight of `receive_messages_proof` call
|
||||
|
||||
#### Related benchmarks
|
||||
|
||||
| Benchmark | Description* |
|
||||
|---------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
|
||||
| `receive_single_message_proof` | Receives proof of single `EXPECTED_DEFAULT_MESSAGE_LENGTH` message |
|
||||
| `receive_two_messages_proof` | Receives proof of two identical `EXPECTED_DEFAULT_MESSAGE_LENGTH` messages |
|
||||
| `receive_single_message_proof_with_outbound_lane_state` | Receives proof of single `EXPECTED_DEFAULT_MESSAGE_LENGTH` message and proof of outbound lane state at the source chain |
|
||||
| `receive_single_message_proof_1_kb` | Receives proof of single message. The proof has size of approximately 1KB** |
|
||||
| `receive_single_message_proof_16_kb` | Receives proof of single message. The proof has size of approximately 16KB** |
|
||||
|
||||
*\* - In all benchmarks all received messages are dispatched and their dispatch cost is near to zero*
|
||||
|
||||
*\*\* - Trie leafs are assumed to have minimal values. The proof is derived from the minimal proof
|
||||
by including more trie nodes. That's because according to `receive_message_proofs_with_large_leaf`
|
||||
and `receive_message_proofs_with_extra_nodes` benchmarks, increasing proof by including more nodes
|
||||
has slightly larger impact on performance than increasing values stored in leafs*.
|
||||
|
||||
#### Weight formula
|
||||
|
||||
The weight formula is:
|
||||
```
|
||||
Weight = BaseWeight + OutboundStateDeliveryWeight
|
||||
+ MessagesCount * MessageDeliveryWeight
|
||||
+ MessagesDispatchWeight
|
||||
+ Max(0, ActualProofSize - ExpectedProofSize) * ProofByteDeliveryWeight
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
| Component | How it is computed? | Description |
|
||||
|-------------------------------|------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `BaseWeight` | `2*receive_single_message_proof - receive_two_messages_proof` | Weight of receiving and parsing minimal proof |
|
||||
| `OutboundStateDeliveryWeight` | `receive_single_message_proof_with_outbound_lane_state - receive_single_message_proof` | Additional weight when proof includes outbound lane state |
|
||||
| `MessageDeliveryWeight` | `receive_two_messages_proof - receive_single_message_proof` | Weight of of parsing and dispatching (without actual dispatch cost) of every message |
|
||||
| `MessagesCount` | | Provided by relayer |
|
||||
| `MessagesDispatchWeight` | | Provided by relayer |
|
||||
| `ActualProofSize` | | Provided by relayer |
|
||||
| `ExpectedProofSize` | `EXPECTED_DEFAULT_MESSAGE_LENGTH * MessagesCount + EXTRA_STORAGE_PROOF_SIZE` | Size of proof that we are expecting. This only includes `EXTRA_STORAGE_PROOF_SIZE` once, because we assume that intermediate nodes likely to be included in the proof only once. This may be wrong, but since weight of processing proof with many nodes is almost equal to processing proof with large leafs, additional cost will be covered because we're charging for extra proof bytes anyway |
|
||||
| `ProofByteDeliveryWeight` | `(receive_single_message_proof_16_kb - receive_single_message_proof_1_kb) / (15 * 1024)` | Weight of processing every additional proof byte over `ExpectedProofSize` limit |
|
||||
|
||||
#### Why for every message sent using `send_message` we will be able to craft `receive_messages_proof` transaction?
|
||||
|
||||
We have following checks in `send_message` transaction on the source chain:
|
||||
- message size should be less than or equal to `2/3` of maximal extrinsic size on the target chain;
|
||||
- message dispatch weight should be less than or equal to the `1/2` of maximal extrinsic dispatch
|
||||
weight on the target chain.
|
||||
|
||||
Delivery transaction is an encoded delivery call and signed extensions. So we have `1/3` of maximal
|
||||
extrinsic size reserved for:
|
||||
- storage proof, excluding the message itself. Currently, on our test chains, the overhead is always
|
||||
within `EXTRA_STORAGE_PROOF_SIZE` limits (1024 bytes);
|
||||
- signed extras and other call arguments (`relayer_id: SourceChain::AccountId`, `messages_count:
|
||||
u32`, `dispatch_weight: u64`).
|
||||
|
||||
On Millau chain, maximal extrinsic size is `0.75 * 2MB`, so `1/3` is `512KB` (`524_288` bytes). This
|
||||
should be enough to cover these extra arguments and signed extensions.
|
||||
|
||||
Let's exclude message dispatch cost from single message delivery transaction weight formula:
|
||||
```
|
||||
Weight = BaseWeight + OutboundStateDeliveryWeight + MessageDeliveryWeight
|
||||
+ Max(0, ActualProofSize - ExpectedProofSize) * ProofByteDeliveryWeight
|
||||
```
|
||||
|
||||
So we have `1/2` of maximal extrinsic weight to cover these components. `BaseWeight`,
|
||||
`OutboundStateDeliveryWeight` and `MessageDeliveryWeight` are determined using benchmarks and are
|
||||
hardcoded into runtime. Adequate relayer would only include required trie nodes into the proof. So
|
||||
if message size would be maximal (`2/3` of `MaximalExtrinsicSize`), then the extra proof size would
|
||||
be `MaximalExtrinsicSize / 3 * 2 - EXPECTED_DEFAULT_MESSAGE_LENGTH`.
|
||||
|
||||
Both conditions are verified by `pallet_bridge_messages::ensure_weights_are_correct` and
|
||||
`pallet_bridge_messages::ensure_able_to_receive_messages` functions, which must be called from every
|
||||
runtime's tests.
|
||||
|
||||
### Weight of `receive_messages_delivery_proof` call
|
||||
|
||||
#### Related benchmarks
|
||||
|
||||
| Benchmark | Description |
|
||||
|-------------------------------------------------------------|------------------------------------------------------------------------------------------|
|
||||
| `receive_delivery_proof_for_single_message` | Receives proof of single message delivery |
|
||||
| `receive_delivery_proof_for_two_messages_by_single_relayer` | Receives proof of two messages delivery. Both messages are delivered by the same relayer |
|
||||
| `receive_delivery_proof_for_two_messages_by_two_relayers` | Receives proof of two messages delivery. Messages are delivered by different relayers |
|
||||
|
||||
#### Weight formula
|
||||
|
||||
The weight formula is:
|
||||
```
|
||||
Weight = BaseWeight + MessagesCount * MessageConfirmationWeight
|
||||
+ RelayersCount * RelayerRewardWeight
|
||||
+ Max(0, ActualProofSize - ExpectedProofSize) * ProofByteDeliveryWeight
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
| Component | How it is computed? | Description |
|
||||
|---------------------------|-----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `BaseWeight` | `2*receive_delivery_proof_for_single_message - receive_delivery_proof_for_two_messages_by_single_relayer` | Weight of receiving and parsing minimal delivery proof |
|
||||
| `MessageDeliveryWeight` | `receive_delivery_proof_for_two_messages_by_single_relayer - receive_delivery_proof_for_single_message` | Weight of confirming every additional message |
|
||||
| `MessagesCount` | | Provided by relayer |
|
||||
| `RelayerRewardWeight` | `receive_delivery_proof_for_two_messages_by_two_relayers - receive_delivery_proof_for_two_messages_by_single_relayer` | Weight of rewarding every additional relayer |
|
||||
| `RelayersCount` | | Provided by relayer |
|
||||
| `ActualProofSize` | | Provided by relayer |
|
||||
| `ExpectedProofSize` | `EXTRA_STORAGE_PROOF_SIZE` | Size of proof that we are expecting |
|
||||
| `ProofByteDeliveryWeight` | `(receive_single_message_proof_16_kb - receive_single_message_proof_1_kb) / (15 * 1024)` | Weight of processing every additional proof byte over `ExpectedProofSize` limit. We're using the same formula, as for message delivery, because proof mechanism is assumed to be the same in both cases |
|
||||
|
||||
#### Why we're always able to craft `receive_messages_delivery_proof` transaction?
|
||||
|
||||
There can be at most `<PeerRuntime as pallet_bridge_messages::Config>::MaxUnconfirmedMessagesAtInboundLane`
|
||||
messages and at most
|
||||
`<PeerRuntime as pallet_bridge_messages::Config>::MaxUnrewardedRelayerEntriesAtInboundLane` unrewarded
|
||||
relayers in the single delivery confirmation transaction.
|
||||
|
||||
We're checking that this transaction may be crafted in the
|
||||
`pallet_bridge_messages::ensure_able_to_receive_confirmation` function, which must be called from every
|
||||
runtime' tests.
|
||||
@@ -0,0 +1,830 @@
|
||||
// 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/>.
|
||||
|
||||
//! Messages pallet benchmarking.
|
||||
|
||||
use crate::weights_ext::EXPECTED_DEFAULT_MESSAGE_LENGTH;
|
||||
use crate::{inbound_lane::InboundLaneStorage, inbound_lane_storage, outbound_lane, Call, Instance};
|
||||
|
||||
use bp_messages::{
|
||||
source_chain::TargetHeaderChain, target_chain::SourceHeaderChain, InboundLaneData, LaneId, MessageData,
|
||||
MessageNonce, OutboundLaneData, UnrewardedRelayersState,
|
||||
};
|
||||
use frame_benchmarking::{account, benchmarks_instance};
|
||||
use frame_support::{traits::Get, weights::Weight};
|
||||
use frame_system::RawOrigin;
|
||||
use sp_std::{collections::btree_map::BTreeMap, convert::TryInto, ops::RangeInclusive, prelude::*};
|
||||
|
||||
/// Fee paid by submitter for single message delivery.
|
||||
pub const MESSAGE_FEE: u64 = 10_000_000_000;
|
||||
|
||||
const SEED: u32 = 0;
|
||||
|
||||
/// Pallet we're benchmarking here.
|
||||
pub struct Pallet<T: Config<I>, I: crate::Instance>(crate::Pallet<T, I>);
|
||||
|
||||
/// Proof size requirements.
|
||||
pub enum ProofSize {
|
||||
/// The proof is expected to be minimal. If value size may be changed, then it is expected to
|
||||
/// have given size.
|
||||
Minimal(u32),
|
||||
/// The proof is expected to have at least given size and grow by increasing number of trie nodes
|
||||
/// included in the proof.
|
||||
HasExtraNodes(u32),
|
||||
/// The proof is expected to have at least given size and grow by increasing value that is stored
|
||||
/// in the trie.
|
||||
HasLargeLeaf(u32),
|
||||
}
|
||||
|
||||
/// Benchmark-specific message parameters.
|
||||
pub struct MessageParams<ThisAccountId> {
|
||||
/// Size of the message payload.
|
||||
pub size: u32,
|
||||
/// Message sender account.
|
||||
pub sender_account: ThisAccountId,
|
||||
}
|
||||
|
||||
/// Benchmark-specific message proof parameters.
|
||||
pub struct MessageProofParams {
|
||||
/// 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>,
|
||||
/// Proof size requirements.
|
||||
pub size: ProofSize,
|
||||
}
|
||||
|
||||
/// Benchmark-specific message delivery proof parameters.
|
||||
pub struct MessageDeliveryProofParams<ThisChainAccountId> {
|
||||
/// 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 size: ProofSize,
|
||||
}
|
||||
|
||||
/// Trait that must be implemented by runtime.
|
||||
pub trait Config<I: Instance>: crate::Config<I> {
|
||||
/// Lane id to use in benchmarks.
|
||||
fn bench_lane_id() -> LaneId {
|
||||
Default::default()
|
||||
}
|
||||
/// Get maximal size of the message payload.
|
||||
fn maximal_message_size() -> u32;
|
||||
/// Return id of relayer account at the bridged chain.
|
||||
fn bridged_relayer_id() -> Self::InboundRelayer;
|
||||
/// Return balance of given account.
|
||||
fn account_balance(account: &Self::AccountId) -> Self::OutboundMessageFee;
|
||||
/// Create given account and give it enough balance for test purposes.
|
||||
fn endow_account(account: &Self::AccountId);
|
||||
/// Prepare message to send over lane.
|
||||
fn prepare_outbound_message(
|
||||
params: MessageParams<Self::AccountId>,
|
||||
) -> (Self::OutboundPayload, Self::OutboundMessageFee);
|
||||
/// Prepare messages proof to receive by the module.
|
||||
fn prepare_message_proof(
|
||||
params: MessageProofParams,
|
||||
) -> (
|
||||
<Self::SourceHeaderChain as SourceHeaderChain<Self::InboundMessageFee>>::MessagesProof,
|
||||
Weight,
|
||||
);
|
||||
/// Prepare messages delivery proof to receive by the module.
|
||||
fn prepare_message_delivery_proof(
|
||||
params: MessageDeliveryProofParams<Self::AccountId>,
|
||||
) -> <Self::TargetHeaderChain as TargetHeaderChain<Self::OutboundPayload, Self::AccountId>>::MessagesDeliveryProof;
|
||||
}
|
||||
|
||||
benchmarks_instance! {
|
||||
//
|
||||
// Benchmarks that are used directly by the runtime.
|
||||
//
|
||||
|
||||
// Benchmark `send_message` extrinsic with the worst possible conditions:
|
||||
// * outbound lane already has state, so it needs to be read and decoded;
|
||||
// * relayers fund account does not exists (in practice it needs to exist in production environment);
|
||||
// * maximal number of messages is being pruned during the call;
|
||||
// * message size is minimal for the target chain.
|
||||
//
|
||||
// Result of this benchmark is used as a base weight for `send_message` call. Then the 'message weight'
|
||||
// (estimated using `send_half_maximal_message_worst_case` and `send_maximal_message_worst_case`) is
|
||||
// added.
|
||||
send_minimal_message_worst_case {
|
||||
let lane_id = T::bench_lane_id();
|
||||
let sender = account("sender", 0, SEED);
|
||||
T::endow_account(&sender);
|
||||
|
||||
// 'send' messages that are to be pruned when our message is sent
|
||||
for _nonce in 1..=T::MaxMessagesToPruneAtOnce::get() {
|
||||
send_regular_message::<T, I>();
|
||||
}
|
||||
confirm_message_delivery::<T, I>(T::MaxMessagesToPruneAtOnce::get());
|
||||
|
||||
let (payload, fee) = T::prepare_outbound_message(MessageParams {
|
||||
size: 0,
|
||||
sender_account: sender.clone(),
|
||||
});
|
||||
}: send_message(RawOrigin::Signed(sender), lane_id, payload, fee)
|
||||
verify {
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::outbound_latest_generated_nonce(T::bench_lane_id()),
|
||||
T::MaxMessagesToPruneAtOnce::get() + 1,
|
||||
);
|
||||
}
|
||||
|
||||
// Benchmark `send_message` extrinsic with the worst possible conditions:
|
||||
// * outbound lane already has state, so it needs to be read and decoded;
|
||||
// * relayers fund account does not exists (in practice it needs to exist in production environment);
|
||||
// * maximal number of messages is being pruned during the call;
|
||||
// * message size is 1KB.
|
||||
//
|
||||
// With single KB of message size, the weight of the call is increased (roughly) by
|
||||
// `(send_16_kb_message_worst_case - send_1_kb_message_worst_case) / 15`.
|
||||
send_1_kb_message_worst_case {
|
||||
let lane_id = T::bench_lane_id();
|
||||
let sender = account("sender", 0, SEED);
|
||||
T::endow_account(&sender);
|
||||
|
||||
// 'send' messages that are to be pruned when our message is sent
|
||||
for _nonce in 1..=T::MaxMessagesToPruneAtOnce::get() {
|
||||
send_regular_message::<T, I>();
|
||||
}
|
||||
confirm_message_delivery::<T, I>(T::MaxMessagesToPruneAtOnce::get());
|
||||
|
||||
let size = 1024;
|
||||
assert!(
|
||||
T::maximal_message_size() > size,
|
||||
"This benchmark can only be used with runtime that accepts 1KB messages",
|
||||
);
|
||||
|
||||
let (payload, fee) = T::prepare_outbound_message(MessageParams {
|
||||
size,
|
||||
sender_account: sender.clone(),
|
||||
});
|
||||
}: send_message(RawOrigin::Signed(sender), lane_id, payload, fee)
|
||||
verify {
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::outbound_latest_generated_nonce(T::bench_lane_id()),
|
||||
T::MaxMessagesToPruneAtOnce::get() + 1,
|
||||
);
|
||||
}
|
||||
|
||||
// Benchmark `send_message` extrinsic with the worst possible conditions:
|
||||
// * outbound lane already has state, so it needs to be read and decoded;
|
||||
// * relayers fund account does not exists (in practice it needs to exist in production environment);
|
||||
// * maximal number of messages is being pruned during the call;
|
||||
// * message size is 16KB.
|
||||
//
|
||||
// With single KB of message size, the weight of the call is increased (roughly) by
|
||||
// `(send_16_kb_message_worst_case - send_1_kb_message_worst_case) / 15`.
|
||||
send_16_kb_message_worst_case {
|
||||
let lane_id = T::bench_lane_id();
|
||||
let sender = account("sender", 0, SEED);
|
||||
T::endow_account(&sender);
|
||||
|
||||
// 'send' messages that are to be pruned when our message is sent
|
||||
for _nonce in 1..=T::MaxMessagesToPruneAtOnce::get() {
|
||||
send_regular_message::<T, I>();
|
||||
}
|
||||
confirm_message_delivery::<T, I>(T::MaxMessagesToPruneAtOnce::get());
|
||||
|
||||
let size = 16 * 1024;
|
||||
assert!(
|
||||
T::maximal_message_size() > size,
|
||||
"This benchmark can only be used with runtime that accepts 16KB messages",
|
||||
);
|
||||
|
||||
let (payload, fee) = T::prepare_outbound_message(MessageParams {
|
||||
size,
|
||||
sender_account: sender.clone(),
|
||||
});
|
||||
}: send_message(RawOrigin::Signed(sender), lane_id, payload, fee)
|
||||
verify {
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::outbound_latest_generated_nonce(T::bench_lane_id()),
|
||||
T::MaxMessagesToPruneAtOnce::get() + 1,
|
||||
);
|
||||
}
|
||||
|
||||
// Benchmark `increase_message_fee` with following conditions:
|
||||
// * message has maximal message;
|
||||
// * submitter account is killed because its balance is less than ED after payment.
|
||||
increase_message_fee {
|
||||
let sender = account("sender", 42, SEED);
|
||||
T::endow_account(&sender);
|
||||
|
||||
let additional_fee = T::account_balance(&sender);
|
||||
let lane_id = T::bench_lane_id();
|
||||
let nonce = 1;
|
||||
|
||||
send_regular_message_with_payload::<T, I>(vec![42u8; T::maximal_message_size() as _]);
|
||||
}: increase_message_fee(RawOrigin::Signed(sender.clone()), lane_id, nonce, additional_fee)
|
||||
verify {
|
||||
assert_eq!(T::account_balance(&sender), 0.into());
|
||||
}
|
||||
|
||||
// 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 successfully dispatched;
|
||||
// * message requires all heavy checks done by dispatcher.
|
||||
//
|
||||
// This is base benchmark for all other message delivery benchmarks.
|
||||
receive_single_message_proof {
|
||||
let relayer_id_on_source = T::bridged_relayer_id();
|
||||
let relayer_id_on_target = account("relayer", 0, SEED);
|
||||
|
||||
// mark messages 1..=20 as delivered
|
||||
receive_messages::<T, I>(20);
|
||||
|
||||
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
message_nonces: 21..=21,
|
||||
outbound_lane_data: None,
|
||||
size: ProofSize::Minimal(EXPECTED_DEFAULT_MESSAGE_LENGTH),
|
||||
});
|
||||
}: receive_messages_proof(RawOrigin::Signed(relayer_id_on_target), relayer_id_on_source, proof, 1, dispatch_weight)
|
||||
verify {
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
|
||||
21,
|
||||
);
|
||||
}
|
||||
|
||||
// Benchmark `receive_messages_proof` extrinsic with two 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 successfully dispatched;
|
||||
// * message requires all heavy checks done by dispatcher.
|
||||
//
|
||||
// The weight of single message delivery could be approximated as
|
||||
// `weight(receive_two_messages_proof) - 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.
|
||||
receive_two_messages_proof {
|
||||
let relayer_id_on_source = T::bridged_relayer_id();
|
||||
let relayer_id_on_target = account("relayer", 0, SEED);
|
||||
|
||||
// mark messages 1..=20 as delivered
|
||||
receive_messages::<T, I>(20);
|
||||
|
||||
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
message_nonces: 21..=22,
|
||||
outbound_lane_data: None,
|
||||
size: ProofSize::Minimal(EXPECTED_DEFAULT_MESSAGE_LENGTH),
|
||||
});
|
||||
}: receive_messages_proof(RawOrigin::Signed(relayer_id_on_target), relayer_id_on_source, proof, 2, dispatch_weight)
|
||||
verify {
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
|
||||
22,
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
// * 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.
|
||||
receive_single_message_proof_with_outbound_lane_state {
|
||||
let relayer_id_on_source = T::bridged_relayer_id();
|
||||
let relayer_id_on_target = account("relayer", 0, SEED);
|
||||
|
||||
// mark messages 1..=20 as delivered
|
||||
receive_messages::<T, I>(20);
|
||||
|
||||
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
message_nonces: 21..=21,
|
||||
outbound_lane_data: Some(OutboundLaneData {
|
||||
oldest_unpruned_nonce: 21,
|
||||
latest_received_nonce: 20,
|
||||
latest_generated_nonce: 21,
|
||||
}),
|
||||
size: ProofSize::Minimal(EXPECTED_DEFAULT_MESSAGE_LENGTH),
|
||||
});
|
||||
}: receive_messages_proof(RawOrigin::Signed(relayer_id_on_target), relayer_id_on_source, proof, 1, dispatch_weight)
|
||||
verify {
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
|
||||
21,
|
||||
);
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::inbound_latest_confirmed_nonce(T::bench_lane_id()),
|
||||
20,
|
||||
);
|
||||
}
|
||||
|
||||
// Benchmark `receive_messages_proof` extrinsic with single minimal-weight message and following conditions:
|
||||
// * the proof has many redundand trie nodes with total size of approximately 1KB;
|
||||
// * 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.
|
||||
//
|
||||
// With single KB of messages proof, the weight of the call is increased (roughly) by
|
||||
// `(receive_single_message_proof_16KB - receive_single_message_proof_1_kb) / 15`.
|
||||
receive_single_message_proof_1_kb {
|
||||
let relayer_id_on_source = T::bridged_relayer_id();
|
||||
let relayer_id_on_target = account("relayer", 0, SEED);
|
||||
|
||||
// mark messages 1..=20 as delivered
|
||||
receive_messages::<T, I>(20);
|
||||
|
||||
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
message_nonces: 21..=21,
|
||||
outbound_lane_data: None,
|
||||
size: ProofSize::HasExtraNodes(1024),
|
||||
});
|
||||
}: receive_messages_proof(RawOrigin::Signed(relayer_id_on_target), relayer_id_on_source, proof, 1, dispatch_weight)
|
||||
verify {
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
|
||||
21,
|
||||
);
|
||||
}
|
||||
|
||||
// Benchmark `receive_messages_proof` extrinsic with single minimal-weight message and following conditions:
|
||||
// * the proof has many redundand trie nodes with total size of approximately 16KB;
|
||||
// * 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.
|
||||
//
|
||||
// Size of proof grows because it contains extra trie nodes in it.
|
||||
//
|
||||
// With single KB of messages proof, the weight of the call is increased (roughly) by
|
||||
// `(receive_single_message_proof_16KB - receive_single_message_proof) / 15`.
|
||||
receive_single_message_proof_16_kb {
|
||||
let relayer_id_on_source = T::bridged_relayer_id();
|
||||
let relayer_id_on_target = account("relayer", 0, SEED);
|
||||
|
||||
// mark messages 1..=20 as delivered
|
||||
receive_messages::<T, I>(20);
|
||||
|
||||
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
message_nonces: 21..=21,
|
||||
outbound_lane_data: None,
|
||||
size: ProofSize::HasExtraNodes(16 * 1024),
|
||||
});
|
||||
}: receive_messages_proof(RawOrigin::Signed(relayer_id_on_target), relayer_id_on_source, proof, 1, dispatch_weight)
|
||||
verify {
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
|
||||
21,
|
||||
);
|
||||
}
|
||||
|
||||
// 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.
|
||||
receive_delivery_proof_for_single_message {
|
||||
let relayers_fund_id = crate::Pallet::<T, I>::relayer_fund_account_id();
|
||||
let relayer_id: T::AccountId = account("relayer", 0, SEED);
|
||||
let relayer_balance = T::account_balance(&relayer_id);
|
||||
T::endow_account(&relayers_fund_id);
|
||||
|
||||
// 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,
|
||||
};
|
||||
let proof = T::prepare_message_delivery_proof(MessageDeliveryProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
inbound_lane_data: InboundLaneData {
|
||||
relayers: vec![(1, 1, relayer_id.clone())].into_iter().collect(),
|
||||
last_confirmed_nonce: 0,
|
||||
},
|
||||
size: ProofSize::Minimal(0),
|
||||
});
|
||||
}: receive_messages_delivery_proof(RawOrigin::Signed(relayer_id.clone()), proof, relayers_state)
|
||||
verify {
|
||||
assert_eq!(
|
||||
T::account_balance(&relayer_id),
|
||||
relayer_balance + MESSAGE_FEE.into(),
|
||||
);
|
||||
}
|
||||
|
||||
// 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)`.
|
||||
receive_delivery_proof_for_two_messages_by_single_relayer {
|
||||
let relayers_fund_id = crate::Pallet::<T, I>::relayer_fund_account_id();
|
||||
let relayer_id: T::AccountId = account("relayer", 0, SEED);
|
||||
let relayer_balance = T::account_balance(&relayer_id);
|
||||
T::endow_account(&relayers_fund_id);
|
||||
|
||||
// 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,
|
||||
};
|
||||
let proof = T::prepare_message_delivery_proof(MessageDeliveryProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
inbound_lane_data: InboundLaneData {
|
||||
relayers: vec![(1, 2, relayer_id.clone())].into_iter().collect(),
|
||||
last_confirmed_nonce: 0,
|
||||
},
|
||||
size: ProofSize::Minimal(0),
|
||||
});
|
||||
}: receive_messages_delivery_proof(RawOrigin::Signed(relayer_id.clone()), proof, relayers_state)
|
||||
verify {
|
||||
ensure_relayer_rewarded::<T, I>(&relayer_id, &relayer_balance);
|
||||
}
|
||||
|
||||
// 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)`.
|
||||
receive_delivery_proof_for_two_messages_by_two_relayers {
|
||||
let relayers_fund_id = crate::Pallet::<T, I>::relayer_fund_account_id();
|
||||
let relayer1_id: T::AccountId = account("relayer1", 1, SEED);
|
||||
let relayer1_balance = T::account_balance(&relayer1_id);
|
||||
let relayer2_id: T::AccountId = account("relayer2", 2, SEED);
|
||||
let relayer2_balance = T::account_balance(&relayer2_id);
|
||||
T::endow_account(&relayers_fund_id);
|
||||
|
||||
// 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,
|
||||
};
|
||||
let proof = T::prepare_message_delivery_proof(MessageDeliveryProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
inbound_lane_data: InboundLaneData {
|
||||
relayers: vec![
|
||||
(1, 1, relayer1_id.clone()),
|
||||
(2, 2, relayer2_id.clone()),
|
||||
].into_iter().collect(),
|
||||
last_confirmed_nonce: 0,
|
||||
},
|
||||
size: ProofSize::Minimal(0),
|
||||
});
|
||||
}: receive_messages_delivery_proof(RawOrigin::Signed(relayer1_id.clone()), proof, relayers_state)
|
||||
verify {
|
||||
ensure_relayer_rewarded::<T, I>(&relayer1_id, &relayer1_balance);
|
||||
ensure_relayer_rewarded::<T, I>(&relayer2_id, &relayer2_balance);
|
||||
}
|
||||
|
||||
//
|
||||
// Benchmarks for manual checks.
|
||||
//
|
||||
|
||||
// Benchmark `send_message` extrinsic with following conditions:
|
||||
// * outbound lane already has state, so it needs to be read and decoded;
|
||||
// * relayers fund account does not exists (in practice it needs to exist in production environment);
|
||||
// * maximal number of messages is being pruned during the call;
|
||||
// * message size varies from minimal to maximal for the target chain.
|
||||
//
|
||||
// Results of this benchmark may be used to check how message size affects `send_message` performance.
|
||||
send_messages_of_various_lengths {
|
||||
let i in 0..T::maximal_message_size().try_into().unwrap_or_default();
|
||||
|
||||
let lane_id = T::bench_lane_id();
|
||||
let sender = account("sender", 0, SEED);
|
||||
T::endow_account(&sender);
|
||||
|
||||
// 'send' messages that are to be pruned when our message is sent
|
||||
for _nonce in 1..=T::MaxMessagesToPruneAtOnce::get() {
|
||||
send_regular_message::<T, I>();
|
||||
}
|
||||
confirm_message_delivery::<T, I>(T::MaxMessagesToPruneAtOnce::get());
|
||||
|
||||
let (payload, fee) = T::prepare_outbound_message(MessageParams {
|
||||
size: i as _,
|
||||
sender_account: sender.clone(),
|
||||
});
|
||||
}: send_message(RawOrigin::Signed(sender), lane_id, payload, fee)
|
||||
verify {
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::outbound_latest_generated_nonce(T::bench_lane_id()),
|
||||
T::MaxMessagesToPruneAtOnce::get() + 1,
|
||||
);
|
||||
}
|
||||
|
||||
// Benchmark `receive_messages_proof` extrinsic with multiple 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 successfully dispatched;
|
||||
// * message requires all heavy checks done by dispatcher.
|
||||
//
|
||||
// This benchmarks gives us an approximation of single message delivery weight. It is similar to the
|
||||
// `weight(receive_two_messages_proof) - weight(receive_single_message_proof)`. So it may be used
|
||||
// to verify that the other approximation is correct.
|
||||
receive_multiple_messages_proof {
|
||||
let i in 1..64;
|
||||
|
||||
let relayer_id_on_source = T::bridged_relayer_id();
|
||||
let relayer_id_on_target = account("relayer", 0, SEED);
|
||||
let messages_count = i as _;
|
||||
|
||||
// mark messages 1..=20 as delivered
|
||||
receive_messages::<T, I>(20);
|
||||
|
||||
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
message_nonces: 21..=(20 + i as MessageNonce),
|
||||
outbound_lane_data: None,
|
||||
size: ProofSize::Minimal(EXPECTED_DEFAULT_MESSAGE_LENGTH),
|
||||
});
|
||||
}: receive_messages_proof(
|
||||
RawOrigin::Signed(relayer_id_on_target),
|
||||
relayer_id_on_source,
|
||||
proof,
|
||||
messages_count,
|
||||
dispatch_weight
|
||||
)
|
||||
verify {
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
|
||||
20 + i as MessageNonce,
|
||||
);
|
||||
}
|
||||
|
||||
// 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 successfully dispatched;
|
||||
// * message requires all heavy checks done by dispatcher.
|
||||
//
|
||||
// Results of this benchmark may be used to check how proof size affects `receive_message_proof` performance.
|
||||
receive_message_proofs_with_extra_nodes {
|
||||
let i in 0..T::maximal_message_size();
|
||||
|
||||
let relayer_id_on_source = T::bridged_relayer_id();
|
||||
let relayer_id_on_target = account("relayer", 0, SEED);
|
||||
let messages_count = 1u32;
|
||||
|
||||
// mark messages 1..=20 as delivered
|
||||
receive_messages::<T, I>(20);
|
||||
|
||||
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
message_nonces: 21..=21,
|
||||
outbound_lane_data: None,
|
||||
size: ProofSize::HasExtraNodes(i as _),
|
||||
});
|
||||
}: receive_messages_proof(
|
||||
RawOrigin::Signed(relayer_id_on_target),
|
||||
relayer_id_on_source,
|
||||
proof,
|
||||
messages_count,
|
||||
dispatch_weight
|
||||
)
|
||||
verify {
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
|
||||
21,
|
||||
);
|
||||
}
|
||||
|
||||
// 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 successfully dispatched;
|
||||
// * message requires all heavy checks done by dispatcher.
|
||||
//
|
||||
// Results of this benchmark may be used to check how message size affects `receive_message_proof` performance.
|
||||
receive_message_proofs_with_large_leaf {
|
||||
let i in 0..T::maximal_message_size();
|
||||
|
||||
let relayer_id_on_source = T::bridged_relayer_id();
|
||||
let relayer_id_on_target = account("relayer", 0, SEED);
|
||||
let messages_count = 1u32;
|
||||
|
||||
// mark messages 1..=20 as delivered
|
||||
receive_messages::<T, I>(20);
|
||||
|
||||
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
message_nonces: 21..=21,
|
||||
outbound_lane_data: None,
|
||||
size: ProofSize::HasLargeLeaf(i as _),
|
||||
});
|
||||
}: receive_messages_proof(
|
||||
RawOrigin::Signed(relayer_id_on_target),
|
||||
relayer_id_on_source,
|
||||
proof,
|
||||
messages_count,
|
||||
dispatch_weight
|
||||
)
|
||||
verify {
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
|
||||
21,
|
||||
);
|
||||
}
|
||||
|
||||
// Benchmark `receive_messages_proof` extrinsic with multiple minimal-weight messages 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;
|
||||
// * message requires all heavy checks done by dispatcher.
|
||||
//
|
||||
// This benchmarks gives us an approximation of outbound lane state delivery weight. It is similar to the
|
||||
// `weight(receive_single_message_proof_with_outbound_lane_state) - weight(receive_single_message_proof)`.
|
||||
// So it may be used to verify that the other approximation is correct.
|
||||
receive_multiple_messages_proof_with_outbound_lane_state {
|
||||
let i in 1..128;
|
||||
|
||||
let relayer_id_on_source = T::bridged_relayer_id();
|
||||
let relayer_id_on_target = account("relayer", 0, SEED);
|
||||
let messages_count = i as _;
|
||||
|
||||
// mark messages 1..=20 as delivered
|
||||
receive_messages::<T, I>(20);
|
||||
|
||||
let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
message_nonces: 21..=20 + i as MessageNonce,
|
||||
outbound_lane_data: Some(OutboundLaneData {
|
||||
oldest_unpruned_nonce: 21,
|
||||
latest_received_nonce: 20,
|
||||
latest_generated_nonce: 21,
|
||||
}),
|
||||
size: ProofSize::Minimal(0),
|
||||
});
|
||||
}: receive_messages_proof(
|
||||
RawOrigin::Signed(relayer_id_on_target),
|
||||
relayer_id_on_source,
|
||||
proof,
|
||||
messages_count,
|
||||
dispatch_weight
|
||||
)
|
||||
verify {
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
|
||||
20 + i as MessageNonce,
|
||||
);
|
||||
assert_eq!(
|
||||
crate::Pallet::<T, I>::inbound_latest_confirmed_nonce(T::bench_lane_id()),
|
||||
20,
|
||||
);
|
||||
}
|
||||
|
||||
// Benchmark `receive_messages_delivery_proof` extrinsic where single relayer delivers multiple messages.
|
||||
receive_delivery_proof_for_multiple_messages_by_single_relayer {
|
||||
// there actually should be used value of `MaxUnrewardedRelayerEntriesAtInboundLane` from the bridged
|
||||
// chain, but we're more interested in additional weight/message than in max weight
|
||||
let i in 1..T::MaxUnrewardedRelayerEntriesAtInboundLane::get()
|
||||
.try_into()
|
||||
.expect("Value of MaxUnrewardedRelayerEntriesAtInboundLane is too large");
|
||||
|
||||
let relayers_fund_id = crate::Pallet::<T, I>::relayer_fund_account_id();
|
||||
let relayer_id: T::AccountId = account("relayer", 0, SEED);
|
||||
let relayer_balance = T::account_balance(&relayer_id);
|
||||
T::endow_account(&relayers_fund_id);
|
||||
|
||||
// send messages that we're going to confirm
|
||||
for _ in 1..=i {
|
||||
send_regular_message::<T, I>();
|
||||
}
|
||||
|
||||
let relayers_state = UnrewardedRelayersState {
|
||||
unrewarded_relayer_entries: 1,
|
||||
messages_in_oldest_entry: 1,
|
||||
total_messages: i as MessageNonce,
|
||||
};
|
||||
let proof = T::prepare_message_delivery_proof(MessageDeliveryProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
inbound_lane_data: InboundLaneData {
|
||||
relayers: vec![(1, i as MessageNonce, relayer_id.clone())].into_iter().collect(),
|
||||
last_confirmed_nonce: 0,
|
||||
},
|
||||
size: ProofSize::Minimal(0),
|
||||
});
|
||||
}: receive_messages_delivery_proof(RawOrigin::Signed(relayer_id.clone()), proof, relayers_state)
|
||||
verify {
|
||||
ensure_relayer_rewarded::<T, I>(&relayer_id, &relayer_balance);
|
||||
}
|
||||
|
||||
// Benchmark `receive_messages_delivery_proof` extrinsic where every relayer delivers single messages.
|
||||
receive_delivery_proof_for_multiple_messages_by_multiple_relayers {
|
||||
// there actually should be used value of `MaxUnconfirmedMessagesAtInboundLane` from the bridged
|
||||
// chain, but we're more interested in additional weight/message than in max weight
|
||||
let i in 1..T::MaxUnconfirmedMessagesAtInboundLane::get()
|
||||
.try_into()
|
||||
.expect("Value of MaxUnconfirmedMessagesAtInboundLane is too large ");
|
||||
|
||||
let relayers_fund_id = crate::Pallet::<T, I>::relayer_fund_account_id();
|
||||
let confirmation_relayer_id = account("relayer", 0, SEED);
|
||||
let relayers: BTreeMap<T::AccountId, T::OutboundMessageFee> = (1..=i)
|
||||
.map(|j| {
|
||||
let relayer_id = account("relayer", j + 1, SEED);
|
||||
let relayer_balance = T::account_balance(&relayer_id);
|
||||
(relayer_id, relayer_balance)
|
||||
})
|
||||
.collect();
|
||||
T::endow_account(&relayers_fund_id);
|
||||
|
||||
// send messages that we're going to confirm
|
||||
for _ in 1..=i {
|
||||
send_regular_message::<T, I>();
|
||||
}
|
||||
|
||||
let relayers_state = UnrewardedRelayersState {
|
||||
unrewarded_relayer_entries: i as MessageNonce,
|
||||
messages_in_oldest_entry: 1,
|
||||
total_messages: i as MessageNonce,
|
||||
};
|
||||
let proof = T::prepare_message_delivery_proof(MessageDeliveryProofParams {
|
||||
lane: T::bench_lane_id(),
|
||||
inbound_lane_data: InboundLaneData {
|
||||
relayers: relayers
|
||||
.keys()
|
||||
.enumerate()
|
||||
.map(|(j, relayer_id)| (j as MessageNonce + 1, j as MessageNonce + 1, relayer_id.clone()))
|
||||
.collect(),
|
||||
last_confirmed_nonce: 0,
|
||||
},
|
||||
size: ProofSize::Minimal(0),
|
||||
});
|
||||
}: receive_messages_delivery_proof(RawOrigin::Signed(confirmation_relayer_id), proof, relayers_state)
|
||||
verify {
|
||||
for (relayer_id, prev_balance) in relayers {
|
||||
ensure_relayer_rewarded::<T, I>(&relayer_id, &prev_balance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_regular_message<T: Config<I>, I: Instance>() {
|
||||
let mut outbound_lane = outbound_lane::<T, I>(T::bench_lane_id());
|
||||
outbound_lane.send_message(MessageData {
|
||||
payload: vec![],
|
||||
fee: MESSAGE_FEE.into(),
|
||||
});
|
||||
}
|
||||
|
||||
fn send_regular_message_with_payload<T: Config<I>, I: Instance>(payload: Vec<u8>) {
|
||||
let mut outbound_lane = outbound_lane::<T, I>(T::bench_lane_id());
|
||||
outbound_lane.send_message(MessageData {
|
||||
payload,
|
||||
fee: MESSAGE_FEE.into(),
|
||||
});
|
||||
}
|
||||
|
||||
fn confirm_message_delivery<T: Config<I>, I: Instance>(nonce: MessageNonce) {
|
||||
let mut outbound_lane = outbound_lane::<T, I>(T::bench_lane_id());
|
||||
assert!(outbound_lane.confirm_delivery(nonce).is_some());
|
||||
}
|
||||
|
||||
fn receive_messages<T: Config<I>, I: Instance>(nonce: MessageNonce) {
|
||||
let mut inbound_lane_storage = inbound_lane_storage::<T, I>(T::bench_lane_id());
|
||||
inbound_lane_storage.set_data(InboundLaneData {
|
||||
relayers: vec![(1, nonce, T::bridged_relayer_id())].into_iter().collect(),
|
||||
last_confirmed_nonce: 0,
|
||||
});
|
||||
}
|
||||
|
||||
fn ensure_relayer_rewarded<T: Config<I>, I: Instance>(relayer_id: &T::AccountId, old_balance: &T::OutboundMessageFee) {
|
||||
let new_balance = T::account_balance(relayer_id);
|
||||
assert!(
|
||||
new_balance > *old_balance,
|
||||
"Relayer haven't received reward for relaying message: old balance = {:?}, new balance = {:?}",
|
||||
old_balance,
|
||||
new_balance,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
// 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/>.
|
||||
|
||||
//! Everything about incoming messages receival.
|
||||
|
||||
use bp_messages::{
|
||||
target_chain::{DispatchMessage, DispatchMessageData, MessageDispatch},
|
||||
InboundLaneData, LaneId, MessageKey, MessageNonce, OutboundLaneData,
|
||||
};
|
||||
use sp_std::prelude::PartialEq;
|
||||
|
||||
/// Inbound lane storage.
|
||||
pub trait InboundLaneStorage {
|
||||
/// Delivery and dispatch fee type on source chain.
|
||||
type MessageFee;
|
||||
/// Id of relayer on source chain.
|
||||
type Relayer: PartialEq;
|
||||
|
||||
/// Lane id.
|
||||
fn id(&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>);
|
||||
}
|
||||
|
||||
/// 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 }
|
||||
}
|
||||
|
||||
/// 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(|(_, nonce_high, _)| *nonce_high <= 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((nonce_low, _, _)) if *nonce_low < new_confirmed_nonce => {
|
||||
*nonce_low = new_confirmed_nonce + 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.storage.set_data(data);
|
||||
Some(outbound_lane_data.latest_received_nonce)
|
||||
}
|
||||
|
||||
/// Receive new message.
|
||||
pub fn receive_message<P: MessageDispatch<S::MessageFee>>(
|
||||
&mut self,
|
||||
relayer: S::Relayer,
|
||||
nonce: MessageNonce,
|
||||
message_data: DispatchMessageData<P::DispatchPayload, S::MessageFee>,
|
||||
) -> bool {
|
||||
let mut data = self.storage.data();
|
||||
let is_correct_message = nonce == data.last_delivered_nonce() + 1;
|
||||
if !is_correct_message {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 false;
|
||||
}
|
||||
|
||||
// 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 false;
|
||||
}
|
||||
|
||||
let push_new = match data.relayers.back_mut() {
|
||||
Some((_, nonce_high, last_relayer)) if last_relayer == &relayer => {
|
||||
*nonce_high = nonce;
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
if push_new {
|
||||
data.relayers.push_back((nonce, nonce, relayer));
|
||||
}
|
||||
|
||||
self.storage.set_data(data);
|
||||
|
||||
P::dispatch(DispatchMessage {
|
||||
key: MessageKey {
|
||||
lane_id: self.storage.id(),
|
||||
nonce,
|
||||
},
|
||||
data: message_data,
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
inbound_lane,
|
||||
mock::{
|
||||
message_data, run_test, TestMessageDispatch, TestRuntime, REGULAR_PAYLOAD, TEST_LANE_ID, TEST_RELAYER_A,
|
||||
TEST_RELAYER_B, TEST_RELAYER_C,
|
||||
},
|
||||
DefaultInstance, RuntimeInboundLaneStorage,
|
||||
};
|
||||
|
||||
fn receive_regular_message(
|
||||
lane: &mut InboundLane<RuntimeInboundLaneStorage<TestRuntime, DefaultInstance>>,
|
||||
nonce: MessageNonce,
|
||||
) {
|
||||
assert!(lane.receive_message::<TestMessageDispatch>(
|
||||
TEST_RELAYER_A,
|
||||
nonce,
|
||||
message_data(REGULAR_PAYLOAD).into()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn receive_status_update_ignores_status_from_the_future() {
|
||||
run_test(|| {
|
||||
let mut lane = inbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
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 = inbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
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 = inbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
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![(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![(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 = inbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
let mut seed_storage_data = lane.storage.data();
|
||||
// Prepare data
|
||||
seed_storage_data.last_confirmed_nonce = 0;
|
||||
seed_storage_data.relayers.push_back((1, 1, TEST_RELAYER_A));
|
||||
// Simulate messages batch (2, 3, 4) from relayer #2
|
||||
seed_storage_data.relayers.push_back((2, 4, TEST_RELAYER_B));
|
||||
seed_storage_data.relayers.push_back((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![(4, 4, TEST_RELAYER_B), (5, 5, TEST_RELAYER_C)]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_to_receive_message_with_incorrect_nonce() {
|
||||
run_test(|| {
|
||||
let mut lane = inbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
assert!(!lane.receive_message::<TestMessageDispatch>(
|
||||
TEST_RELAYER_A,
|
||||
10,
|
||||
message_data(REGULAR_PAYLOAD).into()
|
||||
));
|
||||
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 = inbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
let max_nonce = <TestRuntime as crate::Config>::MaxUnrewardedRelayerEntriesAtInboundLane::get();
|
||||
for current_nonce in 1..max_nonce + 1 {
|
||||
assert!(lane.receive_message::<TestMessageDispatch>(
|
||||
TEST_RELAYER_A + current_nonce,
|
||||
current_nonce,
|
||||
message_data(REGULAR_PAYLOAD).into()
|
||||
));
|
||||
}
|
||||
// Fails to dispatch new message from different than latest relayer.
|
||||
assert_eq!(
|
||||
false,
|
||||
lane.receive_message::<TestMessageDispatch>(
|
||||
TEST_RELAYER_A + max_nonce + 1,
|
||||
max_nonce + 1,
|
||||
message_data(REGULAR_PAYLOAD).into()
|
||||
)
|
||||
);
|
||||
// Fails to dispatch new messages from latest relayer. Prevents griefing attacks.
|
||||
assert_eq!(
|
||||
false,
|
||||
lane.receive_message::<TestMessageDispatch>(
|
||||
TEST_RELAYER_A + max_nonce,
|
||||
max_nonce + 1,
|
||||
message_data(REGULAR_PAYLOAD).into()
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_to_receive_messages_above_unconfirmed_messages_limit_per_lane() {
|
||||
run_test(|| {
|
||||
let mut lane = inbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
let max_nonce = <TestRuntime as crate::Config>::MaxUnconfirmedMessagesAtInboundLane::get();
|
||||
for current_nonce in 1..=max_nonce {
|
||||
assert!(lane.receive_message::<TestMessageDispatch>(
|
||||
TEST_RELAYER_A,
|
||||
current_nonce,
|
||||
message_data(REGULAR_PAYLOAD).into()
|
||||
));
|
||||
}
|
||||
// Fails to dispatch new message from different than latest relayer.
|
||||
assert_eq!(
|
||||
false,
|
||||
lane.receive_message::<TestMessageDispatch>(
|
||||
TEST_RELAYER_B,
|
||||
max_nonce + 1,
|
||||
message_data(REGULAR_PAYLOAD).into()
|
||||
)
|
||||
);
|
||||
// Fails to dispatch new messages from latest relayer.
|
||||
assert_eq!(
|
||||
false,
|
||||
lane.receive_message::<TestMessageDispatch>(
|
||||
TEST_RELAYER_A,
|
||||
max_nonce + 1,
|
||||
message_data(REGULAR_PAYLOAD).into()
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn correctly_receives_following_messages_from_two_relayers_alternately() {
|
||||
run_test(|| {
|
||||
let mut lane = inbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
assert!(lane.receive_message::<TestMessageDispatch>(
|
||||
TEST_RELAYER_A,
|
||||
1,
|
||||
message_data(REGULAR_PAYLOAD).into()
|
||||
));
|
||||
assert!(lane.receive_message::<TestMessageDispatch>(
|
||||
TEST_RELAYER_B,
|
||||
2,
|
||||
message_data(REGULAR_PAYLOAD).into()
|
||||
));
|
||||
assert!(lane.receive_message::<TestMessageDispatch>(
|
||||
TEST_RELAYER_A,
|
||||
3,
|
||||
message_data(REGULAR_PAYLOAD).into()
|
||||
));
|
||||
assert_eq!(
|
||||
lane.storage.data().relayers,
|
||||
vec![(1, 1, TEST_RELAYER_A), (2, 2, TEST_RELAYER_B), (3, 3, TEST_RELAYER_A)]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_same_message_from_two_different_relayers() {
|
||||
run_test(|| {
|
||||
let mut lane = inbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
assert!(lane.receive_message::<TestMessageDispatch>(
|
||||
TEST_RELAYER_A,
|
||||
1,
|
||||
message_data(REGULAR_PAYLOAD).into()
|
||||
));
|
||||
assert_eq!(
|
||||
false,
|
||||
lane.receive_message::<TestMessageDispatch>(TEST_RELAYER_B, 1, message_data(REGULAR_PAYLOAD).into())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn correct_message_is_processed_instantly() {
|
||||
run_test(|| {
|
||||
let mut lane = inbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
receive_regular_message(&mut lane, 1);
|
||||
assert_eq!(lane.storage.data().last_delivered_nonce(), 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
// 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/>.
|
||||
|
||||
//! Implementation of `MessageDeliveryAndDispatchPayment` trait on top of `Currency` trait.
|
||||
//!
|
||||
//! The payment is first transferred to a special `relayers-fund` account and only transferred
|
||||
//! to the actual relayer in case confirmation is received.
|
||||
|
||||
use bp_messages::{
|
||||
source_chain::{MessageDeliveryAndDispatchPayment, RelayersRewards, Sender},
|
||||
MessageNonce,
|
||||
};
|
||||
use codec::Encode;
|
||||
use frame_support::traits::{Currency as CurrencyT, ExistenceRequirement, Get};
|
||||
use num_traits::Zero;
|
||||
use sp_runtime::traits::Saturating;
|
||||
use sp_std::fmt::Debug;
|
||||
|
||||
/// Instant message payments made in given currency.
|
||||
///
|
||||
/// The balance is initally reserved in a special `relayers-fund` account, and transferred
|
||||
/// to the relayer when message delivery is confirmed.
|
||||
///
|
||||
/// Additionaly, confirmation transaction submitter (`confirmation_relayer`) is reimbursed
|
||||
/// with the confirmation rewards (part of message fee, reserved to pay for delivery confirmation).
|
||||
///
|
||||
/// NOTE The `relayers-fund` account must always exist i.e. be over Existential Deposit (ED; the
|
||||
/// pallet enforces that) to make sure that even if the message cost is below ED it is still payed
|
||||
/// to the relayer account.
|
||||
/// NOTE It's within relayer's interest to keep their balance above ED as well, to make sure they
|
||||
/// can receive the payment.
|
||||
pub struct InstantCurrencyPayments<T, Currency, GetConfirmationFee, RootAccount> {
|
||||
_phantom: sp_std::marker::PhantomData<(T, Currency, GetConfirmationFee, RootAccount)>,
|
||||
}
|
||||
|
||||
impl<T, Currency, GetConfirmationFee, RootAccount> MessageDeliveryAndDispatchPayment<T::AccountId, Currency::Balance>
|
||||
for InstantCurrencyPayments<T, Currency, GetConfirmationFee, RootAccount>
|
||||
where
|
||||
T: frame_system::Config,
|
||||
Currency: CurrencyT<T::AccountId>,
|
||||
Currency::Balance: From<MessageNonce>,
|
||||
GetConfirmationFee: Get<Currency::Balance>,
|
||||
RootAccount: Get<Option<T::AccountId>>,
|
||||
{
|
||||
type Error = &'static str;
|
||||
|
||||
fn initialize(relayer_fund_account: &T::AccountId) -> usize {
|
||||
assert!(
|
||||
frame_system::Pallet::<T>::account_exists(relayer_fund_account),
|
||||
"The relayer fund account ({:?}) must exist for the message lanes pallet to work correctly.",
|
||||
relayer_fund_account,
|
||||
);
|
||||
1
|
||||
}
|
||||
|
||||
fn pay_delivery_and_dispatch_fee(
|
||||
submitter: &Sender<T::AccountId>,
|
||||
fee: &Currency::Balance,
|
||||
relayer_fund_account: &T::AccountId,
|
||||
) -> Result<(), Self::Error> {
|
||||
let root_account = RootAccount::get();
|
||||
let account = match submitter {
|
||||
Sender::Signed(submitter) => submitter,
|
||||
Sender::Root | Sender::None => root_account
|
||||
.as_ref()
|
||||
.ok_or("Sending messages using Root or None origin is disallowed.")?,
|
||||
};
|
||||
|
||||
Currency::transfer(
|
||||
account,
|
||||
relayer_fund_account,
|
||||
*fee,
|
||||
// it's fine for the submitter to go below Existential Deposit and die.
|
||||
ExistenceRequirement::AllowDeath,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn pay_relayers_rewards(
|
||||
confirmation_relayer: &T::AccountId,
|
||||
relayers_rewards: RelayersRewards<T::AccountId, Currency::Balance>,
|
||||
relayer_fund_account: &T::AccountId,
|
||||
) {
|
||||
pay_relayers_rewards::<Currency, _>(
|
||||
confirmation_relayer,
|
||||
relayers_rewards,
|
||||
relayer_fund_account,
|
||||
GetConfirmationFee::get(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pay rewards to given relayers, optionally rewarding confirmation relayer.
|
||||
fn pay_relayers_rewards<Currency, AccountId>(
|
||||
confirmation_relayer: &AccountId,
|
||||
relayers_rewards: RelayersRewards<AccountId, Currency::Balance>,
|
||||
relayer_fund_account: &AccountId,
|
||||
confirmation_fee: Currency::Balance,
|
||||
) where
|
||||
AccountId: Debug + Default + Encode + PartialEq,
|
||||
Currency: CurrencyT<AccountId>,
|
||||
Currency::Balance: From<u64>,
|
||||
{
|
||||
// reward every relayer except `confirmation_relayer`
|
||||
let mut confirmation_relayer_reward = Currency::Balance::zero();
|
||||
for (relayer, reward) in relayers_rewards {
|
||||
let mut relayer_reward = reward.reward;
|
||||
|
||||
if relayer != *confirmation_relayer {
|
||||
// If delivery confirmation is submitted by other relayer, let's deduct confirmation fee
|
||||
// from relayer reward.
|
||||
//
|
||||
// If confirmation fee has been increased (or if it was the only component of message fee),
|
||||
// then messages relayer may receive zero reward.
|
||||
let mut confirmation_reward = confirmation_fee.saturating_mul(reward.messages.into());
|
||||
if confirmation_reward > relayer_reward {
|
||||
confirmation_reward = relayer_reward;
|
||||
}
|
||||
relayer_reward = relayer_reward.saturating_sub(confirmation_reward);
|
||||
confirmation_relayer_reward = confirmation_relayer_reward.saturating_add(confirmation_reward);
|
||||
} else {
|
||||
// If delivery confirmation is submitted by this relayer, let's add confirmation fee
|
||||
// from other relayers to this relayer reward.
|
||||
confirmation_relayer_reward = confirmation_relayer_reward.saturating_add(reward.reward);
|
||||
continue;
|
||||
}
|
||||
|
||||
pay_relayer_reward::<Currency, _>(relayer_fund_account, &relayer, relayer_reward);
|
||||
}
|
||||
|
||||
// finally - pay reward to confirmation relayer
|
||||
pay_relayer_reward::<Currency, _>(relayer_fund_account, confirmation_relayer, confirmation_relayer_reward);
|
||||
}
|
||||
|
||||
/// Transfer funds from relayers fund account to given relayer.
|
||||
fn pay_relayer_reward<Currency, AccountId>(
|
||||
relayer_fund_account: &AccountId,
|
||||
relayer_account: &AccountId,
|
||||
reward: Currency::Balance,
|
||||
) where
|
||||
AccountId: Debug,
|
||||
Currency: CurrencyT<AccountId>,
|
||||
{
|
||||
if reward.is_zero() {
|
||||
return;
|
||||
}
|
||||
|
||||
let pay_result = Currency::transfer(
|
||||
relayer_fund_account,
|
||||
relayer_account,
|
||||
reward,
|
||||
// the relayer fund account must stay above ED (needs to be pre-funded)
|
||||
ExistenceRequirement::KeepAlive,
|
||||
);
|
||||
|
||||
match pay_result {
|
||||
Ok(_) => log::trace!(
|
||||
target: "runtime::bridge-messages",
|
||||
"Rewarded relayer {:?} with {:?}",
|
||||
relayer_account,
|
||||
reward,
|
||||
),
|
||||
Err(error) => log::trace!(
|
||||
target: "runtime::bridge-messages",
|
||||
"Failed to pay relayer {:?} reward {:?}: {:?}",
|
||||
relayer_account,
|
||||
reward,
|
||||
error,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{run_test, AccountId as TestAccountId, Balance as TestBalance, TestRuntime};
|
||||
use bp_messages::source_chain::RelayerRewards;
|
||||
|
||||
type Balances = pallet_balances::Pallet<TestRuntime>;
|
||||
|
||||
const RELAYER_1: TestAccountId = 1;
|
||||
const RELAYER_2: TestAccountId = 2;
|
||||
const RELAYER_3: TestAccountId = 3;
|
||||
const RELAYERS_FUND_ACCOUNT: TestAccountId = crate::mock::ENDOWED_ACCOUNT;
|
||||
|
||||
fn relayers_rewards() -> RelayersRewards<TestAccountId, TestBalance> {
|
||||
vec![
|
||||
(
|
||||
RELAYER_1,
|
||||
RelayerRewards {
|
||||
reward: 100,
|
||||
messages: 2,
|
||||
},
|
||||
),
|
||||
(
|
||||
RELAYER_2,
|
||||
RelayerRewards {
|
||||
reward: 100,
|
||||
messages: 3,
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmation_relayer_is_rewarded_if_it_has_also_delivered_messages() {
|
||||
run_test(|| {
|
||||
pay_relayers_rewards::<Balances, _>(&RELAYER_2, relayers_rewards(), &RELAYERS_FUND_ACCOUNT, 10);
|
||||
|
||||
assert_eq!(Balances::free_balance(&RELAYER_1), 80);
|
||||
assert_eq!(Balances::free_balance(&RELAYER_2), 120);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmation_relayer_is_rewarded_if_it_has_not_delivered_any_delivered_messages() {
|
||||
run_test(|| {
|
||||
pay_relayers_rewards::<Balances, _>(&RELAYER_3, relayers_rewards(), &RELAYERS_FUND_ACCOUNT, 10);
|
||||
|
||||
assert_eq!(Balances::free_balance(&RELAYER_1), 80);
|
||||
assert_eq!(Balances::free_balance(&RELAYER_2), 70);
|
||||
assert_eq!(Balances::free_balance(&RELAYER_3), 50);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_confirmation_relayer_is_rewarded_if_confirmation_fee_has_significantly_increased() {
|
||||
run_test(|| {
|
||||
pay_relayers_rewards::<Balances, _>(&RELAYER_3, relayers_rewards(), &RELAYERS_FUND_ACCOUNT, 1000);
|
||||
|
||||
assert_eq!(Balances::free_balance(&RELAYER_1), 0);
|
||||
assert_eq!(Balances::free_balance(&RELAYER_2), 0);
|
||||
assert_eq!(Balances::free_balance(&RELAYER_3), 200);
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,404 @@
|
||||
// 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/>.
|
||||
|
||||
// From construct_runtime macro
|
||||
#![allow(clippy::from_over_into)]
|
||||
|
||||
use crate::Config;
|
||||
|
||||
use bp_messages::{
|
||||
source_chain::{
|
||||
LaneMessageVerifier, MessageDeliveryAndDispatchPayment, RelayersRewards, Sender, TargetHeaderChain,
|
||||
},
|
||||
target_chain::{DispatchMessage, MessageDispatch, ProvedLaneMessages, ProvedMessages, SourceHeaderChain},
|
||||
InboundLaneData, LaneId, Message, MessageData, MessageKey, MessageNonce, OutboundLaneData,
|
||||
Parameter as MessagesParameter,
|
||||
};
|
||||
use bp_runtime::Size;
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::{parameter_types, weights::Weight};
|
||||
use sp_core::H256;
|
||||
use sp_runtime::{
|
||||
testing::Header as SubstrateHeader,
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
FixedU128, Perbill,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub type AccountId = u64;
|
||||
pub type Balance = u64;
|
||||
#[derive(Decode, Encode, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TestPayload(pub u64, pub Weight);
|
||||
pub type TestMessageFee = u64;
|
||||
pub type TestRelayer = u64;
|
||||
|
||||
pub struct AccountIdConverter;
|
||||
|
||||
impl sp_runtime::traits::Convert<H256, AccountId> for AccountIdConverter {
|
||||
fn convert(hash: H256) -> AccountId {
|
||||
hash.to_low_u64_ne()
|
||||
}
|
||||
}
|
||||
|
||||
type Block = frame_system::mocking::MockBlock<TestRuntime>;
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<TestRuntime>;
|
||||
|
||||
use crate as pallet_bridge_messages;
|
||||
|
||||
frame_support::construct_runtime! {
|
||||
pub enum TestRuntime where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
|
||||
Balances: pallet_balances::{Pallet, Call, Event<T>},
|
||||
Messages: pallet_bridge_messages::{Pallet, Call, Event<T>},
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub const MaximumBlockWeight: Weight = 1024;
|
||||
pub const MaximumBlockLength: u32 = 2 * 1024;
|
||||
pub const AvailableBlockRatio: Perbill = Perbill::one();
|
||||
}
|
||||
|
||||
impl frame_system::Config for TestRuntime {
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type Call = Call;
|
||||
type BlockNumber = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = SubstrateHeader;
|
||||
type Event = Event;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pallet_balances::AccountData<Balance>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type BaseCallFilter = ();
|
||||
type SystemWeightInfo = ();
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: u64 = 1;
|
||||
}
|
||||
|
||||
impl pallet_balances::Config for TestRuntime {
|
||||
type MaxLocks = ();
|
||||
type Balance = Balance;
|
||||
type DustRemoval = ();
|
||||
type Event = Event;
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = frame_system::Pallet<TestRuntime>;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const MaxMessagesToPruneAtOnce: u64 = 10;
|
||||
pub const MaxUnrewardedRelayerEntriesAtInboundLane: u64 = 16;
|
||||
pub const MaxUnconfirmedMessagesAtInboundLane: u64 = 32;
|
||||
pub storage TokenConversionRate: FixedU128 = 1.into();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
|
||||
pub enum TestMessagesParameter {
|
||||
TokenConversionRate(FixedU128),
|
||||
}
|
||||
|
||||
impl MessagesParameter for TestMessagesParameter {
|
||||
fn save(&self) {
|
||||
match *self {
|
||||
TestMessagesParameter::TokenConversionRate(conversion_rate) => TokenConversionRate::set(&conversion_rate),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config for TestRuntime {
|
||||
type Event = Event;
|
||||
type WeightInfo = ();
|
||||
type Parameter = TestMessagesParameter;
|
||||
type MaxMessagesToPruneAtOnce = MaxMessagesToPruneAtOnce;
|
||||
type MaxUnrewardedRelayerEntriesAtInboundLane = MaxUnrewardedRelayerEntriesAtInboundLane;
|
||||
type MaxUnconfirmedMessagesAtInboundLane = MaxUnconfirmedMessagesAtInboundLane;
|
||||
|
||||
type OutboundPayload = TestPayload;
|
||||
type OutboundMessageFee = TestMessageFee;
|
||||
|
||||
type InboundPayload = TestPayload;
|
||||
type InboundMessageFee = TestMessageFee;
|
||||
type InboundRelayer = TestRelayer;
|
||||
|
||||
type AccountIdConverter = AccountIdConverter;
|
||||
|
||||
type TargetHeaderChain = TestTargetHeaderChain;
|
||||
type LaneMessageVerifier = TestLaneMessageVerifier;
|
||||
type MessageDeliveryAndDispatchPayment = TestMessageDeliveryAndDispatchPayment;
|
||||
|
||||
type SourceHeaderChain = TestSourceHeaderChain;
|
||||
type MessageDispatch = TestMessageDispatch;
|
||||
}
|
||||
|
||||
impl Size for TestPayload {
|
||||
fn size_hint(&self) -> u32 {
|
||||
16
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Error that is returned by all test implementations.
|
||||
pub const TEST_ERROR: &str = "Test error";
|
||||
|
||||
/// Lane that we're using in tests.
|
||||
pub const TEST_LANE_ID: LaneId = [0, 0, 0, 1];
|
||||
|
||||
/// Regular message payload.
|
||||
pub const REGULAR_PAYLOAD: TestPayload = TestPayload(0, 50);
|
||||
|
||||
/// Payload that is rejected by `TestTargetHeaderChain`.
|
||||
pub const PAYLOAD_REJECTED_BY_TARGET_CHAIN: TestPayload = TestPayload(1, 50);
|
||||
|
||||
/// Vec of proved messages, grouped by lane.
|
||||
pub type MessagesByLaneVec = Vec<(LaneId, ProvedLaneMessages<Message<TestMessageFee>>)>;
|
||||
|
||||
/// Test messages proof.
|
||||
#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq)]
|
||||
pub struct TestMessagesProof {
|
||||
pub result: Result<MessagesByLaneVec, ()>,
|
||||
}
|
||||
|
||||
impl Size for TestMessagesProof {
|
||||
fn size_hint(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<Vec<Message<TestMessageFee>>, ()>> for TestMessagesProof {
|
||||
fn from(result: Result<Vec<Message<TestMessageFee>>, ()>) -> Self {
|
||||
Self {
|
||||
result: result.map(|messages| {
|
||||
let mut messages_by_lane: BTreeMap<LaneId, ProvedLaneMessages<Message<TestMessageFee>>> =
|
||||
BTreeMap::new();
|
||||
for message in messages {
|
||||
messages_by_lane
|
||||
.entry(message.key.lane_id)
|
||||
.or_default()
|
||||
.messages
|
||||
.push(message);
|
||||
}
|
||||
messages_by_lane.into_iter().collect()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Messages delivery proof used in tests.
|
||||
#[derive(Debug, Encode, Decode, Eq, Clone, PartialEq)]
|
||||
pub struct TestMessagesDeliveryProof(pub Result<(LaneId, InboundLaneData<TestRelayer>), ()>);
|
||||
|
||||
impl Size for TestMessagesDeliveryProof {
|
||||
fn size_hint(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Target header chain that is used in tests.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TestTargetHeaderChain;
|
||||
|
||||
impl TargetHeaderChain<TestPayload, TestRelayer> for TestTargetHeaderChain {
|
||||
type Error = &'static str;
|
||||
|
||||
type MessagesDeliveryProof = TestMessagesDeliveryProof;
|
||||
|
||||
fn verify_message(payload: &TestPayload) -> Result<(), Self::Error> {
|
||||
if *payload == PAYLOAD_REJECTED_BY_TARGET_CHAIN {
|
||||
Err(TEST_ERROR)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_messages_delivery_proof(
|
||||
proof: Self::MessagesDeliveryProof,
|
||||
) -> Result<(LaneId, InboundLaneData<TestRelayer>), Self::Error> {
|
||||
proof.0.map_err(|_| TEST_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
/// Lane message verifier that is used in tests.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TestLaneMessageVerifier;
|
||||
|
||||
impl LaneMessageVerifier<AccountId, TestPayload, TestMessageFee> for TestLaneMessageVerifier {
|
||||
type Error = &'static str;
|
||||
|
||||
fn verify_message(
|
||||
_submitter: &Sender<AccountId>,
|
||||
delivery_and_dispatch_fee: &TestMessageFee,
|
||||
_lane: &LaneId,
|
||||
_lane_outbound_data: &OutboundLaneData,
|
||||
_payload: &TestPayload,
|
||||
) -> Result<(), Self::Error> {
|
||||
if *delivery_and_dispatch_fee != 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(TEST_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Message fee payment system that is used in tests.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TestMessageDeliveryAndDispatchPayment;
|
||||
|
||||
impl TestMessageDeliveryAndDispatchPayment {
|
||||
/// Reject all payments.
|
||||
pub fn reject_payments() {
|
||||
frame_support::storage::unhashed::put(b":reject-message-fee:", &true);
|
||||
}
|
||||
|
||||
/// Returns true if given fee has been paid by given submitter.
|
||||
pub fn is_fee_paid(submitter: AccountId, fee: TestMessageFee) -> bool {
|
||||
frame_support::storage::unhashed::get(b":message-fee:") == Some((Sender::Signed(submitter), fee))
|
||||
}
|
||||
|
||||
/// 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();
|
||||
frame_support::storage::unhashed::take::<bool>(&key).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageDeliveryAndDispatchPayment<AccountId, TestMessageFee> for TestMessageDeliveryAndDispatchPayment {
|
||||
type Error = &'static str;
|
||||
|
||||
fn pay_delivery_and_dispatch_fee(
|
||||
submitter: &Sender<AccountId>,
|
||||
fee: &TestMessageFee,
|
||||
_relayer_fund_account: &AccountId,
|
||||
) -> Result<(), Self::Error> {
|
||||
if frame_support::storage::unhashed::get(b":reject-message-fee:") == Some(true) {
|
||||
return Err(TEST_ERROR);
|
||||
}
|
||||
|
||||
frame_support::storage::unhashed::put(b":message-fee:", &(submitter, fee));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pay_relayers_rewards(
|
||||
_confirmation_relayer: &AccountId,
|
||||
relayers_rewards: RelayersRewards<AccountId, TestMessageFee>,
|
||||
_relayer_fund_account: &AccountId,
|
||||
) {
|
||||
for (relayer, reward) in relayers_rewards {
|
||||
let key = (b":relayer-reward:", relayer, reward.reward).encode();
|
||||
frame_support::storage::unhashed::put(&key, &true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Source header chain that is used in tests.
|
||||
#[derive(Debug)]
|
||||
pub struct TestSourceHeaderChain;
|
||||
|
||||
impl SourceHeaderChain<TestMessageFee> for TestSourceHeaderChain {
|
||||
type Error = &'static str;
|
||||
|
||||
type MessagesProof = TestMessagesProof;
|
||||
|
||||
fn verify_messages_proof(
|
||||
proof: Self::MessagesProof,
|
||||
_messages_count: u32,
|
||||
) -> Result<ProvedMessages<Message<TestMessageFee>>, Self::Error> {
|
||||
proof
|
||||
.result
|
||||
.map(|proof| proof.into_iter().collect())
|
||||
.map_err(|_| TEST_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
/// Source header chain that is used in tests.
|
||||
#[derive(Debug)]
|
||||
pub struct TestMessageDispatch;
|
||||
|
||||
impl MessageDispatch<TestMessageFee> for TestMessageDispatch {
|
||||
type DispatchPayload = TestPayload;
|
||||
|
||||
fn dispatch_weight(message: &DispatchMessage<TestPayload, TestMessageFee>) -> Weight {
|
||||
match message.data.payload.as_ref() {
|
||||
Ok(payload) => payload.1,
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch(_message: DispatchMessage<TestPayload, TestMessageFee>) {}
|
||||
}
|
||||
|
||||
/// Return test lane message with given nonce and payload.
|
||||
pub fn message(nonce: MessageNonce, payload: TestPayload) -> Message<TestMessageFee> {
|
||||
Message {
|
||||
key: MessageKey {
|
||||
lane_id: TEST_LANE_ID,
|
||||
nonce,
|
||||
},
|
||||
data: message_data(payload),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return message data with valid fee for given payload.
|
||||
pub fn message_data(payload: TestPayload) -> MessageData<TestMessageFee> {
|
||||
MessageData {
|
||||
payload: payload.encode(),
|
||||
fee: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run pallet test.
|
||||
pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
|
||||
let mut t = frame_system::GenesisConfig::default()
|
||||
.build_storage::<TestRuntime>()
|
||||
.unwrap();
|
||||
pallet_balances::GenesisConfig::<TestRuntime> {
|
||||
balances: vec![(ENDOWED_ACCOUNT, 1_000_000)],
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
let mut ext = sp_io::TestExternalities::new(t);
|
||||
ext.execute_with(test)
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// 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/>.
|
||||
|
||||
//! Everything about outgoing messages sending.
|
||||
|
||||
use bp_messages::{LaneId, MessageData, MessageNonce, OutboundLaneData};
|
||||
|
||||
/// Outbound lane storage.
|
||||
pub trait OutboundLaneStorage {
|
||||
/// Delivery and dispatch fee type on source chain.
|
||||
type MessageFee;
|
||||
|
||||
/// Lane id.
|
||||
fn id(&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<MessageData<Self::MessageFee>>;
|
||||
/// Save outbound message in the storage.
|
||||
fn save_message(&mut self, nonce: MessageNonce, message_data: MessageData<Self::MessageFee>);
|
||||
/// Remove outbound message from the storage.
|
||||
fn remove_message(&mut self, nonce: &MessageNonce);
|
||||
}
|
||||
|
||||
/// Outbound messages lane.
|
||||
pub struct OutboundLane<S> {
|
||||
storage: S,
|
||||
}
|
||||
|
||||
impl<S: OutboundLaneStorage> OutboundLane<S> {
|
||||
/// Create new inbound 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()
|
||||
}
|
||||
|
||||
/// Send message over lane.
|
||||
///
|
||||
/// Returns new message nonce.
|
||||
pub fn send_message(&mut self, message_data: MessageData<S::MessageFee>) -> 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_data);
|
||||
self.storage.set_data(data);
|
||||
|
||||
nonce
|
||||
}
|
||||
|
||||
/// Confirm messages delivery.
|
||||
///
|
||||
/// Returns `None` if confirmation is wrong/duplicate.
|
||||
/// Returns `Some` with inclusive ranges of message nonces that have been received.
|
||||
pub fn confirm_delivery(&mut self, latest_received_nonce: MessageNonce) -> Option<(MessageNonce, MessageNonce)> {
|
||||
let mut data = self.storage.data();
|
||||
if latest_received_nonce <= data.latest_received_nonce || latest_received_nonce > data.latest_generated_nonce {
|
||||
return None;
|
||||
}
|
||||
|
||||
let prev_latest_received_nonce = data.latest_received_nonce;
|
||||
data.latest_received_nonce = latest_received_nonce;
|
||||
self.storage.set_data(data);
|
||||
|
||||
Some((prev_latest_received_nonce + 1, latest_received_nonce))
|
||||
}
|
||||
|
||||
/// Prune at most `max_messages_to_prune` already received messages.
|
||||
///
|
||||
/// Returns number of pruned messages.
|
||||
pub fn prune_messages(&mut self, max_messages_to_prune: MessageNonce) -> MessageNonce {
|
||||
let mut pruned_messages = 0;
|
||||
let mut anything_changed = false;
|
||||
let mut data = self.storage.data();
|
||||
while pruned_messages < max_messages_to_prune && data.oldest_unpruned_nonce <= data.latest_received_nonce {
|
||||
self.storage.remove_message(&data.oldest_unpruned_nonce);
|
||||
|
||||
anything_changed = true;
|
||||
pruned_messages += 1;
|
||||
data.oldest_unpruned_nonce += 1;
|
||||
}
|
||||
|
||||
if anything_changed {
|
||||
self.storage.set_data(data);
|
||||
}
|
||||
|
||||
pruned_messages
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
mock::{message_data, run_test, TestRuntime, REGULAR_PAYLOAD, TEST_LANE_ID},
|
||||
outbound_lane,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn send_message_works() {
|
||||
run_test(|| {
|
||||
let mut lane = outbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
assert_eq!(lane.storage.data().latest_generated_nonce, 0);
|
||||
assert_eq!(lane.send_message(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 = outbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
assert_eq!(lane.send_message(message_data(REGULAR_PAYLOAD)), 1);
|
||||
assert_eq!(lane.send_message(message_data(REGULAR_PAYLOAD)), 2);
|
||||
assert_eq!(lane.send_message(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.confirm_delivery(3), Some((1, 3)));
|
||||
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
|
||||
assert_eq!(lane.storage.data().latest_received_nonce, 3);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_delivery_rejects_nonce_lesser_than_latest_received() {
|
||||
run_test(|| {
|
||||
let mut lane = outbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
lane.send_message(message_data(REGULAR_PAYLOAD));
|
||||
lane.send_message(message_data(REGULAR_PAYLOAD));
|
||||
lane.send_message(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.confirm_delivery(3), Some((1, 3)));
|
||||
assert_eq!(lane.confirm_delivery(3), None);
|
||||
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
|
||||
assert_eq!(lane.storage.data().latest_received_nonce, 3);
|
||||
|
||||
assert_eq!(lane.confirm_delivery(2), None);
|
||||
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
|
||||
assert_eq!(lane.storage.data().latest_received_nonce, 3);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_delivery_rejects_nonce_larger_than_last_generated() {
|
||||
run_test(|| {
|
||||
let mut lane = outbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
lane.send_message(message_data(REGULAR_PAYLOAD));
|
||||
lane.send_message(message_data(REGULAR_PAYLOAD));
|
||||
lane.send_message(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.confirm_delivery(10), None);
|
||||
assert_eq!(lane.storage.data().latest_generated_nonce, 3);
|
||||
assert_eq!(lane.storage.data().latest_received_nonce, 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prune_messages_works() {
|
||||
run_test(|| {
|
||||
let mut lane = outbound_lane::<TestRuntime, _>(TEST_LANE_ID);
|
||||
// when lane is empty, nothing is pruned
|
||||
assert_eq!(lane.prune_messages(100), 0);
|
||||
assert_eq!(lane.storage.data().oldest_unpruned_nonce, 1);
|
||||
// when nothing is confirmed, nothing is pruned
|
||||
lane.send_message(message_data(REGULAR_PAYLOAD));
|
||||
lane.send_message(message_data(REGULAR_PAYLOAD));
|
||||
lane.send_message(message_data(REGULAR_PAYLOAD));
|
||||
assert_eq!(lane.prune_messages(100), 0);
|
||||
assert_eq!(lane.storage.data().oldest_unpruned_nonce, 1);
|
||||
// after confirmation, some messages are received
|
||||
assert_eq!(lane.confirm_delivery(2), Some((1, 2)));
|
||||
assert_eq!(lane.prune_messages(100), 2);
|
||||
assert_eq!(lane.storage.data().oldest_unpruned_nonce, 3);
|
||||
// after last message is confirmed, everything is pruned
|
||||
assert_eq!(lane.confirm_delivery(3), Some((3, 3)));
|
||||
assert_eq!(lane.prune_messages(100), 1);
|
||||
assert_eq!(lane.storage.data().oldest_unpruned_nonce, 4);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
// 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/>.
|
||||
|
||||
//! Autogenerated weights for pallet_bridge_messages
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0
|
||||
//! DATE: 2021-04-14, STEPS: [50, ], REPEAT: 20
|
||||
//! LOW RANGE: [], HIGH RANGE: []
|
||||
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled
|
||||
//! CHAIN: Some("dev"), DB CACHE: 128
|
||||
|
||||
// Executed Command:
|
||||
// target/release/rialto-bridge-node
|
||||
// benchmark
|
||||
// --chain=dev
|
||||
// --steps=50
|
||||
// --repeat=20
|
||||
// --pallet=pallet_bridge_messages
|
||||
// --extrinsic=*
|
||||
// --execution=wasm
|
||||
// --wasm-execution=Compiled
|
||||
// --heap-pages=4096
|
||||
// --output=./modules/messages/src/weights.rs
|
||||
// --template=./.maintain/rialto-weight-template.hbs
|
||||
|
||||
#![allow(clippy::all)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
|
||||
use frame_support::{
|
||||
traits::Get,
|
||||
weights::{constants::RocksDbWeight, Weight},
|
||||
};
|
||||
use sp_std::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for pallet_bridge_messages.
|
||||
pub trait WeightInfo {
|
||||
fn send_minimal_message_worst_case() -> Weight;
|
||||
fn send_1_kb_message_worst_case() -> Weight;
|
||||
fn send_16_kb_message_worst_case() -> Weight;
|
||||
fn increase_message_fee() -> Weight;
|
||||
fn receive_single_message_proof() -> Weight;
|
||||
fn receive_two_messages_proof() -> Weight;
|
||||
fn receive_single_message_proof_with_outbound_lane_state() -> Weight;
|
||||
fn receive_single_message_proof_1_kb() -> Weight;
|
||||
fn receive_single_message_proof_16_kb() -> 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 send_messages_of_various_lengths(i: u32) -> Weight;
|
||||
fn receive_multiple_messages_proof(i: u32) -> Weight;
|
||||
fn receive_message_proofs_with_extra_nodes(i: u32) -> Weight;
|
||||
fn receive_message_proofs_with_large_leaf(i: u32) -> Weight;
|
||||
fn receive_multiple_messages_proof_with_outbound_lane_state(i: u32) -> Weight;
|
||||
fn receive_delivery_proof_for_multiple_messages_by_single_relayer(i: u32) -> Weight;
|
||||
fn receive_delivery_proof_for_multiple_messages_by_multiple_relayers(i: u32) -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for pallet_bridge_messages using the Rialto node and recommended hardware.
|
||||
pub struct RialtoWeight<T>(PhantomData<T>);
|
||||
impl<T: frame_system::Config> WeightInfo for RialtoWeight<T> {
|
||||
fn send_minimal_message_worst_case() -> Weight {
|
||||
(149_497_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(12 as Weight))
|
||||
}
|
||||
fn send_1_kb_message_worst_case() -> Weight {
|
||||
(154_339_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(12 as Weight))
|
||||
}
|
||||
fn send_16_kb_message_worst_case() -> Weight {
|
||||
(200_066_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(12 as Weight))
|
||||
}
|
||||
fn increase_message_fee() -> Weight {
|
||||
(6_432_637_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(4 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(3 as Weight))
|
||||
}
|
||||
fn receive_single_message_proof() -> Weight {
|
||||
(141_671_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_two_messages_proof() -> Weight {
|
||||
(247_393_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_single_message_proof_with_outbound_lane_state() -> Weight {
|
||||
(159_312_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_single_message_proof_1_kb() -> Weight {
|
||||
(167_935_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_single_message_proof_16_kb() -> Weight {
|
||||
(449_846_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_delivery_proof_for_single_message() -> Weight {
|
||||
(127_322_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(6 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(3 as Weight))
|
||||
}
|
||||
fn receive_delivery_proof_for_two_messages_by_single_relayer() -> Weight {
|
||||
(134_120_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(7 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(3 as Weight))
|
||||
}
|
||||
fn receive_delivery_proof_for_two_messages_by_two_relayers() -> Weight {
|
||||
(191_193_000 as Weight)
|
||||
.saturating_add(T::DbWeight::get().reads(8 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
fn send_messages_of_various_lengths(i: u32) -> Weight {
|
||||
(115_699_000 as Weight)
|
||||
.saturating_add((3_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(12 as Weight))
|
||||
}
|
||||
fn receive_multiple_messages_proof(i: u32) -> Weight {
|
||||
(0 as Weight)
|
||||
.saturating_add((113_551_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_message_proofs_with_extra_nodes(i: u32) -> Weight {
|
||||
(458_731_000 as Weight)
|
||||
.saturating_add((9_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_message_proofs_with_large_leaf(i: u32) -> Weight {
|
||||
(82_314_000 as Weight)
|
||||
.saturating_add((7_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_multiple_messages_proof_with_outbound_lane_state(i: u32) -> Weight {
|
||||
(16_766_000 as Weight)
|
||||
.saturating_add((115_533_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_delivery_proof_for_multiple_messages_by_single_relayer(i: u32) -> Weight {
|
||||
(122_146_000 as Weight)
|
||||
.saturating_add((6_789_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
|
||||
.saturating_add(T::DbWeight::get().writes(3 as Weight))
|
||||
}
|
||||
fn receive_delivery_proof_for_multiple_messages_by_multiple_relayers(i: u32) -> Weight {
|
||||
(155_671_000 as Weight)
|
||||
.saturating_add((63_020_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(T::DbWeight::get().reads((2 as Weight).saturating_mul(i as Weight)))
|
||||
.saturating_add(T::DbWeight::get().writes(3 as Weight))
|
||||
.saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(i as Weight)))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests
|
||||
impl WeightInfo for () {
|
||||
fn send_minimal_message_worst_case() -> Weight {
|
||||
(149_497_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(12 as Weight))
|
||||
}
|
||||
fn send_1_kb_message_worst_case() -> Weight {
|
||||
(154_339_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(12 as Weight))
|
||||
}
|
||||
fn send_16_kb_message_worst_case() -> Weight {
|
||||
(200_066_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(12 as Weight))
|
||||
}
|
||||
fn increase_message_fee() -> Weight {
|
||||
(6_432_637_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(4 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(3 as Weight))
|
||||
}
|
||||
fn receive_single_message_proof() -> Weight {
|
||||
(141_671_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_two_messages_proof() -> Weight {
|
||||
(247_393_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_single_message_proof_with_outbound_lane_state() -> Weight {
|
||||
(159_312_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_single_message_proof_1_kb() -> Weight {
|
||||
(167_935_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_single_message_proof_16_kb() -> Weight {
|
||||
(449_846_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_delivery_proof_for_single_message() -> Weight {
|
||||
(127_322_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(6 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(3 as Weight))
|
||||
}
|
||||
fn receive_delivery_proof_for_two_messages_by_single_relayer() -> Weight {
|
||||
(134_120_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(7 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(3 as Weight))
|
||||
}
|
||||
fn receive_delivery_proof_for_two_messages_by_two_relayers() -> Weight {
|
||||
(191_193_000 as Weight)
|
||||
.saturating_add(RocksDbWeight::get().reads(8 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
|
||||
}
|
||||
fn send_messages_of_various_lengths(i: u32) -> Weight {
|
||||
(115_699_000 as Weight)
|
||||
.saturating_add((3_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(12 as Weight))
|
||||
}
|
||||
fn receive_multiple_messages_proof(i: u32) -> Weight {
|
||||
(0 as Weight)
|
||||
.saturating_add((113_551_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_message_proofs_with_extra_nodes(i: u32) -> Weight {
|
||||
(458_731_000 as Weight)
|
||||
.saturating_add((9_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_message_proofs_with_large_leaf(i: u32) -> Weight {
|
||||
(82_314_000 as Weight)
|
||||
.saturating_add((7_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_multiple_messages_proof_with_outbound_lane_state(i: u32) -> Weight {
|
||||
(16_766_000 as Weight)
|
||||
.saturating_add((115_533_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
|
||||
}
|
||||
fn receive_delivery_proof_for_multiple_messages_by_single_relayer(i: u32) -> Weight {
|
||||
(122_146_000 as Weight)
|
||||
.saturating_add((6_789_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
|
||||
.saturating_add(RocksDbWeight::get().writes(3 as Weight))
|
||||
}
|
||||
fn receive_delivery_proof_for_multiple_messages_by_multiple_relayers(i: u32) -> Weight {
|
||||
(155_671_000 as Weight)
|
||||
.saturating_add((63_020_000 as Weight).saturating_mul(i as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads(5 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().reads((2 as Weight).saturating_mul(i as Weight)))
|
||||
.saturating_add(RocksDbWeight::get().writes(3 as Weight))
|
||||
.saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(i as Weight)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
// 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/>.
|
||||
|
||||
//! Weight-related utilities.
|
||||
|
||||
use crate::weights::WeightInfo;
|
||||
|
||||
use bp_messages::{MessageNonce, UnrewardedRelayersState};
|
||||
use bp_runtime::{PreComputedSize, Size};
|
||||
use frame_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;
|
||||
|
||||
/// Ensure that weights from `WeightInfoExt` implementation are looking correct.
|
||||
pub fn ensure_weights_are_correct<W: WeightInfoExt>(
|
||||
expected_default_message_delivery_tx_weight: Weight,
|
||||
expected_additional_byte_delivery_weight: Weight,
|
||||
expected_messages_delivery_confirmation_tx_weight: Weight,
|
||||
) {
|
||||
// verify `send_message` weight components
|
||||
assert_ne!(W::send_message_overhead(), 0);
|
||||
assert_ne!(W::send_message_size_overhead(0), 0);
|
||||
|
||||
// verify `receive_messages_proof` weight components
|
||||
assert_ne!(W::receive_messages_proof_overhead(), 0);
|
||||
assert_ne!(W::receive_messages_proof_messages_overhead(1), 0);
|
||||
assert_ne!(W::receive_messages_proof_outbound_lane_state_overhead(), 0);
|
||||
assert_ne!(W::storage_proof_size_overhead(1), 0);
|
||||
|
||||
// verify that the hardcoded value covers `receive_messages_proof` weight
|
||||
let actual_single_regular_message_delivery_tx_weight = W::receive_messages_proof_weight(
|
||||
&PreComputedSize((EXPECTED_DEFAULT_MESSAGE_LENGTH + W::expected_extra_storage_proof_size()) as usize),
|
||||
1,
|
||||
0,
|
||||
);
|
||||
assert!(
|
||||
actual_single_regular_message_delivery_tx_weight <= expected_default_message_delivery_tx_weight,
|
||||
"Default message delivery transaction weight {} is larger than expected weight {}",
|
||||
actual_single_regular_message_delivery_tx_weight,
|
||||
expected_default_message_delivery_tx_weight,
|
||||
);
|
||||
|
||||
// verify that hardcoded value covers additional byte length of `receive_messages_proof` weight
|
||||
let actual_additional_byte_delivery_weight = W::storage_proof_size_overhead(1);
|
||||
assert!(
|
||||
actual_additional_byte_delivery_weight <= expected_additional_byte_delivery_weight,
|
||||
"Single additional byte delivery weight {} is larger than expected weight {}",
|
||||
actual_additional_byte_delivery_weight,
|
||||
expected_additional_byte_delivery_weight,
|
||||
);
|
||||
|
||||
// verify `receive_messages_delivery_proof` weight components
|
||||
assert_ne!(W::receive_messages_delivery_proof_overhead(), 0);
|
||||
assert_ne!(W::receive_messages_delivery_proof_messages_overhead(1), 0);
|
||||
assert_ne!(W::receive_messages_delivery_proof_relayers_overhead(1), 0);
|
||||
assert_ne!(W::storage_proof_size_overhead(1), 0);
|
||||
|
||||
// verify that the hardcoded value covers `receive_messages_delivery_proof` weight
|
||||
let actual_messages_delivery_confirmation_tx_weight = W::receive_messages_delivery_proof_weight(
|
||||
&PreComputedSize(W::expected_extra_storage_proof_size() as usize),
|
||||
&UnrewardedRelayersState {
|
||||
unrewarded_relayer_entries: 1,
|
||||
total_messages: 1,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(
|
||||
actual_messages_delivery_confirmation_tx_weight <= expected_messages_delivery_confirmation_tx_weight,
|
||||
"Messages delivery confirmation transaction weight {} is larger than expected weight {}",
|
||||
actual_messages_delivery_confirmation_tx_weight,
|
||||
expected_messages_delivery_confirmation_tx_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 {} + {} is larger than maximal possible transaction size {}",
|
||||
max_incoming_message_proof_size,
|
||||
SIGNED_EXTENSIONS_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 <= max_extrinsic_weight,
|
||||
"Weight of maximal message delivery transaction + {} is larger than maximal possible transaction weight {}",
|
||||
max_delivery_transaction_dispatch_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 {} + {} is larger than maximal possible transaction size {}",
|
||||
max_inbound_lane_data_proof_size_from_peer_chain,
|
||||
SIGNED_EXTENSIONS_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 <= max_extrinsic_weight,
|
||||
"Weight of maximal confirmation transaction {} is larger than maximal possible transaction weight {}",
|
||||
max_confirmation_transaction_dispatch_weight,
|
||||
max_extrinsic_weight,
|
||||
);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
// Functions that are directly mapped to extrinsics weights.
|
||||
|
||||
/// Weight of message send extrinsic.
|
||||
fn send_message_weight(message: &impl Size) -> Weight {
|
||||
let transaction_overhead = Self::send_message_overhead();
|
||||
let message_size_overhead = Self::send_message_size_overhead(message.size_hint());
|
||||
|
||||
transaction_overhead.saturating_add(message_size_overhead)
|
||||
}
|
||||
|
||||
/// 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 transaction_overhead = Self::receive_messages_proof_overhead();
|
||||
let outbound_state_delivery_weight = Self::receive_messages_proof_outbound_lane_state_overhead();
|
||||
let messages_delivery_weight =
|
||||
Self::receive_messages_proof_messages_overhead(MessageNonce::from(messages_count));
|
||||
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_hint();
|
||||
let proof_size_overhead =
|
||||
Self::storage_proof_size_overhead(actual_proof_size.saturating_sub(expected_proof_size));
|
||||
|
||||
transaction_overhead
|
||||
.saturating_add(outbound_state_delivery_weight)
|
||||
.saturating_add(messages_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 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_hint();
|
||||
let proof_size_overhead =
|
||||
Self::storage_proof_size_overhead(actual_proof_size.saturating_sub(expected_proof_size));
|
||||
|
||||
transaction_overhead
|
||||
.saturating_add(messages_overhead)
|
||||
.saturating_add(relayers_overhead)
|
||||
.saturating_add(proof_size_overhead)
|
||||
}
|
||||
|
||||
// Functions that are used by extrinsics weights formulas.
|
||||
|
||||
/// Returns weight of message send transaction (`send_message`).
|
||||
fn send_message_overhead() -> Weight {
|
||||
Self::send_minimal_message_worst_case()
|
||||
}
|
||||
|
||||
/// Returns weight that needs to be accounted when message of given size is sent (`send_message`).
|
||||
fn send_message_size_overhead(message_size: u32) -> Weight {
|
||||
let message_size_in_kb = (1024u64 + message_size as u64) / 1024;
|
||||
let single_kb_weight = (Self::send_16_kb_message_worst_case() - Self::send_1_kb_message_worst_case()) / 15;
|
||||
message_size_in_kb * single_kb_weight
|
||||
}
|
||||
|
||||
/// Returns weight overhead of message delivery transaction (`receive_messages_proof`).
|
||||
fn receive_messages_proof_overhead() -> Weight {
|
||||
let weight_of_two_messages_and_two_tx_overheads = Self::receive_single_message_proof().saturating_mul(2);
|
||||
let weight_of_two_messages_and_single_tx_overhead = Self::receive_two_messages_proof();
|
||||
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 given number of messages with message
|
||||
/// delivery transaction (`receive_messages_proof`).
|
||||
fn receive_messages_proof_messages_overhead(messages: MessageNonce) -> Weight {
|
||||
let weight_of_two_messages_and_single_tx_overhead = Self::receive_two_messages_proof();
|
||||
let weight_of_single_message_and_single_tx_overhead = Self::receive_single_message_proof();
|
||||
weight_of_two_messages_and_single_tx_overhead
|
||||
.saturating_sub(weight_of_single_message_and_single_tx_overhead)
|
||||
.saturating_mul(messages as Weight)
|
||||
}
|
||||
|
||||
/// 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 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 Weight)
|
||||
}
|
||||
|
||||
/// Returns weight that needs to be accounted when receiving confirmations for given 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 Weight)
|
||||
}
|
||||
|
||||
/// Returns weight that needs to be accounted when storage proof of given size is recieved (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 as Weight;
|
||||
let byte_weight =
|
||||
(Self::receive_single_message_proof_16_kb() - Self::receive_single_message_proof_1_kb()) / (15 * 1024);
|
||||
proof_size_in_bytes * byte_weight
|
||||
}
|
||||
}
|
||||
|
||||
impl WeightInfoExt for () {
|
||||
fn expected_extra_storage_proof_size() -> u32 {
|
||||
bp_rialto::EXTRA_STORAGE_PROOF_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: frame_system::Config> WeightInfoExt for crate::weights::RialtoWeight<T> {
|
||||
fn expected_extra_storage_proof_size() -> u32 {
|
||||
bp_rialto::EXTRA_STORAGE_PROOF_SIZE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "pallet-shift-session-manager"
|
||||
description = "A Substrate Runtime module that selects 2/3 of initial validators for every session"
|
||||
version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false }
|
||||
|
||||
# Substrate Dependencies
|
||||
|
||||
frame-support = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
frame-system = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
pallet-session = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-staking = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
sp-std = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||
serde = "1.0"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"pallet-session/std",
|
||||
"sp-staking/std",
|
||||
"sp-std/std",
|
||||
]
|
||||
@@ -0,0 +1,228 @@
|
||||
// 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/>.
|
||||
|
||||
//! Substrate session manager that selects 2/3 validators from initial set,
|
||||
//! starting from session 2.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use frame_support::{decl_module, decl_storage};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
/// The module configuration trait.
|
||||
pub trait Config: pallet_session::Config {}
|
||||
|
||||
decl_module! {
|
||||
/// Shift session manager pallet.
|
||||
pub struct Module<T: Config> for enum Call where origin: T::Origin {}
|
||||
}
|
||||
|
||||
decl_storage! {
|
||||
trait Store for Pallet<T: Config> as ShiftSessionManager {
|
||||
/// Validators of first two sessions.
|
||||
InitialValidators: Option<Vec<T::ValidatorId>>;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> pallet_session::SessionManager<T::ValidatorId> for Pallet<T> {
|
||||
fn end_session(_: sp_staking::SessionIndex) {}
|
||||
fn start_session(_: sp_staking::SessionIndex) {}
|
||||
fn new_session(session_index: sp_staking::SessionIndex) -> Option<Vec<T::ValidatorId>> {
|
||||
// we don't want to add even more fields to genesis config => just return None
|
||||
if session_index == 0 || session_index == 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// the idea that on first call (i.e. when session 1 ends) we're reading current
|
||||
// set of validators from session module (they are initial validators) and save
|
||||
// in our 'local storage'.
|
||||
// then for every session we select (deterministically) 2/3 of these initial
|
||||
// validators to serve validators of new session
|
||||
let available_validators = InitialValidators::<T>::get().unwrap_or_else(|| {
|
||||
let validators = <pallet_session::Pallet<T>>::validators();
|
||||
InitialValidators::<T>::put(validators.clone());
|
||||
validators
|
||||
});
|
||||
|
||||
Some(Self::select_validators(session_index, &available_validators))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Select validators for session.
|
||||
fn select_validators(
|
||||
session_index: sp_staking::SessionIndex,
|
||||
available_validators: &[T::ValidatorId],
|
||||
) -> Vec<T::ValidatorId> {
|
||||
let available_validators_count = available_validators.len();
|
||||
let count = sp_std::cmp::max(1, 2 * available_validators_count / 3);
|
||||
let offset = session_index as usize % available_validators_count;
|
||||
let end = offset + count;
|
||||
let session_validators = match end.overflowing_sub(available_validators_count) {
|
||||
(wrapped_end, false) if wrapped_end != 0 => available_validators[offset..]
|
||||
.iter()
|
||||
.chain(available_validators[..wrapped_end].iter())
|
||||
.cloned()
|
||||
.collect(),
|
||||
_ => available_validators[offset..end].to_vec(),
|
||||
};
|
||||
|
||||
session_validators
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// From construct_runtime macro
|
||||
#![allow(clippy::from_over_into)]
|
||||
|
||||
use super::*;
|
||||
use frame_support::sp_io::TestExternalities;
|
||||
use frame_support::sp_runtime::{
|
||||
testing::{Header, UintAuthorityId},
|
||||
traits::{BlakeTwo256, ConvertInto, IdentityLookup},
|
||||
Perbill, RuntimeAppPublic,
|
||||
};
|
||||
use frame_support::{parameter_types, weights::Weight, BasicExternalities};
|
||||
use sp_core::H256;
|
||||
|
||||
type AccountId = u64;
|
||||
|
||||
type Block = frame_system::mocking::MockBlock<TestRuntime>;
|
||||
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<TestRuntime>;
|
||||
|
||||
frame_support::construct_runtime! {
|
||||
pub enum TestRuntime where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic,
|
||||
{
|
||||
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
|
||||
Session: pallet_session::{Pallet},
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub const MaximumBlockWeight: Weight = 1024;
|
||||
pub const MaximumBlockLength: u32 = 2 * 1024;
|
||||
pub const AvailableBlockRatio: Perbill = Perbill::one();
|
||||
}
|
||||
|
||||
impl frame_system::Config for TestRuntime {
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type Call = Call;
|
||||
type BlockNumber = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type Event = ();
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = ();
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type BaseCallFilter = ();
|
||||
type SystemWeightInfo = ();
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const Period: u64 = 1;
|
||||
pub const Offset: u64 = 0;
|
||||
}
|
||||
|
||||
impl pallet_session::Config for TestRuntime {
|
||||
type Event = ();
|
||||
type ValidatorId = <Self as frame_system::Config>::AccountId;
|
||||
type ValidatorIdOf = ConvertInto;
|
||||
type ShouldEndSession = pallet_session::PeriodicSessions<Period, Offset>;
|
||||
type NextSessionRotation = pallet_session::PeriodicSessions<Period, Offset>;
|
||||
type SessionManager = ();
|
||||
type SessionHandler = TestSessionHandler;
|
||||
type Keys = UintAuthorityId;
|
||||
type DisabledValidatorsThreshold = ();
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
impl Config for TestRuntime {}
|
||||
|
||||
pub struct TestSessionHandler;
|
||||
impl pallet_session::SessionHandler<AccountId> for TestSessionHandler {
|
||||
const KEY_TYPE_IDS: &'static [sp_runtime::KeyTypeId] = &[UintAuthorityId::ID];
|
||||
|
||||
fn on_genesis_session<Ks: sp_runtime::traits::OpaqueKeys>(_validators: &[(AccountId, Ks)]) {}
|
||||
|
||||
fn on_new_session<Ks: sp_runtime::traits::OpaqueKeys>(_: bool, _: &[(AccountId, Ks)], _: &[(AccountId, Ks)]) {}
|
||||
|
||||
fn on_disabled(_: usize) {}
|
||||
}
|
||||
|
||||
fn new_test_ext() -> TestExternalities {
|
||||
let mut t = frame_system::GenesisConfig::default()
|
||||
.build_storage::<TestRuntime>()
|
||||
.unwrap();
|
||||
|
||||
let keys = vec![
|
||||
(1, 1, UintAuthorityId(1)),
|
||||
(2, 2, UintAuthorityId(2)),
|
||||
(3, 3, UintAuthorityId(3)),
|
||||
(4, 4, UintAuthorityId(4)),
|
||||
(5, 5, UintAuthorityId(5)),
|
||||
];
|
||||
|
||||
BasicExternalities::execute_with_storage(&mut t, || {
|
||||
for (ref k, ..) in &keys {
|
||||
frame_system::Pallet::<TestRuntime>::inc_providers(k);
|
||||
}
|
||||
});
|
||||
|
||||
pallet_session::GenesisConfig::<TestRuntime> { keys }
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
TestExternalities::new(t)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_session_manager_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let all_accs = vec![1, 2, 3, 4, 5];
|
||||
|
||||
// at least 1 validator is selected
|
||||
assert_eq!(Pallet::<TestRuntime>::select_validators(0, &[1]), vec![1],);
|
||||
|
||||
// at session#0, shift is also 0
|
||||
assert_eq!(Pallet::<TestRuntime>::select_validators(0, &all_accs), vec![1, 2, 3],);
|
||||
|
||||
// at session#1, shift is also 1
|
||||
assert_eq!(Pallet::<TestRuntime>::select_validators(1, &all_accs), vec![2, 3, 4],);
|
||||
|
||||
// at session#3, we're wrapping
|
||||
assert_eq!(Pallet::<TestRuntime>::select_validators(3, &all_accs), vec![4, 5, 1],);
|
||||
|
||||
// at session#5, we're starting from the beginning again
|
||||
assert_eq!(Pallet::<TestRuntime>::select_validators(5, &all_accs), vec![1, 2, 3],);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user