1use crate::{Config, Scheme};
2use bytes::{BufMut, Bytes};
3use commonware_codec::{
4 Buf, BufsMut, EncodeSize, FixedSize, RangeCfg, Read, ReadExt, Write, util::max_items_size,
5};
6use commonware_cryptography::{
7 Digest, Hasher,
8 reed_solomon::{Decoder, Encoder, Error as RsError, Plan, SHARD_CHUNK_BYTES},
9};
10use commonware_parallel::{Batches, Strategy};
11use commonware_storage::bmt::{self, Builder};
12use commonware_utils::{Cached, MAX_ALLOCATION_BYTES, NZUsize, Widen};
13use std::{marker::PhantomData, ops::Range};
14use thiserror::Error;
15
16commonware_utils::thread_local_cache!(static CACHED_ENCODER: Encoder);
21commonware_utils::thread_local_cache!(static CACHED_DECODER: Decoder);
22
23const MIN_STRIPE_BYTES: usize = 8 * 1024;
30
31const MAX_TILE_WORK_BYTES: usize = 2 * 1024 * 1024;
36
37#[derive(Error, Debug)]
39pub enum Error {
40 #[error("reed-solomon error: {0}")]
41 ReedSolomon(#[from] RsError),
42 #[error("inconsistent")]
43 Inconsistent,
44 #[error("invalid proof")]
45 InvalidProof,
46 #[error("not enough chunks")]
47 NotEnoughChunks,
48 #[error("duplicate chunk index: {0}")]
49 DuplicateIndex(u16),
50 #[error("invalid data length: {0}")]
51 InvalidDataLength(usize),
52 #[error("invalid index: {0}")]
53 InvalidIndex(u16),
54 #[error("too many total shards: {0}")]
55 TooManyTotalShards(u32),
56 #[error("checked shard commitment does not match decode commitment")]
57 CommitmentMismatch,
58}
59
60fn total_shards(config: &Config) -> Result<u16, Error> {
61 let total = config.total_shards();
62 total
63 .try_into()
64 .map_err(|_| Error::TooManyTotalShards(total))
65}
66
67fn check_metadata<D: Digest>(total: u16, index: u16, shard: &Chunk<D>) -> Result<(), Error> {
72 if index >= total {
73 return Err(Error::InvalidIndex(index));
74 }
75 let width = shard.shard.len();
76 if width == 0 || !width.is_multiple_of(2) {
77 return Err(RsError::InvalidShardSize { shard_bytes: width }.into());
78 }
79 if shard.proof.leaf_count != u32::from(total) {
80 return Err(Error::InvalidProof);
81 }
82 if shard.index != index {
83 return Err(Error::InvalidIndex(shard.index));
84 }
85 Ok(())
86}
87
88fn check_chunk<H: Hasher>(
91 total: u16,
92 commitment: &H::Digest,
93 index: u16,
94 shard: &Chunk<H::Digest>,
95 digest: impl FnOnce() -> H::Digest,
96) -> Result<CheckedChunk<H>, Error> {
97 check_metadata(total, index, shard)?;
98
99 let digest = digest();
101 shard
102 .proof
103 .verify_element_inclusion::<H>(&digest, u32::from(index), commitment)
104 .map_err(|_| Error::InvalidProof)?;
105
106 Ok(CheckedChunk::new(
108 *commitment,
109 shard.shard.clone(),
110 index,
111 digest,
112 ))
113}
114
115#[derive(Debug, Clone)]
117pub struct Chunk<D: Digest> {
118 shard: Bytes,
120
121 index: u16,
123
124 proof: bmt::Proof<D>,
126}
127
128impl<D: Digest> Chunk<D> {
129 pub fn max_encode_size(shard: usize, siblings: usize) -> usize {
132 max_items_size(shard, u8::SIZE) + u16::SIZE + bmt::Proof::<D>::max_encode_size(siblings)
133 }
134
135 const fn new(shard: Bytes, index: u16, proof: bmt::Proof<D>) -> Self {
137 Self {
138 shard,
139 index,
140 proof,
141 }
142 }
143}
144
145pub struct CheckedChunk<H: Hasher> {
155 root: H::Digest,
156 shard: Bytes,
157 index: u16,
158 digest: H::Digest,
159}
160
161impl<H: Hasher> CheckedChunk<H> {
162 const fn new(root: H::Digest, shard: Bytes, index: u16, digest: H::Digest) -> Self {
163 Self {
164 root,
165 shard,
166 index,
167 digest,
168 }
169 }
170}
171
172impl<H: Hasher> Clone for CheckedChunk<H> {
173 fn clone(&self) -> Self {
174 Self {
175 root: self.root,
176 shard: self.shard.clone(),
177 index: self.index,
178 digest: self.digest,
179 }
180 }
181}
182
183impl<H: Hasher> std::fmt::Debug for CheckedChunk<H> {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 f.debug_struct("CheckedChunk")
186 .field("root", &self.root)
187 .field("shard", &self.shard)
188 .field("index", &self.index)
189 .field("digest", &self.digest)
190 .finish()
191 }
192}
193
194impl<H: Hasher> PartialEq for CheckedChunk<H> {
195 fn eq(&self, other: &Self) -> bool {
196 self.root == other.root
197 && self.shard == other.shard
198 && self.index == other.index
199 && self.digest == other.digest
200 }
201}
202
203impl<H: Hasher> Eq for CheckedChunk<H> {}
204
205impl<D: Digest> Write for Chunk<D> {
206 fn write(&self, writer: &mut impl BufMut) {
207 self.shard.write(writer);
208 self.index.write(writer);
209 self.proof.write(writer);
210 }
211
212 fn write_bufs(&self, buf: &mut impl BufsMut) {
213 self.shard.write_bufs(buf);
214 self.index.write(buf);
215 self.proof.write(buf);
216 }
217}
218
219impl<D: Digest> Read for Chunk<D> {
220 type Cfg = (Config, usize);
226
227 fn read_cfg(
228 reader: &mut impl Buf,
229 (config, maximum): &Self::Cfg,
230 ) -> Result<Self, commonware_codec::Error> {
231 let width = canonical_shard_len(
232 (*maximum).min(Widen::widen(u32::MAX)),
233 config.minimum_shards.widen(),
234 );
235 let shard = Bytes::read_cfg(reader, &RangeCfg::new(..=width))?;
236 let index = u16::read(reader)?;
237 let proof = bmt::Proof::<D>::read_cfg(reader, &1)?;
238 Ok(Self {
239 shard,
240 index,
241 proof,
242 })
243 }
244}
245
246impl<D: Digest> EncodeSize for Chunk<D> {
247 fn encode_size(&self) -> usize {
248 self.shard.encode_size() + self.index.encode_size() + self.proof.encode_size()
249 }
250
251 fn encode_inline_size(&self) -> usize {
252 self.shard.encode_inline_size() + self.index.encode_size() + self.proof.encode_size()
253 }
254}
255
256impl<D: Digest> PartialEq for Chunk<D> {
257 fn eq(&self, other: &Self) -> bool {
258 self.shard == other.shard && self.index == other.index && self.proof == other.proof
259 }
260}
261
262impl<D: Digest> Eq for Chunk<D> {}
263
264#[cfg(feature = "arbitrary")]
265impl<D: Digest> arbitrary::Arbitrary<'_> for Chunk<D>
266where
267 D: for<'a> arbitrary::Arbitrary<'a>,
268{
269 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
270 Ok(Self {
271 shard: u.arbitrary::<Vec<u8>>()?.into(),
272 index: u.arbitrary()?,
273 proof: u.arbitrary()?,
274 })
275 }
276}
277
278fn prepare_data(mut data: impl bytes::Buf, k: usize, shard_len: usize) -> Vec<u8> {
284 let data_len = data.remaining();
285 let length_bytes = (data_len as u32).to_be_bytes();
286 let mut padded = vec![0u8; k * shard_len];
287 padded[..u32::SIZE].copy_from_slice(&length_bytes);
288 data.copy_to_slice(&mut padded[u32::SIZE..u32::SIZE + data_len]);
289 padded
290}
291
292const fn canonical_shard_len(data_len: usize, k: usize) -> usize {
299 let prefixed_len = data_len.saturating_add(u32::SIZE);
300 let mut shard_len = prefixed_len.div_ceil(k);
301
302 if !shard_len.is_multiple_of(2) {
304 shard_len = shard_len.saturating_add(1);
305 }
306
307 shard_len
308}
309
310const fn codeword_fits(n: usize, shard_len: usize) -> bool {
312 match n.checked_mul(shard_len) {
313 Some(bytes) => bytes <= MAX_ALLOCATION_BYTES,
314 None => false,
315 }
316}
317
318fn validate_data_layout(shards: &[&[u8]], expected_shard_len: usize) -> Result<usize, Error> {
321 let data_len = read_data_len(shards)?;
322 let mut data_end = u32::SIZE + data_len;
323 for shard in shards {
324 if data_end >= shard.len() {
325 data_end -= shard.len();
326 continue;
327 }
328 if !shard[data_end..].iter().all(|byte| *byte == 0) {
329 return Err(Error::Inconsistent);
330 }
331 data_end = 0;
332 }
333 if canonical_shard_len(data_len, shards.len()) != expected_shard_len {
334 return Err(Error::Inconsistent);
335 }
336 Ok(data_len)
337}
338
339fn extract_data(shards: &[&[u8]], k: usize, expected_shard_len: usize) -> Result<Vec<u8>, Error> {
341 let shards = shards.get(..k).ok_or(Error::NotEnoughChunks)?;
342 let data_len = validate_data_layout(shards, expected_shard_len)?;
343 let mut data = Vec::with_capacity(data_len);
344 let mut prefix_bytes_left = u32::SIZE;
345 let mut data_bytes_left = data_len;
346 for shard in shards {
347 if prefix_bytes_left >= shard.len() {
350 prefix_bytes_left -= shard.len();
351 continue;
352 }
353
354 let payload = &shard[prefix_bytes_left..];
356 let copy_len = data_bytes_left.min(payload.len());
357 data.extend_from_slice(&payload[..copy_len]);
358 data_bytes_left -= copy_len;
359
360 prefix_bytes_left = 0;
361 }
362 Ok(data)
363}
364
365fn read_data_len(shards: &[&[u8]]) -> Result<usize, Error> {
368 let total_len: usize = shards.iter().map(|s| s.len()).sum();
369 if total_len < u32::SIZE {
370 return Err(Error::Inconsistent);
371 }
372
373 let mut prefix = [0u8; u32::SIZE];
375 let mut prefix_len = 0usize;
376 for shard in shards {
377 if prefix_len == u32::SIZE {
378 break;
379 }
380 let read = (u32::SIZE - prefix_len).min(shard.len());
381 prefix[prefix_len..prefix_len + read].copy_from_slice(&shard[..read]);
382 prefix_len += read;
383 }
384
385 let data_len = u32::from_be_bytes(prefix) as usize;
386 let payload_len = total_len - u32::SIZE;
387 if data_len > payload_len {
388 return Err(Error::Inconsistent);
389 }
390 Ok(data_len)
391}
392
393type Encoding<D> = (D, Vec<Chunk<D>>);
395
396fn validate_counts(k: usize, m: usize) -> Result<(), Error> {
398 if Encoder::supports(k, m) {
399 Ok(())
400 } else {
401 Err(Error::ReedSolomon(RsError::UnsupportedShardCount {
402 original_count: k,
403 recovery_count: m,
404 }))
405 }
406}
407
408fn encode<H: Hasher, S: Strategy>(
422 total: u16,
423 min: u16,
424 data: impl bytes::Buf,
425 strategy: &S,
426) -> Result<Encoding<H::Digest>, Error> {
427 assert!(total > min);
429 assert!(min > 0);
430 let n = total as usize;
431 let k = min as usize;
432 let m = n - k;
433 let data_len = data.remaining();
434 if data_len > Widen::widen(u32::MAX) {
435 return Err(Error::InvalidDataLength(data_len));
436 }
437
438 validate_counts(k, m)?;
441
442 let shard_len = canonical_shard_len(data_len, k);
445 if shard_len > Widen::widen(u32::MAX) || !codeword_fits(n, shard_len) {
446 return Err(Error::InvalidDataLength(data_len));
447 }
448
449 let padded = prepare_data(data, k, shard_len);
451
452 let recovery_buf = strategy.try_run_batches(
454 shard_len / SHARD_CHUNK_BYTES,
455 NZUsize!(MIN_STRIPE_BYTES / SHARD_CHUNK_BYTES),
456 SHARD_CHUNK_BYTES * n,
457 |batches| {
458 let original_shards = padded.chunks(shard_len).collect::<Vec<_>>();
459 let mut buf = vec![0u8; m * shard_len];
460 batches.try_map_collect_vec(
461 |ranges| {
462 let ranges = striped::byte_ranges(shard_len, ranges);
463 let groups = striped::stripe_columns(&mut buf, shard_len, &ranges);
464 ranges.into_iter().zip(groups)
465 },
466 |(range, out)| striped::encode_recovery_into(k, m, range, &original_shards, out),
467 )?;
468 Ok::<_, Error>(buf)
469 },
470 )?;
471
472 let originals: Bytes = padded.into();
474 let recoveries: Bytes = recovery_buf.into();
475
476 let mut builder = Builder::<H>::new(n);
478 let shard_slices: Vec<Bytes> = (0..k)
479 .map(|i| originals.slice(i * shard_len..(i + 1) * shard_len))
480 .chain((0..m).map(|i| recoveries.slice(i * shard_len..(i + 1) * shard_len)))
481 .collect();
482 let shard_hashes = H::hash_many(&shard_slices, strategy);
483 for hash in &shard_hashes {
484 builder.add(hash);
485 }
486 let tree = builder.build();
487 let root = tree.root();
488
489 let mut chunks = Vec::with_capacity(n);
491 for (i, shard) in shard_slices.into_iter().enumerate() {
492 let proof = tree.proof(i as u32).map_err(|_| Error::InvalidProof)?;
493 chunks.push(Chunk::new(shard, i as u16, proof));
494 }
495
496 Ok((root, chunks))
497}
498
499struct DecodeCtx<'a, H: Hasher, S: Strategy> {
501 n: usize,
503 k: usize,
505 m: usize,
507 shard_len: usize,
509 root: &'a H::Digest,
511 strategy: &'a S,
513}
514
515mod striped {
541 use super::*;
542
543 pub(super) fn tiles(
557 k: usize,
558 m: usize,
559 range: Range<usize>,
560 ) -> impl Iterator<Item = Range<usize>> {
561 let work_count = 2 * (k + m).next_power_of_two();
562 let target = (MAX_TILE_WORK_BYTES / (work_count * SHARD_CHUNK_BYTES)).max(1);
563 let blocks = range.len().div_ceil(SHARD_CHUNK_BYTES);
564 let mut count = if blocks < 2 * target {
565 1
566 } else {
567 blocks.div_ceil(target)
568 };
569 let page = 4096 / SHARD_CHUNK_BYTES;
570 while count > 1 && count < blocks {
571 let (width, extra) = (blocks / count, blocks % count);
572 if !width.is_multiple_of(page) && (extra == 0 || !(width + 1).is_multiple_of(page)) {
573 break;
574 }
575 count += 1;
576 }
577
578 let (width, extra) = (blocks / count, blocks % count);
581 let (start, end) = (range.start, range.end);
582 let edge = move |i: usize| start + (i * width + i.min(extra)) * SHARD_CHUNK_BYTES;
583 (0..count).map(move |i| edge(i)..if i + 1 == count { end } else { edge(i + 1) })
584 }
585
586 pub(super) fn stripe_columns<'a>(
591 buf: &'a mut [u8],
592 shard_len: usize,
593 ranges: &[Range<usize>],
594 ) -> Vec<Vec<&'a mut [u8]>> {
595 let mut groups: Vec<Vec<&'a mut [u8]>> = ranges.iter().map(|_| Vec::new()).collect();
596 for shard in buf.chunks_mut(shard_len) {
597 let mut rest = shard;
598 for (group, range) in groups.iter_mut().zip(ranges) {
599 let (head, tail) = rest.split_at_mut(range.len());
600 group.push(head);
601 rest = tail;
602 }
603 }
604 groups
605 }
606
607 struct StripeOut<'a> {
610 originals: Vec<&'a mut [u8]>,
611 recoveries: Vec<&'a mut [u8]>,
612 }
613
614 #[derive(Clone, Copy)]
616 struct Missing<'a> {
617 originals: &'a [usize],
618 recoveries: &'a [usize],
619 }
620
621 #[derive(Clone, Copy)]
624 struct Provided<'a> {
625 originals: &'a [(usize, &'a [u8])],
626 recoveries: &'a [(usize, &'a [u8])],
627 plan: &'a Plan,
628 }
629
630 pub(super) fn byte_ranges(
638 shard_len: usize,
639 mut ranges: Vec<Range<usize>>,
640 ) -> Vec<Range<usize>> {
641 let full_blocks = shard_len / SHARD_CHUNK_BYTES;
642 for range in &mut ranges {
643 range.start *= SHARD_CHUNK_BYTES;
644 range.end = if range.end == full_blocks {
645 shard_len
646 } else {
647 range.end * SHARD_CHUNK_BYTES
648 };
649 }
650 ranges
651 }
652
653 fn recover_all_into(
660 k: usize,
661 m: usize,
662 range: Range<usize>,
663 provided: Provided<'_>,
664 missing: Missing<'_>,
665 mut out: StripeOut<'_>,
666 ) -> Result<(), Error> {
667 if range.is_empty() {
668 return Ok(());
669 }
670 let offset = range.start;
671 let mut tiles = tiles(k, m, range).peekable();
672 let mut len = tiles.peek().expect("a nonempty stripe has a tile").len();
673 let mut decoder = Cached::take(
674 &CACHED_DECODER,
675 || Decoder::new(k, m, len),
676 |dec| dec.reset(k, m, len),
677 )
678 .map_err(Error::ReedSolomon)?;
679
680 for tile in tiles {
684 let local = tile.start - offset..tile.end - offset;
685 if tile.len() != len {
686 len = tile.len();
687 decoder.reset(k, m, len).map_err(Error::ReedSolomon)?;
688 }
689
690 for (idx, shard) in provided.originals {
691 decoder
692 .add_original_shard(*idx, &shard[tile.clone()])
693 .map_err(Error::ReedSolomon)?;
694 }
695 for (idx, shard) in provided.recoveries {
696 decoder
697 .add_recovery_shard(*idx, &shard[tile.clone()])
698 .map_err(Error::ReedSolomon)?;
699 }
700 let decoding = decoder
701 .decode_with_recovery_plan(provided.plan)
702 .map_err(Error::ReedSolomon)?
703 .expect("decode runs only when an original is missing");
704
705 for (slot, &idx) in out.originals.iter_mut().zip(missing.originals) {
706 let shard = decoding.original(idx).ok_or(Error::Inconsistent)?;
707 slot[local.clone()].copy_from_slice(shard);
708 }
709 for (slot, &idx) in out.recoveries.iter_mut().zip(missing.recoveries) {
710 let shard = decoding.recovery(idx).ok_or(Error::Inconsistent)?;
711 slot[local.clone()].copy_from_slice(shard);
712 }
713 }
714 Ok(())
715 }
716
717 pub(super) fn encode_recovery_into(
720 k: usize,
721 m: usize,
722 range: Range<usize>,
723 originals: &[impl AsRef<[u8]>],
724 mut out: Vec<&mut [u8]>,
725 ) -> Result<(), Error> {
726 if range.is_empty() {
727 return Ok(());
728 }
729 let offset = range.start;
730 let mut tiles = tiles(k, m, range).peekable();
731 let mut len = tiles.peek().expect("a nonempty stripe has a tile").len();
732 let mut encoder = Cached::take(
733 &CACHED_ENCODER,
734 || Encoder::new(k, m, len),
735 |enc| enc.reset(k, m, len),
736 )
737 .map_err(Error::ReedSolomon)?;
738
739 for tile in tiles {
742 let local = tile.start - offset..tile.end - offset;
743 if tile.len() != len {
744 len = tile.len();
745 encoder.reset(k, m, len).map_err(Error::ReedSolomon)?;
746 }
747
748 for shard in originals.iter().take(k) {
749 let shard = shard.as_ref();
750 encoder
751 .add_original_shard(&shard[tile.clone()])
752 .map_err(Error::ReedSolomon)?;
753 }
754 let encoding = encoder.encode().map_err(Error::ReedSolomon)?;
755 for (slot, shard) in out.iter_mut().zip(encoding.recovery_iter()) {
756 slot[local.clone()].copy_from_slice(shard);
757 }
758 }
759 Ok(())
760 }
761
762 pub(super) fn decode<'a, H: Hasher, S: Strategy>(
766 ctx: &DecodeCtx<'_, H, S>,
767 batches: Batches<'_, S>,
768 shard_digests: Vec<Option<H::Digest>>,
769 provided_originals: Vec<(usize, &'a [u8])>,
770 provided_recoveries: Vec<(usize, &'a [u8])>,
771 ) -> Result<Vec<u8>, Error> {
772 let &DecodeCtx {
773 k, m, shard_len, ..
774 } = ctx;
775
776 let mut original_refs: Vec<&[u8]> = vec![&[]; k];
777 for &(idx, shard) in &provided_originals {
778 original_refs[idx] = shard;
779 }
780
781 let mut recovery_buf = vec![0u8; m * shard_len];
783 batches.try_map_collect_vec(
784 |ranges| {
785 let ranges = byte_ranges(shard_len, ranges);
786 let groups = stripe_columns(&mut recovery_buf, shard_len, &ranges);
787 ranges.into_iter().zip(groups)
788 },
789 |(range, out)| encode_recovery_into(k, m, range, &original_refs, out),
790 )?;
791 let recovery_refs: Vec<&[u8]> = recovery_buf.chunks_exact(shard_len).collect();
792 verify_reencoded::<H, S>(
793 ctx,
794 shard_digests,
795 &original_refs,
796 &recovery_refs,
797 &provided_recoveries,
798 )
799 }
800
801 pub(super) fn decode_reveal<'a, H: Hasher, S: Strategy>(
809 ctx: &DecodeCtx<'_, H, S>,
810 batches: Batches<'_, S>,
811 shard_digests: Vec<Option<H::Digest>>,
812 provided_originals: Vec<(usize, &'a [u8])>,
813 provided_recoveries: Vec<(usize, &'a [u8])>,
814 ) -> Result<Vec<u8>, Error> {
815 let &DecodeCtx {
816 k, m, shard_len, ..
817 } = ctx;
818
819 let missing_originals = shard_digests
820 .iter()
821 .take(k)
822 .enumerate()
823 .filter_map(|(i, digest)| digest.is_none().then_some(i))
824 .collect::<Vec<_>>();
825 let missing_recoveries = shard_digests
826 .iter()
827 .skip(k)
828 .enumerate()
829 .filter_map(|(i, digest)| digest.is_none().then_some(i))
830 .collect::<Vec<_>>();
831
832 let mut restored_originals = vec![0u8; missing_originals.len() * shard_len];
833 let mut restored_recoveries = vec![0u8; missing_recoveries.len() * shard_len];
834
835 let plan = Plan::new(
838 k,
839 m,
840 provided_originals.iter().map(|&(index, _)| index),
841 provided_recoveries.iter().map(|&(index, _)| index),
842 )
843 .map_err(Error::ReedSolomon)?;
844 let provided = Provided {
845 originals: &provided_originals,
846 recoveries: &provided_recoveries,
847 plan: &plan,
848 };
849 let missing = Missing {
850 originals: &missing_originals,
851 recoveries: &missing_recoveries,
852 };
853 batches.try_map_collect_vec(
854 |ranges| {
855 let ranges = byte_ranges(shard_len, ranges);
856 let original_groups = stripe_columns(&mut restored_originals, shard_len, &ranges);
857 let recovery_groups = stripe_columns(&mut restored_recoveries, shard_len, &ranges);
858 ranges
859 .into_iter()
860 .zip(original_groups.into_iter().zip(recovery_groups))
861 .map(|(range, (originals, recoveries))| {
862 (
863 range,
864 StripeOut {
865 originals,
866 recoveries,
867 },
868 )
869 })
870 },
871 |(range, out)| recover_all_into(k, m, range, provided, missing, out),
872 )?;
873
874 let mut original_refs: Vec<&[u8]> = vec![&[]; k];
875 for &(idx, shard) in &provided_originals {
876 original_refs[idx] = shard;
877 }
878 for (pos, idx) in missing_originals.iter().enumerate() {
879 let start = pos * shard_len;
880 original_refs[*idx] = &restored_originals[start..start + shard_len];
881 }
882 let mut recovery_refs: Vec<&[u8]> = vec![&[]; m];
883 for (pos, idx) in missing_recoveries.iter().enumerate() {
884 let start = pos * shard_len;
885 recovery_refs[*idx] = &restored_recoveries[start..start + shard_len];
886 }
887
888 if provided_originals.is_empty() {
889 let data_len = validate_data_layout(&original_refs, shard_len)?;
893 verify_commitment::<H, S>(ctx, shard_digests, &original_refs, &recovery_refs)?;
894 restored_originals.copy_within(u32::SIZE..u32::SIZE + data_len, 0);
895 restored_originals.truncate(data_len);
896 Ok(restored_originals)
897 } else {
898 verify_root::<H, S>(ctx, shard_digests, &original_refs, &recovery_refs)
899 }
900 }
901}
902
903fn verify_root<H: Hasher, S: Strategy>(
906 ctx: &DecodeCtx<'_, H, S>,
907 shard_digests: Vec<Option<H::Digest>>,
908 originals: &[&[u8]],
909 recoveries: &[&[u8]],
910) -> Result<Vec<u8>, Error> {
911 let data = extract_data(originals, ctx.k, ctx.shard_len)?;
912 verify_commitment::<H, S>(ctx, shard_digests, originals, recoveries)?;
913 Ok(data)
914}
915
916fn verify_commitment<H: Hasher, S: Strategy>(
918 ctx: &DecodeCtx<'_, H, S>,
919 mut shard_digests: Vec<Option<H::Digest>>,
920 originals: &[&[u8]],
921 recoveries: &[&[u8]],
922) -> Result<(), Error> {
923 let &DecodeCtx {
924 n,
925 k,
926 root,
927 strategy,
928 ..
929 } = ctx;
930 let (missing_indices, missing_payloads): (Vec<_>, Vec<_>) = shard_digests
931 .iter()
932 .enumerate()
933 .filter(|(_, digest)| digest.is_none())
934 .map(|(i, _)| {
935 (
936 i,
937 if i < k {
938 originals[i]
939 } else {
940 recoveries[i - k]
941 },
942 )
943 })
944 .unzip();
945 for (i, digest) in missing_indices
946 .into_iter()
947 .zip(H::hash_many(&missing_payloads, strategy))
948 {
949 shard_digests[i] = Some(digest);
950 }
951
952 let mut builder = Builder::<H>::new(n);
953 shard_digests
954 .into_iter()
955 .map(|digest| digest.expect("digest must be present for every shard"))
956 .for_each(|digest| {
957 builder.add(&digest);
958 });
959 let tree = builder.build();
960 if tree.root() != *root {
961 return Err(Error::Inconsistent);
962 }
963
964 Ok(())
965}
966
967fn verify_reencoded<H: Hasher, S: Strategy>(
972 ctx: &DecodeCtx<'_, H, S>,
973 shard_digests: Vec<Option<H::Digest>>,
974 originals: &[&[u8]],
975 recoveries: &[&[u8]],
976 provided_recoveries: &[(usize, &[u8])],
977) -> Result<Vec<u8>, Error> {
978 for &(idx, shard) in provided_recoveries {
980 if shard != recoveries[idx] {
981 return Err(Error::Inconsistent);
982 }
983 }
984 verify_root::<H, S>(ctx, shard_digests, originals, recoveries)
985}
986
987fn decode<'a, H: Hasher, S: Strategy>(
1003 total: u16,
1004 min: u16,
1005 root: &H::Digest,
1006 chunks: impl Iterator<Item = &'a CheckedChunk<H>>,
1007 strategy: &S,
1008) -> Result<Vec<u8>, Error> {
1009 assert!(total > min);
1011 assert!(min > 0);
1012 let n = total as usize;
1013 let k = min as usize;
1014 let m = n - k;
1015
1016 validate_counts(k, m)?;
1018 let mut chunks = chunks.peekable();
1019 let Some(first) = chunks.peek() else {
1020 return Err(Error::NotEnoughChunks);
1021 };
1022
1023 let shard_len = first.shard.len();
1025 if !codeword_fits(n, shard_len) {
1026 return Err(RsError::InvalidShardSize {
1027 shard_bytes: shard_len,
1028 }
1029 .into());
1030 }
1031 let mut shard_digests: Vec<Option<H::Digest>> = vec![None; n];
1032 let mut provided_originals: Vec<(usize, &[u8])> = Vec::new();
1033 let mut provided_recoveries: Vec<(usize, &[u8])> = Vec::new();
1034 let mut provided = 0usize;
1035 for chunk in chunks {
1036 provided += 1;
1037 if &chunk.root != root {
1038 return Err(Error::CommitmentMismatch);
1039 }
1040
1041 if chunk.shard.len() != shard_len {
1044 return Err(Error::Inconsistent);
1045 }
1046
1047 let index = chunk.index;
1049 if index >= total {
1050 return Err(Error::InvalidIndex(index));
1051 }
1052 let digest_slot = &mut shard_digests[index as usize];
1053 if digest_slot.is_some() {
1054 return Err(Error::DuplicateIndex(index));
1055 }
1056
1057 *digest_slot = Some(chunk.digest);
1059 if index < min {
1060 provided_originals.push((index as usize, chunk.shard.as_ref()));
1061 } else {
1062 provided_recoveries.push((index as usize - k, chunk.shard.as_ref()));
1063 }
1064 }
1065 if provided < k {
1066 return Err(Error::NotEnoughChunks);
1067 }
1068
1069 let recovery_needed = provided_originals.len() < k;
1076 if recovery_needed {
1077 let keep = k - provided_originals.len();
1078 for &(idx, _) in &provided_recoveries[keep..] {
1079 shard_digests[k + idx] = None;
1080 }
1081 provided_recoveries.truncate(keep);
1082 }
1083
1084 let ctx = DecodeCtx {
1085 n,
1086 k,
1087 m,
1088 shard_len,
1089 root,
1090 strategy,
1091 };
1092
1093 if recovery_needed {
1095 strategy.try_run_batches(
1096 shard_len / SHARD_CHUNK_BYTES,
1097 NZUsize!(MIN_STRIPE_BYTES / SHARD_CHUNK_BYTES),
1098 SHARD_CHUNK_BYTES * n,
1099 |batches| {
1100 striped::decode_reveal::<H, S>(
1101 &ctx,
1102 batches,
1103 shard_digests,
1104 provided_originals,
1105 provided_recoveries,
1106 )
1107 },
1108 )
1109 } else {
1110 strategy.try_run_batches(
1111 shard_len / SHARD_CHUNK_BYTES,
1112 NZUsize!(MIN_STRIPE_BYTES / SHARD_CHUNK_BYTES),
1113 SHARD_CHUNK_BYTES * n,
1114 |batches| {
1115 striped::decode::<H, S>(
1116 &ctx,
1117 batches,
1118 shard_digests,
1119 provided_originals,
1120 provided_recoveries,
1121 )
1122 },
1123 )
1124 }
1125}
1126
1127pub struct ReedSolomon<H> {
1250 _marker: PhantomData<H>,
1251}
1252
1253impl<H> Clone for ReedSolomon<H> {
1254 fn clone(&self) -> Self {
1255 *self
1256 }
1257}
1258
1259impl<H> Copy for ReedSolomon<H> {}
1260
1261impl<H> std::fmt::Debug for ReedSolomon<H> {
1262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1263 f.debug_struct("ReedSolomon").finish()
1264 }
1265}
1266
1267impl<H: Hasher> Scheme for ReedSolomon<H> {
1268 type Commitment = H::Digest;
1269 type Shard = Chunk<H::Digest>;
1270 type CheckedShard = CheckedChunk<H>;
1271 type Error = Error;
1272
1273 fn max_shard_size(config: &Config, data: usize) -> usize {
1282 let total = total_shards(config).expect("total shards must fit in u16");
1284 let n = usize::from(total);
1285 let k = usize::from(config.minimum_shards.get());
1286 validate_counts(k, n - k).expect("backend must support the shard counts");
1287 u32::try_from(data).expect("data must fit the u32 length prefix");
1288 let len = canonical_shard_len(data, k);
1289 assert!(
1290 codeword_fits(n, len),
1291 "codeword must fit the allocation bound"
1292 );
1293
1294 let siblings: usize = Widen::widen(u32::from(total).next_power_of_two().trailing_zeros());
1298 Chunk::<H::Digest>::max_encode_size(len, siblings)
1299 }
1300
1301 fn encode(
1302 config: &Config,
1303 data: impl bytes::Buf,
1304 strategy: &impl Strategy,
1305 ) -> Result<(Self::Commitment, Vec<Self::Shard>), Self::Error> {
1306 encode::<H, _>(
1307 total_shards(config)?,
1308 config.minimum_shards.get(),
1309 data,
1310 strategy,
1311 )
1312 }
1313
1314 fn check(
1315 config: &Config,
1316 commitment: &Self::Commitment,
1317 index: u16,
1318 shard: &Self::Shard,
1319 strategy: &impl Strategy,
1320 ) -> Result<Self::CheckedShard, Self::Error> {
1321 let total = total_shards(config)?;
1322 check_chunk::<H>(total, commitment, index, shard, || {
1323 H::hash_across(&[&shard.shard], strategy)
1324 })
1325 }
1326
1327 fn check_many(
1328 config: &Config,
1329 commitment: &Self::Commitment,
1330 shards: &[(u16, &Self::Shard)],
1331 strategy: &impl Strategy,
1332 ) -> Vec<Result<Self::CheckedShard, Self::Error>> {
1333 let Ok(total) = total_shards(config) else {
1334 return shards
1335 .iter()
1336 .map(|&(index, shard)| Self::check(config, commitment, index, shard, strategy))
1337 .collect();
1338 };
1339
1340 let width = shards.first().map_or(0, |(_, shard)| shard.shard.len());
1343 if shards.iter().any(|&(index, shard)| {
1344 shard.shard.len() != width || check_metadata(total, index, shard).is_err()
1345 }) {
1346 return strategy.map_collect_vec(shards, |&(index, shard)| {
1347 Self::check(config, commitment, index, shard, strategy)
1348 });
1349 }
1350
1351 let payloads = shards
1352 .iter()
1353 .map(|(_, shard)| shard.shard.as_ref())
1354 .collect::<Vec<_>>();
1355 let digests = H::hash_many(&payloads, strategy);
1356 strategy.map_collect_vec(
1357 shards.iter().copied().zip(digests),
1358 |((index, shard), digest)| check_chunk::<H>(total, commitment, index, shard, || digest),
1359 )
1360 }
1361
1362 fn decode<'a>(
1363 config: &Config,
1364 commitment: &Self::Commitment,
1365 shards: impl Iterator<Item = &'a Self::CheckedShard>,
1366 strategy: &impl Strategy,
1367 ) -> Result<Vec<u8>, Self::Error> {
1368 decode::<H, _>(
1369 total_shards(config)?,
1370 config.minimum_shards.get(),
1371 commitment,
1372 shards,
1373 strategy,
1374 )
1375 }
1376}
1377
1378#[cfg(test)]
1379mod tests {
1380 use super::*;
1381 use bytes::Buf as _;
1382 use commonware_codec::{Decode as _, Encode, MaxEncodeSize, varint::UInt};
1383 use commonware_cryptography::{Sha256, sha256::Digest as Sha256Digest};
1384 use commonware_invariants::minifuzz;
1385 use commonware_parallel::{
1386 Rayon, Sequential,
1387 mocks::{Explorer, explore, inline},
1388 };
1389 use commonware_runtime::iobuf::EncodeExt;
1390 use commonware_utils::{NZU16, NZUsize};
1391 use rstest::rstest;
1392 use std::{any::TypeId, cell::RefCell};
1393
1394 type RS = ReedSolomon<Sha256>;
1395 type InstrumentedRS = ReedSolomon<InstrumentedSha256>;
1396 const STRATEGY: Sequential = Sequential;
1397 const FUZZ_MAX_MIN_SHARDS: u16 = 8;
1398 const FUZZ_MAX_EXTRA_SHARDS: u16 = 8;
1399 const FUZZ_MAX_DATA_LEN: usize = 256;
1400 const FUZZ_MAX_EXTRA_SHARD_WIDTH: usize = 16;
1401
1402 std::thread_local! {
1403 static HASH_MANY_CALLS: RefCell<Vec<Vec<Vec<u8>>>> = const { RefCell::new(Vec::new()) };
1404 static HASH_INPUTS: RefCell<Vec<Vec<u8>>> = const { RefCell::new(Vec::new()) };
1405 }
1406
1407 #[derive(Default)]
1408 struct InstrumentedSha256(Sha256);
1409
1410 impl Hasher for InstrumentedSha256 {
1411 type Digest = <Sha256 as Hasher>::Digest;
1412
1413 fn hash(parts: &[&[u8]]) -> Self::Digest {
1414 if let [message] = parts {
1415 HASH_INPUTS.with(|inputs| inputs.borrow_mut().push(message.to_vec()));
1416 }
1417 Sha256::hash(parts)
1418 }
1419
1420 fn hash_pair(
1421 left: &[&[u8]],
1422 right: &[&[u8]],
1423 strategy: &impl Strategy,
1424 ) -> (Self::Digest, Self::Digest) {
1425 Sha256::hash_pair(left, right, strategy)
1426 }
1427
1428 fn hash_many<M: AsRef<[u8]> + Sync>(
1429 messages: &[M],
1430 _strategy: &impl Strategy,
1431 ) -> Vec<Self::Digest> {
1432 HASH_INPUTS.with(|inputs| {
1433 inputs
1434 .borrow_mut()
1435 .extend(messages.iter().map(|message| message.as_ref().to_vec()));
1436 });
1437 HASH_MANY_CALLS.with(|calls| {
1438 calls.borrow_mut().push(
1439 messages
1440 .iter()
1441 .map(|message| message.as_ref().to_vec())
1442 .collect(),
1443 );
1444 });
1445 messages
1446 .iter()
1447 .map(|message| Sha256::hash(&[message.as_ref()]))
1448 .collect()
1449 }
1450
1451 fn update(&mut self, bytes: &[u8]) -> &mut Self {
1452 self.0.update(bytes);
1453 self
1454 }
1455
1456 fn finalize(self) -> (Self, Self::Digest) {
1457 let (hasher, digest) = self.0.finalize();
1458 (Self(hasher), digest)
1459 }
1460 }
1461
1462 fn reset_hash_many_calls() {
1463 HASH_INPUTS.with(|inputs| inputs.borrow_mut().clear());
1464 HASH_MANY_CALLS.with(|calls| calls.borrow_mut().clear());
1465 }
1466
1467 fn take_hash_many_calls() -> Vec<Vec<Vec<u8>>> {
1468 HASH_MANY_CALLS.with(|calls| std::mem::take(&mut *calls.borrow_mut()))
1469 }
1470
1471 fn checked(
1472 root: <Sha256 as Hasher>::Digest,
1473 chunk: Chunk<<Sha256 as Hasher>::Digest>,
1474 ) -> CheckedChunk<Sha256> {
1475 let Chunk { shard, index, .. } = chunk;
1476 let digest = Sha256::hash(&[&shard]);
1477 CheckedChunk::new(root, shard, index, digest)
1478 }
1479
1480 fn build_chunks(
1481 shards: &[Vec<u8>],
1482 ) -> (
1483 <Sha256 as Hasher>::Digest,
1484 Vec<Chunk<<Sha256 as Hasher>::Digest>>,
1485 ) {
1486 let mut builder = Builder::<Sha256>::new(shards.len());
1487 for shard in shards {
1488 builder.add(&Sha256::hash(&[shard]));
1489 }
1490 let tree = builder.build();
1491 let root = tree.root();
1492 let chunks = shards
1493 .iter()
1494 .enumerate()
1495 .map(|(i, shard)| {
1496 let proof = tree.proof(i as u32).unwrap();
1497 Chunk::new(shard.clone().into(), i as u16, proof)
1498 })
1499 .collect();
1500
1501 (root, chunks)
1502 }
1503
1504 #[test]
1507 fn test_checked_shard_type_is_bound_to_hasher() {
1508 assert_eq!(
1509 TypeId::of::<<RS as Scheme>::Commitment>(),
1510 TypeId::of::<<InstrumentedRS as Scheme>::Commitment>()
1511 );
1512 assert_ne!(
1513 TypeId::of::<<RS as Scheme>::CheckedShard>(),
1514 TypeId::of::<<InstrumentedRS as Scheme>::CheckedShard>()
1515 );
1516 }
1517
1518 #[test]
1519 fn test_check_many_batches_payloads_in_input_order() {
1520 let config = Config {
1521 minimum_shards: NZU16!(4),
1522 extra_shards: NZU16!(4),
1523 };
1524 let (root, chunks) =
1525 RS::encode(&config, b"ordered batch checking".as_slice(), &STRATEGY).unwrap();
1526 let requested = [3u16, 0, 6, 2];
1527 let shards = requested
1528 .iter()
1529 .map(|&index| (index, &chunks[usize::from(index)]))
1530 .collect::<Vec<_>>();
1531 let expected_payloads = requested
1532 .iter()
1533 .map(|&index| chunks[usize::from(index)].shard.to_vec())
1534 .collect::<Vec<_>>();
1535
1536 reset_hash_many_calls();
1537 let checked = InstrumentedRS::check_many(&config, &root, &shards, &STRATEGY)
1538 .into_iter()
1539 .collect::<Result<Vec<_>, _>>()
1540 .unwrap();
1541
1542 assert_eq!(take_hash_many_calls(), vec![expected_payloads]);
1543 assert_eq!(
1544 checked.iter().map(|chunk| chunk.index).collect::<Vec<_>>(),
1545 requested
1546 );
1547 for (checked, &index) in checked.iter().zip(&requested) {
1548 let scalar = InstrumentedRS::check(
1549 &config,
1550 &root,
1551 index,
1552 &chunks[usize::from(index)],
1553 &STRATEGY,
1554 )
1555 .unwrap();
1556 assert_eq!(checked, &scalar);
1557 }
1558 }
1559
1560 #[test]
1561 fn test_instrumented_encode_matches_sha256_commitment_and_wire() {
1562 let config = Config {
1563 minimum_shards: NZU16!(5),
1564 extra_shards: NZU16!(7),
1565 };
1566 let data = (0..4096).map(|i| i as u8).collect::<Vec<_>>();
1567 let (expected_root, expected_chunks) =
1568 RS::encode(&config, data.as_slice(), &STRATEGY).unwrap();
1569
1570 reset_hash_many_calls();
1571 let (actual_root, actual_chunks) =
1572 InstrumentedRS::encode(&config, data.as_slice(), &STRATEGY).unwrap();
1573
1574 assert_eq!(actual_root, expected_root);
1575 assert_eq!(actual_chunks, expected_chunks);
1576 assert_eq!(
1577 actual_chunks.iter().map(Encode::encode).collect::<Vec<_>>(),
1578 expected_chunks
1579 .iter()
1580 .map(Encode::encode)
1581 .collect::<Vec<_>>()
1582 );
1583 let calls = take_hash_many_calls();
1584 assert_eq!(calls.len(), 1);
1585 assert_eq!(calls[0].len(), config.total_shards() as usize);
1586 }
1587
1588 #[test]
1589 fn test_decode_reuses_checked_digests_and_rehashes_trimmed_surplus() {
1590 let config = Config {
1591 minimum_shards: NZU16!(3),
1592 extra_shards: NZU16!(3),
1593 };
1594 let data = (0..1024).map(|i| (i * 7) as u8).collect::<Vec<_>>();
1595 let (root, chunks) = InstrumentedRS::encode(&config, data.as_slice(), &STRATEGY).unwrap();
1596 let checked = InstrumentedRS::check_many(
1597 &config,
1598 &root,
1599 &(0..6u16)
1600 .map(|index| (index, &chunks[usize::from(index)]))
1601 .collect::<Vec<_>>(),
1602 &STRATEGY,
1603 )
1604 .into_iter()
1605 .collect::<Result<Vec<_>, _>>()
1606 .unwrap();
1607 let selected = [&checked[0], &checked[3], &checked[4], &checked[5]];
1608
1609 reset_hash_many_calls();
1610 let decoded =
1611 InstrumentedRS::decode(&config, &root, selected.into_iter(), &STRATEGY).unwrap();
1612
1613 assert_eq!(decoded, data);
1614 assert_eq!(
1615 take_hash_many_calls(),
1616 vec![vec![
1617 chunks[1].shard.to_vec(),
1618 chunks[2].shard.to_vec(),
1619 chunks[5].shard.to_vec(),
1620 ]]
1621 );
1622 }
1623
1624 #[test]
1625 fn test_check_many_rejects_metadata_before_hashing() {
1626 let config = Config {
1627 minimum_shards: NZU16!(3),
1628 extra_shards: NZU16!(3),
1629 };
1630 let data = (0..192).map(|i| i as u8).collect::<Vec<_>>();
1631 let (root, chunks) = RS::encode(&config, data.as_slice(), &STRATEGY).unwrap();
1632 let mut invalid_index = chunks[1].clone();
1633 invalid_index.index = 5;
1634 invalid_index.shard = vec![0xa1; chunks[1].shard.len()].into();
1635 let mut invalid_leaf_count = chunks[2].clone();
1636 invalid_leaf_count.proof.leaf_count -= 1;
1637 invalid_leaf_count.shard = vec![0xb2; chunks[2].shard.len()].into();
1638 let mut out_of_range = chunks[3].clone();
1639 out_of_range.shard = vec![0xc3; chunks[3].shard.len()].into();
1640 let mut invalid_proof = chunks[4].clone();
1641 invalid_proof.shard = vec![0xd4; chunks[4].shard.len()].into();
1642 let shards = [
1643 (1, &invalid_index),
1644 (0, &chunks[0]),
1645 (2, &invalid_leaf_count),
1646 (6, &out_of_range),
1647 (4, &invalid_proof),
1648 ];
1649 let expected = shards
1650 .iter()
1651 .map(|&(index, shard)| {
1652 format!("{:?}", RS::check(&config, &root, index, shard, &STRATEGY))
1653 })
1654 .collect::<Vec<_>>();
1655
1656 reset_hash_many_calls();
1657 let actual = InstrumentedRS::check_many(&config, &root, &shards, &STRATEGY)
1658 .into_iter()
1659 .map(|result| format!("{result:?}"))
1660 .collect::<Vec<_>>();
1661 assert_eq!(actual, expected);
1662 let inputs = HASH_INPUTS.with(|inputs| std::mem::take(&mut *inputs.borrow_mut()));
1663 for rejected in [&invalid_index, &invalid_leaf_count, &out_of_range] {
1664 assert!(
1665 !inputs
1666 .iter()
1667 .any(|input| input.as_slice() == rejected.shard)
1668 );
1669 }
1670 for eligible in [&chunks[0], &invalid_proof] {
1671 assert!(
1672 inputs
1673 .iter()
1674 .any(|input| input.as_slice() == eligible.shard)
1675 );
1676 }
1677 }
1678
1679 #[test]
1680 fn test_check_many_checks_mixed_widths_individually() {
1681 let config = Config {
1682 minimum_shards: NZU16!(32),
1683 extra_shards: NZU16!(32),
1684 };
1685 let payloads = (0..64)
1686 .map(|i| vec![i as u8; if i < 8 { 8192 - i * 64 } else { 2 }])
1687 .collect::<Vec<_>>();
1688 let (root, chunks) = build_chunks(&payloads);
1689 let shards = chunks
1690 .iter()
1691 .enumerate()
1692 .map(|(index, chunk)| (index as u16, chunk))
1693 .collect::<Vec<_>>();
1694 let expected = shards
1695 .iter()
1696 .map(|&(index, shard)| {
1697 InstrumentedRS::check(&config, &root, index, shard, &STRATEGY).unwrap()
1698 })
1699 .collect::<Vec<_>>();
1700
1701 let strategy = Explorer::parallel(&inline(NZUsize!(8)));
1704 let (actual, calls) = strategy
1705 .join(
1706 || {
1707 reset_hash_many_calls();
1708 let actual = InstrumentedRS::check_many(&config, &root, &shards, &strategy)
1709 .into_iter()
1710 .collect::<Result<Vec<_>, _>>()
1711 .unwrap();
1712 (actual, take_hash_many_calls())
1713 },
1714 || (),
1715 )
1716 .0;
1717 assert_eq!(actual, expected);
1718 assert!(calls.is_empty());
1719 assert!(matches!(
1720 InstrumentedRS::decode(&config, &root, actual.iter(), &STRATEGY),
1721 Err(Error::Inconsistent)
1722 ));
1723 }
1724
1725 #[test]
1726 fn test_check_many_matches_scalar_errors() {
1727 let config = Config {
1728 minimum_shards: NZU16!(3),
1729 extra_shards: NZU16!(3),
1730 };
1731 let (root, chunks) =
1732 RS::encode(&config, b"mixed check errors".as_slice(), &STRATEGY).unwrap();
1733 let mut invalid_leaf_count = chunks[1].clone();
1734 invalid_leaf_count.proof.leaf_count -= 1;
1735 let mut invalid_chunk_index = chunks[2].clone();
1736 invalid_chunk_index.index = 4;
1737 let mut invalid_payload = chunks[3].clone();
1738 let mut payload = invalid_payload.shard.to_vec();
1739 payload[0] ^= 0xff;
1740 invalid_payload.shard = payload.into();
1741 let shards = [
1742 (1, &invalid_leaf_count),
1743 (0, &chunks[0]),
1744 (6, &chunks[1]),
1745 (2, &invalid_chunk_index),
1746 (3, &invalid_payload),
1747 ];
1748
1749 let scalar = shards
1750 .iter()
1751 .map(|&(index, shard)| {
1752 format!(
1753 "{:?}",
1754 InstrumentedRS::check(&config, &root, index, shard, &STRATEGY)
1755 )
1756 })
1757 .collect::<Vec<_>>();
1758 let batched = InstrumentedRS::check_many(&config, &root, &shards, &STRATEGY)
1759 .into_iter()
1760 .map(|result| format!("{result:?}"))
1761 .collect::<Vec<_>>();
1762 assert_eq!(batched, scalar);
1763
1764 let invalid_config = Config {
1765 minimum_shards: NZU16!(u16::MAX),
1766 extra_shards: NZU16!(1),
1767 };
1768 let scalar = shards
1769 .iter()
1770 .map(|&(index, shard)| {
1771 format!(
1772 "{:?}",
1773 InstrumentedRS::check(&invalid_config, &root, index, shard, &STRATEGY)
1774 )
1775 })
1776 .collect::<Vec<_>>();
1777 reset_hash_many_calls();
1778 let batched = InstrumentedRS::check_many(&invalid_config, &root, &shards, &STRATEGY)
1779 .into_iter()
1780 .map(|result| format!("{result:?}"))
1781 .collect::<Vec<_>>();
1782 assert_eq!(batched, scalar);
1783 assert!(take_hash_many_calls().is_empty());
1784 }
1785
1786 fn selected_indices(
1787 u: &mut arbitrary::Unstructured<'_>,
1788 total: u16,
1789 minimum: u16,
1790 ) -> arbitrary::Result<Vec<u16>> {
1791 let to_use = u.int_in_range(minimum..=total)?;
1792 let mut selected = (0..total).collect::<Vec<_>>();
1793 for i in 0..usize::from(to_use) {
1794 let remaining = usize::from(total) - i;
1795 let j = i + u.choose_index(remaining)?;
1796 selected.swap(i, j);
1797 }
1798 selected.truncate(usize::from(to_use));
1799 Ok(selected)
1800 }
1801
1802 fn assert_decode_unique_commitment(
1803 total: u16,
1804 min: u16,
1805 root: <Sha256 as Hasher>::Digest,
1806 chunks: &[Chunk<<Sha256 as Hasher>::Digest>],
1807 selected: &[u16],
1808 ) {
1809 let config = Config {
1810 minimum_shards: NZU16!(min),
1811 extra_shards: NZU16!(total - min),
1812 };
1813 let pieces = selected
1814 .iter()
1815 .map(|&i| RS::check(&config, &root, i, &chunks[usize::from(i)], &STRATEGY).unwrap())
1816 .collect::<Vec<_>>();
1817
1818 let Ok(decoded) = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY) else {
1819 return;
1820 };
1821 let (canonical_root, _) =
1822 encode::<Sha256, _>(total, min, decoded.as_slice(), &STRATEGY).unwrap();
1823 assert_eq!(
1824 root, canonical_root,
1825 "decode accepted a root not produced by canonical encode"
1826 );
1827 }
1828
1829 fn fuzz_arbitrary_codeword(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()> {
1830 let min = u.int_in_range(1..=FUZZ_MAX_MIN_SHARDS)?;
1831 let extra = u.int_in_range(1..=FUZZ_MAX_EXTRA_SHARDS)?;
1832 let total = min + extra;
1833 let k = usize::from(min);
1834 let m = usize::from(extra);
1835
1836 let data_len = u.int_in_range(0..=FUZZ_MAX_DATA_LEN)?;
1837 let data = u.bytes(data_len)?.to_vec();
1838 let canonical = canonical_shard_len(data.len(), k);
1839 let extra_width = u.int_in_range(0..=FUZZ_MAX_EXTRA_SHARD_WIDTH / 2)? * 2;
1840 let shard_len = canonical + extra_width;
1841
1842 let mut padded = vec![0u8; k * shard_len];
1843 padded[..u32::SIZE].copy_from_slice(&(data.len() as u32).to_be_bytes());
1844 padded[u32::SIZE..u32::SIZE + data.len()].copy_from_slice(&data);
1845
1846 let payload_end = u32::SIZE + data.len();
1847 if payload_end < padded.len() && u.int_in_range(0..=3)? == 0 {
1848 let offset = payload_end + u.choose_index(padded.len() - payload_end)?;
1849 padded[offset] ^= u.arbitrary::<u8>()? | 1;
1850 }
1851
1852 let mut encoder = Encoder::new(k, m, shard_len).unwrap();
1853 for shard in padded.chunks(shard_len) {
1854 encoder.add_original_shard(shard).unwrap();
1855 }
1856 let recovery = encoder.encode().unwrap();
1857
1858 let mut shards = padded
1859 .chunks(shard_len)
1860 .map(|shard| shard.to_vec())
1861 .collect::<Vec<_>>();
1862 shards.extend(recovery.recovery_iter().map(|shard| shard.to_vec()));
1863
1864 let (root, chunks) = build_chunks(&shards);
1865 let selected = selected_indices(u, total, min)?;
1866 assert_decode_unique_commitment(total, min, root, &chunks, &selected);
1867
1868 Ok(())
1869 }
1870
1871 fn fuzz_mixed_codeword(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()> {
1872 let min = u.int_in_range(1..=FUZZ_MAX_MIN_SHARDS)?;
1873 let extra = u.int_in_range(1..=FUZZ_MAX_EXTRA_SHARDS)?;
1874 let total = min + extra;
1875
1876 let data_len = u.int_in_range(0..=FUZZ_MAX_DATA_LEN)?;
1877 let data = u.bytes(data_len)?.to_vec();
1878 let (_canonical_root, chunks) =
1879 encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1880 let mut shards = chunks
1881 .iter()
1882 .map(|chunk| chunk.shard.to_vec())
1883 .collect::<Vec<_>>();
1884
1885 let mutated = usize::from(min + u.int_in_range(0..=extra - 1)?);
1886 let offset = u.choose_index(shards[mutated].len())?;
1887 shards[mutated][offset] ^= u.arbitrary::<u8>()? | 1;
1888
1889 let (root, chunks) = build_chunks(&shards);
1890 let mut selected = (0..min).collect::<Vec<_>>();
1891 selected.push(mutated as u16);
1892 assert_decode_unique_commitment(total, min, root, &chunks, &selected);
1893
1894 Ok(())
1895 }
1896
1897 #[test]
1898 fn test_recovery() {
1899 let data = b"Testing recovery pieces";
1900 let total = 8u16;
1901 let min = 3u16;
1902
1903 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1905
1906 let pieces: Vec<_> = vec![
1908 checked(root, chunks[0].clone()), checked(root, chunks[4].clone()), checked(root, chunks[6].clone()), ];
1912
1913 let decoded = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY).unwrap();
1915 assert_eq!(decoded, data);
1916 }
1917
1918 #[test]
1919 fn test_not_enough_pieces() {
1920 let data = b"Test insufficient pieces";
1921 let total = 6u16;
1922 let min = 4u16;
1923
1924 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1926
1927 let pieces: Vec<_> = chunks
1929 .into_iter()
1930 .take(2)
1931 .map(|c| checked(root, c))
1932 .collect();
1933
1934 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1936 assert!(matches!(result, Err(Error::NotEnoughChunks)));
1937 }
1938
1939 #[test]
1940 fn test_duplicate_index() {
1941 let data = b"Test duplicate detection";
1942 let total = 5u16;
1943 let min = 3u16;
1944
1945 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1947
1948 let pieces = [
1950 checked(root, chunks[0].clone()),
1951 checked(root, chunks[0].clone()),
1952 checked(root, chunks[1].clone()),
1953 ];
1954
1955 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1957 assert!(matches!(result, Err(Error::DuplicateIndex(0))));
1958 }
1959
1960 #[test]
1961 fn test_invalid_index() {
1962 let data = b"Test invalid index";
1963 let total = 5u16;
1964 let min = 3u16;
1965 let config = Config {
1966 minimum_shards: NZU16!(min),
1967 extra_shards: NZU16!(total - min),
1968 };
1969
1970 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1972
1973 for i in 0..total {
1975 assert!(RS::check(&config, &root, i + 1, &chunks[i as usize], &STRATEGY).is_err());
1976 }
1977 }
1978
1979 #[test]
1980 #[should_panic(expected = "assertion failed: total > min")]
1981 fn test_invalid_total() {
1982 let data = b"Test parameter validation";
1983
1984 encode::<Sha256, _>(3, 3, data.as_slice(), &STRATEGY).unwrap();
1986 }
1987
1988 #[test]
1989 #[should_panic(expected = "assertion failed: min > 0")]
1990 fn test_invalid_min() {
1991 let data = b"Test parameter validation";
1992
1993 encode::<Sha256, _>(5, 0, data.as_slice(), &STRATEGY).unwrap();
1995 }
1996
1997 #[test]
1998 fn test_empty_data() {
1999 let data = b"";
2000 let total = 100u16;
2001 let min = 30u16;
2002
2003 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2005
2006 let minimal = chunks
2008 .into_iter()
2009 .take(min as usize)
2010 .map(|c| checked(root, c))
2011 .collect::<Vec<_>>();
2012 let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
2013 assert_eq!(decoded, data);
2014 }
2015
2016 #[test]
2017 fn test_large_data() {
2018 let data = vec![42u8; 1000]; let total = 7u16;
2020 let min = 4u16;
2021
2022 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2024
2025 let minimal = chunks
2027 .into_iter()
2028 .take(min as usize)
2029 .map(|c| checked(root, c))
2030 .collect::<Vec<_>>();
2031 let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
2032 assert_eq!(decoded, data);
2033 }
2034
2035 #[test]
2036 fn test_parallel_encode_matches_sequential() {
2037 let total = 24u16;
2038 let min = 8u16;
2039 for shard_len in [
2040 MIN_STRIPE_BYTES - 2,
2041 MIN_STRIPE_BYTES,
2042 2 * MIN_STRIPE_BYTES - 2,
2043 2 * MIN_STRIPE_BYTES,
2044 2 * MIN_STRIPE_BYTES + 2,
2045 4 * MIN_STRIPE_BYTES + 62,
2046 ] {
2047 let data = vec![42u8; min as usize * shard_len - u32::SIZE];
2048 explore(|strategy| encode::<Sha256, _>(total, min, data.as_slice(), strategy).unwrap());
2049 }
2050 }
2051
2052 #[test]
2053 fn test_parallel_recovery_decode() {
2054 let strategy = Rayon::new(NZUsize!(4)).unwrap();
2055 let data = vec![42u8; 256 * 1024];
2056 let total = 24u16;
2057 let min = 8u16;
2058
2059 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &strategy).unwrap();
2060
2061 let minimal = chunks
2062 .into_iter()
2063 .skip(min as usize)
2064 .take(min as usize)
2065 .map(|c| checked(root, c))
2066 .collect::<Vec<_>>();
2067 let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &strategy).unwrap();
2068 assert_eq!(decoded, data);
2069 }
2070
2071 #[test]
2079 fn test_striped_recovery_matches_sequential() {
2080 for &data_len in &[
2081 128 * 1024usize,
2082 257 * 1024,
2083 512 * 1024,
2084 1024 * 1024,
2085 4 * 1024 * 1024 + 14,
2086 ] {
2087 for &(total, min) in &[(12u16, 4u16), (24, 8), (33, 11), (20, 13)] {
2088 let data: Vec<u8> = (0..data_len)
2089 .map(|i| (i as u8) ^ ((i >> 7) as u8))
2090 .collect();
2091 let (root, chunks) =
2092 encode::<Sha256, _>(total, min, data.as_slice(), &Sequential).unwrap();
2093 let originals_needed = min.saturating_sub(total - min) as usize;
2094 let selected = chunks
2095 .into_iter()
2096 .enumerate()
2097 .filter(|&(index, _)| index < originals_needed || index >= min as usize)
2098 .map(|(_, chunk)| chunk)
2099 .take(min as usize)
2100 .map(|c| checked(root, c))
2101 .collect::<Vec<_>>();
2102 let decoded = explore(|strategy| {
2103 decode::<Sha256, _>(total, min, &root, selected.iter(), strategy).unwrap()
2104 });
2105 assert_eq!(
2106 decoded, data,
2107 "striped decode must return the original data (len={data_len} total={total} min={min})"
2108 );
2109 }
2110 }
2111 }
2112
2113 #[test]
2116 fn test_striped_all_originals_decode() {
2117 let data = vec![42u8; 256 * 1024];
2118 let total = 24u16;
2119 let min = 8u16;
2120
2121 let shard_len = canonical_shard_len(data.len(), min as usize);
2123 assert!(
2124 shard_len >= 2 * MIN_STRIPE_BYTES,
2125 "test must exercise >= 2 stripes (shard_len={shard_len})"
2126 );
2127
2128 let decoded = explore(|strategy| {
2131 let (root, chunks) =
2132 encode::<Sha256, _>(total, min, data.as_slice(), strategy).unwrap();
2133 let originals = chunks
2134 .into_iter()
2135 .take(usize::from(min))
2136 .map(|c| checked(root, c))
2137 .collect::<Vec<_>>();
2138 decode::<Sha256, _>(total, min, &root, originals.iter(), strategy).unwrap()
2139 });
2140 assert_eq!(decoded, data);
2141 }
2142
2143 #[test]
2147 fn test_striped_encode_into_matches_full_width() {
2148 let strategy = Explorer::parallel(&Rayon::new(NZUsize!(4)).unwrap());
2149 let mut tiled = false;
2150 for (k, m) in [(2usize, 2usize), (32, 64)] {
2151 for shard_len in [
2152 2 * MIN_STRIPE_BYTES,
2153 2 * MIN_STRIPE_BYTES + 2,
2154 3 * MIN_STRIPE_BYTES + 62,
2155 5 * MIN_STRIPE_BYTES + 64,
2156 17 * MIN_STRIPE_BYTES + 2,
2157 ] {
2158 let mut originals_buf = vec![0u8; k * shard_len];
2159 for (i, byte) in originals_buf.iter_mut().enumerate() {
2160 *byte = (i % 251) as u8;
2161 }
2162 let originals: Vec<&[u8]> = originals_buf.chunks(shard_len).collect();
2163
2164 let mut striped_recovery = vec![0u8; m * shard_len];
2166 let results = strategy
2167 .try_run_batches(
2168 shard_len / SHARD_CHUNK_BYTES,
2169 NZUsize!(MIN_STRIPE_BYTES / SHARD_CHUNK_BYTES),
2170 SHARD_CHUNK_BYTES * (k + m),
2171 |batches| {
2172 batches.try_map_collect_vec(
2173 |ranges| {
2174 let ranges = striped::byte_ranges(shard_len, ranges);
2175 tiled |= ranges.iter().any(|range| {
2176 striped::tiles(k, m, range.clone()).count() > 1
2177 });
2178 let groups = striped::stripe_columns(
2179 &mut striped_recovery,
2180 shard_len,
2181 &ranges,
2182 );
2183 ranges.into_iter().zip(groups)
2184 },
2185 |(range, out)| {
2186 striped::encode_recovery_into(k, m, range, &originals, out)
2187 },
2188 )
2189 },
2190 )
2191 .unwrap();
2192 assert!(results.len() >= 2, "must split into stripes");
2193
2194 let mut encoder = Encoder::new(k, m, shard_len).unwrap();
2196 for original in &originals {
2197 encoder.add_original_shard(original).unwrap();
2198 }
2199 let encoding = encoder.encode().unwrap();
2200 for (actual, expected) in striped_recovery
2201 .chunks_exact(shard_len)
2202 .zip(encoding.recovery_iter())
2203 {
2204 assert_eq!(actual, expected);
2205 }
2206 }
2207 }
2208 assert!(tiled, "no case splits a stripe into tiles");
2209 }
2210
2211 #[test]
2215 fn test_striped_tiles() {
2216 let page = 4096 / SHARD_CHUNK_BYTES;
2217 let start = 3 * SHARD_CHUNK_BYTES;
2218 for (k, m) in [(4usize, 8usize), (32, 64), (100, 100), (512, 512)] {
2219 let target =
2220 MAX_TILE_WORK_BYTES / (2 * (k + m).next_power_of_two() * SHARD_CHUNK_BYTES);
2221 for blocks in 1..3000 {
2222 for tail in [0, 2, SHARD_CHUNK_BYTES - 2] {
2223 let range = start..start + blocks * SHARD_CHUNK_BYTES + tail;
2224 let tiles: Vec<_> = striped::tiles(k, m, range.clone()).collect();
2225
2226 assert_eq!(tiles[0].start, range.start);
2228 assert_eq!(tiles.last().unwrap().end, range.end);
2229 for pair in tiles.windows(2) {
2230 assert_eq!(pair[0].end, pair[1].start);
2231 assert!((pair[0].end - start).is_multiple_of(SHARD_CHUNK_BYTES));
2232 }
2233
2234 let widths: Vec<_> = tiles
2237 .iter()
2238 .map(|tile| tile.len().div_ceil(SHARD_CHUNK_BYTES))
2239 .collect();
2240 let narrow = *widths.iter().min().unwrap();
2241 let wide = *widths.iter().max().unwrap();
2242 assert!(narrow >= 1 && wide - narrow <= 1);
2243 let padded = range.len().div_ceil(SHARD_CHUNK_BYTES);
2244 if padded < 2 * target {
2245 assert_eq!(tiles.len(), 1);
2246 } else {
2247 assert!(wide <= target);
2248 assert!(widths.iter().all(|width| !width.is_multiple_of(page)));
2249
2250 for fewer in 1..tiles.len() {
2252 let (width, extra) = (padded / fewer, padded % fewer);
2253 assert!(
2254 padded.div_ceil(fewer) > target
2255 || width.is_multiple_of(page)
2256 || (extra > 0 && (width + 1).is_multiple_of(page))
2257 );
2258 }
2259 }
2260 }
2261 }
2262 }
2263 }
2264
2265 fn tamper_flip_recovery(shards: &mut [Vec<u8>], k: usize, _shard_len: usize) {
2269 shards[k][0] ^= 0xFF;
2270 }
2271
2272 fn tamper_flip_original_data(shards: &mut [Vec<u8>], _k: usize, shard_len: usize) {
2273 let offset = u32::SIZE;
2275 shards[offset / shard_len][offset % shard_len] ^= 0xFF;
2276 }
2277
2278 fn tamper_corrupt_padding(shards: &mut [Vec<u8>], k: usize, shard_len: usize) {
2279 shards[k - 1][shard_len - 1] = 0xAA;
2282 }
2283
2284 fn decode_tampered_codeword<S: Strategy>(
2288 total: u16,
2289 min: u16,
2290 data: &[u8],
2291 selected: &[u16],
2292 tamper: fn(&mut [Vec<u8>], usize, usize),
2293 strategy: &S,
2294 ) -> Result<Vec<u8>, Error> {
2295 let (_root, chunks) = encode::<Sha256, _>(total, min, data, &Sequential).unwrap();
2296 let mut shards = chunks.iter().map(|c| c.shard.to_vec()).collect::<Vec<_>>();
2297 let shard_len = shards[0].len();
2298 tamper(&mut shards, min as usize, shard_len);
2299 let (root, chunks) = build_chunks(&shards);
2300 let pieces = selected
2301 .iter()
2302 .map(|&i| checked(root, chunks[i as usize].clone()))
2303 .collect::<Vec<_>>();
2304 decode::<Sha256, _>(total, min, &root, pieces.iter(), strategy)
2305 }
2306
2307 #[allow(clippy::type_complexity)]
2310 const ADVERSARIAL_SCENARIOS: &[(&str, &[u16], fn(&mut [Vec<u8>], usize, usize))] = &[
2311 (
2314 "tampered_recovery_unprovided",
2315 &[0, 1, 2, 3],
2316 tamper_flip_recovery,
2317 ),
2318 (
2321 "tampered_recovery_provided",
2322 &[0, 1, 2, 4],
2323 tamper_flip_recovery,
2324 ),
2325 (
2327 "non_canonical_padding",
2328 &[0, 1, 2, 3],
2329 tamper_corrupt_padding,
2330 ),
2331 (
2333 "tampered_original_data",
2334 &[0, 1, 2, 3],
2335 tamper_flip_original_data,
2336 ),
2337 (
2339 "recovery_only_tampered",
2340 &[4, 5, 6, 7],
2341 tamper_flip_recovery,
2342 ),
2343 ];
2344
2345 fn assert_adversarial_rejected<S: Strategy>(total: u16, min: u16, data: &[u8], strategy: &S) {
2348 for &(name, selected, tamper) in ADVERSARIAL_SCENARIOS {
2349 let result = decode_tampered_codeword(total, min, data, selected, tamper, strategy);
2350 assert!(
2351 matches!(result, Err(Error::Inconsistent)),
2352 "scenario {name} not rejected as Inconsistent: {result:?}"
2353 );
2354 }
2355 }
2356
2357 #[test]
2358 fn test_adversarial_rejection_sequential_small() {
2359 assert_adversarial_rejected(12, 4, &[0xCDu8; 30], &Sequential);
2362 }
2363
2364 #[test]
2368 fn test_adversarial_rejection_striped_matches_sequential() {
2369 let total = 12u16;
2370 let min = 4u16;
2371
2372 let data = vec![0xABu8; 64 * 1024];
2374 let shard_len = canonical_shard_len(data.len(), min as usize);
2375 assert!(
2376 shard_len >= 2 * MIN_STRIPE_BYTES,
2377 "test must exercise >= 2 stripes (shard_len={shard_len})"
2378 );
2379
2380 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &Sequential).unwrap();
2384 let mixed = [0u16, 1, 2, 4]
2385 .into_iter()
2386 .map(|i| checked(root, chunks[i as usize].clone()))
2387 .collect::<Vec<_>>();
2388 let decoded = explore(|strategy| {
2389 assert_adversarial_rejected(total, min, &data, strategy);
2390 decode::<Sha256, _>(total, min, &root, mixed.iter(), strategy).unwrap()
2391 });
2392 assert_eq!(decoded, data);
2393 }
2394
2395 #[rstest]
2404 #[case::high_rate(
2405 5,
2406 3,
2407 "e38bb9dbba4a102c4bd8447e212957742dab0af0c4148d4660c671f2f33d3df2"
2408 )]
2409 #[case::low_rate(
2410 4,
2411 6,
2412 "9f55e86ae11f9de757087e9052810b6359ed7c3a107ccde5a6b304fa54bd72fa"
2413 )]
2414 #[case::equal_powers(
2415 3,
2416 4,
2417 "aaaeebde791afabb30dc98356130ddd196e600c5a8c33882a8404539df795823"
2418 )]
2419 fn test_recovery_output_format_pinned(
2420 #[case] k: usize,
2421 #[case] m: usize,
2422 #[case] expected: &str,
2423 ) {
2424 let shard_len = 2 * SHARD_CHUNK_BYTES + 2;
2426
2427 let mut encoder = Encoder::new(k, m, shard_len).unwrap();
2428 for i in 0..k {
2429 let shard: Vec<u8> = (0..shard_len)
2430 .map(|j| ((i * 31 + j * 7) & 0xFF) as u8)
2431 .collect();
2432 encoder.add_original_shard(&shard).unwrap();
2433 }
2434 let encoding = encoder.encode().unwrap();
2435
2436 let recovery: Vec<&[u8]> = encoding.recovery_iter().collect();
2437 let digest = Sha256::hash(&recovery);
2438 assert_eq!(
2439 format!("{digest}"),
2440 expected,
2441 "vendored Reed-Solomon recovery output changed for ({k}, {m}); re-verify the \
2442 striping assumption before updating this fixture"
2443 );
2444 }
2445
2446 #[rstest]
2451 #[case::k2_m2_len0(
2452 2,
2453 2,
2454 0,
2455 "87d6a38f0987af00691b75fbfb710b7a50df08115c15254e09f109a1b74170c3"
2456 )]
2457 #[case::k2_m2_len1000(
2458 2,
2459 2,
2460 1000,
2461 "b90ea02e9b2eb25cc04f92c80b4f0dc8f68b154feff5a24495a8f090918b10d7"
2462 )]
2463 #[case::k2_m2_len70000(
2464 2,
2465 2,
2466 70_000,
2467 "b55ebdadfe51a23ab3a3ee8200a07dded9e10b8226abe34cbad171a11c070189"
2468 )]
2469 #[case::k3_m4_len0(
2470 3,
2471 4,
2472 0,
2473 "85a86686f0c01d863dde8f994dc3c9e063fa4458ff9458e218f5f260ed225c60"
2474 )]
2475 #[case::k3_m4_len1000(
2476 3,
2477 4,
2478 1000,
2479 "1fef0811649ed7f925bbc915ec3bf6d97509682e513f595881b2462e5ef1da8e"
2480 )]
2481 #[case::k3_m4_len70000(
2482 3,
2483 4,
2484 70_000,
2485 "a460ad1f30a8518a4e91de264deefbed57622c1ee6cf44c8e32573f437df8ba0"
2486 )]
2487 #[case::k4_m6_len0(
2488 4,
2489 6,
2490 0,
2491 "dd18f239803d542063c6dfebc9618c716406b299d1fd84d88476344b05e09d7b"
2492 )]
2493 #[case::k4_m6_len1000(
2494 4,
2495 6,
2496 1000,
2497 "e18fcc7b41a8b4ed1ca2420df986787f84452e1fb4d3d294827ebd325bcaaa7d"
2498 )]
2499 #[case::k4_m6_len70000(
2500 4,
2501 6,
2502 70_000,
2503 "18e9301ae82f44bf2ce2a16e44bd8901c33f357149a26b7981c6474b1fbd95dd"
2504 )]
2505 #[case::k5_m3_len0(
2506 5,
2507 3,
2508 0,
2509 "dd24cc230c717006fbdea807fe8d14a8e95e34d8b3efb40d8300711c3d1b03f5"
2510 )]
2511 #[case::k5_m3_len1000(
2512 5,
2513 3,
2514 1000,
2515 "90cdb8c92a13b9b2d2040f861df5ea24dbc9e27e8d0f10295aec80b93a8397b2"
2516 )]
2517 #[case::k5_m3_len70000(
2518 5,
2519 3,
2520 70_000,
2521 "a33bcd6552d8cc593363c994ed4113b59a6da32234846ee0bc1c570ec75f1e7b"
2522 )]
2523 #[case::k9_m16_len0(
2524 9,
2525 16,
2526 0,
2527 "84082dbcb47449967323f8b4893592d1361392387ad0371ee8faf5caad795bd8"
2528 )]
2529 #[case::k9_m16_len1000(
2530 9,
2531 16,
2532 1000,
2533 "b7a717d37d93331142139f14a14e61029bc0fac8a2dd3b1b78362d5091f5923f"
2534 )]
2535 #[case::k9_m16_len70000(
2536 9,
2537 16,
2538 70_000,
2539 "2f1b1764b12669a09362bfe0dd19e0a5ec1862f81ff67148f4413cfc7ca75cfc"
2540 )]
2541 fn test_commitment_pinned(
2542 #[case] k: u16,
2543 #[case] m: u16,
2544 #[case] len: usize,
2545 #[case] expected: &str,
2546 ) {
2547 let data: Vec<u8> = (0..len).map(|i| ((i * 31 + 7) % 251) as u8).collect();
2548 let config = Config {
2549 minimum_shards: NZU16!(k),
2550 extra_shards: NZU16!(m),
2551 };
2552 let (commitment, _) = RS::encode(&config, data.as_slice(), &STRATEGY).unwrap();
2553 assert_eq!(
2554 format!("{commitment}"),
2555 expected,
2556 "commitment changed for ({k}, {m}, {len} bytes); this is a consensus format change"
2557 );
2558 }
2559
2560 #[test]
2563 fn test_encode_rejects_unrepresentable_widths() {
2564 struct Claimed(usize);
2567
2568 impl bytes::Buf for Claimed {
2569 fn remaining(&self) -> usize {
2570 self.0
2571 }
2572
2573 fn chunk(&self) -> &[u8] {
2574 static ZEROS: [u8; 64] = [0; 64];
2575 &ZEROS[..self.0.min(ZEROS.len())]
2576 }
2577
2578 fn advance(&mut self, cnt: usize) {
2579 assert!(cnt <= self.0);
2580 self.0 -= cnt;
2581 }
2582 }
2583
2584 let max = Widen::widen(u32::MAX);
2585 let single = Config {
2586 minimum_shards: NZU16!(1),
2587 extra_shards: NZU16!(1),
2588 };
2589 let pair = Config {
2590 minimum_shards: NZU16!(2),
2591 extra_shards: NZU16!(1),
2592 };
2593
2594 for config in [single, pair] {
2596 assert!(matches!(
2597 RS::encode(&config, Claimed(max + 1), &STRATEGY),
2598 Err(Error::InvalidDataLength(len)) if len == max + 1
2599 ));
2600 }
2601
2602 for len in max - 4..=max {
2605 assert!(canonical_shard_len(len, 1) > max);
2606 assert!(matches!(
2607 RS::encode(&single, Claimed(len), &STRATEGY),
2608 Err(Error::InvalidDataLength(got)) if got == len
2609 ));
2610 }
2611 assert!(canonical_shard_len(max - 5, 1) <= max);
2612 assert!(canonical_shard_len(max, 2) <= max);
2613
2614 assert!(codeword_fits(3, 1 << 20));
2617 assert!(codeword_fits(1, MAX_ALLOCATION_BYTES));
2618 assert!(!codeword_fits(1, MAX_ALLOCATION_BYTES + 1));
2619 assert!(!codeword_fits(2, MAX_ALLOCATION_BYTES));
2620 assert!(!codeword_fits(2, usize::MAX));
2621 }
2622
2623 fn cached_decoder_width() -> usize {
2625 CACHED_DECODER.with(|cell| {
2626 let mut slot = cell.borrow_mut();
2627 let decoder = slot.1.as_mut().expect("decoder must be cached");
2628 match decoder.add_original_shard(0, [0u8; 0]) {
2629 Err(RsError::DifferentShardSize {
2630 shard_bytes,
2631 got: 0,
2632 }) => shard_bytes,
2633 other => panic!("unexpected decoder state: {other:?}"),
2634 }
2635 })
2636 }
2637
2638 fn cached_encoder_width() -> usize {
2640 CACHED_ENCODER.with(|cell| {
2641 let mut slot = cell.borrow_mut();
2642 let encoder = slot.1.as_mut().expect("encoder must be cached");
2643 match encoder.add_original_shard([0u8; 0]) {
2644 Err(RsError::DifferentShardSize {
2645 shard_bytes,
2646 got: 0,
2647 }) => shard_bytes,
2648 other => panic!("unexpected encoder state: {other:?}"),
2649 }
2650 })
2651 }
2652
2653 #[test]
2656 fn test_single_stripe_coding_caches_tile_width() {
2657 let total = 96u16;
2658 let min = 32u16;
2659 let (k, m) = (min as usize, (total - min) as usize);
2660
2661 let shard_len = 17 * MIN_STRIPE_BYTES + 2;
2663 let tile = striped::tiles(k, m, 0..shard_len)
2664 .last()
2665 .expect("a nonempty shard has a tile")
2666 .len();
2667 assert!(striped::tiles(k, m, 0..shard_len).count() > 1);
2668 assert!(tile < shard_len);
2669 let data: Vec<u8> = (0..k * shard_len - u32::SIZE)
2670 .map(|i| (i % 251) as u8)
2671 .collect();
2672
2673 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &Sequential).unwrap();
2675 assert_eq!(chunks[0].shard.len(), shard_len);
2676 assert_eq!(cached_encoder_width(), tile);
2677
2678 let recoveries = chunks[k..2 * k]
2680 .iter()
2681 .map(|chunk| checked(root, chunk.clone()))
2682 .collect::<Vec<_>>();
2683 let decoded =
2684 decode::<Sha256, _>(total, min, &root, recoveries.iter(), &Sequential).unwrap();
2685 assert_eq!(decoded, data);
2686 assert_eq!(cached_decoder_width(), tile);
2687
2688 let originals = chunks[..k]
2690 .iter()
2691 .map(|chunk| checked(root, chunk.clone()))
2692 .collect::<Vec<_>>();
2693 let decoded =
2694 decode::<Sha256, _>(total, min, &root, originals.iter(), &Sequential).unwrap();
2695 assert_eq!(decoded, data);
2696 assert_eq!(cached_encoder_width(), tile);
2697
2698 let selected = (min..2 * min).collect::<Vec<_>>();
2701 let result = decode_tampered_codeword(
2702 total,
2703 min,
2704 &data,
2705 &selected,
2706 tamper_flip_recovery,
2707 &Sequential,
2708 );
2709 assert!(matches!(result, Err(Error::Inconsistent)), "{result:?}");
2710 assert_eq!(cached_decoder_width(), tile);
2711 }
2712
2713 #[test]
2716 fn test_read_cfg_bounds_chunk_width() {
2717 type TestChunk = Chunk<<Sha256 as Hasher>::Digest>;
2718 for (min, extra) in [(1u16, 1u16), (2, 2), (4, 6), (34, 66)] {
2719 let config = Config {
2720 minimum_shards: NZU16!(min),
2721 extra_shards: NZU16!(extra),
2722 };
2723 for data_len in [0, 1, 2 * usize::from(min), 1000, 4099] {
2724 let cfg = (config, data_len);
2726 let (_, chunks) =
2727 RS::encode(&config, vec![0; data_len].as_slice(), &STRATEGY).unwrap();
2728 assert!(TestChunk::decode_cfg(chunks[0].encode(), &cfg).is_ok());
2729
2730 let width = chunks[0].shard.len();
2732 let wider = (data_len..)
2733 .find(|&len| canonical_shard_len(len, usize::from(min)) > width)
2734 .unwrap();
2735 let (_, chunks) =
2736 RS::encode(&config, vec![0; wider].as_slice(), &STRATEGY).unwrap();
2737 assert!(TestChunk::decode_cfg(chunks[0].encode(), &cfg).is_err());
2738 }
2739
2740 let (_, chunks) = RS::encode(&config, [0u8; 8].as_slice(), &STRATEGY).unwrap();
2742 assert!(TestChunk::decode_cfg(chunks[0].encode(), &(config, usize::MAX)).is_ok());
2743 }
2744 }
2745
2746 #[test]
2748 fn test_check_rejects_invalid_shard_width() {
2749 let config = Config {
2750 minimum_shards: NZU16!(4),
2751 extra_shards: NZU16!(8),
2752 };
2753 for width in [0, 3, 2 * MIN_STRIPE_BYTES + 1] {
2754 let (root, chunks) = build_chunks(&vec![vec![0u8; width]; 12]);
2755 let invalid = |result: &Result<_, Error>| {
2756 matches!(
2757 result,
2758 Err(Error::ReedSolomon(RsError::InvalidShardSize { shard_bytes }))
2759 if *shard_bytes == width
2760 )
2761 };
2762
2763 for chunk in &chunks {
2765 assert!(invalid(&RS::check(
2766 &config,
2767 &root,
2768 chunk.index,
2769 chunk,
2770 &STRATEGY
2771 )));
2772 }
2773
2774 let batch = chunks
2776 .iter()
2777 .map(|chunk| (chunk.index, chunk))
2778 .collect::<Vec<_>>();
2779 assert!(
2780 RS::check_many(&config, &root, &batch, &STRATEGY)
2781 .iter()
2782 .all(invalid)
2783 );
2784 }
2785 }
2786
2787 #[test]
2788 fn test_parallel_decode_rejects_mismatched_shard_lengths() {
2789 let strategy = Rayon::new(NZUsize!(4)).unwrap();
2790 let data = vec![42u8; 256 * 1024];
2791 let total = 24u16;
2792 let min = 8u16;
2793
2794 let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &strategy).unwrap();
2795 let mut shards = chunks
2796 .iter()
2797 .map(|chunk| chunk.shard.to_vec())
2798 .collect::<Vec<_>>();
2799 shards[min as usize].pop();
2800
2801 let mut builder = Builder::<Sha256>::new(total as usize);
2802 for shard in &shards {
2803 builder.add(&Sha256::hash(&[shard]));
2804 }
2805 let tree = builder.build();
2806 let root = tree.root();
2807
2808 let pieces = [9u16, 8, 10, 11, 12, 13, 14, 15]
2809 .into_iter()
2810 .map(|i| {
2811 let proof = tree.proof(i as u32).unwrap();
2812 checked(
2813 root,
2814 Chunk::new(shards[i as usize].clone().into(), i, proof),
2815 )
2816 })
2817 .collect::<Vec<_>>();
2818
2819 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &strategy);
2820 assert!(matches!(result, Err(Error::Inconsistent)));
2821 }
2822
2823 #[test]
2824 fn test_malicious_root_detection() {
2825 let data = b"Original data that should be protected";
2826 let total = 7u16;
2827 let min = 4u16;
2828 let config = Config {
2829 minimum_shards: NZU16!(min),
2830 extra_shards: NZU16!(total - min),
2831 };
2832
2833 let (_correct_root, chunks) =
2835 encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2836
2837 let malicious_root = Sha256::hash(&[b"malicious_data_that_wasnt_actually_encoded"]);
2839
2840 for i in 0..total {
2842 assert!(
2843 RS::check(&config, &malicious_root, i, &chunks[i as usize], &STRATEGY).is_err()
2844 );
2845 }
2846
2847 let minimal = chunks
2850 .into_iter()
2851 .take(min as usize)
2852 .map(|c| checked(_correct_root, c))
2853 .collect::<Vec<_>>();
2854
2855 let result = decode::<Sha256, _>(total, min, &malicious_root, minimal.iter(), &STRATEGY);
2858 assert!(matches!(result, Err(Error::CommitmentMismatch)));
2859 }
2860
2861 #[test]
2862 fn test_mismatched_config_rejected_during_check() {
2863 let config_expected = Config {
2864 minimum_shards: NZU16!(2),
2865 extra_shards: NZU16!(2),
2866 };
2867 let config_actual = Config {
2868 minimum_shards: NZU16!(3),
2869 extra_shards: NZU16!(3),
2870 };
2871
2872 let data = b"leaf_count mismatch proof";
2873 let (commitment, shards) = RS::encode(&config_actual, data.as_slice(), &STRATEGY).unwrap();
2874
2875 let check_result = RS::check(&config_expected, &commitment, 0, &shards[0], &STRATEGY);
2878 assert!(matches!(check_result, Err(Error::InvalidProof)));
2879 }
2880
2881 #[test]
2882 fn test_manipulated_chunk_detection() {
2883 let data = b"Data integrity must be maintained";
2884 let total = 6u16;
2885 let min = 3u16;
2886
2887 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2889 let mut pieces: Vec<_> = chunks.into_iter().map(|c| checked(root, c)).collect();
2890
2891 if !pieces[1].shard.is_empty() {
2893 let mut shard = pieces[1].shard.to_vec();
2894 shard[0] ^= 0xFF; pieces[1].shard = shard.into();
2896 }
2897
2898 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
2900 assert!(matches!(result, Err(Error::Inconsistent)));
2901 }
2902
2903 #[test]
2904 fn test_inconsistent_shards() {
2905 let data = b"Test data for malicious encoding";
2906 let total = 5u16;
2907 let min = 3u16;
2908 let m = total - min;
2909
2910 let shard_size = canonical_shard_len(data.len(), min as usize);
2912 let padded = prepare_data(data.as_slice(), min as usize, shard_size);
2913
2914 let mut encoder = Encoder::new(min as usize, m as usize, shard_size).unwrap();
2916 for shard in padded.chunks(shard_size) {
2917 encoder.add_original_shard(shard).unwrap();
2918 }
2919 let recovery_result = encoder.encode().unwrap();
2920 let mut recovery_shards: Vec<Vec<u8>> = recovery_result
2921 .recovery_iter()
2922 .map(|s| s.to_vec())
2923 .collect();
2924
2925 if !recovery_shards[0].is_empty() {
2927 recovery_shards[0][0] ^= 0xFF;
2928 }
2929
2930 let mut malicious_shards: Vec<Vec<u8>> =
2932 padded.chunks(shard_size).map(|s| s.to_vec()).collect();
2933 malicious_shards.extend(recovery_shards);
2934
2935 let mut builder = Builder::<Sha256>::new(total as usize);
2937 for shard in &malicious_shards {
2938 builder.add(&Sha256::hash(&[shard]));
2939 }
2940 let malicious_tree = builder.build();
2941 let malicious_root = malicious_tree.root();
2942
2943 let selected_indices = vec![0, 1, 3]; let mut pieces = Vec::new();
2946 for &i in &selected_indices {
2947 let merkle_proof = malicious_tree.proof(i as u32).unwrap();
2948 let shard = malicious_shards[i].clone();
2949 let chunk = Chunk::new(shard.into(), i as u16, merkle_proof);
2950 pieces.push(chunk);
2951 }
2952 let pieces: Vec<_> = pieces
2953 .into_iter()
2954 .map(|c| checked(malicious_root, c))
2955 .collect();
2956
2957 let result = decode::<Sha256, _>(total, min, &malicious_root, pieces.iter(), &STRATEGY);
2959 assert!(matches!(result, Err(Error::Inconsistent)));
2960 }
2961
2962 #[test]
2966 fn test_non_canonical_padding_rejected() {
2967 let data = b"X";
2968 let total = 6u16;
2969 let min = 3u16;
2970 let k = min as usize;
2971 let m = total as usize - k;
2972
2973 let shard_len = canonical_shard_len(data.len(), k);
2974 let mut padded = prepare_data(data.as_slice(), k, shard_len);
2975 let payload_end = u32::SIZE + data.len();
2976 let total_original_len = k * shard_len;
2977 assert!(payload_end < total_original_len, "test requires padding");
2978
2979 let pad_shard = payload_end / shard_len;
2981 let pad_offset = payload_end % shard_len;
2982 padded[pad_shard * shard_len + pad_offset] = 0xAA;
2983
2984 let mut encoder = Encoder::new(k, m, shard_len).unwrap();
2985 for shard in padded.chunks(shard_len) {
2986 encoder.add_original_shard(shard).unwrap();
2987 }
2988 let recovery = encoder.encode().unwrap();
2989 let mut shards: Vec<Vec<u8>> = padded.chunks(shard_len).map(|s| s.to_vec()).collect();
2990 shards.extend(recovery.recovery_iter().map(|s| s.to_vec()));
2991
2992 let mut builder = Builder::<Sha256>::new(total as usize);
2993 for shard in &shards {
2994 builder.add(&Sha256::hash(&[shard]));
2995 }
2996 let tree = builder.build();
2997 let non_canonical_root = tree.root();
2998
2999 let mut pieces = Vec::with_capacity(k);
3000 for (i, shard) in shards.iter().take(k).enumerate() {
3001 let proof = tree.proof(i as u32).unwrap();
3002 pieces.push(checked(
3003 non_canonical_root,
3004 Chunk::new(shard.clone().into(), i as u16, proof),
3005 ));
3006 }
3007
3008 let result = decode::<Sha256, _>(total, min, &non_canonical_root, pieces.iter(), &STRATEGY);
3009 assert!(matches!(result, Err(Error::Inconsistent)));
3010 }
3011
3012 #[test]
3013 fn minifuzz_decode_unique_commitment() {
3014 minifuzz::Builder::default()
3015 .with_search_limit(2048)
3016 .test(|u| {
3017 fuzz_arbitrary_codeword(u)?;
3018 fuzz_mixed_codeword(u)?;
3019 Ok(())
3020 });
3021 }
3022
3023 #[test]
3024 fn test_oversized_zero_padded_shards_rejected() {
3025 let data = b"X";
3026 let total = 6u16;
3027 let min = 3u16;
3028 let k = min as usize;
3029 let m = total as usize - k;
3030
3031 let oversized_shard_len = 4usize;
3032 let mut padded = vec![0u8; k * oversized_shard_len];
3033 padded[..u32::SIZE].copy_from_slice(&(data.len() as u32).to_be_bytes());
3034 padded[u32::SIZE..u32::SIZE + data.len()].copy_from_slice(data);
3035
3036 let mut encoder = Encoder::new(k, m, oversized_shard_len).unwrap();
3037 for shard in padded.chunks(oversized_shard_len) {
3038 encoder.add_original_shard(shard).unwrap();
3039 }
3040 let recovery = encoder.encode().unwrap();
3041
3042 let mut oversized_shards: Vec<Vec<u8>> = padded
3043 .chunks(oversized_shard_len)
3044 .map(|shard| shard.to_vec())
3045 .collect();
3046 oversized_shards.extend(recovery.recovery_iter().map(|shard| shard.to_vec()));
3047
3048 let mut builder = Builder::<Sha256>::new(total as usize);
3049 for shard in &oversized_shards {
3050 builder.add(&Sha256::hash(&[shard]));
3051 }
3052 let oversized_tree = builder.build();
3053 let oversized_root = oversized_tree.root();
3054
3055 let (canonical_root, _) =
3056 encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
3057 assert_ne!(oversized_root, canonical_root);
3058
3059 let pieces = [0u16, 1u16, 4u16]
3060 .into_iter()
3061 .map(|i| {
3062 let proof = oversized_tree.proof(i as u32).unwrap();
3063 checked(
3064 oversized_root,
3065 Chunk::new(oversized_shards[i as usize].clone().into(), i, proof),
3066 )
3067 })
3068 .collect::<Vec<_>>();
3069
3070 let result = decode::<Sha256, _>(total, min, &oversized_root, pieces.iter(), &STRATEGY);
3071 assert!(matches!(result, Err(Error::Inconsistent)));
3072 }
3073
3074 #[test]
3075 fn test_extra_non_canonical_recovery_rejected() {
3076 let data = b"canonical originals with bad recovery";
3077 let total = 6u16;
3078 let min = 3u16;
3079
3080 let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
3081 let mut shards = chunks
3082 .iter()
3083 .map(|chunk| chunk.shard.to_vec())
3084 .collect::<Vec<_>>();
3085 shards[min as usize][0] ^= 0xFF;
3086
3087 let mut builder = Builder::<Sha256>::new(total as usize);
3088 for shard in &shards {
3089 builder.add(&Sha256::hash(&[shard]));
3090 }
3091 let tree = builder.build();
3092 let root = tree.root();
3093
3094 let pieces = (0u16..=3u16)
3095 .map(|i| {
3096 let proof = tree.proof(i as u32).unwrap();
3097 checked(
3098 root,
3099 Chunk::new(shards[i as usize].clone().into(), i, proof),
3100 )
3101 })
3102 .collect::<Vec<_>>();
3103
3104 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
3105 assert!(matches!(result, Err(Error::Inconsistent)));
3106 }
3107
3108 #[test]
3109 fn test_reconstructed_original_with_extra_non_canonical_recovery_rejected() {
3110 let data = b"canonical reconstructed originals with bad extra recovery";
3111 let total = 6u16;
3112 let min = 3u16;
3113
3114 let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
3115 let mut shards = chunks
3116 .iter()
3117 .map(|chunk| chunk.shard.to_vec())
3118 .collect::<Vec<_>>();
3119 shards[4][0] ^= 0xFF;
3120
3121 let mut builder = Builder::<Sha256>::new(total as usize);
3122 for shard in &shards {
3123 builder.add(&Sha256::hash(&[shard]));
3124 }
3125 let tree = builder.build();
3126 let root = tree.root();
3127
3128 let pieces = [0u16, 1u16, 3u16, 4u16]
3129 .into_iter()
3130 .map(|i| {
3131 let proof = tree.proof(i as u32).unwrap();
3132 checked(
3133 root,
3134 Chunk::new(shards[i as usize].clone().into(), i, proof),
3135 )
3136 })
3137 .collect::<Vec<_>>();
3138
3139 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
3140 assert!(matches!(result, Err(Error::Inconsistent)));
3141 }
3142
3143 #[test]
3144 fn test_striped_surplus_recovery_tampered_rejected() {
3145 let data = vec![0xABu8; 64 * 1024];
3150 let total = 12u16;
3151 let min = 4u16;
3152 let shard_len = canonical_shard_len(data.len(), min as usize);
3153 assert!(
3154 shard_len >= 2 * MIN_STRIPE_BYTES,
3155 "test must exercise >= 2 stripes (shard_len={shard_len})"
3156 );
3157
3158 let selected = [0u16, 1, 4, 5, 6];
3161 let tamper: fn(&mut [Vec<u8>], usize, usize) = |shards, _, _| shards[6][0] ^= 0xFF;
3162 let rejected = explore(|strategy| {
3163 matches!(
3164 decode_tampered_codeword(total, min, &data, &selected, tamper, strategy),
3165 Err(Error::Inconsistent)
3166 )
3167 });
3168 assert!(rejected);
3169 }
3170
3171 #[test]
3172 fn test_decode_invalid_index() {
3173 let data = b"Testing recovery pieces";
3174 let total = 8u16;
3175 let min = 3u16;
3176
3177 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
3179
3180 let mut invalid = checked(root, chunks[1].clone());
3182 invalid.index = 8;
3183 let pieces: Vec<_> = vec![
3184 checked(root, chunks[0].clone()), invalid, checked(root, chunks[6].clone()), ];
3188
3189 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
3191 assert!(matches!(result, Err(Error::InvalidIndex(8))));
3192 }
3193
3194 #[test]
3195 fn test_max_chunks() {
3196 let data = vec![42u8; 1000]; let total = u16::MAX;
3198 let min = u16::MAX / 2;
3199
3200 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
3202
3203 let minimal = chunks
3205 .into_iter()
3206 .take(min as usize)
3207 .map(|c| checked(root, c))
3208 .collect::<Vec<_>>();
3209 let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
3210 assert_eq!(decoded, data);
3211 }
3212
3213 #[test]
3214 fn test_too_many_chunks() {
3215 let data = vec![42u8; 1000]; let total = u16::MAX;
3217 let min = u16::MAX / 2 - 1;
3218
3219 let result = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY);
3221 assert!(matches!(
3222 result,
3223 Err(Error::ReedSolomon(RsError::UnsupportedShardCount {
3224 original_count: _,
3225 recovery_count: _,
3226 }))
3227 ));
3228 }
3229
3230 #[test]
3232 fn test_decode_rejects_unsupported_shard_counts() {
3233 let data = vec![42u8; 1000];
3234 let (root, chunks) = encode::<Sha256, _>(10, 4, data.as_slice(), &STRATEGY).unwrap();
3235 let checked = chunks
3236 .into_iter()
3237 .map(|c| checked(root, c))
3238 .collect::<Vec<_>>();
3239 let result =
3240 decode::<Sha256, _>(u16::MAX, u16::MAX / 2 - 1, &root, checked.iter(), &STRATEGY);
3241 assert!(matches!(
3242 result,
3243 Err(Error::ReedSolomon(RsError::UnsupportedShardCount { .. }))
3244 ));
3245 }
3246
3247 #[test]
3248 fn test_too_many_total_shards() {
3249 assert!(
3250 RS::encode(
3251 &Config {
3252 minimum_shards: NZU16!(u16::MAX / 2 + 1),
3253 extra_shards: NZU16!(u16::MAX),
3254 },
3255 [].as_slice(),
3256 &STRATEGY,
3257 )
3258 .is_err()
3259 )
3260 }
3261
3262 #[test]
3263 fn test_max_shard_size() {
3264 let config = |min, extra| Config {
3265 minimum_shards: NZU16!(min),
3266 extra_shards: NZU16!(extra),
3267 };
3268
3269 for (min, extra) in [(1, 1), (2, 1), (3, 4), (171, 342), (342, 682)] {
3273 let config = config(min, extra);
3274 let mut previous = 0;
3275 for len in [0, 1, 2, 3, 333, 612, 4096, 1 << 20] {
3276 let data = vec![0x67; len];
3277 let (_, chunks) = RS::encode(&config, data.as_slice(), &STRATEGY).unwrap();
3278 let sizes: Vec<_> = chunks.iter().map(|chunk| chunk.encode().len()).collect();
3279 let bound = RS::max_shard_size(&config, len);
3280 assert_eq!(sizes.iter().max(), Some(&sizes[0]));
3281 assert!(sizes[0] <= bound);
3282 assert_eq!(
3283 bound,
3284 chunks[0].shard.len()
3285 + u16::SIZE
3286 + u32::SIZE
3287 + chunks[0].proof.siblings.len() * Sha256Digest::SIZE
3288 + 2 * UInt::<u32>::MAX_ENCODE_SIZE
3289 );
3290 assert!(bound >= previous);
3291 previous = bound;
3292 }
3293 }
3294
3295 let (_, chunks) = RS::encode(&config(16385, 32768), [].as_slice(), &STRATEGY).unwrap();
3298 let bound = RS::max_shard_size(&config(16385, 32768), 0);
3299 assert!(chunks[0].encode().len() <= bound);
3300 assert_eq!(
3301 bound,
3302 2 + u16::SIZE + u32::SIZE + 16 * Sha256Digest::SIZE + 2 * UInt::<u32>::MAX_ENCODE_SIZE
3303 );
3304 assert!(RS::encode(&config(16385, 32769), [].as_slice(), &STRATEGY).is_err());
3305
3306 let max: usize = Widen::widen(u32::MAX);
3308 RS::max_shard_size(&config(2, 1), max);
3309 }
3310
3311 #[test]
3312 #[should_panic(expected = "backend must support the shard counts")]
3313 fn test_max_shard_size_unsupported_counts() {
3314 let config = Config {
3316 minimum_shards: NZU16!(16385),
3317 extra_shards: NZU16!(32769),
3318 };
3319 RS::max_shard_size(&config, 0);
3320 }
3321
3322 #[test]
3323 #[should_panic(expected = "total shards must fit in u16")]
3324 fn test_max_shard_size_too_many_shards() {
3325 let config = Config {
3327 minimum_shards: NZU16!(u16::MAX),
3328 extra_shards: NZU16!(1),
3329 };
3330 RS::max_shard_size(&config, 0);
3331 }
3332
3333 #[test]
3334 #[should_panic(expected = "data must fit the u32 length prefix")]
3335 fn test_max_shard_size_data_beyond_prefix() {
3336 let config = Config {
3338 minimum_shards: NZU16!(2),
3339 extra_shards: NZU16!(1),
3340 };
3341 let max: usize = Widen::widen(u32::MAX);
3342 RS::max_shard_size(&config, max + 1);
3343 }
3344
3345 #[test]
3346 #[should_panic(expected = "len must fit the u32 length prefix")]
3347 fn test_max_shard_size_shard_beyond_prefix() {
3348 let config = Config {
3350 minimum_shards: NZU16!(1),
3351 extra_shards: NZU16!(1),
3352 };
3353 let max: usize = Widen::widen(u32::MAX);
3354 RS::max_shard_size(&config, max);
3355 }
3356
3357 #[test]
3358 fn test_chunk_encode_iobufs_matches_encode() {
3359 let data = b"iobufs encoding test";
3360 let (_root, chunks) = encode::<Sha256, _>(5, 3, data.as_slice(), &STRATEGY).unwrap();
3361 let chunk = &chunks[0];
3362
3363 let encoded = chunk.encode();
3364 let mut encoded_iobufs = chunk.encode_iobufs();
3365 let mut encoded_iobufs_bytes = vec![0u8; encoded_iobufs.remaining()];
3366 encoded_iobufs.copy_to_slice(&mut encoded_iobufs_bytes);
3367 assert_eq!(encoded_iobufs_bytes, encoded.as_ref());
3368 }
3369
3370 #[cfg(feature = "arbitrary")]
3371 mod conformance {
3372 use super::*;
3373 use commonware_codec::conformance::CodecConformance;
3374 use commonware_cryptography::sha256::Digest as Sha256Digest;
3375
3376 commonware_conformance::conformance_tests! {
3377 CodecConformance<Chunk<Sha256Digest>>,
3378 }
3379 }
3380
3381 fn owned_output_codeword(padded: &[u8], k: usize, m: usize) -> Vec<Vec<u8>> {
3382 assert_eq!(padded.len() % k, 0);
3383 let width = padded.len() / k;
3384 let mut encoder = Encoder::new(k, m, width).unwrap();
3385 for shard in padded.chunks_exact(width) {
3386 encoder.add_original_shard(shard).unwrap();
3387 }
3388 let encoded = encoder.encode().unwrap();
3389 let mut shards: Vec<_> = padded.chunks_exact(width).map(<[u8]>::to_vec).collect();
3390 shards.extend(encoded.recovery_iter().map(<[u8]>::to_vec));
3391 shards
3392 }
3393
3394 fn owned_output_reconstructed(shards: &[Vec<u8>], k: usize) -> Vec<Vec<u8>> {
3395 let mut decoder = Decoder::new(k, shards.len() - k, shards[0].len()).unwrap();
3396 for (index, shard) in shards[k..2 * k].iter().enumerate() {
3397 decoder.add_recovery_shard(index, shard).unwrap();
3398 }
3399 let decoded = decoder.decode_with_recovery().unwrap().unwrap();
3400 let mut reconstructed = shards.to_vec();
3401 for (index, shard) in decoded.original_iter() {
3402 reconstructed[index] = shard.to_vec();
3403 }
3404 for (index, shard) in decoded.recovery_iter() {
3405 reconstructed[k + index] = shard.to_vec();
3406 }
3407 reconstructed
3408 }
3409
3410 #[test]
3411 fn test_owned_output_authenticated_rejections() {
3412 let k = 4usize;
3413 let m = 8usize;
3414 let total = u16::try_from(k + m).expect("k + m must fit in u16");
3415 let min = u16::try_from(k).expect("k must fit in u16");
3416 let width = 2 * MIN_STRIPE_BYTES + 2;
3417 let data = vec![0x5A; k * width - u32::SIZE - 1];
3418 let canonical_width = canonical_shard_len(data.len(), k);
3419 let base = prepare_data(data.as_slice(), k, canonical_width);
3420 assert_eq!(canonical_width, width);
3421 assert_eq!(base.last(), Some(&0));
3422 for scenario in ["length_too_large", "length_max", "padding", "width", "root"] {
3423 let mut padded = base.clone();
3424 match scenario {
3425 "length_too_large" => {
3426 let length = (padded.len() - u32::SIZE + 1) as u32;
3427 padded[..u32::SIZE].copy_from_slice(&length.to_be_bytes());
3428 }
3429 "length_max" => padded[..u32::SIZE].copy_from_slice(&u32::MAX.to_be_bytes()),
3430 "padding" => *padded.last_mut().unwrap() = 1,
3431 "width" => padded.resize(padded.len() + 2 * k, 0),
3432 "root" => {}
3433 _ => unreachable!(),
3434 }
3435 let mut shards = owned_output_codeword(&padded, k, m);
3436 if scenario == "root" {
3437 shards[0][u32::SIZE] ^= 1;
3440 }
3441 assert!(shards[0].len() >= 2 * MIN_STRIPE_BYTES);
3442 let (root, chunks) = build_chunks(&shards);
3443 let pieces: Vec<_> = chunks[k..2 * k]
3444 .iter()
3445 .map(|chunk| {
3446 check_chunk::<Sha256>(total, &root, chunk.index, chunk, || {
3447 Sha256::hash(&[chunk.shard.as_ref()])
3448 })
3449 .unwrap()
3450 })
3451 .collect();
3452
3453 let reconstructed = owned_output_reconstructed(&shards, k);
3456 assert_eq!(reconstructed[..k].concat(), padded, "{scenario}");
3457 let originals: Vec<_> = reconstructed[..k].iter().map(Vec::as_slice).collect();
3458 if scenario == "root" {
3459 assert_eq!(extract_data(&originals, k, width).unwrap(), data);
3460 assert_ne!(build_chunks(&reconstructed).0, root);
3461 } else {
3462 assert_eq!(build_chunks(&reconstructed).0, root, "{scenario}");
3463 match scenario {
3464 "length_too_large" | "length_max" => {
3465 assert!(matches!(
3466 read_data_len(&originals),
3467 Err(Error::Inconsistent)
3468 ));
3469 }
3470 "padding" => {
3471 let len = read_data_len(&originals).unwrap();
3472 assert_eq!(canonical_shard_len(len, k), shards[0].len());
3473 assert_eq!(len, data.len());
3474 assert_eq!(&padded[u32::SIZE..u32::SIZE + len], data);
3475 assert_eq!(&padded[u32::SIZE + len..], &[1]);
3476 }
3477 "width" => {
3478 let len = read_data_len(&originals).unwrap();
3479 assert_eq!(len, data.len());
3480 assert!(padded[u32::SIZE + len..].iter().all(|byte| *byte == 0));
3481 assert_ne!(canonical_shard_len(len, k), shards[0].len());
3482 }
3483 _ => unreachable!(),
3484 }
3485 }
3486 let rejected = explore(|strategy| {
3487 matches!(
3488 decode::<Sha256, _>(total, min, &root, pieces.iter(), strategy),
3489 Err(Error::Inconsistent)
3490 )
3491 });
3492 assert!(rejected, "{scenario}");
3493 }
3494 }
3495
3496 #[test]
3497 fn test_owned_output_recovery_and_mixed_roundtrip() {
3498 let total = 12u16;
3499 let min = 4u16;
3500 for width in [
3501 2 * MIN_STRIPE_BYTES,
3502 2 * MIN_STRIPE_BYTES + 2,
3503 3 * MIN_STRIPE_BYTES + 62,
3504 ] {
3505 let data: Vec<_> = (0..min as usize * width - u32::SIZE - 1)
3506 .map(|index| (index ^ (index >> 8)) as u8)
3507 .collect();
3508 let (root, chunks) =
3509 encode::<Sha256, _>(total, min, data.as_slice(), &Sequential).unwrap();
3510 assert_eq!(chunks[0].shard.len(), width);
3511 for selected in [[4usize, 5, 6, 7], [0, 4, 5, 6]] {
3512 let pieces: Vec<_> = selected
3513 .into_iter()
3514 .map(|index| {
3515 let chunk = &chunks[index];
3516 check_chunk::<Sha256>(total, &root, chunk.index, chunk, || {
3517 Sha256::hash(&[chunk.shard.as_ref()])
3518 })
3519 .unwrap()
3520 })
3521 .collect();
3522 assert_eq!(
3523 explore(|strategy| {
3524 decode::<Sha256, _>(total, min, &root, pieces.iter(), strategy).unwrap()
3525 }),
3526 data
3527 );
3528 }
3529 }
3530 }
3531
3532 #[test]
3533 fn test_read_data_len_prefix_bounds() {
3534 let prefix = [0u8; u32::SIZE];
3535 for len in 0..u32::SIZE {
3536 assert!(matches!(
3537 read_data_len(&[&prefix[..len]]),
3538 Err(Error::Inconsistent)
3539 ));
3540 }
3541 assert_eq!(read_data_len(&[&prefix[..1], &prefix[1..]]).unwrap(), 0);
3542 }
3543}