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