diff --git a/node/network/protocol/src/lib.rs b/node/network/protocol/src/lib.rs index 9f50bf29406e..0f2f5a9327ac 100644 --- a/node/network/protocol/src/lib.rs +++ b/node/network/protocol/src/lib.rs @@ -737,7 +737,7 @@ pub mod vstaging { /// Network messages used by the statement distribution subsystem. #[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)] pub enum StatementDistributionMessage { - /// A notification of a signed statement in compact form. + /// A notification of a signed statement in compact form, for a given relay parent. #[codec(index = 0)] Statement(Hash, UncheckedSignedStatement), diff --git a/node/network/statement-distribution/src/vstaging/grid.rs b/node/network/statement-distribution/src/vstaging/grid.rs index bdcbabffd3e5..5934e05378e5 100644 --- a/node/network/statement-distribution/src/vstaging/grid.rs +++ b/node/network/statement-distribution/src/vstaging/grid.rs @@ -695,7 +695,7 @@ fn decompose_statement_filter<'a>( } /// A summary of a manifest being sent by a counterparty. -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct ManifestSummary { /// The claimed parent head data hash of the candidate. pub claimed_parent_hash: Hash, diff --git a/node/network/statement-distribution/src/vstaging/mod.rs b/node/network/statement-distribution/src/vstaging/mod.rs index a562668627a1..5fe39096c1cb 100644 --- a/node/network/statement-distribution/src/vstaging/mod.rs +++ b/node/network/statement-distribution/src/vstaging/mod.rs @@ -119,7 +119,7 @@ const COST_UNEXPECTED_REQUEST: Rep = Rep::CostMajor("Unexpected attested candida const BENEFIT_VALID_RESPONSE: Rep = Rep::BenefitMajor("Peer Answered Candidate Request"); const BENEFIT_VALID_STATEMENT: Rep = Rep::BenefitMajor("Peer provided a valid statement"); const BENEFIT_VALID_STATEMENT_FIRST: Rep = - Rep::BenefitMajorFirst("Peer was the first to provide a valid statement"); + Rep::BenefitMajorFirst("Peer was the first to provide a given valid statement"); struct PerRelayParentState { local_validator: Option, @@ -144,6 +144,7 @@ struct LocalValidatorState { grid_tracker: GridTracker, } +#[derive(Debug)] struct PerSessionState { session_info: SessionInfo, groups: Groups, @@ -1006,6 +1007,7 @@ pub(crate) async fn share_local_statement( // two kinds of targets: those in our 'cluster' (currently just those in the same group), // and those we are propagating to through the grid. +#[derive(Debug)] enum DirectTargetKind { Cluster, Grid, diff --git a/node/network/statement-distribution/src/vstaging/tests/cluster.rs b/node/network/statement-distribution/src/vstaging/tests/cluster.rs index ca849dbd39a6..88fa13d98dc3 100644 --- a/node/network/statement-distribution/src/vstaging/tests/cluster.rs +++ b/node/network/statement-distribution/src/vstaging/tests/cluster.rs @@ -20,7 +20,12 @@ use polkadot_primitives_test_helpers::make_candidate; #[test] fn share_seconded_circulated_to_cluster() { - let config = TestConfig { validator_count: 20, group_size: 3, local_validator: true }; + let config = TestConfig { + validator_count: 20, + group_size: 3, + local_validator: true, + async_backing_params: None, + }; let relay_parent = Hash::repeat_byte(1); let peer_a = PeerId::random(); @@ -31,37 +36,18 @@ fn share_seconded_circulated_to_cluster() { let local_validator = state.local.clone().unwrap(); let local_para = ParaId::from(local_validator.group_index.0); + let test_leaf = state.make_dummy_leaf(relay_parent); + let (candidate, pvd) = make_candidate( relay_parent, 1, local_para, - vec![1, 2, 3].into(), + test_leaf.para_data(local_para).head_data.clone(), vec![4, 5, 6].into(), Hash::repeat_byte(42).into(), ); let candidate_hash = candidate.hash(); - let test_leaf = TestLeaf { - number: 1, - hash: relay_parent, - parent_hash: Hash::repeat_byte(0), - session: 1, - availability_cores: state.make_availability_cores(|i| { - CoreState::Scheduled(ScheduledCore { - para_id: ParaId::from(i as u32), - collator: None, - }) - }), - para_data: (0..state.session_info.validator_groups.len()) - .map(|i| { - ( - ParaId::from(i as u32), - PerParaData { min_relay_parent: 1, head_data: vec![1, 2, 3].into() }, - ) - }) - .collect(), - }; - // peer A is in group, has relay parent in view. // peer B is in group, has no relay parent in view. // peer C is not in group, has relay parent in view. @@ -88,7 +74,7 @@ fn share_seconded_circulated_to_cluster() { send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; } - activate_leaf(&mut overseer, local_para, &test_leaf, &state, true).await; + activate_leaf(&mut overseer, &test_leaf, &state, true).await; answer_expected_hypothetical_depth_request( &mut overseer, @@ -141,36 +127,21 @@ fn share_seconded_circulated_to_cluster() { #[test] fn cluster_valid_statement_before_seconded_ignored() { - let config = TestConfig { validator_count: 20, group_size: 3, local_validator: true }; + let config = TestConfig { + validator_count: 20, + group_size: 3, + local_validator: true, + async_backing_params: None, + }; let relay_parent = Hash::repeat_byte(1); let peer_a = PeerId::random(); test_harness(config, |state, mut overseer| async move { let local_validator = state.local.clone().unwrap(); - let local_para = ParaId::from(local_validator.group_index.0); let candidate_hash = CandidateHash(Hash::repeat_byte(42)); - let test_leaf = TestLeaf { - number: 1, - hash: relay_parent, - parent_hash: Hash::repeat_byte(0), - session: 1, - availability_cores: state.make_availability_cores(|i| { - CoreState::Scheduled(ScheduledCore { - para_id: ParaId::from(i as u32), - collator: None, - }) - }), - para_data: (0..state.session_info.validator_groups.len()) - .map(|i| { - ( - ParaId::from(i as u32), - PerParaData { min_relay_parent: 1, head_data: vec![1, 2, 3].into() }, - ) - }) - .collect(), - }; + let test_leaf = state.make_dummy_leaf(relay_parent); // peer A is in group, has relay parent in view. let other_group_validators = state.group_validators(local_validator.group_index, true); @@ -183,7 +154,7 @@ fn cluster_valid_statement_before_seconded_ignored() { .await; send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; - activate_leaf(&mut overseer, local_para, &test_leaf, &state, true).await; + activate_leaf(&mut overseer, &test_leaf, &state, true).await; answer_expected_hypothetical_depth_request( &mut overseer, @@ -223,36 +194,21 @@ fn cluster_valid_statement_before_seconded_ignored() { #[test] fn cluster_statement_bad_signature() { - let config = TestConfig { validator_count: 20, group_size: 3, local_validator: true }; + let config = TestConfig { + validator_count: 20, + group_size: 3, + local_validator: true, + async_backing_params: None, + }; let relay_parent = Hash::repeat_byte(1); let peer_a = PeerId::random(); test_harness(config, |state, mut overseer| async move { let local_validator = state.local.clone().unwrap(); - let local_para = ParaId::from(local_validator.group_index.0); let candidate_hash = CandidateHash(Hash::repeat_byte(42)); - let test_leaf = TestLeaf { - number: 1, - hash: relay_parent, - parent_hash: Hash::repeat_byte(0), - session: 1, - availability_cores: state.make_availability_cores(|i| { - CoreState::Scheduled(ScheduledCore { - para_id: ParaId::from(i as u32), - collator: None, - }) - }), - para_data: (0..state.session_info.validator_groups.len()) - .map(|i| { - ( - ParaId::from(i as u32), - PerParaData { min_relay_parent: 1, head_data: vec![1, 2, 3].into() }, - ) - }) - .collect(), - }; + let test_leaf = state.make_dummy_leaf(relay_parent); // peer A is in group, has relay parent in view. let other_group_validators = state.group_validators(local_validator.group_index, true); @@ -267,7 +223,7 @@ fn cluster_statement_bad_signature() { .await; send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; - activate_leaf(&mut overseer, local_para, &test_leaf, &state, true).await; + activate_leaf(&mut overseer, &test_leaf, &state, true).await; answer_expected_hypothetical_depth_request( &mut overseer, @@ -318,36 +274,21 @@ fn cluster_statement_bad_signature() { #[test] fn useful_cluster_statement_from_non_cluster_peer_rejected() { - let config = TestConfig { validator_count: 20, group_size: 3, local_validator: true }; + let config = TestConfig { + validator_count: 20, + group_size: 3, + local_validator: true, + async_backing_params: None, + }; let relay_parent = Hash::repeat_byte(1); let peer_a = PeerId::random(); test_harness(config, |state, mut overseer| async move { let local_validator = state.local.clone().unwrap(); - let local_para = ParaId::from(local_validator.group_index.0); let candidate_hash = CandidateHash(Hash::repeat_byte(42)); - let test_leaf = TestLeaf { - number: 1, - hash: relay_parent, - parent_hash: Hash::repeat_byte(0), - session: 1, - availability_cores: state.make_availability_cores(|i| { - CoreState::Scheduled(ScheduledCore { - para_id: ParaId::from(i as u32), - collator: None, - }) - }), - para_data: (0..state.session_info.validator_groups.len()) - .map(|i| { - ( - ParaId::from(i as u32), - PerParaData { min_relay_parent: 1, head_data: vec![1, 2, 3].into() }, - ) - }) - .collect(), - }; + let test_leaf = state.make_dummy_leaf(relay_parent); // peer A is not in group, has relay parent in view. let not_our_group = @@ -364,7 +305,7 @@ fn useful_cluster_statement_from_non_cluster_peer_rejected() { .await; send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; - activate_leaf(&mut overseer, local_para, &test_leaf, &state, true).await; + activate_leaf(&mut overseer, &test_leaf, &state, true).await; answer_expected_hypothetical_depth_request( &mut overseer, @@ -402,36 +343,21 @@ fn useful_cluster_statement_from_non_cluster_peer_rejected() { #[test] fn statement_from_non_cluster_originator_unexpected() { - let config = TestConfig { validator_count: 20, group_size: 3, local_validator: true }; + let config = TestConfig { + validator_count: 20, + group_size: 3, + local_validator: true, + async_backing_params: None, + }; let relay_parent = Hash::repeat_byte(1); let peer_a = PeerId::random(); test_harness(config, |state, mut overseer| async move { let local_validator = state.local.clone().unwrap(); - let local_para = ParaId::from(local_validator.group_index.0); let candidate_hash = CandidateHash(Hash::repeat_byte(42)); - let test_leaf = TestLeaf { - number: 1, - hash: relay_parent, - parent_hash: Hash::repeat_byte(0), - session: 1, - availability_cores: state.make_availability_cores(|i| { - CoreState::Scheduled(ScheduledCore { - para_id: ParaId::from(i as u32), - collator: None, - }) - }), - para_data: (0..state.session_info.validator_groups.len()) - .map(|i| { - ( - ParaId::from(i as u32), - PerParaData { min_relay_parent: 1, head_data: vec![1, 2, 3].into() }, - ) - }) - .collect(), - }; + let test_leaf = state.make_dummy_leaf(relay_parent); // peer A is not in group, has relay parent in view. let other_group_validators = state.group_validators(local_validator.group_index, true); @@ -440,7 +366,7 @@ fn statement_from_non_cluster_originator_unexpected() { connect_peer(&mut overseer, peer_a.clone(), None).await; send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; - activate_leaf(&mut overseer, local_para, &test_leaf, &state, true).await; + activate_leaf(&mut overseer, &test_leaf, &state, true).await; answer_expected_hypothetical_depth_request( &mut overseer, @@ -478,7 +404,13 @@ fn statement_from_non_cluster_originator_unexpected() { #[test] fn seconded_statement_leads_to_request() { - let config = TestConfig { validator_count: 20, group_size: 3, local_validator: true }; + let group_size = 3; + let config = TestConfig { + validator_count: 20, + group_size, + local_validator: true, + async_backing_params: None, + }; let relay_parent = Hash::repeat_byte(1); let peer_a = PeerId::random(); @@ -486,28 +418,18 @@ fn seconded_statement_leads_to_request() { test_harness(config, |state, mut overseer| async move { let local_validator = state.local.clone().unwrap(); let local_para = ParaId::from(local_validator.group_index.0); - let candidate_hash = CandidateHash(Hash::repeat_byte(42)); - let test_leaf = TestLeaf { - number: 1, - hash: relay_parent, - parent_hash: Hash::repeat_byte(0), - session: 1, - availability_cores: state.make_availability_cores(|i| { - CoreState::Scheduled(ScheduledCore { - para_id: ParaId::from(i as u32), - collator: None, - }) - }), - para_data: (0..state.session_info.validator_groups.len()) - .map(|i| { - ( - ParaId::from(i as u32), - PerParaData { min_relay_parent: 1, head_data: vec![1, 2, 3].into() }, - ) - }) - .collect(), - }; + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + local_para, + test_leaf.para_data(local_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); // peer A is in group, has relay parent in view. let other_group_validators = state.group_validators(local_validator.group_index, true); @@ -521,7 +443,7 @@ fn seconded_statement_leads_to_request() { .await; send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; - activate_leaf(&mut overseer, local_para, &test_leaf, &state, true).await; + activate_leaf(&mut overseer, &test_leaf, &state, true).await; answer_expected_hypothetical_depth_request( &mut overseer, @@ -553,27 +475,37 @@ fn seconded_statement_leads_to_request() { if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } ); + handle_sent_request( + &mut overseer, + peer_a, + candidate_hash, + StatementFilter::blank(group_size), + candidate.clone(), + pvd.clone(), + vec![], + ) + .await; + assert_matches!( overseer.recv().await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendRequests(requests, IfDisconnected::ImmediateError)) => { - assert_eq!(requests.len(), 1); - assert_matches!( - &requests[0], - Requests::AttestedCandidateVStaging(outgoing) => { - assert_eq!(outgoing.peer, Recipient::Peer(peer_a.clone())); - assert_eq!(outgoing.payload.candidate_hash, candidate_hash); - } - ); - } + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_RESPONSE => { } ); + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + overseer }); } #[test] fn cluster_statements_shared_seconded_first() { - let config = TestConfig { validator_count: 20, group_size: 3, local_validator: true }; + let config = TestConfig { + validator_count: 20, + group_size: 3, + local_validator: true, + async_backing_params: None, + }; let relay_parent = Hash::repeat_byte(1); let peer_a = PeerId::random(); @@ -582,37 +514,18 @@ fn cluster_statements_shared_seconded_first() { let local_validator = state.local.clone().unwrap(); let local_para = ParaId::from(local_validator.group_index.0); + let test_leaf = state.make_dummy_leaf(relay_parent); + let (candidate, pvd) = make_candidate( relay_parent, 1, local_para, - vec![1, 2, 3].into(), + test_leaf.para_data(local_para).head_data.clone(), vec![4, 5, 6].into(), Hash::repeat_byte(42).into(), ); let candidate_hash = candidate.hash(); - let test_leaf = TestLeaf { - number: 1, - hash: relay_parent, - parent_hash: Hash::repeat_byte(0), - session: 1, - availability_cores: state.make_availability_cores(|i| { - CoreState::Scheduled(ScheduledCore { - para_id: ParaId::from(i as u32), - collator: None, - }) - }), - para_data: (0..state.session_info.validator_groups.len()) - .map(|i| { - ( - ParaId::from(i as u32), - PerParaData { min_relay_parent: 1, head_data: vec![1, 2, 3].into() }, - ) - }) - .collect(), - }; - // peer A is in group, no relay parent in view. { let other_group_validators = state.group_validators(local_validator.group_index, true); @@ -625,7 +538,7 @@ fn cluster_statements_shared_seconded_first() { .await; } - activate_leaf(&mut overseer, local_para, &test_leaf, &state, true).await; + activate_leaf(&mut overseer, &test_leaf, &state, true).await; answer_expected_hypothetical_depth_request( &mut overseer, @@ -708,7 +621,12 @@ fn cluster_statements_shared_seconded_first() { #[test] fn cluster_accounts_for_implicit_view() { - let config = TestConfig { validator_count: 20, group_size: 3, local_validator: true }; + let config = TestConfig { + validator_count: 20, + group_size: 3, + local_validator: true, + async_backing_params: None, + }; let relay_parent = Hash::repeat_byte(1); let peer_a = PeerId::random(); @@ -718,37 +636,18 @@ fn cluster_accounts_for_implicit_view() { let local_validator = state.local.clone().unwrap(); let local_para = ParaId::from(local_validator.group_index.0); + let test_leaf = state.make_dummy_leaf(relay_parent); + let (candidate, pvd) = make_candidate( relay_parent, 1, local_para, - vec![1, 2, 3].into(), + test_leaf.para_data(local_para).head_data.clone(), vec![4, 5, 6].into(), Hash::repeat_byte(42).into(), ); let candidate_hash = candidate.hash(); - let test_leaf = TestLeaf { - number: 1, - hash: relay_parent, - parent_hash: Hash::repeat_byte(0), - session: 1, - availability_cores: state.make_availability_cores(|i| { - CoreState::Scheduled(ScheduledCore { - para_id: ParaId::from(i as u32), - collator: None, - }) - }), - para_data: (0..state.session_info.validator_groups.len()) - .map(|i| { - ( - ParaId::from(i as u32), - PerParaData { min_relay_parent: 1, head_data: vec![1, 2, 3].into() }, - ) - }) - .collect(), - }; - // peer A is in group, has relay parent in view. // peer B is in group, has no relay parent in view. { @@ -771,7 +670,7 @@ fn cluster_accounts_for_implicit_view() { send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; } - activate_leaf(&mut overseer, local_para, &test_leaf, &state, true).await; + activate_leaf(&mut overseer, &test_leaf, &state, true).await; answer_expected_hypothetical_depth_request( &mut overseer, @@ -820,28 +719,11 @@ fn cluster_accounts_for_implicit_view() { // activate new leaf, which has relay-parent in implicit view. let next_relay_parent = Hash::repeat_byte(2); - let next_test_leaf = TestLeaf { - number: 2, - hash: next_relay_parent, - parent_hash: relay_parent, - session: 1, - availability_cores: state.make_availability_cores(|i| { - CoreState::Scheduled(ScheduledCore { - para_id: ParaId::from(i as u32), - collator: None, - }) - }), - para_data: (0..state.session_info.validator_groups.len()) - .map(|i| { - ( - ParaId::from(i as u32), - PerParaData { min_relay_parent: 1, head_data: vec![1, 2, 3].into() }, - ) - }) - .collect(), - }; + let mut next_test_leaf = state.make_dummy_leaf(next_relay_parent); + next_test_leaf.parent_hash = relay_parent; + next_test_leaf.number = 2; - activate_leaf(&mut overseer, local_para, &next_test_leaf, &state, false).await; + activate_leaf(&mut overseer, &next_test_leaf, &state, false).await; answer_expected_hypothetical_depth_request( &mut overseer, @@ -889,47 +771,33 @@ fn cluster_accounts_for_implicit_view() { #[test] fn cluster_messages_imported_after_confirmed_candidate_importable_check() { - let config = TestConfig { validator_count: 20, group_size: 3, local_validator: true }; + let group_size = 3; + let config = TestConfig { + validator_count: 20, + group_size, + local_validator: true, + async_backing_params: None, + }; let relay_parent = Hash::repeat_byte(1); let peer_a = PeerId::random(); - let peer_b = PeerId::random(); test_harness(config, |state, mut overseer| async move { let local_validator = state.local.clone().unwrap(); let local_para = ParaId::from(local_validator.group_index.0); + let test_leaf = state.make_dummy_leaf(relay_parent); + let (candidate, pvd) = make_candidate( relay_parent, 1, local_para, - vec![1, 2, 3].into(), + test_leaf.para_data(local_para).head_data.clone(), vec![4, 5, 6].into(), Hash::repeat_byte(42).into(), ); let candidate_hash = candidate.hash(); - let test_leaf = TestLeaf { - number: 1, - hash: relay_parent, - parent_hash: Hash::repeat_byte(0), - session: 1, - availability_cores: state.make_availability_cores(|i| { - CoreState::Scheduled(ScheduledCore { - para_id: ParaId::from(i as u32), - collator: None, - }) - }), - para_data: (0..state.session_info.validator_groups.len()) - .map(|i| { - ( - ParaId::from(i as u32), - PerParaData { min_relay_parent: 1, head_data: vec![1, 2, 3].into() }, - ) - }) - .collect(), - }; - // peer A is in group, has relay parent in view. let other_group_validators = state.group_validators(local_validator.group_index, true); let v_a = other_group_validators[0]; @@ -944,7 +812,7 @@ fn cluster_messages_imported_after_confirmed_candidate_importable_check() { send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; } - activate_leaf(&mut overseer, local_para, &test_leaf, &state, true).await; + activate_leaf(&mut overseer, &test_leaf, &state, true).await; answer_expected_hypothetical_depth_request( &mut overseer, @@ -954,54 +822,53 @@ fn cluster_messages_imported_after_confirmed_candidate_importable_check() { ) .await; - let a_seconded = state - .sign_statement( - v_a, - CompactStatement::Seconded(candidate_hash), - &SigningContext { parent_hash: relay_parent, session_index: 1 }, - ) - .as_unchecked() - .clone(); + // Peer sends `Seconded` statement. + { + let a_seconded = state + .sign_statement( + v_a, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); - send_peer_message( - &mut overseer, - peer_a.clone(), - protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, a_seconded), - ) - .await; + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::Statement( + relay_parent, + a_seconded, + ), + ) + .await; - assert_matches!( - overseer.recv().await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) - if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } - ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + } - let req = assert_matches!( - overseer.recv().await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendRequests(mut requests, IfDisconnected::ImmediateError)) => { - assert_eq!(requests.len(), 1); - assert_matches!( - requests.pop().unwrap(), - Requests::AttestedCandidateVStaging(mut outgoing) => { - assert_eq!(outgoing.peer, Recipient::Peer(peer_a.clone())); - assert_eq!(outgoing.payload.candidate_hash, candidate_hash); - - let res = AttestedCandidateResponse { - candidate_receipt: candidate.clone(), - persisted_validation_data: pvd.clone(), - statements: vec![], - }; - outgoing.pending_response.send(Ok(res.encode())); - } - ); - } - ); + // Send a request to peer and mock its response. + { + handle_sent_request( + &mut overseer, + peer_a, + candidate_hash, + StatementFilter::blank(group_size), + candidate.clone(), + pvd.clone(), + vec![], + ) + .await; - assert_matches!( - overseer.recv().await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) - if p == peer_a && r == BENEFIT_VALID_RESPONSE => { } - ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_RESPONSE + ); + } answer_expected_hypothetical_depth_request( &mut overseer, @@ -1039,47 +906,33 @@ fn cluster_messages_imported_after_confirmed_candidate_importable_check() { #[test] fn cluster_messages_imported_after_new_leaf_importable_check() { - let config = TestConfig { validator_count: 20, group_size: 3, local_validator: true }; + let group_size = 3; + let config = TestConfig { + validator_count: 20, + group_size, + local_validator: true, + async_backing_params: None, + }; let relay_parent = Hash::repeat_byte(1); let peer_a = PeerId::random(); - let peer_b = PeerId::random(); test_harness(config, |state, mut overseer| async move { let local_validator = state.local.clone().unwrap(); let local_para = ParaId::from(local_validator.group_index.0); + let test_leaf = state.make_dummy_leaf(relay_parent); + let (candidate, pvd) = make_candidate( relay_parent, 1, local_para, - vec![1, 2, 3].into(), + test_leaf.para_data(local_para).head_data.clone(), vec![4, 5, 6].into(), Hash::repeat_byte(42).into(), ); let candidate_hash = candidate.hash(); - let test_leaf = TestLeaf { - number: 1, - hash: relay_parent, - parent_hash: Hash::repeat_byte(0), - session: 1, - availability_cores: state.make_availability_cores(|i| { - CoreState::Scheduled(ScheduledCore { - para_id: ParaId::from(i as u32), - collator: None, - }) - }), - para_data: (0..state.session_info.validator_groups.len()) - .map(|i| { - ( - ParaId::from(i as u32), - PerParaData { min_relay_parent: 1, head_data: vec![1, 2, 3].into() }, - ) - }) - .collect(), - }; - // peer A is in group, has relay parent in view. let other_group_validators = state.group_validators(local_validator.group_index, true); let v_a = other_group_validators[0]; @@ -1094,7 +947,7 @@ fn cluster_messages_imported_after_new_leaf_importable_check() { send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; } - activate_leaf(&mut overseer, local_para, &test_leaf, &state, true).await; + activate_leaf(&mut overseer, &test_leaf, &state, true).await; answer_expected_hypothetical_depth_request( &mut overseer, @@ -1104,80 +957,62 @@ fn cluster_messages_imported_after_new_leaf_importable_check() { ) .await; - let a_seconded = state - .sign_statement( - v_a, - CompactStatement::Seconded(candidate_hash), - &SigningContext { parent_hash: relay_parent, session_index: 1 }, - ) - .as_unchecked() - .clone(); + // Peer sends `Seconded` statement. + { + let a_seconded = state + .sign_statement( + v_a, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); - send_peer_message( - &mut overseer, - peer_a.clone(), - protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, a_seconded), - ) - .await; + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::Statement( + relay_parent, + a_seconded, + ), + ) + .await; - assert_matches!( - overseer.recv().await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) - if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } - ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + } - let req = assert_matches!( - overseer.recv().await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendRequests(mut requests, IfDisconnected::ImmediateError)) => { - assert_eq!(requests.len(), 1); - assert_matches!( - requests.pop().unwrap(), - Requests::AttestedCandidateVStaging(mut outgoing) => { - assert_eq!(outgoing.peer, Recipient::Peer(peer_a.clone())); - assert_eq!(outgoing.payload.candidate_hash, candidate_hash); - - let res = AttestedCandidateResponse { - candidate_receipt: candidate.clone(), - persisted_validation_data: pvd.clone(), - statements: vec![], - }; - outgoing.pending_response.send(Ok(res.encode())); - } - ); - } - ); + // Send a request to peer and mock its response. + { + handle_sent_request( + &mut overseer, + peer_a, + candidate_hash, + StatementFilter::blank(group_size), + candidate.clone(), + pvd.clone(), + vec![], + ) + .await; - assert_matches!( - overseer.recv().await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) - if p == peer_a && r == BENEFIT_VALID_RESPONSE => { } - ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_RESPONSE => { } + ); + } answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; let next_relay_parent = Hash::repeat_byte(2); - let next_test_leaf = TestLeaf { - number: 2, - hash: next_relay_parent, - parent_hash: relay_parent, - session: 1, - availability_cores: state.make_availability_cores(|i| { - CoreState::Scheduled(ScheduledCore { - para_id: ParaId::from(i as u32), - collator: None, - }) - }), - para_data: (0..state.session_info.validator_groups.len()) - .map(|i| { - ( - ParaId::from(i as u32), - PerParaData { min_relay_parent: 1, head_data: vec![1, 2, 3].into() }, - ) - }) - .collect(), - }; + let mut next_test_leaf = state.make_dummy_leaf(next_relay_parent); + next_test_leaf.parent_hash = relay_parent; + next_test_leaf.number = 2; - activate_leaf(&mut overseer, local_para, &next_test_leaf, &state, false).await; + activate_leaf(&mut overseer, &next_test_leaf, &state, false).await; answer_expected_hypothetical_depth_request( &mut overseer, @@ -1203,7 +1038,7 @@ fn cluster_messages_imported_after_new_leaf_importable_check() { assert_matches!( s.payload(), FullStatementWithPVD::Seconded(c, p) - if c == &candidate && p == &pvd => {} + if c == &candidate && p == &pvd ); assert_eq!(s.validator_index(), v_a); } @@ -1213,4 +1048,210 @@ fn cluster_messages_imported_after_new_leaf_importable_check() { }); } -// TODO [now]: ensure seconding limit is respected +#[test] +fn ensure_seconding_limit_is_respected() { + // `max_candidate_depth: 1` for a `seconding_limit` of 2. + let config = TestConfig { + validator_count: 20, + group_size: 4, + local_validator: true, + async_backing_params: Some(AsyncBackingParameters { + max_candidate_depth: 1, + allowed_ancestry_len: 3, + }), + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_a = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + let local_para = ParaId::from(local_validator.group_index.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate_1, pvd_1) = make_candidate( + relay_parent, + 1, + local_para, + test_leaf.para_data(local_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let (candidate_2, pvd_2) = make_candidate( + relay_parent, + 1, + local_para, + test_leaf.para_data(local_para).head_data.clone(), + vec![7, 8, 9].into(), + Hash::repeat_byte(43).into(), + ); + let (candidate_3, _pvd_3) = make_candidate( + relay_parent, + 1, + local_para, + test_leaf.para_data(local_para).head_data.clone(), + vec![10, 11, 12].into(), + Hash::repeat_byte(44).into(), + ); + let candidate_hash_1 = candidate_1.hash(); + let candidate_hash_2 = candidate_2.hash(); + let candidate_hash_3 = candidate_3.hash(); + + let other_group_validators = state.group_validators(local_validator.group_index, true); + let v_a = other_group_validators[0]; + + // peers A,B,C are in group, have relay parent in view. + { + connect_peer( + &mut overseer, + peer_a.clone(), + Some(vec![state.discovery_id(v_a)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Confirm the candidates locally so that we don't send out requests. + + // Candidate 1. + { + let validator_index = state.local.as_ref().unwrap().validator_index; + let statement = state + .sign_full_statement( + validator_index, + Statement::Seconded(candidate_1), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + pvd_1, + ) + .clone(); + + overseer + .send(FromOrchestra::Communication { + msg: StatementDistributionMessage::Share(relay_parent, statement), + }) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(peers, _)) if peers == vec![peer_a] + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Candidate 2. + { + let validator_index = state.local.as_ref().unwrap().validator_index; + let statement = state + .sign_full_statement( + validator_index, + Statement::Seconded(candidate_2), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + pvd_2, + ) + .clone(); + + overseer + .send(FromOrchestra::Communication { + msg: StatementDistributionMessage::Share(relay_parent, statement), + }) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(peers, _)) if peers == vec![peer_a] + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Send first statement from peer A. + { + let statement = state + .sign_statement( + v_a, + CompactStatement::Seconded(candidate_hash_1), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, statement), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + } + + // Send second statement from peer A. + { + let statement = state + .sign_statement( + v_a, + CompactStatement::Seconded(candidate_hash_2), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, statement), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + } + + // Send third statement from peer A. + { + let statement = state + .sign_statement( + v_a, + CompactStatement::Seconded(candidate_hash_3), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, statement), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == COST_EXCESSIVE_SECONDED => { } + ); + } + + overseer + }); +} diff --git a/node/network/statement-distribution/src/vstaging/tests/grid.rs b/node/network/statement-distribution/src/vstaging/tests/grid.rs index c5eb3826846e..a861755a7681 100644 --- a/node/network/statement-distribution/src/vstaging/tests/grid.rs +++ b/node/network/statement-distribution/src/vstaging/tests/grid.rs @@ -14,28 +14,2442 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -// TODO [now]: backed candidate leads to advertisement to relevant validators with relay-parent +use super::*; -// TODO [now]: received advertisement before confirmation leads to request +use bitvec::order::Lsb0; +use polkadot_node_network_protocol::vstaging::{ + BackedCandidateAcknowledgement, BackedCandidateManifest, +}; +use polkadot_node_subsystem::messages::CandidateBackingMessage; +use polkadot_primitives_test_helpers::make_candidate; -// TODO [now]: received advertisement after backing leads to acknowledgement +// Backed candidate leads to advertisement to relevant validators with relay-parent. +#[test] +fn backed_candidate_leads_to_advertisement() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; -// TODO [now]: received advertisement after confirmation but before backing leads to nothing + let relay_parent = Hash::repeat_byte(1); + let peer_a = PeerId::random(); + let peer_b = PeerId::random(); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); -// TODO [now]: additional statements are shared after manifest exchange + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + let local_para = ParaId::from(local_validator.group_index.0); -// TODO [now]: grid-sending validator view entering relay-parent leads to advertisement + let test_leaf = state.make_dummy_leaf(relay_parent); -// TODO [now]: advertisement not re-sent after re-entering relay parent (view oscillation) + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + local_para, + test_leaf.para_data(local_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); -// TODO [now]: acknowledgements sent only when candidate backed + let other_group_validators = state.group_validators(local_validator.group_index, true); + let target_group_validators = + state.group_validators((local_validator.group_index.0 + 1).into(), true); + let v_a = other_group_validators[0]; + let v_b = other_group_validators[1]; + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; -// TODO [now]: grid statements imported to backing once candidate enters hypothetical frontier + // peer A is in group, has relay parent in view. + // peer B is in group, has no relay parent in view. + // peer C is not in group, has relay parent in view. + // peer D is not in group, has no relay parent in view. + { + connect_peer( + &mut overseer, + peer_a.clone(), + Some(vec![state.discovery_id(v_a)].into_iter().collect()), + ) + .await; -// TODO [now]: advertisements rejected from incorrect peers + connect_peer( + &mut overseer, + peer_b.clone(), + Some(vec![state.discovery_id(v_b)].into_iter().collect()), + ) + .await; -// TODO [now]: manifests rejected with unknown relay parent or when not a validator + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; -// TODO [now]: advertisements rejected when candidate group doers not match para + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; -// TODO [now]: peer reported when advertisement conflicting with confirmed candidate. + send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + // Confirm the candidate locally so that we don't send out requests. + { + let statement = state + .sign_full_statement( + local_validator.validator_index, + Statement::Seconded(candidate.clone()), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + pvd.clone(), + ) + .clone(); + + overseer + .send(FromOrchestra::Communication { + msg: StatementDistributionMessage::Share(relay_parent, statement), + }) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(peers, _)) if peers == vec![peer_a] + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Send enough statements to make candidate backable, make sure announcements are sent. + + // Send statement from peer A. + { + let statement = state + .sign_statement( + v_a, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, statement), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + } + + // Send statement from peer B. + { + let statement = state + .sign_statement( + v_b, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_b.clone(), + protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, statement), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_b && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(peers, _)) if peers == vec![peer_a] + ); + } + + // Send Backed notification. + { + overseer + .send(FromOrchestra::Communication { + msg: StatementDistributionMessage::Backed(candidate_hash), + }) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages:: NetworkBridgeTx( + NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::VStaging( + protocol_vstaging::ValidationProtocol::StatementDistribution( + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest(manifest), + ), + ), + ) + ) => { + assert_eq!(peers, vec![peer_c]); + assert_eq!(manifest, BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: local_validator.group_index, + para_id: local_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }); + } + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + overseer + }); +} + +#[test] +fn received_advertisement_before_confirmation_leads_to_request() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_a = PeerId::random(); + let peer_b = PeerId::random(); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + + let other_group = + next_group_index(local_validator.group_index, validator_count, group_size); + let other_para = ParaId::from(other_group.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let other_group_validators = state.group_validators(local_validator.group_index, true); + let target_group_validators = state.group_validators(other_group, true); + let v_a = other_group_validators[0]; + let v_b = other_group_validators[1]; + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + + // peer A is in group, has relay parent in view. + // peer B is in group, has no relay parent in view. + // peer C is not in group, has relay parent in view. + // peer D is not in group, has relay parent in view. + { + connect_peer( + &mut overseer, + peer_a.clone(), + Some(vec![state.discovery_id(v_a)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_b.clone(), + Some(vec![state.discovery_id(v_b)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_d.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + // Receive an advertisement from C on an unconfirmed candidate. + { + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest(manifest), + ) + .await; + + let statements = vec![ + state + .sign_statement( + v_c, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + state + .sign_statement( + v_d, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + ]; + handle_sent_request( + &mut overseer, + peer_c, + candidate_hash, + StatementFilter::blank(group_size), + candidate.clone(), + pvd.clone(), + statements, + ) + .await; + + // C provided two statements we're seeing for the first time. + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT => { } + ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT => { } + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_RESPONSE => { } + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + overseer + }); +} + +// 1. We receive manifest from grid peer, request, pass votes to backing, then receive Backed +// message. Only then should we send an acknowledgement to the grid peer. +// +// 2. (starting from end state of (1)) we receive a manifest about the same candidate from another +// grid peer and instantaneously acknowledge. +// +// Bit more context about this design choice: Statement-distribution doesn't fully emulate the +// statement logic of backing and only focuses on the number of statements. That means that we might +// request a manifest and for some reason the backing subsystem would still not consider the +// candidate as backed. So, in particular, we don't want to advertise such an unbacked candidate +// along the grid & increase load on ourselves and our peers for serving & importing such a +// candidate. +#[test] +fn received_advertisement_after_backing_leads_to_acknowledgement() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + let peer_e = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + + let other_group = + next_group_index(local_validator.group_index, validator_count, group_size); + let other_para = ParaId::from(other_group.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let target_group_validators = state.group_validators(other_group, true); + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + let v_e = target_group_validators[2]; + + // Connect C, D, E + { + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_e.clone(), + Some(vec![state.discovery_id(v_e)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_d.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_e.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + let statement_c = state + .sign_statement( + v_c, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + let statement_d = state + .sign_statement( + v_d, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + // Receive an advertisement from C. + { + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + + // Should send a request to C. + let statements = vec![ + statement_c.clone(), + statement_d.clone(), + state + .sign_statement( + v_e, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + ]; + handle_sent_request( + &mut overseer, + peer_c, + candidate_hash, + StatementFilter::blank(group_size), + candidate.clone(), + pvd.clone(), + statements, + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_RESPONSE + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Receive Backed message. + overseer + .send(FromOrchestra::Communication { + msg: StatementDistributionMessage::Backed(candidate_hash), + }) + .await; + + // Should send an acknowledgement back to C. + { + assert_matches!( + overseer.recv().await, + AllMessages:: NetworkBridgeTx( + NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::VStaging( + protocol_vstaging::ValidationProtocol::StatementDistribution( + protocol_vstaging::StatementDistributionMessage::BackedCandidateKnown(ack), + ), + ), + ) + ) => { + assert_eq!(peers, vec![peer_c]); + assert_eq!(ack, BackedCandidateAcknowledgement { + candidate_hash, + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }); + } + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Receive a manifest about the same candidate from peer D. + { + send_peer_message( + &mut overseer, + peer_d.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + + let expected_ack = BackedCandidateAcknowledgement { + candidate_hash, + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + // Instantaneously acknowledge. + assert_matches!( + overseer.recv().await, + AllMessages:: NetworkBridgeTx( + NetworkBridgeTxMessage::SendValidationMessages(messages) + ) => { + assert_eq!(messages.len(), 1); + assert_eq!(messages[0].0, vec![peer_d]); + + assert_matches!( + &messages[0].1, + Versioned::VStaging(protocol_vstaging::ValidationProtocol::StatementDistribution( + protocol_vstaging::StatementDistributionMessage::BackedCandidateKnown(ack) + )) if *ack == expected_ack + ); + } + ); + } + + overseer + }); +} + +// Received advertisement after confirmation but before backing leads to nothing. +#[test] +fn received_advertisement_after_confirmation_before_backing() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + let peer_e = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + + let other_group = + next_group_index(local_validator.group_index, validator_count, group_size); + let other_para = ParaId::from(other_group.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let target_group_validators = state.group_validators(other_group, true); + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + let v_e = target_group_validators[2]; + + // Connect C, D, E + { + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_e.clone(), + Some(vec![state.discovery_id(v_e)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_d.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_e.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + let statement_c = state + .sign_statement( + v_c, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + let statement_d = state + .sign_statement( + v_d, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + // Receive an advertisement from C. + { + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + + // Should send a request to C. + let statements = vec![ + statement_c.clone(), + statement_d.clone(), + state + .sign_statement( + v_e, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + ]; + handle_sent_request( + &mut overseer, + peer_c, + candidate_hash, + StatementFilter::blank(group_size), + candidate.clone(), + pvd.clone(), + statements, + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_RESPONSE + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Receive advertisement from peer D (after confirmation but before backing). + { + send_peer_message( + &mut overseer, + peer_d.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + } + + overseer + }); +} + +#[test] +fn additional_statements_are_shared_after_manifest_exchange() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + let peer_e = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + + let other_group = + next_group_index(local_validator.group_index, validator_count, group_size); + let other_para = ParaId::from(other_group.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let target_group_validators = state.group_validators(other_group, true); + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + let v_e = target_group_validators[2]; + + // Connect C, D, E + { + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_e.clone(), + Some(vec![state.discovery_id(v_e)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_d.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_e.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + // Receive an advertisement from C. + { + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + } + + // Should send a request to C. + { + let statements = vec![ + state + .sign_statement( + v_d, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + state + .sign_statement( + v_e, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + ]; + + handle_sent_request( + &mut overseer, + peer_c, + candidate_hash, + StatementFilter::blank(group_size), + candidate.clone(), + pvd.clone(), + statements, + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_RESPONSE + ); + } + + let hypothetical = HypotheticalCandidate::Complete { + candidate_hash, + receipt: Arc::new(candidate.clone()), + persisted_validation_data: pvd.clone(), + }; + let membership = vec![(relay_parent, vec![0])]; + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![(hypothetical, membership)], + None, + false, + ) + .await; + + // Statements are sent to the Backing subsystem. + { + assert_matches!( + overseer.recv().await, + AllMessages::CandidateBacking( + CandidateBackingMessage::Statement(hash, statement) + ) => { + assert_eq!(hash, relay_parent); + assert_matches!( + statement.payload(), + FullStatementWithPVD::Seconded(c, p) + if c == &candidate && p == &pvd + ); + } + ); + assert_matches!( + overseer.recv().await, + AllMessages::CandidateBacking( + CandidateBackingMessage::Statement(hash, statement) + ) => { + assert_eq!(hash, relay_parent); + assert_matches!( + statement.payload(), + FullStatementWithPVD::Seconded(c, p) + if c == &candidate && p == &pvd + ); + } + ); + } + + // Receive Backed message. + overseer + .send(FromOrchestra::Communication { + msg: StatementDistributionMessage::Backed(candidate_hash), + }) + .await; + + // Should send an acknowledgement back to C. + { + assert_matches!( + overseer.recv().await, + AllMessages:: NetworkBridgeTx( + NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::VStaging( + protocol_vstaging::ValidationProtocol::StatementDistribution( + protocol_vstaging::StatementDistributionMessage::BackedCandidateKnown(ack), + ), + ), + ) + ) => { + assert_eq!(peers, vec![peer_c]); + assert_eq!(ack, BackedCandidateAcknowledgement { + candidate_hash, + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }); + } + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Receive a manifest about the same candidate from peer D. Contains different statements. + { + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 0], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + send_peer_message( + &mut overseer, + peer_d.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + + let expected_ack = BackedCandidateAcknowledgement { + candidate_hash, + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + // Instantaneously acknowledge. + assert_matches!( + overseer.recv().await, + AllMessages:: NetworkBridgeTx( + NetworkBridgeTxMessage::SendValidationMessages(messages) + ) => { + assert_eq!(messages.len(), 2); + assert_eq!(messages[0].0, vec![peer_d]); + assert_eq!(messages[1].0, vec![peer_d]); + + assert_matches!( + &messages[0].1, + Versioned::VStaging(protocol_vstaging::ValidationProtocol::StatementDistribution( + protocol_vstaging::StatementDistributionMessage::BackedCandidateKnown(ack) + )) if *ack == expected_ack + ); + + assert_matches!( + &messages[1].1, + Versioned::VStaging(protocol_vstaging::ValidationProtocol::StatementDistribution( + protocol_vstaging::StatementDistributionMessage::Statement(r, s) + )) if *r == relay_parent && s.unchecked_payload() == &CompactStatement::Seconded(candidate_hash) && s.unchecked_validator_index() == v_e + ); + } + ); + } + + overseer + }); +} + +// Grid-sending validator view entering relay-parent leads to advertisement. +#[test] +fn advertisement_sent_when_peer_enters_relay_parent_view() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_a = PeerId::random(); + let peer_b = PeerId::random(); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + let local_para = ParaId::from(local_validator.group_index.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + local_para, + test_leaf.para_data(local_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let other_group_validators = state.group_validators(local_validator.group_index, true); + let target_group_validators = + state.group_validators((local_validator.group_index.0 + 1).into(), true); + let v_a = other_group_validators[0]; + let v_b = other_group_validators[1]; + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + + // peer A is in group, has relay parent in view. + // peer B is in group, has no relay parent in view. + // peer C is not in group, has relay parent in view. + // peer D is not in group, has no relay parent in view. + { + connect_peer( + &mut overseer, + peer_a.clone(), + Some(vec![state.discovery_id(v_a)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_b.clone(), + Some(vec![state.discovery_id(v_b)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + // Confirm the candidate locally so that we don't send out requests. + { + let statement = state + .sign_full_statement( + local_validator.validator_index, + Statement::Seconded(candidate.clone()), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + pvd.clone(), + ) + .clone(); + + overseer + .send(FromOrchestra::Communication { + msg: StatementDistributionMessage::Share(relay_parent, statement), + }) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(peers, _)) if peers == vec![peer_a] + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Send enough statements to make candidate backable, make sure announcements are sent. + + // Send statement from peer A. + { + let statement = state + .sign_statement( + v_a, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, statement), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + } + + // Send statement from peer B. + { + let statement = state + .sign_statement( + v_b, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_b.clone(), + protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, statement), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_b && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(peers, _)) if peers == vec![peer_a] + ); + } + + // Send Backed notification. + overseer + .send(FromOrchestra::Communication { + msg: StatementDistributionMessage::Backed(candidate_hash), + }) + .await; + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + + // Relay parent enters view of peer C. + { + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + + let expected_manifest = BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: local_validator.group_index, + para_id: local_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + assert_matches!( + overseer.recv().await, + AllMessages:: NetworkBridgeTx( + NetworkBridgeTxMessage::SendValidationMessages(messages) + ) => { + assert_eq!(messages.len(), 1); + assert_eq!(messages[0].0, vec![peer_c]); + + assert_matches!( + &messages[0].1, + Versioned::VStaging(protocol_vstaging::ValidationProtocol::StatementDistribution( + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest(manifest) + )) => { + assert_eq!(*manifest, expected_manifest); + } + ); + } + ); + } + + overseer + }); +} + +// Advertisement not re-sent after re-entering relay parent (view oscillation). +#[test] +fn advertisement_not_re_sent_when_peer_re_enters_view() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_a = PeerId::random(); + let peer_b = PeerId::random(); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + let local_para = ParaId::from(local_validator.group_index.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + local_para, + test_leaf.para_data(local_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let other_group_validators = state.group_validators(local_validator.group_index, true); + let target_group_validators = + state.group_validators((local_validator.group_index.0 + 1).into(), true); + let v_a = other_group_validators[0]; + let v_b = other_group_validators[1]; + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + + // peer A is in group, has relay parent in view. + // peer B is in group, has no relay parent in view. + // peer C is not in group, has relay parent in view. + // peer D is not in group, has no relay parent in view. + { + connect_peer( + &mut overseer, + peer_a.clone(), + Some(vec![state.discovery_id(v_a)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_b.clone(), + Some(vec![state.discovery_id(v_b)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + // Confirm the candidate locally so that we don't send out requests. + { + let statement = state + .sign_full_statement( + local_validator.validator_index, + Statement::Seconded(candidate.clone()), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + pvd.clone(), + ) + .clone(); + + overseer + .send(FromOrchestra::Communication { + msg: StatementDistributionMessage::Share(relay_parent, statement), + }) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(peers, _)) if peers == vec![peer_a] + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Send enough statements to make candidate backable, make sure announcements are sent. + + // Send statement from peer A. + { + let statement = state + .sign_statement( + v_a, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, statement), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + } + + // Send statement from peer B. + { + let statement = state + .sign_statement( + v_b, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_b.clone(), + protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, statement), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_b && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(peers, _)) if peers == vec![peer_a] + ); + } + + // Send Backed notification. + { + overseer + .send(FromOrchestra::Communication { + msg: StatementDistributionMessage::Backed(candidate_hash), + }) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages:: NetworkBridgeTx( + NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::VStaging( + protocol_vstaging::ValidationProtocol::StatementDistribution( + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest(manifest), + ), + ), + ) + ) => { + assert_eq!(peers, vec![peer_c]); + assert_eq!(manifest, BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: local_validator.group_index, + para_id: local_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }); + } + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Peer leaves view. + send_peer_view_change(&mut overseer, peer_c.clone(), view![]).await; + + // Peer re-enters view. + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + + overseer + }); +} + +// Grid statements imported to backing once candidate enters hypothetical frontier. +#[test] +fn grid_statements_imported_to_backing() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + let peer_e = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + + let other_group = + next_group_index(local_validator.group_index, validator_count, group_size); + let other_para = ParaId::from(other_group.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let target_group_validators = state.group_validators(other_group, true); + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + let v_e = target_group_validators[2]; + + // Connect C, D, E + { + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_e.clone(), + Some(vec![state.discovery_id(v_e)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_d.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_e.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + // Receive an advertisement from C. + { + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + } + + // Should send a request to C. + { + let statements = vec![ + state + .sign_statement( + v_d, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + state + .sign_statement( + v_e, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + ]; + + handle_sent_request( + &mut overseer, + peer_c, + candidate_hash, + StatementFilter::blank(group_size), + candidate.clone(), + pvd.clone(), + statements, + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_RESPONSE + ); + } + + let hypothetical = HypotheticalCandidate::Complete { + candidate_hash, + receipt: Arc::new(candidate.clone()), + persisted_validation_data: pvd.clone(), + }; + let membership = vec![(relay_parent, vec![0])]; + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![(hypothetical, membership)], + None, + false, + ) + .await; + + // Receive messages from Backing subsystem. + { + assert_matches!( + overseer.recv().await, + AllMessages::CandidateBacking( + CandidateBackingMessage::Statement(hash, statement) + ) => { + assert_eq!(hash, relay_parent); + assert_matches!( + statement.payload(), + FullStatementWithPVD::Seconded(c, p) + if c == &candidate && p == &pvd + ); + } + ); + assert_matches!( + overseer.recv().await, + AllMessages::CandidateBacking( + CandidateBackingMessage::Statement(hash, statement) + ) => { + assert_eq!(hash, relay_parent); + assert_matches!( + statement.payload(), + FullStatementWithPVD::Seconded(c, p) + if c == &candidate && p == &pvd + ); + } + ); + } + + overseer + }); +} + +#[test] +fn advertisements_rejected_from_incorrect_peers() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_a = PeerId::random(); + let peer_b = PeerId::random(); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + + let other_group = + next_group_index(local_validator.group_index, validator_count, group_size); + let other_para = ParaId::from(other_group.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let other_group_validators = state.group_validators(local_validator.group_index, true); + let target_group_validators = state.group_validators(other_group, true); + let v_a = other_group_validators[0]; + let v_b = other_group_validators[1]; + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + + // peer A is in group, has relay parent in view. + // peer B is in group, has no relay parent in view. + // peer C is not in group, has relay parent in view. + // peer D is not in group, has no relay parent in view. + { + connect_peer( + &mut overseer, + peer_a.clone(), + Some(vec![state.discovery_id(v_a)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_b.clone(), + Some(vec![state.discovery_id(v_b)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + // Receive an advertisement from A (our group). + { + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == COST_UNEXPECTED_MANIFEST_DISALLOWED => { } + ); + } + + // Receive an advertisement from B (our group). + { + send_peer_message( + &mut overseer, + peer_b.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest(manifest), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_b && r == COST_UNEXPECTED_MANIFEST_DISALLOWED => { } + ); + } + + overseer + }); +} + +#[test] +fn manifest_rejected_with_unknown_relay_parent() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let unknown_parent = Hash::repeat_byte(2); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + + let other_group = + next_group_index(local_validator.group_index, validator_count, group_size); + let other_para = ParaId::from(other_group.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + unknown_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let target_group_validators = state.group_validators(other_group, true); + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + + // peer C is not in group, has relay parent in view. + // peer D is not in group, has no relay parent in view. + { + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + let manifest = BackedCandidateManifest { + relay_parent: unknown_parent, + candidate_hash, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + // Receive an advertisement from C. + { + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == COST_UNEXPECTED_MANIFEST_MISSING_KNOWLEDGE => { } + ); + } + + overseer + }); +} + +#[test] +fn manifest_rejected_when_not_a_validator() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: false, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let other_group = GroupIndex::from(0); + let other_para = ParaId::from(other_group.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let target_group_validators = state.group_validators(other_group, true); + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + + // peer C is not in group, has relay parent in view. + // peer D is not in group, has no relay parent in view. + { + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + // Receive an advertisement from C. + { + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == COST_UNEXPECTED_MANIFEST_MISSING_KNOWLEDGE => { } + ); + } + + overseer + }); +} + +#[test] +fn manifest_rejected_when_group_does_not_match_para() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + + let other_group = + next_group_index(local_validator.group_index, validator_count, group_size); + // Create a mismatch between group and para. + let other_para = next_group_index(other_group, validator_count, group_size); + let other_para = ParaId::from(other_para.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let target_group_validators = state.group_validators(other_group, true); + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + + // peer C is not in group, has relay parent in view. + // peer D is not in group, has no relay parent in view. + { + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + // Receive an advertisement from C. + { + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == COST_MALFORMED_MANIFEST => { } + ); + } + + overseer + }); +} + +#[test] +fn peer_reported_for_advertisement_conflicting_with_confirmed_candidate() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + let peer_e = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + + let other_group = + next_group_index(local_validator.group_index, validator_count, group_size); + let other_para = ParaId::from(other_group.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let target_group_validators = state.group_validators(other_group, true); + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + let v_e = target_group_validators[2]; + + // Connect C, D, E + { + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_e.clone(), + Some(vec![state.discovery_id(v_e)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_d.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_e.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 0], + }, + }; + + let statement_c = state + .sign_statement( + v_c, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + let statement_d = state + .sign_statement( + v_d, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + // Receive an advertisement from C. + { + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + + // Should send a request to C. + let statements = vec![ + statement_c.clone(), + statement_d.clone(), + state + .sign_statement( + v_e, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + ]; + + handle_sent_request( + &mut overseer, + peer_c, + candidate_hash, + StatementFilter::blank(group_size), + candidate.clone(), + pvd.clone(), + statements, + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_RESPONSE + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Receive conflicting advertisement from peer C after confirmation. + // + // NOTE: This causes a conflict because we track received manifests on a per-validator basis, + // and this is the second time we're getting a manifest from C. + { + let mut manifest = manifest.clone(); + manifest.statement_knowledge = StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 0], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }; + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest(manifest), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == COST_CONFLICTING_MANIFEST + ); + } + + overseer + }); +} diff --git a/node/network/statement-distribution/src/vstaging/tests/mod.rs b/node/network/statement-distribution/src/vstaging/tests/mod.rs index 583d17616629..f290c860ac5c 100644 --- a/node/network/statement-distribution/src/vstaging/tests/mod.rs +++ b/node/network/statement-distribution/src/vstaging/tests/mod.rs @@ -14,15 +14,14 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -// TODO [now]: Remove once some tests are written. -#![allow(unused)] - use super::*; use crate::*; use polkadot_node_network_protocol::{ + grid_topology::TopologyPeerInfo, request_response::{outgoing::Recipient, ReqProtocolNames}, view, ObservedRole, }; +use polkadot_node_primitives::Statement; use polkadot_node_subsystem::messages::{ network_bridge_event::NewGossipTopology, AllMessages, ChainApiMessage, FragmentTreeMembership, HypotheticalCandidate, NetworkBridgeEvent, ProspectiveParachainsMessage, RuntimeApiMessage, @@ -31,10 +30,9 @@ use polkadot_node_subsystem::messages::{ use polkadot_node_subsystem_test_helpers as test_helpers; use polkadot_node_subsystem_types::{jaeger, ActivatedLeaf, LeafStatus}; use polkadot_primitives::vstaging::{ - AssignmentId, AssignmentPair, AsyncBackingParameters, BlockNumber, CandidateCommitments, - CandidateDescriptor, CommittedCandidateReceipt, CoreState, GroupRotationInfo, HeadData, Header, - IndexedVec, PersistedValidationData, ScheduledCore, SessionIndex, SessionInfo, - ValidationCodeHash, ValidatorPair, + AssignmentPair, AsyncBackingParameters, BlockNumber, CommittedCandidateReceipt, CoreState, + GroupRotationInfo, HeadData, Header, IndexedVec, PersistedValidationData, ScheduledCore, + SessionIndex, SessionInfo, ValidatorPair, }; use sc_keystore::LocalKeystore; use sp_application_crypto::Pair as PairT; @@ -54,7 +52,7 @@ mod requests; type VirtualOverseer = test_helpers::TestSubsystemContextHandle; -const ASYNC_BACKING_PARAMETERS: AsyncBackingParameters = +const DEFAULT_ASYNC_BACKING_PARAMETERS: AsyncBackingParameters = AsyncBackingParameters { max_candidate_depth: 4, allowed_ancestry_len: 3 }; // Some deterministic genesis hash for req/res protocol names @@ -66,11 +64,11 @@ struct TestConfig { group_size: usize, // whether the local node should be a validator local_validator: bool, + async_backing_params: Option, } -#[derive(Clone)] +#[derive(Debug, Clone)] struct TestLocalValidator { - validator_id: ValidatorId, validator_index: ValidatorIndex, group_index: GroupIndex, } @@ -129,7 +127,6 @@ impl TestState { let local = if let Some(local_pos) = local_validator_pos { Some(TestLocalValidator { - validator_id: validators[local_pos].public().clone(), validator_index: ValidatorIndex(local_pos as _), group_index: GroupIndex((local_pos / config.group_size) as _), }) @@ -157,10 +154,46 @@ impl TestState { TestState { config, local, validators, session_info, req_sender } } + fn make_dummy_leaf(&self, relay_parent: Hash) -> TestLeaf { + TestLeaf { + number: 1, + hash: relay_parent, + parent_hash: Hash::repeat_byte(0), + session: 1, + availability_cores: self.make_availability_cores(|i| { + CoreState::Scheduled(ScheduledCore { + para_id: ParaId::from(i as u32), + collator: None, + }) + }), + para_data: (0..self.session_info.validator_groups.len()) + .map(|i| (ParaId::from(i as u32), PerParaData::new(1, vec![1, 2, 3].into()))) + .collect(), + } + } + fn make_availability_cores(&self, f: impl Fn(usize) -> CoreState) -> Vec { (0..self.session_info.validator_groups.len()).map(f).collect() } + fn make_dummy_topology(&self) -> NewGossipTopology { + let validator_count = self.config.validator_count; + NewGossipTopology { + session: 1, + topology: SessionGridTopology::new( + (0..validator_count).collect(), + (0..validator_count) + .map(|i| TopologyPeerInfo { + peer_ids: Vec::new(), + validator_index: ValidatorIndex(i as u32), + discovery_id: AuthorityDiscoveryPair::generate().0.public(), + }) + .collect(), + ), + local_index: self.local.as_ref().map(|local| local.validator_index), + } + } + fn group_validators( &self, group_index: GroupIndex, @@ -178,10 +211,6 @@ impl TestState { .collect() } - fn validator_id(&self, validator_index: ValidatorIndex) -> ValidatorId { - self.session_info.validators.get(validator_index).unwrap().clone() - } - fn discovery_id(&self, validator_index: ValidatorIndex) -> AuthorityDiscoveryId { self.session_info.discovery_keys[validator_index.0 as usize].clone() } @@ -200,6 +229,27 @@ impl TestState { .unwrap() } + fn sign_full_statement( + &self, + validator_index: ValidatorIndex, + statement: Statement, + context: &SigningContext, + pvd: PersistedValidationData, + ) -> SignedFullStatementWithPVD { + let payload = statement.to_compact().signing_payload(context); + let pair = &self.validators[validator_index.0 as usize]; + let signature = pair.sign(&payload[..]); + + SignedFullStatementWithPVD::new( + statement.supply_pvd(pvd), + validator_index, + signature, + context, + &pair.public(), + ) + .unwrap() + } + // send a request out, returning a future which expects a response. async fn send_request( &mut self, @@ -258,6 +308,11 @@ fn test_harness>( futures::executor::block_on(future::join( async move { let mut virtual_overseer = test_fut.await; + // Ensure we have handled all responses. + if let Ok(Some(msg)) = virtual_overseer.rx.try_next() { + panic!("Did not handle all responses: {:?}", msg); + } + // Conclude. virtual_overseer.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await; }, subsystem, @@ -295,7 +350,6 @@ impl TestLeaf { async fn activate_leaf( virtual_overseer: &mut VirtualOverseer, - para_id: ParaId, leaf: &TestLeaf, test_state: &TestState, expect_session_info_request: bool, @@ -313,32 +367,23 @@ async fn activate_leaf( )))) .await; - handle_leaf_activation( - virtual_overseer, - para_id, - leaf, - test_state, - expect_session_info_request, - ) - .await; + handle_leaf_activation(virtual_overseer, leaf, test_state, expect_session_info_request).await; } async fn handle_leaf_activation( virtual_overseer: &mut VirtualOverseer, - para_id: ParaId, leaf: &TestLeaf, test_state: &TestState, expect_session_info_request: bool, ) { let TestLeaf { number, hash, parent_hash, para_data, session, availability_cores } = leaf; - let PerParaData { min_relay_parent, head_data } = leaf.para_data(para_id); assert_matches!( virtual_overseer.recv().await, AllMessages::RuntimeApi( RuntimeApiMessage::Request(parent, RuntimeApiRequest::StagingAsyncBackingParameters(tx)) ) if parent == *hash => { - tx.send(Ok(ASYNC_BACKING_PARAMETERS)).unwrap(); + tx.send(Ok(test_state.config.async_backing_params.unwrap_or(DEFAULT_ASYNC_BACKING_PARAMETERS))).unwrap(); } ); @@ -402,13 +447,46 @@ async fn handle_leaf_activation( assert_matches!( virtual_overseer.recv().await, AllMessages::RuntimeApi( - RuntimeApiMessage::Request(parent, RuntimeApiRequest::SessionInfo(s, tx))) if s == *session => { + RuntimeApiMessage::Request(parent, RuntimeApiRequest::SessionInfo(s, tx))) if parent == *hash && s == *session => { tx.send(Ok(Some(test_state.session_info.clone()))).unwrap(); } ); } } +/// Intercepts an outgoing request, checks the fields, and sends the response. +async fn handle_sent_request( + virtual_overseer: &mut VirtualOverseer, + peer: PeerId, + candidate_hash: CandidateHash, + mask: StatementFilter, + candidate_receipt: CommittedCandidateReceipt, + persisted_validation_data: PersistedValidationData, + statements: Vec, +) { + assert_matches!( + virtual_overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendRequests(mut requests, IfDisconnected::ImmediateError)) => { + assert_eq!(requests.len(), 1); + assert_matches!( + requests.pop().unwrap(), + Requests::AttestedCandidateVStaging(outgoing) => { + assert_eq!(outgoing.peer, Recipient::Peer(peer)); + assert_eq!(outgoing.payload.candidate_hash, candidate_hash); + assert_eq!(outgoing.payload.mask, mask); + + let res = AttestedCandidateResponse { + candidate_receipt, + persisted_validation_data, + statements, + }; + outgoing.pending_response.send(Ok(res.encode())).unwrap(); + } + ); + } + ); +} + async fn answer_expected_hypothetical_depth_request( virtual_overseer: &mut VirtualOverseer, responses: Vec<(HypotheticalCandidate, FragmentTreeMembership)>, @@ -430,7 +508,7 @@ async fn answer_expected_hypothetical_depth_request( ); } - tx.send(responses); + tx.send(responses).unwrap(); } ) } @@ -458,6 +536,8 @@ async fn connect_peer( .await; } +// TODO: Add some tests using this? +#[allow(dead_code)] async fn disconnect_peer(virtual_overseer: &mut VirtualOverseer, peer: PeerId) { virtual_overseer .send(FromOrchestra::Communication { @@ -501,3 +581,14 @@ async fn send_new_topology(virtual_overseer: &mut VirtualOverseer, topology: New }) .await; } + +fn next_group_index( + group_index: GroupIndex, + validator_count: usize, + group_size: usize, +) -> GroupIndex { + let next_group = group_index.0 + 1; + let num_groups = + validator_count / group_size + if validator_count % group_size > 0 { 1 } else { 0 }; + GroupIndex::from(next_group % num_groups as u32) +} diff --git a/node/network/statement-distribution/src/vstaging/tests/requests.rs b/node/network/statement-distribution/src/vstaging/tests/requests.rs index 5e624bd622eb..313f2831a992 100644 --- a/node/network/statement-distribution/src/vstaging/tests/requests.rs +++ b/node/network/statement-distribution/src/vstaging/tests/requests.rs @@ -14,14 +14,1559 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -// TODO [now]: peer reported for providing statements meant to be masked out +use super::*; -// TODO [now]: peer reported for not providing enough statements, request retried +use bitvec::order::Lsb0; +use parity_scale_codec::{Decode, Encode}; +use polkadot_node_network_protocol::{ + request_response::vstaging as request_vstaging, vstaging::BackedCandidateManifest, +}; +use polkadot_primitives_test_helpers::make_candidate; +use sc_network::config::{ + IncomingRequest as RawIncomingRequest, OutgoingResponse as RawOutgoingResponse, +}; -// TODO [now]: peer reported for providing duplicate statements +#[test] +fn cluster_peer_allowed_to_send_incomplete_statements() { + let group_size = 3; + let config = TestConfig { + validator_count: 20, + group_size, + local_validator: true, + async_backing_params: None, + }; -// TODO [now]: peer reported for providing statements with invalid signatures or wrong validator IDs + let relay_parent = Hash::repeat_byte(1); + let peer_a = PeerId::random(); + let peer_b = PeerId::random(); + let peer_c = PeerId::random(); -// TODO [now]: local node sanity checks incoming requests + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + let local_para = ParaId::from(local_validator.group_index.0); -// TODO [now]: local node respects statement mask + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + local_para, + test_leaf.para_data(local_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let other_group_validators = state.group_validators(local_validator.group_index, true); + let v_a = other_group_validators[0]; + let v_b = other_group_validators[1]; + + // peer A is in group, has relay parent in view. + // peer B is in group, has no relay parent in view. + // peer C is not in group, has relay parent in view. + { + connect_peer( + &mut overseer, + peer_a.clone(), + Some(vec![state.discovery_id(other_group_validators[0])].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_b.clone(), + Some(vec![state.discovery_id(other_group_validators[1])].into_iter().collect()), + ) + .await; + + connect_peer(&mut overseer, peer_c.clone(), None).await; + + send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Peer in cluster sends a statement, triggering a request. + { + let a_seconded = state + .sign_statement( + v_a, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::Statement( + relay_parent, + a_seconded, + ), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + } + + // Send a request to peer and mock its response to include just one statement. + { + let b_seconded = state + .sign_statement( + v_b, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + let statements = vec![b_seconded.clone()]; + // `1` indicates statements NOT to request. + let mask = StatementFilter::blank(group_size); + handle_sent_request( + &mut overseer, + peer_a, + candidate_hash, + mask, + candidate.clone(), + pvd.clone(), + statements, + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT => { } + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_RESPONSE => { } + ); + + assert_matches!( + overseer.recv().await, + AllMessages:: NetworkBridgeTx( + NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::VStaging( + protocol_vstaging::ValidationProtocol::StatementDistribution( + protocol_vstaging::StatementDistributionMessage::Statement(hash, statement), + ), + ), + ) + ) => { + assert_eq!(peers, vec![peer_a]); + assert_eq!(hash, relay_parent); + assert_eq!(statement, b_seconded); + } + ); + } + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + + overseer + }); +} + +#[test] +fn peer_reported_for_providing_statements_meant_to_be_masked_out() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: Some(AsyncBackingParameters { + // Makes `seconding_limit: 2` (easier to hit the limit). + max_candidate_depth: 1, + allowed_ancestry_len: 3, + }), + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + let peer_e = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + + let other_group = + next_group_index(local_validator.group_index, validator_count, group_size); + let other_para = ParaId::from(other_group.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate_1, pvd_1) = make_candidate( + relay_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let (candidate_2, pvd_2) = make_candidate( + relay_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![7, 8, 9].into(), + Hash::repeat_byte(43).into(), + ); + let (candidate_3, pvd_3) = make_candidate( + relay_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![10, 11, 12].into(), + Hash::repeat_byte(44).into(), + ); + let candidate_hash_1 = candidate_1.hash(); + let candidate_hash_2 = candidate_2.hash(); + let candidate_hash_3 = candidate_3.hash(); + + let target_group_validators = state.group_validators(other_group, true); + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + let v_e = target_group_validators[2]; + + // Connect C, D, E + { + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_e.clone(), + Some(vec![state.discovery_id(v_e)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_d.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_e.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + // Peer C advertises candidate 1. + { + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash: candidate_hash_1, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd_1.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 0], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + + let statements = vec![ + state + .sign_statement( + v_c, + CompactStatement::Seconded(candidate_hash_1), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + state + .sign_statement( + v_d, + CompactStatement::Seconded(candidate_hash_1), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + ]; + handle_sent_request( + &mut overseer, + peer_c, + candidate_hash_1, + StatementFilter::blank(group_size), + candidate_1.clone(), + pvd_1.clone(), + statements, + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_RESPONSE + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Peer C advertises candidate 2. + { + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash: candidate_hash_2, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd_2.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + + let statements = vec![ + state + .sign_statement( + v_d, + CompactStatement::Seconded(candidate_hash_2), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + state + .sign_statement( + v_e, + CompactStatement::Seconded(candidate_hash_2), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + ]; + handle_sent_request( + &mut overseer, + peer_c, + candidate_hash_2, + StatementFilter::blank(group_size), + candidate_2.clone(), + pvd_2.clone(), + statements, + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_RESPONSE + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Peer C sends an announcement for candidate 3. Should hit seconding limit for validator 1. + // + // NOTE: The manifest is immediately rejected before a request is made due to + // "over-seconding" validator 1. On the other hand, if the manifest does not include + // validator 1 as a seconder, then including its Second statement in the response instead + // would fail with "Un-requested Statement In Response". + { + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash: candidate_hash_3, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd_3.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }; + + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == COST_EXCESSIVE_SECONDED + ); + } + + overseer + }); +} + +// Peer reported for not providing enough statements, request retried. +#[test] +fn peer_reported_for_not_enough_statements() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + let peer_e = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + + let other_group = + next_group_index(local_validator.group_index, validator_count, group_size); + let other_para = ParaId::from(other_group.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + other_para, + test_leaf.para_data(other_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let target_group_validators = state.group_validators(other_group, true); + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + let v_e = target_group_validators[2]; + + // Connect C, D, E + { + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_e.clone(), + Some(vec![state.discovery_id(v_e)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_d.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_e.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + let manifest = BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: other_group, + para_id: other_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 0], + }, + }; + + // Peer sends an announcement. + send_peer_message( + &mut overseer, + peer_c.clone(), + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest( + manifest.clone(), + ), + ) + .await; + let c_seconded = state + .sign_statement( + v_c, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + let statements = vec![c_seconded.clone()]; + // `1` indicates statements NOT to request. + let mask = StatementFilter::blank(group_size); + + // We send a request to peer. Mock its response to include just one statement. + { + handle_sent_request( + &mut overseer, + peer_c, + candidate_hash, + mask.clone(), + candidate.clone(), + pvd.clone(), + statements.clone(), + ) + .await; + + // The peer is reported for only sending one statement. + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == COST_INVALID_RESPONSE => { } + ); + } + + // We re-try the request. + { + let statements = vec![ + c_seconded, + state + .sign_statement( + v_d, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + state + .sign_statement( + v_e, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(), + ]; + handle_sent_request( + &mut overseer, + peer_c, + candidate_hash, + mask, + candidate.clone(), + pvd.clone(), + statements.clone(), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_STATEMENT + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_c && r == BENEFIT_VALID_RESPONSE + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + overseer + }); +} + +// Test that a peer answering an `AttestedCandidateRequest` with duplicate statements is punished. +#[test] +fn peer_reported_for_duplicate_statements() { + let group_size = 3; + let config = TestConfig { + validator_count: 20, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_a = PeerId::random(); + let peer_b = PeerId::random(); + let peer_c = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + let local_para = ParaId::from(local_validator.group_index.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + local_para, + test_leaf.para_data(local_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let other_group_validators = state.group_validators(local_validator.group_index, true); + let v_a = other_group_validators[0]; + let v_b = other_group_validators[1]; + + // peer A is in group, has relay parent in view. + // peer B is in group, has no relay parent in view. + // peer C is not in group, has relay parent in view. + { + connect_peer( + &mut overseer, + peer_a.clone(), + Some(vec![state.discovery_id(other_group_validators[0])].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_b.clone(), + Some(vec![state.discovery_id(other_group_validators[1])].into_iter().collect()), + ) + .await; + + connect_peer(&mut overseer, peer_c.clone(), None).await; + + send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Peer in cluster sends a statement, triggering a request. + { + let a_seconded = state + .sign_statement( + v_a, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::Statement( + relay_parent, + a_seconded, + ), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + } + + // Send a request to peer and mock its response to include two identical statements. + { + let b_seconded = state + .sign_statement( + v_b, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + let statements = vec![b_seconded.clone(), b_seconded.clone()]; + + handle_sent_request( + &mut overseer, + peer_a, + candidate_hash, + StatementFilter::blank(group_size), + candidate.clone(), + pvd.clone(), + statements, + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT => { } + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == COST_UNREQUESTED_RESPONSE_STATEMENT => { } + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_RESPONSE => { } + ); + + assert_matches!( + overseer.recv().await, + AllMessages:: NetworkBridgeTx( + NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::VStaging( + protocol_vstaging::ValidationProtocol::StatementDistribution( + protocol_vstaging::StatementDistributionMessage::Statement(hash, statement), + ), + ), + ) + ) => { + assert_eq!(peers, vec![peer_a]); + assert_eq!(hash, relay_parent); + assert_eq!(statement, b_seconded); + } + ); + } + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + + overseer + }); +} + +#[test] +fn peer_reported_for_providing_statements_with_invalid_signatures() { + let group_size = 3; + let config = TestConfig { + validator_count: 20, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_a = PeerId::random(); + let peer_b = PeerId::random(); + let peer_c = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + let local_para = ParaId::from(local_validator.group_index.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + local_para, + test_leaf.para_data(local_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let other_group_validators = state.group_validators(local_validator.group_index, true); + state.group_validators((local_validator.group_index.0 + 1).into(), true); + let v_a = other_group_validators[0]; + let v_b = other_group_validators[1]; + + // peer A is in group, has relay parent in view. + // peer B is in group, has no relay parent in view. + // peer C is not in group, has relay parent in view. + { + connect_peer( + &mut overseer, + peer_a.clone(), + Some(vec![state.discovery_id(other_group_validators[0])].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_b.clone(), + Some(vec![state.discovery_id(other_group_validators[1])].into_iter().collect()), + ) + .await; + + connect_peer(&mut overseer, peer_c.clone(), None).await; + + send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Peer in cluster sends a statement, triggering a request. + { + let a_seconded = state + .sign_statement( + v_a, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::Statement( + relay_parent, + a_seconded, + ), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + } + + // Send a request to peer and mock its response to include invalid statements. + { + // Sign statement with wrong signing context, leading to bad signature. + let b_seconded_invalid = state + .sign_statement( + v_b, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: Hash::repeat_byte(42), session_index: 1 }, + ) + .as_unchecked() + .clone(); + let statements = vec![b_seconded_invalid.clone()]; + + handle_sent_request( + &mut overseer, + peer_a, + candidate_hash, + StatementFilter::blank(group_size), + candidate.clone(), + pvd.clone(), + statements, + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == COST_INVALID_SIGNATURE => { } + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_RESPONSE => { } + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + overseer + }); +} + +#[test] +fn peer_reported_for_providing_statements_with_wrong_validator_id() { + let group_size = 3; + let config = TestConfig { + validator_count: 20, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_a = PeerId::random(); + let peer_b = PeerId::random(); + let peer_c = PeerId::random(); + + test_harness(config, |state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + let local_para = ParaId::from(local_validator.group_index.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + local_para, + test_leaf.para_data(local_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let other_group_validators = state.group_validators(local_validator.group_index, true); + let next_group_validators = + state.group_validators((local_validator.group_index.0 + 1).into(), true); + let v_a = other_group_validators[0]; + let v_c = next_group_validators[0]; + + // peer A is in group, has relay parent in view. + // peer B is in group, has no relay parent in view. + // peer C is not in group, has relay parent in view. + { + connect_peer( + &mut overseer, + peer_a.clone(), + Some(vec![state.discovery_id(other_group_validators[0])].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_b.clone(), + Some(vec![state.discovery_id(other_group_validators[1])].into_iter().collect()), + ) + .await; + + connect_peer(&mut overseer, peer_c.clone(), None).await; + + send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Peer in cluster sends a statement, triggering a request. + { + let a_seconded = state + .sign_statement( + v_a, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::Statement( + relay_parent, + a_seconded, + ), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + } + + // Send a request to peer and mock its response to include a wrong validator ID. + { + let c_seconded_invalid = state + .sign_statement( + v_c, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + let statements = vec![c_seconded_invalid.clone()]; + + handle_sent_request( + &mut overseer, + peer_a, + candidate_hash, + StatementFilter::blank(group_size), + candidate.clone(), + pvd.clone(), + statements, + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == COST_UNREQUESTED_RESPONSE_STATEMENT => { } + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_RESPONSE => { } + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + overseer + }); +} + +#[test] +fn local_node_sanity_checks_incoming_requests() { + let config = TestConfig { + validator_count: 20, + group_size: 3, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_a = PeerId::random(); + let peer_b = PeerId::random(); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + + test_harness(config, |mut state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + let local_para = ParaId::from(local_validator.group_index.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + local_para, + test_leaf.para_data(local_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + // peer A is in group, has relay parent in view. + // peer B is in group, has no relay parent in view. + // peer C is not in group, has relay parent in view. + { + let other_group_validators = state.group_validators(local_validator.group_index, true); + + connect_peer( + &mut overseer, + peer_a.clone(), + Some(vec![state.discovery_id(other_group_validators[0])].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_b.clone(), + Some(vec![state.discovery_id(other_group_validators[1])].into_iter().collect()), + ) + .await; + + connect_peer(&mut overseer, peer_c.clone(), None).await; + + send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + let mask = StatementFilter::blank(state.config.group_size); + + // Should drop requests for unknown candidates. + { + let (pending_response, rx) = oneshot::channel(); + state + .req_sender + .send(RawIncomingRequest { + // Request from peer that received manifest. + peer: peer_c, + payload: request_vstaging::AttestedCandidateRequest { + candidate_hash: candidate.hash(), + mask: mask.clone(), + } + .encode(), + pending_response, + }) + .await + .unwrap(); + + assert_matches!(rx.await, Err(oneshot::Canceled)); + } + + // Confirm candidate. + { + let full_signed = state + .sign_statement( + local_validator.validator_index, + CompactStatement::Seconded(candidate_hash), + &SigningContext { session_index: 1, parent_hash: relay_parent }, + ) + .convert_to_superpayload(StatementWithPVD::Seconded(candidate.clone(), pvd.clone())) + .unwrap(); + + overseer + .send(FromOrchestra::Communication { + msg: StatementDistributionMessage::Share(relay_parent, full_signed), + }) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::VStaging(protocol_vstaging::ValidationProtocol::StatementDistribution( + protocol_vstaging::StatementDistributionMessage::Statement( + r, + s, + ) + )) + )) => { + assert_eq!(peers, vec![peer_a.clone()]); + assert_eq!(r, relay_parent); + assert_eq!(s.unchecked_payload(), &CompactStatement::Seconded(candidate_hash)); + assert_eq!(s.unchecked_validator_index(), local_validator.validator_index); + } + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Should drop requests from unknown peers. + { + let (pending_response, rx) = oneshot::channel(); + state + .req_sender + .send(RawIncomingRequest { + // Request from peer that received manifest. + peer: peer_d, + payload: request_vstaging::AttestedCandidateRequest { + candidate_hash: candidate.hash(), + mask: mask.clone(), + } + .encode(), + pending_response, + }) + .await + .unwrap(); + + assert_matches!(rx.await, Err(oneshot::Canceled)); + } + + // Should drop requests with bitfields of the wrong size. + { + let mask = StatementFilter::blank(state.config.group_size + 1); + let response = state + .send_request( + peer_c, + request_vstaging::AttestedCandidateRequest { + candidate_hash: candidate.hash(), + mask, + }, + ) + .await + .await; + + assert_matches!( + response, + RawOutgoingResponse { + result, + reputation_changes, + sent_feedback + } => { + assert_matches!(result, Err(())); + assert_eq!(reputation_changes, vec![COST_INVALID_REQUEST_BITFIELD_SIZE.into_base_rep()]); + assert_matches!(sent_feedback, None); + } + ); + } + + // Local node should reject requests if we did not send a manifest to that peer. + { + let response = state + .send_request( + peer_c, + request_vstaging::AttestedCandidateRequest { + candidate_hash: candidate.hash(), + mask: mask.clone(), + }, + ) + .await + .await; + + // Should get `COST_UNEXPECTED_REQUEST` response. + assert_matches!( + response, + RawOutgoingResponse { + result, + reputation_changes, + sent_feedback + } => { + assert_matches!(result, Err(())); + assert_eq!(reputation_changes, vec![COST_UNEXPECTED_REQUEST.into_base_rep()]); + assert_matches!(sent_feedback, None); + } + ); + } + + overseer + }); +} + +#[test] +fn local_node_respects_statement_mask() { + let validator_count = 6; + let group_size = 3; + let config = TestConfig { + validator_count, + group_size, + local_validator: true, + async_backing_params: None, + }; + + let relay_parent = Hash::repeat_byte(1); + let peer_a = PeerId::random(); + let peer_b = PeerId::random(); + let peer_c = PeerId::random(); + let peer_d = PeerId::random(); + + test_harness(config, |mut state, mut overseer| async move { + let local_validator = state.local.clone().unwrap(); + let local_para = ParaId::from(local_validator.group_index.0); + + let test_leaf = state.make_dummy_leaf(relay_parent); + + let (candidate, pvd) = make_candidate( + relay_parent, + 1, + local_para, + test_leaf.para_data(local_para).head_data.clone(), + vec![4, 5, 6].into(), + Hash::repeat_byte(42).into(), + ); + let candidate_hash = candidate.hash(); + + let other_group_validators = state.group_validators(local_validator.group_index, true); + let target_group_validators = + state.group_validators((local_validator.group_index.0 + 1).into(), true); + let v_a = other_group_validators[0]; + let v_b = other_group_validators[1]; + let v_c = target_group_validators[0]; + let v_d = target_group_validators[1]; + + // peer A is in group, has relay parent in view. + // peer B is in group, has no relay parent in view. + // peer C is not in group, has relay parent in view. + // peer D is not in group, has no relay parent in view. + { + connect_peer( + &mut overseer, + peer_a.clone(), + Some(vec![state.discovery_id(v_a)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_b.clone(), + Some(vec![state.discovery_id(v_b)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_c.clone(), + Some(vec![state.discovery_id(v_c)].into_iter().collect()), + ) + .await; + + connect_peer( + &mut overseer, + peer_d.clone(), + Some(vec![state.discovery_id(v_d)].into_iter().collect()), + ) + .await; + + send_peer_view_change(&mut overseer, peer_a.clone(), view![relay_parent]).await; + send_peer_view_change(&mut overseer, peer_c.clone(), view![relay_parent]).await; + } + + activate_leaf(&mut overseer, &test_leaf, &state, true).await; + + answer_expected_hypothetical_depth_request( + &mut overseer, + vec![], + Some(relay_parent), + false, + ) + .await; + + // Send gossip topology. + send_new_topology(&mut overseer, state.make_dummy_topology()).await; + + // Confirm the candidate locally so that we don't send out requests. + { + let statement = state + .sign_full_statement( + local_validator.validator_index, + Statement::Seconded(candidate.clone()), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + pvd.clone(), + ) + .clone(); + + overseer + .send(FromOrchestra::Communication { + msg: StatementDistributionMessage::Share(relay_parent, statement), + }) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(peers, _)) if peers == vec![peer_a] + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // Send enough statements to make candidate backable, make sure announcements are sent. + + // Send statement from peer A. + { + let statement = state + .sign_statement( + v_a, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + + send_peer_message( + &mut overseer, + peer_a.clone(), + protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, statement), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_a && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + } + + // Send statement from peer B. + let statement_b = state + .sign_statement( + v_b, + CompactStatement::Seconded(candidate_hash), + &SigningContext { parent_hash: relay_parent, session_index: 1 }, + ) + .as_unchecked() + .clone(); + { + send_peer_message( + &mut overseer, + peer_b.clone(), + protocol_vstaging::StatementDistributionMessage::Statement( + relay_parent, + statement_b.clone(), + ), + ) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::ReportPeer(p, r)) + if p == peer_b && r == BENEFIT_VALID_STATEMENT_FIRST => { } + ); + + assert_matches!( + overseer.recv().await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(peers, _)) if peers == vec![peer_a] + ); + } + + // Send Backed notification. + { + overseer + .send(FromOrchestra::Communication { + msg: StatementDistributionMessage::Backed(candidate_hash), + }) + .await; + + assert_matches!( + overseer.recv().await, + AllMessages:: NetworkBridgeTx( + NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::VStaging( + protocol_vstaging::ValidationProtocol::StatementDistribution( + protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest(manifest), + ), + ), + ) + ) => { + assert_eq!(peers, vec![peer_c]); + assert_eq!(manifest, BackedCandidateManifest { + relay_parent, + candidate_hash, + group_index: local_validator.group_index, + para_id: local_para, + parent_head_data_hash: pvd.parent_head.hash(), + statement_knowledge: StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }, + }); + } + ); + + answer_expected_hypothetical_depth_request(&mut overseer, vec![], None, false).await; + } + + // `1` indicates statements NOT to request. + let mask = StatementFilter { + seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1], + validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0], + }; + + // Incoming request to local node. Local node should send statements, respecting mask. + { + let response = state + .send_request( + peer_c, + request_vstaging::AttestedCandidateRequest { + candidate_hash: candidate.hash(), + mask, + }, + ) + .await + .await; + + let expected_statements = vec![statement_b]; + assert_matches!(response, full_response => { + // Response is the same for vstaging. + let request_vstaging::AttestedCandidateResponse { candidate_receipt, persisted_validation_data, statements } = + request_vstaging::AttestedCandidateResponse::decode( + &mut full_response.result.expect("We should have a proper answer").as_ref(), + ).expect("Decoding should work"); + assert_eq!(candidate_receipt, candidate); + assert_eq!(persisted_validation_data, pvd); + assert_eq!(statements, expected_statements); + }); + } + + overseer + }); +} diff --git a/node/subsystem-types/src/messages.rs b/node/subsystem-types/src/messages.rs index bef3b1975920..3f433c4870cc 100644 --- a/node/subsystem-types/src/messages.rs +++ b/node/subsystem-types/src/messages.rs @@ -88,9 +88,10 @@ pub enum CandidateBackingMessage { /// Note that the Candidate Backing subsystem should second the given candidate in the context of the /// given relay-parent (ref. by hash). This candidate must be validated. Second(Hash, CandidateReceipt, PersistedValidationData, PoV), - /// Note a validator's statement about a particular candidate. Disagreements about validity must be escalated - /// to a broader check by the Disputes Subsystem, though that escalation is deferred until the approval voting - /// stage to guarantee availability. Agreements are simply tallied until a quorum is reached. + /// Note a validator's statement about a particular candidate in the context of the given + /// relay-parent. Disagreements about validity must be escalated to a broader check by the + /// Disputes Subsystem, though that escalation is deferred until the approval voting stage to + /// guarantee availability. Agreements are simply tallied until a quorum is reached. Statement(Hash, SignedFullStatementWithPVD), }