Skip to content

Commit

Permalink
feat(consensus): bincode compatibility for header and transaction typ…
Browse files Browse the repository at this point in the history
…es (#1397)

* fix(serde): encode optional quantity as Some

* use serialize_some

* fix in other types too

* feat(consensus): bincode compatibility for header and transaction types

* add docs

* reorganize modules

* add legacy bincode compat impl, remove comments

* less cows

* fix doctest for legacy tx

* rename feature

* more info about limitation

* remove special treatment for quantity fields
  • Loading branch information
shekhirin authored Sep 30, 2024
1 parent 32edd0c commit 9f18b7a
Show file tree
Hide file tree
Showing 7 changed files with 552 additions and 7 deletions.
10 changes: 7 additions & 3 deletions crates/consensus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ rustdoc-args = ["--cfg", "docsrs"]
workspace = true

[dependencies]
alloy-eips = { workspace = true, features = ["kzg-sidecar"] }
alloy-primitives = { workspace = true, features = ["rlp"] }
alloy-rlp.workspace = true
alloy-eips = { workspace = true, features = ["kzg-sidecar"] }
alloy-serde = { workspace = true, optional = true }

# kzg
Expand All @@ -32,6 +32,7 @@ arbitrary = { workspace = true, features = ["derive"], optional = true }

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

# misc
derive_more = { workspace = true, features = [
Expand All @@ -43,14 +44,16 @@ derive_more = { workspace = true, features = [
auto_impl.workspace = true

[dev-dependencies]
alloy-primitives = { workspace = true, features = ["arbitrary", "rand"] }
alloy-eips = { workspace = true, features = ["arbitrary"] }
alloy-primitives = { workspace = true, features = ["arbitrary", "rand"] }
alloy-signer.workspace = true

arbitrary = { workspace = true, features = ["derive"] }
bincode = "1.3"
k256.workspace = true
tokio = { workspace = true, features = ["macros"] }
rand.workspace = true
serde_json.workspace = true
tokio = { workspace = true, features = ["macros"] }

[features]
default = ["std"]
Expand All @@ -64,3 +67,4 @@ serde = [
"dep:alloy-serde",
"alloy-eips/serde",
]
serde-bincode-compat = ["serde_with"]
169 changes: 165 additions & 4 deletions crates/consensus/src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -793,18 +793,18 @@ mod tests {
use super::*;

#[test]
fn header_serde() {
fn test_header_serde_json_roundtrip() {
let raw = r#"{"parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000","ommersHash":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","beneficiary":"0x0000000000000000000000000000000000000000","stateRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","withdrawalsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","difficulty":"0x0","number":"0x0","gasLimit":"0x0","gasUsed":"0x0","timestamp":"0x0","mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0000000000000000","baseFeePerGas":"0x1","extraData":"0x"}"#;
let header = Header {
base_fee_per_gas: Some(1),
withdrawals_root: Some(EMPTY_ROOT_HASH),
..Default::default()
};

let json = serde_json::to_string(&header).unwrap();
assert_eq!(json, raw);
let encoded = serde_json::to_string(&header).unwrap();
assert_eq!(encoded, raw);

let decoded: Header = serde_json::from_str(&json).unwrap();
let decoded: Header = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded, header);

// Create a vector to store the encoded RLP
Expand All @@ -820,3 +820,164 @@ mod tests {
assert_eq!(decoded_rlp, decoded);
}
}

/// Bincode-compatibl [`Header`] serde implementation.
#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
pub(super) mod serde_bincode_compat {
use alloc::borrow::Cow;
use alloy_primitives::{Address, BlockNumber, Bloom, Bytes, B256, B64, U256};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::{DeserializeAs, SerializeAs};

/// Bincode-compatible [`super::Header`] serde implementation.
///
/// Intended to use with the [`serde_with::serde_as`] macro in the following way:
/// ```rust
/// use alloy_consensus::{serde_bincode_compat, Header};
/// use serde::{Deserialize, Serialize};
/// use serde_with::serde_as;
///
/// #[serde_as]
/// #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
/// struct Data {
/// #[serde_as(as = "serde_bincode_compat::Header")]
/// header: Header,
/// }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Header<'a> {
parent_hash: B256,
ommers_hash: B256,
beneficiary: Address,
state_root: B256,
transactions_root: B256,
receipts_root: B256,
#[serde(default)]
withdrawals_root: Option<B256>,
logs_bloom: Bloom,
difficulty: U256,
number: BlockNumber,
gas_limit: u64,
gas_used: u64,
timestamp: u64,
mix_hash: B256,
nonce: B64,
#[serde(default)]
base_fee_per_gas: Option<u64>,
#[serde(default)]
blob_gas_used: Option<u64>,
#[serde(default)]
excess_blob_gas: Option<u64>,
#[serde(default)]
parent_beacon_block_root: Option<B256>,
#[serde(default)]
requests_root: Option<B256>,
extra_data: Cow<'a, Bytes>,
}

impl<'a> From<&'a super::Header> for Header<'a> {
fn from(value: &'a super::Header) -> Self {
Self {
parent_hash: value.parent_hash,
ommers_hash: value.ommers_hash,
beneficiary: value.beneficiary,
state_root: value.state_root,
transactions_root: value.transactions_root,
receipts_root: value.receipts_root,
withdrawals_root: value.withdrawals_root,
logs_bloom: value.logs_bloom,
difficulty: value.difficulty,
number: value.number,
gas_limit: value.gas_limit,
gas_used: value.gas_used,
timestamp: value.timestamp,
mix_hash: value.mix_hash,
nonce: value.nonce,
base_fee_per_gas: value.base_fee_per_gas,
blob_gas_used: value.blob_gas_used,
excess_blob_gas: value.excess_blob_gas,
parent_beacon_block_root: value.parent_beacon_block_root,
requests_root: value.requests_root,
extra_data: Cow::Borrowed(&value.extra_data),
}
}
}

impl<'a> From<Header<'a>> for super::Header {
fn from(value: Header<'a>) -> Self {
Self {
parent_hash: value.parent_hash,
ommers_hash: value.ommers_hash,
beneficiary: value.beneficiary,
state_root: value.state_root,
transactions_root: value.transactions_root,
receipts_root: value.receipts_root,
withdrawals_root: value.withdrawals_root,
logs_bloom: value.logs_bloom,
difficulty: value.difficulty,
number: value.number,
gas_limit: value.gas_limit,
gas_used: value.gas_used,
timestamp: value.timestamp,
mix_hash: value.mix_hash,
nonce: value.nonce,
base_fee_per_gas: value.base_fee_per_gas,
blob_gas_used: value.blob_gas_used,
excess_blob_gas: value.excess_blob_gas,
parent_beacon_block_root: value.parent_beacon_block_root,
requests_root: value.requests_root,
extra_data: value.extra_data.into_owned(),
}
}
}

impl<'a> SerializeAs<super::Header> for Header<'a> {
fn serialize_as<S>(source: &super::Header, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Header::from(source).serialize(serializer)
}
}

impl<'de> DeserializeAs<'de, super::Header> for Header<'de> {
fn deserialize_as<D>(deserializer: D) -> Result<super::Header, D::Error>
where
D: Deserializer<'de>,
{
Header::deserialize(deserializer).map(Into::into)
}
}

#[cfg(test)]
mod tests {
use arbitrary::Arbitrary;
use rand::Rng;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;

use super::super::{serde_bincode_compat, Header};

#[test]
fn test_header_bincode_roundtrip() {
#[serde_as]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Data {
#[serde_as(as = "serde_bincode_compat::Header")]
header: Header,
}

let mut bytes = [0u8; 1024];
rand::thread_rng().fill(bytes.as_mut_slice());
let data = Data {
header: Header::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(),
};

let encoded = bincode::serialize(&data).unwrap();
let decoded: Data = bincode::deserialize(&encoded).unwrap();
assert_eq!(decoded, data);
}
}
}
14 changes: 14 additions & 0 deletions crates/consensus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,17 @@ pub use alloy_primitives::{Sealable, Sealed};

mod signed;
pub use signed::Signed;

/// Bincode-compatible serde implementations for consensus types.
///
/// `bincode` crate doesn't work well with optionally serializable serde fields, but some of the
/// consensus types require optional serialization for RPC compatibility. This module makes so that
/// all fields are serialized.
///
/// Read more: <https://github.com/bincode-org/bincode/issues/326>
#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
pub mod serde_bincode_compat {
pub use super::{
header::serde_bincode_compat::*, transaction::serde_bincode_compat as transaction,
};
}
122 changes: 122 additions & 0 deletions crates/consensus/src/transaction/eip1559.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,3 +449,125 @@ mod tests {
assert_eq!(*decoded.hash(), hash);
}
}

/// serde-bincode-compatible [`TxEip1559`] serde implementation.
#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
pub(super) mod serde_bincode_compat {
use alloc::borrow::Cow;
use alloy_eips::eip2930::AccessList;
use alloy_primitives::{Bytes, ChainId, TxKind, U256};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::{DeserializeAs, SerializeAs};

/// Bincode-compatible [`super::TxEip1559`] serde implementation.
///
/// Intended to use with the [`serde_with::serde_as`] macro in the following way:
/// ```rust
/// use alloy_consensus::{serde_bincode_compat, TxEip1559};
/// use serde::{Deserialize, Serialize};
/// use serde_with::serde_as;
///
/// #[serde_as]
/// #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
/// struct Data {
/// #[serde_as(as = "serde_bincode_compat::transaction::TxEip1559")]
/// transaction: TxEip1559,
/// }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct TxEip1559<'a> {
chain_id: ChainId,
nonce: u64,
gas_limit: u64,
max_fee_per_gas: u128,
max_priority_fee_per_gas: u128,
#[serde(default)]
to: TxKind,
value: U256,
access_list: Cow<'a, AccessList>,
input: Cow<'a, Bytes>,
}

impl<'a> From<&'a super::TxEip1559> for TxEip1559<'a> {
fn from(value: &'a super::TxEip1559) -> Self {
Self {
chain_id: value.chain_id,
nonce: value.nonce,
gas_limit: value.gas_limit,
max_fee_per_gas: value.max_fee_per_gas,
max_priority_fee_per_gas: value.max_priority_fee_per_gas,
to: value.to,
value: value.value,
access_list: Cow::Borrowed(&value.access_list),
input: Cow::Borrowed(&value.input),
}
}
}

impl<'a> From<TxEip1559<'a>> for super::TxEip1559 {
fn from(value: TxEip1559<'a>) -> Self {
Self {
chain_id: value.chain_id,
nonce: value.nonce,
gas_limit: value.gas_limit,
max_fee_per_gas: value.max_fee_per_gas,
max_priority_fee_per_gas: value.max_priority_fee_per_gas,
to: value.to,
value: value.value,
access_list: value.access_list.into_owned(),
input: value.input.into_owned(),
}
}
}

impl<'a> SerializeAs<super::TxEip1559> for TxEip1559<'a> {
fn serialize_as<S>(source: &super::TxEip1559, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
TxEip1559::from(source).serialize(serializer)
}
}

impl<'de> DeserializeAs<'de, super::TxEip1559> for TxEip1559<'de> {
fn deserialize_as<D>(deserializer: D) -> Result<super::TxEip1559, D::Error>
where
D: Deserializer<'de>,
{
TxEip1559::deserialize(deserializer).map(Into::into)
}
}

#[cfg(test)]
mod tests {
use arbitrary::Arbitrary;
use rand::Rng;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;

use super::super::{serde_bincode_compat, TxEip1559};

#[test]
fn test_tx_eip1559_bincode_roundtrip() {
#[serde_as]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Data {
#[serde_as(as = "serde_bincode_compat::TxEip1559")]
transaction: TxEip1559,
}

let mut bytes = [0u8; 1024];
rand::thread_rng().fill(bytes.as_mut_slice());
let data = Data {
transaction: TxEip1559::arbitrary(&mut arbitrary::Unstructured::new(&bytes))
.unwrap(),
};

let encoded = bincode::serialize(&data).unwrap();
let decoded: Data = bincode::deserialize(&encoded).unwrap();
assert_eq!(decoded, data);
}
}
}
Loading

0 comments on commit 9f18b7a

Please sign in to comment.