Skip to main content

commonware_consensus/marshal/coding/
types.rs

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