Skip to main content

commonware_consensus/marshal/coding/
types.rs

1//! Types for erasure coding.
2
3use crate::{
4    Block, CertifiableBlock, Heightable,
5    marshal::core::ExpectedCommitment,
6    types::{Height, coding::Commitment},
7};
8use commonware_codec::{BufsMut, EncodeSize, FixedSize, Read, ReadExt, Write};
9use commonware_coding::{Config as CodingConfig, Scheme};
10use commonware_cryptography::{Committable, Digested, Digestible, Hasher};
11use commonware_parallel::Strategy;
12use commonware_utils::{Faults, N3f1, NZU16};
13use std::{
14    future::Future,
15    sync::{Arc, OnceLock},
16};
17
18/// A broadcastable shard of erasure coded data, including the coding commitment and
19/// the configuration used to code the data.
20pub struct Shard<B: Digestible, C: Scheme, H: Hasher> {
21    /// The coding commitment
22    pub(crate) commitment: Commitment<B, C, H>,
23    /// The index of this shard within the commitment.
24    pub(crate) index: u16,
25    /// An individual shard within the commitment.
26    pub(crate) inner: C::Shard,
27}
28
29impl<B: Digestible, C: Scheme, H: Hasher> Shard<B, C, H> {
30    /// Returns the largest encoded size of a shard whose inner shard encodes to at most `shard`
31    /// bytes.
32    pub const fn max_encode_size(shard: usize) -> usize {
33        shard + Commitment::<B, C, H>::SIZE + u16::SIZE
34    }
35
36    pub const fn new(commitment: Commitment<B, C, H>, index: u16, inner: C::Shard) -> Self {
37        Self {
38            commitment,
39            index,
40            inner,
41        }
42    }
43
44    /// Returns the index of this shard within the commitment.
45    pub const fn index(&self) -> u16 {
46        self.index
47    }
48
49    /// Returns the [`Commitment`] for this shard.
50    pub const fn commitment(&self) -> Commitment<B, C, H> {
51        self.commitment
52    }
53
54    /// Takes the inner shard.
55    pub fn into_inner(self) -> C::Shard {
56        self.inner
57    }
58}
59
60impl<B: Digestible, C: Scheme, H: Hasher> Clone for Shard<B, C, H> {
61    fn clone(&self) -> Self {
62        Self {
63            commitment: self.commitment,
64            index: self.index,
65            inner: self.inner.clone(),
66        }
67    }
68}
69
70impl<B: Digestible, C: Scheme, H: Hasher> Committable for Shard<B, C, H> {
71    type Commitment = Commitment<B, C, H>;
72
73    fn commitment(&self) -> Self::Commitment {
74        self.commitment
75    }
76}
77
78impl<B: Digestible, C: Scheme, H: Hasher> Write for Shard<B, C, H> {
79    fn write(&self, buf: &mut impl bytes::BufMut) {
80        self.commitment.write(buf);
81        self.index.write(buf);
82        self.inner.write(buf);
83    }
84
85    fn write_bufs(&self, buf: &mut impl BufsMut) {
86        self.commitment.write(buf);
87        self.index.write(buf);
88        self.inner.write_bufs(buf);
89    }
90}
91
92impl<B: Digestible, C: Scheme, H: Hasher> EncodeSize for Shard<B, C, H> {
93    fn encode_size(&self) -> usize {
94        self.commitment.encode_size() + self.index.encode_size() + self.inner.encode_size()
95    }
96
97    fn encode_inline_size(&self) -> usize {
98        self.commitment.encode_size() + self.index.encode_size() + self.inner.encode_inline_size()
99    }
100}
101
102impl<B: Digestible, C: Scheme, H: Hasher> Read for Shard<B, C, H> {
103    /// An upper bound on the encoded size of an application block.
104    type Cfg = usize;
105
106    fn read_cfg(
107        buf: &mut impl commonware_codec::Buf,
108        max_block_size: &Self::Cfg,
109    ) -> Result<Self, commonware_codec::Error> {
110        let commitment = Commitment::<B, C, H>::read(buf)?;
111        let index = u16::read(buf)?;
112
113        // A coded payload is the block followed by its coding config. The claimed config is
114        // untrusted until consensus confirms the commitment, so the data width it admits may be
115        // the scheme's widest for a payload at this bound.
116        let maximum = max_block_size.saturating_add(CodingConfig::SIZE);
117        let inner = C::Shard::read_cfg(buf, &(commitment.config(), maximum))?;
118
119        Ok(Self {
120            commitment,
121            index,
122            inner,
123        })
124    }
125}
126
127impl<B: Digestible, C: Scheme, H: Hasher> PartialEq for Shard<B, C, H> {
128    fn eq(&self, other: &Self) -> bool {
129        self.commitment == other.commitment
130            && self.index == other.index
131            && self.inner == other.inner
132    }
133}
134
135impl<B: Digestible, C: Scheme, H: Hasher> Eq for Shard<B, C, H> {}
136
137#[cfg(feature = "arbitrary")]
138impl<B: Digestible, C: Scheme, H: Hasher> arbitrary::Arbitrary<'_> for Shard<B, C, H>
139where
140    Commitment<B, C, H>: for<'a> arbitrary::Arbitrary<'a>,
141    C::Shard: for<'a> arbitrary::Arbitrary<'a>,
142{
143    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
144        Ok(Self {
145            commitment: u.arbitrary()?,
146            index: u.arbitrary()?,
147            inner: u.arbitrary()?,
148        })
149    }
150}
151
152/// An envelope type for an erasure coded [`Block`].
153///
154/// The consensus commitment is fixed when the block is built, decoded, or restored, so reading
155/// the commitment or the inner block's digest never hashes or erasure codes.
156#[derive(Debug)]
157pub struct CodedBlock<B: Block, C: Scheme, H: Hasher> {
158    /// The inner block type.
159    inner: Arc<B>,
160    /// The consensus commitment, binding the inner block's digest, the coding root, the digest
161    /// of the consensus context, and the coding configuration.
162    commitment: Commitment<B, C, H>,
163    /// The coded shards.
164    ///
165    /// These shards are lazily-constructed when [`CodedBlock`] is formed without erasure coding,
166    /// by [`Self::precomputed`], a trusted decode, or restoration from storage.
167    shards: OnceLock<Arc<[C::Shard]>>,
168}
169
170impl<B: Block, C: Scheme, H: Hasher> CodedBlock<B, C, H> {
171    /// Returns the largest encoded size of a coded block whose application block encodes to at
172    /// most `application` bytes.
173    pub const fn max_encode_size(application: usize) -> usize {
174        application + CodingConfig::SIZE
175    }
176
177    /// Erasure codes the block.
178    #[allow(clippy::type_complexity)]
179    fn encode_shards(
180        inner: &B,
181        config: CodingConfig,
182        strategy: &impl Strategy,
183    ) -> Result<(C::Commitment, Vec<C::Shard>), C::Error> {
184        let mut buf = Vec::with_capacity(inner.encode_size() + config.encode_size());
185        inner.write(&mut buf);
186        config.write(&mut buf);
187
188        C::encode(&config, buf.as_slice(), strategy)
189    }
190
191    /// Creates a [`CodedBlock`] from a [`Block`] and its precomputed [`Commitment`].
192    ///
193    /// `commitment` must encode `inner`. [`Self::shards`] generates shards on
194    /// demand and panics if the coding root does not match.
195    ///
196    /// Panics if `commitment` does not commit to the block's digest.
197    pub fn precomputed(inner: Digested<B>, commitment: Commitment<B, C, H>) -> Self {
198        assert_eq!(
199            inner.digest(),
200            commitment.block(),
201            "commitment does not commit to block"
202        );
203        Self {
204            inner: inner.into(),
205            commitment,
206            shards: OnceLock::new(),
207        }
208    }
209
210    /// Returns the coding configuration for the data committed.
211    pub fn config(&self) -> CodingConfig {
212        self.commitment.config()
213    }
214
215    /// Returns a reference to the shards in this coded block.
216    ///
217    /// If the shards have not yet been generated, they will be created via [`Scheme::encode`].
218    pub fn shards(&self, strategy: &impl Strategy) -> &[C::Shard] {
219        self.shards.get_or_init(|| {
220            let (root, shards) = Self::encode_shards(&self.inner, self.config(), strategy)
221                .expect("must encode block successfully");
222
223            // A mismatch means a commitment trusted at construction does not
224            // encode this block, which is a contract violation or a consensus
225            // safety failure. Never serve shards under a different root.
226            assert_eq!(
227                root,
228                self.commitment.root(),
229                "coded block constructed with trusted commitment does not match commitment"
230            );
231
232            shards.into()
233        })
234    }
235
236    /// Returns a [`Shard`] at the given index, if the index is valid.
237    pub fn shard(&self, index: u16) -> Option<Shard<B, C, H>> {
238        Some(Shard::new(
239            self.commitment,
240            index,
241            self.shards.get()?.get(usize::from(index))?.clone(),
242        ))
243    }
244
245    /// Returns a reference to the inner [`Block`].
246    pub fn inner(&self) -> &B {
247        &self.inner
248    }
249
250    /// Returns a shared reference to the inner [`Block`].
251    pub fn inner_shared(&self) -> Arc<B> {
252        Arc::clone(&self.inner)
253    }
254
255    /// Returns the inner [`Block`] with its digest, which the commitment binds.
256    pub fn inner_digested(&self) -> Digested<B> {
257        Digested::precomputed(Arc::clone(&self.inner), self.commitment.block())
258    }
259}
260
261impl<B: CertifiableBlock, C: Scheme, H: Hasher> CodedBlock<B, C, H> {
262    /// Builds the consensus commitment of `inner` from its coding `root` and `config`.
263    fn commit(
264        inner: &Digested<B>,
265        root: C::Commitment,
266        config: CodingConfig,
267    ) -> Commitment<B, C, H> {
268        Commitment::from((
269            inner.digest(),
270            root,
271            hash_context::<H, _>(&inner.context()),
272            config,
273        ))
274    }
275
276    /// Erasure codes `inner` with `config` as a job on `strategy`, producing a [`CodedBlock`] that
277    /// holds its shards.
278    #[track_caller]
279    pub fn encode(
280        inner: Digested<B>,
281        config: CodingConfig,
282        strategy: &impl Strategy,
283    ) -> impl Future<Output = Self> + Send + 'static {
284        strategy.spawn(inner.encode_size(), move |strategy| {
285            let (root, shards) = Self::encode_shards(inner.inner(), config, &strategy)
286                .expect("must encode block successfully");
287            Self {
288                commitment: Self::commit(&inner, root, config),
289                inner: inner.into(),
290                shards: OnceLock::from(Arc::<[C::Shard]>::from(shards)),
291            }
292        })
293    }
294
295    /// Builds a [`CodedBlock`] from a decoded block and coding configuration, failing unless the
296    /// result's commitment equals the one in `expected`.
297    ///
298    /// The block's digest and, for [`ExpectedCommitment::Untrusted`], its coding root are computed
299    /// as jobs on `strategy`.
300    pub(crate) fn from_decoded(
301        inner: B,
302        config: CodingConfig,
303        expected: ExpectedCommitment<Commitment<B, C, H>>,
304        strategy: &impl Strategy,
305    ) -> impl Future<Output = Result<Self, commonware_codec::Error>> + Send {
306        let (ExpectedCommitment::Trusted(commitment) | ExpectedCommitment::Untrusted(commitment)) =
307            expected;
308        let strategy = strategy.clone();
309        async move {
310            // Erasure coding is bounded by the expected config and skipped for a block that
311            // cannot match, so both are checked before it.
312            if config != commitment.config() {
313                return Err(commonware_codec::Error::Invalid(
314                    "CodedBlock",
315                    "config mismatch",
316                ));
317            }
318            let inner = Digested::compute(inner, &strategy).await;
319            if inner.digest() != commitment.block() {
320                return Err(commonware_codec::Error::Invalid(
321                    "CodedBlock",
322                    "block digest mismatch",
323                ));
324            }
325
326            // A certified commitment already fixes the coding root of these bytes,
327            // so recomputing it would only re-derive the root already in `expected`.
328            let (root, shards) = match expected {
329                ExpectedCommitment::Trusted(_) => (commitment.root(), None),
330                ExpectedCommitment::Untrusted(_) => {
331                    let encoding = inner.clone();
332                    let (root, shards) = strategy
333                        .spawn(inner.encode_size(), move |strategy| {
334                            Self::encode_shards(encoding.inner(), config, &strategy)
335                        })
336                        .await
337                        .map_err(|_| {
338                            commonware_codec::Error::Invalid(
339                                "CodedBlock",
340                                "Failed to re-commit to block",
341                            )
342                        })?;
343                    (root, Some(shards))
344                }
345            };
346
347            // Binds the components not checked above, which are the root and the context digest.
348            let computed = Self::commit(&inner, root, config);
349            if computed != commitment {
350                return Err(commonware_codec::Error::Invalid(
351                    "CodedBlock",
352                    "commitment mismatch",
353                ));
354            }
355            Ok(Self {
356                inner: inner.into(),
357                commitment: computed,
358                shards: shards.map_or_else(OnceLock::new, |shards| {
359                    OnceLock::from(Arc::<[C::Shard]>::from(shards))
360                }),
361            })
362        }
363    }
364}
365
366impl<B: Block, C: Scheme, H: Hasher> From<CodedBlock<B, C, H>> for StoredCodedBlock<B, C, H> {
367    fn from(block: CodedBlock<B, C, H>) -> Self {
368        Self::new(block)
369    }
370}
371
372/// Shares the inner block of a [`CodedBlock`] for archival.
373impl<B: Block, C: Scheme, H: Hasher> From<Arc<CodedBlock<B, C, H>>> for StoredCodedBlock<B, C, H> {
374    fn from(block: Arc<CodedBlock<B, C, H>>) -> Self {
375        Self {
376            commitment: block.commitment,
377            inner: block.inner_shared(),
378        }
379    }
380}
381
382impl<B: Block, C: Scheme, H: Hasher> Clone for CodedBlock<B, C, H> {
383    fn clone(&self) -> Self {
384        Self {
385            inner: Arc::clone(&self.inner),
386            commitment: self.commitment,
387            shards: self.shards.clone(),
388        }
389    }
390}
391
392impl<B: Block, C: Scheme, H: Hasher> Committable for CodedBlock<B, C, H> {
393    type Commitment = Commitment<B, C, H>;
394
395    fn commitment(&self) -> Self::Commitment {
396        self.commitment
397    }
398}
399
400impl<B: Block, C: Scheme, H: Hasher> Digestible for CodedBlock<B, C, H> {
401    type Digest = B::Digest;
402
403    fn digest(&self) -> Self::Digest {
404        self.commitment.block()
405    }
406}
407
408impl<B: Block, C: Scheme, H: Hasher> Write for CodedBlock<B, C, H> {
409    fn write(&self, buf: &mut impl bytes::BufMut) {
410        self.inner.write(buf);
411        self.config().write(buf);
412    }
413}
414
415impl<B: Block, C: Scheme, H: Hasher> EncodeSize for CodedBlock<B, C, H> {
416    fn encode_size(&self) -> usize {
417        self.inner.encode_size() + self.config().encode_size()
418    }
419}
420
421impl<B: Block, C: Scheme, H: Hasher> Block for CodedBlock<B, C, H> {
422    fn parent(&self) -> Self::Digest {
423        self.inner.parent()
424    }
425}
426
427impl<B: Block, C: Scheme, H: Hasher> Heightable for CodedBlock<B, C, H> {
428    fn height(&self) -> Height {
429        self.inner.height()
430    }
431}
432
433impl<B: CertifiableBlock, C: Scheme, H: Hasher> CertifiableBlock for CodedBlock<B, C, H> {
434    type Context = B::Context;
435
436    fn context(&self) -> Self::Context {
437        self.inner.context()
438    }
439}
440
441/// Hashes a consensus context for inclusion in a [`Commitment`].
442pub fn hash_context<H: Hasher, C: EncodeSize + Write>(context: &C) -> H::Digest {
443    let mut buf = Vec::with_capacity(context.encode_size());
444    context.write(&mut buf);
445    H::hash(&[&buf])
446}
447
448impl<B: Block + PartialEq, C: Scheme, H: Hasher> PartialEq for CodedBlock<B, C, H> {
449    fn eq(&self, other: &Self) -> bool {
450        self.inner == other.inner
451            && self.commitment == other.commitment
452            && self.shards == other.shards
453    }
454}
455
456impl<B: Block + Eq, C: Scheme, H: Hasher> Eq for CodedBlock<B, C, H> {}
457
458/// A [`CodedBlock`] paired with its [`Commitment`] for efficient storage and retrieval.
459///
460/// This type should be preferred for storing verified [`CodedBlock`]s on disk, and it
461/// should never be sent over the network. Use [`CodedBlock`] for network transmission.
462///
463/// When reading from storage, we don't need to re-encode or hash the block to compute
464/// the commitment, because we stored it alongside the block when we first verified it.
465/// This avoids expensive erasure coding operations on the read path.
466pub struct StoredCodedBlock<B: Block, C: Scheme, H: Hasher> {
467    inner: Arc<B>,
468    commitment: Commitment<B, C, H>,
469}
470
471impl<B: Block, C: Scheme, H: Hasher> StoredCodedBlock<B, C, H> {
472    /// Create a [`StoredCodedBlock`] from a verified [`CodedBlock`].
473    ///
474    /// The caller must ensure the [`CodedBlock`] has been properly verified
475    /// (i.e., its commitment was computed or validated against a trusted source).
476    pub fn new(block: CodedBlock<B, C, H>) -> Self {
477        Self {
478            commitment: block.commitment,
479            inner: block.inner,
480        }
481    }
482
483    /// Convert back to a [`CodedBlock`] using the trusted commitment.
484    ///
485    /// The returned [`CodedBlock`] generates shards lazily if they are needed.
486    pub fn into_coded_block(self) -> CodedBlock<B, C, H> {
487        CodedBlock {
488            inner: self.inner,
489            commitment: self.commitment,
490            shards: OnceLock::new(),
491        }
492    }
493
494    /// Returns a reference to the inner block.
495    pub fn inner(&self) -> &B {
496        &self.inner
497    }
498}
499
500/// Converts a [`StoredCodedBlock`] back to a [`CodedBlock`].
501impl<B: Block, C: Scheme, H: Hasher> From<StoredCodedBlock<B, C, H>> for CodedBlock<B, C, H> {
502    fn from(stored: StoredCodedBlock<B, C, H>) -> Self {
503        stored.into_coded_block()
504    }
505}
506
507/// Restores a shared [`CodedBlock`] from its stored form.
508impl<B: Block, C: Scheme, H: Hasher> From<StoredCodedBlock<B, C, H>> for Arc<CodedBlock<B, C, H>> {
509    fn from(stored: StoredCodedBlock<B, C, H>) -> Self {
510        Self::new(stored.into())
511    }
512}
513
514impl<B: Block, C: Scheme, H: Hasher> Clone for StoredCodedBlock<B, C, H> {
515    fn clone(&self) -> Self {
516        Self {
517            commitment: self.commitment,
518            inner: Arc::clone(&self.inner),
519        }
520    }
521}
522
523impl<B: Block, C: Scheme, H: Hasher> Committable for StoredCodedBlock<B, C, H> {
524    type Commitment = Commitment<B, C, H>;
525
526    fn commitment(&self) -> Self::Commitment {
527        self.commitment
528    }
529}
530
531impl<B: Block, C: Scheme, H: Hasher> Digestible for StoredCodedBlock<B, C, H> {
532    type Digest = B::Digest;
533
534    fn digest(&self) -> Self::Digest {
535        self.commitment.block()
536    }
537}
538
539impl<B: Block, C: Scheme, H: Hasher> Write for StoredCodedBlock<B, C, H> {
540    fn write(&self, buf: &mut impl bytes::BufMut) {
541        self.inner.write(buf);
542        self.commitment.write(buf);
543    }
544}
545
546impl<B: Block, C: Scheme, H: Hasher> EncodeSize for StoredCodedBlock<B, C, H> {
547    fn encode_size(&self) -> usize {
548        self.inner.encode_size() + self.commitment.encode_size()
549    }
550}
551
552impl<B: Block + Read, C: Scheme, H: Hasher> Read for StoredCodedBlock<B, C, H> {
553    type Cfg = B::Cfg;
554
555    fn read_cfg(
556        buf: &mut impl commonware_codec::Buf,
557        block_cfg: &Self::Cfg,
558    ) -> Result<Self, commonware_codec::Error> {
559        let inner = B::read_cfg(buf, block_cfg)?;
560        let commitment = Commitment::<B, C, H>::read(buf)?;
561        Ok(Self {
562            commitment,
563            inner: Arc::new(inner),
564        })
565    }
566}
567
568impl<B: Block, C: Scheme, H: Hasher> Block for StoredCodedBlock<B, C, H> {
569    fn parent(&self) -> Self::Digest {
570        self.inner.parent()
571    }
572}
573
574impl<B: CertifiableBlock, C: Scheme, H: Hasher> CertifiableBlock for StoredCodedBlock<B, C, H> {
575    type Context = B::Context;
576
577    fn context(&self) -> Self::Context {
578        self.inner.context()
579    }
580}
581
582impl<B: Block, C: Scheme, H: Hasher> Heightable for StoredCodedBlock<B, C, H> {
583    fn height(&self) -> Height {
584        self.inner.height()
585    }
586}
587
588impl<B: Block + PartialEq, C: Scheme, H: Hasher> PartialEq for StoredCodedBlock<B, C, H> {
589    fn eq(&self, other: &Self) -> bool {
590        self.commitment == other.commitment && self.inner == other.inner
591    }
592}
593
594impl<B: Block + Eq, C: Scheme, H: Hasher> Eq for StoredCodedBlock<B, C, H> {}
595
596/// Compute the [`CodingConfig`] for a given number of participants.
597///
598/// Panics if `n_participants < 4`.
599pub fn coding_config_for_participants(n_participants: u16) -> CodingConfig {
600    let max_faults = N3f1::max_faults(n_participants);
601    assert!(
602        max_faults >= 1,
603        "Need at least 4 participants to maintain fault tolerance"
604    );
605    let max_faults = u16::try_from(max_faults).expect("max_faults must fit in u16");
606    let minimum_shards = NZU16!(max_faults + 1);
607    CodingConfig {
608        minimum_shards,
609        extra_shards: NZU16!(n_participants - minimum_shards.get()),
610    }
611}
612
613#[cfg(test)]
614mod test {
615    use super::*;
616    use crate::marshal::mocks::block::{EmptyBlock, encode_block};
617    use bytes::Buf as _;
618    use commonware_codec::{Decode, Encode, Error, Input};
619    use commonware_coding::ReedSolomon;
620    use commonware_cryptography::{Digest, Sha256, sha256::Digest as Sha256Digest};
621    use commonware_parallel::Sequential;
622    use commonware_runtime::iobuf::EncodeExt;
623    use futures::executor::block_on;
624
625    const MAX_BLOCK_SIZE: usize = 1024 * 1024;
626
627    type H = Sha256;
628    type RS = ReedSolomon<H>;
629    type TestBlock = EmptyBlock<H>;
630    type RShard = Shard<TestBlock, RS, H>;
631
632    /// Decodes a coded block's wire bytes against `expected` the way the coding variant does.
633    fn decode(
634        encoded: impl Input,
635        expected: ExpectedCommitment<Commitment<TestBlock, RS, H>>,
636    ) -> Result<CodedBlock<TestBlock, RS, H>, Error> {
637        let (inner, config) = <(TestBlock, CodingConfig)>::decode_cfg(encoded, &((), ()))?;
638        block_on(CodedBlock::from_decoded(
639            inner,
640            config,
641            expected,
642            &Sequential,
643        ))
644    }
645
646    /// A shard decodes only when it is no wider than a block of the maximum size produces under
647    /// the coding config its commitment claims.
648    #[test]
649    fn test_shard_width_bounded_by_commitment_config() {
650        const MAX: usize = 1000;
651        const CONFIG: CodingConfig = CodingConfig {
652            minimum_shards: NZU16!(2),
653            extra_shards: NZU16!(2),
654        };
655        let encode = |len: usize, claimed: CodingConfig| {
656            let (root, shards) =
657                RS::encode(&CONFIG, vec![0u8; len].as_slice(), &Sequential).unwrap();
658            let commitment =
659                Commitment::from((Sha256Digest::EMPTY, root, Sha256Digest::EMPTY, claimed));
660            RShard::new(commitment, 0, shards[0].clone()).encode()
661        };
662
663        // A maximum block and its coding config produce the widest admitted shard.
664        let widest = MAX + CodingConfig::SIZE;
665        assert!(RShard::decode_cfg(encode(widest, CONFIG), &MAX).is_ok());
666
667        // One more byte widens the shard past the bound.
668        assert!(RShard::decode_cfg(encode(widest + 1, CONFIG), &MAX).is_err());
669
670        // A commitment claiming more minimum shards narrows the bound.
671        let claimed = CodingConfig {
672            minimum_shards: NZU16!(4),
673            extra_shards: NZU16!(2),
674        };
675        assert!(RShard::decode_cfg(encode(widest, claimed), &MAX).is_err());
676    }
677
678    #[test]
679    fn test_shard_wrapper_codec_roundtrip() {
680        const MOCK_BLOCK_DATA: &[u8] = b"commonware shape rotator club";
681        const CONFIG: CodingConfig = CodingConfig {
682            minimum_shards: NZU16!(1),
683            extra_shards: NZU16!(2),
684        };
685
686        let (commitment, shards) = RS::encode(&CONFIG, MOCK_BLOCK_DATA, &Sequential).unwrap();
687        let raw_shard = shards.first().cloned().unwrap();
688
689        let commitment =
690            Commitment::from((Sha256Digest::EMPTY, commitment, Sha256Digest::EMPTY, CONFIG));
691        let shard = RShard::new(commitment, 0, raw_shard);
692        let encoded = shard.encode();
693        let decoded = RShard::decode_cfg(encoded, &MAX_BLOCK_SIZE).unwrap();
694        assert!(shard == decoded);
695    }
696
697    #[test]
698    fn test_shard_decode_truncated_returns_error() {
699        let decode = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
700            let buf = commonware_codec::Copying(&[]);
701            RShard::decode_cfg(buf, &MAX_BLOCK_SIZE)
702        }));
703        assert!(decode.is_ok(), "decode must not panic on truncated input");
704        assert!(decode.unwrap().is_err());
705    }
706
707    #[test]
708    fn test_coding_config_for_participants_valid_for_minimum_set() {
709        let config = coding_config_for_participants(4);
710        assert_eq!(config.minimum_shards.get(), 2);
711        assert_eq!(config.extra_shards.get(), 2);
712    }
713
714    #[test]
715    #[should_panic(expected = "Need at least 4 participants to maintain fault tolerance")]
716    fn test_coding_config_for_participants_panics_for_small_sets() {
717        let _ = coding_config_for_participants(3);
718    }
719
720    #[test]
721    fn test_shard_codec_roundtrip() {
722        const MOCK_BLOCK_DATA: &[u8] = b"deadc0de";
723        const CONFIG: CodingConfig = CodingConfig {
724            minimum_shards: NZU16!(1),
725            extra_shards: NZU16!(2),
726        };
727
728        let (commitment, shards) = RS::encode(&CONFIG, MOCK_BLOCK_DATA, &Sequential).unwrap();
729        let raw_shard = shards.first().cloned().unwrap();
730
731        let commitment =
732            Commitment::from((Sha256Digest::EMPTY, commitment, Sha256Digest::EMPTY, CONFIG));
733        let shard = RShard::new(commitment, 0, raw_shard);
734        let encoded = shard.encode();
735        let decoded = RShard::decode_cfg(encoded, &MAX_BLOCK_SIZE).unwrap();
736        assert!(shard == decoded);
737    }
738
739    #[test]
740    fn test_coded_block_codec_roundtrip() {
741        const CONFIG: CodingConfig = CodingConfig {
742            minimum_shards: NZU16!(1),
743            extra_shards: NZU16!(2),
744        };
745
746        let block = TestBlock::new(Sha256::hash(&[b"parent"]), Height::new(42), 1_234_567);
747        let coded_block = encode_block::<TestBlock, RS, H>(block, CONFIG);
748
749        let encoded = coded_block.encode();
750        let decoded = decode(
751            encoded,
752            ExpectedCommitment::Trusted(coded_block.commitment()),
753        )
754        .unwrap();
755        assert_eq!(decoded.commitment(), coded_block.commitment());
756        assert_eq!(decoded.shards(&Sequential), coded_block.shards(&Sequential));
757        assert!(coded_block == decoded);
758
759        // Binding the same bytes to an untrusted commitment recomputes the same block.
760        let decoded = decode(
761            coded_block.encode(),
762            ExpectedCommitment::Untrusted(coded_block.commitment()),
763        )
764        .unwrap();
765        assert!(coded_block == decoded);
766    }
767
768    #[test]
769    fn test_coded_block_decode_rejects_config_mismatch() {
770        const EXPECTED_CONFIG: CodingConfig = CodingConfig {
771            minimum_shards: NZU16!(1),
772            extra_shards: NZU16!(3),
773        };
774        const EMBEDDED_CONFIG: CodingConfig = CodingConfig {
775            minimum_shards: NZU16!(2),
776            extra_shards: NZU16!(2),
777        };
778
779        let block = TestBlock::new(Sha256::hash(&[b"parent"]), Height::new(42), 1_234_567);
780        let expected =
781            encode_block::<TestBlock, RS, H>(block.clone(), EXPECTED_CONFIG).commitment();
782        let encoded = (block, EMBEDDED_CONFIG).encode();
783
784        let Err(err) = decode(encoded, ExpectedCommitment::Untrusted(expected)) else {
785            panic!("config mismatch should be rejected");
786        };
787
788        assert!(
789            matches!(err, Error::Invalid("CodedBlock", "config mismatch")),
790            "unexpected error: {err:?}"
791        );
792    }
793
794    #[test]
795    fn test_coded_block_decode_rejects_coding_root_mismatch() {
796        const CONFIG: CodingConfig = CodingConfig {
797            minimum_shards: NZU16!(1),
798            extra_shards: NZU16!(2),
799        };
800
801        // Build an expected commitment that differs only in its coding root.
802        let block = TestBlock::new(Sha256::hash(&[b"parent"]), Height::new(42), 1_234_567);
803        let coded = encode_block::<TestBlock, RS, H>(block, CONFIG);
804        let commitment = coded.commitment();
805        let wrong_root = Sha256::hash(&[b"wrong root"]);
806        assert_ne!(wrong_root, commitment.root());
807        let expected = Commitment::<TestBlock, RS, H>::from((
808            commitment.block(),
809            wrong_root,
810            commitment.context(),
811            commitment.config(),
812        ));
813
814        // Decoding must reject bytes that do not satisfy the exact expected
815        // commitment.
816        let Err(err) = decode(coded.encode(), ExpectedCommitment::Untrusted(expected)) else {
817            panic!("coding root mismatch should be rejected");
818        };
819
820        assert!(
821            matches!(err, Error::Invalid("CodedBlock", "commitment mismatch")),
822            "unexpected error: {err:?}"
823        );
824    }
825
826    #[test]
827    fn test_coded_block_decode_trusted_is_lazy() {
828        const CONFIG: CodingConfig = CodingConfig {
829            minimum_shards: NZU16!(1),
830            extra_shards: NZU16!(2),
831        };
832
833        let block = TestBlock::new(Sha256::hash(&[b"parent"]), Height::new(42), 1_234_567);
834        let coded = encode_block::<TestBlock, RS, H>(block, CONFIG);
835        let decoded = decode(
836            coded.encode(),
837            ExpectedCommitment::Trusted(coded.commitment()),
838        )
839        .unwrap();
840
841        // The root comes from the commitment and shards are generated on demand.
842        assert_eq!(decoded.commitment(), coded.commitment());
843        assert!(decoded.shard(0).is_none());
844        assert_eq!(decoded.shards(&Sequential), coded.shards(&Sequential));
845    }
846
847    #[test]
848    fn test_coded_block_decode_trusted_rejects_config_mismatch() {
849        const EXPECTED_CONFIG: CodingConfig = CodingConfig {
850            minimum_shards: NZU16!(1),
851            extra_shards: NZU16!(3),
852        };
853        const EMBEDDED_CONFIG: CodingConfig = CodingConfig {
854            minimum_shards: NZU16!(2),
855            extra_shards: NZU16!(2),
856        };
857
858        let block = TestBlock::new(Sha256::hash(&[b"parent"]), Height::new(42), 1_234_567);
859        let expected =
860            encode_block::<TestBlock, RS, H>(block.clone(), EXPECTED_CONFIG).commitment();
861        let encoded = (block, EMBEDDED_CONFIG).encode();
862
863        let Err(err) = decode(encoded, ExpectedCommitment::Trusted(expected)) else {
864            panic!("config mismatch should be rejected");
865        };
866        assert!(
867            matches!(err, Error::Invalid("CodedBlock", "config mismatch")),
868            "unexpected error: {err:?}"
869        );
870    }
871
872    #[test]
873    fn test_coded_block_decode_trusted_rejects_block_digest_mismatch() {
874        const CONFIG: CodingConfig = CodingConfig {
875            minimum_shards: NZU16!(1),
876            extra_shards: NZU16!(2),
877        };
878
879        let block = TestBlock::new(Sha256::hash(&[b"parent"]), Height::new(42), 1_234_567);
880        let other = TestBlock::new(Sha256::hash(&[b"parent"]), Height::new(43), 1_234_567);
881        let expected = encode_block::<TestBlock, RS, H>(block, CONFIG).commitment();
882        assert_ne!(other.digest(), expected.block());
883        let encoded = (other, CONFIG).encode();
884
885        let Err(err) = decode(encoded, ExpectedCommitment::Trusted(expected)) else {
886            panic!("block digest mismatch should be rejected");
887        };
888
889        assert!(
890            matches!(err, Error::Invalid("CodedBlock", "block digest mismatch")),
891            "unexpected error: {err:?}"
892        );
893    }
894
895    #[test]
896    #[should_panic(expected = "does not match commitment")]
897    fn test_coded_block_trusted_wrong_root_panics_on_shards() {
898        const CONFIG: CodingConfig = CodingConfig {
899            minimum_shards: NZU16!(1),
900            extra_shards: NZU16!(2),
901        };
902
903        // A trusted commitment is not re-encoded at decode, so a root that does
904        // not encode the block is only caught when shards are generated.
905        let block = TestBlock::new(Sha256::hash(&[b"parent"]), Height::new(42), 1_234_567);
906        let coded = encode_block::<TestBlock, RS, H>(block, CONFIG);
907        let commitment = coded.commitment();
908        let expected = Commitment::<TestBlock, RS, H>::from((
909            commitment.block(),
910            Sha256::hash(&[b"wrong root"]),
911            commitment.context(),
912            commitment.config(),
913        ));
914        let decoded = decode(coded.encode(), ExpectedCommitment::Trusted(expected)).unwrap();
915
916        decoded.shards(&Sequential);
917    }
918
919    #[test]
920    fn test_coded_block_clone_shares_shards() {
921        const CONFIG: CodingConfig = CodingConfig {
922            minimum_shards: NZU16!(1),
923            extra_shards: NZU16!(2),
924        };
925
926        let block = TestBlock::new(Sha256::hash(&[b"parent"]), Height::new(42), 1_234_567);
927        let coded_block = encode_block::<TestBlock, RS, H>(block, CONFIG);
928        let cloned = coded_block.clone();
929
930        assert!(Arc::ptr_eq(&coded_block.inner, &cloned.inner));
931        assert!(Arc::ptr_eq(
932            coded_block.shards.get().unwrap(),
933            cloned.shards.get().unwrap()
934        ));
935    }
936
937    #[test]
938    fn test_stored_coded_block_codec_roundtrip() {
939        const CONFIG: CodingConfig = CodingConfig {
940            minimum_shards: NZU16!(1),
941            extra_shards: NZU16!(2),
942        };
943
944        let block = TestBlock::new(Sha256::hash(&[b"parent"]), Height::new(42), 1_234_567);
945        let coded_block = encode_block::<TestBlock, RS, H>(block, CONFIG);
946        let stored = StoredCodedBlock::<TestBlock, RS, H>::new(coded_block.clone());
947
948        assert_eq!(stored.commitment(), coded_block.commitment());
949        assert_eq!(stored.digest(), coded_block.digest());
950        assert_eq!(stored.height(), coded_block.height());
951        assert_eq!(stored.parent(), coded_block.parent());
952
953        let encoded = stored.encode();
954        let decoded = StoredCodedBlock::<TestBlock, RS, H>::decode_cfg(encoded, &()).unwrap();
955
956        assert!(stored == decoded);
957        assert_eq!(decoded.commitment(), coded_block.commitment());
958        assert_eq!(decoded.digest(), coded_block.digest());
959    }
960
961    #[test]
962    fn test_stored_coded_block_into_coded_block() {
963        const CONFIG: CodingConfig = CodingConfig {
964            minimum_shards: NZU16!(1),
965            extra_shards: NZU16!(2),
966        };
967
968        let block = TestBlock::new(Sha256::hash(&[b"parent"]), Height::new(42), 1_234_567);
969        let coded_block = encode_block::<TestBlock, RS, H>(block, CONFIG);
970        let original_commitment = coded_block.commitment();
971        let original_digest = coded_block.digest();
972
973        let stored = StoredCodedBlock::<TestBlock, RS, H>::new(coded_block);
974        let encoded = stored.encode();
975        let decoded = StoredCodedBlock::<TestBlock, RS, H>::decode_cfg(encoded, &()).unwrap();
976        let restored = decoded.into_coded_block();
977
978        assert_eq!(restored.commitment(), original_commitment);
979        assert_eq!(restored.digest(), original_digest);
980    }
981
982    #[test]
983    fn test_shard_encode_iobufs_matches_encode() {
984        const CONFIG: CodingConfig = CodingConfig {
985            minimum_shards: NZU16!(1),
986            extra_shards: NZU16!(2),
987        };
988
989        let (commitment, shards) =
990            RS::encode(&CONFIG, b"iobufs encoding test".as_slice(), &Sequential).unwrap();
991        let commitment =
992            Commitment::from((Sha256Digest::EMPTY, commitment, Sha256Digest::EMPTY, CONFIG));
993        let shard = RShard::new(commitment, 0, shards.into_iter().next().unwrap());
994
995        let encoded = shard.encode();
996        let mut encoded_iobufs = shard.encode_iobufs();
997        let mut encoded_iobufs_bytes = vec![0u8; encoded_iobufs.remaining()];
998        encoded_iobufs.copy_to_slice(&mut encoded_iobufs_bytes);
999        assert_eq!(encoded_iobufs_bytes, encoded.as_ref());
1000    }
1001
1002    #[cfg(feature = "arbitrary")]
1003    mod conformance {
1004        use super::*;
1005        use commonware_codec::conformance::CodecConformance;
1006
1007        commonware_conformance::conformance_tests! {
1008            CodecConformance<Shard<TestBlock, ReedSolomon<Sha256>, Sha256>>,
1009        }
1010    }
1011}