1use 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
17pub struct Shard<B: Digestible, C: Scheme, H: Hasher> {
20 pub(crate) commitment: Commitment<B, C, H>,
22 pub(crate) index: u16,
24 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 pub const fn index(&self) -> u16 {
39 self.index
40 }
41
42 pub const fn commitment(&self) -> Commitment<B, C, H> {
44 self.commitment
45 }
46
47 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#[derive(Debug)]
141pub struct CodedBlock<B: Block, C: Scheme, H: Hasher> {
142 inner: Arc<B>,
144 config: CodingConfig,
146 commitment: C::Commitment,
148 shards: OnceLock<Arc<[C::Shard]>>,
152 _hasher: PhantomData<H>,
154}
155
156impl<B: Block, C: Scheme, H: Hasher> CodedBlock<B, C, H> {
157 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 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 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 pub const fn config(&self) -> CodingConfig {
199 self.config
200 }
201
202 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 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 pub fn inner(&self) -> &B {
232 &self.inner
233 }
234
235 pub fn inner_shared(&self) -> Arc<B> {
237 Arc::clone(&self.inner)
238 }
239
240 pub fn into_inner_shared(self) -> Arc<B> {
242 self.inner
243 }
244
245 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
305pub struct CodedBlockCfg<B: Block, C: Scheme, H: Hasher> {
312 pub inner: <B as Read>::Cfg,
314 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 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
400pub 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
418pub 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 pub fn new(block: CodedBlock<B, C, H>) -> Self {
441 Self {
442 commitment: block.commitment(),
443 inner: block.inner,
444 }
445 }
446
447 pub fn into_coded_block(self) -> CodedBlock<B, C, H> {
451 CodedBlock::new_trusted_shared(self.inner, self.commitment)
452 }
453
454 pub fn inner(&self) -> &B {
456 &self.inner
457 }
458}
459
460impl<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 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 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
559pub 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, };
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 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 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 let block_size = stored.inner().encode_size();
828 encoded[block_size] ^= 0xFF;
829
830 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}