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}