From 86f14eed076298066a085838d3a512c68c03e5bd Mon Sep 17 00:00:00 2001
From: Lldenaurois <ljdenaurois@gmail.com>
Date: Wed, 4 Aug 2021 11:49:21 -0400
Subject: [PATCH] node/approval-voting: Continue to migrate tests to subsystem
 tests (#3471)

* node/approval-voting: Continue to migrate tests to subsystem tests

* node/approval-voting: Continue to implement subsystem tests.

* Add more tests

* node/approval-voting: Add more tests

* node/approval-voting: Difficuly determining the proper test scenario for threshold tests

* node/approval-voting: Introduce should_trigger_assignment tests

* node/approval-voting: Finalize threshold tests

* node/approval-voting: Address Feedback and add comments

* node/approval-voting: Tidy up approval tests

* Tidy up tests

* Fix rustfmt
---
 polkadot/node/core/approval-voting/src/lib.rs |   13 +-
 .../core/approval-voting/src/old_tests.rs     | 1925 --------------
 .../approval-voting/src/persisted_entries.rs  |   31 -
 .../node/core/approval-voting/src/tests.rs    | 2355 +++++++++++++----
 4 files changed, 1862 insertions(+), 2462 deletions(-)
 delete mode 100644 polkadot/node/core/approval-voting/src/old_tests.rs

diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs
index d168f43b5bf..0d5c745fb90 100644
--- a/polkadot/node/core/approval-voting/src/lib.rs
+++ b/polkadot/node/core/approval-voting/src/lib.rs
@@ -92,9 +92,6 @@ use crate::{
 #[cfg(test)]
 mod tests;
 
-#[cfg(test)]
-mod old_tests;
-
 const APPROVAL_SESSIONS: SessionIndex = 6;
 const APPROVAL_CHECKING_TIMEOUT: Duration = Duration::from_secs(120);
 const APPROVAL_CACHE_SIZE: usize = 1024;
@@ -455,8 +452,9 @@ impl Wakeups {
 			Some(tick) => {
 				clock.wait(tick).await;
 				match self.wakeups.entry(tick) {
-					Entry::Vacant(_) =>
-						panic!("entry is known to exist since `first` was `Some`; qed"),
+					Entry::Vacant(_) => {
+						panic!("entry is known to exist since `first` was `Some`; qed")
+					},
 					Entry::Occupied(mut entry) => {
 						let (hash, candidate_hash) = entry.get_mut().pop()
 							.expect("empty entries are removed here and in `schedule`; no other mutation of this map; qed");
@@ -919,8 +917,9 @@ async fn handle_actions(
 				.await;
 
 				match confirmation_rx.await {
-					Err(oneshot::Canceled) =>
-						tracing::warn!(target: LOG_TARGET, "Dispute coordinator confirmation lost",),
+					Err(oneshot::Canceled) => {
+						tracing::warn!(target: LOG_TARGET, "Dispute coordinator confirmation lost",)
+					},
 					Ok(ImportStatementsResult::ValidImport) => {},
 					Ok(ImportStatementsResult::InvalidImport) => tracing::warn!(
 						target: LOG_TARGET,
diff --git a/polkadot/node/core/approval-voting/src/old_tests.rs b/polkadot/node/core/approval-voting/src/old_tests.rs
deleted file mode 100644
index a122bce7df8..00000000000
--- a/polkadot/node/core/approval-voting/src/old_tests.rs
+++ /dev/null
@@ -1,1925 +0,0 @@
-// Copyright 2021 Parity Technologies (UK) Ltd.
-// This file is part of Polkadot.
-
-// Polkadot is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// Polkadot is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
-
-use super::{
-	approval_db::v1::Config,
-	backend::{Backend, BackendWriteOp},
-	*,
-};
-use polkadot_node_primitives::approval::{
-	AssignmentCert, AssignmentCertKind, DelayTranche, VRFOutput, VRFProof, RELAY_VRF_MODULO_CONTEXT,
-};
-use polkadot_node_subsystem::messages::{AllMessages, HighestApprovedAncestorBlock};
-use polkadot_node_subsystem_test_helpers::make_subsystem_context;
-use polkadot_primitives::v1::{
-	CandidateDescriptor, CoreIndex, DisputeStatement, GroupIndex, ValidDisputeStatementKind,
-	ValidatorSignature,
-};
-use sp_core::testing::TaskExecutor;
-
-use assert_matches::assert_matches;
-use bitvec::order::Lsb0 as BitOrderLsb0;
-use parking_lot::Mutex;
-use sp_keyring::sr25519::Keyring as Sr25519Keyring;
-use std::{pin::Pin, sync::Arc};
-
-const SLOT_DURATION_MILLIS: u64 = 5000;
-
-const DATA_COL: u32 = 0;
-const NUM_COLUMNS: u32 = 1;
-
-const TEST_CONFIG: Config = Config { col_data: DATA_COL };
-
-fn make_db() -> DbBackend {
-	let db_writer: Arc<dyn KeyValueDB> = Arc::new(kvdb_memorydb::create(NUM_COLUMNS));
-	DbBackend::new(db_writer.clone(), TEST_CONFIG)
-}
-
-fn overlay_txn<T, F>(db: &mut T, mut f: F)
-where
-	T: Backend,
-	F: FnMut(&mut OverlayedBackend<'_, T>),
-{
-	let mut overlay_db = OverlayedBackend::new(db);
-	f(&mut overlay_db);
-	let write_ops = overlay_db.into_write_ops();
-	db.write(write_ops).unwrap();
-}
-
-fn slot_to_tick(t: impl Into<Slot>) -> crate::time::Tick {
-	crate::time::slot_number_to_tick(SLOT_DURATION_MILLIS, t.into())
-}
-
-#[derive(Default, Clone)]
-struct MockClock {
-	inner: Arc<Mutex<MockClockInner>>,
-}
-
-impl MockClock {
-	fn new(tick: Tick) -> Self {
-		let me = Self::default();
-		me.inner.lock().set_tick(tick);
-		me
-	}
-}
-
-impl Clock for MockClock {
-	fn tick_now(&self) -> Tick {
-		self.inner.lock().tick
-	}
-
-	fn wait(&self, tick: Tick) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> {
-		let rx = self.inner.lock().register_wakeup(tick, true);
-
-		Box::pin(async move {
-			rx.await.expect("i exist in a timeless void. yet, i remain");
-		})
-	}
-}
-
-// This mock clock allows us to manipulate the time and
-// be notified when wakeups have been triggered.
-#[derive(Default)]
-struct MockClockInner {
-	tick: Tick,
-	wakeups: Vec<(Tick, oneshot::Sender<()>)>,
-}
-
-impl MockClockInner {
-	fn set_tick(&mut self, tick: Tick) {
-		self.tick = tick;
-		self.wakeup_all(tick);
-	}
-
-	fn wakeup_all(&mut self, up_to: Tick) {
-		// This finds the position of the first wakeup after
-		// the given tick, or the end of the map.
-		let drain_up_to =
-			self.wakeups.binary_search_by_key(&(up_to + 1), |w| w.0).unwrap_or_else(|i| i);
-
-		for (_, wakeup) in self.wakeups.drain(..drain_up_to) {
-			let _ = wakeup.send(());
-		}
-	}
-
-	// If `pre_emptive` is true, we compare the given tick to the internal
-	// tick of the clock for an early return.
-	//
-	// Otherwise, the wakeup will only trigger alongside another wakeup of
-	// equal or greater tick.
-	//
-	// When the pre-emptive wakeup is disabled, this can be used in combination with
-	// a preceding call to `set_tick` to wait until some other wakeup at that same tick
-	//  has been triggered.
-	fn register_wakeup(&mut self, tick: Tick, pre_emptive: bool) -> oneshot::Receiver<()> {
-		let (tx, rx) = oneshot::channel();
-
-		let pos = self.wakeups.binary_search_by_key(&tick, |w| w.0).unwrap_or_else(|i| i);
-
-		self.wakeups.insert(pos, (tick, tx));
-
-		if pre_emptive {
-			// if `tick > self.tick`, this won't wake up the new
-			// listener.
-			self.wakeup_all(self.tick);
-		}
-
-		rx
-	}
-}
-
-struct MockAssignmentCriteria<Compute, Check>(Compute, Check);
-
-impl<Compute, Check> AssignmentCriteria for MockAssignmentCriteria<Compute, Check>
-where
-	Compute: Fn() -> HashMap<polkadot_primitives::v1::CoreIndex, criteria::OurAssignment>,
-	Check: Fn() -> Result<DelayTranche, criteria::InvalidAssignment>,
-{
-	fn compute_assignments(
-		&self,
-		_keystore: &LocalKeystore,
-		_relay_vrf_story: polkadot_node_primitives::approval::RelayVRFStory,
-		_config: &criteria::Config,
-		_leaving_cores: Vec<(
-			CandidateHash,
-			polkadot_primitives::v1::CoreIndex,
-			polkadot_primitives::v1::GroupIndex,
-		)>,
-	) -> HashMap<polkadot_primitives::v1::CoreIndex, criteria::OurAssignment> {
-		self.0()
-	}
-
-	fn check_assignment_cert(
-		&self,
-		_claimed_core_index: polkadot_primitives::v1::CoreIndex,
-		_validator_index: ValidatorIndex,
-		_config: &criteria::Config,
-		_relay_vrf_story: polkadot_node_primitives::approval::RelayVRFStory,
-		_assignment: &polkadot_node_primitives::approval::AssignmentCert,
-		_backing_group: polkadot_primitives::v1::GroupIndex,
-	) -> Result<polkadot_node_primitives::approval::DelayTranche, criteria::InvalidAssignment> {
-		self.1()
-	}
-}
-
-impl<F>
-	MockAssignmentCriteria<
-		fn() -> HashMap<polkadot_primitives::v1::CoreIndex, criteria::OurAssignment>,
-		F,
-	>
-{
-	fn check_only(f: F) -> Self {
-		MockAssignmentCriteria(Default::default, f)
-	}
-}
-
-fn blank_state() -> State {
-	State {
-		session_window: RollingSessionWindow::new(APPROVAL_SESSIONS),
-		keystore: Arc::new(LocalKeystore::in_memory()),
-		slot_duration_millis: SLOT_DURATION_MILLIS,
-		clock: Box::new(MockClock::default()),
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))),
-	}
-}
-
-fn single_session_state(index: SessionIndex, info: SessionInfo) -> State {
-	State {
-		session_window: RollingSessionWindow::with_session_info(
-			APPROVAL_SESSIONS,
-			index,
-			vec![info],
-		),
-		..blank_state()
-	}
-}
-
-fn garbage_assignment_cert(kind: AssignmentCertKind) -> AssignmentCert {
-	let ctx = schnorrkel::signing_context(RELAY_VRF_MODULO_CONTEXT);
-	let msg = b"test-garbage";
-	let mut prng = rand_core::OsRng;
-	let keypair = schnorrkel::Keypair::generate_with(&mut prng);
-	let (inout, proof, _) = keypair.vrf_sign(ctx.bytes(msg));
-	let out = inout.to_output();
-
-	AssignmentCert { kind, vrf: (VRFOutput(out), VRFProof(proof)) }
-}
-
-fn sign_approval(
-	key: Sr25519Keyring,
-	candidate_hash: CandidateHash,
-	session_index: SessionIndex,
-) -> ValidatorSignature {
-	key.sign(&ApprovalVote(candidate_hash).signing_payload(session_index)).into()
-}
-
-#[derive(Clone)]
-struct StateConfig {
-	session_index: SessionIndex,
-	slot: Slot,
-	tick: Tick,
-	validators: Vec<Sr25519Keyring>,
-	validator_groups: Vec<Vec<ValidatorIndex>>,
-	needed_approvals: u32,
-	no_show_slots: u32,
-	candidate_hash: Option<CandidateHash>,
-}
-
-impl Default for StateConfig {
-	fn default() -> Self {
-		StateConfig {
-			session_index: 1,
-			slot: Slot::from(0),
-			tick: 0,
-			validators: vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob],
-			validator_groups: vec![vec![ValidatorIndex(0)], vec![ValidatorIndex(1)]],
-			needed_approvals: 1,
-			no_show_slots: 2,
-			candidate_hash: None,
-		}
-	}
-}
-
-// one block with one candidate. Alice and Bob are in the assignment keys.
-fn some_state(config: StateConfig, db: &mut DbBackend) -> State {
-	let StateConfig {
-		session_index,
-		slot,
-		tick,
-		validators,
-		validator_groups,
-		needed_approvals,
-		no_show_slots,
-		candidate_hash,
-	} = config;
-
-	let n_validators = validators.len();
-
-	let state = State {
-		clock: Box::new(MockClock::new(tick)),
-		..single_session_state(
-			session_index,
-			SessionInfo {
-				validators: validators.iter().map(|v| v.public().into()).collect(),
-				discovery_keys: validators.iter().map(|v| v.public().into()).collect(),
-				assignment_keys: validators.iter().map(|v| v.public().into()).collect(),
-				validator_groups: validator_groups.clone(),
-				n_cores: validator_groups.len() as _,
-				zeroth_delay_tranche_width: 5,
-				relay_vrf_modulo_samples: 3,
-				n_delay_tranches: 50,
-				no_show_slots,
-				needed_approvals,
-				..Default::default()
-			},
-		)
-	};
-	let core_index = 0.into();
-
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_hash = candidate_hash.unwrap_or_else(|| CandidateHash(Hash::repeat_byte(0xCC)));
-
-	add_block(db, block_hash, session_index, slot);
-
-	add_candidate_to_block(
-		db,
-		block_hash,
-		candidate_hash,
-		n_validators,
-		core_index,
-		GroupIndex(0),
-		None,
-	);
-
-	state
-}
-
-fn add_block(db: &mut DbBackend, block_hash: Hash, session: SessionIndex, slot: Slot) {
-	overlay_txn(db, |overlay_db| {
-		overlay_db.write_block_entry(
-			approval_db::v1::BlockEntry {
-				block_hash,
-				parent_hash: Default::default(),
-				block_number: 0,
-				session,
-				slot,
-				candidates: Vec::new(),
-				relay_vrf_story: Default::default(),
-				approved_bitfield: Default::default(),
-				children: Default::default(),
-			}
-			.into(),
-		)
-	});
-}
-
-fn add_candidate_to_block(
-	db: &mut DbBackend,
-	block_hash: Hash,
-	candidate_hash: CandidateHash,
-	n_validators: usize,
-	core: CoreIndex,
-	backing_group: GroupIndex,
-	candidate_receipt: Option<CandidateReceipt>,
-) {
-	let mut block_entry = db.load_block_entry(&block_hash).unwrap().unwrap();
-
-	let mut candidate_entry =
-		db.load_candidate_entry(&candidate_hash).unwrap().unwrap_or_else(|| {
-			approval_db::v1::CandidateEntry {
-				session: block_entry.session(),
-				block_assignments: Default::default(),
-				candidate: candidate_receipt.unwrap_or_default(),
-				approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; n_validators],
-			}
-			.into()
-		});
-
-	block_entry.add_candidate(core, candidate_hash);
-
-	candidate_entry.add_approval_entry(
-		block_hash,
-		approval_db::v1::ApprovalEntry {
-			tranches: Vec::new(),
-			backing_group,
-			our_assignment: None,
-			our_approval_sig: None,
-			assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; n_validators],
-			approved: false,
-		}
-		.into(),
-	);
-
-	overlay_txn(db, |overlay_db| {
-		overlay_db.write_block_entry(block_entry.clone());
-		overlay_db.write_candidate_entry(candidate_entry.clone());
-	})
-}
-
-fn import_assignment<F>(
-	db: &mut DbBackend,
-	candidate_hash: &CandidateHash,
-	block_hash: &Hash,
-	validator_index: ValidatorIndex,
-	mut f: F,
-) where
-	F: FnMut(&mut CandidateEntry),
-{
-	let mut candidate_entry = db.load_candidate_entry(candidate_hash).unwrap().unwrap();
-	candidate_entry.approval_entry_mut(block_hash).unwrap().import_assignment(
-		0,
-		validator_index,
-		0,
-	);
-
-	f(&mut candidate_entry);
-
-	overlay_txn(db, |overlay_db| overlay_db.write_candidate_entry(candidate_entry.clone()));
-}
-
-fn set_our_assignment<F>(
-	db: &mut DbBackend,
-	candidate_hash: &CandidateHash,
-	block_hash: &Hash,
-	tranche: DelayTranche,
-	mut f: F,
-) where
-	F: FnMut(&mut CandidateEntry),
-{
-	let mut candidate_entry = db.load_candidate_entry(&candidate_hash).unwrap().unwrap();
-	let approval_entry = candidate_entry.approval_entry_mut(&block_hash).unwrap();
-
-	approval_entry.set_our_assignment(
-		approval_db::v1::OurAssignment {
-			cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-			tranche,
-			validator_index: ValidatorIndex(0),
-			triggered: false,
-		}
-		.into(),
-	);
-
-	f(&mut candidate_entry);
-
-	overlay_txn(db, |overlay_db| overlay_db.write_candidate_entry(candidate_entry.clone()));
-}
-
-#[test]
-fn rejects_bad_assignment() {
-	let mut db = make_db();
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-	let assignment_good = IndirectAssignmentCert {
-		block_hash,
-		validator: ValidatorIndex(0),
-		cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-	};
-	let mut state = some_state(
-		StateConfig { candidate_hash: Some(candidate_hash), ..Default::default() },
-		&mut db,
-	);
-	let candidate_index = 0;
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let res = check_and_import_assignment(
-		&mut state,
-		&mut overlay_db,
-		assignment_good.clone(),
-		candidate_index,
-	)
-	.unwrap();
-	assert_eq!(res.0, AssignmentCheckResult::Accepted);
-	// Check that the assignment's been imported.
-	assert_eq!(res.1.len(), 1);
-
-	let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>();
-	assert_eq!(write_ops.len(), 1);
-	assert_matches!(write_ops.get(0).unwrap(), BackendWriteOp::WriteCandidateEntry(..));
-	db.write(write_ops).unwrap();
-
-	// unknown hash
-	let unknown_hash = Hash::repeat_byte(0x02);
-	let assignment = IndirectAssignmentCert {
-		block_hash: unknown_hash,
-		validator: ValidatorIndex(0),
-		cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-	};
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let res = check_and_import_assignment(&mut state, &mut overlay_db, assignment, candidate_index)
-		.unwrap();
-	assert_eq!(res.0, AssignmentCheckResult::Bad(AssignmentCheckError::UnknownBlock(unknown_hash)));
-	assert_eq!(overlay_db.into_write_ops().count(), 0);
-
-	let mut state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| {
-			Err(criteria::InvalidAssignment)
-		})),
-		..some_state(
-			StateConfig { candidate_hash: Some(candidate_hash), ..Default::default() },
-			&mut db,
-		)
-	};
-
-	// same assignment, but this time rejected
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let res =
-		check_and_import_assignment(&mut state, &mut overlay_db, assignment_good, candidate_index)
-			.unwrap();
-	assert_eq!(
-		res.0,
-		AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCert(ValidatorIndex(0)))
-	);
-	assert_eq!(overlay_db.into_write_ops().count(), 0);
-}
-
-#[test]
-fn rejects_assignment_in_future() {
-	let mut db = make_db();
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_index = 0;
-	let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-	let assignment = IndirectAssignmentCert {
-		block_hash,
-		validator: ValidatorIndex(0),
-		cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-	};
-
-	let tick = 9;
-	let mut state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(move || {
-			Ok((tick + 20) as _)
-		})),
-		..some_state(
-			StateConfig { tick, candidate_hash: Some(candidate_hash), ..Default::default() },
-			&mut db,
-		)
-	};
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let res = check_and_import_assignment(
-		&mut state,
-		&mut overlay_db,
-		assignment.clone(),
-		candidate_index,
-	)
-	.unwrap();
-	assert_eq!(res.0, AssignmentCheckResult::TooFarInFuture);
-
-	let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>();
-
-	assert_eq!(write_ops.len(), 0);
-	db.write(write_ops).unwrap();
-
-	let mut state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(move || {
-			Ok((tick + 20 - 1) as _)
-		})),
-		..some_state(
-			StateConfig { tick, candidate_hash: Some(candidate_hash), ..Default::default() },
-			&mut db,
-		)
-	};
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let res = check_and_import_assignment(
-		&mut state,
-		&mut overlay_db,
-		assignment.clone(),
-		candidate_index,
-	)
-	.unwrap();
-	assert_eq!(res.0, AssignmentCheckResult::Accepted);
-
-	let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>();
-	assert_eq!(write_ops.len(), 1);
-
-	assert_matches!(write_ops.get(0).unwrap(), BackendWriteOp::WriteCandidateEntry(..));
-}
-
-#[test]
-fn rejects_assignment_with_unknown_candidate() {
-	let mut db = make_db();
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_index = 1;
-	let assignment = IndirectAssignmentCert {
-		block_hash,
-		validator: ValidatorIndex(0),
-		cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-	};
-
-	let mut state = some_state(Default::default(), &mut db);
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let res = check_and_import_assignment(
-		&mut state,
-		&mut overlay_db,
-		assignment.clone(),
-		candidate_index,
-	)
-	.unwrap();
-	assert_eq!(
-		res.0,
-		AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCandidateIndex(candidate_index))
-	);
-	assert_eq!(overlay_db.into_write_ops().count(), 0);
-}
-
-#[test]
-fn assignment_import_updates_candidate_entry_and_schedules_wakeup() {
-	let mut db = make_db();
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-
-	let candidate_index = 0;
-	let assignment = IndirectAssignmentCert {
-		block_hash,
-		validator: ValidatorIndex(0),
-		cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-	};
-
-	let mut state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))),
-		..some_state(
-			StateConfig { candidate_hash: Some(candidate_hash), ..Default::default() },
-			&mut db,
-		)
-	};
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let (res, actions) = check_and_import_assignment(
-		&mut state,
-		&mut overlay_db,
-		assignment.clone(),
-		candidate_index,
-	)
-	.unwrap();
-
-	assert_eq!(res, AssignmentCheckResult::Accepted);
-	assert_eq!(actions.len(), 1);
-
-	assert_matches!(
-		actions.get(0).unwrap(),
-		Action::ScheduleWakeup {
-			block_hash: b,
-			candidate_hash: c,
-			tick,
-			..
-		} => {
-			assert_eq!(b, &block_hash);
-			assert_eq!(c, &candidate_hash);
-			assert_eq!(tick, &slot_to_tick(0 + 2)); // current tick + no-show-duration.
-		}
-	);
-
-	let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>();
-	assert_eq!(1, write_ops.len());
-	assert_matches!(
-		write_ops.get(0).unwrap(),
-		BackendWriteOp::WriteCandidateEntry(ref c_entry) => {
-			assert_eq!(&c_entry.candidate.hash(), &candidate_hash);
-			assert!(c_entry.approval_entry(&block_hash).unwrap().is_assigned(ValidatorIndex(0)));
-		}
-	);
-}
-
-#[test]
-fn rejects_approval_before_assignment() {
-	let mut db = make_db();
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-
-	let state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))),
-		..some_state(
-			StateConfig { candidate_hash: Some(candidate_hash), ..Default::default() },
-			&mut db,
-		)
-	};
-
-	let vote = IndirectSignedApprovalVote {
-		block_hash,
-		candidate_index: 0,
-		validator: ValidatorIndex(0),
-		signature: sign_approval(Sr25519Keyring::Alice, candidate_hash, 1),
-	};
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let (actions, res) =
-		check_and_import_approval(&state, &mut overlay_db, &Metrics(None), vote, |r| r).unwrap();
-
-	assert_eq!(res, ApprovalCheckResult::Bad(ApprovalCheckError::NoAssignment(ValidatorIndex(0))));
-	assert!(actions.is_empty());
-	assert_eq!(overlay_db.into_write_ops().count(), 0);
-}
-
-#[test]
-fn rejects_approval_if_no_candidate_entry() {
-	let mut db = make_db();
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-
-	let state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))),
-		..some_state(
-			StateConfig { candidate_hash: Some(candidate_hash), ..Default::default() },
-			&mut db,
-		)
-	};
-
-	let vote = IndirectSignedApprovalVote {
-		block_hash,
-		candidate_index: 0,
-		validator: ValidatorIndex(0),
-		signature: sign_approval(Sr25519Keyring::Alice, candidate_hash, 1),
-	};
-
-	overlay_txn(&mut db, |overlay_db| overlay_db.delete_candidate_entry(&candidate_hash));
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let (actions, res) =
-		check_and_import_approval(&state, &mut overlay_db, &Metrics(None), vote, |r| r).unwrap();
-
-	assert_eq!(
-		res,
-		ApprovalCheckResult::Bad(ApprovalCheckError::InvalidCandidate(0, candidate_hash))
-	);
-	assert!(actions.is_empty());
-	assert_eq!(overlay_db.into_write_ops().count(), 0);
-}
-
-#[test]
-fn rejects_approval_if_no_block_entry() {
-	let mut db = make_db();
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-	let validator_index = ValidatorIndex(0);
-
-	let state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))),
-		..some_state(
-			StateConfig { candidate_hash: Some(candidate_hash), ..Default::default() },
-			&mut db,
-		)
-	};
-
-	let vote = IndirectSignedApprovalVote {
-		block_hash,
-		candidate_index: 0,
-		validator: ValidatorIndex(0),
-		signature: sign_approval(Sr25519Keyring::Alice, candidate_hash, 1),
-	};
-
-	import_assignment(&mut db, &candidate_hash, &block_hash, validator_index, |_| {});
-
-	overlay_txn(&mut db, |overlay_db| overlay_db.delete_block_entry(&block_hash));
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let (actions, res) =
-		check_and_import_approval(&state, &mut overlay_db, &Metrics(None), vote, |r| r).unwrap();
-
-	assert_eq!(res, ApprovalCheckResult::Bad(ApprovalCheckError::UnknownBlock(block_hash)));
-	assert!(actions.is_empty());
-	assert_eq!(overlay_db.into_write_ops().count(), 0);
-}
-
-#[test]
-fn accepts_and_imports_approval_after_assignment() {
-	let mut db = make_db();
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-	let validator_index = ValidatorIndex(0);
-
-	let candidate_index = 0;
-	let state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))),
-		..some_state(
-			StateConfig {
-				validators: vec![
-					Sr25519Keyring::Alice,
-					Sr25519Keyring::Bob,
-					Sr25519Keyring::Charlie,
-				],
-				validator_groups: vec![
-					vec![ValidatorIndex(0), ValidatorIndex(1)],
-					vec![ValidatorIndex(2)],
-				],
-				needed_approvals: 2,
-				candidate_hash: Some(candidate_hash),
-				..Default::default()
-			},
-			&mut db,
-		)
-	};
-
-	let vote = IndirectSignedApprovalVote {
-		block_hash,
-		candidate_index,
-		validator: validator_index,
-		signature: sign_approval(Sr25519Keyring::Alice, candidate_hash, 1),
-	};
-
-	import_assignment(&mut db, &candidate_hash, &block_hash, validator_index, |_| {});
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let (actions, res) =
-		check_and_import_approval(&state, &mut overlay_db, &Metrics(None), vote, |r| r).unwrap();
-
-	assert_eq!(res, ApprovalCheckResult::Accepted);
-
-	assert_eq!(actions.len(), 1);
-
-	assert_matches!(
-		&actions[0],
-		Action::InformDisputeCoordinator {
-			dispute_statement,
-			candidate_hash: c_hash,
-			validator_index: v,
-			..
-		} => {
-			assert_matches!(
-				dispute_statement.statement(),
-				&DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalChecking)
-			);
-
-			assert_eq!(c_hash, &candidate_hash);
-			assert_eq!(v, &validator_index);
-		}
-	);
-
-	let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>();
-	assert_eq!(write_ops.len(), 1);
-	assert_matches!(
-		write_ops.get(0).unwrap(),
-		BackendWriteOp::WriteCandidateEntry(ref c_entry) => {
-			assert_eq!(&c_entry.candidate.hash(), &candidate_hash);
-			assert!(!c_entry.approval_entry(&block_hash).unwrap().is_approved());
-		}
-	);
-}
-
-#[test]
-fn second_approval_import_only_schedules_wakeups() {
-	let mut db = make_db();
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-	let validator_index = ValidatorIndex(0);
-	let validator_index_b = ValidatorIndex(1);
-
-	let candidate_index = 0;
-	let state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))),
-		..some_state(
-			StateConfig {
-				validators: vec![
-					Sr25519Keyring::Alice,
-					Sr25519Keyring::Bob,
-					Sr25519Keyring::Charlie,
-				],
-				validator_groups: vec![
-					vec![ValidatorIndex(0), ValidatorIndex(1)],
-					vec![ValidatorIndex(2)],
-				],
-				needed_approvals: 2,
-				candidate_hash: Some(candidate_hash),
-				..Default::default()
-			},
-			&mut db,
-		)
-	};
-
-	let vote = IndirectSignedApprovalVote {
-		block_hash,
-		candidate_index,
-		validator: validator_index,
-		signature: sign_approval(Sr25519Keyring::Alice, candidate_hash, 1),
-	};
-
-	import_assignment(&mut db, &candidate_hash, &block_hash, validator_index, |candidate_entry| {
-		assert!(!candidate_entry.mark_approval(validator_index));
-	});
-
-	// There is only one assignment, so nothing to schedule if we double-import.
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let (actions, res) =
-		check_and_import_approval(&state, &mut overlay_db, &Metrics(None), vote.clone(), |r| r)
-			.unwrap();
-
-	assert_eq!(res, ApprovalCheckResult::Accepted);
-	assert!(actions.is_empty());
-	assert_eq!(overlay_db.into_write_ops().count(), 0);
-
-	// After adding a second assignment, there should be a schedule wakeup action.
-
-	import_assignment(&mut db, &candidate_hash, &block_hash, validator_index_b, |_| {});
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let (actions, res) =
-		check_and_import_approval(&state, &mut overlay_db, &Metrics(None), vote, |r| r).unwrap();
-
-	assert_eq!(res, ApprovalCheckResult::Accepted);
-	assert_eq!(actions.len(), 1);
-	assert_eq!(overlay_db.into_write_ops().count(), 0);
-
-	assert_matches!(
-		actions.get(0).unwrap(),
-		Action::ScheduleWakeup { .. } => {}
-	);
-}
-
-#[test]
-fn import_checked_approval_updates_entries_and_schedules() {
-	let mut db = make_db();
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-	let validator_index_a = ValidatorIndex(0);
-	let validator_index_b = ValidatorIndex(1);
-
-	let state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))),
-		..some_state(
-			StateConfig {
-				validators: vec![
-					Sr25519Keyring::Alice,
-					Sr25519Keyring::Bob,
-					Sr25519Keyring::Charlie,
-				],
-				validator_groups: vec![
-					vec![ValidatorIndex(0), ValidatorIndex(1)],
-					vec![ValidatorIndex(2)],
-				],
-				needed_approvals: 2,
-				candidate_hash: Some(candidate_hash),
-				..Default::default()
-			},
-			&mut db,
-		)
-	};
-
-	import_assignment(&mut db, &candidate_hash, &block_hash, validator_index_a, |_| {});
-	import_assignment(&mut db, &candidate_hash, &block_hash, validator_index_b, |_| {});
-
-	{
-		let mut overlay_db = OverlayedBackend::new(&db);
-		let actions = import_checked_approval(
-			&state,
-			&mut overlay_db,
-			&Metrics(None),
-			db.load_block_entry(&block_hash).unwrap().unwrap(),
-			candidate_hash,
-			db.load_candidate_entry(&candidate_hash).unwrap().unwrap(),
-			ApprovalSource::Remote(validator_index_a),
-		);
-
-		assert_eq!(actions.len(), 1);
-		assert_matches!(
-			actions.get(0).unwrap(),
-			Action::ScheduleWakeup {
-				block_hash: b_hash,
-				candidate_hash: c_hash,
-				..
-			} => {
-				assert_eq!(b_hash, &block_hash);
-				assert_eq!(c_hash, &candidate_hash);
-			}
-		);
-
-		let mut write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>();
-		assert_eq!(write_ops.len(), 1);
-		assert_matches!(
-			write_ops.get_mut(0).unwrap(),
-			BackendWriteOp::WriteCandidateEntry(ref mut c_entry) => {
-				assert!(!c_entry.approval_entry(&block_hash).unwrap().is_approved());
-				assert!(c_entry.mark_approval(validator_index_a));
-			}
-		);
-
-		db.write(write_ops).unwrap();
-	}
-
-	{
-		let mut overlay_db = OverlayedBackend::new(&db);
-		let actions = import_checked_approval(
-			&state,
-			&mut overlay_db,
-			&Metrics(None),
-			db.load_block_entry(&block_hash).unwrap().unwrap(),
-			candidate_hash,
-			db.load_candidate_entry(&candidate_hash).unwrap().unwrap(),
-			ApprovalSource::Remote(validator_index_b),
-		);
-		assert_eq!(actions.len(), 1);
-
-		let mut write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>();
-		assert_eq!(write_ops.len(), 2);
-
-		assert_matches!(
-			write_ops.get(0).unwrap(),
-			BackendWriteOp::WriteBlockEntry(b_entry) => {
-				assert_eq!(b_entry.block_hash(), block_hash);
-				assert!(b_entry.is_fully_approved());
-				assert!(b_entry.is_candidate_approved(&candidate_hash));
-			}
-		);
-
-		assert_matches!(
-			write_ops.get_mut(1).unwrap(),
-			BackendWriteOp::WriteCandidateEntry(ref mut c_entry) => {
-				assert_eq!(&c_entry.candidate.hash(), &candidate_hash);
-				assert!(c_entry.approval_entry(&block_hash).unwrap().is_approved());
-				assert!(c_entry.mark_approval(validator_index_b));
-			}
-		);
-	}
-}
-
-#[test]
-fn assignment_triggered_by_all_with_less_than_threshold() {
-	let block_hash = Hash::repeat_byte(0x01);
-
-	let mut candidate_entry: CandidateEntry = {
-		let approval_entry = approval_db::v1::ApprovalEntry {
-			tranches: Vec::new(),
-			backing_group: GroupIndex(0),
-			our_assignment: Some(approval_db::v1::OurAssignment {
-				cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-				tranche: 1,
-				validator_index: ValidatorIndex(4),
-				triggered: false,
-			}),
-			our_approval_sig: None,
-			assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-			approved: false,
-		};
-
-		approval_db::v1::CandidateEntry {
-			candidate: Default::default(),
-			session: 1,
-			block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(),
-			approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-		}
-		.into()
-	};
-
-	// 1-of-4
-	candidate_entry.approval_entry_mut(&block_hash).unwrap().import_assignment(
-		0,
-		ValidatorIndex(0),
-		0,
-	);
-
-	candidate_entry.mark_approval(ValidatorIndex(0));
-
-	let tranche_now = 1;
-	assert!(should_trigger_assignment(
-		candidate_entry.approval_entry(&block_hash).unwrap(),
-		&candidate_entry,
-		RequiredTranches::All,
-		tranche_now,
-	));
-}
-
-#[test]
-fn assignment_not_triggered_by_all_with_threshold() {
-	let block_hash = Hash::repeat_byte(0x01);
-
-	let mut candidate_entry: CandidateEntry = {
-		let approval_entry = approval_db::v1::ApprovalEntry {
-			tranches: Vec::new(),
-			backing_group: GroupIndex(0),
-			our_assignment: Some(approval_db::v1::OurAssignment {
-				cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-				tranche: 1,
-				validator_index: ValidatorIndex(4),
-				triggered: false,
-			}),
-			our_approval_sig: None,
-			assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-			approved: false,
-		};
-
-		approval_db::v1::CandidateEntry {
-			candidate: Default::default(),
-			session: 1,
-			block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(),
-			approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-		}
-		.into()
-	};
-
-	// 2-of-4
-	candidate_entry.approval_entry_mut(&block_hash).unwrap().import_assignment(
-		0,
-		ValidatorIndex(0),
-		0,
-	);
-
-	candidate_entry.approval_entry_mut(&block_hash).unwrap().import_assignment(
-		0,
-		ValidatorIndex(1),
-		0,
-	);
-
-	candidate_entry.mark_approval(ValidatorIndex(0));
-	candidate_entry.mark_approval(ValidatorIndex(1));
-
-	let tranche_now = 1;
-	assert!(!should_trigger_assignment(
-		candidate_entry.approval_entry(&block_hash).unwrap(),
-		&candidate_entry,
-		RequiredTranches::All,
-		tranche_now,
-	));
-}
-
-#[test]
-fn assignment_not_triggered_if_already_triggered() {
-	let block_hash = Hash::repeat_byte(0x01);
-
-	let candidate_entry: CandidateEntry = {
-		let approval_entry = approval_db::v1::ApprovalEntry {
-			tranches: Vec::new(),
-			backing_group: GroupIndex(0),
-			our_assignment: Some(approval_db::v1::OurAssignment {
-				cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-				tranche: 1,
-				validator_index: ValidatorIndex(4),
-				triggered: true,
-			}),
-			our_approval_sig: None,
-			assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-			approved: false,
-		};
-
-		approval_db::v1::CandidateEntry {
-			candidate: Default::default(),
-			session: 1,
-			block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(),
-			approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-		}
-		.into()
-	};
-
-	let tranche_now = 1;
-	assert!(!should_trigger_assignment(
-		candidate_entry.approval_entry(&block_hash).unwrap(),
-		&candidate_entry,
-		RequiredTranches::All,
-		tranche_now,
-	));
-}
-
-#[test]
-fn assignment_not_triggered_by_exact() {
-	let block_hash = Hash::repeat_byte(0x01);
-
-	let candidate_entry: CandidateEntry = {
-		let approval_entry = approval_db::v1::ApprovalEntry {
-			tranches: Vec::new(),
-			backing_group: GroupIndex(0),
-			our_assignment: Some(approval_db::v1::OurAssignment {
-				cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-				tranche: 1,
-				validator_index: ValidatorIndex(4),
-				triggered: false,
-			}),
-			our_approval_sig: None,
-			assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-			approved: false,
-		};
-
-		approval_db::v1::CandidateEntry {
-			candidate: Default::default(),
-			session: 1,
-			block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(),
-			approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-		}
-		.into()
-	};
-
-	let tranche_now = 1;
-	assert!(!should_trigger_assignment(
-		candidate_entry.approval_entry(&block_hash).unwrap(),
-		&candidate_entry,
-		RequiredTranches::Exact { needed: 2, next_no_show: None, tolerated_missing: 0 },
-		tranche_now,
-	));
-}
-
-#[test]
-fn assignment_not_triggered_more_than_maximum() {
-	let block_hash = Hash::repeat_byte(0x01);
-	let maximum_broadcast = 10;
-
-	let candidate_entry: CandidateEntry = {
-		let approval_entry = approval_db::v1::ApprovalEntry {
-			tranches: Vec::new(),
-			backing_group: GroupIndex(0),
-			our_assignment: Some(approval_db::v1::OurAssignment {
-				cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-				tranche: maximum_broadcast + 1,
-				validator_index: ValidatorIndex(4),
-				triggered: false,
-			}),
-			our_approval_sig: None,
-			assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-			approved: false,
-		};
-
-		approval_db::v1::CandidateEntry {
-			candidate: Default::default(),
-			session: 1,
-			block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(),
-			approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-		}
-		.into()
-	};
-
-	let tranche_now = 50;
-	assert!(!should_trigger_assignment(
-		candidate_entry.approval_entry(&block_hash).unwrap(),
-		&candidate_entry,
-		RequiredTranches::Pending {
-			maximum_broadcast,
-			clock_drift: 0,
-			considered: 10,
-			next_no_show: None,
-		},
-		tranche_now,
-	));
-}
-
-#[test]
-fn assignment_triggered_if_at_maximum() {
-	let block_hash = Hash::repeat_byte(0x01);
-	let maximum_broadcast = 10;
-
-	let candidate_entry: CandidateEntry = {
-		let approval_entry = approval_db::v1::ApprovalEntry {
-			tranches: Vec::new(),
-			backing_group: GroupIndex(0),
-			our_assignment: Some(approval_db::v1::OurAssignment {
-				cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-				tranche: maximum_broadcast,
-				validator_index: ValidatorIndex(4),
-				triggered: false,
-			}),
-			our_approval_sig: None,
-			assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-			approved: false,
-		};
-
-		approval_db::v1::CandidateEntry {
-			candidate: Default::default(),
-			session: 1,
-			block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(),
-			approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-		}
-		.into()
-	};
-
-	let tranche_now = maximum_broadcast;
-	assert!(should_trigger_assignment(
-		candidate_entry.approval_entry(&block_hash).unwrap(),
-		&candidate_entry,
-		RequiredTranches::Pending {
-			maximum_broadcast,
-			clock_drift: 0,
-			considered: 10,
-			next_no_show: None,
-		},
-		tranche_now,
-	));
-}
-
-#[test]
-fn assignment_not_triggered_if_at_maximum_but_clock_is_before() {
-	let block_hash = Hash::repeat_byte(0x01);
-	let maximum_broadcast = 10;
-
-	let candidate_entry: CandidateEntry = {
-		let approval_entry = approval_db::v1::ApprovalEntry {
-			tranches: Vec::new(),
-			backing_group: GroupIndex(0),
-			our_assignment: Some(approval_db::v1::OurAssignment {
-				cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-				tranche: maximum_broadcast,
-				validator_index: ValidatorIndex(4),
-				triggered: false,
-			}),
-			our_approval_sig: None,
-			assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-			approved: false,
-		};
-
-		approval_db::v1::CandidateEntry {
-			candidate: Default::default(),
-			session: 1,
-			block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(),
-			approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-		}
-		.into()
-	};
-
-	let tranche_now = 9;
-	assert!(!should_trigger_assignment(
-		candidate_entry.approval_entry(&block_hash).unwrap(),
-		&candidate_entry,
-		RequiredTranches::Pending {
-			maximum_broadcast,
-			clock_drift: 0,
-			considered: 10,
-			next_no_show: None,
-		},
-		tranche_now,
-	));
-}
-
-#[test]
-fn assignment_not_triggered_if_at_maximum_but_clock_is_before_with_drift() {
-	let block_hash = Hash::repeat_byte(0x01);
-	let maximum_broadcast = 10;
-
-	let candidate_entry: CandidateEntry = {
-		let approval_entry = approval_db::v1::ApprovalEntry {
-			tranches: Vec::new(),
-			backing_group: GroupIndex(0),
-			our_assignment: Some(approval_db::v1::OurAssignment {
-				cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-				tranche: maximum_broadcast,
-				validator_index: ValidatorIndex(4),
-				triggered: false,
-			}),
-			our_approval_sig: None,
-			assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-			approved: false,
-		};
-
-		approval_db::v1::CandidateEntry {
-			candidate: Default::default(),
-			session: 1,
-			block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(),
-			approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
-		}
-		.into()
-	};
-
-	let tranche_now = 10;
-	assert!(!should_trigger_assignment(
-		candidate_entry.approval_entry(&block_hash).unwrap(),
-		&candidate_entry,
-		RequiredTranches::Pending {
-			maximum_broadcast,
-			clock_drift: 1,
-			considered: 10,
-			next_no_show: None,
-		},
-		tranche_now,
-	));
-}
-
-#[test]
-fn wakeups_next() {
-	let mut wakeups = Wakeups::default();
-
-	let b_a = Hash::repeat_byte(0);
-	let b_b = Hash::repeat_byte(1);
-
-	let c_a = CandidateHash(Hash::repeat_byte(2));
-	let c_b = CandidateHash(Hash::repeat_byte(3));
-
-	wakeups.schedule(b_a, 0, c_a, 1);
-	wakeups.schedule(b_a, 0, c_b, 4);
-	wakeups.schedule(b_b, 1, c_b, 3);
-
-	assert_eq!(wakeups.first().unwrap(), 1);
-
-	let clock = MockClock::new(0);
-	let clock_aux = clock.clone();
-
-	let test_fut = Box::pin(async move {
-		assert_eq!(wakeups.next(&clock).await, (1, b_a, c_a));
-		assert_eq!(wakeups.next(&clock).await, (3, b_b, c_b));
-		assert_eq!(wakeups.next(&clock).await, (4, b_a, c_b));
-		assert!(wakeups.first().is_none());
-		assert!(wakeups.wakeups.is_empty());
-
-		assert_eq!(
-			wakeups.block_numbers.get(&0).unwrap(),
-			&vec![b_a].into_iter().collect::<HashSet<_>>(),
-		);
-		assert_eq!(
-			wakeups.block_numbers.get(&1).unwrap(),
-			&vec![b_b].into_iter().collect::<HashSet<_>>(),
-		);
-
-		wakeups.prune_finalized_wakeups(0);
-
-		assert!(wakeups.block_numbers.get(&0).is_none());
-		assert_eq!(
-			wakeups.block_numbers.get(&1).unwrap(),
-			&vec![b_b].into_iter().collect::<HashSet<_>>(),
-		);
-
-		wakeups.prune_finalized_wakeups(1);
-
-		assert!(wakeups.block_numbers.get(&0).is_none());
-		assert!(wakeups.block_numbers.get(&1).is_none());
-	});
-
-	let aux_fut = Box::pin(async move {
-		clock_aux.inner.lock().set_tick(1);
-		// skip direct set to 3.
-		clock_aux.inner.lock().set_tick(4);
-	});
-
-	futures::executor::block_on(futures::future::join(test_fut, aux_fut));
-}
-
-#[test]
-fn wakeup_earlier_supersedes_later() {
-	let mut wakeups = Wakeups::default();
-
-	let b_a = Hash::repeat_byte(0);
-	let c_a = CandidateHash(Hash::repeat_byte(2));
-
-	wakeups.schedule(b_a, 0, c_a, 4);
-	wakeups.schedule(b_a, 0, c_a, 2);
-	wakeups.schedule(b_a, 0, c_a, 3);
-
-	let clock = MockClock::new(0);
-	let clock_aux = clock.clone();
-
-	let test_fut = Box::pin(async move {
-		assert_eq!(wakeups.next(&clock).await, (2, b_a, c_a));
-		assert!(wakeups.first().is_none());
-		assert!(wakeups.reverse_wakeups.is_empty());
-	});
-
-	let aux_fut = Box::pin(async move {
-		clock_aux.inner.lock().set_tick(2);
-	});
-
-	futures::executor::block_on(futures::future::join(test_fut, aux_fut));
-}
-
-#[test]
-fn import_checked_approval_sets_one_block_bit_at_a_time() {
-	let mut db = make_db();
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-	let candidate_receipt_2 = CandidateReceipt::<Hash> {
-		descriptor: CandidateDescriptor::default(),
-		commitments_hash: Hash::repeat_byte(0x02),
-	};
-	let candidate_hash_2 = candidate_receipt_2.hash();
-
-	let validator_index_a = ValidatorIndex(0);
-	let validator_index_b = ValidatorIndex(1);
-
-	let state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))),
-		..some_state(
-			StateConfig {
-				validators: vec![
-					Sr25519Keyring::Alice,
-					Sr25519Keyring::Bob,
-					Sr25519Keyring::Charlie,
-				],
-				validator_groups: vec![
-					vec![ValidatorIndex(0), ValidatorIndex(1)],
-					vec![ValidatorIndex(2)],
-				],
-				needed_approvals: 2,
-				candidate_hash: Some(candidate_hash),
-				..Default::default()
-			},
-			&mut db,
-		)
-	};
-
-	add_candidate_to_block(
-		&mut db,
-		block_hash,
-		candidate_hash_2,
-		3,
-		CoreIndex(1),
-		GroupIndex(1),
-		Some(candidate_receipt_2),
-	);
-
-	let setup_candidate = |db: &mut DbBackend, c_hash| {
-		import_assignment(db, &c_hash, &block_hash, validator_index_a, |candidate_entry| {
-			let approval_entry = candidate_entry.approval_entry_mut(&block_hash).unwrap();
-			approval_entry.import_assignment(0, validator_index_b, 0);
-			assert!(!candidate_entry.mark_approval(validator_index_a));
-		})
-	};
-
-	setup_candidate(&mut db, candidate_hash);
-	setup_candidate(&mut db, candidate_hash_2);
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let actions = import_checked_approval(
-		&state,
-		&mut overlay_db,
-		&Metrics(None),
-		db.load_block_entry(&block_hash).unwrap().unwrap(),
-		candidate_hash,
-		db.load_candidate_entry(&candidate_hash).unwrap().unwrap(),
-		ApprovalSource::Remote(validator_index_b),
-	);
-
-	assert_eq!(actions.len(), 0);
-
-	let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>();
-	assert_eq!(write_ops.len(), 2);
-
-	assert_matches!(
-		write_ops.get(0).unwrap(),
-		BackendWriteOp::WriteBlockEntry(b_entry) => {
-			assert_eq!(b_entry.block_hash(), block_hash);
-			assert!(!b_entry.is_fully_approved());
-			assert!(b_entry.is_candidate_approved(&candidate_hash));
-			assert!(!b_entry.is_candidate_approved(&candidate_hash_2));
-		}
-	);
-
-	assert_matches!(
-		write_ops.get(1).unwrap(),
-		BackendWriteOp::WriteCandidateEntry(c_entry) => {
-			assert_eq!(&c_entry.candidate.hash(), &candidate_hash);
-			assert!(c_entry.approval_entry(&block_hash).unwrap().is_approved());
-		}
-	);
-
-	db.write(write_ops).unwrap();
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let actions = import_checked_approval(
-		&state,
-		&mut overlay_db,
-		&Metrics(None),
-		db.load_block_entry(&block_hash).unwrap().unwrap(),
-		candidate_hash_2,
-		db.load_candidate_entry(&candidate_hash_2).unwrap().unwrap(),
-		ApprovalSource::Remote(validator_index_b),
-	);
-
-	assert_eq!(actions.len(), 1);
-
-	let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>();
-	assert_eq!(write_ops.len(), 2);
-
-	assert_matches!(
-		write_ops.get(0).unwrap(),
-		BackendWriteOp::WriteBlockEntry(b_entry) => {
-			assert_eq!(b_entry.block_hash(), block_hash);
-			assert!(b_entry.is_fully_approved());
-			assert!(b_entry.is_candidate_approved(&candidate_hash));
-			assert!(b_entry.is_candidate_approved(&candidate_hash_2));
-		}
-	);
-
-	assert_matches!(
-		write_ops.get(1).unwrap(),
-		BackendWriteOp::WriteCandidateEntry(c_entry) => {
-			assert_eq!(&c_entry.candidate.hash(), &candidate_hash_2);
-			assert!(c_entry.approval_entry(&block_hash).unwrap().is_approved());
-		}
-	);
-}
-
-#[test]
-fn approved_ancestor_all_approved() {
-	let mut db = make_db();
-
-	let block_hash_1 = Hash::repeat_byte(0x01);
-	let block_hash_2 = Hash::repeat_byte(0x02);
-	let block_hash_3 = Hash::repeat_byte(0x03);
-	let block_hash_4 = Hash::repeat_byte(0x04);
-
-	let candidate_hash = CandidateHash(Hash::repeat_byte(0xCC));
-
-	let slot = Slot::from(1);
-	let session_index = 1;
-
-	let add_block = |db: &mut DbBackend, block_hash, approved| {
-		add_block(db, block_hash, session_index, slot);
-
-		let mut block_entry = db.load_block_entry(&block_hash).unwrap().unwrap();
-		block_entry.add_candidate(CoreIndex(0), candidate_hash);
-		if approved {
-			block_entry.mark_approved_by_hash(&candidate_hash);
-		}
-
-		overlay_txn(db, |overlay_db| overlay_db.write_block_entry(block_entry.clone()));
-	};
-
-	add_block(&mut db, block_hash_1, true);
-	add_block(&mut db, block_hash_2, true);
-	add_block(&mut db, block_hash_3, true);
-	add_block(&mut db, block_hash_4, true);
-
-	let pool = TaskExecutor::new();
-	let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone());
-
-	let test_fut = Box::pin(async move {
-		let overlay_db = OverlayedBackend::new(&db);
-		assert_matches!(
-			handle_approved_ancestor(&mut ctx, &overlay_db, block_hash_4, 0, &Default::default())
-				.await,
-			Ok(Some(HighestApprovedAncestorBlock { hash, number, .. } )) => {
-				assert_eq!((block_hash_4, 4), (hash, number));
-			}
-		)
-	});
-
-	let aux_fut = Box::pin(async move {
-		assert_matches!(
-			handle.recv().await,
-			AllMessages::ChainApi(ChainApiMessage::BlockNumber(target, tx)) => {
-				assert_eq!(target, block_hash_4);
-				let _ = tx.send(Ok(Some(4)));
-			}
-		);
-
-		assert_matches!(
-			handle.recv().await,
-			AllMessages::ChainApi(ChainApiMessage::Ancestors {
-				hash,
-				k,
-				response_channel: tx,
-			}) => {
-				assert_eq!(hash, block_hash_4);
-				assert_eq!(k, 4 - (0 + 1));
-				let _ = tx.send(Ok(vec![block_hash_3, block_hash_2, block_hash_1]));
-			}
-		);
-	});
-
-	futures::executor::block_on(futures::future::join(test_fut, aux_fut));
-}
-
-#[test]
-fn approved_ancestor_missing_approval() {
-	let mut db = make_db();
-
-	let block_hash_1 = Hash::repeat_byte(0x01);
-	let block_hash_2 = Hash::repeat_byte(0x02);
-	let block_hash_3 = Hash::repeat_byte(0x03);
-	let block_hash_4 = Hash::repeat_byte(0x04);
-
-	let candidate_hash = CandidateHash(Hash::repeat_byte(0xCC));
-
-	let slot = Slot::from(1);
-	let session_index = 1;
-
-	let add_block = |db: &mut DbBackend, block_hash, approved| {
-		add_block(db, block_hash, session_index, slot);
-
-		let mut block_entry = db.load_block_entry(&block_hash).unwrap().unwrap();
-		block_entry.add_candidate(CoreIndex(0), candidate_hash);
-		if approved {
-			block_entry.mark_approved_by_hash(&candidate_hash);
-		}
-
-		overlay_txn(db, |overlay_db| overlay_db.write_block_entry(block_entry.clone()));
-	};
-
-	add_block(&mut db, block_hash_1, true);
-	add_block(&mut db, block_hash_2, true);
-	add_block(&mut db, block_hash_3, false);
-	add_block(&mut db, block_hash_4, true);
-
-	let pool = TaskExecutor::new();
-	let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone());
-
-	let test_fut = Box::pin(async move {
-		let overlay_db = OverlayedBackend::new(&db);
-		assert_matches!(
-			handle_approved_ancestor(&mut ctx, &overlay_db, block_hash_4, 0, &Default::default())
-				.await,
-			Ok(Some(HighestApprovedAncestorBlock { hash, number, .. })) => {
-				assert_eq!((block_hash_2, 2), (hash, number));
-			}
-		)
-	});
-
-	let aux_fut = Box::pin(async move {
-		assert_matches!(
-			handle.recv().await,
-			AllMessages::ChainApi(ChainApiMessage::BlockNumber(target, tx)) => {
-				assert_eq!(target, block_hash_4);
-				let _ = tx.send(Ok(Some(4)));
-			}
-		);
-
-		assert_matches!(
-			handle.recv().await,
-			AllMessages::ChainApi(ChainApiMessage::Ancestors {
-				hash,
-				k,
-				response_channel: tx,
-			}) => {
-				assert_eq!(hash, block_hash_4);
-				assert_eq!(k, 4 - (0 + 1));
-				let _ = tx.send(Ok(vec![block_hash_3, block_hash_2, block_hash_1]));
-			}
-		);
-	});
-
-	futures::executor::block_on(futures::future::join(test_fut, aux_fut));
-}
-
-#[test]
-fn process_wakeup_trigger_assignment_launch_approval() {
-	let mut db = make_db();
-
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-	let slot = Slot::from(1);
-	let session_index = 1;
-
-	let state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))),
-		..some_state(
-			StateConfig {
-				validators: vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob],
-				validator_groups: vec![vec![ValidatorIndex(0)], vec![ValidatorIndex(1)]],
-				needed_approvals: 2,
-				session_index,
-				slot,
-				candidate_hash: Some(candidate_hash),
-				..Default::default()
-			},
-			&mut db,
-		)
-	};
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let actions = process_wakeup(&state, &mut overlay_db, block_hash, candidate_hash, 1).unwrap();
-
-	assert!(actions.is_empty());
-	assert_eq!(overlay_db.into_write_ops().count(), 0);
-
-	set_our_assignment(&mut db, &candidate_hash, &block_hash, 0, |_| {});
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let actions = process_wakeup(&state, &mut overlay_db, block_hash, candidate_hash, 1).unwrap();
-
-	assert_eq!(actions.len(), 2);
-
-	assert_matches!(
-		actions.get(0).unwrap(),
-		Action::LaunchApproval {
-			candidate_index,
-			..
-		} => {
-			assert_eq!(candidate_index, &0);
-		}
-	);
-
-	assert_matches!(
-		actions.get(1).unwrap(),
-		Action::ScheduleWakeup {
-			tick,
-			..
-		} => {
-			assert_eq!(tick, &slot_to_tick(0 + 2));
-		}
-	);
-
-	let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>();
-	assert_eq!(write_ops.len(), 1);
-
-	assert_matches!(
-		write_ops.get(0).unwrap(),
-		BackendWriteOp::WriteCandidateEntry(c_entry) => {
-			assert_eq!(&c_entry.candidate.hash(), &candidate_hash);
-			assert!(c_entry
-				.approval_entry(&block_hash)
-				.unwrap()
-				.our_assignment()
-				.unwrap()
-				.triggered()
-			);
-		}
-	);
-}
-
-#[test]
-fn process_wakeup_schedules_wakeup() {
-	let mut db = make_db();
-
-	let block_hash = Hash::repeat_byte(0x01);
-	let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-	let slot = Slot::from(1);
-	let session_index = 1;
-
-	let state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(10))),
-		..some_state(
-			StateConfig {
-				validators: vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob],
-				validator_groups: vec![vec![ValidatorIndex(0)], vec![ValidatorIndex(1)]],
-				needed_approvals: 2,
-				session_index,
-				slot,
-				candidate_hash: Some(candidate_hash),
-				..Default::default()
-			},
-			&mut db,
-		)
-	};
-
-	set_our_assignment(&mut db, &candidate_hash, &block_hash, 10, |_| {});
-
-	let mut overlay_db = OverlayedBackend::new(&db);
-	let actions = process_wakeup(&state, &mut overlay_db, block_hash, candidate_hash, 1).unwrap();
-
-	assert_eq!(actions.len(), 1);
-	assert_matches!(
-		actions.get(0).unwrap(),
-		Action::ScheduleWakeup { block_hash: b, candidate_hash: c, tick, .. } => {
-			assert_eq!(b, &block_hash);
-			assert_eq!(c, &candidate_hash);
-			assert_eq!(tick, &(slot_to_tick(slot) + 10));
-		}
-	);
-	assert_eq!(overlay_db.into_write_ops().count(), 0);
-}
-
-#[test]
-fn triggered_assignment_leads_to_recovery_and_validation() {}
-
-#[test]
-fn finalization_event_prunes() {}
-
-#[test]
-fn local_approval_import_always_updates_approval_entry() {
-	let mut db = make_db();
-	let block_hash = Hash::repeat_byte(0x01);
-	let block_hash_2 = Hash::repeat_byte(0x02);
-	let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-	let validator_index = ValidatorIndex(0);
-
-	let state_config = StateConfig {
-		validators: vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob, Sr25519Keyring::Charlie],
-		validator_groups: vec![vec![ValidatorIndex(0), ValidatorIndex(1)], vec![ValidatorIndex(2)]],
-		needed_approvals: 2,
-		candidate_hash: Some(candidate_hash),
-		..Default::default()
-	};
-
-	let state = State {
-		assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))),
-		..some_state(state_config.clone(), &mut db)
-	};
-
-	add_block(&mut db, block_hash_2, state_config.session_index, state_config.slot);
-
-	add_candidate_to_block(
-		&mut db,
-		block_hash_2,
-		candidate_hash,
-		state_config.validators.len(),
-		1.into(),
-		GroupIndex(1),
-		None,
-	);
-
-	let sig_a = sign_approval(Sr25519Keyring::Alice, candidate_hash, 1);
-	let sig_b = sign_approval(Sr25519Keyring::Alice, candidate_hash, 1);
-
-	{
-		let mut import_local_assignment = |block_hash: Hash| {
-			set_our_assignment(&mut db, &candidate_hash, &block_hash, 0, |candidate_entry| {
-				let approval_entry = candidate_entry.approval_entry_mut(&block_hash).unwrap();
-				assert!(approval_entry.trigger_our_assignment(0).is_some());
-				assert!(approval_entry.local_statements().0.is_some());
-			});
-		};
-
-		import_local_assignment(block_hash);
-		import_local_assignment(block_hash_2);
-	}
-
-	{
-		let mut overlay_db = OverlayedBackend::new(&db);
-		let actions = import_checked_approval(
-			&state,
-			&mut overlay_db,
-			&Metrics(None),
-			db.load_block_entry(&block_hash).unwrap().unwrap(),
-			candidate_hash,
-			db.load_candidate_entry(&candidate_hash).unwrap().unwrap(),
-			ApprovalSource::Local(validator_index, sig_a.clone()),
-		);
-
-		assert_eq!(actions.len(), 0);
-
-		let mut write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>();
-		assert_eq!(write_ops.len(), 1);
-
-		assert_matches!(
-			write_ops.get_mut(0).unwrap(),
-			BackendWriteOp::WriteCandidateEntry(ref mut c_entry) => {
-				assert_eq!(&c_entry.candidate.hash(), &candidate_hash);
-				assert_eq!(
-					c_entry.approval_entry(&block_hash).unwrap().local_statements().1,
-					Some(sig_a),
-				);
-				assert!(c_entry.mark_approval(validator_index));
-			}
-		);
-
-		db.write(write_ops).unwrap();
-	}
-
-	{
-		let mut overlay_db = OverlayedBackend::new(&db);
-		let actions = import_checked_approval(
-			&state,
-			&mut overlay_db,
-			&Metrics(None),
-			db.load_block_entry(&block_hash_2).unwrap().unwrap(),
-			candidate_hash,
-			db.load_candidate_entry(&candidate_hash).unwrap().unwrap(),
-			ApprovalSource::Local(validator_index, sig_b.clone()),
-		);
-
-		assert_eq!(actions.len(), 0);
-
-		let mut write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>();
-		assert_eq!(write_ops.len(), 1);
-
-		assert_matches!(
-			write_ops.get_mut(0).unwrap(),
-			BackendWriteOp::WriteCandidateEntry(ref mut c_entry) => {
-				assert_eq!(&c_entry.candidate.hash(), &candidate_hash);
-				assert_eq!(
-					c_entry.approval_entry(&block_hash_2).unwrap().local_statements().1,
-					Some(sig_b),
-				);
-				assert!(c_entry.mark_approval(validator_index));
-			}
-		);
-	}
-}
-
-// TODO [now]: handling `BecomeActive` action broadcasts everything.
diff --git a/polkadot/node/core/approval-voting/src/persisted_entries.rs b/polkadot/node/core/approval-voting/src/persisted_entries.rs
index aaeeda98b86..f5be1a5d885 100644
--- a/polkadot/node/core/approval-voting/src/persisted_entries.rs
+++ b/polkadot/node/core/approval-voting/src/persisted_entries.rs
@@ -217,12 +217,6 @@ impl ApprovalEntry {
 			(None, None)
 		}
 	}
-
-	/// For tests: set our assignment.
-	#[cfg(test)]
-	pub fn set_our_assignment(&mut self, our_assignment: OurAssignment) {
-		self.our_assignment = Some(our_assignment);
-	}
 }
 
 impl From<crate::approval_db::v1::ApprovalEntry> for ApprovalEntry {
@@ -294,11 +288,6 @@ impl CandidateEntry {
 	pub fn approval_entry(&self, block_hash: &Hash) -> Option<&ApprovalEntry> {
 		self.block_assignments.get(block_hash)
 	}
-
-	#[cfg(test)]
-	pub fn add_approval_entry(&mut self, block_hash: Hash, approval_entry: ApprovalEntry) {
-		self.block_assignments.insert(block_hash, approval_entry);
-	}
 }
 
 impl From<crate::approval_db::v1::CandidateEntry> for CandidateEntry {
@@ -384,26 +373,6 @@ impl BlockEntry {
 		})
 	}
 
-	/// For tests: Add a candidate to the block entry. Returns the
-	/// index where the candidate was added.
-	///
-	/// Panics if the core is already used.
-	#[cfg(test)]
-	pub fn add_candidate(&mut self, core: CoreIndex, candidate_hash: CandidateHash) -> usize {
-		let pos = self.candidates.binary_search_by_key(&core, |(c, _)| *c).unwrap_err();
-
-		self.candidates.insert(pos, (core, candidate_hash));
-
-		// bug in bitvec?
-		if pos < self.approved_bitfield.len() {
-			self.approved_bitfield.insert(pos, false);
-		} else {
-			self.approved_bitfield.push(false);
-		}
-
-		pos
-	}
-
 	/// Get the slot of the block.
 	pub fn slot(&self) -> Slot {
 		self.slot
diff --git a/polkadot/node/core/approval-voting/src/tests.rs b/polkadot/node/core/approval-voting/src/tests.rs
index fc39815e3e9..9819991641c 100644
--- a/polkadot/node/core/approval-voting/src/tests.rs
+++ b/polkadot/node/core/approval-voting/src/tests.rs
@@ -1,4 +1,3 @@
-#![allow(dead_code)]
 // Copyright 2021 Parity Technologies (UK) Ltd.
 // This file is part of Polkadot.
 
@@ -26,7 +25,9 @@ use polkadot_node_subsystem::{
 use polkadot_node_subsystem_test_helpers as test_helpers;
 use polkadot_node_subsystem_util::TimeoutExt;
 use polkadot_overseer::HeadSupportsParachains;
-use polkadot_primitives::v1::{CandidateEvent, CoreIndex, GroupIndex, Header, ValidatorSignature};
+use polkadot_primitives::v1::{
+	CandidateEvent, CoreIndex, GroupIndex, Header, Id as ParaId, ValidatorSignature,
+};
 use std::time::Duration;
 
 use assert_matches::assert_matches;
@@ -60,14 +61,9 @@ struct TestSyncOracle {
 
 struct TestSyncOracleHandle {
 	done_syncing_receiver: oneshot::Receiver<()>,
-	flag: Arc<AtomicBool>,
 }
 
 impl TestSyncOracleHandle {
-	fn set_done(&self) {
-		self.flag.store(false, Ordering::SeqCst);
-	}
-
 	async fn await_mode_switch(self) {
 		let _ = self.done_syncing_receiver.await;
 	}
@@ -92,19 +88,13 @@ impl SyncOracle for TestSyncOracle {
 }
 
 // val - result of `is_major_syncing`.
-fn make_sync_oracle(val: bool) -> (TestSyncOracle, TestSyncOracleHandle) {
+fn make_sync_oracle(val: bool) -> (Box<dyn SyncOracle + Send>, TestSyncOracleHandle) {
 	let (tx, rx) = oneshot::channel();
 	let flag = Arc::new(AtomicBool::new(val));
+	let oracle = TestSyncOracle { flag, done_syncing_sender: Arc::new(Mutex::new(Some(tx))) };
+	let handle = TestSyncOracleHandle { done_syncing_receiver: rx };
 
-	(
-		TestSyncOracle { flag: flag.clone(), done_syncing_sender: Arc::new(Mutex::new(Some(tx))) },
-		TestSyncOracleHandle { flag, done_syncing_receiver: rx },
-	)
-}
-
-fn done_syncing_oracle() -> Box<dyn SyncOracle + Send> {
-	let (oracle, _) = make_sync_oracle(false);
-	Box::new(oracle)
+	(Box::new(oracle), handle)
 }
 
 #[cfg(test)]
@@ -172,14 +162,16 @@ impl MockClockInner {
 	fn wakeup_all(&mut self, up_to: Tick) {
 		// This finds the position of the first wakeup after
 		// the given tick, or the end of the map.
-		let drain_up_to =
-			self.wakeups.binary_search_by_key(&(up_to + 1), |w| w.0).unwrap_or_else(|i| i);
-
+		let drain_up_to = self.wakeups.partition_point(|w| w.0 <= up_to);
 		for (_, wakeup) in self.wakeups.drain(..drain_up_to) {
 			let _ = wakeup.send(());
 		}
 	}
 
+	fn next_wakeup(&self) -> Option<Tick> {
+		self.wakeups.iter().map(|w| w.0).next()
+	}
+
 	fn has_wakeup(&self, tick: Tick) -> bool {
 		self.wakeups.binary_search_by_key(&tick, |w| w.0).is_ok()
 	}
@@ -196,8 +188,7 @@ impl MockClockInner {
 	fn register_wakeup(&mut self, tick: Tick, pre_emptive: bool) -> oneshot::Receiver<()> {
 		let (tx, rx) = oneshot::channel();
 
-		let pos = self.wakeups.binary_search_by_key(&tick, |w| w.0).unwrap_or_else(|i| i);
-
+		let pos = self.wakeups.partition_point(|w| w.0 <= tick);
 		self.wakeups.insert(pos, (tick, tx));
 
 		if pre_emptive {
@@ -215,7 +206,7 @@ struct MockAssignmentCriteria<Compute, Check>(Compute, Check);
 impl<Compute, Check> AssignmentCriteria for MockAssignmentCriteria<Compute, Check>
 where
 	Compute: Fn() -> HashMap<polkadot_primitives::v1::CoreIndex, criteria::OurAssignment>,
-	Check: Fn() -> Result<DelayTranche, criteria::InvalidAssignment>,
+	Check: Fn(ValidatorIndex) -> Result<DelayTranche, criteria::InvalidAssignment>,
 {
 	fn compute_assignments(
 		&self,
@@ -234,13 +225,13 @@ where
 	fn check_assignment_cert(
 		&self,
 		_claimed_core_index: polkadot_primitives::v1::CoreIndex,
-		_validator_index: ValidatorIndex,
+		validator_index: ValidatorIndex,
 		_config: &criteria::Config,
 		_relay_vrf_story: polkadot_node_primitives::approval::RelayVRFStory,
 		_assignment: &polkadot_node_primitives::approval::AssignmentCert,
 		_backing_group: polkadot_primitives::v1::GroupIndex,
 	) -> Result<polkadot_node_primitives::approval::DelayTranche, criteria::InvalidAssignment> {
-		self.1()
+		self.1(validator_index)
 	}
 }
 
@@ -255,15 +246,15 @@ impl<F>
 	}
 }
 
-#[derive(Default)]
-struct TestStore {
+#[derive(Default, Clone)]
+struct TestStoreInner {
 	stored_block_range: Option<StoredBlockRange>,
 	blocks_at_height: HashMap<BlockNumber, Vec<Hash>>,
 	block_entries: HashMap<Hash, BlockEntry>,
 	candidate_entries: HashMap<CandidateHash, CandidateEntry>,
 }
 
-impl Backend for TestStore {
+impl Backend for TestStoreInner {
 	fn load_block_entry(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>> {
 		Ok(self.block_entries.get(block_hash).cloned())
 	}
@@ -326,6 +317,49 @@ impl Backend for TestStore {
 	}
 }
 
+#[derive(Default, Clone)]
+pub struct TestStore {
+	store: Arc<Mutex<TestStoreInner>>,
+}
+
+impl Backend for TestStore {
+	fn load_block_entry(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>> {
+		let store = self.store.lock();
+		store.load_block_entry(block_hash)
+	}
+
+	fn load_candidate_entry(
+		&self,
+		candidate_hash: &CandidateHash,
+	) -> SubsystemResult<Option<CandidateEntry>> {
+		let store = self.store.lock();
+		store.load_candidate_entry(candidate_hash)
+	}
+
+	fn load_blocks_at_height(&self, height: &BlockNumber) -> SubsystemResult<Vec<Hash>> {
+		let store = self.store.lock();
+		store.load_blocks_at_height(height)
+	}
+
+	fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>> {
+		let store = self.store.lock();
+		store.load_all_blocks()
+	}
+
+	fn load_stored_blocks(&self) -> SubsystemResult<Option<StoredBlockRange>> {
+		let store = self.store.lock();
+		store.load_stored_blocks()
+	}
+
+	fn write<I>(&mut self, ops: I) -> SubsystemResult<()>
+	where
+		I: IntoIterator<Item = BackendWriteOp>,
+	{
+		let mut store = self.store.lock();
+		store.write(ops)
+	}
+}
+
 fn garbage_assignment_cert(kind: AssignmentCertKind) -> AssignmentCert {
 	let ctx = schnorrkel::signing_context(RELAY_VRF_MODULO_CONTEXT);
 	let msg = b"test-garbage";
@@ -347,22 +381,75 @@ fn sign_approval(
 
 type VirtualOverseer = test_helpers::TestSubsystemContextHandle<ApprovalVotingMessage>;
 
-struct TestHarness {
-	virtual_overseer: VirtualOverseer,
-	clock: Box<MockClock>,
+#[derive(Default)]
+struct HarnessConfigBuilder {
+	sync_oracle: Option<(Box<dyn SyncOracle + Send>, TestSyncOracleHandle)>,
+	clock: Option<MockClock>,
+	backend: Option<TestStore>,
+	assignment_criteria: Option<Box<dyn AssignmentCriteria + Send + Sync + 'static>>,
+}
+
+impl HarnessConfigBuilder {
+	pub fn assignment_criteria(
+		&mut self,
+		assignment_criteria: Box<dyn AssignmentCriteria + Send + Sync + 'static>,
+	) -> &mut Self {
+		self.assignment_criteria = Some(assignment_criteria);
+		self
+	}
+
+	pub fn build(&mut self) -> HarnessConfig {
+		let (sync_oracle, sync_oracle_handle) =
+			self.sync_oracle.take().unwrap_or_else(|| make_sync_oracle(false));
+
+		let assignment_criteria = self
+			.assignment_criteria
+			.take()
+			.unwrap_or_else(|| Box::new(MockAssignmentCriteria::check_only(|_| Ok(0))));
+
+		HarnessConfig {
+			sync_oracle,
+			sync_oracle_handle,
+			clock: self.clock.take().unwrap_or_else(|| MockClock::new(0)),
+			backend: self.backend.take().unwrap_or_else(|| TestStore::default()),
+			assignment_criteria,
+		}
+	}
 }
 
-#[derive(Default)]
 struct HarnessConfig {
-	tick_start: Tick,
-	assigned_tranche: DelayTranche,
+	sync_oracle: Box<dyn SyncOracle + Send>,
+	sync_oracle_handle: TestSyncOracleHandle,
+	clock: MockClock,
+	backend: TestStore,
+	assignment_criteria: Box<dyn AssignmentCriteria + Send + Sync + 'static>,
+}
+
+impl HarnessConfig {
+	pub fn backend(&self) -> TestStore {
+		self.backend.clone()
+	}
+}
+
+impl Default for HarnessConfig {
+	fn default() -> Self {
+		HarnessConfigBuilder::default().build()
+	}
+}
+
+struct TestHarness {
+	virtual_overseer: VirtualOverseer,
+	clock: Box<MockClock>,
+	sync_oracle_handle: TestSyncOracleHandle,
 }
 
 fn test_harness<T: Future<Output = VirtualOverseer>>(
 	config: HarnessConfig,
-	sync_oracle: Box<dyn SyncOracle + Send>,
 	test: impl FnOnce(TestHarness) -> T,
 ) {
+	let HarnessConfig { sync_oracle, sync_oracle_handle, clock, backend, assignment_criteria } =
+		config;
+
 	let pool = sp_core::testing::TaskExecutor::new();
 	let (context, virtual_overseer) = test_helpers::make_subsystem_context(pool);
 
@@ -372,26 +459,25 @@ fn test_harness<T: Future<Output = VirtualOverseer>>(
 		Some(&Sr25519Keyring::Alice.to_seed()),
 	);
 
-	let store = TestStore::default();
-
-	let HarnessConfig { tick_start, assigned_tranche } = config;
-
-	let clock = Box::new(MockClock::new(tick_start));
+	let clock = Box::new(clock);
 	let subsystem = run(
 		context,
 		ApprovalVotingSubsystem::with_config(
-			Config { col_data: test_constants::TEST_CONFIG.col_data, slot_duration_millis: 100u64 },
+			Config {
+				col_data: test_constants::TEST_CONFIG.col_data,
+				slot_duration_millis: SLOT_DURATION_MILLIS,
+			},
 			Arc::new(kvdb_memorydb::create(test_constants::NUM_COLUMNS)),
 			Arc::new(keystore),
 			sync_oracle,
 			Metrics::default(),
 		),
 		clock.clone(),
-		Box::new(MockAssignmentCriteria::check_only(move || Ok(assigned_tranche))),
-		store,
+		assignment_criteria,
+		backend,
 	);
 
-	let test_fut = test(TestHarness { virtual_overseer, clock });
+	let test_fut = test(TestHarness { virtual_overseer, clock, sync_oracle_handle });
 
 	futures::pin_mut!(test_fut);
 	futures::pin_mut!(subsystem);
@@ -443,387 +529,124 @@ async fn overseer_signal(overseer: &mut VirtualOverseer, signal: OverseerSignal)
 		.expect(&format!("{:?} is more than enough for sending signals.", TIMEOUT));
 }
 
-#[test]
-fn blank_subsystem_act_on_bad_block() {
-	let (oracle, handle) = make_sync_oracle(false);
-	test_harness(Default::default(), Box::new(oracle), |test_harness| async move {
-		let TestHarness { mut virtual_overseer, .. } = test_harness;
-
-		let (tx, rx) = oneshot::channel();
-
-		let bad_block_hash: Hash = Default::default();
-
-		overseer_send(
-			&mut virtual_overseer,
-			FromOverseer::Communication {
-				msg: ApprovalVotingMessage::CheckAndImportAssignment(
-					IndirectAssignmentCert {
-						block_hash: bad_block_hash.clone(),
-						validator: 0u32.into(),
-						cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
-							sample: 0,
-						}),
-					},
-					0u32,
-					tx,
-				),
-			},
-		)
-		.await;
-
-		handle.await_mode_switch().await;
-
-		assert_matches!(
-			rx.await,
-			Ok(
-				AssignmentCheckResult::Bad(AssignmentCheckError::UnknownBlock(hash))
-			) => {
-				assert_eq!(hash, bad_block_hash);
-			}
-		);
-
-		virtual_overseer
-	});
+fn overlay_txn<T, F>(db: &mut T, mut f: F)
+where
+	T: Backend,
+	F: FnMut(&mut OverlayedBackend<'_, T>),
+{
+	let mut overlay_db = OverlayedBackend::new(db);
+	f(&mut overlay_db);
+	let write_ops = overlay_db.into_write_ops();
+	db.write(write_ops).unwrap();
 }
 
-#[test]
-fn ss_rejects_approval_if_no_block_entry() {
-	let (oracle, _handle) = make_sync_oracle(false);
-	test_harness(Default::default(), Box::new(oracle), |test_harness| async move {
-		let TestHarness { mut virtual_overseer, .. } = test_harness;
-
-		let block_hash = Hash::repeat_byte(0x01);
-		let candidate_index = 0;
-		let validator = ValidatorIndex(0);
-		let candidate_hash = CandidateReceipt::<Hash>::default().hash();
-		let session_index = 1;
-
-		let rx = cai_approval(
-			&mut virtual_overseer,
-			block_hash,
-			candidate_index,
-			validator,
-			candidate_hash,
-			session_index,
-			false,
-		)
-		.await;
+fn make_candidate(para_id: ParaId, hash: &Hash) -> CandidateReceipt {
+	let mut r = CandidateReceipt::default();
+	r.descriptor.para_id = para_id;
+	r.descriptor.relay_parent = hash.clone();
+	r
+}
 
+async fn check_and_import_approval(
+	overseer: &mut VirtualOverseer,
+	block_hash: Hash,
+	candidate_index: CandidateIndex,
+	validator: ValidatorIndex,
+	candidate_hash: CandidateHash,
+	session_index: SessionIndex,
+	expect_chain_approved: bool,
+	expect_coordinator: bool,
+	signature_opt: Option<ValidatorSignature>,
+) -> oneshot::Receiver<ApprovalCheckResult> {
+	let signature = signature_opt.unwrap_or(sign_approval(
+		Sr25519Keyring::Alice,
+		candidate_hash,
+		session_index,
+	));
+	let (tx, rx) = oneshot::channel();
+	overseer_send(
+		overseer,
+		FromOverseer::Communication {
+			msg: ApprovalVotingMessage::CheckAndImportApproval(
+				IndirectSignedApprovalVote { block_hash, candidate_index, validator, signature },
+				tx,
+			),
+		},
+	)
+	.await;
+	if expect_chain_approved {
 		assert_matches!(
-			rx.await,
-			Ok(ApprovalCheckResult::Bad(ApprovalCheckError::UnknownBlock(hash))) => {
-				assert_eq!(hash, block_hash);
+			overseer_recv(overseer).await,
+			AllMessages::ChainSelection(ChainSelectionMessage::Approved(b_hash)) => {
+				assert_eq!(b_hash, block_hash);
 			}
 		);
-
-		virtual_overseer
-	});
-}
-
-#[test]
-fn ss_rejects_approval_before_assignment() {
-	let (oracle, _handle) = make_sync_oracle(false);
-	test_harness(Default::default(), Box::new(oracle), |test_harness| async move {
-		let TestHarness { mut virtual_overseer, .. } = test_harness;
-
-		let block_hash = Hash::repeat_byte(0x01);
-
-		let candidate_hash = {
-			let mut candidate_receipt = CandidateReceipt::<Hash>::default();
-			candidate_receipt.descriptor.para_id = 1.into();
-			candidate_receipt.descriptor.relay_parent = block_hash;
-			candidate_receipt.hash()
-		};
-
-		let candidate_index = 0;
-		let validator = ValidatorIndex(0);
-		let session_index = 1;
-
-		// Add block hash 00.
-		ChainBuilder::new()
-			.add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1)
-			.build(&mut virtual_overseer)
-			.await;
-
-		let rx = cai_approval(
-			&mut virtual_overseer,
-			block_hash,
-			candidate_index,
-			validator,
-			candidate_hash,
-			session_index,
-			false,
-		)
-		.await;
-
+	}
+	if expect_coordinator {
 		assert_matches!(
-			rx.await,
-			Ok(ApprovalCheckResult::Bad(ApprovalCheckError::NoAssignment(v))) => {
-				assert_eq!(v, validator);
+			overseer_recv(overseer).await,
+			AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::ImportStatements {
+				candidate_hash: c_hash,
+				pending_confirmation,
+				..
+			}) => {
+				assert_eq!(c_hash, candidate_hash);
+				let _ = pending_confirmation.send(ImportStatementsResult::ValidImport);
 			}
 		);
-
-		virtual_overseer
-	});
+	}
+	rx
 }
 
-#[test]
-fn ss_rejects_assignment_in_future() {
-	let (oracle, _handle) = make_sync_oracle(false);
-	test_harness(
-		HarnessConfig {
-			tick_start: 0,
-			assigned_tranche: TICK_TOO_FAR_IN_FUTURE as _,
-			..Default::default()
+async fn check_and_import_assignment(
+	overseer: &mut VirtualOverseer,
+	block_hash: Hash,
+	candidate_index: CandidateIndex,
+	validator: ValidatorIndex,
+) -> oneshot::Receiver<AssignmentCheckResult> {
+	let (tx, rx) = oneshot::channel();
+	overseer_send(
+		overseer,
+		FromOverseer::Communication {
+			msg: ApprovalVotingMessage::CheckAndImportAssignment(
+				IndirectAssignmentCert {
+					block_hash,
+					validator,
+					cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
+				},
+				candidate_index,
+				tx,
+			),
 		},
-		Box::new(oracle),
-		|test_harness| async move {
-			let TestHarness { mut virtual_overseer, clock } = test_harness;
-
-			let block_hash = Hash::repeat_byte(0x01);
-			let candidate_index = 0;
-			let validator = ValidatorIndex(0);
-
-			// Add block hash 00.
-			ChainBuilder::new()
-				.add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1)
-				.build(&mut virtual_overseer)
-				.await;
-
-			let rx =
-				cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await;
-
-			assert_eq!(rx.await, Ok(AssignmentCheckResult::TooFarInFuture));
-
-			// Advance clock to make assignment reasonably near.
-			clock.inner.lock().set_tick(1);
-
-			let rx =
-				cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await;
-
-			assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
+	)
+	.await;
+	rx
+}
 
-			virtual_overseer
-		},
-	);
+struct BlockConfig {
+	slot: Slot,
+	candidates: Option<Vec<(CandidateReceipt, CoreIndex, GroupIndex)>>,
+	session_info: Option<SessionInfo>,
 }
 
-#[test]
-fn ss_accepts_duplicate_assignment() {
-	let (oracle, _handle) = make_sync_oracle(false);
-	test_harness(Default::default(), Box::new(oracle), |test_harness| async move {
-		let TestHarness { mut virtual_overseer, .. } = test_harness;
+struct ChainBuilder {
+	blocks_by_hash: HashMap<Hash, (Header, BlockConfig)>,
+	blocks_at_height: BTreeMap<u32, Vec<Hash>>,
+}
 
-		let block_hash = Hash::repeat_byte(0x01);
-		let candidate_index = 0;
-		let validator = ValidatorIndex(0);
-
-		// Add block hash 00.
-		ChainBuilder::new()
-			.add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1)
-			.build(&mut virtual_overseer)
-			.await;
-
-		let rx =
-			cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await;
-
-		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
-
-		let rx =
-			cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await;
-
-		assert_eq!(rx.await, Ok(AssignmentCheckResult::AcceptedDuplicate));
-
-		virtual_overseer
-	});
-}
-
-#[test]
-fn ss_rejects_assignment_with_unknown_candidate() {
-	let (oracle, _handle) = make_sync_oracle(false);
-	test_harness(Default::default(), Box::new(oracle), |test_harness| async move {
-		let TestHarness { mut virtual_overseer, .. } = test_harness;
-
-		let block_hash = Hash::repeat_byte(0x01);
-		let candidate_index = 7;
-		let validator = ValidatorIndex(0);
-
-		// Add block hash 00.
-		ChainBuilder::new()
-			.add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1)
-			.build(&mut virtual_overseer)
-			.await;
-
-		let rx =
-			cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await;
-
-		assert_eq!(
-			rx.await,
-			Ok(AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCandidateIndex(
-				candidate_index
-			))),
-		);
-
-		virtual_overseer
-	});
-}
-
-#[test]
-fn ss_accepts_and_imports_approval_after_assignment() {
-	let (oracle, _handle) = make_sync_oracle(false);
-	test_harness(Default::default(), Box::new(oracle), |test_harness| async move {
-		let TestHarness { mut virtual_overseer, .. } = test_harness;
-
-		let block_hash = Hash::repeat_byte(0x01);
-
-		let candidate_hash = {
-			let mut candidate_receipt = CandidateReceipt::<Hash>::default();
-			candidate_receipt.descriptor.para_id = 1.into();
-			candidate_receipt.descriptor.relay_parent = block_hash;
-			candidate_receipt.hash()
-		};
-
-		let candidate_index = 0;
-		let validator = ValidatorIndex(0);
-		let session_index = 1;
-
-		// Add block hash 0x01...
-		ChainBuilder::new()
-			.add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1)
-			.build(&mut virtual_overseer)
-			.await;
-
-		let rx =
-			cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await;
-
-		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
-
-		let rx = cai_approval(
-			&mut virtual_overseer,
-			block_hash,
-			candidate_index,
-			validator,
-			candidate_hash,
-			session_index,
-			true,
-		)
-		.await;
-
-		assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted));
-
-		virtual_overseer
-	});
-}
-#[test]
-fn ss_assignment_import_updates_candidate_entry_and_schedules_wakeup() {
-	let (oracle, _handle) = make_sync_oracle(false);
-	test_harness(Default::default(), Box::new(oracle), |test_harness| async move {
-		let TestHarness { mut virtual_overseer, .. } = test_harness;
-
-		let block_hash = Hash::repeat_byte(0x01);
-
-		let _candidate_hash = {
-			let mut candidate_receipt = CandidateReceipt::<Hash>::default();
-			candidate_receipt.descriptor.para_id = 1.into();
-			candidate_receipt.descriptor.relay_parent = block_hash;
-			candidate_receipt.hash()
-		};
-
-		let candidate_index = 0;
-		let validator = ValidatorIndex(0);
-
-		// Add block hash 0x01...
-		ChainBuilder::new()
-			.add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1)
-			.build(&mut virtual_overseer)
-			.await;
-
-		let rx =
-			cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await;
-
-		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
-
-		// TODO(ladi): fix
-		//assert!(clock.inner.lock().has_wakeup(20));
-
-		virtual_overseer
-	});
-}
-
-async fn cai_approval(
-	overseer: &mut VirtualOverseer,
-	block_hash: Hash,
-	candidate_index: CandidateIndex,
-	validator: ValidatorIndex,
-	candidate_hash: CandidateHash,
-	session_index: SessionIndex,
-	expect_coordinator: bool,
-) -> oneshot::Receiver<ApprovalCheckResult> {
-	let signature = sign_approval(Sr25519Keyring::Alice, candidate_hash, session_index);
-	let (tx, rx) = oneshot::channel();
-	overseer_send(
-		overseer,
-		FromOverseer::Communication {
-			msg: ApprovalVotingMessage::CheckAndImportApproval(
-				IndirectSignedApprovalVote { block_hash, candidate_index, validator, signature },
-				tx,
-			),
-		},
-	)
-	.await;
-
-	if expect_coordinator {
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::ImportStatements {
-				pending_confirmation,
-				..
-			}) => {
-				let _ = pending_confirmation.send(ImportStatementsResult::ValidImport);
-			}
-		);
-	}
-	rx
-}
-
-async fn cai_assignment(
-	overseer: &mut VirtualOverseer,
-	block_hash: Hash,
-	candidate_index: CandidateIndex,
-	validator: ValidatorIndex,
-) -> oneshot::Receiver<AssignmentCheckResult> {
-	let (tx, rx) = oneshot::channel();
-	overseer_send(
-		overseer,
-		FromOverseer::Communication {
-			msg: ApprovalVotingMessage::CheckAndImportAssignment(
-				IndirectAssignmentCert {
-					block_hash,
-					validator,
-					cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
-				},
-				candidate_index,
-				tx,
-			),
-		},
-	)
-	.await;
-	rx
-}
-
-struct ChainBuilder {
-	blocks_by_hash: HashMap<Hash, Header>,
-	blocks_at_height: BTreeMap<u32, Vec<Hash>>,
-}
-
-impl ChainBuilder {
-	const GENESIS_HASH: Hash = Hash::repeat_byte(0xff);
-	const GENESIS_PARENT_HASH: Hash = Hash::repeat_byte(0x00);
+impl ChainBuilder {
+	const GENESIS_HASH: Hash = Hash::repeat_byte(0xff);
+	const GENESIS_PARENT_HASH: Hash = Hash::repeat_byte(0x00);
 
 	pub fn new() -> Self {
 		let mut builder =
 			Self { blocks_by_hash: HashMap::new(), blocks_at_height: BTreeMap::new() };
-		builder.add_block_inner(Self::GENESIS_HASH, Self::GENESIS_PARENT_HASH, Slot::from(0), 0);
+		builder.add_block_inner(
+			Self::GENESIS_HASH,
+			Self::GENESIS_PARENT_HASH,
+			0,
+			BlockConfig { slot: Slot::from(0), candidates: None, session_info: None },
+		);
 		builder
 	}
 
@@ -831,8 +654,8 @@ impl ChainBuilder {
 		&'a mut self,
 		hash: Hash,
 		parent_hash: Hash,
-		slot: Slot,
 		number: u32,
+		config: BlockConfig,
 	) -> &'a mut Self {
 		assert!(number != 0, "cannot add duplicate genesis block");
 		assert!(hash != Self::GENESIS_HASH, "cannot add block with genesis hash");
@@ -841,19 +664,19 @@ impl ChainBuilder {
 			"cannot add block with genesis parent hash"
 		);
 		assert!(self.blocks_by_hash.len() < u8::MAX.into());
-		self.add_block_inner(hash, parent_hash, slot, number)
+		self.add_block_inner(hash, parent_hash, number, config)
 	}
 
 	fn add_block_inner<'a>(
 		&'a mut self,
 		hash: Hash,
 		parent_hash: Hash,
-		slot: Slot,
 		number: u32,
+		config: BlockConfig,
 	) -> &'a mut Self {
-		let header = ChainBuilder::make_header(parent_hash, slot, number);
+		let header = ChainBuilder::make_header(parent_hash, config.slot, number);
 		assert!(
-			self.blocks_by_hash.insert(hash, header).is_none(),
+			self.blocks_by_hash.insert(hash, (header, config)).is_none(),
 			"block with hash {:?} already exists",
 			hash,
 		);
@@ -865,15 +688,19 @@ impl ChainBuilder {
 		for (number, blocks) in self.blocks_at_height.iter() {
 			for (i, hash) in blocks.iter().enumerate() {
 				let mut cur_hash = *hash;
+				let (_, block_config) =
+					self.blocks_by_hash.get(&cur_hash).expect("block not found");
 				let mut ancestry = Vec::new();
 				while cur_hash != Self::GENESIS_PARENT_HASH {
-					let cur_header =
+					let (cur_header, _) =
 						self.blocks_by_hash.get(&cur_hash).expect("chain is not contiguous");
 					ancestry.push((cur_hash, cur_header.clone()));
 					cur_hash = cur_header.parent_hash;
 				}
 				ancestry.reverse();
-				import_block(overseer, ancestry.as_ref(), *number, false, i > 0).await;
+
+				import_block(overseer, ancestry.as_ref(), *number, block_config, false, i > 0)
+					.await;
 				let _: Option<()> = future::pending().timeout(Duration::from_millis(100)).await;
 			}
 		}
@@ -902,31 +729,40 @@ impl ChainBuilder {
 async fn import_block(
 	overseer: &mut VirtualOverseer,
 	hashes: &[(Hash, Header)],
-	session: u32,
+	number: u32,
+	config: &BlockConfig,
 	gap: bool,
 	fork: bool,
 ) {
-	let validators = vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob];
-	let session_info = SessionInfo {
-		validators: validators.iter().map(|v| v.public().into()).collect(),
-		discovery_keys: validators.iter().map(|v| v.public().into()).collect(),
-		assignment_keys: validators.iter().map(|v| v.public().into()).collect(),
-		validator_groups: vec![vec![ValidatorIndex(0)], vec![ValidatorIndex(1)]],
-		n_cores: 6,
-		needed_approvals: 1,
-		zeroth_delay_tranche_width: 5,
-		relay_vrf_modulo_samples: 3,
-		n_delay_tranches: 50,
-		no_show_slots: 2,
-	};
-
 	let (new_head, new_header) = &hashes[hashes.len() - 1];
+	let candidates = config.candidates.clone().unwrap_or(vec![(
+		make_candidate(0.into(), &new_head),
+		CoreIndex(0),
+		GroupIndex(0),
+	)]);
+
+	let session_info = config.session_info.clone().unwrap_or({
+		let validators = vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob];
+		SessionInfo {
+			validators: validators.iter().map(|v| v.public().into()).collect(),
+			discovery_keys: validators.iter().map(|v| v.public().into()).collect(),
+			assignment_keys: validators.iter().map(|v| v.public().into()).collect(),
+			validator_groups: vec![vec![ValidatorIndex(0)], vec![ValidatorIndex(1)]],
+			n_cores: validators.len() as _,
+			needed_approvals: 1,
+			zeroth_delay_tranche_width: 5,
+			relay_vrf_modulo_samples: 3,
+			n_delay_tranches: 50,
+			no_show_slots: 2,
+		}
+	});
+
 	overseer_send(
 		overseer,
 		FromOverseer::Signal(OverseerSignal::ActiveLeaves(ActiveLeavesUpdate::start_work(
 			ActivatedLeaf {
 				hash: *new_head,
-				number: session,
+				number,
 				status: LeafStatus::Fresh,
 				span: Arc::new(jaeger::Span::Disabled),
 			},
@@ -950,9 +786,9 @@ async fn import_block(
 				RuntimeApiRequest::SessionIndexForChild(s_tx)
 			)
 		) => {
-			let hash = &hashes[session.saturating_sub(1) as usize];
+			let hash = &hashes[number.saturating_sub(1) as usize];
 			assert_eq!(req_block_hash, hash.0.clone());
-			s_tx.send(Ok(session.into())).unwrap();
+			s_tx.send(Ok(number.into())).unwrap();
 		}
 	);
 
@@ -965,7 +801,7 @@ async fn import_block(
 					RuntimeApiRequest::SessionInfo(idx, si_tx),
 				)
 			) => {
-				assert_eq!(session, idx);
+				assert_eq!(number, idx);
 				assert_eq!(req_block_hash, *new_head);
 				si_tx.send(Ok(Some(session_info.clone()))).unwrap();
 			}
@@ -1000,7 +836,7 @@ async fn import_block(
 						response_channel,
 					}) => {
 						assert_eq!(hash, *new_head);
-						assert_eq!(k as u32, session - 1);
+						assert_eq!(k as u32, number - 1);
 						let history: Vec<Hash> = hashes.iter().map(|v| v.0).take(k).collect();
 						response_channel.send(Ok(history)).unwrap();
 					},
@@ -1010,25 +846,13 @@ async fn import_block(
 		}
 	}
 
-	if session > 0 {
+	if number > 0 {
 		assert_matches!(
 			overseer_recv(overseer).await,
 			AllMessages::RuntimeApi(
 				RuntimeApiMessage::Request(hash, RuntimeApiRequest::CandidateEvents(c_tx))
 			) => {
 				assert_eq!(hash, *new_head);
-
-				let make_candidate = |para_id| {
-					let mut r = CandidateReceipt::default();
-					r.descriptor.para_id = para_id;
-					r.descriptor.relay_parent = hash;
-					r
-				};
-				let candidates = vec![
-					(make_candidate(1.into()), CoreIndex(0), GroupIndex(2)),
-					(make_candidate(2.into()), CoreIndex(1), GroupIndex(3)),
-				];
-
 				let inclusion_events = candidates.into_iter()
 					.map(|(r, c, g)| CandidateEvent::CandidateIncluded(r, Vec::new().into(), c, g))
 					.collect::<Vec<_>>();
@@ -1044,9 +868,9 @@ async fn import_block(
 					RuntimeApiRequest::SessionIndexForChild(s_tx)
 				)
 			) => {
-				let hash = &hashes[(session-1) as usize];
+				let hash = &hashes[(number-1) as usize];
 				assert_eq!(req_block_hash, hash.0.clone());
-				s_tx.send(Ok(session.into())).unwrap();
+				s_tx.send(Ok(number.into())).unwrap();
 			}
 		);
 
@@ -1058,10 +882,10 @@ async fn import_block(
 					RuntimeApiRequest::CurrentBabeEpoch(c_tx),
 				)
 			) => {
-				let hash = &hashes[session as usize];
+				let hash = &hashes[number as usize];
 				assert_eq!(req_block_hash, hash.0.clone());
 				let _ = c_tx.send(Ok(BabeEpoch {
-					epoch_index: session as _,
+					epoch_index: number as _,
 					start_slot: Slot::from(0),
 					duration: 200,
 					authorities: vec![(Sr25519Keyring::Alice.public().into(), 1)],
@@ -1075,7 +899,7 @@ async fn import_block(
 		);
 	}
 
-	if session == 0 {
+	if number == 0 {
 		assert_matches!(
 			overseer_recv(overseer).await,
 			AllMessages::ApprovalDistribution(ApprovalDistributionMessage::NewBlocks(v)) => {
@@ -1090,44 +914,123 @@ async fn import_block(
 			) => {
 				assert_eq!(approval_vec.len(), 1);
 				let metadata = approval_vec.pop().unwrap();
-				let hash = &hashes[session as usize];
-				let parent_hash = &hashes[(session - 1) as usize];
+				let hash = &hashes[number as usize];
+				let parent_hash = &hashes[(number - 1) as usize];
 				assert_eq!(metadata.hash, hash.0.clone());
 				assert_eq!(metadata.parent_hash, parent_hash.0.clone());
-				assert_eq!(metadata.slot, Slot::from(session as u64));
+				assert_eq!(metadata.slot, config.slot);
 			}
 		);
 	}
 }
 
 #[test]
-fn linear_import_act_on_leaf() {
-	let session = 3u32;
+fn subsystem_rejects_bad_assignment_ok_criteria() {
+	test_harness(HarnessConfig::default(), |test_harness| async move {
+		let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } =
+			test_harness;
 
-	let (oracle, _handle) = make_sync_oracle(false);
-	test_harness(Default::default(), Box::new(oracle), |test_harness| async move {
-		let TestHarness { mut virtual_overseer, .. } = test_harness;
+		let block_hash = Hash::repeat_byte(0x01);
+		let candidate_index = 0;
+		let validator = ValidatorIndex(0);
 
-		let mut head: Hash = ChainBuilder::GENESIS_HASH;
+		let head: Hash = ChainBuilder::GENESIS_HASH;
 		let mut builder = ChainBuilder::new();
-		for i in 1..session {
-			let slot = Slot::from(i as u64);
+		let slot = Slot::from(1 as u64);
+		builder.add_block(
+			block_hash,
+			head,
+			1,
+			BlockConfig { slot, candidates: None, session_info: None },
+		);
+		builder.build(&mut virtual_overseer).await;
 
-			let hash = Hash::repeat_byte(i as u8);
-			builder.add_block(hash, head, slot, i);
-			head = hash;
-		}
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),);
+
+		// unknown hash
+		let unknown_hash = Hash::repeat_byte(0x02);
+
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			unknown_hash,
+			candidate_index,
+			validator,
+		)
+		.await;
+
+		assert_eq!(
+			rx.await,
+			Ok(AssignmentCheckResult::Bad(AssignmentCheckError::UnknownBlock(unknown_hash))),
+		);
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn subsystem_rejects_bad_assignment_err_criteria() {
+	let assignment_criteria =
+		Box::new(MockAssignmentCriteria::check_only(move |_| Err(criteria::InvalidAssignment)));
+	let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build();
+	test_harness(config, |test_harness| async move {
+		let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } =
+			test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+		let candidate_index = 0;
+		let validator = ValidatorIndex(0);
 
+		let head: Hash = ChainBuilder::GENESIS_HASH;
+		let mut builder = ChainBuilder::new();
+		let slot = Slot::from(1 as u64);
+		builder.add_block(
+			block_hash,
+			head,
+			1,
+			BlockConfig { slot, candidates: None, session_info: None },
+		);
 		builder.build(&mut virtual_overseer).await;
 
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+		)
+		.await;
+
+		assert_eq!(
+			rx.await,
+			Ok(AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCert(ValidatorIndex(0)))),
+		);
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn blank_subsystem_act_on_bad_block() {
+	test_harness(HarnessConfig::default(), |test_harness| async move {
+		let TestHarness { mut virtual_overseer, sync_oracle_handle, .. } = test_harness;
+
 		let (tx, rx) = oneshot::channel();
 
+		let bad_block_hash: Hash = Default::default();
+
 		overseer_send(
 			&mut virtual_overseer,
 			FromOverseer::Communication {
 				msg: ApprovalVotingMessage::CheckAndImportAssignment(
 					IndirectAssignmentCert {
-						block_hash: head,
+						block_hash: bad_block_hash.clone(),
 						validator: 0u32.into(),
 						cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
 							sample: 0,
@@ -1140,61 +1043,1515 @@ fn linear_import_act_on_leaf() {
 		)
 		.await;
 
-		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
+		sync_oracle_handle.await_mode_switch().await;
+
+		assert_matches!(
+			rx.await,
+			Ok(
+				AssignmentCheckResult::Bad(AssignmentCheckError::UnknownBlock(hash))
+			) => {
+				assert_eq!(hash, bad_block_hash);
+			}
+		);
 
 		virtual_overseer
 	});
 }
 
 #[test]
-fn forkful_import_at_same_height_act_on_leaf() {
-	let session = 3u32;
+fn subsystem_rejects_approval_if_no_candidate_entry() {
+	let config = HarnessConfig::default();
+	let store = config.backend();
+	test_harness(config, |test_harness| async move {
+		let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } =
+			test_harness;
 
-	let (oracle, _handle) = make_sync_oracle(false);
-	test_harness(Default::default(), Box::new(oracle), |test_harness| async move {
+		let block_hash = Hash::repeat_byte(0x01);
+		let candidate_index = 0;
+		let validator = ValidatorIndex(0);
+
+		let candidate_descriptor = make_candidate(1.into(), &block_hash);
+		let candidate_hash = candidate_descriptor.hash();
+
+		let head: Hash = ChainBuilder::GENESIS_HASH;
+		let mut builder = ChainBuilder::new();
+		let slot = Slot::from(1 as u64);
+		builder.add_block(
+			block_hash,
+			head,
+			1,
+			BlockConfig {
+				slot,
+				candidates: Some(vec![(candidate_descriptor, CoreIndex(1), GroupIndex(1))]),
+				session_info: None,
+			},
+		);
+		builder.build(&mut virtual_overseer).await;
+
+		overlay_txn(&mut store.clone(), |overlay_db| {
+			overlay_db.delete_candidate_entry(&candidate_hash)
+		});
+
+		let session_index = 1;
+		let rx = check_and_import_approval(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+			candidate_hash,
+			session_index,
+			false,
+			false,
+			None,
+		)
+		.await;
+
+		assert_matches!(
+			rx.await,
+			Ok(ApprovalCheckResult::Bad(ApprovalCheckError::InvalidCandidate(0, hash))) => {
+				assert_eq!(candidate_hash, hash);
+			}
+		);
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn subsystem_rejects_approval_if_no_block_entry() {
+	test_harness(HarnessConfig::default(), |test_harness| async move {
+		let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } =
+			test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+		let candidate_index = 0;
+		let validator = ValidatorIndex(0);
+		let candidate_hash = CandidateReceipt::<Hash>::default().hash();
+		let session_index = 1;
+
+		let rx = check_and_import_approval(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+			candidate_hash,
+			session_index,
+			false,
+			false,
+			None,
+		)
+		.await;
+
+		assert_matches!(
+			rx.await,
+			Ok(ApprovalCheckResult::Bad(ApprovalCheckError::UnknownBlock(hash))) => {
+				assert_eq!(hash, block_hash);
+			}
+		);
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn subsystem_rejects_approval_before_assignment() {
+	test_harness(HarnessConfig::default(), |test_harness| async move {
+		let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } =
+			test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+
+		let candidate_hash = {
+			let mut candidate_receipt = CandidateReceipt::<Hash>::default();
+			candidate_receipt.descriptor.para_id = 0.into();
+			candidate_receipt.descriptor.relay_parent = block_hash;
+			candidate_receipt.hash()
+		};
+
+		let candidate_index = 0;
+		let validator = ValidatorIndex(0);
+		let session_index = 1;
+
+		// Add block hash 00.
+		ChainBuilder::new()
+			.add_block(
+				block_hash,
+				ChainBuilder::GENESIS_HASH,
+				1,
+				BlockConfig { slot: Slot::from(1), candidates: None, session_info: None },
+			)
+			.build(&mut virtual_overseer)
+			.await;
+
+		let rx = check_and_import_approval(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+			candidate_hash,
+			session_index,
+			false,
+			false,
+			None,
+		)
+		.await;
+
+		assert_matches!(
+			rx.await,
+			Ok(ApprovalCheckResult::Bad(ApprovalCheckError::NoAssignment(v))) => {
+				assert_eq!(v, validator);
+			}
+		);
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn subsystem_rejects_assignment_in_future() {
+	let assignment_criteria =
+		Box::new(MockAssignmentCriteria::check_only(|_| Ok(TICK_TOO_FAR_IN_FUTURE as _)));
+	let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build();
+	test_harness(config, |test_harness| async move {
+		let TestHarness { mut virtual_overseer, clock, sync_oracle_handle: _sync_oracle_handle } =
+			test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+		let candidate_index = 0;
+		let validator = ValidatorIndex(0);
+
+		// Add block hash 00.
+		ChainBuilder::new()
+			.add_block(
+				block_hash,
+				ChainBuilder::GENESIS_HASH,
+				1,
+				BlockConfig { slot: Slot::from(0), candidates: None, session_info: None },
+			)
+			.build(&mut virtual_overseer)
+			.await;
+
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(AssignmentCheckResult::TooFarInFuture));
+
+		// Advance clock to make assignment reasonably near.
+		clock.inner.lock().set_tick(9);
+
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn subsystem_accepts_duplicate_assignment() {
+	test_harness(HarnessConfig::default(), |test_harness| async move {
+		let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } =
+			test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+		let candidate_index = 0;
+		let validator = ValidatorIndex(0);
+
+		// Add block hash 00.
+		ChainBuilder::new()
+			.add_block(
+				block_hash,
+				ChainBuilder::GENESIS_HASH,
+				1,
+				BlockConfig { slot: Slot::from(1), candidates: None, session_info: None },
+			)
+			.build(&mut virtual_overseer)
+			.await;
+
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
+
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(AssignmentCheckResult::AcceptedDuplicate));
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn subsystem_rejects_assignment_with_unknown_candidate() {
+	test_harness(HarnessConfig::default(), |test_harness| async move {
+		let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } =
+			test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+		let candidate_index = 7;
+		let validator = ValidatorIndex(0);
+
+		// Add block hash 00.
+		ChainBuilder::new()
+			.add_block(
+				block_hash,
+				ChainBuilder::GENESIS_HASH,
+				1,
+				BlockConfig { slot: Slot::from(1), candidates: None, session_info: None },
+			)
+			.build(&mut virtual_overseer)
+			.await;
+
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+		)
+		.await;
+
+		assert_eq!(
+			rx.await,
+			Ok(AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCandidateIndex(
+				candidate_index
+			))),
+		);
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn subsystem_accepts_and_imports_approval_after_assignment() {
+	test_harness(HarnessConfig::default(), |test_harness| async move {
+		let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } =
+			test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+
+		let candidate_hash = {
+			let mut candidate_receipt = CandidateReceipt::<Hash>::default();
+			candidate_receipt.descriptor.para_id = 0.into();
+			candidate_receipt.descriptor.relay_parent = block_hash;
+			candidate_receipt.hash()
+		};
+
+		let candidate_index = 0;
+		let validator = ValidatorIndex(0);
+		let session_index = 1;
+
+		// Add block hash 0x01...
+		ChainBuilder::new()
+			.add_block(
+				block_hash,
+				ChainBuilder::GENESIS_HASH,
+				1,
+				BlockConfig { slot: Slot::from(1), candidates: None, session_info: None },
+			)
+			.build(&mut virtual_overseer)
+			.await;
+
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
+
+		let rx = check_and_import_approval(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+			candidate_hash,
+			session_index,
+			true,
+			true,
+			None,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted));
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn subsystem_second_approval_import_only_schedules_wakeups() {
+	test_harness(HarnessConfig::default(), |test_harness| async move {
+		let TestHarness {
+			mut virtual_overseer,
+			clock,
+			sync_oracle_handle: _sync_oracle_handle,
+			..
+		} = test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+
+		let candidate_hash = {
+			let mut candidate_receipt = CandidateReceipt::<Hash>::default();
+			candidate_receipt.descriptor.para_id = 0.into();
+			candidate_receipt.descriptor.relay_parent = block_hash;
+			candidate_receipt.hash()
+		};
+
+		let candidate_index = 0;
+		let validator = ValidatorIndex(0);
+		let session_index = 1;
+
+		// Add block hash 0x01...
+		ChainBuilder::new()
+			.add_block(
+				block_hash,
+				ChainBuilder::GENESIS_HASH,
+				1,
+				BlockConfig { slot: Slot::from(0), candidates: None, session_info: None },
+			)
+			.build(&mut virtual_overseer)
+			.await;
+
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
+
+		let rx = check_and_import_approval(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+			candidate_hash,
+			session_index,
+			true,
+			true,
+			None,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted));
+
+		// The clock should already have wakeups from the prior operations. Clear them to assert
+		// that the second approval adds more wakeups.
+		assert!(clock.inner.lock().has_wakeup(20));
+		clock.inner.lock().wakeup_all(20);
+		assert!(!clock.inner.lock().has_wakeup(20));
+
+		let rx = check_and_import_approval(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+			candidate_hash,
+			session_index,
+			false,
+			false,
+			None,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted));
+
+		assert!(clock.inner.lock().has_wakeup(20));
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn subsystem_assignment_import_updates_candidate_entry_and_schedules_wakeup() {
+	test_harness(HarnessConfig::default(), |test_harness| async move {
+		let TestHarness {
+			mut virtual_overseer,
+			clock,
+			sync_oracle_handle: _sync_oracle_handle,
+			..
+		} = test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+
+		let candidate_index = 0;
+		let validator = ValidatorIndex(0);
+
+		// Add block hash 0x01...
+		ChainBuilder::new()
+			.add_block(
+				block_hash,
+				ChainBuilder::GENESIS_HASH,
+				1,
+				BlockConfig { slot: Slot::from(1), candidates: None, session_info: None },
+			)
+			.build(&mut virtual_overseer)
+			.await;
+
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
+
+		assert!(clock.inner.lock().has_wakeup(20));
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn subsystem_process_wakeup_schedules_wakeup() {
+	test_harness(HarnessConfig::default(), |test_harness| async move {
+		let TestHarness {
+			mut virtual_overseer,
+			clock,
+			sync_oracle_handle: _sync_oracle_handle,
+			..
+		} = test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+
+		let candidate_index = 0;
+		let validator = ValidatorIndex(0);
+
+		// Add block hash 0x01...
+		ChainBuilder::new()
+			.add_block(
+				block_hash,
+				ChainBuilder::GENESIS_HASH,
+				1,
+				BlockConfig { slot: Slot::from(1), candidates: None, session_info: None },
+			)
+			.build(&mut virtual_overseer)
+			.await;
+
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
+
+		assert!(clock.inner.lock().has_wakeup(20));
+
+		// Activate the wakeup present above, and sleep to allow process_wakeups to execute..
+		clock.inner.lock().wakeup_all(20);
+		futures_timer::Delay::new(Duration::from_millis(100)).await;
+
+		// The wakeup should have been rescheduled.
+		assert!(clock.inner.lock().has_wakeup(20));
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn linear_import_act_on_leaf() {
+	let session = 3u32;
+
+	test_harness(HarnessConfig::default(), |test_harness| async move {
 		let TestHarness { mut virtual_overseer, .. } = test_harness;
 
-		let mut head: Hash = ChainBuilder::GENESIS_HASH;
-		let mut builder = ChainBuilder::new();
-		for i in 1..session {
-			let slot = Slot::from(i as u64);
-			let hash = Hash::repeat_byte(i as u8);
-			builder.add_block(hash, head, slot, i);
-			head = hash;
+		let mut head: Hash = ChainBuilder::GENESIS_HASH;
+		let mut builder = ChainBuilder::new();
+		for i in 1..session {
+			let slot = Slot::from(i as u64);
+
+			let hash = Hash::repeat_byte(i as u8);
+			builder.add_block(
+				hash,
+				head,
+				i,
+				BlockConfig { slot, candidates: None, session_info: None },
+			);
+			head = hash;
+		}
+
+		builder.build(&mut virtual_overseer).await;
+
+		let (tx, rx) = oneshot::channel();
+
+		overseer_send(
+			&mut virtual_overseer,
+			FromOverseer::Communication {
+				msg: ApprovalVotingMessage::CheckAndImportAssignment(
+					IndirectAssignmentCert {
+						block_hash: head,
+						validator: 0u32.into(),
+						cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
+							sample: 0,
+						}),
+					},
+					0u32,
+					tx,
+				),
+			},
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn forkful_import_at_same_height_act_on_leaf() {
+	let session = 3u32;
+
+	test_harness(HarnessConfig::default(), |test_harness| async move {
+		let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } =
+			test_harness;
+
+		let mut head: Hash = ChainBuilder::GENESIS_HASH;
+		let mut builder = ChainBuilder::new();
+		for i in 1..session {
+			let slot = Slot::from(i as u64);
+			let hash = Hash::repeat_byte(i as u8);
+			builder.add_block(
+				hash,
+				head,
+				i,
+				BlockConfig { slot, candidates: None, session_info: None },
+			);
+			head = hash;
+		}
+		let num_forks = 3;
+		let forks = Vec::new();
+
+		for i in 0..num_forks {
+			let slot = Slot::from(session as u64);
+			let hash = Hash::repeat_byte(session as u8 + i);
+			builder.add_block(
+				hash,
+				head,
+				session,
+				BlockConfig { slot, candidates: None, session_info: None },
+			);
+		}
+		builder.build(&mut virtual_overseer).await;
+
+		for head in forks.into_iter() {
+			let (tx, rx) = oneshot::channel();
+
+			overseer_send(
+				&mut virtual_overseer,
+				FromOverseer::Communication {
+					msg: ApprovalVotingMessage::CheckAndImportAssignment(
+						IndirectAssignmentCert {
+							block_hash: head,
+							validator: 0u32.into(),
+							cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
+								sample: 0,
+							}),
+						},
+						0u32,
+						tx,
+					),
+				},
+			)
+			.await;
+
+			assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
+		}
+		virtual_overseer
+	});
+}
+
+#[test]
+fn import_checked_approval_updates_entries_and_schedules() {
+	let config = HarnessConfig::default();
+	let store = config.backend();
+	test_harness(config, |test_harness| async move {
+		let TestHarness {
+			mut virtual_overseer,
+			clock,
+			sync_oracle_handle: _sync_oracle_handle,
+			..
+		} = test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+		let validator_index_a = ValidatorIndex(0);
+		let validator_index_b = ValidatorIndex(1);
+
+		let validators = vec![
+			Sr25519Keyring::Alice,
+			Sr25519Keyring::Bob,
+			Sr25519Keyring::Charlie,
+			Sr25519Keyring::Dave,
+			Sr25519Keyring::Eve,
+		];
+		let session_info = SessionInfo {
+			validators: validators.iter().map(|v| v.public().into()).collect(),
+			validator_groups: vec![
+				vec![ValidatorIndex(0), ValidatorIndex(1)],
+				vec![ValidatorIndex(2)],
+				vec![ValidatorIndex(3), ValidatorIndex(4)],
+			],
+			needed_approvals: 2,
+			discovery_keys: validators.iter().map(|v| v.public().into()).collect(),
+			assignment_keys: validators.iter().map(|v| v.public().into()).collect(),
+			n_cores: validators.len() as _,
+			zeroth_delay_tranche_width: 5,
+			relay_vrf_modulo_samples: 3,
+			n_delay_tranches: 50,
+			no_show_slots: 2,
+		};
+
+		let candidate_descriptor = make_candidate(1.into(), &block_hash);
+		let candidate_hash = candidate_descriptor.hash();
+
+		let head: Hash = ChainBuilder::GENESIS_HASH;
+		let mut builder = ChainBuilder::new();
+		let slot = Slot::from(1 as u64);
+		builder.add_block(
+			block_hash,
+			head,
+			1,
+			BlockConfig {
+				slot,
+				candidates: Some(vec![(candidate_descriptor, CoreIndex(0), GroupIndex(0))]),
+				session_info: Some(session_info),
+			},
+		);
+		builder.build(&mut virtual_overseer).await;
+
+		let candidate_index = 0;
+
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator_index_a,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),);
+
+		let rx = check_and_import_assignment(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator_index_b,
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),);
+
+		// Clear any wake ups from the assignment imports.
+		assert!(clock.inner.lock().has_wakeup(20));
+		clock.inner.lock().wakeup_all(20);
+
+		let session_index = 1;
+		let sig_a = sign_approval(Sr25519Keyring::Alice, candidate_hash, session_index);
+
+		let rx = check_and_import_approval(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator_index_a,
+			candidate_hash,
+			session_index,
+			false,
+			true,
+			Some(sig_a),
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted),);
+
+		// Sleep to ensure we get a consistent read on the database.
+		futures_timer::Delay::new(Duration::from_millis(100)).await;
+
+		// The candidate should not yet be approved and a wakeup should be scheduled on the first
+		// approval.
+		let candidate_entry = store.load_candidate_entry(&candidate_hash).unwrap().unwrap();
+		assert!(!candidate_entry.approval_entry(&block_hash).unwrap().is_approved());
+		assert!(clock.inner.lock().has_wakeup(20));
+
+		// Clear the wake ups to assert that later approval also schedule wakeups.
+		clock.inner.lock().wakeup_all(20);
+
+		let sig_b = sign_approval(Sr25519Keyring::Bob, candidate_hash, session_index);
+		let rx = check_and_import_approval(
+			&mut virtual_overseer,
+			block_hash,
+			candidate_index,
+			validator_index_b,
+			candidate_hash,
+			session_index,
+			true,
+			true,
+			Some(sig_b),
+		)
+		.await;
+
+		assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted),);
+
+		// Sleep to ensure we get a consistent read on the database.
+		//
+		// NOTE: Since the response above occurs before writing to the database, we are somewhat
+		// breaking the external consistency of the API by reaching into the database directly.
+		// Under normal operation, this wouldn't be necessary, since all requests are serialized by
+		// the event loop and we write at the end of each pass. However, if the database write were
+		// to fail, a downstream subsystem may expect for this candidate to be approved, and
+		// possibly take further actions on the assumption that the candidate is approved, when
+		// that may not be the reality from the database's perspective. This could be avoided
+		// entirely by having replies processed after database writes, but that would constitute a
+		// larger refactor and incur a performance penalty.
+		futures_timer::Delay::new(Duration::from_millis(100)).await;
+
+		// The candidate should now be approved.
+		let candidate_entry = store.load_candidate_entry(&candidate_hash).unwrap().unwrap();
+		assert!(candidate_entry.approval_entry(&block_hash).unwrap().is_approved());
+		assert!(clock.inner.lock().has_wakeup(20));
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn subsystem_import_checked_approval_sets_one_block_bit_at_a_time() {
+	let config = HarnessConfig::default();
+	let store = config.backend();
+	test_harness(config, |test_harness| async move {
+		let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } =
+			test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+
+		let candidate_receipt1 = {
+			let mut receipt = CandidateReceipt::<Hash>::default();
+			receipt.descriptor.para_id = 1.into();
+			receipt
+		};
+		let candidate_receipt2 = {
+			let mut receipt = CandidateReceipt::<Hash>::default();
+			receipt.descriptor.para_id = 2.into();
+			receipt
+		};
+		let candidate_hash1 = candidate_receipt1.hash();
+		let candidate_hash2 = candidate_receipt2.hash();
+		let candidate_index1 = 0;
+		let candidate_index2 = 1;
+
+		let validator1 = ValidatorIndex(0);
+		let validator2 = ValidatorIndex(1);
+		let session_index = 1;
+
+		let validators = vec![
+			Sr25519Keyring::Alice,
+			Sr25519Keyring::Bob,
+			Sr25519Keyring::Charlie,
+			Sr25519Keyring::Dave,
+			Sr25519Keyring::Eve,
+		];
+		let session_info = SessionInfo {
+			validators: validators.iter().map(|v| v.public().into()).collect(),
+			validator_groups: vec![
+				vec![ValidatorIndex(0), ValidatorIndex(1)],
+				vec![ValidatorIndex(2)],
+				vec![ValidatorIndex(3), ValidatorIndex(4)],
+			],
+			needed_approvals: 2,
+			discovery_keys: validators.iter().map(|v| v.public().into()).collect(),
+			assignment_keys: validators.iter().map(|v| v.public().into()).collect(),
+			n_cores: validators.len() as _,
+			zeroth_delay_tranche_width: 5,
+			relay_vrf_modulo_samples: 3,
+			n_delay_tranches: 50,
+			no_show_slots: 2,
+		};
+
+		ChainBuilder::new()
+			.add_block(
+				block_hash,
+				ChainBuilder::GENESIS_HASH,
+				1,
+				BlockConfig {
+					slot: Slot::from(0),
+					candidates: Some(vec![
+						(candidate_receipt1, CoreIndex(1), GroupIndex(1)),
+						(candidate_receipt2, CoreIndex(1), GroupIndex(1)),
+					]),
+					session_info: Some(session_info),
+				},
+			)
+			.build(&mut virtual_overseer)
+			.await;
+
+		let assignments = vec![
+			(candidate_index1, validator1),
+			(candidate_index2, validator1),
+			(candidate_index1, validator2),
+			(candidate_index2, validator2),
+		];
+
+		for (candidate_index, validator) in assignments {
+			let rx = check_and_import_assignment(
+				&mut virtual_overseer,
+				block_hash,
+				candidate_index,
+				validator,
+			)
+			.await;
+			assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
 		}
-		let num_forks = 3;
-		let forks = Vec::new();
 
-		for i in 0..num_forks {
-			let slot = Slot::from(session as u64);
-			let hash = Hash::repeat_byte(session as u8 + i);
-			builder.add_block(hash, head, slot, session);
+		let approvals = vec![
+			(candidate_index1, validator1, candidate_hash1),
+			(candidate_index1, validator2, candidate_hash1),
+			(candidate_index2, validator1, candidate_hash2),
+			(candidate_index2, validator2, candidate_hash2),
+		];
+
+		for (i, (candidate_index, validator, candidate_hash)) in approvals.iter().enumerate() {
+			let expect_candidate1_approved = i >= 1;
+			let expect_candidate2_approved = i >= 3;
+			let expect_block_approved = expect_candidate2_approved;
+
+			let signature = if *validator == validator1 {
+				sign_approval(Sr25519Keyring::Alice, *candidate_hash, session_index)
+			} else {
+				sign_approval(Sr25519Keyring::Bob, *candidate_hash, session_index)
+			};
+			let rx = check_and_import_approval(
+				&mut virtual_overseer,
+				block_hash,
+				*candidate_index,
+				*validator,
+				*candidate_hash,
+				session_index,
+				expect_block_approved,
+				true,
+				Some(signature),
+			)
+			.await;
+			assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted));
+
+			// Sleep to get a consistent read on the database.
+			futures_timer::Delay::new(Duration::from_millis(200)).await;
+
+			let block_entry = store.load_block_entry(&block_hash).unwrap().unwrap();
+			assert_eq!(block_entry.is_fully_approved(), expect_block_approved);
+			assert_eq!(
+				block_entry.is_candidate_approved(&candidate_hash1),
+				expect_candidate1_approved
+			);
+			assert_eq!(
+				block_entry.is_candidate_approved(&candidate_hash2),
+				expect_candidate2_approved
+			);
+		}
+
+		virtual_overseer
+	});
+}
+
+fn approved_ancestor_test(
+	skip_approval: impl Fn(BlockNumber) -> bool,
+	approved_height: BlockNumber,
+) {
+	test_harness(HarnessConfig::default(), |test_harness| async move {
+		let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } =
+			test_harness;
+
+		let block_hashes = vec![
+			Hash::repeat_byte(0x01),
+			Hash::repeat_byte(0x02),
+			Hash::repeat_byte(0x03),
+			Hash::repeat_byte(0x04),
+		];
+
+		let candidate_receipts: Vec<_> = block_hashes
+			.iter()
+			.enumerate()
+			.map(|(i, hash)| {
+				let mut candidate_receipt = CandidateReceipt::<Hash>::default();
+				candidate_receipt.descriptor.para_id = i.into();
+				candidate_receipt.descriptor.relay_parent = *hash;
+				candidate_receipt
+			})
+			.collect();
+
+		let candidate_hashes: Vec<_> = candidate_receipts.iter().map(|r| r.hash()).collect();
+
+		let candidate_index = 0;
+		let validator = ValidatorIndex(0);
+
+		let mut builder = ChainBuilder::new();
+		for (i, (block_hash, candidate_receipt)) in
+			block_hashes.iter().zip(candidate_receipts).enumerate()
+		{
+			let parent_hash = if i == 0 { ChainBuilder::GENESIS_HASH } else { block_hashes[i - 1] };
+			builder.add_block(
+				*block_hash,
+				parent_hash,
+				i as u32 + 1,
+				BlockConfig {
+					slot: Slot::from(i as u64),
+					candidates: Some(vec![(candidate_receipt, CoreIndex(0), GroupIndex(0))]),
+					session_info: None,
+				},
+			);
 		}
 		builder.build(&mut virtual_overseer).await;
 
-		for head in forks.into_iter() {
-			let (tx, rx) = oneshot::channel();
+		for (i, (block_hash, candidate_hash)) in
+			block_hashes.iter().zip(candidate_hashes).enumerate()
+		{
+			let rx = check_and_import_assignment(
+				&mut virtual_overseer,
+				*block_hash,
+				candidate_index,
+				validator,
+			)
+			.await;
+			assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
 
-			overseer_send(
+			if skip_approval(i as BlockNumber + 1) {
+				continue
+			}
+
+			let rx = check_and_import_approval(
 				&mut virtual_overseer,
-				FromOverseer::Communication {
-					msg: ApprovalVotingMessage::CheckAndImportAssignment(
-						IndirectAssignmentCert {
-							block_hash: head,
-							validator: 0u32.into(),
-							cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
-								sample: 0,
-							}),
-						},
-						0u32,
-						tx,
-					),
+				*block_hash,
+				candidate_index,
+				validator,
+				candidate_hash,
+				i as u32 + 1,
+				true,
+				true,
+				None,
+			)
+			.await;
+			assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted));
+		}
+
+		let target = block_hashes[block_hashes.len() - 1];
+		let block_number = block_hashes.len();
+
+		let (tx, rx) = oneshot::channel();
+		overseer_send(
+			&mut virtual_overseer,
+			FromOverseer::Communication {
+				msg: ApprovalVotingMessage::ApprovedAncestor(target, 0, tx),
+			},
+		)
+		.await;
+
+		assert_matches!(
+			overseer_recv(&mut virtual_overseer).await,
+			AllMessages::ChainApi(ChainApiMessage::BlockNumber(hash, tx)) => {
+				assert_eq!(target, hash);
+				tx.send(Ok(Some(block_number as BlockNumber))).unwrap();
+			}
+		);
+
+		assert_matches!(
+			overseer_recv(&mut virtual_overseer).await,
+			AllMessages::ChainApi(ChainApiMessage::Ancestors {
+				hash,
+				k,
+				response_channel: tx,
+			}) => {
+				assert_eq!(target, hash);
+				assert_eq!(k, block_number - (0 + 1));
+				let ancestors = block_hashes.iter()
+					.take(block_number-1)
+					.rev()
+					.cloned()
+					.collect::<Vec<Hash>>();
+				tx.send(Ok(ancestors)).unwrap();
+			}
+		);
+
+		let approved_hash = block_hashes[approved_height as usize - 1];
+		let HighestApprovedAncestorBlock { hash, number, .. } = rx.await.unwrap().unwrap();
+		assert_eq!(approved_hash, hash);
+		assert_eq!(number, approved_height);
+
+		virtual_overseer
+	});
+}
+
+#[test]
+fn subsystem_approved_ancestor_all_approved() {
+	// Don't skip any approvals, highest approved ancestor should be 4.
+	approved_ancestor_test(|_| false, 4);
+}
+
+#[test]
+fn subsystem_approved_ancestor_missing_approval() {
+	// Skip approval for the third block, highest approved ancestor should be 2.
+	approved_ancestor_test(|i| i == 3, 2);
+}
+
+#[test]
+fn subsystem_process_wakeup_trigger_assignment_launch_approval() {
+	let assignment_criteria = Box::new(MockAssignmentCriteria(
+		|| {
+			let mut assignments = HashMap::new();
+			let _ = assignments.insert(
+				CoreIndex(0),
+				approval_db::v1::OurAssignment {
+					cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
+					tranche: 0,
+					validator_index: ValidatorIndex(0),
+					triggered: false,
+				}
+				.into(),
+			);
+			assignments
+		},
+		|_| Ok(0),
+	));
+	let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build();
+	let store = config.backend();
+
+	test_harness(config, |test_harness| async move {
+		let TestHarness {
+			mut virtual_overseer,
+			clock,
+			sync_oracle_handle: _sync_oracle_handle,
+			..
+		} = test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+		let candidate_receipt = CandidateReceipt::<Hash>::default();
+		let candidate_hash = candidate_receipt.hash();
+		let slot = Slot::from(1);
+		let candidate_index = 0;
+
+		let validators = vec![
+			Sr25519Keyring::Alice,
+			Sr25519Keyring::Bob,
+			Sr25519Keyring::Charlie,
+			Sr25519Keyring::Dave,
+			Sr25519Keyring::Eve,
+		];
+		let session_info = SessionInfo {
+			validators: validators.iter().map(|v| v.public().into()).collect(),
+			validator_groups: vec![
+				vec![ValidatorIndex(0), ValidatorIndex(1)],
+				vec![ValidatorIndex(2)],
+				vec![ValidatorIndex(3), ValidatorIndex(4)],
+			],
+			needed_approvals: 2,
+			discovery_keys: validators.iter().map(|v| v.public().into()).collect(),
+			assignment_keys: validators.iter().map(|v| v.public().into()).collect(),
+			n_cores: validators.len() as _,
+			zeroth_delay_tranche_width: 5,
+			relay_vrf_modulo_samples: 3,
+			n_delay_tranches: 50,
+			no_show_slots: 2,
+		};
+
+		ChainBuilder::new()
+			.add_block(
+				block_hash,
+				ChainBuilder::GENESIS_HASH,
+				1,
+				BlockConfig {
+					slot,
+					candidates: Some(vec![(candidate_receipt, CoreIndex(0), GroupIndex(0))]),
+					session_info: Some(session_info),
+				},
+			)
+			.build(&mut virtual_overseer)
+			.await;
+
+		assert!(!clock.inner.lock().has_wakeup(1));
+		clock.inner.lock().wakeup_all(1);
+
+		assert!(clock.inner.lock().has_wakeup(slot_to_tick(slot)));
+		clock.inner.lock().wakeup_all(slot_to_tick(slot));
+
+		futures_timer::Delay::new(Duration::from_millis(200)).await;
+
+		assert!(clock.inner.lock().has_wakeup(slot_to_tick(slot + 1)));
+		clock.inner.lock().wakeup_all(slot_to_tick(slot + 1));
+
+		assert_matches!(
+			overseer_recv(&mut virtual_overseer).await,
+			AllMessages::ApprovalDistribution(ApprovalDistributionMessage::DistributeAssignment(
+				_,
+				c_index,
+			)) => {
+				assert_eq!(candidate_index, c_index);
+			}
+		);
+
+		assert_eq!(clock.inner.lock().wakeups.len(), 0);
+
+		futures_timer::Delay::new(Duration::from_millis(200)).await;
+
+		let candidate_entry = store.load_candidate_entry(&candidate_hash).unwrap().unwrap();
+		let our_assignment =
+			candidate_entry.approval_entry(&block_hash).unwrap().our_assignment().unwrap();
+		assert!(our_assignment.triggered());
+
+		virtual_overseer
+	});
+}
+
+struct TriggersAssignmentConfig<F1, F2> {
+	our_assigned_tranche: DelayTranche,
+	assign_validator_tranche: F1,
+	no_show_slots: u32,
+	assignments_to_import: Vec<u32>,
+	approvals_to_import: Vec<u32>,
+	ticks: Vec<Tick>,
+	should_be_triggered: F2,
+}
+
+fn triggers_assignment_test<F1, F2>(config: TriggersAssignmentConfig<F1, F2>)
+where
+	F1: 'static
+		+ Fn(ValidatorIndex) -> Result<DelayTranche, criteria::InvalidAssignment>
+		+ Send
+		+ Sync,
+	F2: Fn(Tick) -> bool,
+{
+	let TriggersAssignmentConfig {
+		our_assigned_tranche,
+		assign_validator_tranche,
+		no_show_slots,
+		assignments_to_import,
+		approvals_to_import,
+		ticks,
+		should_be_triggered,
+	} = config;
+
+	let assignment_criteria = Box::new(MockAssignmentCriteria(
+		move || {
+			let mut assignments = HashMap::new();
+			let _ = assignments.insert(
+				CoreIndex(0),
+				approval_db::v1::OurAssignment {
+					cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
+					tranche: our_assigned_tranche,
+					validator_index: ValidatorIndex(0),
+					triggered: false,
+				}
+				.into(),
+			);
+			assignments
+		},
+		assign_validator_tranche,
+	));
+	let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build();
+	let store = config.backend();
+
+	test_harness(config, |test_harness| async move {
+		let TestHarness {
+			mut virtual_overseer,
+			clock,
+			sync_oracle_handle: _sync_oracle_handle,
+			..
+		} = test_harness;
+
+		let block_hash = Hash::repeat_byte(0x01);
+		let candidate_receipt = CandidateReceipt::<Hash>::default();
+		let candidate_hash = candidate_receipt.hash();
+		let slot = Slot::from(1);
+		let candidate_index = 0;
+
+		let validators = vec![
+			Sr25519Keyring::Alice,
+			Sr25519Keyring::Bob,
+			Sr25519Keyring::Charlie,
+			Sr25519Keyring::Dave,
+			Sr25519Keyring::Eve,
+			Sr25519Keyring::Ferdie,
+		];
+		let session_info = SessionInfo {
+			validators: validators.iter().map(|v| v.public().into()).collect(),
+			validator_groups: vec![
+				vec![ValidatorIndex(0), ValidatorIndex(1)],
+				vec![ValidatorIndex(2), ValidatorIndex(3)],
+				vec![ValidatorIndex(4), ValidatorIndex(5)],
+			],
+			needed_approvals: 2,
+			discovery_keys: validators.iter().map(|v| v.public().into()).collect(),
+			assignment_keys: validators.iter().map(|v| v.public().into()).collect(),
+			n_cores: validators.len() as _,
+			zeroth_delay_tranche_width: 5,
+			relay_vrf_modulo_samples: 2,
+			n_delay_tranches: 50,
+			no_show_slots,
+		};
+
+		ChainBuilder::new()
+			.add_block(
+				block_hash,
+				ChainBuilder::GENESIS_HASH,
+				1,
+				BlockConfig {
+					slot,
+					candidates: Some(vec![(candidate_receipt, CoreIndex(0), GroupIndex(2))]),
+					session_info: Some(session_info),
 				},
 			)
+			.build(&mut virtual_overseer)
 			.await;
 
+		for validator in assignments_to_import {
+			let rx = check_and_import_assignment(
+				&mut virtual_overseer,
+				block_hash,
+				candidate_index,
+				ValidatorIndex(validator),
+			)
+			.await;
 			assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
 		}
+
+		let n_validators = validators.len();
+		for (i, &validator_index) in approvals_to_import.iter().enumerate() {
+			let expect_chain_approved = 3 * (i + 1) > n_validators;
+			let rx = check_and_import_approval(
+				&mut virtual_overseer,
+				block_hash,
+				candidate_index,
+				ValidatorIndex(validator_index),
+				candidate_hash,
+				1,
+				expect_chain_approved,
+				true,
+				Some(sign_approval(
+					validators[validator_index as usize].clone(),
+					candidate_hash,
+					1,
+				)),
+			)
+			.await;
+			assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted));
+		}
+
+		let debug = false;
+		if debug {
+			step_until_done(&clock).await;
+			return virtual_overseer
+		}
+
+		futures_timer::Delay::new(Duration::from_millis(200)).await;
+
+		for tick in ticks {
+			// Assert that this tick is the next to wake up, requiring the test harness to encode
+			// all relevant wakeups sequentially.
+			assert_eq!(Some(tick), clock.inner.lock().next_wakeup());
+
+			clock.inner.lock().set_tick(tick);
+			futures_timer::Delay::new(Duration::from_millis(100)).await;
+
+			// Assert that Alice's assignment is triggered at the correct tick.
+			let candidate_entry = store.load_candidate_entry(&candidate_hash).unwrap().unwrap();
+			let our_assignment =
+				candidate_entry.approval_entry(&block_hash).unwrap().our_assignment().unwrap();
+			assert_eq!(our_assignment.triggered(), should_be_triggered(tick), "at tick {:?}", tick);
+		}
+
 		virtual_overseer
 	});
 }
+
+// This method is used to generate a trace for an execution of a triggers_assignment_test given a
+// starting configuration. The relevant ticks (all scheduled wakeups) are printed after no further
+// ticks are scheduled. To create a valid test, a prefix of the relevant ticks should be included
+// in the final test configuration, ending at the tick with the desired inputs to
+// should_trigger_assignemnt.
+async fn step_until_done(clock: &MockClock) {
+	let mut relevant_ticks = Vec::new();
+	loop {
+		futures_timer::Delay::new(Duration::from_millis(200)).await;
+		let mut clock = clock.inner.lock();
+		if let Some(tick) = clock.next_wakeup() {
+			println!("TICK: {:?}", tick);
+			relevant_ticks.push(tick);
+			clock.set_tick(tick);
+		} else {
+			break
+		}
+	}
+	println!("relevant_ticks: {:?}", relevant_ticks);
+}
+
+#[test]
+fn subsystem_assignment_triggered_solo_zero_tranche() {
+	triggers_assignment_test(TriggersAssignmentConfig {
+		our_assigned_tranche: 0,
+		assign_validator_tranche: |_| Ok(0),
+		no_show_slots: 2,
+		assignments_to_import: vec![],
+		approvals_to_import: vec![],
+		ticks: vec![
+			10, // Alice wakeup, assignment triggered
+		],
+		should_be_triggered: |_| true,
+	});
+}
+
+#[test]
+fn subsystem_assignment_triggered_by_all_with_less_than_threshold() {
+	triggers_assignment_test(TriggersAssignmentConfig {
+		our_assigned_tranche: 11,
+		assign_validator_tranche: |_| Ok(0),
+		no_show_slots: 2,
+		assignments_to_import: vec![1, 2, 3, 4, 5],
+		approvals_to_import: vec![2, 4],
+		ticks: vec![
+			20, // Check for no shows
+		],
+		should_be_triggered: |_| true,
+	});
+}
+
+#[test]
+fn subsystem_assignment_not_triggered_by_all_with_threshold() {
+	triggers_assignment_test(TriggersAssignmentConfig {
+		our_assigned_tranche: 11,
+		assign_validator_tranche: |_| Ok(0),
+		no_show_slots: 2,
+		assignments_to_import: vec![1, 2, 3, 4, 5],
+		approvals_to_import: vec![1, 3, 5],
+		ticks: vec![
+			20, // Check no shows
+		],
+		should_be_triggered: |_| false,
+	});
+}
+
+#[test]
+fn subsystem_assignment_triggered_if_below_maximum_and_clock_is_equal() {
+	triggers_assignment_test(TriggersAssignmentConfig {
+		our_assigned_tranche: 11,
+		assign_validator_tranche: |_| Ok(0),
+		no_show_slots: 2,
+		assignments_to_import: vec![1],
+		approvals_to_import: vec![],
+		ticks: vec![
+			20, // Check no shows
+			21, // Alice wakeup, assignment triggered
+		],
+		should_be_triggered: |tick| tick >= 21,
+	});
+}
+
+#[test]
+fn subsystem_assignment_not_triggered_more_than_maximum() {
+	triggers_assignment_test(TriggersAssignmentConfig {
+		our_assigned_tranche: 3,
+		assign_validator_tranche: |_| Ok(0),
+		no_show_slots: 2,
+		assignments_to_import: vec![2, 3],
+		approvals_to_import: vec![],
+		ticks: vec![
+			13, // Alice wakeup
+			20, // Check no shows
+		],
+		should_be_triggered: |_| false,
+	});
+}
+
+#[test]
+fn subsystem_assignment_triggered_if_at_maximum() {
+	// TODO(ladi): is this possible?
+	triggers_assignment_test(TriggersAssignmentConfig {
+		our_assigned_tranche: 11,
+		assign_validator_tranche: |_| Ok(2),
+		no_show_slots: 2,
+		assignments_to_import: vec![1],
+		approvals_to_import: vec![],
+		ticks: vec![
+			12, // Bob wakeup
+			20, // Check no shows
+		],
+		should_be_triggered: |_| false,
+	});
+}
+
+#[test]
+fn subsystem_assignment_not_triggered_by_exact() {
+	triggers_assignment_test(TriggersAssignmentConfig {
+		our_assigned_tranche: 2,
+		assign_validator_tranche: |_| Ok(1),
+		no_show_slots: 2,
+		assignments_to_import: vec![2, 3],
+		approvals_to_import: vec![],
+		ticks: vec![
+			11, // Charlie and Dave wakeup
+		],
+		should_be_triggered: |_| false,
+	});
+}
+
+#[test]
+fn subsystem_assignment_not_triggered_if_at_maximum_but_clock_is_before() {
+	triggers_assignment_test(TriggersAssignmentConfig {
+		our_assigned_tranche: 6,
+		assign_validator_tranche: |validator: ValidatorIndex| Ok(validator.0 as _),
+		no_show_slots: 0,
+		assignments_to_import: vec![2, 3, 4],
+		approvals_to_import: vec![],
+		ticks: vec![
+			12, // Charlie wakeup
+			13, // Dave wakeup
+			14, // Eve wakeup
+		],
+		should_be_triggered: |_| false,
+	});
+}
+
+#[test]
+fn subsystem_assignment_not_triggered_if_at_maximum_but_clock_is_before_with_drift() {
+	triggers_assignment_test(TriggersAssignmentConfig {
+		our_assigned_tranche: 5,
+		assign_validator_tranche: |validator: ValidatorIndex| Ok(validator.0 as _),
+		no_show_slots: 2,
+		assignments_to_import: vec![2, 3, 4],
+		approvals_to_import: vec![],
+		ticks: vec![
+			12, // Charlie wakeup
+			13, // Dave wakeup
+			15, // Alice wakeup, noop
+			20, // Check no shows
+			34, // Eve wakeup
+		],
+		should_be_triggered: |_| false,
+	});
+}
-- 
GitLab