tempo_consensus/consensus/
block.rs1use alloy_consensus::BlockHeader as _;
7use alloy_primitives::{B256, Bytes, keccak256};
8use alloy_rlp::Encodable as _;
9use bytes::{Buf, BufMut};
10#[cfg(feature = "bal")]
11use commonware_codec::RangeCfg;
12use commonware_codec::{EncodeSize, Read, Write};
13use commonware_consensus::{
14 Heightable,
15 simplex::types::Context,
16 types::{Epoch, Height, Round, View},
17};
18use commonware_cryptography::{
19 Committable, Digestible, Signer as _,
20 ed25519::{PrivateKey, PublicKey},
21};
22use reth_node_core::primitives::SealedBlock;
23use std::{
24 fmt::Display,
25 sync::{Arc, OnceLock},
26};
27use tracing::warn;
28
29use crate::consensus::Digest;
30
31#[derive(Debug, thiserror::Error, PartialEq, Eq)]
33pub(crate) enum BlockAccessListError {
34 #[error("block access list hash {expected} is present but block access list is missing")]
36 Missing { expected: B256 },
37 #[error("block access list is present but block access list hash is missing")]
39 Unexpected,
40 #[error("block access list hash mismatch: expected {expected}, got {actual}")]
42 HashMismatch { expected: B256, actual: B256 },
43}
44
45impl BlockAccessListError {
46 fn codec_error(self) -> commonware_codec::Error {
47 match self {
48 Self::Missing { .. } => {
49 commonware_codec::Error::Invalid("block access list", "missing for header hash")
50 }
51 Self::Unexpected => {
52 commonware_codec::Error::Invalid("block access list", "present without header hash")
53 }
54 Self::HashMismatch { .. } => {
55 commonware_codec::Error::Invalid("block access list", "hash does not match header")
56 }
57 }
58 }
59}
60
61#[derive(Clone, Debug)]
67pub(crate) struct Block(Arc<BlockInner>);
68
69impl Block {
70 pub(crate) fn from_execution_block(
72 execution_block: SealedBlock<tempo_primitives::Block>,
73 block_access_list: Option<Bytes>,
74 ) -> Result<Self, BlockAccessListError> {
75 validate_block_access_list_hash(
76 execution_block.block_access_list_hash(),
77 block_access_list.as_ref(),
78 )?;
79
80 Ok(Self::from_execution_block_unchecked(
81 execution_block,
82 block_access_list,
83 ))
84 }
85
86 pub(crate) fn from_execution_block_with_encoded_size(
88 execution_block: SealedBlock<tempo_primitives::Block>,
89 block_access_list: Option<Bytes>,
90 execution_block_encoded_size: usize,
91 ) -> Result<Self, BlockAccessListError> {
92 let block = Self::from_execution_block(execution_block, block_access_list)?;
93 let _ = block
94 .0
95 .execution_block_encoded_size
96 .set(execution_block_encoded_size);
97 Ok(block)
98 }
99
100 pub(crate) fn from_execution_block_unchecked(
106 execution_block: SealedBlock<tempo_primitives::Block>,
107 block_access_list: Option<Bytes>,
108 ) -> Self {
109 #[cfg(not(feature = "bal"))]
110 let _ = block_access_list;
111
112 Self(Arc::new(BlockInner {
113 execution_block,
114 execution_block_encoded_size: OnceLock::new(),
115 #[cfg(feature = "bal")]
116 block_access_list,
117 }))
118 }
119
120 pub(crate) fn into_inner(self) -> SealedBlock<tempo_primitives::Block> {
122 self.into_inner_parts().execution_block
123 }
124
125 pub(crate) fn into_parts(self) -> (SealedBlock<tempo_primitives::Block>, Option<Bytes>) {
127 let inner = self.into_inner_parts();
128 (
129 inner.execution_block,
130 #[cfg(feature = "bal")]
131 {
132 inner.block_access_list
133 },
134 #[cfg(not(feature = "bal"))]
135 {
136 None
137 },
138 )
139 }
140
141 fn into_inner_parts(self) -> BlockInner {
142 Arc::try_unwrap(self.0).unwrap_or_else(|inner| (*inner).clone())
143 }
144
145 pub(crate) fn block_hash(&self) -> B256 {
147 self.0.execution_block.hash()
148 }
149
150 pub(crate) fn digest(&self) -> Digest {
152 Digest(self.0.execution_block.hash())
153 }
154
155 pub(crate) fn parent_digest(&self) -> Digest {
157 Digest(self.0.execution_block.parent_hash())
158 }
159
160 pub(crate) fn timestamp(&self) -> u64 {
162 self.0.execution_block.timestamp()
163 }
164
165 pub(crate) fn block(&self) -> &SealedBlock<tempo_primitives::Block> {
167 &self.0.execution_block
168 }
169
170 pub(crate) fn block_access_list(&self) -> Option<&Bytes> {
172 #[cfg(feature = "bal")]
173 {
174 self.0.block_access_list.as_ref()
175 }
176 #[cfg(not(feature = "bal"))]
177 {
178 None
179 }
180 }
181}
182
183impl Display for Block {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 f.write_fmt(format_args!(
186 "digest: {}, height: {}",
187 self.digest(),
188 self.height()
189 ))
190 }
191}
192
193impl PartialEq for Block {
194 fn eq(&self, other: &Self) -> bool {
195 self.0.execution_block == other.0.execution_block && {
196 #[cfg(feature = "bal")]
197 {
198 self.0.block_access_list == other.0.block_access_list
199 }
200 #[cfg(not(feature = "bal"))]
201 {
202 true
203 }
204 }
205 }
206}
207
208impl Eq for Block {}
209
210impl std::ops::Deref for Block {
211 type Target = SealedBlock<tempo_primitives::Block>;
212
213 fn deref(&self) -> &Self::Target {
214 &self.0.execution_block
215 }
216}
217
218impl Write for Block {
219 fn write(&self, buf: &mut impl BufMut) {
220 self.0.execution_block.encode(buf);
221 #[cfg(feature = "bal")]
222 if self.0.execution_block.block_access_list_hash().is_some() {
223 let block_access_list = self
227 .0
228 .block_access_list
229 .as_ref()
230 .expect("BAL bytes must be present when header contains a BAL hash");
231 block_access_list.write(buf);
232 }
233 }
234}
235
236impl Read for Block {
237 type Cfg = ();
239
240 fn read_cfg(buf: &mut impl Buf, _cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
241 let header = alloy_rlp::Header::decode(&mut buf.chunk()).map_err(|rlp_err| {
248 commonware_codec::Error::Wrapped("reading RLP header", rlp_err.into())
249 })?;
250
251 if header.length_with_payload() > buf.remaining() {
252 return Err(commonware_codec::Error::EndOfBuffer);
255 }
256 let execution_block_encoded_size = header.length_with_payload();
257 let bytes = buf.copy_to_bytes(execution_block_encoded_size);
258
259 let inner = <tempo_primitives::Block as reth_primitives_traits::Block>::decode_sealed(
260 &mut bytes.as_ref(),
261 )
262 .map_err(|rlp_err| {
263 commonware_codec::Error::Wrapped("reading RLP encoded block", rlp_err.into())
264 })?;
265
266 #[cfg(feature = "bal")]
267 let block_access_list = {
268 if inner.block_access_list_hash().is_some() {
269 let block_access_list: Bytes = bytes::Bytes::read_cfg(buf, &RangeCfg::from(..))
270 .map_err(|err| {
271 commonware_codec::Error::Wrapped("reading block access list", err.into())
272 })?
273 .into();
274 Some(block_access_list)
275 } else {
276 None
277 }
278 };
279 #[cfg(not(feature = "bal"))]
280 let block_access_list = None;
281
282 Self::from_execution_block_with_encoded_size(
283 inner,
284 block_access_list,
285 execution_block_encoded_size,
286 )
287 .map_err(|err| err.codec_error())
288 }
289}
290
291impl EncodeSize for Block {
292 fn encode_size(&self) -> usize {
293 let execution_block_size = *self
294 .0
295 .execution_block_encoded_size
296 .get_or_init(|| self.0.execution_block.length());
297
298 #[cfg(feature = "bal")]
299 {
300 execution_block_size
301 + if self.0.execution_block.block_access_list_hash().is_some() {
302 self.0
303 .block_access_list
304 .as_ref()
305 .expect("BAL bytes must be present when header contains a BAL hash")
306 .encode_size()
307 } else {
308 0
309 }
310 }
311 #[cfg(not(feature = "bal"))]
312 {
313 execution_block_size
314 }
315 }
316}
317
318impl Committable for Block {
319 type Commitment = Digest;
320
321 fn commitment(&self) -> Self::Commitment {
322 self.digest()
323 }
324}
325
326impl Digestible for Block {
327 type Digest = Digest;
328
329 fn digest(&self) -> Self::Digest {
330 self.digest()
331 }
332}
333
334impl Heightable for Block {
335 fn height(&self) -> Height {
336 Height::new(self.0.execution_block.number())
337 }
338}
339
340impl commonware_consensus::Block for Block {
341 fn parent(&self) -> Digest {
342 self.parent_digest()
343 }
344}
345
346impl commonware_consensus::CertifiableBlock for Block {
347 type Context = Context<Digest, PublicKey>;
348
349 fn context(&self) -> Self::Context {
350 match self.consensus_context {
351 Some(ctx) => Context {
352 leader: ctx.proposer.get().into(),
353 round: Round::new(Epoch::new(ctx.epoch), View::new(ctx.view)),
354 parent: (View::new(ctx.parent_view), self.parent_digest()),
355 },
356 None => {
357 warn!(
364 "context request for block `{}` with no consensus context",
365 self.digest()
366 );
367
368 let leader = PublicKey::from(PrivateKey::from_seed(0));
369 Context {
370 leader,
371 round: Round::new(Epoch::new(0), View::new(0)),
372 parent: (View::new(0), Digest(B256::ZERO)),
373 }
374 }
375 }
376 }
377}
378
379#[derive(Debug)]
381struct BlockInner {
382 execution_block: SealedBlock<tempo_primitives::Block>,
384 execution_block_encoded_size: OnceLock<usize>,
386 #[cfg(feature = "bal")]
388 block_access_list: Option<Bytes>,
389}
390
391impl Clone for BlockInner {
392 fn clone(&self) -> Self {
393 let execution_block_encoded_size = OnceLock::new();
394 if let Some(size) = self.execution_block_encoded_size.get() {
395 let _ = execution_block_encoded_size.set(*size);
396 }
397
398 Self {
399 execution_block: self.execution_block.clone(),
400 execution_block_encoded_size,
401 #[cfg(feature = "bal")]
402 block_access_list: self.block_access_list.clone(),
403 }
404 }
405}
406
407fn validate_block_access_list_hash(
408 expected: Option<B256>,
409 block_access_list: Option<&Bytes>,
410) -> Result<(), BlockAccessListError> {
411 match (expected, block_access_list) {
412 (Some(expected), Some(block_access_list)) => {
413 let actual = keccak256(block_access_list.as_ref());
414 if actual == expected {
415 Ok(())
416 } else {
417 Err(BlockAccessListError::HashMismatch { expected, actual })
418 }
419 }
420 (Some(expected), None) => Err(BlockAccessListError::Missing { expected }),
421 (None, Some(_)) => Err(BlockAccessListError::Unexpected),
422 (None, None) => Ok(()),
423 }
424}
425
426#[cfg(test)]
591mod tests {
592 #[cfg(feature = "bal")]
593 use alloy_consensus::BlockHeader as _;
594 use alloy_primitives::{B256, bytes, keccak256};
595 #[cfg(not(feature = "bal"))]
596 use commonware_codec::Write as _;
597 use commonware_codec::{Encode, Read as _};
598 use reth_node_core::primitives::SealedBlock;
599 use tempo_primitives::{Block as TempoBlock, TempoHeader};
600
601 use super::Block;
602 #[cfg(feature = "bal")]
603 use super::BlockAccessListError;
604
605 fn execution_block_with_block_access_list_hash(
606 block_access_list_hash: B256,
607 ) -> SealedBlock<TempoBlock> {
608 SealedBlock::seal_slow(TempoBlock {
609 header: TempoHeader {
610 inner: alloy_consensus::Header {
611 base_fee_per_gas: Some(0),
612 withdrawals_root: Some(B256::ZERO),
613 blob_gas_used: Some(0),
614 excess_blob_gas: Some(0),
615 parent_beacon_block_root: Some(B256::ZERO),
616 requests_hash: Some(B256::ZERO),
617 block_access_list_hash: Some(block_access_list_hash),
618 ..Default::default()
619 },
620 ..Default::default()
621 },
622 body: Default::default(),
623 })
624 }
625
626 #[test]
644 fn reads_block_without_block_access_list_bytes() {
645 let execution_block = SealedBlock::seal_slow(TempoBlock {
646 header: TempoHeader {
647 inner: alloy_consensus::Header {
648 number: 42,
649 gas_limit: 30_000_000,
650 timestamp: 1_700_000_000,
651 base_fee_per_gas: Some(1_000_000_000),
652 withdrawals_root: Some(B256::ZERO),
653 blob_gas_used: Some(0),
654 excess_blob_gas: Some(0),
655 parent_beacon_block_root: Some(B256::ZERO),
656 requests_hash: Some(B256::ZERO),
657 ..Default::default()
658 },
659 ..Default::default()
660 },
661 body: Default::default(),
662 });
663 let expected = Block::from_execution_block(execution_block.clone(), None)
664 .expect("block has no BAL side data");
665 let mut block_bytes = Vec::new();
666 alloy_rlp::Encodable::encode(&execution_block, &mut block_bytes);
667
668 let decoded = Block::read_cfg(&mut block_bytes.as_ref(), &()).unwrap();
669 assert_eq!(decoded, expected);
670 assert!(decoded.block_access_list().is_none());
671
672 let encoded = decoded.encode();
673
674 assert_eq!(encoded.as_ref(), block_bytes.as_slice());
675 }
676
677 #[cfg(not(feature = "bal"))]
678 #[test]
679 fn read_rejects_block_access_list_hash_when_bal_feature_disabled() {
680 let block_access_list = bytes!("0xc0");
681 let execution_block =
682 execution_block_with_block_access_list_hash(keccak256(block_access_list.as_ref()));
683 let mut encoded = Vec::new();
684 alloy_rlp::Encodable::encode(&execution_block, &mut encoded);
685 block_access_list.write(&mut encoded);
686
687 let err = Block::read_cfg(&mut encoded.as_ref(), &()).unwrap_err();
688
689 assert!(matches!(
690 err,
691 commonware_codec::Error::Invalid("block access list", "missing for header hash")
692 ));
693 }
694
695 #[cfg(feature = "bal")]
696 #[test]
697 fn rejects_block_access_list_without_header_hash() {
698 let execution_block = SealedBlock::seal_slow(TempoBlock {
699 header: TempoHeader::default(),
700 body: Default::default(),
701 });
702 assert!(execution_block.block_access_list_hash().is_none());
703
704 let block_access_list = bytes!("0xc0");
705 let err =
706 Block::from_execution_block(execution_block, Some(block_access_list)).unwrap_err();
707
708 assert_eq!(err, BlockAccessListError::Unexpected);
709 }
710
711 #[cfg(feature = "bal")]
712 #[test]
713 fn rejects_missing_block_access_list_with_header_hash() {
714 let execution_block = execution_block_with_block_access_list_hash(B256::ZERO);
715 let err = Block::from_execution_block(execution_block, None).unwrap_err();
716
717 assert_eq!(
718 err,
719 BlockAccessListError::Missing {
720 expected: B256::ZERO
721 }
722 );
723 }
724
725 #[cfg(feature = "bal")]
726 #[test]
727 fn reads_wraps_missing_block_access_list_error() {
728 let execution_block = execution_block_with_block_access_list_hash(B256::ZERO);
729 let mut encoded = Vec::new();
730 alloy_rlp::Encodable::encode(&execution_block, &mut encoded);
731
732 let err = Block::read_cfg(&mut encoded.as_ref(), &()).unwrap_err();
733
734 assert!(matches!(
735 err,
736 commonware_codec::Error::Wrapped("reading block access list", _)
737 ));
738 }
739
740 #[cfg(feature = "bal")]
741 #[test]
742 fn roundtrips_block_access_list_with_matching_header_hash() {
743 let block_access_list = bytes!("0xc0");
744 let execution_block =
745 execution_block_with_block_access_list_hash(keccak256(block_access_list.as_ref()));
746 let block =
747 Block::from_execution_block(execution_block, Some(block_access_list.clone())).unwrap();
748
749 let encoded = block.encode();
750 let decoded = Block::read_cfg(&mut encoded.as_ref(), &()).unwrap();
751
752 assert_eq!(decoded, block);
753 assert_eq!(
754 decoded.block_access_list().map(|bytes| bytes.as_ref()),
755 Some(block_access_list.as_ref())
756 );
757 }
758
759 #[cfg(feature = "bal")]
760 #[test]
761 fn rejects_block_access_list_with_mismatched_header_hash() {
762 let block_access_list = bytes!("0xc0");
763 let execution_block = execution_block_with_block_access_list_hash(B256::ZERO);
764 let err =
765 Block::from_execution_block(execution_block, Some(block_access_list)).unwrap_err();
766
767 assert_eq!(
768 err,
769 BlockAccessListError::HashMismatch {
770 expected: B256::ZERO,
771 actual: keccak256(bytes!("0xc0").as_ref())
772 }
773 );
774 }
775
776 #[cfg(feature = "bal")]
777 #[test]
778 fn reads_reject_block_access_list_with_mismatched_header_hash() {
779 let block_access_list = bytes!("0xc0");
780 let execution_block = execution_block_with_block_access_list_hash(B256::ZERO);
781 let block = Block::from_execution_block_unchecked(execution_block, Some(block_access_list));
782 let encoded = block.encode();
783 let err = Block::read_cfg(&mut encoded.as_ref(), &()).unwrap_err();
784
785 assert!(matches!(
786 err,
787 commonware_codec::Error::Invalid("block access list", "hash does not match header")
788 ));
789 }
790}