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
65#[derive(PartialEq, Eq, Debug, Clone, Encode, Decode)]
67pub enum Statement<Candidate, Digest> {
68 #[codec(index = 1)]
72 Seconded(Candidate),
73 #[codec(index = 2)]
75 Valid(Digest),
76}
77
78#[derive(PartialEq, Eq, Debug, Clone, Encode, Decode)]
80pub struct SignedStatement<Candidate, Digest, AuthorityId, Signature> {
81 pub statement: Statement<Candidate, Digest>,
83 pub signature: Signature,
85 pub sender: AuthorityId,
87}
88
89#[derive(PartialEq, Eq, Debug, Clone)]
94pub enum ValidityDoubleVote<Candidate, Digest, Signature> {
95 IssuedAndValidity((Candidate, Signature), (Digest, Signature)),
97}
98
99impl<Candidate, Digest, Signature> ValidityDoubleVote<Candidate, Digest, Signature> {
100 pub fn deconstruct<Ctx>(
103 self,
104 ) -> ((Statement<Candidate, Digest>, Signature), (Statement<Candidate, Digest>, Signature))
105 where
106 Ctx: Context<Candidate = Candidate, Digest = Digest, Signature = Signature>,
107 Candidate: Debug + Ord + Eq + Clone,
108 Digest: Debug + Hash + Eq + Clone,
109 Signature: Debug + Eq + Clone,
110 {
111 match self {
112 Self::IssuedAndValidity((c, s1), (d, s2)) => {
113 ((Statement::Seconded(c), s1), (Statement::Valid(d), s2))
114 },
115 }
116 }
117}
118
119#[derive(PartialEq, Eq, Debug, Clone)]
121pub enum DoubleSign<Candidate, Digest, Signature> {
122 Seconded(Candidate, Signature, Signature),
124 Validity(Digest, Signature, Signature),
126}
127
128impl<Candidate, Digest, Signature> DoubleSign<Candidate, Digest, Signature> {
129 pub fn deconstruct(self) -> (Statement<Candidate, Digest>, Signature, Signature) {
132 match self {
133 Self::Seconded(candidate, a, b) => (Statement::Seconded(candidate), a, b),
134 Self::Validity(digest, a, b) => (Statement::Valid(digest), a, b),
135 }
136 }
137}
138
139#[derive(PartialEq, Eq, Debug, Clone)]
141pub struct UnauthorizedStatement<Candidate, Digest, AuthorityId, Signature> {
142 pub statement: SignedStatement<Candidate, Digest, AuthorityId, Signature>,
144}
145
146#[derive(PartialEq, Eq, Debug, Clone)]
149pub enum Misbehavior<Candidate, Digest, AuthorityId, Signature> {
150 ValidityDoubleVote(ValidityDoubleVote<Candidate, Digest, Signature>),
152 UnauthorizedStatement(UnauthorizedStatement<Candidate, Digest, AuthorityId, Signature>),
154 DoubleSign(DoubleSign<Candidate, Digest, Signature>),
156}
157
158pub type MisbehaviorFor<Ctx> = Misbehavior<
160 <Ctx as Context>::Candidate,
161 <Ctx as Context>::Digest,
162 <Ctx as Context>::AuthorityId,
163 <Ctx as Context>::Signature,
164>;
165
166#[derive(Clone, PartialEq, Eq)]
168enum ValidityVote<Signature: Eq + Clone> {
169 Issued(Signature),
171 Valid(Signature),
173}
174
175#[derive(Clone, PartialEq, Eq, Debug)]
177pub struct Summary<Digest, Group> {
178 pub candidate: Digest,
180 pub group_id: Group,
182 pub validity_votes: usize,
184}
185
186#[derive(Clone, PartialEq, Decode, Encode)]
188pub enum ValidityAttestation<Signature> {
189 Implicit(Signature),
192 Explicit(Signature),
195}
196
197impl Into<PrimitiveValidityAttestation> for ValidityAttestation<ValidatorSignature> {
198 fn into(self) -> PrimitiveValidityAttestation {
199 match self {
200 Self::Implicit(s) => PrimitiveValidityAttestation::Implicit(s),
201 Self::Explicit(s) => PrimitiveValidityAttestation::Explicit(s),
202 }
203 }
204}
205
206#[derive(Clone, PartialEq, Decode, Encode)]
208pub struct AttestedCandidate<Group, Candidate, AuthorityId, Signature> {
209 pub group_id: Group,
211 pub candidate: Candidate,
213 pub validity_votes: Vec<(AuthorityId, ValidityAttestation<Signature>)>,
215}
216
217pub struct CandidateData<Ctx: Context> {
219 group_id: Ctx::GroupId,
220 candidate: Ctx::Candidate,
221 validity_votes: HashMap<Ctx::AuthorityId, ValidityVote<Ctx::Signature>>,
222}
223
224impl<Ctx: Context> CandidateData<Ctx> {
225 pub fn attested(
228 &self,
229 validity_threshold: usize,
230 ) -> Option<AttestedCandidate<Ctx::GroupId, Ctx::Candidate, Ctx::AuthorityId, Ctx::Signature>>
231 {
232 let valid_votes = self.validity_votes.len();
233 if valid_votes < validity_threshold {
234 return None;
235 }
236
237 let validity_votes = self
238 .validity_votes
239 .iter()
240 .map(|(a, v)| match *v {
241 ValidityVote::Valid(ref s) => (a.clone(), ValidityAttestation::Explicit(s.clone())),
242 ValidityVote::Issued(ref s) => {
243 (a.clone(), ValidityAttestation::Implicit(s.clone()))
244 },
245 })
246 .collect();
247
248 Some(AttestedCandidate {
249 group_id: self.group_id.clone(),
250 candidate: self.candidate.clone(),
251 validity_votes,
252 })
253 }
254
255 fn summary(&self, digest: Ctx::Digest) -> Summary<Ctx::Digest, Ctx::GroupId> {
256 Summary {
257 candidate: digest,
258 group_id: self.group_id.clone(),
259 validity_votes: self.validity_votes.len(),
260 }
261 }
262}
263
264struct AuthorityData<Ctx: Context> {
266 proposals: Vec<(Ctx::Digest, Ctx::Signature)>,
267}
268
269impl<Ctx: Context> Default for AuthorityData<Ctx> {
270 fn default() -> Self {
271 AuthorityData { proposals: Vec::new() }
272 }
273}
274
275pub type ImportResult<Ctx> = Result<
277 Option<Summary<<Ctx as Context>::Digest, <Ctx as Context>::GroupId>>,
278 MisbehaviorFor<Ctx>,
279>;
280
281pub struct Table<Ctx: Context> {
283 authority_data: HashMap<Ctx::AuthorityId, AuthorityData<Ctx>>,
284 detected_misbehavior: HashMap<Ctx::AuthorityId, Vec<MisbehaviorFor<Ctx>>>,
285 candidate_votes: HashMap<Ctx::Digest, CandidateData<Ctx>>,
286}
287
288impl<Ctx: Context> Table<Ctx> {
289 pub fn new() -> Self {
290 Table {
291 authority_data: HashMap::default(),
292 detected_misbehavior: HashMap::default(),
293 candidate_votes: HashMap::default(),
294 }
295 }
296
297 pub fn attested_candidate(
301 &self,
302 digest: &Ctx::Digest,
303 context: &Ctx,
304 minimum_backing_votes: u32,
305 ) -> Option<AttestedCandidate<Ctx::GroupId, Ctx::Candidate, Ctx::AuthorityId, Ctx::Signature>>
306 {
307 self.candidate_votes.get(digest).and_then(|data| {
308 let v_threshold = context.get_group_size(&data.group_id).map_or(usize::MAX, |len| {
309 effective_minimum_backing_votes(len, minimum_backing_votes)
310 });
311 data.attested(v_threshold)
312 })
313 }
314
315 pub fn import_statement(
323 &mut self,
324 context: &Ctx,
325 group_id: Ctx::GroupId,
326 statement: SignedStatement<Ctx::Candidate, Ctx::Digest, Ctx::AuthorityId, Ctx::Signature>,
327 ) -> Option<Summary<Ctx::Digest, Ctx::GroupId>> {
328 let SignedStatement { statement, signature, sender: signer } = statement;
329 let res = match statement {
330 Statement::Seconded(candidate) => {
331 self.import_candidate(context, signer.clone(), candidate, signature, group_id)
332 },
333 Statement::Valid(digest) => {
334 self.validity_vote(context, signer.clone(), digest, ValidityVote::Valid(signature))
335 },
336 };
337
338 match res {
339 Ok(maybe_summary) => maybe_summary,
340 Err(misbehavior) => {
341 self.detected_misbehavior.entry(signer).or_default().push(misbehavior);
344 None
345 },
346 }
347 }
348
349 pub fn get_candidate(&self, digest: &Ctx::Digest) -> Option<&Ctx::Candidate> {
351 self.candidate_votes.get(digest).map(|d| &d.candidate)
352 }
353
354 pub fn get_misbehavior(&self) -> &HashMap<Ctx::AuthorityId, Vec<MisbehaviorFor<Ctx>>> {
356 &self.detected_misbehavior
357 }
358
359 pub fn drain_misbehaviors(&mut self) -> DrainMisbehaviors<'_, Ctx> {
363 self.detected_misbehavior.drain().into()
364 }
365
366 fn import_candidate(
367 &mut self,
368 context: &Ctx,
369 authority: Ctx::AuthorityId,
370 candidate: Ctx::Candidate,
371 signature: Ctx::Signature,
372 group: Ctx::GroupId,
373 ) -> ImportResult<Ctx> {
374 if !context.is_member_of(&authority, &group) {
375 gum::debug!(target: LOG_TARGET, authority = ?authority, group = ?group, "New `Misbehavior::UnauthorizedStatement`, candidate backed by validator that doesn't belong to expected group" );
376 return Err(Misbehavior::UnauthorizedStatement(UnauthorizedStatement {
377 statement: SignedStatement {
378 signature,
379 statement: Statement::Seconded(candidate),
380 sender: authority,
381 },
382 }));
383 }
384
385 let digest = Ctx::candidate_digest(&candidate);
387
388 let new_proposal = match self.authority_data.entry(authority.clone()) {
389 Entry::Occupied(mut occ) => {
390 let existing = occ.get_mut();
393 if existing.proposals.iter().any(|(ref od, _)| od == &digest) {
394 false
395 } else {
396 existing.proposals.push((digest.clone(), signature.clone()));
397 true
398 }
399 },
400 Entry::Vacant(vacant) => {
401 vacant
402 .insert(AuthorityData { proposals: vec![(digest.clone(), signature.clone())] });
403 true
404 },
405 };
406
407 if new_proposal {
410 self.candidate_votes
411 .entry(digest.clone())
412 .or_insert_with(move || CandidateData {
413 group_id: group,
414 candidate,
415 validity_votes: HashMap::new(),
416 });
417 }
418
419 self.validity_vote(context, authority, digest, ValidityVote::Issued(signature))
420 }
421
422 fn validity_vote(
423 &mut self,
424 context: &Ctx,
425 from: Ctx::AuthorityId,
426 digest: Ctx::Digest,
427 vote: ValidityVote<Ctx::Signature>,
428 ) -> ImportResult<Ctx> {
429 let votes = match self.candidate_votes.get_mut(&digest) {
430 None => return Ok(None),
431 Some(votes) => votes,
432 };
433
434 if !context.is_member_of(&from, &votes.group_id) {
436 let sig = match vote {
437 ValidityVote::Valid(s) => s,
438 ValidityVote::Issued(s) => s,
439 };
440
441 return Err(Misbehavior::UnauthorizedStatement(UnauthorizedStatement {
442 statement: SignedStatement {
443 signature: sig,
444 sender: from,
445 statement: Statement::Valid(digest),
446 },
447 }));
448 }
449
450 match votes.validity_votes.entry(from.clone()) {
452 Entry::Occupied(occ) => {
453 let make_vdv = |v| Misbehavior::ValidityDoubleVote(v);
454 let make_ds = |ds| Misbehavior::DoubleSign(ds);
455 return if occ.get() != &vote {
456 Err(match (occ.get().clone(), vote) {
457 (ValidityVote::Issued(iss), ValidityVote::Valid(good)) |
459 (ValidityVote::Valid(good), ValidityVote::Issued(iss)) => {
460 make_vdv(ValidityDoubleVote::IssuedAndValidity(
461 (votes.candidate.clone(), iss),
462 (digest, good),
463 ))
464 },
465
466 (ValidityVote::Issued(a), ValidityVote::Issued(b)) => {
468 make_ds(DoubleSign::Seconded(votes.candidate.clone(), a, b))
469 },
470
471 (ValidityVote::Valid(a), ValidityVote::Valid(b)) => {
473 make_ds(DoubleSign::Validity(digest, a, b))
474 },
475 })
476 } else {
477 Ok(None)
478 };
479 },
480 Entry::Vacant(vacant) => {
481 vacant.insert(vote);
482 },
483 }
484
485 Ok(Some(votes.summary(digest)))
486 }
487}
488
489type Drain<'a, Ctx> = hash_map::Drain<'a, <Ctx as Context>::AuthorityId, Vec<MisbehaviorFor<Ctx>>>;
490
491struct MisbehaviorForAuthority<Ctx: Context> {
492 id: Ctx::AuthorityId,
493 misbehaviors: Vec<MisbehaviorFor<Ctx>>,
494}
495
496impl<Ctx: Context> From<(Ctx::AuthorityId, Vec<MisbehaviorFor<Ctx>>)>
497 for MisbehaviorForAuthority<Ctx>
498{
499 fn from((id, mut misbehaviors): (Ctx::AuthorityId, Vec<MisbehaviorFor<Ctx>>)) -> Self {
500 misbehaviors.reverse();
503 Self { id, misbehaviors }
504 }
505}
506
507impl<Ctx: Context> Iterator for MisbehaviorForAuthority<Ctx> {
508 type Item = (Ctx::AuthorityId, MisbehaviorFor<Ctx>);
509
510 fn next(&mut self) -> Option<Self::Item> {
511 self.misbehaviors.pop().map(|misbehavior| (self.id.clone(), misbehavior))
512 }
513}
514
515pub struct DrainMisbehaviors<'a, Ctx: Context> {
516 drain: Drain<'a, Ctx>,
517 in_progress: Option<MisbehaviorForAuthority<Ctx>>,
518}
519
520impl<'a, Ctx: Context> From<Drain<'a, Ctx>> for DrainMisbehaviors<'a, Ctx> {
521 fn from(drain: Drain<'a, Ctx>) -> Self {
522 Self { drain, in_progress: None }
523 }
524}
525
526impl<'a, Ctx: Context> DrainMisbehaviors<'a, Ctx> {
527 fn maybe_item(&mut self) -> Option<(Ctx::AuthorityId, MisbehaviorFor<Ctx>)> {
528 self.in_progress.as_mut().and_then(Iterator::next)
529 }
530}
531
532impl<'a, Ctx: Context> Iterator for DrainMisbehaviors<'a, Ctx> {
533 type Item = (Ctx::AuthorityId, MisbehaviorFor<Ctx>);
534
535 fn next(&mut self) -> Option<Self::Item> {
536 self.maybe_item().or_else(|| {
542 self.in_progress = self.drain.next().map(Into::into);
543 self.maybe_item()
544 })
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551 use std::collections::HashMap;
552
553 #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
554 struct AuthorityId(usize);
555
556 #[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
557 struct GroupId(usize);
558
559 #[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
561 struct Candidate(usize, usize);
562
563 #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
564 struct Signature(usize);
565
566 #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
567 struct Digest(usize);
568
569 #[derive(Debug, PartialEq, Eq)]
570 struct TestContext {
571 authorities: HashMap<AuthorityId, GroupId>,
573 }
574
575 impl Context for TestContext {
576 type AuthorityId = AuthorityId;
577 type Digest = Digest;
578 type Candidate = Candidate;
579 type GroupId = GroupId;
580 type Signature = Signature;
581
582 fn candidate_digest(candidate: &Candidate) -> Digest {
583 Digest(candidate.1)
584 }
585
586 fn is_member_of(&self, authority: &AuthorityId, group: &GroupId) -> bool {
587 self.authorities.get(authority).map(|v| v == group).unwrap_or(false)
588 }
589
590 fn get_group_size(&self, group: &Self::GroupId) -> Option<usize> {
591 let count = self.authorities.values().filter(|g| *g == group).count();
592 if count == 0 {
593 None
594 } else {
595 Some(count)
596 }
597 }
598 }
599
600 #[test]
601 fn submitting_two_candidates_can_be_allowed() {
602 let context = TestContext {
603 authorities: {
604 let mut map = HashMap::new();
605 map.insert(AuthorityId(1), GroupId(2));
606 map
607 },
608 };
609
610 let mut table = Table::new();
611 let statement_a = SignedStatement {
612 statement: Statement::Seconded(Candidate(2, 100)),
613 signature: Signature(1),
614 sender: AuthorityId(1),
615 };
616
617 let statement_b = SignedStatement {
618 statement: Statement::Seconded(Candidate(2, 999)),
619 signature: Signature(1),
620 sender: AuthorityId(1),
621 };
622
623 table.import_statement(&context, GroupId(2), statement_a);
624 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
625
626 table.import_statement(&context, GroupId(2), statement_b);
627 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
628 }
629
630 #[test]
631 fn submitting_candidate_from_wrong_group_is_misbehavior() {
632 let context = TestContext {
633 authorities: {
634 let mut map = HashMap::new();
635 map.insert(AuthorityId(1), GroupId(3));
636 map
637 },
638 };
639
640 let mut table = Table::new();
641 let statement = SignedStatement {
642 statement: Statement::Seconded(Candidate(2, 100)),
643 signature: Signature(1),
644 sender: AuthorityId(1),
645 };
646
647 table.import_statement(&context, GroupId(2), statement);
648
649 assert_eq!(
650 table.detected_misbehavior[&AuthorityId(1)][0],
651 Misbehavior::UnauthorizedStatement(UnauthorizedStatement {
652 statement: SignedStatement {
653 statement: Statement::Seconded(Candidate(2, 100)),
654 signature: Signature(1),
655 sender: AuthorityId(1),
656 },
657 })
658 );
659 }
660
661 #[test]
662 fn unauthorized_votes() {
663 let context = TestContext {
664 authorities: {
665 let mut map = HashMap::new();
666 map.insert(AuthorityId(1), GroupId(2));
667 map.insert(AuthorityId(2), GroupId(3));
668 map
669 },
670 };
671
672 let mut table = Table::new();
673
674 let candidate_a = SignedStatement {
675 statement: Statement::Seconded(Candidate(2, 100)),
676 signature: Signature(1),
677 sender: AuthorityId(1),
678 };
679 let candidate_a_digest = Digest(100);
680
681 table.import_statement(&context, GroupId(2), candidate_a);
682 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
683 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2)));
684
685 let bad_validity_vote = SignedStatement {
687 statement: Statement::Valid(candidate_a_digest),
688 signature: Signature(2),
689 sender: AuthorityId(2),
690 };
691 table.import_statement(&context, GroupId(3), bad_validity_vote);
692
693 assert_eq!(
694 table.detected_misbehavior[&AuthorityId(2)][0],
695 Misbehavior::UnauthorizedStatement(UnauthorizedStatement {
696 statement: SignedStatement {
697 statement: Statement::Valid(candidate_a_digest),
698 signature: Signature(2),
699 sender: AuthorityId(2),
700 },
701 })
702 );
703 }
704
705 #[test]
706 fn candidate_double_signature_is_misbehavior() {
707 let context = TestContext {
708 authorities: {
709 let mut map = HashMap::new();
710 map.insert(AuthorityId(1), GroupId(2));
711 map.insert(AuthorityId(2), GroupId(2));
712 map
713 },
714 };
715
716 let mut table = Table::new();
717 let statement = SignedStatement {
718 statement: Statement::Seconded(Candidate(2, 100)),
719 signature: Signature(1),
720 sender: AuthorityId(1),
721 };
722
723 table.import_statement(&context, GroupId(2), statement);
724 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
725
726 let invalid_statement = SignedStatement {
727 statement: Statement::Seconded(Candidate(2, 100)),
728 signature: Signature(999),
729 sender: AuthorityId(1),
730 };
731
732 table.import_statement(&context, GroupId(2), invalid_statement);
733 assert!(table.detected_misbehavior.contains_key(&AuthorityId(1)));
734 }
735
736 #[test]
737 fn issue_and_vote_is_misbehavior() {
738 let context = TestContext {
739 authorities: {
740 let mut map = HashMap::new();
741 map.insert(AuthorityId(1), GroupId(2));
742 map
743 },
744 };
745
746 let mut table = Table::new();
747 let statement = SignedStatement {
748 statement: Statement::Seconded(Candidate(2, 100)),
749 signature: Signature(1),
750 sender: AuthorityId(1),
751 };
752 let candidate_digest = Digest(100);
753
754 table.import_statement(&context, GroupId(2), statement);
755 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
756
757 let extra_vote = SignedStatement {
758 statement: Statement::Valid(candidate_digest),
759 signature: Signature(1),
760 sender: AuthorityId(1),
761 };
762
763 table.import_statement(&context, GroupId(2), extra_vote);
764 assert_eq!(
765 table.detected_misbehavior[&AuthorityId(1)][0],
766 Misbehavior::ValidityDoubleVote(ValidityDoubleVote::IssuedAndValidity(
767 (Candidate(2, 100), Signature(1)),
768 (Digest(100), Signature(1)),
769 ))
770 );
771 }
772
773 #[test]
774 fn candidate_attested_works() {
775 let validity_threshold = 6;
776
777 let mut candidate = CandidateData::<TestContext> {
778 group_id: GroupId(4),
779 candidate: Candidate(4, 12345),
780 validity_votes: HashMap::new(),
781 };
782
783 assert!(candidate.attested(validity_threshold).is_none());
784
785 for i in 0..validity_threshold {
786 candidate
787 .validity_votes
788 .insert(AuthorityId(i + 100), ValidityVote::Valid(Signature(i + 100)));
789 }
790
791 assert!(candidate.attested(validity_threshold).is_some());
792
793 candidate.validity_votes.insert(
794 AuthorityId(validity_threshold + 100),
795 ValidityVote::Valid(Signature(validity_threshold + 100)),
796 );
797
798 assert!(candidate.attested(validity_threshold).is_some());
799 }
800
801 #[test]
802 fn includability_works() {
803 let context = TestContext {
804 authorities: {
805 let mut map = HashMap::new();
806 map.insert(AuthorityId(1), GroupId(2));
807 map.insert(AuthorityId(2), GroupId(2));
808 map.insert(AuthorityId(3), GroupId(2));
809 map
810 },
811 };
812
813 let mut table = Table::new();
815 let statement = SignedStatement {
816 statement: Statement::Seconded(Candidate(2, 100)),
817 signature: Signature(1),
818 sender: AuthorityId(1),
819 };
820 let candidate_digest = Digest(100);
821
822 table.import_statement(&context, GroupId(2), statement);
823
824 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
825 assert!(table.attested_candidate(&candidate_digest, &context, 2).is_none());
826
827 let vote = SignedStatement {
828 statement: Statement::Valid(candidate_digest),
829 signature: Signature(2),
830 sender: AuthorityId(2),
831 };
832
833 table.import_statement(&context, GroupId(2), vote);
834 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2)));
835 assert!(table.attested_candidate(&candidate_digest, &context, 2).is_some());
836 }
837
838 #[test]
839 fn candidate_import_gives_summary() {
840 let context = TestContext {
841 authorities: {
842 let mut map = HashMap::new();
843 map.insert(AuthorityId(1), GroupId(2));
844 map
845 },
846 };
847
848 let mut table = Table::new();
849 let statement = SignedStatement {
850 statement: Statement::Seconded(Candidate(2, 100)),
851 signature: Signature(1),
852 sender: AuthorityId(1),
853 };
854
855 let summary = table
856 .import_statement(&context, GroupId(2), statement)
857 .expect("candidate import to give summary");
858
859 assert_eq!(summary.candidate, Digest(100));
860 assert_eq!(summary.group_id, GroupId(2));
861 assert_eq!(summary.validity_votes, 1);
862 }
863
864 #[test]
865 fn candidate_vote_gives_summary() {
866 let context = TestContext {
867 authorities: {
868 let mut map = HashMap::new();
869 map.insert(AuthorityId(1), GroupId(2));
870 map.insert(AuthorityId(2), GroupId(2));
871 map
872 },
873 };
874
875 let mut table = Table::new();
876 let statement = SignedStatement {
877 statement: Statement::Seconded(Candidate(2, 100)),
878 signature: Signature(1),
879 sender: AuthorityId(1),
880 };
881 let candidate_digest = Digest(100);
882
883 table.import_statement(&context, GroupId(2), statement);
884 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
885
886 let vote = SignedStatement {
887 statement: Statement::Valid(candidate_digest),
888 signature: Signature(2),
889 sender: AuthorityId(2),
890 };
891
892 let summary = table
893 .import_statement(&context, GroupId(2), vote)
894 .expect("candidate vote to give summary");
895
896 assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2)));
897
898 assert_eq!(summary.candidate, Digest(100));
899 assert_eq!(summary.group_id, GroupId(2));
900 assert_eq!(summary.validity_votes, 2);
901 }
902}