diff --git a/polkadot/node/network/collator-protocol/src/validator_side_experimental/peer_manager/mod.rs b/polkadot/node/network/collator-protocol/src/validator_side_experimental/peer_manager/mod.rs index b4502a9647e0..d3ac7aed6396 100644 --- a/polkadot/node/network/collator-protocol/src/validator_side_experimental/peer_manager/mod.rs +++ b/polkadot/node/network/collator-protocol/src/validator_side_experimental/peer_manager/mod.rs @@ -427,6 +427,11 @@ impl PeerManager { self.connected.clone().consume().0.into_keys().collect() } + #[cfg(test)] + pub async fn processed_finalized_block_number(&self) -> Option { + self.db.processed_finalized_block_number().await + } + async fn disconnect_peers( &self, sender: &mut Sender, diff --git a/polkadot/node/network/collator-protocol/src/validator_side_experimental/state.rs b/polkadot/node/network/collator-protocol/src/validator_side_experimental/state.rs index e5c85306812b..f9220a1143dd 100644 --- a/polkadot/node/network/collator-protocol/src/validator_side_experimental/state.rs +++ b/polkadot/node/network/collator-protocol/src/validator_side_experimental/state.rs @@ -657,6 +657,11 @@ impl State { pub fn advertisements(&self) -> std::collections::BTreeSet { self.collation_manager.advertisements() } + + #[cfg(test)] + pub async fn processed_finalized_block_number(&self) -> Option { + self.peer_manager.processed_finalized_block_number().await + } } // Specific implementation for PersistentDb to support disk persistence. diff --git a/polkadot/node/network/collator-protocol/src/validator_side_experimental/tests.rs b/polkadot/node/network/collator-protocol/src/validator_side_experimental/tests.rs index 7804929c11b2..6babe0ed2394 100644 --- a/polkadot/node/network/collator-protocol/src/validator_side_experimental/tests.rs +++ b/polkadot/node/network/collator-protocol/src/validator_side_experimental/tests.rs @@ -509,14 +509,15 @@ impl TestState { }); } - async fn handle_finalized_block(&mut self, finalized: BlockNumber) { - let old_finalized = self.finalized_block; + async fn handle_finalized_block_lookback( + &mut self, + processed: BlockNumber, + finalized: BlockNumber, + ) { self.finalized_block = finalized; - let diff = std::cmp::min( - finalized.checked_sub(old_finalized).unwrap(), - MAX_STARTUP_ANCESTRY_LOOKBACK, - ); + let diff = + std::cmp::min(finalized.checked_sub(processed).unwrap(), MAX_STARTUP_ANCESTRY_LOOKBACK); if diff == 0 { return; } @@ -545,7 +546,7 @@ impl TestState { } ); - let mut extra_msg = loop { + let extra_msg = loop { let had_buffered_msg = self.buffered_msg.is_some(); let msg = match self.buffered_msg.take() { Some(msg) => msg, @@ -607,7 +608,15 @@ impl TestState { }; }; - let msg = match extra_msg.take() { + self.buffered_msg = extra_msg; + } + + async fn handle_finalized_block(&mut self, processed: BlockNumber, finalized: BlockNumber) { + self.handle_finalized_block_lookback(processed, finalized).await; + + // Finality notifications additionally check for changes in the registered paras + // (once per session). + let msg = match self.buffered_msg.take() { Some(msg) => msg, None => self.timeout_recv().await, }; @@ -980,6 +989,8 @@ async fn make_state( let keystore = test_state.keystore.clone(); + let processed_block_number = db.processed_finalized_block_number().await.unwrap_or_default(); + let mut sender = test_state.sender.clone(); let responder = async move { @@ -1008,8 +1019,12 @@ async fn make_state( } ); - if finalized_block_number > 0 { - test_state.handle_finalized_block(finalized_block_number).await; + // Startup only extracts reputation bumps from the finalized chain, it doesn't prune the + // registered paras. + if finalized_block_number > processed_block_number { + test_state + .handle_finalized_block_lookback(processed_block_number, finalized_block_number) + .await; } // No more messages are expected @@ -1049,6 +1064,7 @@ struct MockDb { // Use BTreeMaps to ensure ordering when asserting. witnessed_bumps: Arc>>>, witnessed_slash: Arc>>, + witnessed_prunes: Arc>>>, query_fn: Arc Option + Send>>, } @@ -1066,6 +1082,7 @@ impl MockDb { finalized: Default::default(), witnessed_bumps: Default::default(), witnessed_slash: Default::default(), + witnessed_prunes: Default::default(), query_fn, } } @@ -1077,6 +1094,14 @@ impl MockDb { fn witnessed_slash(&self) -> Option<(PeerId, ParaId, Score)> { std::mem::take(self.witnessed_slash.lock().unwrap().deref_mut()) } + + fn witnessed_prunes(&self) -> Vec> { + std::mem::take(self.witnessed_prunes.lock().unwrap().deref_mut()) + } + + fn set_processed_finalized_block_number(&self, number: BlockNumber) { + *self.finalized.lock().unwrap() = number; + } } #[async_trait] @@ -1100,7 +1125,9 @@ impl Backend for MockDb { assert!(old_slash.is_none()); } - async fn prune_paras(&mut self, _registered_paras: BTreeSet) {} + async fn prune_paras(&mut self, registered_paras: BTreeSet) { + self.witnessed_prunes.lock().unwrap().push(registered_paras); + } async fn process_bumps( &mut self, @@ -1109,6 +1136,11 @@ impl Backend for MockDb { _decay_value: Option, _now: std::time::Duration, ) -> Vec { + // Mirror the real backend: never move the processed finalized block backwards. + if *(self.finalized.lock().unwrap()) >= leaf_number { + return vec![]; + } + let old_bumps = std::mem::replace( self.witnessed_bumps.lock().unwrap().deref_mut(), bumps.into_iter().map(|(k, v)| (k, v.into_iter().collect())).collect(), @@ -1130,12 +1162,13 @@ impl Drop for MockDb { fn drop(&mut self) { assert!(self.witnessed_bumps().is_empty()); assert!(self.witnessed_slash().is_none()); + assert!(self.witnessed_prunes().is_empty()); } } -#[tokio::test] // Test scenarios concerning connects/disconnects and declares. // More fine grained tests are in the `ConnectedPeers` unit tests. +#[tokio::test] async fn test_connection_flow() { let mut test_state = TestState::default(); let active_leaf = get_hash(10); @@ -1192,7 +1225,8 @@ async fn test_connection_flow() { ); // Reputations are bumped on finalized block notifications. - futures::join!(test_state.handle_finalized_block(2), async { + let processed = state.processed_finalized_block_number().await.unwrap_or_default(); + futures::join!(test_state.handle_finalized_block(processed, 2), async { state.handle_finalized_block(&mut sender, get_hash(2), 2).await.unwrap() }); test_state.assert_no_messages().await; @@ -1256,8 +1290,8 @@ async fn test_connection_flow() { assert_eq!(state.connected_peers(), peer_ids.clone().into_iter().skip(1).collect()); } -#[tokio::test] // Test peer disconnects before the collation is successfully fetched. +#[tokio::test] async fn test_peer_disconnects_before_fetch() { let mut test_state = TestState::default(); let active_leaf = get_hash(10); @@ -1304,7 +1338,8 @@ async fn test_peer_disconnects_before_fetch() { Hash::from_low_u64_be(2), ); - // Advertise a collation from the first peer, it will be launched, leaving only one free slot. + // Advertise a collation from the first peer, it will be launched, leaving only one free + // slot. test_state.handle_advertisement(&mut state, first_adv).await; state.try_launch_new_fetch_requests(&mut sender).await; @@ -1349,7 +1384,8 @@ async fn test_peer_disconnects_before_fetch() { assert_eq!(state.advertisements(), [first_adv, second_adv, third_adv].into()); test_state.assert_no_messages().await; - // Second advertisement is not launched since the third one already occupied the other slot. + // Second advertisement is not launched since the third one already occupied the other + // slot. state.try_launch_new_fetch_requests(&mut sender).await; test_state.assert_no_messages().await; @@ -1377,8 +1413,8 @@ async fn test_peer_disconnects_before_fetch() { } } -#[tokio::test] // Test peer disconnects after the collation is successfully fetched. +#[tokio::test] async fn test_peer_disconnects_after_fetch() { let mut test_state = TestState::default(); let active_leaf = get_hash(10); @@ -1425,15 +1461,16 @@ async fn test_peer_disconnects_after_fetch() { dummy_pvd().hash(), ); - // Advertise a collation from the first peer, it will be launched, leaving only one free slot. + // Advertise a collation from the first peer, it will be launched, leaving only one free + // slot. test_state.handle_advertisement(&mut state, first_adv).await; state.try_launch_new_fetch_requests(&mut sender).await; test_state.assert_collation_request(first_adv).await; test_state.assert_no_messages().await; - // Test a peer disconnect after the collation was successfully fetched. In this case, we won't - // free up the slot. + // Test a peer disconnect after the collation was successfully fetched. In this case, we + // won't free up the slot. state .handle_peer_connected(&mut sender, second_peer, CollationVersion::V2) @@ -1466,8 +1503,8 @@ async fn test_peer_disconnects_after_fetch() { ) .await; - // Second peer disconnects, which will not free up the claim queue slot since the collation was - // already fetched. + // Second peer disconnects, which will not free up the claim queue slot since the collation + // was already fetched. state.handle_peer_disconnected(second_peer).await; assert_eq!(state.advertisements(), [first_adv, second_adv].into()); @@ -1475,8 +1512,8 @@ async fn test_peer_disconnects_after_fetch() { state.try_launch_new_fetch_requests(&mut sender).await; test_state.assert_no_messages().await; - // The collation was seconded, the claim will still not be freed but we won't be able to send - // back a notification to the collator. + // The collation was seconded, the claim will still not be freed but we won't be able to + // send back a notification to the collator. let parent = third_ccr.descriptor.scheduling_parent(); let statement = make_seconded_statement(&test_state.keystore, third_ccr); @@ -1490,8 +1527,8 @@ async fn test_peer_disconnects_after_fetch() { test_state.assert_no_messages().await; } -#[tokio::test] // Test that peer connections are rejected if we have no assignments. +#[tokio::test] async fn test_no_assignments() { let mut test_state = TestState::default(); let active_leaf = get_hash(10); @@ -1789,8 +1826,8 @@ async fn test_peer_connections_across_group_rotations() { assert_eq!(state.connected_peers(), (&peer_ids[5..]).into_iter().cloned().collect()); } -#[tokio::test] // Test reputation bumps on finalized block notification. +#[tokio::test] async fn finalized_block_notification() { let mut test_state = TestState::default(); let active_leaf = get_hash(10); @@ -1812,10 +1849,16 @@ async fn finalized_block_notification() { assert_eq!(state.connected_peers(), peers.clone().into_iter().collect()); // Finalize block 5, no expected bumps, because there are no included candidates. - futures::join!(test_state.handle_finalized_block(5), async { + let processed = state.processed_finalized_block_number().await.unwrap_or_default(); + futures::join!(test_state.handle_finalized_block(processed, 5), async { state.handle_finalized_block(&mut sender, get_hash(5), 5).await.unwrap() }); test_state.assert_no_messages().await; + // The first finalized block notification queries and prunes the registered paras. + assert_eq!( + db.witnessed_prunes(), + vec![[100.into(), 200.into(), 600.into()].into_iter().collect::>()] + ); // Add one included candidate at block 6 for first peer and para 100. test_state.set_candidates_pending_availability( @@ -1828,7 +1871,8 @@ async fn finalized_block_notification() { [(first_peer, Score::new(VALID_INCLUDED_CANDIDATE_BUMP))].into_iter().collect(), ); - futures::join!(test_state.handle_finalized_block(6), async { + let processed = state.processed_finalized_block_number().await.unwrap_or_default(); + futures::join!(test_state.handle_finalized_block(processed, 6), async { state.handle_finalized_block(&mut sender, get_hash(6), 6).await.unwrap() }); test_state.assert_no_messages().await; @@ -1879,7 +1923,8 @@ async fn finalized_block_notification() { // Add multiple included candidates at different block heights and check that they are processed // accordingly. - futures::join!(test_state.handle_finalized_block(10), async { + let processed = state.processed_finalized_block_number().await.unwrap_or_default(); + futures::join!(test_state.handle_finalized_block(processed, 10), async { state.handle_finalized_block(&mut sender, get_hash(10), 10).await.unwrap() }); test_state.assert_no_messages().await; @@ -1888,8 +1933,8 @@ async fn finalized_block_notification() { assert_eq!(db.witnessed_bumps(), expected_bumps); } -#[tokio::test] // Test various scenarios for rejecting advertisements. +#[tokio::test] async fn test_advertisement_rejections() { let mut test_state = TestState::default(); let active_leaf = get_hash(10); @@ -2410,8 +2455,8 @@ async fn test_collation_fetch_failure() { } } -#[tokio::test] // Test a successful collation fetch that went out of the view in the meantime. +#[tokio::test] async fn test_collation_response_out_of_view() { let mut test_state = TestState::default(); let active_leaf = get_hash(10); @@ -2493,9 +2538,9 @@ async fn test_collation_response_out_of_view() { // removed because V2 is now always enabled. The v3_enabled parameter has been removed from // descriptor methods. +// Test that we accept v1 candidates. #[rstest] #[tokio::test] -// Test that we accept v1 candidates. async fn v1_descriptor_compatibility() { let mut test_state = TestState::default(); let active_leaf = get_hash(10); @@ -2545,9 +2590,9 @@ async fn v1_descriptor_compatibility() { test_state.assert_no_messages().await; } -#[tokio::test] // Test a collation that is deemed invalid by candidate-backing. It will lead to a slash and the // slot will be freed. +#[tokio::test] async fn test_invalid_collation() { let mut test_state = TestState::default(); let active_leaf = get_hash(10); @@ -2631,12 +2676,12 @@ async fn test_invalid_collation() { test_state.assert_no_messages().await; } +// Test that we can block the seconding of a candidate on its parent being seconded. If the parent +// is later seconded, the child is unblocked as well. If it's invalid, it will be dropped. #[rstest] #[case(true)] #[case(false)] #[tokio::test] -// Test that we can block the seconding of a candidate on its parent being seconded. If the parent -// is later seconded, the child is unblocked as well. If it's invalid, it will be dropped. async fn test_blocked_from_seconding_by_parent(#[case] valid_parent: bool) { let mut test_state = TestState::default(); let active_leaf = get_hash(10); @@ -3081,8 +3126,8 @@ async fn test_outdated_fetching_collations_are_pruned() { test_state.assert_no_messages().await; } -#[tokio::test] // Test that a v1 advertisement cannot claim a future slot. +#[tokio::test] async fn test_single_collation_per_rp_for_v1_advertisement() { let mut test_state = TestState::default(); let active_leaf = get_hash(10); @@ -3173,9 +3218,9 @@ async fn test_single_collation_per_rp_for_v1_advertisement() { test_state.assert_collation_request(third_adv).await; } -#[tokio::test] // Test that activating a new leaf on top of an existing one doesn't overwrite the `PerRelayParent` // state. +#[tokio::test] async fn test_view_update_preserves_relay_parent_state() { let mut test_state = TestState::default(); let leaf_a = get_hash(10); @@ -3221,9 +3266,9 @@ async fn test_view_update_preserves_relay_parent_state() { assert_eq!(state.advertisements(), [adv_a].into()); } -#[tokio::test] // Test that a V3 candidate descriptor is correctly accepted and // seconded when the CandidateReceiptV3 node feature is enabled. +#[tokio::test] async fn v3_descriptor_accepted_when_v3_enabled() { let mut test_state = TestState::default(); test_state @@ -3280,9 +3325,9 @@ async fn v3_descriptor_accepted_when_v3_enabled() { .await; } -#[tokio::test] // V3 advertisement is accepted when the scheduling parent is a leaf // whose slot has already finished (current_slot == leaf_slot + 1). +#[tokio::test] async fn v3_advertisement_accepted_when_sp_is_finished_slot_leaf() { let mut test_state = TestState::default(); test_state @@ -3334,10 +3379,10 @@ async fn v3_advertisement_accepted_when_sp_is_finished_slot_leaf() { .await; } -#[tokio::test] // V3 advertisements require the scheduling parent to be a RC block from // the last finished slot. Check that the leaf of an active slot and its // grand parent are rejected +#[tokio::test] async fn v3_advertisement_rejected_when_sp_not_last_finished_slot() { let mut test_state = TestState::default(); test_state @@ -3440,8 +3485,8 @@ async fn v3_advertisement_rejected_when_sp_not_last_finished_slot() { .await; } -#[tokio::test] // V3 descriptor via V2 protocol → rejected (wrong protocol). +#[tokio::test] async fn v3_descriptor_rejected_via_v2_protocol() { let mut test_state = TestState::default(); @@ -3488,9 +3533,9 @@ async fn v3_descriptor_rejected_via_v2_protocol() { test_state.assert_no_messages().await; } -#[tokio::test] // Test that a collator advertising a V3 descriptor but fetching a V2 candidate // is rejected due to descriptor version mismatch and the peer is slashed. +#[tokio::test] async fn v3_advertised_but_v2_fetched_descriptor_version_mismatch() { let mut test_state = TestState::default(); test_state @@ -3556,9 +3601,9 @@ async fn v3_advertised_but_v2_fetched_descriptor_version_mismatch() { test_state.assert_no_messages().await; } -#[tokio::test] // Test that a crafted descriptor is rejected as Unknown version when // CandidateReceiptV3 feature is disabled. +#[tokio::test] async fn v3_descriptor_unknown_rejected_when_v3_disabled() { let mut test_state = TestState::default(); @@ -3611,9 +3656,9 @@ async fn v3_descriptor_unknown_rejected_when_v3_disabled() { test_state.assert_no_messages().await; } -#[tokio::test] // Regression test for checking that the core is computed correctly // for ancestors. +#[tokio::test] async fn core_assignment_uses_ancestor_not_leaf() { let mut test_state = TestState::default(); // Rotate groups every block so block 9 and block 10 have different @@ -4297,6 +4342,217 @@ async fn short_claim_queue_does_not_reject_ancestor_advertisements() { assert_eq!(state.advertisements(), [adv].into()); } -// TODO: -// - Test subsystem startup: make sure we are properly populating the db. -// - Test a change in the registered paras on finalized block notification. +#[tokio::test] +async fn startup_populates_db_from_finalized_chain() { + let mut test_state = TestState::default(); + let active_leaf = get_hash(10); + + let first_peer = peer_id(1); + let second_peer = peer_id(2); + let third_peer = peer_id(3); + + // The node starts with finalized block 6 and a DB which hasn't processed any blocks yet. + test_state.finalized_block = 6; + test_state.set_candidates_pending_availability( + [ + (get_hash(2), vec![(ParaId::from(100), first_peer)]), + ( + get_hash(5), + vec![ + (ParaId::from(200), first_peer), + (ParaId::from(200), first_peer), + (ParaId::from(200), second_peer), + ], + ), + (get_hash(6), vec![(ParaId::from(100), third_peer)]), + ] + .into_iter() + .collect(), + ); + + let db = MockDb::default(); + let state = make_state(db.clone(), &mut test_state, active_leaf).await; + + let mut expected_bumps = BTreeMap::new(); + expected_bumps.insert( + ParaId::new(100), + [ + (first_peer, Score::new(VALID_INCLUDED_CANDIDATE_BUMP)), + (third_peer, Score::new(VALID_INCLUDED_CANDIDATE_BUMP)), + ] + .into_iter() + .collect(), + ); + expected_bumps.insert( + ParaId::new(200), + [ + (first_peer, Score::new(2 * VALID_INCLUDED_CANDIDATE_BUMP)), + (second_peer, Score::new(VALID_INCLUDED_CANDIDATE_BUMP)), + ] + .into_iter() + .collect(), + ); + assert_eq!(db.witnessed_bumps(), expected_bumps); + // The DB has caught up with the finalized chain. + assert_eq!(db.processed_finalized_block_number().await, Some(6)); + // Startup doesn't prune the registered paras. + assert!(db.witnessed_prunes().is_empty()); + + drop(state); + + // Simulate a restart with the same DB: the already processed blocks are not re-processed, + // so the candidates of the ancestry don't produce bumps again. + let _state = make_state(db.clone(), &mut test_state, active_leaf).await; + + assert!(db.witnessed_bumps().is_empty()); + // assert!(db.witnessed_decay_values().is_empty()); + assert_eq!(db.processed_finalized_block_number().await, Some(6)); +} + +#[tokio::test] +async fn startup_lookback_is_capped() { + let mut test_state = TestState::default(); + let active_leaf = get_hash(10); + + let outside_peer = peer_id(1); + let oldest_peer = peer_id(2); + let newest_peer = peer_id(3); + + let finalized = MAX_STARTUP_ANCESTRY_LOOKBACK + 10; + // The oldest block whose included candidates are within the lookback window. + let window_start = finalized - MAX_STARTUP_ANCESTRY_LOOKBACK + 1; + + test_state.finalized_block = finalized; + test_state.set_candidates_pending_availability( + [ + // Right outside of the lookback window. + (get_hash(window_start - 1), vec![(ParaId::from(100), outside_peer)]), + (get_hash(window_start), vec![(ParaId::from(100), oldest_peer)]), + (get_hash(finalized), vec![(ParaId::from(100), newest_peer)]), + ] + .into_iter() + .collect(), + ); + + let db = MockDb::default(); + let _state = make_state(db.clone(), &mut test_state, active_leaf).await; + + let mut expected_bumps = BTreeMap::new(); + expected_bumps.insert( + ParaId::new(100), + [ + (oldest_peer, Score::new(VALID_INCLUDED_CANDIDATE_BUMP)), + (newest_peer, Score::new(VALID_INCLUDED_CANDIDATE_BUMP)), + ] + .into_iter() + .collect(), + ); + assert_eq!(db.witnessed_bumps(), expected_bumps); + assert_eq!(db.processed_finalized_block_number().await, Some(finalized)); +} + +// Should never happen but verify that the case is handled gracefully. +#[tokio::test] +async fn startup_with_db_ahead_of_finalized_chain() { + let mut test_state = TestState::default(); + let active_leaf = get_hash(10); + + test_state.finalized_block = 8; + let db = MockDb::default(); + db.set_processed_finalized_block_number(15); + // A candidate on the finalized chain which must not be processed. + test_state.set_candidates_pending_availability( + [(get_hash(5), vec![(ParaId::from(100), peer_id(1))])].into_iter().collect(), + ); + + let _state = make_state(db.clone(), &mut test_state, active_leaf).await; + + assert!(db.witnessed_bumps().is_empty()); + assert_eq!(db.processed_finalized_block_number().await, Some(15)); +} + +// Test the registered paras processing on finalized block notifications: the registered paras +// are queried once per session and the DB is told to prune the paras that are no longer +// registered. +#[tokio::test] +async fn registered_paras_pruned_on_new_session() { + let mut test_state = TestState::default(); + let active_leaf = get_hash(10); + let leaf_info = test_state.rp_info.get(&active_leaf).unwrap().clone(); + + let db = MockDb::default(); + let mut state = make_state(db.clone(), &mut test_state, active_leaf).await; + let mut sender = test_state.sender.clone(); + + // The first finalized block notification always queries the registered paras. + let processed = state.processed_finalized_block_number().await.unwrap_or_default(); + futures::join!(test_state.handle_finalized_block(processed, 2), async { + state.handle_finalized_block(&mut sender, get_hash(2), 2).await.unwrap() + }); + test_state.assert_no_messages().await; + assert_eq!( + db.witnessed_prunes(), + vec![[100.into(), 200.into(), 600.into()].into_iter().collect::>()] + ); + + // A subsequent finalized block in the same session doesn't re-query the registered paras + // (the harness panics on an unexpected `ParaIds` request). + let processed = state.processed_finalized_block_number().await.unwrap_or_default(); + futures::join!(test_state.handle_finalized_block(processed, 3), async { + state.handle_finalized_block(&mut sender, get_hash(3), 3).await.unwrap() + }); + test_state.assert_no_messages().await; + assert!(db.witnessed_prunes().is_empty()); + + // Start session 2, where para 200 is deregistered. + let mut session_2_info = test_state.session_info.get(&1).unwrap().clone(); + session_2_info.paras = vec![100.into(), 600.into()]; + test_state.session_info.insert(2, session_2_info); + test_state.rp_info.insert( + get_hash(4), + RelayParentInfo { + number: 4, + parent: get_parent_hash(4), + session_index: 2, + claim_queue: leaf_info.claim_queue.clone(), + assigned_core: leaf_info.assigned_core, + }, + ); + + let processed = state.processed_finalized_block_number().await.unwrap_or_default(); + futures::join!(test_state.handle_finalized_block(processed, 4), async { + state.handle_finalized_block(&mut sender, get_hash(4), 4).await.unwrap() + }); + test_state.assert_no_messages().await; + assert_eq!( + db.witnessed_prunes(), + vec![[100.into(), 600.into()].into_iter().collect::>()] + ); + + // A finalized block with an older session index doesn't trigger a query. + // (block 5 is not in `rp_info`, so the harness reports session 1 for it) + let processed = state.processed_finalized_block_number().await.unwrap_or_default(); + futures::join!(test_state.handle_finalized_block(processed, 5), async { + state.handle_finalized_block(&mut sender, get_hash(5), 5).await.unwrap() + }); + test_state.assert_no_messages().await; + assert!(db.witnessed_prunes().is_empty()); + + // Same for an equal session index. + test_state.rp_info.insert( + get_hash(6), + RelayParentInfo { + number: 6, + parent: get_parent_hash(6), + session_index: 2, + claim_queue: leaf_info.claim_queue.clone(), + assigned_core: leaf_info.assigned_core, + }, + ); + let processed = state.processed_finalized_block_number().await.unwrap_or_default(); + futures::join!(test_state.handle_finalized_block(processed, 6), async { + state.handle_finalized_block(&mut sender, get_hash(6), 6).await.unwrap() + }); + test_state.assert_no_messages().await; + assert!(db.witnessed_prunes().is_empty()); +} diff --git a/prdoc/pr_12675.prdoc b/prdoc/pr_12675.prdoc new file mode 100644 index 000000000000..38d7e0175678 --- /dev/null +++ b/prdoc/pr_12675.prdoc @@ -0,0 +1,7 @@ +title: Collator revamp - tests for subsystem startup and registered parachains pruning +doc: +- audience: Node Dev + description: null +crates: +- name: polkadot-collator-protocol + bump: patch