use polkadot_node_network_protocol::{grid_topology::SessionGridTopology, v2::StatementFilter};
use polkadot_primitives::{CandidateHash, CompactStatement, GroupIndex, Hash, ValidatorIndex};
use std::collections::{
hash_map::{Entry, HashMap},
HashSet,
};
use bitvec::{order::Lsb0, slice::BitSlice};
use super::{groups::Groups, LOG_TARGET};
#[derive(Debug, PartialEq)]
struct GroupSubView {
sending: HashSet<ValidatorIndex>,
receiving: HashSet<ValidatorIndex>,
}
#[derive(Debug)]
pub struct SessionTopologyView {
group_views: HashMap<GroupIndex, GroupSubView>,
}
impl SessionTopologyView {
pub fn iter_sending_for_group(
&self,
group: GroupIndex,
kind: ManifestKind,
) -> impl Iterator<Item = ValidatorIndex> + '_ {
self.group_views.get(&group).into_iter().flat_map(move |sub| match kind {
ManifestKind::Full => sub.receiving.iter().cloned(),
ManifestKind::Acknowledgement => sub.sending.iter().cloned(),
})
}
}
pub fn build_session_topology<'a>(
groups: impl IntoIterator<Item = &'a Vec<ValidatorIndex>>,
topology: &SessionGridTopology,
our_index: Option<ValidatorIndex>,
) -> SessionTopologyView {
let mut view = SessionTopologyView { group_views: HashMap::new() };
let our_index = match our_index {
None => return view,
Some(i) => i,
};
let our_neighbors = match topology.compute_grid_neighbors_for(our_index) {
None => {
gum::warn!(target: LOG_TARGET, ?our_index, "our index unrecognized in topology?");
return view
},
Some(n) => n,
};
for (i, group) in groups.into_iter().enumerate() {
let mut sub_view = GroupSubView { sending: HashSet::new(), receiving: HashSet::new() };
if group.contains(&our_index) {
sub_view.sending.extend(our_neighbors.validator_indices_x.iter().cloned());
sub_view.sending.extend(our_neighbors.validator_indices_y.iter().cloned());
for v in group {
sub_view.sending.remove(v);
}
} else {
for &group_val in group {
if our_neighbors.validator_indices_x.contains(&group_val) {
sub_view.receiving.insert(group_val);
sub_view.sending.extend(
our_neighbors
.validator_indices_y
.iter()
.filter(|v| !group.contains(v))
.cloned(),
);
continue
}
if our_neighbors.validator_indices_y.contains(&group_val) {
sub_view.receiving.insert(group_val);
sub_view.sending.extend(
our_neighbors
.validator_indices_x
.iter()
.filter(|v| !group.contains(v))
.cloned(),
);
continue
}
let their_neighbors = match topology.compute_grid_neighbors_for(group_val) {
None => {
gum::warn!(
target: LOG_TARGET,
index = ?group_val,
"validator index unrecognized in topology?"
);
continue
},
Some(n) => n,
};
for potential_link in &their_neighbors.validator_indices_x {
if our_neighbors.validator_indices_y.contains(potential_link) {
sub_view.receiving.insert(*potential_link);
break }
}
for potential_link in &their_neighbors.validator_indices_y {
if our_neighbors.validator_indices_x.contains(potential_link) {
sub_view.receiving.insert(*potential_link);
break }
}
}
}
view.group_views.insert(GroupIndex(i as _), sub_view);
}
view
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ManifestKind {
Full,
Acknowledgement,
}
#[derive(Default)]
pub struct GridTracker {
received: HashMap<ValidatorIndex, ReceivedManifests>,
confirmed_backed: HashMap<CandidateHash, KnownBackedCandidate>,
unconfirmed: HashMap<CandidateHash, Vec<(ValidatorIndex, GroupIndex)>>,
pending_manifests: HashMap<ValidatorIndex, HashMap<CandidateHash, ManifestKind>>,
pending_statements: HashMap<ValidatorIndex, HashSet<(ValidatorIndex, CompactStatement)>>,
}
impl GridTracker {
pub fn import_manifest(
&mut self,
session_topology: &SessionTopologyView,
groups: &Groups,
candidate_hash: CandidateHash,
seconding_limit: usize,
manifest: ManifestSummary,
kind: ManifestKind,
sender: ValidatorIndex,
) -> Result<bool, ManifestImportError> {
let claimed_group_index = manifest.claimed_group_index;
let group_topology = match session_topology.group_views.get(&manifest.claimed_group_index) {
None => return Err(ManifestImportError::Disallowed),
Some(g) => g,
};
let receiving_from = group_topology.receiving.contains(&sender);
let sending_to = group_topology.sending.contains(&sender);
let manifest_allowed = match kind {
ManifestKind::Full => receiving_from,
ManifestKind::Acknowledgement =>
sending_to &&
self.confirmed_backed
.get(&candidate_hash)
.map_or(false, |c| c.has_sent_manifest_to(sender)),
};
if !manifest_allowed {
return Err(ManifestImportError::Disallowed)
}
let (group_size, backing_threshold) =
match groups.get_size_and_backing_threshold(manifest.claimed_group_index) {
Some(x) => x,
None => return Err(ManifestImportError::Malformed),
};
let remote_knowledge = manifest.statement_knowledge.clone();
if !remote_knowledge.has_len(group_size) {
return Err(ManifestImportError::Malformed)
}
if !remote_knowledge.has_seconded() {
return Err(ManifestImportError::Malformed)
}
let votes = remote_knowledge.backing_validators();
if votes < backing_threshold {
return Err(ManifestImportError::Insufficient)
}
self.received.entry(sender).or_default().import_received(
group_size,
seconding_limit,
candidate_hash,
manifest,
)?;
let mut ack = false;
if let Some(confirmed) = self.confirmed_backed.get_mut(&candidate_hash) {
if receiving_from && !confirmed.has_sent_manifest_to(sender) {
self.pending_manifests
.entry(sender)
.or_default()
.insert(candidate_hash, ManifestKind::Acknowledgement);
ack = true;
}
confirmed.manifest_received_from(sender, remote_knowledge);
if let Some(pending_statements) = confirmed.pending_statements(sender) {
self.pending_statements.entry(sender).or_default().extend(
decompose_statement_filter(
groups,
claimed_group_index,
candidate_hash,
&pending_statements,
),
);
}
} else {
self.unconfirmed
.entry(candidate_hash)
.or_default()
.push((sender, claimed_group_index))
}
Ok(ack)
}
pub fn add_backed_candidate(
&mut self,
session_topology: &SessionTopologyView,
candidate_hash: CandidateHash,
group_index: GroupIndex,
local_knowledge: StatementFilter,
) -> Vec<(ValidatorIndex, ManifestKind)> {
let c = match self.confirmed_backed.entry(candidate_hash) {
Entry::Occupied(_) => return Vec::new(),
Entry::Vacant(v) => v.insert(KnownBackedCandidate {
group_index,
mutual_knowledge: HashMap::new(),
local_knowledge,
}),
};
for (v, claimed_group_index) in
self.unconfirmed.remove(&candidate_hash).into_iter().flatten()
{
if claimed_group_index != group_index {
continue
}
let statement_filter = self
.received
.get(&v)
.and_then(|r| r.candidate_statement_filter(&candidate_hash))
.expect("unconfirmed is only populated by validators who have sent manifest; qed");
c.manifest_received_from(v, statement_filter);
}
let group_topology = match session_topology.group_views.get(&group_index) {
None => return Vec::new(),
Some(g) => g,
};
let sending_group_manifests =
group_topology.sending.iter().map(|v| (*v, ManifestKind::Full));
let receiving_group_manifests = group_topology.receiving.iter().filter_map(|v| {
if c.has_received_manifest_from(*v) {
Some((*v, ManifestKind::Acknowledgement))
} else {
None
}
});
for (v, manifest_mode) in sending_group_manifests.chain(receiving_group_manifests) {
gum::trace!(
target: LOG_TARGET,
validator_index = ?v,
?manifest_mode,
"Preparing to send manifest/acknowledgement"
);
self.pending_manifests
.entry(v)
.or_default()
.insert(candidate_hash, manifest_mode);
}
self.pending_manifests
.iter()
.filter_map(|(v, x)| x.get(&candidate_hash).map(|k| (*v, *k)))
.collect()
}
pub fn manifest_sent_to(
&mut self,
groups: &Groups,
validator_index: ValidatorIndex,
candidate_hash: CandidateHash,
local_knowledge: StatementFilter,
) {
if let Some(c) = self.confirmed_backed.get_mut(&candidate_hash) {
c.manifest_sent_to(validator_index, local_knowledge);
if let Some(pending_statements) = c.pending_statements(validator_index) {
self.pending_statements.entry(validator_index).or_default().extend(
decompose_statement_filter(
groups,
c.group_index,
candidate_hash,
&pending_statements,
),
);
}
}
if let Some(x) = self.pending_manifests.get_mut(&validator_index) {
x.remove(&candidate_hash);
}
}
pub fn pending_manifests_for(
&self,
validator_index: ValidatorIndex,
) -> Vec<(CandidateHash, ManifestKind)> {
self.pending_manifests
.get(&validator_index)
.into_iter()
.flat_map(|pending| pending.iter().map(|(c, m)| (*c, *m)))
.collect()
}
pub fn pending_statements_for(
&self,
validator_index: ValidatorIndex,
candidate_hash: CandidateHash,
) -> Option<StatementFilter> {
self.confirmed_backed
.get(&candidate_hash)
.and_then(|x| x.pending_statements(validator_index))
}
pub fn all_pending_statements_for(
&self,
validator_index: ValidatorIndex,
) -> Vec<(ValidatorIndex, CompactStatement)> {
let mut v = self
.pending_statements
.get(&validator_index)
.map(|x| x.iter().cloned().collect())
.unwrap_or(Vec::new());
v.sort_by_key(|(_, s)| match s {
CompactStatement::Seconded(_) => 0u32,
CompactStatement::Valid(_) => 1u32,
});
v
}
pub fn can_request(&self, validator: ValidatorIndex, candidate_hash: CandidateHash) -> bool {
self.confirmed_backed.get(&candidate_hash).map_or(false, |c| {
c.has_sent_manifest_to(validator) && !c.has_received_manifest_from(validator)
})
}
pub fn direct_statement_providers(
&self,
groups: &Groups,
originator: ValidatorIndex,
statement: &CompactStatement,
) -> Vec<(ValidatorIndex, bool)> {
let (g, c_h, kind, in_group) =
match extract_statement_and_group_info(groups, originator, statement) {
None => return Vec::new(),
Some(x) => x,
};
self.confirmed_backed
.get(&c_h)
.map(|k| k.direct_statement_senders(g, in_group, kind))
.unwrap_or_default()
}
pub fn direct_statement_targets(
&self,
groups: &Groups,
originator: ValidatorIndex,
statement: &CompactStatement,
) -> Vec<ValidatorIndex> {
let (g, c_h, kind, in_group) =
match extract_statement_and_group_info(groups, originator, statement) {
None => return Vec::new(),
Some(x) => x,
};
self.confirmed_backed
.get(&c_h)
.map(|k| k.direct_statement_recipients(g, in_group, kind))
.unwrap_or_default()
}
pub fn learned_fresh_statement(
&mut self,
groups: &Groups,
session_topology: &SessionTopologyView,
originator: ValidatorIndex,
statement: &CompactStatement,
) {
let (g, c_h, kind, in_group) =
match extract_statement_and_group_info(groups, originator, statement) {
None => return,
Some(x) => x,
};
let known = match self.confirmed_backed.get_mut(&c_h) {
None => return,
Some(x) => x,
};
if !known.note_fresh_statement(in_group, kind) {
return
}
let all_group_validators = session_topology
.group_views
.get(&g)
.into_iter()
.flat_map(|g| g.sending.iter().chain(g.receiving.iter()));
for v in all_group_validators {
if known.is_pending_statement(*v, in_group, kind) {
self.pending_statements
.entry(*v)
.or_default()
.insert((originator, statement.clone()));
}
}
}
pub fn sent_or_received_direct_statement(
&mut self,
groups: &Groups,
originator: ValidatorIndex,
counterparty: ValidatorIndex,
statement: &CompactStatement,
received: bool,
) {
if let Some((_, c_h, kind, in_group)) =
extract_statement_and_group_info(groups, originator, statement)
{
if let Some(known) = self.confirmed_backed.get_mut(&c_h) {
known.sent_or_received_direct_statement(counterparty, in_group, kind, received);
if let Some(pending) = self.pending_statements.get_mut(&counterparty) {
pending.remove(&(originator, statement.clone()));
}
}
}
}
pub fn advertised_statements(
&self,
validator: ValidatorIndex,
candidate_hash: &CandidateHash,
) -> Option<StatementFilter> {
self.received.get(&validator)?.candidate_statement_filter(candidate_hash)
}
#[cfg(test)]
fn is_manifest_pending_for(
&self,
validator: ValidatorIndex,
candidate_hash: &CandidateHash,
) -> Option<ManifestKind> {
self.pending_manifests
.get(&validator)
.and_then(|m| m.get(candidate_hash))
.map(|x| *x)
}
}
fn extract_statement_and_group_info(
groups: &Groups,
originator: ValidatorIndex,
statement: &CompactStatement,
) -> Option<(GroupIndex, CandidateHash, StatementKind, usize)> {
let (statement_kind, candidate_hash) = match statement {
CompactStatement::Seconded(h) => (StatementKind::Seconded, h),
CompactStatement::Valid(h) => (StatementKind::Valid, h),
};
let group = match groups.by_validator_index(originator) {
None => return None,
Some(g) => g,
};
let index_in_group = groups.get(group)?.iter().position(|v| v == &originator)?;
Some((group, *candidate_hash, statement_kind, index_in_group))
}
fn decompose_statement_filter<'a>(
groups: &'a Groups,
group_index: GroupIndex,
candidate_hash: CandidateHash,
statement_filter: &'a StatementFilter,
) -> impl Iterator<Item = (ValidatorIndex, CompactStatement)> + 'a {
groups.get(group_index).into_iter().flat_map(move |g| {
let s = statement_filter
.seconded_in_group
.iter_ones()
.map(|i| g[i])
.map(move |i| (i, CompactStatement::Seconded(candidate_hash)));
let v = statement_filter
.validated_in_group
.iter_ones()
.map(|i| g[i])
.map(move |i| (i, CompactStatement::Valid(candidate_hash)));
s.chain(v)
})
}
#[derive(Debug, Clone)]
pub struct ManifestSummary {
pub claimed_parent_hash: Hash,
pub claimed_group_index: GroupIndex,
pub statement_knowledge: StatementFilter,
}
#[derive(Debug, Clone)]
pub enum ManifestImportError {
Conflicting,
Overflow,
Insufficient,
Malformed,
Disallowed,
}
#[derive(Default)]
struct ReceivedManifests {
received: HashMap<CandidateHash, ManifestSummary>,
seconded_counts: HashMap<GroupIndex, Vec<usize>>,
}
impl ReceivedManifests {
fn candidate_statement_filter(
&self,
candidate_hash: &CandidateHash,
) -> Option<StatementFilter> {
self.received.get(candidate_hash).map(|m| m.statement_knowledge.clone())
}
fn import_received(
&mut self,
group_size: usize,
seconding_limit: usize,
candidate_hash: CandidateHash,
manifest_summary: ManifestSummary,
) -> Result<(), ManifestImportError> {
match self.received.entry(candidate_hash) {
Entry::Occupied(mut e) => {
{
let prev = e.get();
if prev.claimed_group_index != manifest_summary.claimed_group_index {
return Err(ManifestImportError::Conflicting)
}
if prev.claimed_parent_hash != manifest_summary.claimed_parent_hash {
return Err(ManifestImportError::Conflicting)
}
if !manifest_summary
.statement_knowledge
.seconded_in_group
.contains(&prev.statement_knowledge.seconded_in_group)
{
return Err(ManifestImportError::Conflicting)
}
if !manifest_summary
.statement_knowledge
.validated_in_group
.contains(&prev.statement_knowledge.validated_in_group)
{
return Err(ManifestImportError::Conflicting)
}
let mut fresh_seconded =
manifest_summary.statement_knowledge.seconded_in_group.clone();
fresh_seconded |= &prev.statement_knowledge.seconded_in_group;
let within_limits = updating_ensure_within_seconding_limit(
&mut self.seconded_counts,
manifest_summary.claimed_group_index,
group_size,
seconding_limit,
&fresh_seconded,
);
if !within_limits {
return Err(ManifestImportError::Overflow)
}
}
*e.get_mut() = manifest_summary;
Ok(())
},
Entry::Vacant(e) => {
let within_limits = updating_ensure_within_seconding_limit(
&mut self.seconded_counts,
manifest_summary.claimed_group_index,
group_size,
seconding_limit,
&manifest_summary.statement_knowledge.seconded_in_group,
);
if within_limits {
e.insert(manifest_summary);
Ok(())
} else {
Err(ManifestImportError::Overflow)
}
},
}
}
}
fn updating_ensure_within_seconding_limit(
seconded_counts: &mut HashMap<GroupIndex, Vec<usize>>,
group_index: GroupIndex,
group_size: usize,
seconding_limit: usize,
new_seconded: &BitSlice<u8, Lsb0>,
) -> bool {
if seconding_limit == 0 {
return false
}
let counts = seconded_counts.entry(group_index).or_insert_with(|| vec![0; group_size]);
for i in new_seconded.iter_ones() {
if counts[i] == seconding_limit {
return false
}
}
for i in new_seconded.iter_ones() {
counts[i] += 1;
}
true
}
#[derive(Debug, Clone, Copy)]
enum StatementKind {
Seconded,
Valid,
}
trait FilterQuery {
fn contains(&self, index: usize, statement_kind: StatementKind) -> bool;
fn set(&mut self, index: usize, statement_kind: StatementKind);
}
impl FilterQuery for StatementFilter {
fn contains(&self, index: usize, statement_kind: StatementKind) -> bool {
match statement_kind {
StatementKind::Seconded => self.seconded_in_group.get(index).map_or(false, |x| *x),
StatementKind::Valid => self.validated_in_group.get(index).map_or(false, |x| *x),
}
}
fn set(&mut self, index: usize, statement_kind: StatementKind) {
let b = match statement_kind {
StatementKind::Seconded => self.seconded_in_group.get_mut(index),
StatementKind::Valid => self.validated_in_group.get_mut(index),
};
if let Some(mut b) = b {
*b = true;
}
}
}
#[derive(Debug, Clone)]
struct MutualKnowledge {
remote_knowledge: Option<StatementFilter>,
local_knowledge: Option<StatementFilter>,
received_knowledge: Option<StatementFilter>,
}
#[derive(Debug, Clone)]
struct KnownBackedCandidate {
group_index: GroupIndex,
local_knowledge: StatementFilter,
mutual_knowledge: HashMap<ValidatorIndex, MutualKnowledge>,
}
impl KnownBackedCandidate {
fn has_received_manifest_from(&self, validator: ValidatorIndex) -> bool {
self.mutual_knowledge
.get(&validator)
.map_or(false, |k| k.remote_knowledge.is_some())
}
fn has_sent_manifest_to(&self, validator: ValidatorIndex) -> bool {
self.mutual_knowledge
.get(&validator)
.map_or(false, |k| k.local_knowledge.is_some())
}
fn manifest_sent_to(&mut self, validator: ValidatorIndex, local_knowledge: StatementFilter) {
let k = self.mutual_knowledge.entry(validator).or_insert_with(|| MutualKnowledge {
remote_knowledge: None,
local_knowledge: None,
received_knowledge: None,
});
k.received_knowledge =
Some(StatementFilter::blank(local_knowledge.seconded_in_group.len()));
k.local_knowledge = Some(local_knowledge);
}
fn manifest_received_from(
&mut self,
validator: ValidatorIndex,
remote_knowledge: StatementFilter,
) {
let k = self.mutual_knowledge.entry(validator).or_insert_with(|| MutualKnowledge {
remote_knowledge: None,
local_knowledge: None,
received_knowledge: None,
});
k.remote_knowledge = Some(remote_knowledge);
}
fn direct_statement_senders(
&self,
group_index: GroupIndex,
originator_index_in_group: usize,
statement_kind: StatementKind,
) -> Vec<(ValidatorIndex, bool)> {
if group_index != self.group_index {
return Vec::new()
}
self.mutual_knowledge
.iter()
.filter(|(_, k)| k.remote_knowledge.is_some())
.filter(|(_, k)| {
k.received_knowledge
.as_ref()
.map_or(false, |r| !r.contains(originator_index_in_group, statement_kind))
})
.map(|(v, k)| {
(
*v,
k.local_knowledge
.as_ref()
.map_or(false, |r| r.contains(originator_index_in_group, statement_kind)),
)
})
.collect()
}
fn direct_statement_recipients(
&self,
group_index: GroupIndex,
originator_index_in_group: usize,
statement_kind: StatementKind,
) -> Vec<ValidatorIndex> {
if group_index != self.group_index {
return Vec::new()
}
self.mutual_knowledge
.iter()
.filter(|(_, k)| k.local_knowledge.is_some())
.filter(|(_, k)| {
k.remote_knowledge
.as_ref()
.map_or(false, |r| !r.contains(originator_index_in_group, statement_kind))
})
.map(|(v, _)| *v)
.collect()
}
fn note_fresh_statement(
&mut self,
statement_index_in_group: usize,
statement_kind: StatementKind,
) -> bool {
let really_fresh = !self.local_knowledge.contains(statement_index_in_group, statement_kind);
self.local_knowledge.set(statement_index_in_group, statement_kind);
really_fresh
}
fn sent_or_received_direct_statement(
&mut self,
validator: ValidatorIndex,
statement_index_in_group: usize,
statement_kind: StatementKind,
received: bool,
) {
if let Some(k) = self.mutual_knowledge.get_mut(&validator) {
if let (Some(r), Some(l)) = (k.remote_knowledge.as_mut(), k.local_knowledge.as_mut()) {
r.set(statement_index_in_group, statement_kind);
l.set(statement_index_in_group, statement_kind);
}
if received {
k.received_knowledge
.as_mut()
.map(|knowledge| knowledge.set(statement_index_in_group, statement_kind));
}
}
}
fn is_pending_statement(
&self,
validator: ValidatorIndex,
statement_index_in_group: usize,
statement_kind: StatementKind,
) -> bool {
self.mutual_knowledge
.get(&validator)
.filter(|k| k.local_knowledge.is_some())
.and_then(|k| k.remote_knowledge.as_ref())
.map(|k| !k.contains(statement_index_in_group, statement_kind))
.unwrap_or(false)
}
fn pending_statements(&self, validator: ValidatorIndex) -> Option<StatementFilter> {
let full_local = &self.local_knowledge;
self.mutual_knowledge
.get(&validator)
.filter(|k| k.local_knowledge.is_some())
.and_then(|k| k.remote_knowledge.as_ref())
.map(|remote| StatementFilter {
seconded_in_group: full_local.seconded_in_group.clone() &
!remote.seconded_in_group.clone(),
validated_in_group: full_local.validated_in_group.clone() &
!remote.validated_in_group.clone(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use polkadot_node_network_protocol::grid_topology::TopologyPeerInfo;
use sp_authority_discovery::AuthorityPair as AuthorityDiscoveryPair;
use sp_core::crypto::Pair as PairT;
fn dummy_groups(group_size: usize) -> Groups {
let groups = vec![(0..(group_size as u32)).map(ValidatorIndex).collect()].into();
Groups::new(groups, 2)
}
#[test]
fn topology_empty_for_no_index() {
let base_topology = SessionGridTopology::new(
vec![0, 1, 2],
vec![
TopologyPeerInfo {
peer_ids: Vec::new(),
validator_index: ValidatorIndex(0),
discovery_id: AuthorityDiscoveryPair::generate().0.public(),
},
TopologyPeerInfo {
peer_ids: Vec::new(),
validator_index: ValidatorIndex(1),
discovery_id: AuthorityDiscoveryPair::generate().0.public(),
},
TopologyPeerInfo {
peer_ids: Vec::new(),
validator_index: ValidatorIndex(2),
discovery_id: AuthorityDiscoveryPair::generate().0.public(),
},
],
);
let t = build_session_topology(
&[vec![ValidatorIndex(0)], vec![ValidatorIndex(1)], vec![ValidatorIndex(2)]],
&base_topology,
None,
);
assert!(t.group_views.is_empty());
}
#[test]
fn topology_setup() {
let base_topology = SessionGridTopology::new(
(0..9).collect(),
(0..9)
.map(|i| TopologyPeerInfo {
peer_ids: Vec::new(),
validator_index: ValidatorIndex(i),
discovery_id: AuthorityDiscoveryPair::generate().0.public(),
})
.collect(),
);
let t = build_session_topology(
&[
vec![ValidatorIndex(0), ValidatorIndex(3), ValidatorIndex(6)],
vec![ValidatorIndex(4), ValidatorIndex(2), ValidatorIndex(7)],
vec![ValidatorIndex(8), ValidatorIndex(5), ValidatorIndex(1)],
],
&base_topology,
Some(ValidatorIndex(0)),
);
assert_eq!(t.group_views.len(), 3);
assert_eq!(
t.group_views.get(&GroupIndex(0)).unwrap().sending,
vec![1, 2].into_iter().map(ValidatorIndex).collect::<HashSet<_>>(),
);
assert_eq!(t.group_views.get(&GroupIndex(0)).unwrap().receiving, HashSet::new(),);
assert_eq!(
t.group_views.get(&GroupIndex(1)).unwrap().sending,
vec![3, 6].into_iter().map(ValidatorIndex).collect::<HashSet<_>>(),
);
assert_eq!(
t.group_views.get(&GroupIndex(1)).unwrap().receiving,
vec![1, 2, 3, 6].into_iter().map(ValidatorIndex).collect::<HashSet<_>>(),
);
assert_eq!(
t.group_views.get(&GroupIndex(2)).unwrap().sending,
vec![3, 6].into_iter().map(ValidatorIndex).collect::<HashSet<_>>(),
);
assert_eq!(
t.group_views.get(&GroupIndex(2)).unwrap().receiving,
vec![1, 2, 3, 6].into_iter().map(ValidatorIndex).collect::<HashSet<_>>(),
);
}
#[test]
fn knowledge_rejects_conflicting_manifest() {
let mut knowledge = ReceivedManifests::default();
let expected_manifest_summary = ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(2),
claimed_group_index: GroupIndex(0),
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1],
},
};
knowledge
.import_received(
3,
2,
CandidateHash(Hash::repeat_byte(1)),
expected_manifest_summary.clone(),
)
.unwrap();
let mut s = expected_manifest_summary.clone();
s.claimed_group_index = GroupIndex(1);
assert_matches!(
knowledge.import_received(3, 2, CandidateHash(Hash::repeat_byte(1)), s,),
Err(ManifestImportError::Conflicting)
);
let mut s = expected_manifest_summary.clone();
s.claimed_parent_hash = Hash::repeat_byte(3);
assert_matches!(
knowledge.import_received(3, 2, CandidateHash(Hash::repeat_byte(1)), s,),
Err(ManifestImportError::Conflicting)
);
let mut s = expected_manifest_summary.clone();
s.statement_knowledge.seconded_in_group = bitvec::bitvec![u8, Lsb0; 0, 1, 0];
assert_matches!(
knowledge.import_received(3, 2, CandidateHash(Hash::repeat_byte(1)), s,),
Err(ManifestImportError::Conflicting)
);
let mut s = expected_manifest_summary.clone();
s.statement_knowledge.validated_in_group = bitvec::bitvec![u8, Lsb0; 0, 1, 0];
assert_matches!(
knowledge.import_received(3, 2, CandidateHash(Hash::repeat_byte(1)), s,),
Err(ManifestImportError::Conflicting)
);
}
#[test]
fn reject_overflowing_manifests() {
let mut knowledge = ReceivedManifests::default();
knowledge
.import_received(
3,
2,
CandidateHash(Hash::repeat_byte(1)),
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0xA),
claimed_group_index: GroupIndex(0),
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1],
},
},
)
.unwrap();
knowledge
.import_received(
3,
2,
CandidateHash(Hash::repeat_byte(2)),
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0xB),
claimed_group_index: GroupIndex(0),
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1],
},
},
)
.unwrap();
assert_matches!(
knowledge.import_received(
3,
2,
CandidateHash(Hash::repeat_byte(3)),
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0xC),
claimed_group_index: GroupIndex(0),
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 1],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1],
}
},
),
Err(ManifestImportError::Overflow)
);
knowledge
.import_received(
3,
2,
CandidateHash(Hash::repeat_byte(3)),
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0xC),
claimed_group_index: GroupIndex(0),
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 1],
},
},
)
.unwrap();
}
#[test]
fn reject_disallowed_manifest() {
let mut tracker = GridTracker::default();
let session_topology = SessionTopologyView {
group_views: vec![(
GroupIndex(0),
GroupSubView {
sending: HashSet::new(),
receiving: vec![ValidatorIndex(0)].into_iter().collect(),
},
)]
.into_iter()
.collect(),
};
let groups = dummy_groups(3);
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
assert_eq!(groups.get_size_and_backing_threshold(GroupIndex(0)), Some((3, 2)),);
assert_matches!(
tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: GroupIndex(0),
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1],
}
},
ManifestKind::Full,
ValidatorIndex(1),
),
Err(ManifestImportError::Disallowed)
);
assert_matches!(
tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: GroupIndex(1),
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1],
}
},
ManifestKind::Full,
ValidatorIndex(0),
),
Err(ManifestImportError::Disallowed)
);
}
#[test]
fn reject_malformed_wrong_group_size() {
let mut tracker = GridTracker::default();
let session_topology = SessionTopologyView {
group_views: vec![(
GroupIndex(0),
GroupSubView {
sending: HashSet::new(),
receiving: vec![ValidatorIndex(0)].into_iter().collect(),
},
)]
.into_iter()
.collect(),
};
let groups = dummy_groups(3);
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
assert_eq!(groups.get_size_and_backing_threshold(GroupIndex(0)), Some((3, 2)),);
assert_matches!(
tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: GroupIndex(0),
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 0, 1],
validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1],
}
},
ManifestKind::Full,
ValidatorIndex(0),
),
Err(ManifestImportError::Malformed)
);
assert_matches!(
tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: GroupIndex(0),
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1, 0],
}
},
ManifestKind::Full,
ValidatorIndex(0),
),
Err(ManifestImportError::Malformed)
);
}
#[test]
fn reject_malformed_no_seconders() {
let mut tracker = GridTracker::default();
let session_topology = SessionTopologyView {
group_views: vec![(
GroupIndex(0),
GroupSubView {
sending: HashSet::new(),
receiving: vec![ValidatorIndex(0)].into_iter().collect(),
},
)]
.into_iter()
.collect(),
};
let groups = dummy_groups(3);
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
assert_eq!(groups.get_size_and_backing_threshold(GroupIndex(0)), Some((3, 2)),);
assert_matches!(
tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: GroupIndex(0),
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 1, 1],
}
},
ManifestKind::Full,
ValidatorIndex(0),
),
Err(ManifestImportError::Malformed)
);
}
#[test]
fn reject_insufficient_below_threshold() {
let mut tracker = GridTracker::default();
let session_topology = SessionTopologyView {
group_views: vec![(
GroupIndex(0),
GroupSubView {
sending: HashSet::new(),
receiving: HashSet::from([ValidatorIndex(0)]),
},
)]
.into_iter()
.collect(),
};
let groups = dummy_groups(3);
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
assert_eq!(groups.get_size_and_backing_threshold(GroupIndex(0)), Some((3, 2)),);
assert_matches!(
tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: GroupIndex(0),
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 1],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0],
}
},
ManifestKind::Full,
ValidatorIndex(0),
),
Err(ManifestImportError::Insufficient)
);
assert_matches!(
tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: GroupIndex(0),
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 1],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 1],
}
},
ManifestKind::Full,
ValidatorIndex(0),
),
Err(ManifestImportError::Insufficient)
);
assert_matches!(
tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: GroupIndex(0),
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 1],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 0],
}
},
ManifestKind::Full,
ValidatorIndex(0),
),
Ok(false)
);
}
#[test]
fn senders_can_provide_manifests_in_acknowledgement() {
let validator_index = ValidatorIndex(0);
let mut tracker = GridTracker::default();
let session_topology = SessionTopologyView {
group_views: vec![(
GroupIndex(0),
GroupSubView {
sending: HashSet::from([validator_index]),
receiving: HashSet::from([ValidatorIndex(1)]),
},
)]
.into_iter()
.collect(),
};
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
let group_index = GroupIndex(0);
let group_size = 3;
let local_knowledge = StatementFilter::blank(group_size);
let groups = dummy_groups(group_size);
let receivers = tracker.add_backed_candidate(
&session_topology,
candidate_hash,
group_index,
local_knowledge.clone(),
);
assert_eq!(receivers, vec![(validator_index, ManifestKind::Full)]);
tracker.manifest_sent_to(&groups, validator_index, candidate_hash, local_knowledge);
let ack = tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: group_index,
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1],
},
},
ManifestKind::Acknowledgement,
validator_index,
);
assert_matches!(ack, Ok(false));
}
#[test]
fn pending_communication_receiving_manifest_on_confirmed_candidate() {
let validator_index = ValidatorIndex(0);
let mut tracker = GridTracker::default();
let session_topology = SessionTopologyView {
group_views: vec![(
GroupIndex(0),
GroupSubView {
sending: HashSet::from([validator_index]),
receiving: HashSet::from([ValidatorIndex(1)]),
},
)]
.into_iter()
.collect(),
};
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
let group_index = GroupIndex(0);
let group_size = 3;
let local_knowledge = StatementFilter::blank(group_size);
let groups = dummy_groups(group_size);
let pending_manifest = tracker.is_manifest_pending_for(validator_index, &candidate_hash);
assert_eq!(pending_manifest, None);
tracker.add_backed_candidate(
&session_topology,
candidate_hash,
group_index,
local_knowledge.clone(),
);
let pending_manifest = tracker.is_manifest_pending_for(validator_index, &candidate_hash);
assert_eq!(pending_manifest, Some(ManifestKind::Full));
tracker.manifest_sent_to(&groups, validator_index, candidate_hash, local_knowledge);
let ack = tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: group_index,
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1],
},
},
ManifestKind::Acknowledgement,
validator_index,
);
assert_matches!(ack, Ok(false));
let pending_manifest = tracker.is_manifest_pending_for(validator_index, &candidate_hash);
assert_eq!(pending_manifest, None);
}
#[test]
fn pending_communication_is_cleared() {
let validator_index = ValidatorIndex(0);
let mut tracker = GridTracker::default();
let session_topology = SessionTopologyView {
group_views: vec![(
GroupIndex(0),
GroupSubView {
sending: HashSet::new(),
receiving: HashSet::from([validator_index]),
},
)]
.into_iter()
.collect(),
};
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
let group_index = GroupIndex(0);
let group_size = 3;
let local_knowledge = StatementFilter::blank(group_size);
let groups = dummy_groups(group_size);
tracker.add_backed_candidate(
&session_topology,
candidate_hash,
group_index,
local_knowledge.clone(),
);
let pending_manifest = tracker.is_manifest_pending_for(validator_index, &candidate_hash);
assert_eq!(pending_manifest, None);
let ack = tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: group_index,
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1],
},
},
ManifestKind::Full,
validator_index,
);
assert_matches!(ack, Ok(true));
let pending_manifest = tracker.is_manifest_pending_for(validator_index, &candidate_hash);
assert_eq!(pending_manifest, Some(ManifestKind::Acknowledgement));
tracker.manifest_sent_to(&groups, validator_index, candidate_hash, local_knowledge);
let pending_manifest = tracker.is_manifest_pending_for(validator_index, &candidate_hash);
assert_eq!(pending_manifest, None);
}
#[test]
fn pending_statements_are_updated_after_manifest_exchange() {
let send_to = ValidatorIndex(0);
let receive_from = ValidatorIndex(1);
let mut tracker = GridTracker::default();
let session_topology = SessionTopologyView {
group_views: vec![(
GroupIndex(0),
GroupSubView {
sending: HashSet::from([send_to]),
receiving: HashSet::from([receive_from]),
},
)]
.into_iter()
.collect(),
};
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
let group_index = GroupIndex(0);
let group_size = 3;
let local_knowledge = StatementFilter::blank(group_size);
let groups = dummy_groups(group_size);
let receivers = tracker.add_backed_candidate(
&session_topology,
candidate_hash,
group_index,
local_knowledge.clone(),
);
assert_eq!(receivers, vec![(send_to, ManifestKind::Full)]);
tracker.learned_fresh_statement(
&groups,
&session_topology,
ValidatorIndex(2),
&CompactStatement::Seconded(candidate_hash),
);
{
assert_eq!(tracker.pending_statements_for(receive_from, candidate_hash), None);
assert_eq!(tracker.all_pending_statements_for(receive_from), vec![]);
let ack = tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: group_index,
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1],
},
},
ManifestKind::Full,
receive_from,
);
assert_matches!(ack, Ok(true));
tracker.manifest_sent_to(
&groups,
receive_from,
candidate_hash,
local_knowledge.clone(),
);
assert_eq!(
tracker.pending_statements_for(receive_from, candidate_hash),
Some(StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 1],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0],
})
);
assert_eq!(
tracker.all_pending_statements_for(receive_from),
vec![(ValidatorIndex(2), CompactStatement::Seconded(candidate_hash))]
);
}
{
assert_eq!(tracker.pending_statements_for(send_to, candidate_hash), None);
assert_eq!(tracker.all_pending_statements_for(send_to), vec![]);
tracker.manifest_sent_to(&groups, send_to, candidate_hash, local_knowledge.clone());
let ack = tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: group_index,
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 1],
},
},
ManifestKind::Acknowledgement,
send_to,
);
assert_matches!(ack, Ok(false));
assert_eq!(
tracker.pending_statements_for(send_to, candidate_hash),
Some(StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 1],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0],
})
);
assert_eq!(
tracker.all_pending_statements_for(send_to),
vec![(ValidatorIndex(2), CompactStatement::Seconded(candidate_hash))]
);
}
}
#[test]
fn invalid_fresh_statement_import() {
let validator_index = ValidatorIndex(0);
let mut tracker = GridTracker::default();
let session_topology = SessionTopologyView {
group_views: vec![(
GroupIndex(0),
GroupSubView {
sending: HashSet::new(),
receiving: HashSet::from([validator_index]),
},
)]
.into_iter()
.collect(),
};
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
let group_index = GroupIndex(0);
let group_size = 3;
let local_knowledge = StatementFilter::blank(group_size);
let groups = dummy_groups(group_size);
assert_eq!(tracker.pending_statements_for(validator_index, candidate_hash), None);
assert_eq!(tracker.all_pending_statements_for(validator_index), vec![]);
let statement = CompactStatement::Seconded(candidate_hash);
tracker.learned_fresh_statement(&groups, &session_topology, validator_index, &statement);
assert_eq!(tracker.pending_statements_for(validator_index, candidate_hash), None);
assert_eq!(tracker.all_pending_statements_for(validator_index), vec![]);
tracker.add_backed_candidate(
&session_topology,
candidate_hash,
group_index,
local_knowledge.clone(),
);
let statement = CompactStatement::Seconded(candidate_hash);
tracker.learned_fresh_statement(&groups, &session_topology, ValidatorIndex(1), &statement);
assert_eq!(tracker.pending_statements_for(validator_index, candidate_hash), None);
assert_eq!(tracker.all_pending_statements_for(validator_index), vec![]);
}
#[test]
fn pending_statements_updated_when_importing_fresh_statement() {
let validator_index = ValidatorIndex(0);
let mut tracker = GridTracker::default();
let session_topology = SessionTopologyView {
group_views: vec![(
GroupIndex(0),
GroupSubView {
sending: HashSet::new(),
receiving: HashSet::from([validator_index]),
},
)]
.into_iter()
.collect(),
};
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
let group_index = GroupIndex(0);
let group_size = 3;
let local_knowledge = StatementFilter::blank(group_size);
let groups = dummy_groups(group_size);
assert_eq!(tracker.pending_statements_for(validator_index, candidate_hash), None);
assert_eq!(tracker.all_pending_statements_for(validator_index), vec![]);
tracker.add_backed_candidate(
&session_topology,
candidate_hash,
group_index,
local_knowledge.clone(),
);
let ack = tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: group_index,
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1],
},
},
ManifestKind::Full,
validator_index,
);
assert_matches!(ack, Ok(true));
tracker.manifest_sent_to(&groups, validator_index, candidate_hash, local_knowledge);
let statement = CompactStatement::Seconded(candidate_hash);
tracker.learned_fresh_statement(&groups, &session_topology, validator_index, &statement);
let statements = StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0],
};
assert_eq!(
tracker.pending_statements_for(validator_index, candidate_hash),
Some(statements.clone())
);
assert_eq!(
tracker.all_pending_statements_for(validator_index),
vec![(ValidatorIndex(0), CompactStatement::Seconded(candidate_hash))]
);
tracker.learned_fresh_statement(&groups, &session_topology, validator_index, &statement);
assert_eq!(
tracker.pending_statements_for(validator_index, candidate_hash),
Some(statements)
);
assert_eq!(
tracker.all_pending_statements_for(validator_index),
vec![(ValidatorIndex(0), CompactStatement::Seconded(candidate_hash))]
);
}
#[test]
fn pending_statements_respect_remote_knowledge() {
let validator_index = ValidatorIndex(0);
let mut tracker = GridTracker::default();
let session_topology = SessionTopologyView {
group_views: vec![(
GroupIndex(0),
GroupSubView {
sending: HashSet::new(),
receiving: HashSet::from([validator_index]),
},
)]
.into_iter()
.collect(),
};
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
let group_index = GroupIndex(0);
let group_size = 3;
let local_knowledge = StatementFilter::blank(group_size);
let groups = dummy_groups(group_size);
assert_eq!(tracker.pending_statements_for(validator_index, candidate_hash), None);
assert_eq!(tracker.all_pending_statements_for(validator_index), vec![]);
tracker.add_backed_candidate(
&session_topology,
candidate_hash,
group_index,
local_knowledge.clone(),
);
let ack = tracker.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: group_index,
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0],
},
},
ManifestKind::Full,
validator_index,
);
assert_matches!(ack, Ok(true));
tracker.manifest_sent_to(&groups, validator_index, candidate_hash, local_knowledge);
tracker.learned_fresh_statement(
&groups,
&session_topology,
validator_index,
&CompactStatement::Seconded(candidate_hash),
);
tracker.learned_fresh_statement(
&groups,
&session_topology,
validator_index,
&CompactStatement::Valid(candidate_hash),
);
let statements = StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 0],
};
assert_eq!(
tracker.pending_statements_for(validator_index, candidate_hash),
Some(statements.clone())
);
assert_eq!(
tracker.all_pending_statements_for(validator_index),
vec![(ValidatorIndex(0), CompactStatement::Valid(candidate_hash))]
);
}
#[test]
fn pending_statements_cleared_when_sending() {
let validator_index = ValidatorIndex(0);
let counterparty = ValidatorIndex(1);
let mut tracker = GridTracker::default();
let session_topology = SessionTopologyView {
group_views: vec![(
GroupIndex(0),
GroupSubView {
sending: HashSet::new(),
receiving: HashSet::from([validator_index, counterparty]),
},
)]
.into_iter()
.collect(),
};
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
let group_index = GroupIndex(0);
let group_size = 3;
let local_knowledge = StatementFilter::blank(group_size);
let groups = dummy_groups(group_size);
assert_eq!(tracker.pending_statements_for(validator_index, candidate_hash), None);
assert_eq!(tracker.all_pending_statements_for(validator_index), vec![]);
tracker.add_backed_candidate(
&session_topology,
candidate_hash,
group_index,
local_knowledge.clone(),
);
tracker
.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: group_index,
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1],
},
},
ManifestKind::Full,
validator_index,
)
.ok()
.unwrap();
tracker.manifest_sent_to(&groups, validator_index, candidate_hash, local_knowledge.clone());
let statement = CompactStatement::Seconded(candidate_hash);
tracker.learned_fresh_statement(&groups, &session_topology, validator_index, &statement);
tracker
.import_manifest(
&session_topology,
&groups,
candidate_hash,
3,
ManifestSummary {
claimed_parent_hash: Hash::repeat_byte(0),
claimed_group_index: group_index,
statement_knowledge: StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 0, 1, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 1],
},
},
ManifestKind::Full,
counterparty,
)
.ok()
.unwrap();
tracker.manifest_sent_to(&groups, counterparty, candidate_hash, local_knowledge);
let statement = CompactStatement::Seconded(candidate_hash);
tracker.learned_fresh_statement(&groups, &session_topology, counterparty, &statement);
let statements = StatementFilter {
seconded_in_group: bitvec::bitvec![u8, Lsb0; 1, 0, 0],
validated_in_group: bitvec::bitvec![u8, Lsb0; 0, 0, 0],
};
assert_eq!(
tracker.pending_statements_for(validator_index, candidate_hash),
Some(statements.clone())
);
assert_eq!(
tracker.all_pending_statements_for(validator_index),
vec![(ValidatorIndex(0), CompactStatement::Seconded(candidate_hash))]
);
assert_eq!(
tracker.pending_statements_for(counterparty, candidate_hash),
Some(statements.clone())
);
assert_eq!(
tracker.all_pending_statements_for(counterparty),
vec![(ValidatorIndex(0), CompactStatement::Seconded(candidate_hash))]
);
tracker.learned_fresh_statement(&groups, &session_topology, validator_index, &statement);
tracker.sent_or_received_direct_statement(
&groups,
validator_index,
counterparty,
&statement,
false,
);
assert_eq!(
tracker.pending_statements_for(counterparty, candidate_hash),
Some(StatementFilter::blank(group_size))
);
assert_eq!(tracker.all_pending_statements_for(counterparty), vec![]);
}
#[test]
fn session_grid_topology_consistent() {
let n_validators = 300;
let group_size = 5;
let validator_indices =
(0..n_validators).map(|i| ValidatorIndex(i as u32)).collect::<Vec<_>>();
let groups = validator_indices.chunks(group_size).map(|x| x.to_vec()).collect::<Vec<_>>();
let topology = SessionGridTopology::new(
(0..n_validators).collect::<Vec<_>>(),
(0..n_validators)
.map(|i| TopologyPeerInfo {
peer_ids: Vec::new(),
validator_index: ValidatorIndex(i as u32),
discovery_id: AuthorityDiscoveryPair::generate().0.public(),
})
.collect(),
);
let computed_topologies = validator_indices
.iter()
.cloned()
.map(|v| build_session_topology(groups.iter(), &topology, Some(v)))
.collect::<Vec<_>>();
let pairwise_check_topologies = |i, j| {
let v_i = ValidatorIndex(i);
let v_j = ValidatorIndex(j);
for group in (0..groups.len()).map(|i| GroupIndex(i as u32)) {
let g_i = computed_topologies[i as usize].group_views.get(&group).unwrap();
let g_j = computed_topologies[j as usize].group_views.get(&group).unwrap();
if g_i.sending.contains(&v_j) {
assert!(
g_j.receiving.contains(&v_i),
"{:?}: {:?}, sending but not receiving",
group,
&(i, j)
);
}
if g_j.sending.contains(&v_i) {
assert!(
g_i.receiving.contains(&v_j),
"{:?}: {:?}, sending but not receiving",
group,
&(j, i)
);
}
if g_i.receiving.contains(&v_j) {
assert!(g_j.sending.contains(&v_i), "{:?}, receiving but not sending", &(i, j));
}
if g_j.receiving.contains(&v_i) {
assert!(g_i.sending.contains(&v_j), "{:?}, receiving but not sending", &(j, i));
}
}
};
for i in 0..n_validators {
for j in (i + 1)..n_validators {
pairwise_check_topologies(i as u32, j as u32);
}
}
}
}