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::{
5    reed_solomon::{Decoder, Encoder, Error as RsError, SHARD_CHUNK_BYTES},
6    Digest, Hasher,
7};
8use commonware_parallel::Strategy;
9use commonware_storage::bmt::{self, Builder};
10use commonware_utils::Cached;
11use std::{marker::PhantomData, ops::Range};
12use thiserror::Error;
13
14// Thread-local caches for reusing `Encoder` and `Decoder`
15// instances across calls. Constructing these objects is expensive because
16// the underlying engine initializes GF lookup tables. The `reset()` method
17// reconfigures the work buffers without rebuilding those tables.
18commonware_utils::thread_local_cache!(static CACHED_ENCODER: Encoder);
19commonware_utils::thread_local_cache!(static CACHED_DECODER: Decoder);
20
21// Keep each stripe large enough to amortize extra encoder/decoder setup.
22const MIN_STRIPE_BYTES: usize = 8 * 1024;
23
24/// Errors that can occur when interacting with the Reed-Solomon coder.
25#[derive(Error, Debug)]
26pub enum Error {
27    #[error("reed-solomon error: {0}")]
28    ReedSolomon(#[from] RsError),
29    #[error("inconsistent")]
30    Inconsistent,
31    #[error("invalid proof")]
32    InvalidProof,
33    #[error("not enough chunks")]
34    NotEnoughChunks,
35    #[error("duplicate chunk index: {0}")]
36    DuplicateIndex(u16),
37    #[error("invalid data length: {0}")]
38    InvalidDataLength(usize),
39    #[error("invalid index: {0}")]
40    InvalidIndex(u16),
41    #[error("too many total shards: {0}")]
42    TooManyTotalShards(u32),
43    #[error("checked shard commitment does not match decode commitment")]
44    CommitmentMismatch,
45}
46
47fn total_shards(config: &Config) -> Result<u16, Error> {
48    let total = config.total_shards();
49    total
50        .try_into()
51        .map_err(|_| Error::TooManyTotalShards(total))
52}
53
54/// A piece of data from a Reed-Solomon encoded object.
55#[derive(Debug, Clone)]
56pub struct Chunk<D: Digest> {
57    /// The shard of encoded data.
58    shard: Bytes,
59
60    /// The index of [`Chunk`] in the original data.
61    index: u16,
62
63    /// The multi-proof of the shard in the [`bmt`] at the given index.
64    proof: bmt::Proof<D>,
65}
66
67impl<D: Digest> Chunk<D> {
68    /// Create a new [`Chunk`] from the given shard, index, and proof.
69    const fn new(shard: Bytes, index: u16, proof: bmt::Proof<D>) -> Self {
70        Self {
71            shard,
72            index,
73            proof,
74        }
75    }
76
77    /// Verify a [`Chunk`] against the given root.
78    fn verify<H: Hasher<Digest = D>>(&self, index: u16, root: &D) -> Option<CheckedChunk<D>> {
79        // Ensure the index matches
80        if index != self.index {
81            return None;
82        }
83
84        // Compute shard digest
85        let mut hasher = H::new();
86        hasher.update(&self.shard);
87        let shard_digest = hasher.finalize();
88
89        // Verify proof
90        self.proof
91            .verify_element_inclusion(&mut hasher, &shard_digest, self.index as u32, root)
92            .ok()?;
93
94        Some(CheckedChunk::new(
95            *root,
96            self.shard.clone(),
97            self.index,
98            shard_digest,
99        ))
100    }
101}
102
103/// A shard that has been checked against a commitment.
104///
105/// This stores the shard digest computed during [`Chunk::verify`] and the
106/// commitment root it was verified against. The root is checked at decode
107/// time to prevent cross-commitment shard mixing.
108#[derive(Clone, Debug, PartialEq, Eq)]
109pub struct CheckedChunk<D: Digest> {
110    root: D,
111    shard: Bytes,
112    index: u16,
113    digest: D,
114}
115
116impl<D: Digest> CheckedChunk<D> {
117    const fn new(root: D, shard: Bytes, index: u16, digest: D) -> Self {
118        Self {
119            root,
120            shard,
121            index,
122            digest,
123        }
124    }
125}
126
127impl<D: Digest> Write for Chunk<D> {
128    fn write(&self, writer: &mut impl BufMut) {
129        self.shard.write(writer);
130        self.index.write(writer);
131        self.proof.write(writer);
132    }
133
134    fn write_bufs(&self, buf: &mut impl BufsMut) {
135        self.shard.write_bufs(buf);
136        self.index.write(buf);
137        self.proof.write(buf);
138    }
139}
140
141impl<D: Digest> Read for Chunk<D> {
142    /// The maximum size of the shard.
143    type Cfg = crate::CodecConfig;
144
145    fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
146        let shard = Bytes::read_cfg(reader, &RangeCfg::new(..=cfg.maximum_shard_size))?;
147        let index = u16::read(reader)?;
148        let proof = bmt::Proof::<D>::read_cfg(reader, &1)?;
149        Ok(Self {
150            shard,
151            index,
152            proof,
153        })
154    }
155}
156
157impl<D: Digest> EncodeSize for Chunk<D> {
158    fn encode_size(&self) -> usize {
159        self.shard.encode_size() + self.index.encode_size() + self.proof.encode_size()
160    }
161
162    fn encode_inline_size(&self) -> usize {
163        self.shard.encode_inline_size() + self.index.encode_size() + self.proof.encode_size()
164    }
165}
166
167impl<D: Digest> PartialEq for Chunk<D> {
168    fn eq(&self, other: &Self) -> bool {
169        self.shard == other.shard && self.index == other.index && self.proof == other.proof
170    }
171}
172
173impl<D: Digest> Eq for Chunk<D> {}
174
175#[cfg(feature = "arbitrary")]
176impl<D: Digest> arbitrary::Arbitrary<'_> for Chunk<D>
177where
178    D: for<'a> arbitrary::Arbitrary<'a>,
179{
180    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
181        Ok(Self {
182            shard: u.arbitrary::<Vec<u8>>()?.into(),
183            index: u.arbitrary()?,
184            proof: u.arbitrary()?,
185        })
186    }
187}
188
189/// Prepare data for encoding.
190///
191/// Returns a contiguous buffer of `k` padded shards and the shard length.
192/// The buffer layout is `[length_prefix | data | zero_padding]` split into
193/// `k` equal-sized shards of `shard_len` bytes each.
194fn prepare_data(mut data: impl Buf, k: usize) -> (Vec<u8>, usize) {
195    // Compute shard length
196    let data_len = data.remaining();
197    let shard_len = canonical_shard_len(data_len, k);
198
199    // Prepare data
200    let length_bytes = (data_len as u32).to_be_bytes();
201    let mut padded = vec![0u8; k * shard_len];
202    padded[..u32::SIZE].copy_from_slice(&length_bytes);
203    data.copy_to_slice(&mut padded[u32::SIZE..u32::SIZE + data_len]);
204
205    (padded, shard_len)
206}
207
208/// Return the canonical shard width for a payload and shard count.
209///
210/// Encoding prefixes the payload with its length, splits the result across
211/// `k` original shards, and rounds up to an even width required by the
212/// Reed-Solomon implementation. Decode uses the same calculation to reject
213/// commitments that decode to the same payload with a non-canonical shard width.
214const fn canonical_shard_len(data_len: usize, k: usize) -> usize {
215    let prefixed_len = u32::SIZE + data_len;
216    let mut shard_len = prefixed_len.div_ceil(k);
217
218    // Ensure shard length is even, as required by the Reed-Solomon implementation.
219    if !shard_len.is_multiple_of(2) {
220        shard_len += 1;
221    }
222
223    shard_len
224}
225
226/// Extract data from encoded shards and verify that original shards use the canonical width.
227///
228/// The first `k` shards, when concatenated, form `[length_prefix | data | padding]`.
229/// This function copies only the data bytes while validating trailing zero
230/// padding directly from the shard slices.
231fn extract_data(shards: &[&[u8]], k: usize, expected_shard_len: usize) -> Result<Vec<u8>, Error> {
232    let shards = shards.get(..k).ok_or(Error::NotEnoughChunks)?;
233    let data_len = read_data_len(shards)?;
234    let mut data = Vec::with_capacity(data_len);
235    let mut prefix_bytes_left = u32::SIZE;
236    let mut data_bytes_left = data_len;
237    for shard in shards {
238        // The length prefix may straddle shard boundaries, so ignore bytes until
239        // we reach the first payload byte.
240        if prefix_bytes_left >= shard.len() {
241            prefix_bytes_left -= shard.len();
242            continue;
243        }
244
245        // Copy only the live payload bytes from this shard.
246        let payload = &shard[prefix_bytes_left..];
247        let copy_len = data_bytes_left.min(payload.len());
248        data.extend_from_slice(&payload[..copy_len]);
249        data_bytes_left -= copy_len;
250
251        // Any remaining bytes in this shard must be canonical zero padding.
252        if !payload[copy_len..].iter().all(|byte| *byte == 0) {
253            return Err(Error::Inconsistent);
254        }
255        prefix_bytes_left = 0;
256    }
257
258    // The prefix advertised more payload bytes than were present in the first
259    // `k` shards.
260    if data_bytes_left != 0 {
261        return Err(Error::Inconsistent);
262    }
263
264    // Validate that the original shards use the canonical shard width.
265    if canonical_shard_len(data.len(), k) != expected_shard_len {
266        return Err(Error::Inconsistent);
267    }
268    Ok(data)
269}
270
271/// Read the 4-byte big-endian length prefix from `shards` and validate that
272/// the decoded length fits in the post-prefix payload region.
273fn read_data_len(shards: &[&[u8]]) -> Result<usize, Error> {
274    let total_len: usize = shards.iter().map(|s| s.len()).sum();
275    if total_len < u32::SIZE {
276        return Err(Error::Inconsistent);
277    }
278
279    // Read the length prefix, which may span multiple shards.
280    let mut prefix = [0u8; u32::SIZE];
281    let mut prefix_len = 0usize;
282    for shard in shards {
283        if prefix_len == u32::SIZE {
284            break;
285        }
286        let read = (u32::SIZE - prefix_len).min(shard.len());
287        prefix[prefix_len..prefix_len + read].copy_from_slice(&shard[..read]);
288        prefix_len += read;
289    }
290
291    let data_len = u32::from_be_bytes(prefix) as usize;
292    let payload_len = total_len - u32::SIZE;
293    if data_len > payload_len {
294        return Err(Error::Inconsistent);
295    }
296    Ok(data_len)
297}
298
299/// Type alias for the internal encoding result.
300type Encoding<D> = (D, Vec<Chunk<D>>);
301
302/// Encode data using a Reed-Solomon coder and insert it into a [`bmt`].
303///
304/// # Parameters
305///
306/// - `total`: The total number of chunks to generate.
307/// - `min`: The minimum number of chunks required to decode the data.
308/// - `data`: The data to encode.
309/// - `strategy`: The parallelism strategy to use.
310///
311/// # Returns
312///
313/// - `root`: The root of the [`bmt`].
314/// - `chunks`: [`Chunk`]s of encoded data (that can be proven against `root`).
315fn encode<H: Hasher, S: Strategy>(
316    total: u16,
317    min: u16,
318    data: impl Buf,
319    strategy: &S,
320) -> Result<Encoding<H::Digest>, Error> {
321    // Validate parameters
322    assert!(total > min);
323    assert!(min > 0);
324    let n = total as usize;
325    let k = min as usize;
326    let m = n - k;
327    let data_len = data.remaining();
328    if data_len > u32::MAX as usize {
329        return Err(Error::InvalidDataLength(data_len));
330    }
331
332    // Prepare data as a contiguous buffer of k shards
333    let (padded, shard_len) = prepare_data(data, k);
334
335    // Compute recovery shards, striping large shard widths across the strategy
336    let manual = strategy.manual();
337    let recovery_buf = match striped::ranges(shard_len, manual.parallelism()) {
338        Some(ranges) => {
339            let original_shards = padded.chunks(shard_len).collect::<Vec<_>>();
340            let mut buf = vec![0u8; m * shard_len];
341            let groups = striped::stripe_columns(&mut buf, shard_len, &ranges);
342            let stripes: Vec<_> = ranges.into_iter().zip(groups).collect();
343            manual.try_map_collect_vec(stripes, |(range, out)| {
344                striped::encode_recovery_into(k, m, range, &original_shards, out)
345            })?;
346            buf
347        }
348        None => {
349            let mut encoder = Cached::take(
350                &CACHED_ENCODER,
351                || Encoder::new(k, m, shard_len),
352                |enc| enc.reset(k, m, shard_len),
353            )
354            .map_err(Error::ReedSolomon)?;
355            for shard in padded.chunks(shard_len) {
356                encoder
357                    .add_original_shard(shard)
358                    .map_err(Error::ReedSolomon)?;
359            }
360
361            // Compute recovery shards and collect into a contiguous buffer
362            let encoding = encoder.encode().map_err(Error::ReedSolomon)?;
363            let mut buf = Vec::with_capacity(m * shard_len);
364            for shard in encoding.recovery_iter() {
365                buf.extend_from_slice(shard);
366            }
367            buf
368        }
369    };
370
371    // Create zero-copy Bytes views into the original and recovery buffers
372    let originals: Bytes = padded.into();
373    let recoveries: Bytes = recovery_buf.into();
374
375    // Build Merkle tree
376    let mut builder = Builder::<H>::new(n);
377    let shard_slices: Vec<Bytes> = (0..k)
378        .map(|i| originals.slice(i * shard_len..(i + 1) * shard_len))
379        .chain((0..m).map(|i| recoveries.slice(i * shard_len..(i + 1) * shard_len)))
380        .collect();
381    let shard_hashes = strategy.map_init_collect_vec_with_multiplier(
382        &shard_slices,
383        shard_len,
384        H::new,
385        |hasher, shard| {
386            hasher.update(shard);
387            hasher.finalize()
388        },
389    );
390    for hash in &shard_hashes {
391        builder.add(hash);
392    }
393    let tree = builder.build();
394    let root = tree.root();
395
396    // Generate chunks with zero-copy shard views
397    let mut chunks = Vec::with_capacity(n);
398    for (i, shard) in shard_slices.into_iter().enumerate() {
399        let proof = tree.proof(i as u32).map_err(|_| Error::InvalidProof)?;
400        chunks.push(Chunk::new(shard, i as u16, proof));
401    }
402
403    Ok((root, chunks))
404}
405
406/// Inputs shared by the decode helpers.
407struct DecodeCtx<'a, H: Hasher, S: Strategy> {
408    /// Total number of shards (`k + m`).
409    n: usize,
410    /// Minimum shards required to decode (the number of original shards).
411    k: usize,
412    /// Number of recovery shards (`n - k`).
413    m: usize,
414    /// Width of every shard, in bytes.
415    shard_len: usize,
416    /// Commitment that the reconstructed codeword must reproduce.
417    root: &'a H::Digest,
418    /// Parallelism strategy.
419    strategy: &'a S,
420}
421
422/// Striped Reed-Solomon: split every shard by byte range and run independent
423/// Reed-Solomon operations over those ranges.
424///
425/// ```text
426///   originals:
427///     O0: [ stripe 0 ][ stripe 1 ][ tail ]
428///     O1: [ stripe 0 ][ stripe 1 ][ tail ]
429///     O(k-1): [ stripe 0 ][ stripe 1 ][ tail ]
430///
431///   encode stripe 0 -> R0[0], R1[0], more recoveries
432///   encode stripe 1 -> R0[1], R1[1], more recoveries
433///   encode tail     -> R0[t], R1[t], more recoveries
434///
435///   recovery Ri = concat(Ri[0], Ri[1], Ri[t])
436/// ```
437///
438/// Both decode paths reuse this per-stripe layout. With all originals present,
439/// [`decode`](striped::decode) re-encodes the recovery stripes and verifies them against the
440/// commitment. With an original missing, [`decode_reveal`](striped::decode_reveal)
441/// feeds exactly `k` shards and recovers the missing original and recovery stripes from a single
442/// Reed-Solomon decode (no re-encode).
443mod striped {
444    use super::*;
445
446    /// Split a shard-major buffer (`num_shards * shard_len`) into one group of mutable column
447    /// slices per stripe range: `groups[s][shard]` is bytes `ranges[s]` of shard `shard`. Using
448    /// `chunks_mut` + `split_at_mut` hands each parallel stripe task genuine, provably-disjoint
449    /// `&mut [u8]` slices, so the tasks fill the shared buffer without `unsafe`.
450    pub(super) fn stripe_columns<'a>(
451        buf: &'a mut [u8],
452        shard_len: usize,
453        ranges: &[Range<usize>],
454    ) -> Vec<Vec<&'a mut [u8]>> {
455        let mut groups: Vec<Vec<&'a mut [u8]>> = ranges.iter().map(|_| Vec::new()).collect();
456        for shard in buf.chunks_mut(shard_len) {
457            let mut rest = shard;
458            for (group, range) in groups.iter_mut().zip(ranges) {
459                let (head, tail) = rest.split_at_mut(range.len());
460                group.push(head);
461                rest = tail;
462            }
463        }
464        groups
465    }
466
467    /// The output column slices a recover-all stripe task writes into: one mutable slice per
468    /// missing original and per missing recovery (paired by position with [`Missing`]).
469    struct StripeOut<'a> {
470        originals: Vec<&'a mut [u8]>,
471        recoveries: Vec<&'a mut [u8]>,
472    }
473
474    /// The shard indices a stripe task must reconstruct (paired by position with [`StripeOut`]).
475    #[derive(Clone, Copy)]
476    struct Missing<'a> {
477        originals: &'a [usize],
478        recoveries: &'a [usize],
479    }
480
481    /// Split a shard of `shard_len` bytes into disjoint stripe ranges, or return `None`
482    /// when striping would not help (too little parallelism or data).
483    ///
484    /// Every non-final stripe ends on a [`SHARD_CHUNK_BYTES`] boundary: the engine lays
485    /// shards out in symbol blocks of that width (a partial final block is padded
486    /// internally), so a boundary in the middle of a block would change the bytes each
487    /// sub-instance encodes.
488    pub(super) fn ranges(shard_len: usize, parallelism: usize) -> Option<Vec<Range<usize>>> {
489        // Bound the stripe count by available parallelism, the number of MIN_STRIPE_BYTES
490        // chunks (so each stripe stays large enough to amortize encoder/decoder setup), and
491        // the number of `SHARD_CHUNK_BYTES` blocks (each non-final stripe needs at least one
492        // whole block). The block bound is implied by the MIN_STRIPE_BYTES bound today
493        // (MIN_STRIPE_BYTES is a multiple of SHARD_CHUNK_BYTES), but is kept to state the
494        // invariant explicitly.
495        let full_blocks = shard_len / SHARD_CHUNK_BYTES;
496        let stripe_count = parallelism
497            .min(shard_len / MIN_STRIPE_BYTES)
498            .min(full_blocks)
499            .max(1);
500        if stripe_count <= 1 {
501            return None;
502        }
503
504        let mut ranges = Vec::with_capacity(stripe_count);
505        let mut start = 0usize;
506        for stripe in 0..stripe_count {
507            let remaining_stripes = stripe_count - stripe;
508            let remaining = shard_len - start;
509            let len = if remaining_stripes == 1 {
510                remaining
511            } else {
512                let remaining_full_blocks = remaining / SHARD_CHUNK_BYTES;
513                (remaining_full_blocks / remaining_stripes).max(1) * SHARD_CHUNK_BYTES
514            };
515            let end = start + len;
516            ranges.push(start..end);
517            start = end;
518        }
519        Some(ranges)
520    }
521
522    /// Reconstruct a single stripe's missing shards from exactly `k` provided shards, reading
523    /// both missing originals and missing recoveries straight out of the decoder (the decode
524    /// reveals all positions), so no separate re-encode is needed. Writes each restored shard's
525    /// stripe into the matching `out.originals` / `out.recoveries` column slice.
526    fn recover_all_into(
527        k: usize,
528        m: usize,
529        range: Range<usize>,
530        provided_originals: &[(usize, &[u8])],
531        provided_recoveries: &[(usize, &[u8])],
532        missing: Missing<'_>,
533        mut out: StripeOut<'_>,
534    ) -> Result<(), Error> {
535        let shard_len = range.len();
536        let mut decoder = Cached::take(
537            &CACHED_DECODER,
538            || Decoder::new(k, m, shard_len),
539            |dec| dec.reset(k, m, shard_len),
540        )
541        .map_err(Error::ReedSolomon)?;
542
543        for (idx, shard) in provided_originals {
544            decoder
545                .add_original_shard(*idx, &shard[range.clone()])
546                .map_err(Error::ReedSolomon)?;
547        }
548        for (idx, shard) in provided_recoveries {
549            decoder
550                .add_recovery_shard(*idx, &shard[range.clone()])
551                .map_err(Error::ReedSolomon)?;
552        }
553        let decoding = decoder
554            .decode_with_recovery()
555            .map_err(Error::ReedSolomon)?
556            .expect("decode runs only when an original is missing");
557
558        for (slot, &idx) in out.originals.iter_mut().zip(missing.originals) {
559            let shard = decoding.original(idx).ok_or(Error::Inconsistent)?;
560            slot.copy_from_slice(shard);
561        }
562        for (slot, &idx) in out.recoveries.iter_mut().zip(missing.recoveries) {
563            let shard = decoding.recovery(idx).ok_or(Error::Inconsistent)?;
564            slot.copy_from_slice(shard);
565        }
566
567        Ok(())
568    }
569
570    /// Encode the recovery shards for a single stripe, writing each recovery shard's stripe
571    /// into the matching `out` column slice (one slot per recovery shard, in index order).
572    pub(super) fn encode_recovery_into(
573        k: usize,
574        m: usize,
575        range: Range<usize>,
576        originals: &[impl AsRef<[u8]>],
577        mut out: Vec<&mut [u8]>,
578    ) -> Result<(), Error> {
579        let shard_len = range.len();
580        let mut encoder = Cached::take(
581            &CACHED_ENCODER,
582            || Encoder::new(k, m, shard_len),
583            |enc| enc.reset(k, m, shard_len),
584        )
585        .map_err(Error::ReedSolomon)?;
586
587        for shard in originals.iter().take(k) {
588            let shard = shard.as_ref();
589            encoder
590                .add_original_shard(&shard[range.clone()])
591                .map_err(Error::ReedSolomon)?;
592        }
593        let encoding = encoder.encode().map_err(Error::ReedSolomon)?;
594        for (slot, shard) in out.iter_mut().zip(encoding.recovery_iter()) {
595            slot.copy_from_slice(shard);
596        }
597
598        Ok(())
599    }
600
601    /// Decode when all `k` originals are present: re-encode the recovery shards (recovery with
602    /// missing originals uses [`decode_reveal`]), confirm any provided recovery shards
603    /// match the canonical re-encode, and verify the rebuilt commitment against `ctx.root`.
604    pub(super) fn decode<'a, H: Hasher, S: Strategy>(
605        ctx: &DecodeCtx<'_, H, S>,
606        ranges: Vec<Range<usize>>,
607        shard_digests: Vec<Option<H::Digest>>,
608        provided_originals: Vec<(usize, &'a [u8])>,
609        provided_recoveries: Vec<(usize, &'a [u8])>,
610    ) -> Result<Vec<u8>, Error> {
611        let &DecodeCtx {
612            k,
613            m,
614            shard_len,
615            strategy,
616            ..
617        } = ctx;
618        assert!(ranges.len() > 1);
619
620        let mut original_refs: Vec<&[u8]> = vec![&[]; k];
621        for &(idx, shard) in &provided_originals {
622            original_refs[idx] = shard;
623        }
624
625        // Re-encode all recovery shards from the originals, one stripe per task.
626        let mut recovery_buf = vec![0u8; m * shard_len];
627        let groups = stripe_columns(&mut recovery_buf, shard_len, &ranges);
628        let stripes: Vec<_> = ranges.into_iter().zip(groups).collect();
629        strategy.try_map_collect_vec(stripes, |(range, out)| {
630            encode_recovery_into(k, m, range, &original_refs, out)
631        })?;
632        let recovery_refs: Vec<&[u8]> = recovery_buf.chunks_exact(shard_len).collect();
633        verify_reencoded::<H, S>(
634            ctx,
635            shard_digests,
636            &original_refs,
637            &recovery_refs,
638            &provided_recoveries,
639        )
640    }
641
642    /// Decode when an original is missing: recover the missing originals AND read the missing
643    /// recoveries straight out of one decode (the decode reveals every position), so no
644    /// separate re-encode is needed. The caller feeds exactly `k` shards (surplus recoveries
645    /// were trimmed and their digests cleared), so the trimmed positions are reconstructed
646    /// here and bound by the commitment root check like any other missing shard. No
647    /// provided-recovery comparison is needed: every reconstructed shard is the unique RS
648    /// output for the `k` inputs, and the root check alone binds it to the commitment.
649    pub(super) fn decode_reveal<'a, H: Hasher, S: Strategy>(
650        ctx: &DecodeCtx<'_, H, S>,
651        ranges: Vec<Range<usize>>,
652        shard_digests: Vec<Option<H::Digest>>,
653        provided_originals: Vec<(usize, &'a [u8])>,
654        provided_recoveries: Vec<(usize, &'a [u8])>,
655    ) -> Result<Vec<u8>, Error> {
656        let &DecodeCtx {
657            k,
658            m,
659            shard_len,
660            strategy,
661            ..
662        } = ctx;
663        assert!(ranges.len() > 1);
664
665        let missing_originals = shard_digests
666            .iter()
667            .take(k)
668            .enumerate()
669            .filter_map(|(i, digest)| digest.is_none().then_some(i))
670            .collect::<Vec<_>>();
671        let missing_recoveries = shard_digests
672            .iter()
673            .skip(k)
674            .enumerate()
675            .filter_map(|(i, digest)| digest.is_none().then_some(i))
676            .collect::<Vec<_>>();
677
678        let mut restored_originals = vec![0u8; missing_originals.len() * shard_len];
679        let mut restored_recoveries = vec![0u8; missing_recoveries.len() * shard_len];
680        let missing = Missing {
681            originals: &missing_originals,
682            recoveries: &missing_recoveries,
683        };
684        let original_groups = stripe_columns(&mut restored_originals, shard_len, &ranges);
685        let recovery_groups = stripe_columns(&mut restored_recoveries, shard_len, &ranges);
686        let stripes: Vec<_> = ranges
687            .into_iter()
688            .zip(original_groups.into_iter().zip(recovery_groups))
689            .map(|(range, (originals, recoveries))| {
690                (
691                    range,
692                    StripeOut {
693                        originals,
694                        recoveries,
695                    },
696                )
697            })
698            .collect();
699        strategy.try_map_collect_vec(stripes, |(range, out)| {
700            recover_all_into(
701                k,
702                m,
703                range,
704                &provided_originals,
705                &provided_recoveries,
706                missing,
707                out,
708            )
709        })?;
710
711        let mut original_refs: Vec<&[u8]> = vec![&[]; k];
712        for &(idx, shard) in &provided_originals {
713            original_refs[idx] = shard;
714        }
715        for (pos, idx) in missing_originals.iter().enumerate() {
716            let start = pos * shard_len;
717            original_refs[*idx] = &restored_originals[start..start + shard_len];
718        }
719        let mut recovery_refs: Vec<&[u8]> = vec![&[]; m];
720        for (pos, idx) in missing_recoveries.iter().enumerate() {
721            let start = pos * shard_len;
722            recovery_refs[*idx] = &restored_recoveries[start..start + shard_len];
723        }
724
725        verify_root::<H, S>(ctx, shard_digests, &original_refs, &recovery_refs)
726    }
727}
728
729/// Extract the data and verify the commitment from a fully reconstructed codeword: read the data
730/// from `originals`, hash every missing shard (original or recovery), rebuild the Merkle tree, and
731/// confirm its root matches the commitment. Shared by both strategies and both decode paths once the
732/// full codeword is available (by re-encode with all originals present, or by decode-reveal).
733fn verify_root<H: Hasher, S: Strategy>(
734    ctx: &DecodeCtx<'_, H, S>,
735    mut shard_digests: Vec<Option<H::Digest>>,
736    originals: &[&[u8]],
737    recoveries: &[&[u8]],
738) -> Result<Vec<u8>, Error> {
739    let &DecodeCtx {
740        n,
741        k,
742        shard_len,
743        root,
744        strategy,
745        ..
746    } = ctx;
747    let data = extract_data(originals, k, shard_len)?;
748
749    let missing_shards = shard_digests
750        .iter()
751        .enumerate()
752        .filter(|(_, digest)| digest.is_none())
753        .map(|(i, _)| {
754            (
755                i,
756                if i < k {
757                    originals[i]
758                } else {
759                    recoveries[i - k]
760                },
761            )
762        })
763        .collect::<Vec<_>>();
764
765    for (i, digest) in strategy.map_init_collect_vec_with_multiplier(
766        missing_shards,
767        shard_len,
768        H::new,
769        |hasher, (i, shard)| {
770            hasher.update(shard);
771            (i, hasher.finalize())
772        },
773    ) {
774        shard_digests[i] = Some(digest);
775    }
776
777    let mut builder = Builder::<H>::new(n);
778    shard_digests
779        .into_iter()
780        .map(|digest| digest.expect("digest must be present for every shard"))
781        .for_each(|digest| {
782            builder.add(&digest);
783        });
784    let tree = builder.build();
785    if tree.root() != *root {
786        return Err(Error::Inconsistent);
787    }
788
789    Ok(data)
790}
791
792/// All-originals verification: confirm any provided recovery shards match the canonical re-encode
793/// `recoveries` before their digests are trusted, then verify the commitment via [`verify_root`].
794/// This compare is what rejects a malicious encoder's non-canonical recovery shard when every
795/// original is present; with an original missing, the decode binds the recoveries instead.
796fn verify_reencoded<H: Hasher, S: Strategy>(
797    ctx: &DecodeCtx<'_, H, S>,
798    shard_digests: Vec<Option<H::Digest>>,
799    originals: &[&[u8]],
800    recoveries: &[&[u8]],
801    provided_recoveries: &[(usize, &[u8])],
802) -> Result<Vec<u8>, Error> {
803    // Provided originals are already bound to the commitment by their checked digests.
804    for &(idx, shard) in provided_recoveries {
805        if shard != recoveries[idx] {
806            return Err(Error::Inconsistent);
807        }
808    }
809    verify_root::<H, S>(ctx, shard_digests, originals, recoveries)
810}
811
812/// Sequential Reed-Solomon: reconstruct the codeword as a single Reed-Solomon instance (re-encode
813/// when all originals are present, decode-reveal when one is missing), used when striping would not
814/// help.
815mod sequential {
816    use super::*;
817
818    /// Decode the codeword as a single Reed-Solomon instance, reconstructing the
819    /// original data and verifying the rebuilt commitment against `ctx.root`.
820    pub(super) fn decode<'a, H: Hasher, S: Strategy>(
821        ctx: &DecodeCtx<'_, H, S>,
822        shard_digests: Vec<Option<H::Digest>>,
823        provided_originals: Vec<(usize, &'a [u8])>,
824        provided_recoveries: Vec<(usize, &'a [u8])>,
825    ) -> Result<Vec<u8>, Error> {
826        let &DecodeCtx {
827            k, m, shard_len, ..
828        } = ctx;
829        if provided_originals.len() == k {
830            // All originals are present, so skip the Reed-Solomon decode and re-encode the
831            // recovery shards to verify the rebuilt commitment.
832            let mut shards: Vec<&[u8]> = vec![&[]; k];
833            for &(idx, shard) in &provided_originals {
834                shards[idx] = shard;
835            }
836            return verify_codeword::<H, S>(ctx, shard_digests, &provided_recoveries, &shards);
837        }
838
839        // An original is missing: recover it and read the missing recovery shards straight out of
840        // one decode (decode-reveal), so no separate re-encode is needed. The caller feeds exactly
841        // `k` shards (surplus recoveries were trimmed and their digests cleared), so every
842        // reconstructed shard is the unique Reed-Solomon output for those `k` inputs and the root
843        // check binds it to the commitment, like in `striped::decode_reveal`.
844        let mut decoder = Cached::take(
845            &CACHED_DECODER,
846            || Decoder::new(k, m, shard_len),
847            |dec| dec.reset(k, m, shard_len),
848        )
849        .map_err(Error::ReedSolomon)?;
850        for (idx, shard) in &provided_originals {
851            decoder
852                .add_original_shard(*idx, shard)
853                .map_err(Error::ReedSolomon)?;
854        }
855        for (idx, shard) in &provided_recoveries {
856            decoder
857                .add_recovery_shard(*idx, shard)
858                .map_err(Error::ReedSolomon)?;
859        }
860        let decoding = decoder
861            .decode_with_recovery()
862            .map_err(Error::ReedSolomon)?
863            .expect("decode runs only when an original is missing");
864
865        let mut originals: Vec<&[u8]> = vec![&[]; k];
866        for &(idx, shard) in &provided_originals {
867            originals[idx] = shard;
868        }
869        for (idx, shard) in decoding.original_iter() {
870            originals[idx] = shard;
871        }
872        let mut recoveries: Vec<&[u8]> = vec![&[]; m];
873        for &(idx, shard) in &provided_recoveries {
874            recoveries[idx] = shard;
875        }
876        for (idx, shard) in decoding.recovery_iter() {
877            recoveries[idx] = shard;
878        }
879
880        verify_root::<H, S>(ctx, shard_digests, &originals, &recoveries)
881    }
882
883    /// Re-encode the recovery shards from the originals, then verify the commitment via
884    /// [`verify_reencoded`].
885    fn verify_codeword<H: Hasher, S: Strategy>(
886        ctx: &DecodeCtx<'_, H, S>,
887        shard_digests: Vec<Option<H::Digest>>,
888        provided_recoveries: &[(usize, &[u8])],
889        originals: &[&[u8]],
890    ) -> Result<Vec<u8>, Error> {
891        let &DecodeCtx {
892            k, m, shard_len, ..
893        } = ctx;
894        let mut encoder = Cached::take(
895            &CACHED_ENCODER,
896            || Encoder::new(k, m, shard_len),
897            |enc| enc.reset(k, m, shard_len),
898        )
899        .map_err(Error::ReedSolomon)?;
900        for shard in originals.iter().take(k) {
901            encoder
902                .add_original_shard(shard)
903                .map_err(Error::ReedSolomon)?;
904        }
905        let encoding = encoder.encode().map_err(Error::ReedSolomon)?;
906        let recovery_refs: Vec<&[u8]> = (0..m)
907            .map(|i| {
908                encoding
909                    .recovery(i)
910                    .expect("recovery index must be in range")
911            })
912            .collect();
913
914        verify_reencoded::<H, S>(
915            ctx,
916            shard_digests,
917            originals,
918            &recovery_refs,
919            provided_recoveries,
920        )
921    }
922}
923
924/// Decode data from a set of [`CheckedChunk`]s.
925///
926/// It is assumed that all chunks have already been verified against the given root using [`Chunk::verify`].
927///
928/// # Parameters
929///
930/// - `total`: The total number of chunks to generate.
931/// - `min`: The minimum number of chunks required to decode the data.
932/// - `root`: The root of the [`bmt`].
933/// - `chunks`: [`CheckedChunk`]s of encoded data (that can be proven against `root`)
934///
935/// # Returns
936///
937/// - `data`: The decoded data.
938fn decode<'a, H: Hasher, S: Strategy>(
939    total: u16,
940    min: u16,
941    root: &H::Digest,
942    chunks: impl Iterator<Item = &'a CheckedChunk<H::Digest>>,
943    strategy: &S,
944) -> Result<Vec<u8>, Error> {
945    // Validate parameters
946    assert!(total > min);
947    assert!(min > 0);
948    let n = total as usize;
949    let k = min as usize;
950    let m = n - k;
951    let mut chunks = chunks.peekable();
952    let Some(first) = chunks.peek() else {
953        return Err(Error::NotEnoughChunks);
954    };
955
956    // Process checked chunks
957    let shard_len = first.shard.len();
958    let manual = strategy.manual();
959    let stripes = striped::ranges(shard_len, manual.parallelism());
960    let mut shard_digests: Vec<Option<H::Digest>> = vec![None; n];
961    let mut provided_originals: Vec<(usize, &[u8])> = Vec::new();
962    let mut provided_recoveries: Vec<(usize, &[u8])> = Vec::new();
963    let mut provided = 0usize;
964    for chunk in chunks {
965        provided += 1;
966        if &chunk.root != root {
967            return Err(Error::CommitmentMismatch);
968        }
969
970        // Every shard must share the first shard's width. The striped decode path slices
971        // each shard by stripe range, so a wrong-width shard would otherwise panic.
972        if chunk.shard.len() != shard_len {
973            return Err(Error::Inconsistent);
974        }
975
976        // Check for duplicate index
977        let index = chunk.index;
978        if index >= total {
979            return Err(Error::InvalidIndex(index));
980        }
981        let digest_slot = &mut shard_digests[index as usize];
982        if digest_slot.is_some() {
983            return Err(Error::DuplicateIndex(index));
984        }
985
986        // Retain the checked digest and split provided bytes by shard type
987        *digest_slot = Some(chunk.digest);
988        if index < min {
989            provided_originals.push((index as usize, chunk.shard.as_ref()));
990        } else {
991            provided_recoveries.push((index as usize - k, chunk.shard.as_ref()));
992        }
993    }
994    if provided < k {
995        return Err(Error::NotEnoughChunks);
996    }
997
998    // Feed the Reed-Solomon decoder exactly `k` shards. Only `k` original positions exist, so
999    // originals are never redundant; when an original is missing, trim surplus recovery shards
1000    // beyond the `k - originals` needed to decode. Their checked digests are dropped and their
1001    // bytes forgotten, so the reconstruction rebuilds those positions and the commitment root
1002    // check binds them. This verifies extra shards without over-feeding the decoder or a
1003    // separate canonical re-encode/compare.
1004    let recovery_needed = provided_originals.len() < k;
1005    if recovery_needed {
1006        let keep = k - provided_originals.len();
1007        for &(idx, _) in &provided_recoveries[keep..] {
1008            shard_digests[k + idx] = None;
1009        }
1010        provided_recoveries.truncate(keep);
1011    }
1012
1013    // Decode the data, striping the work across the strategy when shards are large enough
1014    if let Some(ranges) = stripes {
1015        let ctx = DecodeCtx {
1016            n,
1017            k,
1018            m,
1019            shard_len,
1020            root,
1021            strategy: &manual,
1022        };
1023        // Recovery reads the missing originals and recoveries straight out of one decode;
1024        // with all originals present there is nothing to decode, so re-encode instead.
1025        if recovery_needed {
1026            return striped::decode_reveal::<H, _>(
1027                &ctx,
1028                ranges,
1029                shard_digests,
1030                provided_originals,
1031                provided_recoveries,
1032            );
1033        }
1034        return striped::decode::<H, _>(
1035            &ctx,
1036            ranges,
1037            shard_digests,
1038            provided_originals,
1039            provided_recoveries,
1040        );
1041    }
1042    let ctx = DecodeCtx {
1043        n,
1044        k,
1045        m,
1046        shard_len,
1047        root,
1048        strategy,
1049    };
1050    sequential::decode::<H, S>(&ctx, shard_digests, provided_originals, provided_recoveries)
1051}
1052
1053/// A SIMD-optimized Reed-Solomon coder that emits chunks that can be proven against a [`bmt`].
1054///
1055/// # Behavior
1056///
1057/// The encoder takes input data, splits it into `k` data shards, and generates `m` recovery
1058/// shards using [Reed-Solomon encoding](https://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction).
1059/// All `n = k + m` shards are then used to build a [`bmt`], producing a single root hash. Each shard
1060/// is packaged as a chunk containing the shard data, its index, and a Merkle multi-proof against the [`bmt`] root.
1061///
1062/// ## Encoding
1063///
1064/// ```text
1065///               +--------------------------------------+
1066///               |         Original Data (Bytes)        |
1067///               +--------------------------------------+
1068///                                  |
1069///                                  v
1070///               +--------------------------------------+
1071///               | [Length Prefix | Original Data...]   |
1072///               +--------------------------------------+
1073///                                  |
1074///                                  v
1075///              +----------+ +----------+    +-----------+
1076///              |  Shard 0 | |  Shard 1 | .. | Shard k-1 |  (Data Shards)
1077///              +----------+ +----------+    +-----------+
1078///                     |            |             |
1079///                     |            |             |
1080///                     +------------+-------------+
1081///                                  |
1082///                                  v
1083///                        +------------------+
1084///                        | Reed-Solomon     |
1085///                        | Encoder (k, m)   |
1086///                        +------------------+
1087///                                  |
1088///                                  v
1089///              +----------+ +----------+    +-----------+
1090///              |  Shard k | | Shard k+1| .. | Shard n-1 |  (Recovery Shards)
1091///              +----------+ +----------+    +-----------+
1092/// ```
1093///
1094/// ## Merkle Tree Construction
1095///
1096/// All `n` shards (data and recovery) are hashed and used as leaves to build a [`bmt`].
1097///
1098/// ```text
1099/// Shards:    [Shard 0, Shard 1, ..., Shard n-1]
1100///             |        |              |
1101///             v        v              v
1102/// Hashes:    [H(S_0), H(S_1), ..., H(S_n-1)]
1103///             \       / \       /
1104///              \     /   \     /
1105///               +---+     +---+
1106///                 |         |
1107///                 \         /
1108///                  \       /
1109///                   +-----+
1110///                      |
1111///                      v
1112///                +----------+
1113///                |   Root   |
1114///                +----------+
1115/// ```
1116///
1117/// The final output is the [`bmt`] root and a set of `n` chunks.
1118///
1119/// `(Root, [Chunk 0, Chunk 1, ..., Chunk n-1])`
1120///
1121/// Each chunk contains:
1122/// - `shard`: The shard data (original or recovery).
1123/// - `index`: The shard's original index (0 to n-1).
1124/// - `proof`: A Merkle multi-proof of the shard's inclusion in the [`bmt`].
1125///
1126/// ## Decoding and Verification
1127///
1128/// The decoder requires any `k` chunks to reconstruct the original data.
1129/// 1. Each chunk's Merkle multi-proof is verified against the [`bmt`] root.
1130/// 2. Exactly `k` shards are fed to the Reed-Solomon decoder (any surplus chunks are
1131///    redundant), which reconstructs the full codeword: the missing data shards and the
1132///    missing recovery shards both come out of the same decode. When all `k` data shards are
1133///    already present there is nothing to decode, so the recovery shards are re-encoded
1134///    instead.
1135/// 3. To ensure consistency, a new [`bmt`] root is generated over the full reconstructed
1136///    codeword. This new root MUST match the original [`bmt`] root. This prevents attacks
1137///    where an adversary provides a valid set of chunks that decode to different data, and
1138///    binds any surplus chunks (which are rebuilt from the reconstruction, not trusted).
1139/// 4. If the roots match, the original data is extracted from the reconstructed data shards.
1140#[derive(Clone, Copy)]
1141pub struct ReedSolomon<H> {
1142    _marker: PhantomData<H>,
1143}
1144
1145impl<H> std::fmt::Debug for ReedSolomon<H> {
1146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1147        f.debug_struct("ReedSolomon").finish()
1148    }
1149}
1150
1151impl<H: Hasher> Scheme for ReedSolomon<H> {
1152    type Commitment = H::Digest;
1153    type Shard = Chunk<H::Digest>;
1154    type CheckedShard = CheckedChunk<H::Digest>;
1155    type Error = Error;
1156
1157    fn encode(
1158        config: &Config,
1159        data: impl Buf,
1160        strategy: &impl Strategy,
1161    ) -> Result<(Self::Commitment, Vec<Self::Shard>), Self::Error> {
1162        encode::<H, _>(
1163            total_shards(config)?,
1164            config.minimum_shards.get(),
1165            data,
1166            strategy,
1167        )
1168    }
1169
1170    fn check(
1171        config: &Config,
1172        commitment: &Self::Commitment,
1173        index: u16,
1174        shard: &Self::Shard,
1175    ) -> Result<Self::CheckedShard, Self::Error> {
1176        let total = total_shards(config)?;
1177        if index >= total {
1178            return Err(Error::InvalidIndex(index));
1179        }
1180        if shard.proof.leaf_count != u32::from(total) {
1181            return Err(Error::InvalidProof);
1182        }
1183        if shard.index != index {
1184            return Err(Error::InvalidIndex(shard.index));
1185        }
1186        shard
1187            .verify::<H>(shard.index, commitment)
1188            .ok_or(Error::InvalidProof)
1189    }
1190
1191    fn decode<'a>(
1192        config: &Config,
1193        commitment: &Self::Commitment,
1194        shards: impl Iterator<Item = &'a Self::CheckedShard>,
1195        strategy: &impl Strategy,
1196    ) -> Result<Vec<u8>, Self::Error> {
1197        decode::<H, _>(
1198            total_shards(config)?,
1199            config.minimum_shards.get(),
1200            commitment,
1201            shards,
1202            strategy,
1203        )
1204    }
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209    use super::*;
1210    use commonware_codec::Encode;
1211    use commonware_cryptography::Sha256;
1212    use commonware_invariants::minifuzz;
1213    use commonware_parallel::{Rayon, Sequential};
1214    use commonware_runtime::{deterministic, iobuf::EncodeExt, BufferPooler, Runner};
1215    use commonware_utils::{NZUsize, NZU16};
1216
1217    type RS = ReedSolomon<Sha256>;
1218    const STRATEGY: Sequential = Sequential;
1219    const FUZZ_MAX_MIN_SHARDS: u16 = 8;
1220    const FUZZ_MAX_EXTRA_SHARDS: u16 = 8;
1221    const FUZZ_MAX_DATA_LEN: usize = 256;
1222    const FUZZ_MAX_EXTRA_SHARD_WIDTH: usize = 16;
1223
1224    fn checked(
1225        root: <Sha256 as Hasher>::Digest,
1226        chunk: Chunk<<Sha256 as Hasher>::Digest>,
1227    ) -> CheckedChunk<<Sha256 as Hasher>::Digest> {
1228        let Chunk { shard, index, .. } = chunk;
1229        let digest = Sha256::hash(&shard);
1230        CheckedChunk::new(root, shard, index, digest)
1231    }
1232
1233    fn build_chunks(
1234        shards: &[Vec<u8>],
1235    ) -> (
1236        <Sha256 as Hasher>::Digest,
1237        Vec<Chunk<<Sha256 as Hasher>::Digest>>,
1238    ) {
1239        let mut builder = Builder::<Sha256>::new(shards.len());
1240        for shard in shards {
1241            let mut hasher = Sha256::new();
1242            hasher.update(shard);
1243            builder.add(&hasher.finalize());
1244        }
1245        let tree = builder.build();
1246        let root = tree.root();
1247        let chunks = shards
1248            .iter()
1249            .enumerate()
1250            .map(|(i, shard)| {
1251                let proof = tree.proof(i as u32).unwrap();
1252                Chunk::new(shard.clone().into(), i as u16, proof)
1253            })
1254            .collect();
1255
1256        (root, chunks)
1257    }
1258
1259    fn selected_indices(
1260        u: &mut arbitrary::Unstructured<'_>,
1261        total: u16,
1262        minimum: u16,
1263    ) -> arbitrary::Result<Vec<u16>> {
1264        let to_use = u.int_in_range(minimum..=total)?;
1265        let mut selected = (0..total).collect::<Vec<_>>();
1266        for i in 0..usize::from(to_use) {
1267            let remaining = usize::from(total) - i;
1268            let j = i + u.choose_index(remaining)?;
1269            selected.swap(i, j);
1270        }
1271        selected.truncate(usize::from(to_use));
1272        Ok(selected)
1273    }
1274
1275    fn assert_decode_unique_commitment(
1276        total: u16,
1277        min: u16,
1278        root: <Sha256 as Hasher>::Digest,
1279        chunks: &[Chunk<<Sha256 as Hasher>::Digest>],
1280        selected: &[u16],
1281    ) {
1282        let pieces = selected
1283            .iter()
1284            .map(|&i| chunks[usize::from(i)].verify::<Sha256>(i, &root).unwrap())
1285            .collect::<Vec<_>>();
1286
1287        let Ok(decoded) = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY) else {
1288            return;
1289        };
1290        let (canonical_root, _) =
1291            encode::<Sha256, _>(total, min, decoded.as_slice(), &STRATEGY).unwrap();
1292        assert_eq!(
1293            root, canonical_root,
1294            "decode accepted a root not produced by canonical encode"
1295        );
1296    }
1297
1298    fn fuzz_arbitrary_codeword(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()> {
1299        let min = u.int_in_range(1..=FUZZ_MAX_MIN_SHARDS)?;
1300        let extra = u.int_in_range(1..=FUZZ_MAX_EXTRA_SHARDS)?;
1301        let total = min + extra;
1302        let k = usize::from(min);
1303        let m = usize::from(extra);
1304
1305        let data_len = u.int_in_range(0..=FUZZ_MAX_DATA_LEN)?;
1306        let data = u.bytes(data_len)?.to_vec();
1307        let canonical = canonical_shard_len(data.len(), k);
1308        let extra_width = u.int_in_range(0..=FUZZ_MAX_EXTRA_SHARD_WIDTH / 2)? * 2;
1309        let shard_len = canonical + extra_width;
1310
1311        let mut padded = vec![0u8; k * shard_len];
1312        padded[..u32::SIZE].copy_from_slice(&(data.len() as u32).to_be_bytes());
1313        padded[u32::SIZE..u32::SIZE + data.len()].copy_from_slice(&data);
1314
1315        let payload_end = u32::SIZE + data.len();
1316        if payload_end < padded.len() && u.int_in_range(0..=3)? == 0 {
1317            let offset = payload_end + u.choose_index(padded.len() - payload_end)?;
1318            padded[offset] ^= u.arbitrary::<u8>()? | 1;
1319        }
1320
1321        let mut encoder = Encoder::new(k, m, shard_len).unwrap();
1322        for shard in padded.chunks(shard_len) {
1323            encoder.add_original_shard(shard).unwrap();
1324        }
1325        let recovery = encoder.encode().unwrap();
1326
1327        let mut shards = padded
1328            .chunks(shard_len)
1329            .map(|shard| shard.to_vec())
1330            .collect::<Vec<_>>();
1331        shards.extend(recovery.recovery_iter().map(|shard| shard.to_vec()));
1332
1333        let (root, chunks) = build_chunks(&shards);
1334        let selected = selected_indices(u, total, min)?;
1335        assert_decode_unique_commitment(total, min, root, &chunks, &selected);
1336
1337        Ok(())
1338    }
1339
1340    fn fuzz_mixed_codeword(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()> {
1341        let min = u.int_in_range(1..=FUZZ_MAX_MIN_SHARDS)?;
1342        let extra = u.int_in_range(1..=FUZZ_MAX_EXTRA_SHARDS)?;
1343        let total = min + extra;
1344
1345        let data_len = u.int_in_range(0..=FUZZ_MAX_DATA_LEN)?;
1346        let data = u.bytes(data_len)?.to_vec();
1347        let (_canonical_root, chunks) =
1348            encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1349        let mut shards = chunks
1350            .iter()
1351            .map(|chunk| chunk.shard.to_vec())
1352            .collect::<Vec<_>>();
1353
1354        let mutated = usize::from(min + u.int_in_range(0..=extra - 1)?);
1355        let offset = u.choose_index(shards[mutated].len())?;
1356        shards[mutated][offset] ^= u.arbitrary::<u8>()? | 1;
1357
1358        let (root, chunks) = build_chunks(&shards);
1359        let mut selected = (0..min).collect::<Vec<_>>();
1360        selected.push(mutated as u16);
1361        assert_decode_unique_commitment(total, min, root, &chunks, &selected);
1362
1363        Ok(())
1364    }
1365
1366    #[test]
1367    fn test_recovery() {
1368        let data = b"Testing recovery pieces";
1369        let total = 8u16;
1370        let min = 3u16;
1371
1372        // Encode the data
1373        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1374
1375        // Use a mix of original and recovery pieces
1376        let pieces: Vec<_> = vec![
1377            checked(root, chunks[0].clone()), // original
1378            checked(root, chunks[4].clone()), // recovery
1379            checked(root, chunks[6].clone()), // recovery
1380        ];
1381
1382        // Try to decode with a mix of original and recovery pieces
1383        let decoded = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY).unwrap();
1384        assert_eq!(decoded, data);
1385    }
1386
1387    #[test]
1388    fn test_not_enough_pieces() {
1389        let data = b"Test insufficient pieces";
1390        let total = 6u16;
1391        let min = 4u16;
1392
1393        // Encode data
1394        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1395
1396        // Try with fewer than min
1397        let pieces: Vec<_> = chunks
1398            .into_iter()
1399            .take(2)
1400            .map(|c| checked(root, c))
1401            .collect();
1402
1403        // Fail to decode
1404        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1405        assert!(matches!(result, Err(Error::NotEnoughChunks)));
1406    }
1407
1408    #[test]
1409    fn test_duplicate_index() {
1410        let data = b"Test duplicate detection";
1411        let total = 5u16;
1412        let min = 3u16;
1413
1414        // Encode data
1415        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1416
1417        // Include duplicate index by cloning the first chunk
1418        let pieces = [
1419            checked(root, chunks[0].clone()),
1420            checked(root, chunks[0].clone()),
1421            checked(root, chunks[1].clone()),
1422        ];
1423
1424        // Fail to decode
1425        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1426        assert!(matches!(result, Err(Error::DuplicateIndex(0))));
1427    }
1428
1429    #[test]
1430    fn test_invalid_index() {
1431        let data = b"Test invalid index";
1432        let total = 5u16;
1433        let min = 3u16;
1434
1435        // Encode data
1436        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1437
1438        // Verify all proofs at invalid index
1439        for i in 0..total {
1440            assert!(chunks[i as usize].verify::<Sha256>(i + 1, &root).is_none());
1441        }
1442    }
1443
1444    #[test]
1445    #[should_panic(expected = "assertion failed: total > min")]
1446    fn test_invalid_total() {
1447        let data = b"Test parameter validation";
1448
1449        // The total shard count must exceed the recovery threshold.
1450        encode::<Sha256, _>(3, 3, data.as_slice(), &STRATEGY).unwrap();
1451    }
1452
1453    #[test]
1454    #[should_panic(expected = "assertion failed: min > 0")]
1455    fn test_invalid_min() {
1456        let data = b"Test parameter validation";
1457
1458        // The recovery threshold must be non-zero.
1459        encode::<Sha256, _>(5, 0, data.as_slice(), &STRATEGY).unwrap();
1460    }
1461
1462    #[test]
1463    fn test_empty_data() {
1464        let data = b"";
1465        let total = 100u16;
1466        let min = 30u16;
1467
1468        // Encode data
1469        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1470
1471        // Try to decode with min
1472        let minimal = chunks
1473            .into_iter()
1474            .take(min as usize)
1475            .map(|c| checked(root, c))
1476            .collect::<Vec<_>>();
1477        let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
1478        assert_eq!(decoded, data);
1479    }
1480
1481    #[test]
1482    fn test_large_data() {
1483        let data = vec![42u8; 1000]; // 1KB of data
1484        let total = 7u16;
1485        let min = 4u16;
1486
1487        // Encode data
1488        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1489
1490        // Try to decode with min
1491        let minimal = chunks
1492            .into_iter()
1493            .take(min as usize)
1494            .map(|c| checked(root, c))
1495            .collect::<Vec<_>>();
1496        let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
1497        assert_eq!(decoded, data);
1498    }
1499
1500    #[test]
1501    fn test_parallel_encode_matches_sequential() {
1502        let strategy = Rayon::new(NZUsize!(4)).unwrap();
1503        let data = vec![42u8; 256 * 1024];
1504        let total = 24u16;
1505        let min = 8u16;
1506
1507        let (sequential_root, sequential_chunks) =
1508            encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1509        let (parallel_root, parallel_chunks) =
1510            encode::<Sha256, _>(total, min, data.as_slice(), &strategy).unwrap();
1511
1512        assert_eq!(sequential_root, parallel_root);
1513        assert_eq!(sequential_chunks, parallel_chunks);
1514    }
1515
1516    #[test]
1517    fn test_parallel_recovery_decode() {
1518        let strategy = Rayon::new(NZUsize!(4)).unwrap();
1519        let data = vec![42u8; 256 * 1024];
1520        let total = 24u16;
1521        let min = 8u16;
1522
1523        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &strategy).unwrap();
1524
1525        let minimal = chunks
1526            .into_iter()
1527            .skip(min as usize)
1528            .take(min as usize)
1529            .map(|c| checked(root, c))
1530            .collect::<Vec<_>>();
1531        let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &strategy).unwrap();
1532        assert_eq!(decoded, data);
1533    }
1534
1535    /// Striped recovery decode must be byte-identical to the sequential (full-shard)
1536    /// path. The striped path only activates for shards of at least
1537    /// `MIN_STRIPE_BYTES`, so this sweeps payload sizes and shard counts that land on
1538    /// several stripe-count boundaries under a parallel `Strategy`, decoding from a
1539    /// recovery-only set (which forces Reed-Solomon recovery) and checking the result
1540    /// against the original data on both the sequential and parallel paths.
1541    #[test]
1542    fn test_striped_recovery_matches_sequential() {
1543        for &data_len in &[128 * 1024usize, 257 * 1024, 512 * 1024, 1024 * 1024] {
1544            for &(total, min) in &[(12u16, 4u16), (24, 8), (33, 11)] {
1545                let data: Vec<u8> = (0..data_len)
1546                    .map(|i| (i as u8) ^ ((i >> 7) as u8))
1547                    .collect();
1548                let (root, chunks) =
1549                    encode::<Sha256, _>(total, min, data.as_slice(), &Sequential).unwrap();
1550                let recovery_only = chunks
1551                    .into_iter()
1552                    .skip(min as usize)
1553                    .take(min as usize)
1554                    .map(|c| checked(root, c))
1555                    .collect::<Vec<_>>();
1556                let sequential =
1557                    decode::<Sha256, _>(total, min, &root, recovery_only.iter(), &Sequential)
1558                        .unwrap();
1559                assert_eq!(sequential, data);
1560                for &parallelism in &[2usize, 8] {
1561                    let strategy = Rayon::new(NZUsize!(parallelism)).unwrap();
1562                    let striped =
1563                        decode::<Sha256, _>(total, min, &root, recovery_only.iter(), &strategy)
1564                            .unwrap();
1565                    assert_eq!(
1566                        striped, data,
1567                        "striped decode mismatch (len={data_len} total={total} min={min} parallelism={parallelism})"
1568                    );
1569                }
1570            }
1571        }
1572    }
1573
1574    /// All `k` originals provided under a parallel strategy must round-trip via the striped
1575    /// re-encode-and-verify path (`striped::decode`), which other tests only reach in the rejection
1576    /// direction.
1577    #[test]
1578    fn test_striped_all_originals_decode() {
1579        let strategy = Rayon::new(NZUsize!(4)).unwrap();
1580        let data = vec![42u8; 256 * 1024];
1581        let total = 24u16;
1582        let min = 8u16;
1583
1584        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &strategy).unwrap();
1585
1586        // Assert the striped path actually engages (>= 2 stripes); otherwise this would silently
1587        // exercise the sequential path.
1588        let shard_len = canonical_shard_len(data.len(), min as usize);
1589        assert!(
1590            striped::ranges(shard_len, strategy.manual().parallelism()).map_or(0, |r| r.len()) >= 2,
1591            "test must exercise >= 2 stripes (shard_len={shard_len})"
1592        );
1593
1594        // Provide exactly the `k` original chunks (indices 0..min), so decode takes the all-originals
1595        // striped re-encode path rather than reconstructing.
1596        let originals = chunks
1597            .into_iter()
1598            .take(min as usize)
1599            .map(|c| checked(root, c))
1600            .collect::<Vec<_>>();
1601        let decoded = decode::<Sha256, _>(total, min, &root, originals.iter(), &strategy).unwrap();
1602        assert_eq!(decoded, data);
1603    }
1604
1605    /// Splitting a shard into stripes and encoding each must reproduce the single full-width
1606    /// encode byte-for-byte.
1607    #[test]
1608    fn test_striped_encode_into_matches_full_width() {
1609        let k = 2usize;
1610        let m = 2usize;
1611        let shard_len = 2 * MIN_STRIPE_BYTES;
1612        let ranges = striped::ranges(shard_len, 4).expect("must split into stripes");
1613        assert!(ranges.len() >= 2);
1614
1615        let mut originals_buf = vec![0u8; k * shard_len];
1616        for (i, byte) in originals_buf.iter_mut().enumerate() {
1617            *byte = (i % 251) as u8;
1618        }
1619        let originals: Vec<&[u8]> = originals_buf.chunks(shard_len).collect();
1620
1621        // Encode each stripe into its column slices of the shared buffer.
1622        let mut striped_recovery = vec![0u8; m * shard_len];
1623        let groups = striped::stripe_columns(&mut striped_recovery, shard_len, &ranges);
1624        for (range, out) in ranges.iter().cloned().zip(groups) {
1625            striped::encode_recovery_into(k, m, range, &originals, out).unwrap();
1626        }
1627
1628        // A single full-width encode must produce the identical recovery buffer.
1629        let mut full_recovery = vec![0u8; m * shard_len];
1630        let full_out: Vec<&mut [u8]> = full_recovery.chunks_mut(shard_len).collect();
1631        striped::encode_recovery_into(k, m, 0..shard_len, &originals, full_out).unwrap();
1632
1633        assert_eq!(striped_recovery, full_recovery);
1634    }
1635
1636    // Each tamper mutates a canonical codeword in place before a (malicious) commitment is
1637    // rebuilt over the tampered shards. `k` is the original-shard count, `shard_len` the
1638    // per-shard width.
1639    fn tamper_flip_recovery(shards: &mut [Vec<u8>], k: usize, _shard_len: usize) {
1640        shards[k][0] ^= 0xFF;
1641    }
1642
1643    fn tamper_flip_original_data(shards: &mut [Vec<u8>], _k: usize, shard_len: usize) {
1644        // First payload byte sits right after the 4-byte length prefix.
1645        let offset = u32::SIZE;
1646        shards[offset / shard_len][offset % shard_len] ^= 0xFF;
1647    }
1648
1649    fn tamper_corrupt_padding(shards: &mut [Vec<u8>], k: usize, shard_len: usize) {
1650        // Final byte of the last original shard is canonical zero padding for the data
1651        // sizes used by the adversarial tests below.
1652        shards[k - 1][shard_len - 1] = 0xAA;
1653    }
1654
1655    /// Encode `data` canonically, apply `tamper`, rebuild a (malicious) commitment over the
1656    /// tampered shards, and decode the `selected` shard indices with `strategy`. Generic over
1657    /// [`Strategy`] so the same attack drives both the sequential and striped decode paths.
1658    fn decode_tampered_codeword<S: Strategy>(
1659        total: u16,
1660        min: u16,
1661        data: &[u8],
1662        selected: &[u16],
1663        tamper: fn(&mut [Vec<u8>], usize, usize),
1664        strategy: &S,
1665    ) -> Result<Vec<u8>, Error> {
1666        let (_root, chunks) = encode::<Sha256, _>(total, min, data, &Sequential).unwrap();
1667        let mut shards = chunks.iter().map(|c| c.shard.to_vec()).collect::<Vec<_>>();
1668        let shard_len = shards[0].len();
1669        tamper(&mut shards, min as usize, shard_len);
1670        let (root, chunks) = build_chunks(&shards);
1671        let pieces = selected
1672            .iter()
1673            .map(|&i| checked(root, chunks[i as usize].clone()))
1674            .collect::<Vec<_>>();
1675        decode::<Sha256, _>(total, min, &root, pieces.iter(), strategy)
1676    }
1677
1678    // (name, selected shard indices, tamper). With total=12/min=4, indices 0..4 are
1679    // originals and 4..12 are recoveries.
1680    #[allow(clippy::type_complexity)]
1681    const ADVERSARIAL_SCENARIOS: &[(&str, &[u16], fn(&mut [Vec<u8>], usize, usize))] = &[
1682        // Tampered recovery that is NOT provided: caught when the root is rebuilt from the
1683        // re-encoded recoveries.
1684        (
1685            "tampered_recovery_unprovided",
1686            &[0, 1, 2, 3],
1687            tamper_flip_recovery,
1688        ),
1689        // Tampered recovery that IS provided and forces RS reconstruction of a missing
1690        // original (exercises the striped `decode_reveal` path).
1691        (
1692            "tampered_recovery_provided",
1693            &[0, 1, 2, 4],
1694            tamper_flip_recovery,
1695        ),
1696        // Non-canonical (non-zero) trailing padding in an original shard (extract_data).
1697        (
1698            "non_canonical_padding",
1699            &[0, 1, 2, 3],
1700            tamper_corrupt_padding,
1701        ),
1702        // Tampered original payload byte: detected by the rebuilt commitment.
1703        (
1704            "tampered_original_data",
1705            &[0, 1, 2, 3],
1706            tamper_flip_original_data,
1707        ),
1708        // Recovery-only decode where one provided recovery is tampered.
1709        (
1710            "recovery_only_tampered",
1711            &[4, 5, 6, 7],
1712            tamper_flip_recovery,
1713        ),
1714    ];
1715
1716    /// Every adversarial scenario must be rejected as [`Error::Inconsistent`] on whichever
1717    /// decode path `strategy` + `data` selects.
1718    fn assert_adversarial_rejected<S: Strategy>(total: u16, min: u16, data: &[u8], strategy: &S) {
1719        for &(name, selected, tamper) in ADVERSARIAL_SCENARIOS {
1720            let result = decode_tampered_codeword(total, min, data, selected, tamper, strategy);
1721            assert!(
1722                matches!(result, Err(Error::Inconsistent)),
1723                "scenario {name} not rejected as Inconsistent: {result:?}"
1724            );
1725        }
1726    }
1727
1728    #[test]
1729    fn test_adversarial_rejection_sequential_small() {
1730        // Small payload: striping never engages, so this exercises the sequential path
1731        // (matching the rest of the small-data adversarial tests).
1732        assert_adversarial_rejected(12, 4, &[0xCDu8; 30], &Sequential);
1733    }
1734
1735    /// The striped decode path re-implements every consensus-critical rejection check in a
1736    /// separate code path from the sequential one. Drive each malicious scenario through both
1737    /// paths on the same large payload and require identical (Inconsistent) verdicts.
1738    #[test]
1739    fn test_adversarial_rejection_striped_matches_sequential() {
1740        let total = 12u16;
1741        let min = 4u16;
1742
1743        // Large payload so the parallel strategy takes the striped path. Assert it actually
1744        // splits into >= 2 stripes; otherwise this would silently degrade to the sequential
1745        // path and give false confidence.
1746        let data = vec![0xABu8; 64 * 1024];
1747        let shard_len = canonical_shard_len(data.len(), min as usize);
1748        let rayon = Rayon::new(NZUsize!(4)).unwrap();
1749        assert!(
1750            striped::ranges(shard_len, rayon.manual().parallelism()).map_or(0, |r| r.len()) >= 2,
1751            "test must exercise >= 2 stripes (shard_len={shard_len})"
1752        );
1753
1754        // Same attacks, same data: the striped path must reject identically to sequential.
1755        assert_adversarial_rejected(total, min, &data, &Sequential);
1756        assert_adversarial_rejected(total, min, &data, &rayon);
1757
1758        // Sanity: an untampered mixed (some originals + a recovery) set still decodes via the
1759        // striped path, forcing striped::decode_reveal to reconstruct an original.
1760        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &Sequential).unwrap();
1761        let mixed = [0u16, 1, 2, 4]
1762            .into_iter()
1763            .map(|i| checked(root, chunks[i as usize].clone()))
1764            .collect::<Vec<_>>();
1765        let decoded = decode::<Sha256, _>(total, min, &root, mixed.iter(), &rayon).unwrap();
1766        assert_eq!(decoded, data);
1767    }
1768
1769    /// Pin the exact vendored Reed-Solomon recovery output for a fixed `(k, m, shard_len)` with a
1770    /// partial final block (`shard_len % SHARD_CHUNK_BYTES != 0`). The striped coder assumes this
1771    /// crate's internal block layout and tail packing (see [`SHARD_CHUNK_BYTES`]); if the
1772    /// implementation changes its produced bytes this fixture trips, signalling that the striping
1773    /// assumption must be re-verified before the new output is accepted.
1774    #[test]
1775    fn test_recovery_output_format_pinned() {
1776        let k = 5usize;
1777        let m = 3usize;
1778        // Two full blocks plus a 2-byte partial tail.
1779        let shard_len = 2 * SHARD_CHUNK_BYTES + 2;
1780
1781        let mut encoder = Encoder::new(k, m, shard_len).unwrap();
1782        for i in 0..k {
1783            let shard: Vec<u8> = (0..shard_len)
1784                .map(|j| ((i * 31 + j * 7) & 0xFF) as u8)
1785                .collect();
1786            encoder.add_original_shard(&shard).unwrap();
1787        }
1788        let encoding = encoder.encode().unwrap();
1789
1790        let mut hasher = Sha256::new();
1791        for shard in encoding.recovery_iter() {
1792            hasher.update(shard);
1793        }
1794        let digest = hasher.finalize();
1795        assert_eq!(
1796            format!("{digest}"),
1797            "e38bb9dbba4a102c4bd8447e212957742dab0af0c4148d4660c671f2f33d3df2",
1798            "vendored Reed-Solomon recovery output changed; re-verify the striping \
1799             assumption before updating this fixture"
1800        );
1801    }
1802
1803    #[test]
1804    fn test_parallel_decode_rejects_mismatched_shard_lengths() {
1805        let strategy = Rayon::new(NZUsize!(4)).unwrap();
1806        let data = vec![42u8; 256 * 1024];
1807        let total = 24u16;
1808        let min = 8u16;
1809
1810        let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &strategy).unwrap();
1811        let mut shards = chunks
1812            .iter()
1813            .map(|chunk| chunk.shard.to_vec())
1814            .collect::<Vec<_>>();
1815        shards[min as usize].pop();
1816
1817        let mut builder = Builder::<Sha256>::new(total as usize);
1818        for shard in &shards {
1819            let mut hasher = Sha256::new();
1820            hasher.update(shard);
1821            builder.add(&hasher.finalize());
1822        }
1823        let tree = builder.build();
1824        let root = tree.root();
1825
1826        let pieces = [9u16, 8, 10, 11, 12, 13, 14, 15]
1827            .into_iter()
1828            .map(|i| {
1829                let proof = tree.proof(i as u32).unwrap();
1830                checked(
1831                    root,
1832                    Chunk::new(shards[i as usize].clone().into(), i, proof),
1833                )
1834            })
1835            .collect::<Vec<_>>();
1836
1837        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &strategy);
1838        assert!(matches!(result, Err(Error::Inconsistent)));
1839    }
1840
1841    #[test]
1842    fn test_malicious_root_detection() {
1843        let data = b"Original data that should be protected";
1844        let total = 7u16;
1845        let min = 4u16;
1846
1847        // Encode data correctly to get valid chunks
1848        let (_correct_root, chunks) =
1849            encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1850
1851        // Create a malicious/fake root (simulating a malicious encoder)
1852        let mut hasher = Sha256::new();
1853        hasher.update(b"malicious_data_that_wasnt_actually_encoded");
1854        let malicious_root = hasher.finalize();
1855
1856        // Verify all proofs at incorrect root
1857        for i in 0..total {
1858            assert!(chunks[i as usize]
1859                .clone()
1860                .verify::<Sha256>(i, &malicious_root)
1861                .is_none());
1862        }
1863
1864        // Collect valid pieces (these are legitimate fragments checked against
1865        // the correct root).
1866        let minimal = chunks
1867            .into_iter()
1868            .take(min as usize)
1869            .map(|c| checked(_correct_root, c))
1870            .collect::<Vec<_>>();
1871
1872        // Attempt to decode with malicious root - rejected because checked
1873        // chunks are bound to a different commitment.
1874        let result = decode::<Sha256, _>(total, min, &malicious_root, minimal.iter(), &STRATEGY);
1875        assert!(matches!(result, Err(Error::CommitmentMismatch)));
1876    }
1877
1878    #[test]
1879    fn test_mismatched_config_rejected_during_check() {
1880        let config_expected = Config {
1881            minimum_shards: NZU16!(2),
1882            extra_shards: NZU16!(2),
1883        };
1884        let config_actual = Config {
1885            minimum_shards: NZU16!(3),
1886            extra_shards: NZU16!(3),
1887        };
1888
1889        let data = b"leaf_count mismatch proof";
1890        let (commitment, shards) = RS::encode(&config_actual, data.as_slice(), &STRATEGY).unwrap();
1891
1892        // A proof generated under a different shard configuration is invalid
1893        // for this commitment.
1894        let check_result = RS::check(&config_expected, &commitment, 0, &shards[0]);
1895        assert!(matches!(check_result, Err(Error::InvalidProof)));
1896    }
1897
1898    #[test]
1899    fn test_manipulated_chunk_detection() {
1900        let data = b"Data integrity must be maintained";
1901        let total = 6u16;
1902        let min = 3u16;
1903
1904        // Encode data
1905        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1906        let mut pieces: Vec<_> = chunks.into_iter().map(|c| checked(root, c)).collect();
1907
1908        // Tamper with one of the checked chunks by modifying the shard data.
1909        if !pieces[1].shard.is_empty() {
1910            let mut shard = pieces[1].shard.to_vec();
1911            shard[0] ^= 0xFF; // Flip bits in first byte
1912            pieces[1].shard = shard.into();
1913        }
1914
1915        // Try to decode with the tampered chunk
1916        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1917        assert!(matches!(result, Err(Error::Inconsistent)));
1918    }
1919
1920    #[test]
1921    fn test_inconsistent_shards() {
1922        let data = b"Test data for malicious encoding";
1923        let total = 5u16;
1924        let min = 3u16;
1925        let m = total - min;
1926
1927        // Compute original data encoding
1928        let (padded, shard_size) = prepare_data(data.as_slice(), min as usize);
1929
1930        // Re-encode the data
1931        let mut encoder = Encoder::new(min as usize, m as usize, shard_size).unwrap();
1932        for shard in padded.chunks(shard_size) {
1933            encoder.add_original_shard(shard).unwrap();
1934        }
1935        let recovery_result = encoder.encode().unwrap();
1936        let mut recovery_shards: Vec<Vec<u8>> = recovery_result
1937            .recovery_iter()
1938            .map(|s| s.to_vec())
1939            .collect();
1940
1941        // Tamper with one recovery shard
1942        if !recovery_shards[0].is_empty() {
1943            recovery_shards[0][0] ^= 0xFF;
1944        }
1945
1946        // Build malicious shards
1947        let mut malicious_shards: Vec<Vec<u8>> =
1948            padded.chunks(shard_size).map(|s| s.to_vec()).collect();
1949        malicious_shards.extend(recovery_shards);
1950
1951        // Build malicious tree
1952        let mut builder = Builder::<Sha256>::new(total as usize);
1953        for shard in &malicious_shards {
1954            let mut hasher = Sha256::new();
1955            hasher.update(shard);
1956            builder.add(&hasher.finalize());
1957        }
1958        let malicious_tree = builder.build();
1959        let malicious_root = malicious_tree.root();
1960
1961        // Generate chunks for min pieces, including the tampered recovery
1962        let selected_indices = vec![0, 1, 3]; // originals 0,1 and recovery 0 (index 3)
1963        let mut pieces = Vec::new();
1964        for &i in &selected_indices {
1965            let merkle_proof = malicious_tree.proof(i as u32).unwrap();
1966            let shard = malicious_shards[i].clone();
1967            let chunk = Chunk::new(shard.into(), i as u16, merkle_proof);
1968            pieces.push(chunk);
1969        }
1970        let pieces: Vec<_> = pieces
1971            .into_iter()
1972            .map(|c| checked(malicious_root, c))
1973            .collect();
1974
1975        // Fail to decode
1976        let result = decode::<Sha256, _>(total, min, &malicious_root, pieces.iter(), &STRATEGY);
1977        assert!(matches!(result, Err(Error::Inconsistent)));
1978    }
1979
1980    // Regression: a commitment built from shards with non-zero trailing padding
1981    // used to pass decode(), even though canonical re-encoding (zero padding)
1982    // produces a different root. decode() must reject such non-canonical shards.
1983    #[test]
1984    fn test_non_canonical_padding_rejected() {
1985        let data = b"X";
1986        let total = 6u16;
1987        let min = 3u16;
1988        let k = min as usize;
1989        let m = total as usize - k;
1990
1991        let (mut padded, shard_len) = prepare_data(data.as_slice(), k);
1992        let payload_end = u32::SIZE + data.len();
1993        let total_original_len = k * shard_len;
1994        assert!(payload_end < total_original_len, "test requires padding");
1995
1996        // Corrupt one canonical padding byte while keeping payload unchanged.
1997        let pad_shard = payload_end / shard_len;
1998        let pad_offset = payload_end % shard_len;
1999        padded[pad_shard * shard_len + pad_offset] = 0xAA;
2000
2001        let mut encoder = Encoder::new(k, m, shard_len).unwrap();
2002        for shard in padded.chunks(shard_len) {
2003            encoder.add_original_shard(shard).unwrap();
2004        }
2005        let recovery = encoder.encode().unwrap();
2006        let mut shards: Vec<Vec<u8>> = padded.chunks(shard_len).map(|s| s.to_vec()).collect();
2007        shards.extend(recovery.recovery_iter().map(|s| s.to_vec()));
2008
2009        let mut builder = Builder::<Sha256>::new(total as usize);
2010        for shard in &shards {
2011            let mut hasher = Sha256::new();
2012            hasher.update(shard);
2013            builder.add(&hasher.finalize());
2014        }
2015        let tree = builder.build();
2016        let non_canonical_root = tree.root();
2017
2018        let mut pieces = Vec::with_capacity(k);
2019        for (i, shard) in shards.iter().take(k).enumerate() {
2020            let proof = tree.proof(i as u32).unwrap();
2021            pieces.push(checked(
2022                non_canonical_root,
2023                Chunk::new(shard.clone().into(), i as u16, proof),
2024            ));
2025        }
2026
2027        let result = decode::<Sha256, _>(total, min, &non_canonical_root, pieces.iter(), &STRATEGY);
2028        assert!(matches!(result, Err(Error::Inconsistent)));
2029    }
2030
2031    #[test]
2032    fn minifuzz_decode_unique_commitment() {
2033        minifuzz::Builder::default()
2034            .with_search_limit(2048)
2035            .test(|u| {
2036                fuzz_arbitrary_codeword(u)?;
2037                fuzz_mixed_codeword(u)?;
2038                Ok(())
2039            });
2040    }
2041
2042    #[test]
2043    fn test_oversized_zero_padded_shards_rejected() {
2044        let data = b"X";
2045        let total = 6u16;
2046        let min = 3u16;
2047        let k = min as usize;
2048        let m = total as usize - k;
2049
2050        let oversized_shard_len = 4usize;
2051        let mut padded = vec![0u8; k * oversized_shard_len];
2052        padded[..u32::SIZE].copy_from_slice(&(data.len() as u32).to_be_bytes());
2053        padded[u32::SIZE..u32::SIZE + data.len()].copy_from_slice(data);
2054
2055        let mut encoder = Encoder::new(k, m, oversized_shard_len).unwrap();
2056        for shard in padded.chunks(oversized_shard_len) {
2057            encoder.add_original_shard(shard).unwrap();
2058        }
2059        let recovery = encoder.encode().unwrap();
2060
2061        let mut oversized_shards: Vec<Vec<u8>> = padded
2062            .chunks(oversized_shard_len)
2063            .map(|shard| shard.to_vec())
2064            .collect();
2065        oversized_shards.extend(recovery.recovery_iter().map(|shard| shard.to_vec()));
2066
2067        let mut builder = Builder::<Sha256>::new(total as usize);
2068        for shard in &oversized_shards {
2069            let mut hasher = Sha256::new();
2070            hasher.update(shard);
2071            builder.add(&hasher.finalize());
2072        }
2073        let oversized_tree = builder.build();
2074        let oversized_root = oversized_tree.root();
2075
2076        let (canonical_root, _) =
2077            encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2078        assert_ne!(oversized_root, canonical_root);
2079
2080        let pieces = [0u16, 1u16, 4u16]
2081            .into_iter()
2082            .map(|i| {
2083                let proof = oversized_tree.proof(i as u32).unwrap();
2084                checked(
2085                    oversized_root,
2086                    Chunk::new(oversized_shards[i as usize].clone().into(), i, proof),
2087                )
2088            })
2089            .collect::<Vec<_>>();
2090
2091        let result = decode::<Sha256, _>(total, min, &oversized_root, pieces.iter(), &STRATEGY);
2092        assert!(matches!(result, Err(Error::Inconsistent)));
2093    }
2094
2095    #[test]
2096    fn test_extra_non_canonical_recovery_rejected() {
2097        let data = b"canonical originals with bad recovery";
2098        let total = 6u16;
2099        let min = 3u16;
2100
2101        let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2102        let mut shards = chunks
2103            .iter()
2104            .map(|chunk| chunk.shard.to_vec())
2105            .collect::<Vec<_>>();
2106        shards[min as usize][0] ^= 0xFF;
2107
2108        let mut builder = Builder::<Sha256>::new(total as usize);
2109        for shard in &shards {
2110            let mut hasher = Sha256::new();
2111            hasher.update(shard);
2112            builder.add(&hasher.finalize());
2113        }
2114        let tree = builder.build();
2115        let root = tree.root();
2116
2117        let pieces = (0u16..=3u16)
2118            .map(|i| {
2119                let proof = tree.proof(i as u32).unwrap();
2120                checked(
2121                    root,
2122                    Chunk::new(shards[i as usize].clone().into(), i, proof),
2123                )
2124            })
2125            .collect::<Vec<_>>();
2126
2127        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
2128        assert!(matches!(result, Err(Error::Inconsistent)));
2129    }
2130
2131    #[test]
2132    fn test_reconstructed_original_with_extra_non_canonical_recovery_rejected() {
2133        let data = b"canonical reconstructed originals with bad extra recovery";
2134        let total = 6u16;
2135        let min = 3u16;
2136
2137        let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2138        let mut shards = chunks
2139            .iter()
2140            .map(|chunk| chunk.shard.to_vec())
2141            .collect::<Vec<_>>();
2142        shards[4][0] ^= 0xFF;
2143
2144        let mut builder = Builder::<Sha256>::new(total as usize);
2145        for shard in &shards {
2146            let mut hasher = Sha256::new();
2147            hasher.update(shard);
2148            builder.add(&hasher.finalize());
2149        }
2150        let tree = builder.build();
2151        let root = tree.root();
2152
2153        let pieces = [0u16, 1u16, 3u16, 4u16]
2154            .into_iter()
2155            .map(|i| {
2156                let proof = tree.proof(i as u32).unwrap();
2157                checked(
2158                    root,
2159                    Chunk::new(shards[i as usize].clone().into(), i, proof),
2160                )
2161            })
2162            .collect::<Vec<_>>();
2163
2164        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
2165        assert!(matches!(result, Err(Error::Inconsistent)));
2166    }
2167
2168    #[test]
2169    fn test_striped_surplus_recovery_tampered_rejected() {
2170        // Striped decode-reveal with more than `k` shards provided and an original missing,
2171        // where a surplus recovery shard is committed but non-canonical. The decoder is fed
2172        // exactly `k`; the trimmed surplus is reconstructed and rejected by the root check
2173        // (there is no separate provided-recovery comparison on this path).
2174        let strategy = Rayon::new(NZUsize!(4)).unwrap();
2175        let data = vec![0xABu8; 64 * 1024];
2176        let total = 12u16;
2177        let min = 4u16;
2178        let shard_len = canonical_shard_len(data.len(), min as usize);
2179        assert!(
2180            striped::ranges(shard_len, strategy.manual().parallelism()).map_or(0, |r| r.len()) >= 2,
2181            "test must exercise the striped path (shard_len={shard_len})"
2182        );
2183
2184        // Provide originals 0,1 and recoveries 4,5,6 (5 > k=4, originals 2,3 missing). The
2185        // decoder is fed originals 0,1 + recoveries 4,5; recovery 6 is the trimmed surplus.
2186        let selected = [0u16, 1, 4, 5, 6];
2187        let tamper: fn(&mut [Vec<u8>], usize, usize) = |shards, _, _| shards[6][0] ^= 0xFF;
2188        let result = decode_tampered_codeword(total, min, &data, &selected, tamper, &strategy);
2189        assert!(matches!(result, Err(Error::Inconsistent)));
2190    }
2191
2192    #[test]
2193    fn test_decode_invalid_index() {
2194        let data = b"Testing recovery pieces";
2195        let total = 8u16;
2196        let min = 3u16;
2197
2198        // Encode the data
2199        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2200
2201        // Use a mix of original and recovery pieces
2202        let mut invalid = checked(root, chunks[1].clone());
2203        invalid.index = 8;
2204        let pieces: Vec<_> = vec![
2205            checked(root, chunks[0].clone()), // original
2206            invalid,                          // recovery with invalid index
2207            checked(root, chunks[6].clone()), // recovery
2208        ];
2209
2210        // Fail to decode
2211        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
2212        assert!(matches!(result, Err(Error::InvalidIndex(8))));
2213    }
2214
2215    #[test]
2216    fn test_max_chunks() {
2217        let data = vec![42u8; 1000]; // 1KB of data
2218        let total = u16::MAX;
2219        let min = u16::MAX / 2;
2220
2221        // Encode data
2222        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2223
2224        // Try to decode with min
2225        let minimal = chunks
2226            .into_iter()
2227            .take(min as usize)
2228            .map(|c| checked(root, c))
2229            .collect::<Vec<_>>();
2230        let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
2231        assert_eq!(decoded, data);
2232    }
2233
2234    #[test]
2235    fn test_too_many_chunks() {
2236        let data = vec![42u8; 1000]; // 1KB of data
2237        let total = u16::MAX;
2238        let min = u16::MAX / 2 - 1;
2239
2240        // Encode data
2241        let result = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY);
2242        assert!(matches!(
2243            result,
2244            Err(Error::ReedSolomon(RsError::UnsupportedShardCount {
2245                original_count: _,
2246                recovery_count: _,
2247            }))
2248        ));
2249    }
2250
2251    #[test]
2252    fn test_too_many_total_shards() {
2253        assert!(RS::encode(
2254            &Config {
2255                minimum_shards: NZU16!(u16::MAX / 2 + 1),
2256                extra_shards: NZU16!(u16::MAX),
2257            },
2258            [].as_slice(),
2259            &STRATEGY,
2260        )
2261        .is_err())
2262    }
2263
2264    #[test]
2265    fn test_chunk_encode_with_pool_matches_encode() {
2266        let executor = deterministic::Runner::default();
2267        executor.start(|context| async move {
2268            let pool = context.network_buffer_pool();
2269
2270            let data = b"pool encoding test";
2271            let (_root, chunks) = encode::<Sha256, _>(5, 3, data.as_slice(), &STRATEGY).unwrap();
2272            let chunk = &chunks[0];
2273
2274            let encoded = chunk.encode();
2275            let mut encoded_pool = chunk.encode_with_pool(pool);
2276            let mut encoded_pool_bytes = vec![0u8; encoded_pool.remaining()];
2277            encoded_pool.copy_to_slice(&mut encoded_pool_bytes);
2278            assert_eq!(encoded_pool_bytes, encoded.as_ref());
2279        });
2280    }
2281
2282    #[cfg(feature = "arbitrary")]
2283    mod conformance {
2284        use super::*;
2285        use commonware_codec::conformance::CodecConformance;
2286        use commonware_cryptography::sha256::Digest as Sha256Digest;
2287
2288        commonware_conformance::conformance_tests! {
2289            CodecConformance<Chunk<Sha256Digest>>,
2290        }
2291    }
2292}