diff --git a/Cargo.toml b/Cargo.toml index 7e3e413e..dc54720a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,26 +46,26 @@ op-alloy-rpc-jsonrpsee = { version = "0.5.2", path = "crates/rpc-jsonrpsee", def op-alloy-rpc-types-engine = { version = "0.5.2", path = "crates/rpc-types-engine", default-features = false } # Alloy -alloy-eips = { version = "0.5.4", default-features = false } -alloy-serde = { version = "0.5.4", default-features = false } -alloy-signer = { version = "0.5.4", default-features = false } -alloy-network = { version = "0.5.4", default-features = false } -alloy-provider = { version = "0.5.4", default-features = false } -alloy-transport = { version = "0.5.4", default-features = false } -alloy-consensus = { version = "0.5.4", default-features = false } -alloy-rpc-types-eth = { version = "0.5.4", default-features = false } -alloy-rpc-types-engine = { version = "0.5.4", default-features = false } -alloy-network-primitives = { version = "0.5.4", default-features = false } +alloy-eips = { version = "0.6.0", default-features = false } +alloy-serde = { version = "0.6.0", default-features = false } +alloy-signer = { version = "0.6.0", default-features = false } +alloy-network = { version = "0.6.0", default-features = false } +alloy-provider = { version = "0.6.0", default-features = false } +alloy-transport = { version = "0.6.0", default-features = false } +alloy-consensus = { version = "0.6.0", default-features = false } +alloy-rpc-types-eth = { version = "0.6.0", default-features = false } +alloy-rpc-types-engine = { version = "0.6.0", default-features = false } +alloy-network-primitives = { version = "0.6.0", default-features = false } # Alloy RLP alloy-rlp = { version = "0.3", default-features = false } # Alloy Core -alloy-sol-types = { version = "0.8", default-features = false } -alloy-primitives = { version = "0.8", default-features = false } +alloy-sol-types = { version = "0.8.11", default-features = false } +alloy-primitives = { version = "0.8.11", default-features = false } # Revm -revm = "17.1.0" +revm = "18.0.0" # Serde serde_repr = "0.1" diff --git a/crates/consensus/src/lib.rs b/crates/consensus/src/lib.rs index 99f03fdc..5b817e8f 100644 --- a/crates/consensus/src/lib.rs +++ b/crates/consensus/src/lib.rs @@ -25,6 +25,9 @@ pub use hardforks::Hardforks; mod block; pub use block::OpBlock; +#[cfg(feature = "serde")] +pub use transaction::serde_deposit_tx_rpc; + /// Bincode-compatible serde implementations for consensus types. /// /// `bincode` crate doesn't work well with optionally serializable serde fields, but some of the diff --git a/crates/consensus/src/transaction/deposit.rs b/crates/consensus/src/transaction/deposit.rs index ceb760c8..646216a3 100644 --- a/crates/consensus/src/transaction/deposit.rs +++ b/crates/consensus/src/transaction/deposit.rs @@ -4,7 +4,9 @@ use super::OpTxType; use crate::DepositTransaction; use alloy_consensus::Transaction; use alloy_eips::eip2930::AccessList; -use alloy_primitives::{Address, Bytes, ChainId, Parity, Signature, TxKind, B256, U256}; +use alloy_primitives::{ + Address, Bytes, ChainId, PrimitiveSignature as Signature, TxKind, B256, U256, +}; use alloy_rlp::{ Buf, BufMut, Decodable, Encodable, Error as DecodeError, Header, EMPTY_STRING_CODE, }; @@ -33,7 +35,15 @@ pub struct TxDeposit { #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity", rename = "gas"))] pub gas_limit: u64, /// Field indicating if this transaction is exempt from the L2 gas limit. - #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity", rename = "isSystemTx"))] + #[cfg_attr( + feature = "serde", + serde( + default, + with = "alloy_serde::quantity", + rename = "isSystemTx", + skip_serializing_if = "core::ops::Not::not" + ) + )] pub is_system_transaction: bool, /// Input has two uses depending if transaction is Create or Call (if `to` field is None or /// Some). @@ -72,7 +82,7 @@ impl TxDeposit { /// - `gas_limit` /// - `is_system_transaction` /// - `input` - pub fn decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result { + pub fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result { Ok(Self { source_hash: Decodable::decode(buf)?, from: Decodable::decode(buf)?, @@ -90,9 +100,30 @@ impl TxDeposit { }) } + /// Decodes the transaction from RLP bytes. + pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let header = Header::decode(buf)?; + if !header.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + let remaining = buf.len(); + + if header.payload_length > remaining { + return Err(alloy_rlp::Error::InputTooShort); + } + + let this = Self::rlp_decode_fields(buf)?; + + if buf.len() + header.payload_length != remaining { + return Err(alloy_rlp::Error::UnexpectedLength); + } + + Ok(this) + } + /// 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 { + pub(crate) fn rlp_encoded_fields_length(&self) -> usize { self.source_hash.length() + self.from.length() + self.to.length() @@ -105,7 +136,7 @@ impl TxDeposit { /// 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) { + pub(crate) fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) { self.source_hash.encode(out); self.from.encode(out); self.to.encode(out); @@ -138,46 +169,55 @@ impl TxDeposit { OpTxType::Deposit } - /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating - /// hash that for eip2718 does not require rlp header - pub fn encode_inner(&self, out: &mut dyn BufMut, with_header: bool) { - let payload_length = self.fields_len(); - if with_header { - Header { - list: false, - payload_length: 1 + Header { list: true, payload_length }.length() + payload_length, - } - .encode(out); - } + /// Create an rlp header for the transaction. + fn rlp_header(&self) -> Header { + Header { list: true, payload_length: self.rlp_encoded_fields_length() } + } + + /// RLP encodes the transaction. + pub fn rlp_encode(&self, out: &mut dyn BufMut) { + self.rlp_header().encode(out); + self.rlp_encode_fields(out); + } + + /// Get the length of the transaction when RLP encoded. + pub fn rlp_encoded_length(&self) -> usize { + self.rlp_header().length_with_payload() + } + + /// Get the length of the transaction when EIP-2718 encoded. This is the + /// 1 byte type flag + the length of the RLP encoded transaction. + pub fn eip2718_encoded_length(&self) -> usize { + self.rlp_encoded_length() + 1 + } + + /// EIP-2718 encode the transaction with the given signature and the default + /// type flag. + pub fn eip2718_encode(&self, out: &mut dyn BufMut) { out.put_u8(self.tx_type() as u8); - let header = Header { list: true, payload_length }; - header.encode(out); - self.encode_fields(out); + self.rlp_encode(out); } - /// Output the length of the RLP signed transaction encoding. - /// - /// If `with_header` is true, the length includes the RLP header. - pub fn encoded_len(&self, with_header: bool) -> usize { - // Count the length of the payload - let payload_length = self.fields_len(); - - // 'transaction type byte length' + 'header length' + 'payload length' - let inner_payload_length = - 1 + Header { list: true, payload_length }.length() + payload_length; - - if with_header { - Header { list: true, payload_length: inner_payload_length }.length() - + inner_payload_length - } else { - inner_payload_length - } + fn network_header(&self) -> Header { + Header { list: false, payload_length: self.eip2718_encoded_length() } + } + + /// Get the length of the transaction when network encoded. This is the + /// EIP-2718 encoded length with an outer RLP header. + pub fn network_encoded_length(&self) -> usize { + self.network_header().length_with_payload() + } + + /// Network encode the transaction with the given signature. + pub fn network_encode(&self, out: &mut dyn BufMut) { + self.network_header().encode(out); + self.eip2718_encode(out); } /// Returns the signature for the optimism deposit transactions, which don't include a /// signature. pub fn signature() -> Signature { - Signature::new(U256::ZERO, U256::ZERO, Parity::Parity(false)) + Signature::new(U256::ZERO, U256::ZERO, false) } } @@ -245,29 +285,45 @@ impl Transaction for TxDeposit { 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); + Header { list: true, payload_length: self.rlp_encoded_fields_length() }.encode(out); + self.rlp_encode_fields(out); } fn length(&self) -> usize { - let payload_length = self.fields_len(); + let payload_length = self.rlp_encoded_fields_length(); 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) + Self::rlp_decode(data) } } +/// Deposit transactions don't have a signature, however, we include an empty signature in the +/// response for better compatibility. +/// +/// This function can be used as `serialize_with` serde attribute for the [`TxDeposit`] and will +/// flatten [`TxDeposit::signature`] into response. +#[cfg(feature = "serde")] +pub fn serde_deposit_tx_rpc( + value: &TxDeposit, + serializer: S, +) -> Result { + use serde::Serialize; + + #[derive(Serialize)] + struct SerdeHelper<'a> { + #[serde(flatten)] + value: &'a TxDeposit, + #[serde(flatten)] + signature: Signature, + } + + SerdeHelper { value, signature: TxDeposit::signature() }.serialize(serializer) +} + #[cfg(test)] mod tests { use super::*; @@ -356,8 +412,8 @@ mod tests { }; let mut buffer = BytesMut::new(); - original.encode_fields(&mut buffer); - let decoded = TxDeposit::decode_fields(&mut &buffer[..]).expect("Failed to decode"); + original.rlp_encode_fields(&mut buffer); + let decoded = TxDeposit::rlp_decode_fields(&mut &buffer[..]).expect("Failed to decode"); assert_eq!(original, decoded); } @@ -379,7 +435,7 @@ mod tests { tx_deposit.encode(&mut buffer_with_header); let mut buffer_without_header = BytesMut::new(); - tx_deposit.encode_fields(&mut buffer_without_header); + tx_deposit.rlp_encode_fields(&mut buffer_without_header); assert!(buffer_with_header.len() > buffer_without_header.len()); } @@ -397,7 +453,7 @@ mod tests { input: Bytes::default(), }; - assert!(tx_deposit.size() > tx_deposit.fields_len()); + assert!(tx_deposit.size() > tx_deposit.rlp_encoded_fields_length()); } #[test] @@ -414,10 +470,10 @@ mod tests { }; let mut buffer_with_header = BytesMut::new(); - tx_deposit.encode_inner(&mut buffer_with_header, true); + tx_deposit.network_encode(&mut buffer_with_header); let mut buffer_without_header = BytesMut::new(); - tx_deposit.encode_inner(&mut buffer_without_header, false); + tx_deposit.eip2718_encode(&mut buffer_without_header); assert!(buffer_with_header.len() > buffer_without_header.len()); } @@ -435,8 +491,8 @@ mod tests { input: Bytes::default(), }; - let total_len = tx_deposit.encoded_len(true); - let len_without_header = tx_deposit.encoded_len(false); + let total_len = tx_deposit.network_encoded_length(); + let len_without_header = tx_deposit.eip2718_encoded_length(); assert!(total_len > len_without_header); } diff --git a/crates/consensus/src/transaction/envelope.rs b/crates/consensus/src/transaction/envelope.rs index b22356e5..3be2f508 100644 --- a/crates/consensus/src/transaction/envelope.rs +++ b/crates/consensus/src/transaction/envelope.rs @@ -1,11 +1,13 @@ -use alloy_consensus::{Signed, Transaction, TxEip1559, TxEip2930, TxEip7702, TxLegacy}; +use alloy_consensus::{ + transaction::RlpEcdsaTx, Signed, Transaction, TxEip1559, TxEip2930, TxEip7702, TxLegacy, +}; use alloy_eips::{ eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718}, eip2930::AccessList, eip7702::SignedAuthorization, }; use alloy_primitives::{Address, Bytes, TxKind, B256, U256}; -use alloy_rlp::{Decodable, Encodable, Header}; +use alloy_rlp::{Decodable, Encodable}; use derive_more::Display; use crate::TxDeposit; @@ -87,23 +89,22 @@ impl TryFrom for OpTxType { /// [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"))] +#[cfg_attr( + feature = "serde", + serde(into = "serde_from::TaggedTxEnvelope", from = "serde_from::MaybeTaggedTxEnvelope") +)] +#[cfg_attr(all(any(test, feature = "arbitrary"), feature = "k256"), derive(arbitrary::Arbitrary))] #[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 [`TxEip7702`] tagged with type 4. - #[cfg_attr(feature = "serde", serde(rename = "0x4", alias = "0x04"))] Eip7702(Signed), /// A [`TxDeposit`] tagged with type 0x7E. - #[cfg_attr(feature = "serde", serde(rename = "0x7E", alias = "0x7E"))] Deposit(TxDeposit), } @@ -376,39 +377,15 @@ impl OpTxEnvelope { } } - /// Return the length of the inner txn, __without a type byte__. - pub fn inner_length(&self) -> usize { + /// Return the length of the inner txn, including type byte length + pub fn eip2718_encoded_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::Eip7702(t) => { - let payload_length = t.tx().fields_len() + t.signature().rlp_vrs_len(); - Header { list: true, payload_length }.length() + 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; + Self::Legacy(t) => t.eip2718_encoded_length(), + Self::Eip2930(t) => t.eip2718_encoded_length(), + Self::Eip1559(t) => t.eip2718_encoded_length(), + Self::Eip7702(t) => t.eip2718_encoded_length(), + Self::Deposit(t) => t.eip2718_encoded_length(), } - // length of inner tx body - let inner_length = self.inner_length(); - // with tx type byte - inner_length + 1 } } @@ -418,34 +395,22 @@ impl Encodable for OpTxEnvelope { } 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 + self.network_len() } } 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")), - } + Ok(Self::network_decode(buf)?) } } 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::Eip7702 => Ok(Self::Eip7702(TxEip7702::decode_signed_fields(buf)?)), + OpTxType::Eip2930 => Ok(Self::Eip2930(TxEip2930::rlp_decode_signed(buf)?)), + OpTxType::Eip1559 => Ok(Self::Eip1559(TxEip1559::rlp_decode_signed(buf)?)), + OpTxType::Eip7702 => Ok(Self::Eip7702(TxEip7702::rlp_decode_signed(buf)?)), OpTxType::Deposit => Ok(Self::Deposit(TxDeposit::decode(buf)?)), OpTxType::Legacy => { Err(alloy_rlp::Error::Custom("type-0 eip2718 transactions are not supported") @@ -455,7 +420,7 @@ impl Decodable2718 for OpTxEnvelope { } fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result { - Ok(Self::Legacy(TxLegacy::decode_signed_fields(buf)?)) + Ok(Self::Legacy(TxLegacy::rlp_decode_signed(buf)?)) } } @@ -471,24 +436,98 @@ impl Encodable2718 for OpTxEnvelope { } fn encode_2718_len(&self) -> usize { - self.inner_length() + !self.is_legacy() as usize + self.eip2718_encoded_length() } fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) { match self { // Legacy transactions have no difference between network and 2718 - Self::Legacy(tx) => tx.tx().encode_with_signature_fields(tx.signature(), out), + Self::Legacy(tx) => tx.eip2718_encode(out), Self::Eip2930(tx) => { - tx.tx().encode_with_signature(tx.signature(), out, false); + tx.eip2718_encode(out); } Self::Eip1559(tx) => { - tx.tx().encode_with_signature(tx.signature(), out, false); + tx.eip2718_encode(out); } Self::Eip7702(tx) => { - tx.tx().encode_with_signature(tx.signature(), out, false); + tx.eip2718_encode(out); } Self::Deposit(tx) => { - tx.encode_inner(out, false); + tx.eip2718_encode(out); + } + } + } +} + +#[cfg(feature = "serde")] +mod serde_from { + //! NB: Why do we need this? + //! + //! Because the tag may be missing, we need an abstraction over tagged (with + //! type) and untagged (always legacy). This is [`MaybeTaggedTxEnvelope`]. + //! + //! The tagged variant is [`TaggedTxEnvelope`], which always has a type tag. + //! + //! We serialize via [`TaggedTxEnvelope`] and deserialize via + //! [`MaybeTaggedTxEnvelope`]. + use super::*; + + #[derive(Debug, serde::Deserialize)] + #[serde(untagged)] + pub(crate) enum MaybeTaggedTxEnvelope { + Tagged(TaggedTxEnvelope), + #[serde(with = "alloy_consensus::transaction::signed_legacy_serde")] + Untagged(Signed), + } + + #[derive(Debug, serde::Serialize, serde::Deserialize)] + #[serde(tag = "type")] + pub(crate) enum TaggedTxEnvelope { + #[serde( + rename = "0x0", + alias = "0x00", + with = "alloy_consensus::transaction::signed_legacy_serde" + )] + Legacy(Signed), + #[serde(rename = "0x1", alias = "0x01")] + Eip2930(Signed), + #[serde(rename = "0x2", alias = "0x02")] + Eip1559(Signed), + #[serde(rename = "0x4", alias = "0x04")] + Eip7702(Signed), + #[serde(rename = "0x7e", alias = "0x7E", serialize_with = "crate::serde_deposit_tx_rpc")] + Deposit(TxDeposit), + } + + impl From for OpTxEnvelope { + fn from(value: MaybeTaggedTxEnvelope) -> Self { + match value { + MaybeTaggedTxEnvelope::Tagged(tagged) => tagged.into(), + MaybeTaggedTxEnvelope::Untagged(tx) => Self::Legacy(tx), + } + } + } + + impl From for OpTxEnvelope { + fn from(value: TaggedTxEnvelope) -> Self { + match value { + TaggedTxEnvelope::Legacy(signed) => Self::Legacy(signed), + TaggedTxEnvelope::Eip2930(signed) => Self::Eip2930(signed), + TaggedTxEnvelope::Eip1559(signed) => Self::Eip1559(signed), + TaggedTxEnvelope::Eip7702(signed) => Self::Eip7702(signed), + TaggedTxEnvelope::Deposit(tx) => Self::Deposit(tx), + } + } + } + + impl From for TaggedTxEnvelope { + fn from(value: OpTxEnvelope) -> Self { + match value { + OpTxEnvelope::Legacy(signed) => Self::Legacy(signed), + OpTxEnvelope::Eip2930(signed) => Self::Eip2930(signed), + OpTxEnvelope::Eip1559(signed) => Self::Eip1559(signed), + OpTxEnvelope::Eip7702(signed) => Self::Eip7702(signed), + OpTxEnvelope::Deposit(tx) => Self::Deposit(tx), } } } diff --git a/crates/consensus/src/transaction/mod.rs b/crates/consensus/src/transaction/mod.rs index 4fce2ecb..a17b097f 100644 --- a/crates/consensus/src/transaction/mod.rs +++ b/crates/consensus/src/transaction/mod.rs @@ -15,6 +15,9 @@ pub use source::{ UserDepositSource, }; +#[cfg(feature = "serde")] +pub use deposit::serde_deposit_tx_rpc; + /// Bincode-compatible serde implementations for transaction types. #[cfg(all(feature = "serde", feature = "serde-bincode-compat"))] pub(super) mod serde_bincode_compat { diff --git a/crates/consensus/src/transaction/typed.rs b/crates/consensus/src/transaction/typed.rs index 838545fd..dfb6c52e 100644 --- a/crates/consensus/src/transaction/typed.rs +++ b/crates/consensus/src/transaction/typed.rs @@ -14,22 +14,23 @@ use alloy_primitives::{Address, Bytes, TxKind}; #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(tag = "type"))] +#[cfg_attr( + feature = "serde", + serde( + from = "serde_from::MaybeTaggedTypedTransaction", + into = "serde_from::TaggedTypedTransaction" + ) +)] 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-7702 transaction - #[cfg_attr(feature = "serde", serde(rename = "0x04", alias = "0x4"))] Eip7702(TxEip7702), /// Optimism deposit transaction - #[cfg_attr(feature = "serde", serde(rename = "0x7E", alias = "0x7E"))] Deposit(TxDeposit), } @@ -275,3 +276,79 @@ impl Transaction for OpTypedTransaction { } } } + +#[cfg(feature = "serde")] +mod serde_from { + //! NB: Why do we need this? + //! + //! Because the tag may be missing, we need an abstraction over tagged (with + //! type) and untagged (always legacy). This is + //! [`MaybeTaggedTypedTransaction`]. + //! + //! The tagged variant is [`TaggedTypedTransaction`], which always has a + //! type tag. + //! + //! We serialize via [`TaggedTypedTransaction`] and deserialize via + //! [`MaybeTaggedTypedTransaction`]. + use super::*; + + #[derive(Debug, serde::Deserialize)] + #[serde(untagged)] + pub(crate) enum MaybeTaggedTypedTransaction { + Tagged(TaggedTypedTransaction), + Untagged(TxLegacy), + } + + #[derive(Debug, serde::Serialize, serde::Deserialize)] + #[serde(tag = "type")] + pub(crate) enum TaggedTypedTransaction { + /// Legacy transaction + #[serde(rename = "0x00", alias = "0x0")] + Legacy(TxLegacy), + /// EIP-2930 transaction + #[serde(rename = "0x01", alias = "0x1")] + Eip2930(TxEip2930), + /// EIP-1559 transaction + #[serde(rename = "0x02", alias = "0x2")] + Eip1559(TxEip1559), + /// EIP-7702 transaction + #[serde(rename = "0x04", alias = "0x4")] + Eip7702(TxEip7702), + /// Deposit transaction + #[serde(rename = "0x7e", alias = "0x7E", serialize_with = "crate::serde_deposit_tx_rpc")] + Deposit(TxDeposit), + } + + impl From for OpTypedTransaction { + fn from(value: MaybeTaggedTypedTransaction) -> Self { + match value { + MaybeTaggedTypedTransaction::Tagged(tagged) => tagged.into(), + MaybeTaggedTypedTransaction::Untagged(tx) => Self::Legacy(tx), + } + } + } + + impl From for OpTypedTransaction { + fn from(value: TaggedTypedTransaction) -> Self { + match value { + TaggedTypedTransaction::Legacy(signed) => Self::Legacy(signed), + TaggedTypedTransaction::Eip2930(signed) => Self::Eip2930(signed), + TaggedTypedTransaction::Eip1559(signed) => Self::Eip1559(signed), + TaggedTypedTransaction::Eip7702(signed) => Self::Eip7702(signed), + TaggedTypedTransaction::Deposit(tx) => Self::Deposit(tx), + } + } + } + + impl From for TaggedTypedTransaction { + fn from(value: OpTypedTransaction) -> Self { + match value { + OpTypedTransaction::Legacy(signed) => Self::Legacy(signed), + OpTypedTransaction::Eip2930(signed) => Self::Eip2930(signed), + OpTypedTransaction::Eip1559(signed) => Self::Eip1559(signed), + OpTypedTransaction::Eip7702(signed) => Self::Eip7702(signed), + OpTypedTransaction::Deposit(tx) => Self::Deposit(tx), + } + } + } +} diff --git a/crates/protocol/src/batch/raw.rs b/crates/protocol/src/batch/raw.rs index 59d990f3..f180e657 100644 --- a/crates/protocol/src/batch/raw.rs +++ b/crates/protocol/src/batch/raw.rs @@ -93,9 +93,6 @@ impl RawSpanBatch { } } - // Recover `v` values in transaction signatures within the batch. - self.payload.txs.recover_v(chain_id)?; - // Get all transactions in the batch. let enveloped_txs = self.payload.txs.full_txs(chain_id)?; @@ -165,8 +162,7 @@ mod test { fn test_decode_encode_raw_span_batch() { // Load in the raw span batch from the `op-node` derivation pipeline implementation. let raw_span_batch_hex = include_bytes!("./testdata/raw_batch.hex"); - let mut raw_span_batch = RawSpanBatch::decode(&mut raw_span_batch_hex.as_slice()).unwrap(); - raw_span_batch.payload.txs.recover_v(981).unwrap(); + let raw_span_batch = RawSpanBatch::decode(&mut raw_span_batch_hex.as_slice()).unwrap(); let mut encoding_buf = Vec::new(); raw_span_batch.encode(&mut encoding_buf).unwrap(); diff --git a/crates/protocol/src/batch/transactions.rs b/crates/protocol/src/batch/transactions.rs index f434d2ac..92eacb2f 100644 --- a/crates/protocol/src/batch/transactions.rs +++ b/crates/protocol/src/batch/transactions.rs @@ -2,13 +2,13 @@ //! transactions in a span batch. use crate::{ - is_protected_v, read_tx_data, SpanBatchBits, SpanBatchError, SpanBatchTransactionData, - SpanDecodingError, MAX_SPAN_BATCH_ELEMENTS, + read_tx_data, SpanBatchBits, SpanBatchError, SpanBatchTransactionData, SpanDecodingError, + MAX_SPAN_BATCH_ELEMENTS, }; use alloc::vec::Vec; use alloy_consensus::{Transaction, TxEnvelope, TxType}; use alloy_eips::eip2718::Encodable2718; -use alloy_primitives::{Address, Bytes, Parity, Signature, U256}; +use alloy_primitives::{Address, Bytes, PrimitiveSignature as Signature, U256}; use alloy_rlp::{Buf, Decodable, Encodable}; /// This struct contains the decoded information for transactions in a span batch. @@ -18,8 +18,6 @@ pub struct SpanBatchTransactions { pub total_block_tx_count: u64, /// The contract creation bits, standard span-batch bitlist. pub contract_creation_bits: SpanBatchBits, - /// The y parity bits, standard span-batch bitlist. - pub y_parity_bits: SpanBatchBits, /// The transaction signatures. pub tx_sigs: Vec, /// The transaction nonces @@ -42,8 +40,7 @@ impl SpanBatchTransactions { /// Encodes the [SpanBatchTransactions] into a writer. pub fn encode(&self, w: &mut Vec) -> Result<(), SpanBatchError> { self.encode_contract_creation_bits(w)?; - self.encode_y_parity_bits(w)?; - self.encode_tx_sigs_rs(w)?; + self.encode_tx_sigs(w)?; self.encode_tx_tos(w)?; self.encode_tx_datas(w)?; self.encode_tx_nonces(w)?; @@ -55,8 +52,7 @@ impl SpanBatchTransactions { /// Decodes the [SpanBatchTransactions] from a reader. pub fn decode(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { self.decode_contract_creation_bits(r)?; - self.decode_y_parity_bits(r)?; - self.decode_tx_sigs_rs(r)?; + self.decode_tx_sigs(r)?; self.decode_tx_tos(r)?; self.decode_tx_datas(r)?; self.decode_tx_nonces(r)?; @@ -77,14 +73,14 @@ impl SpanBatchTransactions { Ok(()) } - /// Encode the y parity bits into a writer. - pub fn encode_y_parity_bits(&self, w: &mut Vec) -> Result<(), SpanBatchError> { - SpanBatchBits::encode(w, self.total_block_tx_count as usize, &self.y_parity_bits)?; - Ok(()) - } - /// Encode the transaction signatures into a writer (excluding `v` field). - pub fn encode_tx_sigs_rs(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + pub fn encode_tx_sigs(&self, w: &mut Vec) -> Result<(), SpanBatchError> { + let mut y_parity_bits = SpanBatchBits::default(); + for (i, sig) in self.tx_sigs.iter().enumerate() { + y_parity_bits.set_bit(i, sig.v()); + } + + SpanBatchBits::encode(w, self.total_block_tx_count as usize, &y_parity_bits)?; for sig in &self.tx_sigs { w.extend_from_slice(&sig.r().to_be_bytes::<32>()); w.extend_from_slice(&sig.s().to_be_bytes::<32>()); @@ -148,19 +144,15 @@ impl SpanBatchTransactions { Ok(()) } - /// Decode the y parity bits from a reader. - pub fn decode_y_parity_bits(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { - self.y_parity_bits = SpanBatchBits::decode(r, self.total_block_tx_count as usize)?; - Ok(()) - } - /// Decode the transaction signatures from a reader (excluding `v` field). - pub fn decode_tx_sigs_rs(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + pub fn decode_tx_sigs(&mut self, r: &mut &[u8]) -> Result<(), SpanBatchError> { + let y_parity_bits = SpanBatchBits::decode(r, self.total_block_tx_count as usize)?; let mut sigs = Vec::with_capacity(self.total_block_tx_count as usize); - for _ in 0..self.total_block_tx_count { + for i in 0..self.total_block_tx_count { + let y_parity = y_parity_bits.get_bit(i as usize).expect("same length"); let r_val = U256::from_be_slice(&r[..32]); let s_val = U256::from_be_slice(&r[32..64]); - sigs.push(Signature::new(r_val, s_val, Parity::Eip155(0))); + sigs.push(Signature::new(r_val, s_val, y_parity == 1)); r.advance(64); } self.tx_sigs = sigs; @@ -233,35 +225,6 @@ impl SpanBatchTransactions { self.contract_creation_bits.as_ref().iter().map(|b| b.count_ones() as u64).sum() } - /// Recover the `v` values of the transaction signatures. - pub fn recover_v(&mut self, chain_id: u64) -> Result<(), SpanBatchError> { - if self.tx_sigs.len() != self.tx_types.len() { - return Err(SpanBatchError::Decoding(SpanDecodingError::TypeSignatureLenMismatch)); - } - let mut protected_bits_idx = 0; - for (i, tx_type) in self.tx_types.iter().enumerate() { - let bit = self.y_parity_bits.get_bit(i).ok_or(SpanBatchError::BitfieldTooLong)?; - let v = match tx_type { - TxType::Legacy => { - // Legacy transaction - let protected_bit = self.protected_bits.get_bit(protected_bits_idx); - protected_bits_idx += 1; - if protected_bit.is_none() || protected_bit.is_some_and(|b| b == 0) { - Ok(27 + bit as u64) - } else { - // EIP-155 - Ok(chain_id * 2 + 35 + bit as u64) - } - } - TxType::Eip2930 | TxType::Eip1559 => Ok(bit as u64), - _ => Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType)), - }?; - self.tx_sigs[i] = - Signature::new(self.tx_sigs[i].r(), self.tx_sigs[i].s(), Parity::Eip155(v)); - } - Ok(()) - } - /// Retrieve all of the raw transactions from the [SpanBatchTransactions]. pub fn full_txs(&self, chain_id: u64) -> Result>, SpanBatchError> { let mut txs = Vec::new(); @@ -297,7 +260,12 @@ impl SpanBatchTransactions { .tx_sigs .get(idx as usize) .ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; - let tx_envelope = tx.to_enveloped_tx(*nonce, *gas, to, chain_id, sig)?; + let is_protected = self + .protected_bits + .get_bit(idx as usize) + .ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))? + == 1; + let tx_envelope = tx.to_enveloped_tx(*nonce, *gas, to, chain_id, sig, is_protected)?; let mut buf = Vec::new(); tx_envelope.encode_2718(&mut buf); txs.push(buf); @@ -317,7 +285,7 @@ impl SpanBatchTransactions { let tx_type = tx_enveloped.tx_type(); if matches!(tx_type, TxType::Legacy) { - let protected_bit = is_protected_v(&tx_enveloped); + let protected_bit = tx_enveloped.is_replay_protected(); self.protected_bits.set_bit(self.legacy_tx_count as usize, protected_bit); self.legacy_tx_count += 1; } @@ -340,7 +308,7 @@ impl SpanBatchTransactions { } }; - if is_protected_v(&tx_enveloped) + if tx_enveloped.is_replay_protected() && tx_chain_id .ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))? != chain_id @@ -348,7 +316,6 @@ impl SpanBatchTransactions { return Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)); } - let y_parity = signature.v().y_parity(); let contract_creation_bit = match to { Some(address) => { self.tx_tos.push(address); @@ -361,7 +328,6 @@ impl SpanBatchTransactions { self.tx_sigs.push(*signature); self.contract_creation_bits.set_bit((i + offset) as usize, contract_creation_bit == 1); - self.y_parity_bits.set_bit((i + offset) as usize, y_parity); self.tx_nonces.push(nonce); self.tx_datas.push(tx_data_buf); self.tx_gases.push(gas); @@ -375,8 +341,8 @@ impl SpanBatchTransactions { #[cfg(test)] mod tests { use super::*; - use alloy_consensus::{Signed, TxEip1559, TxEip2930, TxLegacy}; - use alloy_primitives::{address, Signature, TxKind}; + use alloy_consensus::{Signed, TxEip1559, TxEip2930}; + use alloy_primitives::{address, PrimitiveSignature as Signature, TxKind}; #[test] fn test_span_batch_transactions_add_empty_txs() { @@ -388,24 +354,6 @@ mod tests { assert_eq!(span_batch_txs.total_block_tx_count, 0); } - #[test] - fn test_span_batch_transactions_add_invalid_legacy_parity_decoding() { - let sig = Signature::test_signature(); - let to = address!("0123456789012345678901234567890123456789"); - let tx = TxEnvelope::Legacy(Signed::new_unchecked( - TxLegacy { to: TxKind::Call(to), ..Default::default() }, - sig, - Default::default(), - )); - let mut span_batch_txs = SpanBatchTransactions::default(); - let mut buf = vec![]; - tx.encode(&mut buf); - let txs = vec![Bytes::from(buf)]; - let chain_id = 1; - let err = span_batch_txs.add_txs(txs, chain_id).unwrap_err(); - assert_eq!(err, SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)); - } - #[test] fn test_span_batch_transactions_add_eip2930_tx_wrong_chain_id() { let sig = Signature::test_signature(); diff --git a/crates/protocol/src/batch/tx_data/eip1559.rs b/crates/protocol/src/batch/tx_data/eip1559.rs index c8406a56..b335c099 100644 --- a/crates/protocol/src/batch/tx_data/eip1559.rs +++ b/crates/protocol/src/batch/tx_data/eip1559.rs @@ -3,7 +3,7 @@ use crate::{SpanBatchError, SpanDecodingError}; use alloy_consensus::{SignableTransaction, Signed, TxEip1559}; use alloy_eips::eip2930::AccessList; -use alloy_primitives::{Address, Signature, TxKind, U256}; +use alloy_primitives::{Address, PrimitiveSignature as Signature, TxKind, U256}; use alloy_rlp::{Bytes, RlpDecodable, RlpEncodable}; use op_alloy_consensus::OpTxEnvelope; diff --git a/crates/protocol/src/batch/tx_data/eip2930.rs b/crates/protocol/src/batch/tx_data/eip2930.rs index d829f333..f825bc7c 100644 --- a/crates/protocol/src/batch/tx_data/eip2930.rs +++ b/crates/protocol/src/batch/tx_data/eip2930.rs @@ -3,7 +3,7 @@ use crate::{SpanBatchError, SpanDecodingError}; use alloy_consensus::{SignableTransaction, Signed, TxEip2930}; use alloy_eips::eip2930::AccessList; -use alloy_primitives::{Address, Signature, TxKind, U256}; +use alloy_primitives::{Address, PrimitiveSignature as Signature, TxKind, U256}; use alloy_rlp::{Bytes, RlpDecodable, RlpEncodable}; use op_alloy_consensus::OpTxEnvelope; diff --git a/crates/protocol/src/batch/tx_data/legacy.rs b/crates/protocol/src/batch/tx_data/legacy.rs index 67a09aca..4ff17dc3 100644 --- a/crates/protocol/src/batch/tx_data/legacy.rs +++ b/crates/protocol/src/batch/tx_data/legacy.rs @@ -2,7 +2,7 @@ use crate::{SpanBatchError, SpanDecodingError}; use alloy_consensus::{SignableTransaction, Signed, TxLegacy}; -use alloy_primitives::{Address, Signature, TxKind, U256}; +use alloy_primitives::{Address, PrimitiveSignature as Signature, TxKind, U256}; use alloy_rlp::{Bytes, RlpDecodable, RlpEncodable}; use op_alloy_consensus::OpTxEnvelope; @@ -26,9 +26,10 @@ impl SpanBatchLegacyTransactionData { to: Option
, chain_id: u64, signature: Signature, + is_protected: bool, ) -> Result { let legacy_tx = TxLegacy { - chain_id: Some(chain_id), + chain_id: is_protected.then_some(chain_id), nonce, gas_price: u128::from_be_bytes( self.gas_price.to_be_bytes::<32>()[16..].try_into().map_err(|_| { diff --git a/crates/protocol/src/batch/tx_data/wrapper.rs b/crates/protocol/src/batch/tx_data/wrapper.rs index b4f41d8c..c96267a0 100644 --- a/crates/protocol/src/batch/tx_data/wrapper.rs +++ b/crates/protocol/src/batch/tx_data/wrapper.rs @@ -1,7 +1,7 @@ //! This module contains the top level span batch transaction data type. use alloy_consensus::{Transaction, TxEnvelope, TxType}; -use alloy_primitives::{Address, Signature, U256}; +use alloy_primitives::{Address, PrimitiveSignature as Signature, U256}; use alloy_rlp::{Bytes, Decodable, Encodable}; use op_alloy_consensus::OpTxEnvelope; @@ -122,9 +122,12 @@ impl SpanBatchTransactionData { to: Option
, chain_id: u64, signature: Signature, + is_protected: bool, ) -> Result { match self { - Self::Legacy(data) => data.to_enveloped_tx(nonce, gas, to, chain_id, signature), + Self::Legacy(data) => { + data.to_enveloped_tx(nonce, gas, to, chain_id, signature, is_protected) + } Self::Eip2930(data) => data.to_enveloped_tx(nonce, gas, to, chain_id, signature), Self::Eip1559(data) => data.to_enveloped_tx(nonce, gas, to, chain_id, signature), } diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index f9f46a22..ba0454c5 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -34,10 +34,7 @@ mod iter; pub use iter::FrameIter; mod utils; -pub use utils::{ - is_protected_v, read_tx_data, starts_with_2718_deposit, to_system_config, - OpBlockConversionError, -}; +pub use utils::{read_tx_data, starts_with_2718_deposit, to_system_config, OpBlockConversionError}; mod channel; pub use channel::{ diff --git a/crates/protocol/src/utils.rs b/crates/protocol/src/utils.rs index 003e7dd3..560edff3 100644 --- a/crates/protocol/src/utils.rs +++ b/crates/protocol/src/utils.rs @@ -1,7 +1,7 @@ //! Utility methods used by protocol types. use alloc::vec::Vec; -use alloy_consensus::{TxEnvelope, TxType}; +use alloy_consensus::TxType; use alloy_primitives::B256; use alloy_rlp::{Buf, Header}; use op_alloy_consensus::{OpBlock, OpTxEnvelope}; @@ -268,28 +268,9 @@ pub fn read_tx_data(r: &mut &[u8]) -> Result<(Vec, TxType), SpanBatchError> )) } -/// Checks if the signature of the passed [TxEnvelope] is protected. -pub const fn is_protected_v(tx: &TxEnvelope) -> bool { - match tx { - TxEnvelope::Legacy(tx) => { - let v = tx.signature().v().to_u64(); - if 64 - v.leading_zeros() <= 8 { - return v != 27 && v != 28 && v != 1 && v != 0; - } - // anything not 27 or 28 is considered protected - true - } - _ => true, - } -} - #[cfg(test)] mod tests { use super::*; - use alloy_consensus::{ - Signed, TxEip1559, TxEip2930, TxEip4844, TxEip4844Variant, TxEip7702, TxLegacy, - }; - use alloy_primitives::{b256, Signature}; use alloy_sol_types::{sol, SolCall}; use revm::{ db::BenchmarkDB, @@ -299,45 +280,6 @@ mod tests { use rstest::rstest; use std::vec::Vec; - #[test] - fn test_is_protected_v() { - let sig = Signature::test_signature(); - assert!(!is_protected_v(&TxEnvelope::Legacy(Signed::new_unchecked( - TxLegacy::default(), - sig, - Default::default(), - )))); - let r = b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"); - let s = b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"); - let v = 27; - let valid_sig = Signature::from_scalars_and_parity(r, s, v).unwrap(); - assert!(!is_protected_v(&TxEnvelope::Legacy(Signed::new_unchecked( - TxLegacy::default(), - valid_sig, - Default::default(), - )))); - assert!(is_protected_v(&TxEnvelope::Eip2930(Signed::new_unchecked( - TxEip2930::default(), - sig, - Default::default(), - )))); - assert!(is_protected_v(&TxEnvelope::Eip1559(Signed::new_unchecked( - TxEip1559::default(), - sig, - Default::default(), - )))); - assert!(is_protected_v(&TxEnvelope::Eip4844(Signed::new_unchecked( - TxEip4844Variant::TxEip4844(TxEip4844::default()), - sig, - Default::default(), - )))); - assert!(is_protected_v(&TxEnvelope::Eip7702(Signed::new_unchecked( - TxEip7702::default(), - sig, - Default::default(), - )))); - } - #[rstest] #[case::empty(&[], 0)] #[case::thousand_zeros(&[0; 1000], 21)] diff --git a/crates/rpc-types/Cargo.toml b/crates/rpc-types/Cargo.toml index c9d96958..643518d4 100644 --- a/crates/rpc-types/Cargo.toml +++ b/crates/rpc-types/Cargo.toml @@ -21,7 +21,6 @@ op-alloy-consensus = { workspace = true, features = ["serde"] } # Alloy alloy-serde.workspace = true alloy-consensus.workspace = true -alloy-network.workspace = true alloy-network-primitives.workspace = true alloy-eips = { workspace = true, features = ["serde"] } alloy-rpc-types-eth = { workspace = true, features = ["serde"] } @@ -56,7 +55,9 @@ arbitrary = [ "dep:arbitrary", "alloy-primitives/arbitrary", "alloy-rpc-types-eth/arbitrary", + "op-alloy-consensus/arbitrary", ] k256 = [ - "alloy-consensus/k256", + "alloy-rpc-types-eth/k256", + "op-alloy-consensus/k256", ] diff --git a/crates/rpc-types/src/transaction.rs b/crates/rpc-types/src/transaction.rs index 0c23d437..952a356a 100644 --- a/crates/rpc-types/src/transaction.rs +++ b/crates/rpc-types/src/transaction.rs @@ -1,39 +1,28 @@ //! Optimism specific types related to transactions. -use alloc::string::{String, ToString}; -use alloy_consensus::{ - SignableTransaction, Transaction as ConsensusTransaction, TxEip1559, TxEip2930, TxEip7702, - TxLegacy, -}; -use alloy_eips::{eip2718::Eip2718Error, eip2930::AccessList, eip7702::SignedAuthorization}; -use alloy_primitives::{Address, BlockHash, Bytes, ChainId, SignatureError, TxKind, B256, U256}; +use alloy_consensus::Transaction as ConsensusTransaction; +use alloy_eips::{eip2930::AccessList, eip7702::SignedAuthorization}; +use alloy_primitives::{Address, BlockHash, Bytes, ChainId, TxKind, B256, U256}; use alloy_serde::OtherFields; -use op_alloy_consensus::{OpTxEnvelope, OpTxType, TxDeposit}; +use op_alloy_consensus::OpTxEnvelope; use serde::{Deserialize, Serialize}; mod request; pub use request::OpTransactionRequest; /// OP Transaction type -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] -#[serde(rename_all = "camelCase")] +#[derive( + Clone, Debug, PartialEq, Eq, Serialize, Deserialize, derive_more::Deref, derive_more::DerefMut, +)] +#[serde(try_from = "tx_serde::TransactionSerdeHelper", into = "tx_serde::TransactionSerdeHelper")] +#[cfg_attr(all(any(test, feature = "arbitrary"), feature = "k256"), derive(arbitrary::Arbitrary))] pub struct Transaction { /// Ethereum Transaction Types - #[serde(flatten)] - pub inner: alloy_rpc_types_eth::Transaction, - /// The ETH value to mint on L2 - #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")] - pub mint: Option, - /// Hash that uniquely identifies the source of the deposit. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_hash: Option, - /// Field indicating whether the transaction is a system transaction, and therefore - /// exempt from the L2 gas limit. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub is_system_tx: Option, + #[deref] + #[deref_mut] + pub inner: alloy_rpc_types_eth::Transaction, + /// Deposit receipt version for deposit transactions post-canyon - #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")] pub deposit_receipt_version: Option, } @@ -155,149 +144,138 @@ impl From for OtherFields { } } -/// Errors that can occur when converting a [Transaction] to an [OpTxEnvelope]. -#[derive(Debug)] -pub enum TransactionConversionError { - /// The transaction type is not supported. - UnsupportedTransactionType(Eip2718Error), - /// The transaction's signature could not be converted to the consensus type. - SignatureConversionError(SignatureError), - /// The transaction is missing a required field. - MissingRequiredField(String), - /// The transaction's signature is missing. - MissingSignature, +impl AsRef for Transaction { + fn as_ref(&self) -> &OpTxEnvelope { + self.inner.as_ref() + } } -impl core::fmt::Display for TransactionConversionError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::UnsupportedTransactionType(e) => { - write!(f, "Unsupported transaction type: {}", e) - } - Self::SignatureConversionError(e) => { - write!(f, "Signature conversion error: {}", e) - } - Self::MissingRequiredField(field) => { - write!(f, "Missing required field for conversion: {}", field) - } - Self::MissingSignature => { - write!(f, "Missing signature") +mod tx_serde { + //! Helper module for serializing and deserializing OP [`Transaction`]. + //! + //! This is needed because we might need to deserialize the `from` field into both + //! [`alloy_rpc_types_eth::Transaction::from`] and [`op_alloy_consensus::TxDeposit::from`]. + use super::*; + use serde::de::Error; + + /// Helper struct which will be flattened into the transaction and will only contain `from` + /// field if inner [`OpTxEnvelope`] did not consume it. + #[derive(Serialize, Deserialize)] + struct MaybeFrom { + #[serde(default, skip_serializing_if = "Option::is_none")] + from: Option
, + } + + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub(crate) struct TransactionSerdeHelper { + #[serde(flatten)] + inner: OpTxEnvelope, + #[serde(default, skip_serializing_if = "Option::is_none")] + block_hash: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "alloy_serde::quantity::opt" + )] + block_number: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "alloy_serde::quantity::opt" + )] + transaction_index: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "alloy_serde::quantity::opt" + )] + deposit_receipt_version: Option, + + #[serde(flatten)] + from: MaybeFrom, + } + + impl From for TransactionSerdeHelper { + fn from(value: Transaction) -> Self { + let Transaction { + inner: + alloy_rpc_types_eth::Transaction { + inner, + block_hash, + block_number, + transaction_index, + from, + }, + deposit_receipt_version, + } = value; + + // if inner transaction is deposit, then don't serialize `from` directly + let from = if matches!(inner, OpTxEnvelope::Deposit(_)) { None } else { Some(from) }; + + Self { + inner, + block_hash, + block_number, + transaction_index, + deposit_receipt_version, + from: MaybeFrom { from }, } } } -} -impl core::error::Error for TransactionConversionError {} - -impl TryFrom for OpTxEnvelope { - type Error = TransactionConversionError; - - fn try_from(value: Transaction) -> Result { - /// Helper function to extract the signature from an RPC [Transaction]. - #[inline(always)] - fn extract_signature( - value: &Transaction, - ) -> Result { - value - .inner - .signature - .ok_or(TransactionConversionError::MissingSignature)? - .try_into() - .map_err(TransactionConversionError::SignatureConversionError) + impl TryFrom for Transaction { + type Error = serde_json::Error; + + fn try_from(value: TransactionSerdeHelper) -> Result { + let TransactionSerdeHelper { + inner, + block_hash, + block_number, + transaction_index, + deposit_receipt_version, + from, + } = value; + + // Try to get `from` field from inner envelope or from `MaybeFrom`, otherwise return + // error + let from = if let Some(from) = from.from { + from + } else if let OpTxEnvelope::Deposit(tx) = &inner { + tx.from + } else { + return Err(serde_json::Error::custom("missing `from` field")); + }; + + Ok(Self { + inner: alloy_rpc_types_eth::Transaction { + inner, + block_hash, + block_number, + transaction_index, + from, + }, + deposit_receipt_version, + }) } + } +} - let ty = OpTxType::try_from(value.ty()) - .map_err(TransactionConversionError::UnsupportedTransactionType)?; - match ty { - OpTxType::Legacy => { - let signature = extract_signature(&value)?; - let legacy = TxLegacy { - chain_id: value.chain_id(), - nonce: value.nonce(), - gas_price: value.gas_price().unwrap_or_default(), - gas_limit: value.gas_limit(), - to: value.inner.to.map(TxKind::Call).unwrap_or(TxKind::Create), - value: value.value(), - input: value.inner.input, - }; - Ok(Self::Legacy(legacy.into_signed(signature))) - } - OpTxType::Eip2930 => { - let signature = extract_signature(&value)?; - let access_list_tx = TxEip2930 { - chain_id: value.chain_id().ok_or_else(|| { - TransactionConversionError::MissingRequiredField("chain_id".to_string()) - })?, - nonce: value.nonce(), - gas_price: value.gas_price().unwrap_or_default(), - gas_limit: value.gas_limit(), - to: value.inner.to.map(TxKind::Call).unwrap_or(TxKind::Create), - value: value.value(), - input: value.inner.input, - access_list: value.inner.access_list.unwrap_or_default(), - }; - Ok(Self::Eip2930(access_list_tx.into_signed(signature))) - } - OpTxType::Eip1559 => { - let signature = extract_signature(&value)?; - let dynamic_fee_tx = TxEip1559 { - chain_id: value.chain_id().ok_or_else(|| { - TransactionConversionError::MissingRequiredField("chain_id".to_string()) - })?, - nonce: value.nonce(), - gas_limit: value.gas_limit(), - to: value.inner.to.map(TxKind::Call).unwrap_or(TxKind::Create), - value: value.value(), - input: value.inner.input, - access_list: value.inner.access_list.unwrap_or_default(), - max_fee_per_gas: value.inner.max_fee_per_gas.unwrap_or_default(), - max_priority_fee_per_gas: value - .inner - .max_priority_fee_per_gas - .unwrap_or_default(), - }; - Ok(Self::Eip1559(dynamic_fee_tx.into_signed(signature))) - } - OpTxType::Eip7702 => { - let signature = extract_signature(&value)?; - let set_code_tx = TxEip7702 { - chain_id: value.chain_id().ok_or_else(|| { - TransactionConversionError::MissingRequiredField("chain_id".to_string()) - })?, - nonce: value.nonce(), - gas_limit: value.gas_limit(), - to: value.inner.to.ok_or_else(|| { - TransactionConversionError::MissingRequiredField("to".to_string()) - })?, - value: value.value(), - input: value.inner.input, - access_list: value.inner.access_list.unwrap_or_default(), - max_fee_per_gas: value.inner.max_fee_per_gas.unwrap_or_default(), - max_priority_fee_per_gas: value - .inner - .max_priority_fee_per_gas - .unwrap_or_default(), - authorization_list: value.inner.authorization_list.unwrap_or_default(), - }; - Ok(Self::Eip7702(set_code_tx.into_signed(signature))) - } - OpTxType::Deposit => { - let deposit_tx = TxDeposit { - source_hash: value.source_hash.ok_or_else(|| { - TransactionConversionError::MissingRequiredField("source_hash".to_string()) - })?, - from: value.inner.from, - to: value.inner.to.map(TxKind::Call).unwrap_or(TxKind::Create), - mint: value.mint, - value: value.inner.value, - gas_limit: value.gas_limit(), - is_system_transaction: value.is_system_tx.ok_or_else(|| { - TransactionConversionError::MissingRequiredField("is_system_tx".to_string()) - })?, - input: value.inner.input, - }; - Ok(Self::Deposit(deposit_tx)) - } - } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_deserialize_deposit() { + // cast rpc eth_getTransactionByHash + // 0xbc9329afac05556497441e2b3ee4c5d4da7ca0b2a4c212c212d0739e94a24df9 --rpc-url optimism + let rpc_tx = r#"{"blockHash":"0x9d86bb313ebeedf4f9f82bf8a19b426be656a365648a7c089b618771311db9f9","blockNumber":"0x798ad0b","hash":"0xbc9329afac05556497441e2b3ee4c5d4da7ca0b2a4c212c212d0739e94a24df9","transactionIndex":"0x0","type":"0x7e","nonce":"0x152ea95","input":"0x440a5e200000146b000f79c50000000000000003000000006725333f000000000141e287000000000000000000000000000000000000000000000000000000012439ee7e0000000000000000000000000000000000000000000000000000000063f363e973e96e7145ff001c81b9562cba7b6104eeb12a2bc4ab9f07c27d45cd81a986620000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985","mint":"0x0","sourceHash":"0x04e9a69416471ead93b02f0c279ab11ca0b635db5c1726a56faf22623bafde52","r":"0x0","s":"0x0","v":"0x0","gas":"0xf4240","from":"0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001","to":"0x4200000000000000000000000000000000000015","depositReceiptVersion":"0x1","value":"0x0","gasPrice":"0x0"}"#; + + let tx = serde_json::from_str::(rpc_tx).unwrap(); + + let OpTxEnvelope::Deposit(inner) = tx.as_ref() else { + panic!("Expected deposit transaction"); + }; + assert_eq!(tx.from, inner.from); } } diff --git a/crates/rpc-types/src/transaction/request.rs b/crates/rpc-types/src/transaction/request.rs index 6f889aa5..086a98dc 100644 --- a/crates/rpc-types/src/transaction/request.rs +++ b/crates/rpc-types/src/transaction/request.rs @@ -1,8 +1,8 @@ use alloc::vec::Vec; use alloy_consensus::{SignableTransaction, Signed, TxEip1559, TxEip4844, TypedTransaction}; use alloy_eips::eip7702::SignedAuthorization; -use alloy_network::TransactionBuilder7702; -use alloy_primitives::{Address, Signature, TxKind, U256}; +use alloy_network_primitives::TransactionBuilder7702; +use alloy_primitives::{Address, PrimitiveSignature as Signature, TxKind, U256}; use alloy_rpc_types_eth::{AccessList, TransactionInput, TransactionRequest}; use op_alloy_consensus::{OpTxEnvelope, OpTypedTransaction, TxDeposit}; use serde::{Deserialize, Serialize}; diff --git a/scripts/check_no_std.sh b/scripts/check_no_std.sh index 4a5caa97..3ea6d71d 100755 --- a/scripts/check_no_std.sh +++ b/scripts/check_no_std.sh @@ -5,6 +5,7 @@ no_std_packages=( op-alloy-consensus op-alloy-protocol op-alloy-genesis + op-alloy-rpc-types op-alloy-rpc-types-engine )