diff --git a/Cargo.lock b/Cargo.lock index 8cca54c46..22304b806 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,6 +141,21 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -249,6 +264,23 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.1" @@ -328,6 +360,7 @@ dependencies = [ "async-trait", "hashbrown", "miniz_oxide", + "proptest", "serde", "tokio", "unsigned-varint", @@ -344,6 +377,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.153" @@ -522,14 +561,26 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ + "bit-set", + "bit-vec", "bitflags 2.4.2", + "lazy_static", "num-traits", "rand", "rand_chacha", "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", "unarray", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.35" @@ -545,6 +596,8 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha", "rand_core", ] @@ -563,6 +616,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "rand_xorshift" @@ -582,6 +638,12 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "ruint" version = "1.11.1" @@ -631,6 +693,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -812,6 +886,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/crates/derive/Cargo.toml b/crates/derive/Cargo.toml index fbdb6947a..5adf8b0cd 100644 --- a/crates/derive/Cargo.toml +++ b/crates/derive/Cargo.toml @@ -26,6 +26,7 @@ serde = { version = "1.0.197", default-features = false, features = ["derive"], [dev-dependencies] tokio = { version = "1.36", features = ["full"] } +proptest = "1.4.0" [features] serde = ["dep:serde", "alloy-primitives/serde"] diff --git a/crates/derive/src/stages/channel_reader.rs b/crates/derive/src/stages/channel_reader.rs index 05c2edc78..abebafbda 100644 --- a/crates/derive/src/stages/channel_reader.rs +++ b/crates/derive/src/stages/channel_reader.rs @@ -8,7 +8,7 @@ use crate::{ use alloc::vec::Vec; use anyhow::anyhow; use core::fmt::Debug; -use miniz_oxide::inflate::decompress_to_vec; +use miniz_oxide::inflate::decompress_to_vec_zlib; /// [ChannelReader] is a stateful stage that does the following: #[derive(Debug)] @@ -45,7 +45,7 @@ where match self .next_batch .as_mut() - .unwrap() + .expect("Cannot be None") .next_batch() .ok_or(StageError::NotEnoughData) { @@ -88,15 +88,26 @@ pub(crate) struct BatchReader { data: Option>, /// Decompressed data. decompressed: Vec, + /// The current cursor in the `decompressed` data. + cursor: usize, } impl BatchReader { /// Pulls out the next batch from the reader. pub(crate) fn next_batch(&mut self) -> Option { + // If the data is not already decompressed, decompress it. if let Some(data) = self.data.take() { - self.decompressed = decompress_to_vec(&data).ok()?; + let decompressed_data = decompress_to_vec_zlib(&data).ok()?; + self.decompressed = decompressed_data; } - let batch = Batch::decode(&mut self.decompressed.as_ref()).ok()?; + + // Decompress and RLP decode the batch data, before finally decoding the batch itself. + let mut decompressed_reader = self.decompressed.as_slice(); + let batch = Batch::decode(&mut decompressed_reader).ok()?; + + // Advance the cursor on the reader. + self.cursor += self.decompressed.len() - decompressed_reader.len(); + Some(batch) } } @@ -106,6 +117,39 @@ impl From<&[u8]> for BatchReader { Self { data: Some(data.to_vec()), decompressed: Vec::new(), + cursor: 0, + } + } +} + +impl From> for BatchReader { + fn from(data: Vec) -> Self { + Self { + data: Some(data), + decompressed: Vec::new(), + cursor: 0, } } } + +#[cfg(test)] +mod test { + use crate::{stages::channel_reader::BatchReader, types::BatchType}; + use alloc::vec; + use miniz_oxide::deflate::compress_to_vec_zlib; + + // TODO(clabby): More tests here for multiple batches, integration w/ channel bank, etc. + + #[test] + fn test_batch_reader() { + let raw_data = include_bytes!("../../testdata/raw_batch.hex"); + let mut typed_data = vec![BatchType::Span as u8]; + typed_data.extend_from_slice(raw_data.as_slice()); + + let compressed_raw_data = compress_to_vec_zlib(typed_data.as_slice(), 5); + let mut reader = BatchReader::from(compressed_raw_data); + reader.next_batch().unwrap(); + + assert_eq!(reader.cursor, typed_data.len()); + } +} diff --git a/crates/derive/src/types/eips/eip1559/basefee.rs b/crates/derive/src/types/alloy/eips/eip1559/basefee.rs similarity index 100% rename from crates/derive/src/types/eips/eip1559/basefee.rs rename to crates/derive/src/types/alloy/eips/eip1559/basefee.rs diff --git a/crates/derive/src/types/eips/eip1559/constants.rs b/crates/derive/src/types/alloy/eips/eip1559/constants.rs similarity index 100% rename from crates/derive/src/types/eips/eip1559/constants.rs rename to crates/derive/src/types/alloy/eips/eip1559/constants.rs diff --git a/crates/derive/src/types/eips/eip1559/helpers.rs b/crates/derive/src/types/alloy/eips/eip1559/helpers.rs similarity index 98% rename from crates/derive/src/types/eips/eip1559/helpers.rs rename to crates/derive/src/types/alloy/eips/eip1559/helpers.rs index ff87d790c..6987c4de7 100644 --- a/crates/derive/src/types/eips/eip1559/helpers.rs +++ b/crates/derive/src/types/alloy/eips/eip1559/helpers.rs @@ -63,7 +63,7 @@ pub fn calc_next_block_base_fee( #[cfg(test)] mod tests { - use crate::types::eips::eip1559::{MIN_PROTOCOL_BASE_FEE, MIN_PROTOCOL_BASE_FEE_U256}; + use crate::types::eip1559::{MIN_PROTOCOL_BASE_FEE, MIN_PROTOCOL_BASE_FEE_U256}; use super::*; diff --git a/crates/derive/src/types/eips/eip1559/mod.rs b/crates/derive/src/types/alloy/eips/eip1559/mod.rs similarity index 100% rename from crates/derive/src/types/eips/eip1559/mod.rs rename to crates/derive/src/types/alloy/eips/eip1559/mod.rs diff --git a/crates/derive/src/types/eips/eip2718.rs b/crates/derive/src/types/alloy/eips/eip2718.rs similarity index 100% rename from crates/derive/src/types/eips/eip2718.rs rename to crates/derive/src/types/alloy/eips/eip2718.rs diff --git a/crates/derive/src/types/eips/eip2930.rs b/crates/derive/src/types/alloy/eips/eip2930.rs similarity index 100% rename from crates/derive/src/types/eips/eip2930.rs rename to crates/derive/src/types/alloy/eips/eip2930.rs diff --git a/crates/derive/src/types/eips/eip4788.rs b/crates/derive/src/types/alloy/eips/eip4788.rs similarity index 100% rename from crates/derive/src/types/eips/eip4788.rs rename to crates/derive/src/types/alloy/eips/eip4788.rs diff --git a/crates/derive/src/types/eips/eip4844.rs b/crates/derive/src/types/alloy/eips/eip4844.rs similarity index 100% rename from crates/derive/src/types/eips/eip4844.rs rename to crates/derive/src/types/alloy/eips/eip4844.rs diff --git a/crates/derive/src/types/eips/merge.rs b/crates/derive/src/types/alloy/eips/merge.rs similarity index 100% rename from crates/derive/src/types/eips/merge.rs rename to crates/derive/src/types/alloy/eips/merge.rs diff --git a/crates/derive/src/types/eips/mod.rs b/crates/derive/src/types/alloy/eips/mod.rs similarity index 100% rename from crates/derive/src/types/eips/mod.rs rename to crates/derive/src/types/alloy/eips/mod.rs diff --git a/crates/derive/src/types/header.rs b/crates/derive/src/types/alloy/header.rs similarity index 99% rename from crates/derive/src/types/header.rs rename to crates/derive/src/types/alloy/header.rs index 42a844377..e78955035 100644 --- a/crates/derive/src/types/header.rs +++ b/crates/derive/src/types/alloy/header.rs @@ -1,9 +1,7 @@ use crate::types::{ - eips::{ - eip1559::{calc_next_block_base_fee, BaseFeeParams}, - eip4844::{calc_blob_gasprice, calc_excess_blob_gas}, - }, - network::Sealable, + eip1559::{calc_next_block_base_fee, BaseFeeParams}, + eip4844::{calc_blob_gasprice, calc_excess_blob_gas}, + Sealable, }; use alloc::vec::Vec; use alloy_primitives::{b256, keccak256, Address, BlockNumber, Bloom, Bytes, B256, B64, U256}; diff --git a/crates/derive/src/types/alloy/mod.rs b/crates/derive/src/types/alloy/mod.rs new file mode 100644 index 000000000..379d49645 --- /dev/null +++ b/crates/derive/src/types/alloy/mod.rs @@ -0,0 +1,19 @@ +//! This module contains `alloy` types that have been ported from various alloy crates to support `no_std`. + +mod transaction; +pub use transaction::{TxDeposit, TxEip1559, TxEip2930, TxEip4844, TxEnvelope, TxLegacy, TxType}; + +mod network; +pub use network::{Receipt as NetworkReceipt, Sealable, Sealed, Signed, Transaction, TxKind}; + +mod header; +pub use header::{Header, EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH}; + +mod receipt; +pub use receipt::{Receipt, ReceiptWithBloom}; + +mod eips; +pub use eips::{ + calc_blob_gasprice, calc_excess_blob_gas, calc_next_block_base_fee, eip1559, eip2718, eip2930, + eip4788, eip4844, +}; diff --git a/crates/derive/src/types/network/mod.rs b/crates/derive/src/types/alloy/network/mod.rs similarity index 86% rename from crates/derive/src/types/network/mod.rs rename to crates/derive/src/types/alloy/network/mod.rs index b9762275d..da5af84d1 100644 --- a/crates/derive/src/types/network/mod.rs +++ b/crates/derive/src/types/alloy/network/mod.rs @@ -2,7 +2,7 @@ #![allow(unused, unreachable_pub)] -use crate::types::eips::eip2718::Eip2718Envelope; +use crate::types::eip2718::Eip2718Envelope; use alloc::vec::Vec; use alloy_primitives::B256; diff --git a/crates/derive/src/types/network/receipt.rs b/crates/derive/src/types/alloy/network/receipt.rs similarity index 100% rename from crates/derive/src/types/network/receipt.rs rename to crates/derive/src/types/alloy/network/receipt.rs diff --git a/crates/derive/src/types/network/sealed.rs b/crates/derive/src/types/alloy/network/sealed.rs similarity index 100% rename from crates/derive/src/types/network/sealed.rs rename to crates/derive/src/types/alloy/network/sealed.rs diff --git a/crates/derive/src/types/network/transaction/common.rs b/crates/derive/src/types/alloy/network/transaction/common.rs similarity index 100% rename from crates/derive/src/types/network/transaction/common.rs rename to crates/derive/src/types/alloy/network/transaction/common.rs diff --git a/crates/derive/src/types/network/transaction/mod.rs b/crates/derive/src/types/alloy/network/transaction/mod.rs similarity index 100% rename from crates/derive/src/types/network/transaction/mod.rs rename to crates/derive/src/types/alloy/network/transaction/mod.rs diff --git a/crates/derive/src/types/network/transaction/signed.rs b/crates/derive/src/types/alloy/network/transaction/signed.rs similarity index 98% rename from crates/derive/src/types/network/transaction/signed.rs rename to crates/derive/src/types/alloy/network/transaction/signed.rs index 1c272e7f9..761cfc830 100644 --- a/crates/derive/src/types/network/transaction/signed.rs +++ b/crates/derive/src/types/alloy/network/transaction/signed.rs @@ -1,4 +1,4 @@ -use crate::types::network::Transaction; +use crate::types::Transaction; use alloc::{vec, vec::Vec}; use alloy_primitives::{Signature, B256}; use alloy_rlp::BufMut; diff --git a/crates/derive/src/types/receipt.rs b/crates/derive/src/types/alloy/receipt.rs similarity index 99% rename from crates/derive/src/types/receipt.rs rename to crates/derive/src/types/alloy/receipt.rs index 18df6641b..81f48b7dc 100644 --- a/crates/derive/src/types/receipt.rs +++ b/crates/derive/src/types/alloy/receipt.rs @@ -1,11 +1,10 @@ //! This module contains the receipt types used within the derivation pipeline. -use core::cmp::Ordering; - -use crate::types::transaction::TxType; +use crate::types::TxType; use alloc::vec::Vec; use alloy_primitives::{Bloom, Log}; use alloy_rlp::{length_of_length, Buf, BufMut, BytesMut, Decodable, Encodable}; +use core::cmp::Ordering; /// Receipt containing result of transaction execution. #[derive(Clone, Debug, PartialEq, Eq, Default)] diff --git a/crates/derive/src/types/transaction/deposit.rs b/crates/derive/src/types/alloy/transaction/deposit.rs similarity index 98% rename from crates/derive/src/types/transaction/deposit.rs rename to crates/derive/src/types/alloy/transaction/deposit.rs index 0003df168..763c388a0 100644 --- a/crates/derive/src/types/transaction/deposit.rs +++ b/crates/derive/src/types/alloy/transaction/deposit.rs @@ -1,7 +1,4 @@ -use crate::types::{ - network::{Signed, Transaction, TxKind}, - transaction::TxType, -}; +use crate::types::{Signed, Transaction, TxKind, TxType}; use alloc::vec::Vec; use alloy_primitives::{keccak256, Address, Bytes, ChainId, Signature, B256, U256}; use alloy_rlp::{ diff --git a/crates/derive/src/types/transaction/eip1559.rs b/crates/derive/src/types/alloy/transaction/eip1559.rs similarity index 99% rename from crates/derive/src/types/transaction/eip1559.rs rename to crates/derive/src/types/alloy/transaction/eip1559.rs index 9acb8a887..b80dc55ec 100644 --- a/crates/derive/src/types/transaction/eip1559.rs +++ b/crates/derive/src/types/alloy/transaction/eip1559.rs @@ -1,8 +1,4 @@ -use crate::types::{ - eips::eip2930::AccessList, - network::{Signed, Transaction, TxKind}, - transaction::TxType, -}; +use crate::types::{eip2930::AccessList, Signed, Transaction, TxKind, TxType}; use alloc::vec::Vec; use alloy_primitives::{keccak256, Bytes, ChainId, Signature, U256}; use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header}; diff --git a/crates/derive/src/types/transaction/eip2930.rs b/crates/derive/src/types/alloy/transaction/eip2930.rs similarity index 98% rename from crates/derive/src/types/transaction/eip2930.rs rename to crates/derive/src/types/alloy/transaction/eip2930.rs index 0cb861bd8..2c894029a 100644 --- a/crates/derive/src/types/transaction/eip2930.rs +++ b/crates/derive/src/types/alloy/transaction/eip2930.rs @@ -1,8 +1,4 @@ -use crate::types::{ - eips::eip2930::AccessList, - network::{Signed, Transaction, TxKind}, - transaction::TxType, -}; +use crate::types::{eip2930::AccessList, Signed, Transaction, TxKind, TxType}; use alloc::vec::Vec; use alloy_primitives::{keccak256, Bytes, ChainId, Signature, U256}; use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header}; diff --git a/crates/derive/src/types/transaction/eip4844.rs b/crates/derive/src/types/alloy/transaction/eip4844.rs similarity index 98% rename from crates/derive/src/types/transaction/eip4844.rs rename to crates/derive/src/types/alloy/transaction/eip4844.rs index cdb7229dc..06f457513 100644 --- a/crates/derive/src/types/transaction/eip4844.rs +++ b/crates/derive/src/types/alloy/transaction/eip4844.rs @@ -1,7 +1,5 @@ use crate::types::{ - eips::{eip2930::AccessList, eip4844::DATA_GAS_PER_BLOB}, - network::{Signed, Transaction, TxKind}, - transaction::TxType, + eip2930::AccessList, eip4844::DATA_GAS_PER_BLOB, Signed, Transaction, TxKind, TxType, }; use alloc::vec::Vec; use alloy_primitives::{keccak256, Bytes, ChainId, Signature, B256, U256}; diff --git a/crates/derive/src/types/transaction/envelope.rs b/crates/derive/src/types/alloy/transaction/envelope.rs similarity index 97% rename from crates/derive/src/types/transaction/envelope.rs rename to crates/derive/src/types/alloy/transaction/envelope.rs index c74bef8ee..ba18f225a 100644 --- a/crates/derive/src/types/transaction/envelope.rs +++ b/crates/derive/src/types/alloy/transaction/envelope.rs @@ -1,7 +1,6 @@ use crate::types::{ - eips::eip2718::{Decodable2718, Eip2718Error, Encodable2718}, - network::Signed, - transaction::{TxDeposit, TxEip1559, TxEip2930, TxEip4844, TxLegacy}, + eip2718::{Decodable2718, Eip2718Error, Encodable2718}, + Signed, TxDeposit, TxEip1559, TxEip2930, TxEip4844, TxLegacy, }; use alloy_rlp::{length_of_length, Decodable, Encodable}; diff --git a/crates/derive/src/types/transaction/legacy.rs b/crates/derive/src/types/alloy/transaction/legacy.rs similarity index 99% rename from crates/derive/src/types/transaction/legacy.rs rename to crates/derive/src/types/alloy/transaction/legacy.rs index 8cea88bdc..671578900 100644 --- a/crates/derive/src/types/transaction/legacy.rs +++ b/crates/derive/src/types/alloy/transaction/legacy.rs @@ -1,4 +1,4 @@ -use crate::types::network::{Signed, Transaction, TxKind}; +use crate::types::{Signed, Transaction, TxKind}; use alloc::vec::Vec; use alloy_primitives::{keccak256, Bytes, ChainId, Signature, U256}; use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header, Result}; diff --git a/crates/derive/src/types/transaction/mod.rs b/crates/derive/src/types/alloy/transaction/mod.rs similarity index 100% rename from crates/derive/src/types/transaction/mod.rs rename to crates/derive/src/types/alloy/transaction/mod.rs diff --git a/crates/derive/src/types/batch.rs b/crates/derive/src/types/batch.rs deleted file mode 100644 index 5b52bd8e3..000000000 --- a/crates/derive/src/types/batch.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! This module contains the enumerable [Batch]. - -use super::batch_type::BatchType; -use super::single_batch::SingleBatch; -use crate::types::errors::DecodeError; - -use alloy_rlp::Decodable; - -// TODO: replace this with a span batch -/// Span Batch. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SpanBatch {} - -/// A Batch. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Batch { - /// A single batch - Single(SingleBatch), - /// Span Batches - Span(SpanBatch), -} - -impl Batch { - /// Attempts to decode a batch from a byte slice. - pub fn decode(r: &mut &[u8]) -> Result { - if r.is_empty() { - return Err(DecodeError::EmptyBuffer); - } - match BatchType::from(r[0]) { - BatchType::Single => { - let single_batch = SingleBatch::decode(r)?; - Ok(Batch::Single(single_batch)) - } - BatchType::Span => { - // TODO: implement span batch decoding - unimplemented!() - } - } - } -} diff --git a/crates/derive/src/types/batch_type.rs b/crates/derive/src/types/batch/batch_type.rs similarity index 91% rename from crates/derive/src/types/batch_type.rs rename to crates/derive/src/types/batch/batch_type.rs index 068452816..8fea6f5ff 100644 --- a/crates/derive/src/types/batch_type.rs +++ b/crates/derive/src/types/batch/batch_type.rs @@ -3,10 +3,10 @@ use alloy_rlp::{Decodable, Encodable}; /// The single batch type identifier. -pub(crate) const SINGLE_BATCH_TYPE: u8 = 0x01; +pub(crate) const SINGLE_BATCH_TYPE: u8 = 0x00; /// The span batch type identifier. -pub(crate) const SPAN_BATCH_TYPE: u8 = 0x02; +pub(crate) const SPAN_BATCH_TYPE: u8 = 0x01; /// The Batch Type. #[derive(Debug, Clone, PartialEq, Eq)] @@ -57,7 +57,7 @@ mod test { use alloc::vec::Vec; #[test] - fn test_batch_type() { + fn test_batch_type_rlp_roundtrip() { let batch_type = BatchType::Single; let mut buf = Vec::new(); batch_type.encode(&mut buf); diff --git a/crates/derive/src/types/batch/mod.rs b/crates/derive/src/types/batch/mod.rs new file mode 100644 index 000000000..8af807815 --- /dev/null +++ b/crates/derive/src/types/batch/mod.rs @@ -0,0 +1,64 @@ +//! This module contains the batch types for the OP Stack derivation pipeline: [SpanBatch] & [SingleBatch]. + +use super::DecodeError; +use alloc::vec::Vec; +use alloy_rlp::{Buf, Decodable, Encodable}; + +mod batch_type; +pub use batch_type::BatchType; + +mod span_batch; +pub use span_batch::{ + RawSpanBatch, SpanBatch, SpanBatchBits, SpanBatchBuilder, SpanBatchEip1559TransactionData, + SpanBatchEip2930TransactionData, SpanBatchElement, SpanBatchError, + SpanBatchLegacyTransactionData, SpanBatchPayload, SpanBatchPrefix, SpanBatchTransactionData, + SpanBatchTransactions, SpanDecodingError, MAX_SPAN_BATCH_SIZE, +}; + +mod single_batch; +pub use single_batch::SingleBatch; + +/// A Batch. +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub enum Batch { + /// A single batch + Single(SingleBatch), + /// Span Batches + Span(RawSpanBatch), +} + +impl Batch { + /// Attempts to encode a batch into a writer. + pub fn encode(&self, w: &mut Vec) -> Result<(), DecodeError> { + match self { + Self::Single(single_batch) => { + single_batch.encode(w); + Ok(()) + } + Self::Span(span_batch) => span_batch.encode(w).map_err(DecodeError::SpanBatchError), + } + } + + /// Attempts to decode a batch from a reader. + pub fn decode(r: &mut &[u8]) -> Result { + if r.is_empty() { + return Err(DecodeError::EmptyBuffer); + } + + // Read the batch type + let batch_type = BatchType::from(r[0]); + r.advance(1); + + match batch_type { + BatchType::Single => { + let single_batch = SingleBatch::decode(r)?; + Ok(Batch::Single(single_batch)) + } + BatchType::Span => { + let span_batch = RawSpanBatch::decode(r).map_err(DecodeError::SpanBatchError)?; + Ok(Batch::Span(span_batch)) + } + } + } +} diff --git a/crates/derive/src/types/single_batch.rs b/crates/derive/src/types/batch/single_batch.rs similarity index 77% rename from crates/derive/src/types/single_batch.rs rename to crates/derive/src/types/batch/single_batch.rs index 9122efa7f..1c9015b0b 100644 --- a/crates/derive/src/types/single_batch.rs +++ b/crates/derive/src/types/batch/single_batch.rs @@ -1,14 +1,14 @@ //! This module contains the [SingleBatch] type. -use super::RawTransaction; +use crate::types::RawTransaction; use alloc::vec::Vec; use alloy_primitives::BlockHash; use alloy_rlp::{Decodable, Encodable}; /// Represents a single batch: a single encoded L2 block -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct SingleBatch { - /// Block hash of the previous L2 block + /// 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, @@ -20,6 +20,15 @@ pub struct SingleBatch { 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) + } +} + impl Encodable for SingleBatch { fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { self.parent_hash.encode(out); @@ -31,14 +40,13 @@ impl Encodable for SingleBatch { } impl Decodable for SingleBatch { - fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { - let parent_hash = Decodable::decode(buf)?; - let epoch_num = Decodable::decode(buf)?; - let epoch_hash = Decodable::decode(buf)?; - let timestamp = Decodable::decode(buf)?; - let transactions = Decodable::decode(buf)?; - - Ok(SingleBatch { + fn decode(rlp: &mut &[u8]) -> alloy_rlp::Result { + let parent_hash = BlockHash::decode(rlp)?; + let epoch_num = u64::decode(rlp)?; + let epoch_hash = BlockHash::decode(rlp)?; + let timestamp = u64::decode(rlp)?; + let transactions = Vec::::decode(rlp)?; + Ok(Self { parent_hash, epoch_num, epoch_hash, @@ -48,21 +56,12 @@ impl Decodable for SingleBatch { } } -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) - } -} - #[cfg(test)] mod test { use super::SingleBatch; use crate::types::RawTransaction; use alloc::vec; - use alloy_primitives::B256; + use alloy_primitives::{hex, B256}; use alloy_rlp::{BytesMut, Decodable, Encodable}; #[test] @@ -72,7 +71,7 @@ mod test { epoch_num: 0xFF, epoch_hash: B256::ZERO, timestamp: 0xEE, - transactions: vec![RawTransaction(vec![0x00])], + transactions: vec![RawTransaction(hex!("00").into())], }; let mut out_buf = BytesMut::default(); @@ -89,7 +88,7 @@ mod test { epoch_num: 0xFF, epoch_hash: B256::ZERO, timestamp: 0xEE, - transactions: vec![RawTransaction(vec![0x7E])], + transactions: vec![RawTransaction(hex!("7E").into())], }; assert!(single_batch.has_invalid_transactions()); diff --git a/crates/derive/src/types/batch/span_batch/batch.rs b/crates/derive/src/types/batch/span_batch/batch.rs new file mode 100644 index 000000000..5bd5a312b --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/batch.rs @@ -0,0 +1,151 @@ +//! The Span Batch Type + +#![allow(unused)] + +use super::{SpanBatchError, SpanBatchTransactions}; +use crate::types::{ + block::L2BlockInfo, BlockInfo, RawSpanBatch, SingleBatch, SpanBatchBits, SpanBatchElement, + SpanBatchPayload, SpanBatchPrefix, +}; +use alloc::{vec, vec::Vec}; +use alloy_primitives::FixedBytes; + +/// 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 timestamp for the first batch in the span. + pub fn get_timestamp(&self) -> u64 { + self.batches[0].timestamp + } + + /// Converts the span batch to a raw span batch. + pub fn to_raw_span_batch( + &self, + origin_changed_bit: u8, + genesis_timestamp: u64, + chain_id: u64, + ) -> Result { + if self.batches.is_empty() { + return Err(SpanBatchError::EmptySpanBatch); + } + + let span_start = self.batches.first().ok_or(SpanBatchError::EmptySpanBatch)?; + let span_end = self.batches.last().ok_or(SpanBatchError::EmptySpanBatch)?; + + Ok(RawSpanBatch { + prefix: SpanBatchPrefix { + rel_timestamp: span_start.timestamp - genesis_timestamp, + l1_origin_num: span_end.epoch_num, + parent_check: self.parent_check, + l1_origin_check: self.l1_origin_check, + }, + payload: SpanBatchPayload { + block_count: self.batches.len() as u64, + origin_bits: self.origin_bits.clone(), + block_tx_counts: self.block_tx_counts.clone(), + txs: self.txs.clone(), + }, + }) + } + + /// 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: Vec, + 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; + } + let origin_epoch_hash = l1_origins[origin_index..l1_origins.len()] + .iter() + .enumerate() + .find(|(i, origin)| origin.timestamp == batch.timestamp) + .map(|(i, origin)| { + origin_index = i; + origin.hash + }) + .ok_or(SpanBatchError::MissingL1Origin)?; + let mut 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] + } +} diff --git a/crates/derive/src/types/batch/span_batch/bits.rs b/crates/derive/src/types/batch/span_batch/bits.rs new file mode 100644 index 000000000..c0be418a5 --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/bits.rs @@ -0,0 +1,187 @@ +//! Module for working with span batch bits. + +use crate::types::{SpanBatchError, MAX_SPAN_BATCH_SIZE}; +use alloc::{vec, vec::Vec}; +use alloy_rlp::Buf; +use anyhow::Result; + +/// Type for span batch bits. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct SpanBatchBits(pub Vec); + +impl AsRef> for SpanBatchBits { + fn as_ref(&self) -> &Vec { + &self.0 + } +} + +impl AsRef<[u8]> for SpanBatchBits { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From for Vec { + fn from(bits: SpanBatchBits) -> Vec { + bits.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 [MAX_SPAN_BATCH_SIZE]. + pub fn decode(b: &mut &[u8], bit_length: usize) -> Result { + let buffer_len = bit_length / 8 + if bit_length % 8 != 0 { 1 } else { 0 }; + if buffer_len > MAX_SPAN_BATCH_SIZE { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + + // TODO(refcell): This can definitely be optimized. + 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 = SpanBatchBits(bits.to_vec()); + + // TODO(clabby): Why doesn't this check work? + // 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 [MAX_SPAN_BATCH_SIZE]. + pub fn encode( + w: &mut Vec, + bit_length: usize, + bits: &SpanBatchBits, + ) -> Result<(), SpanBatchError> { + // TODO(clabby): Why doesn't this check work? + // 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 }; + if buf_len > MAX_SPAN_BATCH_SIZE { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + // TODO(refcell): This can definitely be optimized. + 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[byte_index]; + + // 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 << (8 - 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.0.resize(byte_index + 1, 0); + } + + // Retrieve the specific byte to modify + let byte = &mut self.0[byte_index]; + + if value { + // Set the bit to 1 + *byte |= 1 << (8 - bit_index); + } else { + // Set the bit to 0 + *byte &= !(1 << (8 - bit_index)); + } + } + + /// Calculates the bit length of the [SpanBatchBits] bitfield. + pub fn bit_len(&self) -> usize { + if let Some((top_word, rest)) = self.0.split_last() { + // Calculate bit length. Rust's leading_zeros counts zeros from the MSB, so subtract from total bits. + let significant_bits = 8 - top_word.leading_zeros() as usize; + + // Return total bits, taking into account the full words in `rest` and the significant bits in `top`. + rest.len() * 8 + significant_bits + } else { + // If the slice is empty, return 0. + 0 + } + } +} + +#[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_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/derive/src/types/batch/span_batch/builder.rs b/crates/derive/src/types/batch/span_batch/builder.rs new file mode 100644 index 000000000..5f8787a82 --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/builder.rs @@ -0,0 +1,72 @@ +//! Span Batch Builder + +#![allow(unused)] + +use crate::types::SingleBatch; +use crate::types::{RawSpanBatch, SpanBatch, SpanBatchElement}; +use alloc::vec::Vec; +use alloy_primitives::FixedBytes; + +/// The span batch builder builds a [SpanBatch] by adding +/// [SpanBatchElement] iteratively. Provides a way to stack +/// [SingleBatch]s and convert to [RawSpanBatch] for encoding. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SpanBatchBuilder { + /// The genesis timestamp of the span + genesis_timestamp: u64, + /// The chain ID of the span + chain_id: u64, + /// The span batch + span_batch: SpanBatch, + /// The origin changed bit + origin_changed_bit: u8, +} + +impl SpanBatchBuilder { + /// Create a new span batch builder + pub fn new(genesis_timestamp: u64, chain_id: u64) -> Self { + SpanBatchBuilder { + genesis_timestamp, + chain_id, + span_batch: SpanBatch::default(), + origin_changed_bit: 0, + } + } + + /// Gets the current lock count. + pub fn get_block_count(&self) -> usize { + self.span_batch.batches.len() + } + + /// Resets the span batch builder. + pub fn reset(&mut self) { + self.span_batch = SpanBatch::default(); + self.origin_changed_bit = 0; + } + + /// Returns the raw span batch ready for encoding. + pub fn get_raw_span_batch(&self) -> RawSpanBatch { + // self.span_batch.to_raw_span_batch( + // self.origin_changed_bit, + // self.genesis_timestamp, + // self.chain_id, + // ) + unimplemented!() + } + + /// Append a singular batch to the span batch and update the origin changed bit + pub fn append_singular_batch(&mut self, _singular_batch: &SingleBatch, _seq_num: u64) { + // if self.get_block_count() == 0 { + // self.origin_changed_bit = 0; + // if seq_num == 0 { + // self.origin_changed_bit = 1; + // } + // } + // self.span_batch.batches.push(SpanBatchElement { + // epoch_num: singular_batch.epoch_num, + // timestamp: singular_batch.timestamp, + // transactions: singular_batch.transactions.clone(), + // }); + unimplemented!() + } +} diff --git a/crates/derive/src/types/batch/span_batch/element.rs b/crates/derive/src/types/batch/span_batch/element.rs new file mode 100644 index 000000000..4be577660 --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/element.rs @@ -0,0 +1,27 @@ +//! Span Batch Element + +use crate::types::{RawTransaction, SingleBatch}; +use alloc::vec::Vec; + +/// 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, 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 { + SpanBatchElement { + epoch_num: batch.epoch_num, + timestamp: batch.timestamp, + transactions: batch.transactions, + } + } +} diff --git a/crates/derive/src/types/batch/span_batch/errors.rs b/crates/derive/src/types/batch/span_batch/errors.rs new file mode 100644 index 000000000..230e1149b --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/errors.rs @@ -0,0 +1,108 @@ +//! Span Batch Errors + +use core::fmt::Display; + +/// Span Batch Errors +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SpanBatchError { + /// The span batch is too big + TooBigSpanBatchSize, + /// The bit field is too long + BitfieldTooLong, + /// Failed to set [alloy_primitives::U256] from big-endian slice + InvalidBitSlice, + /// Empty Span Batch + EmptySpanBatch, + /// Missing L1 origin + MissingL1Origin, + /// Encoding errors + Encoding(EncodingError), + /// Decoding errors + Decoding(SpanDecodingError), +} + +impl Display for SpanBatchError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + SpanBatchError::TooBigSpanBatchSize => write!(f, "The span batch is too big"), + SpanBatchError::BitfieldTooLong => write!(f, "The bit field is too long"), + SpanBatchError::InvalidBitSlice => write!( + f, + "Failed to set [alloy_primitives::U256] from big-endian slice" + ), + SpanBatchError::EmptySpanBatch => write!(f, "Empty Span Batch"), + SpanBatchError::MissingL1Origin => write!(f, "Missing L1 origin"), + SpanBatchError::Encoding(e) => write!(f, "Encoding error: {:?}", e), + SpanBatchError::Decoding(e) => write!(f, "Decoding error: {:?}", e), + } + } +} + +/// Encoding Error +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EncodingError { + /// Failed to encode span batch + SpanBatch, + /// Failed to encode span batch bits + SpanBatchBits, +} + +impl Display for EncodingError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + EncodingError::SpanBatch => write!(f, "Failed to encode span batch"), + EncodingError::SpanBatchBits => write!(f, "Failed to encode span batch bits"), + } + } +} + +/// Decoding Error +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SpanDecodingError { + /// Failed to decode relative timestamp + RelativeTimestamp, + /// Failed to decode L1 origin number + L1OriginNumber, + /// Failed to decode parent check + ParentCheck, + /// Failed to decode L1 origin check + L1OriginCheck, + /// Failed to decode block count + BlockCount, + /// Failed to decode block tx counts + BlockTxCounts, + /// Failed to decode transaction nonces + TxNonces, + /// Mismatch in length between the transaction type and signature arrays in a span batch transaction payload. + TypeSignatureLenMismatch, + /// Invalid transaction type + InvalidTransactionType, + /// Invalid transaction data + InvalidTransactionData, + /// Invalid transaction signature + InvalidTransactionSignature, +} + +impl Display for SpanDecodingError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + SpanDecodingError::RelativeTimestamp => { + write!(f, "Failed to decode relative timestamp") + } + SpanDecodingError::L1OriginNumber => write!(f, "Failed to decode L1 origin number"), + SpanDecodingError::ParentCheck => write!(f, "Failed to decode parent check"), + SpanDecodingError::L1OriginCheck => write!(f, "Failed to decode L1 origin check"), + SpanDecodingError::BlockCount => write!(f, "Failed to decode block count"), + SpanDecodingError::BlockTxCounts => write!(f, "Failed to decode block tx counts"), + SpanDecodingError::TxNonces => write!(f, "Failed to decode transaction nonces"), + SpanDecodingError::TypeSignatureLenMismatch => { + write!(f, "Mismatch in length between the transaction type and signature arrays in a span batch transaction payload") + } + SpanDecodingError::InvalidTransactionType => write!(f, "Invalid transaction type"), + SpanDecodingError::InvalidTransactionData => write!(f, "Invalid transaction data"), + SpanDecodingError::InvalidTransactionSignature => { + write!(f, "Invalid transaction signature") + } + } + } +} diff --git a/crates/derive/src/types/batch/span_batch/mod.rs b/crates/derive/src/types/batch/span_batch/mod.rs new file mode 100644 index 000000000..11cf60401 --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/mod.rs @@ -0,0 +1,57 @@ +//! Contains all Span Batch types and logic. +//! +//! ## Batch format +//! +//! ```text +//! [SPAN_BATCH_TYPE] = 1 +//! span_batch = [SPAN_BATCH_TYPE] ++ prefix ++ payload +//! prefix = rel_timestamp ++ l1_origin_num ++ parent_check ++ l1_origin_check +//! payload = block_count ++ origin_bits ++ block_tx_counts ++ txs +//! txs = contract_creation_bits ++ y_parity_bits ++ tx_sigs ++ tx_tos ++ tx_datas ++ tx_nonces ++ tx_gases ++ protected_bits +//! ``` + +use crate::MAX_RLP_BYTES_PER_CHANNEL; + +/// The maximum amount of bytes that will be needed to decode every span +/// batch field. This value cannot be larger than [MAX_RLP_BYTES_PER_CHANNEL] +/// because single batch cannot be larger than channel size. +pub const MAX_SPAN_BATCH_SIZE: usize = MAX_RLP_BYTES_PER_CHANNEL as usize; + +mod batch; +pub use batch::SpanBatch; + +mod bits; +pub use bits::SpanBatchBits; + +mod payload; +pub use payload::SpanBatchPayload; + +mod prefix; +pub use prefix::SpanBatchPrefix; + +mod errors; +pub use errors::*; // Re-export all error types + +mod raw; +pub use raw::RawSpanBatch; + +mod element; +pub use element::SpanBatchElement; + +mod builder; +pub use builder::SpanBatchBuilder; + +mod signature; +pub(crate) use signature::SpanBatchSignature; + +mod tx_data; +pub use tx_data::{ + SpanBatchEip1559TransactionData, SpanBatchEip2930TransactionData, + SpanBatchLegacyTransactionData, SpanBatchTransactionData, +}; + +mod transactions; +pub use transactions::SpanBatchTransactions; + +mod utils; +pub(crate) use utils::{convert_v_to_y_parity, read_tx_data}; diff --git a/crates/derive/src/types/batch/span_batch/payload.rs b/crates/derive/src/types/batch/span_batch/payload.rs new file mode 100644 index 000000000..918c4f115 --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/payload.rs @@ -0,0 +1,136 @@ +//! Raw Span Batch Payload + +use crate::types::{ + SpanBatchBits, SpanBatchError, SpanBatchTransactions, SpanDecodingError, MAX_SPAN_BATCH_SIZE, +}; +use alloc::vec::Vec; + +/// Span Batch Payload +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SpanBatchPayload { + /// Number of L2 block in the span + pub block_count: u64, + /// Standard span-batch bitlist of blockCount bits. Each bit indicates if the L1 origin is changed at the L2 block. + pub origin_bits: SpanBatchBits, + /// List of transaction counts for each L2 block + pub block_tx_counts: Vec, + /// Transactions encoded in SpanBatch specs + pub txs: SpanBatchTransactions, +} + +impl SpanBatchPayload { + /// Decodes a [SpanBatchPayload] from a reader. + pub fn decode_payload(r: &mut &[u8]) -> Result { + let mut payload = Self::default(); + payload.decode_block_count(r)?; + payload.decode_origin_bits(r)?; + payload.decode_block_tx_counts(r)?; + payload.decode_txs(r)?; + Ok(payload) + } + + /// Encodes a [SpanBatchPayload] into a writer. + pub fn encode_payload(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + self.encode_block_count(w); + self.encode_origin_bits(w)?; + self.encode_block_tx_counts(w); + self.encode_txs(w) + } + + /// Decodes the origin bits from a reader. + pub fn decode_origin_bits(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + self.origin_bits = SpanBatchBits::decode(r, self.block_count as usize)?; + Ok(()) + } + + /// Decode a block count from a reader. + pub fn decode_block_count(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (block_count, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::BlockCount))?; + // The number of transactions in a single L2 block cannot be greater than [MAX_SPAN_BATCH_SIZE]. + if block_count as usize > MAX_SPAN_BATCH_SIZE { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + if block_count == 0 { + return Err(SpanBatchError::EmptySpanBatch); + } + self.block_count = block_count; + *r = remaining; + Ok(()) + } + + /// Decode block transaction counts from a reader. + pub fn decode_block_tx_counts(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + // Initially allocate the vec with the block count, to reduce re-allocations in the first few blocks. + let mut block_tx_counts = Vec::with_capacity(self.block_count as usize); + + for _ in 0..self.block_count { + let (block_tx_count, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::BlockTxCounts))?; + + // The number of transactions in a single L2 block cannot be greater than [MAX_SPAN_BATCH_SIZE]. + // Every transaction will take at least a single byte. + if block_tx_count as usize > MAX_SPAN_BATCH_SIZE { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + block_tx_counts.push(block_tx_count); + *r = remaining; + } + self.block_tx_counts = block_tx_counts; + Ok(()) + } + + /// Decode transactions from a reader. + pub fn decode_txs(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + if self.block_tx_counts.is_empty() { + return Err(SpanBatchError::EmptySpanBatch); + } + + let total_block_tx_count = + self.block_tx_counts + .iter() + .try_fold(0u64, |acc, block_tx_count| { + acc.checked_add(*block_tx_count) + .ok_or(SpanBatchError::TooBigSpanBatchSize) + })?; + + // The total number of transactions in a span batch cannot be greater than [MAX_SPAN_BATCH_SIZE]. + if total_block_tx_count as usize > MAX_SPAN_BATCH_SIZE { + return Err(SpanBatchError::TooBigSpanBatchSize); + } + self.txs.total_block_tx_count = total_block_tx_count; + self.txs.decode(r)?; + Ok(()) + } + + /// Encode the origin bits into a writer. + pub fn encode_origin_bits(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + SpanBatchBits::encode(w, self.block_count as usize, &self.origin_bits) + } + + /// Encode the block count into a writer. + pub fn encode_block_count(&self, w: &mut Vec) { + let mut u64_varint_buf = [0u8; 10]; + w.extend_from_slice(unsigned_varint::encode::u64( + self.block_count, + &mut u64_varint_buf, + )); + } + + /// Encode the block transaction counts into a writer. + pub fn encode_block_tx_counts(&self, w: &mut Vec) { + let mut u64_varint_buf = [0u8; 10]; + for block_tx_count in &self.block_tx_counts { + u64_varint_buf.fill(0); + w.extend_from_slice(unsigned_varint::encode::u64( + *block_tx_count, + &mut u64_varint_buf, + )); + } + } + + /// Encode the transactions into a writer. + pub fn encode_txs(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + self.txs.encode(w) + } +} diff --git a/crates/derive/src/types/batch/span_batch/prefix.rs b/crates/derive/src/types/batch/span_batch/prefix.rs new file mode 100644 index 000000000..f136482a2 --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/prefix.rs @@ -0,0 +1,106 @@ +//! Raw Span Batch Prefix + +use crate::types::{SpanBatchError, SpanDecodingError}; +use alloc::vec::Vec; +use alloy_primitives::FixedBytes; + +/// Span Batch Prefix +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SpanBatchPrefix { + /// Relative timestamp of the first block + pub rel_timestamp: u64, + /// L1 origin number + pub l1_origin_num: u64, + /// 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>, +} + +impl SpanBatchPrefix { + /// Decodes a [SpanBatchPrefix] from a reader. + pub fn decode_prefix(r: &mut &[u8]) -> Result { + let mut prefix = Self::default(); + prefix.decode_rel_timestamp(r)?; + prefix.decode_l1_origin_num(r)?; + prefix.decode_parent_check(r)?; + prefix.decode_l1_origin_check(r)?; + Ok(prefix) + } + + /// Decodes the relative timestamp from a reader. + pub fn decode_rel_timestamp(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (rel_timestamp, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::RelativeTimestamp))?; + *r = remaining; + self.rel_timestamp = rel_timestamp; + Ok(()) + } + + /// Decodes the L1 origin number from a reader. + pub fn decode_l1_origin_num(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (l1_origin_num, remaining) = unsigned_varint::decode::u64(r) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::L1OriginNumber))?; + *r = remaining; + self.l1_origin_num = l1_origin_num; + Ok(()) + } + + /// Decodes the parent check from a reader. + pub fn decode_parent_check(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (parent_check, remaining) = r.split_at(20); + let parent_check = FixedBytes::<20>::from_slice(parent_check); + *r = remaining; + self.parent_check = parent_check; + Ok(()) + } + + /// Decodes the L1 origin check from a reader. + pub fn decode_l1_origin_check(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let (l1_origin_check, remaining) = r.split_at(20); + let l1_origin_check = FixedBytes::<20>::from_slice(l1_origin_check); + *r = remaining; + self.l1_origin_check = l1_origin_check; + Ok(()) + } + + /// Encodes the [SpanBatchPrefix] into a writer. + pub fn encode_prefix(&self, w: &mut Vec) { + let mut u64_buf = [0u8; 10]; + w.extend_from_slice(unsigned_varint::encode::u64( + self.rel_timestamp, + &mut u64_buf, + )); + w.extend_from_slice(unsigned_varint::encode::u64( + self.l1_origin_num, + &mut u64_buf, + )); + w.extend_from_slice(self.parent_check.as_slice()); + w.extend_from_slice(self.l1_origin_check.as_slice()); + } +} + +#[cfg(test)] +mod test { + use super::SpanBatchPrefix; + use alloc::vec::Vec; + use alloy_primitives::address; + + #[test] + fn test_span_batch_prefix_encoding_roundtrip() { + let expected = SpanBatchPrefix { + rel_timestamp: 0xFF, + l1_origin_num: 0xEE, + parent_check: address!("beef00000000000000000000000000000000beef").into(), + l1_origin_check: address!("babe00000000000000000000000000000000babe").into(), + }; + + let mut buf = Vec::new(); + expected.encode_prefix(&mut buf); + + assert_eq!( + SpanBatchPrefix::decode_prefix(&mut buf.as_slice()).unwrap(), + expected + ); + } +} diff --git a/crates/derive/src/types/batch/span_batch/raw.rs b/crates/derive/src/types/batch/span_batch/raw.rs new file mode 100644 index 000000000..aef202957 --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/raw.rs @@ -0,0 +1,119 @@ +//! Raw Span Batch + +use alloc::vec::Vec; + +use crate::types::{ + BatchType, RawTransaction, SpanBatchElement, SpanBatchPayload, SpanBatchPrefix, + SpanDecodingError, +}; + +use super::{SpanBatch, SpanBatchError}; + +/// Raw Span Batch +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawSpanBatch { + /// The span batch prefix + pub prefix: SpanBatchPrefix, + /// The span batch payload + pub payload: SpanBatchPayload, +} + +impl RawSpanBatch { + /// Returns the batch type + pub fn get_batch_type(&self) -> BatchType { + BatchType::Span + } + + /// Encodes the [RawSpanBatch] into a writer. + pub fn encode(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + self.prefix.encode_prefix(w); + self.payload.encode_payload(w) + } + + /// Decodes the [RawSpanBatch] from a reader.] + pub fn decode(r: &mut &[u8]) -> Result { + let prefix = SpanBatchPrefix::decode_prefix(r)?; + let payload = SpanBatchPayload::decode_payload(r)?; + Ok(Self { prefix, payload }) + } + + /// Converts a [RawSpanBatch] into a [SpanBatch], which has a list of [SpanBatchElement]s. Thos function does not + /// populate the [SpanBatch] with chain configuration data, which is required for making payload attributes. + pub fn derive( + &mut self, + block_time: u64, + genesis_time: u64, + chain_id: u64, + ) -> Result { + if self.payload.block_count == 0 { + return Err(SpanBatchError::EmptySpanBatch); + } + + let mut block_origin_nums = Vec::with_capacity(self.payload.block_count as usize); + let mut l1_origin_number = self.prefix.l1_origin_num; + for i in (0..self.payload.block_count).rev() { + block_origin_nums.push(l1_origin_number); + if self + .payload + .origin_bits + .get_bit(i as usize) + .ok_or(SpanBatchError::Decoding(SpanDecodingError::L1OriginCheck))? + == 1 + && i > 0 + { + l1_origin_number -= 1; + } + } + + // Recover `v` values in transaction signatures within the batch. + self.payload.txs.recover_v(chain_id)?; + + // Get all transactions in the batch. + let enveloped_txs = self.payload.txs.full_txs(chain_id)?; + + let mut tx_idx = 0; + let batches = (0..self.payload.block_count).fold(Vec::new(), |mut acc, i| { + let transactions = + (0..self.payload.block_tx_counts[i as usize]).fold(Vec::new(), |mut acc, _| { + acc.push(enveloped_txs[tx_idx].clone()); + tx_idx += 1; + acc + }); + acc.push(SpanBatchElement { + epoch_num: block_origin_nums[i as usize], + timestamp: genesis_time + self.prefix.rel_timestamp + block_time * i, + transactions: transactions + .into_iter() + .map(|v| RawTransaction(v.into())) + .collect(), + }); + acc + }); + + Ok(SpanBatch { + parent_check: self.prefix.parent_check, + l1_origin_check: self.prefix.l1_origin_check, + batches, + ..Default::default() + }) + } +} + +#[cfg(test)] +mod test { + extern crate std; + use super::RawSpanBatch; + use alloc::vec::Vec; + + #[test] + fn test_decode_encode_raw_span_batch() { + // Load in the raw span batch from the `op-node` derivation pipeline implementation. + let raw_span_batch_hex = include_bytes!("../../../../testdata/raw_batch.hex"); + let mut raw_span_batch = RawSpanBatch::decode(&mut raw_span_batch_hex.as_slice()).unwrap(); + raw_span_batch.payload.txs.recover_v(981).unwrap(); + + let mut encoding_buf = Vec::new(); + raw_span_batch.encode(&mut encoding_buf).unwrap(); + assert_eq!(encoding_buf, raw_span_batch_hex); + } +} diff --git a/crates/derive/src/types/batch/span_batch/signature.rs b/crates/derive/src/types/batch/span_batch/signature.rs new file mode 100644 index 000000000..61a20d289 --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/signature.rs @@ -0,0 +1,37 @@ +//! This module contains the [SpanBatchSignature] type, which represents the ECDSA signature of a +//! transaction within a span batch. + +use super::{convert_v_to_y_parity, SpanBatchError, SpanDecodingError}; +use crate::types::TxType; +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, + convert_v_to_y_parity(value.v, TxType::Legacy)?, + ) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionSignature)) + } +} diff --git a/crates/derive/src/types/batch/span_batch/transactions.rs b/crates/derive/src/types/batch/span_batch/transactions.rs new file mode 100644 index 000000000..5016ab80c --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/transactions.rs @@ -0,0 +1,380 @@ +//! This module contains the [SpanBatchTransactions] type and logic for encoding and decoding transactions in a span batch. + +use alloc::vec::Vec; +use alloy_primitives::{Address, U256}; +use alloy_rlp::{Buf, Decodable, Encodable}; + +use super::{convert_v_to_y_parity, read_tx_data}; +use super::{ + SpanBatchBits, SpanBatchError, SpanBatchSignature, SpanBatchTransactionData, SpanDecodingError, +}; +use crate::types::{RawTransaction, Transaction, TxEnvelope, TxKind, TxType}; + +/// 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> { + 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> { + 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(&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) { + // TODO: Check protected signature + self.protected_bits + .set_bit(self.legacy_tx_count as usize, false); + self.legacy_tx_count += 1; + } + + // TODO: Check protected in contrast to chain ID + + let (signature, to, nonce, gas) = match tx_enveloped { + TxEnvelope::Legacy(s) => (*s.signature(), s.to(), s.nonce(), s.gas_limit()), + TxEnvelope::Eip2930(s) => (*s.signature(), s.to(), s.nonce(), s.gas_limit()), + TxEnvelope::Eip1559(s) => (*s.signature(), s.to(), s.nonce(), s.gas_limit()), + _ => { + 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 { + TxKind::Call(address) => { + self.tx_tos.push(address); + 0 + } + TxKind::Create => 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(()) + } +} diff --git a/crates/derive/src/types/batch/span_batch/tx_data/eip1559.rs b/crates/derive/src/types/batch/span_batch/tx_data/eip1559.rs new file mode 100644 index 000000000..4a0518486 --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/tx_data/eip1559.rs @@ -0,0 +1,97 @@ +//! This module contains the eip1559 transaction data type for a span batch. + +use crate::types::eip2930::AccessList; +use crate::types::{ + Signed, SpanBatchError, SpanDecodingError, Transaction, TxEip1559, TxEnvelope, TxKind, +}; +use alloy_primitives::{Address, Signature, 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 fee per gas. + pub max_fee_per_gas: U256, + /// Maximum priority fee per gas. + pub max_priority_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: if let Some(to) = to { + TxKind::Call(to) + } else { + TxKind::Create + }, + 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::types::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/derive/src/types/batch/span_batch/tx_data/eip2930.rs b/crates/derive/src/types/batch/span_batch/tx_data/eip2930.rs new file mode 100644 index 000000000..d12e9240d --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/tx_data/eip2930.rs @@ -0,0 +1,88 @@ +//! This module contains the eip2930 transaction data type for a span batch. + +use crate::types::eip2930::AccessList; +use crate::types::{ + Signed, SpanBatchError, SpanDecodingError, Transaction, TxEip2930, TxEnvelope, TxKind, +}; +use alloy_primitives::{Address, Signature, 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: if let Some(to) = to { + TxKind::Call(to) + } else { + TxKind::Create + }, + 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::types::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/derive/src/types/batch/span_batch/tx_data/legacy.rs b/crates/derive/src/types/batch/span_batch/tx_data/legacy.rs new file mode 100644 index 000000000..3e3a79be2 --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/tx_data/legacy.rs @@ -0,0 +1,113 @@ +//! This module contains the legacy transaction data type for a span batch. + +use crate::types::{ + Signed, SpanBatchError, SpanDecodingError, Transaction, TxEnvelope, TxKind, TxLegacy, +}; +use alloy_primitives::{Address, Signature, 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: if let Some(to) = to { + TxKind::Call(to) + } else { + TxKind::Create + }, + 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::types::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/derive/src/types/batch/span_batch/tx_data/mod.rs b/crates/derive/src/types/batch/span_batch/tx_data/mod.rs new file mode 100644 index 000000000..06f13ad11 --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/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/derive/src/types/batch/span_batch/tx_data/wrapper.rs b/crates/derive/src/types/batch/span_batch/tx_data/wrapper.rs new file mode 100644 index 000000000..fa7ef9228 --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/tx_data/wrapper.rs @@ -0,0 +1,134 @@ +//! This module contains the top level span batch transaction data type. + +use super::{ + SpanBatchEip1559TransactionData, SpanBatchEip2930TransactionData, + SpanBatchLegacyTransactionData, +}; +use crate::types::{SpanBatchError, SpanDecodingError, Transaction, TxEnvelope, TxType}; +use alloy_primitives::{Address, Signature, U256}; +use alloy_rlp::{Bytes, Decodable, Encodable}; + +/// 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(SpanBatchTransactionData::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) => Ok(SpanBatchTransactionData::Legacy( + SpanBatchLegacyTransactionData { + value: s.value, + gas_price: U256::from(s.gas_price), + data: Bytes::from(s.input().to_vec()), + }, + )), + TxEnvelope::Eip2930(s) => Ok(SpanBatchTransactionData::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) => Ok(SpanBatchTransactionData::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 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(SpanBatchTransactionData::Eip2930( + SpanBatchEip2930TransactionData::decode(&mut &b[1..])?, + )), + TxType::Eip1559 => Ok(SpanBatchTransactionData::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/derive/src/types/batch/span_batch/utils.rs b/crates/derive/src/types/batch/span_batch/utils.rs new file mode 100644 index 000000000..dc227d608 --- /dev/null +++ b/crates/derive/src/types/batch/span_batch/utils.rs @@ -0,0 +1,67 @@ +//! Utilities for Span Batch Encoding and Decoding. + +use super::{SpanBatchError, SpanDecodingError}; +use crate::types::TxType; +use alloc::vec::Vec; +use alloy_rlp::{Buf, Header}; + +/// Reads transaction data from a reader. +pub(crate) 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); + } + + // Copy the reader, as we need to read the header to determine if the payload is a list. + // TODO(clabby): This is horribly inefficient. It'd be nice if we could peek at this rather than forcibly having to + // advance the buffer passed, should read more into the alloy rlp docs to see if this is possible. + let r_copy = Vec::from(*r); + let rlp_header = Header::decode(&mut r_copy.as_slice()) + .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(crate) 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, + )), + } +} diff --git a/crates/derive/src/types/block.rs b/crates/derive/src/types/block.rs index 36012fc2c..cfdaaf979 100644 --- a/crates/derive/src/types/block.rs +++ b/crates/derive/src/types/block.rs @@ -31,18 +31,28 @@ impl BlockInfo { } } -// impl TryFrom for BlockInfo { -// type Error = anyhow::Error; -// -// fn try_from(block: BlockWithTransactions) -> anyhow::Result { -// Ok(BlockInfo { -// number: block.number.unwrap_or_default().to::(), -// hash: block.hash.unwrap_or_default(), -// parent_hash: block.parent_hash, -// timestamp: block.timestamp.to::(), -// }) -// } -// } +/// L2 Block Header Info +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +pub struct L2BlockInfo { + /// The base [BlockInfo] + pub block_info: BlockInfo, + /// The L1 origin [BlockId] + pub l1_origin: BlockId, + /// The sequence number of the L2 block + pub seq_num: u64, +} + +impl L2BlockInfo { + /// Instantiates a new [L2BlockInfo]. + pub fn new(block_info: BlockInfo, l1_origin: BlockId, seq_num: u64) -> Self { + Self { + block_info, + l1_origin, + seq_num, + } + } +} /// A Block Identifier #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -78,30 +88,3 @@ pub enum BlockKind { /// The latest finalized block. Finalized, } - -// /// A Block with Transactions -// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -// #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -// #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -// pub struct Block { -// /// Header of the block. -// #[serde(flatten)] -// pub header: Header, -// /// Uncles' hashes. -// pub uncles: Vec, -// /// Block Transactions. In the case of an uncle block, this field is not included in RPC -// /// responses, and when deserialized, it will be set to [BlockTransactions::Uncle]. -// #[serde( -// skip_serializing_if = "BlockTransactions::is_uncle", -// default = "BlockTransactions::uncle" -// )] -// pub transactions: BlockTransactions, -// /// Integer the size of this block in bytes. -// pub size: Option, -// /// Withdrawals in the block. -// #[serde(default, skip_serializing_if = "Option::is_none")] -// pub withdrawals: Option>, -// /// Support for arbitrary additional fields. -// #[serde(flatten)] -// pub other: OtherFields, -// } diff --git a/crates/derive/src/types/errors.rs b/crates/derive/src/types/errors.rs index b5659922c..114a94954 100644 --- a/crates/derive/src/types/errors.rs +++ b/crates/derive/src/types/errors.rs @@ -2,6 +2,8 @@ use core::fmt::Display; +use super::SpanBatchError; + /// An error that is thrown within the stages of the derivation pipeline. #[derive(Debug)] pub enum StageError { @@ -51,6 +53,8 @@ pub enum DecodeError { EmptyBuffer, /// Alloy RLP Encoding Error. AlloyRlpError(alloy_rlp::Error), + /// Span Batch Error. + SpanBatchError(SpanBatchError), } impl From for DecodeError { @@ -74,6 +78,7 @@ impl Display for DecodeError { match self { DecodeError::EmptyBuffer => write!(f, "Empty buffer"), DecodeError::AlloyRlpError(e) => write!(f, "Alloy RLP Decoding Error: {}", e), + DecodeError::SpanBatchError(e) => write!(f, "Span Batch Decoding Error: {:?}", e), } } } diff --git a/crates/derive/src/types/mod.rs b/crates/derive/src/types/mod.rs index 2025ab547..fe5cbb87e 100644 --- a/crates/derive/src/types/mod.rs +++ b/crates/derive/src/types/mod.rs @@ -1,14 +1,9 @@ //! This module contains all of the types used within the derivation pipeline. use alloc::vec::Vec; +use alloy_primitives::Bytes; use alloy_rlp::{Decodable, Encodable}; -mod batch; -pub use batch::Batch; - -mod batch_type; -pub use batch_type::BatchType; - mod system_config; pub use system_config::{ SystemAccounts, SystemConfig, SystemConfigUpdateType, CONFIG_UPDATE_EVENT_VERSION_0, @@ -18,27 +13,25 @@ pub use system_config::{ mod rollup_config; pub use rollup_config::RollupConfig; -mod transaction; -pub use transaction::{TxDeposit, TxEip1559, TxEip2930, TxEip4844, TxEnvelope, TxLegacy, TxType}; - -mod network; -pub use network::{Receipt as NetworkReceipt, Sealable, Sealed, Transaction, TxKind}; +pub mod batch; +pub use batch::{ + Batch, BatchType, RawSpanBatch, SingleBatch, SpanBatch, SpanBatchBits, SpanBatchBuilder, + SpanBatchEip1559TransactionData, SpanBatchEip2930TransactionData, SpanBatchElement, + SpanBatchError, SpanBatchLegacyTransactionData, SpanBatchPayload, SpanBatchPrefix, + SpanBatchTransactionData, SpanBatchTransactions, SpanDecodingError, MAX_SPAN_BATCH_SIZE, +}; -mod header; -pub use header::{Header, EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH}; +mod alloy; +pub use alloy::{ + calc_blob_gasprice, calc_excess_blob_gas, calc_next_block_base_fee, eip1559, eip2718, eip2930, + eip4788, eip4844, Header, NetworkReceipt, Receipt, ReceiptWithBloom, Sealable, Sealed, Signed, + Transaction, TxDeposit, TxEip1559, TxEip2930, TxEip4844, TxEnvelope, TxKind, TxLegacy, TxType, + EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH, +}; mod block; pub use block::{BlockId, BlockInfo, BlockKind}; -mod receipt; -pub use receipt::{Receipt, ReceiptWithBloom}; - -mod eips; -pub use eips::{ - calc_blob_gasprice, calc_excess_blob_gas, calc_next_block_base_fee, eip1559, eip2718, eip2930, - eip4788, eip4844, -}; - mod genesis; pub use genesis::Genesis; @@ -51,12 +44,9 @@ pub use channel::Channel; mod errors; pub use errors::{DecodeError, StageError, StageResult}; -mod single_batch; -pub use single_batch::SingleBatch; - /// A raw transaction #[derive(Debug, Clone, PartialEq, Eq)] -pub struct RawTransaction(pub Vec); +pub struct RawTransaction(pub Bytes); impl Encodable for RawTransaction { fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { @@ -67,11 +57,17 @@ impl Encodable for RawTransaction { impl Decodable for RawTransaction { /// Decodes RLP encoded bytes into [RawTransaction] bytes fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { - let tx_bytes: Vec = Decodable::decode(buf)?; + let tx_bytes = Bytes::decode(buf)?; Ok(Self(tx_bytes)) } } +impl AsRef<[u8]> for RawTransaction { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + /// A single L2 block derived from a batch. #[derive(Debug, Clone)] pub struct BlockInput { diff --git a/crates/derive/testdata/raw_batch.hex b/crates/derive/testdata/raw_batch.hex new file mode 100644 index 000000000..311a7b999 Binary files /dev/null and b/crates/derive/testdata/raw_batch.hex differ