use super::{ Application, Cancellation, Execution, PendingDigest, PrepareBatchesError, ReplayFlights, ReplayTracking, VerificationProgress, await_or_cancel, fetch_ancestor, is_already_processed, }; use crate::stateful::{actor::core::Verification, db::DatabaseSet}; use commonware_consensus::{ Heightable, Roundable, marshal::{ ancestry::{self as marshal_ancestry, Ancestry, BlockProvider}, core::{Mailbox as MarshalMailbox, Variant as MarshalVariant}, }, }; use commonware_cryptography::{Digestible, certificate::Scheme}; use commonware_runtime::{Clock, Metrics, Spawner}; use rand_core::Rng; use std::sync::Arc; use tracing::{debug, info_span, warn}; /// Parent-relative database batches passed to application verification. type Unmerkleized = <>::Databases as DatabaseSet>::Unmerkleized; /// Result of comparing a candidate with the applied canonical chain. enum ProcessedBlock { /// The candidate is above the applied anchor and still requires execution. Continue, /// The candidate is already on the applied canonical chain. Accepted, /// The candidate conflicts with the applied canonical chain. Rejected, /// The check ended without a verdict because its request was cancelled. Cancelled, } /// Failure to prepare the parent state needed for verification. enum PrepareFailure { /// The supplied ancestry is provably invalid. Invalid, /// Preparation ended without a verdict because its request was cancelled. Cancelled, } /// A candidate's parent and forked batches, ready for application verification. struct PreparedParent where E: Rng + Spawner + Metrics + Clock, A: Application, { /// Parent block consumed while preparing the candidate's state. block: Arc, /// Digest of `block`. digest: PendingDigest, /// Batches forked from the parent's speculative or applied state. batches: Unmerkleized, } /// Executes one independently-polled verification request. pub(in crate::stateful::actor) struct Verifier where E: Rng + Spawner + Metrics + Clock, A: Application, { pub(super) app: A, pub(super) execution: Execution, pub(super) replays: ReplayFlights>, } impl Clone for Verifier where E: Rng + Spawner + Metrics + Clock, A: Application, { fn clone(&self) -> Self { Self { app: self.app.clone(), execution: self.execution.clone(), replays: self.replays.clone(), } } } impl Verifier where E: Rng + Spawner + Metrics + Clock, A: Application, { /// Runs one verification request while allowing unrelated requests to be /// polled. pub(in crate::stateful::actor) async fn run( &mut self, context: &E, marshal: MarshalMailbox, consensus_context: A::Context, ancestry: impl Ancestry, progress: &VerificationProgress>, verification: &mut Verification, ) -> Option where S: Scheme, V: MarshalVariant, MarshalMailbox: BlockProvider, { let timer = self.execution.metrics.verify_duration.timer(context); let mut ancestry = ancestry; // Acquire the candidate independently for each request. Availability is // round-scoped, so requests cannot safely share this part of the work. let block = match fetch_ancestor(verification, &mut ancestry).await { Some(Some(block)) => block, Some(None) => { debug!("verification request waiting on incomplete block ancestry"); verification.cancelled().await; return None; } None => { debug!("verification request cancelled before initial block arrived"); return None; } }; let block_digest = block.digest(); // Only skip verification for blocks the application built or verified. if self.execution.pending_verified(&block_digest) { timer.observe(context); return Some(true); } // A finalized candidate cannot be re-executed against newer database // state. Prove it belongs to the canonical chain before accepting it. match self .check_processed(marshal.clone(), block.as_ref(), verification) .await { ProcessedBlock::Continue => {} ProcessedBlock::Accepted => { timer.observe(context); return Some(true); } ProcessedBlock::Rejected => return Some(false), ProcessedBlock::Cancelled => return None, } // Reconstruct the candidate's parent state. This is the only phase // shared across requests, keyed by the acquired parent's block digest. let parent = match self .prepare_parent( context, marshal, block_digest, &mut ancestry, progress, verification, ) .await { Ok(parent) => parent, Err(PrepareFailure::Invalid) => return Some(false), Err(PrepareFailure::Cancelled) => return None, }; progress.verifying(block_digest, parent.digest, consensus_context.round()); let result = self .verify( context, consensus_context, block, parent, ancestry, verification, ) .await; if result == Some(true) { timer.observe(context); } result } /// Classifies a candidate at or below the applied height without /// re-executing it. async fn check_processed( &mut self, marshal: MarshalMailbox, block: &A::Block, verification: &mut Verification, ) -> ProcessedBlock where S: Scheme, V: MarshalVariant, MarshalMailbox: BlockProvider, { let block_digest = block.digest(); let last_processed = self.execution.last_processed(); match is_already_processed(last_processed, marshal, block, verification).await { Ok(true) => ProcessedBlock::Accepted, Ok(false) if block.height() <= last_processed.height => ProcessedBlock::Rejected, Ok(false) => ProcessedBlock::Continue, Err(PrepareBatchesError::Cancelled) => { debug!( ?block_digest, "verification request cancelled during processed-block check" ); ProcessedBlock::Cancelled } Err(PrepareBatchesError::Incomplete) => { debug!( ?block_digest, "verification request waiting on incomplete processed-block ancestry" ); // Incomplete ancestry is not an invalid verdict. Keep the job // parked until its caller leaves. verification.cancelled().await; ProcessedBlock::Cancelled } Err(PrepareBatchesError::Invalid) => { unreachable!("processed-block check cannot return Invalid") } } } /// Reconstructs and forks the candidate's parent state. async fn prepare_parent( &mut self, context: &E, marshal: MarshalMailbox, block_digest: PendingDigest, ancestry: &mut impl Ancestry, progress: &VerificationProgress>, verification: &mut Verification, ) -> Result, PrepareFailure> where S: Scheme, V: MarshalVariant, MarshalMailbox: BlockProvider, { let block = match fetch_ancestor(verification, ancestry).await { Some(Some(block)) => block, Some(None) => { debug!( ?block_digest, "verification request waiting on incomplete parent ancestry" ); // As with incomplete candidate ancestry, only cancellation or // actor-driven invalidation should release this pending request. verification.cancelled().await; return Err(PrepareFailure::Cancelled); } None => { debug!( ?block_digest, "verification request cancelled before parent ancestry arrived" ); return Err(PrepareFailure::Cancelled); } }; let digest = block.digest(); let batches = match self .execution .prepare_batches( &mut self.app, context, marshal, block.clone(), verification, Some(ReplayTracking { flights: &self.replays, progress, }), ) .await { Ok(batches) => batches, Err(PrepareBatchesError::Invalid) => { let (last_processed, pending_keys) = self.execution.summary(); warn!( parent_digest = ?digest, ?block_digest, pending_keys, last_processed = ?last_processed.digest, "verification rejected: prepare_batches returned Invalid" ); return Err(PrepareFailure::Invalid); } Err(PrepareBatchesError::Incomplete) => { debug!( parent_digest = ?digest, ?block_digest, "verification request waiting on incomplete ancestry during prepare_batches" ); verification.cancelled().await; return Err(PrepareFailure::Cancelled); } Err(PrepareBatchesError::Cancelled) => { debug!( parent_digest = ?digest, "verification request cancelled during prepare_batches" ); return Err(PrepareFailure::Cancelled); } }; Ok(PreparedParent { block, digest, batches, }) } /// Executes application verification and caches commitment-matching state. async fn verify( &mut self, context: &E, consensus_context: A::Context, block: Arc, parent: PreparedParent, ancestry: impl Ancestry, verification: &mut Verification, ) -> Option { let block_digest = block.digest(); let round = consensus_context.round(); // The application expects the full candidate-first ancestry even // though the processor consumed those two entries while preparing state. let ancestry = marshal_ancestry::with_prefix([block.clone(), parent.block], ancestry); let verified = match await_or_cancel( verification, self.app.verify( ( context.child("application").child("verify_attempt"), consensus_context, ), ancestry, parent.batches, ), ) .await { Some(result) => result, None => { debug!( parent_digest = ?parent.digest, "verification request cancelled during verify" ); return None; } }; let Some(merkleized) = verified else { warn!( parent_digest = ?parent.digest, ?block_digest, "verification rejected: app.verify returned None" ); return Some(false); }; let tail = info_span!( "stateful.processor.match_commitments", block = %block_digest, parent = %parent.digest, ) .entered(); // Application output is adversarial until it matches the commitments // carried by the candidate block. Never cache it before this check. if !A::Databases::matches_sync_targets(&merkleized, &A::sync_targets(&block)) { warn!( parent_digest = ?parent.digest, ?block_digest, "verification rejected: verified state must match block commitments" ); return Some(false); } if !self .execution .cache_pending(block_digest, parent.digest, round, merkleized, true) { warn!( parent_digest = ?parent.digest, ?block_digest, "verification result became incompatible before caching" ); return Some(false); } self.execution.update_pending_metric(); drop(block); drop(tail); Some(true) } }