Skip to main content

commonware_consensus/marshal/core/
actor.rs

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