From d6113e10401070f7ae9ebfe90ffefdba4d9dd125 Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Tue, 1 Oct 2024 00:20:43 +0300 Subject: [PATCH] feat(exex, primitives): serde bincode compatibility (#11331) Co-authored-by: Matthias Seitz --- Cargo.lock | 44 ++- Cargo.toml | 13 +- crates/evm/execution-types/Cargo.toml | 7 +- crates/evm/execution-types/src/chain.rs | 121 ++++++++ crates/evm/execution-types/src/lib.rs | 12 + crates/exex/types/Cargo.toml | 14 +- crates/exex/types/src/lib.rs | 14 +- crates/exex/types/src/notification.rs | 143 +++++++++- crates/primitives-traits/Cargo.toml | 16 +- crates/primitives-traits/src/header/mod.rs | 5 + crates/primitives-traits/src/header/sealed.rs | 92 ++++++ crates/primitives-traits/src/lib.rs | 14 +- crates/primitives/Cargo.toml | 44 +-- crates/primitives/src/block.rs | 270 ++++++++++++++++++ crates/primitives/src/lib.rs | 17 +- crates/primitives/src/transaction/mod.rs | 216 +++++++++++++- crates/storage/nippy-jar/Cargo.toml | 2 +- 17 files changed, 995 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0da2c2d59..0f320b233 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,7 @@ dependencies = [ "c-kzg", "derive_more", "serde", + "serde_with", ] [[package]] @@ -5103,9 +5104,9 @@ checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "op-alloy-consensus" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "274ce39752bdd16614292484839eb3e62139724c15087d9175a5838dab8d6317" +checksum = "c662868734bd5a274c4474dc0642b5211f008367e591573277e5895333cb78f5" dependencies = [ "alloy-consensus", "alloy-eips", @@ -5115,14 +5116,15 @@ dependencies = [ "arbitrary", "derive_more", "serde", + "serde_with", "spin", ] [[package]] name = "op-alloy-genesis" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1691c004810c0cda7e429866a8c561f21a26649f4143db61b1ce4e390493ce2" +checksum = "67b4faf4f93b34c263e66cb163a085d9da72ced1f3adb34b7bd70c6e9fc7e5d6" dependencies = [ "alloy-consensus", "alloy-eips", @@ -5134,9 +5136,9 @@ dependencies = [ [[package]] name = "op-alloy-network" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f08eccaddff3ecf46c7c9850e4842ef6218481c6829b4135ce230610d0a8f679" +checksum = "a51504fd83b75b5d5e09320a0b4657b3bf23fc8018d40038ebab4eafcd7b9a40" dependencies = [ "alloy-consensus", "alloy-network", @@ -5148,9 +5150,9 @@ dependencies = [ [[package]] name = "op-alloy-protocol" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ca8f42c59b06ed0267e39279c3426576979b9e217db1d0f3f2e8f0c913fc01" +checksum = "20bec4f5aff4fe44e1e5beecd988096e6b757bd4bdfe6b10bb3f08c410287348" dependencies = [ "alloy-consensus", "alloy-eips", @@ -5165,9 +5167,9 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "118f7e47fa822356fe4529bfa3b5d828308c1b53769d2e268337fa5b7d357929" +checksum = "971fb1d31a1764327e4cf27a5372d2fde5db8bead90f75a750eeab306979b34c" dependencies = [ "alloy-consensus", "alloy-eips", @@ -5182,9 +5184,9 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types-engine" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a5cbbffe83cbec46f19b184b63270c2090ce72c200cff19bc29e1f47519952" +checksum = "eb2b515967262eae36ccecf868ab123dd8a098476f08f28f8ab4c3db5e1ee306" dependencies = [ "alloy-eips", "alloy-primitives", @@ -7332,11 +7334,15 @@ version = "1.0.7" dependencies = [ "alloy-eips", "alloy-primitives", + "arbitrary", + "bincode", + "rand 0.8.5", "reth-execution-errors", "reth-primitives", "reth-trie", "revm", "serde", + "serde_with", ] [[package]] @@ -7421,8 +7427,14 @@ version = "1.0.7" dependencies = [ "alloy-eips", "alloy-primitives", - "reth-provider", + "arbitrary", + "bincode", + "rand 0.8.5", + "reth-chain-state", + "reth-execution-types", + "reth-primitives", "serde", + "serde_with", ] [[package]] @@ -8242,6 +8254,7 @@ dependencies = [ "alloy-serde", "arbitrary", "assert_matches", + "bincode", "bytes", "c-kzg", "criterion", @@ -8262,11 +8275,13 @@ dependencies = [ "reth-optimism-chainspec", "reth-primitives-traits", "reth-static-file-types", + "reth-testing-utils", "reth-trie-common", "revm-primitives", "secp256k1", "serde", "serde_json", + "serde_with", "test-fuzz", "zstd", ] @@ -8281,6 +8296,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "arbitrary", + "bincode", "byteorder", "bytes", "derive_more", @@ -8289,10 +8305,12 @@ dependencies = [ "proptest-arbitrary-interop", "rand 0.8.5", "reth-codecs", + "reth-testing-utils", "revm-primitives", "roaring", "serde", "serde_json", + "serde_with", "test-fuzz", ] diff --git a/Cargo.toml b/Cargo.toml index c1b526def..6e89dd00f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -461,20 +461,21 @@ alloy-transport-ipc = { version = "0.4.0", default-features = false } alloy-transport-ws = { version = "0.4.0", default-features = false } # op -op-alloy-rpc-types = "0.3.1" -op-alloy-rpc-types-engine = "0.3.1" -op-alloy-network = "0.3.1" -op-alloy-consensus = "0.3.1" +op-alloy-rpc-types = "0.3.2" +op-alloy-rpc-types-engine = "0.3.2" +op-alloy-network = "0.3.2" +op-alloy-consensus = "0.3.2" # misc aquamarine = "0.5" auto_impl = "1" backon = "0.4" +bincode = "1.3" bitflags = "2.4" boyer-moore-magiclen = "0.2.16" bytes = "1.5" -clap = "4" cfg-if = "1.0" +clap = "4" const_format = { version = "0.2.32", features = ["rust_1_64"] } dashmap = "6.0" derive_more = { version = "1", features = ["full"] } @@ -589,7 +590,7 @@ tikv-jemalloc-ctl = "0.6" tikv-jemallocator = "0.6" tracy-client = "0.17.3" -#[patch.crates-io] +[patch.crates-io] #alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} #alloy-eips = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} #alloy-genesis = { git = "https://github.com/alloy-rs/alloy", rev = "8c499409"} diff --git a/crates/evm/execution-types/Cargo.toml b/crates/evm/execution-types/Cargo.toml index 65426e308..bb5c17a9f 100644 --- a/crates/evm/execution-types/Cargo.toml +++ b/crates/evm/execution-types/Cargo.toml @@ -22,13 +22,18 @@ alloy-primitives.workspace = true alloy-eips.workspace = true serde = { workspace = true, optional = true } +serde_with = { workspace = true, optional = true } [dev-dependencies] -reth-primitives = { workspace = true, features = ["test-utils"] } alloy-eips.workspace = true +arbitrary.workspace = true +bincode.workspace = true +rand.workspace = true +reth-primitives = { workspace = true, features = ["test-utils"] } [features] default = ["std"] optimism = [] serde = ["dep:serde", "reth-trie/serde", "revm/serde"] +serde-bincode-compat = ["reth-primitives/serde-bincode-compat", "serde_with"] std = [] diff --git a/crates/evm/execution-types/src/chain.rs b/crates/evm/execution-types/src/chain.rs index 588bc832e..38be21144 100644 --- a/crates/evm/execution-types/src/chain.rs +++ b/crates/evm/execution-types/src/chain.rs @@ -723,3 +723,124 @@ mod tests { assert_eq!(chain.execution_outcome_at_block(11), Some(execution_outcome)); } } + +/// Bincode-compatible [`Chain`] serde implementation. +#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))] +pub(super) mod serde_bincode_compat { + use std::collections::BTreeMap; + + use alloc::borrow::Cow; + use alloy_primitives::BlockNumber; + use reth_primitives::serde_bincode_compat::SealedBlockWithSenders; + use reth_trie::updates::TrieUpdates; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use serde_with::{DeserializeAs, SerializeAs}; + + use crate::ExecutionOutcome; + + /// Bincode-compatible [`super::Chain`] serde implementation. + /// + /// Intended to use with the [`serde_with::serde_as`] macro in the following way: + /// ```rust + /// use reth_execution_types::{serde_bincode_compat, Chain}; + /// use serde::{Deserialize, Serialize}; + /// use serde_with::serde_as; + /// + /// #[serde_as] + /// #[derive(Serialize, Deserialize)] + /// struct Data { + /// #[serde_as(as = "serde_bincode_compat::Chain")] + /// chain: Chain, + /// } + /// ``` + #[derive(Debug, Serialize, Deserialize)] + pub struct Chain<'a> { + blocks: BTreeMap>, + execution_outcome: Cow<'a, ExecutionOutcome>, + trie_updates: Cow<'a, Option>, + } + + impl<'a> From<&'a super::Chain> for Chain<'a> { + fn from(value: &'a super::Chain) -> Self { + Self { + blocks: value + .blocks + .iter() + .map(|(block_number, block)| (*block_number, block.into())) + .collect(), + execution_outcome: Cow::Borrowed(&value.execution_outcome), + trie_updates: Cow::Borrowed(&value.trie_updates), + } + } + } + + impl<'a> From> for super::Chain { + fn from(value: Chain<'a>) -> Self { + Self { + blocks: value + .blocks + .into_iter() + .map(|(block_number, block)| (block_number, block.into())) + .collect(), + execution_outcome: value.execution_outcome.into_owned(), + trie_updates: value.trie_updates.into_owned(), + } + } + } + + impl<'a> SerializeAs for Chain<'a> { + fn serialize_as(source: &super::Chain, serializer: S) -> Result + where + S: Serializer, + { + Chain::from(source).serialize(serializer) + } + } + + impl<'de> DeserializeAs<'de, super::Chain> for Chain<'de> { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Chain::deserialize(deserializer).map(Into::into) + } + } + + #[cfg(test)] + mod tests { + use arbitrary::Arbitrary; + use rand::Rng; + use reth_primitives::SealedBlockWithSenders; + use serde::{Deserialize, Serialize}; + use serde_with::serde_as; + + use super::super::{serde_bincode_compat, Chain}; + + #[test] + fn test_chain_bincode_roundtrip() { + #[serde_as] + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct Data { + #[serde_as(as = "serde_bincode_compat::Chain")] + chain: Chain, + } + + let mut bytes = [0u8; 1024]; + rand::thread_rng().fill(bytes.as_mut_slice()); + let data = Data { + chain: Chain::new( + vec![SealedBlockWithSenders::arbitrary(&mut arbitrary::Unstructured::new( + &bytes, + )) + .unwrap()], + Default::default(), + None, + ), + }; + + let encoded = bincode::serialize(&data).unwrap(); + let decoded: Data = bincode::deserialize(&encoded).unwrap(); + assert_eq!(decoded, data); + } + } +} diff --git a/crates/evm/execution-types/src/lib.rs b/crates/evm/execution-types/src/lib.rs index 8965f04d7..fb872cd59 100644 --- a/crates/evm/execution-types/src/lib.rs +++ b/crates/evm/execution-types/src/lib.rs @@ -18,3 +18,15 @@ pub use execute::*; mod execution_outcome; pub use execution_outcome::*; + +/// Bincode-compatible serde implementations for commonly used types for (EVM) block execution. +/// +/// `bincode` crate doesn't work with optionally serializable serde fields, but some of the +/// execution types require optional serialization for RPC compatibility. This module makes so that +/// all fields are serialized. +/// +/// Read more: +#[cfg(feature = "serde-bincode-compat")] +pub mod serde_bincode_compat { + pub use super::chain::serde_bincode_compat::*; +} diff --git a/crates/exex/types/Cargo.toml b/crates/exex/types/Cargo.toml index 17b8c8634..a146cbc22 100644 --- a/crates/exex/types/Cargo.toml +++ b/crates/exex/types/Cargo.toml @@ -13,7 +13,8 @@ workspace = true [dependencies] # reth -reth-provider.workspace = true +reth-chain-state.workspace = true +reth-execution-types.workspace = true # reth alloy-primitives.workspace = true @@ -21,7 +22,16 @@ alloy-eips.workspace = true # misc serde = { workspace = true, optional = true } +serde_with = { workspace = true, optional = true } + +[dev-dependencies] +reth-primitives = { workspace = true, features = ["arbitrary"] } + +arbitrary.workspace = true +bincode.workspace = true +rand.workspace = true [features] default = [] -serde = ["dep:serde", "reth-provider/serde"] +serde = ["dep:serde", "reth-execution-types/serde"] +serde-bincode-compat = ["reth-execution-types/serde-bincode-compat", "serde_with"] diff --git a/crates/exex/types/src/lib.rs b/crates/exex/types/src/lib.rs index 8e71fbc61..ffed819d6 100644 --- a/crates/exex/types/src/lib.rs +++ b/crates/exex/types/src/lib.rs @@ -1,4 +1,4 @@ -//! Commonly used types for exex usage. +//! Commonly used ExEx types. #![doc( html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png", @@ -15,3 +15,15 @@ mod notification; pub use finished_height::FinishedExExHeight; pub use head::ExExHead; pub use notification::ExExNotification; + +/// Bincode-compatible serde implementations for commonly used ExEx types. +/// +/// `bincode` crate doesn't work with optionally serializable serde fields, but some of the +/// ExEx types require optional serialization for RPC compatibility. This module makes so that +/// all fields are serialized. +/// +/// Read more: +#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))] +pub mod serde_bincode_compat { + pub use super::notification::serde_bincode_compat::*; +} diff --git a/crates/exex/types/src/notification.rs b/crates/exex/types/src/notification.rs index 2a5595787..534112502 100644 --- a/crates/exex/types/src/notification.rs +++ b/crates/exex/types/src/notification.rs @@ -1,6 +1,7 @@ use std::sync::Arc; -use reth_provider::{CanonStateNotification, Chain}; +use reth_chain_state::CanonStateNotification; +use reth_execution_types::Chain; /// Notifications sent to an `ExEx`. #[derive(Debug, Clone, PartialEq, Eq)] @@ -67,3 +68,143 @@ impl From for ExExNotification { } } } + +/// Bincode-compatible [`ExExNotification`] serde implementation. +#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))] +pub(super) mod serde_bincode_compat { + use std::sync::Arc; + + use reth_execution_types::serde_bincode_compat::Chain; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use serde_with::{DeserializeAs, SerializeAs}; + + /// Bincode-compatible [`super::ExExNotification`] serde implementation. + /// + /// Intended to use with the [`serde_with::serde_as`] macro in the following way: + /// ```rust + /// use reth_exex_types::{serde_bincode_compat, ExExNotification}; + /// use serde::{Deserialize, Serialize}; + /// use serde_with::serde_as; + /// + /// #[serde_as] + /// #[derive(Serialize, Deserialize)] + /// struct Data { + /// #[serde_as(as = "serde_bincode_compat::ExExNotification")] + /// notification: ExExNotification, + /// } + /// ``` + #[derive(Debug, Serialize, Deserialize)] + #[allow(missing_docs)] + pub enum ExExNotification<'a> { + ChainCommitted { new: Chain<'a> }, + ChainReorged { old: Chain<'a>, new: Chain<'a> }, + ChainReverted { old: Chain<'a> }, + } + + impl<'a> From<&'a super::ExExNotification> for ExExNotification<'a> { + fn from(value: &'a super::ExExNotification) -> Self { + match value { + super::ExExNotification::ChainCommitted { new } => { + ExExNotification::ChainCommitted { new: Chain::from(new.as_ref()) } + } + super::ExExNotification::ChainReorged { old, new } => { + ExExNotification::ChainReorged { + old: Chain::from(old.as_ref()), + new: Chain::from(new.as_ref()), + } + } + super::ExExNotification::ChainReverted { old } => { + ExExNotification::ChainReverted { old: Chain::from(old.as_ref()) } + } + } + } + } + + impl<'a> From> for super::ExExNotification { + fn from(value: ExExNotification<'a>) -> Self { + match value { + ExExNotification::ChainCommitted { new } => { + Self::ChainCommitted { new: Arc::new(new.into()) } + } + ExExNotification::ChainReorged { old, new } => { + Self::ChainReorged { old: Arc::new(old.into()), new: Arc::new(new.into()) } + } + ExExNotification::ChainReverted { old } => { + Self::ChainReverted { old: Arc::new(old.into()) } + } + } + } + } + + impl<'a> SerializeAs for ExExNotification<'a> { + fn serialize_as( + source: &super::ExExNotification, + serializer: S, + ) -> Result + where + S: Serializer, + { + ExExNotification::from(source).serialize(serializer) + } + } + + impl<'de> DeserializeAs<'de, super::ExExNotification> for ExExNotification<'de> { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + ExExNotification::deserialize(deserializer).map(Into::into) + } + } + + #[cfg(test)] + mod tests { + use std::sync::Arc; + + use arbitrary::Arbitrary; + use rand::Rng; + use reth_execution_types::Chain; + use reth_primitives::SealedBlockWithSenders; + use serde::{Deserialize, Serialize}; + use serde_with::serde_as; + + use super::super::{serde_bincode_compat, ExExNotification}; + + #[test] + fn test_exex_notification_bincode_roundtrip() { + #[serde_as] + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct Data { + #[serde_as(as = "serde_bincode_compat::ExExNotification")] + notification: ExExNotification, + } + + let mut bytes = [0u8; 1024]; + rand::thread_rng().fill(bytes.as_mut_slice()); + let data = Data { + notification: ExExNotification::ChainReorged { + old: Arc::new(Chain::new( + vec![SealedBlockWithSenders::arbitrary(&mut arbitrary::Unstructured::new( + &bytes, + )) + .unwrap()], + Default::default(), + None, + )), + new: Arc::new(Chain::new( + vec![SealedBlockWithSenders::arbitrary(&mut arbitrary::Unstructured::new( + &bytes, + )) + .unwrap()], + Default::default(), + None, + )), + }, + }; + + let encoded = bincode::serialize(&data).unwrap(); + let decoded: Data = bincode::deserialize(&encoded).unwrap(); + assert_eq!(decoded, data); + } + } +} diff --git a/crates/primitives-traits/Cargo.toml b/crates/primitives-traits/Cargo.toml index ca57a2e57..2fec75666 100644 --- a/crates/primitives-traits/Cargo.toml +++ b/crates/primitives-traits/Cargo.toml @@ -20,16 +20,17 @@ alloy-genesis.workspace = true alloy-primitives.workspace = true alloy-rlp.workspace = true -derive_more.workspace = true revm-primitives = { workspace = true, features = ["serde"] } # misc -roaring = "0.10.2" byteorder = "1" +derive_more.workspace = true +roaring = "0.10.2" +serde_with = { workspace = true, optional = true } # required by reth-codecs -modular-bitfield.workspace = true bytes.workspace = true +modular-bitfield.workspace = true serde.workspace = true # arbitrary utils @@ -38,14 +39,18 @@ proptest = { workspace = true, optional = true } proptest-arbitrary-interop = { workspace = true, optional = true } [dev-dependencies] +reth-testing-utils.workspace = true + alloy-primitives = { workspace = true, features = ["arbitrary"] } alloy-consensus = { workspace = true, features = ["arbitrary"] } + arbitrary = { workspace = true, features = ["derive"] } -proptest.workspace = true +bincode.workspace = true proptest-arbitrary-interop.workspace = true -test-fuzz.workspace = true +proptest.workspace = true rand.workspace = true serde_json.workspace = true +test-fuzz.workspace = true [features] default = ["std"] @@ -59,3 +64,4 @@ arbitrary = [ "dep:proptest", "dep:proptest-arbitrary-interop", ] +serde-bincode-compat = ["serde_with", "alloy-consensus/serde-bincode-compat"] diff --git a/crates/primitives-traits/src/header/mod.rs b/crates/primitives-traits/src/header/mod.rs index 2dbfecd15..8793ad41b 100644 --- a/crates/primitives-traits/src/header/mod.rs +++ b/crates/primitives-traits/src/header/mod.rs @@ -11,6 +11,11 @@ pub use alloy_consensus::Header; use alloy_primitives::{Address, BlockNumber, B256, U256}; +#[cfg(feature = "serde-bincode-compat")] +pub(super) mod serde_bincode_compat { + pub use super::sealed::serde_bincode_compat::SealedHeader; +} + /// Trait for extracting specific Ethereum block data from a header pub trait BlockHeader { /// Retrieves the beneficiary (miner) of the block diff --git a/crates/primitives-traits/src/header/sealed.rs b/crates/primitives-traits/src/header/sealed.rs index ec4ea7843..b77fc7f05 100644 --- a/crates/primitives-traits/src/header/sealed.rs +++ b/crates/primitives-traits/src/header/sealed.rs @@ -140,3 +140,95 @@ impl<'a> arbitrary::Arbitrary<'a> for SealedHeader { Ok(Self::new(header, seal)) } } + +/// Bincode-compatible [`SealedHeader`] serde implementation. +#[cfg(feature = "serde-bincode-compat")] +pub(super) mod serde_bincode_compat { + use alloy_consensus::serde_bincode_compat::Header; + use alloy_primitives::BlockHash; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use serde_with::{DeserializeAs, SerializeAs}; + + /// Bincode-compatible [`super::SealedHeader`] serde implementation. + /// + /// Intended to use with the [`serde_with::serde_as`] macro in the following way: + /// ```rust + /// use reth_primitives_traits::{header::SealedHeader, serde_bincode_compat}; + /// use serde::{Deserialize, Serialize}; + /// use serde_with::serde_as; + /// + /// #[serde_as] + /// #[derive(Serialize, Deserialize)] + /// struct Data { + /// #[serde_as(as = "serde_bincode_compat::header::SealedHeader")] + /// header: SealedHeader, + /// } + /// ``` + #[derive(Debug, Serialize, Deserialize)] + pub struct SealedHeader<'a> { + hash: BlockHash, + header: Header<'a>, + } + + impl<'a> From<&'a super::SealedHeader> for SealedHeader<'a> { + fn from(value: &'a super::SealedHeader) -> Self { + Self { hash: value.hash, header: Header::from(&value.header) } + } + } + + impl<'a> From> for super::SealedHeader { + fn from(value: SealedHeader<'a>) -> Self { + Self { hash: value.hash, header: value.header.into() } + } + } + + impl<'a> SerializeAs for SealedHeader<'a> { + fn serialize_as(source: &super::SealedHeader, serializer: S) -> Result + where + S: Serializer, + { + SealedHeader::from(source).serialize(serializer) + } + } + + impl<'de> DeserializeAs<'de, super::SealedHeader> for SealedHeader<'de> { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + SealedHeader::deserialize(deserializer).map(Into::into) + } + } + + #[cfg(test)] + mod tests { + use super::super::{serde_bincode_compat, SealedHeader}; + + use arbitrary::Arbitrary; + use rand::Rng; + use reth_testing_utils::generators; + use serde::{Deserialize, Serialize}; + use serde_with::serde_as; + + #[test] + fn test_sealed_header_bincode_roundtrip() { + #[serde_as] + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct Data { + #[serde_as(as = "serde_bincode_compat::SealedHeader")] + transaction: SealedHeader, + } + + let mut bytes = [0u8; 1024]; + generators::rng().fill(bytes.as_mut_slice()); + let data = Data { + transaction: SealedHeader::arbitrary(&mut arbitrary::Unstructured::new(&bytes)) + .unwrap(), + }; + + let encoded = bincode::serialize(&data).unwrap(); + let decoded: Data = bincode::deserialize(&encoded).unwrap(); + assert_eq!(decoded, data); + } + } +} diff --git a/crates/primitives-traits/src/lib.rs b/crates/primitives-traits/src/lib.rs index 5445ce467..d16fee91f 100644 --- a/crates/primitives-traits/src/lib.rs +++ b/crates/primitives-traits/src/lib.rs @@ -1,4 +1,4 @@ -//! Common abstracted types in reth. +//! Common abstracted types in Reth. #![doc( html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png", @@ -43,3 +43,15 @@ pub mod header; #[cfg(any(test, feature = "arbitrary", feature = "test-utils"))] pub use header::test_utils; pub use header::{BlockHeader, Header, HeaderError, SealedHeader}; + +/// Bincode-compatible serde implementations for common abstracted types in Reth. +/// +/// `bincode` crate doesn't work with optionally serializable serde fields, but some of the +/// Reth types require optional serialization for RPC compatibility. This module makes so that +/// all fields are serialized. +/// +/// Read more: +#[cfg(feature = "serde-bincode-compat")] +pub mod serde_bincode_compat { + pub use super::header::serde_bincode_compat::*; +} diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 9745159e8..8596f8d76 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -51,9 +51,11 @@ c-kzg = { workspace = true, features = ["serde"], optional = true } bytes.workspace = true derive_more.workspace = true modular-bitfield = { workspace = true, optional = true } +once_cell.workspace = true +rand = { workspace = true, optional = true } rayon.workspace = true serde.workspace = true -once_cell.workspace = true +serde_with = { workspace = true, optional = true } zstd = { workspace = true, features = ["experimental"], optional = true } # arbitrary utils @@ -62,22 +64,24 @@ proptest = { workspace = true, optional = true } [dev-dependencies] # eth -reth-primitives-traits = { workspace = true, features = ["arbitrary"] } -revm-primitives = { workspace = true, features = ["arbitrary"] } reth-chainspec.workspace = true reth-codecs.workspace = true +reth-primitives-traits = { workspace = true, features = ["arbitrary"] } +reth-testing-utils.workspace = true +revm-primitives = { workspace = true, features = ["arbitrary"] } + alloy-eips = { workspace = true, features = ["arbitrary"] } alloy-genesis.workspace = true -assert_matches.workspace = true arbitrary = { workspace = true, features = ["derive"] } -proptest.workspace = true +assert_matches.workspace = true +bincode.workspace = true +modular-bitfield.workspace = true proptest-arbitrary-interop.workspace = true +proptest.workspace = true rand.workspace = true serde_json.workspace = true test-fuzz.workspace = true -modular-bitfield.workspace = true - criterion.workspace = true pprof = { workspace = true, features = [ @@ -92,26 +96,28 @@ std = ["reth-primitives-traits/std"] reth-codec = ["dep:reth-codecs", "dep:zstd", "dep:modular-bitfield", "std"] asm-keccak = ["alloy-primitives/asm-keccak"] arbitrary = [ - "reth-primitives-traits/arbitrary", - "revm-primitives/arbitrary", - "reth-ethereum-forks/arbitrary", - "alloy-eips/arbitrary", "dep:arbitrary", "dep:proptest", + "alloy-eips/arbitrary", + "rand", "reth-codec", + "reth-ethereum-forks/arbitrary", + "reth-primitives-traits/arbitrary", + "revm-primitives/arbitrary", + "secp256k1", ] secp256k1 = ["dep:secp256k1"] c-kzg = [ "dep:c-kzg", - "revm-primitives/c-kzg", - "alloy-eips/kzg", "alloy-consensus/kzg", + "alloy-eips/kzg", + "revm-primitives/c-kzg", ] optimism = [ - "revm-primitives/optimism", - "reth-codecs?/optimism", - "dep:reth-optimism-chainspec", "dep:op-alloy-consensus", + "dep:reth-optimism-chainspec", + "reth-codecs?/optimism", + "revm-primitives/optimism", ] alloy-compat = [ "dep:alloy-rpc-types", @@ -119,6 +125,12 @@ alloy-compat = [ "dep:op-alloy-rpc-types", ] test-utils = ["reth-primitives-traits/test-utils"] +serde-bincode-compat = [ + "alloy-consensus/serde-bincode-compat", + "op-alloy-consensus?/serde-bincode-compat", + "reth-primitives-traits/serde-bincode-compat", + "serde_with", +] [[bench]] name = "recover_ecdsa_crit" diff --git a/crates/primitives/src/block.rs b/crates/primitives/src/block.rs index 0960cbac1..3ab8a1dad 100644 --- a/crates/primitives/src/block.rs +++ b/crates/primitives/src/block.rs @@ -541,6 +541,22 @@ impl SealedBlockWithSenders { } } +#[cfg(any(test, feature = "arbitrary"))] +impl<'a> arbitrary::Arbitrary<'a> for SealedBlockWithSenders { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let block = SealedBlock::arbitrary(u)?; + + let senders = block + .body + .transactions + .iter() + .map(|tx| tx.recover_signer().unwrap()) + .collect::>(); + + Ok(Self { block, senders }) + } +} + /// A response to `GetBlockBodies`, containing bodies if any bodies were found. /// /// Withdrawals can be optionally included at the end of the RLP encoded message. @@ -861,3 +877,257 @@ mod tests { ); } } + +/// Bincode-compatible block type serde implementations. +#[cfg(feature = "serde-bincode-compat")] +pub(super) mod serde_bincode_compat { + use alloc::{borrow::Cow, vec::Vec}; + use alloy_consensus::serde_bincode_compat::Header; + use alloy_primitives::Address; + use reth_primitives_traits::{serde_bincode_compat::SealedHeader, Requests, Withdrawals}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use serde_with::{DeserializeAs, SerializeAs}; + + use crate::transaction::serde_bincode_compat::TransactionSigned; + + /// Bincode-compatible [`super::BlockBody`] serde implementation. + /// + /// Intended to use with the [`serde_with::serde_as`] macro in the following way: + /// ```rust + /// use reth_primitives::{serde_bincode_compat, BlockBody}; + /// use serde::{Deserialize, Serialize}; + /// use serde_with::serde_as; + /// + /// #[serde_as] + /// #[derive(Serialize, Deserialize)] + /// struct Data { + /// #[serde_as(as = "serde_bincode_compat::BlockBody")] + /// body: BlockBody, + /// } + /// ``` + #[derive(Debug, Serialize, Deserialize)] + pub struct BlockBody<'a> { + transactions: Vec>, + ommers: Vec>, + withdrawals: Cow<'a, Option>, + requests: Cow<'a, Option>, + } + + impl<'a> From<&'a super::BlockBody> for BlockBody<'a> { + fn from(value: &'a super::BlockBody) -> Self { + Self { + transactions: value.transactions.iter().map(Into::into).collect(), + ommers: value.ommers.iter().map(Into::into).collect(), + withdrawals: Cow::Borrowed(&value.withdrawals), + requests: Cow::Borrowed(&value.requests), + } + } + } + + impl<'a> From> for super::BlockBody { + fn from(value: BlockBody<'a>) -> Self { + Self { + transactions: value.transactions.into_iter().map(Into::into).collect(), + ommers: value.ommers.into_iter().map(Into::into).collect(), + withdrawals: value.withdrawals.into_owned(), + requests: value.requests.into_owned(), + } + } + } + + impl<'a> SerializeAs for BlockBody<'a> { + fn serialize_as(source: &super::BlockBody, serializer: S) -> Result + where + S: Serializer, + { + BlockBody::from(source).serialize(serializer) + } + } + + impl<'de> DeserializeAs<'de, super::BlockBody> for BlockBody<'de> { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + BlockBody::deserialize(deserializer).map(Into::into) + } + } + + /// Bincode-compatible [`super::SealedBlock`] serde implementation. + /// + /// Intended to use with the [`serde_with::serde_as`] macro in the following way: + /// ```rust + /// use reth_primitives::{serde_bincode_compat, SealedBlock}; + /// use serde::{Deserialize, Serialize}; + /// use serde_with::serde_as; + /// + /// #[serde_as] + /// #[derive(Serialize, Deserialize)] + /// struct Data { + /// #[serde_as(as = "serde_bincode_compat::SealedBlock")] + /// block: SealedBlock, + /// } + /// ``` + #[derive(Debug, Serialize, Deserialize)] + pub struct SealedBlock<'a> { + header: SealedHeader<'a>, + body: BlockBody<'a>, + } + + impl<'a> From<&'a super::SealedBlock> for SealedBlock<'a> { + fn from(value: &'a super::SealedBlock) -> Self { + Self { header: SealedHeader::from(&value.header), body: BlockBody::from(&value.body) } + } + } + + impl<'a> From> for super::SealedBlock { + fn from(value: SealedBlock<'a>) -> Self { + Self { header: value.header.into(), body: value.body.into() } + } + } + + impl<'a> SerializeAs for SealedBlock<'a> { + fn serialize_as(source: &super::SealedBlock, serializer: S) -> Result + where + S: Serializer, + { + SealedBlock::from(source).serialize(serializer) + } + } + + impl<'de> DeserializeAs<'de, super::SealedBlock> for SealedBlock<'de> { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + SealedBlock::deserialize(deserializer).map(Into::into) + } + } + + /// Bincode-compatible [`super::SealedBlockWithSenders`] serde implementation. + /// + /// Intended to use with the [`serde_with::serde_as`] macro in the following way: + /// ```rust + /// use reth_primitives::{serde_bincode_compat, SealedBlockWithSenders}; + /// use serde::{Deserialize, Serialize}; + /// use serde_with::serde_as; + /// + /// #[serde_as] + /// #[derive(Serialize, Deserialize)] + /// struct Data { + /// #[serde_as(as = "serde_bincode_compat::SealedBlockWithSenders")] + /// block: SealedBlockWithSenders, + /// } + /// ``` + #[derive(Debug, Serialize, Deserialize)] + pub struct SealedBlockWithSenders<'a> { + block: SealedBlock<'a>, + senders: Cow<'a, Vec
>, + } + + impl<'a> From<&'a super::SealedBlockWithSenders> for SealedBlockWithSenders<'a> { + fn from(value: &'a super::SealedBlockWithSenders) -> Self { + Self { block: SealedBlock::from(&value.block), senders: Cow::Borrowed(&value.senders) } + } + } + + impl<'a> From> for super::SealedBlockWithSenders { + fn from(value: SealedBlockWithSenders<'a>) -> Self { + Self { block: value.block.into(), senders: value.senders.into_owned() } + } + } + + impl<'a> SerializeAs for SealedBlockWithSenders<'a> { + fn serialize_as( + source: &super::SealedBlockWithSenders, + serializer: S, + ) -> Result + where + S: Serializer, + { + SealedBlockWithSenders::from(source).serialize(serializer) + } + } + + impl<'de> DeserializeAs<'de, super::SealedBlockWithSenders> for SealedBlockWithSenders<'de> { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + SealedBlockWithSenders::deserialize(deserializer).map(Into::into) + } + } + + #[cfg(test)] + mod tests { + use super::super::{serde_bincode_compat, BlockBody, SealedBlock, SealedBlockWithSenders}; + + use arbitrary::Arbitrary; + use rand::Rng; + use reth_testing_utils::generators; + use serde::{Deserialize, Serialize}; + use serde_with::serde_as; + + #[test] + fn test_block_body_bincode_roundtrip() { + #[serde_as] + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct Data { + #[serde_as(as = "serde_bincode_compat::BlockBody")] + block_body: BlockBody, + } + + let mut bytes = [0u8; 1024]; + generators::rng().fill(bytes.as_mut_slice()); + let data = Data { + block_body: BlockBody::arbitrary(&mut arbitrary::Unstructured::new(&bytes)) + .unwrap(), + }; + + let encoded = bincode::serialize(&data).unwrap(); + let decoded: Data = bincode::deserialize(&encoded).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn test_sealed_block_bincode_roundtrip() { + #[serde_as] + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct Data { + #[serde_as(as = "serde_bincode_compat::SealedBlock")] + block: SealedBlock, + } + + let mut bytes = [0u8; 1024]; + generators::rng().fill(bytes.as_mut_slice()); + let data = Data { + block: SealedBlock::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(), + }; + + let encoded = bincode::serialize(&data).unwrap(); + let decoded: Data = bincode::deserialize(&encoded).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn test_sealed_block_with_senders_bincode_roundtrip() { + #[serde_as] + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct Data { + #[serde_as(as = "serde_bincode_compat::SealedBlockWithSenders")] + block: SealedBlockWithSenders, + } + + let mut bytes = [0u8; 1024]; + generators::rng().fill(bytes.as_mut_slice()); + let data = Data { + block: SealedBlockWithSenders::arbitrary(&mut arbitrary::Unstructured::new(&bytes)) + .unwrap(), + }; + + let encoded = bincode::serialize(&data).unwrap(); + let decoded: Data = bincode::deserialize(&encoded).unwrap(); + assert_eq!(decoded, data); + } + } +} diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index bf45ce3ba..da620e50e 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -1,4 +1,4 @@ -//! Commonly used types in reth. +//! Commonly used types in Reth. //! //! This crate contains Ethereum primitive types and helper functions. //! @@ -87,3 +87,18 @@ mod optimism { #[cfg(feature = "optimism")] pub use optimism::*; + +/// Bincode-compatible serde implementations for commonly used types in Reth. +/// +/// `bincode` crate doesn't work with optionally serializable serde fields, but some of the +/// Reth types require optional serialization for RPC compatibility. This module makes so that +/// all fields are serialized. +/// +/// Read more: +#[cfg(feature = "serde-bincode-compat")] +pub mod serde_bincode_compat { + pub use super::{ + block::serde_bincode_compat::*, + transaction::{serde_bincode_compat as transaction, serde_bincode_compat::*}, + }; +} diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index 4f5c36002..1e604acdf 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -1416,7 +1416,14 @@ impl<'a> arbitrary::Arbitrary<'a> for TransactionSigned { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { #[allow(unused_mut)] let mut transaction = Transaction::arbitrary(u)?; - let mut signature = Signature::arbitrary(u)?; + + let secp = secp256k1::Secp256k1::new(); + let key_pair = secp256k1::Keypair::new(&secp, &mut rand::thread_rng()); + let mut signature = crate::sign_message( + B256::from_slice(&key_pair.secret_bytes()[..]), + transaction.signature_hash(), + ) + .unwrap(); signature = if matches!(transaction, Transaction::Legacy(_)) { if let Some(chain_id) = transaction.chain_id() { @@ -1969,3 +1976,210 @@ mod tests { assert!(res.is_err()); } } + +/// Bincode-compatible transaction type serde implementations. +#[cfg(feature = "serde-bincode-compat")] +pub mod serde_bincode_compat { + use alloc::borrow::Cow; + use alloy_consensus::{ + transaction::serde_bincode_compat::{TxEip1559, TxEip2930, TxLegacy}, + TxEip4844, TxEip7702, + }; + use alloy_primitives::{Signature, TxHash}; + #[cfg(feature = "optimism")] + use op_alloy_consensus::serde_bincode_compat::TxDeposit; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use serde_with::{DeserializeAs, SerializeAs}; + + /// Bincode-compatible [`super::Transaction`] serde implementation. + /// + /// Intended to use with the [`serde_with::serde_as`] macro in the following way: + /// ```rust + /// use reth_primitives_traits::{serde_bincode_compat, Transaction}; + /// use serde::{Deserialize, Serialize}; + /// use serde_with::serde_as; + /// + /// #[serde_as] + /// #[derive(Serialize, Deserialize)] + /// struct Data { + /// #[serde_as(as = "serde_bincode_compat::transaction::Transaction")] + /// transaction: Transaction, + /// } + /// ``` + #[derive(Debug, Serialize, Deserialize)] + #[allow(missing_docs)] + pub enum Transaction<'a> { + Legacy(TxLegacy<'a>), + Eip2930(TxEip2930<'a>), + Eip1559(TxEip1559<'a>), + Eip4844(Cow<'a, TxEip4844>), + Eip7702(Cow<'a, TxEip7702>), + #[cfg(feature = "optimism")] + #[cfg(feature = "optimism")] + Deposit(TxDeposit<'a>), + } + + impl<'a> From<&'a super::Transaction> for Transaction<'a> { + fn from(value: &'a super::Transaction) -> Self { + match value { + super::Transaction::Legacy(tx) => Self::Legacy(TxLegacy::from(tx)), + super::Transaction::Eip2930(tx) => Self::Eip2930(TxEip2930::from(tx)), + super::Transaction::Eip1559(tx) => Self::Eip1559(TxEip1559::from(tx)), + super::Transaction::Eip4844(tx) => Self::Eip4844(Cow::Borrowed(tx)), + super::Transaction::Eip7702(tx) => Self::Eip7702(Cow::Borrowed(tx)), + #[cfg(feature = "optimism")] + super::Transaction::Deposit(tx) => Self::Deposit(TxDeposit::from(tx)), + } + } + } + + impl<'a> From> for super::Transaction { + fn from(value: Transaction<'a>) -> Self { + match value { + Transaction::Legacy(tx) => Self::Legacy(tx.into()), + Transaction::Eip2930(tx) => Self::Eip2930(tx.into()), + Transaction::Eip1559(tx) => Self::Eip1559(tx.into()), + Transaction::Eip4844(tx) => Self::Eip4844(tx.into_owned()), + Transaction::Eip7702(tx) => Self::Eip7702(tx.into_owned()), + #[cfg(feature = "optimism")] + Transaction::Deposit(tx) => Self::Deposit(tx.into()), + } + } + } + + impl<'a> SerializeAs for Transaction<'a> { + fn serialize_as(source: &super::Transaction, serializer: S) -> Result + where + S: Serializer, + { + Transaction::from(source).serialize(serializer) + } + } + + impl<'de> DeserializeAs<'de, super::Transaction> for Transaction<'de> { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Transaction::deserialize(deserializer).map(Into::into) + } + } + + /// Bincode-compatible [`super::TransactionSigned`] serde implementation. + /// + /// Intended to use with the [`serde_with::serde_as`] macro in the following way: + /// ```rust + /// use reth_primitives_traits::{serde_bincode_compat, TransactionSigned}; + /// use serde::{Deserialize, Serialize}; + /// use serde_with::serde_as; + /// + /// #[serde_as] + /// #[derive(Serialize, Deserialize)] + /// struct Data { + /// #[serde_as(as = "serde_bincode_compat::transaction::TransactionSigned")] + /// transaction: TransactionSigned, + /// } + /// ``` + #[derive(Debug, Serialize, Deserialize)] + pub struct TransactionSigned<'a> { + hash: TxHash, + signature: Signature, + transaction: Transaction<'a>, + } + + impl<'a> From<&'a super::TransactionSigned> for TransactionSigned<'a> { + fn from(value: &'a super::TransactionSigned) -> Self { + Self { + hash: value.hash, + signature: value.signature, + transaction: Transaction::from(&value.transaction), + } + } + } + + impl<'a> From> for super::TransactionSigned { + fn from(value: TransactionSigned<'a>) -> Self { + Self { + hash: value.hash, + signature: value.signature, + transaction: value.transaction.into(), + } + } + } + + impl<'a> SerializeAs for TransactionSigned<'a> { + fn serialize_as( + source: &super::TransactionSigned, + serializer: S, + ) -> Result + where + S: Serializer, + { + TransactionSigned::from(source).serialize(serializer) + } + } + + impl<'de> DeserializeAs<'de, super::TransactionSigned> for TransactionSigned<'de> { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + TransactionSigned::deserialize(deserializer).map(Into::into) + } + } + + #[cfg(test)] + mod tests { + use super::super::{serde_bincode_compat, Transaction, TransactionSigned}; + + use arbitrary::Arbitrary; + use rand::Rng; + use reth_testing_utils::generators; + use serde::{Deserialize, Serialize}; + use serde_with::serde_as; + + #[test] + fn test_transaction_bincode_roundtrip() { + #[serde_as] + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct Data { + #[serde_as(as = "serde_bincode_compat::Transaction")] + transaction: Transaction, + } + + let mut bytes = [0u8; 1024]; + generators::rng().fill(bytes.as_mut_slice()); + let data = Data { + transaction: Transaction::arbitrary(&mut arbitrary::Unstructured::new(&bytes)) + .unwrap(), + }; + + let encoded = bincode::serialize(&data).unwrap(); + let decoded: Data = bincode::deserialize(&encoded).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn test_transaction_signed_bincode_roundtrip() { + #[serde_as] + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] + struct Data { + #[serde_as(as = "serde_bincode_compat::TransactionSigned")] + transaction: TransactionSigned, + } + + let mut bytes = [0u8; 1024]; + generators::rng().fill(bytes.as_mut_slice()); + let data = Data { + transaction: TransactionSigned::arbitrary(&mut arbitrary::Unstructured::new( + &bytes, + )) + .unwrap(), + }; + + let encoded = bincode::serialize(&data).unwrap(); + let decoded: Data = bincode::deserialize(&encoded).unwrap(); + assert_eq!(decoded, data); + } + } +} diff --git a/crates/storage/nippy-jar/Cargo.toml b/crates/storage/nippy-jar/Cargo.toml index 7c391483a..9f212bf44 100644 --- a/crates/storage/nippy-jar/Cargo.toml +++ b/crates/storage/nippy-jar/Cargo.toml @@ -23,7 +23,7 @@ zstd = { workspace = true, features = ["experimental", "zdict_builder"] } lz4_flex = { version = "0.11", default-features = false } memmap2 = "0.9.4" -bincode = "1.3" +bincode.workspace = true serde = { workspace = true, features = ["derive"] } tracing.workspace = true anyhow = "1.0"