Skip to content

Commit

Permalink
feat: de/serialize based on human readability (#532)
Browse files Browse the repository at this point in the history
Co-authored-by: Jonathan LEI <[email protected]>
  • Loading branch information
kariy and xJonathanLEI authored Jul 16, 2024
1 parent 08a7807 commit b8a0003
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 23 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions starknet-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
46 changes: 43 additions & 3 deletions starknet-core/src/serde/num_hex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
Expand All @@ -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<E>(self, v: u64) -> Result<Self::Value, E>
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::<TestStruct>(&hex!("3412000000000000")).unwrap();
assert_eq!(r.0, 0x1234);
}
}
184 changes: 167 additions & 17 deletions starknet-core/src/serde/unsigned_field_element.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use alloc::{fmt::Formatter, format};

use crypto_bigint::U256;
use serde::{
de::{Error as DeError, Visitor},
Deserializer, Serializer,
Expand All @@ -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;
Expand All @@ -23,7 +27,11 @@ impl SerializeAs<Felt> 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())
}
}
}

Expand All @@ -32,15 +40,19 @@ 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)
}
}
}

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<E>(self, v: &str) -> Result<Self::Value, E>
Expand All @@ -49,16 +61,33 @@ impl<'de> Visitor<'de> for UfeHexVisitor {
{
Felt::from_hex(v).map_err(|err| DeError::custom(format!("invalid hex string: {err}")))
}

fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
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<Option<Felt>> for UfeHexOption {
fn serialize_as<S>(value: &Option<Felt>, serializer: S) -> Result<S::Ok, S::Error>
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(&[]),
}
}
}
}
Expand All @@ -68,7 +97,11 @@ impl<'de> DeserializeAs<'de, Option<Felt>> for UfeHexOption {
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(UfeHexOptionVisitor)
if deserializer.is_human_readable() {
deserializer.deserialize_any(UfeHexOptionVisitor)
} else {
deserializer.deserialize_bytes(UfeHexOptionVisitor)
}
}
}

Expand All @@ -91,17 +124,38 @@ impl<'de> Visitor<'de> for UfeHexOptionVisitor {
},
}
}

fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
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<Option<Felt>> for UfePendingBlockHash {
fn serialize_as<S>(value: &Option<Felt>, serializer: S) -> Result<S::Ok, S::Error>
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(&[]),
}
}
}
}
Expand All @@ -111,7 +165,11 @@ impl<'de> DeserializeAs<'de, Option<Felt>> for UfePendingBlockHash {
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(UfePendingBlockHashVisitor)
if deserializer.is_human_readable() {
deserializer.deserialize_any(UfePendingBlockHashVisitor)
} else {
deserializer.deserialize_bytes(UfePendingBlockHashVisitor)
}
}
}

Expand All @@ -135,23 +193,115 @@ impl<'de> Visitor<'de> for UfePendingBlockHashVisitor {
}
}
}

fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
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<Felt>);
#[derive(Serialize, Deserialize)]
struct TestStruct(#[serde_as(as = "UfeHex")] pub Felt);

#[serde_as]
#[derive(Serialize, Deserialize)]
struct TestOptionStruct(#[serde_as(as = "UfeHexOption")] pub Option<Felt>);

#[serde_as]
#[derive(Serialize, Deserialize)]
struct TestBlockHashStruct(#[serde_as(as = "UfePendingBlockHash")] pub Option<Felt>);

#[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::<TestStruct>(&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::<TestStruct>(&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::<TestOptionStruct>("\"\"").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::<TestOptionStruct>(&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::<TestBlockHashStruct>("\"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::<TestStruct>("\"\"").unwrap();
fn pending_block_hash_bin_deser_none() {
let r = bincode::deserialize::<TestBlockHashStruct>(&hex!("0000000000000000")).unwrap();
assert_eq!(r.0, None);
}
}
Loading

0 comments on commit b8a0003

Please sign in to comment.