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}