1use 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#[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 pub hash: B256,
18 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
20 pub number: u64,
21 pub parent_hash: B256,
23 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
25 pub timestamp: u64,
26}
27
28impl BlockInfo {
29 pub const fn new(hash: B256, number: u64, parent_hash: B256, timestamp: u64) -> Self {
31 Self { hash, number, parent_hash, timestamp }
32 }
33
34 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#[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 pub block_info: BlockInfo,
74 pub l1_origin: BlockNumHash,
76 #[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#[derive(Debug, Clone, thiserror::Error)]
94pub enum FromBlockError {
95 #[error("Invalid genesis hash")]
97 InvalidGenesisHash,
98 #[error("L2 block is missing L1 info deposit transaction ({0})")]
100 MissingL1InfoDeposit(B256),
101 #[error("First payload transaction has unexpected type: {0}")]
103 UnexpectedTxType(u8),
104 #[error("Failed to decode the first transaction into an OP transaction: {0}")]
106 TxEnvelopeDecodeError(Eip2718Error),
107 #[error("First payload transaction is not a deposit transaction, type: {0}")]
109 FirstTxNonDeposit(u8),
110 #[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 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 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}