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