lib.rs 56.4 KiB
Newer Older
			initialize_substrate_bridge();

			assert_ok!(Pallet::<TestRuntime>::set_operating_mode(
				BasicOperatingMode::Halted
			));
			assert_noop!(
				submit_finality_proof(1),
				Error::<TestRuntime>::BridgeModule(bp_runtime::OwnedBridgeModuleError::Halted)
			);
			assert_ok!(Pallet::<TestRuntime>::set_operating_mode(
				BasicOperatingMode::Normal
			));
			assert_ok!(submit_finality_proof(1));
	#[test]
	fn pallet_rejects_header_if_not_initialized_yet() {
		run_test(|| {
			assert_noop!(submit_finality_proof(1), Error::<TestRuntime>::NotInitialized);
		});
	}

	fn successfully_imports_header_with_valid_finality() {
		run_test(|| {
			initialize_substrate_bridge();

			let header_number = 1;
			let header = test_header(header_number.into());
			let justification = make_default_justification(&header);

			let pre_dispatch_weight = <TestRuntime as Config>::WeightInfo::submit_finality_proof(
				justification.commit.precommits.len().try_into().unwrap_or(u32::MAX),
				justification.votes_ancestries.len().try_into().unwrap_or(u32::MAX),
			);

			let result = submit_finality_proof(header_number);
			assert_ok!(result);
			assert_eq!(result.unwrap().pays_fee, frame_support::dispatch::Pays::Yes);
			// our test config assumes 2048 max authorities and we are just using couple
			let pre_dispatch_proof_size = pre_dispatch_weight.proof_size();
			let actual_proof_size = result.unwrap().actual_weight.unwrap().proof_size();
			assert!(actual_proof_size > 0);
			assert!(
				actual_proof_size < pre_dispatch_proof_size,
				"Actual proof size {actual_proof_size} must be less than the pre-dispatch {pre_dispatch_proof_size}",
			);
			let header = test_header(1);
			assert_eq!(<BestFinalized<TestRuntime>>::get().unwrap().1, header.hash());
			assert!(<ImportedHeaders<TestRuntime>>::contains_key(header.hash()));

			assert_eq!(
				System::events(),
				vec![EventRecord {
					phase: Phase::Initialization,
					event: TestEvent::Grandpa(Event::UpdatedBestFinalizedHeader {
						number: *header.number(),
						hash: header.hash(),
						grandpa_info: StoredHeaderGrandpaInfo {
							finality_proof: justification.clone(),
							new_verification_context: None,
			assert_eq!(
				Pallet::<TestRuntime>::synced_headers_grandpa_info(),
				vec![StoredHeaderGrandpaInfo {
					finality_proof: justification,
					new_verification_context: None
				}]
	#[test]
	fn rejects_justification_that_skips_authority_set_transition() {
		run_test(|| {
			initialize_substrate_bridge();

			let header = test_header(1);
			let next_set_id = 2;
			let params = JustificationGeneratorParams::<TestHeader> {
				set_id: next_set_id,
				..Default::default()
			};
			let justification = make_justification_for_header(params);
				Pallet::<TestRuntime>::submit_finality_proof_ex(
					RuntimeOrigin::signed(1),
					Box::new(header.clone()),
					justification.clone(),
					TEST_GRANDPA_SET_ID,
				),
				<Error<TestRuntime>>::InvalidJustification
			);
			assert_err!(
				Pallet::<TestRuntime>::submit_finality_proof_ex(
hacpy's avatar
hacpy committed
					Box::new(header),
					justification,
hacpy's avatar
hacpy committed
				),
				<Error<TestRuntime>>::InvalidAuthoritySetId
	#[test]
	fn does_not_import_header_with_invalid_finality_proof() {
		run_test(|| {
			initialize_substrate_bridge();

			let header = test_header(1);
			let mut justification = make_default_justification(&header);
			justification.round = 42;
				Pallet::<TestRuntime>::submit_finality_proof_ex(
hacpy's avatar
hacpy committed
					Box::new(header),
					justification,
hacpy's avatar
hacpy committed
				),
				<Error<TestRuntime>>::InvalidJustification
			);
		})
	}

	#[test]
	fn disallows_invalid_authority_set() {
		run_test(|| {
			let genesis = test_header(0);

			let invalid_authority_list = vec![(ALICE.into(), u64::MAX), (BOB.into(), u64::MAX)];
			let init_data = InitializationData {
				header: Box::new(genesis),
				authority_list: invalid_authority_list,
				set_id: 1,
				operating_mode: BasicOperatingMode::Normal,
			assert_ok!(Pallet::<TestRuntime>::initialize(RuntimeOrigin::root(), init_data));

			let header = test_header(1);
			let justification = make_default_justification(&header);
				Pallet::<TestRuntime>::submit_finality_proof_ex(
hacpy's avatar
hacpy committed
					Box::new(header),
					justification,
hacpy's avatar
hacpy committed
				),
				<Error<TestRuntime>>::InvalidAuthoritySet
			);
		})
	}
	fn importing_header_ensures_that_chain_is_extended() {
		run_test(|| {
			initialize_substrate_bridge();

			assert_ok!(submit_finality_proof(4));
			assert_err!(submit_finality_proof(3), Error::<TestRuntime>::OldHeader);
			assert_ok!(submit_finality_proof(5));
		})
	}

	#[test]
	fn importing_header_enacts_new_authority_set() {
		run_test(|| {
			initialize_substrate_bridge();

			let next_set_id = 2;
			let next_authorities = vec![(ALICE.into(), 1), (BOB.into(), 1)];

			// Need to update the header digest to indicate that our header signals an authority set
			// change. The change will be enacted when we import our header.
			let mut header = test_header(2);
			header.digest = change_log(0);

			// Create a valid justification for the header
			let justification = make_default_justification(&header);
			// Let's import our test header
			let result = Pallet::<TestRuntime>::submit_finality_proof_ex(
				RuntimeOrigin::signed(1),
				Box::new(header.clone()),
			assert_ok!(result);
			assert_eq!(result.unwrap().pays_fee, frame_support::dispatch::Pays::No);

			// Make sure that our header is the best finalized
			assert_eq!(<BestFinalized<TestRuntime>>::get().unwrap().1, header.hash());
			assert!(<ImportedHeaders<TestRuntime>>::contains_key(header.hash()));

			// Make sure that the authority set actually changed upon importing our header
			assert_eq!(
				<CurrentAuthoritySet<TestRuntime>>::get(),
				StoredAuthoritySet::<TestRuntime, ()>::try_new(next_authorities, next_set_id)
					.unwrap(),

			// Here
			assert_eq!(
				System::events(),
				vec![EventRecord {
					phase: Phase::Initialization,
					event: TestEvent::Grandpa(Event::UpdatedBestFinalizedHeader {
						number: *header.number(),
						hash: header.hash(),
						grandpa_info: StoredHeaderGrandpaInfo {
							finality_proof: justification.clone(),
							new_verification_context: Some(
								<CurrentAuthoritySet<TestRuntime>>::get().into()
							),
						},
					}),
					topics: vec![],
				}],
			);
			assert_eq!(
				Pallet::<TestRuntime>::synced_headers_grandpa_info(),
				vec![StoredHeaderGrandpaInfo {
					finality_proof: justification,
					new_verification_context: Some(
						<CurrentAuthoritySet<TestRuntime>>::get().into()
					),
	#[test]
	fn relayer_pays_tx_fee_when_submitting_huge_mandatory_header() {
		run_test(|| {
			initialize_substrate_bridge();

			// let's prepare a huge authorities change header, which is definitely above size limits
			let mut header = test_header(2);
			header.digest = change_log(0);
			header.digest.push(DigestItem::Other(vec![42u8; 1024 * 1024]));
			let justification = make_default_justification(&header);

			// without large digest item ^^^ the relayer would have paid zero transaction fee
			// (`Pays::No`)
			let result = Pallet::<TestRuntime>::submit_finality_proof_ex(
				RuntimeOrigin::signed(1),
				Box::new(header.clone()),
				justification,
			);
			assert_ok!(result);
			assert_eq!(result.unwrap().pays_fee, frame_support::dispatch::Pays::Yes);

			// Make sure that our header is the best finalized
			assert_eq!(<BestFinalized<TestRuntime>>::get().unwrap().1, header.hash());
			assert!(<ImportedHeaders<TestRuntime>>::contains_key(header.hash()));
		})
	}

	#[test]
	fn relayer_pays_tx_fee_when_submitting_justification_with_long_ancestry_votes() {
		run_test(|| {
			initialize_substrate_bridge();

			// let's prepare a huge authorities change header, which is definitely above weight
			// limits
			let mut header = test_header(2);
			header.digest = change_log(0);
			let justification = make_justification_for_header(JustificationGeneratorParams {
				header: header.clone(),
				ancestors: TestBridgedChain::REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY + 1,
				..Default::default()
			});

			// without many headers in votes ancestries ^^^ the relayer would have paid zero
			// transaction fee (`Pays::No`)
			let result = Pallet::<TestRuntime>::submit_finality_proof_ex(
				RuntimeOrigin::signed(1),
				Box::new(header.clone()),
				justification,
			);
			assert_ok!(result);
			assert_eq!(result.unwrap().pays_fee, frame_support::dispatch::Pays::Yes);

			// Make sure that our header is the best finalized
			assert_eq!(<BestFinalized<TestRuntime>>::get().unwrap().1, header.hash());
			assert!(<ImportedHeaders<TestRuntime>>::contains_key(header.hash()));
		})
	}

	#[test]
	fn importing_header_rejects_header_with_scheduled_change_delay() {
		run_test(|| {
			initialize_substrate_bridge();

			// Need to update the header digest to indicate that our header signals an authority set
			// change. However, the change doesn't happen until the next block.
			let mut header = test_header(2);
			header.digest = change_log(1);

			// Create a valid justification for the header
			let justification = make_default_justification(&header);
			// Should not be allowed to import this header
			assert_err!(
				Pallet::<TestRuntime>::submit_finality_proof_ex(
hacpy's avatar
hacpy committed
					Box::new(header),
hacpy's avatar
hacpy committed
				),
				<Error<TestRuntime>>::UnsupportedScheduledChange
			);
		})
	}

	#[test]
	fn importing_header_rejects_header_with_forced_changes() {
		run_test(|| {
			initialize_substrate_bridge();

			// Need to update the header digest to indicate that it signals a forced authority set
			// change.
			let mut header = test_header(2);
			header.digest = forced_change_log(0);

			// Create a valid justification for the header
			let justification = make_default_justification(&header);
			// Should not be allowed to import this header
			assert_err!(
				Pallet::<TestRuntime>::submit_finality_proof_ex(
hacpy's avatar
hacpy committed
					Box::new(header),
hacpy's avatar
hacpy committed
				),
				<Error<TestRuntime>>::UnsupportedScheduledChange
			);
		})
	}
	#[test]
	fn importing_header_rejects_header_with_too_many_authorities() {
		run_test(|| {
			initialize_substrate_bridge();

			// Need to update the header digest to indicate that our header signals an authority set
			// change. However, the change doesn't happen until the next block.
			let mut header = test_header(2);
			header.digest = many_authorities_log();

			// Create a valid justification for the header
			let justification = make_default_justification(&header);

			// Should not be allowed to import this header
			assert_err!(
				Pallet::<TestRuntime>::submit_finality_proof_ex(
	#[test]
	fn parse_finalized_storage_proof_rejects_proof_on_unknown_header() {
		run_test(|| {
			assert_noop!(
				Pallet::<TestRuntime>::storage_proof_checker(Default::default(), vec![],)
					.map(|_| ()),
				bp_header_chain::HeaderChainError::UnknownHeader,
			);
		});
	}

	#[test]
	fn parse_finalized_storage_accepts_valid_proof() {
		run_test(|| {
			let (state_root, storage_proof) = bp_runtime::craft_valid_storage_proof();

			let mut header = test_header(2);
			header.set_state_root(state_root);

			let hash = header.hash();
			<BestFinalized<TestRuntime>>::put(HeaderId(2, hash));
			<ImportedHeaders<TestRuntime>>::insert(hash, header.build());
				Pallet::<TestRuntime>::storage_proof_checker(hash, storage_proof).map(|_| ())
	fn rate_limiter_disallows_free_imports_once_limit_is_hit_in_single_block() {
		run_test(|| {
			initialize_substrate_bridge();
			let result = submit_mandatory_finality_proof(1, 1);
			assert_eq!(result.expect("call failed").pays_fee, Pays::No);

			let result = submit_mandatory_finality_proof(2, 2);
			assert_eq!(result.expect("call failed").pays_fee, Pays::No);

			let result = submit_mandatory_finality_proof(3, 3);
			assert_eq!(result.expect("call failed").pays_fee, Pays::Yes);
	fn rate_limiter_invalid_requests_do_not_count_towards_request_count() {
		run_test(|| {
			let submit_invalid_request = || {
				let mut header = test_header(1);
				header.digest = change_log(0);
				let mut invalid_justification = make_default_justification(&header);
				invalid_justification.round = 42;
				Pallet::<TestRuntime>::submit_finality_proof_ex(
hacpy's avatar
hacpy committed
					Box::new(header),
					invalid_justification,
hacpy's avatar
hacpy committed
				)
			for _ in 0..<TestRuntime as Config>::MaxFreeHeadersPerBlock::get() + 1 {
				assert_err!(submit_invalid_request(), <Error<TestRuntime>>::InvalidJustification);
			}

			// Can still submit free mandatory headers afterwards
			let result = submit_mandatory_finality_proof(1, 1);
			assert_eq!(result.expect("call failed").pays_fee, Pays::No);

			let result = submit_mandatory_finality_proof(2, 2);
			assert_eq!(result.expect("call failed").pays_fee, Pays::No);

			let result = submit_mandatory_finality_proof(3, 3);
			assert_eq!(result.expect("call failed").pays_fee, Pays::Yes);
	fn rate_limiter_allows_request_after_new_block_has_started() {
		run_test(|| {
			initialize_substrate_bridge();

			let result = submit_mandatory_finality_proof(1, 1);
			assert_eq!(result.expect("call failed").pays_fee, Pays::No);
			let result = submit_mandatory_finality_proof(2, 2);
			assert_eq!(result.expect("call failed").pays_fee, Pays::No);

			let result = submit_mandatory_finality_proof(3, 3);
			assert_eq!(result.expect("call failed").pays_fee, Pays::Yes);

			let result = submit_mandatory_finality_proof(4, 4);
			assert_eq!(result.expect("call failed").pays_fee, Pays::No);

			let result = submit_mandatory_finality_proof(5, 5);
			assert_eq!(result.expect("call failed").pays_fee, Pays::No);

			let result = submit_mandatory_finality_proof(6, 6);
			assert_eq!(result.expect("call failed").pays_fee, Pays::Yes);
	fn rate_limiter_ignores_non_mandatory_headers() {
		run_test(|| {
			initialize_substrate_bridge();

			let result = submit_finality_proof(1);
			assert_eq!(result.expect("call failed").pays_fee, Pays::Yes);
			let result = submit_mandatory_finality_proof(2, 1);
			assert_eq!(result.expect("call failed").pays_fee, Pays::No);

			let result = submit_finality_proof_with_set_id(3, 2);
			assert_eq!(result.expect("call failed").pays_fee, Pays::Yes);

			let result = submit_mandatory_finality_proof(4, 2);
			assert_eq!(result.expect("call failed").pays_fee, Pays::No);

			let result = submit_finality_proof_with_set_id(5, 3);
			assert_eq!(result.expect("call failed").pays_fee, Pays::Yes);

			let result = submit_mandatory_finality_proof(6, 3);
			assert_eq!(result.expect("call failed").pays_fee, Pays::Yes);
	#[test]
	fn may_import_non_mandatory_header_for_free() {
		run_test(|| {
			initialize_substrate_bridge();

			// set best finalized to `100`
			const BEST: u8 = 12;
			fn reset_best() {
				BestFinalized::<TestRuntime, ()>::set(Some(HeaderId(
					BEST as _,
					Default::default(),
				)));
			}

			// non-mandatory header is imported with fee
			reset_best();
			let non_free_header_number = BEST + FreeHeadersInterval::get() as u8 - 1;
			let result = submit_finality_proof(non_free_header_number);
			assert_eq!(result.unwrap().pays_fee, Pays::Yes);

			// non-mandatory free header is imported without fee
			reset_best();
			let free_header_number = BEST + FreeHeadersInterval::get() as u8;
			let result = submit_finality_proof(free_header_number);
			assert_eq!(result.unwrap().pays_fee, Pays::No);

			// another non-mandatory free header is imported without fee
			let free_header_number = BEST + FreeHeadersInterval::get() as u8 * 2;
			let result = submit_finality_proof(free_header_number);
			assert_eq!(result.unwrap().pays_fee, Pays::No);

			// now the rate limiter starts charging fees even for free headers
			let free_header_number = BEST + FreeHeadersInterval::get() as u8 * 3;
			let result = submit_finality_proof(free_header_number);
			assert_eq!(result.unwrap().pays_fee, Pays::Yes);

			// check that we can import for free if `improved_by` is larger
			// than the free interval
			next_block();
			reset_best();
			let free_header_number = FreeHeadersInterval::get() as u8 + 42;
			let result = submit_finality_proof(free_header_number);
			assert_eq!(result.unwrap().pays_fee, Pays::No);

			// check that the rate limiter shares the counter between mandatory
			// and free non-mandatory headers
			next_block();
			reset_best();
			let free_header_number = BEST + FreeHeadersInterval::get() as u8 * 4;
			let result = submit_finality_proof(free_header_number);
			assert_eq!(result.unwrap().pays_fee, Pays::No);
			let result = submit_mandatory_finality_proof(free_header_number + 1, 1);
			assert_eq!(result.expect("call failed").pays_fee, Pays::No);
			let result = submit_mandatory_finality_proof(free_header_number + 2, 2);
			assert_eq!(result.expect("call failed").pays_fee, Pays::Yes);
		});
	}

	#[test]
	fn should_prune_headers_over_headers_to_keep_parameter() {
		run_test(|| {
			initialize_substrate_bridge();
			assert_ok!(submit_finality_proof(1));
			let first_header_hash = Pallet::<TestRuntime>::best_finalized().unwrap().hash();
			next_block();

			assert_ok!(submit_finality_proof(2));
			next_block();
			assert_ok!(submit_finality_proof(3));
			next_block();
			assert_ok!(submit_finality_proof(4));
			next_block();
			assert_ok!(submit_finality_proof(5));
			next_block();

			assert_ok!(submit_finality_proof(6));

			assert!(
				!ImportedHeaders::<TestRuntime, ()>::contains_key(first_header_hash),
				"First header should be pruned.",
			PalletOperatingMode::<TestRuntime>::storage_value_final_key().to_vec(),
			bp_header_chain::storage_keys::pallet_operating_mode_key("Grandpa").0,
		assert_eq!(
			CurrentAuthoritySet::<TestRuntime>::storage_value_final_key().to_vec(),
			bp_header_chain::storage_keys::current_authority_set_key("Grandpa").0,
		);

		assert_eq!(
			BestFinalized::<TestRuntime>::storage_value_final_key().to_vec(),
			bp_header_chain::storage_keys::best_finalized_key("Grandpa").0,
	#[test]
	fn test_bridge_grandpa_call_is_correctly_defined() {
		let header = test_header(0);
		let init_data = InitializationData {
			header: Box::new(header.clone()),
			authority_list: authority_list(),
			set_id: 1,
			operating_mode: BasicOperatingMode::Normal,
		};
		let justification = make_default_justification(&header);

		let direct_initialize_call =
			Call::<TestRuntime>::initialize { init_data: init_data.clone() };
		let indirect_initialize_call = BridgeGrandpaCall::<TestHeader>::initialize { init_data };
		assert_eq!(direct_initialize_call.encode(), indirect_initialize_call.encode());

		let direct_submit_finality_proof_call = Call::<TestRuntime>::submit_finality_proof {
			finality_target: Box::new(header.clone()),
			justification: justification.clone(),
		};
		let indirect_submit_finality_proof_call =
			BridgeGrandpaCall::<TestHeader>::submit_finality_proof {
				finality_target: Box::new(header),
				justification,
			};
		assert_eq!(
			direct_submit_finality_proof_call.encode(),
			indirect_submit_finality_proof_call.encode()
		);
	}

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

	#[test]
	fn maybe_headers_to_keep_returns_correct_value() {
		assert_eq!(MaybeHeadersToKeep::<TestRuntime, ()>::get(), Some(mock::HeadersToKeep::get()));
	}

	#[test]
	fn submit_finality_proof_requires_signed_origin() {
		run_test(|| {
			initialize_substrate_bridge();

			let header = test_header(1);
			let justification = make_default_justification(&header);

			assert_noop!(
				Pallet::<TestRuntime>::submit_finality_proof_ex(

	#[test]
	fn on_free_header_imported_never_sets_to_none() {
		run_test(|| {
			FreeHeadersRemaining::<TestRuntime, ()>::set(Some(2));
			on_free_header_imported::<TestRuntime, ()>();
			assert_eq!(FreeHeadersRemaining::<TestRuntime, ()>::get(), Some(1));
			on_free_header_imported::<TestRuntime, ()>();
			assert_eq!(FreeHeadersRemaining::<TestRuntime, ()>::get(), Some(0));
			on_free_header_imported::<TestRuntime, ()>();
			assert_eq!(FreeHeadersRemaining::<TestRuntime, ()>::get(), Some(0));
		})
	}