Skip to main content

vid/
avidm.rs

1//! This module implements the AVID-M scheme, whose name came after the DispersedLedger paper <https://www.usenix.org/conference/nsdi22/presentation/yang>.
2//!
3//! To disperse a payload to a number of storage nodes according to a weight
4//! distribution, the payload is first converted into field elements and then
5//! divided into chunks of `k` elements each, and each chunk is then encoded
6//! into `n` field elements using Reed Solomon code. The parameter `n` equals to
7//! the total weight of all storage nodes, and `k` is the minimum collective
8//! weights required to recover the original payload. After the encoding, it can
9//! be viewed as `n` vectors of field elements each of length equals to the
10//! number of chunks. The VID commitment is obtained by Merklized these `n`
11//! vectors. And for dispersal, each storage node gets some vectors and their
12//! Merkle proofs according to its weight.
13
14use std::{collections::HashMap, fmt, iter, ops::Range};
15
16use ark_ff::PrimeField;
17use ark_poly::{EvaluationDomain, Radix2EvaluationDomain};
18use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
19use ark_std::{end_timer, start_timer};
20use config::AvidMConfig;
21use jf_merkle_tree::MerkleTreeScheme;
22use jf_utils::canonical;
23use p3_maybe_rayon::prelude::{
24    IntoParallelIterator, IntoParallelRefIterator, ParallelIterator, ParallelSlice,
25};
26use serde::{Deserialize, Serialize};
27use tagged_base64::tagged;
28
29use crate::{
30    VidError, VidResult, VidScheme,
31    utils::bytes_to_field::{self, bytes_to_field, field_to_bytes},
32};
33
34mod config;
35
36pub mod namespaced;
37pub mod proofs;
38
39#[cfg(all(not(feature = "sha256"), not(feature = "keccak256")))]
40type Config = config::Poseidon2Config;
41#[cfg(feature = "sha256")]
42type Config = config::Sha256Config;
43#[cfg(feature = "keccak256")]
44type Config = config::Keccak256Config;
45
46// Type alias for convenience
47type F = <Config as AvidMConfig>::BaseField;
48type MerkleTree = <Config as AvidMConfig>::MerkleTree;
49type MerkleProof = <MerkleTree as MerkleTreeScheme>::MembershipProof;
50type MerkleCommit = <MerkleTree as MerkleTreeScheme>::Commitment;
51
52/// Commit type for AVID-M scheme.
53#[derive(
54    Clone,
55    Copy,
56    Debug,
57    Default,
58    Hash,
59    CanonicalSerialize,
60    CanonicalDeserialize,
61    Eq,
62    PartialEq,
63    Ord,
64    PartialOrd,
65)]
66#[tagged("AvidMCommit")]
67#[repr(C)]
68pub struct AvidMCommit {
69    /// Root commitment of the Merkle tree.
70    pub commit: MerkleCommit,
71}
72
73impl AsRef<[u8]> for AvidMCommit {
74    fn as_ref(&self) -> &[u8] {
75        unsafe {
76            ::core::slice::from_raw_parts(
77                (self as *const Self) as *const u8,
78                ::core::mem::size_of::<Self>(),
79            )
80        }
81    }
82}
83
84impl AsRef<[u8; 32]> for AvidMCommit {
85    fn as_ref(&self) -> &[u8; 32] {
86        unsafe { ::core::slice::from_raw_parts((self as *const Self) as *const u8, 32) }
87            .try_into()
88            .unwrap()
89    }
90}
91
92/// Share type to be distributed among the parties.
93#[derive(Clone, Hash, Serialize, Deserialize, Eq, PartialEq)]
94pub struct RawAvidMShare {
95    /// Range of this share in the encoded payload.
96    range: Range<usize>,
97    /// Actual share content.
98    #[serde(with = "canonical")]
99    payload: Vec<Vec<F>>,
100    /// Merkle proof of the content.
101    #[serde(with = "canonical")]
102    mt_proofs: Vec<MerkleProof>,
103}
104
105impl fmt::Debug for RawAvidMShare {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        f.debug_struct("RawAvidMShare")
108            .field("range", &self.range)
109            .field("payload", &format_args!("..."))
110            .field("mt_proofs", &format_args!("..."))
111            .finish()
112    }
113}
114
115/// Share type to be distributed among the parties.
116#[derive(Clone, Debug, Hash, Serialize, Deserialize, Eq, PartialEq)]
117pub struct AvidMShare {
118    /// Index number of the given share.
119    index: u32,
120    /// The length of payload in bytes.
121    payload_byte_len: usize,
122    /// Content of this AvidMShare.
123    content: RawAvidMShare,
124}
125
126// Padding grows with the threshold. Node weights total at most
127// `VID_TARGET_TOTAL_STAKE` (1000) plus one per validator.
128const MAX_TOTAL_WEIGHTS: usize = 1 << 16;
129
130// Nodes expand 3x, so 4 is headroom.
131const MAX_EXPANSION: usize = 4;
132
133/// Public parameters of the AVID-M scheme.
134///
135/// Valid when `0 < recovery_threshold <= total_weights <= 2^16` and
136/// `total_weights <= 4 * recovery_threshold`. The fields are public and
137/// deserialized unchecked, so [`Self::new`] and every [`AvidMScheme`] entry
138/// point return [`VidError::InvalidParam`] for an invalid param.
139#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq)]
140pub struct AvidMParam {
141    /// Total weights of all storage nodes
142    pub total_weights: usize,
143    /// Minimum collective weights required to recover the original payload.
144    pub recovery_threshold: usize,
145}
146
147impl AvidMParam {
148    /// Construct a new [`AvidMParam`].
149    pub fn new(recovery_threshold: usize, total_weights: usize) -> VidResult<Self> {
150        let param = Self {
151            total_weights,
152            recovery_threshold,
153        };
154        param.validate()?;
155        Ok(param)
156    }
157
158    fn validate(&self) -> VidResult<()> {
159        if self.recovery_threshold == 0
160            || self.total_weights < self.recovery_threshold
161            || self.total_weights > MAX_TOTAL_WEIGHTS
162            || self.total_weights > self.recovery_threshold * MAX_EXPANSION
163        {
164            return Err(VidError::InvalidParam);
165        }
166        Ok(())
167    }
168}
169
170/// Helper: initialize a FFT domain
171#[inline]
172fn radix2_domain<F: PrimeField>(domain_size: usize) -> VidResult<Radix2EvaluationDomain<F>> {
173    Radix2EvaluationDomain::<F>::new(domain_size).ok_or_else(|| VidError::InvalidParam)
174}
175
176/// Dummy struct for AVID-M scheme.
177pub struct AvidMScheme;
178
179impl AvidMScheme {
180    /// Setup an instance for AVID-M scheme
181    pub fn setup(recovery_threshold: usize, total_weights: usize) -> VidResult<AvidMParam> {
182        AvidMParam::new(recovery_threshold, total_weights)
183    }
184}
185
186impl AvidMScheme {
187    /// Helper function.
188    /// Transform the payload bytes into a list of fields elements.
189    /// This function also pads the bytes with a 1 in the end, following by many 0's
190    /// until the length of the output is a multiple of `param.recovery_threshold`.
191    fn pad_to_fields(param: &AvidMParam, payload: &[u8]) -> Vec<F> {
192        // The number of bytes that can be encoded into a single F element.
193        let elem_bytes_len = bytes_to_field::elem_byte_capacity::<F>();
194
195        // A "chunk" is a byte slice whose size holds exactly `recovery_threshold`
196        // F elements.
197        let num_bytes_per_chunk = param.recovery_threshold * elem_bytes_len;
198
199        let remainder = (payload.len() + 1) % num_bytes_per_chunk;
200        let pad_num_zeros = (num_bytes_per_chunk - remainder) % num_bytes_per_chunk;
201
202        // Pad the payload with a 1 and many 0's.
203        bytes_to_field::<_, F>(
204            payload
205                .iter()
206                .chain(iter::once(&1u8))
207                .chain(iter::repeat_n(&0u8, pad_num_zeros)),
208        )
209        .collect()
210    }
211
212    /// Helper function.
213    /// Let `k = recovery_threshold` and `n = total_weights`. This function
214    /// partition the `payload` into many chunks, each containing `k` field
215    /// elements. Then each chunk is encoded into `n` field element with Reed
216    /// Solomon erasure code. They are then re-organized as `n` vectors, each
217    /// collecting one field element from each chunk. These `n` vectors are
218    /// then Merklized for commitment and membership proof generation.
219    #[allow(clippy::type_complexity)]
220    #[inline]
221    fn raw_encode(param: &AvidMParam, payload: &[F]) -> VidResult<(MerkleTree, Vec<Vec<F>>)> {
222        let domain = radix2_domain::<F>(param.total_weights)?; // See docs at `domains`.
223
224        let encoding_timer = start_timer!(|| "Encoding payload");
225
226        // RS-encode each chunk
227        let codewords: Vec<_> = payload
228            .par_chunks(param.recovery_threshold)
229            .map(|chunk| {
230                let mut fft_vec = domain.fft(chunk); // RS-encode the chunk
231                fft_vec.truncate(param.total_weights); // truncate the useless evaluations
232                fft_vec
233            })
234            .collect();
235        // Generate `total_weights` raw shares. Each share collects one field element
236        // from each encode chunk.
237        let raw_shares: Vec<_> = (0..param.total_weights)
238            .into_par_iter()
239            .map(|i| codewords.iter().map(|v| v[i]).collect::<Vec<F>>())
240            .collect();
241        end_timer!(encoding_timer);
242
243        let hash_timer = start_timer!(|| "Compressing each raw share");
244        let compressed_raw_shares = raw_shares
245            .par_iter()
246            .map(|v| Config::raw_share_digest(v))
247            .collect::<Result<Vec<_>, _>>()?;
248        end_timer!(hash_timer);
249
250        let mt_timer = start_timer!(|| "Constructing Merkle tree");
251        let mt = MerkleTree::from_elems(None, &compressed_raw_shares)?;
252        end_timer!(mt_timer);
253
254        Ok((mt, raw_shares))
255    }
256
257    /// Short hand for `pad_to_field` and `raw_encode`.
258    fn pad_and_encode(param: &AvidMParam, payload: &[u8]) -> VidResult<(MerkleTree, Vec<Vec<F>>)> {
259        param.validate()?;
260        let payload = Self::pad_to_fields(param, payload);
261        Self::raw_encode(param, &payload)
262    }
263
264    /// Consume in the constructed Merkle tree and the raw shares from `raw_encode`, provide the AvidM commitment and shares.
265    fn distribute_shares(
266        param: &AvidMParam,
267        distribution: &[u32],
268        mt: MerkleTree,
269        raw_shares: Vec<Vec<F>>,
270        payload_byte_len: usize,
271    ) -> VidResult<(AvidMCommit, Vec<AvidMShare>)> {
272        // let payload_byte_len = payload.len();
273        let total_weights = distribution.iter().map(|&w| w as usize).sum::<usize>();
274        if total_weights != param.total_weights {
275            return Err(VidError::Argument(
276                "Weight distribution is inconsistent with the given param".to_string(),
277            ));
278        }
279        if distribution.contains(&0u32) {
280            return Err(VidError::Argument("Weight cannot be zero".to_string()));
281        }
282
283        let distribute_timer = start_timer!(|| "Distribute codewords to the storage nodes");
284        // Distribute the raw shares to each storage node according to the weight
285        // distribution. For each chunk, storage `i` gets `distribution[i]`
286        // consecutive raw shares ranging as `ranges[i]`.
287        let ranges: Vec<_> = distribution
288            .iter()
289            .scan(0usize, |sum, w| {
290                let prefix_sum = *sum;
291                *sum += *w as usize;
292                Some(prefix_sum..*sum)
293            })
294            .collect();
295        let shares: Vec<_> = ranges
296            .par_iter()
297            .map(|range| {
298                range
299                    .clone()
300                    .map(|k| raw_shares[k].to_owned())
301                    .collect::<Vec<_>>()
302            })
303            .collect();
304        end_timer!(distribute_timer);
305
306        let mt_proof_timer = start_timer!(|| "Generate Merkle tree proofs");
307        let shares = shares
308            .into_iter()
309            .enumerate()
310            .map(|(i, payload)| AvidMShare {
311                index: i as u32,
312                payload_byte_len,
313                content: RawAvidMShare {
314                    range: ranges[i].clone(),
315                    payload,
316                    mt_proofs: ranges[i]
317                        .clone()
318                        .map(|k| {
319                            mt.lookup(k as u64)
320                                .expect_ok()
321                                .expect("MT lookup shouldn't fail")
322                                .1
323                        })
324                        .collect::<Vec<_>>(),
325                },
326            })
327            .collect::<Vec<_>>();
328        end_timer!(mt_proof_timer);
329
330        let commit = AvidMCommit {
331            commit: mt.commitment(),
332        };
333
334        Ok((commit, shares))
335    }
336
337    pub(crate) fn verify_internal(
338        param: &AvidMParam,
339        commit: &AvidMCommit,
340        share: &RawAvidMShare,
341    ) -> VidResult<crate::VerificationResult> {
342        param.validate()?;
343        if share.range.is_empty()
344            || share.range.end > param.total_weights
345            || share.range.len() != share.payload.len()
346            || share.range.len() != share.mt_proofs.len()
347        {
348            return Err(VidError::InvalidShare);
349        }
350        for (i, index) in share.range.clone().enumerate() {
351            let compressed_payload = Config::raw_share_digest(&share.payload[i])?;
352            if MerkleTree::verify(
353                commit.commit,
354                index as u64,
355                compressed_payload,
356                &share.mt_proofs[i],
357            )?
358            .is_err()
359            {
360                return Ok(Err(()));
361            }
362        }
363        Ok(Ok(()))
364    }
365
366    pub(crate) fn recover_fields(param: &AvidMParam, shares: &[AvidMShare]) -> VidResult<Vec<F>> {
367        param.validate()?;
368        let recovery_threshold: usize = param.recovery_threshold;
369
370        // Each share's payload contains some evaluations from `num_polys`
371        // polynomials.
372        let num_polys = shares
373            .iter()
374            .find(|s| !s.content.payload.is_empty())
375            .ok_or(VidError::Argument("All shares are empty".to_string()))?
376            .content
377            .payload[0]
378            .len();
379
380        let mut raw_shares = HashMap::new();
381        for share in shares {
382            if share.content.range.len() != share.content.payload.len()
383                || share.content.range.end > param.total_weights
384            {
385                return Err(VidError::InvalidShare);
386            }
387            for (i, p) in share.content.range.clone().zip(&share.content.payload) {
388                if p.len() != num_polys {
389                    return Err(VidError::InvalidShare);
390                }
391                if raw_shares.contains_key(&i) {
392                    return Err(VidError::InvalidShare);
393                }
394                raw_shares.insert(i, p);
395                if raw_shares.len() >= recovery_threshold {
396                    break;
397                }
398            }
399            if raw_shares.len() >= recovery_threshold {
400                break;
401            }
402        }
403
404        if raw_shares.len() < recovery_threshold {
405            return Err(VidError::InsufficientShares);
406        }
407
408        let domain = radix2_domain::<F>(param.total_weights)?;
409
410        // Lagrange interpolation
411        // step 1: find all evaluation points and their raw shares
412        let (x, raw_shares): (Vec<_>, Vec<_>) = raw_shares
413            .into_iter()
414            .map(|(i, p)| (domain.element(i), p))
415            .unzip();
416        // step 2: interpolate each polynomial
417        Ok((0..num_polys)
418            .into_par_iter()
419            .map(|poly_index| {
420                jf_utils::reed_solomon_code::reed_solomon_erasure_decode(
421                    x.iter().zip(raw_shares.iter().map(|p| p[poly_index])),
422                    recovery_threshold,
423                )
424                .map_err(|err| VidError::Internal(err.into()))
425            })
426            .collect::<Result<Vec<_>, _>>()?
427            .into_iter()
428            .flatten()
429            .collect())
430    }
431}
432
433impl VidScheme for AvidMScheme {
434    type Param = AvidMParam;
435
436    type Share = AvidMShare;
437
438    type Commit = AvidMCommit;
439
440    fn commit(param: &Self::Param, payload: &[u8]) -> VidResult<Self::Commit> {
441        let (mt, _) = Self::pad_and_encode(param, payload)?;
442        Ok(AvidMCommit {
443            commit: mt.commitment(),
444        })
445    }
446
447    fn disperse(
448        param: &Self::Param,
449        distribution: &[u32],
450        payload: &[u8],
451    ) -> VidResult<(Self::Commit, Vec<Self::Share>)> {
452        let (mt, raw_shares) = Self::pad_and_encode(param, payload)?;
453        Self::distribute_shares(param, distribution, mt, raw_shares, payload.len())
454    }
455
456    fn verify_share(
457        param: &Self::Param,
458        commit: &Self::Commit,
459        share: &Self::Share,
460    ) -> VidResult<crate::VerificationResult> {
461        Self::verify_internal(param, commit, &share.content)
462    }
463
464    /// Recover payload data from shares.
465    ///
466    /// # Requirements
467    /// - Total weight of all shares must be at least `recovery_threshold`.
468    /// - Each share's `payload` must have equal length.
469    /// - All shares must be verified under the given commitment.
470    ///
471    /// Shares beyond `recovery_threshold` are ignored.
472    fn recover(
473        param: &Self::Param,
474        _commit: &Self::Commit,
475        shares: &[Self::Share],
476    ) -> VidResult<Vec<u8>> {
477        let mut bytes: Vec<u8> = field_to_bytes(Self::recover_fields(param, shares)?).collect();
478        // Remove the trimming zeros and the last 1 to get the actual payload bytes.
479        // See `pad_to_fields`.
480        if let Some(pad_index) = bytes.iter().rposition(|&b| b != 0)
481            && bytes[pad_index] == 1u8
482        {
483            bytes.truncate(pad_index);
484            return Ok(bytes);
485        }
486        Err(VidError::Argument(
487            "Malformed payload, cannot find the padding position".to_string(),
488        ))
489    }
490}
491
492/// Unit tests
493#[cfg(test)]
494pub mod tests {
495    use rand::{RngCore, seq::SliceRandom};
496
497    use super::{AvidMParam, F};
498    use crate::{VidError, VidScheme, avidm::AvidMScheme, utils::bytes_to_field};
499
500    #[test]
501    fn test_padding() {
502        let elem_bytes_len = bytes_to_field::elem_byte_capacity::<F>();
503        let param = AvidMScheme::setup(2usize, 5usize).unwrap();
504        let bytes = vec![2u8; 1];
505        let padded = AvidMScheme::pad_to_fields(&param, &bytes);
506        assert_eq!(padded.len(), 2usize);
507        assert_eq!(padded, [F::from(2u32 + u8::MAX as u32 + 1), F::from(0)]);
508
509        let bytes = vec![2u8; elem_bytes_len * 2];
510        let padded = AvidMScheme::pad_to_fields(&param, &bytes);
511        assert_eq!(padded.len(), 4usize);
512    }
513
514    #[test]
515    fn round_trip() {
516        // play with these items
517        let num_storage_nodes_list = [4, 9, 6, 16];
518        let payload_byte_lens = [1, 31, 32, 500];
519
520        // more items as a function of the above
521
522        let mut rng = jf_utils::test_rng();
523
524        for num_storage_nodes in num_storage_nodes_list {
525            let weights: Vec<u32> = (0..num_storage_nodes)
526                .map(|_| rng.next_u32() % 5 + 1)
527                .collect();
528            let total_weights: u32 = weights.iter().sum();
529            let recovery_threshold = total_weights.div_ceil(3) as usize;
530            let params = AvidMScheme::setup(recovery_threshold, total_weights as usize).unwrap();
531
532            for payload_byte_len in payload_byte_lens {
533                println!(
534                    "recovery_threshold:: {recovery_threshold} num_storage_nodes: \
535                     {num_storage_nodes} payload_byte_len: {payload_byte_len}"
536                );
537                println!("weights: {weights:?}");
538
539                let payload = {
540                    let mut bytes_random = vec![0u8; payload_byte_len];
541                    rng.fill_bytes(&mut bytes_random);
542                    bytes_random
543                };
544
545                let (commit, mut shares) =
546                    AvidMScheme::disperse(&params, &weights, &payload).unwrap();
547
548                assert_eq!(shares.len(), num_storage_nodes);
549
550                // verify shares
551                shares.iter().for_each(|share| {
552                    assert!(
553                        AvidMScheme::verify_share(&params, &commit, share).is_ok_and(|r| r.is_ok())
554                    )
555                });
556
557                // test payload recovery on a random subset of shares
558                shares.shuffle(&mut rng);
559                let mut cumulated_weights = 0;
560                let mut cut_index = 0;
561                while cumulated_weights <= recovery_threshold {
562                    cumulated_weights += shares[cut_index].content.range.len();
563                    cut_index += 1;
564                }
565                let payload_recovered =
566                    AvidMScheme::recover(&params, &commit, &shares[..cut_index]).unwrap();
567                assert_eq!(payload_recovered, payload);
568            }
569        }
570    }
571
572    #[test]
573    #[cfg(feature = "print-trace")]
574    fn round_trip_breakdown() {
575        use ark_std::{end_timer, start_timer};
576
577        let mut rng = jf_utils::test_rng();
578
579        let params = AvidMScheme::setup(50usize, 200usize).unwrap();
580        let weights = vec![2u32; 100usize];
581        let payload_byte_len = 1024 * 1024 * 32; // 32MB
582
583        let payload = {
584            let mut bytes_random = vec![0u8; payload_byte_len];
585            rng.fill_bytes(&mut bytes_random);
586            bytes_random
587        };
588
589        let disperse_timer = start_timer!(|| format!("Disperse {} bytes", payload_byte_len));
590        let (commit, shares) = AvidMScheme::disperse(&params, &weights, &payload).unwrap();
591        end_timer!(disperse_timer);
592
593        let recover_timer = start_timer!(|| "Recovery");
594        AvidMScheme::recover(&params, &commit, &shares).unwrap();
595        end_timer!(recover_timer);
596    }
597
598    #[test]
599    fn param_bounds_are_inclusive() {
600        for (recovery_threshold, total_weights) in [(1, 1), (1 << 14, 1 << 16), (2, 8)] {
601            assert!(
602                AvidMParam::new(recovery_threshold, total_weights).is_ok(),
603                "({recovery_threshold}, {total_weights})"
604            );
605        }
606        for (recovery_threshold, total_weights) in
607            [(0, 4), (3, 2), ((1 << 14) + 1, (1 << 16) + 1), (2, 9)]
608        {
609            assert!(
610                matches!(
611                    AvidMParam::new(recovery_threshold, total_weights),
612                    Err(VidError::InvalidParam)
613                ),
614                "({recovery_threshold}, {total_weights})"
615            );
616        }
617    }
618
619    #[test]
620    fn invalid_param_is_rejected() {
621        let param = AvidMScheme::setup(3, 9).unwrap();
622        let payload = [7u8; 100];
623        let (commit, shares) = AvidMScheme::disperse(&param, &[3, 3, 3], &payload).unwrap();
624
625        for (recovery_threshold, total_weights) in
626            [(0, 4), (3, 2), (1 << 15, 1 << 17), (1, 1 << 16)]
627        {
628            let forged = AvidMParam {
629                total_weights,
630                recovery_threshold,
631            };
632            assert!(
633                matches!(
634                    AvidMScheme::recover(&forged, &commit, &shares),
635                    Err(VidError::InvalidParam)
636                ),
637                "recover {forged:?}"
638            );
639            assert!(
640                matches!(
641                    AvidMScheme::commit(&forged, &payload),
642                    Err(VidError::InvalidParam)
643                ),
644                "commit {forged:?}"
645            );
646            assert!(
647                matches!(
648                    AvidMScheme::disperse(&forged, &[1], &payload),
649                    Err(VidError::InvalidParam)
650                ),
651                "disperse {forged:?}"
652            );
653            assert!(
654                matches!(
655                    AvidMScheme::verify_share(&forged, &commit, &shares[0]),
656                    Err(VidError::InvalidParam)
657                ),
658                "verify_share {forged:?}"
659            );
660        }
661    }
662}