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;
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: 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<Vec<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,
179            config,
180            commitment,
181            shards: Some(shards),
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,
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);
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 const fn inner(&self) -> &B {
236        &self.inner
237    }
238
239    /// Takes the inner [`Block`].
240    pub fn into_inner(self) -> B {
241        self.inner
242    }
243}
244
245impl<B: CertifiableBlock, 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 + Clone, C: Scheme, H: Hasher> Clone for CodedBlock<B, C, H> {
254    fn clone(&self) -> Self {
255        Self {
256            inner: self.inner.clone(),
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
299impl<B: Block, C: Scheme, H: Hasher> Read for CodedBlock<B, C, H> {
300    type Cfg = <B as Read>::Cfg;
301
302    fn read_cfg(
303        buf: &mut impl bytes::Buf,
304        block_cfg: &Self::Cfg,
305    ) -> Result<Self, commonware_codec::Error> {
306        let inner = B::read_cfg(buf, block_cfg)?;
307        let config = CodingConfig::read(buf)?;
308
309        let mut buf = Vec::with_capacity(inner.encode_size() + config.encode_size());
310        inner.write(&mut buf);
311        config.write(&mut buf);
312        let (commitment, shards) =
313            C::encode(&config, buf.as_slice(), &Sequential).map_err(|_| {
314                commonware_codec::Error::Invalid("CodedBlock", "Failed to re-commit to block")
315            })?;
316
317        Ok(Self {
318            inner,
319            config,
320            commitment,
321            shards: Some(shards),
322            _hasher: PhantomData,
323        })
324    }
325}
326
327impl<B: CertifiableBlock, C: Scheme, H: Hasher> Block for CodedBlock<B, C, H> {
328    fn parent(&self) -> Self::Digest {
329        self.inner.parent()
330    }
331}
332
333impl<B: Block, C: Scheme, H: Hasher> Heightable for CodedBlock<B, C, H> {
334    fn height(&self) -> Height {
335        self.inner.height()
336    }
337}
338
339impl<B: CertifiableBlock, C: Scheme, H: Hasher> CertifiableBlock for CodedBlock<B, C, H> {
340    type Context = B::Context;
341
342    fn context(&self) -> Self::Context {
343        self.inner.context()
344    }
345}
346
347/// Hashes a consensus context for inclusion in a [`Commitment`].
348pub fn hash_context<H: Hasher, C: EncodeSize + Write>(context: &C) -> H::Digest {
349    let mut buf = Vec::with_capacity(context.encode_size());
350    context.write(&mut buf);
351    H::hash(&buf)
352}
353
354impl<B: Block + PartialEq, C: Scheme, H: Hasher> PartialEq for CodedBlock<B, C, H> {
355    fn eq(&self, other: &Self) -> bool {
356        self.inner == other.inner
357            && self.config == other.config
358            && self.commitment == other.commitment
359            && self.shards == other.shards
360    }
361}
362
363impl<B: Block + Eq, C: Scheme, H: Hasher> Eq for CodedBlock<B, C, H> {}
364
365/// A [`CodedBlock`] paired with its [`Commitment`] for efficient storage and retrieval.
366///
367/// This type should be preferred for storing verified [`CodedBlock`]s on disk - it
368/// should never be sent over the network. Use [`CodedBlock`] for network transmission,
369/// as it re-encodes the block with [`Scheme::encode`] on deserialization to ensure integrity.
370///
371/// When reading from storage, we don't need to re-encode the block to compute
372/// the commitment - we stored it alongside the block when we first verified it.
373/// This avoids expensive erasure coding operations on the read path.
374///
375/// The [`Read`] implementation performs a light verification (block digest check)
376/// to detect storage corruption, but does not re-encode the block.
377pub struct StoredCodedBlock<B: Block, C: Scheme, H: Hasher> {
378    inner: B,
379    commitment: Commitment,
380    _scheme: PhantomData<(C, H)>,
381}
382
383impl<B: CertifiableBlock, C: Scheme, H: Hasher> StoredCodedBlock<B, C, H> {
384    /// Create a [`StoredCodedBlock`] from a verified [`CodedBlock`].
385    ///
386    /// The caller must ensure the [`CodedBlock`] has been properly verified
387    /// (i.e., its commitment was computed or validated against a trusted source).
388    pub fn new(block: CodedBlock<B, C, H>) -> Self {
389        Self {
390            commitment: block.commitment(),
391            inner: block.inner,
392            _scheme: PhantomData,
393        }
394    }
395
396    /// Convert back to a [`CodedBlock`] using the trusted commitment.
397    ///
398    /// The returned [`CodedBlock`] will have `shards: None`, meaning shards
399    /// will be lazily generated if needed via [`CodedBlock::shards`].
400    pub fn into_coded_block(self) -> CodedBlock<B, C, H> {
401        CodedBlock::new_trusted(self.inner, self.commitment)
402    }
403
404    /// Returns a reference to the inner block.
405    pub const fn inner(&self) -> &B {
406        &self.inner
407    }
408}
409
410/// Converts a [`StoredCodedBlock`] back to a [`CodedBlock`].
411impl<B: Block, C: Scheme, H: Hasher> From<StoredCodedBlock<B, C, H>> for CodedBlock<B, C, H> {
412    fn from(stored: StoredCodedBlock<B, C, H>) -> Self {
413        Self::new_trusted(stored.inner, stored.commitment)
414    }
415}
416
417impl<B: Block + Clone, C: Scheme, H: Hasher> Clone for StoredCodedBlock<B, C, H> {
418    fn clone(&self) -> Self {
419        Self {
420            commitment: self.commitment,
421            inner: self.inner.clone(),
422            _scheme: PhantomData,
423        }
424    }
425}
426
427impl<B: Block, C: Scheme, H: Hasher> Committable for StoredCodedBlock<B, C, H> {
428    type Commitment = Commitment;
429
430    fn commitment(&self) -> Self::Commitment {
431        self.commitment
432    }
433}
434
435impl<B: Block, C: Scheme, H: Hasher> Digestible for StoredCodedBlock<B, C, H> {
436    type Digest = B::Digest;
437
438    fn digest(&self) -> Self::Digest {
439        self.inner.digest()
440    }
441}
442
443impl<B: Block, C: Scheme, H: Hasher> Write for StoredCodedBlock<B, C, H> {
444    fn write(&self, buf: &mut impl bytes::BufMut) {
445        self.inner.write(buf);
446        self.commitment.write(buf);
447    }
448}
449
450impl<B: Block, C: Scheme, H: Hasher> EncodeSize for StoredCodedBlock<B, C, H> {
451    fn encode_size(&self) -> usize {
452        self.inner.encode_size() + self.commitment.encode_size()
453    }
454}
455
456impl<B: Block, C: Scheme, H: Hasher> Read for StoredCodedBlock<B, C, H> {
457    // Note: No concurrency parameter needed since we don't re-encode!
458    type Cfg = B::Cfg;
459
460    fn read_cfg(
461        buf: &mut impl bytes::Buf,
462        block_cfg: &Self::Cfg,
463    ) -> Result<Self, commonware_codec::Error> {
464        let inner = B::read_cfg(buf, block_cfg)?;
465        let commitment = Commitment::read(buf)?;
466
467        // Light verification to detect storage corruption
468        if inner.digest() != commitment.block::<B::Digest>() {
469            return Err(commonware_codec::Error::Invalid(
470                "StoredCodedBlock",
471                "storage corruption: block digest mismatch",
472            ));
473        }
474
475        Ok(Self {
476            commitment,
477            inner,
478            _scheme: PhantomData,
479        })
480    }
481}
482
483impl<B: Block, C: Scheme, H: Hasher> Block for StoredCodedBlock<B, C, H> {
484    fn parent(&self) -> Self::Digest {
485        self.inner.parent()
486    }
487}
488
489impl<B: CertifiableBlock, C: Scheme, H: Hasher> CertifiableBlock for StoredCodedBlock<B, C, H> {
490    type Context = B::Context;
491
492    fn context(&self) -> Self::Context {
493        self.inner.context()
494    }
495}
496
497impl<B: Block, C: Scheme, H: Hasher> Heightable for StoredCodedBlock<B, C, H> {
498    fn height(&self) -> Height {
499        self.inner.height()
500    }
501}
502
503impl<B: Block + PartialEq, C: Scheme, H: Hasher> PartialEq for StoredCodedBlock<B, C, H> {
504    fn eq(&self, other: &Self) -> bool {
505        self.commitment == other.commitment && self.inner == other.inner
506    }
507}
508
509impl<B: Block + Eq, C: Scheme, H: Hasher> Eq for StoredCodedBlock<B, C, H> {}
510
511/// Compute the [`CodingConfig`] for a given number of participants.
512///
513/// Panics if `n_participants < 4`.
514pub fn coding_config_for_participants(n_participants: u16) -> CodingConfig {
515    let max_faults = N3f1::max_faults(n_participants);
516    assert!(
517        max_faults >= 1,
518        "Need at least 4 participants to maintain fault tolerance"
519    );
520    let max_faults = u16::try_from(max_faults).expect("max_faults must fit in u16");
521    let minimum_shards = NZU16!(max_faults + 1);
522    CodingConfig {
523        minimum_shards,
524        extra_shards: NZU16!(n_participants - minimum_shards.get()),
525    }
526}
527
528#[cfg(test)]
529mod test {
530    use super::*;
531    use crate::{marshal::mocks::block::Block as MockBlock, Block as _};
532    use bytes::Buf;
533    use commonware_codec::{Decode, Encode};
534    use commonware_coding::{CodecConfig, ReedSolomon};
535    use commonware_cryptography::{sha256::Digest as Sha256Digest, Digest, Sha256};
536    use commonware_runtime::{deterministic, iobuf::EncodeExt, BufferPooler, Runner};
537
538    const MAX_SHARD_SIZE: CodecConfig = CodecConfig {
539        maximum_shard_size: 1024 * 1024, // 1 MiB
540    };
541
542    type H = Sha256;
543    type RS = ReedSolomon<H>;
544    type RShard = Shard<RS, H>;
545    type Block = MockBlock<<H as Hasher>::Digest, ()>;
546
547    #[test]
548    fn test_shard_wrapper_codec_roundtrip() {
549        const MOCK_BLOCK_DATA: &[u8] = b"commonware shape rotator club";
550        const CONFIG: CodingConfig = CodingConfig {
551            minimum_shards: NZU16!(1),
552            extra_shards: NZU16!(2),
553        };
554
555        let (commitment, shards) = RS::encode(&CONFIG, MOCK_BLOCK_DATA, &Sequential).unwrap();
556        let raw_shard = shards.first().cloned().unwrap();
557
558        let commitment =
559            Commitment::from((Sha256Digest::EMPTY, commitment, Sha256Digest::EMPTY, CONFIG));
560        let shard = RShard::new(commitment, 0, raw_shard);
561        let encoded = shard.encode();
562        let decoded = RShard::decode_cfg(&mut encoded.as_ref(), &MAX_SHARD_SIZE).unwrap();
563        assert!(shard == decoded);
564    }
565
566    #[test]
567    fn test_shard_decode_truncated_returns_error() {
568        let decode = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
569            let mut buf = &[][..];
570            RShard::decode_cfg(&mut buf, &MAX_SHARD_SIZE)
571        }));
572        assert!(decode.is_ok(), "decode must not panic on truncated input");
573        assert!(decode.unwrap().is_err());
574    }
575
576    #[test]
577    fn test_coding_config_for_participants_valid_for_minimum_set() {
578        let config = coding_config_for_participants(4);
579        assert_eq!(config.minimum_shards.get(), 2);
580        assert_eq!(config.extra_shards.get(), 2);
581    }
582
583    #[test]
584    #[should_panic(expected = "Need at least 4 participants to maintain fault tolerance")]
585    fn test_coding_config_for_participants_panics_for_small_sets() {
586        let _ = coding_config_for_participants(3);
587    }
588
589    #[test]
590    fn test_shard_codec_roundtrip() {
591        const MOCK_BLOCK_DATA: &[u8] = b"deadc0de";
592        const CONFIG: CodingConfig = CodingConfig {
593            minimum_shards: NZU16!(1),
594            extra_shards: NZU16!(2),
595        };
596
597        let (commitment, shards) = RS::encode(&CONFIG, MOCK_BLOCK_DATA, &Sequential).unwrap();
598        let raw_shard = shards.first().cloned().unwrap();
599
600        let commitment =
601            Commitment::from((Sha256Digest::EMPTY, commitment, Sha256Digest::EMPTY, CONFIG));
602        let shard = RShard::new(commitment, 0, raw_shard);
603        let encoded = shard.encode();
604        let decoded = RShard::decode_cfg(&mut encoded.as_ref(), &MAX_SHARD_SIZE).unwrap();
605        assert!(shard == decoded);
606    }
607
608    #[test]
609    fn test_coded_block_codec_roundtrip() {
610        const CONFIG: CodingConfig = CodingConfig {
611            minimum_shards: NZU16!(1),
612            extra_shards: NZU16!(2),
613        };
614
615        let block = Block::new::<Sha256>((), Sha256::hash(b"parent"), Height::new(42), 1_234_567);
616        let coded_block = CodedBlock::<Block, RS, H>::new(block, CONFIG, &Sequential);
617
618        let encoded = coded_block.encode();
619        let decoded = CodedBlock::<Block, RS, H>::decode_cfg(encoded, &()).unwrap();
620
621        assert!(coded_block == decoded);
622    }
623
624    #[test]
625    fn test_stored_coded_block_codec_roundtrip() {
626        const CONFIG: CodingConfig = CodingConfig {
627            minimum_shards: NZU16!(1),
628            extra_shards: NZU16!(2),
629        };
630
631        let block = Block::new::<Sha256>((), Sha256::hash(b"parent"), Height::new(42), 1_234_567);
632        let coded_block = CodedBlock::<Block, RS, H>::new(block, CONFIG, &Sequential);
633        let stored = StoredCodedBlock::<Block, RS, H>::new(coded_block.clone());
634
635        assert_eq!(stored.commitment(), coded_block.commitment());
636        assert_eq!(stored.digest(), coded_block.digest());
637        assert_eq!(stored.height(), coded_block.height());
638        assert_eq!(stored.parent(), coded_block.parent());
639
640        let encoded = stored.encode();
641        let decoded = StoredCodedBlock::<Block, RS, H>::decode_cfg(encoded, &()).unwrap();
642
643        assert!(stored == decoded);
644        assert_eq!(decoded.commitment(), coded_block.commitment());
645        assert_eq!(decoded.digest(), coded_block.digest());
646    }
647
648    #[test]
649    fn test_stored_coded_block_into_coded_block() {
650        const CONFIG: CodingConfig = CodingConfig {
651            minimum_shards: NZU16!(1),
652            extra_shards: NZU16!(2),
653        };
654
655        let block = Block::new::<Sha256>((), Sha256::hash(b"parent"), Height::new(42), 1_234_567);
656        let coded_block = CodedBlock::<Block, RS, H>::new(block, CONFIG, &Sequential);
657        let original_commitment = coded_block.commitment();
658        let original_digest = coded_block.digest();
659
660        let stored = StoredCodedBlock::<Block, RS, H>::new(coded_block);
661        let encoded = stored.encode();
662        let decoded = StoredCodedBlock::<Block, RS, H>::decode_cfg(encoded, &()).unwrap();
663        let restored = decoded.into_coded_block();
664
665        assert_eq!(restored.commitment(), original_commitment);
666        assert_eq!(restored.digest(), original_digest);
667    }
668
669    #[test]
670    fn test_stored_coded_block_corruption_detection() {
671        const CONFIG: CodingConfig = CodingConfig {
672            minimum_shards: NZU16!(1),
673            extra_shards: NZU16!(2),
674        };
675
676        let block = Block::new::<Sha256>((), Sha256::hash(b"parent"), Height::new(42), 1_234_567);
677        let coded_block = CodedBlock::<Block, RS, H>::new(block, CONFIG, &Sequential);
678        let stored = StoredCodedBlock::<Block, RS, H>::new(coded_block);
679
680        let mut encoded = stored.encode().to_vec();
681
682        // Corrupt the commitment (located after the block bytes)
683        let block_size = stored.inner().encode_size();
684        encoded[block_size] ^= 0xFF;
685
686        // Decoding should fail due to digest mismatch
687        let result = StoredCodedBlock::<Block, RS, H>::decode_cfg(&mut encoded.as_slice(), &());
688        assert!(result.is_err());
689    }
690
691    #[test]
692    fn test_shard_encode_with_pool_matches_encode() {
693        let executor = deterministic::Runner::default();
694        executor.start(|context| async move {
695            let pool = context.network_buffer_pool();
696
697            const CONFIG: CodingConfig = CodingConfig {
698                minimum_shards: NZU16!(1),
699                extra_shards: NZU16!(2),
700            };
701
702            let (commitment, shards) =
703                RS::encode(&CONFIG, b"pool encoding test".as_slice(), &Sequential).unwrap();
704            let commitment =
705                Commitment::from((Sha256Digest::EMPTY, commitment, Sha256Digest::EMPTY, CONFIG));
706            let shard = RShard::new(commitment, 0, shards.into_iter().next().unwrap());
707
708            let encoded = shard.encode();
709            let mut encoded_pool = shard.encode_with_pool(pool);
710            let mut encoded_pool_bytes = vec![0u8; encoded_pool.remaining()];
711            encoded_pool.copy_to_slice(&mut encoded_pool_bytes);
712            assert_eq!(encoded_pool_bytes, encoded.as_ref());
713        });
714    }
715
716    #[cfg(feature = "arbitrary")]
717    mod conformance {
718        use super::*;
719        use commonware_codec::conformance::CodecConformance;
720
721        commonware_conformance::conformance_tests! {
722            CodecConformance<Shard<ReedSolomon<Sha256>, Sha256>>,
723        }
724    }
725}