From a26253c77c2b1aac437b38318cca5a2eb51916a6 Mon Sep 17 00:00:00 2001 From: Emilia Hane Date: Wed, 2 Oct 2024 16:02:33 +0200 Subject: [PATCH 1/2] Checkout OpTransactionSigned from emhane/tx-signed --- Cargo.lock | 13 + crates/optimism/primitives/Cargo.toml | 49 +++ crates/optimism/primitives/src/lib.rs | 5 + .../primitives/src/signed_transaction.rs | 285 ++++++++++++++++++ 4 files changed, 352 insertions(+) create mode 100644 crates/optimism/primitives/src/signed_transaction.rs diff --git a/Cargo.lock b/Cargo.lock index d14e97f7a82a..b30ce28eea9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8186,9 +8186,22 @@ dependencies = [ name = "reth-optimism-primitives" version = "1.0.7" dependencies = [ + "alloy-consensus", + "alloy-eips", "alloy-primitives", + "alloy-rlp", + "arbitrary", + "derive_more 1.0.0", + "modular-bitfield", + "proptest", + "proptest-arbitrary-interop", + "rand 0.8.5", + "reth-codecs", + "reth-optimism-chainspec", "reth-primitives", "reth-primitives-traits", + "serde", + "zstd", ] [[package]] diff --git a/crates/optimism/primitives/Cargo.toml b/crates/optimism/primitives/Cargo.toml index 73a3bab1e445..896215d21ac6 100644 --- a/crates/optimism/primitives/Cargo.toml +++ b/crates/optimism/primitives/Cargo.toml @@ -12,6 +12,55 @@ description = "OP primitive types" workspace = true [dependencies] +# reth reth-primitives.workspace = true reth-primitives-traits.workspace = true +reth-codecs = { workspace = true, optional = true } + +# op-reth +reth-optimism-chainspec.workspace = true + +# ethereum alloy-primitives.workspace = true +alloy-consensus.workspace = true +alloy-rlp.workspace = true +alloy-eips.workspace = true + +# codecs +modular-bitfield = { workspace = true, optional = true } +zstd = { workspace = true, optional = true } +serde.workspace = true +arbitrary = { workspace = true, features = ["derive"], optional = true } + +# misc +derive_more = { workspace = true, features = ["deref"] } +proptest = { workspace = true, optional = true } +rand = { workspace = true, optional = true } + +[dev-dependencies] +proptest-arbitrary-interop.workspace = true + +[features] +default = ["std", "reth-codec"] +std = ["reth-primitives-traits/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", +] +optimism = [ + "reth-primitives/optimism", + "reth-codecs/optimism", +] \ No newline at end of file diff --git a/crates/optimism/primitives/src/lib.rs b/crates/optimism/primitives/src/lib.rs index 659900b9adbb..107b11fb2b2e 100644 --- a/crates/optimism/primitives/src/lib.rs +++ b/crates/optimism/primitives/src/lib.rs @@ -6,5 +6,10 @@ 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 signed_transaction; + +pub use signed_transaction::OpTransactionSigned; diff --git a/crates/optimism/primitives/src/signed_transaction.rs b/crates/optimism/primitives/src/signed_transaction.rs new file mode 100644 index 000000000000..666637e167b4 --- /dev/null +++ b/crates/optimism/primitives/src/signed_transaction.rs @@ -0,0 +1,285 @@ +//! A signed Optimism transaction. + +use alloy_consensus::{TxEip1559, TxEip2930, TxEip4844, TxEip7702}; +use alloy_eips::eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718}; +use alloy_primitives::{keccak256, Address, Parity, Signature, TxHash, B256}; +use alloy_rlp::{Buf, Decodable as _, Header}; +use derive_more::{AsRef, Deref}; +use reth_primitives::{ + optimism_deposit_tx_signature, + transaction::{recover_signer, recover_signer_unchecked, with_eip155_parity}, + SignedTransaction, Transaction, TxDeposit, TxType, +}; +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)] +pub struct OpTransactionSigned { + /// Transaction hash + pub hash: TxHash, + /// The transaction signature values + pub signature: Signature, + /// Raw transaction info + #[deref] + #[as_ref] + pub transaction: Transaction, +} + +impl SignedTransaction for OpTransactionSigned { + fn recover_signer(&self) -> Option
{ + // Optimism's Deposit transaction does not have a signature. Directly return the + // `from` address. + if let Transaction::Deposit(TxDeposit { from, .. }) = self.transaction { + return Some(from) + } + let signature_hash = self.signature_hash(); + recover_signer(&self.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 Transaction::Deposit(TxDeposit { from, .. }) = self.transaction { + return Some(from) + } + let signature_hash = self.signature_hash(); + recover_signer_unchecked(&self.signature, signature_hash) + } + + fn payload_len_inner(&self) -> usize { + match &self.transaction { + Transaction::Legacy(legacy_tx) => legacy_tx.encoded_len_with_signature( + &with_eip155_parity(&self.signature, legacy_tx.chain_id), + ), + Transaction::Eip2930(access_list_tx) => { + access_list_tx.encoded_len_with_signature(&self.signature, true) + } + Transaction::Eip1559(dynamic_fee_tx) => { + dynamic_fee_tx.encoded_len_with_signature(&self.signature, true) + } + Transaction::Eip4844(blob_tx) => { + blob_tx.encoded_len_with_signature(&self.signature, true) + } + Transaction::Eip7702(set_code_tx) => { + set_code_tx.encoded_len_with_signature(&self.signature, true) + } + Transaction::Deposit(deposit_tx) => deposit_tx.encoded_len(true), + } + } + + fn decode_enveloped_typed_transaction(data: &mut &[u8]) -> alloy_rlp::Result { + // keep this around so we can use it to calculate the hash + let original_encoding_without_header = *data; + + let tx_type = *data.first().ok_or(alloy_rlp::Error::InputTooShort)?; + data.advance(1); + + // decode the list header for the rest of the transaction + let header = Header::decode(data)?; + if !header.list { + return Err(alloy_rlp::Error::Custom("typed tx fields must be encoded as a list")) + } + + let remaining_len = data.len(); + + // length of tx encoding = tx type byte (size = 1) + length of header + payload length + let tx_length = 1 + header.length() + header.payload_length; + + // decode common fields + let Ok(tx_type) = TxType::try_from(tx_type) else { + return Err(alloy_rlp::Error::Custom("unsupported typed transaction type")) + }; + + let transaction = match tx_type { + TxType::Eip2930 => Transaction::Eip2930(TxEip2930::decode_fields(data)?), + TxType::Eip1559 => Transaction::Eip1559(TxEip1559::decode_fields(data)?), + TxType::Eip4844 => Transaction::Eip4844(TxEip4844::decode_fields(data)?), + TxType::Eip7702 => Transaction::Eip7702(TxEip7702::decode_fields(data)?), + + TxType::Deposit => Transaction::Deposit(TxDeposit::decode_fields(data)?), + TxType::Legacy => return Err(alloy_rlp::Error::Custom("unexpected legacy tx type")), + }; + + let signature = if tx_type == TxType::Deposit { + optimism_deposit_tx_signature() + } else { + Signature::decode_rlp_vrs(data)? + }; + + if !matches!(signature.v(), Parity::Parity(_)) { + return Err(alloy_rlp::Error::Custom("invalid parity for typed transaction")); + } + + let bytes_consumed = remaining_len - data.len(); + if bytes_consumed != header.payload_length { + return Err(alloy_rlp::Error::UnexpectedLength) + } + + let hash = keccak256(&original_encoding_without_header[..tx_length]); + let signed = Self { transaction, hash, signature }; + Ok(signed) + } + + fn length_without_header(&self) -> usize { + // method computes the payload len without a RLP header + match &self.transaction { + Transaction::Legacy(legacy_tx) => legacy_tx.encoded_len_with_signature( + &with_eip155_parity(&self.signature, legacy_tx.chain_id), + ), + Transaction::Eip2930(access_list_tx) => { + access_list_tx.encoded_len_with_signature(&self.signature, false) + } + Transaction::Eip1559(dynamic_fee_tx) => { + dynamic_fee_tx.encoded_len_with_signature(&self.signature, false) + } + Transaction::Eip4844(blob_tx) => { + blob_tx.encoded_len_with_signature(&self.signature, false) + } + Transaction::Eip7702(set_code_tx) => { + set_code_tx.encoded_len_with_signature(&self.signature, false) + } + Transaction::Deposit(deposit_tx) => deposit_tx.encoded_len(false), + } + } + + fn decode_rlp_legacy_transaction(data: &mut &[u8]) -> alloy_rlp::Result { + let (transaction, hash, signature) = Self::decode_rlp_legacy_transaction_tuple(data)?; + let signed = Self { transaction: Transaction::Legacy(transaction), hash, signature }; + Ok(signed) + } + + fn from_transaction_and_signature(transaction: 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()) + } +} + +impl alloy_rlp::Encodable for OpTransactionSigned { + /// See [`alloy_rlp::Encodable`] impl for + /// [`TransactionSigned`](reth_primitives::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`](reth_primitives::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() { + TxType::Legacy => None, + tx_type => Some(tx_type as u8), + } + } + + fn encode_2718_len(&self) -> usize { + match &self.transaction { + Transaction::Legacy(legacy_tx) => legacy_tx.encoded_len_with_signature( + &with_eip155_parity(&self.signature, legacy_tx.chain_id), + ), + Transaction::Eip2930(access_list_tx) => { + access_list_tx.encoded_len_with_signature(&self.signature, false) + } + Transaction::Eip1559(dynamic_fee_tx) => { + dynamic_fee_tx.encoded_len_with_signature(&self.signature, false) + } + Transaction::Eip4844(blob_tx) => { + blob_tx.encoded_len_with_signature(&self.signature, false) + } + Transaction::Eip7702(set_code_tx) => { + set_code_tx.encoded_len_with_signature(&self.signature, false) + } + Transaction::Deposit(deposit_tx) => deposit_tx.encoded_len(false), + } + } + + fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) { + self.transaction.encode_with_signature(&self.signature, out, false) + } +} + +impl Decodable2718 for OpTransactionSigned { + fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result { + match ty.try_into().map_err(|_| Eip2718Error::UnexpectedType(ty))? { + TxType::Legacy => Err(Eip2718Error::UnexpectedType(0)), + TxType::Eip2930 => { + let (tx, signature, hash) = TxEip2930::decode_signed_fields(buf)?.into_parts(); + Ok(Self { transaction: Transaction::Eip2930(tx), signature, hash }) + } + TxType::Eip1559 => { + let (tx, signature, hash) = TxEip1559::decode_signed_fields(buf)?.into_parts(); + Ok(Self { transaction: Transaction::Eip1559(tx), signature, hash }) + } + TxType::Eip7702 => { + let (tx, signature, hash) = TxEip7702::decode_signed_fields(buf)?.into_parts(); + Ok(Self { transaction: Transaction::Eip7702(tx), signature, hash }) + } + TxType::Eip4844 => { + let (tx, signature, hash) = TxEip4844::decode_signed_fields(buf)?.into_parts(); + Ok(Self { transaction: Transaction::Eip4844(tx), signature, hash }) + } + TxType::Deposit => Ok(Self::from_transaction_and_signature( + Transaction::Deposit(TxDeposit::decode(buf)?), + optimism_deposit_tx_signature(), + )), + } + } + + fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result { + Ok(Self::decode_rlp_legacy_transaction(buf)?) + } +} + +#[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 = Transaction::arbitrary(u)?; + let mut signature = Signature::arbitrary(u)?; + + signature = if matches!(transaction, Transaction::Legacy(_)) { + if let Some(chain_id) = transaction.chain_id() { + signature.with_chain_id(chain_id) + } else { + signature.with_parity(alloy_primitives::Parity::NonEip155(bool::arbitrary(u)?)) + } + } else { + signature.with_parity_bool() + }; + + // 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 Transaction::Deposit(ref mut tx_deposit) = transaction { + if tx_deposit.mint == Some(0) { + tx_deposit.mint = None; + } + } + + let signature = + if transaction.is_deposit() { optimism_deposit_tx_signature() } else { signature }; + + Ok(Self::from_transaction_and_signature(transaction, signature)) + } +} From d7b024215f54355cca217a865f0d44bfd30eb911 Mon Sep 17 00:00:00 2001 From: Emilia Hane Date: Wed, 2 Oct 2024 16:16:28 +0200 Subject: [PATCH 2/2] Fix export SignedTransaction --- crates/primitives/src/traits/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/primitives/src/traits/mod.rs b/crates/primitives/src/traits/mod.rs index 736f99cc881c..d7be18df91c5 100644 --- a/crates/primitives/src/traits/mod.rs +++ b/crates/primitives/src/traits/mod.rs @@ -1,9 +1,9 @@ //! Abstractions of primitive data types pub mod block; -pub mod signed; +pub mod transaction; -pub use signed::SignedTransaction; +pub use transaction::signed::SignedTransaction; pub use block::{body::BlockBody, Block}; pub use alloy_consensus::BlockHeader; \ No newline at end of file