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	#[allow(deprecated)]
184	#[pallet::validate_unsigned]
185	impl<T: Config> ValidateUnsigned for Pallet<T> {
186		type Call = Call<T>;
187		fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
188			if let Call::submit_unsigned { paged_solution, .. } = call {
189				match source {
190					TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ },
191					_ => return InvalidTransaction::Call.into(),
192				}
193
194				let _ = Self::validate_unsigned_checks(paged_solution.as_ref())
195					.map_err(|err| {
196						sublog!(
197							debug,
198							"unsigned",
199							"unsigned transaction validation failed due to {:?}",
200							err
201						);
202						err
203					})
204					.map_err(base_error_to_invalid)?;
205
206				ValidTransaction::with_tag_prefix("OffchainElection")
207					// The higher the score.minimal_stake, the better a paged_solution is.
208					.priority(
209						T::MinerTxPriority::get()
210							.saturating_add(paged_solution.score.minimal_stake.saturated_into()),
211					)
212					// Used to deduplicate unsigned solutions: each validator should produce one
213					// paged_solution per round at most, and solutions are not propagate.
214					.and_provides(paged_solution.round)
215					// Transaction should stay in the pool for the duration of the unsigned phase.
216					.longevity(T::UnsignedPhase::get().saturated_into::<u64>())
217					// We don't propagate this. This can never be validated at a remote node.
218					.propagate(false)
219					.build()
220			} else {
221				InvalidTransaction::Call.into()
222			}
223		}
224
225		fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
226			if let Call::submit_unsigned { paged_solution, .. } = call {
227				Self::validate_unsigned_checks(paged_solution.as_ref())
228					.map_err(base_error_to_invalid)
229					.map_err(Into::into)
230			} else {
231				Err(InvalidTransaction::Call.into())
232			}
233		}
234	}
235
236	#[pallet::hooks]
237	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
238		fn integrity_test() {
239			assert!(
240				UnsignedWeightsOf::<T>::submit_unsigned().all_lte(T::BlockWeights::get().max_block),
241				"weight of `submit_unsigned` is too high"
242			);
243			assert!(
244				<T as Config>::MinerPages::get() as usize <=
245					<T as crate::Config>::Pages::get() as usize,
246				"number of pages in the unsigned phase is too high"
247			);
248		}
249
250		#[cfg(feature = "try-runtime")]
251		fn try_state(now: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
252			Self::do_try_state(now)
253		}
254
255		fn offchain_worker(now: BlockNumberFor<T>) {
256			use sp_runtime::offchain::storage_lock::{BlockAndTime, StorageLock};
257
258			// Create a lock with the maximum deadline of number of blocks in the unsigned phase.
259			// This should only come useful in an **abrupt** termination of execution, otherwise the
260			// guard will be dropped upon successful execution.
261			let mut lock =
262				StorageLock::<BlockAndTime<frame_system::Pallet<T>>>::with_block_deadline(
263					miner::OffchainWorkerMiner::<T>::OFFCHAIN_LOCK,
264					T::UnsignedPhase::get().saturated_into(),
265				);
266
267			match lock.try_lock() {
268				Ok(_guard) => {
269					Self::do_synchronized_offchain_worker(now);
270				},
271				Err(deadline) => {
272					sublog!(
273						trace,
274						"unsigned",
275						"offchain worker lock not released, deadline is {:?}",
276						deadline
277					);
278				},
279			};
280		}
281	}
282
283	impl<T: Config> Pallet<T> {
284		/// Internal logic of the offchain worker, to be executed only when the offchain lock is
285		/// acquired with success.
286		fn do_synchronized_offchain_worker(now: BlockNumberFor<T>) {
287			use miner::OffchainWorkerMiner;
288			let current_phase = crate::Pallet::<T>::current_phase();
289			sublog!(
290				trace,
291				"unsigned",
292				"lock for offchain worker acquired. Phase = {:?}",
293				current_phase
294			);
295
296			// do the repeat frequency check just one, if we are in unsigned phase.
297			if current_phase.is_unsigned() {
298				if let Err(reason) = OffchainWorkerMiner::<T>::ensure_offchain_repeat_frequency(now)
299				{
300					sublog!(
301						debug,
302						"unsigned",
303						"offchain worker repeat frequency check failed: {:?}",
304						reason
305					);
306					return;
307				}
308			}
309
310			if current_phase.is_unsigned_opened_now() {
311				// Mine a new solution, (maybe) cache it, and attempt to submit it
312				let initial_output = if T::OffchainStorage::get() {
313					OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(true)
314				} else {
315					OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(false)
316				};
317				sublog!(debug, "unsigned", "initial offchain worker output: {:?}", initial_output);
318			} else if current_phase.is_unsigned() {
319				// Maybe resubmit the cached solution, else re-compute.
320				let resubmit_output = if T::OffchainStorage::get() {
321					OffchainWorkerMiner::<T>::restore_or_compute_then_maybe_submit()
322				} else {
323					OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(false)
324				};
325				sublog!(debug, "unsigned", "later offchain worker output: {:?}", resubmit_output);
326			};
327		}
328
329		/// The checks that should happen in the `ValidateUnsigned`'s `pre_dispatch` and
330		/// `validate_unsigned` functions.
331		///
332		/// These check both for snapshot independent checks, and some checks that are specific to
333		/// the unsigned phase.
334		pub(crate) fn validate_unsigned_checks(
335			paged_solution: &PagedRawSolution<T::MinerConfig>,
336		) -> Result<(), CommonError> {
337			Self::unsigned_specific_checks(paged_solution)
338				.and(crate::Pallet::<T>::snapshot_independent_checks(paged_solution, None))
339				.map_err(Into::into)
340		}
341
342		/// The checks that are specific to the (this) unsigned pallet.
343		///
344		/// ensure solution has the correct phase, and it has only 1 page.
345		pub fn unsigned_specific_checks(
346			paged_solution: &PagedRawSolution<T::MinerConfig>,
347		) -> Result<(), CommonError> {
348			ensure!(
349				crate::Pallet::<T>::current_phase().is_unsigned(),
350				CommonError::EarlySubmission
351			);
352			ensure!(
353				paged_solution.solution_pages.len() == T::MinerPages::get() as usize,
354				CommonError::WrongPageCount
355			);
356			ensure!(
357				paged_solution.solution_pages.len() <= <T as crate::Config>::Pages::get() as usize,
358				CommonError::WrongPageCount
359			);
360
361			Ok(())
362		}
363
364		#[cfg(any(test, feature = "runtime-benchmarks", feature = "try-runtime"))]
365		pub(crate) fn do_try_state(
366			_now: BlockNumberFor<T>,
367		) -> Result<(), sp_runtime::TryRuntimeError> {
368			Ok(())
369		}
370	}
371}
372
373#[cfg(test)]
374#[allow(deprecated)]
375mod validate_unsigned {
376	use frame_election_provider_support::Support;
377	use frame_support::{
378		pallet_prelude::InvalidTransaction,
379		unsigned::{TransactionSource, TransactionValidityError, ValidateUnsigned},
380	};
381
382	use super::Call;
383	use crate::{mock::*, types::*, verifier::Verifier};
384
385	#[test]
386	fn retracts_weak_score_accepts_better() {
387		ExtBuilder::mock_signed().build_and_execute(|| {
388			roll_to_snapshot_created();
389
390			let base_minimal_stake = 55;
391			let solution = mine_full_solution().unwrap();
392			load_mock_signed_and_start(solution.clone());
393			roll_to_full_verification();
394
395			// Some good solution is queued now.
396			assert_eq!(
397				<VerifierPallet as Verifier>::queued_score(),
398				Some(ElectionScore {
399					minimal_stake: base_minimal_stake,
400					sum_stake: 130,
401					sum_stake_squared: 8650
402				})
403			);
404
405			roll_to_unsigned_open();
406
407			// This is just worse.
408			let attempt = fake_solution(ElectionScore {
409				minimal_stake: base_minimal_stake - 1,
410				..Default::default()
411			});
412			let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
413			assert_eq!(
414				UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
415				TransactionValidityError::Invalid(InvalidTransaction::Custom(2)),
416			);
417
418			// This is better, but the number of winners is incorrect.
419			let attempt = fake_solution(ElectionScore {
420				minimal_stake: base_minimal_stake + 1,
421				..Default::default()
422			});
423			let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
424			assert_eq!(
425				UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
426				TransactionValidityError::Invalid(InvalidTransaction::Custom(4)),
427			);
428
429			// Note that we now have to use a solution with 2 winners, just to pass all of the
430			// snapshot independent checks.
431			let mut paged = raw_paged_from_supports(
432				vec![vec![
433					(40, Support { total: 10, voters: vec![(3, 5)] }),
434					(30, Support { total: 10, voters: vec![(3, 5)] }),
435				]],
436				0,
437			);
438
439			paged.score =
440				ElectionScore { minimal_stake: base_minimal_stake + 1, ..Default::default() };
441			let call = Call::submit_unsigned { paged_solution: Box::new(paged) };
442			assert!(UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).is_ok());
443		})
444	}
445
446	#[test]
447	fn retracts_wrong_round() {
448		ExtBuilder::mock_signed().build_and_execute(|| {
449			roll_to_unsigned_open();
450
451			let mut attempt =
452				fake_solution(ElectionScore { minimal_stake: 5, ..Default::default() });
453			attempt.round += 1;
454			let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
455
456			assert_eq!(
457				UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
458				// WrongRound is index 1
459				TransactionValidityError::Invalid(InvalidTransaction::Custom(1)),
460			);
461		})
462	}
463
464	#[test]
465	fn retracts_too_many_pages_unsigned() {
466		ExtBuilder::mock_signed().build_and_execute(|| {
467			// NOTE: unsigned solutions should have just 1 page, regardless of the configured
468			// page count.
469			roll_to_unsigned_open();
470			let attempt = mine_full_solution().unwrap();
471			let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
472
473			assert_eq!(
474				UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
475				// WrongPageCount is index 3
476				TransactionValidityError::Invalid(InvalidTransaction::Custom(3)),
477			);
478
479			let attempt = mine_solution(2).unwrap();
480			let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
481
482			assert_eq!(
483				UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
484				TransactionValidityError::Invalid(InvalidTransaction::Custom(3)),
485			);
486
487			let attempt = mine_solution(1).unwrap();
488			let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
489
490			assert!(UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).is_ok(),);
491		})
492	}
493
494	#[test]
495	fn retracts_wrong_winner_count() {
496		ExtBuilder::mock_signed().desired_targets(2).build_and_execute(|| {
497			roll_to_unsigned_open();
498
499			let paged = raw_paged_from_supports(
500				vec![vec![(40, Support { total: 10, voters: vec![(3, 10)] })]],
501				0,
502			);
503
504			let call = Call::submit_unsigned { paged_solution: Box::new(paged) };
505
506			assert_eq!(
507				UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
508				// WrongWinnerCount is index 4
509				TransactionValidityError::Invalid(InvalidTransaction::Custom(4)),
510			);
511		});
512	}
513
514	#[test]
515	fn retracts_wrong_phase() {
516		ExtBuilder::mock_signed().signed_phase(5, 6).build_and_execute(|| {
517			let solution = raw_paged_solution_low_score();
518			let call = Call::submit_unsigned { paged_solution: Box::new(solution.clone()) };
519
520			// initial
521			assert_eq!(MultiBlock::current_phase(), Phase::Off);
522			assert!(matches!(
523				<UnsignedPallet as ValidateUnsigned>::validate_unsigned(
524					TransactionSource::Local,
525					&call
526				)
527				.unwrap_err(),
528				// because EarlySubmission is index 0.
529				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
530			));
531			assert!(matches!(
532				<UnsignedPallet as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
533				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
534			));
535
536			// signed
537			roll_to_signed_open();
538			assert!(MultiBlock::current_phase().is_signed());
539			assert!(matches!(
540				<UnsignedPallet as ValidateUnsigned>::validate_unsigned(
541					TransactionSource::Local,
542					&call
543				)
544				.unwrap_err(),
545				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
546			));
547			assert!(matches!(
548				<UnsignedPallet as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
549				TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
550			));
551
552			// unsigned
553			roll_to_unsigned_open();
554			assert!(MultiBlock::current_phase().is_unsigned());
555
556			assert_ok!(<UnsignedPallet as ValidateUnsigned>::validate_unsigned(
557				TransactionSource::Local,
558				&call
559			));
560			assert_ok!(<UnsignedPallet as ValidateUnsigned>::pre_dispatch(&call));
561		})
562	}
563
564	#[test]
565	fn priority_is_set() {
566		ExtBuilder::mock_signed()
567			.miner_tx_priority(20)
568			.desired_targets(0)
569			.build_and_execute(|| {
570				roll_to_unsigned_open();
571				assert!(MultiBlock::current_phase().is_unsigned());
572
573				let solution =
574					fake_solution(ElectionScore { minimal_stake: 5, ..Default::default() });
575				let call = Call::submit_unsigned { paged_solution: Box::new(solution.clone()) };
576
577				assert_eq!(
578					<UnsignedPallet as ValidateUnsigned>::validate_unsigned(
579						TransactionSource::Local,
580						&call
581					)
582					.unwrap()
583					.priority,
584					25
585				);
586			})
587	}
588}
589
590#[cfg(test)]
591mod call {
592	use crate::{mock::*, verifier::Verifier, Snapshot};
593
594	#[test]
595	fn unsigned_submission_e2e() {
596		let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
597		ext.execute_with_sanity_checks(|| {
598			roll_to_unsigned_open();
599
600			// snapshot is created..
601			assert_full_snapshot();
602			// ..txpool is empty..
603			assert_eq!(pool.read().transactions.len(), 0);
604			// ..but nothing queued.
605			assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
606
607			// now the OCW should submit something.
608			roll_next_with_ocw(Some(pool.clone()));
609			assert_eq!(pool.read().transactions.len(), 1);
610			assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
611
612			// and now it should be applied.
613			roll_next_with_ocw(Some(pool.clone()));
614			assert_eq!(pool.read().transactions.len(), 0);
615			assert!(matches!(<VerifierPallet as Verifier>::queued_score(), Some(_)));
616		})
617	}
618
619	#[test]
620	#[should_panic(
621		expected = "Invalid unsigned submission must produce invalid block and deprive validator from their authoring reward."
622	)]
623	fn unfeasible_solution_panics() {
624		let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
625		ext.execute_with_sanity_checks(|| {
626			roll_to_unsigned_open();
627
628			// snapshot is created..
629			assert_full_snapshot();
630			// ..txpool is empty..
631			assert_eq!(pool.read().transactions.len(), 0);
632			// ..but nothing queued.
633			assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
634
635			// now the OCW should submit something.
636			roll_next_with_ocw(Some(pool.clone()));
637			assert_eq!(pool.read().transactions.len(), 1);
638			assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
639
640			// now we change the snapshot -- this should ensure that the solution becomes invalid.
641			// Note that we don't change the known fingerprint of the solution.
642			Snapshot::<Runtime>::remove_target(2);
643
644			// and now it should be applied.
645			roll_next_with_ocw(Some(pool.clone()));
646			assert_eq!(pool.read().transactions.len(), 0);
647			assert!(matches!(<VerifierPallet as Verifier>::queued_score(), Some(_)));
648		})
649	}
650}