Skip to main content

hotshot_new_protocol/
cert_verifier.rs

1use std::{
2    any::type_name,
3    collections::{BTreeMap, BTreeSet, HashMap},
4    fmt::Display,
5    hash::Hash,
6    mem,
7    ops::Deref,
8};
9
10use alloy::primitives::U256;
11use hotshot_types::{
12    data::{EpochNumber, ViewNumber},
13    epoch_membership::EpochMembershipCoordinator,
14    message::UpgradeLock,
15    simple_certificate::{
16        Certificate1, Certificate2, SimpleCertificate, Threshold, TimeoutCertificate2,
17        TimeoutCertificate3,
18    },
19    simple_vote::{
20        HasEpoch, LockView, QuorumData2, TimeoutData2, TimeoutLock, Vote2Data, Voteable,
21    },
22    stake_table::StakeTableEntries,
23    traits::{node_implementation::NodeType, signature_key::SignatureKey},
24    vote::{Certificate, HasViewNumber},
25};
26use hotshot_utils::anytrace::{Result, Wrap, bail, ensure};
27use tokio_util::task::JoinMap;
28use tracing::{error, warn};
29
30use crate::{
31    helpers::ViewEpoch,
32    message::{EpochChangeMessage, ReVoteMessage, Unchecked, Validated},
33};
34
35#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub struct ValidCert<C> {
37    cert: C,
38    epoch: EpochNumber,
39}
40
41impl<C> ValidCert<C> {
42    pub(crate) fn new(cert: C, epoch: EpochNumber) -> Self {
43        Self { cert, epoch }
44    }
45
46    pub fn cert(&self) -> &C {
47        &self.cert
48    }
49
50    pub fn epoch(&self) -> EpochNumber {
51        self.epoch
52    }
53
54    pub fn into_cert(self) -> C {
55        self.cert
56    }
57
58    pub fn map<D, F>(self, f: F) -> ValidCert<D>
59    where
60        F: FnOnce(C) -> D,
61    {
62        ValidCert {
63            cert: f(self.cert),
64            epoch: self.epoch,
65        }
66    }
67}
68
69impl<C> Deref for ValidCert<C> {
70    type Target = C;
71
72    fn deref(&self) -> &Self::Target {
73        self.cert()
74    }
75}
76
77impl<C: HasViewNumber> HasViewNumber for ValidCert<C> {
78    fn view_number(&self) -> ViewNumber {
79        self.cert.view_number()
80    }
81}
82
83pub trait Verifiable<T: NodeType>: HasViewNumber + HasEpoch + Sized {
84    /// Identifies the `Verifiable`, e.g. `ViewNumber` or `EpochNumber`.
85    type Key: Copy + Ord + Hash + Display + Send + Sync + 'static;
86
87    type Output: Send + 'static;
88
89    fn key(&self) -> Option<Self::Key>;
90
91    /// The epochs whose committees [`Self::check`] reads.
92    ///
93    /// The item waits until every one of them is known.
94    fn epochs(&self) -> impl Iterator<Item = EpochNumber> {
95        self.epoch().into_iter()
96    }
97
98    fn check(
99        self,
100        stake_table: &[<T::SignatureKey as SignatureKey>::StakeTableEntry],
101        threshold: U256,
102        epoch_height: u64,
103        upgrade_lock: &UpgradeLock<T>,
104        memberships: &EpochMembershipCoordinator<T>,
105    ) -> Result<Self::Output>;
106}
107
108impl<T, D, V> Verifiable<T> for SimpleCertificate<T, D, V>
109where
110    T: NodeType,
111    D: Voteable<T> + HasEpoch + Meta + 'static,
112    V: Threshold<T>,
113    Self: Certificate<T, D> + Send + 'static,
114{
115    type Key = ViewEpoch;
116    type Output = Self;
117
118    fn key(&self) -> Option<ViewEpoch> {
119        Some(ViewEpoch(self.view_number(), self.epoch()?))
120    }
121
122    fn check(
123        self,
124        stake_table: &[<T::SignatureKey as SignatureKey>::StakeTableEntry],
125        threshold: U256,
126        epoch_height: u64,
127        upgrade_lock: &UpgradeLock<T>,
128        _: &EpochMembershipCoordinator<T>,
129    ) -> Result<Self> {
130        ensure! {
131            self.data.consistent_with(self.view_number(), epoch_height),
132            "certificate data is inconsistent"
133        }
134        if self.view_number() == ViewNumber::genesis() && D::UNSIGNED_AT_GENESIS_ALLOWED {
135            return Ok(self);
136        }
137        verify_signatures(&self, stake_table, threshold, upgrade_lock)?;
138        Ok(self)
139    }
140}
141
142/// Meta information about vote data.
143trait Meta {
144    const UNSIGNED_AT_GENESIS_ALLOWED: bool = false;
145
146    /// Whether the data is consistent, internally and with the certificate's view.
147    ///
148    /// Checked in `check`, so a certificate failing it is never recorded as
149    /// verified and cannot retire its key for other senders. It runs before the
150    /// genesis exemption, which therefore cannot skip it.
151    fn consistent_with(&self, view: ViewNumber, epoch_height: u64) -> bool;
152}
153
154impl<T: NodeType> Meta for QuorumData2<T> {
155    // A `Cert1` at the genesis view may be unsigned, because the genesis QC is.
156    // Every other certificate is formed from signed votes, at the genesis view
157    // as at any other, so none of them gets this exception.
158    const UNSIGNED_AT_GENESIS_ALLOWED: bool = true;
159
160    fn consistent_with(&self, _: ViewNumber, epoch_height: u64) -> bool {
161        self.is_well_formed(epoch_height)
162    }
163}
164
165impl<T: NodeType> Meta for Vote2Data<T> {
166    fn consistent_with(&self, _: ViewNumber, epoch_height: u64) -> bool {
167        self.is_well_formed(epoch_height)
168    }
169}
170
171impl Meta for TimeoutData2 {
172    fn consistent_with(&self, view: ViewNumber, _: u64) -> bool {
173        self.view == view
174    }
175}
176
177/// Verify a certificate's signatures, at the genesis view as at any other.
178///
179/// TODO: Almost a duplicate of `SimpleCertificate::is_valid_cert`.
180pub(crate) fn verify_signatures<T, D, V>(
181    cert: &SimpleCertificate<T, D, V>,
182    stake_table: &[<T::SignatureKey as SignatureKey>::StakeTableEntry],
183    threshold: U256,
184    upgrade_lock: &UpgradeLock<T>,
185) -> Result<()>
186where
187    T: NodeType,
188    D: Voteable<T> + 'static,
189    V: Threshold<T>,
190    SimpleCertificate<T, D, V>: Certificate<T, D>,
191{
192    let Some(signatures) = cert.signatures.as_ref() else {
193        bail!("certificate has no signatures");
194    };
195    let params = <T::SignatureKey as SignatureKey>::public_parameter(stake_table, threshold);
196    let commit = cert.data_commitment(upgrade_lock)?;
197    <T::SignatureKey as SignatureKey>::check(&params, commit.as_ref(), signatures).wrap()
198}
199
200impl<T: NodeType> Verifiable<T> for EpochChangeMessage<T, Unchecked> {
201    type Key = EpochNumber;
202    type Output = EpochChangeMessage<T, Validated>;
203
204    fn key(&self) -> Option<EpochNumber> {
205        self.epoch()
206    }
207
208    fn check(
209        self,
210        stake_table: &[<T::SignatureKey as SignatureKey>::StakeTableEntry],
211        threshold: U256,
212        epoch_height: u64,
213        upgrade_lock: &UpgradeLock<T>,
214        _: &EpochMembershipCoordinator<T>,
215    ) -> Result<Self::Output> {
216        self.well_formed(epoch_height).wrap()?;
217        verify_signatures(&self.cert1, stake_table, threshold, upgrade_lock)?;
218        verify_signatures(&self.cert2, stake_table, threshold, upgrade_lock)?;
219        Ok(self.into_validated())
220    }
221}
222
223impl<T: NodeType> Verifiable<T> for TimeoutCertificate3<T> {
224    type Key = ViewNumber;
225    type Output = Self;
226
227    fn key(&self) -> Option<ViewNumber> {
228        Some(self.view_number())
229    }
230
231    /// Its own epoch, and its lock's if the lock is recent: the lock
232    /// certificate is verified against that epoch's committee.
233    fn epochs(&self) -> impl Iterator<Item = EpochNumber> {
234        std::iter::once(self.epoch).chain(self.timeout_lock().verified_in())
235    }
236
237    fn check(
238        self,
239        stake_table: &[<T::SignatureKey as SignatureKey>::StakeTableEntry],
240        threshold: U256,
241        epoch_height: u64,
242        upgrade_lock: &UpgradeLock<T>,
243        memberships: &EpochMembershipCoordinator<T>,
244    ) -> Result<Self> {
245        verify_timeout3(
246            &self,
247            stake_table,
248            threshold,
249            epoch_height,
250            upgrade_lock,
251            memberships,
252        )?;
253        Ok(self)
254    }
255}
256
257impl<T: NodeType> Verifiable<T> for ReVoteMessage<T, Unchecked> {
258    type Key = ViewNumber;
259    type Output = ReVoteMessage<T, Validated>;
260
261    fn key(&self) -> Option<ViewNumber> {
262        Some(self.view_number())
263    }
264
265    /// Its own epoch, and the lock's of its timeout certificate if that lock
266    /// is recent: the lock certificate is verified against that epoch's
267    /// committee.
268    fn epochs(&self) -> impl Iterator<Item = EpochNumber> {
269        let lock = self
270            .revote
271            .timeout
272            .as_ref()
273            .and_then(|tc| tc.timeout_lock().verified_in());
274        std::iter::once(self.revote.epoch).chain(lock)
275    }
276
277    fn check(
278        self,
279        stake_table: &[<T::SignatureKey as SignatureKey>::StakeTableEntry],
280        threshold: U256,
281        epoch_height: u64,
282        upgrade_lock: &UpgradeLock<T>,
283        memberships: &EpochMembershipCoordinator<T>,
284    ) -> Result<Self::Output> {
285        let revote = &self.revote;
286        ensure! {
287            upgrade_lock.certificate_rule(revote.view),
288            "re-vote request for view {} before the certificate rule", revote.view
289        }
290        revote.well_formed(epoch_height).wrap()?;
291        let leader = memberships
292            .membership_for_epoch(Some(revote.epoch))
293            .wrap()?
294            .leader(revote.view)?;
295        ensure! {
296            self.signed_by(&leader),
297            "re-vote request not signed by its leader"
298        }
299        verify_signatures(&revote.cert1, stake_table, threshold, upgrade_lock)?;
300        if let Some(timeout) = &revote.timeout {
301            verify_timeout3(
302                timeout,
303                stake_table,
304                threshold,
305                epoch_height,
306                upgrade_lock,
307                memberships,
308            )?;
309        }
310        Ok(self.into_validated())
311    }
312}
313
314/// Verify a lock-carrying timeout certificate: its groups against the
315/// committee given, and its lock certificate against the committee of the
316/// lock's own epoch.
317pub(crate) fn verify_timeout3<T: NodeType>(
318    tc: &TimeoutCertificate3<T>,
319    stake_table: &[<T::SignatureKey as SignatureKey>::StakeTableEntry],
320    threshold: U256,
321    epoch_height: u64,
322    upgrade_lock: &UpgradeLock<T>,
323    memberships: &EpochMembershipCoordinator<T>,
324) -> Result<()> {
325    ensure! {
326        upgrade_lock.timeout_epoch_bound(tc.view_number()),
327        "timeout certificate for view {} must not bind its epoch", tc.view_number()
328    }
329    tc.check_signatures(stake_table, threshold, upgrade_lock)?;
330    let TimeoutLock::Recent(cert) = tc.timeout_lock() else {
331        return Ok(());
332    };
333    ensure! {
334        cert.data.is_well_formed(epoch_height),
335        "timeout certificate lock is not well formed"
336    }
337    let epoch = LockView::of(cert).epoch;
338    let membership = memberships.membership_for_epoch(Some(epoch)).wrap()?;
339    let entries = StakeTableEntries::from_iter(membership.stake_table()).0;
340    verify_signatures(cert, &entries, membership.success_threshold(), upgrade_lock)
341}
342
343/// Verifies certificates off the main coordinator thread.
344///
345/// The threshold-signature check is slow (> 1ms), so running it inline would
346/// stall the consensus loop. Each item's check runs in a `spawn_blocking`
347/// task; `next()` yields only those that pass. An item whose epoch
348/// membership isn't known yet is held in `pending_membership` and retried on
349/// [`Self::retry_pending`].
350///
351/// Items are deduplicated per ([`Verifiable::Key`], sender) and verified one
352/// at a time per key, trying the next sender's item if one proves invalid; a
353/// faulty sender can neither shadow a key nor hold more than one slot per
354/// key. Since intake bounds the key space, memory is bounded by (admissible
355/// keys * committee size).
356pub struct CertVerifier<T: NodeType, C: Verifiable<T>> {
357    tasks: JoinMap<C::Key, Option<ValidCert<C::Output>>>,
358    pending_task: BTreeMap<C::Key, HashMap<T::SignatureKey, C>>,
359    pending_membership: BTreeMap<C::Key, HashMap<T::SignatureKey, C>>,
360    completed: BTreeSet<C::Key>,
361    lower_bound: Option<C::Key>,
362    membership: EpochMembershipCoordinator<T>,
363    upgrade_lock: UpgradeLock<T>,
364    invalid_certs: u64,
365}
366
367impl<T: NodeType, C: Verifiable<T> + Send + 'static> CertVerifier<T, C> {
368    pub fn new(membership: EpochMembershipCoordinator<T>, upgrade_lock: UpgradeLock<T>) -> Self {
369        Self {
370            tasks: JoinMap::new(),
371            pending_task: BTreeMap::new(),
372            pending_membership: BTreeMap::new(),
373            completed: BTreeSet::new(),
374            lower_bound: None,
375            membership,
376            upgrade_lock,
377            invalid_certs: 0,
378        }
379    }
380
381    /// Submit an item received from the network for verification. If the
382    /// epoch's membership isn't ready the item is held and its epoch returned
383    /// so the caller can drive that epoch's catchup. Duplicates are dropped.
384    pub fn verify(&mut self, sender: T::SignatureKey, cert: C) -> Option<EpochNumber> {
385        let Some(key) = cert.key() else {
386            warn!(cert = type_name::<C>(), "certificate has no key");
387            return None;
388        };
389
390        let Some(epoch) = cert.epoch() else {
391            warn!(%key, cert = type_name::<C>(), "certificate has no epoch number");
392            return None;
393        };
394
395        if self.is_stale(key) || self.completed.contains(&key) {
396            return None;
397        }
398
399        if let Some(senders) = self.pending_task.get(&key)
400            && senders.contains_key(&sender)
401        {
402            return None;
403        }
404
405        if let Some(senders) = self.pending_membership.get(&key)
406            && senders.contains_key(&sender)
407        {
408            return None;
409        }
410
411        if self.tasks.contains_key(&key) {
412            self.pending_task
413                .entry(key)
414                .or_default()
415                .insert(sender, cert);
416            return None;
417        }
418
419        let Ok(membership) = self.membership.membership_for_epoch(Some(epoch)) else {
420            self.pending_membership
421                .entry(key)
422                .or_default()
423                .insert(sender, cert);
424            return Some(epoch);
425        };
426        let missing = cert
427            .epochs()
428            .find(|epoch| self.membership.membership_for_epoch(Some(*epoch)).is_err());
429        if let Some(missing) = missing {
430            self.pending_membership
431                .entry(key)
432                .or_default()
433                .insert(sender, cert);
434            return Some(missing);
435        }
436
437        let lock = self.upgrade_lock.clone();
438        let epoch_height = *self.membership.epoch_height();
439        let memberships = self.membership.clone();
440
441        self.tasks.spawn_blocking(key, move || {
442            let entries = StakeTableEntries::from_iter(membership.stake_table()).0;
443            let threshold = membership.success_threshold();
444            match cert.check(&entries, threshold, epoch_height, &lock, &memberships) {
445                Ok(valid) => Some(ValidCert::new(valid, epoch)),
446                Err(err) => {
447                    warn!(%key, %epoch, %err, cert = type_name::<C>(), "invalid certificate");
448                    None
449                },
450            }
451        });
452
453        None
454    }
455
456    /// Record that this key's item was completed by other means.
457    ///
458    /// This can happen locally from votes for example.
459    pub fn mark_completed(&mut self, key: C::Key) {
460        if self.is_stale(key) {
461            return;
462        }
463        self.completed.insert(key);
464        self.pending_task.remove(&key);
465        self.pending_membership.remove(&key);
466        self.tasks.abort(&key);
467    }
468
469    /// Re-attempt any items deferred because their epoch stake table wasn't
470    /// available. Called when new epoch data arrives. Returns the epochs
471    /// whose stake table is still missing so the caller can keep driving their
472    /// catchup.
473    pub fn retry_pending(&mut self) -> Vec<EpochNumber> {
474        mem::take(&mut self.pending_membership)
475            .into_values()
476            .flatten()
477            .filter_map(|(sender, cert)| self.verify(sender, cert))
478            .collect()
479    }
480
481    pub async fn next(&mut self) -> Option<ValidCert<C::Output>> {
482        loop {
483            match self.tasks.join_next().await? {
484                (key, Ok(Some(cert))) => {
485                    if !self.is_stale(key) {
486                        self.completed.insert(key);
487                        self.pending_task.remove(&key);
488                        self.pending_membership.remove(&key);
489                        return Some(cert);
490                    }
491                },
492                (key, Ok(None)) => {
493                    self.invalid_certs += 1;
494                    if !self.is_stale(key)
495                        && let Some((sender, cert)) = self.next_pending_sender(key)
496                    {
497                        self.verify(sender, cert);
498                    }
499                },
500                (key, Err(err)) => {
501                    if err.is_panic() {
502                        error!(%key, %err, cert = type_name::<C>(), "cert verification task panic");
503                    }
504                    if !self.is_stale(key)
505                        && let Some((sender, cert)) = self.next_pending_sender(key)
506                    {
507                        self.verify(sender, cert);
508                    }
509                },
510            }
511        }
512    }
513
514    pub fn gc(&mut self, key: C::Key) {
515        self.completed = self.completed.split_off(&key);
516        self.pending_task = self.pending_task.split_off(&key);
517        self.pending_membership = self.pending_membership.split_off(&key);
518        self.lower_bound = Some(key);
519        self.tasks.abort_matching(|k| *k < key);
520    }
521
522    pub fn num_invalid_certs(&self) -> u64 {
523        self.invalid_certs
524    }
525
526    fn next_pending_sender(&mut self, k: C::Key) -> Option<(T::SignatureKey, C)> {
527        let map = self.pending_task.get_mut(&k)?;
528        let sender = map.keys().next().cloned()?;
529        let cert = map.remove(&sender)?;
530        if map.is_empty() {
531            self.pending_task.remove(&k);
532        }
533        Some((sender, cert))
534    }
535
536    fn is_stale(&self, key: C::Key) -> bool {
537        self.lower_bound.is_some_and(|lb| key < lb)
538    }
539}
540
541/// What a completed certificate retires: its view, or its view and epoch.
542#[derive(Clone, Copy, Debug)]
543pub enum Completion {
544    PerView,
545    PerViewAndEpoch,
546}
547
548/// What a [`CertBySenderVerifier`] retires, tracks in flight and parks by: a
549/// certificate's view, plus its epoch where the [`Completion`] binds it.
550type CompletionKey = (ViewNumber, EpochNumber);
551
552/// What a verification task reports: its key, and the certificate if it held.
553type VerifyOutcome<O> = (CompletionKey, Option<ValidCert<O>>);
554
555/// Verifies certificates off the main coordinator thread.
556///
557/// Unlike [`CertVerifier`], these certificates are keyed by sender key
558/// instead of view/epoch, helping a lagging node jump to the frontier. A
559/// sender's submission is dropped while its previous one is still being
560/// verified, and only one verification runs per [`CompletionKey`]: copies of
561/// an in-flight certificate from other senders are parked and tried in turn
562/// only if the in-flight one proves invalid.
563///
564/// Memory is bounded by the committee, not by the view space. A copy is only
565/// parked while its key is in flight (`parked.keys() ⊆ in_flight.keys()`)
566/// and `in_flight` holds at most one key per sender, so at most
567/// `committee_size` keys are parked at once, each holding at most
568/// `committee_size` copies. A single peer occupies its own in-flight slot
569/// plus at most one parked slot per other in-flight key. The subset
570/// relation holds because every path that removes an `in_flight` entry also
571/// clears or re-fills the key's `parked` entry: the three arms of
572/// [`Self::next`], [`Self::mark_completed`] and [`Self::gc`].
573pub struct CertBySenderVerifier<T: NodeType, C: Verifiable<T>> {
574    tasks: JoinMap<T::SignatureKey, VerifyOutcome<C::Output>>,
575    /// The sender whose copy is being verified, per key. A sender has at
576    /// most one entry at a time, since [`Self::verify`] drops its submissions
577    /// while it has a task; the `Err` arm of [`Self::next`] relies on that to
578    /// find a panicked task's key.
579    in_flight: BTreeMap<CompletionKey, T::SignatureKey>,
580    /// Copies of an in-flight certificate from other senders, waiting on it.
581    parked: BTreeMap<CompletionKey, HashMap<T::SignatureKey, C>>,
582    pending: HashMap<T::SignatureKey, C>,
583    completed: BTreeSet<CompletionKey>,
584    completion: Completion,
585    lower_bound: ViewNumber,
586    membership: EpochMembershipCoordinator<T>,
587    upgrade_lock: UpgradeLock<T>,
588    invalid_certs: u64,
589}
590
591impl<T: NodeType, C: Verifiable<T> + Send + 'static> CertBySenderVerifier<T, C>
592where
593    C::Output: HasViewNumber,
594{
595    pub fn new(
596        membership: EpochMembershipCoordinator<T>,
597        upgrade_lock: UpgradeLock<T>,
598        completion: Completion,
599    ) -> Self {
600        Self {
601            tasks: JoinMap::new(),
602            in_flight: BTreeMap::new(),
603            parked: BTreeMap::new(),
604            pending: HashMap::new(),
605            completed: BTreeSet::new(),
606            completion,
607            lower_bound: ViewNumber::genesis(),
608            membership,
609            upgrade_lock,
610            invalid_certs: 0,
611        }
612    }
613
614    /// Submit an item received from `sender` for verification.
615    ///
616    /// Dropped if the sender's previous submission is still being verified;
617    /// parked if the same certificate is already being verified for another
618    /// sender. If the epoch's membership isn't ready the item is held and its
619    /// epoch returned so the caller can drive that epoch's catchup.
620    pub fn verify(&mut self, sender: T::SignatureKey, cert: C) -> Option<EpochNumber> {
621        let view = cert.view_number();
622
623        if view < self.lower_bound {
624            return None;
625        }
626
627        let Some(epoch) = cert.epoch() else {
628            warn!(%view, cert = type_name::<C>(), "received certificate has no epoch number");
629            return None;
630        };
631
632        let key = self.completion_key(view, epoch);
633        if self.completed.contains(&key) {
634            return None;
635        }
636
637        if let Some(in_flight_sender) = self.in_flight.get(&key) {
638            if *in_flight_sender != sender {
639                self.parked.entry(key).or_default().insert(sender, cert);
640            }
641            return None;
642        }
643
644        if self.tasks.contains_key(&sender) {
645            return None;
646        }
647
648        let Ok(membership) = self.membership.membership_for_epoch(Some(epoch)) else {
649            self.pending.insert(sender, cert);
650            return Some(epoch);
651        };
652        let missing = cert
653            .epochs()
654            .find(|epoch| self.membership.membership_for_epoch(Some(*epoch)).is_err());
655        if let Some(missing) = missing {
656            self.pending.insert(sender, cert);
657            return Some(missing);
658        }
659
660        let lock = self.upgrade_lock.clone();
661        let epoch_height = *self.membership.epoch_height();
662        let memberships = self.membership.clone();
663
664        self.in_flight.insert(key, sender.clone());
665        self.tasks.spawn_blocking(sender, move || {
666            let entries = StakeTableEntries::from_iter(membership.stake_table()).0;
667            let threshold = membership.success_threshold();
668            match cert.check(&entries, threshold, epoch_height, &lock, &memberships) {
669                Ok(valid) => (key, Some(ValidCert::new(valid, epoch))),
670                Err(err) => {
671                    warn!(%view, %epoch, %err, cert = type_name::<C>(), "invalid certificate");
672                    (key, None)
673                },
674            }
675        });
676
677        None
678    }
679
680    /// Record that this view's item was completed by other means, in `epoch`.
681    ///
682    /// This can happen locally from votes for example.
683    pub fn mark_completed(&mut self, view: ViewNumber, epoch: EpochNumber) {
684        if view < self.lower_bound {
685            return;
686        }
687        let key = self.completion_key(view, epoch);
688        self.completed.insert(key);
689        let and_epoch = matches!(self.completion, Completion::PerViewAndEpoch);
690        self.pending
691            .retain(|_, c| c.view_number() != view || (and_epoch && c.epoch() != Some(epoch)));
692        self.parked.remove(&key);
693        if let Some(sender) = self.in_flight.remove(&key) {
694            self.tasks.abort(&sender);
695        }
696    }
697
698    /// Re-attempt any items deferred because their epoch stake table wasn't
699    /// available. Returns the epochs whose stake table is still missing so
700    /// the caller can keep driving their catchup.
701    pub fn retry_pending(&mut self) -> Vec<EpochNumber> {
702        mem::take(&mut self.pending)
703            .into_iter()
704            .filter_map(|(sender, cert)| self.verify(sender, cert))
705            .collect()
706    }
707
708    pub async fn next(&mut self) -> Option<ValidCert<C::Output>> {
709        loop {
710            match self.tasks.join_next().await? {
711                (_, Ok((key, Some(cert)))) => {
712                    self.in_flight.remove(&key);
713                    self.parked.remove(&key);
714                    let (view, _) = key;
715                    if view >= self.lower_bound && self.completed.insert(key) {
716                        return Some(cert);
717                    }
718                },
719                (_, Ok((key, None))) => {
720                    self.invalid_certs += 1;
721                    self.in_flight.remove(&key);
722                    self.promote_parked(key);
723                },
724                (sender, Err(err)) => {
725                    // `mark_completed` and `gc` abort a task only after
726                    // removing its `in_flight` entry, so a cancellation
727                    // leaves nothing to promote.
728                    if err.is_cancelled() {
729                        continue;
730                    }
731                    if err.is_panic() {
732                        error!(?sender, %err, cert = type_name::<C>(), "cert verification task panic");
733                    }
734                    // Don't strand parked copies of the panicked sender's key.
735                    let key = self
736                        .in_flight
737                        .iter()
738                        .find_map(|(key, s)| (*s == sender).then_some(*key));
739                    if let Some(key) = key {
740                        self.in_flight.remove(&key);
741                        self.promote_parked(key);
742                    }
743                },
744            }
745        }
746    }
747
748    pub fn gc(&mut self, view: ViewNumber) {
749        let floor = (view, EpochNumber::new(0));
750        self.completed = self.completed.split_off(&floor);
751        self.parked = self.parked.split_off(&floor);
752        self.pending.retain(|_, c| c.view_number() >= view);
753        let live = self.in_flight.split_off(&floor);
754        for sender in self.in_flight.values() {
755            self.tasks.abort(sender);
756        }
757        self.in_flight = live;
758        self.lower_bound = view;
759    }
760
761    /// What a completed item is remembered by.
762    fn completion_key(&self, view: ViewNumber, epoch: EpochNumber) -> CompletionKey {
763        match self.completion {
764            Completion::PerView => (view, EpochNumber::new(0)),
765            Completion::PerViewAndEpoch => (view, epoch),
766        }
767    }
768
769    pub fn num_invalid_certs(&self) -> u64 {
770        self.invalid_certs
771    }
772
773    /// Try `key`'s parked copies, in random order, until one spawns.
774    ///
775    /// The order is random so that a Byzantine sender whose copies always
776    /// prove invalid can't be first in line every time and delay the honest
777    /// copies behind it. A copy whose sender is now busy on another key is
778    /// dropped rather than re-parked, and a copy deferred for a missing stake
779    /// table lands in `pending` without its epoch being requested here: the
780    /// table was available when the in-flight copy spawned, so
781    /// [`Self::retry_pending`] recovers it. Either way the key stays
782    /// incomplete and the next rebroadcast spawns afresh.
783    fn promote_parked(&mut self, key: CompletionKey) {
784        let (view, _) = key;
785        if view < self.lower_bound || self.completed.contains(&key) {
786            self.parked.remove(&key);
787            return;
788        }
789        while let Some((sender, cert)) = self.next_parked_sender(key) {
790            self.verify(sender, cert);
791            if self.in_flight.contains_key(&key) {
792                return;
793            }
794        }
795    }
796
797    fn next_parked_sender(&mut self, key: CompletionKey) -> Option<(T::SignatureKey, C)> {
798        use rand::seq::IteratorRandom;
799
800        let map = self.parked.get_mut(&key)?;
801        let sender = map.keys().choose(&mut rand::thread_rng()).cloned()?;
802        let cert = map.remove(&sender)?;
803        if map.is_empty() {
804            self.parked.remove(&key);
805        }
806        Some((sender, cert))
807    }
808}
809
810/// The coordinator's network-certificate verifiers, one per certificate type.
811pub struct CertVerifiers<T: NodeType> {
812    pub cert1: CertVerifier<T, Certificate1<T>>,
813    pub cert2: CertVerifier<T, Certificate2<T>>,
814    pub timeout: CertBySenderVerifier<T, TimeoutCertificate2<T>>,
815    pub timeout3: CertBySenderVerifier<T, TimeoutCertificate3<T>>,
816    pub advance: CertBySenderVerifier<T, Certificate1<T>>,
817    pub epoch_change: CertVerifier<T, EpochChangeMessage<T, Unchecked>>,
818    pub revote: CertVerifier<T, ReVoteMessage<T, Unchecked>>,
819}
820
821impl<T: NodeType> CertVerifiers<T> {
822    pub fn new(membership: EpochMembershipCoordinator<T>, upgrade_lock: UpgradeLock<T>) -> Self {
823        Self {
824            cert1: CertVerifier::new(membership.clone(), upgrade_lock.clone()),
825            cert2: CertVerifier::new(membership.clone(), upgrade_lock.clone()),
826            timeout: CertBySenderVerifier::new(
827                membership.clone(),
828                upgrade_lock.clone(),
829                Completion::PerView,
830            ),
831            timeout3: CertBySenderVerifier::new(
832                membership.clone(),
833                upgrade_lock.clone(),
834                Completion::PerViewAndEpoch,
835            ),
836            advance: CertBySenderVerifier::new(
837                membership.clone(),
838                upgrade_lock.clone(),
839                Completion::PerViewAndEpoch,
840            ),
841            epoch_change: CertVerifier::new(membership.clone(), upgrade_lock.clone()),
842            revote: CertVerifier::new(membership, upgrade_lock),
843        }
844    }
845
846    pub fn retry_pending<F>(&mut self, mut request: F)
847    where
848        F: FnMut(EpochNumber),
849    {
850        for epoch in self.cert1.retry_pending() {
851            request(epoch);
852        }
853        for epoch in self.cert2.retry_pending() {
854            request(epoch);
855        }
856        for epoch in self.timeout.retry_pending() {
857            request(epoch);
858        }
859        for epoch in self.timeout3.retry_pending() {
860            request(epoch);
861        }
862        for epoch in self.advance.retry_pending() {
863            request(epoch);
864        }
865        for epoch in self.epoch_change.retry_pending() {
866            request(epoch);
867        }
868        for epoch in self.revote.retry_pending() {
869            request(epoch);
870        }
871    }
872
873    pub fn gc(&mut self, view: ViewNumber, epoch: EpochNumber) {
874        self.cert1.gc(ViewEpoch::start(view));
875        self.cert2.gc(ViewEpoch::start(view));
876        self.timeout.gc(view);
877        self.timeout3.gc(view);
878        self.advance.gc(view);
879        self.epoch_change.gc(epoch);
880        self.revote.gc(view);
881    }
882
883    pub fn num_invalid_certs(&self) -> u64 {
884        self.cert1
885            .num_invalid_certs()
886            .saturating_add(self.cert2.num_invalid_certs())
887            .saturating_add(self.timeout.num_invalid_certs())
888            .saturating_add(self.timeout3.num_invalid_certs())
889            .saturating_add(self.advance.num_invalid_certs())
890            .saturating_add(self.epoch_change.num_invalid_certs())
891            .saturating_add(self.revote.num_invalid_certs())
892    }
893}
894
895#[cfg(test)]
896mod tests {
897    use std::marker::PhantomData;
898
899    use committable::Committable;
900    use hotshot_example_types::node_types::TestTypes;
901    use hotshot_types::{
902        data::{EpochNumber, ViewNumber},
903        signature_key::BLSPubKey,
904        simple_certificate::{TimeoutCertificate2, TimeoutCertificate3, TimeoutEvidence},
905        simple_vote::TimeoutData2,
906        traits::signature_key::SignatureKey as _,
907        vote::HasViewNumber,
908    };
909
910    use super::{CertBySenderVerifier, Completion, CompletionKey};
911    use crate::{
912        helpers::{test_timeout_epoch_lock, test_upgrade_lock},
913        tests::common::utils::{
914            TEST_DRB_RESULT, build_cert1, build_timeout_cert, build_timeout_cert3_with_lock,
915            mock_membership,
916        },
917    };
918
919    fn sender(i: u64) -> BLSPubKey {
920        BLSPubKey::generated_from_seed_indexed([0u8; 32], i).0
921    }
922
923    /// A verifier for the form that does not bind the epoch, as the tests
924    /// running under [`test_upgrade_lock`] need.
925    fn verifier() -> CertBySenderVerifier<TestTypes, TimeoutCertificate2<TestTypes>> {
926        CertBySenderVerifier::new(mock_membership(), test_upgrade_lock(), Completion::PerView)
927    }
928
929    fn verifier3() -> CertBySenderVerifier<TestTypes, TimeoutCertificate3<TestTypes>> {
930        CertBySenderVerifier::new(
931            mock_membership(),
932            test_timeout_epoch_lock(),
933            Completion::PerViewAndEpoch,
934        )
935    }
936
937    /// What [`verifier`] retires and parks `view` by.
938    fn per_view(view: u64) -> CompletionKey {
939        (ViewNumber::new(view), EpochNumber::new(0))
940    }
941
942    fn valid_tc(view: u64) -> TimeoutCertificate2<TestTypes> {
943        let epoch = EpochNumber::genesis();
944        let membership = mock_membership().membership_for_epoch(Some(epoch)).unwrap();
945        let (pub_key, priv_key) = BLSPubKey::generated_from_seed_indexed([0u8; 32], 0);
946        match build_timeout_cert(
947            ViewNumber::new(view),
948            epoch,
949            &membership,
950            &pub_key,
951            &priv_key,
952        ) {
953            TimeoutEvidence::V2(tc) => tc,
954            TimeoutEvidence::V3(_) => unreachable!("build_timeout_cert forms the unbound form"),
955        }
956    }
957
958    /// A certificate whose aggregate signature doesn't match its data.
959    fn invalid_tc(view: u64) -> TimeoutCertificate2<TestTypes> {
960        let mut tc = valid_tc(view);
961        tc.signatures = valid_tc(view + 1).signatures;
962        tc
963    }
964
965    /// An unsigned certificate, which verification is bound to reject: what is
966    /// under test is whether it is examined at all.
967    fn junk_tc(view: ViewNumber, epoch: EpochNumber) -> TimeoutCertificate2<TestTypes> {
968        let data = TimeoutData2 {
969            view,
970            epoch: Some(epoch),
971        };
972        TimeoutCertificate2::new(data.clone(), data.commit(), view, None, PhantomData)
973    }
974
975    fn junk_tc3(view: ViewNumber, epoch: EpochNumber) -> TimeoutCertificate3<TestTypes> {
976        // Signed by a quorum, but over another view than the one it is for.
977        let membership = mock_membership().membership_for_epoch(Some(epoch)).unwrap();
978        let mut cert = build_timeout_cert3_with_lock(view + 1, epoch, None, &membership);
979        cert.view_number = view;
980        cert
981    }
982
983    /// Certificates of two epochs at one view are both verified: the outgoing
984    /// committee may certify a re-vote at a view the incoming one certifies
985    /// as well.
986    #[tokio::test]
987    async fn certificates_of_two_epochs_at_one_view_are_both_verified() {
988        let coordinator = mock_membership();
989        let mut verifier = super::CertVerifier::<TestTypes, super::Certificate1<TestTypes>>::new(
990            coordinator.clone(),
991            test_upgrade_lock(),
992        );
993        let view = ViewNumber::new(12);
994        let (pk, sk) = BLSPubKey::generated_from_seed_indexed([0u8; 32], 0);
995        let cert = |epoch: u64, block: u64| {
996            let epoch = EpochNumber::new(epoch);
997            let membership = coordinator.membership_for_epoch(Some(epoch)).unwrap();
998            let leaf = committable::RawCommitmentBuilder::new("leaf")
999                .u64(block)
1000                .finalize();
1001            build_cert1(leaf, epoch, block, &membership, view, &pk, &sk)
1002        };
1003        let (older, newer) = (cert(1, 10), cert(2, 11));
1004        assert!(verifier.verify(pk, older.clone()).is_none());
1005        assert_eq!(verifier.next().await.map(|c| c.into_cert()), Some(older));
1006        assert!(verifier.verify(pk, newer.clone()).is_none());
1007        assert_eq!(verifier.next().await.map(|c| c.into_cert()), Some(newer));
1008    }
1009
1010    /// A certificate whose lock is of an epoch whose committee is not known yet
1011    /// waits until it is, as one of an unknown epoch of its own does, and is
1012    /// then verified.
1013    #[tokio::test]
1014    async fn certificate_with_a_lock_of_an_unknown_epoch_waits() {
1015        let (lock_epoch, epoch) = (EpochNumber::new(3), EpochNumber::new(4));
1016        let known = mock_membership();
1017        for e in [lock_epoch, epoch] {
1018            known.membership().register_epoch(e, TEST_DRB_RESULT);
1019        }
1020        let lock_membership = known.membership_for_epoch(Some(lock_epoch)).unwrap();
1021        let membership = known.membership_for_epoch(Some(epoch)).unwrap();
1022        let (pk, sk) = BLSPubKey::generated_from_seed_indexed([0u8; 32], 0);
1023        let leaf = committable::RawCommitmentBuilder::new("leaf")
1024            .u64(29)
1025            .finalize();
1026        let lock = build_cert1(
1027            leaf,
1028            lock_epoch,
1029            29,
1030            &lock_membership,
1031            ViewNumber::new(30),
1032            &pk,
1033            &sk,
1034        );
1035        let tc = build_timeout_cert3_with_lock(ViewNumber::new(31), epoch, Some(lock), &membership);
1036
1037        let coordinator = mock_membership();
1038        coordinator
1039            .membership()
1040            .register_epoch(epoch, TEST_DRB_RESULT);
1041        assert!(
1042            coordinator.membership_for_epoch(Some(epoch)).is_ok()
1043                && coordinator.membership_for_epoch(Some(lock_epoch)).is_err(),
1044            "setup: the certificate's committee is known, the lock's is not"
1045        );
1046        let mut verifier = CertBySenderVerifier::<TestTypes, TimeoutCertificate3<TestTypes>>::new(
1047            coordinator.clone(),
1048            test_timeout_epoch_lock(),
1049            Completion::PerViewAndEpoch,
1050        );
1051        assert_eq!(verifier.verify(sender(1), tc.clone()), Some(lock_epoch));
1052        assert!(verifier.tasks.is_empty(), "nothing is checked yet");
1053
1054        coordinator
1055            .membership()
1056            .register_epoch(lock_epoch, TEST_DRB_RESULT);
1057        assert!(verifier.retry_pending().is_empty());
1058        let valid = verifier
1059            .next()
1060            .await
1061            .expect("verified once the lock's committee is known");
1062        assert_eq!(valid.into_cert(), tc);
1063    }
1064
1065    // ==================== Parking ====================
1066
1067    /// Same-view copies park instead of spawning redundant tasks.
1068    #[tokio::test]
1069    async fn test_same_view_copies_are_parked() {
1070        // Tests behaviour before 0.7.
1071        crate::helpers::pin_test_version(versions::NEW_PROTOCOL_VERSION);
1072        let mut verifier = verifier();
1073        let view = ViewNumber::new(1);
1074        let tc = valid_tc(1);
1075
1076        verifier.verify(sender(1), tc.clone());
1077        verifier.verify(sender(2), tc.clone());
1078        verifier.verify(sender(3), tc);
1079        assert_eq!(verifier.tasks.len(), 1);
1080        assert_eq!(verifier.parked.get(&per_view(1)).unwrap().len(), 2);
1081
1082        let cert = verifier.next().await.expect("certificate should verify");
1083        assert_eq!(cert.view_number(), view);
1084        assert!(verifier.parked.is_empty());
1085        assert!(verifier.in_flight.is_empty());
1086        assert!(verifier.next().await.is_none());
1087    }
1088
1089    /// A sender can't park a second copy behind its own in-flight one.
1090    #[tokio::test]
1091    async fn test_same_sender_resubmit_is_dropped() {
1092        // Tests behaviour before 0.7.
1093        crate::helpers::pin_test_version(versions::NEW_PROTOCOL_VERSION);
1094        let mut verifier = verifier();
1095        let view = ViewNumber::new(1);
1096        let tc = valid_tc(1);
1097
1098        verifier.verify(sender(0), tc.clone());
1099        verifier.verify(sender(0), tc);
1100        assert_eq!(verifier.tasks.len(), 1);
1101        assert!(verifier.parked.is_empty());
1102
1103        let cert = verifier.next().await.expect("certificate should verify");
1104        assert_eq!(cert.view_number(), view);
1105        assert!(verifier.next().await.is_none());
1106    }
1107
1108    /// A parked copy is verified when the in-flight one proves invalid.
1109    #[tokio::test]
1110    async fn test_parked_copy_promoted_after_invalid() {
1111        // Tests behaviour before 0.7.
1112        crate::helpers::pin_test_version(versions::NEW_PROTOCOL_VERSION);
1113        let mut verifier = verifier();
1114        let view = ViewNumber::new(1);
1115
1116        verifier.verify(sender(1), invalid_tc(1));
1117        verifier.verify(sender(2), valid_tc(1));
1118        assert_eq!(verifier.tasks.len(), 1);
1119
1120        let cert = verifier
1121            .next()
1122            .await
1123            .expect("parked copy should be verified");
1124        assert_eq!(cert.view_number(), view);
1125        assert_eq!(verifier.num_invalid_certs(), 1);
1126    }
1127
1128    /// Certificates for distinct views still verify concurrently.
1129    #[tokio::test]
1130    async fn test_distinct_views_verify_concurrently() {
1131        // Tests behaviour before 0.7.
1132        crate::helpers::pin_test_version(versions::NEW_PROTOCOL_VERSION);
1133        let mut verifier = verifier();
1134
1135        verifier.verify(sender(1), valid_tc(1));
1136        verifier.verify(sender(2), valid_tc(2));
1137        assert_eq!(verifier.tasks.len(), 2);
1138
1139        let mut views = vec![
1140            verifier.next().await.unwrap().view_number(),
1141            verifier.next().await.unwrap().view_number(),
1142        ];
1143        views.sort();
1144        assert_eq!(views, vec![ViewNumber::new(1), ViewNumber::new(2)]);
1145    }
1146
1147    /// Where the epoch is not bound, a relabelled copy of an in-flight
1148    /// certificate is the same object and parks behind it.
1149    #[tokio::test]
1150    async fn test_relabelled_copy_parks_where_epoch_is_unbound() {
1151        let mut verifier = verifier();
1152        let view = ViewNumber::new(1);
1153        let (ours, theirs) = (EpochNumber::genesis(), EpochNumber::genesis() + 1);
1154
1155        verifier.verify(sender(1), junk_tc(view, ours));
1156        verifier.verify(sender(2), junk_tc(view, theirs));
1157        assert_eq!(verifier.tasks.len(), 1);
1158        assert_eq!(verifier.parked.get(&per_view(1)).unwrap().len(), 1);
1159
1160        // Neither copy verifies, so the parked one is promoted and examined.
1161        assert!(verifier.next().await.is_none());
1162        assert_eq!(verifier.num_invalid_certs(), 2);
1163        assert!(verifier.parked.is_empty());
1164        assert!(verifier.in_flight.is_empty());
1165    }
1166
1167    /// Where the epoch is bound, the same view under two committees is two
1168    /// certificates, and one must not shadow the other.
1169    #[tokio::test]
1170    async fn test_distinct_epochs_verify_concurrently_where_epoch_is_bound() {
1171        let mut verifier = verifier3();
1172        let view = ViewNumber::new(1);
1173        let (old, new) = (EpochNumber::genesis(), EpochNumber::genesis() + 1);
1174
1175        verifier.verify(sender(1), junk_tc3(view, old));
1176        verifier.verify(sender(2), junk_tc3(view, new));
1177        assert_eq!(verifier.tasks.len(), 2);
1178        assert!(verifier.parked.is_empty());
1179        assert_eq!(verifier.in_flight.len(), 2);
1180
1181        assert!(verifier.next().await.is_none());
1182        assert_eq!(verifier.num_invalid_certs(), 2);
1183    }
1184
1185    /// `mark_completed` drops parked copies, the in-flight result and new
1186    /// submissions for the view.
1187    #[tokio::test]
1188    async fn test_mark_completed_clears_view() {
1189        // Tests behaviour before 0.7.
1190        crate::helpers::pin_test_version(versions::NEW_PROTOCOL_VERSION);
1191        let mut verifier = verifier();
1192        let view = ViewNumber::new(1);
1193
1194        verifier.verify(sender(1), valid_tc(1));
1195        verifier.verify(sender(2), valid_tc(1));
1196        verifier.mark_completed(view, EpochNumber::genesis());
1197        assert!(verifier.parked.is_empty());
1198        assert!(verifier.next().await.is_none());
1199        // Draining the aborted in-flight task through `next()`'s error arm
1200        // must not have promoted or respawned anything for the view.
1201        assert!(verifier.tasks.is_empty());
1202        assert!(verifier.in_flight.is_empty());
1203
1204        verifier.verify(sender(3), valid_tc(1));
1205        assert!(verifier.tasks.is_empty());
1206    }
1207
1208    /// `gc` drops parked and in-flight state below the cutoff, rejects
1209    /// stale resubmits, and leaves views at or above the cutoff live.
1210    #[tokio::test]
1211    async fn test_gc_clears_stale_state() {
1212        // Tests behaviour before 0.7.
1213        crate::helpers::pin_test_version(versions::NEW_PROTOCOL_VERSION);
1214        let mut verifier = verifier();
1215        let live = ViewNumber::new(5);
1216
1217        verifier.verify(sender(1), valid_tc(1));
1218        verifier.verify(sender(2), valid_tc(1));
1219        verifier.verify(sender(3), valid_tc(5));
1220        verifier.gc(live);
1221
1222        assert!(verifier.parked.is_empty());
1223        assert_eq!(
1224            verifier.in_flight.keys().copied().collect::<Vec<_>>(),
1225            [per_view(5)]
1226        );
1227
1228        verifier.verify(sender(4), valid_tc(1));
1229        assert!(!verifier.in_flight.contains_key(&per_view(1)));
1230        assert!(!verifier.tasks.contains_key(&sender(4)));
1231
1232        let cert = verifier.next().await.expect("live view should verify");
1233        assert_eq!(cert.view_number(), live);
1234        assert!(verifier.next().await.is_none());
1235    }
1236
1237    // ==================== Completion ====================
1238
1239    /// Completing a view in one epoch must not retire it in another, where the
1240    /// epoch is bound.
1241    ///
1242    /// A view at an epoch boundary can have a certificate from each committee.
1243    /// Retiring the view on the first would leave the second permanently
1244    /// unverified, with the one the node ends up acting on decided by arrival
1245    /// order.
1246    #[tokio::test]
1247    async fn completing_one_epoch_leaves_the_other_open() {
1248        let mut verifier = verifier3();
1249        let view = ViewNumber::new(1);
1250        let (old, new) = (EpochNumber::genesis(), EpochNumber::genesis() + 1);
1251        let sender = sender(0);
1252
1253        verifier.mark_completed(view, old);
1254
1255        // The same view under the other committee is still examined, and
1256        // rejected on its merits rather than dropped on the view alone.
1257        assert!(verifier.verify(sender, junk_tc3(view, new)).is_none());
1258        assert!(verifier.next().await.is_none());
1259        assert_eq!(verifier.num_invalid_certs(), 1);
1260
1261        // The epoch that was marked stays retired, so nothing examines it.
1262        assert!(verifier.verify(sender, junk_tc3(view, old)).is_none());
1263        assert!(verifier.next().await.is_none());
1264        assert_eq!(verifier.num_invalid_certs(), 1);
1265    }
1266
1267    /// Completing a view retires it under every epoch, where the epoch is not
1268    /// bound.
1269    ///
1270    /// The label is not covered by the signers, so a copy of the same
1271    /// certificate under another epoch is the same object, and verifying it
1272    /// again buys nothing: a second certificate for a view is dropped by
1273    /// `Consensus::handle_timeout_certificate` whatever it names.
1274    #[tokio::test]
1275    async fn completing_a_view_retires_every_epoch_of_it() {
1276        let mut verifier = verifier();
1277        let view = ViewNumber::new(1);
1278        let (ours, theirs) = (EpochNumber::genesis(), EpochNumber::genesis() + 1);
1279        let sender = sender(0);
1280
1281        verifier.mark_completed(view, ours);
1282
1283        for epoch in [ours, theirs] {
1284            assert!(verifier.verify(sender, junk_tc(view, epoch)).is_none());
1285            assert!(verifier.next().await.is_none());
1286            assert_eq!(
1287                verifier.num_invalid_certs(),
1288                0,
1289                "a retired view must not be examined again under {epoch}"
1290            );
1291        }
1292
1293        // A later view is untouched.
1294        assert!(verifier.verify(sender, junk_tc(view + 1, theirs)).is_none());
1295        assert!(verifier.next().await.is_none());
1296        assert_eq!(verifier.num_invalid_certs(), 1);
1297    }
1298}