Skip to main content

commonware_consensus/marshal/coding/
types.rs

1//! Types for erasure coding.
2
3use crate::{
4    types::{coding::Commitment, Height},
5    Block, CertifiableBlock, Heightable,
6};
7use commonware_codec::{EncodeSize, Read, ReadExt, Write};
8use commonware_coding::{Config as CodingConfig, Scheme};
9use commonware_cryptography::{Committable, Digestible, Hasher};
10use commonware_parallel::{Sequential, Strategy};
11use commonware_utils::{Faults, N3f1, NZU16};
12use std::{marker::PhantomData, ops::Deref};
13
14const STRONG_SHARD_TAG: u8 = 0;
15const WEAK_SHARD_TAG: u8 = 1;
16
17/// A shard of erasure coded data, either a strong shard (from the proposer) or a weak shard
18/// (from a non-proposer).
19///
20/// A weak shard cannot be checked for validity on its own.
21#[derive(Clone)]
22pub enum DistributionShard<C: Scheme> {
23    /// A shard that is broadcasted by the proposer, containing extra information for generating
24    /// checking data.
25    Strong(C::StrongShard),
26    /// A shard that is broadcasted by a non-proposer, containing only the shard data.
27    Weak(C::WeakShard),
28}
29
30impl<C: Scheme> Write for DistributionShard<C> {
31    fn write(&self, buf: &mut impl bytes::BufMut) {
32        match self {
33            Self::Strong(shard) => {
34                buf.put_u8(STRONG_SHARD_TAG);
35                shard.write(buf);
36            }
37            Self::Weak(weak_shard) => {
38                buf.put_u8(WEAK_SHARD_TAG);
39                weak_shard.write(buf);
40            }
41        }
42    }
43}
44
45impl<C: Scheme> EncodeSize for DistributionShard<C> {
46    fn encode_size(&self) -> usize {
47        1 + match self {
48            Self::Strong(shard) => shard.encode_size(),
49            Self::Weak(weak_shard) => weak_shard.encode_size(),
50        }
51    }
52}
53
54impl<C: Scheme> Read for DistributionShard<C> {
55    type Cfg = commonware_coding::CodecConfig;
56
57    fn read_cfg(
58        buf: &mut impl bytes::Buf,
59        shard_cfg: &Self::Cfg,
60    ) -> Result<Self, commonware_codec::Error> {
61        match u8::read(buf)? {
62            STRONG_SHARD_TAG => {
63                let shard = C::StrongShard::read_cfg(buf, shard_cfg)?;
64                Ok(Self::Strong(shard))
65            }
66            WEAK_SHARD_TAG => {
67                let weak_shard = C::WeakShard::read_cfg(buf, shard_cfg)?;
68                Ok(Self::Weak(weak_shard))
69            }
70            _ => Err(commonware_codec::Error::Invalid(
71                "DistributionShard",
72                "invalid tag",
73            )),
74        }
75    }
76}
77
78impl<C: Scheme> PartialEq for DistributionShard<C> {
79    fn eq(&self, other: &Self) -> bool {
80        match (self, other) {
81            (Self::Strong(a), Self::Strong(b)) => a == b,
82            (Self::Weak(a), Self::Weak(b)) => a == b,
83            _ => false,
84        }
85    }
86}
87
88impl<C: Scheme> Eq for DistributionShard<C> {}
89
90#[cfg(feature = "arbitrary")]
91impl<C: Scheme> arbitrary::Arbitrary<'_> for DistributionShard<C>
92where
93    C::StrongShard: for<'a> arbitrary::Arbitrary<'a>,
94    C::WeakShard: for<'a> arbitrary::Arbitrary<'a>,
95{
96    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
97        if u.arbitrary::<bool>()? {
98            Ok(Self::Strong(u.arbitrary()?))
99        } else {
100            Ok(Self::Weak(u.arbitrary()?))
101        }
102    }
103}
104
105/// A broadcastable shard of erasure coded data, including the coding commitment and
106/// the configuration used to code the data.
107pub struct Shard<C: Scheme, H: Hasher> {
108    /// The coding commitment
109    pub(crate) commitment: Commitment,
110    /// The index of this shard within the commitment.
111    pub(crate) index: u16,
112    /// An individual shard within the commitment.
113    pub(crate) inner: DistributionShard<C>,
114    /// Phantom data for the hasher.
115    _hasher: PhantomData<H>,
116}
117
118impl<C: Scheme, H: Hasher> Shard<C, H> {
119    pub const fn new(commitment: Commitment, index: u16, inner: DistributionShard<C>) -> Self {
120        Self {
121            commitment,
122            index,
123            inner,
124            _hasher: PhantomData,
125        }
126    }
127
128    /// Returns the index of this shard within the commitment.
129    pub const fn index(&self) -> u16 {
130        self.index
131    }
132
133    /// Returns the [`Commitment`] for this shard.
134    pub const fn commitment(&self) -> Commitment {
135        self.commitment
136    }
137
138    /// Returns true if the inner shard is strong.
139    pub const fn is_strong(&self) -> bool {
140        matches!(self.inner, DistributionShard::Strong(_))
141    }
142
143    /// Returns true if the inner shard is weak.
144    pub const fn is_weak(&self) -> bool {
145        matches!(self.inner, DistributionShard::Weak(_))
146    }
147
148    /// Takes the inner [`DistributionShard`].
149    pub fn into_inner(self) -> DistributionShard<C> {
150        self.inner
151    }
152
153    /// Verifies the shard and returns the weak shard for broadcasting if valid.
154    ///
155    /// Returns `Some(weak_shard)` if the shard is valid and can be rebroadcast,
156    /// or `None` if the shard is invalid or already weak.
157    pub fn verify_into_weak(self) -> Option<Self> {
158        let DistributionShard::Strong(shard) = self.inner else {
159            return None;
160        };
161
162        let weak_shard = C::weaken(
163            &self.commitment.config(),
164            &self.commitment.root(),
165            self.index,
166            shard,
167        )
168        .ok()
169        .map(|(_, _, weak_shard)| weak_shard)?;
170
171        Some(Self::new(
172            self.commitment,
173            self.index,
174            DistributionShard::Weak(weak_shard),
175        ))
176    }
177}
178
179impl<C: Scheme, H: Hasher> Clone for Shard<C, H> {
180    fn clone(&self) -> Self {
181        Self {
182            commitment: self.commitment,
183            index: self.index,
184            inner: self.inner.clone(),
185            _hasher: PhantomData,
186        }
187    }
188}
189
190impl<C: Scheme, H: Hasher> Deref for Shard<C, H> {
191    type Target = DistributionShard<C>;
192
193    fn deref(&self) -> &Self::Target {
194        &self.inner
195    }
196}
197
198impl<C: Scheme, H: Hasher> Committable for Shard<C, H> {
199    type Commitment = Commitment;
200
201    fn commitment(&self) -> Self::Commitment {
202        self.commitment
203    }
204}
205
206impl<C: Scheme, H: Hasher> Write for Shard<C, H> {
207    fn write(&self, buf: &mut impl bytes::BufMut) {
208        self.commitment.write(buf);
209        self.index.write(buf);
210        self.inner.write(buf);
211    }
212}
213
214impl<C: Scheme, H: Hasher> EncodeSize for Shard<C, H> {
215    fn encode_size(&self) -> usize {
216        self.commitment.encode_size() + self.index.encode_size() + self.inner.encode_size()
217    }
218}
219
220impl<C: Scheme, H: Hasher> Read for Shard<C, H> {
221    type Cfg = commonware_coding::CodecConfig;
222
223    fn read_cfg(
224        buf: &mut impl bytes::Buf,
225        cfg: &Self::Cfg,
226    ) -> Result<Self, commonware_codec::Error> {
227        let commitment = Commitment::read(buf)?;
228        let index = u16::read(buf)?;
229        let inner = DistributionShard::read_cfg(buf, cfg)?;
230
231        Ok(Self {
232            commitment,
233            index,
234            inner,
235            _hasher: PhantomData,
236        })
237    }
238}
239
240impl<C: Scheme, H: Hasher> PartialEq for Shard<C, H> {
241    fn eq(&self, other: &Self) -> bool {
242        self.commitment == other.commitment
243            && self.index == other.index
244            && self.inner == other.inner
245    }
246}
247
248impl<C: Scheme, H: Hasher> Eq for Shard<C, H> {}
249
250#[cfg(feature = "arbitrary")]
251impl<C: Scheme, H: Hasher> arbitrary::Arbitrary<'_> for Shard<C, H>
252where
253    DistributionShard<C>: for<'a> arbitrary::Arbitrary<'a>,
254{
255    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
256        Ok(Self {
257            commitment: u.arbitrary()?,
258            index: u.arbitrary()?,
259            inner: u.arbitrary()?,
260            _hasher: PhantomData,
261        })
262    }
263}
264
265/// An envelope type for an erasure coded [`Block`].
266#[derive(Debug)]
267pub struct CodedBlock<B: Block, C: Scheme, H: Hasher> {
268    /// The inner block type.
269    inner: B,
270    /// The erasure coding configuration.
271    config: CodingConfig,
272    /// The erasure coding commitment.
273    commitment: C::Commitment,
274    /// The coded shards.
275    ///
276    /// These shards are optional to enable lazy construction. If the block is
277    /// constructed with [`Self::new_trusted`], the shards are computed lazily
278    /// via [`Self::shards`].
279    shards: Option<Vec<C::StrongShard>>,
280    /// Phantom data for the hasher.
281    _hasher: PhantomData<H>,
282}
283
284impl<B: Block, C: Scheme, H: Hasher> CodedBlock<B, C, H> {
285    /// Erasure codes the block.
286    fn encode(
287        inner: &B,
288        config: CodingConfig,
289        strategy: &impl Strategy,
290    ) -> (C::Commitment, Vec<C::StrongShard>) {
291        let mut buf = Vec::with_capacity(inner.encode_size() + config.encode_size());
292        inner.write(&mut buf);
293        config.write(&mut buf);
294
295        C::encode(&config, buf.as_slice(), strategy).expect("must encode block successfully")
296    }
297
298    /// Create a new [`CodedBlock`] from a [`Block`] and a configuration.
299    pub fn new(inner: B, config: CodingConfig, strategy: &impl Strategy) -> Self {
300        let (commitment, shards) = Self::encode(&inner, config, strategy);
301        Self {
302            inner,
303            config,
304            commitment,
305            shards: Some(shards),
306            _hasher: PhantomData,
307        }
308    }
309
310    /// Create a new [`CodedBlock`] from a [`Block`] and trusted [`Commitment`].
311    pub fn new_trusted(inner: B, commitment: Commitment) -> Self {
312        Self {
313            inner,
314            config: commitment.config(),
315            commitment: commitment.root(),
316            shards: None,
317            _hasher: PhantomData,
318        }
319    }
320
321    /// Returns the coding configuration for the data committed.
322    pub const fn config(&self) -> CodingConfig {
323        self.config
324    }
325
326    /// Returns a reference to the shards in this coded block.
327    ///
328    /// If the shards have not yet been generated, they will be created via [`Scheme::encode`].
329    pub fn shards(&mut self, strategy: &impl Strategy) -> &[C::StrongShard] {
330        match self.shards {
331            Some(ref shards) => shards,
332            None => {
333                let (commitment, shards) = Self::encode(&self.inner, self.config, strategy);
334
335                assert_eq!(
336                    commitment, self.commitment,
337                    "coded block constructed with trusted commitment does not match commitment"
338                );
339
340                self.shards = Some(shards);
341                self.shards.as_ref().unwrap()
342            }
343        }
344    }
345
346    /// Returns a [`Shard`] at the given index, if the index is valid.
347    pub fn shard(&self, index: u16) -> Option<Shard<C, H>>
348    where
349        B: CertifiableBlock,
350    {
351        Some(Shard::new(
352            self.commitment(),
353            index,
354            DistributionShard::Strong(self.shards.as_ref()?.get(usize::from(index))?.clone()),
355        ))
356    }
357
358    /// Returns a reference to the inner [`Block`].
359    pub const fn inner(&self) -> &B {
360        &self.inner
361    }
362
363    /// Takes the inner [`Block`].
364    pub fn into_inner(self) -> B {
365        self.inner
366    }
367}
368
369impl<B: CertifiableBlock, C: Scheme, H: Hasher> From<CodedBlock<B, C, H>>
370    for StoredCodedBlock<B, C, H>
371{
372    fn from(block: CodedBlock<B, C, H>) -> Self {
373        Self::new(block)
374    }
375}
376
377impl<B: Block + Clone, C: Scheme, H: Hasher> Clone for CodedBlock<B, C, H> {
378    fn clone(&self) -> Self {
379        Self {
380            inner: self.inner.clone(),
381            config: self.config,
382            commitment: self.commitment,
383            shards: self.shards.clone(),
384            _hasher: PhantomData,
385        }
386    }
387}
388
389impl<B: CertifiableBlock, C: Scheme, H: Hasher> Committable for CodedBlock<B, C, H> {
390    type Commitment = Commitment;
391
392    fn commitment(&self) -> Self::Commitment {
393        Commitment::from((
394            self.digest(),
395            self.commitment,
396            hash_context::<H, _>(&self.inner.context()),
397            self.config,
398        ))
399    }
400}
401
402impl<B: Block, C: Scheme, H: Hasher> Digestible for CodedBlock<B, C, H> {
403    type Digest = B::Digest;
404
405    fn digest(&self) -> Self::Digest {
406        self.inner.digest()
407    }
408}
409
410impl<B: Block, C: Scheme, H: Hasher> Write for CodedBlock<B, C, H> {
411    fn write(&self, buf: &mut impl bytes::BufMut) {
412        self.inner.write(buf);
413        self.config.write(buf);
414    }
415}
416
417impl<B: Block, C: Scheme, H: Hasher> EncodeSize for CodedBlock<B, C, H> {
418    fn encode_size(&self) -> usize {
419        self.inner.encode_size() + self.config.encode_size()
420    }
421}
422
423impl<B: Block, C: Scheme, H: Hasher> Read for CodedBlock<B, C, H> {
424    type Cfg = <B as Read>::Cfg;
425
426    fn read_cfg(
427        buf: &mut impl bytes::Buf,
428        block_cfg: &Self::Cfg,
429    ) -> Result<Self, commonware_codec::Error> {
430        let inner = B::read_cfg(buf, block_cfg)?;
431        let config = CodingConfig::read(buf)?;
432
433        let mut buf = Vec::with_capacity(inner.encode_size() + config.encode_size());
434        inner.write(&mut buf);
435        config.write(&mut buf);
436        let (commitment, shards) =
437            C::encode(&config, buf.as_slice(), &Sequential).map_err(|_| {
438                commonware_codec::Error::Invalid("CodedBlock", "Failed to re-commit to block")
439            })?;
440
441        Ok(Self {
442            inner,
443            config,
444            commitment,
445            shards: Some(shards),
446            _hasher: PhantomData,
447        })
448    }
449}
450
451impl<B: CertifiableBlock, C: Scheme, H: Hasher> Block for CodedBlock<B, C, H> {
452    fn parent(&self) -> Self::Digest {
453        self.inner.parent()
454    }
455}
456
457impl<B: Block, C: Scheme, H: Hasher> Heightable for CodedBlock<B, C, H> {
458    fn height(&self) -> Height {
459        self.inner.height()
460    }
461}
462
463impl<B: CertifiableBlock, C: Scheme, H: Hasher> CertifiableBlock for CodedBlock<B, C, H> {
464    type Context = B::Context;
465
466    fn context(&self) -> Self::Context {
467        self.inner.context()
468    }
469}
470
471/// Hashes a consensus context for inclusion in a [`Commitment`].
472pub fn hash_context<H: Hasher, C: EncodeSize + Write>(context: &C) -> H::Digest {
473    let mut buf = Vec::with_capacity(context.encode_size());
474    context.write(&mut buf);
475    H::hash(&buf)
476}
477
478impl<B: Block + PartialEq, C: Scheme, H: Hasher> PartialEq for CodedBlock<B, C, H> {
479    fn eq(&self, other: &Self) -> bool {
480        self.inner == other.inner
481            && self.config == other.config
482            && self.commitment == other.commitment
483            && self.shards == other.shards
484    }
485}
486
487impl<B: Block + Eq, C: Scheme, H: Hasher> Eq for CodedBlock<B, C, H> {}
488
489/// A [`CodedBlock`] paired with its [`Commitment`] for efficient storage and retrieval.
490///
491/// This type should be preferred for storing verified [`CodedBlock`]s on disk - it
492/// should never be sent over the network. Use [`CodedBlock`] for network transmission,
493/// as it re-encodes the block with [`Scheme::encode`] on deserialization to ensure integrity.
494///
495/// When reading from storage, we don't need to re-encode the block to compute
496/// the commitment - we stored it alongside the block when we first verified it.
497/// This avoids expensive erasure coding operations on the read path.
498///
499/// The [`Read`] implementation performs a light verification (block digest check)
500/// to detect storage corruption, but does not re-encode the block.
501pub struct StoredCodedBlock<B: Block, C: Scheme, H: Hasher> {
502    inner: B,
503    commitment: Commitment,
504    _scheme: PhantomData<(C, H)>,
505}
506
507impl<B: CertifiableBlock, C: Scheme, H: Hasher> StoredCodedBlock<B, C, H> {
508    /// Create a [`StoredCodedBlock`] from a verified [`CodedBlock`].
509    ///
510    /// The caller must ensure the [`CodedBlock`] has been properly verified
511    /// (i.e., its commitment was computed or validated against a trusted source).
512    pub fn new(block: CodedBlock<B, C, H>) -> Self {
513        Self {
514            commitment: block.commitment(),
515            inner: block.inner,
516            _scheme: PhantomData,
517        }
518    }
519
520    /// Convert back to a [`CodedBlock`] using the trusted commitment.
521    ///
522    /// The returned [`CodedBlock`] will have `shards: None`, meaning shards
523    /// will be lazily generated if needed via [`CodedBlock::shards`].
524    pub fn into_coded_block(self) -> CodedBlock<B, C, H> {
525        CodedBlock::new_trusted(self.inner, self.commitment)
526    }
527
528    /// Returns a reference to the inner block.
529    pub const fn inner(&self) -> &B {
530        &self.inner
531    }
532}
533
534/// Converts a [`StoredCodedBlock`] back to a [`CodedBlock`].
535impl<B: Block, C: Scheme, H: Hasher> From<StoredCodedBlock<B, C, H>> for CodedBlock<B, C, H> {
536    fn from(stored: StoredCodedBlock<B, C, H>) -> Self {
537        Self::new_trusted(stored.inner, stored.commitment)
538    }
539}
540
541impl<B: Block + Clone, C: Scheme, H: Hasher> Clone for StoredCodedBlock<B, C, H> {
542    fn clone(&self) -> Self {
543        Self {
544            commitment: self.commitment,
545            inner: self.inner.clone(),
546            _scheme: PhantomData,
547        }
548    }
549}
550
551impl<B: Block, C: Scheme, H: Hasher> Committable for StoredCodedBlock<B, C, H> {
552    type Commitment = Commitment;
553
554    fn commitment(&self) -> Self::Commitment {
555        self.commitment
556    }
557}
558
559impl<B: Block, C: Scheme, H: Hasher> Digestible for StoredCodedBlock<B, C, H> {
560    type Digest = B::Digest;
561
562    fn digest(&self) -> Self::Digest {
563        self.inner.digest()
564    }
565}
566
567impl<B: Block, C: Scheme, H: Hasher> Write for StoredCodedBlock<B, C, H> {
568    fn write(&self, buf: &mut impl bytes::BufMut) {
569        self.inner.write(buf);
570        self.commitment.write(buf);
571    }
572}
573
574impl<B: Block, C: Scheme, H: Hasher> EncodeSize for StoredCodedBlock<B, C, H> {
575    fn encode_size(&self) -> usize {
576        self.inner.encode_size() + self.commitment.encode_size()
577    }
578}
579
580impl<B: Block, C: Scheme, H: Hasher> Read for StoredCodedBlock<B, C, H> {
581    // Note: No concurrency parameter needed since we don't re-encode!
582    type Cfg = B::Cfg;
583
584    fn read_cfg(
585        buf: &mut impl bytes::Buf,
586        block_cfg: &Self::Cfg,
587    ) -> Result<Self, commonware_codec::Error> {
588        let inner = B::read_cfg(buf, block_cfg)?;
589        let commitment = Commitment::read(buf)?;
590
591        // Light verification to detect storage corruption
592        if inner.digest() != commitment.block::<B::Digest>() {
593            return Err(commonware_codec::Error::Invalid(
594                "StoredCodedBlock",
595                "storage corruption: block digest mismatch",
596            ));
597        }
598
599        Ok(Self {
600            commitment,
601            inner,
602            _scheme: PhantomData,
603        })
604    }
605}
606
607impl<B: Block, C: Scheme, H: Hasher> Block for StoredCodedBlock<B, C, H> {
608    fn parent(&self) -> Self::Digest {
609        self.inner.parent()
610    }
611}
612
613impl<B: CertifiableBlock, C: Scheme, H: Hasher> CertifiableBlock for StoredCodedBlock<B, C, H> {
614    type Context = B::Context;
615
616    fn context(&self) -> Self::Context {
617        self.inner.context()
618    }
619}
620
621impl<B: Block, C: Scheme, H: Hasher> Heightable for StoredCodedBlock<B, C, H> {
622    fn height(&self) -> Height {
623        self.inner.height()
624    }
625}
626
627impl<B: Block + PartialEq, C: Scheme, H: Hasher> PartialEq for StoredCodedBlock<B, C, H> {
628    fn eq(&self, other: &Self) -> bool {
629        self.commitment == other.commitment && self.inner == other.inner
630    }
631}
632
633impl<B: Block + Eq, C: Scheme, H: Hasher> Eq for StoredCodedBlock<B, C, H> {}
634
635/// Compute the [`CodingConfig`] for a given number of participants.
636///
637/// Panics if `n_participants < 4`.
638pub fn coding_config_for_participants(n_participants: u16) -> CodingConfig {
639    let max_faults = N3f1::max_faults(n_participants);
640    assert!(
641        max_faults >= 1,
642        "Need at least 4 participants to maintain fault tolerance"
643    );
644    let max_faults = u16::try_from(max_faults).expect("max_faults must fit in u16");
645    let minimum_shards = NZU16!(max_faults + 1);
646    CodingConfig {
647        minimum_shards,
648        extra_shards: NZU16!(n_participants - minimum_shards.get()),
649    }
650}
651
652#[cfg(test)]
653mod test {
654    use super::*;
655    use crate::{marshal::mocks::block::Block as MockBlock, Block as _};
656    use commonware_codec::{Decode, Encode};
657    use commonware_coding::{CodecConfig, ReedSolomon};
658    use commonware_cryptography::{sha256::Digest as Sha256Digest, Digest, Sha256};
659
660    const MAX_SHARD_SIZE: CodecConfig = CodecConfig {
661        maximum_shard_size: 1024 * 1024, // 1 MiB
662    };
663
664    type H = Sha256;
665    type RS = ReedSolomon<H>;
666    type RShard = Shard<RS, H>;
667    type Block = MockBlock<<H as Hasher>::Digest, ()>;
668
669    #[test]
670    fn test_distribution_shard_codec_roundtrip() {
671        const MOCK_BLOCK_DATA: &[u8] = b"commonware shape rotator club";
672        const CONFIG: CodingConfig = CodingConfig {
673            minimum_shards: NZU16!(1),
674            extra_shards: NZU16!(2),
675        };
676
677        let (_, shards) = RS::encode(&CONFIG, MOCK_BLOCK_DATA, &Sequential).unwrap();
678        let raw_shard = shards.first().cloned().unwrap();
679
680        let strong_shard = DistributionShard::<RS>::Strong(raw_shard.clone());
681        let encoded = strong_shard.encode();
682        let decoded =
683            DistributionShard::<RS>::decode_cfg(&mut encoded.as_ref(), &MAX_SHARD_SIZE).unwrap();
684        assert!(strong_shard == decoded);
685
686        let weak_shard = DistributionShard::<RS>::Weak(raw_shard);
687        let encoded = weak_shard.encode();
688        let decoded =
689            DistributionShard::<RS>::decode_cfg(&mut encoded.as_ref(), &MAX_SHARD_SIZE).unwrap();
690        assert!(weak_shard == decoded);
691    }
692
693    #[test]
694    fn test_distribution_shard_decode_truncated_returns_error() {
695        let decode = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
696            let mut buf = &[][..];
697            DistributionShard::<RS>::decode_cfg(&mut buf, &MAX_SHARD_SIZE)
698        }));
699        assert!(decode.is_ok(), "decode must not panic on truncated input");
700        assert!(decode.unwrap().is_err());
701    }
702
703    #[test]
704    fn test_coding_config_for_participants_valid_for_minimum_set() {
705        let config = coding_config_for_participants(4);
706        assert_eq!(config.minimum_shards.get(), 2);
707        assert_eq!(config.extra_shards.get(), 2);
708    }
709
710    #[test]
711    #[should_panic(expected = "Need at least 4 participants to maintain fault tolerance")]
712    fn test_coding_config_for_participants_panics_for_small_sets() {
713        let _ = coding_config_for_participants(3);
714    }
715
716    #[test]
717    fn test_shard_codec_roundtrip() {
718        const MOCK_BLOCK_DATA: &[u8] = b"deadc0de";
719        const CONFIG: CodingConfig = CodingConfig {
720            minimum_shards: NZU16!(1),
721            extra_shards: NZU16!(2),
722        };
723
724        let (commitment, shards) = RS::encode(&CONFIG, MOCK_BLOCK_DATA, &Sequential).unwrap();
725        let raw_shard = shards.first().cloned().unwrap();
726
727        let commitment =
728            Commitment::from((Sha256Digest::EMPTY, commitment, Sha256Digest::EMPTY, CONFIG));
729        let shard = RShard::new(commitment, 0, DistributionShard::Strong(raw_shard.clone()));
730        let encoded = shard.encode();
731        let decoded = RShard::decode_cfg(&mut encoded.as_ref(), &MAX_SHARD_SIZE).unwrap();
732        assert!(shard == decoded);
733
734        let shard = RShard::new(commitment, 0, DistributionShard::Weak(raw_shard));
735        let encoded = shard.encode();
736        let decoded = RShard::decode_cfg(&mut encoded.as_ref(), &MAX_SHARD_SIZE).unwrap();
737        assert!(shard == decoded);
738    }
739
740    #[test]
741    fn test_coded_block_codec_roundtrip() {
742        const CONFIG: CodingConfig = CodingConfig {
743            minimum_shards: NZU16!(1),
744            extra_shards: NZU16!(2),
745        };
746
747        let block = Block::new::<Sha256>((), Sha256::hash(b"parent"), Height::new(42), 1_234_567);
748        let coded_block = CodedBlock::<Block, RS, H>::new(block, CONFIG, &Sequential);
749
750        let encoded = coded_block.encode();
751        let decoded = CodedBlock::<Block, RS, H>::decode_cfg(encoded, &()).unwrap();
752
753        assert!(coded_block == decoded);
754    }
755
756    #[test]
757    fn test_stored_coded_block_codec_roundtrip() {
758        const CONFIG: CodingConfig = CodingConfig {
759            minimum_shards: NZU16!(1),
760            extra_shards: NZU16!(2),
761        };
762
763        let block = Block::new::<Sha256>((), Sha256::hash(b"parent"), Height::new(42), 1_234_567);
764        let coded_block = CodedBlock::<Block, RS, H>::new(block, CONFIG, &Sequential);
765        let stored = StoredCodedBlock::<Block, RS, H>::new(coded_block.clone());
766
767        assert_eq!(stored.commitment(), coded_block.commitment());
768        assert_eq!(stored.digest(), coded_block.digest());
769        assert_eq!(stored.height(), coded_block.height());
770        assert_eq!(stored.parent(), coded_block.parent());
771
772        let encoded = stored.encode();
773        let decoded = StoredCodedBlock::<Block, RS, H>::decode_cfg(encoded, &()).unwrap();
774
775        assert!(stored == decoded);
776        assert_eq!(decoded.commitment(), coded_block.commitment());
777        assert_eq!(decoded.digest(), coded_block.digest());
778    }
779
780    #[test]
781    fn test_stored_coded_block_into_coded_block() {
782        const CONFIG: CodingConfig = CodingConfig {
783            minimum_shards: NZU16!(1),
784            extra_shards: NZU16!(2),
785        };
786
787        let block = Block::new::<Sha256>((), Sha256::hash(b"parent"), Height::new(42), 1_234_567);
788        let coded_block = CodedBlock::<Block, RS, H>::new(block, CONFIG, &Sequential);
789        let original_commitment = coded_block.commitment();
790        let original_digest = coded_block.digest();
791
792        let stored = StoredCodedBlock::<Block, RS, H>::new(coded_block);
793        let encoded = stored.encode();
794        let decoded = StoredCodedBlock::<Block, RS, H>::decode_cfg(encoded, &()).unwrap();
795        let restored = decoded.into_coded_block();
796
797        assert_eq!(restored.commitment(), original_commitment);
798        assert_eq!(restored.digest(), original_digest);
799    }
800
801    #[test]
802    fn test_stored_coded_block_corruption_detection() {
803        const CONFIG: CodingConfig = CodingConfig {
804            minimum_shards: NZU16!(1),
805            extra_shards: NZU16!(2),
806        };
807
808        let block = Block::new::<Sha256>((), Sha256::hash(b"parent"), Height::new(42), 1_234_567);
809        let coded_block = CodedBlock::<Block, RS, H>::new(block, CONFIG, &Sequential);
810        let stored = StoredCodedBlock::<Block, RS, H>::new(coded_block);
811
812        let mut encoded = stored.encode().to_vec();
813
814        // Corrupt the commitment (located after the block bytes)
815        let block_size = stored.inner().encode_size();
816        encoded[block_size] ^= 0xFF;
817
818        // Decoding should fail due to digest mismatch
819        let result = StoredCodedBlock::<Block, RS, H>::decode_cfg(&mut encoded.as_slice(), &());
820        assert!(result.is_err());
821    }
822
823    #[cfg(feature = "arbitrary")]
824    mod conformance {
825        use super::*;
826        use commonware_codec::conformance::CodecConformance;
827
828        commonware_conformance::conformance_tests! {
829            CodecConformance<DistributionShard<ReedSolomon<Sha256>>>,
830            CodecConformance<Shard<ReedSolomon<Sha256>, Sha256>>,
831        }
832    }
833}