kona_protocol/
block.rs

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