1use 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
46type 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#[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 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#[derive(Clone, Hash, Serialize, Deserialize, Eq, PartialEq)]
94pub struct RawAvidMShare {
95 range: Range<usize>,
97 #[serde(with = "canonical")]
99 payload: Vec<Vec<F>>,
100 #[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#[derive(Clone, Debug, Hash, Serialize, Deserialize, Eq, PartialEq)]
117pub struct AvidMShare {
118 index: u32,
120 payload_byte_len: usize,
122 content: RawAvidMShare,
124}
125
126const MAX_TOTAL_WEIGHTS: usize = 1 << 16;
129
130const MAX_EXPANSION: usize = 4;
132
133#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq)]
140pub struct AvidMParam {
141 pub total_weights: usize,
143 pub recovery_threshold: usize,
145}
146
147impl AvidMParam {
148 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#[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
176pub struct AvidMScheme;
178
179impl AvidMScheme {
180 pub fn setup(recovery_threshold: usize, total_weights: usize) -> VidResult<AvidMParam> {
182 AvidMParam::new(recovery_threshold, total_weights)
183 }
184}
185
186impl AvidMScheme {
187 fn pad_to_fields(param: &AvidMParam, payload: &[u8]) -> Vec<F> {
192 let elem_bytes_len = bytes_to_field::elem_byte_capacity::<F>();
194
195 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 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 #[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)?; let encoding_timer = start_timer!(|| "Encoding payload");
225
226 let codewords: Vec<_> = payload
228 .par_chunks(param.recovery_threshold)
229 .map(|chunk| {
230 let mut fft_vec = domain.fft(chunk); fft_vec.truncate(param.total_weights); fft_vec
233 })
234 .collect();
235 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 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 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 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 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 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 let (x, raw_shares): (Vec<_>, Vec<_>) = raw_shares
413 .into_iter()
414 .map(|(i, p)| (domain.element(i), p))
415 .unzip();
416 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 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 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#[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(¶m, &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(¶m, &bytes);
511 assert_eq!(padded.len(), 4usize);
512 }
513
514 #[test]
515 fn round_trip() {
516 let num_storage_nodes_list = [4, 9, 6, 16];
518 let payload_byte_lens = [1, 31, 32, 500];
519
520 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(¶ms, &weights, &payload).unwrap();
547
548 assert_eq!(shares.len(), num_storage_nodes);
549
550 shares.iter().for_each(|share| {
552 assert!(
553 AvidMScheme::verify_share(¶ms, &commit, share).is_ok_and(|r| r.is_ok())
554 )
555 });
556
557 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(¶ms, &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; 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(¶ms, &weights, &payload).unwrap();
591 end_timer!(disperse_timer);
592
593 let recover_timer = start_timer!(|| "Recovery");
594 AvidMScheme::recover(¶ms, &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(¶m, &[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}