Extend the work of https://github.com/0xPARC/pod2/pull/487 to the Containers (Dictionary, Set, Array). The merkle tree only stores `RawValue` for both the key and the value, so it is the responsibility of the Container to store the rich value. In order to handle containers with persistent storage efficiently (which means, cloning them or updating them should not cause an O(n) data copy) I figured we need to have a database of `Value`s indexed by their raw value; as this gives us deduplication and free cloning of containers. The issue with this approach is that in the current design we have collisions between Value's of different types: https://github.com/0xPARC/pod2/issues/426 and the current API relies on the single type of values. To resolve this issue I decided to change the API, instead of assuming that a Value has a fixed type, let the value be possibly multiple compatible types and let the user of the library try casting the Value to a particular type. For this I deprecated the public access of everything related to `TypedValue` and I propose for it to be considered an implementation detail and a blackbox from the external developer point of view. The `Value` type is now used like this: - To create a new Value use `Value::from(...)` where you can pass any compatible type (the same types as before) - To access the Value in typed form you cast it like `value.as_foo()` which returns `Option<Foo>`. Previously we had a collision between `true` and `1` (and `false` and `0`). Now it doesn't matter whether a value holds a `true` or a `1`, both should be seen as the same and both return `Some` when doing `as_int` and `as_bool`. Similarly we had collisions with containers. For example `set(0, 1, 2) == array[0, 1, 2]` and `set("a", "b") = dict("a": "a", "b": "b")`. Now any container can be casted to any of `set, array, dict`. There's a caveat here: each of these types expects a particular encoding of keys, so casting to the wrong type will return errors on some operations. With this design it no longer matters what is being stored and recovered because the API requires the user to express the expected type and any type with collisions for particular values can be casted to the right type. There's only one case where it's not desirable to swap one `TypedValue` for another: the `TypedValue::Raw`. If a non-`RawValue` in the DB is replaced by the corresponding `RawValue` we erase the required information to recover the rich value. For this reason the implementations of the database treat the `RawValue` as a special case: if an value is stored in non-`RawValue`, the corresponding `RawValue` can never overwrite it. If a value is stored in `RawValue`, a matching non-`RawValue` will overwrite it (promoting it to a rich value). This way we never lose data. A consequence of this is that the serialization, `Display` and `Debug` of a container is not stable. At any point any of the entries can be swapped for a "compatible" one if they share the storage with other containers that introduce collisions. I rewrote all containers as wrapper to a generic `Container` which holds a `Map` from `Value` to `Value`. The serialization of each container now uses the single implementation of the generic `Container`.
427 lines
14 KiB
Rust
427 lines
14 KiB
Rust
pub mod custom;
|
|
|
|
use std::{collections::HashSet, sync::LazyLock};
|
|
|
|
use custom::eth_dos_batch;
|
|
use num::BigUint;
|
|
|
|
pub static MOCK_VD_SET: LazyLock<VDSet> = LazyLock::new(|| VDSet::new(&[]));
|
|
|
|
use crate::{
|
|
backends::plonky2::{primitives::ec::schnorr::SecretKey, signer::Signer},
|
|
frontend::{
|
|
MainPod, MainPodBuilder, Operation, PodRequest, Result, SignedDict, SignedDictBuilder,
|
|
},
|
|
lang::parse_request,
|
|
middleware::{
|
|
self, containers::Set, hash_values, CustomPredicateRef, Params, Predicate, PublicKey,
|
|
Signer as _, Statement, StatementArg, TypedValue, VDSet, Value,
|
|
},
|
|
};
|
|
|
|
// ZuKYC
|
|
|
|
pub fn zu_kyc_sign_dict_builders(params: &Params) -> (SignedDictBuilder, SignedDictBuilder) {
|
|
let mut gov_id = SignedDictBuilder::new(params);
|
|
gov_id.insert("idNumber", "4242424242");
|
|
gov_id.insert("dateOfBirth", 1169909384);
|
|
gov_id.insert("socialSecurityNumber", "G2121210");
|
|
|
|
let mut pay_stub = SignedDictBuilder::new(params);
|
|
pay_stub.insert("socialSecurityNumber", "G2121210");
|
|
pay_stub.insert("startDate", 1706367566);
|
|
|
|
(gov_id, pay_stub)
|
|
}
|
|
|
|
pub const ZU_KYC_NOW_MINUS_18Y: i64 = 1169909388;
|
|
pub const ZU_KYC_NOW_MINUS_1Y: i64 = 1706367566;
|
|
pub const ZU_KYC_SANCTION_LIST: &[&str] = &["A343434340"];
|
|
|
|
pub fn zu_kyc_pod_builder(
|
|
params: &Params,
|
|
vd_set: &VDSet,
|
|
gov_id: &SignedDict,
|
|
pay_stub: &SignedDict,
|
|
) -> Result<MainPodBuilder> {
|
|
let now_minus_18y = ZU_KYC_NOW_MINUS_18Y;
|
|
let now_minus_1y = ZU_KYC_NOW_MINUS_1Y;
|
|
let sanctions_values: HashSet<Value> = ZU_KYC_SANCTION_LIST
|
|
.iter()
|
|
.map(|s| Value::from(*s))
|
|
.collect();
|
|
let sanction_set = Value::from(Set::new(sanctions_values));
|
|
|
|
let mut kyc = MainPodBuilder::new(params, vd_set);
|
|
kyc.pub_op(Operation::dict_signed_by(gov_id))?;
|
|
kyc.pub_op(Operation::dict_signed_by(pay_stub))?;
|
|
|
|
kyc.pub_op(Operation::set_not_contains(
|
|
sanction_set,
|
|
(gov_id, "idNumber"),
|
|
))?;
|
|
kyc.pub_op(Operation::lt((gov_id, "dateOfBirth"), now_minus_18y))?;
|
|
kyc.pub_op(Operation::eq(
|
|
(gov_id, "socialSecurityNumber"),
|
|
(pay_stub, "socialSecurityNumber"),
|
|
))?;
|
|
kyc.pub_op(Operation::eq((pay_stub, "startDate"), now_minus_1y))?;
|
|
|
|
Ok(kyc)
|
|
}
|
|
|
|
pub fn zu_kyc_pod_request(gov_signer: &Value, pay_signer: &Value) -> Result<PodRequest> {
|
|
let sanctions_values: HashSet<Value> = ZU_KYC_SANCTION_LIST
|
|
.iter()
|
|
.map(|s| Value::from(*s))
|
|
.collect();
|
|
let sanction_set = Value::from(Set::new(sanctions_values));
|
|
let input = format!(
|
|
r#"
|
|
REQUEST(
|
|
SetNotContains({sanction_set}, gov.idNumber)
|
|
Lt(gov.dateOfBirth, {ZU_KYC_NOW_MINUS_18Y})
|
|
Equal(pay.startDate, {ZU_KYC_NOW_MINUS_1Y})
|
|
Equal(gov.socialSecurityNumber, pay.socialSecurityNumber)
|
|
SignedBy(gov, {gov_signer})
|
|
SignedBy(pay, {pay_signer})
|
|
// TODO: Ownership check and watermarking
|
|
// Depends partly on https://github.com/0xPARC/pod2/issues/351
|
|
)
|
|
"#,
|
|
);
|
|
Ok(parse_request(&input, &Params::default(), &[])?)
|
|
}
|
|
|
|
// ETHDoS
|
|
|
|
pub fn attest_eth_friend(
|
|
params: &Params,
|
|
src: &impl middleware::Signer,
|
|
dst: PublicKey,
|
|
) -> SignedDict {
|
|
let mut attestation = SignedDictBuilder::new(params);
|
|
attestation.insert("attestation", dst);
|
|
attestation.sign(src).unwrap()
|
|
}
|
|
|
|
pub struct EthDosHelper {
|
|
params: Params,
|
|
vd_set: VDSet,
|
|
eth_friend: CustomPredicateRef,
|
|
eth_dos_base: CustomPredicateRef,
|
|
eth_dos_ind: CustomPredicateRef,
|
|
eth_dos: CustomPredicateRef,
|
|
src: PublicKey,
|
|
}
|
|
|
|
impl EthDosHelper {
|
|
pub fn new(params: &Params, vd_set: &VDSet, src: PublicKey) -> Result<Self> {
|
|
let eth_dos_batch = eth_dos_batch(params)?;
|
|
let eth_friend = eth_dos_batch.predicate_ref_by_name("eth_friend").unwrap();
|
|
let eth_dos_base = eth_dos_batch.predicate_ref_by_name("eth_dos_base").unwrap();
|
|
let eth_dos_ind = eth_dos_batch.predicate_ref_by_name("eth_dos_ind").unwrap();
|
|
let eth_dos = eth_dos_batch.predicate_ref_by_name("eth_dos").unwrap();
|
|
Ok(Self {
|
|
params: params.clone(),
|
|
vd_set: vd_set.clone(),
|
|
eth_friend,
|
|
eth_dos_base,
|
|
eth_dos_ind,
|
|
eth_dos,
|
|
src,
|
|
})
|
|
}
|
|
|
|
pub fn dist_1(&self, src_attestation: &SignedDict) -> Result<MainPodBuilder> {
|
|
assert_eq!(self.src, src_attestation.public_key);
|
|
|
|
let mut pod = MainPodBuilder::new(&self.params, &self.vd_set);
|
|
pod.pub_op(Operation::dict_signed_by(src_attestation))?;
|
|
|
|
let src_eq_src = pod.priv_op(Operation::eq(self.src, self.src))?;
|
|
let distance_eq_zero = pod.priv_op(Operation::eq(0, 0))?;
|
|
let eth_dos_src_to_src_base = pod.priv_op(Operation::custom(
|
|
self.eth_dos_base.clone(),
|
|
[src_eq_src, distance_eq_zero],
|
|
))?;
|
|
let eth_dos_src_to_src = pod.priv_op(Operation::custom(
|
|
self.eth_dos.clone(),
|
|
[eth_dos_src_to_src_base, Statement::None],
|
|
))?;
|
|
|
|
// eth_dos src->dst dist=1
|
|
self.n_plus_1(&mut pod, eth_dos_src_to_src, src_attestation, 0)?;
|
|
|
|
Ok(pod)
|
|
}
|
|
|
|
pub fn dist_n_plus_1(
|
|
&self,
|
|
eth_dos_src_to_int_pod: &MainPod,
|
|
int_attestation: &SignedDict, // int signs dst
|
|
) -> Result<MainPodBuilder> {
|
|
let mut pod = MainPodBuilder::new(&self.params, &self.vd_set);
|
|
pod.add_pod(eth_dos_src_to_int_pod.clone())?;
|
|
|
|
let eth_dos_int_to_dst = eth_dos_src_to_int_pod
|
|
.pod
|
|
.pub_statements()
|
|
.into_iter()
|
|
.rev() // Find the last predicate because dist_1 has two: dist=0, dist=1
|
|
.find(|st| st.predicate() == Predicate::Custom(self.eth_dos.clone()))
|
|
.expect("eth_dos custom predicate");
|
|
let [_src, int, n] = {
|
|
let args: [_; 3] = eth_dos_int_to_dst.args().try_into().expect("Vec::len=3");
|
|
args.map(|arg| match arg {
|
|
StatementArg::Literal(v) => v,
|
|
_ => panic!("expected StatementArg::Literal"),
|
|
})
|
|
};
|
|
assert_eq!(int, Value::from(int_attestation.public_key));
|
|
|
|
let n_i64 = n.as_int().unwrap();
|
|
|
|
// eth_dos src->dst dist=n+1
|
|
self.n_plus_1(&mut pod, eth_dos_int_to_dst, int_attestation, n_i64)?;
|
|
|
|
Ok(pod)
|
|
}
|
|
|
|
fn n_plus_1(
|
|
&self,
|
|
pod: &mut MainPodBuilder,
|
|
eth_dos_int_to_dst: Statement,
|
|
int_attestation: &SignedDict,
|
|
n: i64,
|
|
) -> Result<()> {
|
|
// eth_friend statement
|
|
let attestation_signed_by_int = pod.priv_op(Operation::dict_signed_by(int_attestation))?;
|
|
let int_attests_to_dst = int_attestation.get_statement("attestation").unwrap();
|
|
let ethfriends_int_dst = pod.priv_op(Operation::custom(
|
|
self.eth_friend.clone(),
|
|
[attestation_signed_by_int, int_attests_to_dst],
|
|
))?;
|
|
|
|
// distance = n + 1
|
|
let ethdos_sum = pod.priv_op(Operation::sum_of(n + 1, n, 1))?;
|
|
let eth_dos_src_to_dst_ind = pod.priv_op(Operation::custom(
|
|
self.eth_dos_ind.clone(),
|
|
[eth_dos_int_to_dst, ethdos_sum, ethfriends_int_dst],
|
|
))?;
|
|
let _eth_dos_src_dst = pod.pub_op(Operation::custom(
|
|
self.eth_dos.clone(),
|
|
[Statement::None, eth_dos_src_to_dst_ind],
|
|
))?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// GreatBoy
|
|
|
|
pub fn good_boy_sign_pod_builder(params: &Params, user: &PublicKey, age: i64) -> SignedDictBuilder {
|
|
let mut good_boy = SignedDictBuilder::new(params);
|
|
good_boy.insert("user", *user);
|
|
good_boy.insert("age", age);
|
|
|
|
good_boy
|
|
}
|
|
|
|
pub fn friend_sign_pod_builder(params: &Params, friend: &PublicKey) -> SignedDictBuilder {
|
|
let mut friend_pod = SignedDictBuilder::new(params);
|
|
friend_pod.insert("friend", *friend);
|
|
|
|
friend_pod
|
|
}
|
|
|
|
pub fn great_boy_pod_builder(
|
|
params: &Params,
|
|
vd_set: &VDSet,
|
|
good_boy_signed_dicts: [&SignedDict; 4],
|
|
friend_signed_dicts: [&SignedDict; 2],
|
|
good_boy_issuers: &Value,
|
|
receiver: &PublicKey,
|
|
) -> Result<MainPodBuilder> {
|
|
// Attestment chain (issuer -> good boy -> great boy):
|
|
// issuer 0 -> good_boy_pods[0] => good boy 0
|
|
// issuer 1 -> good_boy_pods[1] => good boy 0
|
|
// issuer 2 -> good_boy_pods[2] => good boy 1
|
|
// issuer 3 -> good_boy_pods[3] => good boy 1
|
|
// good boy 0 -> friend_pods[0] => receiver
|
|
// good boy 1 -> friend_pods[1] => receiver
|
|
|
|
let mut great_boy = MainPodBuilder::new(params, vd_set);
|
|
for good_boy_signed_dict in good_boy_signed_dicts {
|
|
great_boy.priv_op(Operation::dict_signed_by(good_boy_signed_dict))?;
|
|
}
|
|
for friend_signed_dict in friend_signed_dicts {
|
|
great_boy.priv_op(Operation::dict_signed_by(friend_signed_dict))?;
|
|
}
|
|
|
|
for good_boy_idx in 0..2 {
|
|
for issuer_idx in 0..2 {
|
|
// Each good boy POD comes from a valid issuer
|
|
great_boy.priv_op(Operation::set_contains(
|
|
good_boy_issuers,
|
|
good_boy_signed_dicts[good_boy_idx * 2 + issuer_idx].public_key,
|
|
))?;
|
|
// Each good boy has 2 good boy pods
|
|
great_boy.priv_op(Operation::eq(
|
|
(good_boy_signed_dicts[good_boy_idx * 2 + issuer_idx], "user"),
|
|
friend_signed_dicts[good_boy_idx].public_key,
|
|
))?;
|
|
}
|
|
// The good boy PODs from each good boy have different issuers
|
|
great_boy.pub_op(Operation::ne(
|
|
good_boy_signed_dicts[good_boy_idx * 2].public_key,
|
|
good_boy_signed_dicts[good_boy_idx * 2 + 1].public_key,
|
|
))?;
|
|
// Each good boy is receivers' friend
|
|
great_boy.pub_op(Operation::eq(
|
|
(friend_signed_dicts[good_boy_idx], "friend"),
|
|
*receiver,
|
|
))?;
|
|
}
|
|
// The two good boys are different
|
|
great_boy.pub_op(Operation::ne(
|
|
friend_signed_dicts[0].public_key,
|
|
friend_signed_dicts[1].public_key,
|
|
))?;
|
|
|
|
Ok(great_boy)
|
|
}
|
|
|
|
pub fn great_boy_pod_full_flow() -> Result<MainPodBuilder> {
|
|
let params = Params {
|
|
max_signed_by: 6,
|
|
max_input_pods: 0,
|
|
max_statements: 100,
|
|
..Default::default()
|
|
};
|
|
let vd_set = &*MOCK_VD_SET;
|
|
|
|
let giggles_signer = Signer(SecretKey(1u32.into()));
|
|
let macrosoft_signer = Signer(SecretKey(2u32.into()));
|
|
let faebook_signer = Signer(SecretKey(3u32.into()));
|
|
let good_boy_issuers =
|
|
[&giggles_signer, ¯osoft_signer, &faebook_signer].map(|s| s.0.public_key());
|
|
let bob_signer = Signer(SecretKey(11u32.into()));
|
|
let charlie_signer = Signer(SecretKey(12u32.into()));
|
|
let alice_signer = Signer(SecretKey(13u32.into()));
|
|
let bob = bob_signer.public_key();
|
|
let charlie = charlie_signer.public_key();
|
|
let alice = alice_signer.public_key();
|
|
|
|
// Bob receives two good_boy pods from Giggles and Macrosoft.
|
|
|
|
let mut bob_good_boys = Vec::new();
|
|
|
|
let good_boy = good_boy_sign_pod_builder(¶ms, &bob, 36);
|
|
bob_good_boys.push(good_boy.sign(&giggles_signer).unwrap());
|
|
bob_good_boys.push(good_boy.sign(¯osoft_signer).unwrap());
|
|
|
|
// Charlie receives two good_boy pods from Macrosoft and Faebook
|
|
|
|
let mut charlie_good_boys = Vec::new();
|
|
|
|
let good_boy = good_boy_sign_pod_builder(¶ms, &charlie, 27);
|
|
charlie_good_boys.push(good_boy.sign(¯osoft_signer).unwrap());
|
|
charlie_good_boys.push(good_boy.sign(&faebook_signer).unwrap());
|
|
|
|
// Bob and Charlie send Alice a Friend POD
|
|
|
|
let mut alice_friend_pods = Vec::new();
|
|
let friend = friend_sign_pod_builder(¶ms, &alice);
|
|
alice_friend_pods.push(friend.sign(&bob_signer).unwrap());
|
|
alice_friend_pods.push(friend.sign(&charlie_signer).unwrap());
|
|
|
|
let good_boy_issuers = Value::from(Set::new(
|
|
good_boy_issuers.into_iter().map(Value::from).collect(),
|
|
));
|
|
|
|
let builder = great_boy_pod_builder(
|
|
¶ms,
|
|
vd_set,
|
|
[
|
|
&bob_good_boys[0],
|
|
&bob_good_boys[1],
|
|
&charlie_good_boys[0],
|
|
&charlie_good_boys[1],
|
|
],
|
|
[&alice_friend_pods[0], &alice_friend_pods[1]],
|
|
&good_boy_issuers,
|
|
&alice,
|
|
)?;
|
|
|
|
Ok(builder)
|
|
}
|
|
|
|
// Tickets
|
|
|
|
pub const TICKET_OWNER_SECRET_KEY: SecretKey = SecretKey(BigUint::ZERO);
|
|
|
|
pub fn tickets_sign_pod_builder(params: &Params) -> SignedDictBuilder {
|
|
// Create a signed pod with all atomic types (string, int, bool)
|
|
let mut builder = SignedDictBuilder::new(params);
|
|
builder.insert("eventId", 123);
|
|
builder.insert("productId", 456);
|
|
// Removed temporarily to make the example fit in 8 entries.
|
|
// builder.insert("attendeeName", "John Doe");
|
|
builder.insert("attendeeEmail", "john.doe@example.com");
|
|
builder.insert("attendeePublicKey", TICKET_OWNER_SECRET_KEY.public_key());
|
|
builder.insert("isConsumed", true);
|
|
builder.insert("isRevoked", false);
|
|
builder
|
|
}
|
|
|
|
pub fn tickets_pod_builder(
|
|
params: &Params,
|
|
vd_set: &VDSet,
|
|
signed_dict: &SignedDict,
|
|
expected_event_id: i64,
|
|
expect_consumed: bool,
|
|
blacklisted_emails: &Set,
|
|
) -> Result<MainPodBuilder> {
|
|
let blacklisted_email_set_value = Value::from(TypedValue::Set(blacklisted_emails.clone()));
|
|
// Create a main pod referencing this signed pod with some statements
|
|
let mut builder = MainPodBuilder::new(params, vd_set);
|
|
builder.pub_op(Operation::dict_signed_by(signed_dict))?;
|
|
builder.pub_op(Operation::eq((signed_dict, "eventId"), expected_event_id))?;
|
|
builder.pub_op(Operation::eq((signed_dict, "isConsumed"), expect_consumed))?;
|
|
builder.pub_op(Operation::eq((signed_dict, "isRevoked"), false))?;
|
|
builder.pub_op(Operation::dict_not_contains(
|
|
blacklisted_email_set_value,
|
|
(signed_dict, "attendeeEmail"),
|
|
))?;
|
|
|
|
// This isn't the most fool-proof way to prove ownership (it requires
|
|
// verifier to check pod ID on an anchored key to confirm statement wasn't
|
|
// copied), but it's the simplest.
|
|
let sk = TICKET_OWNER_SECRET_KEY;
|
|
builder.pub_op(Operation::public_key_of(
|
|
(signed_dict, "attendeePublicKey"),
|
|
sk.clone(),
|
|
))?;
|
|
|
|
// Nullifier calculation is public, but based on the private sk.
|
|
let external_nullifier = "external nullifier";
|
|
let nullifier = hash_values(&[TICKET_OWNER_SECRET_KEY.into(), external_nullifier.into()]);
|
|
builder.pub_op(Operation::hash_of(nullifier, sk, external_nullifier))?;
|
|
|
|
Ok(builder)
|
|
}
|
|
|
|
pub fn tickets_pod_full_flow(params: &Params, vd_set: &VDSet) -> Result<MainPodBuilder> {
|
|
let builder = tickets_sign_pod_builder(params);
|
|
|
|
let signed_dict = builder.sign(&Signer(SecretKey(1u32.into()))).unwrap();
|
|
tickets_pod_builder(
|
|
params,
|
|
vd_set,
|
|
&signed_dict,
|
|
123,
|
|
true,
|
|
&Set::new(HashSet::new()),
|
|
)
|
|
}
|