referrerpolicy=no-referrer-when-downgrade

pallet_election_provider_multi_block/unsigned/
mod.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! ## The unsigned phase, and its miner.
19//!
20//! This pallet deals with unsigned submissions. These are backup, "possibly" multi-page submissions
21//! from validators.
22//!
23//! This pallet has two miners, described in [`unsigned::miner`].
24//!
25//! As it stands, a validator can, during the unsigned phase, submit up to
26//! [`unsigned::Config::MinerPages`] pages. While this can be more than 1, it can likely not be a
27//! full, high quality solution. This is because unsigned validator solutions are verified on the
28//! fly, all within a single block. The exact value of this parameter should be determined by the
29//! benchmarks of a runtime.
30//!
31//! We could implement a protocol to allow multi-block, multi-page collaborative submissions from
32//! different validators, but it is not trivial. Moreover, recall that the unsigned phase is merely
33//! a backup and we should primarily rely on offchain staking miners to fulfill this role during
34//! `Phase::Signed`.
35//!
36//! ## Future Idea: Multi-Page unsigned submission
37//!
38//! the following is the idea of how to implement multi-page unsigned, which we don't have.
39//!
40//! All validators will run their miners and compute the full paginated solution. They submit all
41//! pages as individual unsigned transactions to their local tx-pool.
42//!
43//! Upon validation, if any page is now present the corresponding transaction is dropped.
44//!
45//! At each block, the first page that may be valid is included as a high priority operational
46//! transaction. This page is validated on the fly to be correct. Since this transaction is sourced
47//! from a validator, we can panic if they submit an invalid transaction.
48//!
49//! Then, once the final page is submitted, some extra checks are done, as explained in
50//! [`crate::verifier`]:
51//!
52//! 1. bounds
53//! 2. total score
54//!
55//! These checks might still fail. If they do, the solution is dropped. At this point, we don't know
56//! which validator may have submitted a slightly-faulty solution.
57//!
58//! In order to prevent this, the transaction validation process always includes a check to ensure
59//! all of the previous pages that have been submitted match what the local validator has computed.
60//! If they match, the validator knows that they are putting skin in a game that is valid.
61//!
62//! If any bad paged are detected, the next validator can bail. This process means:
63//!
64//! * As long as all validators are honest, and run the same miner code, a correct solution is
65//!   found.
66//! * As little as one malicious validator can stall the process, but no one is accidentally
67//!   slashed, and no panic happens.
68//!
69//! Alternatively, we can keep track of submitters, and report a slash if it occurs. Or, if
70//! the signed process is bullet-proof, we can be okay with the status quo.
71
72/// Export weights
73pub use crate::weights::traits::pallet_election_provider_multi_block_unsigned::*;
74/// Exports of this pallet
75pub use pallet::*;
76#[cfg(feature = "runtime-benchmarks")]
77mod benchmarking;
78
79/// The miner.
80pub mod miner;
81
82#[frame_support::pallet]
83mod pallet {
84	use super::WeightInfo;
85	use crate::{
86		types::*,
87		unsigned::miner::{self},
88		verifier::Verifier,
89		CommonError,
90	};
91	use frame_support::pallet_prelude::*;
92	use frame_system::{offchain::CreateBare, pallet_prelude::*};
93	use sp_runtime::traits::SaturatedConversion;
94	use sp_std::prelude::*;
95
96	/// convert a [`crate::CommonError`] to a custom InvalidTransaction with the inner code being
97	/// the index of the variant.
98	fn base_error_to_invalid(error: CommonError) -> InvalidTransaction {
99		let index = error.encode().pop().unwrap_or(0);
100		InvalidTransaction::Custom(index)
101	}
102
103	pub(crate) type UnsignedWeightsOf<T> = <T as Config>::WeightInfo;
104
105	#[pallet::config]
106	#[pallet::disable_frame_system_supertrait_check]
107	pub trait Config: crate::Config + CreateBare<Call<Self>> {
108		/// The repeat threshold of the offchain worker.
109		///
110		/// For example, if it is `5`, that means that at least 5 blocks will elapse between
111		/// attempts to submit the worker's solution.
112		type OffchainRepeat: Get<BlockNumberFor<Self>>;
113
114		/// The solver used in hte offchain worker miner
115		type OffchainSolver: frame_election_provider_support::NposSolver<
116			AccountId = Self::AccountId,
117		>;
118
119		/// Whether the offchain worker miner would attempt to store the solutions in a local
120		/// database and reuse then. If set to `false`, it will try and re-mine solutions every
121		/// time.
122		type OffchainStorage: Get<bool>;
123
124		/// The priority of the unsigned transaction submitted in the unsigned-phase
125		type MinerTxPriority: Get<TransactionPriority>;
126
127		/// The number of pages that the offchain miner will try and submit.
128		type MinerPages: Get<PageIndex>;
129
130		/// Runtime weight information of this pallet.
131		type WeightInfo: WeightInfo;
132	}
133
134	#[pallet::pallet]
135	pub struct Pallet<T>(PhantomData<T>);
136
137	#[pallet::call]
138	impl<T: Config> Pallet<T> {
139		/// Submit an unsigned solution.
140		///
141		/// This works very much like an inherent, as only the validators are permitted to submit
142		/// anything. By default validators will compute this call in their `offchain_worker` hook
143		/// and try and submit it back.
144		///
145		/// This is different from signed page submission mainly in that the solution page is
146		/// verified on the fly.
147		///
148		/// The `paged_solution` may contain at most [`Config::MinerPages`] pages. They are
149		/// interpreted as msp -> lsp, as per [`crate::Pallet::msp_range_for`].
150		///
151		/// For example, if `Pages = 4`, and `MinerPages = 2`, our full snapshot range would be [0,
152		/// 1, 2, 3], with 3 being msp. But, in this case, then the `paged_raw_solution.pages` is
153		/// expected to correspond to `[snapshot(2), snapshot(3)]`.
154		#[pallet::weight((UnsignedWeightsOf::<T>::submit_unsigned(), DispatchClass::Operational))]
155		#[pallet::call_index(0)]
156		pub fn submit_unsigned(
157			origin: OriginFor<T>,
158			paged_solution: Box<PagedRawSolution<T::MinerConfig>>,
159		) -> DispatchResultWithPostInfo {
160			ensure_none(origin)?;
161			let error_message = "Invalid unsigned submission must produce invalid block and \
162				 deprive validator from their authoring reward.";
163
164			// phase, round, claimed score, page-count and hash are checked in pre-dispatch. we
165			// don't check them here anymore.
166			debug_assert!(Self::validate_unsigned_checks(&paged_solution).is_ok());
167
168			let claimed_score = paged_solution.score;
169
170			// we select the most significant pages, based on `T::MinerPages`.
171			let page_indices = crate::Pallet::<T>::msp_range_for(T::MinerPages::get() as usize);
172			<T::Verifier as Verifier>::verify_synchronous_multi(
173				paged_solution.solution_pages,
174				page_indices,
175				claimed_score,
176			)
177			.expect(error_message);
178
179			Ok(None.into())
180		}
181	}
182
183	#[pallet::validate_unsigned]
184	impl<T: Config> ValidateUnsigned for Pallet<T> {
185		type Call = Call<T>;
186		fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
187			if let Call::submit_unsigned { paged_solution, .. } = call {
188				match source {
189					TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ },
190					_ => return InvalidTransaction::Call.into(),
191				}
192
193				let _ = Self::validate_unsigned_checks(paged_solution.as_ref())
194					.map_err(|err| {
195						sublog!(
196							debug,
197							"unsigned",
198							"unsigned transaction validation failed due to {:?}",
199							err
200						);
201						err
202					})
203					.map_err(base_error_to_invalid)?;
204
205				ValidTransaction::with_tag_prefix("OffchainElection")
206					// The higher the score.minimal_stake, the better a paged_solution is.
207					.priority(
208						T::MinerTxPriority::get()
209							.saturating_add(paged_solution.score.minimal_stake.saturated_into()),
210					)
211					// Used to deduplicate unsigned solutions: each validator should produce one
212					// paged_solution per round at most, and solutions are not propagate.
213					.and_provides(paged_solution.round)
214					// Transaction should stay in the pool for the duration of the unsigned phase.
215					.longevity(T::UnsignedPhase::get().saturated_into::<u64>())
216					// We don't propagate this. This can never be validated at a remote node.
217					.propagate(false)
218					.build()
219			} else {
220				InvalidTransaction::Call.into()
221			}
222		}
223
224		fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
225			if let Call::submit_unsigned { paged_solution, .. } = call {
226				Self::validate_unsigned_checks(paged_solution.as_ref())
227					.map_err(base_error_to_invalid)
228					.map_err(Into::into)
229			} else {
230				Err(InvalidTransaction::Call.into())
231			}
232		}
233	}
234
235	#[pallet::hooks]
236	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
237		fn integrity_test() {
238			assert!(
239				UnsignedWeightsOf::<T>::submit_unsigned().all_lte(T::BlockWeights::get().max_block),
240				"weight of `submit_unsigned` is too high"
241			);
242			assert!(
243				<T as Config>::MinerPages::get() as usize <=
244					<T as crate::Config>::Pages::get() as usize,
245				"number of pages in the unsigned phase is too high"
246			);
247		}
248
249		#[cfg(feature = "try-runtime")]
250		fn try_state(now: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
251			Self::do_try_state(now)
252		}
253
254		fn offchain_worker(now: BlockNumberFor<T>) {
255			use sp_runtime::offchain::storage_lock::{BlockAndTime, StorageLock};
256
257			// Create a lock with the maximum deadline of number of blocks in the unsigned phase.
258			// This should only come useful in an **abrupt** termination of execution, otherwise the
259			// guard will be dropped upon successful execution.
260			let mut lock =
261				StorageLock::<BlockAndTime<frame_system::Pallet<T>>>::with_block_deadline(
262					miner::OffchainWorkerMiner::<T>::OFFCHAIN_LOCK,
263					T::UnsignedPhase::get().saturated_into(),
264				);
265
266			match lock.try_lock() {
267				Ok(_guard) => {
268					Self::do_synchronized_offchain_worker(now);
269				},
270				Err(deadline) => {
271					sublog!(
272						trace,
273						"unsigned",
274						"offchain worker lock not released, deadline is {:?}",
275						deadline
276					);
277				},
278			};
279		}
280	}
281
282	impl<T: Config> Pallet<T> {
283		/// Internal logic of the offchain worker, to be executed only when the offchain lock is
284		/// acquired with success.
285		fn do_synchronized_offchain_worker(now: BlockNumberFor<T>) {
286			use miner::OffchainWorkerMiner;
287			let current_phase = crate::Pallet::<T>::current_phase();
288			sublog!(
289				trace,
290				"unsigned",
291				"lock for offchain worker acquired. Phase = {:?}",
292				current_phase
293			);
294
295			// do the repeat frequency check just one, if we are in unsigned phase.
296			if current_phase.is_unsigned() {
297				if let Err(reason) = OffchainWorkerMiner::<T>::ensure_offchain_repeat_frequency(now)
298				{
299					sublog!(
300						debug,
301						"unsigned",
302						"offchain worker repeat frequency check failed: {:?}",
303						reason
304					);
305					return;
306				}
307			}
308
309			if current_phase.is_unsigned_opened_now() {
310				// Mine a new solution, (maybe) cache it, and attempt to submit it
311				let initial_output = if T::OffchainStorage::get() {
312					OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(true)
313				} else {
314					OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(false)
315				};
316				sublog!(debug, "unsigned", "initial offchain worker output: {:?}", initial_output);
317			} else if current_phase.is_unsigned() {
318				// Maybe resubmit the cached solution, else re-compute.
319				let resubmit_output = if T::OffchainStorage::get() {
320					OffchainWorkerMiner::<T>::restore_or_compute_then_maybe_submit()
321				} else {
322					OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(false)
323				};
324				sublog!(debug, "unsigned", "later offchain worker output: {:?}", resubmit_output);
325			};
326		}
327
328		/// The checks that should happen in the `ValidateUnsigned`'s `pre_dispatch` and
329		/// `validate_unsigned` functions.
330		///
331		/// These check both for snapshot independent checks, and some checks that are specific to
332		/// the unsigned phase.
333		pub(crate) fn validate_unsigned_checks(
334			paged_solution: &PagedRawSolution<T::MinerConfig>,
335		) -> Result<(), CommonError> {
336			Self::unsigned_specific_checks(paged_solution)
337				.and(crate::Pallet::<T>::snapshot_independent_checks(paged_solution, None))
338				.map_err(Into::into)
339		}
340
341		/// The checks that are specific to the (this) unsigned pallet.
342		///
343		/// ensure solution has the correct phase, and it has only 1 page.
344		pub fn unsigned_specific_checks(
345			paged_solution: &PagedRawSolution<T::MinerConfig>,
346		) -> Result<(), CommonError> {
347			ensure!(
348				crate::Pallet::<T>::current_phase().is_unsigned(),
349				CommonError::EarlySubmission
350			);
351			ensure!(
352				paged_solution.solution_pages.len() == T::MinerPages::get() as usize,
353				CommonError::WrongPageCount
354			);
355			ensure!(
356				paged_solution.solution_pages.len() <= <T as crate::Config>::Pages::get() as usize,
357				CommonError::WrongPageCount
358			);
359
360			Ok(())
361		}
362
363		#[cfg(any(test, feature = "runtime-benchmarks", feature = "try-runtime"))]
364		pub(crate) fn do_try_state(
365			_now: BlockNumberFor<T>,
366		) -> Result<(), sp_runtime::TryRuntimeError> {
367			Ok(())
368		}
369	}
370}
371
372#[cfg(test)]
373mod validate_unsigned {
374	use frame_election_provider_support::Support;
375	use frame_support::{
376		pallet_prelude::InvalidTransaction,
377		unsigned::{TransactionSource, TransactionValidityError, ValidateUnsigned},
378	};
379
380	use super::Call;
381	use crate::{mock::*, types::*, verifier::Verifier};
382
383	#[test]
384	fn retracts_weak_score_accepts_threshold_better() {
385		ExtBuilder::unsigned()
386			.solution_improvement_threshold(sp_runtime::Perbill::from_percent(10))
387			.build_and_execute(|| {
388				roll_to_snapshot_created();
389
390				let solution = mine_full_solution().unwrap();
391				load_mock_signed_and_start(solution.clone());
392				roll_to_full_verification();
393
394				// Some good solution is queued now.
395				assert_eq!(
396					<VerifierPallet as Verifier>::queued_score(),
397					Some(ElectionScore {
398						minimal_stake: 55,
399						sum_stake: 130,
400						sum_stake_squared: 8650
401					})
402				);
403
404				roll_to_unsigned_open();
405
406				// this is just worse
407				let attempt =
408					fake_solution(ElectionScore { minimal_stake: 20, ..Default::default() });
409				let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
410				assert_eq!(
411					UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
412					TransactionValidityError::Invalid(InvalidTransaction::Custom(2)),
413				);
414
415				// this is better, but not enough better.
416				let insufficient_improvement = 55 * 105 / 100;
417				let attempt = fake_solution(ElectionScore {
418					minimal_stake: insufficient_improvement,
419					..Default::default()
420				});
421				let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
422				assert_eq!(
423					UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
424					TransactionValidityError::Invalid(InvalidTransaction::Custom(2)),
425				);
426
427				// note that we now have to use a solution with 2 winners, just to pass all of the
428				// snapshot independent checks.
429				let mut paged = raw_paged_from_supports(
430					vec![vec![
431						(40, Support { total: 10, voters: vec![(3, 5)] }),
432						(30, Support { total: 10, voters: vec![(3, 5)] }),
433					]],
434					0,
435				);
436				let sufficient_improvement = 55 * 115 / 100;
437				paged.score =
438					ElectionScore { minimal_stake: sufficient_improvement, ..Default::default() };
439				let call = Call::submit_unsigned { paged_solution: Box::new(paged) };
440				assert!(UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).is_ok());
441			})
442	}
443
444	#[test]
445	fn retracts_wrong_round() {
446		ExtBuilder::unsigned().build_and_execute(|| {
447			roll_to_unsigned_open();
448
449			let mut attempt =
450				fake_solution(ElectionScore { minimal_stake: 5, ..Default::default() });
451			attempt.round += 1;
452			let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
453
454			assert_eq!(
455				UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
456				// WrongRound is index 1
457				TransactionValidityError::Invalid(InvalidTransaction::Custom(1)),
458			);
459		})
460	}
461
462	#[test]
463	fn retracts_too_many_pages_unsigned() {
464		ExtBuilder::unsigned().build_and_execute(|| {
465			// NOTE: unsigned solutions should have just 1 page, regardless of the configured
466			// page count.
467			roll_to_unsigned_open();
468			let attempt = mine_full_solution().unwrap();
469			let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
470
471			assert_eq!(
472				UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
473				// WrongPageCount is index 3
474				TransactionValidityError::Invalid(InvalidTransaction::Custom(3)),
475			);
476
477			let attempt = mine_solution(2).unwrap();
478			let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
479
480			assert_eq!(
481				UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
482				TransactionValidityError::Invalid(InvalidTransaction::Custom(3)),
483			);
484
485			let attempt = mine_solution(1).unwrap();
486			let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
487
488			assert!(UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).is_ok(),);
489		})
490	}
491
492	#[test]
493	fn retracts_wrong_winner_count() {
494		ExtBuilder::unsigned().desired_targets(2).build_and_execute(|| {
495			roll_to_unsigned_open();
496
497			let paged = raw_paged_from_supports(
498				vec![vec![(40, Support { total: 10, voters: vec![(3, 10)] })]],
499				0,
500			);
501
502			let call = Call::submit_unsigned { paged_solution: Box::new(paged) };
503
504			assert_eq!(
505				UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
506				// WrongWinnerCount is index 4
507				TransactionValidityError::Invalid(InvalidTransaction::Custom(4)),
508			);
509		});
510	}
511
512	#[test]
513	fn retracts_wrong_phase() {
514		ExtBuilder::unsigned().signed_phase(5, 6).build_and_execute(|| {
515			let solution = raw_paged_solution_low_score();
516			let call = Call::submit_unsigned { paged_solution: Box::new(solution.clone()) };
517
518			// initial
519			assert_eq!(MultiBlock::current_phase(), Phase::Off);
520			assert!(matches!(
521				<UnsignedPallet as ValidateUnsigned>::validate_unsigned(
522					TransactionSource::Local,
523					&call
524				)
525				.unwrap_err(),
526				// because EarlySubmission is index 0.
527				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
528			));
529			assert!(matches!(
530				<UnsignedPallet as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
531				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
532			));
533
534			// signed
535			roll_to_signed_open();
536			assert!(MultiBlock::current_phase().is_signed());
537			assert!(matches!(
538				<UnsignedPallet as ValidateUnsigned>::validate_unsigned(
539					TransactionSource::Local,
540					&call
541				)
542				.unwrap_err(),
543				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
544			));
545			assert!(matches!(
546				<UnsignedPallet as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
547				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
548			));
549
550			// unsigned
551			roll_to_unsigned_open();
552			assert!(MultiBlock::current_phase().is_unsigned());
553
554			assert_ok!(<UnsignedPallet as ValidateUnsigned>::validate_unsigned(
555				TransactionSource::Local,
556				&call
557			));
558			assert_ok!(<UnsignedPallet as ValidateUnsigned>::pre_dispatch(&call));
559		})
560	}
561
562	#[test]
563	fn priority_is_set() {
564		ExtBuilder::unsigned()
565			.miner_tx_priority(20)
566			.desired_targets(0)
567			.build_and_execute(|| {
568				roll_to_unsigned_open();
569				assert!(MultiBlock::current_phase().is_unsigned());
570
571				let solution =
572					fake_solution(ElectionScore { minimal_stake: 5, ..Default::default() });
573				let call = Call::submit_unsigned { paged_solution: Box::new(solution.clone()) };
574
575				assert_eq!(
576					<UnsignedPallet as ValidateUnsigned>::validate_unsigned(
577						TransactionSource::Local,
578						&call
579					)
580					.unwrap()
581					.priority,
582					25
583				);
584			})
585	}
586}
587
588#[cfg(test)]
589mod call {
590	use crate::{mock::*, verifier::Verifier, Snapshot};
591
592	#[test]
593	fn unsigned_submission_e2e() {
594		let (mut ext, pool) = ExtBuilder::unsigned().build_offchainify();
595		ext.execute_with_sanity_checks(|| {
596			roll_to_unsigned_open();
597
598			// snapshot is created..
599			assert_full_snapshot();
600			// ..txpool is empty..
601			assert_eq!(pool.read().transactions.len(), 0);
602			// ..but nothing queued.
603			assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
604
605			// now the OCW should submit something.
606			roll_next_with_ocw(Some(pool.clone()));
607			assert_eq!(pool.read().transactions.len(), 1);
608			assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
609
610			// and now it should be applied.
611			roll_next_with_ocw(Some(pool.clone()));
612			assert_eq!(pool.read().transactions.len(), 0);
613			assert!(matches!(<VerifierPallet as Verifier>::queued_score(), Some(_)));
614		})
615	}
616
617	#[test]
618	#[should_panic(
619		expected = "Invalid unsigned submission must produce invalid block and deprive validator from their authoring reward."
620	)]
621	fn unfeasible_solution_panics() {
622		let (mut ext, pool) = ExtBuilder::unsigned().build_offchainify();
623		ext.execute_with_sanity_checks(|| {
624			roll_to_unsigned_open();
625
626			// snapshot is created..
627			assert_full_snapshot();
628			// ..txpool is empty..
629			assert_eq!(pool.read().transactions.len(), 0);
630			// ..but nothing queued.
631			assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
632
633			// now the OCW should submit something.
634			roll_next_with_ocw(Some(pool.clone()));
635			assert_eq!(pool.read().transactions.len(), 1);
636			assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
637
638			// now we change the snapshot -- this should ensure that the solution becomes invalid.
639			// Note that we don't change the known fingerprint of the solution.
640			Snapshot::<Runtime>::remove_target(2);
641
642			// and now it should be applied.
643			roll_next_with_ocw(Some(pool.clone()));
644			assert_eq!(pool.read().transactions.len(), 0);
645			assert!(matches!(<VerifierPallet as Verifier>::queued_score(), Some(_)));
646		})
647	}
648}