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