linera_chain/
manager.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! # Chain manager
5//!
6//! This module contains the consensus mechanism for all microchains. Whenever a block is
7//! confirmed, a new chain manager is created for the next block height. It manages the consensus
8//! state until a new block is confirmed. As long as less than a third of the validators are faulty,
9//! it guarantees that at most one `ConfirmedBlock` certificate will be created for this height.
10//!
11//! The protocol proceeds in rounds, until it reaches a round where a block gets confirmed.
12//!
13//! There are four kinds of rounds:
14//!
15//! * In `Round::Fast`, only super owners can propose blocks, and validators vote to confirm a
16//!   block immediately. Super owners must be careful to make only one block proposal, or else they
17//!   can permanently block the microchain. If there are no super owners, `Round::Fast` is skipped.
18//! * In cooperative mode (`Round::MultiLeader`), all chain owners can propose blocks at any time.
19//!   The protocol is guaranteed to eventually confirm a block as long as no chain owner
20//!   continuously actively prevents progress.
21//! * In leader rotation mode (`Round::SingleLeader`), chain owners take turns at proposing blocks.
22//!   It can make progress as long as at least one owner is honest, even if other owners try to
23//!   prevent it.
24//! * In fallback/public mode (`Round::Validator`), validators take turns at proposing blocks.
25//!   It can always make progress under the standard assumption that there is a quorum of honest
26//!   validators.
27//!
28//! ## Safety, i.e. at most one block will be confirmed
29//!
30//! In all modes this is guaranteed as follows:
31//!
32//! * Validators (honest ones) never cast a vote if they have already cast any vote in a later
33//!   round.
34//! * Validators never vote for a `ValidatedBlock` **A** in round **r** if they have voted for a
35//!   _different_ `ConfirmedBlock` **B** in an earlier round **s** ≤ **r**, unless there is a
36//!   `ValidatedBlock` certificate (with a quorum of validator signatures) for **A** in some round
37//!   between **s** and **r** included in the block proposal.
38//! * Validators only vote for a `ConfirmedBlock` if there is a `ValidatedBlock` certificate for the
39//!   same block in the same round. (Or, in the `Fast` round, if there is a valid proposal.)
40//!
41//! This guarantees that once a quorum votes for some `ConfirmedBlock`, there can never be a
42//! `ValidatedBlock` certificate (and thus also no `ConfirmedBlock` certificate) for a different
43//! block in a later round. So if there are two different `ConfirmedBlock` certificates, they may
44//! be from different rounds, but they are guaranteed to contain the same block.
45//!
46//! ## Liveness, i.e. some block will eventually be confirmed
47//!
48//! In `Round::Fast`, liveness depends on the super owners coordinating, and proposing at most one
49//! block.
50//!
51//! If they propose none, and there are other owners, `Round::Fast` will eventually time out.
52//!
53//! In cooperative mode, if there is contention, the owners need to agree on a single owner as the
54//! next proposer. That owner should then download all highest-round certificates and block
55//! proposals known to the honest validators. They can then make a proposal in a round higher than
56//! all previous proposals. If there is any `ValidatedBlock` certificate they must include the
57//! highest one in their proposal, and propose that block. Otherwise they can propose a new block.
58//! Now all honest validators are allowed to vote for that proposal, and eventually confirm it.
59//!
60//! If the owners fail to cooperate, any honest owner can initiate the last multi-leader round by
61//! making a proposal there, then wait for it to time out, which starts the leader-based mode:
62//!
63//! In leader-based and fallback/public mode, an honest participant should subscribe to
64//! notifications from all validators, and follow the chain. Whenever another leader's round takes
65//! too long, they should request timeout votes from the validators to make the next round begin.
66//! Once the honest participant becomes the round leader, they should update all validators, so
67//! that they all agree on the current round. Then they download the highest `ValidatedBlock`
68//! certificate known to any honest validator and include that in their block proposal, just like
69//! in the cooperative case.
70
71use std::collections::BTreeMap;
72
73use async_graphql::{ComplexObject, SimpleObject};
74use custom_debug_derive::Debug;
75use futures::future::Either;
76use linera_base::{
77    crypto::{AccountPublicKey, ValidatorSecretKey},
78    data_types::{Blob, BlockHeight, Round, Timestamp},
79    ensure,
80    identifiers::{AccountOwner, BlobId, ChainId},
81    ownership::ChainOwnership,
82};
83use linera_execution::{committee::Epoch, ExecutionRuntimeContext};
84use linera_views::{
85    context::Context,
86    map_view::MapView,
87    register_view::RegisterView,
88    views::{ClonableView, View, ViewError},
89};
90use rand_chacha::{rand_core::SeedableRng, ChaCha8Rng};
91use rand_distr::{Distribution, WeightedAliasIndex};
92use serde::{Deserialize, Serialize};
93
94use crate::{
95    block::{Block, ConfirmedBlock, Timeout, ValidatedBlock},
96    data_types::{BlockProposal, LiteVote, ProposedBlock, Vote},
97    types::{TimeoutCertificate, ValidatedBlockCertificate},
98    ChainError,
99};
100
101/// The result of verifying a (valid) query.
102#[derive(Eq, PartialEq)]
103pub enum Outcome {
104    Accept,
105    Skip,
106}
107
108pub type ValidatedOrConfirmedVote<'a> = Either<&'a Vote<ValidatedBlock>, &'a Vote<ConfirmedBlock>>;
109
110/// The latest block that validators may have voted to confirm: this is either the block proposal
111/// from the fast round or a validated block certificate. Validators are allowed to vote for this
112/// even if they have locked (i.e. voted to confirm) a different block earlier.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[cfg_attr(with_testing, derive(Eq, PartialEq))]
115pub enum LockingBlock {
116    /// A proposal in the `Fast` round.
117    Fast(BlockProposal),
118    /// A `ValidatedBlock` certificate in a round other than `Fast`.
119    Regular(ValidatedBlockCertificate),
120}
121
122impl LockingBlock {
123    /// Returns the locking block's round. To propose a different block, a `ValidatedBlock`
124    /// certificate from a higher round is needed.
125    pub fn round(&self) -> Round {
126        match self {
127            Self::Fast(_) => Round::Fast,
128            Self::Regular(certificate) => certificate.round,
129        }
130    }
131
132    pub fn chain_id(&self) -> ChainId {
133        match self {
134            Self::Fast(proposal) => proposal.content.block.chain_id,
135            Self::Regular(certificate) => certificate.value().chain_id(),
136        }
137    }
138}
139
140/// The state of the certification process for a chain's next block.
141#[derive(Debug, View, ClonableView, SimpleObject)]
142#[graphql(complex)]
143pub struct ChainManager<C>
144where
145    C: Clone + Context + Send + Sync + 'static,
146{
147    /// The public keys, weights and types of the chain's owners.
148    pub ownership: RegisterView<C, ChainOwnership>,
149    /// The seed for the pseudo-random number generator that determines the round leaders.
150    pub seed: RegisterView<C, u64>,
151    /// The probability distribution for choosing a round leader.
152    #[graphql(skip)] // Derived from ownership.
153    pub distribution: RegisterView<C, Option<WeightedAliasIndex<u64>>>,
154    /// The probability distribution for choosing a fallback round leader.
155    #[graphql(skip)] // Derived from validator weights.
156    pub fallback_distribution: RegisterView<C, Option<WeightedAliasIndex<u64>>>,
157    /// Highest-round authenticated block that we have received and checked. If there are multiple
158    /// proposals in the same round, this contains only the first one.
159    #[graphql(skip)]
160    pub proposed: RegisterView<C, Option<BlockProposal>>,
161    /// These are blobs published or read by the proposed block.
162    pub proposed_blobs: MapView<C, BlobId, Blob>,
163    /// Latest validated proposal that a validator may have voted to confirm. This is either the
164    /// latest `ValidatedBlock` we have seen, or the proposal from the `Fast` round.
165    #[graphql(skip)]
166    pub locking_block: RegisterView<C, Option<LockingBlock>>,
167    /// These are blobs published or read by the locking block.
168    pub locking_blobs: MapView<C, BlobId, Blob>,
169    /// Latest leader timeout certificate we have received.
170    #[graphql(skip)]
171    pub timeout: RegisterView<C, Option<TimeoutCertificate>>,
172    /// Latest vote we cast to confirm a block.
173    #[graphql(skip)]
174    pub confirmed_vote: RegisterView<C, Option<Vote<ConfirmedBlock>>>,
175    /// Latest vote we cast to validate a block.
176    #[graphql(skip)]
177    pub validated_vote: RegisterView<C, Option<Vote<ValidatedBlock>>>,
178    /// Latest timeout vote we cast.
179    #[graphql(skip)]
180    pub timeout_vote: RegisterView<C, Option<Vote<Timeout>>>,
181    /// Fallback vote we cast.
182    #[graphql(skip)]
183    pub fallback_vote: RegisterView<C, Option<Vote<Timeout>>>,
184    /// The time after which we are ready to sign a timeout certificate for the current round.
185    pub round_timeout: RegisterView<C, Option<Timestamp>>,
186    /// The lowest round where we can still vote to validate or confirm a block. This is
187    /// the round to which the timeout applies.
188    ///
189    /// Having a leader timeout certificate in any given round causes the next one to become
190    /// current. Seeing a validated block certificate or a valid proposal in any round causes that
191    /// round to become current, unless a higher one already is.
192    #[graphql(skip)]
193    pub current_round: RegisterView<C, Round>,
194    /// The owners that take over in fallback mode.
195    pub fallback_owners: RegisterView<C, BTreeMap<AccountOwner, u64>>,
196}
197
198#[ComplexObject]
199impl<C> ChainManager<C>
200where
201    C: Context + Clone + Send + Sync + 'static,
202{
203    /// Returns the lowest round where we can still vote to validate or confirm a block. This is
204    /// the round to which the timeout applies.
205    ///
206    /// Having a leader timeout certificate in any given round causes the next one to become
207    /// current. Seeing a validated block certificate or a valid proposal in any round causes that
208    /// round to become current, unless a higher one already is.
209    #[graphql(derived(name = "current_round"))]
210    async fn _current_round(&self) -> Round {
211        self.current_round()
212    }
213}
214
215impl<C> ChainManager<C>
216where
217    C: Context + Clone + Send + Sync + 'static,
218{
219    /// Replaces `self` with a new chain manager.
220    pub fn reset<'a>(
221        &mut self,
222        ownership: ChainOwnership,
223        height: BlockHeight,
224        local_time: Timestamp,
225        fallback_owners: impl Iterator<Item = (AccountPublicKey, u64)> + 'a,
226    ) -> Result<(), ChainError> {
227        let distribution = if !ownership.owners.is_empty() {
228            let weights = ownership.owners.values().copied().collect();
229            Some(WeightedAliasIndex::new(weights)?)
230        } else {
231            None
232        };
233        let fallback_owners = fallback_owners
234            .map(|(pub_key, weight)| (AccountOwner::from(pub_key), weight))
235            .collect::<BTreeMap<_, _>>();
236        let fallback_distribution = if !fallback_owners.is_empty() {
237            let weights = fallback_owners.values().copied().collect();
238            Some(WeightedAliasIndex::new(weights)?)
239        } else {
240            None
241        };
242
243        let current_round = ownership.first_round();
244        let round_duration = ownership.round_timeout(current_round);
245        let round_timeout = round_duration.map(|rd| local_time.saturating_add(rd));
246
247        self.clear();
248        self.seed.set(height.0);
249        self.ownership.set(ownership);
250        self.distribution.set(distribution);
251        self.fallback_distribution.set(fallback_distribution);
252        self.fallback_owners.set(fallback_owners);
253        self.current_round.set(current_round);
254        self.round_timeout.set(round_timeout);
255        Ok(())
256    }
257
258    /// Returns the most recent confirmed vote we cast.
259    pub fn confirmed_vote(&self) -> Option<&Vote<ConfirmedBlock>> {
260        self.confirmed_vote.get().as_ref()
261    }
262
263    /// Returns the most recent validated vote we cast.
264    pub fn validated_vote(&self) -> Option<&Vote<ValidatedBlock>> {
265        self.validated_vote.get().as_ref()
266    }
267
268    /// Returns the most recent timeout vote we cast.
269    pub fn timeout_vote(&self) -> Option<&Vote<Timeout>> {
270        self.timeout_vote.get().as_ref()
271    }
272
273    /// Returns the most recent fallback vote we cast.
274    pub fn fallback_vote(&self) -> Option<&Vote<Timeout>> {
275        self.fallback_vote.get().as_ref()
276    }
277
278    /// Returns the lowest round where we can still vote to validate or confirm a block. This is
279    /// the round to which the timeout applies.
280    ///
281    /// Having a leader timeout certificate in any given round causes the next one to become
282    /// current. Seeing a validated block certificate or a valid proposal in any round causes that
283    /// round to become current, unless a higher one already is.
284    pub fn current_round(&self) -> Round {
285        *self.current_round.get()
286    }
287
288    /// Verifies that a proposed block is relevant and should be handled.
289    pub fn check_proposed_block(&self, proposal: &BlockProposal) -> Result<Outcome, ChainError> {
290        let new_block = &proposal.content.block;
291        let new_round = proposal.content.round;
292        if let Some(old_proposal) = self.proposed.get() {
293            if old_proposal.content == proposal.content {
294                return Ok(Outcome::Skip); // We have already seen this proposal; nothing to do.
295            }
296        }
297        // When a block is certified, incrementing its height must succeed.
298        ensure!(
299            new_block.height < BlockHeight::MAX,
300            ChainError::InvalidBlockHeight
301        );
302        let current_round = self.current_round();
303        match new_round {
304            // The proposal from the fast round may still be relevant as a locking block, so
305            // we don't compare against the current round here.
306            Round::Fast => {}
307            Round::MultiLeader(_) | Round::SingleLeader(0) => {
308                // If the fast round has not timed out yet, only a super owner is allowed to open
309                // a later round by making a proposal.
310                ensure!(
311                    self.is_super(&proposal.public_key.into()) || !current_round.is_fast(),
312                    ChainError::WrongRound(current_round)
313                );
314                // After the fast round, proposals older than the current round are obsolete.
315                ensure!(
316                    new_round >= current_round,
317                    ChainError::InsufficientRound(new_round)
318                );
319            }
320            Round::SingleLeader(_) | Round::Validator(_) => {
321                // After the first single-leader round, only proposals from the current round are relevant.
322                ensure!(
323                    new_round == current_round,
324                    ChainError::WrongRound(current_round)
325                );
326            }
327        }
328        // The round of our validation votes is only allowed to increase.
329        if let Some(vote) = self.validated_vote() {
330            ensure!(
331                new_round > vote.round,
332                ChainError::InsufficientRoundStrict(vote.round)
333            );
334        }
335        // A proposal that isn't newer than the locking block is not relevant anymore.
336        if let Some(locking_block) = self.locking_block.get() {
337            ensure!(
338                locking_block.round() < new_round,
339                ChainError::MustBeNewerThanLockingBlock(new_block.height, locking_block.round())
340            );
341        }
342        // If we have voted to confirm we cannot vote to validate a different block anymore, except
343        // if there is a validated block certificate from a later round.
344        if let Some(vote) = self.confirmed_vote() {
345            ensure!(
346                if let Some(validated_cert) = proposal.validated_block_certificate.as_ref() {
347                    vote.round <= validated_cert.round
348                } else {
349                    vote.round.is_fast() && vote.value().matches_proposed_block(new_block)
350                },
351                ChainError::HasIncompatibleConfirmedVote(new_block.height, vote.round)
352            );
353        }
354        Ok(Outcome::Accept)
355    }
356
357    /// Checks if the current round has timed out, and signs a `Timeout`.
358    pub fn vote_timeout(
359        &mut self,
360        chain_id: ChainId,
361        height: BlockHeight,
362        epoch: Epoch,
363        key_pair: Option<&ValidatorSecretKey>,
364        local_time: Timestamp,
365    ) -> bool {
366        let Some(key_pair) = key_pair else {
367            return false; // We are not a validator.
368        };
369        let Some(round_timeout) = *self.round_timeout.get() else {
370            return false; // The current round does not time out.
371        };
372        if local_time < round_timeout || self.ownership.get().owners.is_empty() {
373            return false; // Round has not timed out yet, or there are no regular owners.
374        }
375        let current_round = self.current_round();
376        if let Some(vote) = self.timeout_vote.get() {
377            if vote.round == current_round {
378                return false; // We already signed this timeout.
379            }
380        }
381        let value = Timeout::new(chain_id, height, epoch);
382        self.timeout_vote
383            .set(Some(Vote::new(value, current_round, key_pair)));
384        true
385    }
386
387    /// Signs a `Timeout` certificate to switch to fallback mode.
388    ///
389    /// This must only be called after verifying that the condition for fallback mode is
390    /// satisfied locally.
391    pub fn vote_fallback(
392        &mut self,
393        chain_id: ChainId,
394        height: BlockHeight,
395        epoch: Epoch,
396        key_pair: Option<&ValidatorSecretKey>,
397    ) -> bool {
398        let Some(key_pair) = key_pair else {
399            return false; // We are not a validator.
400        };
401        if self.fallback_vote.get().is_some() || self.current_round() >= Round::Validator(0) {
402            return false; // We already signed this or are already in fallback mode.
403        }
404        let value = Timeout::new(chain_id, height, epoch);
405        let last_regular_round = Round::SingleLeader(u32::MAX);
406        self.fallback_vote
407            .set(Some(Vote::new(value, last_regular_round, key_pair)));
408        true
409    }
410
411    /// Verifies that a validated block is still relevant and should be handled.
412    pub fn check_validated_block(
413        &self,
414        certificate: &ValidatedBlockCertificate,
415    ) -> Result<Outcome, ChainError> {
416        let new_block = certificate.block();
417        let new_round = certificate.round;
418        if let Some(Vote { value, round, .. }) = self.confirmed_vote.get() {
419            if value.block() == new_block && *round == new_round {
420                return Ok(Outcome::Skip); // We already voted to confirm this block.
421            }
422        }
423
424        // Check if we already voted to validate in a later round.
425        if let Some(Vote { round, .. }) = self.validated_vote.get() {
426            ensure!(new_round >= *round, ChainError::InsufficientRound(*round))
427        }
428
429        if let Some(locking) = self.locking_block.get() {
430            if let LockingBlock::Regular(locking_cert) = locking {
431                if locking_cert.hash() == certificate.hash() && locking.round() == new_round {
432                    return Ok(Outcome::Skip); // We already handled this certificate.
433                }
434            }
435            ensure!(
436                new_round > locking.round(),
437                ChainError::InsufficientRoundStrict(locking.round())
438            );
439        }
440        Ok(Outcome::Accept)
441    }
442
443    /// Signs a vote to validate the proposed block.
444    pub fn create_vote(
445        &mut self,
446        proposal: BlockProposal,
447        block: Block,
448        key_pair: Option<&ValidatorSecretKey>,
449        local_time: Timestamp,
450        blobs: BTreeMap<BlobId, Blob>,
451    ) -> Result<Option<ValidatedOrConfirmedVote>, ChainError> {
452        let round = proposal.content.round;
453
454        // If the validated block certificate is more recent, update our locking block.
455        if let Some(lite_cert) = &proposal.validated_block_certificate {
456            if self
457                .locking_block
458                .get()
459                .as_ref()
460                .is_none_or(|locking| locking.round() < lite_cert.round)
461            {
462                let value = ValidatedBlock::new(block.clone());
463                if let Some(certificate) = lite_cert.clone().with_value(value) {
464                    self.update_locking(LockingBlock::Regular(certificate), blobs.clone())?;
465                }
466            }
467        } else if round.is_fast() && self.locking_block.get().is_none() {
468            // The fast block also counts as locking.
469            self.update_locking(LockingBlock::Fast(proposal.clone()), blobs.clone())?;
470        }
471
472        // We record the proposed block, in case it affects the current round number.
473        self.update_proposed(proposal.clone(), blobs)?;
474        self.update_current_round(local_time);
475
476        let Some(key_pair) = key_pair else {
477            // Not a validator.
478            return Ok(None);
479        };
480
481        // If this is a fast block, vote to confirm. Otherwise vote to validate.
482        if round.is_fast() {
483            self.validated_vote.set(None);
484            let value = ConfirmedBlock::new(block);
485            let vote = Vote::new(value, round, key_pair);
486            Ok(Some(Either::Right(
487                self.confirmed_vote.get_mut().insert(vote),
488            )))
489        } else {
490            let value = ValidatedBlock::new(block);
491            let vote = Vote::new(value, round, key_pair);
492            Ok(Some(Either::Left(
493                self.validated_vote.get_mut().insert(vote),
494            )))
495        }
496    }
497
498    /// Signs a vote to confirm the validated block.
499    pub fn create_final_vote(
500        &mut self,
501        validated: ValidatedBlockCertificate,
502        key_pair: Option<&ValidatorSecretKey>,
503        local_time: Timestamp,
504        blobs: BTreeMap<BlobId, Blob>,
505    ) -> Result<(), ViewError> {
506        let round = validated.round;
507        let confirmed_block = ConfirmedBlock::new(validated.inner().block().clone());
508        self.update_locking(LockingBlock::Regular(validated), blobs)?;
509        self.update_current_round(local_time);
510        if let Some(key_pair) = key_pair {
511            if self.current_round() != round {
512                return Ok(()); // We never vote in a past round.
513            }
514            // Vote to confirm.
515            let vote = Vote::new(confirmed_block, round, key_pair);
516            // Ok to overwrite validation votes with confirmation votes at equal or higher round.
517            self.confirmed_vote.set(Some(vote));
518            self.validated_vote.set(None);
519        }
520        Ok(())
521    }
522
523    /// Returns the requested blob if it belongs to the proposal or the locking block.
524    pub async fn pending_blob(&self, blob_id: &BlobId) -> Result<Option<Blob>, ViewError> {
525        if let Some(blob) = self.proposed_blobs.get(blob_id).await? {
526            return Ok(Some(blob));
527        }
528        self.locking_blobs.get(blob_id).await
529    }
530
531    /// Updates `current_round` and `round_timeout` if necessary.
532    ///
533    /// This must be after every change to `timeout`, `locking` or `proposed`.
534    fn update_current_round(&mut self, local_time: Timestamp) {
535        let current_round = self
536            .timeout
537            .get()
538            .iter()
539            .map(|certificate| {
540                self.ownership
541                    .get()
542                    .next_round(certificate.round)
543                    .unwrap_or(Round::Validator(u32::MAX))
544            })
545            .chain(self.locking_block.get().as_ref().map(LockingBlock::round))
546            .chain(
547                self.proposed
548                    .get()
549                    .iter()
550                    .map(|proposal| proposal.content.round),
551            )
552            .max()
553            .unwrap_or_default()
554            .max(self.ownership.get().first_round());
555        if current_round <= self.current_round() {
556            return;
557        }
558        let round_duration = self.ownership.get().round_timeout(current_round);
559        self.round_timeout
560            .set(round_duration.map(|rd| local_time.saturating_add(rd)));
561        self.current_round.set(current_round);
562    }
563
564    /// Updates the round number and timer if the timeout certificate is from a higher round than
565    /// any known certificate.
566    pub fn handle_timeout_certificate(
567        &mut self,
568        certificate: TimeoutCertificate,
569        local_time: Timestamp,
570    ) {
571        let round = certificate.round;
572        if let Some(known_certificate) = self.timeout.get() {
573            if known_certificate.round >= round {
574                return;
575            }
576        }
577        self.timeout.set(Some(certificate));
578        self.update_current_round(local_time);
579    }
580
581    /// Returns whether the signer is a valid owner and allowed to propose a block in the
582    /// proposal's round.
583    pub fn verify_owner(&self, proposal: &BlockProposal) -> bool {
584        let owner = &proposal.public_key.into();
585        if self.ownership.get().super_owners.contains(owner) {
586            return true;
587        }
588        match proposal.content.round {
589            Round::Fast => {
590                false // Only super owners can propose in the first round.
591            }
592            Round::MultiLeader(_) => {
593                let ownership = self.ownership.get();
594                // Not in leader rotation mode; any owner is allowed to propose.
595                ownership.open_multi_leader_rounds || ownership.owners.contains_key(owner)
596            }
597            Round::SingleLeader(r) => {
598                let Some(index) = self.round_leader_index(r) else {
599                    return false;
600                };
601                self.ownership.get().owners.keys().nth(index) == Some(owner)
602            }
603            Round::Validator(r) => {
604                let Some(index) = self.fallback_round_leader_index(r) else {
605                    return false;
606                };
607                self.fallback_owners.get().keys().nth(index) == Some(owner)
608            }
609        }
610    }
611
612    /// Returns the leader who is allowed to propose a block in the given round, or `None` if every
613    /// owner is allowed to propose. Exception: In `Round::Fast`, only super owners can propose.
614    fn round_leader(&self, round: Round) -> Option<&AccountOwner> {
615        match round {
616            Round::SingleLeader(r) => {
617                let index = self.round_leader_index(r)?;
618                self.ownership.get().owners.keys().nth(index)
619            }
620            Round::Validator(r) => {
621                let index = self.fallback_round_leader_index(r)?;
622                self.fallback_owners.get().keys().nth(index)
623            }
624            Round::Fast | Round::MultiLeader(_) => None,
625        }
626    }
627
628    /// Returns the index of the leader who is allowed to propose a block in the given round.
629    fn round_leader_index(&self, round: u32) -> Option<usize> {
630        let seed = u64::from(round)
631            .rotate_left(32)
632            .wrapping_add(*self.seed.get());
633        let mut rng = ChaCha8Rng::seed_from_u64(seed);
634        Some(self.distribution.get().as_ref()?.sample(&mut rng))
635    }
636
637    /// Returns the index of the fallback leader who is allowed to propose a block in the given
638    /// round.
639    fn fallback_round_leader_index(&self, round: u32) -> Option<usize> {
640        let seed = u64::from(round)
641            .rotate_left(32)
642            .wrapping_add(*self.seed.get());
643        let mut rng = ChaCha8Rng::seed_from_u64(seed);
644        Some(self.fallback_distribution.get().as_ref()?.sample(&mut rng))
645    }
646
647    /// Returns whether the owner is a super owner.
648    fn is_super(&self, owner: &AccountOwner) -> bool {
649        self.ownership.get().super_owners.contains(owner)
650    }
651
652    /// Sets the proposed block, if it is newer than our known latest proposal.
653    fn update_proposed(
654        &mut self,
655        proposal: BlockProposal,
656        blobs: BTreeMap<BlobId, Blob>,
657    ) -> Result<(), ViewError> {
658        if let Some(old_proposal) = self.proposed.get() {
659            if old_proposal.content.round >= proposal.content.round {
660                return Ok(());
661            }
662        }
663        self.proposed.set(Some(proposal));
664        self.proposed_blobs.clear();
665        for (blob_id, blob) in blobs {
666            self.proposed_blobs.insert(&blob_id, blob)?;
667        }
668        Ok(())
669    }
670
671    /// Sets the locking block and the associated blobs, if it is newer than the known one.
672    fn update_locking(
673        &mut self,
674        locking: LockingBlock,
675        blobs: BTreeMap<BlobId, Blob>,
676    ) -> Result<(), ViewError> {
677        if let Some(old_locked) = self.locking_block.get() {
678            if old_locked.round() >= locking.round() {
679                return Ok(());
680            }
681        }
682        self.locking_block.set(Some(locking));
683        self.locking_blobs.clear();
684        for (blob_id, blob) in blobs {
685            self.locking_blobs.insert(&blob_id, blob)?;
686        }
687        Ok(())
688    }
689}
690
691/// Chain manager information that is included in `ChainInfo` sent to clients.
692#[derive(Default, Clone, Debug, Serialize, Deserialize)]
693#[cfg_attr(with_testing, derive(Eq, PartialEq))]
694pub struct ChainManagerInfo {
695    /// The configuration of the chain's owners.
696    pub ownership: ChainOwnership,
697    /// Latest authenticated block that we have received, if requested.
698    #[debug(skip_if = Option::is_none)]
699    pub requested_proposed: Option<Box<BlockProposal>>,
700    /// Latest validated proposal that we have voted to confirm (or would have, if we are not a
701    /// validator).
702    #[debug(skip_if = Option::is_none)]
703    pub requested_locking: Option<Box<LockingBlock>>,
704    /// Latest timeout certificate we have seen.
705    #[debug(skip_if = Option::is_none)]
706    pub timeout: Option<Box<TimeoutCertificate>>,
707    /// Latest vote we cast (either to validate or to confirm a block).
708    #[debug(skip_if = Option::is_none)]
709    pub pending: Option<LiteVote>,
710    /// Latest timeout vote we cast.
711    #[debug(skip_if = Option::is_none)]
712    pub timeout_vote: Option<LiteVote>,
713    /// Fallback vote we cast.
714    #[debug(skip_if = Option::is_none)]
715    pub fallback_vote: Option<LiteVote>,
716    /// The value we voted for, if requested.
717    #[debug(skip_if = Option::is_none)]
718    pub requested_confirmed: Option<Box<ConfirmedBlock>>,
719    /// The value we voted for, if requested.
720    #[debug(skip_if = Option::is_none)]
721    pub requested_validated: Option<Box<ValidatedBlock>>,
722    /// The current round, i.e. the lowest round where we can still vote to validate a block.
723    pub current_round: Round,
724    /// The current leader, who is allowed to propose the next block.
725    /// `None` if everyone is allowed to propose.
726    #[debug(skip_if = Option::is_none)]
727    pub leader: Option<AccountOwner>,
728    /// The timestamp when the current round times out.
729    #[debug(skip_if = Option::is_none)]
730    pub round_timeout: Option<Timestamp>,
731}
732
733impl<C> From<&ChainManager<C>> for ChainManagerInfo
734where
735    C: Context + Clone + Send + Sync + 'static,
736{
737    fn from(manager: &ChainManager<C>) -> Self {
738        let current_round = manager.current_round();
739        let pending = match (manager.confirmed_vote.get(), manager.validated_vote.get()) {
740            (None, None) => None,
741            (Some(confirmed_vote), Some(validated_vote))
742                if validated_vote.round > confirmed_vote.round =>
743            {
744                Some(validated_vote.lite())
745            }
746            (Some(vote), _) => Some(vote.lite()),
747            (None, Some(vote)) => Some(vote.lite()),
748        };
749        ChainManagerInfo {
750            ownership: manager.ownership.get().clone(),
751            requested_proposed: None,
752            requested_locking: None,
753            timeout: manager.timeout.get().clone().map(Box::new),
754            pending,
755            timeout_vote: manager.timeout_vote.get().as_ref().map(Vote::lite),
756            fallback_vote: manager.fallback_vote.get().as_ref().map(Vote::lite),
757            requested_confirmed: None,
758            requested_validated: None,
759            current_round,
760            leader: manager.round_leader(current_round).cloned(),
761            round_timeout: *manager.round_timeout.get(),
762        }
763    }
764}
765
766impl ChainManagerInfo {
767    /// Adds requested certificate values and proposals to the `ChainManagerInfo`.
768    pub fn add_values<C>(&mut self, manager: &ChainManager<C>)
769    where
770        C: Context + Clone + Send + Sync + 'static,
771        C::Extra: ExecutionRuntimeContext,
772    {
773        self.requested_proposed = manager.proposed.get().clone().map(Box::new);
774        self.requested_locking = manager.locking_block.get().clone().map(Box::new);
775        self.requested_confirmed = manager
776            .confirmed_vote
777            .get()
778            .as_ref()
779            .map(|vote| Box::new(vote.value.clone()));
780        self.requested_validated = manager
781            .validated_vote
782            .get()
783            .as_ref()
784            .map(|vote| Box::new(vote.value.clone()));
785    }
786
787    /// Returns whether the `identity` is allowed to propose a block in `round`.
788    /// This is dependent on the type of round and whether `identity` is a validator or (super)owner.
789    pub fn can_propose(&self, identity: &AccountOwner, round: Round) -> bool {
790        match round {
791            Round::Fast => self.ownership.super_owners.contains(identity),
792            Round::MultiLeader(_) => true,
793            Round::SingleLeader(_) | Round::Validator(_) => self.leader.as_ref() == Some(identity),
794        }
795    }
796
797    /// Returns whether a proposal with this content was already handled.
798    pub fn already_handled_proposal(&self, round: Round, proposed_block: &ProposedBlock) -> bool {
799        self.requested_proposed.as_ref().is_some_and(|proposal| {
800            proposal.content.round == round && *proposed_block == proposal.content.block
801        })
802    }
803
804    /// Returns whether there is a locking block in the current round.
805    pub fn has_locking_block_in_current_round(&self) -> bool {
806        self.requested_locking
807            .as_ref()
808            .is_some_and(|locking| locking.round() == self.current_round)
809    }
810}