From 71e0178f71c308562907b0e2ff36caeb4a5326bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <3535019+leruaa@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:35:15 +0200 Subject: [PATCH] feat: migrate to alloy TxLegacy (#9593) Co-authored-by: Arsenii Kulikov --- crates/net/eth-wire-types/src/blocks.rs | 8 +- crates/net/eth-wire-types/src/transactions.rs | 24 +-- crates/primitives/Cargo.toml | 3 +- crates/primitives/src/alloy_compat.rs | 5 +- crates/primitives/src/transaction/compat.rs | 2 +- crates/primitives/src/transaction/legacy.rs | 178 +----------------- crates/primitives/src/transaction/mod.rs | 44 ++++- crates/primitives/src/transaction/pooled.rs | 25 ++- .../primitives/src/transaction/signature.rs | 175 +++++++++-------- .../src/transaction/signature.rs | 2 +- crates/storage/codecs/Cargo.toml | 6 +- crates/storage/codecs/derive/src/arbitrary.rs | 2 +- crates/storage/codecs/src/alloy/mod.rs | 1 + .../storage/codecs/src/alloy/transaction.rs | 94 +++++++++ crates/storage/db-api/src/models/mod.rs | 4 +- crates/transaction-pool/src/test_utils/gen.rs | 2 +- .../transaction-pool/src/test_utils/mock.rs | 14 +- 17 files changed, 280 insertions(+), 309 deletions(-) create mode 100644 crates/storage/codecs/src/alloy/transaction.rs diff --git a/crates/net/eth-wire-types/src/blocks.rs b/crates/net/eth-wire-types/src/blocks.rs index 9cf1ee00283b..b0a0d4a747b1 100644 --- a/crates/net/eth-wire-types/src/blocks.rs +++ b/crates/net/eth-wire-types/src/blocks.rs @@ -364,7 +364,7 @@ mod tests { chain_id: Some(1), nonce: 0x8u64, gas_price: 0x4a817c808, - gas_limit: 0x2e248u64, + gas_limit: 0x2e248, to: TxKind::Call(hex!("3535353535353535353535353535353535353535").into()), value: U256::from(0x200u64), input: Default::default(), @@ -379,7 +379,7 @@ mod tests { chain_id: Some(1), nonce: 0x9u64, gas_price: 0x4a817c809, - gas_limit: 0x33450u64, + gas_limit: 0x33450, to: TxKind::Call(hex!("3535353535353535353535353535353535353535").into()), value: U256::from(0x2d9u64), input: Default::default(), @@ -438,7 +438,7 @@ mod tests { chain_id: Some(1), nonce: 0x8u64, gas_price: 0x4a817c808, - gas_limit: 0x2e248u64, + gas_limit: 0x2e248, to: TxKind::Call(hex!("3535353535353535353535353535353535353535").into()), value: U256::from(0x200u64), input: Default::default(), @@ -454,7 +454,7 @@ mod tests { chain_id: Some(1), nonce: 0x9u64, gas_price: 0x4a817c809, - gas_limit: 0x33450u64, + gas_limit: 0x33450, to: TxKind::Call(hex!("3535353535353535353535353535353535353535").into()), value: U256::from(0x2d9u64), input: Default::default(), diff --git a/crates/net/eth-wire-types/src/transactions.rs b/crates/net/eth-wire-types/src/transactions.rs index b4c7bc8b2b57..2e3a6015d20a 100644 --- a/crates/net/eth-wire-types/src/transactions.rs +++ b/crates/net/eth-wire-types/src/transactions.rs @@ -127,7 +127,7 @@ mod tests { chain_id: Some(1), nonce: 0x8u64, gas_price: 0x4a817c808, - gas_limit: 0x2e248u64, + gas_limit: 0x2e248, to: TxKind::Call(hex!("3535353535353535353535353535353535353535").into()), value: U256::from(0x200u64), input: Default::default(), @@ -149,7 +149,7 @@ mod tests { chain_id: Some(1), nonce: 0x09u64, gas_price: 0x4a817c809, - gas_limit: 0x33450u64, + gas_limit: 0x33450, to: TxKind::Call(hex!("3535353535353535353535353535353535353535").into()), value: U256::from(0x2d9u64), input: Default::default(), @@ -193,7 +193,7 @@ mod tests { chain_id: Some(1), nonce: 0x8u64, gas_price: 0x4a817c808, - gas_limit: 0x2e248u64, + gas_limit: 0x2e248, to: TxKind::Call(hex!("3535353535353535353535353535353535353535").into()), value: U256::from(0x200u64), input: Default::default(), @@ -215,7 +215,7 @@ mod tests { chain_id: Some(1), nonce: 0x09u64, gas_price: 0x4a817c809, - gas_limit: 0x33450u64, + gas_limit: 0x33450, to: TxKind::Call(hex!("3535353535353535353535353535353535353535").into()), value: U256::from(0x2d9u64), input: Default::default(), @@ -260,7 +260,7 @@ mod tests { chain_id: Some(4), nonce: 15u64, gas_price: 2200000000, - gas_limit: 34811u64, + gas_limit: 34811, to: TxKind::Call(hex!("cf7f9e66af820a19257a2108375b180b0ec49167").into()), value: U256::from(1234u64), input: Default::default(), @@ -306,7 +306,7 @@ mod tests { chain_id: Some(4), nonce: 3u64, gas_price: 2000000000, - gas_limit: 10000000u64, + gas_limit: 10000000, to: TxKind::Call(hex!("d3e8763675e4c425df46cc3b5c0f6cbdac396046").into()), value: U256::from(1000000000000000u64), input: Default::default(), @@ -328,7 +328,7 @@ mod tests { chain_id: Some(4), nonce: 1u64, gas_price: 1000000000, - gas_limit: 100000u64, + gas_limit: 100000, to: TxKind::Call(hex!("d3e8763675e4c425df46cc3b5c0f6cbdac396046").into()), value: U256::from(693361000000000u64), input: Default::default(), @@ -350,7 +350,7 @@ mod tests { chain_id: Some(4), nonce: 2u64, gas_price: 1000000000, - gas_limit: 100000u64, + gas_limit: 100000, to: TxKind::Call(hex!("d3e8763675e4c425df46cc3b5c0f6cbdac396046").into()), value: U256::from(1000000000000000u64), input: Default::default(), @@ -399,7 +399,7 @@ mod tests { chain_id: Some(4), nonce: 15u64, gas_price: 2200000000, - gas_limit: 34811u64, + gas_limit: 34811, to: TxKind::Call(hex!("cf7f9e66af820a19257a2108375b180b0ec49167").into()), value: U256::from(1234u64), input: Default::default(), @@ -445,7 +445,7 @@ mod tests { chain_id: Some(4), nonce: 3u64, gas_price: 2000000000, - gas_limit: 10000000u64, + gas_limit: 10000000, to: TxKind::Call(hex!("d3e8763675e4c425df46cc3b5c0f6cbdac396046").into()), value: U256::from(1000000000000000u64), input: Default::default(), @@ -467,7 +467,7 @@ mod tests { chain_id: Some(4), nonce: 1u64, gas_price: 1000000000, - gas_limit: 100000u64, + gas_limit: 100000, to: TxKind::Call(hex!("d3e8763675e4c425df46cc3b5c0f6cbdac396046").into()), value: U256::from(693361000000000u64), input: Default::default(), @@ -489,7 +489,7 @@ mod tests { chain_id: Some(4), nonce: 2u64, gas_price: 1000000000, - gas_limit: 100000u64, + gas_limit: 100000, to: TxKind::Call(hex!("d3e8763675e4c425df46cc3b5c0f6cbdac396046").into()), value: U256::from(1000000000000000u64), input: Default::default(), diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 2da37359c023..ff6e8d59c378 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -23,13 +23,13 @@ reth-codecs = { workspace = true, optional = true } reth-optimism-chainspec = { workspace = true, optional = true } # ethereum +alloy-consensus.workspace = true alloy-primitives = { workspace = true, features = ["rand", "rlp"] } alloy-rlp = { workspace = true, features = ["arrayvec"] } alloy-rpc-types = { workspace = true, optional = true } alloy-serde = { workspace = true, optional = true } alloy-genesis.workspace = true alloy-eips = { workspace = true, features = ["serde"] } -alloy-consensus.workspace = true # optimism op-alloy-rpc-types = { workspace = true, optional = true } @@ -118,4 +118,3 @@ harness = false name = "validate_blob_tx" required-features = ["arbitrary", "c-kzg"] harness = false - diff --git a/crates/primitives/src/alloy_compat.rs b/crates/primitives/src/alloy_compat.rs index ee5843b1e849..d63bac7c0045 100644 --- a/crates/primitives/src/alloy_compat.rs +++ b/crates/primitives/src/alloy_compat.rs @@ -102,10 +102,7 @@ impl TryFrom> for Transaction { chain_id, nonce: tx.nonce, gas_price: tx.gas_price.ok_or(ConversionError::MissingGasPrice)?, - gas_limit: tx - .gas - .try_into() - .map_err(|_| ConversionError::Eip2718Error(RlpError::Overflow.into()))?, + gas_limit: tx.gas, to: tx.to.map_or(TxKind::Create, TxKind::Call), value: tx.value, input: tx.input, diff --git a/crates/primitives/src/transaction/compat.rs b/crates/primitives/src/transaction/compat.rs index 7f47aa7e6655..a58277374ecb 100644 --- a/crates/primitives/src/transaction/compat.rs +++ b/crates/primitives/src/transaction/compat.rs @@ -22,7 +22,7 @@ impl FillTxEnv for TransactionSigned { tx_env.caller = sender; match self.as_ref() { Transaction::Legacy(tx) => { - tx_env.gas_limit = tx.gas_limit; + tx_env.gas_limit = tx.gas_limit as u64; tx_env.gas_price = U256::from(tx.gas_price); tx_env.gas_priority_fee = None; tx_env.transact_to = tx.to; diff --git a/crates/primitives/src/transaction/legacy.rs b/crates/primitives/src/transaction/legacy.rs index 181c543803e5..7154f73cc9ef 100644 --- a/crates/primitives/src/transaction/legacy.rs +++ b/crates/primitives/src/transaction/legacy.rs @@ -1,180 +1,4 @@ -use crate::{keccak256, Bytes, ChainId, Signature, TxKind, TxType, B256, U256}; -use alloy_rlp::{length_of_length, Encodable, Header}; -use core::mem; - -#[cfg(any(test, feature = "reth-codec"))] -use reth_codecs::Compact; - -#[cfg(not(feature = "std"))] -use alloc::vec::Vec; -use serde::{Deserialize, Serialize}; - -/// Legacy transaction. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] -#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] -#[cfg_attr(any(test, feature = "reth-codec"), derive(Compact))] -#[cfg_attr(any(test, feature = "reth-codec"), reth_codecs::add_arbitrary_tests(compact))] -pub struct TxLegacy { - /// Added as EIP-155: Simple replay attack protection - pub chain_id: Option, - /// A scalar value equal to the number of transactions sent by the sender; formally Tn. - pub nonce: u64, - /// A scalar value equal to the number of - /// Wei to be paid per unit of gas for all computation - /// costs incurred as a result of the execution of this transaction; formally Tp. - /// - /// As ethereum circulation is around 120mil eth as of 2022 that is around - /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: - /// 340282366920938463463374607431768211455 - pub gas_price: u128, - /// A scalar value equal to the maximum - /// amount of gas that should be used in executing - /// this transaction. This is paid up-front, before any - /// computation is done and may not be increased - /// later; formally Tg. - pub gas_limit: u64, - /// The 160-bit address of the message call’s recipient or, for a contract creation - /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. - pub to: TxKind, - /// A scalar value equal to the number of Wei to - /// be transferred to the message call’s recipient or, - /// in the case of contract creation, as an endowment - /// to the newly created account; formally Tv. - pub value: U256, - /// Input has two uses depending if transaction is Create or Call (if `to` field is None or - /// Some). pub init: An unlimited size byte array specifying the - /// EVM-code for the account initialisation procedure CREATE, - /// data: An unlimited size byte array specifying the - /// input data of the message call, formally Td. - pub input: Bytes, -} - -impl TxLegacy { - /// Calculates a heuristic for the in-memory size of the [`TxLegacy`] transaction. - #[inline] - pub fn size(&self) -> usize { - mem::size_of::>() + // chain_id - mem::size_of::() + // nonce - mem::size_of::() + // gas_price - mem::size_of::() + // gas_limit - self.to.size() + // to - mem::size_of::() + // value - self.input.len() // input - } - - /// Outputs the length of the transaction's fields, without a RLP header or length of the - /// eip155 fields. - pub(crate) fn fields_len(&self) -> usize { - self.nonce.length() + - self.gas_price.length() + - self.gas_limit.length() + - self.to.length() + - self.value.length() + - self.input.0.length() - } - - /// Encodes only the transaction's fields into the desired buffer, without a RLP header or - /// eip155 fields. - pub(crate) fn encode_fields(&self, out: &mut dyn bytes::BufMut) { - self.nonce.encode(out); - self.gas_price.encode(out); - self.gas_limit.encode(out); - self.to.encode(out); - self.value.encode(out); - self.input.0.encode(out); - } - - /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating - /// hash. - /// - /// This encodes the transaction as: - /// `rlp(nonce, gas_price, gas_limit, to, value, input, v, r, s)` - /// - /// The `v` value is encoded according to EIP-155 if the `chain_id` is not `None`. - pub(crate) fn encode_with_signature(&self, signature: &Signature, out: &mut dyn bytes::BufMut) { - let payload_length = - self.fields_len() + signature.payload_len_with_eip155_chain_id(self.chain_id); - let header = Header { list: true, payload_length }; - header.encode(out); - self.encode_fields(out); - signature.encode_with_eip155_chain_id(out, self.chain_id); - } - - /// Output the length of the RLP signed transaction encoding. - pub(crate) fn payload_len_with_signature(&self, signature: &Signature) -> usize { - let payload_length = - self.fields_len() + signature.payload_len_with_eip155_chain_id(self.chain_id); - // 'header length' + 'payload length' - length_of_length(payload_length) + payload_length - } - - /// Get transaction type - pub(crate) const fn tx_type(&self) -> TxType { - TxType::Legacy - } - - /// Encodes EIP-155 arguments into the desired buffer. Only encodes values for legacy - /// transactions. - /// - /// If a `chain_id` is `Some`, this encodes the `chain_id`, followed by two zeroes, as defined - /// by [EIP-155](https://eips.ethereum.org/EIPS/eip-155). - pub(crate) fn encode_eip155_fields(&self, out: &mut dyn bytes::BufMut) { - // if this is a legacy transaction without a chain ID, it must be pre-EIP-155 - // and does not need to encode the chain ID for the signature hash encoding - if let Some(id) = self.chain_id { - // EIP-155 encodes the chain ID and two zeroes - id.encode(out); - 0x00u8.encode(out); - 0x00u8.encode(out); - } - } - - /// Outputs the length of EIP-155 fields. Only outputs a non-zero value for EIP-155 legacy - /// transactions. - pub(crate) fn eip155_fields_len(&self) -> usize { - if let Some(id) = self.chain_id { - // EIP-155 encodes the chain ID and two zeroes, so we add 2 to the length of the chain - // ID to get the length of all 3 fields - // len(chain_id) + (0x00) + (0x00) - id.length() + 2 - } else { - // this is either a pre-EIP-155 legacy transaction or a typed transaction - 0 - } - } - - /// Encodes the legacy transaction in RLP for signing, including the EIP-155 fields if possible. - /// - /// If a `chain_id` is `Some`, this encodes the transaction as: - /// `rlp(nonce, gas_price, gas_limit, to, value, input, chain_id, 0, 0)` - /// - /// Otherwise, this encodes the transaction as: - /// `rlp(nonce, gas_price, gas_limit, to, value, input)` - pub(crate) fn encode_for_signing(&self, out: &mut dyn bytes::BufMut) { - Header { list: true, payload_length: self.fields_len() + self.eip155_fields_len() } - .encode(out); - self.encode_fields(out); - self.encode_eip155_fields(out); - } - - /// Outputs the length of the signature RLP encoding for the transaction, including the length - /// of the EIP-155 fields if possible. - pub(crate) fn payload_len_for_signature(&self) -> usize { - let payload_length = self.fields_len() + self.eip155_fields_len(); - // 'header length' + 'payload length' - length_of_length(payload_length) + payload_length - } - - /// Outputs the signature hash of the transaction by first encoding without a signature, then - /// hashing. - /// - /// See [`Self::encode_for_signing`] for more information on the encoding format. - pub(crate) fn signature_hash(&self) -> B256 { - let mut buf = Vec::with_capacity(self.payload_len_for_signature()); - self.encode_for_signing(&mut buf); - keccak256(&buf) - } -} +pub use alloy_consensus::TxLegacy; #[cfg(test)] mod tests { diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index c1bdcc964824..a9f80a23012b 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -5,6 +5,7 @@ use crate::{ B256, U256, }; +use alloy_consensus::SignableTransaction; use alloy_rlp::{ Decodable, Encodable, Error as RlpError, Header, EMPTY_LIST_CODE, EMPTY_STRING_CODE, }; @@ -87,7 +88,6 @@ pub(crate) static PARALLEL_SENDER_RECOVERY_THRESHOLD: Lazy = /// /// Transaction types were introduced in [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718). #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] #[cfg_attr(any(test, feature = "reth-codec"), reth_codecs::add_arbitrary_tests(compact))] pub enum Transaction { /// Legacy transaction (type `0x0`). @@ -140,6 +140,27 @@ pub enum Transaction { Deposit(TxDeposit), } +#[cfg(any(test, feature = "arbitrary"))] +impl<'a> arbitrary::Arbitrary<'a> for Transaction { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let mut tx = match TxType::arbitrary(u)? { + TxType::Legacy => Self::Legacy(TxLegacy::arbitrary(u)?), + TxType::Eip2930 => Self::Eip2930(TxEip2930::arbitrary(u)?), + TxType::Eip1559 => Self::Eip1559(TxEip1559::arbitrary(u)?), + TxType::Eip4844 => Self::Eip4844(TxEip4844::arbitrary(u)?), + TxType::Eip7702 => Self::Eip7702(TxEip7702::arbitrary(u)?), + #[cfg(feature = "optimism")] + TxType::Deposit => Self::Deposit(TxDeposit::arbitrary(u)?), + }; + + if let Self::Legacy(tx) = &mut tx { + tx.gas_limit = (tx.gas_limit as u64).into(); + }; + + Ok(tx) + } +} + // === impl Transaction === impl Transaction { @@ -208,7 +229,7 @@ impl Transaction { /// Get the transaction's type pub const fn tx_type(&self) -> TxType { match self { - Self::Legacy(legacy_tx) => legacy_tx.tx_type(), + Self::Legacy(_) => TxType::Legacy, Self::Eip2930(access_list_tx) => access_list_tx.tx_type(), Self::Eip1559(dynamic_fee_tx) => dynamic_fee_tx.tx_type(), Self::Eip4844(blob_tx) => blob_tx.tx_type(), @@ -273,7 +294,7 @@ impl Transaction { /// Get the gas limit of the transaction. pub const fn gas_limit(&self) -> u64 { match self { - Self::Legacy(TxLegacy { gas_limit, .. }) | + Self::Legacy(TxLegacy { gas_limit, .. }) => *gas_limit as u64, Self::Eip2930(TxEip2930 { gas_limit, .. }) | Self::Eip1559(TxEip1559 { gas_limit, .. }) | Self::Eip4844(TxEip4844 { gas_limit, .. }) | @@ -494,7 +515,10 @@ impl Transaction { match self { Self::Legacy(legacy_tx) => { // do nothing w/ with_header - legacy_tx.encode_with_signature(signature, out) + legacy_tx.encode_with_signature_fields( + &signature.as_signature_with_eip155_parity(legacy_tx.chain_id), + out, + ) } Self::Eip2930(access_list_tx) => { access_list_tx.encode_with_signature(signature, out, with_header) @@ -514,7 +538,7 @@ impl Transaction { /// This sets the transaction's gas limit. pub fn set_gas_limit(&mut self, gas_limit: u64) { match self { - Self::Legacy(tx) => tx.gas_limit = gas_limit, + Self::Legacy(tx) => tx.gas_limit = gas_limit.into(), Self::Eip2930(tx) => tx.gas_limit = gas_limit, Self::Eip1559(tx) => tx.gas_limit = gas_limit, Self::Eip4844(tx) => tx.gas_limit = gas_limit, @@ -1174,7 +1198,9 @@ impl TransactionSigned { /// only `true`. pub(crate) fn payload_len_inner(&self) -> usize { match &self.transaction { - Transaction::Legacy(legacy_tx) => legacy_tx.payload_len_with_signature(&self.signature), + Transaction::Legacy(legacy_tx) => legacy_tx.encoded_len_with_signature( + &self.signature.as_signature_with_eip155_parity(legacy_tx.chain_id), + ), Transaction::Eip2930(access_list_tx) => { access_list_tx.payload_len_with_signature(&self.signature) } @@ -1378,7 +1404,9 @@ impl TransactionSigned { pub fn length_without_header(&self) -> usize { // method computes the payload len without a RLP header match &self.transaction { - Transaction::Legacy(legacy_tx) => legacy_tx.payload_len_with_signature(&self.signature), + Transaction::Legacy(legacy_tx) => legacy_tx.encoded_len_with_signature( + &self.signature.as_signature_with_eip155_parity(legacy_tx.chain_id), + ), Transaction::Eip2930(access_list_tx) => { access_list_tx.payload_len_with_signature_without_header(&self.signature) } @@ -1770,7 +1798,7 @@ mod tests { chain_id: Some(4), nonce: 1u64, gas_price: 1000000000, - gas_limit: 100000u64, + gas_limit: 100000, to: Address::from_slice(&hex!("d3e8763675e4c425df46cc3b5c0f6cbdac396046")[..]).into(), value: U256::from(693361000000000u64), input: Default::default(), diff --git a/crates/primitives/src/transaction/pooled.rs b/crates/primitives/src/transaction/pooled.rs index ff6284d3758f..3f6739bfd609 100644 --- a/crates/primitives/src/transaction/pooled.rs +++ b/crates/primitives/src/transaction/pooled.rs @@ -7,6 +7,7 @@ use crate::{ TransactionSigned, TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxEip4844, TxHash, TxLegacy, B256, EIP4844_TX_TYPE_ID, }; +use alloy_consensus::SignableTransaction; use alloy_rlp::{Decodable, Encodable, Error as RlpError, Header, EMPTY_LIST_CODE}; use bytes::Buf; use derive_more::{AsRef, Deref}; @@ -301,7 +302,9 @@ impl PooledTransactionsElement { match self { Self::Legacy { transaction, signature, .. } => { // method computes the payload len with a RLP header - transaction.payload_len_with_signature(signature) + transaction.encoded_len_with_signature( + &signature.as_signature_with_eip155_parity(transaction.chain_id), + ) } Self::Eip2930 { transaction, signature, .. } => { // method computes the payload len without a RLP header @@ -347,9 +350,11 @@ impl PooledTransactionsElement { // - EIP-4844: BlobTransaction::encode_with_type_inner // - EIP-7702: TxEip7702::encode_with_signature match self { - Self::Legacy { transaction, signature, .. } => { - transaction.encode_with_signature(signature, out) - } + Self::Legacy { transaction, signature, .. } => transaction + .encode_with_signature_fields( + &signature.as_signature_with_eip155_parity(transaction.chain_id), + out, + ), Self::Eip2930 { transaction, signature, .. } => { transaction.encode_with_signature(signature, out, false) } @@ -478,9 +483,11 @@ impl Encodable for PooledTransactionsElement { // - EIP-4844: BlobTransaction::encode_with_type_inner // - EIP-7702: TxEip7702::encode_with_signature match self { - Self::Legacy { transaction, signature, .. } => { - transaction.encode_with_signature(signature, out) - } + Self::Legacy { transaction, signature, .. } => transaction + .encode_with_signature_fields( + &signature.as_signature_with_eip155_parity(transaction.chain_id), + out, + ), Self::Eip2930 { transaction, signature, .. } => { // encodes with string header transaction.encode_with_signature(signature, out, true) @@ -506,7 +513,9 @@ impl Encodable for PooledTransactionsElement { match self { Self::Legacy { transaction, signature, .. } => { // method computes the payload len with a RLP header - transaction.payload_len_with_signature(signature) + transaction.encoded_len_with_signature( + &signature.as_signature_with_eip155_parity(transaction.chain_id), + ) } Self::Eip2930 { transaction, signature, .. } => { // method computes the payload len with a RLP header diff --git a/crates/primitives/src/transaction/signature.rs b/crates/primitives/src/transaction/signature.rs index 69fbd6ab1cb1..dad5ef522dbf 100644 --- a/crates/primitives/src/transaction/signature.rs +++ b/crates/primitives/src/transaction/signature.rs @@ -1,5 +1,8 @@ +use core::fmt::Debug; + use crate::{transaction::util::secp256k1, Address, B256, U256}; -use alloy_primitives::Bytes; +use alloy_consensus::EncodableSignature; +use alloy_primitives::{Bytes, Parity}; use alloy_rlp::{Decodable, Encodable, Error as RlpError}; use serde::{Deserialize, Serialize}; @@ -18,6 +21,9 @@ const SECP256K1N_HALF: U256 = U256::from_be_bytes([ /// r, s: Values corresponding to the signature of the /// transaction and used to determine the sender of /// the transaction; formally Tr and Ts. This is expanded in Appendix F of yellow paper. +/// +/// This type is unaware of the chain id, and thus shouldn't be used when encoding or decoding +/// legacy transactions. Use `SignatureWithParity` instead. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] #[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] #[cfg_attr(any(test, feature = "reth-codec"), reth_codecs::add_arbitrary_tests(compact))] @@ -33,15 +39,6 @@ pub struct Signature { pub odd_y_parity: bool, } -impl Signature { - /// Returns the signature for the optimism deposit transactions, which don't include a - /// signature. - #[cfg(feature = "optimism")] - pub const fn optimism_deposit_tx_signature() -> Self { - Self { r: U256::ZERO, s: U256::ZERO, odd_y_parity: false } - } -} - #[cfg(any(test, feature = "reth-codec"))] impl reth_codecs::Compact for Signature { fn to_compact(&self, buf: &mut B) -> usize @@ -64,44 +61,6 @@ impl reth_codecs::Compact for Signature { } impl Signature { - /// Output the length of the signature without the length of the RLP header, using the legacy - /// scheme with EIP-155 support depends on `chain_id`. - pub(crate) fn payload_len_with_eip155_chain_id(&self, chain_id: Option) -> usize { - self.v(chain_id).length() + self.r.length() + self.s.length() - } - - /// Encode the `v`, `r`, `s` values without a RLP header. - /// Encodes the `v` value using the legacy scheme with EIP-155 support depends on `chain_id`. - pub(crate) fn encode_with_eip155_chain_id( - &self, - out: &mut dyn alloy_rlp::BufMut, - chain_id: Option, - ) { - self.v(chain_id).encode(out); - self.r.encode(out); - self.s.encode(out); - } - - /// Output the `v` of the signature depends on `chain_id` - #[inline] - #[allow(clippy::missing_const_for_fn)] - pub fn v(&self, chain_id: Option) -> u64 { - if let Some(chain_id) = chain_id { - // EIP-155: v = {0, 1} + CHAIN_ID * 2 + 35 - self.odd_y_parity as u64 + chain_id * 2 + 35 - } else { - #[cfg(feature = "optimism")] - // pre bedrock system transactions were sent from the zero address as legacy - // transactions with an empty signature - // - // NOTE: this is very hacky and only relevant for op-mainnet pre bedrock - if *self == Self::optimism_deposit_tx_signature() { - return 0 - } - self.odd_y_parity as u64 + 27 - } - } - /// Decodes the `v`, `r`, `s` values without a RLP header. /// This will return a chain ID if the `v` value is [EIP-155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md) compatible. pub(crate) fn decode_with_eip155_chain_id( @@ -202,6 +161,39 @@ impl Signature { pub const fn size(&self) -> usize { core::mem::size_of::() } + + /// Returns [Parity] value based on `chain_id` for legacy transaction signature. + #[allow(clippy::missing_const_for_fn)] + pub fn legacy_parity(&self, chain_id: Option) -> Parity { + if let Some(chain_id) = chain_id { + Parity::Parity(self.odd_y_parity).with_chain_id(chain_id) + } else { + #[cfg(feature = "optimism")] + // pre bedrock system transactions were sent from the zero address as legacy + // transactions with an empty signature + // + // NOTE: this is very hacky and only relevant for op-mainnet pre bedrock + if *self == Self::optimism_deposit_tx_signature() { + return Parity::Parity(false) + } + Parity::NonEip155(self.odd_y_parity) + } + } + + /// Returns a signature with the given chain ID applied to the `v` value. + pub(crate) fn as_signature_with_eip155_parity( + &self, + chain_id: Option, + ) -> SignatureWithParity { + SignatureWithParity::new(self.r, self.s, self.legacy_parity(chain_id)) + } + + /// Returns the signature for the optimism deposit transactions, which don't include a + /// signature. + #[cfg(feature = "optimism")] + pub const fn optimism_deposit_tx_signature() -> Self { + Self { r: U256::ZERO, s: U256::ZERO, odd_y_parity: false } + } } impl From for Signature { @@ -228,52 +220,69 @@ pub const fn extract_chain_id(v: u64) -> alloy_rlp::Result<(bool, Option)> } } -#[cfg(test)] -mod tests { - use crate::{transaction::signature::SECP256K1N_HALF, Address, Signature, B256, U256}; - use alloy_primitives::{hex, hex::FromHex, Bytes}; - use std::str::FromStr; +/// A signature with full parity included. +// TODO: replace by alloy Signature when there will be an easy way to instantiate them. +pub(crate) struct SignatureWithParity { + /// The R field of the signature; the point on the curve. + r: U256, + /// The S field of the signature; the point on the curve. + s: U256, + /// Signature parity + parity: Parity, +} - #[test] - fn test_payload_len_with_eip155_chain_id() { - // Select 1 as an arbitrary nonzero value for R and S, as v() always returns 0 for (0, 0). - let signature = Signature { r: U256::from(1), s: U256::from(1), odd_y_parity: false }; +impl SignatureWithParity { + /// Creates a new [`SignatureWithParity`]. + pub(crate) const fn new(r: U256, s: U256, parity: Parity) -> Self { + Self { r, s, parity } + } +} - assert_eq!(3, signature.payload_len_with_eip155_chain_id(None)); - assert_eq!(3, signature.payload_len_with_eip155_chain_id(Some(1))); - assert_eq!(4, signature.payload_len_with_eip155_chain_id(Some(47))); +impl EncodableSignature for SignatureWithParity { + fn from_rs_and_parity< + P: TryInto, + E: Into, + >( + r: U256, + s: U256, + parity: P, + ) -> Result { + Ok(Self { r, s, parity: parity.try_into().map_err(Into::into)? }) } - #[test] - fn test_v() { - // Select 1 as an arbitrary nonzero value for R and S, as v() always returns 0 for (0, 0). - let signature = Signature { r: U256::from(1), s: U256::from(1), odd_y_parity: false }; - assert_eq!(27, signature.v(None)); - assert_eq!(37, signature.v(Some(1))); + fn r(&self) -> U256 { + self.r + } - let signature = Signature { r: U256::from(1), s: U256::from(1), odd_y_parity: true }; - assert_eq!(28, signature.v(None)); - assert_eq!(38, signature.v(Some(1))); + fn s(&self) -> U256 { + self.s + } + + fn v(&self) -> Parity { + self.parity + } + + fn with_parity>(self, parity: T) -> Self { + Self { r: self.r, s: self.s, parity: parity.into() } } +} + +#[cfg(test)] +mod tests { + use crate::{hex, transaction::signature::SECP256K1N_HALF, Address, Signature, B256, U256}; + use alloy_primitives::{hex::FromHex, Bytes, Parity}; + use std::str::FromStr; #[test] - fn test_encode_and_decode_with_eip155_chain_id() { + fn test_legacy_parity() { // Select 1 as an arbitrary nonzero value for R and S, as v() always returns 0 for (0, 0). let signature = Signature { r: U256::from(1), s: U256::from(1), odd_y_parity: false }; + assert_eq!(Parity::NonEip155(false), signature.legacy_parity(None)); + assert_eq!(Parity::Eip155(37), signature.legacy_parity(Some(1))); - let mut encoded = Vec::new(); - signature.encode_with_eip155_chain_id(&mut encoded, None); - assert_eq!(encoded.len(), signature.payload_len_with_eip155_chain_id(None)); - let (decoded, chain_id) = Signature::decode_with_eip155_chain_id(&mut &*encoded).unwrap(); - assert_eq!(signature, decoded); - assert_eq!(None, chain_id); - - let mut encoded = Vec::new(); - signature.encode_with_eip155_chain_id(&mut encoded, Some(1)); - assert_eq!(encoded.len(), signature.payload_len_with_eip155_chain_id(Some(1))); - let (decoded, chain_id) = Signature::decode_with_eip155_chain_id(&mut &*encoded).unwrap(); - assert_eq!(signature, decoded); - assert_eq!(Some(1), chain_id); + let signature = Signature { r: U256::from(1), s: U256::from(1), odd_y_parity: true }; + assert_eq!(Parity::NonEip155(true), signature.legacy_parity(None)); + assert_eq!(Parity::Eip155(38), signature.legacy_parity(Some(1))); } #[test] diff --git a/crates/rpc/rpc-types-compat/src/transaction/signature.rs b/crates/rpc/rpc-types-compat/src/transaction/signature.rs index 0e7fe5f5876a..fb41f517d533 100644 --- a/crates/rpc/rpc-types-compat/src/transaction/signature.rs +++ b/crates/rpc/rpc-types-compat/src/transaction/signature.rs @@ -13,7 +13,7 @@ pub(crate) fn from_legacy_primitive_signature( Signature { r: signature.r, s: signature.s, - v: U256::from(signature.v(chain_id)), + v: U256::from(signature.legacy_parity(chain_id).to_u64()), y_parity: None, } } diff --git a/crates/storage/codecs/Cargo.toml b/crates/storage/codecs/Cargo.toml index 7ecf75208ac1..13292c680c6f 100644 --- a/crates/storage/codecs/Cargo.toml +++ b/crates/storage/codecs/Cargo.toml @@ -31,7 +31,11 @@ alloy-eips = { workspace = true, default-features = false, features = [ "arbitrary", "serde", ] } -alloy-primitives = { workspace = true, features = ["arbitrary", "serde", "rand"] } +alloy-primitives = { workspace = true, features = [ + "arbitrary", + "serde", + "rand", +] } alloy-consensus = { workspace = true, features = ["arbitrary"] } test-fuzz.workspace = true serde_json.workspace = true diff --git a/crates/storage/codecs/derive/src/arbitrary.rs b/crates/storage/codecs/derive/src/arbitrary.rs index 4feae63c4f8f..8aa44062e21f 100644 --- a/crates/storage/codecs/derive/src/arbitrary.rs +++ b/crates/storage/codecs/derive/src/arbitrary.rs @@ -27,7 +27,7 @@ pub fn maybe_generate_tests( let mut buf = vec![]; let len = field.clone().to_compact(&mut buf); let (decoded, _): (super::#type_ident, _) = Compact::from_compact(&buf, len); - assert!(field == decoded, "maybe_generate_tests::compact"); + assert_eq!(field, decoded, "maybe_generate_tests::compact"); } }); } else if arg.to_string() == "rlp" { diff --git a/crates/storage/codecs/src/alloy/mod.rs b/crates/storage/codecs/src/alloy/mod.rs index 41a5ba20e2a9..efd76b1075dd 100644 --- a/crates/storage/codecs/src/alloy/mod.rs +++ b/crates/storage/codecs/src/alloy/mod.rs @@ -3,6 +3,7 @@ mod authorization_list; mod genesis_account; mod log; mod request; +mod transaction; mod trie; mod txkind; mod withdrawal; diff --git a/crates/storage/codecs/src/alloy/transaction.rs b/crates/storage/codecs/src/alloy/transaction.rs new file mode 100644 index 000000000000..389fef8ac2d3 --- /dev/null +++ b/crates/storage/codecs/src/alloy/transaction.rs @@ -0,0 +1,94 @@ +use crate::Compact; +use alloy_consensus::TxLegacy as AlloyTxLegacy; +use alloy_primitives::{Bytes, ChainId, TxKind, U256}; +use serde::{Deserialize, Serialize}; + +/// Legacy transaction. +#[derive(Debug, Clone, PartialEq, Eq, Default, Compact, Serialize, Deserialize)] +#[cfg_attr(test, derive(arbitrary::Arbitrary))] +#[cfg_attr(test, crate::add_arbitrary_tests(compact))] +struct TxLegacy { + /// Added as EIP-155: Simple replay attack protection + chain_id: Option, + /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + nonce: u64, + /// A scalar value equal to the number of + /// Wei to be paid per unit of gas for all computation + /// costs incurred as a result of the execution of this transaction; formally Tp. + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + gas_price: u128, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + gas_limit: u64, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + to: TxKind, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; formally Tv. + value: U256, + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). pub init: An unlimited size byte array specifying the + /// EVM-code for the account initialisation procedure CREATE, + /// data: An unlimited size byte array specifying the + /// input data of the message call, formally Td. + input: Bytes, +} + +impl Compact for AlloyTxLegacy { + fn to_compact(&self, buf: &mut B) -> usize + where + B: bytes::BufMut + AsMut<[u8]>, + { + let tx = TxLegacy { + chain_id: self.chain_id, + nonce: self.nonce, + gas_price: self.gas_price, + gas_limit: self.gas_limit as u64, + to: self.to, + value: self.value, + input: self.input.clone(), + }; + + tx.to_compact(buf) + } + + fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { + let (tx, _) = TxLegacy::from_compact(buf, len); + + let alloy_tx = Self { + chain_id: tx.chain_id, + nonce: tx.nonce, + gas_price: tx.gas_price, + gas_limit: tx.gas_limit as u128, + to: tx.to, + value: tx.value, + input: tx.input, + }; + + (alloy_tx, buf) + } +} + +#[cfg(test)] +mod tests { + use crate::alloy::transaction::TxLegacy; + + // each value in the database has an extra field named flags that encodes metadata about other + // fields in the value, e.g. offset and length. + // + // this check is to ensure we do not inadvertently add too many fields to a struct which would + // expand the flags field and break backwards compatibility + + #[test] + fn test_ensure_backwards_compatibility() { + assert_eq!(TxLegacy::bitflag_encoded_bytes(), 3); + } +} diff --git a/crates/storage/db-api/src/models/mod.rs b/crates/storage/db-api/src/models/mod.rs index c177ca80b33f..ede53d4b9bfe 100644 --- a/crates/storage/db-api/src/models/mod.rs +++ b/crates/storage/db-api/src/models/mod.rs @@ -303,7 +303,7 @@ mod tests { use super::*; use reth_primitives::{ Account, Header, Receipt, ReceiptWithBloom, SealedHeader, TxEip1559, TxEip2930, TxEip4844, - TxLegacy, Withdrawals, + Withdrawals, }; use reth_prune_types::{PruneCheckpoint, PruneMode, PruneSegment}; use reth_stages_types::{ @@ -346,7 +346,6 @@ mod tests { assert_eq!(TxEip1559::bitflag_encoded_bytes(), 4); assert_eq!(TxEip2930::bitflag_encoded_bytes(), 3); assert_eq!(TxEip4844::bitflag_encoded_bytes(), 5); - assert_eq!(TxLegacy::bitflag_encoded_bytes(), 3); assert_eq!(Withdrawals::bitflag_encoded_bytes(), 0); } @@ -379,7 +378,6 @@ mod tests { assert_eq!(TxEip1559::bitflag_encoded_bytes(), 4); assert_eq!(TxEip2930::bitflag_encoded_bytes(), 3); assert_eq!(TxEip4844::bitflag_encoded_bytes(), 5); - assert_eq!(TxLegacy::bitflag_encoded_bytes(), 3); assert_eq!(Withdrawals::bitflag_encoded_bytes(), 0); } } diff --git a/crates/transaction-pool/src/test_utils/gen.rs b/crates/transaction-pool/src/test_utils/gen.rs index 81314caa1bfb..c4069382d780 100644 --- a/crates/transaction-pool/src/test_utils/gen.rs +++ b/crates/transaction-pool/src/test_utils/gen.rs @@ -143,7 +143,7 @@ impl TransactionBuilder { TxLegacy { chain_id: Some(self.chain_id), nonce: self.nonce, - gas_limit: self.gas_limit, + gas_limit: self.gas_limit.into(), gas_price: self.max_fee_per_gas, to: self.to, value: self.value, diff --git a/crates/transaction-pool/src/test_utils/mock.rs b/crates/transaction-pool/src/test_utils/mock.rs index e7679eff888c..6a0c2f8b59a2 100644 --- a/crates/transaction-pool/src/test_utils/mock.rs +++ b/crates/transaction-pool/src/test_utils/mock.rs @@ -798,7 +798,7 @@ impl TryFrom for MockTransaction { sender, nonce, gas_price, - gas_limit, + gas_limit: gas_limit as u64, to, value, input, @@ -919,7 +919,15 @@ impl From for Transaction { value, input, size: _, - } => Self::Legacy(TxLegacy { chain_id, nonce, gas_price, gas_limit, to, value, input }), + } => Self::Legacy(TxLegacy { + chain_id, + nonce, + gas_price, + gas_limit: gas_limit.into(), + to, + value, + input, + }), MockTransaction::Eip2930 { chain_id, hash: _, @@ -1023,7 +1031,7 @@ impl proptest::arbitrary::Arbitrary for MockTransaction { hash: tx_hash, nonce: *nonce, gas_price: *gas_price, - gas_limit: *gas_limit, + gas_limit: *gas_limit as u64, to: *to, value: *value, input: input.clone(),