1use 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
47enum Pending<S: Scheme, D: Digest> {
49 Unverified(BTreeMap<Epoch, BTreeMap<Participant, Ack<S, D>>>),
52
53 Verified(D, BTreeMap<Epoch, BTreeMap<Participant, Ack<S, D>>>),
55}
56
57struct DigestRequest<D: Digest> {
60 height: Height,
62
63 result: Result<D, Error>,
65
66 timer: histogram::Timer,
68}
69
70pub 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 context: ContextCell<E>,
83 automaton: A,
84 monitor: M,
85 provider: P,
86 reporter: Z,
87 blocker: B,
88 strategy: T,
89
90 epoch_bounds: (EpochDelta, EpochDelta),
99
100 window: HeightDelta,
102
103 activity_timeout: HeightDelta,
105
106 digest_requests: FuturesPool<'static, DigestRequest<D>>,
109
110 epoch: Epoch,
113
114 tip: Height,
116
117 safe_tip: SafeTip<<P::Scheme as Verifier>::PublicKey>,
119
120 pending: BTreeMap<Height, Pending<P::Scheme, D>>,
124
125 confirmed: BTreeMap<Height, Certificate<P::Scheme, D>>,
127
128 rebroadcast_timeout: Duration,
131
132 rebroadcast_deadlines: PrioritySet<Height, SystemTime>,
134
135 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 priority_acks: bool,
148
149 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 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 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 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 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 let (latest, mut epoch_updates) = self.monitor.subscribe().await;
243 self.epoch = latest;
244
245 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 for height in unverified_heights {
261 trace!(%height, "requesting digest for unverified height from replay");
262 self.get_digest(height);
263 }
264
265 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 let next = self.next();
278
279 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 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 Some(epoch) = epoch_updates.recv() else {
302 error!("epoch subscription failed");
303 break;
304 } => {
305 debug!(current = %self.epoch, new = %epoch, "refresh epoch");
307 assert!(epoch >= self.epoch);
308 self.epoch = epoch;
309
310 let scheme = self
312 .scheme(self.epoch)
313 .expect("current epoch scheme must exist");
314 self.safe_tip.reconcile(scheme.participants());
315
316 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 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 msg = receiver.recv() => {
353 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 if self.safe_tip.update(sender.clone(), tip).is_some() {
372 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 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 let accepted;
396 (self, accepted) = self.handle_ack(&ack).await;
397 if !accepted {
398 guard.set(Status::Failure);
399 continue;
400 }
401
402 debug!(?sender, epoch = %ack.epoch, height = %ack.item.height, "ack");
404 guard.set(Status::Success);
405 },
406
407 _ = rebroadcast => {
409 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 if let Some(journal) = self.journal.take() {
421 journal.sync_all().await.expect("unable to sync journal");
422 }
423 }
424
425 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 if !matches!(self.pending.get(&height), Some(Pending::Unverified(_))) {
439 debug!(%height, "digest height not pending");
440 return self;
441 };
442
443 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 for epoch_acks in acks.values() {
454 for epoch_ack in epoch_acks.values() {
455 if epoch_ack.item.digest != digest {
457 continue;
458 }
459
460 (self, _) = self.handle_ack(epoch_ack).await;
462 }
463 if self.confirmed.contains_key(&height) {
465 break;
466 }
467 }
468
469 let signed;
471 (self, signed) = self.sign_ack(height, digest).await;
472 let Some(ack) = signed else {
473 return self;
474 };
475
476 self.rebroadcast_deadlines
478 .put(height, self.context.current() + self.rebroadcast_timeout);
479
480 (self, _) = self.handle_ack(&ack).await;
482
483 self.broadcast(ack, sender);
485
486 self
487 }
488
489 async fn handle_ack(mut self, ack: &Ack<P::Scheme, D>) -> (Self, bool) {
495 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 let acks_by_epoch = match self.pending.get_mut(&ack.item.height) {
508 None => {
509 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 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 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 let filtered = acks
534 .values()
535 .filter(|a| a.item.digest == ack.item.digest)
536 .collect::<Vec<_>>();
537 if filtered.len() >= quorum {
538 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 async fn handle_certificate(mut self, certificate: Certificate<P::Scheme, D>) -> Self {
552 let height = certificate.item.height;
554 if self.confirmed.contains_key(&height) {
555 return self;
556 }
557
558 self.confirmed.insert(height, certificate.clone());
560
561 let certified = Activity::Certified(certificate);
563 self = self.record(certified.clone()).await.sync(height).await;
564 self.reporter.report(certified);
565
566 if height == self.tip {
568 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 new_tip > self.tip {
576 self = self.fast_forward_tip(new_tip).await;
577 }
578 }
579
580 self
581 }
582
583 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 return self;
595 };
596 let digest = *digest;
597
598 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 self.rebroadcast_deadlines
626 .put(height, self.context.current() + self.rebroadcast_timeout);
627
628 self.broadcast(ack, sender);
630
631 self
632 }
633
634 fn validate_ack(
640 &mut self,
641 ack: &Ack<P::Scheme, D>,
642 sender: &<P::Scheme as Verifier>::PublicKey,
643 ) -> Result<(), Error> {
644 {
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 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 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 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 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 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 if !ack.verify(self.context.as_mut(), &*scheme, &self.strategy) {
706 return Err(Error::InvalidAckSignature);
707 }
708
709 Ok(())
710 }
711
712 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 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 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 self = self
753 .record(Activity::Ack(ack.clone()))
754 .await
755 .sync(height)
756 .await;
757
758 (self, Some(ack))
759 }
760
761 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 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 async fn fast_forward_tip(mut self, tip: Height) -> Self {
799 assert!(tip > self.tip);
800
801 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 self = self.record(Activity::Tip(tip)).await.sync(tip).await;
810 self.reporter.report(Activity::Tip(tip));
811
812 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 self.tip = tip;
820
821 self
822 }
823
824 const fn get_journal_section(&self, height: Height) -> u64 {
828 height.get() / self.journal_heights_per_section.get()
829 }
830
831 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 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 self.tip = tip;
867 let activity_threshold = tip.saturating_sub(self.activity_timeout);
868
869 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 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 let mut unverified = Vec::new();
890 for (height, mut acks_group) in acks_by_height {
891 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 let Some(digest) = our_digest {
903 acks_group.retain(|other| other.item.digest == digest);
904 }
905
906 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 match our_digest {
918 Some(digest) => {
919 self.pending
920 .insert(height, Pending::Verified(digest, epoch_map));
921
922 self.rebroadcast_deadlines
924 .put(height, self.context.current());
925 }
926 None => {
927 self.pending.insert(height, Pending::Unverified(epoch_map));
928
929 unverified.push(height);
931 }
932 }
933 }
934
935 let next = self.next();
938 for height in Height::range(self.tip, next) {
939 if self.pending.contains_key(&height) || self.confirmed.contains_key(&height) {
941 continue;
942 }
943
944 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 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 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}