Skip to main content

commonware_consensus/aggregation/
engine.rs

1//! Engine for the module.
2
3use super::{
4    Config, metrics,
5    safe_tip::SafeTip,
6    types::{Ack, Activity, Error, Item, TipAck},
7};
8use crate::{
9    Automaton, Monitor, Reporter,
10    aggregation::{scheme, types::Certificate},
11    types::{Epoch, EpochDelta, Height, HeightDelta, Participant},
12};
13use commonware_cryptography::{
14    Digest,
15    certificate::{Provider, Scheme, Verifier},
16};
17use commonware_macros::select_loop;
18use commonware_p2p::{
19    Blocker, Receiver, Recipients, Sender,
20    utils::codec::{WrappedSender, wrap},
21};
22use commonware_parallel::Strategy;
23use commonware_runtime::{
24    BufferPooler, Clock, ContextCell, Handle, Metrics, ReadOptions, Spawner, Storage,
25    buffer::paged::CacheRef,
26    spawn_cell,
27    telemetry::metrics::{GaugeExt, histogram, status::Status},
28};
29use commonware_storage::journal::segmented::variable::{Config as JConfig, Journal};
30use commonware_utils::{
31    N3f1, PrioritySet,
32    futures::{Pool as FuturesPool, rebind},
33    non_empty,
34    ordered::Quorum,
35};
36use futures::future::{self, Either};
37use rand_core::CryptoRng;
38use std::{
39    cmp::max,
40    collections::BTreeMap,
41    num::{NonZeroU64, NonZeroUsize},
42    sync::Arc,
43    time::{Duration, SystemTime},
44};
45use tracing::{debug, error, info, trace, warn};
46
47/// An entry for a height that does not yet have a certificate.
48enum Pending<S: Scheme, D: Digest> {
49    /// The automaton has not yet provided the digest for this height.
50    /// The signatures may have arbitrary digests.
51    Unverified(BTreeMap<Epoch, BTreeMap<Participant, Ack<S, D>>>),
52
53    /// Verified by the automaton. Now stores the digest.
54    Verified(D, BTreeMap<Epoch, BTreeMap<Participant, Ack<S, D>>>),
55}
56
57/// The type returned by the `pending` pool, used by the application to return which digest is
58/// associated with the given height.
59struct DigestRequest<D: Digest> {
60    /// The height in question.
61    height: Height,
62
63    /// The result of the verification.
64    result: Result<D, Error>,
65
66    /// Records the time taken to get the digest.
67    timer: histogram::Timer,
68}
69
70/// Instance of the engine.
71pub struct Engine<
72    E: BufferPooler + Clock + Spawner + Storage + Metrics + CryptoRng,
73    P: Provider<Scope = Epoch>,
74    D: Digest,
75    A: Automaton<Context = Height, Digest = D>,
76    Z: Reporter<Activity = Activity<P::Scheme, D>>,
77    M: Monitor<Index = Epoch>,
78    B: Blocker<PublicKey = <P::Scheme as Verifier>::PublicKey>,
79    T: Strategy,
80> {
81    // ---------- Interfaces ----------
82    context: ContextCell<E>,
83    automaton: A,
84    monitor: M,
85    provider: P,
86    reporter: Z,
87    blocker: B,
88    strategy: T,
89
90    // Pruning
91    /// A tuple representing the epochs to keep in memory.
92    /// The first element is the number of old epochs to keep.
93    /// The second element is the number of future epochs to accept.
94    ///
95    /// For example, if the current epoch is 10, and the bounds are (1, 2), then
96    /// epochs 9, 10, 11, and 12 are kept (and accepted);
97    /// all others are pruned or rejected.
98    epoch_bounds: (EpochDelta, EpochDelta),
99
100    /// The concurrent number of chunks to process.
101    window: HeightDelta,
102
103    /// Number of heights to track below the tip when collecting acks and/or pruning.
104    activity_timeout: HeightDelta,
105
106    // Messaging
107    /// Pool of pending futures to request a digest from the automaton.
108    digest_requests: FuturesPool<'static, DigestRequest<D>>,
109
110    // State
111    /// The current epoch.
112    epoch: Epoch,
113
114    /// The current tip.
115    tip: Height,
116
117    /// Tracks the tips of all validators.
118    safe_tip: SafeTip<<P::Scheme as Verifier>::PublicKey>,
119
120    /// The keys represent the set of all `Height` values for which we are attempting to form a
121    /// certificate, but do not yet have one. Values may be [Pending::Unverified] or [Pending::Verified],
122    /// depending on whether the automaton has verified the digest or not.
123    pending: BTreeMap<Height, Pending<P::Scheme, D>>,
124
125    /// A map of heights with a certificate. Cached in memory if needed to send to other peers.
126    confirmed: BTreeMap<Height, Certificate<P::Scheme, D>>,
127
128    // ---------- Rebroadcasting ----------
129    /// The frequency at which to rebroadcast pending heights.
130    rebroadcast_timeout: Duration,
131
132    /// A set of deadlines for rebroadcasting `Height` values that do not have a certificate.
133    rebroadcast_deadlines: PrioritySet<Height, SystemTime>,
134
135    // ---------- Journal ----------
136    /// Journal for storing acks signed by this node.
137    journal: Option<Journal<E, Activity<P::Scheme, D>>>,
138    journal_partition: String,
139    journal_write_buffer: NonZeroUsize,
140    journal_replay_buffer: NonZeroUsize,
141    journal_heights_per_section: NonZeroU64,
142    journal_compression: Option<u8>,
143    journal_page_cache: CacheRef,
144
145    // ---------- Network ----------
146    /// Whether to send acks as priority messages.
147    priority_acks: bool,
148
149    // ---------- Metrics ----------
150    /// Metrics
151    metrics: metrics::Metrics,
152}
153
154impl<
155    E: BufferPooler + Clock + Spawner + Storage + Metrics + CryptoRng,
156    P: Provider<Scope = Epoch, Scheme: scheme::Scheme<D>>,
157    D: Digest,
158    A: Automaton<Context = Height, Digest = D>,
159    Z: Reporter<Activity = Activity<P::Scheme, D>>,
160    M: Monitor<Index = Epoch>,
161    B: Blocker<PublicKey = <P::Scheme as Verifier>::PublicKey>,
162    T: Strategy,
163> Engine<E, P, D, A, Z, M, B, T>
164{
165    /// Creates a new engine with the given context and configuration.
166    pub fn new(context: E, cfg: Config<P, D, A, Z, M, B, T>) -> Self {
167        let metrics = metrics::Metrics::init(&context);
168
169        Self {
170            context: ContextCell::new(context),
171            automaton: cfg.automaton,
172            reporter: cfg.reporter,
173            monitor: cfg.monitor,
174            provider: cfg.provider,
175            blocker: cfg.blocker,
176            strategy: cfg.strategy,
177            epoch_bounds: cfg.epoch_bounds,
178            window: HeightDelta::new(cfg.window.into()),
179            activity_timeout: cfg.activity_timeout,
180            epoch: Epoch::zero(),
181            tip: Height::zero(),
182            safe_tip: SafeTip::default(),
183            digest_requests: FuturesPool::default(),
184            pending: BTreeMap::new(),
185            confirmed: BTreeMap::new(),
186            rebroadcast_timeout: cfg.rebroadcast_timeout.into(),
187            rebroadcast_deadlines: PrioritySet::new(),
188            journal: None,
189            journal_partition: cfg.journal_partition,
190            journal_write_buffer: cfg.journal_write_buffer,
191            journal_replay_buffer: cfg.journal_replay_buffer,
192            journal_heights_per_section: cfg.journal_heights_per_section,
193            journal_compression: cfg.journal_compression,
194            journal_page_cache: cfg.journal_page_cache,
195            priority_acks: cfg.priority_acks,
196            metrics,
197        }
198    }
199
200    /// Gets the scheme for a given epoch, returning an error if unavailable.
201    fn scheme(&self, epoch: Epoch) -> Result<Arc<P::Scheme>, Error> {
202        self.provider
203            .scheme(epoch)
204            .ok_or(Error::UnknownEpoch(epoch))
205    }
206
207    /// Runs the engine until the context is stopped.
208    ///
209    /// The engine will handle:
210    /// - Requesting and processing digests from the automaton
211    /// - Timeouts
212    ///   - Refreshing the Epoch
213    ///   - Rebroadcasting Acks
214    /// - Messages from the network:
215    ///   - Acks from other validators
216    pub fn start(
217        mut self,
218        network: (
219            impl Sender<PublicKey = <P::Scheme as Verifier>::PublicKey>,
220            impl Receiver<PublicKey = <P::Scheme as Verifier>::PublicKey>,
221        ),
222    ) -> Handle<()> {
223        spawn_cell!(self.context, self.run(network))
224    }
225
226    /// Inner run loop called by `start`.
227    async fn run(
228        mut self,
229        network: (
230            impl Sender<PublicKey = <P::Scheme as Verifier>::PublicKey>,
231            impl Receiver<PublicKey = <P::Scheme as Verifier>::PublicKey>,
232        ),
233    ) {
234        let (mut sender, mut receiver) = wrap(
235            (),
236            self.context.network_buffer_pool().clone(),
237            network.0,
238            network.1,
239        );
240
241        // Initialize the epoch
242        let (latest, mut epoch_updates) = self.monitor.subscribe().await;
243        self.epoch = latest;
244
245        // Initialize Journal
246        let journal_cfg = JConfig {
247            partition: self.journal_partition.clone(),
248            compression: self.journal_compression,
249            codec_config: P::Scheme::certificate_codec_config_unbounded(),
250            page_cache: self.journal_page_cache.clone(),
251            write_buffer: self.journal_write_buffer,
252        };
253        let journal = Journal::init(self.context.child("journal"), journal_cfg)
254            .await
255            .expect("init failed");
256        let (journal, unverified_heights) = self.replay(journal).await;
257        self.journal = Some(journal);
258
259        // Request digests for unverified heights
260        for height in unverified_heights {
261            trace!(%height, "requesting digest for unverified height from replay");
262            self.get_digest(height);
263        }
264
265        // Initialize the tip manager
266        let scheme = self
267            .scheme(self.epoch)
268            .expect("current epoch scheme must exist");
269        self.safe_tip.init(scheme.participants());
270
271        select_loop! {
272            self.context,
273            on_start => {
274                let _ = self.metrics.tip.try_set(self.tip.get());
275
276                // Propose a new digest if we are processing less than the window
277                let next = self.next();
278
279                // Underflow safe: next >= self.tip is guaranteed by next()
280                if next.delta_from(self.tip).unwrap() < self.window {
281                    trace!(%next, "requesting new digest");
282                    assert!(
283                        self.pending
284                            .insert(next, Pending::Unverified(BTreeMap::new()))
285                            .is_none()
286                    );
287                    self.get_digest(next);
288                    continue;
289                }
290
291                // Get the rebroadcast deadline for the next height
292                let rebroadcast = match self.rebroadcast_deadlines.peek() {
293                    Some((_, &deadline)) => Either::Left(self.context.sleep_until(deadline)),
294                    None => Either::Right(future::pending()),
295                };
296            },
297            on_stopped => {
298                debug!("shutdown");
299            },
300            // Handle refresh epoch deadline
301            Some(epoch) = epoch_updates.recv() else {
302                error!("epoch subscription failed");
303                break;
304            } => {
305                // Refresh the epoch
306                debug!(current = %self.epoch, new = %epoch, "refresh epoch");
307                assert!(epoch >= self.epoch);
308                self.epoch = epoch;
309
310                // Update the tip manager
311                let scheme = self
312                    .scheme(self.epoch)
313                    .expect("current epoch scheme must exist");
314                self.safe_tip.reconcile(scheme.participants());
315
316                // Update data structures by purging old epochs
317                let min_epoch = self.epoch.saturating_sub(self.epoch_bounds.0);
318                self.pending
319                    .iter_mut()
320                    .for_each(|(_, pending)| match pending {
321                        self::Pending::Unverified(acks) => {
322                            acks.retain(|epoch, _| *epoch >= min_epoch);
323                        }
324                        self::Pending::Verified(_, acks) => {
325                            acks.retain(|epoch, _| *epoch >= min_epoch);
326                        }
327                    });
328
329                continue;
330            },
331
332            // Sign a new ack
333            request = self.digest_requests.next_completed() => {
334                let DigestRequest {
335                    height,
336                    result,
337                    timer,
338                } = request;
339                match result {
340                    Err(err) => {
341                        warn!(?err, %height, "automaton returned error");
342                        self.metrics.digest.inc(Status::Dropped);
343                    }
344                    Ok(digest) => {
345                        timer.observe(self.context.as_ref());
346                        self = self.handle_digest(height, digest, &mut sender).await;
347                    }
348                }
349            },
350
351            // Handle incoming acks
352            msg = receiver.recv() => {
353                // Error handling
354                let (sender, msg) = match msg {
355                    Ok(r) => r,
356                    Err(err) => {
357                        warn!(?err, "ack receiver failed");
358                        break;
359                    }
360                };
361                let mut guard = self.metrics.acks.guard(Status::Invalid);
362                let TipAck { ack, tip } = match msg {
363                    Ok(peer_ack) => peer_ack,
364                    Err(err) => {
365                        commonware_p2p::block!(self.blocker, sender, ?err, "ack decode failed");
366                        continue;
367                    }
368                };
369
370                // Update the tip manager
371                if self.safe_tip.update(sender.clone(), tip).is_some() {
372                    // Fast-forward our tip if needed
373                    let safe_tip = self.safe_tip.get();
374                    if safe_tip > self.tip {
375                        self = self.fast_forward_tip(safe_tip).await;
376                    }
377                }
378
379                // Validate that we need to process the ack
380                if let Err(err) = self.validate_ack(&ack, &sender) {
381                    if err.blockable() {
382                        commonware_p2p::block!(
383                            self.blocker,
384                            sender,
385                            ?err,
386                            "ack validation failure"
387                        );
388                    } else {
389                        debug!(?sender, ?err, "ack validate failed");
390                    }
391                    continue;
392                };
393
394                // Handle the ack
395                let accepted;
396                (self, accepted) = self.handle_ack(&ack).await;
397                if !accepted {
398                    guard.set(Status::Failure);
399                    continue;
400                }
401
402                // Update the metrics
403                debug!(?sender, epoch = %ack.epoch, height = %ack.item.height, "ack");
404                guard.set(Status::Success);
405            },
406
407            // Rebroadcast
408            _ = rebroadcast => {
409                // Get the next height to rebroadcast
410                let (height, _) = self
411                    .rebroadcast_deadlines
412                    .pop()
413                    .expect("no rebroadcast deadline");
414                trace!(%height, "rebroadcasting");
415                self = self.handle_rebroadcast(height, &mut sender).await;
416            },
417        }
418
419        // Close journal on shutdown
420        if let Some(journal) = self.journal.take() {
421            journal.sync_all().await.expect("unable to sync journal");
422        }
423    }
424
425    // ---------- Handling ----------
426
427    /// Handles a digest returned by the automaton.
428    async fn handle_digest(
429        mut self,
430        height: Height,
431        digest: D,
432        sender: &mut WrappedSender<
433            impl Sender<PublicKey = <P::Scheme as Verifier>::PublicKey>,
434            TipAck<P::Scheme, D>,
435        >,
436    ) -> Self {
437        // Entry must be `Pending::Unverified`, or return early
438        if !matches!(self.pending.get(&height), Some(Pending::Unverified(_))) {
439            debug!(%height, "digest height not pending");
440            return self;
441        };
442
443        // Move the entry to `Pending::Verified`
444        let Some(Pending::Unverified(acks)) = self.pending.remove(&height) else {
445            panic!("Pending::Unverified entry not found");
446        };
447        self.pending
448            .insert(height, Pending::Verified(digest, BTreeMap::new()));
449
450        // Handle each `ack` as if it was received over the network. This inserts the values into
451        // the new map, and may form a certificate if enough acks are present. Only process acks
452        // that match the verified digest.
453        for epoch_acks in acks.values() {
454            for epoch_ack in epoch_acks.values() {
455                // Drop acks that don't match the verified digest
456                if epoch_ack.item.digest != digest {
457                    continue;
458                }
459
460                // Handle the ack
461                (self, _) = self.handle_ack(epoch_ack).await;
462            }
463            // Break early if a certificate was formed
464            if self.confirmed.contains_key(&height) {
465                break;
466            }
467        }
468
469        // Sign my own ack
470        let signed;
471        (self, signed) = self.sign_ack(height, digest).await;
472        let Some(ack) = signed else {
473            return self;
474        };
475
476        // Set the rebroadcast deadline for this height
477        self.rebroadcast_deadlines
478            .put(height, self.context.current() + self.rebroadcast_timeout);
479
480        // Handle ack as if it was received over the network
481        (self, _) = self.handle_ack(&ack).await;
482
483        // Send ack over the network.
484        self.broadcast(ack, sender);
485
486        self
487    }
488
489    /// Handles an ack.
490    ///
491    /// Returns whether the ack was accepted. An ack is rejected if it is invalid or
492    /// inapplicable (e.g. unknown scheme, non-pending height, digest mismatch).
493    /// Duplicate acks are accepted as no-ops.
494    async fn handle_ack(mut self, ack: &Ack<P::Scheme, D>) -> (Self, bool) {
495        // Get the quorum (from scheme participants for the ack's epoch)
496        let scheme = match self.scheme(ack.epoch) {
497            Ok(scheme) => scheme,
498            Err(err) => {
499                debug!(?err, epoch = %ack.epoch, signer = %ack.attestation.signer, "ack for unknown scheme");
500                return (self, false);
501            }
502        };
503        let quorum = usize::try_from(scheme.participants().quorum::<N3f1>())
504            .expect("quorum exceeds usize::MAX");
505
506        // Get the acks and check digest consistency
507        let acks_by_epoch = match self.pending.get_mut(&ack.item.height) {
508            None => {
509                // If the height is not in the pending pool, it may be confirmed
510                // (i.e. we have a certificate for it).
511                debug!(height = %ack.item.height, signer = %ack.attestation.signer, "ack height not pending");
512                return (self, false);
513            }
514            Some(Pending::Unverified(acks)) => acks,
515            Some(Pending::Verified(digest, acks)) => {
516                // If we have a verified digest, ensure the ack matches it
517                if ack.item.digest != *digest {
518                    debug!(height = %ack.item.height, signer = %ack.attestation.signer, "ack digest mismatch");
519                    return (self, false);
520                }
521                acks
522            }
523        };
524
525        // Add the attestation (if not already present)
526        let acks = acks_by_epoch.entry(ack.epoch).or_default();
527        if acks.contains_key(&ack.attestation.signer) {
528            return (self, true);
529        }
530        acks.insert(ack.attestation.signer, ack.clone());
531
532        // If there exists a quorum of acks with the same digest (or for the verified digest if it exists), form a certificate
533        let filtered = acks
534            .values()
535            .filter(|a| a.item.digest == ack.item.digest)
536            .collect::<Vec<_>>();
537        if filtered.len() >= quorum {
538            // Every stored acknowledgement is verified and signer-unique, so a same-item quorum
539            // satisfies the certificate scheme's assembly contract.
540            let certificate =
541                Certificate::from_acks(&*scheme, non_empty![@filtered], &self.strategy)
542                    .expect("verified acknowledgement quorum must assemble");
543            self.metrics.certificates.inc();
544            self = self.handle_certificate(certificate).await;
545        }
546
547        (self, true)
548    }
549
550    /// Handles a certificate.
551    async fn handle_certificate(mut self, certificate: Certificate<P::Scheme, D>) -> Self {
552        // Check if we already have the certificate
553        let height = certificate.item.height;
554        if self.confirmed.contains_key(&height) {
555            return self;
556        }
557
558        // Store the certificate
559        self.confirmed.insert(height, certificate.clone());
560
561        // Journal and notify the automaton
562        let certified = Activity::Certified(certificate);
563        self = self.record(certified.clone()).await.sync(height).await;
564        self.reporter.report(certified);
565
566        // Increase the tip if needed
567        if height == self.tip {
568            // Compute the next tip
569            let mut new_tip = height.next();
570            while self.confirmed.contains_key(&new_tip) && new_tip.get() < u64::MAX {
571                new_tip = new_tip.next();
572            }
573
574            // If the next tip is larger, try to fast-forward the tip (may not be possible)
575            if new_tip > self.tip {
576                self = self.fast_forward_tip(new_tip).await;
577            }
578        }
579
580        self
581    }
582
583    /// Handles a rebroadcast request for the given height.
584    async fn handle_rebroadcast(
585        mut self,
586        height: Height,
587        sender: &mut WrappedSender<
588            impl Sender<PublicKey = <P::Scheme as Verifier>::PublicKey>,
589            TipAck<P::Scheme, D>,
590        >,
591    ) -> Self {
592        let Some(Pending::Verified(digest, acks)) = self.pending.get(&height) else {
593            // The height may already be confirmed; continue silently if so
594            return self;
595        };
596        let digest = *digest;
597
598        // Get our signature
599        let epoch = self.epoch;
600        let scheme = match self.scheme(epoch) {
601            Ok(scheme) => scheme,
602            Err(err) => {
603                warn!(?err, %height, "cannot rebroadcast: unknown scheme");
604                return self;
605            }
606        };
607        let Some(signer) = scheme.me() else {
608            warn!(%epoch, %height, "cannot rebroadcast: not a signer");
609            return self;
610        };
611        let ack = acks.get(&epoch).and_then(|acks| acks.get(&signer).cloned());
612        let ack = match ack {
613            Some(ack) => ack,
614            None => {
615                let signed;
616                (self, signed) = self.sign_ack(height, digest).await;
617                match signed {
618                    Some(ack) => ack,
619                    None => return self,
620                }
621            }
622        };
623
624        // Reinsert the height with a new deadline
625        self.rebroadcast_deadlines
626            .put(height, self.context.current() + self.rebroadcast_timeout);
627
628        // Broadcast the ack to all peers
629        self.broadcast(ack, sender);
630
631        self
632    }
633
634    // ---------- Validation ----------
635
636    /// Takes a raw ack (from sender) from the p2p network and validates it.
637    ///
638    /// Returns an error if the ack is invalid.
639    fn validate_ack(
640        &mut self,
641        ack: &Ack<P::Scheme, D>,
642        sender: &<P::Scheme as Verifier>::PublicKey,
643    ) -> Result<(), Error> {
644        // Validate epoch
645        {
646            let (eb_lo, eb_hi) = self.epoch_bounds;
647            let bound_lo = self.epoch.saturating_sub(eb_lo);
648            let bound_hi = self.epoch.saturating_add(eb_hi);
649            if ack.epoch < bound_lo || ack.epoch > bound_hi {
650                return Err(Error::AckEpochOutsideBounds(ack.epoch, bound_lo, bound_hi));
651            }
652        }
653
654        // Validate sender matches the signer
655        let scheme = self.scheme(ack.epoch)?;
656        let participants = scheme.participants();
657        let Some(signer) = participants.index(sender) else {
658            return Err(Error::UnknownValidator(ack.epoch, sender.to_string()));
659        };
660        if signer != ack.attestation.signer {
661            return Err(Error::PeerMismatch);
662        }
663
664        // Collect acks below the tip (if we don't yet have a certificate)
665        let activity_threshold = self.tip.saturating_sub(self.activity_timeout);
666        if ack.item.height < activity_threshold {
667            return Err(Error::AckCertified(ack.item.height));
668        }
669
670        // If the height is above the tip (and the window), ignore for now
671        if ack
672            .item
673            .height
674            .delta_from(self.tip)
675            .is_some_and(|d| d >= self.window)
676        {
677            return Err(Error::AckHeight(ack.item.height));
678        }
679
680        // Validate that we don't already have the ack
681        if self.confirmed.contains_key(&ack.item.height) {
682            return Err(Error::AckCertified(ack.item.height));
683        }
684        let have_ack = match self.pending.get(&ack.item.height) {
685            None => false,
686            Some(Pending::Unverified(epoch_map)) => epoch_map
687                .get(&ack.epoch)
688                .is_some_and(|acks| acks.contains_key(&ack.attestation.signer)),
689            Some(Pending::Verified(digest, epoch_map)) => {
690                // While we check this in the `handle_ack` function, checking early here avoids an
691                // unnecessary signature check.
692                if ack.item.digest != *digest {
693                    return Err(Error::AckDigest(ack.item.height));
694                }
695                epoch_map
696                    .get(&ack.epoch)
697                    .is_some_and(|acks| acks.contains_key(&ack.attestation.signer))
698            }
699        };
700        if have_ack {
701            return Err(Error::AckDuplicate(sender.to_string(), ack.item.height));
702        }
703
704        // Validate signature
705        if !ack.verify(self.context.as_mut(), &*scheme, &self.strategy) {
706            return Err(Error::InvalidAckSignature);
707        }
708
709        Ok(())
710    }
711
712    // ---------- Helpers ----------
713
714    /// Requests the digest from the automaton.
715    ///
716    /// Pending must contain the height.
717    fn get_digest(&mut self, height: Height) {
718        assert!(self.pending.contains_key(&height));
719        let mut automaton = self.automaton.clone();
720        let timer = self.metrics.digest_duration.timer(self.context.as_ref());
721        self.digest_requests.push(async move {
722            let receiver = automaton.propose(height).await;
723            let result = receiver.await.map_err(Error::AppProposeCanceled);
724            DigestRequest {
725                height,
726                result,
727                timer,
728            }
729        });
730    }
731
732    /// Signs an ack for the given height, and digest. Stores the ack in the journal and returns it.
733    /// Returns `None` if this node cannot sign at the current epoch.
734    async fn sign_ack(mut self, height: Height, digest: D) -> (Self, Option<Ack<P::Scheme, D>>) {
735        let epoch = self.epoch;
736        let scheme = match self.scheme(epoch) {
737            Ok(scheme) => scheme,
738            Err(err) => {
739                warn!(?err, %height, "cannot sign ack: unknown scheme");
740                return (self, None);
741            }
742        };
743
744        // Sign the item
745        let item = Item { height, digest };
746        let Some(ack) = Ack::sign(&*scheme, epoch, item) else {
747            debug!(%epoch, %height, "cannot sign ack: not a signer");
748            return (self, None);
749        };
750
751        // Journal the ack
752        self = self
753            .record(Activity::Ack(ack.clone()))
754            .await
755            .sync(height)
756            .await;
757
758        (self, Some(ack))
759    }
760
761    /// Broadcasts an ack to all peers with the appropriate priority.
762    fn broadcast(
763        &mut self,
764        ack: Ack<P::Scheme, D>,
765        sender: &mut WrappedSender<
766            impl Sender<PublicKey = <P::Scheme as Verifier>::PublicKey>,
767            TipAck<P::Scheme, D>,
768        >,
769    ) {
770        sender.send(
771            Recipients::All,
772            TipAck { ack, tip: self.tip },
773            self.priority_acks,
774        );
775    }
776
777    /// Returns the next height that we should process. This is the minimum height for
778    /// which we do not have a digest or an outstanding request to the automaton for the digest.
779    fn next(&self) -> Height {
780        let max_pending = self
781            .pending
782            .last_key_value()
783            .map(|(k, _)| k.next())
784            .unwrap_or_default();
785        let max_confirmed = self
786            .confirmed
787            .last_key_value()
788            .map(|(k, _)| k.next())
789            .unwrap_or_default();
790        max(self.tip, max(max_pending, max_confirmed))
791    }
792
793    /// Increases the tip to the given value, pruning stale entries.
794    ///
795    /// # Panics
796    ///
797    /// Panics if the given tip is less-than-or-equal-to the current tip.
798    async fn fast_forward_tip(mut self, tip: Height) -> Self {
799        assert!(tip > self.tip);
800
801        // Prune data structures with buffer to prevent losing certificates
802        let activity_threshold = tip.saturating_sub(self.activity_timeout);
803        self.pending
804            .retain(|height, _| *height >= activity_threshold);
805        self.confirmed
806            .retain(|height, _| *height >= activity_threshold);
807
808        // Add tip to journal
809        self = self.record(Activity::Tip(tip)).await.sync(tip).await;
810        self.reporter.report(Activity::Tip(tip));
811
812        // Prune journal with buffer
813        let section = self.get_journal_section(activity_threshold);
814        rebind(&mut self.journal, |journal| journal.prune(section))
815            .await
816            .expect("unable to prune journal");
817
818        // Update the tip
819        self.tip = tip;
820
821        self
822    }
823
824    // ---------- Journal ----------
825
826    /// Returns the section of the journal for the given `height`.
827    const fn get_journal_section(&self, height: Height) -> u64 {
828        height.get() / self.journal_heights_per_section.get()
829    }
830
831    /// Replays the journal, updating the state of the engine.
832    /// Returns the journal and a list of unverified pending heights that need digest requests.
833    async fn replay(
834        &mut self,
835        journal: Journal<E, Activity<P::Scheme, D>>,
836    ) -> (Journal<E, Activity<P::Scheme, D>>, Vec<Height>) {
837        let mut tip = Height::default();
838        let mut certified = Vec::new();
839        let mut acks = Vec::new();
840
841        // Replay rebuilds the engine's in-memory state, so journal pages need
842        // not remain in the OS page cache.
843        let mut replay = journal
844            .replay(0, 0, self.journal_replay_buffer, ReadOptions::DONT_CACHE)
845            .await
846            .expect("replay failed");
847        while let Some(msg) = replay.next().await {
848            let (_, _, _, activity) = msg.expect("replay failed");
849            match activity {
850                Activity::Tip(height) => {
851                    tip = max(tip, height);
852                    self.reporter.report(Activity::Tip(height));
853                }
854                Activity::Certified(certificate) => {
855                    certified.push(certificate.clone());
856                    self.reporter.report(Activity::Certified(certificate));
857                }
858                Activity::Ack(ack) => {
859                    acks.push(ack.clone());
860                    self.reporter.report(Activity::Ack(ack));
861                }
862            }
863        }
864
865        // Update the tip to the highest height in the journal
866        self.tip = tip;
867        let activity_threshold = tip.saturating_sub(self.activity_timeout);
868
869        // Add certified items
870        certified
871            .iter()
872            .filter(|certificate| certificate.item.height >= activity_threshold)
873            .for_each(|certificate| {
874                self.confirmed
875                    .insert(certificate.item.height, certificate.clone());
876            });
877
878        // Group acks by height
879        let mut acks_by_height: BTreeMap<Height, Vec<Ack<P::Scheme, D>>> = BTreeMap::new();
880        for ack in acks {
881            if ack.item.height >= activity_threshold
882                && !self.confirmed.contains_key(&ack.item.height)
883            {
884                acks_by_height.entry(ack.item.height).or_default().push(ack);
885            }
886        }
887
888        // Process each height's acks
889        let mut unverified = Vec::new();
890        for (height, mut acks_group) in acks_by_height {
891            // Check if we have our own ack (which means we've verified the digest)
892            let current_scheme = self.scheme(self.epoch).ok();
893            let our_signer = current_scheme.as_ref().and_then(|s| s.me());
894            let our_digest = our_signer.and_then(|signer| {
895                acks_group
896                    .iter()
897                    .find(|ack| ack.epoch == self.epoch && ack.attestation.signer == signer)
898                    .map(|ack| ack.item.digest)
899            });
900
901            // If our_digest exists, delete everything from acks_group that doesn't match it
902            if let Some(digest) = our_digest {
903                acks_group.retain(|other| other.item.digest == digest);
904            }
905
906            // Create a new epoch map
907            let mut epoch_map = BTreeMap::new();
908            for ack in acks_group {
909                epoch_map
910                    .entry(ack.epoch)
911                    .or_insert_with(BTreeMap::new)
912                    .insert(ack.attestation.signer, ack);
913            }
914
915            // Insert as Verified if we have our own ack (meaning we verified the digest),
916            // otherwise as Unverified
917            match our_digest {
918                Some(digest) => {
919                    self.pending
920                        .insert(height, Pending::Verified(digest, epoch_map));
921
922                    // If we've already generated an ack and it isn't yet confirmed, mark for immediate rebroadcast
923                    self.rebroadcast_deadlines
924                        .put(height, self.context.current());
925                }
926                None => {
927                    self.pending.insert(height, Pending::Unverified(epoch_map));
928
929                    // Add to unverified heights
930                    unverified.push(height);
931                }
932            }
933        }
934
935        // After replay, ensure we have all heights from tip to next in pending or confirmed
936        // to handle the case where we restart and some heights have no acks yet
937        let next = self.next();
938        for height in Height::range(self.tip, next) {
939            // If we already have the height in pending or confirmed, skip
940            if self.pending.contains_key(&height) || self.confirmed.contains_key(&height) {
941                continue;
942            }
943
944            // Add missing height to pending
945            self.pending
946                .insert(height, Pending::Unverified(BTreeMap::new()));
947            unverified.push(height);
948        }
949        info!(tip = %self.tip, %next, ?unverified, "replayed journal");
950
951        (replay.finish().expect("replay failed"), unverified)
952    }
953
954    /// Appends an activity to the journal.
955    async fn record(mut self, activity: Activity<P::Scheme, D>) -> Self {
956        let height = match activity {
957            Activity::Ack(ref ack) => ack.item.height,
958            Activity::Certified(ref certificate) => certificate.item.height,
959            Activity::Tip(h) => h,
960        };
961        let section = self.get_journal_section(height);
962        rebind(&mut self.journal, |journal| {
963            journal.append(section, &activity)
964        })
965        .await
966        .expect("unable to append to journal");
967        self
968    }
969
970    /// Syncs (ensures all data is written to disk).
971    async fn sync(mut self, height: Height) -> Self {
972        let section = self.get_journal_section(height);
973        rebind(&mut self.journal, |journal| journal.sync(section))
974            .await
975            .expect("unable to sync journal");
976        self
977    }
978}
979
980#[cfg(test)]
981mod tests {
982    use super::*;
983    use crate::{
984        aggregation::{mocks, scheme::ed25519},
985        simplex::mocks::wrapped::{Behavior, Scheme as WrappedScheme},
986    };
987    use commonware_actor::Feedback;
988    use commonware_cryptography::{Hasher as _, Sha256, certificate::mocks::Fixture};
989    use commonware_p2p::Blocker;
990    use commonware_parallel::Sequential;
991    use commonware_runtime::{
992        Runner as _, Supervisor as _, buffer::paged::CacheRef, deterministic,
993    };
994    use commonware_utils::{NZU16, NZUsize, NonZeroDuration};
995
996    #[derive(Clone)]
997    struct NoopBlocker;
998
999    impl Blocker for NoopBlocker {
1000        type PublicKey = commonware_cryptography::ed25519::PublicKey;
1001
1002        fn block(&mut self, _peer: Self::PublicKey) -> Feedback {
1003            Feedback::Ok
1004        }
1005
1006        fn blocked(&mut self) -> commonware_p2p::BlockedSubscription<Self::PublicKey> {
1007            let (_, receiver) =
1008                commonware_utils::channel::ring::channel(commonware_utils::NZUsize!(1));
1009            receiver
1010        }
1011    }
1012
1013    #[test]
1014    #[should_panic(expected = "verified acknowledgement quorum must assemble")]
1015    fn assembly_failure_panics() {
1016        let runner = deterministic::Runner::timed(Duration::from_secs(10));
1017        runner.start(|mut context| async move {
1018            let epoch = Epoch::new(111);
1019            let Fixture {
1020                schemes, verifier, ..
1021            } = ed25519::fixture(&mut context, b"aggregation-recovery-failure", 4);
1022            let provider = mocks::Provider::new();
1023            assert!(provider.register(
1024                epoch,
1025                WrappedScheme::new(schemes[0].clone(), Behavior::RecoveryFailure),
1026            ));
1027            let (_, reporter) = mocks::Reporter::new(
1028                context.child("reporter"),
1029                WrappedScheme::new(verifier, Behavior::Honest),
1030            );
1031            let page_cache = CacheRef::from_pooler(&context, NZU16!(1024), NZUsize!(10));
1032            let mut engine = Engine::new(
1033                context.child("engine"),
1034                Config {
1035                    monitor: mocks::Monitor::new(epoch),
1036                    provider,
1037                    automaton: mocks::Application::new(mocks::Strategy::Correct),
1038                    reporter,
1039                    blocker: NoopBlocker,
1040                    priority_acks: false,
1041                    rebroadcast_timeout: NonZeroDuration::new_panic(Duration::from_secs(1)),
1042                    epoch_bounds: (EpochDelta::new(1), EpochDelta::new(1)),
1043                    window: NonZeroU64::new(1).unwrap(),
1044                    activity_timeout: HeightDelta::new(10),
1045                    journal_partition: "aggregation-recovery-failure".to_string(),
1046                    journal_write_buffer: NZUsize!(4096),
1047                    journal_replay_buffer: NZUsize!(4096),
1048                    journal_heights_per_section: NonZeroU64::new(6).unwrap(),
1049                    journal_compression: None,
1050                    journal_page_cache: page_cache,
1051                    strategy: Sequential,
1052                },
1053            );
1054
1055            let height = Height::new(0);
1056            let digest = Sha256::hash(&[b"payload"]);
1057            engine
1058                .pending
1059                .insert(height, Pending::Verified(digest, BTreeMap::new()));
1060
1061            for scheme in schemes.iter().take(3) {
1062                let scheme = WrappedScheme::new(scheme.clone(), Behavior::Honest);
1063                let ack = Ack::sign(&scheme, epoch, Item { height, digest }).unwrap();
1064                (engine, _) = engine.handle_ack(&ack).await;
1065            }
1066        });
1067    }
1068}