kona_protocol/batch/
single.rs

1//! This module contains the [SingleBatch] type.
2
3use 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/// Represents a single batch: a single encoded L2 block
11#[derive(Debug, Default, RlpDecodable, RlpEncodable, Clone, PartialEq, Eq)]
12pub struct SingleBatch {
13    /// Block hash of the previous L2 block. `B256::ZERO` if it has not been set by the Batch
14    /// Queue.
15    pub parent_hash: BlockHash,
16    /// The batch epoch number. Same as the first L1 block number in the epoch.
17    pub epoch_num: u64,
18    /// The block hash of the first L1 block in the epoch
19    pub epoch_hash: BlockHash,
20    /// The L2 block timestamp of this batch
21    pub timestamp: u64,
22    /// The L2 block transactions in this batch
23    pub transactions: Vec<Bytes>,
24}
25
26impl SingleBatch {
27    /// If any transactions are empty or deposited transaction types.
28    pub fn has_invalid_transactions(&self) -> bool {
29        self.transactions.iter().any(|tx| tx.0.is_empty() || tx.0[0] == 0x7E)
30    }
31
32    /// Returns the [BlockNumHash] of the batch.
33    pub const fn epoch(&self) -> BlockNumHash {
34        BlockNumHash { number: self.epoch_num, hash: self.epoch_hash }
35    }
36
37    /// Validate the batch timestamp.
38    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    /// Checks if the batch is valid.
61    ///
62    /// The batch format type is defined in the [OP Stack Specs][specs].
63    ///
64    /// [specs]: https://specs.optimism.io/protocol/derivation.html#batch-format
65    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        // Cannot have empty l1_blocks for batch validation.
73        if l1_blocks.is_empty() {
74            return BatchValidity::Undecided;
75        }
76
77        let epoch = l1_blocks[0];
78
79        // If the batch is not accepted by the timestamp check, return the result.
80        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        // Dependent on the above timestamp check.
86        // If the timestamp is correct, then it must build on top of the safe head.
87        if self.parent_hash != l2_safe_head.block_info.hash {
88            return BatchValidity::Drop;
89        }
90
91        // Filter out batches that were included too late.
92        if self.epoch_num + cfg.seq_window_size < inclusion_block.number {
93            return BatchValidity::Drop;
94        }
95
96        // Check the L1 origin of the batch
97        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            // Batch is sticking to the current epoch, continue.
102        } else if self.epoch_num == epoch.number + 1 {
103            // With only 1 l1Block we cannot look at the next L1 Origin.
104            // Note: This means that we are unable to determine validity of a batch
105            // without more information. In this case we should bail out until we have
106            // more information otherwise the eager algorithm may diverge from a non-eager
107            // algorithm.
108            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        // Validate the batch epoch hash
117        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        // Check if we ran out of sequencer time drift
126        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            // If the sequencer is ignoring the time drift rule, then drop the batch and force an
136            // empty batch instead, as the sequencer is not allowed to include anything
137            // past this point without moving to the next epoch.
138            return BatchValidity::Drop;
139        }
140        if self.timestamp > max && no_txs {
141            // If the sequencer is co-operating by producing an empty batch,
142            // allow the batch if it was the right thing to do to maintain the L2 time >= L1 time
143            // invariant. Only check batches that do not advance the epoch, to ensure
144            // epoch advancement regardless of time drift is allowed.
145            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                // Check if the next L1 Origin could have been adopted
151                if self.timestamp >= next_origin.timestamp {
152                    return BatchValidity::Drop;
153                }
154            }
155        }
156
157        // We can do this check earlier, but it's intensive so we do it last for the sad-path.
158        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 isthmus is not active yet and the transaction is a 7702, drop the batch.
166            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        // First Transaction in the batch.
367        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        // Second transaction in the batch.
378        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        // Use the example transaction
395        let transactions = example_transactions();
396
397        // Construct a basic `SingleBatch`
398        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        // Use the example transaction
436        let mut transactions = example_transactions();
437
438        // Extend the transactions with the 7702 transaction
439        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        // Construct a basic `SingleBatch`
447        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        // Notice: Isthmus is _not_ active yet.
456        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        // Use the example transaction
472        let mut transactions = example_transactions();
473
474        // Extend the transactions with the 7702 transaction
475        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        // Construct a basic `SingleBatch`
483        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        // Notice: Isthmus is active.
492        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        // An empty tx is not valid 2718 encoding.
512        // The batch must be dropped.
513        let transactions = vec![Default::default()];
514
515        // Construct a basic `SingleBatch`
516        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        // Notice: Isthmus is _not_ active yet.
525        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        // Add a 2718 deposit transaction to the batch.
541        let mut transactions = example_transactions();
542
543        // Extend the transactions with the 2718 deposit transaction
544        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        // Construct a basic `SingleBatch`
559        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        // Notice: Isthmus is _not_ active yet.
568        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}