kona_protocol/
block.rs

1//! Block Types for Optimism.
2
3use crate::{DecodeError, L1BlockInfoTx};
4use alloy_consensus::{Block, Transaction, Typed2718};
5use alloy_eips::{eip2718::Eip2718Error, BlockNumHash};
6use alloy_primitives::B256;
7use kona_genesis::ChainGenesis;
8use op_alloy_consensus::OpBlock;
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    pub block_info: BlockInfo,
74    /// The L1 origin [BlockNumHash]
75    pub l1_origin: BlockNumHash,
76    /// The sequence number of the L2 block
77    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
78    pub seq_num: u64,
79}
80
81#[cfg(feature = "arbitrary")]
82impl arbitrary::Arbitrary<'_> for L2BlockInfo {
83    fn arbitrary(g: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
84        Ok(Self {
85            block_info: g.arbitrary()?,
86            l1_origin: BlockNumHash { number: g.arbitrary()?, hash: g.arbitrary()? },
87            seq_num: g.arbitrary()?,
88        })
89    }
90}
91
92/// An error that can occur when converting an OP [Block] to [L2BlockInfo].
93#[derive(Debug, Clone, thiserror::Error)]
94pub enum FromBlockError {
95    /// The genesis block hash does not match the expected value.
96    #[error("Invalid genesis hash")]
97    InvalidGenesisHash,
98    /// The L2 block is missing the L1 info deposit transaction.
99    #[error("L2 block is missing L1 info deposit transaction ({0})")]
100    MissingL1InfoDeposit(B256),
101    /// The first payload transaction has an unexpected type.
102    #[error("First payload transaction has unexpected type: {0}")]
103    UnexpectedTxType(u8),
104    /// Failed to decode the first transaction into an OP transaction.
105    #[error("Failed to decode the first transaction into an OP transaction: {0}")]
106    TxEnvelopeDecodeError(Eip2718Error),
107    /// The first payload transaction is not a deposit transaction.
108    #[error("First payload transaction is not a deposit transaction, type: {0}")]
109    FirstTxNonDeposit(u8),
110    /// Failed to decode the [L1BlockInfoTx] from the deposit transaction.
111    #[error("Failed to decode the L1BlockInfoTx from the deposit transaction: {0}")]
112    BlockInfoDecodeError(#[from] DecodeError),
113}
114
115impl PartialEq<Self> for FromBlockError {
116    fn eq(&self, other: &Self) -> bool {
117        match (self, other) {
118            (Self::InvalidGenesisHash, Self::InvalidGenesisHash) => true,
119            (Self::MissingL1InfoDeposit(a), Self::MissingL1InfoDeposit(b)) => a == b,
120            (Self::UnexpectedTxType(a), Self::UnexpectedTxType(b)) => a == b,
121            (Self::TxEnvelopeDecodeError(_), Self::TxEnvelopeDecodeError(_)) => true,
122            (Self::FirstTxNonDeposit(a), Self::FirstTxNonDeposit(b)) => a == b,
123            (Self::BlockInfoDecodeError(a), Self::BlockInfoDecodeError(b)) => a == b,
124            _ => false,
125        }
126    }
127}
128
129impl From<Eip2718Error> for FromBlockError {
130    fn from(value: Eip2718Error) -> Self {
131        Self::TxEnvelopeDecodeError(value)
132    }
133}
134
135impl L2BlockInfo {
136    /// Instantiates a new [L2BlockInfo].
137    pub const fn new(block_info: BlockInfo, l1_origin: BlockNumHash, seq_num: u64) -> Self {
138        Self { block_info, l1_origin, seq_num }
139    }
140
141    /// Constructs an [L2BlockInfo] from a given OP [Block] and [ChainGenesis].
142    pub fn from_block_and_genesis(
143        block: &OpBlock,
144        genesis: &ChainGenesis,
145    ) -> Result<Self, FromBlockError> {
146        let block_info = BlockInfo::from(block);
147
148        let (l1_origin, sequence_number) = if block_info.number == genesis.l2.number {
149            if block_info.hash != genesis.l2.hash {
150                return Err(FromBlockError::InvalidGenesisHash);
151            }
152            (genesis.l1, 0)
153        } else {
154            if block.body.transactions.is_empty() {
155                return Err(FromBlockError::MissingL1InfoDeposit(block_info.hash));
156            }
157
158            let tx = &block.body.transactions[0];
159
160            let Some(tx) = tx.as_deposit() else {
161                return Err(FromBlockError::FirstTxNonDeposit(tx.ty()));
162            };
163
164            let l1_info = L1BlockInfoTx::decode_calldata(tx.input().as_ref())
165                .map_err(FromBlockError::BlockInfoDecodeError)?;
166            (l1_info.id(), l1_info.sequence_number())
167        };
168
169        Ok(Self { block_info, l1_origin, seq_num: sequence_number })
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use alloc::string::ToString;
177    use alloy_consensus::{Header, TxEnvelope};
178    use alloy_primitives::b256;
179
180    #[test]
181    fn test_from_block_error_partial_eq() {
182        assert_eq!(FromBlockError::InvalidGenesisHash, FromBlockError::InvalidGenesisHash);
183        assert_eq!(
184            FromBlockError::MissingL1InfoDeposit(b256!(
185                "04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"
186            )),
187            FromBlockError::MissingL1InfoDeposit(b256!(
188                "04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"
189            )),
190        );
191        assert_eq!(FromBlockError::UnexpectedTxType(1), FromBlockError::UnexpectedTxType(1));
192        assert_eq!(
193            FromBlockError::TxEnvelopeDecodeError(Eip2718Error::UnexpectedType(1)),
194            FromBlockError::TxEnvelopeDecodeError(Eip2718Error::UnexpectedType(1))
195        );
196        assert_eq!(FromBlockError::FirstTxNonDeposit(1), FromBlockError::FirstTxNonDeposit(1));
197        assert_eq!(
198            FromBlockError::BlockInfoDecodeError(DecodeError::InvalidSelector),
199            FromBlockError::BlockInfoDecodeError(DecodeError::InvalidSelector)
200        );
201    }
202
203    #[test]
204    fn test_l2_block_info_invalid_genesis_hash() {
205        let genesis = ChainGenesis {
206            l1: BlockNumHash { hash: B256::from([4; 32]), number: 2 },
207            l2: BlockNumHash { hash: B256::from([5; 32]), number: 1 },
208            ..Default::default()
209        };
210        let op_block = OpBlock {
211            header: Header {
212                number: 1,
213                parent_hash: B256::from([2; 32]),
214                timestamp: 1,
215                ..Default::default()
216            },
217            body: Default::default(),
218        };
219        let err = L2BlockInfo::from_block_and_genesis(&op_block, &genesis).unwrap_err();
220        assert_eq!(err, FromBlockError::InvalidGenesisHash);
221    }
222
223    #[test]
224    fn test_from_block() {
225        let block: Block<TxEnvelope, Header> = Block {
226            header: Header {
227                number: 1,
228                parent_hash: B256::from([2; 32]),
229                timestamp: 1,
230                ..Default::default()
231            },
232            body: Default::default(),
233        };
234        let block_info = BlockInfo::from(&block);
235        assert_eq!(
236            block_info,
237            BlockInfo {
238                hash: b256!("04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"),
239                number: block.header.number,
240                parent_hash: block.header.parent_hash,
241                timestamp: block.header.timestamp,
242            }
243        );
244    }
245
246    #[test]
247    fn test_block_info_display() {
248        let hash = B256::from([1; 32]);
249        let parent_hash = B256::from([2; 32]);
250        let block_info = BlockInfo::new(hash, 1, parent_hash, 1);
251        assert_eq!(
252            block_info.to_string(),
253            "BlockInfo { hash: 0x0101010101010101010101010101010101010101010101010101010101010101, number: 1, parent_hash: 0x0202020202020202020202020202020202020202020202020202020202020202, timestamp: 1 }"
254        );
255    }
256
257    #[test]
258    #[cfg(feature = "arbitrary")]
259    fn test_arbitrary_block_info() {
260        use arbitrary::Arbitrary;
261        use rand::Rng;
262        let mut bytes = [0u8; 1024];
263        rand::rng().fill(bytes.as_mut_slice());
264        BlockInfo::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap();
265    }
266
267    #[test]
268    #[cfg(feature = "arbitrary")]
269    fn test_arbitrary_l2_block_info() {
270        use arbitrary::Arbitrary;
271        use rand::Rng;
272        let mut bytes = [0u8; 1024];
273        rand::rng().fill(bytes.as_mut_slice());
274        L2BlockInfo::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap();
275    }
276
277    #[test]
278    fn test_block_id_bounds() {
279        let block_info = BlockInfo {
280            hash: B256::from([1; 32]),
281            number: 0,
282            parent_hash: B256::from([2; 32]),
283            timestamp: 1,
284        };
285        let expected = BlockNumHash { hash: B256::from([1; 32]), number: 0 };
286        assert_eq!(block_info.id(), expected);
287
288        let block_info = BlockInfo {
289            hash: B256::from([1; 32]),
290            number: u64::MAX,
291            parent_hash: B256::from([2; 32]),
292            timestamp: 1,
293        };
294        let expected = BlockNumHash { hash: B256::from([1; 32]), number: u64::MAX };
295        assert_eq!(block_info.id(), expected);
296    }
297
298    #[test]
299    #[cfg(feature = "serde")]
300    fn test_deserialize_block_info() {
301        let block_info = BlockInfo {
302            hash: B256::from([1; 32]),
303            number: 1,
304            parent_hash: B256::from([2; 32]),
305            timestamp: 1,
306        };
307
308        let json = r#"{
309            "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
310            "number": 1,
311            "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
312            "timestamp": 1
313        }"#;
314
315        let deserialized: BlockInfo = serde_json::from_str(json).unwrap();
316        assert_eq!(deserialized, block_info);
317    }
318
319    #[test]
320    #[cfg(feature = "serde")]
321    fn test_deserialize_block_info_with_hex() {
322        let block_info = BlockInfo {
323            hash: B256::from([1; 32]),
324            number: 1,
325            parent_hash: B256::from([2; 32]),
326            timestamp: 1,
327        };
328
329        let json = r#"{
330            "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
331            "number": "0x1",
332            "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
333            "timestamp": "0x1"
334        }"#;
335
336        let deserialized: BlockInfo = serde_json::from_str(json).unwrap();
337        assert_eq!(deserialized, block_info);
338    }
339
340    #[test]
341    #[cfg(feature = "serde")]
342    fn test_deserialize_l2_block_info() {
343        let l2_block_info = L2BlockInfo {
344            block_info: BlockInfo {
345                hash: B256::from([1; 32]),
346                number: 1,
347                parent_hash: B256::from([2; 32]),
348                timestamp: 1,
349            },
350            l1_origin: BlockNumHash { hash: B256::from([3; 32]), number: 2 },
351            seq_num: 3,
352        };
353
354        let json = r#"{
355            "blockInfo": {
356                "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
357                "number": 1,
358                "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
359                "timestamp": 1
360            },
361            "l1Origin": {
362                "hash": "0x0303030303030303030303030303030303030303030303030303030303030303",
363                "number": 2
364            },
365            "seqNum": 3
366        }"#;
367
368        let deserialized: L2BlockInfo = serde_json::from_str(json).unwrap();
369        assert_eq!(deserialized, l2_block_info);
370    }
371
372    #[test]
373    #[cfg(feature = "serde")]
374    fn test_deserialize_l2_block_info_hex() {
375        let l2_block_info = L2BlockInfo {
376            block_info: BlockInfo {
377                hash: B256::from([1; 32]),
378                number: 1,
379                parent_hash: B256::from([2; 32]),
380                timestamp: 1,
381            },
382            l1_origin: BlockNumHash { hash: B256::from([3; 32]), number: 2 },
383            seq_num: 3,
384        };
385
386        let json = r#"{
387            "blockInfo": {
388                "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
389                "number": "0x1",
390                "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
391                "timestamp": "0x1"
392            },
393            "l1Origin": {
394                "hash": "0x0303030303030303030303030303030303030303030303030303030303030303",
395                "number": 2
396            },
397            "seqNum": "0x3"
398        }"#;
399
400        let deserialized: L2BlockInfo = serde_json::from_str(json).unwrap();
401        assert_eq!(deserialized, l2_block_info);
402    }
403}