1use 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#[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 pub hash: B256,
19 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
21 pub number: u64,
22 pub parent_hash: B256,
24 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
26 pub timestamp: u64,
27}
28
29impl BlockInfo {
30 pub const fn new(hash: B256, number: u64, parent_hash: B256, timestamp: u64) -> Self {
32 Self { hash, number, parent_hash, timestamp }
33 }
34
35 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#[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 #[cfg_attr(feature = "serde", serde(flatten))]
75 pub block_info: BlockInfo,
76 #[cfg_attr(feature = "serde", serde(rename = "l1origin", alias = "l1Origin"))]
78 pub l1_origin: BlockNumHash,
79 #[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#[derive(Debug, Clone, thiserror::Error)]
100pub enum FromBlockError {
101 #[error("Invalid genesis hash")]
103 InvalidGenesisHash,
104 #[error("L2 block is missing L1 info deposit transaction ({0})")]
106 MissingL1InfoDeposit(B256),
107 #[error("First payload transaction has unexpected type: {0}")]
109 UnexpectedTxType(u8),
110 #[error("Failed to decode the first transaction into an OP transaction: {0}")]
112 TxEnvelopeDecodeError(Eip2718Error),
113 #[error("First payload transaction is not a deposit transaction, type: {0}")]
115 FirstTxNonDeposit(u8),
116 #[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 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 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 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}