1use crate::{
4 types::{coding::Commitment, Height},
5 Block, CertifiableBlock, Heightable,
6};
7use commonware_codec::{BufsMut, EncodeSize, Read, ReadExt, Write};
8use commonware_coding::{Config as CodingConfig, Scheme};
9use commonware_cryptography::{Committable, Digestible, Hasher};
10use commonware_parallel::{Sequential, Strategy};
11use commonware_utils::{Faults, N3f1, NZU16};
12use std::marker::PhantomData;
13
14pub struct Shard<C: Scheme, H: Hasher> {
17 pub(crate) commitment: Commitment,
19 pub(crate) index: u16,
21 pub(crate) inner: C::Shard,
23 _hasher: PhantomData<H>,
25}
26
27impl<C: Scheme, H: Hasher> Shard<C, H> {
28 pub const fn new(commitment: Commitment, index: u16, inner: C::Shard) -> Self {
29 Self {
30 commitment,
31 index,
32 inner,
33 _hasher: PhantomData,
34 }
35 }
36
37 pub const fn index(&self) -> u16 {
39 self.index
40 }
41
42 pub const fn commitment(&self) -> Commitment {
44 self.commitment
45 }
46
47 pub fn into_inner(self) -> C::Shard {
49 self.inner
50 }
51}
52
53impl<C: Scheme, H: Hasher> Clone for Shard<C, H> {
54 fn clone(&self) -> Self {
55 Self {
56 commitment: self.commitment,
57 index: self.index,
58 inner: self.inner.clone(),
59 _hasher: PhantomData,
60 }
61 }
62}
63
64impl<C: Scheme, H: Hasher> Committable for Shard<C, H> {
65 type Commitment = Commitment;
66
67 fn commitment(&self) -> Self::Commitment {
68 self.commitment
69 }
70}
71
72impl<C: Scheme, H: Hasher> Write for Shard<C, H> {
73 fn write(&self, buf: &mut impl bytes::BufMut) {
74 self.commitment.write(buf);
75 self.index.write(buf);
76 self.inner.write(buf);
77 }
78
79 fn write_bufs(&self, buf: &mut impl BufsMut) {
80 self.commitment.write(buf);
81 self.index.write(buf);
82 self.inner.write_bufs(buf);
83 }
84}
85
86impl<C: Scheme, H: Hasher> EncodeSize for Shard<C, H> {
87 fn encode_size(&self) -> usize {
88 self.commitment.encode_size() + self.index.encode_size() + self.inner.encode_size()
89 }
90
91 fn encode_inline_size(&self) -> usize {
92 self.commitment.encode_size() + self.index.encode_size() + self.inner.encode_inline_size()
93 }
94}
95
96impl<C: Scheme, H: Hasher> Read for Shard<C, H> {
97 type Cfg = commonware_coding::CodecConfig;
98
99 fn read_cfg(
100 buf: &mut impl bytes::Buf,
101 cfg: &Self::Cfg,
102 ) -> Result<Self, commonware_codec::Error> {
103 let commitment = Commitment::read(buf)?;
104 let index = u16::read(buf)?;
105 let inner = C::Shard::read_cfg(buf, cfg)?;
106
107 Ok(Self {
108 commitment,
109 index,
110 inner,
111 _hasher: PhantomData,
112 })
113 }
114}
115
116impl<C: Scheme, H: Hasher> PartialEq for Shard<C, H> {
117 fn eq(&self, other: &Self) -> bool {
118 self.commitment == other.commitment
119 && self.index == other.index
120 && self.inner == other.inner
121 }
122}
123
124impl<C: Scheme, H: Hasher> Eq for Shard<C, H> {}
125
126#[cfg(feature = "arbitrary")]
127impl<C: Scheme, H: Hasher> arbitrary::Arbitrary<'_> for Shard<C, H>
128where
129 C::Shard: for<'a> arbitrary::Arbitrary<'a>,
130{
131 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
132 Ok(Self {
133 commitment: u.arbitrary()?,
134 index: u.arbitrary()?,
135 inner: u.arbitrary()?,
136 _hasher: PhantomData,
137 })
138 }
139}
140
141#[derive(Debug)]
143pub struct CodedBlock<B: Block, C: Scheme, H: Hasher> {
144 inner: B,
146 config: CodingConfig,
148 commitment: C::Commitment,
150 shards: Option<Vec<C::Shard>>,
156 _hasher: PhantomData<H>,
158}
159
160impl<B: Block, C: Scheme, H: Hasher> CodedBlock<B, C, H> {
161 fn encode(
163 inner: &B,
164 config: CodingConfig,
165 strategy: &impl Strategy,
166 ) -> (C::Commitment, Vec<C::Shard>) {
167 let mut buf = Vec::with_capacity(inner.encode_size() + config.encode_size());
168 inner.write(&mut buf);
169 config.write(&mut buf);
170
171 C::encode(&config, buf.as_slice(), strategy).expect("must encode block successfully")
172 }
173
174 pub fn new(inner: B, config: CodingConfig, strategy: &impl Strategy) -> Self {
176 let (commitment, shards) = Self::encode(&inner, config, strategy);
177 Self {
178 inner,
179 config,
180 commitment,
181 shards: Some(shards),
182 _hasher: PhantomData,
183 }
184 }
185
186 pub fn new_trusted(inner: B, commitment: Commitment) -> Self {
188 Self {
189 inner,
190 config: commitment.config(),
191 commitment: commitment.root(),
192 shards: None,
193 _hasher: PhantomData,
194 }
195 }
196
197 pub const fn config(&self) -> CodingConfig {
199 self.config
200 }
201
202 pub fn shards(&mut self, strategy: &impl Strategy) -> &[C::Shard] {
206 match self.shards {
207 Some(ref shards) => shards,
208 None => {
209 let (commitment, shards) = Self::encode(&self.inner, self.config, strategy);
210
211 assert_eq!(
212 commitment, self.commitment,
213 "coded block constructed with trusted commitment does not match commitment"
214 );
215
216 self.shards = Some(shards);
217 self.shards.as_ref().unwrap()
218 }
219 }
220 }
221
222 pub fn shard(&self, index: u16) -> Option<Shard<C, H>>
224 where
225 B: CertifiableBlock,
226 {
227 Some(Shard::new(
228 self.commitment(),
229 index,
230 self.shards.as_ref()?.get(usize::from(index))?.clone(),
231 ))
232 }
233
234 pub const fn inner(&self) -> &B {
236 &self.inner
237 }
238
239 pub fn into_inner(self) -> B {
241 self.inner
242 }
243}
244
245impl<B: CertifiableBlock, C: Scheme, H: Hasher> From<CodedBlock<B, C, H>>
246 for StoredCodedBlock<B, C, H>
247{
248 fn from(block: CodedBlock<B, C, H>) -> Self {
249 Self::new(block)
250 }
251}
252
253impl<B: Block + Clone, C: Scheme, H: Hasher> Clone for CodedBlock<B, C, H> {
254 fn clone(&self) -> Self {
255 Self {
256 inner: self.inner.clone(),
257 config: self.config,
258 commitment: self.commitment,
259 shards: self.shards.clone(),
260 _hasher: PhantomData,
261 }
262 }
263}
264
265impl<B: CertifiableBlock, C: Scheme, H: Hasher> Committable for CodedBlock<B, C, H> {
266 type Commitment = Commitment;
267
268 fn commitment(&self) -> Self::Commitment {
269 Commitment::from((
270 self.digest(),
271 self.commitment,
272 hash_context::<H, _>(&self.inner.context()),
273 self.config,
274 ))
275 }
276}
277
278impl<B: Block, C: Scheme, H: Hasher> Digestible for CodedBlock<B, C, H> {
279 type Digest = B::Digest;
280
281 fn digest(&self) -> Self::Digest {
282 self.inner.digest()
283 }
284}
285
286impl<B: Block, C: Scheme, H: Hasher> Write for CodedBlock<B, C, H> {
287 fn write(&self, buf: &mut impl bytes::BufMut) {
288 self.inner.write(buf);
289 self.config.write(buf);
290 }
291}
292
293impl<B: Block, C: Scheme, H: Hasher> EncodeSize for CodedBlock<B, C, H> {
294 fn encode_size(&self) -> usize {
295 self.inner.encode_size() + self.config.encode_size()
296 }
297}
298
299impl<B: Block, C: Scheme, H: Hasher> Read for CodedBlock<B, C, H> {
300 type Cfg = <B as Read>::Cfg;
301
302 fn read_cfg(
303 buf: &mut impl bytes::Buf,
304 block_cfg: &Self::Cfg,
305 ) -> Result<Self, commonware_codec::Error> {
306 let inner = B::read_cfg(buf, block_cfg)?;
307 let config = CodingConfig::read(buf)?;
308
309 let mut buf = Vec::with_capacity(inner.encode_size() + config.encode_size());
310 inner.write(&mut buf);
311 config.write(&mut buf);
312 let (commitment, shards) =
313 C::encode(&config, buf.as_slice(), &Sequential).map_err(|_| {
314 commonware_codec::Error::Invalid("CodedBlock", "Failed to re-commit to block")
315 })?;
316
317 Ok(Self {
318 inner,
319 config,
320 commitment,
321 shards: Some(shards),
322 _hasher: PhantomData,
323 })
324 }
325}
326
327impl<B: CertifiableBlock, C: Scheme, H: Hasher> Block for CodedBlock<B, C, H> {
328 fn parent(&self) -> Self::Digest {
329 self.inner.parent()
330 }
331}
332
333impl<B: Block, C: Scheme, H: Hasher> Heightable for CodedBlock<B, C, H> {
334 fn height(&self) -> Height {
335 self.inner.height()
336 }
337}
338
339impl<B: CertifiableBlock, C: Scheme, H: Hasher> CertifiableBlock for CodedBlock<B, C, H> {
340 type Context = B::Context;
341
342 fn context(&self) -> Self::Context {
343 self.inner.context()
344 }
345}
346
347pub fn hash_context<H: Hasher, C: EncodeSize + Write>(context: &C) -> H::Digest {
349 let mut buf = Vec::with_capacity(context.encode_size());
350 context.write(&mut buf);
351 H::hash(&buf)
352}
353
354impl<B: Block + PartialEq, C: Scheme, H: Hasher> PartialEq for CodedBlock<B, C, H> {
355 fn eq(&self, other: &Self) -> bool {
356 self.inner == other.inner
357 && self.config == other.config
358 && self.commitment == other.commitment
359 && self.shards == other.shards
360 }
361}
362
363impl<B: Block + Eq, C: Scheme, H: Hasher> Eq for CodedBlock<B, C, H> {}
364
365pub struct StoredCodedBlock<B: Block, C: Scheme, H: Hasher> {
378 inner: B,
379 commitment: Commitment,
380 _scheme: PhantomData<(C, H)>,
381}
382
383impl<B: CertifiableBlock, C: Scheme, H: Hasher> StoredCodedBlock<B, C, H> {
384 pub fn new(block: CodedBlock<B, C, H>) -> Self {
389 Self {
390 commitment: block.commitment(),
391 inner: block.inner,
392 _scheme: PhantomData,
393 }
394 }
395
396 pub fn into_coded_block(self) -> CodedBlock<B, C, H> {
401 CodedBlock::new_trusted(self.inner, self.commitment)
402 }
403
404 pub const fn inner(&self) -> &B {
406 &self.inner
407 }
408}
409
410impl<B: Block, C: Scheme, H: Hasher> From<StoredCodedBlock<B, C, H>> for CodedBlock<B, C, H> {
412 fn from(stored: StoredCodedBlock<B, C, H>) -> Self {
413 Self::new_trusted(stored.inner, stored.commitment)
414 }
415}
416
417impl<B: Block + Clone, C: Scheme, H: Hasher> Clone for StoredCodedBlock<B, C, H> {
418 fn clone(&self) -> Self {
419 Self {
420 commitment: self.commitment,
421 inner: self.inner.clone(),
422 _scheme: PhantomData,
423 }
424 }
425}
426
427impl<B: Block, C: Scheme, H: Hasher> Committable for StoredCodedBlock<B, C, H> {
428 type Commitment = Commitment;
429
430 fn commitment(&self) -> Self::Commitment {
431 self.commitment
432 }
433}
434
435impl<B: Block, C: Scheme, H: Hasher> Digestible for StoredCodedBlock<B, C, H> {
436 type Digest = B::Digest;
437
438 fn digest(&self) -> Self::Digest {
439 self.inner.digest()
440 }
441}
442
443impl<B: Block, C: Scheme, H: Hasher> Write for StoredCodedBlock<B, C, H> {
444 fn write(&self, buf: &mut impl bytes::BufMut) {
445 self.inner.write(buf);
446 self.commitment.write(buf);
447 }
448}
449
450impl<B: Block, C: Scheme, H: Hasher> EncodeSize for StoredCodedBlock<B, C, H> {
451 fn encode_size(&self) -> usize {
452 self.inner.encode_size() + self.commitment.encode_size()
453 }
454}
455
456impl<B: Block, C: Scheme, H: Hasher> Read for StoredCodedBlock<B, C, H> {
457 type Cfg = B::Cfg;
459
460 fn read_cfg(
461 buf: &mut impl bytes::Buf,
462 block_cfg: &Self::Cfg,
463 ) -> Result<Self, commonware_codec::Error> {
464 let inner = B::read_cfg(buf, block_cfg)?;
465 let commitment = Commitment::read(buf)?;
466
467 if inner.digest() != commitment.block::<B::Digest>() {
469 return Err(commonware_codec::Error::Invalid(
470 "StoredCodedBlock",
471 "storage corruption: block digest mismatch",
472 ));
473 }
474
475 Ok(Self {
476 commitment,
477 inner,
478 _scheme: PhantomData,
479 })
480 }
481}
482
483impl<B: Block, C: Scheme, H: Hasher> Block for StoredCodedBlock<B, C, H> {
484 fn parent(&self) -> Self::Digest {
485 self.inner.parent()
486 }
487}
488
489impl<B: CertifiableBlock, C: Scheme, H: Hasher> CertifiableBlock for StoredCodedBlock<B, C, H> {
490 type Context = B::Context;
491
492 fn context(&self) -> Self::Context {
493 self.inner.context()
494 }
495}
496
497impl<B: Block, C: Scheme, H: Hasher> Heightable for StoredCodedBlock<B, C, H> {
498 fn height(&self) -> Height {
499 self.inner.height()
500 }
501}
502
503impl<B: Block + PartialEq, C: Scheme, H: Hasher> PartialEq for StoredCodedBlock<B, C, H> {
504 fn eq(&self, other: &Self) -> bool {
505 self.commitment == other.commitment && self.inner == other.inner
506 }
507}
508
509impl<B: Block + Eq, C: Scheme, H: Hasher> Eq for StoredCodedBlock<B, C, H> {}
510
511pub fn coding_config_for_participants(n_participants: u16) -> CodingConfig {
515 let max_faults = N3f1::max_faults(n_participants);
516 assert!(
517 max_faults >= 1,
518 "Need at least 4 participants to maintain fault tolerance"
519 );
520 let max_faults = u16::try_from(max_faults).expect("max_faults must fit in u16");
521 let minimum_shards = NZU16!(max_faults + 1);
522 CodingConfig {
523 minimum_shards,
524 extra_shards: NZU16!(n_participants - minimum_shards.get()),
525 }
526}
527
528#[cfg(test)]
529mod test {
530 use super::*;
531 use crate::{marshal::mocks::block::Block as MockBlock, Block as _};
532 use bytes::Buf;
533 use commonware_codec::{Decode, Encode};
534 use commonware_coding::{CodecConfig, ReedSolomon};
535 use commonware_cryptography::{sha256::Digest as Sha256Digest, Digest, Sha256};
536 use commonware_runtime::{deterministic, iobuf::EncodeExt, BufferPooler, Runner};
537
538 const MAX_SHARD_SIZE: CodecConfig = CodecConfig {
539 maximum_shard_size: 1024 * 1024, };
541
542 type H = Sha256;
543 type RS = ReedSolomon<H>;
544 type RShard = Shard<RS, H>;
545 type Block = MockBlock<<H as Hasher>::Digest, ()>;
546
547 #[test]
548 fn test_shard_wrapper_codec_roundtrip() {
549 const MOCK_BLOCK_DATA: &[u8] = b"commonware shape rotator club";
550 const CONFIG: CodingConfig = CodingConfig {
551 minimum_shards: NZU16!(1),
552 extra_shards: NZU16!(2),
553 };
554
555 let (commitment, shards) = RS::encode(&CONFIG, MOCK_BLOCK_DATA, &Sequential).unwrap();
556 let raw_shard = shards.first().cloned().unwrap();
557
558 let commitment =
559 Commitment::from((Sha256Digest::EMPTY, commitment, Sha256Digest::EMPTY, CONFIG));
560 let shard = RShard::new(commitment, 0, raw_shard);
561 let encoded = shard.encode();
562 let decoded = RShard::decode_cfg(&mut encoded.as_ref(), &MAX_SHARD_SIZE).unwrap();
563 assert!(shard == decoded);
564 }
565
566 #[test]
567 fn test_shard_decode_truncated_returns_error() {
568 let decode = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
569 let mut buf = &[][..];
570 RShard::decode_cfg(&mut buf, &MAX_SHARD_SIZE)
571 }));
572 assert!(decode.is_ok(), "decode must not panic on truncated input");
573 assert!(decode.unwrap().is_err());
574 }
575
576 #[test]
577 fn test_coding_config_for_participants_valid_for_minimum_set() {
578 let config = coding_config_for_participants(4);
579 assert_eq!(config.minimum_shards.get(), 2);
580 assert_eq!(config.extra_shards.get(), 2);
581 }
582
583 #[test]
584 #[should_panic(expected = "Need at least 4 participants to maintain fault tolerance")]
585 fn test_coding_config_for_participants_panics_for_small_sets() {
586 let _ = coding_config_for_participants(3);
587 }
588
589 #[test]
590 fn test_shard_codec_roundtrip() {
591 const MOCK_BLOCK_DATA: &[u8] = b"deadc0de";
592 const CONFIG: CodingConfig = CodingConfig {
593 minimum_shards: NZU16!(1),
594 extra_shards: NZU16!(2),
595 };
596
597 let (commitment, shards) = RS::encode(&CONFIG, MOCK_BLOCK_DATA, &Sequential).unwrap();
598 let raw_shard = shards.first().cloned().unwrap();
599
600 let commitment =
601 Commitment::from((Sha256Digest::EMPTY, commitment, Sha256Digest::EMPTY, CONFIG));
602 let shard = RShard::new(commitment, 0, raw_shard);
603 let encoded = shard.encode();
604 let decoded = RShard::decode_cfg(&mut encoded.as_ref(), &MAX_SHARD_SIZE).unwrap();
605 assert!(shard == decoded);
606 }
607
608 #[test]
609 fn test_coded_block_codec_roundtrip() {
610 const CONFIG: CodingConfig = CodingConfig {
611 minimum_shards: NZU16!(1),
612 extra_shards: NZU16!(2),
613 };
614
615 let block = Block::new::<Sha256>((), Sha256::hash(b"parent"), Height::new(42), 1_234_567);
616 let coded_block = CodedBlock::<Block, RS, H>::new(block, CONFIG, &Sequential);
617
618 let encoded = coded_block.encode();
619 let decoded = CodedBlock::<Block, RS, H>::decode_cfg(encoded, &()).unwrap();
620
621 assert!(coded_block == decoded);
622 }
623
624 #[test]
625 fn test_stored_coded_block_codec_roundtrip() {
626 const CONFIG: CodingConfig = CodingConfig {
627 minimum_shards: NZU16!(1),
628 extra_shards: NZU16!(2),
629 };
630
631 let block = Block::new::<Sha256>((), Sha256::hash(b"parent"), Height::new(42), 1_234_567);
632 let coded_block = CodedBlock::<Block, RS, H>::new(block, CONFIG, &Sequential);
633 let stored = StoredCodedBlock::<Block, RS, H>::new(coded_block.clone());
634
635 assert_eq!(stored.commitment(), coded_block.commitment());
636 assert_eq!(stored.digest(), coded_block.digest());
637 assert_eq!(stored.height(), coded_block.height());
638 assert_eq!(stored.parent(), coded_block.parent());
639
640 let encoded = stored.encode();
641 let decoded = StoredCodedBlock::<Block, RS, H>::decode_cfg(encoded, &()).unwrap();
642
643 assert!(stored == decoded);
644 assert_eq!(decoded.commitment(), coded_block.commitment());
645 assert_eq!(decoded.digest(), coded_block.digest());
646 }
647
648 #[test]
649 fn test_stored_coded_block_into_coded_block() {
650 const CONFIG: CodingConfig = CodingConfig {
651 minimum_shards: NZU16!(1),
652 extra_shards: NZU16!(2),
653 };
654
655 let block = Block::new::<Sha256>((), Sha256::hash(b"parent"), Height::new(42), 1_234_567);
656 let coded_block = CodedBlock::<Block, RS, H>::new(block, CONFIG, &Sequential);
657 let original_commitment = coded_block.commitment();
658 let original_digest = coded_block.digest();
659
660 let stored = StoredCodedBlock::<Block, RS, H>::new(coded_block);
661 let encoded = stored.encode();
662 let decoded = StoredCodedBlock::<Block, RS, H>::decode_cfg(encoded, &()).unwrap();
663 let restored = decoded.into_coded_block();
664
665 assert_eq!(restored.commitment(), original_commitment);
666 assert_eq!(restored.digest(), original_digest);
667 }
668
669 #[test]
670 fn test_stored_coded_block_corruption_detection() {
671 const CONFIG: CodingConfig = CodingConfig {
672 minimum_shards: NZU16!(1),
673 extra_shards: NZU16!(2),
674 };
675
676 let block = Block::new::<Sha256>((), Sha256::hash(b"parent"), Height::new(42), 1_234_567);
677 let coded_block = CodedBlock::<Block, RS, H>::new(block, CONFIG, &Sequential);
678 let stored = StoredCodedBlock::<Block, RS, H>::new(coded_block);
679
680 let mut encoded = stored.encode().to_vec();
681
682 let block_size = stored.inner().encode_size();
684 encoded[block_size] ^= 0xFF;
685
686 let result = StoredCodedBlock::<Block, RS, H>::decode_cfg(&mut encoded.as_slice(), &());
688 assert!(result.is_err());
689 }
690
691 #[test]
692 fn test_shard_encode_with_pool_matches_encode() {
693 let executor = deterministic::Runner::default();
694 executor.start(|context| async move {
695 let pool = context.network_buffer_pool();
696
697 const CONFIG: CodingConfig = CodingConfig {
698 minimum_shards: NZU16!(1),
699 extra_shards: NZU16!(2),
700 };
701
702 let (commitment, shards) =
703 RS::encode(&CONFIG, b"pool encoding test".as_slice(), &Sequential).unwrap();
704 let commitment =
705 Commitment::from((Sha256Digest::EMPTY, commitment, Sha256Digest::EMPTY, CONFIG));
706 let shard = RShard::new(commitment, 0, shards.into_iter().next().unwrap());
707
708 let encoded = shard.encode();
709 let mut encoded_pool = shard.encode_with_pool(pool);
710 let mut encoded_pool_bytes = vec![0u8; encoded_pool.remaining()];
711 encoded_pool.copy_to_slice(&mut encoded_pool_bytes);
712 assert_eq!(encoded_pool_bytes, encoded.as_ref());
713 });
714 }
715
716 #[cfg(feature = "arbitrary")]
717 mod conformance {
718 use super::*;
719 use commonware_codec::conformance::CodecConformance;
720
721 commonware_conformance::conformance_tests! {
722 CodecConformance<Shard<ReedSolomon<Sha256>, Sha256>>,
723 }
724 }
725}