Skip to main content

iota_types/
committee.rs

1// Copyright (c) 2021, Facebook, Inc. and its affiliates
2// Copyright (c) Mysten Labs, Inc.
3// Modifications Copyright (c) 2024 IOTA Stiftung
4// SPDX-License-Identifier: Apache-2.0
5
6use std::{
7    collections::{BTreeMap, BTreeSet, HashMap},
8    fmt::{Display, Formatter, Write},
9    hash::{Hash, Hasher},
10};
11
12use fastcrypto::traits::KeyPair;
13use iota_common::portable_random;
14use iota_multiaddr::Multiaddr;
15pub use iota_protocol_config::ProtocolVersion;
16use iota_sdk_types::{TransactionDigest, ValidatorCommitteeMember};
17use once_cell::sync::OnceCell;
18use rand::{
19    Rng, SeedableRng,
20    rngs::{ChaCha12Rng, ThreadRng},
21};
22use serde::{Deserialize, Serialize};
23
24use super::base_types::*;
25use crate::{
26    crypto::{
27        AggregateAuthorityPublicKey, AuthorityKeyPair, NetworkPublicKey,
28        random_committee_key_pairs_of_size,
29    },
30    error::{IotaError, IotaResult},
31    messages_checkpoint::{CertifiedCheckpointSummary, VerifiedCheckpoint},
32};
33
34pub type EpochId = u64;
35
36// TODO: the stake and voting power of a validator can be different so
37// in some places when we are actually referring to the voting power, we
38// should use a different type alias, field name, etc.
39pub type StakeUnit = u64;
40
41pub type CommitteeDigest = [u8; 32];
42
43// The voting power, quorum threshold and max voting power are defined in the
44// `voting_power.move` module. We're following the very same convention in the
45// validator binaries.
46
47/// Set total_voting_power as 10_000 by convention. Individual voting powers can
48/// be interpreted as easily understandable basis points (e.g., voting_power:
49/// 100 = 1%, voting_power: 1 = 0.01%). Fixing the total voting power allows
50/// clients to hardcode the quorum threshold and total_voting power rather
51/// than recomputing these.
52pub const TOTAL_VOTING_POWER: StakeUnit = 10_000;
53
54/// Quorum threshold for our fixed voting power--any message signed by this much
55/// voting power can be trusted up to BFT assumptions
56pub const QUORUM_THRESHOLD: StakeUnit = 6_667;
57
58/// Validity threshold defined by f+1
59pub const VALIDITY_THRESHOLD: StakeUnit = 3_334;
60
61#[derive(Clone, Debug, Serialize, Deserialize, Eq)]
62pub struct Committee {
63    pub epoch: EpochId,
64    pub voting_rights: Vec<(AuthorityName, StakeUnit)>,
65    expanded_keys: HashMap<AuthorityName, AggregateAuthorityPublicKey>,
66    index_map: HashMap<AuthorityName, usize>,
67}
68
69impl Committee {
70    pub fn new(epoch: EpochId, voting_rights: BTreeMap<AuthorityName, StakeUnit>) -> Self {
71        let mut voting_rights: Vec<(AuthorityName, StakeUnit)> =
72            voting_rights.iter().map(|(a, s)| (*a, *s)).collect();
73
74        assert!(!voting_rights.is_empty());
75        assert!(voting_rights.iter().any(|(_, s)| *s != 0));
76
77        voting_rights.sort_by_key(|(a, _)| *a);
78        let total_votes: StakeUnit = voting_rights.iter().map(|(_, votes)| *votes).sum();
79        assert_eq!(total_votes, TOTAL_VOTING_POWER);
80
81        let (expanded_keys, index_map) = Self::load_inner(&voting_rights);
82
83        Committee {
84            epoch,
85            voting_rights,
86            expanded_keys,
87            index_map,
88        }
89    }
90
91    /// Build a committee for `epoch` from a [`ValidatorCommitteeMember`]
92    /// list.
93    pub fn from_committee_members(epoch: EpochId, members: &[ValidatorCommitteeMember]) -> Self {
94        Self::new(
95            epoch,
96            members
97                .iter()
98                .map(|member| (member.public_key.into(), member.stake))
99                .collect(),
100        )
101    }
102
103    /// The committee's voting rights expressed as a
104    /// [`ValidatorCommitteeMember`] list used by `EndOfEpochData`.
105    pub fn committee_members(&self) -> Vec<ValidatorCommitteeMember> {
106        self.voting_rights
107            .iter()
108            .map(|(name, stake)| ValidatorCommitteeMember {
109                public_key: (*name).into(),
110                stake: *stake,
111            })
112            .collect()
113    }
114
115    /// Normalize the given weights to TOTAL_VOTING_POWER and create the
116    /// committee. Used for testing only: a production system is using the
117    /// voting weights of the Iota System object.
118    pub fn new_for_testing_with_normalized_voting_power(
119        epoch: EpochId,
120        mut voting_weights: BTreeMap<AuthorityName, StakeUnit>,
121    ) -> Self {
122        let num_nodes = voting_weights.len();
123        let total_votes: StakeUnit = voting_weights.values().cloned().sum();
124
125        let normalization_coef = TOTAL_VOTING_POWER as f64 / total_votes as f64;
126        let mut total_sum = 0;
127        for (idx, (_auth, weight)) in voting_weights.iter_mut().enumerate() {
128            if idx < num_nodes - 1 {
129                *weight = (*weight as f64 * normalization_coef).floor() as u64; // adjust the weights following the normalization coef
130                total_sum += *weight;
131            } else {
132                // the last element is taking all the rest
133                *weight = TOTAL_VOTING_POWER - total_sum;
134            }
135        }
136
137        Self::new(epoch, voting_weights)
138    }
139
140    // We call this if these have not yet been computed
141    pub fn load_inner(
142        voting_rights: &[(AuthorityName, StakeUnit)],
143    ) -> (
144        HashMap<AuthorityName, AggregateAuthorityPublicKey>,
145        HashMap<AuthorityName, usize>,
146    ) {
147        let expanded_keys: HashMap<AuthorityName, AggregateAuthorityPublicKey> = voting_rights
148            .iter()
149            .map(|(addr, _)| {
150                (
151                    *addr,
152                    (*addr)
153                        .try_into()
154                        .expect("Validator pubkey is always verified on-chain"),
155                )
156            })
157            .collect();
158
159        let index_map: HashMap<AuthorityName, usize> = voting_rights
160            .iter()
161            .enumerate()
162            .map(|(index, (addr, _))| (*addr, index))
163            .collect();
164        (expanded_keys, index_map)
165    }
166
167    pub fn authority_index(&self, author: &AuthorityName) -> Option<u32> {
168        self.index_map.get(author).map(|i| *i as u32)
169    }
170
171    pub fn authority_by_index(&self, index: u32) -> Option<&AuthorityName> {
172        self.voting_rights.get(index as usize).map(|(name, _)| name)
173    }
174
175    pub fn epoch(&self) -> EpochId {
176        self.epoch
177    }
178
179    pub fn public_key(
180        &self,
181        authority: &AuthorityName,
182    ) -> IotaResult<&AggregateAuthorityPublicKey> {
183        debug_assert_eq!(self.expanded_keys.len(), self.voting_rights.len());
184        match self.expanded_keys.get(authority) {
185            Some(v) => Ok(v),
186            None => Err(IotaError::InvalidCommittee(format!(
187                "Authority #{} not found, committee size {}",
188                authority,
189                self.expanded_keys.len()
190            ))),
191        }
192    }
193
194    /// Samples authorities by weight
195    pub fn sample(&self) -> &AuthorityName {
196        // unwrap safe unless committee is empty
197        Self::choose_multiple_weighted(&self.voting_rights[..], 1, &mut ThreadRng::default())
198            .next()
199            .unwrap()
200    }
201
202    /// Draws `count` authorities without replacement, with probability
203    /// proportional to stake.
204    ///
205    /// `portable_random` rather than `rand`'s own sampler because
206    /// `shuffle_by_stake_from_tx_digest` feeds this a digest-seeded RNG and
207    /// every validator has to reach the same order; `rand` does not promise
208    /// the same draws across versions. Weights are validated in `new`.
209    fn choose_multiple_weighted<'a>(
210        slice: &'a [(AuthorityName, StakeUnit)],
211        count: usize,
212        rng: &mut impl Rng,
213    ) -> impl Iterator<Item = &'a AuthorityName> {
214        portable_random::sample_weighted(rng, slice.len(), |i| slice[i].1 as f64, count)
215            .into_iter()
216            .map(|i| &slice[i].0)
217    }
218
219    pub fn choose_multiple_weighted_iter(
220        &self,
221        count: usize,
222    ) -> impl Iterator<Item = &AuthorityName> {
223        Self::choose_multiple_weighted(&self.voting_rights, count, &mut ThreadRng::default())
224    }
225
226    pub fn total_votes(&self) -> StakeUnit {
227        TOTAL_VOTING_POWER
228    }
229
230    pub fn quorum_threshold(&self) -> StakeUnit {
231        QUORUM_THRESHOLD
232    }
233
234    pub fn validity_threshold(&self) -> StakeUnit {
235        VALIDITY_THRESHOLD
236    }
237
238    pub fn threshold<const STRENGTH: bool>(&self) -> StakeUnit {
239        if STRENGTH {
240            QUORUM_THRESHOLD
241        } else {
242            VALIDITY_THRESHOLD
243        }
244    }
245
246    /// Calculates the effective threshold for protocol version selection.
247    /// This is the quorum threshold plus a buffer stake based on the given
248    /// basis points.
249    ///
250    /// The buffer is calculated as: f * buffer_stake_bps / 10000, rounded up
251    /// where f = total_votes - quorum_threshold
252    ///
253    /// buffer_stake_bps is clamped to a maximum of 10000.
254    pub fn effective_threshold(&self, mut buffer_stake_bps: u64) -> StakeUnit {
255        if buffer_stake_bps > 10000 {
256            buffer_stake_bps = 10000;
257        }
258        let quorum_threshold = self.quorum_threshold();
259        let f = self.total_votes() - quorum_threshold;
260        let buffer_stake = (f * buffer_stake_bps).div_ceil(10000);
261        quorum_threshold + buffer_stake
262    }
263
264    pub fn num_members(&self) -> usize {
265        self.voting_rights.len()
266    }
267
268    pub fn members(&self) -> impl Iterator<Item = &(AuthorityName, StakeUnit)> {
269        self.voting_rights.iter()
270    }
271
272    pub fn names(&self) -> impl Iterator<Item = &AuthorityName> {
273        self.voting_rights.iter().map(|(name, _)| name)
274    }
275
276    pub fn stakes(&self) -> impl Iterator<Item = StakeUnit> + '_ {
277        self.voting_rights.iter().map(|(_, stake)| *stake)
278    }
279
280    /// Returns the stake of the authority at the given index, or `None` if out
281    /// of bounds.
282    pub fn stake_by_index(&self, index: u32) -> Option<StakeUnit> {
283        self.voting_rights
284            .get(index as usize)
285            .map(|(_, stake)| *stake)
286    }
287
288    pub fn authority_exists(&self, name: &AuthorityName) -> bool {
289        self.voting_rights
290            .binary_search_by_key(name, |(a, _)| *a)
291            .is_ok()
292    }
293
294    /// Derive a seed deterministically from the transaction digest and shuffle
295    /// the validators.
296    pub fn shuffle_by_stake_from_tx_digest(
297        &self,
298        tx_digest: &TransactionDigest,
299    ) -> Vec<AuthorityName> {
300        let digest_bytes = tx_digest.into_bytes();
301
302        // Permute the validators deterministically from the digest. Named
303        // `ChaCha12Rng` rather than `StdRng`, whose algorithm `rand` is free to
304        // change between versions.
305        let mut rng = ChaCha12Rng::from_seed(digest_bytes);
306        self.shuffle_by_stake_with_rng(None, None, &mut rng)
307    }
308
309    // ===== Testing-only methods =====
310    //
311    pub fn new_simple_test_committee_of_size(size: usize) -> (Self, Vec<AuthorityKeyPair>) {
312        let key_pairs: Vec<_> = random_committee_key_pairs_of_size(size)
313            .into_iter()
314            .collect();
315        let committee = Self::new_for_testing_with_normalized_voting_power(
316            0,
317            key_pairs
318                .iter()
319                .map(|key| {
320                    (AuthorityName::from(key.public()), /* voting right */ 1)
321                })
322                .collect(),
323        );
324        (committee, key_pairs)
325    }
326
327    /// Generate a simple committee with 4 validators each with equal voting
328    /// stake of 1.
329    pub fn new_simple_test_committee() -> (Self, Vec<AuthorityKeyPair>) {
330        Self::new_simple_test_committee_of_size(4)
331    }
332}
333
334impl CommitteeTrait<AuthorityName> for Committee {
335    fn shuffle_by_stake_with_rng(
336        &self,
337        // try these authorities first
338        preferences: Option<&BTreeSet<AuthorityName>>,
339        // only attempt from these authorities.
340        restrict_to: Option<&BTreeSet<AuthorityName>>,
341        rng: &mut impl Rng,
342    ) -> Vec<AuthorityName> {
343        let restricted = self
344            .voting_rights
345            .iter()
346            .filter(|(name, _)| {
347                if let Some(restrict_to) = restrict_to {
348                    restrict_to.contains(name)
349                } else {
350                    true
351                }
352            })
353            .cloned();
354
355        let (preferred, rest): (Vec<_>, Vec<_>) = if let Some(preferences) = preferences {
356            restricted.partition(|(name, _)| preferences.contains(name))
357        } else {
358            (Vec::new(), restricted.collect())
359        };
360
361        Self::choose_multiple_weighted(&preferred, preferred.len(), rng)
362            .chain(Self::choose_multiple_weighted(&rest, rest.len(), rng))
363            .cloned()
364            .collect()
365    }
366
367    fn weight(&self, author: &AuthorityName) -> StakeUnit {
368        match self.voting_rights.binary_search_by_key(author, |(a, _)| *a) {
369            Err(_) => 0,
370            Ok(idx) => self.voting_rights[idx].1,
371        }
372    }
373}
374
375impl PartialEq for Committee {
376    fn eq(&self, other: &Self) -> bool {
377        self.epoch == other.epoch && self.voting_rights == other.voting_rights
378    }
379}
380
381impl Hash for Committee {
382    fn hash<H: Hasher>(&self, state: &mut H) {
383        self.epoch.hash(state);
384        self.voting_rights.hash(state);
385    }
386}
387
388impl Display for Committee {
389    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
390        let mut voting_rights = String::new();
391        for (name, vote) in &self.voting_rights {
392            write!(voting_rights, "{}: {}, ", name.concise(), vote)?;
393        }
394        write!(
395            f,
396            "Committee (epoch={:?}, voting_rights=[{}])",
397            self.epoch, voting_rights
398        )
399    }
400}
401
402pub trait CommitteeTrait<K: Ord> {
403    fn shuffle_by_stake_with_rng(
404        &self,
405        // try these authorities first
406        preferences: Option<&BTreeSet<K>>,
407        // only attempt from these authorities.
408        restrict_to: Option<&BTreeSet<K>>,
409        rng: &mut impl Rng,
410    ) -> Vec<K>;
411
412    fn shuffle_by_stake(
413        &self,
414        // try these authorities first
415        preferences: Option<&BTreeSet<K>>,
416        // only attempt from these authorities.
417        restrict_to: Option<&BTreeSet<K>>,
418    ) -> Vec<K> {
419        self.shuffle_by_stake_with_rng(preferences, restrict_to, &mut ThreadRng::default())
420    }
421
422    fn weight(&self, author: &K) -> StakeUnit;
423}
424
425#[derive(Clone, Debug, Serialize, Deserialize)]
426pub struct NetworkMetadata {
427    pub network_address: Multiaddr,
428    pub primary_address: Multiaddr,
429    pub network_public_key: Option<NetworkPublicKey>,
430}
431
432#[derive(Clone, Debug, Serialize, Deserialize)]
433pub struct CommitteeWithNetworkMetadata {
434    epoch_id: EpochId,
435    validators: BTreeMap<AuthorityName, (StakeUnit, NetworkMetadata)>,
436
437    #[serde(skip)]
438    committee: OnceCell<Committee>,
439}
440
441impl CommitteeWithNetworkMetadata {
442    pub fn new(
443        epoch_id: EpochId,
444        validators: BTreeMap<AuthorityName, (StakeUnit, NetworkMetadata)>,
445    ) -> Self {
446        Self {
447            epoch_id,
448            validators,
449            committee: OnceCell::new(),
450        }
451    }
452    pub fn epoch(&self) -> EpochId {
453        self.epoch_id
454    }
455
456    pub fn validators(&self) -> &BTreeMap<AuthorityName, (StakeUnit, NetworkMetadata)> {
457        &self.validators
458    }
459
460    pub fn committee(&self) -> &Committee {
461        self.committee.get_or_init(|| {
462            Committee::new(
463                self.epoch_id,
464                self.validators
465                    .iter()
466                    .map(|(name, (stake, _))| (*name, *stake))
467                    .collect(),
468            )
469        })
470    }
471}
472
473impl Display for CommitteeWithNetworkMetadata {
474    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
475        write!(
476            f,
477            "CommitteeWithNetworkMetadata (epoch={}, validators={:?})",
478            self.epoch_id, self.validators
479        )
480    }
481}
482
483/// Verifies the committee chain: the sequence of committees linked by each
484/// epoch's certified closing checkpoint, whose `end_of_epoch_data` elects the
485/// committee of the next epoch.
486///
487/// Starting from a trusted committee (typically the genesis committee, the
488/// operator's trust root), feed it each epoch's closing
489/// [`CertifiedCheckpointSummary`] in epoch order via
490/// [`Self::verify_epoch_close`]; every summary a consumer obtains this way is
491/// committee-verified, with no trust in whatever transport delivered it.
492///
493/// The walk is transport-agnostic by design — callers drive their own loop
494/// (an in-memory list, a remote-store stream, files on disk) and feed
495/// summaries in; this type only holds the verification state.
496#[derive(Debug)]
497pub struct CommitteeChainVerifier {
498    committee: Committee,
499}
500
501impl CommitteeChainVerifier {
502    /// Start the walk at a trusted committee — the trust root for everything
503    /// verified after it.
504    pub fn new(trusted_committee: Committee) -> Self {
505        Self {
506            committee: trusted_committee,
507        }
508    }
509
510    /// The epoch whose closing checkpoint must be fed next.
511    pub fn epoch(&self) -> EpochId {
512        self.committee.epoch
513    }
514
515    /// The committee of [`Self::epoch`] (trusted root or chain-verified).
516    pub fn committee(&self) -> &Committee {
517        &self.committee
518    }
519
520    /// Verify `summary` as the certified closing checkpoint of
521    /// [`Self::epoch`] and advance to the committee it elects for the next
522    /// epoch.
523    ///
524    /// Errors leave the verifier unchanged, e.g. if the summary is for a
525    /// different epoch, its signatures don't verify under the current
526    /// committee, or it is not a close-of-epoch checkpoint (no
527    /// `end_of_epoch_data`).
528    pub fn verify_epoch_close(
529        &mut self,
530        summary: CertifiedCheckpointSummary,
531    ) -> IotaResult<VerifiedCheckpoint> {
532        // The structural checks run before the (expensive) signature
533        // verification. They can only ever reject; nothing from the summary
534        // is trusted until the signatures verify.
535        if summary.data().epoch != self.committee.epoch {
536            return Err(IotaError::WrongEpoch {
537                expected_epoch: self.committee.epoch,
538                actual_epoch: summary.data().epoch,
539            });
540        }
541
542        if summary.data().end_of_epoch_data.is_none() {
543            return Err(IotaError::GenericAuthority {
544                error: format!(
545                    "checkpoint {} is not the closing checkpoint of epoch {} (no \
546                     end-of-epoch data)",
547                    summary.data().sequence_number,
548                    self.committee.epoch,
549                ),
550            });
551        }
552
553        let verified = summary.try_into_verified(&self.committee)?;
554        let end_of_epoch_data = verified
555            .end_of_epoch_data
556            .as_ref()
557            .expect("checked before verification");
558
559        self.committee = Committee::from_committee_members(
560            self.committee
561                .epoch
562                .checked_add(1)
563                .ok_or(IotaError::AdvanceEpoch {
564                    error: "epoch number overflow".to_string(),
565                })?,
566            &end_of_epoch_data.next_epoch_committee,
567        );
568        Ok(verified)
569    }
570}
571
572#[cfg(test)]
573mod test {
574    use fastcrypto::traits::KeyPair;
575    use iota_sdk_types::{CheckpointSummary, EndOfEpochData};
576
577    use super::*;
578    use crate::{
579        crypto::{AuthorityKeyPair, get_key_pair},
580        messages_checkpoint::SignedCheckpointSummary,
581        utils::make_committee_key,
582    };
583
584    const RNG_SEED: [u8; 32] = [
585        21, 23, 199, 200, 234, 250, 252, 178, 94, 15, 202, 178, 62, 186, 88, 137, 233, 192, 130,
586        157, 179, 179, 65, 9, 31, 249, 221, 123, 225, 112, 199, 247,
587    ];
588
589    #[test]
590    fn test_shuffle_by_weight() {
591        let (_, sec1): (_, AuthorityKeyPair) = get_key_pair();
592        let (_, sec2): (_, AuthorityKeyPair) = get_key_pair();
593        let (_, sec3): (_, AuthorityKeyPair) = get_key_pair();
594        let a1: AuthorityName = sec1.public().into();
595        let a2: AuthorityName = sec2.public().into();
596        let a3: AuthorityName = sec3.public().into();
597
598        let mut authorities = BTreeMap::new();
599        authorities.insert(a1, 1);
600        authorities.insert(a2, 1);
601        authorities.insert(a3, 1);
602
603        let committee = Committee::new_for_testing_with_normalized_voting_power(0, authorities);
604
605        assert_eq!(committee.shuffle_by_stake(None, None).len(), 3);
606
607        let mut pref = BTreeSet::new();
608        pref.insert(a2);
609
610        // preference always comes first
611        for _ in 0..100 {
612            assert_eq!(
613                a2,
614                *committee
615                    .shuffle_by_stake(Some(&pref), None)
616                    .first()
617                    .unwrap()
618            );
619        }
620
621        let mut restrict = BTreeSet::new();
622        restrict.insert(a2);
623
624        for _ in 0..100 {
625            let res = committee.shuffle_by_stake(None, Some(&restrict));
626            assert_eq!(1, res.len());
627            assert_eq!(a2, res[0]);
628        }
629
630        // empty preferences are valid
631        let res = committee.shuffle_by_stake(Some(&BTreeSet::new()), None);
632        assert_eq!(3, res.len());
633
634        let res = committee.shuffle_by_stake(None, Some(&BTreeSet::new()));
635        assert_eq!(0, res.len());
636    }
637
638    /// `CommitteeChainVerifier` accepts a correctly signed chain of closing
639    /// checkpoints, advancing one committee per epoch — and rejects, without
640    /// advancing, a summary for the wrong epoch, one signed by a different
641    /// committee, and one that is not a close of epoch.
642    #[test]
643    fn committee_chain_verifier_walks_and_rejects() {
644        let mut rng = rand::rngs::StdRng::from_seed(RNG_SEED);
645        let (keys, committee) = make_committee_key(&mut rng);
646        let (other_keys, other_committee) = make_committee_key(&mut rng);
647
648        let close_of_epoch = |epoch: EpochId, end_of_epoch_data: Option<EndOfEpochData>| {
649            let summary = CheckpointSummary {
650                epoch,
651                sequence_number: epoch,
652                network_total_transactions: 0,
653                contents_digest: Default::default(),
654                previous_digest: None,
655                epoch_rolling_gas_cost_summary: Default::default(),
656                end_of_epoch_data,
657                timestamp_ms: 0,
658                version_specific_data: Vec::new(),
659                checkpoint_commitments: Vec::new(),
660            };
661            let signatures = keys
662                .iter()
663                .map(|k| SignedCheckpointSummary::sign(epoch, &summary, k, k.public().into()))
664                .collect();
665            let committee_at_epoch =
666                Committee::new(epoch, committee.voting_rights.iter().cloned().collect());
667            CertifiedCheckpointSummary::new(summary, signatures, &committee_at_epoch)
668                .expect("test summary must certify")
669        };
670        let handing_forward = Some(EndOfEpochData {
671            next_epoch_committee: committee.committee_members(),
672            next_epoch_protocol_version: 1,
673            epoch_commitments: Vec::new(),
674            epoch_supply_change: 0,
675        });
676
677        let mut verifier = CommitteeChainVerifier::new(committee.clone());
678
679        // Wrong epoch: epoch 1's close fed while epoch 0 is expected.
680        assert!(matches!(
681            verifier.verify_epoch_close(close_of_epoch(1, handing_forward.clone())),
682            Err(IotaError::WrongEpoch { .. })
683        ));
684        assert_eq!(verifier.epoch(), 0, "a rejected summary must not advance");
685
686        // Not a close of epoch.
687        verifier
688            .verify_epoch_close(close_of_epoch(0, None))
689            .expect_err("a non-closing checkpoint must be rejected");
690        assert_eq!(verifier.epoch(), 0);
691
692        // The close-of-epoch check runs before the (expensive) signature
693        // verification: a non-closing summary certified by a foreign
694        // committee yields the structural error, not a signature error.
695        let foreign_non_closing = {
696            let summary = CheckpointSummary {
697                epoch: 0,
698                sequence_number: 0,
699                network_total_transactions: 0,
700                contents_digest: Default::default(),
701                previous_digest: None,
702                epoch_rolling_gas_cost_summary: Default::default(),
703                end_of_epoch_data: None,
704                timestamp_ms: 0,
705                version_specific_data: Vec::new(),
706                checkpoint_commitments: Vec::new(),
707            };
708            let signatures = other_keys
709                .iter()
710                .map(|k| SignedCheckpointSummary::sign(0, &summary, k, k.public().into()))
711                .collect();
712            CertifiedCheckpointSummary::new(summary, signatures, &other_committee)
713                .expect("certifies under the foreign committee")
714        };
715        assert!(matches!(
716            verifier.verify_epoch_close(foreign_non_closing),
717            Err(IotaError::GenericAuthority { .. })
718        ));
719        assert_eq!(verifier.epoch(), 0);
720
721        // The valid chain walks: epoch 0 then epoch 1.
722        verifier
723            .verify_epoch_close(close_of_epoch(0, handing_forward.clone()))
724            .expect("epoch 0 close must verify");
725        assert_eq!(verifier.epoch(), 1);
726        verifier
727            .verify_epoch_close(close_of_epoch(1, handing_forward.clone()))
728            .expect("epoch 1 close must verify");
729        assert_eq!(verifier.epoch(), 2);
730
731        // A different trust root rejects the same (validly signed) chain.
732        let mut wrong_root = CommitteeChainVerifier::new(other_committee);
733        wrong_root
734            .verify_epoch_close(close_of_epoch(0, handing_forward))
735            .expect_err("a chain signed by a different committee must be rejected");
736        assert_eq!(wrong_root.epoch(), 0);
737    }
738}