1use std::{
28 collections::hash_map::{self, Entry, HashMap},
29 fmt::Debug,
30 hash::Hash,
31};
32
33use polkadot_primitives::{
34 effective_minimum_backing_votes, ValidatorSignature,
35 ValidityAttestation as PrimitiveValidityAttestation,
36};
37
38use codec::{Decode, Encode};
39const LOG_TARGET: &str = "parachain::statement-table";
40
41pub trait Context {
43 type AuthorityId: Debug + Hash + Eq + Clone;
45 type Digest: Debug + Hash + Eq + Clone;
47 type GroupId: Debug + Hash + Ord + Eq + Clone;
49 type Signature: Debug + Eq + Clone;
51 type Candidate: Debug + Ord + Eq + Clone;
53
54 fn candidate_digest(candidate: &Self::Candidate) -> Self::Digest;
56
57 fn is_member_of(&self, authority: &Self::AuthorityId, group: &Self::GroupId) -> bool;
60
61 fn get_group_size(&self, group: &Self::GroupId) -> Option<usize>;
63}
64
65pub struct Config {
67 pub allow_multiple_seconded: bool,
71}
72
73#[derive(PartialEq, Eq, Debug, Clone, Encode, Decode)]
75pub enum Statement<Candidate, Digest> {
76 #[codec(index = 1)]
80 Seconded(Candidate),
81 #[codec(index = 2)]
83 Valid(Digest),
84}
85
86#[derive(PartialEq, Eq, Debug, Clone, Encode, Decode)]
88pub struct SignedStatement<Candidate, Digest, AuthorityId, Signature> {
89 pub statement: Statement<Candidate, Digest>,
91 pub signature: Signature,
93 pub sender: AuthorityId,
95}
96
97#[derive(PartialEq, Eq, Debug, Clone)]
102pub enum ValidityDoubleVote<Candidate, Digest, Signature> {
103 IssuedAndValidity((Candidate, Signature), (Digest, Signature)),
105}
106
107impl<Candidate, Digest, Signature> ValidityDoubleVote<Candidate, Digest, Signature> {
108 pub fn deconstruct<Ctx>(
111 self,
112 ) -> ((Statement<Candidate, Digest>, Signature), (Statement<Candidate, Digest>, Signature))
113 where
114 Ctx: Context<Candidate = Candidate, Digest = Digest, Signature = Signature>,
115 Candidate: Debug + Ord + Eq + Clone,
116 Digest: Debug + Hash + Eq + Clone,
117 Signature: Debug + Eq + Clone,
118 {
119 match self {
120 Self::IssuedAndValidity((c, s1), (d, s2)) =>
121 ((Statement::Seconded(c), s1), (Statement::Valid(d), s2)),
122 }
123 }
124}
125
126#[derive(PartialEq, Eq, Debug, Clone)]
128pub enum DoubleSign<Candidate, Digest, Signature> {
129 Seconded(Candidate, Signature, Signature),
131 Validity(Digest, Signature, Signature),
133}
134
135impl<Candidate, Digest, Signature> DoubleSign<Candidate, Digest, Signature> {
136 pub fn deconstruct(self) -> (Statement<Candidate, Digest>, Signature, Signature) {
139 match self {
140 Self::Seconded(candidate, a, b) => (Statement::Seconded(candidate), a, b),
141 Self::Validity(digest, a, b) => (Statement::Valid(digest), a, b),
142 }
143 }
144}
145
146#[derive(PartialEq, Eq, Debug, Clone)]
148pub struct MultipleCandidates<Candidate, Signature> {
149 pub first: (Candidate, Signature),
151 pub second: (Candidate, Signature),
153}
154
155#[derive(PartialEq, Eq, Debug, Clone)]
157pub struct UnauthorizedStatement<Candidate, Digest, AuthorityId, Signature> {
158 pub statement: SignedStatement<Candidate, Digest, AuthorityId, Signature>,
160}
161
162#[derive(PartialEq, Eq, Debug, Clone)]
165pub enum Misbehavior<Candidate, Digest, AuthorityId, Signature> {
166 ValidityDoubleVote(ValidityDoubleVote<Candidate, Digest, Signature>),
168 MultipleCandidates(MultipleCandidates<Candidate, Signature>),
170 UnauthorizedStatement(UnauthorizedStatement<Candidate, Digest, AuthorityId, Signature>),
172 DoubleSign(DoubleSign<Candidate, Digest, Signature>),
174}
175
176pub type MisbehaviorFor<Ctx> = Misbehavior<
178 <Ctx as Context>::Candidate,
179 <Ctx as Context>::Digest,
180 <Ctx as Context>::AuthorityId,
181 <Ctx as Context>::Signature,
182>;
183
184#[derive(Clone, PartialEq, Eq)]
186enum ValidityVote<Signature: Eq + Clone> {
187 Issued(Signature),
189 Valid(Signature),
191}
192
193#[derive(Clone, PartialEq, Eq, Debug)]
195pub struct Summary<Digest, Group> {
196 pub candidate: Digest,
198 pub group_id: Group,
200 pub validity_votes: usize,
202}
203
204#[derive(Clone, PartialEq, Decode, Encode)]
206pub enum ValidityAttestation<Signature> {
207 Implicit(Signature),
210 Explicit(Signature),
213}
214
215impl Into<PrimitiveValidityAttestation> for ValidityAttestation<ValidatorSignature> {
216 fn into(self) -> PrimitiveValidityAttestation {
217 match self {
218 Self::Implicit(s) => PrimitiveValidityAttestation::Implicit(s),
219 Self::Explicit(s) => PrimitiveValidityAttestation::Explicit(s),
220 }
221 }
222}
223
224#[derive(Clone, PartialEq, Decode, Encode)]
226pub struct AttestedCandidate<Group, Candidate, AuthorityId, Signature> {
227 pub group_id: Group,
229 pub candidate: Candidate,
231 pub validity_votes: Vec<(AuthorityId, ValidityAttestation<Signature>)>,
233}
234
235pub struct CandidateData<Ctx: Context> {
237 group_id: Ctx::GroupId,
238 candidate: Ctx::Candidate,
239 validity_votes: HashMap<Ctx::AuthorityId, ValidityVote<Ctx::Signature>>,
240}
241
242impl<Ctx: Context> CandidateData<Ctx> {
243 pub fn attested(
246 &self,
247 validity_threshold: usize,
248 ) -> Option<AttestedCandidate<Ctx::GroupId, Ctx::Candidate, Ctx::AuthorityId, Ctx::Signature>>
249 {
250 let valid_votes = self.validity_votes.len();
251 if valid_votes < validity_threshold {
252 return None
253 }
254
255 let validity_votes = self
256 .validity_votes
257 .iter()
258 .map(|(a, v)| match *v {
259 ValidityVote::Valid(ref s) => (a.clone(), ValidityAttestation::Explicit(s.clone())),
260 ValidityVote::Issued(ref s) =>
261 (a.clone(), ValidityAttestation::Implicit(s.clone())),
262 })
263 .collect();
264
265 Some(AttestedCandidate {
266 group_id: self.group_id.clone(),
267 candidate: self.candidate.clone(),
268 validity_votes,
269 })
270 }
271
272 fn summary(&self, digest: Ctx::Digest) -> Summary<Ctx::Digest, Ctx::GroupId> {
273 Summary {
274 candidate: digest,
275 group_id: self.group_id.clone(),
276 validity_votes: self.validity_votes.len(),
277 }
278 }
279}
280
281struct AuthorityData<Ctx: Context> {
283 proposals: Vec<(Ctx::Digest, Ctx::Signature)>,
284}
285
286impl<Ctx: Context> Default for AuthorityData<Ctx> {
287 fn default() -> Self {
288 AuthorityData { proposals: Vec::new() }
289 }
290}
291
292pub type ImportResult<Ctx> = Result<
294 Option<Summary<<Ctx as Context>::Digest, <Ctx as Context>::GroupId>>,
295 MisbehaviorFor<Ctx>,
296>;
297
298pub struct Table<Ctx: Context> {
300 authority_data: HashMap<Ctx::AuthorityId, AuthorityData<Ctx>>,
301 detected_misbehavior: HashMap<Ctx::AuthorityId, Vec<MisbehaviorFor<Ctx>>>,
302 candidate_votes: HashMap<Ctx::Digest, CandidateData<Ctx>>,
303 config: Config,
304}
305
306impl<Ctx: Context> Table<Ctx> {
307 pub fn new(config: Config) -> Self {
309 Table {
310 authority_data: HashMap::default(),
311 detected_misbehavior: HashMap::default(),
312 candidate_votes: HashMap::default(),
313 config,
314 }
315 }
316
317 pub fn attested_candidate(
321 &self,
322 digest: &Ctx::Digest,
323 context: &Ctx,
324 minimum_backing_votes: u32,
325 ) -> Option<AttestedCandidate<Ctx::GroupId, Ctx::Candidate, Ctx::AuthorityId, Ctx::Signature>>
326 {
327 self.candidate_votes.get(digest).and_then(|data| {
328 let v_threshold = context.get_group_size(&data.group_id).map_or(usize::MAX, |len| {
329 effective_minimum_backing_votes(len, minimum_backing_votes)
330 });
331 data.attested(v_threshold)
332 })
333 }
334
335 pub fn import_statement(
343 &mut self,
344 context: &Ctx,
345 group_id: Ctx::GroupId,
346 statement: SignedStatement<Ctx::Candidate, Ctx::Digest, Ctx::AuthorityId, Ctx::Signature>,
347 ) -> Option<Summary<Ctx::Digest, Ctx::GroupId>> {
348 let SignedStatement { statement, signature, sender: signer } = statement;
349 let res = match statement {
350 Statement::Seconded(candidate) =>
351 self.import_candidate(context, signer.clone(), candidate, signature, group_id),
352 Statement::Valid(digest) =>
353 self.validity_vote(context, signer.clone(), digest, ValidityVote::Valid(signature)),
354 };
355
356 match res {
357 Ok(maybe_summary) => maybe_summary,
358 Err(misbehavior) => {
359 self.detected_misbehavior.entry(signer).or_default().push(misbehavior);
362 None
363 },
364 }
365 }
366
367 pub fn get_candidate(&self, digest: &Ctx::Digest) -> Option<&Ctx::Candidate> {
369 self.candidate_votes.get(digest).map(|d| &d.candidate)
370 }
371
372 pub fn get_misbehavior(&self) -> &HashMap<Ctx::AuthorityId, Vec<MisbehaviorFor<Ctx>>> {
374 &self.detected_misbehavior
375 }
376
377 pub fn drain_misbehaviors(&mut self) -> DrainMisbehaviors<'_, Ctx> {
381 self.detected_misbehavior.drain().into()
382 }
383
384 fn import_candidate(
385 &mut self,
386 context: &Ctx,
387 authority: Ctx::AuthorityId,
388 candidate: Ctx::Candidate,
389 signature: Ctx::Signature,
390 group: Ctx::GroupId,
391 ) -> ImportResult<Ctx> {
392 if !context.is_member_of(&authority, &group) {
393 gum::debug!(target: LOG_TARGET, authority = ?authority, group = ?group, "New `Misbehavior::UnauthorizedStatement`, candidate backed by validator that doesn't belong to expected group" );
394 return Err(Misbehavior::UnauthorizedStatement(UnauthorizedStatement {
395 statement: SignedStatement {
396 signature,
397 statement: Statement::Seconded(candidate),
398 sender: authority,
399 },
400 }))
401 }
402
403 let digest = Ctx::candidate_digest(&candidate);
405
406 let new_proposal = match self.authority_data.entry(authority.clone()) {
407 Entry::Occupied(mut occ) => {
408 let existing = occ.get_mut();
411
412 if !self.config.allow_multiple_seconded && existing.proposals.len() == 1 {
413 let (old_digest, old_sig) = &existing.proposals[0];
414
415 if old_digest != &digest {
416 const EXISTENCE_PROOF: &str =
417 "when proposal first received from authority, candidate \
418 votes entry is created. proposal here is `Some`, therefore \
419 candidate votes entry exists; qed";
420
421 let old_candidate = self
422 .candidate_votes
423 .get(old_digest)
424 .expect(EXISTENCE_PROOF)
425 .candidate
426 .clone();
427
428 return Err(Misbehavior::MultipleCandidates(MultipleCandidates {
429 first: (old_candidate, old_sig.clone()),
430 second: (candidate, signature.clone()),
431 }))
432 }
433
434 false
435 } else if self.config.allow_multiple_seconded &&
436 existing.proposals.iter().any(|(ref od, _)| od == &digest)
437 {
438 false
439 } else {
440 existing.proposals.push((digest.clone(), signature.clone()));
441 true
442 }
443 },
444 Entry::Vacant(vacant) => {
445 vacant
446 .insert(AuthorityData { proposals: vec![(digest.clone(), signature.clone())] });
447 true
448 },
449 };
450
451 if new_proposal {
454 self.candidate_votes
455 .entry(digest.clone())
456 .or_insert_with(move || CandidateData {
457 group_id: group,
458 candidate,
459 validity_votes: HashMap::new(),
460 });
461 }
462
463 self.validity_vote(context, authority, digest, ValidityVote::Issued(signature))
464 }
465
466 fn validity_vote(
467 &mut self,
468 context: &Ctx,
469 from: Ctx::AuthorityId,
470 digest: Ctx::Digest,
471 vote: ValidityVote<Ctx::Signature>,
472 ) -> ImportResult<Ctx> {
473 let votes = match self.candidate_votes.get_mut(&digest) {
474 None => return Ok(None),
475 Some(votes) => votes,
476 };
477
478 if !context.is_member_of(&from, &votes.group_id) {
480 let sig = match vote {
481 ValidityVote::Valid(s) => s,
482 ValidityVote::Issued(s) => s,
483 };
484
485 return Err(Misbehavior::UnauthorizedStatement(UnauthorizedStatement {
486 statement: SignedStatement {
487 signature: sig,
488 sender: from,
489 statement: Statement::Valid(digest),
490 },
491 }))
492 }
493
494 match votes.validity_votes.entry(from.clone()) {
496 Entry::Occupied(occ) => {
497 let make_vdv = |v| Misbehavior::ValidityDoubleVote(v);
498 let make_ds = |ds| Misbehavior::DoubleSign(ds);
499 return if occ.get() != &vote {
500 Err(match (occ.get().clone(), vote) {
501 (ValidityVote::Issued(iss), ValidityVote::Valid(good)) |
503 (ValidityVote::Valid(good), ValidityVote::Issued(iss)) =>
504 make_vdv(ValidityDoubleVote::IssuedAndValidity(
505 (votes.candidate.clone(), iss),
506 (digest, good),
507 )),
508
509 (ValidityVote::Issued(a), ValidityVote::Issued(b)) =>
511 make_ds(DoubleSign::Seconded(votes.candidate.clone(), a, b)),
512
513 (ValidityVote::Valid(a), ValidityVote::Valid(b)) =>
515 make_ds(DoubleSign::Validity(digest, a, b)),
516 })
517 } else {
518 Ok(None)
519 }
520 },
521 Entry::Vacant(vacant) => {
522 vacant.insert(vote);
523 },
524 }
525
526 Ok(Some(votes.summary(digest)))
527 }
528}
529
530type Drain<'a, Ctx> = hash_map::Drain<'a, <Ctx as Context>::AuthorityId, Vec<MisbehaviorFor<Ctx>>>;
531
532struct MisbehaviorForAuthority<Ctx: Context> {
533 id: Ctx::AuthorityId,
534 misbehaviors: Vec<MisbehaviorFor<Ctx>>,
535}
536
537impl<Ctx: Context> From<(Ctx::AuthorityId, Vec<MisbehaviorFor<Ctx>>)>
538 for MisbehaviorForAuthority<Ctx>
539{
540 fn from((id, mut misbehaviors): (Ctx::AuthorityId, Vec<MisbehaviorFor<Ctx>>)) -> Self {
541 misbehaviors.reverse();
544 Self { id, misbehaviors }
545 }
546}
547
548impl<Ctx: Context> Iterator for MisbehaviorForAuthority<Ctx> {
549 type Item = (Ctx::AuthorityId, MisbehaviorFor<Ctx>);
550
551 fn next(&mut self) -> Option<Self::Item> {
552 self.misbehaviors.pop().map(|misbehavior| (self.id.clone(), misbehavior))
553 }
554}
555
556pub struct DrainMisbehaviors<'a, Ctx: Context> {
557 drain: Drain<'a, Ctx>,
558 in_progress: Option<MisbehaviorForAuthority<Ctx>>,
559}
560
561impl<'a, Ctx: Context> From<Drain<'a, Ctx>> for DrainMisbehaviors<'a, Ctx> {
562 fn from(drain: Drain<'a, Ctx>) -> Self {
563 Self { drain, in_progress: None }
564 }
565}
566
567impl<'a, Ctx: Context> DrainMisbehaviors<'a, Ctx> {
568 fn maybe_item(&mut self) -> Option<(Ctx::AuthorityId, MisbehaviorFor<Ctx>)> {
569 self.in_progress.as_mut().and_then(Iterator::next)
570 }
571}
572
573impl<'a, Ctx: Context> Iterator for DrainMisbehaviors<'a, Ctx> {
574 type Item = (Ctx::AuthorityId, MisbehaviorFor<Ctx>);
575
576 fn next(&mut self) -> Option<Self::Item> {
577 self.maybe_item().or_else(|| {
583 self.in_progress = self.drain.next().map(Into::into);
584 self.maybe_item()
585 })
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592 use std::collections::HashMap;
593
594 fn create_single_seconded<Candidate: Context>() -> Table<Candidate> {
595 Table::new(Config { allow_multiple_seconded: false })
596 }
597
598 fn create_many_seconded<Candidate: Context>() -> Table<Candidate> {
599 Table::new(Config { allow_multiple_seconded: true })
600 }
601
602 #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
603 struct AuthorityId(usize);
604
605 #[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
606 struct GroupId(usize);
607
608 #[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
610 struct Candidate(usize, usize);
611
612 #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
613 struct Signature(usize);
614
615 #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
616 struct Digest(usize);
617
618 #[derive(Debug, PartialEq, Eq)]
619 struct TestContext {
620 authorities: HashMap<AuthorityId, GroupId>,
622 }
623
624 impl Context for TestContext {
625 type AuthorityId = AuthorityId;
626 type Digest = Digest;
627 type Candidate = Candidate;
628 type GroupId = GroupId;
629 type Signature = Signature;
630
631 fn candidate_digest(candidate: &Candidate) -> Digest {
632 Digest(candidate.1)
633 }
634
635 fn is_member_of(&self, authority: &AuthorityId, group: &GroupId) -> bool {
636 self.authorities.get(authority).map(|v| v == group).unwrap_or(false)
637 }
638
639 fn get_group_size(&self, group: &Self::GroupId) -> Option<usize> {
640 let count = self.authorities.values().filter(|g| *g == group).count();
641 if count == 0 {
642 None
643 } else {
644 Some(count)
645 }
646 }
647 }
648
649 #[test]
650 fn submitting_two_candidates_can_be_misbehavior() {
651 let context = TestContext {
652 authorities: {
653 let mut map = HashMap::new();
654 map.insert(AuthorityId(1), GroupId(2));
655 map
656 },
657 };
658
659 let mut table = create_single_seconded();
660 let statement_a = SignedStatement {
661 statement: Statement::Seconded(Candidate(2, 100)),
662 signature: Signature(1),
663 sender: AuthorityId(1),
664 };
665
666 let statement_b = SignedStatement {
667 statement: Statement::Seconded(Candidate(2, 999)),
668 signature: Signature(1),
669 sender: AuthorityId(1),
670 };
671
672 table.import_statement(&context, GroupId(2), statement_a);
673 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
674
675 table.import_statement(&context, GroupId(2), statement_b);
676 assert_eq!(
677 table.detected_misbehavior[&AuthorityId(1)][0],
678 Misbehavior::MultipleCandidates(MultipleCandidates {
679 first: (Candidate(2, 100), Signature(1)),
680 second: (Candidate(2, 999), Signature(1)),
681 })
682 );
683 }
684
685 #[test]
686 fn submitting_two_candidates_can_be_allowed() {
687 let context = TestContext {
688 authorities: {
689 let mut map = HashMap::new();
690 map.insert(AuthorityId(1), GroupId(2));
691 map
692 },
693 };
694
695 let mut table = create_many_seconded();
696 let statement_a = SignedStatement {
697 statement: Statement::Seconded(Candidate(2, 100)),
698 signature: Signature(1),
699 sender: AuthorityId(1),
700 };
701
702 let statement_b = SignedStatement {
703 statement: Statement::Seconded(Candidate(2, 999)),
704 signature: Signature(1),
705 sender: AuthorityId(1),
706 };
707
708 table.import_statement(&context, GroupId(2), statement_a);
709 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
710
711 table.import_statement(&context, GroupId(2), statement_b);
712 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
713 }
714
715 #[test]
716 fn submitting_candidate_from_wrong_group_is_misbehavior() {
717 let context = TestContext {
718 authorities: {
719 let mut map = HashMap::new();
720 map.insert(AuthorityId(1), GroupId(3));
721 map
722 },
723 };
724
725 let mut table = create_single_seconded();
726 let statement = SignedStatement {
727 statement: Statement::Seconded(Candidate(2, 100)),
728 signature: Signature(1),
729 sender: AuthorityId(1),
730 };
731
732 table.import_statement(&context, GroupId(2), statement);
733
734 assert_eq!(
735 table.detected_misbehavior[&AuthorityId(1)][0],
736 Misbehavior::UnauthorizedStatement(UnauthorizedStatement {
737 statement: SignedStatement {
738 statement: Statement::Seconded(Candidate(2, 100)),
739 signature: Signature(1),
740 sender: AuthorityId(1),
741 },
742 })
743 );
744 }
745
746 #[test]
747 fn unauthorized_votes() {
748 let context = TestContext {
749 authorities: {
750 let mut map = HashMap::new();
751 map.insert(AuthorityId(1), GroupId(2));
752 map.insert(AuthorityId(2), GroupId(3));
753 map
754 },
755 };
756
757 let mut table = create_single_seconded();
758
759 let candidate_a = SignedStatement {
760 statement: Statement::Seconded(Candidate(2, 100)),
761 signature: Signature(1),
762 sender: AuthorityId(1),
763 };
764 let candidate_a_digest = Digest(100);
765
766 table.import_statement(&context, GroupId(2), candidate_a);
767 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
768 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2)));
769
770 let bad_validity_vote = SignedStatement {
772 statement: Statement::Valid(candidate_a_digest),
773 signature: Signature(2),
774 sender: AuthorityId(2),
775 };
776 table.import_statement(&context, GroupId(3), bad_validity_vote);
777
778 assert_eq!(
779 table.detected_misbehavior[&AuthorityId(2)][0],
780 Misbehavior::UnauthorizedStatement(UnauthorizedStatement {
781 statement: SignedStatement {
782 statement: Statement::Valid(candidate_a_digest),
783 signature: Signature(2),
784 sender: AuthorityId(2),
785 },
786 })
787 );
788 }
789
790 #[test]
791 fn candidate_double_signature_is_misbehavior() {
792 let context = TestContext {
793 authorities: {
794 let mut map = HashMap::new();
795 map.insert(AuthorityId(1), GroupId(2));
796 map.insert(AuthorityId(2), GroupId(2));
797 map
798 },
799 };
800
801 let mut table = create_single_seconded();
802 let statement = SignedStatement {
803 statement: Statement::Seconded(Candidate(2, 100)),
804 signature: Signature(1),
805 sender: AuthorityId(1),
806 };
807
808 table.import_statement(&context, GroupId(2), statement);
809 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
810
811 let invalid_statement = SignedStatement {
812 statement: Statement::Seconded(Candidate(2, 100)),
813 signature: Signature(999),
814 sender: AuthorityId(1),
815 };
816
817 table.import_statement(&context, GroupId(2), invalid_statement);
818 assert!(table.detected_misbehavior.contains_key(&AuthorityId(1)));
819 }
820
821 #[test]
822 fn issue_and_vote_is_misbehavior() {
823 let context = TestContext {
824 authorities: {
825 let mut map = HashMap::new();
826 map.insert(AuthorityId(1), GroupId(2));
827 map
828 },
829 };
830
831 let mut table = create_single_seconded();
832 let statement = SignedStatement {
833 statement: Statement::Seconded(Candidate(2, 100)),
834 signature: Signature(1),
835 sender: AuthorityId(1),
836 };
837 let candidate_digest = Digest(100);
838
839 table.import_statement(&context, GroupId(2), statement);
840 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
841
842 let extra_vote = SignedStatement {
843 statement: Statement::Valid(candidate_digest),
844 signature: Signature(1),
845 sender: AuthorityId(1),
846 };
847
848 table.import_statement(&context, GroupId(2), extra_vote);
849 assert_eq!(
850 table.detected_misbehavior[&AuthorityId(1)][0],
851 Misbehavior::ValidityDoubleVote(ValidityDoubleVote::IssuedAndValidity(
852 (Candidate(2, 100), Signature(1)),
853 (Digest(100), Signature(1)),
854 ))
855 );
856 }
857
858 #[test]
859 fn candidate_attested_works() {
860 let validity_threshold = 6;
861
862 let mut candidate = CandidateData::<TestContext> {
863 group_id: GroupId(4),
864 candidate: Candidate(4, 12345),
865 validity_votes: HashMap::new(),
866 };
867
868 assert!(candidate.attested(validity_threshold).is_none());
869
870 for i in 0..validity_threshold {
871 candidate
872 .validity_votes
873 .insert(AuthorityId(i + 100), ValidityVote::Valid(Signature(i + 100)));
874 }
875
876 assert!(candidate.attested(validity_threshold).is_some());
877
878 candidate.validity_votes.insert(
879 AuthorityId(validity_threshold + 100),
880 ValidityVote::Valid(Signature(validity_threshold + 100)),
881 );
882
883 assert!(candidate.attested(validity_threshold).is_some());
884 }
885
886 #[test]
887 fn includability_works() {
888 let context = TestContext {
889 authorities: {
890 let mut map = HashMap::new();
891 map.insert(AuthorityId(1), GroupId(2));
892 map.insert(AuthorityId(2), GroupId(2));
893 map.insert(AuthorityId(3), GroupId(2));
894 map
895 },
896 };
897
898 let mut table = create_single_seconded();
900 let statement = SignedStatement {
901 statement: Statement::Seconded(Candidate(2, 100)),
902 signature: Signature(1),
903 sender: AuthorityId(1),
904 };
905 let candidate_digest = Digest(100);
906
907 table.import_statement(&context, GroupId(2), statement);
908
909 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
910 assert!(table.attested_candidate(&candidate_digest, &context, 2).is_none());
911
912 let vote = SignedStatement {
913 statement: Statement::Valid(candidate_digest),
914 signature: Signature(2),
915 sender: AuthorityId(2),
916 };
917
918 table.import_statement(&context, GroupId(2), vote);
919 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2)));
920 assert!(table.attested_candidate(&candidate_digest, &context, 2).is_some());
921 }
922
923 #[test]
924 fn candidate_import_gives_summary() {
925 let context = TestContext {
926 authorities: {
927 let mut map = HashMap::new();
928 map.insert(AuthorityId(1), GroupId(2));
929 map
930 },
931 };
932
933 let mut table = create_single_seconded();
934 let statement = SignedStatement {
935 statement: Statement::Seconded(Candidate(2, 100)),
936 signature: Signature(1),
937 sender: AuthorityId(1),
938 };
939
940 let summary = table
941 .import_statement(&context, GroupId(2), statement)
942 .expect("candidate import to give summary");
943
944 assert_eq!(summary.candidate, Digest(100));
945 assert_eq!(summary.group_id, GroupId(2));
946 assert_eq!(summary.validity_votes, 1);
947 }
948
949 #[test]
950 fn candidate_vote_gives_summary() {
951 let context = TestContext {
952 authorities: {
953 let mut map = HashMap::new();
954 map.insert(AuthorityId(1), GroupId(2));
955 map.insert(AuthorityId(2), GroupId(2));
956 map
957 },
958 };
959
960 let mut table = create_single_seconded();
961 let statement = SignedStatement {
962 statement: Statement::Seconded(Candidate(2, 100)),
963 signature: Signature(1),
964 sender: AuthorityId(1),
965 };
966 let candidate_digest = Digest(100);
967
968 table.import_statement(&context, GroupId(2), statement);
969 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
970
971 let vote = SignedStatement {
972 statement: Statement::Valid(candidate_digest),
973 signature: Signature(2),
974 sender: AuthorityId(2),
975 };
976
977 let summary = table
978 .import_statement(&context, GroupId(2), vote)
979 .expect("candidate vote to give summary");
980
981 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2)));
982
983 assert_eq!(summary.candidate, Digest(100));
984 assert_eq!(summary.group_id, GroupId(2));
985 assert_eq!(summary.validity_votes, 2);
986 }
987}