diff --git a/Cargo.toml b/Cargo.toml index e7683e61f..17e9036cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,12 @@ thiserror = "1.0" proptest = "1.5" proptest-derive = "0.5" tokio = "1" +async-trait = "0.1.83" +unsigned-varint = "0.8.0" + +# tracing +tracing-subscriber = "0.3.18" +tracing = { version = "0.1.40", default-features = false } ## crypto c-kzg = { version = "1.0", default-features = false } diff --git a/crates/protocol/Cargo.toml b/crates/protocol/Cargo.toml index 59d66138f..ef02108a6 100644 --- a/crates/protocol/Cargo.toml +++ b/crates/protocol/Cargo.toml @@ -26,7 +26,10 @@ alloy-eips.workspace = true alloy-consensus.workspace = true # Misc +tracing.workspace = true +async-trait.workspace = true derive_more.workspace = true +unsigned-varint.workspace = true # `arbitrary` feature arbitrary = { workspace = true, features = ["derive"], optional = true } @@ -35,16 +38,28 @@ arbitrary = { workspace = true, features = ["derive"], optional = true } serde = { workspace = true, optional = true } alloy-serde = { workspace = true, optional = true } +# `test-utils` feature +spin = { workspace = true, optional = true } +tracing-subscriber = { workspace = true, features = ["fmt"], optional = true } + [dev-dependencies] +proptest.workspace = true +tokio = { workspace = true, features = ["full"] } +tracing-subscriber = { workspace = true, features = ["fmt"] } arbitrary = { workspace = true, features = ["derive"] } rand.workspace = true +spin.workspace = true serde_json.workspace = true -rstest = "0.22.0" +rstest = "0.23.0" revm = "14.0.2" alloy-sol-types.workspace = true [features] default = ["serde", "std"] +test-utils = [ + "dep:spin", + "dep:tracing-subscriber", +] arbitrary = ["std", "dep:arbitrary", "alloy-consensus/arbitrary", "alloy-eips/arbitrary", "alloy-primitives/rand"] std = ["op-alloy-consensus/std", "op-alloy-genesis/std"] serde = ["dep:serde", "dep:alloy-serde", "op-alloy-consensus/serde", "op-alloy-genesis/serde"] diff --git a/crates/protocol/src/batch.rs b/crates/protocol/src/batch.rs new file mode 100644 index 000000000..2b1163a53 --- /dev/null +++ b/crates/protocol/src/batch.rs @@ -0,0 +1,85 @@ +//! Batch Types +//! +//! This module contains the batch types for the OP Stack derivation pipeline. +//! +//! ## Batch +//! +//! A batch is either a `SpanBatch` or a `SingleBatch`. +//! +//! The batch type is encoded as a single byte: +//! - `0x00` for a `SingleBatch` +//! - `0x01` for a `SpanBatch` +//! +//! ### Single Batch +//! +//! // TODO +//! +//! ### Span Batch +//! +//! // TODO + +use alloy_rlp::{Decodable, Encodable}; + +/// The single batch type identifier. +pub(crate) const SINGLE_BATCH_TYPE: u8 = 0x00; + +/// The span batch type identifier. +pub(crate) const SPAN_BATCH_TYPE: u8 = 0x01; + +/// The Batch Type. +#[derive(Debug, Clone, PartialEq, Eq)] +#[repr(u8)] +pub enum BatchType { + /// Single Batch. + Single = SINGLE_BATCH_TYPE, + /// Span Batch. + Span = SPAN_BATCH_TYPE, +} + +impl From for BatchType { + fn from(val: u8) -> Self { + match val { + SINGLE_BATCH_TYPE => Self::Single, + SPAN_BATCH_TYPE => Self::Span, + _ => panic!("Invalid batch type: {val}"), + } + } +} + +impl From<&[u8]> for BatchType { + fn from(buf: &[u8]) -> Self { + Self::from(buf[0]) + } +} + +impl Encodable for BatchType { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + let val = match self { + Self::Single => SINGLE_BATCH_TYPE, + Self::Span => SPAN_BATCH_TYPE, + }; + val.encode(out); + } +} + +impl Decodable for BatchType { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let val = u8::decode(buf)?; + Ok(Self::from(val)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::vec::Vec; + + #[test] + fn test_batch_type_rlp_roundtrip() { + let batch_type = BatchType::Single; + let mut buf = Vec::new(); + batch_type.encode(&mut buf); + let decoded = BatchType::decode(&mut buf.as_slice()).unwrap(); + assert_eq!(batch_type, decoded); + } +} diff --git a/crates/protocol/src/bits.rs b/crates/protocol/src/bits.rs new file mode 100644 index 000000000..8d1655468 --- /dev/null +++ b/crates/protocol/src/bits.rs @@ -0,0 +1,224 @@ +//! Module for working with span batch bits. + +use crate::SpanBatchError; +use alloc::{vec, vec::Vec}; +use alloy_rlp::Buf; +use core::cmp::Ordering; + +/// Type for span batch bits. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct SpanBatchBits(pub Vec); + +impl AsRef<[u8]> for SpanBatchBits { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl SpanBatchBits { + /// Decodes a standard span-batch bitlist from a reader. + /// The bitlist is encoded as big-endian integer, left-padded with zeroes to a multiple of 8 + /// bits. The encoded bitlist cannot be longer than `bit_length`. + pub fn decode(b: &mut &[u8], bit_length: usize) -> Result { + let buffer_len = bit_length / 8 + if bit_length % 8 != 0 { 1 } else { 0 }; + let bits = if b.len() < buffer_len { + let mut bits = vec![0; buffer_len]; + bits[..b.len()].copy_from_slice(b); + b.advance(b.len()); + bits + } else { + let v = b[..buffer_len].to_vec(); + b.advance(buffer_len); + v + }; + let sb_bits = Self(bits); + + if sb_bits.bit_len() > bit_length { + return Err(SpanBatchError::BitfieldTooLong); + } + + Ok(sb_bits) + } + + /// Encodes a standard span-batch bitlist. + /// The bitlist is encoded as big-endian integer, left-padded with zeroes to a multiple of 8 + /// bits. The encoded bitlist cannot be longer than `bit_length` + pub fn encode(w: &mut Vec, bit_length: usize, bits: &Self) -> Result<(), SpanBatchError> { + if bits.bit_len() > bit_length { + return Err(SpanBatchError::BitfieldTooLong); + } + + // Round up, ensure enough bytes when number of bits is not a multiple of 8. + // Alternative of (L+7)/8 is not overflow-safe. + let buf_len = bit_length / 8 + if bit_length % 8 != 0 { 1 } else { 0 }; + let mut buf = vec![0; buf_len]; + buf[buf_len - bits.0.len()..].copy_from_slice(bits.as_ref()); + w.extend_from_slice(&buf); + Ok(()) + } + + /// Get a bit from the [SpanBatchBits] bitlist. + pub fn get_bit(&self, index: usize) -> Option { + let byte_index = index / 8; + let bit_index = index % 8; + + // Check if the byte index is within the bounds of the bitlist + if byte_index < self.0.len() { + // Retrieve the specific byte that contains the bit we're interested in + let byte = self.0[self.0.len() - byte_index - 1]; + + // Shift the bits of the byte to the right, based on the bit index, and + // mask it with 1 to isolate the bit we're interested in. + // If the result is not zero, the bit is set to 1, otherwise it's 0. + Some(if byte & (1 << bit_index) != 0 { 1 } else { 0 }) + } else { + // Return None if the index is out of bounds + None + } + } + + /// Sets a bit in the [SpanBatchBits] bitlist. + pub fn set_bit(&mut self, index: usize, value: bool) { + let byte_index = index / 8; + let bit_index = index % 8; + + // Ensure the vector is large enough to contain the bit at 'index'. + // If not, resize the vector, filling with 0s. + if byte_index >= self.0.len() { + Self::resize_from_right(&mut self.0, byte_index + 1); + } + + // Retrieve the specific byte to modify + let len = self.0.len(); + let byte = &mut self.0[len - byte_index - 1]; + + if value { + // Set the bit to 1 + *byte |= 1 << bit_index; + } else { + // Set the bit to 0 + *byte &= !(1 << bit_index); + } + } + + /// Calculates the bit length of the [SpanBatchBits] bitfield. + pub fn bit_len(&self) -> usize { + // Iterate over the bytes from left to right to find the first non-zero byte + for (i, &byte) in self.0.iter().enumerate() { + if byte != 0 { + // Calculate the index of the most significant bit in the byte + let msb_index = 7 - byte.leading_zeros() as usize; // 0-based index + + // Calculate the total bit length + let total_bit_length = msb_index + 1 + ((self.0.len() - i - 1) * 8); + return total_bit_length; + } + } + + // If all bytes are zero, the bitlist is considered to have a length of 0 + 0 + } + + /// Resizes an array from the right. Useful for big-endian zero extension. + fn resize_from_right(vec: &mut Vec, new_size: usize) { + let current_size = vec.len(); + match new_size.cmp(¤t_size) { + Ordering::Less => { + // Remove elements from the beginning. + let remove_count = current_size - new_size; + vec.drain(0..remove_count); + } + Ordering::Greater => { + // Calculate how many new elements to add. + let additional = new_size - current_size; + // Prepend new elements with default values. + let mut prepend_elements = vec![T::default(); additional]; + prepend_elements.append(vec); + *vec = prepend_elements; + } + Ordering::Equal => { /* If new_size == current_size, do nothing. */ } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use proptest::{collection::vec, prelude::any, proptest}; + + proptest! { + #[test] + fn test_encode_decode_roundtrip_span_bitlist(vec in vec(any::(), 0..5096)) { + let bits = SpanBatchBits(vec); + assert_eq!(SpanBatchBits::decode(&mut bits.as_ref(), bits.0.len() * 8).unwrap(), bits); + let mut encoded = Vec::new(); + SpanBatchBits::encode(&mut encoded, bits.0.len() * 8, &bits).unwrap(); + assert_eq!(encoded, bits.0); + } + + #[test] + fn test_span_bitlist_bitlen(index in 0usize..65536) { + let mut bits = SpanBatchBits::default(); + bits.set_bit(index, true); + assert_eq!(bits.0.len(), (index / 8) + 1); + assert_eq!(bits.bit_len(), index + 1); + } + + #[test] + fn test_span_bitlist_bitlen_shrink(first_index in 8usize..65536) { + let second_index = first_index.clamp(0, first_index - 8); + let mut bits = SpanBatchBits::default(); + + // Set and clear first index. + bits.set_bit(first_index, true); + assert_eq!(bits.0.len(), (first_index / 8) + 1); + assert_eq!(bits.bit_len(), first_index + 1); + bits.set_bit(first_index, false); + assert_eq!(bits.0.len(), (first_index / 8) + 1); + assert_eq!(bits.bit_len(), 0); + + // Set second bit. Even though the array is larger, as it was originally allocated with more words, + // the bitlength should still be lowered as the higher-order words are 0'd out. + bits.set_bit(second_index, true); + assert_eq!(bits.0.len(), (first_index / 8) + 1); + assert_eq!(bits.bit_len(), second_index + 1); + } + } + + #[test] + fn bitlist_big_endian_zero_extended() { + let mut bits = SpanBatchBits::default(); + + bits.set_bit(1, true); + bits.set_bit(6, true); + bits.set_bit(8, true); + bits.set_bit(15, true); + assert_eq!(bits.0[0], 0b1000_0001); + assert_eq!(bits.0[1], 0b0100_0010); + assert_eq!(bits.0.len(), 2); + assert_eq!(bits.bit_len(), 16); + } + + #[test] + fn test_static_set_get_bits_span_bitlist() { + let mut bits = SpanBatchBits::default(); + assert!(bits.0.is_empty()); + + bits.set_bit(0, true); + bits.set_bit(1, true); + bits.set_bit(2, true); + bits.set_bit(4, true); + bits.set_bit(7, true); + assert_eq!(bits.0.len(), 1); + assert_eq!(bits.get_bit(0), Some(1)); + assert_eq!(bits.get_bit(1), Some(1)); + assert_eq!(bits.get_bit(2), Some(1)); + assert_eq!(bits.get_bit(3), Some(0)); + assert_eq!(bits.get_bit(4), Some(1)); + + bits.set_bit(17, true); + assert_eq!(bits.get_bit(17), Some(1)); + assert_eq!(bits.get_bit(32), None); + assert_eq!(bits.0.len(), 3); + } +} diff --git a/crates/protocol/src/channel.rs b/crates/protocol/src/channel.rs index a3006bd41..a82d90a78 100644 --- a/crates/protocol/src/channel.rs +++ b/crates/protocol/src/channel.rs @@ -3,7 +3,13 @@ use alloc::vec::Vec; use alloy_primitives::{map::HashMap, Bytes}; -use crate::{block::BlockInfo, frame::Frame, ChannelId}; +use crate::{block::BlockInfo, frame::Frame}; + +/// [CHANNEL_ID_LENGTH] is the length of the channel ID. +pub const CHANNEL_ID_LENGTH: usize = 16; + +/// [ChannelId] is an opaque identifier for a channel. +pub type ChannelId = [u8; CHANNEL_ID_LENGTH]; /// [MAX_RLP_BYTES_PER_CHANNEL] is the maximum amount of bytes that will be read from /// a channel. This limit is set when decoding the RLP. diff --git a/crates/protocol/src/element.rs b/crates/protocol/src/element.rs new file mode 100644 index 000000000..c17d4e1e8 --- /dev/null +++ b/crates/protocol/src/element.rs @@ -0,0 +1,56 @@ +//! Span Batch Element + +use crate::SingleBatch; +use alloc::vec::Vec; +use alloy_primitives::Bytes; + +/// MAX_SPAN_BATCH_ELEMENTS is the maximum number of blocks, transactions in total, +/// or transaction per block allowed in a span batch. +pub const MAX_SPAN_BATCH_ELEMENTS: u64 = 10_000_000; + +/// A single batch element is similar to the [SingleBatch] type +/// but does not contain the parent hash and epoch hash since spans +/// do not contain this data for every block in the span. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct SpanBatchElement { + /// The epoch number of the L1 block + pub epoch_num: u64, + /// The timestamp of the L2 block + pub timestamp: u64, + /// The transactions in the L2 block + pub transactions: Vec, +} + +impl From for SpanBatchElement { + fn from(batch: SingleBatch) -> Self { + Self { + epoch_num: batch.epoch_num, + timestamp: batch.timestamp, + transactions: batch.transactions, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::{collection::vec, prelude::any, proptest}; + + proptest! { + #[test] + fn test_span_batch_element_from_single_batch(epoch_num in 0u64..u64::MAX, timestamp in 0u64..u64::MAX, transactions in vec(any::(), 0..100)) { + let single_batch = SingleBatch { + epoch_num, + timestamp, + transactions: transactions.clone(), + ..Default::default() + }; + + let span_batch_element: SpanBatchElement = single_batch.into(); + + assert_eq!(span_batch_element.epoch_num, epoch_num); + assert_eq!(span_batch_element.timestamp, timestamp); + assert_eq!(span_batch_element.transactions, transactions); + } + } +} diff --git a/crates/protocol/src/errors.rs b/crates/protocol/src/errors.rs new file mode 100644 index 000000000..8422f4aba --- /dev/null +++ b/crates/protocol/src/errors.rs @@ -0,0 +1,77 @@ +//! Span Batch Errors + +/// Span Batch Errors +#[derive(Debug, derive_more::Display, Clone, PartialEq, Eq)] +pub enum SpanBatchError { + /// The span batch is too big + #[display("The span batch is too big.")] + TooBigSpanBatchSize, + /// The bit field is too long + #[display("The bit field is too long")] + BitfieldTooLong, + /// Empty Span Batch + #[display("Empty span batch")] + EmptySpanBatch, + /// Missing L1 origin + #[display("Missing L1 origin")] + MissingL1Origin, + /// Decoding errors + #[display("Span batch decoding error: {_0}")] + Decoding(SpanDecodingError), +} + +impl From for SpanBatchError { + fn from(err: SpanDecodingError) -> Self { + Self::Decoding(err) + } +} + +impl core::error::Error for SpanBatchError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::Decoding(err) => Some(err), + _ => None, + } + } +} + +/// Decoding Error +#[derive(Debug, derive_more::Display, Clone, PartialEq, Eq)] +pub enum SpanDecodingError { + /// Failed to decode relative timestamp + #[display("Failed to decode relative timestamp")] + RelativeTimestamp, + /// Failed to decode L1 origin number + #[display("Failed to decode L1 origin number")] + L1OriginNumber, + /// Failed to decode parent check + #[display("Failed to decode parent check")] + ParentCheck, + /// Failed to decode L1 origin check + #[display("Failed to decode L1 origin check")] + L1OriginCheck, + /// Failed to decode block count + #[display("Failed to decode block count")] + BlockCount, + /// Failed to decode block tx counts + #[display("Failed to decode block tx counts")] + BlockTxCounts, + /// Failed to decode transaction nonces + #[display("Failed to decode transaction nonces")] + TxNonces, + /// Mismatch in length between the transaction type and signature arrays in a span batch + /// transaction payload. + #[display("Mismatch in length between the transaction type and signature arrays")] + TypeSignatureLenMismatch, + /// Invalid transaction type + #[display("Invalid transaction type")] + InvalidTransactionType, + /// Invalid transaction data + #[display("Invalid transaction data")] + InvalidTransactionData, + /// Invalid transaction signature + #[display("Invalid transaction signature")] + InvalidTransactionSignature, +} + +impl core::error::Error for SpanDecodingError {} diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 0a233f34e..0f770ee70 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -9,11 +9,41 @@ extern crate alloc; -/// [CHANNEL_ID_LENGTH] is the length of the channel ID. -pub const CHANNEL_ID_LENGTH: usize = 16; +mod traits; +pub use traits::BatchValidator; -/// [ChannelId] is an opaque identifier for a channel. -pub type ChannelId = [u8; CHANNEL_ID_LENGTH]; +mod errors; +pub use errors::{SpanBatchError, SpanDecodingError}; + +mod txs; +pub use txs::SpanBatchTransactions; + +mod tx_data; +pub use tx_data::{ + SpanBatchEip1559TransactionData, SpanBatchEip2930TransactionData, + SpanBatchLegacyTransactionData, SpanBatchTransactionData, +}; + +mod element; +pub use element::{SpanBatchElement, MAX_SPAN_BATCH_ELEMENTS}; + +mod batch; +pub use batch::BatchType; + +mod signature; +pub use signature::SpanBatchSignature; + +mod bits; +pub use bits::SpanBatchBits; + +mod span_batch; +pub use span_batch::SpanBatch; + +mod validity; +pub use validity::BatchValidity; + +mod single_batch; +pub use single_batch::SingleBatch; mod block; pub use block::{BlockInfo, FromBlockError, L2BlockInfo}; @@ -30,11 +60,15 @@ mod iter; pub use iter::FrameIter; mod utils; -pub use utils::{starts_with_2718_deposit, to_system_config, OpBlockConversionError}; +pub use utils::{ + convert_v_to_y_parity, is_protected_v, read_tx_data, starts_with_2718_deposit, + to_system_config, OpBlockConversionError, +}; mod channel; pub use channel::{ - Channel, ChannelError, FJORD_MAX_RLP_BYTES_PER_CHANNEL, MAX_RLP_BYTES_PER_CHANNEL, + Channel, ChannelError, ChannelId, CHANNEL_ID_LENGTH, FJORD_MAX_RLP_BYTES_PER_CHANNEL, + MAX_RLP_BYTES_PER_CHANNEL, }; pub mod deposits; @@ -51,3 +85,6 @@ pub use fee::{ calculate_tx_l1_cost_bedrock, calculate_tx_l1_cost_ecotone, calculate_tx_l1_cost_fjord, calculate_tx_l1_cost_regolith, data_gas_bedrock, data_gas_fjord, data_gas_regolith, }; + +#[cfg(any(test, feature = "test-utils"))] +pub mod test_utils; diff --git a/crates/protocol/src/signature.rs b/crates/protocol/src/signature.rs new file mode 100644 index 000000000..172b5d0c7 --- /dev/null +++ b/crates/protocol/src/signature.rs @@ -0,0 +1,28 @@ +//! This module contains the [SpanBatchSignature] type, which represents the ECDSA signature of a +//! transaction within a span batch. + +use crate::{SpanBatchError, SpanDecodingError}; +use alloy_primitives::{Signature, U256}; + +/// The ECDSA signature of a transaction within a span batch. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SpanBatchSignature { + pub(crate) v: u64, + pub(crate) r: U256, + pub(crate) s: U256, +} + +impl From for SpanBatchSignature { + fn from(value: Signature) -> Self { + Self { v: value.v().to_u64(), r: value.r(), s: value.s() } + } +} + +impl TryFrom for Signature { + type Error = SpanBatchError; + + fn try_from(value: SpanBatchSignature) -> Result { + Self::from_rs_and_parity(value.r, value.s, value.v) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionSignature)) + } +} diff --git a/crates/protocol/src/single_batch.rs b/crates/protocol/src/single_batch.rs new file mode 100644 index 000000000..ee0bdfdd9 --- /dev/null +++ b/crates/protocol/src/single_batch.rs @@ -0,0 +1,246 @@ +//! This module contains the [SingleBatch] type. + +use 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; + +/// Represents a single batch: a single encoded L2 block +#[derive(Debug, Default, RlpDecodable, RlpEncodable, Clone, PartialEq, Eq)] +pub struct SingleBatch { + /// Block hash of the previous L2 block. `B256::ZERO` if it has not been set by the Batch + /// Queue. + pub parent_hash: BlockHash, + /// The batch epoch number. Same as the first L1 block number in the epoch. + pub epoch_num: u64, + /// The block hash of the first L1 block in the epoch + pub epoch_hash: BlockHash, + /// The L2 block timestamp of this batch + pub timestamp: u64, + /// The L2 block transactions in this batch + pub transactions: Vec, +} + +impl SingleBatch { + /// If any transactions are empty or deposited transaction types. + pub fn has_invalid_transactions(&self) -> bool { + self.transactions.iter().any(|tx| tx.0.is_empty() || tx.0[0] == 0x7E) + } + + /// Returns the [BlockNumHash] of the batch. + pub const fn epoch(&self) -> BlockNumHash { + BlockNumHash { number: self.epoch_num, hash: self.epoch_hash } + } + + /// Validate the batch timestamp. + 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 { + return if cfg.is_holocene_active(inclusion_block.timestamp) { + BatchValidity::Past + } else { + BatchValidity::Drop + }; + } + BatchValidity::Accept + } + + /// Checks if the batch is valid. + pub fn check_batch( + &self, + cfg: &RollupConfig, + l1_blocks: &[BlockInfo], + l2_safe_head: L2BlockInfo, + inclusion_block: &BlockInfo, + ) -> BatchValidity { + // Cannot have empty l1_blocks for batch validation. + if l1_blocks.is_empty() { + return BatchValidity::Undecided; + } + + let epoch = l1_blocks[0]; + + // If the batch is not accepted by the timestamp check, return the result. + let timestamp_check = self.check_batch_timestamp(cfg, l2_safe_head, inclusion_block); + if !timestamp_check.is_accept() { + return timestamp_check; + } + + // Dependent on the above timestamp check. + // If the timestamp is correct, then it must build on top of the safe head. + if self.parent_hash != l2_safe_head.block_info.hash { + return BatchValidity::Drop; + } + + // Filter out batches that were included too late. + if self.epoch_num + cfg.seq_window_size < inclusion_block.number { + return BatchValidity::Drop; + } + + // Check the L1 origin of the batch + let mut batch_origin = epoch; + if self.epoch_num < epoch.number { + return BatchValidity::Drop; + } else if self.epoch_num == epoch.number { + // Batch is sticking to the current epoch, continue. + } else if self.epoch_num == epoch.number + 1 { + // With only 1 l1Block we cannot look at the next L1 Origin. + // Note: This means that we are unable to determine validity of a batch + // without more information. In this case we should bail out until we have + // more information otherwise the eager algorithm may diverge from a non-eager + // algorithm. + if l1_blocks.len() < 2 { + return BatchValidity::Undecided; + } + batch_origin = l1_blocks[1]; + } else { + return BatchValidity::Drop; + } + + // Validate the batch epoch hash + if self.epoch_hash != batch_origin.hash { + return BatchValidity::Drop; + } + + if self.timestamp < batch_origin.timestamp { + return BatchValidity::Drop; + } + + // Check if we ran out of sequencer time drift + 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 { + // If the sequencer is ignoring the time drift rule, then drop the batch and force an + // empty batch instead, as the sequencer is not allowed to include anything + // past this point without moving to the next epoch. + return BatchValidity::Drop; + } + if self.timestamp > max && no_txs { + // If the sequencer is co-operating by producing an empty batch, + // allow the batch if it was the right thing to do to maintain the L2 time >= L1 time + // invariant. Only check batches that do not advance the epoch, to ensure + // epoch advancement regardless of time drift is allowed. + if epoch.number == batch_origin.number { + if l1_blocks.len() < 2 { + return BatchValidity::Undecided; + } + let next_origin = l1_blocks[1]; + // Check if the next L1 Origin could have been adopted + if self.timestamp >= next_origin.timestamp { + return BatchValidity::Drop; + } + } + } + + // We can do this check earlier, but it's intensive so we do it last for the sad-path. + 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 + ); + } +} diff --git a/crates/protocol/src/span_batch.rs b/crates/protocol/src/span_batch.rs new file mode 100644 index 000000000..4b0166794 --- /dev/null +++ b/crates/protocol/src/span_batch.rs @@ -0,0 +1,1630 @@ +//! The Span Batch Type + +use alloc::vec::Vec; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::FixedBytes; +use op_alloy_consensus::OpTxType; +use op_alloy_genesis::RollupConfig; +use tracing::{info, warn}; + +use super::{SpanBatchBits, SpanBatchElement, SpanBatchError, SpanBatchTransactions}; + +use crate::{traits::BatchValidator, BatchValidity, BlockInfo, L2BlockInfo, SingleBatch}; + +/// The span batch contains the input to build a span of L2 blocks in derived form. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct SpanBatch { + /// First 20 bytes of the first block's parent hash + pub parent_check: FixedBytes<20>, + /// First 20 bytes of the last block's L1 origin hash + pub l1_origin_check: FixedBytes<20>, + /// Genesis block timestamp + pub genesis_timestamp: u64, + /// Chain ID + pub chain_id: u64, + /// List of block input in derived form + pub batches: Vec, + /// Caching - origin bits + pub origin_bits: SpanBatchBits, + /// Caching - block tx counts + pub block_tx_counts: Vec, + /// Caching - span batch txs + pub txs: SpanBatchTransactions, +} + +impl SpanBatch { + /// Returns the starting timestamp for the first batch in the span. + /// + /// ## Safety + /// Panics if [Self::batches] is empty. + pub fn starting_timestamp(&self) -> u64 { + self.batches[0].timestamp + } + + /// Returns the final timestamp for the last batch in the span. + /// + /// ## Safety + /// Panics if [Self::batches] is empty. + pub fn final_timestamp(&self) -> u64 { + self.batches[self.batches.len() - 1].timestamp + } + + /// Returns the epoch number for the first batch in the span. + /// + /// ## Safety + /// Panics if [Self::batches] is empty. + pub fn starting_epoch_num(&self) -> u64 { + self.batches[0].epoch_num + } + + /// Checks if the first 20 bytes of the given hash match the L1 origin check. + pub fn check_origin_hash(&self, hash: FixedBytes<32>) -> bool { + self.l1_origin_check == hash[..20] + } + + /// Checks if the first 20 bytes of the given hash match the parent check. + pub fn check_parent_hash(&self, hash: FixedBytes<32>) -> bool { + self.parent_check == hash[..20] + } + + /// Checks if the span batch is valid. + pub async fn check_batch( + &self, + cfg: &RollupConfig, + l1_blocks: &[BlockInfo], + l2_safe_head: L2BlockInfo, + inclusion_block: &BlockInfo, + fetcher: &mut BV, + ) -> BatchValidity { + let (prefix_validity, parent_block) = + self.check_batch_prefix(cfg, l1_blocks, l2_safe_head, inclusion_block, fetcher).await; + if !matches!(prefix_validity, BatchValidity::Accept) { + return prefix_validity; + } + + let starting_epoch_num = self.starting_epoch_num(); + let parent_block = parent_block.expect("parent_block must be Some"); + + let mut origin_index = 0; + let mut origin_advanced = starting_epoch_num == parent_block.l1_origin.number + 1; + for (i, batch) in self.batches.iter().enumerate() { + if batch.timestamp <= l2_safe_head.block_info.timestamp { + continue; + } + // Find the L1 origin for the batch. + for (j, j_block) in l1_blocks.iter().enumerate().skip(origin_index) { + if batch.epoch_num == j_block.number { + origin_index = j; + break; + } + } + let l1_origin = l1_blocks[origin_index]; + if i > 0 { + origin_advanced = false; + if batch.epoch_num > self.batches[i - 1].epoch_num { + origin_advanced = true; + } + } + let block_timestamp = batch.timestamp; + if block_timestamp < l1_origin.timestamp { + warn!( + "block timestamp is less than L1 origin timestamp, l2_timestamp: {}, l1_timestamp: {}, origin: {:?}", + block_timestamp, + l1_origin.timestamp, + l1_origin.id() + ); + return BatchValidity::Drop; + } + + // Check if we ran out of sequencer time drift + let max_drift = cfg.max_sequencer_drift(l1_origin.timestamp); + if block_timestamp > l1_origin.timestamp + max_drift { + if batch.transactions.is_empty() { + // If the sequencer is co-operating by producing an empty batch, + // then allow the batch if it was the right thing to do to maintain the L2 time + // >= L1 time invariant. We only check batches that do not + // advance the epoch, to ensure epoch advancement regardless of time drift is + // allowed. + if !origin_advanced { + if origin_index + 1 >= l1_blocks.len() { + info!("without the next L1 origin we cannot determine yet if this empty batch that exceeds the time drift is still valid"); + return BatchValidity::Undecided; + } + if block_timestamp >= l1_blocks[origin_index + 1].timestamp { + // check if the next L1 origin could have been adopted + info!("batch exceeded sequencer time drift without adopting next origin, and next L1 origin would have been valid"); + return BatchValidity::Drop; + } else { + info!("continuing with empty batch before late L1 block to preserve L2 time invariant"); + } + } + } else { + // If the sequencer is ignoring the time drift rule, then drop the batch and + // force an empty batch instead, as the sequencer is not + // allowed to include anything past this point without moving to the next epoch. + warn!( + "batch exceeded sequencer time drift, sequencer must adopt new L1 origin to include transactions again, max_time: {}", + l1_origin.timestamp + max_drift + ); + return BatchValidity::Drop; + } + } + + // Check that the transactions are not empty and do not contain any deposits. + for (tx_index, tx_bytes) in batch.transactions.iter().enumerate() { + if tx_bytes.is_empty() { + warn!( + "transaction data must not be empty, but found empty tx, tx_index: {}", + tx_index + ); + return BatchValidity::Drop; + } + if tx_bytes.0[0] == OpTxType::Deposit as u8 { + warn!("sequencers may not embed any deposits into batch data, but found tx that has one, tx_index: {}", tx_index); + return BatchValidity::Drop; + } + } + } + + // Check overlapped blocks + let parent_num = parent_block.block_info.number; + let next_timestamp = l2_safe_head.block_info.timestamp + cfg.block_time; + if self.starting_timestamp() < next_timestamp { + for i in 0..(l2_safe_head.block_info.number - parent_num) { + let safe_block_num = parent_num + i + 1; + let safe_block_payload = match fetcher.block_by_number(safe_block_num).await { + Ok(p) => p, + Err(e) => { + warn!("failed to fetch block number {safe_block_num}: {e}"); + return BatchValidity::Undecided; + } + }; + let safe_block = &safe_block_payload.body; + let batch_txs = &self.batches[i as usize].transactions; + // Execution payload has deposit txs but batch does not. + let deposit_count: usize = safe_block + .transactions + .iter() + .map(|tx| if tx.is_deposit() { 1 } else { 0 }) + .sum(); + if safe_block.transactions.len() - deposit_count != batch_txs.len() { + warn!( + "overlapped block's tx count does not match, safe_block_txs: {}, batch_txs: {}", + safe_block.transactions.len(), + batch_txs.len() + ); + return BatchValidity::Drop; + } + let batch_txs_len = batch_txs.len(); + #[allow(clippy::needless_range_loop)] + for j in 0..batch_txs_len { + let mut buf = Vec::new(); + safe_block.transactions[j + deposit_count].encode_2718(&mut buf); + if buf != batch_txs[j].0 { + warn!("overlapped block's transaction does not match"); + return BatchValidity::Drop; + } + } + let safe_block_ref = match L2BlockInfo::from_block_and_genesis( + &safe_block_payload, + &cfg.genesis, + ) { + Ok(r) => r, + Err(e) => { + warn!("failed to extract L2BlockInfo from execution payload, hash: {}, err: {e}", safe_block_payload.header.hash_slow()); + return BatchValidity::Drop; + } + }; + if safe_block_ref.l1_origin.number != self.batches[i as usize].epoch_num { + warn!( + "overlapped block's L1 origin number does not match {}, {}", + safe_block_ref.l1_origin.number, self.batches[i as usize].epoch_num + ); + return BatchValidity::Drop; + } + } + } + + BatchValidity::Accept + } + + /// Checks the validity of the batch's prefix. + /// + /// This function is used for post-Holocene hardfork to perform batch validation + /// as each batch is being loaded in. + pub async fn check_batch_prefix( + &self, + cfg: &RollupConfig, + l1_origins: &[BlockInfo], + l2_safe_head: L2BlockInfo, + inclusion_block: &BlockInfo, + fetcher: &mut BF, + ) -> (BatchValidity, Option) { + if l1_origins.is_empty() { + warn!("missing L1 block input, cannot proceed with batch checking"); + return (BatchValidity::Undecided, None); + } + if self.batches.is_empty() { + warn!("empty span batch, cannot proceed with batch checking"); + return (BatchValidity::Undecided, None); + } + + let epoch = l1_origins[0]; + let next_timestamp = l2_safe_head.block_info.timestamp + cfg.block_time; + + let starting_epoch_num = self.starting_epoch_num(); + let mut batch_origin = epoch; + if starting_epoch_num == batch_origin.number + 1 { + if l1_origins.len() < 2 { + info!("eager batch wants to advance current epoch {:?}, but could not without more L1 blocks", epoch.id()); + return (BatchValidity::Undecided, None); + } + batch_origin = l1_origins[1]; + } + if !cfg.is_delta_active(batch_origin.timestamp) { + warn!( + "received SpanBatch (id {:?}) with L1 origin (timestamp {}) before Delta hard fork", + batch_origin.id(), + batch_origin.timestamp + ); + return (BatchValidity::Drop, None); + } + + if self.starting_timestamp() > next_timestamp { + warn!( + "received out-of-order batch for future processing after next batch ({} > {})", + self.starting_timestamp(), + next_timestamp + ); + + // After holocene is activated, gaps are disallowed. + if cfg.is_holocene_active(inclusion_block.timestamp) { + return (BatchValidity::Drop, None); + } + return (BatchValidity::Future, None); + } + + // Drop the batch if it has no new blocks after the safe head. + if self.final_timestamp() < next_timestamp { + warn!("span batch has no new blocks after safe head"); + return if cfg.is_holocene_active(inclusion_block.timestamp) { + (BatchValidity::Past, None) + } else { + (BatchValidity::Drop, None) + }; + } + + // Find the parent block of the span batch. + // If the span batch does not overlap the current safe chain, parent block should be the L2 + // safe head. + let mut parent_num = l2_safe_head.block_info.number; + let mut parent_block = l2_safe_head; + if self.starting_timestamp() < next_timestamp { + if self.starting_timestamp() > l2_safe_head.block_info.timestamp { + // Batch timestamp cannot be between safe head and next timestamp. + warn!("batch has misaligned timestamp, block time is too short"); + return (BatchValidity::Drop, None); + } + if (l2_safe_head.block_info.timestamp - self.starting_timestamp()) % cfg.block_time != 0 + { + warn!("batch has misaligned timestamp, not overlapped exactly"); + return (BatchValidity::Drop, None); + } + parent_num = l2_safe_head.block_info.number + - (l2_safe_head.block_info.timestamp - self.starting_timestamp()) / cfg.block_time + - 1; + parent_block = match fetcher.l2_block_info_by_number(parent_num).await { + Ok(block) => block, + Err(e) => { + warn!("failed to fetch L2 block number {parent_num}: {e}"); + // Unable to validate the batch for now. Retry later. + return (BatchValidity::Undecided, None); + } + }; + } + if !self.check_parent_hash(parent_block.block_info.hash) { + warn!( + "parent block mismatch, expected: {parent_num}, received: {}. parent hash: {}, parent hash check: {}", + parent_block.block_info.number, + parent_block.block_info.hash, + self.parent_check, + ); + return (BatchValidity::Drop, None); + } + + // Filter out batches that were included too late. + if starting_epoch_num + cfg.seq_window_size < inclusion_block.number { + warn!("batch was included too late, sequence window expired"); + return (BatchValidity::Drop, None); + } + + // Check the L1 origin of the batch + if starting_epoch_num > parent_block.l1_origin.number + 1 { + warn!( + "batch is for future epoch too far ahead, while it has the next timestamp, so it must be invalid. starting epoch: {} | next epoch: {}", + starting_epoch_num, + parent_block.l1_origin.number + 1 + ); + return (BatchValidity::Drop, None); + } + + // Verify the l1 origin hash for each l1 block. + // SAFETY: `Self::batches` is not empty, so the last element is guaranteed to exist. + let end_epoch_num = self.batches.last().unwrap().epoch_num; + let mut origin_checked = false; + // l1Blocks is supplied from batch queue and its length is limited to SequencerWindowSize. + for l1_block in l1_origins { + if l1_block.number == end_epoch_num { + if !self.check_origin_hash(l1_block.hash) { + warn!( + "batch is for different L1 chain, epoch hash does not match, expected: {}", + l1_block.hash + ); + return (BatchValidity::Drop, None); + } + origin_checked = true; + break; + } + } + if !origin_checked { + info!("need more l1 blocks to check entire origins of span batch"); + return (BatchValidity::Undecided, None); + } + + if starting_epoch_num < parent_block.l1_origin.number { + warn!("dropped batch, epoch is too old, minimum: {:?}", parent_block.block_info.id()); + return (BatchValidity::Drop, None); + } + + (BatchValidity::Accept, Some(parent_block)) + } + + /// Converts all [SpanBatchElement]s after the L2 safe head to [SingleBatch]es. The resulting + /// [SingleBatch]es do not contain a parent hash, as it is populated by the Batch Queue + /// stage. + pub fn get_singular_batches( + &self, + l1_origins: &[BlockInfo], + l2_safe_head: L2BlockInfo, + ) -> Result, SpanBatchError> { + let mut single_batches = Vec::new(); + let mut origin_index = 0; + for batch in &self.batches { + if batch.timestamp <= l2_safe_head.block_info.timestamp { + continue; + } + tracing::info!( + "checking {} l1 origins with first timestamp: {}, batch timestamp: {}, {}", + l1_origins.len(), + l1_origins[0].timestamp, + batch.timestamp, + batch.epoch_num + ); + let origin_epoch_hash = l1_origins[origin_index..l1_origins.len()] + .iter() + .enumerate() + .find(|(_, origin)| origin.number == batch.epoch_num) + .map(|(i, origin)| { + origin_index = i; + origin.hash + }) + .ok_or(SpanBatchError::MissingL1Origin)?; + let single_batch = SingleBatch { + epoch_num: batch.epoch_num, + epoch_hash: origin_epoch_hash, + timestamp: batch.timestamp, + transactions: batch.transactions.clone(), + ..Default::default() + }; + single_batches.push(single_batch); + } + Ok(single_batches) + } + + /// Append a [SingleBatch] to the [SpanBatch]. Updates the L1 origin check if need be. + pub fn append_singular_batch( + &mut self, + singular_batch: SingleBatch, + seq_num: u64, + ) -> Result<(), SpanBatchError> { + // If the new element is not ordered with respect to the last element, panic. + if !self.batches.is_empty() && self.peek(0).timestamp > singular_batch.timestamp { + panic!("Batch is not ordered"); + } + + let SingleBatch { epoch_hash, parent_hash, .. } = singular_batch; + + // Always append the new batch and set the L1 origin check. + self.batches.push(singular_batch.into()); + // Always update the L1 origin check. + self.l1_origin_check = epoch_hash[..20].try_into().expect("Sub-slice cannot fail"); + + let epoch_bit = if self.batches.len() == 1 { + // If there is only one batch, initialize the parent check and set the epoch bit based + // on the sequence number. + self.parent_check = parent_hash[..20].try_into().expect("Sub-slice cannot fail"); + seq_num == 0 + } else { + // If there is more than one batch, set the epoch bit based on the last two batches. + self.peek(1).epoch_num < self.peek(0).epoch_num + }; + + // Set the respective bit in the origin bits. + self.origin_bits.set_bit(self.batches.len() - 1, epoch_bit); + + let new_txs = self.peek(0).transactions.clone(); + + // Update the block tx counts cache with the latest batch's transaction count. + self.block_tx_counts.push(new_txs.len() as u64); + + // Add the new transactions to the transaction cache. + self.txs.add_txs(new_txs, self.chain_id) + } + + /// Peek at the `n`th-to-last last element in the batch. + fn peek(&self, n: usize) -> &SpanBatchElement { + &self.batches[self.batches.len() - 1 - n] + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{CollectingLayer, TestBatchValidator, TraceStorage}; + use alloc::vec; + use alloy_consensus::Header; + use alloy_eips::BlockNumHash; + use alloy_primitives::{b256, Bytes}; + use op_alloy_consensus::{OpBlock, OpTxType}; + use op_alloy_genesis::ChainGenesis; + use tracing::Level; + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + + #[test] + fn test_timestamp() { + let timestamp = 10; + let first_element = SpanBatchElement { timestamp, ..Default::default() }; + let batch = + SpanBatch { batches: vec![first_element, Default::default()], ..Default::default() }; + assert_eq!(batch.starting_timestamp(), timestamp); + } + + #[test] + fn test_starting_epoch_num() { + let epoch_num = 10; + let first_element = SpanBatchElement { epoch_num, ..Default::default() }; + let batch = + SpanBatch { batches: vec![first_element, Default::default()], ..Default::default() }; + assert_eq!(batch.starting_epoch_num(), epoch_num); + } + + #[test] + fn test_check_origin_hash() { + let l1_origin_check = FixedBytes::from([17u8; 20]); + let hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let batch = SpanBatch { l1_origin_check, ..Default::default() }; + assert!(batch.check_origin_hash(hash)); + // This hash has 19 matching bytes, the other 13 are zeros. + let invalid = b256!("1111111111111111111111111111111111111100000000000000000000000000"); + assert!(!batch.check_origin_hash(invalid)); + } + + #[test] + fn test_check_parent_hash() { + let parent_check = FixedBytes::from([17u8; 20]); + let hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let batch = SpanBatch { parent_check, ..Default::default() }; + assert!(batch.check_parent_hash(hash)); + // This hash has 19 matching bytes, the other 13 are zeros. + let invalid = b256!("1111111111111111111111111111111111111100000000000000000000000000"); + assert!(!batch.check_parent_hash(invalid)); + } + + #[tokio::test] + async fn test_check_batch_missing_l1_block_input() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig::default(); + let l1_blocks = vec![]; + let l2_safe_head = L2BlockInfo::default(); + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let batch = SpanBatch::default(); + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("missing L1 block input, cannot proceed with batch checking")); + } + + #[tokio::test] + async fn test_check_batches_is_empty() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig::default(); + let l1_blocks = vec![BlockInfo::default()]; + let l2_safe_head = L2BlockInfo::default(); + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let batch = SpanBatch::default(); + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("empty span batch, cannot proceed with batch checking")); + } + + #[tokio::test] + async fn test_eager_block_missing_origins() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig::default(); + let block = BlockInfo { number: 9, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo::default(); + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, ..Default::default() }; + let batch = SpanBatch { batches: vec![first], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::INFO); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "eager batch wants to advance current epoch {:?}, but could not without more L1 blocks", + block.id() + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_check_batch_delta_inactive() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(10), ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 9, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo::default(); + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let batch = SpanBatch { batches: vec![first], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "received SpanBatch (id {:?}) with L1 origin (timestamp {}) before Delta hard fork", + block.id(), + block.timestamp + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_check_batch_out_of_order() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 21, ..Default::default() }; + let batch = SpanBatch { batches: vec![first], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Future + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains( + "received out-of-order batch for future processing after next batch (21 > 20)" + )); + } + + #[tokio::test] + async fn test_check_batch_no_new_blocks() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let batch = SpanBatch { batches: vec![first], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("span batch has no new blocks after safe head")); + } + + #[tokio::test] + async fn test_check_batch_misaligned_timestamp() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 11, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 21, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch has misaligned timestamp, block time is too short")); + } + + #[tokio::test] + async fn test_check_batch_misaligned_without_overlap() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 8, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch has misaligned timestamp, not overlapped exactly")); + } + + #[tokio::test] + async fn test_check_batch_failed_to_fetch_l2_block() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let mut fetcher = TestBatchValidator::default(); + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { batches: vec![first, second], ..Default::default() }; + // parent number = 41 - (10 - 10) / 10 - 1 = 40 + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("failed to fetch L2 block number 40: Block not found")); + } + + #[tokio::test] + async fn test_check_batch_parent_hash_fail() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo::default(); + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + fetcher.short_circuit = true; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice( + &b256!("1111111111111111111111111111111111111111000000000000000000000000")[..20], + ), + ..Default::default() + }; + // parent number = 41 - (10 - 10) / 10 - 1 = 40 + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("parent block mismatch, expected: 40, received: 41")); + } + + #[tokio::test] + async fn test_check_sequence_window_expired() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { delta_time: Some(0), block_time: 10, ..Default::default() }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 10, + ..Default::default() + }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + ..Default::default() + }; + // parent number = 41 - (10 - 10) / 10 - 1 = 40 + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch was included too late, sequence window expired")); + } + + #[tokio::test] + async fn test_starting_epoch_too_far_ahead() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let block = BlockInfo { number: 10, timestamp: 10, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 8, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 10, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 8, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + ..Default::default() + }; + // parent number = 41 - (10 - 10) / 10 - 1 = 40 + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = "batch is for future epoch too far ahead, while it has the next timestamp, so it must be invalid. starting epoch: 10 | next epoch: 9"; + assert!(logs[0].contains(str)); + } + + #[tokio::test] + async fn test_check_batch_epoch_hash_mismatch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "batch is for different L1 chain, epoch hash does not match, expected: {}", + l1_block_hash, + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_need_more_l1_blocks() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 10, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::INFO); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("need more l1 blocks to check entire origins of span batch")); + } + + #[tokio::test] + async fn test_drop_batch_epoch_too_old() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 13, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 14, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "dropped batch, epoch is too old, minimum: {:?}", + l2_block.block_info.id(), + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_check_batch_exceeds_max_seq_drif() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 0, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let second_block = + BlockInfo { number: 12, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block, second_block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 20, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 10, timestamp: 20, ..Default::default() }; + let third = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::INFO); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch exceeded sequencer time drift without adopting next origin, and next L1 origin would have been valid")); + } + + #[tokio::test] + async fn test_continuing_with_empty_batch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 0, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let second_block = + BlockInfo { number: 12, timestamp: 21, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block, second_block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 20, transactions: vec![] }; + let second = SpanBatchElement { epoch_num: 10, timestamp: 20, transactions: vec![] }; + let third = SpanBatchElement { epoch_num: 11, timestamp: 20, transactions: vec![] }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + txs: SpanBatchTransactions::default(), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Accept + ); + let infos = trace_store.get_by_level(Level::INFO); + assert_eq!(infos.len(), 1); + assert!(infos[0].contains( + "continuing with empty batch before late L1 block to preserve L2 time invariant" + )); + } + + #[tokio::test] + async fn test_check_batch_exceeds_sequencer_time_drift() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 0, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let second_block = + BlockInfo { number: 12, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block, second_block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Default::default()], + }; + let second = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Default::default()], + }; + let third = SpanBatchElement { + epoch_num: 11, + timestamp: 20, + transactions: vec![Default::default()], + }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + txs: SpanBatchTransactions::default(), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("batch exceeded sequencer time drift, sequencer must adopt new L1 origin to include transactions again, max_time: 10")); + } + + #[tokio::test] + async fn test_check_batch_empty_txs() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let second_block = + BlockInfo { number: 12, timestamp: 21, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block, second_block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Default::default()], + }; + let second = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Default::default()], + }; + let third = SpanBatchElement { epoch_num: 11, timestamp: 20, transactions: vec![] }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + txs: SpanBatchTransactions::default(), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("transaction data must not be empty, but found empty tx")); + } + + #[tokio::test] + async fn test_check_batch_with_deposit_tx() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + max_sequencer_drift: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let second_block = + BlockInfo { number: 12, timestamp: 21, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block, second_block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { number: 40, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let filler_bytes = Bytes::copy_from_slice(&[OpTxType::Eip1559 as u8]); + let first = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![filler_bytes.clone()], + }; + let second = SpanBatchElement { + epoch_num: 10, + timestamp: 20, + transactions: vec![Bytes::copy_from_slice(&[OpTxType::Deposit as u8])], + }; + let third = + SpanBatchElement { epoch_num: 11, timestamp: 20, transactions: vec![filler_bytes] }; + let batch = SpanBatch { + batches: vec![first, second, third], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + txs: SpanBatchTransactions::default(), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("sequencers may not embed any deposits into batch data, but found tx that has one, tx_index: 0")); + } + + #[tokio::test] + async fn test_check_batch_failed_to_fetch_payload() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let mut fetcher = TestBatchValidator { blocks: vec![l2_block], ..Default::default() }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Undecided + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("failed to fetch block number 41: L2 Block not found")); + } + + #[tokio::test] + async fn test_check_batch_failed_to_extract_l2_block_info() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let block = OpBlock { + header: Header { number: 41, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: Vec::new(), + ommers: Vec::new(), + withdrawals: None, + }, + }; + let mut fetcher = TestBatchValidator { + blocks: vec![l2_block], + op_blocks: vec![block], + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + let str = alloc::format!( + "failed to extract L2BlockInfo from execution payload, hash: {:?}", + b256!("0e2ee9abe94ee4514b170d7039d8151a7469d434a8575dbab5bd4187a27732dd"), + ); + assert!(logs[0].contains(&str)); + } + + #[tokio::test] + async fn test_overlapped_blocks_origin_mismatch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let payload_block_hash = + b256!("0e2ee9abe94ee4514b170d7039d8151a7469d434a8575dbab5bd4187a27732dd"); + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + genesis: ChainGenesis { + l2: BlockNumHash { number: 41, hash: payload_block_hash }, + ..Default::default() + }, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 41, timestamp: 10, parent_hash, ..Default::default() }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 10, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let block = OpBlock { + header: Header { number: 41, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: Vec::new(), + ommers: Vec::new(), + withdrawals: None, + }, + }; + let mut fetcher = TestBatchValidator { + blocks: vec![l2_block], + op_blocks: vec![block], + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Drop + ); + let logs = trace_store.get_by_level(Level::WARN); + assert_eq!(logs.len(), 1); + assert!(logs[0].contains("overlapped block's L1 origin number does not match")); + } + + #[tokio::test] + async fn test_check_batch_valid_with_genesis_epoch() { + let trace_store: TraceStorage = Default::default(); + let layer = CollectingLayer::new(trace_store.clone()); + tracing_subscriber::Registry::default().with(layer).init(); + + let payload_block_hash = + b256!("0e2ee9abe94ee4514b170d7039d8151a7469d434a8575dbab5bd4187a27732dd"); + let cfg = RollupConfig { + seq_window_size: 100, + delta_time: Some(0), + block_time: 10, + genesis: ChainGenesis { + l2: BlockNumHash { number: 41, hash: payload_block_hash }, + l1: BlockNumHash { number: 10, ..Default::default() }, + ..Default::default() + }, + ..Default::default() + }; + let l1_block_hash = + b256!("3333333333333333333333333333333333333333000000000000000000000000"); + let block = + BlockInfo { number: 11, timestamp: 10, hash: l1_block_hash, ..Default::default() }; + let l1_blocks = vec![block]; + let parent_hash = b256!("1111111111111111111111111111111111111111000000000000000000000000"); + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { + number: 41, + timestamp: 10, + hash: parent_hash, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let inclusion_block = BlockInfo { number: 50, ..Default::default() }; + let l2_block = L2BlockInfo { + block_info: BlockInfo { + number: 40, + hash: parent_hash, + timestamp: 10, + ..Default::default() + }, + l1_origin: BlockNumHash { number: 9, ..Default::default() }, + ..Default::default() + }; + let block = OpBlock { + header: Header { number: 41, ..Default::default() }, + body: alloy_consensus::BlockBody { + transactions: Vec::new(), + ommers: Vec::new(), + withdrawals: None, + }, + }; + let mut fetcher = TestBatchValidator { + blocks: vec![l2_block], + op_blocks: vec![block], + ..Default::default() + }; + let first = SpanBatchElement { epoch_num: 10, timestamp: 10, ..Default::default() }; + let second = SpanBatchElement { epoch_num: 11, timestamp: 20, ..Default::default() }; + let batch = SpanBatch { + batches: vec![first, second], + parent_check: FixedBytes::<20>::from_slice(&parent_hash[..20]), + l1_origin_check: FixedBytes::<20>::from_slice(&l1_block_hash[..20]), + ..Default::default() + }; + assert_eq!( + batch.check_batch(&cfg, &l1_blocks, l2_safe_head, &inclusion_block, &mut fetcher).await, + BatchValidity::Accept + ); + assert!(trace_store.is_empty()); + } +} diff --git a/crates/protocol/src/test_utils.rs b/crates/protocol/src/test_utils.rs new file mode 100644 index 000000000..962af0670 --- /dev/null +++ b/crates/protocol/src/test_utils.rs @@ -0,0 +1,119 @@ +//! Test utilities for the protocol crate. + +use alloc::{boxed::Box, format, string::String, sync::Arc, vec::Vec}; +use async_trait::async_trait; +use op_alloy_consensus::OpBlock; +use spin::Mutex; +use tracing::{Event, Level, Subscriber}; +use tracing_subscriber::{layer::Context, Layer}; + +use crate::{BatchValidator, L2BlockInfo}; + +/// An error for implementations of the [BatchValidator] trait. +#[derive(Debug, derive_more::Display)] +pub enum TestBatchValidatorError { + /// The block was not found. + #[display("Block not found")] + BlockNotFound, + /// The L2 block was not found. + #[display("L2 Block not found")] + L2BlockNotFound, +} + +impl core::error::Error for TestBatchValidatorError {} + +/// An [TestBatchValidator] implementation for testing. +#[derive(Debug, Default, Clone)] +pub struct TestBatchValidator { + /// Blocks + pub blocks: Vec, + /// Short circuit the block return to be the first block. + pub short_circuit: bool, + /// Blocks + pub op_blocks: Vec, +} + +impl TestBatchValidator { + /// Creates a new [TestBatchValidator] with the given origin and batches. + pub const fn new(blocks: Vec, op_blocks: Vec) -> Self { + Self { blocks, short_circuit: false, op_blocks } + } +} + +#[async_trait] +impl BatchValidator for TestBatchValidator { + type Error = TestBatchValidatorError; + + async fn l2_block_info_by_number(&mut self, number: u64) -> Result { + if self.short_circuit { + return self + .blocks + .first() + .copied() + .ok_or_else(|| TestBatchValidatorError::BlockNotFound); + } + self.blocks + .iter() + .find(|b| b.block_info.number == number) + .cloned() + .ok_or_else(|| TestBatchValidatorError::BlockNotFound) + } + + async fn block_by_number(&mut self, number: u64) -> Result { + self.op_blocks + .iter() + .find(|p| p.header.number == number) + .cloned() + .ok_or_else(|| TestBatchValidatorError::L2BlockNotFound) + } +} + +/// The storage for the collected traces. +#[derive(Debug, Default, Clone)] +pub struct TraceStorage(pub Arc>>); + +impl TraceStorage { + /// Returns the items in the storage that match the specified level. + pub fn get_by_level(&self, level: Level) -> Vec { + self.0 + .lock() + .iter() + .filter_map(|(l, message)| if *l == level { Some(message.clone()) } else { None }) + .collect() + } + + /// Locks the storage and returns the items. + pub fn lock(&self) -> spin::MutexGuard<'_, Vec<(Level, String)>> { + self.0.lock() + } + + /// Returns if the storage is empty. + pub fn is_empty(&self) -> bool { + self.0.lock().is_empty() + } +} + +/// A subscriber layer that collects traces and their log levels. +#[derive(Debug, Default)] +pub struct CollectingLayer { + /// The storage for the collected traces. + pub storage: TraceStorage, +} + +impl CollectingLayer { + /// Creates a new collecting layer with the specified storage. + pub const fn new(storage: TraceStorage) -> Self { + Self { storage } + } +} + +impl Layer for CollectingLayer { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let metadata = event.metadata(); + let level = *metadata.level(); + let message = format!("{:?}", event); + + let mut storage = self.storage.0.lock(); + storage.push((level, message)); + } +} diff --git a/crates/protocol/src/traits.rs b/crates/protocol/src/traits.rs new file mode 100644 index 000000000..3aa4605d0 --- /dev/null +++ b/crates/protocol/src/traits.rs @@ -0,0 +1,25 @@ +//! Traits for working with protocol types. + +use alloc::{boxed::Box, string::ToString}; +use async_trait::async_trait; +use core::fmt::Display; +use op_alloy_consensus::OpBlock; + +use crate::L2BlockInfo; + +/// Describes the functionality of a data source that fetches safe blocks. +#[async_trait] +pub trait BatchValidator { + /// The error type for the [BatchValidator]. + type Error: Display + ToString; + + /// Returns the [L2BlockInfo] given a block number. + /// + /// Errors if the block does not exist. + async fn l2_block_info_by_number(&mut self, number: u64) -> Result; + + /// Returns the [OpBlock] for a given number. + /// + /// Errors if no block is available for the given block number. + async fn block_by_number(&mut self, number: u64) -> Result; +} diff --git a/crates/protocol/src/tx_data/eip1559.rs b/crates/protocol/src/tx_data/eip1559.rs new file mode 100644 index 000000000..9bb6302de --- /dev/null +++ b/crates/protocol/src/tx_data/eip1559.rs @@ -0,0 +1,85 @@ +//! This module contains the eip1559 transaction data type for a span batch. + +use crate::{SpanBatchError, SpanDecodingError}; +use alloy_consensus::{SignableTransaction, Signed, TxEip1559, TxEnvelope}; +use alloy_eips::eip2930::AccessList; +use alloy_primitives::{Address, Signature, TxKind, U256}; +use alloy_rlp::{Bytes, RlpDecodable, RlpEncodable}; + +/// The transaction data for an EIP-1559 transaction within a span batch. +#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)] +pub struct SpanBatchEip1559TransactionData { + /// The ETH value of the transaction. + pub value: U256, + /// Maximum priority fee per gas. + pub max_priority_fee_per_gas: U256, + /// Maximum fee per gas. + pub max_fee_per_gas: U256, + /// Transaction calldata. + pub data: Bytes, + /// Access list, used to pre-warm storage slots through static declaration. + pub access_list: AccessList, +} + +impl SpanBatchEip1559TransactionData { + /// Converts [SpanBatchEip1559TransactionData] into a [TxEnvelope]. + pub fn to_enveloped_tx( + &self, + nonce: u64, + gas: u64, + to: Option
, + chain_id: u64, + signature: Signature, + ) -> Result { + let eip1559_tx = TxEip1559 { + chain_id, + nonce, + max_fee_per_gas: u128::from_be_bytes( + self.max_fee_per_gas.to_be_bytes::<32>()[16..].try_into().map_err(|_| { + SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData) + })?, + ), + max_priority_fee_per_gas: u128::from_be_bytes( + self.max_priority_fee_per_gas.to_be_bytes::<32>()[16..].try_into().map_err( + |_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData), + )?, + ), + gas_limit: gas, + to: to.map_or(TxKind::Create, TxKind::Call), + value: self.value, + input: self.data.clone().into(), + access_list: self.access_list.clone(), + }; + let signature_hash = eip1559_tx.signature_hash(); + let signed_eip1559_tx = Signed::new_unchecked(eip1559_tx, signature, signature_hash); + Ok(TxEnvelope::Eip1559(signed_eip1559_tx)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::SpanBatchTransactionData; + use alloc::vec::Vec; + use alloy_rlp::{Decodable, Encodable}; + + #[test] + fn encode_eip1559_tx_data_roundtrip() { + let variable_fee_tx = SpanBatchEip1559TransactionData { + value: U256::from(0xFF), + max_fee_per_gas: U256::from(0xEE), + max_priority_fee_per_gas: U256::from(0xDD), + data: Bytes::from(alloc::vec![0x01, 0x02, 0x03]), + access_list: AccessList::default(), + }; + let mut encoded_buf = Vec::new(); + SpanBatchTransactionData::Eip1559(variable_fee_tx.clone()).encode(&mut encoded_buf); + + let decoded = SpanBatchTransactionData::decode(&mut encoded_buf.as_slice()).unwrap(); + let SpanBatchTransactionData::Eip1559(variable_fee_decoded) = decoded else { + panic!("Expected SpanBatchEip1559TransactionData, got {:?}", decoded); + }; + + assert_eq!(variable_fee_tx, variable_fee_decoded); + } +} diff --git a/crates/protocol/src/tx_data/eip2930.rs b/crates/protocol/src/tx_data/eip2930.rs new file mode 100644 index 000000000..3cbad4743 --- /dev/null +++ b/crates/protocol/src/tx_data/eip2930.rs @@ -0,0 +1,78 @@ +//! This module contains the eip2930 transaction data type for a span batch. + +use crate::{SpanBatchError, SpanDecodingError}; +use alloy_consensus::{SignableTransaction, Signed, TxEip2930, TxEnvelope}; +use alloy_eips::eip2930::AccessList; +use alloy_primitives::{Address, Signature, TxKind, U256}; +use alloy_rlp::{Bytes, RlpDecodable, RlpEncodable}; + +/// The transaction data for an EIP-2930 transaction within a span batch. +#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)] +pub struct SpanBatchEip2930TransactionData { + /// The ETH value of the transaction. + pub value: U256, + /// The gas price of the transaction. + pub gas_price: U256, + /// Transaction calldata. + pub data: Bytes, + /// Access list, used to pre-warm storage slots through static declaration. + pub access_list: AccessList, +} + +impl SpanBatchEip2930TransactionData { + /// Converts [SpanBatchEip2930TransactionData] into a [TxEnvelope]. + pub fn to_enveloped_tx( + &self, + nonce: u64, + gas: u64, + to: Option
, + chain_id: u64, + signature: Signature, + ) -> Result { + let access_list_tx = TxEip2930 { + chain_id, + nonce, + gas_price: u128::from_be_bytes( + self.gas_price.to_be_bytes::<32>()[16..].try_into().map_err(|_| { + SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData) + })?, + ), + gas_limit: gas, + to: to.map_or(TxKind::Create, TxKind::Call), + value: self.value, + input: self.data.clone().into(), + access_list: self.access_list.clone(), + }; + let signature_hash = access_list_tx.signature_hash(); + let signed_access_list_tx = + Signed::new_unchecked(access_list_tx, signature, signature_hash); + Ok(TxEnvelope::Eip2930(signed_access_list_tx)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::SpanBatchTransactionData; + use alloc::vec::Vec; + use alloy_rlp::{Decodable, Encodable}; + + #[test] + fn encode_eip2930_tx_data_roundtrip() { + let access_list_tx = SpanBatchEip2930TransactionData { + value: U256::from(0xFF), + gas_price: U256::from(0xEE), + data: Bytes::from(alloc::vec![0x01, 0x02, 0x03]), + access_list: AccessList::default(), + }; + let mut encoded_buf = Vec::new(); + SpanBatchTransactionData::Eip2930(access_list_tx.clone()).encode(&mut encoded_buf); + + let decoded = SpanBatchTransactionData::decode(&mut encoded_buf.as_slice()).unwrap(); + let SpanBatchTransactionData::Eip2930(access_list_decoded) = decoded else { + panic!("Expected SpanBatchEip2930TransactionData, got {:?}", decoded); + }; + + assert_eq!(access_list_tx, access_list_decoded); + } +} diff --git a/crates/protocol/src/tx_data/legacy.rs b/crates/protocol/src/tx_data/legacy.rs new file mode 100644 index 000000000..e9ff5a457 --- /dev/null +++ b/crates/protocol/src/tx_data/legacy.rs @@ -0,0 +1,106 @@ +//! This module contains the legacy transaction data type for a span batch. + +use crate::{SpanBatchError, SpanDecodingError}; +use alloy_consensus::{SignableTransaction, Signed, TxEnvelope, TxLegacy}; +use alloy_primitives::{Address, Signature, TxKind, U256}; +use alloy_rlp::{Bytes, RlpDecodable, RlpEncodable}; + +/// The transaction data for a legacy transaction within a span batch. +#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)] +pub struct SpanBatchLegacyTransactionData { + /// The ETH value of the transaction. + pub value: U256, + /// The gas price of the transaction. + pub gas_price: U256, + /// Transaction calldata. + pub data: Bytes, +} + +impl SpanBatchLegacyTransactionData { + /// Converts [SpanBatchLegacyTransactionData] into a [TxEnvelope]. + pub fn to_enveloped_tx( + &self, + nonce: u64, + gas: u64, + to: Option
, + chain_id: u64, + signature: Signature, + ) -> Result { + let legacy_tx = TxLegacy { + chain_id: Some(chain_id), + nonce, + gas_price: u128::from_be_bytes( + self.gas_price.to_be_bytes::<32>()[16..].try_into().map_err(|_| { + SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData) + })?, + ), + gas_limit: gas, + to: to.map_or(TxKind::Create, TxKind::Call), + value: self.value, + input: self.data.clone().into(), + }; + let signature_hash = legacy_tx.signature_hash(); + let signed_legacy_tx = Signed::new_unchecked(legacy_tx, signature, signature_hash); + Ok(TxEnvelope::Legacy(signed_legacy_tx)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::SpanBatchTransactionData; + use alloc::vec::Vec; + use alloy_rlp::{Decodable, Encodable as _}; + // use alloy_primitives::B256; + + // #[test] + // fn to_enveloped_tx() { + // let legacy_tx = SpanBatchLegacyTransactionData { + // value: U256::from(0xFF), + // gas_price: U256::from(0xEE), + // data: Bytes::from(alloc::vec![0x01, 0x02, 0x03]), + // }; + // let nonce = 0x1234; + // let gas = 0x5678; + // let to = None; + // let chain_id = 0x9ABC; + // let signature = &[0x01; 65]; + // let signature = Signature::decode(&mut &signature[..]).unwrap(); + // let enveloped_tx = legacy_tx + // .to_enveloped_tx(nonce, gas, to, chain_id, signature) + // .unwrap(); + // let expected = TxEnvelope::Legacy(crate::types::network::Signed::new_unchecked( + // crate::types::TxLegacy { + // chain_id: Some(chain_id), + // nonce, + // gas_price: 0xEE, + // gas_limit: gas, + // to: crate::types::TxKind::Create, + // value: U256::from(0xFF), + // input: Bytes::from(alloc::vec![0x01, 0x02, 0x03]).into(), + // }, + // signature, + // B256::from([0x01; 32]), + // )); + // assert_eq!(enveloped_tx, expected); + // } + + #[test] + fn encode_legacy_tx_data_roundtrip() { + let legacy_tx = SpanBatchLegacyTransactionData { + value: U256::from(0xFF), + gas_price: U256::from(0xEE), + data: Bytes::from(alloc::vec![0x01, 0x02, 0x03]), + }; + + let mut encoded_buf = Vec::new(); + SpanBatchTransactionData::Legacy(legacy_tx.clone()).encode(&mut encoded_buf); + + let decoded = SpanBatchTransactionData::decode(&mut encoded_buf.as_slice()).unwrap(); + let SpanBatchTransactionData::Legacy(legacy_decoded) = decoded else { + panic!("Expected SpanBatchLegacyTransactionData, got {:?}", decoded); + }; + + assert_eq!(legacy_tx, legacy_decoded); + } +} diff --git a/crates/protocol/src/tx_data/mod.rs b/crates/protocol/src/tx_data/mod.rs new file mode 100644 index 000000000..06f13ad11 --- /dev/null +++ b/crates/protocol/src/tx_data/mod.rs @@ -0,0 +1,13 @@ +//! Contains all the Span Batch Transaction Data types. + +mod wrapper; +pub use wrapper::SpanBatchTransactionData; + +mod legacy; +pub use legacy::SpanBatchLegacyTransactionData; + +mod eip1559; +pub use eip1559::SpanBatchEip1559TransactionData; + +mod eip2930; +pub use eip2930::SpanBatchEip2930TransactionData; diff --git a/crates/protocol/src/tx_data/wrapper.rs b/crates/protocol/src/tx_data/wrapper.rs new file mode 100644 index 000000000..27520c198 --- /dev/null +++ b/crates/protocol/src/tx_data/wrapper.rs @@ -0,0 +1,131 @@ +//! This module contains the top level span batch transaction data type. + +use alloy_consensus::{Transaction, TxEnvelope, TxType}; +use alloy_primitives::{Address, Signature, U256}; +use alloy_rlp::{Bytes, Decodable, Encodable}; + +use crate::{ + SpanBatchEip1559TransactionData, SpanBatchEip2930TransactionData, SpanBatchError, + SpanBatchLegacyTransactionData, SpanDecodingError, +}; + +/// The typed transaction data for a transaction within a span batch. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SpanBatchTransactionData { + /// Legacy transaction data. + Legacy(SpanBatchLegacyTransactionData), + /// EIP-2930 transaction data. + Eip2930(SpanBatchEip2930TransactionData), + /// EIP-1559 transaction data. + Eip1559(SpanBatchEip1559TransactionData), +} + +impl Encodable for SpanBatchTransactionData { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + match self { + Self::Legacy(data) => { + data.encode(out); + } + Self::Eip2930(data) => { + out.put_u8(TxType::Eip2930 as u8); + data.encode(out); + } + Self::Eip1559(data) => { + out.put_u8(TxType::Eip1559 as u8); + data.encode(out); + } + } + } +} + +impl Decodable for SpanBatchTransactionData { + fn decode(r: &mut &[u8]) -> Result { + if !r.is_empty() && r[0] > 0x7F { + // Legacy transaction + return Ok(Self::Legacy(SpanBatchLegacyTransactionData::decode(r)?)); + } + // Non-legacy transaction (EIP-2718 envelope encoding) + Self::decode_typed(r) + } +} + +impl TryFrom<&TxEnvelope> for SpanBatchTransactionData { + type Error = SpanBatchError; + + fn try_from(tx_envelope: &TxEnvelope) -> Result { + match tx_envelope { + TxEnvelope::Legacy(s) => { + let s = s.tx(); + Ok(Self::Legacy(SpanBatchLegacyTransactionData { + value: s.value, + gas_price: U256::from(s.gas_price), + data: Bytes::from(s.input().to_vec()), + })) + } + TxEnvelope::Eip2930(s) => { + let s = s.tx(); + Ok(Self::Eip2930(SpanBatchEip2930TransactionData { + value: s.value, + gas_price: U256::from(s.gas_price), + data: Bytes::from(s.input().to_vec()), + access_list: s.access_list.clone(), + })) + } + TxEnvelope::Eip1559(s) => { + let s = s.tx(); + Ok(Self::Eip1559(SpanBatchEip1559TransactionData { + value: s.value, + max_fee_per_gas: U256::from(s.max_fee_per_gas), + max_priority_fee_per_gas: U256::from(s.max_priority_fee_per_gas), + data: Bytes::from(s.input().to_vec()), + access_list: s.access_list.clone(), + })) + } + _ => Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType)), + } + } +} + +impl SpanBatchTransactionData { + /// Returns the transaction type of the [SpanBatchTransactionData]. + pub const fn tx_type(&self) -> TxType { + match self { + Self::Legacy(_) => TxType::Legacy, + Self::Eip2930(_) => TxType::Eip2930, + Self::Eip1559(_) => TxType::Eip1559, + } + } + + /// Decodes a typed transaction into a [SpanBatchTransactionData] from a byte slice. + pub fn decode_typed(b: &[u8]) -> Result { + if b.len() <= 1 { + return Err(alloy_rlp::Error::Custom("Invalid transaction data")); + } + + match b[0].try_into().map_err(|_| alloy_rlp::Error::Custom("Invalid tx type"))? { + TxType::Eip2930 => { + Ok(Self::Eip2930(SpanBatchEip2930TransactionData::decode(&mut &b[1..])?)) + } + TxType::Eip1559 => { + Ok(Self::Eip1559(SpanBatchEip1559TransactionData::decode(&mut &b[1..])?)) + } + _ => Err(alloy_rlp::Error::Custom("Invalid transaction type")), + } + } + + /// Converts the [SpanBatchTransactionData] into a [TxEnvelope]. + pub fn to_enveloped_tx( + &self, + nonce: u64, + gas: u64, + to: Option
, + chain_id: u64, + signature: Signature, + ) -> Result { + match self { + Self::Legacy(data) => data.to_enveloped_tx(nonce, gas, to, chain_id, signature), + Self::Eip2930(data) => data.to_enveloped_tx(nonce, gas, to, chain_id, signature), + Self::Eip1559(data) => data.to_enveloped_tx(nonce, gas, to, chain_id, signature), + } + } +} diff --git a/crates/protocol/src/txs.rs b/crates/protocol/src/txs.rs new file mode 100644 index 000000000..5e552dbe0 --- /dev/null +++ b/crates/protocol/src/txs.rs @@ -0,0 +1,464 @@ +//! This module contains the [SpanBatchTransactions] type and logic for encoding and decoding +//! transactions in a span batch. + +use super::{ + convert_v_to_y_parity, is_protected_v, read_tx_data, SpanBatchBits, SpanBatchError, + SpanBatchSignature, SpanBatchTransactionData, SpanDecodingError, MAX_SPAN_BATCH_ELEMENTS, +}; +use alloc::vec::Vec; +use alloy_consensus::{Transaction, TxEnvelope, TxType}; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{Address, Bytes, U256}; +use alloy_rlp::{Buf, Decodable, Encodable}; + +/// This struct contains the decoded information for transactions in a span batch. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct SpanBatchTransactions { + /// The total number of transactions in a span batch. Must be manually set. + pub total_block_tx_count: u64, + /// The contract creation bits, standard span-batch bitlist. + pub contract_creation_bits: SpanBatchBits, + /// The y parity bits, standard span-batch bitlist. + pub y_parity_bits: SpanBatchBits, + /// The transaction signatures. + pub tx_sigs: Vec, + /// The transaction nonces + pub tx_nonces: Vec, + /// The transaction gas limits. + pub tx_gases: Vec, + /// The `to` addresses of the transactions. + pub tx_tos: Vec
, + /// The transaction data. + pub tx_datas: Vec>, + /// The protected bits, standard span-batch bitlist. + pub protected_bits: SpanBatchBits, + /// The types of the transactions. + pub tx_types: Vec, + /// Total legacy transaction count in the span batch. + pub legacy_tx_count: u64, +} + +impl SpanBatchTransactions { + /// Encodes the [SpanBatchTransactions] into a writer. + pub fn encode(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + self.encode_contract_creation_bits(w)?; + self.encode_y_parity_bits(w)?; + self.encode_tx_sigs_rs(w)?; + self.encode_tx_tos(w)?; + self.encode_tx_datas(w)?; + self.encode_tx_nonces(w)?; + self.encode_tx_gases(w)?; + self.encode_protected_bits(w)?; + Ok(()) + } + + /// Decodes the [SpanBatchTransactions] from a reader. + pub fn decode(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + self.decode_contract_creation_bits(r)?; + self.decode_y_parity_bits(r)?; + self.decode_tx_sigs_rs(r)?; + self.decode_tx_tos(r)?; + self.decode_tx_datas(r)?; + self.decode_tx_nonces(r)?; + self.decode_tx_gases(r)?; + self.decode_protected_bits(r)?; + Ok(()) + } + + /// Encode the contract creation bits into a writer. + pub fn encode_contract_creation_bits(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + SpanBatchBits::encode(w, self.total_block_tx_count as usize, &self.contract_creation_bits)?; + Ok(()) + } + + /// Encode the protected bits into a writer. + pub fn encode_protected_bits(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + SpanBatchBits::encode(w, self.legacy_tx_count as usize, &self.protected_bits)?; + Ok(()) + } + + /// Encode the y parity bits into a writer. + pub fn encode_y_parity_bits(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + SpanBatchBits::encode(w, self.total_block_tx_count as usize, &self.y_parity_bits)?; + Ok(()) + } + + /// Encode the transaction signatures into a writer (excluding `v` field). + pub fn encode_tx_sigs_rs(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + for sig in &self.tx_sigs { + w.extend_from_slice(&sig.r.to_be_bytes::<32>()); + w.extend_from_slice(&sig.s.to_be_bytes::<32>()); + } + Ok(()) + } + + /// Encode the transaction nonces into a writer. + pub fn encode_tx_nonces(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + let mut buf = [0u8; 10]; + for nonce in &self.tx_nonces { + let slice = unsigned_varint::encode::u64(*nonce, &mut buf); + w.extend_from_slice(slice); + } + Ok(()) + } + + /// Encode the transaction gas limits into a writer. + pub fn encode_tx_gases(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + let mut buf = [0u8; 10]; + for gas in &self.tx_gases { + let slice = unsigned_varint::encode::u64(*gas, &mut buf); + w.extend_from_slice(slice); + } + Ok(()) + } + + /// Encode the `to` addresses of the transactions into a writer. + pub fn encode_tx_tos(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + for to in &self.tx_tos { + w.extend_from_slice(to.as_ref()); + } + Ok(()) + } + + /// Encode the transaction data into a writer. + pub fn encode_tx_datas(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + for data in &self.tx_datas { + w.extend_from_slice(data); + } + Ok(()) + } + + /// Decode the contract creation bits from a reader. + pub fn decode_contract_creation_bits(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + if self.total_block_tx_count > MAX_SPAN_BATCH_ELEMENTS { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + + self.contract_creation_bits = SpanBatchBits::decode(r, self.total_block_tx_count as usize)?; + Ok(()) + } + + /// Decode the protected bits from a reader. + pub fn decode_protected_bits(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + if self.legacy_tx_count > MAX_SPAN_BATCH_ELEMENTS { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + + self.protected_bits = SpanBatchBits::decode(r, self.legacy_tx_count as usize)?; + Ok(()) + } + + /// Decode the y parity bits from a reader. + pub fn decode_y_parity_bits(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + self.y_parity_bits = SpanBatchBits::decode(r, self.total_block_tx_count as usize)?; + Ok(()) + } + + /// Decode the transaction signatures from a reader (excluding `v` field). + pub fn decode_tx_sigs_rs(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let mut sigs = Vec::with_capacity(self.total_block_tx_count as usize); + for _ in 0..self.total_block_tx_count { + let r_val = U256::from_be_slice(&r[..32]); + let s_val = U256::from_be_slice(&r[32..64]); + sigs.push(SpanBatchSignature { v: 0, r: r_val, s: s_val }); + r.advance(64); + } + self.tx_sigs = sigs; + Ok(()) + } + + /// Decode the transaction nonces from a reader. + pub fn decode_tx_nonces(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let mut nonces = Vec::with_capacity(self.total_block_tx_count as usize); + for _ in 0..self.total_block_tx_count { + let (nonce, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::TxNonces))?; + nonces.push(nonce); + *r = remaining; + } + self.tx_nonces = nonces; + Ok(()) + } + + /// Decode the transaction gas limits from a reader. + pub fn decode_tx_gases(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let mut gases = Vec::with_capacity(self.total_block_tx_count as usize); + for _ in 0..self.total_block_tx_count { + let (gas, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::TxNonces))?; + gases.push(gas); + *r = remaining; + } + self.tx_gases = gases; + Ok(()) + } + + /// Decode the `to` addresses of the transactions from a reader. + pub fn decode_tx_tos(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let mut tos = Vec::with_capacity(self.total_block_tx_count as usize); + let contract_creation_count = self.contract_creation_count(); + for _ in 0..(self.total_block_tx_count - contract_creation_count) { + let to = Address::from_slice(&r[..20]); + tos.push(to); + r.advance(20); + } + self.tx_tos = tos; + Ok(()) + } + + /// Decode the transaction data from a reader. + pub fn decode_tx_datas(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let mut tx_datas = Vec::new(); + let mut tx_types = Vec::new(); + + // Do not need the transaction data header because the RLP stream already includes the + // length information. + for _ in 0..self.total_block_tx_count { + let (tx_data, tx_type) = read_tx_data(r)?; + tx_datas.push(tx_data); + tx_types.push(tx_type); + if matches!(tx_type, TxType::Legacy) { + self.legacy_tx_count += 1; + } + } + + self.tx_datas = tx_datas; + self.tx_types = tx_types; + + Ok(()) + } + + /// Returns the number of contract creation transactions in the span batch. + pub fn contract_creation_count(&self) -> u64 { + self.contract_creation_bits.0.iter().map(|b| b.count_ones() as u64).sum() + } + + /// Recover the `v` values of the transaction signatures. + pub fn recover_v(&mut self, chain_id: u64) -> Result<(), SpanBatchError> { + if self.tx_sigs.len() != self.tx_types.len() { + return Err(SpanBatchError::Decoding(SpanDecodingError::TypeSignatureLenMismatch)); + } + let mut protected_bits_idx = 0; + for (i, tx_type) in self.tx_types.iter().enumerate() { + let bit = self.y_parity_bits.get_bit(i).ok_or(SpanBatchError::BitfieldTooLong)?; + let v = match tx_type { + TxType::Legacy => { + // Legacy transaction + let protected_bit = self.protected_bits.get_bit(protected_bits_idx); + protected_bits_idx += 1; + if protected_bit.is_none() || protected_bit.is_some_and(|b| b == 0) { + Ok(27 + bit as u64) + } else { + // EIP-155 + Ok(chain_id * 2 + 35 + bit as u64) + } + } + TxType::Eip2930 | TxType::Eip1559 => Ok(bit as u64), + _ => Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType)), + }?; + self.tx_sigs.get_mut(i).expect("Transaction must exist").v = v; + } + Ok(()) + } + + /// Retrieve all of the raw transactions from the [SpanBatchTransactions]. + pub fn full_txs(&self, chain_id: u64) -> Result>, SpanBatchError> { + let mut txs = Vec::new(); + let mut to_idx = 0; + for idx in 0..self.total_block_tx_count { + let mut datas = self.tx_datas[idx as usize].as_slice(); + let tx = SpanBatchTransactionData::decode(&mut datas) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let nonce = self + .tx_nonces + .get(idx as usize) + .ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let gas = self + .tx_gases + .get(idx as usize) + .ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let bit = self + .contract_creation_bits + .get_bit(idx as usize) + .ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let to = if bit == 0 { + if self.tx_tos.len() <= to_idx { + return Err(SpanBatchError::Decoding( + SpanDecodingError::InvalidTransactionData, + )); + } + to_idx += 1; + Some(self.tx_tos[to_idx - 1]) + } else { + None + }; + let sig = *self + .tx_sigs + .get(idx as usize) + .ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let tx_envelope = tx.to_enveloped_tx(*nonce, *gas, to, chain_id, sig.try_into()?)?; + let mut buf = Vec::new(); + tx_envelope.encode_2718(&mut buf); + txs.push(buf); + } + Ok(txs) + } + + /// Add raw transactions into the [SpanBatchTransactions]. + pub fn add_txs(&mut self, txs: Vec, chain_id: u64) -> Result<(), SpanBatchError> { + let total_block_tx_count = txs.len() as u64; + let offset = self.total_block_tx_count; + + for i in 0..total_block_tx_count { + let tx_enveloped = TxEnvelope::decode(&mut txs[i as usize].as_ref()) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let span_batch_tx = SpanBatchTransactionData::try_from(&tx_enveloped)?; + + let tx_type = tx_enveloped.tx_type(); + if matches!(tx_type, TxType::Legacy) { + let protected_bit = is_protected_v(&tx_enveloped); + self.protected_bits.set_bit(self.legacy_tx_count as usize, protected_bit); + self.legacy_tx_count += 1; + } + + let (signature, to, nonce, gas, tx_chain_id) = match &tx_enveloped { + TxEnvelope::Legacy(tx) => { + let (tx, sig) = (tx.tx(), tx.signature()); + (sig, tx.to(), tx.nonce(), tx.gas_limit(), tx.chain_id()) + } + TxEnvelope::Eip2930(tx) => { + let (tx, sig) = (tx.tx(), tx.signature()); + (sig, tx.to(), tx.nonce(), tx.gas_limit(), tx.chain_id()) + } + TxEnvelope::Eip1559(tx) => { + let (tx, sig) = (tx.tx(), tx.signature()); + (sig, tx.to(), tx.nonce(), tx.gas_limit(), tx.chain_id()) + } + _ => { + return Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)) + } + }; + + if is_protected_v(&tx_enveloped) + && tx_chain_id + .ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))? + != chain_id + { + return Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)); + } + + let signature_v = signature.v().to_u64(); + let y_parity_bit = convert_v_to_y_parity(signature_v, tx_type)?; + let contract_creation_bit = match to { + Some(address) => { + self.tx_tos.push(address); + 0 + } + None => 1, + }; + let mut tx_data_buf = Vec::new(); + span_batch_tx.encode(&mut tx_data_buf); + + self.tx_sigs.push((*signature).into()); + self.contract_creation_bits.set_bit((i + offset) as usize, contract_creation_bit == 1); + self.y_parity_bits.set_bit((i + offset) as usize, y_parity_bit); + self.tx_nonces.push(nonce); + self.tx_datas.push(tx_data_buf); + self.tx_gases.push(gas); + self.tx_types.push(tx_type); + } + self.total_block_tx_count += total_block_tx_count; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_consensus::{Signed, TxEip1559, TxEip2930, TxLegacy}; + use alloy_primitives::{address, Signature, TxKind}; + + #[test] + fn test_span_batch_transactions_add_empty_txs() { + let mut span_batch_txs = SpanBatchTransactions::default(); + let txs = vec![]; + let chain_id = 1; + let result = span_batch_txs.add_txs(txs, chain_id); + assert!(result.is_ok()); + assert_eq!(span_batch_txs.total_block_tx_count, 0); + } + + #[test] + fn test_span_batch_transactions_add_invalid_legacy_parity_decoding() { + let sig = Signature::test_signature(); + let to = address!("0123456789012345678901234567890123456789"); + let tx = TxEnvelope::Legacy(Signed::new_unchecked( + TxLegacy { to: TxKind::Call(to), ..Default::default() }, + sig, + Default::default(), + )); + let mut span_batch_txs = SpanBatchTransactions::default(); + let mut buf = vec![]; + tx.encode(&mut buf); + let txs = vec![Bytes::from(buf)]; + let chain_id = 1; + let err = span_batch_txs.add_txs(txs, chain_id).unwrap_err(); + assert_eq!(err, SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)); + } + + #[test] + fn test_span_batch_transactions_add_eip2930_tx_wrong_chain_id() { + let sig = Signature::test_signature(); + let to = address!("0123456789012345678901234567890123456789"); + let tx = TxEnvelope::Eip2930(Signed::new_unchecked( + TxEip2930 { to: TxKind::Call(to), ..Default::default() }, + sig, + Default::default(), + )); + let mut span_batch_txs = SpanBatchTransactions::default(); + let mut buf = vec![]; + tx.encode(&mut buf); + let txs = vec![Bytes::from(buf)]; + let chain_id = 1; + let err = span_batch_txs.add_txs(txs, chain_id).unwrap_err(); + assert_eq!(err, SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)); + } + + #[test] + fn test_span_batch_transactions_add_eip2930_tx() { + let sig = Signature::test_signature(); + let to = address!("0123456789012345678901234567890123456789"); + let tx = TxEnvelope::Eip2930(Signed::new_unchecked( + TxEip2930 { to: TxKind::Call(to), chain_id: 1, ..Default::default() }, + sig, + Default::default(), + )); + let mut span_batch_txs = SpanBatchTransactions::default(); + let mut buf = vec![]; + tx.encode(&mut buf); + let txs = vec![Bytes::from(buf)]; + let chain_id = 1; + let result = span_batch_txs.add_txs(txs, chain_id); + assert_eq!(result, Ok(())); + assert_eq!(span_batch_txs.total_block_tx_count, 1); + } + + #[test] + fn test_span_batch_transactions_add_eip1559_tx() { + let sig = Signature::test_signature(); + let to = address!("0123456789012345678901234567890123456789"); + let tx = TxEnvelope::Eip1559(Signed::new_unchecked( + TxEip1559 { to: TxKind::Call(to), chain_id: 1, ..Default::default() }, + sig, + Default::default(), + )); + let mut span_batch_txs = SpanBatchTransactions::default(); + let mut buf = vec![]; + tx.encode(&mut buf); + let txs = vec![Bytes::from(buf)]; + let chain_id = 1; + let result = span_batch_txs.add_txs(txs, chain_id); + assert_eq!(result, Ok(())); + assert_eq!(span_batch_txs.total_block_tx_count, 1); + } +} diff --git a/crates/protocol/src/utils.rs b/crates/protocol/src/utils.rs index e415e1ad0..337085f29 100644 --- a/crates/protocol/src/utils.rs +++ b/crates/protocol/src/utils.rs @@ -1,10 +1,17 @@ //! Utility methods used by protocol types. -use crate::{block_info::DecodeError, L1BlockInfoBedrock, L1BlockInfoEcotone, L1BlockInfoTx}; +use alloc::vec::Vec; +use alloy_consensus::{TxEnvelope, TxType}; use alloy_primitives::B256; +use alloy_rlp::{Buf, Header}; use op_alloy_consensus::{OpBlock, OpTxEnvelope}; use op_alloy_genesis::{RollupConfig, SystemConfig}; +use crate::{ + block_info::DecodeError, L1BlockInfoBedrock, L1BlockInfoEcotone, L1BlockInfoTx, SpanBatchError, + SpanDecodingError, +}; + /// Returns if the given `value` is a deposit transaction. pub fn starts_with_2718_deposit(value: &B) -> bool where @@ -51,6 +58,75 @@ impl From for OpBlockConversionError { } } +/// Reads transaction data from a reader. +pub fn read_tx_data(r: &mut &[u8]) -> Result<(Vec, TxType), SpanBatchError> { + let mut tx_data = Vec::new(); + let first_byte = + *r.first().ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let mut tx_type = 0; + if first_byte <= 0x7F { + // EIP-2718: Non-legacy tx, so write tx type + tx_type = first_byte; + tx_data.push(tx_type); + r.advance(1); + } + + // Read the RLP header with a different reader pointer. This prevents the initial pointer from + // being advanced in the case that what we read is invalid. + let rlp_header = Header::decode(&mut (**r).as_ref()) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + + let tx_payload = if rlp_header.list { + // Grab the raw RLP for the transaction data from `r`. It was unaffected since we copied it. + let payload_length_with_header = rlp_header.payload_length + rlp_header.length(); + let payload = r[0..payload_length_with_header].to_vec(); + r.advance(payload_length_with_header); + Ok(payload) + } else { + Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)) + }?; + tx_data.extend_from_slice(&tx_payload); + + Ok(( + tx_data, + tx_type + .try_into() + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType))?, + )) +} + +/// Converts a `v` value to a y parity bit, from the transaaction type. +pub const fn convert_v_to_y_parity(v: u64, tx_type: TxType) -> Result { + match tx_type { + TxType::Legacy => { + if v != 27 && v != 28 { + // EIP-155: v = 2 * chain_id + 35 + yParity + Ok((v - 35) & 1 == 1) + } else { + // Unprotected legacy txs must have v = 27 or 28 + Ok(v - 27 == 1) + } + } + TxType::Eip2930 | TxType::Eip1559 => Ok(v == 1), + _ => Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType)), + } +} + +/// Checks if the signature of the passed [TxEnvelope] is protected. +pub const fn is_protected_v(tx: &TxEnvelope) -> bool { + match tx { + TxEnvelope::Legacy(tx) => { + let v = tx.signature().v().to_u64(); + if 64 - v.leading_zeros() <= 8 { + return v != 27 && v != 28 && v != 1 && v != 0; + } + // anything not 27 or 28 is considered protected + true + } + _ => true, + } +} + /// Converts the [OpBlock] to a partial [SystemConfig]. pub fn to_system_config( block: &OpBlock, @@ -227,15 +303,75 @@ fn u24(input: &[u8], idx: u32) -> u32 { #[cfg(test)] mod tests { use super::*; + use alloy_consensus::{ + Signed, TxEip1559, TxEip2930, TxEip4844, TxEip4844Variant, TxEip7702, TxLegacy, + }; + use alloy_primitives::{b256, Signature}; use alloy_sol_types::{sol, SolCall}; use revm::{ db::BenchmarkDB, primitives::{address, bytes, Bytecode, Bytes, TxKind, U256}, Evm, }; + use rstest::rstest; use std::vec::Vec; - use rstest::rstest; + #[test] + fn test_convert_v_to_y_parity() { + assert_eq!(convert_v_to_y_parity(27, TxType::Legacy), Ok(false)); + assert_eq!(convert_v_to_y_parity(28, TxType::Legacy), Ok(true)); + assert_eq!(convert_v_to_y_parity(36, TxType::Legacy), Ok(true)); + assert_eq!(convert_v_to_y_parity(37, TxType::Legacy), Ok(false)); + assert_eq!(convert_v_to_y_parity(1, TxType::Eip2930), Ok(true)); + assert_eq!(convert_v_to_y_parity(1, TxType::Eip1559), Ok(true)); + assert_eq!( + convert_v_to_y_parity(1, TxType::Eip4844), + Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType)) + ); + assert_eq!( + convert_v_to_y_parity(0, TxType::Eip7702), + Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType)) + ); + } + + #[test] + fn test_is_protected_v() { + let sig = Signature::test_signature(); + assert!(!is_protected_v(&TxEnvelope::Legacy(Signed::new_unchecked( + TxLegacy::default(), + sig, + Default::default(), + )))); + let r = b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"); + let s = b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"); + let v = 27; + let valid_sig = Signature::from_scalars_and_parity(r, s, v).unwrap(); + assert!(!is_protected_v(&TxEnvelope::Legacy(Signed::new_unchecked( + TxLegacy::default(), + valid_sig, + Default::default(), + )))); + assert!(is_protected_v(&TxEnvelope::Eip2930(Signed::new_unchecked( + TxEip2930::default(), + sig, + Default::default(), + )))); + assert!(is_protected_v(&TxEnvelope::Eip1559(Signed::new_unchecked( + TxEip1559::default(), + sig, + Default::default(), + )))); + assert!(is_protected_v(&TxEnvelope::Eip4844(Signed::new_unchecked( + TxEip4844Variant::TxEip4844(TxEip4844::default()), + sig, + Default::default(), + )))); + assert!(is_protected_v(&TxEnvelope::Eip7702(Signed::new_unchecked( + TxEip7702::default(), + sig, + Default::default(), + )))); + } #[rstest] #[case::empty(&[], 0)] diff --git a/crates/protocol/src/validity.rs b/crates/protocol/src/validity.rs new file mode 100644 index 000000000..c31112408 --- /dev/null +++ b/crates/protocol/src/validity.rs @@ -0,0 +1,40 @@ +//! Contains the [BatchValidity] and its encodings. + +/// Batch Validity +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BatchValidity { + /// The batch is invalid now and in the future, unless we reorg + Drop, + /// The batch is valid and should be processed + Accept, + /// We are lacking L1 information until we can proceed batch filtering + Undecided, + /// The batch may be valid, but cannot be processed yet and should be checked again later + Future, + /// Introduced in Holocene, a special variant of the Drop variant that signals not to flush + /// the active batch and channel, in the case of processing an old batch + Past, +} + +impl BatchValidity { + /// Returns if the batch is accepted. + pub const fn is_accept(&self) -> bool { + matches!(self, Self::Accept) + } + + /// Returns if the batch is dropped. + pub const fn is_drop(&self) -> bool { + matches!(self, Self::Drop) + } + + /// Returns if the batch is outdated. + pub const fn is_outdated(&self) -> bool { + matches!(self, Self::Past) + } + + /// Returns if the batch is future. + pub const fn is_future(&self) -> bool { + matches!(self, Self::Future) + } +}