op_alloy_protocol/batch/
single.rsuse crate::{starts_with_2718_deposit, BatchValidity, BlockInfo, L2BlockInfo};
use alloc::vec::Vec;
use alloy_eips::BlockNumHash;
use alloy_primitives::{BlockHash, Bytes};
use alloy_rlp::{RlpDecodable, RlpEncodable};
use op_alloy_genesis::RollupConfig;
#[derive(Debug, Default, RlpDecodable, RlpEncodable, Clone, PartialEq, Eq)]
pub struct SingleBatch {
pub parent_hash: BlockHash,
pub epoch_num: u64,
pub epoch_hash: BlockHash,
pub timestamp: u64,
pub transactions: Vec<Bytes>,
}
impl SingleBatch {
pub fn has_invalid_transactions(&self) -> bool {
self.transactions.iter().any(|tx| tx.0.is_empty() || tx.0[0] == 0x7E)
}
pub const fn epoch(&self) -> BlockNumHash {
BlockNumHash { number: self.epoch_num, hash: self.epoch_hash }
}
pub fn check_batch_timestamp(
&self,
cfg: &RollupConfig,
l2_safe_head: L2BlockInfo,
inclusion_block: &BlockInfo,
) -> BatchValidity {
let next_timestamp = l2_safe_head.block_info.timestamp + cfg.block_time;
if self.timestamp > next_timestamp {
if cfg.is_holocene_active(inclusion_block.timestamp) {
return BatchValidity::Drop;
}
return BatchValidity::Future;
}
if self.timestamp < next_timestamp {
if cfg.is_holocene_active(inclusion_block.timestamp) {
return BatchValidity::Past;
}
return BatchValidity::Drop;
}
BatchValidity::Accept
}
pub fn check_batch(
&self,
cfg: &RollupConfig,
l1_blocks: &[BlockInfo],
l2_safe_head: L2BlockInfo,
inclusion_block: &BlockInfo,
) -> BatchValidity {
if l1_blocks.is_empty() {
return BatchValidity::Undecided;
}
let epoch = l1_blocks[0];
let timestamp_check = self.check_batch_timestamp(cfg, l2_safe_head, inclusion_block);
if !timestamp_check.is_accept() {
return timestamp_check;
}
if self.parent_hash != l2_safe_head.block_info.hash {
return BatchValidity::Drop;
}
if self.epoch_num + cfg.seq_window_size < inclusion_block.number {
return BatchValidity::Drop;
}
let mut batch_origin = epoch;
if self.epoch_num < epoch.number {
return BatchValidity::Drop;
} else if self.epoch_num == epoch.number {
} else if self.epoch_num == epoch.number + 1 {
if l1_blocks.len() < 2 {
return BatchValidity::Undecided;
}
batch_origin = l1_blocks[1];
} else {
return BatchValidity::Drop;
}
if self.epoch_hash != batch_origin.hash {
return BatchValidity::Drop;
}
if self.timestamp < batch_origin.timestamp {
return BatchValidity::Drop;
}
let max_drift = cfg.max_sequencer_drift(batch_origin.timestamp);
let max = if let Some(max) = batch_origin.timestamp.checked_add(max_drift) {
max
} else {
return BatchValidity::Drop;
};
let no_txs = self.transactions.is_empty();
if self.timestamp > max && !no_txs {
return BatchValidity::Drop;
}
if self.timestamp > max && no_txs {
if epoch.number == batch_origin.number {
if l1_blocks.len() < 2 {
return BatchValidity::Undecided;
}
let next_origin = l1_blocks[1];
if self.timestamp >= next_origin.timestamp {
return BatchValidity::Drop;
}
}
}
for tx in self.transactions.iter() {
if tx.is_empty() {
return BatchValidity::Drop;
}
if starts_with_2718_deposit(tx) {
return BatchValidity::Drop;
}
}
BatchValidity::Accept
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_batch_timestamp_holocene_inactive_future() {
let cfg = RollupConfig::default();
let l2_safe_head = L2BlockInfo {
block_info: BlockInfo { timestamp: 1, ..Default::default() },
..Default::default()
};
let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() };
let batch = SingleBatch { epoch_num: 1, timestamp: 2, ..Default::default() };
assert_eq!(
batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
BatchValidity::Future
);
}
#[test]
fn test_check_batch_timestamp_holocene_active_drop() {
let cfg = RollupConfig { holocene_time: Some(0), ..Default::default() };
let l2_safe_head = L2BlockInfo {
block_info: BlockInfo { timestamp: 1, ..Default::default() },
..Default::default()
};
let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() };
let batch = SingleBatch { epoch_num: 1, timestamp: 2, ..Default::default() };
assert_eq!(
batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
BatchValidity::Drop
);
}
#[test]
fn test_check_batch_timestamp_holocene_active_past() {
let cfg = RollupConfig { holocene_time: Some(0), ..Default::default() };
let l2_safe_head = L2BlockInfo {
block_info: BlockInfo { timestamp: 2, ..Default::default() },
..Default::default()
};
let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() };
let batch = SingleBatch { epoch_num: 1, timestamp: 1, ..Default::default() };
assert_eq!(
batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
BatchValidity::Past
);
}
#[test]
fn test_check_batch_timestamp_holocene_inactive_drop() {
let cfg = RollupConfig::default();
let l2_safe_head = L2BlockInfo {
block_info: BlockInfo { timestamp: 2, ..Default::default() },
..Default::default()
};
let inclusion_block = BlockInfo { timestamp: 1, ..Default::default() };
let batch = SingleBatch { epoch_num: 1, timestamp: 1, ..Default::default() };
assert_eq!(
batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
BatchValidity::Drop
);
}
#[test]
fn test_check_batch_timestamp_accept() {
let cfg = RollupConfig::default();
let l2_safe_head = L2BlockInfo {
block_info: BlockInfo { timestamp: 2, ..Default::default() },
..Default::default()
};
let inclusion_block = BlockInfo::default();
let batch = SingleBatch { timestamp: 2, ..Default::default() };
assert_eq!(
batch.check_batch_timestamp(&cfg, l2_safe_head, &inclusion_block),
BatchValidity::Accept
);
}
}