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	Debug,
30};
31
32/// Verified `SubmitParachainHeadsInfo`.
33#[derive(PartialEq, Debug)]
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			},
168			None => false,
169		}
170	}
171}
172
173/// Trait representing a call that is a sub type of this pallet's call.
174pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
175	IsSubType<CallableCallFor<Pallet<T, I>, T>>
176{
177	/// Create a new instance of `SubmitParachainHeadsInfo` from a `SubmitParachainHeads` call with
178	/// one single parachain entry.
179	fn one_entry_submit_parachain_heads_info(&self) -> Option<SubmitParachainHeadsInfo> {
180		match self.is_sub_type() {
181			Some(crate::Call::<T, I>::submit_parachain_heads {
182				ref at_relay_block,
183				ref parachains,
184				..
185			}) => match &parachains[..] {
186				&[(para_id, para_head_hash)] => Some(SubmitParachainHeadsInfo {
187					at_relay_block: HeaderId(at_relay_block.0, at_relay_block.1),
188					para_id,
189					para_head_hash,
190					is_free_execution_expected: false,
191				}),
192				_ => None,
193			},
194			Some(crate::Call::<T, I>::submit_parachain_heads_ex {
195				ref at_relay_block,
196				ref parachains,
197				is_free_execution_expected,
198				..
199			}) => match &parachains[..] {
200				&[(para_id, para_head_hash)] => Some(SubmitParachainHeadsInfo {
201					at_relay_block: HeaderId(at_relay_block.0, at_relay_block.1),
202					para_id,
203					para_head_hash,
204					is_free_execution_expected: *is_free_execution_expected,
205				}),
206				_ => None,
207			},
208			_ => None,
209		}
210	}
211
212	/// Create a new instance of `SubmitParachainHeadsInfo` from a `SubmitParachainHeads` call with
213	/// one single parachain entry, if the entry is for the provided parachain id.
214	fn submit_parachain_heads_info_for(&self, para_id: u32) -> Option<SubmitParachainHeadsInfo> {
215		self.one_entry_submit_parachain_heads_info()
216			.filter(|update| update.para_id.0 == para_id)
217	}
218
219	/// Validate parachain heads in order to avoid "mining" transactions that provide
220	/// outdated bridged parachain heads. Without this validation, even honest relayers
221	/// may lose their funds if there are multiple relays running and submitting the
222	/// same information.
223	///
224	/// This validation only works with transactions that are updating single parachain
225	/// head. We can't use unbounded validation - it may take too long and either break
226	/// block production, or "eat" significant portion of block production time literally
227	/// for nothing. In addition, the single-parachain-head-per-transaction is how the
228	/// pallet will be used in our environment.
229	fn check_obsolete_submit_parachain_heads(
230		&self,
231	) -> Result<Option<VerifiedSubmitParachainHeadsInfo>, TransactionValidityError>
232	where
233		Self: Sized,
234	{
235		let update = match self.one_entry_submit_parachain_heads_info() {
236			Some(update) => update,
237			None => return Ok(None),
238		};
239
240		if Pallet::<T, I>::ensure_not_halted().is_err() {
241			return Err(InvalidTransaction::Call.into());
242		}
243
244		SubmitParachainHeadsHelper::<T, I>::check_obsolete_from_extension(&update)
245			.map(|improved_by| Some(VerifiedSubmitParachainHeadsInfo { base: update, improved_by }))
246	}
247}
248
249impl<T, I: 'static> CallSubType<T, I> for T::RuntimeCall
250where
251	T: Config<I>,
252	T::RuntimeCall: IsSubType<CallableCallFor<Pallet<T, I>, T>>,
253{
254}
255
256#[cfg(test)]
257mod tests {
258	use crate::{
259		mock::{run_test, FreeHeadersInterval, RuntimeCall, TestRuntime},
260		CallSubType, PalletOperatingMode, ParaInfo, ParasInfo, RelayBlockHash, RelayBlockNumber,
261	};
262	use bp_header_chain::StoredHeaderData;
263	use bp_parachains::BestParaHeadHash;
264	use bp_polkadot_core::parachains::{ParaHash, ParaHeadsProof, ParaId};
265	use bp_runtime::BasicOperatingMode;
266
267	fn validate_submit_parachain_heads(
268		num: RelayBlockNumber,
269		parachains: Vec<(ParaId, ParaHash)>,
270	) -> bool {
271		RuntimeCall::Parachains(crate::Call::<TestRuntime, ()>::submit_parachain_heads_ex {
272			at_relay_block: (num, [num as u8; 32].into()),
273			parachains,
274			parachain_heads_proof: ParaHeadsProof { storage_proof: Default::default() },
275			is_free_execution_expected: false,
276		})
277		.check_obsolete_submit_parachain_heads()
278		.is_ok()
279	}
280
281	fn validate_free_submit_parachain_heads(
282		num: RelayBlockNumber,
283		parachains: Vec<(ParaId, ParaHash)>,
284	) -> bool {
285		RuntimeCall::Parachains(crate::Call::<TestRuntime, ()>::submit_parachain_heads_ex {
286			at_relay_block: (num, [num as u8; 32].into()),
287			parachains,
288			parachain_heads_proof: ParaHeadsProof { storage_proof: Default::default() },
289			is_free_execution_expected: true,
290		})
291		.check_obsolete_submit_parachain_heads()
292		.is_ok()
293	}
294
295	fn insert_relay_block(num: RelayBlockNumber) {
296		pallet_bridge_grandpa::ImportedHeaders::<TestRuntime, crate::Instance1>::insert(
297			RelayBlockHash::from([num as u8; 32]),
298			StoredHeaderData { number: num, state_root: RelayBlockHash::from([10u8; 32]) },
299		);
300	}
301
302	fn sync_to_relay_header_10() {
303		ParasInfo::<TestRuntime, ()>::insert(
304			ParaId(1),
305			ParaInfo {
306				best_head_hash: BestParaHeadHash {
307					at_relay_block_number: 10,
308					head_hash: [1u8; 32].into(),
309				},
310				next_imported_hash_position: 0,
311			},
312		);
313	}
314
315	#[test]
316	fn extension_rejects_header_from_the_obsolete_relay_block() {
317		run_test(|| {
318			// when current best finalized is #10 and we're trying to import header#5 => tx is
319			// rejected
320			sync_to_relay_header_10();
321			assert!(!validate_submit_parachain_heads(5, vec![(ParaId(1), [1u8; 32].into())]));
322		});
323	}
324
325	#[test]
326	fn extension_rejects_header_from_the_same_relay_block() {
327		run_test(|| {
328			// when current best finalized is #10 and we're trying to import header#10 => tx is
329			// rejected
330			sync_to_relay_header_10();
331			assert!(!validate_submit_parachain_heads(10, vec![(ParaId(1), [1u8; 32].into())]));
332		});
333	}
334
335	#[test]
336	fn extension_rejects_header_from_new_relay_block_with_same_hash() {
337		run_test(|| {
338			// when current best finalized is #10 and we're trying to import header#10 => tx is
339			// rejected
340			sync_to_relay_header_10();
341			assert!(!validate_submit_parachain_heads(20, vec![(ParaId(1), [1u8; 32].into())]));
342		});
343	}
344
345	#[test]
346	fn extension_rejects_header_if_pallet_is_halted() {
347		run_test(|| {
348			// when pallet is halted => tx is rejected
349			sync_to_relay_header_10();
350			PalletOperatingMode::<TestRuntime, ()>::put(BasicOperatingMode::Halted);
351
352			assert!(!validate_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
353		});
354	}
355
356	#[test]
357	fn extension_accepts_new_header() {
358		run_test(|| {
359			// when current best finalized is #10 and we're trying to import header#15 => tx is
360			// accepted
361			sync_to_relay_header_10();
362			insert_relay_block(15);
363			assert!(validate_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
364		});
365	}
366
367	#[test]
368	fn extension_accepts_if_more_than_one_parachain_is_submitted() {
369		run_test(|| {
370			// when current best finalized is #10 and we're trying to import header#5, but another
371			// parachain head is also supplied => tx is accepted
372			sync_to_relay_header_10();
373			assert!(validate_submit_parachain_heads(
374				5,
375				vec![(ParaId(1), [1u8; 32].into()), (ParaId(2), [1u8; 32].into())]
376			));
377		});
378	}
379
380	#[test]
381	fn extension_rejects_initial_parachain_head_if_missing_relay_chain_header() {
382		run_test(|| {
383			// when relay chain header is unknown => "obsolete"
384			assert!(!validate_submit_parachain_heads(10, vec![(ParaId(1), [1u8; 32].into())]));
385			// when relay chain header is unknown => "ok"
386			insert_relay_block(10);
387			assert!(validate_submit_parachain_heads(10, vec![(ParaId(1), [1u8; 32].into())]));
388		});
389	}
390
391	#[test]
392	fn extension_rejects_free_parachain_head_if_missing_relay_chain_header() {
393		run_test(|| {
394			sync_to_relay_header_10();
395			// when relay chain header is unknown => "obsolete"
396			assert!(!validate_submit_parachain_heads(15, vec![(ParaId(2), [15u8; 32].into())]));
397			// when relay chain header is unknown => "ok"
398			insert_relay_block(15);
399			assert!(validate_submit_parachain_heads(15, vec![(ParaId(2), [15u8; 32].into())]));
400		});
401	}
402
403	#[test]
404	fn extension_rejects_free_parachain_head_if_no_free_slots_remaining() {
405		run_test(|| {
406			// when current best finalized is #10 and we're trying to import header#15 => tx should
407			// be accepted
408			sync_to_relay_header_10();
409			insert_relay_block(15);
410			// ... but since we have specified `is_free_execution_expected = true`, it'll be
411			// rejected
412			assert!(!validate_free_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
413			// ... if we have specify `is_free_execution_expected = false`, it'll be accepted
414			assert!(validate_submit_parachain_heads(15, vec![(ParaId(1), [2u8; 32].into())]));
415		});
416	}
417
418	#[test]
419	fn extension_rejects_free_parachain_head_if_improves_by_is_below_expected() {
420		run_test(|| {
421			// when current best finalized is #10 and we're trying to import header#15 => tx should
422			// be accepted
423			sync_to_relay_header_10();
424			insert_relay_block(10 + FreeHeadersInterval::get() - 1);
425			insert_relay_block(10 + FreeHeadersInterval::get());
426			// try to submit at 10 + FreeHeadersInterval::get() - 1 => failure
427			let relay_header = 10 + FreeHeadersInterval::get() - 1;
428			assert!(!validate_free_submit_parachain_heads(
429				relay_header,
430				vec![(ParaId(1), [2u8; 32].into())]
431			));
432			// try to submit at 10 + FreeHeadersInterval::get() => ok
433			let relay_header = 10 + FreeHeadersInterval::get();
434			assert!(validate_free_submit_parachain_heads(
435				relay_header,
436				vec![(ParaId(1), [2u8; 32].into())]
437			));
438		});
439	}
440}