......@@ -67,6 +67,8 @@ pub const PARAS_PALLET_NAME: &str = "Paras";
/// Name of the With-Rococo GRANDPA pallet instance that is deployed at bridged chains.
pub const WITH_ROCOCO_GRANDPA_PALLET_NAME: &str = "BridgeRococoGrandpa";
/// Name of the With-Rococo parachains pallet instance that is deployed at bridged chains.
pub const WITH_ROCOCO_BRIDGE_PARACHAINS_PALLET_NAME: &str = "BridgeRococoParachains";
/// Maximal size of encoded `bp_parachains::ParaStoredHeaderData` structure among all Rococo
/// parachains.
......
......@@ -67,6 +67,8 @@ pub const PARAS_PALLET_NAME: &str = "Paras";
/// Name of the With-Westend GRANDPA pallet instance that is deployed at bridged chains.
pub const WITH_WESTEND_GRANDPA_PALLET_NAME: &str = "BridgeWestendGrandpa";
/// Name of the With-Westend parachains pallet instance that is deployed at bridged chains.
pub const WITH_WESTEND_BRIDGE_PARACHAINS_PALLET_NAME: &str = "BridgeWestendParachains";
/// Maximal size of encoded `bp_parachains::ParaStoredHeaderData` structure among all Westend
/// parachains.
......
......@@ -15,20 +15,24 @@
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
use crate::{
weights::WeightInfo, BridgedBlockNumber, BridgedHeader, Config, CurrentAuthoritySet, Error,
Pallet,
weights::WeightInfo, BestFinalized, BridgedBlockNumber, BridgedHeader, Config,
CurrentAuthoritySet, Error, FreeHeadersRemaining, Pallet,
};
use bp_header_chain::{
justification::GrandpaJustification, max_expected_submit_finality_proof_arguments_size,
ChainWithGrandpa, GrandpaConsensusLogReader,
};
use bp_runtime::{BlockNumberOf, OwnedBridgeModule};
use bp_runtime::{BlockNumberOf, Chain, OwnedBridgeModule};
use codec::Encode;
use frame_support::{dispatch::CallableCallFor, traits::IsSubType, weights::Weight};
use frame_support::{
dispatch::CallableCallFor,
traits::{Get, IsSubType},
weights::Weight,
};
use sp_consensus_grandpa::SetId;
use sp_runtime::{
traits::{Header, Zero},
transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction},
traits::{CheckedSub, Header, Zero},
transaction_validity::{InvalidTransaction, TransactionValidityError},
RuntimeDebug, SaturatedConversion,
};
......@@ -40,6 +44,11 @@ pub struct SubmitFinalityProofInfo<N> {
/// An identifier of the validators set that has signed the submitted justification.
/// It might be `None` if deprecated version of the `submit_finality_proof` is used.
pub current_set_id: Option<SetId>,
/// If `true`, then the call proves new **mandatory** header.
pub is_mandatory: bool,
/// If `true`, then the call must be free (assuming that everything else is valid) to
/// be treated as valid.
pub is_free_execution_expected: bool,
/// Extra weight that we assume is included in the call.
///
/// We have some assumptions about headers and justifications of the bridged chain.
......@@ -54,6 +63,16 @@ pub struct SubmitFinalityProofInfo<N> {
pub extra_size: u32,
}
/// Verified `SubmitFinalityProofInfo<N>`.
#[derive(Copy, Clone, PartialEq, RuntimeDebug)]
pub struct VerifiedSubmitFinalityProofInfo<N> {
/// Base call information.
pub base: SubmitFinalityProofInfo<N>,
/// A difference between bundled bridged header and best bridged header known to us
/// before the call.
pub improved_by: N,
}
impl<N> SubmitFinalityProofInfo<N> {
/// Returns `true` if call size/weight is below our estimations for regular calls.
pub fn fits_limits(&self) -> bool {
......@@ -67,14 +86,86 @@ pub struct SubmitFinalityProofHelper<T: Config<I>, I: 'static> {
}
impl<T: Config<I>, I: 'static> SubmitFinalityProofHelper<T, I> {
/// Returns `true` if we may fit more free headers into the current block. If `false` is
/// returned, the call will be paid even if `is_free_execution_expected` has been set
/// to `true`.
pub fn has_free_header_slots() -> bool {
// `unwrap_or(u32::MAX)` means that if `FreeHeadersRemaining` is `None`, we may accept
// this header for free. That is a small cheat - it is `None` if executed outside of
// transaction (e.g. during block initialization). Normal relayer would never submit
// such calls, but if he did, that is not our problem. During normal transactions,
// the `FreeHeadersRemaining` is always `Some(_)`.
let free_headers_remaining = FreeHeadersRemaining::<T, I>::get().unwrap_or(u32::MAX);
free_headers_remaining > 0
}
/// Check that the: (1) GRANDPA head provided by the `SubmitFinalityProof` is better than the
/// best one we know (2) if `current_set_id` matches the current authority set id, if specified
/// and (3) whether transaction MAY be free for the submitter if `is_free_execution_expected`
/// is `true`.
///
/// Returns number of headers between the current best finalized header, known to the pallet
/// and the bundled header.
pub fn check_obsolete_from_extension(
call_info: &SubmitFinalityProofInfo<BlockNumberOf<T::BridgedChain>>,
) -> Result<BlockNumberOf<T::BridgedChain>, Error<T, I>> {
// do basic checks first
let improved_by = Self::check_obsolete(call_info.block_number, call_info.current_set_id)?;
// if submitter has NOT specified that it wants free execution, then we are done
if !call_info.is_free_execution_expected {
return Ok(improved_by);
}
// else - if we can not accept more free headers, "reject" the transaction
if !Self::has_free_header_slots() {
log::trace!(
target: crate::LOG_TARGET,
"Cannot accept free {:?} header {:?}. No more free slots remaining",
T::BridgedChain::ID,
call_info.block_number,
);
return Err(Error::<T, I>::FreeHeadersLimitExceded);
}
// ensure that the `improved_by` is larger than the configured free interval
if !call_info.is_mandatory {
if let Some(free_headers_interval) = T::FreeHeadersInterval::get() {
if improved_by < free_headers_interval.into() {
log::trace!(
target: crate::LOG_TARGET,
"Cannot accept free {:?} header {:?}. Too small difference \
between submitted headers: {:?} vs {}",
T::BridgedChain::ID,
call_info.block_number,
improved_by,
free_headers_interval,
);
return Err(Error::<T, I>::BelowFreeHeaderInterval);
}
}
}
// we do not check whether the header matches free submission criteria here - it is the
// relayer responsibility to check that
Ok(improved_by)
}
/// Check that the GRANDPA head provided by the `SubmitFinalityProof` is better than the best
/// one we know. Additionally, checks if `current_set_id` matches the current authority set
/// id, if specified.
/// id, if specified. This method is called by the call code and the transaction extension,
/// so it does not check the free execution.
///
/// Returns number of headers between the current best finalized header, known to the pallet
/// and the bundled header.
pub fn check_obsolete(
finality_target: BlockNumberOf<T::BridgedChain>,
current_set_id: Option<SetId>,
) -> Result<(), Error<T, I>> {
let best_finalized = crate::BestFinalized::<T, I>::get().ok_or_else(|| {
) -> Result<BlockNumberOf<T::BridgedChain>, Error<T, I>> {
let best_finalized = BestFinalized::<T, I>::get().ok_or_else(|| {
log::trace!(
target: crate::LOG_TARGET,
"Cannot finalize header {:?} because pallet is not yet initialized",
......@@ -83,16 +174,19 @@ impl<T: Config<I>, I: 'static> SubmitFinalityProofHelper<T, I> {
<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,
);
let improved_by = match finality_target.checked_sub(&best_finalized.number()) {
Some(improved_by) if improved_by > Zero::zero() => improved_by,
_ => {
log::trace!(
target: crate::LOG_TARGET,
"Cannot finalize obsolete header: bundled {:?}, best {:?}",
finality_target,
best_finalized,
);
return Err(Error::<T, I>::OldHeader)
}
return Err(Error::<T, I>::OldHeader)
},
};
if let Some(current_set_id) = current_set_id {
let actual_set_id = <CurrentAuthoritySet<T, I>>::get().set_id;
......@@ -108,12 +202,12 @@ impl<T: Config<I>, I: 'static> SubmitFinalityProofHelper<T, I> {
}
}
Ok(())
Ok(improved_by)
}
/// Check if the `SubmitFinalityProof` was successfully executed.
pub fn was_successful(finality_target: BlockNumberOf<T::BridgedChain>) -> bool {
match crate::BestFinalized::<T, I>::get() {
match BestFinalized::<T, I>::get() {
Some(best_finalized) => best_finalized.number() == finality_target,
None => false,
}
......@@ -135,17 +229,20 @@ pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
finality_target,
justification,
None,
false,
))
} else if let Some(crate::Call::<T, I>::submit_finality_proof_ex {
finality_target,
justification,
current_set_id,
is_free_execution_expected,
}) = self.is_sub_type()
{
return Some(submit_finality_proof_info_from_args::<T, I>(
finality_target,
justification,
Some(*current_set_id),
*is_free_execution_expected,
))
}
......@@ -155,26 +252,36 @@ pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
/// 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
///
/// Returns `Ok(None)` if the call is not the `submit_finality_proof` call of our pallet.
/// Returns `Ok(Some(_))` if the call is the `submit_finality_proof` call of our pallet and
/// we believe the call brings header that improves the pallet state.
/// Returns `Err(_)` if the call is the `submit_finality_proof` call of our pallet and we
/// believe that the call will fail.
fn check_obsolete_submit_finality_proof(
&self,
) -> Result<
Option<VerifiedSubmitFinalityProofInfo<BridgedBlockNumber<T, I>>>,
TransactionValidityError,
>
where
Self: Sized,
{
let finality_target = match self.submit_finality_proof_info() {
let call_info = match self.submit_finality_proof_info() {
Some(finality_proof) => finality_proof,
_ => return Ok(ValidTransaction::default()),
_ => return Ok(None),
};
if Pallet::<T, I>::ensure_not_halted().is_err() {
return InvalidTransaction::Call.into()
return Err(InvalidTransaction::Call.into())
}
match SubmitFinalityProofHelper::<T, I>::check_obsolete(
finality_target.block_number,
finality_target.current_set_id,
) {
Ok(_) => Ok(ValidTransaction::default()),
Err(Error::<T, I>::OldHeader) => InvalidTransaction::Stale.into(),
Err(_) => InvalidTransaction::Call.into(),
let result = SubmitFinalityProofHelper::<T, I>::check_obsolete_from_extension(&call_info);
match result {
Ok(improved_by) =>
Ok(Some(VerifiedSubmitFinalityProofInfo { base: call_info, improved_by })),
Err(Error::<T, I>::OldHeader) => Err(InvalidTransaction::Stale.into()),
Err(_) => Err(InvalidTransaction::Call.into()),
}
}
}
......@@ -189,6 +296,7 @@ pub(crate) fn submit_finality_proof_info_from_args<T: Config<I>, I: 'static>(
finality_target: &BridgedHeader<T, I>,
justification: &GrandpaJustification<BridgedHeader<T, I>>,
current_set_id: Option<SetId>,
is_free_execution_expected: bool,
) -> SubmitFinalityProofInfo<BridgedBlockNumber<T, I>> {
let block_number = *finality_target.number();
......@@ -230,16 +338,26 @@ pub(crate) fn submit_finality_proof_info_from_args<T: Config<I>, I: 'static>(
);
let extra_size = actual_call_size.saturating_sub(max_expected_call_size);
SubmitFinalityProofInfo { block_number, current_set_id, extra_weight, extra_size }
SubmitFinalityProofInfo {
block_number,
current_set_id,
is_mandatory: is_mandatory_finality_target,
is_free_execution_expected,
extra_weight,
extra_size,
}
}
#[cfg(test)]
mod tests {
use crate::{
call_ext::CallSubType,
mock::{run_test, test_header, RuntimeCall, TestBridgedChain, TestNumber, TestRuntime},
BestFinalized, Config, CurrentAuthoritySet, PalletOperatingMode, StoredAuthoritySet,
SubmitFinalityProofInfo, WeightInfo,
mock::{
run_test, test_header, FreeHeadersInterval, RuntimeCall, TestBridgedChain, TestNumber,
TestRuntime,
},
BestFinalized, Config, CurrentAuthoritySet, FreeHeadersRemaining, PalletOperatingMode,
StoredAuthoritySet, SubmitFinalityProofInfo, WeightInfo,
};
use bp_header_chain::ChainWithGrandpa;
use bp_runtime::{BasicOperatingMode, HeaderId};
......@@ -247,6 +365,7 @@ mod tests {
make_default_justification, make_justification_for_header, JustificationGeneratorParams,
TEST_GRANDPA_SET_ID,
};
use codec::Encode;
use frame_support::weights::Weight;
use sp_runtime::{testing::DigestItem, traits::Header as _, SaturatedConversion};
......@@ -256,6 +375,7 @@ mod tests {
justification: make_default_justification(&test_header(num)),
// not initialized => zero
current_set_id: 0,
is_free_execution_expected: false,
};
RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call,
......@@ -311,6 +431,121 @@ mod tests {
});
}
#[test]
fn extension_rejects_new_header_if_free_execution_is_requested_and_free_submissions_are_not_accepted(
) {
run_test(|| {
let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
finality_target: Box::new(test_header(10 + FreeHeadersInterval::get() as u64)),
justification: make_default_justification(&test_header(
10 + FreeHeadersInterval::get() as u64,
)),
current_set_id: 0,
is_free_execution_expected: true,
};
sync_to_header_10();
// when we can accept free headers => Ok
FreeHeadersRemaining::<TestRuntime, ()>::put(2);
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_ok());
// when we can NOT accept free headers => Err
FreeHeadersRemaining::<TestRuntime, ()>::put(0);
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_err());
// when called outside of transaction => Ok
FreeHeadersRemaining::<TestRuntime, ()>::kill();
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call,
),)
.is_ok());
})
}
#[test]
fn extension_rejects_new_header_if_free_execution_is_requested_and_improved_by_is_below_expected(
) {
run_test(|| {
let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
finality_target: Box::new(test_header(100)),
justification: make_default_justification(&test_header(100)),
current_set_id: 0,
is_free_execution_expected: true,
};
sync_to_header_10();
// when `improved_by` is less than the free interval
BestFinalized::<TestRuntime, ()>::put(HeaderId(
100 - FreeHeadersInterval::get() as u64 + 1,
sp_core::H256::default(),
));
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_err());
// when `improved_by` is equal to the free interval
BestFinalized::<TestRuntime, ()>::put(HeaderId(
100 - FreeHeadersInterval::get() as u64,
sp_core::H256::default(),
));
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_ok());
// when `improved_by` is larger than the free interval
BestFinalized::<TestRuntime, ()>::put(HeaderId(
100 - FreeHeadersInterval::get() as u64 - 1,
sp_core::H256::default(),
));
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_ok());
// when `improved_by` is less than the free interval BUT it is a mandatory header
let mut mandatory_header = test_header(100);
let consensus_log = sp_consensus_grandpa::ConsensusLog::<TestNumber>::ScheduledChange(
sp_consensus_grandpa::ScheduledChange {
next_authorities: bp_test_utils::authority_list(),
delay: 0,
},
);
mandatory_header.digest = sp_runtime::Digest {
logs: vec![DigestItem::Consensus(
sp_consensus_grandpa::GRANDPA_ENGINE_ID,
consensus_log.encode(),
)],
};
let justification = make_justification_for_header(JustificationGeneratorParams {
header: mandatory_header.clone(),
set_id: 1,
..Default::default()
});
let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
finality_target: Box::new(mandatory_header),
justification,
current_set_id: 0,
is_free_execution_expected: true,
};
BestFinalized::<TestRuntime, ()>::put(HeaderId(
100 - FreeHeadersInterval::get() as u64 + 1,
sp_core::H256::default(),
));
assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
bridge_grandpa_call.clone(),
),)
.is_ok());
})
}
#[test]
fn extension_accepts_new_header() {
run_test(|| {
......@@ -336,6 +571,8 @@ mod tests {
current_set_id: None,
extra_weight: Weight::zero(),
extra_size: 0,
is_mandatory: false,
is_free_execution_expected: false,
})
);
......@@ -345,6 +582,7 @@ mod tests {
finality_target: Box::new(test_header(42)),
justification: make_default_justification(&test_header(42)),
current_set_id: 777,
is_free_execution_expected: false,
});
assert_eq!(
deprecated_call.submit_finality_proof_info(),
......@@ -353,6 +591,8 @@ mod tests {
current_set_id: Some(777),
extra_weight: Weight::zero(),
extra_size: 0,
is_mandatory: false,
is_free_execution_expected: false,
})
);
}
......@@ -370,6 +610,7 @@ mod tests {
finality_target: Box::new(small_finality_target),
justification: small_justification,
current_set_id: TEST_GRANDPA_SET_ID,
is_free_execution_expected: false,
});
assert_eq!(small_call.submit_finality_proof_info().unwrap().extra_size, 0);
......@@ -387,6 +628,7 @@ mod tests {
finality_target: Box::new(large_finality_target),
justification: large_justification,
current_set_id: TEST_GRANDPA_SET_ID,
is_free_execution_expected: false,
});
assert_ne!(large_call.submit_finality_proof_info().unwrap().extra_size, 0);
}
......@@ -406,6 +648,7 @@ mod tests {
finality_target: Box::new(finality_target.clone()),
justification,
current_set_id: TEST_GRANDPA_SET_ID,
is_free_execution_expected: false,
});
assert_eq!(call.submit_finality_proof_info().unwrap().extra_weight, Weight::zero());
......@@ -420,7 +663,52 @@ mod tests {
finality_target: Box::new(finality_target),
justification,
current_set_id: TEST_GRANDPA_SET_ID,
is_free_execution_expected: false,
});
assert_eq!(call.submit_finality_proof_info().unwrap().extra_weight, call_weight);
}
#[test]
fn check_obsolete_submit_finality_proof_returns_correct_improved_by() {
run_test(|| {
fn make_call(number: u64) -> RuntimeCall {
RuntimeCall::Grandpa(crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
finality_target: Box::new(test_header(number)),
justification: make_default_justification(&test_header(number)),
current_set_id: 0,
is_free_execution_expected: false,
})
}
sync_to_header_10();
// when the difference between headers is 1
assert_eq!(
RuntimeCall::check_obsolete_submit_finality_proof(&make_call(11))
.unwrap()
.unwrap()
.improved_by,
1,
);
// when the difference between headers is 2
assert_eq!(
RuntimeCall::check_obsolete_submit_finality_proof(&make_call(12))
.unwrap()
.unwrap()
.improved_by,
2,
);
})
}
#[test]
fn check_obsolete_submit_finality_proof_ignores_other_calls() {
run_test(|| {
let call =
RuntimeCall::System(frame_system::Call::<TestRuntime>::remark { remark: vec![42] });
assert_eq!(RuntimeCall::check_obsolete_submit_finality_proof(&call), Ok(None));
})
}
}
......@@ -44,6 +44,7 @@ use bp_header_chain::{
};
use bp_runtime::{BlockNumberOf, HashOf, HasherOf, HeaderId, HeaderOf, OwnedBridgeModule};
use frame_support::{dispatch::PostDispatchInfo, ensure, DefaultNoBound};
use sp_consensus_grandpa::SetId;
use sp_runtime::{
traits::{Header as HeaderT, Zero},
SaturatedConversion,
......@@ -57,6 +58,7 @@ mod storage_types;
/// Module, containing weights for this pallet.
pub mod weights;
pub mod weights_ext;
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
......@@ -65,6 +67,7 @@ pub mod benchmarking;
pub use call_ext::*;
pub use pallet::*;
pub use weights::WeightInfo;
pub use weights_ext::WeightInfoExt;
/// The target that will be used when publishing logs related to this pallet.
pub const LOG_TARGET: &str = "runtime::bridge-grandpa";
......@@ -101,17 +104,31 @@ pub mod pallet {
/// The chain we are bridging to here.
type BridgedChain: ChainWithGrandpa;
/// Maximal number of "free" mandatory header transactions per block.
/// Maximal number of "free" header transactions per block.
///
/// To be able to track the bridged chain, the pallet requires all headers that are
/// changing GRANDPA authorities set at the bridged chain (we call them mandatory).
/// So it is a common good deed to submit mandatory headers to the pallet. However, if the
/// bridged chain gets compromised, its validators may generate as many mandatory headers
/// as they want. And they may fill the whole block (at this chain) for free. This constants
/// limits number of calls that we may refund in a single block. All calls above this
/// limit are accepted, but are not refunded.
/// So it is a common good deed to submit mandatory headers to the pallet.
///
/// The pallet may be configured (see `[Self::FreeHeadersInterval]`) to import some
/// non-mandatory headers for free as well. It also may be treated as a common good
/// deed, because it may help to reduce bridge fees - this cost may be deducted from
/// bridge fees, paid by message senders.
///
/// However, if the bridged chain gets compromised, its validators may generate as many
/// "free" headers as they want. And they may fill the whole block (at this chain) for
/// free. This constants limits number of calls that we may refund in a single block.
/// All calls above this limit are accepted, but are not refunded.
#[pallet::constant]
type MaxFreeHeadersPerBlock: Get<u32>;
/// The distance between bridged chain headers, that may be submitted for free. The
/// first free header is header number zero, the next one is header number
/// `FreeHeadersInterval::get()` or any of its descendant if that header has not
/// been submitted. In other words, interval between free headers should be at least
/// `FreeHeadersInterval`.
#[pallet::constant]
type MaxFreeMandatoryHeadersPerBlock: Get<u32>;
type FreeHeadersInterval: Get<Option<u32>>;
/// Maximal number of finalized headers to keep in the storage.
///
......@@ -124,7 +141,7 @@ pub mod pallet {
type HeadersToKeep: Get<u32>;
/// Weights gathered through benchmarking.
type WeightInfo: WeightInfo;
type WeightInfo: WeightInfoExt;
}
#[pallet::pallet]
......@@ -133,12 +150,12 @@ pub mod pallet {
#[pallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
FreeMandatoryHeadersRemaining::<T, I>::put(T::MaxFreeMandatoryHeadersPerBlock::get());
FreeHeadersRemaining::<T, I>::put(T::MaxFreeHeadersPerBlock::get());
Weight::zero()
}
fn on_finalize(_n: BlockNumberFor<T>) {
FreeMandatoryHeadersRemaining::<T, I>::kill();
FreeHeadersRemaining::<T, I>::kill();
}
}
......@@ -155,7 +172,7 @@ pub mod pallet {
/// `submit_finality_proof_ex` instead. Semantically, this call is an equivalent of the
/// `submit_finality_proof_ex` call without current authority set id check.
#[pallet::call_index(0)]
#[pallet::weight(<T::WeightInfo as WeightInfo>::submit_finality_proof(
#[pallet::weight(T::WeightInfo::submit_finality_proof_weight(
justification.commit.precommits.len().saturated_into(),
justification.votes_ancestries.len().saturated_into(),
))]
......@@ -175,6 +192,8 @@ pub mod pallet {
// the `submit_finality_proof_ex` also reads this value, but it is done from the
// cache, so we don't treat it as an additional db access
<CurrentAuthoritySet<T, I>>::get().set_id,
// cannot enforce free execution using this call
false,
)
}
......@@ -250,8 +269,14 @@ pub mod pallet {
/// - verification is not optimized or invalid;
///
/// - header contains forced authorities set change or change with non-zero delay.
///
/// The `is_free_execution_expected` parameter is not really used inside the call. It is
/// used by the transaction extension, which should be registered at the runtime level. If
/// this parameter is `true`, the transaction will be treated as invalid, if the call won't
/// be executed for free. If transaction extension is not used by the runtime, this
/// parameter is not used at all.
#[pallet::call_index(4)]
#[pallet::weight(<T::WeightInfo as WeightInfo>::submit_finality_proof(
#[pallet::weight(T::WeightInfo::submit_finality_proof_weight(
justification.commit.precommits.len().saturated_into(),
justification.votes_ancestries.len().saturated_into(),
))]
......@@ -260,6 +285,7 @@ pub mod pallet {
finality_target: Box<BridgedHeader<T, I>>,
justification: GrandpaJustification<BridgedHeader<T, I>>,
current_set_id: sp_consensus_grandpa::SetId,
_is_free_execution_expected: bool,
) -> DispatchResultWithPostInfo {
Self::ensure_not_halted().map_err(Error::<T, I>::BridgeModule)?;
ensure_signed(origin)?;
......@@ -273,7 +299,8 @@ pub mod pallet {
// it checks whether the `number` is better than the current best block number
// and whether the `current_set_id` matches the best known set id
SubmitFinalityProofHelper::<T, I>::check_obsolete(number, Some(current_set_id))?;
let improved_by =
SubmitFinalityProofHelper::<T, I>::check_obsolete(number, Some(current_set_id))?;
let authority_set = <CurrentAuthoritySet<T, I>>::get();
let unused_proof_size = authority_set.unused_proof_size();
......@@ -283,23 +310,16 @@ pub mod pallet {
let maybe_new_authority_set =
try_enact_authority_change::<T, I>(&finality_target, set_id)?;
let may_refund_call_fee = maybe_new_authority_set.is_some() &&
// if we have seen too many mandatory headers in this block, we don't want to refund
Self::free_mandatory_headers_remaining() > 0 &&
// if arguments out of expected bounds, we don't want to refund
submit_finality_proof_info_from_args::<T, I>(&finality_target, &justification, Some(current_set_id))
.fits_limits();
let may_refund_call_fee = may_refund_call_fee::<T, I>(
&finality_target,
&justification,
current_set_id,
improved_by,
);
if may_refund_call_fee {
FreeMandatoryHeadersRemaining::<T, I>::mutate(|count| {
*count = count.saturating_sub(1)
});
on_free_header_imported::<T, I>();
}
insert_header::<T, I>(*finality_target, hash);
log::info!(
target: LOG_TARGET,
"Successfully imported finalized header with hash {:?}!",
hash
);
// mandatory header is a header that changes authorities set. The pallet can't go
// further without importing this header. So every bridge MUST import mandatory headers.
......@@ -311,6 +331,13 @@ pub mod pallet {
// to pay for the transaction.
let pays_fee = if may_refund_call_fee { Pays::No } else { Pays::Yes };
log::info!(
target: LOG_TARGET,
"Successfully imported finalized header with hash {:?}! Free: {}",
hash,
if may_refund_call_fee { "Yes" } else { "No" },
);
// the proof size component of the call weight assumes that there are
// `MaxBridgedAuthorities` in the `CurrentAuthoritySet` (we use `MaxEncodedLen`
// estimation). But if their number is lower, then we may "refund" some `proof_size`,
......@@ -335,20 +362,18 @@ pub mod pallet {
}
}
/// Number mandatory headers that we may accept in the current block for free (returning
/// `Pays::No`).
/// Number of free header submissions that we may yet accept in the current block.
///
/// If the `FreeMandatoryHeadersRemaining` hits zero, all following mandatory headers in the
/// If the `FreeHeadersRemaining` hits zero, all following mandatory headers in the
/// current block are accepted with fee (`Pays::Yes` is returned).
///
/// The `FreeMandatoryHeadersRemaining` is an ephemeral value that is set to
/// `MaxFreeMandatoryHeadersPerBlock` at each block initialization and is killed on block
/// The `FreeHeadersRemaining` is an ephemeral value that is set to
/// `MaxFreeHeadersPerBlock` at each block initialization and is killed on block
/// finalization. So it never ends up in the storage trie.
#[pallet::storage]
#[pallet::whitelist_storage]
#[pallet::getter(fn free_mandatory_headers_remaining)]
pub(super) type FreeMandatoryHeadersRemaining<T: Config<I>, I: 'static = ()> =
StorageValue<_, u32, ValueQuery>;
pub type FreeHeadersRemaining<T: Config<I>, I: 'static = ()> =
StorageValue<_, u32, OptionQuery>;
/// Hash of the header used to bootstrap the pallet.
#[pallet::storage]
......@@ -473,6 +498,68 @@ pub mod pallet {
/// The `current_set_id` argument of the `submit_finality_proof_ex` doesn't match
/// the id of the current set, known to the pallet.
InvalidAuthoritySetId,
/// The submitter wanted free execution, but we can't fit more free transactions
/// to the block.
FreeHeadersLimitExceded,
/// The submitter wanted free execution, but the difference between best known and
/// bundled header numbers is below the `FreeHeadersInterval`.
BelowFreeHeaderInterval,
}
/// Called when new free header is imported.
pub fn on_free_header_imported<T: Config<I>, I: 'static>() {
FreeHeadersRemaining::<T, I>::mutate(|count| {
*count = match *count {
None => None,
// the signed extension expects that `None` means outside of block
// execution - i.e. when transaction is validated from the transaction pool,
// so use `saturating_sub` and don't go from `Some(0)`->`None`
Some(count) => Some(count.saturating_sub(1)),
}
});
}
/// Return true if we may refund transaction cost to the submitter. In other words,
/// this transaction is considered as common good deed w.r.t to pallet configuration.
fn may_refund_call_fee<T: Config<I>, I: 'static>(
finality_target: &BridgedHeader<T, I>,
justification: &GrandpaJustification<BridgedHeader<T, I>>,
current_set_id: SetId,
improved_by: BridgedBlockNumber<T, I>,
) -> bool {
// if we have refunded too much at this block => not refunding
if FreeHeadersRemaining::<T, I>::get().unwrap_or(0) == 0 {
return false;
}
// if size/weight of call is larger than expected => not refunding
let call_info = submit_finality_proof_info_from_args::<T, I>(
&finality_target,
&justification,
Some(current_set_id),
// this function is called from the transaction body and we do not want
// to do MAY-be-free-executed checks here - they had to be done in the
// transaction extension before
false,
);
if !call_info.fits_limits() {
return false;
}
// if that's a mandatory header => refund
if call_info.is_mandatory {
return true;
}
// if configuration allows free non-mandatory headers and the header
// matches criteria => refund
if let Some(free_headers_interval) = T::FreeHeadersInterval::get() {
if improved_by >= free_headers_interval.into() {
return true;
}
}
false
}
/// Check the given header for a GRANDPA scheduled authority set change. If a change
......@@ -692,8 +779,8 @@ pub fn initialize_for_benchmarks<T: Config<I>, I: 'static>(header: BridgedHeader
mod tests {
use super::*;
use crate::mock::{
run_test, test_header, RuntimeEvent as TestEvent, RuntimeOrigin, System, TestBridgedChain,
TestHeader, TestNumber, TestRuntime, MAX_BRIDGED_AUTHORITIES,
run_test, test_header, FreeHeadersInterval, RuntimeEvent as TestEvent, RuntimeOrigin,
System, TestBridgedChain, TestHeader, TestNumber, TestRuntime, MAX_BRIDGED_AUTHORITIES,
};
use bp_header_chain::BridgeGrandpaCall;
use bp_runtime::BasicOperatingMode;
......@@ -747,6 +834,7 @@ mod tests {
Box::new(header),
justification,
TEST_GRANDPA_SET_ID,
false,
)
}
......@@ -766,6 +854,7 @@ mod tests {
Box::new(header),
justification,
set_id,
false,
)
}
......@@ -794,6 +883,7 @@ mod tests {
Box::new(header),
justification,
set_id,
false,
)
}
......@@ -1009,6 +1099,7 @@ mod tests {
Box::new(header.clone()),
justification.clone(),
TEST_GRANDPA_SET_ID,
false,
),
<Error<TestRuntime>>::InvalidJustification
);
......@@ -1018,6 +1109,7 @@ mod tests {
Box::new(header),
justification,
next_set_id,
false,
),
<Error<TestRuntime>>::InvalidAuthoritySetId
);
......@@ -1039,6 +1131,7 @@ mod tests {
Box::new(header),
justification,
TEST_GRANDPA_SET_ID,
false,
),
<Error<TestRuntime>>::InvalidJustification
);
......@@ -1069,6 +1162,7 @@ mod tests {
Box::new(header),
justification,
TEST_GRANDPA_SET_ID,
false,
),
<Error<TestRuntime>>::InvalidAuthoritySet
);
......@@ -1108,6 +1202,7 @@ mod tests {
Box::new(header.clone()),
justification.clone(),
TEST_GRANDPA_SET_ID,
false,
);
assert_ok!(result);
assert_eq!(result.unwrap().pays_fee, frame_support::dispatch::Pays::No);
......@@ -1171,6 +1266,7 @@ mod tests {
Box::new(header.clone()),
justification,
TEST_GRANDPA_SET_ID,
false,
);
assert_ok!(result);
assert_eq!(result.unwrap().pays_fee, frame_support::dispatch::Pays::Yes);
......@@ -1203,6 +1299,7 @@ mod tests {
Box::new(header.clone()),
justification,
TEST_GRANDPA_SET_ID,
false,
);
assert_ok!(result);
assert_eq!(result.unwrap().pays_fee, frame_support::dispatch::Pays::Yes);
......@@ -1233,6 +1330,7 @@ mod tests {
Box::new(header),
justification,
TEST_GRANDPA_SET_ID,
false,
),
<Error<TestRuntime>>::UnsupportedScheduledChange
);
......@@ -1259,6 +1357,7 @@ mod tests {
Box::new(header),
justification,
TEST_GRANDPA_SET_ID,
false,
),
<Error<TestRuntime>>::UnsupportedScheduledChange
);
......@@ -1285,6 +1384,7 @@ mod tests {
Box::new(header),
justification,
TEST_GRANDPA_SET_ID,
false,
),
<Error<TestRuntime>>::TooManyAuthoritiesInSet
);
......@@ -1350,12 +1450,13 @@ mod tests {
Box::new(header),
invalid_justification,
TEST_GRANDPA_SET_ID,
false,
)
};
initialize_substrate_bridge();
for _ in 0..<TestRuntime as Config>::MaxFreeMandatoryHeadersPerBlock::get() + 1 {
for _ in 0..<TestRuntime as Config>::MaxFreeHeadersPerBlock::get() + 1 {
assert_err!(submit_invalid_request(), <Error<TestRuntime>>::InvalidJustification);
}
......@@ -1423,6 +1524,64 @@ mod tests {
})
}
#[test]
fn may_import_non_mandatory_header_for_free() {
run_test(|| {
initialize_substrate_bridge();
// set best finalized to `100`
const BEST: u8 = 12;
fn reset_best() {
BestFinalized::<TestRuntime, ()>::set(Some(HeaderId(
BEST as _,
Default::default(),
)));
}
// non-mandatory header is imported with fee
reset_best();
let non_free_header_number = BEST + FreeHeadersInterval::get() as u8 - 1;
let result = submit_finality_proof(non_free_header_number);
assert_eq!(result.unwrap().pays_fee, Pays::Yes);
// non-mandatory free header is imported without fee
reset_best();
let free_header_number = BEST + FreeHeadersInterval::get() as u8;
let result = submit_finality_proof(free_header_number);
assert_eq!(result.unwrap().pays_fee, Pays::No);
// another non-mandatory free header is imported without fee
let free_header_number = BEST + FreeHeadersInterval::get() as u8 * 2;
let result = submit_finality_proof(free_header_number);
assert_eq!(result.unwrap().pays_fee, Pays::No);
// now the rate limiter starts charging fees even for free headers
let free_header_number = BEST + FreeHeadersInterval::get() as u8 * 3;
let result = submit_finality_proof(free_header_number);
assert_eq!(result.unwrap().pays_fee, Pays::Yes);
// check that we can import for free if `improved_by` is larger
// than the free interval
next_block();
reset_best();
let free_header_number = FreeHeadersInterval::get() as u8 + 42;
let result = submit_finality_proof(free_header_number);
assert_eq!(result.unwrap().pays_fee, Pays::No);
// check that the rate limiter shares the counter between mandatory
// and free non-mandatory headers
next_block();
reset_best();
let free_header_number = BEST + FreeHeadersInterval::get() as u8 * 4;
let result = submit_finality_proof(free_header_number);
assert_eq!(result.unwrap().pays_fee, Pays::No);
let result = submit_mandatory_finality_proof(free_header_number + 1, 1);
assert_eq!(result.expect("call failed").pays_fee, Pays::No);
let result = submit_mandatory_finality_proof(free_header_number + 2, 2);
assert_eq!(result.expect("call failed").pays_fee, Pays::Yes);
});
}
#[test]
fn should_prune_headers_over_headers_to_keep_parameter() {
run_test(|| {
......@@ -1519,9 +1678,23 @@ mod tests {
Box::new(header),
justification,
TEST_GRANDPA_SET_ID,
false,
),
DispatchError::BadOrigin,
);
})
}
#[test]
fn on_free_header_imported_never_sets_to_none() {
run_test(|| {
FreeHeadersRemaining::<TestRuntime, ()>::set(Some(2));
on_free_header_imported::<TestRuntime, ()>();
assert_eq!(FreeHeadersRemaining::<TestRuntime, ()>::get(), Some(1));
on_free_header_imported::<TestRuntime, ()>();
assert_eq!(FreeHeadersRemaining::<TestRuntime, ()>::get(), Some(0));
on_free_header_imported::<TestRuntime, ()>();
assert_eq!(FreeHeadersRemaining::<TestRuntime, ()>::get(), Some(0));
})
}
}
......@@ -48,14 +48,16 @@ impl frame_system::Config for TestRuntime {
}
parameter_types! {
pub const MaxFreeMandatoryHeadersPerBlock: u32 = 2;
pub const MaxFreeHeadersPerBlock: u32 = 2;
pub const FreeHeadersInterval: u32 = 32;
pub const HeadersToKeep: u32 = 5;
}
impl grandpa::Config for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type BridgedChain = TestBridgedChain;
type MaxFreeMandatoryHeadersPerBlock = MaxFreeMandatoryHeadersPerBlock;
type MaxFreeHeadersPerBlock = MaxFreeHeadersPerBlock;
type FreeHeadersInterval = FreeHeadersInterval;
type HeadersToKeep = HeadersToKeep;
type WeightInfo = ();
}
......
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity Bridges Common is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
//! Weight-related utilities.
use crate::weights::{BridgeWeight, WeightInfo};
use frame_support::weights::Weight;
/// Extended weight info.
pub trait WeightInfoExt: WeightInfo {
// Our configuration assumes that the runtime has special signed extensions used to:
//
// 1) boost priority of `submit_finality_proof` transactions;
//
// 2) slash relayer if he submits an invalid transaction.
//
// We read and update storage values of other pallets (`pallet-bridge-relayers` and
// balances/assets pallet). So we need to add this weight to the weight of our call.
// Hence two following methods.
/// Extra weight that is added to the `submit_finality_proof` call weight by signed extensions
/// that are declared at runtime level.
fn submit_finality_proof_overhead_from_runtime() -> Weight;
// Functions that are directly mapped to extrinsics weights.
/// Weight of message delivery extrinsic.
fn submit_finality_proof_weight(precommits_len: u32, votes_ancestries_len: u32) -> Weight {
let base_weight = Self::submit_finality_proof(precommits_len, votes_ancestries_len);
base_weight.saturating_add(Self::submit_finality_proof_overhead_from_runtime())
}
}
impl<T: frame_system::Config> WeightInfoExt for BridgeWeight<T> {
fn submit_finality_proof_overhead_from_runtime() -> Weight {
Weight::zero()
}
}
impl WeightInfoExt for () {
fn submit_finality_proof_overhead_from_runtime() -> Weight {
Weight::zero()
}
}
......@@ -14,25 +14,45 @@
// 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 crate::{Config, GrandpaPalletOf, Pallet, RelayBlockHash, RelayBlockNumber};
use bp_header_chain::HeaderChain;
use bp_parachains::BestParaHeadHash;
use bp_polkadot_core::parachains::{ParaHash, ParaId};
use bp_runtime::OwnedBridgeModule;
use frame_support::{dispatch::CallableCallFor, traits::IsSubType};
use bp_runtime::{HeaderId, OwnedBridgeModule};
use frame_support::{
dispatch::CallableCallFor,
traits::{Get, IsSubType},
};
use pallet_bridge_grandpa::SubmitFinalityProofHelper;
use sp_runtime::{
transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction},
traits::Zero,
transaction_validity::{InvalidTransaction, TransactionValidityError},
RuntimeDebug,
};
/// Info about a `SubmitParachainHeads` call which tries to update a single parachain.
#[derive(PartialEq, RuntimeDebug)]
pub struct SubmitParachainHeadsInfo {
/// Number of the finalized relay block that has been used to prove parachain finality.
pub at_relay_block_number: RelayBlockNumber,
/// Number and hash of the finalized relay block that has been used to prove parachain
/// finality.
pub at_relay_block: HeaderId<RelayBlockHash, RelayBlockNumber>,
/// Parachain identifier.
pub para_id: ParaId,
/// Hash of the bundled parachain head.
pub para_head_hash: ParaHash,
/// If `true`, then the call must be free (assuming that everything else is valid) to
/// be treated as valid.
pub is_free_execution_expected: bool,
}
/// Verified `SubmitParachainHeadsInfo`.
#[derive(PartialEq, RuntimeDebug)]
pub struct VerifiedSubmitParachainHeadsInfo {
/// Base call information.
pub base: SubmitParachainHeadsInfo,
/// A difference between bundled bridged relay chain header and relay chain header number
/// used to prove best bridged parachain header, known to us before the call.
pub improved_by: RelayBlockNumber,
}
/// Helper struct that provides methods for working with the `SubmitParachainHeads` call.
......@@ -41,40 +61,117 @@ pub struct SubmitParachainHeadsHelper<T: Config<I>, I: 'static> {
}
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,
/// Check that is called from signed extension and takes the `is_free_execution_expected`
/// into account.
pub fn check_obsolete_from_extension(
update: &SubmitParachainHeadsInfo,
) -> Result<RelayBlockNumber, TransactionValidityError> {
// first do all base checks
let improved_by = Self::check_obsolete(update)?;
// if we don't expect free execution - no more checks
if !update.is_free_execution_expected {
return Ok(improved_by);
}
// reject if no more free slots remaining in the block
if !SubmitFinalityProofHelper::<T, T::BridgesGrandpaPalletInstance>::has_free_header_slots()
{
log::trace!(
target: crate::LOG_TARGET,
"The free parachain {:?} head can't be updated: no more free slots \
left in the block.",
update.para_id,
);
return Err(InvalidTransaction::Call.into());
}
// if free headers interval is not configured and call is expected to execute
// for free => it is a relayer error, it should've been able to detect that.
let free_headers_interval = match T::FreeHeadersInterval::get() {
Some(free_headers_interval) => free_headers_interval,
None => return Ok(improved_by),
};
if stored_best_head.best_head_hash.at_relay_block_number >= update.at_relay_block_number {
// reject if we are importing parachain headers too often
if improved_by < free_headers_interval {
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 {} >= {}.",
"The free parachain {:?} head can't be updated: it improves previous
best head by {} while at least {} is expected.",
update.para_id,
stored_best_head.best_head_hash.at_relay_block_number,
update.at_relay_block_number
improved_by,
free_headers_interval,
);
return true
return Err(InvalidTransaction::Stale.into());
}
if stored_best_head.best_head_hash.head_hash == update.para_head_hash {
Ok(improved_by)
}
/// Check if the para head provided by the `SubmitParachainHeads` is better than the best one
/// we know.
pub fn check_obsolete(
update: &SubmitParachainHeadsInfo,
) -> Result<RelayBlockNumber, TransactionValidityError> {
// check if we know better parachain head already
let improved_by = match crate::ParasInfo::<T, I>::get(update.para_id) {
Some(stored_best_head) => {
let improved_by = match update
.at_relay_block
.0
.checked_sub(stored_best_head.best_head_hash.at_relay_block_number)
{
Some(improved_by) if improved_by > Zero::zero() => improved_by,
_ => {
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.0
);
return Err(InvalidTransaction::Stale.into())
},
};
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.0
);
return Err(InvalidTransaction::Stale.into())
}
improved_by
},
None => RelayBlockNumber::MAX,
};
// let's check if our chain had no reorgs and we still know the relay chain header
// used to craft the proof
if GrandpaPalletOf::<T, I>::finalized_header_state_root(update.at_relay_block.1).is_none() {
log::trace!(
target: crate::LOG_TARGET,
"The parachain head can't be updated. The parachain head hash for {:?} \
was already updated to {} at block {} < {}.",
"The parachain {:?} head can't be updated. Relay chain header {}/{} used to create \
parachain proof is missing from the storage.",
update.para_id,
update.para_head_hash,
stored_best_head.best_head_hash.at_relay_block_number,
update.at_relay_block_number
update.at_relay_block.0,
update.at_relay_block.1,
);
return true
return Err(InvalidTransaction::Call.into())
}
false
Ok(improved_by)
}
/// Check if the `SubmitParachainHeads` was successfully executed.
......@@ -83,7 +180,7 @@ impl<T: Config<I>, I: 'static> SubmitParachainHeadsHelper<T, I> {
Some(stored_best_head) =>
stored_best_head.best_head_hash ==
BestParaHeadHash {
at_relay_block_number: update.at_relay_block_number,
at_relay_block_number: update.at_relay_block.0,
head_hash: update.para_head_hash,
},
None => false,
......@@ -98,22 +195,36 @@ pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
/// 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,
match self.is_sub_type() {
Some(crate::Call::<T, I>::submit_parachain_heads {
ref at_relay_block,
ref parachains,
..
}) => match &parachains[..] {
&[(para_id, para_head_hash)] => Some(SubmitParachainHeadsInfo {
at_relay_block: HeaderId(at_relay_block.0, at_relay_block.1),
para_id,
para_head_hash,
})
}
is_free_execution_expected: false,
}),
_ => None,
},
Some(crate::Call::<T, I>::submit_parachain_heads_ex {
ref at_relay_block,
ref parachains,
is_free_execution_expected,
..
}) => match &parachains[..] {
&[(para_id, para_head_hash)] => Some(SubmitParachainHeadsInfo {
at_relay_block: HeaderId(at_relay_block.0, at_relay_block.1),
para_id,
para_head_hash,
is_free_execution_expected: *is_free_execution_expected,
}),
_ => None,
},
_ => None,
}
None
}
/// Create a new instance of `SubmitParachainHeadsInfo` from a `SubmitParachainHeads` call with
......@@ -133,24 +244,23 @@ pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
/// 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
fn check_obsolete_submit_parachain_heads(
&self,
) -> Result<Option<VerifiedSubmitParachainHeadsInfo>, TransactionValidityError>
where
Self: Sized,
{
let update = match self.one_entry_submit_parachain_heads_info() {
Some(update) => update,
None => return Ok(ValidTransaction::default()),
None => return Ok(None),
};
if Pallet::<T, I>::ensure_not_halted().is_err() {
return InvalidTransaction::Call.into()
return Err(InvalidTransaction::Call.into())
}
if SubmitParachainHeadsHelper::<T, I>::is_obsolete(&update) {
return InvalidTransaction::Stale.into()
}
Ok(ValidTransaction::default())
SubmitParachainHeadsHelper::<T, I>::check_obsolete_from_extension(&update)
.map(|improved_by| Some(VerifiedSubmitParachainHeadsInfo { base: update, improved_by }))
}
}
......@@ -164,9 +274,10 @@ where
#[cfg(test)]
mod tests {
use crate::{
mock::{run_test, RuntimeCall, TestRuntime},
CallSubType, PalletOperatingMode, ParaInfo, ParasInfo, RelayBlockNumber,
mock::{run_test, FreeHeadersInterval, RuntimeCall, TestRuntime},
CallSubType, PalletOperatingMode, ParaInfo, ParasInfo, RelayBlockHash, RelayBlockNumber,
};
use bp_header_chain::StoredHeaderData;
use bp_parachains::BestParaHeadHash;
use bp_polkadot_core::parachains::{ParaHash, ParaHeadsProof, ParaId};
use bp_runtime::BasicOperatingMode;
......@@ -175,15 +286,37 @@ mod tests {
num: RelayBlockNumber,
parachains: Vec<(ParaId, ParaHash)>,
) -> bool {
RuntimeCall::Parachains(crate::Call::<TestRuntime, ()>::submit_parachain_heads {
at_relay_block: (num, Default::default()),
RuntimeCall::Parachains(crate::Call::<TestRuntime, ()>::submit_parachain_heads_ex {
at_relay_block: (num, [num as u8; 32].into()),
parachains,
parachain_heads_proof: ParaHeadsProof { storage_proof: Vec::new() },
is_free_execution_expected: false,
})
.check_obsolete_submit_parachain_heads()
.is_ok()
}
fn validate_free_submit_parachain_heads(
num: RelayBlockNumber,
parachains: Vec<(ParaId, ParaHash)>,
) -> bool {
RuntimeCall::Parachains(crate::Call::<TestRuntime, ()>::submit_parachain_heads_ex {
at_relay_block: (num, [num as u8; 32].into()),
parachains,
parachain_heads_proof: ParaHeadsProof { storage_proof: Vec::new() },
is_free_execution_expected: true,
})
.check_obsolete_submit_parachain_heads()
.is_ok()
}
fn insert_relay_block(num: RelayBlockNumber) {
pallet_bridge_grandpa::ImportedHeaders::<TestRuntime, crate::Instance1>::insert(
RelayBlockHash::from([num as u8; 32]),
StoredHeaderData { number: num, state_root: RelayBlockHash::from([10u8; 32]) },
);
}
fn sync_to_relay_header_10() {
ParasInfo::<TestRuntime, ()>::insert(
ParaId(1),
......@@ -244,6 +377,7 @@ mod tests {
// when current best finalized is #10 and we're trying to import header#15 => tx is
// accepted
sync_to_relay_header_10();
insert_relay_block(15);
assert!(validate_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
});
}
......@@ -260,4 +394,65 @@ mod tests {
));
});
}
#[test]
fn extension_rejects_initial_parachain_head_if_missing_relay_chain_header() {
run_test(|| {
// when relay chain header is unknown => "obsolete"
assert!(!validate_submit_parachain_heads(10, vec![(ParaId(1), [1u8; 32].into())]));
// when relay chain header is unknown => "ok"
insert_relay_block(10);
assert!(validate_submit_parachain_heads(10, vec![(ParaId(1), [1u8; 32].into())]));
});
}
#[test]
fn extension_rejects_free_parachain_head_if_missing_relay_chain_header() {
run_test(|| {
sync_to_relay_header_10();
// when relay chain header is unknown => "obsolete"
assert!(!validate_submit_parachain_heads(15, vec![(ParaId(2), [15u8; 32].into())]));
// when relay chain header is unknown => "ok"
insert_relay_block(15);
assert!(validate_submit_parachain_heads(15, vec![(ParaId(2), [15u8; 32].into())]));
});
}
#[test]
fn extension_rejects_free_parachain_head_if_no_free_slots_remaining() {
run_test(|| {
// when current best finalized is #10 and we're trying to import header#15 => tx should
// be accepted
sync_to_relay_header_10();
insert_relay_block(15);
// ... but since we have specified `is_free_execution_expected = true`, it'll be
// rejected
assert!(!validate_free_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
// ... if we have specify `is_free_execution_expected = false`, it'll be accepted
assert!(validate_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
});
}
#[test]
fn extension_rejects_free_parachain_head_if_improves_by_is_below_expected() {
run_test(|| {
// when current best finalized is #10 and we're trying to import header#15 => tx should
// be accepted
sync_to_relay_header_10();
insert_relay_block(10 + FreeHeadersInterval::get() - 1);
insert_relay_block(10 + FreeHeadersInterval::get());
// try to submit at 10 + FreeHeadersInterval::get() - 1 => failure
let relay_header = 10 + FreeHeadersInterval::get() - 1;
assert!(!validate_free_submit_parachain_heads(
relay_header,
vec![(ParaId(1), [2u8; 32].into())]
));
// try to submit at 10 + FreeHeadersInterval::get() => ok
let relay_header = 10 + FreeHeadersInterval::get();
assert!(validate_free_submit_parachain_heads(
relay_header,
vec![(ParaId(1), [2u8; 32].into())]
));
});
}
}
This diff is collapsed.
......@@ -70,6 +70,7 @@ impl Chain for Parachain1 {
impl Parachain for Parachain1 {
const PARACHAIN_ID: u32 = 1;
const MAX_HEADER_SIZE: u32 = 1_024;
}
pub struct Parachain2;
......@@ -96,6 +97,7 @@ impl Chain for Parachain2 {
impl Parachain for Parachain2 {
const PARACHAIN_ID: u32 = 2;
const MAX_HEADER_SIZE: u32 = 1_024;
}
pub struct Parachain3;
......@@ -122,6 +124,7 @@ impl Chain for Parachain3 {
impl Parachain for Parachain3 {
const PARACHAIN_ID: u32 = 3;
const MAX_HEADER_SIZE: u32 = 1_024;
}
// this parachain is using u128 as block number and stored head data size exceeds limit
......@@ -149,6 +152,7 @@ impl Chain for BigParachain {
impl Parachain for BigParachain {
const PARACHAIN_ID: u32 = 4;
const MAX_HEADER_SIZE: u32 = 2_048;
}
construct_runtime! {
......@@ -168,12 +172,14 @@ impl frame_system::Config for TestRuntime {
parameter_types! {
pub const HeadersToKeep: u32 = 5;
pub const FreeHeadersInterval: u32 = 15;
}
impl pallet_bridge_grandpa::Config<pallet_bridge_grandpa::Instance1> for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type BridgedChain = TestBridgedChain;
type MaxFreeMandatoryHeadersPerBlock = ConstU32<2>;
type MaxFreeHeadersPerBlock = ConstU32<2>;
type FreeHeadersInterval = FreeHeadersInterval;
type HeadersToKeep = HeadersToKeep;
type WeightInfo = ();
}
......@@ -181,7 +187,8 @@ impl pallet_bridge_grandpa::Config<pallet_bridge_grandpa::Instance1> for TestRun
impl pallet_bridge_grandpa::Config<pallet_bridge_grandpa::Instance2> for TestRuntime {
type RuntimeEvent = RuntimeEvent;
type BridgedChain = TestBridgedChain;
type MaxFreeMandatoryHeadersPerBlock = ConstU32<2>;
type MaxFreeHeadersPerBlock = ConstU32<2>;
type FreeHeadersInterval = FreeHeadersInterval;
type HeadersToKeep = HeadersToKeep;
type WeightInfo = ();
}
......
......@@ -36,6 +36,20 @@ pub const EXTRA_STORAGE_PROOF_SIZE: u32 = 1024;
/// Extended weight info.
pub trait WeightInfoExt: WeightInfo {
// Our configuration assumes that the runtime has special signed extensions used to:
//
// 1) boost priority of `submit_parachain_heads` transactions;
//
// 2) slash relayer if he submits an invalid transaction.
//
// We read and update storage values of other pallets (`pallet-bridge-relayers` and
// balances/assets pallet). So we need to add this weight to the weight of our call.
// Hence two following methods.
/// Extra weight that is added to the `submit_finality_proof` call weight by signed extensions
/// that are declared at runtime level.
fn submit_parachain_heads_overhead_from_runtime() -> Weight;
/// Storage proof overhead, that is included in every storage proof.
///
/// The relayer would pay some extra fee for additional proof bytes, since they mean
......@@ -65,7 +79,10 @@ pub trait WeightInfoExt: WeightInfo {
let pruning_weight =
Self::parachain_head_pruning_weight(db_weight).saturating_mul(parachains_count as u64);
base_weight.saturating_add(proof_size_overhead).saturating_add(pruning_weight)
base_weight
.saturating_add(proof_size_overhead)
.saturating_add(pruning_weight)
.saturating_add(Self::submit_parachain_heads_overhead_from_runtime())
}
/// Returns weight of single parachain head storage update.
......@@ -95,12 +112,20 @@ pub trait WeightInfoExt: WeightInfo {
}
impl WeightInfoExt for () {
fn submit_parachain_heads_overhead_from_runtime() -> Weight {
Weight::zero()
}
fn expected_extra_storage_proof_size() -> u32 {
EXTRA_STORAGE_PROOF_SIZE
}
}
impl<T: frame_system::Config> WeightInfoExt for BridgeWeight<T> {
fn submit_parachain_heads_overhead_from_runtime() -> Weight {
Weight::zero()
}
fn expected_extra_storage_proof_size() -> u32 {
EXTRA_STORAGE_PROOF_SIZE
}
......
......@@ -116,6 +116,10 @@ impl ParaStoredHeaderData {
/// Stored parachain head data builder.
pub trait ParaStoredHeaderDataBuilder {
/// Maximal parachain head size that we may accept for free. All heads above
/// this limit are submitted for a regular fee.
fn max_free_head_size() -> u32;
/// Return number of parachains that are supported by this builder.
fn supported_parachains() -> u32;
......@@ -127,6 +131,10 @@ pub trait ParaStoredHeaderDataBuilder {
pub struct SingleParaStoredHeaderDataBuilder<C: Parachain>(PhantomData<C>);
impl<C: Parachain> ParaStoredHeaderDataBuilder for SingleParaStoredHeaderDataBuilder<C> {
fn max_free_head_size() -> u32 {
C::MAX_HEADER_SIZE
}
fn supported_parachains() -> u32 {
1
}
......@@ -147,6 +155,17 @@ impl<C: Parachain> ParaStoredHeaderDataBuilder for SingleParaStoredHeaderDataBui
#[impl_trait_for_tuples::impl_for_tuples(1, 30)]
#[tuple_types_custom_trait_bound(Parachain)]
impl ParaStoredHeaderDataBuilder for C {
fn max_free_head_size() -> u32 {
let mut result = 0_u32;
for_tuples!( #(
result = sp_std::cmp::max(
result,
SingleParaStoredHeaderDataBuilder::<C>::max_free_head_size(),
);
)* );
result
}
fn supported_parachains() -> u32 {
let mut result = 0;
for_tuples!( #(
......
......@@ -236,6 +236,12 @@ where
pub trait Parachain: Chain {
/// Parachain identifier.
const PARACHAIN_ID: u32;
/// Maximal size of the parachain header.
///
/// This isn't a strict limit. The relayer may submit larger headers and the
/// pallet will accept the call. The limit is only used to compute whether
/// the refund can be made.
const MAX_HEADER_SIZE: u32;
}
impl<T> Parachain for T
......@@ -244,6 +250,8 @@ where
<T as UnderlyingChainProvider>::Chain: Parachain,
{
const PARACHAIN_ID: u32 = <<T as UnderlyingChainProvider>::Chain as Parachain>::PARACHAIN_ID;
const MAX_HEADER_SIZE: u32 =
<<T as UnderlyingChainProvider>::Chain as Parachain>::MAX_HEADER_SIZE;
}
/// Adapter for `Get<u32>` to access `PARACHAIN_ID` from `trait Parachain`
......@@ -306,6 +314,11 @@ macro_rules! decl_bridge_finality_runtime_apis {
pub const [<BEST_FINALIZED_ $chain:upper _HEADER_METHOD>]: &str =
stringify!([<$chain:camel FinalityApi_best_finalized>]);
/// Name of the `<ThisChain>FinalityApi::free_headers_interval` runtime method.
pub const [<FREE_HEADERS_INTERVAL_FOR_ $chain:upper _METHOD>]: &str =
stringify!([<$chain:camel FinalityApi_free_headers_interval>]);
$(
/// Name of the `<ThisChain>FinalityApi::accepted_<consensus>_finality_proofs`
/// runtime method.
......@@ -322,6 +335,13 @@ macro_rules! decl_bridge_finality_runtime_apis {
/// Returns number and hash of the best finalized header known to the bridge module.
fn best_finalized() -> Option<bp_runtime::HeaderId<Hash, BlockNumber>>;
/// Returns free headers interval, if it is configured in the runtime.
/// The caller expects that if his transaction improves best known header
/// at least by the free_headers_interval`, it will be fee-free.
///
/// See [`pallet_bridge_grandpa::Config::FreeHeadersInterval`] for details.
fn free_headers_interval() -> Option<BlockNumber>;
$(
/// Returns the justifications accepted in the current block.
fn [<synced_headers_ $consensus:lower _info>](
......
......@@ -46,6 +46,12 @@ pub trait Chain: ChainBase + Clone {
/// Keep in mind that this method is normally provided by the other chain, which is
/// bridged with this chain.
const BEST_FINALIZED_HEADER_ID_METHOD: &'static str;
/// Name of the runtime API method that is returning interval between source chain
/// headers that may be submitted for free to the target chain.
///
/// Keep in mind that this method is normally provided by the other chain, which is
/// bridged with this chain.
const FREE_HEADERS_INTERVAL_METHOD: &'static str;
/// Average block interval.
///
......@@ -75,6 +81,9 @@ pub trait ChainWithRuntimeVersion: Chain {
pub trait RelayChain: Chain {
/// Name of the `runtime_parachains::paras` pallet in the runtime of this chain.
const PARAS_PALLET_NAME: &'static str;
/// Name of the `pallet-bridge-parachains`, deployed at the **bridged** chain to sync
/// parachains of **this** chain.
const WITH_CHAIN_BRIDGE_PARACHAINS_PALLET_NAME: &'static str;
}
/// Substrate-based chain that is using direct GRANDPA finality from minimal relay-client point of
......
......@@ -56,6 +56,7 @@ impl bp_runtime::Chain for TestChain {
impl Chain for TestChain {
const NAME: &'static str = "Test";
const BEST_FINALIZED_HEADER_ID_METHOD: &'static str = "TestMethod";
const FREE_HEADERS_INTERVAL_METHOD: &'static str = "TestMethod";
const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_millis(0);
type SignedBlock = sp_runtime::generic::SignedBlock<
......@@ -110,6 +111,7 @@ impl bp_runtime::Chain for TestParachainBase {
impl bp_runtime::Parachain for TestParachainBase {
const PARACHAIN_ID: u32 = 1000;
const MAX_HEADER_SIZE: u32 = 1_024;
}
/// Parachain that may be used in tests.
......@@ -123,6 +125,7 @@ impl bp_runtime::UnderlyingChainProvider for TestParachain {
impl Chain for TestParachain {
const NAME: &'static str = "TestParachain";
const BEST_FINALIZED_HEADER_ID_METHOD: &'static str = "TestParachainMethod";
const FREE_HEADERS_INTERVAL_METHOD: &'static str = "TestParachainMethod";
const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_millis(0);
type SignedBlock = sp_runtime::generic::SignedBlock<
......
......@@ -33,7 +33,9 @@ node. The transaction is then tracked by the relay until it is mined and finaliz
The main entrypoint for the crate is the [`run` function](./src/finality_loop.rs), which takes source and target
clients and [`FinalitySyncParams`](./src/finality_loop.rs) parameters. The most important parameter is the
`only_mandatory_headers` - it is set to `true`, the relay will only submit mandatory headers. Since transactions
with mandatory headers are fee-free, the cost of running such relay is zero (in terms of fees).
with mandatory headers are fee-free, the cost of running such relay is zero (in terms of fees). If a similar,
`only_free_headers` parameter, is set to `true`, then free headers (if configured in the runtime) are also
relayed.
## Finality Relay Metrics
......
......@@ -16,10 +16,11 @@
use crate::{
finality_loop::SyncInfo, finality_proofs::FinalityProofsBuf, Error, FinalitySyncPipeline,
SourceClient, SourceHeader, TargetClient,
HeadersToRelay, SourceClient, SourceHeader, TargetClient,
};
use bp_header_chain::FinalityProof;
use num_traits::Saturating;
use std::cmp::Ordering;
/// Unjustified headers container. Ordered by header number.
......@@ -50,9 +51,13 @@ pub enum JustifiedHeaderSelector<P: FinalitySyncPipeline> {
}
impl<P: FinalitySyncPipeline> JustifiedHeaderSelector<P> {
/// Selects last header with persistent justification, missing from the target and matching
/// the `headers_to_relay` criteria.
pub(crate) async fn new<SC: SourceClient<P>, TC: TargetClient<P>>(
source_client: &SC,
info: &SyncInfo<P>,
headers_to_relay: HeadersToRelay,
free_headers_interval: Option<P::Number>,
) -> Result<Self, Error<P, SC::Error, TC::Error>> {
let mut unjustified_headers = Vec::new();
let mut maybe_justified_header = None;
......@@ -70,12 +75,19 @@ impl<P: FinalitySyncPipeline> JustifiedHeaderSelector<P> {
return Ok(Self::Mandatory(JustifiedHeader { header, proof }))
},
(true, None) => return Err(Error::MissingMandatoryFinalityProof(header.number())),
(false, Some(proof)) => {
(false, Some(proof))
if need_to_relay::<P>(
info,
headers_to_relay,
free_headers_interval,
&header,
) =>
{
log::trace!(target: "bridge", "Header {:?} has persistent finality proof", header_number);
unjustified_headers.clear();
maybe_justified_header = Some(JustifiedHeader { header, proof });
},
(false, None) => {
_ => {
unjustified_headers.push(header);
},
}
......@@ -97,6 +109,7 @@ impl<P: FinalitySyncPipeline> JustifiedHeaderSelector<P> {
})
}
/// Returns selected mandatory header if we have seen one. Otherwise returns `None`.
pub fn select_mandatory(self) -> Option<JustifiedHeader<P>> {
match self {
JustifiedHeaderSelector::Mandatory(header) => Some(header),
......@@ -104,7 +117,15 @@ impl<P: FinalitySyncPipeline> JustifiedHeaderSelector<P> {
}
}
pub fn select(self, buf: &FinalityProofsBuf<P>) -> Option<JustifiedHeader<P>> {
/// Tries to improve previously selected header using ephemeral
/// justifications stream.
pub fn select(
self,
info: &SyncInfo<P>,
headers_to_relay: HeadersToRelay,
free_headers_interval: Option<P::Number>,
buf: &FinalityProofsBuf<P>,
) -> Option<JustifiedHeader<P>> {
let (unjustified_headers, maybe_justified_header) = match self {
JustifiedHeaderSelector::Mandatory(justified_header) => return Some(justified_header),
JustifiedHeaderSelector::Regular(unjustified_headers, justified_header) =>
......@@ -122,7 +143,14 @@ impl<P: FinalitySyncPipeline> JustifiedHeaderSelector<P> {
(maybe_finality_proof, maybe_unjustified_header)
{
match finality_proof.target_header_number().cmp(&unjustified_header.number()) {
Ordering::Equal => {
Ordering::Equal
if need_to_relay::<P>(
info,
headers_to_relay,
free_headers_interval,
&unjustified_header,
) =>
{
log::trace!(
target: "bridge",
"Managed to improve selected {} finality proof {:?} to {:?}.",
......@@ -135,6 +163,10 @@ impl<P: FinalitySyncPipeline> JustifiedHeaderSelector<P> {
proof: finality_proof.clone(),
})
},
Ordering::Equal => {
maybe_finality_proof = finality_proofs_iter.next();
maybe_unjustified_header = unjustified_headers_iter.next();
},
Ordering::Less => maybe_unjustified_header = unjustified_headers_iter.next(),
Ordering::Greater => {
maybe_finality_proof = finality_proofs_iter.next();
......@@ -152,6 +184,27 @@ impl<P: FinalitySyncPipeline> JustifiedHeaderSelector<P> {
}
}
/// Returns true if we want to relay header `header_number`.
fn need_to_relay<P: FinalitySyncPipeline>(
info: &SyncInfo<P>,
headers_to_relay: HeadersToRelay,
free_headers_interval: Option<P::Number>,
header: &P::Header,
) -> bool {
match headers_to_relay {
HeadersToRelay::All => true,
HeadersToRelay::Mandatory => header.is_mandatory(),
HeadersToRelay::Free =>
header.is_mandatory() ||
free_headers_interval
.map(|free_headers_interval| {
header.number().saturating_sub(info.best_number_at_target) >=
free_headers_interval
})
.unwrap_or(false),
}
}
#[cfg(test)]
mod tests {
use super::*;
......@@ -159,13 +212,22 @@ mod tests {
#[test]
fn select_better_recent_finality_proof_works() {
let info = SyncInfo {
best_number_at_source: 10,
best_number_at_target: 5,
is_using_same_fork: true,
};
// if there are no unjustified headers, nothing is changed
let finality_proofs_buf =
FinalityProofsBuf::<TestFinalitySyncPipeline>::new(vec![TestFinalityProof(5)]);
let justified_header =
JustifiedHeader { header: TestSourceHeader(false, 2, 2), proof: TestFinalityProof(2) };
let selector = JustifiedHeaderSelector::Regular(vec![], justified_header.clone());
assert_eq!(selector.select(&finality_proofs_buf), Some(justified_header));
assert_eq!(
selector.select(&info, HeadersToRelay::All, None, &finality_proofs_buf),
Some(justified_header)
);
// if there are no buffered finality proofs, nothing is changed
let finality_proofs_buf = FinalityProofsBuf::<TestFinalitySyncPipeline>::new(vec![]);
......@@ -175,7 +237,10 @@ mod tests {
vec![TestSourceHeader(false, 5, 5)],
justified_header.clone(),
);
assert_eq!(selector.select(&finality_proofs_buf), Some(justified_header));
assert_eq!(
selector.select(&info, HeadersToRelay::All, None, &finality_proofs_buf),
Some(justified_header)
);
// if there's no intersection between recent finality proofs and unjustified headers,
// nothing is changed
......@@ -189,7 +254,10 @@ mod tests {
vec![TestSourceHeader(false, 9, 9), TestSourceHeader(false, 10, 10)],
justified_header.clone(),
);
assert_eq!(selector.select(&finality_proofs_buf), Some(justified_header));
assert_eq!(
selector.select(&info, HeadersToRelay::All, None, &finality_proofs_buf),
Some(justified_header)
);
// if there's intersection between recent finality proofs and unjustified headers, but there
// are no proofs in this intersection, nothing is changed
......@@ -207,7 +275,10 @@ mod tests {
],
justified_header.clone(),
);
assert_eq!(selector.select(&finality_proofs_buf), Some(justified_header));
assert_eq!(
selector.select(&info, HeadersToRelay::All, None, &finality_proofs_buf),
Some(justified_header)
);
// if there's intersection between recent finality proofs and unjustified headers and
// there's a proof in this intersection:
......@@ -228,11 +299,63 @@ mod tests {
justified_header,
);
assert_eq!(
selector.select(&finality_proofs_buf),
selector.select(&info, HeadersToRelay::All, None, &finality_proofs_buf),
Some(JustifiedHeader {
header: TestSourceHeader(false, 9, 9),
proof: TestFinalityProof(9)
})
);
// when only free headers needs to be relayed and there are no free headers
let finality_proofs_buf = FinalityProofsBuf::<TestFinalitySyncPipeline>::new(vec![
TestFinalityProof(7),
TestFinalityProof(9),
]);
let selector = JustifiedHeaderSelector::None(vec![
TestSourceHeader(false, 8, 8),
TestSourceHeader(false, 9, 9),
TestSourceHeader(false, 10, 10),
]);
assert_eq!(
selector.select(&info, HeadersToRelay::Free, Some(7), &finality_proofs_buf),
None,
);
// when only free headers needs to be relayed, mandatory header may be selected
let finality_proofs_buf = FinalityProofsBuf::<TestFinalitySyncPipeline>::new(vec![
TestFinalityProof(6),
TestFinalityProof(9),
]);
let selector = JustifiedHeaderSelector::None(vec![
TestSourceHeader(false, 8, 8),
TestSourceHeader(true, 9, 9),
TestSourceHeader(false, 10, 10),
]);
assert_eq!(
selector.select(&info, HeadersToRelay::Free, Some(7), &finality_proofs_buf),
Some(JustifiedHeader {
header: TestSourceHeader(true, 9, 9),
proof: TestFinalityProof(9)
})
);
// when only free headers needs to be relayed and there is free header
let finality_proofs_buf = FinalityProofsBuf::<TestFinalitySyncPipeline>::new(vec![
TestFinalityProof(7),
TestFinalityProof(9),
TestFinalityProof(14),
]);
let selector = JustifiedHeaderSelector::None(vec![
TestSourceHeader(false, 7, 7),
TestSourceHeader(false, 10, 10),
TestSourceHeader(false, 14, 14),
]);
assert_eq!(
selector.select(&info, HeadersToRelay::Free, Some(7), &finality_proofs_buf),
Some(JustifiedHeader {
header: TestSourceHeader(false, 14, 14),
proof: TestFinalityProof(14)
})
);
}
}
......@@ -21,7 +21,9 @@
pub use crate::{
base::{FinalityPipeline, SourceClientBase},
finality_loop::{metrics_prefix, run, FinalitySyncParams, SourceClient, TargetClient},
finality_loop::{
metrics_prefix, run, FinalitySyncParams, HeadersToRelay, SourceClient, TargetClient,
},
finality_proofs::{FinalityProofsBuf, FinalityProofsStream},
sync_loop_metrics::SyncLoopMetrics,
};
......
......@@ -198,10 +198,15 @@ impl TargetClient<TestFinalitySyncPipeline> for TestTargetClient {
Ok(data.target_best_block_id)
}
async fn free_source_headers_interval(&self) -> Result<Option<TestNumber>, TestError> {
Ok(Some(3))
}
async fn submit_finality_proof(
&self,
header: TestSourceHeader,
proof: TestFinalityProof,
_is_free_execution_expected: bool,
) -> Result<TestTransactionTracker, TestError> {
let mut data = self.data.lock();
(self.on_method_call)(&mut data);
......