1use 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
18pub struct Shard<B: Digestible, C: Scheme, H: Hasher> {
21 pub(crate) commitment: Commitment<B, C, H>,
23 pub(crate) index: u16,
25 pub(crate) inner: C::Shard,
27}
28
29impl<B: Digestible, C: Scheme, H: Hasher> Shard<B, C, H> {
30 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 pub const fn index(&self) -> u16 {
46 self.index
47 }
48
49 pub const fn commitment(&self) -> Commitment<B, C, H> {
51 self.commitment
52 }
53
54 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 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 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#[derive(Debug)]
157pub struct CodedBlock<B: Block, C: Scheme, H: Hasher> {
158 inner: Arc<B>,
160 commitment: Commitment<B, C, H>,
163 shards: OnceLock<Arc<[C::Shard]>>,
168}
169
170impl<B: Block, C: Scheme, H: Hasher> CodedBlock<B, C, H> {
171 pub const fn max_encode_size(application: usize) -> usize {
174 application + CodingConfig::SIZE
175 }
176
177 #[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 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 pub fn config(&self) -> CodingConfig {
212 self.commitment.config()
213 }
214
215 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 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 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 pub fn inner(&self) -> &B {
247 &self.inner
248 }
249
250 pub fn inner_shared(&self) -> Arc<B> {
252 Arc::clone(&self.inner)
253 }
254
255 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 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 #[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 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 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 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 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
372impl<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
441pub 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
458pub 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 pub fn new(block: CodedBlock<B, C, H>) -> Self {
477 Self {
478 commitment: block.commitment,
479 inner: block.inner,
480 }
481 }
482
483 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 pub fn inner(&self) -> &B {
496 &self.inner
497 }
498}
499
500impl<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
507impl<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
596pub 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 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 #[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 let widest = MAX + CodingConfig::SIZE;
665 assert!(RShard::decode_cfg(encode(widest, CONFIG), &MAX).is_ok());
666
667 assert!(RShard::decode_cfg(encode(widest + 1, CONFIG), &MAX).is_err());
669
670 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 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 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 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 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 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}