diff --git a/Cargo.lock b/Cargo.lock index 4d5b040b..913a3aa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,6 +134,15 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[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 = "1.3.2" @@ -2063,6 +2072,7 @@ name = "starknet-core" version = "0.11.1" dependencies = [ "base64 0.21.0", + "bincode", "criterion", "crypto-bigint", "flate2", diff --git a/starknet-core/Cargo.toml b/starknet-core/Cargo.toml index a9215c23..a035ded2 100644 --- a/starknet-core/Cargo.toml +++ b/starknet-core/Cargo.toml @@ -30,6 +30,7 @@ sha3 = { version = "0.10.7", default-features = false } starknet-types-core = { version = "0.1.3", default-features = false, features = ["curve", "serde", "num-traits"] } [dev-dependencies] +bincode = "1.3.3" criterion = { version = "0.4.0", default-features = false } hex-literal = "0.4.1" starknet-core = { path = ".", features = ["no_unknown_fields"] } diff --git a/starknet-core/src/serde/num_hex.rs b/starknet-core/src/serde/num_hex.rs index 985b9053..6268fb1e 100644 --- a/starknet-core/src/serde/num_hex.rs +++ b/starknet-core/src/serde/num_hex.rs @@ -9,21 +9,29 @@ pub mod u64 { where S: Serializer, { - serializer.serialize_str(&format!("{value:#x}")) + if serializer.is_human_readable() { + serializer.serialize_str(&format!("{value:#x}")) + } else { + serializer.serialize_u64(*value) + } } pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - deserializer.deserialize_any(NumHexVisitor) + if deserializer.is_human_readable() { + deserializer.deserialize_str(NumHexVisitor) + } else { + deserializer.deserialize_u64(NumHexVisitor) + } } impl<'de> Visitor<'de> for NumHexVisitor { type Value = u64; fn expecting(&self, formatter: &mut Formatter) -> alloc::fmt::Result { - write!(formatter, "string") + write!(formatter, "string, or an array of u8") } fn visit_str(self, v: &str) -> Result @@ -33,5 +41,37 @@ pub mod u64 { u64::from_str_radix(v.trim_start_matches("0x"), 16) .map_err(|err| serde::de::Error::custom(format!("invalid u64 hex string: {err}"))) } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + Ok(v) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use hex_literal::hex; + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize)] + struct TestStruct(#[serde(with = "u64")] pub u64); + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn bin_ser() { + let r = bincode::serialize(&TestStruct(0x1234)).unwrap(); + assert_eq!(r, hex!("3412000000000000")); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn bin_deser() { + let r = bincode::deserialize::(&hex!("3412000000000000")).unwrap(); + assert_eq!(r.0, 0x1234); } } diff --git a/starknet-core/src/serde/unsigned_field_element.rs b/starknet-core/src/serde/unsigned_field_element.rs index 38317ff9..f443797a 100644 --- a/starknet-core/src/serde/unsigned_field_element.rs +++ b/starknet-core/src/serde/unsigned_field_element.rs @@ -1,5 +1,6 @@ use alloc::{fmt::Formatter, format}; +use crypto_bigint::U256; use serde::{ de::{Error as DeError, Visitor}, Deserializer, Serializer, @@ -8,6 +9,9 @@ use serde_with::{DeserializeAs, SerializeAs}; use starknet_types_core::felt::Felt; +const PRIME: U256 = + U256::from_be_hex("0800000000000011000000000000000000000000000000000000000000000001"); + pub struct UfeHex; pub struct UfeHexOption; @@ -23,7 +27,11 @@ impl SerializeAs for UfeHex { where S: Serializer, { - serializer.serialize_str(&format!("{value:#x}")) + if serializer.is_human_readable() { + serializer.serialize_str(&format!("{value:#x}")) + } else { + serializer.serialize_bytes(&value.to_bytes_be()) + } } } @@ -32,7 +40,11 @@ impl<'de> DeserializeAs<'de, Felt> for UfeHex { where D: Deserializer<'de>, { - deserializer.deserialize_any(UfeHexVisitor) + if deserializer.is_human_readable() { + deserializer.deserialize_any(UfeHexVisitor) + } else { + deserializer.deserialize_bytes(UfeHexVisitor) + } } } @@ -40,7 +52,7 @@ impl<'de> Visitor<'de> for UfeHexVisitor { type Value = Felt; fn expecting(&self, formatter: &mut Formatter) -> alloc::fmt::Result { - write!(formatter, "string") + write!(formatter, "a hex string, or an array of u8") } fn visit_str(self, v: &str) -> Result @@ -49,6 +61,16 @@ impl<'de> Visitor<'de> for UfeHexVisitor { { Felt::from_hex(v).map_err(|err| DeError::custom(format!("invalid hex string: {err}"))) } + + fn visit_bytes(self, v: &[u8]) -> Result { + let buf = <[u8; 32]>::try_from(v).map_err(serde::de::Error::custom)?; + + if U256::from_be_slice(&buf) < PRIME { + Ok(Felt::from_bytes_be(&buf)) + } else { + Err(serde::de::Error::custom("field element value out of range")) + } + } } impl SerializeAs> for UfeHexOption { @@ -56,9 +78,16 @@ impl SerializeAs> for UfeHexOption { where S: Serializer, { - match value { - Some(value) => serializer.serialize_str(&format!("{value:#064x}")), - None => serializer.serialize_none(), + if serializer.is_human_readable() { + match value { + Some(value) => serializer.serialize_str(&format!("{value:#064x}")), + None => serializer.serialize_none(), + } + } else { + match value { + Some(value) => serializer.serialize_bytes(&value.to_bytes_be()), + None => serializer.serialize_bytes(&[]), + } } } } @@ -68,7 +97,11 @@ impl<'de> DeserializeAs<'de, Option> for UfeHexOption { where D: Deserializer<'de>, { - deserializer.deserialize_any(UfeHexOptionVisitor) + if deserializer.is_human_readable() { + deserializer.deserialize_any(UfeHexOptionVisitor) + } else { + deserializer.deserialize_bytes(UfeHexOptionVisitor) + } } } @@ -91,6 +124,20 @@ impl<'de> Visitor<'de> for UfeHexOptionVisitor { }, } } + + fn visit_bytes(self, v: &[u8]) -> Result { + if v.is_empty() { + return Ok(None); + } + + let buf = <[u8; 32]>::try_from(v).map_err(serde::de::Error::custom)?; + + if U256::from_be_slice(&buf) < PRIME { + Ok(Some(Felt::from_bytes_be(&buf))) + } else { + Err(serde::de::Error::custom("field element value out of range")) + } + } } impl SerializeAs> for UfePendingBlockHash { @@ -98,10 +145,17 @@ impl SerializeAs> for UfePendingBlockHash { where S: Serializer, { - match value { - Some(value) => serializer.serialize_str(&format!("{value:#064x}")), - // We don't know if it's `null` or `"pending"` - None => serializer.serialize_none(), + if serializer.is_human_readable() { + match value { + Some(value) => serializer.serialize_str(&format!("{value:#064x}")), + // We don't know if it's `null` or `"pending"` + None => serializer.serialize_none(), + } + } else { + match value { + Some(value) => serializer.serialize_bytes(&value.to_bytes_be()), + None => serializer.serialize_bytes(&[]), + } } } } @@ -111,7 +165,11 @@ impl<'de> DeserializeAs<'de, Option> for UfePendingBlockHash { where D: Deserializer<'de>, { - deserializer.deserialize_any(UfePendingBlockHashVisitor) + if deserializer.is_human_readable() { + deserializer.deserialize_any(UfePendingBlockHashVisitor) + } else { + deserializer.deserialize_bytes(UfePendingBlockHashVisitor) + } } } @@ -135,23 +193,115 @@ impl<'de> Visitor<'de> for UfePendingBlockHashVisitor { } } } + + fn visit_bytes(self, v: &[u8]) -> Result { + if v.is_empty() { + return Ok(None); + } + + let buf = <[u8; 32]>::try_from(v).map_err(serde::de::Error::custom)?; + + if U256::from_be_slice(&buf) < PRIME { + Ok(Some(Felt::from_bytes_be(&buf))) + } else { + Err(serde::de::Error::custom("field element value out of range")) + } + } } #[cfg(test)] mod tests { use super::*; - use serde::Deserialize; + use hex_literal::hex; + use serde::{Deserialize, Serialize}; use serde_with::serde_as; #[serde_as] - #[derive(Deserialize)] - struct TestStruct(#[serde_as(as = "UfeHexOption")] pub Option); + #[derive(Serialize, Deserialize)] + struct TestStruct(#[serde_as(as = "UfeHex")] pub Felt); + + #[serde_as] + #[derive(Serialize, Deserialize)] + struct TestOptionStruct(#[serde_as(as = "UfeHexOption")] pub Option); + + #[serde_as] + #[derive(Serialize, Deserialize)] + struct TestBlockHashStruct(#[serde_as(as = "UfePendingBlockHash")] pub Option); + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn bin_ser() { + let r = bincode::serialize(&TestStruct(Felt::ONE)).unwrap(); + assert_eq!( + r, + hex!( + "2000000000000000 0000000000000000000000000000000000000000000000000000000000000001" + ) + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn bin_deser() { + let r = bincode::deserialize::(&hex!( + "2000000000000000 0000000000000000000000000000000000000000000000000000000000000001" + )) + .unwrap(); + assert_eq!(r.0, Felt::ONE); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn bin_deser_out_of_range() { + if bincode::deserialize::(&hex!( + "2000000000000000 0800000000000011000000000000000000000000000000000000000000000001" + )) + .is_ok() + { + panic!("deserialization should fail") + } + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn option_deser_empty_string() { + let r = serde_json::from_str::("\"\"").unwrap(); + assert_eq!(r.0, None); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn option_bin_ser_none() { + let r = bincode::serialize(&TestOptionStruct(None)).unwrap(); + assert_eq!(r, hex!("0000000000000000")); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn option_bin_deser_none() { + let r = bincode::deserialize::(&hex!("0000000000000000")).unwrap(); + assert_eq!(r.0, None); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn pending_block_hash_deser_pending() { + let r = serde_json::from_str::("\"pending\"").unwrap(); + assert_eq!(r.0, None); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn pending_block_hash_bin_ser_none() { + let r = bincode::serialize(&TestBlockHashStruct(None)).unwrap(); + assert_eq!(r, hex!("0000000000000000")); + } #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] - fn empty_string_deser() { - let r = serde_json::from_str::("\"\"").unwrap(); + fn pending_block_hash_bin_deser_none() { + let r = bincode::deserialize::(&hex!("0000000000000000")).unwrap(); assert_eq!(r.0, None); } } diff --git a/starknet-core/src/types/hash_256.rs b/starknet-core/src/types/hash_256.rs index dd3e31d0..ec0bb839 100644 --- a/starknet-core/src/types/hash_256.rs +++ b/starknet-core/src/types/hash_256.rs @@ -78,7 +78,11 @@ impl Serialize for Hash256 { where S: serde::Serializer, { - serializer.serialize_str(&format!("0x{}", hex::encode(self.inner))) + if serializer.is_human_readable() { + serializer.serialize_str(&format!("0x{}", hex::encode(self.inner))) + } else { + serializer.serialize_bytes(self.as_bytes()) + } } } @@ -87,7 +91,11 @@ impl<'de> Deserialize<'de> for Hash256 { where D: serde::Deserializer<'de>, { - deserializer.deserialize_any(Hash256Visitor) + if deserializer.is_human_readable() { + deserializer.deserialize_any(Hash256Visitor) + } else { + deserializer.deserialize_bytes(Hash256Visitor) + } } } @@ -95,7 +103,7 @@ impl<'de> Visitor<'de> for Hash256Visitor { type Value = Hash256; fn expecting(&self, formatter: &mut Formatter) -> alloc::fmt::Result { - write!(formatter, "string") + write!(formatter, "string, or an array of u8") } fn visit_str(self, v: &str) -> Result @@ -105,6 +113,12 @@ impl<'de> Visitor<'de> for Hash256Visitor { v.parse() .map_err(|err| serde::de::Error::custom(format!("{}", err))) } + + fn visit_bytes(self, v: &[u8]) -> Result { + <[u8; HASH_256_BYTE_COUNT]>::try_from(v) + .map(Hash256::from_bytes) + .map_err(serde::de::Error::custom) + } } impl FromStr for Hash256 { @@ -186,6 +200,8 @@ impl From<[u8; HASH_256_BYTE_COUNT]> for Hash256 { mod tests { use super::{Felt, FromHexError, Hash256, HASH_256_BYTE_COUNT}; + use hex_literal::hex; + #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] fn test_hash_256_from_hex_error_unexpected_length() { @@ -243,4 +259,32 @@ mod tests { // Assert that the conversion from the `Felt` to `Hash256` is successful assert_eq!(Hash256::from_felt(&felt), hash_256); } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn bin_ser() { + let r = bincode::serialize(&Hash256::from_bytes(hex!( + "1111111111111111111111111111111111111111111111111111111111111111" + ))) + .unwrap(); + assert_eq!( + r, + hex!( + "2000000000000000 1111111111111111111111111111111111111111111111111111111111111111" + ) + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn bin_deser() { + let r = bincode::deserialize::(&hex!( + "2000000000000000 1111111111111111111111111111111111111111111111111111111111111111" + )) + .unwrap(); + assert_eq!( + r.inner, + hex!("1111111111111111111111111111111111111111111111111111111111111111") + ); + } }