1use crate::{
2 CertifiableBlock, Heightable,
3 marshal::{
4 ancestry::BlockProvider,
5 coding::{
6 shards,
7 types::{CodedBlock, Shard, StoredCodedBlock, coding_config_for_participants},
8 },
9 core::{Buffer, CommitmentFallback, ExpectedCommitment, Mailbox, Variant},
10 },
11 simplex::{scheme::Scheme as SimplexScheme, types::Context},
12 types::{Round, coding::Commitment},
13};
14use commonware_codec::{Codec, Decode as _, Error as CodecError, Input, Read};
15use commonware_coding::{Config as CodingConfig, Scheme as CodingScheme};
16use commonware_cryptography::{
17 Committable, Digested, Digestible, Hasher, PublicKey, certificate::Scheme,
18};
19use commonware_p2p::Recipients;
20use commonware_parallel::Strategy;
21use commonware_utils::channel::oneshot;
22use std::{future::Future, sync::Arc};
23
24#[derive(Default)]
29pub struct Coding<B, C, H, P>(std::marker::PhantomData<(B, C, H, P)>)
30where
31 B: CertifiableBlock<Context = Context<Commitment<B, C, H>, P>> + Codec,
32 C: CodingScheme,
33 H: Hasher,
34 P: PublicKey;
35
36impl<B, C, H, P> Clone for Coding<B, C, H, P>
37where
38 B: CertifiableBlock<Context = Context<Commitment<B, C, H>, P>> + Codec,
39 C: CodingScheme,
40 H: Hasher,
41 P: PublicKey,
42{
43 fn clone(&self) -> Self {
44 *self
45 }
46}
47
48impl<B, C, H, P> Copy for Coding<B, C, H, P>
49where
50 B: CertifiableBlock<Context = Context<Commitment<B, C, H>, P>> + Codec,
51 C: CodingScheme,
52 H: Hasher,
53 P: PublicKey,
54{
55}
56
57impl<B, C, H, P> Variant for Coding<B, C, H, P>
58where
59 B: CertifiableBlock<Context = Context<Commitment<B, C, H>, P>> + Codec,
60 C: CodingScheme,
61 H: Hasher,
62 P: PublicKey,
63{
64 type ApplicationBlock = B;
65 type Block = Arc<CodedBlock<B, C, H>>;
66 type StoredBlock = StoredCodedBlock<B, C, H>;
67 type Commitment = Commitment<B, C, H>;
68
69 fn commitment(block: &Self::Block) -> Self::Commitment {
70 block.commitment()
71 }
72
73 fn commitment_to_inner(commitment: Self::Commitment) -> <Self::Block as Digestible>::Digest {
74 commitment.block()
76 }
77
78 fn parent_commitment(block: &Self::Block) -> Self::Commitment {
79 block.context().parent.1
81 }
82
83 fn stored_parent_commitment(block: &Self::StoredBlock) -> Self::Commitment {
84 block.context().parent.1
85 }
86
87 fn check_payload<S>(scheme: &S, payload: Self::Commitment) -> bool
88 where
89 S: SimplexScheme<Self::Commitment>,
90 {
91 let n_participants = u16::try_from(scheme.participants().len())
92 .expect("scheme must have at most 2^16-1 participants");
93 payload.config() == coding_config_for_participants(n_participants)
94 }
95
96 fn decode_block(
97 buf: impl Input,
98 block_cfg: &<Self::ApplicationBlock as Read>::Cfg,
99 expected: ExpectedCommitment<Self::Commitment>,
100 strategy: &impl Strategy,
101 ) -> impl Future<Output = Result<Self::Block, CodecError>> + Send {
102 let decoded = <(B, CodingConfig)>::decode_cfg(buf, &(block_cfg.clone(), ()))
103 .map(|(inner, config)| CodedBlock::from_decoded(inner, config, expected, strategy));
104 async move { decoded?.await.map(Arc::new) }
105 }
106
107 fn restore(
108 stored: Self::StoredBlock,
109 digest: <Self::Block as Digestible>::Digest,
110 ) -> Self::Block {
111 assert_eq!(
112 stored.digest(),
113 digest,
114 "stored block was keyed by another digest"
115 );
116 stored.into()
117 }
118
119 fn restore_across(
120 stored: Self::StoredBlock,
121 _strategy: &impl Strategy,
122 ) -> impl Future<Output = Self::Block> + Send {
123 std::future::ready(stored.into())
125 }
126
127 fn into_shared(block: Self::Block) -> Digested<Self::ApplicationBlock> {
128 block.inner_digested()
129 }
130
131 fn from_application_block(
132 block: Digested<Self::ApplicationBlock>,
133 payload: Self::Commitment,
134 ) -> Self::Block {
135 Arc::new(CodedBlock::precomputed(block, payload))
136 }
137
138 fn max_backfill_block_size(application: usize) -> usize {
139 CodedBlock::<B, C, H>::max_encode_size(application)
140 }
141
142 fn max_buffer_payload_size(participants: u32, application: usize) -> usize {
149 let block = CodedBlock::<B, C, H>::max_encode_size(application);
151
152 let participants =
156 u16::try_from(participants).expect("committee must have at most u16::MAX members");
157 let widest = (4..=participants)
158 .map(|n| C::max_shard_size(&coding_config_for_participants(n), block))
159 .max()
160 .expect("committee must have at least 4 members");
161
162 Shard::<B, C, H>::max_encode_size(widest)
163 }
164}
165
166impl<B, C, H, P> Buffer<Coding<B, C, H, P>> for shards::Mailbox<B, C, H, P>
167where
168 B: CertifiableBlock<Context = Context<Commitment<B, C, H>, P>> + Codec,
169 C: CodingScheme,
170 H: Hasher,
171 P: PublicKey,
172{
173 type PublicKey = P;
174
175 async fn find_by_digest(
176 &self,
177 digest: <CodedBlock<B, C, H> as Digestible>::Digest,
178 ) -> Option<Arc<CodedBlock<B, C, H>>> {
179 self.get_by_digest(digest).await
180 }
181
182 async fn find_by_commitment(
183 &self,
184 commitment: Commitment<B, C, H>,
185 ) -> Option<Arc<CodedBlock<B, C, H>>> {
186 self.get(commitment).await
187 }
188
189 fn subscribe_by_digest(
190 &self,
191 digest: <CodedBlock<B, C, H> as Digestible>::Digest,
192 ) -> Option<oneshot::Receiver<Arc<CodedBlock<B, C, H>>>> {
193 Some(self.subscribe_by_digest(digest))
194 }
195
196 fn subscribe_by_commitment(
197 &self,
198 commitment: Commitment<B, C, H>,
199 ) -> Option<oneshot::Receiver<Arc<CodedBlock<B, C, H>>>> {
200 Some(self.subscribe(commitment))
201 }
202
203 fn send(&self, round: Round, block: Arc<CodedBlock<B, C, H>>, _recipients: Recipients<P>) {
204 self.proposed_shared(round, block);
206 }
207}
208
209impl<S, B, C, H, P> BlockProvider for Mailbox<S, Coding<B, C, H, P>>
210where
211 S: Scheme,
212 B: CertifiableBlock<Context = Context<Commitment<B, C, H>, P>> + Codec,
213 C: CodingScheme,
214 H: Hasher,
215 P: PublicKey,
216{
217 type Block = B;
218
219 fn subscribe_parent(
220 &self,
221 block: &Digested<Self::Block>,
222 ) -> impl Future<Output = Option<Digested<Self::Block>>> + Send + 'static {
223 let receiver = block.height().previous().map(|parent_height| {
224 self.subscribe_by_commitment(
225 block.context().parent.1,
226 CommitmentFallback::FetchByCommitment {
227 height: parent_height,
228 },
229 )
230 });
231 async move { receiver?.await.ok().map(|block| block.inner_digested()) }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::{
239 marshal::{
240 coding::types::StoredCodedBlock,
241 mocks::block::{Block, encode_block},
242 },
243 types::{Epoch, Height, View},
244 };
245 use bytes::BufMut;
246 use commonware_codec::{Buf, Encode, EncodeSize, Error, Read, Write};
247 use commonware_coding::{Config as CodingConfig, ReedSolomon};
248 use commonware_cryptography::{
249 Digest as _, Digestible, Signer as _,
250 ed25519::{PrivateKey, PublicKey},
251 sha256::{Digest as Sha256Digest, Sha256},
252 };
253 use commonware_math::algebra::Random;
254 use commonware_parallel::{Sequential, mocks::explore};
255 use commonware_utils::{NZU16, test_rng};
256 use futures::executor::block_on;
257
258 type TestCommitment = Commitment<NoCloneBlock, ReedSolomon<Sha256>, Sha256>;
259 type TestContext = Context<TestCommitment, PublicKey>;
260 type InnerBlock = Block<Sha256Digest, TestContext>;
261 type TestScheme = ReedSolomon<Sha256>;
262 type TestVariant = Coding<NoCloneBlock, TestScheme, Sha256, PublicKey>;
263
264 const CONFIG: CodingConfig = CodingConfig {
266 minimum_shards: NZU16!(1),
267 extra_shards: NZU16!(2),
268 };
269
270 struct NoCloneBlock {
271 inner: InnerBlock,
272 }
273
274 impl Clone for NoCloneBlock {
275 fn clone(&self) -> Self {
276 panic!("shared block operations must not clone the inner block");
277 }
278 }
279
280 impl Write for NoCloneBlock {
281 fn write(&self, writer: &mut impl BufMut) {
282 self.inner.write(writer);
283 }
284 }
285
286 impl Read for NoCloneBlock {
287 type Cfg = ();
288
289 fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, Error> {
290 Ok(Self {
291 inner: InnerBlock::read_cfg(reader, cfg)?,
292 })
293 }
294 }
295
296 impl EncodeSize for NoCloneBlock {
297 fn encode_size(&self) -> usize {
298 self.inner.encode_size()
299 }
300 }
301
302 impl Digestible for NoCloneBlock {
303 type Digest = Sha256Digest;
304
305 fn digest(&self) -> Self::Digest {
306 self.inner.digest()
307 }
308 }
309
310 impl crate::Heightable for NoCloneBlock {
311 fn height(&self) -> Height {
312 self.inner.height
313 }
314 }
315
316 impl crate::Block for NoCloneBlock {
317 fn parent(&self) -> Self::Digest {
318 self.inner.parent
319 }
320 }
321
322 impl CertifiableBlock for NoCloneBlock {
323 type Context = TestContext;
324
325 fn context(&self) -> Self::Context {
326 self.inner.context.clone()
327 }
328 }
329
330 fn no_clone_block(config: CodingConfig) -> NoCloneBlock {
331 let mut rng = test_rng();
332 let leader = PrivateKey::random(&mut rng).public_key();
333 let parent_commitment = TestCommitment::from((
334 Sha256Digest::EMPTY,
335 Sha256Digest::EMPTY,
336 Sha256Digest::EMPTY,
337 config,
338 ));
339 let context = Context {
340 round: Round::new(Epoch::new(1), View::new(2)),
341 leader,
342 parent: (View::new(1), parent_commitment),
343 };
344 let inner = InnerBlock::new::<Sha256>(
345 context,
346 Sha256::hash(&[b"parent"]),
347 Height::new(7),
348 1_234_567,
349 );
350 NoCloneBlock { inner }
351 }
352
353 #[test]
354 fn max_backfill_block_size_matches_encoding() {
355 let block = no_clone_block(CONFIG);
358 let size = block.encode_size();
359 let coded = encode_block::<NoCloneBlock, TestScheme, Sha256>(block, CONFIG);
360 assert_eq!(
361 TestVariant::max_backfill_block_size(size),
362 coded.encode().len()
363 );
364 }
365
366 #[test]
367 fn storage_conversion_shares_coding_block() {
368 let block = no_clone_block(CONFIG);
369 let coded = Arc::new(encode_block::<NoCloneBlock, TestScheme, Sha256>(
370 block, CONFIG,
371 ));
372 let expected = coded.commitment();
373 let stored: StoredCodedBlock<_, _, _> = coded.clone().into();
374
375 assert_eq!(stored.commitment(), expected);
376 assert!(std::ptr::eq(stored.inner(), coded.inner()));
377
378 let recovered = stored.into();
379 assert_eq!(TestVariant::commitment(&recovered), expected);
380 assert!(Arc::ptr_eq(
381 &TestVariant::into_shared(recovered).inner_shared(),
382 &coded.inner_shared()
383 ));
384 }
385
386 #[test]
387 fn decode_block_recomputes_untrusted_root() {
388 let block = no_clone_block(CONFIG);
389 let coded = encode_block::<NoCloneBlock, TestScheme, Sha256>(block, CONFIG);
390 let expected = coded.commitment();
391 let encoded = coded.encode();
392 explore(|strategy| {
393 let decoded = block_on(TestVariant::decode_block(
395 encoded.clone(),
396 &(),
397 ExpectedCommitment::Untrusted(expected),
398 strategy,
399 ))
400 .unwrap();
401 assert_eq!(TestVariant::commitment(&decoded), expected);
402 assert!(decoded.shard(0).is_some());
403
404 let other = TestCommitment::from((
406 expected.block(),
407 Sha256::hash(&[b"other root"]),
408 expected.context(),
409 CONFIG,
410 ));
411 assert!(
412 block_on(TestVariant::decode_block(
413 encoded.clone(),
414 &(),
415 ExpectedCommitment::Untrusted(other),
416 strategy,
417 ))
418 .is_err()
419 );
420 let trusted = block_on(TestVariant::decode_block(
421 encoded.clone(),
422 &(),
423 ExpectedCommitment::Trusted(other),
424 strategy,
425 ))
426 .unwrap();
427 assert_eq!(TestVariant::commitment(&trusted), other);
428 assert!(trusted.shard(0).is_none());
429
430 let mut extended = encoded.to_vec();
432 extended.push(0);
433 assert!(
434 block_on(TestVariant::decode_block(
435 extended,
436 &(),
437 ExpectedCommitment::Untrusted(expected),
438 strategy,
439 ))
440 .is_err()
441 );
442
443 (
445 TestVariant::commitment(&decoded),
446 TestVariant::commitment(&trusted),
447 )
448 });
449 }
450
451 #[test]
452 fn decode_block_binds_context_digest() {
453 let block = no_clone_block(CONFIG);
454 let coded = encode_block::<NoCloneBlock, TestScheme, Sha256>(block, CONFIG);
455 let expected = coded.commitment();
456 let encoded = coded.encode();
457
458 let other = TestCommitment::from((
460 expected.block(),
461 expected.root(),
462 Sha256::hash(&[b"other context"]),
463 CONFIG,
464 ));
465 for expected in [
466 ExpectedCommitment::Trusted(other),
467 ExpectedCommitment::Untrusted(other),
468 ] {
469 assert!(
470 block_on(TestVariant::decode_block(
471 encoded.clone(),
472 &(),
473 expected,
474 &Sequential
475 ))
476 .is_err()
477 );
478 }
479 }
480}