kona_protocol/batch/
single.rs

1//! This module contains the [`SingleBatch`] type.
2
3use 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/// Represents a single batch: a single encoded L2 block
13#[derive(Debug, Default, RlpDecodable, RlpEncodable, Clone, PartialEq, Eq)]
14pub struct SingleBatch {
15    /// Block hash of the previous L2 block. `B256::ZERO` if it has not been set by the Batch
16    /// Queue.
17    pub parent_hash: BlockHash,
18    /// The batch epoch number. Same as the first L1 block number in the epoch.
19    pub epoch_num: u64,
20    /// The block hash of the first L1 block in the epoch
21    pub epoch_hash: BlockHash,
22    /// The L2 block timestamp of this batch
23    pub timestamp: u64,
24    /// The L2 block transactions in this batch
25    pub transactions: Vec<Bytes>,
26}
27
28impl SingleBatch {
29    /// If any transactions are empty or deposited transaction types.
30    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    /// Returns the [`BlockNumHash`] of the batch.
35    pub const fn epoch(&self) -> BlockNumHash {
36        BlockNumHash { number: self.epoch_num, hash: self.epoch_hash }
37    }
38
39    /// Validate the batch timestamp.
40    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    /// Checks if the batch is valid.
63    ///
64    /// The batch format type is defined in the [OP Stack Specs][specs].
65    ///
66    /// [specs]: https://specs.optimism.io/protocol/derivation.html#batch-format
67    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        // Cannot have empty l1_blocks for batch validation.
75        if l1_blocks.is_empty() {
76            return BatchValidity::Undecided;
77        }
78
79        let epoch = l1_blocks[0];
80
81        // If the batch is not accepted by the timestamp check, return the result.
82        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        // Dependent on the above timestamp check.
88        // If the timestamp is correct, then it must build on top of the safe head.
89        if self.parent_hash != l2_safe_head.block_info.hash {
90            return BatchValidity::Drop;
91        }
92
93        // Filter out batches that were included too late.
94        if self.epoch_num + cfg.seq_window_size < inclusion_block.number {
95            return BatchValidity::Drop;
96        }
97
98        // Check the L1 origin of the batch
99        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            // Batch is sticking to the current epoch, continue.
104        } else if self.epoch_num == epoch.number + 1 {
105            // With only 1 l1Block we cannot look at the next L1 Origin.
106            // Note: This means that we are unable to determine validity of a batch
107            // without more information. In this case we should bail out until we have
108            // more information otherwise the eager algorithm may diverge from a non-eager
109            // algorithm.
110            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        // Validate the batch epoch hash
119        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        // Check if we ran out of sequencer time drift
128        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            // If the sequencer is ignoring the time drift rule, then drop the batch and force an
138            // empty batch instead, as the sequencer is not allowed to include anything
139            // past this point without moving to the next epoch.
140            return BatchValidity::Drop;
141        }
142        if self.timestamp > max && no_txs {
143            // If the sequencer is co-operating by producing an empty batch,
144            // allow the batch if it was the right thing to do to maintain the L2 time >= L1 time
145            // invariant. Only check batches that do not advance the epoch, to ensure
146            // epoch advancement regardless of time drift is allowed.
147            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                // Check if the next L1 Origin could have been adopted
153                if self.timestamp >= next_origin.timestamp {
154                    return BatchValidity::Drop;
155                }
156            }
157        }
158
159        // If this is the first block in the interop hardfork, and the batch contains any
160        // transactions, it must be dropped.
161        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        // We can do this check earlier, but it's intensive so we do it last for the sad-path.
170        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 isthmus is not active yet and the transaction is a 7702, drop the batch.
178            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        // First Transaction in the batch.
385        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        // Second transaction in the batch.
396        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        // Use the example transaction
413        let transactions = example_transactions();
414
415        // Construct a basic `SingleBatch`
416        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        // Use the example transaction
454        let mut transactions = example_transactions();
455
456        // Extend the transactions with the 7702 transaction
457        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        // Construct a basic `SingleBatch`
465        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        // Notice: Isthmus is _not_ active yet.
474        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        // Use the example transaction
490        let mut transactions = example_transactions();
491
492        // Extend the transactions with the 7702 transaction
493        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        // Construct a basic `SingleBatch`
501        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        // Notice: Isthmus is active.
510        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        // An empty tx is not valid 2718 encoding.
530        // The batch must be dropped.
531        let transactions = vec![Default::default()];
532
533        // Construct a basic `SingleBatch`
534        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        // Notice: Isthmus is _not_ active yet.
543        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        // Add a 2718 deposit transaction to the batch.
559        let mut transactions = example_transactions();
560
561        // Extend the transactions with the 2718 deposit transaction
562        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        // Construct a basic `SingleBatch`
577        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        // Notice: Isthmus is _not_ active yet.
586        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        // Gather a few test transactions for the batch.
607        let transactions = example_transactions();
608
609        // Construct a basic `SingleBatch`
610        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}