Skip to main content

commonware_consensus/marshal/coding/
variant.rs

1use crate::{
2    marshal::{
3        ancestry::BlockProvider,
4        coding::{
5            shards,
6            types::{coding_config_for_participants, CodedBlock, CodedBlockCfg, StoredCodedBlock},
7        },
8        core::{Buffer, CommitmentFallback, Mailbox, Variant},
9    },
10    simplex::{scheme::Scheme as SimplexScheme, types::Context},
11    types::{coding::Commitment, Round},
12    CertifiableBlock,
13};
14use commonware_codec::Read;
15use commonware_coding::Scheme as CodingScheme;
16use commonware_cryptography::{certificate::Scheme, Committable, Digestible, Hasher, PublicKey};
17use commonware_p2p::Recipients;
18use commonware_utils::channel::oneshot;
19use std::{future::Future, sync::Arc};
20
21/// The coding variant of Marshal, which uses erasure coding for block dissemination.
22///
23/// This variant distributes blocks as erasure-coded shards, allowing reconstruction
24/// from a subset of shards. This reduces bandwidth requirements for block propagation.
25#[derive(Default, Clone, Copy)]
26pub struct Coding<B, C, H, P>(std::marker::PhantomData<(B, C, H, P)>)
27where
28    B: CertifiableBlock<Context = Context<Commitment, P>>,
29    C: CodingScheme,
30    H: Hasher,
31    P: PublicKey;
32
33impl<B, C, H, P> Variant for Coding<B, C, H, P>
34where
35    B: CertifiableBlock<Context = Context<Commitment, P>>,
36    C: CodingScheme,
37    H: Hasher,
38    P: PublicKey,
39{
40    type ApplicationBlock = B;
41    type Block = CodedBlock<B, C, H>;
42    type StoredBlock = StoredCodedBlock<B, C, H>;
43    type Commitment = Commitment;
44
45    fn commitment(block: &Self::Block) -> Self::Commitment {
46        // Commitment is deterministic from the coded block contents.
47        block.commitment()
48    }
49
50    fn stored_commitment(block: &Self::StoredBlock) -> Self::Commitment {
51        block.commitment()
52    }
53
54    fn commitment_to_inner(commitment: Self::Commitment) -> <Self::Block as Digestible>::Digest {
55        // The inner digest is embedded in the coding commitment.
56        commitment.block()
57    }
58
59    fn parent_commitment(block: &Self::Block) -> Self::Commitment {
60        // Parent commitment is embedded in the consensus context.
61        block.context().parent.1
62    }
63
64    fn check_payload<S>(scheme: &S, payload: Self::Commitment) -> bool
65    where
66        S: SimplexScheme<Self::Commitment>,
67    {
68        let n_participants = u16::try_from(scheme.participants().len())
69            .expect("scheme must have at most 2^16-1 participants");
70        payload.config() == coding_config_for_participants(n_participants)
71    }
72
73    fn block_cfg(
74        block_cfg: &<Self::ApplicationBlock as Read>::Cfg,
75        expected: Self::Commitment,
76    ) -> <Self::Block as Read>::Cfg {
77        CodedBlockCfg {
78            inner: block_cfg.clone(),
79            expected,
80        }
81    }
82
83    fn into_inner(block: Self::Block) -> Self::ApplicationBlock {
84        block.into_inner()
85    }
86
87    fn into_inner_shared(block: Arc<Self::Block>) -> Arc<Self::ApplicationBlock> {
88        block.inner_shared()
89    }
90
91    fn owned_into_inner_shared(block: Self::Block) -> Arc<Self::ApplicationBlock> {
92        block.into_inner_shared()
93    }
94
95    fn from_application_block(
96        block: Self::ApplicationBlock,
97        payload: Self::Commitment,
98    ) -> Self::Block {
99        CodedBlock::new_trusted(block, payload)
100    }
101}
102
103impl<B, C, H, P> Buffer<Coding<B, C, H, P>> for shards::Mailbox<B, C, H, P>
104where
105    B: CertifiableBlock<Context = Context<Commitment, P>>,
106    C: CodingScheme,
107    H: Hasher,
108    P: PublicKey,
109{
110    type PublicKey = P;
111
112    async fn find_by_digest(
113        &self,
114        digest: <CodedBlock<B, C, H> as Digestible>::Digest,
115    ) -> Option<Arc<CodedBlock<B, C, H>>> {
116        self.get_by_digest(digest).await
117    }
118
119    async fn find_by_commitment(&self, commitment: Commitment) -> Option<Arc<CodedBlock<B, C, H>>> {
120        self.get(commitment).await
121    }
122
123    fn subscribe_by_digest(
124        &self,
125        digest: <CodedBlock<B, C, H> as Digestible>::Digest,
126    ) -> Option<oneshot::Receiver<Arc<CodedBlock<B, C, H>>>> {
127        Some(self.subscribe_by_digest(digest))
128    }
129
130    fn subscribe_by_commitment(
131        &self,
132        commitment: Commitment,
133    ) -> Option<oneshot::Receiver<Arc<CodedBlock<B, C, H>>>> {
134        Some(self.subscribe(commitment))
135    }
136
137    fn finalized(&self, commitment: Commitment) {
138        self.prune(commitment);
139    }
140
141    fn send(&self, round: Round, block: Arc<CodedBlock<B, C, H>>, _recipients: Recipients<P>) {
142        // Targeted forwarding is not supported by the coding variant.
143        self.proposed_shared(round, block);
144    }
145}
146
147impl<S, B, C, H, P> BlockProvider for Mailbox<S, Coding<B, C, H, P>>
148where
149    S: Scheme,
150    B: CertifiableBlock<Context = Context<Commitment, P>>,
151    C: CodingScheme,
152    H: Hasher,
153    P: PublicKey,
154{
155    type Block = B;
156
157    fn subscribe_parent(
158        &self,
159        block: &Self::Block,
160    ) -> impl Future<Output = Option<Arc<Self::Block>>> + Send + 'static {
161        let receiver = block.height().previous().map(|parent_height| {
162            self.subscribe_by_commitment(
163                block.context().parent.1,
164                CommitmentFallback::FetchByCommitment {
165                    height: parent_height,
166                },
167            )
168        });
169        async move { receiver?.await.ok().map(|block| block.inner_shared()) }
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::{
177        marshal::{coding::types::StoredCodedBlock, mocks::block::Block as MockBlock},
178        types::{Epoch, Height, View},
179    };
180    use bytes::{Buf, BufMut};
181    use commonware_codec::{EncodeSize, Error, Read, Write};
182    use commonware_coding::{Config as CodingConfig, ReedSolomon};
183    use commonware_cryptography::{
184        ed25519::{PrivateKey, PublicKey},
185        sha256::{Digest as Sha256Digest, Sha256},
186        Digest as _, Digestible, Signer as _,
187    };
188    use commonware_math::algebra::Random;
189    use commonware_parallel::Sequential;
190    use commonware_utils::{test_rng, NZU16};
191
192    type TestContext = Context<Commitment, PublicKey>;
193    type InnerBlock = MockBlock<Sha256Digest, TestContext>;
194
195    struct NoCloneBlock {
196        inner: InnerBlock,
197    }
198
199    impl Clone for NoCloneBlock {
200        fn clone(&self) -> Self {
201            panic!("stored commitment lookup must not clone the inner block");
202        }
203    }
204
205    impl Write for NoCloneBlock {
206        fn write(&self, writer: &mut impl BufMut) {
207            self.inner.write(writer);
208        }
209    }
210
211    impl Read for NoCloneBlock {
212        type Cfg = ();
213
214        fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, Error> {
215            Ok(Self {
216                inner: InnerBlock::read_cfg(reader, cfg)?,
217            })
218        }
219    }
220
221    impl EncodeSize for NoCloneBlock {
222        fn encode_size(&self) -> usize {
223            self.inner.encode_size()
224        }
225    }
226
227    impl Digestible for NoCloneBlock {
228        type Digest = Sha256Digest;
229
230        fn digest(&self) -> Self::Digest {
231            self.inner.digest()
232        }
233    }
234
235    impl crate::Heightable for NoCloneBlock {
236        fn height(&self) -> Height {
237            self.inner.height
238        }
239    }
240
241    impl crate::Block for NoCloneBlock {
242        fn parent(&self) -> Self::Digest {
243            self.inner.parent
244        }
245    }
246
247    impl CertifiableBlock for NoCloneBlock {
248        type Context = TestContext;
249
250        fn context(&self) -> Self::Context {
251            self.inner.context.clone()
252        }
253    }
254
255    fn no_clone_block(config: CodingConfig) -> NoCloneBlock {
256        let mut rng = test_rng();
257        let leader = PrivateKey::random(&mut rng).public_key();
258        let parent_commitment = Commitment::from((
259            Sha256Digest::EMPTY,
260            Sha256Digest::EMPTY,
261            Sha256Digest::EMPTY,
262            config,
263        ));
264        let context = Context {
265            round: Round::new(Epoch::new(1), View::new(2)),
266            leader,
267            parent: (View::new(1), parent_commitment),
268        };
269        let inner =
270            InnerBlock::new::<Sha256>(context, Sha256::hash(b"parent"), Height::new(7), 1_234_567);
271        NoCloneBlock { inner }
272    }
273
274    #[test]
275    fn stored_commitment_does_not_clone_coding_block() {
276        const CONFIG: CodingConfig = CodingConfig {
277            minimum_shards: NZU16!(1),
278            extra_shards: NZU16!(2),
279        };
280
281        type TestScheme = ReedSolomon<Sha256>;
282        type TestVariant = Coding<NoCloneBlock, TestScheme, Sha256, PublicKey>;
283
284        let block = no_clone_block(CONFIG);
285        let coded = CodedBlock::<NoCloneBlock, TestScheme, Sha256>::new(block, CONFIG, &Sequential);
286        let expected = coded.commitment();
287        let stored = StoredCodedBlock::new(coded);
288
289        assert_eq!(TestVariant::stored_commitment(&stored), expected);
290    }
291}