1use crate::dkg::{
143 ReshareBlock,
144 network::Directory,
145 types::{EpochInfo, Participants},
146};
147use commonware_consensus::{
148 marshal::core::Variant as MarshalVariant,
149 simplex::{scheme::Scheme, types::Finalization},
150 types::Epoch,
151};
152use commonware_cryptography::{
153 Digest, PublicKey, bls12381::primitives::variant::Variant as BlsVariant,
154};
155use commonware_utils::sequence::Unit;
156
157mod actor;
158pub use actor::{Actor, Config};
159
160mod mailbox;
161pub use mailbox::Mailbox;
162
163mod wire;
164
165#[derive(Clone, Debug, PartialEq, Eq)]
167pub struct Bootstrap<P: PublicKey, D: Directory<P> = Unit> {
168 pub epoch: Epoch,
173 pub participants: Participants<P>,
188 pub directory: D,
195}
196
197pub(crate) type ActorArtifact<S, V> = Artifact<
199 S,
200 <V as MarshalVariant>::Commitment,
201 <<V as MarshalVariant>::ApplicationBlock as ReshareBlock>::Variant,
202 <<V as MarshalVariant>::ApplicationBlock as ReshareBlock>::Directory,
203>;
204
205#[derive(Clone, Debug, PartialEq, Eq)]
207pub struct Artifact<S, D, V, Dir = Unit>
208where
209 S: Scheme<D>,
210 D: Digest,
211 V: BlsVariant,
212 Dir: Directory<S::PublicKey>,
213{
214 pub finalization: Option<Finalization<S, D>>,
218 pub info: EpochInfo<V, S::PublicKey, Dir>,
223 pub floor: Finalization<S, D>,
228}
229
230#[cfg(test)]
231mod tests {
232 use super::{Actor, Bootstrap, Config, wire};
233 use crate::dkg::{
234 probe::Artifact,
235 tests::mocks,
236 types::{EpochInfo, EpochOutcome, Payload},
237 };
238 use commonware_actor::Feedback;
239 use commonware_codec::Encode as _;
240 use commonware_consensus::{
241 Epochable as _, Heightable as _, Reporter as _,
242 marshal::{self, Start, resolver::p2p as marshal_resolver},
243 simplex::types::{Activity, Finalization, Finalize, Proposal},
244 types::{Epoch, Epocher as _, FixedEpocher, Height, Round, View, ViewDelta},
245 };
246 use commonware_cryptography::{
247 Digest as _, Digestible as _, Hasher as _,
248 bls12381::{
249 dkg::feldman_desmedt::deal,
250 primitives::sharing::{Mode, Sharing},
251 },
252 certificate::Verifier as _,
253 sha256::Sha256,
254 };
255 use commonware_macros::select;
256 use commonware_p2p::{
257 Receiver as _, Recipients, Sender as _,
258 simulated::{
259 Config as NetworkConfig, Link, Network, Oracle, Receiver as SimReceiver,
260 Sender as SimSender,
261 },
262 };
263 use commonware_parallel::Sequential;
264 use commonware_runtime::{
265 Clock as _, Handle, Quota, Runner as _, Supervisor as _, buffer::paged::CacheRef,
266 deterministic,
267 };
268 use commonware_storage::archive::immutable;
269 use commonware_utils::{
270 N3f1, NZDuration, NZU16, NZU32, NZU64, NZUsize, TestRng, channel::oneshot, non_empty,
271 ordered::Set, probability, sequence::Unit,
272 };
273 use std::{num::NonZeroU64, time::Duration};
274
275 const BACKFILL_CHANNEL: u64 = 0;
276 const BOUNDARY_CHANNEL: u64 = 1;
277 const TEST_QUOTA: Quota = Quota::per_second(NZU32!(1_000_000));
278 const BLOCKS_PER_EPOCH: NonZeroU64 = NZU64!(2);
279 const LINK: Link = Link {
280 latency: Duration::from_millis(1),
281 jitter: Duration::ZERO,
282 success_rate: probability!(1.0),
283 };
284
285 struct Harness {
286 participants: Vec<mocks::TestPublicKey>,
287 schemes: Vec<mocks::TestScheme>,
288 source_boundary_sender: SimSender<mocks::TestPublicKey, deterministic::Context>,
289 client_boundary_sender: SimSender<mocks::TestPublicKey, deterministic::Context>,
290 client_boundary_receiver: SimReceiver<mocks::TestPublicKey>,
291 backup_boundary_sender: SimSender<mocks::TestPublicKey, deterministic::Context>,
292 backup_boundary_receiver: SimReceiver<mocks::TestPublicKey>,
293 oracle: Oracle<mocks::TestPublicKey, deterministic::Context>,
294 joiner: super::Mailbox<mocks::TestScheme, mocks::TestMarshalVariant>,
295 boundary: mocks::TestBlock,
296 boundary_finalization: Finalization<mocks::TestScheme, mocks::TestDigest>,
297 boundary_sharing: Sharing<mocks::TestBlsVariant>,
298 _handles: Vec<Handle<()>>,
299 _network: Handle<()>,
300 }
301
302 impl Harness {
303 async fn start(context: &mut deterministic::Context) -> Self {
304 Self::start_with(context, true).await
305 }
306
307 async fn start_with(context: &mut deterministic::Context, source_serves: bool) -> Self {
308 let boundaries = if source_serves {
309 vec![Epoch::new(1)]
310 } else {
311 Vec::new()
312 };
313 Self::start_with_boundaries(context, boundaries).await
314 }
315
316 async fn start_with_boundaries(
317 context: &mut deterministic::Context,
318 source_boundaries: Vec<Epoch>,
319 ) -> Self {
320 Self::start_full(context, source_boundaries, Epoch::zero()).await
321 }
322
323 async fn start_full(
324 context: &mut deterministic::Context,
325 source_boundaries: Vec<Epoch>,
326 bootstrap_epoch: Epoch,
327 ) -> Self {
328 let fixture = mocks::scheme_fixture_n(context, 4);
329 let participants = fixture.participants.clone();
330
331 let (network, oracle) = Network::new_with_peers(
332 context.child("network"),
333 NetworkConfig {
334 max_size: 1024 * 1024,
335 max_peers_per_set: NZUsize!(participants.len()),
336 disconnect_on_block: true,
337 tracked_peer_sets: NZUsize!(1),
338 },
339 participants.clone(),
340 )
341 .await;
342 let network = network.start();
343 for from in &participants {
344 for to in &participants {
345 if from != to {
346 oracle
347 .add_link(from.clone(), to.clone(), LINK)
348 .await
349 .expect("failed to add link");
350 }
351 }
352 }
353
354 let (boundary, boundary_sharing) =
355 boundary_block(Epoch::new(1), participants[0].clone(), &participants);
356 let genesis = genesis_info(&participants);
357 let first_boundary_finalization =
358 boundary_finalization(Epoch::new(1), boundary.digest(), &fixture.schemes);
359 let source_boundaries = source_boundaries
360 .into_iter()
361 .map(|epoch| {
362 let (block, _) = boundary_block(epoch, participants[0].clone(), &participants);
363 let finalization =
364 boundary_finalization(epoch, block.digest(), &fixture.schemes);
365 (block, finalization)
366 })
367 .collect::<Vec<_>>();
368
369 let (source_marshal, marshal_handle) = start_marshal(
370 context.child("source_marshal"),
371 &oracle,
372 &fixture.participants,
373 &fixture.schemes,
374 0,
375 source_boundaries,
376 )
377 .await;
378
379 let source_control = oracle.control(participants[0].clone());
380 let source_boundaries = source_control
381 .register(BOUNDARY_CHANNEL, TEST_QUOTA)
382 .await
383 .expect("failed to register source boundaries");
384 let source_boundary_sender = source_boundaries.0.clone();
385 let (source_actor, source_mailbox) = Actor::new(Config {
386 context: context.child("source_probe"),
387 manager: oracle.manager(),
388 bootstrap: Bootstrap {
389 epoch: bootstrap_epoch,
390 participants: genesis.participants(),
391 directory: Unit,
392 },
393 verifier: fixture.schemes[0].clone(),
394 genesis: genesis.clone(),
395 strategy: Sequential,
396 blocker: oracle.control(participants[0].clone()),
397 blocks_per_epoch: BLOCKS_PER_EPOCH,
398 retry_timeout: NZDuration!(Duration::from_millis(500)),
399 mailbox_size: NZUsize!(16),
400 block_codec_config: (),
401 });
402 source_mailbox.attach(source_marshal.clone());
403 let source_handle = source_actor.start(source_boundaries);
404
405 let joiner_control = oracle.control(participants[1].clone());
406 let joiner_boundaries = joiner_control
407 .register(BOUNDARY_CHANNEL, TEST_QUOTA)
408 .await
409 .expect("failed to register joiner boundaries");
410 let (joiner_actor, joiner) = Actor::new(Config {
411 context: context.child("joiner_probe"),
412 manager: oracle.manager(),
413 bootstrap: Bootstrap {
414 epoch: bootstrap_epoch,
415 participants: genesis.participants(),
416 directory: Unit,
417 },
418 verifier: fixture.schemes[1].clone(),
419 genesis,
420 strategy: Sequential,
421 blocker: oracle.control(participants[1].clone()),
422 blocks_per_epoch: BLOCKS_PER_EPOCH,
423 retry_timeout: NZDuration!(Duration::from_millis(500)),
424 mailbox_size: NZUsize!(16),
425 block_codec_config: (),
426 });
427 let joiner_handle = joiner_actor.start(joiner_boundaries);
428 let client_boundaries = oracle
429 .control(participants[2].clone())
430 .register(BOUNDARY_CHANNEL, TEST_QUOTA)
431 .await
432 .expect("failed to register client boundaries");
433 let backup_boundaries = oracle
434 .control(participants[3].clone())
435 .register(BOUNDARY_CHANNEL, TEST_QUOTA)
436 .await
437 .expect("failed to register backup boundaries");
438
439 Self {
440 participants,
441 schemes: fixture.schemes,
442 source_boundary_sender,
443 client_boundary_sender: client_boundaries.0,
444 client_boundary_receiver: client_boundaries.1,
445 backup_boundary_sender: backup_boundaries.0,
446 backup_boundary_receiver: backup_boundaries.1,
447 oracle,
448 joiner,
449 boundary,
450 boundary_finalization: first_boundary_finalization,
451 boundary_sharing,
452 _handles: vec![marshal_handle, source_handle, joiner_handle],
453 _network: network,
454 }
455 }
456
457 fn latest_finalization(
459 &self,
460 epoch: Epoch,
461 digest: mocks::TestDigest,
462 ) -> Finalization<mocks::TestScheme, mocks::TestDigest> {
463 finalization(
464 Proposal::new(Round::new(epoch, View::new(2)), View::new(1), digest),
465 &self.schemes,
466 )
467 }
468
469 fn target_finalization(&self) -> Finalization<mocks::TestScheme, mocks::TestDigest> {
472 self.latest_finalization(Epoch::new(1), self.boundary.digest())
473 }
474
475 fn latest_response(
476 finalization: Finalization<mocks::TestScheme, mocks::TestDigest>,
477 ) -> Vec<u8> {
478 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::LatestResponse(
479 finalization,
480 )
481 .encode()
482 .to_vec()
483 }
484
485 fn reply_latest_from_client(
486 &mut self,
487 finalization: Finalization<mocks::TestScheme, mocks::TestDigest>,
488 ) {
489 self.client_boundary_sender.send(
490 Recipients::One(self.participants[1].clone()),
491 Self::latest_response(finalization),
492 false,
493 );
494 }
495
496 fn reply_latest_from_backup(
497 &mut self,
498 finalization: Finalization<mocks::TestScheme, mocks::TestDigest>,
499 ) {
500 self.backup_boundary_sender.send(
501 Recipients::One(self.participants[1].clone()),
502 Self::latest_response(finalization),
503 false,
504 );
505 }
506
507 fn complete_target_sample(&mut self) -> Finalization<mocks::TestScheme, mocks::TestDigest> {
510 let target = self.target_finalization();
511 self.reply_latest_from_client(target.clone());
512 self.reply_latest_from_backup(target.clone());
513 target
514 }
515
516 async fn next_request(receiver: &mut SimReceiver<mocks::TestPublicKey>) -> wire::Request {
517 let (_, message) = receiver.recv().await.expect("boundary request");
518 wire::read_request(message)
519 .expect("decode boundary request")
520 .expect("boundary request tag")
521 }
522
523 async fn expect_latest_request(receiver: &mut SimReceiver<mocks::TestPublicKey>) {
524 match Self::next_request(receiver).await {
525 wire::Request::Latest => {}
526 wire::Request::Boundary(_) | wire::Request::Block(_) => {
527 panic!("expected latest request")
528 }
529 }
530 }
531
532 async fn next_boundary_request(receiver: &mut SimReceiver<mocks::TestPublicKey>) -> Epoch {
533 match Self::next_request(receiver).await {
534 wire::Request::Boundary(epoch) => epoch,
535 wire::Request::Block(_) | wire::Request::Latest => {
536 panic!("expected finalization request")
537 }
538 }
539 }
540
541 async fn next_block_request(receiver: &mut SimReceiver<mocks::TestPublicKey>) -> Epoch {
542 match Self::next_request(receiver).await {
543 wire::Request::Block(epoch) => epoch,
544 wire::Request::Boundary(_) | wire::Request::Latest => {
545 panic!("expected block request")
546 }
547 }
548 }
549
550 async fn next_client_boundary_request(&mut self) -> Epoch {
551 Self::next_boundary_request(&mut self.client_boundary_receiver).await
552 }
553 }
554
555 async fn start_marshal(
556 context: deterministic::Context,
557 oracle: &Oracle<mocks::TestPublicKey, deterministic::Context>,
558 participants: &[mocks::TestPublicKey],
559 schemes: &[mocks::TestScheme],
560 index: usize,
561 boundaries: Vec<(
562 mocks::TestBlock,
563 Finalization<mocks::TestScheme, mocks::TestDigest>,
564 )>,
565 ) -> (mocks::TestMarshalMailbox, Handle<()>) {
566 let public_key = participants[index].clone();
567 let partition_prefix = format!("probe-node-{index}");
568 let page_cache = CacheRef::from_pooler(&context, NZU16!(1024), NZUsize!(16));
569 let control = oracle.control(public_key.clone());
570 let backfill = control
571 .register(BACKFILL_CHANNEL, TEST_QUOTA)
572 .await
573 .expect("failed to register marshal backfill");
574 let resolver = marshal_resolver::init(
575 context.child("marshal_resolver"),
576 marshal_resolver::Config {
577 public_key: public_key.clone(),
578 peer_provider: oracle.manager(),
579 blocker: oracle.control(public_key.clone()),
580 mailbox_size: NZUsize!(16),
581 timeout: Duration::from_secs(2),
582 fetch_retry_timeout: Duration::from_millis(100),
583 priority_requests: false,
584 priority_responses: false,
585 },
586 backfill,
587 );
588 let finalizations_by_height =
589 immutable::Archive::init(context.child("finalizations_by_height"), {
590 let _: () = mocks::TestScheme::certificate_codec_config_unbounded();
591 archive_config(
592 &partition_prefix,
593 "finalizations_by_height",
594 page_cache.clone(),
595 (),
596 )
597 })
598 .await
599 .expect("failed to initialize finalizations archive");
600 let finalized_blocks = immutable::Archive::init(
601 context.child("finalized_blocks"),
602 archive_config(
603 &partition_prefix,
604 "finalized_blocks",
605 page_cache.clone(),
606 (),
607 ),
608 )
609 .await
610 .expect("failed to initialize finalized blocks archive");
611
612 let (marshal_actor, mut marshal, _) = marshal::core::Actor::init(
613 context.child("marshal"),
614 finalizations_by_height,
615 finalized_blocks,
616 marshal::Config {
617 provider: mocks::TestProvider::new(schemes[index].clone()),
618 epocher: FixedEpocher::new(BLOCKS_PER_EPOCH),
619 start: Start::Genesis(mocks::genesis_block(public_key)),
620 partition_prefix,
621 mailbox_size: NZUsize!(16),
622 view_retention: ViewDelta::new(8),
623 prunable_items_per_section: NZU64!(10),
624 page_cache,
625 replay_buffer: NZUsize!(1024),
626 key_write_buffer: NZUsize!(1024),
627 value_write_buffer: NZUsize!(1024),
628 block_codec_config: (),
629 max_repair: NZUsize!(4),
630 max_pending_acks: NZUsize!(4),
631 strategy: Sequential,
632 },
633 )
634 .await;
635 let handle = marshal_actor.start_unbuffered(mocks::MarshalApplication::default(), resolver);
636
637 for (block, finalization) in boundaries {
638 assert!(marshal.certified(block.context().round, block).await);
639 assert_eq!(
640 marshal.report(Activity::Finalization(finalization)),
641 Feedback::Ok
642 );
643 }
644
645 (marshal, handle)
646 }
647
648 fn archive_config<C>(
649 prefix: &str,
650 name: &str,
651 page_cache: CacheRef,
652 codec_config: C,
653 ) -> immutable::Config<C> {
654 immutable::Config {
655 metadata_partition: format!("{prefix}-{name}-metadata"),
656 freezer_table_partition: format!("{prefix}-{name}-freezer-table"),
657 freezer_table_initial_size: 64,
658 freezer_table_resize_frequency: 10,
659 freezer_table_resize_chunk_size: 10,
660 freezer_key_partition: format!("{prefix}-{name}-freezer-key"),
661 freezer_key_page_cache: page_cache,
662 freezer_value_partition: format!("{prefix}-{name}-freezer-value"),
663 freezer_value_target_size: 1024,
664 freezer_value_compression: None,
665 ordinal_partition: format!("{prefix}-{name}-ordinal"),
666 items_per_section: NZU64!(10),
667 codec_config,
668 replay_buffer: NZUsize!(1024),
669 freezer_key_write_buffer: NZUsize!(1024),
670 freezer_value_write_buffer: NZUsize!(1024),
671 ordinal_write_buffer: NZUsize!(1024),
672 }
673 }
674
675 fn boundary_block(
676 epoch: Epoch,
677 leader: mocks::TestPublicKey,
678 participants: &[mocks::TestPublicKey],
679 ) -> (mocks::TestBlock, Sharing<mocks::TestBlsVariant>) {
680 let height = FixedEpocher::new(BLOCKS_PER_EPOCH)
681 .last(epoch.previous().expect("boundary epoch must be non-zero"))
682 .expect("test epoch must be supported");
683 let parent = if height == Height::zero() {
684 mocks::TestDigest::EMPTY
685 } else {
686 Sha256::hash(&[&height
687 .previous()
688 .expect("non-genesis height")
689 .get()
690 .to_be_bytes()])
691 };
692 let context = mocks::TestContext {
693 round: Round::new(
694 epoch.previous().expect("boundary epoch must be non-zero"),
695 View::new(1),
696 ),
697 leader,
698 parent: (View::zero(), parent),
699 };
700 let participants = Set::from_iter_dedup(participants.iter().cloned());
701 let (output, _) = deal::<mocks::TestBlsVariant, _, N3f1>(
702 TestRng::new(epoch.get()),
703 Mode::NonZeroCounter,
704 participants.clone(),
705 )
706 .expect("failed to create test DKG output");
707 let sharing = output.public().clone();
708 let block = mocks::TestBlock::new::<Sha256>(context, parent, height, epoch.get())
709 .with_payload::<Sha256, mocks::TestBlsVariant, mocks::TestSigner>(
710 NZU32!(16),
711 Payload::EpochInfo(EpochInfo {
712 outcome: EpochOutcome::Success,
713 epoch,
714 output,
715 players: participants.clone(),
716 next_players: participants,
717 directory: Unit,
718 }),
719 );
720 (block, sharing)
721 }
722
723 fn boundary_finalization(
724 epoch: Epoch,
725 digest: mocks::TestDigest,
726 schemes: &[mocks::TestScheme],
727 ) -> Finalization<mocks::TestScheme, mocks::TestDigest> {
728 finalization(
729 Proposal::new(
730 Round::new(
731 epoch.previous().expect("boundary epoch must be non-zero"),
732 View::new(1),
733 ),
734 View::zero(),
735 digest,
736 ),
737 schemes,
738 )
739 }
740
741 fn genesis_info(
742 participants: &[mocks::TestPublicKey],
743 ) -> EpochInfo<mocks::TestBlsVariant, mocks::TestPublicKey> {
744 let participants = Set::from_iter_dedup(participants.iter().cloned());
745 let (output, _) = deal::<mocks::TestBlsVariant, _, N3f1>(
746 TestRng::new(0),
747 Mode::NonZeroCounter,
748 participants.clone(),
749 )
750 .expect("failed to create test DKG output");
751 EpochInfo {
752 outcome: EpochOutcome::Success,
753 epoch: Epoch::zero(),
754 output,
755 players: participants.clone(),
756 next_players: participants,
757 directory: Unit,
758 }
759 }
760
761 fn finalization(
762 proposal: Proposal<mocks::TestDigest>,
763 schemes: &[mocks::TestScheme],
764 ) -> Finalization<mocks::TestScheme, mocks::TestDigest> {
765 let finalizes = schemes
766 .iter()
767 .map(|scheme| Finalize::sign(scheme, proposal.clone()).unwrap())
768 .collect::<Vec<_>>();
769 Finalization::from_finalizes(&schemes[0], non_empty![@finalizes.iter()], &Sequential)
770 .expect("finalization quorum")
771 }
772
773 fn assert_artifact(
774 artifact: Artifact<mocks::TestScheme, mocks::TestDigest, mocks::TestBlsVariant>,
775 expected_finalization: &Finalization<mocks::TestScheme, mocks::TestDigest>,
776 expected_sharing: &Sharing<mocks::TestBlsVariant>,
777 participants: &[mocks::TestPublicKey],
778 ) {
779 let expected_epoch = expected_finalization.epoch().next();
780 let participants = Set::from_iter_dedup(participants.iter().cloned());
781 assert_eq!(artifact.finalization.as_ref(), Some(expected_finalization));
782 assert_eq!(artifact.info.epoch, expected_epoch);
783 assert_eq!(artifact.info.output.public(), expected_sharing);
784 assert_eq!(artifact.info.output.players(), &participants);
785 assert_eq!(artifact.info.players, participants);
786 assert_eq!(artifact.floor.epoch(), expected_epoch);
787 }
788
789 #[test]
790 fn discovers_artifact_from_sample() {
791 let runner = deterministic::Runner::timed(Duration::from_secs(30));
792 runner.start(|mut context| async move {
793 let mut harness = Harness::start(&mut context).await;
794 let mut subscription = harness.joiner.subscribe();
795 let target = harness.complete_target_sample();
796
797 context.sleep(Duration::from_millis(100)).await;
798 let artifact = subscription.try_recv().expect("artifact resolved");
799 assert_eq!(artifact.floor, target);
800 assert_artifact(
801 artifact,
802 &harness.boundary_finalization,
803 &harness.boundary_sharing,
804 &harness.participants,
805 );
806 });
807 }
808
809 #[test]
810 fn waits_for_full_sample() {
811 let runner = deterministic::Runner::timed(Duration::from_secs(30));
812 runner.start(|mut context| async move {
813 let mut harness = Harness::start_with(&mut context, false).await;
814 let mut subscription = harness.joiner.subscribe();
815
816 let target = harness.target_finalization();
818 harness.reply_latest_from_client(target);
819
820 context.sleep(Duration::from_millis(100)).await;
821 assert!(
822 matches!(
823 subscription.try_recv(),
824 Err(oneshot::error::TryRecvError::Empty)
825 ),
826 "a single reply must not complete the sample"
827 );
828 });
829 }
830
831 #[test]
832 fn duplicate_latest_reply_is_ignored() {
833 let runner = deterministic::Runner::timed(Duration::from_secs(30));
834 runner.start(|mut context| async move {
835 let mut harness = Harness::start_with(&mut context, false).await;
836 let mut subscription = harness.joiner.subscribe();
837
838 let first = harness.latest_finalization(Epoch::new(1), Sha256::hash(&[b"first"]));
840 let second = harness.latest_finalization(Epoch::new(2), Sha256::hash(&[b"second"]));
841 harness.reply_latest_from_client(first);
842 harness.reply_latest_from_client(second);
843
844 context.sleep(Duration::from_millis(100)).await;
845 assert!(
846 matches!(
847 subscription.try_recv(),
848 Err(oneshot::error::TryRecvError::Empty)
849 ),
850 "duplicate replies must not inflate the sample"
851 );
852 let blocked = harness.oracle.blocked().await.unwrap();
853 assert!(
854 blocked.is_empty(),
855 "a duplicate reply must be ignored, not treated as a fault"
856 );
857 });
858 }
859
860 #[test]
861 fn sample_selects_highest_reply() {
862 let runner = deterministic::Runner::timed(Duration::from_secs(30));
863 runner.start(|mut context| async move {
864 let mut harness =
866 Harness::start_with_boundaries(&mut context, vec![Epoch::new(2)]).await;
867 let mut subscription = harness.joiner.subscribe();
868
869 let (newer_boundary, newer_sharing) = boundary_block(
870 Epoch::new(2),
871 harness.participants[0].clone(),
872 &harness.participants,
873 );
874 let stale = harness.latest_finalization(Epoch::new(1), Sha256::hash(&[b"stale"]));
875 let newest = harness.latest_finalization(Epoch::new(2), newer_boundary.digest());
876 harness.reply_latest_from_client(stale);
877 harness.reply_latest_from_backup(newest.clone());
878
879 context.sleep(Duration::from_millis(100)).await;
880 let artifact = subscription.try_recv().expect("artifact resolved");
881 assert_eq!(artifact.floor, newest);
882 assert_artifact(
883 artifact,
884 &boundary_finalization(Epoch::new(2), newer_boundary.digest(), &harness.schemes),
885 &newer_sharing,
886 &harness.participants,
887 );
888 });
889 }
890
891 #[test]
892 fn genesis_floor_resolves_locally() {
893 let runner = deterministic::Runner::timed(Duration::from_secs(30));
894 runner.start(|mut context| async move {
895 let mut harness = Harness::start_with(&mut context, false).await;
896 let mut subscription = harness.joiner.subscribe();
897
898 let floor =
902 harness.latest_finalization(Epoch::zero(), Sha256::hash(&[b"genesis floor"]));
903 harness.reply_latest_from_client(floor.clone());
904 harness.reply_latest_from_backup(floor.clone());
905
906 context.sleep(Duration::from_millis(100)).await;
907 let artifact = subscription.try_recv().expect("genesis resolved");
908 assert_eq!(artifact.info.epoch, Epoch::zero());
909 assert!(artifact.finalization.is_none());
910 assert_eq!(artifact.info, genesis_info(&harness.participants));
911 assert_eq!(artifact.floor, floor);
912 });
913 }
914
915 #[test]
916 fn resolicits_when_sample_incomplete() {
917 let runner = deterministic::Runner::timed(Duration::from_secs(30));
918 runner.start(|mut context| async move {
919 let mut harness = Harness::start_with(&mut context, false).await;
920 let _subscription = harness.joiner.subscribe();
921
922 Harness::expect_latest_request(&mut harness.client_boundary_receiver).await;
925 Harness::expect_latest_request(&mut harness.client_boundary_receiver).await;
926 });
927 }
928
929 #[test]
930 fn ignores_latest_reply_below_bootstrap_epoch() {
931 let runner = deterministic::Runner::timed(Duration::from_secs(30));
932 runner.start(|mut context| async move {
933 let mut harness =
934 Harness::start_full(&mut context, vec![Epoch::new(1)], Epoch::new(1)).await;
935 let mut subscription = harness.joiner.subscribe();
936
937 let stale = harness.latest_finalization(Epoch::zero(), Sha256::hash(&[b"stale"]));
940 harness.reply_latest_from_client(stale.clone());
941 harness.reply_latest_from_backup(stale);
942 context.sleep(Duration::from_millis(100)).await;
943 assert!(
944 matches!(
945 subscription.try_recv(),
946 Err(oneshot::error::TryRecvError::Empty)
947 ),
948 "below-bootstrap replies must not complete the sample"
949 );
950 let blocked = harness.oracle.blocked().await.unwrap();
951 assert!(blocked.is_empty(), "stale replies must not block peers");
952
953 let target = harness.complete_target_sample();
955 context.sleep(Duration::from_millis(100)).await;
956 let artifact = subscription.try_recv().expect("artifact resolved");
957 assert_eq!(artifact.floor, target);
958 });
959 }
960
961 #[test]
962 fn invalid_latest_reply_blocks_peer() {
963 let runner = deterministic::Runner::timed(Duration::from_secs(30));
964 runner.start(|mut context| async move {
965 let mut harness = Harness::start_with(&mut context, false).await;
966 let _subscription = harness.joiner.subscribe();
967
968 let foreign = mocks::scheme_fixture_n(&mut context, 4);
971 let invalid = finalization(
972 Proposal::new(
973 Round::new(Epoch::new(1), View::new(2)),
974 View::new(1),
975 Sha256::hash(&[b"foreign"]),
976 ),
977 &foreign.schemes,
978 );
979 harness.reply_latest_from_client(invalid);
980
981 context.sleep(Duration::from_millis(100)).await;
982 let blocked = harness.oracle.blocked().await.unwrap();
983 assert!(
984 blocked.contains(&(
985 harness.participants[1].clone(),
986 harness.participants[2].clone(),
987 )),
988 "joiner should block the sender of an invalid reply"
989 );
990 });
991 }
992
993 #[test]
994 fn fetches_boundary_block_from_one_responder() {
995 let runner = deterministic::Runner::timed(Duration::from_secs(30));
996 runner.start(|mut context| async move {
997 let mut harness = Harness::start_with(&mut context, false).await;
998 let mut subscription = harness.joiner.subscribe();
999
1000 Harness::expect_latest_request(&mut harness.client_boundary_receiver).await;
1001 Harness::expect_latest_request(&mut harness.backup_boundary_receiver).await;
1002 harness.complete_target_sample();
1003
1004 assert_eq!(harness.next_client_boundary_request().await, Epoch::new(1));
1005 assert_eq!(
1006 Harness::next_boundary_request(&mut harness.backup_boundary_receiver).await,
1007 Epoch::new(1)
1008 );
1009
1010 harness.client_boundary_sender.send(
1011 Recipients::One(harness.participants[1].clone()),
1012 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::BoundaryResponse(
1013 harness.boundary_finalization.clone(),
1014 )
1015 .encode()
1016 .to_vec(),
1017 false,
1018 );
1019 assert_eq!(
1020 Harness::next_block_request(&mut harness.client_boundary_receiver).await,
1021 Epoch::new(1)
1022 );
1023
1024 select! {
1025 _ = harness.backup_boundary_receiver.recv() => {
1026 panic!("block request sent to an unselected responder");
1027 },
1028 _ = context.sleep(Duration::from_millis(100)) => {},
1029 }
1030
1031 harness.client_boundary_sender.send(
1032 Recipients::One(harness.participants[1].clone()),
1033 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::BlockResponse {
1034 epoch: Epoch::new(1),
1035 block: harness.boundary.clone(),
1036 }
1037 .encode()
1038 .to_vec(),
1039 false,
1040 );
1041 context.sleep(Duration::from_millis(100)).await;
1042
1043 let artifact = subscription.try_recv().expect("artifact resolved");
1044 assert_artifact(
1045 artifact,
1046 &harness.boundary_finalization,
1047 &harness.boundary_sharing,
1048 &harness.participants,
1049 );
1050 });
1051 }
1052
1053 #[test]
1054 fn retries_boundary_block_with_another_responder() {
1055 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1056 runner.start(|mut context| async move {
1057 let mut harness = Harness::start_with(&mut context, false).await;
1058 let mut subscription = harness.joiner.subscribe();
1059
1060 Harness::expect_latest_request(&mut harness.client_boundary_receiver).await;
1061 Harness::expect_latest_request(&mut harness.backup_boundary_receiver).await;
1062 harness.complete_target_sample();
1063
1064 assert_eq!(harness.next_client_boundary_request().await, Epoch::new(1));
1065 assert_eq!(
1066 Harness::next_boundary_request(&mut harness.backup_boundary_receiver).await,
1067 Epoch::new(1)
1068 );
1069
1070 harness.client_boundary_sender.send(
1071 Recipients::One(harness.participants[1].clone()),
1072 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::BoundaryResponse(
1073 harness.boundary_finalization.clone(),
1074 )
1075 .encode()
1076 .to_vec(),
1077 false,
1078 );
1079 assert_eq!(
1080 Harness::next_block_request(&mut harness.client_boundary_receiver).await,
1081 Epoch::new(1)
1082 );
1083
1084 harness.backup_boundary_sender.send(
1085 Recipients::One(harness.participants[1].clone()),
1086 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::BoundaryResponse(
1087 harness.boundary_finalization.clone(),
1088 )
1089 .encode()
1090 .to_vec(),
1091 false,
1092 );
1093 assert_eq!(
1094 Harness::next_block_request(&mut harness.backup_boundary_receiver).await,
1095 Epoch::new(1)
1096 );
1097
1098 harness.backup_boundary_sender.send(
1099 Recipients::One(harness.participants[1].clone()),
1100 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::BlockResponse {
1101 epoch: Epoch::new(1),
1102 block: harness.boundary.clone(),
1103 }
1104 .encode()
1105 .to_vec(),
1106 false,
1107 );
1108 context.sleep(Duration::from_millis(100)).await;
1109
1110 let artifact = subscription.try_recv().expect("artifact resolved");
1111 assert_artifact(
1112 artifact,
1113 &harness.boundary_finalization,
1114 &harness.boundary_sharing,
1115 &harness.participants,
1116 );
1117 let blocked = harness.oracle.blocked().await.unwrap();
1118 assert!(blocked.is_empty(), "silent responders must not be blocked");
1119 });
1120 }
1121
1122 #[test]
1123 fn invalid_boundary_block_tries_another_responder_immediately() {
1124 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1125 runner.start(|mut context| async move {
1126 let mut harness = Harness::start_with(&mut context, false).await;
1127 let mut subscription = harness.joiner.subscribe();
1128
1129 Harness::expect_latest_request(&mut harness.client_boundary_receiver).await;
1130 Harness::expect_latest_request(&mut harness.backup_boundary_receiver).await;
1131 harness.complete_target_sample();
1132
1133 assert_eq!(harness.next_client_boundary_request().await, Epoch::new(1));
1134 assert_eq!(
1135 Harness::next_boundary_request(&mut harness.backup_boundary_receiver).await,
1136 Epoch::new(1)
1137 );
1138
1139 harness.client_boundary_sender.send(
1140 Recipients::One(harness.participants[1].clone()),
1141 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::BoundaryResponse(
1142 harness.boundary_finalization.clone(),
1143 )
1144 .encode()
1145 .to_vec(),
1146 false,
1147 );
1148 assert_eq!(
1149 Harness::next_block_request(&mut harness.client_boundary_receiver).await,
1150 Epoch::new(1)
1151 );
1152
1153 harness.backup_boundary_sender.send(
1154 Recipients::One(harness.participants[1].clone()),
1155 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::BoundaryResponse(
1156 harness.boundary_finalization.clone(),
1157 )
1158 .encode()
1159 .to_vec(),
1160 false,
1161 );
1162 context.sleep(Duration::from_millis(100)).await;
1163
1164 let (wrong_block, _) = boundary_block(
1165 Epoch::new(2),
1166 harness.participants[0].clone(),
1167 &harness.participants,
1168 );
1169 harness.client_boundary_sender.send(
1170 Recipients::One(harness.participants[1].clone()),
1171 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::BlockResponse {
1172 epoch: Epoch::new(1),
1173 block: wrong_block,
1174 }
1175 .encode()
1176 .to_vec(),
1177 false,
1178 );
1179
1180 select! {
1181 epoch = Harness::next_block_request(&mut harness.backup_boundary_receiver) => {
1182 assert_eq!(epoch, Epoch::new(1));
1183 },
1184 _ = context.sleep(Duration::from_millis(100)) => {
1185 panic!("invalid block did not trigger immediate failover");
1186 },
1187 }
1188
1189 harness.backup_boundary_sender.send(
1190 Recipients::One(harness.participants[1].clone()),
1191 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::BlockResponse {
1192 epoch: Epoch::new(1),
1193 block: harness.boundary.clone(),
1194 }
1195 .encode()
1196 .to_vec(),
1197 false,
1198 );
1199 context.sleep(Duration::from_millis(100)).await;
1200
1201 let artifact = subscription.try_recv().expect("artifact resolved");
1202 assert_artifact(
1203 artifact,
1204 &harness.boundary_finalization,
1205 &harness.boundary_sharing,
1206 &harness.participants,
1207 );
1208 let blocked = harness.oracle.blocked().await.unwrap();
1209 assert!(blocked.contains(&(
1210 harness.participants[1].clone(),
1211 harness.participants[2].clone()
1212 )));
1213 });
1214 }
1215
1216 #[test]
1217 fn rebroadcasts_finalization_request_when_unanswered() {
1218 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1219 runner.start(|mut context| async move {
1220 let mut harness = Harness::start_with(&mut context, false).await;
1223 let mut subscription = harness.joiner.subscribe();
1224
1225 Harness::expect_latest_request(&mut harness.client_boundary_receiver).await;
1226 harness.complete_target_sample();
1227
1228 assert_eq!(harness.next_client_boundary_request().await, Epoch::new(1));
1230 assert!(matches!(
1231 subscription.try_recv(),
1232 Err(oneshot::error::TryRecvError::Empty)
1233 ));
1234
1235 assert_eq!(harness.next_client_boundary_request().await, Epoch::new(1));
1237 assert!(matches!(
1238 subscription.try_recv(),
1239 Err(oneshot::error::TryRecvError::Empty)
1240 ));
1241 });
1242 }
1243
1244 #[test]
1245 fn terminal_epoch_boundary_response_does_not_panic() {
1246 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1247 runner.start(|mut context| async move {
1248 let mut harness = Harness::start_with_boundaries(&mut context, Vec::new()).await;
1249 let mut subscription = harness.joiner.subscribe();
1250
1251 Harness::expect_latest_request(&mut harness.client_boundary_receiver).await;
1252 harness.complete_target_sample();
1253 assert_eq!(harness.next_client_boundary_request().await, Epoch::new(1));
1254
1255 let terminal_finalization = finalization(
1256 Proposal::new(
1257 Round::new(Epoch::new(u64::MAX), View::new(1)),
1258 View::zero(),
1259 harness.boundary.digest(),
1260 ),
1261 &harness.schemes,
1262 );
1263 let message =
1264 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::BoundaryResponse(
1265 terminal_finalization,
1266 )
1267 .encode()
1268 .to_vec();
1269 let decoded = wire::read_response::<mocks::TestScheme, mocks::TestMarshalVariant, _>(
1270 message.as_slice(),
1271 &harness.schemes[2].certificate_codec_config(),
1272 )
1273 .expect("terminal response decoded")
1274 .expect("terminal response tag");
1275 let wire::Response::Boundary(decoded) = decoded else {
1276 panic!("expected finalization response");
1277 };
1278 assert_eq!(decoded.epoch(), Epoch::new(u64::MAX));
1279
1280 harness.source_boundary_sender.send(
1281 Recipients::One(harness.participants[1].clone()),
1282 message,
1283 false,
1284 );
1285 context.sleep(Duration::from_millis(100)).await;
1286
1287 let blocked = harness.oracle.blocked().await.unwrap();
1288 assert!(
1289 blocked.contains(&(
1290 harness.participants[1].clone(),
1291 harness.participants[0].clone()
1292 )),
1293 "terminal-epoch response should block source peer"
1294 );
1295 assert!(matches!(
1296 subscription.try_recv(),
1297 Err(oneshot::error::TryRecvError::Empty)
1298 ));
1299 });
1300 }
1301
1302 #[test]
1303 fn does_not_solicit_without_subscriber() {
1304 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1305 runner.start(|mut context| async move {
1306 let mut harness = Harness::start(&mut context).await;
1307 select! {
1308 _ = harness.client_boundary_receiver.recv() => {
1309 panic!("solicitation sent before any subscriber");
1310 },
1311 _ = context.sleep(Duration::from_millis(700)) => {},
1312 }
1313 });
1314 }
1315
1316 #[test]
1317 fn late_subscriber_receives_cached_artifact() {
1318 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1319 runner.start(|mut context| async move {
1320 let mut harness = Harness::start(&mut context).await;
1321 let mut first = harness.joiner.subscribe();
1322 harness.complete_target_sample();
1323
1324 context.sleep(Duration::from_millis(100)).await;
1325 let artifact = first.try_recv().expect("artifact resolved");
1326 assert_artifact(
1327 artifact,
1328 &harness.boundary_finalization,
1329 &harness.boundary_sharing,
1330 &harness.participants,
1331 );
1332
1333 let mut second = harness.joiner.subscribe();
1334 context.sleep(Duration::from_millis(10)).await;
1335 let artifact = second.try_recv().expect("cached artifact resolved");
1336 assert_artifact(
1337 artifact,
1338 &harness.boundary_finalization,
1339 &harness.boundary_sharing,
1340 &harness.participants,
1341 );
1342 });
1343 }
1344
1345 #[test]
1346 fn serving_answers_latest_request_from_marshal() {
1347 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1348 runner.start(|mut context| async move {
1349 let mut harness = Harness::start(&mut context).await;
1350 harness.client_boundary_sender.send(
1351 Recipients::One(harness.participants[0].clone()),
1352 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::LatestRequest
1353 .encode()
1354 .to_vec(),
1355 false,
1356 );
1357
1358 let (_peer, message) = harness
1359 .client_boundary_receiver
1360 .recv()
1361 .await
1362 .expect("latest response delivered");
1363 let response = wire::read_response::<mocks::TestScheme, mocks::TestMarshalVariant, _>(
1364 message,
1365 &harness.schemes[2].certificate_codec_config(),
1366 )
1367 .expect("latest response decoded")
1368 .expect("latest response");
1369 let wire::Response::Latest(finalization) = response else {
1370 panic!("expected latest response");
1371 };
1372 assert_eq!(finalization, harness.boundary_finalization);
1373 });
1374 }
1375
1376 #[test]
1377 fn serving_answers_finalization_and_block_requests_from_marshal() {
1378 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1379 runner.start(|mut context| async move {
1380 let mut harness = Harness::start(&mut context).await;
1381 harness.client_boundary_sender.send(
1382 Recipients::One(harness.participants[0].clone()),
1383 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::BoundaryRequest(
1384 Epoch::new(1),
1385 )
1386 .encode()
1387 .to_vec(),
1388 false,
1389 );
1390
1391 let (_peer, message) = harness
1392 .client_boundary_receiver
1393 .recv()
1394 .await
1395 .expect("boundary response delivered");
1396 let response = wire::read_response::<mocks::TestScheme, mocks::TestMarshalVariant, _>(
1397 message,
1398 &harness.schemes[2].certificate_codec_config(),
1399 )
1400 .expect("boundary response decoded")
1401 .expect("boundary response");
1402 let wire::Response::Boundary(finalization) = response else {
1403 panic!("expected finalization response");
1404 };
1405 let commitment = finalization.proposal.payload;
1406 assert_eq!(finalization, harness.boundary_finalization);
1407
1408 harness.client_boundary_sender.send(
1409 Recipients::One(harness.participants[0].clone()),
1410 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::BlockRequest(
1411 Epoch::new(1),
1412 )
1413 .encode()
1414 .to_vec(),
1415 false,
1416 );
1417 let (_peer, message) = harness
1418 .client_boundary_receiver
1419 .recv()
1420 .await
1421 .expect("block response delivered");
1422 let response = wire::read_response::<mocks::TestScheme, mocks::TestMarshalVariant, _>(
1423 message,
1424 &harness.schemes[2].certificate_codec_config(),
1425 )
1426 .expect("block response decoded")
1427 .expect("block response");
1428 let wire::Response::Block { epoch, body } = response else {
1429 panic!("expected block response");
1430 };
1431 assert_eq!(epoch, Epoch::new(1));
1432 let block = wire::read_block::<mocks::TestMarshalVariant>(body, commitment, &())
1433 .expect("boundary block decoded");
1434
1435 assert_eq!(block.digest(), harness.boundary.digest());
1436 assert_eq!(block.height(), Height::new(1));
1437 });
1438 }
1439
1440 #[test]
1441 fn serving_ignores_epoch_without_boundary() {
1442 let runner = deterministic::Runner::timed(Duration::from_secs(30));
1443 runner.start(|mut context| async move {
1444 let mut harness = Harness::start(&mut context).await;
1445 harness.client_boundary_sender.send(
1446 Recipients::One(harness.participants[0].clone()),
1447 wire::Message::<mocks::TestScheme, mocks::TestMarshalVariant>::BoundaryRequest(
1448 Epoch::zero(),
1449 )
1450 .encode()
1451 .to_vec(),
1452 false,
1453 );
1454
1455 select! {
1456 _ = harness.client_boundary_receiver.recv() => {
1457 panic!("boundary response delivered");
1458 },
1459 _ = context.sleep(Duration::from_millis(100)) => {},
1460 };
1461 });
1462 }
1463
1464 #[test]
1465 fn address_failure_stops_probe_and_drops_subscriber() {
1466 let runner = deterministic::Runner::timed(Duration::from_secs(5));
1467 runner.start(|mut context| async move {
1468 let fixture = mocks::scheme_fixture_n(&mut context, 1);
1469 let participants = fixture.participants.clone();
1470 let (network, oracle) = Network::new_with_peers(
1471 context.child("network"),
1472 NetworkConfig {
1473 max_size: 1024 * 1024,
1474 max_peers_per_set: NZUsize!(participants.len()),
1475 disconnect_on_block: true,
1476 tracked_peer_sets: NZUsize!(1),
1477 },
1478 participants.clone(),
1479 )
1480 .await;
1481 let network = network.start();
1482 let control = oracle.control(participants[0].clone());
1483 let boundaries = control
1484 .register(BOUNDARY_CHANNEL, TEST_QUOTA)
1485 .await
1486 .expect("failed to register boundaries");
1487 let genesis = genesis_info(&participants);
1488 let (actor, mailbox): (
1489 _,
1490 super::Mailbox<mocks::TestScheme, mocks::TestMarshalVariant>,
1491 ) = Actor::new(Config {
1492 context: context.child("probe"),
1493 manager: mocks::FailingManager(oracle.manager()),
1494 bootstrap: Bootstrap {
1495 epoch: Epoch::zero(),
1496 participants: genesis.participants(),
1497 directory: Unit,
1498 },
1499 verifier: fixture.schemes[0].clone(),
1500 genesis,
1501 strategy: Sequential,
1502 blocker: control,
1503 blocks_per_epoch: BLOCKS_PER_EPOCH,
1504 retry_timeout: NZDuration!(Duration::from_millis(500)),
1505 mailbox_size: NZUsize!(16),
1506 block_codec_config: (),
1507 });
1508 let handle = actor.start(boundaries);
1509
1510 assert!(mailbox.subscribe().await.is_err());
1511 handle.await.expect("probe should stop cleanly");
1512 network.abort();
1513 });
1514 }
1515}