1use super::{
142 mailbox::{Mailbox, Message},
143 metrics::ShardMetrics,
144};
145use crate::{
146 marshal::coding::{
147 types::{CodedBlock, Shard},
148 validation::{validate_reconstruction, ReconstructionError as InvariantError},
149 },
150 types::{coding::Commitment, Epoch, Round},
151 Block, CertifiableBlock, Heightable,
152};
153use commonware_actor::mailbox;
154use commonware_codec::{Decode, Error as CodecError, Read};
155use commonware_coding::{Config as CodingConfig, Scheme as CodingScheme};
156use commonware_cryptography::{
157 certificate::{Provider, Scheme as CertificateScheme},
158 Committable, Digestible, Hasher, PublicKey,
159};
160use commonware_macros::select_loop;
161use commonware_p2p::{
162 utils::codec::{WrappedBackgroundReceiver, WrappedSender},
163 Blocker, Provider as PeerProvider, Receiver, Recipients, Sender,
164};
165use commonware_parallel::Strategy;
166use commonware_runtime::{
167 spawn_cell,
168 telemetry::metrics::{GaugeExt, HistogramExt},
169 BufferPooler, Clock, ContextCell, Handle, Metrics, Spawner,
170};
171use commonware_utils::{
172 bitmap::BitMap,
173 channel::{fallible::OneshotExt, oneshot},
174 ordered::{Quorum, Set},
175};
176use rand_core::Rng;
177use std::{
178 collections::{BTreeMap, VecDeque},
179 num::NonZeroUsize,
180 sync::Arc,
181};
182use thiserror::Error;
183use tracing::{debug, warn};
184
185#[derive(Debug, Error)]
187pub enum Error<C: CodingScheme> {
188 #[error(transparent)]
190 Coding(C::Error),
191
192 #[error(transparent)]
194 Codec(#[from] CodecError),
195
196 #[error("block digest mismatch: reconstructed block does not match commitment digest")]
198 DigestMismatch,
199
200 #[error("block config mismatch: reconstructed config does not match commitment config")]
202 ConfigMismatch,
203
204 #[error("block context mismatch: reconstructed context does not match commitment context")]
206 ContextMismatch,
207}
208
209#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
210enum BlockSubscriptionKey<D> {
211 Commitment(Commitment),
212 Digest(D),
213}
214
215pub struct Config<P, S, X, D, C, H, B, T>
217where
218 P: PublicKey,
219 S: Provider<Scope = Epoch>,
220 X: Blocker<PublicKey = P>,
221 D: PeerProvider<PublicKey = P>,
222 C: CodingScheme,
223 H: Hasher,
224 B: CertifiableBlock,
225 T: Strategy,
226{
227 pub scheme_provider: S,
229
230 pub blocker: X,
232
233 pub shard_codec_cfg: <Shard<C, H> as Read>::Cfg,
235
236 pub block_codec_cfg: B::Cfg,
238
239 pub strategy: T,
241
242 pub mailbox_size: NonZeroUsize,
244
245 pub peer_buffer_size: NonZeroUsize,
256
257 pub background_channel_capacity: NonZeroUsize,
263
264 pub peer_provider: D,
270}
271
272struct ReconstructedBlock<B, C, H>
274where
275 B: Block,
276 C: CodingScheme,
277 H: Hasher,
278{
279 round: Round,
280 block: Arc<CodedBlock<B, C, H>>,
281}
282
283pub struct Engine<E, S, X, D, C, H, B, P, T>
288where
289 E: BufferPooler + Rng + Spawner + Metrics + Clock,
290 S: Provider<Scope = Epoch>,
291 S::Scheme: CertificateScheme<PublicKey = P>,
292 X: Blocker,
293 D: PeerProvider<PublicKey = P>,
294 C: CodingScheme,
295 H: Hasher,
296 B: CertifiableBlock,
297 P: PublicKey,
298 T: Strategy,
299{
300 context: ContextCell<E>,
302
303 mailbox: mailbox::Receiver<Message<B, C, H, P>>,
305
306 scheme_provider: S,
308
309 blocker: X,
311
312 shard_codec_cfg: <Shard<C, H> as Read>::Cfg,
314
315 block_codec_cfg: B::Cfg,
317
318 strategy: T,
320
321 state: BTreeMap<Commitment, ReconstructionState<P, C, H>>,
323
324 peer_buffers: BTreeMap<P, VecDeque<Shard<C, H>>>,
329
330 peer_buffer_size: NonZeroUsize,
332
333 peer_provider: D,
335
336 aggregate_peers: Set<P>,
339
340 latest_primary_peers: Set<P>,
342
343 background_channel_capacity: NonZeroUsize,
345
346 reconstructed_blocks: BTreeMap<Commitment, ReconstructedBlock<B, C, H>>,
350
351 assigned_shard_verified_subscriptions: BTreeMap<Commitment, Vec<oneshot::Sender<()>>>,
362
363 #[allow(clippy::type_complexity)]
366 block_subscriptions:
367 BTreeMap<BlockSubscriptionKey<B::Digest>, Vec<oneshot::Sender<Arc<CodedBlock<B, C, H>>>>>,
368
369 metrics: ShardMetrics<P>,
371}
372
373impl<E, S, X, D, C, H, B, P, T> Engine<E, S, X, D, C, H, B, P, T>
374where
375 E: BufferPooler + Rng + Spawner + Metrics + Clock,
376 S: Provider<Scope = Epoch>,
377 S::Scheme: CertificateScheme<PublicKey = P>,
378 X: Blocker<PublicKey = P>,
379 D: PeerProvider<PublicKey = P>,
380 C: CodingScheme,
381 H: Hasher,
382 B: CertifiableBlock,
383 P: PublicKey,
384 T: Strategy,
385{
386 pub fn new(context: E, config: Config<P, S, X, D, C, H, B, T>) -> (Self, Mailbox<B, C, H, P>) {
388 let metrics = ShardMetrics::new(&context);
389 let (sender, mailbox) = mailbox::new(context.child("mailbox"), config.mailbox_size);
390 (
391 Self {
392 context: ContextCell::new(context),
393 mailbox,
394 scheme_provider: config.scheme_provider,
395 blocker: config.blocker,
396 shard_codec_cfg: config.shard_codec_cfg,
397 block_codec_cfg: config.block_codec_cfg,
398 strategy: config.strategy,
399 state: BTreeMap::new(),
400 peer_buffers: BTreeMap::new(),
401 peer_buffer_size: config.peer_buffer_size,
402 peer_provider: config.peer_provider,
403 aggregate_peers: Set::default(),
404 latest_primary_peers: Set::default(),
405 background_channel_capacity: config.background_channel_capacity,
406 reconstructed_blocks: BTreeMap::new(),
407 assigned_shard_verified_subscriptions: BTreeMap::new(),
408 block_subscriptions: BTreeMap::new(),
409 metrics,
410 },
411 Mailbox::new(sender),
412 )
413 }
414
415 pub fn start(
417 mut self,
418 network: (impl Sender<PublicKey = P>, impl Receiver<PublicKey = P>),
419 ) -> Handle<()> {
420 spawn_cell!(self.context, self.run(network))
421 }
422
423 async fn run(
425 mut self,
426 (sender, receiver): (impl Sender<PublicKey = P>, impl Receiver<PublicKey = P>),
427 ) {
428 let mut sender = WrappedSender::<_, Shard<C, H>>::new(
429 self.context.network_buffer_pool().clone(),
430 sender,
431 );
432 let (receiver_service, mut receiver) =
433 WrappedBackgroundReceiver::<_, P, X, _, Shard<C, H>, T>::new(
434 self.context.child("shard_ingress"),
435 receiver,
436 self.shard_codec_cfg.clone(),
437 self.blocker.clone(),
438 self.background_channel_capacity,
439 self.strategy.clone(),
440 );
441 let _receiver_handle = receiver_service.start();
443 let mut peer_set_subscription = self.peer_provider.subscribe().await;
444
445 select_loop! {
446 self.context,
447 on_start => {
448 let _ = self
449 .metrics
450 .reconstruction_states_count
451 .try_set(self.state.len());
452 let _ = self
453 .metrics
454 .reconstructed_blocks_cache_count
455 .try_set(self.reconstructed_blocks.len());
456
457 self.block_subscriptions.retain(|_, subscribers| {
459 subscribers.retain(|tx| !tx.is_closed());
460 !subscribers.is_empty()
461 });
462 self.assigned_shard_verified_subscriptions
463 .retain(|_, subscribers| {
464 subscribers.retain(|tx| !tx.is_closed());
465 !subscribers.is_empty()
466 });
467 },
468 on_stopped => {
469 debug!("received shutdown signal, stopping shard engine");
470 },
471 Some(update) = peer_set_subscription.recv() else {
472 debug!("peer set subscription closed");
473 return;
474 } => {
475 let all_peers = update.all.union();
476 self.update_latest_primary_peers(update.latest.primary);
477 self.aggregate_peers = all_peers;
478 },
479 Some(message) = self.mailbox.recv() else {
480 debug!("shard mailbox closed, stopping shard engine");
481 return;
482 } => {
483 if message.response_closed() {
484 continue;
485 }
486
487 match message {
488 Message::Proposed { block, round } => {
489 self.broadcast_shards(&mut sender, round, block);
490 }
491 Message::Discovered {
492 commitment,
493 leader,
494 round,
495 } => {
496 self.handle_external_proposal(&mut sender, commitment, leader, round);
497 }
498 Message::Notarized { commitment, round } => {
499 self.handle_notarized_commitment(&mut sender, commitment, round);
500 }
501 Message::GetByCommitment {
502 commitment,
503 response,
504 } => {
505 let block = self
506 .reconstructed_blocks
507 .get(&commitment)
508 .map(|entry| entry.block.clone());
509 response.send_lossy(block);
510 }
511 Message::GetByDigest { digest, response } => {
512 let block = self.reconstructed_blocks.values().find_map(|entry| {
513 (entry.block.digest() == digest).then_some(entry.block.clone())
514 });
515 response.send_lossy(block);
516 }
517 Message::SubscribeAssignedShardVerified {
518 commitment,
519 response,
520 } => {
521 self.handle_assigned_shard_verified_subscription(commitment, response);
522 }
523 Message::SubscribeByCommitment {
524 commitment,
525 response,
526 } => {
527 self.handle_block_subscription(
528 BlockSubscriptionKey::Commitment(commitment),
529 response,
530 );
531 }
532 Message::SubscribeByDigest { digest, response } => {
533 self.handle_block_subscription(
534 BlockSubscriptionKey::Digest(digest),
535 response,
536 );
537 }
538 Message::Prune { through } => {
539 self.prune(through);
540 }
541 }
542 },
543 Some((peer, shard)) = receiver.recv() else {
544 debug!("receiver closed, stopping shard engine");
545 return;
546 } => {
547 self.handle_network_shard(&mut sender, peer, shard);
548 },
549 }
550 }
551
552 fn handle_network_shard<Sr: Sender<PublicKey = P>>(
554 &mut self,
555 sender: &mut WrappedSender<Sr, Shard<C, H>>,
556 peer: P,
557 shard: Shard<C, H>,
558 ) {
559 self.metrics.shards_received.get_or_create_by(&peer).inc();
560
561 let commitment = shard.commitment();
562 if !self.should_handle_network_shard(commitment) {
563 return;
564 }
565
566 if let Some(existing) = self.state.get(&commitment) {
567 let round = existing.round();
568 let Some(scheme) = self.scheme_provider.scheme(round.epoch()) else {
569 warn!(%commitment, "no scheme for epoch, ignoring shard");
570 return;
571 };
572
573 if existing.leader().is_none() {
577 if let Some(sender_index) = scheme.participants().index(&peer) {
578 let expected_index: u16 = sender_index
579 .get()
580 .try_into()
581 .expect("participant index impossibly out of bounds");
582 if shard.index() != expected_index {
583 self.buffer_peer_shard(peer, shard);
587 return;
588 }
589 }
590 }
591
592 let state = self
593 .state
594 .get_mut(&commitment)
595 .expect("state checked as present");
596 let progressed = state.on_network_shard(
597 peer,
598 shard,
599 InsertCtx::new(scheme.as_ref(), &self.strategy),
600 &mut self.blocker,
601 );
602 if progressed {
603 self.try_advance(sender, commitment);
604 }
605 } else {
606 self.buffer_peer_shard(peer, shard);
607 }
608 }
609
610 fn should_handle_network_shard(&self, commitment: Commitment) -> bool {
617 if self.reconstructed_blocks.contains_key(&commitment) {
618 return self
623 .state
624 .get(&commitment)
625 .is_some_and(|s| !s.is_assigned_shard_verified());
626 }
627 true
628 }
629
630 #[allow(clippy::type_complexity)]
638 fn try_reconstruct(
639 &mut self,
640 commitment: Commitment,
641 ) -> Result<Option<Arc<CodedBlock<B, C, H>>>, Error<C>> {
642 if let Some(entry) = self.reconstructed_blocks.get(&commitment) {
643 return Ok(Some(entry.block.clone()));
644 }
645 let Some(state) = self.state.get_mut(&commitment) else {
646 return Ok(None);
647 };
648 let round = state.round();
649 if state.checked_shards().len() < usize::from(commitment.config().minimum_shards.get()) {
650 debug!(%commitment, "not enough checked shards to reconstruct block");
651 return Ok(None);
652 }
653 let start = self.context.current();
655 let blob = C::decode(
656 &commitment.config(),
657 &commitment.root(),
658 state.checked_shards().iter(),
659 &self.strategy,
660 )
661 .map_err(Error::Coding)?;
662 self.metrics
663 .erasure_decode_duration
664 .observe_between(start, self.context.current());
665
666 let (inner, config): (B, CodingConfig) =
668 Decode::decode_cfg(&mut blob.as_slice(), &(self.block_codec_cfg.clone(), ()))?;
669
670 match validate_reconstruction::<H, _>(&inner, config, commitment) {
671 Ok(()) => {}
672 Err(InvariantError::BlockDigest) => {
673 return Err(Error::DigestMismatch);
674 }
675 Err(InvariantError::CodingConfig) => {
676 warn!(
677 %commitment,
678 expected_config = ?commitment.config(),
679 actual_config = ?config,
680 "reconstructed block config does not match commitment config, but digest matches"
681 );
682 return Err(Error::ConfigMismatch);
683 }
684 Err(InvariantError::ContextDigest(expected, actual)) => {
685 warn!(
686 %commitment,
687 expected_context_digest = ?expected,
688 actual_context_digest = ?actual,
689 "reconstructed block context digest does not match commitment context digest"
690 );
691 return Err(Error::ContextMismatch);
692 }
693 }
694
695 let block = self.cache_block(round, Arc::new(CodedBlock::new_trusted(inner, commitment)));
698 self.metrics.blocks_reconstructed_total.inc();
699 Ok(Some(block))
700 }
701
702 fn handle_external_proposal<Sr: Sender<PublicKey = P>>(
704 &mut self,
705 sender: &mut WrappedSender<Sr, Shard<C, H>>,
706 commitment: Commitment,
707 leader: P,
708 round: Round,
709 ) {
710 if self.reconstructed_blocks.contains_key(&commitment)
715 && self
716 .state
717 .get(&commitment)
718 .is_none_or(|state| state.leader().is_some())
719 {
720 return;
721 }
722 let Some(scheme) = self.scheme_provider.scheme(round.epoch()) else {
723 warn!(%commitment, "no scheme for epoch, ignoring external proposal");
724 return;
725 };
726 let participants = scheme.participants();
727 if participants.index(&leader).is_none() {
728 warn!(?leader, %commitment, "leader update for non-participant, ignoring");
729 return;
730 }
731 if let Some(state) = self.state.get_mut(&commitment) {
732 if let Some(existing) = state.leader() {
733 if existing != &leader {
734 warn!(
735 existing = ?existing,
736 ?leader,
737 %commitment,
738 "conflicting leader update, ignoring"
739 );
740 }
741 return;
742 }
743 state
744 .set_leader(leader)
745 .expect("leader was checked as absent");
746 } else {
747 let participants_len = u64::try_from(participants.len())
748 .expect("participant count impossibly out of bounds");
749 self.state.insert(
750 commitment,
751 ReconstructionState::new(Some(leader), round, participants_len),
752 );
753 }
754 let buffered_progress = self.ingest_buffered_shards(commitment);
755 if buffered_progress {
756 self.try_advance(sender, commitment);
757 }
758 }
759
760 fn handle_notarized_commitment<Sr: Sender<PublicKey = P>>(
766 &mut self,
767 sender: &mut WrappedSender<Sr, Shard<C, H>>,
768 commitment: Commitment,
769 round: Round,
770 ) {
771 if self.reconstructed_blocks.contains_key(&commitment) {
772 return;
773 }
774 if self.state.contains_key(&commitment) {
775 let buffered_progress = self.ingest_buffered_shards(commitment);
776 if buffered_progress {
777 self.try_advance(sender, commitment);
778 }
779 return;
780 }
781 let Some(scheme) = self.scheme_provider.scheme(round.epoch()) else {
782 warn!(%commitment, "no scheme for epoch, ignoring notarized commitment");
783 return;
784 };
785 let participants_len = u64::try_from(scheme.participants().len())
786 .expect("participant count impossibly out of bounds");
787 self.state.insert(
788 commitment,
789 ReconstructionState::new(None, round, participants_len),
790 );
791 let buffered_progress = self.ingest_buffered_shards(commitment);
792 if buffered_progress {
793 self.try_advance(sender, commitment);
794 }
795 }
796
797 fn buffer_peer_shard(&mut self, peer: P, shard: Shard<C, H>) {
799 if self.latest_primary_peers.position(&peer).is_none() {
800 debug!(
801 ?peer,
802 "pre-leader shard from peer outside latest.primary not buffered"
803 );
804 return;
805 }
806 let queue = self.peer_buffers.entry(peer).or_default();
807 if queue.len() >= self.peer_buffer_size.get() {
808 let _ = queue.pop_front();
809 }
810 queue.push_back(shard);
811 }
812
813 fn update_latest_primary_peers(&mut self, peers: Set<P>) {
814 self.peer_buffers
815 .retain(|peer, _| peers.position(peer).is_some());
816 self.latest_primary_peers = peers;
817 }
818
819 fn ingest_buffered_shards(&mut self, commitment: Commitment) -> bool {
829 let state = self
830 .state
831 .get(&commitment)
832 .expect("buffered shards can only be ingested with reconstruction state");
833 let round = state.round();
834 let leader_known = state.leader().is_some();
835 let Some(scheme) = self.scheme_provider.scheme(round.epoch()) else {
836 warn!(%commitment, "no scheme for epoch, dropping buffered shards");
837 return false;
838 };
839
840 let mut buffered = Vec::new();
841 for (peer, queue) in self.peer_buffers.iter_mut() {
842 let mut i = 0;
843 while i < queue.len() {
844 if queue[i].commitment() != commitment {
845 i += 1;
846 continue;
847 }
848 if !leader_known {
849 let Some(sender_index) = scheme.participants().index(peer) else {
850 i += 1;
851 continue;
852 };
853 let expected_index: u16 = sender_index
854 .get()
855 .try_into()
856 .expect("participant index impossibly out of bounds");
857 if queue[i].index() != expected_index {
858 i += 1;
859 continue;
860 }
861 }
862 let shard = queue.swap_remove_back(i).expect("index is valid");
863 buffered.push((peer.clone(), shard));
864 }
865 }
866
867 let state = self
868 .state
869 .get_mut(&commitment)
870 .expect("reconstruction state checked before buffered shard drain");
871
872 let mut progressed = false;
875 let ctx = InsertCtx::new(scheme.as_ref(), &self.strategy);
876 for (peer, shard) in buffered {
877 progressed |= state.on_network_shard(peer, shard, ctx, &mut self.blocker);
878 }
879 progressed
880 }
881
882 fn cache_block(
884 &mut self,
885 round: Round,
886 block: Arc<CodedBlock<B, C, H>>,
887 ) -> Arc<CodedBlock<B, C, H>> {
888 let commitment = block.commitment();
889 self.reconstructed_blocks.insert(
890 commitment,
891 ReconstructedBlock {
892 round,
893 block: Arc::clone(&block),
894 },
895 );
896 self.notify_block_subscribers(Arc::clone(&block));
897 block
898 }
899
900 fn broadcast_shards<Sr: Sender<PublicKey = P>>(
905 &mut self,
906 sender: &mut WrappedSender<Sr, Shard<C, H>>,
907 round: Round,
908 block: Arc<CodedBlock<B, C, H>>,
909 ) {
910 let commitment = block.commitment();
911
912 let Some(scheme) = self.scheme_provider.scheme(round.epoch()) else {
913 warn!(%commitment, "no scheme available, cannot broadcast shards");
914 return;
915 };
916 let participants = scheme.participants();
917 let Some(me) = scheme.me() else {
918 warn!(
919 %commitment,
920 "cannot broadcast shards: local proposer is not a participant"
921 );
922 return;
923 };
924
925 let shard_count = block.shards(&self.strategy).len();
926 if shard_count != participants.len() {
927 warn!(
928 %commitment,
929 shard_count,
930 participants = participants.len(),
931 "cannot broadcast shards: participant/shard count mismatch"
932 );
933 return;
934 }
935
936 let my_index = me.get() as usize;
937 let leader_shard = block
938 .shard(my_index as u16)
939 .expect("proposer's shard must exist");
940
941 for (index, peer) in participants.iter().enumerate() {
943 if index == my_index {
944 continue;
945 }
946
947 let Some(shard) = block.shard(index as u16) else {
948 warn!(
949 %commitment,
950 index,
951 "cannot broadcast shards: missing shard for participant index"
952 );
953 return;
954 };
955 let _ = sender.send(Recipients::One(peer.clone()), shard, true);
956 }
957
958 let non_participants: Vec<P> = self
960 .aggregate_peers
961 .iter()
962 .filter(|peer| participants.index(peer).is_none())
963 .cloned()
964 .collect();
965 if !non_participants.is_empty() {
966 let _ = sender.send(Recipients::Some(non_participants), leader_shard, true);
967 }
968
969 self.cache_block(round, block);
971
972 self.notify_assigned_shard_verified_subscribers(commitment);
975
976 debug!(?commitment, "broadcasted shards");
977 }
978
979 fn broadcast_shard<Sr: Sender<PublicKey = P>>(
981 &mut self,
982 sender: &mut WrappedSender<Sr, Shard<C, H>>,
983 shard: Shard<C, H>,
984 ) {
985 let commitment = shard.commitment();
986 let peers = sender.send(Recipients::All, shard, true);
987 debug!(
988 ?commitment,
989 peers = peers.len(),
990 "broadcasted shard to all peers"
991 );
992 }
993
994 fn try_advance<Sr: Sender<PublicKey = P>>(
998 &mut self,
999 sender: &mut WrappedSender<Sr, Shard<C, H>>,
1000 commitment: Commitment,
1001 ) {
1002 if let Some(state) = self.state.get_mut(&commitment) {
1003 match state.take_pending_action() {
1004 Some(AssignedShardVerifiedAction::Broadcast(shard)) => {
1005 self.broadcast_shard(sender, shard);
1006 self.notify_assigned_shard_verified_subscribers(commitment);
1007 }
1008 Some(AssignedShardVerifiedAction::NotifyOnly) => {
1009 self.notify_assigned_shard_verified_subscribers(commitment);
1010 }
1011 None => {}
1012 }
1013 }
1014
1015 match self.try_reconstruct(commitment) {
1016 Ok(Some(block)) => {
1017 debug!(
1023 %commitment,
1024 parent = %block.parent(),
1025 height = %block.height(),
1026 "successfully reconstructed block from shards"
1027 );
1028 }
1029 Ok(None) => {
1030 debug!(%commitment, "not enough checked shards to reconstruct block");
1031 }
1032 Err(err) => {
1033 warn!(%commitment, ?err, "failed to reconstruct block from checked shards");
1034 self.state.remove(&commitment);
1035 self.drop_subscriptions(commitment);
1036 self.metrics.reconstruction_failures_total.inc();
1037 }
1038 }
1039 }
1040
1041 fn handle_assigned_shard_verified_subscription(
1046 &mut self,
1047 commitment: Commitment,
1048 response: oneshot::Sender<()>,
1049 ) {
1050 let has_shard = self
1052 .state
1053 .get(&commitment)
1054 .is_some_and(|state| state.is_assigned_shard_verified());
1055 if has_shard {
1056 response.send_lossy(());
1057 return;
1058 }
1059
1060 if !self.state.contains_key(&commitment)
1064 && self.reconstructed_blocks.contains_key(&commitment)
1065 {
1066 response.send_lossy(());
1067 return;
1068 }
1069
1070 self.assigned_shard_verified_subscriptions
1071 .entry(commitment)
1072 .or_default()
1073 .push(response);
1074 }
1075
1076 fn handle_block_subscription(
1078 &mut self,
1079 key: BlockSubscriptionKey<B::Digest>,
1080 response: oneshot::Sender<Arc<CodedBlock<B, C, H>>>,
1081 ) {
1082 let block = match key {
1083 BlockSubscriptionKey::Commitment(commitment) => self
1084 .reconstructed_blocks
1085 .get(&commitment)
1086 .map(|entry| &entry.block),
1087 BlockSubscriptionKey::Digest(digest) => self
1088 .reconstructed_blocks
1089 .values()
1090 .find_map(|entry| (entry.block.digest() == digest).then_some(&entry.block)),
1091 };
1092
1093 if let Some(block) = block {
1095 response.send_lossy(Arc::clone(block));
1096 return;
1097 }
1098
1099 self.block_subscriptions
1100 .entry(key)
1101 .or_default()
1102 .push(response);
1103 }
1104
1105 fn notify_assigned_shard_verified_subscribers(&mut self, commitment: Commitment) {
1108 if let Some(mut subscribers) = self
1109 .assigned_shard_verified_subscriptions
1110 .remove(&commitment)
1111 {
1112 for subscriber in subscribers.drain(..) {
1113 subscriber.send_lossy(());
1114 }
1115 }
1116 }
1117
1118 fn notify_block_subscribers(&mut self, block: Arc<CodedBlock<B, C, H>>) {
1120 let commitment = block.commitment();
1121 let digest = block.digest();
1122
1123 if let Some(mut subscribers) = self
1125 .block_subscriptions
1126 .remove(&BlockSubscriptionKey::Commitment(commitment))
1127 {
1128 for subscriber in subscribers.drain(..) {
1129 subscriber.send_lossy(Arc::clone(&block));
1130 }
1131 }
1132
1133 if let Some(mut subscribers) = self
1135 .block_subscriptions
1136 .remove(&BlockSubscriptionKey::Digest(digest))
1137 {
1138 for subscriber in subscribers.drain(..) {
1139 subscriber.send_lossy(Arc::clone(&block));
1140 }
1141 }
1142 }
1143
1144 fn drop_subscriptions(&mut self, commitment: Commitment) {
1149 self.assigned_shard_verified_subscriptions
1150 .remove(&commitment);
1151 self.block_subscriptions
1152 .remove(&BlockSubscriptionKey::Commitment(commitment));
1153 self.block_subscriptions
1154 .remove(&BlockSubscriptionKey::Digest(
1155 commitment.block::<B::Digest>(),
1156 ));
1157 }
1158
1159 fn prune(&mut self, through: Commitment) {
1169 let cached = self
1170 .reconstructed_blocks
1171 .get(&through)
1172 .map(|entry| (entry.round, entry.block.height()));
1173 if let Some((_, height)) = cached {
1174 self.reconstructed_blocks
1175 .retain(|_, entry| entry.block.height() > height);
1176 }
1177
1178 self.drop_subscriptions(through);
1182 let state_round = self.state.remove(&through).map(|state| state.round());
1183 let cached_round = cached.map(|(round, _)| round);
1184 let Some(round) = state_round.or(cached_round) else {
1185 return;
1186 };
1187
1188 let mut pruned_commitments = Vec::new();
1189 self.state.retain(|c, s| {
1190 let keep = s.round() > round;
1191 if !keep {
1192 pruned_commitments.push(*c);
1193 }
1194 keep
1195 });
1196 for pruned in pruned_commitments {
1197 self.drop_subscriptions(pruned);
1198 }
1199 }
1200}
1201
1202enum ReconstructionState<P, C, H>
1204where
1205 P: PublicKey,
1206 C: CodingScheme,
1207 H: Hasher,
1208{
1209 AwaitingQuorum(AwaitingQuorumState<P, C, H>),
1213 Ready(ReadyState<P, C, H>),
1216}
1217
1218enum AssignedShardVerifiedAction<C: CodingScheme, H: Hasher> {
1223 Broadcast(Shard<C, H>),
1225 NotifyOnly,
1227}
1228
1229struct IndexedShard<C: CodingScheme> {
1231 index: u16,
1232 data: C::Shard,
1233}
1234
1235struct CommonState<P, C, H>
1237where
1238 P: PublicKey,
1239 C: CodingScheme,
1240 H: Hasher,
1241{
1242 leader: Option<P>,
1245 pending_action: Option<AssignedShardVerifiedAction<C, H>>,
1247 checked_shards: Vec<C::CheckedShard>,
1249 contributed: BitMap,
1251 round: Round,
1253 received_shards: BTreeMap<u16, C::Shard>,
1256 assigned_shard_verified: bool,
1258}
1259
1260struct AwaitingQuorumState<P, C, H>
1267where
1268 P: PublicKey,
1269 C: CodingScheme,
1270 H: Hasher,
1271{
1272 common: CommonState<P, C, H>,
1273 pending_shards: BTreeMap<P, IndexedShard<C>>,
1275}
1276
1277struct ReadyState<P, C, H>
1282where
1283 P: PublicKey,
1284 C: CodingScheme,
1285 H: Hasher,
1286{
1287 common: CommonState<P, C, H>,
1288}
1289
1290impl<P, C, H> CommonState<P, C, H>
1291where
1292 P: PublicKey,
1293 C: CodingScheme,
1294 H: Hasher,
1295{
1296 fn new(leader: Option<P>, round: Round, participants_len: u64) -> Self {
1298 Self {
1299 leader,
1300 pending_action: None,
1301 checked_shards: Vec::new(),
1302 contributed: BitMap::zeroes(participants_len),
1303 round,
1304 received_shards: BTreeMap::new(),
1305 assigned_shard_verified: false,
1306 }
1307 }
1308}
1309
1310impl<P, C, H> CommonState<P, C, H>
1311where
1312 P: PublicKey,
1313 C: CodingScheme,
1314 H: Hasher,
1315{
1316 fn verify_assigned_shard(
1325 &mut self,
1326 sender: P,
1327 commitment: Commitment,
1328 shard: IndexedShard<C>,
1329 is_participant: bool,
1330 blocker: &mut impl Blocker<PublicKey = P>,
1331 ) -> bool {
1332 self.received_shards.insert(shard.index, shard.data);
1336 let data = self.received_shards.get(&shard.index).unwrap();
1337 let Ok(checked) = C::check(&commitment.config(), &commitment.root(), shard.index, data)
1338 else {
1339 self.received_shards.remove(&shard.index);
1340 commonware_p2p::block!(blocker, sender, "invalid shard received from leader");
1341 return false;
1342 };
1343
1344 self.contributed.set(u64::from(shard.index), true);
1345 self.checked_shards.push(checked);
1346 self.assigned_shard_verified = true;
1347 self.pending_action = Some(if is_participant {
1348 AssignedShardVerifiedAction::Broadcast(Shard::new(
1349 commitment,
1350 shard.index,
1351 data.clone(),
1352 ))
1353 } else {
1354 AssignedShardVerifiedAction::NotifyOnly
1355 });
1356 true
1357 }
1358}
1359
1360impl<P, C, H> AwaitingQuorumState<P, C, H>
1361where
1362 P: PublicKey,
1363 C: CodingScheme,
1364 H: Hasher,
1365{
1366 fn try_transition(
1369 &mut self,
1370 commitment: Commitment,
1371 participants_len: u64,
1372 strategy: &impl Strategy,
1373 blocker: &mut impl Blocker<PublicKey = P>,
1374 ) -> Option<ReadyState<P, C, H>> {
1375 let minimum = usize::from(commitment.config().minimum_shards.get());
1376 if self.common.checked_shards.len() + self.pending_shards.len() < minimum {
1377 return None;
1378 }
1379
1380 let pending = std::mem::take(&mut self.pending_shards);
1382 let (new_checked, to_block) =
1383 strategy.map_partition_collect_vec(pending, |(peer, shard)| {
1384 let checked = C::check(
1385 &commitment.config(),
1386 &commitment.root(),
1387 shard.index,
1388 &shard.data,
1389 );
1390 (peer, checked.ok())
1391 });
1392
1393 for peer in to_block {
1394 commonware_p2p::block!(blocker, peer, "invalid shard received");
1395 }
1396 for checked in new_checked {
1397 self.common.checked_shards.push(checked);
1398 }
1399
1400 if self.common.checked_shards.len() < minimum {
1402 return None;
1403 }
1404
1405 let round = self.common.round;
1407 let leader = self.common.leader.clone();
1408 let common = std::mem::replace(
1409 &mut self.common,
1410 CommonState::new(leader, round, participants_len),
1411 );
1412 Some(ReadyState { common })
1413 }
1414}
1415
1416struct InsertCtx<'a, Sch, S>
1418where
1419 Sch: CertificateScheme,
1420 S: Strategy,
1421{
1422 scheme: &'a Sch,
1423 strategy: &'a S,
1424 participants_len: u64,
1425}
1426
1427impl<Sch: CertificateScheme, S: Strategy> Clone for InsertCtx<'_, Sch, S> {
1428 fn clone(&self) -> Self {
1429 *self
1430 }
1431}
1432
1433impl<Sch: CertificateScheme, S: Strategy> Copy for InsertCtx<'_, Sch, S> {}
1434
1435impl<'a, Sch: CertificateScheme, S: Strategy> InsertCtx<'a, Sch, S> {
1436 fn new(scheme: &'a Sch, strategy: &'a S) -> Self {
1437 let participants_len = u64::try_from(scheme.participants().len())
1438 .expect("participant count impossibly out of bounds");
1439 Self {
1440 scheme,
1441 strategy,
1442 participants_len,
1443 }
1444 }
1445}
1446
1447impl<P, C, H> ReconstructionState<P, C, H>
1448where
1449 P: PublicKey,
1450 C: CodingScheme,
1451 H: Hasher,
1452{
1453 fn new(leader: Option<P>, round: Round, participants_len: u64) -> Self {
1455 Self::AwaitingQuorum(AwaitingQuorumState {
1456 common: CommonState::new(leader, round, participants_len),
1457 pending_shards: BTreeMap::new(),
1458 })
1459 }
1460
1461 const fn common(&self) -> &CommonState<P, C, H> {
1463 match self {
1464 Self::AwaitingQuorum(state) => &state.common,
1465 Self::Ready(state) => &state.common,
1466 }
1467 }
1468
1469 const fn common_mut(&mut self) -> &mut CommonState<P, C, H> {
1471 match self {
1472 Self::AwaitingQuorum(state) => &mut state.common,
1473 Self::Ready(state) => &mut state.common,
1474 }
1475 }
1476
1477 const fn leader(&self) -> Option<&P> {
1479 self.common().leader.as_ref()
1480 }
1481
1482 fn set_leader(&mut self, leader: P) -> Result<(), P> {
1484 if self.common().leader.is_some() {
1485 return Err(leader);
1486 }
1487 self.common_mut().leader = Some(leader);
1488 Ok(())
1489 }
1490
1491 const fn is_assigned_shard_verified(&self) -> bool {
1493 self.common().assigned_shard_verified
1494 }
1495
1496 const fn round(&self) -> Round {
1498 self.common().round
1499 }
1500
1501 const fn checked_shards(&self) -> &[C::CheckedShard] {
1503 self.common().checked_shards.as_slice()
1504 }
1505
1506 const fn take_pending_action(&mut self) -> Option<AssignedShardVerifiedAction<C, H>> {
1510 self.common_mut().pending_action.take()
1511 }
1512
1513 fn on_network_shard<Sch, S, X>(
1560 &mut self,
1561 sender: P,
1562 shard: Shard<C, H>,
1563 ctx: InsertCtx<'_, Sch, S>,
1564 blocker: &mut X,
1565 ) -> bool
1566 where
1567 Sch: CertificateScheme<PublicKey = P>,
1568 S: Strategy,
1569 X: Blocker<PublicKey = P>,
1570 {
1571 let Some(sender_index) = ctx.scheme.participants().index(&sender) else {
1572 commonware_p2p::block!(blocker, sender, "shard sent by non-participant");
1573 return false;
1574 };
1575 let commitment = shard.commitment();
1576 let indexed = IndexedShard {
1577 index: shard.index(),
1578 data: shard.into_inner(),
1579 };
1580
1581 let leader = self.common().leader.as_ref();
1586 let is_from_leader = leader.is_some_and(|leader| leader == &sender);
1587 let expected_participant = if is_from_leader {
1588 ctx.scheme.me().unwrap_or(sender_index)
1589 } else {
1590 sender_index
1591 };
1592 let expected_index: u16 = expected_participant
1593 .get()
1594 .try_into()
1595 .expect("participant index impossibly out of bounds");
1596 if indexed.index != expected_index {
1597 if leader.is_some() {
1598 commonware_p2p::block!(
1599 blocker,
1600 sender,
1601 shard_index = indexed.index,
1602 expected_index,
1603 "shard index does not match expected index"
1604 );
1605 }
1606 return false;
1607 }
1608
1609 if let Some(existing) = self.common().received_shards.get(&indexed.index) {
1611 if existing != &indexed.data {
1612 commonware_p2p::block!(blocker, sender, "shard equivocation");
1613 }
1614 return false;
1615 }
1616
1617 if self.common().contributed.get(u64::from(indexed.index)) {
1619 return false;
1620 }
1621
1622 if is_from_leader && !self.common().assigned_shard_verified {
1626 let progressed = self.common_mut().verify_assigned_shard(
1627 sender,
1628 commitment,
1629 indexed,
1630 ctx.scheme.me().is_some(),
1631 blocker,
1632 );
1633
1634 if progressed {
1635 if let Self::AwaitingQuorum(state) = self {
1636 if let Some(ready) = state.try_transition(
1637 commitment,
1638 ctx.participants_len,
1639 ctx.strategy,
1640 blocker,
1641 ) {
1642 *self = Self::Ready(ready);
1643 }
1644 }
1645 }
1646 return progressed;
1647 }
1648
1649 let Self::AwaitingQuorum(state) = self else {
1651 return false;
1652 };
1653
1654 state
1656 .common
1657 .received_shards
1658 .insert(indexed.index, indexed.data.clone());
1659 state.common.contributed.set(u64::from(indexed.index), true);
1660 state.pending_shards.insert(sender, indexed);
1661 if let Some(ready) =
1662 state.try_transition(commitment, ctx.participants_len, ctx.strategy, blocker)
1663 {
1664 *self = Self::Ready(ready);
1665 }
1666
1667 true
1668 }
1669}
1670
1671#[cfg(test)]
1672mod tests {
1673 use super::*;
1674 use crate::{
1675 marshal::{
1676 coding::types::coding_config_for_participants, mocks::block::Block as MockBlock,
1677 },
1678 types::{Epoch, Height, View},
1679 };
1680 use bytes::Bytes;
1681 use commonware_codec::Encode;
1682 use commonware_coding::{
1683 CodecConfig, Config as CodingConfig, PhasedAsScheme, ReedSolomon, Zoda,
1684 };
1685 use commonware_cryptography::{
1686 certificate::{Scoped, Subject},
1687 ed25519::{PrivateKey, PublicKey},
1688 impl_certificate_ed25519,
1689 sha256::Digest as Sha256Digest,
1690 Committable, Digest, Sha256, Signer,
1691 };
1692 use commonware_macros::{select, test_traced};
1693 use commonware_p2p::{
1694 simulated::{self, Control, Link, Oracle},
1695 Manager as _, TrackedPeers,
1696 };
1697 use commonware_parallel::Sequential;
1698 use commonware_runtime::{deterministic, Quota, Runner, Supervisor as _};
1699 use commonware_utils::{
1700 channel::oneshot::error::TryRecvError, ordered::Set, NZUsize, Participant,
1701 };
1702 use std::{
1703 future::Future,
1704 marker::PhantomData,
1705 num::NonZeroU32,
1706 sync::{
1707 atomic::{AtomicIsize, Ordering},
1708 Arc,
1709 },
1710 time::Duration,
1711 };
1712
1713 #[derive(Clone, Debug)]
1714 pub struct TestSubject {
1715 pub message: Bytes,
1716 }
1717
1718 impl Subject for TestSubject {
1719 type Namespace = Vec<u8>;
1720
1721 fn namespace<'a>(&self, derived: &'a Self::Namespace) -> &'a [u8] {
1722 derived
1723 }
1724
1725 fn message(&self) -> Bytes {
1726 self.message.clone()
1727 }
1728 }
1729
1730 impl_certificate_ed25519!(TestSubject, Vec<u8>);
1731
1732 const SCHEME_NAMESPACE: &[u8] = b"_COMMONWARE_SHARD_ENGINE_TEST";
1733
1734 const MAX_SHARD_SIZE: usize = 1024 * 1024; const DEFAULT_LINK: Link = Link {
1739 latency: Duration::from_millis(50),
1740 jitter: Duration::ZERO,
1741 success_rate: 1.0,
1742 };
1743
1744 const TEST_QUOTA: Quota = Quota::per_second(NonZeroU32::MAX);
1746
1747 const STRATEGY: Sequential = Sequential;
1749
1750 #[derive(Clone)]
1756 struct MultiEpochProvider {
1757 schemes: BTreeMap<Epoch, Arc<Scheme>>,
1758 }
1759
1760 impl MultiEpochProvider {
1761 fn single(scheme: Scheme) -> Self {
1762 let mut schemes = BTreeMap::new();
1763 schemes.insert(Epoch::zero(), Arc::new(scheme));
1764 Self { schemes }
1765 }
1766
1767 fn with_epoch(mut self, epoch: Epoch, scheme: Scheme) -> Self {
1768 self.schemes.insert(epoch, Arc::new(scheme));
1769 self
1770 }
1771 }
1772
1773 impl Provider for MultiEpochProvider {
1774 type Scope = Epoch;
1775 type Scheme = Scheme;
1776
1777 fn scoped(&self, scope: Epoch) -> Option<Scoped<Scheme>> {
1778 self.schemes.get(&scope).cloned().map(Scoped::scheme)
1779 }
1780 }
1781
1782 #[derive(Clone)]
1785 struct ChurningProvider {
1786 scheme: Arc<Scheme>,
1787 remaining_successes: Arc<AtomicIsize>,
1788 }
1789
1790 impl ChurningProvider {
1791 fn new(scheme: Scheme, successes: isize) -> Self {
1792 Self {
1793 scheme: Arc::new(scheme),
1794 remaining_successes: Arc::new(AtomicIsize::new(successes)),
1795 }
1796 }
1797 }
1798
1799 impl Provider for ChurningProvider {
1800 type Scope = Epoch;
1801 type Scheme = Scheme;
1802
1803 fn scoped(&self, scope: Epoch) -> Option<Scoped<Scheme>> {
1804 if scope != Epoch::zero() {
1805 return None;
1806 }
1807 if self.remaining_successes.fetch_sub(1, Ordering::AcqRel) <= 0 {
1808 return None;
1809 }
1810 Some(Scoped::scheme(Arc::clone(&self.scheme)))
1811 }
1812 }
1813
1814 type B = MockBlock<Sha256Digest, ()>;
1816 type H = Sha256;
1817 type P = PublicKey;
1818 type C = ReedSolomon<H>;
1819 type X = Control<P, deterministic::Context>;
1820 type O = Oracle<P, deterministic::Context>;
1821 type Prov = MultiEpochProvider;
1822 type NetworkSender = simulated::Sender<P, deterministic::Context>;
1823 type D = simulated::Manager<P, deterministic::Context>;
1824 type ShardEngine<S> = Engine<deterministic::Context, Prov, X, D, S, H, B, P, Sequential>;
1825 type ChurningShardEngine<S> =
1826 Engine<deterministic::Context, ChurningProvider, X, D, S, H, B, P, Sequential>;
1827
1828 async fn assert_blocked(oracle: &O, blocker: &P, blocked: &P) {
1829 let blocked_peers = oracle.blocked().await.unwrap();
1830 let is_blocked = blocked_peers
1831 .iter()
1832 .any(|(a, b)| a == blocker && b == blocked);
1833 assert!(is_blocked, "expected {blocker} to have blocked {blocked}");
1834 }
1835
1836 struct Peer<S: CodingScheme = C> {
1838 public_key: PublicKey,
1840 index: Participant,
1842 mailbox: Mailbox<B, S, H, P>,
1844 sender: NetworkSender,
1846 }
1847
1848 #[allow(dead_code)]
1850 struct NonParticipant<S: CodingScheme = C> {
1851 public_key: PublicKey,
1853 mailbox: Mailbox<B, S, H, P>,
1855 sender: NetworkSender,
1857 }
1858
1859 struct Fixture<S: CodingScheme = C> {
1861 num_peers: usize,
1863 num_non_participants: usize,
1865 link: Link,
1867 _marker: PhantomData<S>,
1869 }
1870
1871 impl<S: CodingScheme> Default for Fixture<S> {
1872 fn default() -> Self {
1873 Self {
1874 num_peers: 4,
1875 num_non_participants: 0,
1876 link: DEFAULT_LINK,
1877 _marker: PhantomData,
1878 }
1879 }
1880 }
1881
1882 impl<S: CodingScheme> Fixture<S> {
1883 pub fn start<F: Future<Output = ()>>(
1884 self,
1885 f: impl FnOnce(
1886 Self,
1887 deterministic::Context,
1888 O,
1889 Vec<Peer<S>>,
1890 Vec<NonParticipant<S>>,
1891 CodingConfig,
1892 ) -> F,
1893 ) {
1894 let executor = deterministic::Runner::default();
1895 executor.start(|context| async move {
1896 let mut private_keys = (0..self.num_peers)
1897 .map(|i| PrivateKey::from_seed(i as u64))
1898 .collect::<Vec<_>>();
1899 private_keys.sort_by_key(|s| s.public_key());
1900 let peer_keys: Vec<P> = private_keys.iter().map(|c| c.public_key()).collect();
1901
1902 let participants: Set<P> = Set::from_iter_dedup(peer_keys.clone());
1903
1904 let mut np_private_keys = (0..self.num_non_participants)
1905 .map(|i| PrivateKey::from_seed((self.num_peers + i) as u64))
1906 .collect::<Vec<_>>();
1907 np_private_keys.sort_by_key(|s| s.public_key());
1908 let np_keys: Vec<P> = np_private_keys.iter().map(|k| k.public_key()).collect();
1909
1910 let (network, oracle) =
1911 simulated::Network::<deterministic::Context, P>::new_with_split_peers(
1912 context.child("network"),
1913 simulated::Config {
1914 max_size: MAX_SHARD_SIZE as u32,
1915 disconnect_on_block: true,
1916 tracked_peer_sets: NZUsize!(1),
1917 },
1918 peer_keys.clone(),
1919 np_keys.clone(),
1920 )
1921 .await;
1922 network.start();
1923
1924 let all_keys: Vec<P> = peer_keys.iter().chain(np_keys.iter()).cloned().collect();
1925
1926 let mut registrations = BTreeMap::new();
1927 for key in all_keys.iter() {
1928 let control = oracle.control(key.clone());
1929 let (sender, receiver) = control
1930 .register(0, TEST_QUOTA)
1931 .await
1932 .expect("registration should succeed");
1933 registrations.insert(key.clone(), (control, sender, receiver));
1934 }
1935 for p1 in all_keys.iter() {
1936 for p2 in all_keys.iter() {
1937 if p2 == p1 {
1938 continue;
1939 }
1940 oracle
1941 .add_link(p1.clone(), p2.clone(), self.link.clone())
1942 .await
1943 .expect("link should be added");
1944 }
1945 }
1946
1947 let coding_config =
1948 coding_config_for_participants(u16::try_from(self.num_peers).unwrap());
1949
1950 let mut peers = Vec::with_capacity(self.num_peers);
1951 for (idx, peer_key) in peer_keys.iter().enumerate() {
1952 let (control, sender, receiver) = registrations
1953 .remove(peer_key)
1954 .expect("peer should be registered");
1955
1956 let participant = Participant::new(idx as u32);
1957 let engine_context = context.child("peer").with_attribute("index", idx);
1958
1959 let scheme = Scheme::signer(
1960 SCHEME_NAMESPACE,
1961 participants.clone(),
1962 private_keys[idx].clone(),
1963 )
1964 .expect("signer scheme should be created");
1965 let scheme_provider: Prov = MultiEpochProvider::single(scheme);
1966
1967 let config = Config {
1968 scheme_provider,
1969 blocker: control.clone(),
1970 shard_codec_cfg: CodecConfig {
1971 maximum_shard_size: MAX_SHARD_SIZE,
1972 },
1973 block_codec_cfg: (),
1974 strategy: STRATEGY,
1975 mailbox_size: NZUsize!(1024),
1976 peer_buffer_size: NZUsize!(64),
1977 background_channel_capacity: NZUsize!(1024),
1978 peer_provider: oracle.manager(),
1979 };
1980
1981 let (engine, mailbox) = ShardEngine::new(engine_context, config);
1982 let sender_clone = sender.clone();
1983 engine.start((sender, receiver));
1984
1985 peers.push(Peer {
1986 public_key: peer_key.clone(),
1987 index: participant,
1988 mailbox,
1989 sender: sender_clone,
1990 });
1991 }
1992
1993 let mut non_participants = Vec::with_capacity(self.num_non_participants);
1994 for (idx, np_key) in np_keys.iter().enumerate() {
1995 let (control, sender, receiver) = registrations
1996 .remove(np_key)
1997 .expect("non-participant should be registered");
1998
1999 let engine_context = context
2000 .child("non_participant")
2001 .with_attribute("index", idx);
2002
2003 let scheme = Scheme::verifier(SCHEME_NAMESPACE, participants.clone());
2004 let scheme_provider: Prov = MultiEpochProvider::single(scheme);
2005
2006 let config = Config {
2007 scheme_provider,
2008 blocker: control.clone(),
2009 shard_codec_cfg: CodecConfig {
2010 maximum_shard_size: MAX_SHARD_SIZE,
2011 },
2012 block_codec_cfg: (),
2013 strategy: STRATEGY,
2014 mailbox_size: NZUsize!(1024),
2015 peer_buffer_size: NZUsize!(64),
2016 background_channel_capacity: NZUsize!(1024),
2017 peer_provider: oracle.manager(),
2018 };
2019
2020 let (engine, mailbox) = ShardEngine::new(engine_context, config);
2021 let sender_clone = sender.clone();
2022 engine.start((sender, receiver));
2023
2024 non_participants.push(NonParticipant {
2025 public_key: np_key.clone(),
2026 mailbox,
2027 sender: sender_clone,
2028 });
2029 }
2030
2031 f(
2032 self,
2033 context,
2034 oracle,
2035 peers,
2036 non_participants,
2037 coding_config,
2038 )
2039 .await;
2040 });
2041 }
2042 }
2043
2044 #[test_traced]
2045 fn test_e2e_broadcast_and_reconstruction() {
2046 let fixture = Fixture {
2047 num_peers: 10,
2048 ..Default::default()
2049 };
2050
2051 fixture.start(
2052 |config, context, _, mut peers, _, coding_config| async move {
2053 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2054 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2055 let commitment = coded_block.commitment();
2056
2057 let leader = peers[0].public_key.clone();
2058 let round = Round::new(Epoch::zero(), View::new(1));
2059 peers[0].mailbox.proposed(round, coded_block.clone());
2060
2061 for peer in peers[1..].iter_mut() {
2063 peer.mailbox.discovered(commitment, leader.clone(), round);
2064 }
2065 context.sleep(config.link.latency).await;
2066
2067 for peer in peers.iter_mut() {
2068 peer.mailbox
2069 .subscribe_assigned_shard_verified(commitment)
2070 .await
2071 .expect("shard subscription should complete");
2072 }
2073 context.sleep(config.link.latency).await;
2074
2075 for peer in peers.iter_mut() {
2076 let reconstructed = peer
2077 .mailbox
2078 .get(commitment)
2079 .await
2080 .expect("block should be reconstructed");
2081 assert_eq!(reconstructed.commitment(), commitment);
2082 assert_eq!(reconstructed.height(), coded_block.height());
2083 }
2084 },
2085 );
2086 }
2087
2088 #[test_traced]
2089 fn test_e2e_broadcast_and_reconstruction_zoda() {
2090 let fixture = Fixture {
2091 num_peers: 10,
2092 ..Default::default()
2093 };
2094
2095 fixture.start(
2096 |config, context, _, mut peers, _, coding_config| async move {
2097 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2098 let coded_block = CodedBlock::<B, PhasedAsScheme<Zoda<H>>, H>::new(
2099 inner,
2100 coding_config,
2101 &STRATEGY,
2102 );
2103 let commitment = coded_block.commitment();
2104
2105 let leader = peers[0].public_key.clone();
2106 let round = Round::new(Epoch::zero(), View::new(1));
2107 peers[0].mailbox.proposed(round, coded_block.clone());
2108
2109 for peer in peers[1..].iter_mut() {
2111 peer.mailbox.discovered(commitment, leader.clone(), round);
2112 }
2113 context.sleep(config.link.latency).await;
2114
2115 for peer in peers.iter_mut() {
2116 peer.mailbox
2117 .subscribe_assigned_shard_verified(commitment)
2118 .await
2119 .expect("shard subscription should complete");
2120 }
2121 context.sleep(config.link.latency).await;
2122
2123 for peer in peers.iter_mut() {
2124 let reconstructed = peer
2125 .mailbox
2126 .get(commitment)
2127 .await
2128 .expect("block should be reconstructed");
2129 assert_eq!(reconstructed.commitment(), commitment);
2130 assert_eq!(reconstructed.height(), coded_block.height());
2131 }
2132 },
2133 );
2134 }
2135
2136 #[test_traced]
2137 fn test_block_subscriptions() {
2138 let fixture = Fixture {
2139 num_peers: 10,
2140 ..Default::default()
2141 };
2142
2143 fixture.start(
2144 |config, context, _, mut peers, _, coding_config| async move {
2145 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2146 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2147 let commitment = coded_block.commitment();
2148 let digest = coded_block.digest();
2149
2150 let leader = peers[0].public_key.clone();
2151 let round = Round::new(Epoch::zero(), View::new(1));
2152
2153 let commitment_sub = peers[1].mailbox.subscribe(commitment);
2155 let digest_sub = peers[2].mailbox.subscribe_by_digest(digest);
2156
2157 peers[0].mailbox.proposed(round, coded_block.clone());
2158
2159 for peer in peers[1..].iter_mut() {
2161 peer.mailbox.discovered(commitment, leader.clone(), round);
2162 }
2163 context.sleep(config.link.latency * 2).await;
2164
2165 for peer in peers.iter_mut() {
2166 peer.mailbox
2167 .subscribe_assigned_shard_verified(commitment)
2168 .await
2169 .expect("shard subscription should complete");
2170 }
2171 context.sleep(config.link.latency).await;
2172
2173 let block_by_commitment =
2174 commitment_sub.await.expect("subscription should resolve");
2175 assert_eq!(block_by_commitment.commitment(), commitment);
2176 assert_eq!(block_by_commitment.height(), coded_block.height());
2177
2178 let block_by_digest = digest_sub.await.expect("subscription should resolve");
2179 assert_eq!(block_by_digest.commitment(), commitment);
2180 assert_eq!(block_by_digest.height(), coded_block.height());
2181 },
2182 );
2183 }
2184
2185 #[test_traced]
2186 fn test_proposer_preproposal_subscriptions_resolve_after_local_cache() {
2187 let fixture = Fixture {
2188 num_peers: 10,
2189 ..Default::default()
2190 };
2191
2192 fixture.start(|config, context, _, peers, _, coding_config| async move {
2193 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2194 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2195 let commitment = coded_block.commitment();
2196 let digest = coded_block.digest();
2197 let round = Round::new(Epoch::zero(), View::new(1));
2198
2199 let shard_sub = peers[0].mailbox.subscribe_assigned_shard_verified(commitment);
2201 let commitment_sub = peers[0].mailbox.subscribe(commitment);
2202 let digest_sub = peers[0].mailbox.subscribe_by_digest(digest);
2203
2204 peers[0].mailbox.proposed(round, coded_block.clone());
2205 context.sleep(config.link.latency).await;
2206
2207 select! {
2208 result = shard_sub => {
2209 result.expect("shard subscription should resolve");
2210 },
2211 _ = context.sleep(Duration::from_secs(5)) => {
2212 panic!("shard subscription did not resolve after local proposal cache");
2213 }
2214 }
2215
2216 let block_by_commitment = select! {
2217 result = commitment_sub => {
2218 result.expect("block subscription by commitment should resolve")
2219 },
2220 _ = context.sleep(Duration::from_secs(5)) => {
2221 panic!("block subscription by commitment did not resolve after local proposal cache");
2222 }
2223 };
2224 assert_eq!(block_by_commitment.commitment(), commitment);
2225 assert_eq!(block_by_commitment.height(), coded_block.height());
2226
2227 let block_by_digest = select! {
2228 result = digest_sub => {
2229 result.expect("block subscription by digest should resolve")
2230 },
2231 _ = context.sleep(Duration::from_secs(5)) => {
2232 panic!("block subscription by digest did not resolve after local proposal cache");
2233 }
2234 };
2235 assert_eq!(block_by_digest.commitment(), commitment);
2236 assert_eq!(block_by_digest.height(), coded_block.height());
2237 });
2238 }
2239
2240 #[test_traced]
2241 fn test_shard_subscription_rejects_invalid_shard() {
2242 let fixture = Fixture::<C>::default();
2243 fixture.start(
2244 |config, context, oracle, mut peers, _, coding_config| async move {
2245 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2250 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2251 let commitment = coded_block.commitment();
2252 let receiver_index = peers[2].index.get() as u16;
2253
2254 let valid_shard = coded_block.shard(receiver_index).expect("missing shard");
2255
2256 let mut invalid_shard = valid_shard.clone();
2259 invalid_shard.index = peers[3].index.get() as u16;
2260
2261 let receiver_pk = peers[2].public_key.clone();
2263 let leader = peers[1].public_key.clone();
2264 peers[2].mailbox.discovered(
2265 commitment,
2266 leader,
2267 Round::new(Epoch::zero(), View::new(1)),
2268 );
2269 let mut shard_sub = peers[2]
2270 .mailbox
2271 .subscribe_assigned_shard_verified(commitment);
2272
2273 let invalid_bytes = invalid_shard.encode();
2275 peers[0]
2276 .sender
2277 .send(Recipients::One(receiver_pk.clone()), invalid_bytes, true);
2278
2279 context.sleep(config.link.latency * 2).await;
2280
2281 assert!(
2282 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
2283 "subscription should not resolve from invalid shard"
2284 );
2285 assert_blocked(&oracle, &peers[2].public_key, &peers[0].public_key).await;
2286
2287 let valid_bytes = valid_shard.encode();
2289 peers[1]
2290 .sender
2291 .send(Recipients::One(receiver_pk), valid_bytes, true);
2292 context.sleep(config.link.latency * 2).await;
2293
2294 select! {
2296 _ = shard_sub => {},
2297 _ = context.sleep(Duration::from_secs(5)) => {
2298 panic!("subscription did not complete after valid shard arrival");
2299 },
2300 };
2301 },
2302 );
2303 }
2304
2305 #[test_traced]
2306 fn test_durable_prunes_reconstructed_blocks() {
2307 let fixture = Fixture::<C>::default();
2308 fixture.start(|_, context, _, mut peers, _, coding_config| async move {
2309 let block1 = CodedBlock::<B, C, H>::new(
2311 B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100),
2312 coding_config,
2313 &STRATEGY,
2314 );
2315 let block2 = CodedBlock::<B, C, H>::new(
2316 B::new::<H>((), Sha256Digest::EMPTY, Height::new(2), 100),
2317 coding_config,
2318 &STRATEGY,
2319 );
2320 let block3 = CodedBlock::<B, C, H>::new(
2321 B::new::<H>((), Sha256Digest::EMPTY, Height::new(3), 100),
2322 coding_config,
2323 &STRATEGY,
2324 );
2325 let commitment1 = block1.commitment();
2326 let commitment2 = block2.commitment();
2327 let commitment3 = block3.commitment();
2328
2329 let peer = &mut peers[0];
2331 let round = Round::new(Epoch::zero(), View::new(1));
2332 peer.mailbox.proposed(round, block1);
2333 peer.mailbox.proposed(round, block2);
2334 peer.mailbox.proposed(round, block3);
2335 context.sleep(Duration::from_millis(10)).await;
2336
2337 assert!(
2339 peer.mailbox.get(commitment1).await.is_some(),
2340 "block1 should be cached"
2341 );
2342 assert!(
2343 peer.mailbox.get(commitment2).await.is_some(),
2344 "block2 should be cached"
2345 );
2346 assert!(
2347 peer.mailbox.get(commitment3).await.is_some(),
2348 "block3 should be cached"
2349 );
2350
2351 peer.mailbox.prune(commitment2);
2353 context.sleep(Duration::from_millis(10)).await;
2354
2355 assert!(
2357 peer.mailbox.get(commitment1).await.is_none(),
2358 "block1 should be pruned"
2359 );
2360 assert!(
2361 peer.mailbox.get(commitment2).await.is_none(),
2362 "block2 should be pruned"
2363 );
2364
2365 assert!(
2367 peer.mailbox.get(commitment3).await.is_some(),
2368 "block3 should still be cached"
2369 );
2370 });
2371 }
2372
2373 #[test_traced]
2374 fn test_duplicate_leader_shard_ignored() {
2375 let fixture = Fixture::<C>::default();
2376 fixture.start(
2377 |config, context, oracle, mut peers, _, coding_config| async move {
2378 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2379 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2380 let commitment = coded_block.commitment();
2381
2382 let peer2_index = peers[2].index.get() as u16;
2384 let peer2_shard = coded_block.shard(peer2_index).expect("missing shard");
2385 let shard_bytes = peer2_shard.encode();
2386
2387 let peer2_pk = peers[2].public_key.clone();
2388 let leader = peers[0].public_key.clone();
2389
2390 peers[2].mailbox.discovered(
2392 commitment,
2393 leader,
2394 Round::new(Epoch::zero(), View::new(1)),
2395 );
2396
2397 peers[0]
2399 .sender
2400 .send(Recipients::One(peer2_pk.clone()), shard_bytes.clone(), true);
2401 context.sleep(config.link.latency * 2).await;
2402
2403 peers[0]
2405 .sender
2406 .send(Recipients::One(peer2_pk), shard_bytes, true);
2407 context.sleep(config.link.latency * 2).await;
2408
2409 let blocked_peers = oracle.blocked().await.unwrap();
2411 let is_blocked = blocked_peers
2412 .iter()
2413 .any(|(a, b)| a == &peers[2].public_key && b == &peers[0].public_key);
2414 assert!(
2415 !is_blocked,
2416 "leader should not be blocked for duplicate shard"
2417 );
2418 },
2419 );
2420 }
2421
2422 #[test_traced]
2423 fn test_equivocating_leader_shard_blocks_peer() {
2424 let fixture = Fixture::<C>::default();
2425 fixture.start(
2426 |config, context, oracle, mut peers, _, coding_config| async move {
2427 let inner1 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2428 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
2429 let commitment = coded_block1.commitment();
2430
2431 let inner2 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 200);
2433 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
2434
2435 let peer2_index = peers[2].index.get() as u16;
2437 let shard_bytes1 = coded_block1
2438 .shard(peer2_index)
2439 .expect("missing shard")
2440 .encode();
2441 let mut equivocating_shard =
2442 coded_block2.shard(peer2_index).expect("missing shard");
2443 equivocating_shard.commitment = commitment;
2445 let shard_bytes2 = equivocating_shard.encode();
2446
2447 let peer2_pk = peers[2].public_key.clone();
2448 let leader = peers[0].public_key.clone();
2449
2450 peers[2].mailbox.discovered(
2452 commitment,
2453 leader,
2454 Round::new(Epoch::zero(), View::new(1)),
2455 );
2456
2457 peers[0]
2459 .sender
2460 .send(Recipients::One(peer2_pk.clone()), shard_bytes1, true);
2461 context.sleep(config.link.latency * 2).await;
2462
2463 peers[0]
2465 .sender
2466 .send(Recipients::One(peer2_pk), shard_bytes2, true);
2467 context.sleep(config.link.latency * 2).await;
2468
2469 assert_blocked(&oracle, &peers[2].public_key, &peers[0].public_key).await;
2471 },
2472 );
2473 }
2474
2475 #[test_traced]
2476 fn test_non_leader_wrong_index_shard_blocked() {
2477 let fixture = Fixture::<C>::default();
2480 fixture.start(
2481 |config, context, oracle, mut peers, _, coding_config| async move {
2482 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2483 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2484 let commitment = coded_block.commitment();
2485
2486 let peer2_index = peers[2].index.get() as u16;
2488 let peer2_shard = coded_block.shard(peer2_index).expect("missing shard");
2489 let shard_bytes = peer2_shard.encode();
2490
2491 let peer2_pk = peers[2].public_key.clone();
2492 let leader = peers[0].public_key.clone();
2493
2494 peers[2].mailbox.discovered(
2496 commitment,
2497 leader,
2498 Round::new(Epoch::zero(), View::new(1)),
2499 );
2500
2501 peers[1]
2504 .sender
2505 .send(Recipients::One(peer2_pk), shard_bytes, true);
2506 context.sleep(config.link.latency * 2).await;
2507
2508 assert_blocked(&oracle, &peers[2].public_key, &peers[1].public_key).await;
2510 },
2511 );
2512 }
2513
2514 #[test_traced]
2515 fn test_buffered_wrong_index_shard_blocked_on_leader_arrival() {
2516 let fixture = Fixture::<C>::default();
2519 fixture.start(
2520 |config, context, oracle, mut peers, _, coding_config| async move {
2521 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2522 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2523 let commitment = coded_block.commitment();
2524
2525 let peer2_index = peers[2].index.get() as u16;
2527 let peer2_shard = coded_block.shard(peer2_index).expect("missing shard");
2528 let shard_bytes = peer2_shard.encode();
2529
2530 let peer2_pk = peers[2].public_key.clone();
2531
2532 peers[1]
2535 .sender
2536 .send(Recipients::One(peer2_pk), shard_bytes, true);
2537 context.sleep(config.link.latency * 2).await;
2538
2539 let blocked = oracle.blocked().await.unwrap();
2541 assert!(
2542 blocked.is_empty(),
2543 "no peers should be blocked while leader is unknown"
2544 );
2545
2546 let leader = peers[0].public_key.clone();
2550 peers[2].mailbox.discovered(
2551 commitment,
2552 leader,
2553 Round::new(Epoch::zero(), View::new(1)),
2554 );
2555 context.sleep(Duration::from_millis(10)).await;
2556
2557 assert_blocked(&oracle, &peers[2].public_key, &peers[1].public_key).await;
2558 },
2559 );
2560 }
2561
2562 #[test_traced]
2563 fn test_conflicting_external_proposed_ignored() {
2564 let fixture = Fixture::<C>::default();
2565 fixture.start(
2566 |config, context, oracle, mut peers, _, coding_config| async move {
2567 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2568 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2569 let commitment = coded_block.commitment();
2570
2571 let peer2_index = peers[2].index.get() as u16;
2573 let peer2_shard = coded_block.shard(peer2_index).expect("missing shard");
2574 let shard_bytes = peer2_shard.encode();
2575
2576 let peer2_pk = peers[2].public_key.clone();
2577 let leader_a = peers[0].public_key.clone();
2578 let leader_b = peers[1].public_key.clone();
2579
2580 let shard_sub = peers[2]
2582 .mailbox
2583 .subscribe_assigned_shard_verified(commitment);
2584
2585 peers[2].mailbox.discovered(
2587 commitment,
2588 leader_a.clone(),
2589 Round::new(Epoch::zero(), View::new(1)),
2590 );
2591
2592 peers[2].mailbox.discovered(
2594 commitment,
2595 leader_b,
2596 Round::new(Epoch::zero(), View::new(1)),
2597 );
2598
2599 peers[0]
2601 .sender
2602 .send(Recipients::One(peer2_pk.clone()), shard_bytes.clone(), true);
2603 context.sleep(config.link.latency * 2).await;
2604
2605 select! {
2607 _ = shard_sub => {},
2608 _ = context.sleep(Duration::from_secs(5)) => {
2609 panic!("subscription did not complete after shard from original leader");
2610 },
2611 };
2612
2613 peers[1]
2615 .sender
2616 .send(Recipients::One(peer2_pk), shard_bytes, true);
2617 context.sleep(config.link.latency * 2).await;
2618
2619 assert_blocked(&oracle, &peers[2].public_key, &peers[1].public_key).await;
2620
2621 let blocked_peers = oracle.blocked().await.unwrap();
2623 let leader_a_blocked = blocked_peers
2624 .iter()
2625 .any(|(a, b)| a == &peers[2].public_key && b == &leader_a);
2626 assert!(
2627 !leader_a_blocked,
2628 "original leader should not be blocked after conflicting leader update"
2629 );
2630 },
2631 );
2632 }
2633
2634 #[test_traced]
2635 fn test_non_participant_external_proposed_ignored() {
2636 let fixture = Fixture::<C>::default();
2637 fixture.start(
2638 |config, context, oracle, mut peers, _, coding_config| async move {
2639 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2640 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2641 let commitment = coded_block.commitment();
2642
2643 let peer2_index = peers[2].index.get() as u16;
2645 let peer2_shard = coded_block.shard(peer2_index).expect("missing shard");
2646 let shard_bytes = peer2_shard.encode();
2647
2648 let peer2_pk = peers[2].public_key.clone();
2649 let leader = peers[0].public_key.clone();
2650 let non_participant_leader = PrivateKey::from_seed(10_000).public_key();
2651
2652 let shard_sub = peers[2]
2654 .mailbox
2655 .subscribe_assigned_shard_verified(commitment);
2656
2657 peers[2].mailbox.discovered(
2659 commitment,
2660 non_participant_leader,
2661 Round::new(Epoch::zero(), View::new(1)),
2662 );
2663
2664 peers[0]
2666 .sender
2667 .send(Recipients::One(peer2_pk.clone()), shard_bytes.clone(), true);
2668 context.sleep(config.link.latency * 2).await;
2669
2670 let blocked = oracle.blocked().await.unwrap();
2671 let leader_blocked = blocked
2672 .iter()
2673 .any(|(a, b)| a == &peers[2].public_key && b == &leader);
2674 assert!(
2675 !leader_blocked,
2676 "leader should not be blocked when non-participant update is ignored"
2677 );
2678
2679 peers[2].mailbox.discovered(
2681 commitment,
2682 leader,
2683 Round::new(Epoch::zero(), View::new(1)),
2684 );
2685 context.sleep(config.link.latency * 2).await;
2686
2687 select! {
2688 _ = shard_sub => {},
2689 _ = context.sleep(Duration::from_secs(5)) => {
2690 panic!("subscription did not complete after valid leader update");
2691 },
2692 };
2693 },
2694 );
2695 }
2696
2697 #[test_traced]
2698 fn test_shard_from_non_participant_blocks_peer() {
2699 let fixture = Fixture::<C>::default();
2700 fixture.start(
2701 |config, context, oracle, peers, _, coding_config| async move {
2702 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2703 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2704 let commitment = coded_block.commitment();
2705
2706 let leader = peers[0].public_key.clone();
2707 let receiver_pk = peers[2].public_key.clone();
2708
2709 let non_participant_key = PrivateKey::from_seed(10_000);
2710 let non_participant_pk = non_participant_key.public_key();
2711
2712 let non_participant_control = oracle.control(non_participant_pk.clone());
2713 let (mut non_participant_sender, _non_participant_receiver) =
2714 non_participant_control
2715 .register(0, TEST_QUOTA)
2716 .await
2717 .expect("registration should succeed");
2718 oracle
2719 .add_link(
2720 non_participant_pk.clone(),
2721 receiver_pk.clone(),
2722 DEFAULT_LINK,
2723 )
2724 .await
2725 .expect("link should be added");
2726 oracle.manager().track(
2727 2,
2728 TrackedPeers::new(
2729 Set::from_iter_dedup(peers.iter().map(|peer| peer.public_key.clone())),
2730 Set::from_iter_dedup([non_participant_pk.clone()]),
2731 ),
2732 );
2733 context.sleep(Duration::from_millis(10)).await;
2734
2735 peers[2].mailbox.discovered(
2736 commitment,
2737 leader,
2738 Round::new(Epoch::zero(), View::new(1)),
2739 );
2740
2741 let peer2_index = peers[2].index.get() as u16;
2742 let shard = coded_block.shard(peer2_index).expect("missing shard");
2743 let shard_bytes = shard.encode();
2744
2745 non_participant_sender.send(Recipients::One(receiver_pk), shard_bytes, true);
2746 context.sleep(config.link.latency * 2).await;
2747
2748 assert_blocked(&oracle, &peers[2].public_key, &non_participant_pk).await;
2749 },
2750 );
2751 }
2752
2753 #[test_traced]
2754 fn test_preleader_shard_from_non_participant_is_not_buffered() {
2755 let fixture = Fixture::<C>::default();
2756 fixture.start(
2757 |config, context, oracle, peers, _, coding_config| async move {
2758 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2759 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2760 let commitment = coded_block.commitment();
2761
2762 let leader = peers[0].public_key.clone();
2763 let receiver_pk = peers[2].public_key.clone();
2764
2765 let non_participant_key = PrivateKey::from_seed(10_000);
2766 let non_participant_pk = non_participant_key.public_key();
2767
2768 let non_participant_control = oracle.control(non_participant_pk.clone());
2769 let (mut non_participant_sender, _non_participant_receiver) =
2770 non_participant_control
2771 .register(0, TEST_QUOTA)
2772 .await
2773 .expect("registration should succeed");
2774 oracle
2775 .add_link(
2776 non_participant_pk.clone(),
2777 receiver_pk.clone(),
2778 DEFAULT_LINK,
2779 )
2780 .await
2781 .expect("link should be added");
2782 oracle.manager().track(
2783 2,
2784 TrackedPeers::new(
2785 Set::from_iter_dedup(peers.iter().map(|peer| peer.public_key.clone())),
2786 Set::from_iter_dedup([non_participant_pk.clone()]),
2787 ),
2788 );
2789 context.sleep(Duration::from_millis(10)).await;
2790
2791 let peer2_index = peers[2].index.get() as u16;
2792 let shard = coded_block.shard(peer2_index).expect("missing shard");
2793 let shard_bytes = shard.encode();
2794 let mut shard_sub = peers[2]
2795 .mailbox
2796 .subscribe_assigned_shard_verified(commitment);
2797
2798 non_participant_sender.send(Recipients::One(receiver_pk), shard_bytes, true);
2799 context.sleep(config.link.latency * 2).await;
2800
2801 peers[2].mailbox.discovered(
2802 commitment,
2803 leader,
2804 Round::new(Epoch::zero(), View::new(1)),
2805 );
2806 context.sleep(config.link.latency * 2).await;
2807
2808 let blocked = oracle.blocked().await.unwrap();
2809 let non_participant_blocked = blocked
2810 .iter()
2811 .any(|(a, b)| a == &peers[2].public_key && b == &non_participant_pk);
2812 assert!(
2813 !non_participant_blocked,
2814 "non-participant should not be blocked when its pre-leader shard is ignored"
2815 );
2816 assert!(
2817 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
2818 "pre-leader shard from non-participant should not be buffered"
2819 );
2820 },
2821 );
2822 }
2823
2824 #[test_traced]
2825 fn test_duplicate_shard_ignored() {
2826 let fixture: Fixture<C> = Fixture {
2828 num_peers: 10,
2829 ..Default::default()
2830 };
2831
2832 fixture.start(
2833 |config, context, oracle, mut peers, _, coding_config| async move {
2834 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2835 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
2836
2837 let peer2_index = peers[2].index.get() as u16;
2839 let peer2_shard = coded_block.shard(peer2_index).expect("missing shard");
2840
2841 let peer1_index = peers[1].index.get() as u16;
2843 let peer1_shard = coded_block.shard(peer1_index).expect("missing shard");
2844
2845 let peer2_pk = peers[2].public_key.clone();
2846 let leader = peers[0].public_key.clone();
2847
2848 peers[2].mailbox.discovered(
2850 coded_block.commitment(),
2851 leader,
2852 Round::new(Epoch::zero(), View::new(1)),
2853 );
2854
2855 let leader_shard_bytes = peer2_shard.encode();
2857 peers[0]
2858 .sender
2859 .send(Recipients::One(peer2_pk.clone()), leader_shard_bytes, true);
2860 context.sleep(config.link.latency * 2).await;
2861
2862 let peer1_shard_bytes = peer1_shard.encode();
2864 peers[1].sender.send(
2865 Recipients::One(peer2_pk.clone()),
2866 peer1_shard_bytes.clone(),
2867 true,
2868 );
2869 context.sleep(config.link.latency * 2).await;
2870
2871 peers[1]
2874 .sender
2875 .send(Recipients::One(peer2_pk), peer1_shard_bytes, true);
2876 context.sleep(config.link.latency * 2).await;
2877
2878 let blocked_peers = oracle.blocked().await.unwrap();
2880 let is_blocked = blocked_peers
2881 .iter()
2882 .any(|(a, b)| a == &peers[2].public_key && b == &peers[1].public_key);
2883 assert!(
2884 !is_blocked,
2885 "peer should not be blocked for exact duplicate shard"
2886 );
2887 },
2888 );
2889 }
2890
2891 #[test_traced]
2892 fn test_equivocating_shard_blocks_peer() {
2893 let fixture: Fixture<C> = Fixture {
2895 num_peers: 10,
2896 ..Default::default()
2897 };
2898
2899 fixture.start(
2900 |config, context, oracle, mut peers, _, coding_config| async move {
2901 let inner1 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
2902 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
2903
2904 let inner2 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 200);
2906 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
2907
2908 let peer1_index = peers[1].index.get() as u16;
2910 let peer1_shard = coded_block1.shard(peer1_index).expect("missing shard");
2911
2912 let mut peer1_equivocating_shard =
2914 coded_block2.shard(peer1_index).expect("missing shard");
2915 peer1_equivocating_shard.commitment = coded_block1.commitment();
2918
2919 let peer2_pk = peers[2].public_key.clone();
2920 let leader = peers[0].public_key.clone();
2921
2922 peers[2].mailbox.discovered(
2924 coded_block1.commitment(),
2925 leader,
2926 Round::new(Epoch::zero(), View::new(1)),
2927 );
2928
2929 let peer2_index = peers[2].index.get() as u16;
2931 let leader_shard = coded_block1.shard(peer2_index).expect("missing shard");
2932 let leader_shard_bytes = leader_shard.encode();
2933 peers[0]
2934 .sender
2935 .send(Recipients::One(peer2_pk.clone()), leader_shard_bytes, true);
2936 context.sleep(config.link.latency * 2).await;
2937
2938 let shard_bytes = peer1_shard.encode();
2940 peers[1]
2941 .sender
2942 .send(Recipients::One(peer2_pk.clone()), shard_bytes, true);
2943 context.sleep(config.link.latency * 2).await;
2944
2945 let equivocating_bytes = peer1_equivocating_shard.encode();
2947 peers[1]
2948 .sender
2949 .send(Recipients::One(peer2_pk), equivocating_bytes, true);
2950 context.sleep(config.link.latency * 2).await;
2951
2952 assert_blocked(&oracle, &peers[2].public_key, &peers[1].public_key).await;
2954 },
2955 );
2956 }
2957
2958 #[test_traced]
2959 fn test_reconstruction_states_pruned_at_or_below_reconstructed_view() {
2960 let fixture: Fixture<C> = Fixture {
2962 num_peers: 10,
2963 ..Default::default()
2964 };
2965
2966 fixture.start(
2967 |config, context, oracle, mut peers, _, coding_config| async move {
2968 let block_a = CodedBlock::<B, C, H>::new(
2970 B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100),
2971 coding_config,
2972 &STRATEGY,
2973 );
2974 let commitment_a = block_a.commitment();
2975
2976 let block_b = CodedBlock::<B, C, H>::new(
2978 B::new::<H>((), Sha256Digest::EMPTY, Height::new(2), 200),
2979 coding_config,
2980 &STRATEGY,
2981 );
2982 let commitment_b = block_b.commitment();
2983
2984 let peer2_pk = peers[2].public_key.clone();
2985 let leader = peers[0].public_key.clone();
2986
2987 peers[2].mailbox.discovered(
2989 commitment_a,
2990 leader.clone(),
2991 Round::new(Epoch::zero(), View::new(1)),
2992 );
2993 let shard_a = block_a
2994 .shard(peers[1].index.get() as u16)
2995 .expect("missing shard")
2996 .encode();
2997 peers[1]
2998 .sender
2999 .send(Recipients::One(peer2_pk.clone()), shard_a.clone(), true);
3000 context.sleep(config.link.latency * 2).await;
3001
3002 peers[2].mailbox.discovered(
3004 commitment_b,
3005 leader,
3006 Round::new(Epoch::zero(), View::new(2)),
3007 );
3008 let leader_shard_b = block_b
3010 .shard(peers[2].index.get() as u16)
3011 .expect("missing shard")
3012 .encode();
3013 peers[0]
3014 .sender
3015 .send(Recipients::One(peer2_pk.clone()), leader_shard_b, true);
3016
3017 for i in [1usize, 3usize, 4usize] {
3019 let shard = block_b
3020 .shard(peers[i].index.get() as u16)
3021 .expect("missing shard")
3022 .encode();
3023 peers[i]
3024 .sender
3025 .send(Recipients::One(peer2_pk.clone()), shard, true);
3026 }
3027 context.sleep(config.link.latency * 4).await;
3028
3029 let reconstructed = peers[2]
3031 .mailbox
3032 .get(commitment_b)
3033 .await
3034 .expect("block B should reconstruct");
3035 assert_eq!(reconstructed.commitment(), commitment_b);
3036
3037 peers[1]
3040 .sender
3041 .send(Recipients::One(peer2_pk), shard_a, true);
3042 context.sleep(config.link.latency * 2).await;
3043
3044 let blocked = oracle.blocked().await.unwrap();
3045 let blocked_peer1 = blocked
3046 .iter()
3047 .any(|(a, b)| a == &peers[2].public_key && b == &peers[1].public_key);
3048 assert!(
3049 !blocked_peer1,
3050 "peer1 should not be blocked after lower-view state was pruned"
3051 );
3052 },
3053 );
3054 }
3055
3056 #[test_traced]
3057 fn test_local_proposal_prune_clears_older_reconstruction_state() {
3058 let fixture: Fixture<C> = Fixture {
3059 num_peers: 10,
3060 ..Default::default()
3061 };
3062
3063 fixture.start(
3064 |config, context, oracle, mut peers, _, coding_config| async move {
3065 let block_a = CodedBlock::<B, C, H>::new(
3066 B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100),
3067 coding_config,
3068 &STRATEGY,
3069 );
3070 let commitment_a = block_a.commitment();
3071
3072 let block_b = CodedBlock::<B, C, H>::new(
3073 B::new::<H>((), Sha256Digest::EMPTY, Height::new(2), 200),
3074 coding_config,
3075 &STRATEGY,
3076 );
3077 let commitment_b = block_b.commitment();
3078
3079 let peer2_pk = peers[2].public_key.clone();
3080 let leader = peers[0].public_key.clone();
3081 let round_a = Round::new(Epoch::zero(), View::new(1));
3082 let round_b = Round::new(Epoch::zero(), View::new(2));
3083
3084 peers[2].mailbox.discovered(commitment_a, leader, round_a);
3085 let block_sub = peers[2].mailbox.subscribe(commitment_a);
3086
3087 let peer1_index = peers[1].index.get() as u16;
3088 let shard_a = block_a.shard(peer1_index).expect("missing shard");
3089
3090 let block_a_equivocating = CodedBlock::<B, C, H>::new(
3091 B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 300),
3092 coding_config,
3093 &STRATEGY,
3094 );
3095 let mut equivocating_shard = block_a_equivocating
3096 .shard(peer1_index)
3097 .expect("missing shard");
3098 equivocating_shard.commitment = commitment_a;
3099
3100 peers[1]
3101 .sender
3102 .send(Recipients::One(peer2_pk.clone()), shard_a.encode(), true);
3103 context.sleep(config.link.latency * 2).await;
3104
3105 peers[2].mailbox.proposed(round_b, block_b);
3106 assert!(
3107 peers[2].mailbox.get(commitment_b).await.is_some(),
3108 "local proposal should be cached before pruning"
3109 );
3110 peers[2].mailbox.prune(commitment_b);
3111
3112 select! {
3113 result = block_sub => {
3114 assert!(
3115 result.is_err(),
3116 "older block subscription should close after local-proposal prune"
3117 );
3118 },
3119 _ = context.sleep(Duration::from_secs(5)) => {
3120 panic!("older block subscription remained open after local-proposal prune");
3121 },
3122 }
3123
3124 peers[1]
3125 .sender
3126 .send(Recipients::One(peer2_pk), equivocating_shard.encode(), true);
3127 context.sleep(config.link.latency * 2).await;
3128
3129 let blocked = oracle.blocked().await.unwrap();
3130 let blocked_peer1 = blocked
3131 .iter()
3132 .any(|(a, b)| a == &peers[2].public_key && b == &peers[1].public_key);
3133 assert!(
3134 !blocked_peer1,
3135 "peer1 should not be blocked after older state was pruned"
3136 );
3137 },
3138 );
3139 }
3140
3141 #[test_traced]
3142 fn test_pending_shards_batch_validated_at_quorum() {
3143 let fixture: Fixture<C> = Fixture {
3152 num_peers: 10,
3153 ..Default::default()
3154 };
3155
3156 fixture.start(
3157 |config, context, oracle, mut peers, _, coding_config| async move {
3158 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
3159 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
3160 let commitment = coded_block.commitment();
3161
3162 let peer3_pk = peers[3].public_key.clone();
3163 let leader = peers[0].public_key.clone();
3164
3165 peers[3].mailbox.discovered(
3167 commitment,
3168 leader,
3169 Round::new(Epoch::zero(), View::new(1)),
3170 );
3171
3172 for &sender_idx in &[1, 2, 4] {
3175 let shard = coded_block
3176 .shard(peers[sender_idx].index.get() as u16)
3177 .expect("missing shard");
3178 let shard_bytes = shard.encode();
3179 peers[sender_idx].sender.send(
3180 Recipients::One(peer3_pk.clone()),
3181 shard_bytes,
3182 true,
3183 );
3184 }
3185
3186 context.sleep(config.link.latency * 2).await;
3187
3188 let block = peers[3].mailbox.get(commitment).await;
3190 assert!(block.is_none(), "block should not be reconstructed yet");
3191
3192 let peer3_index = peers[3].index.get() as u16;
3196 let leader_shard = coded_block.shard(peer3_index).expect("missing shard");
3197 let leader_shard_bytes = leader_shard.encode();
3198 peers[0]
3199 .sender
3200 .send(Recipients::One(peer3_pk), leader_shard_bytes, true);
3201
3202 context.sleep(config.link.latency * 2).await;
3203
3204 let blocked = oracle.blocked().await.unwrap();
3206 assert!(
3207 blocked.is_empty(),
3208 "no peers should be blocked for valid pending shards"
3209 );
3210
3211 let block = peers[3].mailbox.get(commitment).await;
3213 assert!(
3214 block.is_some(),
3215 "block should be reconstructed after batch validation"
3216 );
3217
3218 let reconstructed = block.unwrap();
3220 assert_eq!(
3221 reconstructed.commitment(),
3222 commitment,
3223 "reconstructed block should have correct commitment"
3224 );
3225 },
3226 );
3227 }
3228
3229 #[test_traced]
3230 fn test_peer_shards_buffered_until_external_proposed() {
3231 let fixture: Fixture<C> = Fixture {
3234 num_peers: 10,
3235 ..Default::default()
3236 };
3237
3238 fixture.start(
3239 |config, context, oracle, mut peers, _, coding_config| async move {
3240 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
3241 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
3242 let commitment = coded_block.commitment();
3243
3244 let receiver_idx = 3usize;
3245 let receiver_pk = peers[receiver_idx].public_key.clone();
3246 let leader = peers[0].public_key.clone();
3247
3248 let mut shard_sub = peers[receiver_idx]
3250 .mailbox
3251 .subscribe_assigned_shard_verified(commitment);
3252
3253 let leader_shard = coded_block
3256 .shard(peers[receiver_idx].index.get() as u16)
3257 .expect("missing shard")
3258 .encode();
3259 peers[0]
3260 .sender
3261 .send(Recipients::One(receiver_pk.clone()), leader_shard, true);
3262
3263 for i in [1usize, 2usize, 4usize] {
3264 let shard = coded_block
3265 .shard(peers[i].index.get() as u16)
3266 .expect("missing shard")
3267 .encode();
3268 peers[i]
3269 .sender
3270 .send(Recipients::One(receiver_pk.clone()), shard, true);
3271 }
3272
3273 context.sleep(config.link.latency * 2).await;
3274
3275 assert!(
3277 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
3278 "shard subscription should not resolve before leader announcement"
3279 );
3280 assert!(
3281 peers[receiver_idx].mailbox.get(commitment).await.is_none(),
3282 "block should not reconstruct before leader announcement"
3283 );
3284
3285 peers[receiver_idx].mailbox.discovered(
3287 commitment,
3288 leader,
3289 Round::new(Epoch::zero(), View::new(1)),
3290 );
3291
3292 select! {
3293 _ = shard_sub => {},
3294 _ = context.sleep(Duration::from_secs(5)) => {
3295 panic!("shard subscription did not resolve after leader announcement");
3296 },
3297 }
3298
3299 context.sleep(config.link.latency * 2).await;
3300 assert!(
3301 peers[receiver_idx].mailbox.get(commitment).await.is_some(),
3302 "block should reconstruct after buffered shards are ingested"
3303 );
3304
3305 assert!(
3307 oracle.blocked().await.unwrap().is_empty(),
3308 "no peers should be blocked for valid buffered shards"
3309 );
3310 },
3311 );
3312 }
3313
3314 #[test_traced]
3315 fn test_notarized_commitment_reconstructs_from_buffered_peer_shards_without_leader() {
3316 let fixture: Fixture<C> = Fixture {
3317 num_peers: 10,
3318 ..Default::default()
3319 };
3320
3321 fixture.start(
3322 |config, context, oracle, mut peers, _, coding_config| async move {
3323 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
3324 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
3325 let commitment = coded_block.commitment();
3326 let round = Round::new(Epoch::zero(), View::new(1));
3327
3328 let receiver_idx = 3usize;
3329 let receiver = peers[receiver_idx].public_key.clone();
3330
3331 let block_sub = peers[receiver_idx].mailbox.subscribe(commitment);
3332
3333 for sender_idx in [1usize, 2, 4, 5] {
3336 let shard = coded_block
3337 .shard(peers[sender_idx].index.get() as u16)
3338 .expect("missing shard")
3339 .encode();
3340 peers[sender_idx].sender.send(
3341 Recipients::One(receiver.clone()),
3342 shard,
3343 true,
3344 );
3345 }
3346 context.sleep(config.link.latency * 2).await;
3347
3348 assert!(
3349 peers[receiver_idx].mailbox.get(commitment).await.is_none(),
3350 "block should not reconstruct before the commitment is notarized"
3351 );
3352
3353 peers[receiver_idx].mailbox.notarized(commitment, round);
3354
3355 select! {
3356 _ = block_sub => {},
3357 _ = context.sleep(Duration::from_secs(5)) => {
3358 panic!("block subscription did not resolve after notarized reconstruction interest");
3359 },
3360 }
3361
3362 let reconstructed = peers[receiver_idx]
3363 .mailbox
3364 .get(commitment)
3365 .await
3366 .expect("block should reconstruct from buffered peer shards");
3367 assert_eq!(reconstructed.commitment(), commitment);
3368
3369 let mut assigned = peers[receiver_idx]
3370 .mailbox
3371 .subscribe_assigned_shard_verified(commitment);
3372 assert!(
3373 matches!(assigned.try_recv(), Err(TryRecvError::Empty)),
3374 "leaderless reconstruction must not satisfy assigned shard readiness"
3375 );
3376
3377 let leader = peers[0].public_key.clone();
3378 peers[receiver_idx]
3379 .mailbox
3380 .discovered(commitment, leader, round);
3381 let leader_shard = coded_block
3382 .shard(peers[receiver_idx].index.get() as u16)
3383 .expect("missing leader shard")
3384 .encode();
3385 peers[0].sender.send(
3386 Recipients::One(receiver),
3387 leader_shard,
3388 true,
3389 );
3390
3391 select! {
3392 _ = assigned => {},
3393 _ = context.sleep(Duration::from_secs(5)) => {
3394 panic!("assigned shard subscription did not resolve after leader discovery");
3395 },
3396 }
3397
3398 assert!(
3399 oracle.blocked().await.unwrap().is_empty(),
3400 "valid sender-indexed shards should not block peers"
3401 );
3402 },
3403 );
3404 }
3405
3406 #[test_traced]
3407 fn test_leader_shard_after_notarized_is_buffered_until_discovered() {
3408 let fixture: Fixture<C> = Fixture {
3409 num_peers: 10,
3410 ..Default::default()
3411 };
3412
3413 fixture.start(
3414 |config, context, oracle, mut peers, _, coding_config| async move {
3415 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
3416 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
3417 let commitment = coded_block.commitment();
3418 let round = Round::new(Epoch::zero(), View::new(1));
3419
3420 let leader_idx = 0usize;
3421 let receiver_idx = 3usize;
3422 let leader = peers[leader_idx].public_key.clone();
3423 let receiver = peers[receiver_idx].public_key.clone();
3424
3425 peers[receiver_idx].mailbox.notarized(commitment, round);
3426 let assigned = peers[receiver_idx]
3427 .mailbox
3428 .subscribe_assigned_shard_verified(commitment);
3429
3430 let leader_shard = coded_block
3431 .shard(peers[receiver_idx].index.get() as u16)
3432 .expect("missing receiver shard")
3433 .encode();
3434 peers[leader_idx]
3435 .sender
3436 .send(Recipients::One(receiver), leader_shard, true);
3437
3438 context.sleep(config.link.latency * 2).await;
3439 peers[receiver_idx]
3440 .mailbox
3441 .discovered(commitment, leader, round);
3442
3443 assigned
3444 .await
3445 .expect("assigned shard should resolve after leader discovery");
3446 assert!(
3447 oracle.blocked().await.unwrap().is_empty(),
3448 "valid leader shard should not block peers"
3449 );
3450 },
3451 );
3452 }
3453
3454 #[test_traced]
3455 fn test_post_leader_shards_processed_immediately() {
3456 let fixture: Fixture<C> = Fixture {
3459 num_peers: 10,
3460 ..Default::default()
3461 };
3462
3463 fixture.start(
3464 |config, context, oracle, mut peers, _, coding_config| async move {
3465 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
3466 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
3467 let commitment = coded_block.commitment();
3468
3469 let receiver_idx = 3usize;
3470 let receiver_pk = peers[receiver_idx].public_key.clone();
3471 let leader = peers[0].public_key.clone();
3472
3473 let shard_sub = peers[receiver_idx]
3474 .mailbox
3475 .subscribe_assigned_shard_verified(commitment);
3476 peers[receiver_idx].mailbox.discovered(
3477 commitment,
3478 leader.clone(),
3479 Round::new(Epoch::zero(), View::new(1)),
3480 );
3481
3482 let leader_shard = coded_block
3484 .shard(peers[receiver_idx].index.get() as u16)
3485 .expect("missing shard")
3486 .encode();
3487 peers[0]
3488 .sender
3489 .send(Recipients::One(receiver_pk.clone()), leader_shard, true);
3490
3491 select! {
3493 _ = shard_sub => {},
3494 _ = context.sleep(Duration::from_secs(5)) => {
3495 panic!("shard subscription did not resolve after post-leader shard");
3496 },
3497 }
3498
3499 for i in [1usize, 2usize, 4usize] {
3501 let shard = coded_block
3502 .shard(peers[i].index.get() as u16)
3503 .expect("missing shard")
3504 .encode();
3505 peers[i]
3506 .sender
3507 .send(Recipients::One(receiver_pk.clone()), shard, true);
3508 }
3509
3510 context.sleep(config.link.latency * 2).await;
3511 let reconstructed = peers[receiver_idx]
3512 .mailbox
3513 .get(commitment)
3514 .await
3515 .expect("block should reconstruct from post-leader shards");
3516 assert_eq!(reconstructed.commitment(), commitment);
3517
3518 assert!(
3519 oracle.blocked().await.unwrap().is_empty(),
3520 "no peers should be blocked for valid post-leader shards"
3521 );
3522 },
3523 );
3524 }
3525
3526 #[test_traced]
3527 fn test_invalid_shard_codec_blocks_peer() {
3528 let fixture: Fixture<C> = Fixture {
3530 num_peers: 4,
3531 ..Default::default()
3532 };
3533
3534 fixture.start(
3535 |config, context, oracle, mut peers, _, _coding_config| async move {
3536 let peer0_pk = peers[0].public_key.clone();
3537 let peer1_pk = peers[1].public_key.clone();
3538
3539 let garbage = Bytes::from(vec![0xFF, 0xFE, 0xFD, 0xFC, 0xFB]);
3541 peers[1]
3542 .sender
3543 .send(Recipients::One(peer0_pk.clone()), garbage, true);
3544
3545 context.sleep(config.link.latency * 2).await;
3546
3547 assert_blocked(&oracle, &peer0_pk, &peer1_pk).await;
3549 },
3550 );
3551 }
3552
3553 #[test_traced]
3554 fn test_duplicate_buffered_shard_does_not_block_before_leader() {
3555 let fixture: Fixture<C> = Fixture {
3558 ..Default::default()
3559 };
3560
3561 fixture.start(
3562 |config, context, oracle, mut peers, _, coding_config| async move {
3563 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
3564 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
3565
3566 let peer2_index = peers[2].index.get() as u16;
3568 let peer2_shard = coded_block.shard(peer2_index).expect("missing shard");
3569 let shard_bytes = peer2_shard.encode();
3570
3571 let peer2_pk = peers[2].public_key.clone();
3572
3573 peers[1]
3577 .sender
3578 .send(Recipients::One(peer2_pk.clone()), shard_bytes.clone(), true);
3579 context.sleep(config.link.latency * 2).await;
3580
3581 let blocked = oracle.blocked().await.unwrap();
3583 assert!(blocked.is_empty(), "no peers should be blocked yet");
3584
3585 peers[1]
3587 .sender
3588 .send(Recipients::One(peer2_pk), shard_bytes, true);
3589 context.sleep(config.link.latency * 2).await;
3590
3591 let blocked = oracle.blocked().await.unwrap();
3593 assert!(
3594 blocked.is_empty(),
3595 "no peers should be blocked before leader"
3596 );
3597 },
3598 );
3599 }
3600
3601 #[test_traced]
3602 fn test_invalid_leader_shard_crypto_blocks_leader() {
3603 let fixture: Fixture<C> = Fixture {
3606 ..Default::default()
3607 };
3608
3609 fixture.start(
3610 |config, context, oracle, mut peers, _, coding_config| async move {
3611 let inner1 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
3614 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
3615 let commitment1 = coded_block1.commitment();
3616
3617 let inner2 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(2), 200);
3618 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
3619
3620 let peer2_index = peers[2].index.get() as u16;
3623 let mut wrong_shard = coded_block2.shard(peer2_index).expect("missing shard");
3624 wrong_shard.commitment = commitment1;
3625 let wrong_bytes = wrong_shard.encode();
3626
3627 let peer2_pk = peers[2].public_key.clone();
3628 let leader = peers[0].public_key.clone();
3629
3630 peers[2].mailbox.discovered(
3632 commitment1,
3633 leader,
3634 Round::new(Epoch::zero(), View::new(1)),
3635 );
3636
3637 peers[0]
3639 .sender
3640 .send(Recipients::One(peer2_pk), wrong_bytes, true);
3641 context.sleep(config.link.latency * 2).await;
3642
3643 assert_blocked(&oracle, &peers[2].public_key, &peers[0].public_key).await;
3645 },
3646 );
3647 }
3648
3649 #[test_traced]
3650 fn test_shard_index_mismatch_blocks_peer() {
3651 let fixture: Fixture<C> = Fixture {
3654 num_peers: 10,
3655 ..Default::default()
3656 };
3657
3658 fixture.start(
3659 |config, context, oracle, mut peers, _, coding_config| async move {
3660 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
3661 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
3662 let commitment = coded_block.commitment();
3663
3664 let peer3_index = peers[3].index.get() as u16;
3666 let leader_shard = coded_block.shard(peer3_index).expect("missing shard");
3667
3668 let peer1_index = peers[1].index.get() as u16;
3670 let mut wrong_index_shard = coded_block.shard(peer1_index).expect("missing shard");
3671 wrong_index_shard.index = peers[4].index.get() as u16;
3673 let wrong_bytes = wrong_index_shard.encode();
3674
3675 let peer3_pk = peers[3].public_key.clone();
3676 let leader = peers[0].public_key.clone();
3677
3678 peers[3].mailbox.discovered(
3680 commitment,
3681 leader,
3682 Round::new(Epoch::zero(), View::new(1)),
3683 );
3684 let shard_bytes = leader_shard.encode();
3685 peers[0]
3686 .sender
3687 .send(Recipients::One(peer3_pk.clone()), shard_bytes, true);
3688 context.sleep(config.link.latency * 2).await;
3689
3690 peers[1]
3692 .sender
3693 .send(Recipients::One(peer3_pk), wrong_bytes, true);
3694 context.sleep(config.link.latency * 2).await;
3695
3696 assert_blocked(&oracle, &peers[3].public_key, &peers[1].public_key).await;
3698 },
3699 );
3700 }
3701
3702 #[test_traced]
3703 fn test_invalid_shard_crypto_blocks_peer() {
3704 let fixture: Fixture<C> = Fixture {
3707 num_peers: 10,
3708 ..Default::default()
3709 };
3710
3711 fixture.start(
3712 |config, context, oracle, mut peers, _, coding_config| async move {
3713 let inner1 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
3715 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
3716 let commitment1 = coded_block1.commitment();
3717
3718 let inner2 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(2), 200);
3719 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
3720
3721 let peer3_index = peers[3].index.get() as u16;
3723 let leader_shard = coded_block1.shard(peer3_index).expect("missing shard");
3724
3725 let peer1_index = peers[1].index.get() as u16;
3728 let mut wrong_shard = coded_block2.shard(peer1_index).expect("missing shard");
3729 wrong_shard.commitment = commitment1;
3730 let wrong_bytes = wrong_shard.encode();
3731
3732 let peer3_pk = peers[3].public_key.clone();
3733 let leader = peers[0].public_key.clone();
3734
3735 peers[3].mailbox.discovered(
3737 commitment1,
3738 leader,
3739 Round::new(Epoch::zero(), View::new(1)),
3740 );
3741 let shard_bytes = leader_shard.encode();
3742 peers[0]
3743 .sender
3744 .send(Recipients::One(peer3_pk.clone()), shard_bytes, true);
3745 context.sleep(config.link.latency * 2).await;
3746
3747 peers[1]
3749 .sender
3750 .send(Recipients::One(peer3_pk.clone()), wrong_bytes, true);
3751 context.sleep(config.link.latency * 2).await;
3752
3753 for &idx in &[2, 4] {
3757 let peer_index = peers[idx].index.get() as u16;
3758 let shard = coded_block1.shard(peer_index).expect("missing shard");
3759 let bytes = shard.encode();
3760 peers[idx]
3761 .sender
3762 .send(Recipients::One(peer3_pk.clone()), bytes, true);
3763 }
3764 context.sleep(config.link.latency * 2).await;
3765
3766 assert_blocked(&oracle, &peers[3].public_key, &peers[1].public_key).await;
3768 },
3769 );
3770 }
3771
3772 #[test_traced]
3773 fn test_reconstruction_recovers_after_quorum_with_one_invalid_shard() {
3774 let fixture: Fixture<C> = Fixture {
3779 num_peers: 10,
3780 ..Default::default()
3781 };
3782
3783 fixture.start(
3784 |config, context, oracle, mut peers, _, coding_config| async move {
3785 let inner1 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
3786 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
3787 let commitment1 = coded_block1.commitment();
3788
3789 let inner2 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(2), 200);
3790 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
3791
3792 let receiver_idx = 3usize;
3793 let receiver_pk = peers[receiver_idx].public_key.clone();
3794
3795 let peer1_index = peers[1].index.get() as u16;
3797 let mut invalid_shard = coded_block2.shard(peer1_index).expect("missing shard");
3798 invalid_shard.commitment = commitment1;
3799
3800 let leader = peers[0].public_key.clone();
3802 peers[receiver_idx].mailbox.discovered(
3803 commitment1,
3804 leader,
3805 Round::new(Epoch::zero(), View::new(1)),
3806 );
3807 let leader_shard = coded_block1
3808 .shard(peers[receiver_idx].index.get() as u16)
3809 .expect("missing shard")
3810 .encode();
3811 peers[0]
3812 .sender
3813 .send(Recipients::One(receiver_pk.clone()), leader_shard, true);
3814
3815 peers[1].sender.send(
3820 Recipients::One(receiver_pk.clone()),
3821 invalid_shard.encode(),
3822 true,
3823 );
3824 for idx in [2usize, 4usize] {
3825 let shard = coded_block1
3826 .shard(peers[idx].index.get() as u16)
3827 .expect("missing shard")
3828 .encode();
3829 peers[idx]
3830 .sender
3831 .send(Recipients::One(receiver_pk.clone()), shard, true);
3832 }
3833
3834 context.sleep(config.link.latency * 2).await;
3835
3836 assert_blocked(
3838 &oracle,
3839 &peers[receiver_idx].public_key,
3840 &peers[1].public_key,
3841 )
3842 .await;
3843 assert!(
3844 peers[receiver_idx].mailbox.get(commitment1).await.is_none(),
3845 "block should not reconstruct with only 3 checked shards"
3846 );
3847
3848 let extra_shard = coded_block1
3850 .shard(peers[5].index.get() as u16)
3851 .expect("missing shard")
3852 .encode();
3853 peers[5]
3854 .sender
3855 .send(Recipients::One(receiver_pk), extra_shard, true);
3856
3857 context.sleep(config.link.latency * 2).await;
3858
3859 let reconstructed = peers[receiver_idx]
3860 .mailbox
3861 .get(commitment1)
3862 .await
3863 .expect("block should reconstruct after additional valid shard");
3864 assert_eq!(reconstructed.commitment(), commitment1);
3865 },
3866 );
3867 }
3868
3869 #[test_traced]
3870 fn test_invalid_pending_shard_blocked_on_drain() {
3871 let fixture: Fixture<C> = Fixture {
3874 num_peers: 10,
3875 ..Default::default()
3876 };
3877
3878 fixture.start(
3879 |config, context, oracle, mut peers, _, coding_config| async move {
3880 let inner1 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
3882 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
3883 let commitment1 = coded_block1.commitment();
3884
3885 let inner2 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(2), 200);
3886 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
3887
3888 let peer1_index = peers[1].index.get() as u16;
3890 let mut wrong_shard = coded_block2.shard(peer1_index).expect("missing shard");
3891 wrong_shard.commitment = commitment1;
3892 let wrong_bytes = wrong_shard.encode();
3893
3894 let peer3_pk = peers[3].public_key.clone();
3895
3896 peers[1]
3899 .sender
3900 .send(Recipients::One(peer3_pk.clone()), wrong_bytes, true);
3901 context.sleep(config.link.latency * 2).await;
3902
3903 let blocked = oracle.blocked().await.unwrap();
3905 assert!(blocked.is_empty(), "no peers should be blocked yet");
3906
3907 for &idx in &[2, 4] {
3911 let peer_index = peers[idx].index.get() as u16;
3912 let shard = coded_block1.shard(peer_index).expect("missing shard");
3913 let bytes = shard.encode();
3914 peers[idx]
3915 .sender
3916 .send(Recipients::One(peer3_pk.clone()), bytes, true);
3917 }
3918 context.sleep(config.link.latency * 2).await;
3919
3920 let blocked = oracle.blocked().await.unwrap();
3922 assert!(blocked.is_empty(), "no peers should be blocked yet");
3923
3924 let leader = peers[0].public_key.clone();
3926 peers[3].mailbox.discovered(
3927 commitment1,
3928 leader,
3929 Round::new(Epoch::zero(), View::new(1)),
3930 );
3931 let peer3_index = peers[3].index.get() as u16;
3932 let leader_shard = coded_block1.shard(peer3_index).expect("missing shard");
3933 let shard_bytes = leader_shard.encode();
3934 peers[0]
3935 .sender
3936 .send(Recipients::One(peer3_pk), shard_bytes, true);
3937 context.sleep(config.link.latency * 2).await;
3938
3939 assert_blocked(&oracle, &peers[3].public_key, &peers[1].public_key).await;
3942 },
3943 );
3944 }
3945
3946 #[test_traced]
3947 fn test_cross_epoch_buffered_shard_not_blocked() {
3948 let executor = deterministic::Runner::default();
3949 executor.start(|context| async move {
3950 let (network, oracle) = simulated::Network::<deterministic::Context, P>::new(
3951 context.child("network"),
3952 simulated::Config {
3953 max_size: MAX_SHARD_SIZE as u32,
3954 disconnect_on_block: true,
3955 tracked_peer_sets: NZUsize!(1),
3956 },
3957 );
3958 network.start();
3959
3960 let mut epoch0_keys: Vec<PrivateKey> = (0..4).map(PrivateKey::from_seed).collect();
3963 epoch0_keys.sort_by_key(|s| s.public_key());
3964 let epoch0_pks: Vec<P> = epoch0_keys.iter().map(|c| c.public_key()).collect();
3965 let epoch0_set: Set<P> = Set::from_iter_dedup(epoch0_pks.clone());
3966
3967 let future_peer_key = PrivateKey::from_seed(4);
3968 let future_peer_pk = future_peer_key.public_key();
3969 let mut epoch1_pks: Vec<P> = epoch0_pks[..3]
3970 .iter()
3971 .cloned()
3972 .chain(std::iter::once(future_peer_pk.clone()))
3973 .collect();
3974 epoch1_pks.sort();
3975 let epoch1_set: Set<P> = Set::from_iter_dedup(epoch1_pks);
3976
3977 let receiver_idx_in_epoch0 = epoch0_set
3978 .index(&epoch0_pks[0])
3979 .expect("receiver must be in epoch 0")
3980 .get() as usize;
3981 let receiver_key = epoch0_keys[receiver_idx_in_epoch0].clone();
3982 let receiver_pk = receiver_key.public_key();
3983
3984 let receiver_control = oracle.control(receiver_pk.clone());
3985 let (sender_handle, receiver_handle) = receiver_control
3986 .register(0, TEST_QUOTA)
3987 .await
3988 .expect("registration should succeed");
3989
3990 let future_peer_control = oracle.control(future_peer_pk.clone());
3991 let (mut future_peer_sender, _future_peer_receiver) = future_peer_control
3992 .register(0, TEST_QUOTA)
3993 .await
3994 .expect("registration should succeed");
3995 oracle
3996 .add_link(future_peer_pk.clone(), receiver_pk.clone(), DEFAULT_LINK)
3997 .await
3998 .expect("link should be added");
3999 oracle.manager().track(
4000 0,
4001 Set::from_iter_dedup([receiver_pk.clone(), future_peer_pk.clone()]),
4002 );
4003 context.sleep(Duration::from_millis(10)).await;
4004
4005 let scheme_epoch0 =
4007 Scheme::signer(SCHEME_NAMESPACE, epoch0_set.clone(), receiver_key.clone())
4008 .expect("signer scheme should be created");
4009 let scheme_epoch1 =
4010 Scheme::signer(SCHEME_NAMESPACE, epoch1_set.clone(), receiver_key.clone())
4011 .expect("signer scheme should be created");
4012 let scheme_provider =
4013 MultiEpochProvider::single(scheme_epoch0).with_epoch(Epoch::new(1), scheme_epoch1);
4014
4015 let config: Config<_, _, _, _, C, _, _, _> = Config {
4016 scheme_provider,
4017 blocker: receiver_control.clone(),
4018 shard_codec_cfg: CodecConfig {
4019 maximum_shard_size: MAX_SHARD_SIZE,
4020 },
4021 block_codec_cfg: (),
4022 strategy: STRATEGY,
4023 mailbox_size: NZUsize!(1024),
4024 peer_buffer_size: NZUsize!(64),
4025 background_channel_capacity: NZUsize!(1024),
4026 peer_provider: oracle.manager(),
4027 };
4028
4029 let (engine, mailbox) = ShardEngine::new(context.child("receiver"), config);
4030 engine.start((sender_handle, receiver_handle));
4031
4032 let coding_config = coding_config_for_participants(epoch1_set.len() as u16);
4034 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
4035 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4036 let commitment = coded_block.commitment();
4037
4038 let future_peer_index = epoch1_set
4040 .index(&future_peer_pk)
4041 .expect("future peer must be in epoch 1");
4042 let future_shard = coded_block
4043 .shard(future_peer_index.get() as u16)
4044 .expect("missing shard");
4045 let shard_bytes = future_shard.encode();
4046
4047 future_peer_sender.send(Recipients::One(receiver_pk.clone()), shard_bytes, true);
4049 context.sleep(DEFAULT_LINK.latency * 2).await;
4050
4051 let blocked = oracle.blocked().await.unwrap();
4053 assert!(
4054 blocked.is_empty(),
4055 "no peers should be blocked while shard is buffered"
4056 );
4057
4058 let leader = epoch0_pks[1].clone();
4060 mailbox.discovered(commitment, leader, Round::new(Epoch::new(1), View::new(1)));
4061 context.sleep(DEFAULT_LINK.latency * 2).await;
4062
4063 let blocked = oracle.blocked().await.unwrap();
4066 assert!(
4067 blocked.is_empty(),
4068 "future-epoch participant should not be blocked: {blocked:?}"
4069 );
4070 });
4071 }
4072
4073 #[test_traced]
4074 fn test_shard_broadcast_survives_provider_churn() {
4075 let executor = deterministic::Runner::default();
4076 executor.start(|context| async move {
4077 let (network, oracle) = simulated::Network::<deterministic::Context, P>::new(
4078 context.child("network"),
4079 simulated::Config {
4080 max_size: MAX_SHARD_SIZE as u32,
4081 disconnect_on_block: true,
4082 tracked_peer_sets: NZUsize!(1),
4083 },
4084 );
4085 network.start();
4086
4087 let mut private_keys: Vec<PrivateKey> = (0..4).map(PrivateKey::from_seed).collect();
4088 private_keys.sort_by_key(|s| s.public_key());
4089 let peer_keys: Vec<P> = private_keys.iter().map(|k| k.public_key()).collect();
4090 let participants: Set<P> = Set::from_iter_dedup(peer_keys.clone());
4091
4092 let leader_idx = 0usize;
4093 let broadcaster_idx = 1usize;
4094 let receiver_idx = 2usize;
4095
4096 let leader_pk = peer_keys[leader_idx].clone();
4097 let broadcaster_pk = peer_keys[broadcaster_idx].clone();
4098 let receiver_pk = peer_keys[receiver_idx].clone();
4099
4100 let mut registrations = BTreeMap::new();
4101 for key in &peer_keys {
4102 let control = oracle.control(key.clone());
4103 let (sender, receiver) = control
4104 .register(0, TEST_QUOTA)
4105 .await
4106 .expect("registration should succeed");
4107 registrations.insert(key.clone(), (control, sender, receiver));
4108 }
4109
4110 for src in &peer_keys {
4111 for dst in &peer_keys {
4112 if src == dst {
4113 continue;
4114 }
4115 oracle
4116 .add_link(src.clone(), dst.clone(), DEFAULT_LINK)
4117 .await
4118 .expect("link should be added");
4119 }
4120 }
4121 oracle.manager().track(0, participants.clone());
4122 context.sleep(Duration::from_millis(10)).await;
4123
4124 let (_leader_control, mut leader_sender, _leader_receiver) = registrations
4125 .remove(&leader_pk)
4126 .expect("leader should be registered");
4127 let (broadcaster_control, broadcaster_sender, broadcaster_receiver) = registrations
4128 .remove(&broadcaster_pk)
4129 .expect("broadcaster should be registered");
4130 let (receiver_control, receiver_sender, receiver_receiver) = registrations
4131 .remove(&receiver_pk)
4132 .expect("receiver should be registered");
4133
4134 let broadcaster_scheme = Scheme::signer(
4135 SCHEME_NAMESPACE,
4136 participants.clone(),
4137 private_keys[broadcaster_idx].clone(),
4138 )
4139 .expect("signer scheme should be created");
4140 let broadcaster_provider = ChurningProvider::new(broadcaster_scheme, 3);
4144 let broadcaster_config: Config<_, _, _, _, C, _, _, _> = Config {
4145 scheme_provider: broadcaster_provider,
4146 blocker: broadcaster_control.clone(),
4147 shard_codec_cfg: CodecConfig {
4148 maximum_shard_size: MAX_SHARD_SIZE,
4149 },
4150 block_codec_cfg: (),
4151 strategy: STRATEGY,
4152 mailbox_size: NZUsize!(1024),
4153 peer_buffer_size: NZUsize!(64),
4154 background_channel_capacity: NZUsize!(1024),
4155 peer_provider: oracle.manager(),
4156 };
4157 let (broadcaster_engine, broadcaster_mailbox) =
4158 ChurningShardEngine::new(context.child("broadcaster"), broadcaster_config);
4159 broadcaster_engine.start((broadcaster_sender, broadcaster_receiver));
4160
4161 let receiver_scheme = Scheme::signer(
4162 SCHEME_NAMESPACE,
4163 participants.clone(),
4164 private_keys[receiver_idx].clone(),
4165 )
4166 .expect("signer scheme should be created");
4167 let receiver_config: Config<_, _, _, _, C, _, _, _> = Config {
4168 scheme_provider: MultiEpochProvider::single(receiver_scheme),
4169 blocker: receiver_control.clone(),
4170 shard_codec_cfg: CodecConfig {
4171 maximum_shard_size: MAX_SHARD_SIZE,
4172 },
4173 block_codec_cfg: (),
4174 strategy: STRATEGY,
4175 mailbox_size: NZUsize!(1024),
4176 peer_buffer_size: NZUsize!(64),
4177 background_channel_capacity: NZUsize!(1024),
4178 peer_provider: oracle.manager(),
4179 };
4180 let (receiver_engine, receiver_mailbox) =
4181 ShardEngine::new(context.child("receiver"), receiver_config);
4182 receiver_engine.start((receiver_sender, receiver_receiver));
4183
4184 let coding_config = coding_config_for_participants(peer_keys.len() as u16);
4185 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
4186 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4187 let commitment = coded_block.commitment();
4188 let round = Round::new(Epoch::zero(), View::new(1));
4189
4190 broadcaster_mailbox.discovered(commitment, leader_pk.clone(), round);
4191 receiver_mailbox.discovered(commitment, leader_pk.clone(), round);
4192 context.sleep(DEFAULT_LINK.latency).await;
4193
4194 let broadcaster_index = participants
4195 .index(&broadcaster_pk)
4196 .expect("broadcaster must be a participant")
4197 .get() as u16;
4198 let broadcaster_shard = coded_block
4199 .shard(broadcaster_index)
4200 .expect("missing shard")
4201 .encode();
4202 leader_sender.send(Recipients::One(broadcaster_pk), broadcaster_shard, true);
4203
4204 let receiver_index = participants
4205 .index(&receiver_pk)
4206 .expect("receiver must be a participant")
4207 .get() as u16;
4208 let receiver_shard = coded_block
4209 .shard(receiver_index)
4210 .expect("missing shard")
4211 .encode();
4212 leader_sender.send(Recipients::One(receiver_pk.clone()), receiver_shard, true);
4213
4214 context.sleep(DEFAULT_LINK.latency * 3).await;
4215
4216 let reconstructed = receiver_mailbox.get(commitment).await;
4217 assert!(
4218 reconstructed.is_some(),
4219 "receiver should reconstruct after broadcaster validates and broadcasts shard"
4220 );
4221 });
4222 }
4223
4224 #[test_traced]
4225 fn test_failed_reconstruction_digest_mismatch_then_recovery() {
4226 let fixture: Fixture<C> = Fixture {
4233 num_peers: 10,
4234 ..Default::default()
4235 };
4236
4237 fixture.start(
4238 |config, context, _oracle, mut peers, _, coding_config| async move {
4239 let inner1 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
4241 let coded_block1 = CodedBlock::<B, C, H>::new(inner1, coding_config, &STRATEGY);
4242
4243 let inner2 = B::new::<H>((), Sha256Digest::EMPTY, Height::new(2), 200);
4245 let coded_block2 = CodedBlock::<B, C, H>::new(inner2, coding_config, &STRATEGY);
4246 let real_commitment2 = coded_block2.commitment();
4247
4248 let fake_commitment = Commitment::from((
4252 coded_block1.digest(),
4253 real_commitment2.root::<Sha256Digest>(),
4254 real_commitment2.context::<Sha256Digest>(),
4255 coding_config,
4256 ));
4257
4258 let receiver_idx = 3usize;
4259 let receiver_pk = peers[receiver_idx].public_key.clone();
4260 let leader = peers[0].public_key.clone();
4261 let round = Round::new(Epoch::zero(), View::new(1));
4262
4263 peers[receiver_idx]
4265 .mailbox
4266 .discovered(fake_commitment, leader.clone(), round);
4267
4268 let mut block_sub = peers[receiver_idx].mailbox.subscribe(fake_commitment);
4270 let mut digest_sub = peers[receiver_idx]
4271 .mailbox
4272 .subscribe_by_digest(coded_block1.digest());
4273
4274 let receiver_shard_idx = peers[receiver_idx].index.get() as u16;
4276 let mut leader_shard = coded_block2
4277 .shard(receiver_shard_idx)
4278 .expect("missing shard");
4279 leader_shard.commitment = fake_commitment;
4280 peers[0].sender.send(
4281 Recipients::One(receiver_pk.clone()),
4282 leader_shard.encode(),
4283 true,
4284 );
4285
4286 for &idx in &[1usize, 2, 4] {
4289 let peer_shard_idx = peers[idx].index.get() as u16;
4290 let mut shard = coded_block2.shard(peer_shard_idx).expect("missing shard");
4291 shard.commitment = fake_commitment;
4292 peers[idx].sender.send(
4293 Recipients::One(receiver_pk.clone()),
4294 shard.encode(),
4295 true,
4296 );
4297 }
4298
4299 context.sleep(config.link.latency * 2).await;
4300
4301 assert!(
4304 peers[receiver_idx]
4305 .mailbox
4306 .get(fake_commitment)
4307 .await
4308 .is_none(),
4309 "block should not be available after DigestMismatch"
4310 );
4311
4312 assert!(
4314 matches!(block_sub.try_recv(), Err(TryRecvError::Closed)),
4315 "subscription should close for failed reconstruction"
4316 );
4317 assert!(
4318 matches!(digest_sub.try_recv(), Err(TryRecvError::Closed)),
4319 "digest subscription should close after failed reconstruction"
4320 );
4321
4322 let real_commitment1 = coded_block1.commitment();
4325 let round2 = Round::new(Epoch::zero(), View::new(2));
4326 peers[receiver_idx]
4327 .mailbox
4328 .discovered(real_commitment1, leader.clone(), round2);
4329
4330 let leader_shard1 = coded_block1
4331 .shard(receiver_shard_idx)
4332 .expect("missing shard");
4333 peers[0].sender.send(
4334 Recipients::One(receiver_pk.clone()),
4335 leader_shard1.encode(),
4336 true,
4337 );
4338
4339 for &idx in &[1usize, 2, 4] {
4340 let peer_shard_idx = peers[idx].index.get() as u16;
4341 let shard = coded_block1.shard(peer_shard_idx).expect("missing shard");
4342 peers[idx].sender.send(
4343 Recipients::One(receiver_pk.clone()),
4344 shard.encode(),
4345 true,
4346 );
4347 }
4348
4349 context.sleep(config.link.latency * 2).await;
4350
4351 let reconstructed = peers[receiver_idx]
4352 .mailbox
4353 .get(real_commitment1)
4354 .await
4355 .expect("valid block should reconstruct after prior failure");
4356 assert_eq!(reconstructed.commitment(), real_commitment1);
4357 },
4358 );
4359 }
4360
4361 #[test_traced]
4362 fn test_failed_reconstruction_context_mismatch_then_recovery() {
4363 let fixture: Fixture<C> = Fixture {
4367 num_peers: 10,
4368 ..Default::default()
4369 };
4370
4371 fixture.start(
4372 |config, context, _oracle, mut peers, _, coding_config| async move {
4373 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
4374 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4375 let real_commitment = coded_block.commitment();
4376
4377 let wrong_context_digest = Sha256::hash(b"wrong_context");
4378 assert_ne!(
4379 real_commitment.context::<Sha256Digest>(),
4380 wrong_context_digest,
4381 "test requires a distinct context digest"
4382 );
4383 let fake_commitment = Commitment::from((
4384 coded_block.digest(),
4385 real_commitment.root::<Sha256Digest>(),
4386 wrong_context_digest,
4387 coding_config,
4388 ));
4389
4390 let receiver_idx = 3usize;
4391 let receiver_pk = peers[receiver_idx].public_key.clone();
4392 let leader = peers[0].public_key.clone();
4393 let round = Round::new(Epoch::zero(), View::new(1));
4394
4395 peers[receiver_idx]
4396 .mailbox
4397 .discovered(fake_commitment, leader.clone(), round);
4398 let mut block_sub = peers[receiver_idx].mailbox.subscribe(fake_commitment);
4399
4400 let receiver_shard_idx = peers[receiver_idx].index.get() as u16;
4401 let mut leader_shard = coded_block
4402 .shard(receiver_shard_idx)
4403 .expect("missing shard");
4404 leader_shard.commitment = fake_commitment;
4405 peers[0].sender.send(
4406 Recipients::One(receiver_pk.clone()),
4407 leader_shard.encode(),
4408 true,
4409 );
4410
4411 for &idx in &[1usize, 2, 4] {
4412 let peer_shard_idx = peers[idx].index.get() as u16;
4413 let mut shard = coded_block.shard(peer_shard_idx).expect("missing shard");
4414 shard.commitment = fake_commitment;
4415 peers[idx].sender.send(
4416 Recipients::One(receiver_pk.clone()),
4417 shard.encode(),
4418 true,
4419 );
4420 }
4421
4422 context.sleep(config.link.latency * 2).await;
4423
4424 assert!(
4425 peers[receiver_idx]
4426 .mailbox
4427 .get(fake_commitment)
4428 .await
4429 .is_none(),
4430 "block should not be available after ContextMismatch"
4431 );
4432 assert!(
4433 matches!(block_sub.try_recv(), Err(TryRecvError::Closed)),
4434 "subscription should close for context-mismatched commitment"
4435 );
4436
4437 let round2 = Round::new(Epoch::zero(), View::new(2));
4439 peers[receiver_idx]
4440 .mailbox
4441 .discovered(real_commitment, leader.clone(), round2);
4442
4443 let real_leader_shard = coded_block
4444 .shard(receiver_shard_idx)
4445 .expect("missing shard");
4446 peers[0].sender.send(
4447 Recipients::One(receiver_pk.clone()),
4448 real_leader_shard.encode(),
4449 true,
4450 );
4451
4452 for &idx in &[1usize, 2, 4] {
4453 let peer_shard_idx = peers[idx].index.get() as u16;
4454 let shard = coded_block.shard(peer_shard_idx).expect("missing shard");
4455 peers[idx].sender.send(
4456 Recipients::One(receiver_pk.clone()),
4457 shard.encode(),
4458 true,
4459 );
4460 }
4461
4462 context.sleep(config.link.latency * 2).await;
4463
4464 let reconstructed = peers[receiver_idx]
4465 .mailbox
4466 .get(real_commitment)
4467 .await
4468 .expect("valid block should reconstruct after prior context mismatch");
4469 assert_eq!(reconstructed.commitment(), real_commitment);
4470 },
4471 );
4472 }
4473
4474 #[test_traced]
4475 fn test_same_round_equivocation_preserves_certifiable_recovery() {
4476 let fixture: Fixture<C> = Fixture {
4485 num_peers: 10,
4486 ..Default::default()
4487 };
4488
4489 fixture.start(
4490 |config, context, oracle, mut peers, _, coding_config| async move {
4491 let receiver_idx = 3usize;
4492 let receiver_pk = peers[receiver_idx].public_key.clone();
4493 let receiver_shard_idx = peers[receiver_idx].index.get() as u16;
4494
4495 let leader = peers[0].public_key.clone();
4496 let round = Round::new(Epoch::zero(), View::new(7));
4497
4498 let block_a = CodedBlock::<B, C, H>::new(
4500 B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 111),
4501 coding_config,
4502 &STRATEGY,
4503 );
4504 let commitment_a = block_a.commitment();
4505 let block_b = CodedBlock::<B, C, H>::new(
4506 B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 222),
4507 coding_config,
4508 &STRATEGY,
4509 );
4510 let commitment_b = block_b.commitment();
4511
4512 peers[receiver_idx]
4514 .mailbox
4515 .discovered(commitment_a, leader.clone(), round);
4516 peers[receiver_idx]
4517 .mailbox
4518 .discovered(commitment_b, leader.clone(), round);
4519
4520 let certifiable_sub = peers[receiver_idx].mailbox.subscribe(commitment_b);
4522
4523 let shard_b = block_b
4525 .shard(receiver_shard_idx)
4526 .expect("missing shard")
4527 .encode();
4528 peers[0]
4529 .sender
4530 .send(Recipients::One(receiver_pk.clone()), shard_b, true);
4531
4532 let shard_a = block_a
4534 .shard(receiver_shard_idx)
4535 .expect("missing shard")
4536 .encode();
4537 peers[0]
4538 .sender
4539 .send(Recipients::One(receiver_pk.clone()), shard_a, true);
4540 for i in [1usize, 2usize, 4usize] {
4541 let shard_a = block_a
4542 .shard(peers[i].index.get() as u16)
4543 .expect("missing shard")
4544 .encode();
4545 peers[i]
4546 .sender
4547 .send(Recipients::One(receiver_pk.clone()), shard_a, true);
4548 }
4549 context.sleep(config.link.latency * 4).await;
4550 let reconstructed_a = peers[receiver_idx]
4551 .mailbox
4552 .get(commitment_a)
4553 .await
4554 .expect("conflicting commitment should reconstruct first");
4555 assert_eq!(reconstructed_a.commitment(), commitment_a);
4556
4557 for i in [1usize, 2usize, 4usize] {
4559 let shard_b = block_b
4560 .shard(peers[i].index.get() as u16)
4561 .expect("missing shard")
4562 .encode();
4563 peers[i]
4564 .sender
4565 .send(Recipients::One(receiver_pk.clone()), shard_b, true);
4566 }
4567
4568 select! {
4569 result = certifiable_sub => {
4570 let reconstructed_b =
4571 result.expect("certifiable commitment should remain recoverable");
4572 assert_eq!(reconstructed_b.commitment(), commitment_b);
4573 },
4574 _ = context.sleep(Duration::from_secs(5)) => {
4575 panic!("certifiable commitment was not recoverable after same-round equivocation");
4576 },
4577 }
4578
4579 let blocked_peers = oracle.blocked().await.unwrap();
4582 let is_blocked = blocked_peers
4583 .iter()
4584 .any(|(a, b)| a == &receiver_pk && b == &leader);
4585 assert!(
4586 !is_blocked,
4587 "leader must not be blocked for same-round cross-commitment shards"
4588 );
4589 },
4590 );
4591 }
4592
4593 #[test_traced]
4594 fn test_leader_unrelated_shard_blocks_peer() {
4595 let fixture: Fixture<C> = Fixture {
4599 num_peers: 10,
4600 ..Default::default()
4601 };
4602
4603 fixture.start(
4604 |config, context, oracle, mut peers, _, coding_config| async move {
4605 let tracked_block = CodedBlock::<B, C, H>::new(
4607 B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100),
4608 coding_config,
4609 &STRATEGY,
4610 );
4611 let tracked_commitment = tracked_block.commitment();
4612
4613 let unrelated_block = CodedBlock::<B, C, H>::new(
4615 B::new::<H>((), Sha256Digest::EMPTY, Height::new(2), 200),
4616 coding_config,
4617 &STRATEGY,
4618 );
4619
4620 let receiver_idx = 3usize;
4621 let receiver_pk = peers[receiver_idx].public_key.clone();
4622 let leader_idx = 0usize;
4623 let leader_pk = peers[leader_idx].public_key.clone();
4624
4625 peers[receiver_idx].mailbox.discovered(
4627 tracked_commitment,
4628 leader_pk.clone(),
4629 Round::new(Epoch::zero(), View::new(1)),
4630 );
4631
4632 let mut unrelated_shard = unrelated_block
4635 .shard(peers[1].index.get() as u16)
4636 .expect("missing shard");
4637 unrelated_shard.commitment = tracked_commitment;
4638
4639 peers[leader_idx].sender.send(
4643 Recipients::One(receiver_pk),
4644 unrelated_shard.encode(),
4645 true,
4646 );
4647 context.sleep(config.link.latency * 2).await;
4648
4649 assert_blocked(&oracle, &peers[receiver_idx].public_key, &leader_pk).await;
4650 },
4651 );
4652 }
4653
4654 #[test_traced]
4655 fn test_withholding_leader_victim_reconstructs_via_gossip() {
4656 let fixture = Fixture {
4661 num_peers: 10,
4662 ..Default::default()
4663 };
4664
4665 fixture.start(
4666 |config, context, oracle, mut peers, _, coding_config| async move {
4667 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
4668 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4669 let commitment = coded_block.commitment();
4670 let round = Round::new(Epoch::zero(), View::new(1));
4671
4672 let leader = peers[0].public_key.clone();
4673 let victim = peers[1].public_key.clone();
4674
4675 oracle
4678 .remove_link(leader.clone(), victim.clone())
4679 .await
4680 .expect("remove_link should succeed");
4681
4682 peers[0].mailbox.proposed(round, coded_block.clone());
4685
4686 for peer in peers[1..].iter_mut() {
4689 peer.mailbox.discovered(commitment, leader.clone(), round);
4690 }
4691 context.sleep(config.link.latency * 2).await;
4692
4693 let block_sub = peers[1].mailbox.subscribe(commitment);
4696 select! {
4697 result = block_sub => {
4698 let reconstructed = result.expect("block subscription should resolve");
4699 assert_eq!(reconstructed.commitment(), commitment);
4700 assert_eq!(reconstructed.height(), coded_block.height());
4701 },
4702 _ = context.sleep(Duration::from_secs(5)) => {
4703 panic!("victim did not reconstruct block despite withholding leader");
4704 },
4705 }
4706
4707 for peer in peers[2..].iter_mut() {
4709 let reconstructed = peer
4710 .mailbox
4711 .get(commitment)
4712 .await
4713 .expect("block should be reconstructed");
4714 assert_eq!(reconstructed.commitment(), commitment);
4715 }
4716
4717 let blocked = oracle.blocked().await.unwrap();
4719 assert!(
4720 blocked.is_empty(),
4721 "no peer should be blocked in withholding leader test"
4722 );
4723 },
4724 );
4725 }
4726
4727 #[test_traced]
4733 fn test_shard_subscription_pending_after_reconstruction_without_leader_shard() {
4734 let fixture = Fixture {
4735 num_peers: 10,
4736 ..Default::default()
4737 };
4738
4739 fixture.start(
4740 |config, context, oracle, mut peers, _, coding_config| async move {
4741 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
4742 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4743 let commitment = coded_block.commitment();
4744 let round = Round::new(Epoch::zero(), View::new(1));
4745
4746 let leader = peers[0].public_key.clone();
4747 let victim = peers[1].public_key.clone();
4748
4749 oracle
4752 .remove_link(leader.clone(), victim.clone())
4753 .await
4754 .expect("remove_link should succeed");
4755
4756 let mut shard_sub = peers[1]
4758 .mailbox
4759 .subscribe_assigned_shard_verified(commitment);
4760 let block_sub = peers[1].mailbox.subscribe(commitment);
4761
4762 peers[0].mailbox.proposed(round, coded_block.clone());
4764
4765 for peer in peers[1..].iter_mut() {
4767 peer.mailbox.discovered(commitment, leader.clone(), round);
4768 }
4769
4770 context.sleep(config.link.latency * 4).await;
4772
4773 let reconstructed = block_sub.await.expect("block subscription should resolve");
4776 assert_eq!(reconstructed.commitment(), commitment);
4777
4778 assert!(
4781 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
4782 "shard subscription must not resolve without own shard verification"
4783 );
4784 },
4785 );
4786 }
4787
4788 #[test_traced]
4789 fn test_broadcast_routes_participant_and_non_participant_shards() {
4790 let fixture = Fixture {
4791 num_non_participants: 1,
4792 ..Default::default()
4793 };
4794
4795 fixture.start(
4796 |config, context, oracle, mut peers, non_participants, coding_config| async move {
4797 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
4798 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4799 let commitment = coded_block.commitment();
4800
4801 let leader = peers[0].public_key.clone();
4802 let round = Round::new(Epoch::zero(), View::new(1));
4803 peers[0].mailbox.proposed(round, coded_block.clone());
4804
4805 for peer in peers[1..].iter_mut() {
4806 peer.mailbox.discovered(commitment, leader.clone(), round);
4807 }
4808 for np in non_participants.iter() {
4809 np.mailbox.discovered(commitment, leader.clone(), round);
4810 }
4811 context.sleep(config.link.latency * 2).await;
4812
4813 for peer in peers.iter_mut() {
4815 peer.mailbox
4816 .subscribe_assigned_shard_verified(commitment)
4817 .await
4818 .expect("participant shard subscription should complete");
4819 }
4820
4821 for np in non_participants.iter() {
4823 np.mailbox
4824 .subscribe_assigned_shard_verified(commitment)
4825 .await
4826 .expect("non-participant shard subscription should complete");
4827 }
4828 context.sleep(config.link.latency).await;
4829
4830 for np in non_participants.iter() {
4832 let reconstructed = np
4833 .mailbox
4834 .get(commitment)
4835 .await
4836 .expect("non-participant should reconstruct block");
4837 assert_eq!(reconstructed.commitment(), commitment);
4838 }
4839
4840 let blocked = oracle.blocked().await.unwrap();
4841 assert!(
4842 blocked.is_empty(),
4843 "no peer should be blocked in participant/non-participant shard routing test"
4844 );
4845 },
4846 );
4847 }
4848
4849 #[test_traced]
4850 fn test_non_participant_reconstructs_after_discovered() {
4851 let fixture = Fixture {
4852 num_non_participants: 1,
4853 ..Default::default()
4854 };
4855
4856 fixture.start(
4857 |config, context, oracle, mut peers, non_participants, coding_config| async move {
4858 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
4859 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4860 let commitment = coded_block.commitment();
4861 let round = Round::new(Epoch::zero(), View::new(1));
4862
4863 let leader = peers[0].public_key.clone();
4864 peers[0].mailbox.proposed(round, coded_block.clone());
4865
4866 for peer in peers[1..].iter_mut() {
4869 peer.mailbox.discovered(commitment, leader.clone(), round);
4870 }
4871 context.sleep(config.link.latency).await;
4872
4873 let np = &non_participants[0];
4876 let block_sub = np.mailbox.subscribe(commitment);
4877 np.mailbox.discovered(commitment, leader.clone(), round);
4878
4879 select! {
4882 result = block_sub => {
4883 let reconstructed = result.expect("block subscription should resolve");
4884 assert_eq!(reconstructed.commitment(), commitment);
4885 assert_eq!(reconstructed.height(), coded_block.height());
4886 },
4887 _ = context.sleep(Duration::from_secs(5)) => {
4888 panic!("non-participant block subscription did not resolve");
4889 },
4890 }
4891
4892 let blocked = oracle.blocked().await.unwrap();
4893 assert!(
4894 blocked.is_empty(),
4895 "no peer should be blocked in non-participant reconstruction test"
4896 );
4897 },
4898 );
4899 }
4900
4901 #[test_traced]
4902 fn test_peer_set_update_evicts_peer_buffers() {
4903 let executor = deterministic::Runner::default();
4908 executor.start(|context| async move {
4909 let num_peers = 10usize;
4910 let (network, oracle) = simulated::Network::<deterministic::Context, P>::new(
4911 context.child("network"),
4912 simulated::Config {
4913 max_size: MAX_SHARD_SIZE as u32,
4914 disconnect_on_block: true,
4915 tracked_peer_sets: NZUsize!(2),
4916 },
4917 );
4918 network.start();
4919
4920 let mut private_keys = (0..num_peers)
4921 .map(|i| PrivateKey::from_seed(i as u64))
4922 .collect::<Vec<_>>();
4923 private_keys.sort_by_key(|s| s.public_key());
4924 let peer_keys: Vec<P> = private_keys.iter().map(|c| c.public_key()).collect();
4925 let participants: Set<P> = Set::from_iter_dedup(peer_keys.clone());
4926
4927 let receiver_idx = 3usize;
4929 let receiver_pk = peer_keys[receiver_idx].clone();
4930 let leader_pk = peer_keys[0].clone();
4931
4932 let receiver_control = oracle.control(receiver_pk.clone());
4933 let (sender_handle, receiver_handle) = receiver_control
4934 .register(0, TEST_QUOTA)
4935 .await
4936 .expect("registration should succeed");
4937
4938 let leader_control = oracle.control(leader_pk.clone());
4940 let (mut leader_sender, _leader_receiver) = leader_control
4941 .register(0, TEST_QUOTA)
4942 .await
4943 .expect("registration should succeed");
4944 oracle
4945 .add_link(leader_pk.clone(), receiver_pk.clone(), DEFAULT_LINK)
4946 .await
4947 .expect("link should be added");
4948
4949 oracle.manager().track(0, participants.clone());
4951 context.sleep(Duration::from_millis(10)).await;
4952
4953 let scheme = Scheme::signer(
4954 SCHEME_NAMESPACE,
4955 participants.clone(),
4956 private_keys[receiver_idx].clone(),
4957 )
4958 .expect("signer scheme should be created");
4959
4960 let config: Config<_, _, _, _, C, _, _, _> = Config {
4961 scheme_provider: MultiEpochProvider::single(scheme),
4962 blocker: receiver_control.clone(),
4963 shard_codec_cfg: CodecConfig {
4964 maximum_shard_size: MAX_SHARD_SIZE,
4965 },
4966 block_codec_cfg: (),
4967 strategy: STRATEGY,
4968 mailbox_size: NZUsize!(1024),
4969 peer_buffer_size: NZUsize!(64),
4970 background_channel_capacity: NZUsize!(1024),
4971 peer_provider: oracle.manager(),
4972 };
4973
4974 let (engine, mailbox) = ShardEngine::new(context.child("receiver"), config);
4975 engine.start((sender_handle, receiver_handle));
4976
4977 let coding_config = coding_config_for_participants(num_peers as u16);
4979 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
4980 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
4981 let commitment = coded_block.commitment();
4982
4983 let receiver_participant = participants
4984 .index(&receiver_pk)
4985 .expect("receiver must be a participant");
4986 let leader_shard = coded_block
4987 .shard(receiver_participant.get() as u16)
4988 .expect("missing shard");
4989 let shard_bytes = leader_shard.encode();
4990
4991 leader_sender.send(
4993 Recipients::One(receiver_pk.clone()),
4994 shard_bytes.clone(),
4995 true,
4996 );
4997 context.sleep(DEFAULT_LINK.latency * 2).await;
4998
4999 let remaining: Set<P> =
5001 Set::from_iter_dedup(peer_keys.iter().filter(|pk| **pk != leader_pk).cloned());
5002 oracle.manager().track(1, remaining);
5003 context.sleep(Duration::from_millis(10)).await;
5004
5005 leader_sender.send(Recipients::One(receiver_pk.clone()), shard_bytes, true);
5008 context.sleep(DEFAULT_LINK.latency * 2).await;
5009
5010 let mut shard_sub = mailbox.subscribe_assigned_shard_verified(commitment);
5013 mailbox.discovered(
5014 commitment,
5015 leader_pk.clone(),
5016 Round::new(Epoch::zero(), View::new(1)),
5017 );
5018 context.sleep(DEFAULT_LINK.latency * 2).await;
5019
5020 assert!(
5022 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
5023 "shard subscription should not resolve after evicted leader's buffer"
5024 );
5025 assert!(
5026 mailbox.get(commitment).await.is_none(),
5027 "block should not reconstruct from evicted buffers"
5028 );
5029 });
5030 }
5031
5032 #[test_traced]
5033 fn test_peer_buffer_lifetime_tracks_latest_primary() {
5034 let executor = deterministic::Runner::default();
5035 executor.start(|context| async move {
5036 let (network, oracle) = simulated::Network::<deterministic::Context, P>::new(
5037 context.child("network"),
5038 simulated::Config {
5039 max_size: MAX_SHARD_SIZE as u32,
5040 disconnect_on_block: true,
5041 tracked_peer_sets: NZUsize!(1),
5042 },
5043 );
5044 network.start();
5045
5046 let mut private_keys = (0..4)
5047 .map(|i| PrivateKey::from_seed(i as u64))
5048 .collect::<Vec<_>>();
5049 private_keys.sort_by_key(|s| s.public_key());
5050 let peer_keys: Vec<P> = private_keys.iter().map(|c| c.public_key()).collect();
5051 let receiver_pk = peer_keys[0].clone();
5052 let sender_pk = peer_keys[1].clone();
5053 let participants: Set<P> = Set::from_iter_dedup(peer_keys);
5054
5055 let receiver_control = oracle.control(receiver_pk);
5056 let scheme = Scheme::signer(
5057 SCHEME_NAMESPACE,
5058 participants.clone(),
5059 private_keys[0].clone(),
5060 )
5061 .expect("signer scheme should be created");
5062
5063 let config: Config<_, _, _, _, C, _, _, _> = Config {
5064 scheme_provider: MultiEpochProvider::single(scheme),
5065 blocker: receiver_control,
5066 shard_codec_cfg: CodecConfig {
5067 maximum_shard_size: MAX_SHARD_SIZE,
5068 },
5069 block_codec_cfg: (),
5070 strategy: STRATEGY,
5071 mailbox_size: NZUsize!(16),
5072 peer_buffer_size: NZUsize!(4),
5073 background_channel_capacity: NZUsize!(16),
5074 peer_provider: oracle.manager(),
5075 };
5076
5077 let (mut engine, _mailbox) = ShardEngine::new(context.child("engine"), config);
5078
5079 engine.update_latest_primary_peers(Set::from_iter_dedup([sender_pk.clone()]));
5082
5083 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
5084 let coded_block = CodedBlock::<B, C, H>::new(
5085 inner,
5086 coding_config_for_participants(participants.len() as u16),
5087 &STRATEGY,
5088 );
5089 let shard = coded_block.shard(0).expect("missing shard");
5090
5091 engine.buffer_peer_shard(sender_pk.clone(), shard);
5093 assert_eq!(
5094 engine.peer_buffers.get(&sender_pk).map(VecDeque::len),
5095 Some(1),
5096 "peer buffer should contain the buffered shard"
5097 );
5098
5099 engine.update_latest_primary_peers(Set::default());
5102 assert!(
5103 !engine.peer_buffers.contains_key(&sender_pk),
5104 "peer buffer should be evicted once sender leaves latest.primary"
5105 );
5106 });
5107 }
5108
5109 #[test_traced]
5110 fn test_old_epoch_buffered_shards_are_dropped_after_cutover() {
5111 let executor = deterministic::Runner::default();
5112 executor.start(|context| async move {
5113 let num_peers = 6usize;
5114 let (network, oracle) = simulated::Network::<deterministic::Context, P>::new(
5115 context.child("network"),
5116 simulated::Config {
5117 max_size: MAX_SHARD_SIZE as u32,
5118 disconnect_on_block: true,
5119 tracked_peer_sets: NZUsize!(2),
5120 },
5121 );
5122 network.start();
5123
5124 let mut private_keys = (0..num_peers)
5125 .map(|i| PrivateKey::from_seed(i as u64))
5126 .collect::<Vec<_>>();
5127 private_keys.sort_by_key(|s| s.public_key());
5128 let peer_keys: Vec<P> = private_keys.iter().map(|c| c.public_key()).collect();
5129
5130 let epoch0_set: Set<P> = Set::from_iter_dedup(peer_keys[..5].iter().cloned());
5133 let epoch1_set: Set<P> = Set::from_iter_dedup([
5134 peer_keys[1].clone(),
5135 peer_keys[2].clone(),
5136 peer_keys[3].clone(),
5137 peer_keys[4].clone(),
5138 peer_keys[5].clone(),
5139 ]);
5140
5141 let receiver_idx = 3usize;
5142 let receiver_pk = peer_keys[receiver_idx].clone();
5143 let receiver_key = private_keys[receiver_idx].clone();
5144 let leader_pk = peer_keys[0].clone();
5145
5146 let receiver_control = oracle.control(receiver_pk.clone());
5147 let (sender_handle, receiver_handle) = receiver_control
5148 .register(0, TEST_QUOTA)
5149 .await
5150 .expect("registration should succeed");
5151
5152 let leader_control = oracle.control(leader_pk.clone());
5153 let (mut leader_sender, _leader_receiver) = leader_control
5154 .register(0, TEST_QUOTA)
5155 .await
5156 .expect("registration should succeed");
5157 oracle
5158 .add_link(leader_pk.clone(), receiver_pk.clone(), DEFAULT_LINK)
5159 .await
5160 .expect("link should be added");
5161
5162 oracle.manager().track(0, epoch0_set.clone());
5164 context.sleep(Duration::from_millis(10)).await;
5165
5166 let scheme_epoch0 =
5167 Scheme::signer(SCHEME_NAMESPACE, epoch0_set.clone(), receiver_key.clone())
5168 .expect("epoch 0 signer scheme should be created");
5169 let scheme_epoch1 =
5170 Scheme::signer(SCHEME_NAMESPACE, epoch1_set.clone(), receiver_key.clone())
5171 .expect("epoch 1 signer scheme should be created");
5172
5173 let config: Config<_, _, _, _, C, _, _, _> = Config {
5174 scheme_provider: MultiEpochProvider::single(scheme_epoch0)
5175 .with_epoch(Epoch::new(1), scheme_epoch1),
5176 blocker: receiver_control.clone(),
5177 shard_codec_cfg: CodecConfig {
5178 maximum_shard_size: MAX_SHARD_SIZE,
5179 },
5180 block_codec_cfg: (),
5181 strategy: STRATEGY,
5182 mailbox_size: NZUsize!(1024),
5183 peer_buffer_size: NZUsize!(64),
5184 background_channel_capacity: NZUsize!(1024),
5185 peer_provider: oracle.manager(),
5186 };
5187
5188 let (engine, mailbox) = ShardEngine::new(context.child("receiver"), config);
5190 engine.start((sender_handle, receiver_handle));
5191
5192 let coding_config = coding_config_for_participants(epoch0_set.len() as u16);
5193 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
5194 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
5195 let commitment = coded_block.commitment();
5196
5197 let receiver_participant = epoch0_set
5198 .index(&receiver_pk)
5199 .expect("receiver must be an epoch 0 participant");
5200 let leader_shard = coded_block
5201 .shard(receiver_participant.get() as u16)
5202 .expect("missing shard");
5203
5204 leader_sender.send(
5206 Recipients::One(receiver_pk.clone()),
5207 leader_shard.encode(),
5208 true,
5209 );
5210 context.sleep(DEFAULT_LINK.latency * 2).await;
5211
5212 oracle.manager().track(1, epoch1_set);
5216 context.sleep(Duration::from_millis(10)).await;
5217
5218 let mut shard_sub = mailbox.subscribe_assigned_shard_verified(commitment);
5221 mailbox.discovered(
5222 commitment,
5223 leader_pk,
5224 Round::new(Epoch::zero(), View::new(1)),
5225 );
5226 context.sleep(DEFAULT_LINK.latency * 2).await;
5227
5228 assert!(
5229 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
5230 "old-epoch shard subscription should stay pending after cutover"
5231 );
5232 assert!(
5233 mailbox.get(commitment).await.is_none(),
5234 "old-epoch commitment should not reconstruct from overlap-only buffered shards"
5235 );
5236 });
5237 }
5238
5239 #[test_traced]
5248 fn test_evicted_node_still_reconstructs_from_buffered_peer_shards() {
5249 let executor = deterministic::Runner::default();
5250 executor.start(|context| async move {
5251 let num_peers = 10usize;
5252 let (network, oracle) = simulated::Network::<deterministic::Context, P>::new(
5253 context.child("network"),
5254 simulated::Config {
5255 max_size: MAX_SHARD_SIZE as u32,
5256 disconnect_on_block: true,
5257 tracked_peer_sets: NZUsize!(2),
5258 },
5259 );
5260 network.start();
5261
5262 let mut private_keys = (0..num_peers)
5263 .map(|i| PrivateKey::from_seed(i as u64))
5264 .collect::<Vec<_>>();
5265 private_keys.sort_by_key(|s| s.public_key());
5266 let peer_keys: Vec<P> = private_keys.iter().map(|c| c.public_key()).collect();
5267 let participants: Set<P> = Set::from_iter_dedup(peer_keys.clone());
5268
5269 let receiver_idx = 1usize;
5273 let receiver_pk = peer_keys[receiver_idx].clone();
5274 let leader_pk = peer_keys[0].clone();
5275 let peer2_pk = peer_keys[2].clone();
5276 let peer4_pk = peer_keys[4].clone();
5277 let peer5_pk = peer_keys[5].clone();
5278 let peer6_pk = peer_keys[6].clone();
5279
5280 let receiver_control = oracle.control(receiver_pk.clone());
5281 let (evicted_sender, evicted_receiver) = receiver_control
5282 .register(0, TEST_QUOTA)
5283 .await
5284 .expect("registration should succeed");
5285
5286 let peer2_control = oracle.control(peer2_pk.clone());
5287 let (mut peer2_sender, _peer2_receiver) = peer2_control
5288 .register(0, TEST_QUOTA)
5289 .await
5290 .expect("registration should succeed");
5291
5292 let peer4_control = oracle.control(peer4_pk.clone());
5293 let (mut peer4_sender, _peer4_receiver) = peer4_control
5294 .register(0, TEST_QUOTA)
5295 .await
5296 .expect("registration should succeed");
5297
5298 let peer5_control = oracle.control(peer5_pk.clone());
5299 let (mut peer5_sender, _peer5_receiver) = peer5_control
5300 .register(0, TEST_QUOTA)
5301 .await
5302 .expect("registration should succeed");
5303
5304 let peer6_control = oracle.control(peer6_pk.clone());
5305 let (mut peer6_sender, _peer6_receiver) = peer6_control
5306 .register(0, TEST_QUOTA)
5307 .await
5308 .expect("registration should succeed");
5309
5310 for sender in [&peer2_pk, &peer4_pk, &peer5_pk, &peer6_pk] {
5312 oracle
5313 .add_link(sender.clone(), receiver_pk.clone(), DEFAULT_LINK)
5314 .await
5315 .expect("link should be added");
5316 }
5317
5318 oracle.manager().track(0, participants.clone());
5320 context.sleep(Duration::from_millis(10)).await;
5321
5322 let scheme = Scheme::signer(
5323 SCHEME_NAMESPACE,
5324 participants.clone(),
5325 private_keys[receiver_idx].clone(),
5326 )
5327 .expect("signer scheme should be created");
5328
5329 let config: Config<_, _, _, _, C, _, _, _> = Config {
5330 scheme_provider: MultiEpochProvider::single(scheme),
5331 blocker: receiver_control.clone(),
5332 shard_codec_cfg: CodecConfig {
5333 maximum_shard_size: MAX_SHARD_SIZE,
5334 },
5335 block_codec_cfg: (),
5336 strategy: STRATEGY,
5337 mailbox_size: NZUsize!(1024),
5338 peer_buffer_size: NZUsize!(64),
5339 background_channel_capacity: NZUsize!(1024),
5340 peer_provider: oracle.manager(),
5341 };
5342
5343 let (engine, mailbox) = ShardEngine::new(context.child("evicted"), config);
5344 engine.start((evicted_sender, evicted_receiver));
5345
5346 let coding_config = coding_config_for_participants(num_peers as u16);
5347 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
5348 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
5349 let commitment = coded_block.commitment();
5350
5351 let peer2_shard = coded_block.shard(2).expect("missing shard 2").encode();
5352 let peer4_shard = coded_block.shard(4).expect("missing shard 4").encode();
5353 let peer5_shard = coded_block.shard(5).expect("missing shard 5").encode();
5354 let peer6_shard = coded_block.shard(6).expect("missing shard 6").encode();
5355
5356 let block_sub = mailbox.subscribe(commitment);
5357
5358 peer2_sender
5361 .send(
5362 Recipients::One(receiver_pk.clone()),
5363 peer2_shard,
5364 true,
5365 );
5366 peer4_sender
5367 .send(
5368 Recipients::One(receiver_pk.clone()),
5369 peer4_shard,
5370 true,
5371 );
5372 peer5_sender
5373 .send(
5374 Recipients::One(receiver_pk.clone()),
5375 peer5_shard,
5376 true,
5377 );
5378 peer6_sender
5379 .send(
5380 Recipients::One(receiver_pk.clone()),
5381 peer6_shard,
5382 true,
5383 );
5384 context.sleep(DEFAULT_LINK.latency * 2).await;
5385
5386 let latest_primary: Set<P> = Set::from_iter_dedup(
5389 peer_keys
5390 .iter()
5391 .filter(|pk| **pk != receiver_pk)
5392 .cloned(),
5393 );
5394 oracle.manager().track(1, latest_primary);
5395 context.sleep(Duration::from_millis(10)).await;
5396
5397 mailbox
5400 .discovered(
5401 commitment,
5402 leader_pk.clone(),
5403 Round::new(Epoch::zero(), View::new(1)),
5404 );
5405
5406 select! {
5407 _ = block_sub => {},
5408 _ = context.sleep(Duration::from_secs(5)) => {
5409 panic!("block subscription did not resolve after leader discovery");
5410 },
5411 }
5412
5413 context.sleep(DEFAULT_LINK.latency * 2).await;
5414 let block = mailbox.get(commitment).await;
5415 assert!(
5416 block.is_some(),
5417 "evicted node should reconstruct from buffered shards sent by remaining latest.primary peers"
5418 );
5419 assert_eq!(block.unwrap().commitment(), commitment);
5420
5421 assert!(
5422 oracle.blocked().await.unwrap().is_empty(),
5423 "no peer should be blocked when overlapping shards are valid"
5424 );
5425 });
5426 }
5427
5428 #[test_traced]
5433 fn test_late_leader_shard_accepted_after_quorum_transition() {
5434 let fixture = Fixture {
5435 num_peers: 10,
5436 ..Default::default()
5437 };
5438
5439 fixture.start(
5440 |config, context, oracle, mut peers, _, coding_config| async move {
5441 let inner = B::new::<H>((), Sha256Digest::EMPTY, Height::new(1), 100);
5442 let coded_block = CodedBlock::<B, C, H>::new(inner, coding_config, &STRATEGY);
5443 let commitment = coded_block.commitment();
5444 let round = Round::new(Epoch::zero(), View::new(1));
5445
5446 let leader_idx = 0usize;
5447 let victim_idx = 1usize;
5448 let leader = peers[leader_idx].public_key.clone();
5449 let victim = peers[victim_idx].public_key.clone();
5450
5451 oracle
5454 .remove_link(leader.clone(), victim.clone())
5455 .await
5456 .expect("remove_link should succeed");
5457
5458 peers[leader_idx]
5461 .mailbox
5462 .proposed(round, coded_block.clone());
5463
5464 for peer in peers[1..].iter_mut() {
5466 peer.mailbox.discovered(commitment, leader.clone(), round);
5467 }
5468
5469 context.sleep(config.link.latency * 4).await;
5473
5474 let block_sub = peers[victim_idx].mailbox.subscribe(commitment);
5475 select! {
5476 result = block_sub => {
5477 let reconstructed = result.expect("block subscription should resolve");
5478 assert_eq!(reconstructed.commitment(), commitment);
5479 },
5480 _ = context.sleep(Duration::from_secs(5)) => {
5481 panic!("victim did not reconstruct block from gossip");
5482 },
5483 }
5484
5485 let mut shard_sub = peers[victim_idx]
5488 .mailbox
5489 .subscribe_assigned_shard_verified(commitment);
5490 assert!(
5491 matches!(shard_sub.try_recv(), Err(TryRecvError::Empty)),
5492 "shard subscription must not resolve before own shard is verified"
5493 );
5494
5495 oracle
5497 .add_link(leader.clone(), victim.clone(), DEFAULT_LINK)
5498 .await
5499 .expect("add_link should succeed");
5500
5501 let leader_shard = coded_block
5505 .shard(peers[victim_idx].index.get() as u16)
5506 .expect("missing victim shard");
5507 peers[leader_idx].sender.send(
5508 Recipients::One(victim.clone()),
5509 leader_shard.encode(),
5510 true,
5511 );
5512 context.sleep(config.link.latency * 2).await;
5513
5514 select! {
5517 _ = shard_sub => {},
5518 _ = context.sleep(Duration::from_secs(5)) => {
5519 panic!("shard subscription did not resolve after late leader shard");
5520 },
5521 }
5522
5523 let blocked = oracle.blocked().await.unwrap();
5525 assert!(
5526 blocked.is_empty(),
5527 "no peer should be blocked in late leader shard test"
5528 );
5529
5530 let extra_sender_idx = 2usize;
5533 let extra_shard = coded_block
5534 .shard(peers[extra_sender_idx].index.get() as u16)
5535 .expect("missing shard");
5536 peers[extra_sender_idx].sender.send(
5537 Recipients::One(victim.clone()),
5538 extra_shard.encode(),
5539 true,
5540 );
5541 context.sleep(config.link.latency * 2).await;
5542
5543 let blocked = oracle.blocked().await.unwrap();
5545 assert!(
5546 blocked.is_empty(),
5547 "gossip shard after full reconstruction should be silently ignored"
5548 );
5549 },
5550 );
5551 }
5552}