use crate::{
marshal::{self, ingress::mailbox::AncestorStream, Update},
simplex::types::Context,
types::{Epoch, Epocher, Round},
Application, Automaton, Block, CertifiableAutomaton, Epochable, Relay, Reporter,
VerifyingApplication,
};
use commonware_cryptography::certificate::Scheme;
use commonware_runtime::{telemetry::metrics::status::GaugeExt, Clock, Metrics, Spawner};
use commonware_utils::{channels::fallible::OneshotExt, futures::ClosedExt};
use futures::{
channel::oneshot::{self, Canceled},
future::{select, try_join, Either, Ready},
lock::Mutex,
pin_mut,
};
use prometheus_client::metrics::gauge::Gauge;
use rand::Rng;
use std::{sync::Arc, time::Instant};
use tracing::{debug, warn};
#[derive(Clone)]
pub struct Marshaled<E, S, A, B, ES>
where
E: Rng + Spawner + Metrics + Clock,
S: Scheme,
A: Application<E>,
B: Block,
ES: Epocher,
{
context: E,
application: A,
marshal: marshal::Mailbox<S, B>,
epocher: ES,
last_built: Arc<Mutex<Option<(Round, B)>>>,
build_duration: Gauge,
}
impl<E, S, A, B, ES> Marshaled<E, S, A, B, ES>
where
E: Rng + Spawner + Metrics + Clock,
S: Scheme,
A: Application<E, Block = B, Context = Context<B::Commitment, S::PublicKey>>,
B: Block,
ES: Epocher,
{
pub fn new(context: E, application: A, marshal: marshal::Mailbox<S, B>, epocher: ES) -> Self {
let build_duration = Gauge::default();
context.register(
"build_duration",
"Time taken for the application to build a new block, in milliseconds",
build_duration.clone(),
);
Self {
context,
application,
marshal,
epocher,
last_built: Arc::new(Mutex::new(None)),
build_duration,
}
}
}
impl<E, S, A, B, ES> Automaton for Marshaled<E, S, A, B, ES>
where
E: Rng + Spawner + Metrics + Clock,
S: Scheme,
A: VerifyingApplication<
E,
Block = B,
SigningScheme = S,
Context = Context<B::Commitment, S::PublicKey>,
>,
B: Block,
ES: Epocher,
{
type Digest = B::Commitment;
type Context = Context<Self::Digest, S::PublicKey>;
async fn genesis(&mut self, epoch: Epoch) -> Self::Digest {
if epoch.is_zero() {
return self.application.genesis().await.commitment();
}
let prev = epoch.previous().expect("checked to be non-zero above");
let last_height = self
.epocher
.last(prev)
.expect("previous epoch should exist");
let Some(block) = self.marshal.get_block(last_height).await else {
unreachable!("missing starting epoch block at height {}", last_height);
};
block.commitment()
}
async fn propose(
&mut self,
consensus_context: Context<Self::Digest, S::PublicKey>,
) -> oneshot::Receiver<Self::Digest> {
let mut marshal = self.marshal.clone();
let mut application = self.application.clone();
let last_built = self.last_built.clone();
let epocher = self.epocher.clone();
let build_duration = self.build_duration.clone();
let (mut tx, rx) = oneshot::channel();
self.context
.with_label("propose")
.spawn(move |runtime_context| async move {
let tx_closed = tx.closed();
pin_mut!(tx_closed);
let (parent_view, parent_commitment) = consensus_context.parent;
let parent_request = fetch_parent(
parent_commitment,
Some(Round::new(consensus_context.epoch(), parent_view)),
&mut application,
&mut marshal,
)
.await;
pin_mut!(parent_request);
let parent = match select(parent_request, &mut tx_closed).await {
Either::Left((Ok(parent), _)) => parent,
Either::Left((Err(_), _)) => {
debug!(
?parent_commitment,
reason = "failed to fetch parent block",
"skipping proposal"
);
return;
}
Either::Right(_) => {
debug!(reason = "consensus dropped receiver", "skipping proposal");
return;
}
};
let last_in_epoch = epocher
.last(consensus_context.epoch())
.expect("current epoch should exist");
if parent.height() == last_in_epoch {
let digest = parent.commitment();
{
let mut lock = last_built.lock().await;
*lock = Some((consensus_context.round, parent));
}
let result = tx.send(digest);
debug!(
round = ?consensus_context.round,
?digest,
success = result.is_ok(),
"re-proposed parent block at epoch boundary"
);
return;
}
let ancestor_stream = AncestorStream::new(marshal.clone(), [parent]);
let build_request = application.propose(
(
runtime_context.with_label("app_propose"),
consensus_context.clone(),
),
ancestor_stream,
);
pin_mut!(build_request);
let start = Instant::now();
let built_block = match select(build_request, &mut tx_closed).await {
Either::Left((Some(block), _)) => block,
Either::Left((None, _)) => {
debug!(
?parent_commitment,
reason = "block building failed",
"skipping proposal"
);
return;
}
Either::Right(_) => {
debug!(reason = "consensus dropped receiver", "skipping proposal");
return;
}
};
let _ = build_duration.try_set(start.elapsed().as_millis());
let digest = built_block.commitment();
{
let mut lock = last_built.lock().await;
*lock = Some((consensus_context.round, built_block));
}
let result = tx.send(digest);
debug!(
round = ?consensus_context.round,
?digest,
success = result.is_ok(),
"proposed new block"
);
});
rx
}
async fn verify(
&mut self,
context: Context<Self::Digest, S::PublicKey>,
digest: Self::Digest,
) -> oneshot::Receiver<bool> {
let mut marshal = self.marshal.clone();
let mut application = self.application.clone();
let epocher = self.epocher.clone();
let (mut tx, rx) = oneshot::channel();
self.context
.with_label("verify")
.spawn(move |runtime_context| async move {
let tx_closed = tx.closed();
pin_mut!(tx_closed);
let (parent_view, parent_commitment) = context.parent;
let parent_request = fetch_parent(
parent_commitment,
Some(Round::new(context.epoch(), parent_view)),
&mut application,
&mut marshal,
)
.await;
let block_request = marshal.subscribe(None, digest).await;
let block_requests = try_join(parent_request, block_request);
pin_mut!(block_requests);
let (parent, block) = match select(block_requests, &mut tx_closed).await {
Either::Left((Ok((parent, block)), _)) => (parent, block),
Either::Left((Err(_), _)) => {
debug!(
reason = "failed to fetch parent or block",
"skipping verification"
);
return;
}
Either::Right(_) => {
debug!(
reason = "consensus dropped receiver",
"skipping verification"
);
return;
}
};
if parent.commitment() == block.commitment() {
let last_in_epoch = epocher
.last(context.epoch())
.expect("current epoch should exist");
if block.height() == last_in_epoch {
marshal.verified(context.round, block).await;
tx.send_lossy(true);
} else {
tx.send_lossy(false);
}
return;
}
let Some(block_bounds) = epocher.containing(block.height()) else {
debug!(
height = %block.height(),
"block height not covered by epoch strategy"
);
tx.send_lossy(false);
return;
};
if block_bounds.epoch() != context.epoch() {
tx.send_lossy(false);
return;
}
if block.parent() != parent.commitment() {
debug!(
block_parent = %block.parent(),
expected_parent = %parent.commitment(),
"block parent commitment does not match expected parent"
);
tx.send_lossy(false);
return;
}
if parent.height().next() != block.height() {
debug!(
parent_height = %parent.height(),
block_height = %block.height(),
"block height is not contiguous with parent height"
);
tx.send_lossy(false);
return;
}
let ancestry_stream = AncestorStream::new(marshal.clone(), [block.clone(), parent]);
let validity_request = application.verify(
(runtime_context.with_label("app_verify"), context.clone()),
ancestry_stream,
);
pin_mut!(validity_request);
let application_valid = match select(validity_request, &mut tx_closed).await {
Either::Left((is_valid, _)) => is_valid,
Either::Right(_) => {
debug!(
reason = "consensus dropped receiver",
"skipping verification"
);
return;
}
};
if application_valid {
marshal.verified(context.round, block).await;
}
tx.send_lossy(application_valid);
});
rx
}
}
impl<E, S, A, B, ES> CertifiableAutomaton for Marshaled<E, S, A, B, ES>
where
E: Rng + Spawner + Metrics + Clock,
S: Scheme,
A: VerifyingApplication<
E,
Block = B,
SigningScheme = S,
Context = Context<B::Commitment, S::PublicKey>,
>,
B: Block,
ES: Epocher,
{
}
impl<E, S, A, B, ES> Relay for Marshaled<E, S, A, B, ES>
where
E: Rng + Spawner + Metrics + Clock,
S: Scheme,
A: Application<E, Block = B, Context = Context<B::Commitment, S::PublicKey>>,
B: Block,
ES: Epocher,
{
type Digest = B::Commitment;
async fn broadcast(&mut self, commitment: Self::Digest) {
let Some((round, block)) = self.last_built.lock().await.clone() else {
warn!("missing block to broadcast");
return;
};
if block.commitment() != commitment {
warn!(
round = %round,
commitment = %block.commitment(),
height = %block.height(),
"skipping requested broadcast of block with mismatched commitment"
);
return;
}
debug!(
round = %round,
commitment = %block.commitment(),
height = %block.height(),
"requested broadcast of built block"
);
self.marshal.proposed(round, block).await;
}
}
impl<E, S, A, B, ES> Reporter for Marshaled<E, S, A, B, ES>
where
E: Rng + Spawner + Metrics + Clock,
S: Scheme,
A: Application<E, Block = B, Context = Context<B::Commitment, S::PublicKey>>
+ Reporter<Activity = Update<B>>,
B: Block,
ES: Epocher,
{
type Activity = A::Activity;
async fn report(&mut self, update: Self::Activity) {
self.application.report(update).await
}
}
#[inline]
async fn fetch_parent<E, S, A, B>(
parent_commitment: B::Commitment,
parent_round: Option<Round>,
application: &mut A,
marshal: &mut marshal::Mailbox<S, B>,
) -> Either<Ready<Result<B, Canceled>>, oneshot::Receiver<B>>
where
E: Rng + Spawner + Metrics + Clock,
S: Scheme,
A: Application<E, Block = B, Context = Context<B::Commitment, S::PublicKey>>,
B: Block,
{
let genesis = application.genesis().await;
if parent_commitment == genesis.commitment() {
Either::Left(futures::future::ready(Ok(genesis)))
} else {
Either::Right(marshal.subscribe(parent_round, parent_commitment).await)
}
}