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}