Skip to main content

commonware_coding/
reed_solomon.rs

1use crate::{Config, Scheme};
2use bytes::{BufMut, Bytes};
3use commonware_codec::{
4    Buf, BufsMut, EncodeSize, FixedSize, RangeCfg, Read, ReadExt, Write, util::max_items_size,
5};
6use commonware_cryptography::{
7    Digest, Hasher,
8    reed_solomon::{Decoder, Encoder, Error as RsError, Plan, SHARD_CHUNK_BYTES},
9};
10use commonware_parallel::{Batches, Strategy};
11use commonware_storage::bmt::{self, Builder};
12use commonware_utils::{Cached, MAX_ALLOCATION_BYTES, NZUsize, Widen};
13use std::{marker::PhantomData, ops::Range};
14use thiserror::Error;
15
16// Thread-local caches for reusing `Encoder` and `Decoder`
17// instances across calls. Constructing these objects is expensive because
18// the underlying engine initializes GF lookup tables. The `reset()` method
19// reconfigures the work buffers without rebuilding those tables.
20commonware_utils::thread_local_cache!(static CACHED_ENCODER: Encoder);
21commonware_utils::thread_local_cache!(static CACHED_DECODER: Decoder);
22
23/// Minimum stripe width for amortizing encoder/decoder setup.
24///
25/// Batching operates in `SHARD_CHUNK_BYTES` units. This minimum is a positive
26/// multiple of that width, so its stripe-count bound also guarantees at least
27/// one complete block per stripe. The strategy further caps the stripe count
28/// by available parallelism.
29const MIN_STRIPE_BYTES: usize = 8 * 1024;
30
31/// Target transform storage per tile within a stripe.
32///
33/// A tile's transform storage is at most twice this target. Every coder instance is configured
34/// for a single tile, which also bounds the work that each thread's cached coders retain.
35const MAX_TILE_WORK_BYTES: usize = 2 * 1024 * 1024;
36
37/// Errors that can occur when interacting with the Reed-Solomon coder.
38#[derive(Error, Debug)]
39pub enum Error {
40    #[error("reed-solomon error: {0}")]
41    ReedSolomon(#[from] RsError),
42    #[error("inconsistent")]
43    Inconsistent,
44    #[error("invalid proof")]
45    InvalidProof,
46    #[error("not enough chunks")]
47    NotEnoughChunks,
48    #[error("duplicate chunk index: {0}")]
49    DuplicateIndex(u16),
50    #[error("invalid data length: {0}")]
51    InvalidDataLength(usize),
52    #[error("invalid index: {0}")]
53    InvalidIndex(u16),
54    #[error("too many total shards: {0}")]
55    TooManyTotalShards(u32),
56    #[error("checked shard commitment does not match decode commitment")]
57    CommitmentMismatch,
58}
59
60fn total_shards(config: &Config) -> Result<u16, Error> {
61    let total = config.total_shards();
62    total
63        .try_into()
64        .map_err(|_| Error::TooManyTotalShards(total))
65}
66
67/// Validate the requested shard index, embedded index, proof leaf count, and shard width before
68/// hashing.
69///
70/// Every codeword has a positive, even shard width. Decode relies on this for checked shards.
71fn check_metadata<D: Digest>(total: u16, index: u16, shard: &Chunk<D>) -> Result<(), Error> {
72    if index >= total {
73        return Err(Error::InvalidIndex(index));
74    }
75    let width = shard.shard.len();
76    if width == 0 || !width.is_multiple_of(2) {
77        return Err(RsError::InvalidShardSize { shard_bytes: width }.into());
78    }
79    if shard.proof.leaf_count != u32::from(total) {
80        return Err(Error::InvalidProof);
81    }
82    if shard.index != index {
83        return Err(Error::InvalidIndex(shard.index));
84    }
85    Ok(())
86}
87
88/// Authenticate a shard at its requested position and cache its digest with the commitment.
89/// The digest is evaluated only after the shard metadata is valid.
90fn check_chunk<H: Hasher>(
91    total: u16,
92    commitment: &H::Digest,
93    index: u16,
94    shard: &Chunk<H::Digest>,
95    digest: impl FnOnce() -> H::Digest,
96) -> Result<CheckedChunk<H>, Error> {
97    check_metadata(total, index, shard)?;
98
99    // Bind the shard digest to its requested position and commitment.
100    let digest = digest();
101    shard
102        .proof
103        .verify_element_inclusion::<H>(&digest, u32::from(index), commitment)
104        .map_err(|_| Error::InvalidProof)?;
105
106    // Cache the verified digest with the commitment and shard it authenticates.
107    Ok(CheckedChunk::new(
108        *commitment,
109        shard.shard.clone(),
110        index,
111        digest,
112    ))
113}
114
115/// A piece of data from a Reed-Solomon encoded object.
116#[derive(Debug, Clone)]
117pub struct Chunk<D: Digest> {
118    /// The shard of encoded data.
119    shard: Bytes,
120
121    /// The index of [`Chunk`] in the original data.
122    index: u16,
123
124    /// The multi-proof of the shard in the [`bmt`] at the given index.
125    proof: bmt::Proof<D>,
126}
127
128impl<D: Digest> Chunk<D> {
129    /// Returns an upper bound on the encoded size of a chunk whose shard holds at most `shard`
130    /// bytes and whose proof holds at most `siblings` sibling digests.
131    pub fn max_encode_size(shard: usize, siblings: usize) -> usize {
132        max_items_size(shard, u8::SIZE) + u16::SIZE + bmt::Proof::<D>::max_encode_size(siblings)
133    }
134
135    /// Create a new [`Chunk`] from the given shard, index, and proof.
136    const fn new(shard: Bytes, index: u16, proof: bmt::Proof<D>) -> Self {
137        Self {
138            shard,
139            index,
140            proof,
141        }
142    }
143}
144
145/// A shard that has been checked against a commitment.
146///
147/// This stores the shard digest computed during [`Scheme::check`] and the
148/// commitment root it was verified against. The root is checked at decode
149/// time to prevent cross-commitment shard mixing.
150///
151/// Decoding trusts the stored digest and does not hash the shard again, so the type is
152/// parameterized by the hasher that computed it. A shard checked with one hasher cannot be
153/// decoded with another, even when both hashers produce the same digest type.
154pub struct CheckedChunk<H: Hasher> {
155    root: H::Digest,
156    shard: Bytes,
157    index: u16,
158    digest: H::Digest,
159}
160
161impl<H: Hasher> CheckedChunk<H> {
162    const fn new(root: H::Digest, shard: Bytes, index: u16, digest: H::Digest) -> Self {
163        Self {
164            root,
165            shard,
166            index,
167            digest,
168        }
169    }
170}
171
172impl<H: Hasher> Clone for CheckedChunk<H> {
173    fn clone(&self) -> Self {
174        Self {
175            root: self.root,
176            shard: self.shard.clone(),
177            index: self.index,
178            digest: self.digest,
179        }
180    }
181}
182
183impl<H: Hasher> std::fmt::Debug for CheckedChunk<H> {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        f.debug_struct("CheckedChunk")
186            .field("root", &self.root)
187            .field("shard", &self.shard)
188            .field("index", &self.index)
189            .field("digest", &self.digest)
190            .finish()
191    }
192}
193
194impl<H: Hasher> PartialEq for CheckedChunk<H> {
195    fn eq(&self, other: &Self) -> bool {
196        self.root == other.root
197            && self.shard == other.shard
198            && self.index == other.index
199            && self.digest == other.digest
200    }
201}
202
203impl<H: Hasher> Eq for CheckedChunk<H> {}
204
205impl<D: Digest> Write for Chunk<D> {
206    fn write(&self, writer: &mut impl BufMut) {
207        self.shard.write(writer);
208        self.index.write(writer);
209        self.proof.write(writer);
210    }
211
212    fn write_bufs(&self, buf: &mut impl BufsMut) {
213        self.shard.write_bufs(buf);
214        self.index.write(buf);
215        self.proof.write(buf);
216    }
217}
218
219impl<D: Digest> Read for Chunk<D> {
220    /// The coding config and the maximum number of data bytes.
221    ///
222    /// A chunk decodes only if its shard is no wider than encoding that many bytes under the
223    /// config would produce. The bound shrinks as `minimum_shards` grows and is widest at one,
224    /// where a single shard holds the length prefix and all of the data.
225    type Cfg = (Config, usize);
226
227    fn read_cfg(
228        reader: &mut impl Buf,
229        (config, maximum): &Self::Cfg,
230    ) -> Result<Self, commonware_codec::Error> {
231        let width = canonical_shard_len(
232            (*maximum).min(Widen::widen(u32::MAX)),
233            config.minimum_shards.widen(),
234        );
235        let shard = Bytes::read_cfg(reader, &RangeCfg::new(..=width))?;
236        let index = u16::read(reader)?;
237        let proof = bmt::Proof::<D>::read_cfg(reader, &1)?;
238        Ok(Self {
239            shard,
240            index,
241            proof,
242        })
243    }
244}
245
246impl<D: Digest> EncodeSize for Chunk<D> {
247    fn encode_size(&self) -> usize {
248        self.shard.encode_size() + self.index.encode_size() + self.proof.encode_size()
249    }
250
251    fn encode_inline_size(&self) -> usize {
252        self.shard.encode_inline_size() + self.index.encode_size() + self.proof.encode_size()
253    }
254}
255
256impl<D: Digest> PartialEq for Chunk<D> {
257    fn eq(&self, other: &Self) -> bool {
258        self.shard == other.shard && self.index == other.index && self.proof == other.proof
259    }
260}
261
262impl<D: Digest> Eq for Chunk<D> {}
263
264#[cfg(feature = "arbitrary")]
265impl<D: Digest> arbitrary::Arbitrary<'_> for Chunk<D>
266where
267    D: for<'a> arbitrary::Arbitrary<'a>,
268{
269    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
270        Ok(Self {
271            shard: u.arbitrary::<Vec<u8>>()?.into(),
272            index: u.arbitrary()?,
273            proof: u.arbitrary()?,
274        })
275    }
276}
277
278/// Prepare data for encoding.
279///
280/// Returns a contiguous buffer of `k` padded shards of `shard_len` bytes each, where
281/// `shard_len` is [`canonical_shard_len`] of the payload. The buffer layout is
282/// `[length_prefix | data | zero_padding]`.
283fn prepare_data(mut data: impl bytes::Buf, k: usize, shard_len: usize) -> Vec<u8> {
284    let data_len = data.remaining();
285    let length_bytes = (data_len as u32).to_be_bytes();
286    let mut padded = vec![0u8; k * shard_len];
287    padded[..u32::SIZE].copy_from_slice(&length_bytes);
288    data.copy_to_slice(&mut padded[u32::SIZE..u32::SIZE + data_len]);
289    padded
290}
291
292/// Return the canonical shard width for a payload and shard count.
293///
294/// Encoding prefixes the payload with its length, splits the result across
295/// `k` original shards, and rounds up to an even width required by the
296/// Reed-Solomon implementation. Decode uses the same calculation to reject
297/// commitments that decode to the same payload with a non-canonical shard width.
298const fn canonical_shard_len(data_len: usize, k: usize) -> usize {
299    let prefixed_len = data_len.saturating_add(u32::SIZE);
300    let mut shard_len = prefixed_len.div_ceil(k);
301
302    // Ensure shard length is even, as required by the Reed-Solomon implementation.
303    if !shard_len.is_multiple_of(2) {
304        shard_len = shard_len.saturating_add(1);
305    }
306
307    shard_len
308}
309
310/// Returns whether `n` shards of `shard_len` bytes fit in one allocation.
311const fn codeword_fits(n: usize, shard_len: usize) -> bool {
312    match n.checked_mul(shard_len) {
313        Some(bytes) => bytes <= MAX_ALLOCATION_BYTES,
314        None => false,
315    }
316}
317
318/// Validate the length prefix, zero padding, and canonical width of the original shards.
319/// Returns the payload length without allocating or copying it.
320fn validate_data_layout(shards: &[&[u8]], expected_shard_len: usize) -> Result<usize, Error> {
321    let data_len = read_data_len(shards)?;
322    let mut data_end = u32::SIZE + data_len;
323    for shard in shards {
324        if data_end >= shard.len() {
325            data_end -= shard.len();
326            continue;
327        }
328        if !shard[data_end..].iter().all(|byte| *byte == 0) {
329            return Err(Error::Inconsistent);
330        }
331        data_end = 0;
332    }
333    if canonical_shard_len(data_len, shards.len()) != expected_shard_len {
334        return Err(Error::Inconsistent);
335    }
336    Ok(data_len)
337}
338
339/// Validate the first `k` original shards and copy their payload into an owned buffer.
340fn extract_data(shards: &[&[u8]], k: usize, expected_shard_len: usize) -> Result<Vec<u8>, Error> {
341    let shards = shards.get(..k).ok_or(Error::NotEnoughChunks)?;
342    let data_len = validate_data_layout(shards, expected_shard_len)?;
343    let mut data = Vec::with_capacity(data_len);
344    let mut prefix_bytes_left = u32::SIZE;
345    let mut data_bytes_left = data_len;
346    for shard in shards {
347        // The length prefix may straddle shard boundaries, so ignore bytes until
348        // we reach the first payload byte.
349        if prefix_bytes_left >= shard.len() {
350            prefix_bytes_left -= shard.len();
351            continue;
352        }
353
354        // Copy only the live payload bytes from this shard.
355        let payload = &shard[prefix_bytes_left..];
356        let copy_len = data_bytes_left.min(payload.len());
357        data.extend_from_slice(&payload[..copy_len]);
358        data_bytes_left -= copy_len;
359
360        prefix_bytes_left = 0;
361    }
362    Ok(data)
363}
364
365/// Read the 4-byte big-endian length prefix from `shards` and validate that
366/// the decoded length fits in the post-prefix payload region.
367fn read_data_len(shards: &[&[u8]]) -> Result<usize, Error> {
368    let total_len: usize = shards.iter().map(|s| s.len()).sum();
369    if total_len < u32::SIZE {
370        return Err(Error::Inconsistent);
371    }
372
373    // Read the length prefix, which may span multiple shards.
374    let mut prefix = [0u8; u32::SIZE];
375    let mut prefix_len = 0usize;
376    for shard in shards {
377        if prefix_len == u32::SIZE {
378            break;
379        }
380        let read = (u32::SIZE - prefix_len).min(shard.len());
381        prefix[prefix_len..prefix_len + read].copy_from_slice(&shard[..read]);
382        prefix_len += read;
383    }
384
385    let data_len = u32::from_be_bytes(prefix) as usize;
386    let payload_len = total_len - u32::SIZE;
387    if data_len > payload_len {
388        return Err(Error::Inconsistent);
389    }
390    Ok(data_len)
391}
392
393/// Type alias for the internal encoding result.
394type Encoding<D> = (D, Vec<Chunk<D>>);
395
396/// Rejects shard counts the Reed-Solomon backend does not support.
397fn validate_counts(k: usize, m: usize) -> Result<(), Error> {
398    if Encoder::supports(k, m) {
399        Ok(())
400    } else {
401        Err(Error::ReedSolomon(RsError::UnsupportedShardCount {
402            original_count: k,
403            recovery_count: m,
404        }))
405    }
406}
407
408/// Encode data using a Reed-Solomon coder and insert it into a [`bmt`].
409///
410/// # Parameters
411///
412/// - `total`: The total number of chunks to generate.
413/// - `min`: The minimum number of chunks required to decode the data.
414/// - `data`: The data to encode.
415/// - `strategy`: The parallelism strategy to use.
416///
417/// # Returns
418///
419/// - `root`: The root of the [`bmt`].
420/// - `chunks`: [`Chunk`]s of encoded data (that can be proven against `root`).
421fn encode<H: Hasher, S: Strategy>(
422    total: u16,
423    min: u16,
424    data: impl bytes::Buf,
425    strategy: &S,
426) -> Result<Encoding<H::Digest>, Error> {
427    // Validate parameters
428    assert!(total > min);
429    assert!(min > 0);
430    let n = total as usize;
431    let k = min as usize;
432    let m = n - k;
433    let data_len = data.remaining();
434    if data_len > Widen::widen(u32::MAX) {
435        return Err(Error::InvalidDataLength(data_len));
436    }
437
438    // Reject unsupported shard counts before allocating any buffer. The buffers below are then
439    // bounded by the shard counts and `data_len`.
440    validate_counts(k, m)?;
441
442    // The shard width is the length prefix of every chunk on the wire, and the codeword is
443    // `n` shards of that width, so both must be representable before anything is allocated.
444    let shard_len = canonical_shard_len(data_len, k);
445    if shard_len > Widen::widen(u32::MAX) || !codeword_fits(n, shard_len) {
446        return Err(Error::InvalidDataLength(data_len));
447    }
448
449    // Prepare data as a contiguous buffer of k shards
450    let padded = prepare_data(data, k, shard_len);
451
452    // Compute recovery shards, striping large shard widths across the strategy
453    let recovery_buf = strategy.try_run_batches(
454        shard_len / SHARD_CHUNK_BYTES,
455        NZUsize!(MIN_STRIPE_BYTES / SHARD_CHUNK_BYTES),
456        SHARD_CHUNK_BYTES * n,
457        |batches| {
458            let original_shards = padded.chunks(shard_len).collect::<Vec<_>>();
459            let mut buf = vec![0u8; m * shard_len];
460            batches.try_map_collect_vec(
461                |ranges| {
462                    let ranges = striped::byte_ranges(shard_len, ranges);
463                    let groups = striped::stripe_columns(&mut buf, shard_len, &ranges);
464                    ranges.into_iter().zip(groups)
465                },
466                |(range, out)| striped::encode_recovery_into(k, m, range, &original_shards, out),
467            )?;
468            Ok::<_, Error>(buf)
469        },
470    )?;
471
472    // Create zero-copy Bytes views into the original and recovery buffers
473    let originals: Bytes = padded.into();
474    let recoveries: Bytes = recovery_buf.into();
475
476    // Build Merkle tree
477    let mut builder = Builder::<H>::new(n);
478    let shard_slices: Vec<Bytes> = (0..k)
479        .map(|i| originals.slice(i * shard_len..(i + 1) * shard_len))
480        .chain((0..m).map(|i| recoveries.slice(i * shard_len..(i + 1) * shard_len)))
481        .collect();
482    let shard_hashes = H::hash_many(&shard_slices, strategy);
483    for hash in &shard_hashes {
484        builder.add(hash);
485    }
486    let tree = builder.build();
487    let root = tree.root();
488
489    // Generate chunks with zero-copy shard views
490    let mut chunks = Vec::with_capacity(n);
491    for (i, shard) in shard_slices.into_iter().enumerate() {
492        let proof = tree.proof(i as u32).map_err(|_| Error::InvalidProof)?;
493        chunks.push(Chunk::new(shard, i as u16, proof));
494    }
495
496    Ok((root, chunks))
497}
498
499/// Inputs shared by the decode helpers.
500struct DecodeCtx<'a, H: Hasher, S: Strategy> {
501    /// Total number of shards (`k + m`).
502    n: usize,
503    /// Minimum shards required to decode (the number of original shards).
504    k: usize,
505    /// Number of recovery shards (`n - k`).
506    m: usize,
507    /// Width of every shard, in bytes.
508    shard_len: usize,
509    /// Commitment that the reconstructed codeword must reproduce.
510    root: &'a H::Digest,
511    /// Caller strategy used for reconstruction and shard hashing.
512    strategy: &'a S,
513}
514
515/// Striped Reed-Solomon: split every shard by byte range and run independent Reed-Solomon
516/// operations over those ranges. A whole-input run codes one stripe that spans the whole shard.
517///
518/// ```text
519///   originals:
520///     O0: [ stripe 0 ][ stripe 1 ][ tail ]
521///     O1: [ stripe 0 ][ stripe 1 ][ tail ]
522///     O(k-1): [ stripe 0 ][ stripe 1 ][ tail ]
523///
524///   encode stripe 0 -> R0[0], R1[0], more recoveries
525///   encode stripe 1 -> R0[1], R1[1], more recoveries
526///   encode tail     -> R0[t], R1[t], more recoveries
527///
528///   recovery Ri = concat(Ri[0], Ri[1], Ri[t])
529/// ```
530///
531/// Both decode paths reuse this per-stripe layout. With all originals present,
532/// [`decode`](striped::decode) re-encodes the recovery stripes and verifies them against the
533/// commitment. With an original missing, [`decode_reveal`](striped::decode_reveal)
534/// feeds exactly `k` shards and recovers the missing original and recovery stripes from the same
535/// Reed-Solomon decode (no re-encode).
536///
537/// Each stripe task codes its range in tiles that target [`MAX_TILE_WORK_BYTES`] of transform
538/// storage. Tiles start on [`SHARD_CHUNK_BYTES`] boundaries, so tiling does not change the coded
539/// bytes.
540mod striped {
541    use super::*;
542
543    /// Split a stripe `range` into the tiles that code it.
544    ///
545    /// The transform holds at most `2 * (k + m).next_power_of_two()` shards, so the target tile
546    /// width is the number of blocks whose transform storage fits in [`MAX_TILE_WORK_BYTES`],
547    /// and at least one. A partial final block counts as a block, since the engine pads it to a
548    /// whole one. A stripe with fewer than twice the target blocks is one tile. Otherwise its
549    /// blocks spread over the fewest tiles within the target, with widths that differ by at most
550    /// one block, and the partial final block ends the last tile. Every tile but the last ends
551    /// on a symbol-block boundary (see [`byte_ranges`]).
552    ///
553    /// The decoder's sixteen-shard AVX-512 derivative leaf skips shards whose padded length is a
554    /// multiple of 4 KiB. A split into several tiles therefore adds tiles until no tile has such
555    /// a length.
556    pub(super) fn tiles(
557        k: usize,
558        m: usize,
559        range: Range<usize>,
560    ) -> impl Iterator<Item = Range<usize>> {
561        let work_count = 2 * (k + m).next_power_of_two();
562        let target = (MAX_TILE_WORK_BYTES / (work_count * SHARD_CHUNK_BYTES)).max(1);
563        let blocks = range.len().div_ceil(SHARD_CHUNK_BYTES);
564        let mut count = if blocks < 2 * target {
565            1
566        } else {
567            blocks.div_ceil(target)
568        };
569        let page = 4096 / SHARD_CHUNK_BYTES;
570        while count > 1 && count < blocks {
571            let (width, extra) = (blocks / count, blocks % count);
572            if !width.is_multiple_of(page) && (extra == 0 || !(width + 1).is_multiple_of(page)) {
573                break;
574            }
575            count += 1;
576        }
577
578        // Tile `i` holds `width` blocks, plus one more for the first `extra` tiles. The last tile
579        // ends at `end`, so its final block may be partial.
580        let (width, extra) = (blocks / count, blocks % count);
581        let (start, end) = (range.start, range.end);
582        let edge = move |i: usize| start + (i * width + i.min(extra)) * SHARD_CHUNK_BYTES;
583        (0..count).map(move |i| edge(i)..if i + 1 == count { end } else { edge(i + 1) })
584    }
585
586    /// Split a shard-major buffer (`num_shards * shard_len`) into one group of mutable column
587    /// slices per stripe range: `groups[s][shard]` is bytes `ranges[s]` of shard `shard`. Using
588    /// `chunks_mut` + `split_at_mut` hands each parallel stripe task genuine, provably-disjoint
589    /// `&mut [u8]` slices, so the tasks fill the shared buffer without `unsafe`.
590    pub(super) fn stripe_columns<'a>(
591        buf: &'a mut [u8],
592        shard_len: usize,
593        ranges: &[Range<usize>],
594    ) -> Vec<Vec<&'a mut [u8]>> {
595        let mut groups: Vec<Vec<&'a mut [u8]>> = ranges.iter().map(|_| Vec::new()).collect();
596        for shard in buf.chunks_mut(shard_len) {
597            let mut rest = shard;
598            for (group, range) in groups.iter_mut().zip(ranges) {
599                let (head, tail) = rest.split_at_mut(range.len());
600                group.push(head);
601                rest = tail;
602            }
603        }
604        groups
605    }
606
607    /// The output column slices a recover-all stripe task writes into: one mutable slice per
608    /// missing original and per missing recovery (paired by position with [`Missing`]).
609    struct StripeOut<'a> {
610        originals: Vec<&'a mut [u8]>,
611        recoveries: Vec<&'a mut [u8]>,
612    }
613
614    /// The shard indices a stripe task must reconstruct (paired by position with [`StripeOut`]).
615    #[derive(Clone, Copy)]
616    struct Missing<'a> {
617        originals: &'a [usize],
618        recoveries: &'a [usize],
619    }
620
621    /// The shards a recover-all stripe task decodes from, with the [`Plan`] built from
622    /// their indices.
623    #[derive(Clone, Copy)]
624    struct Provided<'a> {
625        originals: &'a [(usize, &'a [u8])],
626        recoveries: &'a [(usize, &'a [u8])],
627        plan: &'a Plan,
628    }
629
630    /// Convert batches of complete symbol blocks into byte ranges, attaching any partial
631    /// final block to the last batch.
632    ///
633    /// Every non-final stripe ends on a [`SHARD_CHUNK_BYTES`] boundary: the engine lays
634    /// shards out in symbol blocks of that width (a partial final block is padded
635    /// internally), so a boundary in the middle of a block would change the bytes each
636    /// sub-instance encodes.
637    pub(super) fn byte_ranges(
638        shard_len: usize,
639        mut ranges: Vec<Range<usize>>,
640    ) -> Vec<Range<usize>> {
641        let full_blocks = shard_len / SHARD_CHUNK_BYTES;
642        for range in &mut ranges {
643            range.start *= SHARD_CHUNK_BYTES;
644            range.end = if range.end == full_blocks {
645                shard_len
646            } else {
647                range.end * SHARD_CHUNK_BYTES
648            };
649        }
650        ranges
651    }
652
653    /// Reconstruct a single stripe's missing shards from exactly `k` provided shards, reading
654    /// both missing originals and missing recoveries straight out of the decoder (the decode
655    /// reveals all positions), so no separate re-encode is needed. Writes each restored shard's
656    /// stripe into the matching `out.originals` / `out.recoveries` column slice.
657    ///
658    /// Every tile decodes with `provided.plan`.
659    fn recover_all_into(
660        k: usize,
661        m: usize,
662        range: Range<usize>,
663        provided: Provided<'_>,
664        missing: Missing<'_>,
665        mut out: StripeOut<'_>,
666    ) -> Result<(), Error> {
667        if range.is_empty() {
668            return Ok(());
669        }
670        let offset = range.start;
671        let mut tiles = tiles(k, m, range).peekable();
672        let mut len = tiles.peek().expect("a nonempty stripe has a tile").len();
673        let mut decoder = Cached::take(
674            &CACHED_DECODER,
675            || Decoder::new(k, m, len),
676            |dec| dec.reset(k, m, len),
677        )
678        .map_err(Error::ReedSolomon)?;
679
680        // Dropping a decode result clears the received shards, so a tile of the configured length
681        // reuses the decoder as is and a tile of another length resets it. The plan does not
682        // depend on shard length, so every tile shares it.
683        for tile in tiles {
684            let local = tile.start - offset..tile.end - offset;
685            if tile.len() != len {
686                len = tile.len();
687                decoder.reset(k, m, len).map_err(Error::ReedSolomon)?;
688            }
689
690            for (idx, shard) in provided.originals {
691                decoder
692                    .add_original_shard(*idx, &shard[tile.clone()])
693                    .map_err(Error::ReedSolomon)?;
694            }
695            for (idx, shard) in provided.recoveries {
696                decoder
697                    .add_recovery_shard(*idx, &shard[tile.clone()])
698                    .map_err(Error::ReedSolomon)?;
699            }
700            let decoding = decoder
701                .decode_with_recovery_plan(provided.plan)
702                .map_err(Error::ReedSolomon)?
703                .expect("decode runs only when an original is missing");
704
705            for (slot, &idx) in out.originals.iter_mut().zip(missing.originals) {
706                let shard = decoding.original(idx).ok_or(Error::Inconsistent)?;
707                slot[local.clone()].copy_from_slice(shard);
708            }
709            for (slot, &idx) in out.recoveries.iter_mut().zip(missing.recoveries) {
710                let shard = decoding.recovery(idx).ok_or(Error::Inconsistent)?;
711                slot[local.clone()].copy_from_slice(shard);
712            }
713        }
714        Ok(())
715    }
716
717    /// Encode the recovery shards for a single stripe, writing each recovery shard's stripe
718    /// into the matching `out` column slice (one slot per recovery shard, in index order).
719    pub(super) fn encode_recovery_into(
720        k: usize,
721        m: usize,
722        range: Range<usize>,
723        originals: &[impl AsRef<[u8]>],
724        mut out: Vec<&mut [u8]>,
725    ) -> Result<(), Error> {
726        if range.is_empty() {
727            return Ok(());
728        }
729        let offset = range.start;
730        let mut tiles = tiles(k, m, range).peekable();
731        let mut len = tiles.peek().expect("a nonempty stripe has a tile").len();
732        let mut encoder = Cached::take(
733            &CACHED_ENCODER,
734            || Encoder::new(k, m, len),
735            |enc| enc.reset(k, m, len),
736        )
737        .map_err(Error::ReedSolomon)?;
738
739        // Dropping an encode result clears the added originals, so a tile of the configured length
740        // reuses the encoder as is and a tile of another length resets it.
741        for tile in tiles {
742            let local = tile.start - offset..tile.end - offset;
743            if tile.len() != len {
744                len = tile.len();
745                encoder.reset(k, m, len).map_err(Error::ReedSolomon)?;
746            }
747
748            for shard in originals.iter().take(k) {
749                let shard = shard.as_ref();
750                encoder
751                    .add_original_shard(&shard[tile.clone()])
752                    .map_err(Error::ReedSolomon)?;
753            }
754            let encoding = encoder.encode().map_err(Error::ReedSolomon)?;
755            for (slot, shard) in out.iter_mut().zip(encoding.recovery_iter()) {
756                slot[local.clone()].copy_from_slice(shard);
757            }
758        }
759        Ok(())
760    }
761
762    /// Decode when all `k` originals are present: re-encode the recovery shards (recovery with
763    /// missing originals uses [`decode_reveal`]), confirm any provided recovery shards
764    /// match the canonical re-encode, and verify the rebuilt commitment against `ctx.root`.
765    pub(super) fn decode<'a, H: Hasher, S: Strategy>(
766        ctx: &DecodeCtx<'_, H, S>,
767        batches: Batches<'_, S>,
768        shard_digests: Vec<Option<H::Digest>>,
769        provided_originals: Vec<(usize, &'a [u8])>,
770        provided_recoveries: Vec<(usize, &'a [u8])>,
771    ) -> Result<Vec<u8>, Error> {
772        let &DecodeCtx {
773            k, m, shard_len, ..
774        } = ctx;
775
776        let mut original_refs: Vec<&[u8]> = vec![&[]; k];
777        for &(idx, shard) in &provided_originals {
778            original_refs[idx] = shard;
779        }
780
781        // Re-encode all recovery shards from the originals, one stripe per task.
782        let mut recovery_buf = vec![0u8; m * shard_len];
783        batches.try_map_collect_vec(
784            |ranges| {
785                let ranges = byte_ranges(shard_len, ranges);
786                let groups = stripe_columns(&mut recovery_buf, shard_len, &ranges);
787                ranges.into_iter().zip(groups)
788            },
789            |(range, out)| encode_recovery_into(k, m, range, &original_refs, out),
790        )?;
791        let recovery_refs: Vec<&[u8]> = recovery_buf.chunks_exact(shard_len).collect();
792        verify_reencoded::<H, S>(
793            ctx,
794            shard_digests,
795            &original_refs,
796            &recovery_refs,
797            &provided_recoveries,
798        )
799    }
800
801    /// Decode when an original is missing: recover the missing originals AND read the missing
802    /// recoveries straight out of the same decode (the decode reveals every position), so no
803    /// separate re-encode is needed. The caller feeds exactly `k` shards (surplus recoveries
804    /// were trimmed and their digests cleared), so the trimmed positions are reconstructed
805    /// here and bound by the commitment root check like any other missing shard. No
806    /// provided-recovery comparison is needed: every reconstructed shard is the unique RS
807    /// output for the `k` inputs, and the root check alone binds it to the commitment.
808    pub(super) fn decode_reveal<'a, H: Hasher, S: Strategy>(
809        ctx: &DecodeCtx<'_, H, S>,
810        batches: Batches<'_, S>,
811        shard_digests: Vec<Option<H::Digest>>,
812        provided_originals: Vec<(usize, &'a [u8])>,
813        provided_recoveries: Vec<(usize, &'a [u8])>,
814    ) -> Result<Vec<u8>, Error> {
815        let &DecodeCtx {
816            k, m, shard_len, ..
817        } = ctx;
818
819        let missing_originals = shard_digests
820            .iter()
821            .take(k)
822            .enumerate()
823            .filter_map(|(i, digest)| digest.is_none().then_some(i))
824            .collect::<Vec<_>>();
825        let missing_recoveries = shard_digests
826            .iter()
827            .skip(k)
828            .enumerate()
829            .filter_map(|(i, digest)| digest.is_none().then_some(i))
830            .collect::<Vec<_>>();
831
832        let mut restored_originals = vec![0u8; missing_originals.len() * shard_len];
833        let mut restored_recoveries = vec![0u8; missing_recoveries.len() * shard_len];
834
835        // Erasure coefficients depend only on the shard counts and provided indices, so every
836        // stripe shares one plan.
837        let plan = Plan::new(
838            k,
839            m,
840            provided_originals.iter().map(|&(index, _)| index),
841            provided_recoveries.iter().map(|&(index, _)| index),
842        )
843        .map_err(Error::ReedSolomon)?;
844        let provided = Provided {
845            originals: &provided_originals,
846            recoveries: &provided_recoveries,
847            plan: &plan,
848        };
849        let missing = Missing {
850            originals: &missing_originals,
851            recoveries: &missing_recoveries,
852        };
853        batches.try_map_collect_vec(
854            |ranges| {
855                let ranges = byte_ranges(shard_len, ranges);
856                let original_groups = stripe_columns(&mut restored_originals, shard_len, &ranges);
857                let recovery_groups = stripe_columns(&mut restored_recoveries, shard_len, &ranges);
858                ranges
859                    .into_iter()
860                    .zip(original_groups.into_iter().zip(recovery_groups))
861                    .map(|(range, (originals, recoveries))| {
862                        (
863                            range,
864                            StripeOut {
865                                originals,
866                                recoveries,
867                            },
868                        )
869                    })
870            },
871            |(range, out)| recover_all_into(k, m, range, provided, missing, out),
872        )?;
873
874        let mut original_refs: Vec<&[u8]> = vec![&[]; k];
875        for &(idx, shard) in &provided_originals {
876            original_refs[idx] = shard;
877        }
878        for (pos, idx) in missing_originals.iter().enumerate() {
879            let start = pos * shard_len;
880            original_refs[*idx] = &restored_originals[start..start + shard_len];
881        }
882        let mut recovery_refs: Vec<&[u8]> = vec![&[]; m];
883        for (pos, idx) in missing_recoveries.iter().enumerate() {
884            let start = pos * shard_len;
885            recovery_refs[*idx] = &restored_recoveries[start..start + shard_len];
886        }
887
888        if provided_originals.is_empty() {
889            // Supplied originals fill their digest slots. With none supplied,
890            // missing_originals covers 0..k in order, so the owned buffer is in shard
891            // order. Verify the complete codeword before moving its payload.
892            let data_len = validate_data_layout(&original_refs, shard_len)?;
893            verify_commitment::<H, S>(ctx, shard_digests, &original_refs, &recovery_refs)?;
894            restored_originals.copy_within(u32::SIZE..u32::SIZE + data_len, 0);
895            restored_originals.truncate(data_len);
896            Ok(restored_originals)
897        } else {
898            verify_root::<H, S>(ctx, shard_digests, &original_refs, &recovery_refs)
899        }
900    }
901}
902
903/// Extract and validate the payload from borrowed original shards, then verify the commitment
904/// to the complete codeword.
905fn verify_root<H: Hasher, S: Strategy>(
906    ctx: &DecodeCtx<'_, H, S>,
907    shard_digests: Vec<Option<H::Digest>>,
908    originals: &[&[u8]],
909    recoveries: &[&[u8]],
910) -> Result<Vec<u8>, Error> {
911    let data = extract_data(originals, ctx.k, ctx.shard_len)?;
912    verify_commitment::<H, S>(ctx, shard_digests, originals, recoveries)?;
913    Ok(data)
914}
915
916/// Hash reconstructed shards and verify the commitment to the complete codeword.
917fn verify_commitment<H: Hasher, S: Strategy>(
918    ctx: &DecodeCtx<'_, H, S>,
919    mut shard_digests: Vec<Option<H::Digest>>,
920    originals: &[&[u8]],
921    recoveries: &[&[u8]],
922) -> Result<(), Error> {
923    let &DecodeCtx {
924        n,
925        k,
926        root,
927        strategy,
928        ..
929    } = ctx;
930    let (missing_indices, missing_payloads): (Vec<_>, Vec<_>) = shard_digests
931        .iter()
932        .enumerate()
933        .filter(|(_, digest)| digest.is_none())
934        .map(|(i, _)| {
935            (
936                i,
937                if i < k {
938                    originals[i]
939                } else {
940                    recoveries[i - k]
941                },
942            )
943        })
944        .unzip();
945    for (i, digest) in missing_indices
946        .into_iter()
947        .zip(H::hash_many(&missing_payloads, strategy))
948    {
949        shard_digests[i] = Some(digest);
950    }
951
952    let mut builder = Builder::<H>::new(n);
953    shard_digests
954        .into_iter()
955        .map(|digest| digest.expect("digest must be present for every shard"))
956        .for_each(|digest| {
957            builder.add(&digest);
958        });
959    let tree = builder.build();
960    if tree.root() != *root {
961        return Err(Error::Inconsistent);
962    }
963
964    Ok(())
965}
966
967/// All-originals verification: confirm any provided recovery shards match the canonical re-encode
968/// `recoveries` before their digests are trusted, then verify the commitment via [`verify_root`].
969/// This compare is what rejects a malicious encoder's non-canonical recovery shard when every
970/// original is present; with an original missing, the decode binds the recoveries instead.
971fn verify_reencoded<H: Hasher, S: Strategy>(
972    ctx: &DecodeCtx<'_, H, S>,
973    shard_digests: Vec<Option<H::Digest>>,
974    originals: &[&[u8]],
975    recoveries: &[&[u8]],
976    provided_recoveries: &[(usize, &[u8])],
977) -> Result<Vec<u8>, Error> {
978    // Provided originals are already bound to the commitment by their checked digests.
979    for &(idx, shard) in provided_recoveries {
980        if shard != recoveries[idx] {
981            return Err(Error::Inconsistent);
982        }
983    }
984    verify_root::<H, S>(ctx, shard_digests, originals, recoveries)
985}
986
987/// Decode data from a set of [`CheckedChunk`]s.
988///
989/// It is assumed that all chunks have already been verified against the given root using
990/// [`Scheme::check`].
991///
992/// # Parameters
993///
994/// - `total`: The total number of chunks to generate.
995/// - `min`: The minimum number of chunks required to decode the data.
996/// - `root`: The root of the [`bmt`].
997/// - `chunks`: [`CheckedChunk`]s of encoded data (that can be proven against `root`)
998///
999/// # Returns
1000///
1001/// - `data`: The decoded data.
1002fn decode<'a, H: Hasher, S: Strategy>(
1003    total: u16,
1004    min: u16,
1005    root: &H::Digest,
1006    chunks: impl Iterator<Item = &'a CheckedChunk<H>>,
1007    strategy: &S,
1008) -> Result<Vec<u8>, Error> {
1009    // Validate parameters
1010    assert!(total > min);
1011    assert!(min > 0);
1012    let n = total as usize;
1013    let k = min as usize;
1014    let m = n - k;
1015
1016    // Reject unsupported shard counts before allocating any buffer.
1017    validate_counts(k, m)?;
1018    let mut chunks = chunks.peekable();
1019    let Some(first) = chunks.peek() else {
1020        return Err(Error::NotEnoughChunks);
1021    };
1022
1023    // Process checked chunks
1024    let shard_len = first.shard.len();
1025    if !codeword_fits(n, shard_len) {
1026        return Err(RsError::InvalidShardSize {
1027            shard_bytes: shard_len,
1028        }
1029        .into());
1030    }
1031    let mut shard_digests: Vec<Option<H::Digest>> = vec![None; n];
1032    let mut provided_originals: Vec<(usize, &[u8])> = Vec::new();
1033    let mut provided_recoveries: Vec<(usize, &[u8])> = Vec::new();
1034    let mut provided = 0usize;
1035    for chunk in chunks {
1036        provided += 1;
1037        if &chunk.root != root {
1038            return Err(Error::CommitmentMismatch);
1039        }
1040
1041        // Every shard must share the first shard's width. The striped decode path slices
1042        // each shard by stripe range, so a wrong-width shard would otherwise panic.
1043        if chunk.shard.len() != shard_len {
1044            return Err(Error::Inconsistent);
1045        }
1046
1047        // Check for duplicate index
1048        let index = chunk.index;
1049        if index >= total {
1050            return Err(Error::InvalidIndex(index));
1051        }
1052        let digest_slot = &mut shard_digests[index as usize];
1053        if digest_slot.is_some() {
1054            return Err(Error::DuplicateIndex(index));
1055        }
1056
1057        // Retain the checked digest and split provided bytes by shard type
1058        *digest_slot = Some(chunk.digest);
1059        if index < min {
1060            provided_originals.push((index as usize, chunk.shard.as_ref()));
1061        } else {
1062            provided_recoveries.push((index as usize - k, chunk.shard.as_ref()));
1063        }
1064    }
1065    if provided < k {
1066        return Err(Error::NotEnoughChunks);
1067    }
1068
1069    // Feed the Reed-Solomon decoder exactly `k` shards. Only `k` original positions exist, so
1070    // originals are never redundant; when an original is missing, trim surplus recovery shards
1071    // beyond the `k - originals` needed to decode. Their checked digests are dropped and their
1072    // bytes forgotten, so the reconstruction rebuilds those positions and the commitment root
1073    // check binds them. This verifies extra shards without over-feeding the decoder or a
1074    // separate canonical re-encode/compare.
1075    let recovery_needed = provided_originals.len() < k;
1076    if recovery_needed {
1077        let keep = k - provided_originals.len();
1078        for &(idx, _) in &provided_recoveries[keep..] {
1079            shard_digests[k + idx] = None;
1080        }
1081        provided_recoveries.truncate(keep);
1082    }
1083
1084    let ctx = DecodeCtx {
1085        n,
1086        k,
1087        m,
1088        shard_len,
1089        root,
1090        strategy,
1091    };
1092
1093    // Distinct call sites keep the reconstruction algorithms' cost estimates separate.
1094    if recovery_needed {
1095        strategy.try_run_batches(
1096            shard_len / SHARD_CHUNK_BYTES,
1097            NZUsize!(MIN_STRIPE_BYTES / SHARD_CHUNK_BYTES),
1098            SHARD_CHUNK_BYTES * n,
1099            |batches| {
1100                striped::decode_reveal::<H, S>(
1101                    &ctx,
1102                    batches,
1103                    shard_digests,
1104                    provided_originals,
1105                    provided_recoveries,
1106                )
1107            },
1108        )
1109    } else {
1110        strategy.try_run_batches(
1111            shard_len / SHARD_CHUNK_BYTES,
1112            NZUsize!(MIN_STRIPE_BYTES / SHARD_CHUNK_BYTES),
1113            SHARD_CHUNK_BYTES * n,
1114            |batches| {
1115                striped::decode::<H, S>(
1116                    &ctx,
1117                    batches,
1118                    shard_digests,
1119                    provided_originals,
1120                    provided_recoveries,
1121                )
1122            },
1123        )
1124    }
1125}
1126
1127/// A SIMD-optimized Reed-Solomon coder that emits chunks that can be proven against a [`bmt`].
1128///
1129/// # Behavior
1130///
1131/// The encoder takes input data, splits it into `k` data shards, and generates `m` recovery
1132/// shards using [Reed-Solomon encoding](https://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction).
1133/// All `n = k + m` shards are then used to build a [`bmt`], producing a single root hash. Each shard
1134/// is packaged as a chunk containing the shard data, its index, and a Merkle multi-proof against the [`bmt`] root.
1135///
1136/// ## Encoding
1137///
1138/// ```text
1139///               +--------------------------------------+
1140///               |         Original Data (Bytes)        |
1141///               +--------------------------------------+
1142///                                  |
1143///                                  v
1144///               +--------------------------------------+
1145///               | [Length Prefix | Original Data...]   |
1146///               +--------------------------------------+
1147///                                  |
1148///                                  v
1149///              +----------+ +----------+    +-----------+
1150///              |  Shard 0 | |  Shard 1 | .. | Shard k-1 |  (Data Shards)
1151///              +----------+ +----------+    +-----------+
1152///                     |            |             |
1153///                     |            |             |
1154///                     +------------+-------------+
1155///                                  |
1156///                                  v
1157///                        +------------------+
1158///                        | Reed-Solomon     |
1159///                        | Encoder (k, m)   |
1160///                        +------------------+
1161///                                  |
1162///                                  v
1163///              +----------+ +----------+    +-----------+
1164///              |  Shard k | | Shard k+1| .. | Shard n-1 |  (Recovery Shards)
1165///              +----------+ +----------+    +-----------+
1166/// ```
1167///
1168/// ## Merkle Tree Construction
1169///
1170/// All `n` shards (data and recovery) are hashed and used as leaves to build a [`bmt`].
1171///
1172/// ```text
1173/// Shards:    [Shard 0, Shard 1, ..., Shard n-1]
1174///             |        |              |
1175///             v        v              v
1176/// Hashes:    [H(S_0), H(S_1), ..., H(S_n-1)]
1177///             \       / \       /
1178///              \     /   \     /
1179///               +---+     +---+
1180///                 |         |
1181///                 \         /
1182///                  \       /
1183///                   +-----+
1184///                      |
1185///                      v
1186///                +----------+
1187///                |   Root   |
1188///                +----------+
1189/// ```
1190///
1191/// The final output is the [`bmt`] root and a set of `n` chunks.
1192///
1193/// `(Root, [Chunk 0, Chunk 1, ..., Chunk n-1])`
1194///
1195/// Each chunk contains:
1196/// - `shard`: The shard data (original or recovery).
1197/// - `index`: The shard's original index (0 to n-1).
1198/// - `proof`: A Merkle multi-proof of the shard's inclusion in the [`bmt`].
1199///
1200/// ## Decoding and Verification
1201///
1202/// The decoder requires any `k` chunks to reconstruct the original data.
1203/// 1. Each chunk's Merkle multi-proof is verified against the [`bmt`] root.
1204/// 2. Exactly `k` shards are fed to the Reed-Solomon decoder (any surplus chunks are
1205///    redundant), which reconstructs the full codeword: the missing data shards and the
1206///    missing recovery shards both come out of the same decode. When all `k` data shards are
1207///    already present there is nothing to decode, so the recovery shards are re-encoded
1208///    instead.
1209/// 3. To ensure consistency, a new [`bmt`] root is generated over the full reconstructed
1210///    codeword. This new root MUST match the original [`bmt`] root. This prevents attacks
1211///    where an adversary provides a valid set of chunks that decode to different data, and
1212///    binds any surplus chunks (which are rebuilt from the reconstruction, not trusted).
1213/// 4. If the roots match, the original data is extracted from the reconstructed data shards.
1214///
1215/// Decoding is all-or-nothing per commitment. A successful decode proves that the committed
1216/// codeword is the canonical encoding of the returned data under this [`Config`], so every set
1217/// of at least `k` shards checked against that commitment decodes to the same data. A
1218/// commitment that is not the canonical encoding of any payload fails for every such set.
1219///
1220/// ## Supported Configurations
1221///
1222/// With `k = minimum_shards` and `m = extra_shards`, the total `k + m` must fit in a `u16`, and
1223/// larger totals return [`Error::TooManyTotalShards`]. The coder works over GF(2^16) and also
1224/// requires `min(k, m).next_power_of_two() + max(k, m)` to be at most 65536. Totals that fit but
1225/// fail this condition return [`Error::ReedSolomon`] carrying
1226/// [`UnsupportedShardCount`](RsError::UnsupportedShardCount).
1227///
1228/// The canonical shard width is the payload length plus its four-byte prefix, divided by `k`
1229/// and rounded up to an even number of bytes. Encoding returns [`Error::InvalidDataLength`] when
1230/// the payload length or that width exceeds `u32::MAX` (with `k` of one, the five largest payload
1231/// lengths) or when `k + m` shards of that width exceed [`MAX_ALLOCATION_BYTES`].
1232///
1233/// ## Resource Usage
1234///
1235/// Encoding and decoding hold all `k + m` shards of the codeword in memory, along with the Merkle
1236/// tree over those shards and a bounded per-thread coding workspace. At the canonical width, the
1237/// codeword is about `(k + m) / k` times the payload. Encoding also returns a Merkle proof for
1238/// every shard.
1239///
1240/// Decoding reconstructs the `m` shards beyond the `k` it decodes from and rebuilds the
1241/// commitment over all of them, so even the minimum `k` shards cost the full codeword. With `k`
1242/// of one and `m` of 65,534, decoding a single shard reconstructs and hashes 65,534 shards of
1243/// its width.
1244///
1245/// Callers that accept a [`Config`] or shards from an untrusted source must bound the shard counts
1246/// and the maximum payload length before encoding or decoding. Reading a [`Scheme::Shard`] takes
1247/// the same maximum and rejects any shard wider than the canonical width of a maximum-length
1248/// payload.
1249pub struct ReedSolomon<H> {
1250    _marker: PhantomData<H>,
1251}
1252
1253impl<H> Clone for ReedSolomon<H> {
1254    fn clone(&self) -> Self {
1255        *self
1256    }
1257}
1258
1259impl<H> Copy for ReedSolomon<H> {}
1260
1261impl<H> std::fmt::Debug for ReedSolomon<H> {
1262    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1263        f.debug_struct("ReedSolomon").finish()
1264    }
1265}
1266
1267impl<H: Hasher> Scheme for ReedSolomon<H> {
1268    type Commitment = H::Digest;
1269    type Shard = Chunk<H::Digest>;
1270    type CheckedShard = CheckedChunk<H>;
1271    type Error = Error;
1272
1273    /// Returns an upper bound on the encoded size of any chunk [`Scheme::encode`] produces under
1274    /// `config` from at most `data` bytes.
1275    ///
1276    /// # Panics
1277    ///
1278    /// Panics if `config` cannot encode `data` bytes: its total exceeds `u16::MAX` shards, the
1279    /// backend does not support its shard counts, `data` or the shard width exceeds `u32::MAX`,
1280    /// or the codeword exceeds [`MAX_ALLOCATION_BYTES`].
1281    fn max_shard_size(config: &Config, data: usize) -> usize {
1282        // Reject the shard counts, payload lengths, and codeword sizes that `encode` rejects.
1283        let total = total_shards(config).expect("total shards must fit in u16");
1284        let n = usize::from(total);
1285        let k = usize::from(config.minimum_shards.get());
1286        validate_counts(k, n - k).expect("backend must support the shard counts");
1287        u32::try_from(data).expect("data must fit the u32 length prefix");
1288        let len = canonical_shard_len(data, k);
1289        assert!(
1290            codeword_fits(n, len),
1291            "codeword must fit the allocation bound"
1292        );
1293
1294        // Every chunk carries a shard of the canonical width, and `Chunk::max_encode_size`
1295        // rejects a width too long for its length prefix, as `encode` does. Chunk 0 has a sibling
1296        // at every level of the tree, so its proof is the longest.
1297        let siblings: usize = Widen::widen(u32::from(total).next_power_of_two().trailing_zeros());
1298        Chunk::<H::Digest>::max_encode_size(len, siblings)
1299    }
1300
1301    fn encode(
1302        config: &Config,
1303        data: impl bytes::Buf,
1304        strategy: &impl Strategy,
1305    ) -> Result<(Self::Commitment, Vec<Self::Shard>), Self::Error> {
1306        encode::<H, _>(
1307            total_shards(config)?,
1308            config.minimum_shards.get(),
1309            data,
1310            strategy,
1311        )
1312    }
1313
1314    fn check(
1315        config: &Config,
1316        commitment: &Self::Commitment,
1317        index: u16,
1318        shard: &Self::Shard,
1319        strategy: &impl Strategy,
1320    ) -> Result<Self::CheckedShard, Self::Error> {
1321        let total = total_shards(config)?;
1322        check_chunk::<H>(total, commitment, index, shard, || {
1323            H::hash_across(&[&shard.shard], strategy)
1324        })
1325    }
1326
1327    fn check_many(
1328        config: &Config,
1329        commitment: &Self::Commitment,
1330        shards: &[(u16, &Self::Shard)],
1331        strategy: &impl Strategy,
1332    ) -> Vec<Result<Self::CheckedShard, Self::Error>> {
1333        let Ok(total) = total_shards(config) else {
1334            return shards
1335                .iter()
1336                .map(|&(index, shard)| Self::check(config, commitment, index, shard, strategy))
1337                .collect();
1338        };
1339
1340        // Per-shard checks reject invalid metadata before hashing and let the strategy
1341        // distribute uneven payloads independently.
1342        let width = shards.first().map_or(0, |(_, shard)| shard.shard.len());
1343        if shards.iter().any(|&(index, shard)| {
1344            shard.shard.len() != width || check_metadata(total, index, shard).is_err()
1345        }) {
1346            return strategy.map_collect_vec(shards, |&(index, shard)| {
1347                Self::check(config, commitment, index, shard, strategy)
1348            });
1349        }
1350
1351        let payloads = shards
1352            .iter()
1353            .map(|(_, shard)| shard.shard.as_ref())
1354            .collect::<Vec<_>>();
1355        let digests = H::hash_many(&payloads, strategy);
1356        strategy.map_collect_vec(
1357            shards.iter().copied().zip(digests),
1358            |((index, shard), digest)| check_chunk::<H>(total, commitment, index, shard, || digest),
1359        )
1360    }
1361
1362    fn decode<'a>(
1363        config: &Config,
1364        commitment: &Self::Commitment,
1365        shards: impl Iterator<Item = &'a Self::CheckedShard>,
1366        strategy: &impl Strategy,
1367    ) -> Result<Vec<u8>, Self::Error> {
1368        decode::<H, _>(
1369            total_shards(config)?,
1370            config.minimum_shards.get(),
1371            commitment,
1372            shards,
1373            strategy,
1374        )
1375    }
1376}
1377
1378#[cfg(test)]
1379mod tests {
1380    use super::*;
1381    use bytes::Buf as _;
1382    use commonware_codec::{Decode as _, Encode, MaxEncodeSize, varint::UInt};
1383    use commonware_cryptography::{Sha256, sha256::Digest as Sha256Digest};
1384    use commonware_invariants::minifuzz;
1385    use commonware_parallel::{
1386        Rayon, Sequential,
1387        mocks::{Explorer, explore, inline},
1388    };
1389    use commonware_runtime::iobuf::EncodeExt;
1390    use commonware_utils::{NZU16, NZUsize};
1391    use rstest::rstest;
1392    use std::{any::TypeId, cell::RefCell};
1393
1394    type RS = ReedSolomon<Sha256>;
1395    type InstrumentedRS = ReedSolomon<InstrumentedSha256>;
1396    const STRATEGY: Sequential = Sequential;
1397    const FUZZ_MAX_MIN_SHARDS: u16 = 8;
1398    const FUZZ_MAX_EXTRA_SHARDS: u16 = 8;
1399    const FUZZ_MAX_DATA_LEN: usize = 256;
1400    const FUZZ_MAX_EXTRA_SHARD_WIDTH: usize = 16;
1401
1402    std::thread_local! {
1403        static HASH_MANY_CALLS: RefCell<Vec<Vec<Vec<u8>>>> = const { RefCell::new(Vec::new()) };
1404        static HASH_INPUTS: RefCell<Vec<Vec<u8>>> = const { RefCell::new(Vec::new()) };
1405    }
1406
1407    #[derive(Default)]
1408    struct InstrumentedSha256(Sha256);
1409
1410    impl Hasher for InstrumentedSha256 {
1411        type Digest = <Sha256 as Hasher>::Digest;
1412
1413        fn hash(parts: &[&[u8]]) -> Self::Digest {
1414            if let [message] = parts {
1415                HASH_INPUTS.with(|inputs| inputs.borrow_mut().push(message.to_vec()));
1416            }
1417            Sha256::hash(parts)
1418        }
1419
1420        fn hash_pair(
1421            left: &[&[u8]],
1422            right: &[&[u8]],
1423            strategy: &impl Strategy,
1424        ) -> (Self::Digest, Self::Digest) {
1425            Sha256::hash_pair(left, right, strategy)
1426        }
1427
1428        fn hash_many<M: AsRef<[u8]> + Sync>(
1429            messages: &[M],
1430            _strategy: &impl Strategy,
1431        ) -> Vec<Self::Digest> {
1432            HASH_INPUTS.with(|inputs| {
1433                inputs
1434                    .borrow_mut()
1435                    .extend(messages.iter().map(|message| message.as_ref().to_vec()));
1436            });
1437            HASH_MANY_CALLS.with(|calls| {
1438                calls.borrow_mut().push(
1439                    messages
1440                        .iter()
1441                        .map(|message| message.as_ref().to_vec())
1442                        .collect(),
1443                );
1444            });
1445            messages
1446                .iter()
1447                .map(|message| Sha256::hash(&[message.as_ref()]))
1448                .collect()
1449        }
1450
1451        fn update(&mut self, bytes: &[u8]) -> &mut Self {
1452            self.0.update(bytes);
1453            self
1454        }
1455
1456        fn finalize(self) -> (Self, Self::Digest) {
1457            let (hasher, digest) = self.0.finalize();
1458            (Self(hasher), digest)
1459        }
1460    }
1461
1462    fn reset_hash_many_calls() {
1463        HASH_INPUTS.with(|inputs| inputs.borrow_mut().clear());
1464        HASH_MANY_CALLS.with(|calls| calls.borrow_mut().clear());
1465    }
1466
1467    fn take_hash_many_calls() -> Vec<Vec<Vec<u8>>> {
1468        HASH_MANY_CALLS.with(|calls| std::mem::take(&mut *calls.borrow_mut()))
1469    }
1470
1471    fn checked(
1472        root: <Sha256 as Hasher>::Digest,
1473        chunk: Chunk<<Sha256 as Hasher>::Digest>,
1474    ) -> CheckedChunk<Sha256> {
1475        let Chunk { shard, index, .. } = chunk;
1476        let digest = Sha256::hash(&[&shard]);
1477        CheckedChunk::new(root, shard, index, digest)
1478    }
1479
1480    fn build_chunks(
1481        shards: &[Vec<u8>],
1482    ) -> (
1483        <Sha256 as Hasher>::Digest,
1484        Vec<Chunk<<Sha256 as Hasher>::Digest>>,
1485    ) {
1486        let mut builder = Builder::<Sha256>::new(shards.len());
1487        for shard in shards {
1488            builder.add(&Sha256::hash(&[shard]));
1489        }
1490        let tree = builder.build();
1491        let root = tree.root();
1492        let chunks = shards
1493            .iter()
1494            .enumerate()
1495            .map(|(i, shard)| {
1496                let proof = tree.proof(i as u32).unwrap();
1497                Chunk::new(shard.clone().into(), i as u16, proof)
1498            })
1499            .collect();
1500
1501        (root, chunks)
1502    }
1503
1504    /// Decoding trusts a checked shard's stored digest, so two hashers with the same digest type
1505    /// must have distinct checked shard types.
1506    #[test]
1507    fn test_checked_shard_type_is_bound_to_hasher() {
1508        assert_eq!(
1509            TypeId::of::<<RS as Scheme>::Commitment>(),
1510            TypeId::of::<<InstrumentedRS as Scheme>::Commitment>()
1511        );
1512        assert_ne!(
1513            TypeId::of::<<RS as Scheme>::CheckedShard>(),
1514            TypeId::of::<<InstrumentedRS as Scheme>::CheckedShard>()
1515        );
1516    }
1517
1518    #[test]
1519    fn test_check_many_batches_payloads_in_input_order() {
1520        let config = Config {
1521            minimum_shards: NZU16!(4),
1522            extra_shards: NZU16!(4),
1523        };
1524        let (root, chunks) =
1525            RS::encode(&config, b"ordered batch checking".as_slice(), &STRATEGY).unwrap();
1526        let requested = [3u16, 0, 6, 2];
1527        let shards = requested
1528            .iter()
1529            .map(|&index| (index, &chunks[usize::from(index)]))
1530            .collect::<Vec<_>>();
1531        let expected_payloads = requested
1532            .iter()
1533            .map(|&index| chunks[usize::from(index)].shard.to_vec())
1534            .collect::<Vec<_>>();
1535
1536        reset_hash_many_calls();
1537        let checked = InstrumentedRS::check_many(&config, &root, &shards, &STRATEGY)
1538            .into_iter()
1539            .collect::<Result<Vec<_>, _>>()
1540            .unwrap();
1541
1542        assert_eq!(take_hash_many_calls(), vec![expected_payloads]);
1543        assert_eq!(
1544            checked.iter().map(|chunk| chunk.index).collect::<Vec<_>>(),
1545            requested
1546        );
1547        for (checked, &index) in checked.iter().zip(&requested) {
1548            let scalar = InstrumentedRS::check(
1549                &config,
1550                &root,
1551                index,
1552                &chunks[usize::from(index)],
1553                &STRATEGY,
1554            )
1555            .unwrap();
1556            assert_eq!(checked, &scalar);
1557        }
1558    }
1559
1560    #[test]
1561    fn test_instrumented_encode_matches_sha256_commitment_and_wire() {
1562        let config = Config {
1563            minimum_shards: NZU16!(5),
1564            extra_shards: NZU16!(7),
1565        };
1566        let data = (0..4096).map(|i| i as u8).collect::<Vec<_>>();
1567        let (expected_root, expected_chunks) =
1568            RS::encode(&config, data.as_slice(), &STRATEGY).unwrap();
1569
1570        reset_hash_many_calls();
1571        let (actual_root, actual_chunks) =
1572            InstrumentedRS::encode(&config, data.as_slice(), &STRATEGY).unwrap();
1573
1574        assert_eq!(actual_root, expected_root);
1575        assert_eq!(actual_chunks, expected_chunks);
1576        assert_eq!(
1577            actual_chunks.iter().map(Encode::encode).collect::<Vec<_>>(),
1578            expected_chunks
1579                .iter()
1580                .map(Encode::encode)
1581                .collect::<Vec<_>>()
1582        );
1583        let calls = take_hash_many_calls();
1584        assert_eq!(calls.len(), 1);
1585        assert_eq!(calls[0].len(), config.total_shards() as usize);
1586    }
1587
1588    #[test]
1589    fn test_decode_reuses_checked_digests_and_rehashes_trimmed_surplus() {
1590        let config = Config {
1591            minimum_shards: NZU16!(3),
1592            extra_shards: NZU16!(3),
1593        };
1594        let data = (0..1024).map(|i| (i * 7) as u8).collect::<Vec<_>>();
1595        let (root, chunks) = InstrumentedRS::encode(&config, data.as_slice(), &STRATEGY).unwrap();
1596        let checked = InstrumentedRS::check_many(
1597            &config,
1598            &root,
1599            &(0..6u16)
1600                .map(|index| (index, &chunks[usize::from(index)]))
1601                .collect::<Vec<_>>(),
1602            &STRATEGY,
1603        )
1604        .into_iter()
1605        .collect::<Result<Vec<_>, _>>()
1606        .unwrap();
1607        let selected = [&checked[0], &checked[3], &checked[4], &checked[5]];
1608
1609        reset_hash_many_calls();
1610        let decoded =
1611            InstrumentedRS::decode(&config, &root, selected.into_iter(), &STRATEGY).unwrap();
1612
1613        assert_eq!(decoded, data);
1614        assert_eq!(
1615            take_hash_many_calls(),
1616            vec![vec![
1617                chunks[1].shard.to_vec(),
1618                chunks[2].shard.to_vec(),
1619                chunks[5].shard.to_vec(),
1620            ]]
1621        );
1622    }
1623
1624    #[test]
1625    fn test_check_many_rejects_metadata_before_hashing() {
1626        let config = Config {
1627            minimum_shards: NZU16!(3),
1628            extra_shards: NZU16!(3),
1629        };
1630        let data = (0..192).map(|i| i as u8).collect::<Vec<_>>();
1631        let (root, chunks) = RS::encode(&config, data.as_slice(), &STRATEGY).unwrap();
1632        let mut invalid_index = chunks[1].clone();
1633        invalid_index.index = 5;
1634        invalid_index.shard = vec![0xa1; chunks[1].shard.len()].into();
1635        let mut invalid_leaf_count = chunks[2].clone();
1636        invalid_leaf_count.proof.leaf_count -= 1;
1637        invalid_leaf_count.shard = vec![0xb2; chunks[2].shard.len()].into();
1638        let mut out_of_range = chunks[3].clone();
1639        out_of_range.shard = vec![0xc3; chunks[3].shard.len()].into();
1640        let mut invalid_proof = chunks[4].clone();
1641        invalid_proof.shard = vec![0xd4; chunks[4].shard.len()].into();
1642        let shards = [
1643            (1, &invalid_index),
1644            (0, &chunks[0]),
1645            (2, &invalid_leaf_count),
1646            (6, &out_of_range),
1647            (4, &invalid_proof),
1648        ];
1649        let expected = shards
1650            .iter()
1651            .map(|&(index, shard)| {
1652                format!("{:?}", RS::check(&config, &root, index, shard, &STRATEGY))
1653            })
1654            .collect::<Vec<_>>();
1655
1656        reset_hash_many_calls();
1657        let actual = InstrumentedRS::check_many(&config, &root, &shards, &STRATEGY)
1658            .into_iter()
1659            .map(|result| format!("{result:?}"))
1660            .collect::<Vec<_>>();
1661        assert_eq!(actual, expected);
1662        let inputs = HASH_INPUTS.with(|inputs| std::mem::take(&mut *inputs.borrow_mut()));
1663        for rejected in [&invalid_index, &invalid_leaf_count, &out_of_range] {
1664            assert!(
1665                !inputs
1666                    .iter()
1667                    .any(|input| input.as_slice() == rejected.shard)
1668            );
1669        }
1670        for eligible in [&chunks[0], &invalid_proof] {
1671            assert!(
1672                inputs
1673                    .iter()
1674                    .any(|input| input.as_slice() == eligible.shard)
1675            );
1676        }
1677    }
1678
1679    #[test]
1680    fn test_check_many_checks_mixed_widths_individually() {
1681        let config = Config {
1682            minimum_shards: NZU16!(32),
1683            extra_shards: NZU16!(32),
1684        };
1685        let payloads = (0..64)
1686            .map(|i| vec![i as u8; if i < 8 { 8192 - i * 64 } else { 2 }])
1687            .collect::<Vec<_>>();
1688        let (root, chunks) = build_chunks(&payloads);
1689        let shards = chunks
1690            .iter()
1691            .enumerate()
1692            .map(|(index, chunk)| (index as u16, chunk))
1693            .collect::<Vec<_>>();
1694        let expected = shards
1695            .iter()
1696            .map(|&(index, shard)| {
1697                InstrumentedRS::check(&config, &root, index, shard, &STRATEGY).unwrap()
1698            })
1699            .collect::<Vec<_>>();
1700
1701        // Run the check on the pool's single worker, so the thread-local hash counters see every
1702        // hashing call while the strategy plans eight tasks.
1703        let strategy = Explorer::parallel(&inline(NZUsize!(8)));
1704        let (actual, calls) = strategy
1705            .join(
1706                || {
1707                    reset_hash_many_calls();
1708                    let actual = InstrumentedRS::check_many(&config, &root, &shards, &strategy)
1709                        .into_iter()
1710                        .collect::<Result<Vec<_>, _>>()
1711                        .unwrap();
1712                    (actual, take_hash_many_calls())
1713                },
1714                || (),
1715            )
1716            .0;
1717        assert_eq!(actual, expected);
1718        assert!(calls.is_empty());
1719        assert!(matches!(
1720            InstrumentedRS::decode(&config, &root, actual.iter(), &STRATEGY),
1721            Err(Error::Inconsistent)
1722        ));
1723    }
1724
1725    #[test]
1726    fn test_check_many_matches_scalar_errors() {
1727        let config = Config {
1728            minimum_shards: NZU16!(3),
1729            extra_shards: NZU16!(3),
1730        };
1731        let (root, chunks) =
1732            RS::encode(&config, b"mixed check errors".as_slice(), &STRATEGY).unwrap();
1733        let mut invalid_leaf_count = chunks[1].clone();
1734        invalid_leaf_count.proof.leaf_count -= 1;
1735        let mut invalid_chunk_index = chunks[2].clone();
1736        invalid_chunk_index.index = 4;
1737        let mut invalid_payload = chunks[3].clone();
1738        let mut payload = invalid_payload.shard.to_vec();
1739        payload[0] ^= 0xff;
1740        invalid_payload.shard = payload.into();
1741        let shards = [
1742            (1, &invalid_leaf_count),
1743            (0, &chunks[0]),
1744            (6, &chunks[1]),
1745            (2, &invalid_chunk_index),
1746            (3, &invalid_payload),
1747        ];
1748
1749        let scalar = shards
1750            .iter()
1751            .map(|&(index, shard)| {
1752                format!(
1753                    "{:?}",
1754                    InstrumentedRS::check(&config, &root, index, shard, &STRATEGY)
1755                )
1756            })
1757            .collect::<Vec<_>>();
1758        let batched = InstrumentedRS::check_many(&config, &root, &shards, &STRATEGY)
1759            .into_iter()
1760            .map(|result| format!("{result:?}"))
1761            .collect::<Vec<_>>();
1762        assert_eq!(batched, scalar);
1763
1764        let invalid_config = Config {
1765            minimum_shards: NZU16!(u16::MAX),
1766            extra_shards: NZU16!(1),
1767        };
1768        let scalar = shards
1769            .iter()
1770            .map(|&(index, shard)| {
1771                format!(
1772                    "{:?}",
1773                    InstrumentedRS::check(&invalid_config, &root, index, shard, &STRATEGY)
1774                )
1775            })
1776            .collect::<Vec<_>>();
1777        reset_hash_many_calls();
1778        let batched = InstrumentedRS::check_many(&invalid_config, &root, &shards, &STRATEGY)
1779            .into_iter()
1780            .map(|result| format!("{result:?}"))
1781            .collect::<Vec<_>>();
1782        assert_eq!(batched, scalar);
1783        assert!(take_hash_many_calls().is_empty());
1784    }
1785
1786    fn selected_indices(
1787        u: &mut arbitrary::Unstructured<'_>,
1788        total: u16,
1789        minimum: u16,
1790    ) -> arbitrary::Result<Vec<u16>> {
1791        let to_use = u.int_in_range(minimum..=total)?;
1792        let mut selected = (0..total).collect::<Vec<_>>();
1793        for i in 0..usize::from(to_use) {
1794            let remaining = usize::from(total) - i;
1795            let j = i + u.choose_index(remaining)?;
1796            selected.swap(i, j);
1797        }
1798        selected.truncate(usize::from(to_use));
1799        Ok(selected)
1800    }
1801
1802    fn assert_decode_unique_commitment(
1803        total: u16,
1804        min: u16,
1805        root: <Sha256 as Hasher>::Digest,
1806        chunks: &[Chunk<<Sha256 as Hasher>::Digest>],
1807        selected: &[u16],
1808    ) {
1809        let config = Config {
1810            minimum_shards: NZU16!(min),
1811            extra_shards: NZU16!(total - min),
1812        };
1813        let pieces = selected
1814            .iter()
1815            .map(|&i| RS::check(&config, &root, i, &chunks[usize::from(i)], &STRATEGY).unwrap())
1816            .collect::<Vec<_>>();
1817
1818        let Ok(decoded) = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY) else {
1819            return;
1820        };
1821        let (canonical_root, _) =
1822            encode::<Sha256, _>(total, min, decoded.as_slice(), &STRATEGY).unwrap();
1823        assert_eq!(
1824            root, canonical_root,
1825            "decode accepted a root not produced by canonical encode"
1826        );
1827    }
1828
1829    fn fuzz_arbitrary_codeword(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()> {
1830        let min = u.int_in_range(1..=FUZZ_MAX_MIN_SHARDS)?;
1831        let extra = u.int_in_range(1..=FUZZ_MAX_EXTRA_SHARDS)?;
1832        let total = min + extra;
1833        let k = usize::from(min);
1834        let m = usize::from(extra);
1835
1836        let data_len = u.int_in_range(0..=FUZZ_MAX_DATA_LEN)?;
1837        let data = u.bytes(data_len)?.to_vec();
1838        let canonical = canonical_shard_len(data.len(), k);
1839        let extra_width = u.int_in_range(0..=FUZZ_MAX_EXTRA_SHARD_WIDTH / 2)? * 2;
1840        let shard_len = canonical + extra_width;
1841
1842        let mut padded = vec![0u8; k * shard_len];
1843        padded[..u32::SIZE].copy_from_slice(&(data.len() as u32).to_be_bytes());
1844        padded[u32::SIZE..u32::SIZE + data.len()].copy_from_slice(&data);
1845
1846        let payload_end = u32::SIZE + data.len();
1847        if payload_end < padded.len() && u.int_in_range(0..=3)? == 0 {
1848            let offset = payload_end + u.choose_index(padded.len() - payload_end)?;
1849            padded[offset] ^= u.arbitrary::<u8>()? | 1;
1850        }
1851
1852        let mut encoder = Encoder::new(k, m, shard_len).unwrap();
1853        for shard in padded.chunks(shard_len) {
1854            encoder.add_original_shard(shard).unwrap();
1855        }
1856        let recovery = encoder.encode().unwrap();
1857
1858        let mut shards = padded
1859            .chunks(shard_len)
1860            .map(|shard| shard.to_vec())
1861            .collect::<Vec<_>>();
1862        shards.extend(recovery.recovery_iter().map(|shard| shard.to_vec()));
1863
1864        let (root, chunks) = build_chunks(&shards);
1865        let selected = selected_indices(u, total, min)?;
1866        assert_decode_unique_commitment(total, min, root, &chunks, &selected);
1867
1868        Ok(())
1869    }
1870
1871    fn fuzz_mixed_codeword(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()> {
1872        let min = u.int_in_range(1..=FUZZ_MAX_MIN_SHARDS)?;
1873        let extra = u.int_in_range(1..=FUZZ_MAX_EXTRA_SHARDS)?;
1874        let total = min + extra;
1875
1876        let data_len = u.int_in_range(0..=FUZZ_MAX_DATA_LEN)?;
1877        let data = u.bytes(data_len)?.to_vec();
1878        let (_canonical_root, chunks) =
1879            encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1880        let mut shards = chunks
1881            .iter()
1882            .map(|chunk| chunk.shard.to_vec())
1883            .collect::<Vec<_>>();
1884
1885        let mutated = usize::from(min + u.int_in_range(0..=extra - 1)?);
1886        let offset = u.choose_index(shards[mutated].len())?;
1887        shards[mutated][offset] ^= u.arbitrary::<u8>()? | 1;
1888
1889        let (root, chunks) = build_chunks(&shards);
1890        let mut selected = (0..min).collect::<Vec<_>>();
1891        selected.push(mutated as u16);
1892        assert_decode_unique_commitment(total, min, root, &chunks, &selected);
1893
1894        Ok(())
1895    }
1896
1897    #[test]
1898    fn test_recovery() {
1899        let data = b"Testing recovery pieces";
1900        let total = 8u16;
1901        let min = 3u16;
1902
1903        // Encode the data
1904        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1905
1906        // Use a mix of original and recovery pieces
1907        let pieces: Vec<_> = vec![
1908            checked(root, chunks[0].clone()), // original
1909            checked(root, chunks[4].clone()), // recovery
1910            checked(root, chunks[6].clone()), // recovery
1911        ];
1912
1913        // Try to decode with a mix of original and recovery pieces
1914        let decoded = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY).unwrap();
1915        assert_eq!(decoded, data);
1916    }
1917
1918    #[test]
1919    fn test_not_enough_pieces() {
1920        let data = b"Test insufficient pieces";
1921        let total = 6u16;
1922        let min = 4u16;
1923
1924        // Encode data
1925        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1926
1927        // Try with fewer than min
1928        let pieces: Vec<_> = chunks
1929            .into_iter()
1930            .take(2)
1931            .map(|c| checked(root, c))
1932            .collect();
1933
1934        // Fail to decode
1935        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1936        assert!(matches!(result, Err(Error::NotEnoughChunks)));
1937    }
1938
1939    #[test]
1940    fn test_duplicate_index() {
1941        let data = b"Test duplicate detection";
1942        let total = 5u16;
1943        let min = 3u16;
1944
1945        // Encode data
1946        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1947
1948        // Include duplicate index by cloning the first chunk
1949        let pieces = [
1950            checked(root, chunks[0].clone()),
1951            checked(root, chunks[0].clone()),
1952            checked(root, chunks[1].clone()),
1953        ];
1954
1955        // Fail to decode
1956        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
1957        assert!(matches!(result, Err(Error::DuplicateIndex(0))));
1958    }
1959
1960    #[test]
1961    fn test_invalid_index() {
1962        let data = b"Test invalid index";
1963        let total = 5u16;
1964        let min = 3u16;
1965        let config = Config {
1966            minimum_shards: NZU16!(min),
1967            extra_shards: NZU16!(total - min),
1968        };
1969
1970        // Encode data
1971        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
1972
1973        // Verify all proofs at invalid index
1974        for i in 0..total {
1975            assert!(RS::check(&config, &root, i + 1, &chunks[i as usize], &STRATEGY).is_err());
1976        }
1977    }
1978
1979    #[test]
1980    #[should_panic(expected = "assertion failed: total > min")]
1981    fn test_invalid_total() {
1982        let data = b"Test parameter validation";
1983
1984        // The total shard count must exceed the recovery threshold.
1985        encode::<Sha256, _>(3, 3, data.as_slice(), &STRATEGY).unwrap();
1986    }
1987
1988    #[test]
1989    #[should_panic(expected = "assertion failed: min > 0")]
1990    fn test_invalid_min() {
1991        let data = b"Test parameter validation";
1992
1993        // The recovery threshold must be non-zero.
1994        encode::<Sha256, _>(5, 0, data.as_slice(), &STRATEGY).unwrap();
1995    }
1996
1997    #[test]
1998    fn test_empty_data() {
1999        let data = b"";
2000        let total = 100u16;
2001        let min = 30u16;
2002
2003        // Encode data
2004        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2005
2006        // Try to decode with min
2007        let minimal = chunks
2008            .into_iter()
2009            .take(min as usize)
2010            .map(|c| checked(root, c))
2011            .collect::<Vec<_>>();
2012        let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
2013        assert_eq!(decoded, data);
2014    }
2015
2016    #[test]
2017    fn test_large_data() {
2018        let data = vec![42u8; 1000]; // 1KB of data
2019        let total = 7u16;
2020        let min = 4u16;
2021
2022        // Encode data
2023        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2024
2025        // Try to decode with min
2026        let minimal = chunks
2027            .into_iter()
2028            .take(min as usize)
2029            .map(|c| checked(root, c))
2030            .collect::<Vec<_>>();
2031        let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
2032        assert_eq!(decoded, data);
2033    }
2034
2035    #[test]
2036    fn test_parallel_encode_matches_sequential() {
2037        let total = 24u16;
2038        let min = 8u16;
2039        for shard_len in [
2040            MIN_STRIPE_BYTES - 2,
2041            MIN_STRIPE_BYTES,
2042            2 * MIN_STRIPE_BYTES - 2,
2043            2 * MIN_STRIPE_BYTES,
2044            2 * MIN_STRIPE_BYTES + 2,
2045            4 * MIN_STRIPE_BYTES + 62,
2046        ] {
2047            let data = vec![42u8; min as usize * shard_len - u32::SIZE];
2048            explore(|strategy| encode::<Sha256, _>(total, min, data.as_slice(), strategy).unwrap());
2049        }
2050    }
2051
2052    #[test]
2053    fn test_parallel_recovery_decode() {
2054        let strategy = Rayon::new(NZUsize!(4)).unwrap();
2055        let data = vec![42u8; 256 * 1024];
2056        let total = 24u16;
2057        let min = 8u16;
2058
2059        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &strategy).unwrap();
2060
2061        let minimal = chunks
2062            .into_iter()
2063            .skip(min as usize)
2064            .take(min as usize)
2065            .map(|c| checked(root, c))
2066            .collect::<Vec<_>>();
2067        let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &strategy).unwrap();
2068        assert_eq!(decoded, data);
2069    }
2070
2071    /// Multi-stripe recovery decode must be byte-identical to the single-stripe path.
2072    ///
2073    /// The multi-stripe path requires shards of at least `2 * MIN_STRIPE_BYTES`, so the test
2074    /// sweeps payload sizes and shard counts that land on several stripe-count boundaries in the
2075    /// parallel shapes. Each case decodes a set with as many recoveries as possible in serial,
2076    /// parallel, and mixed shapes and checks the result against the original data. Larger
2077    /// payloads also split stripes into multiple cache-sized tiles.
2078    #[test]
2079    fn test_striped_recovery_matches_sequential() {
2080        for &data_len in &[
2081            128 * 1024usize,
2082            257 * 1024,
2083            512 * 1024,
2084            1024 * 1024,
2085            4 * 1024 * 1024 + 14,
2086        ] {
2087            for &(total, min) in &[(12u16, 4u16), (24, 8), (33, 11), (20, 13)] {
2088                let data: Vec<u8> = (0..data_len)
2089                    .map(|i| (i as u8) ^ ((i >> 7) as u8))
2090                    .collect();
2091                let (root, chunks) =
2092                    encode::<Sha256, _>(total, min, data.as_slice(), &Sequential).unwrap();
2093                let originals_needed = min.saturating_sub(total - min) as usize;
2094                let selected = chunks
2095                    .into_iter()
2096                    .enumerate()
2097                    .filter(|&(index, _)| index < originals_needed || index >= min as usize)
2098                    .map(|(_, chunk)| chunk)
2099                    .take(min as usize)
2100                    .map(|c| checked(root, c))
2101                    .collect::<Vec<_>>();
2102                let decoded = explore(|strategy| {
2103                    decode::<Sha256, _>(total, min, &root, selected.iter(), strategy).unwrap()
2104                });
2105                assert_eq!(
2106                    decoded, data,
2107                    "striped decode must return the original data (len={data_len} total={total} min={min})"
2108                );
2109            }
2110        }
2111    }
2112
2113    /// All `k` originals must round-trip through the re-encode-and-verify path (`striped::decode`)
2114    /// in every execution shape, the multi-stripe ones included.
2115    #[test]
2116    fn test_striped_all_originals_decode() {
2117        let data = vec![42u8; 256 * 1024];
2118        let total = 24u16;
2119        let min = 8u16;
2120
2121        // The parallel shapes need enough complete blocks for at least two stripes.
2122        let shard_len = canonical_shard_len(data.len(), min as usize);
2123        assert!(
2124            shard_len >= 2 * MIN_STRIPE_BYTES,
2125            "test must exercise >= 2 stripes (shard_len={shard_len})"
2126        );
2127
2128        // Provide exactly the `k` original chunks (indices 0..min), so decode takes the
2129        // all-originals re-encode path rather than reconstructing.
2130        let decoded = explore(|strategy| {
2131            let (root, chunks) =
2132                encode::<Sha256, _>(total, min, data.as_slice(), strategy).unwrap();
2133            let originals = chunks
2134                .into_iter()
2135                .take(usize::from(min))
2136                .map(|c| checked(root, c))
2137                .collect::<Vec<_>>();
2138            decode::<Sha256, _>(total, min, &root, originals.iter(), strategy).unwrap()
2139        });
2140        assert_eq!(decoded, data);
2141    }
2142
2143    /// Splitting a shard into stripes, and stripes into cache-sized tiles, must reproduce a
2144    /// single full-width `Encoder` pass byte-for-byte. `(32, 64)` at `17 * MIN_STRIPE_BYTES + 2`
2145    /// splits stripes into multiple tiles.
2146    #[test]
2147    fn test_striped_encode_into_matches_full_width() {
2148        let strategy = Explorer::parallel(&Rayon::new(NZUsize!(4)).unwrap());
2149        let mut tiled = false;
2150        for (k, m) in [(2usize, 2usize), (32, 64)] {
2151            for shard_len in [
2152                2 * MIN_STRIPE_BYTES,
2153                2 * MIN_STRIPE_BYTES + 2,
2154                3 * MIN_STRIPE_BYTES + 62,
2155                5 * MIN_STRIPE_BYTES + 64,
2156                17 * MIN_STRIPE_BYTES + 2,
2157            ] {
2158                let mut originals_buf = vec![0u8; k * shard_len];
2159                for (i, byte) in originals_buf.iter_mut().enumerate() {
2160                    *byte = (i % 251) as u8;
2161                }
2162                let originals: Vec<&[u8]> = originals_buf.chunks(shard_len).collect();
2163
2164                // Encode each stripe into its column slices of the shared buffer.
2165                let mut striped_recovery = vec![0u8; m * shard_len];
2166                let results = strategy
2167                    .try_run_batches(
2168                        shard_len / SHARD_CHUNK_BYTES,
2169                        NZUsize!(MIN_STRIPE_BYTES / SHARD_CHUNK_BYTES),
2170                        SHARD_CHUNK_BYTES * (k + m),
2171                        |batches| {
2172                            batches.try_map_collect_vec(
2173                                |ranges| {
2174                                    let ranges = striped::byte_ranges(shard_len, ranges);
2175                                    tiled |= ranges.iter().any(|range| {
2176                                        striped::tiles(k, m, range.clone()).count() > 1
2177                                    });
2178                                    let groups = striped::stripe_columns(
2179                                        &mut striped_recovery,
2180                                        shard_len,
2181                                        &ranges,
2182                                    );
2183                                    ranges.into_iter().zip(groups)
2184                                },
2185                                |(range, out)| {
2186                                    striped::encode_recovery_into(k, m, range, &originals, out)
2187                                },
2188                            )
2189                        },
2190                    )
2191                    .unwrap();
2192                assert!(results.len() >= 2, "must split into stripes");
2193
2194                // A single full-width encode must produce the identical recovery buffer.
2195                let mut encoder = Encoder::new(k, m, shard_len).unwrap();
2196                for original in &originals {
2197                    encoder.add_original_shard(original).unwrap();
2198                }
2199                let encoding = encoder.encode().unwrap();
2200                for (actual, expected) in striped_recovery
2201                    .chunks_exact(shard_len)
2202                    .zip(encoding.recovery_iter())
2203                {
2204                    assert_eq!(actual, expected);
2205                }
2206            }
2207        }
2208        assert!(tiled, "no case splits a stripe into tiles");
2209    }
2210
2211    /// Tiles cover a stripe contiguously with balanced widths. A split into several tiles uses
2212    /// the fewest tiles within the target that leave no tile whose padded length is a multiple
2213    /// of 4 KiB.
2214    #[test]
2215    fn test_striped_tiles() {
2216        let page = 4096 / SHARD_CHUNK_BYTES;
2217        let start = 3 * SHARD_CHUNK_BYTES;
2218        for (k, m) in [(4usize, 8usize), (32, 64), (100, 100), (512, 512)] {
2219            let target =
2220                MAX_TILE_WORK_BYTES / (2 * (k + m).next_power_of_two() * SHARD_CHUNK_BYTES);
2221            for blocks in 1..3000 {
2222                for tail in [0, 2, SHARD_CHUNK_BYTES - 2] {
2223                    let range = start..start + blocks * SHARD_CHUNK_BYTES + tail;
2224                    let tiles: Vec<_> = striped::tiles(k, m, range.clone()).collect();
2225
2226                    // Tiles run from start to end, and every inner edge is block aligned.
2227                    assert_eq!(tiles[0].start, range.start);
2228                    assert_eq!(tiles.last().unwrap().end, range.end);
2229                    for pair in tiles.windows(2) {
2230                        assert_eq!(pair[0].end, pair[1].start);
2231                        assert!((pair[0].end - start).is_multiple_of(SHARD_CHUNK_BYTES));
2232                    }
2233
2234                    // Padded widths differ by at most one block. A stripe under twice the target is
2235                    // one tile, and a split keeps every width within the target and off 4 KiB.
2236                    let widths: Vec<_> = tiles
2237                        .iter()
2238                        .map(|tile| tile.len().div_ceil(SHARD_CHUNK_BYTES))
2239                        .collect();
2240                    let narrow = *widths.iter().min().unwrap();
2241                    let wide = *widths.iter().max().unwrap();
2242                    assert!(narrow >= 1 && wide - narrow <= 1);
2243                    let padded = range.len().div_ceil(SHARD_CHUNK_BYTES);
2244                    if padded < 2 * target {
2245                        assert_eq!(tiles.len(), 1);
2246                    } else {
2247                        assert!(wide <= target);
2248                        assert!(widths.iter().all(|width| !width.is_multiple_of(page)));
2249
2250                        // Every smaller count exceeds the target or lands a width on 4 KiB.
2251                        for fewer in 1..tiles.len() {
2252                            let (width, extra) = (padded / fewer, padded % fewer);
2253                            assert!(
2254                                padded.div_ceil(fewer) > target
2255                                    || width.is_multiple_of(page)
2256                                    || (extra > 0 && (width + 1).is_multiple_of(page))
2257                            );
2258                        }
2259                    }
2260                }
2261            }
2262        }
2263    }
2264
2265    // Each tamper mutates a canonical codeword in place before a (malicious) commitment is
2266    // rebuilt over the tampered shards. `k` is the original-shard count, `shard_len` the
2267    // per-shard width.
2268    fn tamper_flip_recovery(shards: &mut [Vec<u8>], k: usize, _shard_len: usize) {
2269        shards[k][0] ^= 0xFF;
2270    }
2271
2272    fn tamper_flip_original_data(shards: &mut [Vec<u8>], _k: usize, shard_len: usize) {
2273        // First payload byte sits right after the 4-byte length prefix.
2274        let offset = u32::SIZE;
2275        shards[offset / shard_len][offset % shard_len] ^= 0xFF;
2276    }
2277
2278    fn tamper_corrupt_padding(shards: &mut [Vec<u8>], k: usize, shard_len: usize) {
2279        // Final byte of the last original shard is canonical zero padding for the data
2280        // sizes used by the adversarial tests below.
2281        shards[k - 1][shard_len - 1] = 0xAA;
2282    }
2283
2284    /// Encode `data` canonically, apply `tamper`, rebuild a (malicious) commitment over the
2285    /// tampered shards, and decode the `selected` shard indices with `strategy`. Generic over
2286    /// [`Strategy`] so the same attack drives both the single-stripe and multi-stripe decode paths.
2287    fn decode_tampered_codeword<S: Strategy>(
2288        total: u16,
2289        min: u16,
2290        data: &[u8],
2291        selected: &[u16],
2292        tamper: fn(&mut [Vec<u8>], usize, usize),
2293        strategy: &S,
2294    ) -> Result<Vec<u8>, Error> {
2295        let (_root, chunks) = encode::<Sha256, _>(total, min, data, &Sequential).unwrap();
2296        let mut shards = chunks.iter().map(|c| c.shard.to_vec()).collect::<Vec<_>>();
2297        let shard_len = shards[0].len();
2298        tamper(&mut shards, min as usize, shard_len);
2299        let (root, chunks) = build_chunks(&shards);
2300        let pieces = selected
2301            .iter()
2302            .map(|&i| checked(root, chunks[i as usize].clone()))
2303            .collect::<Vec<_>>();
2304        decode::<Sha256, _>(total, min, &root, pieces.iter(), strategy)
2305    }
2306
2307    // (name, selected shard indices, tamper). With total=12/min=4, indices 0..4 are
2308    // originals and 4..12 are recoveries.
2309    #[allow(clippy::type_complexity)]
2310    const ADVERSARIAL_SCENARIOS: &[(&str, &[u16], fn(&mut [Vec<u8>], usize, usize))] = &[
2311        // Tampered recovery that is NOT provided: caught when the root is rebuilt from the
2312        // re-encoded recoveries.
2313        (
2314            "tampered_recovery_unprovided",
2315            &[0, 1, 2, 3],
2316            tamper_flip_recovery,
2317        ),
2318        // Tampered recovery that IS provided and forces RS reconstruction of a missing
2319        // original (exercises the striped `decode_reveal` path).
2320        (
2321            "tampered_recovery_provided",
2322            &[0, 1, 2, 4],
2323            tamper_flip_recovery,
2324        ),
2325        // Non-canonical (non-zero) trailing padding in an original shard (extract_data).
2326        (
2327            "non_canonical_padding",
2328            &[0, 1, 2, 3],
2329            tamper_corrupt_padding,
2330        ),
2331        // Tampered original payload byte: detected by the rebuilt commitment.
2332        (
2333            "tampered_original_data",
2334            &[0, 1, 2, 3],
2335            tamper_flip_original_data,
2336        ),
2337        // Recovery-only decode where one provided recovery is tampered.
2338        (
2339            "recovery_only_tampered",
2340            &[4, 5, 6, 7],
2341            tamper_flip_recovery,
2342        ),
2343    ];
2344
2345    /// Every adversarial scenario must be rejected as [`Error::Inconsistent`] on whichever
2346    /// decode path `strategy` + `data` selects.
2347    fn assert_adversarial_rejected<S: Strategy>(total: u16, min: u16, data: &[u8], strategy: &S) {
2348        for &(name, selected, tamper) in ADVERSARIAL_SCENARIOS {
2349            let result = decode_tampered_codeword(total, min, data, selected, tamper, strategy);
2350            assert!(
2351                matches!(result, Err(Error::Inconsistent)),
2352                "scenario {name} not rejected as Inconsistent: {result:?}"
2353            );
2354        }
2355    }
2356
2357    #[test]
2358    fn test_adversarial_rejection_sequential_small() {
2359        // Small payload: striping never engages, so this exercises the single-stripe path
2360        // (matching the rest of the small-data adversarial tests).
2361        assert_adversarial_rejected(12, 4, &[0xCDu8; 30], &Sequential);
2362    }
2363
2364    /// Splitting a decode into stripes must not change any consensus-critical rejection. Drive
2365    /// each malicious scenario through the single-stripe and multi-stripe paths on the same large
2366    /// payload and require identical (Inconsistent) verdicts.
2367    #[test]
2368    fn test_adversarial_rejection_striped_matches_sequential() {
2369        let total = 12u16;
2370        let min = 4u16;
2371
2372        // Give the parallel shapes enough complete blocks for at least two stripes.
2373        let data = vec![0xABu8; 64 * 1024];
2374        let shard_len = canonical_shard_len(data.len(), min as usize);
2375        assert!(
2376            shard_len >= 2 * MIN_STRIPE_BYTES,
2377            "test must exercise >= 2 stripes (shard_len={shard_len})"
2378        );
2379
2380        // Every shape, the multi-stripe ones included, must reject each attack on the same data as
2381        // inconsistent. An untampered mixed set (some originals and a recovery) still decodes, and
2382        // decoding it reconstructs a missing original.
2383        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &Sequential).unwrap();
2384        let mixed = [0u16, 1, 2, 4]
2385            .into_iter()
2386            .map(|i| checked(root, chunks[i as usize].clone()))
2387            .collect::<Vec<_>>();
2388        let decoded = explore(|strategy| {
2389            assert_adversarial_rejected(total, min, &data, strategy);
2390            decode::<Sha256, _>(total, min, &root, mixed.iter(), strategy).unwrap()
2391        });
2392        assert_eq!(decoded, data);
2393    }
2394
2395    /// Pin the exact vendored Reed-Solomon recovery output for a fixed `(k, m, shard_len)` with a
2396    /// partial final block (`shard_len % SHARD_CHUNK_BYTES != 0`). The striped coder assumes this
2397    /// crate's internal block layout and tail packing (see [`SHARD_CHUNK_BYTES`]); if the
2398    /// implementation changes its produced bytes this fixture trips, signalling that the striping
2399    /// assumption must be re-verified before the new output is accepted.
2400    ///
2401    /// The cases take one shape per branch of the backend's rate selection, with more originals
2402    /// (high rate), more recoveries (low rate), and counts that round to the same power of two.
2403    #[rstest]
2404    #[case::high_rate(
2405        5,
2406        3,
2407        "e38bb9dbba4a102c4bd8447e212957742dab0af0c4148d4660c671f2f33d3df2"
2408    )]
2409    #[case::low_rate(
2410        4,
2411        6,
2412        "9f55e86ae11f9de757087e9052810b6359ed7c3a107ccde5a6b304fa54bd72fa"
2413    )]
2414    #[case::equal_powers(
2415        3,
2416        4,
2417        "aaaeebde791afabb30dc98356130ddd196e600c5a8c33882a8404539df795823"
2418    )]
2419    fn test_recovery_output_format_pinned(
2420        #[case] k: usize,
2421        #[case] m: usize,
2422        #[case] expected: &str,
2423    ) {
2424        // Two full blocks plus a 2-byte partial tail.
2425        let shard_len = 2 * SHARD_CHUNK_BYTES + 2;
2426
2427        let mut encoder = Encoder::new(k, m, shard_len).unwrap();
2428        for i in 0..k {
2429            let shard: Vec<u8> = (0..shard_len)
2430                .map(|j| ((i * 31 + j * 7) & 0xFF) as u8)
2431                .collect();
2432            encoder.add_original_shard(&shard).unwrap();
2433        }
2434        let encoding = encoder.encode().unwrap();
2435
2436        let recovery: Vec<&[u8]> = encoding.recovery_iter().collect();
2437        let digest = Sha256::hash(&recovery);
2438        assert_eq!(
2439            format!("{digest}"),
2440            expected,
2441            "vendored Reed-Solomon recovery output changed for ({k}, {m}); re-verify the \
2442             striping assumption before updating this fixture"
2443        );
2444    }
2445
2446    /// Commitments are part of the consensus format, so every node, on every architecture and
2447    /// engine, must derive these exact roots. The configurations cover each branch of the
2448    /// backend's rate selection. The payloads include empty input and inputs that produce short
2449    /// and multi-block shards.
2450    #[rstest]
2451    #[case::k2_m2_len0(
2452        2,
2453        2,
2454        0,
2455        "87d6a38f0987af00691b75fbfb710b7a50df08115c15254e09f109a1b74170c3"
2456    )]
2457    #[case::k2_m2_len1000(
2458        2,
2459        2,
2460        1000,
2461        "b90ea02e9b2eb25cc04f92c80b4f0dc8f68b154feff5a24495a8f090918b10d7"
2462    )]
2463    #[case::k2_m2_len70000(
2464        2,
2465        2,
2466        70_000,
2467        "b55ebdadfe51a23ab3a3ee8200a07dded9e10b8226abe34cbad171a11c070189"
2468    )]
2469    #[case::k3_m4_len0(
2470        3,
2471        4,
2472        0,
2473        "85a86686f0c01d863dde8f994dc3c9e063fa4458ff9458e218f5f260ed225c60"
2474    )]
2475    #[case::k3_m4_len1000(
2476        3,
2477        4,
2478        1000,
2479        "1fef0811649ed7f925bbc915ec3bf6d97509682e513f595881b2462e5ef1da8e"
2480    )]
2481    #[case::k3_m4_len70000(
2482        3,
2483        4,
2484        70_000,
2485        "a460ad1f30a8518a4e91de264deefbed57622c1ee6cf44c8e32573f437df8ba0"
2486    )]
2487    #[case::k4_m6_len0(
2488        4,
2489        6,
2490        0,
2491        "dd18f239803d542063c6dfebc9618c716406b299d1fd84d88476344b05e09d7b"
2492    )]
2493    #[case::k4_m6_len1000(
2494        4,
2495        6,
2496        1000,
2497        "e18fcc7b41a8b4ed1ca2420df986787f84452e1fb4d3d294827ebd325bcaaa7d"
2498    )]
2499    #[case::k4_m6_len70000(
2500        4,
2501        6,
2502        70_000,
2503        "18e9301ae82f44bf2ce2a16e44bd8901c33f357149a26b7981c6474b1fbd95dd"
2504    )]
2505    #[case::k5_m3_len0(
2506        5,
2507        3,
2508        0,
2509        "dd24cc230c717006fbdea807fe8d14a8e95e34d8b3efb40d8300711c3d1b03f5"
2510    )]
2511    #[case::k5_m3_len1000(
2512        5,
2513        3,
2514        1000,
2515        "90cdb8c92a13b9b2d2040f861df5ea24dbc9e27e8d0f10295aec80b93a8397b2"
2516    )]
2517    #[case::k5_m3_len70000(
2518        5,
2519        3,
2520        70_000,
2521        "a33bcd6552d8cc593363c994ed4113b59a6da32234846ee0bc1c570ec75f1e7b"
2522    )]
2523    #[case::k9_m16_len0(
2524        9,
2525        16,
2526        0,
2527        "84082dbcb47449967323f8b4893592d1361392387ad0371ee8faf5caad795bd8"
2528    )]
2529    #[case::k9_m16_len1000(
2530        9,
2531        16,
2532        1000,
2533        "b7a717d37d93331142139f14a14e61029bc0fac8a2dd3b1b78362d5091f5923f"
2534    )]
2535    #[case::k9_m16_len70000(
2536        9,
2537        16,
2538        70_000,
2539        "2f1b1764b12669a09362bfe0dd19e0a5ec1862f81ff67148f4413cfc7ca75cfc"
2540    )]
2541    fn test_commitment_pinned(
2542        #[case] k: u16,
2543        #[case] m: u16,
2544        #[case] len: usize,
2545        #[case] expected: &str,
2546    ) {
2547        let data: Vec<u8> = (0..len).map(|i| ((i * 31 + 7) % 251) as u8).collect();
2548        let config = Config {
2549            minimum_shards: NZU16!(k),
2550            extra_shards: NZU16!(m),
2551        };
2552        let (commitment, _) = RS::encode(&config, data.as_slice(), &STRATEGY).unwrap();
2553        assert_eq!(
2554            format!("{commitment}"),
2555            expected,
2556            "commitment changed for ({k}, {m}, {len} bytes); this is a consensus format change"
2557        );
2558    }
2559
2560    /// A payload is rejected before any allocation when its canonical shard width would not fit
2561    /// the `u32` length prefix every chunk carries, or when the codeword could not be allocated.
2562    #[test]
2563    fn test_encode_rejects_unrepresentable_widths() {
2564        /// A `Buf` of `remaining` zero bytes that never allocates its claimed length. Encoding must
2565        /// reject the lengths below before reading any of them.
2566        struct Claimed(usize);
2567
2568        impl bytes::Buf for Claimed {
2569            fn remaining(&self) -> usize {
2570                self.0
2571            }
2572
2573            fn chunk(&self) -> &[u8] {
2574                static ZEROS: [u8; 64] = [0; 64];
2575                &ZEROS[..self.0.min(ZEROS.len())]
2576            }
2577
2578            fn advance(&mut self, cnt: usize) {
2579                assert!(cnt <= self.0);
2580                self.0 -= cnt;
2581            }
2582        }
2583
2584        let max = Widen::widen(u32::MAX);
2585        let single = Config {
2586            minimum_shards: NZU16!(1),
2587            extra_shards: NZU16!(1),
2588        };
2589        let pair = Config {
2590            minimum_shards: NZU16!(2),
2591            extra_shards: NZU16!(1),
2592        };
2593
2594        // Any configuration rejects a payload longer than the length prefix can express.
2595        for config in [single, pair] {
2596            assert!(matches!(
2597                RS::encode(&config, Claimed(max + 1), &STRATEGY),
2598                Err(Error::InvalidDataLength(len)) if len == max + 1
2599            ));
2600        }
2601
2602        // With one original shard, the width is the prefixed payload rounded up to even bytes,
2603        // so the five largest payload lengths push it past `u32::MAX`.
2604        for len in max - 4..=max {
2605            assert!(canonical_shard_len(len, 1) > max);
2606            assert!(matches!(
2607                RS::encode(&single, Claimed(len), &STRATEGY),
2608                Err(Error::InvalidDataLength(got)) if got == len
2609            ));
2610        }
2611        assert!(canonical_shard_len(max - 5, 1) <= max);
2612        assert!(canonical_shard_len(max, 2) <= max);
2613
2614        // The allocation bound accepts the largest allocation and rejects both a product that
2615        // overflows and a representable product beyond that limit.
2616        assert!(codeword_fits(3, 1 << 20));
2617        assert!(codeword_fits(1, MAX_ALLOCATION_BYTES));
2618        assert!(!codeword_fits(1, MAX_ALLOCATION_BYTES + 1));
2619        assert!(!codeword_fits(2, MAX_ALLOCATION_BYTES));
2620        assert!(!codeword_fits(2, usize::MAX));
2621    }
2622
2623    /// Returns the shard width this thread's cached decoder was last configured for.
2624    fn cached_decoder_width() -> usize {
2625        CACHED_DECODER.with(|cell| {
2626            let mut slot = cell.borrow_mut();
2627            let decoder = slot.1.as_mut().expect("decoder must be cached");
2628            match decoder.add_original_shard(0, [0u8; 0]) {
2629                Err(RsError::DifferentShardSize {
2630                    shard_bytes,
2631                    got: 0,
2632                }) => shard_bytes,
2633                other => panic!("unexpected decoder state: {other:?}"),
2634            }
2635        })
2636    }
2637
2638    /// Returns the shard width this thread's cached encoder was last configured for.
2639    fn cached_encoder_width() -> usize {
2640        CACHED_ENCODER.with(|cell| {
2641            let mut slot = cell.borrow_mut();
2642            let encoder = slot.1.as_mut().expect("encoder must be cached");
2643            match encoder.add_original_shard([0u8; 0]) {
2644                Err(RsError::DifferentShardSize {
2645                    shard_bytes,
2646                    got: 0,
2647                }) => shard_bytes,
2648                other => panic!("unexpected encoder state: {other:?}"),
2649            }
2650        })
2651    }
2652
2653    /// Single-stripe coding configures the thread-local coders for one tile at a time, so a wide
2654    /// shard never leaves full-width work cached, whether coding succeeds or fails.
2655    #[test]
2656    fn test_single_stripe_coding_caches_tile_width() {
2657        let total = 96u16;
2658        let min = 32u16;
2659        let (k, m) = (min as usize, (total - min) as usize);
2660
2661        // The shard spans several tiles, so its last tile is narrower than the shard.
2662        let shard_len = 17 * MIN_STRIPE_BYTES + 2;
2663        let tile = striped::tiles(k, m, 0..shard_len)
2664            .last()
2665            .expect("a nonempty shard has a tile")
2666            .len();
2667        assert!(striped::tiles(k, m, 0..shard_len).count() > 1);
2668        assert!(tile < shard_len);
2669        let data: Vec<u8> = (0..k * shard_len - u32::SIZE)
2670            .map(|i| (i % 251) as u8)
2671            .collect();
2672
2673        // Encoding leaves the encoder configured for the last tile.
2674        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &Sequential).unwrap();
2675        assert_eq!(chunks[0].shard.len(), shard_len);
2676        assert_eq!(cached_encoder_width(), tile);
2677
2678        // Recovering every original leaves the decoder configured for the last tile.
2679        let recoveries = chunks[k..2 * k]
2680            .iter()
2681            .map(|chunk| checked(root, chunk.clone()))
2682            .collect::<Vec<_>>();
2683        let decoded =
2684            decode::<Sha256, _>(total, min, &root, recoveries.iter(), &Sequential).unwrap();
2685        assert_eq!(decoded, data);
2686        assert_eq!(cached_decoder_width(), tile);
2687
2688        // Re-encoding from every original leaves the encoder configured for the last tile.
2689        let originals = chunks[..k]
2690            .iter()
2691            .map(|chunk| checked(root, chunk.clone()))
2692            .collect::<Vec<_>>();
2693        let decoded =
2694            decode::<Sha256, _>(total, min, &root, originals.iter(), &Sequential).unwrap();
2695        assert_eq!(decoded, data);
2696        assert_eq!(cached_encoder_width(), tile);
2697
2698        // A recovery that fails the commitment check leaves the decoder configured for the
2699        // last tile.
2700        let selected = (min..2 * min).collect::<Vec<_>>();
2701        let result = decode_tampered_codeword(
2702            total,
2703            min,
2704            &data,
2705            &selected,
2706            tamper_flip_recovery,
2707            &Sequential,
2708        );
2709        assert!(matches!(result, Err(Error::Inconsistent)), "{result:?}");
2710        assert_eq!(cached_decoder_width(), tile);
2711    }
2712
2713    /// A chunk decodes when it is no wider than the maximum data produces, and a wider chunk is
2714    /// rejected.
2715    #[test]
2716    fn test_read_cfg_bounds_chunk_width() {
2717        type TestChunk = Chunk<<Sha256 as Hasher>::Digest>;
2718        for (min, extra) in [(1u16, 1u16), (2, 2), (4, 6), (34, 66)] {
2719            let config = Config {
2720                minimum_shards: NZU16!(min),
2721                extra_shards: NZU16!(extra),
2722            };
2723            for data_len in [0, 1, 2 * usize::from(min), 1000, 4099] {
2724                // Encoding exactly the maximum data produces a chunk that decodes.
2725                let cfg = (config, data_len);
2726                let (_, chunks) =
2727                    RS::encode(&config, vec![0; data_len].as_slice(), &STRATEGY).unwrap();
2728                assert!(TestChunk::decode_cfg(chunks[0].encode(), &cfg).is_ok());
2729
2730                // Enough extra data to widen the chunk is rejected.
2731                let width = chunks[0].shard.len();
2732                let wider = (data_len..)
2733                    .find(|&len| canonical_shard_len(len, usize::from(min)) > width)
2734                    .unwrap();
2735                let (_, chunks) =
2736                    RS::encode(&config, vec![0; wider].as_slice(), &STRATEGY).unwrap();
2737                assert!(TestChunk::decode_cfg(chunks[0].encode(), &cfg).is_err());
2738            }
2739
2740            // A maximum beyond what encoding accepts decodes without overflow.
2741            let (_, chunks) = RS::encode(&config, [0u8; 8].as_slice(), &STRATEGY).unwrap();
2742            assert!(TestChunk::decode_cfg(chunks[0].encode(), &(config, usize::MAX)).is_ok());
2743        }
2744    }
2745
2746    /// Shards of zero or odd width are rejected at admission, individually and in batches.
2747    #[test]
2748    fn test_check_rejects_invalid_shard_width() {
2749        let config = Config {
2750            minimum_shards: NZU16!(4),
2751            extra_shards: NZU16!(8),
2752        };
2753        for width in [0, 3, 2 * MIN_STRIPE_BYTES + 1] {
2754            let (root, chunks) = build_chunks(&vec![vec![0u8; width]; 12]);
2755            let invalid = |result: &Result<_, Error>| {
2756                matches!(
2757                    result,
2758                    Err(Error::ReedSolomon(RsError::InvalidShardSize { shard_bytes }))
2759                        if *shard_bytes == width
2760                )
2761            };
2762
2763            // Each shard is rejected individually.
2764            for chunk in &chunks {
2765                assert!(invalid(&RS::check(
2766                    &config,
2767                    &root,
2768                    chunk.index,
2769                    chunk,
2770                    &STRATEGY
2771                )));
2772            }
2773
2774            // The same shards are rejected as a batch.
2775            let batch = chunks
2776                .iter()
2777                .map(|chunk| (chunk.index, chunk))
2778                .collect::<Vec<_>>();
2779            assert!(
2780                RS::check_many(&config, &root, &batch, &STRATEGY)
2781                    .iter()
2782                    .all(invalid)
2783            );
2784        }
2785    }
2786
2787    #[test]
2788    fn test_parallel_decode_rejects_mismatched_shard_lengths() {
2789        let strategy = Rayon::new(NZUsize!(4)).unwrap();
2790        let data = vec![42u8; 256 * 1024];
2791        let total = 24u16;
2792        let min = 8u16;
2793
2794        let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &strategy).unwrap();
2795        let mut shards = chunks
2796            .iter()
2797            .map(|chunk| chunk.shard.to_vec())
2798            .collect::<Vec<_>>();
2799        shards[min as usize].pop();
2800
2801        let mut builder = Builder::<Sha256>::new(total as usize);
2802        for shard in &shards {
2803            builder.add(&Sha256::hash(&[shard]));
2804        }
2805        let tree = builder.build();
2806        let root = tree.root();
2807
2808        let pieces = [9u16, 8, 10, 11, 12, 13, 14, 15]
2809            .into_iter()
2810            .map(|i| {
2811                let proof = tree.proof(i as u32).unwrap();
2812                checked(
2813                    root,
2814                    Chunk::new(shards[i as usize].clone().into(), i, proof),
2815                )
2816            })
2817            .collect::<Vec<_>>();
2818
2819        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &strategy);
2820        assert!(matches!(result, Err(Error::Inconsistent)));
2821    }
2822
2823    #[test]
2824    fn test_malicious_root_detection() {
2825        let data = b"Original data that should be protected";
2826        let total = 7u16;
2827        let min = 4u16;
2828        let config = Config {
2829            minimum_shards: NZU16!(min),
2830            extra_shards: NZU16!(total - min),
2831        };
2832
2833        // Encode data correctly to get valid chunks
2834        let (_correct_root, chunks) =
2835            encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2836
2837        // Create a malicious/fake root (simulating a malicious encoder)
2838        let malicious_root = Sha256::hash(&[b"malicious_data_that_wasnt_actually_encoded"]);
2839
2840        // Verify all proofs at incorrect root
2841        for i in 0..total {
2842            assert!(
2843                RS::check(&config, &malicious_root, i, &chunks[i as usize], &STRATEGY).is_err()
2844            );
2845        }
2846
2847        // Collect valid pieces (these are legitimate fragments checked against
2848        // the correct root).
2849        let minimal = chunks
2850            .into_iter()
2851            .take(min as usize)
2852            .map(|c| checked(_correct_root, c))
2853            .collect::<Vec<_>>();
2854
2855        // Decoding with a malicious root fails because the checked chunks
2856        // are bound to a different commitment.
2857        let result = decode::<Sha256, _>(total, min, &malicious_root, minimal.iter(), &STRATEGY);
2858        assert!(matches!(result, Err(Error::CommitmentMismatch)));
2859    }
2860
2861    #[test]
2862    fn test_mismatched_config_rejected_during_check() {
2863        let config_expected = Config {
2864            minimum_shards: NZU16!(2),
2865            extra_shards: NZU16!(2),
2866        };
2867        let config_actual = Config {
2868            minimum_shards: NZU16!(3),
2869            extra_shards: NZU16!(3),
2870        };
2871
2872        let data = b"leaf_count mismatch proof";
2873        let (commitment, shards) = RS::encode(&config_actual, data.as_slice(), &STRATEGY).unwrap();
2874
2875        // A proof generated under a different shard configuration is invalid
2876        // for this commitment.
2877        let check_result = RS::check(&config_expected, &commitment, 0, &shards[0], &STRATEGY);
2878        assert!(matches!(check_result, Err(Error::InvalidProof)));
2879    }
2880
2881    #[test]
2882    fn test_manipulated_chunk_detection() {
2883        let data = b"Data integrity must be maintained";
2884        let total = 6u16;
2885        let min = 3u16;
2886
2887        // Encode data
2888        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
2889        let mut pieces: Vec<_> = chunks.into_iter().map(|c| checked(root, c)).collect();
2890
2891        // Tamper with one of the checked chunks by modifying the shard data.
2892        if !pieces[1].shard.is_empty() {
2893            let mut shard = pieces[1].shard.to_vec();
2894            shard[0] ^= 0xFF; // Flip bits in first byte
2895            pieces[1].shard = shard.into();
2896        }
2897
2898        // Try to decode with the tampered chunk
2899        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
2900        assert!(matches!(result, Err(Error::Inconsistent)));
2901    }
2902
2903    #[test]
2904    fn test_inconsistent_shards() {
2905        let data = b"Test data for malicious encoding";
2906        let total = 5u16;
2907        let min = 3u16;
2908        let m = total - min;
2909
2910        // Compute original data encoding
2911        let shard_size = canonical_shard_len(data.len(), min as usize);
2912        let padded = prepare_data(data.as_slice(), min as usize, shard_size);
2913
2914        // Re-encode the data
2915        let mut encoder = Encoder::new(min as usize, m as usize, shard_size).unwrap();
2916        for shard in padded.chunks(shard_size) {
2917            encoder.add_original_shard(shard).unwrap();
2918        }
2919        let recovery_result = encoder.encode().unwrap();
2920        let mut recovery_shards: Vec<Vec<u8>> = recovery_result
2921            .recovery_iter()
2922            .map(|s| s.to_vec())
2923            .collect();
2924
2925        // Tamper with one recovery shard
2926        if !recovery_shards[0].is_empty() {
2927            recovery_shards[0][0] ^= 0xFF;
2928        }
2929
2930        // Build malicious shards
2931        let mut malicious_shards: Vec<Vec<u8>> =
2932            padded.chunks(shard_size).map(|s| s.to_vec()).collect();
2933        malicious_shards.extend(recovery_shards);
2934
2935        // Build malicious tree
2936        let mut builder = Builder::<Sha256>::new(total as usize);
2937        for shard in &malicious_shards {
2938            builder.add(&Sha256::hash(&[shard]));
2939        }
2940        let malicious_tree = builder.build();
2941        let malicious_root = malicious_tree.root();
2942
2943        // Generate chunks for min pieces, including the tampered recovery
2944        let selected_indices = vec![0, 1, 3]; // originals 0,1 and recovery 0 (index 3)
2945        let mut pieces = Vec::new();
2946        for &i in &selected_indices {
2947            let merkle_proof = malicious_tree.proof(i as u32).unwrap();
2948            let shard = malicious_shards[i].clone();
2949            let chunk = Chunk::new(shard.into(), i as u16, merkle_proof);
2950            pieces.push(chunk);
2951        }
2952        let pieces: Vec<_> = pieces
2953            .into_iter()
2954            .map(|c| checked(malicious_root, c))
2955            .collect();
2956
2957        // Fail to decode
2958        let result = decode::<Sha256, _>(total, min, &malicious_root, pieces.iter(), &STRATEGY);
2959        assert!(matches!(result, Err(Error::Inconsistent)));
2960    }
2961
2962    // Regression: a commitment built from shards with non-zero trailing padding
2963    // used to pass decode(), even though canonical re-encoding (zero padding)
2964    // produces a different root. decode() must reject such non-canonical shards.
2965    #[test]
2966    fn test_non_canonical_padding_rejected() {
2967        let data = b"X";
2968        let total = 6u16;
2969        let min = 3u16;
2970        let k = min as usize;
2971        let m = total as usize - k;
2972
2973        let shard_len = canonical_shard_len(data.len(), k);
2974        let mut padded = prepare_data(data.as_slice(), k, shard_len);
2975        let payload_end = u32::SIZE + data.len();
2976        let total_original_len = k * shard_len;
2977        assert!(payload_end < total_original_len, "test requires padding");
2978
2979        // Corrupt one canonical padding byte while keeping payload unchanged.
2980        let pad_shard = payload_end / shard_len;
2981        let pad_offset = payload_end % shard_len;
2982        padded[pad_shard * shard_len + pad_offset] = 0xAA;
2983
2984        let mut encoder = Encoder::new(k, m, shard_len).unwrap();
2985        for shard in padded.chunks(shard_len) {
2986            encoder.add_original_shard(shard).unwrap();
2987        }
2988        let recovery = encoder.encode().unwrap();
2989        let mut shards: Vec<Vec<u8>> = padded.chunks(shard_len).map(|s| s.to_vec()).collect();
2990        shards.extend(recovery.recovery_iter().map(|s| s.to_vec()));
2991
2992        let mut builder = Builder::<Sha256>::new(total as usize);
2993        for shard in &shards {
2994            builder.add(&Sha256::hash(&[shard]));
2995        }
2996        let tree = builder.build();
2997        let non_canonical_root = tree.root();
2998
2999        let mut pieces = Vec::with_capacity(k);
3000        for (i, shard) in shards.iter().take(k).enumerate() {
3001            let proof = tree.proof(i as u32).unwrap();
3002            pieces.push(checked(
3003                non_canonical_root,
3004                Chunk::new(shard.clone().into(), i as u16, proof),
3005            ));
3006        }
3007
3008        let result = decode::<Sha256, _>(total, min, &non_canonical_root, pieces.iter(), &STRATEGY);
3009        assert!(matches!(result, Err(Error::Inconsistent)));
3010    }
3011
3012    #[test]
3013    fn minifuzz_decode_unique_commitment() {
3014        minifuzz::Builder::default()
3015            .with_search_limit(2048)
3016            .test(|u| {
3017                fuzz_arbitrary_codeword(u)?;
3018                fuzz_mixed_codeword(u)?;
3019                Ok(())
3020            });
3021    }
3022
3023    #[test]
3024    fn test_oversized_zero_padded_shards_rejected() {
3025        let data = b"X";
3026        let total = 6u16;
3027        let min = 3u16;
3028        let k = min as usize;
3029        let m = total as usize - k;
3030
3031        let oversized_shard_len = 4usize;
3032        let mut padded = vec![0u8; k * oversized_shard_len];
3033        padded[..u32::SIZE].copy_from_slice(&(data.len() as u32).to_be_bytes());
3034        padded[u32::SIZE..u32::SIZE + data.len()].copy_from_slice(data);
3035
3036        let mut encoder = Encoder::new(k, m, oversized_shard_len).unwrap();
3037        for shard in padded.chunks(oversized_shard_len) {
3038            encoder.add_original_shard(shard).unwrap();
3039        }
3040        let recovery = encoder.encode().unwrap();
3041
3042        let mut oversized_shards: Vec<Vec<u8>> = padded
3043            .chunks(oversized_shard_len)
3044            .map(|shard| shard.to_vec())
3045            .collect();
3046        oversized_shards.extend(recovery.recovery_iter().map(|shard| shard.to_vec()));
3047
3048        let mut builder = Builder::<Sha256>::new(total as usize);
3049        for shard in &oversized_shards {
3050            builder.add(&Sha256::hash(&[shard]));
3051        }
3052        let oversized_tree = builder.build();
3053        let oversized_root = oversized_tree.root();
3054
3055        let (canonical_root, _) =
3056            encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
3057        assert_ne!(oversized_root, canonical_root);
3058
3059        let pieces = [0u16, 1u16, 4u16]
3060            .into_iter()
3061            .map(|i| {
3062                let proof = oversized_tree.proof(i as u32).unwrap();
3063                checked(
3064                    oversized_root,
3065                    Chunk::new(oversized_shards[i as usize].clone().into(), i, proof),
3066                )
3067            })
3068            .collect::<Vec<_>>();
3069
3070        let result = decode::<Sha256, _>(total, min, &oversized_root, pieces.iter(), &STRATEGY);
3071        assert!(matches!(result, Err(Error::Inconsistent)));
3072    }
3073
3074    #[test]
3075    fn test_extra_non_canonical_recovery_rejected() {
3076        let data = b"canonical originals with bad recovery";
3077        let total = 6u16;
3078        let min = 3u16;
3079
3080        let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
3081        let mut shards = chunks
3082            .iter()
3083            .map(|chunk| chunk.shard.to_vec())
3084            .collect::<Vec<_>>();
3085        shards[min as usize][0] ^= 0xFF;
3086
3087        let mut builder = Builder::<Sha256>::new(total as usize);
3088        for shard in &shards {
3089            builder.add(&Sha256::hash(&[shard]));
3090        }
3091        let tree = builder.build();
3092        let root = tree.root();
3093
3094        let pieces = (0u16..=3u16)
3095            .map(|i| {
3096                let proof = tree.proof(i as u32).unwrap();
3097                checked(
3098                    root,
3099                    Chunk::new(shards[i as usize].clone().into(), i, proof),
3100                )
3101            })
3102            .collect::<Vec<_>>();
3103
3104        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
3105        assert!(matches!(result, Err(Error::Inconsistent)));
3106    }
3107
3108    #[test]
3109    fn test_reconstructed_original_with_extra_non_canonical_recovery_rejected() {
3110        let data = b"canonical reconstructed originals with bad extra recovery";
3111        let total = 6u16;
3112        let min = 3u16;
3113
3114        let (_root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
3115        let mut shards = chunks
3116            .iter()
3117            .map(|chunk| chunk.shard.to_vec())
3118            .collect::<Vec<_>>();
3119        shards[4][0] ^= 0xFF;
3120
3121        let mut builder = Builder::<Sha256>::new(total as usize);
3122        for shard in &shards {
3123            builder.add(&Sha256::hash(&[shard]));
3124        }
3125        let tree = builder.build();
3126        let root = tree.root();
3127
3128        let pieces = [0u16, 1u16, 3u16, 4u16]
3129            .into_iter()
3130            .map(|i| {
3131                let proof = tree.proof(i as u32).unwrap();
3132                checked(
3133                    root,
3134                    Chunk::new(shards[i as usize].clone().into(), i, proof),
3135                )
3136            })
3137            .collect::<Vec<_>>();
3138
3139        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
3140        assert!(matches!(result, Err(Error::Inconsistent)));
3141    }
3142
3143    #[test]
3144    fn test_striped_surplus_recovery_tampered_rejected() {
3145        // Striped decode-reveal with more than `k` shards provided and an original missing,
3146        // where a surplus recovery shard is committed but non-canonical. The decoder is fed
3147        // exactly `k`; the trimmed surplus is reconstructed and rejected by the root check
3148        // (there is no separate provided-recovery comparison on this path).
3149        let data = vec![0xABu8; 64 * 1024];
3150        let total = 12u16;
3151        let min = 4u16;
3152        let shard_len = canonical_shard_len(data.len(), min as usize);
3153        assert!(
3154            shard_len >= 2 * MIN_STRIPE_BYTES,
3155            "test must exercise >= 2 stripes (shard_len={shard_len})"
3156        );
3157
3158        // Provide originals 0,1 and recoveries 4,5,6 (5 > k=4, originals 2,3 missing). The
3159        // decoder is fed originals 0,1 + recoveries 4,5; recovery 6 is the trimmed surplus.
3160        let selected = [0u16, 1, 4, 5, 6];
3161        let tamper: fn(&mut [Vec<u8>], usize, usize) = |shards, _, _| shards[6][0] ^= 0xFF;
3162        let rejected = explore(|strategy| {
3163            matches!(
3164                decode_tampered_codeword(total, min, &data, &selected, tamper, strategy),
3165                Err(Error::Inconsistent)
3166            )
3167        });
3168        assert!(rejected);
3169    }
3170
3171    #[test]
3172    fn test_decode_invalid_index() {
3173        let data = b"Testing recovery pieces";
3174        let total = 8u16;
3175        let min = 3u16;
3176
3177        // Encode the data
3178        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
3179
3180        // Use a mix of original and recovery pieces
3181        let mut invalid = checked(root, chunks[1].clone());
3182        invalid.index = 8;
3183        let pieces: Vec<_> = vec![
3184            checked(root, chunks[0].clone()), // original
3185            invalid,                          // recovery with invalid index
3186            checked(root, chunks[6].clone()), // recovery
3187        ];
3188
3189        // Fail to decode
3190        let result = decode::<Sha256, _>(total, min, &root, pieces.iter(), &STRATEGY);
3191        assert!(matches!(result, Err(Error::InvalidIndex(8))));
3192    }
3193
3194    #[test]
3195    fn test_max_chunks() {
3196        let data = vec![42u8; 1000]; // 1KB of data
3197        let total = u16::MAX;
3198        let min = u16::MAX / 2;
3199
3200        // Encode data
3201        let (root, chunks) = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY).unwrap();
3202
3203        // Try to decode with min
3204        let minimal = chunks
3205            .into_iter()
3206            .take(min as usize)
3207            .map(|c| checked(root, c))
3208            .collect::<Vec<_>>();
3209        let decoded = decode::<Sha256, _>(total, min, &root, minimal.iter(), &STRATEGY).unwrap();
3210        assert_eq!(decoded, data);
3211    }
3212
3213    #[test]
3214    fn test_too_many_chunks() {
3215        let data = vec![42u8; 1000]; // 1KB of data
3216        let total = u16::MAX;
3217        let min = u16::MAX / 2 - 1;
3218
3219        // Encode data
3220        let result = encode::<Sha256, _>(total, min, data.as_slice(), &STRATEGY);
3221        assert!(matches!(
3222            result,
3223            Err(Error::ReedSolomon(RsError::UnsupportedShardCount {
3224                original_count: _,
3225                recovery_count: _,
3226            }))
3227        ));
3228    }
3229
3230    /// Decoding rejects unsupported shard counts before inspecting any chunk.
3231    #[test]
3232    fn test_decode_rejects_unsupported_shard_counts() {
3233        let data = vec![42u8; 1000];
3234        let (root, chunks) = encode::<Sha256, _>(10, 4, data.as_slice(), &STRATEGY).unwrap();
3235        let checked = chunks
3236            .into_iter()
3237            .map(|c| checked(root, c))
3238            .collect::<Vec<_>>();
3239        let result =
3240            decode::<Sha256, _>(u16::MAX, u16::MAX / 2 - 1, &root, checked.iter(), &STRATEGY);
3241        assert!(matches!(
3242            result,
3243            Err(Error::ReedSolomon(RsError::UnsupportedShardCount { .. }))
3244        ));
3245    }
3246
3247    #[test]
3248    fn test_too_many_total_shards() {
3249        assert!(
3250            RS::encode(
3251                &Config {
3252                    minimum_shards: NZU16!(u16::MAX / 2 + 1),
3253                    extra_shards: NZU16!(u16::MAX),
3254                },
3255                [].as_slice(),
3256                &STRATEGY,
3257            )
3258            .is_err()
3259        )
3260    }
3261
3262    #[test]
3263    fn test_max_shard_size() {
3264        let config = |min, extra| Config {
3265            minimum_shards: NZU16!(min),
3266            extra_shards: NZU16!(extra),
3267        };
3268
3269        // Chunk 0 has the longest proof, since totals that are not powers of two give some later
3270        // chunks shorter proofs. The bound is chunk 0's shard and proof with both length prefixes
3271        // at their widest, and it never decreases as data grows.
3272        for (min, extra) in [(1, 1), (2, 1), (3, 4), (171, 342), (342, 682)] {
3273            let config = config(min, extra);
3274            let mut previous = 0;
3275            for len in [0, 1, 2, 3, 333, 612, 4096, 1 << 20] {
3276                let data = vec![0x67; len];
3277                let (_, chunks) = RS::encode(&config, data.as_slice(), &STRATEGY).unwrap();
3278                let sizes: Vec<_> = chunks.iter().map(|chunk| chunk.encode().len()).collect();
3279                let bound = RS::max_shard_size(&config, len);
3280                assert_eq!(sizes.iter().max(), Some(&sizes[0]));
3281                assert!(sizes[0] <= bound);
3282                assert_eq!(
3283                    bound,
3284                    chunks[0].shard.len()
3285                        + u16::SIZE
3286                        + u32::SIZE
3287                        + chunks[0].proof.siblings.len() * Sha256Digest::SIZE
3288                        + 2 * UInt::<u32>::MAX_ENCODE_SIZE
3289                );
3290                assert!(bound >= previous);
3291                previous = bound;
3292            }
3293        }
3294
3295        // With 16385 original shards, the backend supports at most 32768 recovery shards. The
3296        // bound covers `encode` at that limit, and `encode` rejects one more.
3297        let (_, chunks) = RS::encode(&config(16385, 32768), [].as_slice(), &STRATEGY).unwrap();
3298        let bound = RS::max_shard_size(&config(16385, 32768), 0);
3299        assert!(chunks[0].encode().len() <= bound);
3300        assert_eq!(
3301            bound,
3302            2 + u16::SIZE + u32::SIZE + 16 * Sha256Digest::SIZE + 2 * UInt::<u32>::MAX_ENCODE_SIZE
3303        );
3304        assert!(RS::encode(&config(16385, 32769), [].as_slice(), &STRATEGY).is_err());
3305
3306        // Data up to `u32::MAX` bytes has a bound.
3307        let max: usize = Widen::widen(u32::MAX);
3308        RS::max_shard_size(&config(2, 1), max);
3309    }
3310
3311    #[test]
3312    #[should_panic(expected = "backend must support the shard counts")]
3313    fn test_max_shard_size_unsupported_counts() {
3314        // With 16385 original shards, the backend supports at most 32768 recovery shards.
3315        let config = Config {
3316            minimum_shards: NZU16!(16385),
3317            extra_shards: NZU16!(32769),
3318        };
3319        RS::max_shard_size(&config, 0);
3320    }
3321
3322    #[test]
3323    #[should_panic(expected = "total shards must fit in u16")]
3324    fn test_max_shard_size_too_many_shards() {
3325        // 65535 original shards and one recovery shard exceed `u16::MAX` in total.
3326        let config = Config {
3327            minimum_shards: NZU16!(u16::MAX),
3328            extra_shards: NZU16!(1),
3329        };
3330        RS::max_shard_size(&config, 0);
3331    }
3332
3333    #[test]
3334    #[should_panic(expected = "data must fit the u32 length prefix")]
3335    fn test_max_shard_size_data_beyond_prefix() {
3336        // One byte above `u32::MAX` does not fit the payload's length prefix.
3337        let config = Config {
3338            minimum_shards: NZU16!(2),
3339            extra_shards: NZU16!(1),
3340        };
3341        let max: usize = Widen::widen(u32::MAX);
3342        RS::max_shard_size(&config, max + 1);
3343    }
3344
3345    #[test]
3346    #[should_panic(expected = "len must fit the u32 length prefix")]
3347    fn test_max_shard_size_shard_beyond_prefix() {
3348        // A single original shard of the largest payload is too wide for its length prefix.
3349        let config = Config {
3350            minimum_shards: NZU16!(1),
3351            extra_shards: NZU16!(1),
3352        };
3353        let max: usize = Widen::widen(u32::MAX);
3354        RS::max_shard_size(&config, max);
3355    }
3356
3357    #[test]
3358    fn test_chunk_encode_iobufs_matches_encode() {
3359        let data = b"iobufs encoding test";
3360        let (_root, chunks) = encode::<Sha256, _>(5, 3, data.as_slice(), &STRATEGY).unwrap();
3361        let chunk = &chunks[0];
3362
3363        let encoded = chunk.encode();
3364        let mut encoded_iobufs = chunk.encode_iobufs();
3365        let mut encoded_iobufs_bytes = vec![0u8; encoded_iobufs.remaining()];
3366        encoded_iobufs.copy_to_slice(&mut encoded_iobufs_bytes);
3367        assert_eq!(encoded_iobufs_bytes, encoded.as_ref());
3368    }
3369
3370    #[cfg(feature = "arbitrary")]
3371    mod conformance {
3372        use super::*;
3373        use commonware_codec::conformance::CodecConformance;
3374        use commonware_cryptography::sha256::Digest as Sha256Digest;
3375
3376        commonware_conformance::conformance_tests! {
3377            CodecConformance<Chunk<Sha256Digest>>,
3378        }
3379    }
3380
3381    fn owned_output_codeword(padded: &[u8], k: usize, m: usize) -> Vec<Vec<u8>> {
3382        assert_eq!(padded.len() % k, 0);
3383        let width = padded.len() / k;
3384        let mut encoder = Encoder::new(k, m, width).unwrap();
3385        for shard in padded.chunks_exact(width) {
3386            encoder.add_original_shard(shard).unwrap();
3387        }
3388        let encoded = encoder.encode().unwrap();
3389        let mut shards: Vec<_> = padded.chunks_exact(width).map(<[u8]>::to_vec).collect();
3390        shards.extend(encoded.recovery_iter().map(<[u8]>::to_vec));
3391        shards
3392    }
3393
3394    fn owned_output_reconstructed(shards: &[Vec<u8>], k: usize) -> Vec<Vec<u8>> {
3395        let mut decoder = Decoder::new(k, shards.len() - k, shards[0].len()).unwrap();
3396        for (index, shard) in shards[k..2 * k].iter().enumerate() {
3397            decoder.add_recovery_shard(index, shard).unwrap();
3398        }
3399        let decoded = decoder.decode_with_recovery().unwrap().unwrap();
3400        let mut reconstructed = shards.to_vec();
3401        for (index, shard) in decoded.original_iter() {
3402            reconstructed[index] = shard.to_vec();
3403        }
3404        for (index, shard) in decoded.recovery_iter() {
3405            reconstructed[k + index] = shard.to_vec();
3406        }
3407        reconstructed
3408    }
3409
3410    #[test]
3411    fn test_owned_output_authenticated_rejections() {
3412        let k = 4usize;
3413        let m = 8usize;
3414        let total = u16::try_from(k + m).expect("k + m must fit in u16");
3415        let min = u16::try_from(k).expect("k must fit in u16");
3416        let width = 2 * MIN_STRIPE_BYTES + 2;
3417        let data = vec![0x5A; k * width - u32::SIZE - 1];
3418        let canonical_width = canonical_shard_len(data.len(), k);
3419        let base = prepare_data(data.as_slice(), k, canonical_width);
3420        assert_eq!(canonical_width, width);
3421        assert_eq!(base.last(), Some(&0));
3422        for scenario in ["length_too_large", "length_max", "padding", "width", "root"] {
3423            let mut padded = base.clone();
3424            match scenario {
3425                "length_too_large" => {
3426                    let length = (padded.len() - u32::SIZE + 1) as u32;
3427                    padded[..u32::SIZE].copy_from_slice(&length.to_be_bytes());
3428                }
3429                "length_max" => padded[..u32::SIZE].copy_from_slice(&u32::MAX.to_be_bytes()),
3430                "padding" => *padded.last_mut().unwrap() = 1,
3431                "width" => padded.resize(padded.len() + 2 * k, 0),
3432                "root" => {}
3433                _ => unreachable!(),
3434            }
3435            let mut shards = owned_output_codeword(&padded, k, m);
3436            if scenario == "root" {
3437                // Supplied recoveries still reconstruct the canonical payload. Only an
3438                // unprovided original leaf differs from that reconstruction.
3439                shards[0][u32::SIZE] ^= 1;
3440            }
3441            assert!(shards[0].len() >= 2 * MIN_STRIPE_BYTES);
3442            let (root, chunks) = build_chunks(&shards);
3443            let pieces: Vec<_> = chunks[k..2 * k]
3444                .iter()
3445                .map(|chunk| {
3446                    check_chunk::<Sha256>(total, &root, chunk.index, chunk, || {
3447                        Sha256::hash(&[chunk.shard.as_ref()])
3448                    })
3449                    .unwrap()
3450                })
3451                .collect();
3452
3453            // Establish that RS reconstruction and authentication succeed independently
3454            // of the format or commitment check under test.
3455            let reconstructed = owned_output_reconstructed(&shards, k);
3456            assert_eq!(reconstructed[..k].concat(), padded, "{scenario}");
3457            let originals: Vec<_> = reconstructed[..k].iter().map(Vec::as_slice).collect();
3458            if scenario == "root" {
3459                assert_eq!(extract_data(&originals, k, width).unwrap(), data);
3460                assert_ne!(build_chunks(&reconstructed).0, root);
3461            } else {
3462                assert_eq!(build_chunks(&reconstructed).0, root, "{scenario}");
3463                match scenario {
3464                    "length_too_large" | "length_max" => {
3465                        assert!(matches!(
3466                            read_data_len(&originals),
3467                            Err(Error::Inconsistent)
3468                        ));
3469                    }
3470                    "padding" => {
3471                        let len = read_data_len(&originals).unwrap();
3472                        assert_eq!(canonical_shard_len(len, k), shards[0].len());
3473                        assert_eq!(len, data.len());
3474                        assert_eq!(&padded[u32::SIZE..u32::SIZE + len], data);
3475                        assert_eq!(&padded[u32::SIZE + len..], &[1]);
3476                    }
3477                    "width" => {
3478                        let len = read_data_len(&originals).unwrap();
3479                        assert_eq!(len, data.len());
3480                        assert!(padded[u32::SIZE + len..].iter().all(|byte| *byte == 0));
3481                        assert_ne!(canonical_shard_len(len, k), shards[0].len());
3482                    }
3483                    _ => unreachable!(),
3484                }
3485            }
3486            let rejected = explore(|strategy| {
3487                matches!(
3488                    decode::<Sha256, _>(total, min, &root, pieces.iter(), strategy),
3489                    Err(Error::Inconsistent)
3490                )
3491            });
3492            assert!(rejected, "{scenario}");
3493        }
3494    }
3495
3496    #[test]
3497    fn test_owned_output_recovery_and_mixed_roundtrip() {
3498        let total = 12u16;
3499        let min = 4u16;
3500        for width in [
3501            2 * MIN_STRIPE_BYTES,
3502            2 * MIN_STRIPE_BYTES + 2,
3503            3 * MIN_STRIPE_BYTES + 62,
3504        ] {
3505            let data: Vec<_> = (0..min as usize * width - u32::SIZE - 1)
3506                .map(|index| (index ^ (index >> 8)) as u8)
3507                .collect();
3508            let (root, chunks) =
3509                encode::<Sha256, _>(total, min, data.as_slice(), &Sequential).unwrap();
3510            assert_eq!(chunks[0].shard.len(), width);
3511            for selected in [[4usize, 5, 6, 7], [0, 4, 5, 6]] {
3512                let pieces: Vec<_> = selected
3513                    .into_iter()
3514                    .map(|index| {
3515                        let chunk = &chunks[index];
3516                        check_chunk::<Sha256>(total, &root, chunk.index, chunk, || {
3517                            Sha256::hash(&[chunk.shard.as_ref()])
3518                        })
3519                        .unwrap()
3520                    })
3521                    .collect();
3522                assert_eq!(
3523                    explore(|strategy| {
3524                        decode::<Sha256, _>(total, min, &root, pieces.iter(), strategy).unwrap()
3525                    }),
3526                    data
3527                );
3528            }
3529        }
3530    }
3531
3532    #[test]
3533    fn test_read_data_len_prefix_bounds() {
3534        let prefix = [0u8; u32::SIZE];
3535        for len in 0..u32::SIZE {
3536            assert!(matches!(
3537                read_data_len(&[&prefix[..len]]),
3538                Err(Error::Inconsistent)
3539            ));
3540        }
3541        assert_eq!(read_data_len(&[&prefix[..1], &prefix[1..]]).unwrap(), 0);
3542    }
3543}