From 6b854acc69cd64f7c0e6cdb606e741e630e45032 Mon Sep 17 00:00:00 2001
From: Alexandru Gheorghe <49718502+alexggh@users.noreply.github.com>
Date: Mon, 2 Sep 2024 12:05:03 +0300
Subject: [PATCH] [3 / 5] Move crypto checks in the approval-distribution
 (#4928)

# Prerequisite
This is part of the work to further optimize the approval subsystems, if
you want to understand the full context start with reading
https://github.com/paritytech/polkadot-sdk/pull/4849#issue-2364261568,

# Description
This PR contain changes, so that the crypto checks are performed by the
approval-distribution subsystem instead of the approval-voting one. The
benefit for these, is twofold:
1. Approval-distribution won't have to wait every single time for the
approval-voting to finish its job, so the work gets to be pipelined
between approval-distribution and approval-voting.

2. By running in parallel multiple instances of approval-distribution as
described here
https://github.com/paritytech/polkadot-sdk/pull/4849#issue-2364261568,
this significant body of work gets to run in parallel.

## Changes:
1. When approval-voting send `ApprovalDistributionMessage::NewBlocks` it
needs to pass the core_index and candidate_hash of the candidates.
2. ApprovalDistribution needs to use `RuntimeInfo` to be able to fetch
the SessionInfo from the runtime.
3. Move `approval-voting` logic that checks VRF assignment into
`approval-distribution`
4. Move `approval-voting` logic that checks vote is correctly signed
into `approval-distribution`
5. Plumb `approval-distribution` and `approval-voting` tests to support
the new logic.

## Benefits
Even without parallelisation the gains are significant, for example on
my machine if we run approval subsystem bench for 500 validators and 100
cores and trigger all 89 tranches of assignments and approvals, the
system won't fall behind anymore because of late processing of messages.
```
Before change
Chain selection approved  after 11500 ms hash=0x0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a

After change

Chain selection approved  after 5500 ms hash=0x0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a
```

## TODO:
- [x] Run on versi.
- [x] Update parachain host documentation.

---------

Signed-off-by: Alexandru Gheorghe <alexandru.gheorghe@parity.io>
---
 Cargo.lock                                    |    5 +
 .../approval-voting-regression-bench.rs       |    4 +-
 .../approval-voting/src/approval_checking.rs  |    8 +-
 .../node/core/approval-voting/src/criteria.rs |  174 +-
 .../node/core/approval-voting/src/import.rs   |   12 +-
 polkadot/node/core/approval-voting/src/lib.rs |  239 +-
 .../approval-voting/src/persisted_entries.rs  |    4 +-
 .../node/core/approval-voting/src/tests.rs    |  464 +-
 .../network/approval-distribution/Cargo.toml  |    2 +
 .../network/approval-distribution/src/lib.rs  |  500 +-
 .../approval-distribution/src/metrics.rs      |   16 -
 .../approval-distribution/src/tests.rs        | 6307 ++++++++++-------
 polkadot/node/overseer/src/lib.rs             |    1 +
 polkadot/node/primitives/Cargo.toml           |    3 +
 .../node/primitives/src/approval/criteria.rs  |  177 +
 .../src/{approval.rs => approval/mod.rs}      |   14 +-
 .../src => primitives/src/approval}/time.rs   |   13 +-
 polkadot/node/service/src/overseer.rs         |    8 +-
 .../src/lib/approval/helpers.rs               |    2 +-
 .../src/lib/approval/message_generator.rs     |    7 +-
 .../src/lib/approval/mock_chain_selection.rs  |    2 +-
 .../subsystem-bench/src/lib/approval/mod.rs   |   14 +-
 polkadot/node/subsystem-types/src/messages.rs |   79 +-
 .../node/approval/approval-distribution.md    |   36 +-
 .../src/node/approval/approval-voting.md      |   40 +-
 .../src/types/overseer-protocol.md            |   24 +-
 prdoc/pr_4928.prdoc                           |   28 +
 27 files changed, 4648 insertions(+), 3535 deletions(-)
 create mode 100644 polkadot/node/primitives/src/approval/criteria.rs
 rename polkadot/node/primitives/src/{approval.rs => approval/mod.rs} (98%)
 rename polkadot/node/{core/approval-voting/src => primitives/src/approval}/time.rs (95%)
 create mode 100644 prdoc/pr_4928.prdoc

diff --git a/Cargo.lock b/Cargo.lock
index 4cb69d1a178..9d8f4cef142 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -12989,7 +12989,9 @@ dependencies = [
  "rand",
  "rand_chacha",
  "rand_core",
+ "sc-keystore",
  "schnorrkel 0.11.4",
+ "sp-application-crypto",
  "sp-authority-discovery",
  "sp-core",
  "sp-tracing 16.0.0",
@@ -13802,14 +13804,17 @@ dependencies = [
  "bitvec",
  "bounded-vec",
  "futures",
+ "futures-timer",
  "parity-scale-codec",
  "polkadot-erasure-coding",
  "polkadot-parachain-primitives",
  "polkadot-primitives",
+ "sc-keystore",
  "schnorrkel 0.11.4",
  "serde",
  "sp-application-crypto",
  "sp-consensus-babe",
+ "sp-consensus-slots",
  "sp-core",
  "sp-keystore",
  "sp-maybe-compressed-blob",
diff --git a/polkadot/node/core/approval-voting/benches/approval-voting-regression-bench.rs b/polkadot/node/core/approval-voting/benches/approval-voting-regression-bench.rs
index 41418bcc511..0b03f1127ee 100644
--- a/polkadot/node/core/approval-voting/benches/approval-voting-regression-bench.rs
+++ b/polkadot/node/core/approval-voting/benches/approval-voting-regression-bench.rs
@@ -82,8 +82,8 @@ fn main() -> Result<(), String> {
 		("Sent to peers", 63995.2200, 0.01),
 	]));
 	messages.extend(average_usage.check_cpu_usage(&[
-		("approval-distribution", 6.3912, 0.1),
-		("approval-voting", 10.0578, 0.1),
+		("approval-distribution", 12.2736, 0.1),
+		("approval-voting", 2.7174, 0.1),
 	]));
 
 	if messages.is_empty() {
diff --git a/polkadot/node/core/approval-voting/src/approval_checking.rs b/polkadot/node/core/approval-voting/src/approval_checking.rs
index 96eb25626de..3774edc6998 100644
--- a/polkadot/node/core/approval-voting/src/approval_checking.rs
+++ b/polkadot/node/core/approval-voting/src/approval_checking.rs
@@ -22,9 +22,9 @@ use polkadot_primitives::ValidatorIndex;
 
 use crate::{
 	persisted_entries::{ApprovalEntry, CandidateEntry, TrancheEntry},
-	time::Tick,
 	MAX_RECORDED_NO_SHOW_VALIDATORS_PER_CANDIDATE,
 };
+use polkadot_node_primitives::approval::time::Tick;
 
 /// Result of counting the necessary tranches needed for approving a block.
 #[derive(Debug, PartialEq, Clone)]
@@ -1195,9 +1195,9 @@ mod tests {
 	struct NoShowTest {
 		assignments: Vec<(ValidatorIndex, Tick)>,
 		approvals: Vec<usize>,
-		clock_drift: crate::time::Tick,
-		no_show_duration: crate::time::Tick,
-		drifted_tick_now: crate::time::Tick,
+		clock_drift: Tick,
+		no_show_duration: Tick,
+		drifted_tick_now: Tick,
 		exp_no_shows: usize,
 		exp_next_no_show: Option<u64>,
 	}
diff --git a/polkadot/node/core/approval-voting/src/criteria.rs b/polkadot/node/core/approval-voting/src/criteria.rs
index fb9d281e43b..669b6001538 100644
--- a/polkadot/node/core/approval-voting/src/criteria.rs
+++ b/polkadot/node/core/approval-voting/src/criteria.rs
@@ -16,8 +16,11 @@
 
 //! Assignment criteria VRF generation and checking.
 
-use codec::{Decode, Encode};
+use codec::Encode;
 use itertools::Itertools;
+pub use polkadot_node_primitives::approval::criteria::{
+	AssignmentCriteria, Config, InvalidAssignment, InvalidAssignmentReason, OurAssignment,
+};
 use polkadot_node_primitives::approval::{
 	self as approval_types,
 	v1::{AssignmentCert, AssignmentCertKind, DelayTranche, RelayVRFStory},
@@ -25,9 +28,9 @@ use polkadot_node_primitives::approval::{
 		AssignmentCertKindV2, AssignmentCertV2, CoreBitfield, VrfPreOutput, VrfProof, VrfSignature,
 	},
 };
+
 use polkadot_primitives::{
-	AssignmentId, AssignmentPair, CandidateHash, CoreIndex, GroupIndex, IndexedVec, SessionInfo,
-	ValidatorIndex,
+	AssignmentPair, CandidateHash, CoreIndex, GroupIndex, IndexedVec, ValidatorIndex,
 };
 use rand::{seq::SliceRandom, SeedableRng};
 use rand_chacha::ChaCha20Rng;
@@ -44,56 +47,19 @@ use std::{
 
 use super::LOG_TARGET;
 
-/// Details pertaining to our assignment on a block.
-#[derive(Debug, Clone, Encode, Decode, PartialEq)]
-pub struct OurAssignment {
-	cert: AssignmentCertV2,
-	tranche: DelayTranche,
-	validator_index: ValidatorIndex,
-	// Whether the assignment has been triggered already.
-	triggered: bool,
-}
-
-impl OurAssignment {
-	pub fn cert(&self) -> &AssignmentCertV2 {
-		&self.cert
-	}
-
-	pub fn tranche(&self) -> DelayTranche {
-		self.tranche
-	}
-
-	pub(crate) fn validator_index(&self) -> ValidatorIndex {
-		self.validator_index
-	}
-
-	pub(crate) fn triggered(&self) -> bool {
-		self.triggered
-	}
-
-	pub(crate) fn mark_triggered(&mut self) {
-		self.triggered = true;
-	}
-}
-
 impl From<crate::approval_db::v2::OurAssignment> for OurAssignment {
 	fn from(entry: crate::approval_db::v2::OurAssignment) -> Self {
-		OurAssignment {
-			cert: entry.cert,
-			tranche: entry.tranche,
-			validator_index: entry.validator_index,
-			triggered: entry.triggered,
-		}
+		OurAssignment::new(entry.cert, entry.tranche, entry.validator_index, entry.triggered)
 	}
 }
 
 impl From<OurAssignment> for crate::approval_db::v2::OurAssignment {
 	fn from(entry: OurAssignment) -> Self {
 		Self {
-			cert: entry.cert,
-			tranche: entry.tranche,
-			validator_index: entry.validator_index,
-			triggered: entry.triggered,
+			tranche: entry.tranche(),
+			validator_index: entry.validator_index(),
+			triggered: entry.triggered(),
+			cert: entry.into_cert(),
 		}
 	}
 }
@@ -223,60 +189,7 @@ fn assigned_core_transcript(core_index: CoreIndex) -> Transcript {
 	t
 }
 
-/// Information about the world assignments are being produced in.
-#[derive(Clone, Debug)]
-pub struct Config {
-	/// The assignment public keys for validators.
-	assignment_keys: Vec<AssignmentId>,
-	/// The groups of validators assigned to each core.
-	validator_groups: IndexedVec<GroupIndex, Vec<ValidatorIndex>>,
-	/// The number of availability cores used by the protocol during this session.
-	n_cores: u32,
-	/// The zeroth delay tranche width.
-	zeroth_delay_tranche_width: u32,
-	/// The number of samples we do of `relay_vrf_modulo`.
-	relay_vrf_modulo_samples: u32,
-	/// The number of delay tranches in total.
-	n_delay_tranches: u32,
-}
-
-impl<'a> From<&'a SessionInfo> for Config {
-	fn from(s: &'a SessionInfo) -> Self {
-		Config {
-			assignment_keys: s.assignment_keys.clone(),
-			validator_groups: s.validator_groups.clone(),
-			n_cores: s.n_cores,
-			zeroth_delay_tranche_width: s.zeroth_delay_tranche_width,
-			relay_vrf_modulo_samples: s.relay_vrf_modulo_samples,
-			n_delay_tranches: s.n_delay_tranches,
-		}
-	}
-}
-
-/// A trait for producing and checking assignments. Used to mock.
-pub(crate) trait AssignmentCriteria {
-	fn compute_assignments(
-		&self,
-		keystore: &LocalKeystore,
-		relay_vrf_story: RelayVRFStory,
-		config: &Config,
-		leaving_cores: Vec<(CandidateHash, CoreIndex, GroupIndex)>,
-		enable_v2_assignments: bool,
-	) -> HashMap<CoreIndex, OurAssignment>;
-
-	fn check_assignment_cert(
-		&self,
-		claimed_core_bitfield: CoreBitfield,
-		validator_index: ValidatorIndex,
-		config: &Config,
-		relay_vrf_story: RelayVRFStory,
-		assignment: &AssignmentCertV2,
-		// Backing groups for each "leaving core".
-		backing_groups: Vec<GroupIndex>,
-	) -> Result<DelayTranche, InvalidAssignment>;
-}
-
-pub(crate) struct RealAssignmentCriteria;
+pub struct RealAssignmentCriteria;
 
 impl AssignmentCriteria for RealAssignmentCriteria {
 	fn compute_assignments(
@@ -469,12 +382,12 @@ fn compute_relay_vrf_modulo_assignments_v1(
 			};
 
 			// All assignments of type RelayVRFModulo have tranche 0.
-			assignments.entry(core).or_insert(OurAssignment {
-				cert: cert.into(),
-				tranche: 0,
+			assignments.entry(core).or_insert(OurAssignment::new(
+				cert.into(),
+				0,
 				validator_index,
-				triggered: false,
-			});
+				false,
+			));
 		}
 	}
 }
@@ -549,7 +462,7 @@ fn compute_relay_vrf_modulo_assignments_v2(
 		};
 
 		// All assignments of type RelayVRFModulo have tranche 0.
-		OurAssignment { cert, tranche: 0, validator_index, triggered: false }
+		OurAssignment::new(cert, 0, validator_index, false)
 	}) {
 		for core_index in assigned_cores {
 			assignments.insert(core_index, assignment.clone());
@@ -583,7 +496,7 @@ fn compute_relay_vrf_delay_assignments(
 			},
 		};
 
-		let our_assignment = OurAssignment { cert, tranche, validator_index, triggered: false };
+		let our_assignment = OurAssignment::new(cert, tranche, validator_index, false);
 
 		let used = match assignments.entry(core) {
 			Entry::Vacant(e) => {
@@ -591,7 +504,7 @@ fn compute_relay_vrf_delay_assignments(
 				true
 			},
 			Entry::Occupied(mut e) =>
-				if e.get().tranche > our_assignment.tranche {
+				if e.get().tranche() > our_assignment.tranche() {
 					e.insert(our_assignment);
 					true
 				} else {
@@ -612,35 +525,6 @@ fn compute_relay_vrf_delay_assignments(
 	}
 }
 
-/// Assignment invalid.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct InvalidAssignment(pub(crate) InvalidAssignmentReason);
-
-impl std::fmt::Display for InvalidAssignment {
-	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-		write!(f, "Invalid Assignment: {:?}", self.0)
-	}
-}
-
-impl std::error::Error for InvalidAssignment {}
-
-/// Failure conditions when checking an assignment cert.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum InvalidAssignmentReason {
-	ValidatorIndexOutOfBounds,
-	SampleOutOfBounds,
-	CoreIndexOutOfBounds,
-	InvalidAssignmentKey,
-	IsInBackingGroup,
-	VRFModuloCoreIndexMismatch,
-	VRFModuloOutputMismatch,
-	VRFDelayCoreIndexMismatch,
-	VRFDelayOutputMismatch,
-	InvalidArguments,
-	/// Assignment vrf check resulted in 0 assigned cores.
-	NullAssignment,
-}
-
 /// Checks the crypto of an assignment cert. Failure conditions:
 ///   * Validator index out of bounds
 ///   * VRF signature check fails
@@ -820,13 +704,13 @@ fn is_in_backing_group(
 /// Migration helpers.
 impl From<crate::approval_db::v1::OurAssignment> for OurAssignment {
 	fn from(value: crate::approval_db::v1::OurAssignment) -> Self {
-		Self {
-			cert: value.cert.into(),
-			tranche: value.tranche,
-			validator_index: value.validator_index,
+		Self::new(
+			value.cert.into(),
+			value.tranche,
+			value.validator_index,
 			// Whether the assignment has been triggered already.
-			triggered: value.triggered,
-		}
+			value.triggered,
+		)
 	}
 }
 
@@ -834,7 +718,7 @@ impl From<crate::approval_db::v1::OurAssignment> for OurAssignment {
 mod tests {
 	use super::*;
 	use crate::import::tests::garbage_vrf_signature;
-	use polkadot_primitives::{Hash, ASSIGNMENT_KEY_TYPE_ID};
+	use polkadot_primitives::{AssignmentId, Hash, ASSIGNMENT_KEY_TYPE_ID};
 	use sp_application_crypto::sr25519;
 	use sp_core::crypto::Pair as PairT;
 	use sp_keyring::sr25519::Keyring as Sr25519Keyring;
@@ -1053,7 +937,7 @@ mod tests {
 
 		let mut counted = 0;
 		for (core, assignment) in assignments {
-			let cores = match assignment.cert.kind.clone() {
+			let cores = match assignment.cert().kind.clone() {
 				AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield } => core_bitfield,
 				AssignmentCertKindV2::RelayVRFModulo { sample: _ } => core.into(),
 				AssignmentCertKindV2::RelayVRFDelay { core_index } => core_index.into(),
@@ -1062,7 +946,7 @@ mod tests {
 			let mut mutated = MutatedAssignment {
 				cores: cores.clone(),
 				groups: cores.iter_ones().map(|core| group_for_core(core)).collect(),
-				cert: assignment.cert,
+				cert: assignment.into_cert(),
 				own_group: GroupIndex(0),
 				val_index: ValidatorIndex(0),
 				config: config.clone(),
diff --git a/polkadot/node/core/approval-voting/src/import.rs b/polkadot/node/core/approval-voting/src/import.rs
index 3ddef1e01c4..b163d718eb2 100644
--- a/polkadot/node/core/approval-voting/src/import.rs
+++ b/polkadot/node/core/approval-voting/src/import.rs
@@ -62,9 +62,10 @@ use crate::{
 	criteria::{AssignmentCriteria, OurAssignment},
 	get_extended_session_info, get_session_info,
 	persisted_entries::CandidateEntry,
-	time::{slot_number_to_tick, Tick},
 };
 
+use polkadot_node_primitives::approval::time::{slot_number_to_tick, Tick};
+
 use super::{State, LOG_TARGET};
 
 #[derive(Debug)]
@@ -574,9 +575,13 @@ pub(crate) async fn handle_new_head<Context, B: Backend>(
 			hash: block_hash,
 			number: block_header.number,
 			parent_hash: block_header.parent_hash,
-			candidates: included_candidates.iter().map(|(hash, _, _, _)| *hash).collect(),
+			candidates: included_candidates
+				.iter()
+				.map(|(hash, _, core_index, group_index)| (*hash, *core_index, *group_index))
+				.collect(),
 			slot,
 			session: session_index,
+			vrf_story: relay_vrf_story,
 		});
 
 		imported_candidates.push(BlockImportedCandidates {
@@ -609,6 +614,7 @@ pub(crate) mod tests {
 		approval_db::common::{load_block_entry, DbBackend},
 		RuntimeInfo, RuntimeInfoConfig, MAX_BLOCKS_WITH_ASSIGNMENT_TIMESTAMPS,
 	};
+	use approval_types::time::Clock;
 	use assert_matches::assert_matches;
 	use polkadot_node_primitives::{
 		approval::v1::{VrfSignature, VrfTranscript},
@@ -642,7 +648,7 @@ pub(crate) mod tests {
 	#[derive(Default)]
 	struct MockClock;
 
-	impl crate::time::Clock for MockClock {
+	impl Clock for MockClock {
 		fn tick_now(&self) -> Tick {
 			42 // chosen by fair dice roll
 		}
diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs
index d4b6855a44d..942922cba6d 100644
--- a/polkadot/node/core/approval-voting/src/lib.rs
+++ b/polkadot/node/core/approval-voting/src/lib.rs
@@ -40,8 +40,9 @@ use polkadot_node_subsystem::{
 		ApprovalCheckError, ApprovalCheckResult, ApprovalDistributionMessage,
 		ApprovalVotingMessage, AssignmentCheckError, AssignmentCheckResult,
 		AvailabilityRecoveryMessage, BlockDescription, CandidateValidationMessage, ChainApiMessage,
-		ChainSelectionMessage, DisputeCoordinatorMessage, HighestApprovedAncestorBlock,
-		RuntimeApiMessage, RuntimeApiRequest,
+		ChainSelectionMessage, CheckedIndirectAssignment, CheckedIndirectSignedApprovalVote,
+		DisputeCoordinatorMessage, HighestApprovedAncestorBlock, RuntimeApiMessage,
+		RuntimeApiRequest,
 	},
 	overseer, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError, SubsystemResult,
 	SubsystemSender,
@@ -55,9 +56,8 @@ use polkadot_node_subsystem_util::{
 };
 use polkadot_primitives::{
 	ApprovalVoteMultipleCandidates, ApprovalVotingParams, BlockNumber, CandidateHash,
-	CandidateIndex, CandidateReceipt, CoreIndex, DisputeStatement, ExecutorParams, GroupIndex,
-	Hash, PvfExecKind, SessionIndex, SessionInfo, ValidDisputeStatementKind, ValidatorId,
-	ValidatorIndex, ValidatorPair, ValidatorSignature,
+	CandidateIndex, CandidateReceipt, CoreIndex, ExecutorParams, GroupIndex, Hash, PvfExecKind,
+	SessionIndex, SessionInfo, ValidatorId, ValidatorIndex, ValidatorPair, ValidatorSignature,
 };
 use sc_keystore::LocalKeystore;
 use sp_application_crypto::Pair;
@@ -91,9 +91,11 @@ use schnellru::{ByLength, LruMap};
 
 use approval_checking::RequiredTranches;
 use bitvec::{order::Lsb0, vec::BitVec};
-use criteria::{AssignmentCriteria, RealAssignmentCriteria};
+pub use criteria::{AssignmentCriteria, Config as AssignmentConfig, RealAssignmentCriteria};
 use persisted_entries::{ApprovalEntry, BlockEntry, CandidateEntry};
-use time::{slot_number_to_tick, Clock, ClockExt, DelayedApprovalTimer, SystemClock, Tick};
+use polkadot_node_primitives::approval::time::{
+	slot_number_to_tick, Clock, ClockExt, DelayedApprovalTimer, SystemClock, Tick,
+};
 
 mod approval_checking;
 pub mod approval_db;
@@ -102,7 +104,6 @@ pub mod criteria;
 mod import;
 mod ops;
 mod persisted_entries;
-pub mod time;
 
 use crate::{
 	approval_checking::{Check, TranchesToApproveResult},
@@ -123,7 +124,6 @@ const APPROVAL_CHECKING_TIMEOUT: Duration = Duration::from_secs(120);
 const WAIT_FOR_SIGS_TIMEOUT: Duration = Duration::from_millis(500);
 const APPROVAL_CACHE_SIZE: u32 = 1024;
 
-const TICK_TOO_FAR_IN_FUTURE: Tick = 20; // 10 seconds.
 const APPROVAL_DELAY: Tick = 2;
 pub(crate) const LOG_TARGET: &str = "parachain::approval-voting";
 
@@ -1607,9 +1607,30 @@ async fn distribution_messages_for_activation<Context>(
 			hash: block_hash,
 			number: block_entry.block_number(),
 			parent_hash: block_entry.parent_hash(),
-			candidates: block_entry.candidates().iter().map(|(_, c_hash)| *c_hash).collect(),
+			candidates: block_entry
+				.candidates()
+				.iter()
+				.map(|(core_index, c_hash)| {
+					let candidate = db.load_candidate_entry(c_hash).ok().flatten();
+					let group_index = candidate
+						.and_then(|entry| {
+							entry.approval_entry(&block_hash).map(|entry| entry.backing_group())
+						})
+						.unwrap_or_else(|| {
+							gum::warn!(
+								target: LOG_TARGET,
+								?block_hash,
+								?c_hash,
+								"Missing candidate entry or approval entry",
+							);
+							GroupIndex::default()
+						});
+					(*c_hash, *core_index, group_index)
+				})
+				.collect(),
 			slot: block_entry.slot(),
 			session: block_entry.session(),
+			vrf_story: block_entry.relay_vrf_story(),
 		});
 		let mut signatures_queued = HashSet::new();
 		for (core_index, candidate_hash) in block_entry.candidates() {
@@ -1872,35 +1893,45 @@ async fn handle_from_overseer<Context>(
 			vec![Action::Conclude]
 		},
 		FromOrchestra::Communication { msg } => match msg {
-			ApprovalVotingMessage::CheckAndImportAssignment(a, claimed_cores, res) => {
-				let (check_outcome, actions) = check_and_import_assignment(
+			ApprovalVotingMessage::ImportAssignment(checked_assignment, tx) => {
+				let (check_outcome, actions) = import_assignment(
 					ctx.sender(),
 					state,
 					db,
 					session_info_provider,
-					a,
-					claimed_cores,
+					checked_assignment,
 				)
 				.await?;
-				let _ = res.send(check_outcome);
-
+				// approval-distribution makes sure this assignment is valid and expected,
+				// so this import should never fail, if it does it might mean one of two things,
+				// there is a bug in the code or the two subsystems got out of sync.
+				if let AssignmentCheckResult::Bad(ref err) = check_outcome {
+					gum::debug!(target: LOG_TARGET, ?err, "Unexpected fail when importing an assignment");
+				}
+				let _ = tx.map(|tx| tx.send(check_outcome));
 				actions
 			},
-			ApprovalVotingMessage::CheckAndImportApproval(a, res) =>
-				check_and_import_approval(
+			ApprovalVotingMessage::ImportApproval(a, tx) => {
+				let result = import_approval(
 					ctx.sender(),
 					state,
 					db,
 					session_info_provider,
 					metrics,
 					a,
-					|r| {
-						let _ = res.send(r);
-					},
 					&wakeups,
 				)
-				.await?
-				.0,
+				.await?;
+				// approval-distribution makes sure this vote is valid and expected,
+				// so this import should never fail, if it does it might mean one of two things,
+				// there is a bug in the code or the two subsystems got out of sync.
+				if let ApprovalCheckResult::Bad(ref err) = result.1 {
+					gum::debug!(target: LOG_TARGET, ?err, "Unexpected fail when importing an approval");
+				}
+				let _ = tx.map(|tx| tx.send(result.1));
+
+				result.0
+			},
 			ApprovalVotingMessage::ApprovedAncestor(target, lower_bound, res) => {
 				let mut approved_ancestor_span = state
 					.spans
@@ -2439,29 +2470,30 @@ fn schedule_wakeup_action(
 	maybe_action
 }
 
-async fn check_and_import_assignment<Sender>(
+async fn import_assignment<Sender>(
 	sender: &mut Sender,
 	state: &State,
 	db: &mut OverlayedBackend<'_, impl Backend>,
 	session_info_provider: &mut RuntimeInfo,
-	assignment: IndirectAssignmentCertV2,
-	candidate_indices: CandidateBitfield,
+	checked_assignment: CheckedIndirectAssignment,
 ) -> SubsystemResult<(AssignmentCheckResult, Vec<Action>)>
 where
 	Sender: SubsystemSender<RuntimeApiMessage>,
 {
 	let tick_now = state.clock.tick_now();
-
-	let mut check_and_import_assignment_span = state
+	let assignment = checked_assignment.assignment();
+	let candidate_indices = checked_assignment.candidate_indices();
+	let tranche = checked_assignment.tranche();
+	let mut import_assignment_span = state
 		.spans
 		.get(&assignment.block_hash)
-		.map(|span| span.child("check-and-import-assignment"))
-		.unwrap_or_else(|| jaeger::Span::new(assignment.block_hash, "check-and-import-assignment"))
+		.map(|span| span.child("import-assignment"))
+		.unwrap_or_else(|| jaeger::Span::new(assignment.block_hash, "import-assignment"))
 		.with_relay_parent(assignment.block_hash)
 		.with_stage(jaeger::Stage::ApprovalChecking);
 
 	for candidate_index in candidate_indices.iter_ones() {
-		check_and_import_assignment_span.add_uint_tag("candidate-index", candidate_index as u64);
+		import_assignment_span.add_uint_tag("candidate-index", candidate_index as u64);
 	}
 
 	let block_entry = match db.load_block_entry(&assignment.block_hash)? {
@@ -2514,8 +2546,6 @@ where
 		))
 	}
 
-	// The Compact VRF modulo assignment cert has multiple core assignments.
-	let mut backing_groups = Vec::new();
 	let mut claimed_core_indices = Vec::new();
 	let mut assigned_candidate_hashes = Vec::new();
 
@@ -2544,26 +2574,23 @@ where
 				)), // no candidate at core.
 		};
 
-		check_and_import_assignment_span
+		import_assignment_span
 			.add_string_tag("candidate-hash", format!("{:?}", assigned_candidate_hash));
-		check_and_import_assignment_span.add_string_tag(
+		import_assignment_span.add_string_tag(
 			"traceID",
 			format!("{:?}", jaeger::hash_to_trace_identifier(assigned_candidate_hash.0)),
 		);
 
-		let approval_entry = match candidate_entry.approval_entry_mut(&assignment.block_hash) {
-			Some(a) => a,
-			None =>
-				return Ok((
-					AssignmentCheckResult::Bad(AssignmentCheckError::Internal(
-						assignment.block_hash,
-						assigned_candidate_hash,
-					)),
-					Vec::new(),
+		if candidate_entry.approval_entry_mut(&assignment.block_hash).is_none() {
+			return Ok((
+				AssignmentCheckResult::Bad(AssignmentCheckError::Internal(
+					assignment.block_hash,
+					assigned_candidate_hash,
 				)),
+				Vec::new(),
+			));
 		};
 
-		backing_groups.push(approval_entry.backing_group());
 		claimed_core_indices.push(claimed_core_index);
 		assigned_candidate_hashes.push(assigned_candidate_hash);
 	}
@@ -2579,42 +2606,6 @@ where
 		))
 	}
 
-	// Check the assignment certificate.
-	let res = state.assignment_criteria.check_assignment_cert(
-		claimed_core_indices
-			.clone()
-			.try_into()
-			.expect("Checked for null assignment above; qed"),
-		assignment.validator,
-		&criteria::Config::from(session_info),
-		block_entry.relay_vrf_story(),
-		&assignment.cert,
-		backing_groups,
-	);
-
-	let tranche = match res {
-		Err(crate::criteria::InvalidAssignment(reason)) =>
-			return Ok((
-				AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCert(
-					assignment.validator,
-					format!("{:?}", reason),
-				)),
-				Vec::new(),
-			)),
-		Ok(tranche) => {
-			let current_tranche =
-				state.clock.tranche_now(state.slot_duration_millis, block_entry.slot());
-
-			let too_far_in_future = current_tranche + TICK_TOO_FAR_IN_FUTURE as DelayTranche;
-
-			if tranche >= too_far_in_future {
-				return Ok((AssignmentCheckResult::TooFarInFuture, Vec::new()))
-			}
-
-			tranche
-		},
-	};
-
 	let mut actions = Vec::new();
 	let res = {
 		let mut is_duplicate = true;
@@ -2647,7 +2638,7 @@ where
 			};
 			is_duplicate &= approval_entry.is_assigned(assignment.validator);
 			approval_entry.import_assignment(tranche, assignment.validator, tick_now);
-			check_and_import_assignment_span.add_uint_tag("tranche", tranche as u64);
+			import_assignment_span.add_uint_tag("tranche", tranche as u64);
 
 			// We've imported a new assignment, so we need to schedule a wake-up for when that might
 			// no-show.
@@ -2704,30 +2695,28 @@ where
 	Ok((res, actions))
 }
 
-async fn check_and_import_approval<T, Sender>(
+async fn import_approval<Sender>(
 	sender: &mut Sender,
 	state: &mut State,
 	db: &mut OverlayedBackend<'_, impl Backend>,
 	session_info_provider: &mut RuntimeInfo,
 	metrics: &Metrics,
-	approval: IndirectSignedApprovalVoteV2,
-	with_response: impl FnOnce(ApprovalCheckResult) -> T,
+	approval: CheckedIndirectSignedApprovalVote,
 	wakeups: &Wakeups,
-) -> SubsystemResult<(Vec<Action>, T)>
+) -> SubsystemResult<(Vec<Action>, ApprovalCheckResult)>
 where
 	Sender: SubsystemSender<RuntimeApiMessage>,
 {
 	macro_rules! respond_early {
 		($e: expr) => {{
-			let t = with_response($e);
-			return Ok((Vec::new(), t))
+			return Ok((Vec::new(), $e))
 		}};
 	}
 	let mut span = state
 		.spans
 		.get(&approval.block_hash)
-		.map(|span| span.child("check-and-import-approval"))
-		.unwrap_or_else(|| jaeger::Span::new(approval.block_hash, "check-and-import-approval"))
+		.map(|span| span.child("import-approval"))
+		.unwrap_or_else(|| jaeger::Span::new(approval.block_hash, "import-approval"))
 		.with_string_fmt_debug_tag("candidate-index", approval.candidate_indices.clone())
 		.with_relay_parent(approval.block_hash)
 		.with_stage(jaeger::Stage::ApprovalChecking);
@@ -2774,67 +2763,11 @@ where
 		),
 	);
 
-	{
-		let session_info = match get_session_info(
-			session_info_provider,
-			sender,
-			approval.block_hash,
-			block_entry.session(),
-		)
-		.await
-		{
-			Some(s) => s,
-			None => {
-				respond_early!(ApprovalCheckResult::Bad(ApprovalCheckError::UnknownSessionIndex(
-					block_entry.session()
-				),))
-			},
-		};
-
-		let pubkey = match session_info.validators.get(approval.validator) {
-			Some(k) => k,
-			None => respond_early!(ApprovalCheckResult::Bad(
-				ApprovalCheckError::InvalidValidatorIndex(approval.validator),
-			)),
-		};
-
-		gum::trace!(
-			target: LOG_TARGET,
-			"Received approval for num_candidates {:}",
-			approval.candidate_indices.count_ones()
-		);
-
-		let candidate_hashes: Vec<CandidateHash> =
-			approved_candidates_info.iter().map(|candidate| candidate.1).collect();
-		// Signature check:
-		match DisputeStatement::Valid(
-			ValidDisputeStatementKind::ApprovalCheckingMultipleCandidates(candidate_hashes.clone()),
-		)
-		.check_signature(
-			&pubkey,
-			if let Some(candidate_hash) = candidate_hashes.first() {
-				*candidate_hash
-			} else {
-				respond_early!(ApprovalCheckResult::Bad(ApprovalCheckError::InvalidValidatorIndex(
-					approval.validator
-				),))
-			},
-			block_entry.session(),
-			&approval.signature,
-		) {
-			Err(_) => {
-				gum::error!(
-					target: LOG_TARGET,
-					"Error while checking signature {:}",
-					approval.candidate_indices.count_ones()
-				);
-				respond_early!(ApprovalCheckResult::Bad(ApprovalCheckError::InvalidSignature(
-					approval.validator
-				),))
-			},
-			Ok(()) => {},
-		};
-	}
+	gum::trace!(
+		target: LOG_TARGET,
+		"Received approval for num_candidates {:}",
+		approval.candidate_indices.count_ones()
+	);
 
 	let mut actions = Vec::new();
 	for (approval_candidate_index, approved_candidate_hash) in approved_candidates_info {
@@ -2898,9 +2831,7 @@ where
 	}
 
 	// importing the approval can be heavy as it may trigger acceptance for a series of blocks.
-	let t = with_response(ApprovalCheckResult::Accepted);
-
-	Ok((actions, t))
+	Ok((actions, ApprovalCheckResult::Accepted))
 }
 
 #[derive(Debug)]
diff --git a/polkadot/node/core/approval-voting/src/persisted_entries.rs b/polkadot/node/core/approval-voting/src/persisted_entries.rs
index 59a46181005..16e231aa1a2 100644
--- a/polkadot/node/core/approval-voting/src/persisted_entries.rs
+++ b/polkadot/node/core/approval-voting/src/persisted_entries.rs
@@ -36,7 +36,9 @@ use std::collections::BTreeMap;
 
 use crate::approval_db::v2::Bitfield;
 
-use super::{criteria::OurAssignment, time::Tick};
+use super::criteria::OurAssignment;
+
+use polkadot_node_primitives::approval::time::Tick;
 
 /// Metadata regarding a specific tranche of assignments for a specific candidate.
 #[derive(Debug, Clone, PartialEq)]
diff --git a/polkadot/node/core/approval-voting/src/tests.rs b/polkadot/node/core/approval-voting/src/tests.rs
index b912449baa4..7126f209a94 100644
--- a/polkadot/node/core/approval-voting/src/tests.rs
+++ b/polkadot/node/core/approval-voting/src/tests.rs
@@ -41,8 +41,9 @@ use polkadot_node_subsystem_test_helpers as test_helpers;
 use polkadot_node_subsystem_util::TimeoutExt;
 use polkadot_overseer::HeadSupportsParachains;
 use polkadot_primitives::{
-	ApprovalVote, CandidateCommitments, CandidateEvent, CoreIndex, GroupIndex, Header,
-	Id as ParaId, IndexedVec, NodeFeatures, ValidationCode, ValidatorSignature,
+	ApprovalVote, CandidateCommitments, CandidateEvent, CoreIndex, DisputeStatement, GroupIndex,
+	Header, Id as ParaId, IndexedVec, NodeFeatures, ValidDisputeStatementKind, ValidationCode,
+	ValidatorSignature,
 };
 use std::{cmp::max, time::Duration};
 
@@ -139,8 +140,8 @@ impl HeadSupportsParachains for MockSupportsParachains {
 	}
 }
 
-fn slot_to_tick(t: impl Into<Slot>) -> crate::time::Tick {
-	crate::time::slot_number_to_tick(SLOT_DURATION_MILLIS, t.into())
+fn slot_to_tick(t: impl Into<Slot>) -> Tick {
+	slot_number_to_tick(SLOT_DURATION_MILLIS, t.into())
 }
 
 #[derive(Default, Clone)]
@@ -647,7 +648,7 @@ fn make_candidate(para_id: ParaId, hash: &Hash) -> CandidateReceipt {
 	r
 }
 
-async fn check_and_import_approval(
+async fn import_approval(
 	overseer: &mut VirtualOverseer,
 	block_hash: Hash,
 	candidate_index: CandidateIndex,
@@ -666,14 +667,14 @@ async fn check_and_import_approval(
 	overseer_send(
 		overseer,
 		FromOrchestra::Communication {
-			msg: ApprovalVotingMessage::CheckAndImportApproval(
-				IndirectSignedApprovalVoteV2 {
+			msg: ApprovalVotingMessage::ImportApproval(
+				CheckedIndirectSignedApprovalVote::from_checked(IndirectSignedApprovalVoteV2 {
 					block_hash,
 					candidate_indices: candidate_index.into(),
 					validator,
 					signature,
-				},
-				tx,
+				}),
+				Some(tx),
 			),
 		},
 	)
@@ -689,25 +690,31 @@ async fn check_and_import_approval(
 	rx
 }
 
-async fn check_and_import_assignment(
+async fn import_assignment(
 	overseer: &mut VirtualOverseer,
 	block_hash: Hash,
 	candidate_index: CandidateIndex,
 	validator: ValidatorIndex,
+	tranche: DelayTranche,
 ) -> oneshot::Receiver<AssignmentCheckResult> {
 	let (tx, rx) = oneshot::channel();
 	overseer_send(
 		overseer,
 		FromOrchestra::Communication {
-			msg: ApprovalVotingMessage::CheckAndImportAssignment(
-				IndirectAssignmentCertV2 {
-					block_hash,
-					validator,
-					cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 })
+			msg: ApprovalVotingMessage::ImportAssignment(
+				CheckedIndirectAssignment::from_checked(
+					IndirectAssignmentCertV2 {
+						block_hash,
+						validator,
+						cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
+							sample: 0,
+						})
 						.into(),
-				},
-				candidate_index.into(),
-				tx,
+					},
+					candidate_index.into(),
+					tranche,
+				),
+				Some(tx),
 			),
 		},
 	)
@@ -715,7 +722,7 @@ async fn check_and_import_assignment(
 	rx
 }
 
-async fn check_and_import_assignment_v2(
+async fn import_assignment_v2(
 	overseer: &mut VirtualOverseer,
 	block_hash: Hash,
 	core_indices: Vec<u32>,
@@ -725,22 +732,27 @@ async fn check_and_import_assignment_v2(
 	overseer_send(
 		overseer,
 		FromOrchestra::Communication {
-			msg: ApprovalVotingMessage::CheckAndImportAssignment(
-				IndirectAssignmentCertV2 {
-					block_hash,
-					validator,
-					cert: garbage_assignment_cert_v2(AssignmentCertKindV2::RelayVRFModuloCompact {
-						core_bitfield: core_indices
-							.clone()
-							.into_iter()
-							.map(|c| CoreIndex(c))
-							.collect::<Vec<_>>()
-							.try_into()
-							.unwrap(),
-					}),
-				},
-				core_indices.try_into().unwrap(),
-				tx,
+			msg: ApprovalVotingMessage::ImportAssignment(
+				CheckedIndirectAssignment::from_checked(
+					IndirectAssignmentCertV2 {
+						block_hash,
+						validator,
+						cert: garbage_assignment_cert_v2(
+							AssignmentCertKindV2::RelayVRFModuloCompact {
+								core_bitfield: core_indices
+									.clone()
+									.into_iter()
+									.map(|c| CoreIndex(c))
+									.collect::<Vec<_>>()
+									.try_into()
+									.unwrap(),
+							},
+						),
+					},
+					core_indices.try_into().unwrap(),
+					0,
+				),
+				Some(tx),
 			),
 		},
 	)
@@ -1121,26 +1133,18 @@ fn subsystem_rejects_bad_assignment_ok_criteria() {
 		);
 		builder.build(&mut virtual_overseer).await;
 
-		let rx = check_and_import_assignment(
-			&mut virtual_overseer,
-			block_hash,
-			candidate_index,
-			validator,
-		)
-		.await;
+		let rx =
+			import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0)
+				.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;
+		let rx =
+			import_assignment(&mut virtual_overseer, unknown_hash, candidate_index, validator, 0)
+				.await;
 
 		assert_eq!(
 			rx.await,
@@ -1151,59 +1155,6 @@ fn subsystem_rejects_bad_assignment_ok_criteria() {
 	});
 }
 
-#[test]
-fn subsystem_rejects_bad_assignment_err_criteria() {
-	let assignment_criteria = Box::new(MockAssignmentCriteria::check_only(move |_| {
-		Err(criteria::InvalidAssignment(
-			criteria::InvalidAssignmentReason::ValidatorIndexOutOfBounds,
-		))
-	}));
-	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;
-		assert_matches!(
-			overseer_recv(&mut virtual_overseer).await,
-			AllMessages::ChainApi(ChainApiMessage::FinalizedBlockNumber(rx)) => {
-				rx.send(Ok(0)).unwrap();
-			}
-		);
-
-		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, end_syncing: false },
-		);
-		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),
-				"ValidatorIndexOutOfBounds".to_string(),
-			))),
-		);
-
-		virtual_overseer
-	});
-}
-
 #[test]
 fn blank_subsystem_act_on_bad_block() {
 	test_harness(HarnessConfig::default(), |test_harness| async move {
@@ -1222,17 +1173,20 @@ fn blank_subsystem_act_on_bad_block() {
 		overseer_send(
 			&mut virtual_overseer,
 			FromOrchestra::Communication {
-				msg: ApprovalVotingMessage::CheckAndImportAssignment(
-					IndirectAssignmentCertV2 {
-						block_hash: bad_block_hash,
-						validator: 0u32.into(),
-						cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
-							sample: 0,
-						})
-						.into(),
-					},
-					0u32.into(),
-					tx,
+				msg: ApprovalVotingMessage::ImportAssignment(
+					CheckedIndirectAssignment::from_checked(
+						IndirectAssignmentCertV2 {
+							block_hash: bad_block_hash,
+							validator: 0u32.into(),
+							cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
+								sample: 0,
+							})
+							.into(),
+						},
+						0u32.into(),
+						0,
+					),
+					Some(tx),
 				),
 			},
 		)
@@ -1295,7 +1249,7 @@ fn subsystem_rejects_approval_if_no_candidate_entry() {
 		});
 
 		let session_index = 1;
-		let rx = check_and_import_approval(
+		let rx = import_approval(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
@@ -1336,7 +1290,7 @@ fn subsystem_rejects_approval_if_no_block_entry() {
 		let candidate_hash = dummy_candidate_receipt(block_hash).hash();
 		let session_index = 1;
 
-		let rx = check_and_import_approval(
+		let rx = import_approval(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
@@ -1401,7 +1355,7 @@ fn subsystem_rejects_approval_before_assignment() {
 			.build(&mut virtual_overseer)
 			.await;
 
-		let rx = check_and_import_approval(
+		let rx = import_approval(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
@@ -1424,68 +1378,6 @@ fn subsystem_rejects_approval_before_assignment() {
 	});
 }
 
-#[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;
-		assert_matches!(
-			overseer_recv(&mut virtual_overseer).await,
-			AllMessages::ChainApi(ChainApiMessage::FinalizedBlockNumber(rx)) => {
-				rx.send(Ok(0)).unwrap();
-			}
-		);
-
-		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,
-					end_syncing: false,
-				},
-			)
-			.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 {
@@ -1535,7 +1427,7 @@ fn subsystem_accepts_duplicate_assignment() {
 			.await;
 
 		// Initial assignment.
-		let rx = check_and_import_assignment_v2(
+		let rx = import_assignment_v2(
 			&mut virtual_overseer,
 			block_hash,
 			vec![candidate_index1, candidate_index2],
@@ -1546,19 +1438,15 @@ fn subsystem_accepts_duplicate_assignment() {
 		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
 
 		// Test with single assigned core.
-		let rx = check_and_import_assignment(
-			&mut virtual_overseer,
-			block_hash,
-			candidate_index,
-			validator,
-		)
-		.await;
+		let rx =
+			import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0)
+				.await;
 
 		assert_eq!(rx.await, Ok(AssignmentCheckResult::AcceptedDuplicate));
 
 		// Test with multiple assigned cores. This cannot happen in practice, as tranche0
 		// assignments are sent first, but we should still ensure correct behavior.
-		let rx = check_and_import_assignment_v2(
+		let rx = import_assignment_v2(
 			&mut virtual_overseer,
 			block_hash,
 			vec![candidate_index1, candidate_index2],
@@ -1604,13 +1492,9 @@ fn subsystem_rejects_assignment_with_unknown_candidate() {
 			.build(&mut virtual_overseer)
 			.await;
 
-		let rx = check_and_import_assignment(
-			&mut virtual_overseer,
-			block_hash,
-			candidate_index,
-			validator,
-		)
-		.await;
+		let rx =
+			import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0)
+				.await;
 
 		assert_eq!(
 			rx.await,
@@ -1654,13 +1538,9 @@ fn subsystem_rejects_oversized_bitfields() {
 			.build(&mut virtual_overseer)
 			.await;
 
-		let rx = check_and_import_assignment(
-			&mut virtual_overseer,
-			block_hash,
-			candidate_index,
-			validator,
-		)
-		.await;
+		let rx =
+			import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0)
+				.await;
 
 		assert_eq!(
 			rx.await,
@@ -1669,13 +1549,9 @@ fn subsystem_rejects_oversized_bitfields() {
 			))),
 		);
 
-		let rx = check_and_import_assignment_v2(
-			&mut virtual_overseer,
-			block_hash,
-			vec![1, 2, 10, 50],
-			validator,
-		)
-		.await;
+		let rx =
+			import_assignment_v2(&mut virtual_overseer, block_hash, vec![1, 2, 10, 50], validator)
+				.await;
 
 		assert_eq!(
 			rx.await,
@@ -1727,17 +1603,13 @@ fn subsystem_accepts_and_imports_approval_after_assignment() {
 			.build(&mut virtual_overseer)
 			.await;
 
-		let rx = check_and_import_assignment(
-			&mut virtual_overseer,
-			block_hash,
-			candidate_index,
-			validator,
-		)
-		.await;
+		let rx =
+			import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0)
+				.await;
 
 		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
 
-		let rx = check_and_import_approval(
+		let rx = import_approval(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
@@ -1819,19 +1691,15 @@ fn subsystem_second_approval_import_only_schedules_wakeups() {
 			.build(&mut virtual_overseer)
 			.await;
 
-		let rx = check_and_import_assignment(
-			&mut virtual_overseer,
-			block_hash,
-			candidate_index,
-			validator,
-		)
-		.await;
+		let rx =
+			import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0)
+				.await;
 
 		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
 
 		assert!(clock.inner.lock().current_wakeup_is(APPROVAL_DELAY + 2));
 
-		let rx = check_and_import_approval(
+		let rx = import_approval(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
@@ -1848,7 +1716,7 @@ fn subsystem_second_approval_import_only_schedules_wakeups() {
 		futures_timer::Delay::new(Duration::from_millis(100)).await;
 		assert!(clock.inner.lock().current_wakeup_is(APPROVAL_DELAY + 2));
 
-		let rx = check_and_import_approval(
+		let rx = import_approval(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
@@ -1907,13 +1775,9 @@ fn subsystem_assignment_import_updates_candidate_entry_and_schedules_wakeup() {
 			.build(&mut virtual_overseer)
 			.await;
 
-		let rx = check_and_import_assignment(
-			&mut virtual_overseer,
-			block_hash,
-			candidate_index,
-			validator,
-		)
-		.await;
+		let rx =
+			import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0)
+				.await;
 
 		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
 
@@ -2029,20 +1893,17 @@ fn test_approvals_on_fork_are_always_considered_after_no_show(
 			.await;
 
 		// Send assignments for the same candidate on both forks
-		let rx = check_and_import_assignment(
-			&mut virtual_overseer,
-			block_hash,
-			candidate_index,
-			validator,
-		)
-		.await;
+		let rx =
+			import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0)
+				.await;
 		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
 
-		let rx = check_and_import_assignment(
+		let rx = import_assignment(
 			&mut virtual_overseer,
 			block_hash_fork,
 			candidate_index,
 			validator,
+			0,
 		)
 		.await;
 
@@ -2072,7 +1933,7 @@ fn test_approvals_on_fork_are_always_considered_after_no_show(
 		futures_timer::Delay::new(Duration::from_millis(100)).await;
 
 		// Send the approval for candidate just in the context of 0x01 block.
-		let rx = check_and_import_approval(
+		let rx = import_approval(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
@@ -2142,13 +2003,9 @@ fn subsystem_process_wakeup_schedules_wakeup() {
 			.build(&mut virtual_overseer)
 			.await;
 
-		let rx = check_and_import_assignment(
-			&mut virtual_overseer,
-			block_hash,
-			candidate_index,
-			validator,
-		)
-		.await;
+		let rx =
+			import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0)
+				.await;
 
 		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
 
@@ -2201,17 +2058,20 @@ fn linear_import_act_on_leaf() {
 		overseer_send(
 			&mut virtual_overseer,
 			FromOrchestra::Communication {
-				msg: ApprovalVotingMessage::CheckAndImportAssignment(
-					IndirectAssignmentCertV2 {
-						block_hash: head,
-						validator: 0u32.into(),
-						cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
-							sample: 0,
-						})
-						.into(),
-					},
-					0u32.into(),
-					tx,
+				msg: ApprovalVotingMessage::ImportAssignment(
+					CheckedIndirectAssignment::from_checked(
+						IndirectAssignmentCertV2 {
+							block_hash: head,
+							validator: 0u32.into(),
+							cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
+								sample: 0,
+							})
+							.into(),
+						},
+						0u32.into(),
+						0,
+					),
+					Some(tx),
 				),
 			},
 		)
@@ -2272,17 +2132,20 @@ fn forkful_import_at_same_height_act_on_leaf() {
 			overseer_send(
 				&mut virtual_overseer,
 				FromOrchestra::Communication {
-					msg: ApprovalVotingMessage::CheckAndImportAssignment(
-						IndirectAssignmentCertV2 {
-							block_hash: head,
-							validator: 0u32.into(),
-							cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
-								sample: 0,
-							})
-							.into(),
-						},
-						0u32.into(),
-						tx,
+					msg: ApprovalVotingMessage::ImportAssignment(
+						CheckedIndirectAssignment::from_checked(
+							IndirectAssignmentCertV2 {
+								block_hash: head,
+								validator: 0u32.into(),
+								cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
+									sample: 0,
+								})
+								.into(),
+							},
+							0u32.into(),
+							0,
+						),
+						Some(tx),
 					),
 				},
 			)
@@ -2439,21 +2302,23 @@ fn import_checked_approval_updates_entries_and_schedules() {
 
 		let candidate_index = 0;
 
-		let rx = check_and_import_assignment(
+		let rx = import_assignment(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
 			validator_index_a,
+			0,
 		)
 		.await;
 
 		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),);
 
-		let rx = check_and_import_assignment(
+		let rx = import_assignment(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
 			validator_index_b,
+			0,
 		)
 		.await;
 
@@ -2462,7 +2327,7 @@ fn import_checked_approval_updates_entries_and_schedules() {
 		let session_index = 1;
 		let sig_a = sign_approval(Sr25519Keyring::Alice, candidate_hash, session_index);
 
-		let rx = check_and_import_approval(
+		let rx = import_approval(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
@@ -2489,7 +2354,7 @@ fn import_checked_approval_updates_entries_and_schedules() {
 		clock.inner.lock().wakeup_all(2);
 
 		let sig_b = sign_approval(Sr25519Keyring::Bob, candidate_hash, session_index);
-		let rx = check_and_import_approval(
+		let rx = import_approval(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
@@ -2602,13 +2467,9 @@ fn subsystem_import_checked_approval_sets_one_block_bit_at_a_time() {
 		];
 
 		for (candidate_index, validator) in assignments {
-			let rx = check_and_import_assignment(
-				&mut virtual_overseer,
-				block_hash,
-				candidate_index,
-				validator,
-			)
-			.await;
+			let rx =
+				import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0)
+					.await;
 			assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
 		}
 
@@ -2629,7 +2490,7 @@ fn subsystem_import_checked_approval_sets_one_block_bit_at_a_time() {
 			} else {
 				sign_approval(Sr25519Keyring::Bob, *candidate_hash, session_index)
 			};
-			let rx = check_and_import_approval(
+			let rx = import_approval(
 				&mut virtual_overseer,
 				block_hash,
 				*candidate_index,
@@ -2887,11 +2748,12 @@ fn approved_ancestor_test(
 		for (i, (block_hash, candidate_hash)) in
 			block_hashes.iter().zip(candidate_hashes).enumerate()
 		{
-			let rx = check_and_import_assignment(
+			let rx = import_assignment(
 				&mut virtual_overseer,
 				*block_hash,
 				candidate_index,
 				validator,
+				0,
 			)
 			.await;
 			assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
@@ -2900,7 +2762,7 @@ fn approved_ancestor_test(
 				continue
 			}
 
-			let rx = check_and_import_approval(
+			let rx = import_approval(
 				&mut virtual_overseer,
 				*block_hash,
 				candidate_index,
@@ -3355,7 +3217,8 @@ where
 	F1: 'static
 		+ Fn(ValidatorIndex) -> Result<DelayTranche, criteria::InvalidAssignment>
 		+ Send
-		+ Sync,
+		+ Sync
+		+ Clone,
 	F2: Fn(Tick) -> bool,
 {
 	let TriggersAssignmentConfig {
@@ -3384,7 +3247,7 @@ where
 			);
 			assignments
 		},
-		assign_validator_tranche,
+		assign_validator_tranche.clone(),
 	));
 	let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build();
 	let store = config.backend();
@@ -3445,11 +3308,12 @@ where
 			.await;
 
 		for validator in assignments_to_import {
-			let rx = check_and_import_assignment(
+			let rx = import_assignment(
 				&mut virtual_overseer,
 				block_hash,
 				candidate_index,
 				ValidatorIndex(validator),
+				assign_validator_tranche(ValidatorIndex(validator)).unwrap(),
 			)
 			.await;
 			assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
@@ -3458,7 +3322,7 @@ where
 		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(
+			let rx = import_approval(
 				&mut virtual_overseer,
 				block_hash,
 				candidate_index,
@@ -3763,31 +3627,34 @@ fn pre_covers_dont_stall_approval() {
 
 		let candidate_index = 0;
 
-		let rx = check_and_import_assignment(
+		let rx = import_assignment(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
 			validator_index_a,
+			0,
 		)
 		.await;
 
 		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),);
 
-		let rx = check_and_import_assignment(
+		let rx = import_assignment(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
 			validator_index_b,
+			0,
 		)
 		.await;
 
 		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),);
 
-		let rx = check_and_import_assignment(
+		let rx = import_assignment(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
 			validator_index_c,
+			1,
 		)
 		.await;
 
@@ -3796,7 +3663,7 @@ fn pre_covers_dont_stall_approval() {
 		let session_index = 1;
 		let sig_b = sign_approval(Sr25519Keyring::Bob, candidate_hash, session_index);
 
-		let rx = check_and_import_approval(
+		let rx = import_approval(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
@@ -3811,7 +3678,7 @@ fn pre_covers_dont_stall_approval() {
 		assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted),);
 
 		let sig_c = sign_approval(Sr25519Keyring::Charlie, candidate_hash, session_index);
-		let rx = check_and_import_approval(
+		let rx = import_approval(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
@@ -3941,21 +3808,23 @@ fn waits_until_approving_assignments_are_old_enough() {
 
 		let candidate_index = 0;
 
-		let rx = check_and_import_assignment(
+		let rx = import_assignment(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
 			validator_index_a,
+			0,
 		)
 		.await;
 
 		assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),);
 
-		let rx = check_and_import_assignment(
+		let rx = import_assignment(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
 			validator_index_b,
+			0,
 		)
 		.await;
 
@@ -3966,7 +3835,7 @@ fn waits_until_approving_assignments_are_old_enough() {
 		let session_index = 1;
 
 		let sig_a = sign_approval(Sr25519Keyring::Alice, candidate_hash, session_index);
-		let rx = check_and_import_approval(
+		let rx = import_approval(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
@@ -3982,7 +3851,7 @@ fn waits_until_approving_assignments_are_old_enough() {
 
 		let sig_b = sign_approval(Sr25519Keyring::Bob, candidate_hash, session_index);
 
-		let rx = check_and_import_approval(
+		let rx = import_approval(
 			&mut virtual_overseer,
 			block_hash,
 			candidate_index,
@@ -4992,7 +4861,6 @@ fn subsystem_sends_pending_approvals_on_approval_restart() {
 				}));
 			}
 		);
-
 		assert_matches!(
 			overseer_recv(&mut virtual_overseer).await,
 			AllMessages::RuntimeApi(
diff --git a/polkadot/node/network/approval-distribution/Cargo.toml b/polkadot/node/network/approval-distribution/Cargo.toml
index 1bd3d51b5c9..51478dfa4a4 100644
--- a/polkadot/node/network/approval-distribution/Cargo.toml
+++ b/polkadot/node/network/approval-distribution/Cargo.toml
@@ -26,6 +26,8 @@ gum = { workspace = true, default-features = true }
 bitvec = { features = ["alloc"], workspace = true }
 
 [dev-dependencies]
+sc-keystore = { workspace = true }
+sp-application-crypto = { workspace = true, default-features = true }
 sp-authority-discovery = { workspace = true, default-features = true }
 sp-core = { features = ["std"], workspace = true, default-features = true }
 
diff --git a/polkadot/node/network/approval-distribution/src/lib.rs b/polkadot/node/network/approval-distribution/src/lib.rs
index a1bdc47e9fb..971b6de5f8f 100644
--- a/polkadot/node/network/approval-distribution/src/lib.rs
+++ b/polkadot/node/network/approval-distribution/src/lib.rs
@@ -24,7 +24,7 @@
 #![warn(missing_docs)]
 
 use self::metrics::Metrics;
-use futures::{channel::oneshot, select, FutureExt as _};
+use futures::{select, FutureExt as _};
 use itertools::Itertools;
 use net_protocol::peer_set::{ProtocolVersion, ValidationVersion};
 use polkadot_node_jaeger as jaeger;
@@ -35,29 +35,41 @@ use polkadot_node_network_protocol::{
 	v1 as protocol_v1, v2 as protocol_v2, v3 as protocol_v3, PeerId,
 	UnifiedReputationChange as Rep, Versioned, View,
 };
-use polkadot_node_primitives::approval::{
-	v1::{
-		AssignmentCertKind, BlockApprovalMeta, IndirectAssignmentCert, IndirectSignedApprovalVote,
-	},
-	v2::{
-		AsBitIndex, AssignmentCertKindV2, CandidateBitfield, IndirectAssignmentCertV2,
-		IndirectSignedApprovalVoteV2,
+use polkadot_node_primitives::{
+	approval::{
+		criteria::{AssignmentCriteria, InvalidAssignment},
+		time::{Clock, ClockExt, SystemClock, TICK_TOO_FAR_IN_FUTURE},
+		v1::{
+			AssignmentCertKind, BlockApprovalMeta, DelayTranche, IndirectAssignmentCert,
+			IndirectSignedApprovalVote, RelayVRFStory,
+		},
+		v2::{
+			AsBitIndex, AssignmentCertKindV2, CandidateBitfield, IndirectAssignmentCertV2,
+			IndirectSignedApprovalVoteV2,
+		},
 	},
+	DISPUTE_WINDOW,
 };
 use polkadot_node_subsystem::{
 	messages::{
-		ApprovalCheckResult, ApprovalDistributionMessage, ApprovalVotingMessage,
-		AssignmentCheckResult, NetworkBridgeEvent, NetworkBridgeTxMessage,
+		ApprovalDistributionMessage, ApprovalVotingMessage, CheckedIndirectAssignment,
+		CheckedIndirectSignedApprovalVote, NetworkBridgeEvent, NetworkBridgeTxMessage,
+		RuntimeApiMessage,
 	},
 	overseer, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError,
 };
-use polkadot_node_subsystem_util::reputation::{ReputationAggregator, REPUTATION_CHANGE_INTERVAL};
+use polkadot_node_subsystem_util::{
+	reputation::{ReputationAggregator, REPUTATION_CHANGE_INTERVAL},
+	runtime::{Config as RuntimeInfoConfig, ExtendedSessionInfo, RuntimeInfo},
+};
 use polkadot_primitives::{
-	BlockNumber, CandidateIndex, Hash, SessionIndex, ValidatorIndex, ValidatorSignature,
+	BlockNumber, CandidateHash, CandidateIndex, CoreIndex, DisputeStatement, GroupIndex, Hash,
+	SessionIndex, Slot, ValidDisputeStatementKind, ValidatorIndex, ValidatorSignature,
 };
 use rand::{CryptoRng, Rng, SeedableRng};
 use std::{
 	collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque},
+	sync::Arc,
 	time::Duration,
 };
 
@@ -86,6 +98,9 @@ const MAX_BITFIELD_SIZE: usize = 500;
 /// The Approval Distribution subsystem.
 pub struct ApprovalDistribution {
 	metrics: Metrics,
+	slot_duration_millis: u64,
+	clock: Box<dyn Clock + Send + Sync>,
+	assignment_criteria: Arc<dyn AssignmentCriteria + Send + Sync>,
 }
 
 /// Contains recently finalized
@@ -354,6 +369,9 @@ pub struct State {
 
 	/// Aggregated reputation change
 	reputation: ReputationAggregator,
+
+	/// Slot duration in millis
+	slot_duration_millis: u64,
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -488,11 +506,17 @@ struct BlockEntry {
 	knowledge: Knowledge,
 	/// A votes entry for each candidate indexed by [`CandidateIndex`].
 	candidates: Vec<CandidateEntry>,
+	/// Information about candidate metadata.
+	candidates_metadata: Vec<(CandidateHash, CoreIndex, GroupIndex)>,
 	/// The session index of this block.
 	session: SessionIndex,
 	/// Approval entries for whole block. These also contain all approvals in the case of multiple
 	/// candidates being claimed by assignments.
 	approval_entries: HashMap<(ValidatorIndex, CandidateBitfield), ApprovalEntry>,
+	/// The block vrf story.
+	vrf_story: RelayVRFStory,
+	/// The block slot.
+	slot: Slot,
 }
 
 impl BlockEntry {
@@ -646,6 +670,41 @@ enum MessageSource {
 	Local,
 }
 
+// Encountered error while validating an assignment.
+#[derive(Debug)]
+enum InvalidAssignmentError {
+	// The vrf check for the assignment failed.
+	#[allow(dead_code)]
+	CryptoCheckFailed(InvalidAssignment),
+	// The assignment did not claim any valid candidate.
+	NoClaimedCandidates,
+	// Claimed invalid candidate.
+	#[allow(dead_code)]
+	ClaimedInvalidCandidateIndex {
+		claimed_index: usize,
+		max_index: usize,
+	},
+	// The assignment claimes more candidates than the maximum allowed.
+	OversizedClaimedBitfield,
+	// `SessionInfo`  was not found for the block hash in the assignment.
+	#[allow(dead_code)]
+	SessionInfoNotFound(polkadot_node_subsystem_util::runtime::Error),
+}
+
+// Encountered error while validating an approval.
+#[derive(Debug)]
+enum InvalidVoteError {
+	// The candidate index was out of bounds.
+	CandidateIndexOutOfBounds,
+	// The validator index was out of bounds.
+	ValidatorIndexOutOfBounds,
+	// The signature of the vote was invalid.
+	InvalidSignature,
+	// `SessionInfo` was not found for the block hash in the approval.
+	#[allow(dead_code)]
+	SessionInfoNotFound(polkadot_node_subsystem_util::runtime::Error),
+}
+
 impl MessageSource {
 	fn peer_id(&self) -> Option<PeerId> {
 		match self {
@@ -662,16 +721,26 @@ enum PendingMessage {
 
 #[overseer::contextbounds(ApprovalDistribution, prefix = self::overseer)]
 impl State {
+	/// Build State with specified slot duration.
+	pub fn with_config(slot_duration_millis: u64) -> Self {
+		Self { slot_duration_millis, ..Default::default() }
+	}
+
 	async fn handle_network_msg<
 		N: overseer::SubsystemSender<NetworkBridgeTxMessage>,
 		A: overseer::SubsystemSender<ApprovalVotingMessage>,
+		RA: overseer::SubsystemSender<RuntimeApiMessage>,
 	>(
 		&mut self,
 		approval_voting_sender: &mut A,
 		network_sender: &mut N,
+		runtime_api_sender: &mut RA,
 		metrics: &Metrics,
 		event: NetworkBridgeEvent<net_protocol::ApprovalDistributionMessage>,
 		rng: &mut (impl CryptoRng + Rng),
+		assignment_criteria: &(impl AssignmentCriteria + ?Sized),
+		clock: &(impl Clock + ?Sized),
+		session_info_provider: &mut RuntimeInfo,
 	) {
 		match event {
 			NetworkBridgeEvent::PeerConnected(peer_id, role, version, authority_ids) => {
@@ -727,10 +796,14 @@ impl State {
 				self.process_incoming_peer_message(
 					approval_voting_sender,
 					network_sender,
+					runtime_api_sender,
 					metrics,
 					peer_id,
 					message,
 					rng,
+					assignment_criteria,
+					clock,
+					session_info_provider,
 				)
 				.await;
 			},
@@ -776,16 +849,28 @@ impl State {
 	async fn handle_new_blocks<
 		N: overseer::SubsystemSender<NetworkBridgeTxMessage>,
 		A: overseer::SubsystemSender<ApprovalVotingMessage>,
+		RA: overseer::SubsystemSender<RuntimeApiMessage>,
 	>(
 		&mut self,
 		approval_voting_sender: &mut A,
 		network_sender: &mut N,
+		runtime_api_sender: &mut RA,
 		metrics: &Metrics,
 		metas: Vec<BlockApprovalMeta>,
 		rng: &mut (impl CryptoRng + Rng),
+		assignment_criteria: &(impl AssignmentCriteria + ?Sized),
+		clock: &(impl Clock + ?Sized),
+		session_info_provider: &mut RuntimeInfo,
 	) {
 		let mut new_hashes = HashSet::new();
-		for meta in &metas {
+
+		gum::debug!(
+			target: LOG_TARGET,
+			"Got new blocks {:?}",
+			metas.iter().map(|m| (m.hash, m.number)).collect::<Vec<_>>(),
+		);
+
+		for meta in metas {
 			let mut span = self
 				.spans
 				.get(&meta.hash)
@@ -809,6 +894,9 @@ impl State {
 						candidates,
 						session: meta.session,
 						approval_entries: HashMap::new(),
+						candidates_metadata: meta.candidates,
+						vrf_story: meta.vrf_story,
+						slot: meta.slot,
 					});
 
 					self.topologies.inc_session_refs(meta.session);
@@ -823,12 +911,6 @@ impl State {
 			}
 		}
 
-		gum::debug!(
-			target: LOG_TARGET,
-			"Got new blocks {:?}",
-			metas.iter().map(|m| (m.hash, m.number)).collect::<Vec<_>>(),
-		);
-
 		{
 			for (peer_id, PeerEntry { view, version }) in self.peer_views.iter() {
 				let intersection = view.iter().filter(|h| new_hashes.contains(h));
@@ -883,11 +965,15 @@ impl State {
 							self.import_and_circulate_assignment(
 								approval_voting_sender,
 								network_sender,
+								runtime_api_sender,
 								metrics,
 								MessageSource::Peer(peer_id),
 								assignment,
 								claimed_indices,
 								rng,
+								assignment_criteria,
+								clock,
+								session_info_provider,
 							)
 							.await;
 						},
@@ -895,9 +981,11 @@ impl State {
 							self.import_and_circulate_approval(
 								approval_voting_sender,
 								network_sender,
+								runtime_api_sender,
 								metrics,
 								MessageSource::Peer(peer_id),
 								approval_vote,
+								session_info_provider,
 							)
 							.await;
 						},
@@ -943,17 +1031,22 @@ impl State {
 		.await;
 	}
 
-	async fn process_incoming_assignments<A, N, R>(
+	async fn process_incoming_assignments<A, N, R, RA>(
 		&mut self,
 		approval_voting_sender: &mut A,
 		network_sender: &mut N,
+		runtime_api_sender: &mut RA,
 		metrics: &Metrics,
 		peer_id: PeerId,
 		assignments: Vec<(IndirectAssignmentCertV2, CandidateBitfield)>,
 		rng: &mut R,
+		assignment_criteria: &(impl AssignmentCriteria + ?Sized),
+		clock: &(impl Clock + ?Sized),
+		session_info_provider: &mut RuntimeInfo,
 	) where
 		A: overseer::SubsystemSender<ApprovalVotingMessage>,
 		N: overseer::SubsystemSender<NetworkBridgeTxMessage>,
+		RA: overseer::SubsystemSender<RuntimeApiMessage>,
 		R: CryptoRng + Rng,
 	{
 		for (assignment, claimed_indices) in assignments {
@@ -978,11 +1071,15 @@ impl State {
 			self.import_and_circulate_assignment(
 				approval_voting_sender,
 				network_sender,
+				runtime_api_sender,
 				metrics,
 				MessageSource::Peer(peer_id),
 				assignment,
 				claimed_indices,
 				rng,
+				assignment_criteria,
+				clock,
+				session_info_provider,
 			)
 			.await;
 		}
@@ -992,13 +1089,16 @@ impl State {
 	async fn process_incoming_approvals<
 		N: overseer::SubsystemSender<NetworkBridgeTxMessage>,
 		A: overseer::SubsystemSender<ApprovalVotingMessage>,
+		RA: overseer::SubsystemSender<RuntimeApiMessage>,
 	>(
 		&mut self,
 		approval_voting_sender: &mut A,
 		network_sender: &mut N,
+		runtime_api_sender: &mut RA,
 		metrics: &Metrics,
 		peer_id: PeerId,
 		approvals: Vec<IndirectSignedApprovalVoteV2>,
+		session_info_provider: &mut RuntimeInfo,
 	) {
 		gum::trace!(
 			target: LOG_TARGET,
@@ -1028,18 +1128,21 @@ impl State {
 			self.import_and_circulate_approval(
 				approval_voting_sender,
 				network_sender,
+				runtime_api_sender,
 				metrics,
 				MessageSource::Peer(peer_id),
 				approval_vote,
+				session_info_provider,
 			)
 			.await;
 		}
 	}
 
-	async fn process_incoming_peer_message<A, N, R>(
+	async fn process_incoming_peer_message<A, N, RA, R>(
 		&mut self,
 		approval_voting_sender: &mut A,
 		network_sender: &mut N,
+		runtime_api_sender: &mut RA,
 		metrics: &Metrics,
 		peer_id: PeerId,
 		msg: Versioned<
@@ -1048,9 +1151,13 @@ impl State {
 			protocol_v3::ApprovalDistributionMessage,
 		>,
 		rng: &mut R,
+		assignment_criteria: &(impl AssignmentCriteria + ?Sized),
+		clock: &(impl Clock + ?Sized),
+		session_info_provider: &mut RuntimeInfo,
 	) where
 		A: overseer::SubsystemSender<ApprovalVotingMessage>,
 		N: overseer::SubsystemSender<NetworkBridgeTxMessage>,
+		RA: overseer::SubsystemSender<RuntimeApiMessage>,
 		R: CryptoRng + Rng,
 	{
 		match msg {
@@ -1067,10 +1174,14 @@ impl State {
 				self.process_incoming_assignments(
 					approval_voting_sender,
 					network_sender,
+					runtime_api_sender,
 					metrics,
 					peer_id,
 					sanitized_assignments,
 					rng,
+					assignment_criteria,
+					clock,
+					session_info_provider,
 				)
 				.await;
 			},
@@ -1089,10 +1200,14 @@ impl State {
 				self.process_incoming_assignments(
 					approval_voting_sender,
 					network_sender,
+					runtime_api_sender,
 					metrics,
 					peer_id,
 					sanitized_assignments,
 					rng,
+					assignment_criteria,
+					clock,
+					session_info_provider,
 				)
 				.await;
 			},
@@ -1102,9 +1217,11 @@ impl State {
 				self.process_incoming_approvals(
 					approval_voting_sender,
 					network_sender,
+					runtime_api_sender,
 					metrics,
 					peer_id,
 					sanitized_approvals,
+					session_info_provider,
 				)
 				.await;
 			},
@@ -1115,9 +1232,11 @@ impl State {
 				self.process_incoming_approvals(
 					approval_voting_sender,
 					network_sender,
+					runtime_api_sender,
 					metrics,
 					peer_id,
 					sanitized_approvals,
+					session_info_provider,
 				)
 				.await;
 			},
@@ -1218,18 +1337,23 @@ impl State {
 		self.enable_aggression(network_sender, Resend::No, metrics).await;
 	}
 
-	async fn import_and_circulate_assignment<A, N, R>(
+	async fn import_and_circulate_assignment<A, N, RA, R>(
 		&mut self,
 		approval_voting_sender: &mut A,
 		network_sender: &mut N,
+		runtime_api_sender: &mut RA,
 		metrics: &Metrics,
 		source: MessageSource,
 		assignment: IndirectAssignmentCertV2,
 		claimed_candidate_indices: CandidateBitfield,
 		rng: &mut R,
+		assignment_criteria: &(impl AssignmentCriteria + ?Sized),
+		clock: &(impl Clock + ?Sized),
+		session_info_provider: &mut RuntimeInfo,
 	) where
 		A: overseer::SubsystemSender<ApprovalVotingMessage>,
 		N: overseer::SubsystemSender<NetworkBridgeTxMessage>,
+		RA: overseer::SubsystemSender<RuntimeApiMessage>,
 		R: CryptoRng + Rng,
 	{
 		let _span = self
@@ -1355,35 +1479,47 @@ impl State {
 				return
 			}
 
-			let (tx, rx) = oneshot::channel();
+			let result = Self::check_assignment_valid(
+				assignment_criteria,
+				&entry,
+				&assignment,
+				&claimed_candidate_indices,
+				session_info_provider,
+				runtime_api_sender,
+			)
+			.await;
 
-			approval_voting_sender
-				.send_message(ApprovalVotingMessage::CheckAndImportAssignment(
-					assignment.clone(),
-					claimed_candidate_indices.clone(),
-					tx,
-				))
-				.await;
+			match result {
+				Ok(checked_assignment) => {
+					let current_tranche = clock.tranche_now(self.slot_duration_millis, entry.slot);
+					let too_far_in_future =
+						current_tranche + TICK_TOO_FAR_IN_FUTURE as DelayTranche;
 
-			let timer = metrics.time_awaiting_approval_voting();
-			let result = match rx.await {
-				Ok(result) => result,
-				Err(_) => {
-					gum::debug!(target: LOG_TARGET, "The approval voting subsystem is down");
-					return
-				},
-			};
-			drop(timer);
+					if checked_assignment.tranche() >= too_far_in_future {
+						gum::debug!(
+							target: LOG_TARGET,
+							hash = ?block_hash,
+							?peer_id,
+							"Got an assignment too far in the future",
+						);
+						modify_reputation(
+							&mut self.reputation,
+							network_sender,
+							peer_id,
+							COST_ASSIGNMENT_TOO_FAR_IN_THE_FUTURE,
+						)
+						.await;
+						metrics.on_assignment_far();
 
-			gum::trace!(
-				target: LOG_TARGET,
-				?source,
-				?message_subject,
-				?result,
-				"Checked assignment",
-			);
-			match result {
-				AssignmentCheckResult::Accepted => {
+						return
+					}
+
+					approval_voting_sender
+						.send_message(ApprovalVotingMessage::ImportAssignment(
+							checked_assignment,
+							None,
+						))
+						.await;
 					modify_reputation(
 						&mut self.reputation,
 						network_sender,
@@ -1396,47 +1532,12 @@ impl State {
 						peer_knowledge.received.insert(message_subject.clone(), message_kind);
 					}
 				},
-				AssignmentCheckResult::AcceptedDuplicate => {
-					// "duplicate" assignments aren't necessarily equal.
-					// There is more than one way each validator can be assigned to each core.
-					// cf. https://github.com/paritytech/polkadot/pull/2160#discussion_r557628699
-					if let Some(peer_knowledge) = entry.known_by.get_mut(&peer_id) {
-						peer_knowledge.received.insert(message_subject.clone(), message_kind);
-					}
-					gum::debug!(
-						target: LOG_TARGET,
-						hash = ?block_hash,
-						?peer_id,
-						"Got an `AcceptedDuplicate` assignment",
-					);
-					metrics.on_assignment_duplicatevoting();
-
-					return
-				},
-				AssignmentCheckResult::TooFarInFuture => {
-					gum::debug!(
-						target: LOG_TARGET,
-						hash = ?block_hash,
-						?peer_id,
-						"Got an assignment too far in the future",
-					);
-					modify_reputation(
-						&mut self.reputation,
-						network_sender,
-						peer_id,
-						COST_ASSIGNMENT_TOO_FAR_IN_THE_FUTURE,
-					)
-					.await;
-					metrics.on_assignment_far();
-
-					return
-				},
-				AssignmentCheckResult::Bad(error) => {
+				Err(error) => {
 					gum::info!(
 						target: LOG_TARGET,
 						hash = ?block_hash,
 						?peer_id,
-						%error,
+						?error,
 						"Got a bad assignment from peer",
 					);
 					modify_reputation(
@@ -1577,6 +1678,64 @@ impl State {
 		}
 	}
 
+	async fn check_assignment_valid<RA: overseer::SubsystemSender<RuntimeApiMessage>>(
+		assignment_criteria: &(impl AssignmentCriteria + ?Sized),
+		entry: &BlockEntry,
+		assignment: &IndirectAssignmentCertV2,
+		claimed_candidate_indices: &CandidateBitfield,
+		runtime_info: &mut RuntimeInfo,
+		runtime_api_sender: &mut RA,
+	) -> Result<CheckedIndirectAssignment, InvalidAssignmentError> {
+		let ExtendedSessionInfo { ref session_info, .. } = runtime_info
+			.get_session_info_by_index(runtime_api_sender, assignment.block_hash, entry.session)
+			.await
+			.map_err(|err| InvalidAssignmentError::SessionInfoNotFound(err))?;
+
+		if claimed_candidate_indices.len() > session_info.n_cores as usize {
+			return Err(InvalidAssignmentError::OversizedClaimedBitfield)
+		}
+
+		let claimed_cores: Vec<CoreIndex> = claimed_candidate_indices
+			.iter_ones()
+			.map(|candidate_index| {
+				entry.candidates_metadata.get(candidate_index).map(|(_, core, _)| *core).ok_or(
+					InvalidAssignmentError::ClaimedInvalidCandidateIndex {
+						claimed_index: candidate_index,
+						max_index: entry.candidates_metadata.len(),
+					},
+				)
+			})
+			.collect::<Result<Vec<_>, InvalidAssignmentError>>()?;
+
+		let Ok(claimed_cores) = claimed_cores.try_into() else {
+			return Err(InvalidAssignmentError::NoClaimedCandidates)
+		};
+
+		let backing_groups = claimed_candidate_indices
+			.iter_ones()
+			.flat_map(|candidate_index| {
+				entry.candidates_metadata.get(candidate_index).map(|(_, _, group)| *group)
+			})
+			.collect::<Vec<_>>();
+
+		assignment_criteria
+			.check_assignment_cert(
+				claimed_cores,
+				assignment.validator,
+				&polkadot_node_primitives::approval::criteria::Config::from(session_info),
+				entry.vrf_story.clone(),
+				&assignment.cert,
+				backing_groups,
+			)
+			.map_err(|err| InvalidAssignmentError::CryptoCheckFailed(err))
+			.map(|tranche| {
+				CheckedIndirectAssignment::from_checked(
+					assignment.clone(),
+					claimed_candidate_indices.clone(),
+					tranche,
+				)
+			})
+	}
 	// Checks if an approval can be processed.
 	// Returns true if we can continue with processing the approval and false otherwise.
 	async fn check_approval_can_be_processed<
@@ -1666,13 +1825,16 @@ impl State {
 	async fn import_and_circulate_approval<
 		N: overseer::SubsystemSender<NetworkBridgeTxMessage>,
 		A: overseer::SubsystemSender<ApprovalVotingMessage>,
+		RA: overseer::SubsystemSender<RuntimeApiMessage>,
 	>(
 		&mut self,
 		approval_voting_sender: &mut A,
 		network_sender: &mut N,
+		runtime_api_sender: &mut RA,
 		metrics: &Metrics,
 		source: MessageSource,
 		vote: IndirectSignedApprovalVoteV2,
+		session_info_provider: &mut RuntimeInfo,
 	) {
 		let _span = self
 			.spans
@@ -1740,31 +1902,16 @@ impl State {
 				return
 			}
 
-			let (tx, rx) = oneshot::channel();
-
-			approval_voting_sender
-				.send_message(ApprovalVotingMessage::CheckAndImportApproval(vote.clone(), tx))
-				.await;
-
-			let timer = metrics.time_awaiting_approval_voting();
-			let result = match rx.await {
-				Ok(result) => result,
-				Err(_) => {
-					gum::debug!(target: LOG_TARGET, "The approval voting subsystem is down");
-					return
-				},
-			};
-			drop(timer);
+			let result =
+				Self::check_vote_valid(&vote, &entry, session_info_provider, runtime_api_sender)
+					.await;
 
-			gum::trace!(
-				target: LOG_TARGET,
-				?peer_id,
-				?result,
-				?vote,
-				"Checked approval",
-			);
 			match result {
-				ApprovalCheckResult::Accepted => {
+				Ok(vote) => {
+					approval_voting_sender
+						.send_message(ApprovalVotingMessage::ImportApproval(vote, None))
+						.await;
+
 					modify_reputation(
 						&mut self.reputation,
 						network_sender,
@@ -1782,7 +1929,7 @@ impl State {
 							.insert(approval_knwowledge_key.0.clone(), approval_knwowledge_key.1);
 					}
 				},
-				ApprovalCheckResult::Bad(error) => {
+				Err(err) => {
 					modify_reputation(
 						&mut self.reputation,
 						network_sender,
@@ -1790,10 +1937,11 @@ impl State {
 						COST_INVALID_MESSAGE,
 					)
 					.await;
+
 					gum::info!(
 						target: LOG_TARGET,
 						?peer_id,
-						%error,
+						?err,
 						"Got a bad approval from peer",
 					);
 					metrics.on_approval_bad();
@@ -1891,6 +2039,50 @@ impl State {
 		}
 	}
 
+	// Checks if the approval vote is valid.
+	async fn check_vote_valid<RA: overseer::SubsystemSender<RuntimeApiMessage>>(
+		vote: &IndirectSignedApprovalVoteV2,
+		entry: &BlockEntry,
+		runtime_info: &mut RuntimeInfo,
+		runtime_api_sender: &mut RA,
+	) -> Result<CheckedIndirectSignedApprovalVote, InvalidVoteError> {
+		if vote.candidate_indices.len() > entry.candidates_metadata.len() {
+			return Err(InvalidVoteError::CandidateIndexOutOfBounds)
+		}
+
+		let candidate_hashes = vote
+			.candidate_indices
+			.iter_ones()
+			.flat_map(|candidate_index| {
+				entry
+					.candidates_metadata
+					.get(candidate_index)
+					.map(|(candidate_hash, _, _)| *candidate_hash)
+			})
+			.collect::<Vec<_>>();
+
+		let ExtendedSessionInfo { ref session_info, .. } = runtime_info
+			.get_session_info_by_index(runtime_api_sender, vote.block_hash, entry.session)
+			.await
+			.map_err(|err| InvalidVoteError::SessionInfoNotFound(err))?;
+
+		let pubkey = session_info
+			.validators
+			.get(vote.validator)
+			.ok_or(InvalidVoteError::ValidatorIndexOutOfBounds)?;
+		DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalCheckingMultipleCandidates(
+			candidate_hashes.clone(),
+		))
+		.check_signature(
+			&pubkey,
+			*candidate_hashes.first().unwrap(),
+			entry.session,
+			&vote.signature,
+		)
+		.map_err(|_| InvalidVoteError::InvalidSignature)
+		.map(|_| CheckedIndirectSignedApprovalVote::from_checked(vote.clone()))
+	}
+
 	/// Retrieve approval signatures from state for the given relay block/indices:
 	fn get_approval_signatures(
 		&mut self,
@@ -2468,16 +2660,48 @@ async fn modify_reputation(
 #[overseer::contextbounds(ApprovalDistribution, prefix = self::overseer)]
 impl ApprovalDistribution {
 	/// Create a new instance of the [`ApprovalDistribution`] subsystem.
-	pub fn new(metrics: Metrics) -> Self {
-		Self { metrics }
+	pub fn new(
+		metrics: Metrics,
+		slot_duration_millis: u64,
+		assignment_criteria: Arc<dyn AssignmentCriteria + Send + Sync>,
+	) -> Self {
+		Self::new_with_clock(
+			metrics,
+			slot_duration_millis,
+			Box::new(SystemClock),
+			assignment_criteria,
+		)
+	}
+
+	/// Create a new instance of the [`ApprovalDistribution`] subsystem, with a custom clock.
+	pub fn new_with_clock(
+		metrics: Metrics,
+		slot_duration_millis: u64,
+		clock: Box<dyn Clock + Send + Sync>,
+		assignment_criteria: Arc<dyn AssignmentCriteria + Send + Sync>,
+	) -> Self {
+		Self { metrics, slot_duration_millis, clock, assignment_criteria }
 	}
 
 	async fn run<Context>(self, ctx: Context) {
-		let mut state = State::default();
+		let mut state =
+			State { slot_duration_millis: self.slot_duration_millis, ..Default::default() };
 		// According to the docs of `rand`, this is a ChaCha12 RNG in practice
 		// and will always be chosen for strong performance and security properties.
 		let mut rng = rand::rngs::StdRng::from_entropy();
-		self.run_inner(ctx, &mut state, REPUTATION_CHANGE_INTERVAL, &mut rng).await
+		let mut session_info_provider = RuntimeInfo::new_with_config(RuntimeInfoConfig {
+			keystore: None,
+			session_cache_lru_size: DISPUTE_WINDOW.get(),
+		});
+
+		self.run_inner(
+			ctx,
+			&mut state,
+			REPUTATION_CHANGE_INTERVAL,
+			&mut rng,
+			&mut session_info_provider,
+		)
+		.await
 	}
 
 	/// Used for testing.
@@ -2487,11 +2711,14 @@ impl ApprovalDistribution {
 		state: &mut State,
 		reputation_interval: Duration,
 		rng: &mut (impl CryptoRng + Rng),
+		session_info_provider: &mut RuntimeInfo,
 	) {
 		let new_reputation_delay = || futures_timer::Delay::new(reputation_interval).fuse();
 		let mut reputation_delay = new_reputation_delay();
 		let mut approval_voting_sender = ctx.sender().clone();
 		let mut network_sender = ctx.sender().clone();
+		let mut runtime_api_sender = ctx.sender().clone();
+
 		loop {
 			select! {
 				_ = reputation_delay => {
@@ -2507,8 +2734,7 @@ impl ApprovalDistribution {
 						},
 					};
 
-
-					if self.handle_from_orchestra(message, &mut approval_voting_sender, &mut network_sender, state, rng).await {
+					if self.handle_from_orchestra(message, &mut approval_voting_sender, &mut network_sender, &mut runtime_api_sender, state, rng, session_info_provider).await {
 						return;
 					}
 
@@ -2523,23 +2749,30 @@ impl ApprovalDistribution {
 	pub async fn handle_from_orchestra<
 		N: overseer::SubsystemSender<NetworkBridgeTxMessage>,
 		A: overseer::SubsystemSender<ApprovalVotingMessage>,
+		RA: overseer::SubsystemSender<RuntimeApiMessage>,
 	>(
 		&self,
 		message: FromOrchestra<ApprovalDistributionMessage>,
 		approval_voting_sender: &mut A,
 		network_sender: &mut N,
+		runtime_api_sender: &mut RA,
 		state: &mut State,
 		rng: &mut (impl CryptoRng + Rng),
+		session_info_provider: &mut RuntimeInfo,
 	) -> bool {
 		match message {
 			FromOrchestra::Communication { msg } =>
 				Self::handle_incoming(
 					approval_voting_sender,
 					network_sender,
+					runtime_api_sender,
 					state,
 					msg,
 					&self.metrics,
 					rng,
+					self.assignment_criteria.as_ref(),
+					self.clock.as_ref(),
+					session_info_provider,
 				)
 				.await,
 			FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) => {
@@ -2567,23 +2800,48 @@ impl ApprovalDistribution {
 	async fn handle_incoming<
 		N: overseer::SubsystemSender<NetworkBridgeTxMessage>,
 		A: overseer::SubsystemSender<ApprovalVotingMessage>,
+		RA: overseer::SubsystemSender<RuntimeApiMessage>,
 	>(
 		approval_voting_sender: &mut A,
 		network_sender: &mut N,
+		runtime_api_sender: &mut RA,
 		state: &mut State,
 		msg: ApprovalDistributionMessage,
 		metrics: &Metrics,
 		rng: &mut (impl CryptoRng + Rng),
+		assignment_criteria: &(impl AssignmentCriteria + ?Sized),
+		clock: &(impl Clock + ?Sized),
+		session_info_provider: &mut RuntimeInfo,
 	) {
 		match msg {
 			ApprovalDistributionMessage::NetworkBridgeUpdate(event) => {
 				state
-					.handle_network_msg(approval_voting_sender, network_sender, metrics, event, rng)
+					.handle_network_msg(
+						approval_voting_sender,
+						network_sender,
+						runtime_api_sender,
+						metrics,
+						event,
+						rng,
+						assignment_criteria,
+						clock,
+						session_info_provider,
+					)
 					.await;
 			},
 			ApprovalDistributionMessage::NewBlocks(metas) => {
 				state
-					.handle_new_blocks(approval_voting_sender, network_sender, metrics, metas, rng)
+					.handle_new_blocks(
+						approval_voting_sender,
+						network_sender,
+						runtime_api_sender,
+						metrics,
+						metas,
+						rng,
+						assignment_criteria,
+						clock,
+						session_info_provider,
+					)
 					.await;
 			},
 			ApprovalDistributionMessage::DistributeAssignment(cert, candidate_indices) => {
@@ -2607,11 +2865,15 @@ impl ApprovalDistribution {
 					.import_and_circulate_assignment(
 						approval_voting_sender,
 						network_sender,
+						runtime_api_sender,
 						&metrics,
 						MessageSource::Local,
 						cert,
 						candidate_indices,
 						rng,
+						assignment_criteria,
+						clock,
+						session_info_provider,
 					)
 					.await;
 			},
@@ -2627,9 +2889,11 @@ impl ApprovalDistribution {
 					.import_and_circulate_approval(
 						approval_voting_sender,
 						network_sender,
+						runtime_api_sender,
 						metrics,
 						MessageSource::Local,
 						vote,
+						session_info_provider,
 					)
 					.await;
 			},
diff --git a/polkadot/node/network/approval-distribution/src/metrics.rs b/polkadot/node/network/approval-distribution/src/metrics.rs
index 60c7f2f6d3b..10553c35296 100644
--- a/polkadot/node/network/approval-distribution/src/metrics.rs
+++ b/polkadot/node/network/approval-distribution/src/metrics.rs
@@ -30,7 +30,6 @@ struct MetricsInner {
 	aggression_l2_messages_total: prometheus::Counter<prometheus::U64>,
 	time_unify_with_peer: prometheus::Histogram,
 	time_import_pending_now_known: prometheus::Histogram,
-	time_awaiting_approval_voting: prometheus::Histogram,
 	assignments_received_result: prometheus::CounterVec<prometheus::U64>,
 	approvals_received_result: prometheus::CounterVec<prometheus::U64>,
 }
@@ -206,14 +205,6 @@ impl Metrics {
 		}
 	}
 
-	pub(crate) fn time_awaiting_approval_voting(
-		&self,
-	) -> Option<prometheus::prometheus::HistogramTimer> {
-		self.0
-			.as_ref()
-			.map(|metrics| metrics.time_awaiting_approval_voting.start_timer())
-	}
-
 	pub(crate) fn on_aggression_l1(&self) {
 		if let Some(metrics) = &self.0 {
 			metrics.aggression_l1_messages_total.inc();
@@ -288,13 +279,6 @@ impl MetricsTrait for Metrics {
 				).buckets(vec![0.0001, 0.0004, 0.0016, 0.0064, 0.0256, 0.1024, 0.4096, 1.6384, 3.2768, 4.9152, 6.5536,]))?,
 				registry,
 			)?,
-			time_awaiting_approval_voting: prometheus::register(
-				prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
-					"polkadot_parachain_time_awaiting_approval_voting",
-					"Time spent awaiting a reply from the Approval Voting Subsystem.",
-				).buckets(vec![0.0001, 0.0004, 0.0016, 0.0064, 0.0256, 0.1024, 0.4096, 1.6384, 3.2768, 4.9152, 6.5536,]))?,
-				registry,
-			)?,
 			assignments_received_result: prometheus::register(
 				prometheus::CounterVec::new(
 					prometheus::Opts::new(
diff --git a/polkadot/node/network/approval-distribution/src/tests.rs b/polkadot/node/network/approval-distribution/src/tests.rs
index 1ca571721ea..4ee9320e0e4 100644
--- a/polkadot/node/network/approval-distribution/src/tests.rs
+++ b/polkadot/node/network/approval-distribution/src/tests.rs
@@ -16,7 +16,7 @@
 
 use super::*;
 use assert_matches::assert_matches;
-use futures::{executor, future, Future};
+use futures::{channel::oneshot, executor, future, Future};
 use polkadot_node_network_protocol::{
 	grid_topology::{SessionGridTopology, TopologyPeerInfo},
 	our_view,
@@ -24,6 +24,7 @@ use polkadot_node_network_protocol::{
 	view, ObservedRole,
 };
 use polkadot_node_primitives::approval::{
+	criteria,
 	v1::{
 		AssignmentCert, AssignmentCertKind, IndirectAssignmentCert, IndirectSignedApprovalVote,
 		VrfPreOutput, VrfProof, VrfSignature,
@@ -34,12 +35,17 @@ use polkadot_node_primitives::approval::{
 	},
 };
 use polkadot_node_subsystem::messages::{
-	network_bridge_event, AllMessages, ApprovalCheckError, ReportPeerMessage,
+	network_bridge_event, AllMessages, ReportPeerMessage, RuntimeApiRequest,
 };
 use polkadot_node_subsystem_util::{reputation::add_reputation, TimeoutExt as _};
-use polkadot_primitives::{AuthorityDiscoveryId, BlakeTwo256, CoreIndex, HashT};
+use polkadot_primitives::{
+	ApprovalVoteMultipleCandidates, AuthorityDiscoveryId, BlakeTwo256, CoreIndex, ExecutorParams,
+	HashT, NodeFeatures, SessionInfo, ValidatorId,
+};
 use polkadot_primitives_test_helpers::dummy_signature;
 use rand::SeedableRng;
+use sc_keystore::{Keystore, LocalKeystore};
+use sp_application_crypto::AppCrypto;
 use sp_authority_discovery::AuthorityPair as AuthorityDiscoveryPair;
 use sp_core::crypto::Pair as PairT;
 use std::time::Duration;
@@ -47,6 +53,8 @@ type VirtualOverseer =
 	polkadot_node_subsystem_test_helpers::TestSubsystemContextHandle<ApprovalDistributionMessage>;
 
 fn test_harness<T: Future<Output = VirtualOverseer>>(
+	assignment_criteria: Arc<dyn AssignmentCriteria + Send + Sync>,
+	clock: Box<dyn Clock + Send + Sync>,
 	mut state: State,
 	test_fn: impl FnOnce(VirtualOverseer) -> T,
 ) -> State {
@@ -56,13 +64,29 @@ fn test_harness<T: Future<Output = VirtualOverseer>>(
 	let (context, virtual_overseer) =
 		polkadot_node_subsystem_test_helpers::make_subsystem_context(pool.clone());
 
-	let subsystem = ApprovalDistribution::new(Default::default());
+	let subsystem = ApprovalDistribution::new_with_clock(
+		Metrics::default(),
+		Default::default(),
+		clock,
+		assignment_criteria,
+	);
 	{
 		let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(12345);
+		let mut session_info_provider = RuntimeInfo::new_with_config(RuntimeInfoConfig {
+			keystore: None,
+			session_cache_lru_size: DISPUTE_WINDOW.get(),
+		});
+
 		let (tx, rx) = oneshot::channel();
 		let subsystem = async {
 			subsystem
-				.run_inner(context, &mut state, REPUTATION_CHANGE_TEST_INTERVAL, &mut rng)
+				.run_inner(
+					context,
+					&mut state,
+					REPUTATION_CHANGE_TEST_INTERVAL,
+					&mut rng,
+					&mut session_info_provider,
+				)
 				.await;
 			tx.send(()).expect("Fail to notify subystem is done");
 		};
@@ -121,6 +145,41 @@ async fn overseer_recv(overseer: &mut VirtualOverseer) -> AllMessages {
 	msg
 }
 
+async fn provide_session(virtual_overseer: &mut VirtualOverseer, session_info: SessionInfo) {
+	assert_matches!(
+		overseer_recv(virtual_overseer).await,
+		AllMessages::RuntimeApi(
+			RuntimeApiMessage::Request(
+				_,
+				RuntimeApiRequest::SessionInfo(_, si_tx),
+			)
+		) => {
+			si_tx.send(Ok(Some(session_info.clone()))).unwrap();
+		}
+	);
+	assert_matches!(
+		overseer_recv(virtual_overseer).await,
+		AllMessages::RuntimeApi(
+			RuntimeApiMessage::Request(
+				_,
+				RuntimeApiRequest::SessionExecutorParams(_, si_tx),
+			)
+		) => {
+			// Make sure all SessionExecutorParams calls are not made for the leaf (but for its relay parent)
+			si_tx.send(Ok(Some(ExecutorParams::default()))).unwrap();
+		}
+	);
+
+	assert_matches!(
+		overseer_recv(virtual_overseer).await,
+		AllMessages::RuntimeApi(
+			RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+		) => {
+			si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+		}
+	);
+}
+
 fn make_peers_and_authority_ids(n: usize) -> Vec<(PeerId, AuthorityDiscoveryId)> {
 	(0..n)
 		.map(|_| {
@@ -335,6 +394,30 @@ fn fake_assignment_cert_v2(
 	}
 }
 
+fn fake_assignment_cert_delay(
+	block_hash: Hash,
+	validator: ValidatorIndex,
+	core_bitfield: CoreBitfield,
+) -> IndirectAssignmentCertV2 {
+	let ctx = schnorrkel::signing_context(RELAY_VRF_MODULO_CONTEXT);
+	let msg = b"WhenParachains?";
+	let mut prng = rand_core::OsRng;
+	let keypair = schnorrkel::Keypair::generate_with(&mut prng);
+	let (inout, proof, _) = keypair.vrf_sign(ctx.bytes(msg));
+	let preout = inout.to_preout();
+
+	IndirectAssignmentCertV2 {
+		block_hash,
+		validator,
+		cert: AssignmentCertV2 {
+			kind: AssignmentCertKindV2::RelayVRFDelay {
+				core_index: CoreIndex(core_bitfield.iter_ones().next().unwrap() as u32),
+			},
+			vrf: VrfSignature { pre_output: VrfPreOutput(preout), proof: VrfProof(proof) },
+		},
+	}
+}
+
 async fn expect_reputation_change(
 	virtual_overseer: &mut VirtualOverseer,
 	peer_id: &PeerId,
@@ -378,6 +461,85 @@ fn state_with_reputation_delay() -> State {
 	State { reputation: ReputationAggregator::new(|_| false), ..Default::default() }
 }
 
+fn dummy_session_info_valid(
+	index: SessionIndex,
+	keystore: &mut LocalKeystore,
+	num_validators: usize,
+) -> SessionInfo {
+	let keys = (0..num_validators)
+		.map(|_| {
+			keystore
+				.sr25519_generate_new(ValidatorId::ID, Some("//Node"))
+				.expect("Insert key into keystore")
+		})
+		.collect_vec();
+
+	SessionInfo {
+		validators: keys.clone().into_iter().map(|key| key.into()).collect(),
+		discovery_keys: keys.clone().into_iter().map(|key| key.into()).collect(),
+		assignment_keys: keys.clone().into_iter().map(|key| key.into()).collect(),
+		validator_groups: Default::default(),
+		n_cores: 20,
+		zeroth_delay_tranche_width: index as _,
+		relay_vrf_modulo_samples: index as _,
+		n_delay_tranches: index as _,
+		no_show_slots: index as _,
+		needed_approvals: index as _,
+		active_validator_indices: Vec::new(),
+		dispute_period: 6,
+		random_seed: [0u8; 32],
+	}
+}
+
+fn signature_for(
+	keystore: &LocalKeystore,
+	session: &SessionInfo,
+	candidate_hashes: Vec<CandidateHash>,
+	validator_index: ValidatorIndex,
+) -> ValidatorSignature {
+	let payload = ApprovalVoteMultipleCandidates(&candidate_hashes).signing_payload(1);
+	let sign_key = session.validators.get(validator_index).unwrap().clone();
+	let signature = keystore
+		.sr25519_sign(ValidatorId::ID, &sign_key.into(), &payload[..])
+		.unwrap()
+		.unwrap();
+	signature.into()
+}
+
+struct MockAssignmentCriteria {
+	tranche:
+		Result<polkadot_node_primitives::approval::v1::DelayTranche, criteria::InvalidAssignment>,
+}
+
+impl AssignmentCriteria for MockAssignmentCriteria {
+	fn compute_assignments(
+		&self,
+		_keystore: &LocalKeystore,
+		_relay_vrf_story: polkadot_node_primitives::approval::v1::RelayVRFStory,
+		_config: &criteria::Config,
+		_leaving_cores: Vec<(
+			CandidateHash,
+			polkadot_primitives::CoreIndex,
+			polkadot_primitives::GroupIndex,
+		)>,
+		_enable_assignments_v2: bool,
+	) -> HashMap<polkadot_primitives::CoreIndex, criteria::OurAssignment> {
+		HashMap::new()
+	}
+
+	fn check_assignment_cert(
+		&self,
+		_claimed_core_bitfield: polkadot_node_primitives::approval::v2::CoreBitfield,
+		_validator_index: polkadot_primitives::ValidatorIndex,
+		_config: &criteria::Config,
+		_relay_vrf_story: polkadot_node_primitives::approval::v1::RelayVRFStory,
+		_assignment: &polkadot_node_primitives::approval::v2::AssignmentCertV2,
+		_backing_groups: Vec<polkadot_primitives::GroupIndex>,
+	) -> Result<polkadot_node_primitives::approval::v1::DelayTranche, criteria::InvalidAssignment> {
+		self.tranche
+	}
+}
+
 /// import an assignment
 /// connect a new peer
 /// the new peer sends us the same assignment
@@ -391,89 +553,98 @@ fn try_import_the_same_assignment() {
 	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash = Hash::repeat_byte(0xAA);
 
-	let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// setup peers
-		setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await;
-		setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await;
-		setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V1).await;
-
-		// Set up a gossip topology, where a, b, c and d are topology neighbors to the node under
-		// testing.
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
-		)
-		.await;
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// setup peers
+			setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await;
+			setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await;
+			setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V1).await;
+
+			// Set up a gossip topology, where a, b, c and d are topology neighbors to the node
+			// under testing.
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
+			)
+			.await;
 
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 2,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 2,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			// send the assignment related to `hash`
+			let validator_index = ValidatorIndex(0);
+			let cert = fake_assignment_cert(hash, validator_index);
+			let assignments = vec![(cert.clone(), 0u32)];
 
-		// send the assignment related to `hash`
-		let validator_index = ValidatorIndex(0);
-		let cert = fake_assignment_cert(hash, validator_index);
-		let assignments = vec![(cert.clone(), 0u32)];
+			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+			send_message_from_peer(overseer, &peer_a, msg).await;
 
-		let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
-		send_message_from_peer(overseer, &peer_a, msg).await;
+			expect_reputation_change(overseer, &peer_a, COST_UNEXPECTED_MESSAGE).await;
+			provide_session(
+				overseer,
+				dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+			)
+			.await;
+			// send an `Accept` message from the Approval Voting subsystem
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment,
+					_,
+				)) => {
+					assert_eq!(assignment.candidate_indices(), &0u32.into());
+					assert_eq!(assignment.assignment(), &cert.into());
+					assert_eq!(assignment.tranche(), 0);
+				}
+			);
 
-		expect_reputation_change(overseer, &peer_a, COST_UNEXPECTED_MESSAGE).await;
+			expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await;
 
-		// send an `Accept` message from the Approval Voting subsystem
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-				assignment,
-				claimed_indices,
-				tx,
-			)) => {
-				assert_eq!(claimed_indices, 0u32.into());
-				assert_eq!(assignment, cert.into());
-				tx.send(AssignmentCheckResult::Accepted).unwrap();
-			}
-		);
-
-		expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert_eq!(peers.len(), 2);
-				assert_eq!(assignments.len(), 1);
-			}
-		);
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 2);
+					assert_eq!(assignments.len(), 1);
+				}
+			);
 
-		// setup new peer with V2
-		setup_peer_with_view(overseer, &peer_d, view![], ValidationVersion::V3).await;
+			// setup new peer with V2
+			setup_peer_with_view(overseer, &peer_d, view![], ValidationVersion::V3).await;
 
-		// send the same assignment from peer_d
-		let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments);
-		send_message_from_peer(overseer, &peer_d, msg).await;
+			// send the same assignment from peer_d
+			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments);
+			send_message_from_peer(overseer, &peer_d, msg).await;
 
-		expect_reputation_change(overseer, &peer_d, COST_UNEXPECTED_MESSAGE).await;
-		expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE).await;
+			expect_reputation_change(overseer, &peer_d, COST_UNEXPECTED_MESSAGE).await;
+			expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE).await;
 
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
-		virtual_overseer
-	});
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			virtual_overseer
+		},
+	);
 }
 
 /// Just like `try_import_the_same_assignment` but use `VRFModuloCompact` assignments for multiple
@@ -488,97 +659,106 @@ fn try_import_the_same_assignment_v2() {
 	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash = Hash::repeat_byte(0xAA);
 
-	let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// setup peers
-		setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V3).await;
-		setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await;
-		setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V3).await;
-
-		// Set up a gossip topology, where a, b, c and d are topology neighbors to the node under
-		// testing.
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
-		)
-		.await;
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// setup peers
+			setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V3).await;
+			setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await;
+			setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V3).await;
+
+			// Set up a gossip topology, where a, b, c and d are topology neighbors to the node
+			// under testing.
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
+			)
+			.await;
 
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 2,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		// send the assignment related to `hash`
-		let validator_index = ValidatorIndex(0);
-		let cores = vec![1, 2, 3, 4];
-		let core_bitfield: CoreBitfield = cores
-			.iter()
-			.map(|index| CoreIndex(*index))
-			.collect::<Vec<_>>()
-			.try_into()
-			.unwrap();
-
-		let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield.clone());
-		let assignments = vec![(cert.clone(), cores.clone().try_into().unwrap())];
-
-		let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone());
-		send_message_from_peer_v3(overseer, &peer_a, msg).await;
-
-		expect_reputation_change(overseer, &peer_a, COST_UNEXPECTED_MESSAGE).await;
-
-		// send an `Accept` message from the Approval Voting subsystem
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-				assignment,
-				claimed_indices,
-				tx,
-			)) => {
-				assert_eq!(claimed_indices, cores.try_into().unwrap());
-				assert_eq!(assignment, cert.into());
-				tx.send(AssignmentCheckResult::Accepted).unwrap();
-			}
-		);
-
-		expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
-					protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert_eq!(peers.len(), 2);
-				assert_eq!(assignments.len(), 1);
-			}
-		);
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 2,
+				candidates: vec![Default::default(); 5],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			// send the assignment related to `hash`
+			let validator_index = ValidatorIndex(0);
+			let cores = vec![1, 2, 3, 4];
+			let core_bitfield: CoreBitfield = cores
+				.iter()
+				.map(|index| CoreIndex(*index))
+				.collect::<Vec<_>>()
+				.try_into()
+				.unwrap();
+
+			let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield.clone());
+			let assignments = vec![(cert.clone(), cores.clone().try_into().unwrap())];
+
+			let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone());
+			send_message_from_peer_v3(overseer, &peer_a, msg).await;
+
+			expect_reputation_change(overseer, &peer_a, COST_UNEXPECTED_MESSAGE).await;
+			provide_session(
+				overseer,
+				dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+			)
+			.await;
+			// send an `Accept` message from the Approval Voting subsystem
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment,
+					_,
+				)) => {
+					assert_eq!(assignment.candidate_indices(), &cores.try_into().unwrap());
+					assert_eq!(assignment.assignment(), &cert.into());
+					assert_eq!(assignment.tranche(), 0);
+				}
+			);
+
+			expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 2);
+					assert_eq!(assignments.len(), 1);
+				}
+			);
 
-		// setup new peer
-		setup_peer_with_view(overseer, &peer_d, view![], ValidationVersion::V3).await;
+			// setup new peer
+			setup_peer_with_view(overseer, &peer_d, view![], ValidationVersion::V3).await;
 
-		// send the same assignment from peer_d
-		let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments);
-		send_message_from_peer_v3(overseer, &peer_d, msg).await;
+			// send the same assignment from peer_d
+			let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments);
+			send_message_from_peer_v3(overseer, &peer_d, msg).await;
 
-		expect_reputation_change(overseer, &peer_d, COST_UNEXPECTED_MESSAGE).await;
-		expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE).await;
+			expect_reputation_change(overseer, &peer_d, COST_UNEXPECTED_MESSAGE).await;
+			expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE).await;
 
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
-		virtual_overseer
-	});
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			virtual_overseer
+		},
+	);
 }
 
 /// import an assignment
@@ -590,55 +770,65 @@ fn delay_reputation_change() {
 	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash = Hash::repeat_byte(0xAA);
 
-	let _ = test_harness(state_with_reputation_delay(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-
-		// Setup peers
-		setup_peer_with_view(overseer, &peer, view![], ValidationVersion::V1).await;
-
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 2,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		// send the assignment related to `hash`
-		let validator_index = ValidatorIndex(0);
-		let cert = fake_assignment_cert(hash, validator_index);
-		let assignments = vec![(cert.clone(), 0u32)];
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state_with_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+
+			// Setup peers
+			setup_peer_with_view(overseer, &peer, view![], ValidationVersion::V1).await;
+
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 2,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			// send the assignment related to `hash`
+			let validator_index = ValidatorIndex(0);
+			let cert = fake_assignment_cert(hash, validator_index);
+			let assignments = vec![(cert.clone(), 0u32)];
 
-		let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
-		send_message_from_peer(overseer, &peer, msg).await;
+			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+			send_message_from_peer(overseer, &peer, msg).await;
+			provide_session(
+				overseer,
+				dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+			)
+			.await;
 
-		// send an `Accept` message from the Approval Voting subsystem
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-				assignment,
-				claimed_candidates,
-				tx,
-			)) => {
-				assert_eq!(assignment.cert, cert.cert.into());
-				assert_eq!(claimed_candidates, vec![0u32].try_into().unwrap());
-				tx.send(AssignmentCheckResult::Accepted).unwrap();
-			}
-		);
-		expect_reputation_changes(
-			overseer,
-			&peer,
-			vec![COST_UNEXPECTED_MESSAGE, BENEFIT_VALID_MESSAGE_FIRST],
-		)
-		.await;
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			// send an `Accept` message from the Approval Voting subsystem
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment,
+					_,
+				)) => {
+					assert_eq!(assignment.assignment().cert, cert.cert.into());
+					assert_eq!(assignment.candidate_indices(), &vec![0u32].try_into().unwrap());
+					assert_eq!(assignment.tranche(), 0);
+				}
+			);
+			expect_reputation_changes(
+				overseer,
+				&peer,
+				vec![COST_UNEXPECTED_MESSAGE, BENEFIT_VALID_MESSAGE_FIRST],
+			)
+			.await;
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
 
-		virtual_overseer
-	});
+			virtual_overseer
+		},
+	);
 }
 
 /// <https://github.com/paritytech/polkadot/pull/2160#discussion_r547594835>
@@ -653,77 +843,88 @@ fn spam_attack_results_in_negative_reputation_change() {
 	let peer_a = PeerId::random();
 	let hash_b = Hash::repeat_byte(0xBB);
 
-	let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		let peer = &peer_a;
-		setup_peer_with_view(overseer, peer, view![], ValidationVersion::V1).await;
-
-		// new block `hash_b` with 20 candidates
-		let candidates_count = 20;
-		let meta = BlockApprovalMeta {
-			hash: hash_b,
-			parent_hash,
-			number: 2,
-			candidates: vec![Default::default(); candidates_count],
-			slot: 1.into(),
-			session: 1,
-		};
-
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		// send 20 assignments related to `hash_b`
-		// to populate our knowledge
-		let assignments: Vec<_> = (0..candidates_count)
-			.map(|candidate_index| {
-				let validator_index = ValidatorIndex(candidate_index as u32);
-				let cert = fake_assignment_cert(hash_b, validator_index);
-				(cert, candidate_index as u32)
-			})
-			.collect();
-
-		let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
-		send_message_from_peer(overseer, peer, msg.clone()).await;
-
-		for i in 0..candidates_count {
-			expect_reputation_change(overseer, peer, COST_UNEXPECTED_MESSAGE).await;
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			let peer = &peer_a;
+			setup_peer_with_view(overseer, peer, view![], ValidationVersion::V1).await;
+
+			// new block `hash_b` with 20 candidates
+			let candidates_count = 20;
+			let meta = BlockApprovalMeta {
+				hash: hash_b,
+				parent_hash,
+				number: 2,
+				candidates: vec![Default::default(); candidates_count],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			// send 20 assignments related to `hash_b`
+			// to populate our knowledge
+			let assignments: Vec<_> = (0..candidates_count)
+				.map(|candidate_index| {
+					let validator_index = ValidatorIndex(candidate_index as u32);
+					let cert = fake_assignment_cert(hash_b, validator_index);
+					(cert, candidate_index as u32)
+				})
+				.collect();
 
-			assert_matches!(
-				overseer_recv(overseer).await,
-				AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-					assignment,
-					claimed_candidate_index,
-					tx,
-				)) => {
-					assert_eq!(assignment, assignments[i].0.clone().into());
-					assert_eq!(claimed_candidate_index, assignments[i].1.into());
-					tx.send(AssignmentCheckResult::Accepted).unwrap();
+			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+			send_message_from_peer(overseer, peer, msg.clone()).await;
+
+			for i in 0..candidates_count {
+				expect_reputation_change(overseer, peer, COST_UNEXPECTED_MESSAGE).await;
+				if i == 0 {
+					provide_session(
+						overseer,
+						dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+					)
+					.await;
 				}
-			);
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+						assignment,
+						_,
+					)) => {
+						assert_eq!(assignment.assignment(), &assignments[i].0.clone().into());
+						assert_eq!(assignment.candidate_indices(), &assignments[i].1.into());
+						assert_eq!(assignment.tranche(), 0);
+					}
+				);
 
-			expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await;
-		}
+				expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await;
+			}
 
-		// send a view update that removes block B from peer's view by bumping the finalized_number
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::PeerViewChange(
-				*peer,
-				View::with_finalized(2),
-			)),
-		)
-		.await;
+			// send a view update that removes block B from peer's view by bumping the
+			// finalized_number
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::NetworkBridgeUpdate(
+					NetworkBridgeEvent::PeerViewChange(*peer, View::with_finalized(2)),
+				),
+			)
+			.await;
 
-		// send the assignments again
-		send_message_from_peer(overseer, peer, msg.clone()).await;
+			// send the assignments again
+			send_message_from_peer(overseer, peer, msg.clone()).await;
 
-		// each of them will incur `COST_UNEXPECTED_MESSAGE`, not only the first one
-		for _ in 0..candidates_count {
-			expect_reputation_change(overseer, peer, COST_UNEXPECTED_MESSAGE).await;
-			expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE).await;
-		}
-		virtual_overseer
-	});
+			// each of them will incur `COST_UNEXPECTED_MESSAGE`, not only the first one
+			for _ in 0..candidates_count {
+				expect_reputation_change(overseer, peer, COST_UNEXPECTED_MESSAGE).await;
+				expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE).await;
+			}
+			virtual_overseer
+		},
+	);
 }
 
 /// Imagine we send a message to peer A and peer B.
@@ -739,86 +940,94 @@ fn peer_sending_us_the_same_we_just_sent_them_is_ok() {
 	let peers = make_peers_and_authority_ids(8);
 	let peer_a = peers.first().unwrap().0;
 
-	let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		let peer = &peer_a;
-		setup_peer_with_view(overseer, peer, view![], ValidationVersion::V1).await;
-
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Setup a topology where peer_a is neighbor to current node.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1),
-		)
-		.await;
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			let peer = &peer_a;
+			setup_peer_with_view(overseer, peer, view![], ValidationVersion::V1).await;
+
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Setup a topology where peer_a is neighbor to current node.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1),
+			)
+			.await;
 
-		// new block `hash` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		// import an assignment related to `hash` locally
-		let validator_index = ValidatorIndex(0);
-		let candidate_index = 0u32;
-		let cert = fake_assignment_cert(hash, validator_index);
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(
-				cert.clone().into(),
-				candidate_index.into(),
-			),
-		)
-		.await;
+			// new block `hash` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			// import an assignment related to `hash` locally
+			let validator_index = ValidatorIndex(0);
+			let candidate_index = 0u32;
+			let cert = fake_assignment_cert(hash, validator_index);
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(
+					cert.clone().into(),
+					candidate_index.into(),
+				),
+			)
+			.await;
 
-		// update peer view to include the hash
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::PeerViewChange(
-				*peer,
-				view![hash],
-			)),
-		)
-		.await;
+			// update peer view to include the hash
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::NetworkBridgeUpdate(
+					NetworkBridgeEvent::PeerViewChange(*peer, view![hash]),
+				),
+			)
+			.await;
 
-		// we should send them the assignment
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert_eq!(peers.len(), 1);
-				assert_eq!(assignments.len(), 1);
-			}
-		);
+			// we should send them the assignment
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 1);
+					assert_eq!(assignments.len(), 1);
+				}
+			);
 
-		// but if someone else is sending it the same assignment
-		// the peer could send us it as well
-		let assignments = vec![(cert, candidate_index)];
-		let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments);
-		send_message_from_peer(overseer, peer, msg.clone()).await;
+			// but if someone else is sending it the same assignment
+			// the peer could send us it as well
+			let assignments = vec![(cert, candidate_index)];
+			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments);
+			send_message_from_peer(overseer, peer, msg.clone()).await;
 
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "we should not punish the peer");
+			assert!(
+				overseer.recv().timeout(TIMEOUT).await.is_none(),
+				"we should not punish the peer"
+			);
 
-		// send the assignments again
-		send_message_from_peer(overseer, peer, msg).await;
+			// send the assignments again
+			send_message_from_peer(overseer, peer, msg).await;
 
-		// now we should
-		expect_reputation_change(overseer, peer, COST_DUPLICATE_MESSAGE).await;
-		virtual_overseer
-	});
+			// now we should
+			expect_reputation_change(overseer, peer, COST_DUPLICATE_MESSAGE).await;
+			virtual_overseer
+		},
+	);
 }
 
 #[test]
@@ -830,116 +1039,134 @@ fn import_approval_happy_path_v1_v2_peers() {
 	let peer_c = peers.get(2).unwrap().0;
 	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash = Hash::repeat_byte(0xAA);
+	let candidate_hash = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB));
+
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// setup peers with V1 and V2 protocol versions
+			setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await;
+			setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await;
+			setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V1).await;
+
+			let mut keystore = LocalKeystore::in_memory();
+			let session = dummy_session_info_valid(1, &mut keystore, 1);
+
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![(candidate_hash, 0.into(), 0.into()); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Set up a gossip topology, where a, b, and c are topology neighbors to the node.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
+			)
+			.await;
 
-	let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// setup peers with V1 and V2 protocol versions
-		setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await;
-		setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await;
-		setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V1).await;
-
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Set up a gossip topology, where a, b, and c are topology neighbors to the node.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
-		)
-		.await;
-
-		// import an assignment related to `hash` locally
-		let validator_index = ValidatorIndex(0);
-		let candidate_index = 0u32;
-		let cert = fake_assignment_cert(hash, validator_index);
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(
-				cert.clone().into(),
-				candidate_index.into(),
-			),
-		)
-		.await;
+			// import an assignment related to `hash` locally
+			let validator_index = ValidatorIndex(0);
+			let candidate_index = 0u32;
+			let cert = fake_assignment_cert(hash, validator_index);
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(
+					cert.clone().into(),
+					candidate_index.into(),
+				),
+			)
+			.await;
 
-		// 1 peer is v1
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert_eq!(peers.len(), 1);
-				assert_eq!(assignments.len(), 1);
-			}
-		);
-
-		// 1 peer is v2
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
-					protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert_eq!(peers.len(), 1);
-				assert_eq!(assignments.len(), 1);
-			}
-		);
-
-		// send the an approval from peer_b
-		let approval = IndirectSignedApprovalVoteV2 {
-			block_hash: hash,
-			candidate_indices: candidate_index.into(),
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
-		let msg: protocol_v3::ApprovalDistributionMessage =
-			protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
-		send_message_from_peer_v3(overseer, &peer_b, msg).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval(
-				vote,
-				tx,
-			)) => {
-				assert_eq!(vote, approval);
-				tx.send(ApprovalCheckResult::Accepted).unwrap();
-			}
-		);
-
-		expect_reputation_change(overseer, &peer_b, BENEFIT_VALID_MESSAGE_FIRST).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Approvals(approvals)
-				))
-			)) => {
-				assert_eq!(peers.len(), 1);
-				assert_eq!(approvals.len(), 1);
-			}
-		);
-		virtual_overseer
-	});
+			// 1 peer is v1
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 1);
+					assert_eq!(assignments.len(), 1);
+				}
+			);
+
+			// 1 peer is v2
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 1);
+					assert_eq!(assignments.len(), 1);
+				}
+			);
+
+			// send the an approval from peer_b
+			let approval = IndirectSignedApprovalVoteV2 {
+				block_hash: hash,
+				candidate_indices: candidate_index.into(),
+				validator: validator_index,
+				signature: signature_for(
+					&keystore,
+					&session,
+					vec![candidate_hash],
+					validator_index,
+				),
+			};
+			let msg: protocol_v3::ApprovalDistributionMessage =
+				protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
+			send_message_from_peer_v3(overseer, &peer_b, msg).await;
+			provide_session(
+				overseer,
+				dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+			)
+			.await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportApproval(
+					vote, _,
+				)) => {
+					assert_eq!(Into::<IndirectSignedApprovalVoteV2>::into(vote), approval);
+				}
+			);
+
+			expect_reputation_change(overseer, &peer_b, BENEFIT_VALID_MESSAGE_FIRST).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Approvals(approvals)
+					))
+				)) => {
+					assert_eq!(peers.len(), 1);
+					assert_eq!(approvals.len(), 1);
+				}
+			);
+			virtual_overseer
+		},
+	);
 }
 
 // Test a v2 approval that signs multiple candidate is correctly processed.
@@ -952,103 +1179,123 @@ fn import_approval_happy_path_v2() {
 	let peer_c = peers.get(2).unwrap().0;
 	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash = Hash::repeat_byte(0xAA);
+	let candidate_hash_first = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB));
+	let candidate_hash_second = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xCC));
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// setup peers with  V2 protocol versions
+			setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V3).await;
+			setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await;
+			setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V3).await;
+			let mut keystore = LocalKeystore::in_memory();
+			let session = dummy_session_info_valid(1, &mut keystore, 1);
+
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![
+					(candidate_hash_first, 0.into(), 0.into()),
+					(candidate_hash_second, 1.into(), 1.into()),
+				],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Set up a gossip topology, where a, b, and c are topology neighbors to the node.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
+			)
+			.await;
 
-	let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// setup peers with  V2 protocol versions
-		setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V3).await;
-		setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await;
-		setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V3).await;
-
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 2],
-			slot: 1.into(),
-			session: 1,
-		};
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Set up a gossip topology, where a, b, and c are topology neighbors to the node.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
-		)
-		.await;
+			// import an assignment related to `hash` locally
+			let validator_index = ValidatorIndex(0);
+			let candidate_indices: CandidateBitfield =
+				vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap();
+			let candidate_bitfields = vec![CoreIndex(0), CoreIndex(1)].try_into().unwrap();
+			let cert = fake_assignment_cert_v2(hash, validator_index, candidate_bitfields);
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(
+					cert.clone().into(),
+					candidate_indices.clone(),
+				),
+			)
+			.await;
 
-		// import an assignment related to `hash` locally
-		let validator_index = ValidatorIndex(0);
-		let candidate_indices: CandidateBitfield =
-			vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap();
-		let candidate_bitfields = vec![CoreIndex(0), CoreIndex(1)].try_into().unwrap();
-		let cert = fake_assignment_cert_v2(hash, validator_index, candidate_bitfields);
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(
-				cert.clone().into(),
-				candidate_indices.clone(),
-			),
-		)
-		.await;
+			// 1 peer is v2
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 2);
+					assert_eq!(assignments.len(), 1);
+				}
+			);
 
-		// 1 peer is v2
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
-					protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert_eq!(peers.len(), 2);
-				assert_eq!(assignments.len(), 1);
-			}
-		);
-
-		// send the an approval from peer_b
-		let approval = IndirectSignedApprovalVoteV2 {
-			block_hash: hash,
-			candidate_indices,
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
-		let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
-		send_message_from_peer_v3(overseer, &peer_b, msg).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval(
-				vote,
-				tx,
-			)) => {
-				assert_eq!(vote, approval);
-				tx.send(ApprovalCheckResult::Accepted).unwrap();
-			}
-		);
-
-		expect_reputation_change(overseer, &peer_b, BENEFIT_VALID_MESSAGE_FIRST).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
-					protocol_v3::ApprovalDistributionMessage::Approvals(approvals)
-				))
-			)) => {
-				assert_eq!(peers.len(), 1);
-				assert_eq!(approvals.len(), 1);
-			}
-		);
-		virtual_overseer
-	});
+			// send the an approval from peer_b
+			let approval = IndirectSignedApprovalVoteV2 {
+				block_hash: hash,
+				candidate_indices,
+				validator: validator_index,
+				signature: signature_for(
+					&keystore,
+					&session,
+					vec![candidate_hash_first, candidate_hash_second],
+					validator_index,
+				),
+			};
+			let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
+			send_message_from_peer_v3(overseer, &peer_b, msg).await;
+			provide_session(
+				overseer,
+				dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+			)
+			.await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportApproval(
+					vote, _,
+				)) => {
+					assert_eq!(Into::<IndirectSignedApprovalVoteV2>::into(vote), approval);
+				}
+			);
+
+			expect_reputation_change(overseer, &peer_b, BENEFIT_VALID_MESSAGE_FIRST).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Approvals(approvals)
+					))
+				)) => {
+					assert_eq!(peers.len(), 1);
+					assert_eq!(approvals.len(), 1);
+				}
+			);
+			virtual_overseer
+		},
+	);
 }
 
 // Tests that votes that cover multiple assignments candidates are correctly processed on importing
@@ -1062,187 +1309,203 @@ fn multiple_assignments_covered_with_one_approval_vote() {
 	let peer_d = peers.get(4).unwrap().0;
 	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash = Hash::repeat_byte(0xAA);
+	let candidate_hash_first = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB));
+	let candidate_hash_second = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xCC));
+
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// setup peers with  V2 protocol versions
+			setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await;
+			setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await;
+			setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V3).await;
+			setup_peer_with_view(overseer, &peer_d, view![hash], ValidationVersion::V3).await;
+
+			let mut keystore = LocalKeystore::in_memory();
+			let session = dummy_session_info_valid(1, &mut keystore, 5);
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![
+					(candidate_hash_first, 0.into(), 0.into()),
+					(candidate_hash_second, 1.into(), 1.into()),
+				],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Set up a gossip topology, where a, b, and c, d are topology neighbors to the node.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
+			)
+			.await;
 
-	let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// setup peers with  V2 protocol versions
-		setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await;
-		setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await;
-		setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V3).await;
-		setup_peer_with_view(overseer, &peer_d, view![hash], ValidationVersion::V3).await;
-
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 2],
-			slot: 1.into(),
-			session: 1,
-		};
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Set up a gossip topology, where a, b, and c, d are topology neighbors to the node.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
-		)
-		.await;
+			// import an assignment related to `hash` locally
+			let validator_index = ValidatorIndex(2); // peer_c is the originator
+			let candidate_indices: CandidateBitfield =
+				vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap();
 
-		// import an assignment related to `hash` locally
-		let validator_index = ValidatorIndex(2); // peer_c is the originator
-		let candidate_indices: CandidateBitfield =
-			vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap();
+			let core_bitfields = vec![CoreIndex(0)].try_into().unwrap();
+			let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfields);
 
-		let core_bitfields = vec![CoreIndex(0)].try_into().unwrap();
-		let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfields);
+			// send the candidate 0 assignment from peer_b
+			let assignment = IndirectAssignmentCertV2 {
+				block_hash: hash,
+				validator: validator_index,
+				cert: cert.cert,
+			};
+			let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![(
+				assignment,
+				(0 as CandidateIndex).into(),
+			)]);
+			send_message_from_peer_v3(overseer, &peer_d, msg).await;
+			provide_session(overseer, session.clone()).await;
 
-		// send the candidate 0 assignment from peer_b
-		let assignment = IndirectAssignmentCertV2 {
-			block_hash: hash,
-			validator: validator_index,
-			cert: cert.cert,
-		};
-		let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![(
-			assignment,
-			(0 as CandidateIndex).into(),
-		)]);
-		send_message_from_peer_v3(overseer, &peer_d, msg).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-				_, _,
-				tx,
-			)) => {
-				tx.send(AssignmentCheckResult::Accepted).unwrap();
-			}
-		);
-		expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
-					protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert!(peers.len() >= 2);
-				assert!(peers.contains(&peer_a));
-				assert!(peers.contains(&peer_b));
-				assert_eq!(assignments.len(), 1);
-			}
-		);
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment,
+					_,
+				)) => {
+					assert_eq!(assignment.tranche(), 0);
+				}
+			);
+			expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert!(peers.len() >= 2);
+					assert!(peers.contains(&peer_a));
+					assert!(peers.contains(&peer_b));
+					assert_eq!(assignments.len(), 1);
+				}
+			);
 
-		let candidate_bitfields = vec![CoreIndex(1)].try_into().unwrap();
-		let cert = fake_assignment_cert_v2(hash, validator_index, candidate_bitfields);
+			let candidate_bitfields = vec![CoreIndex(1)].try_into().unwrap();
+			let cert = fake_assignment_cert_v2(hash, validator_index, candidate_bitfields);
 
-		// send the candidate 1 assignment from peer_c
-		let assignment = IndirectAssignmentCertV2 {
-			block_hash: hash,
-			validator: validator_index,
-			cert: cert.cert,
-		};
-		let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![(
-			assignment,
-			(1 as CandidateIndex).into(),
-		)]);
-
-		send_message_from_peer_v3(overseer, &peer_c, msg).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-				_, _,
-				tx,
-			)) => {
-				tx.send(AssignmentCheckResult::Accepted).unwrap();
-			}
-		);
-		expect_reputation_change(overseer, &peer_c, BENEFIT_VALID_MESSAGE_FIRST).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
-					protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert!(peers.len() >= 2);
-				assert!(peers.contains(&peer_b));
-				assert!(peers.contains(&peer_a));
-				assert_eq!(assignments.len(), 1);
-			}
-		);
-
-		// send an approval from peer_b
-		let approval = IndirectSignedApprovalVoteV2 {
-			block_hash: hash,
-			candidate_indices,
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
-		let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
-		send_message_from_peer_v3(overseer, &peer_d, msg).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval(
-				vote,
-				tx,
-			)) => {
-				assert_eq!(vote, approval);
-				tx.send(ApprovalCheckResult::Accepted).unwrap();
-			}
-		);
-
-		expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
-					protocol_v3::ApprovalDistributionMessage::Approvals(approvals)
-				))
-			)) => {
-				assert!(peers.len() >= 2);
-				assert!(peers.contains(&peer_b));
-				assert!(peers.contains(&peer_a));
-				assert_eq!(approvals.len(), 1);
-			}
-		);
-		for candidate_index in 0..1 {
-			let (tx_distribution, rx_distribution) = oneshot::channel();
-			let mut candidates_requesting_signatures = HashSet::new();
-			candidates_requesting_signatures.insert((hash, candidate_index));
-			overseer_send(
-				overseer,
-				ApprovalDistributionMessage::GetApprovalSignatures(
-					candidates_requesting_signatures,
-					tx_distribution,
+			// send the candidate 1 assignment from peer_c
+			let assignment = IndirectAssignmentCertV2 {
+				block_hash: hash,
+				validator: validator_index,
+				cert: cert.cert,
+			};
+			let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![(
+				assignment,
+				(1 as CandidateIndex).into(),
+			)]);
+
+			send_message_from_peer_v3(overseer, &peer_c, msg).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment, _,
+				)) => {
+					assert_eq!(assignment.tranche(), 0);
+				}
+			);
+			expect_reputation_change(overseer, &peer_c, BENEFIT_VALID_MESSAGE_FIRST).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert!(peers.len() >= 2);
+					assert!(peers.contains(&peer_b));
+					assert!(peers.contains(&peer_a));
+					assert_eq!(assignments.len(), 1);
+				}
+			);
+
+			// send an approval from peer_b
+			let approval = IndirectSignedApprovalVoteV2 {
+				block_hash: hash,
+				candidate_indices,
+				validator: validator_index,
+				signature: signature_for(
+					&keystore,
+					&session,
+					vec![candidate_hash_first, candidate_hash_second],
+					validator_index,
 				),
-			)
-			.await;
-			let signatures = rx_distribution.await.unwrap();
+			};
+			let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
+			send_message_from_peer_v3(overseer, &peer_d, msg).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportApproval(
+					vote, _,
+				)) => {
+					assert_eq!(Into::<IndirectSignedApprovalVoteV2>::into(vote), approval);
+				}
+			);
 
-			assert_eq!(signatures.len(), 1);
-			for (signing_validator, signature) in signatures {
-				assert_eq!(validator_index, signing_validator);
-				assert_eq!(signature.0, hash);
-				assert_eq!(signature.2, approval.signature);
-				assert_eq!(signature.1, vec![0, 1]);
+			expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Approvals(approvals)
+					))
+				)) => {
+					assert!(peers.len() >= 2);
+					assert!(peers.contains(&peer_b));
+					assert!(peers.contains(&peer_a));
+					assert_eq!(approvals.len(), 1);
+				}
+			);
+			for candidate_index in 0..1 {
+				let (tx_distribution, rx_distribution) = oneshot::channel();
+				let mut candidates_requesting_signatures = HashSet::new();
+				candidates_requesting_signatures.insert((hash, candidate_index));
+				overseer_send(
+					overseer,
+					ApprovalDistributionMessage::GetApprovalSignatures(
+						candidates_requesting_signatures,
+						tx_distribution,
+					),
+				)
+				.await;
+				let signatures = rx_distribution.await.unwrap();
+
+				assert_eq!(signatures.len(), 1);
+				for (signing_validator, signature) in signatures {
+					assert_eq!(validator_index, signing_validator);
+					assert_eq!(signature.0, hash);
+					assert_eq!(signature.2, approval.signature);
+					assert_eq!(signature.1, vec![0, 1]);
+				}
 			}
-		}
-		virtual_overseer
-	});
+			virtual_overseer
+		},
+	);
 }
 
 // Tests that votes that cover multiple assignments candidates are correctly processed when unify
@@ -1256,305 +1519,335 @@ fn unify_with_peer_multiple_assignments_covered_with_one_approval_vote() {
 	let peer_d = peers.get(4).unwrap().0;
 	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash = Hash::repeat_byte(0xAA);
+	let candidate_hash_first = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB));
+	let candidate_hash_second = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xCC));
+
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			setup_peer_with_view(overseer, &peer_d, view![hash], ValidationVersion::V3).await;
+			let mut keystore = LocalKeystore::in_memory();
+			let session = dummy_session_info_valid(1, &mut keystore, 5);
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![
+					(candidate_hash_first, 0.into(), 0.into()),
+					(candidate_hash_second, 1.into(), 1.into()),
+				],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Set up a gossip topology, where a, b, and c, d are topology neighbors to the node.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
+			)
+			.await;
 
-	let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		setup_peer_with_view(overseer, &peer_d, view![hash], ValidationVersion::V3).await;
-
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 2],
-			slot: 1.into(),
-			session: 1,
-		};
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Set up a gossip topology, where a, b, and c, d are topology neighbors to the node.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
-		)
-		.await;
-
-		// import an assignment related to `hash` locally
-		let validator_index = ValidatorIndex(2); // peer_c is the originator
-		let candidate_indices: CandidateBitfield =
-			vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap();
+			// import an assignment related to `hash` locally
+			let validator_index = ValidatorIndex(2); // peer_c is the originator
+			let candidate_indices: CandidateBitfield =
+				vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap();
 
-		let core_bitfields = vec![CoreIndex(0)].try_into().unwrap();
-		let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfields);
+			let core_bitfields = vec![CoreIndex(0)].try_into().unwrap();
+			let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfields);
 
-		// send the candidate 0 assignment from peer_b
-		let assignment = IndirectAssignmentCertV2 {
-			block_hash: hash,
-			validator: validator_index,
-			cert: cert.cert,
-		};
-		let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![(
-			assignment,
-			(0 as CandidateIndex).into(),
-		)]);
-		send_message_from_peer_v3(overseer, &peer_d, msg).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-				_, _,
-				tx,
-			)) => {
-				tx.send(AssignmentCheckResult::Accepted).unwrap();
-			}
-		);
-		expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await;
+			// send the candidate 0 assignment from peer_b
+			let assignment = IndirectAssignmentCertV2 {
+				block_hash: hash,
+				validator: validator_index,
+				cert: cert.cert,
+			};
+			let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![(
+				assignment,
+				(0 as CandidateIndex).into(),
+			)]);
+			send_message_from_peer_v3(overseer, &peer_d, msg).await;
+			provide_session(overseer, session.clone()).await;
 
-		let candidate_bitfields = vec![CoreIndex(1)].try_into().unwrap();
-		let cert = fake_assignment_cert_v2(hash, validator_index, candidate_bitfields);
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment, _,
+				)) => {
+					assert_eq!(assignment.tranche(), 0);
+				}
+			);
+			expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await;
 
-		// send the candidate 1 assignment from peer_c
-		let assignment = IndirectAssignmentCertV2 {
-			block_hash: hash,
-			validator: validator_index,
-			cert: cert.cert,
-		};
-		let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![(
-			assignment,
-			(1 as CandidateIndex).into(),
-		)]);
-
-		send_message_from_peer_v3(overseer, &peer_d, msg).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-				_, _,
-				tx,
-			)) => {
-				tx.send(AssignmentCheckResult::Accepted).unwrap();
-			}
-		);
-		expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await;
-
-		// send an approval from peer_b
-		let approval = IndirectSignedApprovalVoteV2 {
-			block_hash: hash,
-			candidate_indices,
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
-		let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
-		send_message_from_peer_v3(overseer, &peer_d, msg).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval(
-				vote,
-				tx,
-			)) => {
-				assert_eq!(vote, approval);
-				tx.send(ApprovalCheckResult::Accepted).unwrap();
-			}
-		);
-
-		expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await;
-
-		// setup peers with  V2 protocol versions
-		setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await;
-		setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await;
-		let mut expected_peers_assignments = vec![peer_a, peer_b];
-		let mut expected_peers_approvals = vec![peer_a, peer_b];
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
-					protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert!(peers.len() == 1);
-				assert!(expected_peers_assignments.contains(peers.first().unwrap()));
-				expected_peers_assignments.retain(|peer| peer != peers.first().unwrap());
-				assert_eq!(assignments.len(), 2);
-			}
-		);
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
-					protocol_v3::ApprovalDistributionMessage::Approvals(approvals)
-				))
-			)) => {
-				assert!(peers.len() == 1);
-				assert!(expected_peers_approvals.contains(peers.first().unwrap()));
-				expected_peers_approvals.retain(|peer| peer != peers.first().unwrap());
-				assert_eq!(approvals.len(), 1);
-			}
-		);
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
-					protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert!(peers.len() == 1);
-				assert!(expected_peers_assignments.contains(peers.first().unwrap()));
-				expected_peers_assignments.retain(|peer| peer != peers.first().unwrap());
-				assert_eq!(assignments.len(), 2);
-			}
-		);
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
-					protocol_v3::ApprovalDistributionMessage::Approvals(approvals)
-				))
-			)) => {
-				assert!(peers.len() == 1);
-				assert!(expected_peers_approvals.contains(peers.first().unwrap()));
-				expected_peers_approvals.retain(|peer| peer != peers.first().unwrap());
-				assert_eq!(approvals.len(), 1);
-			}
-		);
+			let candidate_bitfields = vec![CoreIndex(1)].try_into().unwrap();
+			let cert = fake_assignment_cert_v2(hash, validator_index, candidate_bitfields);
 
-		virtual_overseer
-	});
-}
+			// send the candidate 1 assignment from peer_c
+			let assignment = IndirectAssignmentCertV2 {
+				block_hash: hash,
+				validator: validator_index,
+				cert: cert.cert,
+			};
+			let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![(
+				assignment,
+				(1 as CandidateIndex).into(),
+			)]);
 
-#[test]
-fn import_approval_bad() {
-	let peer_a = PeerId::random();
-	let peer_b = PeerId::random();
-	let parent_hash = Hash::repeat_byte(0xFF);
-	let hash = Hash::repeat_byte(0xAA);
+			send_message_from_peer_v3(overseer, &peer_d, msg).await;
 
-	let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// setup peers
-		setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await;
-		setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await;
-
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		let validator_index = ValidatorIndex(0);
-		let candidate_index = 0u32;
-		let cert = fake_assignment_cert(hash, validator_index);
-
-		// send the an approval from peer_b, we don't have an assignment yet
-		let approval = IndirectSignedApprovalVoteV2 {
-			block_hash: hash,
-			candidate_indices: candidate_index.into(),
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
-		let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
-		send_message_from_peer_v3(overseer, &peer_b, msg).await;
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment, _,
+				)) => {
+					assert_eq!(assignment.tranche(), 0);
+				}
+			);
+			expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await;
 
-		expect_reputation_change(overseer, &peer_b, COST_UNEXPECTED_MESSAGE).await;
+			// send an approval from peer_b
+			let approval = IndirectSignedApprovalVoteV2 {
+				block_hash: hash,
+				candidate_indices,
+				validator: validator_index,
+				signature: signature_for(
+					&keystore,
+					&session,
+					vec![candidate_hash_first, candidate_hash_second],
+					validator_index,
+				),
+			};
+			let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
+			send_message_from_peer_v3(overseer, &peer_d, msg).await;
 
-		// now import an assignment from peer_b
-		let assignments = vec![(cert.clone(), candidate_index)];
-		let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments);
-		send_message_from_peer(overseer, &peer_b, msg).await;
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportApproval(
+					vote, _,
+				)) => {
+					assert_eq!(Into::<IndirectSignedApprovalVoteV2>::into(vote), approval);
+				}
+			);
 
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-				assignment,
-				i,
-				tx,
-			)) => {
-				assert_eq!(assignment, cert.into());
-				assert_eq!(i, candidate_index.into());
-				tx.send(AssignmentCheckResult::Accepted).unwrap();
-			}
-		);
-
-		expect_reputation_change(overseer, &peer_b, BENEFIT_VALID_MESSAGE_FIRST).await;
-
-		// and try again
-		let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
-		send_message_from_peer_v3(overseer, &peer_b, msg).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval(
-				vote,
-				tx,
-			)) => {
-				assert_eq!(vote, approval);
-				tx.send(ApprovalCheckResult::Bad(ApprovalCheckError::UnknownBlock(hash))).unwrap();
-			}
-		);
+			expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await;
 
-		expect_reputation_change(overseer, &peer_b, COST_INVALID_MESSAGE).await;
-		virtual_overseer
-	});
-}
+			// setup peers with  V2 protocol versions
+			setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await;
+			setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await;
+			let mut expected_peers_assignments = vec![peer_a, peer_b];
+			let mut expected_peers_approvals = vec![peer_a, peer_b];
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert!(peers.len() == 1);
+					assert!(expected_peers_assignments.contains(peers.first().unwrap()));
+					expected_peers_assignments.retain(|peer| peer != peers.first().unwrap());
+					assert_eq!(assignments.len(), 2);
+				}
+			);
 
-/// make sure we clean up the state on block finalized
-#[test]
-fn update_our_view() {
-	let parent_hash = Hash::repeat_byte(0xFF);
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Approvals(approvals)
+					))
+				)) => {
+					assert!(peers.len() == 1);
+					assert!(expected_peers_approvals.contains(peers.first().unwrap()));
+					expected_peers_approvals.retain(|peer| peer != peers.first().unwrap());
+					assert_eq!(approvals.len(), 1);
+				}
+			);
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert!(peers.len() == 1);
+					assert!(expected_peers_assignments.contains(peers.first().unwrap()));
+					expected_peers_assignments.retain(|peer| peer != peers.first().unwrap());
+					assert_eq!(assignments.len(), 2);
+				}
+			);
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Approvals(approvals)
+					))
+				)) => {
+					assert!(peers.len() == 1);
+					assert!(expected_peers_approvals.contains(peers.first().unwrap()));
+					expected_peers_approvals.retain(|peer| peer != peers.first().unwrap());
+					assert_eq!(approvals.len(), 1);
+				}
+			);
+
+			virtual_overseer
+		},
+	);
+}
+
+#[test]
+fn import_approval_bad() {
+	let peer_a = PeerId::random();
+	let peer_b = PeerId::random();
+	let parent_hash = Hash::repeat_byte(0xFF);
+	let hash = Hash::repeat_byte(0xAA);
+	let candidate_hash = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB));
+
+	let diff_candidate_hash = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xCC));
+
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// setup peers
+			setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await;
+			setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await;
+			let mut keystore = LocalKeystore::in_memory();
+			let session = dummy_session_info_valid(1, &mut keystore, 1);
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![(candidate_hash, 0.into(), 0.into()); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			let validator_index = ValidatorIndex(0);
+			let candidate_index = 0u32;
+			let cert = fake_assignment_cert(hash, validator_index);
+
+			// Sign a different candidate hash.
+			let payload =
+				ApprovalVoteMultipleCandidates(&vec![diff_candidate_hash]).signing_payload(1);
+			let sign_key = session.validators.get(ValidatorIndex(0)).unwrap().clone();
+			let signature = keystore
+				.sr25519_sign(ValidatorId::ID, &sign_key.into(), &payload[..])
+				.unwrap()
+				.unwrap();
+
+			// send the an approval from peer_b, we don't have an assignment yet
+			let approval = IndirectSignedApprovalVoteV2 {
+				block_hash: hash,
+				candidate_indices: candidate_index.into(),
+				validator: validator_index,
+				signature: signature.into(),
+			};
+			let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
+			send_message_from_peer_v3(overseer, &peer_b, msg).await;
+
+			expect_reputation_change(overseer, &peer_b, COST_UNEXPECTED_MESSAGE).await;
+
+			// now import an assignment from peer_b
+			let assignments = vec![(cert.clone(), candidate_index)];
+			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments);
+			send_message_from_peer(overseer, &peer_b, msg).await;
+			provide_session(overseer, session.clone()).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment,
+					_,
+				)) => {
+					assert_eq!(assignment.assignment(), &cert.into());
+					assert_eq!(assignment.candidate_indices(), &candidate_index.into());
+					assert_eq!(assignment.tranche(), 0);
+				}
+			);
+
+			expect_reputation_change(overseer, &peer_b, BENEFIT_VALID_MESSAGE_FIRST).await;
+
+			// and try again
+			let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
+			send_message_from_peer_v3(overseer, &peer_b, msg).await;
+
+			expect_reputation_change(overseer, &peer_b, COST_INVALID_MESSAGE).await;
+			virtual_overseer
+		},
+	);
+}
+
+/// make sure we clean up the state on block finalized
+#[test]
+fn update_our_view() {
+	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash_a = Hash::repeat_byte(0xAA);
 	let hash_b = Hash::repeat_byte(0xBB);
 	let hash_c = Hash::repeat_byte(0xCC);
 
-	let state = test_harness(State::default(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// new block `hash_a` with 1 candidates
-		let meta_a = BlockApprovalMeta {
-			hash: hash_a,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let meta_b = BlockApprovalMeta {
-			hash: hash_b,
-			parent_hash: hash_a,
-			number: 2,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let meta_c = BlockApprovalMeta {
-			hash: hash_c,
-			parent_hash: hash_b,
-			number: 3,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta_a, meta_b, meta_c]);
-		overseer_send(overseer, msg).await;
-		virtual_overseer
-	});
+	let state = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		State::default(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// new block `hash_a` with 1 candidates
+			let meta_a = BlockApprovalMeta {
+				hash: hash_a,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let meta_b = BlockApprovalMeta {
+				hash: hash_b,
+				parent_hash: hash_a,
+				number: 2,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let meta_c = BlockApprovalMeta {
+				hash: hash_c,
+				parent_hash: hash_b,
+				number: 3,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta_a, meta_b, meta_c]);
+			overseer_send(overseer, msg).await;
+			virtual_overseer
+		},
+	);
 
 	assert!(state.blocks_by_number.get(&1).is_some());
 	assert!(state.blocks_by_number.get(&2).is_some());
@@ -1563,12 +1856,17 @@ fn update_our_view() {
 	assert!(state.blocks.get(&hash_b).is_some());
 	assert!(state.blocks.get(&hash_c).is_some());
 
-	let state = test_harness(state, |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// finalize a block
-		overseer_signal_block_finalized(overseer, 2).await;
-		virtual_overseer
-	});
+	let state = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state,
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// finalize a block
+			overseer_signal_block_finalized(overseer, 2).await;
+			virtual_overseer
+		},
+	);
 
 	assert!(state.blocks_by_number.get(&1).is_none());
 	assert!(state.blocks_by_number.get(&2).is_none());
@@ -1577,12 +1875,17 @@ fn update_our_view() {
 	assert!(state.blocks.get(&hash_b).is_none());
 	assert!(state.blocks.get(&hash_c).is_some());
 
-	let state = test_harness(state, |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// finalize a very high block
-		overseer_signal_block_finalized(overseer, 4_000_000_000).await;
-		virtual_overseer
-	});
+	let state = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state,
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// finalize a very high block
+			overseer_signal_block_finalized(overseer, 4_000_000_000).await;
+			virtual_overseer
+		},
+	);
 
 	assert!(state.blocks_by_number.get(&3).is_none());
 	assert!(state.blocks.get(&hash_c).is_none());
@@ -1600,81 +1903,89 @@ fn update_peer_view() {
 	let peer_a = peers.first().unwrap().0;
 	let peer = &peer_a;
 
-	let state = test_harness(State::default(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// new block `hash_a` with 1 candidates
-		let meta_a = BlockApprovalMeta {
-			hash: hash_a,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let meta_b = BlockApprovalMeta {
-			hash: hash_b,
-			parent_hash: hash_a,
-			number: 2,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let meta_c = BlockApprovalMeta {
-			hash: hash_c,
-			parent_hash: hash_b,
-			number: 3,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
+	let state = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		State::default(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// new block `hash_a` with 1 candidates
+			let meta_a = BlockApprovalMeta {
+				hash: hash_a,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let meta_b = BlockApprovalMeta {
+				hash: hash_b,
+				parent_hash: hash_a,
+				number: 2,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let meta_c = BlockApprovalMeta {
+				hash: hash_c,
+				parent_hash: hash_b,
+				number: 3,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta_a, meta_b, meta_c]);
+			overseer_send(overseer, msg).await;
+
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Setup a topology where peer_a is neighbor to current node.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1),
+			)
+			.await;
 
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta_a, meta_b, meta_c]);
-		overseer_send(overseer, msg).await;
-
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Setup a topology where peer_a is neighbor to current node.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1),
-		)
-		.await;
+			let cert_a = fake_assignment_cert(hash_a, ValidatorIndex(0));
+			let cert_b = fake_assignment_cert(hash_b, ValidatorIndex(0));
 
-		let cert_a = fake_assignment_cert(hash_a, ValidatorIndex(0));
-		let cert_b = fake_assignment_cert(hash_b, ValidatorIndex(0));
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(cert_a.into(), 0.into()),
+			)
+			.await;
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(cert_a.into(), 0.into()),
-		)
-		.await;
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(cert_b.into(), 0.into()),
+			)
+			.await;
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(cert_b.into(), 0.into()),
-		)
-		.await;
+			// connect a peer
+			setup_peer_with_view(overseer, peer, view![hash_a], ValidationVersion::V1).await;
 
-		// connect a peer
-		setup_peer_with_view(overseer, peer, view![hash_a], ValidationVersion::V1).await;
-
-		// we should send relevant assignments to the peer
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert_eq!(peers.len(), 1);
-				assert_eq!(assignments.len(), 1);
-			}
-		);
-		virtual_overseer
-	});
+			// we should send relevant assignments to the peer
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 1);
+					assert_eq!(assignments.len(), 1);
+				}
+			);
+			virtual_overseer
+		},
+	);
 
 	assert_eq!(state.peer_views.get(peer).map(|v| v.view.finalized_number), Some(0));
 	assert_eq!(
@@ -1691,42 +2002,49 @@ fn update_peer_view() {
 		1,
 	);
 
-	let state = test_harness(state, |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// update peer's view
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::PeerViewChange(
-				*peer,
-				View::new(vec![hash_b, hash_c, hash_d], 2),
-			)),
-		)
-		.await;
+	let state = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state,
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// update peer's view
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::NetworkBridgeUpdate(
+					NetworkBridgeEvent::PeerViewChange(
+						*peer,
+						View::new(vec![hash_b, hash_c, hash_d], 2),
+					),
+				),
+			)
+			.await;
 
-		let cert_c = fake_assignment_cert(hash_c, ValidatorIndex(0));
+			let cert_c = fake_assignment_cert(hash_c, ValidatorIndex(0));
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(cert_c.clone().into(), 0.into()),
-		)
-		.await;
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(cert_c.clone().into(), 0.into()),
+			)
+			.await;
 
-		// we should send relevant assignments to the peer
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert_eq!(peers.len(), 1);
-				assert_eq!(assignments.len(), 1);
-				assert_eq!(assignments[0].0, cert_c);
-			}
-		);
-		virtual_overseer
-	});
+			// we should send relevant assignments to the peer
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 1);
+					assert_eq!(assignments.len(), 1);
+					assert_eq!(assignments[0].0, cert_c);
+				}
+			);
+			virtual_overseer
+		},
+	);
 
 	assert_eq!(state.peer_views.get(peer).map(|v| v.view.finalized_number), Some(2));
 	assert_eq!(
@@ -1744,19 +2062,26 @@ fn update_peer_view() {
 	);
 
 	let finalized_number = 4_000_000_000;
-	let state = test_harness(state, |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// update peer's view
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::PeerViewChange(
-				*peer,
-				View::with_finalized(finalized_number),
-			)),
-		)
-		.await;
-		virtual_overseer
-	});
+	let state = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state,
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// update peer's view
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::NetworkBridgeUpdate(
+					NetworkBridgeEvent::PeerViewChange(
+						*peer,
+						View::with_finalized(finalized_number),
+					),
+				),
+			)
+			.await;
+			virtual_overseer
+		},
+	);
 
 	assert_eq!(state.peer_views.get(peer).map(|v| v.view.finalized_number), Some(finalized_number));
 	assert!(state.blocks.get(&hash_c).unwrap().known_by.get(peer).is_none());
@@ -1779,164 +2104,176 @@ fn update_peer_authority_id() {
 	// Y neighbour, we simulate that PeerId is not known in the beginning.
 	let neighbour_y = peers.get(neighbour_y_index).unwrap().0;
 
-	let _state = test_harness(State::default(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// new block `hash_a` with 1 candidates
-		let meta_a = BlockApprovalMeta {
-			hash: hash_a,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let meta_b = BlockApprovalMeta {
-			hash: hash_b,
-			parent_hash: hash_a,
-			number: 2,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let meta_c = BlockApprovalMeta {
-			hash: hash_c,
-			parent_hash: hash_b,
-			number: 3,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
+	let _state = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		State::default(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// new block `hash_a` with 1 candidates
+			let meta_a = BlockApprovalMeta {
+				hash: hash_a,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let meta_b = BlockApprovalMeta {
+				hash: hash_b,
+				parent_hash: hash_a,
+				number: 2,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let meta_c = BlockApprovalMeta {
+				hash: hash_c,
+				parent_hash: hash_b,
+				number: 3,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta_a, meta_b, meta_c]);
+			overseer_send(overseer, msg).await;
+
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.enumerate()
+				.map(|(index, (peer_id, authority))| {
+					(if index == 0 { None } else { Some(*peer_id) }, authority.clone())
+				})
+				.collect_vec();
+
+			// Setup a topology where peer_a is neighbor to current node.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(
+					1,
+					&peers_with_optional_peer_id,
+					&[neighbour_x_index],
+					&[neighbour_y_index],
+					local_index,
+				),
+			)
+			.await;
 
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta_a, meta_b, meta_c]);
-		overseer_send(overseer, msg).await;
+			let cert_a = fake_assignment_cert(hash_a, ValidatorIndex(local_index as u32));
+			let cert_b = fake_assignment_cert(hash_b, ValidatorIndex(local_index as u32));
 
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.enumerate()
-			.map(|(index, (peer_id, authority))| {
-				(if index == 0 { None } else { Some(*peer_id) }, authority.clone())
-			})
-			.collect_vec();
-
-		// Setup a topology where peer_a is neighbor to current node.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(
-				1,
-				&peers_with_optional_peer_id,
-				&[neighbour_x_index],
-				&[neighbour_y_index],
-				local_index,
-			),
-		)
-		.await;
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(cert_a.into(), 0.into()),
+			)
+			.await;
 
-		let cert_a = fake_assignment_cert(hash_a, ValidatorIndex(local_index as u32));
-		let cert_b = fake_assignment_cert(hash_b, ValidatorIndex(local_index as u32));
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(cert_b.into(), 0.into()),
+			)
+			.await;
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(cert_a.into(), 0.into()),
-		)
-		.await;
+			// connect a peer
+			setup_peer_with_view(overseer, &neighbour_x, view![hash_a], ValidationVersion::V1)
+				.await;
+			setup_peer_with_view(overseer, &neighbour_y, view![hash_a], ValidationVersion::V1)
+				.await;
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(cert_b.into(), 0.into()),
-		)
-		.await;
+			setup_peer_with_view(overseer, &neighbour_x, view![hash_b], ValidationVersion::V1)
+				.await;
+			setup_peer_with_view(overseer, &neighbour_y, view![hash_b], ValidationVersion::V1)
+				.await;
 
-		// connect a peer
-		setup_peer_with_view(overseer, &neighbour_x, view![hash_a], ValidationVersion::V1).await;
-		setup_peer_with_view(overseer, &neighbour_y, view![hash_a], ValidationVersion::V1).await;
-
-		setup_peer_with_view(overseer, &neighbour_x, view![hash_b], ValidationVersion::V1).await;
-		setup_peer_with_view(overseer, &neighbour_y, view![hash_b], ValidationVersion::V1).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert_eq!(peers.len(), 1);
-				assert_eq!(assignments.len(), 1);
-				assert_eq!(peers.get(0), Some(&neighbour_y));
-			}
-		);
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert_eq!(peers.len(), 1);
-				assert_eq!(assignments.len(), 1);
-				assert_eq!(peers.get(0), Some(&neighbour_y));
-			}
-		);
-
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::NetworkBridgeUpdate(
-				NetworkBridgeEvent::UpdatedAuthorityIds(
-					peers[neighbour_x_index].0,
-					[peers[neighbour_x_index].1.clone()].into_iter().collect(),
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 1);
+					assert_eq!(assignments.len(), 1);
+					assert_eq!(peers.get(0), Some(&neighbour_y));
+				}
+			);
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 1);
+					assert_eq!(assignments.len(), 1);
+					assert_eq!(peers.get(0), Some(&neighbour_y));
+				}
+			);
+
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::NetworkBridgeUpdate(
+					NetworkBridgeEvent::UpdatedAuthorityIds(
+						peers[neighbour_x_index].0,
+						[peers[neighbour_x_index].1.clone()].into_iter().collect(),
+					),
 				),
-			),
-		)
-		.await;
+			)
+			.await;
 
-		// we should send relevant assignments to the peer, after we found it's peer id.
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				gum::info!(target: LOG_TARGET, ?peers, ?assignments);
-				assert_eq!(peers.len(), 1);
-				assert_eq!(assignments.len(), 2);
-				assert_eq!(assignments.get(0).unwrap().0.block_hash, hash_a);
-				assert_eq!(assignments.get(1).unwrap().0.block_hash, hash_b);
-				assert_eq!(peers.get(0), Some(&neighbour_x));
-			}
-		);
-
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::NetworkBridgeUpdate(
-				NetworkBridgeEvent::UpdatedAuthorityIds(
-					peers[neighbour_y_index].0,
-					[peers[neighbour_y_index].1.clone()].into_iter().collect(),
+			// we should send relevant assignments to the peer, after we found it's peer id.
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					gum::info!(target: LOG_TARGET, ?peers, ?assignments);
+					assert_eq!(peers.len(), 1);
+					assert_eq!(assignments.len(), 2);
+					assert_eq!(assignments.get(0).unwrap().0.block_hash, hash_a);
+					assert_eq!(assignments.get(1).unwrap().0.block_hash, hash_b);
+					assert_eq!(peers.get(0), Some(&neighbour_x));
+				}
+			);
+
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::NetworkBridgeUpdate(
+					NetworkBridgeEvent::UpdatedAuthorityIds(
+						peers[neighbour_y_index].0,
+						[peers[neighbour_y_index].1.clone()].into_iter().collect(),
+					),
 				),
-			),
-		)
-		.await;
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::NetworkBridgeUpdate(
-				NetworkBridgeEvent::UpdatedAuthorityIds(
-					peers[neighbour_x_index].0,
-					[peers[neighbour_x_index].1.clone()].into_iter().collect(),
+			)
+			.await;
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::NetworkBridgeUpdate(
+					NetworkBridgeEvent::UpdatedAuthorityIds(
+						peers[neighbour_x_index].0,
+						[peers[neighbour_x_index].1.clone()].into_iter().collect(),
+					),
 				),
-			),
-		)
-		.await;
-		assert!(
-			overseer.recv().timeout(TIMEOUT).await.is_none(),
-			"no message should be sent peers are already known"
-		);
+			)
+			.await;
+			assert!(
+				overseer.recv().timeout(TIMEOUT).await.is_none(),
+				"no message should be sent peers are already known"
+			);
 
-		virtual_overseer
-	});
+			virtual_overseer
+		},
+	);
 }
 
 /// E.g. if someone copies the keys...
@@ -1945,89 +2282,105 @@ fn import_remotely_then_locally() {
 	let peer_a = PeerId::random();
 	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash = Hash::repeat_byte(0xAA);
+	let candidate_hash = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB));
 	let peer = &peer_a;
 
-	let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// setup the peer
-		setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
-
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		// import the assignment remotely first
-		let validator_index = ValidatorIndex(0);
-		let candidate_index = 0u32;
-		let cert = fake_assignment_cert(hash, validator_index);
-		let assignments = vec![(cert.clone(), candidate_index)];
-		let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
-		send_message_from_peer(overseer, peer, msg).await;
-
-		// send an `Accept` message from the Approval Voting subsystem
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-				assignment,
-				i,
-				tx,
-			)) => {
-				assert_eq!(assignment, cert.clone().into());
-				assert_eq!(i, candidate_index.into());
-				tx.send(AssignmentCheckResult::Accepted).unwrap();
-			}
-		);
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// setup the peer
+			setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
+			let mut keystore = LocalKeystore::in_memory();
+
+			let session = dummy_session_info_valid(1, &mut keystore, 1);
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![(candidate_hash, 0.into(), 0.into()); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+
+			let payload = ApprovalVoteMultipleCandidates(&vec![candidate_hash]).signing_payload(1);
+			let sign_key = session.validators.get(ValidatorIndex(0)).unwrap().clone();
+			let signature = keystore
+				.sr25519_sign(ValidatorId::ID, &sign_key.into(), &payload[..])
+				.unwrap()
+				.unwrap();
+
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			// import the assignment remotely first
+			let validator_index = ValidatorIndex(0);
+			let candidate_index = 0u32;
+			let cert = fake_assignment_cert(hash, validator_index);
+			let assignments = vec![(cert.clone(), candidate_index)];
+			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+			send_message_from_peer(overseer, peer, msg).await;
+			provide_session(overseer, session.clone()).await;
+
+			// send an `Accept` message from the Approval Voting subsystem
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment,
+					 _,
+				)) => {
+					assert_eq!(assignment.assignment(), &cert.clone().into());
+					assert_eq!(assignment.candidate_indices(), &candidate_index.into());
+					assert_eq!(assignment.tranche(), 0);
+				}
+			);
 
-		expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await;
+			expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await;
 
-		// import the same assignment locally
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(
-				cert.clone().into(),
-				candidate_index.into(),
-			),
-		)
-		.await;
+			// import the same assignment locally
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(
+					cert.clone().into(),
+					candidate_index.into(),
+				),
+			)
+			.await;
 
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
 
-		// send the approval remotely
-		let approval = IndirectSignedApprovalVoteV2 {
-			block_hash: hash,
-			candidate_indices: candidate_index.into(),
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
-		let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
-		send_message_from_peer_v3(overseer, peer, msg).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval(
-				vote,
-				tx,
-			)) => {
-				assert_eq!(vote, approval);
-				tx.send(ApprovalCheckResult::Accepted).unwrap();
-			}
-		);
-		expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await;
+			// send the approval remotely
+			let approval = IndirectSignedApprovalVoteV2 {
+				block_hash: hash,
+				candidate_indices: candidate_index.into(),
+				validator: validator_index,
+				signature: signature.into(),
+			};
+			let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
+			send_message_from_peer_v3(overseer, peer, msg).await;
 
-		// import the same approval locally
-		overseer_send(overseer, ApprovalDistributionMessage::DistributeApproval(approval)).await;
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportApproval(
+					vote, _,
+				)) => {
+					assert_eq!(Into::<IndirectSignedApprovalVoteV2>::into(vote), approval);
+				}
+			);
+			expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await;
 
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
-		virtual_overseer
-	});
+			// import the same approval locally
+			overseer_send(overseer, ApprovalDistributionMessage::DistributeApproval(approval))
+				.await;
+
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			virtual_overseer
+		},
+	);
 }
 
 #[test]
@@ -2038,94 +2391,100 @@ fn sends_assignments_even_when_state_is_approved() {
 	let hash = Hash::repeat_byte(0xAA);
 	let peer = &peer_a;
 
-	let _ = test_harness(State::default(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		State::default(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Setup a topology where peer_a is neighbor to current node.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1),
+			)
+			.await;
 
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Setup a topology where peer_a is neighbor to current node.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1),
-		)
-		.await;
+			let validator_index = ValidatorIndex(0);
+			let candidate_index = 0u32;
 
-		let validator_index = ValidatorIndex(0);
-		let candidate_index = 0u32;
+			// import an assignment and approval locally.
+			let cert = fake_assignment_cert(hash, validator_index);
+			let approval = IndirectSignedApprovalVote {
+				block_hash: hash,
+				candidate_index,
+				validator: validator_index,
+				signature: dummy_signature(),
+			};
 
-		// import an assignment and approval locally.
-		let cert = fake_assignment_cert(hash, validator_index);
-		let approval = IndirectSignedApprovalVote {
-			block_hash: hash,
-			candidate_index,
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(
+					cert.clone().into(),
+					candidate_index.into(),
+				),
+			)
+			.await;
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(
-				cert.clone().into(),
-				candidate_index.into(),
-			),
-		)
-		.await;
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeApproval(approval.clone().into()),
+			)
+			.await;
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeApproval(approval.clone().into()),
-		)
-		.await;
+			// connect the peer.
+			setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
 
-		// connect the peer.
-		setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
-
-		let assignments = vec![(cert.clone(), candidate_index)];
-		let approvals = vec![approval.clone()];
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
-				))
-			)) => {
-				assert_eq!(peers, vec![*peer]);
-				assert_eq!(sent_assignments, assignments);
-			}
-		);
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals)
-				))
-			)) => {
-				assert_eq!(peers, vec![*peer]);
-				assert_eq!(sent_approvals, approvals);
-			}
-		);
+			let assignments = vec![(cert.clone(), candidate_index)];
+			let approvals = vec![approval.clone()];
 
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
-		virtual_overseer
-	});
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
+					))
+				)) => {
+					assert_eq!(peers, vec![*peer]);
+					assert_eq!(sent_assignments, assignments);
+				}
+			);
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals)
+					))
+				)) => {
+					assert_eq!(peers, vec![*peer]);
+					assert_eq!(sent_approvals, approvals);
+				}
+			);
+
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			virtual_overseer
+		},
+	);
 }
 
 /// Same as `sends_assignments_even_when_state_is_approved_v2` but with `VRFModuloCompact`
@@ -2138,393 +2497,589 @@ fn sends_assignments_even_when_state_is_approved_v2() {
 	let hash = Hash::repeat_byte(0xAA);
 	let peer = &peer_a;
 
-	let _ = test_harness(State::default(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		State::default(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 4],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Setup a topology where peer_a is neighbor to current node.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1),
+			)
+			.await;
 
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 4],
-			slot: 1.into(),
-			session: 1,
-		};
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Setup a topology where peer_a is neighbor to current node.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1),
-		)
-		.await;
+			let validator_index = ValidatorIndex(0);
+			let cores = vec![0, 1, 2, 3];
+			let candidate_bitfield: CandidateBitfield = cores.clone().try_into().unwrap();
+
+			let core_bitfield: CoreBitfield = cores
+				.iter()
+				.map(|index| CoreIndex(*index))
+				.collect::<Vec<_>>()
+				.try_into()
+				.unwrap();
+
+			let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield.clone());
+
+			// Assumes candidate index == core index.
+			let approvals = cores
+				.iter()
+				.map(|core| IndirectSignedApprovalVoteV2 {
+					block_hash: hash,
+					candidate_indices: (*core).into(),
+					validator: validator_index,
+					signature: dummy_signature(),
+				})
+				.collect::<Vec<_>>();
+
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(
+					cert.clone().into(),
+					candidate_bitfield.clone(),
+				),
+			)
+			.await;
+
+			for approval in &approvals {
+				overseer_send(
+					overseer,
+					ApprovalDistributionMessage::DistributeApproval(approval.clone()),
+				)
+				.await;
+			}
+
+			// connect the peer.
+			setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V3).await;
+
+			let assignments = vec![(cert.clone(), candidate_bitfield.clone())];
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Assignments(sent_assignments)
+					))
+				)) => {
+					assert_eq!(peers, vec![*peer]);
+					assert_eq!(sent_assignments, assignments);
+				}
+			);
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Approvals(sent_approvals)
+					))
+				)) => {
+					// Construct a hashmaps of approvals for comparison. Approval distribution reorders messages because they are kept in a
+					// hashmap as well.
+					let sent_approvals = sent_approvals.into_iter().map(|approval| (approval.candidate_indices.clone(), approval)).collect::<HashMap<_,_>>();
+					let approvals = approvals.into_iter().map(|approval| (approval.candidate_indices.clone(), approval)).collect::<HashMap<_,_>>();
+
+					assert_eq!(peers, vec![*peer]);
+					assert_eq!(sent_approvals, approvals);
+				}
+			);
+
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			virtual_overseer
+		},
+	);
+}
+
+/// <https://github.com/paritytech/polkadot/pull/5089>
+///
+/// 1. Receive remote peer view update with an unknown head
+/// 2. Receive assignments for that unknown head
+/// 3. Update our view and import the new block
+/// 4. Expect that no reputation with `COST_UNEXPECTED_MESSAGE` is applied
+#[test]
+fn race_condition_in_local_vs_remote_view_update() {
+	let parent_hash = Hash::repeat_byte(0xFF);
+	let peer_a = PeerId::random();
+	let hash_b = Hash::repeat_byte(0xBB);
 
-		let validator_index = ValidatorIndex(0);
-		let cores = vec![0, 1, 2, 3];
-		let candidate_bitfield: CandidateBitfield = cores.clone().try_into().unwrap();
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			let peer = &peer_a;
+
+			// Test a small number of candidates
+			let candidates_count = 1;
+			let meta = BlockApprovalMeta {
+				hash: hash_b,
+				parent_hash,
+				number: 2,
+				candidates: vec![Default::default(); candidates_count],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+
+			// This will send a peer view that is ahead of our view
+			setup_peer_with_view(overseer, peer, view![hash_b], ValidationVersion::V1).await;
+
+			// Send our view update to include a new head
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::NetworkBridgeUpdate(
+					NetworkBridgeEvent::OurViewChange(our_view![hash_b]),
+				),
+			)
+			.await;
 
-		let core_bitfield: CoreBitfield = cores
-			.iter()
-			.map(|index| CoreIndex(*index))
-			.collect::<Vec<_>>()
-			.try_into()
-			.unwrap();
+			// send assignments related to `hash_b` but they will come to the MessagesPending
+			let assignments: Vec<_> = (0..candidates_count)
+				.map(|candidate_index| {
+					let validator_index = ValidatorIndex(candidate_index as u32);
+					let cert = fake_assignment_cert(hash_b, validator_index);
+					(cert, candidate_index as u32)
+				})
+				.collect();
 
-		let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield.clone());
+			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+			send_message_from_peer(overseer, peer, msg.clone()).await;
+
+			// This will handle pending messages being processed
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			provide_session(
+				overseer,
+				dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+			)
+			.await;
+
+			for i in 0..candidates_count {
+				// Previously, this has caused out-of-view assignments/approvals
+				//expect_reputation_change(overseer, peer, COST_UNEXPECTED_MESSAGE).await;
+
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+						assignment,
+						_,
+					)) => {
+						assert_eq!(assignment.assignment(), &assignments[i].0.clone().into());
+						assert_eq!(assignment.candidate_indices(), &assignments[i].1.into());
+						assert_eq!(assignment.tranche(), 0);
+					}
+				);
+
+				// Since we have a valid statement pending, this should always occur
+				expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await;
+			}
+			virtual_overseer
+		},
+	);
+}
 
-		// Assumes candidate index == core index.
-		let approvals = cores
-			.iter()
-			.map(|core| IndirectSignedApprovalVoteV2 {
+// Tests that local messages propagate to both dimensions.
+#[test]
+fn propagates_locally_generated_assignment_to_both_dimensions() {
+	let parent_hash = Hash::repeat_byte(0xFF);
+	let hash = Hash::repeat_byte(0xAA);
+
+	let peers = make_peers_and_authority_ids(100);
+
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		State::default(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+
+			// Connect all peers.
+			for (peer, _) in &peers {
+				setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
+			}
+
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+
+			// Set up a gossip topology.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(
+					1,
+					&peers_with_optional_peer_id,
+					&[0, 10, 20, 30, 40, 60, 70, 80],
+					&[50, 51, 52, 53, 54, 55, 56, 57],
+					1,
+				),
+			)
+			.await;
+
+			let expected_indices = [
+				// Both dimensions in the gossip topology
+				0, 10, 20, 30, 40, 60, 70, 80, 50, 51, 52, 53, 54, 55, 56, 57,
+			];
+
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			let validator_index = ValidatorIndex(0);
+			let candidate_index = 0u32;
+
+			// import an assignment and approval locally.
+			let cert = fake_assignment_cert(hash, validator_index);
+			let approval = IndirectSignedApprovalVote {
 				block_hash: hash,
-				candidate_indices: (*core).into(),
+				candidate_index,
 				validator: validator_index,
-				signature: dummy_signature(),
-			})
-			.collect::<Vec<_>>();
-
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(
-				cert.clone().into(),
-				candidate_bitfield.clone(),
-			),
-		)
-		.await;
+				signature: dummy_signature(),
+			};
 
-		for approval in &approvals {
 			overseer_send(
 				overseer,
-				ApprovalDistributionMessage::DistributeApproval(approval.clone()),
+				ApprovalDistributionMessage::DistributeAssignment(
+					cert.clone().into(),
+					candidate_index.into(),
+				),
 			)
 			.await;
-		}
-
-		// connect the peer.
-		setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V3).await;
-
-		let assignments = vec![(cert.clone(), candidate_bitfield.clone())];
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
-					protocol_v3::ApprovalDistributionMessage::Assignments(sent_assignments)
-				))
-			)) => {
-				assert_eq!(peers, vec![*peer]);
-				assert_eq!(sent_assignments, assignments);
-			}
-		);
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
-					protocol_v3::ApprovalDistributionMessage::Approvals(sent_approvals)
-				))
-			)) => {
-				// Construct a hashmaps of approvals for comparison. Approval distribution reorders messages because they are kept in a
-				// hashmap as well.
-				let sent_approvals = sent_approvals.into_iter().map(|approval| (approval.candidate_indices.clone(), approval)).collect::<HashMap<_,_>>();
-				let approvals = approvals.into_iter().map(|approval| (approval.candidate_indices.clone(), approval)).collect::<HashMap<_,_>>();
-
-				assert_eq!(peers, vec![*peer]);
-				assert_eq!(sent_approvals, approvals);
-			}
-		);
-
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
-		virtual_overseer
-	});
-}
-
-/// <https://github.com/paritytech/polkadot/pull/5089>
-///
-/// 1. Receive remote peer view update with an unknown head
-/// 2. Receive assignments for that unknown head
-/// 3. Update our view and import the new block
-/// 4. Expect that no reputation with `COST_UNEXPECTED_MESSAGE` is applied
-#[test]
-fn race_condition_in_local_vs_remote_view_update() {
-	let parent_hash = Hash::repeat_byte(0xFF);
-	let peer_a = PeerId::random();
-	let hash_b = Hash::repeat_byte(0xBB);
-
-	let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		let peer = &peer_a;
-
-		// Test a small number of candidates
-		let candidates_count = 1;
-		let meta = BlockApprovalMeta {
-			hash: hash_b,
-			parent_hash,
-			number: 2,
-			candidates: vec![Default::default(); candidates_count],
-			slot: 1.into(),
-			session: 1,
-		};
-
-		// This will send a peer view that is ahead of our view
-		setup_peer_with_view(overseer, peer, view![hash_b], ValidationVersion::V1).await;
-
-		// Send our view update to include a new head
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::OurViewChange(
-				our_view![hash_b],
-			)),
-		)
-		.await;
-
-		// send assignments related to `hash_b` but they will come to the MessagesPending
-		let assignments: Vec<_> = (0..candidates_count)
-			.map(|candidate_index| {
-				let validator_index = ValidatorIndex(candidate_index as u32);
-				let cert = fake_assignment_cert(hash_b, validator_index);
-				(cert, candidate_index as u32)
-			})
-			.collect();
 
-		let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
-		send_message_from_peer(overseer, peer, msg.clone()).await;
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeApproval(approval.clone().into()),
+			)
+			.await;
 
-		// This will handle pending messages being processed
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
+			let assignments = vec![(cert.clone(), candidate_index)];
+			let approvals = vec![approval.clone()];
 
-		for i in 0..candidates_count {
-			// Previously, this has caused out-of-view assignments/approvals
-			//expect_reputation_change(overseer, peer, COST_UNEXPECTED_MESSAGE).await;
+			let mut assignment_sent_peers = assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					sent_peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
+					))
+				)) => {
+					assert_eq!(sent_peers.len(), expected_indices.len() + 4);
+					for &i in &expected_indices {
+						assert!(
+							sent_peers.contains(&peers[i].0),
+							"Message not sent to expected peer {}",
+							i,
+						);
+					}
+					assert_eq!(sent_assignments, assignments);
+					sent_peers
+				}
+			);
 
 			assert_matches!(
 				overseer_recv(overseer).await,
-				AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-					assignment,
-					claimed_candidate_index,
-					tx,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					mut sent_peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals)
+					))
 				)) => {
-					assert_eq!(assignment, assignments[i].0.clone().into());
-					assert_eq!(claimed_candidate_index, assignments[i].1.into());
-					tx.send(AssignmentCheckResult::Accepted).unwrap();
+					// Random sampling is reused from the assignment.
+					sent_peers.sort();
+					assignment_sent_peers.sort();
+					assert_eq!(sent_peers, assignment_sent_peers);
+					assert_eq!(sent_approvals, approvals);
 				}
 			);
 
-			// Since we have a valid statement pending, this should always occur
-			expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await;
-		}
-		virtual_overseer
-	});
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			virtual_overseer
+		},
+	);
 }
 
-// Tests that local messages propagate to both dimensions.
+// Tests that messages propagate to the unshared dimension.
 #[test]
-fn propagates_locally_generated_assignment_to_both_dimensions() {
+fn propagates_assignments_along_unshared_dimension() {
 	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash = Hash::repeat_byte(0xAA);
 
 	let peers = make_peers_and_authority_ids(100);
 
-	let _ = test_harness(State::default(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
 
-		// Connect all peers.
-		for (peer, _) in &peers {
-			setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
-		}
+			// Connect all peers.
+			for (peer, _) in &peers {
+				setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
+			}
 
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-
-		// Set up a gossip topology.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(
-				1,
-				&peers_with_optional_peer_id,
-				&[0, 10, 20, 30, 40, 60, 70, 80],
-				&[50, 51, 52, 53, 54, 55, 56, 57],
-				1,
-			),
-		)
-		.await;
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
 
-		let expected_indices = [
-			// Both dimensions in the gossip topology
-			0, 10, 20, 30, 40, 60, 70, 80, 50, 51, 52, 53, 54, 55, 56, 57,
-		];
-
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
+			// Set up a gossip topology.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(
+					1,
+					&peers_with_optional_peer_id,
+					&[0, 10, 20, 30],
+					&[50, 51, 52, 53],
+					1,
+				),
+			)
+			.await;
 
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			// Test messages from X direction go to Y peers
+			{
+				let validator_index = ValidatorIndex(0);
+				let candidate_index = 0u32;
+
+				// import an assignment and approval locally.
+				let cert = fake_assignment_cert(hash, validator_index);
+				let assignments = vec![(cert.clone(), candidate_index)];
+
+				let msg =
+					protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+
+				// Issuer of the message is important, not the peer we receive from.
+				// 99 deliberately chosen because it's not in X or Y.
+				send_message_from_peer(overseer, &peers[99].0, msg).await;
+				provide_session(
+					overseer,
+					dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+				)
+				.await;
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+						assignment, _,
+					)) => {
+						assert_eq!(assignment.tranche(), 0);
+					}
+				);
+				expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await;
 
-		let validator_index = ValidatorIndex(0);
-		let candidate_index = 0u32;
+				let expected_y = [50, 51, 52, 53];
 
-		// import an assignment and approval locally.
-		let cert = fake_assignment_cert(hash, validator_index);
-		let approval = IndirectSignedApprovalVote {
-			block_hash: hash,
-			candidate_index,
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+						sent_peers,
+						Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+							protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
+						))
+					)) => {
+						assert_eq!(sent_peers.len(), expected_y.len() + 4);
+						for &i in &expected_y {
+							assert!(
+								sent_peers.contains(&peers[i].0),
+								"Message not sent to expected peer {}",
+								i,
+							);
+						}
+						assert_eq!(sent_assignments, assignments);
+					}
+				);
+			};
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(
-				cert.clone().into(),
-				candidate_index.into(),
-			),
-		)
-		.await;
+			// Test messages from X direction go to Y peers
+			{
+				let validator_index = ValidatorIndex(50);
+				let candidate_index = 0u32;
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeApproval(approval.clone().into()),
-		)
-		.await;
+				// import an assignment and approval locally.
+				let cert = fake_assignment_cert(hash, validator_index);
+				let assignments = vec![(cert.clone(), candidate_index)];
 
-		let assignments = vec![(cert.clone(), candidate_index)];
-		let approvals = vec![approval.clone()];
-
-		let mut assignment_sent_peers = assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				sent_peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
-				))
-			)) => {
-				assert_eq!(sent_peers.len(), expected_indices.len() + 4);
-				for &i in &expected_indices {
-					assert!(
-						sent_peers.contains(&peers[i].0),
-						"Message not sent to expected peer {}",
-						i,
-					);
-				}
-				assert_eq!(sent_assignments, assignments);
-				sent_peers
-			}
-		);
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				mut sent_peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals)
-				))
-			)) => {
-				// Random sampling is reused from the assignment.
-				sent_peers.sort();
-				assignment_sent_peers.sort();
-				assert_eq!(sent_peers, assignment_sent_peers);
-				assert_eq!(sent_approvals, approvals);
-			}
-		);
+				let msg =
+					protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
 
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
-		virtual_overseer
-	});
+				// Issuer of the message is important, not the peer we receive from.
+				// 99 deliberately chosen because it's not in X or Y.
+				send_message_from_peer(overseer, &peers[99].0, msg).await;
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+						assignment, _,
+					)) => {
+						assert_eq!(assignment.tranche(), 0);
+					}
+				);
+				expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await;
+
+				let expected_x = [0, 10, 20, 30];
+
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+						sent_peers,
+						Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+							protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
+						))
+					)) => {
+						assert_eq!(sent_peers.len(), expected_x.len() + 4);
+						for &i in &expected_x {
+							assert!(
+								sent_peers.contains(&peers[i].0),
+								"Message not sent to expected peer {}",
+								i,
+							);
+						}
+						assert_eq!(sent_assignments, assignments);
+					}
+				);
+			};
+
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			virtual_overseer
+		},
+	);
 }
 
-// Tests that messages propagate to the unshared dimension.
+// tests that messages are propagated to necessary peers after they connect
 #[test]
-fn propagates_assignments_along_unshared_dimension() {
+fn propagates_to_required_after_connect() {
 	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash = Hash::repeat_byte(0xAA);
 
 	let peers = make_peers_and_authority_ids(100);
 
-	let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-
-		// Connect all peers.
-		for (peer, _) in &peers {
-			setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
-		}
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		State::default(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
 
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-
-		// Set up a gossip topology.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(
-				1,
-				&peers_with_optional_peer_id,
-				&[0, 10, 20, 30],
-				&[50, 51, 52, 53],
-				1,
-			),
-		)
-		.await;
+			let omitted = [0, 10, 50, 51];
 
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
+			// Connect all peers except omitted.
+			for (i, (peer, _)) in peers.iter().enumerate() {
+				if !omitted.contains(&i) {
+					setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
+				}
+			}
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Set up a gossip topology.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(
+					1,
+					&peers_with_optional_peer_id,
+					&[0, 10, 20, 30, 40, 60, 70, 80],
+					&[50, 51, 52, 53, 54, 55, 56, 57],
+					1,
+				),
+			)
+			.await;
 
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
+			let expected_indices = [
+				// Both dimensions in the gossip topology, minus omitted.
+				20, 30, 40, 60, 70, 80, 52, 53, 54, 55, 56, 57,
+			];
+
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
 
-		// Test messages from X direction go to Y peers
-		{
 			let validator_index = ValidatorIndex(0);
 			let candidate_index = 0u32;
 
 			// import an assignment and approval locally.
 			let cert = fake_assignment_cert(hash, validator_index);
-			let assignments = vec![(cert.clone(), candidate_index)];
+			let approval = IndirectSignedApprovalVote {
+				block_hash: hash,
+				candidate_index,
+				validator: validator_index,
+				signature: dummy_signature(),
+			};
 
-			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(
+					cert.clone().into(),
+					candidate_index.into(),
+				),
+			)
+			.await;
 
-			// Issuer of the message is important, not the peer we receive from.
-			// 99 deliberately chosen because it's not in X or Y.
-			send_message_from_peer(overseer, &peers[99].0, msg).await;
-			assert_matches!(
-				overseer_recv(overseer).await,
-				AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-					_,
-					_,
-					tx,
-				)) => {
-					tx.send(AssignmentCheckResult::Accepted).unwrap();
-				}
-			);
-			expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await;
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeApproval(approval.clone().into()),
+			)
+			.await;
 
-			let expected_y = [50, 51, 52, 53];
+			let assignments = vec![(cert.clone(), candidate_index)];
+			let approvals = vec![approval.clone()];
 
-			assert_matches!(
+			let mut assignment_sent_peers = assert_matches!(
 				overseer_recv(overseer).await,
 				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
 					sent_peers,
@@ -2532,8 +3087,8 @@ fn propagates_assignments_along_unshared_dimension() {
 						protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
 					))
 				)) => {
-					assert_eq!(sent_peers.len(), expected_y.len() + 4);
-					for &i in &expected_y {
+					assert_eq!(sent_peers.len(), expected_indices.len() + 4);
+					for &i in &expected_indices {
 						assert!(
 							sent_peers.contains(&peers[i].0),
 							"Message not sent to expected peer {}",
@@ -2541,1123 +3096,1029 @@ fn propagates_assignments_along_unshared_dimension() {
 						);
 					}
 					assert_eq!(sent_assignments, assignments);
+					sent_peers
 				}
 			);
-		};
 
-		// Test messages from X direction go to Y peers
-		{
-			let validator_index = ValidatorIndex(50);
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					mut sent_peers,
+					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+						protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals)
+					))
+				)) => {
+					// Random sampling is reused from the assignment.
+					sent_peers.sort();
+					assignment_sent_peers.sort();
+					assert_eq!(sent_peers, assignment_sent_peers);
+					assert_eq!(sent_approvals, approvals);
+				}
+			);
+
+			for i in omitted.iter().copied() {
+				setup_peer_with_view(overseer, &peers[i].0, view![hash], ValidationVersion::V1)
+					.await;
+
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+						sent_peers,
+						Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+							protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
+						))
+					)) => {
+						assert_eq!(sent_peers.len(), 1);
+						assert_eq!(&sent_peers[0], &peers[i].0);
+						assert_eq!(sent_assignments, assignments);
+					}
+				);
+
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+						sent_peers,
+						Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+							protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals)
+						))
+					)) => {
+						assert_eq!(sent_peers.len(), 1);
+						assert_eq!(&sent_peers[0], &peers[i].0);
+						assert_eq!(sent_approvals, approvals);
+					}
+				);
+			}
+
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			virtual_overseer
+		},
+	);
+}
+
+// test that new gossip topology triggers send of messages.
+#[test]
+fn sends_to_more_peers_after_getting_topology() {
+	let parent_hash = Hash::repeat_byte(0xFF);
+	let hash = Hash::repeat_byte(0xAA);
+
+	let peers = make_peers_and_authority_ids(100);
+
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		State::default(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+
+			// Connect all peers except omitted.
+			for (peer, _) in &peers {
+				setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
+			}
+
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			let validator_index = ValidatorIndex(0);
 			let candidate_index = 0u32;
 
 			// import an assignment and approval locally.
 			let cert = fake_assignment_cert(hash, validator_index);
+			let approval = IndirectSignedApprovalVote {
+				block_hash: hash,
+				candidate_index,
+				validator: validator_index,
+				signature: dummy_signature(),
+			};
+
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(
+					cert.clone().into(),
+					candidate_index.into(),
+				),
+			)
+			.await;
+
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeApproval(approval.clone().into()),
+			)
+			.await;
+
 			let assignments = vec![(cert.clone(), candidate_index)];
+			let approvals = vec![approval.clone()];
+
+			let expected_indices = vec![0, 10, 20, 30, 50, 51, 52, 53];
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Set up a gossip topology.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(
+					1,
+					&peers_with_optional_peer_id,
+					&[0, 10, 20, 30],
+					&[50, 51, 52, 53],
+					1,
+				),
+			)
+			.await;
+
+			let mut expected_indices_assignments = expected_indices.clone();
+			let mut expected_indices_approvals = expected_indices.clone();
+
+			for _ in 0..expected_indices_assignments.len() {
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+						sent_peers,
+						Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+							protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
+						))
+					)) => {
+						// Sends to all expected peers.
+						assert_eq!(sent_peers.len(), 1);
+						assert_eq!(sent_assignments, assignments);
 
-			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+						let pos = expected_indices_assignments.iter()
+							.position(|i| &peers[*i].0 == &sent_peers[0])
+							.unwrap();
+						expected_indices_assignments.remove(pos);
+					}
+				);
+			}
 
-			// Issuer of the message is important, not the peer we receive from.
-			// 99 deliberately chosen because it's not in X or Y.
-			send_message_from_peer(overseer, &peers[99].0, msg).await;
-			assert_matches!(
-				overseer_recv(overseer).await,
-				AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-					_,
-					_,
-					tx,
-				)) => {
-					tx.send(AssignmentCheckResult::Accepted).unwrap();
-				}
-			);
-			expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await;
+			for _ in 0..expected_indices_approvals.len() {
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+						sent_peers,
+						Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+							protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals)
+						))
+					)) => {
+						// Sends to all expected peers.
+						assert_eq!(sent_peers.len(), 1);
+						assert_eq!(sent_approvals, approvals);
 
-			let expected_x = [0, 10, 20, 30];
+						let pos = expected_indices_approvals.iter()
+							.position(|i| &peers[*i].0 == &sent_peers[0])
+							.unwrap();
 
-			assert_matches!(
-				overseer_recv(overseer).await,
-				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-					sent_peers,
-					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-						protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
-					))
-				)) => {
-					assert_eq!(sent_peers.len(), expected_x.len() + 4);
-					for &i in &expected_x {
-						assert!(
-							sent_peers.contains(&peers[i].0),
-							"Message not sent to expected peer {}",
-							i,
-						);
+						expected_indices_approvals.remove(pos);
 					}
-					assert_eq!(sent_assignments, assignments);
-				}
-			);
-		};
+				);
+			}
 
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
-		virtual_overseer
-	});
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			virtual_overseer
+		},
+	);
 }
 
-// tests that messages are propagated to necessary peers after they connect
+// test aggression L1
 #[test]
-fn propagates_to_required_after_connect() {
+fn originator_aggression_l1() {
 	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash = Hash::repeat_byte(0xAA);
 
 	let peers = make_peers_and_authority_ids(100);
 
-	let _ = test_harness(State::default(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
+	let mut state = State::default();
+	state.aggression_config.resend_unfinalized_period = None;
+	let aggression_l1_threshold = state.aggression_config.l1_threshold.unwrap();
 
-		let omitted = [0, 10, 50, 51];
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state,
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
 
-		// Connect all peers except omitted.
-		for (i, (peer, _)) in peers.iter().enumerate() {
-			if !omitted.contains(&i) {
+			// Connect all peers except omitted.
+			for (peer, _) in &peers {
 				setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
 			}
-		}
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Set up a gossip topology.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(
-				1,
-				&peers_with_optional_peer_id,
-				&[0, 10, 20, 30, 40, 60, 70, 80],
-				&[50, 51, 52, 53, 54, 55, 56, 57],
-				1,
-			),
-		)
-		.await;
-
-		let expected_indices = [
-			// Both dimensions in the gossip topology, minus omitted.
-			20, 30, 40, 60, 70, 80, 52, 53, 54, 55, 56, 57,
-		];
-
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
 
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
 
-		let validator_index = ValidatorIndex(0);
-		let candidate_index = 0u32;
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
 
-		// import an assignment and approval locally.
-		let cert = fake_assignment_cert(hash, validator_index);
-		let approval = IndirectSignedApprovalVote {
-			block_hash: hash,
-			candidate_index,
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
+			let validator_index = ValidatorIndex(0);
+			let candidate_index = 0u32;
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(
-				cert.clone().into(),
-				candidate_index.into(),
-			),
-		)
-		.await;
+			// import an assignment and approval locally.
+			let cert = fake_assignment_cert(hash, validator_index);
+			let approval = IndirectSignedApprovalVote {
+				block_hash: hash,
+				candidate_index,
+				validator: validator_index,
+				signature: dummy_signature(),
+			};
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Set up a gossip topology.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(
+					1,
+					&peers_with_optional_peer_id,
+					&[0, 10, 20, 30],
+					&[50, 51, 52, 53],
+					1,
+				),
+			)
+			.await;
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeApproval(approval.clone().into()),
-		)
-		.await;
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(
+					cert.clone().into(),
+					candidate_index.into(),
+				),
+			)
+			.await;
 
-		let assignments = vec![(cert.clone(), candidate_index)];
-		let approvals = vec![approval.clone()];
-
-		let mut assignment_sent_peers = assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				sent_peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
-				))
-			)) => {
-				assert_eq!(sent_peers.len(), expected_indices.len() + 4);
-				for &i in &expected_indices {
-					assert!(
-						sent_peers.contains(&peers[i].0),
-						"Message not sent to expected peer {}",
-						i,
-					);
-				}
-				assert_eq!(sent_assignments, assignments);
-				sent_peers
-			}
-		);
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				mut sent_peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals)
-				))
-			)) => {
-				// Random sampling is reused from the assignment.
-				sent_peers.sort();
-				assignment_sent_peers.sort();
-				assert_eq!(sent_peers, assignment_sent_peers);
-				assert_eq!(sent_approvals, approvals);
-			}
-		);
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeApproval(approval.clone().into()),
+			)
+			.await;
 
-		for i in omitted.iter().copied() {
-			setup_peer_with_view(overseer, &peers[i].0, view![hash], ValidationVersion::V1).await;
+			let assignments = vec![(cert.clone(), candidate_index)];
+			let approvals = vec![approval.clone()];
 
-			assert_matches!(
+			let prev_sent_indices = assert_matches!(
 				overseer_recv(overseer).await,
 				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
 					sent_peers,
 					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-						protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
+						protocol_v1::ApprovalDistributionMessage::Assignments(_)
 					))
 				)) => {
-					assert_eq!(sent_peers.len(), 1);
-					assert_eq!(&sent_peers[0], &peers[i].0);
-					assert_eq!(sent_assignments, assignments);
+					sent_peers.into_iter()
+						.filter_map(|sp| peers.iter().position(|p| &p.0 == &sp))
+						.collect::<Vec<_>>()
 				}
 			);
 
 			assert_matches!(
 				overseer_recv(overseer).await,
 				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-					sent_peers,
+					_,
 					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-						protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals)
+						protocol_v1::ApprovalDistributionMessage::Approvals(_)
 					))
-				)) => {
-					assert_eq!(sent_peers.len(), 1);
-					assert_eq!(&sent_peers[0], &peers[i].0);
-					assert_eq!(sent_approvals, approvals);
-				}
+				)) => { }
 			);
-		}
 
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
-		virtual_overseer
-	});
+			// Add blocks until aggression L1 is triggered.
+			{
+				let mut parent_hash = hash;
+				for level in 0..aggression_l1_threshold {
+					let number = 1 + level + 1; // first block had number 1
+					let hash = BlakeTwo256::hash_of(&(parent_hash, number));
+					let meta = BlockApprovalMeta {
+						hash,
+						parent_hash,
+						number,
+						candidates: vec![],
+						slot: (level as u64).into(),
+						session: 1,
+						vrf_story: RelayVRFStory(Default::default()),
+					};
+
+					let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(level + 1);
+					overseer_send(overseer, msg).await;
+
+					let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+					overseer_send(overseer, msg).await;
+
+					parent_hash = hash;
+				}
+			}
+
+			let unsent_indices =
+				(0..peers.len()).filter(|i| !prev_sent_indices.contains(&i)).collect::<Vec<_>>();
+
+			for _ in 0..unsent_indices.len() {
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+						sent_peers,
+						Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+							protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
+						))
+					)) => {
+						// Sends to all expected peers.
+						assert_eq!(sent_peers.len(), 1);
+						assert_eq!(sent_assignments, assignments);
+
+						assert!(unsent_indices.iter()
+							.any(|i| &peers[*i].0 == &sent_peers[0]));
+					}
+				);
+			}
+
+			for _ in 0..unsent_indices.len() {
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+						sent_peers,
+						Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+							protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals)
+						))
+					)) => {
+						// Sends to all expected peers.
+						assert_eq!(sent_peers.len(), 1);
+						assert_eq!(sent_approvals, approvals);
+
+						assert!(unsent_indices.iter()
+							.any(|i| &peers[*i].0 == &sent_peers[0]));
+					}
+				);
+			}
+
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			virtual_overseer
+		},
+	);
 }
 
-// test that new gossip topology triggers send of messages.
+// test aggression L1
 #[test]
-fn sends_to_more_peers_after_getting_topology() {
+fn non_originator_aggression_l1() {
 	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash = Hash::repeat_byte(0xAA);
 
 	let peers = make_peers_and_authority_ids(100);
 
-	let _ = test_harness(State::default(), |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-
-		// Connect all peers except omitted.
-		for (peer, _) in &peers {
-			setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
-		}
+	let mut state = state_without_reputation_delay();
+	state.aggression_config.resend_unfinalized_period = None;
+	let aggression_l1_threshold = state.aggression_config.l1_threshold.unwrap();
 
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state,
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
 
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
+			// Connect all peers except omitted.
+			for (peer, _) in &peers {
+				setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
+			}
 
-		let validator_index = ValidatorIndex(0);
-		let candidate_index = 0u32;
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
 
-		// import an assignment and approval locally.
-		let cert = fake_assignment_cert(hash, validator_index);
-		let approval = IndirectSignedApprovalVote {
-			block_hash: hash,
-			candidate_index,
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(
-				cert.clone().into(),
-				candidate_index.into(),
-			),
-		)
-		.await;
+			let validator_index = ValidatorIndex(0);
+			let candidate_index = 0u32;
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeApproval(approval.clone().into()),
-		)
-		.await;
+			// import an assignment and approval locally.
+			let cert = fake_assignment_cert(hash, validator_index);
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Set up a gossip topology.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(
+					1,
+					&peers_with_optional_peer_id,
+					&[0, 10, 20, 30],
+					&[50, 51, 52, 53],
+					1,
+				),
+			)
+			.await;
 
-		let assignments = vec![(cert.clone(), candidate_index)];
-		let approvals = vec![approval.clone()];
-
-		let expected_indices = vec![0, 10, 20, 30, 50, 51, 52, 53];
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Set up a gossip topology.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(
-				1,
-				&peers_with_optional_peer_id,
-				&[0, 10, 20, 30],
-				&[50, 51, 52, 53],
-				1,
-			),
-		)
-		.await;
+			let assignments = vec![(cert.clone().into(), candidate_index)];
+			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
 
-		let mut expected_indices_assignments = expected_indices.clone();
-		let mut expected_indices_approvals = expected_indices.clone();
+			// Issuer of the message is important, not the peer we receive from.
+			// 99 deliberately chosen because it's not in X or Y.
+			send_message_from_peer(overseer, &peers[99].0, msg).await;
+			provide_session(
+				overseer,
+				dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+			)
+			.await;
 
-		for _ in 0..expected_indices_assignments.len() {
 			assert_matches!(
 				overseer_recv(overseer).await,
-				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-					sent_peers,
-					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-						protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
-					))
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment, _,
 				)) => {
-					// Sends to all expected peers.
-					assert_eq!(sent_peers.len(), 1);
-					assert_eq!(sent_assignments, assignments);
-
-					let pos = expected_indices_assignments.iter()
-						.position(|i| &peers[*i].0 == &sent_peers[0])
-						.unwrap();
-					expected_indices_assignments.remove(pos);
+					assert_eq!(assignment.tranche(), 0);
 				}
 			);
-		}
 
-		for _ in 0..expected_indices_approvals.len() {
+			expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await;
+
 			assert_matches!(
 				overseer_recv(overseer).await,
 				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-					sent_peers,
+					_,
 					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-						protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals)
+						protocol_v1::ApprovalDistributionMessage::Assignments(_)
 					))
-				)) => {
-					// Sends to all expected peers.
-					assert_eq!(sent_peers.len(), 1);
-					assert_eq!(sent_approvals, approvals);
-
-					let pos = expected_indices_approvals.iter()
-						.position(|i| &peers[*i].0 == &sent_peers[0])
-						.unwrap();
-
-					expected_indices_approvals.remove(pos);
-				}
+				)) => { }
 			);
-		}
-
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
-		virtual_overseer
-	});
-}
 
-// test aggression L1
-#[test]
-fn originator_aggression_l1() {
-	let parent_hash = Hash::repeat_byte(0xFF);
-	let hash = Hash::repeat_byte(0xAA);
+			// Add blocks until aggression L1 is triggered.
+			{
+				let mut parent_hash = hash;
+				for level in 0..aggression_l1_threshold {
+					let number = 1 + level + 1; // first block had number 1
+					let hash = BlakeTwo256::hash_of(&(parent_hash, number));
+					let meta = BlockApprovalMeta {
+						hash,
+						parent_hash,
+						number,
+						candidates: vec![],
+						slot: (level as u64).into(),
+						session: 1,
+						vrf_story: RelayVRFStory(Default::default()),
+					};
 
-	let peers = make_peers_and_authority_ids(100);
+					let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+					overseer_send(overseer, msg).await;
 
-	let mut state = State::default();
-	state.aggression_config.resend_unfinalized_period = None;
-	let aggression_l1_threshold = state.aggression_config.l1_threshold.unwrap();
+					parent_hash = hash;
+				}
+			}
 
-	let _ = test_harness(state, |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
+			// No-op on non-originator
 
-		// Connect all peers except omitted.
-		for (peer, _) in &peers {
-			setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
-		}
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			virtual_overseer
+		},
+	);
+}
 
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
+// test aggression L2 on non-originator
+#[test]
+fn non_originator_aggression_l2() {
+	let parent_hash = Hash::repeat_byte(0xFF);
+	let hash = Hash::repeat_byte(0xAA);
 
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
+	let peers = make_peers_and_authority_ids(100);
 
-		let validator_index = ValidatorIndex(0);
-		let candidate_index = 0u32;
+	let mut state = state_without_reputation_delay();
+	state.aggression_config.resend_unfinalized_period = None;
 
-		// import an assignment and approval locally.
-		let cert = fake_assignment_cert(hash, validator_index);
-		let approval = IndirectSignedApprovalVote {
-			block_hash: hash,
-			candidate_index,
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Set up a gossip topology.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(
-				1,
-				&peers_with_optional_peer_id,
-				&[0, 10, 20, 30],
-				&[50, 51, 52, 53],
-				1,
-			),
-		)
-		.await;
+	let aggression_l1_threshold = state.aggression_config.l1_threshold.unwrap();
+	let aggression_l2_threshold = state.aggression_config.l2_threshold.unwrap();
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state,
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+
+			// Connect all peers except omitted.
+			for (peer, _) in &peers {
+				setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
+			}
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(
-				cert.clone().into(),
-				candidate_index.into(),
-			),
-		)
-		.await;
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
 
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeApproval(approval.clone().into()),
-		)
-		.await;
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
 
-		let assignments = vec![(cert.clone(), candidate_index)];
-		let approvals = vec![approval.clone()];
-
-		let prev_sent_indices = assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				sent_peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(_)
-				))
-			)) => {
-				sent_peers.into_iter()
-					.filter_map(|sp| peers.iter().position(|p| &p.0 == &sp))
-					.collect::<Vec<_>>()
-			}
-		);
+			let validator_index = ValidatorIndex(0);
+			let candidate_index = 0u32;
 
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				_,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Approvals(_)
-				))
-			)) => { }
-		);
-
-		// Add blocks until aggression L1 is triggered.
-		{
-			let mut parent_hash = hash;
-			for level in 0..aggression_l1_threshold {
-				let number = 1 + level + 1; // first block had number 1
-				let hash = BlakeTwo256::hash_of(&(parent_hash, number));
-				let meta = BlockApprovalMeta {
-					hash,
-					parent_hash,
-					number,
-					candidates: vec![],
-					slot: (level as u64).into(),
-					session: 1,
-				};
-
-				let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(level + 1);
-				overseer_send(overseer, msg).await;
-
-				let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-				overseer_send(overseer, msg).await;
-
-				parent_hash = hash;
-			}
-		}
+			// import an assignment and approval locally.
+			let cert = fake_assignment_cert(hash, validator_index);
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Set up a gossip topology.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(
+					1,
+					&peers_with_optional_peer_id,
+					&[0, 10, 20, 30],
+					&[50, 51, 52, 53],
+					1,
+				),
+			)
+			.await;
 
-		let unsent_indices =
-			(0..peers.len()).filter(|i| !prev_sent_indices.contains(&i)).collect::<Vec<_>>();
+			let assignments = vec![(cert.clone(), candidate_index)];
+			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
 
-		for _ in 0..unsent_indices.len() {
+			// Issuer of the message is important, not the peer we receive from.
+			// 99 deliberately chosen because it's not in X or Y.
+			send_message_from_peer(overseer, &peers[99].0, msg).await;
+			provide_session(
+				overseer,
+				dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+			)
+			.await;
 			assert_matches!(
 				overseer_recv(overseer).await,
-				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-					sent_peers,
-					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-						protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
-					))
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment, _,
 				)) => {
-					// Sends to all expected peers.
-					assert_eq!(sent_peers.len(), 1);
-					assert_eq!(sent_assignments, assignments);
-
-					assert!(unsent_indices.iter()
-						.any(|i| &peers[*i].0 == &sent_peers[0]));
+					assert_eq!(assignment.tranche(), 0);
 				}
 			);
-		}
 
-		for _ in 0..unsent_indices.len() {
-			assert_matches!(
+			expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await;
+
+			let prev_sent_indices = assert_matches!(
 				overseer_recv(overseer).await,
 				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
 					sent_peers,
 					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-						protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals)
+						protocol_v1::ApprovalDistributionMessage::Assignments(_)
 					))
 				)) => {
-					// Sends to all expected peers.
-					assert_eq!(sent_peers.len(), 1);
-					assert_eq!(sent_approvals, approvals);
-
-					assert!(unsent_indices.iter()
-						.any(|i| &peers[*i].0 == &sent_peers[0]));
+					sent_peers.into_iter()
+						.filter_map(|sp| peers.iter().position(|p| &p.0 == &sp))
+						.collect::<Vec<_>>()
 				}
 			);
-		}
-
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
-		virtual_overseer
-	});
-}
 
-// test aggression L1
-#[test]
-fn non_originator_aggression_l1() {
-	let parent_hash = Hash::repeat_byte(0xFF);
-	let hash = Hash::repeat_byte(0xAA);
+			// Add blocks until aggression L1 is triggered.
+			let chain_head = {
+				let mut parent_hash = hash;
+				for level in 0..aggression_l1_threshold {
+					let number = 1 + level + 1; // first block had number 1
+					let hash = BlakeTwo256::hash_of(&(parent_hash, number));
+					let meta = BlockApprovalMeta {
+						hash,
+						parent_hash,
+						number,
+						candidates: vec![],
+						slot: (level as u64).into(),
+						session: 1,
+						vrf_story: RelayVRFStory(Default::default()),
+					};
 
-	let peers = make_peers_and_authority_ids(100);
+					let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(level + 1);
+					overseer_send(overseer, msg).await;
 
-	let mut state = state_without_reputation_delay();
-	state.aggression_config.resend_unfinalized_period = None;
-	let aggression_l1_threshold = state.aggression_config.l1_threshold.unwrap();
+					let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+					overseer_send(overseer, msg).await;
 
-	let _ = test_harness(state, |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
+					parent_hash = hash;
+				}
 
-		// Connect all peers except omitted.
-		for (peer, _) in &peers {
-			setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
-		}
+				parent_hash
+			};
 
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
+			// No-op on non-originator
 
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		let validator_index = ValidatorIndex(0);
-		let candidate_index = 0u32;
-
-		// import an assignment and approval locally.
-		let cert = fake_assignment_cert(hash, validator_index);
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Set up a gossip topology.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(
-				1,
-				&peers_with_optional_peer_id,
-				&[0, 10, 20, 30],
-				&[50, 51, 52, 53],
-				1,
-			),
-		)
-		.await;
+			// Add blocks until aggression L2 is triggered.
+			{
+				let mut parent_hash = chain_head;
+				for level in 0..aggression_l2_threshold - aggression_l1_threshold {
+					let number = aggression_l1_threshold + level + 1 + 1; // first block had number 1
+					let hash = BlakeTwo256::hash_of(&(parent_hash, number));
+					let meta = BlockApprovalMeta {
+						hash,
+						parent_hash,
+						number,
+						candidates: vec![],
+						slot: (level as u64).into(),
+						session: 1,
+						vrf_story: RelayVRFStory(Default::default()),
+					};
 
-		let assignments = vec![(cert.clone().into(), candidate_index)];
-		let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+					let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(
+						aggression_l1_threshold + level + 1,
+					);
+					overseer_send(overseer, msg).await;
+					let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+					overseer_send(overseer, msg).await;
 
-		// Issuer of the message is important, not the peer we receive from.
-		// 99 deliberately chosen because it's not in X or Y.
-		send_message_from_peer(overseer, &peers[99].0, msg).await;
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-				_,
-				_,
-				tx,
-			)) => {
-				tx.send(AssignmentCheckResult::Accepted).unwrap();
+					parent_hash = hash;
+				}
 			}
-		);
 
-		expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await;
+			// XY dimension - previously sent.
+			let unsent_indices = [0, 10, 20, 30, 50, 51, 52, 53]
+				.iter()
+				.cloned()
+				.filter(|i| !prev_sent_indices.contains(&i))
+				.collect::<Vec<_>>();
 
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				_,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(_)
-				))
-			)) => { }
-		);
-
-		// Add blocks until aggression L1 is triggered.
-		{
-			let mut parent_hash = hash;
-			for level in 0..aggression_l1_threshold {
-				let number = 1 + level + 1; // first block had number 1
-				let hash = BlakeTwo256::hash_of(&(parent_hash, number));
-				let meta = BlockApprovalMeta {
-					hash,
-					parent_hash,
-					number,
-					candidates: vec![],
-					slot: (level as u64).into(),
-					session: 1,
-				};
-
-				let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-				overseer_send(overseer, msg).await;
-
-				parent_hash = hash;
-			}
-		}
+			for _ in 0..unsent_indices.len() {
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+						sent_peers,
+						Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+							protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
+						))
+					)) => {
+						// Sends to all expected peers.
+						assert_eq!(sent_peers.len(), 1);
+						assert_eq!(sent_assignments, assignments);
 
-		// No-op on non-originator
+						assert!(unsent_indices.iter()
+							.any(|i| &peers[*i].0 == &sent_peers[0]));
+					}
+				);
+			}
 
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
-		virtual_overseer
-	});
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			virtual_overseer
+		},
+	);
 }
 
-// test aggression L2 on non-originator
+// Tests that messages propagate to the unshared dimension.
 #[test]
-fn non_originator_aggression_l2() {
+fn resends_messages_periodically() {
 	let parent_hash = Hash::repeat_byte(0xFF);
 	let hash = Hash::repeat_byte(0xAA);
 
 	let peers = make_peers_and_authority_ids(100);
 
 	let mut state = state_without_reputation_delay();
-	state.aggression_config.resend_unfinalized_period = None;
+	state.aggression_config.l1_threshold = None;
+	state.aggression_config.l2_threshold = None;
+	state.aggression_config.resend_unfinalized_period = Some(2);
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state,
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+
+			// Connect all peers.
+			for (peer, _) in &peers {
+				setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
+			}
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			// Set up a gossip topology.
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(
+					1,
+					&peers_with_optional_peer_id,
+					&[0, 10, 20, 30],
+					&[50, 51, 52, 53],
+					1,
+				),
+			)
+			.await;
 
-	let aggression_l1_threshold = state.aggression_config.l1_threshold.unwrap();
-	let aggression_l2_threshold = state.aggression_config.l2_threshold.unwrap();
-	let _ = test_harness(state, |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
 
-		// Connect all peers except omitted.
-		for (peer, _) in &peers {
-			setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
-		}
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
 
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
+			let validator_index = ValidatorIndex(0);
+			let candidate_index = 0u32;
 
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		let validator_index = ValidatorIndex(0);
-		let candidate_index = 0u32;
-
-		// import an assignment and approval locally.
-		let cert = fake_assignment_cert(hash, validator_index);
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Set up a gossip topology.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(
-				1,
-				&peers_with_optional_peer_id,
-				&[0, 10, 20, 30],
-				&[50, 51, 52, 53],
-				1,
-			),
-		)
-		.await;
+			// import an assignment and approval locally.
+			let cert = fake_assignment_cert(hash, validator_index);
+			let assignments = vec![(cert.clone(), candidate_index)];
 
-		let assignments = vec![(cert.clone(), candidate_index)];
-		let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+			{
+				let msg =
+					protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+
+				// Issuer of the message is important, not the peer we receive from.
+				// 99 deliberately chosen because it's not in X or Y.
+				send_message_from_peer(overseer, &peers[99].0, msg).await;
+				provide_session(
+					overseer,
+					dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+				)
+				.await;
 
-		// Issuer of the message is important, not the peer we receive from.
-		// 99 deliberately chosen because it's not in X or Y.
-		send_message_from_peer(overseer, &peers[99].0, msg).await;
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-				_,
-				_,
-				tx,
-			)) => {
-				tx.send(AssignmentCheckResult::Accepted).unwrap();
-			}
-		);
-
-		expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await;
-
-		let prev_sent_indices = assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				sent_peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(_)
-				))
-			)) => {
-				sent_peers.into_iter()
-					.filter_map(|sp| peers.iter().position(|p| &p.0 == &sp))
-					.collect::<Vec<_>>()
-			}
-		);
-
-		// Add blocks until aggression L1 is triggered.
-		let chain_head = {
-			let mut parent_hash = hash;
-			for level in 0..aggression_l1_threshold {
-				let number = 1 + level + 1; // first block had number 1
-				let hash = BlakeTwo256::hash_of(&(parent_hash, number));
-				let meta = BlockApprovalMeta {
-					hash,
-					parent_hash,
-					number,
-					candidates: vec![],
-					slot: (level as u64).into(),
-					session: 1,
-				};
-
-				let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(level + 1);
-				overseer_send(overseer, msg).await;
-
-				let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-				overseer_send(overseer, msg).await;
-
-				parent_hash = hash;
-			}
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+						assignment, _,
+					)) => {
+						assert_eq!(assignment.tranche(), 0);
+					}
+				);
+				expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await;
 
-			parent_hash
-		};
+				let expected_y = [50, 51, 52, 53];
 
-		// No-op on non-originator
-
-		// Add blocks until aggression L2 is triggered.
-		{
-			let mut parent_hash = chain_head;
-			for level in 0..aggression_l2_threshold - aggression_l1_threshold {
-				let number = aggression_l1_threshold + level + 1 + 1; // first block had number 1
-				let hash = BlakeTwo256::hash_of(&(parent_hash, number));
-				let meta = BlockApprovalMeta {
-					hash,
-					parent_hash,
-					number,
-					candidates: vec![],
-					slot: (level as u64).into(),
-					session: 1,
-				};
-
-				let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(
-					aggression_l1_threshold + level + 1,
+				assert_matches!(
+					overseer_recv(overseer).await,
+					AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+						sent_peers,
+						Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+							protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
+						))
+					)) => {
+						assert_eq!(sent_peers.len(), expected_y.len() + 4);
+						for &i in &expected_y {
+							assert!(
+								sent_peers.contains(&peers[i].0),
+								"Message not sent to expected peer {}",
+								i,
+							);
+						}
+						assert_eq!(sent_assignments, assignments);
+					}
 				);
-				overseer_send(overseer, msg).await;
-				let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-				overseer_send(overseer, msg).await;
+			};
+
+			let mut number = 1;
+			for _ in 0..10 {
+				// Add blocks until resend is done.
+				{
+					let mut parent_hash = hash;
+					for level in 0..2 {
+						number = number + 1;
+						let hash = BlakeTwo256::hash_of(&(parent_hash, number));
+						let meta = BlockApprovalMeta {
+							hash,
+							parent_hash,
+							number,
+							candidates: vec![],
+							slot: (level as u64).into(),
+							session: 1,
+							vrf_story: RelayVRFStory(Default::default()),
+						};
+
+						let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(2);
+						overseer_send(overseer, msg).await;
+						let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+						overseer_send(overseer, msg).await;
+
+						parent_hash = hash;
+					}
+				}
 
-				parent_hash = hash;
+				let mut expected_y = vec![50, 51, 52, 53];
+
+				// Expect messages sent only to topology peers, one by one.
+				for _ in 0..expected_y.len() {
+					assert_matches!(
+						overseer_recv(overseer).await,
+						AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+							sent_peers,
+							Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
+								protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
+							))
+						)) => {
+							assert_eq!(sent_peers.len(), 1);
+							let expected_pos = expected_y.iter()
+								.position(|&i| &peers[i].0 == &sent_peers[0])
+								.unwrap();
+
+							expected_y.remove(expected_pos);
+							assert_eq!(sent_assignments, assignments);
+						}
+					);
+				}
 			}
-		}
 
-		// XY dimension - previously sent.
-		let unsent_indices = [0, 10, 20, 30, 50, 51, 52, 53]
-			.iter()
-			.cloned()
-			.filter(|i| !prev_sent_indices.contains(&i))
-			.collect::<Vec<_>>();
+			assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
+			virtual_overseer
+		},
+	);
+}
+
+/// Tests that peers correctly receive versioned messages.
+#[test]
+fn import_versioned_approval() {
+	let peers = make_peers_and_authority_ids(15);
+	let peer_a = peers.get(0).unwrap().0;
+	let peer_b = peers.get(1).unwrap().0;
+	let peer_c = peers.get(2).unwrap().0;
+
+	let parent_hash = Hash::repeat_byte(0xFF);
+	let hash = Hash::repeat_byte(0xAA);
+	let state = state_without_reputation_delay();
+	let candidate_hash = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB));
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(SystemClock {}),
+		state,
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// All peers are aware of relay parent.
+			setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V2).await;
+			setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await;
+			setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V2).await;
+
+			// Set up a gossip topology, where a, b, c and d are topology neighbors to the node
+			// under testing.
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
+			)
+			.await;
+
+			let mut keystore = LocalKeystore::in_memory();
+			let session = dummy_session_info_valid(1, &mut keystore, 1);
+
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 1,
+				candidates: vec![(candidate_hash, 0.into(), 0.into()); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			// import an assignment related to `hash` locally
+			let validator_index = ValidatorIndex(0);
+			let candidate_index = 0u32;
+			let cert = fake_assignment_cert(hash, validator_index);
+			overseer_send(
+				overseer,
+				ApprovalDistributionMessage::DistributeAssignment(
+					cert.into(),
+					candidate_index.into(),
+				),
+			)
+			.await;
 
-		for _ in 0..unsent_indices.len() {
 			assert_matches!(
 				overseer_recv(overseer).await,
 				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-					sent_peers,
+					peers,
 					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-						protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
+						protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
 					))
 				)) => {
-					// Sends to all expected peers.
-					assert_eq!(sent_peers.len(), 1);
-					assert_eq!(sent_assignments, assignments);
-
-					assert!(unsent_indices.iter()
-						.any(|i| &peers[*i].0 == &sent_peers[0]));
+					assert_eq!(peers, vec![peer_b]);
+					assert_eq!(assignments.len(), 1);
 				}
 			);
-		}
-
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
-		virtual_overseer
-	});
-}
-
-// Tests that messages propagate to the unshared dimension.
-#[test]
-fn resends_messages_periodically() {
-	let parent_hash = Hash::repeat_byte(0xFF);
-	let hash = Hash::repeat_byte(0xAA);
-
-	let peers = make_peers_and_authority_ids(100);
 
-	let mut state = state_without_reputation_delay();
-	state.aggression_config.l1_threshold = None;
-	state.aggression_config.l2_threshold = None;
-	state.aggression_config.resend_unfinalized_period = Some(2);
-	let _ = test_harness(state, |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-
-		// Connect all peers.
-		for (peer, _) in &peers {
-			setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
-		}
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		// Set up a gossip topology.
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(
-				1,
-				&peers_with_optional_peer_id,
-				&[0, 10, 20, 30],
-				&[50, 51, 52, 53],
-				1,
-			),
-		)
-		.await;
-
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		let validator_index = ValidatorIndex(0);
-		let candidate_index = 0u32;
-
-		// import an assignment and approval locally.
-		let cert = fake_assignment_cert(hash, validator_index);
-		let assignments = vec![(cert.clone(), candidate_index)];
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V2(protocol_v2::ValidationProtocol::ApprovalDistribution(
+						protocol_v2::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 2);
+					assert!(peers.contains(&peer_a));
+					assert!(peers.contains(&peer_c));
 
-		{
-			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+					assert_eq!(assignments.len(), 1);
+				}
+			);
 
-			// Issuer of the message is important, not the peer we receive from.
-			// 99 deliberately chosen because it's not in X or Y.
-			send_message_from_peer(overseer, &peers[99].0, msg).await;
+			// send the an approval from peer_a
+			let approval = IndirectSignedApprovalVote {
+				block_hash: hash,
+				candidate_index,
+				validator: validator_index,
+				signature: signature_for(
+					&keystore,
+					&session,
+					vec![candidate_hash],
+					validator_index,
+				),
+			};
+			let msg = protocol_v2::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
+			send_message_from_peer_v2(overseer, &peer_a, msg).await;
+			provide_session(overseer, session).await;
 			assert_matches!(
 				overseer_recv(overseer).await,
-				AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
-					_,
-					_,
-					tx,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportApproval(
+					vote, _,
 				)) => {
-					tx.send(AssignmentCheckResult::Accepted).unwrap();
+					assert_eq!(Into::<IndirectSignedApprovalVoteV2>::into(vote), approval.into());
 				}
 			);
-			expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await;
 
-			let expected_y = [50, 51, 52, 53];
+			expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await;
 
+			// Peers b and c receive versioned approval messages.
 			assert_matches!(
 				overseer_recv(overseer).await,
 				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-					sent_peers,
+					peers,
 					Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-						protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
+						protocol_v1::ApprovalDistributionMessage::Approvals(approvals)
 					))
 				)) => {
-					assert_eq!(sent_peers.len(), expected_y.len() + 4);
-					for &i in &expected_y {
-						assert!(
-							sent_peers.contains(&peers[i].0),
-							"Message not sent to expected peer {}",
-							i,
-						);
-					}
-					assert_eq!(sent_assignments, assignments);
+					assert_eq!(peers, vec![peer_b]);
+					assert_eq!(approvals.len(), 1);
 				}
 			);
-		};
-
-		let mut number = 1;
-		for _ in 0..10 {
-			// Add blocks until resend is done.
-			{
-				let mut parent_hash = hash;
-				for level in 0..2 {
-					number = number + 1;
-					let hash = BlakeTwo256::hash_of(&(parent_hash, number));
-					let meta = BlockApprovalMeta {
-						hash,
-						parent_hash,
-						number,
-						candidates: vec![],
-						slot: (level as u64).into(),
-						session: 1,
-					};
 
-					let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(2);
-					overseer_send(overseer, msg).await;
-					let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-					overseer_send(overseer, msg).await;
-
-					parent_hash = hash;
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V2(protocol_v2::ValidationProtocol::ApprovalDistribution(
+						protocol_v2::ApprovalDistributionMessage::Approvals(approvals)
+					))
+				)) => {
+					assert_eq!(peers, vec![peer_c]);
+					assert_eq!(approvals.len(), 1);
 				}
-			}
-
-			let mut expected_y = vec![50, 51, 52, 53];
-
-			// Expect messages sent only to topology peers, one by one.
-			for _ in 0..expected_y.len() {
-				assert_matches!(
-					overseer_recv(overseer).await,
-					AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-						sent_peers,
-						Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-							protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments)
-						))
-					)) => {
-						assert_eq!(sent_peers.len(), 1);
-						let expected_pos = expected_y.iter()
-							.position(|&i| &peers[i].0 == &sent_peers[0])
-							.unwrap();
-
-						expected_y.remove(expected_pos);
-						assert_eq!(sent_assignments, assignments);
-					}
-				);
-			}
-		}
-
-		assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
-		virtual_overseer
-	});
-}
-
-/// Tests that peers correctly receive versioned messages.
-#[test]
-fn import_versioned_approval() {
-	let peers = make_peers_and_authority_ids(15);
-	let peer_a = peers.get(0).unwrap().0;
-	let peer_b = peers.get(1).unwrap().0;
-	let peer_c = peers.get(2).unwrap().0;
+			);
 
-	let parent_hash = Hash::repeat_byte(0xFF);
-	let hash = Hash::repeat_byte(0xAA);
-	let state = state_without_reputation_delay();
-	let _ = test_harness(state, |mut virtual_overseer| async move {
-		let overseer = &mut virtual_overseer;
-		// All peers are aware of relay parent.
-		setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V2).await;
-		setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await;
-		setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V2).await;
-
-		// Set up a gossip topology, where a, b, c and d are topology neighbors to the node under
-		// testing.
-		let peers_with_optional_peer_id = peers
-			.iter()
-			.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
-			.collect_vec();
-		setup_gossip_topology(
-			overseer,
-			make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
-		)
-		.await;
+			// send an obviously invalid approval
+			let approval = IndirectSignedApprovalVote {
+				block_hash: hash,
+				// Invalid candidate index, should not pass sanitization.
+				candidate_index: 16777284,
+				validator: validator_index,
+				signature: dummy_signature(),
+			};
+			let msg = protocol_v2::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
+			send_message_from_peer_v2(overseer, &peer_a, msg).await;
 
-		// new block `hash_a` with 1 candidates
-		let meta = BlockApprovalMeta {
-			hash,
-			parent_hash,
-			number: 1,
-			candidates: vec![Default::default(); 1],
-			slot: 1.into(),
-			session: 1,
-		};
-		let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
-		overseer_send(overseer, msg).await;
-
-		// import an assignment related to `hash` locally
-		let validator_index = ValidatorIndex(0);
-		let candidate_index = 0u32;
-		let cert = fake_assignment_cert(hash, validator_index);
-		overseer_send(
-			overseer,
-			ApprovalDistributionMessage::DistributeAssignment(cert.into(), candidate_index.into()),
-		)
-		.await;
+			expect_reputation_change(overseer, &peer_a, COST_OVERSIZED_BITFIELD).await;
 
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert_eq!(peers, vec![peer_b]);
-				assert_eq!(assignments.len(), 1);
-			}
-		);
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V2(protocol_v2::ValidationProtocol::ApprovalDistribution(
-					protocol_v2::ApprovalDistributionMessage::Assignments(assignments)
-				))
-			)) => {
-				assert_eq!(peers.len(), 2);
-				assert!(peers.contains(&peer_a));
-				assert!(peers.contains(&peer_c));
-
-				assert_eq!(assignments.len(), 1);
-			}
-		);
-
-		// send the an approval from peer_a
-		let approval = IndirectSignedApprovalVote {
-			block_hash: hash,
-			candidate_index,
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
-		let msg = protocol_v2::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
-		send_message_from_peer_v2(overseer, &peer_a, msg).await;
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval(
-				vote,
-				tx,
-			)) => {
-				assert_eq!(vote, approval.into());
-				tx.send(ApprovalCheckResult::Accepted).unwrap();
-			}
-		);
-
-		expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await;
-
-		// Peers b and c receive versioned approval messages.
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution(
-					protocol_v1::ApprovalDistributionMessage::Approvals(approvals)
-				))
-			)) => {
-				assert_eq!(peers, vec![peer_b]);
-				assert_eq!(approvals.len(), 1);
-			}
-		);
-
-		assert_matches!(
-			overseer_recv(overseer).await,
-			AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
-				peers,
-				Versioned::V2(protocol_v2::ValidationProtocol::ApprovalDistribution(
-					protocol_v2::ApprovalDistributionMessage::Approvals(approvals)
-				))
-			)) => {
-				assert_eq!(peers, vec![peer_c]);
-				assert_eq!(approvals.len(), 1);
-			}
-		);
-
-		// send an obviously invalid approval
-		let approval = IndirectSignedApprovalVote {
-			block_hash: hash,
-			// Invalid candidate index, should not pass sanitization.
-			candidate_index: 16777284,
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
-		let msg = protocol_v2::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
-		send_message_from_peer_v2(overseer, &peer_a, msg).await;
-
-		expect_reputation_change(overseer, &peer_a, COST_OVERSIZED_BITFIELD).await;
-
-		// send an obviously invalid approval
-		let approval = IndirectSignedApprovalVoteV2 {
-			block_hash: hash,
-			// Invalid candidates len, should not pass sanitization.
-			candidate_indices: 16777284.into(),
-			validator: validator_index,
-			signature: dummy_signature(),
-		};
-		let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
-		send_message_from_peer_v3(overseer, &peer_a, msg).await;
+			// send an obviously invalid approval
+			let approval = IndirectSignedApprovalVoteV2 {
+				block_hash: hash,
+				// Invalid candidates len, should not pass sanitization.
+				candidate_indices: 16777284.into(),
+				validator: validator_index,
+				signature: dummy_signature(),
+			};
+			let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
+			send_message_from_peer_v3(overseer, &peer_a, msg).await;
 
-		expect_reputation_change(overseer, &peer_a, COST_OVERSIZED_BITFIELD).await;
+			expect_reputation_change(overseer, &peer_a, COST_OVERSIZED_BITFIELD).await;
 
-		virtual_overseer
-	});
+			virtual_overseer
+		},
+	);
 }
 
 fn batch_test_round(message_count: usize) {
@@ -3667,11 +4128,26 @@ fn batch_test_round(message_count: usize) {
 
 	let (mut context, mut virtual_overseer) =
 		polkadot_node_subsystem_test_helpers::make_subsystem_context(pool.clone());
-	let subsystem = ApprovalDistribution::new(Default::default());
+	let subsystem = ApprovalDistribution::new_with_clock(
+		Default::default(),
+		Default::default(),
+		Box::new(SystemClock {}),
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+	);
 	let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(12345);
 	let mut sender = context.sender().clone();
-	let subsystem =
-		subsystem.run_inner(context, &mut state, REPUTATION_CHANGE_TEST_INTERVAL, &mut rng);
+	let mut session_info_provider = RuntimeInfo::new_with_config(RuntimeInfoConfig {
+		keystore: None,
+		session_cache_lru_size: DISPUTE_WINDOW.get(),
+	});
+
+	let subsystem = subsystem.run_inner(
+		context,
+		&mut state,
+		REPUTATION_CHANGE_TEST_INTERVAL,
+		&mut rng,
+		&mut session_info_provider,
+	);
 
 	let test_fut = async move {
 		let overseer = &mut virtual_overseer;
@@ -3817,3 +4293,420 @@ fn const_ensure_size_not_zero() {
 	crate::ensure_size_not_zero(super::MAX_ASSIGNMENT_BATCH_SIZE);
 	crate::ensure_size_not_zero(super::MAX_APPROVAL_BATCH_SIZE);
 }
+
+struct DummyClock;
+impl Clock for DummyClock {
+	fn tick_now(&self) -> polkadot_node_primitives::approval::time::Tick {
+		0
+	}
+
+	fn wait(
+		&self,
+		_tick: polkadot_node_primitives::approval::time::Tick,
+	) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send + 'static>> {
+		todo!()
+	}
+}
+
+/// Subsystem rejects assignments too far into the future.
+#[test]
+fn subsystem_rejects_assignment_in_future() {
+	let peers = make_peers_and_authority_ids(15);
+	let peer_a = peers.get(0).unwrap().0;
+	let parent_hash = Hash::repeat_byte(0xFF);
+	let hash = Hash::repeat_byte(0xAA);
+
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(89) }),
+		Box::new(DummyClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// setup peers
+			setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await;
+
+			// Set up a gossip topology, where a, b, c and d are topology neighbors to the node
+			// under testing.
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
+			)
+			.await;
+
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 2,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			// send the assignment related to `hash`
+			let validator_index = ValidatorIndex(0);
+			let cert = fake_assignment_cert(hash, validator_index);
+			let assignments = vec![(cert.clone(), 0u32)];
+			setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await;
+
+			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+			send_message_from_peer(overseer, &peer_a, msg).await;
+			provide_session(
+				overseer,
+				dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+			)
+			.await;
+
+			expect_reputation_change(overseer, &peer_a, COST_ASSIGNMENT_TOO_FAR_IN_THE_FUTURE)
+				.await;
+
+			virtual_overseer
+		},
+	);
+}
+
+/// Subsystem rejects bad vrf assignments.
+#[test]
+fn subsystem_rejects_bad_assignments() {
+	let peers = make_peers_and_authority_ids(15);
+	let peer_a = peers.get(0).unwrap().0;
+	let parent_hash = Hash::repeat_byte(0xFF);
+	let hash = Hash::repeat_byte(0xAA);
+
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria {
+			tranche: Err(InvalidAssignment(criteria::InvalidAssignmentReason::NullAssignment)),
+		}),
+		Box::new(DummyClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// setup peers
+			setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await;
+
+			// Set up a gossip topology, where a, b, c and d are topology neighbors to the node
+			// under testing.
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
+			)
+			.await;
+
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 2,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			// send the assignment related to `hash`
+			let validator_index = ValidatorIndex(0);
+			let cert = fake_assignment_cert(hash, validator_index);
+			let assignments = vec![(cert.clone(), 0u32)];
+			setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await;
+
+			let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
+			send_message_from_peer(overseer, &peer_a, msg).await;
+			provide_session(
+				overseer,
+				dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+			)
+			.await;
+
+			expect_reputation_change(overseer, &peer_a, COST_INVALID_MESSAGE).await;
+
+			virtual_overseer
+		},
+	);
+}
+
+/// Subsystem rejects assignments that have invalid claimed candidates.
+#[test]
+fn subsystem_rejects_wrong_claimed_assignments() {
+	let peers = make_peers_and_authority_ids(15);
+	let peer_a = peers.get(0).unwrap().0;
+	let parent_hash = Hash::repeat_byte(0xFF);
+	let hash = Hash::repeat_byte(0xAA);
+
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(DummyClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// setup peers
+			setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await;
+
+			// Set up a gossip topology, where a, b, c and d are topology neighbors to the node
+			// under testing.
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
+			)
+			.await;
+
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 2,
+				candidates: vec![Default::default(); 1],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			// send the assignment related to `hash`
+			let validator_index = ValidatorIndex(0);
+
+			// Claimed core 1 which does not have a candidate included on it, so the assignment
+			// should be rejected.
+			let cores = vec![1];
+			let core_bitfield: CoreBitfield = cores
+				.iter()
+				.map(|index| CoreIndex(*index))
+				.collect::<Vec<_>>()
+				.try_into()
+				.unwrap();
+			let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield);
+			let assignments: Vec<(IndirectAssignmentCertV2, CandidateBitfield)> =
+				vec![(cert.clone(), cores.try_into().unwrap())];
+			setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await;
+
+			let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone());
+			send_message_from_peer_v3(overseer, &peer_a, msg).await;
+			provide_session(
+				overseer,
+				dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+			)
+			.await;
+
+			expect_reputation_change(overseer, &peer_a, COST_INVALID_MESSAGE).await;
+
+			virtual_overseer
+		},
+	);
+}
+
+/// Subsystem accepts tranche0 duplicate assignments, sometimes on validator Compact tranche0
+/// assignment and Delay tranche assignments land on the same candidate. The delay tranche0 can be
+/// safely ignored and we don't need to gossip it however, the compact tranche0 assignment should be
+/// gossiped, because other candidates are included in it, this test makes sure this invariant is
+/// upheld, see  https://github.com/paritytech/polkadot/pull/2160#discussion_r557628699, for
+/// this edge case.
+#[test]
+fn subsystem_accepts_tranche0_duplicate_assignments() {
+	let peers = make_peers_and_authority_ids(15);
+	let peer_a = peers.get(0).unwrap().0;
+	let peer_b = peers.get(1).unwrap().0;
+	let parent_hash = Hash::repeat_byte(0xFF);
+	let hash = Hash::repeat_byte(0xAA);
+	let candidate_hash_first = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB));
+	let candidate_hash_second = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xCC));
+	let candidate_hash_third = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB));
+	let candidate_hash_fourth = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB));
+
+	let _ = test_harness(
+		Arc::new(MockAssignmentCriteria { tranche: Ok(0) }),
+		Box::new(DummyClock {}),
+		state_without_reputation_delay(),
+		|mut virtual_overseer| async move {
+			let overseer = &mut virtual_overseer;
+			// setup peers
+			setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V3).await;
+			setup_peer_with_view(overseer, &peer_b, view![], ValidationVersion::V3).await;
+
+			// Set up a gossip topology, where a, b, c and d are topology neighbors to the node
+			// under testing.
+			let peers_with_optional_peer_id = peers
+				.iter()
+				.map(|(peer_id, authority)| (Some(*peer_id), authority.clone()))
+				.collect_vec();
+			setup_gossip_topology(
+				overseer,
+				make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3),
+			)
+			.await;
+
+			// new block `hash_a` with 1 candidates
+			let meta = BlockApprovalMeta {
+				hash,
+				parent_hash,
+				number: 2,
+				candidates: vec![
+					(candidate_hash_first, 0.into(), 0.into()),
+					(candidate_hash_second, 1.into(), 1.into()),
+					(candidate_hash_third, 2.into(), 2.into()),
+					(candidate_hash_fourth, 3.into(), 3.into()),
+				],
+				slot: 1.into(),
+				session: 1,
+				vrf_story: RelayVRFStory(Default::default()),
+			};
+			let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
+			overseer_send(overseer, msg).await;
+
+			// send the assignment related to `hash`
+			let validator_index = ValidatorIndex(0);
+
+			setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await;
+			setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await;
+
+			// 1. Compact assignment with multiple candidates, coming after delay assignment which
+			//    covered just one of the candidate is still imported and gossiped.
+			let candidate_indices: CandidateBitfield =
+				vec![1 as CandidateIndex].try_into().unwrap();
+			let core_bitfield = vec![CoreIndex(1)].try_into().unwrap();
+			let cert = fake_assignment_cert_delay(hash, validator_index, core_bitfield);
+			let assignments: Vec<(IndirectAssignmentCertV2, CandidateBitfield)> =
+				vec![(cert.clone(), candidate_indices.clone())];
+			let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone());
+			send_message_from_peer_v3(overseer, &peer_a, msg).await;
+			provide_session(
+				overseer,
+				dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1),
+			)
+			.await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment,
+					_,
+				)) => {
+					assert_eq!(assignment.candidate_indices(), &candidate_indices);
+					assert_eq!(assignment.assignment(), &cert.into());
+					assert_eq!(assignment.tranche(), 0);
+				}
+			);
+
+			expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 1);
+					assert_eq!(assignments.len(), 1);
+				}
+			);
+
+			let candidate_indices: CandidateBitfield =
+				vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap();
+			let core_bitfield = vec![CoreIndex(0), CoreIndex(1)].try_into().unwrap();
+
+			let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield);
+
+			let assignments: Vec<(IndirectAssignmentCertV2, CandidateBitfield)> =
+				vec![(cert.clone(), candidate_indices.clone())];
+			let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone());
+			send_message_from_peer_v3(overseer, &peer_a, msg).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment,
+					_,
+				)) => {
+					assert_eq!(assignment.candidate_indices(), &candidate_indices);
+					assert_eq!(assignment.assignment(), &cert.into());
+					assert_eq!(assignment.tranche(), 0);
+				}
+			);
+
+			expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 1);
+					assert_eq!(assignments.len(), 1);
+				}
+			);
+
+			// 2. Delay assignment coming after compact assignment that already covered the
+			//    candidate is not gossiped anymore.
+			let candidate_indices: CandidateBitfield =
+				vec![2 as CandidateIndex, 3 as CandidateIndex].try_into().unwrap();
+			let core_bitfield = vec![CoreIndex(2), CoreIndex(3)].try_into().unwrap();
+			let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield);
+			let assignments: Vec<(IndirectAssignmentCertV2, CandidateBitfield)> =
+				vec![(cert.clone(), candidate_indices.clone())];
+			let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone());
+			send_message_from_peer_v3(overseer, &peer_a, msg).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment(
+					assignment,
+					_,
+				)) => {
+					assert_eq!(assignment.candidate_indices(), &candidate_indices);
+					assert_eq!(assignment.assignment(), &cert.into());
+					assert_eq!(assignment.tranche(), 0);
+				}
+			);
+
+			expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await;
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
+					peers,
+					Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution(
+						protocol_v3::ApprovalDistributionMessage::Assignments(assignments)
+					))
+				)) => {
+					assert_eq!(peers.len(), 1);
+					assert_eq!(assignments.len(), 1);
+				}
+			);
+
+			let candidate_indices: CandidateBitfield = vec![3].try_into().unwrap();
+			let core_bitfield = vec![CoreIndex(3)].try_into().unwrap();
+
+			let cert = fake_assignment_cert_delay(hash, validator_index, core_bitfield);
+
+			let assignments: Vec<(IndirectAssignmentCertV2, CandidateBitfield)> =
+				vec![(cert.clone(), candidate_indices.clone())];
+			let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone());
+			send_message_from_peer_v3(overseer, &peer_a, msg).await;
+
+			expect_reputation_change(overseer, &peer_a, COST_DUPLICATE_MESSAGE).await;
+
+			virtual_overseer
+		},
+	);
+}
diff --git a/polkadot/node/overseer/src/lib.rs b/polkadot/node/overseer/src/lib.rs
index baaff9c7c9f..26a6a907e32 100644
--- a/polkadot/node/overseer/src/lib.rs
+++ b/polkadot/node/overseer/src/lib.rs
@@ -581,6 +581,7 @@ pub struct Overseer<SupportsParachains> {
 	#[subsystem(blocking, message_capacity: 64000, ApprovalDistributionMessage, sends: [
 		NetworkBridgeTxMessage,
 		ApprovalVotingMessage,
+		RuntimeApiMessage,
 	], can_receive_priority_messages)]
 	approval_distribution: ApprovalDistribution,
 
diff --git a/polkadot/node/primitives/Cargo.toml b/polkadot/node/primitives/Cargo.toml
index cd642bf16ff..7185205f905 100644
--- a/polkadot/node/primitives/Cargo.toml
+++ b/polkadot/node/primitives/Cargo.toml
@@ -12,11 +12,13 @@ workspace = true
 [dependencies]
 bounded-vec = { workspace = true }
 futures = { workspace = true }
+futures-timer = { workspace = true }
 polkadot-primitives = { workspace = true, default-features = true }
 codec = { features = ["derive"], workspace = true }
 sp-core = { workspace = true, default-features = true }
 sp-application-crypto = { workspace = true, default-features = true }
 sp-consensus-babe = { workspace = true, default-features = true }
+sp-consensus-slots = { workspace = true }
 sp-keystore = { workspace = true, default-features = true }
 sp-maybe-compressed-blob = { workspace = true, default-features = true }
 sp-runtime = { workspace = true, default-features = true }
@@ -25,6 +27,7 @@ schnorrkel = { workspace = true, default-features = true }
 thiserror = { workspace = true }
 bitvec = { features = ["alloc"], workspace = true }
 serde = { features = ["derive"], workspace = true, default-features = true }
+sc-keystore = { workspace = true }
 
 [target.'cfg(not(target_os = "unknown"))'.dependencies]
 zstd = { version = "0.12.4", default-features = false }
diff --git a/polkadot/node/primitives/src/approval/criteria.rs b/polkadot/node/primitives/src/approval/criteria.rs
new file mode 100644
index 00000000000..0a1a0ee2367
--- /dev/null
+++ b/polkadot/node/primitives/src/approval/criteria.rs
@@ -0,0 +1,177 @@
+// Copyright (C) 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/>.
+
+//! Assignment criteria VRF generation and checking interfaces.
+
+use crate::approval::{
+	v1::{DelayTranche, RelayVRFStory},
+	v2::{AssignmentCertV2, CoreBitfield},
+};
+use codec::{Decode, Encode};
+use polkadot_primitives::{
+	AssignmentId, CandidateHash, CoreIndex, GroupIndex, IndexedVec, SessionInfo, ValidatorIndex,
+};
+use sc_keystore::LocalKeystore;
+
+use std::collections::HashMap;
+
+/// Details pertaining to our assignment on a block.
+#[derive(Debug, Clone, Encode, Decode, PartialEq)]
+pub struct OurAssignment {
+	cert: AssignmentCertV2,
+	tranche: DelayTranche,
+	validator_index: ValidatorIndex,
+	// Whether the assignment has been triggered already.
+	triggered: bool,
+}
+
+impl OurAssignment {
+	/// Create a new `OurAssignment`.
+	pub fn new(
+		cert: AssignmentCertV2,
+		tranche: DelayTranche,
+		validator_index: ValidatorIndex,
+		triggered: bool,
+	) -> Self {
+		OurAssignment { cert, tranche, validator_index, triggered }
+	}
+	/// Returns a reference to the assignment cert.
+	pub fn cert(&self) -> &AssignmentCertV2 {
+		&self.cert
+	}
+
+	/// Returns the assignment cert.
+	pub fn into_cert(self) -> AssignmentCertV2 {
+		self.cert
+	}
+
+	/// Returns the delay tranche of the assignment.
+	pub fn tranche(&self) -> DelayTranche {
+		self.tranche
+	}
+
+	/// Returns the validator index of the assignment.
+	pub fn validator_index(&self) -> ValidatorIndex {
+		self.validator_index
+	}
+
+	/// Returns whether the assignment has been triggered.
+	pub fn triggered(&self) -> bool {
+		self.triggered
+	}
+
+	/// Marks the assignment as triggered.
+	pub fn mark_triggered(&mut self) {
+		self.triggered = true;
+	}
+}
+
+/// Information about the world assignments are being produced in.
+#[derive(Clone, Debug)]
+pub struct Config {
+	/// The assignment public keys for validators.
+	pub assignment_keys: Vec<AssignmentId>,
+	/// The groups of validators assigned to each core.
+	pub validator_groups: IndexedVec<GroupIndex, Vec<ValidatorIndex>>,
+	/// The number of availability cores used by the protocol during this session.
+	pub n_cores: u32,
+	/// The zeroth delay tranche width.
+	pub zeroth_delay_tranche_width: u32,
+	/// The number of samples we do of `relay_vrf_modulo`.
+	pub relay_vrf_modulo_samples: u32,
+	/// The number of delay tranches in total.
+	pub n_delay_tranches: u32,
+}
+
+impl<'a> From<&'a SessionInfo> for Config {
+	fn from(s: &'a SessionInfo) -> Self {
+		Config {
+			assignment_keys: s.assignment_keys.clone(),
+			validator_groups: s.validator_groups.clone(),
+			n_cores: s.n_cores,
+			zeroth_delay_tranche_width: s.zeroth_delay_tranche_width,
+			relay_vrf_modulo_samples: s.relay_vrf_modulo_samples,
+			n_delay_tranches: s.n_delay_tranches,
+		}
+	}
+}
+
+/// A trait for producing and checking assignments.
+///
+/// Approval voting subsystem implements a a real implemention
+/// for it and tests use a mock implementation.
+pub trait AssignmentCriteria {
+	/// Compute the assignments for the given relay VRF story.
+	fn compute_assignments(
+		&self,
+		keystore: &LocalKeystore,
+		relay_vrf_story: RelayVRFStory,
+		config: &Config,
+		leaving_cores: Vec<(CandidateHash, CoreIndex, GroupIndex)>,
+		enable_v2_assignments: bool,
+	) -> HashMap<CoreIndex, OurAssignment>;
+
+	/// Check the assignment cert for the given relay VRF story and returns the delay tranche.
+	fn check_assignment_cert(
+		&self,
+		claimed_core_bitfield: CoreBitfield,
+		validator_index: ValidatorIndex,
+		config: &Config,
+		relay_vrf_story: RelayVRFStory,
+		assignment: &AssignmentCertV2,
+		// Backing groups for each "leaving core".
+		backing_groups: Vec<GroupIndex>,
+	) -> Result<DelayTranche, InvalidAssignment>;
+}
+
+/// Assignment invalid.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct InvalidAssignment(pub InvalidAssignmentReason);
+
+impl std::fmt::Display for InvalidAssignment {
+	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+		write!(f, "Invalid Assignment: {:?}", self.0)
+	}
+}
+
+impl std::error::Error for InvalidAssignment {}
+
+/// Failure conditions when checking an assignment cert.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum InvalidAssignmentReason {
+	/// The validator index is out of bounds.
+	ValidatorIndexOutOfBounds,
+	/// Sample index is out of bounds.
+	SampleOutOfBounds,
+	/// Core index is out of bounds.
+	CoreIndexOutOfBounds,
+	/// Invalid assignment key.
+	InvalidAssignmentKey,
+	/// Node is in backing group.
+	IsInBackingGroup,
+	/// Modulo core index mismatch.
+	VRFModuloCoreIndexMismatch,
+	/// Modulo output mismatch.
+	VRFModuloOutputMismatch,
+	/// Delay core index mismatch.
+	VRFDelayCoreIndexMismatch,
+	/// Delay output mismatch.
+	VRFDelayOutputMismatch,
+	/// Invalid arguments
+	InvalidArguments,
+	/// Assignment vrf check resulted in 0 assigned cores.
+	NullAssignment,
+}
diff --git a/polkadot/node/primitives/src/approval.rs b/polkadot/node/primitives/src/approval/mod.rs
similarity index 98%
rename from polkadot/node/primitives/src/approval.rs
rename to polkadot/node/primitives/src/approval/mod.rs
index 66883b33367..79f4cfa9e0b 100644
--- a/polkadot/node/primitives/src/approval.rs
+++ b/polkadot/node/primitives/src/approval/mod.rs
@@ -16,6 +16,12 @@
 
 //! Types relevant for approval.
 
+/// Criteria for assignment.
+pub mod criteria;
+
+/// Time utilities for approval voting.
+pub mod time;
+
 /// A list of primitives introduced in v1.
 pub mod v1 {
 	use sp_consensus_babe as babe_primitives;
@@ -25,8 +31,8 @@ pub mod v1 {
 
 	use codec::{Decode, Encode};
 	use polkadot_primitives::{
-		BlockNumber, CandidateHash, CandidateIndex, CoreIndex, Hash, Header, SessionIndex,
-		ValidatorIndex, ValidatorSignature,
+		BlockNumber, CandidateHash, CandidateIndex, CoreIndex, GroupIndex, Hash, Header,
+		SessionIndex, ValidatorIndex, ValidatorSignature,
 	};
 	use sp_application_crypto::ByteArray;
 
@@ -128,11 +134,13 @@ pub mod v1 {
 		pub parent_hash: Hash,
 		/// The candidates included by the block.
 		/// Note that these are not the same as the candidates that appear within the block body.
-		pub candidates: Vec<CandidateHash>,
+		pub candidates: Vec<(CandidateHash, CoreIndex, GroupIndex)>,
 		/// The consensus slot of the block.
 		pub slot: Slot,
 		/// The session of the block.
 		pub session: SessionIndex,
+		/// The vrf story.
+		pub vrf_story: RelayVRFStory,
 	}
 
 	/// Errors that can occur during the approvals protocol.
diff --git a/polkadot/node/core/approval-voting/src/time.rs b/polkadot/node/primitives/src/approval/time.rs
similarity index 95%
rename from polkadot/node/core/approval-voting/src/time.rs
rename to polkadot/node/primitives/src/approval/time.rs
index 5c3e7e85a17..465aae23c90 100644
--- a/polkadot/node/core/approval-voting/src/time.rs
+++ b/polkadot/node/primitives/src/approval/time.rs
@@ -14,7 +14,7 @@
 // You should have received a copy of the GNU General Public License
 // along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
 
-//! Time utilities for approval voting.
+//! Time utilities for approval voting subsystems.
 
 use futures::{
 	future::BoxFuture,
@@ -23,7 +23,7 @@ use futures::{
 	Stream, StreamExt,
 };
 
-use polkadot_node_primitives::approval::v1::DelayTranche;
+use crate::approval::v1::DelayTranche;
 use sp_consensus_slots::Slot;
 use std::{
 	collections::HashSet,
@@ -33,11 +33,15 @@ use std::{
 };
 
 use polkadot_primitives::{Hash, ValidatorIndex};
+/// The duration of a single tick in milliseconds.
 pub const TICK_DURATION_MILLIS: u64 = 500;
 
 /// A base unit of time, starting from the Unix epoch, split into half-second intervals.
 pub type Tick = u64;
 
+/// How far in the future a tick can be accepted.
+pub const TICK_TOO_FAR_IN_FUTURE: Tick = 20; // 10 seconds.
+
 /// A clock which allows querying of the current tick as well as
 /// waiting for a tick to be reached.
 pub trait Clock {
@@ -50,6 +54,7 @@ pub trait Clock {
 
 /// Extension methods for clocks.
 pub trait ClockExt {
+	/// Returns the current tranche.
 	fn tranche_now(&self, slot_duration_millis: u64, base_slot: Slot) -> DelayTranche;
 }
 
@@ -124,7 +129,7 @@ impl DelayedApprovalTimer {
 	///
 	/// Guarantees that if a timer already exits for the give block hash,
 	/// no additional timer is started.
-	pub(crate) fn maybe_arm_timer(
+	pub fn maybe_arm_timer(
 		&mut self,
 		wait_until: Tick,
 		clock: &dyn Clock,
@@ -173,7 +178,7 @@ mod tests {
 	use futures_timer::Delay;
 	use polkadot_primitives::{Hash, ValidatorIndex};
 
-	use crate::time::{Clock, SystemClock};
+	use crate::approval::time::{Clock, SystemClock};
 
 	use super::DelayedApprovalTimer;
 
diff --git a/polkadot/node/service/src/overseer.rs b/polkadot/node/service/src/overseer.rs
index add5d239364..0b57ff6e395 100644
--- a/polkadot/node/service/src/overseer.rs
+++ b/polkadot/node/service/src/overseer.rs
@@ -20,7 +20,7 @@ use polkadot_overseer::{DummySubsystem, InitializedOverseerBuilder, SubsystemErr
 use sp_core::traits::SpawnNamed;
 
 use polkadot_availability_distribution::IncomingRequestReceivers;
-use polkadot_node_core_approval_voting::Config as ApprovalVotingConfig;
+use polkadot_node_core_approval_voting::{Config as ApprovalVotingConfig, RealAssignmentCriteria};
 use polkadot_node_core_av_store::Config as AvailabilityConfig;
 use polkadot_node_core_candidate_validation::Config as CandidateValidationConfig;
 use polkadot_node_core_chain_selection::Config as ChainSelectionConfig;
@@ -309,7 +309,11 @@ where
 			Metrics::register(registry)?,
 			rand::rngs::StdRng::from_entropy(),
 		))
-		.approval_distribution(ApprovalDistributionSubsystem::new(Metrics::register(registry)?))
+		.approval_distribution(ApprovalDistributionSubsystem::new(
+			Metrics::register(registry)?,
+			approval_voting_config.slot_duration_millis,
+			Arc::new(RealAssignmentCriteria {}),
+		))
 		.approval_voting(ApprovalVotingSubsystem::with_config(
 			approval_voting_config,
 			parachains_db.clone(),
diff --git a/polkadot/node/subsystem-bench/src/lib/approval/helpers.rs b/polkadot/node/subsystem-bench/src/lib/approval/helpers.rs
index ca58875c813..4b2b9169682 100644
--- a/polkadot/node/subsystem-bench/src/lib/approval/helpers.rs
+++ b/polkadot/node/subsystem-bench/src/lib/approval/helpers.rs
@@ -16,11 +16,11 @@
 
 use crate::configuration::TestAuthorities;
 use itertools::Itertools;
-use polkadot_node_core_approval_voting::time::{Clock, SystemClock, Tick};
 use polkadot_node_network_protocol::{
 	grid_topology::{SessionGridTopology, TopologyPeerInfo},
 	View,
 };
+use polkadot_node_primitives::approval::time::{Clock, SystemClock, Tick};
 use polkadot_node_subsystem_types::messages::{
 	network_bridge_event::NewGossipTopology, ApprovalDistributionMessage, NetworkBridgeEvent,
 };
diff --git a/polkadot/node/subsystem-bench/src/lib/approval/message_generator.rs b/polkadot/node/subsystem-bench/src/lib/approval/message_generator.rs
index 6d3e7dd92db..da25a3bf3b7 100644
--- a/polkadot/node/subsystem-bench/src/lib/approval/message_generator.rs
+++ b/polkadot/node/subsystem-bench/src/lib/approval/message_generator.rs
@@ -28,16 +28,15 @@ use crate::{
 use codec::Encode;
 use futures::SinkExt;
 use itertools::Itertools;
-use polkadot_node_core_approval_voting::{
-	criteria::{compute_assignments, Config},
-	time::tranche_to_tick,
-};
+use polkadot_node_core_approval_voting::criteria::{compute_assignments, Config};
+
 use polkadot_node_network_protocol::{
 	grid_topology::{GridNeighbors, RandomRouting, RequiredRouting, SessionGridTopology},
 	v3 as protocol_v3,
 };
 use polkadot_node_primitives::approval::{
 	self,
+	time::tranche_to_tick,
 	v2::{CoreBitfield, IndirectAssignmentCertV2, IndirectSignedApprovalVoteV2},
 };
 use polkadot_primitives::{
diff --git a/polkadot/node/subsystem-bench/src/lib/approval/mock_chain_selection.rs b/polkadot/node/subsystem-bench/src/lib/approval/mock_chain_selection.rs
index 77ba80d4b2b..709d56d52f0 100644
--- a/polkadot/node/subsystem-bench/src/lib/approval/mock_chain_selection.rs
+++ b/polkadot/node/subsystem-bench/src/lib/approval/mock_chain_selection.rs
@@ -16,7 +16,7 @@
 
 use crate::approval::{ApprovalTestState, PastSystemClock, LOG_TARGET, SLOT_DURATION_MILLIS};
 use futures::FutureExt;
-use polkadot_node_core_approval_voting::time::{slot_number_to_tick, Clock, TICK_DURATION_MILLIS};
+use polkadot_node_primitives::approval::time::{slot_number_to_tick, Clock, TICK_DURATION_MILLIS};
 use polkadot_node_subsystem::{overseer, SpawnedSubsystem, SubsystemError};
 use polkadot_node_subsystem_types::messages::ChainSelectionMessage;
 
diff --git a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs
index 404df2f8c65..f05d061f3fd 100644
--- a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs
+++ b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs
@@ -49,9 +49,13 @@ use itertools::Itertools;
 use orchestra::TimeoutExt;
 use overseer::{metrics::Metrics as OverseerMetrics, MetricsTrait};
 use polkadot_approval_distribution::ApprovalDistribution;
+use polkadot_node_primitives::approval::time::{
+	slot_number_to_tick, tick_to_slot_number, Clock, ClockExt, SystemClock,
+};
+
 use polkadot_node_core_approval_voting::{
-	time::{slot_number_to_tick, tick_to_slot_number, Clock, ClockExt, SystemClock},
 	ApprovalVotingSubsystem, Config as ApprovalVotingConfig, Metrics as ApprovalVotingMetrics,
+	RealAssignmentCriteria,
 };
 use polkadot_node_network_protocol::v3 as protocol_v3;
 use polkadot_node_primitives::approval::{self, v1::RelayVRFStory};
@@ -813,8 +817,12 @@ fn build_overseer(
 		Box::new(system_clock.clone()),
 	);
 
-	let approval_distribution =
-		ApprovalDistribution::new(Metrics::register(Some(&dependencies.registry)).unwrap());
+	let approval_distribution = ApprovalDistribution::new_with_clock(
+		Metrics::register(Some(&dependencies.registry)).unwrap(),
+		SLOT_DURATION_MILLIS,
+		Box::new(system_clock.clone()),
+		Arc::new(RealAssignmentCriteria {}),
+	);
 	let mock_chain_api = MockChainApi::new(state.build_chain_api_state());
 	let mock_chain_selection = MockChainSelection { state: state.clone(), clock: system_clock };
 	let mock_runtime_api = MockRuntimeApi::new(
diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs
index d067ca46801..854a9da158b 100644
--- a/polkadot/node/subsystem-types/src/messages.rs
+++ b/polkadot/node/subsystem-types/src/messages.rs
@@ -33,7 +33,7 @@ use polkadot_node_network_protocol::{
 };
 use polkadot_node_primitives::{
 	approval::{
-		v1::BlockApprovalMeta,
+		v1::{BlockApprovalMeta, DelayTranche},
 		v2::{CandidateBitfield, IndirectAssignmentCertV2, IndirectSignedApprovalVoteV2},
 	},
 	AvailableData, BabeEpoch, BlockWeight, CandidateVotes, CollationGenerationConfig,
@@ -879,7 +879,7 @@ pub enum CollationGenerationMessage {
 	SubmitCollation(SubmitCollationParams),
 }
 
-/// The result type of [`ApprovalVotingMessage::CheckAndImportAssignment`] request.
+/// The result type of [`ApprovalVotingMessage::ImportAssignment`] request.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum AssignmentCheckResult {
 	/// The vote was accepted and should be propagated onwards.
@@ -892,7 +892,7 @@ pub enum AssignmentCheckResult {
 	Bad(AssignmentCheckError),
 }
 
-/// The error result type of [`ApprovalVotingMessage::CheckAndImportAssignment`] request.
+/// The error result type of [`ApprovalVotingMessage::ImportAssignment`] request.
 #[derive(Error, Debug, Clone, PartialEq, Eq)]
 #[allow(missing_docs)]
 pub enum AssignmentCheckError {
@@ -912,7 +912,7 @@ pub enum AssignmentCheckError {
 	InvalidBitfield(usize),
 }
 
-/// The result type of [`ApprovalVotingMessage::CheckAndImportApproval`] request.
+/// The result type of [`ApprovalVotingMessage::ImportApproval`] request.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum ApprovalCheckResult {
 	/// The vote was accepted and should be propagated onwards.
@@ -921,7 +921,7 @@ pub enum ApprovalCheckResult {
 	Bad(ApprovalCheckError),
 }
 
-/// The error result type of [`ApprovalVotingMessage::CheckAndImportApproval`] request.
+/// The error result type of [`ApprovalVotingMessage::ImportApproval`] request.
 #[derive(Error, Debug, Clone, PartialEq, Eq)]
 #[allow(missing_docs)]
 pub enum ApprovalCheckError {
@@ -969,21 +969,68 @@ pub struct HighestApprovedAncestorBlock {
 	pub descriptions: Vec<BlockDescription>,
 }
 
+/// A checked indirect assignment, the crypto for the cert has been validated
+/// and the `candidate_bitfield` is correctly claimed at `delay_tranche`.
+#[derive(Debug)]
+pub struct CheckedIndirectAssignment {
+	assignment: IndirectAssignmentCertV2,
+	candidate_indices: CandidateBitfield,
+	tranche: DelayTranche,
+}
+
+impl CheckedIndirectAssignment {
+	/// Builds a checked assignment from an assignment that was checked to be valid for the
+	/// `claimed_candidate_indices` at the give tranche
+	pub fn from_checked(
+		assignment: IndirectAssignmentCertV2,
+		claimed_candidate_indices: CandidateBitfield,
+		tranche: DelayTranche,
+	) -> Self {
+		Self { assignment, candidate_indices: claimed_candidate_indices, tranche }
+	}
+
+	/// Returns the indirect assignment.
+	pub fn assignment(&self) -> &IndirectAssignmentCertV2 {
+		&self.assignment
+	}
+
+	/// Returns the candidate bitfield claimed by the assignment.
+	pub fn candidate_indices(&self) -> &CandidateBitfield {
+		&self.candidate_indices
+	}
+
+	/// Returns the tranche this assignment is claimed at.
+	pub fn tranche(&self) -> DelayTranche {
+		self.tranche
+	}
+}
+
+/// A checked indirect signed approval vote.
+///
+/// The crypto for the vote has been validated and the signature can be trusted as being valid and
+/// to correspond to the `validator_index` inside the structure.
+#[derive(Debug, derive_more::Deref, derive_more::Into)]
+pub struct CheckedIndirectSignedApprovalVote(IndirectSignedApprovalVoteV2);
+
+impl CheckedIndirectSignedApprovalVote {
+	/// Builds a checked vote from a vote that was checked to be valid and correctly signed.
+	pub fn from_checked(vote: IndirectSignedApprovalVoteV2) -> Self {
+		Self(vote)
+	}
+}
+
 /// Message to the Approval Voting subsystem.
 #[derive(Debug)]
 pub enum ApprovalVotingMessage {
-	/// Check if the assignment is valid and can be accepted by our view of the protocol.
-	/// Should not be sent unless the block hash is known.
-	CheckAndImportAssignment(
-		IndirectAssignmentCertV2,
-		CandidateBitfield,
-		oneshot::Sender<AssignmentCheckResult>,
-	),
-	/// Check if the approval vote is valid and can be accepted by our view of the
-	/// protocol.
+	/// Import an assignment into the approval-voting database.
+	///
+	/// Should not be sent unless the block hash is known and the VRF assignment checks out.
+	ImportAssignment(CheckedIndirectAssignment, Option<oneshot::Sender<AssignmentCheckResult>>),
+	/// Import an approval vote into approval-voting database
 	///
-	/// Should not be sent unless the block hash within the indirect vote is known.
-	CheckAndImportApproval(IndirectSignedApprovalVoteV2, oneshot::Sender<ApprovalCheckResult>),
+	/// Should not be sent unless the block hash within the indirect vote is known, vote is
+	/// correctly signed and we had a previous assignment for the candidate.
+	ImportApproval(CheckedIndirectSignedApprovalVote, Option<oneshot::Sender<ApprovalCheckResult>>),
 	/// Returns the highest possible ancestor hash of the provided block hash which is
 	/// acceptable to vote on finality for.
 	/// The `BlockNumber` provided is the number of the block's ancestor which is the
diff --git a/polkadot/roadmap/implementers-guide/src/node/approval/approval-distribution.md b/polkadot/roadmap/implementers-guide/src/node/approval/approval-distribution.md
index c987b7fe5be..e09418c7d5a 100644
--- a/polkadot/roadmap/implementers-guide/src/node/approval/approval-distribution.md
+++ b/polkadot/roadmap/implementers-guide/src/node/approval/approval-distribution.md
@@ -66,8 +66,8 @@ Input:
   * `OverseerSignal::BlockFinalized`
 
 Output:
-  * `ApprovalVotingMessage::CheckAndImportAssignment`
-  * `ApprovalVotingMessage::CheckAndImportApproval`
+  * `ApprovalVotingMessage::ImportAssignment`
+  * `ApprovalVotingMessage::ImportApproval`
   * `NetworkBridgeMessage::SendValidationMessage::ApprovalDistribution`
 
 ## Functionality
@@ -253,8 +253,30 @@ The algorithm is the following:
     boost, add the fingerprint to the peer's knowledge only if it knows about the block and return. Note that we must do
     this after checking for out-of-view and if the peers knows about the block to avoid being spammed. If we did this
     check earlier, a peer could provide data out-of-view repeatedly and be rewarded for it.
-    * Dispatch `ApprovalVotingMessage::CheckAndImportAssignment(assignment)` and wait for the response.
+    * Check the assignment certificate is valid.
+      * If the cert kind is `RelayVRFModulo`, then the certificate is valid as long as `sample <
+        session_info.relay_vrf_samples` and the VRF is valid for the validator's key with the input
+        `block_entry.relay_vrf_story ++ sample.encode()` as described with
+        [the approvals protocol section](../../protocol-approval.md#assignment-criteria). We set
+        `core_index = vrf.make_bytes().to_u32() % session_info.n_cores`. If the `BlockEntry` causes
+        inclusion of a candidate at `core_index`, then this is a valid assignment for the candidate
+        at `core_index` and has delay tranche 0. Otherwise, it can be ignored.
+      * If the cert kind is `RelayVRFModuloCompact`, then the certificate is valid as long as the VRF
+        is valid for the validator's key with the input `block_entry.relay_vrf_story ++ relay_vrf_samples.encode()`
+        as described with [the approvals protocol section](../../protocol-approval.md#assignment-criteria).
+        We enforce that all `core_bitfield` indices are included in the set of the core indices sampled from the
+        VRF Output. The assignment is considered a valid tranche0 assignment for all claimed candidates if all
+        `core_bitfield` indices match the core indices where the claimed candidates were included at.
+      * If the cert kind is `RelayVRFDelay`, then we check if the VRF is valid for the validator's key with the
+        input `block_entry.relay_vrf_story ++ cert.core_index.encode()` as described in [the approvals protocol
+        section](../../protocol-approval.md#assignment-criteria). The cert can be ignored if the block did not
+        cause inclusion of a candidate on that core index. Otherwise, this is a valid assignment for the included
+        candidate. The delay tranche for the assignment is determined by reducing
+        `(vrf.make_bytes().to_u64() % (session_info.n_delay_tranches + session_info.zeroth_delay_tranche_width)).saturating_sub(session_info.zeroth_delay_tranche_width)`.
+      * We also check that the core index derived by the output is covered by the `VRFProof` by means of an auxiliary signature.
+      * If the delay tranche is too far in the future, return `AssignmentCheckResult::TooFarInFuture`.
     * If the result is `AssignmentCheckResult::Accepted`
+      * Dispatch `ApprovalVotingMessage::ImportAssignment(assignment)` to approval-voting to import the assignment.
       * If the vote was accepted but not duplicate, give the peer a positive reputation boost
       * add the fingerprint to both our and the peer's knowledge in the `BlockEntry`. Note that we only doing this after
         making sure we have the right fingerprint.
@@ -293,10 +315,12 @@ Imports an approval signature referenced by block hash and candidate index:
     boost, add the fingerprint to the peer's knowledge only if it knows about the block and return. Note that we must do
     this after checking for out-of-view to avoid being spammed. If we did this check earlier, a peer could provide data
     out-of-view repeatedly and be rewarded for it.
-    * Dispatch `ApprovalVotingMessage::CheckAndImportApproval(approval)` and wait for the response.
-    * If the result is `VoteCheckResult::Accepted(())`:
+    * Construct a `SignedApprovalVote` using the candidates hashes and check against the validator's approval key,
+      based on the session info of the block. If invalid or no such validator, return `Err(InvalidVoteError)`.
+    * If the result of checking the signature is `Ok(CheckedIndirectSignedApprovalVote)`:
+      * Dispatch `ApprovalVotingMessage::ImportApproval(approval)` .
       * Give the peer a positive reputation boost and add the fingerprint to both our and the peer's knowledge.
-    * If the result is `VoteCheckResult::Bad`:
+    * If the result is `Err(InvalidVoteError)`:
       * Report the peer and return.
   * Load the candidate entry for the given candidate index. It should exist unless there is a logic error in the
     approval voting subsystem.
diff --git a/polkadot/roadmap/implementers-guide/src/node/approval/approval-voting.md b/polkadot/roadmap/implementers-guide/src/node/approval/approval-voting.md
index 9b4082c49e2..40394412d81 100644
--- a/polkadot/roadmap/implementers-guide/src/node/approval/approval-voting.md
+++ b/polkadot/roadmap/implementers-guide/src/node/approval/approval-voting.md
@@ -39,8 +39,8 @@ been approved.
 ## Protocol
 
 Input:
-  * `ApprovalVotingMessage::CheckAndImportAssignment`
-  * `ApprovalVotingMessage::CheckAndImportApproval`
+  * `ApprovalVotingMessage::ImportAssignment`
+  * `ApprovalVotingMessage::ImportApproval`
   * `ApprovalVotingMessage::ApprovedAncestor`
 
 Output:
@@ -266,39 +266,17 @@ On receiving an `OverseerSignal::ActiveLeavesUpdate(update)`:
     0-tranche assignment, kick off approval work, and schedule the next delay.
   * Dispatch an `ApprovalDistributionMessage::NewBlocks` with the meta information filled out for each new block.
 
-#### `ApprovalVotingMessage::CheckAndImportAssignment`
+#### `ApprovalVotingMessage::ImportAssignment`
 
-On receiving a `ApprovalVotingMessage::CheckAndImportAssignment` message, we check the assignment cert against the block
-entry. The cert itself contains information necessary to determine the candidate that is being assigned-to. In detail:
+On receiving a `ApprovalVotingMessage::ImportAssignment` message, we assume the assignment cert itself has already been
+checked to be valid we proceed then to import the assignment inside the block entry. The cert itself contains
+information necessary to determine the candidate that is being assigned-to. In detail:
   * Load the `BlockEntry` for the relay-parent referenced by the message. If there is none, return
     `AssignmentCheckResult::Bad`.
   * Fetch the `SessionInfo` for the session of the block
   * Determine the assignment key of the validator based on that.
   * Determine the claimed core index by looking up the candidate with given index in `block_entry.candidates`. Return
     `AssignmentCheckResult::Bad` if missing.
-  * Check the assignment cert
-    * If the cert kind is `RelayVRFModulo`, then the certificate is valid as long as `sample <
-      session_info.relay_vrf_samples` and the VRF is valid for the validator's key with the input
-      `block_entry.relay_vrf_story ++ sample.encode()` as described with
-      [the approvals protocol section](../../protocol-approval.md#assignment-criteria). We set
-      `core_index = vrf.make_bytes().to_u32() % session_info.n_cores`. If the `BlockEntry` causes
-      inclusion of a candidate at `core_index`, then this is a valid assignment for the candidate
-      at `core_index` and has delay tranche 0. Otherwise, it can be ignored.
-    * If the cert kind is `RelayVRFModuloCompact`, then the certificate is valid as long as the VRF
-      is valid for the validator's key with the input `block_entry.relay_vrf_story ++ relay_vrf_samples.encode()`
-      as described with [the approvals protocol section](../../protocol-approval.md#assignment-criteria).
-      We enforce that all `core_bitfield` indices are included in the set of the core indices sampled from the
-      VRF Output. The assignment is considered a valid tranche0 assignment for all claimed candidates if all
-      `core_bitfield` indices match the core indices where the claimed candidates were included at.
-
-    * If the cert kind is `RelayVRFDelay`, then we check if the VRF is valid for the validator's key with the
-      input `block_entry.relay_vrf_story ++ cert.core_index.encode()` as described in [the approvals protocol
-      section](../../protocol-approval.md#assignment-criteria). The cert can be ignored if the block did not
-      cause inclusion of a candidate on that core index. Otherwise, this is a valid assignment for the included
-      candidate. The delay tranche for the assignment is determined by reducing
-      `(vrf.make_bytes().to_u64() % (session_info.n_delay_tranches + session_info.zeroth_delay_tranche_width)).saturating_sub(session_info.zeroth_delay_tranche_width)`.
-    * We also check that the core index derived by the output is covered by the `VRFProof` by means of an auxiliary signature.
-    * If the delay tranche is too far in the future, return `AssignmentCheckResult::TooFarInFuture`.
   * Import the assignment.
     * Load the candidate in question and access the `approval_entry` for the block hash the cert references.
     * Ignore if we already observe the validator as having been assigned.
@@ -309,14 +287,12 @@ entry. The cert itself contains information necessary to determine the candidate
   * [Schedule a wakeup](#schedule-wakeup) for this block, candidate pair.
   * return the appropriate `AssignmentCheckResult` on the response channel.
 
-#### `ApprovalVotingMessage::CheckAndImportApproval`
+#### `ApprovalVotingMessage::ImportApproval`
 
-On receiving a `CheckAndImportApproval(indirect_approval_vote, response_channel)` message:
+On receiving a `ImportApproval(indirect_approval_vote, response_channel)` message:
   * Fetch the `BlockEntry` from the indirect approval vote's `block_hash`. If none, return `ApprovalCheckResult::Bad`.
   * Fetch all `CandidateEntry` from the indirect approval vote's `candidate_indices`. If the block did not trigger
     inclusion of enough candidates, return `ApprovalCheckResult::Bad`.
-  * Construct a `SignedApprovalVote` using the candidates hashes and check against the validator's approval key,
-    based on the session info of the block. If invalid or no such validator, return `ApprovalCheckResult::Bad`.
   * Send `ApprovalCheckResult::Accepted`
   * [Import the checked approval vote](#import-checked-approval) for all candidates
 
diff --git a/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md b/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md
index c82d89d2d87..317f339ddd4 100644
--- a/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md
+++ b/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md
@@ -111,21 +111,15 @@ pub enum ApprovalCheckError {
 }
 
 enum ApprovalVotingMessage {
-    /// Check if the assignment is valid and can be accepted by our view of the protocol.
-    /// Should not be sent unless the block hash is known.
-    CheckAndImportAssignment(
-        IndirectAssignmentCert,
-        CandidateIndex, // The index of the candidate included in the block.
-        ResponseChannel<AssignmentCheckResult>,
-    ),
-    /// Check if the approval vote is valid and can be accepted by our view of the
-    /// protocol.
-    ///
-    /// Should not be sent unless the block hash within the indirect vote is known.
-    CheckAndImportApproval(
-        IndirectSignedApprovalVote,
-        ResponseChannel<ApprovalCheckResult>,
-    ),
+	/// Import an assignment into the approval-voting database.
+	///
+	/// Should not be sent unless the block hash is known and the VRF assignment checks out.
+	ImportAssignment(CheckedIndirectAssignment, Option<oneshot::Sender<AssignmentCheckResult>>),
+	/// Import an approval vote into approval-voting database
+	///
+	/// Should not be sent unless the block hash within the indirect vote is known, vote is
+	/// correctly signed and we had a previous assignment for the candidate.
+	ImportApproval(CheckedIndirectSignedApprovalVote, Option<oneshot::Sender<ApprovalCheckResult>>),
     /// Returns the highest possible ancestor hash of the provided block hash which is
     /// acceptable to vote on finality for. Along with that, return the lists of candidate hashes
     /// which appear in every block from the (non-inclusive) base number up to (inclusive) the specified
diff --git a/prdoc/pr_4928.prdoc b/prdoc/pr_4928.prdoc
new file mode 100644
index 00000000000..9935652dc51
--- /dev/null
+++ b/prdoc/pr_4928.prdoc
@@ -0,0 +1,28 @@
+# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0
+# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json
+
+title: Move assignment VRF check and vote signature in approval-distribution
+
+doc:
+  - audience: Node Dev
+    description: |
+      This PR moves the assignment VRF check and approval vote signature from approval-voting into approval-distribution.
+      This optimization creates a better pipelining for processing new messages, because in this way approval-distribution
+      does not have to wait after approval-voting anymore and it will just notify it when it received a valid message that
+      is ready to be imported.
+
+crates: 
+  - name: polkadot-node-subsystem-types
+    bump: major
+  - name: polkadot-approval-distribution
+    bump: major
+  - name: polkadot-node-core-approval-voting
+    bump: major
+  - name: polkadot-node-primitives
+    bump: major
+  - name: polkadot-service
+    bump: major
+  - name: polkadot-subsystem-bench
+    bump: major
+  - name: polkadot-overseer
+    bump: patch
\ No newline at end of file
-- 
GitLab