use crate::{
Application, Handoff,
marshal::{ancestry::Ancestry, application::gates::Gates},
simplex::types::Context,
};
use commonware_cryptography::{Digest, Digested, PublicKey};
use commonware_macros::select;
use commonware_runtime::{Clock, Spawner, telemetry::metrics::histogram::Timer};
use commonware_utils::channel::{fallible::OneshotExt, oneshot};
use std::future::Future;
use tracing::{Instrument as _, Span, debug};
pub(crate) enum Resolved<D, S, A, M = ()> {
Reuse(D, S),
Skip,
Build(A, Timer, M),
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn prepare<E, A, D, P, S, N, Fut, M, Seal>(
context: &E,
application: &A,
gates: Gates<D, S>,
consensus_context: Context<D, P>,
checks: Fut,
span: Span,
build_span: impl FnOnce() -> Span + Send + 'static,
seal: impl FnOnce(Digested<A::Block>, M) -> Seal + Send + 'static,
) -> oneshot::Receiver<Handoff<D>>
where
E: Spawner + Clock,
A: Application<E, Context = Context<D, P>, Input = ()>,
D: Digest,
P: PublicKey,
S: Clone + Send + Sync + 'static,
N: Ancestry<A::Block>,
M: Send + 'static,
Fut: Future<Output = Resolved<D, S, N, M>> + Send + 'static,
Seal: Future<Output = (D, S)> + Send + 'static,
{
let mut application = application.clone();
let (mut tx, rx) = oneshot::channel();
let round = consensus_context.round;
let context = context.child("prepare").with_attribute("round", round);
context.spawn(move |runtime_context| {
async move {
let resolved = select! {
_ = tx.closed() => {
debug!(reason = "consensus dropped receiver", "skipping prepare");
return;
},
resolved = checks => resolved,
};
let (ancestry, timer, metadata) = match resolved {
Resolved::Reuse(digest, block) => {
gates
.stage(
round,
digest,
block,
|digest| {
tx.send_lossy(Handoff::Stage(digest));
},
"prepared boundary block",
)
.await;
return;
}
Resolved::Skip => {
tx.send_lossy(Handoff::Wait);
return;
}
Resolved::Build(ancestry, timer, metadata) => (ancestry, timer, metadata),
};
let build = application
.prepare(
(runtime_context.child("app_prepare"), consensus_context),
ancestry,
(),
)
.instrument(build_span());
let prepared = select! {
_ = tx.closed() => {
debug!(reason = "consensus dropped receiver", "skipping prepare");
return;
},
prepared = build => prepared,
};
let (block, decision) = match prepared {
Handoff::Vote(block) => (block, Handoff::Vote(())),
Handoff::Stage(block) => (block, Handoff::Stage(())),
Handoff::Wait => {
debug!(reason = "application declined", "skipping prepare");
tx.send_lossy(Handoff::Wait);
return;
}
};
timer.observe(&runtime_context);
let (digest, block) = seal(block, metadata).await;
gates
.stage(
round,
digest,
block,
|digest| {
tx.send_lossy(decision.map(|()| digest));
},
"prepared block",
)
.await;
}
.instrument(span)
});
rx
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn propose<E, A, D, P, S, N, Fut, M, Seal>(
context: &E,
application: &A,
gates: Gates<D, S>,
consensus_context: Context<D, P>,
checks: Fut,
span: Span,
build_span: impl FnOnce() -> Span + Send + 'static,
seal: impl FnOnce(Digested<A::Block>, M) -> Seal + Send + 'static,
) -> oneshot::Receiver<D>
where
E: Spawner + Clock,
A: Application<E, Context = Context<D, P>, Input = ()>,
D: Digest,
P: PublicKey,
S: Clone + Send + Sync + 'static,
N: Ancestry<A::Block>,
M: Send + 'static,
Fut: Future<Output = Resolved<D, S, N, M>> + Send + 'static,
Seal: Future<Output = (D, S)> + Send + 'static,
{
let mut application = application.clone();
let (mut tx, rx) = oneshot::channel();
let round = consensus_context.round;
let context = context.child("propose").with_attribute("round", round);
context.spawn(move |runtime_context| {
async move {
let resolved = select! {
_ = tx.closed() => {
debug!(reason = "consensus dropped receiver", "skipping proposal");
return;
},
resolved = checks => resolved,
};
let (ancestor_stream, build_timer, metadata) = match resolved {
Resolved::Reuse(digest, block) => {
gates
.stage(
round,
digest,
block,
|digest| {
tx.send_lossy(digest);
},
"proposed boundary block",
)
.await;
return;
}
Resolved::Skip => return,
Resolved::Build(ancestor_stream, build_timer, metadata) => {
(ancestor_stream, build_timer, metadata)
}
};
let parent_commitment = consensus_context.parent.1;
let build_request = application
.propose(
(runtime_context.child("app_propose"), consensus_context),
ancestor_stream,
(),
)
.instrument(build_span());
let built_block = select! {
_ = tx.closed() => {
debug!(reason = "consensus dropped receiver", "skipping proposal");
return;
},
result = build_request => match result {
Some(block) => block,
None => {
debug!(
?parent_commitment,
reason = "block building failed",
"skipping proposal"
);
return;
}
},
};
build_timer.observe(&runtime_context);
let (digest, block) = seal(built_block, metadata).await;
gates
.stage(
round,
digest,
block,
|digest| {
tx.send_lossy(digest);
},
"proposed block",
)
.await;
}
.instrument(span)
});
rx
}