Skip to main content

tempo_consensus/consensus/
block.rs

1//! The foundational data structure the Tempo network comes to consensus over.
2//!
3//! The Tempo [`Block`] contains the execution-layer block plus
4//! consensus-layer validation data that is transmitted over commonware p2p.
5
6use 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/// Error returned when a BAL sidecar does not match the execution block header.
32#[derive(Debug, thiserror::Error, PartialEq, Eq)]
33pub(crate) enum BlockAccessListError {
34    /// The header commits to a BAL, but no BAL bytes were provided.
35    #[error("block access list hash {expected} is present but block access list is missing")]
36    Missing { expected: B256 },
37    /// BAL bytes were provided for a block that does not commit to a BAL.
38    #[error("block access list is present but block access list hash is missing")]
39    Unexpected,
40    /// The BAL bytes do not hash to the value committed in the header.
41    #[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/// A Tempo block.
62///
63// XXX: This is a refinement type around a reth [`SealedBlock`]
64// to hold the trait implementations required by commonwarexyz. Uses
65// Sealed because of the frequent accesses to the hash.
66#[derive(Clone, Debug)]
67pub(crate) struct Block(Arc<BlockInner>);
68
69impl Block {
70    /// Creates a block from an execution-layer block and optional BAL.
71    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    /// Creates a block and seeds the cached execution-layer RLP size.
87    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    /// Creates a block without checking that BAL bytes match the header.
101    ///
102    /// This is for reconstructing blocks from persisted EL data that does not include
103    /// commonware sidecars. Callers must not encode or broadcast a block whose header
104    /// commits to a BAL unless the corresponding BAL bytes have been restored.
105    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    /// Consumes the block and returns only the execution-layer block.
121    pub(crate) fn into_inner(self) -> SealedBlock<tempo_primitives::Block> {
122        self.into_inner_parts().execution_block
123    }
124
125    /// Consumes the block and returns the execution-layer block plus optional BAL.
126    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    /// Returns the (eth) hash of the wrapped block.
146    pub(crate) fn block_hash(&self) -> B256 {
147        self.0.execution_block.hash()
148    }
149
150    /// Returns the hash of the wrapped block as a commonware [`Digest`].
151    pub(crate) fn digest(&self) -> Digest {
152        Digest(self.0.execution_block.hash())
153    }
154
155    /// Returns the parent hash of the wrapped block as a commonware [`Digest`].
156    pub(crate) fn parent_digest(&self) -> Digest {
157        Digest(self.0.execution_block.parent_hash())
158    }
159
160    /// Returns the timestamp of the wrapped block.
161    pub(crate) fn timestamp(&self) -> u64 {
162        self.0.execution_block.timestamp()
163    }
164
165    /// Returns the wrapped block.
166    pub(crate) fn block(&self) -> &SealedBlock<tempo_primitives::Block> {
167        &self.0.execution_block
168    }
169
170    /// Returns the block access list of the wrapped block.
171    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            // FIXME: Blocks reconstructed from persisted EL data can carry a BAL hash
224            // without the commonware BAL sidecar. Encoding one will panic here, which
225            // can crash follower nodes and validators that request blocks over p2p.
226            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    // TODO: Figure out what this is for/when to use it. This is () for both alto and summit.
238    type Cfg = ();
239
240    fn read_cfg(buf: &mut impl Buf, _cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
241        // XXX: this does not advance `buf`. Also, it assumes that the rlp
242        // header is fully contained in the first chunk of `buf`. As per
243        // `bytes::Buf::chunk`'s documentation, the first slice should never be
244        // empty is there are remaining bytes. We hence don't worry about edge
245        // cases where the very tiny rlp header is spread over more than one
246        // chunk.
247        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            // TODO: it would be nice to report more information here, but commonware_codex::Error does not
253            // have the fidelity for it (outside abusing Error::Wrapped).
254            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                // Returns a deterministic sentinel `Context`.
358                //
359                // All consensus-produced blocks must carry a `consensus_context`, so
360                // reaching this branch indicates a malformed block. The sentinel
361                // intentionally does not match any real consensus values, so it will
362                // fail verification rather than panic.
363                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/// Inner block data shared by cheap [`Block`] clones.
380#[derive(Debug)]
381struct BlockInner {
382    /// The execution-layer block.
383    execution_block: SealedBlock<tempo_primitives::Block>,
384    /// Cached execution-layer RLP size when it is already known by the caller.
385    execution_block_encoded_size: OnceLock<usize>,
386    /// Optional block access list. Only provided if the network supports BALs.
387    #[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// =======================================================================
427// TODO: Below here are commented out definitions that will be useful when
428// writing an indexer.
429// =======================================================================
430
431// /// A notarized [`Block`].
432// // XXX: Not used right now but will be used once an indexer is implemented.
433// #[derive(Clone, Debug, PartialEq, Eq)]
434// pub(crate) struct Notarized {
435//     proof: Notarization,
436//     block: Block,
437// }
438
439// #[derive(Debug, thiserror::Error)]
440// #[error(
441//     "invalid notarized block: proof proposal `{proposal}` does not match block digest `{digest}`"
442// )]
443// pub(crate) struct NotarizationProofNotForBlock {
444//     proposal: Digest,
445//     digest: Digest,
446// }
447
448// impl Notarized {
449//     /// Constructs a new [`Notarized`] block.
450//     pub(crate) fn try_new(
451//         proof: Notarization,
452//         block: Block,
453//     ) -> Result<Self, NotarizationProofNotForBlock> {
454//         if proof.proposal.payload != block.digest() {
455//             return Err(NotarizationProofNotForBlock {
456//                 proposal: proof.proposal.payload,
457//                 digest: block.digest(),
458//             });
459//         }
460//         Ok(Self { proof, block })
461//     }
462
463//     pub(crate) fn block(&self) -> &Block {
464//         &self.block
465//     }
466
467//     /// Breaks up [`Notarized`] into its constituent parts.
468//     pub(crate) fn into_parts(self) -> (Notarization, Block) {
469//         (self.proof, self.block)
470//     }
471
472//     /// Verifies the notarized block against `namespace` and `identity`.
473//     ///
474//     // XXX: But why does this ignore the block entirely??
475//     pub(crate) fn verify(&self, namespace: &[u8], identity: &BlsPublicKey) -> bool {
476//         self.proof.verify(namespace, identity)
477//     }
478// }
479
480// impl Write for Notarized {
481//     fn write(&self, buf: &mut impl BufMut) {
482//         self.proof.write(buf);
483//         self.block.write(buf);
484//     }
485// }
486
487// impl Read for Notarized {
488//     // XXX: Same Cfg as for Block.
489//     type Cfg = ();
490
491//     fn read_cfg(buf: &mut impl Buf, _cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
492//         // FIXME: wrapping this to give it some context on what exactly failed, but it doesn't feel great.
493//         // Problem is the catch-all `commonware_codex:Error`.
494//         let proof = Notarization::read(buf)
495//             .map_err(|err| commonware_codec::Error::Wrapped("failed to read proof", err.into()))?;
496//         let block = Block::read(buf)
497//             .map_err(|err| commonware_codec::Error::Wrapped("failed to read block", err.into()))?;
498//         Self::try_new(proof, block).map_err(|err| {
499//             commonware_codec::Error::Wrapped("failed constructing notarized block", err.into())
500//         })
501//     }
502// }
503
504// impl EncodeSize for Notarized {
505//     fn encode_size(&self) -> usize {
506//         self.proof.encode_size() + self.block.encode_size()
507//     }
508// }
509
510// /// Used for an indexer.
511// //
512// // XXX: Not used right now but will be used once an indexer is implemented.
513// #[derive(Clone, Debug, PartialEq, Eq)]
514// pub(crate) struct Finalized {
515//     proof: Finalization,
516//     block: Block,
517// }
518
519// #[derive(Debug, thiserror::Error)]
520// #[error(
521//     "invalid finalized block: proof proposal `{proposal}` does not match block digest `{digest}`"
522// )]
523// pub(crate) struct FinalizationProofNotForBlock {
524//     proposal: Digest,
525//     digest: Digest,
526// }
527
528// impl Finalized {
529//     /// Constructs a new [`Finalized`] block.
530//     pub(crate) fn try_new(
531//         proof: Finalization,
532//         block: Block,
533//     ) -> Result<Self, FinalizationProofNotForBlock> {
534//         if proof.proposal.payload != block.digest() {
535//             return Err(FinalizationProofNotForBlock {
536//                 proposal: proof.proposal.payload,
537//                 digest: block.digest(),
538//             });
539//         }
540//         Ok(Self { proof, block })
541//     }
542
543//     pub(crate) fn block(&self) -> &Block {
544//         &self.block
545//     }
546
547//     /// Breaks up [`Finalized`] into its constituent parts.
548//     pub(crate) fn into_parts(self) -> (Finalization, Block) {
549//         (self.proof, self.block)
550//     }
551
552//     /// Verifies the notarized block against `namespace` and `identity`.
553//     ///
554//     // XXX: But why does this ignore the block entirely??
555//     pub(crate) fn verify(&self, namespace: &[u8], identity: &BlsPublicKey) -> bool {
556//         self.proof.verify(namespace, identity)
557//     }
558// }
559
560// impl Write for Finalized {
561//     fn write(&self, buf: &mut impl BufMut) {
562//         self.proof.write(buf);
563//         self.block.write(buf);
564//     }
565// }
566
567// impl Read for Finalized {
568//     // XXX: Same Cfg as for Block.
569//     type Cfg = ();
570
571//     fn read_cfg(buf: &mut impl Buf, _cfg: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
572//         // FIXME: wrapping this to give it some context on what exactly failed, but it doesn't feel great.
573//         // Problem is the catch-all `commonware_codex:Error`.
574//         let proof = Finalization::read(buf)
575//             .map_err(|err| commonware_codec::Error::Wrapped("failed to read proof", err.into()))?;
576//         let block = Block::read(buf)
577//             .map_err(|err| commonware_codec::Error::Wrapped("failed to read block", err.into()))?;
578//         Self::try_new(proof, block).map_err(|err| {
579//             commonware_codec::Error::Wrapped("failed constructing finalized block", err.into())
580//         })
581//     }
582// }
583
584// impl EncodeSize for Finalized {
585//     fn encode_size(&self) -> usize {
586//         self.proof.encode_size() + self.block.encode_size()
587//     }
588// }
589
590#[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    // required unit tests:
627    //
628    // 1. roundtrip block write -> read -> equality
629    // 2. encode size for block.
630    // 3. roundtrip notarized write -> read -> equality
631    // 4. encode size for notarized
632    // 5. roundtrip finalized write -> read -> equality
633    // 6. encode size for finalized
634    //
635    //
636    // desirable snapshot tests:
637    //
638    // 1. block write -> stable hex or rlp representation
639    // 2. block digest -> stable hex
640    // 3. notarized write -> stable hex (necessary? good to guard against commonware xyz changes?)
641    // 4. finalized write -> stable hex (necessary? good to guard against commonware xyz changes?)
642
643    #[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}