diff --git a/Cargo.lock b/Cargo.lock index 36c1cf7c7..6e53793c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,12 +17,62 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "alloy-primitives" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef197eb250c64962003cb08b90b17f0882c192f4a6f2f544809d424fd7cb0e7d" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "hex-literal", + "itoa", + "ruint", + "serde", + "tiny-keccak", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d58d9f5da7b40e9bfff0b7e7816700be4019db97d4b6359fe7f94a9e22e42ac" +dependencies = [ + "alloy-rlp-derive", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a047897373be4bbb0224c1afdabca92648dc57a9c9ef6e7b0be3aff7a859c83" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "anyhow" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -61,6 +111,9 @@ name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +dependencies = [ + "serde", +] [[package]] name = "cc" @@ -74,6 +127,53 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "const-hex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbd12d49ab0eaf8193ba9175e45f56bbc2e4b27d57b8cfe62aa47942a46b9a9" +dependencies = [ + "cfg-if", + "cpufeatures", + "hex", + "proptest", + "serde", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + [[package]] name = "errno" version = "0.3.8" @@ -102,6 +202,27 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + [[package]] name = "kona-common" version = "0.0.1" @@ -114,6 +235,13 @@ dependencies = [ [[package]] name = "kona-derive" version = "0.0.1" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "anyhow", + "async-trait", + "serde", +] [[package]] name = "kona-preimage" @@ -132,6 +260,12 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "linked_list_allocator" version = "0.10.5" @@ -183,6 +317,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -231,6 +375,12 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.78" @@ -240,6 +390,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +dependencies = [ + "bitflags 2.4.2", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "unarray", +] + [[package]] name = "quote" version = "1.0.35" @@ -249,6 +413,40 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -258,12 +456,42 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "ruint" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608a5726529f2f0ef81b8fde9873c4bb829d6b5b5ca6be4d97345ddf0749c825" +dependencies = [ + "alloy-rlp", + "proptest", + "rand", + "ruint-macro", + "serde", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e666a5496a0b2186dbcd0ff6106e29e093c15591bde62c20d3842007c6978a09" + [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.31" @@ -283,6 +511,32 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -317,6 +571,17 @@ dependencies = [ "lock_api", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.48" @@ -340,6 +605,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tokio" version = "1.36.0" @@ -367,15 +641,27 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -513,3 +799,9 @@ name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/crates/derive/Cargo.toml b/crates/derive/Cargo.toml index 99a56a8fd..7b743b0fd 100644 --- a/crates/derive/Cargo.toml +++ b/crates/derive/Cargo.toml @@ -9,3 +9,16 @@ repository.workspace = true homepage.workspace = true [dependencies] +# Workspace +anyhow.workspace = true + +# External +alloy-primitives = { version = "0.6.3", default-features = false, features = ["rlp"] } +alloy-rlp = { version = "0.3.4", default-features = false, features = ["derive"] } +async-trait = "0.1.77" + +# Optional +serde = { version = "1.0.197", default-features = false, features = ["derive"], optional = true } + +[features] +serde = ["dep:serde", "alloy-primitives/serde"] diff --git a/crates/derive/src/lib.rs b/crates/derive/src/lib.rs index f8b585e96..79206508f 100644 --- a/crates/derive/src/lib.rs +++ b/crates/derive/src/lib.rs @@ -8,8 +8,11 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #![no_std] +// Temp +#![allow(dead_code, unused, unreachable_pub)] -// extern crate alloc; +extern crate alloc; pub mod stages; pub mod traits; +pub mod types; diff --git a/crates/derive/src/stages/l1_traversal.rs b/crates/derive/src/stages/l1_traversal.rs index 8b1378917..8a224e27a 100644 --- a/crates/derive/src/stages/l1_traversal.rs +++ b/crates/derive/src/stages/l1_traversal.rs @@ -1 +1,90 @@ +//! Contains the L1 traversal stage of the derivation pipeline. +use crate::{ + traits::{ChainProvider, ResettableStage}, + types::{BlockInfo, RollupConfig, SystemConfig}, +}; +use anyhow::{anyhow, bail, Result}; + +/// The L1 traversal stage of the derivation pipeline. +#[derive(Debug, Clone, Copy)] +pub struct L1Traversal { + /// The current block in the traversal stage. + block: Option, + /// The data source for the traversal stage. + data_source: F, + /// Signals whether or not the traversal stage has been completed. + done: bool, + /// The system config + system_config: SystemConfig, + /// The rollup config + rollup_config: RollupConfig, +} + +impl L1Traversal { + /// Creates a new [L1Traversal] instance. + pub fn new(data_source: F, cfg: RollupConfig) -> Self { + Self { + block: None, + data_source, + done: false, + system_config: SystemConfig::default(), + rollup_config: cfg, + } + } + + /// Returns the next L1 block in the traversal stage, if the stage has not been completed. This function can only + /// be called once, and will return `None` on subsequent calls unless the stage is reset. + pub fn next_l1_block(&mut self) -> Option { + if !self.done { + self.done = true; + self.block + } else { + None + } + } + + /// Advances the internal state of the [L1Traversal] stage to the next L1 block. + pub async fn advance_l1_block(&mut self) -> Result<()> { + let block = self.block.ok_or(anyhow!("No block to advance from"))?; + let next_l1_origin = self + .data_source + .block_info_by_number(block.number + 1) + .await?; + + // Check for reorgs + if block.hash != next_l1_origin.parent_hash { + bail!( + "Detected L1 reorg from {} to {} with conflicting parent", + block.hash, + next_l1_origin.hash + ); + } + + // Fetch receipts. + let receipts = self + .data_source + .receipts_by_hash(next_l1_origin.hash) + .await?; + self.system_config.update_with_receipts( + receipts.as_slice(), + &self.rollup_config, + next_l1_origin.timestamp, + )?; + + self.block = Some(next_l1_origin); + self.done = false; + Ok(()) + } +} + +impl ResettableStage for L1Traversal { + fn reset(&mut self, base: BlockInfo, cfg: SystemConfig) -> Result<()> { + self.block = Some(base); + self.done = false; + self.system_config = cfg; + + // TODO: Do we want to return an error here w/ EOF? + Ok(()) + } +} diff --git a/crates/derive/src/stages/mod.rs b/crates/derive/src/stages/mod.rs index 320331dc4..e7b700db7 100644 --- a/crates/derive/src/stages/mod.rs +++ b/crates/derive/src/stages/mod.rs @@ -12,11 +12,13 @@ //! 7. Payload Attributes Derivation //! 8. Engine Queue -pub(crate) mod batch_queue; -pub(crate) mod channel_bank; -pub(crate) mod channel_reader; -pub(crate) mod engine_queue; -pub(crate) mod frame_queue; -pub(crate) mod l1_retrieval; -pub(crate) mod l1_traversal; -pub(crate) mod payload_derivation; +mod l1_traversal; +pub use l1_traversal::L1Traversal; + +mod batch_queue; +mod channel_bank; +mod channel_reader; +mod engine_queue; +mod frame_queue; +mod l1_retrieval; +mod payload_derivation; diff --git a/crates/derive/src/traits/data_sources.rs b/crates/derive/src/traits/data_sources.rs index 4ffc6965e..e5e406fb9 100644 --- a/crates/derive/src/traits/data_sources.rs +++ b/crates/derive/src/traits/data_sources.rs @@ -1 +1,19 @@ //! Contains traits that describe the functionality of various data sources used in the derivation pipeline's stages. + +// use alloy_rpc_types::Block; +use crate::types::{BlockInfo, Receipt}; +use alloc::{boxed::Box, vec::Vec}; +use alloy_primitives::B256; +use anyhow::Result; +use async_trait::async_trait; + +/// Describes the functionality of a data source that can provide information from the blockchain. +#[async_trait] +pub trait ChainProvider { + /// Returns the block at the given number, or an error if the block does not exist in the data source. + async fn block_info_by_number(&self, number: u64) -> Result; + + /// Returns all receipts in the block with the given hash, or an error if the block does not exist in the data + /// source. + async fn receipts_by_hash(&self, hash: B256) -> Result>; +} diff --git a/crates/derive/src/traits/mod.rs b/crates/derive/src/traits/mod.rs index 90b81e81c..f4b2f960b 100644 --- a/crates/derive/src/traits/mod.rs +++ b/crates/derive/src/traits/mod.rs @@ -1,3 +1,7 @@ //! This module contains all of the traits describing functionality of portions of the derivation pipeline. -pub mod data_sources; +mod data_sources; +pub use data_sources::ChainProvider; + +mod stages; +pub use stages::ResettableStage; diff --git a/crates/derive/src/traits/stages.rs b/crates/derive/src/traits/stages.rs new file mode 100644 index 000000000..70f42deeb --- /dev/null +++ b/crates/derive/src/traits/stages.rs @@ -0,0 +1,11 @@ +//! This module contains common traits for stages within the derivation pipeline. + +use anyhow::Result; + +use crate::types::{BlockInfo, SystemConfig}; + +/// Describes the functionality fo a resettable stage within the derivation pipeline. +pub trait ResettableStage { + /// Resets the derivation stage to its initial state. + fn reset(&mut self, base: BlockInfo, cfg: SystemConfig) -> Result<()>; +} diff --git a/crates/derive/src/types/block.rs b/crates/derive/src/types/block.rs new file mode 100644 index 000000000..67543c7ab --- /dev/null +++ b/crates/derive/src/types/block.rs @@ -0,0 +1,101 @@ +//! This module contains the various Block types. + +use alloy_primitives::{BlockHash, BlockNumber, B256}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Block Header Info +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +pub struct BlockInfo { + /// The block hash + pub hash: B256, + /// The block number + pub number: u64, + /// The parent block hash + pub parent_hash: B256, + /// The block timestamp + pub timestamp: u64, +} + +impl BlockInfo { + /// Instantiates a new [BlockInfo]. + pub fn new(hash: B256, number: u64, parent_hash: B256, timestamp: u64) -> Self { + Self { + hash, + number, + parent_hash, + timestamp, + } + } +} + +// 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::(), +// }) +// } +// } + +/// A Block Identifier +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum BlockId { + /// The block hash + Hash(BlockHash), + /// The block number + Number(BlockNumber), + /// The block kind + Kind(BlockKind), +} + +/// The Block Kind +/// +/// The block kinds are: +/// - `Earliest`: The earliest known block. +/// - `Latest`: The latest pending block. +/// - `Finalized`: The latest finalized block. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum BlockKind { + /// The earliest known block. + Earliest, + /// The latest pending block. + Latest, + /// 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/eips/eip1559/basefee.rs b/crates/derive/src/types/eips/eip1559/basefee.rs new file mode 100644 index 000000000..49fa0bfe5 --- /dev/null +++ b/crates/derive/src/types/eips/eip1559/basefee.rs @@ -0,0 +1,21 @@ +use super::constants::{DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, DEFAULT_ELASTICITY_MULTIPLIER}; + +/// BaseFeeParams contains the config parameters that control block base fee computation +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BaseFeeParams { + /// The base_fee_max_change_denominator from EIP-1559 + pub max_change_denominator: u64, + /// The elasticity multiplier from EIP-1559 + pub elasticity_multiplier: u64, +} + +impl BaseFeeParams { + /// Get the base fee parameters for Ethereum mainnet + pub const fn ethereum() -> BaseFeeParams { + BaseFeeParams { + max_change_denominator: DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, + elasticity_multiplier: DEFAULT_ELASTICITY_MULTIPLIER, + } + } +} diff --git a/crates/derive/src/types/eips/eip1559/constants.rs b/crates/derive/src/types/eips/eip1559/constants.rs new file mode 100644 index 000000000..de36f1132 --- /dev/null +++ b/crates/derive/src/types/eips/eip1559/constants.rs @@ -0,0 +1,30 @@ +use alloy_primitives::U256; + +/// The default Ethereum block gas limit. +/// +/// TODO: This should be a chain spec parameter. +/// See . +pub const ETHEREUM_BLOCK_GAS_LIMIT: u64 = 30_000_000; + +/// The minimum tx fee below which the txpool will reject the transaction. +/// +/// Configured to `7` WEI which is the lowest possible value of base fee under mainnet EIP-1559 +/// parameters. `BASE_FEE_MAX_CHANGE_DENOMINATOR` +/// is `8`, or 12.5%. Once the base fee has dropped to `7` WEI it cannot decrease further because +/// 12.5% of 7 is less than 1. +/// +/// Note that min base fee under different 1559 parameterizations may differ, but there's no +/// signifant harm in leaving this setting as is. +pub const MIN_PROTOCOL_BASE_FEE: u64 = 7; + +/// Same as [MIN_PROTOCOL_BASE_FEE] but as a U256. +pub const MIN_PROTOCOL_BASE_FEE_U256: U256 = U256::from_limbs([7u64, 0, 0, 0]); + +/// Initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) +pub const INITIAL_BASE_FEE: u64 = 1_000_000_000; + +/// Base fee max change denominator as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) +pub const DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 8; + +/// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) +pub const DEFAULT_ELASTICITY_MULTIPLIER: u64 = 2; diff --git a/crates/derive/src/types/eips/eip1559/helpers.rs b/crates/derive/src/types/eips/eip1559/helpers.rs new file mode 100644 index 000000000..ff87d790c --- /dev/null +++ b/crates/derive/src/types/eips/eip1559/helpers.rs @@ -0,0 +1,175 @@ +use super::BaseFeeParams; + +/// Calculate the base fee for the next block based on the EIP-1559 specification. +/// +/// This function calculates the base fee for the next block according to the rules defined in the +/// EIP-1559. EIP-1559 introduces a new transaction pricing mechanism that includes a +/// fixed-per-block network fee that is burned and dynamically adjusts block sizes to handlez +/// transient congestion. +/// +/// For each block, the base fee per gas is determined by the gas used in the parent block and the +/// target gas (the block gas limit divided by the elasticity multiplier). The algorithm increases +/// the base fee when blocks are congested and decreases it when they are under the target gas +/// usage. The base fee per gas is always burned. +/// +/// Parameters: +/// - `gas_used`: The gas used in the current block. +/// - `gas_limit`: The gas limit of the current block. +/// - `base_fee`: The current base fee per gas. +/// - `base_fee_params`: Base fee parameters such as elasticity multiplier and max change +/// denominator. +/// +/// Returns: +/// The calculated base fee for the next block as a `u64`. +/// +/// For more information, refer to the [EIP-1559 spec](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md). +pub fn calc_next_block_base_fee( + gas_used: u64, + gas_limit: u64, + base_fee: u64, + base_fee_params: BaseFeeParams, +) -> u64 { + // Calculate the target gas by dividing the gas limit by the elasticity multiplier. + let gas_target = gas_limit / base_fee_params.elasticity_multiplier; + + match gas_used.cmp(&gas_target) { + // If the gas used in the current block is equal to the gas target, the base fee remains the + // same (no increase). + core::cmp::Ordering::Equal => base_fee, + // If the gas used in the current block is greater than the gas target, calculate a new + // increased base fee. + core::cmp::Ordering::Greater => { + // Calculate the increase in base fee based on the formula defined by EIP-1559. + base_fee + + (core::cmp::max( + // Ensure a minimum increase of 1. + 1, + base_fee as u128 * (gas_used - gas_target) as u128 + / (gas_target as u128 * base_fee_params.max_change_denominator as u128), + ) as u64) + } + // If the gas used in the current block is less than the gas target, calculate a new + // decreased base fee. + core::cmp::Ordering::Less => { + // Calculate the decrease in base fee based on the formula defined by EIP-1559. + base_fee.saturating_sub( + (base_fee as u128 * (gas_target - gas_used) as u128 + / (gas_target as u128 * base_fee_params.max_change_denominator as u128)) + as u64, + ) + } + } +} + +#[cfg(test)] +mod tests { + use crate::types::eips::eip1559::{MIN_PROTOCOL_BASE_FEE, MIN_PROTOCOL_BASE_FEE_U256}; + + use super::*; + + #[test] + fn min_protocol_sanity() { + assert_eq!( + MIN_PROTOCOL_BASE_FEE_U256.to::(), + MIN_PROTOCOL_BASE_FEE + ); + } + + #[test] + fn calculate_base_fee_success() { + let base_fee = [ + 1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0, + 1, 2, + ]; + let gas_used = [ + 10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000, + 10000000, + ]; + let gas_limit = [ + 10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000, + 18000000, 18000000, + ]; + let next_base_fee = [ + 1125000000, 1083333333, 1053571428, 1179939062, 1116028649, 918084097, 1063811730, 1, + 2, 3, + ]; + + for i in 0..base_fee.len() { + assert_eq!( + next_base_fee[i], + calc_next_block_base_fee( + gas_used[i], + gas_limit[i], + base_fee[i], + BaseFeeParams::ethereum(), + ) + ); + } + } + + #[cfg(feature = "optimism")] + #[test] + fn calculate_optimism_base_fee_success() { + let base_fee = [ + 1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0, + 1, 2, + ]; + let gas_used = [ + 10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000, + 10000000, + ]; + let gas_limit = [ + 10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000, + 18000000, 18000000, + ]; + let next_base_fee = [ + 1100000048, 1080000000, 1065714297, 1167067046, 1128881311, 1028254188, 1098203452, 1, + 2, 3, + ]; + + for i in 0..base_fee.len() { + assert_eq!( + next_base_fee[i], + calc_next_block_base_fee( + gas_used[i], + gas_limit[i], + base_fee[i], + crate::BaseFeeParams::optimism(), + ) + ); + } + } + + #[cfg(feature = "optimism")] + #[test] + fn calculate_optimism_goerli_base_fee_success() { + let base_fee = [ + 1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0, + 1, 2, + ]; + let gas_used = [ + 10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000, + 10000000, + ]; + let gas_limit = [ + 10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000, + 18000000, 18000000, + ]; + let next_base_fee = [ + 1180000000, 1146666666, 1122857142, 1244299375, 1189416692, 1028254188, 1144836295, 1, + 2, 3, + ]; + + for i in 0..base_fee.len() { + assert_eq!( + next_base_fee[i], + calc_next_block_base_fee( + gas_used[i], + gas_limit[i], + base_fee[i], + crate::BaseFeeParams::optimism_goerli(), + ) + ); + } + } +} diff --git a/crates/derive/src/types/eips/eip1559/mod.rs b/crates/derive/src/types/eips/eip1559/mod.rs new file mode 100644 index 000000000..793d2d070 --- /dev/null +++ b/crates/derive/src/types/eips/eip1559/mod.rs @@ -0,0 +1,15 @@ +//! [EIP-1559] constants, helpers, and types. +//! +//! [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + +mod basefee; +pub use basefee::BaseFeeParams; + +mod constants; +pub use constants::{ + DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, DEFAULT_ELASTICITY_MULTIPLIER, + ETHEREUM_BLOCK_GAS_LIMIT, INITIAL_BASE_FEE, MIN_PROTOCOL_BASE_FEE, MIN_PROTOCOL_BASE_FEE_U256, +}; + +mod helpers; +pub use helpers::calc_next_block_base_fee; diff --git a/crates/derive/src/types/eips/eip2718.rs b/crates/derive/src/types/eips/eip2718.rs new file mode 100644 index 000000000..1988e17eb --- /dev/null +++ b/crates/derive/src/types/eips/eip2718.rs @@ -0,0 +1,171 @@ +//! [EIP-2718] traits. +//! +//! [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + +use alloc::{string::String, vec, vec::Vec}; +use alloy_primitives::{keccak256, Sealed, B256}; +use alloy_rlp::{BufMut, Header, EMPTY_STRING_CODE}; + +// https://eips.ethereum.org/EIPS/eip-2718#transactiontype-only-goes-up-to-0x7f +const TX_TYPE_BYTE_MAX: u8 = 0x7f; + +/// [EIP-2718] decoding errors. +/// +/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +pub enum Eip2718Error { + /// Rlp error from [`alloy_rlp`]. + RlpError(alloy_rlp::Error), + /// Got an unexpected type flag while decoding. + UnexpectedType(u8), + /// Some other error occurred. + Custom(String), +} + +/// Decoding trait for [EIP-2718] envelopes. These envelopes wrap a transaction +/// or a receipt with a type flag. +/// +/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +pub trait Decodable2718: Sized { + /// Extract the type byte from the buffer, if any. The type byte is the + /// first byte, provided that that first byte is 0x7f or lower. + fn extract_type_byte(buf: &mut &[u8]) -> Option { + buf.first().copied().filter(|b| *b <= TX_TYPE_BYTE_MAX) + } + + /// Decode the appropriate variant, based on the type flag. + /// + /// This function is invoked by [`Self::decode_2718`] with the type byte, and the tail of the + /// buffer. + /// + /// ## Note + /// + /// This should be a simple match block that invokes an inner type's RLP decoder. + fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result; + + /// Decode the default variant. + /// + /// This function is invoked by [`Self::decode_2718`] when no type byte can be extracted. + fn fallback_decode(buf: &mut &[u8]) -> Result; + + /// Decode an EIP-2718 transaction into a concrete instance + fn decode_2718(buf: &mut &[u8]) -> Result { + Self::extract_type_byte(buf) + .map(|ty| Self::typed_decode(ty, &mut &buf[1..])) + .unwrap_or_else(|| Self::fallback_decode(buf)) + } + + /// Decode an EIP-2718 transaction in the network format. + /// + /// The network format is the RLP encoded string consisting of the + /// type-flag prepended to an opaque inner encoding. The inner encoding is + /// RLP for all current Ethereum transaction types, but may not be in future + /// versions of the protocol. + fn network_decode(buf: &mut &[u8]) -> Result { + // Keep the original buffer around by copying it. + let mut h_decode = *buf; + let h = Header::decode(&mut h_decode).map_err(Eip2718Error::RlpError)?; + + // If it's a list, we need to fallback to the legacy decoding. + if h.list { + return Self::fallback_decode(buf); + } else { + *buf = h_decode; + } + + let remaining_len = buf.len(); + + if remaining_len == 0 || remaining_len < h.payload_length { + return Err(Eip2718Error::RlpError(alloy_rlp::Error::InputTooShort)); + } + + let ty = buf[0]; + let buf = &mut &buf[1..]; + let tx = Self::typed_decode(ty, buf)?; + + let bytes_consumed = remaining_len - buf.len(); + // because Header::decode works for single bytes (including the tx type), returning a + // string Header with payload_length of 1, we need to make sure this check is only + // performed for transactions with a string header + if bytes_consumed != h.payload_length && h_decode[0] > EMPTY_STRING_CODE { + return Err(Eip2718Error::RlpError(alloy_rlp::Error::UnexpectedLength)); + } + + Ok(tx) + } +} + +/// Encoding trait for [EIP-2718] envelopes. These envelopes wrap a transaction +/// or a receipt with a type flag. +/// +/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +pub trait Encodable2718: Sized + Send + Sync + 'static { + /// Return the type flag (if any). + /// + /// This should return `None` for the default (legacy) variant of the + /// envelope. + fn type_flag(&self) -> Option; + + /// True if the envelope is the legacy variant. + fn is_legacy(&self) -> bool { + matches!(self.type_flag(), None | Some(0)) + } + + /// The length of the 2718 encoded envelope. This is the length of the type + /// flag + the length of the inner transaction RLP. + fn encode_2718_len(&self) -> usize; + + /// Encode the transaction according to [EIP-2718] rules. First a 1-byte + /// type flag in the range 0x0-0x7f, then the body of the transaction. + /// + /// This implementation uses RLP for the transaction body. Non-standard + /// users can override this to use some other serialization scheme. + /// + /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + fn encode_2718(&self, out: &mut dyn BufMut); + + /// Encode the transaction according to [EIP-2718] rules. First a 1-byte + /// type flag in the range 0x0-0x7f, then the body of the transaction. + /// + /// This is a convenience method for encoding into a vec, and returning the + /// vec. + fn encoded_2718(&self) -> Vec { + let mut out = vec![]; + self.encode_2718(&mut out); + out + } + + /// Compute the hash as committed to in the MPT trie. + fn trie_hash(&self) -> B256 { + keccak256(self.encoded_2718()) + } + + /// Seal the encodable, by encoding and hashing it. + fn seal(self) -> Sealed { + let hash = self.trie_hash(); + Sealed::new_unchecked(self, hash) + } + + /// Return the network encoding. For non-legacy items, this is the RLP + /// encoding of the bytestring of the 2718 encoding. For legacy items it is + /// simply the legacy encoding. + fn network_encode(&self, out: &mut dyn BufMut) { + if !self.is_legacy() { + Header { + list: false, + payload_length: self.encode_2718_len(), + } + .encode(out); + } + + self.encode_2718(out); + } +} + +/// An [EIP-2718] envelope, blanket implemented for types that impl +/// [`Encodable2718`] and [`Decodable2718`]. This envelope is a wrapper around +/// a transaction, or a receipt, or any other type that is differentiated by an +/// EIP-2718 transaction type. +/// +/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +pub trait Eip2718Envelope: Decodable2718 + Encodable2718 {} +impl Eip2718Envelope for T where T: Decodable2718 + Encodable2718 {} diff --git a/crates/derive/src/types/eips/eip2930.rs b/crates/derive/src/types/eips/eip2930.rs new file mode 100644 index 000000000..8e0dc1608 --- /dev/null +++ b/crates/derive/src/types/eips/eip2930.rs @@ -0,0 +1,78 @@ +//! [EIP-2930] types. +//! +//! [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + +use alloc::vec::Vec; +use alloy_primitives::{Address, B256, U256}; +use alloy_rlp::{RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper}; +use core::mem; + +/// A list of addresses and storage keys that the transaction plans to access. +/// Accesses outside the list are possible, but become more expensive. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, RlpDecodable, RlpEncodable)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct AccessListItem { + /// Account addresses that would be loaded at the start of execution + pub address: Address, + /// Keys of storage that would be loaded at the start of execution + pub storage_keys: Vec, +} + +impl AccessListItem { + /// Calculates a heuristic for the in-memory size of the [AccessListItem]. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::
() + self.storage_keys.capacity() * mem::size_of::() + } +} + +/// AccessList as defined in EIP-2930 +#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, RlpDecodableWrapper, RlpEncodableWrapper)] +pub struct AccessList(pub Vec); + +impl AccessList { + /// Converts the list into a vec, expected by revm + pub fn flattened(&self) -> Vec<(Address, Vec)> { + self.flatten().collect() + } + + /// Consumes the type and converts the list into a vec, expected by revm + pub fn into_flattened(self) -> Vec<(Address, Vec)> { + self.into_flatten().collect() + } + + /// Consumes the type and returns an iterator over the list's addresses and storage keys. + pub fn into_flatten(self) -> impl Iterator)> { + self.0.into_iter().map(|item| { + ( + item.address, + item.storage_keys + .into_iter() + .map(|slot| U256::from_be_bytes(slot.0)) + .collect(), + ) + }) + } + + /// Returns an iterator over the list's addresses and storage keys. + pub fn flatten(&self) -> impl Iterator)> + '_ { + self.0.iter().map(|item| { + ( + item.address, + item.storage_keys + .iter() + .map(|slot| U256::from_be_bytes(slot.0)) + .collect(), + ) + }) + } + + /// Calculates a heuristic for the in-memory size of the [AccessList]. + #[inline] + pub fn size(&self) -> usize { + // take into account capacity + self.0.iter().map(AccessListItem::size).sum::() + + self.0.capacity() * mem::size_of::() + } +} diff --git a/crates/derive/src/types/eips/eip4788.rs b/crates/derive/src/types/eips/eip4788.rs new file mode 100644 index 000000000..42aa14b39 --- /dev/null +++ b/crates/derive/src/types/eips/eip4788.rs @@ -0,0 +1,9 @@ +//! [EIP-4788] constants. +//! +//! [EIP-4788]: https://eips.ethereum.org/EIPS/eip-4788 + +use alloy_primitives::{address, Address}; + +/// The caller to be used when calling the EIP-4788 beacon roots contract at the beginning of the +/// block. +pub const SYSTEM_ADDRESS: Address = address!("fffffffffffffffffffffffffffffffffffffffe"); diff --git a/crates/derive/src/types/eips/eip4844.rs b/crates/derive/src/types/eips/eip4844.rs new file mode 100644 index 000000000..e9694038b --- /dev/null +++ b/crates/derive/src/types/eips/eip4844.rs @@ -0,0 +1,195 @@ +//! [EIP-4844] constants and helpers. +//! +//! [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + +/// Size a single field element in bytes. +pub const FIELD_ELEMENT_BYTES: u64 = 32; + +/// How many field elements are stored in a single data blob. +pub const FIELD_ELEMENTS_PER_BLOB: u64 = 4096; + +/// Gas consumption of a single data blob. +pub const DATA_GAS_PER_BLOB: u64 = 131_072u64; // 32*4096 = 131072 == 2^17 == 0x20000 + +/// Maximum data gas for data blobs in a single block. +pub const MAX_DATA_GAS_PER_BLOCK: u64 = 786_432u64; // 0xC0000 = 6 * 0x20000 + +/// Target data gas for data blobs in a single block. +pub const TARGET_DATA_GAS_PER_BLOCK: u64 = 393_216u64; // 0x60000 = 3 * 0x20000 + +/// Maximum number of data blobs in a single block. +pub const MAX_BLOBS_PER_BLOCK: usize = (MAX_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) as usize; // 786432 / 131072 = 6 + +/// Target number of data blobs in a single block. +pub const TARGET_BLOBS_PER_BLOCK: u64 = TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB; // 393216 / 131072 = 3 + +/// Determines the maximum rate of change for blob fee +pub const BLOB_GASPRICE_UPDATE_FRACTION: u64 = 3_338_477u64; // 3338477 + +/// Minimum gas price for a data blob +pub const BLOB_TX_MIN_BLOB_GASPRICE: u128 = 1u128; + +/// Commitment version of a KZG commitment +pub const VERSIONED_HASH_VERSION_KZG: u8 = 0x01; + +/// Calculates the `excess_blob_gas` from the parent header's `blob_gas_used` and `excess_blob_gas`. +/// +/// See also [the EIP-4844 helpers](https://eips.ethereum.org/EIPS/eip-4844#helpers) +/// (`calc_excess_blob_gas`). +#[inline] +pub const fn calc_excess_blob_gas(parent_excess_blob_gas: u64, parent_blob_gas_used: u64) -> u64 { + (parent_excess_blob_gas + parent_blob_gas_used).saturating_sub(TARGET_DATA_GAS_PER_BLOCK) +} + +/// Calculates the blob gas price from the header's excess blob gas field. +/// +/// See also [the EIP-4844 helpers](https://eips.ethereum.org/EIPS/eip-4844#helpers) +/// (`get_blob_gasprice`). +#[inline] +pub fn calc_blob_gasprice(excess_blob_gas: u64) -> u128 { + fake_exponential( + BLOB_TX_MIN_BLOB_GASPRICE as u64, + excess_blob_gas, + BLOB_GASPRICE_UPDATE_FRACTION, + ) +} + +/// Approximates `factor * e ** (numerator / denominator)` using Taylor expansion. +/// +/// This is used to calculate the blob price. +/// +/// See also [the EIP-4844 helpers](https://eips.ethereum.org/EIPS/eip-4844#helpers) +/// (`fake_exponential`). +/// +/// # Panics +/// +/// This function panics if `denominator` is zero. +#[inline] +fn fake_exponential(factor: u64, numerator: u64, denominator: u64) -> u128 { + assert_ne!(denominator, 0, "attempt to divide by zero"); + let factor = factor as u128; + let numerator = numerator as u128; + let denominator = denominator as u128; + + let mut i = 1; + let mut output = 0; + let mut numerator_accum = factor * denominator; + while numerator_accum > 0 { + output += numerator_accum; + + // Denominator is asserted as not zero at the start of the function. + numerator_accum = (numerator_accum * numerator) / (denominator * i); + i += 1; + } + output / denominator +} + +#[cfg(test)] +mod tests { + use super::*; + + // https://github.com/ethereum/go-ethereum/blob/28857080d732857030eda80c69b9ba2c8926f221/consensus/misc/eip4844/eip4844_test.go#L27 + #[test] + fn test_calc_excess_blob_gas() { + for t @ &(excess, blobs, expected) in &[ + // The excess blob gas should not increase from zero if the used blob + // slots are below - or equal - to the target. + (0, 0, 0), + (0, 1, 0), + (0, TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB, 0), + // If the target blob gas is exceeded, the excessBlobGas should increase + // by however much it was overshot + ( + 0, + (TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) + 1, + DATA_GAS_PER_BLOB, + ), + ( + 1, + (TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) + 1, + DATA_GAS_PER_BLOB + 1, + ), + ( + 1, + (TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) + 2, + 2 * DATA_GAS_PER_BLOB + 1, + ), + // The excess blob gas should decrease by however much the target was + // under-shot, capped at zero. + ( + TARGET_DATA_GAS_PER_BLOCK, + TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB, + TARGET_DATA_GAS_PER_BLOCK, + ), + ( + TARGET_DATA_GAS_PER_BLOCK, + (TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) - 1, + TARGET_DATA_GAS_PER_BLOCK - DATA_GAS_PER_BLOB, + ), + ( + TARGET_DATA_GAS_PER_BLOCK, + (TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) - 2, + TARGET_DATA_GAS_PER_BLOCK - (2 * DATA_GAS_PER_BLOB), + ), + ( + DATA_GAS_PER_BLOB - 1, + (TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) - 1, + 0, + ), + ] { + let actual = calc_excess_blob_gas(excess, blobs * DATA_GAS_PER_BLOB); + assert_eq!(actual, expected, "test: {t:?}"); + } + } + + // https://github.com/ethereum/go-ethereum/blob/28857080d732857030eda80c69b9ba2c8926f221/consensus/misc/eip4844/eip4844_test.go#L60 + #[test] + fn test_calc_blob_fee() { + let blob_fee_vectors = &[ + (0, 1), + (2314057, 1), + (2314058, 2), + (10 * 1024 * 1024, 23), + // calc_blob_gasprice approximates `e ** (excess_blob_gas / + // BLOB_GASPRICE_UPDATE_FRACTION)` using Taylor expansion + // + // to roughly find where boundaries will be hit: + // 2 ** bits = e ** (excess_blob_gas / BLOB_GASPRICE_UPDATE_FRACTION) + // excess_blob_gas = ln(2 ** bits) * BLOB_GASPRICE_UPDATE_FRACTION + (148099578, 18446739238971471609), // output is just below the overflow + (148099579, 18446744762204311910), // output is just after the overflow + (161087488, 902580055246494526580), + ]; + + for &(excess, expected) in blob_fee_vectors { + let actual = calc_blob_gasprice(excess); + assert_eq!(actual, expected, "test: {excess}"); + } + } + + // https://github.com/ethereum/go-ethereum/blob/28857080d732857030eda80c69b9ba2c8926f221/consensus/misc/eip4844/eip4844_test.go#L78 + #[test] + fn fake_exp() { + for t @ &(factor, numerator, denominator, expected) in &[ + (1u64, 0u64, 1u64, 1u128), + (38493, 0, 1000, 38493), + (0, 1234, 2345, 0), + (1, 2, 1, 6), // approximate 7.389 + (1, 4, 2, 6), + (1, 3, 1, 16), // approximate 20.09 + (1, 6, 2, 18), + (1, 4, 1, 49), // approximate 54.60 + (1, 8, 2, 50), + (10, 8, 2, 542), // approximate 540.598 + (11, 8, 2, 596), // approximate 600.58 + (1, 5, 1, 136), // approximate 148.4 + (1, 5, 2, 11), // approximate 12.18 + (2, 5, 2, 23), // approximate 24.36 + (1, 50000000, 2225652, 5709098764), + (1, 380928, BLOB_GASPRICE_UPDATE_FRACTION, 1), + ] { + let actual = fake_exponential(factor, numerator, denominator); + assert_eq!(actual, expected, "test: {t:?}"); + } + } +} diff --git a/crates/derive/src/types/eips/merge.rs b/crates/derive/src/types/eips/merge.rs new file mode 100644 index 000000000..dac534b62 --- /dev/null +++ b/crates/derive/src/types/eips/merge.rs @@ -0,0 +1,35 @@ +//! Constants related to the beacon chain consensus. + +/// An EPOCH is a series of 32 slots. +pub const EPOCH_SLOTS: u64 = 32; + +/// The duration of a slot in seconds. +/// +/// This is the time period of 12 seconds in which a randomly chosen validator has time to propose a +/// block. +pub const SLOT_DURATION_SECS: u64 = 12; + +/// An EPOCH is a series of 32 slots (~6.4min). +pub const EPOCH_DURATION_SECS: u64 = EPOCH_SLOTS * SLOT_DURATION_SECS; + +/// The default block nonce in the beacon consensus +pub const BEACON_NONCE: u64 = 0u64; + +/// The number of blocks to unwind during a reorg that already became a part of canonical chain. +/// +/// In reality, the node can end up in this particular situation very rarely. It would happen only +/// if the node process is abruptly terminated during ongoing reorg and doesn't boot back up for +/// long period of time. +/// +/// Unwind depth of `3` blocks significantly reduces the chance that the reorged block is kept in +/// the database. +pub const BEACON_CONSENSUS_REORG_UNWIND_DEPTH: u64 = 3; + +/// Max seconds from current time allowed for blocks, before they're considered future blocks. +/// +/// This is only used when checking whether or not the timestamp for pre-merge blocks is in the +/// future. +/// +/// See: +/// +pub const ALLOWED_FUTURE_BLOCK_TIME_SECONDS: u64 = 15; diff --git a/crates/derive/src/types/eips/mod.rs b/crates/derive/src/types/eips/mod.rs new file mode 100644 index 000000000..8513380a4 --- /dev/null +++ b/crates/derive/src/types/eips/mod.rs @@ -0,0 +1,15 @@ +//! `alloy-eips` crate ported to `no_std`. + +pub mod eip1559; +pub use eip1559::calc_next_block_base_fee; + +pub mod eip2718; + +pub mod eip2930; + +pub mod eip4788; + +pub mod eip4844; +pub use eip4844::{calc_blob_gasprice, calc_excess_blob_gas}; + +pub mod merge; diff --git a/crates/derive/src/types/header.rs b/crates/derive/src/types/header.rs new file mode 100644 index 000000000..42a844377 --- /dev/null +++ b/crates/derive/src/types/header.rs @@ -0,0 +1,488 @@ +use crate::types::{ + eips::{ + eip1559::{calc_next_block_base_fee, BaseFeeParams}, + eip4844::{calc_blob_gasprice, calc_excess_blob_gas}, + }, + network::Sealable, +}; +use alloc::vec::Vec; +use alloy_primitives::{b256, keccak256, Address, BlockNumber, Bloom, Bytes, B256, B64, U256}; +use alloy_rlp::{ + length_of_length, Buf, BufMut, Decodable, Encodable, EMPTY_LIST_CODE, EMPTY_STRING_CODE, +}; +use core::mem; + +/// Ommer root of empty list. +pub const EMPTY_OMMER_ROOT_HASH: B256 = + b256!("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + +/// Root hash of an empty trie. +pub const EMPTY_ROOT_HASH: B256 = + b256!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + +/// Ethereum Block header +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Header { + /// The Keccak 256-bit hash of the parent + /// block’s header, in its entirety; formally Hp. + pub parent_hash: B256, + /// The Keccak 256-bit hash of the ommers list portion of this block; formally Ho. + pub ommers_hash: B256, + /// The 160-bit address to which all fees collected from the successful mining of this block + /// be transferred; formally Hc. + pub beneficiary: Address, + /// The Keccak 256-bit hash of the root node of the state trie, after all transactions are + /// executed and finalisations applied; formally Hr. + pub state_root: B256, + /// The Keccak 256-bit hash of the root node of the trie structure populated with each + /// transaction in the transactions list portion of the block; formally Ht. + pub transactions_root: B256, + /// The Keccak 256-bit hash of the root node of the trie structure populated with the receipts + /// of each transaction in the transactions list portion of the block; formally He. + pub receipts_root: B256, + /// The Keccak 256-bit hash of the withdrawals list portion of this block. + /// + pub withdrawals_root: Option, + /// The Bloom filter composed from indexable information (logger address and log topics) + /// contained in each log entry from the receipt of each transaction in the transactions list; + /// formally Hb. + pub logs_bloom: Bloom, + /// A scalar value corresponding to the difficulty level of this block. This can be calculated + /// from the previous block’s difficulty level and the timestamp; formally Hd. + pub difficulty: U256, + /// A scalar value equal to the number of ancestor blocks. The genesis block has a number of + /// zero; formally Hi. + pub number: BlockNumber, + /// A scalar value equal to the current limit of gas expenditure per block; formally Hl. + pub gas_limit: u64, + /// A scalar value equal to the total gas used in transactions in this block; formally Hg. + pub gas_used: u64, + /// A scalar value equal to the reasonable output of Unix’s time() at this block’s inception; + /// formally Hs. + pub timestamp: u64, + /// A 256-bit hash which, combined with the + /// nonce, proves that a sufficient amount of computation has been carried out on this block; + /// formally Hm. + pub mix_hash: B256, + /// A 64-bit value which, combined with the mixhash, proves that a sufficient amount of + /// computation has been carried out on this block; formally Hn. + pub nonce: u64, + /// A scalar representing EIP1559 base fee which can move up or down each block according + /// to a formula which is a function of gas used in parent block and gas target + /// (block gas limit divided by elasticity multiplier) of parent block. + /// The algorithm results in the base fee per gas increasing when blocks are + /// above the gas target, and decreasing when blocks are below the gas target. The base fee per + /// gas is burned. + pub base_fee_per_gas: Option, + /// The total amount of blob gas consumed by the transactions within the block, added in + /// EIP-4844. + pub blob_gas_used: Option, + /// A running total of blob gas consumed in excess of the target, prior to the block. Blocks + /// with above-target blob gas consumption increase this value, blocks with below-target blob + /// gas consumption decrease it (bounded at 0). This was added in EIP-4844. + pub excess_blob_gas: Option, + /// The hash of the parent beacon block's root is included in execution blocks, as proposed by + /// EIP-4788. + /// + /// This enables trust-minimized access to consensus state, supporting staking pools, bridges, + /// and more. + /// + /// The beacon roots contract handles root storage, enhancing Ethereum's functionalities. + pub parent_beacon_block_root: Option, + /// An arbitrary byte array containing data relevant to this block. This must be 32 bytes or + /// fewer; formally Hx. + pub extra_data: Bytes, +} + +impl Default for Header { + fn default() -> Self { + Header { + parent_hash: Default::default(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: Default::default(), + state_root: EMPTY_ROOT_HASH, + transactions_root: EMPTY_ROOT_HASH, + receipts_root: EMPTY_ROOT_HASH, + logs_bloom: Default::default(), + difficulty: Default::default(), + number: 0, + gas_limit: 0, + gas_used: 0, + timestamp: 0, + extra_data: Default::default(), + mix_hash: Default::default(), + nonce: 0, + base_fee_per_gas: None, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + } + } +} + +impl Sealable for Header { + fn hash(&self) -> B256 { + self.hash_slow() + } +} + +impl Header { + // TODO: re-enable + + // /// Returns the parent block's number and hash + // pub fn parent_num_hash(&self) -> BlockNumHash { + // BlockNumHash { number: self.number.saturating_sub(1), hash: self.parent_hash } + // } + + /// Heavy function that will calculate hash of data and will *not* save the change to metadata. + /// + /// Use `Header::seal_slow` and unlock if you need the hash to be persistent. + pub fn hash_slow(&self) -> B256 { + let mut out = Vec::::new(); + self.encode(&mut out); + keccak256(&out) + } + + /// Checks if the header is empty - has no transactions and no ommers + pub fn is_empty(&self) -> bool { + let txs_and_ommers_empty = self.transaction_root_is_empty() && self.ommers_hash_is_empty(); + if let Some(withdrawals_root) = self.withdrawals_root { + txs_and_ommers_empty && withdrawals_root == EMPTY_ROOT_HASH + } else { + txs_and_ommers_empty + } + } + + /// Check if the ommers hash equals to empty hash list. + pub fn ommers_hash_is_empty(&self) -> bool { + self.ommers_hash == EMPTY_OMMER_ROOT_HASH + } + + /// Check if the transaction root equals to empty root. + pub fn transaction_root_is_empty(&self) -> bool { + self.transactions_root == EMPTY_ROOT_HASH + } + + // TODO: re-enable + + // /// Converts all roots in the header to a [BlockBodyRoots] struct. + // pub fn body_roots(&self) -> BlockBodyRoots { + // BlockBodyRoots { + // tx_root: self.transactions_root, + // ommers_hash: self.ommers_hash, + // withdrawals_root: self.withdrawals_root, + // } + // } + + /// Returns the blob fee for _this_ block according to the EIP-4844 spec. + /// + /// Returns `None` if `excess_blob_gas` is None + pub fn blob_fee(&self) -> Option { + self.excess_blob_gas.map(calc_blob_gasprice) + } + + /// Returns the blob fee for the next block according to the EIP-4844 spec. + /// + /// Returns `None` if `excess_blob_gas` is None. + /// + /// See also [Self::next_block_excess_blob_gas] + pub fn next_block_blob_fee(&self) -> Option { + self.next_block_excess_blob_gas().map(calc_blob_gasprice) + } + + /// Calculate base fee for next block according to the EIP-1559 spec. + /// + /// Returns a `None` if no base fee is set, no EIP-1559 support + pub fn next_block_base_fee(&self, base_fee_params: BaseFeeParams) -> Option { + Some(calc_next_block_base_fee( + self.gas_used, + self.gas_limit, + self.base_fee_per_gas?, + base_fee_params, + )) + } + + /// Calculate excess blob gas for the next block according to the EIP-4844 + /// spec. + /// + /// Returns a `None` if no excess blob gas is set, no EIP-4844 support + pub fn next_block_excess_blob_gas(&self) -> Option { + Some(calc_excess_blob_gas( + self.excess_blob_gas?, + self.blob_gas_used?, + )) + } + + /// Calculate a heuristic for the in-memory size of the [Header]. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // parent hash + mem::size_of::() + // ommers hash + mem::size_of::
() + // beneficiary + mem::size_of::() + // state root + mem::size_of::() + // transactions root + mem::size_of::() + // receipts root + mem::size_of::>() + // withdrawals root + mem::size_of::() + // logs bloom + mem::size_of::() + // difficulty + mem::size_of::() + // number + mem::size_of::() + // gas limit + mem::size_of::() + // gas used + mem::size_of::() + // timestamp + mem::size_of::() + // mix hash + mem::size_of::() + // nonce + mem::size_of::>() + // base fee per gas + mem::size_of::>() + // blob gas used + mem::size_of::>() + // excess blob gas + mem::size_of::>() + // parent beacon block root + self.extra_data.len() // extra data + } + + fn header_payload_length(&self) -> usize { + let mut length = 0; + length += self.parent_hash.length(); + length += self.ommers_hash.length(); + length += self.beneficiary.length(); + length += self.state_root.length(); + length += self.transactions_root.length(); + length += self.receipts_root.length(); + length += self.logs_bloom.length(); + length += self.difficulty.length(); + length += U256::from(self.number).length(); + length += U256::from(self.gas_limit).length(); + length += U256::from(self.gas_used).length(); + length += self.timestamp.length(); + length += self.extra_data.length(); + length += self.mix_hash.length(); + length += B64::new(self.nonce.to_be_bytes()).length(); + + if let Some(base_fee) = self.base_fee_per_gas { + length += U256::from(base_fee).length(); + } else if self.withdrawals_root.is_some() + || self.blob_gas_used.is_some() + || self.excess_blob_gas.is_some() + || self.parent_beacon_block_root.is_some() + { + length += 1; // EMPTY LIST CODE + } + + if let Some(root) = self.withdrawals_root { + length += root.length(); + } else if self.blob_gas_used.is_some() + || self.excess_blob_gas.is_some() + || self.parent_beacon_block_root.is_some() + { + length += 1; // EMPTY STRING CODE + } + + if let Some(blob_gas_used) = self.blob_gas_used { + length += U256::from(blob_gas_used).length(); + } else if self.excess_blob_gas.is_some() || self.parent_beacon_block_root.is_some() { + length += 1; // EMPTY LIST CODE + } + + if let Some(excess_blob_gas) = self.excess_blob_gas { + length += U256::from(excess_blob_gas).length(); + } else if self.parent_beacon_block_root.is_some() { + length += 1; // EMPTY LIST CODE + } + + // Encode parent beacon block root length. If new fields are added, the above pattern will + // need to be repeated and placeholder length added. Otherwise, it's impossible to + // tell _which_ fields are missing. This is mainly relevant for contrived cases + // where a header is created at random, for example: + // * A header is created with a withdrawals root, but no base fee. Shanghai blocks are + // post-London, so this is technically not valid. However, a tool like proptest would + // generate a block like this. + if let Some(parent_beacon_block_root) = self.parent_beacon_block_root { + length += parent_beacon_block_root.length(); + } + + length + } +} + +impl Encodable for Header { + fn encode(&self, out: &mut dyn BufMut) { + let list_header = alloy_rlp::Header { + list: true, + payload_length: self.header_payload_length(), + }; + list_header.encode(out); + self.parent_hash.encode(out); + self.ommers_hash.encode(out); + self.beneficiary.encode(out); + self.state_root.encode(out); + self.transactions_root.encode(out); + self.receipts_root.encode(out); + self.logs_bloom.encode(out); + self.difficulty.encode(out); + U256::from(self.number).encode(out); + U256::from(self.gas_limit).encode(out); + U256::from(self.gas_used).encode(out); + self.timestamp.encode(out); + self.extra_data.encode(out); + self.mix_hash.encode(out); + B64::new(self.nonce.to_be_bytes()).encode(out); + + // Encode base fee. Put empty list if base fee is missing, + // but withdrawals root is present. + if let Some(ref base_fee) = self.base_fee_per_gas { + U256::from(*base_fee).encode(out); + } else if self.withdrawals_root.is_some() + || self.blob_gas_used.is_some() + || self.excess_blob_gas.is_some() + || self.parent_beacon_block_root.is_some() + { + out.put_u8(EMPTY_LIST_CODE); + } + + // Encode withdrawals root. Put empty string if withdrawals root is missing, + // but blob gas used is present. + if let Some(ref root) = self.withdrawals_root { + root.encode(out); + } else if self.blob_gas_used.is_some() + || self.excess_blob_gas.is_some() + || self.parent_beacon_block_root.is_some() + { + out.put_u8(EMPTY_STRING_CODE); + } + + // Encode blob gas used. Put empty list if blob gas used is missing, + // but excess blob gas is present. + if let Some(ref blob_gas_used) = self.blob_gas_used { + U256::from(*blob_gas_used).encode(out); + } else if self.excess_blob_gas.is_some() || self.parent_beacon_block_root.is_some() { + out.put_u8(EMPTY_LIST_CODE); + } + + // Encode excess blob gas. Put empty list if excess blob gas is missing, + // but parent beacon block root is present. + if let Some(ref excess_blob_gas) = self.excess_blob_gas { + U256::from(*excess_blob_gas).encode(out); + } else if self.parent_beacon_block_root.is_some() { + out.put_u8(EMPTY_LIST_CODE); + } + + // Encode parent beacon block root. If new fields are added, the above pattern will need to + // be repeated and placeholders added. Otherwise, it's impossible to tell _which_ + // fields are missing. This is mainly relevant for contrived cases where a header is + // created at random, for example: + // * A header is created with a withdrawals root, but no base fee. Shanghai blocks are + // post-London, so this is technically not valid. However, a tool like proptest would + // generate a block like this. + if let Some(ref parent_beacon_block_root) = self.parent_beacon_block_root { + parent_beacon_block_root.encode(out); + } + } + + fn length(&self) -> usize { + let mut length = 0; + length += self.header_payload_length(); + length += length_of_length(length); + length + } +} + +impl Decodable for Header { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let rlp_head = alloy_rlp::Header::decode(buf)?; + if !rlp_head.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + let started_len = buf.len(); + let mut this = Self { + parent_hash: Decodable::decode(buf)?, + ommers_hash: Decodable::decode(buf)?, + beneficiary: Decodable::decode(buf)?, + state_root: Decodable::decode(buf)?, + transactions_root: Decodable::decode(buf)?, + receipts_root: Decodable::decode(buf)?, + logs_bloom: Decodable::decode(buf)?, + difficulty: Decodable::decode(buf)?, + number: U256::decode(buf)?.to::(), + gas_limit: U256::decode(buf)?.to::(), + gas_used: U256::decode(buf)?.to::(), + timestamp: Decodable::decode(buf)?, + extra_data: Decodable::decode(buf)?, + mix_hash: Decodable::decode(buf)?, + nonce: u64::from_be_bytes(B64::decode(buf)?.0), + base_fee_per_gas: None, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + }; + + if started_len - buf.len() < rlp_head.payload_length { + if buf + .first() + .map(|b| *b == EMPTY_LIST_CODE) + .unwrap_or_default() + { + buf.advance(1) + } else { + this.base_fee_per_gas = Some(U256::decode(buf)?.to::()); + } + } + + // Withdrawals root for post-shanghai headers + if started_len - buf.len() < rlp_head.payload_length { + if buf + .first() + .map(|b| *b == EMPTY_STRING_CODE) + .unwrap_or_default() + { + buf.advance(1) + } else { + this.withdrawals_root = Some(Decodable::decode(buf)?); + } + } + + // Blob gas used and excess blob gas for post-cancun headers + if started_len - buf.len() < rlp_head.payload_length { + if buf + .first() + .map(|b| *b == EMPTY_LIST_CODE) + .unwrap_or_default() + { + buf.advance(1) + } else { + this.blob_gas_used = Some(U256::decode(buf)?.to::()); + } + } + + if started_len - buf.len() < rlp_head.payload_length { + if buf + .first() + .map(|b| *b == EMPTY_LIST_CODE) + .unwrap_or_default() + { + buf.advance(1) + } else { + this.excess_blob_gas = Some(U256::decode(buf)?.to::()); + } + } + + // Decode parent beacon block root. If new fields are added, the above pattern will need to + // be repeated and placeholders decoded. Otherwise, it's impossible to tell _which_ + // fields are missing. This is mainly relevant for contrived cases where a header is + // created at random, for example: + // * A header is created with a withdrawals root, but no base fee. Shanghai blocks are + // post-London, so this is technically not valid. However, a tool like proptest would + // generate a block like this. + if started_len - buf.len() < rlp_head.payload_length { + this.parent_beacon_block_root = Some(B256::decode(buf)?); + } + + let consumed = started_len - buf.len(); + if consumed != rlp_head.payload_length { + return Err(alloy_rlp::Error::ListLengthMismatch { + expected: rlp_head.payload_length, + got: consumed, + }); + } + Ok(this) + } +} diff --git a/crates/derive/src/types/mod.rs b/crates/derive/src/types/mod.rs new file mode 100644 index 000000000..5c89290fd --- /dev/null +++ b/crates/derive/src/types/mod.rs @@ -0,0 +1,22 @@ +//! This module contains all of the types used within the derivation pipeline. + +mod system_config; +pub use system_config::{SystemAccounts, SystemConfig}; + +mod rollup_config; +pub use rollup_config::RollupConfig; + +mod transaction; + +mod network; + +mod header; +pub use header::{Header, EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH}; + +mod block; +pub use block::{BlockId, BlockInfo, BlockKind}; + +mod receipt; +pub use receipt::{Receipt, ReceiptWithBloom}; + +mod eips; diff --git a/crates/derive/src/types/network/mod.rs b/crates/derive/src/types/network/mod.rs new file mode 100644 index 000000000..2817fcc35 --- /dev/null +++ b/crates/derive/src/types/network/mod.rs @@ -0,0 +1,26 @@ +//! `alloy-network` crate ported to `no_std`. + +use crate::types::eips::eip2718::Eip2718Envelope; +use alloc::vec::Vec; +use alloy_primitives::B256; + +mod sealed; +pub use sealed::{Sealable, Sealed}; + +mod transaction; +pub use transaction::{Eip1559Transaction, Signed, Transaction, TxKind}; + +mod receipt; +pub use receipt::Receipt; + +/// A list of transactions, either hydrated or hashes. +// #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +// #[serde(untagged)] +pub enum TransactionList { + /// Hashes only. + Hashes(Vec), + /// Hydrated tx objects. + Hydrated(Vec), + /// Special case for uncle response + Uncled, +} diff --git a/crates/derive/src/types/network/receipt.rs b/crates/derive/src/types/network/receipt.rs new file mode 100644 index 000000000..2648cce66 --- /dev/null +++ b/crates/derive/src/types/network/receipt.rs @@ -0,0 +1,23 @@ +use alloy_primitives::{Bloom, Log}; + +/// Receipt is the result of a transaction execution. +pub trait Receipt { + /// Returns true if the transaction was successful. + fn success(&self) -> bool; + + /// Returns the bloom filter for the logs in the receipt. This operation + /// may be expensive. + fn bloom(&self) -> Bloom; + + /// Returns the bloom filter for the logs in the receipt, if it is cheap to + /// compute. + fn bloom_cheap(&self) -> Option { + None + } + + /// Returns the cumulative gas used in the block after this transaction was executed. + fn cumulative_gas_used(&self) -> u64; + + /// Returns the logs emitted by this transaction. + fn logs(&self) -> &[Log]; +} diff --git a/crates/derive/src/types/network/sealed.rs b/crates/derive/src/types/network/sealed.rs new file mode 100644 index 000000000..86d5c85a3 --- /dev/null +++ b/crates/derive/src/types/network/sealed.rs @@ -0,0 +1,68 @@ +use alloy_primitives::B256; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// A consensus hashable item, with its memoized hash. +/// +/// We do not implement +pub struct Sealed { + /// The inner item + inner: T, + /// Its hash. + seal: B256, +} + +impl core::ops::Deref for Sealed { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner() + } +} + +impl Sealed { + /// Instantiate without performing the hash. This should be used carefully. + pub const fn new_unchecked(inner: T, seal: B256) -> Self { + Self { inner, seal } + } + + /// Decompose into parts. + #[allow(clippy::missing_const_for_fn)] // false positive + pub fn into_parts(self) -> (T, B256) { + (self.inner, self.seal) + } + + /// Get the inner item. + #[inline(always)] + pub const fn inner(&self) -> &T { + &self.inner + } + + /// Get the hash. + #[inline(always)] + pub const fn seal(&self) -> B256 { + self.seal + } + + /// Geth the hash (alias for [`Self::seal`]). + #[inline(always)] + pub const fn hash(&self) -> B256 { + self.seal() + } +} + +/// Sealeable objects. +pub trait Sealable: Sized { + /// Calculate the seal hash, this may be slow. + fn hash(&self) -> B256; + + /// Seal the object by calculating the hash. This may be slow. + fn seal_slow(self) -> Sealed { + let seal = self.hash(); + Sealed::new_unchecked(self, seal) + } + + /// Instantiate an unchecked seal. This should be used with caution. + fn seal_unchecked(self, seal: B256) -> Sealed { + Sealed::new_unchecked(self, seal) + } +} diff --git a/crates/derive/src/types/network/transaction/common.rs b/crates/derive/src/types/network/transaction/common.rs new file mode 100644 index 000000000..a9c050686 --- /dev/null +++ b/crates/derive/src/types/network/transaction/common.rs @@ -0,0 +1,91 @@ +use alloy_primitives::Address; +use alloy_rlp::{Buf, BufMut, Decodable, Encodable, EMPTY_STRING_CODE}; + +/// The `to` field of a transaction. Either a target address, or empty for a +/// contract creation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum TxKind { + /// A transaction that creates a contract. + #[default] + Create, + /// A transaction that calls a contract or transfer. + Call(Address), +} + +impl From> for TxKind { + /// Creates a `TxKind::Call` with the `Some` address, `None` otherwise. + #[inline] + fn from(value: Option
) -> Self { + match value { + None => TxKind::Create, + Some(addr) => TxKind::Call(addr), + } + } +} + +impl From
for TxKind { + /// Creates a `TxKind::Call` with the given address. + #[inline] + fn from(value: Address) -> Self { + TxKind::Call(value) + } +} + +impl TxKind { + /// Returns the address of the contract that will be called or will receive the transfer. + pub const fn to(self) -> Option
{ + match self { + TxKind::Create => None, + TxKind::Call(to) => Some(to), + } + } + + /// Returns true if the transaction is a contract creation. + #[inline] + pub const fn is_create(self) -> bool { + matches!(self, TxKind::Create) + } + + /// Returns true if the transaction is a contract call. + #[inline] + pub const fn is_call(self) -> bool { + matches!(self, TxKind::Call(_)) + } + + /// Calculates a heuristic for the in-memory size of this object. + #[inline] + pub const fn size(self) -> usize { + core::mem::size_of::() + } +} + +impl Encodable for TxKind { + fn encode(&self, out: &mut dyn BufMut) { + match self { + TxKind::Call(to) => to.encode(out), + TxKind::Create => out.put_u8(EMPTY_STRING_CODE), + } + } + fn length(&self) -> usize { + match self { + TxKind::Call(to) => to.length(), + TxKind::Create => 1, // EMPTY_STRING_CODE is a single byte + } + } +} + +impl Decodable for TxKind { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + if let Some(&first) = buf.first() { + if first == EMPTY_STRING_CODE { + buf.advance(1); + Ok(TxKind::Create) + } else { + let addr =
::decode(buf)?; + Ok(TxKind::Call(addr)) + } + } else { + Err(alloy_rlp::Error::InputTooShort) + } + } +} diff --git a/crates/derive/src/types/network/transaction/mod.rs b/crates/derive/src/types/network/transaction/mod.rs new file mode 100644 index 000000000..97989ebc9 --- /dev/null +++ b/crates/derive/src/types/network/transaction/mod.rs @@ -0,0 +1,121 @@ +use alloc::vec::Vec; +use alloy_primitives::{keccak256, Bytes, ChainId, Signature, B256, U256}; +use alloy_rlp::BufMut; + +mod common; +pub use common::TxKind; + +mod signed; +pub use signed::Signed; + +/// Represents a minimal EVM transaction. +pub trait Transaction: core::any::Any + Send + Sync + 'static { + /// The signature type for this transaction. + /// + /// This is usually [`alloy_primitives::Signature`], however, it may be different for future + /// EIP-2718 transaction types, or in other networks. For example, in Optimism, the deposit + /// transaction signature is the unit type `()`. + type Signature; + + /// RLP-encodes the transaction for signing. + fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut); + + /// Outputs the length of the signature RLP encoding for the transaction. + fn payload_len_for_signature(&self) -> usize; + + /// RLP-encodes the transaction for signing it. Used to calculate `signature_hash`. + /// + /// See [`Transaction::encode_for_signing`]. + fn encoded_for_signing(&self) -> Vec { + let mut buf = Vec::with_capacity(self.payload_len_for_signature()); + self.encode_for_signing(&mut buf); + buf + } + + /// Calculate the signing hash for the transaction. + fn signature_hash(&self) -> B256 { + keccak256(self.encoded_for_signing()) + } + + /// Convert to a signed transaction by adding a signature and computing the + /// hash. + fn into_signed(self, signature: Signature) -> Signed + where + Self: Sized; + + /// Encode with a signature. This encoding is usually RLP, but may be + /// different for future EIP-2718 transaction types. + fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut); + + /// Decode a signed transaction. This decoding is usually RLP, but may be + /// different for future EIP-2718 transaction types. + /// + /// This MUST be the inverse of [`Transaction::encode_signed`]. + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> + where + Self: Sized; + + /// Get `data`. + fn input(&self) -> &[u8]; + /// Get `data`. + fn input_mut(&mut self) -> &mut Bytes; + /// Set `data`. + fn set_input(&mut self, data: Bytes); + + /// Get `to`. + fn to(&self) -> TxKind; + /// Set `to`. + fn set_to(&mut self, to: TxKind); + + /// Get `value`. + fn value(&self) -> U256; + /// Set `value`. + fn set_value(&mut self, value: U256); + + /// Get `chain_id`. + fn chain_id(&self) -> Option; + /// Set `chain_id`. + fn set_chain_id(&mut self, chain_id: ChainId); + + /// Get `nonce`. + fn nonce(&self) -> u64; + /// Set `nonce`. + fn set_nonce(&mut self, nonce: u64); + + /// Get `gas_limit`. + fn gas_limit(&self) -> u64; + /// Set `gas_limit`. + fn set_gas_limit(&mut self, limit: u64); + + /// Get `gas_price`. + fn gas_price(&self) -> Option; + /// Set `gas_price`. + fn set_gas_price(&mut self, price: U256); +} + +// TODO: Remove in favor of dyn trait upcasting (TBD, see https://github.com/rust-lang/rust/issues/65991#issuecomment-1903120162) +#[doc(hidden)] +impl dyn Transaction { + pub fn __downcast_ref(&self) -> Option<&T> { + if core::any::Any::type_id(self) == core::any::TypeId::of::() { + unsafe { Some(&*(self as *const _ as *const T)) } + } else { + None + } + } +} + +/// Captures getters and setters common across EIP-1559 transactions across all networks +pub trait Eip1559Transaction: Transaction { + /// Get `max_priority_fee_per_gas`. + #[doc(alias = "max_tip")] + fn max_priority_fee_per_gas(&self) -> U256; + /// Set `max_priority_fee_per_gas`. + #[doc(alias = "set_max_tip")] + fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: U256); + + /// Get `max_fee_per_gas`. + fn max_fee_per_gas(&self) -> U256; + /// Set `max_fee_per_gas`. + fn set_max_fee_per_gas(&mut self, max_fee_per_gas: U256); +} diff --git a/crates/derive/src/types/network/transaction/signed.rs b/crates/derive/src/types/network/transaction/signed.rs new file mode 100644 index 000000000..1c272e7f9 --- /dev/null +++ b/crates/derive/src/types/network/transaction/signed.rs @@ -0,0 +1,90 @@ +use crate::types::network::Transaction; +use alloc::{vec, vec::Vec}; +use alloy_primitives::{Signature, B256}; +use alloy_rlp::BufMut; + +/// A transaction with a signature and hash seal. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Signed { + tx: T, + signature: Sig, + hash: B256, +} + +impl core::ops::Deref for Signed { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.tx + } +} + +impl Signed { + /// Returns a reference to the transaction. + pub const fn tx(&self) -> &T { + &self.tx + } + + /// Returns a reference to the signature. + pub const fn signature(&self) -> &Sig { + &self.signature + } + + /// Returns a reference to the transaction hash. + pub const fn hash(&self) -> &B256 { + &self.hash + } +} + +impl Signed { + /// Instantiate from a transaction and signature. Does not verify the signature. + pub const fn new_unchecked(tx: T, signature: Signature, hash: B256) -> Self { + Self { + tx, + signature, + hash, + } + } + + /// Calculate the signing hash for the transaction. + pub fn signature_hash(&self) -> B256 { + self.tx.signature_hash() + } + + /// Output the signed RLP for the transaction. + pub fn encode_signed(&self, out: &mut dyn BufMut) { + self.tx.encode_signed(&self.signature, out); + } + + /// Produce the RLP encoded signed transaction. + pub fn rlp_signed(&self) -> Vec { + let mut buf = vec![]; + self.encode_signed(&mut buf); + buf + } +} + +impl alloy_rlp::Encodable for Signed { + fn encode(&self, out: &mut dyn BufMut) { + self.tx.encode_signed(&self.signature, out) + } + + // TODO: impl length +} + +impl alloy_rlp::Decodable for Signed { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + T::decode_signed(buf) + } +} + +#[cfg(feature = "k256")] +impl Signed { + /// Recover the signer of the transaction + pub fn recover_signer( + &self, + ) -> Result { + let sighash = self.tx.signature_hash(); + self.signature.recover_address_from_prehash(&sighash) + } +} diff --git a/crates/derive/src/types/receipt.rs b/crates/derive/src/types/receipt.rs new file mode 100644 index 000000000..da40ee808 --- /dev/null +++ b/crates/derive/src/types/receipt.rs @@ -0,0 +1,146 @@ +//! This module contains the receipt types used within the derivation pipeline. + +use alloc::vec::Vec; +use alloy_primitives::{Bloom, Log}; +use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable}; + +/// Receipt containing result of transaction execution. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct Receipt { + /// If transaction is executed successfully. + /// + /// This is the `statusCode` + pub success: bool, + /// Gas used + pub cumulative_gas_used: u64, + /// Log send from contracts. + pub logs: Vec, +} + +impl Receipt { + /// Calculates [`Log`]'s bloom filter. this is slow operation and [ReceiptWithBloom] can + /// be used to cache this value. + pub fn bloom_slow(&self) -> Bloom { + self.logs.iter().collect() + } + + /// Calculates the bloom filter for the receipt and returns the [ReceiptWithBloom] container + /// type. + pub fn with_bloom(self) -> ReceiptWithBloom { + self.into() + } +} + +/// [`Receipt`] with calculated bloom filter. +/// +/// This convenience type allows us to lazily calculate the bloom filter for a +/// receipt, similar to `Sealed`. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct ReceiptWithBloom { + /// The receipt. + pub receipt: Receipt, + /// The bloom filter. + pub bloom: Bloom, +} + +impl From for ReceiptWithBloom { + fn from(receipt: Receipt) -> Self { + let bloom = receipt.bloom_slow(); + ReceiptWithBloom { receipt, bloom } + } +} + +impl ReceiptWithBloom { + /// Create new [ReceiptWithBloom] + pub const fn new(receipt: Receipt, bloom: Bloom) -> Self { + Self { receipt, bloom } + } + + /// Consume the structure, returning only the receipt + #[allow(clippy::missing_const_for_fn)] // false positive + pub fn into_receipt(self) -> Receipt { + self.receipt + } + + /// Consume the structure, returning the receipt and the bloom filter + #[allow(clippy::missing_const_for_fn)] // false positive + pub fn into_components(self) -> (Receipt, Bloom) { + (self.receipt, self.bloom) + } + + fn payload_len(&self) -> usize { + self.receipt.success.length() + + self.receipt.cumulative_gas_used.length() + + self.bloom.length() + + self.receipt.logs.len() + } + + /// Returns the rlp header for the receipt payload. + fn receipt_rlp_header(&self) -> alloy_rlp::Header { + alloy_rlp::Header { + list: true, + payload_length: self.payload_len(), + } + } + + /// Encodes the receipt data. + fn encode_fields(&self, out: &mut dyn BufMut) { + self.receipt_rlp_header().encode(out); + self.receipt.success.encode(out); + self.receipt.cumulative_gas_used.encode(out); + self.bloom.encode(out); + self.receipt.logs.encode(out); + } + + /// Decodes the receipt payload + fn decode_receipt(buf: &mut &[u8]) -> alloy_rlp::Result { + let b: &mut &[u8] = &mut &**buf; + let rlp_head = alloy_rlp::Header::decode(b)?; + if !rlp_head.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + let started_len = b.len(); + + let success = Decodable::decode(b)?; + let cumulative_gas_used = Decodable::decode(b)?; + let bloom = Decodable::decode(b)?; + let logs = Decodable::decode(b)?; + + let receipt = Receipt { + success, + cumulative_gas_used, + logs, + }; + + let this = Self { receipt, bloom }; + let consumed = started_len - b.len(); + if consumed != rlp_head.payload_length { + return Err(alloy_rlp::Error::ListLengthMismatch { + expected: rlp_head.payload_length, + got: consumed, + }); + } + *buf = *b; + Ok(this) + } +} + +impl alloy_rlp::Encodable for ReceiptWithBloom { + fn encode(&self, out: &mut dyn BufMut) { + self.encode_fields(out); + } + + fn length(&self) -> usize { + let payload_length = self.receipt.success.length() + + self.receipt.cumulative_gas_used.length() + + self.bloom.length() + + self.receipt.logs.length(); + payload_length + length_of_length(payload_length) + } +} + +impl alloy_rlp::Decodable for ReceiptWithBloom { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Self::decode_receipt(buf) + } +} diff --git a/crates/derive/src/types/rollup_config.rs b/crates/derive/src/types/rollup_config.rs new file mode 100644 index 000000000..3f55263d1 --- /dev/null +++ b/crates/derive/src/types/rollup_config.rs @@ -0,0 +1,56 @@ +//! This module contains the [RollupConfig] type. + +use alloy_primitives::Address; + +/// The Rollup configuration. +#[derive(Debug, Clone, Copy)] +pub struct RollupConfig { + /// The block time of the L2, in seconds. + pub block_time: u64, + /// Sequencer batches may not be more than MaxSequencerDrift seconds after + /// the L1 timestamp of the sequencing window end. + /// + /// Note: When L1 has many 1 second consecutive blocks, and L2 grows at fixed 2 seconds, + /// the L2 time may still grow beyond this difference. + pub max_sequencer_drift: u64, + /// The sequencer window size. + pub sequencer_window_size: u64, + /// Number of L1 blocks between when a channel can be opened and when it can be closed. + pub channel_timeout: u64, + /// The L1 chain ID + pub l1_chain_id: u64, + /// The L2 chain ID + pub l2_chain_id: u64, + /// `regolith_time` sets the activation time of the Regolith network-upgrade: + /// a pre-mainnet Bedrock change that addresses findings of the Sherlock contest related to deposit attributes. + /// "Regolith" is the loose deposited rock that sits on top of Bedrock. + /// Active if regolith_time != None && L2 block timestamp >= Some(regolith_time), inactive otherwise. + pub regolith_time: Option, + /// `canyon_time` sets the activation time of the Canyon network upgrade. + /// Active if `canyon_time` != None && L2 block timestamp >= Some(canyon_time), inactive otherwise. + pub canyon_time: Option, + /// `delta_time` sets the activation time of the Delta network upgrade. + /// Active if `delta_time` != None && L2 block timestamp >= Some(delta_time), inactive otherwise. + pub delta_time: Option, + /// `ecotone_time` sets the activation time of the Ecotone network upgrade. + /// Active if `ecotone_time` != None && L2 block timestamp >= Some(ecotone_time), inactive otherwise. + pub ecotone_time: Option, + /// `fjord_time` sets the activation time of the Fjord network upgrade. + /// Active if `fjord_time` != None && L2 block timestamp >= Some(fjord_time), inactive otherwise. + pub fjord_time: Option, + /// `interop_time` sets the activation time for an experimental feature-set, activated like a hardfork. + /// Active if `interop_time` != None && L2 block timestamp >= Some(interop_time), inactive otherwise. + pub interop_time: Option, + /// `batch_inbox_address` is the L1 address that batches are sent to. + pub batch_inbox_address: Address, + /// `deposit_contract_address` is the L1 address that deposits are sent to. + pub deposit_contract_address: Address, + /// `l1_system_config_address` is the L1 address that the system config is stored at. + pub l1_system_config_address: Address, + /// `protocol_versions_address` is the L1 address that the protocol versions are stored at. + pub protocol_versions_address: Address, + /// `blobs_enabled_l1_timestamp` is the timestamp to start reading blobs as a batch data source. Optional. + pub blobs_enabled_l1_timestamp: Option, + /// `da_challenge_address` is the L1 address that the data availability challenge contract is stored at. + pub da_challenge_address: Option
, +} diff --git a/crates/derive/src/types/system_config.rs b/crates/derive/src/types/system_config.rs new file mode 100644 index 000000000..ecc12bd7b --- /dev/null +++ b/crates/derive/src/types/system_config.rs @@ -0,0 +1,83 @@ +//! This module contains the [SystemConfig] type. + +use super::{Receipt, RollupConfig}; +use alloy_primitives::{address, b256, Address, Log, B256, U256}; +use anyhow::Result; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// `keccak256("ConfigUpdate(uint256,uint8,bytes)")` +const CONFIG_UPDATE_TOPIC: B256 = + b256!("1d2b0bda21d56b8bd12d4f94ebacffdfb35f5e226f84b461103bb8beab6353be"); + +/// Optimism system config contract values +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SystemConfig { + /// Batch sender address + pub batch_sender: Address, + /// L2 gas limit + pub gas_limit: U256, + /// Fee overhead + pub l1_fee_overhead: U256, + /// Fee scalar + pub l1_fee_scalar: U256, + /// Sequencer's signer for unsafe blocks + pub unsafe_block_signer: Address, +} + +impl SystemConfig { + /// Filters all L1 receipts to find config updates and applies the config updates. + pub fn update_with_receipts( + &mut self, + receipts: &[Receipt], + rollup_config: &RollupConfig, + l1_time: u64, + ) -> Result<()> { + for receipt in receipts { + if !receipt.success { + continue; + } + + for log in receipt.logs.iter() { + let topics = log.topics(); + // TODO: System config address isn't in this type, replace `Address::ZERO`. + if log.address == Address::ZERO + && !topics.is_empty() + && topics[0] == CONFIG_UPDATE_TOPIC + { + self.process_config_update_log(log, rollup_config, l1_time)?; + } + } + } + Ok(()) + } + + /// Processes a single config update log. + fn process_config_update_log(&mut self, _: &Log, _: &RollupConfig, _: u64) -> Result<()> { + todo!("Process log update event."); + } +} + +/// System accounts +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SystemAccounts { + /// The address that can deposit attributes + pub attributes_depositor: Address, + /// The address of the attributes predeploy + pub attributes_predeploy: Address, + /// The address of the fee vault + pub fee_vault: Address, +} + +impl Default for SystemAccounts { + fn default() -> Self { + Self { + attributes_depositor: address!("deaddeaddeaddeaddeaddeaddeaddeaddead0001"), + attributes_predeploy: address!("4200000000000000000000000000000000000015"), + fee_vault: address!("4200000000000000000000000000000000000011"), + } + } +} diff --git a/crates/derive/src/types/transaction/eip1559.rs b/crates/derive/src/types/transaction/eip1559.rs new file mode 100644 index 000000000..9acb8a887 --- /dev/null +++ b/crates/derive/src/types/transaction/eip1559.rs @@ -0,0 +1,383 @@ +use crate::types::{ + eips::eip2930::AccessList, + network::{Signed, Transaction, TxKind}, + transaction::TxType, +}; +use alloc::vec::Vec; +use alloy_primitives::{keccak256, Bytes, ChainId, Signature, U256}; +use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header}; +use core::mem; + +/// A transaction with a priority fee ([EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)). +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct TxEip1559 { + /// EIP-155: Simple replay attack protection + pub chain_id: u64, + /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + pub nonce: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + pub gas_limit: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + /// + /// This is also known as `GasFeeCap` + pub max_fee_per_gas: u128, + /// Max Priority fee that transaction is paying + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + /// + /// This is also known as `GasTipCap` + pub max_priority_fee_per_gas: u128, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + pub to: TxKind, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; formally Tv. + pub value: U256, + /// The accessList specifies a list of addresses and storage keys; + /// these addresses and storage keys are added into the `accessed_addresses` + /// and `accessed_storage_keys` global sets (introduced in EIP-2929). + /// A gas cost is charged, though at a discount relative to the cost of + /// accessing outside the list. + pub access_list: AccessList, + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). pub init: An unlimited size byte array specifying the + /// EVM-code for the account initialisation procedure CREATE, + /// data: An unlimited size byte array specifying the + /// input data of the message call, formally Td. + pub input: Bytes, +} + +impl TxEip1559 { + /// Returns the effective gas price for the given `base_fee`. + pub const fn effective_gas_price(&self, base_fee: Option) -> u128 { + match base_fee { + None => self.max_fee_per_gas, + Some(base_fee) => { + // if the tip is greater than the max priority fee per gas, set it to the max + // priority fee per gas + base fee + let tip = self.max_fee_per_gas.saturating_sub(base_fee as u128); + if tip > self.max_priority_fee_per_gas { + self.max_priority_fee_per_gas + base_fee as u128 + } else { + // otherwise return the max fee per gas + self.max_fee_per_gas + } + } + } + } + + /// Decodes the inner [TxEip1559] fields from RLP bytes. + /// + /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following + /// RLP fields in the following order: + /// + /// - `chain_id` + /// - `nonce` + /// - `max_priority_fee_per_gas` + /// - `max_fee_per_gas` + /// - `gas_limit` + /// - `to` + /// - `value` + /// - `data` (`input`) + /// - `access_list` + pub(crate) fn decode_inner(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + max_priority_fee_per_gas: Decodable::decode(buf)?, + max_fee_per_gas: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + to: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + }) + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub(crate) fn fields_len(&self) -> usize { + let mut len = 0; + len += self.chain_id.length(); + len += self.nonce.length(); + len += self.max_priority_fee_per_gas.length(); + len += self.max_fee_per_gas.length(); + len += self.gas_limit.length(); + len += self.to.length(); + len += self.value.length(); + len += self.input.0.length(); + len += self.access_list.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub(crate) fn encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.max_priority_fee_per_gas.encode(out); + self.max_fee_per_gas.encode(out); + self.gas_limit.encode(out); + self.to.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + } + + /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating + /// hash that for eip2718 does not require rlp header + pub(crate) fn encode_with_signature( + &self, + signature: &Signature, + out: &mut dyn alloy_rlp::BufMut, + ) { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + let header = Header { + list: true, + payload_length, + }; + header.encode(out); + self.encode_fields(out); + signature.write_rlp_vrs(out); + } + + /// Output the length of the RLP signed transaction encoding, _without_ a RLP string header. + pub fn payload_len_with_signature_without_header(&self, signature: &Signature) -> usize { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. + pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { + let len = self.payload_len_with_signature_without_header(signature); + length_of_length(len) + len + } + + /// Get transaction type + pub(crate) const fn tx_type(&self) -> TxType { + TxType::Eip1559 + } + + /// Calculates a heuristic for the in-memory size of the [TxEip1559] transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_limit + mem::size_of::() + // max_fee_per_gas + mem::size_of::() + // max_priority_fee_per_gas + self.to.size() + // to + mem::size_of::() + // value + self.access_list.size() + // access_list + self.input.len() // input + } +} + +impl Encodable for TxEip1559 { + fn encode(&self, out: &mut dyn BufMut) { + Header { + list: true, + payload_length: self.fields_len(), + } + .encode(out); + self.encode_fields(out); + } + + fn length(&self) -> usize { + let payload_length = self.fields_len(); + length_of_length(payload_length) + payload_length + } +} + +impl Decodable for TxEip1559 { + fn decode(data: &mut &[u8]) -> alloy_rlp::Result { + let header = Header::decode(data)?; + let remaining_len = data.len(); + + if header.payload_length > remaining_len { + return Err(alloy_rlp::Error::InputTooShort); + } + + Self::decode_inner(data) + } +} + +impl Transaction for TxEip1559 { + type Signature = Signature; + + fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { + out.put_u8(self.tx_type() as u8); + Header { + list: true, + payload_length: self.fields_len(), + } + .encode(out); + self.encode_fields(out); + } + + fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + fn into_signed(self, signature: Signature) -> Signed { + let payload_length = 1 + self.fields_len() + signature.rlp_vrs_len(); + let mut buf = Vec::with_capacity(payload_length); + buf.put_u8(TxType::Eip1559 as u8); + self.encode_signed(&signature, &mut buf); + let hash = keccak256(&buf); + + // Drop any v chain id value to ensure the signature format is correct at the time of + // combination for an EIP-1559 transaction. V should indicate the y-parity of the + // signature. + Signed::new_unchecked(self, signature.with_parity_bool(), hash) + } + + fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) { + TxEip1559::encode_with_signature(self, signature, out) + } + + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { + let header = Header::decode(buf)?; + if !header.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + + let tx = Self::decode_inner(buf)?; + let signature = Signature::decode_rlp_vrs(buf)?; + + Ok(tx.into_signed(signature)) + } + + fn input(&self) -> &[u8] { + &self.input + } + + fn input_mut(&mut self) -> &mut Bytes { + &mut self.input + } + + fn set_input(&mut self, input: Bytes) { + self.input = input; + } + + fn to(&self) -> TxKind { + self.to + } + + fn set_to(&mut self, to: TxKind) { + self.to = to; + } + + fn value(&self) -> U256 { + self.value + } + + fn set_value(&mut self, value: U256) { + self.value = value; + } + + fn chain_id(&self) -> Option { + Some(self.chain_id) + } + + fn set_chain_id(&mut self, chain_id: ChainId) { + self.chain_id = chain_id; + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn set_nonce(&mut self, nonce: u64) { + self.nonce = nonce; + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn set_gas_limit(&mut self, limit: u64) { + self.gas_limit = limit; + } + + fn gas_price(&self) -> Option { + None + } + + fn set_gas_price(&mut self, price: U256) { + let _ = price; + } +} + +#[cfg(all(test, feature = "k256"))] +mod tests { + use super::TxEip1559; + use crate::TxKind; + use alloy_eips::eip2930::AccessList; + use alloy_network::Transaction; + use alloy_primitives::{address, b256, hex, Address, Signature, B256, U256}; + use alloy_rlp::Encodable; + + #[test] + fn recover_signer_eip1559() { + let signer: Address = address!("dd6b8b3dc6b7ad97db52f08a275ff4483e024cea"); + let hash: B256 = b256!("0ec0b6a2df4d87424e5f6ad2a654e27aaeb7dac20ae9e8385cc09087ad532ee0"); + + let tx = TxEip1559 { + chain_id: 1, + nonce: 0x42, + gas_limit: 44386, + to: TxKind::Call( address!("6069a6c32cf691f5982febae4faf8a6f3ab2f0f6")), + value: U256::from(0_u64), + input: hex!("a22cb4650000000000000000000000005eee75727d804a2b13038928d36f8b188945a57a0000000000000000000000000000000000000000000000000000000000000000").into(), + max_fee_per_gas: 0x4a817c800, + max_priority_fee_per_gas: 0x3b9aca00, + access_list: AccessList::default(), + }; + + let sig = Signature::from_scalars_and_parity( + b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"), + b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"), + false, + ) + .unwrap(); + + assert_eq!( + tx.signature_hash(), + hex!("0d5688ac3897124635b6cf1bc0e29d6dfebceebdc10a54d74f2ef8b56535b682") + ); + + dbg!({ + let mut buf = vec![]; + tx.encode(&mut buf); + alloy_primitives::hex::encode(&buf) + }); + + dbg!(alloy_primitives::hex::encode(tx.signature_hash())); + + let signed_tx = tx.into_signed(sig); + assert_eq!(*signed_tx.hash(), hash, "Expected same hash"); + assert_eq!( + signed_tx.recover_signer().unwrap(), + signer, + "Recovering signer should pass." + ); + } +} diff --git a/crates/derive/src/types/transaction/eip2930.rs b/crates/derive/src/types/transaction/eip2930.rs new file mode 100644 index 000000000..0cb861bd8 --- /dev/null +++ b/crates/derive/src/types/transaction/eip2930.rs @@ -0,0 +1,291 @@ +use crate::types::{ + eips::eip2930::AccessList, + network::{Signed, Transaction, TxKind}, + transaction::TxType, +}; +use alloc::vec::Vec; +use alloy_primitives::{keccak256, Bytes, ChainId, Signature, U256}; +use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header}; +use core::mem; + +/// Transaction with an [`AccessList`] ([EIP-2930](https://eips.ethereum.org/EIPS/eip-2930)). +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct TxEip2930 { + /// Added as EIP-pub 155: Simple replay attack protection + pub chain_id: ChainId, + /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + pub nonce: u64, + /// A scalar value equal to the number of + /// Wei to be paid per unit of gas for all computation + /// costs incurred as a result of the execution of this transaction; formally Tp. + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + pub gas_price: u128, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + pub gas_limit: u64, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + pub to: TxKind, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; formally Tv. + pub value: U256, + /// The accessList specifies a list of addresses and storage keys; + /// these addresses and storage keys are added into the `accessed_addresses` + /// and `accessed_storage_keys` global sets (introduced in EIP-2929). + /// A gas cost is charged, though at a discount relative to the cost of + /// accessing outside the list. + pub access_list: AccessList, + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). pub init: An unlimited size byte array specifying the + /// EVM-code for the account initialisation procedure CREATE, + /// data: An unlimited size byte array specifying the + /// input data of the message call, formally Td. + pub input: Bytes, +} + +impl TxEip2930 { + /// Calculates a heuristic for the in-memory size of the [TxEip2930] transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_price + mem::size_of::() + // gas_limit + self.to.size() + // to + mem::size_of::() + // value + self.access_list.size() + // access_list + self.input.len() // input + } + + /// Decodes the inner [TxEip2930] fields from RLP bytes. + /// + /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following + /// RLP fields in the following order: + /// + /// - `chain_id` + /// - `nonce` + /// - `gas_price` + /// - `gas_limit` + /// - `to` + /// - `value` + /// - `data` (`input`) + /// - `access_list` + pub(crate) fn decode_inner(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + gas_price: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + to: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + }) + } + + /// Outputs the length of the transaction's fields, without a RLP header. + pub(crate) fn fields_len(&self) -> usize { + let mut len = 0; + len += self.chain_id.length(); + len += self.nonce.length(); + len += self.gas_price.length(); + len += self.gas_limit.length(); + len += self.to.length(); + len += self.value.length(); + len += self.input.0.length(); + len += self.access_list.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub(crate) fn encode_fields(&self, out: &mut dyn BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.gas_price.encode(out); + self.gas_limit.encode(out); + self.to.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + } + + /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating + /// hash that for eip2718 does not require rlp header + pub(crate) fn encode_with_signature(&self, signature: &Signature, out: &mut dyn BufMut) { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + let header = Header { + list: true, + payload_length, + }; + header.encode(out); + self.encode_fields(out); + signature.write_rlp_vrs(out); + } + + /// Output the length of the RLP signed transaction encoding, _without_ a RLP string header. + pub fn payload_len_with_signature_without_header(&self, signature: &Signature) -> usize { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. + pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { + let len = self.payload_len_with_signature_without_header(signature); + length_of_length(len) + len + } + + /// Get transaction type. + pub const fn tx_type(&self) -> TxType { + TxType::Eip2930 + } +} + +impl Encodable for TxEip2930 { + fn encode(&self, out: &mut dyn BufMut) { + Header { + list: true, + payload_length: self.fields_len(), + } + .encode(out); + self.encode_fields(out); + } + + fn length(&self) -> usize { + let payload_length = self.fields_len(); + length_of_length(payload_length) + payload_length + } +} + +impl Decodable for TxEip2930 { + fn decode(data: &mut &[u8]) -> alloy_rlp::Result { + let header = Header::decode(data)?; + let remaining_len = data.len(); + + if header.payload_length > remaining_len { + return Err(alloy_rlp::Error::InputTooShort); + } + + Self::decode_inner(data) + } +} + +impl Transaction for TxEip2930 { + type Signature = Signature; + // type Receipt = ReceiptWithBloom; + + fn encode_for_signing(&self, out: &mut dyn BufMut) { + out.put_u8(self.tx_type() as u8); + Header { + list: true, + payload_length: self.fields_len(), + } + .encode(out); + self.encode_fields(out); + } + + fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + fn into_signed(self, signature: Signature) -> Signed { + let payload_length = 1 + self.fields_len() + signature.rlp_vrs_len(); + let mut buf = Vec::with_capacity(payload_length); + buf.put_u8(TxType::Eip2930 as u8); + self.encode_signed(&signature, &mut buf); + let hash = keccak256(&buf); + + // Drop any v chain id value to ensure the signature format is correct at the time of + // combination for an EIP-2930 transaction. V should indicate the y-parity of the + // signature. + Signed::new_unchecked(self, signature.with_parity_bool(), hash) + } + + fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) { + self.encode_with_signature(signature, out) + } + + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { + let header = Header::decode(buf)?; + if !header.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + + let tx = Self::decode_inner(buf)?; + let signature = Signature::decode_rlp_vrs(buf)?; + + Ok(tx.into_signed(signature)) + } + + fn input(&self) -> &[u8] { + &self.input + } + + fn input_mut(&mut self) -> &mut Bytes { + &mut self.input + } + + fn set_input(&mut self, input: Bytes) { + self.input = input; + } + + fn to(&self) -> TxKind { + self.to + } + + fn set_to(&mut self, to: TxKind) { + self.to = to; + } + + fn value(&self) -> U256 { + self.value + } + + fn set_value(&mut self, value: U256) { + self.value = value; + } + + fn chain_id(&self) -> Option { + Some(self.chain_id) + } + + fn set_chain_id(&mut self, chain_id: ChainId) { + self.chain_id = chain_id; + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn set_nonce(&mut self, nonce: u64) { + self.nonce = nonce; + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn set_gas_limit(&mut self, limit: u64) { + self.gas_limit = limit; + } + + fn gas_price(&self) -> Option { + Some(U256::from(self.gas_price)) + } + + fn set_gas_price(&mut self, price: U256) { + if let Ok(price) = price.try_into() { + self.gas_price = price; + } + } +} diff --git a/crates/derive/src/types/transaction/eip4844.rs b/crates/derive/src/types/transaction/eip4844.rs new file mode 100644 index 000000000..cdb7229dc --- /dev/null +++ b/crates/derive/src/types/transaction/eip4844.rs @@ -0,0 +1,353 @@ +use crate::types::{ + eips::{eip2930::AccessList, eip4844::DATA_GAS_PER_BLOB}, + network::{Signed, Transaction, TxKind}, + transaction::TxType, +}; +use alloc::vec::Vec; +use alloy_primitives::{keccak256, Bytes, ChainId, Signature, B256, U256}; +use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header}; +use core::mem; + +/// [EIP-4844 Blob Transaction](https://eips.ethereum.org/EIPS/eip-4844#blob-transaction) +/// +/// A transaction with blob hashes and max blob fee +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct TxEip4844 { + /// Added as EIP-pub 155: Simple replay attack protection + pub chain_id: ChainId, + /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + pub nonce: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + pub gas_limit: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + /// + /// This is also known as `GasFeeCap` + pub max_fee_per_gas: u128, + /// Max Priority fee that transaction is paying + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + /// + /// This is also known as `GasTipCap` + pub max_priority_fee_per_gas: u128, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + pub to: TxKind, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; formally Tv. + pub value: U256, + /// The accessList specifies a list of addresses and storage keys; + /// these addresses and storage keys are added into the `accessed_addresses` + /// and `accessed_storage_keys` global sets (introduced in EIP-2929). + /// A gas cost is charged, though at a discount relative to the cost of + /// accessing outside the list. + pub access_list: AccessList, + + /// It contains a vector of fixed size hash(32 bytes) + pub blob_versioned_hashes: Vec, + + /// Max fee per data gas + /// + /// aka BlobFeeCap or blobGasFeeCap + pub max_fee_per_blob_gas: u128, + + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). pub init: An unlimited size byte array specifying the + /// EVM-code for the account initialisation procedure CREATE, + /// data: An unlimited size byte array specifying the + /// input data of the message call, formally Td. + pub input: Bytes, +} + +impl TxEip4844 { + /// Returns the effective gas price for the given `base_fee`. + pub const fn effective_gas_price(&self, base_fee: Option) -> u128 { + match base_fee { + None => self.max_fee_per_gas, + Some(base_fee) => { + // if the tip is greater than the max priority fee per gas, set it to the max + // priority fee per gas + base fee + let tip = self.max_fee_per_gas.saturating_sub(base_fee as u128); + if tip > self.max_priority_fee_per_gas { + self.max_priority_fee_per_gas + base_fee as u128 + } else { + // otherwise return the max fee per gas + self.max_fee_per_gas + } + } + } + } + + /// Returns the total gas for all blobs in this transaction. + #[inline] + pub fn blob_gas(&self) -> u64 { + // SAFETY: we don't expect u64::MAX / DATA_GAS_PER_BLOB hashes in a single transaction + self.blob_versioned_hashes.len() as u64 * DATA_GAS_PER_BLOB + } + + /// Decodes the inner [TxEip4844] fields from RLP bytes. + /// + /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following + /// RLP fields in the following order: + /// + /// - `chain_id` + /// - `nonce` + /// - `max_priority_fee_per_gas` + /// - `max_fee_per_gas` + /// - `gas_limit` + /// - `to` + /// - `value` + /// - `data` (`input`) + /// - `access_list` + /// - `max_fee_per_blob_gas` + /// - `blob_versioned_hashes` + pub fn decode_inner(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + max_priority_fee_per_gas: Decodable::decode(buf)?, + max_fee_per_gas: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + to: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + max_fee_per_blob_gas: Decodable::decode(buf)?, + blob_versioned_hashes: Decodable::decode(buf)?, + }) + } + + /// Outputs the length of the transaction's fields, without a RLP header. + pub(crate) fn fields_len(&self) -> usize { + let mut len = 0; + len += self.chain_id.length(); + len += self.nonce.length(); + len += self.gas_limit.length(); + len += self.max_fee_per_gas.length(); + len += self.max_priority_fee_per_gas.length(); + len += self.to.length(); + len += self.value.length(); + len += self.access_list.length(); + len += self.blob_versioned_hashes.length(); + len += self.max_fee_per_blob_gas.length(); + len += self.input.0.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub(crate) fn encode_fields(&self, out: &mut dyn BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.max_priority_fee_per_gas.encode(out); + self.max_fee_per_gas.encode(out); + self.gas_limit.encode(out); + self.to.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + self.max_fee_per_blob_gas.encode(out); + self.blob_versioned_hashes.encode(out); + } + + /// Calculates a heuristic for the in-memory size of the [TxEip4844] transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_limit + mem::size_of::() + // max_fee_per_gas + mem::size_of::() + // max_priority_fee_per_gas + self.to.size() + // to + mem::size_of::() + // value + self.access_list.size() + // access_list + self.input.len() + // input + self.blob_versioned_hashes.capacity() * mem::size_of::() + // blob hashes size + mem::size_of::() // max_fee_per_data_gas + } + + /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating + /// hash that for eip2718 does not require rlp header + pub(crate) fn encode_with_signature( + &self, + signature: &Signature, + out: &mut dyn BufMut, + with_header: bool, + ) { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + if with_header { + Header { + list: false, + payload_length: 1 + length_of_length(payload_length) + payload_length, + } + .encode(out); + } + out.put_u8(self.tx_type() as u8); + let header = Header { + list: true, + payload_length, + }; + header.encode(out); + self.encode_fields(out); + signature.encode(out); + } + + /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. + pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { + let len = self.payload_len_with_signature_without_header(signature); + length_of_length(len) + len + } + + /// Output the length of the RLP signed transaction encoding, _without_ a RLP header. + pub fn payload_len_with_signature_without_header(&self, signature: &Signature) -> usize { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Get transaction type + pub const fn tx_type(&self) -> TxType { + TxType::Eip4844 + } + + /// Encodes the legacy transaction in RLP for signing. + /// + /// This encodes the transaction as: + /// `tx_type || rlp(chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, + /// value, input, access_list, max_fee_per_blob_gas, blob_versioned_hashes)` + /// + /// Note that there is no rlp header before the transaction type byte. + pub fn encode_for_signing(&self, out: &mut dyn BufMut) { + out.put_u8(self.tx_type() as u8); + Header { + list: true, + payload_length: self.fields_len(), + } + .encode(out); + self.encode_fields(out); + } + + /// Outputs the length of the signature RLP encoding for the transaction. + pub fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } +} + +impl Transaction for TxEip4844 { + type Signature = Signature; + + fn chain_id(&self) -> Option { + Some(self.chain_id) + } + + fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + fn into_signed(self, signature: Signature) -> Signed { + let payload_length = 1 + self.fields_len() + signature.rlp_vrs_len(); + let mut buf = Vec::with_capacity(payload_length); + buf.put_u8(TxType::Eip1559 as u8); + self.encode_signed(&signature, &mut buf); + let hash = keccak256(&buf); + + // Drop any v chain id value to ensure the signature format is correct at the time of + // combination for an EIP-4844 transaction. V should indicate the y-parity of the + // signature. + Signed::new_unchecked(self, signature.with_parity_bool(), hash) + } + + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { + let header = Header::decode(buf)?; + if !header.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + + let tx = Self::decode_inner(buf)?; + let signature = Signature::decode_rlp_vrs(buf)?; + + Ok(tx.into_signed(signature)) + } + + fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { + self.encode_for_signing(out); + } + + fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) { + TxEip4844::encode_with_signature(self, signature, out, true); + } + + fn input(&self) -> &[u8] { + &self.input + } + + fn input_mut(&mut self) -> &mut Bytes { + &mut self.input + } + + fn set_input(&mut self, input: Bytes) { + self.input = input; + } + + fn to(&self) -> TxKind { + self.to + } + + fn set_to(&mut self, to: TxKind) { + self.to = to; + } + + fn value(&self) -> U256 { + self.value + } + + fn set_value(&mut self, value: U256) { + self.value = value; + } + + fn set_chain_id(&mut self, chain_id: ChainId) { + self.chain_id = chain_id; + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn set_nonce(&mut self, nonce: u64) { + self.nonce = nonce; + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn set_gas_limit(&mut self, limit: u64) { + self.gas_limit = limit; + } + + fn gas_price(&self) -> Option { + None + } + + fn set_gas_price(&mut self, price: U256) { + let _ = price; + } +} diff --git a/crates/derive/src/types/transaction/envelope.rs b/crates/derive/src/types/transaction/envelope.rs new file mode 100644 index 000000000..3b4e6b945 --- /dev/null +++ b/crates/derive/src/types/transaction/envelope.rs @@ -0,0 +1,195 @@ +use crate::types::{ + eips::eip2718::{Decodable2718, Eip2718Error, Encodable2718}, + network::Signed, + transaction::{TxEip1559, TxEip2930, TxEip4844, TxLegacy}, +}; +use alloy_rlp::{length_of_length, Decodable, Encodable}; + +/// Ethereum `TransactionType` flags as specified in EIPs [2718], [1559], and +/// [2930]. +/// +/// [2718]: https://eips.ethereum.org/EIPS/eip-2718 +/// [1559]: https://eips.ethereum.org/EIPS/eip-1559 +/// [2930]: https://eips.ethereum.org/EIPS/eip-2930 +/// [4844]: https://eips.ethereum.org/EIPS/eip-4844 +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub enum TxType { + /// Wrapped legacy transaction type. + Legacy = 0, + /// EIP-2930 transaction type. + Eip2930 = 1, + /// EIP-1559 transaction type. + Eip1559 = 2, + /// EIP-4844 transaction type. + Eip4844 = 3, +} + +impl TryFrom for TxType { + type Error = Eip2718Error; + + fn try_from(value: u8) -> Result { + match value { + // SAFETY: repr(u8) with explicit discriminant + ..=3 => Ok(unsafe { core::mem::transmute(value) }), + _ => Err(Eip2718Error::UnexpectedType(value)), + } + } +} + +/// The Ethereum [EIP-2718] Transaction Envelope. +/// +/// # Note: +/// +/// This enum distinguishes between tagged and untagged legacy transactions, as +/// the in-protocol merkle tree may commit to EITHER 0-prefixed or raw. +/// Therefore we must ensure that encoding returns the precise byte-array that +/// was decoded, preserving the presence or absence of the `TransactionType` +/// flag. +/// +/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TxEnvelope { + /// An untagged [`TxLegacy`]. + Legacy(Signed), + /// A [`TxLegacy`] tagged with type 0. + TaggedLegacy(Signed), + /// A [`TxEip2930`]. + Eip2930(Signed), + /// A [`TxEip1559`]. + Eip1559(Signed), + /// A [`TxEip4844`]. + Eip4844(Signed), +} + +impl From> for TxEnvelope { + fn from(v: Signed) -> Self { + Self::Eip2930(v) + } +} + +impl From> for TxEnvelope { + fn from(v: Signed) -> Self { + Self::Eip1559(v) + } +} + +impl TxEnvelope { + /// Return the [`TxType`] of the inner txn. + pub const fn tx_type(&self) -> TxType { + match self { + Self::Legacy(_) | Self::TaggedLegacy(_) => TxType::Legacy, + Self::Eip2930(_) => TxType::Eip2930, + Self::Eip1559(_) => TxType::Eip1559, + Self::Eip4844(_) => TxType::Eip4844, + } + } + + /// Return the length of the inner txn. + pub fn inner_length(&self) -> usize { + match self { + Self::Legacy(t) | Self::TaggedLegacy(t) => t.length(), + Self::Eip2930(t) => t.length(), + Self::Eip1559(t) => t.length(), + Self::Eip4844(t) => t.length(), + } + } + + /// Return the RLP payload length of the network-serialized wrapper + fn rlp_payload_length(&self) -> usize { + if let Self::Legacy(t) = self { + return t.length(); + } + // length of inner tx body + let inner_length = self.inner_length(); + // with tx type byte + inner_length + 1 + } +} + +impl Encodable for TxEnvelope { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + self.network_encode(out) + } + + fn length(&self) -> usize { + let mut payload_length = self.rlp_payload_length(); + if !self.is_legacy() { + payload_length += length_of_length(payload_length); + } + payload_length + } +} + +impl Decodable for TxEnvelope { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + match Self::network_decode(buf) { + Ok(t) => Ok(t), + Err(Eip2718Error::RlpError(e)) => Err(e), + Err(_) => Err(alloy_rlp::Error::Custom("Unexpected type")), + } + } +} + +impl Decodable2718 for TxEnvelope { + fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result { + match ty.try_into()? { + TxType::Legacy => Ok(Self::TaggedLegacy( + Decodable::decode(buf).map_err(Eip2718Error::RlpError)?, + )), + TxType::Eip2930 => Ok(Self::Eip2930( + Decodable::decode(buf).map_err(Eip2718Error::RlpError)?, + )), + TxType::Eip1559 => Ok(Self::Eip1559( + Decodable::decode(buf).map_err(Eip2718Error::RlpError)?, + )), + TxType::Eip4844 => Ok(Self::Eip4844( + Decodable::decode(buf).map_err(Eip2718Error::RlpError)?, + )), + } + } + + fn fallback_decode(buf: &mut &[u8]) -> Result { + Ok(TxEnvelope::Legacy( + Decodable::decode(buf).map_err(Eip2718Error::RlpError)?, + )) + } +} + +impl Encodable2718 for TxEnvelope { + fn type_flag(&self) -> Option { + match self { + Self::Legacy(_) => None, + Self::TaggedLegacy(_) => Some(TxType::Legacy as u8), + Self::Eip2930(_) => Some(TxType::Eip2930 as u8), + Self::Eip1559(_) => Some(TxType::Eip1559 as u8), + Self::Eip4844(_) => Some(TxType::Eip4844 as u8), + } + } + + fn encode_2718_len(&self) -> usize { + self.inner_length() + !self.is_legacy() as usize + } + + fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) { + match self { + TxEnvelope::Legacy(tx) => tx.encode(out), + TxEnvelope::TaggedLegacy(tx) => { + out.put_u8(TxType::Legacy as u8); + tx.encode(out); + } + TxEnvelope::Eip2930(tx) => { + out.put_u8(TxType::Eip2930 as u8); + tx.encode(out); + } + TxEnvelope::Eip1559(tx) => { + out.put_u8(TxType::Eip1559 as u8); + tx.encode(out); + } + TxEnvelope::Eip4844(tx) => { + out.put_u8(TxType::Eip4844 as u8); + tx.encode(out); + } + } + } +} diff --git a/crates/derive/src/types/transaction/legacy.rs b/crates/derive/src/types/transaction/legacy.rs new file mode 100644 index 000000000..8cea88bdc --- /dev/null +++ b/crates/derive/src/types/transaction/legacy.rs @@ -0,0 +1,296 @@ +use crate::types::network::{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}; +use core::mem; + +/// Legacy transaction. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct TxLegacy { + /// Added as EIP-155: Simple replay attack protection + pub chain_id: Option, + /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + pub nonce: u64, + /// A scalar value equal to the number of + /// Wei to be paid per unit of gas for all computation + /// costs incurred as a result of the execution of this transaction; formally Tp. + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + pub gas_price: u128, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + pub gas_limit: u64, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + pub to: TxKind, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; formally Tv. + pub value: U256, + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). pub init: An unlimited size byte array specifying the + /// EVM-code for the account initialisation procedure CREATE, + /// data: An unlimited size byte array specifying the + /// input data of the message call, formally Td. + pub input: Bytes, +} + +impl TxLegacy { + /// The EIP-2718 transaction type. + pub const TX_TYPE: isize = 0; + + /// Calculates a heuristic for the in-memory size of the [TxLegacy] transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::>() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_price + mem::size_of::() + // gas_limit + self.to.size() + // to + mem::size_of::() + // value + self.input.len() // input + } + + /// Outputs the length of the transaction's fields, without a RLP header or length of the + /// eip155 fields. + pub(crate) fn fields_len(&self) -> usize { + let mut len = 0; + len += self.nonce.length(); + len += self.gas_price.length(); + len += self.gas_limit.length(); + len += self.to.length(); + len += self.value.length(); + len += self.input.0.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header or + /// eip155 fields. + pub(crate) fn encode_fields(&self, out: &mut dyn BufMut) { + self.nonce.encode(out); + self.gas_price.encode(out); + self.gas_limit.encode(out); + self.to.encode(out); + self.value.encode(out); + self.input.0.encode(out); + } + + /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating + /// hash. + pub fn encode_with_signature(&self, signature: &Signature, out: &mut dyn alloy_rlp::BufMut) { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + let header = Header { + list: true, + payload_length, + }; + header.encode(out); + self.encode_fields(out); + signature.write_rlp_vrs(out); + } + + /// Output the length of the RLP signed transaction encoding. + pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + // 'header length' + 'payload length' + length_of_length(payload_length) + payload_length + } + + /// Encodes EIP-155 arguments into the desired buffer. Only encodes values + /// for legacy transactions. + pub(crate) fn encode_eip155_signing_fields(&self, out: &mut dyn BufMut) { + // if this is a legacy transaction without a chain ID, it must be pre-EIP-155 + // and does not need to encode the chain ID for the signature hash encoding + if let Some(id) = self.chain_id { + // EIP-155 encodes the chain ID and two zeroes + id.encode(out); + 0x00u8.encode(out); + 0x00u8.encode(out); + } + } + + /// Outputs the length of EIP-155 fields. Only outputs a non-zero value for EIP-155 legacy + /// transactions. + pub(crate) fn eip155_fields_len(&self) -> usize { + if let Some(id) = self.chain_id { + // EIP-155 encodes the chain ID and two zeroes, so we add 2 to the length of the chain + // ID to get the length of all 3 fields + // len(chain_id) + (0x00) + (0x00) + id.length() + 2 + } else { + // this is either a pre-EIP-155 legacy transaction or a typed transaction + 0 + } + } + + /// Decode the RLP fields of the transaction, without decoding an RLP + /// header. + pub(crate) fn decode_fields(data: &mut &[u8]) -> Result { + Ok(TxLegacy { + nonce: Decodable::decode(data)?, + gas_price: Decodable::decode(data)?, + gas_limit: Decodable::decode(data)?, + to: Decodable::decode(data)?, + value: Decodable::decode(data)?, + input: Decodable::decode(data)?, + chain_id: None, + }) + } +} + +impl Encodable for TxLegacy { + fn encode(&self, out: &mut dyn BufMut) { + self.encode_for_signing(out) + } + + fn length(&self) -> usize { + let payload_length = self.fields_len() + self.eip155_fields_len(); + // 'header length' + 'payload length' + length_of_length(payload_length) + payload_length + } +} + +impl Decodable for TxLegacy { + fn decode(data: &mut &[u8]) -> Result { + let header = Header::decode(data)?; + let remaining_len = data.len(); + + let transaction_payload_len = header.payload_length; + + if transaction_payload_len > remaining_len { + return Err(alloy_rlp::Error::InputTooShort); + } + + let mut transaction = Self::decode_fields(data)?; + + // If we still have data, it should be an eip-155 encoded chain_id + if !data.is_empty() { + transaction.chain_id = Some(Decodable::decode(data)?); + let _: U256 = Decodable::decode(data)?; // r + let _: U256 = Decodable::decode(data)?; // s + } + + let decoded = remaining_len - data.len(); + if decoded != transaction_payload_len { + return Err(alloy_rlp::Error::UnexpectedLength); + } + + Ok(transaction) + } +} + +impl Transaction for TxLegacy { + type Signature = Signature; + // type Receipt = ReceiptWithBloom; + + fn encode_for_signing(&self, out: &mut dyn BufMut) { + Header { + list: true, + payload_length: self.fields_len() + self.eip155_fields_len(), + } + .encode(out); + self.encode_fields(out); + self.encode_eip155_signing_fields(out); + } + + fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len() + self.eip155_fields_len(); + // 'header length' + 'payload length' + length_of_length(payload_length) + payload_length + } + + fn into_signed(self, signature: Signature) -> Signed { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + let mut buf = Vec::with_capacity(payload_length); + self.encode_with_signature(&signature, &mut buf); + let hash = keccak256(&buf); + Signed::new_unchecked(self, signature, hash) + } + + fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) { + self.encode_with_signature(signature, out); + } + + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { + let header = Header::decode(buf)?; + if !header.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + let mut tx = Self::decode_fields(buf)?; + + let signature = Signature::decode_rlp_vrs(buf)?; + + let v = signature.v(); + + tx.chain_id = v.chain_id(); + + Ok(tx.into_signed(signature)) + } + + fn input(&self) -> &[u8] { + &self.input + } + + fn input_mut(&mut self) -> &mut Bytes { + &mut self.input + } + + fn set_input(&mut self, data: Bytes) { + self.input = data; + } + + fn to(&self) -> TxKind { + self.to + } + + fn set_to(&mut self, to: TxKind) { + self.to = to; + } + + fn value(&self) -> U256 { + self.value + } + + fn set_value(&mut self, value: U256) { + self.value = value; + } + + fn chain_id(&self) -> Option { + self.chain_id + } + + fn set_chain_id(&mut self, chain_id: ChainId) { + self.chain_id = Some(chain_id); + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn set_nonce(&mut self, nonce: u64) { + self.nonce = nonce; + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn set_gas_limit(&mut self, gas_limit: u64) { + self.gas_limit = gas_limit; + } + + fn gas_price(&self) -> Option { + Some(U256::from(self.gas_price)) + } + + fn set_gas_price(&mut self, price: U256) { + if let Ok(price) = price.try_into() { + self.gas_price = price; + } + } +} diff --git a/crates/derive/src/types/transaction/mod.rs b/crates/derive/src/types/transaction/mod.rs new file mode 100644 index 000000000..5b782d821 --- /dev/null +++ b/crates/derive/src/types/transaction/mod.rs @@ -0,0 +1,14 @@ +mod eip1559; +pub use eip1559::TxEip1559; + +mod eip2930; +pub use eip2930::TxEip2930; + +mod legacy; +pub use legacy::TxLegacy; + +mod eip4844; +pub use eip4844::TxEip4844; + +mod envelope; +pub use envelope::{TxEnvelope, TxType};