diff --git a/Cargo.toml b/Cargo.toml index 62e730d2..fe695e2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,12 +14,19 @@ exclude = ["benches/", "tests/"] [workspace.dependencies] # Alloy -alloy-primitives = { version = "0.7.1", default-features = false } -alloy = { git = "https://github.com/alloy-rs/alloy", rev = "55a278c" } -alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "55a278c" } -alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "55a278c" } alloy-op-rpc-types = { version = "0.1.0", path = "crates/rpc-types" } +alloy-rlp = { version = "0.3", default-features = false } +alloy-primitives = { version = "0.7.1", default-features = false } + +alloy = { git = "https://github.com/alloy-rs/alloy" } +alloy-consensus = { git = "https://github.com/alloy-rs/alloy", default-features = false} +alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy" } +alloy-rpc-types-eth = { git = "https://github.com/alloy-rs/alloy" } +alloy-eips = { git = "https://github.com/alloy-rs/alloy", default-features = false } +alloy-serde = { git = "https://github.com/alloy-rs/alloy", default-features = false } +alloy-signer = { git = "https://github.com/alloy-rs/alloy", default-features = false } + # Serde serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } @@ -27,9 +34,15 @@ serde_json = { version = "1.0", default-features = false, features = ["alloc"] } ## misc-testing arbitrary = { version = "1.3", features = ["derive"] } rand = "0.8" +thiserror = "1.0" proptest = "1.4" proptest-derive = "0.4" +tokio = "1" + +## crypto +c-kzg = { version = "1.0", default-features = false } +k256 = { version = "0.13", default-features = false, features = ["ecdsa"] } [workspace.metadata.docs.rs] all-features = true -rustdoc-args = ["--cfg", "docsrs"] \ No newline at end of file +rustdoc-args = ["--cfg", "docsrs"] diff --git a/crates/op-consensus/Cargo.toml b/crates/op-consensus/Cargo.toml new file mode 100644 index 00000000..e8b36b73 --- /dev/null +++ b/crates/op-consensus/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "op-alloy-consensus" +description = "Optimism alloy consensus types" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[dependencies] +alloy-primitives = { workspace = true, features = ["rlp"] } +alloy-consensus.workspace = true +alloy-rlp.workspace = true +alloy-eips.workspace = true +alloy-serde = { workspace = true, optional = true } + +# arbitrary +arbitrary = { workspace = true, features = ["derive"], optional = true } + +# serde +serde = { workspace = true, features = ["derive"], optional = true } + +[dev-dependencies] +alloy-signer.workspace = true +arbitrary = { workspace = true, features = ["derive"] } +tokio = { workspace = true, features = ["macros"] } +serde_json.workspace = true + +[features] +default = ["std"] +std = ["alloy-eips/std", "alloy-consensus/std"] +k256 = ["alloy-primitives/k256", "alloy-consensus/k256"] +kzg = ["alloy-eips/kzg", "alloy-consensus/kzg", "std"] +arbitrary = ["std", "dep:arbitrary", "alloy-consensus/arbitrary", "alloy-eips/arbitrary", "alloy-primitives/rand"] +serde = ["dep:serde", "dep:alloy-serde", "alloy-primitives/serde", "alloy-consensus/serde", "alloy-eips/serde"] diff --git a/crates/op-consensus/README.md b/crates/op-consensus/README.md new file mode 100644 index 00000000..0f8e4079 --- /dev/null +++ b/crates/op-consensus/README.md @@ -0,0 +1,18 @@ +# op-alloy-consensus + +OP Stack consensus interface. + +This crate contains constants, types, and functions for implementing Optimism EL consensus and communication. This +includes an extended `OpTxEnvelope` type with [deposit transactions][deposit], and receipts containing OP Stack +specific fields (`deposit_nonce` + `deposit_receipt_version`). + +In general a type belongs in this crate if it exists in the `alloy-consensus` crate, but was modified from the base Ethereum protocol in the OP Stack. +For consensus types that are not modified by the OP Stack, the `alloy-consensus` types should be used instead. + +[deposit]: https://specs.optimism.io/protocol/deposits.html + +## Provenance + +Much of this code was ported from [reth-primitives] as part of ongoing alloy migrations. + +[reth-primitives]: https://github.com/paradigmxyz/reth/tree/main/crates/primitives diff --git a/crates/op-consensus/src/lib.rs b/crates/op-consensus/src/lib.rs new file mode 100644 index 00000000..a21aad3a --- /dev/null +++ b/crates/op-consensus/src/lib.rs @@ -0,0 +1,26 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(not(feature = "std"))] +extern crate alloc; + +mod receipt; +pub use receipt::{OpDepositReceipt, OpDepositReceiptWithBloom, OpReceiptEnvelope, OpTxReceipt}; + +mod transaction; +pub use transaction::{OpTxEnvelope, OpTxType, OpTypedTransaction, TxDeposit, DEPOSIT_TX_TYPE_ID}; diff --git a/crates/op-consensus/src/receipt/envelope.rs b/crates/op-consensus/src/receipt/envelope.rs new file mode 100644 index 00000000..edb8d1f1 --- /dev/null +++ b/crates/op-consensus/src/receipt/envelope.rs @@ -0,0 +1,236 @@ +use crate::{OpDepositReceipt, OpDepositReceiptWithBloom, OpTxType}; +use alloy_consensus::{Receipt, ReceiptWithBloom}; +use alloy_eips::eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718}; +use alloy_primitives::{Bloom, Log}; +use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable}; + +/// Receipt envelope, as defined in [EIP-2718], modified for OP Stack chains. +/// +/// This enum distinguishes between tagged and untagged legacy receipts, 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. +/// +/// Transaction receipt payloads are specified in their respective EIPs. +/// +/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(tag = "type"))] +#[non_exhaustive] +pub enum OpReceiptEnvelope { + /// Receipt envelope with no type flag. + #[cfg_attr(feature = "serde", serde(rename = "0x0", alias = "0x00"))] + Legacy(ReceiptWithBloom), + /// Receipt envelope with type flag 1, containing a [EIP-2930] receipt. + /// + /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + #[cfg_attr(feature = "serde", serde(rename = "0x1", alias = "0x01"))] + Eip2930(ReceiptWithBloom), + /// Receipt envelope with type flag 2, containing a [EIP-1559] receipt. + /// + /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + #[cfg_attr(feature = "serde", serde(rename = "0x2", alias = "0x02"))] + Eip1559(ReceiptWithBloom), + /// Receipt envelope with type flag 2, containing a [EIP-4844] receipt. + /// + /// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + #[cfg_attr(feature = "serde", serde(rename = "0x3", alias = "0x03"))] + Eip4844(ReceiptWithBloom), + /// Receipt envelope with type flag 126, containing a [deposit] receipt. + /// + /// [deposit]: https://specs.optimism.io/protocol/deposits.html + #[cfg_attr(feature = "serde", serde(rename = "0x7E", alias = "0x7E"))] + Deposit(OpDepositReceiptWithBloom), +} + +impl OpReceiptEnvelope { + /// Return the [`OpTxType`] of the inner receipt. + pub const fn tx_type(&self) -> OpTxType { + match self { + Self::Legacy(_) => OpTxType::Legacy, + Self::Eip2930(_) => OpTxType::Eip2930, + Self::Eip1559(_) => OpTxType::Eip1559, + Self::Eip4844(_) => OpTxType::Eip4844, + Self::Deposit(_) => OpTxType::Deposit, + } + } + + /// Return true if the transaction was successful. + pub fn is_success(&self) -> bool { + self.status() + } + + /// Returns the success status of the receipt's transaction. + pub fn status(&self) -> bool { + self.as_receipt().unwrap().status.coerce_status() + } + + /// Returns the cumulative gas used at this receipt. + pub fn cumulative_gas_used(&self) -> u128 { + self.as_receipt().unwrap().cumulative_gas_used + } + + /// Return the receipt logs. + pub fn logs(&self) -> &[T] { + &self.as_receipt().unwrap().logs + } + + /// Return the receipt's bloom. + pub const fn logs_bloom(&self) -> &Bloom { + match self { + Self::Legacy(t) => &t.logs_bloom, + Self::Eip2930(t) => &t.logs_bloom, + Self::Eip1559(t) => &t.logs_bloom, + Self::Eip4844(t) => &t.logs_bloom, + Self::Deposit(t) => &t.logs_bloom, + } + } + + /// Return the receipt's deposit_nonce if it is a deposit receipt. + pub fn deposit_nonce(&self) -> Option { + self.as_deposit_receipt().and_then(|r| r.deposit_nonce) + } + + /// Return the receipt's deposit version if it is a deposit receipt. + pub fn deposit_receipt_version(&self) -> Option { + self.as_deposit_receipt().and_then(|r| r.deposit_receipt_version) + } + + /// Returns the deposit receipt if it is a deposit receipt. + pub const fn as_deposit_receipt_with_bloom(&self) -> Option<&OpDepositReceiptWithBloom> { + match self { + Self::Deposit(t) => Some(t), + _ => None, + } + } + + /// Returns the deposit receipt if it is a deposit receipt. + pub const fn as_deposit_receipt(&self) -> Option<&OpDepositReceipt> { + match self { + Self::Deposit(t) => Some(&t.receipt), + _ => None, + } + } + + /// Return the inner receipt. Currently this is infallible, however, future + /// receipt types may be added. + pub const fn as_receipt(&self) -> Option<&Receipt> { + match self { + Self::Legacy(t) | Self::Eip2930(t) | Self::Eip1559(t) | Self::Eip4844(t) => { + Some(&t.receipt) + } + Self::Deposit(t) => Some(&t.receipt.inner), + } + } +} + +impl OpReceiptEnvelope { + /// Get the length of the inner receipt in the 2718 encoding. + pub fn inner_length(&self) -> usize { + match self { + Self::Legacy(t) => t.length(), + Self::Eip2930(t) => t.length(), + Self::Eip1559(t) => t.length(), + Self::Eip4844(t) => t.length(), + Self::Deposit(t) => t.length(), + } + } + + /// Calculate the length of the rlp payload of the network encoded receipt. + pub fn rlp_payload_length(&self) -> usize { + let length = self.inner_length(); + match self { + Self::Legacy(_) => length, + _ => length + 1, + } + } +} + +impl Encodable for OpReceiptEnvelope { + 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 OpReceiptEnvelope { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + match Self::network_decode(buf) { + Ok(t) => Ok(t), + Err(_) => Err(alloy_rlp::Error::Custom("Unexpected type")), + } + } +} + +impl Encodable2718 for OpReceiptEnvelope { + fn type_flag(&self) -> Option { + match self { + Self::Legacy(_) => None, + Self::Eip2930(_) => Some(OpTxType::Eip2930 as u8), + Self::Eip1559(_) => Some(OpTxType::Eip1559 as u8), + Self::Eip4844(_) => Some(OpTxType::Eip4844 as u8), + Self::Deposit(_) => Some(OpTxType::Deposit as u8), + } + } + + fn encode_2718_len(&self) -> usize { + self.inner_length() + !self.is_legacy() as usize + } + + fn encode_2718(&self, out: &mut dyn BufMut) { + match self.type_flag() { + None => {} + Some(ty) => out.put_u8(ty), + } + match self { + Self::Deposit(t) => t.encode(out), + OpReceiptEnvelope::Legacy(t) + | OpReceiptEnvelope::Eip2930(t) + | OpReceiptEnvelope::Eip1559(t) + | OpReceiptEnvelope::Eip4844(t) => t.encode(out), + } + } +} + +impl Decodable2718 for OpReceiptEnvelope { + fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result { + match ty.try_into().map_err(|_| Eip2718Error::UnexpectedType(ty))? { + OpTxType::Legacy => { + Err(alloy_rlp::Error::Custom("type-0 eip2718 transactions are not supported") + .into()) + } + OpTxType::Eip1559 => Ok(Self::Eip1559(Decodable::decode(buf)?)), + OpTxType::Eip2930 => Ok(Self::Eip2930(Decodable::decode(buf)?)), + OpTxType::Eip4844 => Ok(Self::Eip4844(Decodable::decode(buf)?)), + OpTxType::Deposit => Ok(Self::Deposit(Decodable::decode(buf)?)), + } + } + + fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result { + Ok(Self::Legacy(Decodable::decode(buf)?)) + } +} + +#[cfg(all(test, feature = "arbitrary"))] +impl<'a, T> arbitrary::Arbitrary<'a> for OpReceiptEnvelope +where + T: arbitrary::Arbitrary<'a>, +{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + match u.int_in_range(0..=4)? { + 0 => Ok(Self::Legacy(ReceiptWithBloom::::arbitrary(u)?)), + 1 => Ok(Self::Eip2930(ReceiptWithBloom::::arbitrary(u)?)), + 2 => Ok(Self::Eip1559(ReceiptWithBloom::::arbitrary(u)?)), + 3 => Ok(Self::Eip4844(ReceiptWithBloom::::arbitrary(u)?)), + _ => Ok(Self::Deposit(OpDepositReceiptWithBloom::::arbitrary(u)?)), + } + } +} diff --git a/crates/op-consensus/src/receipt/mod.rs b/crates/op-consensus/src/receipt/mod.rs new file mode 100644 index 00000000..e58c5bdf --- /dev/null +++ b/crates/op-consensus/src/receipt/mod.rs @@ -0,0 +1,179 @@ +use alloy_consensus::TxReceipt; + +mod envelope; +pub use envelope::OpReceiptEnvelope; + +mod receipts; +pub use receipts::{OpDepositReceipt, OpDepositReceiptWithBloom}; + +/// Receipt is the result of a transaction execution. +pub trait OpTxReceipt: TxReceipt { + /// Returns the deposit nonce of the transaction. + fn deposit_nonce(&self) -> Option; + + /// Returns the deposit receipt version of the transaction. + fn deposit_receipt_version(&self) -> Option; +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_consensus::{Receipt, ReceiptWithBloom}; + use alloy_eips::eip2718::Encodable2718; + use alloy_primitives::{address, b256, bytes, hex, Bytes, Log, LogData}; + use alloy_rlp::{Decodable, Encodable}; + + #[cfg(not(feature = "std"))] + use alloc::{vec, vec::Vec}; + + // Test vector from: https://eips.ethereum.org/EIPS/eip-2481 + #[test] + fn encode_legacy_receipt() { + let expected = hex!("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"); + + let mut data = vec![]; + let receipt = + OpReceiptEnvelope::Legacy(ReceiptWithBloom { + receipt: Receipt { + status: false.into(), + cumulative_gas_used: 0x1u128, + logs: vec![Log { + address: address!("0000000000000000000000000000000000000011"), + data: LogData::new_unchecked( + vec![ + b256!("000000000000000000000000000000000000000000000000000000000000dead"), + b256!("000000000000000000000000000000000000000000000000000000000000beef"), + ], + bytes!("0100ff"), + ), + }], + }, + logs_bloom: [0; 256].into(), + }); + + receipt.network_encode(&mut data); + + // check that the rlp length equals the length of the expected rlp + assert_eq!(receipt.length(), expected.len()); + assert_eq!(data, expected); + } + + // Test vector from: https://eips.ethereum.org/EIPS/eip-2481 + #[test] + fn decode_legacy_receipt() { + let data = hex!("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"); + + // EIP658Receipt + let expected = + OpDepositReceiptWithBloom { + receipt: OpDepositReceipt { + inner: Receipt { + status: false.into(), + cumulative_gas_used: 0x1u128, + logs: vec![Log { + address: address!("0000000000000000000000000000000000000011"), + data: LogData::new_unchecked( + vec![ + b256!("000000000000000000000000000000000000000000000000000000000000dead"), + b256!("000000000000000000000000000000000000000000000000000000000000beef"), + ], + bytes!("0100ff"), + ), + }], + }, + deposit_nonce: None, + deposit_receipt_version: None, + }, + logs_bloom: [0; 256].into(), + }; + + let receipt = OpDepositReceiptWithBloom::decode(&mut &data[..]).unwrap(); + assert_eq!(receipt, expected); + } + + #[test] + fn gigantic_receipt() { + let receipt = OpDepositReceipt { + inner: Receipt { + cumulative_gas_used: 16747627, + status: true.into(), + logs: vec![ + Log { + address: address!("4bf56695415f725e43c3e04354b604bcfb6dfb6e"), + data: LogData::new_unchecked( + vec![b256!( + "c69dc3d7ebff79e41f525be431d5cd3cc08f80eaf0f7819054a726eeb7086eb9" + )], + Bytes::from(vec![1; 0xffffff]), + ), + }, + Log { + address: address!("faca325c86bf9c2d5b413cd7b90b209be92229c2"), + data: LogData::new_unchecked( + vec![b256!( + "8cca58667b1e9ffa004720ac99a3d61a138181963b294d270d91c53d36402ae2" + )], + Bytes::from(vec![1; 0xffffff]), + ), + }, + ], + }, + deposit_nonce: None, + deposit_receipt_version: None, + } + .with_bloom(); + + let mut data = vec![]; + + receipt.encode(&mut data); + let decoded = OpDepositReceiptWithBloom::decode(&mut &data[..]).unwrap(); + + // receipt.clone().to_compact(&mut data); + // let (decoded, _) = Receipt::from_compact(&data[..], data.len()); + assert_eq!(decoded, receipt); + } + + #[test] + fn regolith_receipt_roundtrip() { + let data = hex!("f9010c0182b741b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0833d3bbf"); + + // Deposit Receipt (post-regolith) + let expected = OpDepositReceiptWithBloom { + receipt: OpDepositReceipt { + inner: Receipt { cumulative_gas_used: 46913, logs: vec![], status: true.into() }, + deposit_nonce: Some(4012991), + deposit_receipt_version: None, + }, + logs_bloom: [0; 256].into(), + }; + + let receipt = OpDepositReceiptWithBloom::decode(&mut &data[..]).unwrap(); + assert_eq!(receipt, expected); + + let mut buf = Vec::new(); + receipt.encode(&mut buf); + assert_eq!(buf, &data[..]); + } + + #[test] + fn post_canyon_receipt_roundtrip() { + let data = hex!("f9010d0182b741b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0833d3bbf01"); + + // Deposit Receipt (post-regolith) + let expected = OpDepositReceiptWithBloom { + receipt: OpDepositReceipt { + inner: Receipt { cumulative_gas_used: 46913, logs: vec![], status: true.into() }, + deposit_nonce: Some(4012991), + deposit_receipt_version: Some(1), + }, + logs_bloom: [0; 256].into(), + }; + + let receipt = OpDepositReceiptWithBloom::decode(&mut &data[..]).unwrap(); + assert_eq!(receipt, expected); + + let mut buf = Vec::new(); + expected.encode(&mut buf); + assert_eq!(buf, &data[..]); + } +} diff --git a/crates/op-consensus/src/receipt/receipts.rs b/crates/op-consensus/src/receipt/receipts.rs new file mode 100644 index 00000000..08243ab3 --- /dev/null +++ b/crates/op-consensus/src/receipt/receipts.rs @@ -0,0 +1,277 @@ +use super::OpTxReceipt; +use alloy_consensus::{Eip658Value, Receipt, TxReceipt}; +use alloy_primitives::{Bloom, Log}; +use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable}; + +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; + +/// Receipt containing result of transaction execution. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct OpDepositReceipt { + /// The inner receipt type. + #[cfg_attr(feature = "serde", serde(flatten))] + pub inner: Receipt, + /// Deposit nonce for Optimism deposit transactions + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub deposit_nonce: Option, + /// Deposit receipt version for Optimism deposit transactions + /// + /// The deposit receipt version was introduced in Canyon to indicate an update to how + /// receipt hashes should be computed when set. The state transition process + /// ensures this is only set for post-Canyon deposit transactions. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub deposit_receipt_version: Option, +} + +impl OpDepositReceipt { + /// Calculates [`Log`]'s bloom filter. this is slow operation and [OpDepositReceiptWithBloom] + /// can be used to cache this value. + pub fn bloom_slow(&self) -> Bloom { + self.inner.logs.iter().collect() + } + + /// Calculates the bloom filter for the receipt and returns the [OpDepositReceiptWithBloom] + /// container type. + pub fn with_bloom(self) -> OpDepositReceiptWithBloom { + self.into() + } +} + +impl AsRef> for OpDepositReceipt { + fn as_ref(&self) -> &Receipt { + &self.inner + } +} + +impl TxReceipt for OpDepositReceipt { + fn status_or_post_state(&self) -> &Eip658Value { + self.inner.status_or_post_state() + } + + fn status(&self) -> bool { + self.inner.status() + } + + fn bloom(&self) -> Bloom { + self.bloom_slow() + } + + fn cumulative_gas_used(&self) -> u128 { + self.inner.cumulative_gas_used + } + + fn logs(&self) -> &[Log] { + &self.inner.logs + } +} + +impl OpTxReceipt for OpDepositReceipt { + fn deposit_nonce(&self) -> Option { + self.deposit_nonce + } + + fn deposit_receipt_version(&self) -> Option { + self.deposit_receipt_version + } +} + +/// [`OpDepositReceipt`] with calculated bloom filter, modified for the OP Stack. +/// +/// This convenience type allows us to lazily calculate the bloom filter for a +/// receipt, similar to [`Sealed`]. +/// +/// [`Sealed`]: alloy_consensus::Sealed +#[derive(Clone, Debug, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct OpDepositReceiptWithBloom { + #[cfg_attr(feature = "serde", serde(flatten))] + /// The receipt. + pub receipt: OpDepositReceipt, + /// The bloom filter. + pub logs_bloom: Bloom, +} + +impl TxReceipt for OpDepositReceiptWithBloom { + fn status_or_post_state(&self) -> &Eip658Value { + self.receipt.status_or_post_state() + } + + fn status(&self) -> bool { + self.receipt.status() + } + + fn bloom(&self) -> Bloom { + self.logs_bloom + } + + fn bloom_cheap(&self) -> Option { + Some(self.logs_bloom) + } + + fn cumulative_gas_used(&self) -> u128 { + self.receipt.inner.cumulative_gas_used + } + + fn logs(&self) -> &[Log] { + &self.receipt.inner.logs + } +} + +impl OpTxReceipt for OpDepositReceiptWithBloom { + fn deposit_nonce(&self) -> Option { + self.receipt.deposit_nonce + } + + fn deposit_receipt_version(&self) -> Option { + self.receipt.deposit_receipt_version + } +} + +impl From for OpDepositReceiptWithBloom { + fn from(receipt: OpDepositReceipt) -> Self { + let bloom = receipt.bloom_slow(); + OpDepositReceiptWithBloom { receipt, logs_bloom: bloom } + } +} + +impl OpDepositReceiptWithBloom { + /// Create new [OpDepositReceiptWithBloom] + pub const fn new(receipt: OpDepositReceipt, bloom: Bloom) -> Self { + Self { receipt, logs_bloom: bloom } + } + + /// Consume the structure, returning only the receipt + #[allow(clippy::missing_const_for_fn)] // false positive + pub fn into_receipt(self) -> OpDepositReceipt { + 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) -> (OpDepositReceipt, Bloom) { + (self.receipt, self.logs_bloom) + } + + fn payload_len(&self) -> usize { + self.receipt.inner.status.length() + + self.receipt.inner.cumulative_gas_used.length() + + self.logs_bloom.length() + + self.receipt.inner.logs.length() + + self.receipt.deposit_nonce.map_or(0, |nonce| nonce.length()) + + self.receipt.deposit_receipt_version.map_or(0, |version| version.length()) + } + + /// 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.inner.status.encode(out); + self.receipt.inner.cumulative_gas_used.encode(out); + self.logs_bloom.encode(out); + self.receipt.inner.logs.encode(out); + if let Some(nonce) = self.receipt.deposit_nonce { + nonce.encode(out); + } + if let Some(version) = self.receipt.deposit_receipt_version { + version.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 remaining = |b: &[u8]| rlp_head.payload_length - (started_len - b.len()) > 0; + let deposit_nonce = remaining(b).then(|| alloy_rlp::Decodable::decode(b)).transpose()?; + let deposit_receipt_version = + remaining(b).then(|| alloy_rlp::Decodable::decode(b)).transpose()?; + + let receipt = OpDepositReceipt { + inner: Receipt { status: success, cumulative_gas_used, logs }, + deposit_nonce, + deposit_receipt_version, + }; + + let this = Self { receipt, logs_bloom: 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 OpDepositReceiptWithBloom { + fn encode(&self, out: &mut dyn BufMut) { + self.encode_fields(out); + } + + fn length(&self) -> usize { + let payload_length = self.receipt.inner.status.length() + + self.receipt.inner.cumulative_gas_used.length() + + self.logs_bloom.length() + + self.receipt.inner.logs.length() + + self.receipt.deposit_nonce.map_or(0, |nonce| nonce.length()) + + self.receipt.deposit_receipt_version.map_or(0, |version| version.length()); + payload_length + length_of_length(payload_length) + } +} + +impl alloy_rlp::Decodable for OpDepositReceiptWithBloom { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Self::decode_receipt(buf) + } +} + +#[cfg(all(test, feature = "arbitrary"))] +impl<'a, T> arbitrary::Arbitrary<'a> for OpDepositReceipt +where + T: arbitrary::Arbitrary<'a>, +{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let deposit_nonce = Option::::arbitrary(u)?; + let deposit_receipt_version = + deposit_nonce.is_some().then(|| u64::arbitrary(u)).transpose()?; + Ok(Self { + inner: Receipt { + status: Eip658Value::arbitrary(u)?, + cumulative_gas_used: u128::arbitrary(u)?, + logs: Vec::::arbitrary(u)?, + }, + deposit_nonce, + deposit_receipt_version, + }) + } +} + +#[cfg(all(test, feature = "arbitrary"))] +impl<'a, T> arbitrary::Arbitrary<'a> for OpDepositReceiptWithBloom +where + T: arbitrary::Arbitrary<'a>, +{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + Ok(Self { receipt: OpDepositReceipt::::arbitrary(u)?, logs_bloom: Bloom::arbitrary(u)? }) + } +} diff --git a/crates/op-consensus/src/transaction/envelope.rs b/crates/op-consensus/src/transaction/envelope.rs new file mode 100644 index 00000000..bf4023be --- /dev/null +++ b/crates/op-consensus/src/transaction/envelope.rs @@ -0,0 +1,340 @@ +use crate::TxDeposit; +use alloy_consensus::{ + Signed, TxEip1559, TxEip2930, TxEip4844, TxEip4844Variant, TxEip4844WithSidecar, TxLegacy, +}; +use alloy_eips::eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718}; +use alloy_rlp::{Decodable, Encodable, Header}; + +/// Identifier for an Optimism deposit transaction +pub const DEPOSIT_TX_TYPE_ID: u8 = 126; // 0x7E + +/// Optimism `TransactionType` flags as specified in EIPs [2718], [1559], and +/// [2930], as well as the [deposit transaction spec][deposit-spec] +/// +/// [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 +/// [deposit-spec]: https://specs.optimism.io/protocol/deposits.html +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] +pub enum OpTxType { + /// Legacy transaction type. + Legacy = 0, + /// EIP-2930 transaction type. + Eip2930 = 1, + /// EIP-1559 transaction type. + Eip1559 = 2, + /// EIP-4844 transaction type. + Eip4844 = 3, + /// Optimism Deposit transaction type. + Deposit = 126, +} + +#[cfg(any(test, feature = "arbitrary"))] +impl<'a> arbitrary::Arbitrary<'a> for OpTxType { + fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { + Ok(match u.int_in_range(0..=3)? { + 0 => OpTxType::Legacy, + 1 => OpTxType::Eip2930, + 2 => OpTxType::Eip1559, + 3 => OpTxType::Eip4844, + 126 => OpTxType::Deposit, + _ => unreachable!(), + }) + } +} + +impl TryFrom for OpTxType { + type Error = Eip2718Error; + + fn try_from(value: u8) -> Result { + Ok(match value { + 0 => Self::Legacy, + 1 => Self::Eip2930, + 2 => Self::Eip1559, + 3 => Self::Eip4844, + 126 => Self::Deposit, + _ => return Err(Eip2718Error::UnexpectedType(value)), + }) + } +} + +/// The Ethereum [EIP-2718] Transaction Envelope, modified for OP Stack chains. +/// +/// # 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)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(tag = "type"))] +#[non_exhaustive] +pub enum OpTxEnvelope { + /// An untagged [`TxLegacy`]. + #[cfg_attr(feature = "serde", serde(rename = "0x0", alias = "0x00"))] + Legacy(Signed), + /// A [`TxEip2930`] tagged with type 1. + #[cfg_attr(feature = "serde", serde(rename = "0x1", alias = "0x01"))] + Eip2930(Signed), + /// A [`TxEip1559`] tagged with type 2. + #[cfg_attr(feature = "serde", serde(rename = "0x2", alias = "0x02"))] + Eip1559(Signed), + /// A TxEip4844 tagged with type 3. + /// An EIP-4844 transaction has two network representations: + /// 1 - The transaction itself, which is a regular RLP-encoded transaction and used to retrieve + /// historical transactions.. + /// + /// 2 - The transaction with a sidecar, which is the form used to + /// send transactions to the network. + #[cfg_attr(feature = "serde", serde(rename = "0x3", alias = "0x03"))] + Eip4844(Signed), + /// A [`TxDeposit`] tagged with type 0x7E. + #[cfg_attr(feature = "serde", serde(rename = "0x7E", alias = "0x7E"))] + Deposit(TxDeposit), +} + +impl From> for OpTxEnvelope { + fn from(v: Signed) -> Self { + Self::Legacy(v) + } +} + +impl From> for OpTxEnvelope { + fn from(v: Signed) -> Self { + Self::Eip2930(v) + } +} + +impl From> for OpTxEnvelope { + fn from(v: Signed) -> Self { + Self::Eip1559(v) + } +} + +impl From> for OpTxEnvelope { + fn from(v: Signed) -> Self { + Self::Eip4844(v) + } +} + +impl From> for OpTxEnvelope { + fn from(v: Signed) -> Self { + let (tx, signature, hash) = v.into_parts(); + Self::Eip4844(Signed::new_unchecked(TxEip4844Variant::TxEip4844(tx), signature, hash)) + } +} + +impl From> for OpTxEnvelope { + fn from(v: Signed) -> Self { + let (tx, signature, hash) = v.into_parts(); + Self::Eip4844(Signed::new_unchecked( + TxEip4844Variant::TxEip4844WithSidecar(tx), + signature, + hash, + )) + } +} + +impl From for OpTxEnvelope { + fn from(v: TxDeposit) -> Self { + Self::Deposit(v) + } +} + +impl OpTxEnvelope { + /// Return the [`OpTxType`] of the inner txn. + pub const fn tx_type(&self) -> OpTxType { + match self { + Self::Legacy(_) => OpTxType::Legacy, + Self::Eip2930(_) => OpTxType::Eip2930, + Self::Eip1559(_) => OpTxType::Eip1559, + Self::Eip4844(_) => OpTxType::Eip4844, + Self::Deposit(_) => OpTxType::Deposit, + } + } + + /// Return the length of the inner txn, __without a type byte__. + pub fn inner_length(&self) -> usize { + match self { + Self::Legacy(t) => t.tx().fields_len() + t.signature().rlp_vrs_len(), + Self::Eip2930(t) => { + let payload_length = t.tx().fields_len() + t.signature().rlp_vrs_len(); + Header { list: true, payload_length }.length() + payload_length + } + Self::Eip1559(t) => { + let payload_length = t.tx().fields_len() + t.signature().rlp_vrs_len(); + Header { list: true, payload_length }.length() + payload_length + } + Self::Eip4844(t) => match t.tx() { + TxEip4844Variant::TxEip4844(tx) => { + let payload_length = tx.fields_len() + t.signature().rlp_vrs_len(); + Header { list: true, payload_length }.length() + payload_length + } + TxEip4844Variant::TxEip4844WithSidecar(tx) => { + let inner_payload_length = tx.tx().fields_len() + t.signature().rlp_vrs_len(); + let inner_header = Header { list: true, payload_length: inner_payload_length }; + + let outer_payload_length = + inner_header.length() + inner_payload_length + tx.sidecar.fields_len(); + let outer_header = Header { list: true, payload_length: outer_payload_length }; + + outer_header.length() + outer_payload_length + } + }, + Self::Deposit(t) => { + let payload_length = t.fields_len(); + Header { list: true, payload_length }.length() + payload_length + } + } + } + + /// Return the RLP payload length of the network-serialized wrapper + fn rlp_payload_length(&self) -> usize { + if let Self::Legacy(t) = self { + let payload_length = t.tx().fields_len() + t.signature().rlp_vrs_len(); + return Header { list: true, payload_length }.length() + payload_length; + } + // length of inner tx body + let inner_length = self.inner_length(); + // with tx type byte + inner_length + 1 + } +} + +impl Encodable for OpTxEnvelope { + 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 += Header { list: false, payload_length }.length(); + } + + payload_length + } +} + +impl Decodable for OpTxEnvelope { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + match Self::network_decode(buf) { + Ok(t) => Ok(t), + Err(Eip2718Error::RlpError(e)) => Err(e), + Err(Eip2718Error::UnexpectedType(_)) => { + Err(alloy_rlp::Error::Custom("unexpected tx type")) + } + _ => Err(alloy_rlp::Error::Custom("unknown error decoding tx envelope")), + } + } +} + +impl Decodable2718 for OpTxEnvelope { + fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result { + match ty.try_into().map_err(|_| Eip2718Error::UnexpectedType(ty))? { + OpTxType::Eip2930 => Ok(Self::Eip2930(TxEip2930::decode_signed_fields(buf)?)), + OpTxType::Eip1559 => Ok(Self::Eip1559(TxEip1559::decode_signed_fields(buf)?)), + OpTxType::Eip4844 => Ok(Self::Eip4844(TxEip4844Variant::decode_signed_fields(buf)?)), + OpTxType::Deposit => Ok(Self::Deposit(TxDeposit::decode(buf)?)), + OpTxType::Legacy => { + Err(alloy_rlp::Error::Custom("type-0 eip2718 transactions are not supported") + .into()) + } + } + } + + fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result { + Ok(OpTxEnvelope::Legacy(TxLegacy::decode_signed_fields(buf)?)) + } +} + +impl Encodable2718 for OpTxEnvelope { + fn type_flag(&self) -> Option { + match self { + Self::Legacy(_) => None, + Self::Eip2930(_) => Some(OpTxType::Eip2930 as u8), + Self::Eip1559(_) => Some(OpTxType::Eip1559 as u8), + Self::Eip4844(_) => Some(OpTxType::Eip4844 as u8), + Self::Deposit(_) => Some(OpTxType::Deposit 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 { + // Legacy transactions have no difference between network and 2718 + OpTxEnvelope::Legacy(tx) => tx.tx().encode_with_signature_fields(tx.signature(), out), + OpTxEnvelope::Eip2930(tx) => { + tx.tx().encode_with_signature(tx.signature(), out, false); + } + OpTxEnvelope::Eip1559(tx) => { + tx.tx().encode_with_signature(tx.signature(), out, false); + } + OpTxEnvelope::Eip4844(tx) => { + tx.tx().encode_with_signature(tx.signature(), out, false); + } + OpTxEnvelope::Deposit(tx) => { + out.put_u8(OpTxType::Deposit as u8); + tx.encode(out); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{Address, Bytes, TxKind, B256, U256}; + + #[cfg(not(feature = "std"))] + use alloc::vec; + + #[test] + fn test_encode_decode_deposit() { + let tx = TxDeposit { + source_hash: B256::left_padding_from(&[0xde, 0xad]), + from: Address::left_padding_from(&[0xbe, 0xef]), + mint: Some(1), + gas_limit: 2, + to: TxKind::Call(Address::left_padding_from(&[3])), + value: U256::from(4_u64), + input: Bytes::from(vec![5]), + is_system_transaction: false, + }; + let tx_envelope = OpTxEnvelope::Deposit(tx); + let encoded = tx_envelope.encoded_2718(); + let decoded = OpTxEnvelope::decode_2718(&mut encoded.as_ref()).unwrap(); + assert_eq!(encoded.len(), tx_envelope.encode_2718_len()); + assert_eq!(decoded, tx_envelope); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_roundtrip_deposit() { + let tx = TxDeposit { + gas_limit: u128::MAX, + to: TxKind::Call(Address::random()), + value: U256::MAX, + input: Bytes::new(), + source_hash: U256::MAX.into(), + from: Address::random(), + mint: Some(u128::MAX), + is_system_transaction: false, + }; + let tx_envelope = OpTxEnvelope::Deposit(tx); + + let serialized = serde_json::to_string(&tx_envelope).unwrap(); + let deserialized: OpTxEnvelope = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(tx_envelope, deserialized); + } +} diff --git a/crates/op-consensus/src/transaction/mod.rs b/crates/op-consensus/src/transaction/mod.rs new file mode 100644 index 00000000..0806e9cc --- /dev/null +++ b/crates/op-consensus/src/transaction/mod.rs @@ -0,0 +1,8 @@ +mod optimism; +pub use optimism::TxDeposit; + +mod envelope; +pub use envelope::{OpTxEnvelope, OpTxType, DEPOSIT_TX_TYPE_ID}; + +mod typed; +pub use typed::OpTypedTransaction; diff --git a/crates/op-consensus/src/transaction/optimism.rs b/crates/op-consensus/src/transaction/optimism.rs new file mode 100644 index 00000000..e04a4c37 --- /dev/null +++ b/crates/op-consensus/src/transaction/optimism.rs @@ -0,0 +1,240 @@ +use alloy_consensus::Transaction; +use alloy_primitives::{Address, Bytes, ChainId, TxKind, B256, U256}; +use alloy_rlp::{ + Buf, BufMut, Decodable, Encodable, Error as DecodeError, Header, EMPTY_STRING_CODE, +}; +use core::mem; + +/// Deposit transactions, also known as deposits are initiated on L1, and executed on L2. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct TxDeposit { + /// Hash that uniquely identifies the source of the deposit. + pub source_hash: B256, + /// The address of the sender account. + pub from: Address, + /// The address of the recipient account, or the null (zero-length) address if the deposited + /// transaction is a contract creation. + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "TxKind::is_create"))] + pub to: TxKind, + /// The ETH value to mint on L2. + #[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::u128_opt_via_ruint"))] + pub mint: Option, + /// The ETH value to send to the recipient account. + pub value: U256, + /// The gas limit for the L2 transaction. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_via_ruint"))] + pub gas_limit: u128, + /// Field indicating if this transaction is exempt from the L2 gas limit. + pub is_system_transaction: bool, + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). + pub input: Bytes, +} + +impl TxDeposit { + /// Decodes the inner [TxDeposit] 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: + /// + /// - `source_hash` + /// - `from` + /// - `to` + /// - `mint` + /// - `value` + /// - `gas_limit` + /// - `is_system_transaction` + /// - `input` + pub(crate) fn decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + source_hash: Decodable::decode(buf)?, + from: Decodable::decode(buf)?, + to: Decodable::decode(buf)?, + mint: if *buf.first().ok_or(DecodeError::InputTooShort)? == EMPTY_STRING_CODE { + buf.advance(1); + None + } else { + Some(Decodable::decode(buf)?) + }, + value: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + is_system_transaction: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + }) + } + + /// 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 { + self.source_hash.length() + + self.from.length() + + self.to.length() + + self.mint.map_or(1, |mint| mint.length()) + + self.value.length() + + self.gas_limit.length() + + self.is_system_transaction.length() + + self.input.0.length() + } + + /// 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.source_hash.encode(out); + self.from.encode(out); + self.to.encode(out); + if let Some(mint) = self.mint { + mint.encode(out); + } else { + out.put_u8(EMPTY_STRING_CODE); + } + self.value.encode(out); + self.gas_limit.encode(out); + self.is_system_transaction.encode(out); + self.input.encode(out); + } + + /// Calculates a heuristic for the in-memory size of the [TxDeposit] transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // source_hash + mem::size_of::
() + // from + self.to.size() + // to + mem::size_of::>() + // mint + mem::size_of::() + // value + mem::size_of::() + // gas_limit + mem::size_of::() + // is_system_transaction + self.input.len() // input + } +} + +impl Transaction for TxDeposit { + fn chain_id(&self) -> Option { + None + } + + fn nonce(&self) -> u64 { + 0u64 + } + + fn gas_limit(&self) -> u128 { + self.gas_limit + } + + fn gas_price(&self) -> Option { + None + } + + fn to(&self) -> TxKind { + self.to + } + + fn value(&self) -> U256 { + self.value + } + + fn input(&self) -> &[u8] { + &self.input + } +} + +impl Encodable for TxDeposit { + 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(); + Header { list: true, payload_length }.length() + payload_length + } +} + +impl Decodable for TxDeposit { + 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_fields(data) + } +} + +#[cfg(test)] +mod tests { + use super::*; + //use crate::TxEnvelope; + use alloy_primitives::hex; + use alloy_rlp::BytesMut; + + #[test] + fn test_rlp_roundtrip() { + let bytes = Bytes::from_static(&hex!("7ef9015aa044bae9d41b8380d781187b426c6fe43df5fb2fb57bd4466ef6a701e1f01e015694deaddeaddeaddeaddeaddeaddeaddeaddead000194420000000000000000000000000000000000001580808408f0d18001b90104015d8eb900000000000000000000000000000000000000000000000000000000008057650000000000000000000000000000000000000000000000000000000063d96d10000000000000000000000000000000000000000000000000000000000009f35273d89754a1e0387b89520d989d3be9c37c1f32495a88faf1ea05c61121ab0d1900000000000000000000000000000000000000000000000000000000000000010000000000000000000000002d679b567db6187c0c8323fa982cfb88b74dbcc7000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240")); + let tx_a = TxDeposit::decode(&mut bytes[1..].as_ref()).unwrap(); + let mut buf_a = BytesMut::default(); + tx_a.encode(&mut buf_a); + assert_eq!(&buf_a[..], &bytes[1..]); + } + + #[test] + fn test_encode_decode_fields() { + let original = TxDeposit { + source_hash: B256::default(), + from: Address::default(), + to: TxKind::default(), + mint: Some(100), + value: U256::default(), + gas_limit: 50000, + is_system_transaction: true, + input: Bytes::default(), + }; + + let mut buffer = BytesMut::new(); + original.encode_fields(&mut buffer); + let decoded = TxDeposit::decode_fields(&mut &buffer[..]).expect("Failed to decode"); + + assert_eq!(original, decoded); + } + + #[test] + fn test_encode_with_and_without_header() { + let tx_deposit = TxDeposit { + source_hash: B256::default(), + from: Address::default(), + to: TxKind::default(), + mint: Some(100), + value: U256::default(), + gas_limit: 50000, + is_system_transaction: true, + input: Bytes::default(), + }; + + let mut buffer_with_header = BytesMut::new(); + tx_deposit.encode(&mut buffer_with_header); + + let mut buffer_without_header = BytesMut::new(); + tx_deposit.encode_fields(&mut buffer_without_header); + + assert!(buffer_with_header.len() > buffer_without_header.len()); + } + + #[test] + fn test_payload_length() { + let tx_deposit = TxDeposit { + source_hash: B256::default(), + from: Address::default(), + to: TxKind::default(), + mint: Some(100), + value: U256::default(), + gas_limit: 50000, + is_system_transaction: true, + input: Bytes::default(), + }; + + assert!(tx_deposit.size() > tx_deposit.fields_len()); + } +} diff --git a/crates/op-consensus/src/transaction/typed.rs b/crates/op-consensus/src/transaction/typed.rs new file mode 100644 index 00000000..e595bbb3 --- /dev/null +++ b/crates/op-consensus/src/transaction/typed.rs @@ -0,0 +1,200 @@ +use crate::{OpTxEnvelope, OpTxType, TxDeposit}; +use alloy_consensus::{Transaction, TxEip1559, TxEip2930, TxEip4844Variant, TxLegacy}; +use alloy_primitives::TxKind; + +/// The TypedTransaction enum represents all Ethereum transaction request types, modified for the OP +/// Stack. +/// +/// Its variants correspond to specific allowed transactions: +/// 1. Legacy (pre-EIP2718) [`TxLegacy`] +/// 2. EIP2930 (state access lists) [`TxEip2930`] +/// 3. EIP1559 [`TxEip1559`] +/// 4. EIP4844 [`TxEip4844Variant`] +/// 4. Deposit [`TxDeposit`] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(tag = "type"))] +pub enum OpTypedTransaction { + /// Legacy transaction + #[cfg_attr(feature = "serde", serde(rename = "0x00", alias = "0x0"))] + Legacy(TxLegacy), + /// EIP-2930 transaction + #[cfg_attr(feature = "serde", serde(rename = "0x01", alias = "0x1"))] + Eip2930(TxEip2930), + /// EIP-1559 transaction + #[cfg_attr(feature = "serde", serde(rename = "0x02", alias = "0x2"))] + Eip1559(TxEip1559), + /// EIP-4844 transaction + #[cfg_attr(feature = "serde", serde(rename = "0x03", alias = "0x3"))] + Eip4844(TxEip4844Variant), + /// Optimism deposit transaction + #[cfg_attr(feature = "serde", serde(rename = "0x7E", alias = "0x7E"))] + Deposit(TxDeposit), +} + +impl From for OpTypedTransaction { + fn from(tx: TxLegacy) -> Self { + Self::Legacy(tx) + } +} + +impl From for OpTypedTransaction { + fn from(tx: TxEip2930) -> Self { + Self::Eip2930(tx) + } +} + +impl From for OpTypedTransaction { + fn from(tx: TxEip1559) -> Self { + Self::Eip1559(tx) + } +} + +impl From for OpTypedTransaction { + fn from(tx: TxEip4844Variant) -> Self { + Self::Eip4844(tx) + } +} + +impl From for OpTypedTransaction { + fn from(tx: TxDeposit) -> Self { + Self::Deposit(tx) + } +} + +impl From for OpTypedTransaction { + fn from(envelope: OpTxEnvelope) -> Self { + match envelope { + OpTxEnvelope::Legacy(tx) => Self::Legacy(tx.strip_signature()), + OpTxEnvelope::Eip2930(tx) => Self::Eip2930(tx.strip_signature()), + OpTxEnvelope::Eip1559(tx) => Self::Eip1559(tx.strip_signature()), + OpTxEnvelope::Eip4844(tx) => Self::Eip4844(tx.strip_signature()), + OpTxEnvelope::Deposit(tx) => Self::Deposit(tx), + } + } +} + +impl OpTypedTransaction { + /// Return the [`OpTxType`] of the inner txn. + pub const fn tx_type(&self) -> OpTxType { + match self { + Self::Legacy(_) => OpTxType::Legacy, + Self::Eip2930(_) => OpTxType::Eip2930, + Self::Eip1559(_) => OpTxType::Eip1559, + Self::Eip4844(_) => OpTxType::Eip4844, + Self::Deposit(_) => OpTxType::Deposit, + } + } + + /// Return the inner legacy transaction if it exists. + pub const fn legacy(&self) -> Option<&TxLegacy> { + match self { + Self::Legacy(tx) => Some(tx), + _ => None, + } + } + + /// Return the inner EIP-2930 transaction if it exists. + pub const fn eip2930(&self) -> Option<&TxEip2930> { + match self { + Self::Eip2930(tx) => Some(tx), + _ => None, + } + } + + /// Return the inner EIP-1559 transaction if it exists. + pub const fn eip1559(&self) -> Option<&TxEip1559> { + match self { + Self::Eip1559(tx) => Some(tx), + _ => None, + } + } + + /// Return the inner EIP-4844 transaction if it exists. + pub const fn eip4844(&self) -> Option<&TxEip4844Variant> { + match self { + Self::Eip4844(tx) => Some(tx), + _ => None, + } + } + + /// Return the inner deposit transaction if it exists. + pub const fn deposit(&self) -> Option<&TxDeposit> { + match self { + Self::Deposit(tx) => Some(tx), + _ => None, + } + } +} + +impl Transaction for OpTypedTransaction { + fn chain_id(&self) -> Option { + match self { + Self::Legacy(tx) => tx.chain_id(), + Self::Eip2930(tx) => tx.chain_id(), + Self::Eip1559(tx) => tx.chain_id(), + Self::Eip4844(tx) => tx.chain_id(), + Self::Deposit(tx) => tx.chain_id(), + } + } + + fn nonce(&self) -> u64 { + match self { + Self::Legacy(tx) => tx.nonce(), + Self::Eip2930(tx) => tx.nonce(), + Self::Eip1559(tx) => tx.nonce(), + Self::Eip4844(tx) => tx.nonce(), + Self::Deposit(tx) => tx.nonce(), + } + } + + fn gas_limit(&self) -> u128 { + match self { + Self::Legacy(tx) => tx.gas_limit(), + Self::Eip2930(tx) => tx.gas_limit(), + Self::Eip1559(tx) => tx.gas_limit(), + Self::Eip4844(tx) => tx.gas_limit(), + Self::Deposit(tx) => tx.gas_limit(), + } + } + + fn gas_price(&self) -> Option { + match self { + Self::Legacy(tx) => tx.gas_price(), + Self::Eip2930(tx) => tx.gas_price(), + Self::Eip1559(tx) => tx.gas_price(), + Self::Eip4844(tx) => tx.gas_price(), + Self::Deposit(tx) => tx.gas_price(), + } + } + + fn to(&self) -> TxKind { + match self { + Self::Legacy(tx) => tx.to(), + Self::Eip2930(tx) => tx.to(), + Self::Eip1559(tx) => tx.to(), + Self::Eip4844(tx) => tx.to(), + Self::Deposit(tx) => tx.to(), + } + } + + fn value(&self) -> alloy_primitives::U256 { + match self { + Self::Legacy(tx) => tx.value(), + Self::Eip2930(tx) => tx.value(), + Self::Eip1559(tx) => tx.value(), + Self::Eip4844(tx) => tx.value(), + Self::Deposit(tx) => tx.value(), + } + } + + fn input(&self) -> &[u8] { + match self { + Self::Legacy(tx) => tx.input(), + Self::Eip2930(tx) => tx.input(), + Self::Eip1559(tx) => tx.input(), + Self::Eip4844(tx) => tx.input(), + Self::Deposit(tx) => tx.input(), + } + } +} diff --git a/crates/rpc-types/Cargo.toml b/crates/rpc-types/Cargo.toml index 632666c7..f5f891a1 100644 --- a/crates/rpc-types/Cargo.toml +++ b/crates/rpc-types/Cargo.toml @@ -13,14 +13,12 @@ exclude.workspace = true [dependencies] alloy-primitives = { workspace = true, features = ["rlp", "serde", "std"] } -alloy-consensus = { workspace = true, features = ["std", "serde"] } -alloy-rpc-types.workspace = true +alloy-rpc-types-eth.workspace = true serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true # arbitrary -arbitrary = { version = "1.3", features = ["derive"], optional = true } +arbitrary = { workspace = true, features = ["derive"], optional = true } [dev-dependencies] alloy-primitives = { workspace = true, features = ["arbitrary"] } @@ -34,6 +32,5 @@ rand.workspace = true arbitrary = [ "dep:arbitrary", "alloy-primitives/arbitrary", - "alloy-consensus/arbitrary", - "alloy-rpc-types/arbitrary", + "alloy-rpc-types-eth/arbitrary", ] \ No newline at end of file diff --git a/crates/rpc-types/src/lib.rs b/crates/rpc-types/src/lib.rs index 2ee5bc67..17cf524e 100644 --- a/crates/rpc-types/src/lib.rs +++ b/crates/rpc-types/src/lib.rs @@ -13,5 +13,6 @@ )] #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] pub mod transaction; diff --git a/crates/rpc-types/src/transaction/mod.rs b/crates/rpc-types/src/transaction/mod.rs index d0b54253..c378b72c 100644 --- a/crates/rpc-types/src/transaction/mod.rs +++ b/crates/rpc-types/src/transaction/mod.rs @@ -3,10 +3,6 @@ use alloy_primitives::B256; use serde::{Deserialize, Serialize}; -pub use self::tx_type::TxType; - -pub mod tx_type; - /// OP Transaction type #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] @@ -14,7 +10,7 @@ pub mod tx_type; pub struct Transaction { /// Ethereum Transaction Types #[serde(flatten)] - pub inner: alloy_rpc_types::Transaction, + pub inner: alloy_rpc_types_eth::Transaction, /// The ETH value to mint on L2 #[serde(default, skip_serializing_if = "Option::is_none")] pub mint: Option, diff --git a/crates/rpc-types/src/transaction/tx_type.rs b/crates/rpc-types/src/transaction/tx_type.rs deleted file mode 100644 index 5c26fd93..00000000 --- a/crates/rpc-types/src/transaction/tx_type.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! OP transaction identifiers. - -use serde::{Deserialize, Serialize}; - -/// Identifier for legacy transaction, however a legacy tx is technically not -/// typed. -pub const LEGACY_TX_TYPE_ID: u8 = 0; - -/// Identifier for an EIP2930 transaction. -pub const EIP2930_TX_TYPE_ID: u8 = 1; - -/// Identifier for an EIP1559 transaction. -pub const EIP1559_TX_TYPE_ID: u8 = 2; - -/// Identifier for an EIP4844 transaction. -pub const EIP4844_TX_TYPE_ID: u8 = 3; - -/// Identifier for an Optimism deposit transaction -pub const DEPOSIT_TX_TYPE_ID: u8 = 126; - -/// Transaction Type -#[derive( - Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize, Hash, -)] -pub enum TxType { - /// Legacy transaction pre EIP-2929 - #[default] - Legacy = 0_isize, - /// AccessList transaction - Eip2930 = 1_isize, - /// Transaction with Priority fee - Eip1559 = 2_isize, - /// Shard Blob Transactions - EIP-4844 - Eip4844 = 3_isize, - /// Optimism Deposit transaction. - Deposit = 126_isize, -} - -impl From for u8 { - fn from(value: TxType) -> Self { - match value { - TxType::Legacy => LEGACY_TX_TYPE_ID, - TxType::Eip2930 => EIP2930_TX_TYPE_ID, - TxType::Eip1559 => EIP1559_TX_TYPE_ID, - TxType::Eip4844 => EIP4844_TX_TYPE_ID, - TxType::Deposit => DEPOSIT_TX_TYPE_ID, - } - } -} diff --git a/deny.toml b/deny.toml index 63556efa..59f9a5fb 100644 --- a/deny.toml +++ b/deny.toml @@ -52,6 +52,5 @@ license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] [sources] unknown-registry = "deny" unknown-git = "deny" -allow-git = [ - "https://github.com/alloy-rs/alloy", -] +# TODO: Remove `alloy-rs/core` once alloy-contract is stable. This is only used in tests for `sol!`. +allow-git = ["https://github.com/alloy-rs/core", "https://github.com/alloy-rs/alloy"]