1use crate::dkg::{
2 ReshareBlock,
3 network::Directory,
4 reshare::{EpochInfoResponse, Mailbox},
5 types::Payload,
6};
7use commonware_consensus::{
8 Application as ConsensusApplication, CertifiableBlock,
9 marshal::ancestry::Ancestry,
10 types::{EpochPhase, Epocher as _, FixedEpocher, Height},
11};
12use commonware_cryptography::{Signer, bls12381::primitives::variant::Variant};
13use commonware_runtime::{Clock, Metrics, Spawner, telemetry::traces::TracedExt as _};
14use commonware_utils::sequence::Unit;
15use rand_core::Rng;
16use std::{future, num::NonZeroU64};
17use tracing::{debug, field};
18
19pub struct Input<Upstream, V: Variant, C: Signer, D: Directory<C::PublicKey> = Unit> {
25 pub upstream: Upstream,
27
28 pub payload: Option<Payload<V, C, D>>,
30}
31
32pub struct Application<A, B, V, C>
55where
56 B: ReshareBlock<Variant = V, Signer = C>,
57 V: Variant,
58 C: Signer,
59{
60 inner: A,
61 reshare: Mailbox<B, V, C>,
62 epocher: FixedEpocher,
63}
64
65impl<A, B, V, C> Application<A, B, V, C>
66where
67 B: ReshareBlock<Variant = V, Signer = C>,
68 V: Variant,
69 C: Signer,
70{
71 pub const fn new(inner: A, reshare: Mailbox<B, V, C>, blocks_per_epoch: NonZeroU64) -> Self {
74 Self {
75 inner,
76 reshare,
77 epocher: FixedEpocher::new(blocks_per_epoch),
78 }
79 }
80
81 fn final_block(&self, height: Height) -> bool {
82 self.epocher
83 .containing(height)
84 .is_some_and(|info| info.last() == height)
85 }
86
87 fn phase(&self, height: Height) -> Option<EpochPhase> {
88 self.epocher.containing(height).map(|info| info.phase())
89 }
90}
91
92impl<A, B, V, C> Clone for Application<A, B, V, C>
93where
94 A: Clone,
95 B: ReshareBlock<Variant = V, Signer = C>,
96 V: Variant,
97 C: Signer,
98{
99 fn clone(&self) -> Self {
100 Self {
101 inner: self.inner.clone(),
102 reshare: self.reshare.clone(),
103 epocher: self.epocher.clone(),
104 }
105 }
106}
107
108impl<A, E, B, V, C, I> ConsensusApplication<E> for Application<A, B, V, C>
109where
110 E: Rng + Spawner + Metrics + Clock,
111 A: ConsensusApplication<E, Block = B, Input = Input<I, V, C, B::Directory>>,
112 A::Context: Send,
113 B: ReshareBlock<Variant = V, Signer = C> + CertifiableBlock + Clone,
114 V: Variant,
115 C: Signer,
116 I: Send,
117{
118 type SigningScheme = A::SigningScheme;
119 type Context = A::Context;
120 type Block = A::Block;
121 type Input = I;
122
123 #[tracing::instrument(
124 name = "dkg.reshare.application.propose",
125 level = "info",
126 skip_all,
127 fields(
128 height = field::Empty,
129 phase = field::Empty,
130 has_payload = field::Empty
131 )
132 )]
133 async fn propose(
134 &mut self,
135 context: (E, Self::Context),
136 ancestry: impl Ancestry<Self::Block>,
137 input: Self::Input,
138 ) -> Option<Self::Block> {
139 let Some(parent) = ancestry.peek() else {
142 debug!("proposal rejected: missing parent ancestry");
143 return None;
144 };
145 let height = parent.height().next();
146 let phase = self.phase(height);
147 let span = tracing::Span::current();
148 span.record("height", height.traced());
149 span.record("phase", field::debug(phase));
150
151 let (payload, log_reservation) = if self.final_block(height) {
152 match self.reshare.epoch_info(ancestry.clone()).await {
153 EpochInfoResponse::Available(payload) => (payload, None),
154 EpochInfoResponse::Pending => {
155 debug!("proposal skipped: final block epoch info is not ready");
156 return None;
157 }
158 EpochInfoResponse::Following => {
159 debug!("proposal skipped: follower has no final block epoch info");
160 return None;
161 }
162 EpochInfoResponse::Unavailable => {
163 debug!("proposal skipped: final block epoch info is unavailable");
164 return None;
165 }
166 }
167 } else if matches!(phase, Some(EpochPhase::Midpoint | EpochPhase::Late)) {
168 let mut reservation = self.reshare.next_log(height).await;
169 let payload = reservation
170 .as_mut()
171 .and_then(|reservation| reservation.take_payload());
172 (payload, reservation)
173 } else {
174 (None, None)
175 };
176 span.record("has_payload", payload.is_some());
177 let proposed = self
178 .inner
179 .propose(
180 context,
181 ancestry,
182 Input {
183 upstream: input,
184 payload,
185 },
186 )
187 .await;
188 if proposed.is_some()
189 && let Some(reservation) = log_reservation
190 {
191 reservation.included();
192 }
193 proposed
194 }
195
196 #[tracing::instrument(
197 name = "dkg.reshare.application.verify",
198 level = "info",
199 skip_all,
200 fields(
201 height = field::Empty,
202 phase = field::Empty,
203 has_payload = field::Empty
204 )
205 )]
206 async fn verify(
207 &mut self,
208 context: (E, Self::Context),
209 ancestry: impl Ancestry<Self::Block>,
210 ) -> bool {
211 let Some(tip) = ancestry.peek().cloned() else {
212 return self.inner.verify(context, ancestry).await;
213 };
214 let height = tip.height();
215 let phase = self.phase(height);
216 let tip_payload = tip.payload();
217 let span = tracing::Span::current();
218 span.record("height", height.traced());
219 span.record("phase", field::debug(phase));
220 span.record("has_payload", tip_payload.is_some());
221
222 if self.final_block(height) {
223 match self.reshare.epoch_info(ancestry.clone()).await {
224 EpochInfoResponse::Available(derived) => {
225 if derived != tip_payload {
226 debug!("verification rejected: final block payload mismatch");
227 return false;
228 }
229 }
230 response @ (EpochInfoResponse::Pending | EpochInfoResponse::Following) => {
231 debug!(
235 following = matches!(response, EpochInfoResponse::Following),
236 "verification pending: final block epoch info cannot be derived locally"
237 );
238 future::pending::<()>().await;
239 unreachable!("pending future must not resolve");
240 }
241 EpochInfoResponse::Unavailable => {
242 debug!("verification rejected: final block epoch info is unavailable");
243 return false;
244 }
245 }
246 } else if matches!(phase, Some(EpochPhase::Early)) && tip_payload.is_some() {
247 debug!("verification rejected: early block carried reshare payload");
250 return false;
251 }
252 self.inner.verify(context, ancestry).await
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use crate::dkg::{
260 reshare::{LogReservation, Message},
261 tests::mocks::{self, TestBlock, TestBlsVariant, TestContext, TestScheme},
262 types::{EpochInfo, EpochOutcome},
263 };
264 use commonware_actor::mailbox;
265 use commonware_consensus::{
266 CertifiableBlock, Heightable,
267 marshal::ancestry,
268 types::{Epoch, Height, Round, View},
269 };
270 use commonware_cryptography::{
271 Digestible, Signer,
272 bls12381::{
273 dkg::feldman_desmedt::deal,
274 primitives::{sharing::Mode, variant::MinPk},
275 },
276 ed25519::{PrivateKey, PublicKey},
277 sha256::Sha256,
278 };
279 use commonware_runtime::{Clock, Metrics, Runner, Spawner, Supervisor, deterministic};
280 use commonware_utils::{
281 Acknowledgement, N3f1, NZU32, NZU64, NZUsize, TestRng, channel::oneshot, ordered::Set,
282 sync::Mutex,
283 };
284 use futures::{
285 FutureExt, StreamExt,
286 future::{Either, select},
287 pin_mut,
288 };
289 use rand_core::Rng;
290 use std::{sync::Arc, time::Duration};
291
292 type TestPayload = Payload<TestBlsVariant, PrivateKey>;
293 type TestResponse = EpochInfoResponse<TestBlsVariant, PrivateKey>;
294 type TestWrapper = Application<RecordingApp, TestBlock, TestBlsVariant, PrivateKey>;
295
296 impl CertifiableBlock for TestBlock {
297 type Context = TestContext;
298
299 fn context(&self) -> Self::Context {
300 self.context().clone()
301 }
302 }
303
304 #[derive(Clone, Copy, PartialEq, Eq)]
305 enum ProposalBehavior {
306 Accept,
307 Reject,
308 Pending,
309 }
310
311 #[derive(Clone)]
312 struct RecordingApp {
313 proposed: Arc<Mutex<Vec<Option<TestPayload>>>>,
314 proposal_behavior: ProposalBehavior,
315 proposal_entered: Arc<Mutex<Option<oneshot::Sender<()>>>>,
316 verify_count: Arc<Mutex<usize>>,
317 verify_result: bool,
318 }
319
320 impl RecordingApp {
321 fn accepting() -> Self {
322 Self {
323 proposed: Arc::new(Mutex::new(Vec::new())),
324 proposal_behavior: ProposalBehavior::Accept,
325 proposal_entered: Arc::new(Mutex::new(None)),
326 verify_count: Arc::new(Mutex::new(0)),
327 verify_result: true,
328 }
329 }
330
331 fn rejecting() -> Self {
332 Self {
333 proposal_behavior: ProposalBehavior::Reject,
334 ..Self::accepting()
335 }
336 }
337
338 fn pending(proposal_entered: oneshot::Sender<()>) -> Self {
339 Self {
340 proposal_behavior: ProposalBehavior::Pending,
341 proposal_entered: Arc::new(Mutex::new(Some(proposal_entered))),
342 ..Self::accepting()
343 }
344 }
345
346 fn proposed(&self) -> Vec<Option<TestPayload>> {
347 self.proposed.lock().clone()
348 }
349
350 fn verify_count(&self) -> usize {
351 *self.verify_count.lock()
352 }
353 }
354
355 impl<E> ConsensusApplication<E> for RecordingApp
356 where
357 E: Rng + Spawner + Metrics + Clock,
358 {
359 type SigningScheme = TestScheme;
360 type Context = TestContext;
361 type Block = TestBlock;
362 type Input = Input<(), TestBlsVariant, PrivateKey>;
363
364 async fn propose(
365 &mut self,
366 (_, context): (E, Self::Context),
367 ancestry: impl Ancestry<Self::Block>,
368 input: Self::Input,
369 ) -> Option<Self::Block> {
370 let parent = ancestry.peek()?.clone();
371 self.proposed.lock().push(input.payload.clone());
372 if let Some(entered) = self.proposal_entered.lock().take() {
373 let _ = entered.send(());
374 }
375
376 if self.proposal_behavior == ProposalBehavior::Reject {
377 return None;
378 }
379
380 if self.proposal_behavior == ProposalBehavior::Pending {
381 future::pending().await
382 }
383
384 let block =
385 TestBlock::new::<Sha256>(context, parent.digest(), parent.height().next(), 0);
386 Some(match input.payload {
387 Some(payload) => {
388 block.with_payload::<Sha256, TestBlsVariant, PrivateKey>(NZU32!(16), payload)
389 }
390 None => block,
391 })
392 }
393
394 async fn verify(&mut self, _: (E, Self::Context), _: impl Ancestry<Self::Block>) -> bool {
395 *self.verify_count.lock() += 1;
396 self.verify_result
397 }
398 }
399
400 fn wrapper(context: &deterministic::Context, response: TestResponse) -> TestWrapper {
401 wrapper_with_inner(context, response, RecordingApp::accepting())
402 }
403
404 fn wrapper_with_inner(
405 context: &deterministic::Context,
406 response: TestResponse,
407 inner: RecordingApp,
408 ) -> TestWrapper {
409 let (sender, mut receiver) = mailbox::new::<Message<TestBlock, TestBlsVariant, PrivateKey>>(
410 context.child("mailbox"),
411 NZUsize!(1),
412 );
413 context.child("fake_actor").spawn(|_| async move {
414 let Some(Message::EpochInfo {
415 response: reply, ..
416 }) = receiver.recv().await
417 else {
418 return;
419 };
420 let _ = reply.send(response);
421 });
422
423 Application::new(inner, Mailbox::new(sender), NZU64!(2))
424 }
425
426 fn log_wrapper(
427 context: &deterministic::Context,
428 payload: TestPayload,
429 inner: RecordingApp,
430 ) -> (TestWrapper, oneshot::Receiver<Height>) {
431 let (sender, mut receiver) = mailbox::new::<Message<TestBlock, TestBlsVariant, PrivateKey>>(
432 context.child("mailbox"),
433 NZUsize!(4),
434 );
435 let (release_tx, release_rx) = oneshot::channel();
436 context.child("fake_actor").spawn(|_| async move {
437 let mut served_at = None;
438 let mut release_tx = Some(release_tx);
439 while let Some(message) = receiver.recv().await {
440 match message {
441 Message::NextLog {
442 height,
443 release,
444 response,
445 ..
446 } => {
447 let reservation = served_at.is_none().then(|| {
448 served_at = Some(height);
449 LogReservation::new(height, payload.clone(), release)
450 });
451 let _ = response.send(reservation);
452 }
453 Message::ReleaseLog { height } => {
454 if served_at == Some(height) {
455 served_at = None;
456 }
457 if let Some(release_tx) = release_tx.take() {
458 let _ = release_tx.send(height);
459 }
460 }
461 Message::EpochInfo { response, .. } => {
462 let _ = response.send(EpochInfoResponse::Unavailable);
463 }
464 Message::Finalized { response, .. } => {
465 response.acknowledge();
466 }
467 }
468 }
469 });
470
471 (
472 Application::new(inner, Mailbox::new(sender), NZU64!(4)),
473 release_rx,
474 )
475 }
476
477 fn leader() -> PrivateKey {
478 PrivateKey::from_seed(99)
479 }
480
481 fn block_context(parent: &TestBlock, view: u64) -> TestContext {
482 TestContext {
483 round: Round::new(Epoch::zero(), View::new(view)),
484 leader: leader().public_key(),
485 parent: (View::zero(), parent.digest()),
486 }
487 }
488
489 fn signers() -> Vec<PrivateKey> {
490 (0..4).map(PrivateKey::from_seed).collect()
491 }
492
493 fn players() -> Set<PublicKey> {
494 Set::from_iter_dedup(signers().iter().map(Signer::public_key))
495 }
496
497 fn epoch_payload(seed: u64) -> TestPayload {
498 let (output, _) =
499 deal::<MinPk, _, N3f1>(TestRng::new(seed), Mode::NonZeroCounter, players())
500 .expect("trusted deal");
501 Payload::EpochInfo(EpochInfo {
502 outcome: EpochOutcome::Success,
503 epoch: Epoch::new(1),
504 output,
505 players: Set::default(),
506 next_players: Set::default(),
507 directory: Unit,
508 })
509 }
510
511 fn final_block(parent: &TestBlock, payload: Option<TestPayload>) -> Arc<TestBlock> {
512 let block = TestBlock::new::<Sha256>(
513 block_context(parent, 1),
514 parent.digest(),
515 parent.height().next(),
516 0,
517 );
518 let block = match payload {
519 Some(payload) => {
520 block.with_payload::<Sha256, TestBlsVariant, PrivateKey>(NZU32!(16), payload)
521 }
522 None => block,
523 };
524 Arc::new(block)
525 }
526
527 fn midpoint_parent() -> TestBlock {
528 let genesis = mocks::genesis_block(leader().public_key());
529 TestBlock::new::<Sha256>(
530 block_context(&genesis, 1),
531 genesis.digest(),
532 genesis.height().next(),
533 0,
534 )
535 }
536
537 #[test]
538 fn proposal_none_releases_dealer_log_reservation() {
539 let executor = deterministic::Runner::default();
540 executor.start(|context| async move {
541 let parent = midpoint_parent();
542 let payload = epoch_payload(10);
543 let inner = RecordingApp::rejecting();
544 let (mut app, release_rx) = log_wrapper(&context, payload.clone(), inner.clone());
545
546 let proposed = app
547 .propose(
548 (context.child("app"), block_context(&parent, 2)),
549 ancestry::from_iter([Arc::new(parent.clone())]),
550 (),
551 )
552 .await;
553 assert!(proposed.is_none());
554 assert_eq!(
555 release_rx.await.expect("reservation should be released"),
556 Height::new(2)
557 );
558
559 let proposed = app
560 .propose(
561 (context.child("app_retry"), block_context(&parent, 3)),
562 ancestry::from_iter([Arc::new(parent)]),
563 (),
564 )
565 .await;
566 assert!(proposed.is_none());
567 let proposed_payloads = inner.proposed();
568 assert_eq!(proposed_payloads.len(), 2);
569 assert!(proposed_payloads[0] == Some(payload.clone()));
570 assert!(proposed_payloads[1] == Some(payload));
571 });
572 }
573
574 #[test]
575 fn dropped_proposal_future_releases_dealer_log_reservation() {
576 let executor = deterministic::Runner::default();
577 executor.start(|context| async move {
578 let parent = midpoint_parent();
579 let payload = epoch_payload(11);
580 let (entered_tx, entered_rx) = oneshot::channel();
581 let mut entered_rx = entered_rx;
582 let inner = RecordingApp::pending(entered_tx);
583 let (sender, mut receiver) = mailbox::new::<
584 Message<TestBlock, TestBlsVariant, PrivateKey>,
585 >(context.child("mailbox"), NZUsize!(4));
586 let mut app = Application::new(inner.clone(), Mailbox::new(sender), NZU64!(4));
587
588 let mut propose = Box::pin(app.propose(
589 (context.child("app"), block_context(&parent, 2)),
590 ancestry::from_iter([Arc::new(parent)]),
591 (),
592 ));
593 assert!(propose.as_mut().now_or_never().is_none());
594
595 let Some(Message::NextLog {
596 height,
597 release,
598 response,
599 ..
600 }) = receiver.recv().await
601 else {
602 panic!("proposal should request a dealer log");
603 };
604 assert_eq!(height, Height::new(2));
605 let reservation = LogReservation::new(height, payload.clone(), release);
606 assert!(
607 response.send(Some(reservation)).is_ok(),
608 "proposal should still be waiting for log"
609 );
610
611 assert!(propose.as_mut().now_or_never().is_none());
612 entered_rx
613 .try_recv()
614 .expect("proposal should enter inner application");
615 drop(propose);
616
617 let Some(Message::ReleaseLog { height }) = receiver.recv().await else {
618 panic!("dropped proposal should release reservation");
619 };
620 assert_eq!(height, Height::new(2));
621
622 if let Ok(Message::ReleaseLog { height }) = receiver.try_recv() {
623 panic!("reservation released more than once at {height:?}");
624 }
625 let proposed_payloads = inner.proposed();
626 assert_eq!(proposed_payloads.len(), 1);
627 assert!(proposed_payloads[0] == Some(payload));
628 });
629 }
630
631 #[test]
632 fn successful_proposal_keeps_dealer_log_reserved() {
633 let executor = deterministic::Runner::default();
634 executor.start(|context| async move {
635 let parent = midpoint_parent();
636 let payload = epoch_payload(12);
637 let inner = RecordingApp::accepting();
638 let (mut app, release_rx) = log_wrapper(&context, payload.clone(), inner.clone());
639
640 let proposed = app
641 .propose(
642 (context.child("app"), block_context(&parent, 2)),
643 ancestry::from_iter([Arc::new(parent)]),
644 (),
645 )
646 .await
647 .expect("proposal should be built");
648 assert!(proposed.payload() == Some(payload.clone()));
649 let proposed_payloads = inner.proposed();
650 assert_eq!(proposed_payloads.len(), 1);
651 assert!(proposed_payloads[0] == Some(payload));
652
653 let timeout = context.sleep(Duration::from_millis(1));
654 pin_mut!(release_rx);
655 pin_mut!(timeout);
656 match select(release_rx, timeout).await {
657 Either::Left((released, _)) => {
658 panic!("successful proposal released reservation: {released:?}");
659 }
660 Either::Right(((), _)) => {}
661 }
662 });
663 }
664
665 #[test]
666 fn proposal_skips_unavailable_final_epoch_info() {
667 let executor = deterministic::Runner::default();
668 executor.start(|context| async move {
669 let parent = mocks::genesis_block(leader().public_key());
670 let mut app = wrapper(&context, EpochInfoResponse::Unavailable);
671 let inner = app.inner.clone();
672
673 let proposed = app
674 .propose(
675 (context.child("app"), block_context(&parent, 1)),
676 ancestry::from_iter([Arc::new(parent)]),
677 (),
678 )
679 .await;
680
681 assert!(proposed.is_none());
682 assert!(inner.proposed().is_empty());
683 });
684 }
685
686 #[test]
687 fn proposal_preserves_legitimate_no_artifact_final_block() {
688 let executor = deterministic::Runner::default();
689 executor.start(|context| async move {
690 let parent = mocks::genesis_block(leader().public_key());
691 let mut app = wrapper(&context, EpochInfoResponse::Available(None));
692 let inner = app.inner.clone();
693
694 let proposed = app
695 .propose(
696 (context.child("app"), block_context(&parent, 1)),
697 ancestry::from_iter([Arc::new(parent)]),
698 (),
699 )
700 .await
701 .expect("proposal should be built");
702
703 assert!(proposed.payload().is_none());
704 let proposed_payloads = inner.proposed();
705 assert_eq!(proposed_payloads.len(), 1);
706 assert!(proposed_payloads[0].is_none());
707 });
708 }
709
710 #[test]
711 fn proposal_includes_available_final_epoch_info() {
712 let executor = deterministic::Runner::default();
713 executor.start(|context| async move {
714 let parent = mocks::genesis_block(leader().public_key());
715 let payload = epoch_payload(7);
716 let mut app = wrapper(
717 &context,
718 EpochInfoResponse::Available(Some(payload.clone())),
719 );
720 let inner = app.inner.clone();
721
722 let proposed = app
723 .propose(
724 (context.child("app"), block_context(&parent, 1)),
725 ancestry::from_iter([Arc::new(parent)]),
726 (),
727 )
728 .await
729 .expect("proposal should be built");
730
731 assert!(proposed.payload() == Some(payload.clone()));
732 let proposed_payloads = inner.proposed();
733 assert_eq!(proposed_payloads.len(), 1);
734 assert!(proposed_payloads[0] == Some(payload));
735 });
736 }
737
738 #[test]
739 fn verification_rejects_unavailable_final_epoch_info() {
740 let executor = deterministic::Runner::default();
741 executor.start(|context| async move {
742 let parent = Arc::new(mocks::genesis_block(leader().public_key()));
743 let tip = final_block(&parent, Some(epoch_payload(1)));
744 let mut app = wrapper(&context, EpochInfoResponse::Unavailable);
745 let inner = app.inner.clone();
746
747 let verified = app
748 .verify(
749 (context.child("app"), block_context(&parent, 1)),
750 ancestry::from_iter([tip, parent]),
751 )
752 .await;
753
754 assert!(!verified);
755 assert_eq!(inner.verify_count(), 0);
756 });
757 }
758
759 #[test]
760 fn verification_stays_pending_without_final_epoch_info() {
761 let executor = deterministic::Runner::default();
762 executor.start(|context| async move {
763 for response in [EpochInfoResponse::Following, EpochInfoResponse::Pending] {
764 let parent = Arc::new(mocks::genesis_block(leader().public_key()));
765 let tip = final_block(&parent, Some(epoch_payload(1)));
766 let expected_tip = tip.digest();
767 let expected_parent = parent.digest();
768 let inner = RecordingApp::accepting();
769 let (sender, mut receiver) = mailbox::new::<
770 Message<TestBlock, TestBlsVariant, PrivateKey>,
771 >(
772 context.child("mailbox"), NZUsize!(1)
773 );
774 let mut app = Application::new(inner.clone(), Mailbox::new(sender), NZU64!(2));
775 let mut verify = Box::pin(app.verify(
776 (context.child("app"), block_context(&parent, 1)),
777 ancestry::from_iter([tip, parent]),
778 ));
779
780 assert!(verify.as_mut().now_or_never().is_none());
781 let Some(Message::EpochInfo {
782 mut ancestry,
783 response: reply,
784 ..
785 }) = receiver.recv().await
786 else {
787 panic!("verification should request final epoch info");
788 };
789 assert_eq!(
790 ancestry
791 .next()
792 .await
793 .expect("verification ancestry should retain the candidate")
794 .digest(),
795 expected_tip
796 );
797 assert_eq!(
798 ancestry
799 .next()
800 .await
801 .expect("candidate should be followed by its parent")
802 .digest(),
803 expected_parent
804 );
805 assert!(reply.send(response).is_ok());
806 assert!(
807 verify.as_mut().now_or_never().is_none(),
808 "verification resolved without final epoch info"
809 );
810 assert_eq!(inner.verify_count(), 0);
811 }
812 });
813 }
814
815 #[test]
816 fn verification_accepts_legitimate_no_artifact_final_block() {
817 let executor = deterministic::Runner::default();
818 executor.start(|context| async move {
819 let parent = Arc::new(mocks::genesis_block(leader().public_key()));
820 let tip = final_block(&parent, None);
821 let mut app = wrapper(&context, EpochInfoResponse::Available(None));
822 let inner = app.inner.clone();
823
824 let verified = app
825 .verify(
826 (context.child("app"), block_context(&parent, 1)),
827 ancestry::from_iter([tip, parent]),
828 )
829 .await;
830
831 assert!(verified);
832 assert_eq!(inner.verify_count(), 1);
833 });
834 }
835
836 #[test]
837 fn verification_accepts_equal_final_epoch_info() {
838 let executor = deterministic::Runner::default();
839 executor.start(|context| async move {
840 let parent = Arc::new(mocks::genesis_block(leader().public_key()));
841 let payload = epoch_payload(2);
842 let tip = final_block(&parent, Some(payload.clone()));
843 let mut app = wrapper(&context, EpochInfoResponse::Available(Some(payload)));
844 let inner = app.inner.clone();
845
846 let verified = app
847 .verify(
848 (context.child("app"), block_context(&parent, 1)),
849 ancestry::from_iter([tip, parent]),
850 )
851 .await;
852
853 assert!(verified);
854 assert_eq!(inner.verify_count(), 1);
855 });
856 }
857
858 #[test]
859 fn verification_rejects_mismatched_final_epoch_info() {
860 let executor = deterministic::Runner::default();
861 executor.start(|context| async move {
862 let parent = Arc::new(mocks::genesis_block(leader().public_key()));
863 let tip = final_block(&parent, Some(epoch_payload(3)));
864 let response = EpochInfoResponse::Available(Some(epoch_payload(4)));
865 let mut app = wrapper(&context, response);
866 let inner = app.inner.clone();
867
868 let verified = app
869 .verify(
870 (context.child("app"), block_context(&parent, 1)),
871 ancestry::from_iter([tip, parent]),
872 )
873 .await;
874
875 assert!(!verified);
876 assert_eq!(inner.verify_count(), 0);
877 });
878 }
879}