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