Skip to main content

commonware_coding/
reed_solomon.rs

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
12// Thread-local caches for reusing `ReedSolomonEncoder` and `ReedSolomonDecoder`
13// instances across calls. Constructing these objects is expensive because
14// the underlying engine initializes GF lookup tables. The `reset()` method
15// reconfigures the work buffers without rebuilding those tables.
16commonware_utils::thread_local_cache!(static CACHED_ENCODER: ReedSolomonEncoder);
17commonware_utils::thread_local_cache!(static CACHED_DECODER: ReedSolomonDecoder);
18
19/// Errors that can occur when interacting with the Reed-Solomon coder.
20#[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/// A piece of data from a Reed-Solomon encoded object.
50#[derive(Debug, Clone)]
51pub struct Chunk<D: Digest> {
52    /// The shard of encoded data.
53    shard: Bytes,
54
55    /// The index of [`Chunk`] in the original data.
56    index: u16,
57
58    /// The multi-proof of the shard in the [`bmt`] at the given index.
59    proof: bmt::Proof<D>,
60}
61
62impl<D: Digest> Chunk<D> {
63    /// Create a new [`Chunk`] from the given shard, index, and proof.
64    const fn new(shard: Bytes, index: u16, proof: bmt::Proof<D>) -> Self {
65        Self {
66            shard,
67            index,
68            proof,
69        }
70    }
71
72    /// Verify a [`Chunk`] against the given root.
73    fn verify<H: Hasher<Digest = D>>(&self, index: u16, root: &D) -> Option<CheckedChunk<D>> {
74        // Ensure the index matches
75        if index != self.index {
76            return None;
77        }
78
79        // Compute shard digest
80        let mut hasher = H::new();
81        hasher.update(&self.shard);
82        let shard_digest = hasher.finalize();
83
84        // Verify proof
85        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/// A shard that has been checked against a commitment.
99///
100/// This stores the shard digest computed during [`Chunk::verify`] and the
101/// commitment root it was verified against. The root is checked at decode
102/// time to prevent cross-commitment shard mixing.
103#[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    /// The maximum size of the shard.
138    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
184/// Prepare data for encoding.
185///
186/// Returns a contiguous buffer of `k` padded shards and the shard length.
187/// The buffer layout is `[length_prefix | data | zero_padding]` split into
188/// `k` equal-sized shards of `shard_len` bytes each.
189fn prepare_data(mut data: impl Buf, k: usize) -> (Vec<u8>, usize) {
190    // Compute shard length
191    let data_len = data.remaining();
192    let shard_len = canonical_shard_len(data_len, k);
193
194    // Prepare data
195    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
203/// Return the canonical shard width for a payload and shard count.
204///
205/// Encoding prefixes the payload with its length, splits the result across
206/// `k` original shards, and rounds up to an even width required by
207/// `reed-solomon-simd`. Decode uses the same calculation to reject commitments
208/// that decode to the same payload with a non-canonical shard width.
209const 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    // Ensure shard length is even (required for optimizations in `reed-solomon-simd`)
214    if !shard_len.is_multiple_of(2) {
215        shard_len += 1;
216    }
217
218    shard_len
219}
220
221/// Extract data from encoded shards and verify that original shards use the canonical width.
222///
223/// The first `k` shards, when concatenated, form `[length_prefix | data | padding]`.
224/// This function copies only the data bytes while validating trailing zero
225/// padding directly from the shard slices.
226fn 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        // The length prefix may straddle shard boundaries, so ignore bytes until
234        // we reach the first payload byte.
235        if prefix_bytes_left >= shard.len() {
236            prefix_bytes_left -= shard.len();
237            continue;
238        }
239
240        // Copy only the live payload bytes from this shard.
241        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        // Any remaining bytes in this shard must be canonical zero padding.
247        if !payload[copy_len..].iter().all(|byte| *byte == 0) {
248            return Err(Error::Inconsistent);
249        }
250        prefix_bytes_left = 0;
251    }
252
253    // The prefix advertised more payload bytes than were present in the first
254    // `k` shards.
255    if data_bytes_left != 0 {
256        return Err(Error::Inconsistent);
257    }
258
259    // Validate that the original shards use the canonical shard width.
260    if canonical_shard_len(data.len(), k) != expected_shard_len {
261        return Err(Error::Inconsistent);
262    }
263    Ok(data)
264}
265
266/// Read the 4-byte big-endian length prefix from `shards` and validate that
267/// the decoded length fits in the post-prefix payload region.
268fn 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    // Read the length prefix, which may span multiple shards.
275    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
294/// Type alias for the internal encoding result.
295type Encoding<D> = (D, Vec<Chunk<D>>);
296
297/// Encode data using a Reed-Solomon coder and insert it into a [`bmt`].
298///
299/// # Parameters
300///
301/// - `total`: The total number of chunks to generate.
302/// - `min`: The minimum number of chunks required to decode the data.
303/// - `data`: The data to encode.
304/// - `strategy`: The parallelism strategy to use.
305///
306/// # Returns
307///
308/// - `root`: The root of the [`bmt`].
309/// - `chunks`: [`Chunk`]s of encoded data (that can be proven against `root`).
310fn 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    // Validate parameters
317    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    // Prepare data as a contiguous buffer of k shards
328    let (padded, shard_len) = prepare_data(data, k);
329
330    // Create or reuse encoder
331    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        // Compute recovery shards and collect into a contiguous buffer
345        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    // Create zero-copy Bytes views into the original and recovery buffers
354    let originals: Bytes = padded.into();
355    let recoveries: Bytes = recovery_buf.into();
356
357    // Build Merkle tree
358    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    // Generate chunks with zero-copy shard views
374    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
383/// Decode data from a set of [`CheckedChunk`]s.
384///
385/// It is assumed that all chunks have already been verified against the given root using [`Chunk::verify`].
386///
387/// # Parameters
388///
389/// - `total`: The total number of chunks to generate.
390/// - `min`: The minimum number of chunks required to decode the data.
391/// - `root`: The root of the [`bmt`].
392/// - `chunks`: [`CheckedChunk`]s of encoded data (that can be proven against `root`)
393///
394/// # Returns
395///
396/// - `data`: The decoded data.
397fn 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    // Validate parameters
405    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    // Process checked chunks
416    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        // Check for duplicate index
428        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        // Add to provided shards and retain the checked digest for this index.
438        *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    // Decode original data
451    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    // Reconstruct all original shards
470    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    // Re-encode recovered data to get recovery shards
480    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    // Confirm all checked shards match the canonical codeword before reusing their digests.
495    for (idx, shard) in provided_shards {
496        if shard != shards[idx] {
497            return Err(Error::Inconsistent);
498        }
499    }
500
501    // Build Merkle tree from the canonical codeword.
502    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    // Confirm root is consistent
526    if tree.root() != *root {
527        return Err(Error::Inconsistent);
528    }
529
530    Ok(data)
531}
532
533/// A SIMD-optimized Reed-Solomon coder that emits chunks that can be proven against a [`bmt`].
534///
535/// # Behavior
536///
537/// The encoder takes input data, splits it into `k` data shards, and generates `m` recovery
538/// shards using [Reed-Solomon encoding](https://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction).
539/// All `n = k + m` shards are then used to build a [`bmt`], producing a single root hash. Each shard
540/// is packaged as a chunk containing the shard data, its index, and a Merkle multi-proof against the [`bmt`] root.
541///
542/// ## Encoding
543///
544/// ```text
545///               +--------------------------------------+
546///               |         Original Data (Bytes)        |
547///               +--------------------------------------+
548///                                  |
549///                                  v
550///               +--------------------------------------+
551///               | [Length Prefix | Original Data...]   |
552///               +--------------------------------------+
553///                                  |
554///                                  v
555///              +----------+ +----------+    +-----------+
556///              |  Shard 0 | |  Shard 1 | .. | Shard k-1 |  (Data Shards)
557///              +----------+ +----------+    +-----------+
558///                     |            |             |
559///                     |            |             |
560///                     +------------+-------------+
561///                                  |
562///                                  v
563///                        +------------------+
564///                        | Reed-Solomon     |
565///                        | Encoder (k, m)   |
566///                        +------------------+
567///                                  |
568///                                  v
569///              +----------+ +----------+    +-----------+
570///              |  Shard k | | Shard k+1| .. | Shard n-1 |  (Recovery Shards)
571///              +----------+ +----------+    +-----------+
572/// ```
573///
574/// ## Merkle Tree Construction
575///
576/// All `n` shards (data and recovery) are hashed and used as leaves to build a [`bmt`].
577///
578/// ```text
579/// Shards:    [Shard 0, Shard 1, ..., Shard n-1]
580///             |        |              |
581///             v        v              v
582/// Hashes:    [H(S_0), H(S_1), ..., H(S_n-1)]
583///             \       / \       /
584///              \     /   \     /
585///               +---+     +---+
586///                 |         |
587///                 \         /
588///                  \       /
589///                   +-----+
590///                      |
591///                      v
592///                +----------+
593///                |   Root   |
594///                +----------+
595/// ```
596///
597/// The final output is the [`bmt`] root and a set of `n` chunks.
598///
599/// `(Root, [Chunk 0, Chunk 1, ..., Chunk n-1])`
600///
601/// Each chunk contains:
602/// - `shard`: The shard data (original or recovery).
603/// - `index`: The shard's original index (0 to n-1).
604/// - `proof`: A Merkle multi-proof of the shard's inclusion in the [`bmt`].
605///
606/// ## Decoding and Verification
607///
608/// The decoder requires any `k` chunks to reconstruct the original data.
609/// 1. Each chunk's Merkle multi-proof is verified against the [`bmt`] root.
610/// 2. The shards from the valid chunks are used to reconstruct the original `k` data shards.
611/// 3. To ensure consistency, the recovered data shards are re-encoded, and a new [`bmt`] root is
612///    generated. This new root MUST match the original [`bmt`] root. This prevents attacks where
613///    an adversary provides a valid set of chunks that decode to different data.
614/// 4. If the roots match, the original data is extracted from the reconstructed data shards.
615#[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        // Encode the data
848        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
849
850        // Use a mix of original and recovery pieces
851        let pieces: Vec<_> = vec![
852            checked(root, chunks[0].clone()), // original
853            checked(root, chunks[4].clone()), // recovery
854            checked(root, chunks[6].clone()), // recovery
855        ];
856
857        // Try to decode with a mix of original and recovery pieces
858        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        // Encode data
869        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
870
871        // Try with fewer than min
872        let pieces: Vec<_> = chunks
873            .into_iter()
874            .take(2)
875            .map(|c| checked(root, c))
876            .collect();
877
878        // Fail to decode
879        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        // Encode data
890        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
891
892        // Include duplicate index by cloning the first chunk
893        let pieces = [
894            checked(root, chunks[0].clone()),
895            checked(root, chunks[0].clone()),
896            checked(root, chunks[1].clone()),
897        ];
898
899        // Fail to decode
900        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        // Encode data
911        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
912
913        // Verify all proofs at invalid index
914        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        // total <= min should panic
925        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        // min = 0 should panic
934        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        // Encode data
944        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
945
946        // Try to decode with min
947        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]; // 1KB of data
959        let total = 7u16;
960        let min = 4u16;
961
962        // Encode data
963        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
964
965        // Try to decode with min
966        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        // Encode data correctly to get valid chunks
982        let (_correct_root, chunks) =
983            encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
984
985        // Create a malicious/fake root (simulating a malicious encoder)
986        let mut hasher = Sha256::new();
987        hasher.update(b"malicious_data_that_wasnt_actually_encoded");
988        let malicious_root = hasher.finalize();
989
990        // Verify all proofs at incorrect root
991        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        // Collect valid pieces (these are legitimate fragments checked against
999        // the correct root).
1000        let minimal = chunks
1001            .into_iter()
1002            .take(min as usize)
1003            .map(|c| checked(_correct_root, c))
1004            .collect::<Vec<_>>();
1005
1006        // Attempt to decode with malicious root - rejected because checked
1007        // chunks are bound to a different commitment.
1008        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        // Previously this passed because check() ignored config and only verified
1027        // against commitment root. It must now fail immediately.
1028        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        // Encode data
1039        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        // Tamper with one of the checked chunks by modifying the shard data.
1043        if !pieces[1].shard.is_empty() {
1044            let mut shard = pieces[1].shard.to_vec();
1045            shard[0] ^= 0xFF; // Flip bits in first byte
1046            pieces[1].shard = shard.into();
1047        }
1048
1049        // Try to decode with the tampered chunk
1050        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        // Compute original data encoding
1062        let (padded, shard_size) = prepare_data(data.as_slice(), min as usize);
1063
1064        // Re-encode the data
1065        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        // Tamper with one recovery shard
1076        if !recovery_shards[0].is_empty() {
1077            recovery_shards[0][0] ^= 0xFF;
1078        }
1079
1080        // Build malicious shards
1081        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        // Build malicious tree
1086        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        // Generate chunks for min pieces, including the tampered recovery
1096        let selected_indices = vec![0, 1, 3]; // originals 0,1 and recovery 0 (index 3)
1097        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        // Fail to decode
1110        let result = decode::<Sha256, _>(total, min, &malicious_root, pieces.iter(), &STRATEGY);
1111        assert!(matches!(result, Err(Error::Inconsistent)));
1112    }
1113
1114    // Regression: a commitment built from shards with non-zero trailing padding
1115    // used to pass decode(), even though canonical re-encoding (zero padding)
1116    // produces a different root. decode() must reject such non-canonical shards.
1117    #[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        // Corrupt one canonical padding byte while keeping payload unchanged.
1131        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        // Encode the data
1309        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1310
1311        // Use a mix of original and recovery pieces
1312        let mut invalid = checked(root, chunks[1].clone());
1313        invalid.index = 8;
1314        let pieces: Vec<_> = vec![
1315            checked(root, chunks[0].clone()), // original
1316            invalid,                          // recovery with invalid index
1317            checked(root, chunks[6].clone()), // recovery
1318        ];
1319
1320        // Fail to decode
1321        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]; // 1KB of data
1328        let total = u16::MAX;
1329        let min = u16::MAX / 2;
1330
1331        // Encode data
1332        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1333
1334        // Try to decode with min
1335        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]; // 1KB of data
1347        let total = u16::MAX;
1348        let min = u16::MAX / 2 - 1;
1349
1350        // Encode data
1351        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}