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