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