Skip to main content

commonware_consensus/marshal/coding/
variant.rs

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/// The coding variant of Marshal, which uses erasure coding for block dissemination.
25///
26/// This variant distributes blocks as erasure-coded shards, allowing reconstruction
27/// from a subset of shards. This reduces bandwidth requirements for block propagation.
28#[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        // The inner digest is embedded in the coding commitment.
75        commitment.block()
76    }
77
78    fn parent_commitment(block: &Self::Block) -> Self::Commitment {
79        // Parent commitment is embedded in the consensus context.
80        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        // The stored commitment carries the block's identity, so nothing is computed.
124        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    /// Returns an upper bound on the encoded size of any shard a committee of 4 to `participants`
143    /// members codes from a block of at most `application` bytes.
144    ///
145    /// # Panics
146    ///
147    /// Panics if `participants` is below 4 or above `u16::MAX`.
148    fn max_buffer_payload_size(participants: u32, application: usize) -> usize {
149        // The shards engine sends shards coded from the block with its coding configuration.
150        let block = CodedBlock::<B, C, H>::max_encode_size(application);
151
152        // A larger committee can split a block into more data shards or prove each shard with a
153        // deeper tree, so shard width can shrink or grow with the committee. Coding supports
154        // committees of at least 4, so take the widest over every size from 4 to `participants`.
155        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        // Targeted forwarding is not supported by the coding variant.
205        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    /// Coding configuration for the coded blocks built by these tests.
265    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        // Encoding the block with its coding configuration yields what `max_backfill_block_size`
356        // reports.
357        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            // An untrusted commitment is recomputed, so the decoded block already holds its shards.
394            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            // A root that does not encode the block is rejected unless it is trusted.
405            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            // Bytes after the block are rejected.
431            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            // Every shape must decode to the same commitments.
444            (
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        // Only the context digest differs, which only the full-commitment comparison can see.
459        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}