kona_protocol/
block.rs

1//! Block Types for Optimism.
2
3use crate::{DecodeError, L1BlockInfoTx};
4use alloc::vec::Vec;
5use alloy_consensus::{Block, Transaction, Typed2718};
6use alloy_eips::{BlockNumHash, eip2718::Eip2718Error, eip7685::EMPTY_REQUESTS_HASH};
7use alloy_primitives::B256;
8use alloy_rpc_types_engine::{CancunPayloadFields, PraguePayloadFields};
9use alloy_rpc_types_eth::Block as RpcBlock;
10use derive_more::Display;
11use kona_genesis::ChainGenesis;
12use op_alloy_consensus::{OpBlock, OpTxEnvelope};
13use op_alloy_rpc_types_engine::{OpExecutionPayload, OpExecutionPayloadSidecar, OpPayloadError};
14
15/// Block Header Info
16#[derive(Debug, Clone, Display, Copy, Eq, Hash, PartialEq, Default)]
17#[display(
18    "BlockInfo {{ hash: {hash}, number: {number}, parent_hash: {parent_hash}, timestamp: {timestamp} }}"
19)]
20#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
23pub struct BlockInfo {
24    /// The block hash
25    pub hash: B256,
26    /// The block number
27    pub number: u64,
28    /// The parent block hash
29    pub parent_hash: B256,
30    /// The block timestamp
31    pub timestamp: u64,
32}
33
34impl BlockInfo {
35    /// Instantiates a new [`BlockInfo`].
36    pub const fn new(hash: B256, number: u64, parent_hash: B256, timestamp: u64) -> Self {
37        Self { hash, number, parent_hash, timestamp }
38    }
39
40    /// Returns the block ID.
41    pub const fn id(&self) -> BlockNumHash {
42        BlockNumHash { hash: self.hash, number: self.number }
43    }
44
45    /// Returns `true` if this [`BlockInfo`] is the direct parent of the given block.
46    pub fn is_parent_of(&self, block: &Self) -> bool {
47        self.number + 1 == block.number && self.hash == block.parent_hash
48    }
49}
50
51impl<T> From<Block<T>> for BlockInfo {
52    fn from(block: Block<T>) -> Self {
53        Self::from(&block)
54    }
55}
56
57impl<T> From<&Block<T>> for BlockInfo {
58    fn from(block: &Block<T>) -> Self {
59        Self {
60            hash: block.header.hash_slow(),
61            number: block.header.number,
62            parent_hash: block.header.parent_hash,
63            timestamp: block.header.timestamp,
64        }
65    }
66}
67
68impl<T> From<RpcBlock<T>> for BlockInfo {
69    fn from(block: RpcBlock<T>) -> Self {
70        Self {
71            hash: block.header.hash_slow(),
72            number: block.header.number,
73            parent_hash: block.header.parent_hash,
74            timestamp: block.header.timestamp,
75        }
76    }
77}
78
79impl<T> From<&RpcBlock<T>> for BlockInfo {
80    fn from(block: &RpcBlock<T>) -> Self {
81        Self {
82            hash: block.header.hash_slow(),
83            number: block.header.number,
84            parent_hash: block.header.parent_hash,
85            timestamp: block.header.timestamp,
86        }
87    }
88}
89
90/// L2 Block Header Info
91#[derive(Debug, Display, Clone, Copy, Hash, Eq, PartialEq, Default)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
93#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
94#[display(
95    "L2BlockInfo {{ block_info: {block_info}, l1_origin: {l1_origin:?}, seq_num: {seq_num} }}"
96)]
97pub struct L2BlockInfo {
98    /// The base [`BlockInfo`]
99    #[cfg_attr(feature = "serde", serde(flatten))]
100    pub block_info: BlockInfo,
101    /// The L1 origin [`BlockNumHash`]
102    #[cfg_attr(feature = "serde", serde(rename = "l1origin", alias = "l1Origin"))]
103    pub l1_origin: BlockNumHash,
104    /// The sequence number of the L2 block
105    #[cfg_attr(feature = "serde", serde(rename = "sequenceNumber", alias = "seqNum"))]
106    pub seq_num: u64,
107}
108
109impl L2BlockInfo {
110    /// Returns the block hash.
111    pub const fn hash(&self) -> B256 {
112        self.block_info.hash
113    }
114}
115
116#[cfg(feature = "arbitrary")]
117impl arbitrary::Arbitrary<'_> for L2BlockInfo {
118    fn arbitrary(g: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
119        Ok(Self {
120            block_info: g.arbitrary()?,
121            l1_origin: BlockNumHash { number: g.arbitrary()?, hash: g.arbitrary()? },
122            seq_num: g.arbitrary()?,
123        })
124    }
125}
126
127/// An error that can occur when converting an OP [`Block`] to [`L2BlockInfo`].
128#[derive(Debug, thiserror::Error)]
129pub enum FromBlockError {
130    /// The genesis block hash does not match the expected value.
131    #[error("Invalid genesis hash")]
132    InvalidGenesisHash,
133    /// The L2 block is missing the L1 info deposit transaction.
134    #[error("L2 block is missing L1 info deposit transaction ({0})")]
135    MissingL1InfoDeposit(B256),
136    /// The first payload transaction has an unexpected type.
137    #[error("First payload transaction has unexpected type: {0}")]
138    UnexpectedTxType(u8),
139    /// Failed to decode the first transaction into an OP transaction.
140    #[error("Failed to decode the first transaction into an OP transaction: {0}")]
141    TxEnvelopeDecodeError(Eip2718Error),
142    /// The first payload transaction is not a deposit transaction.
143    #[error("First payload transaction is not a deposit transaction, type: {0}")]
144    FirstTxNonDeposit(u8),
145    /// Failed to decode the [`L1BlockInfoTx`] from the deposit transaction.
146    #[error("Failed to decode the L1BlockInfoTx from the deposit transaction: {0}")]
147    BlockInfoDecodeError(#[from] DecodeError),
148    /// Failed to convert [`OpExecutionPayload`] to [`OpBlock`].
149    #[error(transparent)]
150    OpPayload(#[from] OpPayloadError),
151}
152
153impl PartialEq<Self> for FromBlockError {
154    fn eq(&self, other: &Self) -> bool {
155        match (self, other) {
156            (Self::InvalidGenesisHash, Self::InvalidGenesisHash) => true,
157            (Self::MissingL1InfoDeposit(a), Self::MissingL1InfoDeposit(b)) => a == b,
158            (Self::UnexpectedTxType(a), Self::UnexpectedTxType(b)) => a == b,
159            (Self::TxEnvelopeDecodeError(_), Self::TxEnvelopeDecodeError(_)) => true,
160            (Self::FirstTxNonDeposit(a), Self::FirstTxNonDeposit(b)) => a == b,
161            (Self::BlockInfoDecodeError(a), Self::BlockInfoDecodeError(b)) => a == b,
162            _ => false,
163        }
164    }
165}
166
167impl From<Eip2718Error> for FromBlockError {
168    fn from(value: Eip2718Error) -> Self {
169        Self::TxEnvelopeDecodeError(value)
170    }
171}
172
173impl L2BlockInfo {
174    /// Instantiates a new [`L2BlockInfo`].
175    pub const fn new(block_info: BlockInfo, l1_origin: BlockNumHash, seq_num: u64) -> Self {
176        Self { block_info, l1_origin, seq_num }
177    }
178
179    /// Constructs an [`L2BlockInfo`] from a given OP [`Block`] and [`ChainGenesis`].
180    pub fn from_block_and_genesis<T: Typed2718 + AsRef<OpTxEnvelope>>(
181        block: &Block<T>,
182        genesis: &ChainGenesis,
183    ) -> Result<Self, FromBlockError> {
184        let block_info = BlockInfo::from(block);
185
186        let (l1_origin, sequence_number) = if block_info.number == genesis.l2.number {
187            if block_info.hash != genesis.l2.hash {
188                return Err(FromBlockError::InvalidGenesisHash);
189            }
190            (genesis.l1, 0)
191        } else {
192            if block.body.transactions.is_empty() {
193                return Err(FromBlockError::MissingL1InfoDeposit(block_info.hash));
194            }
195
196            let tx = block.body.transactions[0].as_ref();
197            let Some(tx) = tx.as_deposit() else {
198                return Err(FromBlockError::FirstTxNonDeposit(tx.ty()));
199            };
200
201            let l1_info = L1BlockInfoTx::decode_calldata(tx.input().as_ref())
202                .map_err(FromBlockError::BlockInfoDecodeError)?;
203            (l1_info.id(), l1_info.sequence_number())
204        };
205
206        Ok(Self { block_info, l1_origin, seq_num: sequence_number })
207    }
208
209    /// Constructs an [`L2BlockInfo`] From a given [`OpExecutionPayload`] and [`ChainGenesis`].
210    pub fn from_payload_and_genesis(
211        payload: OpExecutionPayload,
212        parent_beacon_block_root: Option<B256>,
213        genesis: &ChainGenesis,
214    ) -> Result<Self, FromBlockError> {
215        let block: OpBlock = match payload {
216            OpExecutionPayload::V4(_) => {
217                let sidecar = OpExecutionPayloadSidecar::v4(
218                    CancunPayloadFields::new(
219                        parent_beacon_block_root.unwrap_or_default(),
220                        Vec::new(),
221                    ),
222                    PraguePayloadFields::new(EMPTY_REQUESTS_HASH),
223                );
224                payload.try_into_block_with_sidecar(&sidecar)?
225            }
226            OpExecutionPayload::V3(_) => {
227                let sidecar = OpExecutionPayloadSidecar::v3(CancunPayloadFields::new(
228                    parent_beacon_block_root.unwrap_or_default(),
229                    Vec::new(),
230                ));
231                payload.try_into_block_with_sidecar(&sidecar)?
232            }
233            _ => payload.try_into_block()?,
234        };
235        Self::from_block_and_genesis(&block, genesis)
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use alloc::string::ToString;
243    use alloy_consensus::{Header, TxEnvelope};
244    use alloy_primitives::b256;
245    use op_alloy_consensus::OpBlock;
246
247    #[test]
248    fn test_rpc_block_into_info() {
249        let block: alloy_rpc_types_eth::Block<OpTxEnvelope> = alloy_rpc_types_eth::Block {
250            header: alloy_rpc_types_eth::Header {
251                hash: b256!("04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"),
252                inner: alloy_consensus::Header {
253                    number: 1,
254                    parent_hash: b256!(
255                        "0202020202020202020202020202020202020202020202020202020202020202"
256                    ),
257                    timestamp: 1,
258                    ..Default::default()
259                },
260                ..Default::default()
261            },
262            ..Default::default()
263        };
264        let expected = BlockInfo {
265            hash: b256!("04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"),
266            number: 1,
267            parent_hash: b256!("0202020202020202020202020202020202020202020202020202020202020202"),
268            timestamp: 1,
269        };
270        let block = block.into_consensus();
271        assert_eq!(BlockInfo::from(block), expected);
272    }
273
274    #[test]
275    fn test_from_block_and_genesis() {
276        use crate::test_utils::RAW_BEDROCK_INFO_TX;
277        let genesis = ChainGenesis {
278            l1: BlockNumHash { hash: B256::from([4; 32]), number: 2 },
279            l2: BlockNumHash { hash: B256::from([5; 32]), number: 1 },
280            ..Default::default()
281        };
282        let tx_env = alloy_rpc_types_eth::Transaction {
283            inner: alloy_consensus::transaction::Recovered::new_unchecked(
284                op_alloy_consensus::OpTxEnvelope::Deposit(alloy_primitives::Sealed::new(
285                    op_alloy_consensus::TxDeposit {
286                        input: alloy_primitives::Bytes::from(&RAW_BEDROCK_INFO_TX),
287                        ..Default::default()
288                    },
289                )),
290                Default::default(),
291            ),
292            block_hash: None,
293            block_number: Some(1),
294            effective_gas_price: Some(1),
295            transaction_index: Some(0),
296        };
297        let block: alloy_rpc_types_eth::Block<op_alloy_rpc_types::Transaction> =
298            alloy_rpc_types_eth::Block {
299                header: alloy_rpc_types_eth::Header {
300                    hash: b256!("04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"),
301                    inner: alloy_consensus::Header {
302                        number: 3,
303                        parent_hash: b256!(
304                            "0202020202020202020202020202020202020202020202020202020202020202"
305                        ),
306                        timestamp: 1,
307                        ..Default::default()
308                    },
309                    ..Default::default()
310                },
311                transactions: alloy_rpc_types_eth::BlockTransactions::Full(vec![
312                    op_alloy_rpc_types::Transaction {
313                        inner: tx_env,
314                        deposit_nonce: None,
315                        deposit_receipt_version: None,
316                    },
317                ]),
318                ..Default::default()
319            };
320        let expected = L2BlockInfo {
321            block_info: BlockInfo {
322                hash: b256!("e65ecd961cee8e4d2d6e1d424116f6fe9a794df0244578b6d5860a3d2dfcd97e"),
323                number: 3,
324                parent_hash: b256!(
325                    "0202020202020202020202020202020202020202020202020202020202020202"
326                ),
327                timestamp: 1,
328            },
329            l1_origin: BlockNumHash {
330                hash: b256!("392012032675be9f94aae5ab442de73c5f4fb1bf30fa7dd0d2442239899a40fc"),
331                number: 18334955,
332            },
333            seq_num: 4,
334        };
335        let block = block.into_consensus();
336        let derived = L2BlockInfo::from_block_and_genesis(&block, &genesis).unwrap();
337        assert_eq!(derived, expected);
338    }
339
340    #[test]
341    fn test_from_block_error_partial_eq() {
342        assert_eq!(FromBlockError::InvalidGenesisHash, FromBlockError::InvalidGenesisHash);
343        assert_eq!(
344            FromBlockError::MissingL1InfoDeposit(b256!(
345                "04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"
346            )),
347            FromBlockError::MissingL1InfoDeposit(b256!(
348                "04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"
349            )),
350        );
351        assert_eq!(FromBlockError::UnexpectedTxType(1), FromBlockError::UnexpectedTxType(1));
352        assert_eq!(
353            FromBlockError::TxEnvelopeDecodeError(Eip2718Error::UnexpectedType(1)),
354            FromBlockError::TxEnvelopeDecodeError(Eip2718Error::UnexpectedType(1))
355        );
356        assert_eq!(FromBlockError::FirstTxNonDeposit(1), FromBlockError::FirstTxNonDeposit(1));
357        assert_eq!(
358            FromBlockError::BlockInfoDecodeError(DecodeError::InvalidSelector),
359            FromBlockError::BlockInfoDecodeError(DecodeError::InvalidSelector)
360        );
361    }
362
363    #[test]
364    fn test_l2_block_info_invalid_genesis_hash() {
365        let genesis = ChainGenesis {
366            l1: BlockNumHash { hash: B256::from([4; 32]), number: 2 },
367            l2: BlockNumHash { hash: B256::from([5; 32]), number: 1 },
368            ..Default::default()
369        };
370        let op_block = OpBlock {
371            header: Header {
372                number: 1,
373                parent_hash: B256::from([2; 32]),
374                timestamp: 1,
375                ..Default::default()
376            },
377            body: Default::default(),
378        };
379        let err = L2BlockInfo::from_block_and_genesis(&op_block, &genesis).unwrap_err();
380        assert_eq!(err, FromBlockError::InvalidGenesisHash);
381    }
382
383    #[test]
384    fn test_from_block() {
385        let block: Block<TxEnvelope, Header> = Block {
386            header: Header {
387                number: 1,
388                parent_hash: B256::from([2; 32]),
389                timestamp: 1,
390                ..Default::default()
391            },
392            body: Default::default(),
393        };
394        let block_info = BlockInfo::from(&block);
395        assert_eq!(
396            block_info,
397            BlockInfo {
398                hash: b256!("04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"),
399                number: block.header.number,
400                parent_hash: block.header.parent_hash,
401                timestamp: block.header.timestamp,
402            }
403        );
404    }
405
406    #[test]
407    fn test_block_info_display() {
408        let hash = B256::from([1; 32]);
409        let parent_hash = B256::from([2; 32]);
410        let block_info = BlockInfo::new(hash, 1, parent_hash, 1);
411        assert_eq!(
412            block_info.to_string(),
413            "BlockInfo { hash: 0x0101010101010101010101010101010101010101010101010101010101010101, number: 1, parent_hash: 0x0202020202020202020202020202020202020202020202020202020202020202, timestamp: 1 }"
414        );
415    }
416
417    #[test]
418    #[cfg(feature = "arbitrary")]
419    fn test_arbitrary_block_info() {
420        use arbitrary::Arbitrary;
421        use rand::Rng;
422        let mut bytes = [0u8; 1024];
423        rand::rng().fill(bytes.as_mut_slice());
424        BlockInfo::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap();
425    }
426
427    #[test]
428    #[cfg(feature = "arbitrary")]
429    fn test_arbitrary_l2_block_info() {
430        use arbitrary::Arbitrary;
431        use rand::Rng;
432        let mut bytes = [0u8; 1024];
433        rand::rng().fill(bytes.as_mut_slice());
434        L2BlockInfo::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap();
435    }
436
437    #[test]
438    fn test_block_id_bounds() {
439        let block_info = BlockInfo {
440            hash: B256::from([1; 32]),
441            number: 0,
442            parent_hash: B256::from([2; 32]),
443            timestamp: 1,
444        };
445        let expected = BlockNumHash { hash: B256::from([1; 32]), number: 0 };
446        assert_eq!(block_info.id(), expected);
447
448        let block_info = BlockInfo {
449            hash: B256::from([1; 32]),
450            number: u64::MAX,
451            parent_hash: B256::from([2; 32]),
452            timestamp: 1,
453        };
454        let expected = BlockNumHash { hash: B256::from([1; 32]), number: u64::MAX };
455        assert_eq!(block_info.id(), expected);
456    }
457
458    #[test]
459    #[cfg(feature = "serde")]
460    fn test_deserialize_block_info() {
461        let block_info = BlockInfo {
462            hash: B256::from([1; 32]),
463            number: 1,
464            parent_hash: B256::from([2; 32]),
465            timestamp: 1,
466        };
467
468        let json = r#"{
469            "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
470            "number": 1,
471            "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
472            "timestamp": 1
473        }"#;
474
475        let deserialized: BlockInfo = serde_json::from_str(json).unwrap();
476        assert_eq!(deserialized, block_info);
477    }
478
479    #[test]
480    #[cfg(feature = "serde")]
481    fn test_deserialize_block_info_with_hex() {
482        let block_info = BlockInfo {
483            hash: B256::from([1; 32]),
484            number: 1,
485            parent_hash: B256::from([2; 32]),
486            timestamp: 1,
487        };
488
489        let json = r#"{
490            "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
491            "number": 1,
492            "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
493            "timestamp": 1
494        }"#;
495
496        let deserialized: BlockInfo = serde_json::from_str(json).unwrap();
497        assert_eq!(deserialized, block_info);
498    }
499
500    #[test]
501    #[cfg(feature = "serde")]
502    fn test_deserialize_l2_block_info() {
503        let l2_block_info = L2BlockInfo {
504            block_info: BlockInfo {
505                hash: B256::from([1; 32]),
506                number: 1,
507                parent_hash: B256::from([2; 32]),
508                timestamp: 1,
509            },
510            l1_origin: BlockNumHash { hash: B256::from([3; 32]), number: 2 },
511            seq_num: 3,
512        };
513
514        let json = r#"{
515            "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
516            "number": 1,
517            "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
518            "timestamp": 1,
519            "l1origin": {
520                "hash": "0x0303030303030303030303030303030303030303030303030303030303030303",
521                "number": 2
522            },
523            "sequenceNumber": 3
524        }"#;
525
526        let deserialized: L2BlockInfo = serde_json::from_str(json).unwrap();
527        assert_eq!(deserialized, l2_block_info);
528    }
529
530    #[test]
531    #[cfg(feature = "serde")]
532    fn test_deserialize_l2_block_info_hex() {
533        let l2_block_info = L2BlockInfo {
534            block_info: BlockInfo {
535                hash: B256::from([1; 32]),
536                number: 1,
537                parent_hash: B256::from([2; 32]),
538                timestamp: 1,
539            },
540            l1_origin: BlockNumHash { hash: B256::from([3; 32]), number: 2 },
541            seq_num: 3,
542        };
543
544        let json = r#"{
545            "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
546            "number": 1,
547            "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
548            "timestamp": 1,
549            "l1origin": {
550                "hash": "0x0303030303030303030303030303030303030303030303030303030303030303",
551                "number": 2
552            },
553            "sequenceNumber": 3
554        }"#;
555
556        let deserialized: L2BlockInfo = serde_json::from_str(json).unwrap();
557        assert_eq!(deserialized, l2_block_info);
558    }
559
560    #[test]
561    fn test_is_parent_of() {
562        let parent = BlockInfo {
563            hash: B256::from([1u8; 32]),
564            number: 10,
565            parent_hash: B256::from([0u8; 32]),
566            timestamp: 1000,
567        };
568        let child = BlockInfo {
569            hash: B256::from([2u8; 32]),
570            number: 11,
571            parent_hash: parent.hash,
572            timestamp: 1010,
573        };
574        let unrelated = BlockInfo {
575            hash: B256::from([3u8; 32]),
576            number: 12,
577            parent_hash: B256::from([9u8; 32]),
578            timestamp: 1020,
579        };
580
581        assert!(parent.is_parent_of(&child));
582        assert!(!child.is_parent_of(&parent));
583        assert!(!parent.is_parent_of(&unrelated));
584    }
585}