Skip to main content

commonware_coding/
lib.rs

1//! Encode data to enable recovery from a subset of fragments.
2//!
3//! # Status
4//!
5//! Stability varies by primitive. See [README](https://github.com/commonwarexyz/monorepo#stability) for details.
6
7#![doc(
8    html_logo_url = "https://commonware.xyz/imgs/rustdoc_logo.svg",
9    html_favicon_url = "https://commonware.xyz/favicon.ico"
10)]
11
12commonware_macros::stability_scope!(ALPHA {
13    use bytes::Buf;
14    use commonware_codec::{Codec, FixedSize, Read, Write};
15    use commonware_cryptography::Digest;
16    use commonware_parallel::Strategy;
17    use std::{fmt::Debug, num::NonZeroU16};
18    use thiserror::Error;
19
20    mod reed_solomon;
21    pub use reed_solomon::{Error as ReedSolomonError, ReedSolomon};
22
23    mod zoda;
24    pub use zoda::{Error as ZodaError, Zoda};
25
26    /// Configuration common to all encoding schemes.
27    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28    pub struct Config {
29        /// The minimum number of shards needed to encode the data.
30        pub minimum_shards: NonZeroU16,
31        /// Extra shards beyond the minimum number.
32        ///
33        /// Alternatively, one can think of the configuration as having a total number
34        /// `N = extra_shards + minimum_shards`, but by specifying the `extra_shards`
35        /// rather than `N`, we avoid needing to check that `minimum_shards <= N`.
36        pub extra_shards: NonZeroU16,
37    }
38
39    impl Config {
40        /// Returns the total number of shards produced by this configuration.
41        pub fn total_shards(&self) -> u32 {
42            u32::from(self.minimum_shards.get()) + u32::from(self.extra_shards.get())
43        }
44    }
45
46    impl FixedSize for Config {
47        const SIZE: usize = 2 * <NonZeroU16 as FixedSize>::SIZE;
48    }
49
50    impl Write for Config {
51        fn write(&self, buf: &mut impl bytes::BufMut) {
52            self.minimum_shards.write(buf);
53            self.extra_shards.write(buf);
54        }
55    }
56
57    impl Read for Config {
58        type Cfg = ();
59
60        fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
61            Ok(Self {
62                minimum_shards: NonZeroU16::read_cfg(buf, cfg)?,
63                extra_shards: NonZeroU16::read_cfg(buf, cfg)?,
64            })
65        }
66    }
67
68    #[cfg(feature = "arbitrary")]
69    impl<'a> arbitrary::Arbitrary<'a> for Config {
70        fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
71            let minimum_shards = commonware_utils::NZU16!(u.int_in_range(1..=u16::MAX)?);
72            let extra_shards = commonware_utils::NZU16!(u.int_in_range(1..=u16::MAX)?);
73            Ok(Self {
74                minimum_shards,
75                extra_shards,
76            })
77        }
78    }
79
80    /// The configuration for decoding shard data.
81    #[derive(Clone, Debug)]
82    pub struct CodecConfig {
83        /// The maximum number of bytes a shard is expected to contain.
84        ///
85        /// This can be an upper bound, and only constrains the non-fixed-size portion
86        /// of shard data.
87        pub maximum_shard_size: usize,
88    }
89
90    /// A scheme for encoding data into pieces, and recovering the data from those pieces.
91    ///
92    /// # Example
93    /// ```
94    /// use commonware_coding::{Config, ReedSolomon, Scheme as _};
95    /// use commonware_cryptography::Sha256;
96    /// use commonware_parallel::Sequential;
97    /// use commonware_utils::NZU16;
98    ///
99    /// const STRATEGY: Sequential = Sequential;
100    ///
101    /// type RS = ReedSolomon<Sha256>;
102    ///
103    /// let config = Config {
104    ///     minimum_shards: NZU16!(2),
105    ///     extra_shards: NZU16!(1),
106    /// };
107    /// let data = b"Hello!";
108    /// // Turn the data into shards, and a commitment to those shards.
109    /// let (commitment, shards) =
110    ///      RS::encode(&config, data.as_slice(), &STRATEGY).unwrap();
111    ///
112    /// // Each participant checks their shard against the commitment.
113    /// let checked_shards: Vec<_> = shards
114    ///         .iter()
115    ///         .enumerate()
116    ///         .map(|(i, shard)| {
117    ///             RS::check(&config, &commitment, i as u16, shard).unwrap()
118    ///         })
119    ///         .collect();
120    ///
121    /// // Decode from any minimum_shards-sized subset.
122    /// let data2 = RS::decode(&config, &commitment, checked_shards[..2].iter(), &STRATEGY).unwrap();
123    /// assert_eq!(&data[..], &data2[..]);
124    ///
125    /// // Decoding works with different shards, with a guarantee to get the same result.
126    /// let data3 = RS::decode(&config, &commitment, checked_shards[1..].iter(), &STRATEGY).unwrap();
127    /// assert_eq!(&data[..], &data3[..]);
128    /// ```
129    ///
130    /// # Guarantees
131    ///
132    /// Here are additional properties that implementors of this trait need to
133    /// consider, and that users of this trait can rely on.
134    ///
135    /// ## Check Agreement
136    ///
137    /// [`Scheme::check`] should agree across honest parties, even for malicious
138    /// encoders.
139    ///
140    /// It should not be possible for parties A and B to both call `check`
141    /// successfully on their own shards, but then have either of them fail
142    /// when calling `check` on the other's shard.
143    ///
144    /// In other words, if an honest party considers their shard to be correctly
145    /// formed, then other honest parties which have also successfully checked
146    /// their own shards will agree with that shard being correct.
147    ///
148    /// A violation of this property would be, for example, if a malicious
149    /// payload could convince two parties that they both have valid shards, but
150    /// then checking each other's shards reports issues with those shards.
151    ///
152    /// ## Unique Commitments
153    ///
154    /// [`Scheme::encode`] MUST be deterministic.
155    ///
156    /// For a given [`Config`] and `data`, the only [`Scheme::Commitment`] which
157    /// should pass [`Scheme::decode`] MUST be that produced by [`Scheme::encode`].
158    ///
159    /// In other words, a data has a unique valid commitment associated with it.
160    pub trait Scheme: Debug + Clone + Send + Sync + 'static {
161        /// A commitment attesting to the shards of data.
162        type Commitment: Digest;
163        /// A shard of data, to be received by a participant.
164        type Shard: Clone + Debug + Eq + Codec<Cfg = CodecConfig> + Send + Sync + 'static;
165        /// A shard that has been checked for inclusion in the commitment.
166        ///
167        /// This allows excluding invalid shards from the function signature of [`Self::decode`].
168        type CheckedShard: Clone + Send + Sync;
169        /// The type of errors that can occur during encoding, checking, and decoding.
170        type Error: std::fmt::Debug + Send;
171
172        /// Encode a piece of data, returning a commitment, along with shards, and proofs.
173        ///
174        /// Each shard and proof is intended for exactly one participant. The number of shards returned
175        /// should equal `config.minimum_shards + config.extra_shards`.
176        #[allow(clippy::type_complexity)]
177        fn encode(
178            config: &Config,
179            data: impl Buf,
180            strategy: &impl Strategy,
181        ) -> Result<(Self::Commitment, Vec<Self::Shard>), Self::Error>;
182
183        /// Check the integrity of a shard, producing a checked shard.
184        ///
185        /// This takes in an index, to make sure that the shard you're checking
186        /// is associated with the participant you expect it to be.
187        fn check(
188            config: &Config,
189            commitment: &Self::Commitment,
190            index: u16,
191            shard: &Self::Shard,
192        ) -> Result<Self::CheckedShard, Self::Error>;
193
194        /// Decode the data from shards received from other participants.
195        ///
196        /// The data must be decodeable with as few as `config.minimum_shards`,
197        /// including your own shard.
198        ///
199        /// Calls to this function with the same commitment, but with different shards,
200        /// or shards in a different order should also result in the same output data,
201        /// or in failure. In other words, when using the decoding function in a broader
202        /// system, you get a guarantee that every participant decoding will see the same
203        /// final data, even if they receive different shards, or receive them in a
204        /// different order.
205        ///
206        /// ## Commitment Binding
207        ///
208        /// Implementations must reject shards that were checked against a different
209        /// commitment than the one passed to `decode`. Mixing checked shards from
210        /// separate `encode` calls (and thus different commitments) must return an
211        /// error.
212        fn decode<'a>(
213            config: &Config,
214            commitment: &Self::Commitment,
215            shards: impl Iterator<Item = &'a Self::CheckedShard>,
216            strategy: &impl Strategy,
217        ) -> Result<Vec<u8>, Self::Error>;
218    }
219
220    /// A phased coding interface with separate local and forwarded shard handling.
221    ///
222    /// This trait models schemes where the initial distributor attaches extra
223    /// verification material to each participant's strong shard. Participants
224    /// derive checking data from that strong shard, then use it to validate
225    /// weaker forwarded shards received from others before reconstruction.
226    ///
227    /// The tradeoff compared to [`Scheme`] is that weak shards cannot be
228    /// verified independently. A participant must first derive
229    /// [`PhasedScheme::CheckingData`] from a strong shard via
230    /// [`PhasedScheme::weaken`].
231    ///
232    /// # Example
233    /// ```
234    /// use commonware_coding::{Config, PhasedScheme as _, Zoda};
235    /// use commonware_cryptography::Sha256;
236    /// use commonware_parallel::Sequential;
237    /// use commonware_utils::NZU16;
238    ///
239    /// const STRATEGY: Sequential = Sequential;
240    ///
241    /// type Z = Zoda<Sha256>;
242    ///
243    /// let config = Config {
244    ///     minimum_shards: NZU16!(2),
245    ///     extra_shards: NZU16!(1),
246    /// };
247    /// let data = b"Hello!";
248    /// let (commitment, mut shards) = Z::encode(&config, data.as_slice(), &STRATEGY).unwrap();
249    ///
250    /// let (checking_data, checked_0, _) =
251    ///     Z::weaken(&config, &commitment, 0, shards.remove(0)).unwrap();
252    /// let (_, _, weak_1) = Z::weaken(&config, &commitment, 1, shards.remove(0)).unwrap();
253    /// let checked_1 = Z::check(&config, &commitment, &checking_data, 1, weak_1).unwrap();
254    ///
255    /// let data2 = Z::decode(
256    ///     &config,
257    ///     &commitment,
258    ///     checking_data,
259    ///     [checked_0, checked_1].iter(),
260    ///     &STRATEGY,
261    /// )
262    /// .unwrap();
263    /// assert_eq!(&data[..], &data2[..]);
264    /// ```
265    ///
266    /// # Guarantees
267    ///
268    /// Here are additional properties that implementors of this trait need to
269    /// consider, and that users of this trait can rely on.
270    ///
271    /// ## Weaken vs Check
272    ///
273    /// [`PhasedScheme::weaken`] and [`PhasedScheme::check`] should agree, even for malicious encoders.
274    ///
275    /// It should not be possible for parties A and B to call `weaken` successfully,
276    /// but then have either of them fail on the other's shard when calling `check`.
277    ///
278    /// In other words, if an honest party considers their shard to be correctly
279    /// formed, then other honest parties which have successfully constructed their
280    /// checking data will also agree with the shard being correct.
281    ///
282    /// A violation of this property would be, for example, if a malicious payload
283    /// could convince two parties that they both have valid shards, but then the
284    /// checking data they produce from the malicious payload reports issues with
285    /// those shards.
286    pub trait PhasedScheme: Debug + Clone + Send + Sync + 'static {
287        /// A commitment attesting to the shards of data.
288        type Commitment: Digest;
289        /// A strong shard of data, to be received by a participant.
290        type StrongShard: Clone + Debug + Eq + Codec<Cfg = CodecConfig> + Send + Sync + 'static;
291        /// A weak shard shared with other participants, to aid them in reconstruction.
292        ///
293        /// In most cases, this will be the same as `StrongShard`, but some schemes might
294        /// have extra information in `StrongShard` that may not be necessary to reconstruct
295        /// the data.
296        type WeakShard: Clone + Debug + Eq + Codec<Cfg = CodecConfig> + Send + Sync + 'static;
297        /// Data which can assist in checking shards.
298        type CheckingData: Clone + Eq + Send + Sync;
299        /// A shard that has been checked for inclusion in the commitment.
300        ///
301        /// This allows excluding [`PhasedScheme::WeakShard`]s which are invalid, and shouldn't
302        /// be considered as progress towards meeting the minimum number of shards.
303        type CheckedShard: Clone + Send + Sync;
304        /// The type of errors that can occur during encoding, weakening, checking, and decoding.
305        type Error: std::fmt::Debug + Send;
306
307        /// Encode a piece of data, returning a commitment, along with shards, and proofs.
308        ///
309        /// Each shard and proof is intended for exactly one participant. The number of shards returned
310        /// should equal `config.minimum_shards + config.extra_shards`.
311        #[allow(clippy::type_complexity)]
312        fn encode(
313            config: &Config,
314            data: impl Buf,
315            strategy: &impl Strategy,
316        ) -> Result<(Self::Commitment, Vec<Self::StrongShard>), Self::Error>;
317
318        /// Take your own shard, check it, and produce a [`PhasedScheme::WeakShard`] to forward to others.
319        ///
320        /// This takes in an index, which is the index you expect the shard to be.
321        ///
322        /// This will produce a [`PhasedScheme::CheckedShard`] which counts towards the minimum
323        /// number of shards you need to reconstruct the data, in [`PhasedScheme::decode`].
324        ///
325        /// You also get [`PhasedScheme::CheckingData`], which has information you can use to check
326        /// the shards you receive from others.
327        #[allow(clippy::type_complexity)]
328        fn weaken(
329            config: &Config,
330            commitment: &Self::Commitment,
331            index: u16,
332            shard: Self::StrongShard,
333        ) -> Result<(Self::CheckingData, Self::CheckedShard, Self::WeakShard), Self::Error>;
334
335        /// Check the integrity of a weak shard, producing a checked shard.
336        ///
337        /// This requires the [`PhasedScheme::CheckingData`] produced by [`PhasedScheme::weaken`].
338        ///
339        /// This takes in an index, to make sure that the weak shard you're checking
340        /// is associated with the participant you expect it to be.
341        fn check(
342            config: &Config,
343            commitment: &Self::Commitment,
344            checking_data: &Self::CheckingData,
345            index: u16,
346            weak_shard: Self::WeakShard,
347        ) -> Result<Self::CheckedShard, Self::Error>;
348
349        /// Decode the data from shards received from other participants.
350        ///
351        /// The data must be decodeable with as few as `config.minimum_shards`,
352        /// including your own shard.
353        ///
354        /// Calls to this function with the same commitment, but with different shards,
355        /// or shards in a different should also result in the same output data, or in failure.
356        /// In other words, when using the decoding function in a broader system, you
357        /// get a guarantee that every participant decoding will see the same final
358        /// data, even if they receive different shards, or receive them in a different order.
359        ///
360        /// ## Commitment Binding
361        ///
362        /// Implementations must reject shards that were checked against a different
363        /// commitment than the one passed to `decode`. Mixing checked shards from
364        /// separate `encode` calls (and thus different commitments) must return an
365        /// error.
366        fn decode<'a>(
367            config: &Config,
368            commitment: &Self::Commitment,
369            checking_data: Self::CheckingData,
370            shards: impl Iterator<Item = &'a Self::CheckedShard>,
371            strategy: &impl Strategy,
372        ) -> Result<Vec<u8>, Self::Error>;
373    }
374
375    /// An adapter that exposes a [`PhasedScheme`] through the [`Scheme`] trait.
376    ///
377    /// In most cases, this is not the most optimal way to use a [`PhasedScheme`],
378    /// or to expose a [`PhasedScheme`] as a [`Scheme`] for that matter. However,
379    /// it can be useful for testing or for usecases where the phased scheme
380    /// cannot be used directly.
381    #[derive(Clone, Copy, Debug, Default)]
382    pub struct PhasedAsScheme<P>(core::marker::PhantomData<P>);
383
384    /// A checked shard produced by adapting a phased scheme into [`Scheme`].
385    #[derive(Clone)]
386    pub struct PhasedCheckedShard<P: PhasedScheme> {
387        checking_data: P::CheckingData,
388        checked_shard: P::CheckedShard,
389    }
390
391    /// Errors returned by the [`PhasedAsScheme`] adapter.
392    #[derive(Debug, Error)]
393    pub enum PhasedAsSchemeError<E> {
394        #[error(transparent)]
395        Scheme(E),
396        #[error("checked shards do not agree on checking data")]
397        InconsistentCheckingData,
398        #[error("insufficient shards {0} < {1}")]
399        InsufficientShards(usize, usize),
400    }
401
402    impl<P: PhasedScheme> Debug for PhasedCheckedShard<P> {
403        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
404            f.debug_struct("PhasedCheckedShard").finish_non_exhaustive()
405        }
406    }
407
408    impl<P: PhasedScheme> Scheme for PhasedAsScheme<P> {
409        type Commitment = P::Commitment;
410        type Shard = P::StrongShard;
411        type CheckedShard = PhasedCheckedShard<P>;
412        type Error = PhasedAsSchemeError<P::Error>;
413
414        fn encode(
415            config: &Config,
416            data: impl Buf,
417            strategy: &impl Strategy,
418        ) -> Result<(Self::Commitment, Vec<Self::Shard>), Self::Error> {
419            P::encode(config, data, strategy).map_err(PhasedAsSchemeError::Scheme)
420        }
421
422        fn check(
423            config: &Config,
424            commitment: &Self::Commitment,
425            index: u16,
426            shard: &Self::Shard,
427        ) -> Result<Self::CheckedShard, Self::Error> {
428            let (checking_data, checked_shard, _) =
429                P::weaken(config, commitment, index, shard.clone())
430                    .map_err(PhasedAsSchemeError::Scheme)?;
431            Ok(PhasedCheckedShard {
432                checking_data,
433                checked_shard,
434            })
435        }
436
437        fn decode<'a>(
438            config: &Config,
439            commitment: &Self::Commitment,
440            shards: impl Iterator<Item = &'a Self::CheckedShard>,
441            strategy: &impl Strategy,
442        ) -> Result<Vec<u8>, Self::Error> {
443            let mut shards = shards.peekable();
444            let Some(first) = shards.peek() else {
445                return Err(PhasedAsSchemeError::InsufficientShards(
446                    0,
447                    usize::from(config.minimum_shards.get()),
448                ));
449            };
450            let checking_data = first.checking_data.clone();
451            P::decode(
452                config,
453                commitment,
454                checking_data.clone(),
455                shards
456                    .map(|shard| {
457                        if shard.checking_data != checking_data {
458                            return Err(PhasedAsSchemeError::InconsistentCheckingData);
459                        }
460                        Ok(&shard.checked_shard)
461                    })
462                    .collect::<Result<Vec<_>, _>>()?
463                    .into_iter(),
464                strategy,
465            )
466            .map_err(PhasedAsSchemeError::Scheme)
467        }
468    }
469
470    /// A marker trait indicating that [`Scheme::check`] or [`PhasedScheme::check`] proves validity of the encoding.
471    ///
472    /// In more detail, this means that upon a successful call to [`Scheme::check`],
473    /// guarantees that the shard results from a valid encoding of the data, and thus,
474    /// if other participants also call check, then the data is guaranteed to be reconstructable.
475    pub trait ValidatingScheme {}
476});
477
478#[cfg(test)]
479mod test {
480    use super::*;
481    use arbitrary::Unstructured;
482    use commonware_cryptography::Sha256;
483    use commonware_invariants::minifuzz;
484    use commonware_macros::test_group;
485    use commonware_utils::NZU16;
486
487    const MAX_SHARD_SIZE: usize = 1 << 31;
488    const MAX_SHARDS: u16 = 32;
489    const MAX_DATA: usize = 1024;
490    const MIN_EXTRA_SHARDS: u16 = 1;
491
492    fn generate_case(u: &mut Unstructured<'_>) -> arbitrary::Result<(Config, Vec<u8>, Vec<u16>)> {
493        let minimum_shards = (u.arbitrary::<u16>()? % MAX_SHARDS) + 1;
494        let extra_shards =
495            MIN_EXTRA_SHARDS + (u.arbitrary::<u16>()? % (MAX_SHARDS - MIN_EXTRA_SHARDS + 1));
496        let total_shards = minimum_shards + extra_shards;
497
498        let data_len = usize::from(u.arbitrary::<u16>()?) % (MAX_DATA + 1);
499        let data = u.bytes(data_len)?.to_vec();
500
501        let selected_len = usize::from(minimum_shards)
502            + (usize::from(u.arbitrary::<u16>()?) % (usize::from(extra_shards) + 1));
503        let mut selected: Vec<u16> = (0..total_shards).collect();
504        for i in 0..selected_len {
505            let remaining = usize::from(total_shards) - i;
506            let j = i + (usize::from(u.arbitrary::<u16>()?) % remaining);
507            selected.swap(i, j);
508        }
509        selected.truncate(selected_len);
510
511        Ok((
512            Config {
513                minimum_shards: NZU16!(minimum_shards),
514                extra_shards: NZU16!(extra_shards),
515            },
516            data,
517            selected,
518        ))
519    }
520
521    mod scheme {
522        use super::*;
523        use crate::{reed_solomon::ReedSolomon, PhasedAsScheme, Scheme, Zoda};
524        use commonware_codec::Encode;
525        use commonware_parallel::Sequential;
526
527        fn roundtrip<S: Scheme>(config: &Config, data: &[u8], selected: &[u16]) {
528            let (commitment, shards) = S::encode(config, data, &Sequential).unwrap();
529            let read_cfg = CodecConfig {
530                maximum_shard_size: MAX_SHARD_SIZE,
531            };
532            for shard in &shards {
533                let decoded_shard = S::Shard::read_cfg(&mut shard.encode(), &read_cfg).unwrap();
534                assert_eq!(decoded_shard, *shard);
535            }
536
537            let mut checked_shards = Vec::new();
538            for (i, shard) in shards.into_iter().enumerate() {
539                if !selected.contains(&(i as u16)) {
540                    continue;
541                }
542                let checked = S::check(config, &commitment, i as u16, &shard).unwrap();
543                checked_shards.push(checked);
544            }
545
546            checked_shards.reverse();
547            let decoded =
548                S::decode(config, &commitment, checked_shards.iter(), &Sequential).unwrap();
549            assert_eq!(decoded, data);
550        }
551
552        fn decode_rejects_mixed_commitments<S: Scheme>(
553            config: &Config,
554            data_a: &[u8],
555            data_b: &[u8],
556        ) {
557            let (commitment_a, shards_a) = S::encode(config, data_a, &Sequential).unwrap();
558            let (commitment_b, shards_b) = S::encode(config, data_b, &Sequential).unwrap();
559
560            let checked_a = S::check(config, &commitment_a, 0, &shards_a[0]).unwrap();
561            let checked_b = S::check(config, &commitment_b, 1, &shards_b[1]).unwrap();
562
563            let result = S::decode(
564                config,
565                &commitment_a,
566                [checked_a, checked_b].iter(),
567                &Sequential,
568            );
569            assert!(
570                result.is_err(),
571                "decode must reject shards checked against different commitments"
572            );
573        }
574
575        fn decode_rejects_empty_checked_shards<S: Scheme>(config: &Config, data: &[u8]) {
576            let (commitment, _) = S::encode(config, data, &Sequential).unwrap();
577            let result = S::decode(config, &commitment, core::iter::empty(), &Sequential);
578            assert!(
579                result.is_err(),
580                "decode must reject empty checked shard sets"
581            );
582        }
583
584        #[test]
585        fn decode_rejects_mixed_commitment_shards() {
586            let config = Config {
587                minimum_shards: NZU16!(2),
588                extra_shards: NZU16!(1),
589            };
590
591            decode_rejects_mixed_commitments::<ReedSolomon<Sha256>>(
592                &config,
593                b"alpha payload",
594                b"bravo payload",
595            );
596            decode_rejects_mixed_commitments::<PhasedAsScheme<Zoda<Sha256>>>(
597                &config,
598                b"alpha payload",
599                b"bravo payload",
600            );
601            decode_rejects_empty_checked_shards::<ReedSolomon<Sha256>>(&config, b"alpha payload");
602            decode_rejects_empty_checked_shards::<PhasedAsScheme<Zoda<Sha256>>>(
603                &config,
604                b"alpha payload",
605            );
606        }
607
608        #[test]
609        fn roundtrip_empty_data() {
610            let config = Config {
611                minimum_shards: NZU16!(30),
612                extra_shards: NZU16!(70),
613            };
614            let selected: Vec<u16> = (0..30).collect();
615
616            roundtrip::<ReedSolomon<Sha256>>(&config, b"", &selected);
617            roundtrip::<PhasedAsScheme<Zoda<Sha256>>>(&config, b"", &selected);
618        }
619
620        #[test]
621        fn roundtrip_2_pow_16_25_total_shards() {
622            let config = Config {
623                minimum_shards: NZU16!(8),
624                extra_shards: NZU16!(17),
625            };
626            let data = vec![0x67; 1 << 16];
627            let selected: Vec<u16> = (0..8).collect();
628
629            roundtrip::<ReedSolomon<Sha256>>(&config, &data, &selected);
630            roundtrip::<PhasedAsScheme<Zoda<Sha256>>>(&config, &data, &selected);
631        }
632
633        #[test]
634        fn minifuzz_roundtrip_reed_solomon() {
635            minifuzz::test(|u| {
636                let (config, data, selected) = generate_case(u)?;
637                roundtrip::<ReedSolomon<Sha256>>(&config, &data, &selected);
638                Ok(())
639            });
640        }
641
642        #[test_group("slow")]
643        #[test]
644        fn minifuzz_roundtrip_zoda() {
645            minifuzz::Builder::default()
646                .with_search_limit(64)
647                .test(|u| {
648                    let (config, data, selected) = generate_case(u)?;
649                    roundtrip::<PhasedAsScheme<Zoda<Sha256>>>(&config, &data, &selected);
650                    Ok(())
651                });
652        }
653    }
654
655    mod phased_scheme {
656        use super::*;
657        use crate::{PhasedScheme, Zoda};
658        use commonware_codec::Encode;
659        use commonware_parallel::Sequential;
660
661        fn roundtrip<S: PhasedScheme>(config: &Config, data: &[u8], selected: &[u16]) {
662            let owner = *selected.first().expect("selected must not be empty");
663            let (commitment, shards) = S::encode(config, data, &Sequential).unwrap();
664            let read_cfg = CodecConfig {
665                maximum_shard_size: MAX_SHARD_SIZE,
666            };
667            for shard in &shards {
668                let decoded_shard =
669                    S::StrongShard::read_cfg(&mut shard.encode(), &read_cfg).unwrap();
670                assert_eq!(decoded_shard, *shard);
671            }
672
673            let (checking_data, own_checked, _) =
674                S::weaken(config, &commitment, owner, shards[owner as usize].clone()).unwrap();
675            let mut checked_shards = vec![own_checked];
676            for &index in selected {
677                if index == owner {
678                    continue;
679                }
680                let (_, _, weak_shard) =
681                    S::weaken(config, &commitment, index, shards[index as usize].clone()).unwrap();
682                let decoded_weak =
683                    S::WeakShard::read_cfg(&mut weak_shard.encode(), &read_cfg).unwrap();
684                assert_eq!(decoded_weak, weak_shard);
685                let checked =
686                    S::check(config, &commitment, &checking_data, index, decoded_weak).unwrap();
687                checked_shards.push(checked);
688            }
689
690            checked_shards.reverse();
691            let decoded = S::decode(
692                config,
693                &commitment,
694                checking_data,
695                checked_shards.iter(),
696                &Sequential,
697            )
698            .unwrap();
699            assert_eq!(decoded, data);
700        }
701
702        fn check_rejects_mixed_commitments<S: PhasedScheme>(
703            config: &Config,
704            data_a: &[u8],
705            data_b: &[u8],
706        ) {
707            let (commitment_a, shards_a) = S::encode(config, data_a, &Sequential).unwrap();
708            let (commitment_b, shards_b) = S::encode(config, data_b, &Sequential).unwrap();
709
710            let (checking_data_a, checked_a, _) =
711                S::weaken(config, &commitment_a, 0, shards_a[0].clone()).unwrap();
712            let (checking_data_b, checked_b, weak_b) =
713                S::weaken(config, &commitment_b, 1, shards_b[1].clone()).unwrap();
714
715            let check_result = S::check(config, &commitment_a, &checking_data_a, 1, weak_b);
716            assert!(
717                check_result.is_err(),
718                "check must reject weak shards derived from a different commitment"
719            );
720
721            let decode_result = S::decode(
722                config,
723                &commitment_a,
724                checking_data_a,
725                [checked_a, checked_b].iter(),
726                &Sequential,
727            );
728            assert!(
729                decode_result.is_err(),
730                "decode must reject checked shards derived from a different commitment"
731            );
732
733            let decode_result = S::decode(
734                config,
735                &commitment_b,
736                checking_data_b,
737                core::iter::empty(),
738                &Sequential,
739            );
740            assert!(
741                decode_result.is_err(),
742                "decode must reject insufficient checked shards"
743            );
744        }
745
746        #[test]
747        fn check_rejects_mixed_commitment_weak_shards() {
748            let config = Config {
749                minimum_shards: NZU16!(2),
750                extra_shards: NZU16!(1),
751            };
752
753            check_rejects_mixed_commitments::<Zoda<Sha256>>(
754                &config,
755                b"alpha payload",
756                b"bravo payload",
757            );
758        }
759
760        #[test]
761        fn roundtrip_empty_data() {
762            let config = Config {
763                minimum_shards: NZU16!(30),
764                extra_shards: NZU16!(70),
765            };
766            let selected: Vec<u16> = (0..30).collect();
767
768            roundtrip::<Zoda<Sha256>>(&config, b"", &selected);
769        }
770
771        #[test]
772        fn roundtrip_2_pow_16_25_total_shards() {
773            let config = Config {
774                minimum_shards: NZU16!(8),
775                extra_shards: NZU16!(17),
776            };
777            let data = vec![0x67; 1 << 16];
778            let selected: Vec<u16> = (0..8).collect();
779
780            roundtrip::<Zoda<Sha256>>(&config, &data, &selected);
781        }
782
783        #[test_group("slow")]
784        #[test]
785        fn minifuzz_roundtrip_zoda() {
786            minifuzz::Builder::default()
787                .with_search_limit(64)
788                .test(|u| {
789                    let (config, data, selected) = generate_case(u)?;
790                    roundtrip::<Zoda<Sha256>>(&config, &data, &selected);
791                    Ok(())
792                });
793        }
794    }
795
796    #[cfg(feature = "arbitrary")]
797    mod conformance {
798        use super::*;
799        use commonware_codec::conformance::CodecConformance;
800
801        commonware_conformance::conformance_tests! {
802            CodecConformance<Config>,
803        }
804    }
805}