1use std::{borrow::Cow, 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 reed_solomon_simd::ReedSolomonEncoder;
10use serde::{Deserialize, Serialize};
11use tagged_base64::tagged;
12
13use crate::{
14 VidError, VidResult, VidScheme,
15 utils::blake3::{Blake3DigestAlgorithm, Blake3Node},
16};
17
18pub mod namespaced;
20pub mod proofs;
22
23pub(crate) type MerkleTree = JfMerkleTree<Blake3Node, Blake3DigestAlgorithm, u64, 4, Blake3Node>;
28pub type MerkleProof = <MerkleTree as MerkleTreeScheme>::MembershipProof;
30type MerkleCommit = <MerkleTree as MerkleTreeScheme>::Commitment;
31
32pub struct AvidmGf2Scheme;
34
35const MAX_TOTAL_WEIGHTS: usize = 1 << 16;
38
39const MAX_EXPANSION: usize = 4;
41
42#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq)]
49pub struct AvidmGf2Param {
50 pub total_weights: usize,
52 pub recovery_threshold: usize,
54}
55
56impl AvidmGf2Param {
57 pub fn new(recovery_threshold: usize, total_weights: usize) -> VidResult<Self> {
59 let param = Self {
60 total_weights,
61 recovery_threshold,
62 };
63 param.validate()?;
64 Ok(param)
65 }
66
67 fn validate(&self) -> VidResult<()> {
68 if self.recovery_threshold == 0
69 || self.total_weights < self.recovery_threshold
70 || self.total_weights > MAX_TOTAL_WEIGHTS
71 || self.total_weights > self.recovery_threshold * MAX_EXPANSION
72 {
73 return Err(VidError::InvalidParam);
74 }
75 Ok(())
76 }
77}
78
79#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq)]
81pub struct AvidmGf2Share {
82 range: Range<usize>,
84 #[serde(with = "nested_bytes")]
86 payload: Vec<Vec<u8>>,
87 mt_proofs: Vec<MerkleProof>,
89}
90
91mod nested_bytes {
93 use serde::{Deserialize, Deserializer, Serializer, ser::SerializeSeq};
94 use serde_bytes::{ByteBuf, Bytes};
95
96 pub fn serialize<S>(v: &[Vec<u8>], s: S) -> Result<S::Ok, S::Error>
97 where
98 S: Serializer,
99 {
100 let mut seq = s.serialize_seq(Some(v.len()))?;
101 for inner in v {
102 seq.serialize_element(Bytes::new(inner))?;
103 }
104 seq.end()
105 }
106
107 pub fn deserialize<'de, D>(d: D) -> Result<Vec<Vec<u8>>, D::Error>
108 where
109 D: Deserializer<'de>,
110 {
111 let v: Vec<ByteBuf> = Deserialize::deserialize(d)?;
112 Ok(v.into_iter().map(ByteBuf::into_vec).collect())
113 }
114}
115
116impl AvidmGf2Share {
117 pub fn weight(&self) -> usize {
119 self.range.len()
120 }
121
122 pub fn range(&self) -> &Range<usize> {
124 &self.range
125 }
126
127 pub fn payload(&self) -> &[Vec<u8>] {
129 &self.payload
130 }
131
132 pub fn mt_proofs(&self) -> &[MerkleProof] {
134 &self.mt_proofs
135 }
136
137 pub fn validate(&self) -> bool {
139 self.payload.len() == self.range.len() && self.mt_proofs.len() == self.range.len()
140 }
141
142 pub fn shard_bytes(&self) -> Option<usize> {
146 let first = self.payload.first()?.len();
147 self.payload
148 .iter()
149 .all(|shard| shard.len() == first)
150 .then_some(first)
151 }
152}
153
154#[derive(
156 Clone,
157 Copy,
158 Debug,
159 Default,
160 Hash,
161 CanonicalSerialize,
162 CanonicalDeserialize,
163 Eq,
164 PartialEq,
165 Ord,
166 PartialOrd,
167)]
168#[tagged("AvidmGf2Commit")]
169#[repr(C)]
170pub struct AvidmGf2Commit {
171 pub commit: MerkleCommit,
173}
174
175impl AsRef<[u8]> for AvidmGf2Commit {
176 fn as_ref(&self) -> &[u8] {
177 self.commit.as_ref()
178 }
179}
180
181impl AsRef<[u8; 32]> for AvidmGf2Commit {
182 fn as_ref(&self) -> &[u8; 32] {
183 <Self as AsRef<[u8]>>::as_ref(self)
184 .try_into()
185 .expect("AvidmGf2Commit is always 32 bytes")
186 }
187}
188
189impl AvidmGf2Scheme {
190 pub fn setup(recovery_threshold: usize, total_weights: usize) -> VidResult<AvidmGf2Param> {
192 AvidmGf2Param::new(recovery_threshold, total_weights)
193 }
194
195 pub fn shard_bytes(payload_byte_len: usize, original_count: usize) -> Option<usize> {
203 if original_count == 0 {
204 return None;
205 }
206 payload_byte_len
207 .checked_add(1)?
208 .div_ceil(original_count)
209 .checked_next_multiple_of(2)
210 }
211
212 fn payload_shard_bytes(param: &AvidmGf2Param, payload_len: usize) -> VidResult<usize> {
213 Self::shard_bytes(payload_len, param.recovery_threshold).ok_or_else(|| {
214 VidError::Argument("Payload length is too large to disperse".to_string())
215 })
216 }
217
218 fn chunk_and_pad(
224 payload: &[u8],
225 shard_bytes: usize,
226 original_count: usize,
227 ) -> VidResult<Vec<Cow<'_, [u8]>>> {
228 let padded_len = shard_bytes * original_count;
229 if padded_len < payload.len() + 1 {
230 return Err(VidError::Argument(
231 "Payload length is too large to fit in the given payload length".to_string(),
232 ));
233 }
234 let mut original: Vec<Cow<[u8]>> = Vec::with_capacity(original_count);
235 for i in 0..original_count {
236 let start = i * shard_bytes;
237 let end = start + shard_bytes;
238 if end <= payload.len() {
239 original.push(Cow::Borrowed(&payload[start..end]));
240 continue;
241 }
242 let mut chunk = vec![0u8; shard_bytes];
243 if start < payload.len() {
244 let take = payload.len() - start;
245 chunk[..take].copy_from_slice(&payload[start..]);
246 chunk[take] = 1u8;
248 } else if start == payload.len() {
249 chunk[0] = 1u8;
252 }
253 original.push(Cow::Owned(chunk));
254 }
255 Ok(original)
256 }
257
258 fn encode_recovery(
260 param: &AvidmGf2Param,
261 original: &[Cow<'_, [u8]>],
262 ) -> VidResult<Vec<Vec<u8>>> {
263 let recovery_count = param.total_weights - param.recovery_threshold;
264 if recovery_count == 0 {
265 return Ok(vec![]);
266 }
267 Ok(reed_solomon_simd::encode(
268 param.recovery_threshold,
269 recovery_count,
270 original,
271 )?)
272 }
273
274 fn merkle_tree<T: AsRef<[u8]> + Sync>(shards: &[T]) -> VidResult<MerkleTree> {
275 let share_digests: Vec<Blake3Node> = shards
276 .par_iter()
277 .map(|share| Blake3Node::from(blake3::hash(share.as_ref())))
278 .collect();
279 Ok(MerkleTree::from_elems(None, &share_digests)?)
280 }
281
282 fn raw_commit(param: &AvidmGf2Param, payload: &[u8]) -> VidResult<MerkleTree> {
284 let original_count = param.recovery_threshold;
285 let recovery_count = param.total_weights - original_count;
286 let shard_bytes = Self::payload_shard_bytes(param, payload.len())?;
287 let original = Self::chunk_and_pad(payload, shard_bytes, original_count)?;
288 if recovery_count == 0 {
289 return Self::merkle_tree(&original);
290 }
291 let mut encoder = ReedSolomonEncoder::new(original_count, recovery_count, shard_bytes)?;
292 for shard in &original {
293 encoder.add_original_shard(shard)?;
294 }
295 let result = encoder.encode()?;
296 let shards: Vec<&[u8]> = original
297 .iter()
298 .map(AsRef::as_ref)
299 .chain(result.recovery_iter())
300 .collect();
301 Self::merkle_tree(&shards)
302 }
303
304 fn raw_disperse(
305 param: &AvidmGf2Param,
306 payload: &[u8],
307 ) -> VidResult<(MerkleTree, Vec<Vec<u8>>)> {
308 let shard_bytes = Self::payload_shard_bytes(param, payload.len())?;
309 let original = Self::chunk_and_pad(payload, shard_bytes, param.recovery_threshold)?;
310 let recovery = Self::encode_recovery(param, &original)?;
311
312 let mut shares: Vec<Vec<u8>> = original.into_iter().map(Cow::into_owned).collect();
313 shares.extend(recovery);
314 let mt = Self::merkle_tree(&shares)?;
315 Ok((mt, shares))
316 }
317
318 #[cfg(any(test, feature = "testing"))]
329 pub fn disperse_non_codeword(
330 param: &AvidmGf2Param,
331 distribution: &[u32],
332 payload: &[u8],
333 ) -> VidResult<(AvidmGf2Commit, Vec<AvidmGf2Share>)> {
334 param.validate()?;
335 let total_weights = distribution.iter().map(|&w| w as usize).sum::<usize>();
336 if total_weights != param.total_weights {
337 return Err(VidError::Argument(
338 "Weight distribution is inconsistent with the given param".to_string(),
339 ));
340 }
341 if distribution.contains(&0u32) {
342 return Err(VidError::Argument("Weight cannot be zero".to_string()));
343 }
344 let original_count = param.recovery_threshold;
345 let (_, mut shards) = Self::raw_disperse(param, payload)?;
346 if shards.len() <= original_count {
347 return Err(VidError::Argument(
348 "Payload has no recovery shards to corrupt".to_string(),
349 ));
350 }
351 for shard in &mut shards[original_count..] {
356 shard[0] ^= 0xff;
357 }
358 let share_digests: Vec<Blake3Node> = shards
359 .iter()
360 .map(|shard| Blake3Node::from(blake3::hash(shard)))
361 .collect();
362 let mt = MerkleTree::from_elems(None, &share_digests)?;
363 let commit = AvidmGf2Commit {
364 commit: mt.commitment(),
365 };
366 let ranges: Vec<_> = distribution
367 .iter()
368 .scan(0usize, |sum, w| {
369 let prefix_sum = *sum;
370 *sum += *w as usize;
371 Some(prefix_sum..*sum)
372 })
373 .collect();
374 let mut shards_iter = shards.into_iter();
375 let payloads: Vec<Vec<Vec<u8>>> = ranges
376 .iter()
377 .map(|range| shards_iter.by_ref().take(range.len()).collect())
378 .collect();
379 let mut proofs_iter = mt
380 .collect_leaves_with_proofs()
381 .into_iter()
382 .map(|(_, _, proof)| proof);
383 let proof_groups: Vec<Vec<MerkleProof>> = ranges
384 .iter()
385 .map(|range| proofs_iter.by_ref().take(range.len()).collect())
386 .collect();
387 let shares: Vec<_> = ranges
388 .into_iter()
389 .zip(payloads)
390 .zip(proof_groups)
391 .map(|((range, payload), mt_proofs)| AvidmGf2Share {
392 range,
393 payload,
394 mt_proofs,
395 })
396 .collect();
397 Ok((commit, shares))
398 }
399}
400
401impl VidScheme for AvidmGf2Scheme {
402 type Param = AvidmGf2Param;
403 type Share = AvidmGf2Share;
404 type Commit = AvidmGf2Commit;
405
406 fn commit(param: &Self::Param, payload: &[u8]) -> VidResult<Self::Commit> {
407 param.validate()?;
408 let mt = Self::raw_commit(param, payload)?;
409 Ok(Self::Commit {
410 commit: mt.commitment(),
411 })
412 }
413
414 fn disperse(
415 param: &Self::Param,
416 distribution: &[u32],
417 payload: &[u8],
418 ) -> VidResult<(Self::Commit, Vec<Self::Share>)> {
419 param.validate()?;
420 let total_weights = distribution.iter().map(|&w| w as usize).sum::<usize>();
421 if total_weights != param.total_weights {
422 return Err(VidError::Argument(
423 "Weight distribution is inconsistent with the given param".to_string(),
424 ));
425 }
426 if distribution.contains(&0u32) {
427 return Err(VidError::Argument("Weight cannot be zero".to_string()));
428 }
429 let (mt, shares) = Self::raw_disperse(param, payload)?;
430 let commit = AvidmGf2Commit {
431 commit: mt.commitment(),
432 };
433
434 let ranges: Vec<_> = distribution
435 .iter()
436 .scan(0usize, |sum, w| {
437 let prefix_sum = *sum;
438 *sum += *w as usize;
439 Some(prefix_sum..*sum)
440 })
441 .collect();
442 let mut shares_iter = shares.into_iter();
451 let payloads: Vec<Vec<Vec<u8>>> = ranges
452 .iter()
453 .map(|range| shares_iter.by_ref().take(range.len()).collect())
454 .collect();
455 let mut proofs_iter = mt
456 .collect_leaves_with_proofs()
457 .into_iter()
458 .map(|(_, _, proof)| proof);
459 let proof_groups: Vec<Vec<MerkleProof>> = ranges
460 .iter()
461 .map(|range| proofs_iter.by_ref().take(range.len()).collect())
462 .collect();
463 let shares: Vec<_> = ranges
467 .into_iter()
468 .zip(payloads)
469 .zip(proof_groups)
470 .map(|((range, payload), mt_proofs)| AvidmGf2Share {
471 range,
472 payload,
473 mt_proofs,
474 })
475 .collect();
476 Ok((commit, shares))
477 }
478
479 fn verify_share(
480 param: &Self::Param,
481 commit: &Self::Commit,
482 share: &Self::Share,
483 ) -> VidResult<crate::VerificationResult> {
484 param.validate()?;
485 if !share.validate() || share.range.is_empty() || share.range.end > param.total_weights {
486 return Err(VidError::InvalidShare);
487 }
488 let start = share.range.start;
492 let len = share.range.end - start;
493 match (0..len)
494 .into_par_iter()
495 .map(|i| -> VidResult<crate::VerificationResult> {
496 let payload_digest = Blake3Node::from(blake3::hash(&share.payload[i]));
497 MerkleTree::verify(
498 commit.commit,
499 (start + i) as u64,
500 payload_digest,
501 &share.mt_proofs[i],
502 )
503 .map_err(VidError::from)
504 })
505 .find_any(|r| !matches!(r, Ok(Ok(()))))
506 {
507 None => Ok(Ok(())),
508 Some(Ok(v)) => Ok(v),
509 Some(Err(e)) => Err(e),
510 }
511 }
512
513 fn recover(
514 param: &Self::Param,
515 _commit: &Self::Commit,
516 shares: &[Self::Share],
517 ) -> VidResult<Vec<u8>> {
518 param.validate()?;
519 let original_count = param.recovery_threshold;
520 let recovery_count = param.total_weights - param.recovery_threshold;
521 let Some(first_share) = shares.iter().find(|s| !s.payload.is_empty()) else {
523 return Err(VidError::InsufficientShares);
524 };
525 let shard_bytes = first_share.payload[0].len();
526 if shares
530 .iter()
531 .any(|share| !share.validate() || share.payload.iter().any(|p| p.len() != shard_bytes))
532 {
533 return Err(VidError::InvalidShare);
534 }
535 if shares
536 .iter()
537 .map(|share| share.payload.len())
538 .sum::<usize>()
539 < original_count
540 {
541 return Err(VidError::InsufficientShares);
542 }
543
544 let mut input_orig: Vec<Option<&[u8]>> = vec![None; original_count];
549
550 let mut recovered: Vec<u8> = Vec::with_capacity(original_count * shard_bytes);
551 if recovery_count == 0 {
552 for share in shares {
555 for (i, index) in share.range.clone().enumerate() {
556 if index < original_count {
557 input_orig[index] = Some(&share.payload[i]);
558 }
559 }
560 }
561 for slot in &input_orig {
562 let shard = slot
563 .ok_or_else(|| VidError::Internal(anyhow!("Failed to recover the payload.")))?;
564 recovered.extend_from_slice(shard);
565 }
566 } else {
567 let mut decoder = reed_solomon_simd::ReedSolomonDecoder::new(
568 original_count,
569 recovery_count,
570 shard_bytes,
571 )?;
572 for share in shares {
573 for (i, index) in share.range.clone().enumerate() {
574 let shard = &share.payload[i];
575 if index < original_count {
576 input_orig[index] = Some(shard);
577 decoder.add_original_shard(index, shard)?;
578 } else {
579 decoder.add_recovery_shard(index - original_count, shard)?;
580 }
581 }
582 }
583
584 let result = decoder.decode()?;
585 for (i, shard) in input_orig.iter().enumerate().take(original_count) {
586 let shard: &[u8] = match shard {
587 Some(data) => data,
588 None => result.restored_original(i).ok_or_else(|| {
589 VidError::Internal(anyhow!("Failed to recover the payload."))
590 })?,
591 };
592 recovered.extend_from_slice(shard);
593 }
594 }
595 match recovered.iter().rposition(|&b| b != 0) {
596 Some(pad_index) if recovered[pad_index] == 1u8 => {
597 recovered.truncate(pad_index);
598 Ok(recovered)
599 },
600 _ => Err(VidError::Argument(
601 "Malformed payload, cannot find the padding position".to_string(),
602 )),
603 }
604 }
605}
606
607#[cfg(test)]
609pub mod tests {
610 use rand::{RngCore, seq::SliceRandom};
611
612 use super::{AvidmGf2Param, AvidmGf2Scheme};
613 use crate::{VidError, VidScheme};
614
615 fn output_digest(rt: usize, tw: usize, len: usize) -> String {
617 let payload: Vec<u8> = (0..len).map(|i| (i * 31 + 7) as u8).collect();
618 let param = AvidmGf2Scheme::setup(rt, tw).unwrap();
619 let commit = AvidmGf2Scheme::commit(¶m, &payload).unwrap();
620 let distribution: Vec<u32> = (0..tw).map(|_| 1).collect();
621 let (dcommit, shares) = AvidmGf2Scheme::disperse(¶m, &distribution, &payload).unwrap();
622 assert_eq!(commit, dcommit);
623 let mut hasher = blake3::Hasher::new();
624 hasher.update(commit.as_ref());
625 for share in &shares {
626 hasher.update(&share.range().start.to_le_bytes());
627 hasher.update(&share.range().end.to_le_bytes());
628 for shard in share.payload() {
629 hasher.update(&(shard.len() as u64).to_le_bytes());
630 hasher.update(shard);
631 }
632 }
633 hasher.finalize().to_hex().to_string()
634 }
635
636 #[test]
637 fn outputs_are_pinned() {
638 let mut out = String::new();
639 for (rt, tw) in [(1, 1), (1, 3), (3, 10), (4, 12), (5, 5), (7, 20)] {
640 for len in [0, 1, 31, 32, 33, 63, 64, 65, 1000, 4097, 100_000] {
641 out += &format!("{rt} {tw} {len} {}\n", output_digest(rt, tw, len));
642 }
643 }
644 let got = blake3::hash(out.as_bytes()).to_hex().to_string();
645 assert_eq!(
646 got, "3c96b583adfdf90d15d80c117f9ab76625995e851743204c4e9c5725b5b7950a",
647 "{out}"
648 );
649 }
650
651 #[test]
652 fn round_trip() {
653 let num_storage_nodes_list = [4, 9, 16];
655 let payload_byte_lens = [1, 31, 32, 500];
656
657 let mut rng = jf_utils::test_rng();
660
661 for num_storage_nodes in num_storage_nodes_list {
662 let weights: Vec<u32> = (0..num_storage_nodes)
663 .map(|_| rng.next_u32() % 5 + 1)
664 .collect();
665 let total_weights: u32 = weights.iter().sum();
666 let recovery_threshold = total_weights.div_ceil(3) as usize;
667 let params = AvidmGf2Scheme::setup(recovery_threshold, total_weights as usize).unwrap();
668
669 for payload_byte_len in payload_byte_lens {
670 let payload = {
671 let mut bytes_random = vec![0u8; payload_byte_len];
672 rng.fill_bytes(&mut bytes_random);
673 bytes_random
674 };
675
676 let (commit, mut shares) =
677 AvidmGf2Scheme::disperse(¶ms, &weights, &payload).unwrap();
678
679 assert_eq!(shares.len(), num_storage_nodes);
680
681 shares.iter().for_each(|share| {
683 assert!(
684 AvidmGf2Scheme::verify_share(¶ms, &commit, share)
685 .is_ok_and(|r| r.is_ok())
686 )
687 });
688
689 shares.shuffle(&mut rng);
691 let mut cumulated_weights = 0;
692 let mut cut_index = 0;
693 while cumulated_weights < recovery_threshold {
694 cumulated_weights += shares[cut_index].weight();
695 cut_index += 1;
696 }
697 let payload_recovered =
698 AvidmGf2Scheme::recover(¶ms, &commit, &shares[..cut_index]).unwrap();
699 assert_eq!(payload_recovered, payload);
700 }
701 }
702 }
703
704 #[test]
705 fn round_trip_edge_case() {
706 let num_storage_nodes_list = [4, 9, 16];
708 let payload_byte_lens = [1, 31, 32, 500];
709
710 let mut rng = jf_utils::test_rng();
713
714 for num_storage_nodes in num_storage_nodes_list {
715 let weights: Vec<u32> = (0..num_storage_nodes)
716 .map(|_| rng.next_u32() % 5 + 1)
717 .collect();
718 let total_weights: u32 = weights.iter().sum();
719 let recovery_threshold = total_weights as usize;
720 let params = AvidmGf2Scheme::setup(recovery_threshold, total_weights as usize).unwrap();
721
722 for payload_byte_len in payload_byte_lens {
723 let payload = {
724 let mut bytes_random = vec![0u8; payload_byte_len];
725 rng.fill_bytes(&mut bytes_random);
726 bytes_random
727 };
728
729 let (commit, mut shares) =
730 AvidmGf2Scheme::disperse(¶ms, &weights, &payload).unwrap();
731
732 assert_eq!(shares.len(), num_storage_nodes);
733
734 shares.iter().for_each(|share| {
736 assert!(
737 AvidmGf2Scheme::verify_share(¶ms, &commit, share)
738 .is_ok_and(|r| r.is_ok())
739 )
740 });
741
742 shares.shuffle(&mut rng);
744 let payload_recovered =
745 AvidmGf2Scheme::recover(¶ms, &commit, &shares[..]).unwrap();
746 assert_eq!(payload_recovered, payload);
747 }
748 }
749 }
750
751 #[test]
755 fn shard_bytes_matches_dispersal() {
756 let total_weights = 10usize;
757 let recovery_threshold = 4;
758 let params = AvidmGf2Scheme::setup(recovery_threshold, total_weights).unwrap();
759 let weights = vec![2u32; 5];
760
761 for payload_byte_len in [0usize, 1, 3, 4, 7, 8, 31, 32, 100, 101] {
762 let payload = vec![1u8; payload_byte_len];
763 let expected =
764 AvidmGf2Scheme::shard_bytes(payload_byte_len, recovery_threshold).unwrap();
765 assert_eq!(expected % 2, 0, "{payload_byte_len}");
766 assert!(
767 expected * recovery_threshold > payload_byte_len,
768 "{payload_byte_len}: shards must hold the payload and its pad byte"
769 );
770 assert!(
771 (expected - 2) * recovery_threshold <= payload_byte_len,
772 "{payload_byte_len}: shards must not be larger than needed"
773 );
774 let (_, shares) = AvidmGf2Scheme::disperse(¶ms, &weights, &payload).unwrap();
775 for share in &shares {
776 assert_eq!(share.shard_bytes(), Some(expected), "{payload_byte_len}");
777 }
778 }
779
780 assert_eq!(AvidmGf2Scheme::shard_bytes(100, 0), None);
782 assert_eq!(AvidmGf2Scheme::shard_bytes(usize::MAX, 1), None);
783 assert_eq!(AvidmGf2Scheme::shard_bytes(usize::MAX - 1, 1), None);
784 }
785
786 #[test]
787 fn disperse_rejects_inconsistent_distribution() {
788 let total_weights = 10usize;
789 let recovery_threshold = 4;
790 let params = AvidmGf2Scheme::setup(recovery_threshold, total_weights).unwrap();
791 let payload = vec![1u8; 100];
792
793 let bad_weights = vec![3u32; 4];
795 assert!(
796 AvidmGf2Scheme::disperse(¶ms, &bad_weights, &payload).is_err(),
797 "disperse should reject distribution that doesn't sum to total_weights"
798 );
799
800 let zero_weight = vec![0u32, 5, 5];
802 assert!(
803 AvidmGf2Scheme::disperse(¶ms, &zero_weight, &payload).is_err(),
804 "disperse should reject zero-weight entries"
805 );
806
807 let good_weights = vec![2u32; 5];
809 assert!(AvidmGf2Scheme::disperse(¶ms, &good_weights, &payload).is_ok());
810 }
811
812 #[test]
813 fn verify_share_rejects_out_of_range() {
814 let total_weights = 10usize;
815 let recovery_threshold = 4;
816 let params = AvidmGf2Scheme::setup(recovery_threshold, total_weights).unwrap();
817 let payload = vec![1u8; 100];
818 let weights = vec![2u32; 5];
819
820 let (commit, shares) = AvidmGf2Scheme::disperse(¶ms, &weights, &payload).unwrap();
821
822 for share in &shares {
824 assert!(AvidmGf2Scheme::verify_share(¶ms, &commit, share).is_ok_and(|r| r.is_ok()));
825 }
826
827 let smaller_params = AvidmGf2Scheme::setup(2, 5).unwrap();
829 let last_share = shares.last().unwrap();
830 assert!(
831 AvidmGf2Scheme::verify_share(&smaller_params, &commit, last_share).is_err(),
832 "verify_share should reject share with range.end > param.total_weights"
833 );
834 }
835
836 #[test]
837 fn param_bounds_are_inclusive() {
838 for (recovery_threshold, total_weights) in [(1, 1), (1 << 14, 1 << 16), (2, 8)] {
839 assert!(
840 AvidmGf2Param::new(recovery_threshold, total_weights).is_ok(),
841 "({recovery_threshold}, {total_weights})"
842 );
843 }
844 for (recovery_threshold, total_weights) in
845 [(0, 4), (3, 2), ((1 << 14) + 1, (1 << 16) + 1), (2, 9)]
846 {
847 assert!(
848 matches!(
849 AvidmGf2Param::new(recovery_threshold, total_weights),
850 Err(VidError::InvalidParam)
851 ),
852 "({recovery_threshold}, {total_weights})"
853 );
854 }
855 }
856
857 #[test]
858 fn invalid_param_is_rejected() {
859 let param = AvidmGf2Scheme::setup(3, 9).unwrap();
860 let payload = [7u8; 100];
861 let (commit, shares) = AvidmGf2Scheme::disperse(¶m, &[3, 3, 3], &payload).unwrap();
862
863 for (recovery_threshold, total_weights) in
864 [(0, 4), (3, 2), (1 << 15, 1 << 17), (1, 1 << 16)]
865 {
866 let forged = AvidmGf2Param {
867 total_weights,
868 recovery_threshold,
869 };
870 assert!(
871 matches!(
872 AvidmGf2Scheme::recover(&forged, &commit, &shares),
873 Err(VidError::InvalidParam)
874 ),
875 "recover {forged:?}"
876 );
877 assert!(
878 matches!(
879 AvidmGf2Scheme::commit(&forged, &payload),
880 Err(VidError::InvalidParam)
881 ),
882 "commit {forged:?}"
883 );
884 assert!(
885 matches!(
886 AvidmGf2Scheme::disperse(&forged, &[1], &payload),
887 Err(VidError::InvalidParam)
888 ),
889 "disperse {forged:?}"
890 );
891 assert!(
892 matches!(
893 AvidmGf2Scheme::verify_share(&forged, &commit, &shares[0]),
894 Err(VidError::InvalidParam)
895 ),
896 "verify_share {forged:?}"
897 );
898 }
899 }
900
901 #[test]
902 fn recover_rejects_too_few_shards_for_a_large_threshold() {
903 let param = AvidmGf2Scheme::setup(3, 9).unwrap();
904 let (commit, shares) = AvidmGf2Scheme::disperse(¶m, &[3, 3, 3], &[7u8; 100]).unwrap();
905 let forged = AvidmGf2Param::new(1 << 14, 1 << 16).unwrap();
906
907 assert!(matches!(
908 AvidmGf2Scheme::recover(&forged, &commit, &shares[..1]),
909 Err(VidError::InsufficientShares)
910 ));
911 }
912}