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}