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