referrerpolicy=no-referrer-when-downgrade

pallet_bridge_parachains/
call_ext.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
17use crate::{Config, GrandpaPalletOf, Pallet, RelayBlockNumber};
18use bp_header_chain::HeaderChain;
19use bp_parachains::{BestParaHeadHash, SubmitParachainHeadsInfo};
20use bp_runtime::{HeaderId, OwnedBridgeModule};
21use frame_support::{
22	dispatch::CallableCallFor,
23	traits::{Get, IsSubType},
24};
25use pallet_bridge_grandpa::SubmitFinalityProofHelper;
26use sp_runtime::{
27	traits::Zero,
28	transaction_validity::{InvalidTransaction, TransactionValidityError},
29	RuntimeDebug,
30};
31
32/// Verified `SubmitParachainHeadsInfo`.
33#[derive(PartialEq, RuntimeDebug)]
34pub struct VerifiedSubmitParachainHeadsInfo {
35	/// Base call information.
36	pub base: SubmitParachainHeadsInfo,
37	/// A difference between bundled bridged relay chain header and relay chain header number
38	/// used to prove best bridged parachain header, known to us before the call.
39	pub improved_by: RelayBlockNumber,
40}
41
42/// Helper struct that provides methods for working with the `SubmitParachainHeads` call.
43pub struct SubmitParachainHeadsHelper<T: Config<I>, I: 'static> {
44	_phantom_data: sp_std::marker::PhantomData<(T, I)>,
45}
46
47impl<T: Config<I>, I: 'static> SubmitParachainHeadsHelper<T, I> {
48	/// Check that is called from signed extension and takes the `is_free_execution_expected`
49	/// into account.
50	pub fn check_obsolete_from_extension(
51		update: &SubmitParachainHeadsInfo,
52	) -> Result<RelayBlockNumber, TransactionValidityError> {
53		// first do all base checks
54		let improved_by = Self::check_obsolete(update)?;
55
56		// if we don't expect free execution - no more checks
57		if !update.is_free_execution_expected {
58			return Ok(improved_by);
59		}
60
61		// reject if no more free slots remaining in the block
62		if !SubmitFinalityProofHelper::<T, T::BridgesGrandpaPalletInstance>::has_free_header_slots()
63		{
64			tracing::trace!(
65				target: crate::LOG_TARGET,
66				para_id=?update.para_id,
67				"The free parachain head can't be updated: no more free slots left in the block."
68			);
69
70			return Err(InvalidTransaction::Call.into());
71		}
72
73		// if free headers interval is not configured and call is expected to execute
74		// for free => it is a relayer error, it should've been able to detect that.
75		let free_headers_interval = match T::FreeHeadersInterval::get() {
76			Some(free_headers_interval) => free_headers_interval,
77			None => return Ok(improved_by),
78		};
79
80		// reject if we are importing parachain headers too often
81		if improved_by < free_headers_interval {
82			tracing::trace!(
83				target: crate::LOG_TARGET,
84				para_id=?update.para_id,
85				%improved_by,
86				"The free parachain head can't be updated: it improves previous
87				best head while at least {free_headers_interval} is expected."
88			);
89
90			return Err(InvalidTransaction::Stale.into());
91		}
92
93		Ok(improved_by)
94	}
95
96	/// Check if the para head provided by the `SubmitParachainHeads` is better than the best one
97	/// we know.
98	pub fn check_obsolete(
99		update: &SubmitParachainHeadsInfo,
100	) -> Result<RelayBlockNumber, TransactionValidityError> {
101		// check if we know better parachain head already
102		let improved_by = match crate::ParasInfo::<T, I>::get(update.para_id) {
103			Some(stored_best_head) => {
104				let improved_by = match update
105					.at_relay_block
106					.0
107					.checked_sub(stored_best_head.best_head_hash.at_relay_block_number)
108				{
109					Some(improved_by) if improved_by > Zero::zero() => improved_by,
110					_ => {
111						tracing::trace!(
112							target: crate::LOG_TARGET,
113							para_id=?update.para_id,
114							"The parachain head can't be updated. The parachain head \
115								was already updated at better relay chain block {} >= {}.",
116							stored_best_head.best_head_hash.at_relay_block_number,
117							update.at_relay_block.0
118						);
119						return Err(InvalidTransaction::Stale.into())
120					},
121				};
122
123				if stored_best_head.best_head_hash.head_hash == update.para_head_hash {
124					tracing::trace!(
125						target: crate::LOG_TARGET,
126						para_id=?update.para_id,
127						para_head_hash=%update.para_head_hash,
128						"The parachain head can't be updated. The parachain head hash \
129						was already updated at block {} < {}.",
130						stored_best_head.best_head_hash.at_relay_block_number,
131						update.at_relay_block.0
132					);
133					return Err(InvalidTransaction::Stale.into())
134				}
135
136				improved_by
137			},
138			None => RelayBlockNumber::MAX,
139		};
140
141		// let's check if our chain had no reorgs and we still know the relay chain header
142		// used to craft the proof
143		if GrandpaPalletOf::<T, I>::finalized_header_state_root(update.at_relay_block.1).is_none() {
144			tracing::trace!(
145				target: crate::LOG_TARGET,
146				para_id=?update.para_id,
147				at_relay_block=?update.at_relay_block,
148				"The parachain head can't be updated. Relay chain header used to create \
149				parachain proof is missing from the storage."
150			);
151
152			return Err(InvalidTransaction::Call.into())
153		}
154
155		Ok(improved_by)
156	}
157
158	/// Check if the `SubmitParachainHeads` was successfully executed.
159	pub fn was_successful(update: &SubmitParachainHeadsInfo) -> bool {
160		match crate::ParasInfo::<T, I>::get(update.para_id) {
161			Some(stored_best_head) =>
162				stored_best_head.best_head_hash ==
163					BestParaHeadHash {
164						at_relay_block_number: update.at_relay_block.0,
165						head_hash: update.para_head_hash,
166					},
167			None => false,
168		}
169	}
170}
171
172/// Trait representing a call that is a sub type of this pallet's call.
173pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
174	IsSubType<CallableCallFor<Pallet<T, I>, T>>
175{
176	/// Create a new instance of `SubmitParachainHeadsInfo` from a `SubmitParachainHeads` call with
177	/// one single parachain entry.
178	fn one_entry_submit_parachain_heads_info(&self) -> Option<SubmitParachainHeadsInfo> {
179		match self.is_sub_type() {
180			Some(crate::Call::<T, I>::submit_parachain_heads {
181				ref at_relay_block,
182				ref parachains,
183				..
184			}) => match &parachains[..] {
185				&[(para_id, para_head_hash)] => Some(SubmitParachainHeadsInfo {
186					at_relay_block: HeaderId(at_relay_block.0, at_relay_block.1),
187					para_id,
188					para_head_hash,
189					is_free_execution_expected: false,
190				}),
191				_ => None,
192			},
193			Some(crate::Call::<T, I>::submit_parachain_heads_ex {
194				ref at_relay_block,
195				ref parachains,
196				is_free_execution_expected,
197				..
198			}) => match &parachains[..] {
199				&[(para_id, para_head_hash)] => Some(SubmitParachainHeadsInfo {
200					at_relay_block: HeaderId(at_relay_block.0, at_relay_block.1),
201					para_id,
202					para_head_hash,
203					is_free_execution_expected: *is_free_execution_expected,
204				}),
205				_ => None,
206			},
207			_ => None,
208		}
209	}
210
211	/// Create a new instance of `SubmitParachainHeadsInfo` from a `SubmitParachainHeads` call with
212	/// one single parachain entry, if the entry is for the provided parachain id.
213	fn submit_parachain_heads_info_for(&self, para_id: u32) -> Option<SubmitParachainHeadsInfo> {
214		self.one_entry_submit_parachain_heads_info()
215			.filter(|update| update.para_id.0 == para_id)
216	}
217
218	/// Validate parachain heads in order to avoid "mining" transactions that provide
219	/// outdated bridged parachain heads. Without this validation, even honest relayers
220	/// may lose their funds if there are multiple relays running and submitting the
221	/// same information.
222	///
223	/// This validation only works with transactions that are updating single parachain
224	/// head. We can't use unbounded validation - it may take too long and either break
225	/// block production, or "eat" significant portion of block production time literally
226	/// for nothing. In addition, the single-parachain-head-per-transaction is how the
227	/// pallet will be used in our environment.
228	fn check_obsolete_submit_parachain_heads(
229		&self,
230	) -> Result<Option<VerifiedSubmitParachainHeadsInfo>, TransactionValidityError>
231	where
232		Self: Sized,
233	{
234		let update = match self.one_entry_submit_parachain_heads_info() {
235			Some(update) => update,
236			None => return Ok(None),
237		};
238
239		if Pallet::<T, I>::ensure_not_halted().is_err() {
240			return Err(InvalidTransaction::Call.into())
241		}
242
243		SubmitParachainHeadsHelper::<T, I>::check_obsolete_from_extension(&update)
244			.map(|improved_by| Some(VerifiedSubmitParachainHeadsInfo { base: update, improved_by }))
245	}
246}
247
248impl<T, I: 'static> CallSubType<T, I> for T::RuntimeCall
249where
250	T: Config<I>,
251	T::RuntimeCall: IsSubType<CallableCallFor<Pallet<T, I>, T>>,
252{
253}
254
255#[cfg(test)]
256mod tests {
257	use crate::{
258		mock::{run_test, FreeHeadersInterval, RuntimeCall, TestRuntime},
259		CallSubType, PalletOperatingMode, ParaInfo, ParasInfo, RelayBlockHash, RelayBlockNumber,
260	};
261	use bp_header_chain::StoredHeaderData;
262	use bp_parachains::BestParaHeadHash;
263	use bp_polkadot_core::parachains::{ParaHash, ParaHeadsProof, ParaId};
264	use bp_runtime::BasicOperatingMode;
265
266	fn validate_submit_parachain_heads(
267		num: RelayBlockNumber,
268		parachains: Vec<(ParaId, ParaHash)>,
269	) -> bool {
270		RuntimeCall::Parachains(crate::Call::<TestRuntime, ()>::submit_parachain_heads_ex {
271			at_relay_block: (num, [num as u8; 32].into()),
272			parachains,
273			parachain_heads_proof: ParaHeadsProof { storage_proof: Default::default() },
274			is_free_execution_expected: false,
275		})
276		.check_obsolete_submit_parachain_heads()
277		.is_ok()
278	}
279
280	fn validate_free_submit_parachain_heads(
281		num: RelayBlockNumber,
282		parachains: Vec<(ParaId, ParaHash)>,
283	) -> bool {
284		RuntimeCall::Parachains(crate::Call::<TestRuntime, ()>::submit_parachain_heads_ex {
285			at_relay_block: (num, [num as u8; 32].into()),
286			parachains,
287			parachain_heads_proof: ParaHeadsProof { storage_proof: Default::default() },
288			is_free_execution_expected: true,
289		})
290		.check_obsolete_submit_parachain_heads()
291		.is_ok()
292	}
293
294	fn insert_relay_block(num: RelayBlockNumber) {
295		pallet_bridge_grandpa::ImportedHeaders::<TestRuntime, crate::Instance1>::insert(
296			RelayBlockHash::from([num as u8; 32]),
297			StoredHeaderData { number: num, state_root: RelayBlockHash::from([10u8; 32]) },
298		);
299	}
300
301	fn sync_to_relay_header_10() {
302		ParasInfo::<TestRuntime, ()>::insert(
303			ParaId(1),
304			ParaInfo {
305				best_head_hash: BestParaHeadHash {
306					at_relay_block_number: 10,
307					head_hash: [1u8; 32].into(),
308				},
309				next_imported_hash_position: 0,
310			},
311		);
312	}
313
314	#[test]
315	fn extension_rejects_header_from_the_obsolete_relay_block() {
316		run_test(|| {
317			// when current best finalized is #10 and we're trying to import header#5 => tx is
318			// rejected
319			sync_to_relay_header_10();
320			assert!(!validate_submit_parachain_heads(5, vec![(ParaId(1), [1u8; 32].into())]));
321		});
322	}
323
324	#[test]
325	fn extension_rejects_header_from_the_same_relay_block() {
326		run_test(|| {
327			// when current best finalized is #10 and we're trying to import header#10 => tx is
328			// rejected
329			sync_to_relay_header_10();
330			assert!(!validate_submit_parachain_heads(10, vec![(ParaId(1), [1u8; 32].into())]));
331		});
332	}
333
334	#[test]
335	fn extension_rejects_header_from_new_relay_block_with_same_hash() {
336		run_test(|| {
337			// when current best finalized is #10 and we're trying to import header#10 => tx is
338			// rejected
339			sync_to_relay_header_10();
340			assert!(!validate_submit_parachain_heads(20, vec![(ParaId(1), [1u8; 32].into())]));
341		});
342	}
343
344	#[test]
345	fn extension_rejects_header_if_pallet_is_halted() {
346		run_test(|| {
347			// when pallet is halted => tx is rejected
348			sync_to_relay_header_10();
349			PalletOperatingMode::<TestRuntime, ()>::put(BasicOperatingMode::Halted);
350
351			assert!(!validate_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
352		});
353	}
354
355	#[test]
356	fn extension_accepts_new_header() {
357		run_test(|| {
358			// when current best finalized is #10 and we're trying to import header#15 => tx is
359			// accepted
360			sync_to_relay_header_10();
361			insert_relay_block(15);
362			assert!(validate_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
363		});
364	}
365
366	#[test]
367	fn extension_accepts_if_more_than_one_parachain_is_submitted() {
368		run_test(|| {
369			// when current best finalized is #10 and we're trying to import header#5, but another
370			// parachain head is also supplied => tx is accepted
371			sync_to_relay_header_10();
372			assert!(validate_submit_parachain_heads(
373				5,
374				vec![(ParaId(1), [1u8; 32].into()), (ParaId(2), [1u8; 32].into())]
375			));
376		});
377	}
378
379	#[test]
380	fn extension_rejects_initial_parachain_head_if_missing_relay_chain_header() {
381		run_test(|| {
382			// when relay chain header is unknown => "obsolete"
383			assert!(!validate_submit_parachain_heads(10, vec![(ParaId(1), [1u8; 32].into())]));
384			// when relay chain header is unknown => "ok"
385			insert_relay_block(10);
386			assert!(validate_submit_parachain_heads(10, vec![(ParaId(1), [1u8; 32].into())]));
387		});
388	}
389
390	#[test]
391	fn extension_rejects_free_parachain_head_if_missing_relay_chain_header() {
392		run_test(|| {
393			sync_to_relay_header_10();
394			// when relay chain header is unknown => "obsolete"
395			assert!(!validate_submit_parachain_heads(15, vec![(ParaId(2), [15u8; 32].into())]));
396			// when relay chain header is unknown => "ok"
397			insert_relay_block(15);
398			assert!(validate_submit_parachain_heads(15, vec![(ParaId(2), [15u8; 32].into())]));
399		});
400	}
401
402	#[test]
403	fn extension_rejects_free_parachain_head_if_no_free_slots_remaining() {
404		run_test(|| {
405			// when current best finalized is #10 and we're trying to import header#15 => tx should
406			// be accepted
407			sync_to_relay_header_10();
408			insert_relay_block(15);
409			// ... but since we have specified `is_free_execution_expected = true`, it'll be
410			// rejected
411			assert!(!validate_free_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
412			// ... if we have specify `is_free_execution_expected = false`, it'll be accepted
413			assert!(validate_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
414		});
415	}
416
417	#[test]
418	fn extension_rejects_free_parachain_head_if_improves_by_is_below_expected() {
419		run_test(|| {
420			// when current best finalized is #10 and we're trying to import header#15 => tx should
421			// be accepted
422			sync_to_relay_header_10();
423			insert_relay_block(10 + FreeHeadersInterval::get() - 1);
424			insert_relay_block(10 + FreeHeadersInterval::get());
425			// try to submit at 10 + FreeHeadersInterval::get() - 1 => failure
426			let relay_header = 10 + FreeHeadersInterval::get() - 1;
427			assert!(!validate_free_submit_parachain_heads(
428				relay_header,
429				vec![(ParaId(1), [2u8; 32].into())]
430			));
431			// try to submit at 10 + FreeHeadersInterval::get() => ok
432			let relay_header = 10 + FreeHeadersInterval::get();
433			assert!(validate_free_submit_parachain_heads(
434				relay_header,
435				vec![(ParaId(1), [2u8; 32].into())]
436			));
437		});
438	}
439}