referrerpolicy=no-referrer-when-downgrade

pallet_bridge_relayers/extension/
priority.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Parity Bridges Common.
3
4// Parity Bridges Common is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Parity Bridges Common is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Bridge transaction priority calculator.
18//!
19//! We want to prioritize message delivery transactions with more messages over
20//! transactions with less messages. That's because we reject delivery transactions
21//! if it contains already delivered message. And if some transaction delivers
22//! single message with nonce `N`, then the transaction with nonces `N..=N+100` will
23//! be rejected. This can lower bridge throughput down to one message per block.
24
25use frame_support::traits::Get;
26use sp_runtime::transaction_validity::TransactionPriority;
27
28// reexport everything from `integrity_tests` module
29#[allow(unused_imports)]
30pub use integrity_tests::*;
31
32/// We'll deal with different bridge items here - messages, headers, ...
33/// To avoid being too verbose with generic code, let's just define a separate alias.
34pub type ItemCount = u64;
35
36/// Compute priority boost for transaction that brings given number of bridge
37/// items (messages, headers, ...), when every additional item adds `PriorityBoostPerItem`
38/// to transaction priority.
39pub fn compute_priority_boost<PriorityBoostPerItem>(n_items: ItemCount) -> TransactionPriority
40where
41	PriorityBoostPerItem: Get<TransactionPriority>,
42{
43	// we don't want any boost for transaction with single (additional) item => minus one
44	PriorityBoostPerItem::get().saturating_mul(n_items.saturating_sub(1))
45}
46
47#[cfg(not(feature = "integrity-test"))]
48mod integrity_tests {}
49
50#[cfg(feature = "integrity-test")]
51mod integrity_tests {
52	use super::{compute_priority_boost, ItemCount};
53
54	use bp_messages::MessageNonce;
55	use bp_runtime::PreComputedSize;
56	use frame_support::{
57		dispatch::{DispatchClass, DispatchInfo, Pays, PostDispatchInfo},
58		traits::Get,
59	};
60	use pallet_transaction_payment::OnChargeTransaction;
61	use sp_runtime::{
62		traits::{Dispatchable, UniqueSaturatedInto, Zero},
63		transaction_validity::TransactionPriority,
64		FixedPointOperand, SaturatedConversion, Saturating,
65	};
66
67	type BalanceOf<T> =
68		<<T as pallet_transaction_payment::Config>::OnChargeTransaction as OnChargeTransaction<
69			T,
70		>>::Balance;
71
72	/// Ensures that the value of `PriorityBoostPerItem` matches the value of
73	/// `tip_boost_per_item`.
74	///
75	/// We want two transactions, `TX1` with `N` items and `TX2` with `N+1` items, have almost
76	/// the same priority if we'll add `tip_boost_per_item` tip to the `TX1`. We want to be sure
77	/// that if we add plain `PriorityBoostPerItem` priority to `TX1`, the priority will be close
78	/// to `TX2` as well.
79	fn ensure_priority_boost_is_sane<PriorityBoostPerItem, Balance>(
80		param_name: &str,
81		max_items: ItemCount,
82		tip_boost_per_item: Balance,
83		estimate_priority: impl Fn(ItemCount, Balance) -> TransactionPriority,
84	) where
85		PriorityBoostPerItem: Get<TransactionPriority>,
86		ItemCount: UniqueSaturatedInto<Balance>,
87		Balance: FixedPointOperand + Zero,
88	{
89		let priority_boost_per_item = PriorityBoostPerItem::get();
90		for n_items in 1..=max_items {
91			let base_priority = estimate_priority(n_items, Zero::zero());
92			let priority_boost = compute_priority_boost::<PriorityBoostPerItem>(n_items);
93			let priority_with_boost = base_priority
94				.checked_add(priority_boost)
95				.expect("priority overflow: try lowering `max_items` or `tip_boost_per_item`?");
96
97			let tip = tip_boost_per_item.saturating_mul((n_items - 1).unique_saturated_into());
98			let priority_with_tip = estimate_priority(1, tip);
99
100			const ERROR_MARGIN: TransactionPriority = 5; // 5%
101			if priority_with_boost.abs_diff(priority_with_tip).saturating_mul(100) /
102				priority_with_tip >
103				ERROR_MARGIN
104			{
105				panic!(
106					"The {param_name} value ({}) must be fixed to: {}",
107					priority_boost_per_item,
108					compute_priority_boost_per_item(
109						max_items,
110						tip_boost_per_item,
111						estimate_priority
112					),
113				);
114			}
115		}
116	}
117
118	/// Compute priority boost that we give to bridge transaction for every
119	/// additional bridge item.
120	#[cfg(feature = "integrity-test")]
121	fn compute_priority_boost_per_item<Balance>(
122		max_items: ItemCount,
123		tip_boost_per_item: Balance,
124		estimate_priority: impl Fn(ItemCount, Balance) -> TransactionPriority,
125	) -> TransactionPriority
126	where
127		ItemCount: UniqueSaturatedInto<Balance>,
128		Balance: FixedPointOperand + Zero,
129	{
130		// estimate priority of transaction that delivers one item and has large tip
131		let small_with_tip_priority =
132			estimate_priority(1, tip_boost_per_item.saturating_mul(max_items.saturated_into()));
133		// estimate priority of transaction that delivers maximal number of items, but has no tip
134		let large_without_tip_priority = estimate_priority(max_items, Zero::zero());
135
136		small_with_tip_priority
137			.saturating_sub(large_without_tip_priority)
138			.saturating_div(max_items - 1)
139	}
140
141	/// Computations, specific to bridge relay chains transactions.
142	pub mod per_relay_header {
143		use super::*;
144
145		use bp_header_chain::{
146			max_expected_submit_finality_proof_arguments_size, ChainWithGrandpa,
147		};
148		use pallet_bridge_grandpa::WeightInfoExt;
149
150		/// Ensures that the value of `PriorityBoostPerHeader` matches the value of
151		/// `tip_boost_per_header`.
152		///
153		/// We want two transactions, `TX1` with `N` headers and `TX2` with `N+1` headers, have
154		/// almost the same priority if we'll add `tip_boost_per_header` tip to the `TX1`. We want
155		/// to be sure that if we add plain `PriorityBoostPerHeader` priority to `TX1`, the priority
156		/// will be close to `TX2` as well.
157		pub fn ensure_priority_boost_is_sane<Runtime, GrandpaInstance, PriorityBoostPerHeader>(
158			tip_boost_per_header: BalanceOf<Runtime>,
159		) where
160			Runtime:
161				pallet_transaction_payment::Config + pallet_bridge_grandpa::Config<GrandpaInstance>,
162			GrandpaInstance: 'static,
163			PriorityBoostPerHeader: Get<TransactionPriority>,
164			Runtime::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
165			BalanceOf<Runtime>: Send + Sync + FixedPointOperand,
166		{
167			// the meaning of `max_items` here is different when comparing with message
168			// transactions - with messages we have a strict limit on maximal number of
169			// messages we can fit into a single transaction. With headers, current best
170			// header may be improved by any "number of items". But this number is only
171			// used to verify priority boost, so it should be fine to select this arbitrary
172			// value - it SHALL NOT affect any value, it just adds more tests for the value.
173			let maximal_improved_by = 4_096;
174			super::ensure_priority_boost_is_sane::<PriorityBoostPerHeader, BalanceOf<Runtime>>(
175				"PriorityBoostPerRelayHeader",
176				maximal_improved_by,
177				tip_boost_per_header,
178				|_n_headers, tip| {
179					estimate_relay_header_submit_transaction_priority::<Runtime, GrandpaInstance>(
180						tip,
181					)
182				},
183			);
184		}
185
186		/// Estimate relay header delivery transaction priority.
187		#[cfg(feature = "integrity-test")]
188		fn estimate_relay_header_submit_transaction_priority<Runtime, GrandpaInstance>(
189			tip: BalanceOf<Runtime>,
190		) -> TransactionPriority
191		where
192			Runtime:
193				pallet_transaction_payment::Config + pallet_bridge_grandpa::Config<GrandpaInstance>,
194			GrandpaInstance: 'static,
195			Runtime::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
196			BalanceOf<Runtime>: Send + Sync + FixedPointOperand,
197		{
198			// just an estimation of extra transaction bytes that are added to every transaction
199			// (including signature, signed extensions extra and etc + in our case it includes
200			// all call arguments except the proof itself)
201			let base_tx_size = 512;
202			// let's say we are relaying largest relay chain headers
203			let tx_call_size = max_expected_submit_finality_proof_arguments_size::<
204				Runtime::BridgedChain,
205			>(true, Runtime::BridgedChain::MAX_AUTHORITIES_COUNT * 2 / 3 + 1);
206
207			// finally we are able to estimate transaction size and weight
208			let transaction_size = base_tx_size.saturating_add(tx_call_size);
209			let transaction_weight = <Runtime as ::pallet_bridge_grandpa::Config<
210				GrandpaInstance,
211			>>::WeightInfo::submit_finality_proof_weight(
212				Runtime::BridgedChain::MAX_AUTHORITIES_COUNT * 2 / 3 + 1,
213				Runtime::BridgedChain::REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY,
214			);
215
216			pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::get_priority(
217				&DispatchInfo {
218					call_weight: transaction_weight,
219					extension_weight: Default::default(),
220					class: DispatchClass::Normal,
221					pays_fee: Pays::Yes,
222				},
223				transaction_size as _,
224				tip,
225				Zero::zero(),
226			)
227		}
228	}
229
230	/// Computations, specific to bridge parachains transactions.
231	pub mod per_parachain_header {
232		use super::*;
233
234		use bp_runtime::Parachain;
235		use pallet_bridge_parachains::WeightInfoExt;
236
237		/// Ensures that the value of `PriorityBoostPerHeader` matches the value of
238		/// `tip_boost_per_header`.
239		///
240		/// We want two transactions, `TX1` with `N` headers and `TX2` with `N+1` headers, have
241		/// almost the same priority if we'll add `tip_boost_per_header` tip to the `TX1`. We want
242		/// to be sure that if we add plain `PriorityBoostPerHeader` priority to `TX1`, the priority
243		/// will be close to `TX2` as well.
244		pub fn ensure_priority_boost_is_sane<
245			Runtime,
246			ParachainsInstance,
247			Para,
248			PriorityBoostPerHeader,
249		>(
250			tip_boost_per_header: BalanceOf<Runtime>,
251		) where
252			Runtime: pallet_transaction_payment::Config
253				+ pallet_bridge_parachains::Config<ParachainsInstance>,
254			ParachainsInstance: 'static,
255			Para: Parachain,
256			PriorityBoostPerHeader: Get<TransactionPriority>,
257			Runtime::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
258			BalanceOf<Runtime>: Send + Sync + FixedPointOperand,
259		{
260			// the meaning of `max_items` here is different when comparing with message
261			// transactions - with messages we have a strict limit on maximal number of
262			// messages we can fit into a single transaction. With headers, current best
263			// header may be improved by any "number of items". But this number is only
264			// used to verify priority boost, so it should be fine to select this arbitrary
265			// value - it SHALL NOT affect any value, it just adds more tests for the value.
266			let maximal_improved_by = 4_096;
267			super::ensure_priority_boost_is_sane::<PriorityBoostPerHeader, BalanceOf<Runtime>>(
268				"PriorityBoostPerParachainHeader",
269				maximal_improved_by,
270				tip_boost_per_header,
271				|_n_headers, tip| {
272					estimate_parachain_header_submit_transaction_priority::<
273						Runtime,
274						ParachainsInstance,
275						Para,
276					>(tip)
277				},
278			);
279		}
280
281		/// Estimate parachain header delivery transaction priority.
282		#[cfg(feature = "integrity-test")]
283		fn estimate_parachain_header_submit_transaction_priority<
284			Runtime,
285			ParachainsInstance,
286			Para,
287		>(
288			tip: BalanceOf<Runtime>,
289		) -> TransactionPriority
290		where
291			Runtime: pallet_transaction_payment::Config
292				+ pallet_bridge_parachains::Config<ParachainsInstance>,
293			ParachainsInstance: 'static,
294			Para: Parachain,
295			Runtime::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
296			BalanceOf<Runtime>: Send + Sync + FixedPointOperand,
297		{
298			// just an estimation of extra transaction bytes that are added to every transaction
299			// (including signature, signed extensions extra and etc + in our case it includes
300			// all call arguments except the proof itself)
301			let base_tx_size = 512;
302			// let's say we are relaying largest parachain headers and proof takes some more bytes
303			let tx_call_size = <Runtime as pallet_bridge_parachains::Config<
304				ParachainsInstance,
305			>>::WeightInfo::expected_extra_storage_proof_size()
306			.saturating_add(Para::MAX_HEADER_SIZE);
307
308			// finally we are able to estimate transaction size and weight
309			let transaction_size = base_tx_size.saturating_add(tx_call_size);
310			let transaction_weight = <Runtime as pallet_bridge_parachains::Config<
311				ParachainsInstance,
312			>>::WeightInfo::submit_parachain_heads_weight(
313				Runtime::DbWeight::get(),
314				&PreComputedSize(transaction_size as _),
315				// just one parachain - all other submissions won't receive any boost
316				1,
317			);
318
319			pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::get_priority(
320				&DispatchInfo {
321					call_weight: transaction_weight,
322					extension_weight: Default::default(),
323					class: DispatchClass::Normal,
324					pays_fee: Pays::Yes,
325				},
326				transaction_size as _,
327				tip,
328				Zero::zero(),
329			)
330		}
331	}
332
333	/// Computations, specific to bridge messages transactions.
334	pub mod per_message {
335		use super::*;
336
337		use bp_messages::ChainWithMessages;
338		use pallet_bridge_messages::WeightInfoExt;
339
340		/// Ensures that the value of `PriorityBoostPerMessage` matches the value of
341		/// `tip_boost_per_message`.
342		///
343		/// We want two transactions, `TX1` with `N` messages and `TX2` with `N+1` messages, have
344		/// almost the same priority if we'll add `tip_boost_per_message` tip to the `TX1`. We want
345		/// to be sure that if we add plain `PriorityBoostPerMessage` priority to `TX1`, the
346		/// priority will be close to `TX2` as well.
347		pub fn ensure_priority_boost_is_sane<Runtime, MessagesInstance, PriorityBoostPerMessage>(
348			tip_boost_per_message: BalanceOf<Runtime>,
349		) where
350			Runtime: pallet_transaction_payment::Config
351				+ pallet_bridge_messages::Config<MessagesInstance>,
352			MessagesInstance: 'static,
353			PriorityBoostPerMessage: Get<TransactionPriority>,
354			Runtime::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
355			BalanceOf<Runtime>: Send + Sync + FixedPointOperand,
356		{
357			let maximal_messages_in_delivery_transaction =
358				Runtime::BridgedChain::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
359			super::ensure_priority_boost_is_sane::<PriorityBoostPerMessage, BalanceOf<Runtime>>(
360				"PriorityBoostPerMessage",
361				maximal_messages_in_delivery_transaction,
362				tip_boost_per_message,
363				|n_messages, tip| {
364					estimate_message_delivery_transaction_priority::<Runtime, MessagesInstance>(
365						n_messages, tip,
366					)
367				},
368			);
369		}
370
371		/// Estimate message delivery transaction priority.
372		#[cfg(feature = "integrity-test")]
373		fn estimate_message_delivery_transaction_priority<Runtime, MessagesInstance>(
374			messages: MessageNonce,
375			tip: BalanceOf<Runtime>,
376		) -> TransactionPriority
377		where
378			Runtime: pallet_transaction_payment::Config
379				+ pallet_bridge_messages::Config<MessagesInstance>,
380			MessagesInstance: 'static,
381			Runtime::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
382			BalanceOf<Runtime>: Send + Sync + FixedPointOperand,
383		{
384			// just an estimation of extra transaction bytes that are added to every transaction
385			// (including signature, signed extensions extra and etc + in our case it includes
386			// all call arguments except the proof itself)
387			let base_tx_size = 512;
388			// let's say we are relaying similar small messages and for every message we add more
389			// trie nodes to the proof (x0.5 because we expect some nodes to be reused)
390			let estimated_message_size = 512;
391			// let's say all our messages have the same dispatch weight
392			let estimated_message_dispatch_weight = <Runtime as pallet_bridge_messages::Config<
393				MessagesInstance,
394			>>::WeightInfo::message_dispatch_weight(
395				estimated_message_size
396			);
397			// messages proof argument size is (for every message) messages size + some additional
398			// trie nodes. Some of them are reused by different messages, so let's take 2/3 of
399			// default "overhead" constant
400			let messages_proof_size = <Runtime as pallet_bridge_messages::Config<
401				MessagesInstance,
402			>>::WeightInfo::expected_extra_storage_proof_size()
403			.saturating_mul(2)
404			.saturating_div(3)
405			.saturating_add(estimated_message_size)
406			.saturating_mul(messages as _);
407
408			// finally we are able to estimate transaction size and weight
409			let transaction_size = base_tx_size.saturating_add(messages_proof_size);
410			let transaction_weight = <Runtime as pallet_bridge_messages::Config<
411				MessagesInstance,
412			>>::WeightInfo::receive_messages_proof_weight(
413				&PreComputedSize(transaction_size as _),
414				messages as _,
415				estimated_message_dispatch_weight.saturating_mul(messages),
416			);
417
418			pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::get_priority(
419				&DispatchInfo {
420					call_weight: transaction_weight,
421					extension_weight: Default::default(),
422					class: DispatchClass::Normal,
423					pays_fee: Pays::Yes,
424				},
425				transaction_size as _,
426				tip,
427				Zero::zero(),
428			)
429		}
430	}
431}