diff --git a/bin/runtime-common/src/mock.rs b/bin/runtime-common/src/mock.rs index 7b410e2670..dec5ae9738 100644 --- a/bin/runtime-common/src/mock.rs +++ b/bin/runtime-common/src/mock.rs @@ -192,6 +192,7 @@ impl pallet_bridge_parachains::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; type BridgesGrandpaPalletInstance = (); type ParasPalletName = BridgedParasPalletName; + type FreeHeadsUpdateFilter = (); type ParaStoredHeaderDataBuilder = SingleParaStoredHeaderDataBuilder; type HeadsToKeep = ConstU32<8>; diff --git a/modules/parachains/src/lib.rs b/modules/parachains/src/lib.rs index 3c778ddccb..9afd03fde8 100644 --- a/modules/parachains/src/lib.rs +++ b/modules/parachains/src/lib.rs @@ -65,6 +65,24 @@ pub type RelayBlockNumber = bp_polkadot_core::BlockNumber; /// Hasher of the bridged relay chain. pub type RelayBlockHasher = bp_polkadot_core::Hasher; +/// A filter of parachain head updates. +pub trait ParachainHeadsUpdateFilter { + /// Returns true if the update passes the filter. + fn is_free( + at_relay_block: (RelayBlockNumber, RelayBlockHash), + parachains: &[(ParaId, ParaHash)], + ) -> bool; +} + +impl ParachainHeadsUpdateFilter for () { + fn is_free( + _at_relay_block: (RelayBlockNumber, RelayBlockHash), + _parachains: &[(ParaId, ParaHash)], + ) -> bool { + false + } +} + /// Artifacts of the parachains head update. struct UpdateParachainHeadArtifacts { /// New best head of the parachain. @@ -194,6 +212,20 @@ pub mod pallet { /// we're interested in. type BridgesGrandpaPalletInstance: 'static; + /// A way to make `submit_parachain_heads` free for the submitter. If update passes + /// this filter AND at least one parachain head has been updated in the call, the + /// submission will be free for the submitter. + /// + /// It can be used to reduce bridge fees by deducting parachain finality submission cost + /// from the total fee. Instead, relayers may submit some headers for free, allowing + /// queued messages (and confirmations) to be proved without providing additional + /// finality proofs. + /// + /// **IMPORTANT**: we are NOT limiting number of free calls per block. The filter must + /// take that into account or anyone could fill the block with parachain head updates + /// for free. + type FreeHeadsUpdateFilter: ParachainHeadsUpdateFilter; + /// Name of the original `paras` pallet in the `construct_runtime!()` call at the bridged /// chain. /// @@ -339,6 +371,9 @@ pub mod pallet { Self::ensure_not_halted().map_err(Error::::BridgeModule)?; ensure_signed(origin)?; + // check whether this submission may be refunded + let may_be_free = T::FreeHeadsUpdateFilter::is_free(at_relay_block, ¶chains); + // we'll need relay chain header to verify that parachains heads are always increasing. let (relay_block_number, relay_block_hash) = at_relay_block; let relay_block = pallet_bridge_grandpa::ImportedHeaders::< @@ -358,6 +393,7 @@ pub mod pallet { parachains.len() as _, ); + let mut is_updated_something = false; let mut storage = GrandpaPalletOf::::storage_proof_checker( relay_block_hash, parachain_heads_proof.storage_proof, @@ -437,6 +473,7 @@ pub mod pallet { parachain_head_data, parachain_head_hash, )?; + is_updated_something = true; *stored_best_head = Some(artifacts.best_head); Ok(artifacts.prune_happened) }); @@ -467,7 +504,11 @@ pub mod pallet { Error::::HeaderChainStorageProof(HeaderChainError::StorageProof(e)) })?; - Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee: Pays::Yes }) + // we allow free submissions only if the update passes filter and something + // has been updated + let pays_fee = if is_updated_something && may_be_free { Pays::No } else { Pays::Yes }; + + Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee }) } /// Change `PalletOwner`. @@ -736,6 +777,7 @@ pub(crate) mod tests { use frame_support::{ assert_noop, assert_ok, dispatch::DispatchResultWithPostInfo, + pallet_prelude::Pays, storage::generator::{StorageDoubleMap, StorageMap}, traits::{Get, OnInitialize}, weights::Weight, @@ -990,7 +1032,8 @@ pub(crate) mod tests { run_test(|| { // start with relay block #0 and import head#5 of parachain#1 initialize(state_root_5); - assert_ok!(import_parachain_1_head(0, state_root_5, parachains_5, proof_5)); + let result = import_parachain_1_head(0, state_root_5, parachains_5, proof_5); + assert_eq!(result.unwrap().pays_fee, Pays::Yes); assert_eq!( ParasInfo::::get(ParaId(1)), Some(ParaInfo { @@ -1648,4 +1691,30 @@ pub(crate) mod tests { ); }) } + + #[test] + fn may_be_free_for_submitter() { + run_test(|| { + let (state_root, proof, parachains) = + prepare_parachain_heads_proof::(vec![(2, head_data(2, 5))]); + // start with relay block #0 and import head#5 of parachain#2 + initialize(state_root); + // first submission is free + let result = Pallet::::submit_parachain_heads( + RuntimeOrigin::signed(1), + (0, test_relay_header(0, state_root).hash()), + parachains.clone(), + proof.clone(), + ); + assert_eq!(result.unwrap().pays_fee, Pays::No); + // next submission is NOT free, because we haven't updated anything + let result = Pallet::::submit_parachain_heads( + RuntimeOrigin::signed(1), + (0, test_relay_header(0, state_root).hash()), + parachains, + proof, + ); + assert_eq!(result.unwrap().pays_fee, Pays::Yes); + }) + } } diff --git a/modules/parachains/src/mock.rs b/modules/parachains/src/mock.rs index 9eccc9ecff..1a9d22688a 100644 --- a/modules/parachains/src/mock.rs +++ b/modules/parachains/src/mock.rs @@ -27,6 +27,7 @@ use sp_runtime::{ }; use crate as pallet_bridge_parachains; +use crate::{ParaHash, ParachainHeadsUpdateFilter, RelayBlockHash, RelayBlockNumber}; pub type AccountId = u64; @@ -198,12 +199,24 @@ impl pallet_bridge_parachains::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; type WeightInfo = (); type BridgesGrandpaPalletInstance = pallet_bridge_grandpa::Instance1; + type FreeHeadsUpdateFilter = FreeForParachain2; type ParasPalletName = ParasPalletName; type ParaStoredHeaderDataBuilder = (Parachain1, Parachain2, Parachain3, BigParachain); type HeadsToKeep = HeadsToKeep; type MaxParaHeadDataSize = ConstU32; } +pub struct FreeForParachain2; + +impl ParachainHeadsUpdateFilter for FreeForParachain2 { + fn is_free( + _at_relay_block: (RelayBlockNumber, RelayBlockHash), + parachains: &[(ParaId, ParaHash)], + ) -> bool { + parachains.len() == 1 && parachains[0].0 .0 == Parachain2::PARACHAIN_ID + } +} + #[cfg(feature = "runtime-benchmarks")] impl pallet_bridge_parachains::benchmarking::Config<()> for TestRuntime { fn parachains() -> Vec {