Allow literals in statement templates (#287)

This PR is a continuation of the work done in #276 
- Fix PodType in MainPod (we were using `MockMain` instead of `Main`)
- Update anchored keys in statement template arguments to only support wildcards in the origin and literal keys as the key.
  - Update the pest grammar accordingly
  - Update the parser accordingly
- Rewrite the eth_dos example in a recursive manner so that we use one recursive pod for every distance increment of 1.
  - I've also used the podlang to define the eth_dos custom predicates.  Currently all predicates are in a single batch (previously `eth_friend` was in a different batch).  With #286 we could define `eth_friend` in a different batch again.
    - I was feeling a bit creative and used a format macro to pass `Value`s from rust to the podlang code.
  - The eth_dos is now written using literals.  This resolves https://github.com/0xPARC/pod2/issues/255
- Remove `StatementArg::WildcardValue` in favor of `StatementArg::Literal`.  The `WildcardValue` was just a way to have some kind of typing for values that would be used as arguments in custom predicates.  Now that we can have literals in any statement this value can be anything, so I just removed the `WildcardValue` and use `Literal` instead.  On the backend it was already the case that both cases were treated the same way (after all, `WildcardValue` and `Literal` were 4 fields in the backend).
  - Added a new type for Value: `PodId` so that we can use it for custom predicates that take a pod id to be used in a wildcard
- Add a mock vd_set that is empty for tests that don't use plonky2; this allows running those tests individually without paying for the expensive work of calculating the vd for various circuits.
- rename StatementTmplArg::WildcardValue to StatementTmplArg::Wildcard
This commit is contained in:
Eduard S. 2025-06-16 16:38:38 +02:00 committed by GitHub
parent 7d0d3ad769
commit 3c6930dfe6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 659 additions and 1111 deletions

View file

@ -6,67 +6,31 @@ use schemars::JsonSchema;
use crate::{
frontend::{AnchoredKey, Error, Result, Statement, StatementArg},
middleware::{
self, hash_str, CustomPredicate, CustomPredicateBatch, Key, KeyOrWildcard, NativePredicate,
Params, PodId, Predicate, SelfOrWildcard, StatementTmpl, StatementTmplArg, ToFields, Value,
Wildcard,
self, hash_str, CustomPredicate, CustomPredicateBatch, Key, NativePredicate, Params, PodId,
Predicate, StatementTmpl, StatementTmplArg, ToFields, Value, Wildcard,
},
};
#[derive(Clone, Debug, PartialEq, Eq)]
/// Argument to a statement template
pub enum KeyOrWildcardStr {
Key(String), // represents a literal key
Wildcard(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SelfOrWildcardStr {
SELF,
Wildcard(String),
}
/// helper to build a literal KeyOrWildcardStr::Key from the given str
pub fn key(s: &str) -> KeyOrWildcardStr {
KeyOrWildcardStr::Key(s.to_string())
}
/// Builder Argument for the StatementTmplBuilder
#[derive(Clone, Debug)]
pub enum BuilderArg {
Literal(Value),
/// Key: (origin, key), where origin is SELF or Wildcard and key is Key or Wildcard
Key(SelfOrWildcardStr, KeyOrWildcardStr),
/// Key: (origin, key), where origin is Wildcard and key is Key
Key(String, String),
WildcardLiteral(String),
}
impl From<&str> for SelfOrWildcardStr {
fn from(origin: &str) -> Self {
if origin == "SELF" {
SelfOrWildcardStr::SELF
} else {
SelfOrWildcardStr::Wildcard(origin.into())
}
}
}
/// When defining a `BuilderArg`, it can be done from 3 different inputs:
/// i. (&str, literal): this is to set a POD and a field, ie. (POD, literal("field"))
/// ii. (&str, &str): this is to define a origin-key wildcard pair, ie. (src_origin, src_dest)
/// iii. &str: this is to define a WildcardValue wildcard, ie. "src_or"
/// i. (&str, &str): this is to define a origin-key pair, ie. ?attestation_pod["attestation"])
/// ii. &str: this is to define a Value wildcard, ie. ?distance
///
/// case i.
impl From<(&str, KeyOrWildcardStr)> for BuilderArg {
fn from((origin, lit): (&str, KeyOrWildcardStr)) -> Self {
Self::Key(origin.into(), lit)
impl From<(&str, &str)> for BuilderArg {
fn from((origin, field): (&str, &str)) -> Self {
Self::Key(origin.to_string(), field.to_string())
}
}
/// case ii.
impl From<(&str, &str)> for BuilderArg {
fn from((origin, field): (&str, &str)) -> Self {
Self::Key(origin.into(), KeyOrWildcardStr::Wildcard(field.to_string()))
}
}
/// case iii.
impl From<&str> for BuilderArg {
fn from(wc: &str) -> Self {
Self::WildcardLiteral(wc.to_string())
@ -216,12 +180,12 @@ impl CustomPredicateBatchBuilder {
.iter()
.map(|a| match a {
BuilderArg::Literal(v) => StatementTmplArg::Literal(v.clone()),
BuilderArg::Key(pod_id, key) => StatementTmplArg::AnchoredKey(
resolve_self_or_wildcard(args, priv_args, pod_id),
resolve_key_or_wildcard(args, priv_args, key),
BuilderArg::Key(pod_id_wc, key_str) => StatementTmplArg::AnchoredKey(
resolve_wildcard(args, priv_args, pod_id_wc),
Key::from(key_str),
),
BuilderArg::WildcardLiteral(v) => {
StatementTmplArg::WildcardLiteral(resolve_wildcard(args, priv_args, v))
StatementTmplArg::Wildcard(resolve_wildcard(args, priv_args, v))
}
})
.collect();
@ -251,32 +215,6 @@ impl CustomPredicateBatchBuilder {
}
}
fn resolve_self_or_wildcard(
args: &[&str],
priv_args: &[&str],
v: &SelfOrWildcardStr,
) -> SelfOrWildcard {
match v {
SelfOrWildcardStr::SELF => SelfOrWildcard::SELF,
SelfOrWildcardStr::Wildcard(s) => {
SelfOrWildcard::Wildcard(resolve_wildcard(args, priv_args, s))
}
}
}
fn resolve_key_or_wildcard(
args: &[&str],
priv_args: &[&str],
v: &KeyOrWildcardStr,
) -> KeyOrWildcard {
match v {
KeyOrWildcardStr::Key(k) => KeyOrWildcard::Key(Key::from(k)),
KeyOrWildcardStr::Wildcard(s) => {
KeyOrWildcard::Wildcard(resolve_wildcard(args, priv_args, s))
}
}
}
fn resolve_wildcard(args: &[&str], priv_args: &[&str], s: &str) -> Wildcard {
args.iter()
.chain(priv_args.iter())
@ -292,7 +230,7 @@ mod tests {
use super::*;
use crate::{
backends::plonky2::mock::mainpod::MockProver,
examples::custom::{eth_dos_batch, eth_friend_batch},
examples::{custom::eth_dos_batch, MOCK_VD_SET},
frontend::MainPodBuilder,
middleware::{self, containers::Set, CustomPredicateRef, Params, PodType, DEFAULT_VD_SET},
op,
@ -311,12 +249,11 @@ mod tests {
params.print_serialized_sizes();
// ETH friend custom predicate batch
let eth_friend = eth_friend_batch(&params, true)?;
let eth_dos_batch = eth_dos_batch(&params, true)?;
// This batch only has 1 predicate, so we pick it already for convenience
let eth_friend = Predicate::Custom(CustomPredicateRef::new(eth_friend, 0));
let eth_friend = eth_dos_batch.predicate_ref_by_name("eth_friend").unwrap();
let eth_dos_batch = eth_dos_batch(&params, true)?;
let eth_dos_batch_mw: middleware::CustomPredicateBatch =
Arc::unwrap_or_clone(eth_dos_batch);
let fields = eth_dos_batch_mw.to_fields(&params);
@ -328,7 +265,7 @@ mod tests {
#[test]
fn test_desugared_gt_custom_pred() -> Result<()> {
let params = Params::default();
let vd_set = &*DEFAULT_VD_SET;
let vd_set = &*MOCK_VD_SET;
let mut builder = CustomPredicateBatchBuilder::new(params.clone(), "gt_custom_pred".into());
let gt_stb = StatementTmplBuilder::new(NativePredicate::Gt)
@ -337,7 +274,7 @@ mod tests {
builder.predicate_and(
"gt_custom_pred",
&["s1_origin", "s1_key", "s2_origin", "s2_key"],
&["s1_origin", "s2_origin"],
&[],
&[gt_stb],
)?;
@ -348,8 +285,8 @@ mod tests {
let mut mp_builder = MainPodBuilder::new(&params, &vd_set);
// 2 > 1
let s1 = mp_builder.literal(true, Value::from(2))?;
let s2 = mp_builder.literal(true, Value::from(1))?;
let s1 = mp_builder.priv_op(op!(new_entry, "s1_key", Value::from(2)))?;
let s2 = mp_builder.priv_op(op!(new_entry, "s2_key", Value::from(1)))?;
// Adding a gt operation will produce a desugared lt operation
let desugared_gt = mp_builder.pub_op(op!(gt, s1, s2))?;
@ -377,7 +314,7 @@ mod tests {
#[test]
fn test_desugared_set_contains_custom_pred() -> Result<()> {
let params = Params::default();
let vd_set = &*DEFAULT_VD_SET;
let vd_set = &*MOCK_VD_SET;
let mut builder =
CustomPredicateBatchBuilder::new(params.clone(), "set_contains_custom_pred".into());
@ -387,7 +324,7 @@ mod tests {
builder.predicate_and(
"set_contains_custom_pred",
&["s1_origin", "s1_key", "s2_origin", "s2_key"],
&["s1_origin", "s2_origin"],
&[],
&[set_contains_stb],
)?;
@ -397,11 +334,12 @@ mod tests {
let mut mp_builder = MainPodBuilder::new(&params, &vd_set);
let set_values: HashSet<Value> = [1, 2, 3].iter().map(|i| Value::from(*i)).collect();
let s1 = mp_builder.literal(
true,
Value::from(Set::new(params.max_depth_mt_containers, set_values)?),
)?;
let s2 = mp_builder.literal(true, Value::from(1))?;
let s1 = mp_builder.priv_op(op!(
new_entry,
"s1_key",
Value::from(Set::new(params.max_depth_mt_containers, set_values)?)
))?;
let s2 = mp_builder.priv_op(op!(new_entry, "s2_key", Value::from(1)))?;
let set_contains = mp_builder.pub_op(op!(set_contains, s1, s2))?;
assert_eq!(

View file

@ -1,15 +1,29 @@
use std::{backtrace::Backtrace, fmt::Debug};
use crate::middleware::{DynError, Statement, StatementTmpl};
use crate::middleware::{DynError, Statement, StatementTmpl, Value};
pub type Result<T, E = Error> = core::result::Result<T, E>;
fn display_wc_map(wc_map: &[Option<Value>]) -> String {
let mut out = String::new();
use std::fmt::Write;
for (i, v) in wc_map.iter().enumerate() {
write!(out, "- {}: ", i).unwrap();
if let Some(v) = v {
writeln!(out, "{}", v).unwrap();
} else {
writeln!(out, "none").unwrap();
}
}
out
}
#[derive(thiserror::Error, Debug)]
pub enum InnerError {
#[error("{0} {1} is over the limit {2}")]
MaxLength(String, usize, usize),
#[error("{0} doesn't match {1}")]
StatementsDontMatch(Statement, StatementTmpl),
#[error("{0} doesn't match {1:#}.\nWildcard map:\n{map}", map=display_wc_map(.2))]
StatementsDontMatch(Statement, StatementTmpl, Vec<Option<Value>>),
#[error("invalid arguments to {0} operation")]
OpInvalidArgs(String),
// Other
@ -54,8 +68,12 @@ impl Error {
pub(crate) fn op_invalid_args(s: String) -> Self {
new!(OpInvalidArgs(s))
}
pub(crate) fn statements_dont_match(s0: Statement, s1: StatementTmpl) -> Self {
new!(StatementsDontMatch(s0, s1))
pub(crate) fn statements_dont_match(
s0: Statement,
s1: StatementTmpl,
wc_map: Vec<Option<Value>>,
) -> Self {
new!(StatementsDontMatch(s0, s1, wc_map))
}
pub(crate) fn max_length(obj: String, found: usize, expect: usize) -> Self {
new!(MaxLength(obj, found, expect))

View file

@ -10,7 +10,7 @@ use serialization::{SerializedMainPod, SerializedSignedPod};
use crate::middleware::{
self, check_st_tmpl, hash_op, hash_str, max_op, prod_op, sum_op, AnchoredKey, Key,
MainPodInputs, NativeOperation, OperationAux, OperationType, Params, PodId, PodProver,
PodSigner, Statement, StatementArg, VDSet, Value, ValueRef, WildcardValue, KEY_TYPE, SELF,
PodSigner, Statement, StatementArg, VDSet, Value, ValueRef, KEY_TYPE, SELF,
};
mod custom;
@ -49,6 +49,8 @@ impl SignedPodBuilder {
self.kvs.insert(key.into(), value.into());
}
// TODO: Remove mut because Schnorr signature doesn't need any mutability of the signer, the
// nonces are sourced from OS randomness.
pub fn sign<S: PodSigner>(&self, signer: &mut S) -> Result<SignedPod> {
// Sign POD with committed KV store.
let pod = signer.sign(&self.params, &self.kvs)?;
@ -471,12 +473,15 @@ impl MainPodBuilder {
let st_args = st.args();
for (st_tmpl_arg, st_arg) in st_tmpl.args.iter().zip(&st_args) {
if !check_st_tmpl(st_tmpl_arg, st_arg, &mut wildcard_map) {
// TODO: Add wildcard_map in the error for better context
return Err(Error::statements_dont_match(st.clone(), st_tmpl.clone()));
return Err(Error::statements_dont_match(
st.clone(),
st_tmpl.clone(),
wildcard_map,
));
}
}
}
let v_default = WildcardValue::PodId(SELF);
let v_default = Value::from(0);
let st_args: Vec<_> = wildcard_map
.into_iter()
.take(pred.args_len)
@ -762,7 +767,7 @@ pub mod build_utils {
#[macro_export]
macro_rules! op {
(new_entry, ($key:expr, $value:expr)) => { $crate::frontend::Operation(
(new_entry, $key:expr, $value:expr) => { $crate::frontend::Operation(
$crate::middleware::OperationType::Native($crate::middleware::NativeOperation::NewEntry),
$crate::op_args!(($key, $value)), $crate::middleware::OperationAux::None) };
(copy, $($arg:expr),+) => { $crate::frontend::Operation(
@ -821,14 +826,15 @@ pub mod build_utils {
#[cfg(test)]
pub mod tests {
use super::*;
use crate::{
backends::plonky2::mock::{mainpod::MockProver, signedpod::MockSigner},
examples::{
eth_dos_pod_builder, eth_friend_signed_pod_builder, great_boy_pod_full_flow,
tickets_pod_full_flow, zu_kyc_pod_builder, zu_kyc_sign_pod_builders,
attest_eth_friend, great_boy_pod_full_flow, tickets_pod_full_flow, zu_kyc_pod_builder,
zu_kyc_sign_pod_builders, EthDosHelper, MOCK_VD_SET,
},
middleware::{containers::Dictionary, Value, DEFAULT_VD_SET},
middleware::{containers::Dictionary, Value},
};
// Check that frontend public statements agree with those
@ -863,7 +869,7 @@ pub mod tests {
#[test]
fn test_front_zu_kyc() -> Result<()> {
let params = Params::default();
let vd_set = &*DEFAULT_VD_SET;
let vd_set = &*MOCK_VD_SET;
let (gov_id, pay_stub, sanction_list) = zu_kyc_sign_pod_builders(&params);
println!("{}", gov_id);
@ -903,49 +909,45 @@ pub mod tests {
}
#[test]
fn test_ethdos() -> Result<()> {
fn test_ethdos_recursive() -> Result<()> {
let params = Params {
max_input_signed_pods: 3,
max_input_recursive_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,
max_input_pods_public_statements: 8,
max_statements: 24,
max_public_statements: 8,
..Default::default()
};
let vd_set = &*DEFAULT_VD_SET;
let vd_set = &*MOCK_VD_SET;
let mut alice = MockSigner { pk: "Alice".into() };
let bob = MockSigner { pk: "Bob".into() };
let mut bob = MockSigner { pk: "Bob".into() };
let mut charlie = MockSigner {
pk: "Charlie".into(),
};
let david = MockSigner { pk: "David".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.public_key().into()).sign(&mut alice)?;
check_kvs(&alice_attestation)?;
let charlie_attestation =
eth_friend_signed_pod_builder(&params, bob.public_key().into()).sign(&mut charlie)?;
check_kvs(&charlie_attestation)?;
let helper = EthDosHelper::new(&params, vd_set, true, alice.public_key())?;
let mut prover = MockProver {};
let alice_bob_ethdos = eth_dos_pod_builder(
&params,
&vd_set,
true,
&alice_attestation,
&charlie_attestation,
bob.public_key().into(),
)?
.prove(&mut prover, &params)?;
check_public_statements(&alice_bob_ethdos)
let alice_attestation = attest_eth_friend(&params, &mut alice, bob.public_key());
let dist_1 = helper
.dist_1(&alice_attestation)?
.prove(&mut prover, &params)?;
dist_1.pod.verify()?;
let bob_attestation = attest_eth_friend(&params, &mut bob, charlie.public_key());
let dist_2 = helper
.dist_n_plus_1(&dist_1, &bob_attestation)?
.prove(&mut prover, &params)?;
dist_2.pod.verify()?;
let charlie_attestation = attest_eth_friend(&params, &mut charlie, david.public_key());
let dist_3 = helper
.dist_n_plus_1(&dist_2, &charlie_attestation)?
.prove(&mut prover, &params)?;
dist_3.pod.verify()?;
Ok(())
}
#[test]
@ -971,7 +973,7 @@ pub mod tests {
#[should_panic]
fn test_equal() {
let params = Params::default();
let vd_set = &*DEFAULT_VD_SET;
let vd_set = &*MOCK_VD_SET;
let mut signed_builder = SignedPodBuilder::new(&params);
signed_builder.insert("a", 1);
@ -1023,7 +1025,7 @@ pub mod tests {
#[should_panic]
fn test_false_st() {
let params = Params::default();
let vd_set = &*DEFAULT_VD_SET;
let vd_set = &*MOCK_VD_SET;
let mut builder = SignedPodBuilder::new(&params);
builder.insert("num", 2);
@ -1049,7 +1051,7 @@ pub mod tests {
#[test]
fn test_dictionaries() -> Result<()> {
let params = Params::default();
let vd_set = &*DEFAULT_VD_SET;
let vd_set = &*MOCK_VD_SET;
let mut builder = SignedPodBuilder::new(&params);
let mut my_dict_kvs: HashMap<Key, Value> = HashMap::new();
@ -1070,7 +1072,7 @@ pub mod tests {
let mut builder = MainPodBuilder::new(&params, &vd_set);
builder.add_signed_pod(&pod);
let st0 = pod.get_statement("dict").unwrap();
let st1 = builder.op(true, op!(new_entry, ("key", "a"))).unwrap();
let st1 = builder.op(true, op!(new_entry, "key", "a")).unwrap();
let st2 = builder.literal(false, Value::from(1)).unwrap();
builder
@ -1103,7 +1105,7 @@ pub mod tests {
env_logger::init();
let params = Params::default();
let vd_set = &*DEFAULT_VD_SET;
let vd_set = &*MOCK_VD_SET;
let mut builder = MainPodBuilder::new(&params, &vd_set);
let st = Statement::equal(AnchoredKey::from((SELF, "a")), Value::from(3));
let op_new_entry = Operation(
@ -1127,7 +1129,7 @@ pub mod tests {
// try to insert a statement that doesn't follow from the operation
// right now the mock prover catches this when it calls compile()
let params = Params::default();
let vd_set = &*DEFAULT_VD_SET;
let vd_set = &*MOCK_VD_SET;
let mut builder = MainPodBuilder::new(&params, &vd_set);
let self_a = AnchoredKey::from((SELF, "a"));
let self_b = AnchoredKey::from((SELF, "b"));

View file

@ -105,8 +105,8 @@ mod tests {
signedpod::Signer,
},
examples::{
eth_dos_pod_builder, eth_friend_signed_pod_builder, zu_kyc_pod_builder,
zu_kyc_sign_pod_builders,
attest_eth_friend, zu_kyc_pod_builder, zu_kyc_sign_pod_builders, EthDosHelper,
MOCK_VD_SET,
},
frontend::{Result, SignedPodBuilder},
middleware::{
@ -251,7 +251,7 @@ mod tests {
fn build_mock_zukyc_pod() -> Result<MainPod> {
let params = middleware::Params::default();
let vd_set = &*DEFAULT_VD_SET;
let vd_set = &*MOCK_VD_SET;
let (gov_id_builder, pay_stub_builder, sanction_list_builder) =
zu_kyc_sign_pod_builders(&params);
@ -341,45 +341,34 @@ mod tests {
fn build_ethdos_pod() -> Result<MainPod> {
let params = Params {
max_input_signed_pods: 3,
max_input_recursive_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,
max_input_pods_public_statements: 8,
max_statements: 24,
max_public_statements: 8,
..Default::default()
};
let vd_set = &*DEFAULT_VD_SET;
let vd_set = &*MOCK_VD_SET;
let mut alice = MockSigner { pk: "Alice".into() };
let bob = MockSigner { pk: "Bob".into() };
let mut charlie = MockSigner {
let mut bob = MockSigner { pk: "Bob".into() };
let 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.public_key().into()).sign(&mut alice)?;
let charlie_attestation =
eth_friend_signed_pod_builder(&params, bob.public_key().into()).sign(&mut charlie)?;
// Alice attests that she is ETH friends with Bob and Bob
// attests that he is ETH friends with Charlie.
let alice_attestation = attest_eth_friend(&params, &mut alice, bob.public_key());
let bob_attestation = attest_eth_friend(&params, &mut bob, charlie.public_key());
let helper = EthDosHelper::new(&params, vd_set, true, alice.public_key())?;
let mut prover = MockProver {};
let alice_bob_ethdos = eth_dos_pod_builder(
&params,
&vd_set,
true,
&alice_attestation,
&charlie_attestation,
bob.public_key().into(),
)?
.prove(&mut prover, &params)?;
let dist_1 = helper
.dist_1(&alice_attestation)?
.prove(&mut prover, &params)?;
let dist_2 = helper
.dist_n_plus_1(&dist_1, &bob_attestation)?
.prove(&mut prover, &params)?;
Ok(alice_bob_ethdos)
Ok(dist_2)
}
#[test]