1use std::{ops::Range, vec};
4
5use anyhow::anyhow;
6use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
7use jf_merkle_tree::{MerkleTreeScheme, append_only::MerkleTree as JfMerkleTree};
8use p3_maybe_rayon::prelude::*;
9use serde::{Deserialize, Serialize};
10use tagged_base64::tagged;
11
12use crate::{
13 VidError, VidResult, VidScheme,
14 utils::blake3::{Blake3DigestAlgorithm, Blake3Node},
15};
16
17pub mod namespaced;
19pub mod proofs;
21
22pub(crate) type MerkleTree = JfMerkleTree<Blake3Node, Blake3DigestAlgorithm, u64, 4, Blake3Node>;
27type MerkleProof = <MerkleTree as MerkleTreeScheme>::MembershipProof;
28type MerkleCommit = <MerkleTree as MerkleTreeScheme>::Commitment;
29
30pub struct AvidmGf2Scheme;
32
33#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq)]
35pub struct AvidmGf2Param {
36 pub total_weights: usize,
38 pub recovery_threshold: usize,
40}
41
42impl AvidmGf2Param {
43 pub fn new(recovery_threshold: usize, total_weights: usize) -> VidResult<Self> {
45 if recovery_threshold == 0 || total_weights < recovery_threshold {
46 return Err(VidError::InvalidParam);
47 }
48 Ok(Self {
49 total_weights,
50 recovery_threshold,
51 })
52 }
53}
54
55#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq)]
57pub struct AvidmGf2Share {
58 range: Range<usize>,
60 #[serde(with = "nested_bytes")]
62 payload: Vec<Vec<u8>>,
63 mt_proofs: Vec<MerkleProof>,
65}
66
67mod nested_bytes {
69 use serde::{Deserialize, Deserializer, Serializer, ser::SerializeSeq};
70 use serde_bytes::{ByteBuf, Bytes};
71
72 pub fn serialize<S>(v: &[Vec<u8>], s: S) -> Result<S::Ok, S::Error>
73 where
74 S: Serializer,
75 {
76 let mut seq = s.serialize_seq(Some(v.len()))?;
77 for inner in v {
78 seq.serialize_element(Bytes::new(inner))?;
79 }
80 seq.end()
81 }
82
83 pub fn deserialize<'de, D>(d: D) -> Result<Vec<Vec<u8>>, D::Error>
84 where
85 D: Deserializer<'de>,
86 {
87 let v: Vec<ByteBuf> = Deserialize::deserialize(d)?;
88 Ok(v.into_iter().map(ByteBuf::into_vec).collect())
89 }
90}
91
92impl AvidmGf2Share {
93 pub fn weight(&self) -> usize {
95 self.range.len()
96 }
97
98 pub fn range(&self) -> &Range<usize> {
100 &self.range
101 }
102
103 pub fn validate(&self) -> bool {
105 self.payload.len() == self.range.len() && self.mt_proofs.len() == self.range.len()
106 }
107}
108
109#[derive(
111 Clone,
112 Copy,
113 Debug,
114 Default,
115 Hash,
116 CanonicalSerialize,
117 CanonicalDeserialize,
118 Eq,
119 PartialEq,
120 Ord,
121 PartialOrd,
122)]
123#[tagged("AvidmGf2Commit")]
124#[repr(C)]
125pub struct AvidmGf2Commit {
126 pub commit: MerkleCommit,
128}
129
130impl AsRef<[u8]> for AvidmGf2Commit {
131 fn as_ref(&self) -> &[u8] {
132 self.commit.as_ref()
133 }
134}
135
136impl AsRef<[u8; 32]> for AvidmGf2Commit {
137 fn as_ref(&self) -> &[u8; 32] {
138 <Self as AsRef<[u8]>>::as_ref(self)
139 .try_into()
140 .expect("AvidmGf2Commit is always 32 bytes")
141 }
142}
143
144impl AvidmGf2Scheme {
145 pub fn setup(recovery_threshold: usize, total_weights: usize) -> VidResult<AvidmGf2Param> {
147 AvidmGf2Param::new(recovery_threshold, total_weights)
148 }
149
150 fn chunk_and_pad(
157 payload: &[u8],
158 shard_bytes: usize,
159 original_count: usize,
160 ) -> VidResult<Vec<Vec<u8>>> {
161 let padded_len = shard_bytes * original_count;
162 if padded_len < payload.len() + 1 {
163 return Err(VidError::Argument(
164 "Payload length is too large to fit in the given payload length".to_string(),
165 ));
166 }
167 let mut original: Vec<Vec<u8>> = Vec::with_capacity(original_count);
168 for i in 0..original_count {
169 let start = i * shard_bytes;
170 let mut chunk = vec![0u8; shard_bytes];
171 if start < payload.len() {
172 let end = ((i + 1) * shard_bytes).min(payload.len());
173 let take = end - start;
174 chunk[..take].copy_from_slice(&payload[start..end]);
175 if take < shard_bytes {
176 chunk[take] = 1u8;
178 }
179 } else if start == payload.len() {
180 chunk[0] = 1u8;
183 }
184 original.push(chunk);
185 }
186 Ok(original)
187 }
188
189 fn raw_disperse(
190 param: &AvidmGf2Param,
191 payload: &[u8],
192 ) -> VidResult<(MerkleTree, Vec<Vec<u8>>)> {
193 let original_count = param.recovery_threshold;
194 let recovery_count = param.total_weights - param.recovery_threshold;
195 let mut shard_bytes = (payload.len() + 1).div_ceil(original_count);
196 if shard_bytes % 2 == 1 {
197 shard_bytes += 1;
198 }
199 let original = Self::chunk_and_pad(payload, shard_bytes, original_count)?;
200 let recovery = if recovery_count == 0 {
201 vec![]
202 } else {
203 reed_solomon_simd::encode(original_count, recovery_count, &original)?
204 };
205
206 let shares = [original, recovery].concat();
207 let share_digests: Vec<Blake3Node> = shares
208 .par_iter()
209 .map(|share| Blake3Node::from(blake3::hash(share)))
210 .collect();
211 let mt = MerkleTree::from_elems(None, &share_digests)?;
212 Ok((mt, shares))
213 }
214
215 #[cfg(any(test, feature = "testing"))]
226 pub fn disperse_non_codeword(
227 param: &AvidmGf2Param,
228 distribution: &[u32],
229 payload: &[u8],
230 ) -> VidResult<(AvidmGf2Commit, Vec<AvidmGf2Share>)> {
231 let total_weights = distribution.iter().map(|&w| w as usize).sum::<usize>();
232 if total_weights != param.total_weights {
233 return Err(VidError::Argument(
234 "Weight distribution is inconsistent with the given param".to_string(),
235 ));
236 }
237 if distribution.contains(&0u32) {
238 return Err(VidError::Argument("Weight cannot be zero".to_string()));
239 }
240 let original_count = param.recovery_threshold;
241 let (_, mut shards) = Self::raw_disperse(param, payload)?;
242 if shards.len() <= original_count {
243 return Err(VidError::Argument(
244 "Payload has no recovery shards to corrupt".to_string(),
245 ));
246 }
247 for shard in &mut shards[original_count..] {
252 shard[0] ^= 0xff;
253 }
254 let share_digests: Vec<Blake3Node> = shards
255 .iter()
256 .map(|shard| Blake3Node::from(blake3::hash(shard)))
257 .collect();
258 let mt = MerkleTree::from_elems(None, &share_digests)?;
259 let commit = AvidmGf2Commit {
260 commit: mt.commitment(),
261 };
262 let ranges: Vec<_> = distribution
263 .iter()
264 .scan(0usize, |sum, w| {
265 let prefix_sum = *sum;
266 *sum += *w as usize;
267 Some(prefix_sum..*sum)
268 })
269 .collect();
270 let mut shards_iter = shards.into_iter();
271 let payloads: Vec<Vec<Vec<u8>>> = ranges
272 .iter()
273 .map(|range| shards_iter.by_ref().take(range.len()).collect())
274 .collect();
275 let mut proofs_iter = mt
276 .collect_leaves_with_proofs()
277 .into_iter()
278 .map(|(_, _, proof)| proof);
279 let proof_groups: Vec<Vec<MerkleProof>> = ranges
280 .iter()
281 .map(|range| proofs_iter.by_ref().take(range.len()).collect())
282 .collect();
283 let shares: Vec<_> = ranges
284 .into_iter()
285 .zip(payloads)
286 .zip(proof_groups)
287 .map(|((range, payload), mt_proofs)| AvidmGf2Share {
288 range,
289 payload,
290 mt_proofs,
291 })
292 .collect();
293 Ok((commit, shares))
294 }
295}
296
297impl VidScheme for AvidmGf2Scheme {
298 type Param = AvidmGf2Param;
299 type Share = AvidmGf2Share;
300 type Commit = AvidmGf2Commit;
301
302 fn commit(param: &Self::Param, payload: &[u8]) -> VidResult<Self::Commit> {
303 let (mt, _) = Self::raw_disperse(param, payload)?;
304 Ok(Self::Commit {
305 commit: mt.commitment(),
306 })
307 }
308
309 fn disperse(
310 param: &Self::Param,
311 distribution: &[u32],
312 payload: &[u8],
313 ) -> VidResult<(Self::Commit, Vec<Self::Share>)> {
314 let total_weights = distribution.iter().map(|&w| w as usize).sum::<usize>();
315 if total_weights != param.total_weights {
316 return Err(VidError::Argument(
317 "Weight distribution is inconsistent with the given param".to_string(),
318 ));
319 }
320 if distribution.contains(&0u32) {
321 return Err(VidError::Argument("Weight cannot be zero".to_string()));
322 }
323 let (mt, shares) = Self::raw_disperse(param, payload)?;
324 let commit = AvidmGf2Commit {
325 commit: mt.commitment(),
326 };
327
328 let ranges: Vec<_> = distribution
329 .iter()
330 .scan(0usize, |sum, w| {
331 let prefix_sum = *sum;
332 *sum += *w as usize;
333 Some(prefix_sum..*sum)
334 })
335 .collect();
336 let mut shares_iter = shares.into_iter();
345 let payloads: Vec<Vec<Vec<u8>>> = ranges
346 .iter()
347 .map(|range| shares_iter.by_ref().take(range.len()).collect())
348 .collect();
349 let mut proofs_iter = mt
350 .collect_leaves_with_proofs()
351 .into_iter()
352 .map(|(_, _, proof)| proof);
353 let proof_groups: Vec<Vec<MerkleProof>> = ranges
354 .iter()
355 .map(|range| proofs_iter.by_ref().take(range.len()).collect())
356 .collect();
357 let shares: Vec<_> = ranges
361 .into_iter()
362 .zip(payloads)
363 .zip(proof_groups)
364 .map(|((range, payload), mt_proofs)| AvidmGf2Share {
365 range,
366 payload,
367 mt_proofs,
368 })
369 .collect();
370 Ok((commit, shares))
371 }
372
373 fn verify_share(
374 param: &Self::Param,
375 commit: &Self::Commit,
376 share: &Self::Share,
377 ) -> VidResult<crate::VerificationResult> {
378 if !share.validate() || share.range.is_empty() || share.range.end > param.total_weights {
379 return Err(VidError::InvalidShare);
380 }
381 let start = share.range.start;
385 let len = share.range.end - start;
386 match (0..len)
387 .into_par_iter()
388 .map(|i| -> VidResult<crate::VerificationResult> {
389 let payload_digest = Blake3Node::from(blake3::hash(&share.payload[i]));
390 MerkleTree::verify(
391 commit.commit,
392 (start + i) as u64,
393 payload_digest,
394 &share.mt_proofs[i],
395 )
396 .map_err(VidError::from)
397 })
398 .find_any(|r| !matches!(r, Ok(Ok(()))))
399 {
400 None => Ok(Ok(())),
401 Some(Ok(v)) => Ok(v),
402 Some(Err(e)) => Err(e),
403 }
404 }
405
406 fn recover(
407 param: &Self::Param,
408 _commit: &Self::Commit,
409 shares: &[Self::Share],
410 ) -> VidResult<Vec<u8>> {
411 let original_count = param.recovery_threshold;
412 let recovery_count = param.total_weights - param.recovery_threshold;
413 let Some(first_share) = shares.iter().find(|s| !s.payload.is_empty()) else {
415 return Err(VidError::InsufficientShares);
416 };
417 let shard_bytes = first_share.payload[0].len();
418
419 let mut input_orig: Vec<Option<&[u8]>> = vec![None; original_count];
424
425 let mut recovered: Vec<u8> = Vec::with_capacity(original_count * shard_bytes);
426 if recovery_count == 0 {
427 for share in shares {
430 if !share.validate() || share.payload.iter().any(|p| p.len() != shard_bytes) {
431 return Err(VidError::InvalidShare);
432 }
433 for (i, index) in share.range.clone().enumerate() {
434 if index < original_count {
435 input_orig[index] = Some(&share.payload[i]);
436 }
437 }
438 }
439 for slot in &input_orig {
440 let shard = slot
441 .ok_or_else(|| VidError::Internal(anyhow!("Failed to recover the payload.")))?;
442 recovered.extend_from_slice(shard);
443 }
444 } else {
445 let mut decoder = reed_solomon_simd::ReedSolomonDecoder::new(
446 original_count,
447 recovery_count,
448 shard_bytes,
449 )?;
450 for share in shares {
451 if !share.validate() || share.payload.iter().any(|p| p.len() != shard_bytes) {
452 return Err(VidError::InvalidShare);
453 }
454 for (i, index) in share.range.clone().enumerate() {
455 let shard = &share.payload[i];
456 if index < original_count {
457 input_orig[index] = Some(shard);
458 decoder.add_original_shard(index, shard)?;
459 } else {
460 decoder.add_recovery_shard(index - original_count, shard)?;
461 }
462 }
463 }
464
465 let result = decoder.decode()?;
466 for (i, shard) in input_orig.iter().enumerate().take(original_count) {
467 let shard: &[u8] = match shard {
468 Some(data) => data,
469 None => result.restored_original(i).ok_or_else(|| {
470 VidError::Internal(anyhow!("Failed to recover the payload."))
471 })?,
472 };
473 recovered.extend_from_slice(shard);
474 }
475 }
476 match recovered.iter().rposition(|&b| b != 0) {
477 Some(pad_index) if recovered[pad_index] == 1u8 => {
478 recovered.truncate(pad_index);
479 Ok(recovered)
480 },
481 _ => Err(VidError::Argument(
482 "Malformed payload, cannot find the padding position".to_string(),
483 )),
484 }
485 }
486}
487
488#[cfg(test)]
490pub mod tests {
491 use rand::{RngCore, seq::SliceRandom};
492
493 use super::AvidmGf2Scheme;
494 use crate::VidScheme;
495
496 #[test]
497 fn round_trip() {
498 let num_storage_nodes_list = [4, 9, 16];
500 let payload_byte_lens = [1, 31, 32, 500];
501
502 let mut rng = jf_utils::test_rng();
505
506 for num_storage_nodes in num_storage_nodes_list {
507 let weights: Vec<u32> = (0..num_storage_nodes)
508 .map(|_| rng.next_u32() % 5 + 1)
509 .collect();
510 let total_weights: u32 = weights.iter().sum();
511 let recovery_threshold = total_weights.div_ceil(3) as usize;
512 let params = AvidmGf2Scheme::setup(recovery_threshold, total_weights as usize).unwrap();
513
514 for payload_byte_len in payload_byte_lens {
515 let payload = {
516 let mut bytes_random = vec![0u8; payload_byte_len];
517 rng.fill_bytes(&mut bytes_random);
518 bytes_random
519 };
520
521 let (commit, mut shares) =
522 AvidmGf2Scheme::disperse(¶ms, &weights, &payload).unwrap();
523
524 assert_eq!(shares.len(), num_storage_nodes);
525
526 shares.iter().for_each(|share| {
528 assert!(
529 AvidmGf2Scheme::verify_share(¶ms, &commit, share)
530 .is_ok_and(|r| r.is_ok())
531 )
532 });
533
534 shares.shuffle(&mut rng);
536 let mut cumulated_weights = 0;
537 let mut cut_index = 0;
538 while cumulated_weights < recovery_threshold {
539 cumulated_weights += shares[cut_index].weight();
540 cut_index += 1;
541 }
542 let payload_recovered =
543 AvidmGf2Scheme::recover(¶ms, &commit, &shares[..cut_index]).unwrap();
544 assert_eq!(payload_recovered, payload);
545 }
546 }
547 }
548
549 #[test]
550 fn round_trip_edge_case() {
551 let num_storage_nodes_list = [4, 9, 16];
553 let payload_byte_lens = [1, 31, 32, 500];
554
555 let mut rng = jf_utils::test_rng();
558
559 for num_storage_nodes in num_storage_nodes_list {
560 let weights: Vec<u32> = (0..num_storage_nodes)
561 .map(|_| rng.next_u32() % 5 + 1)
562 .collect();
563 let total_weights: u32 = weights.iter().sum();
564 let recovery_threshold = total_weights as usize;
565 let params = AvidmGf2Scheme::setup(recovery_threshold, total_weights as usize).unwrap();
566
567 for payload_byte_len in payload_byte_lens {
568 let payload = {
569 let mut bytes_random = vec![0u8; payload_byte_len];
570 rng.fill_bytes(&mut bytes_random);
571 bytes_random
572 };
573
574 let (commit, mut shares) =
575 AvidmGf2Scheme::disperse(¶ms, &weights, &payload).unwrap();
576
577 assert_eq!(shares.len(), num_storage_nodes);
578
579 shares.iter().for_each(|share| {
581 assert!(
582 AvidmGf2Scheme::verify_share(¶ms, &commit, share)
583 .is_ok_and(|r| r.is_ok())
584 )
585 });
586
587 shares.shuffle(&mut rng);
589 let payload_recovered =
590 AvidmGf2Scheme::recover(¶ms, &commit, &shares[..]).unwrap();
591 assert_eq!(payload_recovered, payload);
592 }
593 }
594 }
595
596 #[test]
597 fn disperse_rejects_inconsistent_distribution() {
598 let total_weights = 10usize;
599 let recovery_threshold = 4;
600 let params = AvidmGf2Scheme::setup(recovery_threshold, total_weights).unwrap();
601 let payload = vec![1u8; 100];
602
603 let bad_weights = vec![3u32; 4];
605 assert!(
606 AvidmGf2Scheme::disperse(¶ms, &bad_weights, &payload).is_err(),
607 "disperse should reject distribution that doesn't sum to total_weights"
608 );
609
610 let zero_weight = vec![0u32, 5, 5];
612 assert!(
613 AvidmGf2Scheme::disperse(¶ms, &zero_weight, &payload).is_err(),
614 "disperse should reject zero-weight entries"
615 );
616
617 let good_weights = vec![2u32; 5];
619 assert!(AvidmGf2Scheme::disperse(¶ms, &good_weights, &payload).is_ok());
620 }
621
622 #[test]
623 fn verify_share_rejects_out_of_range() {
624 let total_weights = 10usize;
625 let recovery_threshold = 4;
626 let params = AvidmGf2Scheme::setup(recovery_threshold, total_weights).unwrap();
627 let payload = vec![1u8; 100];
628 let weights = vec![2u32; 5];
629
630 let (commit, shares) = AvidmGf2Scheme::disperse(¶ms, &weights, &payload).unwrap();
631
632 for share in &shares {
634 assert!(AvidmGf2Scheme::verify_share(¶ms, &commit, share).is_ok_and(|r| r.is_ok()));
635 }
636
637 let smaller_params = AvidmGf2Scheme::setup(2, 5).unwrap();
639 let last_share = shares.last().unwrap();
640 assert!(
641 AvidmGf2Scheme::verify_share(&smaller_params, &commit, last_share).is_err(),
642 "verify_share should reject share with range.end > param.total_weights"
643 );
644 }
645}