,
C: Cancellation,
{
let parent_digest = parent.digest();
let known = {
let state = self.state.lock();
state.last_processed.digest == parent_digest
|| state.pending.contains_key(&parent_digest)
};
if !known {
self.rebuild_pending(app, context, marshal, parent, cancellation, replay)
.await?;
}
await_or_cancel(cancellation, self.fork_batches(&parent_digest))
.await
.ok_or(PrepareBatchesError::Cancelled)?
}
/// Rebuilds missing ancestry through `target`.
///
/// The backward walk stops only at pending state or the applied anchor and
/// rejects stale or non-contiguous ancestry. Blocks are then replayed in
/// ancestor order, with commitments checked before each cache insertion.
async fn rebuild_pending(
&self,
app: &mut A,
context: &E,
provider: P,
target: Arc,
cancellation: &mut C,
replay: Option>>,
) -> Result<(), PrepareBatchesError>
where
P: BlockProvider + Clone,
C: Cancellation,
{
let timer = self.metrics.rebuild_pending_duration.timer(context);
let target_digest = target.digest();
let mut replay_path = Vec::new();
let mut cursor = target;
loop {
let (known, last_processed) = {
let state = self.state.lock();
(
cursor.digest() == state.last_processed.digest
|| state.pending.contains_key(&cursor.digest()),
state.last_processed,
)
};
if known {
break;
}
let cursor_height = cursor.height();
if cursor_height <= last_processed.height {
warn!(
?target_digest,
cursor = ?cursor.digest(),
current_height = cursor_height.get(),
last_processed_height = last_processed.height.get(),
last_processed = ?last_processed.digest,
"rebuild_pending reached stale ancestry at or below processed height"
);
return Err(PrepareBatchesError::Invalid);
}
let Some(parent) =
await_or_cancel(cancellation, provider.clone().subscribe_parent(&cursor)).await
else {
return Err(PrepareBatchesError::Cancelled);
};
let Some(parent) = parent else {
debug!(
?target_digest,
cursor = ?cursor.digest(),
"ancestor subscription ended before delivery"
);
return Err(PrepareBatchesError::Incomplete);
};
if parent.digest() != cursor.parent() || parent.height().next() != cursor_height {
warn!(
?target_digest,
cursor = ?cursor.digest(),
parent = ?parent.digest(),
cursor_height = cursor_height.get(),
parent_height = parent.height().get(),
expected_parent = ?cursor.parent(),
"rebuild_pending received non-contiguous ancestry"
);
return Err(PrepareBatchesError::Invalid);
}
replay_path.push(cursor);
cursor = parent;
}
let depth = replay_path.len();
for block in replay_path.into_iter().rev() {
if let Some(replay) = replay {
self.replay_block_shared(app, context, target_digest, block, cancellation, replay)
.await?;
} else {
self.replay_block(app, context, target_digest, block, cancellation)
.await?;
}
}
self.update_pending_metric();
let _ = self.metrics.rebuild_pending_depth.try_set(depth);
timer.observe(context);
Ok(())
}
}
/// Returns true when `block` is already covered by applied state.
#[tracing::instrument(
name = "stateful.processor.is_already_processed",
level = "info",
skip_all,
fields(height = block.height().traced(), digest = %block.digest())
)]
async fn is_already_processed(
last_processed: Anchor<::Digest>,
marshal: MarshalMailbox,
block: &V::ApplicationBlock,
cancellation: &mut C,
) -> Result
where
S: Scheme,
V: MarshalVariant,
V::ApplicationBlock: Block + Clone,
C: Cancellation,
{
let target_height = block.height();
if target_height > last_processed.height {
return Ok(false);
}
if target_height == last_processed.height {
return Ok(block.digest() == last_processed.digest);
}
let Some(canonical) = await_or_cancel(
cancellation,
marshal.get_block(Identifier::Height(target_height)),
)
.await
else {
return Err(PrepareBatchesError::Cancelled);
};
let Some(canonical) = canonical else {
warn!(
target_height = target_height.get(),
processed_height = last_processed.height.get(),
"failed to fetch canonical processed block for stale-block check"
);
return Err(PrepareBatchesError::Incomplete);
};
Ok(canonical.digest() == block.digest())
}
/// Read the next ancestry item unless the request is cancelled.
#[tracing::instrument(name = "stateful.processor.fetch_ancestor", level = "info", skip_all)]
async fn fetch_ancestor(cancellation: &mut C, stream: &mut S) -> Option>
where
S: Stream- + Unpin,
C: Cancellation,
{
await_or_cancel(cancellation, stream.next()).await
}
/// Wait for `future` unless the request is cancelled.
async fn await_or_cancel
(cancellation: &mut C, future: F) -> Option
where
F: Future,
C: Cancellation,
{
select! {
_ = cancellation.cancelled() => None,
output = future => Some(output),
}
}
#[cfg(test)]
mod tests {
use super::{
Applied, Disposition, FinalizationBoundary, PrepareBatchesError, Processor, Prune, Pruning,
ReplayClaim, ReplayFlights, ReplayTracking, VerificationProgress, fetch_ancestor,
};
use crate::stateful::{
Application, Input, Proposed, PruneConfig,
actor::metrics::Metrics as StatefulMetrics,
db::{Anchor, Barrier, DatabaseSet, Merkleized as _, Shared, Unmerkleized as _},
};
use commonware_codec::{Encode, EncodeSize, Error as CodecError, Read, ReadExt as _, Write};
use commonware_consensus::{
Block as ConsensusBlock, CertifiableBlock, Heightable,
marshal::ancestry::{Ancestry, BlockProvider},
simplex::{mocks::scheme::Scheme as MockScheme, types::Context as ConsensusContext},
types::{Epoch, Height, Round, View},
};
use commonware_cryptography::{
Digest as _, Digestible, Hasher, Sha256, Signer as _, ed25519, sha256::Digest,
};
use commonware_macros::{boxed, select};
use commonware_parallel::Sequential;
use commonware_runtime::{
Clock as _, ContextCell, Runner as _, Supervisor as _, buffer::paged::CacheRef,
deterministic,
};
use commonware_storage::{
journal::contiguous::fixed::Config as FixedLogConfig,
mmr::{self, Location, full::Config as MmrJournalConfig},
qmdb::{any, sync::Target},
translator::TwoCap,
};
use commonware_utils::{
NZU16, NZU64, NZUsize, channel::oneshot, non_empty_range, range::NonEmptyRange, sync::Mutex,
};
use futures::StreamExt;
use std::{
collections::{BTreeMap, HashSet, VecDeque},
future::Future,
num::NonZeroUsize,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
time::Duration,
};
async fn assert_durable(barrier: Option) {
assert!(
barrier
.expect("finalization must start durability")
.durable()
.await,
"database sync must complete",
);
}
type TestContext = ConsensusContext;
const PAGE_SIZE: std::num::NonZeroU16 = NZU16!(1024);
const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(8);
const IO_BUFFER_SIZE: NonZeroUsize = NZUsize!(2048);
type Qmdb =
any::unordered::fixed::Db;
type DbSet = Shared>;
type TestMerkleized =
as DatabaseSet>::Merkleized;
#[test]
fn finalization_dispositions_preserve_winner_and_descendant_work() {
let boundary = FinalizationBoundary {
digest: 10,
round: Round::new(Epoch::zero(), View::new(10)),
processed_digest: 9,
processed_round: Round::new(Epoch::zero(), View::new(9)),
compatible: HashSet::from([10, 11]),
};
let progress = VerificationProgress::default();
assert_eq!(boundary.disposition(&progress), Disposition::Retry,);
progress.replaying(9, 8, Round::new(Epoch::zero(), View::new(9)));
assert_eq!(boundary.disposition(&progress), Disposition::Retry,);
progress.replaying(10, 1, Round::new(Epoch::zero(), View::new(10)));
assert_eq!(boundary.disposition(&progress), Disposition::Retain,);
progress.replaying(12, 11, Round::new(Epoch::zero(), View::new(11)));
assert_eq!(boundary.disposition(&progress), Disposition::Retain,);
progress.replaying(20, 19, Round::new(Epoch::zero(), View::new(11)));
assert_eq!(boundary.disposition(&progress), Disposition::Reject,);
progress.verifying(9, 8, Round::new(Epoch::zero(), View::new(9)));
assert_eq!(boundary.disposition(&progress), Disposition::Retry,);
progress.verifying(10, 1, Round::new(Epoch::zero(), View::new(10)));
assert_eq!(boundary.disposition(&progress), Disposition::Retry,);
progress.verifying(12, 11, Round::new(Epoch::zero(), View::new(11)));
assert_eq!(boundary.disposition(&progress), Disposition::Retain,);
progress.verifying(20, 19, Round::new(Epoch::zero(), View::new(11)));
assert_eq!(boundary.disposition(&progress), Disposition::Reject,);
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Block {
context: TestContext,
parent: Digest,
height: Height,
state_root: Digest,
range: NonEmptyRange,
}
impl Write for Block {
fn write(&self, buf: &mut impl commonware_runtime::BufMut) {
self.context.write(buf);
self.parent.write(buf);
self.height.write(buf);
self.state_root.write(buf);
self.range.write(buf);
}
}
impl EncodeSize for Block {
fn encode_size(&self) -> usize {
self.context.encode_size()
+ self.parent.encode_size()
+ self.height.encode_size()
+ self.state_root.encode_size()
+ self.range.encode_size()
}
}
impl Read for Block {
type Cfg = ();
fn read_cfg(
buf: &mut impl commonware_runtime::Buf,
_: &Self::Cfg,
) -> Result {
Ok(Self {
context: TestContext::read(buf)?,
parent: Digest::read(buf)?,
height: Height::read(buf)?,
state_root: Digest::read(buf)?,
range: commonware_utils::range::NonEmptyRange::read(buf)?,
})
}
}
impl Digestible for Block {
type Digest = Digest;
fn digest(&self) -> Digest {
Sha256::hash(&[&self.encode()])
}
}
impl Heightable for Block {
fn height(&self) -> Height {
self.height
}
}
impl ConsensusBlock for Block {
fn parent(&self) -> Digest {
self.parent
}
}
impl CertifiableBlock for Block {
type Context = TestContext;
fn context(&self) -> Self::Context {
self.context.clone()
}
}
impl Block {
fn genesis() -> Self {
Self {
context: consensus_context(Digest::EMPTY, View::zero()),
parent: Digest::EMPTY,
height: Height::zero(),
state_root: Digest::EMPTY,
range: non_empty_range!(Location::new(0), Location::new(1)),
}
}
}
fn consensus_context(parent: Digest, view: View) -> TestContext {
TestContext {
round: Round::new(Epoch::zero(), view),
leader: ed25519::PrivateKey::from_seed(0).public_key(),
parent: (
if view.is_zero() {
View::zero()
} else {
View::new(view.get() - 1)
},
parent,
),
}
}
fn u64_to_digest(value: u64) -> Digest {
let mut bytes = [0u8; 32];
bytes[..8].copy_from_slice(&value.to_be_bytes());
Digest::from(bytes)
}
fn digest_to_u64(value: &Digest) -> u64 {
let bytes: &[u8] = value.as_ref();
u64::from_be_bytes(
bytes[..8]
.try_into()
.expect("digest prefix should be 8 bytes"),
)
}
fn height_key(height: Height) -> Digest {
Sha256::hash(&[&height.get().to_be_bytes()])
}
fn counter_key() -> Digest {
Sha256::hash(&[b"processor_harness_counter"])
}
struct ApplyGate {
started: oneshot::Sender<()>,
release: oneshot::Receiver<()>,
}
#[derive(Clone)]
struct ApplicationProbe {
target: Digest,
calls: Arc,
gates: Arc>>,
}
impl ApplicationProbe {
fn new(target: Digest, gates: impl IntoIterator- ) -> Self {
Self {
target,
calls: Arc::new(AtomicUsize::new(0)),
gates: Arc::new(Mutex::new(gates.into_iter().collect())),
}
}
async fn call(&self, digest: Digest) {
if digest != self.target {
return;
}
self.calls.fetch_add(1, Ordering::SeqCst);
let Some(mut gate) = self.gates.lock().pop_front() else {
return;
};
gate.started.send(()).expect("test must await replay");
let _ = (&mut gate.release).await;
}
fn calls(&self) -> usize {
self.calls.load(Ordering::SeqCst)
}
}
fn apply_gate() -> (ApplyGate, oneshot::Receiver<()>, oneshot::Sender<()>) {
let (started, started_rx) = oneshot::channel();
let (release, release_rx) = oneshot::channel();
(
ApplyGate {
started,
release: release_rx,
},
started_rx,
release,
)
}
#[derive(Debug, PartialEq, Eq)]
struct Captured {
prior_counter: Option
,
batch_counter: u64,
batch_view: u64,
}
#[derive(Debug, PartialEq, Eq)]
struct FinalizedObservation {
captured: Captured,
post_counter: u64,
post_view: u64,
}
#[derive(Clone)]
struct ExecutionApp {
genesis: Block,
finalized_observer: Option>>>,
apply_probe: Option,
finalized_probe: Option,
}
impl ExecutionApp {
fn new() -> Self {
Self {
genesis: Block::genesis(),
finalized_observer: None,
apply_probe: None,
finalized_probe: None,
}
}
fn with_finalized_observer() -> (Self, Arc>>) {
let observations = Arc::new(Mutex::new(Vec::new()));
(
Self {
genesis: Block::genesis(),
finalized_observer: Some(observations.clone()),
apply_probe: None,
finalized_probe: None,
},
observations,
)
}
async fn execute(
height: Height,
view: View,
mut batches: as DatabaseSet>::Unmerkleized,
) -> as DatabaseSet>::Merkleized
{
let current_counter = batches
.get(&counter_key())
.await
.expect("counter read should succeed")
.map_or(0, |digest| digest_to_u64(&digest));
batches = batches.write(counter_key(), Some(u64_to_digest(current_counter + 1)));
batches = batches.write(height_key(height), Some(u64_to_digest(view.get())));
batches.merkleize().await.expect("merkleize should succeed")
}
}
impl Application for ExecutionApp {
type SigningScheme = MockScheme;
type Context = TestContext;
type Block = Block;
type Databases = DbSet;
type Captured = Captured;
type Provider = ();
type Input = ();
async fn genesis(&mut self) -> Self::Block {
self.genesis.clone()
}
async fn propose(
&mut self,
context: (deterministic::Context, Self::Context),
ancestry: impl Ancestry,
batches: >::Unmerkleized,
_input: Input,
) -> Option> {
let mut ancestry = Box::pin(ancestry);
let parent = ancestry.next().await?;
let context = context.1.clone();
let view = context.round.view();
let height = parent.height().next();
let merkleized = Self::execute(height, view, batches).await;
let block = Block {
context,
parent: parent.digest(),
height,
state_root: merkleized.root(),
range: non_empty_range!(
merkleized.bounds().inactivity_floor,
merkleized.bounds().tip.size
),
};
Some(Proposed { block, merkleized })
}
async fn verify(
&mut self,
_context: (deterministic::Context, Self::Context),
ancestry: impl Ancestry,
batches: >::Unmerkleized,
) -> Option<>::Merkleized> {
let mut ancestry = Box::pin(ancestry);
let block = ancestry.next().await?;
let merkleized =
Self::execute(block.height(), block.context.round.view(), batches).await;
if merkleized.root() != block.state_root {
return None;
}
Some(merkleized)
}
async fn apply(
&mut self,
_context: (deterministic::Context, Self::Context),
block: &Self::Block,
batches: >::Unmerkleized,
) -> Option<>::Merkleized> {
if let Some(probe) = &self.apply_probe {
probe.call(block.digest()).await;
}
Some(Self::execute(block.height(), block.context.round.view(), batches).await)
}
async fn capture(
&mut self,
_context: (deterministic::Context, Self::Context),
block: &Self::Block,
batches: &TestMerkleized,
readers: >::Readers,
) -> Self::Captured {
let prior_counter = readers
.read()
.await
.get(&counter_key())
.await
.expect("database read should succeed")
.map(|value| digest_to_u64(&value));
let pending = batches.new_batch();
let batch_counter = pending
.get(&counter_key())
.await
.expect("batch read should succeed")
.map(|value| digest_to_u64(&value))
.expect("winning batch should contain a counter");
let batch_view = pending
.get(&height_key(block.height()))
.await
.expect("batch read should succeed")
.map(|value| digest_to_u64(&value))
.expect("winning batch should contain its view");
Captured {
prior_counter,
batch_counter,
batch_view,
}
}
async fn finalized(
&mut self,
_context: (deterministic::Context, Self::Context),
block: &Self::Block,
captured: Self::Captured,
readers: >::Readers,
) {
if let Some(probe) = &self.finalized_probe {
probe.call(block.digest()).await;
}
let Some(observer) = &self.finalized_observer else {
return;
};
let db = readers.read().await;
let post_view = db
.get(&height_key(block.height()))
.await
.expect("database read should succeed")
.map(|value| digest_to_u64(&value))
.expect("finalized view should be reflected in the database set");
let post_counter = db
.get(&counter_key())
.await
.expect("database read should succeed")
.map(|value| digest_to_u64(&value))
.expect("finalized counter should be reflected in the database set");
drop(db);
let observation = FinalizedObservation {
captured,
post_counter,
post_view,
};
observer.lock().push(observation);
}
fn sync_targets(
block: &Self::Block,
) -> >::SyncTargets {
Target::new(block.state_root, block.range.clone())
}
}
#[derive(Clone, Default)]
struct MapProvider {
blocks: Arc>>,
fetches: Arc,
}
impl MapProvider {
fn insert(&self, block: Block) {
self.blocks.lock().insert(block.digest(), block);
}
fn fetch_by_digest(&self, digest: Digest) -> Option {
self.fetches.fetch_add(1, Ordering::SeqCst);
self.blocks.lock().get(&digest).cloned()
}
fn fetches(&self) -> usize {
self.fetches.load(Ordering::SeqCst)
}
}
impl BlockProvider for MapProvider {
type Block = Block;
fn subscribe_parent(
&self,
block: &Self::Block,
) -> impl Future>> + Send + 'static {
let provider = self.clone();
let parent = block.parent();
async move { provider.fetch_by_digest(parent).map(Arc::new) }
}
}
#[derive(Clone, Default)]
struct ScriptedParentProvider {
responses: Arc>>>>,
fetches: Arc,
}
impl ScriptedParentProvider {
fn push(&self, child: &Block, responses: impl IntoIterator- >) {
self.responses
.lock()
.insert(child.digest(), responses.into_iter().collect());
}
fn fetches(&self) -> usize {
self.fetches.load(Ordering::SeqCst)
}
}
impl BlockProvider for ScriptedParentProvider {
type Block = Block;
fn subscribe_parent(
&self,
block: &Self::Block,
) -> impl Future
>> + Send + 'static {
let provider = self.clone();
let child = block.digest();
async move {
provider.fetches.fetch_add(1, Ordering::SeqCst);
provider
.responses
.lock()
.get_mut(&child)
.and_then(VecDeque::pop_front)
.flatten()
.map(Arc::new)
}
}
}
struct Harness {
context_cell: ContextCell,
processor: Processor,
provider: MapProvider,
db_config: any::FixedConfig,
}
impl Harness {
async fn new(context: deterministic::Context) -> Self {
let provider = MapProvider::default();
let config = qmdb_config(&next_partition_prefix(), &context);
Self::with_app(context, provider, config.clone(), ExecutionApp::new()).await
}
async fn new_with_finalized_observer(
context: deterministic::Context,
) -> (Self, Arc>>) {
let provider = MapProvider::default();
let config = qmdb_config(&next_partition_prefix(), &context);
let (app, observations) = ExecutionApp::with_finalized_observer();
(
Self::with_app(context, provider, config, app).await,
observations,
)
}
async fn with_app(
context: deterministic::Context,
provider: MapProvider,
config: any::FixedConfig,
app: ExecutionApp,
) -> Self {
let databases = as DatabaseSet<
deterministic::Context,
>>::init(context.child("db_set"), config.clone())
.await;
let metrics = StatefulMetrics::new(&context);
Self {
context_cell: ContextCell::new(context),
processor: Processor::new(
app,
databases,
Anchor {
height: Height::zero(),
round: Block::genesis().context().round,
digest: Block::genesis().digest(),
},
metrics,
None,
),
provider,
db_config: config,
}
}
async fn build_child(&self, parent: &Block, view: View) -> (Block, TestMerkleized) {
let context = consensus_context(parent.digest(), view);
let height = Height::new(parent.height().get() + 1);
let batches = self
.processor
.fork_batches(&parent.digest())
.await
.expect("parent should be available");
let merkleized = ExecutionApp::execute(height, view, batches).await;
let block = Block {
context,
parent: parent.digest(),
height,
state_root: merkleized.root(),
range: non_empty_range!(
merkleized.bounds().inactivity_floor,
merkleized.bounds().tip.size
),
};
(block, merkleized)
}
async fn stage_pending_child(&mut self, parent: &Block, view: View) -> Block {
let (block, merkleized) = self.build_child(parent, view).await;
let round = Round::new(Epoch::zero(), view);
assert!(self.processor.cache_pending(
block.digest(),
parent.digest(),
round,
merkleized,
true,
));
self.provider.insert(block.clone());
block
}
/// Finalize `block` and wait for its database sync.
/// Returns whether the block was newly applied (`false` for a
/// duplicate report).
#[boxed]
async fn finalize(&mut self, block: Block) -> bool {
let Some(Applied { barrier, .. }) = self
.processor
.finalize(self.context_cell.as_present(), &block, true)
.await
else {
return false;
};
assert_durable(barrier).await;
true
}
#[boxed]
async fn finalize_with_prune(
&mut self,
block: Block,
) -> Option<
Prune<
as DatabaseSet>::SyncTargets,
>,
> {
let Applied { barrier, prune } = self
.processor
.finalize(self.context_cell.as_present(), &block, true)
.await
.expect("finalized block must apply");
assert_durable(barrier).await;
prune
}
async fn view_at_height(&self, height: Height) -> Option {
let db = self.processor.databases().read().await;
db.get(&height_key(height))
.await
.expect("database read should succeed")
.map(|value| digest_to_u64(&value))
}
async fn counter_value(&self) -> Option {
let db = self.processor.databases().read().await;
db.get(&counter_key())
.await
.expect("database read should succeed")
.map(|value| digest_to_u64(&value))
}
async fn reopen_view_at_height(
&self,
context: deterministic::Context,
height: Height,
) -> Option {
let reopened: Qmdb =
Qmdb::init(context.child("reopen_db"), self.db_config.clone())
.await
.expect("database reopen should succeed");
reopened
.get(&height_key(height))
.await
.expect("reopened db read should succeed")
.map(|value| digest_to_u64(&value))
}
}
fn next_partition_prefix() -> String {
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
format!("processor_harness_{id}")
}
fn qmdb_config(
prefix: &str,
context: &deterministic::Context,
) -> any::FixedConfig {
let page_cache = CacheRef::from_pooler(context, PAGE_SIZE, PAGE_CACHE_SIZE);
any::FixedConfig {
merkle_config: MmrJournalConfig {
journal_partition: format!("{prefix}_mmr_journal"),
metadata_partition: format!("{prefix}_mmr_metadata"),
items_per_blob: NZU64!(11),
write_buffer: IO_BUFFER_SIZE,
replay_buffer: IO_BUFFER_SIZE,
strategy: Sequential,
page_cache: page_cache.clone(),
},
journal_config: FixedLogConfig {
partition: format!("{prefix}_log_journal"),
items_per_blob: NZU64!(7),
page_cache,
write_buffer: IO_BUFFER_SIZE,
replay_buffer: IO_BUFFER_SIZE,
},
translator: TwoCap,
init_cache_size: Some(NZUsize!(1024)),
init_buffer: NZUsize!(1 << 21),
init_concurrency: (),
}
}
#[test]
fn pruning_waits_for_full_retention_window() {
let config = PruneConfig {
maintenance_interval: NZUsize!(1),
retained_marshal_blocks: 1,
retained_qmdb_blocks: 1,
};
let mut pruning = Pruning::build(config, 2, 0);
assert_eq!(pruning.observe_finalized(Height::new(1), 10_u64), None,);
assert_eq!(pruning.observe_finalized(Height::new(2), 20_u64), None,);
assert_eq!(pruning.observe_finalized(Height::new(3), 30_u64), None,);
assert_eq!(
pruning.observe_finalized(Height::new(4), 40_u64),
Some(Prune {
marshal_height: Height::new(1),
barrier_height: Height::new(1),
qmdb_target: 10,
}),
);
}
#[test]
fn pruning_uses_oldest_retained_target() {
let config = PruneConfig {
maintenance_interval: NZUsize!(1),
retained_marshal_blocks: 1,
retained_qmdb_blocks: 1,
};
let mut pruning = Pruning::build(config, 1, 0);
assert_eq!(pruning.observe_finalized(Height::new(1), 10_u64), None,);
assert_eq!(pruning.observe_finalized(Height::new(2), 20_u64), None,);
assert_eq!(
pruning.observe_finalized(Height::new(3), 30_u64),
Some(Prune {
marshal_height: Height::new(1),
barrier_height: Height::new(1),
qmdb_target: 10,
}),
);
assert_eq!(
pruning.observe_finalized(Height::new(4), 40_u64),
Some(Prune {
marshal_height: Height::new(2),
barrier_height: Height::new(2),
qmdb_target: 20,
}),
);
}
#[test]
fn pruning_can_retain_more_marshal_history_than_qmdb() {
let config = PruneConfig {
maintenance_interval: NZUsize!(3),
retained_marshal_blocks: 3,
retained_qmdb_blocks: 1,
};
let mut pruning = Pruning::build(config, 1, 0);
assert_eq!(pruning.observe_finalized(Height::new(1), 10_u64), None);
assert_eq!(pruning.observe_finalized(Height::new(2), 20_u64), None);
assert_eq!(pruning.observe_finalized(Height::new(3), 30_u64), None);
assert_eq!(pruning.observe_finalized(Height::new(4), 40_u64), None);
assert_eq!(pruning.observe_finalized(Height::new(5), 50_u64), None);
assert_eq!(
pruning.observe_finalized(Height::new(6), 60_u64),
Some(Prune {
marshal_height: Height::new(2),
barrier_height: Height::new(4),
qmdb_target: 40,
}),
);
}
#[test]
fn pruning_uses_maintenance_phase() {
let config = PruneConfig {
maintenance_interval: NZUsize!(5),
retained_marshal_blocks: 1,
retained_qmdb_blocks: 0,
};
let mut pruning = Pruning::build(config, 1, 2);
for height in 1..=6 {
assert_eq!(
pruning.observe_finalized(Height::new(height), height * 10),
None,
);
}
assert_eq!(
pruning.observe_finalized(Height::new(7), 70),
Some(Prune {
marshal_height: Height::new(5),
barrier_height: Height::new(6),
qmdb_target: 60,
}),
);
for height in 8..=11 {
assert_eq!(
pruning.observe_finalized(Height::new(height), height * 10),
None,
);
}
assert_eq!(
pruning.observe_finalized(Height::new(12), 120),
Some(Prune {
marshal_height: Height::new(10),
barrier_height: Height::new(11),
qmdb_target: 110,
}),
);
}
#[test]
#[should_panic(expected = "marshal must retain at least as many blocks as QMDB")]
fn prune_config_rejects_less_marshal_retention_than_qmdb() {
PruneConfig {
maintenance_interval: NZUsize!(1),
retained_marshal_blocks: 1,
retained_qmdb_blocks: 2,
}
.assert_valid();
}
#[test]
fn prune_config_accepts_zero_retention() {
PruneConfig {
maintenance_interval: NZUsize!(1),
retained_marshal_blocks: 0,
retained_qmdb_blocks: 0,
}
.assert_valid();
}
#[test]
fn execution_finalization_returns_deferred_prune() {
deterministic::Runner::default().start(|context| async move {
let provider = MapProvider::default();
let config = qmdb_config("db_config", &context);
let app = ExecutionApp::new();
let mut harness = Harness::with_app(context, provider, config, app).await;
harness.processor = Processor::new(
ExecutionApp::new(),
harness.processor.databases().clone(),
Anchor {
height: Height::zero(),
round: Block::genesis().context().round,
digest: Block::genesis().digest(),
},
StatefulMetrics::new(harness.context_cell.as_present()),
Some(Pruning::build(
PruneConfig {
maintenance_interval: NZUsize!(1),
retained_marshal_blocks: 1,
retained_qmdb_blocks: 1,
},
1,
0,
)),
);
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
let prune = harness.finalize_with_prune(block1).await;
assert_eq!(
prune, None,
"pruning should wait for the full retention window",
);
});
}
#[test]
fn execution_finalization_prunes_losing_fork() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
let winner = harness.stage_pending_child(&block1, View::new(3)).await;
let loser = harness.stage_pending_child(&block1, View::new(2)).await;
assert!(harness.processor.pending_contains(&winner.digest()));
assert!(harness.processor.pending_contains(&loser.digest()));
assert!(
harness.finalize(winner.clone()).await,
"finalization should persist winner state",
);
assert!(
!harness.processor.pending_contains(&loser.digest()),
"losing fork at finalized round should be pruned",
);
assert_eq!(harness.processor.last_processed().digest, winner.digest());
assert_eq!(harness.view_at_height(Height::new(2)).await, Some(3));
});
}
#[test]
fn execution_finalization_prunes_losing_fork_descendants() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
let loser = harness.stage_pending_child(&block1, View::new(2)).await;
let winner = harness.stage_pending_child(&block1, View::new(3)).await;
let loser_child = harness.stage_pending_child(&loser, View::new(4)).await;
assert!(harness.processor.pending_contains(&winner.digest()));
assert!(harness.processor.pending_contains(&loser.digest()));
assert!(harness.processor.pending_contains(&loser_child.digest()));
assert!(
harness.finalize(winner.clone()).await,
"finalization should persist winner state",
);
assert!(
!harness.processor.pending_contains(&loser.digest()),
"losing fork at finalized round should be pruned",
);
assert!(
!harness.processor.pending_contains(&loser_child.digest()),
"descendants of the losing fork should also be pruned",
);
});
}
#[test]
fn execution_finalization_prunes_before_finalized_hook_completes() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
let loser = harness.stage_pending_child(&block1, View::new(2)).await;
let winner = harness.stage_pending_child(&block1, View::new(3)).await;
let winner_child = harness.stage_pending_child(&winner, View::new(4)).await;
let (gate, started, release) = apply_gate();
harness.processor.app.finalized_probe =
Some(ApplicationProbe::new(winner.digest(), [gate]));
let execution = harness.processor.execution.clone();
let mut finalize = Box::pin(harness.processor.finalize(
harness.context_cell.as_present(),
&winner,
true,
));
assert!(futures::poll!(&mut finalize).is_pending());
started.await.expect("finalized hook should start");
assert!(execution.pending_contains(&winner_child.digest()));
assert!(!execution.pending_contains(&block1.digest()));
assert!(!execution.pending_contains(&loser.digest()));
release
.send(())
.expect("finalized hook should remain active");
let Applied { barrier, .. } = finalize
.await
.expect("finalized block should be newly applied");
assert_durable(barrier).await;
});
}
#[test]
fn execution_forks_from_finalizing_winner_before_database_apply() {
deterministic::Runner::timed(Duration::from_secs(5)).start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let winner = harness.stage_pending_child(&genesis, View::new(1)).await;
let databases = harness.processor.databases().clone();
let read = databases.read().await;
let execution = harness.processor.execution.clone();
let mut finalize = Box::pin(harness.processor.finalize(
harness.context_cell.as_present(),
&winner,
true,
));
assert!(futures::poll!(&mut finalize).is_pending());
let winner_digest = winner.digest();
let mut fork = Box::pin(execution.fork_batches(&winner_digest));
let forked = match futures::poll!(&mut fork) {
std::task::Poll::Ready(forked) => forked,
std::task::Poll::Pending => {
panic!("finalizing winner should remain available for child batches")
}
};
assert!(forked.is_ok(), "finalizing winner should remain forkable");
drop(read);
let Applied { barrier, .. } = finalize
.await
.expect("finalized block should be newly applied");
assert_durable(barrier).await;
});
}
#[test]
fn execution_late_winner_publication_is_a_noop() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let view = View::new(1);
let round = Round::new(Epoch::zero(), view);
let (winner, initial_batch) = harness.build_child(&genesis, view).await;
let (_, during_finalization_batch) = harness.build_child(&genesis, view).await;
let (_, after_finalization_batch) = harness.build_child(&genesis, view).await;
assert!(harness.processor.cache_pending(
winner.digest(),
genesis.digest(),
round,
initial_batch,
true,
));
let (gate, started, release) = apply_gate();
harness.processor.app.finalized_probe =
Some(ApplicationProbe::new(winner.digest(), [gate]));
let execution = harness.processor.execution.clone();
let mut finalize = Box::pin(harness.processor.finalize(
harness.context_cell.as_present(),
&winner,
true,
));
assert!(futures::poll!(&mut finalize).is_pending());
started.await.expect("finalized hook should start");
assert!(execution.cache_pending(
winner.digest(),
genesis.digest(),
round,
during_finalization_batch,
true,
));
assert!(!execution.pending_contains(&winner.digest()));
release
.send(())
.expect("finalized hook should remain active");
let Applied { barrier, .. } = finalize
.await
.expect("finalized block should be newly applied");
assert_durable(barrier).await;
assert!(execution.cache_pending(
winner.digest(),
genesis.digest(),
round,
after_finalization_batch,
true,
));
assert!(!execution.pending_contains(&winner.digest()));
});
}
#[test]
fn execution_descendant_replay_survives_finalized_parent_removal() {
deterministic::Runner::timed(Duration::from_secs(5)).start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let parent = harness.stage_pending_child(&genesis, View::new(1)).await;
let (child, _) = harness.build_child(&parent, View::new(2)).await;
let (owner_gate, owner_started, mut owner_release) = apply_gate();
let (retry_gate, retry_started, retry_release) = apply_gate();
harness.processor.app.apply_probe = Some(ApplicationProbe::new(
child.digest(),
[owner_gate, retry_gate],
));
let (finalized_gate, finalized_started, finalized_release) = apply_gate();
harness.processor.app.finalized_probe =
Some(ApplicationProbe::new(parent.digest(), [finalized_gate]));
let execution = harness.processor.execution.clone();
let replays = harness.processor.replays.clone();
let mut owner_app = harness.processor.app.clone();
let mut waiter_app = harness.processor.app.clone();
let replay_context = harness.context_cell.as_present();
let owner_progress = VerificationProgress::default();
let waiter_progress = VerificationProgress::default();
let (mut owner_cancellation, owner_alive) = oneshot::channel::<()>();
let (mut waiter_cancellation, _waiter_alive) = oneshot::channel::<()>();
let mut owner = Box::pin(execution.replay_block_shared(
&mut owner_app,
replay_context,
child.digest(),
Arc::new(child.clone()),
&mut owner_cancellation,
ReplayTracking {
flights: &replays,
progress: &owner_progress,
},
));
assert!(futures::poll!(&mut owner).is_pending());
owner_started.await.expect("replay owner should start");
let mut waiter = Box::pin(execution.replay_block_shared(
&mut waiter_app,
replay_context,
child.digest(),
Arc::new(child.clone()),
&mut waiter_cancellation,
ReplayTracking {
flights: &replays,
progress: &waiter_progress,
},
));
assert!(futures::poll!(&mut waiter).is_pending());
let boundary = harness.processor.finalization_boundary(&parent);
assert_eq!(boundary.disposition(&owner_progress), Disposition::Retain,);
assert_eq!(boundary.disposition(&waiter_progress), Disposition::Retain,);
let mut finalize = Box::pin(harness.processor.finalize(
harness.context_cell.as_present(),
&parent,
true,
));
assert!(futures::poll!(&mut finalize).is_pending());
finalized_started
.await
.expect("finalized hook should start");
drop(owner_alive);
assert_eq!(owner.await, Err(PrepareBatchesError::Cancelled));
owner_release.closed().await;
select! {
result = &mut waiter => {
panic!("retained replay failed after owner cancellation: {result:?}");
},
result = retry_started => {
result.expect("retained replay should restart from finalized state");
},
}
retry_release
.send(())
.expect("retried replay should remain active");
assert_eq!(waiter.await, Ok(()));
finalized_release
.send(())
.expect("finalized hook should remain active");
let Applied { barrier, .. } = finalize
.await
.expect("finalized block should be newly applied");
assert_durable(barrier).await;
});
}
#[test]
fn finalized_reader_preserves_retained_replay_base() {
deterministic::Runner::timed(Duration::from_secs(5)).start(|context| async move {
let (mut harness, observations) = Harness::new_with_finalized_observer(context).await;
let genesis = Block::genesis();
let parent = harness.stage_pending_child(&genesis, View::new(1)).await;
let (child, _) = harness.build_child(&parent, View::new(2)).await;
let (owner_gate, owner_started, mut owner_release) = apply_gate();
let (retry_gate, retry_started, retry_release) = apply_gate();
harness.processor.app.apply_probe = Some(ApplicationProbe::new(
child.digest(),
[owner_gate, retry_gate],
));
let (finalized_gate, finalized_started, finalized_release) = apply_gate();
harness.processor.app.finalized_probe =
Some(ApplicationProbe::new(parent.digest(), [finalized_gate]));
let execution = harness.processor.execution.clone();
let replays = harness.processor.replays.clone();
let mut owner_app = harness.processor.app.clone();
let mut waiter_app = harness.processor.app.clone();
let replay_context = harness.context_cell.as_present();
let owner_progress = VerificationProgress::default();
let waiter_progress = VerificationProgress::default();
let (mut owner_cancellation, owner_alive) = oneshot::channel::<()>();
let (mut waiter_cancellation, _waiter_alive) = oneshot::channel::<()>();
let mut owner = Box::pin(execution.replay_block_shared(
&mut owner_app,
replay_context,
child.digest(),
Arc::new(child.clone()),
&mut owner_cancellation,
ReplayTracking {
flights: &replays,
progress: &owner_progress,
},
));
assert!(futures::poll!(&mut owner).is_pending());
owner_started.await.expect("replay owner should start");
let mut waiter = Box::pin(execution.replay_block_shared(
&mut waiter_app,
replay_context,
child.digest(),
Arc::new(child),
&mut waiter_cancellation,
ReplayTracking {
flights: &replays,
progress: &waiter_progress,
},
));
assert!(futures::poll!(&mut waiter).is_pending());
let mut finalize = Box::pin(harness.processor.finalize(
harness.context_cell.as_present(),
&parent,
true,
));
assert!(futures::poll!(&mut finalize).is_pending());
finalized_started
.await
.expect("finalized hook should start");
drop(owner_alive);
assert_eq!(owner.await, Err(PrepareBatchesError::Cancelled));
owner_release.closed().await;
select! {
result = &mut waiter => {
panic!("retained replay failed after owner cancellation: {result:?}");
},
result = retry_started => {
result.expect("retained replay should restart from finalized state");
},
}
finalized_release
.send(())
.expect("finalized hook should remain active");
let Applied { barrier, .. } = finalize
.await
.expect("finalized block should be newly applied");
assert_durable(barrier).await;
assert!(matches!(
observations.lock().as_slice(),
[FinalizedObservation { post_view: 1, .. }]
));
retry_release
.send(())
.expect("retried replay should remain active");
assert_eq!(waiter.await, Ok(()));
});
}
#[test]
fn execution_finalize_self_applies_after_cancelled_winner_replay() {
deterministic::Runner::timed(Duration::from_secs(5)).start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let (winner, _) = harness.build_child(&genesis, View::new(1)).await;
let (owner_gate, owner_started, mut owner_release) = apply_gate();
let probe = ApplicationProbe::new(winner.digest(), [owner_gate]);
harness.processor.app.apply_probe = Some(probe.clone());
let execution = harness.processor.execution.clone();
let replays = harness.processor.replays.clone();
let mut owner_app = harness.processor.app.clone();
let replay_context = harness.context_cell.as_present();
let (mut owner_cancellation, owner_alive) = oneshot::channel::<()>();
let owner_progress = VerificationProgress::default();
let mut owner = Box::pin(execution.replay_block_shared(
&mut owner_app,
replay_context,
winner.digest(),
Arc::new(winner.clone()),
&mut owner_cancellation,
ReplayTracking {
flights: &replays,
progress: &owner_progress,
},
));
assert!(futures::poll!(&mut owner).is_pending());
owner_started.await.expect("winner replay should start");
let mut finalize = Box::pin(harness.processor.finalize(
harness.context_cell.as_present(),
&winner,
true,
));
assert!(
futures::poll!(&mut finalize).is_pending(),
"finalization should wait on the active winner replay",
);
drop(owner_alive);
assert_eq!(owner.await, Err(PrepareBatchesError::Cancelled));
owner_release.closed().await;
let Applied { barrier, .. } = finalize
.await
.expect("finalized block should be newly applied");
assert_durable(barrier).await;
assert_eq!(
probe.calls(),
2,
"finalization must reconstruct the winner after the owner cancels",
);
assert_eq!(harness.processor.last_processed().digest, winner.digest());
assert!(harness.processor.replays_idle());
});
}
#[test]
fn execution_replay_waiter_recovers_from_invalid_finalizing_winner() {
deterministic::Runner::timed(Duration::from_secs(5)).start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let (winner, _) = harness.build_child(&genesis, View::new(1)).await;
let probe = ApplicationProbe::new(winner.digest(), std::iter::empty());
harness.processor.app.apply_probe = Some(probe.clone());
let owner = match harness
.processor
.execution
.claim_replay(&harness.processor.replays, winner.digest())
{
ReplayClaim::Owner(owner) => owner,
_ => panic!("winner replay claim should own the flight"),
};
let execution = harness.processor.execution.clone();
let replays = harness.processor.replays.clone();
let mut waiter_app = harness.processor.app.clone();
let replay_context = harness.context_cell.as_present();
let waiter_progress = VerificationProgress::default();
let (mut waiter_cancellation, _waiter_alive) = oneshot::channel::<()>();
let mut waiter = Box::pin(execution.replay_block_shared(
&mut waiter_app,
replay_context,
winner.digest(),
Arc::new(winner.clone()),
&mut waiter_cancellation,
ReplayTracking {
flights: &replays,
progress: &waiter_progress,
},
));
assert!(futures::poll!(&mut waiter).is_pending());
let mut finalize = Box::pin(harness.processor.finalize(
harness.context_cell.as_present(),
&winner,
true,
));
assert!(
futures::poll!(&mut finalize).is_pending(),
"finalization should wait on the active winner replay",
);
owner.finish(Err(PrepareBatchesError::Invalid));
assert_eq!(
waiter.await,
Ok(()),
"retained waiter should join recovery of the finalizing winner",
);
let Applied { barrier, .. } = finalize
.await
.expect("finalized block should be newly applied");
assert_durable(barrier).await;
assert_eq!(probe.calls(), 1, "winner should be reconstructed once");
assert_eq!(harness.processor.last_processed().digest, winner.digest());
assert!(harness.processor.replays_idle());
});
}
#[test]
fn execution_finalization_waits_for_active_cached_winner_replay() {
deterministic::Runner::timed(Duration::from_secs(5)).start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let (winner, merkleized) = harness.build_child(&genesis, View::new(1)).await;
let owner = match harness
.processor
.execution
.claim_replay(&harness.processor.replays, winner.digest())
{
ReplayClaim::Owner(owner) => owner,
_ => panic!("winner replay should own the flight"),
};
assert!(harness.processor.cache_pending(
winner.digest(),
genesis.digest(),
winner.context().round,
merkleized,
true,
));
let (gate, mut started, release) = apply_gate();
harness.processor.app.finalized_probe =
Some(ApplicationProbe::new(winner.digest(), [gate]));
let mut finalize = Box::pin(harness.processor.finalize(
harness.context_cell.as_present(),
&winner,
true,
));
select! {
_ = &mut finalize => {
panic!("finalization bypassed active winner replay");
},
result = &mut started => {
result.expect("finalized hook should remain reachable");
panic!("finalization reached the application hook before replay completed");
},
_ = harness.context_cell.as_present().sleep(Duration::from_millis(10)) => {},
}
owner.finish(Ok(()));
select! {
result = &mut started => {
result.expect("finalized hook should start after replay completes");
},
_ = &mut finalize => {
panic!("finalization completed before its application hook");
},
}
release
.send(())
.expect("finalized hook should remain active");
let Applied { barrier, .. } = finalize
.await
.expect("finalized block should be newly applied");
assert_durable(barrier).await;
});
}
#[test]
fn execution_replay_waiter_reuses_retained_finalizing_winner() {
deterministic::Runner::timed(Duration::from_secs(5)).start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let (winner, _) = harness.build_child(&genesis, View::new(1)).await;
let (replay_gate, replay_started, replay_release) = apply_gate();
let probe = ApplicationProbe::new(winner.digest(), [replay_gate]);
harness.processor.app.apply_probe = Some(probe.clone());
let (finalized_gate, finalized_started, finalized_release) = apply_gate();
harness.processor.app.finalized_probe =
Some(ApplicationProbe::new(winner.digest(), [finalized_gate]));
let execution = harness.processor.execution.clone();
let replays = harness.processor.replays.clone();
let mut owner_app = harness.processor.app.clone();
let mut waiter_app = harness.processor.app.clone();
let replay_context = harness.context_cell.as_present();
let owner_progress = VerificationProgress::default();
let waiter_progress = VerificationProgress::default();
let (mut owner_cancellation, _owner_alive) = oneshot::channel::<()>();
let (mut waiter_cancellation, _waiter_alive) = oneshot::channel::<()>();
let mut owner = Box::pin(execution.replay_block_shared(
&mut owner_app,
replay_context,
winner.digest(),
Arc::new(winner.clone()),
&mut owner_cancellation,
ReplayTracking {
flights: &replays,
progress: &owner_progress,
},
));
assert!(futures::poll!(&mut owner).is_pending());
replay_started.await.expect("winner replay should start");
let mut waiter = Box::pin(execution.replay_block_shared(
&mut waiter_app,
replay_context,
winner.digest(),
Arc::new(winner.clone()),
&mut waiter_cancellation,
ReplayTracking {
flights: &replays,
progress: &waiter_progress,
},
));
assert!(futures::poll!(&mut waiter).is_pending());
let mut finalize = Box::pin(harness.processor.finalize(
harness.context_cell.as_present(),
&winner,
true,
));
assert!(futures::poll!(&mut finalize).is_pending());
replay_release
.send(())
.expect("winner replay should remain active");
assert_eq!(owner.await, Ok(()));
select! {
result = finalized_started => {
result.expect("finalized hook should start");
},
_ = &mut finalize => {
panic!("finalization completed before its application hook");
},
}
assert_eq!(
waiter.await,
Ok(()),
"waiter should reuse the retained winner batch",
);
assert_eq!(probe.calls(), 1, "winner should be reconstructed once");
finalized_release
.send(())
.expect("finalized hook should remain active");
let Applied { barrier, .. } = finalize
.await
.expect("finalized block should be newly applied");
assert_durable(barrier).await;
});
}
#[test]
fn execution_finalization_reserves_missing_winner_reconstruction() {
deterministic::Runner::default().start(|context| async move {
let harness = Harness::new(context).await;
let genesis = Block::genesis();
let (winner, _) = harness.build_child(&genesis, View::new(1)).await;
let execution = harness.processor.execution.clone();
let replays = harness.processor.replays.clone();
execution.begin_finalization(Anchor::from(&winner));
let finalization = execution.claim_finalization_batch(&replays, winner.digest());
assert!(
matches!(
execution.claim_replay(&replays, winner.digest()),
ReplayClaim::Wait(_),
),
"missing winner reconstruction must remain single-flight",
);
drop(finalization);
});
}
#[test]
fn execution_rejects_late_losing_fork_publication() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
let loser = harness.stage_pending_child(&block1, View::new(2)).await;
let winner = harness.stage_pending_child(&block1, View::new(3)).await;
let late_view = View::new(4);
let (late_child, merkleized) = harness.build_child(&loser, late_view).await;
assert!(harness.finalize(winner).await);
assert!(
!harness.processor.cache_pending(
late_child.digest(),
loser.digest(),
Round::new(Epoch::zero(), late_view),
merkleized,
true,
),
"completed work on a losing fork must not publish after finalization",
);
assert!(!harness.processor.pending_contains(&late_child.digest()));
});
}
#[test]
fn execution_rebuild_pending_restores_missing_chain() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
assert!(harness.finalize(block1.clone()).await);
let block2 = harness.stage_pending_child(&block1, View::new(2)).await;
let block3 = harness.stage_pending_child(&block2, View::new(3)).await;
harness.processor.clear_pending();
harness.provider.insert(block2.clone());
harness.provider.insert(block3.clone());
let (mut response, _rx) = oneshot::channel::();
let result = harness
.processor
.rebuild_pending(
harness.context_cell.as_present(),
harness.provider.clone(),
Arc::new(block3.clone()),
&mut response,
)
.await;
assert_eq!(result, Ok(()), "rebuild should succeed");
assert!(
harness.processor.pending_contains(&block2.digest()),
"first missing descendant should be reconstructed",
);
assert!(
harness.processor.pending_contains(&block3.digest()),
"target block should be reconstructed",
);
});
}
#[test]
fn execution_fork_batches_rejects_unknown_parent() {
deterministic::Runner::default().start(|context| async move {
let harness = Harness::new(context).await;
assert!(matches!(
harness.processor.fork_batches(&u64_to_digest(999)).await,
Err(PrepareBatchesError::Invalid),
));
});
}
#[test]
fn dropped_replay_waiter_releases_registration() {
deterministic::Runner::default().start(|context| async move {
let harness = Harness::new(context).await;
let replays = ReplayFlights::default();
let digest = u64_to_digest(999);
assert!(matches!(
harness
.processor
.execution
.claim_replay(&replays, harness.processor.last_processed().digest),
ReplayClaim::Ready,
));
assert!(replays.is_empty());
let owner = match harness.processor.execution.claim_replay(&replays, digest) {
ReplayClaim::Owner(owner) => owner,
_ => panic!("first replay claim should own the flight"),
};
let first = match harness.processor.execution.claim_replay(&replays, digest) {
ReplayClaim::Wait(waiter) => waiter,
_ => panic!("duplicate replay claim should wait for the owner"),
};
let second = match harness.processor.execution.claim_replay(&replays, digest) {
ReplayClaim::Wait(waiter) => waiter,
_ => panic!("duplicate replay claim should wait for the owner"),
};
assert_eq!((first.slot, second.slot), (0, 1));
drop(first);
assert!(
replays.entries.lock().get(&digest).is_some_and(|flight| {
let waiters = &flight.waiters;
waiters.len() == 2 && waiters[0].is_none() && waiters[1].is_some()
}),
"dropping a non-tail waiter must release its slot",
);
let replacement = match harness.processor.execution.claim_replay(&replays, digest) {
ReplayClaim::Wait(waiter) => waiter,
_ => panic!("duplicate replay claim should wait for the owner"),
};
assert_eq!(replacement.slot, 0);
assert!(
replays
.entries
.lock()
.get(&digest)
.is_some_and(|flight| flight.waiters.len() == 2
&& flight.waiters.iter().all(Option::is_some)),
"replacement waiter must reuse the released slot",
);
drop(second);
drop(replacement);
assert!(
replays
.entries
.lock()
.get(&digest)
.is_some_and(|flight| flight.waiters.iter().all(Option::is_none)
&& flight.vacant_slots.len() == 2),
"dropped replay waiters must leave reusable vacant slots",
);
drop(owner);
assert!(replays.is_empty());
});
}
#[test]
fn replay_waiter_reuses_most_recent_vacant_slot() {
deterministic::Runner::default().start(|context| async move {
let harness = Harness::new(context).await;
let replays = ReplayFlights::default();
let digest = u64_to_digest(999);
let owner = match harness.processor.execution.claim_replay(&replays, digest) {
ReplayClaim::Owner(owner) => owner,
_ => panic!("first replay claim should own the flight"),
};
let first = match harness.processor.execution.claim_replay(&replays, digest) {
ReplayClaim::Wait(waiter) => waiter,
_ => panic!("duplicate replay claim should wait for the owner"),
};
let second = match harness.processor.execution.claim_replay(&replays, digest) {
ReplayClaim::Wait(waiter) => waiter,
_ => panic!("duplicate replay claim should wait for the owner"),
};
let third = match harness.processor.execution.claim_replay(&replays, digest) {
ReplayClaim::Wait(waiter) => waiter,
_ => panic!("duplicate replay claim should wait for the owner"),
};
assert_eq!((first.slot, second.slot, third.slot), (0, 1, 2));
drop(first);
drop(third);
let replacement = match harness.processor.execution.claim_replay(&replays, digest) {
ReplayClaim::Wait(waiter) => waiter,
_ => panic!("duplicate replay claim should wait for the owner"),
};
assert_eq!(replacement.slot, 2);
drop(second);
drop(replacement);
drop(owner);
assert!(replays.is_empty());
});
}
#[test]
fn stale_replay_waiter_does_not_clear_new_flight() {
deterministic::Runner::default().start(|context| async move {
let harness = Harness::new(context).await;
let replays = ReplayFlights::default();
let digest = u64_to_digest(999);
let first_owner = match harness.processor.execution.claim_replay(&replays, digest) {
ReplayClaim::Owner(owner) => owner,
_ => panic!("first replay claim should own the flight"),
};
let stale_waiter = match harness.processor.execution.claim_replay(&replays, digest) {
ReplayClaim::Wait(waiter) => waiter,
_ => panic!("duplicate replay claim should wait for the owner"),
};
drop(first_owner);
let second_owner = match harness.processor.execution.claim_replay(&replays, digest) {
ReplayClaim::Owner(owner) => owner,
_ => panic!("new replay claim should own the replacement flight"),
};
let mut current_waiter =
match harness.processor.execution.claim_replay(&replays, digest) {
ReplayClaim::Wait(waiter) => waiter,
_ => panic!("duplicate replay claim should wait for the replacement owner"),
};
drop(stale_waiter);
assert!(
futures::poll!(&mut current_waiter.completion).is_pending(),
"stale waiter cleanup must not unregister a newer flight's waiter",
);
drop(current_waiter);
drop(second_owner);
assert!(replays.is_empty());
});
}
#[test]
fn overlapping_rebuilds_share_replay_after_owner_cancellation() {
deterministic::Runner::timed(std::time::Duration::from_secs(5)).start(
|context| async move {
let mut harness = Harness::new(context.child("harness")).await;
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
let block2 = harness.stage_pending_child(&block1, View::new(2)).await;
let block3 = harness.stage_pending_child(&block2, View::new(3)).await;
harness.processor.clear_pending();
harness.provider.insert(genesis);
let (first_gate, first_started, mut first_release) = apply_gate();
let (retry_gate, retry_started, retry_release) = apply_gate();
let (duplicate_gate, mut duplicate_started, _duplicate_release) = apply_gate();
let probe = ApplicationProbe::new(
block1.digest(),
[first_gate, retry_gate, duplicate_gate],
);
harness.processor.app.apply_probe = Some(probe.clone());
let first_execution = harness.processor.execution.clone();
let second_execution = harness.processor.execution.clone();
let third_execution = harness.processor.execution.clone();
let fourth_execution = harness.processor.execution.clone();
let mut first_app = harness.processor.app.clone();
let mut second_app = harness.processor.app.clone();
let mut third_app = harness.processor.app.clone();
let mut fourth_app = harness.processor.app.clone();
let first_provider = harness.provider.clone();
let second_provider = harness.provider.clone();
let third_provider = harness.provider.clone();
let fourth_provider = harness.provider.clone();
let first_replays = harness.processor.replays.clone();
let second_replays = harness.processor.replays.clone();
let third_replays = harness.processor.replays.clone();
let fourth_replays = harness.processor.replays.clone();
let (mut first_cancellation, first_live) = oneshot::channel::();
let (mut second_cancellation, second_live) = oneshot::channel::();
let (mut third_cancellation, third_live) = oneshot::channel::();
let (mut fourth_cancellation, fourth_live) = oneshot::channel::();
let first_progress = VerificationProgress::default();
let second_progress = VerificationProgress::default();
let third_progress = VerificationProgress::default();
let fourth_progress = VerificationProgress::default();
let mut first = Box::pin(first_execution.rebuild_pending(
&mut first_app,
&context,
first_provider,
Arc::new(block2.clone()),
&mut first_cancellation,
Some(ReplayTracking {
flights: &first_replays,
progress: &first_progress,
}),
));
select! {
result = &mut first => panic!("first rebuild completed before replay gate: {result:?}"),
result = first_started => result.expect("first replay should start"),
}
let mut second = Box::pin(second_execution.rebuild_pending(
&mut second_app,
&context,
second_provider,
Arc::new(block2.clone()),
&mut second_cancellation,
Some(ReplayTracking {
flights: &second_replays,
progress: &second_progress,
}),
));
let mut third = Box::pin(third_execution.rebuild_pending(
&mut third_app,
&context,
third_provider,
Arc::new(block3),
&mut third_cancellation,
Some(ReplayTracking {
flights: &third_replays,
progress: &third_progress,
}),
));
let mut fourth = Box::pin(fourth_execution.rebuild_pending(
&mut fourth_app,
&context,
fourth_provider,
Arc::new(block2),
&mut fourth_cancellation,
Some(ReplayTracking {
flights: &fourth_replays,
progress: &fourth_progress,
}),
));
let mut waiters_registered = false;
for _ in 0..100 {
waiters_registered = harness
.processor
.replays
.entries
.lock()
.get(&block1.digest())
.is_some_and(|flight| flight.waiters.len() == 3);
if waiters_registered {
break;
}
select! {
result = &mut first => panic!("first rebuild completed before cancellation: {result:?}"),
result = &mut second => panic!("second rebuild completed before cancellation: {result:?}"),
result = &mut third => panic!("third rebuild completed before cancellation: {result:?}"),
result = &mut fourth => panic!("fourth rebuild completed before cancellation: {result:?}"),
result = &mut duplicate_started => {
result.expect("duplicate replay signal should remain available");
panic!("overlapping rebuilds executed the same ancestor concurrently");
},
_ = context.sleep(std::time::Duration::from_millis(1)) => {},
}
}
assert!(
waiters_registered,
"overlapping replays should wait for the current owner"
);
assert_eq!(probe.calls(), 1);
drop(fourth_live);
let fourth_result = select! {
result = &mut fourth => result,
result = &mut first => panic!("owner completed while cancelling waiter: {result:?}"),
result = &mut second => panic!("live waiter completed while cancelling peer: {result:?}"),
result = &mut third => panic!("live waiter completed while cancelling peer: {result:?}"),
_ = context.sleep(std::time::Duration::from_secs(1)) => {
panic!("cancelled replay waiter did not stop");
},
};
assert_eq!(fourth_result, Err(PrepareBatchesError::Cancelled));
assert!(
harness
.processor
.replays
.entries
.lock()
.get(&block1.digest())
.is_some_and(|flight| {
flight.waiters.iter().flatten().count() == 2
&& flight.vacant_slots.len() == 1
}),
"cancelled waiter must unregister while the owner remains active",
);
drop(first_live);
let first_result = select! {
result = &mut first => result,
result = &mut second => panic!("waiter completed before owner cancellation: {result:?}"),
result = &mut third => panic!("waiter completed before owner cancellation: {result:?}"),
_ = context.sleep(std::time::Duration::from_secs(1)) => {
panic!("cancelled replay owner did not stop");
},
};
assert_eq!(first_result, Err(PrepareBatchesError::Cancelled));
first_release.closed().await;
select! {
result = &mut second => panic!("waiter completed before retry gate: {result:?}"),
result = &mut third => panic!("waiter completed before retry gate: {result:?}"),
result = retry_started => result.expect("live waiter should acquire replay ownership"),
result = &mut duplicate_started => {
result.expect("duplicate replay signal should remain available");
panic!("multiple waiters acquired replay ownership");
},
_ = context.sleep(std::time::Duration::from_secs(1)) => {
panic!("live waiter did not retry cancelled replay");
},
}
retry_release
.send(())
.expect("retried replay should still be running");
let completed = futures::future::join(second, third);
let (second_result, third_result) = select! {
result = &mut duplicate_started => {
result.expect("duplicate replay signal should remain available");
panic!("successful replay was not shared with every waiter");
},
result = completed => result,
_ = context.sleep(std::time::Duration::from_secs(1)) => {
panic!("waiting rebuilds did not complete");
},
};
assert_eq!(second_result, Ok(()));
assert_eq!(third_result, Ok(()));
assert_eq!(probe.calls(), 2);
assert!(harness.processor.replays_idle());
drop((second_live, third_live));
},
);
}
#[test]
fn execution_rebuild_pending_rejects_stale_ancestor_quickly() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let mut chain = Vec::new();
let mut parent = genesis;
for view in 1..=5 {
let block = harness.stage_pending_child(&parent, View::new(view)).await;
assert!(harness.finalize(block.clone()).await);
parent = block.clone();
chain.push(block);
}
harness.processor.clear_pending();
let stale = chain[1].clone(); // height 2, below processed height 5
let fetches_before = harness.provider.fetches();
let (mut response, _rx) = oneshot::channel::();
let result = harness
.processor
.rebuild_pending(
harness.context_cell.as_present(),
harness.provider.clone(),
Arc::new(stale),
&mut response,
)
.await;
assert_eq!(
result,
Err(PrepareBatchesError::Invalid),
"stale ancestry should be rejected",
);
let fetches_after = harness.provider.fetches();
assert_eq!(
fetches_after.saturating_sub(fetches_before),
0,
"stale ancestry should be rejected before fetching its parent",
);
});
}
#[test]
fn execution_rebuild_pending_rejects_sync_target_mismatch_before_caching() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
assert!(harness.finalize(block1.clone()).await);
let mut block2 = harness.stage_pending_child(&block1, View::new(2)).await;
harness.processor.clear_pending();
block2.range = non_empty_range!(Location::new(1), Location::new(2));
harness.provider.insert(block2.clone());
let (mut response, _rx) = oneshot::channel::();
let result = harness
.processor
.rebuild_pending(
harness.context_cell.as_present(),
harness.provider.clone(),
Arc::new(block2.clone()),
&mut response,
)
.await;
assert_eq!(
result,
Err(PrepareBatchesError::Invalid),
"rebuild should reject a replayed batch whose sync target does not match the block",
);
assert!(
!harness.processor.pending_contains(&block2.digest()),
"rejected replay must not be inserted into the pending cache",
);
});
}
#[test]
fn execution_rebuild_pending_rejects_height_gap_to_processed_anchor() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context.child("harness")).await;
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
assert!(harness.finalize(block1.clone()).await);
let gap_height = Height::new(3);
let gap_view = View::new(3);
let batches = harness
.processor
.fork_batches(&block1.digest())
.await
.expect("processed anchor should be available");
let merkleized = ExecutionApp::execute(gap_height, gap_view, batches).await;
let gap_block = Block {
context: consensus_context(block1.digest(), gap_view),
parent: block1.digest(),
height: gap_height,
state_root: merkleized.root(),
range: non_empty_range!(
merkleized.bounds().inactivity_floor,
merkleized.bounds().tip.size
),
};
let provider = ScriptedParentProvider::default();
provider.push(&gap_block, [Some(block1)]);
let (mut response, _rx) = oneshot::channel::();
let result = harness
.processor
.rebuild_pending(
harness.context_cell.as_present(),
provider,
Arc::new(gap_block.clone()),
&mut response,
)
.await;
assert_eq!(
result,
Err(PrepareBatchesError::Invalid),
"rebuild must reject non-contiguous ancestry above the processed anchor",
);
assert!(
!harness.processor.pending_contains(&gap_block.digest()),
"height-gap block must not be cached as pending",
);
});
}
#[test]
fn execution_rebuild_pending_rejects_wrong_parent_digest() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context.child("harness")).await;
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
assert!(harness.finalize(block1.clone()).await);
let block2 = harness.stage_pending_child(&block1, View::new(2)).await;
let block3 = harness.stage_pending_child(&block2, View::new(3)).await;
let mut wrong_parent = block2;
wrong_parent.state_root = u64_to_digest(999);
assert_ne!(wrong_parent.digest(), block3.parent());
harness.processor.clear_pending();
let provider = ScriptedParentProvider::default();
provider.push(&block3, [Some(wrong_parent)]);
let (mut response, _rx) = oneshot::channel::();
let result = harness
.processor
.rebuild_pending(
harness.context_cell.as_present(),
provider.clone(),
Arc::new(block3.clone()),
&mut response,
)
.await;
assert_eq!(result, Err(PrepareBatchesError::Invalid));
assert_eq!(provider.fetches(), 1);
assert!(!harness.processor.pending_contains(&block3.digest()));
});
}
#[test]
#[should_panic(expected = "received conflicting finalized block at processed height")]
fn execution_finalize_panics_on_conflicting_duplicate_height() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let canonical = harness.stage_pending_child(&genesis, View::new(1)).await;
let conflicting = harness.stage_pending_child(&genesis, View::new(2)).await;
assert!(harness.finalize(canonical).await);
let _ = harness.finalize(conflicting).await;
});
}
#[test]
fn execution_finalize_identical_duplicate_returns_false() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let canonical = harness.stage_pending_child(&genesis, View::new(1)).await;
assert!(harness.finalize(canonical.clone()).await);
assert!(!harness.finalize(canonical).await);
assert_eq!(harness.counter_value().await, Some(1));
});
}
#[test]
fn execution_finalization_persists_state_to_db() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context.child("harness")).await;
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
assert!(harness.finalize(block1).await);
assert_eq!(harness.counter_value().await, Some(1));
assert_eq!(
harness
.reopen_view_at_height(context.child("reopen"), Height::new(1))
.await,
Some(1),
"height state should survive reopen after finalization",
);
});
}
#[test]
fn execution_finalized_handoff_preserves_cached_and_reconstructed_captures() {
deterministic::Runner::default().start(|context| async move {
let (mut harness, observations) =
Harness::new_with_finalized_observer(context).await;
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(7)).await;
let block2 = harness.stage_pending_child(&block1, View::new(11)).await;
let cached_probe = ApplicationProbe::new(block1.digest(), []);
harness.processor.app.apply_probe = Some(cached_probe.clone());
assert!(harness.finalize(block1).await);
assert_eq!(
cached_probe.calls(),
0,
"block1 should use its cached merkleized batch",
);
harness.processor.clear_pending();
let reconstructed_probe = ApplicationProbe::new(block2.digest(), []);
harness.processor.app.apply_probe = Some(reconstructed_probe.clone());
assert!(harness.finalize(block2).await);
assert_eq!(
reconstructed_probe.calls(),
1,
"block2 should be reconstructed through Application::apply",
);
assert_eq!(
observations.lock().as_slice(),
[
FinalizedObservation {
captured: Captured {
prior_counter: None,
batch_counter: 1,
batch_view: 7,
},
post_counter: 1,
post_view: 7,
},
FinalizedObservation {
captured: Captured {
prior_counter: Some(1),
batch_counter: 2,
batch_view: 11,
},
post_counter: 2,
post_view: 11,
},
],
"capture should see pre-apply state and finalized should receive the captured value after apply",
);
});
}
#[test]
fn execution_duplicate_finalization_skips_hooks() {
deterministic::Runner::default().start(|context| async move {
let (mut harness, observations) = Harness::new_with_finalized_observer(context).await;
let genesis = Block::genesis();
let block = harness.stage_pending_child(&genesis, View::new(1)).await;
assert!(harness.finalize(block.clone()).await);
observations.lock().clear();
assert!(!harness.finalize(block).await);
assert!(observations.lock().is_empty());
});
}
#[test]
#[should_panic(expected = "finalize reconstruction must match block commitments")]
fn execution_finalize_reconstruction_rejects_state_root_mismatch() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context).await;
let genesis = Block::genesis();
let mut block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
block1.state_root = u64_to_digest(999);
harness.processor.clear_pending();
let _ = harness.finalize(block1.clone()).await;
});
}
#[test]
fn initial_ancestry_read_cancels_when_response_dropped() {
deterministic::Runner::default().start(|_context| async move {
let (mut response, receiver) = oneshot::channel::();
let mut ancestry = Box::pin(futures::stream::pending::());
drop(receiver);
assert_eq!(fetch_ancestor(&mut response, &mut ancestry).await, None);
});
}
#[test]
fn execution_rebuild_pending_returns_incomplete_when_parent_subscription_ends() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context.child("harness")).await;
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
assert!(harness.finalize(block1.clone()).await);
let block2 = harness.stage_pending_child(&block1, View::new(2)).await;
harness.processor.clear_pending();
let provider = ScriptedParentProvider::default();
provider.push(&block2, [None]);
let (mut response, _rx) = oneshot::channel::();
let result = harness
.processor
.rebuild_pending(
harness.context_cell.as_present(),
provider,
Arc::new(block2),
&mut response,
)
.await;
assert_eq!(result, Err(PrepareBatchesError::Incomplete));
});
}
#[test]
fn execution_rebuild_pending_does_not_retry_closed_provider_forever() {
deterministic::Runner::default().start(|context| async move {
let mut harness = Harness::new(context.child("harness")).await;
let genesis = Block::genesis();
let block1 = harness.stage_pending_child(&genesis, View::new(1)).await;
assert!(harness.finalize(block1.clone()).await);
let block2 = harness.stage_pending_child(&block1, View::new(2)).await;
harness.processor.clear_pending();
let provider = ScriptedParentProvider::default();
provider.push(&block2, [None, Some(block1.clone())]);
let (mut response, _rx) = oneshot::channel::();
let result = harness
.processor
.rebuild_pending(
harness.context_cell.as_present(),
provider.clone(),
Arc::new(block2),
&mut response,
)
.await;
assert_eq!(result, Err(PrepareBatchesError::Incomplete));
assert_eq!(
provider.fetches(),
1,
"closed ancestry should not be retried"
);
});
}
}