Skip to main content

commonware_consensus/marshal/coding/
variant.rs

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