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    /// Return the inner share for a given namespace if there exists one.
70    pub fn inner_ns_share(&self, ns_index: usize) -> Option<AvidmGf2Share> {
71        self.0.get(ns_index).cloned()
72    }
73
74    /// The shard range this share covers, identical across all namespaces.
75    /// `None` if the share is empty or its namespaces disagree on the range.
76    pub fn range(&self) -> Option<&Range<usize>> {
77        let first = self.0.first()?.range();
78        self.0
79            .iter()
80            .all(|share| share.range() == first)
81            .then_some(first)
82    }
83}
84
85impl From<Vec<AvidmGf2Share>> for NsAvidmGf2Share {
86    fn from(ns_shares: Vec<AvidmGf2Share>) -> Self {
87        Self(ns_shares)
88    }
89}
90
91impl NsAvidmGf2Scheme {
92    /// Setup an instance for AVID-M scheme
93    pub fn setup(recovery_threshold: usize, total_weights: usize) -> VidResult<NsAvidmGf2Param> {
94        NsAvidmGf2Param::new(recovery_threshold, total_weights)
95    }
96
97    /// Commit to a payload given namespace table.
98    /// WARN: it assumes that the namespace table is well formed, i.e. ranges
99    /// are non-overlapping and cover the whole payload.
100    pub fn commit(
101        param: &NsAvidmGf2Param,
102        payload: &[u8],
103        ns_table: impl IntoIterator<Item = Range<usize>>,
104    ) -> VidResult<(NsAvidmGf2Commit, NsAvidmGf2Common)> {
105        let ns_table = ns_table.into_iter().collect::<Vec<_>>();
106        let ns_lens = ns_table.iter().map(|r| r.len()).collect::<Vec<_>>();
107        let ns_commits = ns_table
108            .into_iter()
109            .map(|ns_range| AvidmGf2Scheme::commit(param, &payload[ns_range]))
110            .collect::<Result<Vec<_>, _>>()?;
111        let common = NsAvidmGf2Common {
112            param: param.clone(),
113            ns_commits,
114            ns_lens,
115        };
116        let commit = MerkleTree::from_elems(None, common.ns_commits.iter().map(|c| c.commit))
117            .map_err(|err| VidError::Internal(err.into()))?
118            .commitment();
119        Ok((NsAvidmGf2Commit { commit }, common))
120    }
121
122    /// Check whether the namespaced commitment is consistent with the common data
123    pub fn is_consistent(commit: &NsAvidmGf2Commit, common: &NsAvidmGf2Common) -> bool {
124        let Ok(mt) =
125            MerkleTree::from_elems(None, common.ns_commits.iter().map(|commit| commit.commit))
126        else {
127            return false;
128        };
129        commit.commit == mt.commitment()
130    }
131
132    /// Disperse a payload according to a distribution table and a namespace
133    /// table.
134    /// WARN: it assumes that the namespace table is well formed, i.e. ranges
135    /// are non-overlapping and cover the whole payload.
136    pub fn ns_disperse(
137        param: &NsAvidmGf2Param,
138        distribution: &[u32],
139        payload: &[u8],
140        ns_table: impl IntoIterator<Item = Range<usize>>,
141    ) -> VidResult<(NsAvidmGf2Commit, NsAvidmGf2Common, Vec<NsAvidmGf2Share>)> {
142        let num_storage_nodes = distribution.len();
143        let ns_ranges: Vec<Range<usize>> = ns_table.into_iter().collect();
144        let ns_lens: Vec<usize> = ns_ranges.iter().map(|r| r.len()).collect();
145
146        // Per-namespace dispersals are independent; run them on rayon.
147        // Inner `AvidmGf2Scheme::disperse` also uses `par_iter` internally for
148        // leaf hashing and share assembly — rayon's work-stealing pool
149        // coordinates the nested parallelism.
150        let per_ns: Vec<(AvidmGf2Commit, Vec<AvidmGf2Share>)> = ns_ranges
151            .par_iter()
152            .map(|ns_range| {
153                AvidmGf2Scheme::disperse(param, distribution, &payload[ns_range.clone()])
154            })
155            .collect::<VidResult<Vec<_>>>()?;
156
157        let (ns_commits, disperses): (Vec<_>, Vec<_>) = per_ns.into_iter().unzip();
158
159        let common = NsAvidmGf2Common {
160            param: param.clone(),
161            ns_commits,
162            ns_lens,
163        };
164        let commit = NsAvidmGf2Commit {
165            commit: MerkleTree::from_elems(None, common.ns_commits.iter().map(|c| c.commit))
166                .map_err(|err| VidError::Internal(err.into()))?
167                .commitment(),
168        };
169        let mut shares = vec![NsAvidmGf2Share::default(); num_storage_nodes];
170        disperses.into_iter().for_each(|ns_disperse| {
171            shares
172                .iter_mut()
173                .zip(ns_disperse)
174                .for_each(|(share, ns_share)| share.0.push(ns_share))
175        });
176        Ok((commit, common, shares))
177    }
178
179    /// Disperse a single namespace's slice of the payload.
180    ///
181    /// `ns_payload` is the namespace's bytes and `ns_index` its position in the
182    /// namespace table.
183    pub fn ns_disperse_one(
184        param: &NsAvidmGf2Param,
185        distribution: &[u32],
186        ns_payload: &[u8],
187        ns_index: usize,
188    ) -> VidResult<NsDispersal> {
189        let payload_byte_len = ns_payload.len();
190        let (commit, shares) = AvidmGf2Scheme::disperse(param, distribution, ns_payload)?;
191        Ok(NsDispersal {
192            ns_index,
193            payload_byte_len,
194            commit,
195            shares,
196        })
197    }
198
199    /// Test-only: like [`Self::ns_disperse`] but commits to a non-codeword in
200    /// every namespace (see [`AvidmGf2Scheme::disperse_non_codeword`]). Every
201    /// returned share verifies against the returned common, yet no
202    /// threshold-covering subset recovers a payload that re-commits to the
203    /// returned commitment.
204    #[cfg(any(test, feature = "testing"))]
205    pub fn ns_disperse_non_codeword(
206        param: &NsAvidmGf2Param,
207        distribution: &[u32],
208        payload: &[u8],
209        ns_table: impl IntoIterator<Item = Range<usize>>,
210    ) -> VidResult<(NsAvidmGf2Commit, NsAvidmGf2Common, Vec<NsAvidmGf2Share>)> {
211        let num_storage_nodes = distribution.len();
212        let ns_ranges: Vec<Range<usize>> = ns_table.into_iter().collect();
213        let ns_lens: Vec<usize> = ns_ranges.iter().map(|r| r.len()).collect();
214        let per_ns: Vec<(AvidmGf2Commit, Vec<AvidmGf2Share>)> = ns_ranges
215            .iter()
216            .map(|ns_range| {
217                AvidmGf2Scheme::disperse_non_codeword(
218                    param,
219                    distribution,
220                    &payload[ns_range.clone()],
221                )
222            })
223            .collect::<VidResult<Vec<_>>>()?;
224        let (ns_commits, disperses): (Vec<_>, Vec<_>) = per_ns.into_iter().unzip();
225        let common = NsAvidmGf2Common {
226            param: param.clone(),
227            ns_commits,
228            ns_lens,
229        };
230        let commit = NsAvidmGf2Commit {
231            commit: MerkleTree::from_elems(None, common.ns_commits.iter().map(|c| c.commit))
232                .map_err(|err| VidError::Internal(err.into()))?
233                .commitment(),
234        };
235        let mut shares = vec![NsAvidmGf2Share::default(); num_storage_nodes];
236        disperses.into_iter().for_each(|ns_disperse| {
237            shares
238                .iter_mut()
239                .zip(ns_disperse)
240                .for_each(|(share, ns_share)| share.0.push(ns_share))
241        });
242        Ok((commit, common, shares))
243    }
244
245    /// Verify a namespaced share given already-verified common data.
246    ///
247    /// # Safety Contract
248    /// Caller MUST ensure `is_consistent(commit, common)` returned `true`
249    /// before calling this. Without that check, a malicious common could
250    /// make an invalid share appear valid.
251    pub fn verify_share_with_verified_common(
252        common: &NsAvidmGf2Common,
253        share: &NsAvidmGf2Share,
254    ) -> VidResult<crate::VerificationResult> {
255        if !(common.ns_commits.len() == common.ns_lens.len()
256            && common.ns_commits.len() == share.num_nss()
257            && share.validate())
258        {
259            return Err(VidError::InvalidShare);
260        }
261        // Per-namespace verifications are independent. `find_any` short-
262        // circuits on the first failing namespace and avoids allocating an
263        // intermediate `Vec<VerificationResult>`.
264        match common
265            .ns_commits
266            .par_iter()
267            .zip(share.0.par_iter())
268            .map(|(commit, content)| AvidmGf2Scheme::verify_share(&common.param, commit, content))
269            .find_any(|r| !matches!(r, Ok(Ok(()))))
270        {
271            None => Ok(Ok(())),
272            Some(Ok(v)) => Ok(v),
273            Some(Err(e)) => Err(e),
274        }
275    }
276
277    /// Verify a namespaced share
278    pub fn verify_share(
279        commit: &NsAvidmGf2Commit,
280        common: &NsAvidmGf2Common,
281        share: &NsAvidmGf2Share,
282    ) -> VidResult<crate::VerificationResult> {
283        if !Self::is_consistent(commit, common) {
284            return Ok(Err(()));
285        }
286        Self::verify_share_with_verified_common(common, share)
287    }
288
289    /// Recover the entire payload from enough share
290    pub fn recover(common: &NsAvidmGf2Common, shares: &[NsAvidmGf2Share]) -> VidResult<Vec<u8>> {
291        if shares.is_empty() {
292            return Err(VidError::InsufficientShares);
293        }
294        // Each `ns_recover` is independent: distinct decoder state, distinct
295        // output bytes. Run them on the rayon pool so multi-namespace blocks
296        // actually use more than one core.
297        let per_ns: Vec<Vec<u8>> = (0..common.ns_lens.len())
298            .into_par_iter()
299            .map(|ns_index| Self::ns_recover(common, ns_index, shares))
300            .collect::<VidResult<Vec<_>>>()?;
301        Ok(per_ns.concat())
302    }
303
304    /// Recover the payload for a given namespace.
305    /// Given namespace ID should be valid for all shares, i.e. `ns_commits` and `content` have
306    /// at least `ns_index` elements for all shares.
307    pub fn ns_recover(
308        common: &NsAvidmGf2Common,
309        ns_index: usize,
310        shares: &[NsAvidmGf2Share],
311    ) -> VidResult<Vec<u8>> {
312        if shares.is_empty() {
313            return Err(VidError::InsufficientShares);
314        }
315        if ns_index >= common.ns_lens.len()
316            || !shares.iter().all(|share| share.contains_ns(ns_index))
317        {
318            return Err(VidError::IndexOutOfBound);
319        }
320        let ns_commit = &common.ns_commits[ns_index];
321        let shares: Vec<_> = shares
322            .iter()
323            .filter_map(|share| share.inner_ns_share(ns_index))
324            .collect();
325        AvidmGf2Scheme::recover(&common.param, ns_commit, &shares)
326    }
327}
328
329/// A namespaced dispersal.
330#[derive(Clone, Debug)]
331#[non_exhaustive]
332pub struct NsDispersal {
333    /// Index of this namespace in the namespace table.
334    pub ns_index: usize,
335    /// Byte length of this namespace's slice of the payload.
336    pub payload_byte_len: usize,
337    /// Commitment to this namespace's shards.
338    pub commit: AvidmGf2Commit,
339    /// One share per storage node, in distribution order.
340    pub shares: Vec<AvidmGf2Share>,
341}
342
343/// Unit tests
344#[cfg(test)]
345pub mod tests {
346    use rand::{RngCore, seq::SliceRandom};
347
348    use crate::avidm_gf2::namespaced::NsAvidmGf2Scheme;
349
350    fn disperse_with_payload(
351        payload: &[u8],
352    ) -> (
353        crate::avidm_gf2::namespaced::NsAvidmGf2Commit,
354        crate::avidm_gf2::namespaced::NsAvidmGf2Common,
355        Vec<crate::avidm_gf2::namespaced::NsAvidmGf2Share>,
356    ) {
357        let num_storage_nodes = 9;
358        let ns_table = [(0usize..15), (15..48)];
359
360        let mut rng = jf_utils::test_rng();
361        let weights: Vec<u32> = (0..num_storage_nodes)
362            .map(|_| rng.next_u32() % 5 + 1)
363            .collect();
364        let total_weights: u32 = weights.iter().sum();
365        let recovery_threshold = total_weights.div_ceil(3) as usize;
366        let params = NsAvidmGf2Scheme::setup(recovery_threshold, total_weights as usize).unwrap();
367
368        NsAvidmGf2Scheme::ns_disperse(&params, &weights, payload, ns_table.iter().cloned()).unwrap()
369    }
370
371    fn setup_test_data() -> (
372        crate::avidm_gf2::namespaced::NsAvidmGf2Commit,
373        crate::avidm_gf2::namespaced::NsAvidmGf2Common,
374        Vec<crate::avidm_gf2::namespaced::NsAvidmGf2Share>,
375    ) {
376        let payload: Vec<u8> = (0u8..48).collect();
377        disperse_with_payload(&payload)
378    }
379
380    #[test]
381    fn verify_share_with_verified_common_accepts_valid() {
382        let (commit, common, shares) = setup_test_data();
383        assert!(NsAvidmGf2Scheme::is_consistent(&commit, &common));
384        for share in &shares {
385            assert!(
386                NsAvidmGf2Scheme::verify_share_with_verified_common(&common, share)
387                    .is_ok_and(|r| r.is_ok())
388            );
389        }
390    }
391
392    #[test]
393    fn verify_share_with_verified_common_rejects_tampered_share() {
394        let (_commit, common, shares) = setup_test_data();
395        // Create a tampered share by removing one namespace entry
396        let mut tampered = shares[0].clone();
397        tampered.0.pop();
398        assert!(NsAvidmGf2Scheme::verify_share_with_verified_common(&common, &tampered).is_err());
399
400        // Create a tampered share by dispersing a different payload and swapping
401        let (_commit2, _common2, shares2) = disperse_with_payload(&[0xAB; 48]);
402        let mut mixed = shares[0].clone();
403        mixed.0[0] = shares2[0].0[0].clone();
404        assert!(
405            NsAvidmGf2Scheme::verify_share_with_verified_common(&common, &mixed)
406                .is_ok_and(|r| r.is_err())
407        );
408    }
409
410    #[test]
411    fn composition_equivalence() {
412        let (commit, common, shares) = setup_test_data();
413        for share in &shares {
414            let full_result = NsAvidmGf2Scheme::verify_share(&commit, &common, share)
415                .unwrap()
416                .is_ok();
417            let composed_result = NsAvidmGf2Scheme::is_consistent(&commit, &common)
418                && NsAvidmGf2Scheme::verify_share_with_verified_common(&common, share)
419                    .unwrap()
420                    .is_ok();
421            assert_eq!(full_result, composed_result);
422        }
423    }
424
425    #[test]
426    fn is_consistent_rejects_tampered_commit() {
427        let (commit, common, _shares) = setup_test_data();
428        // Use commit from a different dispersal
429        let (different_commit, ..) = disperse_with_payload(&[0xCD; 48]);
430        // Verify original is consistent
431        assert!(NsAvidmGf2Scheme::is_consistent(&commit, &common));
432        // Verify different commit is inconsistent with original common
433        assert!(!NsAvidmGf2Scheme::is_consistent(&different_commit, &common));
434    }
435
436    #[test]
437    fn is_consistent_rejects_tampered_common() {
438        let (commit, common, _shares) = setup_test_data();
439        // Swap in ns_commits from a different dispersal
440        let (_, different_common, _) = disperse_with_payload(&[0xCD; 48]);
441        let mut tampered_common = common;
442        tampered_common.ns_commits = different_common.ns_commits;
443        assert!(!NsAvidmGf2Scheme::is_consistent(&commit, &tampered_common));
444    }
445
446    #[test]
447    fn round_trip() {
448        // play with these items
449        let num_storage_nodes = 9;
450        let ns_lens = [15, 33];
451        let ns_table = [(0usize..15), (15..48)];
452        let payload_byte_len = ns_lens.iter().sum();
453
454        let mut rng = jf_utils::test_rng();
455
456        // more items as a function of the above
457        let weights: Vec<u32> = (0..num_storage_nodes)
458            .map(|_| rng.next_u32() % 5 + 1)
459            .collect();
460        let total_weights: u32 = weights.iter().sum();
461        let recovery_threshold = total_weights.div_ceil(3) as usize;
462        let params = NsAvidmGf2Scheme::setup(recovery_threshold, total_weights as usize).unwrap();
463
464        println!(
465            "recovery_threshold:: {recovery_threshold} num_storage_nodes: {num_storage_nodes} \
466             payload_byte_len: {payload_byte_len}"
467        );
468        println!("weights: {weights:?}");
469
470        let payload = {
471            let mut bytes_random = vec![0u8; payload_byte_len];
472            rng.fill_bytes(&mut bytes_random);
473            bytes_random
474        };
475
476        let (commit, common, mut shares) =
477            NsAvidmGf2Scheme::ns_disperse(&params, &weights, &payload, ns_table.iter().cloned())
478                .unwrap();
479
480        assert_eq!(shares.len(), num_storage_nodes);
481
482        assert_eq!(
483            commit,
484            NsAvidmGf2Scheme::commit(&params, &payload, ns_table.iter().cloned())
485                .unwrap()
486                .0
487        );
488
489        // verify shares
490        shares.iter().for_each(|share| {
491            assert!(
492                NsAvidmGf2Scheme::verify_share(&commit, &common, share).is_ok_and(|r| r.is_ok())
493            )
494        });
495
496        // test payload recovery on a random subset of shares
497        shares.shuffle(&mut rng);
498        let mut cumulated_weights = 0;
499        let mut cut_index = 0;
500        while cumulated_weights <= recovery_threshold {
501            cumulated_weights += shares[cut_index].weight();
502            cut_index += 1;
503        }
504        let ns0_payload_recovered =
505            NsAvidmGf2Scheme::ns_recover(&common, 0, &shares[..cut_index]).unwrap();
506        assert_eq!(ns0_payload_recovered[..], payload[ns_table[0].clone()]);
507        let ns1_payload_recovered =
508            NsAvidmGf2Scheme::ns_recover(&common, 1, &shares[..cut_index]).unwrap();
509        assert_eq!(ns1_payload_recovered[..], payload[ns_table[1].clone()]);
510        let payload_recovered = NsAvidmGf2Scheme::recover(&common, &shares[..cut_index]).unwrap();
511        assert_eq!(payload_recovered, payload);
512    }
513}