use core::{
sync::atomic::{Ordering, AtomicU64},
pin::Pin,
time::Duration,
};
use alloc::{boxed::Box, sync::Arc};
use std::time::Instant;
use borsh::{BorshSerialize, BorshDeserialize};
use serai_db::Transaction;
use crate::{
SignatureScheme, ValidatorSet as _, BlockNumber, RoundNumber, Block, CommitFor, Blockchain,
Signer, ValidRound, Data, MessageFor, MessageError, Network,
};
mod block_proposal;
use block_proposal::*;
mod round_messages;
use round_messages::*;
mod round_metrics;
use round_metrics::*;
mod observed_block_numbers;
use observed_block_numbers::*;
mod db;
pub(super) trait Borshy: borsh::BorshSerialize + borsh::BorshDeserialize {}
impl<B: borsh::BorshSerialize + borsh::BorshDeserialize> Borshy for B {}
pub(super) trait BorshyBlockchain:
Blockchain<
Validator: Borshy,
SignatureScheme: SignatureScheme<Signature: Borshy, AggregateSignature: Borshy>,
Block: Borshy + Block<Hash: Borshy>,
>
{
}
impl<
B: Blockchain<
Validator: Borshy,
SignatureScheme: SignatureScheme<Signature: Borshy, AggregateSignature: Borshy>,
Block: Borshy + Block<Hash: Borshy>,
>,
> BorshyBlockchain for B
{
}
#[derive(BorshSerialize, BorshDeserialize)]
pub(super) enum Step {
Propose,
Prevote,
Precommit,
}
pub(super) struct State<B: Blockchain> {
block_number: BlockNumber,
block_number_ref: Arc<AtomicU64>,
observed_block_numbers: ObservedBlockNumbers<B::Validator>,
observed_block_number_ref: Arc<AtomicU64>,
proposal: Pin<Box<BlockProposal<B::Block, B::BlockProposal>>>,
round_number: RoundNumber,
pending_step_timeout: Option<(Instant, Duration)>,
pending_precommit_timeout: Option<(Instant, Duration)>,
step: Step,
#[expect(clippy::type_complexity)]
valid:
Option<(ValidRound<<B::SignatureScheme as SignatureScheme>::AggregateSignature>, B::Block)>,
locked: Option<(RoundNumber, <B::Block as Block>::Hash)>,
round_metrics: RoundMetrics<B>,
round_messages: TrackedRounds<B>,
our_latest_prevotes: [Option<MessageFor<B>>; 2],
our_latest_precommit: Option<MessageFor<B>>,
}
impl<B: BorshyBlockchain> State<B> {
#[must_use]
pub(super) fn block_number(&self) -> BlockNumber {
self.block_number
}
pub(super) fn block_number_ref(&self) -> Arc<AtomicU64> {
self.block_number_ref.clone()
}
pub(super) fn observed_block_number_ref(&self) -> Arc<AtomicU64> {
self.observed_block_number_ref.clone()
}
#[must_use]
async fn proposal_message<
N: Network<
B::Validator,
<B::SignatureScheme as SignatureScheme>::Signature,
<B::SignatureScheme as SignatureScheme>::AggregateSignature,
B::Block,
>,
>(
&mut self,
blockchain: &B,
signer: &(impl ?Sized
+ Signer<
Validator = B::Validator,
Signature = <B::SignatureScheme as SignatureScheme>::Signature,
>),
txn: &mut impl Transaction,
) -> Option<MessageFor<B>> {
let validator_set = blockchain.validator_set();
let proposer = validator_set.proposer(self.block_number, self.round_number);
if proposer != signer.validator() {
None?;
}
let (valid_round, proposal) = match &self.valid {
Some((valid_round, block)) => (Some(valid_round.clone()), block.clone()),
None => {
match crate::timeout::<crate::SleepForNetwork<_, _, _, _, N>, _>(
self.proposal.as_mut(),
N::BLOCK_PROCESSING_TIME,
)
.await
{
Ok(proposal) => (None, proposal),
Err(crate::TimeoutExpired) => None?,
}
}
};
let genesis = blockchain.genesis();
let genesis = genesis.as_ref();
self.round_metrics.accumulate_proposal(
genesis,
validator_set,
txn,
proposer,
valid_round.as_ref().map(|ValidRound { round_number, aggregate_signature: _ }| *round_number),
proposal.clone(),
);
Some(
MessageFor::<B>::sign(
signer,
genesis,
self.block_number,
self.round_number,
Data::Proposal { valid_round, proposal },
)
.await,
)
}
#[must_use]
async fn prevote_message(
&mut self,
blockchain: &B,
signer: &(impl ?Sized
+ Signer<
Validator = B::Validator,
Signature = <B::SignatureScheme as SignatureScheme>::Signature,
>),
txn: &mut impl Transaction,
block: Option<<B::Block as Block>::Hash>,
) -> MessageFor<B> {
let genesis = blockchain.genesis();
let genesis = genesis.as_ref();
let validator = signer.validator();
let result = MessageFor::<B>::sign(
signer,
genesis,
self.block_number,
self.round_number,
Data::Prevote { block },
)
.await;
assert!(self.round_metrics.accumulate_prevote(
genesis,
blockchain.validator_set(),
txn,
validator,
block,
result.signature.clone()
));
let _ = self.pending_step_timeout.take();
db::PendingStepTimeout::del(txn, genesis);
self.step = Step::Prevote;
db::Step::set(txn, genesis, &self.step);
self.our_latest_prevotes = [self.our_latest_prevotes[1].take(), Some(result.clone())];
db::OurLatestPrevotes::<B>::set(txn, genesis, &self.our_latest_prevotes);
result
}
#[must_use]
async fn prevote_upon_proposal_message(
&mut self,
blockchain: &B,
signer: &(impl ?Sized
+ Signer<
Validator = B::Validator,
Signature = <B::SignatureScheme as SignatureScheme>::Signature,
>),
txn: &mut impl Transaction,
) -> Option<MessageFor<B>> {
if !matches!(self.step, Step::Propose) {
None?;
}
let ObservedProposal { proposer, valid_round, proposal } =
self.round_metrics.observed_proposal()?;
let block = ((blockchain.validate(proposer, proposal).await.is_ok()) &&
(match valid_round {
None => match &self.locked {
None => true,
Some((_locked_round, locked_value)) => proposal.hash() == (*locked_value),
},
Some(round_number) => {
match &self.locked {
Some((locked_round, locked_value)) => {
((*locked_round) <= round_number) || (proposal.hash() == (*locked_value))
}
None => true,
}
}
}))
.then(|| proposal.hash());
Some(self.prevote_message(blockchain, signer, txn, block).await)
}
#[expect(clippy::type_complexity)]
#[must_use]
async fn precommit_message(
&mut self,
blockchain: &B,
signer: &(impl ?Sized
+ Signer<
Validator = B::Validator,
Signature = <B::SignatureScheme as SignatureScheme>::Signature,
>),
txn: &mut impl Transaction,
block_and_precommit_signature: Option<(
<B::Block as Block>::Hash,
<B::SignatureScheme as SignatureScheme>::Signature,
)>,
) -> MessageFor<B> {
let genesis = blockchain.genesis();
let genesis = genesis.as_ref();
assert!(self.round_metrics.accumulate_precommit(
genesis,
blockchain.validator_set(),
txn,
signer.validator(),
block_and_precommit_signature.clone()
));
let _ = self.pending_step_timeout.take();
db::PendingStepTimeout::del(txn, genesis);
self.step = Step::Precommit;
db::Step::set(txn, genesis, &self.step);
let result = MessageFor::<B>::sign(
signer,
genesis,
self.block_number,
self.round_number,
Data::Precommit { block_and_precommit_signature },
)
.await;
self.our_latest_prevotes[0] = None;
self.our_latest_precommit = Some(result.clone());
db::OurLatestPrecommit::<B>::set(txn, genesis, &result);
result
}
#[must_use]
async fn precommit_some_message(
&mut self,
blockchain: &B,
signer: &(impl ?Sized
+ Signer<
Validator = B::Validator,
Signature = <B::SignatureScheme as SignatureScheme>::Signature,
>),
txn: &mut impl Transaction,
) -> Option<MessageFor<B>> {
let (proposal, valid_round) = {
if !matches!(self.step, Step::Prevote | Step::Precommit) {
None?;
}
if self.valid.as_ref().is_some_and(
|(ValidRound { round_number, aggregate_signature: _ }, _block)| {
(*round_number) >= self.round_number
},
) {
None?;
}
let ObservedProposal { proposer: _, valid_round: _, proposal } =
self.round_metrics.observed_proposal()?;
let valid_round = self.round_metrics.observed_prevotes_for_proposal(blockchain)?;
(proposal, valid_round)
};
let genesis = blockchain.genesis();
let genesis = genesis.as_ref();
{
let valid = (valid_round, proposal.clone());
db::Valid::<B>::set(txn, genesis, &valid);
self.valid = Some(valid);
}
if !matches!(self.step, Step::Prevote) {
None?;
}
let proposal = proposal.hash();
{
let locked = (self.round_number, proposal);
db::Locked::set(txn, genesis, &locked);
self.locked = Some(locked);
}
let precommit_signature = CommitFor::<B>::sign::<B::SignatureScheme>(
signer,
genesis,
self.block_number,
self.round_number,
proposal.as_ref(),
)
.await;
Some(
self.precommit_message(blockchain, signer, txn, Some((proposal, precommit_signature))).await,
)
}
async fn respond<
N: Network<
B::Validator,
<B::SignatureScheme as SignatureScheme>::Signature,
<B::SignatureScheme as SignatureScheme>::AggregateSignature,
B::Block,
>,
>(
&mut self,
blockchain: &B,
signer: &(impl ?Sized
+ Signer<
Validator = B::Validator,
Signature = <B::SignatureScheme as SignatureScheme>::Signature,
>),
txn: &mut impl Transaction,
) -> RoundMessages<B> {
let prevote_message = {
let prevote_upon_proposal_message =
self.prevote_upon_proposal_message(blockchain, signer, txn).await;
let prevote_upon_timeout_message = if matches!(self.step, Step::Propose) &&
self.pending_step_timeout.is_some_and(|(start, duration)| start.elapsed() >= duration)
{
blockchain.missed_proposal(
blockchain.validator_set().proposer(self.block_number, self.round_number),
);
Some(self.prevote_message(blockchain, signer, txn, None).await)
} else {
None
};
prevote_upon_proposal_message.or(prevote_upon_timeout_message)
};
if matches!(self.step, Step::Prevote) &&
self.pending_step_timeout.is_none() &&
(self.round_metrics.observed_prevotes() >= blockchain.validator_set().threshold())
{
let duration = N::LATENCY_TIME
.saturating_mul(u32::try_from(u64::from(self.round_number)).unwrap_or(u32::MAX));
db::PendingStepTimeout::set(
txn,
blockchain.genesis().as_ref(),
&db::timeout_in_ms_since_epoch(duration),
);
self.pending_step_timeout = Some((Instant::now(), duration));
}
let precommit_message = {
let precommit_some_message = self.precommit_some_message(blockchain, signer, txn).await;
let precommit_none_message = if matches!(self.step, Step::Prevote) &&
((self.round_metrics.observed_prevotes_for_none() >=
blockchain.validator_set().threshold()) ||
self
.pending_step_timeout
.is_some_and(|(start, duration)| start.elapsed() >= duration))
{
Some(self.precommit_message(blockchain, signer, txn, None).await)
} else {
None
};
precommit_some_message.or(precommit_none_message)
};
if self.pending_precommit_timeout.is_none() &&
(self.round_metrics.observed_precommits() >= blockchain.validator_set().threshold())
{
let duration = N::LATENCY_TIME
.saturating_mul(u32::try_from(u64::from(self.round_number)).unwrap_or(u32::MAX));
db::PendingPrecommitTimeout::set(
txn,
blockchain.genesis().as_ref(),
&db::timeout_in_ms_since_epoch(duration),
);
self.pending_precommit_timeout = Some((Instant::now(), duration));
}
let mut round_messages = RoundMessages::NONE;
round_messages.insert(prevote_message);
round_messages.insert(precommit_message);
round_messages
}
async fn start_round<
N: Network<
B::Validator,
<B::SignatureScheme as SignatureScheme>::Signature,
<B::SignatureScheme as SignatureScheme>::AggregateSignature,
B::Block,
>,
>(
&mut self,
blockchain: &B,
signer: &(impl ?Sized
+ Signer<
Validator = B::Validator,
Signature = <B::SignatureScheme as SignatureScheme>::Signature,
>),
txn: &mut impl Transaction,
round: RoundNumber,
) -> RoundMessages<B> {
debug_assert!(
((self.round_number == RoundNumber::ONE) && (self.round_number == round)) ||
(self.round_number < round)
);
let genesis = blockchain.genesis();
let genesis = genesis.as_ref();
{
let Self {
block_number,
round_number,
pending_step_timeout,
pending_precommit_timeout,
step,
round_metrics,
block_number_ref: _,
observed_block_numbers: _,
observed_block_number_ref: _,
proposal: _,
valid: _,
locked: _,
round_messages: _,
our_latest_prevotes: _,
our_latest_precommit: _,
} = self;
let block_number = *block_number;
*round_number = round;
db::RoundNumber::set(txn, genesis, round_number);
*step = Step::Propose;
db::Step::set(txn, genesis, &self.step);
{
let duration =
N::BLOCK_DOWNLOADING_TIME.saturating_add(N::BLOCK_PROCESSING_TIME).saturating_add(
N::LATENCY_TIME
.saturating_mul(u32::try_from(u64::from(*round_number)).unwrap_or(u32::MAX)),
);
db::PendingStepTimeout::set(txn, genesis, &db::timeout_in_ms_since_epoch(duration));
*pending_step_timeout = Some((Instant::now(), duration));
}
*pending_precommit_timeout = None;
db::PendingPrecommitTimeout::del(txn, genesis);
round_metrics.reset(block_number, *round_number);
}
let proposal_message = self.proposal_message::<N>(blockchain, signer, txn).await;
for message in self.round_messages.messages_for_round(self.round_number) {
self.round_metrics.accumulate(genesis, blockchain.validator_set(), txn, message.clone());
}
let mut round_messages = self.respond::<N>(blockchain, signer, txn).await;
round_messages.insert(proposal_message);
round_messages
}
pub(super) async fn new<
N: Network<
B::Validator,
<B::SignatureScheme as SignatureScheme>::Signature,
<B::SignatureScheme as SignatureScheme>::AggregateSignature,
B::Block,
>,
>(
blockchain: &B,
signer: &(impl ?Sized
+ Signer<
Validator = B::Validator,
Signature = <B::SignatureScheme as SignatureScheme>::Signature,
>),
txn: &mut impl Transaction,
proposal: B::Block,
) -> (Self, RoundMessages<B>) {
let genesis = blockchain.genesis();
let genesis = genesis.as_ref();
let (init, mut state) = {
let validator_set = blockchain.validator_set();
let getter = &*txn;
let (init, block_number, round_number, step) = {
let block_number = db::BlockNumber::get(getter, genesis);
let round_number = db::RoundNumber::get(getter, genesis);
let step = db::Step::get(getter, genesis);
debug_assert_eq!(block_number.is_some(), round_number.is_some());
debug_assert_eq!(block_number.is_some(), step.is_some());
let init = block_number.is_none();
let block_number = block_number.unwrap_or(BlockNumber::ONE);
let round_number = round_number.unwrap_or(RoundNumber::ONE);
(init, block_number, round_number, step.unwrap_or(Step::Propose))
};
let block_number_ref = Arc::new(AtomicU64::new(u64::from(block_number)));
let observed_block_number_ref = Arc::new(AtomicU64::new(u64::from(block_number)));
let validators = validator_set.validators().into_iter().count();
let state = State {
block_number,
block_number_ref,
observed_block_numbers: ObservedBlockNumbers::new(validators),
observed_block_number_ref,
proposal: Box::pin(BlockProposal::Ready { proposal }),
round_number,
pending_step_timeout: db::PendingStepTimeout::get(getter, genesis)
.map(db::timeout_from_ms_since_epoch),
pending_precommit_timeout: db::PendingPrecommitTimeout::get(getter, genesis)
.map(db::timeout_from_ms_since_epoch),
step,
valid: db::Valid::<B>::get(getter, genesis),
locked: db::Locked::get(getter, genesis),
round_metrics: RoundMetrics::new(
genesis,
validator_set,
getter,
block_number,
round_number,
),
round_messages: TrackedRounds::new(validators),
our_latest_prevotes: db::OurLatestPrevotes::<B>::get(getter, genesis)
.unwrap_or([const { None }; 2]),
our_latest_precommit: db::OurLatestPrecommit::<B>::get(getter, genesis),
};
(init, state)
};
let messages = if init {
db::BlockNumber::set(txn, genesis, &BlockNumber::ONE);
state.start_round::<N>(blockchain, signer, txn, state.round_number).await
} else {
RoundMessages::NONE
};
(state, messages)
}
pub(super) async fn message<
N: Network<
B::Validator,
<B::SignatureScheme as SignatureScheme>::Signature,
<B::SignatureScheme as SignatureScheme>::AggregateSignature,
B::Block,
>,
>(
&mut self,
blockchain: &B,
signer: &(impl ?Sized
+ Signer<
Validator = B::Validator,
Signature = <B::SignatureScheme as SignatureScheme>::Signature,
>),
txn: &mut impl Transaction,
message: MessageFor<B>,
) -> Result<
RoundMessages<B>,
MessageError<
<B::SignatureScheme as SignatureScheme>::Signature,
<B::SignatureScheme as SignatureScheme>::AggregateSignature,
<B::Block as Block>::Hash,
>,
> {
let validator_set = blockchain.validator_set();
if (message.block_number < self.block_number) || (message.round_number < self.round_number) {
Err(MessageError::Stale)?;
}
if message.block_number > self.block_number {
if let Some(observed_block_number) =
self.observed_block_numbers.update(validator_set, message.validator, message.block_number)
{
let _ = self
.observed_block_number_ref
.fetch_max(u64::from(observed_block_number), Ordering::SeqCst);
}
Err(MessageError::Future)?;
}
debug_assert_eq!(message.block_number, self.block_number);
debug_assert!(message.round_number >= self.round_number);
message.static_verificiation(
blockchain.genesis(),
validator_set,
blockchain.signature_scheme(),
)?;
match self.round_messages.update(validator_set, message.validator, self.round_number, &message)
{
Updated::Fresh => {}
Updated::NotTracked => Err(MessageError::Stale)?,
Updated::AlreadyHandled => return Err(MessageError::AlreadyHandled),
Updated::Equivocation(slash_reason) => {
blockchain.slash(message.validator, slash_reason);
return Ok(RoundMessages::NONE);
}
}
Ok(
if (message.round_number > self.round_number) &&
self.round_messages.should_jump_ahead(validator_set, message.round_number)
{
self.start_round::<N>(blockchain, signer, txn, message.round_number).await
} else if message.round_number == self.round_number {
self.round_metrics.accumulate(blockchain.genesis(), validator_set, txn, message);
self.respond::<N>(blockchain, signer, txn).await
} else {
RoundMessages::NONE
},
)
}
}
pub(super) struct TimeoutExpired<'state, 'blockchain, 'signer, B: Blockchain, S: ?Sized> {
state: &'state mut State<B>,
blockchain: &'blockchain B,
signer: &'signer S,
}
impl<
B: BorshyBlockchain,
S: ?Sized
+ Signer<
Validator = B::Validator,
Signature = <B::SignatureScheme as SignatureScheme>::Signature,
>,
> TimeoutExpired<'_, '_, '_, B, S>
{
pub(super) async fn respond<
N: Network<
B::Validator,
<B::SignatureScheme as SignatureScheme>::Signature,
<B::SignatureScheme as SignatureScheme>::AggregateSignature,
B::Block,
>,
>(
self,
txn: &mut impl Transaction,
) -> <RoundMessages<B> as IntoIterator>::IntoIter {
let Self { state, blockchain, signer } = self;
if state.pending_step_timeout.is_none() && state.pending_precommit_timeout.is_none() {
if state.observed_block_numbers.observed_block_number() > Some(state.block_number) {
return RoundMessages::<B>::NONE.into_iter();
}
let mut to_rebroadcast = {
let [prevote_0, prevote_1] = &state.our_latest_prevotes;
[prevote_0.clone(), state.our_latest_precommit.clone(), prevote_1.clone()]
};
if to_rebroadcast[1].as_ref().map(|message| message.round_number) >
to_rebroadcast[2].as_ref().map(|message| message.round_number)
{
to_rebroadcast.swap(1, 2);
}
to_rebroadcast.into_iter().flatten()
} else if state
.pending_precommit_timeout
.is_some_and(|(start, duration)| start.elapsed() >= duration)
{
if let Some(next_round) = state.round_number.0.checked_add(1) {
state.start_round::<N>(blockchain, signer, txn, RoundNumber(next_round)).await.into_iter()
} else {
RoundMessages::<B>::NONE.into_iter()
}
} else {
state.respond::<N>(blockchain, signer, txn).await.into_iter()
}
}
}
impl<B: BorshyBlockchain> State<B> {
#[must_use]
pub(super) async fn timeout<
'state,
'blockchain,
'signer,
N: Network<
B::Validator,
<B::SignatureScheme as SignatureScheme>::Signature,
<B::SignatureScheme as SignatureScheme>::AggregateSignature,
B::Block,
>,
S: ?Sized
+ Signer<
Validator = B::Validator,
Signature = <B::SignatureScheme as SignatureScheme>::Signature,
>,
>(
&'state mut self,
blockchain: &'blockchain B,
signer: &'signer S,
) -> TimeoutExpired<'state, 'blockchain, 'signer, B, S> {
let time_remaining = |(start, duration): (Instant, Duration)| {
let time_elapsed = start.elapsed();
duration.saturating_sub(time_elapsed)
};
(match (self.pending_step_timeout, self.pending_precommit_timeout) {
(Some(step_timeout), Some(precommit_timeout)) => {
N::sleep(time_remaining(step_timeout).min(time_remaining(precommit_timeout)))
}
(Some(timeout), None) | (None, Some(timeout)) => N::sleep(time_remaining(timeout)),
(None, None) => N::sleep(
N::LATENCY_TIME
.saturating_mul(u32::try_from(u64::from(self.round_number)).unwrap_or(u32::MAX)),
),
})
.await;
TimeoutExpired::<B, S> { state: self, blockchain, signer }
}
pub(super) async fn commit<
N: Network<
B::Validator,
<B::SignatureScheme as SignatureScheme>::Signature,
<B::SignatureScheme as SignatureScheme>::AggregateSignature,
B::Block,
>,
>(
&mut self,
blockchain: &mut B,
signer: &(impl ?Sized
+ Signer<
Validator = B::Validator,
Signature = <B::SignatureScheme as SignatureScheme>::Signature,
>),
txn: &mut impl Transaction,
block: B::Block,
commit: CommitFor<B>,
) -> RoundMessages<B> {
let Some(next_block_number) = self.block_number.0.checked_add(1) else {
return RoundMessages::NONE;
};
{
let Self {
block_number,
block_number_ref,
observed_block_numbers: _,
observed_block_number_ref,
proposal,
round_number,
pending_step_timeout,
pending_precommit_timeout,
step,
round_metrics,
valid,
locked,
round_messages,
our_latest_prevotes,
our_latest_precommit,
} = self;
proposal.set(BlockProposal::new(blockchain.add_block(block, commit).await));
let genesis = blockchain.genesis();
let genesis = genesis.as_ref();
*block_number = BlockNumber(next_block_number);
db::BlockNumber::set(txn, genesis, block_number);
let _ = observed_block_number_ref.fetch_max(u64::from(*block_number), Ordering::SeqCst);
block_number_ref.store(u64::from(*block_number), Ordering::SeqCst);
*round_number = RoundNumber::ONE;
*pending_step_timeout = None;
*pending_precommit_timeout = None;
*step = Step::Propose;
round_metrics.reset(*block_number, *round_number);
*valid = None;
db::Valid::<B>::del(txn, genesis);
*locked = None;
db::Locked::<<B::Block as Block>::Hash>::del(txn, genesis);
round_messages.reset();
*our_latest_prevotes = [const { None }; 2];
db::OurLatestPrevotes::<B>::del(txn, genesis);
let _ = our_latest_precommit.take();
db::OurLatestPrecommit::<B>::del(txn, genesis);
}
self.start_round::<N>(blockchain, signer, txn, RoundNumber::ONE).await
}
pub(super) async fn attempt_commit<
N: Network<
B::Validator,
<B::SignatureScheme as SignatureScheme>::Signature,
<B::SignatureScheme as SignatureScheme>::AggregateSignature,
B::Block,
>,
>(
&mut self,
blockchain: &mut B,
signer: &(impl ?Sized
+ Signer<
Validator = B::Validator,
Signature = <B::SignatureScheme as SignatureScheme>::Signature,
>),
txn: &mut impl Transaction,
) -> RoundMessages<B> {
let Some((block, commit)) = self.round_metrics.commit(blockchain) else {
return RoundMessages::NONE;
};
self.commit::<N>(blockchain, signer, txn, block, commit).await
}
}