diff --git a/Cargo.lock b/Cargo.lock index 9e1d23300918..76c2229869bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8351,11 +8351,21 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", + "arbitrary", "bytes", "derive_more 1.0.0", + "modular-bitfield", "op-alloy-consensus", + "proptest", + "proptest-arbitrary-interop", + "rand 0.8.5", + "reth-codecs", "reth-primitives", "reth-primitives-traits", + "revm-primitives", + "secp256k1", + "serde", + "zstd", ] [[package]] diff --git a/crates/optimism/bin/Cargo.toml b/crates/optimism/bin/Cargo.toml index 77166763100a..6267c71179ba 100644 --- a/crates/optimism/bin/Cargo.toml +++ b/crates/optimism/bin/Cargo.toml @@ -44,7 +44,8 @@ optimism = [ "reth-optimism-evm/optimism", "reth-optimism-payload-builder/optimism", "reth-optimism-rpc/optimism", - "reth-provider/optimism" + "reth-provider/optimism", + "reth-optimism-primitives/optimism", ] dev = [ diff --git a/crates/optimism/cli/Cargo.toml b/crates/optimism/cli/Cargo.toml index 198e5377ec4d..20bd1c6a14ab 100644 --- a/crates/optimism/cli/Cargo.toml +++ b/crates/optimism/cli/Cargo.toml @@ -94,6 +94,7 @@ optimism = [ "reth-execution-types/optimism", "reth-db/optimism", "reth-db-api/optimism", + "reth-optimism-primitives/optimism", "reth-downloaders/optimism" ] asm-keccak = [ diff --git a/crates/optimism/primitives/Cargo.toml b/crates/optimism/primitives/Cargo.toml index bc11c3585046..e5a05ef10f5a 100644 --- a/crates/optimism/primitives/Cargo.toml +++ b/crates/optimism/primitives/Cargo.toml @@ -12,12 +12,77 @@ description = "OP primitive types" workspace = true [dependencies] +# reth reth-primitives.workspace = true reth-primitives-traits.workspace = true +reth-codecs = { workspace = true, optional = true } + +# ethereum alloy-primitives.workspace = true alloy-consensus.workspace = true -op-alloy-consensus.workspace = true -alloy-eips.workspace = true alloy-rlp.workspace = true -derive_more.workspace = true +alloy-eips.workspace = true +revm-primitives.workspace = true +secp256k1.workspace = true + +# op +op-alloy-consensus.workspace = true + +# codec +modular-bitfield = { workspace = true, optional = true } +zstd = { workspace = true, optional = true } bytes.workspace = true + +# io +serde.workspace = true + +# misc +derive_more = { workspace = true, features = ["deref", "from", "constructor"] } +proptest = { workspace = true, optional = true } +rand = { workspace = true, optional = true } + +# test-util +arbitrary = { workspace = true, features = ["derive"], optional = true } + +[dev-dependencies] +proptest-arbitrary-interop.workspace = true + +[features] +default = ["std", "reth-codec"] +std = [ + "reth-primitives/std", + "reth-primitives-traits/std", + "alloy-consensus/std", + "alloy-eips/std", + "alloy-primitives/std", + "revm-primitives/std", + "serde/std", + "secp256k1/std", +] +reth-codec = [ + "dep:reth-codecs", + "dep:zstd", + "dep:modular-bitfield", + "std", + "rand", + "dep:proptest", + "dep:arbitrary", +] +arbitrary = [ + "reth-codec", + "dep:proptest", + "alloy-eips/arbitrary", + "rand", + "reth-primitives-traits/arbitrary", + "reth-primitives/arbitrary", + "alloy-consensus/arbitrary", + "alloy-primitives/arbitrary", + "op-alloy-consensus/arbitrary", + "reth-codecs/arbitrary", + "revm-primitives/arbitrary", +] +optimism = [ + "reth-primitives/optimism", + "reth-codecs/optimism", + "revm-primitives/optimism", +] diff --git a/crates/optimism/primitives/src/lib.rs b/crates/optimism/primitives/src/lib.rs index f8d8e511498b..f49946996f12 100644 --- a/crates/optimism/primitives/src/lib.rs +++ b/crates/optimism/primitives/src/lib.rs @@ -6,6 +6,11 @@ issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/" )] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +// The `optimism` feature must be enabled to use this crate. +#![cfg(feature = "optimism")] pub mod bedrock; pub mod op_tx_type; +pub mod signed_transaction; + +pub use signed_transaction::OpTransactionSigned; diff --git a/crates/optimism/primitives/src/op_tx_type.rs b/crates/optimism/primitives/src/op_tx_type.rs index b317bb05c9c5..303d62d754fb 100644 --- a/crates/optimism/primitives/src/op_tx_type.rs +++ b/crates/optimism/primitives/src/op_tx_type.rs @@ -2,18 +2,19 @@ //! `OpTxType` implements `reth_primitives_traits::TxType`. //! This type is required because a `Compact` impl is needed on the deposit tx type. +use core::fmt::Debug; + use alloy_primitives::{U64, U8}; use alloy_rlp::{Decodable, Encodable, Error}; use bytes::BufMut; -use core::fmt::Debug; use derive_more::{ derive::{From, Into}, Display, }; use op_alloy_consensus::OpTxType as AlloyOpTxType; -use std::convert::TryFrom; -/// Wrapper type for `AlloyOpTxType` to implement `TxType` trait. +/// Wrapper type for [`op_alloy_consensus::OpTxType`] to implement +/// [`TxType`](reth_primitives_traits::TxType) trait. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Display, Ord, Hash, From, Into)] #[into(u8)] pub struct OpTxType(AlloyOpTxType); diff --git a/crates/optimism/primitives/src/signed_transaction.rs b/crates/optimism/primitives/src/signed_transaction.rs new file mode 100644 index 000000000000..219c111454c2 --- /dev/null +++ b/crates/optimism/primitives/src/signed_transaction.rs @@ -0,0 +1,330 @@ +//! A signed Optimism transaction. + +use alloy_consensus::{ + transaction::RlpEcdsaTx, SignableTransaction, TxEip1559, TxEip2930, TxEip7702, +}; +use alloy_eips::eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718}; +use alloy_primitives::{keccak256, Address, PrimitiveSignature as Signature, TxHash, B256, U256}; +use alloy_rlp::Header; +use derive_more::{AsRef, Constructor, Deref}; +use op_alloy_consensus::{OpTxType, OpTypedTransaction, TxDeposit}; +use reth_primitives::{ + transaction::{recover_signer, recover_signer_unchecked}, + TransactionSigned, +}; +use reth_primitives_traits::SignedTransaction; +use revm_primitives::{AuthorizationList, TxEnv}; +use serde::{Deserialize, Serialize}; + +/// Signed transaction. +#[cfg_attr(any(test, feature = "reth-codec"), reth_codecs::add_arbitrary_tests(rlp))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, Deref, Serialize, Deserialize, Constructor)] +pub struct OpTransactionSigned { + /// Transaction hash + pub hash: TxHash, + /// The transaction signature values + pub signature: Signature, + /// Raw transaction info + #[deref] + #[as_ref] + pub transaction: OpTypedTransaction, /* todo: replace with https://github.com/paradigmxyz/reth/issues/12473 */ +} + +impl SignedTransaction for OpTransactionSigned { + type Transaction = OpTypedTransaction; + + fn tx_hash(&self) -> &TxHash { + &self.hash + } + + fn transaction(&self) -> &Self::Transaction { + &self.transaction + } + + fn signature(&self) -> &Signature { + &self.signature + } + + fn recover_signer(&self) -> Option
{ + // Optimism's Deposit transaction does not have a signature. Directly return the + // `from` address. + if let OpTypedTransaction::Deposit(TxDeposit { from, .. }) = self.transaction { + return Some(from) + } + + let Self { transaction, signature, .. } = self; + let signature_hash = signature_hash(transaction); + recover_signer(signature, signature_hash) + } + + fn recover_signer_unchecked(&self) -> Option
{ + // Optimism's Deposit transaction does not have a signature. Directly return the + // `from` address. + if let OpTypedTransaction::Deposit(TxDeposit { from, .. }) = self.transaction { + return Some(from) + } + + let Self { transaction, signature, .. } = self; + let signature_hash = signature_hash(transaction); + recover_signer_unchecked(signature, signature_hash) + } + + fn from_transaction_and_signature( + transaction: Self::Transaction, + signature: Signature, + ) -> Self { + let mut initial_tx = Self { transaction, hash: Default::default(), signature }; + initial_tx.hash = initial_tx.recalculate_hash(); + initial_tx + } + + fn recalculate_hash(&self) -> B256 { + keccak256(self.encoded_2718()) + } + + fn fill_tx_env(&self, tx_env: &mut TxEnv, sender: Address) { + let envelope = self.encoded_2718(); + + tx_env.caller = sender; + match self.as_ref() { + OpTypedTransaction::Legacy(tx) => { + tx_env.gas_limit = tx.gas_limit; + tx_env.gas_price = U256::from(tx.gas_price); + tx_env.gas_priority_fee = None; + tx_env.transact_to = tx.to; + tx_env.value = tx.value; + tx_env.data = tx.input.clone(); + tx_env.chain_id = tx.chain_id; + tx_env.nonce = Some(tx.nonce); + tx_env.access_list.clear(); + tx_env.blob_hashes.clear(); + tx_env.max_fee_per_blob_gas.take(); + tx_env.authorization_list = None; + } + OpTypedTransaction::Eip2930(tx) => { + tx_env.gas_limit = tx.gas_limit; + tx_env.gas_price = U256::from(tx.gas_price); + tx_env.gas_priority_fee = None; + tx_env.transact_to = tx.to; + tx_env.value = tx.value; + tx_env.data = tx.input.clone(); + tx_env.chain_id = Some(tx.chain_id); + tx_env.nonce = Some(tx.nonce); + tx_env.access_list.clone_from(&tx.access_list.0); + tx_env.blob_hashes.clear(); + tx_env.max_fee_per_blob_gas.take(); + tx_env.authorization_list = None; + } + OpTypedTransaction::Eip1559(tx) => { + tx_env.gas_limit = tx.gas_limit; + tx_env.gas_price = U256::from(tx.max_fee_per_gas); + tx_env.gas_priority_fee = Some(U256::from(tx.max_priority_fee_per_gas)); + tx_env.transact_to = tx.to; + tx_env.value = tx.value; + tx_env.data = tx.input.clone(); + tx_env.chain_id = Some(tx.chain_id); + tx_env.nonce = Some(tx.nonce); + tx_env.access_list.clone_from(&tx.access_list.0); + tx_env.blob_hashes.clear(); + tx_env.max_fee_per_blob_gas.take(); + tx_env.authorization_list = None; + } + OpTypedTransaction::Eip7702(tx) => { + tx_env.gas_limit = tx.gas_limit; + tx_env.gas_price = U256::from(tx.max_fee_per_gas); + tx_env.gas_priority_fee = Some(U256::from(tx.max_priority_fee_per_gas)); + tx_env.transact_to = tx.to.into(); + tx_env.value = tx.value; + tx_env.data = tx.input.clone(); + tx_env.chain_id = Some(tx.chain_id); + tx_env.nonce = Some(tx.nonce); + tx_env.access_list.clone_from(&tx.access_list.0); + tx_env.blob_hashes.clear(); + tx_env.max_fee_per_blob_gas.take(); + tx_env.authorization_list = + Some(AuthorizationList::Signed(tx.authorization_list.clone())); + } + OpTypedTransaction::Deposit(tx) => { + tx_env.access_list.clear(); + tx_env.gas_limit = tx.gas_limit; + tx_env.gas_price = U256::ZERO; + tx_env.gas_priority_fee = None; + tx_env.transact_to = tx.to; + tx_env.value = tx.value; + tx_env.data = tx.input.clone(); + tx_env.chain_id = None; + tx_env.nonce = None; + tx_env.authorization_list = None; + + tx_env.optimism = revm_primitives::OptimismFields { + source_hash: Some(tx.source_hash), + mint: tx.mint, + is_system_transaction: Some(tx.is_system_transaction), + enveloped_tx: Some(envelope.into()), + }; + return + } + } + + tx_env.optimism = revm_primitives::OptimismFields { + source_hash: None, + mint: None, + is_system_transaction: Some(false), + enveloped_tx: Some(envelope.into()), + } + } +} + +impl alloy_rlp::Encodable for OpTransactionSigned { + /// See [`alloy_rlp::Encodable`] impl for [`TransactionSigned`]. + fn encode(&self, out: &mut dyn alloy_rlp::bytes::BufMut) { + self.network_encode(out); + } + + fn length(&self) -> usize { + let mut payload_length = self.encode_2718_len(); + if !self.is_legacy() { + payload_length += Header { list: false, payload_length }.length(); + } + + payload_length + } +} + +impl alloy_rlp::Decodable for OpTransactionSigned { + /// See [`alloy_rlp::Decodable`] impl for [`TransactionSigned`]. + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Self::network_decode(buf).map_err(Into::into) + } +} + +impl Encodable2718 for OpTransactionSigned { + fn type_flag(&self) -> Option { + match self.transaction.tx_type() { + OpTxType::Legacy => None, + tx_type => Some(tx_type as u8), + } + } + + fn encode_2718_len(&self) -> usize { + match &self.transaction { + OpTypedTransaction::Legacy(legacy_tx) => { + legacy_tx.eip2718_encoded_length(&self.signature) + } + OpTypedTransaction::Eip2930(access_list_tx) => { + access_list_tx.eip2718_encoded_length(&self.signature) + } + OpTypedTransaction::Eip1559(dynamic_fee_tx) => { + dynamic_fee_tx.eip2718_encoded_length(&self.signature) + } + OpTypedTransaction::Eip7702(set_code_tx) => { + set_code_tx.eip2718_encoded_length(&self.signature) + } + OpTypedTransaction::Deposit(deposit_tx) => deposit_tx.eip2718_encoded_length(), + } + } + + fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) { + let Self { transaction, signature, .. } = self; + + match transaction { + OpTypedTransaction::Legacy(legacy_tx) => { + // do nothing w/ with_header + legacy_tx.eip2718_encode(signature, out) + } + OpTypedTransaction::Eip2930(access_list_tx) => { + access_list_tx.eip2718_encode(signature, out) + } + OpTypedTransaction::Eip1559(dynamic_fee_tx) => { + dynamic_fee_tx.eip2718_encode(signature, out) + } + OpTypedTransaction::Eip7702(set_code_tx) => set_code_tx.eip2718_encode(signature, out), + OpTypedTransaction::Deposit(deposit_tx) => deposit_tx.eip2718_encode(out), + } + } +} + +impl Decodable2718 for OpTransactionSigned { + fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result { + match ty.try_into().map_err(|_| Eip2718Error::UnexpectedType(ty))? { + OpTxType::Legacy => Err(Eip2718Error::UnexpectedType(0)), + OpTxType::Eip2930 => { + let (tx, signature, hash) = TxEip2930::rlp_decode_signed(buf)?.into_parts(); + Ok(Self { transaction: OpTypedTransaction::Eip2930(tx), signature, hash }) + } + OpTxType::Eip1559 => { + let (tx, signature, hash) = TxEip1559::rlp_decode_signed(buf)?.into_parts(); + Ok(Self { transaction: OpTypedTransaction::Eip1559(tx), signature, hash }) + } + OpTxType::Eip7702 => { + let (tx, signature, hash) = TxEip7702::rlp_decode_signed(buf)?.into_parts(); + Ok(Self { transaction: OpTypedTransaction::Eip7702(tx), signature, hash }) + } + OpTxType::Deposit => Ok(Self::from_transaction_and_signature( + OpTypedTransaction::Deposit(TxDeposit::rlp_decode(buf)?), + TxDeposit::signature(), + )), + } + } + + fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result { + let (transaction, hash, signature) = + TransactionSigned::decode_rlp_legacy_transaction_tuple(buf)?; + Ok(Self::new(hash, signature, OpTypedTransaction::Legacy(transaction))) + } +} + +impl Default for OpTransactionSigned { + fn default() -> Self { + Self { + hash: Default::default(), + signature: Signature::test_signature(), + transaction: OpTypedTransaction::Legacy(Default::default()), + } + } +} + +#[cfg(any(test, feature = "arbitrary"))] +impl<'a> arbitrary::Arbitrary<'a> for OpTransactionSigned { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + #[allow(unused_mut)] + let mut transaction = OpTypedTransaction::arbitrary(u)?; + + let secp = secp256k1::Secp256k1::new(); + let key_pair = secp256k1::Keypair::new(&secp, &mut rand::thread_rng()); + let signature = reth_primitives::transaction::util::secp256k1::sign_message( + B256::from_slice(&key_pair.secret_bytes()[..]), + signature_hash(&transaction), + ) + .unwrap(); + + // Both `Some(0)` and `None` values are encoded as empty string byte. This introduces + // ambiguity in roundtrip tests. Patch the mint value of deposit transaction here, so that + // it's `None` if zero. + if let OpTypedTransaction::Deposit(ref mut tx_deposit) = transaction { + if tx_deposit.mint == Some(0) { + tx_deposit.mint = None; + } + } + + let signature = if is_deposit(&transaction) { TxDeposit::signature() } else { signature }; + + Ok(Self::from_transaction_and_signature(transaction, signature)) + } +} + +/// Calculates the signing hash for the transaction. +pub fn signature_hash(tx: &OpTypedTransaction) -> B256 { + match tx { + OpTypedTransaction::Legacy(tx) => tx.signature_hash(), + OpTypedTransaction::Eip2930(tx) => tx.signature_hash(), + OpTypedTransaction::Eip1559(tx) => tx.signature_hash(), + OpTypedTransaction::Eip7702(tx) => tx.signature_hash(), + OpTypedTransaction::Deposit(_) => B256::ZERO, + } +} + +/// Returns `true` if transaction is deposit transaction. +pub const fn is_deposit(tx: &OpTypedTransaction) -> bool { + matches!(tx, OpTypedTransaction::Deposit(_)) +} diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index f0caa2863aa6..c0d7dc7c4533 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -42,6 +42,11 @@ pub use signature::{recover_signer, recover_signer_unchecked}; pub use tx_type::TxType; pub use variant::TransactionSignedVariant; +/// Handling transaction signature operations, including signature recovery, +/// applying chain IDs, and EIP-2 validation. +pub mod signature; +pub mod util; + pub(crate) mod access_list; mod compat; mod error; @@ -49,12 +54,6 @@ mod meta; mod pooled; mod sidecar; mod tx_type; - -/// Handling transaction signature operations, including signature recovery, -/// applying chain IDs, and EIP-2 validation. -pub mod signature; - -pub(crate) mod util; mod variant; #[cfg(feature = "optimism")] @@ -1254,7 +1253,7 @@ impl TransactionSigned { /// /// Refer to the docs for [`Self::decode_rlp_legacy_transaction`] for details on the exact /// format expected. - pub(crate) fn decode_rlp_legacy_transaction_tuple( + pub fn decode_rlp_legacy_transaction_tuple( data: &mut &[u8], ) -> alloy_rlp::Result<(TxLegacy, TxHash, Signature)> { // keep this around, so we can use it to calculate the hash diff --git a/crates/primitives/src/transaction/util.rs b/crates/primitives/src/transaction/util.rs index 7964cc1c5f00..8eb1a639d965 100644 --- a/crates/primitives/src/transaction/util.rs +++ b/crates/primitives/src/transaction/util.rs @@ -1,7 +1,10 @@ +//! Utility functions for signature. + use alloy_primitives::{Address, PrimitiveSignature as Signature}; +/// Secp256k1 utility functions. #[cfg(feature = "secp256k1")] -pub(crate) mod secp256k1 { +pub mod secp256k1 { pub use super::impl_secp256k1::*; } diff --git a/crates/storage/provider/Cargo.toml b/crates/storage/provider/Cargo.toml index 04a0bf42908e..715225c9507d 100644 --- a/crates/storage/provider/Cargo.toml +++ b/crates/storage/provider/Cargo.toml @@ -90,7 +90,7 @@ alloy-consensus.workspace = true optimism = [ "reth-primitives/optimism", "reth-execution-types/optimism", - "reth-optimism-primitives", + "reth-optimism-primitives/optimism", "reth-codecs/optimism", "reth-db/optimism", "reth-db-api/optimism",