RefundRelayerForMessagesFromParachain improvements (#1879)

* RefundRelayerForMessagesFromParachain improvements

* Address code review comments
This commit is contained in:
Serban Iorga
2023-02-14 15:26:35 +02:00
committed by Bastian Köcher
parent 83c3975349
commit c9c6d566d0
13 changed files with 752 additions and 723 deletions
+243
View File
@@ -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())]
));
});
}
}
-166
View File
@@ -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())]
));
});
}
}
+44 -82
View File
@@ -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 })