Re-implement serialization (#201)

* Serialization tests now pass again

* Tidy up and test more edge-cases

* Use attributes rather than custom serializer for arrays

* Add JSON Schema support

* Tests for JSON Schema generation and validation

* Add comments

* Support custom predicates

* Clippy fixes

* Make deserialization/constructor functions pub(crate)
This commit is contained in:
Rob Knight 2025-04-22 04:19:20 -07:00 committed by GitHub
parent 26a6b2d143
commit bf6d8aee8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 554 additions and 255 deletions

View file

@ -4,7 +4,6 @@ use std::{collections::HashMap, fmt, hash as h, iter, iter::zip, sync::Arc};
use anyhow::{anyhow, Result};
use schemars::JsonSchema;
// use serde::{Deserialize, Serialize};
use crate::{
frontend::{AnchoredKey, Statement, StatementArg},
middleware::{

View file

@ -5,10 +5,8 @@ use std::{collections::HashMap, convert::From, fmt};
use anyhow::{anyhow, Result};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
// use schemars::JsonSchema;
// use serde::{Deserialize, Serialize};
use crate::middleware::{
self, check_st_tmpl, hash_str, AnchoredKey, Key, MainPodInputs, NativeOperation,
NativePredicate, OperationAux, OperationType, Params, PodId, PodProver, PodSigner, Predicate,
@ -17,8 +15,10 @@ use crate::middleware::{
mod custom;
mod operation;
mod serialization;
pub use custom::*;
pub use operation::*;
use serialization::*;
/// This type is just for presentation purposes.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
@ -66,8 +66,8 @@ impl SignedPodBuilder {
/// SignedPod is a wrapper on top of backend::SignedPod, which additionally stores the
/// string<-->hash relation of the keys.
#[derive(Debug, Clone)]
// #[serde(try_from = "SignedPodHelper", into = "SignedPodHelper")]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "SignedPodHelper", into = "SignedPodHelper")]
pub struct SignedPod {
pub pod: Box<dyn middleware::Pod>,
// We store a copy of the key values for quick access
@ -591,8 +591,8 @@ impl MainPodBuilder {
}
}
#[derive(Debug, Clone)]
// #[serde(try_from = "MainPodHelper", into = "MainPodHelper")]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "MainPodHelper", into = "MainPodHelper")]
pub struct MainPod {
pub pod: Box<dyn middleware::Pod>,
pub public_statements: Vec<Statement>,

View file

@ -1,6 +1,5 @@
use std::fmt;
// use serde::{Deserialize, Serialize};
use crate::{
frontend::SignedPod,
middleware::{AnchoredKey, OperationAux, OperationType, Statement, Value},

View file

@ -1,19 +1,19 @@
/*
use std::collections::{BTreeMap, HashMap};
use std::collections::HashMap;
use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize, Serializer};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
backends::plonky2::mock::{mainpod::MockMainPod, signedpod::MockSignedPod},
frontend::{containers::Dictionary, MainPod, SignedPod, Statement, TypedValue},
middleware::PodId,
frontend::{MainPod, SignedPod, Statement},
middleware::{containers::Dictionary, Key, PodId, Value},
};
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(title = "SignedPod")]
pub struct SignedPodHelper {
entries: HashMap<String, TypedValue>,
entries: HashMap<Key, Value>,
proof: String,
pod_class: String,
pod_type: String,
@ -30,10 +30,8 @@ impl TryFrom<SignedPodHelper> for SignedPod {
return Err(anyhow::anyhow!("pod_type is not Mock"));
}
let dict = Dictionary::new(helper.entries.clone())?
.middleware_dict()
.clone();
let pod = MockSignedPod::deserialize(PodId(dict.commitment()), helper.proof, dict);
let dict = Dictionary::new(helper.entries.clone())?.clone();
let pod = MockSignedPod::new(PodId(dict.commitment()), helper.proof, dict.kvs().clone());
Ok(SignedPod {
pod: Box::new(pod),
@ -55,6 +53,7 @@ impl From<SignedPod> for SignedPodHelper {
#[derive(Serialize, Deserialize, JsonSchema)]
#[schemars(title = "MainPod")]
#[serde(rename_all = "camelCase")]
pub struct MainPodHelper {
public_statements: Vec<Statement>,
proof: String,
@ -94,76 +93,29 @@ impl From<MainPod> for MainPodHelper {
}
}
pub fn serialize_i64<S>(value: &i64, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&value.to_string())
}
pub fn deserialize_i64<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
D: serde::Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse()
.map_err(serde::de::Error::custom)
}
// HashMap is not ordered, but we want our dictionaries to be ordered
// by key for serialization, so we turn HashMaps into BTreeMaps.
pub fn ordered_map<S, K: Ord + Serialize, V: Serialize>(
value: &HashMap<K, V>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let ordered: BTreeMap<_, _> = value.iter().collect();
ordered.serialize(serializer)
}
pub fn transform_value_schema(schema: &mut Schema) {
let obj = schema.as_object_mut().unwrap();
// Get the oneOf array which contains our variant schemas
if let Some(one_of_container) = obj.get_mut("oneOf") {
if let Some(variants) = one_of_container.as_array_mut() {
// Add String variant (untagged)
variants.push(serde_json::json!({
"type": "string"
}));
// Add Boolean variant (untagged)
variants.push(serde_json::json!({
"type": "boolean"
}));
// Add Array variant (untagged)
variants.push(serde_json::json!({
"type": "array",
"items": {
"$ref": "#/definitions/Value"
}
}));
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use schemars::generate::SchemaSettings;
use std::collections::HashSet;
use anyhow::Result;
// Pretty assertions give nicer diffs between expected and actual values
use pretty_assertions::assert_eq;
use schemars::schema_for;
// use schemars::generate::SchemaSettings;
use super::*;
use crate::{
backends::plonky2::mock::{mainpod::MockProver, signedpod::MockSigner},
examples::{zu_kyc_pod_builder, zu_kyc_sign_pod_builders},
frontend::{
containers::{Array, Dictionary, Set},
SignedPodBuilder,
examples::{
eth_dos_pod_builder, eth_friend_signed_pod_builder, zu_kyc_pod_builder,
zu_kyc_sign_pod_builders,
},
frontend::SignedPodBuilder,
middleware::{
self,
containers::{Array, Set},
Params, TypedValue,
},
middleware::{self, Params},
};
#[test]
@ -174,34 +126,32 @@ mod tests {
(TypedValue::Int(42), "{\"Int\":\"42\"}"),
(TypedValue::Bool(true), "true"),
(
TypedValue::Array(
Array::new(vec![
TypedValue::String("foo".to_string()),
TypedValue::Bool(false),
])
.unwrap(),
),
TypedValue::Array(Array::new(vec!["foo".into(), false.into()]).unwrap()),
"[\"foo\",false]",
),
(
TypedValue::Dictionary(
Dictionary::new(HashMap::from([
("foo".to_string(), TypedValue::Int(123)),
("bar".to_string(), TypedValue::String("baz".to_string())),
// The set of valid keys is equal to the set of valid JSON keys
("foo".into(), 123.into()),
// Empty strings are valid JSON keys
(("".into()), "baz".into()),
// Keys can contain whitespace
((" hi".into()), false.into()),
// Keys can contain special characters
(("!@£$%^&&*()".into()), "".into()),
// Keys can contain _very_ special characters
(("\0".into()), "".into()),
// Keys can contain emojis
(("🥳".into()), "party time!".into()),
]))
.unwrap(),
),
"{\"Dictionary\":{\"bar\":\"baz\",\"foo\":{\"Int\":\"123\"}}}",
"{\"Dictionary\":{\"\":\"baz\",\"\\u0000\":\"\",\" hi\":false,\"!@£$%^&&*()\":\"\",\"foo\":{\"Int\":\"123\"},\"🥳\":\"party time!\"}}",
),
(
TypedValue::Set(
Set::new(vec![
TypedValue::String("foo".to_string()),
TypedValue::String("bar".to_string()),
])
.unwrap(),
),
"{\"Set\":[\"foo\",\"bar\"]}",
TypedValue::Set(Set::new(HashSet::from(["foo".into(), "bar".into()])).unwrap()),
"{\"Set\":[\"bar\",\"foo\"]}",
),
];
@ -209,14 +159,17 @@ mod tests {
let serialized = serde_json::to_string(&value).unwrap();
assert_eq!(serialized, expected);
let deserialized: TypedValue = serde_json::from_str(&serialized).unwrap();
assert_eq!(value, deserialized);
let expected_deserialized: TypedValue = serde_json::from_str(&expected).unwrap();
assert_eq!(
value, deserialized,
"value {:#?} should equal deserialized {:#?}",
value, deserialized
);
let expected_deserialized: TypedValue = serde_json::from_str(expected).unwrap();
assert_eq!(value, expected_deserialized);
}
}
#[test]
fn test_signed_pod_serialization() {
fn build_signed_pod() -> Result<SignedPod> {
let mut signer = MockSigner { pk: "test".into() };
let mut builder = SignedPodBuilder::new(&Params::default());
builder.insert("name", "test");
@ -224,44 +177,36 @@ mod tests {
builder.insert("very_large_int", 1152921504606846976);
builder.insert(
"a_dict_containing_one_key",
TypedValue::Dictionary(
Dictionary::new(HashMap::from([
("foo".to_string(), TypedValue::Int(123)),
(
"an_array_containing_three_ints".to_string(),
TypedValue::Array(
Array::new(vec![
TypedValue::Int(1),
TypedValue::Int(2),
TypedValue::Int(3),
])
.unwrap(),
),
),
(
"a_set_containing_two_strings".to_string(),
TypedValue::Set(
Set::new(vec![
TypedValue::Array(
Array::new(vec![
TypedValue::String("foo".to_string()),
TypedValue::String("bar".to_string()),
])
.unwrap(),
),
TypedValue::String("baz".to_string()),
])
.unwrap(),
),
),
]))
.unwrap(),
),
Dictionary::new(HashMap::from([
("foo".into(), 123.into()),
(
"an_array_containing_three_ints".into(),
Array::new(vec![1.into(), 2.into(), 3.into()])
.unwrap()
.into(),
),
(
"a_set_containing_two_strings".into(),
Set::new(HashSet::from([
Array::new(vec!["foo".into(), "bar".into()]).unwrap().into(),
"baz".into(),
]))
.unwrap()
.into(),
),
]))
.unwrap(),
);
let pod = builder.sign(&mut signer).unwrap();
Ok(pod)
}
let serialized = serde_json::to_string(&pod).unwrap();
#[test]
fn test_signed_pod_serialization() {
let pod = build_signed_pod().unwrap();
let serialized = serde_json::to_string_pretty(&pod).unwrap();
println!("serialized: {}", serialized);
let deserialized: SignedPod = serde_json::from_str(&serialized).unwrap();
@ -270,14 +215,11 @@ mod tests {
assert_eq!(pod.id(), deserialized.id())
}
#[test]
fn test_main_pod_serialization() -> Result<()> {
fn build_zukyc_pod() -> Result<MainPod> {
let params = middleware::Params::default();
let sanctions_values = vec!["A343434340".into()];
let sanction_set = TypedValue::Set(Set::new(sanctions_values)?);
let (gov_id_builder, pay_stub_builder, sanction_list_builder) =
zu_kyc_sign_pod_builders(&params, &sanction_set);
zu_kyc_sign_pod_builders(&params);
let mut signer = MockSigner {
pk: "ZooGov".into(),
};
@ -295,8 +237,13 @@ mod tests {
let mut prover = MockProver {};
let kyc_pod = kyc_builder.prove(&mut prover, &params).unwrap();
Ok(kyc_pod)
}
let serialized = serde_json::to_string(&kyc_pod).unwrap();
#[test]
fn test_main_pod_serialization() -> Result<()> {
let kyc_pod = build_zukyc_pod()?;
let serialized = serde_json::to_string_pretty(&kyc_pod).unwrap();
println!("serialized: {}", serialized);
let deserialized: MainPod = serde_json::from_str(&serialized).unwrap();
@ -307,17 +254,70 @@ mod tests {
Ok(())
}
#[test]
fn test_schema() {
let generator = SchemaSettings::draft07().into_generator();
let mainpod_schema = generator.clone().into_root_schema_for::<MainPodHelper>();
let signedpod_schema = generator.into_root_schema_for::<SignedPodHelper>();
fn build_ethdos_pod() -> Result<MainPod> {
let params = Params {
max_input_signed_pods: 3,
max_input_main_pods: 3,
max_statements: 31,
max_signed_pod_values: 8,
max_public_statements: 10,
max_statement_args: 6,
max_operation_args: 5,
max_custom_predicate_arity: 5,
max_custom_batch_size: 5,
max_custom_predicate_wildcards: 12,
..Default::default()
};
println!("{}", serde_json::to_string_pretty(&mainpod_schema).unwrap());
println!(
"{}",
serde_json::to_string_pretty(&signedpod_schema).unwrap()
);
let mut alice = MockSigner { pk: "Alice".into() };
let bob = MockSigner { pk: "Bob".into() };
let mut charlie = MockSigner {
pk: "Charlie".into(),
};
// Alice attests that she is ETH friends with Charlie and Charlie
// attests that he is ETH friends with Bob.
let alice_attestation =
eth_friend_signed_pod_builder(&params, charlie.pubkey().into()).sign(&mut alice)?;
let charlie_attestation =
eth_friend_signed_pod_builder(&params, bob.pubkey().into()).sign(&mut charlie)?;
let mut prover = MockProver {};
let alice_bob_ethdos = eth_dos_pod_builder(
&params,
&alice_attestation,
&charlie_attestation,
&bob.pubkey().into(),
)?
.prove(&mut prover, &params)?;
Ok(alice_bob_ethdos)
}
#[test]
// This tests that we can generate JSON Schemas for the MainPod and
// SignedPod types, and that we can validate real Signed and Main Pods
// against the schemas.
fn test_schema() {
let mainpod_schema = schema_for!(MainPodHelper);
let signedpod_schema = schema_for!(SignedPodHelper);
let kyc_pod = build_zukyc_pod().unwrap();
let signed_pod = build_signed_pod().unwrap();
let ethdos_pod = build_ethdos_pod().unwrap();
let mainpod_schema_value = serde_json::to_value(&mainpod_schema).unwrap();
let signedpod_schema_value = serde_json::to_value(&signedpod_schema).unwrap();
let kyc_pod_value = serde_json::to_value(&kyc_pod).unwrap();
let mainpod_valid = jsonschema::validate(&mainpod_schema_value, &kyc_pod_value);
assert!(mainpod_valid.is_ok(), "{:#?}", mainpod_valid);
let signed_pod_value = serde_json::to_value(&signed_pod).unwrap();
let signedpod_valid = jsonschema::validate(&signedpod_schema_value, &signed_pod_value);
assert!(signedpod_valid.is_ok(), "{:#?}", signedpod_valid);
let ethdos_pod_value = serde_json::to_value(&ethdos_pod).unwrap();
let ethdos_pod_valid = jsonschema::validate(&mainpod_schema_value, &ethdos_pod_value);
assert!(ethdos_pod_valid.is_ok(), "{:#?}", ethdos_pod_valid);
}
}
*/