Skip to content

Commit

Permalink
feat(rpc-types): RLP encoding/decoding for transaction types (#36)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Evalir authored Nov 23, 2023
1 parent f9d823e commit 81bd5de
Show file tree
Hide file tree
Showing 5 changed files with 798 additions and 9 deletions.
26 changes: 24 additions & 2 deletions crates/rpc-types/src/eth/transaction/access_list.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,8 +16,18 @@ pub struct AccessListItem {
pub storage_keys: Vec<B256>,
}

impl AccessListItem {
/// Calculates a heuristic for the in-memory size of the [AccessListItem].
#[inline]
pub fn size(&self) -> usize {
mem::size_of::<Address>() + self.storage_keys.capacity() * mem::size_of::<U256>()
}
}

/// 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<AccessListItem>);

impl AccessList {
Expand Down Expand Up @@ -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::<usize>()
+ self.0.capacity() * mem::size_of::<AccessListItem>()
}
}

/// Access list with gas used appended.
Expand Down
6 changes: 3 additions & 3 deletions crates/rpc-types/src/eth/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -66,9 +69,6 @@ pub struct Transaction {
#[serde(skip_serializing_if = "Option::is_none")]
pub access_list: Option<Vec<AccessListItem>>,
/// 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<U64>,
}
Expand Down
168 changes: 167 additions & 1 deletion crates/rpc-types/src/eth/transaction/signature.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -23,10 +24,119 @@ pub struct Signature {
pub y_parity: Option<Parity>,
}

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<u64>) -> 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<u64>,
) {
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>) -> 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<u64>)> {
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<Self> {
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::<Self>()
}
}

/// 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,
);
Expand All @@ -37,6 +147,62 @@ impl From<bool> for Parity {
}
}

impl From<U256> for Parity {
fn from(value: U256) -> Self {
match value {
U256::ZERO => Self(false),
_ => Self(true),
}
}
}

impl From<Parity> for U256 {
fn from(p: Parity) -> Self {
if p.0 {
U256::from(1)
} else {
U256::ZERO
}
}
}

impl From<Parity> for u64 {
fn from(p: Parity) -> Self {
if p.0 {
1
} else {
0
}
}
}

impl From<Parity> 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<Self> {
let v = u8::decode(buf)?;
Ok(Self(v != 0))
}
}

fn serialize_parity<S>(parity: &bool, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
Expand Down
52 changes: 52 additions & 0 deletions crates/rpc-types/src/eth/transaction/tx_type.rs
Original file line number Diff line number Diff line change
@@ -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<TxType> 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<TxType> for U8 {
fn from(value: TxType) -> Self {
U8::from(u8::from(value))
}
}
Loading

0 comments on commit 81bd5de

Please sign in to comment.