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