1use crate::{BatchValidity, BlockInfo, L2BlockInfo, starts_with_2718_deposit, starts_with_7702_tx};
4use alloc::vec::Vec;
5use alloy_eips::BlockNumHash;
6use alloy_primitives::{BlockHash, Bytes};
7use alloy_rlp::{RlpDecodable, RlpEncodable};
8use kona_genesis::RollupConfig;
9
10#[derive(Debug, Default, RlpDecodable, RlpEncodable, Clone, PartialEq, Eq)]
12pub struct SingleBatch {
13 pub parent_hash: BlockHash,
16 pub epoch_num: u64,
18 pub epoch_hash: BlockHash,
20 pub timestamp: u64,
22 pub transactions: Vec<Bytes>,
24}
25
26impl SingleBatch {
27 pub fn has_invalid_transactions(&self) -> bool {
29 self.transactions.iter().any(|tx| tx.0.is_empty() || tx.0[0] == 0x7E)
30 }
31
32 pub const fn epoch(&self) -> BlockNumHash {
34 BlockNumHash { number: self.epoch_num, hash: self.epoch_hash }
35 }
36
37 pub fn check_batch_timestamp(
39 &self,
40 cfg: &RollupConfig,
41 l2_safe_head: L2BlockInfo,
42 inclusion_block: &BlockInfo,
43 ) -> BatchValidity {
44 let next_timestamp = l2_safe_head.block_info.timestamp + cfg.block_time;
45 if self.timestamp > next_timestamp {
46 if cfg.is_holocene_active(inclusion_block.timestamp) {
47 return BatchValidity::Drop;
48 }
49 return BatchValidity::Future;
50 }
51 if self.timestamp < next_timestamp {
52 if cfg.is_holocene_active(inclusion_block.timestamp) {
53 return BatchValidity::Past;
54 }
55 return BatchValidity::Drop;
56 }
57 BatchValidity::Accept
58 }
59
60 pub fn check_batch(
66 &self,
67 cfg: &RollupConfig,
68 l1_blocks: &[BlockInfo],
69 l2_safe_head: L2BlockInfo,
70 inclusion_block: &BlockInfo,
71 ) -> BatchValidity {
72 if l1_blocks.is_empty() {
74 return BatchValidity::Undecided;
75 }
76
77 let epoch = l1_blocks[0];
78
79 let timestamp_check = self.check_batch_timestamp(cfg, l2_safe_head, inclusion_block);
81 if !timestamp_check.is_accept() {
82 return timestamp_check;
83 }
84
85 if self.parent_hash != l2_safe_head.block_info.hash {
88 return BatchValidity::Drop;
89 }
90
91 if self.epoch_num + cfg.seq_window_size < inclusion_block.number {
93 return BatchValidity::Drop;
94 }
95
96 let mut batch_origin = epoch;
98 if self.epoch_num < epoch.number {
99 return BatchValidity::Drop;
100 } else if self.epoch_num == epoch.number {
101 } else if self.epoch_num == epoch.number + 1 {
103 if l1_blocks.len() < 2 {
109 return BatchValidity::Undecided;
110 }
111 batch_origin = l1_blocks[1];
112 } else {
113 return BatchValidity::Drop;
114 }
115
116 if self.epoch_hash != batch_origin.hash {
118 return BatchValidity::Drop;
119 }
120
121 if self.timestamp < batch_origin.timestamp {
122 return BatchValidity::Drop;
123 }
124
125 let max_drift = cfg.max_sequencer_drift(batch_origin.timestamp);
127 let max = if let Some(max) = batch_origin.timestamp.checked_add(max_drift) {
128 max
129 } else {
130 return BatchValidity::Drop;
131 };
132
133 let no_txs = self.transactions.is_empty();
134 if self.timestamp > max && !no_txs {
135 return BatchValidity::Drop;
139 }
140 if self.timestamp > max && no_txs {
141 if epoch.number == batch_origin.number {
146 if l1_blocks.len() < 2 {
147 return BatchValidity::Undecided;
148 }
149 let next_origin = l1_blocks[1];
150 if self.timestamp >= next_origin.timestamp {
152 return BatchValidity::Drop;
153 }
154 }
155 }
156
157 for tx in self.transactions.iter() {
159 if tx.is_empty() {
160 return BatchValidity::Drop;
161 }
162 if starts_with_2718_deposit(tx) {
163 return BatchValidity::Drop;
164 }
165 if !cfg.is_isthmus_active(self.timestamp) && starts_with_7702_tx(tx) {
167 return BatchValidity::Drop;
168 }
169 }
170
171 BatchValidity::Accept
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use alloc::vec;
179 use alloy_consensus::{SignableTransaction, TxEip1559, TxEip7702, TxEnvelope};
180 use alloy_eips::eip2718::{Decodable2718, Encodable2718};
181 use alloy_primitives::{Address, PrimitiveSignature, Sealed, TxKind, U256};
182 use kona_genesis::HardForkConfig;
183 use op_alloy_consensus::{OpTxEnvelope, TxDeposit};
184
185 #[test]
186 fn test_empty_l1_blocks() {
187 let cfg = RollupConfig::default();
188 let l1_blocks = vec![];
189 let l2_safe_head = L2BlockInfo::default();
190 let inclusion_block = BlockInfo::default();
191 let batch = SingleBatch::default();
192 assert_eq!(
193 batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
194 BatchValidity::Undecided
195 );
196 }
197
198 #[test]
199 fn test_timestamp_future() {
200 let cfg = RollupConfig::default();
201 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
202 let l2_safe_head = L2BlockInfo {
203 block_info: BlockInfo { timestamp: 1, ..Default::default() },
204 ..Default::default()
205 };
206 let inclusion_block = BlockInfo::default();
207 let batch = SingleBatch { timestamp: 2, ..Default::default() };
208 assert_eq!(
209 batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
210 BatchValidity::Future
211 );
212 }
213
214 #[test]
215 fn test_parent_hash_mismatch() {
216 let cfg = RollupConfig::default();
217 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
218 let l2_safe_head = L2BlockInfo {
219 block_info: BlockInfo { hash: BlockHash::from([0x01; 32]), ..Default::default() },
220 ..Default::default()
221 };
222 let inclusion_block = BlockInfo::default();
223 let batch = SingleBatch { parent_hash: BlockHash::from([0x02; 32]), ..Default::default() };
224 assert_eq!(
225 batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
226 BatchValidity::Drop
227 );
228 }
229
230 #[test]
231 fn test_check_batch_timestamp_holocene_inactive_future() {
232 let cfg = RollupConfig::default();
233 let l2_safe_head = L2BlockInfo {
234 block_info: BlockInfo { timestamp: 1, ..Default::default() },
235 ..Default::default()
236 };
237 let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() };
238 let batch = SingleBatch { epoch_num: 1, timestamp: 2, ..Default::default() };
239 assert_eq!(
240 batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
241 BatchValidity::Future
242 );
243 }
244
245 #[test]
246 fn test_check_batch_timestamp_holocene_active_drop() {
247 let cfg = RollupConfig {
248 hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() },
249 ..Default::default()
250 };
251 let l2_safe_head = L2BlockInfo {
252 block_info: BlockInfo { timestamp: 1, ..Default::default() },
253 ..Default::default()
254 };
255 let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() };
256 let batch = SingleBatch { epoch_num: 1, timestamp: 2, ..Default::default() };
257 assert_eq!(
258 batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
259 BatchValidity::Drop
260 );
261 }
262
263 #[test]
264 fn test_check_batch_timestamp_holocene_active_past() {
265 let cfg = RollupConfig {
266 hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() },
267 ..Default::default()
268 };
269 let l2_safe_head = L2BlockInfo {
270 block_info: BlockInfo { timestamp: 2, ..Default::default() },
271 ..Default::default()
272 };
273 let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() };
274 let batch = SingleBatch { epoch_num: 1, timestamp: 1, ..Default::default() };
275 assert_eq!(
276 batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
277 BatchValidity::Past
278 );
279 }
280
281 #[test]
282 fn test_check_batch_timestamp_holocene_inactive_drop() {
283 let cfg = RollupConfig::default();
284 let l2_safe_head = L2BlockInfo {
285 block_info: BlockInfo { timestamp: 2, ..Default::default() },
286 ..Default::default()
287 };
288 let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() };
289 let batch = SingleBatch { epoch_num: 1, timestamp: 1, ..Default::default() };
290 assert_eq!(
291 batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
292 BatchValidity::Drop
293 );
294 }
295
296 #[test]
297 fn test_check_batch_timestamp_accept() {
298 let cfg = RollupConfig::default();
299 let l2_safe_head = L2BlockInfo {
300 block_info: BlockInfo { timestamp: 2, ..Default::default() },
301 ..Default::default()
302 };
303 let inclusion_block = BlockInfo::default();
304 let batch = SingleBatch { timestamp: 2, ..Default::default() };
305 assert_eq!(
306 batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
307 BatchValidity::Accept
308 );
309 }
310
311 #[test]
312 fn test_roundtrip_encoding() {
313 use alloy_rlp::{Decodable, Encodable};
314 let batch = SingleBatch {
315 parent_hash: BlockHash::from([0x01; 32]),
316 epoch_num: 1,
317 epoch_hash: BlockHash::from([0x02; 32]),
318 timestamp: 1,
319 transactions: vec![Bytes::from(vec![0x01])],
320 };
321 let mut buf = vec![];
322 batch.encode(&mut buf);
323 let decoded = SingleBatch::decode(&mut buf.as_slice()).unwrap();
324 assert_eq!(batch, decoded);
325 }
326
327 #[test]
328 fn test_check_batch_succeeds() {
329 let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() };
330 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
331 let l2_safe_head = L2BlockInfo {
332 block_info: BlockInfo { timestamp: 1, ..Default::default() },
333 ..Default::default()
334 };
335 let inclusion_block = BlockInfo::default();
336 let batch = SingleBatch {
337 parent_hash: BlockHash::ZERO,
338 epoch_num: 1,
339 epoch_hash: BlockHash::ZERO,
340 timestamp: 1,
341 transactions: vec![Bytes::from(vec![0x01])],
342 };
343 assert_eq!(
344 batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
345 BatchValidity::Accept
346 );
347 }
348
349 fn eip_1559_tx() -> TxEip1559 {
350 TxEip1559 {
351 chain_id: 10u64,
352 nonce: 2,
353 max_fee_per_gas: 3,
354 max_priority_fee_per_gas: 4,
355 gas_limit: 5,
356 to: Address::left_padding_from(&[6]).into(),
357 value: U256::from(7_u64),
358 input: vec![8].into(),
359 access_list: Default::default(),
360 }
361 }
362
363 fn example_transactions() -> Vec<Bytes> {
364 let mut transactions = Vec::new();
365
366 let tx = eip_1559_tx();
368 let sig = PrimitiveSignature::test_signature();
369 let tx_signed = tx.into_signed(sig);
370 let envelope: TxEnvelope = tx_signed.into();
371 let encoded = envelope.encoded_2718();
372 transactions.push(encoded.clone().into());
373 let mut slice = encoded.as_slice();
374 let decoded = TxEnvelope::decode_2718(&mut slice).unwrap();
375 assert!(matches!(decoded, TxEnvelope::Eip1559(_)));
376
377 let mut tx = eip_1559_tx();
379 tx.to = Address::left_padding_from(&[7]).into();
380 let sig = PrimitiveSignature::test_signature();
381 let tx_signed = tx.into_signed(sig);
382 let envelope: TxEnvelope = tx_signed.into();
383 let encoded = envelope.encoded_2718();
384 transactions.push(encoded.clone().into());
385 let mut slice = encoded.as_slice();
386 let decoded = TxEnvelope::decode_2718(&mut slice).unwrap();
387 assert!(matches!(decoded, TxEnvelope::Eip1559(_)));
388
389 transactions
390 }
391
392 #[test]
393 fn test_check_batch_full_txs() {
394 let transactions = example_transactions();
396
397 let parent_hash = BlockHash::ZERO;
399 let epoch_num = 1;
400 let epoch_hash = BlockHash::ZERO;
401 let timestamp = 1;
402
403 let single_batch =
404 SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions };
405
406 let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() };
407 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
408 let l2_safe_head = L2BlockInfo {
409 block_info: BlockInfo { timestamp: 1, ..Default::default() },
410 ..Default::default()
411 };
412 let inclusion_block = BlockInfo::default();
413 assert_eq!(
414 single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
415 BatchValidity::Accept
416 );
417 }
418
419 fn eip_7702_tx() -> TxEip7702 {
420 TxEip7702 {
421 chain_id: 10u64,
422 nonce: 2,
423 gas_limit: 5,
424 max_fee_per_gas: 3,
425 max_priority_fee_per_gas: 4,
426 to: Address::left_padding_from(&[7]),
427 value: U256::from(7_u64),
428 input: vec![8].into(),
429 ..Default::default()
430 }
431 }
432
433 #[test]
434 fn test_check_batch_drop_7702_pre_isthmus() {
435 let mut transactions = example_transactions();
437
438 let eip_7702_tx = eip_7702_tx();
440 let sig = PrimitiveSignature::test_signature();
441 let tx_signed = eip_7702_tx.into_signed(sig);
442 let envelope: TxEnvelope = tx_signed.into();
443 let encoded = envelope.encoded_2718();
444 transactions.push(encoded.into());
445
446 let parent_hash = BlockHash::ZERO;
448 let epoch_num = 1;
449 let epoch_hash = BlockHash::ZERO;
450 let timestamp = 1;
451
452 let single_batch =
453 SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions };
454
455 let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() };
457 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
458 let l2_safe_head = L2BlockInfo {
459 block_info: BlockInfo { timestamp: 1, ..Default::default() },
460 ..Default::default()
461 };
462 let inclusion_block = BlockInfo::default();
463 assert_eq!(
464 single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
465 BatchValidity::Drop
466 );
467 }
468
469 #[test]
470 fn test_check_batch_accept_7702_post_isthmus() {
471 let mut transactions = example_transactions();
473
474 let eip_7702_tx = eip_7702_tx();
476 let sig = PrimitiveSignature::test_signature();
477 let tx_signed = eip_7702_tx.into_signed(sig);
478 let envelope: TxEnvelope = tx_signed.into();
479 let encoded = envelope.encoded_2718();
480 transactions.push(encoded.into());
481
482 let parent_hash = BlockHash::ZERO;
484 let epoch_num = 1;
485 let epoch_hash = BlockHash::ZERO;
486 let timestamp = 1;
487
488 let single_batch =
489 SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions };
490
491 let cfg = RollupConfig {
493 max_sequencer_drift: 1,
494 hardforks: HardForkConfig { isthmus_time: Some(0), ..Default::default() },
495 ..Default::default()
496 };
497 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
498 let l2_safe_head = L2BlockInfo {
499 block_info: BlockInfo { timestamp: 1, ..Default::default() },
500 ..Default::default()
501 };
502 let inclusion_block = BlockInfo::default();
503 assert_eq!(
504 single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
505 BatchValidity::Accept
506 );
507 }
508
509 #[test]
510 fn test_check_batch_drop_empty_tx() {
511 let transactions = vec![Default::default()];
514
515 let parent_hash = BlockHash::ZERO;
517 let epoch_num = 1;
518 let epoch_hash = BlockHash::ZERO;
519 let timestamp = 1;
520
521 let single_batch =
522 SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions };
523
524 let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() };
526 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
527 let l2_safe_head = L2BlockInfo {
528 block_info: BlockInfo { timestamp: 1, ..Default::default() },
529 ..Default::default()
530 };
531 let inclusion_block = BlockInfo::default();
532 assert_eq!(
533 single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
534 BatchValidity::Drop
535 );
536 }
537
538 #[test]
539 fn test_check_batch_drop_2718_deposit() {
540 let mut transactions = example_transactions();
542
543 let tx = TxDeposit {
545 source_hash: Default::default(),
546 from: Address::left_padding_from(&[7]),
547 to: TxKind::Create,
548 mint: None,
549 value: U256::from(7_u64),
550 gas_limit: 5,
551 is_system_transaction: false,
552 input: Default::default(),
553 };
554 let envelope = OpTxEnvelope::Deposit(Sealed::new(tx));
555 let encoded = envelope.encoded_2718();
556 transactions.push(encoded.into());
557
558 let parent_hash = BlockHash::ZERO;
560 let epoch_num = 1;
561 let epoch_hash = BlockHash::ZERO;
562 let timestamp = 1;
563
564 let single_batch =
565 SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions };
566
567 let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() };
569 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
570 let l2_safe_head = L2BlockInfo {
571 block_info: BlockInfo { timestamp: 1, ..Default::default() },
572 ..Default::default()
573 };
574 let inclusion_block = BlockInfo::default();
575 assert_eq!(
576 single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
577 BatchValidity::Drop
578 );
579 }
580}