1use super::{
141 mailbox::{Mailbox, Message},
142 metrics::ShardMetrics,
143};
144use crate::{
145 Block, CertifiableBlock, Heightable,
146 marshal::{
147 coding::{
148 types::{CodedBlock, Shard},
149 validation::{ReconstructionError as InvariantError, validate_reconstruction},
150 },
151 core::Retirement,
152 },
153 types::{Epoch, Round, coding::Commitment},
154};
155use commonware_actor::mailbox;
156use commonware_codec::{Decode, Error as CodecError, Read};
157use commonware_coding::{Config as CodingConfig, Scheme as CodingScheme};
158use commonware_cryptography::{
159 Committable, Digestible, Hasher, PublicKey,
160 certificate::{Provider, Scheme as CertificateScheme},
161};
162use commonware_macros::select_loop;
163use commonware_p2p::{
164 Blocker, Provider as PeerProvider, Receiver, Recipients, Sender,
165 utils::codec::{WrappedBackgroundReceiver, WrappedSender},
166};
167use commonware_parallel::Strategy;
168use commonware_runtime::{
169 BufferPooler, Clock, ContextCell, Handle, Metrics, Spawner, spawn_cell,
170 telemetry::metrics::HistogramExt,
171};
172use commonware_utils::{
173 bitmap::BitMap,
174 channel::{fallible::OneshotExt, oneshot},
175 ordered::{Quorum, Set},
176};
177use rand_core::Rng;
178use std::{
179 collections::{BTreeMap, VecDeque, btree_map::Entry},
180 num::NonZeroUsize,
181 sync::Arc,
182};
183use thiserror::Error;
184use tracing::{debug, warn};
185
186#[derive(Debug, Error)]
188pub enum Error<C: CodingScheme> {
189 #[error(transparent)]
191 Coding(C::Error),
192
193 #[error(transparent)]
195 Codec(#[from] CodecError),
196
197 #[error("block digest mismatch: reconstructed block does not match commitment digest")]
199 DigestMismatch,
200
201 #[error("block config mismatch: reconstructed config does not match commitment config")]
203 ConfigMismatch,
204
205 #[error("block context mismatch: reconstructed context does not match commitment context")]
207 ContextMismatch,
208}
209
210#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
211enum BlockSubscriptionKey<K, D> {
212 Commitment(K),
213 Digest(D),
214}
215
216pub struct Config<P, S, X, D, C, H, B, T>
218where
219 P: PublicKey,
220 S: Provider<Scope = Epoch>,
221 X: Blocker<PublicKey = P>,
222 D: PeerProvider<PublicKey = P>,
223 C: CodingScheme,
224 H: Hasher,
225 B: CertifiableBlock,
226 T: Strategy,
227{
228 pub scheme_provider: S,
230
231 pub blocker: X,
233
234 pub shard_codec_cfg: <Shard<B, C, H> as Read>::Cfg,
236
237 pub block_codec_cfg: B::Cfg,
239
240 pub strategy: T,
242
243 pub mailbox_size: NonZeroUsize,
245
246 pub peer_buffer_size: NonZeroUsize,
257
258 pub background_channel_capacity: NonZeroUsize,
264
265 pub peer_provider: D,
271}
272
273enum CommitmentPhase<B, C, H, P>
275where
276 B: Block,
277 C: CodingScheme,
278 H: Hasher,
279 P: PublicKey,
280{
281 Reconstructing(ReconstructionState<P, B, C, H>),
283 Cached {
286 block: Arc<CodedBlock<B, C, H>>,
287 reconstruction: Option<ReconstructionState<P, B, C, H>>,
288 },
289}
290
291struct CommitmentRecord<B, C, H, P>
297where
298 B: Block,
299 C: CodingScheme,
300 H: Hasher,
301 P: PublicKey,
302{
303 round: Round,
304 phase: CommitmentPhase<B, C, H, P>,
305 proposed: bool,
306}
307
308type CommitmentRecords<B, C, H, P> = BTreeMap<Commitment<B, C, H>, CommitmentRecord<B, C, H, P>>;
311
312#[derive(Clone, Copy, Debug, Eq, PartialEq)]
314enum CommitmentStatus {
315 Absent,
317 Reconstructing,
319 Cached,
321}
322
323#[derive(Clone, Copy)]
325enum RetirementReason {
326 Exact,
328 Floor,
330}
331
332impl<B, C, H, P> CommitmentRecord<B, C, H, P>
333where
334 B: Block,
335 C: CodingScheme,
336 H: Hasher,
337 P: PublicKey,
338{
339 const fn reconstructing(round: Round, reconstruction: ReconstructionState<P, B, C, H>) -> Self {
341 Self {
342 round,
343 phase: CommitmentPhase::Reconstructing(reconstruction),
344 proposed: false,
345 }
346 }
347
348 const fn cached(round: Round, block: Arc<CodedBlock<B, C, H>>) -> Self {
350 Self {
351 round,
352 phase: CommitmentPhase::Cached {
353 block,
354 reconstruction: None,
355 },
356 proposed: false,
357 }
358 }
359
360 const fn round(&self) -> Round {
362 self.round
363 }
364
365 fn validate_epoch(&self, round: Round) -> Result<(), Epoch> {
367 let existing_epoch = self.round.epoch();
368 if existing_epoch != round.epoch() {
369 return Err(existing_epoch);
370 }
371 Ok(())
372 }
373
374 fn observe(&mut self, round: Round) -> Result<(), Epoch> {
376 self.validate_epoch(round)?;
377 self.round = self.round.max(round);
378 Ok(())
379 }
380
381 const fn block(&self) -> Option<&Arc<CodedBlock<B, C, H>>> {
383 match &self.phase {
384 CommitmentPhase::Reconstructing(_) => None,
385 CommitmentPhase::Cached { block, .. } => Some(block),
386 }
387 }
388
389 const fn reconstruction(&self) -> Option<&ReconstructionState<P, B, C, H>> {
391 match &self.phase {
392 CommitmentPhase::Reconstructing(reconstruction) => Some(reconstruction),
393 CommitmentPhase::Cached { reconstruction, .. } => reconstruction.as_ref(),
394 }
395 }
396
397 const fn reconstruction_mut(&mut self) -> Option<&mut ReconstructionState<P, B, C, H>> {
399 match &mut self.phase {
400 CommitmentPhase::Reconstructing(reconstruction) => Some(reconstruction),
401 CommitmentPhase::Cached { reconstruction, .. } => reconstruction.as_mut(),
402 }
403 }
404
405 fn is_assigned_shard_ready(&self) -> bool {
407 self.proposed
408 || self
409 .reconstruction()
410 .is_some_and(ReconstructionState::is_assigned_shard_verified)
411 }
412
413 const fn mark_proposed(&mut self) {
415 self.proposed = true;
416 }
417
418 fn install_block(&mut self, block: Arc<CodedBlock<B, C, H>>) -> Arc<CodedBlock<B, C, H>> {
422 let previous = std::mem::replace(
423 &mut self.phase,
424 CommitmentPhase::Cached {
425 block: Arc::clone(&block),
426 reconstruction: None,
427 },
428 );
429 self.phase = match previous {
430 CommitmentPhase::Reconstructing(reconstruction) => CommitmentPhase::Cached {
431 block: Arc::clone(&block),
432 reconstruction: Some(reconstruction),
433 },
434 cached @ CommitmentPhase::Cached { .. } => cached,
435 };
436 self.block()
437 .cloned()
438 .expect("installing a block must leave a cached phase")
439 }
440}
441
442pub struct Engine<E, S, X, D, C, H, B, P, T>
447where
448 E: BufferPooler + Rng + Spawner + Metrics + Clock,
449 S: Provider<Scope = Epoch>,
450 S::Scheme: CertificateScheme<PublicKey = P>,
451 X: Blocker,
452 D: PeerProvider<PublicKey = P>,
453 C: CodingScheme,
454 H: Hasher,
455 B: CertifiableBlock,
456 P: PublicKey,
457 T: Strategy,
458{
459 context: ContextCell<E>,
461
462 mailbox: mailbox::Receiver<Message<B, C, H, P>>,
464
465 scheme_provider: S,
467
468 blocker: X,
470
471 shard_codec_cfg: <Shard<B, C, H> as Read>::Cfg,
473
474 block_codec_cfg: B::Cfg,
476
477 strategy: T,
479
480 records: CommitmentRecords<B, C, H, P>,
482
483 peer_buffers: BTreeMap<P, VecDeque<Shard<B, C, H>>>,
488
489 peer_buffer_size: NonZeroUsize,
491
492 peer_provider: D,
494
495 aggregate_peers: Set<P>,
498
499 latest_primary_peers: Set<P>,
501
502 background_channel_capacity: NonZeroUsize,
504
505 assigned_shard_verified_subscriptions: BTreeMap<Commitment<B, C, H>, Vec<oneshot::Sender<()>>>,
515
516 #[allow(clippy::type_complexity)]
519 block_subscriptions: BTreeMap<
520 BlockSubscriptionKey<Commitment<B, C, H>, B::Digest>,
521 Vec<oneshot::Sender<Arc<CodedBlock<B, C, H>>>>,
522 >,
523
524 metrics: ShardMetrics<P>,
526}
527
528impl<E, S, X, D, C, H, B, P, T> Engine<E, S, X, D, C, H, B, P, T>
529where
530 E: BufferPooler + Rng + Spawner + Metrics + Clock,
531 S: Provider<Scope = Epoch>,
532 S::Scheme: CertificateScheme<PublicKey = P>,
533 X: Blocker<PublicKey = P>,
534 D: PeerProvider<PublicKey = P>,
535 C: CodingScheme,
536 H: Hasher,
537 B: CertifiableBlock,
538 P: PublicKey,
539 T: Strategy,
540{
541 pub fn new(context: E, config: Config<P, S, X, D, C, H, B, T>) -> (Self, Mailbox<B, C, H, P>) {
543 let metrics = ShardMetrics::new(&context);
544 let (sender, mailbox) = mailbox::new(context.child("mailbox"), config.mailbox_size);
545 (
546 Self {
547 context: ContextCell::new(context),
548 mailbox,
549 scheme_provider: config.scheme_provider,
550 blocker: config.blocker,
551 shard_codec_cfg: config.shard_codec_cfg,
552 block_codec_cfg: config.block_codec_cfg,
553 strategy: config.strategy,
554 records: BTreeMap::new(),
555 peer_buffers: BTreeMap::new(),
556 peer_buffer_size: config.peer_buffer_size,
557 peer_provider: config.peer_provider,
558 aggregate_peers: Set::default(),
559 latest_primary_peers: Set::default(),
560 background_channel_capacity: config.background_channel_capacity,
561 assigned_shard_verified_subscriptions: BTreeMap::new(),
562 block_subscriptions: BTreeMap::new(),
563 metrics,
564 },
565 Mailbox::new(sender),
566 )
567 }
568
569 pub fn start(
571 mut self,
572 network: (impl Sender<PublicKey = P>, impl Receiver<PublicKey = P>),
573 ) -> Handle<()> {
574 spawn_cell!(self.context, self.run(network))
575 }
576
577 async fn run(
579 mut self,
580 (sender, receiver): (impl Sender<PublicKey = P>, impl Receiver<PublicKey = P>),
581 ) {
582 let mut sender = WrappedSender::<_, Shard<B, C, H>>::new(
583 self.context.network_buffer_pool().clone(),
584 sender,
585 );
586 let (receiver_service, mut receiver) =
587 WrappedBackgroundReceiver::<_, P, X, _, Shard<B, C, H>, T>::new(
588 self.context.child("shard_ingress"),
589 receiver,
590 self.shard_codec_cfg.clone(),
591 self.blocker.clone(),
592 self.background_channel_capacity,
593 self.strategy.clone(),
594 );
595 let _receiver_handle = receiver_service.start();
597 let mut peer_set_subscription = self.peer_provider.subscribe().await;
598
599 select_loop! {
600 self.context,
601 on_start => {
602 self.block_subscriptions.retain(|_, subscribers| {
604 subscribers.retain(|tx| !tx.is_closed());
605 !subscribers.is_empty()
606 });
607 self.assigned_shard_verified_subscriptions
608 .retain(|_, subscribers| {
609 subscribers.retain(|tx| !tx.is_closed());
610 !subscribers.is_empty()
611 });
612 },
613 on_stopped => {
614 debug!("received shutdown signal, stopping shard engine");
615 },
616 Some(update) = peer_set_subscription.recv() else {
617 debug!("peer set subscription closed");
618 return;
619 } => {
620 let all_peers = update.all.union();
621 self.update_latest_primary_peers(update.latest.primary);
622 self.aggregate_peers = all_peers;
623 },
624 Some(message) = self.mailbox.recv() else {
625 debug!("shard mailbox closed, stopping shard engine");
626 return;
627 } => {
628 if message.response_closed() {
629 continue;
630 }
631
632 match message {
633 Message::Proposed { block, round } => {
634 self.broadcast_shards(&mut sender, round, block);
635 }
636 Message::Discovered {
637 commitment,
638 leader,
639 round,
640 } => {
641 self.handle_external_proposal(&mut sender, commitment, leader, round);
642 }
643 Message::Notarized { commitment, round } => {
644 self.handle_notarized_commitment(&mut sender, commitment, round);
645 }
646 Message::GetByCommitment {
647 commitment,
648 response,
649 } => {
650 let block = self
651 .records
652 .get(&commitment)
653 .and_then(CommitmentRecord::block)
654 .cloned();
655 response.send_lossy(block);
656 }
657 Message::GetByDigest { digest, response } => {
658 let block = self.records.values().find_map(|record| {
659 let block = record.block()?;
660 (block.digest() == digest).then(|| Arc::clone(block))
661 });
662 response.send_lossy(block);
663 }
664 Message::SubscribeAssignedShardVerified {
665 commitment,
666 response,
667 } => {
668 self.handle_assigned_shard_verified_subscription(commitment, response);
669 }
670 Message::SubscribeByCommitment {
671 commitment,
672 response,
673 } => {
674 self.handle_block_subscription(
675 BlockSubscriptionKey::Commitment(commitment),
676 response,
677 );
678 }
679 Message::SubscribeByDigest { digest, response } => {
680 self.handle_block_subscription(
681 BlockSubscriptionKey::Digest(digest),
682 response,
683 );
684 }
685 Message::Retire { update } => {
686 self.retire(update);
687 }
688 }
689 },
690 Some((peer, shard)) = receiver.recv() else {
691 debug!("receiver closed, stopping shard engine");
692 return;
693 } => {
694 self.handle_network_shard(&mut sender, peer, shard);
695 },
696 }
697 }
698
699 fn handle_network_shard<Sr: Sender<PublicKey = P>>(
701 &mut self,
702 sender: &mut WrappedSender<Sr, Shard<B, C, H>>,
703 peer: P,
704 shard: Shard<B, C, H>,
705 ) {
706 self.metrics.shards_received.get_or_create_by(&peer).inc();
707
708 let commitment = shard.commitment();
709 if !self.should_handle_network_shard(commitment) {
710 return;
711 }
712
713 if let Some(record) = self.records.get(&commitment)
714 && let Some(existing) = record.reconstruction()
715 {
716 let round = record.round();
717 let Some(scheme) = self.scheme_provider.scheme(round.epoch()) else {
718 warn!(%commitment, "no scheme for epoch, ignoring shard");
719 return;
720 };
721
722 if existing.leader().is_none()
726 && let Some(sender_index) = scheme.participants().index(&peer)
727 {
728 let expected_index: u16 = sender_index
729 .get()
730 .try_into()
731 .expect("participant index impossibly out of bounds");
732 if shard.index() != expected_index {
733 self.buffer_peer_shard(peer, shard);
736 return;
737 }
738 }
739
740 let state = self
741 .records
742 .get_mut(&commitment)
743 .and_then(CommitmentRecord::reconstruction_mut)
744 .expect("reconstruction checked as present");
745 let progressed = state.on_network_shard(
746 peer,
747 shard,
748 InsertCtx::new(scheme.as_ref(), &self.strategy),
749 &mut self.blocker,
750 );
751 if progressed {
752 self.try_advance(sender, commitment);
753 }
754 } else {
755 self.buffer_peer_shard(peer, shard);
756 }
757 }
758
759 fn should_handle_network_shard(&self, commitment: Commitment<B, C, H>) -> bool {
765 if let Some(record) = self.records.get(&commitment)
766 && record.block().is_some()
767 {
768 return record
771 .reconstruction()
772 .is_some_and(|s| !s.is_assigned_shard_verified());
773 }
774 true
775 }
776
777 #[allow(clippy::type_complexity)]
785 fn try_reconstruct(
786 &mut self,
787 commitment: Commitment<B, C, H>,
788 ) -> Result<Option<Arc<CodedBlock<B, C, H>>>, Error<C>> {
789 let Some(record) = self.records.get_mut(&commitment) else {
790 return Ok(None);
791 };
792 if let Some(block) = record.block() {
793 return Ok(Some(Arc::clone(block)));
794 }
795 let round = record.round();
796 let state = record
797 .reconstruction_mut()
798 .expect("an uncached commitment record must be reconstructing");
799 if state.checked_shards().len() < usize::from(commitment.config().minimum_shards.get()) {
800 debug!(%commitment, "not enough checked shards to reconstruct block");
801 return Ok(None);
802 }
803 let start = self.context.current();
805 let blob = C::decode(
806 &commitment.config(),
807 &commitment.root(),
808 state.checked_shards().iter(),
809 &self.strategy,
810 )
811 .map_err(Error::Coding)?;
812 self.metrics
813 .erasure_decode_duration
814 .observe_between(start, self.context.current());
815
816 let (inner, config): (B, CodingConfig) =
818 Decode::decode_cfg(&mut blob.as_slice(), &(self.block_codec_cfg.clone(), ()))?;
819
820 match validate_reconstruction(&inner, config, commitment) {
821 Ok(()) => {}
822 Err(InvariantError::BlockDigest) => {
823 return Err(Error::DigestMismatch);
824 }
825 Err(InvariantError::CodingConfig) => {
826 warn!(
827 %commitment,
828 expected_config = ?commitment.config(),
829 actual_config = ?config,
830 "reconstructed block config does not match commitment config, but digest matches"
831 );
832 return Err(Error::ConfigMismatch);
833 }
834 Err(InvariantError::ContextDigest(expected, actual)) => {
835 warn!(
836 %commitment,
837 expected_context_digest = ?expected,
838 actual_context_digest = ?actual,
839 "reconstructed block context digest does not match commitment context digest"
840 );
841 return Err(Error::ContextMismatch);
842 }
843 }
844
845 let block = self
848 .cache_block(round, Arc::new(CodedBlock::new_trusted(inner, commitment)))
849 .expect("reconstruction uses its commitment record's epoch");
850 self.metrics.blocks_reconstructed_total.inc();
851 Ok(Some(block))
852 }
853
854 fn handle_external_proposal<Sr: Sender<PublicKey = P>>(
856 &mut self,
857 sender: &mut WrappedSender<Sr, Shard<B, C, H>>,
858 commitment: Commitment<B, C, H>,
859 leader: P,
860 round: Round,
861 ) {
862 let Some(scheme) = self.scheme_provider.scheme(round.epoch()) else {
863 warn!(%commitment, "no scheme for epoch, ignoring external proposal");
864 return;
865 };
866 let participants = scheme.participants();
867 if participants.index(&leader).is_none() {
868 warn!(?leader, %commitment, "leader update for non-participant, ignoring");
869 return;
870 }
871 let Some(status) = self.observe_existing_commitment(commitment, round) else {
876 return;
877 };
878 if status == CommitmentStatus::Cached
879 && self
880 .records
881 .get(&commitment)
882 .and_then(CommitmentRecord::reconstruction)
883 .is_none_or(|state| state.leader().is_some())
884 {
885 return;
886 }
887 if let Some(state) = self
888 .records
889 .get_mut(&commitment)
890 .and_then(CommitmentRecord::reconstruction_mut)
891 {
892 if let Some(existing) = state.leader() {
893 if existing != &leader {
894 debug!(
898 existing = ?existing,
899 ?leader,
900 %commitment,
901 "commitment already has a leader, ignoring update"
902 );
903 }
904 return;
905 }
906 state
907 .set_leader(leader)
908 .expect("leader was checked as absent");
909 } else {
910 let participants_len = u64::try_from(participants.len())
911 .expect("participant count impossibly out of bounds");
912 self.insert_reconstruction_record(
913 commitment,
914 round,
915 ReconstructionState::new(Some(leader), participants_len),
916 );
917 }
918 let buffered_progress = self.ingest_buffered_shards(commitment);
919 if buffered_progress {
920 self.try_advance(sender, commitment);
921 }
922 }
923
924 fn handle_notarized_commitment<Sr: Sender<PublicKey = P>>(
930 &mut self,
931 sender: &mut WrappedSender<Sr, Shard<B, C, H>>,
932 commitment: Commitment<B, C, H>,
933 round: Round,
934 ) {
935 let Some(status) = self.observe_existing_commitment(commitment, round) else {
936 return;
937 };
938 if status == CommitmentStatus::Cached {
939 return;
940 }
941 if status == CommitmentStatus::Reconstructing {
942 let buffered_progress = self.ingest_buffered_shards(commitment);
943 if buffered_progress {
944 self.try_advance(sender, commitment);
945 }
946 return;
947 }
948 let Some(scheme) = self.scheme_provider.scheme(round.epoch()) else {
949 warn!(%commitment, "no scheme for epoch, ignoring notarized commitment");
950 return;
951 };
952 let participants_len = u64::try_from(scheme.participants().len())
953 .expect("participant count impossibly out of bounds");
954 self.insert_reconstruction_record(
955 commitment,
956 round,
957 ReconstructionState::new(None, participants_len),
958 );
959 let buffered_progress = self.ingest_buffered_shards(commitment);
960 if buffered_progress {
961 self.try_advance(sender, commitment);
962 }
963 }
964
965 fn buffer_peer_shard(&mut self, peer: P, shard: Shard<B, C, H>) {
967 if self.latest_primary_peers.position(&peer).is_none() {
968 debug!(
969 ?peer,
970 "pre-leader shard from peer outside latest.primary not buffered"
971 );
972 return;
973 }
974 let queue = self.peer_buffers.entry(peer).or_default();
975 if queue.len() >= self.peer_buffer_size.get() {
976 let _ = queue.pop_front();
977 }
978 queue.push_back(shard);
979 }
980
981 fn update_latest_primary_peers(&mut self, peers: Set<P>) {
982 self.peer_buffers
983 .retain(|peer, _| peers.position(peer).is_some());
984 self.latest_primary_peers = peers;
985 }
986
987 fn ingest_buffered_shards(&mut self, commitment: Commitment<B, C, H>) -> bool {
993 let record = self
994 .records
995 .get(&commitment)
996 .expect("buffered shards can only be ingested with a commitment record");
997 let round = record.round();
998 let state = record
999 .reconstruction()
1000 .expect("buffered shards can only be ingested with reconstruction state");
1001 let leader_known = state.leader().is_some();
1002 let Some(scheme) = self.scheme_provider.scheme(round.epoch()) else {
1003 warn!(%commitment, "no scheme for epoch, dropping buffered shards");
1004 return false;
1005 };
1006
1007 let mut buffered = Vec::new();
1008 for (peer, queue) in self.peer_buffers.iter_mut() {
1009 let mut i = 0;
1010 while i < queue.len() {
1011 if queue[i].commitment() != commitment {
1012 i += 1;
1013 continue;
1014 }
1015 if !leader_known {
1016 let Some(sender_index) = scheme.participants().index(peer) else {
1017 i += 1;
1018 continue;
1019 };
1020 let expected_index: u16 = sender_index
1021 .get()
1022 .try_into()
1023 .expect("participant index impossibly out of bounds");
1024 if queue[i].index() != expected_index {
1025 i += 1;
1026 continue;
1027 }
1028 }
1029 let shard = queue.swap_remove_back(i).expect("index is valid");
1030 buffered.push((peer.clone(), shard));
1031 }
1032 }
1033
1034 let state = self
1035 .records
1036 .get_mut(&commitment)
1037 .and_then(CommitmentRecord::reconstruction_mut)
1038 .expect("reconstruction state checked before buffered shard drain");
1039
1040 let mut progressed = false;
1043 let ctx = InsertCtx::new(scheme.as_ref(), &self.strategy);
1044 for (peer, shard) in buffered {
1045 progressed |= state.on_network_shard(peer, shard, ctx, &mut self.blocker);
1046 }
1047 progressed
1048 }
1049
1050 fn observe_existing_commitment(
1055 &mut self,
1056 commitment: Commitment<B, C, H>,
1057 round: Round,
1058 ) -> Option<CommitmentStatus> {
1059 let observed_epoch = round.epoch();
1060 let Some(record) = self.records.get_mut(&commitment) else {
1061 return Some(CommitmentStatus::Absent);
1062 };
1063 if let Err(existing_epoch) = record.observe(round) {
1064 warn!(
1065 %commitment,
1066 %existing_epoch,
1067 %observed_epoch,
1068 "commitment observation has conflicting epoch, ignoring"
1069 );
1070 return None;
1071 }
1072 Some(if record.block().is_some() {
1073 CommitmentStatus::Cached
1074 } else {
1075 CommitmentStatus::Reconstructing
1076 })
1077 }
1078
1079 fn insert_reconstruction_record(
1081 &mut self,
1082 commitment: Commitment<B, C, H>,
1083 round: Round,
1084 reconstruction: ReconstructionState<P, B, C, H>,
1085 ) {
1086 let Entry::Vacant(entry) = self.records.entry(commitment) else {
1087 unreachable!("commitment status was checked as absent");
1088 };
1089 entry.insert(CommitmentRecord::reconstructing(round, reconstruction));
1090 self.metrics.reconstruction_states_count.inc();
1091 }
1092
1093 fn cache_block(
1095 &mut self,
1096 round: Round,
1097 block: Arc<CodedBlock<B, C, H>>,
1098 ) -> Result<Arc<CodedBlock<B, C, H>>, Epoch> {
1099 let commitment = block.commitment();
1100 let cached = match self.records.entry(commitment) {
1101 Entry::Occupied(mut entry) => {
1102 entry.get_mut().observe(round)?;
1103 let newly_cached = entry.get().block().is_none();
1104 let cached = entry.get_mut().install_block(block);
1105 if newly_cached {
1106 self.metrics.reconstructed_blocks_cache_count.inc();
1107 }
1108 cached
1109 }
1110 Entry::Vacant(entry) => {
1111 entry.insert(CommitmentRecord::cached(round, Arc::clone(&block)));
1112 self.metrics.reconstructed_blocks_cache_count.inc();
1113 block
1114 }
1115 };
1116 self.notify_block_subscribers(Arc::clone(&cached));
1117 Ok(cached)
1118 }
1119
1120 fn broadcast_shards<Sr: Sender<PublicKey = P>>(
1125 &mut self,
1126 sender: &mut WrappedSender<Sr, Shard<B, C, H>>,
1127 round: Round,
1128 block: Arc<CodedBlock<B, C, H>>,
1129 ) {
1130 let commitment = block.commitment();
1131
1132 if let Some(record) = self.records.get(&commitment)
1133 && let Err(existing_epoch) = record.validate_epoch(round)
1134 {
1135 warn!(
1136 %commitment,
1137 %existing_epoch,
1138 observed_epoch = %round.epoch(),
1139 "local proposal has conflicting epoch, ignoring"
1140 );
1141 return;
1142 }
1143
1144 let Some(scheme) = self.scheme_provider.scheme(round.epoch()) else {
1145 warn!(%commitment, "no scheme available, cannot broadcast shards");
1146 return;
1147 };
1148 let participants = scheme.participants();
1149 let Some(me) = scheme.me() else {
1150 warn!(
1151 %commitment,
1152 "cannot broadcast shards: local proposer is not a participant"
1153 );
1154 return;
1155 };
1156
1157 let shard_count = block.shards(&self.strategy).len();
1158 if shard_count != participants.len() {
1159 warn!(
1160 %commitment,
1161 shard_count,
1162 participants = participants.len(),
1163 "cannot broadcast shards: participant/shard count mismatch"
1164 );
1165 return;
1166 }
1167
1168 let my_index = me.get() as usize;
1169 let leader_shard = block
1170 .shard(my_index as u16)
1171 .expect("proposer's shard must exist");
1172
1173 for (index, peer) in participants.iter().enumerate() {
1175 if index == my_index {
1176 continue;
1177 }
1178
1179 let Some(shard) = block.shard(index as u16) else {
1180 warn!(
1181 %commitment,
1182 index,
1183 "cannot broadcast shards: missing shard for participant index"
1184 );
1185 return;
1186 };
1187 let _ = sender.send(Recipients::One(peer.clone()), shard, true);
1188 }
1189
1190 let non_participants: Vec<P> = self
1192 .aggregate_peers
1193 .iter()
1194 .filter(|peer| participants.index(peer).is_none())
1195 .cloned()
1196 .collect();
1197 if !non_participants.is_empty() {
1198 let _ = sender.send(Recipients::Some(non_participants), leader_shard, true);
1199 }
1200
1201 self.cache_block(round, block)
1203 .expect("local proposal epoch was validated before broadcast");
1204 self.records
1205 .get_mut(&commitment)
1206 .expect("caching a local proposal must create a commitment record")
1207 .mark_proposed();
1208
1209 self.notify_assigned_shard_verified_subscribers(commitment);
1212
1213 debug!(?commitment, "broadcasted shards");
1214 }
1215
1216 fn broadcast_shard<Sr: Sender<PublicKey = P>>(
1218 &mut self,
1219 sender: &mut WrappedSender<Sr, Shard<B, C, H>>,
1220 shard: Shard<B, C, H>,
1221 ) {
1222 let commitment = shard.commitment();
1223 let peers = sender.send(Recipients::All, shard, true);
1224 debug!(
1225 ?commitment,
1226 peers = peers.len(),
1227 "broadcasted shard to all peers"
1228 );
1229 }
1230
1231 fn try_advance<Sr: Sender<PublicKey = P>>(
1237 &mut self,
1238 sender: &mut WrappedSender<Sr, Shard<B, C, H>>,
1239 commitment: Commitment<B, C, H>,
1240 ) {
1241 if let Some(state) = self
1242 .records
1243 .get_mut(&commitment)
1244 .and_then(CommitmentRecord::reconstruction_mut)
1245 {
1246 match state.take_pending_action() {
1247 Some(AssignedShardVerifiedAction::Broadcast(shard)) => {
1248 self.broadcast_shard(sender, shard);
1249 self.notify_assigned_shard_verified_subscribers(commitment);
1250 }
1251 Some(AssignedShardVerifiedAction::NotifyOnly) => {
1252 self.notify_assigned_shard_verified_subscribers(commitment);
1253 }
1254 None => {}
1255 }
1256 }
1257
1258 match self.try_reconstruct(commitment) {
1259 Ok(Some(block)) => {
1260 debug!(
1266 %commitment,
1267 parent = %block.parent(),
1268 height = %block.height(),
1269 "successfully reconstructed block from shards"
1270 );
1271 }
1272 Ok(None) => {
1273 debug!(%commitment, "not enough checked shards to reconstruct block");
1274 }
1275 Err(err) => {
1276 warn!(%commitment, ?err, "failed to reconstruct block from checked shards");
1277 self.retire_commitment(commitment, RetirementReason::Exact);
1278 self.metrics.reconstruction_failures_total.inc();
1279 }
1280 }
1281 }
1282
1283 fn handle_assigned_shard_verified_subscription(
1288 &mut self,
1289 commitment: Commitment<B, C, H>,
1290 response: oneshot::Sender<()>,
1291 ) {
1292 if self
1294 .records
1295 .get(&commitment)
1296 .is_some_and(CommitmentRecord::is_assigned_shard_ready)
1297 {
1298 response.send_lossy(());
1299 return;
1300 }
1301
1302 self.assigned_shard_verified_subscriptions
1303 .entry(commitment)
1304 .or_default()
1305 .push(response);
1306 }
1307
1308 fn handle_block_subscription(
1310 &mut self,
1311 key: BlockSubscriptionKey<Commitment<B, C, H>, B::Digest>,
1312 response: oneshot::Sender<Arc<CodedBlock<B, C, H>>>,
1313 ) {
1314 let block = match key {
1315 BlockSubscriptionKey::Commitment(commitment) => self
1316 .records
1317 .get(&commitment)
1318 .and_then(CommitmentRecord::block),
1319 BlockSubscriptionKey::Digest(digest) => self
1320 .records
1321 .values()
1322 .filter_map(CommitmentRecord::block)
1323 .find(|block| block.digest() == digest),
1324 };
1325
1326 if let Some(block) = block {
1328 response.send_lossy(Arc::clone(block));
1329 return;
1330 }
1331
1332 self.block_subscriptions
1333 .entry(key)
1334 .or_default()
1335 .push(response);
1336 }
1337
1338 fn notify_assigned_shard_verified_subscribers(&mut self, commitment: Commitment<B, C, H>) {
1341 if let Some(mut subscribers) = self
1342 .assigned_shard_verified_subscriptions
1343 .remove(&commitment)
1344 {
1345 for subscriber in subscribers.drain(..) {
1346 subscriber.send_lossy(());
1347 }
1348 }
1349 }
1350
1351 fn notify_block_subscribers(&mut self, block: Arc<CodedBlock<B, C, H>>) {
1353 let commitment = block.commitment();
1354 let digest = block.digest();
1355
1356 if let Some(mut subscribers) = self
1358 .block_subscriptions
1359 .remove(&BlockSubscriptionKey::Commitment(commitment))
1360 {
1361 for subscriber in subscribers.drain(..) {
1362 subscriber.send_lossy(Arc::clone(&block));
1363 }
1364 }
1365
1366 if let Some(mut subscribers) = self
1368 .block_subscriptions
1369 .remove(&BlockSubscriptionKey::Digest(digest))
1370 {
1371 for subscriber in subscribers.drain(..) {
1372 subscriber.send_lossy(Arc::clone(&block));
1373 }
1374 }
1375 }
1376
1377 fn retire_commitment(&mut self, commitment: Commitment<B, C, H>, reason: RetirementReason) {
1379 let had_reconstruction = self.records.remove(&commitment).is_some_and(|record| {
1380 let had_reconstruction = record.reconstruction().is_some();
1381 if had_reconstruction {
1382 self.metrics.reconstruction_states_count.dec();
1383 }
1384 if record.block().is_some() {
1385 self.metrics.reconstructed_blocks_cache_count.dec();
1386 }
1387 had_reconstruction
1388 });
1389
1390 if matches!(reason, RetirementReason::Exact) || had_reconstruction {
1391 self.assigned_shard_verified_subscriptions
1392 .remove(&commitment);
1393 }
1394 if matches!(reason, RetirementReason::Exact) {
1395 self.block_subscriptions
1398 .remove(&BlockSubscriptionKey::Commitment(commitment));
1399 }
1400 }
1401
1402 fn retire(&mut self, update: Retirement<Commitment<B, C, H>>) {
1407 let Retirement {
1408 round_floor,
1409 exact_retirements,
1410 } = update;
1411 for commitment in exact_retirements {
1414 self.retire_commitment(commitment, RetirementReason::Exact);
1415 }
1416
1417 let retired = self
1420 .records
1421 .iter()
1422 .filter_map(|(commitment, record)| {
1423 (record.round() <= round_floor).then_some(*commitment)
1424 })
1425 .collect::<Vec<_>>();
1426 for commitment in retired {
1427 self.retire_commitment(commitment, RetirementReason::Floor);
1428 }
1429 }
1430}
1431
1432enum ReconstructionState<P, B, C, H>
1434where
1435 P: PublicKey,
1436 B: Digestible,
1437 C: CodingScheme,
1438 H: Hasher,
1439{
1440 AwaitingQuorum(AwaitingQuorumState<P, B, C, H>),
1444 Ready(ReadyState<P, B, C, H>),
1447}
1448
1449enum AssignedShardVerifiedAction<B: Digestible, C: CodingScheme, H: Hasher> {
1454 Broadcast(Shard<B, C, H>),
1456 NotifyOnly,
1458}
1459
1460struct IndexedShard<C: CodingScheme> {
1462 index: u16,
1463 data: C::Shard,
1464}
1465
1466struct CommonState<P, B, C, H>
1468where
1469 P: PublicKey,
1470 B: Digestible,
1471 C: CodingScheme,
1472 H: Hasher,
1473{
1474 leader: Option<P>,
1477 pending_action: Option<AssignedShardVerifiedAction<B, C, H>>,
1479 checked_shards: Vec<C::CheckedShard>,
1481 contributed: BitMap,
1483 received_shards: BTreeMap<u16, C::Shard>,
1486 assigned_shard_verified: bool,
1488}
1489
1490struct AwaitingQuorumState<P, B, C, H>
1497where
1498 P: PublicKey,
1499 B: Digestible,
1500 C: CodingScheme,
1501 H: Hasher,
1502{
1503 common: CommonState<P, B, C, H>,
1504 pending_shards: BTreeMap<P, IndexedShard<C>>,
1506}
1507
1508struct ReadyState<P, B, C, H>
1513where
1514 P: PublicKey,
1515 B: Digestible,
1516 C: CodingScheme,
1517 H: Hasher,
1518{
1519 common: CommonState<P, B, C, H>,
1520}
1521
1522impl<P, B, C, H> CommonState<P, B, C, H>
1523where
1524 P: PublicKey,
1525 B: Digestible,
1526 C: CodingScheme,
1527 H: Hasher,
1528{
1529 fn new(leader: Option<P>, participants_len: u64) -> Self {
1531 Self {
1532 leader,
1533 pending_action: None,
1534 checked_shards: Vec::new(),
1535 contributed: BitMap::zeroes(participants_len),
1536 received_shards: BTreeMap::new(),
1537 assigned_shard_verified: false,
1538 }
1539 }
1540}
1541
1542impl<P, B, C, H> CommonState<P, B, C, H>
1543where
1544 P: PublicKey,
1545 B: Digestible,
1546 C: CodingScheme,
1547 H: Hasher,
1548{
1549 fn verify_assigned_shard(
1558 &mut self,
1559 sender: P,
1560 commitment: Commitment<B, C, H>,
1561 shard: IndexedShard<C>,
1562 is_participant: bool,
1563 blocker: &mut impl Blocker<PublicKey = P>,
1564 ) -> bool {
1565 self.received_shards.insert(shard.index, shard.data);
1569 let data = self.received_shards.get(&shard.index).unwrap();
1570 let Ok(checked) = C::check(&commitment.config(), &commitment.root(), shard.index, data)
1571 else {
1572 self.received_shards.remove(&shard.index);
1573 commonware_p2p::block!(blocker, sender, "invalid assigned shard received");
1574 return false;
1575 };
1576
1577 self.contributed.set(u64::from(shard.index), true);
1578 self.checked_shards.push(checked);
1579 self.assigned_shard_verified = true;
1580 self.pending_action = Some(if is_participant {
1581 AssignedShardVerifiedAction::Broadcast(Shard::new(
1582 commitment,
1583 shard.index,
1584 data.clone(),
1585 ))
1586 } else {
1587 AssignedShardVerifiedAction::NotifyOnly
1588 });
1589 true
1590 }
1591}
1592
1593impl<P, B, C, H> AwaitingQuorumState<P, B, C, H>
1594where
1595 P: PublicKey,
1596 B: Digestible,
1597 C: CodingScheme,
1598 H: Hasher,
1599{
1600 fn try_transition(
1603 &mut self,
1604 commitment: Commitment<B, C, H>,
1605 participants_len: u64,
1606 strategy: &impl Strategy,
1607 blocker: &mut impl Blocker<PublicKey = P>,
1608 ) -> Option<ReadyState<P, B, C, H>> {
1609 let minimum = usize::from(commitment.config().minimum_shards.get());
1610 if self.common.checked_shards.len() + self.pending_shards.len() < minimum {
1611 return None;
1612 }
1613
1614 let pending = std::mem::take(&mut self.pending_shards);
1616 let (new_checked, to_block) =
1617 strategy.map_partition_collect_vec(pending, |(peer, shard)| {
1618 let checked = C::check(
1619 &commitment.config(),
1620 &commitment.root(),
1621 shard.index,
1622 &shard.data,
1623 );
1624 (peer, checked.ok())
1625 });
1626
1627 for peer in to_block {
1628 commonware_p2p::block!(blocker, peer, "invalid shard received");
1629 }
1630 for checked in new_checked {
1631 self.common.checked_shards.push(checked);
1632 }
1633
1634 if self.common.checked_shards.len() < minimum {
1636 return None;
1637 }
1638
1639 let leader = self.common.leader.clone();
1641 let common =
1642 std::mem::replace(&mut self.common, CommonState::new(leader, participants_len));
1643 Some(ReadyState { common })
1644 }
1645}
1646
1647struct InsertCtx<'a, Sch, S>
1649where
1650 Sch: CertificateScheme,
1651 S: Strategy,
1652{
1653 scheme: &'a Sch,
1654 strategy: &'a S,
1655 participants_len: u64,
1656}
1657
1658impl<Sch: CertificateScheme, S: Strategy> Clone for InsertCtx<'_, Sch, S> {
1659 fn clone(&self) -> Self {
1660 *self
1661 }
1662}
1663
1664impl<Sch: CertificateScheme, S: Strategy> Copy for InsertCtx<'_, Sch, S> {}
1665
1666impl<'a, Sch: CertificateScheme, S: Strategy> InsertCtx<'a, Sch, S> {
1667 fn new(scheme: &'a Sch, strategy: &'a S) -> Self {
1668 let participants_len = u64::try_from(scheme.participants().len())
1669 .expect("participant count impossibly out of bounds");
1670 Self {
1671 scheme,
1672 strategy,
1673 participants_len,
1674 }
1675 }
1676}
1677
1678impl<P, B, C, H> ReconstructionState<P, B, C, H>
1679where
1680 P: PublicKey,
1681 B: Digestible,
1682 C: CodingScheme,
1683 H: Hasher,
1684{
1685 fn new(leader: Option<P>, participants_len: u64) -> Self {
1687 Self::AwaitingQuorum(AwaitingQuorumState {
1688 common: CommonState::new(leader, participants_len),
1689 pending_shards: BTreeMap::new(),
1690 })
1691 }
1692
1693 const fn common(&self) -> &CommonState<P, B, C, H> {
1695 match self {
1696 Self::AwaitingQuorum(state) => &state.common,
1697 Self::Ready(state) => &state.common,
1698 }
1699 }
1700
1701 const fn common_mut(&mut self) -> &mut CommonState<P, B, C, H> {
1703 match self {
1704 Self::AwaitingQuorum(state) => &mut state.common,
1705 Self::Ready(state) => &mut state.common,
1706 }
1707 }
1708
1709 const fn leader(&self) -> Option<&P> {
1711 self.common().leader.as_ref()
1712 }
1713
1714 fn set_leader(&mut self, leader: P) -> Result<(), P> {
1716 if self.common().leader.is_some() {
1717 return Err(leader);
1718 }
1719 self.common_mut().leader = Some(leader);
1720 Ok(())
1721 }
1722
1723 const fn is_assigned_shard_verified(&self) -> bool {
1725 self.common().assigned_shard_verified
1726 }
1727
1728 const fn checked_shards(&self) -> &[C::CheckedShard] {
1730 self.common().checked_shards.as_slice()
1731 }
1732
1733 const fn take_pending_action(&mut self) -> Option<AssignedShardVerifiedAction<B, C, H>> {
1737 self.common_mut().pending_action.take()
1738 }
1739
1740 fn on_network_shard<Sch, S, X>(
1782 &mut self,
1783 sender: P,
1784 shard: Shard<B, C, H>,
1785 ctx: InsertCtx<'_, Sch, S>,
1786 blocker: &mut X,
1787 ) -> bool
1788 where
1789 Sch: CertificateScheme<PublicKey = P>,
1790 S: Strategy,
1791 X: Blocker<PublicKey = P>,
1792 {
1793 let Some(sender_index) = ctx.scheme.participants().index(&sender) else {
1794 commonware_p2p::block!(blocker, sender, "shard sent by non-participant");
1795 return false;
1796 };
1797 let commitment = shard.commitment();
1798 let indexed = IndexedShard {
1799 index: shard.index(),
1800 data: shard.into_inner(),
1801 };
1802
1803 let sender_index: u16 = sender_index
1807 .get()
1808 .try_into()
1809 .expect("participant index impossibly out of bounds");
1810 let assigned_index: Option<u16> = ctx.scheme.me().map(|assigned_index| {
1811 assigned_index
1812 .get()
1813 .try_into()
1814 .expect("participant index impossibly out of bounds")
1815 });
1816 let is_from_leader = self.leader().is_some_and(|leader| leader == &sender);
1817 let is_assigned_shard = assigned_index
1818 .is_some_and(|assigned_index| indexed.index == assigned_index)
1819 || assigned_index.is_none() && is_from_leader && indexed.index == sender_index;
1820 let is_gossip_shard = indexed.index == sender_index;
1821 if !is_assigned_shard && !is_gossip_shard {
1822 if self.leader().is_some() {
1823 commonware_p2p::block!(
1824 blocker,
1825 sender,
1826 shard_index = indexed.index,
1827 "shard index is neither assigned nor sender-owned"
1828 );
1829 }
1830 return false;
1831 }
1832
1833 if let Some(existing) = self.common().received_shards.get(&indexed.index) {
1835 if existing != &indexed.data {
1836 commonware_p2p::block!(blocker, sender, "shard equivocation");
1837 }
1838 return false;
1839 }
1840
1841 if self.common().contributed.get(u64::from(indexed.index)) {
1843 return false;
1844 }
1845
1846 if is_assigned_shard && !self.common().assigned_shard_verified {
1849 let progressed = self.common_mut().verify_assigned_shard(
1850 sender,
1851 commitment,
1852 indexed,
1853 ctx.scheme.me().is_some(),
1854 blocker,
1855 );
1856
1857 if progressed
1858 && let Self::AwaitingQuorum(state) = self
1859 && let Some(ready) =
1860 state.try_transition(commitment, ctx.participants_len, ctx.strategy, blocker)
1861 {
1862 *self = Self::Ready(ready);
1863 }
1864 return progressed;
1865 }
1866
1867 let Self::AwaitingQuorum(state) = self else {
1869 return false;
1870 };
1871
1872 state
1874 .common
1875 .received_shards
1876 .insert(indexed.index, indexed.data.clone());
1877 state.common.contributed.set(u64::from(indexed.index), true);
1878 state.pending_shards.insert(sender, indexed);
1879 if let Some(ready) =
1880 state.try_transition(commitment, ctx.participants_len, ctx.strategy, blocker)
1881 {
1882 *self = Self::Ready(ready);
1883 }
1884
1885 true
1886 }
1887}
1888
1889#[cfg(test)]
1890mod tests {
1891 use super::*;
1892 use crate::{
1893 marshal::{coding::types::coding_config_for_participants, mocks::block::EmptyBlock},
1894 types::{Epoch, Height, View},
1895 };
1896 use bytes::Bytes;
1897 use commonware_codec::Encode;
1898 use commonware_coding::{
1899 CodecConfig, Config as CodingConfig, PhasedAsScheme, ReedSolomon, Zoda,
1900 };
1901 use commonware_cryptography::{
1902 Committable, Digest, Sha256, Signer,
1903 certificate::{Scoped, Subject},
1904 ed25519::{PrivateKey, PublicKey},
1905 impl_certificate_ed25519,
1906 sha256::Digest as Sha256Digest,
1907 };
1908 use commonware_macros::{select, test_traced};
1909 use commonware_p2p::{
1910 Manager as _, TrackedPeers,
1911 simulated::{self, Control, Link, Oracle},
1912 };
1913 use commonware_parallel::Sequential;
1914 use commonware_runtime::{Quota, Runner, Supervisor as _, deterministic};
1915 use commonware_utils::{
1916 N3f1, NZUsize, Participant, channel::oneshot::error::TryRecvError, ordered::Set,
1917 probability,
1918 };
1919 use std::{
1920 future::Future,
1921 marker::PhantomData,
1922 num::{NonZeroU32, NonZeroUsize},
1923 sync::{
1924 Arc,
1925 atomic::{AtomicIsize, Ordering},
1926 },
1927 time::Duration,
1928 };
1929
1930 #[derive(Clone, Debug)]
1931 pub struct TestSubject {
1932 pub message: Bytes,
1933 }
1934
1935 impl Subject for TestSubject {
1936 type Namespace = Vec<u8>;
1937
1938 fn namespace<'a>(&self, derived: &'a Self::Namespace) -> &'a [u8] {
1939 derived
1940 }
1941
1942 fn message(&self) -> Bytes {
1943 self.message.clone()
1944 }
1945 }
1946
1947 impl_certificate_ed25519!(TestSubject, Vec<u8>, N3f1);
1948
1949 const SCHEME_NAMESPACE: &[u8] = b"_COMMONWARE_SHARD_ENGINE_TEST";
1950
1951 const MAX_SHARD_SIZE: usize = 1024 * 1024; const DEFAULT_LINK: Link = Link {
1956 latency: Duration::from_millis(50),
1957 jitter: Duration::ZERO,
1958 success_rate: probability!(1.0),
1959 };
1960
1961 const TEST_QUOTA: Quota = Quota::per_second(NonZeroU32::MAX);
1963
1964 const STRATEGY: Sequential = Sequential;
1966
1967 #[derive(Clone)]
1973 struct MultiEpochProvider {
1974 schemes: BTreeMap<Epoch, Arc<Scheme>>,
1975 }
1976
1977 impl MultiEpochProvider {
1978 fn single(scheme: Scheme) -> Self {
1979 let mut schemes = BTreeMap::new();
1980 schemes.insert(Epoch::zero(), Arc::new(scheme));
1981 Self { schemes }
1982 }
1983
1984 fn with_epoch(mut self, epoch: Epoch, scheme: Scheme) -> Self {
1985 self.schemes.insert(epoch, Arc::new(scheme));
1986 self
1987 }
1988 }
1989
1990 impl Provider for MultiEpochProvider {
1991 type Scope = Epoch;
1992 type Scheme = Scheme;
1993
1994 fn scoped(&self, scope: Epoch) -> Option<Scoped<Scheme>> {
1995 self.schemes.get(&scope).cloned().map(Scoped::scheme)
1996 }
1997 }
1998
1999 #[derive(Clone)]
2002 struct ChurningProvider {
2003 scheme: Arc<Scheme>,
2004 remaining_successes: Arc<AtomicIsize>,
2005 }
2006
2007 impl ChurningProvider {
2008 fn new(scheme: Scheme, successes: isize) -> Self {
2009 Self {
2010 scheme: Arc::new(scheme),
2011 remaining_successes: Arc::new(AtomicIsize::new(successes)),
2012 }
2013 }
2014 }
2015
2016 impl Provider for ChurningProvider {
2017 type Scope = Epoch;
2018 type Scheme = Scheme;
2019
2020 fn scoped(&self, scope: Epoch) -> Option<Scoped<Scheme>> {
2021 if scope != Epoch::zero() {
2022 return None;
2023 }
2024 if self.remaining_successes.fetch_sub(1, Ordering::AcqRel) <= 0 {
2025 return None;
2026 }
2027 Some(Scoped::scheme(Arc::clone(&self.scheme)))
2028 }
2029 }
2030
2031 type B = EmptyBlock<H>;
2033 type H = Sha256;
2034 type P = PublicKey;
2035 type C = ReedSolomon<H>;
2036 type X = Control<P, deterministic::Context>;
2037 type O = Oracle<P, deterministic::Context>;
2038 type Prov = MultiEpochProvider;
2039 type NetworkSender = simulated::Sender<P, deterministic::Context>;
2040 type D = simulated::Manager<P, deterministic::Context>;
2041 type ShardEngine<S> = Engine<deterministic::Context, Prov, X, D, S, H, B, P, Sequential>;
2042 type ChurningShardEngine<S> =
2043 Engine<deterministic::Context, ChurningProvider, X, D, S, H, B, P, Sequential>;
2044
2045 async fn assert_blocked(oracle: &O, blocker: &P, blocked: &P) {
2046 let blocked_peers = oracle.blocked().await.unwrap();
2047 let is_blocked = blocked_peers
2048 .iter()
2049 .any(|(a, b)| a == blocker && b == blocked);
2050 assert!(is_blocked, "expected {blocker} to have blocked {blocked}");
2051 }
2052
2053 struct Peer<S: CodingScheme = C> {
2055 public_key: PublicKey,
2057 index: Participant,
2059 mailbox: Mailbox<B, S, H, P>,
2061 sender: NetworkSender,
2063 }
2064
2065 #[allow(dead_code)]
2067 struct NonParticipant<S: CodingScheme = C> {
2068 public_key: PublicKey,
2070 mailbox: Mailbox<B, S, H, P>,
2072 sender: NetworkSender,
2074 }
2075
2076 struct Fixture<S: CodingScheme = C> {
2078 num_primary_peers: usize,
2080 num_secondary_peers: usize,
2082 num_future_peers: usize,
2084 additional_scheme_epochs: Vec<Epoch>,
2086 link: Link,
2088 peer_buffer_size: NonZeroUsize,
2090 _marker: PhantomData<S>,
2092 }
2093
2094 impl<S: CodingScheme> Default for Fixture<S> {
2095 fn default() -> Self {
2096 Self {
2097 num_primary_peers: 4,
2098 num_secondary_peers: 0,
2099 num_future_peers: 0,
2100 additional_scheme_epochs: Vec::new(),
2101 link: DEFAULT_LINK,
2102 peer_buffer_size: NZUsize!(64),
2103 _marker: PhantomData,
2104 }
2105 }
2106 }
2107
2108 impl<S: CodingScheme> Fixture<S> {
2109 pub fn start<F: Future<Output = ()>>(
2110 self,
2111 f: impl FnOnce(
2112 Self,
2113 deterministic::Context,
2114 O,
2115 Vec<Peer<S>>,
2116 Vec<NonParticipant<S>>,
2117 CodingConfig,
2118 ) -> F,
2119 ) {
2120 let executor = deterministic::Runner::default();
2121 executor.start(|context| async move {
2122 let mut private_keys = (0..self.num_primary_peers)
2123 .map(|i| PrivateKey::from_seed(i as u64))
2124 .collect::<Vec<_>>();
2125 private_keys.sort_by_key(|s| s.public_key());
2126 let peer_keys: Vec<P> = private_keys.iter().map(|c| c.public_key()).collect();
2127
2128 let participants: Set<P> = Set::from_iter_dedup(peer_keys.clone());
2129
2130 let mut np_private_keys = (0..self.num_secondary_peers)
2131 .map(|i| PrivateKey::from_seed((self.num_primary_peers + i) as u64))
2132 .collect::<Vec<_>>();
2133 np_private_keys.sort_by_key(|s| s.public_key());
2134 let np_keys: Vec<P> = np_private_keys.iter().map(|k| k.public_key()).collect();
2135
2136 let (network, oracle) =
2137 simulated::Network::<deterministic::Context, P>::new_with_split_peers(
2138 context.child("network"),
2139 simulated::Config {
2140 max_size: MAX_SHARD_SIZE as u32,
2141 max_peers_per_set: NZUsize!(
2142 self.num_primary_peers
2143 + self.num_secondary_peers.max(self.num_future_peers)
2144 ),
2145 disconnect_on_block: true,
2146 tracked_peer_sets: NZUsize!(1),
2147 },
2148 peer_keys.clone(),
2149 np_keys.clone(),
2150 )
2151 .await;
2152 network.start();
2153
2154 let all_keys: Vec<P> = peer_keys.iter().chain(np_keys.iter()).cloned().collect();
2155
2156 let mut registrations = BTreeMap::new();
2157 for key in all_keys.iter() {
2158 let control = oracle.control(key.clone());
2159 let (sender, receiver) = control
2160 .register(0, TEST_QUOTA)
2161 .await
2162 .expect("registration should succeed");
2163 registrations.insert(key.clone(), (control, sender, receiver));
2164 }
2165 for p1 in all_keys.iter() {
2166 for p2 in all_keys.iter() {
2167 if p2 == p1 {
2168 continue;
2169 }
2170 oracle
2171 .add_link(p1.clone(), p2.clone(), self.link.clone())
2172 .await
2173 .expect("link should be added");
2174 }
2175 }
2176
2177 let coding_config =
2178 coding_config_for_participants(u16::try_from(self.num_primary_peers).unwrap());
2179
2180 let mut peers = Vec::with_capacity(self.num_primary_peers);
2181 for (idx, peer_key) in peer_keys.iter().enumerate() {
2182 let (control, sender, receiver) = registrations
2183 .remove(peer_key)
2184 .expect("peer should be registered");
2185
2186 let participant = Participant::new(idx as u32);
2187 let engine_context = context.child("peer").with_attribute("index", idx);
2188
2189 let scheme = Scheme::signer(
2190 SCHEME_NAMESPACE,
2191 participants.clone(),
2192 private_keys[idx].clone(),
2193 )
2194 .expect("signer scheme should be created");
2195 let mut scheme_provider = MultiEpochProvider::single(scheme);
2196 for epoch in self.additional_scheme_epochs.iter().copied() {
2197 let scheme = Scheme::signer(
2198 SCHEME_NAMESPACE,
2199 participants.clone(),
2200 private_keys[idx].clone(),
2201 )
2202 .expect("signer scheme should be created");
2203 scheme_provider = scheme_provider.with_epoch(epoch, scheme);
2204 }
2205
2206 let config = Config {
2207 scheme_provider,
2208 blocker: control.clone(),
2209 shard_codec_cfg: CodecConfig {
2210 maximum_shard_size: MAX_SHARD_SIZE,
2211 },
2212 block_codec_cfg: (),
2213 strategy: STRATEGY,
2214 mailbox_size: NZUsize!(1024),
2215 peer_buffer_size: self.peer_buffer_size,
2216 background_channel_capacity: NZUsize!(1024),
2217 peer_provider: oracle.manager(),
2218 };
2219
2220 let (engine, mailbox) = ShardEngine::new(engine_context, config);
2221 let sender_clone = sender.clone();
2222 engine.start((sender, receiver));
2223
2224 peers.push(Peer {
2225 public_key: peer_key.clone(),
2226 index: participant,
2227 mailbox,
2228 sender: sender_clone,
2229 });
2230 }
2231
2232 let mut non_participants = Vec::with_capacity(self.num_secondary_peers);
2233 for (idx, np_key) in np_keys.iter().enumerate() {
2234 let (control, sender, receiver) = registrations
2235 .remove(np_key)
2236 .expect("non-participant should be registered");
2237
2238 let engine_context = context
2239 .child("non_participant")
2240 .with_attribute("index", idx);
2241
2242 let scheme = Scheme::verifier(SCHEME_NAMESPACE, participants.clone());
2243 let mut scheme_provider = MultiEpochProvider::single(scheme);
2244 for epoch in self.additional_scheme_epochs.iter().copied() {
2245 scheme_provider = scheme_provider.with_epoch(
2246 epoch,
2247 Scheme::verifier(SCHEME_NAMESPACE, participants.clone()),
2248 );
2249 }
2250
2251 let config = Config {
2252 scheme_provider,
2253 blocker: control.clone(),
2254 shard_codec_cfg: CodecConfig {
2255 maximum_shard_size: MAX_SHARD_SIZE,
2256 },
2257 block_codec_cfg: (),
2258 strategy: STRATEGY,
2259 mailbox_size: NZUsize!(1024),
2260 peer_buffer_size: self.peer_buffer_size,
2261 background_channel_capacity: NZUsize!(1024),
2262 peer_provider: oracle.manager(),
2263 };
2264
2265 let (engine, mailbox) = ShardEngine::new(engine_context, config);
2266 let sender_clone = sender.clone();
2267 engine.start((sender, receiver));
2268
2269 non_participants.push(NonParticipant {
2270 public_key: np_key.clone(),
2271 mailbox,
2272 sender: sender_clone,
2273 });
2274 }
2275
2276 f(
2277 self,
2278 context,
2279 oracle,
2280 peers,
2281 non_participants,
2282 coding_config,
2283 )
2284 .await;
2285 });
2286 }
2287 }
2288
2289 #[test_traced]
2290 fn test_e2e_broadcast_and_reconstruction() {
2291 let fixture = Fixture {
2292 num_primary_peers: 10,
2293 ..Default::default()
2294 };
2295
2296 fixture.start(
2297 |config, context, _, mut peers, _, coding_config| async move {
2298 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
2299 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2300 let commitment = coded_block.commitment();
2301
2302 let leader = peers[0].public_key.clone();
2303 let round = Round::new(Epoch::zero(), View::new(1));
2304 peers[0].mailbox.proposed(round, coded_block.clone());
2305
2306 for peer in peers[1..].iter_mut() {
2308 peer.mailbox.discovered(commitment, leader.clone(), round);
2309 }
2310 context.sleep(config.link.latency).await;
2311
2312 for peer in peers.iter_mut() {
2313 peer.mailbox
2314 .subscribe_assigned_shard_verified(commitment)
2315 .await
2316 .expect("shard subscription should complete");
2317 }
2318 context.sleep(config.link.latency).await;
2319
2320 for peer in peers.iter_mut() {
2321 let reconstructed = peer
2322 .mailbox
2323 .get(commitment)
2324 .await
2325 .expect("block should be reconstructed");
2326 assert_eq!(reconstructed.commitment(), commitment);
2327 assert_eq!(reconstructed.height(), coded_block.height());
2328 }
2329 },
2330 );
2331 }
2332
2333 #[test_traced]
2334 fn test_e2e_broadcast_and_reconstruction_zoda() {
2335 let fixture = Fixture {
2336 num_primary_peers: 10,
2337 ..Default::default()
2338 };
2339
2340 fixture.start(
2341 |config, context, _, mut peers, _, coding_config| async move {
2342 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
2343 let coded_block = CodedBlock::<B, PhasedAsScheme<Zoda<H>>, H>::new(
2344 inner,
2345 coding_config,
2346 &STRATEGY,
2347 );
2348 let commitment = coded_block.commitment();
2349
2350 let leader = peers[0].public_key.clone();
2351 let round = Round::new(Epoch::zero(), View::new(1));
2352 peers[0].mailbox.proposed(round, coded_block.clone());
2353
2354 for peer in peers[1..].iter_mut() {
2356 peer.mailbox.discovered(commitment, leader.clone(), round);
2357 }
2358 context.sleep(config.link.latency).await;
2359
2360 for peer in peers.iter_mut() {
2361 peer.mailbox
2362 .subscribe_assigned_shard_verified(commitment)
2363 .await
2364 .expect("shard subscription should complete");
2365 }
2366 context.sleep(config.link.latency).await;
2367
2368 for peer in peers.iter_mut() {
2369 let reconstructed = peer
2370 .mailbox
2371 .get(commitment)
2372 .await
2373 .expect("block should be reconstructed");
2374 assert_eq!(reconstructed.commitment(), commitment);
2375 assert_eq!(reconstructed.height(), coded_block.height());
2376 }
2377 },
2378 );
2379 }
2380
2381 #[test_traced]
2382 fn test_block_subscriptions() {
2383 let fixture = Fixture {
2384 num_primary_peers: 10,
2385 ..Default::default()
2386 };
2387
2388 fixture.start(
2389 |config, context, _, mut peers, _, coding_config| async move {
2390 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
2391 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2392 let commitment = coded_block.commitment();
2393 let digest = coded_block.digest();
2394
2395 let leader = peers[0].public_key.clone();
2396 let round = Round::new(Epoch::zero(), View::new(1));
2397
2398 let commitment_sub = peers[1].mailbox.subscribe(commitment);
2400 let digest_sub = peers[2].mailbox.subscribe_by_digest(digest);
2401
2402 peers[0].mailbox.proposed(round, coded_block.clone());
2403
2404 for peer in peers[1..].iter_mut() {
2406 peer.mailbox.discovered(commitment, leader.clone(), round);
2407 }
2408 context.sleep(config.link.latency * 2).await;
2409
2410 for peer in peers.iter_mut() {
2411 peer.mailbox
2412 .subscribe_assigned_shard_verified(commitment)
2413 .await
2414 .expect("shard subscription should complete");
2415 }
2416 context.sleep(config.link.latency).await;
2417
2418 let block_by_commitment =
2419 commitment_sub.await.expect("subscription should resolve");
2420 assert_eq!(block_by_commitment.commitment(), commitment);
2421 assert_eq!(block_by_commitment.height(), coded_block.height());
2422
2423 let block_by_digest = digest_sub.await.expect("subscription should resolve");
2424 assert_eq!(block_by_digest.commitment(), commitment);
2425 assert_eq!(block_by_digest.height(), coded_block.height());
2426 },
2427 );
2428 }
2429
2430 #[test_traced]
2431 fn test_proposer_preproposal_subscriptions_resolve_after_local_cache() {
2432 let fixture = Fixture {
2433 num_primary_peers: 10,
2434 ..Default::default()
2435 };
2436
2437 fixture.start(|config, context, _, peers, _, coding_config| async move {
2438 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
2439 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2440 let commitment = coded_block.commitment();
2441 let digest = coded_block.digest();
2442 let round = Round::new(Epoch::zero(), View::new(1));
2443
2444 let shard_sub = peers[0].mailbox.subscribe_assigned_shard_verified(commitment);
2446 let commitment_sub = peers[0].mailbox.subscribe(commitment);
2447 let digest_sub = peers[0].mailbox.subscribe_by_digest(digest);
2448
2449 peers[0].mailbox.proposed(round, coded_block.clone());
2450 context.sleep(config.link.latency).await;
2451
2452 select! {
2453 result = shard_sub => {
2454 result.expect("shard subscription should resolve");
2455 },
2456 _ = context.sleep(Duration::from_secs(5)) => {
2457 panic!("shard subscription did not resolve after local proposal cache");
2458 }
2459 }
2460
2461 let block_by_commitment = select! {
2462 result = commitment_sub => {
2463 result.expect("block subscription by commitment should resolve")
2464 },
2465 _ = context.sleep(Duration::from_secs(5)) => {
2466 panic!("block subscription by commitment did not resolve after local proposal cache");
2467 }
2468 };
2469 assert_eq!(block_by_commitment.commitment(), commitment);
2470 assert_eq!(block_by_commitment.height(), coded_block.height());
2471
2472 let block_by_digest = select! {
2473 result = digest_sub => {
2474 result.expect("block subscription by digest should resolve")
2475 },
2476 _ = context.sleep(Duration::from_secs(5)) => {
2477 panic!("block subscription by digest did not resolve after local proposal cache");
2478 }
2479 };
2480 assert_eq!(block_by_digest.commitment(), commitment);
2481 assert_eq!(block_by_digest.height(), coded_block.height());
2482 });
2483 }
2484
2485 #[test_traced]
2486 fn test_shard_subscription_rejects_invalid_shard() {
2487 let fixture = Fixture::<C>::default();
2488 fixture.start(
2489 |config, context, oracle, mut peers, _, coding_config| async move {
2490 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
2495 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2496 let commitment = coded_block.commitment();
2497 let receiver_index = peers[2].index.get() as u16;
2498
2499 let valid_shard = coded_block.shard(receiver_index).expect("missing shard");
2500
2501 let mut invalid_shard = valid_shard.clone();
2504 invalid_shard.index = peers[3].index.get() as u16;
2505
2506 let receiver_pk = peers[2].public_key.clone();
2508 let leader = peers[1].public_key.clone();
2509 peers[2].mailbox.discovered(
2510 commitment,
2511 leader,
2512 Round::new(Epoch::zero(), View::new(1)),
2513 );
2514 let mut shard_sub = peers[2]
2515 .mailbox
2516 .subscribe_assigned_shard_verified(commitment);
2517
2518 let invalid_bytes = invalid_shard.encode();
2520 peers[0]
2521 .sender
2522 .send(Recipients::One(receiver_pk.clone()), invalid_bytes, true);
2523
2524 context.sleep(config.link.latency * 2).await;
2525
2526 assert!(
2527 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
2528 "subscription should not resolve from invalid shard"
2529 );
2530 assert_blocked(&oracle, &peers[2].public_key, &peers[0].public_key).await;
2531
2532 let valid_bytes = valid_shard.encode();
2534 peers[1]
2535 .sender
2536 .send(Recipients::One(receiver_pk), valid_bytes, true);
2537 context.sleep(config.link.latency * 2).await;
2538
2539 select! {
2541 _ = shard_sub => {},
2542 _ = context.sleep(Duration::from_secs(5)) => {
2543 panic!("subscription did not complete after valid shard arrival");
2544 },
2545 };
2546 },
2547 );
2548 }
2549
2550 #[test_traced]
2551 fn test_retire_uses_inclusive_retirement_floor() {
2552 let fixture = Fixture::<C>::default();
2553 fixture.start(|_, context, _, mut peers, _, coding_config| async move {
2554 let processed = CodedBlock::<B, C, H>::new(
2557 B::new(Sha256Digest::EMPTY, Height::new(1), 100),
2558 coding_config,
2559 &STRATEGY,
2560 );
2561 let malicious = CodedBlock::<B, C, H>::new(
2562 B::new(Sha256Digest::EMPTY, Height::new(u64::MAX), 100),
2563 coding_config,
2564 &STRATEGY,
2565 );
2566 let equal = CodedBlock::<B, C, H>::new(
2567 B::new(Sha256Digest::EMPTY, Height::new(3), 100),
2568 coding_config,
2569 &STRATEGY,
2570 );
2571 let later = CodedBlock::<B, C, H>::new(
2572 B::new(Sha256Digest::EMPTY, Height::new(2), 100),
2573 coding_config,
2574 &STRATEGY,
2575 );
2576 let processed_commitment = processed.commitment();
2577 let malicious_commitment = malicious.commitment();
2578 let equal_commitment = equal.commitment();
2579 let later_commitment = later.commitment();
2580
2581 let peer = &mut peers[0];
2583 peer.mailbox
2584 .proposed(Round::new(Epoch::zero(), View::new(1)), processed.clone());
2585 peer.mailbox
2586 .proposed(Round::new(Epoch::zero(), View::new(2)), malicious);
2587 peer.mailbox
2588 .proposed(Round::new(Epoch::zero(), View::new(3)), equal);
2589 peer.mailbox
2590 .proposed(Round::new(Epoch::zero(), View::new(1)), later.clone());
2591 peer.mailbox
2594 .proposed(Round::new(Epoch::zero(), View::new(5)), processed);
2595 peer.mailbox
2596 .proposed(Round::new(Epoch::zero(), View::new(4)), later);
2597 context.sleep(Duration::from_millis(10)).await;
2598
2599 assert!(
2601 peer.mailbox.get(processed_commitment).await.is_some(),
2602 "processed block should be cached"
2603 );
2604 assert!(
2605 peer.mailbox.get(malicious_commitment).await.is_some(),
2606 "malicious block should be cached"
2607 );
2608 assert!(
2609 peer.mailbox.get(equal_commitment).await.is_some(),
2610 "equal-round block should be cached"
2611 );
2612 assert!(
2613 peer.mailbox.get(later_commitment).await.is_some(),
2614 "later block should be cached"
2615 );
2616
2617 peer.mailbox.retire(Retirement {
2620 round_floor: Round::new(Epoch::zero(), View::new(3)),
2621 exact_retirements: vec![processed_commitment],
2622 });
2623 context.sleep(Duration::from_millis(10)).await;
2624
2625 assert!(
2626 peer.mailbox.get(processed_commitment).await.is_none(),
2627 "exact processed block should be pruned above the floor"
2628 );
2629 assert!(
2630 peer.mailbox.get(malicious_commitment).await.is_none(),
2631 "earlier malicious block should be pruned"
2632 );
2633 assert!(
2634 peer.mailbox.get(equal_commitment).await.is_none(),
2635 "block observed at the retirement floor should be pruned"
2636 );
2637 assert!(
2638 peer.mailbox.get(later_commitment).await.is_some(),
2639 "non-target block observed above the floor should remain cached"
2640 );
2641 });
2642 }
2643
2644 #[test_traced]
2645 fn test_retire_unseen_commitment_applies_floor() {
2646 let fixture = Fixture::<C>::default();
2647 fixture.start(|_, context, _, mut peers, _, coding_config| async move {
2648 let make_block = |id| {
2649 CodedBlock::<B, C, H>::new(
2650 B::new(Sha256Digest::EMPTY, Height::new(id), id),
2651 coding_config,
2652 &STRATEGY,
2653 )
2654 };
2655 let cached_old = make_block(1);
2656 let cached_equal = make_block(2);
2657 let cached_later = make_block(3);
2658 let state_old = make_block(4).commitment();
2659 let state_equal = make_block(5).commitment();
2660 let state_later = make_block(6).commitment();
2661 let unseen = make_block(7).commitment();
2662 let cached_old_commitment = cached_old.commitment();
2663 let cached_equal_commitment = cached_equal.commitment();
2664 let cached_later_commitment = cached_later.commitment();
2665 let old_round = Round::new(Epoch::zero(), View::new(2));
2666 let floor = Round::new(Epoch::zero(), View::new(3));
2667 let later_round = Round::new(Epoch::zero(), View::new(4));
2668 let leader = peers[1].public_key.clone();
2669 let peer = &mut peers[0];
2670
2671 peer.mailbox.proposed(old_round, cached_old);
2672 peer.mailbox.proposed(floor, cached_equal);
2673 peer.mailbox.proposed(later_round, cached_later);
2674 peer.mailbox
2675 .discovered(state_old, leader.clone(), old_round);
2676 peer.mailbox.discovered(state_equal, leader.clone(), floor);
2677 peer.mailbox
2678 .discovered(state_later, leader.clone(), old_round);
2679 peer.mailbox.discovered(state_later, leader, later_round);
2680 let mut state_old_sub = peer.mailbox.subscribe(state_old);
2681 let mut state_equal_sub = peer.mailbox.subscribe(state_equal);
2682 let mut state_later_sub = peer.mailbox.subscribe(state_later);
2683 context.sleep(Duration::from_millis(10)).await;
2684
2685 peer.mailbox.retire(Retirement {
2686 round_floor: floor,
2687 exact_retirements: vec![unseen],
2688 });
2689 context.sleep(Duration::from_millis(10)).await;
2690
2691 assert!(peer.mailbox.get(cached_old_commitment).await.is_none());
2692 assert!(peer.mailbox.get(cached_equal_commitment).await.is_none());
2693 assert!(peer.mailbox.get(cached_later_commitment).await.is_some());
2694 assert!(matches!(state_old_sub.try_recv(), Err(TryRecvError::Empty)));
2695 assert!(matches!(
2696 state_equal_sub.try_recv(),
2697 Err(TryRecvError::Empty)
2698 ));
2699 assert!(matches!(
2700 state_later_sub.try_recv(),
2701 Err(TryRecvError::Empty)
2702 ));
2703 });
2704 }
2705
2706 #[test_traced]
2707 fn test_local_reproposal_refreshes_existing_reconstruction_state() {
2708 let fixture = Fixture::<C>::default();
2709 fixture.start(|_, context, _, mut peers, _, coding_config| async move {
2710 let live = CodedBlock::<B, C, H>::new(
2711 B::new(Sha256Digest::EMPTY, Height::new(1), 100),
2712 coding_config,
2713 &STRATEGY,
2714 );
2715 let unseen = CodedBlock::<B, C, H>::new(
2716 B::new(Sha256Digest::EMPTY, Height::new(2), 200),
2717 coding_config,
2718 &STRATEGY,
2719 )
2720 .commitment();
2721 let state_first = CodedBlock::<B, C, H>::new(
2722 B::new(Sha256Digest::EMPTY, Height::new(3), 300),
2723 coding_config,
2724 &STRATEGY,
2725 );
2726 let live_commitment = live.commitment();
2727 let state_first_commitment = state_first.commitment();
2728 let leader = peers[0].public_key.clone();
2729 let original_round = Round::new(Epoch::zero(), View::new(1));
2730 let floor = Round::new(Epoch::zero(), View::new(3));
2731 let reproposal_round = Round::new(Epoch::zero(), View::new(4));
2732 let peer = &mut peers[0];
2733
2734 peer.mailbox
2735 .discovered(live_commitment, leader, original_round);
2736 peer.mailbox.proposed(reproposal_round, live);
2737 peer.mailbox
2738 .notarized(state_first_commitment, reproposal_round);
2739 peer.mailbox.proposed(floor, state_first);
2740 assert!(peer.mailbox.get(live_commitment).await.is_some());
2741
2742 let mut shard_sub = peer
2743 .mailbox
2744 .subscribe_assigned_shard_verified(live_commitment);
2745 context.sleep(Duration::from_millis(10)).await;
2746 assert!(
2747 matches!(shard_sub.try_recv(), Ok(())),
2748 "late subscription should resolve after a local reproposal"
2749 );
2750
2751 peer.mailbox.retire(Retirement {
2752 round_floor: floor,
2753 exact_retirements: vec![unseen],
2754 });
2755 context.sleep(Duration::from_millis(10)).await;
2756
2757 assert!(peer.mailbox.get(live_commitment).await.is_some());
2758 assert!(peer.mailbox.get(state_first_commitment).await.is_some());
2759 let mut retained_shard_sub = peer
2760 .mailbox
2761 .subscribe_assigned_shard_verified(live_commitment);
2762 context.sleep(Duration::from_millis(10)).await;
2763 assert!(
2764 matches!(retained_shard_sub.try_recv(), Ok(())),
2765 "retained local proposal should resolve a late subscription"
2766 );
2767 });
2768 }
2769
2770 #[test_traced]
2771 fn test_duplicate_leader_shard_ignored() {
2772 let fixture = Fixture::<C>::default();
2773 fixture.start(
2774 |config, context, oracle, mut peers, _, coding_config| async move {
2775 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
2776 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2777 let commitment = coded_block.commitment();
2778
2779 let peer2_index = peers[2].index.get() as u16;
2781 let peer2_shard = coded_block.shard(peer2_index).expect("missing shard");
2782 let shard_bytes = peer2_shard.encode();
2783
2784 let peer2_pk = peers[2].public_key.clone();
2785 let leader = peers[0].public_key.clone();
2786
2787 peers[2].mailbox.discovered(
2789 commitment,
2790 leader,
2791 Round::new(Epoch::zero(), View::new(1)),
2792 );
2793
2794 peers[0]
2796 .sender
2797 .send(Recipients::One(peer2_pk.clone()), shard_bytes.clone(), true);
2798 context.sleep(config.link.latency * 2).await;
2799
2800 peers[0]
2802 .sender
2803 .send(Recipients::One(peer2_pk), shard_bytes, true);
2804 context.sleep(config.link.latency * 2).await;
2805
2806 let blocked_peers = oracle.blocked().await.unwrap();
2808 let is_blocked = blocked_peers
2809 .iter()
2810 .any(|(a, b)| a == &peers[2].public_key && b == &peers[0].public_key);
2811 assert!(
2812 !is_blocked,
2813 "leader should not be blocked for duplicate shard"
2814 );
2815 },
2816 );
2817 }
2818
2819 #[test_traced]
2820 fn test_equivocating_leader_shard_blocks_peer() {
2821 let fixture = Fixture::<C>::default();
2822 fixture.start(
2823 |config, context, oracle, mut peers, _, coding_config| async move {
2824 let inner1 = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
2825 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
2826 let commitment = coded_block1.commitment();
2827
2828 let inner2 = B::new(Sha256Digest::EMPTY, Height::new(1), 200);
2830 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
2831
2832 let peer2_index = peers[2].index.get() as u16;
2834 let shard_bytes1 = coded_block1
2835 .shard(peer2_index)
2836 .expect("missing shard")
2837 .encode();
2838 let mut equivocating_shard =
2839 coded_block2.shard(peer2_index).expect("missing shard");
2840 equivocating_shard.commitment = commitment;
2842 let shard_bytes2 = equivocating_shard.encode();
2843
2844 let peer2_pk = peers[2].public_key.clone();
2845 let leader = peers[0].public_key.clone();
2846
2847 peers[2].mailbox.discovered(
2849 commitment,
2850 leader,
2851 Round::new(Epoch::zero(), View::new(1)),
2852 );
2853
2854 peers[0]
2856 .sender
2857 .send(Recipients::One(peer2_pk.clone()), shard_bytes1, true);
2858 context.sleep(config.link.latency * 2).await;
2859
2860 peers[0]
2862 .sender
2863 .send(Recipients::One(peer2_pk), shard_bytes2, true);
2864 context.sleep(config.link.latency * 2).await;
2865
2866 assert_blocked(&oracle, &peers[2].public_key, &peers[0].public_key).await;
2868 },
2869 );
2870 }
2871
2872 #[test_traced]
2873 fn test_non_leader_wrong_index_shard_blocked() {
2874 let fixture = Fixture::<C>::default();
2877 fixture.start(
2878 |config, context, oracle, mut peers, _, coding_config| async move {
2879 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
2880 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2881 let commitment = coded_block.commitment();
2882
2883 let unrelated_index = peers[3].index.get() as u16;
2885 let unrelated_shard = coded_block.shard(unrelated_index).expect("missing shard");
2886 let shard_bytes = unrelated_shard.encode();
2887
2888 let peer2_pk = peers[2].public_key.clone();
2889 let leader = peers[0].public_key.clone();
2890
2891 peers[2].mailbox.discovered(
2893 commitment,
2894 leader,
2895 Round::new(Epoch::zero(), View::new(1)),
2896 );
2897
2898 peers[1]
2901 .sender
2902 .send(Recipients::One(peer2_pk), shard_bytes, true);
2903 context.sleep(config.link.latency * 2).await;
2904
2905 assert_blocked(&oracle, &peers[2].public_key, &peers[1].public_key).await;
2907 },
2908 );
2909 }
2910
2911 #[test_traced]
2912 fn test_buffered_wrong_index_shard_blocked_on_leader_arrival() {
2913 let fixture = Fixture::<C>::default();
2916 fixture.start(
2917 |config, context, oracle, mut peers, _, coding_config| async move {
2918 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
2919 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2920 let commitment = coded_block.commitment();
2921
2922 let unrelated_index = peers[3].index.get() as u16;
2924 let unrelated_shard = coded_block.shard(unrelated_index).expect("missing shard");
2925 let shard_bytes = unrelated_shard.encode();
2926
2927 let peer2_pk = peers[2].public_key.clone();
2928
2929 peers[1]
2931 .sender
2932 .send(Recipients::One(peer2_pk), shard_bytes, true);
2933 context.sleep(config.link.latency * 2).await;
2934
2935 let blocked = oracle.blocked().await.unwrap();
2937 assert!(
2938 blocked.is_empty(),
2939 "no peers should be blocked while leader is unknown"
2940 );
2941
2942 let leader = peers[0].public_key.clone();
2946 peers[2].mailbox.discovered(
2947 commitment,
2948 leader,
2949 Round::new(Epoch::zero(), View::new(1)),
2950 );
2951 context.sleep(Duration::from_millis(10)).await;
2952
2953 assert_blocked(&oracle, &peers[2].public_key, &peers[1].public_key).await;
2954 },
2955 );
2956 }
2957
2958 #[test_traced]
2959 fn test_assigned_shard_from_non_leader_accepted() {
2960 let fixture = Fixture::<C>::default();
2961 fixture.start(
2962 |_config, context, oracle, mut peers, _, coding_config| async move {
2963 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
2964 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2965 let commitment = coded_block.commitment();
2966
2967 let peer2_index = peers[2].index.get() as u16;
2969 let peer2_shard = coded_block.shard(peer2_index).expect("missing shard");
2970 let shard_bytes = peer2_shard.encode();
2971
2972 let peer2_pk = peers[2].public_key.clone();
2973 let leader = peers[0].public_key.clone();
2974
2975 let shard_sub = peers[2]
2977 .mailbox
2978 .subscribe_assigned_shard_verified(commitment);
2979
2980 peers[2].mailbox.discovered(
2982 commitment,
2983 leader,
2984 Round::new(Epoch::zero(), View::new(1)),
2985 );
2986
2987 peers[1]
2990 .sender
2991 .send(Recipients::One(peer2_pk), shard_bytes, true);
2992
2993 select! {
2994 _ = shard_sub => {},
2995 _ = context.sleep(Duration::from_secs(5)) => {
2996 panic!("subscription did not complete after assigned shard");
2997 },
2998 };
2999
3000 assert!(oracle.blocked().await.unwrap().is_empty());
3001 },
3002 );
3003 }
3004
3005 #[test_traced]
3006 fn test_non_participant_external_proposed_ignored() {
3007 let fixture = Fixture::<C>::default();
3008 fixture.start(
3009 |config, context, oracle, mut peers, _, coding_config| async move {
3010 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
3011 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
3012 let commitment = coded_block.commitment();
3013
3014 let peer2_index = peers[2].index.get() as u16;
3016 let peer2_shard = coded_block.shard(peer2_index).expect("missing shard");
3017 let shard_bytes = peer2_shard.encode();
3018
3019 let peer2_pk = peers[2].public_key.clone();
3020 let leader = peers[0].public_key.clone();
3021 let non_participant_leader = PrivateKey::from_seed(10_000).public_key();
3022
3023 let shard_sub = peers[2]
3025 .mailbox
3026 .subscribe_assigned_shard_verified(commitment);
3027
3028 peers[2].mailbox.discovered(
3030 commitment,
3031 non_participant_leader,
3032 Round::new(Epoch::zero(), View::new(1)),
3033 );
3034
3035 peers[0]
3037 .sender
3038 .send(Recipients::One(peer2_pk.clone()), shard_bytes.clone(), true);
3039 context.sleep(config.link.latency * 2).await;
3040
3041 let blocked = oracle.blocked().await.unwrap();
3042 let leader_blocked = blocked
3043 .iter()
3044 .any(|(a, b)| a == &peers[2].public_key && b == &leader);
3045 assert!(
3046 !leader_blocked,
3047 "leader should not be blocked when non-participant update is ignored"
3048 );
3049
3050 peers[2].mailbox.discovered(
3052 commitment,
3053 leader,
3054 Round::new(Epoch::zero(), View::new(1)),
3055 );
3056 context.sleep(config.link.latency * 2).await;
3057
3058 select! {
3059 _ = shard_sub => {},
3060 _ = context.sleep(Duration::from_secs(5)) => {
3061 panic!("subscription did not complete after valid leader update");
3062 },
3063 };
3064 },
3065 );
3066 }
3067
3068 #[test_traced]
3069 fn test_rejected_leader_does_not_refresh_retirement_round() {
3070 let fixture = Fixture::<C>::default();
3071 fixture.start(|_, context, _, mut peers, _, coding_config| async move {
3072 let block = CodedBlock::<B, C, H>::new(
3073 B::new(Sha256Digest::EMPTY, Height::new(1), 100),
3074 coding_config,
3075 &STRATEGY,
3076 );
3077 let commitment = block.commitment();
3078 let unrelated = CodedBlock::<B, C, H>::new(
3079 B::new(Sha256Digest::EMPTY, Height::new(2), 200),
3080 coding_config,
3081 &STRATEGY,
3082 )
3083 .commitment();
3084 let leader = peers[0].public_key.clone();
3085 let non_participant = PrivateKey::from_seed(10_000).public_key();
3086 let receiver = &mut peers[2];
3087
3088 let mut subscription = receiver
3089 .mailbox
3090 .subscribe_assigned_shard_verified(commitment);
3091 receiver.mailbox.discovered(
3092 commitment,
3093 leader,
3094 Round::new(Epoch::zero(), View::new(1)),
3095 );
3096 receiver.mailbox.discovered(
3097 commitment,
3098 non_participant,
3099 Round::new(Epoch::zero(), View::new(10)),
3100 );
3101 receiver.mailbox.retire(Retirement {
3102 round_floor: Round::new(Epoch::zero(), View::new(5)),
3103 exact_retirements: vec![unrelated],
3104 });
3105 context.sleep(Duration::from_millis(10)).await;
3106
3107 assert!(matches!(subscription.try_recv(), Err(TryRecvError::Closed)));
3108 });
3109 }
3110
3111 #[test_traced]
3112 fn test_cross_epoch_observations_do_not_refresh_retirement_round() {
3113 let fixture = Fixture::<C> {
3114 additional_scheme_epochs: vec![Epoch::new(1)],
3115 ..Default::default()
3116 };
3117 fixture.start(|_, context, _, mut peers, _, coding_config| async move {
3118 let cached = CodedBlock::<B, C, H>::new(
3119 B::new(Sha256Digest::EMPTY, Height::new(1), 100),
3120 coding_config,
3121 &STRATEGY,
3122 );
3123 let incomplete = CodedBlock::<B, C, H>::new(
3124 B::new(Sha256Digest::EMPTY, Height::new(2), 200),
3125 coding_config,
3126 &STRATEGY,
3127 );
3128 let incomplete_commitment = incomplete.commitment();
3129 let unrelated = CodedBlock::<B, C, H>::new(
3130 B::new(Sha256Digest::EMPTY, Height::new(3), 300),
3131 coding_config,
3132 &STRATEGY,
3133 )
3134 .commitment();
3135 let cached_commitment = cached.commitment();
3136 let leader = peers[0].public_key.clone();
3137 let receiver = &mut peers[2];
3138 let original_round = Round::new(Epoch::zero(), View::new(1));
3139
3140 receiver.mailbox.proposed(original_round, cached);
3141 receiver
3142 .mailbox
3143 .discovered(incomplete_commitment, leader, original_round);
3144 let mut subscription = receiver
3145 .mailbox
3146 .subscribe_assigned_shard_verified(incomplete_commitment);
3147
3148 let conflicting_round = Round::new(Epoch::new(1), View::new(10));
3149 receiver.mailbox.proposed(conflicting_round, incomplete);
3150 receiver
3151 .mailbox
3152 .notarized(cached_commitment, conflicting_round);
3153 receiver
3154 .mailbox
3155 .notarized(incomplete_commitment, conflicting_round);
3156 receiver.mailbox.retire(Retirement {
3157 round_floor: Round::new(Epoch::zero(), View::new(5)),
3158 exact_retirements: vec![unrelated],
3159 });
3160 context.sleep(Duration::from_millis(10)).await;
3161
3162 assert!(receiver.mailbox.get(cached_commitment).await.is_none());
3163 assert!(matches!(subscription.try_recv(), Err(TryRecvError::Closed)));
3164 });
3165 }
3166
3167 #[test_traced]
3168 fn test_shard_from_non_participant_blocks_peer() {
3169 let fixture = Fixture {
3170 num_future_peers: 1,
3171 ..Fixture::<C>::default()
3172 };
3173 fixture.start(
3174 |config, context, oracle, peers, _, coding_config| async move {
3175 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
3176 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
3177 let commitment = coded_block.commitment();
3178
3179 let leader = peers[0].public_key.clone();
3180 let receiver_pk = peers[2].public_key.clone();
3181
3182 let non_participant_key = PrivateKey::from_seed(10_000);
3183 let non_participant_pk = non_participant_key.public_key();
3184
3185 let non_participant_control = oracle.control(non_participant_pk.clone());
3186 let (mut non_participant_sender, _non_participant_receiver) =
3187 non_participant_control
3188 .register(0, TEST_QUOTA)
3189 .await
3190 .expect("registration should succeed");
3191 oracle
3192 .add_link(
3193 non_participant_pk.clone(),
3194 receiver_pk.clone(),
3195 DEFAULT_LINK,
3196 )
3197 .await
3198 .expect("link should be added");
3199 oracle.manager().track(
3200 2,
3201 TrackedPeers::new(
3202 Set::from_iter_dedup(peers.iter().map(|peer| peer.public_key.clone())),
3203 Set::from_iter_dedup([non_participant_pk.clone()]),
3204 ),
3205 );
3206 context.sleep(Duration::from_millis(10)).await;
3207
3208 peers[2].mailbox.discovered(
3209 commitment,
3210 leader,
3211 Round::new(Epoch::zero(), View::new(1)),
3212 );
3213
3214 let peer2_index = peers[2].index.get() as u16;
3215 let shard = coded_block.shard(peer2_index).expect("missing shard");
3216 let shard_bytes = shard.encode();
3217
3218 non_participant_sender.send(Recipients::One(receiver_pk), shard_bytes, true);
3219 context.sleep(config.link.latency * 2).await;
3220
3221 assert_blocked(&oracle, &peers[2].public_key, &non_participant_pk).await;
3222 },
3223 );
3224 }
3225
3226 #[test_traced]
3227 fn test_preleader_shard_from_non_participant_is_not_buffered() {
3228 let fixture = Fixture {
3229 num_future_peers: 1,
3230 ..Fixture::<C>::default()
3231 };
3232 fixture.start(
3233 |config, context, oracle, peers, _, coding_config| async move {
3234 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
3235 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
3236 let commitment = coded_block.commitment();
3237
3238 let leader = peers[0].public_key.clone();
3239 let receiver_pk = peers[2].public_key.clone();
3240
3241 let non_participant_key = PrivateKey::from_seed(10_000);
3242 let non_participant_pk = non_participant_key.public_key();
3243
3244 let non_participant_control = oracle.control(non_participant_pk.clone());
3245 let (mut non_participant_sender, _non_participant_receiver) =
3246 non_participant_control
3247 .register(0, TEST_QUOTA)
3248 .await
3249 .expect("registration should succeed");
3250 oracle
3251 .add_link(
3252 non_participant_pk.clone(),
3253 receiver_pk.clone(),
3254 DEFAULT_LINK,
3255 )
3256 .await
3257 .expect("link should be added");
3258 oracle.manager().track(
3259 2,
3260 TrackedPeers::new(
3261 Set::from_iter_dedup(peers.iter().map(|peer| peer.public_key.clone())),
3262 Set::from_iter_dedup([non_participant_pk.clone()]),
3263 ),
3264 );
3265 context.sleep(Duration::from_millis(10)).await;
3266
3267 let peer2_index = peers[2].index.get() as u16;
3268 let shard = coded_block.shard(peer2_index).expect("missing shard");
3269 let shard_bytes = shard.encode();
3270 let mut shard_sub = peers[2]
3271 .mailbox
3272 .subscribe_assigned_shard_verified(commitment);
3273
3274 non_participant_sender.send(Recipients::One(receiver_pk), shard_bytes, true);
3275 context.sleep(config.link.latency * 2).await;
3276
3277 peers[2].mailbox.discovered(
3278 commitment,
3279 leader,
3280 Round::new(Epoch::zero(), View::new(1)),
3281 );
3282 context.sleep(config.link.latency * 2).await;
3283
3284 let blocked = oracle.blocked().await.unwrap();
3285 let non_participant_blocked = blocked
3286 .iter()
3287 .any(|(a, b)| a == &peers[2].public_key && b == &non_participant_pk);
3288 assert!(
3289 !non_participant_blocked,
3290 "non-participant should not be blocked when its pre-leader shard is ignored"
3291 );
3292 assert!(
3293 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
3294 "pre-leader shard from non-participant should not be buffered"
3295 );
3296 },
3297 );
3298 }
3299
3300 #[test_traced]
3301 fn test_duplicate_shard_ignored() {
3302 let fixture: Fixture<C> = Fixture {
3304 num_primary_peers: 10,
3305 ..Default::default()
3306 };
3307
3308 fixture.start(
3309 |config, context, oracle, mut peers, _, coding_config| async move {
3310 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
3311 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
3312
3313 let peer2_index = peers[2].index.get() as u16;
3315 let peer2_shard = coded_block.shard(peer2_index).expect("missing shard");
3316
3317 let peer1_index = peers[1].index.get() as u16;
3319 let peer1_shard = coded_block.shard(peer1_index).expect("missing shard");
3320
3321 let peer2_pk = peers[2].public_key.clone();
3322 let leader = peers[0].public_key.clone();
3323
3324 peers[2].mailbox.discovered(
3326 coded_block.commitment(),
3327 leader,
3328 Round::new(Epoch::zero(), View::new(1)),
3329 );
3330
3331 let leader_shard_bytes = peer2_shard.encode();
3333 peers[0]
3334 .sender
3335 .send(Recipients::One(peer2_pk.clone()), leader_shard_bytes, true);
3336 context.sleep(config.link.latency * 2).await;
3337
3338 let peer1_shard_bytes = peer1_shard.encode();
3340 peers[1].sender.send(
3341 Recipients::One(peer2_pk.clone()),
3342 peer1_shard_bytes.clone(),
3343 true,
3344 );
3345 context.sleep(config.link.latency * 2).await;
3346
3347 peers[1]
3350 .sender
3351 .send(Recipients::One(peer2_pk), peer1_shard_bytes, true);
3352 context.sleep(config.link.latency * 2).await;
3353
3354 let blocked_peers = oracle.blocked().await.unwrap();
3356 let is_blocked = blocked_peers
3357 .iter()
3358 .any(|(a, b)| a == &peers[2].public_key && b == &peers[1].public_key);
3359 assert!(
3360 !is_blocked,
3361 "peer should not be blocked for exact duplicate shard"
3362 );
3363 },
3364 );
3365 }
3366
3367 #[test_traced]
3368 fn test_equivocating_shard_blocks_peer() {
3369 let fixture: Fixture<C> = Fixture {
3371 num_primary_peers: 10,
3372 ..Default::default()
3373 };
3374
3375 fixture.start(
3376 |config, context, oracle, mut peers, _, coding_config| async move {
3377 let inner1 = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
3378 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
3379
3380 let inner2 = B::new(Sha256Digest::EMPTY, Height::new(1), 200);
3382 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
3383
3384 let peer1_index = peers[1].index.get() as u16;
3386 let peer1_shard = coded_block1.shard(peer1_index).expect("missing shard");
3387
3388 let mut peer1_equivocating_shard =
3390 coded_block2.shard(peer1_index).expect("missing shard");
3391 peer1_equivocating_shard.commitment = coded_block1.commitment();
3394
3395 let peer2_pk = peers[2].public_key.clone();
3396 let leader = peers[0].public_key.clone();
3397
3398 peers[2].mailbox.discovered(
3400 coded_block1.commitment(),
3401 leader,
3402 Round::new(Epoch::zero(), View::new(1)),
3403 );
3404
3405 let peer2_index = peers[2].index.get() as u16;
3407 let leader_shard = coded_block1.shard(peer2_index).expect("missing shard");
3408 let leader_shard_bytes = leader_shard.encode();
3409 peers[0]
3410 .sender
3411 .send(Recipients::One(peer2_pk.clone()), leader_shard_bytes, true);
3412 context.sleep(config.link.latency * 2).await;
3413
3414 let shard_bytes = peer1_shard.encode();
3416 peers[1]
3417 .sender
3418 .send(Recipients::One(peer2_pk.clone()), shard_bytes, true);
3419 context.sleep(config.link.latency * 2).await;
3420
3421 let equivocating_bytes = peer1_equivocating_shard.encode();
3423 peers[1]
3424 .sender
3425 .send(Recipients::One(peer2_pk), equivocating_bytes, true);
3426 context.sleep(config.link.latency * 2).await;
3427
3428 assert_blocked(&oracle, &peers[2].public_key, &peers[1].public_key).await;
3430 },
3431 );
3432 }
3433
3434 #[test_traced]
3435 fn test_reconstruction_states_pruned_at_or_below_reconstructed_view() {
3436 let fixture: Fixture<C> = Fixture {
3438 num_primary_peers: 10,
3439 ..Default::default()
3440 };
3441
3442 fixture.start(
3443 |config, context, oracle, mut peers, _, coding_config| async move {
3444 let block_a = CodedBlock::<B, C, H>::new(
3446 B::new(Sha256Digest::EMPTY, Height::new(1), 100),
3447 coding_config,
3448 &STRATEGY,
3449 );
3450 let commitment_a = block_a.commitment();
3451
3452 let block_b = CodedBlock::<B, C, H>::new(
3454 B::new(Sha256Digest::EMPTY, Height::new(2), 200),
3455 coding_config,
3456 &STRATEGY,
3457 );
3458 let commitment_b = block_b.commitment();
3459
3460 let peer2_pk = peers[2].public_key.clone();
3461 let leader = peers[0].public_key.clone();
3462
3463 peers[2].mailbox.discovered(
3465 commitment_a,
3466 leader.clone(),
3467 Round::new(Epoch::zero(), View::new(1)),
3468 );
3469 let shard_a = block_a
3470 .shard(peers[1].index.get() as u16)
3471 .expect("missing shard")
3472 .encode();
3473 peers[1]
3474 .sender
3475 .send(Recipients::One(peer2_pk.clone()), shard_a.clone(), true);
3476 context.sleep(config.link.latency * 2).await;
3477
3478 peers[2].mailbox.discovered(
3480 commitment_b,
3481 leader,
3482 Round::new(Epoch::zero(), View::new(2)),
3483 );
3484 let leader_shard_b = block_b
3486 .shard(peers[2].index.get() as u16)
3487 .expect("missing shard")
3488 .encode();
3489 peers[0]
3490 .sender
3491 .send(Recipients::One(peer2_pk.clone()), leader_shard_b, true);
3492
3493 for i in [1usize, 3usize, 4usize] {
3495 let shard = block_b
3496 .shard(peers[i].index.get() as u16)
3497 .expect("missing shard")
3498 .encode();
3499 peers[i]
3500 .sender
3501 .send(Recipients::One(peer2_pk.clone()), shard, true);
3502 }
3503 context.sleep(config.link.latency * 4).await;
3504
3505 let reconstructed = peers[2]
3507 .mailbox
3508 .get(commitment_b)
3509 .await
3510 .expect("block B should reconstruct");
3511 assert_eq!(reconstructed.commitment(), commitment_b);
3512
3513 peers[1]
3516 .sender
3517 .send(Recipients::One(peer2_pk), shard_a, true);
3518 context.sleep(config.link.latency * 2).await;
3519
3520 let blocked = oracle.blocked().await.unwrap();
3521 let blocked_peer1 = blocked
3522 .iter()
3523 .any(|(a, b)| a == &peers[2].public_key && b == &peers[1].public_key);
3524 assert!(
3525 !blocked_peer1,
3526 "peer1 should not be blocked after lower-view state was pruned"
3527 );
3528 },
3529 );
3530 }
3531
3532 #[test_traced]
3533 fn test_later_notarization_refreshes_reconstruction_state_round() {
3534 let fixture: Fixture<C> = Fixture {
3535 num_primary_peers: 10,
3536 ..Default::default()
3537 };
3538
3539 fixture.start(
3540 |config, context, _, mut peers, _, coding_config| async move {
3541 let live = CodedBlock::<B, C, H>::new(
3542 B::new(Sha256Digest::EMPTY, Height::new(2), 200),
3543 coding_config,
3544 &STRATEGY,
3545 );
3546 let finalized = CodedBlock::<B, C, H>::new(
3547 B::new(Sha256Digest::EMPTY, Height::new(1), 100),
3548 coding_config,
3549 &STRATEGY,
3550 );
3551 let live_commitment = live.commitment();
3552 let finalized_commitment = finalized.commitment();
3553 let receiver_idx = 3usize;
3554 let receiver_pk = peers[receiver_idx].public_key.clone();
3555 let leader = peers[0].public_key.clone();
3556
3557 peers[receiver_idx].mailbox.discovered(
3558 live_commitment,
3559 leader,
3560 Round::new(Epoch::zero(), View::new(1)),
3561 );
3562 let mut live_sub = peers[receiver_idx].mailbox.subscribe(live_commitment);
3563
3564 peers[receiver_idx]
3567 .mailbox
3568 .notarized(live_commitment, Round::new(Epoch::zero(), View::new(4)));
3569 context.sleep(Duration::from_millis(10)).await;
3570 peers[receiver_idx].mailbox.retire(Retirement {
3571 round_floor: Round::new(Epoch::zero(), View::new(3)),
3572 exact_retirements: vec![finalized_commitment],
3573 });
3574 context.sleep(Duration::from_millis(10)).await;
3575
3576 assert!(
3577 matches!(live_sub.try_recv(), Err(TryRecvError::Empty)),
3578 "later-round reconstruction subscription should remain open"
3579 );
3580
3581 let leader_shard = live
3582 .shard(peers[receiver_idx].index.get() as u16)
3583 .expect("missing leader shard");
3584 peers[0].sender.send(
3585 Recipients::One(receiver_pk.clone()),
3586 leader_shard.encode(),
3587 true,
3588 );
3589 for i in [1usize, 2usize, 4usize] {
3590 let shard = live
3591 .shard(peers[i].index.get() as u16)
3592 .expect("missing gossip shard");
3593 peers[i].sender.send(
3594 Recipients::One(receiver_pk.clone()),
3595 shard.encode(),
3596 true,
3597 );
3598 }
3599
3600 select! {
3601 result = live_sub => {
3602 let reconstructed =
3603 result.expect("later-round reconstruction should remain live");
3604 assert_eq!(reconstructed.commitment(), live_commitment);
3605 },
3606 _ = context.sleep(config.link.latency * 10) => {
3607 panic!("later-round reconstruction did not complete");
3608 },
3609 }
3610 },
3611 );
3612 }
3613
3614 #[test_traced]
3615 fn test_cached_observations_refresh_reconstruction_state_round() {
3616 let fixture: Fixture<C> = Fixture {
3617 num_primary_peers: 10,
3618 ..Default::default()
3619 };
3620
3621 fixture.start(
3622 |config, context, _, mut peers, _, coding_config| async move {
3623 let live = CodedBlock::<B, C, H>::new(
3624 B::new(Sha256Digest::EMPTY, Height::new(2), 200),
3625 coding_config,
3626 &STRATEGY,
3627 );
3628 let finalized = CodedBlock::<B, C, H>::new(
3629 B::new(Sha256Digest::EMPTY, Height::new(1), 100),
3630 coding_config,
3631 &STRATEGY,
3632 );
3633 let live_commitment = live.commitment();
3634 let finalized_commitment = finalized.commitment();
3635 let leader = peers[0].public_key.clone();
3636 let receivers = [3usize, 6usize];
3637 let receiver_keys = [
3638 peers[receivers[0]].public_key.clone(),
3639 peers[receivers[1]].public_key.clone(),
3640 ];
3641 let original_round = Round::new(Epoch::zero(), View::new(1));
3642
3643 for &receiver_idx in &receivers {
3644 peers[receiver_idx].mailbox.discovered(
3645 live_commitment,
3646 leader.clone(),
3647 original_round,
3648 );
3649 }
3650
3651 for sender_idx in [1usize, 2usize, 4usize, 5usize] {
3653 let shard = live
3654 .shard(peers[sender_idx].index.get() as u16)
3655 .expect("missing gossip shard")
3656 .encode();
3657 for receiver in &receiver_keys {
3658 peers[sender_idx].sender.send(
3659 Recipients::One(receiver.clone()),
3660 shard.clone(),
3661 true,
3662 );
3663 }
3664 }
3665 context.sleep(config.link.latency * 4).await;
3666
3667 for &receiver_idx in &receivers {
3668 assert!(
3669 peers[receiver_idx]
3670 .mailbox
3671 .get(live_commitment)
3672 .await
3673 .is_some(),
3674 "block should be cached before its round is refreshed"
3675 );
3676 }
3677
3678 let mut notarized_sub = peers[receivers[0]]
3679 .mailbox
3680 .subscribe_assigned_shard_verified(live_commitment);
3681 let mut discovered_sub = peers[receivers[1]]
3682 .mailbox
3683 .subscribe_assigned_shard_verified(live_commitment);
3684 let later_round = Round::new(Epoch::zero(), View::new(4));
3685 peers[receivers[0]]
3686 .mailbox
3687 .notarized(live_commitment, later_round);
3688 peers[receivers[1]].mailbox.discovered(
3689 live_commitment,
3690 leader,
3691 later_round,
3692 );
3693 context.sleep(Duration::from_millis(10)).await;
3694
3695 let prune_round = Round::new(Epoch::zero(), View::new(3));
3696 for &receiver_idx in &receivers {
3697 peers[receiver_idx].mailbox.retire(Retirement {
3698 round_floor: prune_round,
3699 exact_retirements: vec![finalized_commitment],
3700 });
3701 }
3702 context.sleep(Duration::from_millis(10)).await;
3703
3704 assert!(
3705 matches!(notarized_sub.try_recv(), Err(TryRecvError::Empty)),
3706 "cached notarization should keep reconstruction state live"
3707 );
3708 assert!(
3709 matches!(discovered_sub.try_recv(), Err(TryRecvError::Empty)),
3710 "cached discovery should keep reconstruction state live"
3711 );
3712 for &receiver_idx in &receivers {
3713 assert!(
3714 peers[receiver_idx]
3715 .mailbox
3716 .get(live_commitment)
3717 .await
3718 .is_some(),
3719 "cached observation should keep the reconstructed block live"
3720 );
3721 }
3722
3723 for (&receiver_idx, receiver) in receivers.iter().zip(&receiver_keys) {
3724 let leader_shard = live
3725 .shard(peers[receiver_idx].index.get() as u16)
3726 .expect("missing leader shard");
3727 peers[0].sender.send(
3728 Recipients::One(receiver.clone()),
3729 leader_shard.encode(),
3730 true,
3731 );
3732 }
3733
3734 select! {
3735 result = notarized_sub => {
3736 result.expect("notarized reconstruction state should accept the leader shard");
3737 },
3738 _ = context.sleep(config.link.latency * 10) => {
3739 panic!("notarized reconstruction state did not accept the leader shard");
3740 },
3741 }
3742 select! {
3743 result = discovered_sub => {
3744 result.expect("discovered reconstruction state should accept the leader shard");
3745 },
3746 _ = context.sleep(config.link.latency * 10) => {
3747 panic!("discovered reconstruction state did not accept the leader shard");
3748 },
3749 }
3750 },
3751 );
3752 }
3753
3754 #[test_traced]
3755 fn test_local_proposal_prune_clears_older_reconstruction_state() {
3756 let fixture: Fixture<C> = Fixture {
3757 num_primary_peers: 10,
3758 ..Default::default()
3759 };
3760
3761 fixture.start(
3762 |config, context, oracle, mut peers, _, coding_config| async move {
3763 let block_a = CodedBlock::<B, C, H>::new(
3764 B::new(Sha256Digest::EMPTY, Height::new(1), 100),
3765 coding_config,
3766 &STRATEGY,
3767 );
3768 let commitment_a = block_a.commitment();
3769
3770 let block_b = CodedBlock::<B, C, H>::new(
3771 B::new(Sha256Digest::EMPTY, Height::new(2), 200),
3772 coding_config,
3773 &STRATEGY,
3774 );
3775 let commitment_b = block_b.commitment();
3776
3777 let peer2_pk = peers[2].public_key.clone();
3778 let leader = peers[0].public_key.clone();
3779 let round_a = Round::new(Epoch::zero(), View::new(1));
3780 let round_b = Round::new(Epoch::zero(), View::new(2));
3781
3782 peers[2].mailbox.discovered(commitment_a, leader, round_a);
3783
3784 let peer1_index = peers[1].index.get() as u16;
3785 let shard_a = block_a.shard(peer1_index).expect("missing shard");
3786
3787 let block_a_equivocating = CodedBlock::<B, C, H>::new(
3788 B::new(Sha256Digest::EMPTY, Height::new(1), 300),
3789 coding_config,
3790 &STRATEGY,
3791 );
3792 let mut equivocating_shard = block_a_equivocating
3793 .shard(peer1_index)
3794 .expect("missing shard");
3795 equivocating_shard.commitment = commitment_a;
3796
3797 peers[1]
3798 .sender
3799 .send(Recipients::One(peer2_pk.clone()), shard_a.encode(), true);
3800 context.sleep(config.link.latency * 2).await;
3801
3802 peers[2].mailbox.proposed(round_b, block_b);
3803 assert!(
3804 peers[2].mailbox.get(commitment_b).await.is_some(),
3805 "local proposal should be cached before pruning"
3806 );
3807 peers[2].mailbox.retire(Retirement {
3808 round_floor: round_b,
3809 exact_retirements: vec![commitment_b],
3810 });
3811
3812 peers[1]
3813 .sender
3814 .send(Recipients::One(peer2_pk), equivocating_shard.encode(), true);
3815 context.sleep(config.link.latency * 2).await;
3816
3817 let blocked = oracle.blocked().await.unwrap();
3818 let blocked_peer1 = blocked
3819 .iter()
3820 .any(|(a, b)| a == &peers[2].public_key && b == &peers[1].public_key);
3821 assert!(
3822 !blocked_peer1,
3823 "peer1 should not be blocked after older state was pruned"
3824 );
3825 },
3826 );
3827 }
3828
3829 #[test_traced]
3830 fn test_pending_shards_batch_validated_at_quorum() {
3831 let fixture: Fixture<C> = Fixture {
3840 num_primary_peers: 10,
3841 ..Default::default()
3842 };
3843
3844 fixture.start(
3845 |config, context, oracle, mut peers, _, coding_config| async move {
3846 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
3847 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
3848 let commitment = coded_block.commitment();
3849
3850 let peer3_pk = peers[3].public_key.clone();
3851 let leader = peers[0].public_key.clone();
3852
3853 peers[3].mailbox.discovered(
3855 commitment,
3856 leader,
3857 Round::new(Epoch::zero(), View::new(1)),
3858 );
3859
3860 for &sender_idx in &[1, 2, 4] {
3863 let shard = coded_block
3864 .shard(peers[sender_idx].index.get() as u16)
3865 .expect("missing shard");
3866 let shard_bytes = shard.encode();
3867 peers[sender_idx].sender.send(
3868 Recipients::One(peer3_pk.clone()),
3869 shard_bytes,
3870 true,
3871 );
3872 }
3873
3874 context.sleep(config.link.latency * 2).await;
3875
3876 let block = peers[3].mailbox.get(commitment).await;
3878 assert!(block.is_none(), "block should not be reconstructed yet");
3879
3880 let peer3_index = peers[3].index.get() as u16;
3884 let leader_shard = coded_block.shard(peer3_index).expect("missing shard");
3885 let leader_shard_bytes = leader_shard.encode();
3886 peers[0]
3887 .sender
3888 .send(Recipients::One(peer3_pk), leader_shard_bytes, true);
3889
3890 context.sleep(config.link.latency * 2).await;
3891
3892 let blocked = oracle.blocked().await.unwrap();
3894 assert!(
3895 blocked.is_empty(),
3896 "no peers should be blocked for valid pending shards"
3897 );
3898
3899 let block = peers[3].mailbox.get(commitment).await;
3901 assert!(
3902 block.is_some(),
3903 "block should be reconstructed after batch validation"
3904 );
3905
3906 let reconstructed = block.unwrap();
3908 assert_eq!(
3909 reconstructed.commitment(),
3910 commitment,
3911 "reconstructed block should have correct commitment"
3912 );
3913 },
3914 );
3915 }
3916
3917 #[test_traced]
3918 fn test_peer_shards_buffered_until_external_proposed() {
3919 let fixture: Fixture<C> = Fixture {
3922 num_primary_peers: 10,
3923 ..Default::default()
3924 };
3925
3926 fixture.start(
3927 |config, context, oracle, mut peers, _, coding_config| async move {
3928 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
3929 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
3930 let commitment = coded_block.commitment();
3931
3932 let receiver_idx = 3usize;
3933 let receiver_pk = peers[receiver_idx].public_key.clone();
3934 let leader = peers[0].public_key.clone();
3935
3936 let mut shard_sub = peers[receiver_idx]
3938 .mailbox
3939 .subscribe_assigned_shard_verified(commitment);
3940
3941 let leader_shard = coded_block
3944 .shard(peers[receiver_idx].index.get() as u16)
3945 .expect("missing shard")
3946 .encode();
3947 peers[0]
3948 .sender
3949 .send(Recipients::One(receiver_pk.clone()), leader_shard, true);
3950
3951 for i in [1usize, 2usize, 4usize] {
3952 let shard = coded_block
3953 .shard(peers[i].index.get() as u16)
3954 .expect("missing shard")
3955 .encode();
3956 peers[i]
3957 .sender
3958 .send(Recipients::One(receiver_pk.clone()), shard, true);
3959 }
3960
3961 context.sleep(config.link.latency * 2).await;
3962
3963 assert!(
3965 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
3966 "shard subscription should not resolve before leader announcement"
3967 );
3968 assert!(
3969 peers[receiver_idx].mailbox.get(commitment).await.is_none(),
3970 "block should not reconstruct before leader announcement"
3971 );
3972
3973 peers[receiver_idx].mailbox.discovered(
3975 commitment,
3976 leader,
3977 Round::new(Epoch::zero(), View::new(1)),
3978 );
3979
3980 select! {
3981 _ = shard_sub => {},
3982 _ = context.sleep(Duration::from_secs(5)) => {
3983 panic!("shard subscription did not resolve after leader announcement");
3984 },
3985 }
3986
3987 context.sleep(config.link.latency * 2).await;
3988 assert!(
3989 peers[receiver_idx].mailbox.get(commitment).await.is_some(),
3990 "block should reconstruct after buffered shards are ingested"
3991 );
3992
3993 assert!(
3995 oracle.blocked().await.unwrap().is_empty(),
3996 "no peers should be blocked for valid buffered shards"
3997 );
3998 },
3999 );
4000 }
4001
4002 #[test_traced]
4003 fn test_notarized_commitment_reconstructs_from_buffered_peer_shards_without_leader() {
4004 let fixture: Fixture<C> = Fixture {
4005 num_primary_peers: 10,
4006 ..Default::default()
4007 };
4008
4009 fixture.start(
4010 |config, context, oracle, mut peers, _, coding_config| async move {
4011 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
4012 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4013 let commitment = coded_block.commitment();
4014 let round = Round::new(Epoch::zero(), View::new(1));
4015
4016 let receiver_idx = 3usize;
4017 let receiver = peers[receiver_idx].public_key.clone();
4018
4019 let block_sub = peers[receiver_idx].mailbox.subscribe(commitment);
4020
4021 for sender_idx in [1usize, 2, 4, 5] {
4024 let shard = coded_block
4025 .shard(peers[sender_idx].index.get() as u16)
4026 .expect("missing shard")
4027 .encode();
4028 peers[sender_idx].sender.send(
4029 Recipients::One(receiver.clone()),
4030 shard,
4031 true,
4032 );
4033 }
4034 context.sleep(config.link.latency * 2).await;
4035
4036 assert!(
4037 peers[receiver_idx].mailbox.get(commitment).await.is_none(),
4038 "block should not reconstruct before the commitment is notarized"
4039 );
4040
4041 peers[receiver_idx].mailbox.notarized(commitment, round);
4042
4043 select! {
4044 _ = block_sub => {},
4045 _ = context.sleep(Duration::from_secs(5)) => {
4046 panic!("block subscription did not resolve after notarized reconstruction interest");
4047 },
4048 }
4049
4050 let reconstructed = peers[receiver_idx]
4051 .mailbox
4052 .get(commitment)
4053 .await
4054 .expect("block should reconstruct from buffered peer shards");
4055 assert_eq!(reconstructed.commitment(), commitment);
4056
4057 let mut assigned = peers[receiver_idx]
4058 .mailbox
4059 .subscribe_assigned_shard_verified(commitment);
4060 assert!(
4061 matches!(assigned.try_recv(), Err(TryRecvError::Empty)),
4062 "leaderless reconstruction must not satisfy assigned shard readiness"
4063 );
4064
4065 let leader = peers[0].public_key.clone();
4066 peers[receiver_idx]
4067 .mailbox
4068 .discovered(commitment, leader, round);
4069 let leader_shard = coded_block
4070 .shard(peers[receiver_idx].index.get() as u16)
4071 .expect("missing leader shard")
4072 .encode();
4073 peers[0].sender.send(
4074 Recipients::One(receiver),
4075 leader_shard,
4076 true,
4077 );
4078
4079 select! {
4080 _ = assigned => {},
4081 _ = context.sleep(Duration::from_secs(5)) => {
4082 panic!("assigned shard subscription did not resolve after leader discovery");
4083 },
4084 }
4085
4086 assert!(
4087 oracle.blocked().await.unwrap().is_empty(),
4088 "valid sender-indexed shards should not block peers"
4089 );
4090 },
4091 );
4092 }
4093
4094 #[test_traced]
4095 fn test_late_subscription_uses_notarized_cache_after_peer_buffer_pressure() {
4096 let fixture: Fixture<C> = Fixture {
4097 num_primary_peers: 10,
4098 peer_buffer_size: NZUsize!(1),
4099 ..Default::default()
4100 };
4101
4102 fixture.start(
4103 |config, context, oracle, mut peers, _, coding_config| async move {
4104 let target = CodedBlock::<B, C, H>::new(
4105 B::new(Sha256Digest::EMPTY, Height::new(1), 1),
4106 coding_config,
4107 &STRATEGY,
4108 );
4109 let target_commitment = target.commitment();
4110 let receiver_idx = 3usize;
4111 let receiver = peers[receiver_idx].public_key.clone();
4112 let initial_round = Round::new(Epoch::zero(), View::new(1));
4113 let refreshed_round = Round::new(Epoch::zero(), View::new(4));
4114
4115 for sender_idx in [1usize, 2, 4, 5] {
4116 let shard = target
4117 .shard(peers[sender_idx].index.get() as u16)
4118 .expect("missing target shard")
4119 .encode();
4120 peers[sender_idx].sender.send(
4121 Recipients::One(receiver.clone()),
4122 shard,
4123 true,
4124 );
4125 }
4126 context.sleep(config.link.latency * 2).await;
4127
4128 peers[receiver_idx]
4129 .mailbox
4130 .notarized(target_commitment, initial_round);
4131 let target_sub = peers[receiver_idx].mailbox.subscribe(target_commitment);
4132 select! {
4133 result = target_sub => {
4134 let block = result.expect("notarized target should reconstruct");
4135 assert_eq!(block.commitment(), target_commitment);
4136 },
4137 _ = context.sleep(Duration::from_secs(5)) => {
4138 panic!("notarized target did not reconstruct");
4139 },
4140 }
4141
4142 peers[receiver_idx]
4146 .mailbox
4147 .notarized(target_commitment, refreshed_round);
4148 assert!(
4149 peers[receiver_idx]
4150 .mailbox
4151 .get(target_commitment)
4152 .await
4153 .is_some(),
4154 "target should be cached after its notarization is refreshed"
4155 );
4156
4157 let pressure_blocks = [2u64, 3].map(|id| {
4160 CodedBlock::<B, C, H>::new(
4161 B::new(Sha256Digest::EMPTY, Height::new(id), id),
4162 coding_config,
4163 &STRATEGY,
4164 )
4165 });
4166 for block in &pressure_blocks {
4167 let shard = block
4168 .shard(peers[1].index.get() as u16)
4169 .expect("missing pressure shard")
4170 .encode();
4171 peers[1].sender.send(
4172 Recipients::One(receiver.clone()),
4173 shard,
4174 true,
4175 );
4176 }
4177 context.sleep(config.link.latency * 2).await;
4178
4179 let [evicted_block, retained_block] = &pressure_blocks;
4180 for (block, should_reconstruct) in
4181 [(evicted_block, false), (retained_block, true)]
4182 {
4183 for sender_idx in [2usize, 4, 5] {
4184 let shard = block
4185 .shard(peers[sender_idx].index.get() as u16)
4186 .expect("missing complementary pressure shard")
4187 .encode();
4188 peers[sender_idx].sender.send(
4189 Recipients::One(receiver.clone()),
4190 shard,
4191 true,
4192 );
4193 }
4194 context.sleep(config.link.latency * 2).await;
4195
4196 let commitment = block.commitment();
4197 peers[receiver_idx]
4198 .mailbox
4199 .notarized(commitment, initial_round);
4200 if should_reconstruct {
4201 let block_sub = peers[receiver_idx].mailbox.subscribe(commitment);
4202 select! {
4203 result = block_sub => {
4204 let block = result.expect("retained pressure block should reconstruct");
4205 assert_eq!(block.commitment(), commitment);
4206 },
4207 _ = context.sleep(Duration::from_secs(5)) => {
4208 panic!("retained same-peer shard did not reconstruct");
4209 },
4210 }
4211 } else {
4212 assert!(
4213 peers[receiver_idx]
4214 .mailbox
4215 .get(commitment)
4216 .await
4217 .is_none(),
4218 "evicted same-peer shard should not reconstruct"
4219 );
4220 }
4221 }
4222
4223 peers[receiver_idx].mailbox.retire(Retirement {
4224 round_floor: Round::new(Epoch::zero(), View::new(3)),
4225 exact_retirements: Vec::new(),
4226 });
4227
4228 let late_sub = peers[receiver_idx].mailbox.subscribe(target_commitment);
4231 select! {
4232 result = late_sub => {
4233 let block = result.expect("refreshed target should remain cached");
4234 assert_eq!(block.commitment(), target_commitment);
4235 },
4236 _ = context.sleep(Duration::from_secs(5)) => {
4237 panic!("late target subscription lost cached ownership");
4238 },
4239 }
4240
4241 assert!(
4242 oracle.blocked().await.unwrap().is_empty(),
4243 "valid pressure shards should not block peers"
4244 );
4245 },
4246 );
4247 }
4248
4249 #[test_traced]
4250 fn test_leader_shard_after_notarized_is_buffered_until_discovered() {
4251 let fixture: Fixture<C> = Fixture {
4252 num_primary_peers: 10,
4253 ..Default::default()
4254 };
4255
4256 fixture.start(
4257 |config, context, oracle, mut peers, _, coding_config| async move {
4258 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
4259 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4260 let commitment = coded_block.commitment();
4261 let round = Round::new(Epoch::zero(), View::new(1));
4262
4263 let leader_idx = 0usize;
4264 let receiver_idx = 3usize;
4265 let leader = peers[leader_idx].public_key.clone();
4266 let receiver = peers[receiver_idx].public_key.clone();
4267
4268 peers[receiver_idx].mailbox.notarized(commitment, round);
4269 let assigned = peers[receiver_idx]
4270 .mailbox
4271 .subscribe_assigned_shard_verified(commitment);
4272
4273 let leader_shard = coded_block
4274 .shard(peers[receiver_idx].index.get() as u16)
4275 .expect("missing receiver shard")
4276 .encode();
4277 peers[leader_idx]
4278 .sender
4279 .send(Recipients::One(receiver), leader_shard, true);
4280
4281 context.sleep(config.link.latency * 2).await;
4282 peers[receiver_idx]
4283 .mailbox
4284 .discovered(commitment, leader, round);
4285
4286 assigned
4287 .await
4288 .expect("assigned shard should resolve after leader discovery");
4289 assert!(
4290 oracle.blocked().await.unwrap().is_empty(),
4291 "valid leader shard should not block peers"
4292 );
4293 },
4294 );
4295 }
4296
4297 #[test_traced]
4298 fn test_post_leader_shards_processed_immediately() {
4299 let fixture: Fixture<C> = Fixture {
4302 num_primary_peers: 10,
4303 ..Default::default()
4304 };
4305
4306 fixture.start(
4307 |config, context, oracle, mut peers, _, coding_config| async move {
4308 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
4309 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4310 let commitment = coded_block.commitment();
4311
4312 let receiver_idx = 3usize;
4313 let receiver_pk = peers[receiver_idx].public_key.clone();
4314 let leader = peers[0].public_key.clone();
4315
4316 let shard_sub = peers[receiver_idx]
4317 .mailbox
4318 .subscribe_assigned_shard_verified(commitment);
4319 peers[receiver_idx].mailbox.discovered(
4320 commitment,
4321 leader.clone(),
4322 Round::new(Epoch::zero(), View::new(1)),
4323 );
4324
4325 let leader_shard = coded_block
4327 .shard(peers[receiver_idx].index.get() as u16)
4328 .expect("missing shard")
4329 .encode();
4330 peers[0]
4331 .sender
4332 .send(Recipients::One(receiver_pk.clone()), leader_shard, true);
4333
4334 select! {
4336 _ = shard_sub => {},
4337 _ = context.sleep(Duration::from_secs(5)) => {
4338 panic!("shard subscription did not resolve after post-leader shard");
4339 },
4340 }
4341
4342 for i in [1usize, 2usize, 4usize] {
4344 let shard = coded_block
4345 .shard(peers[i].index.get() as u16)
4346 .expect("missing shard")
4347 .encode();
4348 peers[i]
4349 .sender
4350 .send(Recipients::One(receiver_pk.clone()), shard, true);
4351 }
4352
4353 context.sleep(config.link.latency * 2).await;
4354 let reconstructed = peers[receiver_idx]
4355 .mailbox
4356 .get(commitment)
4357 .await
4358 .expect("block should reconstruct from post-leader shards");
4359 assert_eq!(reconstructed.commitment(), commitment);
4360
4361 assert!(
4362 oracle.blocked().await.unwrap().is_empty(),
4363 "no peers should be blocked for valid post-leader shards"
4364 );
4365 },
4366 );
4367 }
4368
4369 #[test_traced]
4370 fn test_invalid_shard_codec_blocks_peer() {
4371 let fixture: Fixture<C> = Fixture {
4373 num_primary_peers: 4,
4374 ..Default::default()
4375 };
4376
4377 fixture.start(
4378 |config, context, oracle, mut peers, _, _coding_config| async move {
4379 let peer0_pk = peers[0].public_key.clone();
4380 let peer1_pk = peers[1].public_key.clone();
4381
4382 let garbage = Bytes::from(vec![0xFF, 0xFE, 0xFD, 0xFC, 0xFB]);
4384 peers[1]
4385 .sender
4386 .send(Recipients::One(peer0_pk.clone()), garbage, true);
4387
4388 context.sleep(config.link.latency * 2).await;
4389
4390 assert_blocked(&oracle, &peer0_pk, &peer1_pk).await;
4392 },
4393 );
4394 }
4395
4396 #[test_traced]
4397 fn test_duplicate_buffered_shard_does_not_block_before_leader() {
4398 let fixture: Fixture<C> = Fixture {
4401 ..Default::default()
4402 };
4403
4404 fixture.start(
4405 |config, context, oracle, mut peers, _, coding_config| async move {
4406 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
4407 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4408
4409 let peer2_index = peers[2].index.get() as u16;
4411 let peer2_shard = coded_block.shard(peer2_index).expect("missing shard");
4412 let shard_bytes = peer2_shard.encode();
4413
4414 let peer2_pk = peers[2].public_key.clone();
4415
4416 peers[1]
4420 .sender
4421 .send(Recipients::One(peer2_pk.clone()), shard_bytes.clone(), true);
4422 context.sleep(config.link.latency * 2).await;
4423
4424 let blocked = oracle.blocked().await.unwrap();
4426 assert!(blocked.is_empty(), "no peers should be blocked yet");
4427
4428 peers[1]
4430 .sender
4431 .send(Recipients::One(peer2_pk), shard_bytes, true);
4432 context.sleep(config.link.latency * 2).await;
4433
4434 let blocked = oracle.blocked().await.unwrap();
4436 assert!(
4437 blocked.is_empty(),
4438 "no peers should be blocked before leader"
4439 );
4440 },
4441 );
4442 }
4443
4444 #[test_traced]
4445 fn test_invalid_leader_shard_crypto_blocks_leader() {
4446 let fixture: Fixture<C> = Fixture {
4449 ..Default::default()
4450 };
4451
4452 fixture.start(
4453 |config, context, oracle, mut peers, _, coding_config| async move {
4454 let inner1 = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
4457 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
4458 let commitment1 = coded_block1.commitment();
4459
4460 let inner2 = B::new(Sha256Digest::EMPTY, Height::new(2), 200);
4461 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
4462
4463 let peer2_index = peers[2].index.get() as u16;
4466 let mut wrong_shard = coded_block2.shard(peer2_index).expect("missing shard");
4467 wrong_shard.commitment = commitment1;
4468 let wrong_bytes = wrong_shard.encode();
4469
4470 let peer2_pk = peers[2].public_key.clone();
4471 let leader = peers[0].public_key.clone();
4472
4473 peers[2].mailbox.discovered(
4475 commitment1,
4476 leader,
4477 Round::new(Epoch::zero(), View::new(1)),
4478 );
4479
4480 peers[0]
4482 .sender
4483 .send(Recipients::One(peer2_pk), wrong_bytes, true);
4484 context.sleep(config.link.latency * 2).await;
4485
4486 assert_blocked(&oracle, &peers[2].public_key, &peers[0].public_key).await;
4488 },
4489 );
4490 }
4491
4492 #[test_traced]
4493 fn test_invalid_assigned_shard_from_non_leader_blocks_only_sender() {
4494 let fixture: Fixture<C> = Fixture {
4499 ..Default::default()
4500 };
4501
4502 fixture.start(
4503 |config, context, oracle, mut peers, _, coding_config| async move {
4504 let inner1 = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
4507 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
4508 let commitment1 = coded_block1.commitment();
4509
4510 let inner2 = B::new(Sha256Digest::EMPTY, Height::new(2), 200);
4511 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
4512
4513 let peer2_index = peers[2].index.get() as u16;
4516 let mut wrong_shard = coded_block2.shard(peer2_index).expect("missing shard");
4517 wrong_shard.commitment = commitment1;
4518 let wrong_bytes = wrong_shard.encode();
4519
4520 let peer2_pk = peers[2].public_key.clone();
4521 let leader = peers[0].public_key.clone();
4522
4523 let mut shard_sub = peers[2]
4524 .mailbox
4525 .subscribe_assigned_shard_verified(commitment1);
4526
4527 peers[2].mailbox.discovered(
4529 commitment1,
4530 leader,
4531 Round::new(Epoch::zero(), View::new(1)),
4532 );
4533
4534 peers[1]
4537 .sender
4538 .send(Recipients::One(peer2_pk.clone()), wrong_bytes, true);
4539 context.sleep(config.link.latency * 2).await;
4540
4541 assert_blocked(&oracle, &peers[2].public_key, &peers[1].public_key).await;
4544 assert!(
4545 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
4546 "subscription should not resolve from invalid shard"
4547 );
4548
4549 let real_bytes = coded_block1
4551 .shard(peer2_index)
4552 .expect("missing shard")
4553 .encode();
4554 peers[0]
4555 .sender
4556 .send(Recipients::One(peer2_pk), real_bytes, true);
4557 select! {
4558 _ = shard_sub => {},
4559 _ = context.sleep(Duration::from_secs(5)) => {
4560 panic!("genuine assigned shard did not verify after invalid one");
4561 },
4562 };
4563 },
4564 );
4565 }
4566
4567 #[test_traced]
4568 fn test_shard_index_mismatch_blocks_peer() {
4569 let fixture: Fixture<C> = Fixture {
4572 num_primary_peers: 10,
4573 ..Default::default()
4574 };
4575
4576 fixture.start(
4577 |config, context, oracle, mut peers, _, coding_config| async move {
4578 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
4579 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4580 let commitment = coded_block.commitment();
4581
4582 let peer3_index = peers[3].index.get() as u16;
4584 let leader_shard = coded_block.shard(peer3_index).expect("missing shard");
4585
4586 let peer1_index = peers[1].index.get() as u16;
4588 let mut wrong_index_shard = coded_block.shard(peer1_index).expect("missing shard");
4589 wrong_index_shard.index = peers[4].index.get() as u16;
4591 let wrong_bytes = wrong_index_shard.encode();
4592
4593 let peer3_pk = peers[3].public_key.clone();
4594 let leader = peers[0].public_key.clone();
4595
4596 peers[3].mailbox.discovered(
4598 commitment,
4599 leader,
4600 Round::new(Epoch::zero(), View::new(1)),
4601 );
4602 let shard_bytes = leader_shard.encode();
4603 peers[0]
4604 .sender
4605 .send(Recipients::One(peer3_pk.clone()), shard_bytes, true);
4606 context.sleep(config.link.latency * 2).await;
4607
4608 peers[1]
4610 .sender
4611 .send(Recipients::One(peer3_pk), wrong_bytes, true);
4612 context.sleep(config.link.latency * 2).await;
4613
4614 assert_blocked(&oracle, &peers[3].public_key, &peers[1].public_key).await;
4616 },
4617 );
4618 }
4619
4620 #[test_traced]
4621 fn test_invalid_shard_crypto_blocks_peer() {
4622 let fixture: Fixture<C> = Fixture {
4625 num_primary_peers: 10,
4626 ..Default::default()
4627 };
4628
4629 fixture.start(
4630 |config, context, oracle, mut peers, _, coding_config| async move {
4631 let inner1 = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
4633 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
4634 let commitment1 = coded_block1.commitment();
4635
4636 let inner2 = B::new(Sha256Digest::EMPTY, Height::new(2), 200);
4637 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
4638
4639 let peer3_index = peers[3].index.get() as u16;
4641 let leader_shard = coded_block1.shard(peer3_index).expect("missing shard");
4642
4643 let peer1_index = peers[1].index.get() as u16;
4646 let mut wrong_shard = coded_block2.shard(peer1_index).expect("missing shard");
4647 wrong_shard.commitment = commitment1;
4648 let wrong_bytes = wrong_shard.encode();
4649
4650 let peer3_pk = peers[3].public_key.clone();
4651 let leader = peers[0].public_key.clone();
4652
4653 peers[3].mailbox.discovered(
4655 commitment1,
4656 leader,
4657 Round::new(Epoch::zero(), View::new(1)),
4658 );
4659 let shard_bytes = leader_shard.encode();
4660 peers[0]
4661 .sender
4662 .send(Recipients::One(peer3_pk.clone()), shard_bytes, true);
4663 context.sleep(config.link.latency * 2).await;
4664
4665 peers[1]
4667 .sender
4668 .send(Recipients::One(peer3_pk.clone()), wrong_bytes, true);
4669 context.sleep(config.link.latency * 2).await;
4670
4671 for &idx in &[2, 4] {
4675 let peer_index = peers[idx].index.get() as u16;
4676 let shard = coded_block1.shard(peer_index).expect("missing shard");
4677 let bytes = shard.encode();
4678 peers[idx]
4679 .sender
4680 .send(Recipients::One(peer3_pk.clone()), bytes, true);
4681 }
4682 context.sleep(config.link.latency * 2).await;
4683
4684 assert_blocked(&oracle, &peers[3].public_key, &peers[1].public_key).await;
4686 },
4687 );
4688 }
4689
4690 #[test_traced]
4691 fn test_reconstruction_recovers_after_quorum_with_one_invalid_shard() {
4692 let fixture: Fixture<C> = Fixture {
4697 num_primary_peers: 10,
4698 ..Default::default()
4699 };
4700
4701 fixture.start(
4702 |config, context, oracle, mut peers, _, coding_config| async move {
4703 let inner1 = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
4704 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
4705 let commitment1 = coded_block1.commitment();
4706
4707 let inner2 = B::new(Sha256Digest::EMPTY, Height::new(2), 200);
4708 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
4709
4710 let receiver_idx = 3usize;
4711 let receiver_pk = peers[receiver_idx].public_key.clone();
4712
4713 let peer1_index = peers[1].index.get() as u16;
4715 let mut invalid_shard = coded_block2.shard(peer1_index).expect("missing shard");
4716 invalid_shard.commitment = commitment1;
4717
4718 let leader = peers[0].public_key.clone();
4720 peers[receiver_idx].mailbox.discovered(
4721 commitment1,
4722 leader,
4723 Round::new(Epoch::zero(), View::new(1)),
4724 );
4725 let leader_shard = coded_block1
4726 .shard(peers[receiver_idx].index.get() as u16)
4727 .expect("missing shard")
4728 .encode();
4729 peers[0]
4730 .sender
4731 .send(Recipients::One(receiver_pk.clone()), leader_shard, true);
4732
4733 peers[1].sender.send(
4738 Recipients::One(receiver_pk.clone()),
4739 invalid_shard.encode(),
4740 true,
4741 );
4742 for idx in [2usize, 4usize] {
4743 let shard = coded_block1
4744 .shard(peers[idx].index.get() as u16)
4745 .expect("missing shard")
4746 .encode();
4747 peers[idx]
4748 .sender
4749 .send(Recipients::One(receiver_pk.clone()), shard, true);
4750 }
4751
4752 context.sleep(config.link.latency * 2).await;
4753
4754 assert_blocked(
4756 &oracle,
4757 &peers[receiver_idx].public_key,
4758 &peers[1].public_key,
4759 )
4760 .await;
4761 assert!(
4762 peers[receiver_idx].mailbox.get(commitment1).await.is_none(),
4763 "block should not reconstruct with only 3 checked shards"
4764 );
4765
4766 let extra_shard = coded_block1
4768 .shard(peers[5].index.get() as u16)
4769 .expect("missing shard")
4770 .encode();
4771 peers[5]
4772 .sender
4773 .send(Recipients::One(receiver_pk), extra_shard, true);
4774
4775 context.sleep(config.link.latency * 2).await;
4776
4777 let reconstructed = peers[receiver_idx]
4778 .mailbox
4779 .get(commitment1)
4780 .await
4781 .expect("block should reconstruct after additional valid shard");
4782 assert_eq!(reconstructed.commitment(), commitment1);
4783 },
4784 );
4785 }
4786
4787 #[test_traced]
4788 fn test_invalid_pending_shard_blocked_on_drain() {
4789 let fixture: Fixture<C> = Fixture {
4792 num_primary_peers: 10,
4793 ..Default::default()
4794 };
4795
4796 fixture.start(
4797 |config, context, oracle, mut peers, _, coding_config| async move {
4798 let inner1 = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
4800 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
4801 let commitment1 = coded_block1.commitment();
4802
4803 let inner2 = B::new(Sha256Digest::EMPTY, Height::new(2), 200);
4804 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
4805
4806 let peer1_index = peers[1].index.get() as u16;
4808 let mut wrong_shard = coded_block2.shard(peer1_index).expect("missing shard");
4809 wrong_shard.commitment = commitment1;
4810 let wrong_bytes = wrong_shard.encode();
4811
4812 let peer3_pk = peers[3].public_key.clone();
4813
4814 peers[1]
4817 .sender
4818 .send(Recipients::One(peer3_pk.clone()), wrong_bytes, true);
4819 context.sleep(config.link.latency * 2).await;
4820
4821 let blocked = oracle.blocked().await.unwrap();
4823 assert!(blocked.is_empty(), "no peers should be blocked yet");
4824
4825 for &idx in &[2, 4] {
4829 let peer_index = peers[idx].index.get() as u16;
4830 let shard = coded_block1.shard(peer_index).expect("missing shard");
4831 let bytes = shard.encode();
4832 peers[idx]
4833 .sender
4834 .send(Recipients::One(peer3_pk.clone()), bytes, true);
4835 }
4836 context.sleep(config.link.latency * 2).await;
4837
4838 let blocked = oracle.blocked().await.unwrap();
4840 assert!(blocked.is_empty(), "no peers should be blocked yet");
4841
4842 let leader = peers[0].public_key.clone();
4844 peers[3].mailbox.discovered(
4845 commitment1,
4846 leader,
4847 Round::new(Epoch::zero(), View::new(1)),
4848 );
4849 let peer3_index = peers[3].index.get() as u16;
4850 let leader_shard = coded_block1.shard(peer3_index).expect("missing shard");
4851 let shard_bytes = leader_shard.encode();
4852 peers[0]
4853 .sender
4854 .send(Recipients::One(peer3_pk), shard_bytes, true);
4855 context.sleep(config.link.latency * 2).await;
4856
4857 assert_blocked(&oracle, &peers[3].public_key, &peers[1].public_key).await;
4860 },
4861 );
4862 }
4863
4864 #[test_traced]
4865 fn test_cross_epoch_buffered_shard_not_blocked() {
4866 let executor = deterministic::Runner::default();
4867 executor.start(|context| async move {
4868 let (network, oracle) = simulated::Network::<deterministic::Context, P>::new(
4869 context.child("network"),
4870 simulated::Config {
4871 max_size: MAX_SHARD_SIZE as u32,
4872 max_peers_per_set: NZUsize!(2),
4873 disconnect_on_block: true,
4874 tracked_peer_sets: NZUsize!(1),
4875 },
4876 );
4877 network.start();
4878
4879 let mut epoch0_keys: Vec<PrivateKey> = (0..4).map(PrivateKey::from_seed).collect();
4882 epoch0_keys.sort_by_key(|s| s.public_key());
4883 let epoch0_pks: Vec<P> = epoch0_keys.iter().map(|c| c.public_key()).collect();
4884 let epoch0_set: Set<P> = Set::from_iter_dedup(epoch0_pks.clone());
4885
4886 let future_peer_key = PrivateKey::from_seed(4);
4887 let future_peer_pk = future_peer_key.public_key();
4888 let mut epoch1_pks: Vec<P> = epoch0_pks[..3]
4889 .iter()
4890 .cloned()
4891 .chain(std::iter::once(future_peer_pk.clone()))
4892 .collect();
4893 epoch1_pks.sort();
4894 let epoch1_set: Set<P> = Set::from_iter_dedup(epoch1_pks);
4895
4896 let receiver_idx_in_epoch0 = epoch0_set
4897 .index(&epoch0_pks[0])
4898 .expect("receiver must be in epoch 0")
4899 .get() as usize;
4900 let receiver_key = epoch0_keys[receiver_idx_in_epoch0].clone();
4901 let receiver_pk = receiver_key.public_key();
4902
4903 let receiver_control = oracle.control(receiver_pk.clone());
4904 let (sender_handle, receiver_handle) = receiver_control
4905 .register(0, TEST_QUOTA)
4906 .await
4907 .expect("registration should succeed");
4908
4909 let future_peer_control = oracle.control(future_peer_pk.clone());
4910 let (mut future_peer_sender, _future_peer_receiver) = future_peer_control
4911 .register(0, TEST_QUOTA)
4912 .await
4913 .expect("registration should succeed");
4914 oracle
4915 .add_link(future_peer_pk.clone(), receiver_pk.clone(), DEFAULT_LINK)
4916 .await
4917 .expect("link should be added");
4918 oracle.manager().track(
4919 0,
4920 Set::from_iter_dedup([receiver_pk.clone(), future_peer_pk.clone()]),
4921 );
4922 context.sleep(Duration::from_millis(10)).await;
4923
4924 let scheme_epoch0 =
4926 Scheme::signer(SCHEME_NAMESPACE, epoch0_set.clone(), receiver_key.clone())
4927 .expect("signer scheme should be created");
4928 let scheme_epoch1 =
4929 Scheme::signer(SCHEME_NAMESPACE, epoch1_set.clone(), receiver_key.clone())
4930 .expect("signer scheme should be created");
4931 let scheme_provider =
4932 MultiEpochProvider::single(scheme_epoch0).with_epoch(Epoch::new(1), scheme_epoch1);
4933
4934 let config: Config<_, _, _, _, C, _, _, _> = Config {
4935 scheme_provider,
4936 blocker: receiver_control.clone(),
4937 shard_codec_cfg: CodecConfig {
4938 maximum_shard_size: MAX_SHARD_SIZE,
4939 },
4940 block_codec_cfg: (),
4941 strategy: STRATEGY,
4942 mailbox_size: NZUsize!(1024),
4943 peer_buffer_size: NZUsize!(64),
4944 background_channel_capacity: NZUsize!(1024),
4945 peer_provider: oracle.manager(),
4946 };
4947
4948 let (engine, mailbox) = ShardEngine::new(context.child("receiver"), config);
4949 engine.start((sender_handle, receiver_handle));
4950
4951 let coding_config = coding_config_for_participants(epoch1_set.len() as u16);
4953 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
4954 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4955 let commitment = coded_block.commitment();
4956
4957 let future_peer_index = epoch1_set
4959 .index(&future_peer_pk)
4960 .expect("future peer must be in epoch 1");
4961 let future_shard = coded_block
4962 .shard(future_peer_index.get() as u16)
4963 .expect("missing shard");
4964 let shard_bytes = future_shard.encode();
4965
4966 future_peer_sender.send(Recipients::One(receiver_pk.clone()), shard_bytes, true);
4968 context.sleep(DEFAULT_LINK.latency * 2).await;
4969
4970 let blocked = oracle.blocked().await.unwrap();
4972 assert!(
4973 blocked.is_empty(),
4974 "no peers should be blocked while shard is buffered"
4975 );
4976
4977 let leader = epoch0_pks[1].clone();
4979 mailbox.discovered(commitment, leader, Round::new(Epoch::new(1), View::new(1)));
4980 context.sleep(DEFAULT_LINK.latency * 2).await;
4981
4982 let blocked = oracle.blocked().await.unwrap();
4985 assert!(
4986 blocked.is_empty(),
4987 "future-epoch participant should not be blocked: {blocked:?}"
4988 );
4989 });
4990 }
4991
4992 #[test_traced]
4993 fn test_shard_broadcast_survives_provider_churn() {
4994 let executor = deterministic::Runner::default();
4995 executor.start(|context| async move {
4996 let (network, oracle) = simulated::Network::<deterministic::Context, P>::new(
4997 context.child("network"),
4998 simulated::Config {
4999 max_size: MAX_SHARD_SIZE as u32,
5000 max_peers_per_set: NZUsize!(4),
5001 disconnect_on_block: true,
5002 tracked_peer_sets: NZUsize!(1),
5003 },
5004 );
5005 network.start();
5006
5007 let mut private_keys: Vec<PrivateKey> = (0..4).map(PrivateKey::from_seed).collect();
5008 private_keys.sort_by_key(|s| s.public_key());
5009 let peer_keys: Vec<P> = private_keys.iter().map(|k| k.public_key()).collect();
5010 let participants: Set<P> = Set::from_iter_dedup(peer_keys.clone());
5011
5012 let leader_idx = 0usize;
5013 let broadcaster_idx = 1usize;
5014 let receiver_idx = 2usize;
5015
5016 let leader_pk = peer_keys[leader_idx].clone();
5017 let broadcaster_pk = peer_keys[broadcaster_idx].clone();
5018 let receiver_pk = peer_keys[receiver_idx].clone();
5019
5020 let mut registrations = BTreeMap::new();
5021 for key in &peer_keys {
5022 let control = oracle.control(key.clone());
5023 let (sender, receiver) = control
5024 .register(0, TEST_QUOTA)
5025 .await
5026 .expect("registration should succeed");
5027 registrations.insert(key.clone(), (control, sender, receiver));
5028 }
5029
5030 for src in &peer_keys {
5031 for dst in &peer_keys {
5032 if src == dst {
5033 continue;
5034 }
5035 oracle
5036 .add_link(src.clone(), dst.clone(), DEFAULT_LINK)
5037 .await
5038 .expect("link should be added");
5039 }
5040 }
5041 oracle.manager().track(0, participants.clone());
5042 context.sleep(Duration::from_millis(10)).await;
5043
5044 let (_leader_control, mut leader_sender, _leader_receiver) = registrations
5045 .remove(&leader_pk)
5046 .expect("leader should be registered");
5047 let (broadcaster_control, broadcaster_sender, broadcaster_receiver) = registrations
5048 .remove(&broadcaster_pk)
5049 .expect("broadcaster should be registered");
5050 let (receiver_control, receiver_sender, receiver_receiver) = registrations
5051 .remove(&receiver_pk)
5052 .expect("receiver should be registered");
5053
5054 let broadcaster_scheme = Scheme::signer(
5055 SCHEME_NAMESPACE,
5056 participants.clone(),
5057 private_keys[broadcaster_idx].clone(),
5058 )
5059 .expect("signer scheme should be created");
5060 let broadcaster_provider = ChurningProvider::new(broadcaster_scheme, 3);
5064 let broadcaster_config: Config<_, _, _, _, C, _, _, _> = Config {
5065 scheme_provider: broadcaster_provider,
5066 blocker: broadcaster_control.clone(),
5067 shard_codec_cfg: CodecConfig {
5068 maximum_shard_size: MAX_SHARD_SIZE,
5069 },
5070 block_codec_cfg: (),
5071 strategy: STRATEGY,
5072 mailbox_size: NZUsize!(1024),
5073 peer_buffer_size: NZUsize!(64),
5074 background_channel_capacity: NZUsize!(1024),
5075 peer_provider: oracle.manager(),
5076 };
5077 let (broadcaster_engine, broadcaster_mailbox) =
5078 ChurningShardEngine::new(context.child("broadcaster"), broadcaster_config);
5079 broadcaster_engine.start((broadcaster_sender, broadcaster_receiver));
5080
5081 let receiver_scheme = Scheme::signer(
5082 SCHEME_NAMESPACE,
5083 participants.clone(),
5084 private_keys[receiver_idx].clone(),
5085 )
5086 .expect("signer scheme should be created");
5087 let receiver_config: Config<_, _, _, _, C, _, _, _> = Config {
5088 scheme_provider: MultiEpochProvider::single(receiver_scheme),
5089 blocker: receiver_control.clone(),
5090 shard_codec_cfg: CodecConfig {
5091 maximum_shard_size: MAX_SHARD_SIZE,
5092 },
5093 block_codec_cfg: (),
5094 strategy: STRATEGY,
5095 mailbox_size: NZUsize!(1024),
5096 peer_buffer_size: NZUsize!(64),
5097 background_channel_capacity: NZUsize!(1024),
5098 peer_provider: oracle.manager(),
5099 };
5100 let (receiver_engine, receiver_mailbox) =
5101 ShardEngine::new(context.child("receiver"), receiver_config);
5102 receiver_engine.start((receiver_sender, receiver_receiver));
5103
5104 let coding_config = coding_config_for_participants(peer_keys.len() as u16);
5105 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
5106 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
5107 let commitment = coded_block.commitment();
5108 let round = Round::new(Epoch::zero(), View::new(1));
5109
5110 broadcaster_mailbox.discovered(commitment, leader_pk.clone(), round);
5111 receiver_mailbox.discovered(commitment, leader_pk.clone(), round);
5112 context.sleep(DEFAULT_LINK.latency).await;
5113
5114 let broadcaster_index = participants
5115 .index(&broadcaster_pk)
5116 .expect("broadcaster must be a participant")
5117 .get() as u16;
5118 let broadcaster_shard = coded_block
5119 .shard(broadcaster_index)
5120 .expect("missing shard")
5121 .encode();
5122 leader_sender.send(Recipients::One(broadcaster_pk), broadcaster_shard, true);
5123
5124 let receiver_index = participants
5125 .index(&receiver_pk)
5126 .expect("receiver must be a participant")
5127 .get() as u16;
5128 let receiver_shard = coded_block
5129 .shard(receiver_index)
5130 .expect("missing shard")
5131 .encode();
5132 leader_sender.send(Recipients::One(receiver_pk.clone()), receiver_shard, true);
5133
5134 context.sleep(DEFAULT_LINK.latency * 3).await;
5135
5136 let reconstructed = receiver_mailbox.get(commitment).await;
5137 assert!(
5138 reconstructed.is_some(),
5139 "receiver should reconstruct after broadcaster validates and broadcasts shard"
5140 );
5141 });
5142 }
5143
5144 #[test_traced]
5145 fn test_failed_reconstruction_digest_mismatch_then_recovery() {
5146 let fixture: Fixture<C> = Fixture {
5154 num_primary_peers: 10,
5155 ..Default::default()
5156 };
5157
5158 fixture.start(
5159 |config, context, _oracle, mut peers, _, coding_config| async move {
5160 let inner1 = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
5162 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
5163
5164 let inner2 = B::new(Sha256Digest::EMPTY, Height::new(2), 200);
5166 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
5167 let real_commitment2 = coded_block2.commitment();
5168
5169 let fake_commitment = Commitment::from((
5174 coded_block1.digest(),
5175 real_commitment2.root(),
5176 real_commitment2.context(),
5177 coding_config,
5178 ));
5179
5180 let receiver_idx = 3usize;
5181 let receiver_pk = peers[receiver_idx].public_key.clone();
5182 let leader = peers[0].public_key.clone();
5183 let round = Round::new(Epoch::zero(), View::new(1));
5184
5185 peers[receiver_idx]
5187 .mailbox
5188 .discovered(fake_commitment, leader.clone(), round);
5189
5190 let mut block_sub = peers[receiver_idx].mailbox.subscribe(fake_commitment);
5192 let mut digest_sub = peers[receiver_idx]
5193 .mailbox
5194 .subscribe_by_digest(coded_block1.digest());
5195
5196 let receiver_shard_idx = peers[receiver_idx].index.get() as u16;
5198 let mut leader_shard = coded_block2
5199 .shard(receiver_shard_idx)
5200 .expect("missing shard");
5201 leader_shard.commitment = fake_commitment;
5202 peers[0].sender.send(
5203 Recipients::One(receiver_pk.clone()),
5204 leader_shard.encode(),
5205 true,
5206 );
5207
5208 for &idx in &[1usize, 2, 4] {
5211 let peer_shard_idx = peers[idx].index.get() as u16;
5212 let mut shard = coded_block2.shard(peer_shard_idx).expect("missing shard");
5213 shard.commitment = fake_commitment;
5214 peers[idx].sender.send(
5215 Recipients::One(receiver_pk.clone()),
5216 shard.encode(),
5217 true,
5218 );
5219 }
5220
5221 context.sleep(config.link.latency * 2).await;
5222
5223 assert!(
5226 peers[receiver_idx]
5227 .mailbox
5228 .get(fake_commitment)
5229 .await
5230 .is_none(),
5231 "block should not be available after DigestMismatch"
5232 );
5233
5234 assert!(
5237 matches!(block_sub.try_recv(), Err(TryRecvError::Closed)),
5238 "subscription should close for failed reconstruction"
5239 );
5240 assert!(
5241 matches!(digest_sub.try_recv(), Err(TryRecvError::Empty)),
5242 "digest subscription should survive failed reconstruction"
5243 );
5244 let real_commitment1 = coded_block1.commitment();
5247 let round2 = Round::new(Epoch::zero(), View::new(2));
5248 peers[receiver_idx]
5249 .mailbox
5250 .discovered(real_commitment1, leader.clone(), round2);
5251
5252 let leader_shard1 = coded_block1
5253 .shard(receiver_shard_idx)
5254 .expect("missing shard");
5255 peers[0].sender.send(
5256 Recipients::One(receiver_pk.clone()),
5257 leader_shard1.encode(),
5258 true,
5259 );
5260
5261 for &idx in &[1usize, 2, 4] {
5262 let peer_shard_idx = peers[idx].index.get() as u16;
5263 let shard = coded_block1.shard(peer_shard_idx).expect("missing shard");
5264 peers[idx].sender.send(
5265 Recipients::One(receiver_pk.clone()),
5266 shard.encode(),
5267 true,
5268 );
5269 }
5270
5271 context.sleep(config.link.latency * 2).await;
5272
5273 let reconstructed = peers[receiver_idx]
5274 .mailbox
5275 .get(real_commitment1)
5276 .await
5277 .expect("valid block should reconstruct after prior failure");
5278 assert_eq!(reconstructed.commitment(), real_commitment1);
5279 let by_digest = digest_sub
5280 .await
5281 .expect("valid commitment should satisfy digest subscription");
5282 assert_eq!(by_digest.commitment(), real_commitment1);
5283 },
5284 );
5285 }
5286
5287 #[test_traced]
5288 fn test_failed_reconstruction_context_mismatch_then_recovery() {
5289 let fixture: Fixture<C> = Fixture {
5293 num_primary_peers: 10,
5294 ..Default::default()
5295 };
5296
5297 fixture.start(
5298 |config, context, _oracle, mut peers, _, coding_config| async move {
5299 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
5300 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
5301 let real_commitment = coded_block.commitment();
5302
5303 let wrong_context_digest = Sha256::hash(&[b"wrong_context"]);
5304 assert_ne!(
5305 real_commitment.context(),
5306 wrong_context_digest,
5307 "test requires a distinct context digest"
5308 );
5309 let fake_commitment = Commitment::from((
5310 coded_block.digest(),
5311 real_commitment.root(),
5312 wrong_context_digest,
5313 coding_config,
5314 ));
5315
5316 let receiver_idx = 3usize;
5317 let receiver_pk = peers[receiver_idx].public_key.clone();
5318 let leader = peers[0].public_key.clone();
5319 let round = Round::new(Epoch::zero(), View::new(1));
5320
5321 peers[receiver_idx]
5322 .mailbox
5323 .discovered(fake_commitment, leader.clone(), round);
5324 let mut block_sub = peers[receiver_idx].mailbox.subscribe(fake_commitment);
5325
5326 let receiver_shard_idx = peers[receiver_idx].index.get() as u16;
5327 let mut leader_shard = coded_block
5328 .shard(receiver_shard_idx)
5329 .expect("missing shard");
5330 leader_shard.commitment = fake_commitment;
5331 peers[0].sender.send(
5332 Recipients::One(receiver_pk.clone()),
5333 leader_shard.encode(),
5334 true,
5335 );
5336
5337 for &idx in &[1usize, 2, 4] {
5338 let peer_shard_idx = peers[idx].index.get() as u16;
5339 let mut shard = coded_block.shard(peer_shard_idx).expect("missing shard");
5340 shard.commitment = fake_commitment;
5341 peers[idx].sender.send(
5342 Recipients::One(receiver_pk.clone()),
5343 shard.encode(),
5344 true,
5345 );
5346 }
5347
5348 context.sleep(config.link.latency * 2).await;
5349
5350 assert!(
5351 peers[receiver_idx]
5352 .mailbox
5353 .get(fake_commitment)
5354 .await
5355 .is_none(),
5356 "block should not be available after ContextMismatch"
5357 );
5358 assert!(
5359 matches!(block_sub.try_recv(), Err(TryRecvError::Closed)),
5360 "subscription should close for context-mismatched commitment"
5361 );
5362
5363 let round2 = Round::new(Epoch::zero(), View::new(2));
5365 peers[receiver_idx]
5366 .mailbox
5367 .discovered(real_commitment, leader.clone(), round2);
5368
5369 let real_leader_shard = coded_block
5370 .shard(receiver_shard_idx)
5371 .expect("missing shard");
5372 peers[0].sender.send(
5373 Recipients::One(receiver_pk.clone()),
5374 real_leader_shard.encode(),
5375 true,
5376 );
5377
5378 for &idx in &[1usize, 2, 4] {
5379 let peer_shard_idx = peers[idx].index.get() as u16;
5380 let shard = coded_block.shard(peer_shard_idx).expect("missing shard");
5381 peers[idx].sender.send(
5382 Recipients::One(receiver_pk.clone()),
5383 shard.encode(),
5384 true,
5385 );
5386 }
5387
5388 context.sleep(config.link.latency * 2).await;
5389
5390 let reconstructed = peers[receiver_idx]
5391 .mailbox
5392 .get(real_commitment)
5393 .await
5394 .expect("valid block should reconstruct after prior context mismatch");
5395 assert_eq!(reconstructed.commitment(), real_commitment);
5396 },
5397 );
5398 }
5399
5400 #[test_traced]
5401 fn test_same_round_equivocation_preserves_certifiable_recovery() {
5402 let fixture: Fixture<C> = Fixture {
5411 num_primary_peers: 10,
5412 ..Default::default()
5413 };
5414
5415 fixture.start(
5416 |config, context, oracle, mut peers, _, coding_config| async move {
5417 let receiver_idx = 3usize;
5418 let receiver_pk = peers[receiver_idx].public_key.clone();
5419 let receiver_shard_idx = peers[receiver_idx].index.get() as u16;
5420
5421 let leader = peers[0].public_key.clone();
5422 let round = Round::new(Epoch::zero(), View::new(7));
5423
5424 let block_a = CodedBlock::<B, C, H>::new(
5426 B::new(Sha256Digest::EMPTY, Height::new(1), 111),
5427 coding_config,
5428 &STRATEGY,
5429 );
5430 let commitment_a = block_a.commitment();
5431 let block_b = CodedBlock::<B, C, H>::new(
5432 B::new(Sha256Digest::EMPTY, Height::new(1), 222),
5433 coding_config,
5434 &STRATEGY,
5435 );
5436 let commitment_b = block_b.commitment();
5437
5438 peers[receiver_idx]
5440 .mailbox
5441 .discovered(commitment_a, leader.clone(), round);
5442 peers[receiver_idx]
5443 .mailbox
5444 .discovered(commitment_b, leader.clone(), round);
5445
5446 let certifiable_sub = peers[receiver_idx].mailbox.subscribe(commitment_b);
5448
5449 let shard_b = block_b
5451 .shard(receiver_shard_idx)
5452 .expect("missing shard")
5453 .encode();
5454 peers[0]
5455 .sender
5456 .send(Recipients::One(receiver_pk.clone()), shard_b, true);
5457
5458 let shard_a = block_a
5460 .shard(receiver_shard_idx)
5461 .expect("missing shard")
5462 .encode();
5463 peers[0]
5464 .sender
5465 .send(Recipients::One(receiver_pk.clone()), shard_a, true);
5466 for i in [1usize, 2usize, 4usize] {
5467 let shard_a = block_a
5468 .shard(peers[i].index.get() as u16)
5469 .expect("missing shard")
5470 .encode();
5471 peers[i]
5472 .sender
5473 .send(Recipients::One(receiver_pk.clone()), shard_a, true);
5474 }
5475 context.sleep(config.link.latency * 4).await;
5476 let reconstructed_a = peers[receiver_idx]
5477 .mailbox
5478 .get(commitment_a)
5479 .await
5480 .expect("conflicting commitment should reconstruct first");
5481 assert_eq!(reconstructed_a.commitment(), commitment_a);
5482
5483 for i in [1usize, 2usize, 4usize] {
5485 let shard_b = block_b
5486 .shard(peers[i].index.get() as u16)
5487 .expect("missing shard")
5488 .encode();
5489 peers[i]
5490 .sender
5491 .send(Recipients::One(receiver_pk.clone()), shard_b, true);
5492 }
5493
5494 select! {
5495 result = certifiable_sub => {
5496 let reconstructed_b =
5497 result.expect("certifiable commitment should remain recoverable");
5498 assert_eq!(reconstructed_b.commitment(), commitment_b);
5499 },
5500 _ = context.sleep(Duration::from_secs(5)) => {
5501 panic!("certifiable commitment was not recoverable after same-round equivocation");
5502 },
5503 }
5504
5505 let blocked_peers = oracle.blocked().await.unwrap();
5508 let is_blocked = blocked_peers
5509 .iter()
5510 .any(|(a, b)| a == &receiver_pk && b == &leader);
5511 assert!(
5512 !is_blocked,
5513 "leader must not be blocked for same-round cross-commitment shards"
5514 );
5515 },
5516 );
5517 }
5518
5519 #[test_traced]
5520 fn test_leader_unrelated_shard_blocks_peer() {
5521 let fixture: Fixture<C> = Fixture {
5525 num_primary_peers: 10,
5526 ..Default::default()
5527 };
5528
5529 fixture.start(
5530 |config, context, oracle, mut peers, _, coding_config| async move {
5531 let tracked_block = CodedBlock::<B, C, H>::new(
5533 B::new(Sha256Digest::EMPTY, Height::new(1), 100),
5534 coding_config,
5535 &STRATEGY,
5536 );
5537 let tracked_commitment = tracked_block.commitment();
5538
5539 let unrelated_block = CodedBlock::<B, C, H>::new(
5541 B::new(Sha256Digest::EMPTY, Height::new(2), 200),
5542 coding_config,
5543 &STRATEGY,
5544 );
5545
5546 let receiver_idx = 3usize;
5547 let receiver_pk = peers[receiver_idx].public_key.clone();
5548 let leader_idx = 0usize;
5549 let leader_pk = peers[leader_idx].public_key.clone();
5550
5551 peers[receiver_idx].mailbox.discovered(
5553 tracked_commitment,
5554 leader_pk.clone(),
5555 Round::new(Epoch::zero(), View::new(1)),
5556 );
5557
5558 let mut unrelated_shard = unrelated_block
5561 .shard(peers[1].index.get() as u16)
5562 .expect("missing shard");
5563 unrelated_shard.commitment = tracked_commitment;
5564
5565 peers[leader_idx].sender.send(
5569 Recipients::One(receiver_pk),
5570 unrelated_shard.encode(),
5571 true,
5572 );
5573 context.sleep(config.link.latency * 2).await;
5574
5575 assert_blocked(&oracle, &peers[receiver_idx].public_key, &leader_pk).await;
5576 },
5577 );
5578 }
5579
5580 #[test_traced]
5581 fn test_withholding_leader_victim_reconstructs_via_gossip() {
5582 let fixture = Fixture {
5587 num_primary_peers: 10,
5588 ..Default::default()
5589 };
5590
5591 fixture.start(
5592 |config, context, oracle, mut peers, _, coding_config| async move {
5593 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
5594 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
5595 let commitment = coded_block.commitment();
5596 let round = Round::new(Epoch::zero(), View::new(1));
5597
5598 let leader = peers[0].public_key.clone();
5599 let victim = peers[1].public_key.clone();
5600
5601 oracle
5604 .remove_link(leader.clone(), victim.clone())
5605 .await
5606 .expect("remove_link should succeed");
5607
5608 peers[0].mailbox.proposed(round, coded_block.clone());
5611
5612 for peer in peers[1..].iter_mut() {
5615 peer.mailbox.discovered(commitment, leader.clone(), round);
5616 }
5617 context.sleep(config.link.latency * 2).await;
5618
5619 let block_sub = peers[1].mailbox.subscribe(commitment);
5622 select! {
5623 result = block_sub => {
5624 let reconstructed = result.expect("block subscription should resolve");
5625 assert_eq!(reconstructed.commitment(), commitment);
5626 assert_eq!(reconstructed.height(), coded_block.height());
5627 },
5628 _ = context.sleep(Duration::from_secs(5)) => {
5629 panic!("victim did not reconstruct block despite withholding leader");
5630 },
5631 }
5632
5633 for peer in peers[2..].iter_mut() {
5635 let reconstructed = peer
5636 .mailbox
5637 .get(commitment)
5638 .await
5639 .expect("block should be reconstructed");
5640 assert_eq!(reconstructed.commitment(), commitment);
5641 }
5642
5643 let blocked = oracle.blocked().await.unwrap();
5645 assert!(
5646 blocked.is_empty(),
5647 "no peer should be blocked in withholding leader test"
5648 );
5649 },
5650 );
5651 }
5652
5653 #[test_traced]
5659 fn test_shard_subscription_pending_after_reconstruction_without_leader_shard() {
5660 let fixture = Fixture {
5661 num_primary_peers: 10,
5662 ..Default::default()
5663 };
5664
5665 fixture.start(
5666 |config, context, oracle, mut peers, _, coding_config| async move {
5667 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
5668 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
5669 let commitment = coded_block.commitment();
5670 let round = Round::new(Epoch::zero(), View::new(1));
5671
5672 let leader = peers[0].public_key.clone();
5673 let victim = peers[1].public_key.clone();
5674
5675 oracle
5678 .remove_link(leader.clone(), victim.clone())
5679 .await
5680 .expect("remove_link should succeed");
5681
5682 let mut shard_sub = peers[1]
5684 .mailbox
5685 .subscribe_assigned_shard_verified(commitment);
5686 let block_sub = peers[1].mailbox.subscribe(commitment);
5687
5688 peers[0].mailbox.proposed(round, coded_block.clone());
5690
5691 for peer in peers[1..].iter_mut() {
5693 peer.mailbox.discovered(commitment, leader.clone(), round);
5694 }
5695
5696 context.sleep(config.link.latency * 4).await;
5698
5699 let reconstructed = block_sub.await.expect("block subscription should resolve");
5702 assert_eq!(reconstructed.commitment(), commitment);
5703
5704 let mut late_shard_sub = peers[1]
5705 .mailbox
5706 .subscribe_assigned_shard_verified(commitment);
5707 context.sleep(Duration::from_millis(10)).await;
5708
5709 assert!(
5712 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
5713 "shard subscription must not resolve without own shard verification"
5714 );
5715 assert!(
5716 matches!(late_shard_sub.try_recv(), Err(TryRecvError::Empty)),
5717 "late shard subscription must not resolve from reconstruction alone"
5718 );
5719 },
5720 );
5721 }
5722
5723 #[test_traced]
5724 fn test_broadcast_routes_participant_and_non_participant_shards() {
5725 let fixture = Fixture {
5726 num_secondary_peers: 1,
5727 ..Default::default()
5728 };
5729
5730 fixture.start(
5731 |config, context, oracle, mut peers, non_participants, coding_config| async move {
5732 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
5733 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
5734 let commitment = coded_block.commitment();
5735
5736 let leader = peers[0].public_key.clone();
5737 let round = Round::new(Epoch::zero(), View::new(1));
5738 peers[0].mailbox.proposed(round, coded_block.clone());
5739
5740 for peer in peers[1..].iter_mut() {
5741 peer.mailbox.discovered(commitment, leader.clone(), round);
5742 }
5743 for np in non_participants.iter() {
5744 np.mailbox.discovered(commitment, leader.clone(), round);
5745 }
5746 context.sleep(config.link.latency * 2).await;
5747
5748 for peer in peers.iter_mut() {
5750 peer.mailbox
5751 .subscribe_assigned_shard_verified(commitment)
5752 .await
5753 .expect("participant shard subscription should complete");
5754 }
5755
5756 for np in non_participants.iter() {
5758 np.mailbox
5759 .subscribe_assigned_shard_verified(commitment)
5760 .await
5761 .expect("non-participant shard subscription should complete");
5762 }
5763 context.sleep(config.link.latency).await;
5764
5765 for np in non_participants.iter() {
5767 let reconstructed = np
5768 .mailbox
5769 .get(commitment)
5770 .await
5771 .expect("non-participant should reconstruct block");
5772 assert_eq!(reconstructed.commitment(), commitment);
5773 }
5774
5775 let blocked = oracle.blocked().await.unwrap();
5776 assert!(
5777 blocked.is_empty(),
5778 "no peer should be blocked in participant/non-participant shard routing test"
5779 );
5780 },
5781 );
5782 }
5783
5784 #[test_traced]
5785 fn test_non_participant_reconstructs_after_discovered() {
5786 let fixture = Fixture {
5787 num_secondary_peers: 1,
5788 ..Default::default()
5789 };
5790
5791 fixture.start(
5792 |config, context, oracle, mut peers, non_participants, coding_config| async move {
5793 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
5794 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
5795 let commitment = coded_block.commitment();
5796 let round = Round::new(Epoch::zero(), View::new(1));
5797
5798 let leader = peers[0].public_key.clone();
5799 peers[0].mailbox.proposed(round, coded_block.clone());
5800
5801 for peer in peers[1..].iter_mut() {
5804 peer.mailbox.discovered(commitment, leader.clone(), round);
5805 }
5806 context.sleep(config.link.latency).await;
5807
5808 let np = &non_participants[0];
5811 let block_sub = np.mailbox.subscribe(commitment);
5812 np.mailbox.discovered(commitment, leader.clone(), round);
5813
5814 select! {
5817 result = block_sub => {
5818 let reconstructed = result.expect("block subscription should resolve");
5819 assert_eq!(reconstructed.commitment(), commitment);
5820 assert_eq!(reconstructed.height(), coded_block.height());
5821 },
5822 _ = context.sleep(Duration::from_secs(5)) => {
5823 panic!("non-participant block subscription did not resolve");
5824 },
5825 }
5826
5827 let blocked = oracle.blocked().await.unwrap();
5828 assert!(
5829 blocked.is_empty(),
5830 "no peer should be blocked in non-participant reconstruction test"
5831 );
5832 },
5833 );
5834 }
5835
5836 #[test_traced]
5837 fn test_peer_set_update_evicts_peer_buffers() {
5838 let executor = deterministic::Runner::default();
5843 executor.start(|context| async move {
5844 let num_peers = 10usize;
5845 let (network, oracle) = simulated::Network::<deterministic::Context, P>::new(
5846 context.child("network"),
5847 simulated::Config {
5848 max_size: MAX_SHARD_SIZE as u32,
5849 max_peers_per_set: NZUsize!(num_peers),
5850 disconnect_on_block: true,
5851 tracked_peer_sets: NZUsize!(2),
5852 },
5853 );
5854 network.start();
5855
5856 let mut private_keys = (0..num_peers)
5857 .map(|i| PrivateKey::from_seed(i as u64))
5858 .collect::<Vec<_>>();
5859 private_keys.sort_by_key(|s| s.public_key());
5860 let peer_keys: Vec<P> = private_keys.iter().map(|c| c.public_key()).collect();
5861 let participants: Set<P> = Set::from_iter_dedup(peer_keys.clone());
5862
5863 let receiver_idx = 3usize;
5865 let receiver_pk = peer_keys[receiver_idx].clone();
5866 let leader_pk = peer_keys[0].clone();
5867
5868 let receiver_control = oracle.control(receiver_pk.clone());
5869 let (sender_handle, receiver_handle) = receiver_control
5870 .register(0, TEST_QUOTA)
5871 .await
5872 .expect("registration should succeed");
5873
5874 let leader_control = oracle.control(leader_pk.clone());
5876 let (mut leader_sender, _leader_receiver) = leader_control
5877 .register(0, TEST_QUOTA)
5878 .await
5879 .expect("registration should succeed");
5880 oracle
5881 .add_link(leader_pk.clone(), receiver_pk.clone(), DEFAULT_LINK)
5882 .await
5883 .expect("link should be added");
5884
5885 oracle.manager().track(0, participants.clone());
5887 context.sleep(Duration::from_millis(10)).await;
5888
5889 let scheme = Scheme::signer(
5890 SCHEME_NAMESPACE,
5891 participants.clone(),
5892 private_keys[receiver_idx].clone(),
5893 )
5894 .expect("signer scheme should be created");
5895
5896 let config: Config<_, _, _, _, C, _, _, _> = Config {
5897 scheme_provider: MultiEpochProvider::single(scheme),
5898 blocker: receiver_control.clone(),
5899 shard_codec_cfg: CodecConfig {
5900 maximum_shard_size: MAX_SHARD_SIZE,
5901 },
5902 block_codec_cfg: (),
5903 strategy: STRATEGY,
5904 mailbox_size: NZUsize!(1024),
5905 peer_buffer_size: NZUsize!(64),
5906 background_channel_capacity: NZUsize!(1024),
5907 peer_provider: oracle.manager(),
5908 };
5909
5910 let (engine, mailbox) = ShardEngine::new(context.child("receiver"), config);
5911 engine.start((sender_handle, receiver_handle));
5912
5913 let coding_config = coding_config_for_participants(num_peers as u16);
5915 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
5916 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
5917 let commitment = coded_block.commitment();
5918
5919 let receiver_participant = participants
5920 .index(&receiver_pk)
5921 .expect("receiver must be a participant");
5922 let leader_shard = coded_block
5923 .shard(receiver_participant.get() as u16)
5924 .expect("missing shard");
5925 let shard_bytes = leader_shard.encode();
5926
5927 leader_sender.send(
5929 Recipients::One(receiver_pk.clone()),
5930 shard_bytes.clone(),
5931 true,
5932 );
5933 context.sleep(DEFAULT_LINK.latency * 2).await;
5934
5935 let remaining: Set<P> =
5937 Set::from_iter_dedup(peer_keys.iter().filter(|pk| **pk != leader_pk).cloned());
5938 oracle.manager().track(1, remaining);
5939 context.sleep(Duration::from_millis(10)).await;
5940
5941 leader_sender.send(Recipients::One(receiver_pk.clone()), shard_bytes, true);
5944 context.sleep(DEFAULT_LINK.latency * 2).await;
5945
5946 let mut shard_sub = mailbox.subscribe_assigned_shard_verified(commitment);
5949 mailbox.discovered(
5950 commitment,
5951 leader_pk.clone(),
5952 Round::new(Epoch::zero(), View::new(1)),
5953 );
5954 context.sleep(DEFAULT_LINK.latency * 2).await;
5955
5956 assert!(
5958 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
5959 "shard subscription should not resolve after evicted leader's buffer"
5960 );
5961 assert!(
5962 mailbox.get(commitment).await.is_none(),
5963 "block should not reconstruct from evicted buffers"
5964 );
5965 });
5966 }
5967
5968 #[test_traced]
5969 fn test_peer_buffer_lifetime_tracks_latest_primary() {
5970 let executor = deterministic::Runner::default();
5971 executor.start(|context| async move {
5972 let (network, oracle) = simulated::Network::<deterministic::Context, P>::new(
5973 context.child("network"),
5974 simulated::Config {
5975 max_size: MAX_SHARD_SIZE as u32,
5976 max_peers_per_set: NZUsize!(1),
5977 disconnect_on_block: true,
5978 tracked_peer_sets: NZUsize!(1),
5979 },
5980 );
5981 network.start();
5982
5983 let mut private_keys = (0..4)
5984 .map(|i| PrivateKey::from_seed(i as u64))
5985 .collect::<Vec<_>>();
5986 private_keys.sort_by_key(|s| s.public_key());
5987 let peer_keys: Vec<P> = private_keys.iter().map(|c| c.public_key()).collect();
5988 let receiver_pk = peer_keys[0].clone();
5989 let sender_pk = peer_keys[1].clone();
5990 let participants: Set<P> = Set::from_iter_dedup(peer_keys);
5991
5992 let receiver_control = oracle.control(receiver_pk);
5993 let scheme = Scheme::signer(
5994 SCHEME_NAMESPACE,
5995 participants.clone(),
5996 private_keys[0].clone(),
5997 )
5998 .expect("signer scheme should be created");
5999
6000 let config: Config<_, _, _, _, C, _, _, _> = Config {
6001 scheme_provider: MultiEpochProvider::single(scheme),
6002 blocker: receiver_control,
6003 shard_codec_cfg: CodecConfig {
6004 maximum_shard_size: MAX_SHARD_SIZE,
6005 },
6006 block_codec_cfg: (),
6007 strategy: STRATEGY,
6008 mailbox_size: NZUsize!(16),
6009 peer_buffer_size: NZUsize!(4),
6010 background_channel_capacity: NZUsize!(16),
6011 peer_provider: oracle.manager(),
6012 };
6013
6014 let (mut engine, _mailbox) = ShardEngine::new(context.child("engine"), config);
6015
6016 engine.update_latest_primary_peers(Set::from_iter_dedup([sender_pk.clone()]));
6019
6020 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
6021 let coded_block = CodedBlock::<B, C, H>::new(
6022 inner,
6023 coding_config_for_participants(participants.len() as u16),
6024 &STRATEGY,
6025 );
6026 let shard = coded_block.shard(0).expect("missing shard");
6027
6028 engine.buffer_peer_shard(sender_pk.clone(), shard);
6030 assert_eq!(
6031 engine.peer_buffers.get(&sender_pk).map(VecDeque::len),
6032 Some(1),
6033 "peer buffer should contain the buffered shard"
6034 );
6035
6036 engine.update_latest_primary_peers(Set::default());
6039 assert!(
6040 !engine.peer_buffers.contains_key(&sender_pk),
6041 "peer buffer should be evicted once sender leaves latest.primary"
6042 );
6043 });
6044 }
6045
6046 #[test_traced]
6047 fn test_old_epoch_buffered_shards_are_dropped_after_cutover() {
6048 let executor = deterministic::Runner::default();
6049 executor.start(|context| async move {
6050 let num_peers = 6usize;
6051 let (network, oracle) = simulated::Network::<deterministic::Context, P>::new(
6052 context.child("network"),
6053 simulated::Config {
6054 max_size: MAX_SHARD_SIZE as u32,
6055 max_peers_per_set: NZUsize!(num_peers - 1),
6056 disconnect_on_block: true,
6057 tracked_peer_sets: NZUsize!(2),
6058 },
6059 );
6060 network.start();
6061
6062 let mut private_keys = (0..num_peers)
6063 .map(|i| PrivateKey::from_seed(i as u64))
6064 .collect::<Vec<_>>();
6065 private_keys.sort_by_key(|s| s.public_key());
6066 let peer_keys: Vec<P> = private_keys.iter().map(|c| c.public_key()).collect();
6067
6068 let epoch0_set: Set<P> = Set::from_iter_dedup(peer_keys[..5].iter().cloned());
6071 let epoch1_set: Set<P> = Set::from_iter_dedup([
6072 peer_keys[1].clone(),
6073 peer_keys[2].clone(),
6074 peer_keys[3].clone(),
6075 peer_keys[4].clone(),
6076 peer_keys[5].clone(),
6077 ]);
6078
6079 let receiver_idx = 3usize;
6080 let receiver_pk = peer_keys[receiver_idx].clone();
6081 let receiver_key = private_keys[receiver_idx].clone();
6082 let leader_pk = peer_keys[0].clone();
6083
6084 let receiver_control = oracle.control(receiver_pk.clone());
6085 let (sender_handle, receiver_handle) = receiver_control
6086 .register(0, TEST_QUOTA)
6087 .await
6088 .expect("registration should succeed");
6089
6090 let leader_control = oracle.control(leader_pk.clone());
6091 let (mut leader_sender, _leader_receiver) = leader_control
6092 .register(0, TEST_QUOTA)
6093 .await
6094 .expect("registration should succeed");
6095 oracle
6096 .add_link(leader_pk.clone(), receiver_pk.clone(), DEFAULT_LINK)
6097 .await
6098 .expect("link should be added");
6099
6100 oracle.manager().track(0, epoch0_set.clone());
6102 context.sleep(Duration::from_millis(10)).await;
6103
6104 let scheme_epoch0 =
6105 Scheme::signer(SCHEME_NAMESPACE, epoch0_set.clone(), receiver_key.clone())
6106 .expect("epoch 0 signer scheme should be created");
6107 let scheme_epoch1 =
6108 Scheme::signer(SCHEME_NAMESPACE, epoch1_set.clone(), receiver_key.clone())
6109 .expect("epoch 1 signer scheme should be created");
6110
6111 let config: Config<_, _, _, _, C, _, _, _> = Config {
6112 scheme_provider: MultiEpochProvider::single(scheme_epoch0)
6113 .with_epoch(Epoch::new(1), scheme_epoch1),
6114 blocker: receiver_control.clone(),
6115 shard_codec_cfg: CodecConfig {
6116 maximum_shard_size: MAX_SHARD_SIZE,
6117 },
6118 block_codec_cfg: (),
6119 strategy: STRATEGY,
6120 mailbox_size: NZUsize!(1024),
6121 peer_buffer_size: NZUsize!(64),
6122 background_channel_capacity: NZUsize!(1024),
6123 peer_provider: oracle.manager(),
6124 };
6125
6126 let (engine, mailbox) = ShardEngine::new(context.child("receiver"), config);
6128 engine.start((sender_handle, receiver_handle));
6129
6130 let coding_config = coding_config_for_participants(epoch0_set.len() as u16);
6131 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
6132 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
6133 let commitment = coded_block.commitment();
6134
6135 let receiver_participant = epoch0_set
6136 .index(&receiver_pk)
6137 .expect("receiver must be an epoch 0 participant");
6138 let leader_shard = coded_block
6139 .shard(receiver_participant.get() as u16)
6140 .expect("missing shard");
6141
6142 leader_sender.send(
6144 Recipients::One(receiver_pk.clone()),
6145 leader_shard.encode(),
6146 true,
6147 );
6148 context.sleep(DEFAULT_LINK.latency * 2).await;
6149
6150 oracle.manager().track(1, epoch1_set);
6154 context.sleep(Duration::from_millis(10)).await;
6155
6156 let mut shard_sub = mailbox.subscribe_assigned_shard_verified(commitment);
6159 mailbox.discovered(
6160 commitment,
6161 leader_pk,
6162 Round::new(Epoch::zero(), View::new(1)),
6163 );
6164 context.sleep(DEFAULT_LINK.latency * 2).await;
6165
6166 assert!(
6167 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
6168 "old-epoch shard subscription should stay pending after cutover"
6169 );
6170 assert!(
6171 mailbox.get(commitment).await.is_none(),
6172 "old-epoch commitment should not reconstruct from overlap-only buffered shards"
6173 );
6174 });
6175 }
6176
6177 #[test_traced]
6186 fn test_evicted_node_still_reconstructs_from_buffered_peer_shards() {
6187 let executor = deterministic::Runner::default();
6188 executor.start(|context| async move {
6189 let num_peers = 10usize;
6190 let (network, oracle) = simulated::Network::<deterministic::Context, P>::new(
6191 context.child("network"),
6192 simulated::Config {
6193 max_size: MAX_SHARD_SIZE as u32,
6194 max_peers_per_set: NZUsize!(num_peers),
6195 disconnect_on_block: true,
6196 tracked_peer_sets: NZUsize!(2),
6197 },
6198 );
6199 network.start();
6200
6201 let mut private_keys = (0..num_peers)
6202 .map(|i| PrivateKey::from_seed(i as u64))
6203 .collect::<Vec<_>>();
6204 private_keys.sort_by_key(|s| s.public_key());
6205 let peer_keys: Vec<P> = private_keys.iter().map(|c| c.public_key()).collect();
6206 let participants: Set<P> = Set::from_iter_dedup(peer_keys.clone());
6207
6208 let receiver_idx = 1usize;
6212 let receiver_pk = peer_keys[receiver_idx].clone();
6213 let leader_pk = peer_keys[0].clone();
6214 let peer2_pk = peer_keys[2].clone();
6215 let peer4_pk = peer_keys[4].clone();
6216 let peer5_pk = peer_keys[5].clone();
6217 let peer6_pk = peer_keys[6].clone();
6218
6219 let receiver_control = oracle.control(receiver_pk.clone());
6220 let (evicted_sender, evicted_receiver) = receiver_control
6221 .register(0, TEST_QUOTA)
6222 .await
6223 .expect("registration should succeed");
6224
6225 let peer2_control = oracle.control(peer2_pk.clone());
6226 let (mut peer2_sender, _peer2_receiver) = peer2_control
6227 .register(0, TEST_QUOTA)
6228 .await
6229 .expect("registration should succeed");
6230
6231 let peer4_control = oracle.control(peer4_pk.clone());
6232 let (mut peer4_sender, _peer4_receiver) = peer4_control
6233 .register(0, TEST_QUOTA)
6234 .await
6235 .expect("registration should succeed");
6236
6237 let peer5_control = oracle.control(peer5_pk.clone());
6238 let (mut peer5_sender, _peer5_receiver) = peer5_control
6239 .register(0, TEST_QUOTA)
6240 .await
6241 .expect("registration should succeed");
6242
6243 let peer6_control = oracle.control(peer6_pk.clone());
6244 let (mut peer6_sender, _peer6_receiver) = peer6_control
6245 .register(0, TEST_QUOTA)
6246 .await
6247 .expect("registration should succeed");
6248
6249 for sender in [&peer2_pk, &peer4_pk, &peer5_pk, &peer6_pk] {
6251 oracle
6252 .add_link(sender.clone(), receiver_pk.clone(), DEFAULT_LINK)
6253 .await
6254 .expect("link should be added");
6255 }
6256
6257 oracle.manager().track(0, participants.clone());
6259 context.sleep(Duration::from_millis(10)).await;
6260
6261 let scheme = Scheme::signer(
6262 SCHEME_NAMESPACE,
6263 participants.clone(),
6264 private_keys[receiver_idx].clone(),
6265 )
6266 .expect("signer scheme should be created");
6267
6268 let config: Config<_, _, _, _, C, _, _, _> = Config {
6269 scheme_provider: MultiEpochProvider::single(scheme),
6270 blocker: receiver_control.clone(),
6271 shard_codec_cfg: CodecConfig {
6272 maximum_shard_size: MAX_SHARD_SIZE,
6273 },
6274 block_codec_cfg: (),
6275 strategy: STRATEGY,
6276 mailbox_size: NZUsize!(1024),
6277 peer_buffer_size: NZUsize!(64),
6278 background_channel_capacity: NZUsize!(1024),
6279 peer_provider: oracle.manager(),
6280 };
6281
6282 let (engine, mailbox) = ShardEngine::new(context.child("evicted"), config);
6283 engine.start((evicted_sender, evicted_receiver));
6284
6285 let coding_config = coding_config_for_participants(num_peers as u16);
6286 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
6287 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
6288 let commitment = coded_block.commitment();
6289
6290 let peer2_shard = coded_block.shard(2).expect("missing shard 2").encode();
6291 let peer4_shard = coded_block.shard(4).expect("missing shard 4").encode();
6292 let peer5_shard = coded_block.shard(5).expect("missing shard 5").encode();
6293 let peer6_shard = coded_block.shard(6).expect("missing shard 6").encode();
6294
6295 let block_sub = mailbox.subscribe(commitment);
6296
6297 peer2_sender
6300 .send(
6301 Recipients::One(receiver_pk.clone()),
6302 peer2_shard,
6303 true,
6304 );
6305 peer4_sender
6306 .send(
6307 Recipients::One(receiver_pk.clone()),
6308 peer4_shard,
6309 true,
6310 );
6311 peer5_sender
6312 .send(
6313 Recipients::One(receiver_pk.clone()),
6314 peer5_shard,
6315 true,
6316 );
6317 peer6_sender
6318 .send(
6319 Recipients::One(receiver_pk.clone()),
6320 peer6_shard,
6321 true,
6322 );
6323 context.sleep(DEFAULT_LINK.latency * 2).await;
6324
6325 let latest_primary: Set<P> = Set::from_iter_dedup(
6328 peer_keys
6329 .iter()
6330 .filter(|pk| **pk != receiver_pk)
6331 .cloned(),
6332 );
6333 oracle.manager().track(1, latest_primary);
6334 context.sleep(Duration::from_millis(10)).await;
6335
6336 mailbox
6339 .discovered(
6340 commitment,
6341 leader_pk.clone(),
6342 Round::new(Epoch::zero(), View::new(1)),
6343 );
6344
6345 select! {
6346 _ = block_sub => {},
6347 _ = context.sleep(Duration::from_secs(5)) => {
6348 panic!("block subscription did not resolve after leader discovery");
6349 },
6350 }
6351
6352 context.sleep(DEFAULT_LINK.latency * 2).await;
6353 let block = mailbox.get(commitment).await;
6354 assert!(
6355 block.is_some(),
6356 "evicted node should reconstruct from buffered shards sent by remaining latest.primary peers"
6357 );
6358 assert_eq!(block.unwrap().commitment(), commitment);
6359
6360 assert!(
6361 oracle.blocked().await.unwrap().is_empty(),
6362 "no peer should be blocked when overlapping shards are valid"
6363 );
6364 });
6365 }
6366
6367 #[test_traced]
6372 fn test_late_leader_shard_accepted_after_quorum_transition() {
6373 let fixture = Fixture {
6374 num_primary_peers: 10,
6375 ..Default::default()
6376 };
6377
6378 fixture.start(
6379 |config, context, oracle, mut peers, _, coding_config| async move {
6380 let inner = B::new(Sha256Digest::EMPTY, Height::new(1), 100);
6381 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
6382 let commitment = coded_block.commitment();
6383 let round = Round::new(Epoch::zero(), View::new(1));
6384
6385 let leader_idx = 0usize;
6386 let victim_idx = 1usize;
6387 let leader = peers[leader_idx].public_key.clone();
6388 let victim = peers[victim_idx].public_key.clone();
6389
6390 oracle
6393 .remove_link(leader.clone(), victim.clone())
6394 .await
6395 .expect("remove_link should succeed");
6396
6397 peers[leader_idx]
6400 .mailbox
6401 .proposed(round, coded_block.clone());
6402
6403 for peer in peers[1..].iter_mut() {
6405 peer.mailbox.discovered(commitment, leader.clone(), round);
6406 }
6407
6408 context.sleep(config.link.latency * 4).await;
6412
6413 let block_sub = peers[victim_idx].mailbox.subscribe(commitment);
6414 select! {
6415 result = block_sub => {
6416 let reconstructed = result.expect("block subscription should resolve");
6417 assert_eq!(reconstructed.commitment(), commitment);
6418 },
6419 _ = context.sleep(Duration::from_secs(5)) => {
6420 panic!("victim did not reconstruct block from gossip");
6421 },
6422 }
6423
6424 let mut shard_sub = peers[victim_idx]
6427 .mailbox
6428 .subscribe_assigned_shard_verified(commitment);
6429 assert!(
6430 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
6431 "shard subscription must not resolve before own shard is verified"
6432 );
6433
6434 oracle
6436 .add_link(leader.clone(), victim.clone(), DEFAULT_LINK)
6437 .await
6438 .expect("add_link should succeed");
6439
6440 let leader_shard = coded_block
6444 .shard(peers[victim_idx].index.get() as u16)
6445 .expect("missing victim shard");
6446 peers[leader_idx].sender.send(
6447 Recipients::One(victim.clone()),
6448 leader_shard.encode(),
6449 true,
6450 );
6451 context.sleep(config.link.latency * 2).await;
6452
6453 select! {
6456 _ = shard_sub => {},
6457 _ = context.sleep(Duration::from_secs(5)) => {
6458 panic!("shard subscription did not resolve after late leader shard");
6459 },
6460 }
6461
6462 let blocked = oracle.blocked().await.unwrap();
6464 assert!(
6465 blocked.is_empty(),
6466 "no peer should be blocked in late leader shard test"
6467 );
6468
6469 let extra_sender_idx = 2usize;
6472 let extra_shard = coded_block
6473 .shard(peers[extra_sender_idx].index.get() as u16)
6474 .expect("missing shard");
6475 peers[extra_sender_idx].sender.send(
6476 Recipients::One(victim.clone()),
6477 extra_shard.encode(),
6478 true,
6479 );
6480 context.sleep(config.link.latency * 2).await;
6481
6482 let blocked = oracle.blocked().await.unwrap();
6484 assert!(
6485 blocked.is_empty(),
6486 "gossip shard after full reconstruction should be silently ignored"
6487 );
6488 },
6489 );
6490 }
6491}