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 namespace = b"my-application";
244    /// let config = Config {
245    ///     minimum_shards: NZU16!(2),
246    ///     extra_shards: NZU16!(1),
247    /// };
248    /// let data = b"Hello!";
249    /// let (commitment, mut shards) = Z::encode(namespace, &config, data.as_slice(), &STRATEGY).unwrap();
250    ///
251    /// let (checking_data, checked_0, _) =
252    ///     Z::weaken(namespace, &config, &commitment, 0, shards.remove(0)).unwrap();
253    /// let (_, _, weak_1) = Z::weaken(namespace, &config, &commitment, 1, shards.remove(0)).unwrap();
254    /// let checked_1 = Z::check(&config, &commitment, &checking_data, 1, weak_1).unwrap();
255    ///
256    /// let data2 = Z::decode(
257    ///     &config,
258    ///     &commitment,
259    ///     checking_data,
260    ///     [checked_0, checked_1].iter(),
261    ///     &STRATEGY,
262    /// )
263    /// .unwrap();
264    /// assert_eq!(&data[..], &data2[..]);
265    /// ```
266    ///
267    /// # Guarantees
268    ///
269    /// Here are additional properties that implementors of this trait need to
270    /// consider, and that users of this trait can rely on.
271    ///
272    /// ## Weaken vs Check
273    ///
274    /// [`PhasedScheme::weaken`] and [`PhasedScheme::check`] should agree, even for malicious encoders.
275    ///
276    /// It should not be possible for parties A and B to call `weaken` successfully,
277    /// but then have either of them fail on the other's shard when calling `check`.
278    ///
279    /// In other words, if an honest party considers their shard to be correctly
280    /// formed, then other honest parties which have successfully constructed their
281    /// checking data will also agree with the shard being correct.
282    ///
283    /// A violation of this property would be, for example, if a malicious payload
284    /// could convince two parties that they both have valid shards, but then the
285    /// checking data they produce from the malicious payload reports issues with
286    /// those shards.
287    pub trait PhasedScheme: Debug + Clone + Send + Sync + 'static {
288        /// A commitment attesting to the shards of data.
289        type Commitment: Digest;
290        /// A strong shard of data, to be received by a participant.
291        type StrongShard: Clone + Debug + Eq + Codec<Cfg = CodecConfig> + Send + Sync + 'static;
292        /// A weak shard shared with other participants, to aid them in reconstruction.
293        ///
294        /// In most cases, this will be the same as `StrongShard`, but some schemes might
295        /// have extra information in `StrongShard` that may not be necessary to reconstruct
296        /// the data.
297        type WeakShard: Clone + Debug + Eq + Codec<Cfg = CodecConfig> + Send + Sync + 'static;
298        /// Data which can assist in checking shards.
299        type CheckingData: Clone + Eq + Send + Sync;
300        /// A shard that has been checked for inclusion in the commitment.
301        ///
302        /// This allows excluding [`PhasedScheme::WeakShard`]s which are invalid, and shouldn't
303        /// be considered as progress towards meeting the minimum number of shards.
304        type CheckedShard: Clone + Send + Sync;
305        /// The type of errors that can occur during encoding, weakening, checking, and decoding.
306        type Error: std::fmt::Debug + Send;
307
308        /// Encode a piece of data, returning a commitment, along with shards, and proofs.
309        ///
310        /// Each shard and proof is intended for exactly one participant. The number of shards returned
311        /// should equal `config.minimum_shards + config.extra_shards`.
312        ///
313        /// `namespace` is a caller-provided byte string used for domain separation. All parties
314        /// participating in the same session must use the same `namespace` when calling `encode`
315        /// and `weaken`. Using `b""` produces the default behavior with no caller-specific context.
316        #[allow(clippy::type_complexity)]
317        fn encode(
318            namespace: &[u8],
319            config: &Config,
320            data: impl Buf,
321            strategy: &impl Strategy,
322        ) -> Result<(Self::Commitment, Vec<Self::StrongShard>), Self::Error>;
323
324        /// Take your own shard, check it, and produce a [`PhasedScheme::WeakShard`] to forward to others.
325        ///
326        /// This takes in an index, which is the index you expect the shard to be.
327        ///
328        /// This will produce a [`PhasedScheme::CheckedShard`] which counts towards the minimum
329        /// number of shards you need to reconstruct the data, in [`PhasedScheme::decode`].
330        ///
331        /// You also get [`PhasedScheme::CheckingData`], which has information you can use to check
332        /// the shards you receive from others.
333        ///
334        /// `namespace` must match the one used in the corresponding `encode` call.
335        #[allow(clippy::type_complexity)]
336        fn weaken(
337            namespace: &[u8],
338            config: &Config,
339            commitment: &Self::Commitment,
340            index: u16,
341            shard: Self::StrongShard,
342        ) -> Result<(Self::CheckingData, Self::CheckedShard, Self::WeakShard), Self::Error>;
343
344        /// Check the integrity of a weak shard, producing a checked shard.
345        ///
346        /// This requires the [`PhasedScheme::CheckingData`] produced by [`PhasedScheme::weaken`].
347        ///
348        /// This takes in an index, to make sure that the weak shard you're checking
349        /// is associated with the participant you expect it to be.
350        fn check(
351            config: &Config,
352            commitment: &Self::Commitment,
353            checking_data: &Self::CheckingData,
354            index: u16,
355            weak_shard: Self::WeakShard,
356        ) -> Result<Self::CheckedShard, Self::Error>;
357
358        /// Decode the data from shards received from other participants.
359        ///
360        /// The data must be decodeable with as few as `config.minimum_shards`,
361        /// including your own shard.
362        ///
363        /// Calls to this function with the same commitment, but with different shards,
364        /// or shards in a different should also result in the same output data, or in failure.
365        /// In other words, when using the decoding function in a broader system, you
366        /// get a guarantee that every participant decoding will see the same final
367        /// data, even if they receive different shards, or receive them in a different order.
368        ///
369        /// ## Commitment Binding
370        ///
371        /// Implementations must reject shards that were checked against a different
372        /// commitment than the one passed to `decode`. Mixing checked shards from
373        /// separate `encode` calls (and thus different commitments) must return an
374        /// error.
375        fn decode<'a>(
376            config: &Config,
377            commitment: &Self::Commitment,
378            checking_data: Self::CheckingData,
379            shards: impl Iterator<Item = &'a Self::CheckedShard>,
380            strategy: &impl Strategy,
381        ) -> Result<Vec<u8>, Self::Error>;
382    }
383
384    /// An adapter that exposes a [`PhasedScheme`] through the [`Scheme`] trait.
385    ///
386    /// In most cases, this is not the most optimal way to use a [`PhasedScheme`],
387    /// or to expose a [`PhasedScheme`] as a [`Scheme`] for that matter. However,
388    /// it can be useful for testing or for usecases where the phased scheme
389    /// cannot be used directly.
390    #[derive(Clone, Copy, Debug, Default)]
391    pub struct PhasedAsScheme<P>(core::marker::PhantomData<P>);
392
393    /// A checked shard produced by adapting a phased scheme into [`Scheme`].
394    #[derive(Clone)]
395    pub struct PhasedCheckedShard<P: PhasedScheme> {
396        checking_data: P::CheckingData,
397        checked_shard: P::CheckedShard,
398    }
399
400    /// Errors returned by the [`PhasedAsScheme`] adapter.
401    #[derive(Debug, Error)]
402    pub enum PhasedAsSchemeError<E> {
403        #[error(transparent)]
404        Scheme(E),
405        #[error("checked shards do not agree on checking data")]
406        InconsistentCheckingData,
407        #[error("insufficient shards {0} < {1}")]
408        InsufficientShards(usize, usize),
409    }
410
411    impl<P: PhasedScheme> Debug for PhasedCheckedShard<P> {
412        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
413            f.debug_struct("PhasedCheckedShard").finish_non_exhaustive()
414        }
415    }
416
417    impl<P: PhasedScheme> Scheme for PhasedAsScheme<P> {
418        type Commitment = P::Commitment;
419        type Shard = P::StrongShard;
420        type CheckedShard = PhasedCheckedShard<P>;
421        type Error = PhasedAsSchemeError<P::Error>;
422
423        fn encode(
424            config: &Config,
425            data: impl Buf,
426            strategy: &impl Strategy,
427        ) -> Result<(Self::Commitment, Vec<Self::Shard>), Self::Error> {
428            P::encode(b"", config, data, strategy).map_err(PhasedAsSchemeError::Scheme)
429        }
430
431        fn check(
432            config: &Config,
433            commitment: &Self::Commitment,
434            index: u16,
435            shard: &Self::Shard,
436        ) -> Result<Self::CheckedShard, Self::Error> {
437            let (checking_data, checked_shard, _) =
438                P::weaken(b"", config, commitment, index, shard.clone())
439                    .map_err(PhasedAsSchemeError::Scheme)?;
440            Ok(PhasedCheckedShard {
441                checking_data,
442                checked_shard,
443            })
444        }
445
446        fn decode<'a>(
447            config: &Config,
448            commitment: &Self::Commitment,
449            shards: impl Iterator<Item = &'a Self::CheckedShard>,
450            strategy: &impl Strategy,
451        ) -> Result<Vec<u8>, Self::Error> {
452            let mut shards = shards.peekable();
453            let Some(first) = shards.peek() else {
454                return Err(PhasedAsSchemeError::InsufficientShards(
455                    0,
456                    usize::from(config.minimum_shards.get()),
457                ));
458            };
459            let checking_data = first.checking_data.clone();
460            P::decode(
461                config,
462                commitment,
463                checking_data.clone(),
464                shards
465                    .map(|shard| {
466                        if shard.checking_data != checking_data {
467                            return Err(PhasedAsSchemeError::InconsistentCheckingData);
468                        }
469                        Ok(&shard.checked_shard)
470                    })
471                    .collect::<Result<Vec<_>, _>>()?
472                    .into_iter(),
473                strategy,
474            )
475            .map_err(PhasedAsSchemeError::Scheme)
476        }
477    }
478
479    /// A marker trait indicating that [`Scheme::check`] or [`PhasedScheme::check`] proves validity of the encoding.
480    ///
481    /// In more detail, this means that upon a successful call to [`Scheme::check`],
482    /// guarantees that the shard results from a valid encoding of the data, and thus,
483    /// if other participants also call check, then the data is guaranteed to be reconstructable.
484    pub trait ValidatingScheme {}
485});
486
487#[cfg(test)]
488mod test {
489    use super::*;
490    use arbitrary::Unstructured;
491    use commonware_cryptography::Sha256;
492    use commonware_invariants::minifuzz;
493    use commonware_macros::test_group;
494    use commonware_utils::NZU16;
495
496    const MAX_SHARD_SIZE: usize = 1 << 31;
497    const MAX_SHARDS: u16 = 32;
498    const MAX_DATA: usize = 1024;
499    const MIN_EXTRA_SHARDS: u16 = 1;
500
501    fn generate_case(u: &mut Unstructured<'_>) -> arbitrary::Result<(Config, Vec<u8>, Vec<u16>)> {
502        let minimum_shards = (u.arbitrary::<u16>()? % MAX_SHARDS) + 1;
503        let extra_shards =
504            MIN_EXTRA_SHARDS + (u.arbitrary::<u16>()? % (MAX_SHARDS - MIN_EXTRA_SHARDS + 1));
505        let total_shards = minimum_shards + extra_shards;
506
507        let data_len = usize::from(u.arbitrary::<u16>()?) % (MAX_DATA + 1);
508        let data = u.bytes(data_len)?.to_vec();
509
510        let selected_len = usize::from(minimum_shards)
511            + (usize::from(u.arbitrary::<u16>()?) % (usize::from(extra_shards) + 1));
512        let mut selected: Vec<u16> = (0..total_shards).collect();
513        for i in 0..selected_len {
514            let remaining = usize::from(total_shards) - i;
515            let j = i + (usize::from(u.arbitrary::<u16>()?) % remaining);
516            selected.swap(i, j);
517        }
518        selected.truncate(selected_len);
519
520        Ok((
521            Config {
522                minimum_shards: NZU16!(minimum_shards),
523                extra_shards: NZU16!(extra_shards),
524            },
525            data,
526            selected,
527        ))
528    }
529
530    mod scheme {
531        use super::*;
532        use crate::{reed_solomon::ReedSolomon, PhasedAsScheme, Scheme, Zoda};
533        use commonware_codec::Encode;
534        use commonware_parallel::Sequential;
535
536        fn roundtrip<S: Scheme>(config: &Config, data: &[u8], selected: &[u16]) {
537            let (commitment, shards) = S::encode(config, data, &Sequential).unwrap();
538            let read_cfg = CodecConfig {
539                maximum_shard_size: MAX_SHARD_SIZE,
540            };
541            for shard in &shards {
542                let decoded_shard = S::Shard::read_cfg(&mut shard.encode(), &read_cfg).unwrap();
543                assert_eq!(decoded_shard, *shard);
544            }
545
546            let mut checked_shards = Vec::new();
547            for (i, shard) in shards.into_iter().enumerate() {
548                if !selected.contains(&(i as u16)) {
549                    continue;
550                }
551                let checked = S::check(config, &commitment, i as u16, &shard).unwrap();
552                checked_shards.push(checked);
553            }
554
555            checked_shards.reverse();
556            let decoded =
557                S::decode(config, &commitment, checked_shards.iter(), &Sequential).unwrap();
558            assert_eq!(decoded, data);
559        }
560
561        fn decode_rejects_mixed_commitments<S: Scheme>(
562            config: &Config,
563            data_a: &[u8],
564            data_b: &[u8],
565        ) {
566            let (commitment_a, shards_a) = S::encode(config, data_a, &Sequential).unwrap();
567            let (commitment_b, shards_b) = S::encode(config, data_b, &Sequential).unwrap();
568
569            let checked_a = S::check(config, &commitment_a, 0, &shards_a[0]).unwrap();
570            let checked_b = S::check(config, &commitment_b, 1, &shards_b[1]).unwrap();
571
572            let result = S::decode(
573                config,
574                &commitment_a,
575                [checked_a, checked_b].iter(),
576                &Sequential,
577            );
578            assert!(
579                result.is_err(),
580                "decode must reject shards checked against different commitments"
581            );
582        }
583
584        fn decode_rejects_empty_checked_shards<S: Scheme>(config: &Config, data: &[u8]) {
585            let (commitment, _) = S::encode(config, data, &Sequential).unwrap();
586            let result = S::decode(config, &commitment, core::iter::empty(), &Sequential);
587            assert!(
588                result.is_err(),
589                "decode must reject empty checked shard sets"
590            );
591        }
592
593        #[test]
594        fn decode_rejects_mixed_commitment_shards() {
595            let config = Config {
596                minimum_shards: NZU16!(2),
597                extra_shards: NZU16!(1),
598            };
599
600            decode_rejects_mixed_commitments::<ReedSolomon<Sha256>>(
601                &config,
602                b"alpha payload",
603                b"bravo payload",
604            );
605            decode_rejects_mixed_commitments::<PhasedAsScheme<Zoda<Sha256>>>(
606                &config,
607                b"alpha payload",
608                b"bravo payload",
609            );
610            decode_rejects_empty_checked_shards::<ReedSolomon<Sha256>>(&config, b"alpha payload");
611            decode_rejects_empty_checked_shards::<PhasedAsScheme<Zoda<Sha256>>>(
612                &config,
613                b"alpha payload",
614            );
615        }
616
617        #[test]
618        fn roundtrip_empty_data() {
619            let config = Config {
620                minimum_shards: NZU16!(30),
621                extra_shards: NZU16!(70),
622            };
623            let selected: Vec<u16> = (0..30).collect();
624
625            roundtrip::<ReedSolomon<Sha256>>(&config, b"", &selected);
626            roundtrip::<PhasedAsScheme<Zoda<Sha256>>>(&config, b"", &selected);
627        }
628
629        #[test]
630        fn roundtrip_2_pow_16_25_total_shards() {
631            let config = Config {
632                minimum_shards: NZU16!(8),
633                extra_shards: NZU16!(17),
634            };
635            let data = vec![0x67; 1 << 16];
636            let selected: Vec<u16> = (0..8).collect();
637
638            roundtrip::<ReedSolomon<Sha256>>(&config, &data, &selected);
639            roundtrip::<PhasedAsScheme<Zoda<Sha256>>>(&config, &data, &selected);
640        }
641
642        #[test]
643        fn minifuzz_roundtrip_reed_solomon() {
644            minifuzz::test(|u| {
645                let (config, data, selected) = generate_case(u)?;
646                roundtrip::<ReedSolomon<Sha256>>(&config, &data, &selected);
647                Ok(())
648            });
649        }
650
651        #[test_group("slow")]
652        #[test]
653        fn minifuzz_roundtrip_zoda() {
654            minifuzz::Builder::default()
655                .with_search_limit(64)
656                .test(|u| {
657                    let (config, data, selected) = generate_case(u)?;
658                    roundtrip::<PhasedAsScheme<Zoda<Sha256>>>(&config, &data, &selected);
659                    Ok(())
660                });
661        }
662    }
663
664    mod phased_scheme {
665        use super::*;
666        use crate::{PhasedScheme, Zoda};
667        use commonware_codec::Encode;
668        use commonware_parallel::Sequential;
669
670        fn roundtrip<S: PhasedScheme>(config: &Config, data: &[u8], selected: &[u16]) {
671            let owner = *selected.first().expect("selected must not be empty");
672            let (commitment, shards) = S::encode(b"", config, data, &Sequential).unwrap();
673            let read_cfg = CodecConfig {
674                maximum_shard_size: MAX_SHARD_SIZE,
675            };
676            for shard in &shards {
677                let decoded_shard =
678                    S::StrongShard::read_cfg(&mut shard.encode(), &read_cfg).unwrap();
679                assert_eq!(decoded_shard, *shard);
680            }
681
682            let (checking_data, own_checked, _) = S::weaken(
683                b"",
684                config,
685                &commitment,
686                owner,
687                shards[owner as usize].clone(),
688            )
689            .unwrap();
690            let mut checked_shards = vec![own_checked];
691            for &index in selected {
692                if index == owner {
693                    continue;
694                }
695                let (_, _, weak_shard) = S::weaken(
696                    b"",
697                    config,
698                    &commitment,
699                    index,
700                    shards[index as usize].clone(),
701                )
702                .unwrap();
703                let decoded_weak =
704                    S::WeakShard::read_cfg(&mut weak_shard.encode(), &read_cfg).unwrap();
705                assert_eq!(decoded_weak, weak_shard);
706                let checked =
707                    S::check(config, &commitment, &checking_data, index, decoded_weak).unwrap();
708                checked_shards.push(checked);
709            }
710
711            checked_shards.reverse();
712            let decoded = S::decode(
713                config,
714                &commitment,
715                checking_data,
716                checked_shards.iter(),
717                &Sequential,
718            )
719            .unwrap();
720            assert_eq!(decoded, data);
721        }
722
723        fn check_rejects_mixed_commitments<S: PhasedScheme>(
724            config: &Config,
725            data_a: &[u8],
726            data_b: &[u8],
727        ) {
728            let (commitment_a, shards_a) = S::encode(b"", config, data_a, &Sequential).unwrap();
729            let (commitment_b, shards_b) = S::encode(b"", config, data_b, &Sequential).unwrap();
730
731            let (checking_data_a, checked_a, _) =
732                S::weaken(b"", config, &commitment_a, 0, shards_a[0].clone()).unwrap();
733            let (checking_data_b, checked_b, weak_b) =
734                S::weaken(b"", config, &commitment_b, 1, shards_b[1].clone()).unwrap();
735
736            let check_result = S::check(config, &commitment_a, &checking_data_a, 1, weak_b);
737            assert!(
738                check_result.is_err(),
739                "check must reject weak shards derived from a different commitment"
740            );
741
742            let decode_result = S::decode(
743                config,
744                &commitment_a,
745                checking_data_a,
746                [checked_a, checked_b].iter(),
747                &Sequential,
748            );
749            assert!(
750                decode_result.is_err(),
751                "decode must reject checked shards derived from a different commitment"
752            );
753
754            let decode_result = S::decode(
755                config,
756                &commitment_b,
757                checking_data_b,
758                core::iter::empty::<&S::CheckedShard>(),
759                &Sequential,
760            );
761            assert!(
762                decode_result.is_err(),
763                "decode must reject insufficient checked shards"
764            );
765        }
766
767        #[test]
768        fn check_rejects_mixed_commitment_weak_shards() {
769            let config = Config {
770                minimum_shards: NZU16!(2),
771                extra_shards: NZU16!(1),
772            };
773
774            check_rejects_mixed_commitments::<Zoda<Sha256>>(
775                &config,
776                b"alpha payload",
777                b"bravo payload",
778            );
779        }
780
781        #[test]
782        fn roundtrip_empty_data() {
783            let config = Config {
784                minimum_shards: NZU16!(30),
785                extra_shards: NZU16!(70),
786            };
787            let selected: Vec<u16> = (0..30).collect();
788
789            roundtrip::<Zoda<Sha256>>(&config, b"", &selected);
790        }
791
792        #[test]
793        fn roundtrip_2_pow_16_25_total_shards() {
794            let config = Config {
795                minimum_shards: NZU16!(8),
796                extra_shards: NZU16!(17),
797            };
798            let data = vec![0x67; 1 << 16];
799            let selected: Vec<u16> = (0..8).collect();
800
801            roundtrip::<Zoda<Sha256>>(&config, &data, &selected);
802        }
803
804        #[test_group("slow")]
805        #[test]
806        fn minifuzz_roundtrip_zoda() {
807            minifuzz::Builder::default()
808                .with_search_limit(64)
809                .test(|u| {
810                    let (config, data, selected) = generate_case(u)?;
811                    roundtrip::<Zoda<Sha256>>(&config, &data, &selected);
812                    Ok(())
813                });
814        }
815    }
816
817    #[cfg(feature = "arbitrary")]
818    mod conformance {
819        use super::*;
820        use commonware_codec::conformance::CodecConformance;
821
822        commonware_conformance::conformance_tests! {
823            CodecConformance<Config>,
824        }
825    }
826}