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(consensus): bincode compatibility for header and transaction types #1397

Merged
merged 13 commits into from
Sep 30, 2024
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"]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

open to renaming this feature to something more generic than bincode, because it also unlocks messagepack via https://github.com/3Hren/msgpack-rust

173 changes: 169 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,168 @@ 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,
#[serde(with = "alloy_serde::quantity")]
shekhirin marked this conversation as resolved.
Show resolved Hide resolved
number: BlockNumber,
#[serde(with = "alloy_serde::quantity")]
gas_limit: u64,
#[serde(with = "alloy_serde::quantity")]
gas_used: u64,
#[serde(with = "alloy_serde::quantity")]
timestamp: u64,
mix_hash: B256,
nonce: B64,
#[serde(default, with = "alloy_serde::quantity::opt")]
base_fee_per_gas: Option<u64>,
#[serde(default, with = "alloy_serde::quantity::opt")]
blob_gas_used: Option<u64>,
#[serde(default, with = "alloy_serde::quantity::opt")]
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);
}
}
}
8 changes: 8 additions & 0 deletions crates/consensus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,11 @@ pub use alloy_primitives::{Sealable, Sealed};

mod signed;
pub use signed::Signed;

/// Bincode-compatible serde implementations for consensus types.
#[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,
};
}
127 changes: 127 additions & 0 deletions crates/consensus/src/transaction/eip1559.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,3 +449,130 @@ 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> {
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
chain_id: ChainId,
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
nonce: u64,
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
gas_limit: u64,
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
max_fee_per_gas: u128,
#[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
max_priority_fee_per_gas: u128,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "TxKind::is_create"))]
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