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