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