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.
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 linera_base::{
74    crypto::{KeyPair, PublicKey},
75    data_types::{ArithmeticError, Blob, BlockHeight, Round, Timestamp},
76    doc_scalar, ensure,
77    identifiers::{BlobId, ChainId, Owner},
78    ownership::ChainOwnership,
79};
80use linera_execution::committee::Epoch;
81use rand_chacha::{rand_core::SeedableRng, ChaCha8Rng};
82use rand_distr::{Distribution, WeightedAliasIndex};
83use serde::{Deserialize, Serialize};
84use tracing::error;
85
86use crate::{
87    data_types::{
88        Block, BlockExecutionOutcome, BlockProposal, Certificate, CertificateValue,
89        HashedCertificateValue, LiteVote, ProposalContent, Vote,
90    },
91    ChainError,
92};
93
94/// The result of verifying a (valid) query.
95#[derive(Eq, PartialEq)]
96pub enum Outcome {
97    Accept,
98    Skip,
99}
100
101/// The state of the certification process for a chain's next block.
102#[derive(Default, Clone, Debug, Serialize, Deserialize)]
103pub struct ChainManager {
104    /// The public keys, weights and types of the chain's owners.
105    pub ownership: ChainOwnership,
106    /// The seed for the pseudo-random number generator that determines the round leaders.
107    pub seed: u64,
108    /// The probability distribution for choosing a round leader.
109    pub distribution: Option<WeightedAliasIndex<u64>>,
110    /// The probability distribution for choosing a fallback round leader.
111    pub fallback_distribution: Option<WeightedAliasIndex<u64>>,
112    /// Highest-round authenticated block that we have received and checked. If there are multiple
113    /// proposals in the same round, this contains only the first one.
114    pub proposed: Option<BlockProposal>,
115    /// Latest validated proposal that we have voted to confirm (or would have, if we are not a
116    /// validator).
117    pub locked: Option<Certificate>,
118    /// Latest leader timeout certificate we have received.
119    pub timeout: Option<Certificate>,
120    /// Latest vote we have cast, to validate or confirm.
121    pub pending: Option<Vote>,
122    /// Latest timeout vote we cast.
123    pub timeout_vote: Option<Vote>,
124    /// Fallback vote we cast.
125    pub fallback_vote: Option<Vote>,
126    /// The time after which we are ready to sign a timeout certificate for the current round.
127    pub round_timeout: Option<Timestamp>,
128    /// The lowest round where we can still vote to validate or confirm a block. This is
129    /// the round to which the timeout applies.
130    ///
131    /// Having a leader timeout certificate in any given round causes the next one to become
132    /// current. Seeing a validated block certificate or a valid proposal in any round causes that
133    /// round to become current, unless a higher one already is.
134    pub current_round: Round,
135    /// The owners that take over in fallback mode.
136    pub fallback_owners: BTreeMap<Owner, (PublicKey, u64)>,
137    /// These are blobs belonging to proposed or validated blocks that have not been confirmed yet.
138    pub pending_blobs: BTreeMap<BlobId, Blob>,
139}
140
141doc_scalar!(
142    ChainManager,
143    "The state of the certification process for a chain's next block"
144);
145
146impl ChainManager {
147    /// Replaces `self` with a new chain manager.
148    pub fn reset<'a>(
149        &mut self,
150        ownership: &ChainOwnership,
151        height: BlockHeight,
152        local_time: Timestamp,
153        fallback_owners: impl Iterator<Item = (PublicKey, u64)> + 'a,
154    ) -> Result<(), ChainError> {
155        *self = ChainManager::new(ownership.clone(), height.0, local_time, fallback_owners)?;
156        Ok(())
157    }
158
159    /// Creates a new `ChainManager`, and starts the first round.
160    fn new<'a>(
161        ownership: ChainOwnership,
162        seed: u64,
163        local_time: Timestamp,
164        fallback_owners: impl Iterator<Item = (PublicKey, u64)> + 'a,
165    ) -> Result<Self, ChainError> {
166        let distribution = if !ownership.owners.is_empty() {
167            let weights = ownership
168                .owners
169                .values()
170                .map(|(_, weight)| *weight)
171                .collect();
172            Some(WeightedAliasIndex::new(weights)?)
173        } else {
174            None
175        };
176        let fallback_owners = fallback_owners
177            .map(|(pub_key, weight)| (Owner::from(pub_key), (pub_key, weight)))
178            .collect::<BTreeMap<_, _>>();
179        let fallback_distribution = if !fallback_owners.is_empty() {
180            let weights = fallback_owners
181                .values()
182                .map(|(_, weight)| *weight)
183                .collect();
184            Some(WeightedAliasIndex::new(weights)?)
185        } else {
186            None
187        };
188
189        let current_round = ownership.first_round();
190        let round_duration = ownership.round_timeout(current_round);
191        let round_timeout = round_duration.map(|rd| local_time.saturating_add(rd));
192
193        Ok(ChainManager {
194            ownership,
195            seed,
196            distribution,
197            fallback_distribution,
198            proposed: None,
199            locked: None,
200            timeout: None,
201            pending: None,
202            timeout_vote: None,
203            fallback_vote: None,
204            round_timeout,
205            current_round,
206            fallback_owners,
207            pending_blobs: BTreeMap::new(),
208        })
209    }
210
211    /// Returns the most recent vote we cast.
212    pub fn pending(&self) -> Option<&Vote> {
213        self.pending.as_ref()
214    }
215
216    /// Verifies the safety of a proposed block with respect to voting rules.
217    pub fn check_proposed_block(&self, proposal: &BlockProposal) -> Result<Outcome, ChainError> {
218        let new_round = proposal.content.round;
219        let new_block = &proposal.content.block;
220        let owner = &proposal.owner;
221
222        // When a block is certified, incrementing its height must succeed.
223        ensure!(
224            new_block.height < BlockHeight::MAX,
225            ChainError::InvalidBlockHeight
226        );
227        let expected_round = match &proposal.validated_block_certificate {
228            None => self.current_round,
229            Some(cert) => self
230                .ownership
231                .next_round(cert.round)
232                .ok_or_else(|| ChainError::ArithmeticError(ArithmeticError::Overflow))?
233                .max(self.current_round),
234        };
235        // In leader rotation mode, the round must equal the expected one exactly.
236        // Only the first single-leader round can be entered at any time.
237        if self.is_super(owner)
238            || (new_round <= Round::SingleLeader(0) && !expected_round.is_fast())
239        {
240            ensure!(
241                expected_round <= new_round,
242                ChainError::InsufficientRound(expected_round)
243            );
244        } else {
245            ensure!(
246                expected_round == new_round,
247                ChainError::WrongRound(expected_round)
248            );
249        }
250        if let Some(old_proposal) = &self.proposed {
251            if old_proposal.content == proposal.content {
252                return Ok(Outcome::Skip); // We already voted for this proposal; nothing to do.
253            }
254            ensure!(
255                new_round > old_proposal.content.round,
256                // We already accepted a proposal in this round or in a higher round.
257                ChainError::InsufficientRoundStrict(old_proposal.content.round)
258            );
259            // Any proposal in the fast round is considered locked, because validators vote to
260            // confirm it immediately.
261            if old_proposal.content.round.is_fast()
262                && proposal.validated_block_certificate.is_none()
263            {
264                ensure!(
265                    old_proposal.content.block == *new_block,
266                    ChainError::HasLockedBlock(new_block.height, Round::Fast)
267                )
268            }
269        }
270        // If we have a locked block, the proposal must contain a certificate that validates the
271        // proposed block and is no older than the locked block.
272        if let Some(locked) = &self.locked {
273            ensure!(
274                proposal
275                    .validated_block_certificate
276                    .as_ref()
277                    .is_some_and(|cert| locked.round <= cert.round),
278                ChainError::HasLockedBlock(locked.value().height(), locked.round)
279            )
280        }
281        Ok(Outcome::Accept)
282    }
283
284    /// Checks if the current round has timed out, and signs a `Timeout`.
285    pub fn vote_timeout(
286        &mut self,
287        chain_id: ChainId,
288        height: BlockHeight,
289        epoch: Epoch,
290        key_pair: Option<&KeyPair>,
291        local_time: Timestamp,
292    ) -> bool {
293        let Some(key_pair) = key_pair else {
294            return false; // We are not a validator.
295        };
296        let Some(round_timeout) = self.round_timeout else {
297            return false; // The current round does not time out.
298        };
299        if local_time < round_timeout || self.ownership.owners.is_empty() {
300            return false; // Round has not timed out yet, or there are no regular owners.
301        }
302        let current_round = self.current_round;
303        if let Some(vote) = &self.timeout_vote {
304            if vote.round == current_round {
305                return false; // We already signed this timeout.
306            }
307        }
308        let value = HashedCertificateValue::new_timeout(chain_id, height, epoch);
309        self.timeout_vote = Some(Vote::new(value, current_round, key_pair));
310        true
311    }
312
313    /// Signs a `Timeout` certificate to switch to fallback mode.
314    ///
315    /// This must only be called after verifying that the condition for fallback mode is
316    /// satisfied locally.
317    pub fn vote_fallback(
318        &mut self,
319        chain_id: ChainId,
320        height: BlockHeight,
321        epoch: Epoch,
322        key_pair: Option<&KeyPair>,
323    ) -> bool {
324        let Some(key_pair) = key_pair else {
325            return false; // We are not a validator.
326        };
327        if self.fallback_vote.is_some() || self.current_round >= Round::Validator(0) {
328            return false; // We already signed this or are already in fallback mode.
329        }
330        let value = HashedCertificateValue::new_timeout(chain_id, height, epoch);
331        let last_regular_round = Round::SingleLeader(u32::MAX);
332        self.fallback_vote = Some(Vote::new(value, last_regular_round, key_pair));
333        true
334    }
335
336    /// Verifies that we can vote to confirm a validated block.
337    pub fn check_validated_block(&self, certificate: &Certificate) -> Result<Outcome, ChainError> {
338        let new_block = certificate.value().block();
339        let new_round = certificate.round;
340        if let Some(Vote { value, round, .. }) = &self.pending {
341            match value.inner() {
342                CertificateValue::ConfirmedBlock { executed_block } => {
343                    if Some(&executed_block.block) == new_block && *round == new_round {
344                        return Ok(Outcome::Skip); // We already voted to confirm this block.
345                    }
346                }
347                CertificateValue::ValidatedBlock { .. } => {
348                    ensure!(new_round >= *round, ChainError::InsufficientRound(*round))
349                }
350                CertificateValue::Timeout { .. } => {
351                    // Unreachable: We only put validated or confirmed blocks in pending.
352                    return Err(ChainError::InternalError(
353                        "pending can only be validated or confirmed block".to_string(),
354                    ));
355                }
356            }
357        }
358        // We don't compare to `current_round` here: Non-validators must update their locked block
359        // even if it is older than the current round. Validators will only sign in the current
360        // round, though. (See `create_final_vote` below.)
361        if let Some(locked) = &self.locked {
362            if locked.hash() == certificate.hash() && locked.round == certificate.round {
363                return Ok(Outcome::Skip);
364            }
365            ensure!(
366                new_round > locked.round,
367                ChainError::InsufficientRoundStrict(locked.round)
368            );
369        }
370        Ok(Outcome::Accept)
371    }
372
373    /// Signs a vote to validate the proposed block.
374    pub fn create_vote(
375        &mut self,
376        proposal: BlockProposal,
377        outcome: BlockExecutionOutcome,
378        key_pair: Option<&KeyPair>,
379        local_time: Timestamp,
380    ) {
381        // Record the proposed block, so it can be supplied to clients that request it.
382        self.proposed = Some(proposal.clone());
383        self.update_current_round(local_time);
384        let ProposalContent { block, round, .. } = proposal.content;
385        let executed_block = outcome.with(block);
386
387        // If the validated block certificate is more recent, update our locked block.
388        if let Some(lite_cert) = proposal.validated_block_certificate {
389            if self
390                .locked
391                .as_ref()
392                .map_or(true, |locked| locked.round < lite_cert.round)
393            {
394                let value = HashedCertificateValue::new_validated(executed_block.clone());
395                if let Some(certificate) = lite_cert.with_value(value) {
396                    self.locked = Some(certificate);
397                }
398            }
399        }
400
401        for blob in proposal.blobs {
402            self.pending_blobs.insert(blob.id(), blob);
403        }
404
405        if let Some(key_pair) = key_pair {
406            // If this is a fast block, vote to confirm. Otherwise vote to validate.
407            let value = if round.is_fast() {
408                HashedCertificateValue::new_confirmed(executed_block)
409            } else {
410                HashedCertificateValue::new_validated(executed_block)
411            };
412            self.pending = Some(Vote::new(value, round, key_pair));
413        }
414    }
415
416    /// Signs a vote to confirm the validated block.
417    pub fn create_final_vote(
418        &mut self,
419        certificate: Certificate,
420        key_pair: Option<&KeyPair>,
421        local_time: Timestamp,
422    ) {
423        let round = certificate.round;
424        // Validators only change their locked block if the new one is included in a proposal in the
425        // current round, or it is itself in the current round.
426        if key_pair.is_some() && round < self.current_round {
427            return;
428        }
429        let Some(value) = certificate.value.validated_to_confirmed() else {
430            // Unreachable: This is only called with validated blocks.
431            error!("Unexpected certificate; expected ValidatedBlock");
432            return;
433        };
434        self.locked = Some(certificate);
435        self.update_current_round(local_time);
436        if let Some(key_pair) = key_pair {
437            // Vote to confirm.
438            let vote = Vote::new(value, round, key_pair);
439            // Ok to overwrite validation votes with confirmation votes at equal or higher round.
440            self.pending = Some(vote);
441        }
442    }
443
444    /// Updates `current_round` and `round_timeout` if necessary.
445    ///
446    /// This must be after every change to `timeout`, `locked` or `proposed`.
447    fn update_current_round(&mut self, local_time: Timestamp) {
448        let current_round = self
449            .timeout
450            .iter()
451            .map(|certificate| {
452                self.ownership
453                    .next_round(certificate.round)
454                    .unwrap_or(Round::Validator(u32::MAX))
455            })
456            .chain(self.locked.iter().map(|certificate| certificate.round))
457            .chain(self.proposed.iter().map(|proposal| proposal.content.round))
458            .max()
459            .unwrap_or_default()
460            .max(self.ownership.first_round());
461        if current_round <= self.current_round {
462            return;
463        }
464        let round_duration = self.ownership.round_timeout(current_round);
465        self.round_timeout = round_duration.map(|rd| local_time.saturating_add(rd));
466        self.current_round = current_round;
467    }
468
469    /// Updates the round number and timer if the timeout certificate is from a higher round than
470    /// any known certificate.
471    pub fn handle_timeout_certificate(&mut self, certificate: Certificate, local_time: Timestamp) {
472        if !certificate.value().is_timeout() {
473            // Unreachable: This is only called with timeout certificates.
474            error!("Unexpected certificate; expected leader timeout");
475            return;
476        }
477        let round = certificate.round;
478        if let Some(known_certificate) = &self.timeout {
479            if known_certificate.round >= round {
480                return;
481            }
482        }
483        self.timeout = Some(certificate);
484        self.update_current_round(local_time);
485    }
486
487    /// Returns the public key of the block proposal's signer, if they are a valid owner and allowed
488    /// to propose a block in the proposal's round.
489    pub fn verify_owner(&self, proposal: &BlockProposal) -> Option<PublicKey> {
490        if let Some(public_key) = self.ownership.super_owners.get(&proposal.owner) {
491            return Some(*public_key);
492        }
493        match proposal.content.round {
494            Round::Fast => {
495                None // Only super owners can propose in the first round.
496            }
497            Round::MultiLeader(_) => {
498                // Not in leader rotation mode; any owner is allowed to propose.
499                self.ownership
500                    .owners
501                    .get(&proposal.owner)
502                    .map(|(public_key, _)| *public_key)
503            }
504            Round::SingleLeader(r) => {
505                let index = self.round_leader_index(r)?;
506                let (leader, (public_key, _)) = self.ownership.owners.iter().nth(index)?;
507                (*leader == proposal.owner).then_some(*public_key)
508            }
509            Round::Validator(r) => {
510                let index = self.fallback_round_leader_index(r)?;
511                let (leader, (public_key, _)) = self.fallback_owners.iter().nth(index)?;
512                (*leader == proposal.owner).then_some(*public_key)
513            }
514        }
515    }
516
517    /// Returns the leader who is allowed to propose a block in the given round, or `None` if every
518    /// owner is allowed to propose. Exception: In `Round::Fast`, only super owners can propose.
519    fn round_leader(&self, round: Round) -> Option<&Owner> {
520        match round {
521            Round::SingleLeader(r) => {
522                let index = self.round_leader_index(r)?;
523                self.ownership.owners.keys().nth(index)
524            }
525            Round::Validator(r) => {
526                let index = self.fallback_round_leader_index(r)?;
527                self.fallback_owners.keys().nth(index)
528            }
529            Round::Fast | Round::MultiLeader(_) => None,
530        }
531    }
532
533    /// Returns the index of the leader who is allowed to propose a block in the given round.
534    fn round_leader_index(&self, round: u32) -> Option<usize> {
535        let seed = u64::from(round).rotate_left(32).wrapping_add(self.seed);
536        let mut rng = ChaCha8Rng::seed_from_u64(seed);
537        Some(self.distribution.as_ref()?.sample(&mut rng))
538    }
539
540    /// Returns the index of the fallback leader who is allowed to propose a block in the given
541    /// round.
542    fn fallback_round_leader_index(&self, round: u32) -> Option<usize> {
543        let seed = u64::from(round).rotate_left(32).wrapping_add(self.seed);
544        let mut rng = ChaCha8Rng::seed_from_u64(seed);
545        Some(self.fallback_distribution.as_ref()?.sample(&mut rng))
546    }
547
548    /// Returns whether the owner is a super owner.
549    fn is_super(&self, owner: &Owner) -> bool {
550        self.ownership.super_owners.contains_key(owner)
551    }
552}
553
554/// Chain manager information that is included in `ChainInfo` sent to clients.
555#[derive(Default, Clone, Debug, Serialize, Deserialize)]
556#[cfg_attr(with_testing, derive(Eq, PartialEq))]
557pub struct ChainManagerInfo {
558    /// The configuration of the chain's owners.
559    pub ownership: ChainOwnership,
560    /// Latest authenticated block that we have received, if requested.
561    pub requested_proposed: Option<Box<BlockProposal>>,
562    /// Latest validated proposal that we have voted to confirm (or would have, if we are not a
563    /// validator).
564    pub requested_locked: Option<Box<Certificate>>,
565    /// Latest timeout certificate we have seen.
566    pub timeout: Option<Box<Certificate>>,
567    /// Latest vote we cast (either to validate or to confirm a block).
568    pub pending: Option<LiteVote>,
569    /// Latest timeout vote we cast.
570    pub timeout_vote: Option<LiteVote>,
571    /// Fallback vote we cast.
572    pub fallback_vote: Option<LiteVote>,
573    /// The value we voted for, if requested.
574    pub requested_pending_value: Option<Box<HashedCertificateValue>>,
575    /// The current round, i.e. the lowest round where we can still vote to validate a block.
576    pub current_round: Round,
577    /// The current leader, who is allowed to propose the next block.
578    /// `None` if everyone is allowed to propose.
579    pub leader: Option<Owner>,
580    /// The timestamp when the current round times out.
581    pub round_timeout: Option<Timestamp>,
582    /// These are blobs belonging to proposed or validated blocks that have not been confirmed yet.
583    pub pending_blobs: BTreeMap<BlobId, Blob>,
584}
585
586impl From<&ChainManager> for ChainManagerInfo {
587    fn from(manager: &ChainManager) -> Self {
588        let current_round = manager.current_round;
589        ChainManagerInfo {
590            ownership: manager.ownership.clone(),
591            requested_proposed: None,
592            requested_locked: None,
593            timeout: manager.timeout.clone().map(Box::new),
594            pending: manager.pending.as_ref().map(|vote| vote.lite()),
595            timeout_vote: manager.timeout_vote.as_ref().map(Vote::lite),
596            fallback_vote: manager.fallback_vote.as_ref().map(Vote::lite),
597            requested_pending_value: None,
598            current_round,
599            leader: manager.round_leader(current_round).cloned(),
600            round_timeout: manager.round_timeout,
601            pending_blobs: BTreeMap::new(),
602        }
603    }
604}
605
606impl ChainManagerInfo {
607    /// Adds requested certificate values and proposals to the `ChainManagerInfo`.
608    pub fn add_values(&mut self, manager: &ChainManager) {
609        self.requested_proposed = manager.proposed.clone().map(Box::new);
610        self.requested_locked = manager.locked.clone().map(Box::new);
611        self.requested_pending_value = manager
612            .pending
613            .as_ref()
614            .map(|vote| Box::new(vote.value.clone()));
615        self.pending_blobs = manager.pending_blobs.clone();
616    }
617
618    /// Gets the highest validated block.
619    pub fn highest_validated_block(&self) -> Option<&Block> {
620        if let Some(certificate) = &self.requested_locked {
621            let block = certificate.value().block();
622            if block.is_some() {
623                return block;
624            }
625        }
626
627        if let Some(proposal) = &self.requested_proposed {
628            if proposal.content.round.is_fast() {
629                return Some(&proposal.content.block);
630            }
631        }
632
633        None
634    }
635
636    /// Returns whether the `identity` is allowed to propose a block in `round`.
637    /// This is dependant on the type of round and whether `identity` is a validator or (super)owner.
638    pub fn can_propose(&self, identity: &Owner, round: Round) -> bool {
639        match round {
640            Round::Fast => self.ownership.super_owners.contains_key(identity),
641            Round::MultiLeader(_) => true,
642            Round::SingleLeader(_) | Round::Validator(_) => self.leader.as_ref() == Some(identity),
643        }
644    }
645}