1use std::{
2 collections::BTreeMap,
3 mem,
4 ops::Range,
5 sync::{
6 Arc, LazyLock,
7 atomic::{AtomicBool, AtomicUsize, Ordering},
8 },
9};
10
11use hotshot_types::{
12 PeerConfig,
13 data::{
14 EpochNumber, VidCommitment2, VidDisperse2, ViewNumber,
15 vid_disperse::{
16 AvidmGf2DisperseParams, AvidmGf2DisperseShareFragment, AvidmGf2NamespacePiece,
17 },
18 },
19 epoch_membership::EpochMembershipCoordinator,
20 message::Proposal as SignedProposal,
21 traits::{
22 BlockPayload, metrics::Histogram, node_implementation::NodeType,
23 signature_key::SignatureKey,
24 },
25 vid::avidm_gf2::{AvidmGf2NsDispersal, AvidmGf2Scheme},
26};
27use hotshot_utils::anytrace::{self, Wrap};
28use rayon::prelude::*;
29use tokio::task::{AbortHandle, JoinSet};
30use tracing::{debug_span, error, warn};
31
32use crate::{
33 coordinator::metrics::{Measurement, finish_measurement},
34 leader_trace::{LeaderEvent, LeaderTracerHandle, now_ns},
35 message::{ConsensusMessage, Message, MessageType, Validated},
36 network::{NetworkError, Sender},
37 trace_leader_event,
38};
39
40static NUM_THREADS: LazyLock<usize> = LazyLock::new(|| rayon::current_num_threads().max(1));
41
42#[derive(Clone, Eq, PartialEq, Debug)]
43pub struct VidDisperseRequest<T: NodeType> {
44 pub view: ViewNumber,
45 pub epoch: EpochNumber,
46 pub block: T::BlockPayload,
47 pub metadata: <T::BlockPayload as BlockPayload<T>>::Metadata,
48 pub payload_commitment: VidCommitment2,
49}
50
51pub struct BuiltDispersal<T: NodeType> {
54 params: AvidmGf2DisperseParams<T>,
55 dispersals: Vec<AvidmGf2NsDispersal>,
57}
58
59impl<T: NodeType> BuiltDispersal<T> {
60 pub fn new(
62 stake_table: &[PeerConfig<T>],
63 payload: Arc<[u8]>,
64 metadata: &[u8],
65 ) -> Result<Self, VidDisperseError> {
66 let params = AvidmGf2DisperseParams::new(stake_table, payload, metadata)
67 .map_err(VidDisperseError::Vid)?;
68 let dispersals = (0..params.ns_table.len())
69 .into_par_iter()
70 .map(|ns_index| disperse_namespace(¶ms, ns_index))
71 .collect::<Result<_, _>>()?;
72 Ok(Self { params, dispersals })
73 }
74
75 pub fn payload_commitment(&self) -> Result<VidCommitment2, VidDisperseError> {
78 let commits: Vec<_> = self
79 .dispersals
80 .iter()
81 .map(|dispersal| dispersal.commit)
82 .collect();
83 AvidmGf2Scheme::aggregate_commit(&commits)
84 .wrap()
85 .map_err(VidDisperseError::Vid)
86 }
87}
88
89pub enum Dispersal<T: NodeType> {
91 Built(BuiltDispersal<T>),
93 Encode(AvidmGf2DisperseParams<T>),
95}
96
97pub struct VidDisperseOutput {
98 pub view: ViewNumber,
99 pub payload_commitment: VidCommitment2,
100}
101
102pub struct VidDisperser<T: NodeType> {
103 calculations: BTreeMap<(ViewNumber, EpochNumber, VidCommitment2), AbortHandle>,
104 epoch_membership_coordinator: EpochMembershipCoordinator<T>,
105 network: Sender<T>,
106 public_key: T::SignatureKey,
107 private_key: <T::SignatureKey as SignatureKey>::PrivateKey,
108 tasks: JoinSet<Result<VidDisperseOutput, VidDisperseError>>,
109 duration_metric: Option<Arc<dyn Histogram>>,
110 tracer: Option<LeaderTracerHandle>,
111}
112
113impl<T: NodeType> VidDisperser<T> {
114 pub fn new(
115 epoch_membership_coordinator: EpochMembershipCoordinator<T>,
116 network: Sender<T>,
117 public_key: T::SignatureKey,
118 private_key: <T::SignatureKey as SignatureKey>::PrivateKey,
119 ) -> Self {
120 Self {
121 calculations: BTreeMap::new(),
122 epoch_membership_coordinator,
123 network,
124 public_key,
125 private_key,
126 tasks: JoinSet::new(),
127 duration_metric: None,
128 tracer: None,
129 }
130 }
131
132 pub fn set_tracer(&mut self, tracer: Option<LeaderTracerHandle>) {
133 self.tracer = tracer;
134 }
135
136 pub fn with_metrics(mut self, hist: Option<Arc<dyn Histogram>>) -> Self {
137 self.duration_metric = hist;
138 self
139 }
140
141 pub fn request_vid_disperse(
146 &mut self,
147 vid_disperse_request: VidDisperseRequest<T>,
148 built: Option<BuiltDispersal<T>>,
149 ) {
150 let key = (
151 vid_disperse_request.view,
152 vid_disperse_request.epoch,
153 vid_disperse_request.payload_commitment,
154 );
155 if self.calculations.contains_key(&key) {
156 return;
157 }
158 let membership = self.epoch_membership_coordinator.clone();
159 let network = self.network.clone();
160 let public_key = self.public_key.clone();
161 let private_key = self.private_key.clone();
162 let duration_metric = self.duration_metric.clone();
163 let tracer = self.tracer.clone();
164 let view = vid_disperse_request.view;
165 let handle = self.tasks.spawn_blocking(move || {
166 let measurement = duration_metric.map(Measurement::start);
167 let result = handle_vid_disperse_request(
168 membership,
169 move |recipient, message| network.unicast(view, recipient, message),
170 public_key,
171 private_key,
172 vid_disperse_request,
173 tracer,
174 built,
175 );
176 finish_measurement(measurement);
177 result
178 });
179 self.calculations.insert(key, handle);
180 }
181
182 pub async fn next(&mut self) -> Option<Result<VidDisperseOutput, VidDisperseError>> {
183 loop {
184 match self.tasks.join_next().await {
185 Some(Ok(result)) => return Some(result),
186 Some(Err(err)) => {
187 if err.is_panic() {
188 error!(%err, "vid disperse task panic");
189 }
190 continue;
191 },
192 None => return None,
193 }
194 }
195 }
196
197 pub fn gc(&mut self, view_number: ViewNumber) {
198 let keep = self.calculations.split_off(&(
199 view_number,
200 EpochNumber::new(0),
201 VidCommitment2::default(),
202 ));
203 for handle in self.calculations.values_mut() {
204 handle.abort();
205 }
206 self.calculations = keep;
207 }
208}
209
210fn handle_vid_disperse_request<T, S>(
211 epoch_membership_coordinator: EpochMembershipCoordinator<T>,
212 send: S,
213 public_key: T::SignatureKey,
214 private_key: <T::SignatureKey as SignatureKey>::PrivateKey,
215 vid_disperse_request: VidDisperseRequest<T>,
216 tracer: Option<LeaderTracerHandle>,
217 built: Option<BuiltDispersal<T>>,
218) -> Result<VidDisperseOutput, VidDisperseError>
219where
220 T: NodeType,
221 S: Fn(&T::SignatureKey, &Message<T, Validated>) -> Result<(), NetworkError> + Clone + Send,
222{
223 let view = vid_disperse_request.view;
224 let epoch = vid_disperse_request.epoch;
225 let payload_commitment = vid_disperse_request.payload_commitment;
226
227 let dispersal = match built {
228 Some(built) => {
229 trace_leader_event!(tracer, view, LeaderEvent::VidFragmentsStart);
230 Dispersal::Built(built)
231 },
232 None => {
233 trace_leader_event!(tracer, view, LeaderEvent::NsDisperseStart);
234 Dispersal::Encode(
235 VidDisperse2::<T>::disperse_params(
236 &vid_disperse_request.block,
237 &epoch_membership_coordinator,
238 Some(epoch),
239 &vid_disperse_request.metadata,
240 )
241 .map_err(VidDisperseError::Vid)?,
242 )
243 },
244 };
245
246 disperse_payload(
247 dispersal,
248 view,
249 epoch,
250 payload_commitment,
251 &public_key,
252 &private_key,
253 tracer.as_ref(),
254 send,
255 )?;
256
257 trace_leader_event!(tracer, view, LeaderEvent::VidSharesUnicastEnd);
258
259 Ok(VidDisperseOutput {
260 view,
261 payload_commitment,
262 })
263}
264
265#[allow(clippy::too_many_arguments)]
270pub fn disperse_payload<T, S>(
271 dispersal: Dispersal<T>,
272 view: ViewNumber,
273 epoch: EpochNumber,
274 payload_commitment: VidCommitment2,
275 public_key: &T::SignatureKey,
276 private_key: &<T::SignatureKey as SignatureKey>::PrivateKey,
277 tracer: Option<&LeaderTracerHandle>,
278 send: S,
279) -> Result<(), VidDisperseError>
280where
281 T: NodeType,
282 S: Fn(&T::SignatureKey, &Message<T, Validated>) -> Result<(), NetworkError> + Clone + Send,
283{
284 let (params, dispersals, assembled_event) = match dispersal {
285 Dispersal::Built(BuiltDispersal { params, dispersals }) => {
286 (params, Some(dispersals), LeaderEvent::VidFragmentsEnd)
287 },
288 Dispersal::Encode(params) => (params, None, LeaderEvent::NsDisperseEnd),
289 };
290
291 let signature = debug_span!("sign")
292 .in_scope(|| T::SignatureKey::sign(private_key, payload_commitment.as_ref()))
293 .map_err(|err| VidDisperseError::Sign(err.into()))?;
294
295 let ns_buckets: Vec<Vec<usize>> = {
301 let threshold = params.payload.len().div_ceil(*NUM_THREADS).max(256 * 1024);
305 bucketize(¶ms.ns_table, threshold)
306 };
307 let buckets: Vec<Bucket> = match dispersals {
308 Some(dispersals) => into_buckets(dispersals, &ns_buckets)
309 .into_iter()
310 .map(Bucket::Built)
311 .collect(),
312 None => ns_buckets.into_iter().map(Bucket::Encode).collect(),
313 };
314
315 let bucket_trace =
316 tracer.map(|tracer| BucketTrace::new(tracer, view, buckets.len(), assembled_event));
317
318 buckets.into_par_iter().try_for_each_with(
319 send,
320 |send, bucket| -> Result<(), VidDisperseError> {
321 let dispersals = bucket.dispersals(¶ms)?;
322 let bucket_fragments = debug_span!("fragments")
323 .in_scope(|| fragments(view, epoch, payload_commitment, ¶ms, dispersals));
324 if let Some(trace) = &bucket_trace {
325 trace.bucket_assembled();
326 trace.send_starting();
327 }
328 for fragment in bucket_fragments {
329 let recipient = fragment.recipient_key.clone();
330 let message = Message {
331 sender: public_key.clone(),
332 message_type: MessageType::Consensus(ConsensusMessage::VidShareFragment(
333 SignedProposal::new(fragment, signature.clone()),
334 )),
335 };
336 if let Err(err) = send(&recipient, &message) {
337 if err.is_critical() {
338 return Err(err.into());
339 }
340 warn!(%err, "network error while sending vid share fragment");
341 }
342 }
343 Ok(())
344 },
345 )
346}
347
348enum Bucket {
350 Built(Vec<AvidmGf2NsDispersal>),
352 Encode(Vec<usize>),
354}
355
356impl Bucket {
357 fn dispersals<T: NodeType>(
358 self,
359 params: &AvidmGf2DisperseParams<T>,
360 ) -> Result<Vec<AvidmGf2NsDispersal>, VidDisperseError> {
361 match self {
362 Self::Built(dispersals) => Ok(dispersals),
363 Self::Encode(ns_indices) => debug_span!("ns_encode").in_scope(|| {
364 ns_indices
365 .into_iter()
366 .map(|ns_index| disperse_namespace(params, ns_index))
367 .collect()
368 }),
369 }
370 }
371}
372
373fn into_buckets(
376 dispersals: Vec<AvidmGf2NsDispersal>,
377 ns_buckets: &[Vec<usize>],
378) -> Vec<Vec<AvidmGf2NsDispersal>> {
379 let mut dispersals: Vec<_> = dispersals.into_iter().map(Some).collect();
380 ns_buckets
381 .iter()
382 .map(|bucket| {
383 bucket
384 .iter()
385 .map(|&ns_index| {
386 dispersals[ns_index]
387 .take()
388 .expect("buckets partition the namespaces")
389 })
390 .collect()
391 })
392 .collect()
393}
394
395struct BucketTrace<'a> {
399 tracer: &'a LeaderTracerHandle,
400 view: ViewNumber,
401 assembled_event: LeaderEvent,
403 buckets_left: AtomicUsize,
404 unicast_started: AtomicBool,
405}
406
407impl<'a> BucketTrace<'a> {
408 fn new(
410 tracer: &'a LeaderTracerHandle,
411 view: ViewNumber,
412 buckets: usize,
413 assembled_event: LeaderEvent,
414 ) -> Self {
415 Self {
416 tracer,
417 view,
418 assembled_event,
419 buckets_left: AtomicUsize::new(buckets),
420 unicast_started: AtomicBool::new(false),
421 }
422 }
423
424 fn bucket_assembled(&self) {
426 if self.buckets_left.fetch_sub(1, Ordering::AcqRel) == 1 {
427 self.record(self.assembled_event);
428 }
429 }
430
431 fn send_starting(&self) {
433 if !self.unicast_started.swap(true, Ordering::AcqRel) {
434 self.record(LeaderEvent::VidSharesUnicastStart);
435 }
436 }
437
438 fn record(&self, event: LeaderEvent) {
439 self.tracer.record(*self.view, event, now_ns());
440 }
441}
442
443fn disperse_namespace<T: NodeType>(
445 params: &AvidmGf2DisperseParams<T>,
446 ns_index: usize,
447) -> Result<AvidmGf2NsDispersal, VidDisperseError> {
448 AvidmGf2Scheme::ns_disperse_one(
449 ¶ms.param,
450 ¶ms.weights,
451 ¶ms.payload[params.ns_table[ns_index].clone()],
452 ns_index,
453 )
454 .wrap()
455 .map_err(VidDisperseError::Vid)
456}
457
458fn fragments<T: NodeType>(
460 view: ViewNumber,
461 epoch: EpochNumber,
462 payload_commitment: VidCommitment2,
463 params: &AvidmGf2DisperseParams<T>,
464 dispersals: Vec<AvidmGf2NsDispersal>,
465) -> Vec<AvidmGf2DisperseShareFragment<T>> {
466 let mut pieces = vec![Vec::new(); params.recipients.len()];
467 for dispersal in dispersals {
468 for (pieces, ns_share) in pieces.iter_mut().zip(dispersal.shares) {
469 pieces.push(AvidmGf2NamespacePiece {
470 ns_index: dispersal.ns_index,
471 ns_payload_byte_len: dispersal.payload_byte_len,
472 ns_commit: dispersal.commit,
473 ns_share,
474 });
475 }
476 }
477 params
478 .recipients
479 .iter()
480 .zip(pieces)
481 .map(|(recipient, pieces)| AvidmGf2DisperseShareFragment {
482 view_number: view,
483 epoch: Some(epoch),
484 target_epoch: Some(epoch),
485 payload_commitment,
486 recipient_key: recipient.clone(),
487 param: params.param.clone(),
488 num_namespaces: params.ns_table.len(),
489 namespaces: pieces,
490 })
491 .collect()
492}
493
494fn bucketize(ns_table: &[Range<usize>], threshold: usize) -> Vec<Vec<usize>> {
503 let mut buckets = Vec::new();
504 let mut current = Vec::new();
505 let mut current_bytes = 0usize;
506 for (ns_index, range) in ns_table.iter().enumerate() {
507 current.push(ns_index);
508 current_bytes += range.len();
509 if current_bytes >= threshold {
510 buckets.push(mem::take(&mut current));
511 current_bytes = 0;
512 }
513 }
514 if !current.is_empty() {
515 buckets.push(current);
516 }
517 buckets
518}
519
520#[derive(Debug, thiserror::Error)]
521pub enum VidDisperseError {
522 #[error("network error: {0}")]
523 Net(#[from] NetworkError),
524
525 #[error("vid error: {0}")]
526 Vid(#[source] anytrace::Error),
527
528 #[error("sign error: {0}")]
529 Sign(#[source] Box<dyn std::error::Error + Send + Sync>),
530}
531
532impl VidDisperseError {
533 pub fn is_critical(&self) -> bool {
534 match self {
535 Self::Net(e) => e.is_critical(),
536 Self::Vid(_) | Self::Sign(_) => false,
537 }
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use std::{
544 collections::{BTreeMap, BTreeSet},
545 mem,
546 ops::Range,
547 sync::{Arc, Mutex},
548 };
549
550 use alloy::primitives::U256;
551 use hotshot_example_types::{
552 block_types::{TestBlockPayload, TestMetadata},
553 node_stake::{TestNodeStakes, gen_node_lists},
554 node_types::TestTypes,
555 };
556 use hotshot_types::{
557 PeerConfig,
558 data::{
559 EpochNumber, VidCommitment, VidCommitment2, ViewNumber, vid_commitment,
560 vid_disperse::{
561 AvidmGf2DisperseParams, AvidmGf2DisperseShareFragment, vid_total_weight,
562 },
563 },
564 signature_key::BLSPubKey,
565 traits::signature_key::{SignatureKey, StakeTableEntryType},
566 };
567 use quickcheck::{TestResult, quickcheck};
568 use versions::NEW_PROTOCOL_VERSION;
569
570 use super::{
571 Bucket, BucketTrace, BuiltDispersal, Dispersal, VidDisperseError, VidDisperseRequest,
572 bucketize, disperse_payload, fragments, handle_vid_disperse_request, into_buckets,
573 };
574 use crate::{
575 leader_trace::LeaderEvent,
576 message::{ConsensusMessage, Message, MessageType, Validated},
577 network::NetworkError,
578 tests::common::utils::{RecordingTracer, mock_membership, ns_table},
579 };
580
581 fn ns_ranges(lens: &[u8]) -> Vec<Range<usize>> {
584 let mut start = 0;
585 lens.iter()
586 .map(|&len| {
587 let range = start..start + usize::from(len);
588 start += usize::from(len);
589 range
590 })
591 .collect()
592 }
593
594 fn bytes(indices: &[usize], ns_table: &[Range<usize>]) -> usize {
596 indices.iter().map(|&i| ns_table[i].len()).sum()
597 }
598
599 #[test]
601 fn bucket_trace_records_assembled_after_the_last_bucket() {
602 let tracer = Arc::new(RecordingTracer::default());
603 let handle = tracer.handle();
604 let trace = BucketTrace::new(&handle, ViewNumber::new(7), 3, LeaderEvent::VidFragmentsEnd);
605
606 trace.bucket_assembled();
607 trace.bucket_assembled();
608 assert_eq!(tracer.events(), []);
609 trace.bucket_assembled();
610 assert_eq!(tracer.events(), [(7, LeaderEvent::VidFragmentsEnd)]);
611 }
612
613 quickcheck! {
614 fn prop_buckets_partition(lens: Vec<u8>, threshold: u16) -> bool {
618 let ns_table = ns_ranges(&lens);
619 bucketize(&ns_table, threshold.into())
620 .into_iter()
621 .flatten()
622 .eq(0..ns_table.len())
623 }
624
625 fn prop_buckets_non_empty(lens: Vec<u8>, threshold: u16) -> bool {
627 let ns_table = ns_ranges(&lens);
628 bucketize(&ns_table, threshold.into())
629 .iter()
630 .all(|bucket| !bucket.is_empty())
631 }
632
633 fn prop_non_last_buckets_meet_threshold(lens: Vec<u8>, threshold: u16) -> bool {
636 let ns_table = ns_ranges(&lens);
637 let buckets = bucketize(&ns_table, threshold.into());
638 let non_last = buckets.len().saturating_sub(1);
639 buckets
640 .iter()
641 .take(non_last)
642 .all(|bucket| bytes(bucket, &ns_table) >= usize::from(threshold))
643 }
644
645 fn prop_non_last_buckets_are_minimal(lens: Vec<u8>, threshold: u16) -> TestResult {
650 if threshold == 0 {
651 return TestResult::discard();
652 }
653 let ns_table = ns_ranges(&lens);
654 let buckets = bucketize(&ns_table, threshold.into());
655 let non_last = buckets.len().saturating_sub(1);
656 TestResult::from_bool(buckets.iter().take(non_last).all(|bucket| {
657 bytes(&bucket[..bucket.len() - 1], &ns_table) < usize::from(threshold)
658 }))
659 }
660 }
661
662 fn stake_table() -> Vec<PeerConfig<TestTypes>> {
664 let stakes = (0..5).fold(TestNodeStakes::default(), |stakes, i| {
665 stakes.with_stake(i, U256::from(i + 1))
666 });
667 gen_node_lists(5, 0, &stakes).0
668 }
669
670 fn payload() -> (Vec<u8>, Vec<u8>) {
672 let payload = (0..6000).map(|i| (i % 251) as u8).collect();
673 (payload, ns_table(&[1000, 3000, 6000]))
674 }
675
676 #[test]
677 fn built_dispersal_commits_like_vid_commitment() {
678 let stake_table = stake_table();
679 let total_weight = vid_total_weight(stake_table.iter(), Some(EpochNumber::genesis()));
681 for ((payload, metadata), num_namespaces) in [(payload(), 3), ((vec![], ns_table(&[])), 1)]
682 {
683 let built =
684 BuiltDispersal::new(&stake_table, payload.clone().into(), &metadata).unwrap();
685 assert_eq!(built.dispersals.len(), num_namespaces);
686 assert_eq!(
687 VidCommitment::V2(built.payload_commitment().unwrap()),
688 vid_commitment(&payload, &metadata, total_weight, NEW_PROTOCOL_VERSION),
689 );
690 }
691 }
692
693 #[test]
694 fn built_fragments_equal_encoded_fragments() {
695 let (payload, metadata) = payload();
696 let built = BuiltDispersal::new(&stake_table(), payload.into(), &metadata).unwrap();
697 let commitment = built.payload_commitment().unwrap();
698 let ns_buckets = bucketize(&built.params.ns_table, 2500);
700 assert_eq!(ns_buckets, vec![vec![0, 1], vec![2]]);
701 let BuiltDispersal { params, dispersals } = built;
702 let fragments = |dispersals| {
703 fragments(
704 ViewNumber::new(7),
705 EpochNumber::new(3),
706 commitment,
707 ¶ms,
708 dispersals,
709 )
710 };
711 for (ns_bucket, built) in ns_buckets.iter().zip(into_buckets(dispersals, &ns_buckets)) {
712 let encoded = Bucket::Encode(ns_bucket.clone())
713 .dispersals(¶ms)
714 .unwrap();
715 assert_eq!(fragments(built), fragments(encoded));
716 }
717 }
718
719 fn large_payload() -> (Vec<u8>, Vec<u8>) {
722 const NS: u32 = 256 * 1024;
723 let payload = (0..3 * NS).map(|i| (i % 251) as u8).collect();
724 (payload, ns_table(&[NS, 2 * NS, 3 * NS]))
725 }
726
727 fn built_dispersal(
729 (payload, metadata): (Vec<u8>, Vec<u8>),
730 ) -> (BuiltDispersal<TestTypes>, VidCommitment2) {
731 let built = BuiltDispersal::new(&stake_table(), payload.into(), &metadata).unwrap();
732 let commitment = built.payload_commitment().unwrap();
733 (built, commitment)
734 }
735
736 fn disperse_recording<R>(
739 dispersal: Dispersal<TestTypes>,
740 commitment: VidCommitment2,
741 reply: R,
742 ) -> (
743 Result<(), VidDisperseError>,
744 Vec<AvidmGf2DisperseShareFragment<TestTypes>>,
745 )
746 where
747 R: Fn() -> Result<(), NetworkError> + Clone + Send,
748 {
749 let (leader, private_key) = BLSPubKey::generated_from_seed_indexed([1; 32], 0);
750 let sent = Arc::new(Mutex::new(Vec::new()));
751 let send = {
752 let (leader, sent) = (leader, Arc::clone(&sent));
753 move |recipient: &BLSPubKey, message: &Message<TestTypes, Validated>| {
754 let MessageType::Consensus(ConsensusMessage::VidShareFragment(fragment)) =
755 &message.message_type
756 else {
757 panic!("the disperser sends only VID share fragments");
758 };
759 assert_eq!(message.sender, leader);
760 assert!(leader.validate(&fragment.signature, commitment.as_ref()));
761 assert_eq!(&fragment.data.recipient_key, recipient);
762 sent.lock().unwrap().push(fragment.data.clone());
763 reply()
764 }
765 };
766 let result = disperse_payload(
767 dispersal,
768 ViewNumber::new(7),
769 EpochNumber::new(3),
770 commitment,
771 &leader,
772 &private_key,
773 None,
774 send,
775 );
776 let sent = mem::take(&mut *sent.lock().unwrap());
777 (result, sent)
778 }
779
780 fn by_recipient(
782 sent: Vec<AvidmGf2DisperseShareFragment<TestTypes>>,
783 ) -> BTreeMap<BLSPubKey, Vec<AvidmGf2DisperseShareFragment<TestTypes>>> {
784 let mut by_recipient: BTreeMap<_, Vec<_>> = BTreeMap::new();
785 for fragment in sent {
786 by_recipient
787 .entry(fragment.recipient_key)
788 .or_default()
789 .push(fragment);
790 }
791 for fragments in by_recipient.values_mut() {
792 fragments.sort_by_key(|fragment| fragment.namespaces[0].ns_index);
793 }
794 by_recipient
795 }
796
797 fn buckets(fragments: &[AvidmGf2DisperseShareFragment<TestTypes>]) -> Vec<Vec<usize>> {
799 fragments
800 .iter()
801 .map(|fragment| {
802 fragment
803 .namespaces
804 .iter()
805 .map(|piece| piece.ns_index)
806 .collect()
807 })
808 .collect()
809 }
810
811 #[test]
812 fn disperse_payload_sends_one_fragment_per_recipient_and_bucket() {
813 let (payload, metadata) = large_payload();
814 let params =
815 AvidmGf2DisperseParams::new(&stake_table(), payload.clone().into(), &metadata).unwrap();
816 let (built, commitment) = built_dispersal((payload, metadata));
817
818 let (result, sent) = disperse_recording(Dispersal::Built(built), commitment, || Ok(()));
819 result.unwrap();
820 let sent = by_recipient(sent);
821 let (result, encoded) =
822 disperse_recording(Dispersal::Encode(params), commitment, || Ok(()));
823 result.unwrap();
824 assert_eq!(
825 sent,
826 by_recipient(encoded),
827 "a built dispersal sends what encoding while sending does"
828 );
829
830 let recipients: BTreeSet<_> = stake_table()
831 .iter()
832 .map(|peer| peer.stake_table_entry.public_key())
833 .collect();
834 assert_eq!(sent.keys().cloned().collect::<BTreeSet<_>>(), recipients);
835 let first = buckets(sent.values().next().unwrap());
836 assert!(
837 first.concat().into_iter().eq(0..3),
838 "the buckets hold every namespace once"
839 );
840 for fragments in sent.values() {
841 assert_eq!(
842 buckets(fragments),
843 first,
844 "every recipient gets each bucket once"
845 );
846 assert!(
847 fragments
848 .iter()
849 .all(|fragment| fragment.num_namespaces == 3)
850 );
851 }
852 }
853
854 #[test]
856 fn disperse_payload_stops_at_a_critical_network_error() {
857 let (built, commitment) = built_dispersal(payload());
858 let (result, sent) = disperse_recording(Dispersal::Built(built), commitment, || {
859 Err(NetworkError::Cliquenet(
860 cliquenet::NetworkError::ChannelClosed,
861 ))
862 });
863 assert!(matches!(result, Err(VidDisperseError::Net(err)) if err.is_critical()));
864 assert_eq!(sent.len(), 1);
865 }
866
867 #[test]
868 fn disperse_payload_goes_on_after_a_non_critical_network_error() {
869 let (built, commitment) = built_dispersal(payload());
870 let (result, sent) = disperse_recording(Dispersal::Built(built), commitment, || {
871 Err(NetworkError::Cliquenet(cliquenet::NetworkError::Timeout))
872 });
873 result.unwrap();
874 assert_eq!(
875 sent.len(),
876 stake_table().len(),
877 "every recipient is still sent to"
878 );
879 }
880
881 #[tokio::test]
884 async fn a_disperse_request_sends_its_built_dispersal() {
885 let (leader, private_key) = BLSPubKey::generated_from_seed_indexed([1; 32], 0);
886 let (built, commitment) = built_dispersal(payload());
887 let request = VidDisperseRequest {
888 view: ViewNumber::new(7),
889 epoch: EpochNumber::new(0),
890 block: TestBlockPayload {
891 transactions: vec![],
892 },
893 metadata: TestMetadata {
894 num_transactions: 0,
895 },
896 payload_commitment: commitment,
897 };
898 let recipients = Arc::new(Mutex::new(BTreeSet::new()));
899 let send = {
900 let recipients = Arc::clone(&recipients);
901 move |recipient: &BLSPubKey, _: &Message<TestTypes, Validated>| {
902 recipients.lock().unwrap().insert(*recipient);
903 Ok(())
904 }
905 };
906 let disperse = |built| {
907 handle_vid_disperse_request(
908 mock_membership(),
909 send.clone(),
910 leader,
911 private_key.clone(),
912 request.clone(),
913 None,
914 built,
915 )
916 };
917
918 assert!(
919 disperse(None).is_err(),
920 "encoding needs the epoch's stake table"
921 );
922 disperse(Some(built)).unwrap();
923 let expected: BTreeSet<_> = stake_table()
924 .iter()
925 .map(|peer| peer.stake_table_entry.public_key())
926 .collect();
927 assert_eq!(*recipients.lock().unwrap(), expected);
928 }
929
930 fn disperse_traced(
932 dispersal: Dispersal<TestTypes>,
933 commitment: VidCommitment2,
934 ) -> Vec<LeaderEvent> {
935 let (leader, private_key) = BLSPubKey::generated_from_seed_indexed([1; 32], 0);
936 let tracer = Arc::new(RecordingTracer::default());
937 disperse_payload(
938 dispersal,
939 ViewNumber::new(7),
940 EpochNumber::new(3),
941 commitment,
942 &leader,
943 &private_key,
944 Some(&tracer.handle()),
945 |_: &BLSPubKey, _: &Message<TestTypes, Validated>| Ok(()),
946 )
947 .unwrap();
948 tracer
949 .events()
950 .into_iter()
951 .map(|(_, event)| event)
952 .collect()
953 }
954
955 fn position_of(events: &[LeaderEvent], event: LeaderEvent) -> usize {
957 let mut positions = events
958 .iter()
959 .enumerate()
960 .filter(|(_, e)| **e == event)
961 .map(|(i, _)| i);
962 let position = positions
963 .next()
964 .unwrap_or_else(|| panic!("{event:?} not recorded"));
965 assert!(positions.next().is_none(), "{event:?} recorded twice");
966 position
967 }
968
969 #[test]
971 fn disperse_built_records_fragment_events() {
972 let (built, commitment) = built_dispersal(large_payload());
973 let events = disperse_traced(Dispersal::Built(built), commitment);
974
975 position_of(&events, LeaderEvent::VidFragmentsEnd);
976 position_of(&events, LeaderEvent::VidSharesUnicastStart);
977 assert!(!events.contains(&LeaderEvent::NsDisperseEnd));
978 assert!(!events.contains(&LeaderEvent::VidFragmentsStart));
979 }
980
981 #[test]
983 fn disperse_encode_records_ns_disperse_events() {
984 let (payload, metadata) = large_payload();
985 let params =
986 AvidmGf2DisperseParams::new(&stake_table(), payload.clone().into(), &metadata).unwrap();
987 let (_, commitment) = built_dispersal((payload, metadata));
988 let events = disperse_traced(Dispersal::Encode(params), commitment);
989
990 position_of(&events, LeaderEvent::NsDisperseEnd);
991 position_of(&events, LeaderEvent::VidSharesUnicastStart);
992 assert!(!events.contains(&LeaderEvent::VidFragmentsEnd));
993 }
994
995 #[test]
997 fn disperse_single_bucket_assembles_before_it_sends() {
998 let (built, commitment) = built_dispersal(payload());
999 let events = disperse_traced(Dispersal::Built(built), commitment);
1000
1001 assert!(
1002 position_of(&events, LeaderEvent::VidFragmentsEnd)
1003 < position_of(&events, LeaderEvent::VidSharesUnicastStart)
1004 );
1005 }
1006
1007 #[tokio::test]
1010 async fn handle_disperse_request_records_each_paths_start() {
1011 let (leader, private_key) = BLSPubKey::generated_from_seed_indexed([1; 32], 0);
1012 let (built, commitment) = built_dispersal(payload());
1013 let request = VidDisperseRequest {
1014 view: ViewNumber::new(7),
1015 epoch: EpochNumber::new(0),
1016 block: TestBlockPayload {
1017 transactions: vec![],
1018 },
1019 metadata: TestMetadata {
1020 num_transactions: 0,
1021 },
1022 payload_commitment: commitment,
1023 };
1024 let disperse = |built| {
1025 let tracer = Arc::new(RecordingTracer::default());
1026 let result = handle_vid_disperse_request(
1027 mock_membership(),
1028 |_: &BLSPubKey, _: &Message<TestTypes, Validated>| Ok(()),
1029 leader,
1030 private_key.clone(),
1031 request.clone(),
1032 Some(tracer.handle()),
1033 built,
1034 );
1035 (result, tracer.events())
1036 };
1037
1038 let (result, events) = disperse(None);
1039 assert!(result.is_err(), "encoding needs the epoch's stake table");
1040 assert_eq!(events, [(7, LeaderEvent::NsDisperseStart)]);
1041
1042 let (result, events) = disperse(Some(built));
1043 result.unwrap();
1044 let events: Vec<_> = events.into_iter().map(|(_, event)| event).collect();
1045 assert_eq!(events[0], LeaderEvent::VidFragmentsStart);
1046 assert_eq!(events.last(), Some(&LeaderEvent::VidSharesUnicastEnd));
1047 assert!(!events.contains(&LeaderEvent::NsDisperseStart));
1048 }
1049}