Skip to main content

commonware_consensus/marshal/core/
actor.rs

1use super::{
2    acks::{PendingAck, PendingAcks},
3    cache,
4    delivery::PendingVerification,
5    durability::{DispatchGate, Durable as _},
6    floor::Floor,
7    mailbox::{CommitmentFallback, Mailbox, Message},
8    stream::Stream,
9    subscriptions::{Key as SubscriptionKey, KeyFor as SubscriptionKeyFor, Subscriptions},
10    variant::NoBuffer,
11    Buffer, Variant,
12};
13use crate::{
14    marshal::{
15        resolver::handler::{self, Annotation, Key, Request},
16        store::{Blocks, Certificates},
17        Config, Identifier as BlockID, Start, Update,
18    },
19    simplex::{
20        scheme::Scheme,
21        types::{verify_certificates, Finalization, Notarization, Subject},
22    },
23    types::{Epoch, Epocher, Height, Round, ViewDelta},
24    Block, Epochable, Heightable, Reporter,
25};
26use bytes::Bytes;
27use commonware_actor::mailbox;
28use commonware_codec::{Decode, Encode, Read};
29use commonware_cryptography::{
30    certificate::{Provider, Verifier},
31    Digestible,
32};
33use commonware_macros::{boxed, select_loop};
34use commonware_p2p::Recipients;
35use commonware_parallel::Strategy;
36use commonware_resolver::{Delivery, Resolver, TargetedResolver};
37use commonware_runtime::{
38    spawn_cell,
39    telemetry::{
40        metrics::{Gauge, GaugeExt, MetricsExt as _},
41        traces::TracedExt as _,
42    },
43    BufferPooler, Clock, ContextCell, Handle, Metrics, Spawner, Storage,
44};
45use commonware_storage::archive::Identifier as ArchiveID;
46use commonware_utils::{
47    acknowledgement::Exact,
48    channel::{fallible::OneshotExt, oneshot},
49    futures::{AbortablePool, Pool},
50    Acknowledgement, BoxedError,
51};
52use futures::{
53    future::{join, join_all},
54    try_join,
55};
56use rand_core::CryptoRng;
57use std::{collections::BTreeMap, future::Future, num::NonZeroUsize, sync::Arc};
58use tracing::{debug, info_span, warn, Instrument as _, Span};
59
60// Resolver request keys are expressed in the variant commitment type, which
61// may differ from the block digest for coded variants.
62type ResolverRequestFor<V> = Key<<V as Variant>::Commitment>;
63
64// A resolver delivery plus the peer-validity response channel. Local
65// annotations on the delivery decide how accepted data is used.
66struct ResolverDelivery<V: Variant> {
67    delivery: Delivery<ResolverRequestFor<V>, Annotation>,
68    value: Bytes,
69    response: oneshot::Sender<bool>,
70}
71
72/// Completion marker for entries in the actor's durability sync pool.
73enum PooledSync {
74    /// A sync that requires no action on completion.
75    Observed,
76    /// A finalized-archive sync batch became durable. Carries the sequence
77    /// assigned by [`Actor::start_finalized_sync`] so the completion arm can
78    /// release every batch the sync covers (see [`DispatchGate::release`]).
79    Finalized(u64),
80}
81
82/// The [Actor] is responsible for receiving uncertified blocks from the broadcast mechanism,
83/// receiving notarizations and finalizations from consensus, and reconstructing a total order
84/// of blocks.
85///
86/// The actor is designed to be used in a view-based model. Each view corresponds to a
87/// potential block in the chain. The actor will only finalize a block if it has a
88/// corresponding finalization.
89///
90/// The actor also provides a backfill mechanism for missing blocks. If the actor receives a
91/// finalization for a block that is ahead of its current view, it will request the missing blocks
92/// from its peers. This ensures that the actor can catch up to the rest of the network if it falls
93/// behind.
94pub struct Actor<E, V, P, FC, FB, ES, T, A = Exact>
95where
96    E: BufferPooler + CryptoRng + Spawner + Metrics + Clock + Storage,
97    V: Variant,
98    P: Provider<Scope = Epoch, Scheme: Scheme<V::Commitment>>,
99    FC: Certificates<
100        BlockDigest = <V::Block as Digestible>::Digest,
101        Commitment = V::Commitment,
102        Scheme = P::Scheme,
103    >,
104    FB: Blocks<Block = V::StoredBlock>,
105    ES: Epocher,
106    T: Strategy,
107    A: Acknowledgement,
108{
109    // ---------- Context ----------
110    context: ContextCell<E>,
111
112    // ---------- Message Passing ----------
113    // Mailbox
114    mailbox: mailbox::Receiver<Message<P::Scheme, V>>,
115
116    // ---------- Configuration ----------
117    // Provider for epoch-specific signing schemes
118    provider: P,
119    // Epoch configuration
120    epocher: ES,
121    // Minimum number of views to retain temporary data after the application processes a block
122    view_retention_timeout: ViewDelta,
123    // Maximum number of blocks to repair at once
124    max_repair: NonZeroUsize,
125    // Codec configuration for block type
126    block_codec_config: <V::ApplicationBlock as Read>::Cfg,
127    // Strategy for parallel operations
128    strategy: T,
129
130    // ---------- State ----------
131    // Current processed floor and any pending floor update
132    floor: Floor<P::Scheme, V::Commitment>,
133    // Application delivery cursor
134    stream: Stream<E>,
135    // Pending application acknowledgements
136    pending_acks: PendingAcks<V, A>,
137    // Highest known finalized height
138    tip: Height,
139    // Outstanding subscriptions for blocks
140    block_subscriptions: Subscriptions<V>,
141    // Defers application dispatch of finalized-archive writes until a sync
142    // covering them completes
143    dispatch_gate: DispatchGate,
144
145    // ---------- Storage ----------
146    // Prunable cache
147    cache: cache::Manager<E, V, P::Scheme>,
148    // Finalizations stored by height
149    finalizations_by_height: FC,
150    // Finalized blocks stored by height
151    finalized_blocks: FB,
152
153    // ---------- Metrics ----------
154    // Latest height metric
155    finalized_height: Gauge,
156    // Latest processed height
157    processed_height: Gauge,
158}
159
160impl<E, V, P, FC, FB, ES, T, A> Actor<E, V, P, FC, FB, ES, T, A>
161where
162    E: BufferPooler + CryptoRng + Spawner + Metrics + Clock + Storage,
163    V: Variant,
164    P: Provider<Scope = Epoch, Scheme: Scheme<V::Commitment>>,
165    FC: Certificates<
166        BlockDigest = <V::Block as Digestible>::Digest,
167        Commitment = V::Commitment,
168        Scheme = P::Scheme,
169    >,
170    FB: Blocks<Block = V::StoredBlock>,
171    ES: Epocher,
172    T: Strategy,
173    A: Acknowledgement,
174{
175    /// Create a new application actor.
176    #[boxed]
177    pub async fn init(
178        context: E,
179        finalizations_by_height: FC,
180        mut finalized_blocks: FB,
181        config: Config<P, ES, T, V::ApplicationBlock, V::Block, V::Commitment>,
182    ) -> (Self, Mailbox<P::Scheme, V>, Option<Height>) {
183        // Initialize cache
184        let prunable_config = cache::Config {
185            partition_prefix: format!("{}-cache", config.partition_prefix),
186            prunable_items_per_section: config.prunable_items_per_section,
187            replay_buffer: config.replay_buffer,
188            key_write_buffer: config.key_write_buffer,
189            value_write_buffer: config.value_write_buffer,
190            key_page_cache: config.page_cache.clone(),
191        };
192        let cache = cache::Manager::init(
193            context.child("cache"),
194            prunable_config,
195            config.block_codec_config.clone(),
196        )
197        .await;
198
199        // The application metadata name is retained for legacy support.
200        let application_metadata_partition =
201            format!("{}-application-metadata", config.partition_prefix);
202        let stream = Stream::new(context.child("stream"), &application_metadata_partition).await;
203        let last_processed_height = stream.processed_height();
204
205        // Genesis is a local anchor. A floor finalization is verified and
206        // resolved after `run` receives the resolver and buffer.
207        let pending_floor_anchor = match config.start {
208            Start::Genesis(anchor) => {
209                assert_eq!(
210                    anchor.height(),
211                    Height::zero(),
212                    "genesis anchor must be at height zero"
213                );
214                Self::ensure_genesis_anchor(&mut finalized_blocks, anchor, last_processed_height)
215                    .await;
216                None
217            }
218            Start::Floor(finalization) => Some(finalization),
219        };
220        let last_processed_round =
221            Self::latest_processed_round(&finalizations_by_height, last_processed_height).await;
222
223        // Create metrics
224        let finalized_height = context.gauge("finalized_height", "Finalized height of application");
225        let processed_height = context.gauge("processed_height", "Processed height of application");
226        if let Some(last_processed_height) = last_processed_height {
227            let _ = processed_height.try_set(last_processed_height.get());
228        }
229        let floor = pending_floor_anchor.map_or_else(
230            || Floor::resolved(last_processed_height, last_processed_round),
231            |finalization| {
232                Floor::awaiting_anchor(last_processed_height, last_processed_round, finalization)
233            },
234        );
235
236        // Initialize mailbox
237        let (sender, mailbox) = mailbox::new(context.child("mailbox"), config.mailbox_size);
238        (
239            Self {
240                context: ContextCell::new(context),
241                mailbox,
242                provider: config.provider,
243                epocher: config.epocher,
244                view_retention_timeout: config.view_retention_timeout,
245                max_repair: config.max_repair,
246                block_codec_config: config.block_codec_config,
247                strategy: config.strategy,
248                floor,
249                stream,
250                pending_acks: PendingAcks::new(config.max_pending_acks.get()),
251                tip: Height::zero(),
252                block_subscriptions: Subscriptions::new(),
253                dispatch_gate: DispatchGate::default(),
254                cache,
255                finalizations_by_height,
256                finalized_blocks,
257                finalized_height,
258                processed_height,
259            },
260            Mailbox::new(sender),
261            last_processed_height,
262        )
263    }
264
265    async fn ensure_genesis_anchor(
266        finalized_blocks: &mut FB,
267        anchor: V::Block,
268        last_processed_height: Option<Height>,
269    ) {
270        let anchor_height = anchor.height();
271        let anchor_commitment = V::commitment(&anchor);
272        match finalized_blocks
273            .get(ArchiveID::Index(anchor_height.get()))
274            .await
275        {
276            Ok(Some(stored)) => {
277                let stored: V::Block = stored.into();
278                assert_eq!(
279                    stored.height(),
280                    anchor_height,
281                    "stored genesis block height mismatch"
282                );
283                assert!(
284                    V::commitment(&stored) == anchor_commitment,
285                    "stored genesis block does not match configured anchor"
286                );
287            }
288            Ok(None) => {
289                if let Some(existing) =
290                    last_processed_height.filter(|height| anchor_height < *height)
291                {
292                    warn!(
293                        height = %anchor_height,
294                        %existing,
295                        "ignoring stale anchor"
296                    );
297                    return;
298                }
299
300                finalized_blocks
301                    .put(anchor.into())
302                    .await
303                    .expect("failed to store startup anchor");
304                finalized_blocks
305                    .sync()
306                    .await
307                    .expect("failed to sync startup anchor");
308                debug!(height = %anchor_height, "stored genesis block");
309            }
310            Err(err) => panic!("failed to check startup anchor: {err}"),
311        }
312    }
313
314    /// Start the actor.
315    pub fn start<R, Buf>(
316        mut self,
317        application: impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
318        buffer: Buf,
319        resolver: (handler::Receiver<V::Commitment>, R),
320    ) -> Handle<()>
321    where
322        R: TargetedResolver<
323            Key = ResolverRequestFor<V>,
324            Subscriber = Annotation,
325            PublicKey = <P::Scheme as Verifier>::PublicKey,
326        >,
327        Buf: Buffer<V, PublicKey = <P::Scheme as Verifier>::PublicKey>,
328    {
329        spawn_cell!(self.context, self.run(application, buffer, resolver))
330    }
331
332    /// Start the actor without a broadcast buffer.
333    pub fn start_unbuffered<R>(
334        self,
335        application: impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
336        resolver: (handler::Receiver<V::Commitment>, R),
337    ) -> Handle<()>
338    where
339        R: TargetedResolver<
340            Key = ResolverRequestFor<V>,
341            Subscriber = Annotation,
342            PublicKey = <P::Scheme as Verifier>::PublicKey,
343        >,
344    {
345        self.start(
346            application,
347            NoBuffer::<<P::Scheme as Verifier>::PublicKey>::new(),
348            resolver,
349        )
350    }
351
352    /// Run the application actor.
353    async fn run<R, Buf>(
354        mut self,
355        mut application: impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
356        mut buffer: Buf,
357        (mut resolver_rx, mut resolver): (handler::Receiver<V::Commitment>, R),
358    ) where
359        R: TargetedResolver<
360            Key = ResolverRequestFor<V>,
361            Subscriber = Annotation,
362            PublicKey = <P::Scheme as Verifier>::PublicKey,
363        >,
364        Buf: Buffer<V, PublicKey = <P::Scheme as Verifier>::PublicKey>,
365    {
366        // Create a local pool for waiter futures.
367        let mut waiters = AbortablePool::<Result<Arc<V::Block>, SubscriptionKeyFor<V>>>::default();
368
369        // Observe durable syncs that no consensus caller awaits (the
370        // notarization and finalization paths). A flush failure inside
371        // `start_sync` is reported only through the returned handle, so every
372        // handle must be observed to apply the fatal policy. This pool does
373        // so without blocking the actor on a sync.
374        let mut syncs = Pool::<PooledSync>::default();
375
376        // Anchor all startup work under a single root span. Tip recovery, floor
377        // installation, gap repair, and the initial dispatch all run before any
378        // mailbox message arrives, so without this root their work would emit as
379        // orphan traces.
380        async {
381            // Get tip and send to application
382            let tip = self.get_latest().await;
383            if let Some((height, digest, round)) = tip {
384                application.report(Update::Tip(round, height, digest));
385                self.tip = height;
386                let _ = self.finalized_height.try_set(height.get());
387            }
388
389            // Load persisted cache epochs so find_block can discover blocks
390            // written before the last shutdown.
391            self.cache.load_persisted_epochs().await;
392
393            // A configured floor follows the same path as `SetFloor`: verify it,
394            // then apply a local anchor or fetch the anchor block.
395            if let Some(finalization) = self.floor.take_pending_anchor() {
396                self.install_floor(
397                    finalization,
398                    false,
399                    &mut resolver,
400                    &mut buffer,
401                    &mut application,
402                )
403                .await;
404            }
405
406            // Attempt to repair any gaps in the finalized blocks archive, if there are any.
407            if self
408                .try_repair_gaps(&mut buffer, &mut resolver, &mut application)
409                .await
410            {
411                self.sync_finalized().await;
412            }
413
414            // Attempt to dispatch the next finalized block to the application, if it is ready.
415            self.try_dispatch_blocks(&mut application).await;
416        }
417        .instrument(info_span!("marshal.actor.start"))
418        .await;
419
420        select_loop! {
421            self.context,
422            on_start => {
423                // Remove any dropped subscribers. If all subscribers dropped, abort the waiter.
424                self.block_subscriptions.retain_open();
425            },
426            on_stopped => {
427                debug!("context shutdown, stopping marshal");
428            },
429            // Drive durability syncs: a real sync failure panics inside the
430            // pooled future (the fatal policy), aborting the actor. A completed
431            // finalized-archive sync additionally releases the dispatch barrier
432            // for the batches it covers and resumes application dispatch.
433            sync = syncs.next_completed() => {
434                if let PooledSync::Finalized(seq) = sync {
435                    self.dispatch_gate.release(seq);
436                    self.try_dispatch_blocks(&mut application).await;
437                }
438            },
439            // Handle waiter completions first
440            Ok(completion) = waiters.next_completed() else continue => match completion {
441                Ok(block) => {
442                    self.ingest(block, &mut buffer, &mut application, &mut resolver)
443                        .await;
444                }
445                Err(key) => {
446                    match key {
447                        SubscriptionKey::Digest(digest) => {
448                            debug!(
449                                ?digest,
450                                "buffer subscription closed, canceling local subscribers"
451                            );
452                        }
453                        SubscriptionKey::Commitment(commitment) => {
454                            debug!(
455                                ?commitment,
456                                "buffer subscription closed, canceling local subscribers"
457                            );
458                        }
459                    }
460                    self.block_subscriptions.remove(&key);
461                }
462            },
463            // Handle application acknowledgements (drain all ready acks, sync once)
464            result = self.pending_acks.current() => {
465                self.handle_ack(result, &mut application, &mut buffer, &mut resolver)
466                    .await;
467            },
468            // Handle consensus inputs before backfill or resolver traffic
469            Some(message) = self.mailbox.recv() else {
470                debug!("mailbox closed, shutting down");
471                break;
472            } => {
473                let span = info_span!(
474                    parent: message.span(),
475                    "marshal.actor.process",
476                    operation = message.name(),
477                );
478                self.handle_mailbox_message(
479                    message,
480                    &mut resolver,
481                    &mut waiters,
482                    &mut syncs,
483                    &mut buffer,
484                    &mut application,
485                )
486                .instrument(span)
487                .await;
488            },
489            // Handle resolver messages last (batched up to max_repair, sync once)
490            Some(message) = resolver_rx.recv() else {
491                debug!("handler closed, shutting down");
492                return;
493            } => {
494                self.handle_resolver_message(
495                    message,
496                    &mut resolver_rx,
497                    &mut resolver,
498                    &mut syncs,
499                    &mut buffer,
500                    &mut application,
501                )
502                .await;
503            },
504        }
505    }
506
507    /// Handles one ready application acknowledgement and drains any queued acks
508    /// that are already complete.
509    async fn handle_ack<Buf, R>(
510        &mut self,
511        result: <A::Waiter as Future>::Output,
512        application: &mut impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
513        buffer: &mut Buf,
514        resolver: &mut R,
515    ) where
516        Buf: Buffer<V>,
517        R: Resolver<Key = ResolverRequestFor<V>, Subscriber = Annotation>,
518    {
519        // Start with the ack that woke this `select_loop!` arm.
520        let mut pending = Some(self.pending_acks.complete_current(result));
521        let last_acked_commitment = loop {
522            let (height, commitment, result) = pending.take().expect("pending ack must exist");
523            match result {
524                Ok(()) => {
525                    // Apply in-memory progress updates for this acknowledged
526                    // block. The metadata sync below makes drained updates durable.
527                    self.update_processed_height(height, resolver);
528                    self.update_processed_round(height, resolver).await;
529                }
530                Err(e) => {
531                    // Ack failures are fatal for marshal/application coordination.
532                    panic!("application did not acknowledge block at height {height}: {e:?}");
533                }
534            }
535
536            // Opportunistically drain any additional already-ready acks so we
537            // can persist one metadata sync for the whole batch below.
538            match self.pending_acks.pop_ready() {
539                Some(next) => pending = Some(next),
540                None => break commitment,
541            }
542        };
543
544        // Persist buffered progress updates once after draining all ready acks.
545        self.stream
546            .sync()
547            .await
548            .expect("failed to sync application progress");
549
550        // Anything below the last acknowledged commitment is safe for the
551        // buffer to prune.
552        buffer.finalized(last_acked_commitment);
553
554        // Refill the application dispatch pipeline.
555        self.try_dispatch_blocks(application).await;
556    }
557
558    /// Handles a single mailbox message from local consensus/application callers.
559    async fn handle_mailbox_message<Buf, R>(
560        &mut self,
561        message: Message<P::Scheme, V>,
562        resolver: &mut R,
563        waiters: &mut AbortablePool<Result<Arc<V::Block>, SubscriptionKeyFor<V>>>,
564        syncs: &mut Pool<PooledSync>,
565        buffer: &mut Buf,
566        application: &mut impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
567    ) where
568        Buf: Buffer<V, PublicKey = <P::Scheme as Verifier>::PublicKey>,
569        R: TargetedResolver<
570            Key = ResolverRequestFor<V>,
571            Subscriber = Annotation,
572            PublicKey = <P::Scheme as Verifier>::PublicKey,
573        >,
574    {
575        if message.response_closed() {
576            return;
577        }
578
579        match message {
580            Message::GetInfo {
581                identifier,
582                response,
583                ..
584            } => {
585                let info = match identifier {
586                    // TODO: Instead of pulling out the entire block, determine the
587                    // height directly from the archive by mapping the digest to
588                    // the index, which is the same as the height.
589                    BlockID::Digest(digest) => self
590                        .finalized_blocks
591                        .get(ArchiveID::Key(&digest))
592                        .await
593                        .ok()
594                        .flatten()
595                        .map(|b| (b.height(), digest)),
596                    BlockID::Height(height) => self.get_info_by_height(height).await,
597                    BlockID::Latest => self.get_latest().await.map(|(h, d, _)| (h, d)),
598                };
599                response.send_lossy(info);
600            }
601            Message::GetVerified {
602                round, response, ..
603            } => {
604                let block = self.cache.get_verified(round).await.map(Into::into);
605                response.send_lossy(block);
606            }
607            Message::Forward {
608                round,
609                commitment,
610                recipients,
611                ..
612            } => {
613                if matches!(&recipients, Recipients::Some(peers) if peers.is_empty()) {
614                    return;
615                }
616                let Some(block) = self.find_block_by_commitment(buffer, commitment).await else {
617                    debug!(?commitment, "block not found for forwarding");
618                    return;
619                };
620                buffer.send(round, block, recipients);
621            }
622            Message::Proposed {
623                round,
624                block,
625                recipients,
626                ack,
627                ..
628            } => {
629                // To lower view latency as much as possible while preserving
630                // safety, we broadcast the block before persisting it
631                // (durability is not required until certify). A leader that
632                // crashes here may broadcast a conflicting block for the same
633                // round after restart. This is tolerated: extra block bytes
634                // cannot form a conflicting certificate (unlike votes), block
635                // storage tolerates multiple candidates per round (see
636                // [Mailbox::get_verified]), and the propose paths skip or
637                // reuse a recovered block on restart.
638                buffer.send(round, Arc::clone(&block), recipients);
639                self.persist_verified(round, block, ack, buffer, application, resolver)
640                    .await;
641            }
642            Message::Verified {
643                round, block, ack, ..
644            } => {
645                self.persist_verified(round, block, ack, buffer, application, resolver)
646                    .await;
647            }
648            Message::Certified {
649                round, block, ack, ..
650            } => {
651                self.ingest(Arc::clone(&block), buffer, application, resolver)
652                    .await;
653                let digest = block.digest();
654
655                // A block the verified archive already holds needs no second copy:
656                // the verified archive's covering sync handle vouches for it. At
657                // most one notarization exists per round, so the notarized slot can
658                // never belong to a different payload: a duplicate put is a no-op
659                // whose handle still covers the original write. If the round has
660                // already been pruned by tip advancement, both writes are no-ops
661                // because the round is below the retention floor.
662                let block_sync = if self.cache.has_verified(round, &digest).await {
663                    debug!(?round, "certified block covered by verified write");
664                    self.cache.start_sync_verified(round).await
665                } else {
666                    self.cache
667                        .put_notarized(round, digest, Arc::unwrap_or_clone(block).into())
668                        .await
669                };
670
671                // Hold the certify barrier until the round's notarization
672                // certificate (when one was accepted before this message) is
673                // durable alongside the block.
674                let notarization_sync = self.cache.start_sync_notarizations(round).await;
675                let handle = Handle::from_future(async move {
676                    let (notarization, block) = join(notarization_sync, block_sync).await;
677                    notarization.and(block)
678                });
679                ack.send_lossy(handle);
680            }
681            Message::Notarization { notarization, .. } => {
682                let round = notarization.round();
683                let commitment = notarization.proposal.payload;
684                let digest = V::commitment_to_inner(commitment);
685
686                // Persist the notarization; the certify barrier folds in its
687                // durability via `start_sync_notarizations`. The archive keeps a
688                // single notarization per round, so a re-delivery is a no-op whose
689                // handle still covers the original write. No consensus caller
690                // awaits this handle, so the pool observes it (applying the fatal
691                // policy) without blocking the actor.
692                let handle = self
693                    .cache
694                    .put_notarization(round, digest, notarization)
695                    .await;
696                syncs.push(async move {
697                    handle.durable(round, "notarization").await;
698                    PooledSync::Observed
699                });
700
701                // A notarization alone is not enough to fetch missing proposal
702                // data. If the block is not locally available, remember the
703                // certificate and wait for a later finalization/repair path.
704                if let Some(block) = self.find_block_by_commitment(buffer, commitment).await {
705                    self.ingest(Arc::clone(&block), buffer, application, resolver)
706                        .await;
707                    if self.cache.has_verified(round, &digest).await {
708                        debug!(?round, "notarized block covered by verified write");
709                    } else {
710                        let handle = self
711                            .cache
712                            .put_notarized(round, digest, Arc::unwrap_or_clone(block).into())
713                            .await;
714                        syncs.push(async move {
715                            handle.durable(round, "notarized").await;
716                            PooledSync::Observed
717                        });
718                    }
719                } else {
720                    debug!(?round, "notarized block unavailable locally");
721                }
722            }
723            Message::Finalization { finalization, .. } => {
724                let round = finalization.round();
725                let commitment = finalization.proposal.payload;
726                let digest = V::commitment_to_inner(commitment);
727
728                // Cache finalization by round.
729                self.cache
730                    .put_finalization(round, digest, finalization.clone())
731                    .await;
732
733                // Search for the finalized block locally, otherwise fetch it remotely.
734                if let Some(block) = self.find_block_by_commitment(buffer, commitment).await {
735                    // The anchor path stores the floor block and finalization,
736                    // advances floors, prunes below them, and resumes dispatch.
737                    if self
738                        .ingest(Arc::clone(&block), buffer, application, resolver)
739                        .await
740                    {
741                        return;
742                    }
743
744                    let height = block.height();
745                    self.update_processed_round_floor(height, round, resolver)
746                        .await;
747                    if self
748                        .store_finalization(
749                            height,
750                            digest,
751                            Arc::unwrap_or_clone(block),
752                            Some(finalization),
753                            application,
754                        )
755                        .await
756                    {
757                        // If a floor anchor is pending, repair and dispatch are
758                        // no-ops until the anchor block is stored.
759                        self.try_repair_gaps(buffer, resolver, application).await;
760                        self.start_finalized_sync(round, syncs).await;
761                        debug!(?round, %height, "finalized block stored");
762                    }
763                } else {
764                    // The finalization carries a round and commitment, but not a
765                    // height. Keep the request round-bound until the block is decoded.
766                    debug!(?round, ?commitment, "finalized block missing");
767                    self.floor
768                        .fetch_if_permitted(
769                            resolver,
770                            Request::finalized_block_by_round(commitment, round),
771                        )
772                        .ignore();
773                }
774            }
775            Message::GetBlock {
776                identifier,
777                response,
778                ..
779            } => match identifier {
780                BlockID::Digest(digest) => {
781                    let result = self
782                        .find_block_by_digest(buffer, digest)
783                        .await
784                        .map(Arc::unwrap_or_clone);
785                    response.send_lossy(result);
786                }
787                BlockID::Height(height) => {
788                    let result = self.get_finalized_block(height).await;
789                    response.send_lossy(result);
790                }
791                BlockID::Latest => {
792                    let block = match self.get_latest().await {
793                        Some((_, digest, _)) => self.find_block_by_digest(buffer, digest).await,
794                        None => None,
795                    }
796                    .map(Arc::unwrap_or_clone);
797                    response.send_lossy(block);
798                }
799            },
800            Message::GetFinalization {
801                height, response, ..
802            } => {
803                let finalization = self.get_finalization_by_height(height).await;
804                response.send_lossy(finalization);
805            }
806            Message::GetProcessedHeight { response, .. } => {
807                response.send_lossy(self.stream.processed_height());
808            }
809            Message::HintFinalized {
810                height, targets, ..
811            } => {
812                // Skip if finalization is already available locally.
813                if self.has_finalization_by_height(height).await {
814                    return;
815                }
816
817                self.floor
818                    .fetch_targeted_if_permitted(resolver, Request::finalized(height), targets)
819                    .ignore();
820            }
821            Message::SubscribeByDigest {
822                span,
823                digest,
824                fallback,
825                response,
826            } => {
827                self.handle_subscribe(
828                    span,
829                    fallback.into(),
830                    SubscriptionKey::Digest(digest),
831                    response,
832                    resolver,
833                    waiters,
834                    buffer,
835                )
836                .await;
837            }
838            Message::SubscribeByCommitment {
839                span,
840                commitment,
841                fallback,
842                response,
843            } => {
844                self.handle_subscribe(
845                    span,
846                    fallback,
847                    SubscriptionKey::Commitment(commitment),
848                    response,
849                    resolver,
850                    waiters,
851                    buffer,
852                )
853                .await;
854            }
855            Message::HintNotarized {
856                round, commitment, ..
857            } => {
858                if self
859                    .find_block_by_commitment(buffer, commitment)
860                    .await
861                    .is_none()
862                {
863                    self.floor
864                        .fetch_if_permitted(resolver, Request::notarized(round))
865                        .ignore();
866                }
867            }
868            Message::SetFloor { finalization, .. } => {
869                self.install_floor(finalization, true, resolver, buffer, application)
870                    .await;
871            }
872            Message::Prune { height, .. } => {
873                // Only allow pruning at or below the current floor.
874                if height > self.floor.processed_height() {
875                    warn!(%height, floor = %self.floor.processed_height(), "prune height above floor, ignoring");
876                    return;
877                }
878
879                self.prune_finalized_archives(height)
880                    .await
881                    .expect("failed to prune finalized archives");
882
883                // Intentionally keep existing block subscriptions alive. Canceling
884                // waiters can have catastrophic consequences because actors do not
885                // retry subscriptions on failed channels.
886            }
887        }
888    }
889
890    /// Handles a batch of resolver messages, starting one pooled
891    /// finalized-archive sync if any accepted delivery buffered a write.
892    async fn handle_resolver_message<Buf, R>(
893        &mut self,
894        message: handler::Message<V::Commitment>,
895        resolver_rx: &mut handler::Receiver<V::Commitment>,
896        resolver: &mut R,
897        syncs: &mut Pool<PooledSync>,
898        buffer: &mut Buf,
899        application: &mut impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
900    ) where
901        Buf: Buffer<V, PublicKey = <P::Scheme as Verifier>::PublicKey>,
902        R: Resolver<Key = ResolverRequestFor<V>, Subscriber = Annotation>,
903    {
904        let mut handled = false;
905        let mut produces = Vec::new();
906        let mut delivers = Vec::new();
907
908        // Drain up to max_repair resolver messages. Block deliveries are handled
909        // immediately, certificate-bearing deliveries are batched for verification,
910        // and produce responses wait until repair has had a chance to fill gaps.
911        for msg in std::iter::once(message)
912            .chain(std::iter::from_fn(|| resolver_rx.try_recv().ok()))
913            .take(self.max_repair.get())
914        {
915            if msg.response_closed() {
916                continue;
917            }
918            handled = true;
919
920            match msg {
921                handler::Message::Produce { key, response } => {
922                    produces.push((key, response));
923                }
924                handler::Message::Deliver {
925                    delivery,
926                    value,
927                    response,
928                } => {
929                    let span = info_span!(
930                        parent: &delivery.subscribers.first().1,
931                        "marshal.resolver.deliver",
932                        key = %delivery.key
933                    );
934                    for (_, subscriber_span) in delivery.subscribers.iter().skip(1) {
935                        span.follows_from(subscriber_span.id());
936                    }
937                    self.handle_deliver(
938                        ResolverDelivery {
939                            delivery,
940                            value,
941                            response,
942                        },
943                        &mut delivers,
944                        buffer,
945                        application,
946                        resolver,
947                    )
948                    .instrument(span)
949                    .await;
950                }
951            }
952        }
953        if !handled {
954            return;
955        }
956
957        // Batch verify and process all certificate-bearing deliveries.
958        self.verify_delivered(delivers, buffer, application, resolver)
959            .await;
960
961        // Attempt to fill gaps before handling produce requests so we can serve
962        // data received earlier in the same batch.
963        self.try_repair_gaps(buffer, resolver, application).await;
964
965        // Start a pooled sync so any writes buffered by this batch become
966        // durable without blocking the mailbox. Dispatch of the written
967        // heights resumes when the sync completes. A batch has no single
968        // round, so the label is the node's processed round when it started.
969        self.start_finalized_sync(self.floor.processed_round(), syncs)
970            .await;
971
972        // Handle produce requests in parallel.
973        join_all(
974            produces
975                .into_iter()
976                .filter(|(_, response)| !response.is_closed())
977                .map(|(key, response)| self.handle_produce(key, response, buffer)),
978        )
979        .await;
980    }
981
982    /// Handle a produce request from a remote peer.
983    #[tracing::instrument(name = "marshal.resolver.produce", level = "debug", skip_all, fields(key = %key))]
984    async fn handle_produce<Buf: Buffer<V>>(
985        &self,
986        key: ResolverRequestFor<V>,
987        response: oneshot::Sender<Bytes>,
988        buffer: &Buf,
989    ) {
990        match key {
991            Key::Block(commitment) => {
992                let Some(block) = self.find_block_by_commitment(buffer, commitment).await else {
993                    debug!(?commitment, "block missing on request");
994                    return;
995                };
996                response.send_lossy(block.encode());
997            }
998            Key::Finalized { height } => {
999                let Some(finalization) = self.get_finalization_by_height(height).await else {
1000                    debug!(%height, "finalization missing on request");
1001                    return;
1002                };
1003                let Some(block) = self.get_finalized_block(height).await else {
1004                    debug!(%height, "finalized block missing on request");
1005                    return;
1006                };
1007                response.send_lossy((finalization, V::into_inner(block)).encode());
1008            }
1009            Key::Notarized { round } => {
1010                let Some(notarization) = self.cache.get_notarization(round).await else {
1011                    debug!(?round, "notarization missing on request");
1012                    return;
1013                };
1014                let commitment = notarization.proposal.payload;
1015                let Some(block) = self.find_block_by_commitment(buffer, commitment).await else {
1016                    debug!(?commitment, "block missing on request");
1017                    return;
1018                };
1019                response.send_lossy((notarization, block).encode());
1020            }
1021        }
1022    }
1023
1024    /// Handle a local subscription request for a block.
1025    #[allow(clippy::too_many_arguments)]
1026    async fn handle_subscribe<Buf: Buffer<V>>(
1027        &mut self,
1028        span: Span,
1029        fallback: CommitmentFallback,
1030        key: SubscriptionKeyFor<V>,
1031        response: oneshot::Sender<Arc<V::Block>>,
1032        resolver: &mut impl Resolver<Key = ResolverRequestFor<V>, Subscriber = Annotation>,
1033        waiters: &mut AbortablePool<Result<Arc<V::Block>, SubscriptionKeyFor<V>>>,
1034        buffer: &mut Buf,
1035    ) {
1036        let digest = match key {
1037            SubscriptionKey::Digest(digest) => digest,
1038            SubscriptionKey::Commitment(commitment) => V::commitment_to_inner(commitment),
1039        };
1040
1041        let block = match key {
1042            SubscriptionKey::Digest(digest) => self.find_block_by_digest(buffer, digest).await,
1043            SubscriptionKey::Commitment(commitment) => {
1044                self.find_block_by_commitment(buffer, commitment).await
1045            }
1046        };
1047        if let Some(block) = block {
1048            response.send_lossy(block);
1049            return;
1050        }
1051
1052        // We don't have the block locally. Local-only waits reach this point
1053        // without a round or height, so they only register a subscriber below.
1054        //
1055        // Round-based fetching is for notarized proposal lookups whose height is
1056        // not known before the request. Height-based fetching is only for callers
1057        // that already have a validated pruning height.
1058        match fallback {
1059            CommitmentFallback::FetchByRound { round } => {
1060                // Fetch the notarized proposal for this round. The response
1061                // must include a certificate so the commitment is tied to the
1062                // certified round context. The decoded block is heightable, but
1063                // that height is not known soon enough to key, coalesce, or prune
1064                // the in-flight resolver request.
1065                if self
1066                    .floor
1067                    .fetch_if_permitted(resolver, Request::notarized(round))
1068                    .denied()
1069                {
1070                    return;
1071                }
1072                debug!(?round, ?digest, "requested block missing");
1073            }
1074            CommitmentFallback::FetchByCommitment { height } => {
1075                let commitment = match key {
1076                    SubscriptionKey::Commitment(commitment) => commitment,
1077                    SubscriptionKey::Digest(_) => {
1078                        unreachable!("digest subscriptions cannot request commitment fallback")
1079                    }
1080                };
1081
1082                // This path is only for accepted ancestry or finalized repair,
1083                // never for a candidate block's immediate parent.
1084                if self
1085                    .floor
1086                    .fetch_if_permitted(resolver, Request::certified_block(commitment, height))
1087                    .denied()
1088                {
1089                    return;
1090                }
1091                debug!(%height, ?commitment, ?digest, "requested certified ancestry block missing");
1092            }
1093            CommitmentFallback::Wait => {}
1094        }
1095
1096        let round = match fallback {
1097            CommitmentFallback::FetchByRound { round } => Some(round),
1098            CommitmentFallback::Wait | CommitmentFallback::FetchByCommitment { .. } => None,
1099        };
1100
1101        // Register subscriber.
1102        match key {
1103            SubscriptionKey::Digest(digest) => {
1104                debug!(?round, ?digest, "registering subscriber");
1105            }
1106            SubscriptionKey::Commitment(commitment) => {
1107                debug!(?round, ?commitment, ?digest, "registering subscriber");
1108            }
1109        }
1110        self.block_subscriptions
1111            .insert(span, key, response, waiters, buffer);
1112    }
1113
1114    /// Verifies and installs a floor, fetching the anchor block if needed.
1115    async fn install_floor<Buf, R>(
1116        &mut self,
1117        finalization: Finalization<P::Scheme, V::Commitment>,
1118        skip_if_superseded: bool,
1119        resolver: &mut R,
1120        buffer: &mut Buf,
1121        application: &mut impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
1122    ) where
1123        Buf: Buffer<V, PublicKey = <P::Scheme as Verifier>::PublicKey>,
1124        R: Resolver<Key = ResolverRequestFor<V>, Subscriber = Annotation>,
1125    {
1126        let round = finalization.round();
1127        if round <= self.floor.processed_round() {
1128            warn!(
1129                ?round,
1130                floor = ?self.floor.processed_round(),
1131                "floor not updated, below existing round floor"
1132            );
1133            return;
1134        }
1135
1136        let Some(scoped) = self.provider.scoped(finalization.epoch()) else {
1137            panic!("floor finalization epoch unavailable");
1138        };
1139        assert!(
1140            finalization.verify(self.context.as_mut(), &scoped, &self.strategy),
1141            "floor finalization must verify"
1142        );
1143
1144        let commitment = finalization.proposal.payload;
1145        let digest = V::commitment_to_inner(commitment);
1146        self.cache
1147            .put_finalization(round, digest, finalization.clone())
1148            .await;
1149
1150        // A pending anchor at the same or a newer floor already blocks
1151        // progress. Keep waiting for it instead of replacing it.
1152        if skip_if_superseded && self.floor.has_pending_anchor_at_or_after(round) {
1153            return;
1154        }
1155
1156        if let Some(block) = self.find_block_by_commitment(buffer, commitment).await {
1157            self.floor.await_anchor(finalization);
1158            assert!(self.ingest(block, buffer, application, resolver).await);
1159            return;
1160        }
1161
1162        // The pending floor owns the next application sync point. Drop any
1163        // in-flight acks before they can advance the processed height past it.
1164        self.pending_acks.clear();
1165
1166        debug!(?round, ?commitment, "starting fetch for floor block");
1167        self.floor.await_anchor(finalization);
1168        self.floor
1169            .fetch_if_permitted(
1170                resolver,
1171                Request::finalized_block_by_round(commitment, round),
1172            )
1173            .ignore();
1174    }
1175
1176    /// Ingests `block` and persists it as a verify-stage candidate for `round`,
1177    /// delivering the write's durable-sync handle through `ack`.
1178    ///
1179    /// If the round has already been pruned by tip advancement, `put_verified`
1180    /// is a no-op because the round is below the retention floor (and no longer
1181    /// is required by consensus to make progress). A duplicate delivery is also
1182    /// a no-op, with the handle still covering the original write's durability.
1183    async fn persist_verified<Buf: Buffer<V>>(
1184        &mut self,
1185        round: Round,
1186        block: Arc<V::Block>,
1187        ack: oneshot::Sender<Handle<()>>,
1188        buffer: &mut Buf,
1189        application: &mut impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
1190        resolver: &mut impl Resolver<Key = ResolverRequestFor<V>, Subscriber = Annotation>,
1191    ) {
1192        self.ingest(Arc::clone(&block), buffer, application, resolver)
1193            .await;
1194        let digest = block.digest();
1195        let handle = self
1196            .cache
1197            .put_verified(round, digest, Arc::unwrap_or_clone(block).into())
1198            .await;
1199        ack.send_lossy(handle);
1200    }
1201
1202    /// Notifies subscribers of a validated block and applies it to any
1203    /// pending floor transition.
1204    ///
1205    /// Subscribers are notified before the block is persisted. This is not
1206    /// observable while running because mailbox requests are only served
1207    /// after the current `select_loop!` arm completes. After an unclean
1208    /// shutdown, however, a subscriber may hold a block that marshal never
1209    /// durably stored. Subscriptions make no durability promise. Durable
1210    /// height-ordered delivery is provided by application dispatch, which
1211    /// only sends blocks once the finalized archives are durable (see
1212    /// [`Self::try_dispatch_blocks`]).
1213    ///
1214    /// Returns true if the block was consumed as the floor anchor.
1215    async fn ingest<Buf: Buffer<V>>(
1216        &mut self,
1217        block: Arc<V::Block>,
1218        buffer: &mut Buf,
1219        application: &mut impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
1220        resolver: &mut impl Resolver<Key = ResolverRequestFor<V>, Subscriber = Annotation>,
1221    ) -> bool {
1222        self.block_subscriptions.notify(Arc::clone(&block));
1223
1224        if !self.floor.matches_pending_anchor(V::commitment(&block)) {
1225            return false;
1226        }
1227
1228        self.apply_pending_floor(block, buffer, application, resolver)
1229            .await;
1230        true
1231    }
1232
1233    /// Applies the pending floor transition using its matching anchor block.
1234    ///
1235    /// # Panics
1236    ///
1237    /// Panics if no pending floor anchor is installed.
1238    async fn apply_pending_floor<Buf: Buffer<V>>(
1239        &mut self,
1240        block: Arc<V::Block>,
1241        buffer: &mut Buf,
1242        application: &mut impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
1243        resolver: &mut impl Resolver<Key = ResolverRequestFor<V>, Subscriber = Annotation>,
1244    ) {
1245        // Floor anchors can bypass the local proposal-verification path. Check
1246        // the parent relationship before using a non-genesis anchor for walkback.
1247        let height = block.height();
1248        if height > Height::zero() {
1249            let parent_commitment = V::parent_commitment(&block);
1250            assert!(
1251                block.parent() == V::commitment_to_inner(parent_commitment),
1252                "floor block parent commitment mismatch"
1253            );
1254        }
1255
1256        // This anchor cannot move the application sync point, but its
1257        // finalization round can still prune round-bound resolver work.
1258        // Keep pending acks intact because processed_height is unchanged.
1259        if height <= self.floor.processed_height() {
1260            warn!(
1261                %height,
1262                existing = %self.floor.processed_height(),
1263                "floor not updated, at or below existing"
1264            );
1265            let finalization = self
1266                .floor
1267                .take_pending_anchor()
1268                .expect("pending floor anchor missing");
1269            self.update_processed_round_floor(height, finalization.round(), resolver)
1270                .await;
1271            if self.try_repair_gaps(buffer, resolver, application).await {
1272                self.sync_finalized().await;
1273            }
1274            self.try_dispatch_blocks(application).await;
1275            return;
1276        }
1277
1278        let digest = block.digest();
1279        let finalization = self
1280            .floor
1281            .take_pending_anchor()
1282            .expect("pending floor anchor missing");
1283        let round = finalization.round();
1284        try_join!(
1285            async {
1286                self.finalized_blocks
1287                    .put(Arc::unwrap_or_clone(block).into())
1288                    .await
1289                    .map_err(Box::new)?;
1290                Ok::<_, BoxedError>(())
1291            },
1292            async {
1293                self.finalizations_by_height
1294                    .put(height, digest, finalization)
1295                    .await
1296                    .map_err(Box::new)?;
1297                Ok::<_, BoxedError>(())
1298            }
1299        )
1300        .expect("failed to store floor anchor");
1301        self.sync_finalized().await;
1302
1303        if height > self.tip {
1304            application.report(Update::Tip(round, height, digest));
1305            self.tip = height;
1306            let _ = self.finalized_height.try_set(height.get());
1307        }
1308
1309        // The anchor is durable, but the application still needs to process it.
1310        // Record the previous height so dispatch resumes at the anchor itself.
1311        let dispatch_floor = height
1312            .previous()
1313            .expect("floor anchor above processed height must have predecessor");
1314        self.update_processed_height(dispatch_floor, resolver);
1315        self.update_processed_round_floor(dispatch_floor, round, resolver)
1316            .await;
1317        self.stream
1318            .sync()
1319            .await
1320            .expect("failed to sync floor metadata");
1321
1322        // Drop all pending acknowledgement waiters so any in-flight application
1323        // acks for blocks below the new floor cannot rewrite the processed floor.
1324        self.pending_acks.clear();
1325
1326        // The floor is durable, so cache/finalized data below it can be pruned.
1327        self.prune_after_floor(height)
1328            .await
1329            .expect("failed to prune data below floor");
1330
1331        // Intentionally keep existing block subscriptions alive. Canceling
1332        // waiters can have catastrophic consequences (nodes can get stuck in
1333        // different views) as actors do not retry subscriptions on failed channels.
1334        if self.try_repair_gaps(buffer, resolver, application).await {
1335            self.sync_finalized().await;
1336        }
1337        self.try_dispatch_blocks(application).await;
1338    }
1339
1340    /// Handle a deliver message from the resolver. Block delivers are handled
1341    /// immediately. Finalized/Notarized delivers are parsed and structurally
1342    /// validated, then collected into `delivers` for batch certificate verification.
1343    async fn handle_deliver<Buf: Buffer<V>>(
1344        &mut self,
1345        message: ResolverDelivery<V>,
1346        delivers: &mut Vec<PendingVerification<P::Scheme, V>>,
1347        buffer: &mut Buf,
1348        application: &mut impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
1349        resolver: &mut impl Resolver<Key = ResolverRequestFor<V>, Subscriber = Annotation>,
1350    ) {
1351        let ResolverDelivery {
1352            delivery,
1353            mut value,
1354            response,
1355        } = message;
1356        let Delivery {
1357            key, subscribers, ..
1358        } = delivery;
1359        match key {
1360            Key::Block(commitment) => {
1361                let block_cfg = V::block_cfg(&self.block_codec_config, commitment);
1362                let Ok(block) = V::Block::decode_cfg(value.as_ref(), &block_cfg) else {
1363                    response.send_lossy(false);
1364                    return;
1365                };
1366                if V::commitment(&block) != commitment {
1367                    response.send_lossy(false);
1368                    return;
1369                }
1370
1371                // This block may match the pending floor request. Whether it
1372                // installs or is rejected as the floor anchor, do not also
1373                // process it as an ordinary block delivery.
1374                let block = Arc::new(block);
1375                if self
1376                    .ingest(Arc::clone(&block), buffer, application, resolver)
1377                    .await
1378                {
1379                    response.send_lossy(true);
1380                    return;
1381                }
1382
1383                // The peer-visible request only says "give me this block".
1384                // Local annotations explain why the block was requested and
1385                // therefore where, if anywhere, it should be stored.
1386                let height = block.height();
1387                let digest = block.digest();
1388                let annotations = subscribers
1389                    .map_into(|(annotation, _)| annotation)
1390                    .into_vec();
1391
1392                // Round-bound proposal-parent fetches are `Key::Notarized`
1393                // deliveries and are handled below. In this block-keyed path,
1394                // `Finalized` means the block belongs in the finalized chain.
1395                let finalization = self.cache.get_finalization_for(digest).await;
1396                if let Some(finalization) = &finalization {
1397                    self.update_processed_round_floor(height, finalization.round(), resolver)
1398                        .await;
1399                }
1400                if finalization.is_some()
1401                    || annotations
1402                        .iter()
1403                        .any(|annotation| matches!(annotation, Annotation::Finalized(_)))
1404                {
1405                    self.store_finalization(
1406                        height,
1407                        digest,
1408                        Arc::unwrap_or_clone(block),
1409                        finalization,
1410                        application,
1411                    )
1412                    .await;
1413                } else if annotations
1414                    .iter()
1415                    .any(|annotation| matches!(annotation, Annotation::Certified { .. }))
1416                    && height > self.floor.processed_height()
1417                {
1418                    if let Some(bounds) = self.epocher.containing(height) {
1419                        self.cache
1420                            .put_certified(
1421                                bounds.epoch(),
1422                                height,
1423                                digest,
1424                                Arc::unwrap_or_clone(block).into(),
1425                            )
1426                            .await;
1427                    }
1428                }
1429                debug!(?digest, %height, "received block");
1430                response.send_lossy(true);
1431            }
1432            Key::Finalized { height } => {
1433                let Some((epoch, certificate_codec_config)) =
1434                    self.certificate_codec_config_for_height(height)
1435                else {
1436                    debug!(
1437                        %height,
1438                        floor = %self.floor.processed_height(),
1439                        "ignoring stale delivery"
1440                    );
1441                    response.send_lossy(true);
1442                    return;
1443                };
1444
1445                let Ok(finalization) =
1446                    Finalization::read_cfg(&mut value, &certificate_codec_config)
1447                else {
1448                    response.send_lossy(false);
1449                    return;
1450                };
1451
1452                // We decoded the certificate with the codec config for the height's epoch, so the
1453                // finalization must claim that same epoch. A mismatch means the bytes were bounded
1454                // against the wrong participant set, so reject before verification.
1455                if finalization.epoch() != epoch {
1456                    response.send_lossy(false);
1457                    return;
1458                }
1459
1460                // Decode the block carried with the finalization. Below, it is checked against
1461                // the requested height and the finalization payload.
1462                let Ok(block) =
1463                    V::ApplicationBlock::decode_cfg(&mut value, &self.block_codec_config)
1464                else {
1465                    response.send_lossy(false);
1466                    return;
1467                };
1468
1469                // In contrast to the `Block` and `Notarization` deliveries, the finalization delivery
1470                // is guaranteed to be certified (assuming the certificate verifies). Because of this,
1471                // we can skip broader payload checks and just check that the application block matches
1472                // the commitment in the finalization proposal.
1473                //
1474                // TODO(https://github.com/commonwarexyz/monorepo/issues/3938): Apply this pattern
1475                // conditionally to `Request::Block` and `Request::Notarized`, if the requester knows
1476                // the requested block is certified.
1477                let commitment = finalization.proposal.payload;
1478                if block.height() != height || block.digest() != V::commitment_to_inner(commitment)
1479                {
1480                    response.send_lossy(false);
1481                    return;
1482                }
1483                delivers.push(PendingVerification::Finalized {
1484                    finalization,
1485                    block,
1486                    response,
1487                });
1488            }
1489            Key::Notarized { round } => {
1490                let Some(scheme) = self.provider.scheme(round.epoch()) else {
1491                    debug!(
1492                        ?round,
1493                        floor = %self.floor.processed_height(),
1494                        "ignoring stale delivery"
1495                    );
1496                    response.send_lossy(true);
1497                    return;
1498                };
1499                let certificate_codec_config = scheme.certificate_codec_config();
1500                let Ok(notarization) =
1501                    Notarization::read_cfg(&mut value, &certificate_codec_config)
1502                else {
1503                    response.send_lossy(false);
1504                    return;
1505                };
1506
1507                // The resolver key binds this response to `round`; a certificate for any other
1508                // round is a bad response even if it decodes correctly.
1509                if notarization.round() != round {
1510                    response.send_lossy(false);
1511                    return;
1512                }
1513
1514                // Use the notarization payload to derive the block decode config. Below, the
1515                // decoded block is checked against the same payload.
1516                let commitment = notarization.proposal.payload;
1517                if !V::check_payload(scheme.as_ref(), commitment) {
1518                    response.send_lossy(false);
1519                    return;
1520                }
1521                let block_cfg = V::block_cfg(&self.block_codec_config, commitment);
1522                let Ok(block) = V::Block::decode_cfg(value, &block_cfg) else {
1523                    response.send_lossy(false);
1524                    return;
1525                };
1526
1527                if V::commitment(&block) != notarization.proposal.payload {
1528                    response.send_lossy(false);
1529                    return;
1530                }
1531                delivers.push(PendingVerification::Notarized {
1532                    notarization,
1533                    block,
1534                    response,
1535                });
1536            }
1537        }
1538    }
1539
1540    /// Batch verify pending certificates and process valid items.
1541    #[tracing::instrument(name = "marshal.actor.verify_delivered", level = "info", skip_all, fields(count = delivers.len().traced()))]
1542    async fn verify_delivered<Buf: Buffer<V>>(
1543        &mut self,
1544        mut delivers: Vec<PendingVerification<P::Scheme, V>>,
1545        buffer: &mut Buf,
1546        application: &mut impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
1547        resolver: &mut impl Resolver<Key = ResolverRequestFor<V>, Subscriber = Annotation>,
1548    ) {
1549        delivers.retain(|item| !item.response_closed());
1550        if delivers.is_empty() {
1551            return;
1552        }
1553
1554        // Extract (subject, certificate) pairs for batch verification.
1555        let certs: Vec<_> = delivers
1556            .iter()
1557            .map(|item| match item {
1558                PendingVerification::Finalized { finalization, .. } => (
1559                    Subject::Finalize {
1560                        proposal: &finalization.proposal,
1561                    },
1562                    &finalization.certificate,
1563                ),
1564                PendingVerification::Notarized { notarization, .. } => (
1565                    Subject::Notarize {
1566                        proposal: &notarization.proposal,
1567                    },
1568                    &notarization.certificate,
1569                ),
1570            })
1571            .collect();
1572
1573        // Group indices by epoch.
1574        let mut by_epoch: BTreeMap<Epoch, Vec<usize>> = BTreeMap::new();
1575        for (i, item) in delivers.iter().enumerate() {
1576            let epoch = match item {
1577                PendingVerification::Notarized { notarization, .. } => notarization.epoch(),
1578                PendingVerification::Finalized { finalization, .. } => finalization.epoch(),
1579            };
1580            by_epoch.entry(epoch).or_default().push(i);
1581        }
1582
1583        // Batch verify each epoch group.
1584        let mut verified = vec![false; delivers.len()];
1585        for (epoch, indices) in &by_epoch {
1586            let Some(scoped) = self.provider.scoped(*epoch) else {
1587                continue;
1588            };
1589            let group: Vec<_> = indices.iter().map(|&i| certs[i]).collect();
1590            let results =
1591                verify_certificates(self.context.as_mut(), &scoped, &group, &self.strategy);
1592            for (j, &idx) in indices.iter().enumerate() {
1593                verified[idx] = results[j];
1594            }
1595        }
1596
1597        // Process each verified item, rejecting unverified ones.
1598        for (index, item) in delivers.drain(..).enumerate() {
1599            if !verified[index] {
1600                match item {
1601                    PendingVerification::Finalized { response, .. }
1602                    | PendingVerification::Notarized { response, .. } => {
1603                        response.send_lossy(false);
1604                    }
1605                }
1606                continue;
1607            }
1608            match item {
1609                PendingVerification::Finalized {
1610                    finalization,
1611                    block,
1612                    response,
1613                } => {
1614                    // Valid finalization received.
1615                    response.send_lossy(true);
1616                    let block = Arc::new(V::from_application_block(
1617                        block,
1618                        finalization.proposal.payload,
1619                    ));
1620                    let round = finalization.round();
1621                    let height = block.height();
1622                    let digest = block.digest();
1623                    debug!(?round, %height, "received finalization");
1624
1625                    // The floor-anchor path fully handles this finalization
1626                    // and moves the lower bound past it.
1627                    if self
1628                        .ingest(Arc::clone(&block), buffer, application, resolver)
1629                        .await
1630                    {
1631                        continue;
1632                    }
1633
1634                    self.update_processed_round_floor(height, round, resolver)
1635                        .await;
1636
1637                    self.store_finalization(
1638                        height,
1639                        digest,
1640                        Arc::unwrap_or_clone(block),
1641                        Some(finalization),
1642                        application,
1643                    )
1644                    .await;
1645                }
1646                PendingVerification::Notarized {
1647                    notarization,
1648                    block,
1649                    response,
1650                } => {
1651                    // Valid notarization received.
1652                    response.send_lossy(true);
1653                    let round = notarization.round();
1654                    let commitment = notarization.proposal.payload;
1655                    let digest = V::commitment_to_inner(commitment);
1656                    debug!(?round, ?digest, "received notarization");
1657
1658                    // Cache the notarization and block, blocking until both are
1659                    // durable (or the runtime is shutting down) so the repair
1660                    // bookkeeping below never runs ahead of storage.
1661                    let height = block.height();
1662                    let block = Arc::new(block);
1663                    let block_sync = self
1664                        .cache
1665                        .put_notarized(round, digest, block.as_ref().clone().into())
1666                        .await;
1667                    let notarization_sync = self
1668                        .cache
1669                        .put_notarization(round, digest, notarization)
1670                        .await;
1671                    join(
1672                        block_sync.durable(round, "notarized"),
1673                        notarization_sync.durable(round, "notarization"),
1674                    )
1675                    .await;
1676
1677                    // A notarized delivery can carry the pending floor block
1678                    // after the finalization is cached.
1679                    if self
1680                        .ingest(Arc::clone(&block), buffer, application, resolver)
1681                        .await
1682                    {
1683                        continue;
1684                    }
1685
1686                    // If there exists a finalization certificate for this block, we
1687                    // should finalize it. This could finalize the block faster when
1688                    // a notarization then a finalization are received via consensus
1689                    // and we resolve the notarization request before the block request.
1690                    if let Some(finalization) = self.cache.get_finalization_for(digest).await {
1691                        self.update_processed_round_floor(height, finalization.round(), resolver)
1692                            .await;
1693
1694                        // SAFETY: `digest` identifies a unique `commitment`, so this
1695                        // cached finalization payload must match `V::commitment(&block)`.
1696                        self.store_finalization(
1697                            height,
1698                            digest,
1699                            Arc::unwrap_or_clone(block),
1700                            Some(finalization),
1701                            application,
1702                        )
1703                        .await;
1704                    }
1705                }
1706            }
1707        }
1708    }
1709
1710    /// Returns the certificate codec config for `epoch`.
1711    fn certificate_codec_config(
1712        &self,
1713        epoch: Epoch,
1714    ) -> Option<<<P::Scheme as Verifier>::Certificate as Read>::Cfg> {
1715        self.provider
1716            .scoped(epoch)
1717            .map(|scoped| scoped.certificate_codec_config())
1718    }
1719
1720    /// Returns the epoch containing `height` and its certificate codec config.
1721    fn certificate_codec_config_for_height(
1722        &self,
1723        height: Height,
1724    ) -> Option<(Epoch, <<P::Scheme as Verifier>::Certificate as Read>::Cfg)> {
1725        let epoch = self.epocher.containing(height)?.epoch();
1726        self.certificate_codec_config(epoch)
1727            .map(|config| (epoch, config))
1728    }
1729
1730    // -------------------- Application Dispatch --------------------
1731
1732    /// Attempt to dispatch the next finalized block to the application if ready.
1733    ///
1734    /// Dispatch finalized blocks to the application until the pipeline is full
1735    /// or no more blocks are available.
1736    ///
1737    /// This does NOT advance the processed floor height or sync metadata. It only
1738    /// sends blocks to the application and enqueues pending acks. Metadata is
1739    /// updated later, in a subsequent `select_loop!` iteration, when the ack
1740    /// handler updates the processed height.
1741    ///
1742    /// Blocks are dispatched only once durable. Every buffered
1743    /// finalized-archive write freezes dispatch at or above its height until
1744    /// a sync covering it completes (see [`DispatchGate`]). Dispatch is
1745    /// re-attempted by the pool-completion arm for pooled syncs and by the
1746    /// caller itself after a blocking sync. Callers that buffer writes must
1747    /// still call [`Self::sync_finalized`] or [`Self::start_finalized_sync`]
1748    /// before yielding to the `select_loop!` so the freeze is released.
1749    ///
1750    /// Acks are processed in FIFO order so the processed floor height always
1751    /// advances sequentially.
1752    ///
1753    /// # Crash safety
1754    ///
1755    /// Because `select_loop!` arms run to completion, archive data is always
1756    /// durable before the ack handler advances the processed floor height:
1757    ///
1758    /// ```text
1759    /// Iteration N (caller):
1760    ///   store_finalization   ->  Archive::put (buffered)
1761    ///   sync_finalized       ->  archive durable
1762    ///   try_dispatch_blocks  ->  sends durable blocks to app, enqueues pending acks
1763    ///
1764    /// Iteration M (ack handler, M > N):
1765    ///   ack handler       ->  update_processed_height  ->  metadata buffered
1766    ///   stream.sync       ->  metadata durable
1767    /// ```
1768    async fn try_dispatch_blocks(
1769        &mut self,
1770        application: &mut impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
1771    ) {
1772        // Dispatch resumes after the floor anchor is durably stored.
1773        if self.floor.blocks_progress() {
1774            return;
1775        }
1776
1777        // Durability barrier: buffered writes are readable from the archives
1778        // before they are durable. Never dispatch at or above the lowest
1779        // write not yet covered by a completed sync.
1780        let barrier = self.dispatch_gate.barrier();
1781        while self.pending_acks.has_capacity() {
1782            let next_height = self
1783                .pending_acks
1784                .next_dispatch_height(self.stream.next_height());
1785            if barrier.is_some_and(|lowest| next_height >= lowest) {
1786                return;
1787            }
1788            let Some(block) = self.get_finalized_block(next_height).await else {
1789                return;
1790            };
1791            assert_eq!(
1792                block.height(),
1793                next_height,
1794                "finalized block height mismatch"
1795            );
1796
1797            let (height, commitment) = (block.height(), V::commitment(&block));
1798            let (ack, ack_waiter) = A::handle();
1799            application.report(Update::Block(V::owned_into_inner_shared(block), ack));
1800            self.pending_acks.enqueue(PendingAck {
1801                height,
1802                commitment,
1803                receiver: ack_waiter,
1804            });
1805        }
1806    }
1807
1808    // -------------------- Prunable Storage --------------------
1809
1810    /// Sync both finalization archives to durable storage, blocking the actor
1811    /// until they are durable.
1812    ///
1813    /// Must be called within the same `select_loop!` arm as any preceding
1814    /// [`Self::store_finalization`] / [`Self::try_repair_gaps`] writes, before yielding back
1815    /// to the loop. This is the durability barrier for application delivery:
1816    /// [`Self::try_dispatch_blocks`] must run only after this sync completes.
1817    /// It also ensures archives are durable before the ack handler advances
1818    /// the processed floor height. See [`Self::try_dispatch_blocks`] for details.
1819    ///
1820    /// Blocking the actor stalls every mailbox caller behind the sync.
1821    /// Prefer [`Self::start_finalized_sync`] unless work later in the same
1822    /// arm requires the writes to already be durable.
1823    #[tracing::instrument(name = "marshal.actor.sync_finalized", level = "info", skip_all)]
1824    async fn sync_finalized(&mut self) {
1825        if let Err(e) = try_join!(
1826            async {
1827                self.finalized_blocks.sync().await.map_err(Box::new)?;
1828                Ok::<_, BoxedError>(())
1829            },
1830            async {
1831                self.finalizations_by_height
1832                    .sync()
1833                    .await
1834                    .map_err(Box::new)?;
1835                Ok::<_, BoxedError>(())
1836            },
1837        ) {
1838            panic!("failed to sync finalization archives: {e}");
1839        }
1840
1841        // Everything accepted before this sync is now durable, so nothing
1842        // remains to gate dispatch.
1843        self.dispatch_gate.clear();
1844    }
1845
1846    /// Start a non-blocking sync of both finalization archives on the
1847    /// durability pool. A no-op if nothing was written since the last sync
1848    /// (blocking or pooled) started.
1849    ///
1850    /// The pooled entry resolves to [`PooledSync::Finalized`] once every write
1851    /// accepted before this call is durable. The sync adopts every deferred
1852    /// write (see [`DispatchGate::adopt`]), and until the pool-completion arm
1853    /// observes the completion, [`Self::try_dispatch_blocks`] will not
1854    /// dispatch at or above the lowest height a pending batch wrote. This
1855    /// preserves the durability barrier described there without blocking the
1856    /// mailbox on a sync like [`Self::sync_finalized`].
1857    ///
1858    /// Like [`Self::sync_finalized`], this must be called within the same
1859    /// `select_loop!` arm as the writes it covers, before yielding back to the
1860    /// loop. `round` only labels the sync in diagnostics.
1861    #[tracing::instrument(name = "marshal.actor.start_finalized_sync", level = "info", skip_all)]
1862    async fn start_finalized_sync(&mut self, round: Round, syncs: &mut Pool<PooledSync>) {
1863        // If no write needs syncing, every accepted write is already covered
1864        // by a blocking or in-flight sync.
1865        let Some(seq) = self.dispatch_gate.adopt() else {
1866            return;
1867        };
1868
1869        let (blocks, finalizations) = match try_join!(
1870            async {
1871                let handle = self.finalized_blocks.start_sync().await.map_err(Box::new)?;
1872                Ok::<_, BoxedError>(handle)
1873            },
1874            async {
1875                let handle = self
1876                    .finalizations_by_height
1877                    .start_sync()
1878                    .await
1879                    .map_err(Box::new)?;
1880                Ok::<_, BoxedError>(handle)
1881            },
1882        ) {
1883            Ok(handles) => handles,
1884            Err(e) => panic!("failed to start finalization archive sync: {e}"),
1885        };
1886        syncs.push(async move {
1887            let (blocks, finalizations) = join(
1888                blocks.durable(round, "finalized blocks"),
1889                finalizations.durable(round, "finalizations"),
1890            )
1891            .await;
1892            if blocks && finalizations {
1893                PooledSync::Finalized(seq)
1894            } else {
1895                // Runtime shutdown before the sync completed: nothing may be
1896                // released for dispatch.
1897                PooledSync::Observed
1898            }
1899        });
1900    }
1901
1902    // -------------------- Immutable Storage --------------------
1903
1904    /// Get a finalized block from the immutable archive.
1905    async fn get_finalized_block(&self, height: Height) -> Option<V::Block> {
1906        match self
1907            .finalized_blocks
1908            .get(ArchiveID::Index(height.get()))
1909            .await
1910        {
1911            Ok(stored) => stored.map(|stored| stored.into()),
1912            Err(e) => panic!("failed to get block: {e}"),
1913        }
1914    }
1915
1916    /// Get a finalization from the archive by height.
1917    async fn get_finalization_by_height(
1918        &self,
1919        height: Height,
1920    ) -> Option<Finalization<P::Scheme, V::Commitment>> {
1921        match self
1922            .finalizations_by_height
1923            .get(ArchiveID::Index(height.get()))
1924            .await
1925        {
1926            Ok(finalization) => finalization,
1927            Err(e) => panic!("failed to get finalization: {e}"),
1928        }
1929    }
1930
1931    /// Check whether a finalization exists in the archive at `height` without
1932    /// fetching it.
1933    async fn has_finalization_by_height(&self, height: Height) -> bool {
1934        match self.finalizations_by_height.has(height).await {
1935            Ok(has) => has,
1936            Err(e) => panic!("failed to check finalization: {e}"),
1937        }
1938    }
1939
1940    /// Get finalized block information from either the finalization archive or
1941    /// the finalized-block archive.
1942    async fn get_info_by_height(
1943        &self,
1944        height: Height,
1945    ) -> Option<(Height, <V::Block as Digestible>::Digest)> {
1946        if let Some(finalization) = self.get_finalization_by_height(height).await {
1947            return Some((
1948                height,
1949                V::commitment_to_inner(finalization.proposal.payload),
1950            ));
1951        }
1952
1953        self.get_finalized_block(height)
1954            .await
1955            .map(|block| (block.height(), block.digest()))
1956    }
1957
1958    /// Add a finalized block, and optionally a finalization, to the archive.
1959    ///
1960    /// After persisting the block, the caller must sync finalized archives
1961    /// before dispatching the next contiguous block to the application. The
1962    /// buffered archive writes from this method are not a sufficient durability
1963    /// guarantee for downstream application state transitions on their own.
1964    ///
1965    /// Writes are buffered and not synced. The caller must call
1966    /// [sync_finalized](Self::sync_finalized) (blocking) or
1967    /// [start_finalized_sync](Self::start_finalized_sync) (pooled) before
1968    /// yielding to the `select_loop!` so that archive data is durable before
1969    /// the ack handler advances the processed floor height. See
1970    /// [`Self::try_dispatch_blocks`] for the crash safety invariant.
1971    async fn store_finalization(
1972        &mut self,
1973        height: Height,
1974        digest: <V::Block as Digestible>::Digest,
1975        block: V::Block,
1976        finalization: Option<Finalization<P::Scheme, V::Commitment>>,
1977        application: &mut impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
1978    ) -> bool {
1979        // Blocks below the last processed height are not useful to us, so we ignore them (this
1980        // has the nice byproduct of ensuring we don't call a backing store with a block below the
1981        // pruning boundary)
1982        if height <= self.floor.processed_height() {
1983            debug!(
1984                %height,
1985                floor = %self.floor.processed_height(),
1986                ?digest,
1987                "dropping finalization at or below processed height floor"
1988            );
1989            return false;
1990        }
1991
1992        // Convert block to storage format
1993        let stored: V::StoredBlock = block.into();
1994        let round = finalization.as_ref().map(|f| f.round());
1995
1996        // In parallel, update the finalized blocks and finalizations archives
1997        if let Err(e) = try_join!(
1998            // Update the finalized blocks archive
1999            async {
2000                self.finalized_blocks.put(stored).await.map_err(Box::new)?;
2001                Ok::<_, BoxedError>(())
2002            },
2003            // Update the finalizations archive (if provided)
2004            async {
2005                if let Some(finalization) = finalization {
2006                    self.finalizations_by_height
2007                        .put(height, digest, finalization)
2008                        .await
2009                        .map_err(Box::new)?;
2010                }
2011                Ok::<_, BoxedError>(())
2012            }
2013        ) {
2014            panic!("failed to finalize: {e}");
2015        }
2016
2017        // The write above is buffered and readable before it is durable, so
2018        // hold dispatch at or above it until a sync covers it.
2019        self.dispatch_gate.defer(height);
2020
2021        // Update metrics and application
2022        if let Some(round) = round.filter(|_| height > self.tip) {
2023            application.report(Update::Tip(round, height, digest));
2024            self.tip = height;
2025            let _ = self.finalized_height.try_set(height.get());
2026        }
2027
2028        true
2029    }
2030
2031    /// Get the latest finalized block information (height and digest tuple).
2032    ///
2033    /// Blocks are only finalized directly with a finalization or indirectly via a descendant
2034    /// block's finalization. Thus, the highest known finalized block must itself have a direct
2035    /// finalization.
2036    ///
2037    /// We return the height and digest using the highest known finalization that we know the
2038    /// block height for. While it's possible that we have a later finalization, if we do not have
2039    /// the full block for that finalization, we do not know its height and therefore it would not
2040    /// yet be found in the `finalizations_by_height` archive. While not checked explicitly, we
2041    /// should have the associated block (in the `finalized_blocks` archive) for the information
2042    /// returned.
2043    async fn get_latest(&mut self) -> Option<(Height, <V::Block as Digestible>::Digest, Round)> {
2044        let height = self.finalizations_by_height.last_index()?;
2045        let finalization = self
2046            .get_finalization_by_height(height)
2047            .await
2048            .expect("finalization missing");
2049        Some((
2050            height,
2051            V::commitment_to_inner(finalization.proposal.payload),
2052            finalization.round(),
2053        ))
2054    }
2055
2056    // -------------------- Mixed Storage --------------------
2057
2058    /// Looks for a block in cache and finalized storage by digest.
2059    async fn find_block_in_storage(
2060        &self,
2061        digest: <V::Block as Digestible>::Digest,
2062    ) -> Option<V::Block> {
2063        // Check verified / notarized blocks via cache manager.
2064        if let Some(block) = self.cache.find_block_matching(digest, |_| true).await {
2065            return Some(block.into());
2066        }
2067        // Check finalized blocks.
2068        match self.finalized_blocks.get(ArchiveID::Key(&digest)).await {
2069            Ok(stored) => stored.map(|stored| stored.into()),
2070            Err(e) => panic!("failed to get block: {e}"),
2071        }
2072    }
2073
2074    /// Looks for a block in cache and finalized storage by full consensus commitment.
2075    async fn find_block_in_storage_by_commitment(
2076        &self,
2077        commitment: V::Commitment,
2078    ) -> Option<V::Block> {
2079        let digest = V::commitment_to_inner(commitment);
2080        if let Some(block) = self
2081            .cache
2082            .find_block_matching(digest, |stored| V::stored_commitment(stored) == commitment)
2083            .await
2084        {
2085            return Some(block.into());
2086        }
2087
2088        match self.finalized_blocks.get(ArchiveID::Key(&digest)).await {
2089            Ok(Some(stored)) => {
2090                (V::stored_commitment(&stored) == commitment).then(|| stored.into())
2091            }
2092            Ok(None) => None,
2093            Err(e) => panic!("failed to get block: {e}"),
2094        }
2095    }
2096
2097    /// Looks for a block anywhere in local storage using only the digest.
2098    ///
2099    /// This is used when we only have a digest (during gap repair following
2100    /// parent links).
2101    async fn find_block_by_digest<Buf: Buffer<V>>(
2102        &self,
2103        buffer: &Buf,
2104        digest: <V::Block as Digestible>::Digest,
2105    ) -> Option<Arc<V::Block>> {
2106        if let Some(block) = buffer.find_by_digest(digest).await {
2107            return Some(block);
2108        }
2109        self.find_block_in_storage(digest).await.map(Arc::new)
2110    }
2111
2112    /// Looks for a block anywhere in local storage using the full commitment.
2113    ///
2114    /// This is used when we have a full commitment (from notarizations/finalizations).
2115    /// Having the full commitment may enable additional retrieval mechanisms.
2116    async fn find_block_by_commitment<Buf: Buffer<V>>(
2117        &self,
2118        buffer: &Buf,
2119        commitment: V::Commitment,
2120    ) -> Option<Arc<V::Block>> {
2121        if let Some(block) = buffer.find_by_commitment(commitment).await {
2122            return Some(block);
2123        }
2124        self.find_block_in_storage_by_commitment(commitment)
2125            .await
2126            .map(Arc::new)
2127    }
2128
2129    /// Attempt to repair any identified gaps in the finalized blocks archive. The total
2130    /// number of missing heights that can be repaired at once is bounded by `self.max_repair`,
2131    /// though multiple gaps may be spanned.
2132    ///
2133    /// This also handles the "trailing" case where finalizations exist beyond
2134    /// the last stored block (the block data was lost before a crash). The
2135    /// trailing block is anchored first so that backward gap repair can fill
2136    /// inward from it.
2137    ///
2138    /// Writes are buffered. Returns `true` if this call wrote repaired blocks and
2139    /// needs a subsequent [`sync_finalized`](Self::sync_finalized).
2140    #[tracing::instrument(name = "marshal.actor.try_repair_gaps", level = "info", skip_all)]
2141    async fn try_repair_gaps<Buf: Buffer<V>>(
2142        &mut self,
2143        buffer: &mut Buf,
2144        resolver: &mut impl Resolver<Key = ResolverRequestFor<V>, Subscriber = Annotation>,
2145        application: &mut impl Reporter<Activity = Update<V::ApplicationBlock, A>>,
2146    ) -> bool {
2147        // Gap repair needs a known processed floor. A floor transition may
2148        // jump the lower bound once its anchor block arrives.
2149        if self.floor.blocks_progress() {
2150            return false;
2151        }
2152
2153        let mut wrote = false;
2154        let start = self.floor.processed_height().next();
2155
2156        // If finalizations extend beyond the last stored block, anchor the
2157        // trailing block so the gap repair loop below can walk backward from it.
2158        if let Some(last_finalized) = self.finalizations_by_height.last_index() {
2159            let have_block = self
2160                .finalized_blocks
2161                .last_index()
2162                .is_some_and(|last| last >= last_finalized);
2163            if last_finalized > self.floor.processed_height() && !have_block {
2164                // Get the finalization for the last finalized block.
2165                let finalization = self
2166                    .get_finalization_by_height(last_finalized)
2167                    .await
2168                    .expect("finalization missing");
2169                let commitment = finalization.proposal.payload;
2170                if let Some(block) = self.find_block_by_commitment(buffer, commitment).await {
2171                    // If found, persist the block.
2172                    let digest = block.digest();
2173                    wrote |= self
2174                        .store_finalization(
2175                            last_finalized,
2176                            digest,
2177                            Arc::unwrap_or_clone(block),
2178                            Some(finalization),
2179                            application,
2180                        )
2181                        .await;
2182                } else {
2183                    // Request the missing block.
2184                    self.floor
2185                        .fetch_if_permitted(
2186                            resolver,
2187                            Request::finalized_block_by_height(commitment, last_finalized),
2188                        )
2189                        .ignore();
2190                }
2191            }
2192        }
2193
2194        // Fill internal gaps by walking backward from each gap's end block.
2195        'cache_repair: loop {
2196            let (gap_start, Some(gap_end)) = self.finalized_blocks.next_gap(start) else {
2197                // No gaps detected
2198                return wrote;
2199            };
2200
2201            // Attempt to repair the gap backwards from the end of the gap, using
2202            // blocks from our local storage. The walkback only needs each
2203            // block's height and parent linkage.
2204            let Some(cursor) = self.get_finalized_block(gap_end).await else {
2205                panic!("gapped block missing that should exist: {gap_end}");
2206            };
2207            let (mut height, mut parent_digest, mut parent_commitment) = (
2208                cursor.height(),
2209                cursor.parent(),
2210                V::parent_commitment(&cursor),
2211            );
2212
2213            // Compute the lower bound of the recursive repair. `gap_start` is `Some`
2214            // if `start` is not in a gap. We add one to it to ensure we don't
2215            // re-persist it to the database in the repair loop below.
2216            let gap_start = gap_start.map(Height::next).unwrap_or(start);
2217
2218            // Iterate backwards, repairing blocks as we go.
2219            while height > gap_start {
2220                if let Some(block) = self
2221                    .find_block_by_commitment(buffer, parent_commitment)
2222                    .await
2223                {
2224                    let finalization = self.cache.get_finalization_for(parent_digest).await;
2225                    let next = (block.height(), block.parent(), V::parent_commitment(&block));
2226                    wrote |= self
2227                        .store_finalization(
2228                            next.0,
2229                            parent_digest,
2230                            Arc::unwrap_or_clone(block),
2231                            finalization,
2232                            application,
2233                        )
2234                        .await;
2235                    debug!(height = %next.0, "repaired block");
2236                    (height, parent_digest, parent_commitment) = next;
2237                } else {
2238                    // Request the next missing commitment.
2239                    //
2240                    // SAFETY: Finalized blocks are archived only after the
2241                    // parent relationship needed for walkback has been
2242                    // validated by marshal.
2243                    let parent_height = height
2244                        .previous()
2245                        .expect("cursor above gap start has a parent");
2246                    self.floor
2247                        .fetch_if_permitted(
2248                            resolver,
2249                            Request::finalized_block_by_height(parent_commitment, parent_height),
2250                        )
2251                        .ignore();
2252                    break 'cache_repair;
2253                }
2254            }
2255        }
2256
2257        // Request any finalizations for missing items in the archive, up to
2258        // the `max_repair` quota. This may help shrink the size of the gap
2259        // closest to the application's processed height if finalizations
2260        // for the requests' heights exist. If not, we rely on the recursive
2261        // digest fetches above.
2262        let missing_items = self
2263            .finalized_blocks
2264            .missing_items(start, self.max_repair.get());
2265        let requests: Vec<_> = missing_items.into_iter().map(Request::finalized).collect();
2266        if !requests.is_empty() {
2267            self.floor
2268                .fetch_all_if_permitted(resolver, requests)
2269                .ignore();
2270        }
2271        wrote
2272    }
2273
2274    /// Buffers a processed height update in memory and metrics. Does NOT sync
2275    /// to durable storage. Sync metadata after buffered updates to make them durable.
2276    fn update_processed_height(
2277        &mut self,
2278        height: Height,
2279        resolver: &mut impl Resolver<Key = ResolverRequestFor<V>, Subscriber = Annotation>,
2280    ) {
2281        self.stream.acknowledge(height);
2282        self.floor.set_processed_height(height);
2283        let _ = self
2284            .processed_height
2285            .try_set(self.floor.processed_height().get());
2286
2287        // Prune any existing requests below the new floor.
2288        resolver.retain(handler::above_height_floor::<V::Commitment>(height));
2289    }
2290
2291    /// Returns the latest known finalization round at or below the processed height.
2292    async fn latest_processed_round(finalizations_by_height: &FC, height: Option<Height>) -> Round {
2293        let Some(height) = height else {
2294            return Round::zero();
2295        };
2296        let Some(finalization_height) = finalizations_by_height
2297            .ranges_from(Height::zero())
2298            .filter_map(|(start, end)| (start <= height).then_some(end.min(height)))
2299            .max()
2300        else {
2301            return Round::zero();
2302        };
2303
2304        match finalizations_by_height
2305            .get(ArchiveID::Index(finalization_height.get()))
2306            .await
2307        {
2308            Ok(Some(finalization)) => finalization.round(),
2309            Ok(None) => panic!("processed finalization missing from stored range"),
2310            Err(err) => panic!("failed to get processed finalization: {err}"),
2311        }
2312    }
2313
2314    /// Buffers a processed round update in memory and prunes round-bound requests.
2315    async fn update_processed_round(
2316        &mut self,
2317        height: Height,
2318        resolver: &mut impl Resolver<Key = ResolverRequestFor<V>, Subscriber = Annotation>,
2319    ) {
2320        let Some(finalization) = self.get_finalization_by_height(height).await else {
2321            return;
2322        };
2323        self.update_processed_round_floor(height, finalization.round(), resolver)
2324            .await;
2325    }
2326
2327    /// Buffers a processed round floor update in memory and prunes round-bound requests.
2328    async fn update_processed_round_floor(
2329        &mut self,
2330        height: Height,
2331        round: Round,
2332        resolver: &mut impl Resolver<Key = ResolverRequestFor<V>, Subscriber = Annotation>,
2333    ) {
2334        if height > self.floor.processed_height() || round <= self.floor.processed_round() {
2335            return;
2336        }
2337
2338        let previous = self.floor.processed_round();
2339        self.floor.set_processed_round(round);
2340
2341        // Retain view-indexed cache data for a window behind the previously
2342        // processed finalized block.
2343        let prune_round = Round::new(
2344            previous.epoch(),
2345            previous.view().saturating_sub(self.view_retention_timeout),
2346        );
2347        self.cache.prune_by_view(prune_round).await;
2348
2349        // Prune round-bound requests at or below the processed round.
2350        resolver.retain(handler::above_round_floor::<V::Commitment>(
2351            self.floor.processed_round(),
2352        ));
2353    }
2354
2355    /// Prunes finalized blocks and certificates below the given height.
2356    async fn prune_finalized_archives(&mut self, height: Height) -> Result<(), BoxedError> {
2357        // Prune the finalized block and finalization certificate archives in parallel.
2358        try_join!(
2359            async {
2360                self.finalized_blocks
2361                    .prune(height)
2362                    .await
2363                    .map_err(Box::new)?;
2364                Ok::<_, BoxedError>(())
2365            },
2366            async {
2367                self.finalizations_by_height
2368                    .prune(height)
2369                    .await
2370                    .map_err(Box::new)?;
2371                Ok::<_, BoxedError>(())
2372            }
2373        )?;
2374        Ok(())
2375    }
2376
2377    /// Prunes finalized archives and height-indexed certified cache data below the durable floor.
2378    async fn prune_after_floor(&mut self, height: Height) -> Result<(), BoxedError> {
2379        let cache = &mut self.cache;
2380        let finalized_blocks = &mut self.finalized_blocks;
2381        let finalizations_by_height = &mut self.finalizations_by_height;
2382        try_join!(
2383            async {
2384                cache.prune_by_height(height).await;
2385                Ok::<_, BoxedError>(())
2386            },
2387            async {
2388                finalized_blocks.prune(height).await.map_err(Box::new)?;
2389                Ok::<_, BoxedError>(())
2390            },
2391            async {
2392                finalizations_by_height
2393                    .prune(height)
2394                    .await
2395                    .map_err(Box::new)?;
2396                Ok::<_, BoxedError>(())
2397            }
2398        )?;
2399        Ok(())
2400    }
2401}