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