From 81bd5de1eed5808a504e75d8d5d1520f3b4ac556 Mon Sep 17 00:00:00 2001 From: evalir Date: Wed, 22 Nov 2023 20:59:18 -0400 Subject: [PATCH] feat(`rpc-types`): RLP encoding/decoding for transaction types (#36) * feat: add rlp encoding/decoding to tx types * feat: add encodable/decodable traits to tx * chore: remove out-of-scope func * chore: remove bad links on comments * chore: fix docs * clippy --- .../src/eth/transaction/access_list.rs | 26 +- crates/rpc-types/src/eth/transaction/mod.rs | 6 +- .../src/eth/transaction/signature.rs | 168 +++++- .../rpc-types/src/eth/transaction/tx_type.rs | 52 ++ crates/rpc-types/src/eth/transaction/typed.rs | 555 +++++++++++++++++- 5 files changed, 798 insertions(+), 9 deletions(-) create mode 100644 crates/rpc-types/src/eth/transaction/tx_type.rs diff --git a/crates/rpc-types/src/eth/transaction/access_list.rs b/crates/rpc-types/src/eth/transaction/access_list.rs index a59877ee87d..c69517809ac 100644 --- a/crates/rpc-types/src/eth/transaction/access_list.rs +++ b/crates/rpc-types/src/eth/transaction/access_list.rs @@ -1,9 +1,13 @@ +use std::mem; +use alloy_rlp::{RlpDecodable, RlpEncodable}; use alloy_primitives::{Address, U256, B256}; use serde::{Deserialize, Serialize}; /// A list of addresses and storage keys that the transaction plans to access. /// Accesses outside the list are possible, but become more expensive. -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Hash, Default)] +#[derive( + Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Hash, Default, RlpEncodable, RlpDecodable, +)] #[serde(rename_all = "camelCase")] pub struct AccessListItem { /// Account addresses that would be loaded at the start of execution @@ -12,8 +16,18 @@ pub struct AccessListItem { pub storage_keys: Vec, } +impl AccessListItem { + /// Calculates a heuristic for the in-memory size of the [AccessListItem]. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::
() + self.storage_keys.capacity() * mem::size_of::() + } +} + /// AccessList as defined in EIP-2930 -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Hash, Default)] +#[derive( + Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Hash, Default, RlpEncodable, RlpDecodable, +)] pub struct AccessList(pub Vec); impl AccessList { @@ -52,6 +66,14 @@ impl AccessList { ) }) } + + /// Calculates a heuristic for the in-memory size of the [AccessList]. + #[inline] + pub fn size(&self) -> usize { + // take into account capacity + self.0.iter().map(AccessListItem::size).sum::() + + self.0.capacity() * mem::size_of::() + } } /// Access list with gas used appended. diff --git a/crates/rpc-types/src/eth/transaction/mod.rs b/crates/rpc-types/src/eth/transaction/mod.rs index 937e0d3b0ff..6dfe965d83d 100644 --- a/crates/rpc-types/src/eth/transaction/mod.rs +++ b/crates/rpc-types/src/eth/transaction/mod.rs @@ -12,8 +12,11 @@ mod common; mod receipt; mod request; mod signature; +mod tx_type; mod typed; +pub use tx_type::*; + /// Transaction object used in RPC #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -66,9 +69,6 @@ pub struct Transaction { #[serde(skip_serializing_if = "Option::is_none")] pub access_list: Option>, /// EIP2718 - /// - /// Transaction type, Some(2) for EIP-1559 transaction, - /// Some(1) for AccessList transaction, None for Legacy #[serde(rename = "type", skip_serializing_if = "Option::is_none")] pub transaction_type: Option, } diff --git a/crates/rpc-types/src/eth/transaction/signature.rs b/crates/rpc-types/src/eth/transaction/signature.rs index 82866a639a6..481de806f34 100644 --- a/crates/rpc-types/src/eth/transaction/signature.rs +++ b/crates/rpc-types/src/eth/transaction/signature.rs @@ -1,5 +1,6 @@ //! Signature related RPC values use alloy_primitives::U256; +use alloy_rlp::{Bytes, Decodable, Encodable, Error as RlpError}; use serde::{Deserialize, Serialize}; /// Container type for all signature fields in RPC @@ -23,10 +24,119 @@ pub struct Signature { pub y_parity: Option, } +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 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 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] + pub fn v(&self, chain_id: Option) -> u64 { + if let Some(chain_id) = chain_id { + // EIP-155: v = {0, 1} + CHAIN_ID * 2 + 35 + let y_parity = u64::from(self.y_parity.unwrap_or(Parity(false))); + y_parity + chain_id * 2 + 35 + } else { + u64::from(self.y_parity.unwrap_or(Parity(false))) + 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 fn decode_with_eip155_chain_id(buf: &mut &[u8]) -> alloy_rlp::Result<(Self, Option)> { + let v = u64::decode(buf)?; + let r = Decodable::decode(buf)?; + let s = Decodable::decode(buf)?; + if v >= 35 { + // EIP-155: v = {0, 1} + CHAIN_ID * 2 + 35 + let y_parity = ((v - 35) % 2) != 0; + let chain_id = (v - 35) >> 1; + Ok(( + Signature { r, s, y_parity: Some(Parity(y_parity)), v: U256::from(v) }, + Some(chain_id), + )) + } else { + // non-EIP-155 legacy scheme, v = 27 for even y-parity, v = 28 for odd y-parity + if v != 27 && v != 28 { + return Err(RlpError::Custom("invalid Ethereum signature (V is not 27 or 28)")); + } + let y_parity = v == 28; + Ok((Signature { r, s, y_parity: Some(Parity(y_parity)), v: U256::from(v) }, None)) + } + } + + /// Output the length of the signature without the length of the RLP header + pub fn payload_len(&self) -> usize { + let y_parity_len = match self.y_parity { + Some(parity) => parity.0 as usize, + None => 0_usize, + }; + y_parity_len + self.r.length() + self.s.length() + } + + /// Encode the `y_parity`, `r`, `s` values without a RLP header. + /// Panics if the y parity is not set. + pub fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + self.y_parity.expect("y_parity not set").encode(out); + self.r.encode(out); + self.s.encode(out); + } + + /// Decodes the `y_parity`, `r`, `s` values without a RLP header. + pub fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let mut sig = + Signature { + y_parity: Some(Decodable::decode(buf)?), + r: Decodable::decode(buf)?, + s: Decodable::decode(buf)?, + v: U256::ZERO, + }; + sig.v = sig.y_parity.unwrap().into(); + Ok(sig) + } + + /// Turn this signature into its byte + /// (hex) representation. + /// Panics: if the y_parity field is not set. + pub fn to_bytes(&self) -> [u8; 65] { + let mut sig = [0u8; 65]; + sig[..32].copy_from_slice(&self.r.to_be_bytes::<32>()); + sig[32..64].copy_from_slice(&self.s.to_be_bytes::<32>()); + let v = u8::from(self.y_parity.expect("y_parity not set")) + 27; + sig[64] = v; + sig + } + + /// Turn this signature into its hex-encoded representation. + pub fn to_hex_bytes(&self) -> Bytes { + alloy_primitives::hex::encode(self.to_bytes()).into() + } + + /// Calculates a heuristic for the in-memory size of the [Signature]. + #[inline] + pub fn size(&self) -> usize { + std::mem::size_of::() + } +} + /// Type that represents the signature parity byte, meant for use in RPC. /// /// This will be serialized as "0x0" if false, and "0x1" if true. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Copy, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Parity( #[serde(serialize_with = "serialize_parity", deserialize_with = "deserialize_parity")] pub bool, ); @@ -37,6 +147,62 @@ impl From for Parity { } } +impl From for Parity { + fn from(value: U256) -> Self { + match value { + U256::ZERO => Self(false), + _ => Self(true), + } + } +} + +impl From for U256 { + fn from(p: Parity) -> Self { + if p.0 { + U256::from(1) + } else { + U256::ZERO + } + } +} + +impl From for u64 { + fn from(p: Parity) -> Self { + if p.0 { + 1 + } else { + 0 + } + } +} + +impl From for u8 { + fn from(value: Parity) -> Self { + match value.0 { + true => 1, + false => 0, + } + } +} + +impl Encodable for Parity { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + let v = u8::from(*self); + v.encode(out); + } + + fn length(&self) -> usize { + 1 + } +} + +impl Decodable for Parity { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let v = u8::decode(buf)?; + Ok(Self(v != 0)) + } +} + fn serialize_parity(parity: &bool, serializer: S) -> Result where S: serde::Serializer, diff --git a/crates/rpc-types/src/eth/transaction/tx_type.rs b/crates/rpc-types/src/eth/transaction/tx_type.rs new file mode 100644 index 00000000000..81f60b533e8 --- /dev/null +++ b/crates/rpc-types/src/eth/transaction/tx_type.rs @@ -0,0 +1,52 @@ +use alloy_primitives::U8; +use serde::{Deserialize, Serialize}; + +/// Identifier for legacy transaction, however a legacy tx is technically not +/// typed. +pub const LEGACY_TX_TYPE_ID: u8 = 0; + +/// Identifier for an EIP2930 transaction. +pub const EIP2930_TX_TYPE_ID: u8 = 1; + +/// Identifier for an EIP1559 transaction. +pub const EIP1559_TX_TYPE_ID: u8 = 2; + +/// Identifier for an EIP4844 transaction. +pub const EIP4844_TX_TYPE_ID: u8 = 3; + +/// Transaction Type +/// +/// Currently being used as 2-bit type when encoding it to Compact on +/// crate::TransactionSignedNoHash (see Reth's Compact encoding). Adding more transaction types will break the codec and +/// database format on Reth. +/// +/// Other required changes when adding a new type can be seen on [PR#3953](https://github.com/paradigmxyz/reth/pull/3953/files). +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)] +pub enum TxType { + /// Legacy transaction pre EIP-2929 + #[default] + Legacy = 0_isize, + /// AccessList transaction + EIP2930 = 1_isize, + /// Transaction with Priority fee + EIP1559 = 2_isize, + /// Shard Blob Transactions - EIP-4844 + EIP4844 = 3_isize, +} + +impl From for u8 { + fn from(value: TxType) -> Self { + match value { + TxType::Legacy => LEGACY_TX_TYPE_ID, + TxType::EIP2930 => EIP2930_TX_TYPE_ID, + TxType::EIP1559 => EIP1559_TX_TYPE_ID, + TxType::EIP4844 => EIP4844_TX_TYPE_ID, + } + } +} + +impl From for U8 { + fn from(value: TxType) -> Self { + U8::from(u8::from(value)) + } +} diff --git a/crates/rpc-types/src/eth/transaction/typed.rs b/crates/rpc-types/src/eth/transaction/typed.rs index e529d37b9f9..98c2cd4b99a 100644 --- a/crates/rpc-types/src/eth/transaction/typed.rs +++ b/crates/rpc-types/src/eth/transaction/typed.rs @@ -3,9 +3,11 @@ //! transaction deserialized from the json input of an RPC call. Depending on what fields are set, //! it can be converted into the container type [`TypedTransactionRequest`]. -use crate::eth::transaction::AccessList; -use alloy_primitives::{Address, Bytes, U128, U256, U64}; -use alloy_rlp::{BufMut, Decodable, Encodable, Error as RlpError}; +use std::{mem, cmp::Ordering}; + +use crate::{eth::transaction::AccessList, Signature, TxType}; +use alloy_primitives::{keccak256, Address, Bytes, B256, U128, U256, U64}; +use alloy_rlp::{bytes, length_of_length, BufMut, Decodable, Encodable, Error as RlpError, Header, EMPTY_LIST_CODE, Buf}; use serde::{Deserialize, Serialize}; /// Container type for various Ethereum transaction requests @@ -24,6 +26,76 @@ pub enum TypedTransactionRequest { EIP1559(EIP1559TransactionRequest), } +impl Encodable for TypedTransactionRequest { + fn encode(&self, out: &mut dyn BufMut) { + match self { + // Just encode as such + TypedTransactionRequest::Legacy(tx) => tx.encode(out), + // For EIP2930 and EIP1559 txs, we need to "envelop" the RLP encoding with the tx type. + // For EIP2930, it's 1. + TypedTransactionRequest::EIP2930(tx) => { + let id = 1_u8; + id.encode(out); + tx.encode(out) + }, + // For EIP1559, it's 2. + TypedTransactionRequest::EIP1559(tx) => { + let id = 2_u8; + id.encode(out); + tx.encode(out) + }, + } + } + + fn length(&self) -> usize { + match self { + TypedTransactionRequest::Legacy(tx) => tx.length(), + TypedTransactionRequest::EIP2930(tx) => tx.length(), + TypedTransactionRequest::EIP1559(tx) => tx.length(), + } + } +} + +impl Decodable for TypedTransactionRequest { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + // First, decode the tx type. + let tx_type = u8::decode(buf)?; + // Then, decode the tx based on the type. + match tx_type.cmp(&EMPTY_LIST_CODE) { + Ordering::Less => { + // strip out the string header + // NOTE: typed transaction encodings either contain a "rlp header" which contains + // the type of the payload and its length, or they do not contain a header and + // start with the tx type byte. + // + // This line works for both types of encodings because byte slices starting with + // 0x01 and 0x02 return a Header { list: false, payload_length: 1 } when input to + // Header::decode. + // If the encoding includes a header, the header will be properly decoded and + // consumed. + // Otherwise, header decoding will succeed but nothing is consumed. + let _header = Header::decode(buf)?; + let tx_type = *buf.first().ok_or(RlpError::Custom( + "typed tx cannot be decoded from an empty slice", + ))?; + if tx_type == 0x01 { + buf.advance(1); + EIP2930TransactionRequest::decode(buf) + .map(TypedTransactionRequest::EIP2930) + } else if tx_type == 0x02 { + buf.advance(1); + EIP1559TransactionRequest::decode(buf) + .map(TypedTransactionRequest::EIP1559) + } else { + Err(RlpError::Custom("invalid tx type")) + } + }, + Ordering::Equal => Err(RlpError::Custom("an empty list is not a valid transaction encoding")), + Ordering::Greater => LegacyTransactionRequest::decode(buf).map(TypedTransactionRequest::Legacy), + } + } +} + /// Represents a legacy transaction request #[derive(Debug, Clone, PartialEq, Eq)] pub struct LegacyTransactionRequest { @@ -36,6 +108,131 @@ pub struct LegacyTransactionRequest { pub chain_id: Option, } +impl Encodable for LegacyTransactionRequest { + fn encode(&self, out: &mut dyn BufMut) { + self.nonce.encode(out); + self.gas_price.encode(out); + self.gas_limit.encode(out); + self.kind.encode(out); + self.value.encode(out); + self.input.0.encode(out); + } + + fn length(&self) -> usize { + self.nonce.length() + + self.gas_price.length() + + self.gas_limit.length() + + self.kind.length() + + self.value.length() + + self.input.0.length() + } +} + +impl Decodable for LegacyTransactionRequest { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + nonce: Decodable::decode(buf)?, + gas_price: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + kind: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + chain_id: None, + }) + } +} + +impl LegacyTransactionRequest { + /// Calculates a heuristic for the in-memory size of the [LegacyTransactionRequest] 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.kind.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 fn fields_len(&self) -> usize { + let mut len = 0; + len += self.nonce.length(); + len += self.gas_price.length(); + len += self.gas_limit.length(); + len += self.kind.length(); + len += self.value.length(); + len += self.input.0.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header or + /// eip155 fields. + pub fn encode_fields(&self, out: &mut dyn bytes::BufMut) { + self.nonce.encode(out); + self.gas_price.encode(out); + self.gas_limit.encode(out); + self.kind.encode(out); + self.value.encode(out); + self.input.0.encode(out); + } + + /// Outputs the length of EIP-155 fields. Only outputs a non-zero value for EIP-155 legacy + /// transactions. + pub 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 EIP-155 arguments into the desired buffer. Only encodes values for legacy + /// transactions. + pub 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); + } + } + + /// Encodes the legacy transaction in RLP for signing, including the EIP-155 fields if possible. + pub 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 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 fn signature_hash(&self) -> B256 { + let mut buf = bytes::BytesMut::with_capacity(self.payload_len_for_signature()); + self.encode_for_signing(&mut buf); + keccak256(&buf) + } +} + /// Represents an EIP-2930 transaction request #[derive(Debug, Clone, PartialEq, Eq)] pub struct EIP2930TransactionRequest { @@ -49,6 +246,175 @@ pub struct EIP2930TransactionRequest { pub access_list: AccessList, } +impl Encodable for EIP2930TransactionRequest { + fn encode(&self, out: &mut dyn BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.gas_price.encode(out); + self.gas_limit.encode(out); + self.kind.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + } + + fn length(&self) -> usize { + self.chain_id.length() + + self.nonce.length() + + self.gas_price.length() + + self.gas_limit.length() + + self.kind.length() + + self.value.length() + + self.input.0.length() + + self.access_list.length() + } +} + +impl Decodable for EIP2930TransactionRequest { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + gas_price: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + kind: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + }) + } +} + +impl EIP2930TransactionRequest { + /// Calculates a heuristic for the in-memory size of the 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.kind.size() + // to + mem::size_of::() + // value + self.access_list.size() + // access_list + self.input.len() // input + } + + /// Decodes the inner fields from RLP bytes. + /// + /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following + /// RLP fields in the following order: + /// + /// - `chain_id` + /// - `nonce` + /// - `gas_price` + /// - `gas_limit` + /// - `to` + /// - `value` + /// - `data` (`input`) + /// - `access_list` + pub fn decode_inner(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + gas_price: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + kind: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + }) + } + + /// Outputs the length of the transaction's fields, without a RLP header. + pub fn fields_len(&self) -> usize { + let mut len = 0; + len += self.chain_id.length(); + len += self.nonce.length(); + len += self.gas_price.length(); + len += self.gas_limit.length(); + len += self.kind.length(); + len += self.value.length(); + len += self.input.0.length(); + len += self.access_list.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub fn encode_fields(&self, out: &mut dyn bytes::BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.gas_price.encode(out); + self.gas_limit.encode(out); + self.kind.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + } + + /// 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_with_signature( + &self, + signature: &Signature, + out: &mut dyn bytes::BufMut, + with_header: bool, + ) { + let payload_length = self.fields_len() + signature.payload_len(); + if with_header { + Header { + list: false, + payload_length: 1 + length_of_length(payload_length) + payload_length, + } + .encode(out); + } + out.put_u8(self.tx_type() as u8); + let header = Header { list: true, payload_length }; + header.encode(out); + self.encode_fields(out); + signature.encode(out); + } + + /// Output the length of the RLP signed transaction encoding, _without_ a RLP string header. + pub fn payload_len_with_signature_without_header(&self, signature: &Signature) -> usize { + let payload_length = self.fields_len() + signature.payload_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. + pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { + let len = self.payload_len_with_signature_without_header(signature); + length_of_length(len) + len + } + + /// Get transaction type + pub fn tx_type(&self) -> TxType { + TxType::EIP2930 + } + + /// Encodes the EIP-2930 transaction in RLP for signing. + pub fn encode_for_signing(&self, out: &mut dyn bytes::BufMut) { + out.put_u8(self.tx_type() as u8); + Header { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); + } + + /// Outputs the length of the signature RLP encoding for the transaction. + pub fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Outputs the signature hash of the transaction by first encoding without a signature, then + /// hashing. + pub fn signature_hash(&self) -> B256 { + let mut buf = bytes::BytesMut::with_capacity(self.payload_len_for_signature()); + self.encode_for_signing(&mut buf); + keccak256(&buf) + } +} + /// Represents an EIP-1559 transaction request #[derive(Debug, Clone, PartialEq, Eq)] pub struct EIP1559TransactionRequest { @@ -63,6 +429,183 @@ pub struct EIP1559TransactionRequest { pub access_list: AccessList, } +impl Encodable for EIP1559TransactionRequest { + fn encode(&self, out: &mut dyn BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.max_priority_fee_per_gas.encode(out); + self.max_fee_per_gas.encode(out); + self.gas_limit.encode(out); + self.kind.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + } + + fn length(&self) -> usize { + self.chain_id.length() + + self.nonce.length() + + self.max_priority_fee_per_gas.length() + + self.max_fee_per_gas.length() + + self.gas_limit.length() + + self.kind.length() + + self.value.length() + + self.input.0.length() + + self.access_list.length() + } +} + +impl Decodable for EIP1559TransactionRequest { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + max_priority_fee_per_gas: Decodable::decode(buf)?, + max_fee_per_gas: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + kind: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + }) + } +} + +impl EIP1559TransactionRequest { + /// Decodes the inner fields from RLP bytes. + /// + /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following + /// RLP fields in the following order: + /// + /// - `chain_id` + /// - `nonce` + /// - `max_priority_fee_per_gas` + /// - `max_fee_per_gas` + /// - `gas_limit` + /// - `to` + /// - `value` + /// - `data` (`input`) + /// - `access_list` + pub fn decode_inner(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + max_priority_fee_per_gas: Decodable::decode(buf)?, + max_fee_per_gas: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + kind: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + }) + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub fn fields_len(&self) -> usize { + let mut len = 0; + len += self.chain_id.length(); + len += self.nonce.length(); + len += self.max_priority_fee_per_gas.length(); + len += self.max_fee_per_gas.length(); + len += self.gas_limit.length(); + len += self.kind.length(); + len += self.value.length(); + len += self.input.0.length(); + len += self.access_list.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub fn encode_fields(&self, out: &mut dyn bytes::BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.max_priority_fee_per_gas.encode(out); + self.max_fee_per_gas.encode(out); + self.gas_limit.encode(out); + self.kind.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + } + + /// 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_with_signature( + &self, + signature: &Signature, + out: &mut dyn bytes::BufMut, + with_header: bool, + ) { + let payload_length = self.fields_len() + signature.payload_len(); + if with_header { + Header { + list: false, + payload_length: 1 + length_of_length(payload_length) + payload_length, + } + .encode(out); + } + out.put_u8(self.tx_type() as u8); + let header = Header { list: true, payload_length }; + header.encode(out); + self.encode_fields(out); + signature.encode(out); + } + + /// Output the length of the RLP signed transaction encoding, _without_ a RLP string header. + pub fn payload_len_with_signature_without_header(&self, signature: &Signature) -> usize { + let payload_length = self.fields_len() + signature.payload_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. + pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { + let len = self.payload_len_with_signature_without_header(signature); + length_of_length(len) + len + } + + /// Get transaction type + pub fn tx_type(&self) -> TxType { + TxType::EIP1559 + } + + /// Calculates a heuristic for the in-memory size of the transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_limit + mem::size_of::() + // max_fee_per_gas + mem::size_of::() + // max_priority_fee_per_gas + self.kind.size() + // to + mem::size_of::() + // value + self.access_list.size() + // access_list + self.input.len() // input + } + + /// Encodes the legacy transaction in RLP for signing. + pub fn encode_for_signing(&self, out: &mut dyn bytes::BufMut) { + out.put_u8(self.tx_type() as u8); + Header { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); + } + + /// Outputs the length of the signature RLP encoding for the transaction. + pub fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Outputs the signature hash of the transaction by first encoding without a signature, then + /// hashing. + pub fn signature_hash(&self) -> B256 { + let mut buf = bytes::BytesMut::with_capacity(self.payload_len_for_signature()); + self.encode_for_signing(&mut buf); + keccak256(&buf) + } +} + /// Represents the `to` field of a transaction request /// /// This determines what kind of transaction this is @@ -84,6 +627,12 @@ impl TransactionKind { TransactionKind::Create => None, } } + + /// Calculates a heuristic for the in-memory size of the [TransactionKind]. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + } } impl Encodable for TransactionKind {