Skip to main content

tempo_commonware_node/consensus/
block.rs

1//! The foundational datastructure the Tempo network comes to consensus over.
2//!
3//! The Tempo [`Block`] at its core is just a thin wrapper around an Ethereum
4//! block.
5
6use alloy_consensus::BlockHeader as _;
7use alloy_primitives::B256;
8use bytes::{Buf, BufMut};
9use commonware_codec::{EncodeSize, Read, Write};
10use commonware_consensus::{
11    Heightable,
12    simplex::types::Context,
13    types::{Epoch, Height, Round, View},
14};
15use commonware_cryptography::{
16    Committable, Digestible, Signer as _,
17    ed25519::{PrivateKey, PublicKey},
18};
19use reth_node_core::primitives::SealedBlock;
20
21use crate::consensus::Digest;
22
23/// A Tempo block.
24///
25// XXX: This is a refinement type around a reth [`SealedBlock`]
26// to hold the trait implementations required by commonwarexyz. Uses
27// Sealed because of the frequent accesses to the hash.
28#[derive(Clone, Debug, PartialEq, Eq)]
29#[repr(transparent)]
30pub(crate) struct Block(SealedBlock<tempo_primitives::Block>);
31
32impl Block {
33    pub(crate) fn from_execution_block(block: SealedBlock<tempo_primitives::Block>) -> Self {
34        Self(block)
35    }
36
37    pub(crate) fn into_inner(self) -> SealedBlock<tempo_primitives::Block> {
38        self.0
39    }
40
41    /// Returns the (eth) hash of the wrapped block.
42    pub(crate) fn block_hash(&self) -> B256 {
43        self.0.hash()
44    }
45
46    /// Returns the hash of the wrapped block as a commonware [`Digest`].
47    pub(crate) fn digest(&self) -> Digest {
48        Digest(self.hash())
49    }
50
51    pub(crate) fn parent_digest(&self) -> Digest {
52        Digest(self.0.parent_hash())
53    }
54
55    pub(crate) fn timestamp(&self) -> u64 {
56        self.0.timestamp()
57    }
58}
59
60impl std::ops::Deref for Block {
61    type Target = SealedBlock<tempo_primitives::Block>;
62
63    fn deref(&self) -> &Self::Target {
64        &self.0
65    }
66}
67
68impl Write for Block {
69    fn write(&self, buf: &mut impl BufMut) {
70        use alloy_rlp::Encodable as _;
71        self.0.encode(buf);
72    }
73}
74
75impl Read for Block {
76    // TODO: Figure out what this is for/when to use it. This is () for both alto and summit.
77    type Cfg = ();
78
79    fn read_cfg(buf: &mut impl Buf, _cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
80        // XXX: this does not advance `buf`. Also, it assumes that the rlp
81        // header is fully contained in the first chunk of `buf`. As per
82        // `bytes::Buf::chunk`'s documentation, the first slice should never be
83        // empty is there are remaining bytes. We hence don't worry about edge
84        // cases where the very tiny rlp header is spread over more than one
85        // chunk.
86        let header = alloy_rlp::Header::decode(&mut buf.chunk()).map_err(|rlp_err| {
87            commonware_codec::Error::Wrapped("reading RLP header", rlp_err.into())
88        })?;
89
90        if header.length_with_payload() > buf.remaining() {
91            // TODO: it would be nice to report more information here, but commonware_codex::Error does not
92            // have the fidelity for it (outside abusing Error::Wrapped).
93            return Err(commonware_codec::Error::EndOfBuffer);
94        }
95        let bytes = buf.copy_to_bytes(header.length_with_payload());
96
97        // TODO: decode straight to a reth SealedBlock once released:
98        // https://github.com/paradigmxyz/reth/pull/18003
99        // For now relies on `Decodable for alloy_consensus::Block`.
100        let inner = alloy_rlp::Decodable::decode(&mut bytes.as_ref()).map_err(|rlp_err| {
101            commonware_codec::Error::Wrapped("reading RLP encoded block", rlp_err.into())
102        })?;
103
104        Ok(Self::from_execution_block(inner))
105    }
106}
107
108impl EncodeSize for Block {
109    fn encode_size(&self) -> usize {
110        use alloy_rlp::Encodable as _;
111        self.0.length()
112    }
113}
114
115impl Committable for Block {
116    type Commitment = Digest;
117
118    fn commitment(&self) -> Self::Commitment {
119        self.digest()
120    }
121}
122
123impl Digestible for Block {
124    type Digest = Digest;
125
126    fn digest(&self) -> Self::Digest {
127        self.digest()
128    }
129}
130
131impl Heightable for Block {
132    fn height(&self) -> Height {
133        Height::new(self.0.number())
134    }
135}
136
137impl commonware_consensus::Block for Block {
138    fn parent(&self) -> Digest {
139        self.parent_digest()
140    }
141}
142
143impl commonware_consensus::CertifiableBlock for Block {
144    type Context = Context<Digest, PublicKey>;
145
146    fn context(&self) -> Self::Context {
147        match self.consensus_context {
148            Some(ctx) => Context {
149                leader: ctx.proposer.get().into(),
150                round: Round::new(Epoch::new(ctx.epoch), View::new(ctx.view)),
151                parent: (View::new(ctx.parent_view), self.parent_digest()),
152            },
153            None => {
154                // Returns a deterministic sentinel `Context`.
155                //
156                // Pre-T4: Unused; consensus does not consult this context.
157                // Post-T4: All blocks must carry a `consensus_context`, so reaching
158                // this branch indicates a malformed block. The sentinel intentionally
159                // does not match any real consensus values, so it will fail
160                // verification rather than panic.
161                let leader = PublicKey::from(PrivateKey::from_seed(0));
162                Context {
163                    leader,
164                    round: Round::new(Epoch::new(0), View::new(0)),
165                    parent: (View::new(0), Digest(B256::ZERO)),
166                }
167            }
168        }
169    }
170}
171
172// =======================================================================
173// TODO: Below here are commented out definitions that will be useful when
174// writing an indexer.
175// =======================================================================
176
177// /// A notarized [`Block`].
178// // XXX: Not used right now but will be used once an indexer is implemented.
179// #[derive(Clone, Debug, PartialEq, Eq)]
180// pub(crate) struct Notarized {
181//     proof: Notarization,
182//     block: Block,
183// }
184
185// #[derive(Debug, thiserror::Error)]
186// #[error(
187//     "invalid notarized block: proof proposal `{proposal}` does not match block digest `{digest}`"
188// )]
189// pub(crate) struct NotarizationProofNotForBlock {
190//     proposal: Digest,
191//     digest: Digest,
192// }
193
194// impl Notarized {
195//     /// Constructs a new [`Notarized`] block.
196//     pub(crate) fn try_new(
197//         proof: Notarization,
198//         block: Block,
199//     ) -> Result<Self, NotarizationProofNotForBlock> {
200//         if proof.proposal.payload != block.digest() {
201//             return Err(NotarizationProofNotForBlock {
202//                 proposal: proof.proposal.payload,
203//                 digest: block.digest(),
204//             });
205//         }
206//         Ok(Self { proof, block })
207//     }
208
209//     pub(crate) fn block(&self) -> &Block {
210//         &self.block
211//     }
212
213//     /// Breaks up [`Notarized`] into its constituent parts.
214//     pub(crate) fn into_parts(self) -> (Notarization, Block) {
215//         (self.proof, self.block)
216//     }
217
218//     /// Verifies the notarized block against `namespace` and `identity`.
219//     ///
220//     // XXX: But why does this ignore the block entirely??
221//     pub(crate) fn verify(&self, namespace: &[u8], identity: &BlsPublicKey) -> bool {
222//         self.proof.verify(namespace, identity)
223//     }
224// }
225
226// impl Write for Notarized {
227//     fn write(&self, buf: &mut impl BufMut) {
228//         self.proof.write(buf);
229//         self.block.write(buf);
230//     }
231// }
232
233// impl Read for Notarized {
234//     // XXX: Same Cfg as for Block.
235//     type Cfg = ();
236
237//     fn read_cfg(buf: &mut impl Buf, _cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
238//         // FIXME: wrapping this to give it some context on what exactly failed, but it doesn't feel great.
239//         // Problem is the catch-all `commonware_codex:Error`.
240//         let proof = Notarization::read(buf)
241//             .map_err(|err| commonware_codec::Error::Wrapped("failed to read proof", err.into()))?;
242//         let block = Block::read(buf)
243//             .map_err(|err| commonware_codec::Error::Wrapped("failed to read block", err.into()))?;
244//         Self::try_new(proof, block).map_err(|err| {
245//             commonware_codec::Error::Wrapped("failed constructing notarized block", err.into())
246//         })
247//     }
248// }
249
250// impl EncodeSize for Notarized {
251//     fn encode_size(&self) -> usize {
252//         self.proof.encode_size() + self.block.encode_size()
253//     }
254// }
255
256// /// Used for an indexer.
257// //
258// // XXX: Not used right now but will be used once an indexer is implemented.
259// #[derive(Clone, Debug, PartialEq, Eq)]
260// pub(crate) struct Finalized {
261//     proof: Finalization,
262//     block: Block,
263// }
264
265// #[derive(Debug, thiserror::Error)]
266// #[error(
267//     "invalid finalized block: proof proposal `{proposal}` does not match block digest `{digest}`"
268// )]
269// pub(crate) struct FinalizationProofNotForBlock {
270//     proposal: Digest,
271//     digest: Digest,
272// }
273
274// impl Finalized {
275//     /// Constructs a new [`Finalized`] block.
276//     pub(crate) fn try_new(
277//         proof: Finalization,
278//         block: Block,
279//     ) -> Result<Self, FinalizationProofNotForBlock> {
280//         if proof.proposal.payload != block.digest() {
281//             return Err(FinalizationProofNotForBlock {
282//                 proposal: proof.proposal.payload,
283//                 digest: block.digest(),
284//             });
285//         }
286//         Ok(Self { proof, block })
287//     }
288
289//     pub(crate) fn block(&self) -> &Block {
290//         &self.block
291//     }
292
293//     /// Breaks up [`Finalized`] into its constituent parts.
294//     pub(crate) fn into_parts(self) -> (Finalization, Block) {
295//         (self.proof, self.block)
296//     }
297
298//     /// Verifies the notarized block against `namespace` and `identity`.
299//     ///
300//     // XXX: But why does this ignore the block entirely??
301//     pub(crate) fn verify(&self, namespace: &[u8], identity: &BlsPublicKey) -> bool {
302//         self.proof.verify(namespace, identity)
303//     }
304// }
305
306// impl Write for Finalized {
307//     fn write(&self, buf: &mut impl BufMut) {
308//         self.proof.write(buf);
309//         self.block.write(buf);
310//     }
311// }
312
313// impl Read for Finalized {
314//     // XXX: Same Cfg as for Block.
315//     type Cfg = ();
316
317//     fn read_cfg(buf: &mut impl Buf, _cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
318//         // FIXME: wrapping this to give it some context on what exactly failed, but it doesn't feel great.
319//         // Problem is the catch-all `commonware_codex:Error`.
320//         let proof = Finalization::read(buf)
321//             .map_err(|err| commonware_codec::Error::Wrapped("failed to read proof", err.into()))?;
322//         let block = Block::read(buf)
323//             .map_err(|err| commonware_codec::Error::Wrapped("failed to read block", err.into()))?;
324//         Self::try_new(proof, block).map_err(|err| {
325//             commonware_codec::Error::Wrapped("failed constructing finalized block", err.into())
326//         })
327//     }
328// }
329
330// impl EncodeSize for Finalized {
331//     fn encode_size(&self) -> usize {
332//         self.proof.encode_size() + self.block.encode_size()
333//     }
334// }
335
336#[cfg(test)]
337mod tests {
338    // required unit tests:
339    //
340    // 1. roundtrip block write -> read -> equality
341    // 2. encode size for block.
342    // 3. roundtrip notarized write -> read -> equality
343    // 4. encode size for notarized
344    // 5. roundtrip finalized write -> read -> equality
345    // 6. encode size for finalized
346    //
347    //
348    // desirable snapshot tests:
349    //
350    // 1. block write -> stable hex or rlp representation
351    // 2. block digest -> stable hex
352    // 3. notarized write -> stable hex (necessary? good to guard against commonware xyz changes?)
353    // 4. finalized write -> stable hex (necessary? good to guard against commonware xyz changes?)
354
355    // TODO: Bring back this unit test; preferably with some flavour of tempo reth block.
356    //
357    // use commonware_codec::{Read as _, Write as _};
358    // use reth_chainspec::ChainSpec;
359
360    // use crate::consensus::block::Block;
361
362    // #[test]
363    // fn commonware_write_read_roundtrip() {
364    //     // TODO: should use a non-default chainspec to make the test more interesting.
365    //     let chainspec = ChainSpec::default();
366    //     let expected = Block::genesis_from_chainspec(&chainspec);
367    //     let mut buf = Vec::new();
368    //     expected.write(&mut buf);
369    //     let actual = Block::read_cfg(&mut buf.as_slice(), &()).unwrap();
370    //     assert_eq!(expected, actual);
371    // }
372}