// Copyright (C) Parity Technologies (UK) Ltd. // This file is part of Polkadot. // Polkadot is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Polkadot is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . //! Direct distribution of statements within a cluster, //! even those concerning candidates which are not yet backed. //! //! Members of a validation group assigned to a para at a given relay-parent //! always distribute statements directly to each other. //! //! The main way we limit the amount of candidates that have to be handled by //! the system is to limit the amount of `Seconded` messages that we allow //! each validator to issue at each relay-parent. Since the amount of relay-parents //! that we have to deal with at any time is itself bounded, this lets us bound //! the memory and work that we have here. Bounding `Seconded` statements is enough //! because they imply a bounded amount of `Valid` statements about the same candidate //! which may follow. //! //! The motivation for this piece of code is that the statements that each validator //! sees may differ. i.e. even though a validator is allowed to issue X `Seconded` //! statements at a relay-parent, they may in fact issue X*2 and issue one set to //! one partition of the backing group and one set to another. Of course, in practice //! these types of partitions will not exist, but in the worst case each validator in the //! group would see an entirely different set of X `Seconded` statements from some validator //! and each validator is in its own partition. After that partition resolves, we'd have to //! deal with up to `limit*group_size` `Seconded` statements from that validator. And then //! if every validator in the group does the same thing, we're dealing with something like //! `limit*group_size^2` `Seconded` statements in total. //! //! Given that both our group sizes and our limits per relay-parent are small, this is //! quite manageable, and the utility here lets us deal with it in only a few kilobytes //! of memory. //! //! It's also worth noting that any case where a validator issues more than the legal limit //! of `Seconded` statements at a relay parent is trivially slashable on-chain, which means //! the 'worst case' adversary that this code defends against is effectively lighting money //! on fire. Nevertheless, we handle the case here to ensure that the behavior of the //! system is well-defined even if an adversary is willing to be slashed. //! //! More concretely, this module exposes a [`ClusterTracker`] utility which allows us to determine //! whether to accept or reject messages from other validators in the same group as we //! are in, based on _the most charitable possible interpretation of our protocol rules_, //! and to keep track of what we have sent to other validators in the group and what we may //! continue to send them. use polkadot_primitives::{CandidateHash, CompactStatement, Hash, ValidatorIndex}; use crate::LOG_TARGET; use std::collections::{HashMap, HashSet}; #[derive(Hash, PartialEq, Eq)] struct ValidStatementManifest { remote: ValidatorIndex, originator: ValidatorIndex, candidate_hash: CandidateHash, } // A piece of knowledge about a candidate #[derive(Hash, Clone, PartialEq, Eq)] enum Knowledge { // General knowledge. General(CandidateHash), // Specific knowledge of a given statement (with its originator) Specific(CompactStatement, ValidatorIndex), } // Knowledge paired with its source. #[derive(Hash, Clone, PartialEq, Eq)] enum TaggedKnowledge { // Knowledge we have received from the validator on the p2p layer. IncomingP2P(Knowledge), // Knowledge we have sent to the validator on the p2p layer. OutgoingP2P(Knowledge), // Knowledge of candidates the validator has seconded. // This is limited only to `Seconded` statements we have accepted // _without prejudice_. Seconded(CandidateHash), } /// Utility for keeping track of limits on direct statements within a group. /// /// See module docs for more details. pub struct ClusterTracker { validators: Vec, seconding_limit: usize, knowledge: HashMap>, // Statements known locally which haven't been sent to particular validators. // maps target validator to (originator, statement) pairs. pending: HashMap>, } impl ClusterTracker { /// Instantiate a new `ClusterTracker` tracker. Fails if `cluster_validators` is empty pub fn new(cluster_validators: Vec, seconding_limit: usize) -> Option { if cluster_validators.is_empty() { return None } Some(ClusterTracker { validators: cluster_validators, seconding_limit, knowledge: HashMap::new(), pending: HashMap::new(), }) } /// Query whether we can receive some statement from the given validator. /// /// This does no deduplication of `Valid` statements. pub fn can_receive( &self, sender: ValidatorIndex, originator: ValidatorIndex, statement: CompactStatement, ) -> Result { if !self.is_in_group(sender) || !self.is_in_group(originator) { return Err(RejectIncoming::NotInGroup) } if self.they_sent(sender, Knowledge::Specific(statement.clone(), originator)) { return Err(RejectIncoming::Duplicate) } match statement { CompactStatement::Seconded(candidate_hash) => { // check whether the sender has not sent too many seconded statements for the // originator. we know by the duplicate check above that this iterator doesn't // include the statement itself. let other_seconded_for_orig_from_remote = self .knowledge .get(&sender) .into_iter() .flat_map(|v_knowledge| v_knowledge.iter()) .filter(|k| match k { TaggedKnowledge::IncomingP2P(Knowledge::Specific( CompactStatement::Seconded(_), orig, )) if orig == &originator => true, _ => false, }) .count(); if other_seconded_for_orig_from_remote == self.seconding_limit { return Err(RejectIncoming::ExcessiveSeconded) } // at this point, it doesn't seem like the remote has done anything wrong. if self.seconded_already_or_within_limit(originator, candidate_hash) { Ok(Accept::Ok) } else { Ok(Accept::WithPrejudice) } }, CompactStatement::Valid(candidate_hash) => { if !self.knows_candidate(sender, candidate_hash) { return Err(RejectIncoming::CandidateUnknown) } Ok(Accept::Ok) }, } } /// Note that we issued a statement. This updates internal structures. pub fn note_issued(&mut self, originator: ValidatorIndex, statement: CompactStatement) { for cluster_member in &self.validators { if !self.they_know_statement(*cluster_member, originator, statement.clone()) { // add the statement to pending knowledge for all peers // which don't know the statement. self.pending .entry(*cluster_member) .or_default() .insert((originator, statement.clone())); } } } /// Note that we accepted an incoming statement. This updates internal structures. /// /// Should only be called after a successful `can_receive` call. pub fn note_received( &mut self, sender: ValidatorIndex, originator: ValidatorIndex, statement: CompactStatement, ) { for cluster_member in &self.validators { if cluster_member == &sender { if let Some(pending) = self.pending.get_mut(&sender) { pending.remove(&(originator, statement.clone())); } } else if !self.they_know_statement(*cluster_member, originator, statement.clone()) { // add the statement to pending knowledge for all peers // which don't know the statement. self.pending .entry(*cluster_member) .or_default() .insert((originator, statement.clone())); } } { let sender_knowledge = self.knowledge.entry(sender).or_default(); sender_knowledge.insert(TaggedKnowledge::IncomingP2P(Knowledge::Specific( statement.clone(), originator, ))); if let CompactStatement::Seconded(candidate_hash) = statement.clone() { sender_knowledge .insert(TaggedKnowledge::IncomingP2P(Knowledge::General(candidate_hash))); } } if let CompactStatement::Seconded(candidate_hash) = statement { // since we accept additional `Seconded` statements beyond the limits // 'with prejudice', we must respect the limit here. if self.seconded_already_or_within_limit(originator, candidate_hash) { let originator_knowledge = self.knowledge.entry(originator).or_default(); originator_knowledge.insert(TaggedKnowledge::Seconded(candidate_hash)); } } } /// Query whether we can send a statement to a given validator. pub fn can_send( &self, target: ValidatorIndex, originator: ValidatorIndex, statement: CompactStatement, ) -> Result<(), RejectOutgoing> { if !self.is_in_group(target) || !self.is_in_group(originator) { return Err(RejectOutgoing::NotInGroup) } if self.they_know_statement(target, originator, statement.clone()) { return Err(RejectOutgoing::Known) } match statement { CompactStatement::Seconded(candidate_hash) => { // we send the same `Seconded` statements to all our peers, and only the first `k` // from each originator. if !self.seconded_already_or_within_limit(originator, candidate_hash) { return Err(RejectOutgoing::ExcessiveSeconded) } Ok(()) }, CompactStatement::Valid(candidate_hash) => { if !self.knows_candidate(target, candidate_hash) { return Err(RejectOutgoing::CandidateUnknown) } Ok(()) }, } } /// Note that we sent an outgoing statement to a peer in the group. /// This must be preceded by a successful `can_send` call. pub fn note_sent( &mut self, target: ValidatorIndex, originator: ValidatorIndex, statement: CompactStatement, ) { { let target_knowledge = self.knowledge.entry(target).or_default(); target_knowledge.insert(TaggedKnowledge::OutgoingP2P(Knowledge::Specific( statement.clone(), originator, ))); if let CompactStatement::Seconded(candidate_hash) = statement.clone() { target_knowledge .insert(TaggedKnowledge::OutgoingP2P(Knowledge::General(candidate_hash))); } } if let CompactStatement::Seconded(candidate_hash) = statement { let originator_knowledge = self.knowledge.entry(originator).or_default(); originator_knowledge.insert(TaggedKnowledge::Seconded(candidate_hash)); } if let Some(pending) = self.pending.get_mut(&target) { pending.remove(&(originator, statement)); } } /// Get all targets as validator-indices. This doesn't attempt to filter /// out the local validator index. pub fn targets(&self) -> &[ValidatorIndex] { &self.validators } /// Get all possible senders for the given originator. /// Returns the empty slice in the case that the originator /// is not part of the cluster. // note: this API is future-proofing for a case where we may // extend clusters beyond just the assigned group, for optimization // purposes. pub fn senders_for_originator(&self, originator: ValidatorIndex) -> &[ValidatorIndex] { if self.validators.contains(&originator) { &self.validators[..] } else { &[] } } /// Whether a validator knows the candidate is `Seconded`. pub fn knows_candidate( &self, validator: ValidatorIndex, candidate_hash: CandidateHash, ) -> bool { // we sent, they sent, or they signed and we received from someone else. self.we_sent_seconded(validator, candidate_hash) || self.they_sent_seconded(validator, candidate_hash) || self.validator_seconded(validator, candidate_hash) } /// Whether a validator can request a candidate from us. pub fn can_request(&self, target: ValidatorIndex, candidate_hash: CandidateHash) -> bool { self.validators.contains(&target) && self.we_sent_seconded(target, candidate_hash) && !self.they_sent_seconded(target, candidate_hash) } /// Returns a Vec of pending statements to be sent to a particular validator /// index. `Seconded` statements are sorted to the front of the vector. /// /// Pending statements have the form (originator, compact statement). pub fn pending_statements_for( &self, target: ValidatorIndex, ) -> Vec<(ValidatorIndex, CompactStatement)> { let mut v = self .pending .get(&target) .map(|x| x.iter().cloned().collect::>()) .unwrap_or_default(); v.sort_by_key(|(_, s)| match s { CompactStatement::Seconded(_) => 0u8, CompactStatement::Valid(_) => 1u8, }); v } // returns true if it's legal to accept a new `Seconded` message from this validator. // This is either // 1. because we've already accepted it. // 2. because there's space for more seconding. fn seconded_already_or_within_limit( &self, validator: ValidatorIndex, candidate_hash: CandidateHash, ) -> bool { let seconded_other_candidates = self .knowledge .get(&validator) .into_iter() .flat_map(|v_knowledge| v_knowledge.iter()) .filter(|k| match k { TaggedKnowledge::Seconded(c) if c != &candidate_hash => true, _ => false, }) .count(); // This fulfills both properties by under-counting when the validator is at the limit // but _has_ seconded the candidate already. seconded_other_candidates < self.seconding_limit } fn they_know_statement( &self, validator: ValidatorIndex, originator: ValidatorIndex, statement: CompactStatement, ) -> bool { let knowledge = Knowledge::Specific(statement, originator); self.we_sent(validator, knowledge.clone()) || self.they_sent(validator, knowledge) } fn they_sent(&self, validator: ValidatorIndex, knowledge: Knowledge) -> bool { self.knowledge .get(&validator) .map_or(false, |k| k.contains(&TaggedKnowledge::IncomingP2P(knowledge))) } fn we_sent(&self, validator: ValidatorIndex, knowledge: Knowledge) -> bool { self.knowledge .get(&validator) .map_or(false, |k| k.contains(&TaggedKnowledge::OutgoingP2P(knowledge))) } fn we_sent_seconded(&self, validator: ValidatorIndex, candidate_hash: CandidateHash) -> bool { self.we_sent(validator, Knowledge::General(candidate_hash)) } fn they_sent_seconded(&self, validator: ValidatorIndex, candidate_hash: CandidateHash) -> bool { self.they_sent(validator, Knowledge::General(candidate_hash)) } fn validator_seconded(&self, validator: ValidatorIndex, candidate_hash: CandidateHash) -> bool { self.knowledge .get(&validator) .map_or(false, |k| k.contains(&TaggedKnowledge::Seconded(candidate_hash))) } fn is_in_group(&self, validator: ValidatorIndex) -> bool { self.validators.contains(&validator) } /// Dumps pending statement for this cluster. /// /// Normally we should not have pending statements to validators in our cluster, /// but if we do for all validators in our cluster, then we don't participate /// in backing. Occasional pending statements are expected if two authorities /// can't detect each other or after restart, where it takes a while to discover /// the whole network. pub fn warn_if_too_many_pending_statements(&self, parent_hash: Hash) { if self.pending.iter().filter(|pending| !pending.1.is_empty()).count() >= self.validators.len() { gum::warn!( target: LOG_TARGET, pending_statements = ?self.pending, ?parent_hash, "Cluster has too many pending statements, something wrong with our connection to our group peers Restart might be needed if validator gets 0 backing rewards for more than 3-4 consecutive sessions" ); } } } /// Incoming statement was accepted. #[derive(Debug, PartialEq)] pub enum Accept { /// Neither the peer nor the originator have apparently exceeded limits. /// Candidate or statement may already be known. Ok, /// Accept the message; the peer hasn't exceeded limits but the originator has. WithPrejudice, } /// Incoming statement was rejected. #[derive(Debug, PartialEq)] pub enum RejectIncoming { /// Peer sent excessive `Seconded` statements. ExcessiveSeconded, /// Sender or originator is not in the group. NotInGroup, /// Candidate is unknown to us. Only applies to `Valid` statements. CandidateUnknown, /// Statement is duplicate. Duplicate, } /// Outgoing statement was rejected. #[derive(Debug, PartialEq)] pub enum RejectOutgoing { /// Candidate was unknown. Only applies to `Valid` statements. CandidateUnknown, /// We attempted to send excessive `Seconded` statements. /// indicates a bug on the local node's code. ExcessiveSeconded, /// The statement was already known to the peer. Known, /// Target or originator not in the group. NotInGroup, } #[cfg(test)] mod tests { use super::*; use polkadot_primitives::Hash; #[test] fn rejects_incoming_outside_of_group() { let group = vec![ValidatorIndex(5), ValidatorIndex(200), ValidatorIndex(24), ValidatorIndex(146)]; let seconding_limit = 2; let tracker = ClusterTracker::new(group.clone(), seconding_limit).expect("not empty"); assert_eq!( tracker.can_receive( ValidatorIndex(100), ValidatorIndex(5), CompactStatement::Seconded(CandidateHash(Hash::repeat_byte(1))), ), Err(RejectIncoming::NotInGroup), ); assert_eq!( tracker.can_receive( ValidatorIndex(5), ValidatorIndex(100), CompactStatement::Seconded(CandidateHash(Hash::repeat_byte(1))), ), Err(RejectIncoming::NotInGroup), ); } #[test] fn begrudgingly_accepts_too_many_seconded_from_multiple_peers() { let group = vec![ValidatorIndex(5), ValidatorIndex(200), ValidatorIndex(24), ValidatorIndex(146)]; let seconding_limit = 2; let hash_a = CandidateHash(Hash::repeat_byte(1)); let hash_b = CandidateHash(Hash::repeat_byte(2)); let hash_c = CandidateHash(Hash::repeat_byte(3)); let mut tracker = ClusterTracker::new(group.clone(), seconding_limit).expect("not empty"); assert_eq!( tracker.can_receive( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ), Ok(Accept::Ok), ); tracker.note_received( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ); assert_eq!( tracker.can_receive( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Seconded(hash_b), ), Ok(Accept::Ok), ); tracker.note_received( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Seconded(hash_b), ); assert_eq!( tracker.can_receive( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Seconded(hash_c), ), Err(RejectIncoming::ExcessiveSeconded), ); } #[test] fn rejects_too_many_seconded_from_sender() { let group = vec![ValidatorIndex(5), ValidatorIndex(200), ValidatorIndex(24), ValidatorIndex(146)]; let seconding_limit = 2; let hash_a = CandidateHash(Hash::repeat_byte(1)); let hash_b = CandidateHash(Hash::repeat_byte(2)); let hash_c = CandidateHash(Hash::repeat_byte(3)); let mut tracker = ClusterTracker::new(group.clone(), seconding_limit).expect("not empty"); assert_eq!( tracker.can_receive( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ), Ok(Accept::Ok), ); tracker.note_received( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ); assert_eq!( tracker.can_receive( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Seconded(hash_b), ), Ok(Accept::Ok), ); tracker.note_received( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Seconded(hash_b), ); assert_eq!( tracker.can_receive( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_c), ), Ok(Accept::WithPrejudice), ); } #[test] fn rejects_duplicates() { let group = vec![ValidatorIndex(5), ValidatorIndex(200), ValidatorIndex(24), ValidatorIndex(146)]; let seconding_limit = 2; let hash_a = CandidateHash(Hash::repeat_byte(1)); let mut tracker = ClusterTracker::new(group, seconding_limit).expect("not empty"); tracker.note_received( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ); tracker.note_received( ValidatorIndex(5), ValidatorIndex(200), CompactStatement::Valid(hash_a), ); assert_eq!( tracker.can_receive( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ), Err(RejectIncoming::Duplicate), ); assert_eq!( tracker.can_receive( ValidatorIndex(5), ValidatorIndex(200), CompactStatement::Valid(hash_a), ), Err(RejectIncoming::Duplicate), ); } #[test] fn rejects_incoming_valid_without_seconded() { let group = vec![ValidatorIndex(5), ValidatorIndex(200), ValidatorIndex(24), ValidatorIndex(146)]; let seconding_limit = 2; let tracker = ClusterTracker::new(group, seconding_limit).expect("not empty"); let hash_a = CandidateHash(Hash::repeat_byte(1)); assert_eq!( tracker.can_receive( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Valid(hash_a), ), Err(RejectIncoming::CandidateUnknown), ); } #[test] fn accepts_incoming_valid_after_receiving_seconded() { let group = vec![ValidatorIndex(5), ValidatorIndex(200), ValidatorIndex(24), ValidatorIndex(146)]; let seconding_limit = 2; let mut tracker = ClusterTracker::new(group.clone(), seconding_limit).expect("not empty"); let hash_a = CandidateHash(Hash::repeat_byte(1)); tracker.note_received( ValidatorIndex(5), ValidatorIndex(200), CompactStatement::Seconded(hash_a), ); assert_eq!( tracker.can_receive( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Valid(hash_a), ), Ok(Accept::Ok) ); } #[test] fn accepts_incoming_valid_after_outgoing_seconded() { let group = vec![ValidatorIndex(5), ValidatorIndex(200), ValidatorIndex(24), ValidatorIndex(146)]; let seconding_limit = 2; let mut tracker = ClusterTracker::new(group.clone(), seconding_limit).expect("not empty"); let hash_a = CandidateHash(Hash::repeat_byte(1)); tracker.note_sent( ValidatorIndex(5), ValidatorIndex(200), CompactStatement::Seconded(hash_a), ); assert_eq!( tracker.can_receive( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Valid(hash_a), ), Ok(Accept::Ok) ); } #[test] fn cannot_send_too_many_seconded_even_to_multiple_peers() { let group = vec![ValidatorIndex(5), ValidatorIndex(200), ValidatorIndex(24), ValidatorIndex(146)]; let seconding_limit = 2; let mut tracker = ClusterTracker::new(group.clone(), seconding_limit).expect("not empty"); let hash_a = CandidateHash(Hash::repeat_byte(1)); let hash_b = CandidateHash(Hash::repeat_byte(2)); let hash_c = CandidateHash(Hash::repeat_byte(3)); tracker.note_sent( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ); tracker.note_sent( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_b), ); assert_eq!( tracker.can_send( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_c), ), Err(RejectOutgoing::ExcessiveSeconded), ); assert_eq!( tracker.can_send( ValidatorIndex(24), ValidatorIndex(5), CompactStatement::Seconded(hash_c), ), Err(RejectOutgoing::ExcessiveSeconded), ); } #[test] fn cannot_send_duplicate() { let group = vec![ValidatorIndex(5), ValidatorIndex(200), ValidatorIndex(24), ValidatorIndex(146)]; let seconding_limit = 2; let mut tracker = ClusterTracker::new(group.clone(), seconding_limit).expect("not empty"); let hash_a = CandidateHash(Hash::repeat_byte(1)); tracker.note_sent( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ); assert_eq!( tracker.can_send( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ), Err(RejectOutgoing::Known), ); } #[test] fn cannot_send_what_was_received() { let group = vec![ValidatorIndex(5), ValidatorIndex(200), ValidatorIndex(24), ValidatorIndex(146)]; let seconding_limit = 2; let mut tracker = ClusterTracker::new(group.clone(), seconding_limit).expect("not empty"); let hash_a = CandidateHash(Hash::repeat_byte(1)); tracker.note_received( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ); assert_eq!( tracker.can_send( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ), Err(RejectOutgoing::Known), ); } // Ensure statements received with prejudice don't prevent sending later. #[test] fn can_send_statements_received_with_prejudice() { let group = vec![ValidatorIndex(5), ValidatorIndex(200), ValidatorIndex(24), ValidatorIndex(146)]; let seconding_limit = 1; let mut tracker = ClusterTracker::new(group.clone(), seconding_limit).expect("not empty"); let hash_a = CandidateHash(Hash::repeat_byte(1)); let hash_b = CandidateHash(Hash::repeat_byte(2)); assert_eq!( tracker.can_receive( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ), Ok(Accept::Ok), ); tracker.note_received( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ); assert_eq!( tracker.can_receive( ValidatorIndex(24), ValidatorIndex(5), CompactStatement::Seconded(hash_b), ), Ok(Accept::WithPrejudice), ); tracker.note_received( ValidatorIndex(24), ValidatorIndex(5), CompactStatement::Seconded(hash_b), ); assert_eq!( tracker.can_send( ValidatorIndex(24), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ), Ok(()), ); } // Test that the `pending_statements` are set whenever we receive a fresh statement. // // Also test that pending statements are sorted, with `Seconded` statements in the front. #[test] fn pending_statements_set_when_receiving_fresh_statements() { let group = vec![ValidatorIndex(5), ValidatorIndex(200), ValidatorIndex(24), ValidatorIndex(146)]; let seconding_limit = 1; let mut tracker = ClusterTracker::new(group.clone(), seconding_limit).expect("not empty"); let hash_a = CandidateHash(Hash::repeat_byte(1)); let hash_b = CandidateHash(Hash::repeat_byte(2)); // Receive a 'Seconded' statement for candidate A. { assert_eq!( tracker.can_receive( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ), Ok(Accept::Ok), ); tracker.note_received( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(5)), vec![(ValidatorIndex(5), CompactStatement::Seconded(hash_a))] ); assert_eq!(tracker.pending_statements_for(ValidatorIndex(200)), vec![]); assert_eq!( tracker.pending_statements_for(ValidatorIndex(24)), vec![(ValidatorIndex(5), CompactStatement::Seconded(hash_a))] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(146)), vec![(ValidatorIndex(5), CompactStatement::Seconded(hash_a))] ); } // Receive a 'Valid' statement for candidate A. { // First, send a `Seconded` statement for the candidate. assert_eq!( tracker.can_send( ValidatorIndex(24), ValidatorIndex(200), CompactStatement::Seconded(hash_a) ), Ok(()) ); tracker.note_sent( ValidatorIndex(24), ValidatorIndex(200), CompactStatement::Seconded(hash_a), ); // We have to see that the candidate is known by the sender, e.g. we sent them // 'Seconded' above. assert_eq!( tracker.can_receive( ValidatorIndex(24), ValidatorIndex(200), CompactStatement::Valid(hash_a), ), Ok(Accept::Ok), ); tracker.note_received( ValidatorIndex(24), ValidatorIndex(200), CompactStatement::Valid(hash_a), ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(5)), vec![ (ValidatorIndex(5), CompactStatement::Seconded(hash_a)), (ValidatorIndex(200), CompactStatement::Valid(hash_a)) ] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(200)), vec![(ValidatorIndex(200), CompactStatement::Valid(hash_a))] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(24)), vec![(ValidatorIndex(5), CompactStatement::Seconded(hash_a))] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(146)), vec![ (ValidatorIndex(5), CompactStatement::Seconded(hash_a)), (ValidatorIndex(200), CompactStatement::Valid(hash_a)) ] ); } // Receive a 'Seconded' statement for candidate B. { assert_eq!( tracker.can_receive( ValidatorIndex(5), ValidatorIndex(146), CompactStatement::Seconded(hash_b), ), Ok(Accept::Ok), ); tracker.note_received( ValidatorIndex(5), ValidatorIndex(146), CompactStatement::Seconded(hash_b), ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(5)), vec![ (ValidatorIndex(5), CompactStatement::Seconded(hash_a)), (ValidatorIndex(200), CompactStatement::Valid(hash_a)) ] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(200)), vec![ (ValidatorIndex(146), CompactStatement::Seconded(hash_b)), (ValidatorIndex(200), CompactStatement::Valid(hash_a)), ] ); { let mut pending_statements = tracker.pending_statements_for(ValidatorIndex(24)); pending_statements.sort(); assert_eq!( pending_statements, vec![ (ValidatorIndex(5), CompactStatement::Seconded(hash_a)), (ValidatorIndex(146), CompactStatement::Seconded(hash_b)) ], ); } { let mut pending_statements = tracker.pending_statements_for(ValidatorIndex(146)); pending_statements.sort(); assert_eq!( pending_statements, vec![ (ValidatorIndex(5), CompactStatement::Seconded(hash_a)), (ValidatorIndex(146), CompactStatement::Seconded(hash_b)), (ValidatorIndex(200), CompactStatement::Valid(hash_a)), ] ); } } } // Test that the `pending_statements` are updated when we send or receive statements from others // in the cluster. #[test] fn pending_statements_updated_when_sending_statements() { let group = vec![ValidatorIndex(5), ValidatorIndex(200), ValidatorIndex(24), ValidatorIndex(146)]; let seconding_limit = 1; let mut tracker = ClusterTracker::new(group.clone(), seconding_limit).expect("not empty"); let hash_a = CandidateHash(Hash::repeat_byte(1)); let hash_b = CandidateHash(Hash::repeat_byte(2)); // Receive a 'Seconded' statement for candidate A. { assert_eq!( tracker.can_receive( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ), Ok(Accept::Ok), ); tracker.note_received( ValidatorIndex(200), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ); // Pending statements should be updated. assert_eq!( tracker.pending_statements_for(ValidatorIndex(5)), vec![(ValidatorIndex(5), CompactStatement::Seconded(hash_a))] ); assert_eq!(tracker.pending_statements_for(ValidatorIndex(200)), vec![]); assert_eq!( tracker.pending_statements_for(ValidatorIndex(24)), vec![(ValidatorIndex(5), CompactStatement::Seconded(hash_a))] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(146)), vec![(ValidatorIndex(5), CompactStatement::Seconded(hash_a))] ); } // Receive a 'Valid' statement for candidate B. { // First, send a `Seconded` statement for the candidate. assert_eq!( tracker.can_send( ValidatorIndex(24), ValidatorIndex(200), CompactStatement::Seconded(hash_b) ), Ok(()) ); tracker.note_sent( ValidatorIndex(24), ValidatorIndex(200), CompactStatement::Seconded(hash_b), ); // We have to see the candidate is known by the sender, e.g. we sent them 'Seconded'. assert_eq!( tracker.can_receive( ValidatorIndex(24), ValidatorIndex(200), CompactStatement::Valid(hash_b), ), Ok(Accept::Ok), ); tracker.note_received( ValidatorIndex(24), ValidatorIndex(200), CompactStatement::Valid(hash_b), ); // Pending statements should be updated. assert_eq!( tracker.pending_statements_for(ValidatorIndex(5)), vec![ (ValidatorIndex(5), CompactStatement::Seconded(hash_a)), (ValidatorIndex(200), CompactStatement::Valid(hash_b)) ] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(200)), vec![(ValidatorIndex(200), CompactStatement::Valid(hash_b))] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(24)), vec![(ValidatorIndex(5), CompactStatement::Seconded(hash_a))] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(146)), vec![ (ValidatorIndex(5), CompactStatement::Seconded(hash_a)), (ValidatorIndex(200), CompactStatement::Valid(hash_b)) ] ); } // Send a 'Seconded' statement. { assert_eq!( tracker.can_send( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Seconded(hash_a) ), Ok(()) ); tracker.note_sent( ValidatorIndex(5), ValidatorIndex(5), CompactStatement::Seconded(hash_a), ); // Pending statements should be updated. assert_eq!( tracker.pending_statements_for(ValidatorIndex(5)), vec![(ValidatorIndex(200), CompactStatement::Valid(hash_b))] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(200)), vec![(ValidatorIndex(200), CompactStatement::Valid(hash_b))] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(24)), vec![(ValidatorIndex(5), CompactStatement::Seconded(hash_a))] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(146)), vec![ (ValidatorIndex(5), CompactStatement::Seconded(hash_a)), (ValidatorIndex(200), CompactStatement::Valid(hash_b)) ] ); } // Send a 'Valid' statement. { // First, send a `Seconded` statement for the candidate. assert_eq!( tracker.can_send( ValidatorIndex(5), ValidatorIndex(200), CompactStatement::Seconded(hash_b) ), Ok(()) ); tracker.note_sent( ValidatorIndex(5), ValidatorIndex(200), CompactStatement::Seconded(hash_b), ); // We have to see that the candidate is known by the sender, e.g. we sent them // 'Seconded' above. assert_eq!( tracker.can_send( ValidatorIndex(5), ValidatorIndex(200), CompactStatement::Valid(hash_b) ), Ok(()) ); tracker.note_sent( ValidatorIndex(5), ValidatorIndex(200), CompactStatement::Valid(hash_b), ); // Pending statements should be updated. assert_eq!(tracker.pending_statements_for(ValidatorIndex(5)), vec![]); assert_eq!( tracker.pending_statements_for(ValidatorIndex(200)), vec![(ValidatorIndex(200), CompactStatement::Valid(hash_b))] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(24)), vec![(ValidatorIndex(5), CompactStatement::Seconded(hash_a))] ); assert_eq!( tracker.pending_statements_for(ValidatorIndex(146)), vec![ (ValidatorIndex(5), CompactStatement::Seconded(hash_a)), (ValidatorIndex(200), CompactStatement::Valid(hash_b)) ] ); } } }