mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-17 03:11:01 +00:00
RefundRelayerForMessagesFromParachain improvements (#1879)
* RefundRelayerForMessagesFromParachain improvements * Address code review comments
This commit is contained in:
committed by
Bastian Köcher
parent
83c3975349
commit
c9c6d566d0
@@ -87,7 +87,7 @@ It'd be better for anyone (for chain and for submitters) to reject all transacti
|
||||
already known headers to the pallet. This way, we leave block space to other useful transactions and
|
||||
we don't charge concurrent submitters for their honest actions.
|
||||
|
||||
To deal with that, we have a [signed extension](./src/extension.rs) that may be added to the runtime.
|
||||
To deal with that, we have a [signed extension](./src/call_ext) that may be added to the runtime.
|
||||
It does exactly what is required - rejects all transactions with already known headers. The submitter
|
||||
pays nothing for such transactions - they're simply removed from the transaction pool, when the block
|
||||
is built.
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
// Copyright 2021 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity Bridges Common.
|
||||
|
||||
// Parity Bridges Common is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity Bridges Common is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{Config, Error, Pallet};
|
||||
use bp_runtime::BlockNumberOf;
|
||||
use frame_support::{dispatch::CallableCallFor, traits::IsSubType};
|
||||
use sp_runtime::{
|
||||
traits::Header,
|
||||
transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction},
|
||||
};
|
||||
|
||||
/// Helper struct that provides methods for working with the `SubmitFinalityProof` call.
|
||||
pub struct SubmitFinalityProofHelper<T: Config<I>, I: 'static> {
|
||||
pub _phantom_data: sp_std::marker::PhantomData<(T, I)>,
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> SubmitFinalityProofHelper<T, I> {
|
||||
/// Check that the GRANDPA head provided by the `SubmitFinalityProof` is better than the best
|
||||
/// one we know.
|
||||
pub fn check_obsolete(
|
||||
finality_target: BlockNumberOf<T::BridgedChain>,
|
||||
) -> Result<(), Error<T, I>> {
|
||||
let best_finalized = crate::BestFinalized::<T, I>::get().ok_or_else(|| {
|
||||
log::trace!(
|
||||
target: crate::LOG_TARGET,
|
||||
"Cannot finalize header {:?} because pallet is not yet initialized",
|
||||
finality_target,
|
||||
);
|
||||
<Error<T, I>>::NotInitialized
|
||||
})?;
|
||||
|
||||
if best_finalized.number() >= finality_target {
|
||||
log::trace!(
|
||||
target: crate::LOG_TARGET,
|
||||
"Cannot finalize obsolete header: bundled {:?}, best {:?}",
|
||||
finality_target,
|
||||
best_finalized,
|
||||
);
|
||||
|
||||
return Err(Error::<T, I>::OldHeader)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the `SubmitFinalityProof` was successfully executed.
|
||||
pub fn was_successful(finality_target: BlockNumberOf<T::BridgedChain>) -> bool {
|
||||
match crate::BestFinalized::<T, I>::get() {
|
||||
Some(best_finalized) => best_finalized.number() == finality_target,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait representing a call that is a sub type of this pallet's call.
|
||||
pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
|
||||
IsSubType<CallableCallFor<Pallet<T, I>, T>>
|
||||
{
|
||||
/// Extract the finality target from a `SubmitParachainHeads` call.
|
||||
fn submit_finality_proof_info(&self) -> Option<BlockNumberOf<T::BridgedChain>> {
|
||||
if let Some(crate::Call::<T, I>::submit_finality_proof { finality_target, .. }) =
|
||||
self.is_sub_type()
|
||||
{
|
||||
return Some(*finality_target.number())
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Validate Grandpa headers in order to avoid "mining" transactions that provide outdated
|
||||
/// bridged chain headers. Without this validation, even honest relayers may lose their funds
|
||||
/// if there are multiple relays running and submitting the same information.
|
||||
fn check_obsolete_submit_finality_proof(&self) -> TransactionValidity
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let finality_target = match self.submit_finality_proof_info() {
|
||||
Some(finality_proof) => finality_proof,
|
||||
_ => return Ok(ValidTransaction::default()),
|
||||
};
|
||||
|
||||
match SubmitFinalityProofHelper::<T, I>::check_obsolete(finality_target) {
|
||||
Ok(_) => Ok(ValidTransaction::default()),
|
||||
Err(Error::<T, I>::OldHeader) => InvalidTransaction::Stale.into(),
|
||||
Err(_) => InvalidTransaction::Call.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> CallSubType<T, I> for T::RuntimeCall where
|
||||
T::RuntimeCall: IsSubType<CallableCallFor<Pallet<T, I>, T>>
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
call_ext::CallSubType,
|
||||
mock::{run_test, test_header, RuntimeCall, TestNumber, TestRuntime},
|
||||
BestFinalized,
|
||||
};
|
||||
use bp_runtime::HeaderId;
|
||||
use bp_test_utils::make_default_justification;
|
||||
|
||||
fn validate_block_submit(num: TestNumber) -> bool {
|
||||
let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof {
|
||||
finality_target: Box::new(test_header(num)),
|
||||
justification: make_default_justification(&test_header(num)),
|
||||
};
|
||||
RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
|
||||
bridge_grandpa_call,
|
||||
))
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn sync_to_header_10() {
|
||||
let header10_hash = sp_core::H256::default();
|
||||
BestFinalized::<TestRuntime, ()>::put(HeaderId(10, header10_hash));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_rejects_obsolete_header() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#5 => tx is
|
||||
// rejected
|
||||
sync_to_header_10();
|
||||
assert!(!validate_block_submit(5));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_rejects_same_header() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#10 => tx is
|
||||
// rejected
|
||||
sync_to_header_10();
|
||||
assert!(!validate_block_submit(10));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_accepts_new_header() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#15 => tx is
|
||||
// accepted
|
||||
sync_to_header_10();
|
||||
assert!(validate_block_submit(15));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
// Copyright 2021 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity Bridges Common.
|
||||
|
||||
// Parity Bridges Common is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity Bridges Common is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{Config, Pallet};
|
||||
use bp_runtime::FilterCall;
|
||||
use frame_support::{dispatch::CallableCallFor, traits::IsSubType};
|
||||
use sp_runtime::{
|
||||
traits::Header,
|
||||
transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction},
|
||||
};
|
||||
|
||||
/// Validate Grandpa headers in order to avoid "mining" transactions that provide outdated
|
||||
/// bridged chain headers. Without this validation, even honest relayers may lose their funds
|
||||
/// if there are multiple relays running and submitting the same information.
|
||||
impl<
|
||||
Call: IsSubType<CallableCallFor<Pallet<T, I>, T>>,
|
||||
T: frame_system::Config<RuntimeCall = Call> + Config<I>,
|
||||
I: 'static,
|
||||
> FilterCall<Call> for Pallet<T, I>
|
||||
{
|
||||
fn validate(call: &<T as frame_system::Config>::RuntimeCall) -> TransactionValidity {
|
||||
let bundled_block_number = match call.is_sub_type() {
|
||||
Some(crate::Call::<T, I>::submit_finality_proof { ref finality_target, .. }) =>
|
||||
*finality_target.number(),
|
||||
_ => return Ok(ValidTransaction::default()),
|
||||
};
|
||||
|
||||
let best_finalized = crate::BestFinalized::<T, I>::get();
|
||||
let best_finalized_number = match best_finalized {
|
||||
Some(best_finalized_id) => best_finalized_id.number(),
|
||||
None => return InvalidTransaction::Call.into(),
|
||||
};
|
||||
|
||||
if best_finalized_number >= bundled_block_number {
|
||||
log::trace!(
|
||||
target: crate::LOG_TARGET,
|
||||
"Rejecting obsolete bridged header: bundled {:?}, best {:?}",
|
||||
bundled_block_number,
|
||||
best_finalized_number,
|
||||
);
|
||||
|
||||
return InvalidTransaction::Stale.into()
|
||||
}
|
||||
|
||||
Ok(ValidTransaction::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FilterCall;
|
||||
use crate::{
|
||||
mock::{run_test, test_header, RuntimeCall, TestNumber, TestRuntime},
|
||||
BestFinalized,
|
||||
};
|
||||
use bp_runtime::HeaderId;
|
||||
use bp_test_utils::make_default_justification;
|
||||
|
||||
fn validate_block_submit(num: TestNumber) -> bool {
|
||||
crate::Pallet::<TestRuntime>::validate(&RuntimeCall::Grandpa(crate::Call::<
|
||||
TestRuntime,
|
||||
(),
|
||||
>::submit_finality_proof {
|
||||
finality_target: Box::new(test_header(num)),
|
||||
justification: make_default_justification(&test_header(num)),
|
||||
}))
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn sync_to_header_10() {
|
||||
let header10_hash = sp_core::H256::default();
|
||||
BestFinalized::<TestRuntime, ()>::put(HeaderId(10, header10_hash));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_rejects_obsolete_header() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#5 => tx is
|
||||
// rejected
|
||||
sync_to_header_10();
|
||||
assert!(!validate_block_submit(5));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_rejects_same_header() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#10 => tx is
|
||||
// rejected
|
||||
sync_to_header_10();
|
||||
assert!(!validate_block_submit(10));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_accepts_new_header() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#15 => tx is
|
||||
// accepted
|
||||
sync_to_header_10();
|
||||
assert!(validate_block_submit(15));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ use bp_header_chain::{
|
||||
};
|
||||
use bp_runtime::{BlockNumberOf, Chain, HashOf, HasherOf, HeaderId, HeaderOf, OwnedBridgeModule};
|
||||
use finality_grandpa::voter_set::VoterSet;
|
||||
use frame_support::{dispatch::PostDispatchInfo, ensure, fail};
|
||||
use frame_support::{dispatch::PostDispatchInfo, ensure};
|
||||
use sp_finality_grandpa::{ConsensusLog, GRANDPA_ENGINE_ID};
|
||||
use sp_runtime::{
|
||||
traits::{Header as HeaderT, Zero},
|
||||
@@ -52,7 +52,7 @@ use sp_runtime::{
|
||||
};
|
||||
use sp_std::{boxed::Box, convert::TryInto};
|
||||
|
||||
mod extension;
|
||||
mod call_ext;
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
mod storage_types;
|
||||
@@ -64,6 +64,7 @@ pub mod weights;
|
||||
pub mod benchmarking;
|
||||
|
||||
// Re-export in crate namespace for `construct_runtime!`
|
||||
pub use call_ext::*;
|
||||
pub use pallet::*;
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
@@ -154,7 +155,7 @@ pub mod pallet {
|
||||
/// If successful in verification, it will write the target header to the underlying storage
|
||||
/// pallet.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(T::WeightInfo::submit_finality_proof(
|
||||
#[pallet::weight(<T::WeightInfo as WeightInfo>::submit_finality_proof(
|
||||
justification.commit.precommits.len().saturated_into(),
|
||||
justification.votes_ancestries.len().saturated_into(),
|
||||
))]
|
||||
@@ -174,22 +175,7 @@ pub mod pallet {
|
||||
finality_target
|
||||
);
|
||||
|
||||
let best_finalized_number = match BestFinalized::<T, I>::get() {
|
||||
Some(best_finalized_id) => best_finalized_id.number(),
|
||||
None => {
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"Cannot finalize header {:?} because pallet is not yet initialized",
|
||||
finality_target,
|
||||
);
|
||||
fail!(<Error<T, I>>::NotInitialized);
|
||||
},
|
||||
};
|
||||
|
||||
// We do a quick check here to ensure that our header chain is making progress and isn't
|
||||
// "travelling back in time" (which could be indicative of something bad, e.g a
|
||||
// hard-fork).
|
||||
ensure!(best_finalized_number < *number, <Error<T, I>>::OldHeader);
|
||||
SubmitFinalityProofHelper::<T, I>::check_obsolete(*number)?;
|
||||
|
||||
let authority_set = <CurrentAuthoritySet<T, I>>::get();
|
||||
let unused_proof_size = authority_set.unused_proof_size();
|
||||
|
||||
@@ -71,7 +71,7 @@ It'd be better for anyone (for chain and for submitters) to reject all transacti
|
||||
already known parachain heads to the pallet. This way, we leave block space to other useful transactions and
|
||||
we don't charge concurrent submitters for their honest actions.
|
||||
|
||||
To deal with that, we have a [signed extension](./src/extension.rs) that may be added to the runtime.
|
||||
To deal with that, we have a [signed extension](./src/call_ext) that may be added to the runtime.
|
||||
It does exactly what is required - rejects all transactions with already known heads. The submitter
|
||||
pays nothing for such transactions - they're simply removed from the transaction pool, when the block
|
||||
is built.
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
// Copyright 2021 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity Bridges Common.
|
||||
|
||||
// Parity Bridges Common is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity Bridges Common is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{Config, Pallet, RelayBlockNumber};
|
||||
use bp_parachains::BestParaHeadHash;
|
||||
use bp_polkadot_core::parachains::{ParaHash, ParaId};
|
||||
use frame_support::{dispatch::CallableCallFor, traits::IsSubType};
|
||||
use sp_runtime::{
|
||||
transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction},
|
||||
RuntimeDebug,
|
||||
};
|
||||
|
||||
/// Info about a `SubmitParachainHeads` call which tries to update a single parachain.
|
||||
#[derive(Copy, Clone, PartialEq, RuntimeDebug)]
|
||||
pub struct SubmitParachainHeadsInfo {
|
||||
pub at_relay_block_number: RelayBlockNumber,
|
||||
pub para_id: ParaId,
|
||||
pub para_head_hash: ParaHash,
|
||||
}
|
||||
|
||||
/// Helper struct that provides methods for working with the `SubmitParachainHeads` call.
|
||||
pub struct SubmitParachainHeadsHelper<T: Config<I>, I: 'static> {
|
||||
pub _phantom_data: sp_std::marker::PhantomData<(T, I)>,
|
||||
}
|
||||
|
||||
impl<T: Config<I>, I: 'static> SubmitParachainHeadsHelper<T, I> {
|
||||
/// Check if the para head provided by the `SubmitParachainHeads` is better than the best one
|
||||
/// we know.
|
||||
pub fn is_obsolete(update: &SubmitParachainHeadsInfo) -> bool {
|
||||
let stored_best_head = match crate::ParasInfo::<T, I>::get(update.para_id) {
|
||||
Some(stored_best_head) => stored_best_head,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
if stored_best_head.best_head_hash.at_relay_block_number >= update.at_relay_block_number {
|
||||
log::trace!(
|
||||
target: crate::LOG_TARGET,
|
||||
"The parachain head can't be updated. The parachain head for {:?} \
|
||||
was already updated at better relay chain block {} >= {}.",
|
||||
update.para_id,
|
||||
stored_best_head.best_head_hash.at_relay_block_number,
|
||||
update.at_relay_block_number
|
||||
);
|
||||
return true
|
||||
}
|
||||
|
||||
if stored_best_head.best_head_hash.head_hash == update.para_head_hash {
|
||||
log::trace!(
|
||||
target: crate::LOG_TARGET,
|
||||
"The parachain head can't be updated. The parachain head hash for {:?} \
|
||||
was already updated to {} at block {} < {}.",
|
||||
update.para_id,
|
||||
update.para_head_hash,
|
||||
stored_best_head.best_head_hash.at_relay_block_number,
|
||||
update.at_relay_block_number
|
||||
);
|
||||
return true
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if the `SubmitParachainHeads` was successfully executed.
|
||||
pub fn was_successful(update: &SubmitParachainHeadsInfo) -> bool {
|
||||
match crate::ParasInfo::<T, I>::get(update.para_id) {
|
||||
Some(stored_best_head) =>
|
||||
stored_best_head.best_head_hash ==
|
||||
BestParaHeadHash {
|
||||
at_relay_block_number: update.at_relay_block_number,
|
||||
head_hash: update.para_head_hash,
|
||||
},
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait representing a call that is a sub type of this pallet's call.
|
||||
pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
|
||||
IsSubType<CallableCallFor<Pallet<T, I>, T>>
|
||||
{
|
||||
/// Create a new instance of `SubmitParachainHeadsInfo` from a `SubmitParachainHeads` call with
|
||||
/// one single parachain entry.
|
||||
fn one_entry_submit_parachain_heads_info(&self) -> Option<SubmitParachainHeadsInfo> {
|
||||
if let Some(crate::Call::<T, I>::submit_parachain_heads {
|
||||
ref at_relay_block,
|
||||
ref parachains,
|
||||
..
|
||||
}) = self.is_sub_type()
|
||||
{
|
||||
if let &[(para_id, para_head_hash)] = parachains.as_slice() {
|
||||
return Some(SubmitParachainHeadsInfo {
|
||||
at_relay_block_number: at_relay_block.0,
|
||||
para_id,
|
||||
para_head_hash,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Create a new instance of `SubmitParachainHeadsInfo` from a `SubmitParachainHeads` call with
|
||||
/// one single parachain entry, if the entry is for the provided parachain id.
|
||||
fn submit_parachain_heads_info_for(&self, para_id: u32) -> Option<SubmitParachainHeadsInfo> {
|
||||
self.one_entry_submit_parachain_heads_info()
|
||||
.filter(|update| update.para_id.0 == para_id)
|
||||
}
|
||||
|
||||
/// Validate parachain heads in order to avoid "mining" transactions that provide
|
||||
/// outdated bridged parachain heads. Without this validation, even honest relayers
|
||||
/// may lose their funds if there are multiple relays running and submitting the
|
||||
/// same information.
|
||||
///
|
||||
/// This validation only works with transactions that are updating single parachain
|
||||
/// head. We can't use unbounded validation - it may take too long and either break
|
||||
/// block production, or "eat" significant portion of block production time literally
|
||||
/// for nothing. In addition, the single-parachain-head-per-transaction is how the
|
||||
/// pallet will be used in our environment.
|
||||
fn check_obsolete_submit_parachain_heads(&self) -> TransactionValidity
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let update = match self.one_entry_submit_parachain_heads_info() {
|
||||
Some(update) => update,
|
||||
None => return Ok(ValidTransaction::default()),
|
||||
};
|
||||
|
||||
if SubmitParachainHeadsHelper::<T, I>::is_obsolete(&update) {
|
||||
return InvalidTransaction::Stale.into()
|
||||
}
|
||||
|
||||
Ok(ValidTransaction::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, I: 'static> CallSubType<T, I> for T::RuntimeCall
|
||||
where
|
||||
T: Config<I>,
|
||||
T::RuntimeCall: IsSubType<CallableCallFor<Pallet<T, I>, T>>,
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
mock::{run_test, RuntimeCall, TestRuntime},
|
||||
CallSubType, ParaInfo, ParasInfo, RelayBlockNumber,
|
||||
};
|
||||
use bp_parachains::BestParaHeadHash;
|
||||
use bp_polkadot_core::parachains::{ParaHash, ParaHeadsProof, ParaId};
|
||||
|
||||
fn validate_submit_parachain_heads(
|
||||
num: RelayBlockNumber,
|
||||
parachains: Vec<(ParaId, ParaHash)>,
|
||||
) -> bool {
|
||||
RuntimeCall::Parachains(crate::Call::<TestRuntime, ()>::submit_parachain_heads {
|
||||
at_relay_block: (num, Default::default()),
|
||||
parachains,
|
||||
parachain_heads_proof: ParaHeadsProof(Vec::new()),
|
||||
})
|
||||
.check_obsolete_submit_parachain_heads()
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn sync_to_relay_header_10() {
|
||||
ParasInfo::<TestRuntime, ()>::insert(
|
||||
ParaId(1),
|
||||
ParaInfo {
|
||||
best_head_hash: BestParaHeadHash {
|
||||
at_relay_block_number: 10,
|
||||
head_hash: [1u8; 32].into(),
|
||||
},
|
||||
next_imported_hash_position: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_rejects_header_from_the_obsolete_relay_block() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#5 => tx is
|
||||
// rejected
|
||||
sync_to_relay_header_10();
|
||||
assert!(!validate_submit_parachain_heads(5, vec![(ParaId(1), [1u8; 32].into())]));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_rejects_header_from_the_same_relay_block() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#10 => tx is
|
||||
// rejected
|
||||
sync_to_relay_header_10();
|
||||
assert!(!validate_submit_parachain_heads(10, vec![(ParaId(1), [1u8; 32].into())]));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_rejects_header_from_new_relay_block_with_same_hash() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#10 => tx is
|
||||
// rejected
|
||||
sync_to_relay_header_10();
|
||||
assert!(!validate_submit_parachain_heads(20, vec![(ParaId(1), [1u8; 32].into())]));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_accepts_new_header() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#15 => tx is
|
||||
// accepted
|
||||
sync_to_relay_header_10();
|
||||
assert!(validate_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_accepts_if_more_than_one_parachain_is_submitted() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#5, but another
|
||||
// parachain head is also supplied => tx is accepted
|
||||
sync_to_relay_header_10();
|
||||
assert!(validate_submit_parachain_heads(
|
||||
5,
|
||||
vec![(ParaId(1), [1u8; 32].into()), (ParaId(2), [1u8; 32].into())]
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
// Copyright 2021 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity Bridges Common.
|
||||
|
||||
// Parity Bridges Common is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity Bridges Common is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{Config, Pallet, RelayBlockHash, RelayBlockHasher, RelayBlockNumber};
|
||||
use bp_runtime::FilterCall;
|
||||
use frame_support::{dispatch::CallableCallFor, traits::IsSubType};
|
||||
use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction};
|
||||
|
||||
/// Validate parachain heads in order to avoid "mining" transactions that provide
|
||||
/// outdated bridged parachain heads. Without this validation, even honest relayers
|
||||
/// may lose their funds if there are multiple relays running and submitting the
|
||||
/// same information.
|
||||
///
|
||||
/// This validation only works with transactions that are updating single parachain
|
||||
/// head. We can't use unbounded validation - it may take too long and either break
|
||||
/// block production, or "eat" significant portion of block production time literally
|
||||
/// for nothing. In addition, the single-parachain-head-per-transaction is how the
|
||||
/// pallet will be used in our environment.
|
||||
impl<
|
||||
Call: IsSubType<CallableCallFor<Pallet<T, I>, T>>,
|
||||
T: frame_system::Config<RuntimeCall = Call> + Config<I>,
|
||||
I: 'static,
|
||||
> FilterCall<Call> for Pallet<T, I>
|
||||
where
|
||||
<T as pallet_bridge_grandpa::Config<T::BridgesGrandpaPalletInstance>>::BridgedChain:
|
||||
bp_runtime::Chain<
|
||||
BlockNumber = RelayBlockNumber,
|
||||
Hash = RelayBlockHash,
|
||||
Hasher = RelayBlockHasher,
|
||||
>,
|
||||
{
|
||||
fn validate(call: &Call) -> TransactionValidity {
|
||||
let (updated_at_relay_block_number, parachains) = match call.is_sub_type() {
|
||||
Some(crate::Call::<T, I>::submit_parachain_heads {
|
||||
ref at_relay_block,
|
||||
ref parachains,
|
||||
..
|
||||
}) => (at_relay_block.0, parachains),
|
||||
_ => return Ok(ValidTransaction::default()),
|
||||
};
|
||||
let (parachain, parachain_head_hash) = match parachains.as_slice() {
|
||||
&[(parachain, parachain_head_hash)] => (parachain, parachain_head_hash),
|
||||
_ => return Ok(ValidTransaction::default()),
|
||||
};
|
||||
|
||||
let maybe_stored_best_head = crate::ParasInfo::<T, I>::get(parachain);
|
||||
let is_valid = Self::validate_updated_parachain_head(
|
||||
parachain,
|
||||
&maybe_stored_best_head,
|
||||
updated_at_relay_block_number,
|
||||
parachain_head_hash,
|
||||
"Rejecting obsolete parachain-head transaction",
|
||||
);
|
||||
|
||||
if is_valid {
|
||||
Ok(ValidTransaction::default())
|
||||
} else {
|
||||
InvalidTransaction::Stale.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
extension::FilterCall,
|
||||
mock::{run_test, RuntimeCall, TestRuntime},
|
||||
ParaInfo, ParasInfo, RelayBlockNumber,
|
||||
};
|
||||
use bp_parachains::BestParaHeadHash;
|
||||
use bp_polkadot_core::parachains::{ParaHash, ParaHeadsProof, ParaId};
|
||||
|
||||
fn validate_submit_parachain_heads(
|
||||
num: RelayBlockNumber,
|
||||
parachains: Vec<(ParaId, ParaHash)>,
|
||||
) -> bool {
|
||||
crate::Pallet::<TestRuntime>::validate(&RuntimeCall::Parachains(crate::Call::<
|
||||
TestRuntime,
|
||||
(),
|
||||
>::submit_parachain_heads {
|
||||
at_relay_block: (num, Default::default()),
|
||||
parachains,
|
||||
parachain_heads_proof: ParaHeadsProof(Vec::new()),
|
||||
}))
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn sync_to_relay_header_10() {
|
||||
ParasInfo::<TestRuntime, ()>::insert(
|
||||
ParaId(1),
|
||||
ParaInfo {
|
||||
best_head_hash: BestParaHeadHash {
|
||||
at_relay_block_number: 10,
|
||||
head_hash: [1u8; 32].into(),
|
||||
},
|
||||
next_imported_hash_position: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_rejects_header_from_the_obsolete_relay_block() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#5 => tx is
|
||||
// rejected
|
||||
sync_to_relay_header_10();
|
||||
assert!(!validate_submit_parachain_heads(5, vec![(ParaId(1), [1u8; 32].into())]));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_rejects_header_from_the_same_relay_block() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#10 => tx is
|
||||
// rejected
|
||||
sync_to_relay_header_10();
|
||||
assert!(!validate_submit_parachain_heads(10, vec![(ParaId(1), [1u8; 32].into())]));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_rejects_header_from_new_relay_block_with_same_hash() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#10 => tx is
|
||||
// rejected
|
||||
sync_to_relay_header_10();
|
||||
assert!(!validate_submit_parachain_heads(20, vec![(ParaId(1), [1u8; 32].into())]));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_accepts_new_header() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#15 => tx is
|
||||
// accepted
|
||||
sync_to_relay_header_10();
|
||||
assert!(validate_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_accepts_if_more_than_one_parachain_is_submitted() {
|
||||
run_test(|| {
|
||||
// when current best finalized is #10 and we're trying to import header#5, but another
|
||||
// parachain head is also supplied => tx is accepted
|
||||
sync_to_relay_header_10();
|
||||
assert!(validate_submit_parachain_heads(
|
||||
5,
|
||||
vec![(ParaId(1), [1u8; 32].into()), (ParaId(2), [1u8; 32].into())]
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ use bp_runtime::HeaderOf;
|
||||
use codec::Encode;
|
||||
|
||||
// Re-export in crate namespace for `construct_runtime!`.
|
||||
pub use call_ext::*;
|
||||
pub use pallet::*;
|
||||
|
||||
pub mod weights;
|
||||
@@ -49,7 +50,7 @@ pub mod weights_ext;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub mod benchmarking;
|
||||
|
||||
mod extension;
|
||||
mod call_ext;
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
|
||||
@@ -136,10 +137,30 @@ pub mod pallet {
|
||||
BridgeModule(bp_runtime::OwnedBridgeModuleError),
|
||||
}
|
||||
|
||||
/// Convenience trait for defining `BridgedChain` bounds.
|
||||
pub trait BoundedBridgeGrandpaConfig<I: 'static>:
|
||||
pallet_bridge_grandpa::Config<I, BridgedChain = Self::BridgedRelayChain>
|
||||
{
|
||||
type BridgedRelayChain: Chain<
|
||||
BlockNumber = RelayBlockNumber,
|
||||
Hash = RelayBlockHash,
|
||||
Hasher = RelayBlockHasher,
|
||||
>;
|
||||
}
|
||||
|
||||
impl<T, I: 'static> BoundedBridgeGrandpaConfig<I> for T
|
||||
where
|
||||
T: pallet_bridge_grandpa::Config<I>,
|
||||
T::BridgedChain:
|
||||
Chain<BlockNumber = RelayBlockNumber, Hash = RelayBlockHash, Hasher = RelayBlockHasher>,
|
||||
{
|
||||
type BridgedRelayChain = T::BridgedChain;
|
||||
}
|
||||
|
||||
#[pallet::config]
|
||||
#[pallet::disable_frame_system_supertrait_check]
|
||||
pub trait Config<I: 'static = ()>:
|
||||
pallet_bridge_grandpa::Config<Self::BridgesGrandpaPalletInstance>
|
||||
BoundedBridgeGrandpaConfig<Self::BridgesGrandpaPalletInstance>
|
||||
{
|
||||
/// The overarching event type.
|
||||
type RuntimeEvent: From<Event<Self, I>>
|
||||
@@ -267,15 +288,7 @@ pub mod pallet {
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I>
|
||||
where
|
||||
<T as pallet_bridge_grandpa::Config<T::BridgesGrandpaPalletInstance>>::BridgedChain:
|
||||
bp_runtime::Chain<
|
||||
BlockNumber = RelayBlockNumber,
|
||||
Hash = RelayBlockHash,
|
||||
Hasher = RelayBlockHasher,
|
||||
>,
|
||||
{
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
/// Submit proof of one or several parachain heads.
|
||||
///
|
||||
/// The proof is supposed to be proof of some `Heads` entries from the
|
||||
@@ -483,89 +496,38 @@ pub mod pallet {
|
||||
storage.read_and_decode_value(parachain_head_key.0.as_ref())
|
||||
}
|
||||
|
||||
/// Check if para head has been already updated at better relay chain block.
|
||||
/// Without this check, we may import heads in random order.
|
||||
///
|
||||
/// Returns `true` if the pallet is ready to import given parachain head.
|
||||
/// Returns `false` if the pallet already knows the same or better parachain head.
|
||||
#[must_use]
|
||||
pub fn validate_updated_parachain_head(
|
||||
parachain: ParaId,
|
||||
maybe_stored_best_head: &Option<ParaInfo>,
|
||||
updated_at_relay_block_number: RelayBlockNumber,
|
||||
updated_head_hash: ParaHash,
|
||||
err_log_prefix: &str,
|
||||
) -> bool {
|
||||
let stored_best_head = match maybe_stored_best_head {
|
||||
Some(stored_best_head) => stored_best_head,
|
||||
None => return true,
|
||||
};
|
||||
|
||||
if stored_best_head.best_head_hash.at_relay_block_number >=
|
||||
updated_at_relay_block_number
|
||||
{
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{}. The parachain head for {:?} was already updated at better relay chain block {} >= {}.",
|
||||
err_log_prefix,
|
||||
parachain,
|
||||
stored_best_head.best_head_hash.at_relay_block_number,
|
||||
updated_at_relay_block_number
|
||||
);
|
||||
return false
|
||||
}
|
||||
|
||||
if stored_best_head.best_head_hash.head_hash == updated_head_hash {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{}. The parachain head hash for {:?} was already updated to {} at block {} < {}.",
|
||||
err_log_prefix,
|
||||
parachain,
|
||||
updated_head_hash,
|
||||
stored_best_head.best_head_hash.at_relay_block_number,
|
||||
updated_at_relay_block_number
|
||||
);
|
||||
return false
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Try to update parachain head.
|
||||
pub(super) fn update_parachain_head(
|
||||
parachain: ParaId,
|
||||
stored_best_head: Option<ParaInfo>,
|
||||
updated_at_relay_block_number: RelayBlockNumber,
|
||||
updated_head_data: ParaStoredHeaderData,
|
||||
updated_head_hash: ParaHash,
|
||||
new_at_relay_block_number: RelayBlockNumber,
|
||||
new_head_data: ParaStoredHeaderData,
|
||||
new_head_hash: ParaHash,
|
||||
) -> Result<UpdateParachainHeadArtifacts, ()> {
|
||||
// check if head has been already updated at better relay chain block. Without this
|
||||
// check, we may import heads in random order
|
||||
let err_log_prefix = "The parachain head can't be updated";
|
||||
let is_valid = Self::validate_updated_parachain_head(
|
||||
parachain,
|
||||
&stored_best_head,
|
||||
updated_at_relay_block_number,
|
||||
updated_head_hash,
|
||||
err_log_prefix,
|
||||
);
|
||||
if !is_valid {
|
||||
let update = SubmitParachainHeadsInfo {
|
||||
at_relay_block_number: new_at_relay_block_number,
|
||||
para_id: parachain,
|
||||
para_head_hash: new_head_hash,
|
||||
};
|
||||
if SubmitParachainHeadsHelper::<T, I>::is_obsolete(&update) {
|
||||
Self::deposit_event(Event::RejectedObsoleteParachainHead {
|
||||
parachain,
|
||||
parachain_head_hash: updated_head_hash,
|
||||
parachain_head_hash: new_head_hash,
|
||||
});
|
||||
return Err(())
|
||||
}
|
||||
|
||||
// verify that the parachain head data size is <= `MaxParaHeadDataSize`
|
||||
let updated_head_data =
|
||||
match StoredParaHeadDataOf::<T, I>::try_from_inner(updated_head_data) {
|
||||
match StoredParaHeadDataOf::<T, I>::try_from_inner(new_head_data) {
|
||||
Ok(updated_head_data) => updated_head_data,
|
||||
Err(e) => {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{}. The parachain head data size for {:?} is {}. It exceeds maximal configured size {}.",
|
||||
err_log_prefix,
|
||||
"The parachain head can't be updated. The parachain head data size \
|
||||
for {:?} is {}. It exceeds maximal configured size {}.",
|
||||
parachain,
|
||||
e.value_size,
|
||||
e.maximal_size,
|
||||
@@ -573,7 +535,7 @@ pub mod pallet {
|
||||
|
||||
Self::deposit_event(Event::RejectedLargeParachainHead {
|
||||
parachain,
|
||||
parachain_head_hash: updated_head_hash,
|
||||
parachain_head_hash: new_head_hash,
|
||||
parachain_head_size: e.value_size as _,
|
||||
});
|
||||
|
||||
@@ -589,8 +551,8 @@ pub mod pallet {
|
||||
ImportedParaHashes::<T, I>::try_get(parachain, next_imported_hash_position);
|
||||
let updated_best_para_head = ParaInfo {
|
||||
best_head_hash: BestParaHeadHash {
|
||||
at_relay_block_number: updated_at_relay_block_number,
|
||||
head_hash: updated_head_hash,
|
||||
at_relay_block_number: new_at_relay_block_number,
|
||||
head_hash: new_head_hash,
|
||||
},
|
||||
next_imported_hash_position: (next_imported_hash_position + 1) %
|
||||
T::HeadsToKeep::get(),
|
||||
@@ -598,14 +560,14 @@ pub mod pallet {
|
||||
ImportedParaHashes::<T, I>::insert(
|
||||
parachain,
|
||||
next_imported_hash_position,
|
||||
updated_head_hash,
|
||||
new_head_hash,
|
||||
);
|
||||
ImportedParaHeads::<T, I>::insert(parachain, updated_head_hash, updated_head_data);
|
||||
ImportedParaHeads::<T, I>::insert(parachain, new_head_hash, updated_head_data);
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"Updated head of parachain {:?} to {}",
|
||||
parachain,
|
||||
updated_head_hash,
|
||||
new_head_hash,
|
||||
);
|
||||
|
||||
// remove old head
|
||||
@@ -621,7 +583,7 @@ pub mod pallet {
|
||||
}
|
||||
Self::deposit_event(Event::UpdatedParachainHead {
|
||||
parachain,
|
||||
parachain_head_hash: updated_head_hash,
|
||||
parachain_head_hash: new_head_hash,
|
||||
});
|
||||
|
||||
Ok(UpdateParachainHeadArtifacts { best_head: updated_best_para_head, prune_happened })
|
||||
|
||||
Reference in New Issue
Block a user