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