lib.rs 63.4 KiB
Newer Older
					(0, test_relay_header(0, state_root).hash()),
					proof.clone(),
				),
				Error::<TestRuntime>::BridgeModule(OwnedBridgeModuleError::Halted)
			);

			// `submit_parachain_heads()` should succeed now that the pallet is resumed.
			PalletOperatingMode::<TestRuntime>::put(BasicOperatingMode::Normal);
			assert_ok!(Pallet::<TestRuntime>::submit_parachain_heads(
				(0, test_relay_header(0, state_root).hash()),
	#[test]
	fn imports_initial_parachain_heads() {
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![
				(1, head_data(1, 0)),
				(3, head_data(3, 10)),
			]);
		run_test(|| {
			initialize(state_root);

			// we're trying to update heads of parachains 1 and 3
			let expected_weight =
				WeightInfo::submit_parachain_heads_weight(DbWeight::get(), &proof, 2);
			let result = Pallet::<TestRuntime>::submit_parachain_heads(
				(0, test_relay_header(0, state_root).hash()),
			assert_eq!(result.expect("checked above").pays_fee, Pays::Yes);
			assert_eq!(result.expect("checked above").actual_weight, Some(expected_weight));
			// 1 and 3 are updated, because proof is missing head of parachain#2
Serban Iorga's avatar
Serban Iorga committed
			assert_eq!(ParasInfo::<TestRuntime>::get(ParaId(1)), Some(initial_best_head(1)));
			assert_eq!(ParasInfo::<TestRuntime>::get(ParaId(2)), None);
Serban Iorga's avatar
Serban Iorga committed
				ParasInfo::<TestRuntime>::get(ParaId(3)),
				Some(ParaInfo {
					best_head_hash: BestParaHeadHash {
						at_relay_block_number: 0,
						head_hash: head_data(3, 10).hash()
					},
					next_imported_hash_position: 1,
				})
			);

			assert_eq!(
Serban Iorga's avatar
Serban Iorga committed
				ImportedParaHeads::<TestRuntime>::get(
					ParaId(1),
					initial_best_head(1).best_head_hash.head_hash
Serban Iorga's avatar
Serban Iorga committed
				ImportedParaHeads::<TestRuntime>::get(
					ParaId(2),
					initial_best_head(2).best_head_hash.head_hash
				ImportedParaHeads::<TestRuntime>::get(ParaId(3), head_hash(3, 10))
					.map(|h| h.into_inner()),

			assert_eq!(
				System::<TestRuntime>::events(),
				vec![
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::UpdatedParachainHead {
							parachain: ParaId(1),
							parachain_head_hash: initial_best_head(1).best_head_hash.head_hash,
						}),
						topics: vec![],
					},
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::UpdatedParachainHead {
							parachain: ParaId(3),
							parachain_head_hash: head_data(3, 10).hash(),
						}),
						topics: vec![],
					}
				],
			);
		});
	}

	#[test]
	fn imports_parachain_heads_is_able_to_progress() {
		let (state_root_5, proof_5, parachains_5) =
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(1, head_data(1, 5))]);
		let (state_root_10, proof_10, parachains_10) =
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(1, head_data(1, 10))]);
		run_test(|| {
			// start with relay block #0 and import head#5 of parachain#1
			initialize(state_root_5);
			let result = import_parachain_1_head(0, state_root_5, parachains_5, proof_5);
			// first parachain head is imported for free
			assert_eq!(result.unwrap().pays_fee, Pays::No);
Serban Iorga's avatar
Serban Iorga committed
				ParasInfo::<TestRuntime>::get(ParaId(1)),
				Some(ParaInfo {
					best_head_hash: BestParaHeadHash {
						at_relay_block_number: 0,
						head_hash: head_data(1, 5).hash()
					},
					next_imported_hash_position: 1,
				})
			);
			assert_eq!(
				ImportedParaHeads::<TestRuntime>::get(ParaId(1), head_data(1, 5).hash())
					.map(|h| h.into_inner()),
				ImportedParaHeads::<TestRuntime>::get(ParaId(1), head_data(1, 10).hash())
					.map(|h| h.into_inner()),
			assert_eq!(
				System::<TestRuntime>::events(),
				vec![EventRecord {
					phase: Phase::Initialization,
					event: TestEvent::Parachains(Event::UpdatedParachainHead {
						parachain: ParaId(1),
						parachain_head_hash: head_data(1, 5).hash(),
					}),
					topics: vec![],
				}],
			);

			// import head#10 of parachain#1 at relay block #1
			let (relay_1_hash, justification) = proceed(1, state_root_10);
			let result = import_parachain_1_head(1, state_root_10, parachains_10, proof_10);
			// second parachain head is imported for fee
			assert_eq!(result.unwrap().pays_fee, Pays::Yes);
Serban Iorga's avatar
Serban Iorga committed
				ParasInfo::<TestRuntime>::get(ParaId(1)),
				Some(ParaInfo {
					best_head_hash: BestParaHeadHash {
						at_relay_block_number: 1,
						head_hash: head_data(1, 10).hash()
					},
					next_imported_hash_position: 2,
				})
			);
			assert_eq!(
				ImportedParaHeads::<TestRuntime>::get(ParaId(1), head_data(1, 5).hash())
					.map(|h| h.into_inner()),
				ImportedParaHeads::<TestRuntime>::get(ParaId(1), head_data(1, 10).hash())
					.map(|h| h.into_inner()),
			assert_eq!(
				System::<TestRuntime>::events(),
				vec![
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::UpdatedParachainHead {
							parachain: ParaId(1),
							parachain_head_hash: head_data(1, 5).hash(),
						}),
						topics: vec![],
					},
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Grandpa1(
							pallet_bridge_grandpa::Event::UpdatedBestFinalizedHeader {
								number: 1,
								hash: relay_1_hash,
								grandpa_info: StoredHeaderGrandpaInfo {
									finality_proof: justification,
									new_verification_context: None,
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::UpdatedParachainHead {
							parachain: ParaId(1),
							parachain_head_hash: head_data(1, 10).hash(),
						}),
						topics: vec![],
					}
				],
			);
	#[test]
	fn ignores_untracked_parachain() {
		let (state_root, proof, parachains) =
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![
				(1, head_data(1, 5)),
				(UNTRACKED_PARACHAIN_ID, head_data(1, 5)),
				(2, head_data(1, 5)),
			]);
		run_test(|| {
			// start with relay block #0 and try to import head#5 of parachain#1 and untracked
			// parachain
			let expected_weight =
				WeightInfo::submit_parachain_heads_weight(DbWeight::get(), &proof, 3)
					.saturating_sub(WeightInfo::parachain_head_storage_write_weight(
						DbWeight::get(),
					));
			initialize(state_root);
			let result = Pallet::<TestRuntime>::submit_parachain_heads(
				(0, test_relay_header(0, state_root).hash()),
			);
			assert_ok!(result);
			assert_eq!(result.expect("checked above").actual_weight, Some(expected_weight));
Serban Iorga's avatar
Serban Iorga committed
				ParasInfo::<TestRuntime>::get(ParaId(1)),
				Some(ParaInfo {
					best_head_hash: BestParaHeadHash {
						at_relay_block_number: 0,
						head_hash: head_data(1, 5).hash()
					},
					next_imported_hash_position: 1,
				})
			);
Serban Iorga's avatar
Serban Iorga committed
			assert_eq!(ParasInfo::<TestRuntime>::get(ParaId(UNTRACKED_PARACHAIN_ID)), None,);
Serban Iorga's avatar
Serban Iorga committed
				ParasInfo::<TestRuntime>::get(ParaId(2)),
				Some(ParaInfo {
					best_head_hash: BestParaHeadHash {
						at_relay_block_number: 0,
						head_hash: head_data(1, 5).hash()
					},
					next_imported_hash_position: 1,
				})
			);
			assert_eq!(
				System::<TestRuntime>::events(),
				vec![
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::UpdatedParachainHead {
							parachain: ParaId(1),
							parachain_head_hash: head_data(1, 5).hash(),
						}),
						topics: vec![],
					},
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::UntrackedParachainRejected {
							parachain: ParaId(UNTRACKED_PARACHAIN_ID),
						}),
						topics: vec![],
					},
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::UpdatedParachainHead {
							parachain: ParaId(2),
							parachain_head_hash: head_data(1, 5).hash(),
						}),
						topics: vec![],
					}
				],
			);
	#[test]
	fn does_nothing_when_already_imported_this_head_at_previous_relay_header() {
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(1, head_data(1, 0))]);
		run_test(|| {
			// import head#0 of parachain#1 at relay block#0
			initialize(state_root);
			assert_ok!(import_parachain_1_head(0, state_root, parachains.clone(), proof.clone()));
Serban Iorga's avatar
Serban Iorga committed
			assert_eq!(ParasInfo::<TestRuntime>::get(ParaId(1)), Some(initial_best_head(1)));
			assert_eq!(
				System::<TestRuntime>::events(),
				vec![EventRecord {
					phase: Phase::Initialization,
					event: TestEvent::Parachains(Event::UpdatedParachainHead {
						parachain: ParaId(1),
						parachain_head_hash: initial_best_head(1).best_head_hash.head_hash,
					}),
					topics: vec![],
				}],
			);

			// try to import head#0 of parachain#1 at relay block#1
			// => call succeeds, but nothing is changed
			let (relay_1_hash, justification) = proceed(1, state_root);
			assert_ok!(import_parachain_1_head(1, state_root, parachains, proof));
Serban Iorga's avatar
Serban Iorga committed
			assert_eq!(ParasInfo::<TestRuntime>::get(ParaId(1)), Some(initial_best_head(1)));
			assert_eq!(
				System::<TestRuntime>::events(),
				vec![
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::UpdatedParachainHead {
							parachain: ParaId(1),
							parachain_head_hash: initial_best_head(1).best_head_hash.head_hash,
						}),
						topics: vec![],
					},
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Grandpa1(
							pallet_bridge_grandpa::Event::UpdatedBestFinalizedHeader {
								number: 1,
								hash: relay_1_hash,
								grandpa_info: StoredHeaderGrandpaInfo {
									finality_proof: justification,
									new_verification_context: None,
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::RejectedObsoleteParachainHead {
							parachain: ParaId(1),
							parachain_head_hash: initial_best_head(1).best_head_hash.head_hash,
						}),
						topics: vec![],
					}
				],
			);
		});
	}

	#[test]
	fn does_nothing_when_already_imported_head_at_better_relay_header() {
		let (state_root_5, proof_5, parachains_5) =
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(1, head_data(1, 5))]);
		let (state_root_10, proof_10, parachains_10) =
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(1, head_data(1, 10))]);
		run_test(|| {
			// start with relay block #0
			initialize(state_root_5);

			// head#10 of parachain#1 at relay block#1
			let (relay_1_hash, justification) = proceed(1, state_root_10);
			assert_ok!(import_parachain_1_head(1, state_root_10, parachains_10, proof_10));
Serban Iorga's avatar
Serban Iorga committed
				ParasInfo::<TestRuntime>::get(ParaId(1)),
				Some(ParaInfo {
					best_head_hash: BestParaHeadHash {
						at_relay_block_number: 1,
						head_hash: head_data(1, 10).hash()
					},
					next_imported_hash_position: 1,
				})
			);
			assert_eq!(
				System::<TestRuntime>::events(),
				vec![
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Grandpa1(
							pallet_bridge_grandpa::Event::UpdatedBestFinalizedHeader {
								number: 1,
								hash: relay_1_hash,
								grandpa_info: StoredHeaderGrandpaInfo {
									finality_proof: justification.clone(),
									new_verification_context: None,
							}
						),
						topics: vec![],
					},
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::UpdatedParachainHead {
							parachain: ParaId(1),
							parachain_head_hash: head_data(1, 10).hash(),
						}),
						topics: vec![],
					}
				],
			// now try to import head#5 at relay block#0
			// => nothing is changed, because better head has already been imported
			assert_ok!(import_parachain_1_head(0, state_root_5, parachains_5, proof_5));
Serban Iorga's avatar
Serban Iorga committed
				ParasInfo::<TestRuntime>::get(ParaId(1)),
				Some(ParaInfo {
					best_head_hash: BestParaHeadHash {
						at_relay_block_number: 1,
						head_hash: head_data(1, 10).hash()
					},
					next_imported_hash_position: 1,
				})
			);
			assert_eq!(
				System::<TestRuntime>::events(),
				vec![
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Grandpa1(
							pallet_bridge_grandpa::Event::UpdatedBestFinalizedHeader {
								number: 1,
								hash: relay_1_hash,
								grandpa_info: StoredHeaderGrandpaInfo {
									finality_proof: justification,
									new_verification_context: None,
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::UpdatedParachainHead {
							parachain: ParaId(1),
							parachain_head_hash: head_data(1, 10).hash(),
						}),
						topics: vec![],
					},
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::RejectedObsoleteParachainHead {
							parachain: ParaId(1),
							parachain_head_hash: head_data(1, 5).hash(),
						}),
						topics: vec![],
					}
				],
			);
	#[test]
	fn does_nothing_when_parachain_head_is_too_large() {
		let (state_root, proof, parachains) =
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![
				(1, head_data(1, 5)),
				(4, big_head_data(1, 5)),
			]);
			// start with relay block #0 and try to import head#5 of parachain#1 and big parachain
			initialize(state_root);
			let result = Pallet::<TestRuntime>::submit_parachain_heads(
				(0, test_relay_header(0, state_root).hash()),
				parachains,
				proof,
			);
			assert_ok!(result);
			assert_eq!(
				ParasInfo::<TestRuntime>::get(ParaId(1)),
				Some(ParaInfo {
					best_head_hash: BestParaHeadHash {
						at_relay_block_number: 0,
						head_hash: head_data(1, 5).hash()
					},
					next_imported_hash_position: 1,
				})
			);
			assert_eq!(ParasInfo::<TestRuntime>::get(ParaId(4)), None);
			assert_eq!(
				System::<TestRuntime>::events(),
				vec![
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::UpdatedParachainHead {
							parachain: ParaId(1),
							parachain_head_hash: head_data(1, 5).hash(),
						}),
						topics: vec![],
					},
					EventRecord {
						phase: Phase::Initialization,
						event: TestEvent::Parachains(Event::RejectedLargeParachainHead {
							parachain: ParaId(4),
							parachain_head_hash: big_head_data(1, 5).hash(),
							parachain_head_size: big_stored_head_data(1, 5).encoded_size() as u32,
	#[test]
	fn prunes_old_heads() {
		run_test(|| {
			let heads_to_keep = crate::mock::HeadsToKeep::get();

			// import exactly `HeadsToKeep` headers
			for i in 0..heads_to_keep {
				let (state_root, proof, parachains) = prepare_parachain_heads_proof::<
					RegularParachainHeader,
				>(vec![(1, head_data(1, i))]);
				if i == 0 {
					initialize(state_root);
				} else {
					proceed(i, state_root);
				}

				let expected_weight = weight_of_import_parachain_1_head(&proof, false);
				let result = import_parachain_1_head(i, state_root, parachains, proof);
				assert_ok!(result);
				assert_eq!(result.expect("checked above").actual_weight, Some(expected_weight));
			}

			// nothing is pruned yet
			for i in 0..heads_to_keep {
				assert!(ImportedParaHeads::<TestRuntime>::get(ParaId(1), head_data(1, i).hash())
					.is_some());
			}

			// import next relay chain header and next parachain head
			let (state_root, proof, parachains) = prepare_parachain_heads_proof::<
				RegularParachainHeader,
			>(vec![(1, head_data(1, heads_to_keep))]);
			proceed(heads_to_keep, state_root);
			let expected_weight = weight_of_import_parachain_1_head(&proof, true);
			let result = import_parachain_1_head(heads_to_keep, state_root, parachains, proof);
			assert_ok!(result);
			assert_eq!(result.expect("checked above").actual_weight, Some(expected_weight));

			// and the head#0 is pruned
			assert!(
				ImportedParaHeads::<TestRuntime>::get(ParaId(1), head_data(1, 0).hash()).is_none()
			);
			for i in 1..=heads_to_keep {
				assert!(ImportedParaHeads::<TestRuntime>::get(ParaId(1), head_data(1, i).hash())
					.is_some());
			}
		});
	}

	#[test]
	fn fails_on_unknown_relay_chain_block() {
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(1, head_data(1, 5))]);
		run_test(|| {
			// start with relay block #0
			initialize(state_root);

			// try to import head#5 of parachain#1 at unknown relay chain block #1
			assert_noop!(
				import_parachain_1_head(1, state_root, parachains, proof),
				Error::<TestRuntime>::UnknownRelayChainBlock
			);
		});
	}

	#[test]
	fn fails_on_invalid_storage_proof() {
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(1, head_data(1, 5))]);
		run_test(|| {
			// start with relay block #0
			initialize(Default::default());

			// try to import head#5 of parachain#1 at relay chain block #0
			assert_noop!(
				import_parachain_1_head(0, Default::default(), parachains, proof),
				Error::<TestRuntime>::HeaderChainStorageProof(HeaderChainError::StorageProof(
					StorageProofError::StorageRootMismatch
				))

	#[test]
	fn is_not_rewriting_existing_head_if_failed_to_read_updated_head() {
		let (state_root_5, proof_5, parachains_5) =
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(1, head_data(1, 5))]);
		let (state_root_10_at_20, proof_10_at_20, parachains_10_at_20) =
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(2, head_data(2, 10))]);
		let (state_root_10_at_30, proof_10_at_30, parachains_10_at_30) =
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(1, head_data(1, 10))]);
		run_test(|| {
			// we've already imported head#5 of parachain#1 at relay block#10
			initialize(state_root_5);
			import_parachain_1_head(0, state_root_5, parachains_5, proof_5).expect("ok");
			assert_eq!(
				Pallet::<TestRuntime>::best_parachain_head(ParaId(1)),
			);

			// then if someone is pretending to provide updated head#10 of parachain#1 at relay
			// block#20, but fails to do that
			//
			// => we'll leave previous value
			proceed(20, state_root_10_at_20);
			assert_ok!(Pallet::<TestRuntime>::submit_parachain_heads(
				(20, test_relay_header(20, state_root_10_at_20).hash()),
				proof_10_at_20,
			),);
			assert_eq!(
				Pallet::<TestRuntime>::best_parachain_head(ParaId(1)),
			);

			// then if someone is pretending to provide updated head#10 of parachain#1 at relay
			// block#30, and actually provides it
			//
			// => we'll update value
			proceed(30, state_root_10_at_30);
			assert_ok!(Pallet::<TestRuntime>::submit_parachain_heads(
				(30, test_relay_header(30, state_root_10_at_30).hash()),
				proof_10_at_30,
			),);
			assert_eq!(
				Pallet::<TestRuntime>::best_parachain_head(ParaId(1)),

	#[test]
	fn storage_keys_computed_properly() {
		assert_eq!(
Serban Iorga's avatar
Serban Iorga committed
			ParasInfo::<TestRuntime>::storage_map_final_key(ParaId(42)).to_vec(),
			ParasInfoKeyProvider::final_key("Parachains", &ParaId(42)).0
		);

		assert_eq!(
			ImportedParaHeads::<TestRuntime>::storage_double_map_final_key(
				ParaId(42),
				ParaHash::from([21u8; 32])
			)
			.to_vec(),
			ImportedParaHeadsKeyProvider::final_key(
				"Parachains",
				&ParaId(42),
				&ParaHash::from([21u8; 32])
	#[test]
	fn ignores_parachain_head_if_it_is_missing_from_storage_proof() {
		let (state_root, proof, _) =
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![]);
		let parachains = vec![(ParaId(2), Default::default())];
		run_test(|| {
			initialize(state_root);
			assert_ok!(Pallet::<TestRuntime>::submit_parachain_heads(
				(0, test_relay_header(0, state_root).hash()),
				parachains,
				proof,
			));
			assert_eq!(
				System::<TestRuntime>::events(),
				vec![EventRecord {
					phase: Phase::Initialization,
					event: TestEvent::Parachains(Event::MissingParachainHead {
						parachain: ParaId(2),
					}),
					topics: vec![],
				}],
			);
		});
	}

	#[test]
	fn ignores_parachain_head_if_parachain_head_hash_is_wrong() {
		let (state_root, proof, _) =
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(1, head_data(1, 0))]);
		let parachains = vec![(ParaId(1), head_data(1, 10).hash())];
		run_test(|| {
			initialize(state_root);
			assert_ok!(Pallet::<TestRuntime>::submit_parachain_heads(
				(0, test_relay_header(0, state_root).hash()),
				parachains,
				proof,
			));
			assert_eq!(
				System::<TestRuntime>::events(),
				vec![EventRecord {
					phase: Phase::Initialization,
					event: TestEvent::Parachains(Event::IncorrectParachainHeadHash {
						parachain: ParaId(1),
						parachain_head_hash: head_data(1, 10).hash(),
						actual_parachain_head_hash: head_data(1, 0).hash(),
					}),
					topics: vec![],
				}],
			);
		});
	}

	#[test]
	fn test_bridge_parachain_call_is_correctly_defined() {
		let (state_root, proof, _) =
			prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(1, head_data(1, 0))]);
		let parachains = vec![(ParaId(2), Default::default())];
		let relay_header_id = (0, test_relay_header(0, state_root).hash());

		let direct_submit_parachain_heads_call = Call::<TestRuntime>::submit_parachain_heads {
			at_relay_block: relay_header_id,
			parachains: parachains.clone(),
			parachain_heads_proof: proof.clone(),
		};
		let indirect_submit_parachain_heads_call = BridgeParachainCall::submit_parachain_heads {
			at_relay_block: relay_header_id,
			parachains,
			parachain_heads_proof: proof,
		};
		assert_eq!(
			direct_submit_parachain_heads_call.encode(),
			indirect_submit_parachain_heads_call.encode()
		);
	}

	generate_owned_bridge_module_tests!(BasicOperatingMode::Normal, BasicOperatingMode::Halted);

	#[test]
	fn maybe_max_parachains_returns_correct_value() {
		assert_eq!(MaybeMaxParachains::<TestRuntime, ()>::get(), Some(mock::TOTAL_PARACHAINS));
	}

	#[test]
	fn maybe_max_total_parachain_hashes_returns_correct_value() {
		assert_eq!(
			MaybeMaxTotalParachainHashes::<TestRuntime, ()>::get(),
			Some(mock::TOTAL_PARACHAINS * mock::HeadsToKeep::get()),
		);
	}

	#[test]
	fn submit_finality_proof_requires_signed_origin() {
		run_test(|| {
			let (state_root, proof, parachains) =
				prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(1, head_data(1, 0))]);

			initialize(state_root);

			// `submit_parachain_heads()` should fail when the pallet is halted.
			assert_noop!(
				Pallet::<TestRuntime>::submit_parachain_heads(
					RuntimeOrigin::root(),
					(0, test_relay_header(0, state_root).hash()),
					parachains,
					proof,
				),
				DispatchError::BadOrigin
			);
		})
	}

	#[test]
	fn may_be_free_for_submitting_filtered_heads() {
		run_test(|| {
			let (state_root, proof, parachains) =
				prepare_parachain_heads_proof::<RegularParachainHeader>(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::<TestRuntime>::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::<TestRuntime>::submit_parachain_heads(
				RuntimeOrigin::signed(1),
				(0, test_relay_header(0, state_root).hash()),
				parachains,
				proof,
			);
			assert_eq!(result.unwrap().pays_fee, Pays::Yes);
			// then we submit new head, proved at relay block `FreeHeadersInterval - 1` => Pays::Yes
			let (state_root, proof, parachains) = prepare_parachain_heads_proof::<
				RegularParachainHeader,
			>(vec![(2, head_data(2, 50))]);
			let relay_block_number = FreeHeadersInterval::get() - 1;
			proceed(relay_block_number, state_root);
			let result = Pallet::<TestRuntime>::submit_parachain_heads(
				RuntimeOrigin::signed(1),
				(relay_block_number, test_relay_header(relay_block_number, state_root).hash()),
				parachains,
				proof,
			);
			assert_eq!(result.unwrap().pays_fee, Pays::Yes);
			// then we submit new head, proved after `FreeHeadersInterval` => Pays::No
			let (state_root, proof, parachains) = prepare_parachain_heads_proof::<
				RegularParachainHeader,
			>(vec![(2, head_data(2, 100))]);
			let relay_block_number = relay_block_number + FreeHeadersInterval::get();
			proceed(relay_block_number, state_root);
			let result = Pallet::<TestRuntime>::submit_parachain_heads(
				RuntimeOrigin::signed(1),
				(relay_block_number, test_relay_header(relay_block_number, state_root).hash()),
				parachains,
				proof,
			);
			assert_eq!(result.unwrap().pays_fee, Pays::No);
			// then we submit new BIG head, proved after `FreeHeadersInterval` => Pays::Yes
			// then we submit new head, proved after `FreeHeadersInterval` => Pays::No
			let mut large_head = head_data(2, 100);
			large_head.0.extend(&[42u8; BigParachain::MAX_HEADER_SIZE as _]);
			let (state_root, proof, parachains) =
				prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(2, large_head)]);
			let relay_block_number = relay_block_number + FreeHeadersInterval::get();
			proceed(relay_block_number, state_root);
			let result = Pallet::<TestRuntime>::submit_parachain_heads(
				RuntimeOrigin::signed(1),
				(relay_block_number, test_relay_header(relay_block_number, state_root).hash()),
				parachains,
				proof,
			);
			assert_eq!(result.unwrap().pays_fee, Pays::Yes);
		})
	}

	#[test]
	fn grandpa_and_parachain_pallets_share_free_headers_counter() {
		run_test(|| {
			initialize(Default::default());
			// set free headers limit to `4`
			let mut free_headers_remaining = 4;
			pallet_bridge_grandpa::FreeHeadersRemaining::<TestRuntime, BridgesGrandpaPalletInstance>::set(
				Some(free_headers_remaining),
			);
			// import free GRANDPA and parachain headers
			let mut relay_block_number = 0;
			for i in 0..2 {
				// import free GRANDPA header
				let (state_root, proof, parachains) = prepare_parachain_heads_proof::<
					RegularParachainHeader,
				>(vec![(2, head_data(2, 5 + i))]);
				relay_block_number = relay_block_number + FreeHeadersInterval::get();
				proceed(relay_block_number, state_root);
				assert_eq!(
					pallet_bridge_grandpa::FreeHeadersRemaining::<
						TestRuntime,
						BridgesGrandpaPalletInstance,
					>::get(),
					Some(free_headers_remaining - 1),
				);
				free_headers_remaining = free_headers_remaining - 1;
				// import free parachain header
				assert_ok!(Pallet::<TestRuntime>::submit_parachain_heads(
					RuntimeOrigin::signed(1),
					(relay_block_number, test_relay_header(relay_block_number, state_root).hash()),
					parachains,
					proof,
				),);
				assert_eq!(
					pallet_bridge_grandpa::FreeHeadersRemaining::<
						TestRuntime,
						BridgesGrandpaPalletInstance,
					>::get(),
					Some(free_headers_remaining - 1),
				);
				free_headers_remaining = free_headers_remaining - 1;
			}
			// try to import free GRANDPA header => non-free execution
			let (state_root, proof, parachains) =
				prepare_parachain_heads_proof::<RegularParachainHeader>(vec![(2, head_data(2, 7))]);
			relay_block_number = relay_block_number + FreeHeadersInterval::get();
			let result = pallet_bridge_grandpa::Pallet::<TestRuntime, BridgesGrandpaPalletInstance>::submit_finality_proof_ex(
				RuntimeOrigin::signed(1),
				Box::new(test_relay_header(relay_block_number, state_root)),
				make_default_justification(&test_relay_header(relay_block_number, state_root)),
				TEST_GRANDPA_SET_ID,
				false,
			);
			assert_eq!(result.unwrap().pays_fee, Pays::Yes);
			// try to import free parachain header => non-free execution
			let result = Pallet::<TestRuntime>::submit_parachain_heads(
				RuntimeOrigin::signed(1),
				(relay_block_number, test_relay_header(relay_block_number, state_root).hash()),
				parachains,
				proof,
			);
			assert_eq!(result.unwrap().pays_fee, Pays::Yes);
			assert_eq!(
				pallet_bridge_grandpa::FreeHeadersRemaining::<
					TestRuntime,
					BridgesGrandpaPalletInstance,
				>::get(),
				Some(0),
			);
		});
	}