Skip to main content

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, NonCanonicalBTreeMap, 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
102pub mod proof;
103
104/// The result of verifying a (valid) query.
105#[derive(Eq, PartialEq)]
106pub enum Outcome {
107    /// The query is accepted and should be acted upon.
108    Accept,
109    /// The query can be skipped without further action.
110    Skip,
111}
112
113/// A reference to a vote for either a validated or a confirmed block.
114pub type ValidatedOrConfirmedVote<'a> = Either<&'a Vote<ValidatedBlock>, &'a Vote<ConfirmedBlock>>;
115
116/// The latest block that validators may have voted to confirm: this is either the block proposal
117/// from the fast round or a validated block certificate. Validators are allowed to vote for this
118/// even if they have locked (i.e. voted to confirm) a different block earlier.
119#[derive(Debug, Clone, Serialize, Deserialize, Allocative)]
120#[cfg_attr(with_testing, derive(Eq, PartialEq))]
121pub enum LockingBlock {
122    /// A proposal in the `Fast` round.
123    Fast(BlockProposal),
124    /// A `ValidatedBlock` certificate in a round other than `Fast`.
125    Regular(ValidatedBlockCertificate),
126}
127
128impl LockingBlock {
129    /// Returns the locking block's round. To propose a different block, a `ValidatedBlock`
130    /// certificate from a higher round is needed.
131    pub fn round(&self) -> Round {
132        match self {
133            Self::Fast(_) => Round::Fast,
134            Self::Regular(certificate) => certificate.round,
135        }
136    }
137
138    /// Returns the ID of the chain this locking block belongs to.
139    pub fn chain_id(&self) -> ChainId {
140        match self {
141            Self::Fast(proposal) => proposal.content.block.chain_id,
142            Self::Regular(certificate) => certificate.value().chain_id(),
143        }
144    }
145}
146
147/// The state of the certification process for a chain's next block.
148#[cfg_attr(with_graphql, derive(async_graphql::SimpleObject), graphql(complex))]
149#[derive(Debug, View, ClonableView, Allocative)]
150#[allocative(bound = "C")]
151pub struct ChainManager<C>
152where
153    C: Clone + Context + 'static,
154{
155    /// The public keys, weights and types of the chain's owners.
156    pub ownership: RegisterView<C, ChainOwnership>,
157    /// The seed for the pseudo-random number generator that determines the round leaders.
158    pub seed: RegisterView<C, u64>,
159    /// The probability distribution for choosing a round leader.
160    #[cfg_attr(with_graphql, graphql(skip))] // Derived from ownership.
161    #[allocative(skip)]
162    pub distribution: RegisterView<C, Option<WeightedAliasIndex<u64>>>,
163    /// The probability distribution for choosing a fallback round leader.
164    #[cfg_attr(with_graphql, graphql(skip))] // Derived from validator weights.
165    #[allocative(skip)]
166    pub fallback_distribution: RegisterView<C, Option<WeightedAliasIndex<u64>>>,
167    /// Highest-round authenticated block that we have received, but not necessarily
168    /// checked yet. If there are multiple proposals in the same round, this contains only the
169    /// first one. This can even contain proposals that did not execute successfully, to determine
170    /// which round to propose in.
171    #[cfg_attr(with_graphql, graphql(skip))]
172    pub signed_proposal: RegisterView<C, Option<BlockProposal>>,
173    /// Highest-round authenticated block that we have received and checked. If there are multiple
174    /// proposals in the same round, this contains only the first one.
175    #[cfg_attr(with_graphql, graphql(skip))]
176    pub proposed: RegisterView<C, Option<BlockProposal>>,
177    /// These are blobs published or read by the proposed block.
178    pub proposed_blobs: MapView<C, BlobId, Blob>,
179    /// Latest validated proposal that a validator may have voted to confirm. This is either the
180    /// latest `ValidatedBlock` we have seen, or the proposal from the `Fast` round.
181    #[cfg_attr(with_graphql, graphql(skip))]
182    pub locking_block: RegisterView<C, Option<LockingBlock>>,
183    /// These are blobs published or read by the locking block.
184    pub locking_blobs: MapView<C, BlobId, Blob>,
185    /// Latest leader timeout certificate we have received.
186    #[cfg_attr(with_graphql, graphql(skip))]
187    pub timeout: RegisterView<C, Option<TimeoutCertificate>>,
188    /// Latest vote we cast to confirm a block.
189    #[cfg_attr(with_graphql, graphql(skip))]
190    pub confirmed_vote: RegisterView<C, Option<Vote<ConfirmedBlock>>>,
191    /// Latest vote we cast to validate a block.
192    #[cfg_attr(with_graphql, graphql(skip))]
193    pub validated_vote: RegisterView<C, Option<Vote<ValidatedBlock>>>,
194    /// Latest timeout vote we cast.
195    #[cfg_attr(with_graphql, graphql(skip))]
196    pub timeout_vote: RegisterView<C, Option<Vote<Timeout>>>,
197    /// Fallback vote we cast.
198    #[cfg_attr(with_graphql, graphql(skip))]
199    pub fallback_vote: RegisterView<C, Option<Vote<Timeout>>>,
200    /// The time after which we are ready to sign a timeout certificate for the current round.
201    pub round_timeout: RegisterView<C, Option<Timestamp>>,
202    /// The lowest round where we can still vote to validate or confirm a block. This is
203    /// the round to which the timeout applies.
204    ///
205    /// Having a leader timeout certificate in any given round causes the next one to become
206    /// current. Seeing a validated block certificate or a valid proposal in any round causes that
207    /// round to become current, unless a higher one already is.
208    #[cfg_attr(with_graphql, graphql(skip))]
209    pub current_round: RegisterView<C, Round>,
210    /// The owners that take over in fallback mode.
211    pub fallback_owners: RegisterView<C, NonCanonicalBTreeMap<AccountOwner, u64>>,
212}
213
214#[cfg(with_graphql)]
215#[async_graphql::ComplexObject]
216impl<C> ChainManager<C>
217where
218    C: Context + Clone + 'static,
219{
220    /// Returns the lowest round where we can still vote to validate or confirm a block. This is
221    /// the round to which the timeout applies.
222    ///
223    /// Having a leader timeout certificate in any given round causes the next one to become
224    /// current. Seeing a validated block certificate or a valid proposal in any round causes that
225    /// round to become current, unless a higher one already is.
226    #[graphql(derived(name = "current_round"))]
227    async fn _current_round(&self) -> Round {
228        self.current_round()
229    }
230}
231
232impl<C> ChainManager<C>
233where
234    C: Context + Clone + 'static,
235{
236    /// Replaces `self` with a new chain manager.
237    pub fn reset<'a>(
238        &mut self,
239        ownership: ChainOwnership,
240        height: BlockHeight,
241        local_time: Timestamp,
242        fallback_owners: impl Iterator<Item = (AccountPublicKey, u64)> + 'a,
243    ) -> Result<(), ChainError> {
244        let distribution = calculate_distribution(ownership.owners.iter());
245
246        let fallback_owners = fallback_owners
247            .map(|(pub_key, weight)| (AccountOwner::from(pub_key), weight))
248            .collect::<NonCanonicalBTreeMap<_, _>>();
249        let fallback_distribution = calculate_distribution(fallback_owners.iter());
250
251        let current_round = ownership.first_round();
252        let round_duration = ownership.round_timeout(current_round);
253        let round_timeout = round_duration.map(|rd| local_time.saturating_add(rd));
254
255        self.clear();
256        self.seed.set(height.0);
257        self.ownership.set(ownership);
258        self.distribution.set(distribution);
259        self.fallback_distribution.set(fallback_distribution);
260        self.fallback_owners.set(fallback_owners);
261        self.current_round.set(current_round);
262        self.round_timeout.set(round_timeout);
263        Ok(())
264    }
265
266    /// Returns the most recent confirmed vote we cast.
267    pub fn confirmed_vote(&self) -> Option<&Vote<ConfirmedBlock>> {
268        self.confirmed_vote.get().as_ref()
269    }
270
271    /// Returns the most recent validated vote we cast.
272    pub fn validated_vote(&self) -> Option<&Vote<ValidatedBlock>> {
273        self.validated_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 a block, we may only vote to validate a *different* block
341        // if a validated block certificate justifies it from a round strictly after our
342        // confirmation. The validation vote will then sign the unlocking round `certificate.round`,
343        // and since our confirmation is in an earlier round, the claim "I have not voted to confirm
344        // a different block in any round at or above the unlocking round" stays truthful.
345        //
346        // Re-validating the very block we confirmed is also allowed, but the certificate must
347        // still be at least as recent as our confirmation. The unlocking round only constrains
348        // switching blocks, yet the round we sign is a claim about *ourselves*: an earlier
349        // confirmation of a different block could fall at or above an older certificate's round
350        // and turn the claim into a lie we could be slashed for. Our confirmed vote sits in the
351        // highest round we ever confirmed in, so `vote.round <= certificate.round` guarantees no
352        // different-block confirmation lies in the unlocking window `[certificate.round, round)`.
353        if let Some(vote) = self.confirmed_vote() {
354            ensure!(
355                match proposal.original_proposal.as_ref() {
356                    None => false,
357                    Some(OriginalProposal::Regular { certificate }) =>
358                        if vote.value().matches_proposed_block(new_block) {
359                            vote.round <= certificate.round
360                        } else {
361                            vote.round < certificate.round
362                        },
363                    Some(OriginalProposal::Fast(_)) => {
364                        vote.round.is_fast() && vote.value().matches_proposed_block(new_block)
365                    }
366                },
367                ChainError::HasIncompatibleConfirmedVote(new_block.height, vote.round)
368            );
369        }
370        Ok(Outcome::Accept)
371    }
372
373    /// Checks if the current round has timed out, and signs a `Timeout`. Returns `true` if the
374    /// chain manager's state has changed.
375    pub fn create_timeout_vote(
376        &mut self,
377        chain_id: ChainId,
378        height: BlockHeight,
379        round: Round,
380        epoch: Epoch,
381        key_pair: Option<&ValidatorSecretKey>,
382        local_time: Timestamp,
383    ) -> Result<bool, ChainError> {
384        let Some(key_pair) = key_pair else {
385            return Ok(false); // We are not a validator.
386        };
387        ensure!(
388            round == self.current_round(),
389            ChainError::WrongRound(self.current_round())
390        );
391        let Some(round_timeout) = *self.round_timeout.get() else {
392            return Err(ChainError::RoundDoesNotTimeOut);
393        };
394        ensure!(
395            local_time >= round_timeout,
396            ChainError::NotTimedOutYet(round_timeout)
397        );
398        if let Some(vote) = self.timeout_vote.get() {
399            if vote.round == round {
400                return Ok(false); // We already signed this timeout.
401            }
402        }
403        let value = Timeout::new(chain_id, height, epoch);
404        self.timeout_vote
405            .set(Some(Vote::new(value, round, key_pair)));
406        Ok(true)
407    }
408
409    /// Signs a `Timeout` certificate to switch to fallback mode.
410    ///
411    /// This must only be called after verifying that the condition for fallback mode is
412    /// satisfied locally.
413    pub fn vote_fallback(
414        &mut self,
415        chain_id: ChainId,
416        height: BlockHeight,
417        epoch: Epoch,
418        key_pair: Option<&ValidatorSecretKey>,
419    ) -> bool {
420        let Some(key_pair) = key_pair else {
421            return false; // We are not a validator.
422        };
423        if self.fallback_vote.get().is_some() || self.current_round() >= Round::Validator(0) {
424            return false; // We already signed this or are already in fallback mode.
425        }
426        let value = Timeout::new(chain_id, height, epoch);
427        let last_regular_round = Round::SingleLeader(u32::MAX);
428        self.fallback_vote
429            .set(Some(Vote::new(value, last_regular_round, key_pair)));
430        true
431    }
432
433    /// Verifies that a validated block is still relevant and should be handled.
434    pub fn check_validated_block(
435        &self,
436        certificate: &ValidatedBlockCertificate,
437    ) -> Result<Outcome, ChainError> {
438        let new_block = certificate.block();
439        let new_round = certificate.round;
440        if let Some(Vote { value, round, .. }) = self.confirmed_vote.get() {
441            if value.block() == new_block && *round == new_round {
442                return Ok(Outcome::Skip); // We already voted to confirm this block.
443            }
444        }
445
446        // Check if we already voted to validate in a later round.
447        if let Some(Vote { round, .. }) = self.validated_vote.get() {
448            ensure!(new_round >= *round, ChainError::InsufficientRound(*round))
449        }
450
451        if let Some(locking) = self.locking_block.get() {
452            ensure!(
453                new_round > locking.round(),
454                ChainError::InsufficientRoundStrict(locking.round())
455            );
456        }
457        Ok(Outcome::Accept)
458    }
459
460    /// Signs a vote to validate the proposed block.
461    pub fn create_vote(
462        &mut self,
463        proposal: &BlockProposal,
464        block: Block,
465        key_pair: Option<&ValidatorSecretKey>,
466        local_time: Timestamp,
467        blobs: BTreeMap<BlobId, Blob>,
468    ) -> Result<Option<ValidatedOrConfirmedVote<'_>>, ChainError> {
469        let round = proposal.content.round;
470
471        match &proposal.original_proposal {
472            // If the validated block certificate is more recent, update our locking block.
473            Some(OriginalProposal::Regular { certificate }) => {
474                if self
475                    .locking_block
476                    .get()
477                    .as_ref()
478                    .is_none_or(|locking| locking.round() < certificate.round)
479                {
480                    let value = ValidatedBlock::new(block.clone());
481                    if let Some(certificate) = certificate.clone().into_validated_certificate(value)
482                    {
483                        self.update_locking(LockingBlock::Regular(certificate), blobs.clone())?;
484                    }
485                }
486            }
487            // If this contains a proposal from the fast round, we consider that a locking block.
488            // It is useful for clients synchronizing with us, so they can re-propose it.
489            Some(OriginalProposal::Fast(signature)) => {
490                if self.locking_block.get().is_none() {
491                    let original_proposal = BlockProposal {
492                        signature: *signature,
493                        ..proposal.clone()
494                    };
495                    self.update_locking(LockingBlock::Fast(original_proposal), blobs.clone())?;
496                }
497            }
498            // If this proposal itself is from the fast round, it is also a locking block: We
499            // will vote to confirm it, so it is locked.
500            None => {
501                if round.is_fast() && self.locking_block.get().is_none() {
502                    // The fast block also counts as locking.
503                    self.update_locking(LockingBlock::Fast(proposal.clone()), blobs.clone())?;
504                }
505            }
506        }
507
508        // We record the proposed block, in case it affects the current round number.
509        self.update_proposed(proposal.clone(), blobs)?;
510        self.update_current_round(local_time);
511
512        let Some(key_pair) = key_pair else {
513            // Not a validator.
514            return Ok(None);
515        };
516
517        // If this is a fast block, vote to confirm. Otherwise vote to validate.
518        if round.is_fast() {
519            self.validated_vote.set(None);
520            let value = ConfirmedBlock::new(block);
521            // Attest that this confirmation is in the chain's first round, so the justification
522            // chain may be omitted: such a block is always the lower one in any fork. A fast
523            // block needs no validation, so there is no quorum to commit to.
524            let first_round = round == self.ownership.get().first_round();
525            let vote = Vote::new_with_first_round(value, round, first_round, None, key_pair);
526            Ok(Some(Either::Right(
527                self.confirmed_vote.get_mut().insert(vote),
528            )))
529        } else {
530            // The unlocking round we sign is the round of the justification this proposal relies
531            // on, and the justification commitment is the hash of that justifying quorum — by
532            // signing it we attest that we verified the quorum, so later receivers only need to
533            // check the signatures built on top of it. A fresh proposal or one retrying a fast
534            // block has no justifying validated certificate, so both are `None`; a regular retry
535            // is justified by its certificate.
536            let (unlocking_round, justification_commitment) = match &proposal.original_proposal {
537                Some(OriginalProposal::Regular { certificate }) => (
538                    Some(certificate.round),
539                    Some(certificate.full_justification_commitment()),
540                ),
541                Some(OriginalProposal::Fast(_)) | None => (None, None),
542            };
543            let value = ValidatedBlock::new(block);
544            let vote = Vote::new_with_unlocking_round(
545                value,
546                round,
547                unlocking_round,
548                justification_commitment,
549                key_pair,
550            );
551            Ok(Some(Either::Left(
552                self.validated_vote.get_mut().insert(vote),
553            )))
554        }
555    }
556
557    /// Signs a vote to confirm the validated block.
558    pub fn create_final_vote(
559        &mut self,
560        validated: ValidatedBlockCertificate,
561        key_pair: Option<&ValidatorSecretKey>,
562        local_time: Timestamp,
563        blobs: BTreeMap<BlobId, Blob>,
564    ) -> Result<(), ViewError> {
565        let round = validated.round;
566        let confirmed_block = ConfirmedBlock::new(validated.inner().block().clone());
567        // Vote to confirm. Attest whether this confirmation is in the chain's first round, so the
568        // justification chain may be omitted: such a block is always the lower one in any fork.
569        // Otherwise commit to the quorum that validated the block, attesting that we verified it
570        // so later receivers only need to check the confirmation signatures built on top.
571        let first_round = round == self.ownership.get().first_round();
572        let justification_commitment = if first_round {
573            None
574        } else {
575            Some(validated.full_justification_commitment())
576        };
577        self.update_locking(LockingBlock::Regular(validated), blobs)?;
578        self.update_current_round(local_time);
579        if let Some(key_pair) = key_pair {
580            if self.current_round() != round {
581                return Ok(()); // We never vote in a past round.
582            }
583            let vote = Vote::new_with_first_round(
584                confirmed_block,
585                round,
586                first_round,
587                justification_commitment,
588                key_pair,
589            );
590            // Ok to overwrite validation votes with confirmation votes at equal or higher round.
591            self.confirmed_vote.set(Some(vote));
592            self.validated_vote.set(None);
593        }
594        Ok(())
595    }
596
597    /// Returns the requested blob if it belongs to the proposal or the locking block.
598    pub async fn pending_blob(&self, blob_id: &BlobId) -> Result<Option<Blob>, ViewError> {
599        if let Some(blob) = self.proposed_blobs.get(blob_id).await? {
600            return Ok(Some(blob));
601        }
602        self.locking_blobs.get(blob_id).await
603    }
604
605    /// Returns the requested blobs if they belong to the proposal or the locking block.
606    pub async fn pending_blobs(&self, blob_ids: &[BlobId]) -> Result<Vec<Option<Blob>>, ViewError> {
607        let mut blobs = self.proposed_blobs.multi_get(blob_ids).await?;
608        let mut missing_indices = Vec::new();
609        let mut missing_blob_ids = Vec::new();
610        for (i, (blob, blob_id)) in blobs.iter().zip(blob_ids).enumerate() {
611            if blob.is_none() {
612                missing_indices.push(i);
613                missing_blob_ids.push(blob_id);
614            }
615        }
616        let second_blobs = self.locking_blobs.multi_get(missing_blob_ids).await?;
617        for (blob, i) in second_blobs.into_iter().zip(missing_indices) {
618            blobs[i] = blob;
619        }
620        Ok(blobs)
621    }
622
623    /// Updates `current_round` and `round_timeout` if necessary.
624    ///
625    /// This must be called after every change to `timeout`, `locking`, `proposed` or
626    /// `signed_proposal`.
627    ///
628    /// The current round starts at `Fast` if there is a super owner, `MultiLeader(0)` if at least
629    /// one multi-leader round is configured, or otherwise `SingleLeader(0)`.
630    ///
631    /// Single-leader rounds can only be ended by a timeout certificate for that round.
632    ///
633    /// The presence of any validated block certificate is also proof that a quorum of validators
634    /// is already in that round, even if we have not seen the corresponding timeout.
635    ///
636    /// Multi-leader rounds can always be skipped, so any correctly signed block proposal in a
637    /// later round ends a multi-leader round.
638    /// Since we don't accept proposals that violate that rule, we can compute the current round in
639    /// general by taking the maximum of all the above.
640    fn update_current_round(&mut self, local_time: Timestamp) {
641        let current_round = self
642            .timeout
643            .get()
644            .iter()
645            // A timeout certificate starts the next round.
646            .map(|certificate| {
647                self.ownership
648                    .get()
649                    .next_round(certificate.round)
650                    .unwrap_or(Round::Validator(u32::MAX))
651            })
652            // A locking block or a proposal is proof we have accepted that we are at least in
653            // this round.
654            .chain(self.locking_block.get().as_ref().map(LockingBlock::round))
655            .chain(
656                self.proposed
657                    .get()
658                    .iter()
659                    .chain(self.signed_proposal.get())
660                    .map(|proposal| proposal.content.round),
661            )
662            .max()
663            .unwrap_or_default()
664            // Otherwise compute the first round for this chain configuration.
665            .max(self.ownership.get().first_round());
666        if current_round <= self.current_round() {
667            return;
668        }
669        let round_duration = self.ownership.get().round_timeout(current_round);
670        self.round_timeout
671            .set(round_duration.map(|rd| local_time.saturating_add(rd)));
672        self.current_round.set(current_round);
673    }
674
675    /// Updates the round number and timer if the timeout certificate is from a higher round than
676    /// any known certificate.
677    pub fn handle_timeout_certificate(
678        &mut self,
679        certificate: TimeoutCertificate,
680        local_time: Timestamp,
681    ) {
682        let round = certificate.round;
683        if let Some(known_certificate) = self.timeout.get() {
684            if known_certificate.round >= round {
685                return;
686            }
687        }
688        self.timeout.set(Some(certificate));
689        self.update_current_round(local_time);
690    }
691
692    /// Returns whether the signer is a valid owner and allowed to propose a block in the
693    /// proposal's round.
694    ///
695    /// Super owners can always propose, except in `Validator` rounds, but it is recommended that
696    /// they don't interfere with single-leader rounds. In multi-leader rounds, any owner can
697    /// propose (or anyone, if `open_multi_leader_rounds`) and in other rounds there is only
698    /// one leader.
699    pub fn can_propose(&self, owner: &AccountOwner, round: Round) -> bool {
700        let ownership = self.ownership.get();
701        if ownership.super_owners.contains(owner) {
702            return !round.is_validator();
703        }
704        match round {
705            Round::Fast => false,
706            Round::MultiLeader(_) => ownership.can_propose_in_multi_leader_round(owner),
707            Round::SingleLeader(_) | Round::Validator(_) => self.round_leader(round) == Some(owner),
708        }
709    }
710
711    /// Returns the leader who is allowed to propose a block in the given round, or `None` if every
712    /// owner is allowed to propose. Exception: In `Round::Fast`, only super owners can propose.
713    fn round_leader(&self, round: Round) -> Option<&AccountOwner> {
714        let ownership = self.ownership.get();
715        compute_round_leader(
716            round,
717            *self.seed.get(),
718            ownership.first_leader.as_ref(),
719            &ownership.owners,
720            self.distribution.get().as_ref(),
721            self.fallback_owners.get(),
722            self.fallback_distribution.get().as_ref(),
723        )
724    }
725
726    /// Returns whether the owner is a super owner.
727    fn is_super(&self, owner: &AccountOwner) -> bool {
728        self.ownership.get().super_owners.contains(owner)
729    }
730
731    /// Sets the signed proposal, if it is newer than the known one, at most from the first
732    /// single-leader round. Returns whether it was updated.
733    ///
734    /// We don't update the signed proposal for any rounds later than `SingleLeader(0)`,
735    /// because single-leader rounds cannot be skipped without a timeout certificate.
736    pub fn update_signed_proposal(
737        &mut self,
738        proposal: &BlockProposal,
739        local_time: Timestamp,
740    ) -> bool {
741        if proposal.content.round > Round::SingleLeader(0) {
742            return false;
743        }
744        if let Some(old_proposal) = self.signed_proposal.get() {
745            if old_proposal.content.round >= proposal.content.round {
746                if *self.current_round.get() < old_proposal.content.round {
747                    tracing::warn!(
748                        chain_id = %proposal.content.block.chain_id,
749                        current_round = ?self.current_round.get(),
750                        proposal_round = ?old_proposal.content.round,
751                        "Proposal round is greater than current round. Updating."
752                    );
753                    self.update_current_round(local_time);
754                    return true;
755                }
756                return false;
757            }
758        }
759        if let Some(old_proposal) = self.proposed.get() {
760            if old_proposal.content.round >= proposal.content.round {
761                return false;
762            }
763        }
764        self.signed_proposal.set(Some(proposal.clone()));
765        self.update_current_round(local_time);
766        true
767    }
768
769    /// Sets the proposed block, if it is newer than our known latest proposal.
770    fn update_proposed(
771        &mut self,
772        proposal: BlockProposal,
773        blobs: BTreeMap<BlobId, Blob>,
774    ) -> Result<(), ViewError> {
775        if let Some(old_proposal) = self.proposed.get() {
776            if old_proposal.content.round >= proposal.content.round {
777                return Ok(());
778            }
779        }
780        if let Some(old_proposal) = self.signed_proposal.get() {
781            if old_proposal.content.round <= proposal.content.round {
782                self.signed_proposal.set(None);
783            }
784        }
785        self.proposed.set(Some(proposal));
786        self.proposed_blobs.clear();
787        for (blob_id, blob) in blobs {
788            self.proposed_blobs.insert(&blob_id, blob)?;
789        }
790        Ok(())
791    }
792
793    /// Sets the locking block and the associated blobs, if it is newer than the known one.
794    fn update_locking(
795        &mut self,
796        locking: LockingBlock,
797        blobs: BTreeMap<BlobId, Blob>,
798    ) -> Result<(), ViewError> {
799        if let Some(old_locked) = self.locking_block.get() {
800            if old_locked.round() >= locking.round() {
801                return Ok(());
802            }
803        }
804        self.locking_block.set(Some(locking));
805        self.locking_blobs.clear();
806        for (blob_id, blob) in blobs {
807            self.locking_blobs.insert(&blob_id, blob)?;
808        }
809        Ok(())
810    }
811}
812
813/// The safety-critical fields of a [`ChainManager`]: previously cast votes and the
814/// locking block. Re-applying these after a chain reset prevents a validator from
815/// being tricked into double-signing at a height/round it has already voted on.
816#[derive(Debug, Default)]
817pub struct ManagerSafetySnapshot {
818    confirmed_vote: Option<Vote<ConfirmedBlock>>,
819    validated_vote: Option<Vote<ValidatedBlock>>,
820    timeout_vote: Option<Vote<Timeout>>,
821    fallback_vote: Option<Vote<Timeout>>,
822    locking_block: Option<LockingBlock>,
823    locking_blobs: Vec<(BlobId, Blob)>,
824}
825
826impl ManagerSafetySnapshot {
827    /// Reads the safety-critical fields from the given `manager`.
828    pub async fn capture<C>(manager: &ChainManager<C>) -> Result<Self, ViewError>
829    where
830        C: Context + Clone + 'static,
831    {
832        Ok(Self {
833            confirmed_vote: manager.confirmed_vote.get().clone(),
834            validated_vote: manager.validated_vote.get().clone(),
835            timeout_vote: manager.timeout_vote.get().clone(),
836            fallback_vote: manager.fallback_vote.get().clone(),
837            locking_block: manager.locking_block.get().clone(),
838            locking_blobs: manager.locking_blobs.index_values().await?,
839        })
840    }
841
842    /// Writes the captured fields back into `manager`, overriding anything that
843    /// may have been produced by re-execution. The restored state is the safe
844    /// upper bound on what this validator has already committed to.
845    pub fn restore<C>(self, manager: &mut ChainManager<C>) -> Result<(), ViewError>
846    where
847        C: Context + Clone + 'static,
848    {
849        manager.confirmed_vote.set(self.confirmed_vote);
850        manager.validated_vote.set(self.validated_vote);
851        manager.timeout_vote.set(self.timeout_vote);
852        manager.fallback_vote.set(self.fallback_vote);
853        manager.locking_block.set(self.locking_block);
854        manager.locking_blobs.clear();
855        for (blob_id, blob) in self.locking_blobs {
856            manager.locking_blobs.insert(&blob_id, blob)?;
857        }
858        Ok(())
859    }
860}
861
862/// Chain manager information that is included in `ChainInfo` sent to clients.
863#[derive(Default, Clone, Debug, Serialize, Deserialize)]
864#[cfg_attr(with_testing, derive(Eq, PartialEq))]
865pub struct ChainManagerInfo {
866    /// The configuration of the chain's owners.
867    pub ownership: ChainOwnership,
868    /// The seed for the pseudo-random number generator that determines the round leaders.
869    pub seed: u64,
870    /// Latest authenticated block that we have received, if requested. This can even contain
871    /// proposals that did not execute successfully, to determine which round to propose in.
872    pub requested_signed_proposal: Option<Box<BlockProposal>>,
873    /// Latest authenticated block that we have received and checked, if requested.
874    #[debug(skip_if = Option::is_none)]
875    pub requested_proposed: Option<Box<BlockProposal>>,
876    /// Latest validated proposal that we have voted to confirm (or would have, if we are not a
877    /// validator).
878    #[debug(skip_if = Option::is_none)]
879    pub requested_locking: Option<Box<LockingBlock>>,
880    /// Latest timeout certificate we have seen.
881    #[debug(skip_if = Option::is_none)]
882    pub timeout: Option<Box<TimeoutCertificate>>,
883    /// Latest vote we cast (either to validate or to confirm a block).
884    #[debug(skip_if = Option::is_none)]
885    pub pending: Option<LiteVote>,
886    /// Latest timeout vote we cast.
887    #[debug(skip_if = Option::is_none)]
888    pub timeout_vote: Option<LiteVote>,
889    /// Fallback vote we cast.
890    #[debug(skip_if = Option::is_none)]
891    pub fallback_vote: Option<LiteVote>,
892    /// The value we voted for, if requested.
893    #[debug(skip_if = Option::is_none)]
894    pub requested_confirmed: Option<Box<ConfirmedBlock>>,
895    /// The value we voted for, if requested.
896    #[debug(skip_if = Option::is_none)]
897    pub requested_validated: Option<Box<ValidatedBlock>>,
898    /// The current round, i.e. the lowest round where we can still vote to validate a block.
899    pub current_round: Round,
900    /// The current leader, who is allowed to propose the next block.
901    /// `None` if everyone is allowed to propose.
902    #[debug(skip_if = Option::is_none)]
903    pub leader: Option<AccountOwner>,
904    /// The timestamp when the current round times out.
905    #[debug(skip_if = Option::is_none)]
906    pub round_timeout: Option<Timestamp>,
907}
908
909impl<C> From<&ChainManager<C>> for ChainManagerInfo
910where
911    C: Context + Clone + 'static,
912{
913    fn from(manager: &ChainManager<C>) -> Self {
914        let current_round = manager.current_round();
915        let pending = match (manager.confirmed_vote.get(), manager.validated_vote.get()) {
916            (None, None) => None,
917            (Some(confirmed_vote), Some(validated_vote))
918                if validated_vote.round > confirmed_vote.round =>
919            {
920                Some(validated_vote.lite())
921            }
922            (Some(vote), _) => Some(vote.lite()),
923            (None, Some(vote)) => Some(vote.lite()),
924        };
925        ChainManagerInfo {
926            ownership: manager.ownership.get().clone(),
927            seed: *manager.seed.get(),
928            requested_signed_proposal: None,
929            requested_proposed: None,
930            requested_locking: None,
931            timeout: manager.timeout.get().clone().map(Box::new),
932            pending,
933            timeout_vote: manager.timeout_vote.get().as_ref().map(Vote::lite),
934            fallback_vote: manager.fallback_vote.get().as_ref().map(Vote::lite),
935            requested_confirmed: None,
936            requested_validated: None,
937            current_round,
938            leader: manager.round_leader(current_round).copied(),
939            round_timeout: *manager.round_timeout.get(),
940        }
941    }
942}
943
944impl ChainManagerInfo {
945    /// Adds requested certificate values and proposals to the `ChainManagerInfo`.
946    pub fn add_values<C>(&mut self, manager: &ChainManager<C>)
947    where
948        C: Context + Clone + 'static,
949        C::Extra: ExecutionRuntimeContext,
950    {
951        self.requested_signed_proposal = manager.signed_proposal.get().clone().map(Box::new);
952        self.requested_proposed = manager.proposed.get().clone().map(Box::new);
953        self.requested_locking = manager.locking_block.get().clone().map(Box::new);
954        self.requested_confirmed = manager
955            .confirmed_vote
956            .get()
957            .as_ref()
958            .map(|vote| Box::new(vote.value.clone()));
959        self.requested_validated = manager
960            .validated_vote
961            .get()
962            .as_ref()
963            .map(|vote| Box::new(vote.value.clone()));
964    }
965
966    /// Returns whether `owner` may propose a block in the current round, based on the
967    /// already-computed [`leader`](Self::leader) for that round.
968    ///
969    /// [`leader`](Self::leader) is `None` in the fast and multi-leader rounds, where any
970    /// eligible owner may propose; in the single-leader and validator rounds it names the
971    /// only owner allowed to propose. Unlike [`should_propose`](Self::should_propose),
972    /// this needs no seed or committee, since the leader is taken as given.
973    pub fn can_propose(&self, owner: &AccountOwner) -> bool {
974        match &self.leader {
975            Some(leader) => leader == owner,
976            None => match self.current_round {
977                Round::Fast => self.ownership.super_owners.contains(owner),
978                _ => self.ownership.can_propose_in_multi_leader_round(owner),
979            },
980        }
981    }
982
983    /// Returns whether the `identity` is allowed to propose a block in `round`.
984    ///
985    /// **Exception:** In single-leader rounds, a **super owner** should only propose
986    /// if they are the designated **leader** for that round.
987    pub fn should_propose(
988        &self,
989        identity: &AccountOwner,
990        round: Round,
991        seed: u64,
992        current_committee: &BTreeMap<AccountOwner, u64>,
993    ) -> bool {
994        match round {
995            Round::Fast => self.ownership.super_owners.contains(identity),
996            Round::MultiLeader(_) => self.ownership.can_propose_in_multi_leader_round(identity),
997            Round::SingleLeader(_) | Round::Validator(_) => {
998                let distribution = calculate_distribution(self.ownership.owners.iter());
999                let fallback_distribution = calculate_distribution(current_committee.iter());
1000                let leader = compute_round_leader(
1001                    round,
1002                    seed,
1003                    self.ownership.first_leader.as_ref(),
1004                    &self.ownership.owners,
1005                    distribution.as_ref(),
1006                    current_committee,
1007                    fallback_distribution.as_ref(),
1008                );
1009                leader == Some(identity)
1010            }
1011        }
1012    }
1013
1014    /// Returns whether a proposal with this content was already handled.
1015    pub fn already_handled_proposal(&self, round: Round, proposed_block: &ProposedBlock) -> bool {
1016        self.requested_proposed.as_ref().is_some_and(|proposal| {
1017            proposal.content.round == round && *proposed_block == proposal.content.block
1018        })
1019    }
1020
1021    /// Returns whether there is a locking block in the current round.
1022    pub fn has_locking_block_in_current_round(&self) -> bool {
1023        self.requested_locking
1024            .as_ref()
1025            .is_some_and(|locking| locking.round() == self.current_round)
1026    }
1027}
1028
1029/// Calculates a probability distribution from the given weights, or `None` if there are no weights.
1030fn calculate_distribution<'a, T: 'a>(
1031    weights: impl IntoIterator<Item = (&'a T, &'a u64)>,
1032) -> Option<WeightedAliasIndex<u64>> {
1033    let weights: Vec<_> = weights.into_iter().map(|(_, weight)| *weight).collect();
1034    if weights.is_empty() {
1035        None
1036    } else {
1037        Some(WeightedAliasIndex::new(weights).ok()?)
1038    }
1039}
1040
1041/// Returns the designated leader for single-leader or validator rounds.
1042/// Returns `None` for fast or multi-leader rounds.
1043fn compute_round_leader<'a>(
1044    round: Round,
1045    seed: u64,
1046    first_leader: Option<&'a AccountOwner>,
1047    owners: &'a BTreeMap<AccountOwner, u64>,
1048    distribution: Option<&WeightedAliasIndex<u64>>,
1049    fallback_owners: &'a BTreeMap<AccountOwner, u64>,
1050    fallback_distribution: Option<&WeightedAliasIndex<u64>>,
1051) -> Option<&'a AccountOwner> {
1052    match round {
1053        Round::SingleLeader(r) => {
1054            if r == 0 {
1055                if let Some(first_leader) = first_leader {
1056                    return Some(first_leader);
1057                }
1058            }
1059            let index = round_leader_index(r, seed, distribution)?;
1060            owners.keys().nth(index)
1061        }
1062        Round::Validator(r) => {
1063            let index = round_leader_index(r, seed, fallback_distribution)?;
1064            fallback_owners.keys().nth(index)
1065        }
1066        Round::Fast | Round::MultiLeader(_) => None,
1067    }
1068}
1069
1070/// Returns the index of the leader who is allowed to propose a block in the given round.
1071fn round_leader_index(
1072    round: u32,
1073    seed: u64,
1074    distribution: Option<&WeightedAliasIndex<u64>>,
1075) -> Option<usize> {
1076    let seed = u64::from(round).rotate_left(32).wrapping_add(seed);
1077    let mut rng = ChaCha8Rng::seed_from_u64(seed);
1078    Some(distribution?.sample(&mut rng))
1079}