From 84c932cd8ace8e389f753608e7a796f77ccdb00d Mon Sep 17 00:00:00 2001
From: Alexandru Gheorghe <49718502+alexggh@users.noreply.github.com>
Date: Mon, 11 Dec 2023 12:49:11 +0200
Subject: [PATCH] Add feature flag to enable v2 assignments (#2444)

Scaffold everything, so that we can enable v2 assignments via a node
feature bit, once all nodes have upgraded to the new protocol.

Implements: https://github.com/paritytech/polkadot-sdk/issues/628

---------

Signed-off-by: Alexandru Gheorghe <alexandru.gheorghe@parity.io>
---
 .../node/core/approval-voting/src/criteria.rs |   4 +-
 .../node/core/approval-voting/src/import.rs   | 339 ++++++++++--------
 .../node/core/approval-voting/src/tests.rs    |  14 +-
 .../core/dispute-coordinator/src/tests.rs     |  43 ++-
 .../src/pov_requester/mod.rs                  |  10 +-
 .../src/requester/tests.rs                    |   8 +-
 .../src/tests/state.rs                        |   7 +-
 .../src/collator_side/tests/mod.rs            |  12 +-
 .../dispute-distribution/src/tests/mod.rs     |  22 +-
 .../src/legacy_v1/tests.rs                    |  57 ++-
 .../node/subsystem-util/src/runtime/mod.rs    |  27 +-
 polkadot/primitives/src/vstaging/mod.rs       |  16 +
 12 files changed, 393 insertions(+), 166 deletions(-)

diff --git a/polkadot/node/core/approval-voting/src/criteria.rs b/polkadot/node/core/approval-voting/src/criteria.rs
index acad1c66a43..b6ad1971ef6 100644
--- a/polkadot/node/core/approval-voting/src/criteria.rs
+++ b/polkadot/node/core/approval-voting/src/criteria.rs
@@ -261,6 +261,7 @@ pub(crate) trait AssignmentCriteria {
 		relay_vrf_story: RelayVRFStory,
 		config: &Config,
 		leaving_cores: Vec<(CandidateHash, CoreIndex, GroupIndex)>,
+		enable_v2_assignments: bool,
 	) -> HashMap<CoreIndex, OurAssignment>;
 
 	fn check_assignment_cert(
@@ -284,8 +285,9 @@ impl AssignmentCriteria for RealAssignmentCriteria {
 		relay_vrf_story: RelayVRFStory,
 		config: &Config,
 		leaving_cores: Vec<(CandidateHash, CoreIndex, GroupIndex)>,
+		enable_v2_assignments: bool,
 	) -> HashMap<CoreIndex, OurAssignment> {
-		compute_assignments(keystore, relay_vrf_story, config, leaving_cores, false)
+		compute_assignments(keystore, relay_vrf_story, config, leaving_cores, enable_v2_assignments)
 	}
 
 	fn check_assignment_cert(
diff --git a/polkadot/node/core/approval-voting/src/import.rs b/polkadot/node/core/approval-voting/src/import.rs
index d7667e8e405..a64685326cf 100644
--- a/polkadot/node/core/approval-voting/src/import.rs
+++ b/polkadot/node/core/approval-voting/src/import.rs
@@ -45,8 +45,8 @@ use polkadot_node_subsystem::{
 };
 use polkadot_node_subsystem_util::{determine_new_blocks, runtime::RuntimeInfo};
 use polkadot_primitives::{
-	BlockNumber, CandidateEvent, CandidateHash, CandidateReceipt, ConsensusLog, CoreIndex,
-	GroupIndex, Hash, Header, SessionIndex,
+	vstaging::node_features, BlockNumber, CandidateEvent, CandidateHash, CandidateReceipt,
+	ConsensusLog, CoreIndex, GroupIndex, Hash, Header, SessionIndex,
 };
 use sc_keystore::LocalKeystore;
 use sp_consensus_slots::Slot;
@@ -60,7 +60,7 @@ use super::approval_db::v2;
 use crate::{
 	backend::{Backend, OverlayedBackend},
 	criteria::{AssignmentCriteria, OurAssignment},
-	get_session_info,
+	get_extended_session_info, get_session_info,
 	persisted_entries::CandidateEntry,
 	time::{slot_number_to_tick, Tick},
 };
@@ -214,10 +214,21 @@ async fn imported_block_info<Context>(
 		}
 	};
 
+	let extended_session_info =
+		get_extended_session_info(env.runtime_info, ctx.sender(), block_hash, session_index).await;
+	let enable_v2_assignments = extended_session_info.map_or(false, |extended_session_info| {
+		*extended_session_info
+			.node_features
+			.get(node_features::FeatureIndex::EnableAssignmentsV2 as usize)
+			.as_deref()
+			.unwrap_or(&false)
+	});
+
 	let session_info = get_session_info(env.runtime_info, ctx.sender(), block_hash, session_index)
 		.await
 		.ok_or(ImportedBlockInfoError::SessionInfoUnavailable)?;
 
+	gum::debug!(target: LOG_TARGET, ?enable_v2_assignments, "V2 assignments");
 	let (assignments, slot, relay_vrf_story) = {
 		let unsafe_vrf = approval_types::v1::babe_unsafe_vrf_info(&block_header);
 
@@ -239,6 +250,7 @@ async fn imported_block_info<Context>(
 								.iter()
 								.map(|(c_hash, _, core, group)| (*c_hash, *core, *group))
 								.collect(),
+							enable_v2_assignments,
 						);
 
 						(assignments, slot, relay_vrf)
@@ -603,6 +615,7 @@ pub(crate) mod tests {
 	use polkadot_node_subsystem_test_helpers::make_subsystem_context;
 	use polkadot_node_subsystem_util::database::Database;
 	use polkadot_primitives::{
+		vstaging::{node_features::FeatureIndex, NodeFeatures},
 		ExecutorParams, Id as ParaId, IndexedVec, SessionInfo, ValidatorId, ValidatorIndex,
 	};
 	pub(crate) use sp_consensus_babe::{
@@ -639,7 +652,7 @@ pub(crate) mod tests {
 			keystore: Arc::new(LocalKeystore::in_memory()),
 			slot_duration_millis: 6_000,
 			clock: Box::new(MockClock::default()),
-			assignment_criteria: Box::new(MockAssignmentCriteria),
+			assignment_criteria: Box::new(MockAssignmentCriteria::default()),
 			spans: HashMap::new(),
 		}
 	}
@@ -654,7 +667,10 @@ pub(crate) mod tests {
 		)
 	}
 
-	struct MockAssignmentCriteria;
+	#[derive(Default)]
+	struct MockAssignmentCriteria {
+		enable_v2: bool,
+	}
 
 	impl AssignmentCriteria for MockAssignmentCriteria {
 		fn compute_assignments(
@@ -667,7 +683,9 @@ pub(crate) mod tests {
 				polkadot_primitives::CoreIndex,
 				polkadot_primitives::GroupIndex,
 			)>,
+			enable_assignments_v2: bool,
 		) -> HashMap<polkadot_primitives::CoreIndex, criteria::OurAssignment> {
+			assert_eq!(enable_assignments_v2, self.enable_v2);
 			HashMap::new()
 		}
 
@@ -711,154 +729,164 @@ pub(crate) mod tests {
 
 	#[test]
 	fn imported_block_info_is_good() {
-		let pool = TaskExecutor::new();
-		let (mut ctx, mut handle) =
-			make_subsystem_context::<ApprovalVotingMessage, _>(pool.clone());
-
-		let session = 5;
-		let session_info = dummy_session_info(session);
-
-		let slot = Slot::from(10);
-
-		let header = Header {
-			digest: {
-				let mut d = Digest::default();
-				let vrf_signature = garbage_vrf_signature();
-				d.push(DigestItem::babe_pre_digest(PreDigest::SecondaryVRF(
-					SecondaryVRFPreDigest { authority_index: 0, slot, vrf_signature },
-				)));
-
-				d
-			},
-			extrinsics_root: Default::default(),
-			number: 5,
-			state_root: Default::default(),
-			parent_hash: Default::default(),
-		};
-
-		let hash = header.hash();
-		let make_candidate = |para_id| {
-			let mut r = dummy_candidate_receipt(dummy_hash());
-			r.descriptor.para_id = para_id;
-			r.descriptor.relay_parent = hash;
-			r
-		};
-		let candidates = vec![
-			(make_candidate(1.into()), CoreIndex(0), GroupIndex(2)),
-			(make_candidate(2.into()), CoreIndex(1), GroupIndex(3)),
-		];
+		for enable_v2 in [false, true] {
+			let pool = TaskExecutor::new();
+			let (mut ctx, mut handle) =
+				make_subsystem_context::<ApprovalVotingMessage, _>(pool.clone());
+
+			let session = 5;
+			let session_info = dummy_session_info(session);
+
+			let slot = Slot::from(10);
+			let header = Header {
+				digest: {
+					let mut d = Digest::default();
+					let vrf_signature = garbage_vrf_signature();
+					d.push(DigestItem::babe_pre_digest(PreDigest::SecondaryVRF(
+						SecondaryVRFPreDigest { authority_index: 0, slot, vrf_signature },
+					)));
+
+					d
+				},
+				extrinsics_root: Default::default(),
+				number: 5,
+				state_root: Default::default(),
+				parent_hash: Default::default(),
+			};
 
-		let inclusion_events = candidates
-			.iter()
-			.cloned()
-			.map(|(r, c, g)| CandidateEvent::CandidateIncluded(r, Vec::new().into(), c, g))
-			.collect::<Vec<_>>();
+			let hash = header.hash();
+			let make_candidate = |para_id| {
+				let mut r = dummy_candidate_receipt(dummy_hash());
+				r.descriptor.para_id = para_id;
+				r.descriptor.relay_parent = hash;
+				r
+			};
+			let candidates = vec![
+				(make_candidate(1.into()), CoreIndex(0), GroupIndex(2)),
+				(make_candidate(2.into()), CoreIndex(1), GroupIndex(3)),
+			];
 
-		let test_fut = {
-			let included_candidates = candidates
+			let inclusion_events = candidates
 				.iter()
-				.map(|(r, c, g)| (r.hash(), r.clone(), *c, *g))
+				.cloned()
+				.map(|(r, c, g)| CandidateEvent::CandidateIncluded(r, Vec::new().into(), c, g))
 				.collect::<Vec<_>>();
 
-			let mut runtime_info = RuntimeInfo::new_with_config(RuntimeInfoConfig {
-				keystore: None,
-				session_cache_lru_size: DISPUTE_WINDOW.get(),
-			});
+			let test_fut = {
+				let included_candidates = candidates
+					.iter()
+					.map(|(r, c, g)| (r.hash(), r.clone(), *c, *g))
+					.collect::<Vec<_>>();
+
+				let mut runtime_info = RuntimeInfo::new_with_config(RuntimeInfoConfig {
+					keystore: None,
+					session_cache_lru_size: DISPUTE_WINDOW.get(),
+				});
+
+				let header = header.clone();
+				Box::pin(async move {
+					let env = ImportedBlockInfoEnv {
+						runtime_info: &mut runtime_info,
+						assignment_criteria: &MockAssignmentCriteria { enable_v2 },
+						keystore: &LocalKeystore::in_memory(),
+					};
 
-			let header = header.clone();
-			Box::pin(async move {
-				let env = ImportedBlockInfoEnv {
-					runtime_info: &mut runtime_info,
-					assignment_criteria: &MockAssignmentCriteria,
-					keystore: &LocalKeystore::in_memory(),
-				};
+					let info =
+						imported_block_info(&mut ctx, env, hash, &header, &Some(4)).await.unwrap();
 
-				let info =
-					imported_block_info(&mut ctx, env, hash, &header, &Some(4)).await.unwrap();
+					assert_eq!(info.included_candidates, included_candidates);
+					assert_eq!(info.session_index, session);
+					assert!(info.assignments.is_empty());
+					assert_eq!(info.n_validators, 0);
+					assert_eq!(info.slot, slot);
+					assert!(info.force_approve.is_none());
+				})
+			};
 
-				assert_eq!(info.included_candidates, included_candidates);
-				assert_eq!(info.session_index, session);
-				assert!(info.assignments.is_empty());
-				assert_eq!(info.n_validators, 0);
-				assert_eq!(info.slot, slot);
-				assert!(info.force_approve.is_none());
-			})
-		};
+			let aux_fut = Box::pin(async move {
+				assert_matches!(
+					handle.recv().await,
+					AllMessages::RuntimeApi(RuntimeApiMessage::Request(
+						h,
+						RuntimeApiRequest::CandidateEvents(c_tx),
+					)) => {
+						assert_eq!(h, hash);
+						let _ = c_tx.send(Ok(inclusion_events));
+					}
+				);
 
-		let aux_fut = Box::pin(async move {
-			assert_matches!(
-				handle.recv().await,
-				AllMessages::RuntimeApi(RuntimeApiMessage::Request(
-					h,
-					RuntimeApiRequest::CandidateEvents(c_tx),
-				)) => {
-					assert_eq!(h, hash);
-					let _ = c_tx.send(Ok(inclusion_events));
-				}
-			);
+				assert_matches!(
+					handle.recv().await,
+					AllMessages::RuntimeApi(RuntimeApiMessage::Request(
+						h,
+						RuntimeApiRequest::SessionIndexForChild(c_tx),
+					)) => {
+						assert_eq!(h, header.parent_hash);
+						let _ = c_tx.send(Ok(session));
+					}
+				);
 
-			assert_matches!(
-				handle.recv().await,
-				AllMessages::RuntimeApi(RuntimeApiMessage::Request(
-					h,
-					RuntimeApiRequest::SessionIndexForChild(c_tx),
-				)) => {
-					assert_eq!(h, header.parent_hash);
-					let _ = c_tx.send(Ok(session));
-				}
-			);
+				assert_matches!(
+					handle.recv().await,
+					AllMessages::RuntimeApi(RuntimeApiMessage::Request(
+						h,
+						RuntimeApiRequest::CurrentBabeEpoch(c_tx),
+					)) => {
+						assert_eq!(h, hash);
+						let _ = c_tx.send(Ok(BabeEpoch {
+							epoch_index: session as _,
+							start_slot: Slot::from(0),
+							duration: 200,
+							authorities: vec![(Sr25519Keyring::Alice.public().into(), 1)],
+							randomness: [0u8; 32],
+							config: BabeEpochConfiguration {
+								c: (1, 4),
+								allowed_slots: AllowedSlots::PrimarySlots,
+							},
+						}));
+					}
+				);
 
-			assert_matches!(
-				handle.recv().await,
-				AllMessages::RuntimeApi(RuntimeApiMessage::Request(
-					h,
-					RuntimeApiRequest::CurrentBabeEpoch(c_tx),
-				)) => {
-					assert_eq!(h, hash);
-					let _ = c_tx.send(Ok(BabeEpoch {
-						epoch_index: session as _,
-						start_slot: Slot::from(0),
-						duration: 200,
-						authorities: vec![(Sr25519Keyring::Alice.public().into(), 1)],
-						randomness: [0u8; 32],
-						config: BabeEpochConfiguration {
-							c: (1, 4),
-							allowed_slots: AllowedSlots::PrimarySlots,
-						},
-					}));
-				}
-			);
+				assert_matches!(
+					handle.recv().await,
+					AllMessages::RuntimeApi(
+						RuntimeApiMessage::Request(
+							req_block_hash,
+							RuntimeApiRequest::SessionInfo(idx, si_tx),
+						)
+					) => {
+						assert_eq!(session, idx);
+						assert_eq!(req_block_hash, hash);
+						si_tx.send(Ok(Some(session_info.clone()))).unwrap();
+					}
+				);
 
-			assert_matches!(
-				handle.recv().await,
-				AllMessages::RuntimeApi(
-					RuntimeApiMessage::Request(
-						req_block_hash,
-						RuntimeApiRequest::SessionInfo(idx, si_tx),
-					)
-				) => {
-					assert_eq!(session, idx);
-					assert_eq!(req_block_hash, hash);
-					si_tx.send(Ok(Some(session_info.clone()))).unwrap();
-				}
-			);
+				assert_matches!(
+					handle.recv().await,
+					AllMessages::RuntimeApi(
+						RuntimeApiMessage::Request(
+							req_block_hash,
+							RuntimeApiRequest::SessionExecutorParams(idx, si_tx),
+						)
+					) => {
+						assert_eq!(session, idx);
+						assert_eq!(req_block_hash, hash);
+						si_tx.send(Ok(Some(ExecutorParams::default()))).unwrap();
+					}
+				);
 
-			assert_matches!(
-				handle.recv().await,
-				AllMessages::RuntimeApi(
-					RuntimeApiMessage::Request(
-						req_block_hash,
-						RuntimeApiRequest::SessionExecutorParams(idx, si_tx),
-					)
-				) => {
-					assert_eq!(session, idx);
-					assert_eq!(req_block_hash, hash);
-					si_tx.send(Ok(Some(ExecutorParams::default()))).unwrap();
-				}
-			);
-		});
+				assert_matches!(
+					handle.recv().await,
+					AllMessages::RuntimeApi(
+						RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+					) => {
+						si_tx.send(Ok(NodeFeatures::repeat(enable_v2, FeatureIndex::EnableAssignmentsV2 as usize + 1))).unwrap();
+					}
+				);
+			});
 
-		futures::executor::block_on(futures::future::join(test_fut, aux_fut));
+			futures::executor::block_on(futures::future::join(test_fut, aux_fut));
+		}
 	}
 
 	#[test]
@@ -906,7 +934,7 @@ pub(crate) mod tests {
 			Box::pin(async move {
 				let env = ImportedBlockInfoEnv {
 					runtime_info: &mut runtime_info,
-					assignment_criteria: &MockAssignmentCriteria,
+					assignment_criteria: &MockAssignmentCriteria::default(),
 					keystore: &LocalKeystore::in_memory(),
 				};
 
@@ -987,6 +1015,15 @@ pub(crate) mod tests {
 					si_tx.send(Ok(Some(ExecutorParams::default()))).unwrap();
 				}
 			);
+
+			assert_matches!(
+				handle.recv().await,
+				AllMessages::RuntimeApi(
+					RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+				) => {
+					si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+				}
+			);
 		});
 
 		futures::executor::block_on(futures::future::join(test_fut, aux_fut));
@@ -1036,7 +1073,7 @@ pub(crate) mod tests {
 			Box::pin(async move {
 				let env = ImportedBlockInfoEnv {
 					runtime_info: &mut runtime_info,
-					assignment_criteria: &MockAssignmentCriteria,
+					assignment_criteria: &MockAssignmentCriteria::default(),
 					keystore: &LocalKeystore::in_memory(),
 				};
 
@@ -1134,7 +1171,7 @@ pub(crate) mod tests {
 			Box::pin(async move {
 				let env = ImportedBlockInfoEnv {
 					runtime_info: &mut runtime_info,
-					assignment_criteria: &MockAssignmentCriteria,
+					assignment_criteria: &MockAssignmentCriteria::default(),
 					keystore: &LocalKeystore::in_memory(),
 				};
 
@@ -1221,6 +1258,15 @@ pub(crate) mod tests {
 					si_tx.send(Ok(Some(ExecutorParams::default()))).unwrap();
 				}
 			);
+
+			assert_matches!(
+				handle.recv().await,
+				AllMessages::RuntimeApi(
+					RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+				) => {
+					si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+				}
+			);
 		});
 
 		futures::executor::block_on(futures::future::join(test_fut, aux_fut));
@@ -1438,6 +1484,15 @@ pub(crate) mod tests {
 				}
 			);
 
+			assert_matches!(
+				handle.recv().await,
+				AllMessages::RuntimeApi(
+					RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+				) => {
+					si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+				}
+			);
+
 			assert_matches!(
 				handle.recv().await,
 				AllMessages::ApprovalDistribution(ApprovalDistributionMessage::NewBlocks(
diff --git a/polkadot/node/core/approval-voting/src/tests.rs b/polkadot/node/core/approval-voting/src/tests.rs
index e1ec194cb54..cdfc170cd2b 100644
--- a/polkadot/node/core/approval-voting/src/tests.rs
+++ b/polkadot/node/core/approval-voting/src/tests.rs
@@ -37,8 +37,8 @@ use polkadot_node_subsystem_test_helpers as test_helpers;
 use polkadot_node_subsystem_util::TimeoutExt;
 use polkadot_overseer::HeadSupportsParachains;
 use polkadot_primitives::{
-	CandidateCommitments, CandidateEvent, CoreIndex, GroupIndex, Header, Id as ParaId, IndexedVec,
-	ValidationCode, ValidatorSignature,
+	vstaging::NodeFeatures, CandidateCommitments, CandidateEvent, CoreIndex, GroupIndex, Header,
+	Id as ParaId, IndexedVec, ValidationCode, ValidatorSignature,
 };
 use std::time::Duration;
 
@@ -243,6 +243,7 @@ where
 			polkadot_primitives::CoreIndex,
 			polkadot_primitives::GroupIndex,
 		)>,
+		_enable_assignments_v2: bool,
 	) -> HashMap<polkadot_primitives::CoreIndex, criteria::OurAssignment> {
 		self.0()
 	}
@@ -1003,6 +1004,15 @@ async fn import_block(
 					si_tx.send(Ok(Some(ExecutorParams::default()))).unwrap();
 				}
 			);
+
+			assert_matches!(
+				overseer_recv(overseer).await,
+				AllMessages::RuntimeApi(
+					RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+				) => {
+					si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+				}
+			);
 		}
 
 		assert_matches!(
diff --git a/polkadot/node/core/dispute-coordinator/src/tests.rs b/polkadot/node/core/dispute-coordinator/src/tests.rs
index 9254c2a851c..6912bbf67e1 100644
--- a/polkadot/node/core/dispute-coordinator/src/tests.rs
+++ b/polkadot/node/core/dispute-coordinator/src/tests.rs
@@ -61,10 +61,11 @@ use polkadot_node_subsystem_test_helpers::{
 	make_buffered_subsystem_context, mock::new_leaf, TestSubsystemContextHandle,
 };
 use polkadot_primitives::{
-	ApprovalVote, BlockNumber, CandidateCommitments, CandidateEvent, CandidateHash,
-	CandidateReceipt, CoreIndex, DisputeStatement, ExecutorParams, GroupIndex, Hash, HeadData,
-	Header, IndexedVec, MultiDisputeStatementSet, ScrapedOnChainVotes, SessionIndex, SessionInfo,
-	SigningContext, ValidDisputeStatementKind, ValidatorId, ValidatorIndex, ValidatorSignature,
+	vstaging::NodeFeatures, ApprovalVote, BlockNumber, CandidateCommitments, CandidateEvent,
+	CandidateHash, CandidateReceipt, CoreIndex, DisputeStatement, ExecutorParams, GroupIndex, Hash,
+	HeadData, Header, IndexedVec, MultiDisputeStatementSet, ScrapedOnChainVotes, SessionIndex,
+	SessionInfo, SigningContext, ValidDisputeStatementKind, ValidatorId, ValidatorIndex,
+	ValidatorSignature,
 };
 
 use crate::{
@@ -352,6 +353,15 @@ impl TestState {
 									let _ = tx.send(Ok(Some(ExecutorParams::default())));
 								}
 							);
+
+							assert_matches!(
+								overseer_recv(virtual_overseer).await,
+								AllMessages::RuntimeApi(
+									RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+								) => {
+									si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+								}
+							);
 						}
 					}
 
@@ -3492,6 +3502,14 @@ fn session_info_is_requested_only_once() {
 					let _ = tx.send(Ok(Some(ExecutorParams::default())));
 				}
 			);
+			assert_matches!(
+				virtual_overseer.recv().await,
+				AllMessages::RuntimeApi(
+					RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+				) => {
+					si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+				}
+			);
 			test_state
 		})
 	});
@@ -3552,6 +3570,15 @@ fn session_info_big_jump_works() {
 						let _ = tx.send(Ok(Some(ExecutorParams::default())));
 					}
 				);
+
+				assert_matches!(
+					virtual_overseer.recv().await,
+					AllMessages::RuntimeApi(
+						RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+					) => {
+						si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+					}
+				);
 			}
 			test_state
 		})
@@ -3612,6 +3639,14 @@ fn session_info_small_jump_works() {
 						let _ = tx.send(Ok(Some(ExecutorParams::default())));
 					}
 				);
+				assert_matches!(
+					virtual_overseer.recv().await,
+					AllMessages::RuntimeApi(
+						RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+					) => {
+						si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+					}
+				);
 			}
 			test_state
 		})
diff --git a/polkadot/node/network/availability-distribution/src/pov_requester/mod.rs b/polkadot/node/network/availability-distribution/src/pov_requester/mod.rs
index 12a97a1fb5a..6f9ef9f6a9f 100644
--- a/polkadot/node/network/availability-distribution/src/pov_requester/mod.rs
+++ b/polkadot/node/network/availability-distribution/src/pov_requester/mod.rs
@@ -146,7 +146,9 @@ mod tests {
 		AllMessages, AvailabilityDistributionMessage, RuntimeApiMessage, RuntimeApiRequest,
 	};
 	use polkadot_node_subsystem_test_helpers as test_helpers;
-	use polkadot_primitives::{CandidateHash, ExecutorParams, Hash, ValidatorIndex};
+	use polkadot_primitives::{
+		vstaging::NodeFeatures, CandidateHash, ExecutorParams, Hash, ValidatorIndex,
+	};
 	use test_helpers::mock::make_ferdie_keystore;
 
 	use super::*;
@@ -214,6 +216,12 @@ mod tests {
 					)) => {
 						tx.send(Ok(Some(ExecutorParams::default()))).unwrap();
 					},
+					AllMessages::RuntimeApi(RuntimeApiMessage::Request(
+						_,
+						RuntimeApiRequest::NodeFeatures(_, si_tx),
+					)) => {
+						si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+					},
 					AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendRequests(
 						mut reqs,
 						_,
diff --git a/polkadot/node/network/availability-distribution/src/requester/tests.rs b/polkadot/node/network/availability-distribution/src/requester/tests.rs
index c4252b4e439..2f5d900b037 100644
--- a/polkadot/node/network/availability-distribution/src/requester/tests.rs
+++ b/polkadot/node/network/availability-distribution/src/requester/tests.rs
@@ -25,8 +25,8 @@ use polkadot_node_primitives::{BlockData, ErasureChunk, PoV};
 use polkadot_node_subsystem_test_helpers::mock::new_leaf;
 use polkadot_node_subsystem_util::runtime::RuntimeInfo;
 use polkadot_primitives::{
-	BlockNumber, CoreState, ExecutorParams, GroupIndex, Hash, Id as ParaId, ScheduledCore,
-	SessionIndex, SessionInfo,
+	vstaging::NodeFeatures, BlockNumber, CoreState, ExecutorParams, GroupIndex, Hash, Id as ParaId,
+	ScheduledCore, SessionIndex, SessionInfo,
 };
 use sp_core::traits::SpawnNamed;
 
@@ -125,6 +125,10 @@ fn spawn_virtual_overseer(
 								tx.send(Ok(Some(ExecutorParams::default())))
 									.expect("Receiver should be alive.");
 							},
+							RuntimeApiRequest::NodeFeatures(_, tx) => {
+								tx.send(Ok(NodeFeatures::EMPTY))
+									.expect("Receiver should be alive.");
+							},
 							RuntimeApiRequest::AvailabilityCores(tx) => {
 								let para_id = ParaId::from(1_u32);
 								let maybe_block_position =
diff --git a/polkadot/node/network/availability-distribution/src/tests/state.rs b/polkadot/node/network/availability-distribution/src/tests/state.rs
index 101d917c0db..e95c1c3a27c 100644
--- a/polkadot/node/network/availability-distribution/src/tests/state.rs
+++ b/polkadot/node/network/availability-distribution/src/tests/state.rs
@@ -46,8 +46,8 @@ use polkadot_node_subsystem::{
 };
 use polkadot_node_subsystem_test_helpers as test_helpers;
 use polkadot_primitives::{
-	CandidateHash, CoreState, ExecutorParams, GroupIndex, Hash, Id as ParaId, ScheduledCore,
-	SessionInfo, ValidatorIndex,
+	vstaging::NodeFeatures, CandidateHash, CoreState, ExecutorParams, GroupIndex, Hash,
+	Id as ParaId, ScheduledCore, SessionInfo, ValidatorIndex,
 };
 use test_helpers::mock::{make_ferdie_keystore, new_leaf};
 
@@ -264,6 +264,9 @@ impl TestState {
 							tx.send(Ok(Some(ExecutorParams::default())))
 								.expect("Receiver should be alive.");
 						},
+						RuntimeApiRequest::NodeFeatures(_, si_tx) => {
+							si_tx.send(Ok(NodeFeatures::EMPTY)).expect("Receiver should be alive.");
+						},
 						RuntimeApiRequest::AvailabilityCores(tx) => {
 							gum::trace!(target: LOG_TARGET, cores= ?self.cores[&hash], hash = ?hash, "Sending out cores for hash");
 							tx.send(Ok(self.cores[&hash].clone()))
diff --git a/polkadot/node/network/collator-protocol/src/collator_side/tests/mod.rs b/polkadot/node/network/collator-protocol/src/collator_side/tests/mod.rs
index 7dd2287dab6..1b1194c7270 100644
--- a/polkadot/node/network/collator-protocol/src/collator_side/tests/mod.rs
+++ b/polkadot/node/network/collator-protocol/src/collator_side/tests/mod.rs
@@ -45,8 +45,9 @@ use polkadot_node_subsystem::{
 use polkadot_node_subsystem_test_helpers as test_helpers;
 use polkadot_node_subsystem_util::{reputation::add_reputation, TimeoutExt};
 use polkadot_primitives::{
-	AuthorityDiscoveryId, CollatorPair, ExecutorParams, GroupIndex, GroupRotationInfo, IndexedVec,
-	ScheduledCore, SessionIndex, SessionInfo, ValidatorId, ValidatorIndex,
+	vstaging::NodeFeatures, AuthorityDiscoveryId, CollatorPair, ExecutorParams, GroupIndex,
+	GroupRotationInfo, IndexedVec, ScheduledCore, SessionIndex, SessionInfo, ValidatorId,
+	ValidatorIndex,
 };
 use polkadot_primitives_test_helpers::TestCandidateBuilder;
 use test_helpers::mock::new_leaf;
@@ -406,7 +407,12 @@ async fn distribute_collation_with_receipt(
 
 				tx.send(Ok(Some(ExecutorParams::default()))).unwrap();
 			},
-
+			AllMessages::RuntimeApi(RuntimeApiMessage::Request(
+				_,
+				RuntimeApiRequest::NodeFeatures(_, si_tx),
+			)) => {
+				si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+			},
 			AllMessages::RuntimeApi(RuntimeApiMessage::Request(
 				_relay_parent,
 				RuntimeApiRequest::ValidatorGroups(tx),
diff --git a/polkadot/node/network/dispute-distribution/src/tests/mod.rs b/polkadot/node/network/dispute-distribution/src/tests/mod.rs
index 96f045cbf76..a3520bf35f8 100644
--- a/polkadot/node/network/dispute-distribution/src/tests/mod.rs
+++ b/polkadot/node/network/dispute-distribution/src/tests/mod.rs
@@ -57,8 +57,8 @@ use polkadot_node_subsystem_test_helpers::{
 	subsystem_test_harness, TestSubsystemContextHandle,
 };
 use polkadot_primitives::{
-	AuthorityDiscoveryId, CandidateHash, CandidateReceipt, ExecutorParams, Hash, SessionIndex,
-	SessionInfo,
+	vstaging::NodeFeatures, AuthorityDiscoveryId, CandidateHash, CandidateReceipt, ExecutorParams,
+	Hash, SessionIndex, SessionInfo,
 };
 
 use self::mock::{
@@ -646,6 +646,16 @@ async fn nested_network_dispute_request<'a, F, O>(
 			},
 			unexpected => panic!("Unexpected message {:?}", unexpected),
 		}
+
+		match handle.recv().await {
+			AllMessages::RuntimeApi(RuntimeApiMessage::Request(
+				_,
+				RuntimeApiRequest::NodeFeatures(_, si_tx),
+			)) => {
+				si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+			},
+			unexpected => panic!("Unexpected message {:?}", unexpected),
+		}
 	}
 
 	// Import should get initiated:
@@ -773,6 +783,14 @@ async fn activate_leaf(
 				tx.send(Ok(Some(ExecutorParams::default()))).expect("Receiver should stay alive.");
 			}
 		);
+		assert_matches!(
+			handle.recv().await,
+			AllMessages::RuntimeApi(
+				RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+			) => {
+				si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+			}
+		);
 	}
 
 	assert_matches!(
diff --git a/polkadot/node/network/statement-distribution/src/legacy_v1/tests.rs b/polkadot/node/network/statement-distribution/src/legacy_v1/tests.rs
index ca3038f9b3f..8ac9895ec5a 100644
--- a/polkadot/node/network/statement-distribution/src/legacy_v1/tests.rs
+++ b/polkadot/node/network/statement-distribution/src/legacy_v1/tests.rs
@@ -43,8 +43,8 @@ use polkadot_node_subsystem::{
 };
 use polkadot_node_subsystem_test_helpers::mock::{make_ferdie_keystore, new_leaf};
 use polkadot_primitives::{
-	ExecutorParams, GroupIndex, Hash, HeadData, Id as ParaId, IndexedVec, SessionInfo,
-	ValidationCode,
+	vstaging::NodeFeatures, ExecutorParams, GroupIndex, Hash, HeadData, Id as ParaId, IndexedVec,
+	SessionInfo, ValidationCode,
 };
 use polkadot_primitives_test_helpers::{
 	dummy_committed_candidate_receipt, dummy_hash, AlwaysZeroRng,
@@ -834,6 +834,15 @@ fn receiving_from_one_sends_to_another_and_to_candidate_backing() {
 			}
 		);
 
+		assert_matches!(
+			handle.recv().await,
+			AllMessages::RuntimeApi(
+				RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+			) => {
+				si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+			}
+		);
+
 		// notify of peers and view
 		handle
 			.send(FromOrchestra::Communication {
@@ -1074,6 +1083,15 @@ fn receiving_large_statement_from_one_sends_to_another_and_to_candidate_backing(
 			}
 		);
 
+		assert_matches!(
+			handle.recv().await,
+			AllMessages::RuntimeApi(
+				RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+			) => {
+				si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+			}
+		);
+
 		// notify of peers and view
 		handle
 			.send(FromOrchestra::Communication {
@@ -1604,6 +1622,15 @@ fn delay_reputation_changes() {
 			}
 		);
 
+		assert_matches!(
+			handle.recv().await,
+			AllMessages::RuntimeApi(
+				RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+			) => {
+				si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+			}
+		);
+
 		// notify of peers and view
 		handle
 			.send(FromOrchestra::Communication {
@@ -2084,6 +2111,15 @@ fn share_prioritizes_backing_group() {
 			}
 		);
 
+		assert_matches!(
+			handle.recv().await,
+			AllMessages::RuntimeApi(
+				RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+			) => {
+				si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+			}
+		);
+
 		// notify of dummy peers and view
 		for (peer, pair) in dummy_peers.clone().into_iter().zip(dummy_pairs) {
 			handle
@@ -2406,6 +2442,15 @@ fn peer_cant_flood_with_large_statements() {
 			}
 		);
 
+		assert_matches!(
+			handle.recv().await,
+			AllMessages::RuntimeApi(
+				RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+			) => {
+				si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+			}
+		);
+
 		// notify of peers and view
 		handle
 			.send(FromOrchestra::Communication {
@@ -2631,6 +2676,14 @@ fn handle_multiple_seconded_statements() {
 			}
 		);
 
+		assert_matches!(
+			handle.recv().await,
+			AllMessages::RuntimeApi(
+				RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), )
+			) => {
+				si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap();
+			}
+		);
 		// notify of peers and view
 		for peer in all_peers.iter() {
 			handle
diff --git a/polkadot/node/subsystem-util/src/runtime/mod.rs b/polkadot/node/subsystem-util/src/runtime/mod.rs
index aada7a5d77a..0e44423b4e3 100644
--- a/polkadot/node/subsystem-util/src/runtime/mod.rs
+++ b/polkadot/node/subsystem-util/src/runtime/mod.rs
@@ -30,10 +30,12 @@ use polkadot_node_subsystem::{
 };
 use polkadot_node_subsystem_types::UnpinHandle;
 use polkadot_primitives::{
-	slashing, vstaging::NodeFeatures, AsyncBackingParams, CandidateEvent, CandidateHash, CoreState,
-	EncodeAs, ExecutorParams, GroupIndex, GroupRotationInfo, Hash, IndexedVec, OccupiedCore,
-	ScrapedOnChainVotes, SessionIndex, SessionInfo, Signed, SigningContext, UncheckedSigned,
-	ValidationCode, ValidationCodeHash, ValidatorId, ValidatorIndex, LEGACY_MIN_BACKING_VOTES,
+	slashing,
+	vstaging::{node_features::FeatureIndex, NodeFeatures},
+	AsyncBackingParams, CandidateEvent, CandidateHash, CoreState, EncodeAs, ExecutorParams,
+	GroupIndex, GroupRotationInfo, Hash, IndexedVec, OccupiedCore, ScrapedOnChainVotes,
+	SessionIndex, SessionInfo, Signed, SigningContext, UncheckedSigned, ValidationCode,
+	ValidationCodeHash, ValidatorId, ValidatorIndex, LEGACY_MIN_BACKING_VOTES,
 };
 
 use crate::{
@@ -92,6 +94,8 @@ pub struct ExtendedSessionInfo {
 	pub validator_info: ValidatorInfo,
 	/// Session executor parameters
 	pub executor_params: ExecutorParams,
+	/// Node features
+	pub node_features: NodeFeatures,
 }
 
 /// Information about ourselves, in case we are an `Authority`.
@@ -202,7 +206,20 @@ impl RuntimeInfo {
 
 			let validator_info = self.get_validator_info(&session_info)?;
 
-			let full_info = ExtendedSessionInfo { session_info, validator_info, executor_params };
+			let node_features = request_node_features(parent, session_index, sender)
+				.await?
+				.unwrap_or(NodeFeatures::EMPTY);
+			let last_set_index = node_features.iter_ones().last().unwrap_or_default();
+			if last_set_index >= FeatureIndex::FirstUnassigned as usize {
+				gum::warn!(target: LOG_TARGET, "Runtime requires feature bit {} that node doesn't support, please upgrade node version", last_set_index);
+			}
+
+			let full_info = ExtendedSessionInfo {
+				session_info,
+				validator_info,
+				executor_params,
+				node_features,
+			};
 
 			self.session_info_cache.insert(session_index, full_info);
 		}
diff --git a/polkadot/primitives/src/vstaging/mod.rs b/polkadot/primitives/src/vstaging/mod.rs
index 083e0f42d56..1639dc15e21 100644
--- a/polkadot/primitives/src/vstaging/mod.rs
+++ b/polkadot/primitives/src/vstaging/mod.rs
@@ -22,3 +22,19 @@ use bitvec::vec::BitVec;
 
 /// Bit indices in the `HostConfiguration.node_features` that correspond to different node features.
 pub type NodeFeatures = BitVec<u8, bitvec::order::Lsb0>;
+
+/// Module containing feature-specific bit indices into the `NodeFeatures` bitvec.
+pub mod node_features {
+	/// A feature index used to indentify a bit into the node_features array stored
+	/// in the HostConfiguration.
+	#[repr(u8)]
+	pub enum FeatureIndex {
+		/// Tells if tranch0 assignments could be sent in a single certificate.
+		/// Reserved for: `<https://github.com/paritytech/polkadot-sdk/issues/628>`
+		EnableAssignmentsV2 = 0,
+		/// First unassigned feature bit.
+		/// Every time a new feature flag is assigned it should take this value.
+		/// and this should be incremented.
+		FirstUnassigned = 1,
+	}
+}
-- 
GitLab