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}