Skip to main content

hotshot_new_protocol/vid/
disperse.rs

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
51/// A block's dispersal, computed by the block builder together with its commitment, so the
52/// disperser only signs and sends it.
53pub struct BuiltDispersal<T: NodeType> {
54    params: AvidmGf2DisperseParams<T>,
55    /// One per namespace, in namespace-table order.
56    dispersals: Vec<AvidmGf2NsDispersal>,
57}
58
59impl<T: NodeType> BuiltDispersal<T> {
60    /// Erasure-codes every namespace of an encoded payload for the nodes of `stake_table`.
61    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(&params, ns_index))
71            .collect::<Result<_, _>>()?;
72        Ok(Self { params, dispersals })
73    }
74
75    /// The payload commitment, folded from the namespaces' commits as
76    /// [`AvidmGf2Scheme::commit`] folds them.
77    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
89/// A block for [`disperse_payload`] to send.
90pub enum Dispersal<T: NodeType> {
91    /// Encoded by the block builder, so it is only signed and sent.
92    Built(BuiltDispersal<T>),
93    /// Encoded bucket by bucket while it is sent.
94    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    /// Disperses a block: sends `built` if the block builder computed it, else encodes the
142    /// payload first. The coordinator always hands over its block builder's dispersal; only the
143    /// bench node, which skips the block builder, and tests that feed `BlockBuilt` directly pass
144    /// `None`. A repeated request is dropped, `built` with it.
145    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/// Signs a block's dispersal and hands every recipient's fragment of each bucket of namespaces to
266/// `send`, which the disperser points at the network. A [`Dispersal::Encode`] is erasure-coded
267/// bucket by bucket first. The `block_build` bench in espresso-types passes a `send` that only
268/// serializes; the spans time each step.
269#[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    // Coalesce namespaces into size-balanced buckets, roughly one per core,
296    // but never below the minimum size, so a block of many tiny namespaces
297    // goes out as a few balanced messages per recipient instead of one tiny
298    // message each. Each bucket is one parallel unit and one message per
299    // recipient.
300    let ns_buckets: Vec<Vec<usize>> = {
301        // We want buckets to be at least 256 KiB, otherwise the per-message
302        // overhead is too large. Larger payloads are divided by the number
303        // of available threads to fully utilise rayon.
304        let threshold = params.payload.len().div_ceil(*NUM_THREADS).max(256 * 1024);
305        bucketize(&params.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(&params)?;
322            let bucket_fragments = debug_span!("fragments")
323                .in_scope(|| fragments(view, epoch, payload_commitment, &params, 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
348/// The namespace dispersals of one bucket, which go out as one fragment per recipient.
349enum Bucket {
350    /// Computed by the block builder.
351    Built(Vec<AvidmGf2NsDispersal>),
352    /// Still to encode, for these namespace indices.
353    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
373/// Moves each namespace's dispersal into its bucket. `dispersals` holds one per namespace, in
374/// namespace-table order, and `ns_buckets` partitions the namespace indices.
375fn 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
395/// Records the last bucket's `assembled_event` and the first bucket's `VidSharesUnicastStart`.
396/// With several buckets `assembled_event` can come after `VidSharesUnicastStart`.
397/// Built only when a tracer is registered.
398struct BucketTrace<'a> {
399    tracer: &'a LeaderTracerHandle,
400    view: ViewNumber,
401    /// `NsDisperseEnd` for `Dispersal::Encode`, `VidFragmentsEnd` for `Dispersal::Built`.
402    assembled_event: LeaderEvent,
403    buckets_left: AtomicUsize,
404    unicast_started: AtomicBool,
405}
406
407impl<'a> BucketTrace<'a> {
408    /// Trace for a dispersal of `buckets` buckets.
409    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    /// Called once per bucket after its fragments are assembled; records for the last one.
425    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    /// Called per bucket before its sends; records for the first one.
432    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
443/// Erasure-codes the namespace at `ns_index` of the payload in `params`.
444fn disperse_namespace<T: NodeType>(
445    params: &AvidmGf2DisperseParams<T>,
446    ns_index: usize,
447) -> Result<AvidmGf2NsDispersal, VidDisperseError> {
448    AvidmGf2Scheme::ns_disperse_one(
449        &params.param,
450        &params.weights,
451        &params.payload[params.ns_table[ns_index].clone()],
452        ns_index,
453    )
454    .wrap()
455    .map_err(VidDisperseError::Vid)
456}
457
458/// One fragment per recipient in `params`, carrying its shares of `dispersals`.
459fn 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
494/// Group namespace indices into buckets whose payload sizes are each at least
495/// `threshold` bytes (except possibly the last), coalescing small namespaces so
496/// the disperser sends one message per bucket rather than one per namespace.
497///
498/// A `threshold` of 0 puts every namespace in its own bucket (no coalescing); a
499/// threshold larger than the whole payload yields a single bucket. A namespace
500/// at least `threshold` bytes on its own seals its bucket immediately, so large
501/// namespaces stay separate while small ones accumulate.
502fn 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    /// Build a contiguous namespace table from per-namespace byte lengths.
582    /// Only the lengths matter to `bucketize`; the offsets are incidental.
583    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    /// Total payload bytes of the namespaces at `indices`.
595    fn bytes(indices: &[usize], ns_table: &[Range<usize>]) -> usize {
596        indices.iter().map(|&i| ns_table[i].len()).sum()
597    }
598
599    /// The assembled event is recorded once, after the last bucket assembles.
600    #[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        /// The buckets partition the namespace indices: every index appears
615        /// exactly once, contiguously, and in namespace-table order. This pins
616        /// completeness, disjointness, and ordering in a single property.
617        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        /// No bucket is ever empty.
626        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        /// Every bucket except the last reaches the threshold: small namespaces
634        /// are coalesced until the bucket is large enough.
635        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        /// Buckets are minimal: dropping a non-last bucket's final namespace
646        /// leaves it below the threshold, so the disperser seals as soon as it
647        /// reaches the cutoff rather than over-coalescing. (Vacuous at
648        /// `threshold == 0`, where every namespace is its own bucket.)
649        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    /// Nodes with unequal stakes, so their VID weights differ.
663    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    /// A payload with three namespaces of different sizes.
671    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        // How the builder weighted the commitment before it took it from the dispersal.
680        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        // The first two namespaces share a bucket.
699        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                &params,
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(&params)
714                .unwrap();
715            assert_eq!(fragments(built), fragments(encoded));
716        }
717    }
718
719    /// A payload with three namespaces of 256 KiB, so each fills a bucket of its own on a machine
720    /// with at least three cores, and fewer cores coalesce them.
721    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    /// The built dispersal of `payload` to [`stake_table`], with its commitment.
728    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    /// Runs [`disperse_payload`] with a `send` that checks each message is the leader's signed
737    /// fragment for its recipient, records the fragment, and returns `reply()`.
738    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    /// Each recipient's fragments, ordered by their first namespace.
781    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    /// The namespace indices each fragment carries: one list per bucket.
798    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    /// The small payload fits one bucket, so nothing else is sent after the first error.
855    #[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    /// The request names an epoch before the first, which has no stake table: encoding the
882    /// payload fails, so only sending the built dispersal can succeed.
883    #[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    /// Runs [`disperse_payload`] with a [`RecordingTracer`] and a `send` that accepts everything.
931    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    /// The position of `event` in `events`, which holds it exactly once.
956    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    /// Tracing a `Dispersal::Built` records the fragment events and no encode events.
970    #[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    /// Tracing a `Dispersal::Encode` records the encode events and no fragment events.
982    #[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    /// One bucket assembles before it sends.
996    #[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    /// The disperser records the start event of the path it takes: `built` and the failing
1008    /// encode of an epoch without a stake table both start a span.
1009    #[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}