1use crate::{DecodeError, L1BlockInfoTx};
4use alloy_consensus::{Block, Transaction, Typed2718};
5use alloy_eips::{BlockNumHash, eip2718::Eip2718Error};
6use alloy_primitives::B256;
7use kona_genesis::ChainGenesis;
8use op_alloy_consensus::OpTxEnvelope;
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 #[cfg_attr(feature = "serde", serde(flatten))]
74 pub block_info: BlockInfo,
75 #[cfg_attr(feature = "serde", serde(rename = "l1origin", alias = "l1Origin"))]
77 pub l1_origin: BlockNumHash,
78 #[cfg_attr(
80 feature = "serde",
81 serde(with = "alloy_serde::quantity", rename = "sequenceNumber", alias = "seqNum")
82 )]
83 pub seq_num: u64,
84}
85
86#[cfg(feature = "arbitrary")]
87impl arbitrary::Arbitrary<'_> for L2BlockInfo {
88 fn arbitrary(g: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
89 Ok(Self {
90 block_info: g.arbitrary()?,
91 l1_origin: BlockNumHash { number: g.arbitrary()?, hash: g.arbitrary()? },
92 seq_num: g.arbitrary()?,
93 })
94 }
95}
96
97#[derive(Debug, Clone, thiserror::Error)]
99pub enum FromBlockError {
100 #[error("Invalid genesis hash")]
102 InvalidGenesisHash,
103 #[error("L2 block is missing L1 info deposit transaction ({0})")]
105 MissingL1InfoDeposit(B256),
106 #[error("First payload transaction has unexpected type: {0}")]
108 UnexpectedTxType(u8),
109 #[error("Failed to decode the first transaction into an OP transaction: {0}")]
111 TxEnvelopeDecodeError(Eip2718Error),
112 #[error("First payload transaction is not a deposit transaction, type: {0}")]
114 FirstTxNonDeposit(u8),
115 #[error("Failed to decode the L1BlockInfoTx from the deposit transaction: {0}")]
117 BlockInfoDecodeError(#[from] DecodeError),
118}
119
120impl PartialEq<Self> for FromBlockError {
121 fn eq(&self, other: &Self) -> bool {
122 match (self, other) {
123 (Self::InvalidGenesisHash, Self::InvalidGenesisHash) => true,
124 (Self::MissingL1InfoDeposit(a), Self::MissingL1InfoDeposit(b)) => a == b,
125 (Self::UnexpectedTxType(a), Self::UnexpectedTxType(b)) => a == b,
126 (Self::TxEnvelopeDecodeError(_), Self::TxEnvelopeDecodeError(_)) => true,
127 (Self::FirstTxNonDeposit(a), Self::FirstTxNonDeposit(b)) => a == b,
128 (Self::BlockInfoDecodeError(a), Self::BlockInfoDecodeError(b)) => a == b,
129 _ => false,
130 }
131 }
132}
133
134impl From<Eip2718Error> for FromBlockError {
135 fn from(value: Eip2718Error) -> Self {
136 Self::TxEnvelopeDecodeError(value)
137 }
138}
139
140impl L2BlockInfo {
141 pub const fn new(block_info: BlockInfo, l1_origin: BlockNumHash, seq_num: u64) -> Self {
143 Self { block_info, l1_origin, seq_num }
144 }
145
146 pub fn from_block_and_genesis<T: Typed2718 + AsRef<OpTxEnvelope>>(
148 block: &Block<T>,
149 genesis: &ChainGenesis,
150 ) -> Result<Self, FromBlockError> {
151 let block_info = BlockInfo::from(block);
152
153 let (l1_origin, sequence_number) = if block_info.number == genesis.l2.number {
154 if block_info.hash != genesis.l2.hash {
155 return Err(FromBlockError::InvalidGenesisHash);
156 }
157 (genesis.l1, 0)
158 } else {
159 if block.body.transactions.is_empty() {
160 return Err(FromBlockError::MissingL1InfoDeposit(block_info.hash));
161 }
162
163 let tx = block.body.transactions[0].as_ref();
164 let Some(tx) = tx.as_deposit() else {
165 return Err(FromBlockError::FirstTxNonDeposit(tx.ty()));
166 };
167
168 let l1_info = L1BlockInfoTx::decode_calldata(tx.input().as_ref())
169 .map_err(FromBlockError::BlockInfoDecodeError)?;
170 (l1_info.id(), l1_info.sequence_number())
171 };
172
173 Ok(Self { block_info, l1_origin, seq_num: sequence_number })
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use alloc::string::ToString;
181 use alloy_consensus::{Header, TxEnvelope};
182 use alloy_primitives::b256;
183 use op_alloy_consensus::OpBlock;
184
185 #[test]
186 fn test_rpc_block_into_info() {
187 let block: alloy_rpc_types_eth::Block<OpTxEnvelope> = alloy_rpc_types_eth::Block {
188 header: alloy_rpc_types_eth::Header {
189 hash: b256!("04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"),
190 inner: alloy_consensus::Header {
191 number: 1,
192 parent_hash: b256!(
193 "0202020202020202020202020202020202020202020202020202020202020202"
194 ),
195 timestamp: 1,
196 ..Default::default()
197 },
198 ..Default::default()
199 },
200 ..Default::default()
201 };
202 let expected = BlockInfo {
203 hash: b256!("04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"),
204 number: 1,
205 parent_hash: b256!("0202020202020202020202020202020202020202020202020202020202020202"),
206 timestamp: 1,
207 };
208 let block = block.into_consensus();
209 assert_eq!(BlockInfo::from(block), expected);
210 }
211
212 #[test]
213 fn test_from_block_and_genesis() {
214 use crate::test_utils::RAW_BEDROCK_INFO_TX;
215 let genesis = ChainGenesis {
216 l1: BlockNumHash { hash: B256::from([4; 32]), number: 2 },
217 l2: BlockNumHash { hash: B256::from([5; 32]), number: 1 },
218 ..Default::default()
219 };
220 let tx_env = alloy_rpc_types_eth::Transaction {
221 inner: alloy_consensus::transaction::Recovered::new_unchecked(
222 op_alloy_consensus::OpTxEnvelope::Deposit(alloy_primitives::Sealed::new(
223 op_alloy_consensus::TxDeposit {
224 input: alloy_primitives::Bytes::from(&RAW_BEDROCK_INFO_TX),
225 ..Default::default()
226 },
227 )),
228 Default::default(),
229 ),
230 block_hash: None,
231 block_number: Some(1),
232 effective_gas_price: Some(1),
233 transaction_index: Some(0),
234 };
235 let block: alloy_rpc_types_eth::Block<op_alloy_rpc_types::Transaction> =
236 alloy_rpc_types_eth::Block {
237 header: alloy_rpc_types_eth::Header {
238 hash: b256!("04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"),
239 inner: alloy_consensus::Header {
240 number: 3,
241 parent_hash: b256!(
242 "0202020202020202020202020202020202020202020202020202020202020202"
243 ),
244 timestamp: 1,
245 ..Default::default()
246 },
247 ..Default::default()
248 },
249 transactions: alloy_rpc_types_eth::BlockTransactions::Full(vec![
250 op_alloy_rpc_types::Transaction {
251 inner: tx_env,
252 deposit_nonce: None,
253 deposit_receipt_version: None,
254 },
255 ]),
256 ..Default::default()
257 };
258 let expected = L2BlockInfo {
259 block_info: BlockInfo {
260 hash: b256!("e65ecd961cee8e4d2d6e1d424116f6fe9a794df0244578b6d5860a3d2dfcd97e"),
261 number: 3,
262 parent_hash: b256!(
263 "0202020202020202020202020202020202020202020202020202020202020202"
264 ),
265 timestamp: 1,
266 },
267 l1_origin: BlockNumHash {
268 hash: b256!("392012032675be9f94aae5ab442de73c5f4fb1bf30fa7dd0d2442239899a40fc"),
269 number: 18334955,
270 },
271 seq_num: 4,
272 };
273 let block = block.into_consensus();
274 let derived = L2BlockInfo::from_block_and_genesis(&block, &genesis).unwrap();
275 assert_eq!(derived, expected);
276 }
277
278 #[test]
279 fn test_from_block_error_partial_eq() {
280 assert_eq!(FromBlockError::InvalidGenesisHash, FromBlockError::InvalidGenesisHash);
281 assert_eq!(
282 FromBlockError::MissingL1InfoDeposit(b256!(
283 "04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"
284 )),
285 FromBlockError::MissingL1InfoDeposit(b256!(
286 "04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"
287 )),
288 );
289 assert_eq!(FromBlockError::UnexpectedTxType(1), FromBlockError::UnexpectedTxType(1));
290 assert_eq!(
291 FromBlockError::TxEnvelopeDecodeError(Eip2718Error::UnexpectedType(1)),
292 FromBlockError::TxEnvelopeDecodeError(Eip2718Error::UnexpectedType(1))
293 );
294 assert_eq!(FromBlockError::FirstTxNonDeposit(1), FromBlockError::FirstTxNonDeposit(1));
295 assert_eq!(
296 FromBlockError::BlockInfoDecodeError(DecodeError::InvalidSelector),
297 FromBlockError::BlockInfoDecodeError(DecodeError::InvalidSelector)
298 );
299 }
300
301 #[test]
302 fn test_l2_block_info_invalid_genesis_hash() {
303 let genesis = ChainGenesis {
304 l1: BlockNumHash { hash: B256::from([4; 32]), number: 2 },
305 l2: BlockNumHash { hash: B256::from([5; 32]), number: 1 },
306 ..Default::default()
307 };
308 let op_block = OpBlock {
309 header: Header {
310 number: 1,
311 parent_hash: B256::from([2; 32]),
312 timestamp: 1,
313 ..Default::default()
314 },
315 body: Default::default(),
316 };
317 let err = L2BlockInfo::from_block_and_genesis(&op_block, &genesis).unwrap_err();
318 assert_eq!(err, FromBlockError::InvalidGenesisHash);
319 }
320
321 #[test]
322 fn test_from_block() {
323 let block: Block<TxEnvelope, Header> = Block {
324 header: Header {
325 number: 1,
326 parent_hash: B256::from([2; 32]),
327 timestamp: 1,
328 ..Default::default()
329 },
330 body: Default::default(),
331 };
332 let block_info = BlockInfo::from(&block);
333 assert_eq!(
334 block_info,
335 BlockInfo {
336 hash: b256!("04d6fefc87466405ba0e5672dcf5c75325b33e5437da2a42423080aab8be889b"),
337 number: block.header.number,
338 parent_hash: block.header.parent_hash,
339 timestamp: block.header.timestamp,
340 }
341 );
342 }
343
344 #[test]
345 fn test_block_info_display() {
346 let hash = B256::from([1; 32]);
347 let parent_hash = B256::from([2; 32]);
348 let block_info = BlockInfo::new(hash, 1, parent_hash, 1);
349 assert_eq!(
350 block_info.to_string(),
351 "BlockInfo { hash: 0x0101010101010101010101010101010101010101010101010101010101010101, number: 1, parent_hash: 0x0202020202020202020202020202020202020202020202020202020202020202, timestamp: 1 }"
352 );
353 }
354
355 #[test]
356 #[cfg(feature = "arbitrary")]
357 fn test_arbitrary_block_info() {
358 use arbitrary::Arbitrary;
359 use rand::Rng;
360 let mut bytes = [0u8; 1024];
361 rand::rng().fill(bytes.as_mut_slice());
362 BlockInfo::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap();
363 }
364
365 #[test]
366 #[cfg(feature = "arbitrary")]
367 fn test_arbitrary_l2_block_info() {
368 use arbitrary::Arbitrary;
369 use rand::Rng;
370 let mut bytes = [0u8; 1024];
371 rand::rng().fill(bytes.as_mut_slice());
372 L2BlockInfo::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap();
373 }
374
375 #[test]
376 fn test_block_id_bounds() {
377 let block_info = BlockInfo {
378 hash: B256::from([1; 32]),
379 number: 0,
380 parent_hash: B256::from([2; 32]),
381 timestamp: 1,
382 };
383 let expected = BlockNumHash { hash: B256::from([1; 32]), number: 0 };
384 assert_eq!(block_info.id(), expected);
385
386 let block_info = BlockInfo {
387 hash: B256::from([1; 32]),
388 number: u64::MAX,
389 parent_hash: B256::from([2; 32]),
390 timestamp: 1,
391 };
392 let expected = BlockNumHash { hash: B256::from([1; 32]), number: u64::MAX };
393 assert_eq!(block_info.id(), expected);
394 }
395
396 #[test]
397 #[cfg(feature = "serde")]
398 fn test_deserialize_block_info() {
399 let block_info = BlockInfo {
400 hash: B256::from([1; 32]),
401 number: 1,
402 parent_hash: B256::from([2; 32]),
403 timestamp: 1,
404 };
405
406 let json = r#"{
407 "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
408 "number": 1,
409 "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
410 "timestamp": 1
411 }"#;
412
413 let deserialized: BlockInfo = serde_json::from_str(json).unwrap();
414 assert_eq!(deserialized, block_info);
415 }
416
417 #[test]
418 #[cfg(feature = "serde")]
419 fn test_deserialize_block_info_with_hex() {
420 let block_info = BlockInfo {
421 hash: B256::from([1; 32]),
422 number: 1,
423 parent_hash: B256::from([2; 32]),
424 timestamp: 1,
425 };
426
427 let json = r#"{
428 "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
429 "number": "0x1",
430 "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
431 "timestamp": "0x1"
432 }"#;
433
434 let deserialized: BlockInfo = serde_json::from_str(json).unwrap();
435 assert_eq!(deserialized, block_info);
436 }
437
438 #[test]
439 #[cfg(feature = "serde")]
440 fn test_deserialize_l2_block_info() {
441 let l2_block_info = L2BlockInfo {
442 block_info: BlockInfo {
443 hash: B256::from([1; 32]),
444 number: 1,
445 parent_hash: B256::from([2; 32]),
446 timestamp: 1,
447 },
448 l1_origin: BlockNumHash { hash: B256::from([3; 32]), number: 2 },
449 seq_num: 3,
450 };
451
452 let json = r#"{
453 "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
454 "number": 1,
455 "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
456 "timestamp": 1,
457 "l1origin": {
458 "hash": "0x0303030303030303030303030303030303030303030303030303030303030303",
459 "number": 2
460 },
461 "sequenceNumber": 3
462 }"#;
463
464 let deserialized: L2BlockInfo = serde_json::from_str(json).unwrap();
465 assert_eq!(deserialized, l2_block_info);
466 }
467
468 #[test]
469 #[cfg(feature = "serde")]
470 fn test_deserialize_l2_block_info_hex() {
471 let l2_block_info = L2BlockInfo {
472 block_info: BlockInfo {
473 hash: B256::from([1; 32]),
474 number: 1,
475 parent_hash: B256::from([2; 32]),
476 timestamp: 1,
477 },
478 l1_origin: BlockNumHash { hash: B256::from([3; 32]), number: 2 },
479 seq_num: 3,
480 };
481
482 let json = r#"{
483 "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
484 "number": "0x1",
485 "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
486 "timestamp": "0x1",
487 "l1origin": {
488 "hash": "0x0303030303030303030303030303030303030303030303030303030303030303",
489 "number": 2
490 },
491 "sequenceNumber": "0x3"
492 }"#;
493
494 let deserialized: L2BlockInfo = serde_json::from_str(json).unwrap();
495 assert_eq!(deserialized, l2_block_info);
496 }
497}