From d6d7ccf0162a3607661e87b1384a4d0567d3f745 Mon Sep 17 00:00:00 2001 From: Rigidity Date: Fri, 16 Aug 2024 23:09:55 -0400 Subject: [PATCH] Serde support for common types --- Cargo.lock | 198 +++++++++++++- Cargo.toml | 14 +- crates/chia-bls/Cargo.toml | 2 + crates/chia-bls/src/gtelement.rs | 28 ++ crates/chia-bls/src/public_key.rs | 28 ++ crates/chia-bls/src/secret_key.rs | 28 ++ crates/chia-bls/src/signature.rs | 28 ++ crates/chia-protocol/Cargo.toml | 4 + crates/chia-protocol/src/bytes.rs | 20 ++ crates/chia-protocol/src/coin.rs | 5 + crates/chia-protocol/src/coin_spend.rs | 5 + crates/chia-protocol/src/coin_state.rs | 5 + crates/chia-protocol/src/program.rs | 1 + crates/chia-protocol/src/spend_bundle.rs | 58 +++++ crates/chia-traits/Cargo.toml | 10 + crates/chia-traits/src/lib.rs | 6 + crates/chia-traits/src/serde.rs | 317 +++++++++++++++++++++++ crates/clvm-utils/Cargo.toml | 6 + crates/clvm-utils/src/tree_hash.rs | 13 +- 19 files changed, 764 insertions(+), 12 deletions(-) create mode 100644 crates/chia-traits/src/serde.rs diff --git a/Cargo.lock b/Cargo.lock index b52c5664c..6825fb45b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -192,6 +207,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.6.0" @@ -318,6 +342,7 @@ dependencies = [ "pyo3", "rand", "rstest", + "serde", "sha2", "thiserror", ] @@ -398,6 +423,9 @@ dependencies = [ "hex", "pyo3", "rstest", + "serde", + "serde_json", + "serde_with", ] [[package]] @@ -486,9 +514,15 @@ dependencies = [ name = "chia-traits" version = "0.11.0" dependencies = [ + "bincode", "chia_streamable_macro 0.11.0", "clvmr", + "hex", + "hex-literal", "pyo3", + "serde", + "serde_json", + "serde_with", "thiserror", ] @@ -549,6 +583,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -652,10 +699,13 @@ dependencies = [ name = "clvm-utils" version = "0.11.0" dependencies = [ + "chia-traits 0.11.0", "clvm-traits", "clvmr", "hex", "rstest", + "serde", + "serde_with", ] [[package]] @@ -699,6 +749,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -806,6 +862,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.72", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.72", +] + [[package]] name = "data-encoding" version = "2.6.0" @@ -844,6 +935,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1137,6 +1229,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1153,7 +1251,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1221,6 +1319,35 @@ version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1231,6 +1358,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -1238,7 +1376,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] [[package]] @@ -1392,7 +1531,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -2109,18 +2248,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", @@ -2129,9 +2268,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.121" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ "itoa", "memchr", @@ -2139,6 +2278,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2407,7 +2576,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.2.6", "toml_datetime", "winnow", ] @@ -2612,6 +2781,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 063713ba2..e154d2a2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ cast_possible_wrap = "allow" cast_lossless = "allow" similar_names = "allow" implicit_hasher = "allow" +unsafe_derive_deserialize = "allow" [dependencies] chia-bls = { workspace = true, optional = true } @@ -72,7 +73,8 @@ default = [ "traits", "puzzles", "clvm-traits", - "clvm-utils" + "clvm-utils", + "serde" ] bls = ["dep:chia-bls"] @@ -86,6 +88,12 @@ clvm-traits = ["dep:clvm-traits"] clvm-utils = ["dep:clvm-utils"] openssl = ["clvmr/openssl"] +serde = [ + "chia-bls/serde", + "chia-traits/serde", + "chia-protocol/serde", + "clvm-utils/serde" +] [profile.release] lto = "thin" @@ -139,3 +147,7 @@ zstd = "0.13.2" blocking-threadpool = "1.0.1" libfuzzer-sys = "0.4" wasm-bindgen = "0.2.92" +serde = "1.0.208" +serde_with = "3.9.0" +serde_json = "1.0.125" +bincode = "1.3.3" diff --git a/crates/chia-bls/Cargo.toml b/crates/chia-bls/Cargo.toml index 851830e2e..5e54c880b 100644 --- a/crates/chia-bls/Cargo.toml +++ b/crates/chia-bls/Cargo.toml @@ -14,6 +14,7 @@ workspace = true [features] py-bindings = ["dep:pyo3", "chia_py_streamable_macro", "chia-traits/py-bindings"] arbitrary = ["dep:arbitrary"] +serde = ["dep:serde", "chia-traits/serde"] [dependencies] chia-traits = { workspace = true } @@ -28,6 +29,7 @@ thiserror = { workspace = true } pyo3 = { workspace = true, features = ["multiple-pymethods"], optional = true } arbitrary = { workspace = true, optional = true } lru = { workspace = true } +serde = { workspace = true, optional = true } [dev-dependencies] rand = { workspace = true } diff --git a/crates/chia-bls/src/gtelement.rs b/crates/chia-bls/src/gtelement.rs index 5c42fbd1c..06dbee083 100644 --- a/crates/chia-bls/src/gtelement.rs +++ b/crates/chia-bls/src/gtelement.rs @@ -101,6 +101,34 @@ impl Streamable for GTElement { } } +#[cfg(feature = "serde")] +impl serde::Serialize for GTElement { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + chia_traits::serialize_hex_or_bytes( + &self.to_bytes(), + serializer, + chia_traits::PrefixKind::PreferPrefix, + ) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for GTElement { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let bytes = chia_traits::deserialize_hex_or_bytes( + deserializer, + chia_traits::PrefixKind::PreferPrefix, + )?; + Ok(Self::from_bytes(&bytes)) + } +} + #[cfg(feature = "py-bindings")] #[pyo3::pymethods] impl GTElement { diff --git a/crates/chia-bls/src/public_key.rs b/crates/chia-bls/src/public_key.rs index c0e164c61..a067facd1 100644 --- a/crates/chia-bls/src/public_key.rs +++ b/crates/chia-bls/src/public_key.rs @@ -296,6 +296,34 @@ pub fn hash_to_g1_with_dst(msg: &[u8], dst: &[u8]) -> PublicKey { PublicKey(p1) } +#[cfg(feature = "serde")] +impl serde::Serialize for PublicKey { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + chia_traits::serialize_hex_or_bytes( + &self.to_bytes(), + serializer, + chia_traits::PrefixKind::PreferPrefix, + ) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for PublicKey { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let bytes = chia_traits::deserialize_hex_or_bytes( + deserializer, + chia_traits::PrefixKind::PreferPrefix, + )?; + Self::from_bytes(&bytes).map_err(serde::de::Error::custom) + } +} + #[cfg(feature = "py-bindings")] #[pyo3::pymethods] impl PublicKey { diff --git a/crates/chia-bls/src/secret_key.rs b/crates/chia-bls/src/secret_key.rs index 31368b206..4a3326903 100644 --- a/crates/chia-bls/src/secret_key.rs +++ b/crates/chia-bls/src/secret_key.rs @@ -236,6 +236,34 @@ impl DerivableKey for SecretKey { } } +#[cfg(feature = "serde")] +impl serde::Serialize for SecretKey { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + chia_traits::serialize_hex_or_bytes( + &self.to_bytes(), + serializer, + chia_traits::PrefixKind::PreferPrefix, + ) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for SecretKey { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let bytes = chia_traits::deserialize_hex_or_bytes( + deserializer, + chia_traits::PrefixKind::PreferPrefix, + )?; + Self::from_bytes(&bytes).map_err(serde::de::Error::custom) + } +} + #[cfg(feature = "py-bindings")] #[pyo3::pymethods] impl SecretKey { diff --git a/crates/chia-bls/src/signature.rs b/crates/chia-bls/src/signature.rs index 879200e98..0123e8d52 100644 --- a/crates/chia-bls/src/signature.rs +++ b/crates/chia-bls/src/signature.rs @@ -475,6 +475,34 @@ pub fn sign>(sk: &SecretKey, msg: Msg) -> Signature { sign_raw(sk, aug_msg) } +#[cfg(feature = "serde")] +impl serde::Serialize for Signature { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + chia_traits::serialize_hex_or_bytes( + &self.to_bytes(), + serializer, + chia_traits::PrefixKind::PreferPrefix, + ) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Signature { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let bytes = chia_traits::deserialize_hex_or_bytes( + deserializer, + chia_traits::PrefixKind::PreferPrefix, + )?; + Self::from_bytes(&bytes).map_err(serde::de::Error::custom) + } +} + #[cfg(feature = "py-bindings")] #[pyo3::pymethods] impl Signature { diff --git a/crates/chia-protocol/Cargo.toml b/crates/chia-protocol/Cargo.toml index 86e2de89b..7ee0d995e 100644 --- a/crates/chia-protocol/Cargo.toml +++ b/crates/chia-protocol/Cargo.toml @@ -14,6 +14,7 @@ workspace = true [features] py-bindings = ["dep:pyo3", "dep:chia_py_streamable_macro", "chia-traits/py-bindings", "chia-bls/py-bindings"] arbitrary = ["dep:arbitrary", "chia-bls/arbitrary"] +serde = ["dep:serde", "dep:serde_with", "chia-traits/serde", "chia-bls/serde"] [dependencies] pyo3 = { workspace = true, features = ["multiple-pymethods", "num-bigint"], optional = true } @@ -26,9 +27,12 @@ clvm-traits = { workspace = true, features = ["derive"] } clvm-utils = { workspace = true } chia-bls = { workspace = true } arbitrary = { workspace = true, features = ["derive"], optional = true } +serde = { workspace = true, features = ["derive"], optional = true } +serde_with = { workspace = true, optional = true } [dev-dependencies] rstest = { workspace = true } +serde_json = { workspace = true } [lib] crate-type = ["rlib"] diff --git a/crates/chia-protocol/src/bytes.rs b/crates/chia-protocol/src/bytes.rs index 87426e71b..5bc0e61f4 100644 --- a/crates/chia-protocol/src/bytes.rs +++ b/crates/chia-protocol/src/bytes.rs @@ -19,10 +19,19 @@ use pyo3::prelude::*; #[cfg(feature = "py-bindings")] use pyo3::types::PyBytes; +#[cfg(not(feature = "serde"))] #[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Bytes(Vec); +#[cfg(feature = "serde")] +#[serde_with::serde_as] +#[derive( + Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, +)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct Bytes(#[serde_as(as = "chia_traits::HexOrBytes")] Vec); + impl Bytes { pub fn new(bytes: Vec) -> Self { Self(bytes) @@ -161,10 +170,21 @@ impl Deref for Bytes { } } +#[cfg(not(feature = "serde"))] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct BytesImpl([u8; N]); +#[cfg(feature = "serde")] +#[serde_with::serde_as] +#[derive( + Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, +)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct BytesImpl( + #[serde_as(as = "chia_traits::HexOrBytes")] [u8; N], +); + impl BytesImpl { pub const fn new(bytes: [u8; N]) -> Self { Self(bytes) diff --git a/crates/chia-protocol/src/coin.rs b/crates/chia-protocol/src/coin.rs index b473b9aa4..2afcccb3f 100644 --- a/crates/chia-protocol/src/coin.rs +++ b/crates/chia-protocol/src/coin.rs @@ -11,6 +11,11 @@ use pyo3::prelude::*; #[streamable] #[derive(Copy)] +#[cfg_attr( + feature = "serde", + serde_with::serde_as, + derive(serde::Serialize, serde::Deserialize) +)] pub struct Coin { parent_coin_info: Bytes32, puzzle_hash: Bytes32, diff --git a/crates/chia-protocol/src/coin_spend.rs b/crates/chia-protocol/src/coin_spend.rs index 7e52cba2a..0c0cef353 100644 --- a/crates/chia-protocol/src/coin_spend.rs +++ b/crates/chia-protocol/src/coin_spend.rs @@ -4,6 +4,11 @@ use crate::coin::Coin; use crate::program::Program; #[streamable] +#[cfg_attr( + feature = "serde", + serde_with::serde_as, + derive(serde::Serialize, serde::Deserialize) +)] pub struct CoinSpend { coin: Coin, puzzle_reveal: Program, diff --git a/crates/chia-protocol/src/coin_state.rs b/crates/chia-protocol/src/coin_state.rs index 07c16de1f..0d9ea47e3 100644 --- a/crates/chia-protocol/src/coin_state.rs +++ b/crates/chia-protocol/src/coin_state.rs @@ -3,6 +3,11 @@ use chia_streamable_macro::streamable; #[streamable] #[derive(Copy)] +#[cfg_attr( + feature = "serde", + serde_with::serde_as, + derive(serde::Serialize, serde::Deserialize) +)] pub struct CoinState { coin: Coin, spent_height: Option, diff --git a/crates/chia-protocol/src/program.rs b/crates/chia-protocol/src/program.rs index 978429ed5..51679a6f3 100644 --- a/crates/chia-protocol/src/program.rs +++ b/crates/chia-protocol/src/program.rs @@ -17,6 +17,7 @@ use std::ops::Deref; #[cfg_attr(feature = "py-bindings", pyclass, derive(PyStreamable))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Program(Bytes); impl Default for Program { diff --git a/crates/chia-protocol/src/spend_bundle.rs b/crates/chia-protocol/src/spend_bundle.rs index 62778fa65..e07ba750b 100644 --- a/crates/chia-protocol/src/spend_bundle.rs +++ b/crates/chia-protocol/src/spend_bundle.rs @@ -16,6 +16,11 @@ use clvmr::ENABLE_FIXED_DIV; use pyo3::prelude::*; #[streamable(subclass)] +#[cfg_attr( + feature = "serde", + serde_with::serde_as, + derive(serde::Serialize, serde::Deserialize) +)] pub struct SpendBundle { coin_spends: Vec, aggregated_signature: G2Element, @@ -247,3 +252,56 @@ ffff0101\ }); } } + +#[cfg(test)] +#[cfg(feature = "serde")] +mod test_serde { + use chia_bls::Signature; + + use crate::{Bytes32, Coin, CoinSpend, Program}; + + use super::SpendBundle; + + #[test] + fn test_serde_json() { + let coin1 = Coin::new(Bytes32::new([1; 32]), Bytes32::new([2; 32]), 1); + let coin2 = Coin::new(Bytes32::new([3; 32]), Bytes32::new([4; 32]), 2); + let puzzle_reveal1 = Program::from(vec![5; 20]); + let puzzle_reveal2 = Program::from(vec![6; 40]); + let solution1 = Program::from(vec![7; 10]); + let solution2 = Program::from(vec![8; 30]); + + let cs1 = CoinSpend::new(coin1, puzzle_reveal1, solution1); + let cs2 = CoinSpend::new(coin2, puzzle_reveal2, solution2); + + let spend_bundle = SpendBundle::new(vec![cs1, cs2], Signature::default()); + let json = serde_json::to_string_pretty(&spend_bundle).unwrap(); + + assert_eq!( + json, + r#"{ + "coin_spends": [ + { + "coin": { + "parent_coin_info": "0x0101010101010101010101010101010101010101010101010101010101010101", + "puzzle_hash": "0x0202020202020202020202020202020202020202020202020202020202020202", + "amount": 1 + }, + "puzzle_reveal": "0505050505050505050505050505050505050505", + "solution": "07070707070707070707" + }, + { + "coin": { + "parent_coin_info": "0x0303030303030303030303030303030303030303030303030303030303030303", + "puzzle_hash": "0x0404040404040404040404040404040404040404040404040404040404040404", + "amount": 2 + }, + "puzzle_reveal": "06060606060606060606060606060606060606060606060606060606060606060606060606060606", + "solution": "080808080808080808080808080808080808080808080808080808080808" + } + ], + "aggregated_signature": "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +}"# + ); + } +} diff --git a/crates/chia-traits/Cargo.toml b/crates/chia-traits/Cargo.toml index 9dadf80f9..d55cba272 100644 --- a/crates/chia-traits/Cargo.toml +++ b/crates/chia-traits/Cargo.toml @@ -11,9 +11,19 @@ workspace = true [features] py-bindings = ["dep:pyo3"] +serde = ["dep:serde", "dep:serde_with", "dep:hex"] [dependencies] pyo3 = { workspace = true, features = ["multiple-pymethods"], optional = true } chia_streamable_macro = { workspace = true } clvmr = { workspace = true } thiserror = { workspace = true } +serde = { workspace = true, optional = true } +serde_with = { workspace = true, optional = true } +hex = { workspace = true, optional = true } + +[dev-dependencies] +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +bincode = { workspace = true } +hex-literal = { workspace = true } diff --git a/crates/chia-traits/src/lib.rs b/crates/chia-traits/src/lib.rs index 4b66c7239..f638394a1 100644 --- a/crates/chia-traits/src/lib.rs +++ b/crates/chia-traits/src/lib.rs @@ -17,3 +17,9 @@ pub use crate::streamable::*; pub mod int; #[cfg(feature = "py-bindings")] pub use crate::int::*; + +#[cfg(feature = "serde")] +mod serde; + +#[cfg(feature = "serde")] +pub use crate::serde::*; diff --git a/crates/chia-traits/src/serde.rs b/crates/chia-traits/src/serde.rs new file mode 100644 index 000000000..fd12d6555 --- /dev/null +++ b/crates/chia-traits/src/serde.rs @@ -0,0 +1,317 @@ +use std::{fmt, marker::PhantomData}; + +use serde::{ + de::{self, Visitor}, + Deserializer, Serializer, +}; +use serde_with::{DeserializeAs, SerializeAs}; + +pub struct PreferPrefix; +pub struct AllowPrefix; +pub struct NoPrefix; +pub struct RequirePrefix; + +#[allow(clippy::enum_variant_names)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PrefixKind { + PreferPrefix, + AllowPrefix, + NoPrefix, + RequirePrefix, +} + +pub struct HexOrBytes

(PhantomData

); + +pub fn serialize_hex_or_bytes( + source: &impl AsRef<[u8]>, + serializer: S, + kind: PrefixKind, +) -> Result +where + S: Serializer, +{ + if serializer.is_human_readable() { + let mut string = hex::encode(source); + match kind { + PrefixKind::PreferPrefix | PrefixKind::RequirePrefix => string.insert_str(0, "0x"), + PrefixKind::AllowPrefix | PrefixKind::NoPrefix => {} + } + serializer.serialize_str(&string) + } else { + serializer.serialize_bytes(source.as_ref()) + } +} + +pub fn deserialize_hex_or_bytes<'de, D, T>(deserializer: D, kind: PrefixKind) -> Result +where + D: Deserializer<'de>, + T: TryFrom>, +{ + if deserializer.is_human_readable() { + struct HexOrBytesVisitor(PrefixKind); + + impl<'de> Visitor<'de> for HexOrBytesVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + PrefixKind::AllowPrefix | PrefixKind::PreferPrefix => formatter + .write_str("a byte buffer or hex string with an optional 0x prefix"), + PrefixKind::NoPrefix => formatter.write_str("a byte buffer or hex string"), + PrefixKind::RequirePrefix => { + formatter.write_str("a byte buffer or hex string with a 0x prefix") + } + } + } + + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(v.to_vec()) + } + + fn visit_byte_buf(self, v: Vec) -> Result { + Ok(v) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + if self.0 == PrefixKind::RequirePrefix + && !(v.starts_with("0x") || v.starts_with("0X")) + { + return Err(de::Error::custom("Hex string missing 0x prefix")); + } + + if matches!( + self.0, + PrefixKind::AllowPrefix | PrefixKind::PreferPrefix | PrefixKind::RequirePrefix + ) { + if let Some(rest) = v.strip_prefix("0x") { + Ok(hex::decode(rest).map_err(de::Error::custom)?) + } else if let Some(rest) = v.strip_prefix("0X") { + Ok(hex::decode(rest).map_err(de::Error::custom)?) + } else { + Ok(hex::decode(v).map_err(de::Error::custom)?) + } + } else { + Ok(hex::decode(v).map_err(de::Error::custom)?) + } + } + + fn visit_string(self, v: String) -> Result + where + E: de::Error, + { + self.visit_str(&v) + } + } + + let bytes = deserializer.deserialize_any(HexOrBytesVisitor(kind))?; + let length = bytes.len(); + + bytes.try_into().map_err(|_: T::Error| { + de::Error::custom(format_args!( + "Can't convert a byte buffer of length {length} to the output type." + )) + }) + } else { + struct BytesVisitor; + + impl<'de> Visitor<'de> for BytesVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a byte buffer") + } + + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(v.to_vec()) + } + + fn visit_byte_buf(self, v: Vec) -> Result { + Ok(v) + } + } + + let bytes = deserializer.deserialize_byte_buf(BytesVisitor)?; + let length = bytes.len(); + + bytes.try_into().map_err(|_: T::Error| { + de::Error::custom(format_args!( + "Can't convert a byte buffer of length {length} to the output type." + )) + }) + } +} + +macro_rules! hex_or_bytes { + ( $prefix:ident ) => { + impl SerializeAs for HexOrBytes<$prefix> + where + T: AsRef<[u8]>, + { + fn serialize_as(source: &T, serializer: S) -> Result + where + S: Serializer, + { + serialize_hex_or_bytes(source, serializer, PrefixKind::$prefix) + } + } + + impl<'de, T> DeserializeAs<'de, T> for HexOrBytes<$prefix> + where + T: TryFrom>, + { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserialize_hex_or_bytes(deserializer, PrefixKind::$prefix) + } + } + }; +} + +hex_or_bytes!(PreferPrefix); +hex_or_bytes!(AllowPrefix); +hex_or_bytes!(NoPrefix); +hex_or_bytes!(RequirePrefix); + +#[cfg(test)] +mod tests { + use de::DeserializeOwned; + use fmt::Debug; + use hex_literal::hex; + use serde::{Deserialize, Serialize}; + use serde_with::serde_as; + + use super::*; + + #[serde_as] + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct BytesDefault(#[serde_as(as = "HexOrBytes")] Vec); + + #[serde_as] + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct BytesAllowPrefix(#[serde_as(as = "HexOrBytes")] Vec); + + #[serde_as] + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct BytesPreferPrefix(#[serde_as(as = "HexOrBytes")] Vec); + + #[serde_as] + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct BytesNoPrefix(#[serde_as(as = "HexOrBytes")] Vec); + + #[serde_as] + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct BytesRequirePrefix(#[serde_as(as = "HexOrBytes")] Vec); + + fn roundtrip_json(value: &T, hex: &str) + where + T: Debug + PartialEq + Serialize + DeserializeOwned, + { + let json = serde_json::to_string(&value).unwrap(); + assert_eq!(json, format!("\"{hex}\"")); + + let roundtrip: T = serde_json::from_str(&json).unwrap(); + assert_eq!(&roundtrip, value); + } + + fn try_parse_json(hex: &str, value: &T) + where + T: Debug + PartialEq + DeserializeOwned, + { + let actual = serde_json::from_str::(&format!("\"{hex}\"")).unwrap(); + assert_eq!(&actual, value); + } + + fn try_error_json(hex: &str) + where + T: Debug + PartialEq + DeserializeOwned, + { + let actual = serde_json::from_str::(&format!("\"{hex}\"")); + assert!(actual.is_err()); + } + + fn roundtrip_binary(value: &T, hex: &str) + where + T: Debug + PartialEq + Serialize + DeserializeOwned, + { + let bytes = bincode::serialize(value).unwrap(); + assert_eq!(hex::encode(&bytes), hex); + + let roundtrip: T = bincode::deserialize(&bytes).unwrap(); + assert_eq!(&roundtrip, value); + } + + #[test] + fn test_bytes_as_json() { + roundtrip_json(&BytesDefault(hex!("cafef00d").to_vec()), "cafef00d"); + try_parse_json("0xcafef00d", &BytesDefault(hex!("cafef00d").to_vec())); + } + + #[test] + fn test_bytes_allow_prefix_as_json() { + roundtrip_json(&BytesAllowPrefix(hex!("cafef00d").to_vec()), "cafef00d"); + try_parse_json("0xcafef00d", &BytesAllowPrefix(hex!("cafef00d").to_vec())); + } + + #[test] + fn test_bytes_prefer_prefix_as_json() { + roundtrip_json(&BytesPreferPrefix(hex!("cafef00d").to_vec()), "0xcafef00d"); + try_parse_json("cafef00d", &BytesPreferPrefix(hex!("cafef00d").to_vec())); + } + + #[test] + fn test_bytes_no_prefix_as_json() { + roundtrip_json(&BytesNoPrefix(hex!("cafef00d").to_vec()), "cafef00d"); + try_error_json::("0xcafef00d"); + } + + #[test] + fn test_bytes_require_prefix_as_json() { + roundtrip_json(&BytesRequirePrefix(hex!("cafef00d").to_vec()), "0xcafef00d"); + try_error_json::("cafef00d"); + } + + #[test] + fn test_bytes_as_binary() { + roundtrip_binary( + &BytesDefault(hex!("cafef00d").to_vec()), + "0400000000000000cafef00d", + ); + } + + #[test] + fn test_bytes_allow_prefix_as_binary() { + roundtrip_binary( + &BytesAllowPrefix(hex!("cafef00d").to_vec()), + "0400000000000000cafef00d", + ); + } + + #[test] + fn test_bytes_prefer_prefix_as_binary() { + roundtrip_binary( + &BytesPreferPrefix(hex!("cafef00d").to_vec()), + "0400000000000000cafef00d", + ); + } + + #[test] + fn test_bytes_no_prefix_as_binary() { + roundtrip_binary( + &BytesNoPrefix(hex!("cafef00d").to_vec()), + "0400000000000000cafef00d", + ); + } + + #[test] + fn test_bytes_require_prefix_as_binary() { + roundtrip_binary( + &BytesRequirePrefix(hex!("cafef00d").to_vec()), + "0400000000000000cafef00d", + ); + } +} diff --git a/crates/clvm-utils/Cargo.toml b/crates/clvm-utils/Cargo.toml index 5ed6e2a56..b7801b0e2 100644 --- a/crates/clvm-utils/Cargo.toml +++ b/crates/clvm-utils/Cargo.toml @@ -11,10 +11,16 @@ repository = "https://github.com/Chia-Network/chia_rs" [lints] workspace = true +[features] +serde = ["dep:serde", "dep:serde_with", "dep:chia-traits"] + [dependencies] clvmr = { workspace = true } clvm-traits = { workspace = true } hex = { workspace = true } +serde = { workspace = true, features = ["derive"], optional = true } +serde_with = { workspace = true, optional = true } +chia-traits = { workspace = true, features = ["serde"], optional = true } [dev-dependencies] rstest = { workspace = true } diff --git a/crates/clvm-utils/src/tree_hash.rs b/crates/clvm-utils/src/tree_hash.rs index df9e12038..984852c65 100644 --- a/crates/clvm-utils/src/tree_hash.rs +++ b/crates/clvm-utils/src/tree_hash.rs @@ -6,7 +6,18 @@ use std::ops::Deref; use std::{fmt, io}; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct TreeHash([u8; 32]); +#[cfg_attr( + feature = "serde", + serde_with::serde_as, + derive(serde::Serialize, serde::Deserialize) +)] +pub struct TreeHash( + #[cfg_attr( + feature = "serde", + serde_as(as = "chia_traits::HexOrBytes") + )] + [u8; 32], +); impl TreeHash { pub const fn new(hash: [u8; 32]) -> Self {