Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: serde for consensus tx types #361

Merged
merged 14 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ tempfile = "3.10"

# TODO: Remove eventually.
[patch.crates-io]
alloy-sol-macro = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" }
alloy-primitives = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" }
alloy-sol-types = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" }
alloy-json-abi = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" }
alloy-dyn-abi = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" }
syn-solidity = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" }
alloy-core = { git = "https://github.com/alloy-rs/core", rev = "1bac7678797fcd1bee2f2580825724b4165b12c1" }
alloy-sol-macro = { git = "https://github.com/alloy-rs/core", rev = "ff33969" }
alloy-primitives = { git = "https://github.com/alloy-rs/core", rev = "ff33969" }
alloy-sol-types = { git = "https://github.com/alloy-rs/core", rev = "ff33969" }
alloy-json-abi = { git = "https://github.com/alloy-rs/core", rev = "ff33969" }
alloy-dyn-abi = { git = "https://github.com/alloy-rs/core", rev = "ff33969" }
syn-solidity = { git = "https://github.com/alloy-rs/core", rev = "ff33969" }
alloy-core = { git = "https://github.com/alloy-rs/core", rev = "ff33969" }
6 changes: 6 additions & 0 deletions crates/consensus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ exclude.workspace = true
alloy-primitives = { workspace = true, features = ["rlp"] }
alloy-rlp.workspace = true
alloy-eips.workspace = true
alloy-serde = { workspace = true, optional = true }
gakonst marked this conversation as resolved.
Show resolved Hide resolved

sha2 = "0.10"

Expand All @@ -25,13 +26,18 @@ c-kzg = { workspace = true, features = ["std", "serde"], optional = true }
# arbitrary
arbitrary = { workspace = true, features = ["derive"], optional = true }

# serde
serde = { workspace = true, features = ["derive"], optional = true }

[dev-dependencies]
alloy-signer.workspace = true
arbitrary = { workspace = true, features = ["derive"] }
k256.workspace = true
tokio = { workspace = true, features = ["macros"] }
serde_json.workspace = true

[features]
k256 = ["alloy-primitives/k256"]
kzg = ["dep:c-kzg", "dep:thiserror", "alloy-eips/kzg"]
arbitrary = ["dep:arbitrary", "alloy-eips/arbitrary"]
serde = ["dep:serde", "alloy-primitives/serde", "dep:alloy-serde", "alloy-eips/serde"]
3 changes: 3 additions & 0 deletions crates/consensus/src/signed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ use alloy_primitives::{Signature, B256};

/// A transaction with a signature and hash seal.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there followup work to dedupe the rpc_types signature here?

pub struct Signed<T, Sig = Signature> {
#[cfg_attr(feature = "serde", serde(flatten))]
tx: T,
#[cfg_attr(feature = "serde", serde(flatten))]
signature: Sig,
hash: B256,
}
Expand Down
10 changes: 9 additions & 1 deletion crates/consensus/src/transaction/eip1559.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ use std::mem;

/// A transaction with a priority fee ([EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct TxEip1559 {
/// EIP-155: Simple replay attack protection
pub chain_id: u64,
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))]
pub chain_id: ChainId,
/// A scalar value equal to the number of transactions sent by the sender; formally Tn.
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))]
pub nonce: u64,
/// 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.
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))]
pub gas_limit: u64,
/// A scalar value equal to the maximum
/// amount of gas that should be used in executing
Expand All @@ -28,6 +33,7 @@ pub struct TxEip1559 {
/// 340282366920938463463374607431768211455
///
/// This is also known as `GasFeeCap`
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_hex_or_decimal"))]
pub max_fee_per_gas: u128,
/// Max Priority fee that transaction is paying
///
Expand All @@ -36,9 +42,11 @@ pub struct TxEip1559 {
/// 340282366920938463463374607431768211455
///
/// This is also known as `GasTipCap`
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_hex_or_decimal"))]
pub max_priority_fee_per_gas: u128,
/// 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.
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "TxKind::is_create"))]
pub to: TxKind,
mattsse marked this conversation as resolved.
Show resolved Hide resolved
/// A scalar value equal to the number of Wei to
/// be transferred to the message call’s recipient or,
Expand Down
7 changes: 7 additions & 0 deletions crates/consensus/src/transaction/eip2930.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ use std::mem;

/// Transaction with an [`AccessList`] ([EIP-2930](https://eips.ethereum.org/EIPS/eip-2930)).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct TxEip2930 {
/// Added as EIP-pub 155: Simple replay attack protection
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))]
pub chain_id: ChainId,
/// A scalar value equal to the number of transactions sent by the sender; formally Tn.
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))]
pub nonce: u64,
/// A scalar value equal to the number of
/// Wei to be paid per unit of gas for all computation
Expand All @@ -18,15 +22,18 @@ pub struct TxEip2930 {
/// 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
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_hex_or_decimal"))]
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.
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))]
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.
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "TxKind::is_create"))]
pub to: TxKind,
/// A scalar value equal to the number of Wei to
/// be transferred to the message call’s recipient or,
Expand Down
41 changes: 41 additions & 0 deletions crates/consensus/src/transaction/eip4844.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,41 @@ pub enum BlobTransactionValidationError {
/// or a transaction with a sidecar, which is used when submitting a transaction to the network and
/// when receiving and sending transactions during the gossip stage.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
prestwich marked this conversation as resolved.
Show resolved Hide resolved
pub enum TxEip4844Variant {
/// A standalone transaction with blob hashes and max blob fee.
TxEip4844(TxEip4844),
/// A transaction with a sidecar, which contains the blob data, commitments, and proofs.
TxEip4844WithSidecar(TxEip4844WithSidecar),
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for TxEip4844Variant {
fn deserialize<D>(deserializer: D) -> Result<TxEip4844Variant, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct TxEip4844SerdeHelper {
gakonst marked this conversation as resolved.
Show resolved Hide resolved
#[serde(flatten)]
tx: TxEip4844,
#[serde(flatten)]
sidecar: Option<BlobTransactionSidecar>,
}

let tx = TxEip4844SerdeHelper::deserialize(deserializer)?;

if let Some(sidecar) = tx.sidecar {
Ok(TxEip4844Variant::TxEip4844WithSidecar(TxEip4844WithSidecar::from_tx_and_sidecar(
tx.tx, sidecar,
)))
} else {
Ok(TxEip4844Variant::TxEip4844(tx.tx))
}
}
}

impl From<TxEip4844WithSidecar> for TxEip4844Variant {
fn from(tx: TxEip4844WithSidecar) -> Self {
TxEip4844Variant::TxEip4844WithSidecar(tx)
Expand Down Expand Up @@ -286,16 +314,21 @@ impl SignableTransaction<Signature> for TxEip4844Variant {
///
/// A transaction with blob hashes and max blob fee. It does not have the Blob sidecar.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct TxEip4844 {
/// Added as EIP-pub 155: Simple replay attack protection
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))]
pub chain_id: ChainId,
/// A scalar value equal to the number of transactions sent by the sender; formally Tn.
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))]
pub nonce: u64,
/// 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.
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u64_hex_or_decimal"))]
pub gas_limit: u64,
/// A scalar value equal to the maximum
/// amount of gas that should be used in executing
Expand All @@ -308,6 +341,7 @@ pub struct TxEip4844 {
/// 340282366920938463463374607431768211455
///
/// This is also known as `GasFeeCap`
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_hex_or_decimal"))]
pub max_fee_per_gas: u128,
/// Max Priority fee that transaction is paying
///
Expand All @@ -316,6 +350,7 @@ pub struct TxEip4844 {
/// 340282366920938463463374607431768211455
///
/// This is also known as `GasTipCap`
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_hex_or_decimal"))]
pub max_priority_fee_per_gas: u128,
/// The 160-bit address of the message call’s recipient.
pub to: Address,
Expand All @@ -337,6 +372,7 @@ pub struct TxEip4844 {
/// Max fee per data gas
///
/// aka BlobFeeCap or blobGasFeeCap
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::u128_hex_or_decimal"))]
pub max_fee_per_blob_gas: u128,

/// Input has two uses depending if transaction is Create or Call (if `to` field is None or
Expand Down Expand Up @@ -719,10 +755,14 @@ impl Decodable for TxEip4844 {
/// of a `PooledTransactions` response, and is also used as the format for sending raw transactions
/// through the network (eth_sendRawTransaction/eth_sendTransaction).
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct TxEip4844WithSidecar {
/// The actual transaction.
#[cfg_attr(feature = "serde", serde(flatten))]
pub tx: TxEip4844,
/// The sidecar.
#[cfg_attr(feature = "serde", serde(flatten))]
pub sidecar: BlobTransactionSidecar,
mattsse marked this conversation as resolved.
Show resolved Hide resolved
}

Expand Down Expand Up @@ -914,6 +954,7 @@ impl Transaction for TxEip4844WithSidecar {
/// This represents a set of blobs, and its corresponding commitments and proofs.
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
#[repr(C)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Copy link
Member

@prestwich prestwich Mar 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we proactively dedupe this as part of this PR?

current code in RPC types is as follows

/// This represents a set of blobs, and its corresponding commitments and proofs.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[repr(C)]
pub struct BlobTransactionSidecar {
    /// The blob data.
    pub blobs: Vec<Blob>,
    /// The blob commitments.
    pub commitments: Vec<Bytes48>,
    /// The blob proofs.
    pub proofs: Vec<Bytes48>,
}

Copy link
Member Author

@klkvr klkvr Mar 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prestwich rpc-engine-types relies on Blob and Bytes48 having ssz support and c-kzg types does not have it, what's the desired way to handle this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manually impl ssz trait with a helper struct? If it's getting that complicated, it should go in a separate PR though

pub struct BlobTransactionSidecar {
/// The blob data.
pub blobs: Vec<Blob>,
Expand Down
116 changes: 116 additions & 0 deletions crates/consensus/src/transaction/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,17 @@ impl TryFrom<u8> for TxType {
///
/// [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"))]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if we want the type as tag here,
I think for readability this makes sense, and untagged could be a mess
wdyt @prestwich

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that type may be omitted for legacy txns, and any objects generated in the past and stored somewhere will definitely have type omitted. So this would prevent us from handling old receipts at the very least

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prestwich so those should be untagged instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally we would write custom impls here I think? but I don't feel super strongly about it. can you check ethers-rs and its issues for how we used to do this? as long as we're not worse than ethers, we should be fine to move forward

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any issues related to this type tag, likely because ethers users couldn't really have issues with deserializing txses serialized by ethers itself. And deserialization of external JSON-encoded transactions was likely done through Transaction which is more like an RPC type without much restrictions.

imo ideal impl here would be the one which is able to deserialize any valid eth_getTransactionByHash response into TxEnvelope and per execution spec all RPC responses must include "type"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thin i'm comfortable leaving as typed, as supporting outdated nodes or long-term stored responses is not a compelling usecase.

imo ideal impl here would be the one which is able to deserialize any valid eth_getTransactionByHash response into TxEnvelope and per execution spec all RPC responses must include "type"

agree, tho as stated elsewhere, i think the "right" way to achieve this is by embedding TxEnvelope in the transaction rpc response type

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, tho as stated elsewhere, i think the "right" way to achieve this is by embedding TxEnvelope in the transaction rpc response type

perhaps this is the next step once network abstraction is generic enough to allow us such strict constraints on RPC responses without breaking stuff for projects using Ethereum network for everything?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the way to enforce it as a bound on network (if we even want to do that) would be to have type TransactionResponse: RpcObject + AsRef<Self::TxEnvelope>, basically. That would ensure that any response object contained an envelope

I think that the longer we go without embedding consensus objects in the RPC types, the worse of a time we'll have making the change

My primary target list is

  • Log
  • TxEnvelope
  • Receipt

pub enum TxEnvelope {
/// An untagged [`TxLegacy`].
#[cfg_attr(feature = "serde", serde(rename = "0x00", alias = "0x0"))]
Legacy(Signed<TxLegacy>),
/// A [`TxEip2930`] tagged with type 1.
#[cfg_attr(feature = "serde", serde(rename = "0x01", alias = "0x1"))]
Eip2930(Signed<TxEip2930>),
/// A [`TxEip1559`] tagged with type 2.
#[cfg_attr(feature = "serde", serde(rename = "0x02", alias = "0x2"))]
Eip1559(Signed<TxEip1559>),
/// A TxEip4844 tagged with type 3.
/// An EIP-4844 transaction has two network representations:
Expand All @@ -75,6 +80,7 @@ pub enum TxEnvelope {
///
/// 2 - The transaction with a sidecar, which is the form used to
/// send transactions to the network.
#[cfg_attr(feature = "serde", serde(rename = "0x03", alias = "0x3"))]
Eip4844(Signed<TxEip4844Variant>),
}

Expand Down Expand Up @@ -398,4 +404,114 @@ mod tests {
assert_eq!(encoded, hex_data);
assert_eq!(tx.encode_2718_len(), hex_data.len());
}

#[cfg(feature = "serde")]
fn test_serde_roundtrip<T: SignableTransaction<Signature>>(tx: T)
where
Signed<T>: Into<TxEnvelope>,
{
let signature = Signature::test_signature();
let tx_envelope: TxEnvelope = tx.into_signed(signature).into();

let serialized = serde_json::to_string(&tx_envelope).unwrap();
let deserialized: TxEnvelope = serde_json::from_str(&serialized).unwrap();

assert_eq!(tx_envelope, deserialized);
}

#[test]
#[cfg(feature = "serde")]
fn test_serde_roundtrip_legacy() {
let tx = TxLegacy {
chain_id: Some(1),
nonce: 100,
gas_price: 3_000_000_000,
gas_limit: 50_000,
to: TxKind::Call(Address::default()),
value: U256::from(10e18),
input: Bytes::new(),
};
test_serde_roundtrip(tx);
}

#[test]
#[cfg(feature = "serde")]
fn test_serde_roundtrip_eip1559() {
let tx = TxEip1559 {
chain_id: 1,
nonce: 100,
max_fee_per_gas: 50_000_000_000,
max_priority_fee_per_gas: 1_000_000_000_000,
gas_limit: 1_000_000,
to: TxKind::Create,
value: U256::from(10e18),
input: Bytes::new(),
access_list: AccessList(vec![AccessListItem {
address: Address::random(),
storage_keys: vec![B256::random()],
}]),
};
test_serde_roundtrip(tx);
}

#[test]
#[cfg(feature = "serde")]
fn test_serde_roundtrip_eip2930() {
let tx = TxEip2930 {
chain_id: u64::MAX,
nonce: u64::MAX,
gas_price: u128::MAX,
gas_limit: u64::MAX,
to: TxKind::Call(Address::random()),
value: U256::MAX,
input: Bytes::new(),
access_list: Default::default(),
};
test_serde_roundtrip(tx);
}

#[test]
#[cfg(feature = "serde")]
fn test_serde_roundtrip_eip4844() {
use crate::BlobTransactionSidecar;

let tx = TxEip4844Variant::TxEip4844(TxEip4844 {
chain_id: 1,
nonce: 100,
max_fee_per_gas: 50_000_000_000,
max_priority_fee_per_gas: 1_000_000_000_000,
gas_limit: 1_000_000,
to: Address::random(),
value: U256::from(10e18),
input: Bytes::new(),
access_list: AccessList(vec![AccessListItem {
address: Address::random(),
storage_keys: vec![B256::random()],
}]),
blob_versioned_hashes: vec![B256::random()],
max_fee_per_blob_gas: 0,
});
test_serde_roundtrip(tx);

let tx = TxEip4844Variant::TxEip4844WithSidecar(TxEip4844WithSidecar {
tx: TxEip4844 {
chain_id: 1,
nonce: 100,
max_fee_per_gas: 50_000_000_000,
max_priority_fee_per_gas: 1_000_000_000_000,
gas_limit: 1_000_000,
to: Address::random(),
value: U256::from(10e18),
input: Bytes::new(),
access_list: AccessList(vec![AccessListItem {
address: Address::random(),
storage_keys: vec![B256::random()],
}]),
blob_versioned_hashes: vec![B256::random()],
max_fee_per_blob_gas: 0,
},
sidecar: BlobTransactionSidecar { ..Default::default() },
});
test_serde_roundtrip(tx);
}
}
Loading
Loading