1use crate::{Config, Scheme};
2use bytes::{Buf, BufMut, Bytes};
3use commonware_codec::{BufsMut, EncodeSize, FixedSize, RangeCfg, Read, ReadExt, Write};
4use commonware_cryptography::{Digest, Hasher};
5use commonware_parallel::Strategy;
6use commonware_storage::bmt::{self, Builder};
7use commonware_utils::Cached;
8use reed_solomon_simd::{Error as RsError, ReedSolomonDecoder, ReedSolomonEncoder};
9use std::marker::PhantomData;
10use thiserror::Error;
11
12commonware_utils::thread_local_cache!(static CACHED_ENCODER: ReedSolomonEncoder);
17commonware_utils::thread_local_cache!(static CACHED_DECODER: ReedSolomonDecoder);
18
19#[derive(Error, Debug)]
21pub enum Error {
22 #[error("reed-solomon error: {0}")]
23 ReedSolomon(#[from] RsError),
24 #[error("inconsistent")]
25 Inconsistent,
26 #[error("invalid proof")]
27 InvalidProof,
28 #[error("not enough chunks")]
29 NotEnoughChunks,
30 #[error("duplicate chunk index: {0}")]
31 DuplicateIndex(u16),
32 #[error("invalid data length: {0}")]
33 InvalidDataLength(usize),
34 #[error("invalid index: {0}")]
35 InvalidIndex(u16),
36 #[error("too many total shards: {0}")]
37 TooManyTotalShards(u32),
38 #[error("checked shard commitment does not match decode commitment")]
39 CommitmentMismatch,
40}
41
42fn total_shards(config: &Config) -> Result<u16, Error> {
43 let total = config.total_shards();
44 total
45 .try_into()
46 .map_err(|_| Error::TooManyTotalShards(total))
47}
48
49#[derive(Debug, Clone)]
51pub struct Chunk<D: Digest> {
52 shard: Bytes,
54
55 index: u16,
57
58 proof: bmt::Proof<D>,
60}
61
62impl<D: Digest> Chunk<D> {
63 const fn new(shard: Bytes, index: u16, proof: bmt::Proof<D>) -> Self {
65 Self {
66 shard,
67 index,
68 proof,
69 }
70 }
71
72 fn verify<H: Hasher<Digest = D>>(&self, index: u16, root: &D) -> Option<CheckedChunk<D>> {
74 if index != self.index {
76 return None;
77 }
78
79 let mut hasher = H::new();
81 hasher.update(&self.shard);
82 let shard_digest = hasher.finalize();
83
84 self.proof
86 .verify_element_inclusion(&mut hasher, &shard_digest, self.index as u32, root)
87 .ok()?;
88
89 Some(CheckedChunk::new(
90 *root,
91 self.shard.clone(),
92 self.index,
93 shard_digest,
94 ))
95 }
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct CheckedChunk<D: Digest> {
105 root: D,
106 shard: Bytes,
107 index: u16,
108 digest: D,
109}
110
111impl<D: Digest> CheckedChunk<D> {
112 const fn new(root: D, shard: Bytes, index: u16, digest: D) -> Self {
113 Self {
114 root,
115 shard,
116 index,
117 digest,
118 }
119 }
120}
121
122impl<D: Digest> Write for Chunk<D> {
123 fn write(&self, writer: &mut impl BufMut) {
124 self.shard.write(writer);
125 self.index.write(writer);
126 self.proof.write(writer);
127 }
128
129 fn write_bufs(&self, buf: &mut impl BufsMut) {
130 self.shard.write_bufs(buf);
131 self.index.write(buf);
132 self.proof.write(buf);
133 }
134}
135
136impl<D: Digest> Read for Chunk<D> {
137 type Cfg = crate::CodecConfig;
139
140 fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
141 let shard = Bytes::read_cfg(reader, &RangeCfg::new(..=cfg.maximum_shard_size))?;
142 let index = u16::read(reader)?;
143 let proof = bmt::Proof::<D>::read_cfg(reader, &1)?;
144 Ok(Self {
145 shard,
146 index,
147 proof,
148 })
149 }
150}
151
152impl<D: Digest> EncodeSize for Chunk<D> {
153 fn encode_size(&self) -> usize {
154 self.shard.encode_size() + self.index.encode_size() + self.proof.encode_size()
155 }
156
157 fn encode_inline_size(&self) -> usize {
158 self.shard.encode_inline_size() + self.index.encode_size() + self.proof.encode_size()
159 }
160}
161
162impl<D: Digest> PartialEq for Chunk<D> {
163 fn eq(&self, other: &Self) -> bool {
164 self.shard == other.shard && self.index == other.index && self.proof == other.proof
165 }
166}
167
168impl<D: Digest> Eq for Chunk<D> {}
169
170#[cfg(feature = "arbitrary")]
171impl<D: Digest> arbitrary::Arbitrary<'_> for Chunk<D>
172where
173 D: for<'a> arbitrary::Arbitrary<'a>,
174{
175 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
176 Ok(Self {
177 shard: u.arbitrary::<Vec<u8>>()?.into(),
178 index: u.arbitrary()?,
179 proof: u.arbitrary()?,
180 })
181 }
182}
183
184fn prepare_data(mut data: impl Buf, k: usize) -> (Vec<u8>, usize) {
190 let data_len = data.remaining();
192 let shard_len = canonical_shard_len(data_len, k);
193
194 let length_bytes = (data_len as u32).to_be_bytes();
196 let mut padded = vec![0u8; k * shard_len];
197 padded[..u32::SIZE].copy_from_slice(&length_bytes);
198 data.copy_to_slice(&mut padded[u32::SIZE..u32::SIZE + data_len]);
199
200 (padded, shard_len)
201}
202
203const fn canonical_shard_len(data_len: usize, k: usize) -> usize {
210 let prefixed_len = u32::SIZE + data_len;
211 let mut shard_len = prefixed_len.div_ceil(k);
212
213 if !shard_len.is_multiple_of(2) {
215 shard_len += 1;
216 }
217
218 shard_len
219}
220
221fn extract_data(shards: &[&[u8]], k: usize, expected_shard_len: usize) -> Result<Vec<u8>, Error> {
227 let shards = shards.get(..k).ok_or(Error::NotEnoughChunks)?;
228 let data_len = read_data_len(shards)?;
229 let mut data = Vec::with_capacity(data_len);
230 let mut prefix_bytes_left = u32::SIZE;
231 let mut data_bytes_left = data_len;
232 for shard in shards {
233 if prefix_bytes_left >= shard.len() {
236 prefix_bytes_left -= shard.len();
237 continue;
238 }
239
240 let payload = &shard[prefix_bytes_left..];
242 let copy_len = data_bytes_left.min(payload.len());
243 data.extend_from_slice(&payload[..copy_len]);
244 data_bytes_left -= copy_len;
245
246 if !payload[copy_len..].iter().all(|byte| *byte == 0) {
248 return Err(Error::Inconsistent);
249 }
250 prefix_bytes_left = 0;
251 }
252
253 if data_bytes_left != 0 {
256 return Err(Error::Inconsistent);
257 }
258
259 if canonical_shard_len(data.len(), k) != expected_shard_len {
261 return Err(Error::Inconsistent);
262 }
263 Ok(data)
264}
265
266fn read_data_len(shards: &[&[u8]]) -> Result<usize, Error> {
269 let total_len: usize = shards.iter().map(|s| s.len()).sum();
270 if total_len < u32::SIZE {
271 return Err(Error::Inconsistent);
272 }
273
274 let mut prefix = [0u8; u32::SIZE];
276 let mut prefix_len = 0usize;
277 for shard in shards {
278 if prefix_len == u32::SIZE {
279 break;
280 }
281 let read = (u32::SIZE - prefix_len).min(shard.len());
282 prefix[prefix_len..prefix_len + read].copy_from_slice(&shard[..read]);
283 prefix_len += read;
284 }
285
286 let data_len = u32::from_be_bytes(prefix) as usize;
287 let payload_len = total_len - u32::SIZE;
288 if data_len > payload_len {
289 return Err(Error::Inconsistent);
290 }
291 Ok(data_len)
292}
293
294type Encoding<D> = (D, Vec<Chunk<D>>);
296
297fn encode<H: Hasher, S: Strategy>(
311 total: u16,
312 min: u16,
313 data: impl Buf,
314 strategy: &S,
315) -> Result<Encoding<H::Digest>, Error> {
316 assert!(total > min);
318 assert!(min > 0);
319 let n = total as usize;
320 let k = min as usize;
321 let m = n - k;
322 let data_len = data.remaining();
323 if data_len > u32::MAX as usize {
324 return Err(Error::InvalidDataLength(data_len));
325 }
326
327 let (padded, shard_len) = prepare_data(data, k);
329
330 let recovery_buf = {
332 let mut encoder = Cached::take(
333 &CACHED_ENCODER,
334 || ReedSolomonEncoder::new(k, m, shard_len),
335 |enc| enc.reset(k, m, shard_len),
336 )
337 .map_err(Error::ReedSolomon)?;
338 for shard in padded.chunks(shard_len) {
339 encoder
340 .add_original_shard(shard)
341 .map_err(Error::ReedSolomon)?;
342 }
343
344 let encoding = encoder.encode().map_err(Error::ReedSolomon)?;
346 let mut buf = Vec::with_capacity(m * shard_len);
347 for shard in encoding.recovery_iter() {
348 buf.extend_from_slice(shard);
349 }
350 buf
351 };
352
353 let originals: Bytes = padded.into();
355 let recoveries: Bytes = recovery_buf.into();
356
357 let mut builder = Builder::<H>::new(n);
359 let shard_slices: Vec<Bytes> = (0..k)
360 .map(|i| originals.slice(i * shard_len..(i + 1) * shard_len))
361 .chain((0..m).map(|i| recoveries.slice(i * shard_len..(i + 1) * shard_len)))
362 .collect();
363 let shard_hashes = strategy.map_init_collect_vec(&shard_slices, H::new, |hasher, shard| {
364 hasher.update(shard);
365 hasher.finalize()
366 });
367 for hash in &shard_hashes {
368 builder.add(hash);
369 }
370 let tree = builder.build();
371 let root = tree.root();
372
373 let mut chunks = Vec::with_capacity(n);
375 for (i, shard) in shard_slices.into_iter().enumerate() {
376 let proof = tree.proof(i as u32).map_err(|_| Error::InvalidProof)?;
377 chunks.push(Chunk::new(shard, i as u16, proof));
378 }
379
380 Ok((root, chunks))
381}
382
383fn decode<'a, H: Hasher, S: Strategy>(
398 total: u16,
399 min: u16,
400 root: &H::Digest,
401 chunks: impl Iterator<Item = &'a CheckedChunk<H::Digest>>,
402 strategy: &S,
403) -> Result<Vec<u8>, Error> {
404 assert!(total > min);
406 assert!(min > 0);
407 let n = total as usize;
408 let k = min as usize;
409 let m = n - k;
410 let mut chunks = chunks.peekable();
411 let Some(first) = chunks.peek() else {
412 return Err(Error::NotEnoughChunks);
413 };
414
415 let shard_len = first.shard.len();
417 let mut shard_digests: Vec<Option<H::Digest>> = vec![None; n];
418 let mut provided_shards: Vec<(usize, &[u8])> = Vec::with_capacity(n);
419 let mut provided_originals: Vec<(usize, &[u8])> = Vec::new();
420 let mut provided_recoveries: Vec<(usize, &[u8])> = Vec::new();
421 let mut provided = 0usize;
422 for chunk in chunks {
423 provided += 1;
424 if &chunk.root != root {
425 return Err(Error::CommitmentMismatch);
426 }
427 let index = chunk.index;
429 if index >= total {
430 return Err(Error::InvalidIndex(index));
431 }
432 let digest_slot = &mut shard_digests[index as usize];
433 if digest_slot.is_some() {
434 return Err(Error::DuplicateIndex(index));
435 }
436
437 *digest_slot = Some(chunk.digest);
439 provided_shards.push((index as usize, chunk.shard.as_ref()));
440 if index < min {
441 provided_originals.push((index as usize, chunk.shard.as_ref()));
442 } else {
443 provided_recoveries.push((index as usize - k, chunk.shard.as_ref()));
444 }
445 }
446 if provided < k {
447 return Err(Error::NotEnoughChunks);
448 }
449
450 let mut decoder = Cached::take(
452 &CACHED_DECODER,
453 || ReedSolomonDecoder::new(k, m, shard_len),
454 |dec| dec.reset(k, m, shard_len),
455 )
456 .map_err(Error::ReedSolomon)?;
457 for (idx, shard) in &provided_originals {
458 decoder
459 .add_original_shard(*idx, shard)
460 .map_err(Error::ReedSolomon)?;
461 }
462 for (idx, shard) in &provided_recoveries {
463 decoder
464 .add_recovery_shard(*idx, shard)
465 .map_err(Error::ReedSolomon)?;
466 }
467 let decoding = decoder.decode().map_err(Error::ReedSolomon)?;
468
469 let mut shards = vec![Default::default(); k];
471 for (idx, shard) in provided_originals
472 .into_iter()
473 .chain(decoding.restored_original_iter())
474 {
475 shards[idx] = shard;
476 }
477 let data = extract_data(&shards, k, shard_len)?;
478
479 let mut encoder = Cached::take(
481 &CACHED_ENCODER,
482 || ReedSolomonEncoder::new(k, m, shard_len),
483 |enc| enc.reset(k, m, shard_len),
484 )
485 .map_err(Error::ReedSolomon)?;
486 for shard in shards.iter().take(k) {
487 encoder
488 .add_original_shard(shard)
489 .map_err(Error::ReedSolomon)?;
490 }
491 let encoding = encoder.encode().map_err(Error::ReedSolomon)?;
492 shards.extend(encoding.recovery_iter());
493
494 for (idx, shard) in provided_shards {
496 if shard != shards[idx] {
497 return Err(Error::Inconsistent);
498 }
499 }
500
501 for (i, digest) in strategy.map_init_collect_vec(
503 shard_digests
504 .iter()
505 .enumerate()
506 .filter_map(|(i, digest)| digest.is_none().then_some(i)),
507 H::new,
508 |hasher, i| {
509 hasher.update(shards[i]);
510 (i, hasher.finalize())
511 },
512 ) {
513 shard_digests[i] = Some(digest);
514 }
515
516 let mut builder = Builder::<H>::new(n);
517 shard_digests
518 .into_iter()
519 .map(|digest| digest.expect("digest must be present for every shard"))
520 .for_each(|digest| {
521 builder.add(&digest);
522 });
523 let tree = builder.build();
524
525 if tree.root() != *root {
527 return Err(Error::Inconsistent);
528 }
529
530 Ok(data)
531}
532
533#[derive(Clone, Copy)]
616pub struct ReedSolomon<H> {
617 _marker: PhantomData<H>,
618}
619
620impl<H> std::fmt::Debug for ReedSolomon<H> {
621 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
622 f.debug_struct("ReedSolomon").finish()
623 }
624}
625
626impl<H: Hasher> Scheme for ReedSolomon<H> {
627 type Commitment = H::Digest;
628 type Shard = Chunk<H::Digest>;
629 type CheckedShard = CheckedChunk<H::Digest>;
630 type Error = Error;
631
632 fn encode(
633 config: &Config,
634 data: impl Buf,
635 strategy: &impl Strategy,
636 ) -> Result<(Self::Commitment, Vec<Self::Shard>), Self::Error> {
637 encode::<H, _>(
638 total_shards(config)?,
639 config.minimum_shards.get(),
640 data,
641 strategy,
642 )
643 }
644
645 fn check(
646 config: &Config,
647 commitment: &Self::Commitment,
648 index: u16,
649 shard: &Self::Shard,
650 ) -> Result<Self::CheckedShard, Self::Error> {
651 let total = total_shards(config)?;
652 if index >= total {
653 return Err(Error::InvalidIndex(index));
654 }
655 if shard.proof.leaf_count != u32::from(total) {
656 return Err(Error::InvalidProof);
657 }
658 if shard.index != index {
659 return Err(Error::InvalidIndex(shard.index));
660 }
661 shard
662 .verify::<H>(shard.index, commitment)
663 .ok_or(Error::InvalidProof)
664 }
665
666 fn decode<'a>(
667 config: &Config,
668 commitment: &Self::Commitment,
669 shards: impl Iterator<Item = &'a Self::CheckedShard>,
670 strategy: &impl Strategy,
671 ) -> Result<Vec<u8>, Self::Error> {
672 decode::<H, _>(
673 total_shards(config)?,
674 config.minimum_shards.get(),
675 commitment,
676 shards,
677 strategy,
678 )
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685 use commonware_codec::Encode;
686 use commonware_cryptography::Sha256;
687 use commonware_invariants::minifuzz;
688 use commonware_parallel::Sequential;
689 use commonware_runtime::{deterministic, iobuf::EncodeExt, BufferPooler, Runner};
690 use commonware_utils::NZU16;
691
692 type RS = ReedSolomon<Sha256>;
693 const STRATEGY: Sequential = Sequential;
694 const FUZZ_MAX_MIN_SHARDS: u16 = 8;
695 const FUZZ_MAX_EXTRA_SHARDS: u16 = 8;
696 const FUZZ_MAX_DATA_LEN: usize = 256;
697 const FUZZ_MAX_EXTRA_SHARD_WIDTH: usize = 16;
698
699 fn checked(
700 root: <Sha256 as Hasher>::Digest,
701 chunk: Chunk<<Sha256 as Hasher>::Digest>,
702 ) -> CheckedChunk<<Sha256 as Hasher>::Digest> {
703 let Chunk { shard, index, .. } = chunk;
704 let digest = Sha256::hash(&shard);
705 CheckedChunk::new(root, shard, index, digest)
706 }
707
708 fn build_chunks(
709 shards: &[Vec<u8>],
710 ) -> (
711 <Sha256 as Hasher>::Digest,
712 Vec<Chunk<<Sha256 as Hasher>::Digest>>,
713 ) {
714 let mut builder = Builder::<Sha256>::new(shards.len());
715 for shard in shards {
716 let mut hasher = Sha256::new();
717 hasher.update(shard);
718 builder.add(&hasher.finalize());
719 }
720 let tree = builder.build();
721 let root = tree.root();
722 let chunks = shards
723 .iter()
724 .enumerate()
725 .map(|(i, shard)| {
726 let proof = tree.proof(i as u32).unwrap();
727 Chunk::new(shard.clone().into(), i as u16, proof)
728 })
729 .collect();
730
731 (root, chunks)
732 }
733
734 fn selected_indices(
735 u: &mut arbitrary::Unstructured<'_>,
736 total: u16,
737 minimum: u16,
738 ) -> arbitrary::Result<Vec<u16>> {
739 let to_use = u.int_in_range(minimum..=total)?;
740 let mut selected = (0..total).collect::<Vec<_>>();
741 for i in 0..usize::from(to_use) {
742 let remaining = usize::from(total) - i;
743 let j = i + u.choose_index(remaining)?;
744 selected.swap(i, j);
745 }
746 selected.truncate(usize::from(to_use));
747 Ok(selected)
748 }
749
750 fn assert_decode_unique_commitment(
751 total: u16,
752 min: u16,
753 root: <Sha256 as Hasher>::Digest,
754 chunks: &[Chunk<<Sha256 as Hasher>::Digest>],
755 selected: &[u16],
756 ) {
757 let pieces = selected
758 .iter()
759 .map(|&i| chunks[usize::from(i)].verify::<Sha256>(i, &root).unwrap())
760 .collect::<Vec<_>>();
761
762 let Ok(decoded) = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY) else {
763 return;
764 };
765 let (canonical_root, _) =
766 encode::<Sha256, _>(total, min, decoded.as_slice(), &STRATEGY).unwrap();
767 assert_eq!(
768 root, canonical_root,
769 "decode accepted a root not produced by canonical encode"
770 );
771 }
772
773 fn fuzz_arbitrary_codeword(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()> {
774 let min = u.int_in_range(1..=FUZZ_MAX_MIN_SHARDS)?;
775 let extra = u.int_in_range(1..=FUZZ_MAX_EXTRA_SHARDS)?;
776 let total = min + extra;
777 let k = usize::from(min);
778 let m = usize::from(extra);
779
780 let data_len = u.int_in_range(0..=FUZZ_MAX_DATA_LEN)?;
781 let data = u.bytes(data_len)?.to_vec();
782 let canonical = canonical_shard_len(data.len(), k);
783 let extra_width = u.int_in_range(0..=FUZZ_MAX_EXTRA_SHARD_WIDTH / 2)? * 2;
784 let shard_len = canonical + extra_width;
785
786 let mut padded = vec![0u8; k * shard_len];
787 padded[..u32::SIZE].copy_from_slice(&(data.len() as u32).to_be_bytes());
788 padded[u32::SIZE..u32::SIZE + data.len()].copy_from_slice(&data);
789
790 let payload_end = u32::SIZE + data.len();
791 if payload_end < padded.len() && u.int_in_range(0..=3)? == 0 {
792 let offset = payload_end + u.choose_index(padded.len() - payload_end)?;
793 padded[offset] ^= u.arbitrary::<u8>()? | 1;
794 }
795
796 let mut encoder = ReedSolomonEncoder::new(k, m, shard_len).unwrap();
797 for shard in padded.chunks(shard_len) {
798 encoder.add_original_shard(shard).unwrap();
799 }
800 let recovery = encoder.encode().unwrap();
801
802 let mut shards = padded
803 .chunks(shard_len)
804 .map(|shard| shard.to_vec())
805 .collect::<Vec<_>>();
806 shards.extend(recovery.recovery_iter().map(|shard| shard.to_vec()));
807
808 let (root, chunks) = build_chunks(&shards);
809 let selected = selected_indices(u, total, min)?;
810 assert_decode_unique_commitment(total, min, root, &chunks, &selected);
811
812 Ok(())
813 }
814
815 fn fuzz_mixed_codeword(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()> {
816 let min = u.int_in_range(1..=FUZZ_MAX_MIN_SHARDS)?;
817 let extra = u.int_in_range(1..=FUZZ_MAX_EXTRA_SHARDS)?;
818 let total = min + extra;
819
820 let data_len = u.int_in_range(0..=FUZZ_MAX_DATA_LEN)?;
821 let data = u.bytes(data_len)?.to_vec();
822 let (_canonical_root, chunks) =
823 encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
824 let mut shards = chunks
825 .iter()
826 .map(|chunk| chunk.shard.to_vec())
827 .collect::<Vec<_>>();
828
829 let mutated = usize::from(min + u.int_in_range(0..=extra - 1)?);
830 let offset = u.choose_index(shards[mutated].len())?;
831 shards[mutated][offset] ^= u.arbitrary::<u8>()? | 1;
832
833 let (root, chunks) = build_chunks(&shards);
834 let mut selected = (0..min).collect::<Vec<_>>();
835 selected.push(mutated as u16);
836 assert_decode_unique_commitment(total, min, root, &chunks, &selected);
837
838 Ok(())
839 }
840
841 #[test]
842 fn test_recovery() {
843 let data = b"Testing recovery pieces";
844 let total = 8u16;
845 let min = 3u16;
846
847 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
849
850 let pieces: Vec<_> = vec![
852 checked(root, chunks[0].clone()), checked(root, chunks[4].clone()), checked(root, chunks[6].clone()), ];
856
857 let decoded = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY).unwrap();
859 assert_eq!(decoded, data);
860 }
861
862 #[test]
863 fn test_not_enough_pieces() {
864 let data = b"Test insufficient pieces";
865 let total = 6u16;
866 let min = 4u16;
867
868 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
870
871 let pieces: Vec<_> = chunks
873 .into_iter()
874 .take(2)
875 .map(|c| checked(root, c))
876 .collect();
877
878 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
880 assert!(matches!(result, Err(Error::NotEnoughChunks)));
881 }
882
883 #[test]
884 fn test_duplicate_index() {
885 let data = b"Test duplicate detection";
886 let total = 5u16;
887 let min = 3u16;
888
889 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
891
892 let pieces = [
894 checked(root, chunks[0].clone()),
895 checked(root, chunks[0].clone()),
896 checked(root, chunks[1].clone()),
897 ];
898
899 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
901 assert!(matches!(result, Err(Error::DuplicateIndex(0))));
902 }
903
904 #[test]
905 fn test_invalid_index() {
906 let data = b"Test invalid index";
907 let total = 5u16;
908 let min = 3u16;
909
910 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
912
913 for i in 0..total {
915 assert!(chunks[i as usize].verify::<Sha256>(i + 1, &root).is_none());
916 }
917 }
918
919 #[test]
920 #[should_panic(expected = "assertion failed: total > min")]
921 fn test_invalid_total() {
922 let data = b"Test parameter validation";
923
924 encode::<Sha256, _>(3, 3, data.as_slice(), &STRATEGY).unwrap();
926 }
927
928 #[test]
929 #[should_panic(expected = "assertion failed: min > 0")]
930 fn test_invalid_min() {
931 let data = b"Test parameter validation";
932
933 encode::<Sha256, _>(5, 0, data.as_slice(), &STRATEGY).unwrap();
935 }
936
937 #[test]
938 fn test_empty_data() {
939 let data = b"";
940 let total = 100u16;
941 let min = 30u16;
942
943 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
945
946 let minimal = chunks
948 .into_iter()
949 .take(min as usize)
950 .map(|c| checked(root, c))
951 .collect::<Vec<_>>();
952 let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
953 assert_eq!(decoded, data);
954 }
955
956 #[test]
957 fn test_large_data() {
958 let data = vec![42u8; 1000]; let total = 7u16;
960 let min = 4u16;
961
962 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
964
965 let minimal = chunks
967 .into_iter()
968 .take(min as usize)
969 .map(|c| checked(root, c))
970 .collect::<Vec<_>>();
971 let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
972 assert_eq!(decoded, data);
973 }
974
975 #[test]
976 fn test_malicious_root_detection() {
977 let data = b"Original data that should be protected";
978 let total = 7u16;
979 let min = 4u16;
980
981 let (_correct_root, chunks) =
983 encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
984
985 let mut hasher = Sha256::new();
987 hasher.update(b"malicious_data_that_wasnt_actually_encoded");
988 let malicious_root = hasher.finalize();
989
990 for i in 0..total {
992 assert!(chunks[i as usize]
993 .clone()
994 .verify::<Sha256>(i, &malicious_root)
995 .is_none());
996 }
997
998 let minimal = chunks
1001 .into_iter()
1002 .take(min as usize)
1003 .map(|c| checked(_correct_root, c))
1004 .collect::<Vec<_>>();
1005
1006 let result = decode::<Sha256, _>(total, min, &malicious_root, minimal.iter(), &STRATEGY);
1009 assert!(matches!(result, Err(Error::CommitmentMismatch)));
1010 }
1011
1012 #[test]
1013 fn test_mismatched_config_rejected_during_check() {
1014 let config_expected = Config {
1015 minimum_shards: NZU16!(2),
1016 extra_shards: NZU16!(2),
1017 };
1018 let config_actual = Config {
1019 minimum_shards: NZU16!(3),
1020 extra_shards: NZU16!(3),
1021 };
1022
1023 let data = b"leaf_count mismatch proof";
1024 let (commitment, shards) = RS::encode(&config_actual, data.as_slice(), &STRATEGY).unwrap();
1025
1026 let check_result = RS::check(&config_expected, &commitment, 0, &shards[0]);
1029 assert!(matches!(check_result, Err(Error::InvalidProof)));
1030 }
1031
1032 #[test]
1033 fn test_manipulated_chunk_detection() {
1034 let data = b"Data integrity must be maintained";
1035 let total = 6u16;
1036 let min = 3u16;
1037
1038 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1040 let mut pieces: Vec<_> = chunks.into_iter().map(|c| checked(root, c)).collect();
1041
1042 if !pieces[1].shard.is_empty() {
1044 let mut shard = pieces[1].shard.to_vec();
1045 shard[0] ^= 0xFF; pieces[1].shard = shard.into();
1047 }
1048
1049 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1051 assert!(matches!(result, Err(Error::Inconsistent)));
1052 }
1053
1054 #[test]
1055 fn test_inconsistent_shards() {
1056 let data = b"Test data for malicious encoding";
1057 let total = 5u16;
1058 let min = 3u16;
1059 let m = total - min;
1060
1061 let (padded, shard_size) = prepare_data(data.as_slice(), min as usize);
1063
1064 let mut encoder = ReedSolomonEncoder::new(min as usize, m as usize, shard_size).unwrap();
1066 for shard in padded.chunks(shard_size) {
1067 encoder.add_original_shard(shard).unwrap();
1068 }
1069 let recovery_result = encoder.encode().unwrap();
1070 let mut recovery_shards: Vec<Vec<u8>> = recovery_result
1071 .recovery_iter()
1072 .map(|s| s.to_vec())
1073 .collect();
1074
1075 if !recovery_shards[0].is_empty() {
1077 recovery_shards[0][0] ^= 0xFF;
1078 }
1079
1080 let mut malicious_shards: Vec<Vec<u8>> =
1082 padded.chunks(shard_size).map(|s| s.to_vec()).collect();
1083 malicious_shards.extend(recovery_shards);
1084
1085 let mut builder = Builder::<Sha256>::new(total as usize);
1087 for shard in &malicious_shards {
1088 let mut hasher = Sha256::new();
1089 hasher.update(shard);
1090 builder.add(&hasher.finalize());
1091 }
1092 let malicious_tree = builder.build();
1093 let malicious_root = malicious_tree.root();
1094
1095 let selected_indices = vec![0, 1, 3]; let mut pieces = Vec::new();
1098 for &i in &selected_indices {
1099 let merkle_proof = malicious_tree.proof(i as u32).unwrap();
1100 let shard = malicious_shards[i].clone();
1101 let chunk = Chunk::new(shard.into(), i as u16, merkle_proof);
1102 pieces.push(chunk);
1103 }
1104 let pieces: Vec<_> = pieces
1105 .into_iter()
1106 .map(|c| checked(malicious_root, c))
1107 .collect();
1108
1109 let result = decode::<Sha256, _>(total, min, &malicious_root, pieces.iter(), &STRATEGY);
1111 assert!(matches!(result, Err(Error::Inconsistent)));
1112 }
1113
1114 #[test]
1118 fn test_non_canonical_padding_rejected() {
1119 let data = b"X";
1120 let total = 6u16;
1121 let min = 3u16;
1122 let k = min as usize;
1123 let m = total as usize - k;
1124
1125 let (mut padded, shard_len) = prepare_data(data.as_slice(), k);
1126 let payload_end = u32::SIZE + data.len();
1127 let total_original_len = k * shard_len;
1128 assert!(payload_end < total_original_len, "test requires padding");
1129
1130 let pad_shard = payload_end / shard_len;
1132 let pad_offset = payload_end % shard_len;
1133 padded[pad_shard * shard_len + pad_offset] = 0xAA;
1134
1135 let mut encoder = ReedSolomonEncoder::new(k, m, shard_len).unwrap();
1136 for shard in padded.chunks(shard_len) {
1137 encoder.add_original_shard(shard).unwrap();
1138 }
1139 let recovery = encoder.encode().unwrap();
1140 let mut shards: Vec<Vec<u8>> = padded.chunks(shard_len).map(|s| s.to_vec()).collect();
1141 shards.extend(recovery.recovery_iter().map(|s| s.to_vec()));
1142
1143 let mut builder = Builder::<Sha256>::new(total as usize);
1144 for shard in &shards {
1145 let mut hasher = Sha256::new();
1146 hasher.update(shard);
1147 builder.add(&hasher.finalize());
1148 }
1149 let tree = builder.build();
1150 let non_canonical_root = tree.root();
1151
1152 let mut pieces = Vec::with_capacity(k);
1153 for (i, shard) in shards.iter().take(k).enumerate() {
1154 let proof = tree.proof(i as u32).unwrap();
1155 pieces.push(checked(
1156 non_canonical_root,
1157 Chunk::new(shard.clone().into(), i as u16, proof),
1158 ));
1159 }
1160
1161 let result = decode::<Sha256, _>(total, min, &non_canonical_root, pieces.iter(), &STRATEGY);
1162 assert!(matches!(result, Err(Error::Inconsistent)));
1163 }
1164
1165 #[test]
1166 fn minifuzz_decode_unique_commitment() {
1167 minifuzz::Builder::default()
1168 .with_search_limit(2048)
1169 .test(|u| {
1170 fuzz_arbitrary_codeword(u)?;
1171 fuzz_mixed_codeword(u)?;
1172 Ok(())
1173 });
1174 }
1175
1176 #[test]
1177 fn test_oversized_zero_padded_shards_rejected() {
1178 let data = b"X";
1179 let total = 6u16;
1180 let min = 3u16;
1181 let k = min as usize;
1182 let m = total as usize - k;
1183
1184 let oversized_shard_len = 4usize;
1185 let mut padded = vec![0u8; k * oversized_shard_len];
1186 padded[..u32::SIZE].copy_from_slice(&(data.len() as u32).to_be_bytes());
1187 padded[u32::SIZE..u32::SIZE + data.len()].copy_from_slice(data);
1188
1189 let mut encoder = ReedSolomonEncoder::new(k, m, oversized_shard_len).unwrap();
1190 for shard in padded.chunks(oversized_shard_len) {
1191 encoder.add_original_shard(shard).unwrap();
1192 }
1193 let recovery = encoder.encode().unwrap();
1194
1195 let mut oversized_shards: Vec<Vec<u8>> = padded
1196 .chunks(oversized_shard_len)
1197 .map(|shard| shard.to_vec())
1198 .collect();
1199 oversized_shards.extend(recovery.recovery_iter().map(|shard| shard.to_vec()));
1200
1201 let mut builder = Builder::<Sha256>::new(total as usize);
1202 for shard in &oversized_shards {
1203 let mut hasher = Sha256::new();
1204 hasher.update(shard);
1205 builder.add(&hasher.finalize());
1206 }
1207 let oversized_tree = builder.build();
1208 let oversized_root = oversized_tree.root();
1209
1210 let (canonical_root, _) =
1211 encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1212 assert_ne!(oversized_root, canonical_root);
1213
1214 let pieces = [0u16, 1u16, 4u16]
1215 .into_iter()
1216 .map(|i| {
1217 let proof = oversized_tree.proof(i as u32).unwrap();
1218 checked(
1219 oversized_root,
1220 Chunk::new(oversized_shards[i as usize].clone().into(), i, proof),
1221 )
1222 })
1223 .collect::<Vec<_>>();
1224
1225 let result = decode::<Sha256, _>(total, min, &oversized_root, pieces.iter(), &STRATEGY);
1226 assert!(matches!(result, Err(Error::Inconsistent)));
1227 }
1228
1229 #[test]
1230 fn test_extra_non_canonical_recovery_rejected() {
1231 let data = b"canonical originals with bad recovery";
1232 let total = 6u16;
1233 let min = 3u16;
1234
1235 let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1236 let mut shards = chunks
1237 .iter()
1238 .map(|chunk| chunk.shard.to_vec())
1239 .collect::<Vec<_>>();
1240 shards[min as usize][0] ^= 0xFF;
1241
1242 let mut builder = Builder::<Sha256>::new(total as usize);
1243 for shard in &shards {
1244 let mut hasher = Sha256::new();
1245 hasher.update(shard);
1246 builder.add(&hasher.finalize());
1247 }
1248 let tree = builder.build();
1249 let root = tree.root();
1250
1251 let pieces = (0u16..=3u16)
1252 .map(|i| {
1253 let proof = tree.proof(i as u32).unwrap();
1254 checked(
1255 root,
1256 Chunk::new(shards[i as usize].clone().into(), i, proof),
1257 )
1258 })
1259 .collect::<Vec<_>>();
1260
1261 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1262 assert!(matches!(result, Err(Error::Inconsistent)));
1263 }
1264
1265 #[test]
1266 fn test_reconstructed_original_with_extra_non_canonical_recovery_rejected() {
1267 let data = b"canonical reconstructed originals with bad extra recovery";
1268 let total = 6u16;
1269 let min = 3u16;
1270
1271 let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1272 let mut shards = chunks
1273 .iter()
1274 .map(|chunk| chunk.shard.to_vec())
1275 .collect::<Vec<_>>();
1276 shards[4][0] ^= 0xFF;
1277
1278 let mut builder = Builder::<Sha256>::new(total as usize);
1279 for shard in &shards {
1280 let mut hasher = Sha256::new();
1281 hasher.update(shard);
1282 builder.add(&hasher.finalize());
1283 }
1284 let tree = builder.build();
1285 let root = tree.root();
1286
1287 let pieces = [0u16, 1u16, 3u16, 4u16]
1288 .into_iter()
1289 .map(|i| {
1290 let proof = tree.proof(i as u32).unwrap();
1291 checked(
1292 root,
1293 Chunk::new(shards[i as usize].clone().into(), i, proof),
1294 )
1295 })
1296 .collect::<Vec<_>>();
1297
1298 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1299 assert!(matches!(result, Err(Error::Inconsistent)));
1300 }
1301
1302 #[test]
1303 fn test_decode_invalid_index() {
1304 let data = b"Testing recovery pieces";
1305 let total = 8u16;
1306 let min = 3u16;
1307
1308 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1310
1311 let mut invalid = checked(root, chunks[1].clone());
1313 invalid.index = 8;
1314 let pieces: Vec<_> = vec![
1315 checked(root, chunks[0].clone()), invalid, checked(root, chunks[6].clone()), ];
1319
1320 let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1322 assert!(matches!(result, Err(Error::InvalidIndex(8))));
1323 }
1324
1325 #[test]
1326 fn test_max_chunks() {
1327 let data = vec![42u8; 1000]; let total = u16::MAX;
1329 let min = u16::MAX / 2;
1330
1331 let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1333
1334 let minimal = chunks
1336 .into_iter()
1337 .take(min as usize)
1338 .map(|c| checked(root, c))
1339 .collect::<Vec<_>>();
1340 let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
1341 assert_eq!(decoded, data);
1342 }
1343
1344 #[test]
1345 fn test_too_many_chunks() {
1346 let data = vec![42u8; 1000]; let total = u16::MAX;
1348 let min = u16::MAX / 2 - 1;
1349
1350 let result = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY);
1352 assert!(matches!(
1353 result,
1354 Err(Error::ReedSolomon(
1355 reed_solomon_simd::Error::UnsupportedShardCount {
1356 original_count: _,
1357 recovery_count: _,
1358 }
1359 ))
1360 ));
1361 }
1362
1363 #[test]
1364 fn test_too_many_total_shards() {
1365 assert!(RS::encode(
1366 &Config {
1367 minimum_shards: NZU16!(u16::MAX / 2 + 1),
1368 extra_shards: NZU16!(u16::MAX),
1369 },
1370 [].as_slice(),
1371 &STRATEGY,
1372 )
1373 .is_err())
1374 }
1375
1376 #[test]
1377 fn test_chunk_encode_with_pool_matches_encode() {
1378 let executor = deterministic::Runner::default();
1379 executor.start(|context| async move {
1380 let pool = context.network_buffer_pool();
1381
1382 let data = b"pool encoding test";
1383 let (_root, chunks) = encode::<Sha256, _>(5, 3, data.as_slice(), &STRATEGY).unwrap();
1384 let chunk = &chunks[0];
1385
1386 let encoded = chunk.encode();
1387 let mut encoded_pool = chunk.encode_with_pool(pool);
1388 let mut encoded_pool_bytes = vec![0u8; encoded_pool.remaining()];
1389 encoded_pool.copy_to_slice(&mut encoded_pool_bytes);
1390 assert_eq!(encoded_pool_bytes, encoded.as_ref());
1391 });
1392 }
1393
1394 #[cfg(feature = "arbitrary")]
1395 mod conformance {
1396 use super::*;
1397 use commonware_codec::conformance::CodecConformance;
1398 use commonware_cryptography::sha256::Digest as Sha256Digest;
1399
1400 commonware_conformance::conformance_tests! {
1401 CodecConformance<Chunk<Sha256Digest>>,
1402 }
1403 }
1404}