Skip to main content

hotshot_new_protocol/
vote.rs

1//! Per-view vote collection and certificate formation.
2//!
3//! Votes for views arrive concurrently, and each view is tallied on its own
4//! until it crosses a threshold and forms a certificate. [`VoteCollector`]
5//! owns the machinery common to every kind of vote, inspecting each vote only
6//! through the [`Ballot`] trait. It creates a task per view and routes votes
7//! to the appropriate task, drops duplicate and stale votes, buffers votes
8//! whose epoch is not yet resolved, and GCs decided views. How a view's task
9//! actually combines votes into an output is delegated to a pluggable [`Tally`]
10//! strategy.
11
12mod accumulate;
13
14use std::{
15    any::{type_name, type_name_of_val},
16    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
17    marker::PhantomData,
18    mem,
19    sync::mpsc::{self, Receiver},
20};
21
22pub(crate) use accumulate::CheckedAccumulator;
23use alloy::primitives::U256;
24use hotshot_types::{
25    data::{EpochNumber, ViewNumber},
26    epoch_membership::{EpochMembership, EpochMembershipCoordinator},
27    message::UpgradeLock,
28    simple_certificate::{LightClientStateUpdateCertificateV2, QuorumCertificate2},
29    simple_vote::{HasEpoch, QuorumVote2, SimpleVote, Voteable},
30    traits::{node_implementation::NodeType, signature_key::StakeTableEntryType},
31    vote::{Certificate, HasViewNumber, LightClientStateUpdateVoteAccumulator, Vote},
32};
33use tokio_util::task::JoinMap;
34use tracing::{error, info, warn};
35
36use crate::{cert_verifier::ValidCert, message::Vote1};
37
38/// Information about a vote.
39///
40/// Used by [`VoteCollector`] to handle incoming votes, i.e. reject
41/// duplicate votes by the same signer, or stale votes for GCed views,
42/// before even tallying the vote.
43pub trait Ballot {
44    type Signer;
45
46    fn view(&self) -> ViewNumber;
47    fn epoch(&self) -> Option<EpochNumber>;
48    fn signer(&self) -> Self::Signer;
49}
50
51/// How to count votes to form a certificate.
52pub trait Tally<T: NodeType> {
53    type Vote: Send + 'static;
54    type Output: Send + 'static;
55
56    fn tally(
57        r: Receiver<Self::Vote>,
58        m: EpochMembership<T>,
59        l: UpgradeLock<T>,
60    ) -> Option<Self::Output>;
61}
62
63impl<T: NodeType, D> Ballot for SimpleVote<T, D>
64where
65    D: Voteable<T> + HasEpoch + 'static,
66{
67    type Signer = T::SignatureKey;
68
69    fn view(&self) -> ViewNumber {
70        self.view_number()
71    }
72
73    fn epoch(&self) -> Option<EpochNumber> {
74        HasEpoch::epoch(self)
75    }
76
77    fn signer(&self) -> Self::Signer {
78        self.signing_key()
79    }
80}
81
82impl<T: NodeType> Ballot for Vote1<T> {
83    type Signer = T::SignatureKey;
84
85    fn view(&self) -> ViewNumber {
86        self.vote.view_number()
87    }
88
89    fn epoch(&self) -> Option<EpochNumber> {
90        HasEpoch::epoch(&self.vote)
91    }
92
93    fn signer(&self) -> Self::Signer {
94        self.vote.signing_key()
95    }
96}
97
98/// Accumulates votes into a single [`Certificate`] via a [`CheckedAccumulator`].
99#[allow(clippy::type_complexity)]
100pub struct SimpleTally<T, V, C>(PhantomData<fn() -> (T, V, C)>);
101
102impl<T, V, C> Tally<T> for SimpleTally<T, V, C>
103where
104    T: NodeType,
105    V: Vote<T> + Send + 'static,
106    C: Certificate<T, V::Commitment, Voteable = V::Commitment> + HasEpoch + Send + 'static,
107{
108    type Vote = V;
109    type Output = ValidCert<C>;
110
111    fn tally(r: Receiver<V>, m: EpochMembership<T>, l: UpgradeLock<T>) -> Option<Self::Output> {
112        let mut a = CheckedAccumulator::<T, V, C>::new(m, l);
113        while let Ok(v) = r.recv() {
114            if let Some(c) = a.add(v) {
115                if let Some(e) = c.epoch() {
116                    return Some(ValidCert::new(c, e));
117                } else {
118                    warn!(cert = type_name::<C>(), "certificate has no epoch number");
119                    break;
120                }
121            }
122        }
123        None
124    }
125}
126
127/// The quorum and light-client state certificates formed at an epoch-root view.
128pub type EpochRootCerts<T> = (
129    ValidCert<QuorumCertificate2<T>>,
130    LightClientStateUpdateCertificateV2<T>,
131);
132
133/// Accumulates epoch-root [`Vote1`]s into the (quorum, state) certificate pair.
134pub struct EpochRootTally<T>(PhantomData<fn() -> T>);
135
136impl<T: NodeType> Tally<T> for EpochRootTally<T> {
137    type Vote = Vote1<T>;
138    type Output = EpochRootCerts<T>;
139
140    fn tally(
141        rx: mpsc::Receiver<Vote1<T>>,
142        mem: EpochMembership<T>,
143        lock: UpgradeLock<T>,
144    ) -> Option<Self::Output> {
145        let mut quorum_accu = CheckedAccumulator::<T, QuorumVote2<T>, QuorumCertificate2<T>>::new(
146            mem.clone(),
147            lock.clone(),
148        );
149
150        let mut state_accu = LightClientStateUpdateVoteAccumulator::<T> {
151            vote_outcomes: HashMap::new(),
152            upgrade_lock: lock,
153        };
154
155        let mut quorum_cert = None;
156        let mut state_cert = None;
157
158        while let Ok(vote1) = rx.recv() {
159            let Some(state_vote) = vote1.state_vote else {
160                error!(view = %vote1.vote.view_number(), "epoch-root vote1 without state vote");
161                continue;
162            };
163            let bls_key = vote1.vote.signing_key();
164
165            if quorum_cert.is_none() {
166                quorum_cert = quorum_accu.add(vote1.vote);
167            }
168
169            // Unlike quorum votes, state votes are fully checked, including
170            // their signatures, by the accumulator, so the certificate does
171            // not need to be validated again.
172            if state_cert.is_none() {
173                state_cert = state_accu.accumulate(&bls_key, &state_vote, &mem);
174            }
175
176            if let (Some(q), Some(s)) = (&quorum_cert, &state_cert) {
177                info!(view = %q.view_number(), epoch = %s.epoch, "epoch-root certificates formed");
178                if let Some(e) = q.epoch() {
179                    return Some((ValidCert::new(q.clone(), e), s.clone()));
180                } else {
181                    warn!(
182                        cert = type_name_of_val(q),
183                        "certificate has no epoch number"
184                    );
185                    break;
186                }
187            }
188        }
189
190        None
191    }
192}
193
194/// Collects votes per view and forms certificate(s) using the strategy `S`.
195pub struct VoteCollector<T: NodeType, S: Tally<T>> {
196    /// Tasks collecting votes and forming certificates.
197    accumulators: JoinMap<ViewNumber, Option<S::Output>>,
198
199    /// Where callers submit their votes.
200    ballot_boxes: BTreeMap<ViewNumber, mpsc::Sender<S::Vote>>,
201
202    /// Votes for epochs we have yet to resolve, deduplicated by signer.
203    pending: BTreeMap<ViewNumber, HashMap<T::SignatureKey, S::Vote>>,
204
205    /// Views that had a valid certificate already.
206    completed: BTreeSet<ViewNumber>,
207
208    /// The signers per view.
209    signers: BTreeMap<ViewNumber, HashSet<T::SignatureKey>>,
210
211    /// The GC threshold.
212    lower_bound: ViewNumber,
213
214    membership: EpochMembershipCoordinator<T>,
215
216    upgrade_lock: UpgradeLock<T>,
217}
218
219impl<T: NodeType, S: Tally<T>> VoteCollector<T, S>
220where
221    <S as Tally<T>>::Vote: Ballot<Signer = T::SignatureKey>,
222{
223    pub fn new(mc: EpochMembershipCoordinator<T>, lock: UpgradeLock<T>) -> Self {
224        Self {
225            accumulators: JoinMap::new(),
226            ballot_boxes: BTreeMap::new(),
227            pending: BTreeMap::new(),
228            signers: BTreeMap::new(),
229            completed: BTreeSet::new(),
230            membership: mc,
231            upgrade_lock: lock,
232            lower_bound: ViewNumber::genesis(),
233        }
234    }
235
236    pub async fn next(&mut self) -> Option<S::Output> {
237        loop {
238            match self.accumulators.join_next().await {
239                Some((view, Ok(Some(cert)))) => {
240                    self.ballot_boxes.remove(&view);
241                    if view >= self.lower_bound {
242                        self.completed.insert(view);
243                        return Some(cert);
244                    }
245                },
246                Some((_, Ok(None))) => {},
247                Some((view, Err(err))) => {
248                    if err.is_panic() {
249                        error!(%view, %err, "vote collection task panic");
250                    }
251                    self.ballot_boxes.remove(&view);
252                },
253                None => return None,
254            }
255        }
256    }
257
258    pub fn accumulate_vote(&mut self, vote: S::Vote) {
259        let view = vote.view();
260
261        if view < self.lower_bound || self.completed.contains(&view) {
262            return;
263        }
264
265        let Some(membership) = self.resolve_membership(&vote) else {
266            // A missing epoch can never resolve, so we only buffer
267            // votes whose epoch's stake table will eventually become
268            // available:
269            if vote.epoch().is_some() {
270                self.pending
271                    .entry(view)
272                    .or_default()
273                    .insert(vote.signer(), vote);
274            }
275            return;
276        };
277
278        // Check that we have not received a vote from this signer already.
279        if !self.signers.entry(view).or_default().insert(vote.signer()) {
280            return;
281        }
282
283        if let Some(tx) = self.ballot_boxes.get(&view) {
284            let _ = tx.send(vote);
285            return;
286        }
287
288        let (tx, rx) = mpsc::channel();
289
290        let _ = tx.send(vote);
291        self.ballot_boxes.insert(view, tx);
292
293        let lock = self.upgrade_lock.clone();
294        self.accumulators
295            .spawn_blocking(view, move || S::tally(rx, membership, lock));
296    }
297
298    pub fn retry_pending_votes(&mut self) {
299        for vote in mem::take(&mut self.pending)
300            .into_values()
301            .flat_map(|votes| votes.into_values())
302        {
303            self.accumulate_vote(vote)
304        }
305    }
306
307    pub fn gc(&mut self, view: ViewNumber) {
308        self.ballot_boxes = self.ballot_boxes.split_off(&view);
309        self.completed = self.completed.split_off(&view);
310        self.pending = self.pending.split_off(&view);
311        self.signers = self.signers.split_off(&view);
312        self.lower_bound = view;
313    }
314
315    fn resolve_membership(&mut self, vote: &S::Vote) -> Option<EpochMembership<T>> {
316        let epoch = vote.epoch()?;
317        self.membership.membership_for_epoch(Some(epoch)).ok()
318    }
319}
320
321impl<T, V, C> VoteCollector<T, SimpleTally<T, V, C>>
322where
323    T: NodeType,
324    V: Vote<T> + Send + 'static,
325    C: Certificate<T, V::Commitment, Voteable = V::Commitment> + HasEpoch + Send + 'static,
326{
327    /// Compute the accumulated stake.
328    ///
329    /// This is the sum across unique signers we've routed to the accumulator
330    /// for `view` and the cert threshold in `epoch`. Looks up each signer's
331    /// stake on demand — only intended for rare paths like timeout
332    /// diagnostics. Returns `None` if no votes have been seen for `view` or
333    /// `epoch`'s stake table is unavailable.
334    pub fn stats(&self, view: ViewNumber, epoch: EpochNumber) -> Option<VoteStats> {
335        let signers = self.signers.get(&view)?;
336        if signers.is_empty() {
337            return None;
338        }
339        let membership = self.membership.membership_for_epoch(Some(epoch)).ok()?;
340        let threshold = C::threshold(&membership);
341        let mut stake = U256::ZERO;
342        for signer in signers {
343            if let Some(peer) = C::stake_table_entry(&membership, signer) {
344                stake += peer.stake_table_entry.stake();
345            }
346        }
347        Some(VoteStats { stake, threshold })
348    }
349}
350
351/// Accumulated stake / threshold for a single view.
352///
353/// Used by diagnostics (e.g. timeout logging) to show how close a view came
354/// to forming a cert.
355#[derive(Clone, Copy, Debug)]
356pub struct VoteStats {
357    pub stake: U256,
358    pub threshold: U256,
359}
360
361#[cfg(test)]
362mod tests {
363    use std::{fmt::Debug, time::Duration};
364
365    use committable::Committable;
366    use hotshot::types::BLSPubKey;
367    use hotshot_example_types::node_types::TestTypes;
368    use hotshot_testing::{node_stake::TestNodeStakes, test_builder::gen_node_lists};
369    use hotshot_types::{
370        data::{EpochNumber, ViewNumber},
371        epoch_membership::EpochMembership,
372        simple_vote::{
373            HasEpoch, QuorumData2, QuorumVote2, SimpleVote, VersionedVoteData, Vote2Data,
374        },
375        stake_table::StakeTableEntries,
376        traits::{node_implementation::NodeType, signature_key::SignatureKey},
377        vote::{Certificate, HasViewNumber, Vote},
378    };
379    use tokio::{sync::mpsc, time::timeout};
380
381    use super::{Ballot, SimpleTally, VoteCollector};
382    use crate::{
383        helpers::test_upgrade_lock,
384        message::{Certificate1, Certificate2, Vote2},
385        tests::common::utils::mock_membership,
386    };
387
388    /// Number of test validators.
389    const NUM_NODES: u64 = 10;
390    /// Threshold for SuccessThreshold with 10 nodes of stake 1: (10*2)/3 + 1 = 7.
391    const THRESHOLD: u64 = 7;
392
393    /// How long to wait for expected certificates before failing.
394    const CERT_TIMEOUT: Duration = Duration::from_millis(100);
395    /// How long to wait to confirm no certificate is produced (failure tests).
396    const NO_CERT_TIMEOUT: Duration = Duration::from_millis(500);
397
398    /// Create a signed QuorumVote2 (used for Certificate1 accumulation).
399    fn make_quorum_vote(
400        node_index: u64,
401        view: ViewNumber,
402        epoch: EpochNumber,
403    ) -> QuorumVote2<TestTypes> {
404        let (pub_key, priv_key) = BLSPubKey::generated_from_seed_indexed([0u8; 32], node_index);
405        let data = QuorumData2 {
406            leaf_commit: committable::RawCommitmentBuilder::new("FakeLeaf")
407                .u64(42)
408                .finalize(),
409            epoch: Some(epoch),
410            block_number: Some(1),
411        };
412        SimpleVote::create_signed_vote(data, view, &pub_key, &priv_key, &test_upgrade_lock())
413            .expect("Failed to sign vote")
414    }
415
416    fn vote_2_data() -> Vote2Data<TestTypes> {
417        Vote2Data {
418            leaf_commit: committable::RawCommitmentBuilder::new("FakeLeaf")
419                .u64(42)
420                .finalize(),
421            epoch: EpochNumber::genesis(),
422            block_number: 1,
423        }
424    }
425
426    /// Create a signed Vote2 (used for Certificate2 accumulation).
427    fn make_vote2(node_index: u64, view: ViewNumber) -> Vote2<TestTypes> {
428        let (pub_key, priv_key) = BLSPubKey::generated_from_seed_indexed([0u8; 32], node_index);
429        let data = vote_2_data();
430        SimpleVote::create_signed_vote(data, view, &pub_key, &priv_key, &test_upgrade_lock())
431            .expect("Failed to sign vote")
432    }
433
434    /// Create a Vote2 with an invalid signature (signed by a different key than claimed).
435    fn make_invalid_vote2(node_index: u64, view: ViewNumber) -> Vote2<TestTypes> {
436        let (pub_key, _) = BLSPubKey::generated_from_seed_indexed([0u8; 32], node_index);
437        // Sign with a completely different key
438        let (_, wrong_priv_key) = BLSPubKey::generated_from_seed_indexed([1u8; 32], node_index);
439        let data = vote_2_data();
440        let commit =
441            VersionedVoteData::<TestTypes, _>::new(data.clone(), view, &test_upgrade_lock())
442                .unwrap()
443                .commit();
444        let bad_sig = BLSPubKey::sign(&wrong_priv_key, commit.as_ref()).unwrap();
445        SimpleVote {
446            signature: (pub_key, bad_sig),
447            data,
448            view_number: view,
449        }
450    }
451
452    /// Spawn a VoteCollectionTask and return:
453    /// - vote sender
454    /// - cert notification channel (receives (view, cert) when a certificate is formed)
455    /// - task JoinHandle (abort this to clean up)
456    fn setup_cert1_task() -> VoteCollector<
457        TestTypes,
458        SimpleTally<TestTypes, QuorumVote2<TestTypes>, Certificate1<TestTypes>>,
459    > {
460        setup_task::<QuorumVote2<TestTypes>, Certificate1<TestTypes>>()
461    }
462
463    fn setup_cert2_task()
464    -> VoteCollector<TestTypes, SimpleTally<TestTypes, Vote2<TestTypes>, Certificate2<TestTypes>>>
465    {
466        setup_task::<Vote2<TestTypes>, Certificate2<TestTypes>>()
467    }
468
469    /// Spawn a VoteCollectionTask for Certificate2.
470    fn setup_task<
471        V: Ballot<Signer = <TestTypes as NodeType>::SignatureKey>
472            + Vote<TestTypes>
473            + HasEpoch
474            + Send
475            + Sync
476            + 'static,
477        C: Certificate<TestTypes, V::Commitment, Voteable = V::Commitment>
478            + HasEpoch
479            + Send
480            + Sync
481            + 'static,
482    >() -> VoteCollector<TestTypes, SimpleTally<TestTypes, V, C>> {
483        let membership = mock_membership();
484        VoteCollector::new(membership, test_upgrade_lock())
485    }
486
487    /// Wait for exactly `expected` certificates, then abort the task.
488    async fn _collect_certs<T: std::fmt::Debug>(
489        cert_rx: &mut mpsc::Receiver<T>,
490        expected: usize,
491    ) -> Vec<T> {
492        let mut results = Vec::new();
493        for _ in 0..expected {
494            let cert = tokio::time::timeout(CERT_TIMEOUT, cert_rx.recv())
495                .await
496                .expect("Timed out waiting for certificate")
497                .expect("Cert channel closed unexpectedly");
498            results.push(cert);
499        }
500        results
501    }
502
503    /// Confirm no certificates are produced within the timeout, then abort the task.
504    async fn assert_no_certs<
505        V: Ballot<Signer = <TestTypes as NodeType>::SignatureKey>
506            + Vote<TestTypes>
507            + HasEpoch
508            + Send
509            + Sync
510            + 'static,
511        C: Certificate<TestTypes, V::Commitment, Voteable = V::Commitment>
512            + HasEpoch
513            + Debug
514            + Send
515            + Sync
516            + 'static,
517    >(
518        task: &mut VoteCollector<TestTypes, SimpleTally<TestTypes, V, C>>,
519    ) {
520        let result = tokio::time::timeout(NO_CERT_TIMEOUT, task.next()).await;
521        match result {
522            Err(_) => { /* timeout — good, no cert produced */ },
523            Ok(None) => { /* good, no cert produced */ },
524            Ok(Some(cert)) => panic!("Expected no certificate but got one: {cert:?}"),
525        }
526    }
527
528    /// Verify that a certificate's data commitment matches `expected_data` and that
529    /// the aggregate signature is valid against the stake table.
530    fn verify_cert<C, D>(cert: &C, expected_data: &D, membership: &EpochMembership<TestTypes>)
531    where
532        D: Committable,
533        C: Certificate<TestTypes, D, Voteable = D>,
534    {
535        // Data commitment must match the vote data that produced the cert.
536        assert_eq!(
537            cert.data().commit(),
538            expected_data.commit(),
539            "Certificate data commitment does not match expected vote data"
540        );
541
542        // Aggregate signature must be valid against the stake table.
543        let stake_table = C::stake_table(membership);
544        let stake_table_entries = StakeTableEntries::<TestTypes>::from(stake_table).0;
545        let threshold = C::threshold(membership);
546        cert.is_valid_cert(&stake_table_entries, threshold, &test_upgrade_lock())
547            .expect("Certificate signature validation failed");
548    }
549
550    // ==================== Certificate1 (QuorumVote2) happy path ====================
551
552    /// Sending enough QuorumVote2s for a single view produces a valid Certificate1
553    /// whose data commitment matches the votes.
554    #[tokio::test]
555    async fn test_cert1_single_view_happy_path() {
556        let mut task = setup_cert1_task();
557        let view = ViewNumber::new(1);
558        let epoch = EpochNumber::genesis();
559        let expected_data = QuorumData2 {
560            leaf_commit: committable::RawCommitmentBuilder::new("FakeLeaf")
561                .u64(42)
562                .finalize(),
563            epoch: Some(epoch),
564            block_number: Some(1),
565        };
566
567        for i in 0..THRESHOLD {
568            task.accumulate_vote(make_quorum_vote(i, view, epoch));
569        }
570
571        let cert = timeout(CERT_TIMEOUT, task.next()).await.unwrap().unwrap();
572        assert_eq!(cert.view_number(), view);
573
574        let membership = mock_membership();
575        let epoch_membership = membership.membership_for_epoch(Some(epoch)).unwrap();
576        verify_cert(cert.cert(), &expected_data, &epoch_membership);
577    }
578
579    /// Sending votes for multiple views produces a valid certificate for each view,
580    /// each with data commitment matching the votes.
581    #[tokio::test]
582    async fn test_cert1_multiple_views_parallel() {
583        let mut task = setup_cert1_task();
584        let epoch = EpochNumber::genesis();
585        let expected_data = QuorumData2 {
586            leaf_commit: committable::RawCommitmentBuilder::new("FakeLeaf")
587                .u64(42)
588                .finalize(),
589            epoch: Some(epoch),
590            block_number: Some(1),
591        };
592
593        let views = [ViewNumber::new(1), ViewNumber::new(2), ViewNumber::new(3)];
594
595        // Interleave votes across views
596        for i in 0..THRESHOLD {
597            for &view in &views {
598                task.accumulate_vote(make_quorum_vote(i, view, epoch));
599            }
600        }
601        let mut certs = Vec::new();
602        for _ in 0..views.len() {
603            certs.push(timeout(CERT_TIMEOUT, task.next()).await.unwrap().unwrap());
604        }
605        assert_eq!(
606            certs.len(),
607            views.len(),
608            "Expected one Certificate1 per view"
609        );
610        let mut cert_views: Vec<_> = certs.iter().map(|c| c.view_number()).collect();
611        cert_views.sort();
612        assert_eq!(cert_views, views.to_vec());
613
614        let membership = mock_membership();
615        let epoch_membership = membership.membership_for_epoch(Some(epoch)).unwrap();
616        for cert in &certs {
617            verify_cert(cert.cert(), &expected_data, &epoch_membership);
618        }
619    }
620
621    // ==================== Certificate2 (Vote2) happy path ====================
622
623    /// Sending enough Vote2s for a single view produces a valid Certificate2
624    /// whose data commitment matches the votes.
625    #[tokio::test]
626    async fn test_cert2_single_view_happy_path() {
627        let mut task = setup_cert2_task();
628        let view = ViewNumber::new(1);
629        let epoch = EpochNumber::genesis();
630        let expected_data = vote_2_data();
631
632        for i in 0..THRESHOLD {
633            task.accumulate_vote(make_vote2(i, view));
634        }
635
636        let cert = timeout(CERT_TIMEOUT, task.next()).await.unwrap().unwrap();
637        assert_eq!(cert.view_number(), view);
638
639        let membership = mock_membership();
640        let epoch_membership = membership.membership_for_epoch(Some(epoch)).unwrap();
641        verify_cert(cert.cert(), &expected_data, &epoch_membership);
642    }
643
644    /// Sending votes for multiple views in parallel produces valid certificates for each,
645    /// each with data commitment matching the votes.
646    #[tokio::test]
647    async fn test_cert2_multiple_views_parallel() {
648        let mut task = setup_cert2_task();
649        let epoch = EpochNumber::genesis();
650        let expected_data = vote_2_data();
651
652        let views = [ViewNumber::new(5), ViewNumber::new(6), ViewNumber::new(7)];
653
654        for i in 0..THRESHOLD {
655            for &view in &views {
656                task.accumulate_vote(make_vote2(i, view));
657            }
658        }
659
660        let mut certs = Vec::new();
661        for _ in 0..views.len() {
662            certs.push(timeout(CERT_TIMEOUT, task.next()).await.unwrap().unwrap());
663        }
664        assert_eq!(
665            certs.len(),
666            views.len(),
667            "Expected one Certificate2 per view"
668        );
669        let mut cert_views: Vec<_> = certs.iter().map(|c| c.view_number()).collect();
670        cert_views.sort();
671        assert_eq!(cert_views, views.to_vec());
672
673        let membership = mock_membership();
674        let epoch_membership = membership.membership_for_epoch(Some(epoch)).unwrap();
675        for cert in &certs {
676            verify_cert(cert.cert(), &expected_data, &epoch_membership);
677        }
678    }
679
680    // ==================== Certificate1 failure cases ====================
681
682    /// Fewer than threshold votes do not produce a certificate.
683    #[tokio::test]
684    async fn test_cert1_below_threshold_no_certificate() {
685        let mut task = setup_cert1_task();
686        let view = ViewNumber::new(1);
687        let epoch = EpochNumber::genesis();
688
689        for i in 0..(THRESHOLD - 1) {
690            task.accumulate_vote(make_quorum_vote(i, view, epoch));
691        }
692
693        assert_no_certs(&mut task).await;
694    }
695
696    /// Duplicate votes from the same signer do not count toward threshold.
697    #[tokio::test]
698    async fn test_cert1_duplicate_votes_ignored() {
699        let mut task = setup_cert1_task();
700        let view = ViewNumber::new(1);
701        let epoch = EpochNumber::genesis();
702
703        // Send 6 unique votes (below threshold of 7)
704        for i in 0..6 {
705            task.accumulate_vote(make_quorum_vote(i, view, epoch));
706        }
707        // Send duplicates of node 0 — should not push us over threshold
708        for _ in 0..5 {
709            task.accumulate_vote(make_quorum_vote(0, view, epoch));
710        }
711
712        assert_no_certs(&mut task).await;
713    }
714
715    // ==================== Certificate2 failure cases ====================
716
717    /// Fewer than threshold Vote2s do not produce a Certificate2.
718    #[tokio::test]
719    async fn test_cert2_below_threshold_no_certificate() {
720        let mut task = setup_cert2_task();
721        let view = ViewNumber::new(1);
722
723        for i in 0..(THRESHOLD - 1) {
724            task.accumulate_vote(make_vote2(i, view));
725        }
726
727        assert_no_certs(&mut task).await;
728    }
729
730    /// Duplicate Vote2s from the same signer do not count toward threshold.
731    #[tokio::test]
732    async fn test_cert2_duplicate_votes_ignored() {
733        let mut task = setup_cert2_task();
734        let view = ViewNumber::new(1);
735
736        // Send 6 unique votes (below threshold of 7)
737        for i in 0..6 {
738            task.accumulate_vote(make_vote2(i, view));
739        }
740        // Repeat node 0 votes — should not reach threshold
741        for _ in 0..5 {
742            task.accumulate_vote(make_vote2(0, view));
743        }
744
745        assert_no_certs(&mut task).await;
746    }
747
748    /// Votes with invalid signatures are rejected and do not count.
749    #[tokio::test]
750    async fn test_cert2_invalid_signature_rejected() {
751        let mut task = setup_cert2_task();
752        let view = ViewNumber::new(1);
753
754        // Send 6 valid votes (below threshold)
755        for i in 0..6 {
756            task.accumulate_vote(make_vote2(i, view));
757        }
758        // Send invalid-signature votes — should be rejected, not reaching threshold
759        for i in 6..NUM_NODES {
760            task.accumulate_vote(make_invalid_vote2(i, view));
761        }
762
763        assert_no_certs(&mut task).await;
764    }
765
766    /// Votes with invalid signatures are rejected and do not count.
767    #[tokio::test]
768    async fn test_cert2_invalid_signature_recovery() {
769        let mut task = setup_cert2_task();
770        let view = ViewNumber::new(1);
771        let epoch = EpochNumber::genesis();
772
773        // Send 6 valid votes (below threshold)
774        for i in 0..6 {
775            task.accumulate_vote(make_vote2(i, view));
776        }
777        // Send invalid-signature votes — should be rejected, not reaching threshold
778        for i in 6..8 {
779            task.accumulate_vote(make_invalid_vote2(i, view));
780        }
781        assert_no_certs(&mut task).await;
782
783        task.accumulate_vote(make_vote2(9, view));
784
785        let cert = timeout(CERT_TIMEOUT, task.next()).await.unwrap().unwrap();
786        assert_no_certs(&mut task).await;
787        let membership = mock_membership();
788        let epoch_membership = membership.membership_for_epoch(Some(epoch)).unwrap();
789        verify_cert(cert.cert(), &vote_2_data(), &epoch_membership);
790    }
791
792    /// Channel closed before threshold means no certificate is produced.
793    #[tokio::test]
794    async fn test_cert2_channel_closed_early() {
795        let mut task = setup_cert2_task();
796        let view = ViewNumber::new(1);
797
798        for i in 0..3 {
799            task.accumulate_vote(make_vote2(i, view));
800        }
801        assert_no_certs(&mut task).await;
802    }
803
804    // ==================== Mixed / advanced scenarios ====================
805
806    /// Only the view that reaches threshold gets a certificate; others don't.
807    #[tokio::test]
808    async fn test_cert2_partial_views_only_complete_one_certifies() {
809        let mut task = setup_cert2_task();
810
811        let complete_view = ViewNumber::new(1);
812        let partial_view = ViewNumber::new(2);
813
814        // Send threshold votes for the complete view
815        for i in 0..THRESHOLD {
816            task.accumulate_vote(make_vote2(i, complete_view));
817        }
818
819        // Send fewer than threshold for the partial view
820        for i in 0..3 {
821            task.accumulate_vote(make_vote2(i, partial_view));
822        }
823
824        // Wait for the one expected certificate
825        let cert = timeout(CERT_TIMEOUT, task.next()).await.unwrap().unwrap();
826        assert_no_certs(&mut task).await;
827        assert_eq!(cert.view_number(), complete_view);
828    }
829
830    /// Extra votes beyond threshold for the same view do not produce a second certificate.
831    #[tokio::test]
832    async fn test_cert2_extra_votes_after_threshold_no_duplicate_cert() {
833        let mut task = setup_cert2_task();
834        let view = ViewNumber::new(1);
835
836        // Send all 10 votes (more than threshold of 7)
837        for i in 0..NUM_NODES {
838            task.accumulate_vote(make_vote2(i, view));
839        }
840
841        // Should get exactly one cert, then confirm no more arrive
842        let cert = timeout(CERT_TIMEOUT, task.next()).await.unwrap().unwrap();
843        assert_eq!(cert.view_number(), view);
844
845        // Confirm no second certificate
846        assert_no_certs(&mut task).await;
847    }
848
849    /// Votes for different data commitments on the same view do not combine.
850    #[tokio::test]
851    async fn test_cert2_conflicting_data_same_view_no_certificate() {
852        let mut task = setup_cert2_task();
853        let view = ViewNumber::new(1);
854
855        // Send 6 votes for one leaf commitment
856        for i in 0..6 {
857            task.accumulate_vote(make_vote2(i, view));
858        }
859
860        // Send 4 votes for a different leaf commitment
861        for i in 6..NUM_NODES {
862            let (pub_key, priv_key) = BLSPubKey::generated_from_seed_indexed([0u8; 32], i);
863            let data = Vote2Data {
864                leaf_commit: committable::RawCommitmentBuilder::new("FakeLeaf")
865                    //different leaf commitment
866                    .u64(1000)
867                    .finalize(),
868                epoch: EpochNumber::genesis(),
869                block_number: 1,
870            };
871            let vote = SimpleVote::create_signed_vote(
872                data,
873                view,
874                &pub_key,
875                &priv_key,
876                &test_upgrade_lock(),
877            )
878            .expect("Failed to sign vote");
879            task.accumulate_vote(vote);
880        }
881        assert_no_certs(&mut task).await;
882    }
883
884    /// Collector over a membership where node 9 is removed from the quorum
885    /// committee starting at epoch 3 (9 members of stake 1, threshold 7).
886    fn setup_cert1_task_with_removed_node() -> VoteCollector<
887        TestTypes,
888        SimpleTally<TestTypes, QuorumVote2<TestTypes>, Certificate1<TestTypes>>,
889    > {
890        let membership = mock_membership();
891        let committee = gen_node_lists::<TestTypes>(9, 9, &TestNodeStakes::default()).0;
892        membership
893            .membership()
894            .add_quorum_committee(EpochNumber::new(3), committee);
895        membership
896            .membership()
897            .register_epoch(EpochNumber::new(3), [0u8; 32]);
898        VoteCollector::new(membership, test_upgrade_lock())
899    }
900
901    /// A vote from a validator that was removed from the quorum committee in
902    /// a later epoch does not count toward that epoch's certificate.
903    #[tokio::test]
904    async fn test_vote_from_removed_validator_ignored() {
905        let mut task = setup_cert1_task_with_removed_node();
906        let view = ViewNumber::new(21);
907        let epoch = EpochNumber::new(3);
908
909        for i in 0..6 {
910            task.accumulate_vote(make_quorum_vote(i, view, epoch));
911        }
912        task.accumulate_vote(make_quorum_vote(9, view, epoch));
913        assert_no_certs(&mut task).await;
914
915        task.accumulate_vote(make_quorum_vote(6, view, epoch));
916        let cert = timeout(CERT_TIMEOUT, task.next()).await.unwrap().unwrap();
917        assert_eq!(cert.view_number(), view);
918    }
919
920    /// The same membership still counts node 9's vote in an epoch where it
921    /// is a member: committee resolution is per-epoch.
922    #[tokio::test]
923    async fn test_vote_counts_in_epoch_before_removal() {
924        let mut task = setup_cert1_task_with_removed_node();
925        let view = ViewNumber::new(11);
926        let epoch = EpochNumber::new(2);
927
928        for i in 0..6 {
929            task.accumulate_vote(make_quorum_vote(i, view, epoch));
930        }
931        task.accumulate_vote(make_quorum_vote(9, view, epoch));
932        let cert = timeout(CERT_TIMEOUT, task.next()).await.unwrap().unwrap();
933        assert_eq!(cert.view_number(), view);
934    }
935}