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