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 allocative::Allocative;
74use custom_debug_derive::Debug;
75use futures::future::Either;
76use linera_base::{
77    crypto::{AccountPublicKey, ValidatorSecretKey},
78    data_types::{Blob, BlockHeight, Epoch, Round, Timestamp},
79    ensure,
80    identifiers::{AccountOwner, BlobId, ChainId},
81    ownership::ChainOwnership,
82};
83use linera_execution::ExecutionRuntimeContext;
84use linera_views::{
85    context::Context,
86    map_view::MapView,
87    register_view::RegisterView,
88    views::{ClonableView, View},
89    ViewError,
90};
91use rand_chacha::{rand_core::SeedableRng, ChaCha8Rng};
92use rand_distr::{Distribution, WeightedAliasIndex};
93use serde::{Deserialize, Serialize};
94
95use crate::{
96    block::{Block, ConfirmedBlock, Timeout, ValidatedBlock},
97    data_types::{BlockProposal, LiteVote, OriginalProposal, ProposedBlock, Vote},
98    types::{TimeoutCertificate, ValidatedBlockCertificate},
99    ChainError,
100};
101
102/// The result of verifying a (valid) query.
103#[derive(Eq, PartialEq)]
104pub enum Outcome {
105    Accept,
106    Skip,
107}
108
109pub type ValidatedOrConfirmedVote<'a> = Either<&'a Vote<ValidatedBlock>, &'a Vote<ConfirmedBlock>>;
110
111/// The latest block that validators may have voted to confirm: this is either the block proposal
112/// from the fast round or a validated block certificate. Validators are allowed to vote for this
113/// even if they have locked (i.e. voted to confirm) a different block earlier.
114#[derive(Debug, Clone, Serialize, Deserialize, Allocative)]
115#[cfg_attr(with_testing, derive(Eq, PartialEq))]
116pub enum LockingBlock {
117    /// A proposal in the `Fast` round.
118    Fast(BlockProposal),
119    /// A `ValidatedBlock` certificate in a round other than `Fast`.
120    Regular(ValidatedBlockCertificate),
121}
122
123impl LockingBlock {
124    /// Returns the locking block's round. To propose a different block, a `ValidatedBlock`
125    /// certificate from a higher round is needed.
126    pub fn round(&self) -> Round {
127        match self {
128            Self::Fast(_) => Round::Fast,
129            Self::Regular(certificate) => certificate.round,
130        }
131    }
132
133    pub fn chain_id(&self) -> ChainId {
134        match self {
135            Self::Fast(proposal) => proposal.content.block.chain_id,
136            Self::Regular(certificate) => certificate.value().chain_id(),
137        }
138    }
139}
140
141/// The state of the certification process for a chain's next block.
142#[cfg_attr(with_graphql, derive(async_graphql::SimpleObject), graphql(complex))]
143#[derive(Debug, View, ClonableView, Allocative)]
144#[allocative(bound = "C")]
145pub struct ChainManager<C>
146where
147    C: Clone + Context + 'static,
148{
149    /// The public keys, weights and types of the chain's owners.
150    pub ownership: RegisterView<C, ChainOwnership>,
151    /// The seed for the pseudo-random number generator that determines the round leaders.
152    pub seed: RegisterView<C, u64>,
153    /// The probability distribution for choosing a round leader.
154    #[cfg_attr(with_graphql, graphql(skip))] // Derived from ownership.
155    #[allocative(skip)]
156    pub distribution: RegisterView<C, Option<WeightedAliasIndex<u64>>>,
157    /// The probability distribution for choosing a fallback round leader.
158    #[cfg_attr(with_graphql, graphql(skip))] // Derived from validator weights.
159    #[allocative(skip)]
160    pub fallback_distribution: RegisterView<C, Option<WeightedAliasIndex<u64>>>,
161    /// Highest-round authenticated block that we have received, but not necessarily
162    /// checked yet. If there are multiple proposals in the same round, this contains only the
163    /// first one. This can even contain proposals that did not execute successfully, to determine
164    /// which round to propose in.
165    #[cfg_attr(with_graphql, graphql(skip))]
166    pub signed_proposal: RegisterView<C, Option<BlockProposal>>,
167    /// Highest-round authenticated block that we have received and checked. If there are multiple
168    /// proposals in the same round, this contains only the first one.
169    #[cfg_attr(with_graphql, graphql(skip))]
170    pub proposed: RegisterView<C, Option<BlockProposal>>,
171    /// These are blobs published or read by the proposed block.
172    pub proposed_blobs: MapView<C, BlobId, Blob>,
173    /// Latest validated proposal that a validator may have voted to confirm. This is either the
174    /// latest `ValidatedBlock` we have seen, or the proposal from the `Fast` round.
175    #[cfg_attr(with_graphql, graphql(skip))]
176    pub locking_block: RegisterView<C, Option<LockingBlock>>,
177    /// These are blobs published or read by the locking block.
178    pub locking_blobs: MapView<C, BlobId, Blob>,
179    /// Latest leader timeout certificate we have received.
180    #[cfg_attr(with_graphql, graphql(skip))]
181    pub timeout: RegisterView<C, Option<TimeoutCertificate>>,
182    /// Latest vote we cast to confirm a block.
183    #[cfg_attr(with_graphql, graphql(skip))]
184    pub confirmed_vote: RegisterView<C, Option<Vote<ConfirmedBlock>>>,
185    /// Latest vote we cast to validate a block.
186    #[cfg_attr(with_graphql, graphql(skip))]
187    pub validated_vote: RegisterView<C, Option<Vote<ValidatedBlock>>>,
188    /// Latest timeout vote we cast.
189    #[cfg_attr(with_graphql, graphql(skip))]
190    pub timeout_vote: RegisterView<C, Option<Vote<Timeout>>>,
191    /// Fallback vote we cast.
192    #[cfg_attr(with_graphql, graphql(skip))]
193    pub fallback_vote: RegisterView<C, Option<Vote<Timeout>>>,
194    /// The time after which we are ready to sign a timeout certificate for the current round.
195    pub round_timeout: RegisterView<C, Option<Timestamp>>,
196    /// The lowest round where we can still vote to validate or confirm a block. This is
197    /// the round to which the timeout applies.
198    ///
199    /// Having a leader timeout certificate in any given round causes the next one to become
200    /// current. Seeing a validated block certificate or a valid proposal in any round causes that
201    /// round to become current, unless a higher one already is.
202    #[cfg_attr(with_graphql, graphql(skip))]
203    pub current_round: RegisterView<C, Round>,
204    /// The owners that take over in fallback mode.
205    pub fallback_owners: RegisterView<C, BTreeMap<AccountOwner, u64>>,
206}
207
208#[cfg(with_graphql)]
209#[async_graphql::ComplexObject]
210impl<C> ChainManager<C>
211where
212    C: Context + Clone + 'static,
213{
214    /// Returns the lowest round where we can still vote to validate or confirm a block. This is
215    /// the round to which the timeout applies.
216    ///
217    /// Having a leader timeout certificate in any given round causes the next one to become
218    /// current. Seeing a validated block certificate or a valid proposal in any round causes that
219    /// round to become current, unless a higher one already is.
220    #[graphql(derived(name = "current_round"))]
221    async fn _current_round(&self) -> Round {
222        self.current_round()
223    }
224}
225
226impl<C> ChainManager<C>
227where
228    C: Context + Clone + 'static,
229{
230    /// Replaces `self` with a new chain manager.
231    pub fn reset<'a>(
232        &mut self,
233        ownership: ChainOwnership,
234        height: BlockHeight,
235        local_time: Timestamp,
236        fallback_owners: impl Iterator<Item = (AccountPublicKey, u64)> + 'a,
237    ) -> Result<(), ChainError> {
238        let distribution = calculate_distribution(ownership.owners.iter());
239
240        let fallback_owners = fallback_owners
241            .map(|(pub_key, weight)| (AccountOwner::from(pub_key), weight))
242            .collect::<BTreeMap<_, _>>();
243        let fallback_distribution = calculate_distribution(fallback_owners.iter());
244
245        let current_round = ownership.first_round();
246        let round_duration = ownership.round_timeout(current_round);
247        let round_timeout = round_duration.map(|rd| local_time.saturating_add(rd));
248
249        self.clear();
250        self.seed.set(height.0);
251        self.ownership.set(ownership);
252        self.distribution.set(distribution);
253        self.fallback_distribution.set(fallback_distribution);
254        self.fallback_owners.set(fallback_owners);
255        self.current_round.set(current_round);
256        self.round_timeout.set(round_timeout);
257        Ok(())
258    }
259
260    /// Returns the most recent confirmed vote we cast.
261    pub fn confirmed_vote(&self) -> Option<&Vote<ConfirmedBlock>> {
262        self.confirmed_vote.get().as_ref()
263    }
264
265    /// Returns the most recent validated vote we cast.
266    pub fn validated_vote(&self) -> Option<&Vote<ValidatedBlock>> {
267        self.validated_vote.get().as_ref()
268    }
269
270    /// Returns the lowest round where we can still vote to validate or confirm a block. This is
271    /// the round to which the timeout applies.
272    ///
273    /// Having a leader timeout certificate in any given round causes the next one to become
274    /// current. Seeing a validated block certificate or a valid proposal in any round causes that
275    /// round to become current, unless a higher one already is.
276    pub fn current_round(&self) -> Round {
277        *self.current_round.get()
278    }
279
280    /// Verifies that a proposed block is relevant and should be handled.
281    pub fn check_proposed_block(&self, proposal: &BlockProposal) -> Result<Outcome, ChainError> {
282        let new_block = &proposal.content.block;
283        let new_round = proposal.content.round;
284        if let Some(old_proposal) = self.proposed.get() {
285            if old_proposal.content == proposal.content {
286                return Ok(Outcome::Skip); // We have already seen this proposal; nothing to do.
287            }
288        }
289        // When a block is certified, incrementing its height must succeed.
290        ensure!(
291            new_block.height < BlockHeight::MAX,
292            ChainError::BlockHeightOverflow
293        );
294        let current_round = self.current_round();
295        match new_round {
296            // The proposal from the fast round may still be relevant as a locking block, so
297            // we don't compare against the current round here.
298            Round::Fast => {}
299            Round::MultiLeader(_) | Round::SingleLeader(0) => {
300                // If the fast round has not timed out yet, only a super owner is allowed to open
301                // a later round by making a proposal.
302                ensure!(
303                    self.is_super(&proposal.owner()) || !current_round.is_fast(),
304                    ChainError::WrongRound(current_round)
305                );
306                // After the fast round, proposals older than the current round are obsolete.
307                ensure!(
308                    new_round >= current_round,
309                    ChainError::InsufficientRound(new_round)
310                );
311            }
312            Round::SingleLeader(_) | Round::Validator(_) => {
313                // After the first single-leader round, only proposals from the current round are relevant.
314                ensure!(
315                    new_round == current_round,
316                    ChainError::WrongRound(current_round)
317                );
318            }
319        }
320        // The round of our validation votes is only allowed to increase.
321        if let Some(vote) = self.validated_vote() {
322            ensure!(
323                new_round > vote.round,
324                ChainError::InsufficientRoundStrict(vote.round)
325            );
326        }
327        // A proposal that isn't newer than the locking block is not relevant anymore.
328        if let Some(locking_block) = self.locking_block.get() {
329            ensure!(
330                locking_block.round() < new_round,
331                ChainError::MustBeNewerThanLockingBlock(new_block.height, locking_block.round())
332            );
333        }
334        // If we have voted to confirm we cannot vote to validate a different block anymore, except
335        // if there is a validated block certificate from a later round.
336        if let Some(vote) = self.confirmed_vote() {
337            ensure!(
338                match proposal.original_proposal.as_ref() {
339                    None => false,
340                    Some(OriginalProposal::Regular { certificate }) =>
341                        vote.round <= certificate.round,
342                    Some(OriginalProposal::Fast(_)) => {
343                        vote.round.is_fast() && vote.value().matches_proposed_block(new_block)
344                    }
345                },
346                ChainError::HasIncompatibleConfirmedVote(new_block.height, vote.round)
347            );
348        }
349        Ok(Outcome::Accept)
350    }
351
352    /// Checks if the current round has timed out, and signs a `Timeout`. Returns `true` if the
353    /// chain manager's state has changed.
354    pub fn create_timeout_vote(
355        &mut self,
356        chain_id: ChainId,
357        height: BlockHeight,
358        round: Round,
359        epoch: Epoch,
360        key_pair: Option<&ValidatorSecretKey>,
361        local_time: Timestamp,
362    ) -> Result<bool, ChainError> {
363        let Some(key_pair) = key_pair else {
364            return Ok(false); // We are not a validator.
365        };
366        ensure!(
367            round == self.current_round(),
368            ChainError::WrongRound(self.current_round())
369        );
370        let Some(round_timeout) = *self.round_timeout.get() else {
371            return Err(ChainError::RoundDoesNotTimeOut);
372        };
373        ensure!(
374            local_time >= round_timeout,
375            ChainError::NotTimedOutYet(round_timeout)
376        );
377        if let Some(vote) = self.timeout_vote.get() {
378            if vote.round == round {
379                return Ok(false); // We already signed this timeout.
380            }
381        }
382        let value = Timeout::new(chain_id, height, epoch);
383        self.timeout_vote
384            .set(Some(Vote::new(value, round, key_pair)));
385        Ok(true)
386    }
387
388    /// Signs a `Timeout` certificate to switch to fallback mode.
389    ///
390    /// This must only be called after verifying that the condition for fallback mode is
391    /// satisfied locally.
392    pub fn vote_fallback(
393        &mut self,
394        chain_id: ChainId,
395        height: BlockHeight,
396        epoch: Epoch,
397        key_pair: Option<&ValidatorSecretKey>,
398    ) -> bool {
399        let Some(key_pair) = key_pair else {
400            return false; // We are not a validator.
401        };
402        if self.fallback_vote.get().is_some() || self.current_round() >= Round::Validator(0) {
403            return false; // We already signed this or are already in fallback mode.
404        }
405        let value = Timeout::new(chain_id, height, epoch);
406        let last_regular_round = Round::SingleLeader(u32::MAX);
407        self.fallback_vote
408            .set(Some(Vote::new(value, last_regular_round, key_pair)));
409        true
410    }
411
412    /// Verifies that a validated block is still relevant and should be handled.
413    pub fn check_validated_block(
414        &self,
415        certificate: &ValidatedBlockCertificate,
416    ) -> Result<Outcome, ChainError> {
417        let new_block = certificate.block();
418        let new_round = certificate.round;
419        if let Some(Vote { value, round, .. }) = self.confirmed_vote.get() {
420            if value.block() == new_block && *round == new_round {
421                return Ok(Outcome::Skip); // We already voted to confirm this block.
422            }
423        }
424
425        // Check if we already voted to validate in a later round.
426        if let Some(Vote { round, .. }) = self.validated_vote.get() {
427            ensure!(new_round >= *round, ChainError::InsufficientRound(*round))
428        }
429
430        if let Some(locking) = self.locking_block.get() {
431            ensure!(
432                new_round > locking.round(),
433                ChainError::InsufficientRoundStrict(locking.round())
434            );
435        }
436        Ok(Outcome::Accept)
437    }
438
439    /// Signs a vote to validate the proposed block.
440    pub fn create_vote(
441        &mut self,
442        proposal: &BlockProposal,
443        block: Block,
444        key_pair: Option<&ValidatorSecretKey>,
445        local_time: Timestamp,
446        blobs: BTreeMap<BlobId, Blob>,
447    ) -> Result<Option<ValidatedOrConfirmedVote<'_>>, ChainError> {
448        let round = proposal.content.round;
449
450        match &proposal.original_proposal {
451            // If the validated block certificate is more recent, update our locking block.
452            Some(OriginalProposal::Regular { certificate }) => {
453                if self
454                    .locking_block
455                    .get()
456                    .as_ref()
457                    .is_none_or(|locking| locking.round() < certificate.round)
458                {
459                    let value = ValidatedBlock::new(block.clone());
460                    if let Some(certificate) = certificate.clone().with_value(value) {
461                        self.update_locking(LockingBlock::Regular(certificate), blobs.clone())?;
462                    }
463                }
464            }
465            // If this contains a proposal from the fast round, we consider that a locking block.
466            // It is useful for clients synchronizing with us, so they can re-propose it.
467            Some(OriginalProposal::Fast(signature)) => {
468                if self.locking_block.get().is_none() {
469                    let original_proposal = BlockProposal {
470                        signature: *signature,
471                        ..proposal.clone()
472                    };
473                    self.update_locking(LockingBlock::Fast(original_proposal), blobs.clone())?;
474                }
475            }
476            // If this proposal itself is from the fast round, it is also a locking block: We
477            // will vote to confirm it, so it is locked.
478            None => {
479                if round.is_fast() && self.locking_block.get().is_none() {
480                    // The fast block also counts as locking.
481                    self.update_locking(LockingBlock::Fast(proposal.clone()), blobs.clone())?;
482                }
483            }
484        }
485
486        // We record the proposed block, in case it affects the current round number.
487        self.update_proposed(proposal.clone(), blobs)?;
488        self.update_current_round(local_time);
489
490        let Some(key_pair) = key_pair else {
491            // Not a validator.
492            return Ok(None);
493        };
494
495        // If this is a fast block, vote to confirm. Otherwise vote to validate.
496        if round.is_fast() {
497            self.validated_vote.set(None);
498            let value = ConfirmedBlock::new(block);
499            let vote = Vote::new(value, round, key_pair);
500            Ok(Some(Either::Right(
501                self.confirmed_vote.get_mut().insert(vote),
502            )))
503        } else {
504            let value = ValidatedBlock::new(block);
505            let vote = Vote::new(value, round, key_pair);
506            Ok(Some(Either::Left(
507                self.validated_vote.get_mut().insert(vote),
508            )))
509        }
510    }
511
512    /// Signs a vote to confirm the validated block.
513    pub fn create_final_vote(
514        &mut self,
515        validated: ValidatedBlockCertificate,
516        key_pair: Option<&ValidatorSecretKey>,
517        local_time: Timestamp,
518        blobs: BTreeMap<BlobId, Blob>,
519    ) -> Result<(), ViewError> {
520        let round = validated.round;
521        let confirmed_block = ConfirmedBlock::new(validated.inner().block().clone());
522        self.update_locking(LockingBlock::Regular(validated), blobs)?;
523        self.update_current_round(local_time);
524        if let Some(key_pair) = key_pair {
525            if self.current_round() != round {
526                return Ok(()); // We never vote in a past round.
527            }
528            // Vote to confirm.
529            let vote = Vote::new(confirmed_block, round, key_pair);
530            // Ok to overwrite validation votes with confirmation votes at equal or higher round.
531            self.confirmed_vote.set(Some(vote));
532            self.validated_vote.set(None);
533        }
534        Ok(())
535    }
536
537    /// Returns the requested blob if it belongs to the proposal or the locking block.
538    pub async fn pending_blob(&self, blob_id: &BlobId) -> Result<Option<Blob>, ViewError> {
539        if let Some(blob) = self.proposed_blobs.get(blob_id).await? {
540            return Ok(Some(blob));
541        }
542        self.locking_blobs.get(blob_id).await
543    }
544
545    /// Returns the requested blobs if they belong to the proposal or the locking block.
546    pub async fn pending_blobs(&self, blob_ids: &[BlobId]) -> Result<Vec<Option<Blob>>, ViewError> {
547        let mut blobs = self.proposed_blobs.multi_get(blob_ids).await?;
548        let mut missing_indices = Vec::new();
549        let mut missing_blob_ids = Vec::new();
550        for (i, (blob, blob_id)) in blobs.iter().zip(blob_ids).enumerate() {
551            if blob.is_none() {
552                missing_indices.push(i);
553                missing_blob_ids.push(blob_id);
554            }
555        }
556        let second_blobs = self.locking_blobs.multi_get(missing_blob_ids).await?;
557        for (blob, i) in second_blobs.into_iter().zip(missing_indices) {
558            blobs[i] = blob;
559        }
560        Ok(blobs)
561    }
562
563    /// Updates `current_round` and `round_timeout` if necessary.
564    ///
565    /// This must be called after every change to `timeout`, `locking`, `proposed` or
566    /// `signed_proposal`.
567    ///
568    /// The current round starts at `Fast` if there is a super owner, `MultiLeader(0)` if at least
569    /// one multi-leader round is configured, or otherwise `SingleLeader(0)`.
570    ///
571    /// Single-leader rounds can only be ended by a timeout certificate for that round.
572    ///
573    /// The presence of any validated block certificate is also proof that a quorum of validators
574    /// is already in that round, even if we have not seen the corresponding timeout.
575    ///
576    /// Multi-leader rounds can always be skipped, so any correctly signed block proposal in a
577    /// later round ends a multi-leader round.
578    /// Since we don't accept proposals that violate that rule, we can compute the current round in
579    /// general by taking the maximum of all the above.
580    fn update_current_round(&mut self, local_time: Timestamp) {
581        let current_round = self
582            .timeout
583            .get()
584            .iter()
585            // A timeout certificate starts the next round.
586            .map(|certificate| {
587                self.ownership
588                    .get()
589                    .next_round(certificate.round)
590                    .unwrap_or(Round::Validator(u32::MAX))
591            })
592            // A locking block or a proposal is proof we have accepted that we are at least in
593            // this round.
594            .chain(self.locking_block.get().as_ref().map(LockingBlock::round))
595            .chain(
596                self.proposed
597                    .get()
598                    .iter()
599                    .chain(self.signed_proposal.get())
600                    .map(|proposal| proposal.content.round),
601            )
602            .max()
603            .unwrap_or_default()
604            // Otherwise compute the first round for this chain configuration.
605            .max(self.ownership.get().first_round());
606        if current_round <= self.current_round() {
607            return;
608        }
609        let round_duration = self.ownership.get().round_timeout(current_round);
610        self.round_timeout
611            .set(round_duration.map(|rd| local_time.saturating_add(rd)));
612        self.current_round.set(current_round);
613    }
614
615    /// Updates the round number and timer if the timeout certificate is from a higher round than
616    /// any known certificate.
617    pub fn handle_timeout_certificate(
618        &mut self,
619        certificate: TimeoutCertificate,
620        local_time: Timestamp,
621    ) {
622        let round = certificate.round;
623        if let Some(known_certificate) = self.timeout.get() {
624            if known_certificate.round >= round {
625                return;
626            }
627        }
628        self.timeout.set(Some(certificate));
629        self.update_current_round(local_time);
630    }
631
632    /// Returns whether the signer is a valid owner and allowed to propose a block in the
633    /// proposal's round.
634    ///
635    /// Super owners can always propose, except in `Validator` rounds, but it is recommended that
636    /// they don't interfere with single-leader rounds. In multi-leader rounds, any owner can
637    /// propose (or anyone, if `open_multi_leader_rounds`) and in other rounds there is only
638    /// one leader.
639    pub fn can_propose(&self, owner: &AccountOwner, round: Round) -> bool {
640        let ownership = self.ownership.get();
641        if ownership.super_owners.contains(owner) {
642            return !round.is_validator();
643        }
644        match round {
645            Round::Fast => false,
646            Round::MultiLeader(_) => ownership.can_propose_in_multi_leader_round(owner),
647            Round::SingleLeader(_) | Round::Validator(_) => self.round_leader(round) == Some(owner),
648        }
649    }
650
651    /// Returns the leader who is allowed to propose a block in the given round, or `None` if every
652    /// owner is allowed to propose. Exception: In `Round::Fast`, only super owners can propose.
653    fn round_leader(&self, round: Round) -> Option<&AccountOwner> {
654        let ownership = self.ownership.get();
655        compute_round_leader(
656            round,
657            *self.seed.get(),
658            ownership.first_leader.as_ref(),
659            &ownership.owners,
660            self.distribution.get().as_ref(),
661            self.fallback_owners.get(),
662            self.fallback_distribution.get().as_ref(),
663        )
664    }
665
666    /// Returns whether the owner is a super owner.
667    fn is_super(&self, owner: &AccountOwner) -> bool {
668        self.ownership.get().super_owners.contains(owner)
669    }
670
671    /// Sets the signed proposal, if it is newer than the known one, at most from the first
672    /// single-leader round. Returns whether it was updated.
673    ///
674    /// We don't update the signed proposal for any rounds later than `SingleLeader(0)`,
675    /// because single-leader rounds cannot be skipped without a timeout certificate.
676    pub fn update_signed_proposal(
677        &mut self,
678        proposal: &BlockProposal,
679        local_time: Timestamp,
680    ) -> bool {
681        if proposal.content.round > Round::SingleLeader(0) {
682            return false;
683        }
684        if let Some(old_proposal) = self.signed_proposal.get() {
685            if old_proposal.content.round >= proposal.content.round {
686                if *self.current_round.get() < old_proposal.content.round {
687                    tracing::warn!(
688                        chain_id = %proposal.content.block.chain_id,
689                        current_round = ?self.current_round.get(),
690                        proposal_round = ?old_proposal.content.round,
691                        "Proposal round is greater than current round. Updating."
692                    );
693                    self.update_current_round(local_time);
694                    return true;
695                }
696                return false;
697            }
698        }
699        if let Some(old_proposal) = self.proposed.get() {
700            if old_proposal.content.round >= proposal.content.round {
701                return false;
702            }
703        }
704        self.signed_proposal.set(Some(proposal.clone()));
705        self.update_current_round(local_time);
706        true
707    }
708
709    /// Sets the proposed block, if it is newer than our known latest proposal.
710    fn update_proposed(
711        &mut self,
712        proposal: BlockProposal,
713        blobs: BTreeMap<BlobId, Blob>,
714    ) -> Result<(), ViewError> {
715        if let Some(old_proposal) = self.proposed.get() {
716            if old_proposal.content.round >= proposal.content.round {
717                return Ok(());
718            }
719        }
720        if let Some(old_proposal) = self.signed_proposal.get() {
721            if old_proposal.content.round <= proposal.content.round {
722                self.signed_proposal.set(None);
723            }
724        }
725        self.proposed.set(Some(proposal));
726        self.proposed_blobs.clear();
727        for (blob_id, blob) in blobs {
728            self.proposed_blobs.insert(&blob_id, blob)?;
729        }
730        Ok(())
731    }
732
733    /// Sets the locking block and the associated blobs, if it is newer than the known one.
734    fn update_locking(
735        &mut self,
736        locking: LockingBlock,
737        blobs: BTreeMap<BlobId, Blob>,
738    ) -> Result<(), ViewError> {
739        if let Some(old_locked) = self.locking_block.get() {
740            if old_locked.round() >= locking.round() {
741                return Ok(());
742            }
743        }
744        self.locking_block.set(Some(locking));
745        self.locking_blobs.clear();
746        for (blob_id, blob) in blobs {
747            self.locking_blobs.insert(&blob_id, blob)?;
748        }
749        Ok(())
750    }
751}
752
753/// Chain manager information that is included in `ChainInfo` sent to clients.
754#[derive(Default, Clone, Debug, Serialize, Deserialize)]
755#[cfg_attr(with_testing, derive(Eq, PartialEq))]
756pub struct ChainManagerInfo {
757    /// The configuration of the chain's owners.
758    pub ownership: ChainOwnership,
759    /// The seed for the pseudo-random number generator that determines the round leaders.
760    pub seed: u64,
761    /// Latest authenticated block that we have received, if requested. This can even contain
762    /// proposals that did not execute successfully, to determine which round to propose in.
763    pub requested_signed_proposal: Option<Box<BlockProposal>>,
764    /// Latest authenticated block that we have received and checked, if requested.
765    #[debug(skip_if = Option::is_none)]
766    pub requested_proposed: Option<Box<BlockProposal>>,
767    /// Latest validated proposal that we have voted to confirm (or would have, if we are not a
768    /// validator).
769    #[debug(skip_if = Option::is_none)]
770    pub requested_locking: Option<Box<LockingBlock>>,
771    /// Latest timeout certificate we have seen.
772    #[debug(skip_if = Option::is_none)]
773    pub timeout: Option<Box<TimeoutCertificate>>,
774    /// Latest vote we cast (either to validate or to confirm a block).
775    #[debug(skip_if = Option::is_none)]
776    pub pending: Option<LiteVote>,
777    /// Latest timeout vote we cast.
778    #[debug(skip_if = Option::is_none)]
779    pub timeout_vote: Option<LiteVote>,
780    /// Fallback vote we cast.
781    #[debug(skip_if = Option::is_none)]
782    pub fallback_vote: Option<LiteVote>,
783    /// The value we voted for, if requested.
784    #[debug(skip_if = Option::is_none)]
785    pub requested_confirmed: Option<Box<ConfirmedBlock>>,
786    /// The value we voted for, if requested.
787    #[debug(skip_if = Option::is_none)]
788    pub requested_validated: Option<Box<ValidatedBlock>>,
789    /// The current round, i.e. the lowest round where we can still vote to validate a block.
790    pub current_round: Round,
791    /// The current leader, who is allowed to propose the next block.
792    /// `None` if everyone is allowed to propose.
793    #[debug(skip_if = Option::is_none)]
794    pub leader: Option<AccountOwner>,
795    /// The timestamp when the current round times out.
796    #[debug(skip_if = Option::is_none)]
797    pub round_timeout: Option<Timestamp>,
798}
799
800impl<C> From<&ChainManager<C>> for ChainManagerInfo
801where
802    C: Context + Clone + 'static,
803{
804    fn from(manager: &ChainManager<C>) -> Self {
805        let current_round = manager.current_round();
806        let pending = match (manager.confirmed_vote.get(), manager.validated_vote.get()) {
807            (None, None) => None,
808            (Some(confirmed_vote), Some(validated_vote))
809                if validated_vote.round > confirmed_vote.round =>
810            {
811                Some(validated_vote.lite())
812            }
813            (Some(vote), _) => Some(vote.lite()),
814            (None, Some(vote)) => Some(vote.lite()),
815        };
816        ChainManagerInfo {
817            ownership: manager.ownership.get().clone(),
818            seed: *manager.seed.get(),
819            requested_signed_proposal: None,
820            requested_proposed: None,
821            requested_locking: None,
822            timeout: manager.timeout.get().clone().map(Box::new),
823            pending,
824            timeout_vote: manager.timeout_vote.get().as_ref().map(Vote::lite),
825            fallback_vote: manager.fallback_vote.get().as_ref().map(Vote::lite),
826            requested_confirmed: None,
827            requested_validated: None,
828            current_round,
829            leader: manager.round_leader(current_round).copied(),
830            round_timeout: *manager.round_timeout.get(),
831        }
832    }
833}
834
835impl ChainManagerInfo {
836    /// Adds requested certificate values and proposals to the `ChainManagerInfo`.
837    pub fn add_values<C>(&mut self, manager: &ChainManager<C>)
838    where
839        C: Context + Clone + 'static,
840        C::Extra: ExecutionRuntimeContext,
841    {
842        self.requested_signed_proposal = manager.signed_proposal.get().clone().map(Box::new);
843        self.requested_proposed = manager.proposed.get().clone().map(Box::new);
844        self.requested_locking = manager.locking_block.get().clone().map(Box::new);
845        self.requested_confirmed = manager
846            .confirmed_vote
847            .get()
848            .as_ref()
849            .map(|vote| Box::new(vote.value.clone()));
850        self.requested_validated = manager
851            .validated_vote
852            .get()
853            .as_ref()
854            .map(|vote| Box::new(vote.value.clone()));
855    }
856
857    /// Returns whether the `identity` is allowed to propose a block in `round`.
858    ///
859    /// **Exception:** In single-leader rounds, a **super owner** should only propose
860    /// if they are the designated **leader** for that round.
861    pub fn should_propose(
862        &self,
863        identity: &AccountOwner,
864        round: Round,
865        seed: u64,
866        current_committee: &BTreeMap<AccountOwner, u64>,
867    ) -> bool {
868        match round {
869            Round::Fast => self.ownership.super_owners.contains(identity),
870            Round::MultiLeader(_) => self.ownership.can_propose_in_multi_leader_round(identity),
871            Round::SingleLeader(_) | Round::Validator(_) => {
872                let distribution = calculate_distribution(self.ownership.owners.iter());
873                let fallback_distribution = calculate_distribution(current_committee.iter());
874                let leader = compute_round_leader(
875                    round,
876                    seed,
877                    self.ownership.first_leader.as_ref(),
878                    &self.ownership.owners,
879                    distribution.as_ref(),
880                    current_committee,
881                    fallback_distribution.as_ref(),
882                );
883                leader == Some(identity)
884            }
885        }
886    }
887
888    /// Returns whether a proposal with this content was already handled.
889    pub fn already_handled_proposal(&self, round: Round, proposed_block: &ProposedBlock) -> bool {
890        self.requested_proposed.as_ref().is_some_and(|proposal| {
891            proposal.content.round == round && *proposed_block == proposal.content.block
892        })
893    }
894
895    /// Returns whether there is a locking block in the current round.
896    pub fn has_locking_block_in_current_round(&self) -> bool {
897        self.requested_locking
898            .as_ref()
899            .is_some_and(|locking| locking.round() == self.current_round)
900    }
901}
902
903/// Calculates a probability distribution from the given weights, or `None` if there are no weights.
904fn calculate_distribution<'a, T: 'a>(
905    weights: impl IntoIterator<Item = (&'a T, &'a u64)>,
906) -> Option<WeightedAliasIndex<u64>> {
907    let weights: Vec<_> = weights.into_iter().map(|(_, weight)| *weight).collect();
908    if weights.is_empty() {
909        None
910    } else {
911        Some(WeightedAliasIndex::new(weights).ok()?)
912    }
913}
914
915/// Returns the designated leader for single-leader or validator rounds.
916/// Returns `None` for fast or multi-leader rounds.
917fn compute_round_leader<'a>(
918    round: Round,
919    seed: u64,
920    first_leader: Option<&'a AccountOwner>,
921    owners: &'a BTreeMap<AccountOwner, u64>,
922    distribution: Option<&WeightedAliasIndex<u64>>,
923    fallback_owners: &'a BTreeMap<AccountOwner, u64>,
924    fallback_distribution: Option<&WeightedAliasIndex<u64>>,
925) -> Option<&'a AccountOwner> {
926    match round {
927        Round::SingleLeader(r) => {
928            if r == 0 {
929                if let Some(first_leader) = first_leader {
930                    return Some(first_leader);
931                }
932            }
933            let index = round_leader_index(r, seed, distribution)?;
934            owners.keys().nth(index)
935        }
936        Round::Validator(r) => {
937            let index = round_leader_index(r, seed, fallback_distribution)?;
938            fallback_owners.keys().nth(index)
939        }
940        Round::Fast | Round::MultiLeader(_) => None,
941    }
942}
943
944/// Returns the index of the leader who is allowed to propose a block in the given round.
945fn round_leader_index(
946    round: u32,
947    seed: u64,
948    distribution: Option<&WeightedAliasIndex<u64>>,
949) -> Option<usize> {
950    let seed = u64::from(round).rotate_left(32).wrapping_add(seed);
951    let mut rng = ChaCha8Rng::seed_from_u64(seed);
952    Some(distribution?.sample(&mut rng))
953}