1use crate::{BatchValidity, BlockInfo, L2BlockInfo};
4use alloc::vec::Vec;
5use alloy_eips::BlockNumHash;
6use alloy_primitives::{BlockHash, Bytes};
7use alloy_rlp::{RlpDecodable, RlpEncodable};
8use kona_genesis::RollupConfig;
9use op_alloy_consensus::OpTxType;
10use tracing::warn;
11
12#[derive(Debug, Default, RlpDecodable, RlpEncodable, Clone, PartialEq, Eq)]
14pub struct SingleBatch {
15 pub parent_hash: BlockHash,
18 pub epoch_num: u64,
20 pub epoch_hash: BlockHash,
22 pub timestamp: u64,
24 pub transactions: Vec<Bytes>,
26}
27
28impl SingleBatch {
29 pub fn has_invalid_transactions(&self) -> bool {
31 self.transactions.iter().any(|tx| tx.0.is_empty() || tx.0[0] == OpTxType::Deposit as u8)
32 }
33
34 pub const fn epoch(&self) -> BlockNumHash {
36 BlockNumHash { number: self.epoch_num, hash: self.epoch_hash }
37 }
38
39 pub fn check_batch_timestamp(
41 &self,
42 cfg: &RollupConfig,
43 l2_safe_head: L2BlockInfo,
44 inclusion_block: &BlockInfo,
45 ) -> BatchValidity {
46 let next_timestamp = l2_safe_head.block_info.timestamp + cfg.block_time;
47 if self.timestamp > next_timestamp {
48 if cfg.is_holocene_active(inclusion_block.timestamp) {
49 return BatchValidity::Drop;
50 }
51 return BatchValidity::Future;
52 }
53 if self.timestamp < next_timestamp {
54 if cfg.is_holocene_active(inclusion_block.timestamp) {
55 return BatchValidity::Past;
56 }
57 return BatchValidity::Drop;
58 }
59 BatchValidity::Accept
60 }
61
62 pub fn check_batch(
68 &self,
69 cfg: &RollupConfig,
70 l1_blocks: &[BlockInfo],
71 l2_safe_head: L2BlockInfo,
72 inclusion_block: &BlockInfo,
73 ) -> BatchValidity {
74 if l1_blocks.is_empty() {
76 return BatchValidity::Undecided;
77 }
78
79 let epoch = l1_blocks[0];
80
81 let timestamp_check = self.check_batch_timestamp(cfg, l2_safe_head, inclusion_block);
83 if !timestamp_check.is_accept() {
84 return timestamp_check;
85 }
86
87 if self.parent_hash != l2_safe_head.block_info.hash {
90 return BatchValidity::Drop;
91 }
92
93 if self.epoch_num + cfg.seq_window_size < inclusion_block.number {
95 return BatchValidity::Drop;
96 }
97
98 let mut batch_origin = epoch;
100 if self.epoch_num < epoch.number {
101 return BatchValidity::Drop;
102 } else if self.epoch_num == epoch.number {
103 } else if self.epoch_num == epoch.number + 1 {
105 if l1_blocks.len() < 2 {
111 return BatchValidity::Undecided;
112 }
113 batch_origin = l1_blocks[1];
114 } else {
115 return BatchValidity::Drop;
116 }
117
118 if self.epoch_hash != batch_origin.hash {
120 return BatchValidity::Drop;
121 }
122
123 if self.timestamp < batch_origin.timestamp {
124 return BatchValidity::Drop;
125 }
126
127 let max_drift = cfg.max_sequencer_drift(batch_origin.timestamp);
129 let max = if let Some(max) = batch_origin.timestamp.checked_add(max_drift) {
130 max
131 } else {
132 return BatchValidity::Drop;
133 };
134
135 let no_txs = self.transactions.is_empty();
136 if self.timestamp > max && !no_txs {
137 return BatchValidity::Drop;
141 }
142 if self.timestamp > max && no_txs {
143 if epoch.number == batch_origin.number {
148 if l1_blocks.len() < 2 {
149 return BatchValidity::Undecided;
150 }
151 let next_origin = l1_blocks[1];
152 if self.timestamp >= next_origin.timestamp {
154 return BatchValidity::Drop;
155 }
156 }
157 }
158
159 if cfg.is_first_interop_block(self.timestamp) && !self.transactions.is_empty() {
162 warn!(
163 target: "single_batch",
164 "Sequencer included user transactions in interop transition block. Dropping batch."
165 );
166 return BatchValidity::Drop;
167 }
168
169 for tx in self.transactions.iter() {
171 if tx.is_empty() {
172 return BatchValidity::Drop;
173 }
174 if tx.as_ref().first() == Some(&(OpTxType::Deposit as u8)) {
175 return BatchValidity::Drop;
176 }
177 if !cfg.is_isthmus_active(self.timestamp) &&
179 tx.as_ref().first() == Some(&(OpTxType::Eip7702 as u8))
180 {
181 return BatchValidity::Drop;
182 }
183 }
184
185 BatchValidity::Accept
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use crate::test_utils::{CollectingLayer, TraceStorage};
192
193 use super::*;
194 use alloc::vec;
195 use alloy_consensus::{SignableTransaction, TxEip1559, TxEip7702, TxEnvelope};
196 use alloy_eips::eip2718::{Decodable2718, Encodable2718};
197 use alloy_primitives::{Address, Sealed, Signature, TxKind, U256};
198 use kona_genesis::HardForkConfig;
199 use op_alloy_consensus::{OpTxEnvelope, TxDeposit};
200 use tracing::Level;
201 use tracing_subscriber::layer::SubscriberExt;
202
203 #[test]
204 fn test_empty_l1_blocks() {
205 let cfg = RollupConfig::default();
206 let l1_blocks = vec![];
207 let l2_safe_head = L2BlockInfo::default();
208 let inclusion_block = BlockInfo::default();
209 let batch = SingleBatch::default();
210 assert_eq!(
211 batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
212 BatchValidity::Undecided
213 );
214 }
215
216 #[test]
217 fn test_timestamp_future() {
218 let cfg = RollupConfig::default();
219 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
220 let l2_safe_head = L2BlockInfo {
221 block_info: BlockInfo { timestamp: 1, ..Default::default() },
222 ..Default::default()
223 };
224 let inclusion_block = BlockInfo::default();
225 let batch = SingleBatch { timestamp: 2, ..Default::default() };
226 assert_eq!(
227 batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
228 BatchValidity::Future
229 );
230 }
231
232 #[test]
233 fn test_parent_hash_mismatch() {
234 let cfg = RollupConfig::default();
235 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
236 let l2_safe_head = L2BlockInfo {
237 block_info: BlockInfo { hash: BlockHash::from([0x01; 32]), ..Default::default() },
238 ..Default::default()
239 };
240 let inclusion_block = BlockInfo::default();
241 let batch = SingleBatch { parent_hash: BlockHash::from([0x02; 32]), ..Default::default() };
242 assert_eq!(
243 batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
244 BatchValidity::Drop
245 );
246 }
247
248 #[test]
249 fn test_check_batch_timestamp_holocene_inactive_future() {
250 let cfg = RollupConfig::default();
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::Future
260 );
261 }
262
263 #[test]
264 fn test_check_batch_timestamp_holocene_active_drop() {
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: 1, ..Default::default() },
271 ..Default::default()
272 };
273 let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() };
274 let batch = SingleBatch { epoch_num: 1, timestamp: 2, ..Default::default() };
275 assert_eq!(
276 batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
277 BatchValidity::Drop
278 );
279 }
280
281 #[test]
282 fn test_check_batch_timestamp_holocene_active_past() {
283 let cfg = RollupConfig {
284 hardforks: HardForkConfig { holocene_time: Some(0), ..Default::default() },
285 ..Default::default()
286 };
287 let l2_safe_head = L2BlockInfo {
288 block_info: BlockInfo { timestamp: 2, ..Default::default() },
289 ..Default::default()
290 };
291 let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() };
292 let batch = SingleBatch { epoch_num: 1, timestamp: 1, ..Default::default() };
293 assert_eq!(
294 batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
295 BatchValidity::Past
296 );
297 }
298
299 #[test]
300 fn test_check_batch_timestamp_holocene_inactive_drop() {
301 let cfg = RollupConfig::default();
302 let l2_safe_head = L2BlockInfo {
303 block_info: BlockInfo { timestamp: 2, ..Default::default() },
304 ..Default::default()
305 };
306 let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() };
307 let batch = SingleBatch { epoch_num: 1, timestamp: 1, ..Default::default() };
308 assert_eq!(
309 batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
310 BatchValidity::Drop
311 );
312 }
313
314 #[test]
315 fn test_check_batch_timestamp_accept() {
316 let cfg = RollupConfig::default();
317 let l2_safe_head = L2BlockInfo {
318 block_info: BlockInfo { timestamp: 2, ..Default::default() },
319 ..Default::default()
320 };
321 let inclusion_block = BlockInfo::default();
322 let batch = SingleBatch { timestamp: 2, ..Default::default() };
323 assert_eq!(
324 batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
325 BatchValidity::Accept
326 );
327 }
328
329 #[test]
330 fn test_roundtrip_encoding() {
331 use alloy_rlp::{Decodable, Encodable};
332 let batch = SingleBatch {
333 parent_hash: BlockHash::from([0x01; 32]),
334 epoch_num: 1,
335 epoch_hash: BlockHash::from([0x02; 32]),
336 timestamp: 1,
337 transactions: vec![Bytes::from(vec![0x01])],
338 };
339 let mut buf = vec![];
340 batch.encode(&mut buf);
341 let decoded = SingleBatch::decode(&mut buf.as_slice()).unwrap();
342 assert_eq!(batch, decoded);
343 }
344
345 #[test]
346 fn test_check_batch_succeeds() {
347 let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() };
348 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
349 let l2_safe_head = L2BlockInfo {
350 block_info: BlockInfo { timestamp: 1, ..Default::default() },
351 ..Default::default()
352 };
353 let inclusion_block = BlockInfo::default();
354 let batch = SingleBatch {
355 parent_hash: BlockHash::ZERO,
356 epoch_num: 1,
357 epoch_hash: BlockHash::ZERO,
358 timestamp: 1,
359 transactions: vec![Bytes::from(vec![0x01])],
360 };
361 assert_eq!(
362 batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
363 BatchValidity::Accept
364 );
365 }
366
367 fn eip_1559_tx() -> TxEip1559 {
368 TxEip1559 {
369 chain_id: 10u64,
370 nonce: 2,
371 max_fee_per_gas: 3,
372 max_priority_fee_per_gas: 4,
373 gas_limit: 5,
374 to: Address::left_padding_from(&[6]).into(),
375 value: U256::from(7_u64),
376 input: vec![8].into(),
377 access_list: Default::default(),
378 }
379 }
380
381 fn example_transactions() -> Vec<Bytes> {
382 let mut transactions = Vec::new();
383
384 let tx = eip_1559_tx();
386 let sig = Signature::test_signature();
387 let tx_signed = tx.into_signed(sig);
388 let envelope: TxEnvelope = tx_signed.into();
389 let encoded = envelope.encoded_2718();
390 transactions.push(encoded.clone().into());
391 let mut slice = encoded.as_slice();
392 let decoded = TxEnvelope::decode_2718(&mut slice).unwrap();
393 assert!(matches!(decoded, TxEnvelope::Eip1559(_)));
394
395 let mut tx = eip_1559_tx();
397 tx.to = Address::left_padding_from(&[7]).into();
398 let sig = Signature::test_signature();
399 let tx_signed = tx.into_signed(sig);
400 let envelope: TxEnvelope = tx_signed.into();
401 let encoded = envelope.encoded_2718();
402 transactions.push(encoded.clone().into());
403 let mut slice = encoded.as_slice();
404 let decoded = TxEnvelope::decode_2718(&mut slice).unwrap();
405 assert!(matches!(decoded, TxEnvelope::Eip1559(_)));
406
407 transactions
408 }
409
410 #[test]
411 fn test_check_batch_full_txs() {
412 let transactions = example_transactions();
414
415 let parent_hash = BlockHash::ZERO;
417 let epoch_num = 1;
418 let epoch_hash = BlockHash::ZERO;
419 let timestamp = 1;
420
421 let single_batch =
422 SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions };
423
424 let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() };
425 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
426 let l2_safe_head = L2BlockInfo {
427 block_info: BlockInfo { timestamp: 1, ..Default::default() },
428 ..Default::default()
429 };
430 let inclusion_block = BlockInfo::default();
431 assert_eq!(
432 single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
433 BatchValidity::Accept
434 );
435 }
436
437 fn eip_7702_tx() -> TxEip7702 {
438 TxEip7702 {
439 chain_id: 10u64,
440 nonce: 2,
441 gas_limit: 5,
442 max_fee_per_gas: 3,
443 max_priority_fee_per_gas: 4,
444 to: Address::left_padding_from(&[7]),
445 value: U256::from(7_u64),
446 input: vec![8].into(),
447 ..Default::default()
448 }
449 }
450
451 #[test]
452 fn test_check_batch_drop_7702_pre_isthmus() {
453 let mut transactions = example_transactions();
455
456 let eip_7702_tx = eip_7702_tx();
458 let sig = Signature::test_signature();
459 let tx_signed = eip_7702_tx.into_signed(sig);
460 let envelope: TxEnvelope = tx_signed.into();
461 let encoded = envelope.encoded_2718();
462 transactions.push(encoded.into());
463
464 let parent_hash = BlockHash::ZERO;
466 let epoch_num = 1;
467 let epoch_hash = BlockHash::ZERO;
468 let timestamp = 1;
469
470 let single_batch =
471 SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions };
472
473 let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() };
475 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
476 let l2_safe_head = L2BlockInfo {
477 block_info: BlockInfo { timestamp: 1, ..Default::default() },
478 ..Default::default()
479 };
480 let inclusion_block = BlockInfo::default();
481 assert_eq!(
482 single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
483 BatchValidity::Drop
484 );
485 }
486
487 #[test]
488 fn test_check_batch_accept_7702_post_isthmus() {
489 let mut transactions = example_transactions();
491
492 let eip_7702_tx = eip_7702_tx();
494 let sig = Signature::test_signature();
495 let tx_signed = eip_7702_tx.into_signed(sig);
496 let envelope: TxEnvelope = tx_signed.into();
497 let encoded = envelope.encoded_2718();
498 transactions.push(encoded.into());
499
500 let parent_hash = BlockHash::ZERO;
502 let epoch_num = 1;
503 let epoch_hash = BlockHash::ZERO;
504 let timestamp = 1;
505
506 let single_batch =
507 SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions };
508
509 let cfg = RollupConfig {
511 max_sequencer_drift: 1,
512 hardforks: HardForkConfig { isthmus_time: Some(0), ..Default::default() },
513 ..Default::default()
514 };
515 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
516 let l2_safe_head = L2BlockInfo {
517 block_info: BlockInfo { timestamp: 1, ..Default::default() },
518 ..Default::default()
519 };
520 let inclusion_block = BlockInfo::default();
521 assert_eq!(
522 single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
523 BatchValidity::Accept
524 );
525 }
526
527 #[test]
528 fn test_check_batch_drop_empty_tx() {
529 let transactions = vec![Default::default()];
532
533 let parent_hash = BlockHash::ZERO;
535 let epoch_num = 1;
536 let epoch_hash = BlockHash::ZERO;
537 let timestamp = 1;
538
539 let single_batch =
540 SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions };
541
542 let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() };
544 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
545 let l2_safe_head = L2BlockInfo {
546 block_info: BlockInfo { timestamp: 1, ..Default::default() },
547 ..Default::default()
548 };
549 let inclusion_block = BlockInfo::default();
550 assert_eq!(
551 single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
552 BatchValidity::Drop
553 );
554 }
555
556 #[test]
557 fn test_check_batch_drop_2718_deposit() {
558 let mut transactions = example_transactions();
560
561 let tx = TxDeposit {
563 source_hash: Default::default(),
564 from: Address::left_padding_from(&[7]),
565 to: TxKind::Create,
566 mint: 0,
567 value: U256::from(7_u64),
568 gas_limit: 5,
569 is_system_transaction: false,
570 input: Default::default(),
571 };
572 let envelope = OpTxEnvelope::Deposit(Sealed::new(tx));
573 let encoded = envelope.encoded_2718();
574 transactions.push(encoded.into());
575
576 let parent_hash = BlockHash::ZERO;
578 let epoch_num = 1;
579 let epoch_hash = BlockHash::ZERO;
580 let timestamp = 1;
581
582 let single_batch =
583 SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions };
584
585 let cfg = RollupConfig { max_sequencer_drift: 1, ..Default::default() };
587 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
588 let l2_safe_head = L2BlockInfo {
589 block_info: BlockInfo { timestamp: 1, ..Default::default() },
590 ..Default::default()
591 };
592 let inclusion_block = BlockInfo::default();
593 assert_eq!(
594 single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
595 BatchValidity::Drop
596 );
597 }
598
599 #[test]
600 fn test_check_batch_drop_non_empty_interop_transition() {
601 let trace_store: TraceStorage = Default::default();
602 let layer = CollectingLayer::new(trace_store.clone());
603 let subscriber = tracing_subscriber::Registry::default().with(layer);
604 let _guard = tracing::subscriber::set_default(subscriber);
605
606 let transactions = example_transactions();
608
609 let parent_hash = BlockHash::ZERO;
611 let epoch_num = 1;
612 let epoch_hash = BlockHash::ZERO;
613 let timestamp = 1;
614
615 let single_batch =
616 SingleBatch { parent_hash, epoch_num, epoch_hash, timestamp, transactions };
617
618 let cfg = RollupConfig {
619 max_sequencer_drift: 1,
620 block_time: 1,
621 hardforks: HardForkConfig { interop_time: Some(1), ..Default::default() },
622 ..Default::default()
623 };
624 let l1_blocks = vec![BlockInfo::default(), BlockInfo::default()];
625 let l2_safe_head = L2BlockInfo {
626 block_info: BlockInfo { timestamp: 0, ..Default::default() },
627 ..Default::default()
628 };
629 let inclusion_block = BlockInfo::default();
630 assert_eq!(
631 single_batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block),
632 BatchValidity::Drop
633 );
634
635 assert!(trace_store.get_by_level(Level::WARN).iter().any(|s| {
636 s.contains("Sequencer included user transactions in interop transition block.")
637 }))
638 }
639}