Skip to main content

vid/avidm_gf2/
namespaced.rs

1//! This file implements the namespaced AvidmGf2 scheme.
2
3use std::ops::Range;
4
5use jf_merkle_tree::MerkleTreeScheme;
6use p3_maybe_rayon::prelude::*;
7use serde::{Deserialize, Serialize};
8
9use super::{AvidmGf2Commit, AvidmGf2Share};
10use crate::{
11    VidError, VidResult, VidScheme,
12    avidm_gf2::{AvidmGf2Scheme, MerkleTree},
13};
14
15/// Dummy struct for namespaced AvidmGf2 scheme
16pub struct NsAvidmGf2Scheme;
17
18/// Namespaced commitment type
19pub type NsAvidmGf2Commit = super::AvidmGf2Commit;
20/// Namespaced parameter type
21pub type NsAvidmGf2Param = super::AvidmGf2Param;
22
23/// VID Common data that needs to be broadcasted to all storage nodes
24#[derive(Clone, Debug, Hash, Serialize, Deserialize, Eq, PartialEq)]
25pub struct NsAvidmGf2Common {
26    /// The AvidmGf2 parameters
27    pub param: NsAvidmGf2Param,
28    /// The list of all namespace commitments
29    pub ns_commits: Vec<AvidmGf2Commit>,
30    /// The size of each namespace
31    pub ns_lens: Vec<usize>,
32}
33
34impl NsAvidmGf2Common {
35    /// Return the total payload byte length
36    pub fn payload_byte_len(&self) -> usize {
37        self.ns_lens.iter().sum()
38    }
39}
40
41/// Namespaced share for each storage node, contains one [`AvidmGf2Share`] for each namespace.
42#[derive(Clone, Debug, Hash, Serialize, Deserialize, Eq, PartialEq, Default)]
43pub struct NsAvidmGf2Share(pub(crate) Vec<AvidmGf2Share>);
44
45impl NsAvidmGf2Share {
46    /// Return the number of namespaces in this share
47    pub fn num_nss(&self) -> usize {
48        self.0.len()
49    }
50
51    /// Return the weight of this share
52    pub fn weight(&self) -> usize {
53        self.0.first().map_or(0, |share| share.weight())
54    }
55
56    /// Validate the share structure
57    pub fn validate(&self) -> bool {
58        let weight = self.weight();
59        self.0
60            .iter()
61            .all(|share| share.validate() && share.weight() == weight)
62    }
63
64    /// Check whether this share contains a given namespace
65    pub fn contains_ns(&self, ns_index: usize) -> bool {
66        ns_index < self.num_nss()
67    }
68
69    /// The per-namespace shares, in namespace order.
70    pub fn ns_shares(&self) -> &[AvidmGf2Share] {
71        &self.0
72    }
73
74    /// Return the inner share for a given namespace if there exists one.
75    pub fn inner_ns_share(&self, ns_index: usize) -> Option<AvidmGf2Share> {
76        self.0.get(ns_index).cloned()
77    }
78
79    /// The shard range this share covers, identical across all namespaces.
80    /// `None` if the share is empty or its namespaces disagree on the range.
81    pub fn range(&self) -> Option<&Range<usize>> {
82        let first = self.0.first()?.range();
83        self.0
84            .iter()
85            .all(|share| share.range() == first)
86            .then_some(first)
87    }
88}
89
90impl From<Vec<AvidmGf2Share>> for NsAvidmGf2Share {
91    fn from(ns_shares: Vec<AvidmGf2Share>) -> Self {
92        Self(ns_shares)
93    }
94}
95
96impl NsAvidmGf2Scheme {
97    /// Setup an instance for AVID-M scheme
98    pub fn setup(recovery_threshold: usize, total_weights: usize) -> VidResult<NsAvidmGf2Param> {
99        NsAvidmGf2Param::new(recovery_threshold, total_weights)
100    }
101
102    /// Commit to a payload given namespace table.
103    /// WARN: it assumes that the namespace table is well formed, i.e. ranges
104    /// are non-overlapping and cover the whole payload.
105    pub fn commit(
106        param: &NsAvidmGf2Param,
107        payload: &[u8],
108        ns_table: impl IntoIterator<Item = Range<usize>>,
109    ) -> VidResult<(NsAvidmGf2Commit, NsAvidmGf2Common)> {
110        let ns_table = ns_table.into_iter().collect::<Vec<_>>();
111        let ns_lens = ns_table.iter().map(|r| r.len()).collect::<Vec<_>>();
112        let ns_commits = ns_table
113            .into_par_iter()
114            .map(|ns_range| AvidmGf2Scheme::commit(param, &payload[ns_range]))
115            .collect::<Result<Vec<_>, _>>()?;
116        let common = NsAvidmGf2Common {
117            param: param.clone(),
118            ns_commits,
119            ns_lens,
120        };
121        let commit = Self::aggregate_commit(&common.ns_commits)?;
122        Ok((commit, common))
123    }
124
125    /// Aggregate per-namespace commitments into the top-level namespaced
126    /// commitment (the Merkle root over the namespace commits).
127    ///
128    /// Lets a caller that has already computed each namespace's commit (e.g. via
129    /// [`Self::ns_disperse_one`]) form the block commitment without re-encoding.
130    pub fn aggregate_commit(ns_commits: &[AvidmGf2Commit]) -> VidResult<NsAvidmGf2Commit> {
131        let commit = MerkleTree::from_elems(None, ns_commits.iter().map(|c| c.commit))
132            .map_err(|err| VidError::Internal(err.into()))?
133            .commitment();
134        Ok(NsAvidmGf2Commit { commit })
135    }
136
137    /// Check whether the namespaced commitment is consistent with the common data
138    pub fn is_consistent(commit: &NsAvidmGf2Commit, common: &NsAvidmGf2Common) -> bool {
139        Self::aggregate_commit(&common.ns_commits).is_ok_and(|c| c == *commit)
140    }
141
142    /// Disperse a payload according to a distribution table and a namespace
143    /// table.
144    /// WARN: it assumes that the namespace table is well formed, i.e. ranges
145    /// are non-overlapping and cover the whole payload.
146    pub fn ns_disperse(
147        param: &NsAvidmGf2Param,
148        distribution: &[u32],
149        payload: &[u8],
150        ns_table: impl IntoIterator<Item = Range<usize>>,
151    ) -> VidResult<(NsAvidmGf2Commit, NsAvidmGf2Common, Vec<NsAvidmGf2Share>)> {
152        let num_storage_nodes = distribution.len();
153        let ns_ranges: Vec<Range<usize>> = ns_table.into_iter().collect();
154        let ns_lens: Vec<usize> = ns_ranges.iter().map(|r| r.len()).collect();
155
156        // Per-namespace dispersals are independent; run them on rayon.
157        // Inner `AvidmGf2Scheme::disperse` also uses `par_iter` internally for
158        // leaf hashing and share assembly — rayon's work-stealing pool
159        // coordinates the nested parallelism.
160        let per_ns: Vec<(AvidmGf2Commit, Vec<AvidmGf2Share>)> = ns_ranges
161            .par_iter()
162            .map(|ns_range| {
163                AvidmGf2Scheme::disperse(param, distribution, &payload[ns_range.clone()])
164            })
165            .collect::<VidResult<Vec<_>>>()?;
166
167        let (ns_commits, disperses): (Vec<_>, Vec<_>) = per_ns.into_iter().unzip();
168
169        let common = NsAvidmGf2Common {
170            param: param.clone(),
171            ns_commits,
172            ns_lens,
173        };
174        let commit = Self::aggregate_commit(&common.ns_commits)?;
175        let mut shares = vec![NsAvidmGf2Share::default(); num_storage_nodes];
176        disperses.into_iter().for_each(|ns_disperse| {
177            shares
178                .iter_mut()
179                .zip(ns_disperse)
180                .for_each(|(share, ns_share)| share.0.push(ns_share))
181        });
182        Ok((commit, common, shares))
183    }
184
185    /// Disperse a single namespace's slice of the payload.
186    ///
187    /// `ns_payload` is the namespace's bytes and `ns_index` its position in the
188    /// namespace table.
189    pub fn ns_disperse_one(
190        param: &NsAvidmGf2Param,
191        distribution: &[u32],
192        ns_payload: &[u8],
193        ns_index: usize,
194    ) -> VidResult<NsDispersal> {
195        let payload_byte_len = ns_payload.len();
196        let (commit, shares) = AvidmGf2Scheme::disperse(param, distribution, ns_payload)?;
197        Ok(NsDispersal {
198            ns_index,
199            payload_byte_len,
200            commit,
201            shares,
202        })
203    }
204
205    /// Test-only: like [`Self::ns_disperse`] but commits to a non-codeword in
206    /// every namespace (see [`AvidmGf2Scheme::disperse_non_codeword`]). Every
207    /// returned share verifies against the returned common, yet no
208    /// threshold-covering subset recovers a payload that re-commits to the
209    /// returned commitment.
210    #[cfg(any(test, feature = "testing"))]
211    pub fn ns_disperse_non_codeword(
212        param: &NsAvidmGf2Param,
213        distribution: &[u32],
214        payload: &[u8],
215        ns_table: impl IntoIterator<Item = Range<usize>>,
216    ) -> VidResult<(NsAvidmGf2Commit, NsAvidmGf2Common, Vec<NsAvidmGf2Share>)> {
217        let num_storage_nodes = distribution.len();
218        let ns_ranges: Vec<Range<usize>> = ns_table.into_iter().collect();
219        let ns_lens: Vec<usize> = ns_ranges.iter().map(|r| r.len()).collect();
220        let per_ns: Vec<(AvidmGf2Commit, Vec<AvidmGf2Share>)> = ns_ranges
221            .iter()
222            .map(|ns_range| {
223                AvidmGf2Scheme::disperse_non_codeword(
224                    param,
225                    distribution,
226                    &payload[ns_range.clone()],
227                )
228            })
229            .collect::<VidResult<Vec<_>>>()?;
230        let (ns_commits, disperses): (Vec<_>, Vec<_>) = per_ns.into_iter().unzip();
231        let common = NsAvidmGf2Common {
232            param: param.clone(),
233            ns_commits,
234            ns_lens,
235        };
236        let commit = Self::aggregate_commit(&common.ns_commits)?;
237        let mut shares = vec![NsAvidmGf2Share::default(); num_storage_nodes];
238        disperses.into_iter().for_each(|ns_disperse| {
239            shares
240                .iter_mut()
241                .zip(ns_disperse)
242                .for_each(|(share, ns_share)| share.0.push(ns_share))
243        });
244        Ok((commit, common, shares))
245    }
246
247    /// Verify a namespaced share given already-verified common data.
248    ///
249    /// Besides each namespace's Merkle proofs, this checks that each
250    /// namespace's shards have the size its `ns_lens` entry disperses to. The
251    /// commitment binds `ns_commits` but not `ns_lens`, so without this a
252    /// share could declare any payload length and have it believed. With it,
253    /// each declared length is pinned to within `2 * recovery_threshold`
254    /// bytes of the one the shards were built from.
255    ///
256    /// That window is per namespace, and the disperser chooses how many
257    /// namespaces there are, so the misdeclared total adds up: a payload split
258    /// into namespaces shorter than a window can declare each as a single
259    /// byte and still verify. This rejects gross forgeries early, such as a
260    /// shrunken one-namespace block or an absurd length; it does not bound
261    /// the total. Exact binding needs a verifier that knows the payload's
262    /// namespace table, or a commitment that covers `ns_lens`.
263    ///
264    /// # Safety Contract
265    /// Caller MUST ensure `is_consistent(commit, common)` returned `true`
266    /// before calling this. Without that check, a malicious common could
267    /// make an invalid share appear valid.
268    pub fn verify_share_with_verified_common(
269        common: &NsAvidmGf2Common,
270        share: &NsAvidmGf2Share,
271    ) -> VidResult<crate::VerificationResult> {
272        common.param.validate()?;
273        if !(common.ns_commits.len() == common.ns_lens.len()
274            && common.ns_commits.len() == share.num_nss()
275            && share.validate())
276        {
277            return Err(VidError::InvalidShare);
278        }
279        // Per-namespace verifications are independent. `find_any` short-
280        // circuits on the first failing namespace and avoids allocating an
281        // intermediate `Vec<VerificationResult>`.
282        match common
283            .ns_commits
284            .par_iter()
285            .zip(common.ns_lens.par_iter())
286            .zip(share.0.par_iter())
287            .map(|((commit, &ns_len), content)| {
288                if !Self::ns_len_matches_shards(&common.param, ns_len, content) {
289                    return Ok(Err(()));
290                }
291                AvidmGf2Scheme::verify_share(&common.param, commit, content)
292            })
293            .find_any(|r| !matches!(r, Ok(Ok(()))))
294        {
295            None => Ok(Ok(())),
296            Some(Ok(v)) => Ok(v),
297            Some(Err(e)) => Err(e),
298        }
299    }
300
301    /// Verify a namespaced share
302    pub fn verify_share(
303        commit: &NsAvidmGf2Commit,
304        common: &NsAvidmGf2Common,
305        share: &NsAvidmGf2Share,
306    ) -> VidResult<crate::VerificationResult> {
307        if !Self::is_consistent(commit, common) {
308            return Ok(Err(()));
309        }
310        Self::verify_share_with_verified_common(common, share)
311    }
312
313    /// Recover the entire payload from enough share
314    pub fn recover(common: &NsAvidmGf2Common, shares: &[NsAvidmGf2Share]) -> VidResult<Vec<u8>> {
315        if shares.is_empty() {
316            return Err(VidError::InsufficientShares);
317        }
318        // Each `ns_recover` is independent: distinct decoder state, distinct
319        // output bytes. Run them on the rayon pool so multi-namespace blocks
320        // actually use more than one core.
321        let per_ns: Vec<Vec<u8>> = (0..common.ns_lens.len())
322            .into_par_iter()
323            .map(|ns_index| Self::ns_recover(common, ns_index, shares))
324            .collect::<VidResult<Vec<_>>>()?;
325        Ok(per_ns.concat())
326    }
327
328    /// Recover the payload for a given namespace.
329    /// Given namespace ID should be valid for all shares, i.e. `ns_commits` and `content` have
330    /// at least `ns_index` elements for all shares.
331    pub fn ns_recover(
332        common: &NsAvidmGf2Common,
333        ns_index: usize,
334        shares: &[NsAvidmGf2Share],
335    ) -> VidResult<Vec<u8>> {
336        common.param.validate()?;
337        if shares.is_empty() {
338            return Err(VidError::InsufficientShares);
339        }
340        let (Some(ns_commit), Some(&ns_len)) = (
341            common.ns_commits.get(ns_index),
342            common.ns_lens.get(ns_index),
343        ) else {
344            return Err(VidError::IndexOutOfBound);
345        };
346        if !shares.iter().all(|share| share.contains_ns(ns_index)) {
347            return Err(VidError::IndexOutOfBound);
348        }
349        let shares: Vec<_> = shares
350            .iter()
351            .filter_map(|share| share.inner_ns_share(ns_index))
352            .collect();
353        // The commitment does not bind `ns_lens`, so shards sized for another
354        // namespace length must be rejected before decoding.
355        if !shares.iter().all(|share| {
356            share.payload.is_empty() || Self::ns_len_matches_shards(&common.param, ns_len, share)
357        }) {
358            return Err(VidError::InvalidShare);
359        }
360        AvidmGf2Scheme::recover(&common.param, ns_commit, &shares)
361    }
362
363    /// Whether `content`'s shards have the size an `ns_len`-byte namespace
364    /// disperses to under `param`.
365    fn ns_len_matches_shards(
366        param: &NsAvidmGf2Param,
367        ns_len: usize,
368        content: &AvidmGf2Share,
369    ) -> bool {
370        content.shard_bytes().is_some_and(|shard_bytes| {
371            AvidmGf2Scheme::shard_bytes(ns_len, param.recovery_threshold) == Some(shard_bytes)
372        })
373    }
374}
375
376/// A namespaced dispersal.
377#[derive(Clone, Debug)]
378#[non_exhaustive]
379pub struct NsDispersal {
380    /// Index of this namespace in the namespace table.
381    pub ns_index: usize,
382    /// Byte length of this namespace's slice of the payload.
383    pub payload_byte_len: usize,
384    /// Commitment to this namespace's shards.
385    pub commit: AvidmGf2Commit,
386    /// One share per storage node, in distribution order.
387    pub shares: Vec<AvidmGf2Share>,
388}
389
390/// Unit tests
391#[cfg(test)]
392pub mod tests {
393    use rand::{RngCore, SeedableRng, rngs::StdRng, seq::SliceRandom};
394
395    use crate::{
396        VidError,
397        avidm_gf2::{AvidmGf2Param, namespaced::NsAvidmGf2Scheme},
398    };
399
400    fn disperse_with_payload(
401        payload: &[u8],
402    ) -> (
403        crate::avidm_gf2::namespaced::NsAvidmGf2Commit,
404        crate::avidm_gf2::namespaced::NsAvidmGf2Common,
405        Vec<crate::avidm_gf2::namespaced::NsAvidmGf2Share>,
406    ) {
407        let num_storage_nodes = 9;
408        let ns_table = [(0usize..15), (15..48)];
409
410        let mut rng = jf_utils::test_rng();
411        let weights: Vec<u32> = (0..num_storage_nodes)
412            .map(|_| rng.next_u32() % 5 + 1)
413            .collect();
414        let total_weights: u32 = weights.iter().sum();
415        let recovery_threshold = total_weights.div_ceil(3) as usize;
416        let params = NsAvidmGf2Scheme::setup(recovery_threshold, total_weights as usize).unwrap();
417
418        NsAvidmGf2Scheme::ns_disperse(&params, &weights, payload, ns_table.iter().cloned()).unwrap()
419    }
420
421    fn setup_test_data() -> (
422        crate::avidm_gf2::namespaced::NsAvidmGf2Commit,
423        crate::avidm_gf2::namespaced::NsAvidmGf2Common,
424        Vec<crate::avidm_gf2::namespaced::NsAvidmGf2Share>,
425    ) {
426        let payload: Vec<u8> = (0u8..48).collect();
427        disperse_with_payload(&payload)
428    }
429
430    #[test]
431    fn verify_share_with_verified_common_accepts_valid() {
432        let (commit, common, shares) = setup_test_data();
433        assert!(NsAvidmGf2Scheme::is_consistent(&commit, &common));
434        for share in &shares {
435            assert!(
436                NsAvidmGf2Scheme::verify_share_with_verified_common(&common, share)
437                    .is_ok_and(|r| r.is_ok())
438            );
439        }
440    }
441
442    #[test]
443    fn verify_share_with_verified_common_rejects_tampered_share() {
444        let (_commit, common, shares) = setup_test_data();
445        // Create a tampered share by removing one namespace entry
446        let mut tampered = shares[0].clone();
447        tampered.0.pop();
448        assert!(NsAvidmGf2Scheme::verify_share_with_verified_common(&common, &tampered).is_err());
449
450        // Create a tampered share by dispersing a different payload and swapping
451        let (_commit2, _common2, shares2) = disperse_with_payload(&[0xAB; 48]);
452        let mut mixed = shares[0].clone();
453        mixed.0[0] = shares2[0].0[0].clone();
454        assert!(
455            NsAvidmGf2Scheme::verify_share_with_verified_common(&common, &mixed)
456                .is_ok_and(|r| r.is_err())
457        );
458    }
459
460    /// Disperse `payload` under a known `recovery_threshold`, so a test knows
461    /// the shard-size window the `ns_lens` check tolerates.
462    fn disperse_with_threshold(
463        recovery_threshold: usize,
464        payload: &[u8],
465        ns_table: &[std::ops::Range<usize>],
466    ) -> (
467        crate::avidm_gf2::namespaced::NsAvidmGf2Commit,
468        crate::avidm_gf2::namespaced::NsAvidmGf2Common,
469        Vec<crate::avidm_gf2::namespaced::NsAvidmGf2Share>,
470    ) {
471        let total_weights = 3 * recovery_threshold;
472        let weights = vec![1u32; total_weights];
473        let params = NsAvidmGf2Scheme::setup(recovery_threshold, total_weights).unwrap();
474        NsAvidmGf2Scheme::ns_disperse(&params, &weights, payload, ns_table.iter().cloned()).unwrap()
475    }
476
477    /// The commitment does not bind `ns_lens`, so a share must not verify
478    /// against a common whose `ns_lens` its shards were not dispersed from.
479    /// The shard size pins each length to a `2 * recovery_threshold`-byte
480    /// window: lengths inside it still verify, lengths outside do not, and
481    /// a mismatch is a verification failure rather than an error or a panic.
482    #[test]
483    fn verify_share_rejects_ns_len_outside_shard_window() {
484        let k = 4;
485        let payload = vec![7u8; 100];
486        let (commit, common, shares) = disperse_with_threshold(k, &payload, &[0..30, 30..100]);
487        assert!(NsAvidmGf2Scheme::is_consistent(&commit, &common));
488        // 30 bytes plus the pad byte need ceil(31 / 4) = 8 bytes per shard,
489        // already even, so those shards fit any length in 24..32.
490        assert_eq!(shares[0].0[0].shard_bytes(), Some(8));
491        let window = (8 - 2) * k..8 * k;
492
493        for ns_len in [window.start, window.end - 1] {
494            let mut tampered = common.clone();
495            tampered.ns_lens[0] = ns_len;
496            for share in &shares {
497                assert_eq!(
498                    NsAvidmGf2Scheme::verify_share_with_verified_common(&tampered, share).unwrap(),
499                    Ok(()),
500                    "{ns_len}"
501                );
502            }
503        }
504        for ns_len in [0, window.start - 1, window.end, 100, usize::MAX] {
505            let mut tampered = common.clone();
506            tampered.ns_lens[0] = ns_len;
507            for share in &shares {
508                assert_eq!(
509                    NsAvidmGf2Scheme::verify_share_with_verified_common(&tampered, share).unwrap(),
510                    Err(()),
511                    "{ns_len}"
512                );
513            }
514        }
515
516        // The forgery this catches: shrink the second namespace by a full
517        // window, so the share reports a smaller block than was committed to.
518        // The same shrink spread over several namespaces is not caught, see
519        // `verify_share_accepts_ns_lens_shrunk_inside_every_window`.
520        let mut shrunk = common.clone();
521        shrunk.ns_lens[1] -= 2 * k;
522        assert!(shrunk.payload_byte_len() < common.payload_byte_len());
523        for share in &shares {
524            assert_eq!(
525                NsAvidmGf2Scheme::verify_share_with_verified_common(&shrunk, share).unwrap(),
526                Err(())
527            );
528        }
529    }
530
531    /// The window is per namespace and the disperser chooses how many there
532    /// are, so the misdeclared total adds up: a payload split into namespaces
533    /// shorter than a window can declare every one of them as a single byte
534    /// and still verify. This pins that residual gap; a check that binds
535    /// `ns_lens` exactly should flip it.
536    #[test]
537    fn verify_share_accepts_ns_lens_shrunk_inside_every_window() {
538        let k = 4;
539        // Lengths 1..=7 all disperse to 2-byte shards under k = 4.
540        let ns_len = 2 * k - 1;
541        let num_ns = 10;
542        let payload = vec![7u8; num_ns * ns_len];
543        let ns_table: Vec<_> = (0..num_ns).map(|i| i * ns_len..(i + 1) * ns_len).collect();
544        let (commit, common, shares) = disperse_with_threshold(k, &payload, &ns_table);
545        for share in &shares {
546            assert_eq!(share.0[0].shard_bytes(), Some(2));
547        }
548
549        let mut shrunk = common.clone();
550        shrunk.ns_lens = vec![1; num_ns];
551        assert!(NsAvidmGf2Scheme::is_consistent(&commit, &shrunk));
552        assert_eq!(shrunk.payload_byte_len(), num_ns);
553        for share in &shares {
554            assert_eq!(
555                NsAvidmGf2Scheme::verify_share_with_verified_common(&shrunk, share).unwrap(),
556                Ok(())
557            );
558        }
559    }
560
561    #[test]
562    fn composition_equivalence() {
563        let (commit, common, shares) = setup_test_data();
564        for share in &shares {
565            let full_result = NsAvidmGf2Scheme::verify_share(&commit, &common, share)
566                .unwrap()
567                .is_ok();
568            let composed_result = NsAvidmGf2Scheme::is_consistent(&commit, &common)
569                && NsAvidmGf2Scheme::verify_share_with_verified_common(&common, share)
570                    .unwrap()
571                    .is_ok();
572            assert_eq!(full_result, composed_result);
573        }
574    }
575
576    #[test]
577    fn is_consistent_rejects_tampered_commit() {
578        let (commit, common, _shares) = setup_test_data();
579        // Use commit from a different dispersal
580        let (different_commit, ..) = disperse_with_payload(&[0xCD; 48]);
581        // Verify original is consistent
582        assert!(NsAvidmGf2Scheme::is_consistent(&commit, &common));
583        // Verify different commit is inconsistent with original common
584        assert!(!NsAvidmGf2Scheme::is_consistent(&different_commit, &common));
585    }
586
587    #[test]
588    fn is_consistent_rejects_tampered_common() {
589        let (commit, common, _shares) = setup_test_data();
590        // Swap in ns_commits from a different dispersal
591        let (_, different_common, _) = disperse_with_payload(&[0xCD; 48]);
592        let mut tampered_common = common;
593        tampered_common.ns_commits = different_common.ns_commits;
594        assert!(!NsAvidmGf2Scheme::is_consistent(&commit, &tampered_common));
595    }
596
597    #[test]
598    fn round_trip() {
599        // play with these items
600        let num_storage_nodes = 9;
601        let ns_lens = [15, 33];
602        let ns_table = [(0usize..15), (15..48)];
603        let payload_byte_len = ns_lens.iter().sum();
604
605        let mut rng = jf_utils::test_rng();
606
607        // more items as a function of the above
608        let weights: Vec<u32> = (0..num_storage_nodes)
609            .map(|_| rng.next_u32() % 5 + 1)
610            .collect();
611        let total_weights: u32 = weights.iter().sum();
612        let recovery_threshold = total_weights.div_ceil(3) as usize;
613        let params = NsAvidmGf2Scheme::setup(recovery_threshold, total_weights as usize).unwrap();
614
615        println!(
616            "recovery_threshold:: {recovery_threshold} num_storage_nodes: {num_storage_nodes} \
617             payload_byte_len: {payload_byte_len}"
618        );
619        println!("weights: {weights:?}");
620
621        let payload = {
622            let mut bytes_random = vec![0u8; payload_byte_len];
623            rng.fill_bytes(&mut bytes_random);
624            bytes_random
625        };
626
627        let (commit, common, mut shares) =
628            NsAvidmGf2Scheme::ns_disperse(&params, &weights, &payload, ns_table.iter().cloned())
629                .unwrap();
630
631        assert_eq!(shares.len(), num_storage_nodes);
632
633        assert_eq!(
634            commit,
635            NsAvidmGf2Scheme::commit(&params, &payload, ns_table.iter().cloned())
636                .unwrap()
637                .0
638        );
639
640        // verify shares
641        shares.iter().for_each(|share| {
642            assert!(
643                NsAvidmGf2Scheme::verify_share(&commit, &common, share).is_ok_and(|r| r.is_ok())
644            )
645        });
646
647        // test payload recovery on a random subset of shares
648        shares.shuffle(&mut rng);
649        let mut cumulated_weights = 0;
650        let mut cut_index = 0;
651        while cumulated_weights <= recovery_threshold {
652            cumulated_weights += shares[cut_index].weight();
653            cut_index += 1;
654        }
655        let ns0_payload_recovered =
656            NsAvidmGf2Scheme::ns_recover(&common, 0, &shares[..cut_index]).unwrap();
657        assert_eq!(ns0_payload_recovered[..], payload[ns_table[0].clone()]);
658        let ns1_payload_recovered =
659            NsAvidmGf2Scheme::ns_recover(&common, 1, &shares[..cut_index]).unwrap();
660        assert_eq!(ns1_payload_recovered[..], payload[ns_table[1].clone()]);
661        let payload_recovered = NsAvidmGf2Scheme::recover(&common, &shares[..cut_index]).unwrap();
662        assert_eq!(payload_recovered, payload);
663    }
664
665    proptest::proptest! {
666        #[test]
667        fn any_threshold_covering_subset_recovers_the_payload(
668            weights in proptest::collection::vec(1u32..=5, 1..=9),
669            payload in proptest::collection::vec(proptest::num::u8::ANY, 0..2048),
670            ns_ends in proptest::collection::vec(proptest::num::usize::ANY, 0..4),
671            shuffle_seed: u64,
672        ) {
673            let total_weights: u32 = weights.iter().sum();
674            let recovery_threshold = total_weights.div_ceil(3) as usize;
675            let params =
676                NsAvidmGf2Scheme::setup(recovery_threshold, total_weights as usize).unwrap();
677            let mut ends: Vec<usize> = ns_ends
678                .iter()
679                .map(|end| end % (payload.len() + 1))
680                .chain([payload.len()])
681                .collect();
682            ends.sort_unstable();
683            ends.dedup();
684            let ns_table: Vec<_> = std::iter::once(0)
685                .chain(ends.iter().copied())
686                .zip(ends.iter().copied())
687                .map(|(start, end)| start..end)
688                .filter(|range| !range.is_empty() || payload.is_empty())
689                .collect();
690
691            let (_commit, common, mut shares) =
692                NsAvidmGf2Scheme::ns_disperse(&params, &weights, &payload, ns_table.clone())
693                    .unwrap();
694            shares.shuffle(&mut StdRng::seed_from_u64(shuffle_seed));
695            let mut weight = 0;
696            let subset: Vec<_> = shares
697                .into_iter()
698                .take_while(|share| {
699                    let needed = weight < recovery_threshold;
700                    weight += share.weight();
701                    needed
702                })
703                .collect();
704
705            proptest::prop_assert_eq!(NsAvidmGf2Scheme::recover(&common, &subset).unwrap(), payload.clone());
706            for (ns_index, range) in ns_table.into_iter().enumerate() {
707                proptest::prop_assert_eq!(
708                    NsAvidmGf2Scheme::ns_recover(&common, ns_index, &subset).unwrap(),
709                    payload[range].to_vec()
710                );
711            }
712        }
713    }
714
715    #[test]
716    fn ns_recover_rejects_ns_len_the_shards_were_not_dispersed_from() {
717        let k = 4;
718        let (_commit, common, shares) = disperse_with_threshold(k, &[7u8; 100], &[0..30, 30..100]);
719        let mut shrunk = common.clone();
720        shrunk.ns_lens[1] -= 2 * k;
721        assert!(matches!(
722            NsAvidmGf2Scheme::ns_recover(&shrunk, 1, &shares),
723            Err(VidError::InvalidShare)
724        ));
725    }
726
727    #[test]
728    fn ns_recover_rejects_common_with_fewer_commits_than_lens() {
729        let (_commit, mut common, shares) = setup_test_data();
730        common.ns_commits.pop();
731        assert!(matches!(
732            NsAvidmGf2Scheme::ns_recover(&common, 1, &shares),
733            Err(VidError::IndexOutOfBound)
734        ));
735    }
736
737    #[test]
738    fn forged_common_param_is_rejected() {
739        let (_commit, common, shares) = setup_test_data();
740        for (recovery_threshold, total_weights) in [(0, 4), (1, 1 << 16)] {
741            let mut forged = common.clone();
742            forged.param = AvidmGf2Param {
743                total_weights,
744                recovery_threshold,
745            };
746            assert!(
747                matches!(
748                    NsAvidmGf2Scheme::verify_share_with_verified_common(&forged, &shares[0]),
749                    Err(VidError::InvalidParam)
750                ),
751                "verify_share_with_verified_common {:?}",
752                forged.param
753            );
754            assert!(
755                matches!(
756                    NsAvidmGf2Scheme::ns_recover(&forged, 0, &shares),
757                    Err(VidError::InvalidParam)
758                ),
759                "ns_recover {:?}",
760                forged.param
761            );
762        }
763    }
764
765    /// A caller that encodes each namespace with `ns_disperse_one` and folds the
766    /// per-namespace commits with `aggregate_commit` must get the same
767    /// commitment as the monolithic `commit`.
768    #[test]
769    fn aggregate_commit_matches_commit() {
770        let num_storage_nodes = 9;
771        let ns_table = [(0usize..15), (15..48), (48..49)];
772        let weights = vec![1u32; num_storage_nodes];
773        let params = NsAvidmGf2Scheme::setup(3, num_storage_nodes).unwrap();
774        let payload: Vec<u8> = (0..49).map(|i| i as u8).collect();
775
776        let ns_commits: Vec<_> = ns_table
777            .iter()
778            .enumerate()
779            .map(|(ns_index, range)| {
780                NsAvidmGf2Scheme::ns_disperse_one(
781                    &params,
782                    &weights,
783                    &payload[range.clone()],
784                    ns_index,
785                )
786                .unwrap()
787                .commit
788            })
789            .collect();
790        let aggregated = NsAvidmGf2Scheme::aggregate_commit(&ns_commits).unwrap();
791
792        let (expected, _) =
793            NsAvidmGf2Scheme::commit(&params, &payload, ns_table.iter().cloned()).unwrap();
794        assert_eq!(aggregated, expected);
795    }
796}