1use crate::{Config, Scheme};
2use bytes::{Buf, BufMut, Bytes};
3use commonware_codec::{BufsMut, EncodeSize, FixedSize, RangeCfg, Read, ReadExt, Write};
4use commonware_cryptography::{
5 reed_solomon::{Decoder, Encoder, Error as RsError, SHARD_CHUNK_BYTES},
6 Digest, Hasher,
7};
8use commonware_parallel::Strategy;
9use commonware_storage::bmt::{self, Builder};
10use commonware_utils::Cached;
11use std::{marker::PhantomData, ops::Range};
12use thiserror::Error;
13
14commonware_utils::thread_local_cache!(static CACHED_ENCODER: Encoder);
19commonware_utils::thread_local_cache!(static CACHED_DECODER: Decoder);
20
21const MIN_STRIPE_BYTES: usize = 8 * 1024;
23
24#[derive(Error, Debug)]
26pub enum Error {
27 #[error("reed-solomon error: {0}")]
28 ReedSolomon(#[from] RsError),
29 #[error("inconsistent")]
30 Inconsistent,
31 #[error("invalid proof")]
32 InvalidProof,
33 #[error("not enough chunks")]
34 NotEnoughChunks,
35 #[error("duplicate chunk index: {0}")]
36 DuplicateIndex(u16),
37 #[error("invalid data length: {0}")]
38 InvalidDataLength(usize),
39 #[error("invalid index: {0}")]
40 InvalidIndex(u16),
41 #[error("too many total shards: {0}")]
42 TooManyTotalShards(u32),
43 #[error("checked shard commitment does not match decode commitment")]
44 CommitmentMismatch,
45}
46
47fn total_shards(config: &Config) -> Result<u16, Error> {
48 let total = config.total_shards();
49 total
50 .try_into()
51 .map_err(|_| Error::TooManyTotalShards(total))
52}
53
54#[derive(Debug, Clone)]
56pub struct Chunk<D: Digest> {
57 shard: Bytes,
59
60 index: u16,
62
63 proof: bmt::Proof<D>,
65}
66
67impl<D: Digest> Chunk<D> {
68 const fn new(shard: Bytes, index: u16, proof: bmt::Proof<D>) -> Self {
70 Self {
71 shard,
72 index,
73 proof,
74 }
75 }
76
77 fn verify<H: Hasher<Digest = D>>(&self, index: u16, root: &D) -> Option<CheckedChunk<D>> {
79 if index != self.index {
81 return None;
82 }
83
84 let mut hasher = H::new();
86 hasher.update(&self.shard);
87 let shard_digest = hasher.finalize();
88
89 self.proof
91 .verify_element_inclusion(&mut hasher, &shard_digest, self.index as u32, root)
92 .ok()?;
93
94 Some(CheckedChunk::new(
95 *root,
96 self.shard.clone(),
97 self.index,
98 shard_digest,
99 ))
100 }
101}
102
103#[derive(Clone, Debug, PartialEq, Eq)]
109pub struct CheckedChunk<D: Digest> {
110 root: D,
111 shard: Bytes,
112 index: u16,
113 digest: D,
114}
115
116impl<D: Digest> CheckedChunk<D> {
117 const fn new(root: D, shard: Bytes, index: u16, digest: D) -> Self {
118 Self {
119 root,
120 shard,
121 index,
122 digest,
123 }
124 }
125}
126
127impl<D: Digest> Write for Chunk<D> {
128 fn write(&self, writer: &mut impl BufMut) {
129 self.shard.write(writer);
130 self.index.write(writer);
131 self.proof.write(writer);
132 }
133
134 fn write_bufs(&self, buf: &mut impl BufsMut) {
135 self.shard.write_bufs(buf);
136 self.index.write(buf);
137 self.proof.write(buf);
138 }
139}
140
141impl<D: Digest> Read for Chunk<D> {
142 type Cfg = crate::CodecConfig;
144
145 fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
146 let shard = Bytes::read_cfg(reader, &RangeCfg::new(..=cfg.maximum_shard_size))?;
147 let index = u16::read(reader)?;
148 let proof = bmt::Proof::<D>::read_cfg(reader, &1)?;
149 Ok(Self {
150 shard,
151 index,
152 proof,
153 })
154 }
155}
156
157impl<D: Digest> EncodeSize for Chunk<D> {
158 fn encode_size(&self) -> usize {
159 self.shard.encode_size() + self.index.encode_size() + self.proof.encode_size()
160 }
161
162 fn encode_inline_size(&self) -> usize {
163 self.shard.encode_inline_size() + self.index.encode_size() + self.proof.encode_size()
164 }
165}
166
167impl<D: Digest> PartialEq for Chunk<D> {
168 fn eq(&self, other: &Self) -> bool {
169 self.shard == other.shard && self.index == other.index && self.proof == other.proof
170 }
171}
172
173impl<D: Digest> Eq for Chunk<D> {}
174
175#[cfg(feature = "arbitrary")]
176impl<D: Digest> arbitrary::Arbitrary<'_> for Chunk<D>
177where
178 D: for<'a> arbitrary::Arbitrary<'a>,
179{
180 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
181 Ok(Self {
182 shard: u.arbitrary::<Vec<u8>>()?.into(),
183 index: u.arbitrary()?,
184 proof: u.arbitrary()?,
185 })
186 }
187}
188
189fn prepare_data(mut data: impl Buf, k: usize) -> (Vec<u8>, usize) {
195 let data_len = data.remaining();
197 let shard_len = canonical_shard_len(data_len, k);
198
199 let length_bytes = (data_len as u32).to_be_bytes();
201 let mut padded = vec![0u8; k * shard_len];
202 padded[..u32::SIZE].copy_from_slice(&length_bytes);
203 data.copy_to_slice(&mut padded[u32::SIZE..u32::SIZE + data_len]);
204
205 (padded, shard_len)
206}
207
208const fn canonical_shard_len(data_len: usize, k: usize) -> usize {
215 let prefixed_len = u32::SIZE + data_len;
216 let mut shard_len = prefixed_len.div_ceil(k);
217
218 if !shard_len.is_multiple_of(2) {
220 shard_len += 1;
221 }
222
223 shard_len
224}
225
226fn extract_data(shards: &[&[u8]], k: usize, expected_shard_len: usize) -> Result<Vec<u8>, Error> {
232 let shards = shards.get(..k).ok_or(Error::NotEnoughChunks)?;
233 let data_len = read_data_len(shards)?;
234 let mut data = Vec::with_capacity(data_len);
235 let mut prefix_bytes_left = u32::SIZE;
236 let mut data_bytes_left = data_len;
237 for shard in shards {
238 if prefix_bytes_left >= shard.len() {
241 prefix_bytes_left -= shard.len();
242 continue;
243 }
244
245 let payload = &shard[prefix_bytes_left..];
247 let copy_len = data_bytes_left.min(payload.len());
248 data.extend_from_slice(&payload[..copy_len]);
249 data_bytes_left -= copy_len;
250
251 if !payload[copy_len..].iter().all(|byte| *byte == 0) {
253 return Err(Error::Inconsistent);
254 }
255 prefix_bytes_left = 0;
256 }
257
258 if data_bytes_left != 0 {
261 return Err(Error::Inconsistent);
262 }
263
264 if canonical_shard_len(data.len(), k) != expected_shard_len {
266 return Err(Error::Inconsistent);
267 }
268 Ok(data)
269}
270
271fn read_data_len(shards: &[&[u8]]) -> Result<usize, Error> {
274 let total_len: usize = shards.iter().map(|s| s.len()).sum();
275 if total_len < u32::SIZE {
276 return Err(Error::Inconsistent);
277 }
278
279 let mut prefix = [0u8; u32::SIZE];
281 let mut prefix_len = 0usize;
282 for shard in shards {
283 if prefix_len == u32::SIZE {
284 break;
285 }
286 let read = (u32::SIZE - prefix_len).min(shard.len());
287 prefix[prefix_len..prefix_len + read].copy_from_slice(&shard[..read]);
288 prefix_len += read;
289 }
290
291 let data_len = u32::from_be_bytes(prefix) as usize;
292 let payload_len = total_len - u32::SIZE;
293 if data_len > payload_len {
294 return Err(Error::Inconsistent);
295 }
296 Ok(data_len)
297}
298
299type Encoding<D> = (D, Vec<Chunk<D>>);
301
302fn encode<H: Hasher, S: Strategy>(
316 total: u16,
317 min: u16,
318 data: impl Buf,
319 strategy: &S,
320) -> Result<Encoding<H::Digest>, Error> {
321 assert!(total > min);
323 assert!(min > 0);
324 let n = total as usize;
325 let k = min as usize;
326 let m = n - k;
327 let data_len = data.remaining();
328 if data_len > u32::MAX as usize {
329 return Err(Error::InvalidDataLength(data_len));
330 }
331
332 let (padded, shard_len) = prepare_data(data, k);
334
335 let manual = strategy.manual();
337 let recovery_buf = match striped::ranges(shard_len, manual.parallelism()) {
338 Some(ranges) => {
339 let original_shards = padded.chunks(shard_len).collect::<Vec<_>>();
340 let mut buf = vec![0u8; m * shard_len];
341 let groups = striped::stripe_columns(&mut buf, shard_len, &ranges);
342 let stripes: Vec<_> = ranges.into_iter().zip(groups).collect();
343 manual.try_map_collect_vec(stripes, |(range, out)| {
344 striped::encode_recovery_into(k, m, range, &original_shards, out)
345 })?;
346 buf
347 }
348 None => {
349 let mut encoder = Cached::take(
350 &CACHED_ENCODER,
351 || Encoder::new(k, m, shard_len),
352 |enc| enc.reset(k, m, shard_len),
353 )
354 .map_err(Error::ReedSolomon)?;
355 for shard in padded.chunks(shard_len) {
356 encoder
357 .add_original_shard(shard)
358 .map_err(Error::ReedSolomon)?;
359 }
360
361 let encoding = encoder.encode().map_err(Error::ReedSolomon)?;
363 let mut buf = Vec::with_capacity(m * shard_len);
364 for shard in encoding.recovery_iter() {
365 buf.extend_from_slice(shard);
366 }
367 buf
368 }
369 };
370
371 let originals: Bytes = padded.into();
373 let recoveries: Bytes = recovery_buf.into();
374
375 let mut builder = Builder::<H>::new(n);
377 let shard_slices: Vec<Bytes> = (0..k)
378 .map(|i| originals.slice(i * shard_len..(i + 1) * shard_len))
379 .chain((0..m).map(|i| recoveries.slice(i * shard_len..(i + 1) * shard_len)))
380 .collect();
381 let shard_hashes = strategy.map_init_collect_vec_with_multiplier(
382 &shard_slices,
383 shard_len,
384 H::new,
385 |hasher, shard| {
386 hasher.update(shard);
387 hasher.finalize()
388 },
389 );
390 for hash in &shard_hashes {
391 builder.add(hash);
392 }
393 let tree = builder.build();
394 let root = tree.root();
395
396 let mut chunks = Vec::with_capacity(n);
398 for (i, shard) in shard_slices.into_iter().enumerate() {
399 let proof = tree.proof(i as u32).map_err(|_| Error::InvalidProof)?;
400 chunks.push(Chunk::new(shard, i as u16, proof));
401 }
402
403 Ok((root, chunks))
404}
405
406struct DecodeCtx<'a, H: Hasher, S: Strategy> {
408 n: usize,
410 k: usize,
412 m: usize,
414 shard_len: usize,
416 root: &'a H::Digest,
418 strategy: &'a S,
420}
421
422mod striped {
444 use super::*;
445
446 pub(super) fn stripe_columns<'a>(
451 buf: &'a mut [u8],
452 shard_len: usize,
453 ranges: &[Range<usize>],
454 ) -> Vec<Vec<&'a mut [u8]>> {
455 let mut groups: Vec<Vec<&'a mut [u8]>> = ranges.iter().map(|_| Vec::new()).collect();
456 for shard in buf.chunks_mut(shard_len) {
457 let mut rest = shard;
458 for (group, range) in groups.iter_mut().zip(ranges) {
459 let (head, tail) = rest.split_at_mut(range.len());
460 group.push(head);
461 rest = tail;
462 }
463 }
464 groups
465 }
466
467 struct StripeOut<'a> {
470 originals: Vec<&'a mut [u8]>,
471 recoveries: Vec<&'a mut [u8]>,
472 }
473
474 #[derive(Clone, Copy)]
476 struct Missing<'a> {
477 originals: &'a [usize],
478 recoveries: &'a [usize],
479 }
480
481 pub(super) fn ranges(shard_len: usize, parallelism: usize) -> Option<Vec<Range<usize>>> {
489 let full_blocks = shard_len / SHARD_CHUNK_BYTES;
496 let stripe_count = parallelism
497 .min(shard_len / MIN_STRIPE_BYTES)
498 .min(full_blocks)
499 .max(1);
500 if stripe_count <= 1 {
501 return None;
502 }
503
504 let mut ranges = Vec::with_capacity(stripe_count);
505 let mut start = 0usize;
506 for stripe in 0..stripe_count {
507 let remaining_stripes = stripe_count - stripe;
508 let remaining = shard_len - start;
509 let len = if remaining_stripes == 1 {
510 remaining
511 } else {
512 let remaining_full_blocks = remaining / SHARD_CHUNK_BYTES;
513 (remaining_full_blocks / remaining_stripes).max(1) * SHARD_CHUNK_BYTES
514 };
515 let end = start + len;
516 ranges.push(start..end);
517 start = end;
518 }
519 Some(ranges)
520 }
521
522 fn recover_all_into(
527 k: usize,
528 m: usize,
529 range: Range<usize>,
530 provided_originals: &[(usize, &[u8])],
531 provided_recoveries: &[(usize, &[u8])],
532 missing: Missing<'_>,
533 mut out: StripeOut<'_>,
534 ) -> Result<(), Error> {
535 let shard_len = range.len();
536 let mut decoder = Cached::take(
537 &CACHED_DECODER,
538 || Decoder::new(k, m, shard_len),
539 |dec| dec.reset(k, m, shard_len),
540 )
541 .map_err(Error::ReedSolomon)?;
542
543 for (idx, shard) in provided_originals {
544 decoder
545 .add_original_shard(*idx, &shard[range.clone()])
546 .map_err(Error::ReedSolomon)?;
547 }
548 for (idx, shard) in provided_recoveries {
549 decoder
550 .add_recovery_shard(*idx, &shard[range.clone()])
551 .map_err(Error::ReedSolomon)?;
552 }
553 let decoding = decoder
554 .decode_with_recovery()
555 .map_err(Error::ReedSolomon)?
556 .expect("decode runs only when an original is missing");
557
558 for (slot, &idx) in out.originals.iter_mut().zip(missing.originals) {
559 let shard = decoding.original(idx).ok_or(Error::Inconsistent)?;
560 slot.copy_from_slice(shard);
561 }
562 for (slot, &idx) in out.recoveries.iter_mut().zip(missing.recoveries) {
563 let shard = decoding.recovery(idx).ok_or(Error::Inconsistent)?;
564 slot.copy_from_slice(shard);
565 }
566
567 Ok(())
568 }
569
570 pub(super) fn encode_recovery_into(
573 k: usize,
574 m: usize,
575 range: Range<usize>,
576 originals: &[impl AsRef<[u8]>],
577 mut out: Vec<&mut [u8]>,
578 ) -> Result<(), Error> {
579 let shard_len = range.len();
580 let mut encoder = Cached::take(
581 &CACHED_ENCODER,
582 || Encoder::new(k, m, shard_len),
583 |enc| enc.reset(k, m, shard_len),
584 )
585 .map_err(Error::ReedSolomon)?;
586
587 for shard in originals.iter().take(k) {
588 let shard = shard.as_ref();
589 encoder
590 .add_original_shard(&shard[range.clone()])
591 .map_err(Error::ReedSolomon)?;
592 }
593 let encoding = encoder.encode().map_err(Error::ReedSolomon)?;
594 for (slot, shard) in out.iter_mut().zip(encoding.recovery_iter()) {
595 slot.copy_from_slice(shard);
596 }
597
598 Ok(())
599 }
600
601 pub(super) fn decode<'a, H: Hasher, S: Strategy>(
605 ctx: &DecodeCtx<'_, H, S>,
606 ranges: Vec<Range<usize>>,
607 shard_digests: Vec<Option<H::Digest>>,
608 provided_originals: Vec<(usize, &'a [u8])>,
609 provided_recoveries: Vec<(usize, &'a [u8])>,
610 ) -> Result<Vec<u8>, Error> {
611 let &DecodeCtx {
612 k,
613 m,
614 shard_len,
615 strategy,
616 ..
617 } = ctx;
618 assert!(ranges.len() > 1);
619
620 let mut original_refs: Vec<&[u8]> = vec![&[]; k];
621 for &(idx, shard) in &provided_originals {
622 original_refs[idx] = shard;
623 }
624
625 let mut recovery_buf = vec![0u8; m * shard_len];
627 let groups = stripe_columns(&mut recovery_buf, shard_len, &ranges);
628 let stripes: Vec<_> = ranges.into_iter().zip(groups).collect();
629 strategy.try_map_collect_vec(stripes, |(range, out)| {
630 encode_recovery_into(k, m, range, &original_refs, out)
631 })?;
632 let recovery_refs: Vec<&[u8]> = recovery_buf.chunks_exact(shard_len).collect();
633 verify_reencoded::<H, S>(
634 ctx,
635 shard_digests,
636 &original_refs,
637 &recovery_refs,
638 &provided_recoveries,
639 )
640 }
641
642 pub(super) fn decode_reveal<'a, H: Hasher, S: Strategy>(
650 ctx: &DecodeCtx<'_, H, S>,
651 ranges: Vec<Range<usize>>,
652 shard_digests: Vec<Option<H::Digest>>,
653 provided_originals: Vec<(usize, &'a [u8])>,
654 provided_recoveries: Vec<(usize, &'a [u8])>,
655 ) -> Result<Vec<u8>, Error> {
656 let &DecodeCtx {
657 k,
658 m,
659 shard_len,
660 strategy,
661 ..
662 } = ctx;
663 assert!(ranges.len() > 1);
664
665 let missing_originals = shard_digests
666 .iter()
667 .take(k)
668 .enumerate()
669 .filter_map(|(i, digest)| digest.is_none().then_some(i))
670 .collect::<Vec<_>>();
671 let missing_recoveries = shard_digests
672 .iter()
673 .skip(k)
674 .enumerate()
675 .filter_map(|(i, digest)| digest.is_none().then_some(i))
676 .collect::<Vec<_>>();
677
678 let mut restored_originals = vec![0u8; missing_originals.len() * shard_len];
679 let mut restored_recoveries = vec![0u8; missing_recoveries.len() * shard_len];
680 let missing = Missing {
681 originals: &missing_originals,
682 recoveries: &missing_recoveries,
683 };
684 let original_groups = stripe_columns(&mut restored_originals, shard_len, &ranges);
685 let recovery_groups = stripe_columns(&mut restored_recoveries, shard_len, &ranges);
686 let stripes: Vec<_> = ranges
687 .into_iter()
688 .zip(original_groups.into_iter().zip(recovery_groups))
689 .map(|(range, (originals, recoveries))| {
690 (
691 range,
692 StripeOut {
693 originals,
694 recoveries,
695 },
696 )
697 })
698 .collect();
699 strategy.try_map_collect_vec(stripes, |(range, out)| {
700 recover_all_into(
701 k,
702 m,
703 range,
704 &provided_originals,
705 &provided_recoveries,
706 missing,
707 out,
708 )
709 })?;
710
711 let mut original_refs: Vec<&[u8]> = vec![&[]; k];
712 for &(idx, shard) in &provided_originals {
713 original_refs[idx] = shard;
714 }
715 for (pos, idx) in missing_originals.iter().enumerate() {
716 let start = pos * shard_len;
717 original_refs[*idx] = &restored_originals[start..start + shard_len];
718 }
719 let mut recovery_refs: Vec<&[u8]> = vec![&[]; m];
720 for (pos, idx) in missing_recoveries.iter().enumerate() {
721 let start = pos * shard_len;
722 recovery_refs[*idx] = &restored_recoveries[start..start + shard_len];
723 }
724
725 verify_root::<H, S>(ctx, shard_digests, &original_refs, &recovery_refs)
726 }
727}
728
729fn verify_root<H: Hasher, S: Strategy>(
734 ctx: &DecodeCtx<'_, H, S>,
735 mut shard_digests: Vec<Option<H::Digest>>,
736 originals: &[&[u8]],
737 recoveries: &[&[u8]],
738) -> Result<Vec<u8>, Error> {
739 let &DecodeCtx {
740 n,
741 k,
742 shard_len,
743 root,
744 strategy,
745 ..
746 } = ctx;
747 let data = extract_data(originals, k, shard_len)?;
748
749 let missing_shards = shard_digests
750 .iter()
751 .enumerate()
752 .filter(|(_, digest)| digest.is_none())
753 .map(|(i, _)| {
754 (
755 i,
756 if i < k {
757 originals[i]
758 } else {
759 recoveries[i - k]
760 },
761 )
762 })
763 .collect::<Vec<_>>();
764
765 for (i, digest) in strategy.map_init_collect_vec_with_multiplier(
766 missing_shards,
767 shard_len,
768 H::new,
769 |hasher, (i, shard)| {
770 hasher.update(shard);
771 (i, hasher.finalize())
772 },
773 ) {
774 shard_digests[i] = Some(digest);
775 }
776
777 let mut builder = Builder::<H>::new(n);
778 shard_digests
779 .into_iter()
780 .map(|digest| digest.expect("digest must be present for every shard"))
781 .for_each(|digest| {
782 builder.add(&digest);
783 });
784 let tree = builder.build();
785 if tree.root() != *root {
786 return Err(Error::Inconsistent);
787 }
788
789 Ok(data)
790}
791
792fn verify_reencoded<H: Hasher, S: Strategy>(
797 ctx: &DecodeCtx<'_, H, S>,
798 shard_digests: Vec<Option<H::Digest>>,
799 originals: &[&[u8]],
800 recoveries: &[&[u8]],
801 provided_recoveries: &[(usize, &[u8])],
802) -> Result<Vec<u8>, Error> {
803 for &(idx, shard) in provided_recoveries {
805 if shard != recoveries[idx] {
806 return Err(Error::Inconsistent);
807 }
808 }
809 verify_root::<H, S>(ctx, shard_digests, originals, recoveries)
810}
811
812mod sequential {
816 use super::*;
817
818 pub(super) fn decode<'a, H: Hasher, S: Strategy>(
821 ctx: &DecodeCtx<'_, H, S>,
822 shard_digests: Vec<Option<H::Digest>>,
823 provided_originals: Vec<(usize, &'a [u8])>,
824 provided_recoveries: Vec<(usize, &'a [u8])>,
825 ) -> Result<Vec<u8>, Error> {
826 let &DecodeCtx {
827 k, m, shard_len, ..
828 } = ctx;
829 if provided_originals.len() == k {
830 let mut shards: Vec<&[u8]> = vec![&[]; k];
833 for &(idx, shard) in &provided_originals {
834 shards[idx] = shard;
835 }
836 return verify_codeword::<H, S>(ctx, shard_digests, &provided_recoveries, &shards);
837 }
838
839 let mut decoder = Cached::take(
845 &CACHED_DECODER,
846 || Decoder::new(k, m, shard_len),
847 |dec| dec.reset(k, m, shard_len),
848 )
849 .map_err(Error::ReedSolomon)?;
850 for (idx, shard) in &provided_originals {
851 decoder
852 .add_original_shard(*idx, shard)
853 .map_err(Error::ReedSolomon)?;
854 }
855 for (idx, shard) in &provided_recoveries {
856 decoder
857 .add_recovery_shard(*idx, shard)
858 .map_err(Error::ReedSolomon)?;
859 }
860 let decoding = decoder
861 .decode_with_recovery()
862 .map_err(Error::ReedSolomon)?
863 .expect("decode runs only when an original is missing");
864
865 let mut originals: Vec<&[u8]> = vec![&[]; k];
866 for &(idx, shard) in &provided_originals {
867 originals[idx] = shard;
868 }
869 for (idx, shard) in decoding.original_iter() {
870 originals[idx] = shard;
871 }
872 let mut recoveries: Vec<&[u8]> = vec![&[]; m];
873 for &(idx, shard) in &provided_recoveries {
874 recoveries[idx] = shard;
875 }
876 for (idx, shard) in decoding.recovery_iter() {
877 recoveries[idx] = shard;
878 }
879
880 verify_root::<H, S>(ctx, shard_digests, &originals, &recoveries)
881 }
882
883 fn verify_codeword<H: Hasher, S: Strategy>(
886 ctx: &DecodeCtx<'_, H, S>,
887 shard_digests: Vec<Option<H::Digest>>,
888 provided_recoveries: &[(usize, &[u8])],
889 originals: &[&[u8]],
890 ) -> Result<Vec<u8>, Error> {
891 let &DecodeCtx {
892 k, m, shard_len, ..
893 } = ctx;
894 let mut encoder = Cached::take(
895 &CACHED_ENCODER,
896 || Encoder::new(k, m, shard_len),
897 |enc| enc.reset(k, m, shard_len),
898 )
899 .map_err(Error::ReedSolomon)?;
900 for shard in originals.iter().take(k) {
901 encoder
902 .add_original_shard(shard)
903 .map_err(Error::ReedSolomon)?;
904 }
905 let encoding = encoder.encode().map_err(Error::ReedSolomon)?;
906 let recovery_refs: Vec<&[u8]> = (0..m)
907 .map(|i| {
908 encoding
909 .recovery(i)
910 .expect("recovery index must be in range")
911 })
912 .collect();
913
914 verify_reencoded::<H, S>(
915 ctx,
916 shard_digests,
917 originals,
918 &recovery_refs,
919 provided_recoveries,
920 )
921 }
922}
923
924fn decode<'a, H: Hasher, S: Strategy>(
939 total: u16,
940 min: u16,
941 root: &H::Digest,
942 chunks: impl Iterator<Item = &'a CheckedChunk<H::Digest>>,
943 strategy: &S,
944) -> Result<Vec<u8>, Error> {
945 assert!(total > min);
947 assert!(min > 0);
948 let n = total as usize;
949 let k = min as usize;
950 let m = n - k;
951 let mut chunks = chunks.peekable();
952 let Some(first) = chunks.peek() else {
953 return Err(Error::NotEnoughChunks);
954 };
955
956 let shard_len = first.shard.len();
958 let manual = strategy.manual();
959 let stripes = striped::ranges(shard_len, manual.parallelism());
960 let mut shard_digests: Vec<Option<H::Digest>> = vec![None; n];
961 let mut provided_originals: Vec<(usize, &[u8])> = Vec::new();
962 let mut provided_recoveries: Vec<(usize, &[u8])> = Vec::new();
963 let mut provided = 0usize;
964 for chunk in chunks {
965 provided += 1;
966 if &chunk.root != root {
967 return Err(Error::CommitmentMismatch);
968 }
969
970 if chunk.shard.len() != shard_len {
973 return Err(Error::Inconsistent);
974 }
975
976 let index = chunk.index;
978 if index >= total {
979 return Err(Error::InvalidIndex(index));
980 }
981 let digest_slot = &mut shard_digests[index as usize];
982 if digest_slot.is_some() {
983 return Err(Error::DuplicateIndex(index));
984 }
985
986 *digest_slot = Some(chunk.digest);
988 if index < min {
989 provided_originals.push((index as usize, chunk.shard.as_ref()));
990 } else {
991 provided_recoveries.push((index as usize - k, chunk.shard.as_ref()));
992 }
993 }
994 if provided < k {
995 return Err(Error::NotEnoughChunks);
996 }
997
998 let recovery_needed = provided_originals.len() < k;
1005 if recovery_needed {
1006 let keep = k - provided_originals.len();
1007 for &(idx, _) in &provided_recoveries[keep..] {
1008 shard_digests[k + idx] = None;
1009 }
1010 provided_recoveries.truncate(keep);
1011 }
1012
1013 if let Some(ranges) = stripes {
1015 let ctx = DecodeCtx {
1016 n,
1017 k,
1018 m,
1019 shard_len,
1020 root,
1021 strategy: &manual,
1022 };
1023 if recovery_needed {
1026 return striped::decode_reveal::<H, _>(
1027 &ctx,
1028 ranges,
1029 shard_digests,
1030 provided_originals,
1031 provided_recoveries,
1032 );
1033 }
1034 return striped::decode::<H, _>(
1035 &ctx,
1036 ranges,
1037 shard_digests,
1038 provided_originals,
1039 provided_recoveries,
1040 );
1041 }
1042 let ctx = DecodeCtx {
1043 n,
1044 k,
1045 m,
1046 shard_len,
1047 root,
1048 strategy,
1049 };
1050 sequential::decode::<H, S>(&ctx, shard_digests, provided_originals, provided_recoveries)
1051}
1052
1053#[derive(Clone, Copy)]
1141pub struct ReedSolomon<H> {
1142 _marker: PhantomData<H>,
1143}
1144
1145impl<H> std::fmt::Debug for ReedSolomon<H> {
1146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1147 f.debug_struct("ReedSolomon").finish()
1148 }
1149}
1150
1151impl<H: Hasher> Scheme for ReedSolomon<H> {
1152 type Commitment = H::Digest;
1153 type Shard = Chunk<H::Digest>;
1154 type CheckedShard = CheckedChunk<H::Digest>;
1155 type Error = Error;
1156
1157 fn encode(
1158 config: &Config,
1159 data: impl Buf,
1160 strategy: &impl Strategy,
1161 ) -> Result<(Self::Commitment, Vec<Self::Shard>), Self::Error> {
1162 encode::<H, _>(
1163 total_shards(config)?,
1164 config.minimum_shards.get(),
1165 data,
1166 strategy,
1167 )
1168 }
1169
1170 fn check(
1171 config: &Config,
1172 commitment: &Self::Commitment,
1173 index: u16,
1174 shard: &Self::Shard,
1175 ) -> Result<Self::CheckedShard, Self::Error> {
1176 let total = total_shards(config)?;
1177 if index >= total {
1178 return Err(Error::InvalidIndex(index));
1179 }
1180 if shard.proof.leaf_count != u32::from(total) {
1181 return Err(Error::InvalidProof);
1182 }
1183 if shard.index != index {
1184 return Err(Error::InvalidIndex(shard.index));
1185 }
1186 shard
1187 .verify::<H>(shard.index, commitment)
1188 .ok_or(Error::InvalidProof)
1189 }
1190
1191 fn decode<'a>(
1192 config: &Config,
1193 commitment: &Self::Commitment,
1194 shards: impl Iterator<Item = &'a Self::CheckedShard>,
1195 strategy: &impl Strategy,
1196 ) -> Result<Vec<u8>, Self::Error> {
1197 decode::<H, _>(
1198 total_shards(config)?,
1199 config.minimum_shards.get(),
1200 commitment,
1201 shards,
1202 strategy,
1203 )
1204 }
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209 use super::*;
1210 use commonware_codec::Encode;
1211 use commonware_cryptography::Sha256;
1212 use commonware_invariants::minifuzz;
1213 use commonware_parallel::{Rayon, Sequential};
1214 use commonware_runtime::{deterministic, iobuf::EncodeExt, BufferPooler, Runner};
1215 use commonware_utils::{NZUsize, NZU16};
1216
1217 type RS = ReedSolomon<Sha256>;
1218 const STRATEGY: Sequential = Sequential;
1219 const FUZZ_MAX_MIN_SHARDS: u16 = 8;
1220 const FUZZ_MAX_EXTRA_SHARDS: u16 = 8;
1221 const FUZZ_MAX_DATA_LEN: usize = 256;
1222 const FUZZ_MAX_EXTRA_SHARD_WIDTH: usize = 16;
1223
1224 fn checked(
1225 root: <Sha256 as Hasher>::Digest,
1226 chunk: Chunk<<Sha256 as Hasher>::Digest>,
1227 ) -> CheckedChunk<<Sha256 as Hasher>::Digest> {
1228 let Chunk { shard, index, .. } = chunk;
1229 let digest = Sha256::hash(&shard);
1230 CheckedChunk::new(root, shard, index, digest)
1231 }
1232
1233 fn build_chunks(
1234 shards: &[Vec<u8>],
1235 ) -> (
1236 <Sha256 as Hasher>::Digest,
1237 Vec<Chunk<<Sha256 as Hasher>::Digest>>,
1238 ) {
1239 let mut builder = Builder::<Sha256>::new(shards.len());
1240 for shard in shards {
1241 let mut hasher = Sha256::new();
1242 hasher.update(shard);
1243 builder.add(&hasher.finalize());
1244 }
1245 let tree = builder.build();
1246 let root = tree.root();
1247 let chunks = shards
1248 .iter()
1249 .enumerate()
1250 .map(|(i, shard)| {
1251 let proof = tree.proof(i as u32).unwrap();
1252 Chunk::new(shard.clone().into(), i as u16, proof)
1253 })
1254 .collect();
1255
1256 (root, chunks)
1257 }
1258
1259 fn selected_indices(
1260 u: &mut arbitrary::Unstructured<'_>,
1261 total: u16,
1262 minimum: u16,
1263 ) -> arbitrary::Result<Vec<u16>> {
1264 let to_use = u.int_in_range(minimum..=total)?;
1265 let mut selected = (0..total).collect::<Vec<_>>();
1266 for i in 0..usize::from(to_use) {
1267 let remaining = usize::from(total) - i;
1268 let j = i + u.choose_index(remaining)?;
1269 selected.swap(i, j);
1270 }
1271 selected.truncate(usize::from(to_use));
1272 Ok(selected)
1273 }
1274
1275 fn assert_decode_unique_commitment(
1276 total: u16,
1277 min: u16,
1278 root: <Sha256 as Hasher>::Digest,
1279 chunks: &[Chunk<<Sha256 as Hasher>::Digest>],
1280 selected: &[u16],
1281 ) {
1282 let pieces = selected
1283 .iter()
1284 .map(|&i| chunks[usize::from(i)].verify::<Sha256>(i, &root).unwrap())
1285 .collect::<Vec<_>>();
1286
1287 let Ok(decoded) = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY) else {
1288 return;
1289 };
1290 let (canonical_root, _) =
1291 encode::<Sha256, _>(total, min, decoded.as_slice(), &STRATEGY).unwrap();
1292 assert_eq!(
1293 root, canonical_root,
1294 "decode accepted a root not produced by canonical encode"
1295 );
1296 }
1297
1298 fn fuzz_arbitrary_codeword(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()> {
1299 let min = u.int_in_range(1..=FUZZ_MAX_MIN_SHARDS)?;
1300 let extra = u.int_in_range(1..=FUZZ_MAX_EXTRA_SHARDS)?;
1301 let total = min + extra;
1302 let k = usize::from(min);
1303 let m = usize::from(extra);
1304
1305 let data_len = u.int_in_range(0..=FUZZ_MAX_DATA_LEN)?;
1306 let data = u.bytes(data_len)?.to_vec();
1307 let canonical = canonical_shard_len(data.len(), k);
1308 let extra_width = u.int_in_range(0..=FUZZ_MAX_EXTRA_SHARD_WIDTH / 2)? * 2;
1309 let shard_len = canonical + extra_width;
1310
1311 let mut padded = vec![0u8; k * shard_len];
1312 padded[..u32::SIZE].copy_from_slice(&(data.len() as u32).to_be_bytes());
1313 padded[u32::SIZE..u32::SIZE + data.len()].copy_from_slice(&data);
1314
1315 let payload_end = u32::SIZE + data.len();
1316 if payload_end < padded.len() && u.int_in_range(0..=3)? == 0 {
1317 let offset = payload_end + u.choose_index(padded.len() - payload_end)?;
1318 padded[offset] ^= u.arbitrary::<u8>()? | 1;
1319 }
1320
1321 let mut encoder = Encoder::new(k, m, shard_len).unwrap();
1322 for shard in padded.chunks(shard_len) {
1323 encoder.add_original_shard(shard).unwrap();
1324 }
1325 let recovery = encoder.encode().unwrap();
1326
1327 let mut shards = padded
1328 .chunks(shard_len)
1329 .map(|shard| shard.to_vec())
1330 .collect::<Vec<_>>();
1331 shards.extend(recovery.recovery_iter().map(|shard| shard.to_vec()));
1332
1333 let (root, chunks) = build_chunks(&shards);
1334 let selected = selected_indices(u, total, min)?;
1335 assert_decode_unique_commitment(total, min, root, &chunks, &selected);
1336
1337 Ok(())
1338 }
1339
1340 fn fuzz_mixed_codeword(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()> {
1341 let min = u.int_in_range(1..=FUZZ_MAX_MIN_SHARDS)?;
1342 let extra = u.int_in_range(1..=FUZZ_MAX_EXTRA_SHARDS)?;
1343 let total = min + extra;
1344
1345 let data_len = u.int_in_range(0..=FUZZ_MAX_DATA_LEN)?;
1346 let data = u.bytes(data_len)?.to_vec();
1347 let (_canonical_root, chunks) =
1348 encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1349 let mut shards = chunks
1350 .iter()
1351 .map(|chunk| chunk.shard.to_vec())
1352 .collect::<Vec<_>>();
1353
1354 let mutated = usize::from(min + u.int_in_range(0..=extra - 1)?);
1355 let offset = u.choose_index(shards[mutated].len())?;
1356 shards[mutated][offset] ^= u.arbitrary::<u8>()? | 1;
1357
1358 let (root, chunks) = build_chunks(&shards);
1359 let mut selected = (0..min).collect::<Vec<_>>();
1360 selected.push(mutated as u16);
1361 assert_decode_unique_commitment(total, min, root, &chunks, &selected);
1362
1363 Ok(())
1364 }
1365
1366 #[test]
1367 fn test_recovery() {
1368 let data = b"Testing recovery pieces";
1369 let total = 8u16;
1370 let min = 3u16;
1371
1372 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1374
1375 let pieces: Vec<_> = vec![
1377 checked(root, chunks[0].clone()), checked(root, chunks[4].clone()), checked(root, chunks[6].clone()), ];
1381
1382 let decoded = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY).unwrap();
1384 assert_eq!(decoded, data);
1385 }
1386
1387 #[test]
1388 fn test_not_enough_pieces() {
1389 let data = b"Test insufficient pieces";
1390 let total = 6u16;
1391 let min = 4u16;
1392
1393 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1395
1396 let pieces: Vec<_> = chunks
1398 .into_iter()
1399 .take(2)
1400 .map(|c| checked(root, c))
1401 .collect();
1402
1403 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1405 assert!(matches!(result, Err(Error::NotEnoughChunks)));
1406 }
1407
1408 #[test]
1409 fn test_duplicate_index() {
1410 let data = b"Test duplicate detection";
1411 let total = 5u16;
1412 let min = 3u16;
1413
1414 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1416
1417 let pieces = [
1419 checked(root, chunks[0].clone()),
1420 checked(root, chunks[0].clone()),
1421 checked(root, chunks[1].clone()),
1422 ];
1423
1424 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1426 assert!(matches!(result, Err(Error::DuplicateIndex(0))));
1427 }
1428
1429 #[test]
1430 fn test_invalid_index() {
1431 let data = b"Test invalid index";
1432 let total = 5u16;
1433 let min = 3u16;
1434
1435 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1437
1438 for i in 0..total {
1440 assert!(chunks[i as usize].verify::<Sha256>(i + 1, &root).is_none());
1441 }
1442 }
1443
1444 #[test]
1445 #[should_panic(expected = "assertion failed: total > min")]
1446 fn test_invalid_total() {
1447 let data = b"Test parameter validation";
1448
1449 encode::<Sha256, _>(3, 3, data.as_slice(), &STRATEGY).unwrap();
1451 }
1452
1453 #[test]
1454 #[should_panic(expected = "assertion failed: min > 0")]
1455 fn test_invalid_min() {
1456 let data = b"Test parameter validation";
1457
1458 encode::<Sha256, _>(5, 0, data.as_slice(), &STRATEGY).unwrap();
1460 }
1461
1462 #[test]
1463 fn test_empty_data() {
1464 let data = b"";
1465 let total = 100u16;
1466 let min = 30u16;
1467
1468 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1470
1471 let minimal = chunks
1473 .into_iter()
1474 .take(min as usize)
1475 .map(|c| checked(root, c))
1476 .collect::<Vec<_>>();
1477 let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
1478 assert_eq!(decoded, data);
1479 }
1480
1481 #[test]
1482 fn test_large_data() {
1483 let data = vec![42u8; 1000]; let total = 7u16;
1485 let min = 4u16;
1486
1487 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1489
1490 let minimal = chunks
1492 .into_iter()
1493 .take(min as usize)
1494 .map(|c| checked(root, c))
1495 .collect::<Vec<_>>();
1496 let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
1497 assert_eq!(decoded, data);
1498 }
1499
1500 #[test]
1501 fn test_parallel_encode_matches_sequential() {
1502 let strategy = Rayon::new(NZUsize!(4)).unwrap();
1503 let data = vec![42u8; 256 * 1024];
1504 let total = 24u16;
1505 let min = 8u16;
1506
1507 let (sequential_root, sequential_chunks) =
1508 encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1509 let (parallel_root, parallel_chunks) =
1510 encode::<Sha256, _>(total, min, data.as_slice(), &strategy).unwrap();
1511
1512 assert_eq!(sequential_root, parallel_root);
1513 assert_eq!(sequential_chunks, parallel_chunks);
1514 }
1515
1516 #[test]
1517 fn test_parallel_recovery_decode() {
1518 let strategy = Rayon::new(NZUsize!(4)).unwrap();
1519 let data = vec![42u8; 256 * 1024];
1520 let total = 24u16;
1521 let min = 8u16;
1522
1523 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &strategy).unwrap();
1524
1525 let minimal = chunks
1526 .into_iter()
1527 .skip(min as usize)
1528 .take(min as usize)
1529 .map(|c| checked(root, c))
1530 .collect::<Vec<_>>();
1531 let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &strategy).unwrap();
1532 assert_eq!(decoded, data);
1533 }
1534
1535 #[test]
1542 fn test_striped_recovery_matches_sequential() {
1543 for &data_len in &[128 * 1024usize, 257 * 1024, 512 * 1024, 1024 * 1024] {
1544 for &(total, min) in &[(12u16, 4u16), (24, 8), (33, 11)] {
1545 let data: Vec<u8> = (0..data_len)
1546 .map(|i| (i as u8) ^ ((i >> 7) as u8))
1547 .collect();
1548 let (root, chunks) =
1549 encode::<Sha256, _>(total, min, data.as_slice(), &Sequential).unwrap();
1550 let recovery_only = chunks
1551 .into_iter()
1552 .skip(min as usize)
1553 .take(min as usize)
1554 .map(|c| checked(root, c))
1555 .collect::<Vec<_>>();
1556 let sequential =
1557 decode::<Sha256, _>(total, min, &root, recovery_only.iter(), &Sequential)
1558 .unwrap();
1559 assert_eq!(sequential, data);
1560 for ¶llelism in &[2usize, 8] {
1561 let strategy = Rayon::new(NZUsize!(parallelism)).unwrap();
1562 let striped =
1563 decode::<Sha256, _>(total, min, &root, recovery_only.iter(), &strategy)
1564 .unwrap();
1565 assert_eq!(
1566 striped, data,
1567 "striped decode mismatch (len={data_len} total={total} min={min} parallelism={parallelism})"
1568 );
1569 }
1570 }
1571 }
1572 }
1573
1574 #[test]
1578 fn test_striped_all_originals_decode() {
1579 let strategy = Rayon::new(NZUsize!(4)).unwrap();
1580 let data = vec![42u8; 256 * 1024];
1581 let total = 24u16;
1582 let min = 8u16;
1583
1584 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &strategy).unwrap();
1585
1586 let shard_len = canonical_shard_len(data.len(), min as usize);
1589 assert!(
1590 striped::ranges(shard_len, strategy.manual().parallelism()).map_or(0, |r| r.len()) >= 2,
1591 "test must exercise >= 2 stripes (shard_len={shard_len})"
1592 );
1593
1594 let originals = chunks
1597 .into_iter()
1598 .take(min as usize)
1599 .map(|c| checked(root, c))
1600 .collect::<Vec<_>>();
1601 let decoded = decode::<Sha256, _>(total, min, &root, originals.iter(), &strategy).unwrap();
1602 assert_eq!(decoded, data);
1603 }
1604
1605 #[test]
1608 fn test_striped_encode_into_matches_full_width() {
1609 let k = 2usize;
1610 let m = 2usize;
1611 let shard_len = 2 * MIN_STRIPE_BYTES;
1612 let ranges = striped::ranges(shard_len, 4).expect("must split into stripes");
1613 assert!(ranges.len() >= 2);
1614
1615 let mut originals_buf = vec![0u8; k * shard_len];
1616 for (i, byte) in originals_buf.iter_mut().enumerate() {
1617 *byte = (i % 251) as u8;
1618 }
1619 let originals: Vec<&[u8]> = originals_buf.chunks(shard_len).collect();
1620
1621 let mut striped_recovery = vec![0u8; m * shard_len];
1623 let groups = striped::stripe_columns(&mut striped_recovery, shard_len, &ranges);
1624 for (range, out) in ranges.iter().cloned().zip(groups) {
1625 striped::encode_recovery_into(k, m, range, &originals, out).unwrap();
1626 }
1627
1628 let mut full_recovery = vec![0u8; m * shard_len];
1630 let full_out: Vec<&mut [u8]> = full_recovery.chunks_mut(shard_len).collect();
1631 striped::encode_recovery_into(k, m, 0..shard_len, &originals, full_out).unwrap();
1632
1633 assert_eq!(striped_recovery, full_recovery);
1634 }
1635
1636 fn tamper_flip_recovery(shards: &mut [Vec<u8>], k: usize, _shard_len: usize) {
1640 shards[k][0] ^= 0xFF;
1641 }
1642
1643 fn tamper_flip_original_data(shards: &mut [Vec<u8>], _k: usize, shard_len: usize) {
1644 let offset = u32::SIZE;
1646 shards[offset / shard_len][offset % shard_len] ^= 0xFF;
1647 }
1648
1649 fn tamper_corrupt_padding(shards: &mut [Vec<u8>], k: usize, shard_len: usize) {
1650 shards[k - 1][shard_len - 1] = 0xAA;
1653 }
1654
1655 fn decode_tampered_codeword<S: Strategy>(
1659 total: u16,
1660 min: u16,
1661 data: &[u8],
1662 selected: &[u16],
1663 tamper: fn(&mut [Vec<u8>], usize, usize),
1664 strategy: &S,
1665 ) -> Result<Vec<u8>, Error> {
1666 let (_root, chunks) = encode::<Sha256, _>(total, min, data, &Sequential).unwrap();
1667 let mut shards = chunks.iter().map(|c| c.shard.to_vec()).collect::<Vec<_>>();
1668 let shard_len = shards[0].len();
1669 tamper(&mut shards, min as usize, shard_len);
1670 let (root, chunks) = build_chunks(&shards);
1671 let pieces = selected
1672 .iter()
1673 .map(|&i| checked(root, chunks[i as usize].clone()))
1674 .collect::<Vec<_>>();
1675 decode::<Sha256, _>(total, min, &root, pieces.iter(), strategy)
1676 }
1677
1678 #[allow(clippy::type_complexity)]
1681 const ADVERSARIAL_SCENARIOS: &[(&str, &[u16], fn(&mut [Vec<u8>], usize, usize))] = &[
1682 (
1685 "tampered_recovery_unprovided",
1686 &[0, 1, 2, 3],
1687 tamper_flip_recovery,
1688 ),
1689 (
1692 "tampered_recovery_provided",
1693 &[0, 1, 2, 4],
1694 tamper_flip_recovery,
1695 ),
1696 (
1698 "non_canonical_padding",
1699 &[0, 1, 2, 3],
1700 tamper_corrupt_padding,
1701 ),
1702 (
1704 "tampered_original_data",
1705 &[0, 1, 2, 3],
1706 tamper_flip_original_data,
1707 ),
1708 (
1710 "recovery_only_tampered",
1711 &[4, 5, 6, 7],
1712 tamper_flip_recovery,
1713 ),
1714 ];
1715
1716 fn assert_adversarial_rejected<S: Strategy>(total: u16, min: u16, data: &[u8], strategy: &S) {
1719 for &(name, selected, tamper) in ADVERSARIAL_SCENARIOS {
1720 let result = decode_tampered_codeword(total, min, data, selected, tamper, strategy);
1721 assert!(
1722 matches!(result, Err(Error::Inconsistent)),
1723 "scenario {name} not rejected as Inconsistent: {result:?}"
1724 );
1725 }
1726 }
1727
1728 #[test]
1729 fn test_adversarial_rejection_sequential_small() {
1730 assert_adversarial_rejected(12, 4, &[0xCDu8; 30], &Sequential);
1733 }
1734
1735 #[test]
1739 fn test_adversarial_rejection_striped_matches_sequential() {
1740 let total = 12u16;
1741 let min = 4u16;
1742
1743 let data = vec![0xABu8; 64 * 1024];
1747 let shard_len = canonical_shard_len(data.len(), min as usize);
1748 let rayon = Rayon::new(NZUsize!(4)).unwrap();
1749 assert!(
1750 striped::ranges(shard_len, rayon.manual().parallelism()).map_or(0, |r| r.len()) >= 2,
1751 "test must exercise >= 2 stripes (shard_len={shard_len})"
1752 );
1753
1754 assert_adversarial_rejected(total, min, &data, &Sequential);
1756 assert_adversarial_rejected(total, min, &data, &rayon);
1757
1758 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &Sequential).unwrap();
1761 let mixed = [0u16, 1, 2, 4]
1762 .into_iter()
1763 .map(|i| checked(root, chunks[i as usize].clone()))
1764 .collect::<Vec<_>>();
1765 let decoded = decode::<Sha256, _>(total, min, &root, mixed.iter(), &rayon).unwrap();
1766 assert_eq!(decoded, data);
1767 }
1768
1769 #[test]
1775 fn test_recovery_output_format_pinned() {
1776 let k = 5usize;
1777 let m = 3usize;
1778 let shard_len = 2 * SHARD_CHUNK_BYTES + 2;
1780
1781 let mut encoder = Encoder::new(k, m, shard_len).unwrap();
1782 for i in 0..k {
1783 let shard: Vec<u8> = (0..shard_len)
1784 .map(|j| ((i * 31 + j * 7) & 0xFF) as u8)
1785 .collect();
1786 encoder.add_original_shard(&shard).unwrap();
1787 }
1788 let encoding = encoder.encode().unwrap();
1789
1790 let mut hasher = Sha256::new();
1791 for shard in encoding.recovery_iter() {
1792 hasher.update(shard);
1793 }
1794 let digest = hasher.finalize();
1795 assert_eq!(
1796 format!("{digest}"),
1797 "e38bb9dbba4a102c4bd8447e212957742dab0af0c4148d4660c671f2f33d3df2",
1798 "vendored Reed-Solomon recovery output changed; re-verify the striping \
1799 assumption before updating this fixture"
1800 );
1801 }
1802
1803 #[test]
1804 fn test_parallel_decode_rejects_mismatched_shard_lengths() {
1805 let strategy = Rayon::new(NZUsize!(4)).unwrap();
1806 let data = vec![42u8; 256 * 1024];
1807 let total = 24u16;
1808 let min = 8u16;
1809
1810 let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &strategy).unwrap();
1811 let mut shards = chunks
1812 .iter()
1813 .map(|chunk| chunk.shard.to_vec())
1814 .collect::<Vec<_>>();
1815 shards[min as usize].pop();
1816
1817 let mut builder = Builder::<Sha256>::new(total as usize);
1818 for shard in &shards {
1819 let mut hasher = Sha256::new();
1820 hasher.update(shard);
1821 builder.add(&hasher.finalize());
1822 }
1823 let tree = builder.build();
1824 let root = tree.root();
1825
1826 let pieces = [9u16, 8, 10, 11, 12, 13, 14, 15]
1827 .into_iter()
1828 .map(|i| {
1829 let proof = tree.proof(i as u32).unwrap();
1830 checked(
1831 root,
1832 Chunk::new(shards[i as usize].clone().into(), i, proof),
1833 )
1834 })
1835 .collect::<Vec<_>>();
1836
1837 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &strategy);
1838 assert!(matches!(result, Err(Error::Inconsistent)));
1839 }
1840
1841 #[test]
1842 fn test_malicious_root_detection() {
1843 let data = b"Original data that should be protected";
1844 let total = 7u16;
1845 let min = 4u16;
1846
1847 let (_correct_root, chunks) =
1849 encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1850
1851 let mut hasher = Sha256::new();
1853 hasher.update(b"malicious_data_that_wasnt_actually_encoded");
1854 let malicious_root = hasher.finalize();
1855
1856 for i in 0..total {
1858 assert!(chunks[i as usize]
1859 .clone()
1860 .verify::<Sha256>(i, &malicious_root)
1861 .is_none());
1862 }
1863
1864 let minimal = chunks
1867 .into_iter()
1868 .take(min as usize)
1869 .map(|c| checked(_correct_root, c))
1870 .collect::<Vec<_>>();
1871
1872 let result = decode::<Sha256, _>(total, min, &malicious_root, minimal.iter(), &STRATEGY);
1875 assert!(matches!(result, Err(Error::CommitmentMismatch)));
1876 }
1877
1878 #[test]
1879 fn test_mismatched_config_rejected_during_check() {
1880 let config_expected = Config {
1881 minimum_shards: NZU16!(2),
1882 extra_shards: NZU16!(2),
1883 };
1884 let config_actual = Config {
1885 minimum_shards: NZU16!(3),
1886 extra_shards: NZU16!(3),
1887 };
1888
1889 let data = b"leaf_count mismatch proof";
1890 let (commitment, shards) = RS::encode(&config_actual, data.as_slice(), &STRATEGY).unwrap();
1891
1892 let check_result = RS::check(&config_expected, &commitment, 0, &shards[0]);
1895 assert!(matches!(check_result, Err(Error::InvalidProof)));
1896 }
1897
1898 #[test]
1899 fn test_manipulated_chunk_detection() {
1900 let data = b"Data integrity must be maintained";
1901 let total = 6u16;
1902 let min = 3u16;
1903
1904 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1906 let mut pieces: Vec<_> = chunks.into_iter().map(|c| checked(root, c)).collect();
1907
1908 if !pieces[1].shard.is_empty() {
1910 let mut shard = pieces[1].shard.to_vec();
1911 shard[0] ^= 0xFF; pieces[1].shard = shard.into();
1913 }
1914
1915 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1917 assert!(matches!(result, Err(Error::Inconsistent)));
1918 }
1919
1920 #[test]
1921 fn test_inconsistent_shards() {
1922 let data = b"Test data for malicious encoding";
1923 let total = 5u16;
1924 let min = 3u16;
1925 let m = total - min;
1926
1927 let (padded, shard_size) = prepare_data(data.as_slice(), min as usize);
1929
1930 let mut encoder = Encoder::new(min as usize, m as usize, shard_size).unwrap();
1932 for shard in padded.chunks(shard_size) {
1933 encoder.add_original_shard(shard).unwrap();
1934 }
1935 let recovery_result = encoder.encode().unwrap();
1936 let mut recovery_shards: Vec<Vec<u8>> = recovery_result
1937 .recovery_iter()
1938 .map(|s| s.to_vec())
1939 .collect();
1940
1941 if !recovery_shards[0].is_empty() {
1943 recovery_shards[0][0] ^= 0xFF;
1944 }
1945
1946 let mut malicious_shards: Vec<Vec<u8>> =
1948 padded.chunks(shard_size).map(|s| s.to_vec()).collect();
1949 malicious_shards.extend(recovery_shards);
1950
1951 let mut builder = Builder::<Sha256>::new(total as usize);
1953 for shard in &malicious_shards {
1954 let mut hasher = Sha256::new();
1955 hasher.update(shard);
1956 builder.add(&hasher.finalize());
1957 }
1958 let malicious_tree = builder.build();
1959 let malicious_root = malicious_tree.root();
1960
1961 let selected_indices = vec![0, 1, 3]; let mut pieces = Vec::new();
1964 for &i in &selected_indices {
1965 let merkle_proof = malicious_tree.proof(i as u32).unwrap();
1966 let shard = malicious_shards[i].clone();
1967 let chunk = Chunk::new(shard.into(), i as u16, merkle_proof);
1968 pieces.push(chunk);
1969 }
1970 let pieces: Vec<_> = pieces
1971 .into_iter()
1972 .map(|c| checked(malicious_root, c))
1973 .collect();
1974
1975 let result = decode::<Sha256, _>(total, min, &malicious_root, pieces.iter(), &STRATEGY);
1977 assert!(matches!(result, Err(Error::Inconsistent)));
1978 }
1979
1980 #[test]
1984 fn test_non_canonical_padding_rejected() {
1985 let data = b"X";
1986 let total = 6u16;
1987 let min = 3u16;
1988 let k = min as usize;
1989 let m = total as usize - k;
1990
1991 let (mut padded, shard_len) = prepare_data(data.as_slice(), k);
1992 let payload_end = u32::SIZE + data.len();
1993 let total_original_len = k * shard_len;
1994 assert!(payload_end < total_original_len, "test requires padding");
1995
1996 let pad_shard = payload_end / shard_len;
1998 let pad_offset = payload_end % shard_len;
1999 padded[pad_shard * shard_len + pad_offset] = 0xAA;
2000
2001 let mut encoder = Encoder::new(k, m, shard_len).unwrap();
2002 for shard in padded.chunks(shard_len) {
2003 encoder.add_original_shard(shard).unwrap();
2004 }
2005 let recovery = encoder.encode().unwrap();
2006 let mut shards: Vec<Vec<u8>> = padded.chunks(shard_len).map(|s| s.to_vec()).collect();
2007 shards.extend(recovery.recovery_iter().map(|s| s.to_vec()));
2008
2009 let mut builder = Builder::<Sha256>::new(total as usize);
2010 for shard in &shards {
2011 let mut hasher = Sha256::new();
2012 hasher.update(shard);
2013 builder.add(&hasher.finalize());
2014 }
2015 let tree = builder.build();
2016 let non_canonical_root = tree.root();
2017
2018 let mut pieces = Vec::with_capacity(k);
2019 for (i, shard) in shards.iter().take(k).enumerate() {
2020 let proof = tree.proof(i as u32).unwrap();
2021 pieces.push(checked(
2022 non_canonical_root,
2023 Chunk::new(shard.clone().into(), i as u16, proof),
2024 ));
2025 }
2026
2027 let result = decode::<Sha256, _>(total, min, &non_canonical_root, pieces.iter(), &STRATEGY);
2028 assert!(matches!(result, Err(Error::Inconsistent)));
2029 }
2030
2031 #[test]
2032 fn minifuzz_decode_unique_commitment() {
2033 minifuzz::Builder::default()
2034 .with_search_limit(2048)
2035 .test(|u| {
2036 fuzz_arbitrary_codeword(u)?;
2037 fuzz_mixed_codeword(u)?;
2038 Ok(())
2039 });
2040 }
2041
2042 #[test]
2043 fn test_oversized_zero_padded_shards_rejected() {
2044 let data = b"X";
2045 let total = 6u16;
2046 let min = 3u16;
2047 let k = min as usize;
2048 let m = total as usize - k;
2049
2050 let oversized_shard_len = 4usize;
2051 let mut padded = vec![0u8; k * oversized_shard_len];
2052 padded[..u32::SIZE].copy_from_slice(&(data.len() as u32).to_be_bytes());
2053 padded[u32::SIZE..u32::SIZE + data.len()].copy_from_slice(data);
2054
2055 let mut encoder = Encoder::new(k, m, oversized_shard_len).unwrap();
2056 for shard in padded.chunks(oversized_shard_len) {
2057 encoder.add_original_shard(shard).unwrap();
2058 }
2059 let recovery = encoder.encode().unwrap();
2060
2061 let mut oversized_shards: Vec<Vec<u8>> = padded
2062 .chunks(oversized_shard_len)
2063 .map(|shard| shard.to_vec())
2064 .collect();
2065 oversized_shards.extend(recovery.recovery_iter().map(|shard| shard.to_vec()));
2066
2067 let mut builder = Builder::<Sha256>::new(total as usize);
2068 for shard in &oversized_shards {
2069 let mut hasher = Sha256::new();
2070 hasher.update(shard);
2071 builder.add(&hasher.finalize());
2072 }
2073 let oversized_tree = builder.build();
2074 let oversized_root = oversized_tree.root();
2075
2076 let (canonical_root, _) =
2077 encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2078 assert_ne!(oversized_root, canonical_root);
2079
2080 let pieces = [0u16, 1u16, 4u16]
2081 .into_iter()
2082 .map(|i| {
2083 let proof = oversized_tree.proof(i as u32).unwrap();
2084 checked(
2085 oversized_root,
2086 Chunk::new(oversized_shards[i as usize].clone().into(), i, proof),
2087 )
2088 })
2089 .collect::<Vec<_>>();
2090
2091 let result = decode::<Sha256, _>(total, min, &oversized_root, pieces.iter(), &STRATEGY);
2092 assert!(matches!(result, Err(Error::Inconsistent)));
2093 }
2094
2095 #[test]
2096 fn test_extra_non_canonical_recovery_rejected() {
2097 let data = b"canonical originals with bad recovery";
2098 let total = 6u16;
2099 let min = 3u16;
2100
2101 let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2102 let mut shards = chunks
2103 .iter()
2104 .map(|chunk| chunk.shard.to_vec())
2105 .collect::<Vec<_>>();
2106 shards[min as usize][0] ^= 0xFF;
2107
2108 let mut builder = Builder::<Sha256>::new(total as usize);
2109 for shard in &shards {
2110 let mut hasher = Sha256::new();
2111 hasher.update(shard);
2112 builder.add(&hasher.finalize());
2113 }
2114 let tree = builder.build();
2115 let root = tree.root();
2116
2117 let pieces = (0u16..=3u16)
2118 .map(|i| {
2119 let proof = tree.proof(i as u32).unwrap();
2120 checked(
2121 root,
2122 Chunk::new(shards[i as usize].clone().into(), i, proof),
2123 )
2124 })
2125 .collect::<Vec<_>>();
2126
2127 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
2128 assert!(matches!(result, Err(Error::Inconsistent)));
2129 }
2130
2131 #[test]
2132 fn test_reconstructed_original_with_extra_non_canonical_recovery_rejected() {
2133 let data = b"canonical reconstructed originals with bad extra recovery";
2134 let total = 6u16;
2135 let min = 3u16;
2136
2137 let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2138 let mut shards = chunks
2139 .iter()
2140 .map(|chunk| chunk.shard.to_vec())
2141 .collect::<Vec<_>>();
2142 shards[4][0] ^= 0xFF;
2143
2144 let mut builder = Builder::<Sha256>::new(total as usize);
2145 for shard in &shards {
2146 let mut hasher = Sha256::new();
2147 hasher.update(shard);
2148 builder.add(&hasher.finalize());
2149 }
2150 let tree = builder.build();
2151 let root = tree.root();
2152
2153 let pieces = [0u16, 1u16, 3u16, 4u16]
2154 .into_iter()
2155 .map(|i| {
2156 let proof = tree.proof(i as u32).unwrap();
2157 checked(
2158 root,
2159 Chunk::new(shards[i as usize].clone().into(), i, proof),
2160 )
2161 })
2162 .collect::<Vec<_>>();
2163
2164 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
2165 assert!(matches!(result, Err(Error::Inconsistent)));
2166 }
2167
2168 #[test]
2169 fn test_striped_surplus_recovery_tampered_rejected() {
2170 let strategy = Rayon::new(NZUsize!(4)).unwrap();
2175 let data = vec![0xABu8; 64 * 1024];
2176 let total = 12u16;
2177 let min = 4u16;
2178 let shard_len = canonical_shard_len(data.len(), min as usize);
2179 assert!(
2180 striped::ranges(shard_len, strategy.manual().parallelism()).map_or(0, |r| r.len()) >= 2,
2181 "test must exercise the striped path (shard_len={shard_len})"
2182 );
2183
2184 let selected = [0u16, 1, 4, 5, 6];
2187 let tamper: fn(&mut [Vec<u8>], usize, usize) = |shards, _, _| shards[6][0] ^= 0xFF;
2188 let result = decode_tampered_codeword(total, min, &data, &selected, tamper, &strategy);
2189 assert!(matches!(result, Err(Error::Inconsistent)));
2190 }
2191
2192 #[test]
2193 fn test_decode_invalid_index() {
2194 let data = b"Testing recovery pieces";
2195 let total = 8u16;
2196 let min = 3u16;
2197
2198 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2200
2201 let mut invalid = checked(root, chunks[1].clone());
2203 invalid.index = 8;
2204 let pieces: Vec<_> = vec![
2205 checked(root, chunks[0].clone()), invalid, checked(root, chunks[6].clone()), ];
2209
2210 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
2212 assert!(matches!(result, Err(Error::InvalidIndex(8))));
2213 }
2214
2215 #[test]
2216 fn test_max_chunks() {
2217 let data = vec![42u8; 1000]; let total = u16::MAX;
2219 let min = u16::MAX / 2;
2220
2221 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2223
2224 let minimal = chunks
2226 .into_iter()
2227 .take(min as usize)
2228 .map(|c| checked(root, c))
2229 .collect::<Vec<_>>();
2230 let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
2231 assert_eq!(decoded, data);
2232 }
2233
2234 #[test]
2235 fn test_too_many_chunks() {
2236 let data = vec![42u8; 1000]; let total = u16::MAX;
2238 let min = u16::MAX / 2 - 1;
2239
2240 let result = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY);
2242 assert!(matches!(
2243 result,
2244 Err(Error::ReedSolomon(RsError::UnsupportedShardCount {
2245 original_count: _,
2246 recovery_count: _,
2247 }))
2248 ));
2249 }
2250
2251 #[test]
2252 fn test_too_many_total_shards() {
2253 assert!(RS::encode(
2254 &Config {
2255 minimum_shards: NZU16!(u16::MAX / 2 + 1),
2256 extra_shards: NZU16!(u16::MAX),
2257 },
2258 [].as_slice(),
2259 &STRATEGY,
2260 )
2261 .is_err())
2262 }
2263
2264 #[test]
2265 fn test_chunk_encode_with_pool_matches_encode() {
2266 let executor = deterministic::Runner::default();
2267 executor.start(|context| async move {
2268 let pool = context.network_buffer_pool();
2269
2270 let data = b"pool encoding test";
2271 let (_root, chunks) = encode::<Sha256, _>(5, 3, data.as_slice(), &STRATEGY).unwrap();
2272 let chunk = &chunks[0];
2273
2274 let encoded = chunk.encode();
2275 let mut encoded_pool = chunk.encode_with_pool(pool);
2276 let mut encoded_pool_bytes = vec![0u8; encoded_pool.remaining()];
2277 encoded_pool.copy_to_slice(&mut encoded_pool_bytes);
2278 assert_eq!(encoded_pool_bytes, encoded.as_ref());
2279 });
2280 }
2281
2282 #[cfg(feature = "arbitrary")]
2283 mod conformance {
2284 use super::*;
2285 use commonware_codec::conformance::CodecConformance;
2286 use commonware_cryptography::sha256::Digest as Sha256Digest;
2287
2288 commonware_conformance::conformance_tests! {
2289 CodecConformance<Chunk<Sha256Digest>>,
2290 }
2291 }
2292}