Check a single POD against a POD Request (#359)

This commit is contained in:
Rob Knight 2025-07-30 02:46:14 +01:00 committed by GitHub
parent c7b39f21f0
commit f10a5adb41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 419 additions and 102 deletions

View file

@ -31,6 +31,10 @@ pub enum InnerError {
),
#[error("invalid arguments to {0} operation")]
OpInvalidArgs(String),
#[error("Podlang parse error: {0}")]
PodlangParse(String),
#[error("POD Request validation error: {0}")]
PodRequestValidation(String),
// Other
#[error("{0}")]
Custom(String),
@ -55,6 +59,12 @@ impl From<std::convert::Infallible> for Error {
}
}
impl From<crate::lang::LangError> for Error {
fn from(value: crate::lang::LangError) -> Self {
Error::podlang_parse(value)
}
}
impl Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
@ -88,4 +98,10 @@ impl Error {
pub(crate) fn max_length(obj: String, found: usize, expect: usize) -> Self {
new!(MaxLength(obj, found, expect))
}
pub(crate) fn podlang_parse(e: crate::lang::LangError) -> Self {
new!(PodlangParse(e.to_string()))
}
pub(crate) fn pod_request_validation(e: String) -> Self {
new!(PodRequestValidation(e))
}
}

View file

@ -16,10 +16,12 @@ use crate::middleware::{
mod custom;
mod error;
mod operation;
mod pod_request;
mod serialization;
pub use custom::*;
pub use error::*;
pub use operation::*;
pub use pod_request::*;
#[derive(Clone, Debug)]
pub struct SignedPodBuilder {
@ -857,7 +859,8 @@ pub mod tests {
mock::mainpod::MockProver, primitives::ec::schnorr::SecretKey, signedpod::Signer,
},
examples::{
attest_eth_friend, great_boy_pod_full_flow, tickets_pod_full_flow, zu_kyc_pod_builder,
attest_eth_friend, custom::eth_dos_request, great_boy_pod_full_flow,
tickets_pod_full_flow, zu_kyc_pod_builder, zu_kyc_pod_request,
zu_kyc_sign_pod_builders, EthDosHelper, MOCK_VD_SET,
},
middleware::{containers::Dictionary, Value},
@ -896,7 +899,7 @@ pub mod tests {
fn test_front_zu_kyc() -> Result<()> {
let params = Params::default();
let vd_set = &*MOCK_VD_SET;
let (gov_id, pay_stub, sanction_list) = zu_kyc_sign_pod_builders(&params);
let (gov_id, pay_stub) = zu_kyc_sign_pod_builders(&params);
println!("{}", gov_id);
println!("{}", pay_stub);
@ -911,12 +914,7 @@ pub mod tests {
check_kvs(&pay_stub)?;
println!("{}", pay_stub);
let signer = Signer(SecretKey(3u32.into()));
let sanction_list = sanction_list.sign(&signer)?;
check_kvs(&sanction_list)?;
println!("{}", sanction_list);
let kyc_builder = zu_kyc_pod_builder(&params, vd_set, &gov_id, &pay_stub, &sanction_list)?;
let kyc_builder = zu_kyc_pod_builder(&params, vd_set, &gov_id, &pay_stub)?;
println!("{}", kyc_builder);
// prove kyc with MockProver and print it
@ -925,6 +923,15 @@ pub mod tests {
println!("{}", kyc);
let request = zu_kyc_pod_request(
gov_id.get("_signer").unwrap(),
pay_stub.get("_signer").unwrap(),
)?;
// Check the bindings of the "gov" and "pay" wildcards from the PodRequest
let bindings = request.exact_match_pod(&*kyc.pod).unwrap();
assert_eq!(*bindings.get("gov").unwrap(), gov_id.id().into());
assert_eq!(*bindings.get("pay").unwrap(), pay_stub.id().into());
check_public_statements(&kyc)
}
@ -950,18 +957,34 @@ pub mod tests {
let alice_attestation = attest_eth_friend(&params, &alice, bob.public_key());
let dist_1 = helper.dist_1(&alice_attestation)?.prove(&prover, &params)?;
dist_1.pod.verify()?;
let request = eth_dos_request()?;
assert!(request.exact_match_pod(&*dist_1.pod).is_ok());
let bindings = request.exact_match_pod(&*dist_1.pod).unwrap();
assert_eq!(*bindings.get("src").unwrap(), alice.public_key());
assert_eq!(*bindings.get("dst").unwrap(), bob.public_key());
assert_eq!(*bindings.get("distance").unwrap(), 1.into());
let bob_attestation = attest_eth_friend(&params, &bob, charlie.public_key());
let dist_2 = helper
.dist_n_plus_1(&dist_1, &bob_attestation)?
.prove(&prover, &params)?;
dist_2.pod.verify()?;
assert!(request.exact_match_pod(&*dist_2.pod).is_ok());
let bindings = request.exact_match_pod(&*dist_2.pod).unwrap();
assert_eq!(*bindings.get("src").unwrap(), alice.public_key());
assert_eq!(*bindings.get("dst").unwrap(), charlie.public_key());
assert_eq!(*bindings.get("distance").unwrap(), 2.into());
let charlie_attestation = attest_eth_friend(&params, &charlie, david.public_key());
let dist_3 = helper
.dist_n_plus_1(&dist_2, &charlie_attestation)?
.prove(&prover, &params)?;
dist_3.pod.verify()?;
assert!(request.exact_match_pod(&*dist_3.pod).is_ok());
let bindings = request.exact_match_pod(&*dist_3.pod).unwrap();
assert_eq!(*bindings.get("src").unwrap(), alice.public_key());
assert_eq!(*bindings.get("dst").unwrap(), david.public_key());
assert_eq!(*bindings.get("distance").unwrap(), 3.into());
Ok(())
}

255
src/frontend/pod_request.rs Normal file
View file

@ -0,0 +1,255 @@
use std::{collections::HashMap, fmt::Display};
use crate::{
frontend::{Error, Result},
lang::PrettyPrint,
middleware::{Pod, Statement, StatementArg, StatementTmpl, StatementTmplArg, Value},
};
/// Represents a request for a POD, in terms of a set of statement templates.
/// The response should be a POD (or PODs) containing a set of statements which
/// satisfy the templates, with consistent wildcard bindings across all templates.
#[derive(Debug, Clone, PartialEq)]
pub struct PodRequest {
pub request_templates: Vec<StatementTmpl>,
}
impl PodRequest {
pub fn new(request_templates: Vec<StatementTmpl>) -> Self {
Self { request_templates }
}
/// Checks if the request is fully satisfied by a single supplied POD.
/// This checks for exact matches to the statement templates; that is to say
/// that it performs a "syntactic" match, not a "semantic" match; no
/// processing of the semantics of the statements is performed.
pub fn exact_match_pod(&self, pod: &dyn Pod) -> Result<HashMap<String, Value>> {
let pod_statements = pod.pub_statements();
let mut bindings: HashMap<String, Value> = HashMap::new();
if self.dfs_match_all(&pod_statements, &mut bindings, 0) {
Ok(bindings)
} else {
Err(Error::pod_request_validation("No match found".to_string()))
}
}
/// Performs a depth-first search through the statement templates, trying to
/// match each template to a statement in the POD.
/// Returns true if all templates are matched, false otherwise.
/// The bindings map is used to store the bindings of the wildcards to the
/// values in the POD.
/// The template_idx is used to track the current template being matched.
fn dfs_match_all(
&self,
pod_statements: &[Statement],
bindings: &mut HashMap<String, Value>,
template_idx: usize,
) -> bool {
// Base case: all templates matched
if template_idx >= self.request_templates.len() {
return true;
}
let template = &self.request_templates[template_idx];
// Try to match this template with each statement in the POD
for stmt in pod_statements {
if let Some(new_bindings) = self.try_match_template(template, stmt, bindings) {
let original_bindings = bindings.clone();
bindings.extend(new_bindings);
if self.dfs_match_all(pod_statements, bindings, template_idx + 1) {
return true;
}
*bindings = original_bindings;
}
}
false
}
fn try_match_template(
&self,
template: &StatementTmpl,
statement: &Statement,
current_bindings: &HashMap<String, Value>,
) -> Option<HashMap<String, Value>> {
if template.pred != statement.predicate() {
return None;
}
let template_args = template.args();
let statement_args = statement.args();
if template_args.len() != statement_args.len() {
return None;
}
let mut new_bindings = HashMap::new();
for (tmpl_arg, stmt_arg) in template_args.iter().zip(statement_args.iter()) {
if !self.try_match_arg(tmpl_arg, stmt_arg, current_bindings, &mut new_bindings) {
return None;
}
}
Some(new_bindings)
}
fn try_match_arg(
&self,
template_arg: &StatementTmplArg,
statement_arg: &StatementArg,
current_bindings: &HashMap<String, Value>,
new_bindings: &mut HashMap<String, Value>,
) -> bool {
match (template_arg, statement_arg) {
// Literal must match exactly
(StatementTmplArg::Literal(tmpl_val), StatementArg::Literal(stmt_val)) => {
tmpl_val == stmt_val
}
// Wildcard can bind to any literal value
(StatementTmplArg::Wildcard(wildcard), StatementArg::Literal(stmt_val)) => self
.try_bind_wildcard(
&wildcard.name,
stmt_val.clone(),
current_bindings,
new_bindings,
),
// AnchoredKey wildcard must match statement's anchored key
(StatementTmplArg::AnchoredKey(wildcard, tmpl_key), StatementArg::Key(stmt_key)) => {
// Check if keys match
if tmpl_key != &stmt_key.key {
return false;
}
// Try to bind wildcard to the POD ID
let pod_id_value = Value::from(stmt_key.pod_id);
self.try_bind_wildcard(&wildcard.name, pod_id_value, current_bindings, new_bindings)
}
// Other combinations don't match
_ => false,
}
}
fn try_bind_wildcard(
&self,
wildcard_name: &str,
value: Value,
current_bindings: &HashMap<String, Value>,
new_bindings: &mut HashMap<String, Value>,
) -> bool {
// Check if wildcard is already bound
if let Some(existing_value) = current_bindings.get(wildcard_name) {
// Must match existing binding
return existing_value == &value;
}
// Check if we're trying to bind it in new_bindings
if let Some(existing_value) = new_bindings.get(wildcard_name) {
// Must match existing binding in this attempt
return existing_value == &value;
}
// Bind the wildcard
new_bindings.insert(wildcard_name.to_string(), value);
true
}
pub fn templates(&self) -> &[StatementTmpl] {
&self.request_templates
}
}
impl Display for PodRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.fmt_podlang(f)
}
}
#[cfg(test)]
mod tests {
use crate::{
backends::plonky2::{
mock::mainpod::MockProver, primitives::ec::schnorr::SecretKey, signedpod::Signer,
},
examples::{zu_kyc_pod_builder, zu_kyc_pod_request, zu_kyc_sign_pod_builders, MOCK_VD_SET},
frontend::{MainPodBuilder, Operation},
lang::parse,
middleware::Params,
};
#[test]
fn test_pod_request_exact_match_pod() {
let params = Params::default();
let vd_set = &*MOCK_VD_SET;
let (gov_id, pay_stub) = zu_kyc_sign_pod_builders(&params);
let gov_id = gov_id.sign(&Signer(SecretKey(1u32.into()))).unwrap();
let pay_stub = pay_stub.sign(&Signer(SecretKey(2u32.into()))).unwrap();
let builder = zu_kyc_pod_builder(&Params::default(), vd_set, &gov_id, &pay_stub).unwrap();
let prover = MockProver {};
let kyc = builder.prove(&prover, &params).unwrap();
// This request matches the POD
let request = zu_kyc_pod_request(
gov_id.get("_signer").unwrap(),
pay_stub.get("_signer").unwrap(),
)
.unwrap();
assert!(request.exact_match_pod(&*kyc.pod).is_ok());
// This request does not match the POD, because the POD does not contain a NotEqual statement.
let non_matching_request = parse(
r#"
REQUEST(
NotEqual(4, 5)
)
"#,
&params,
&[],
)
.unwrap()
.request;
assert!(non_matching_request.exact_match_pod(&*kyc.pod).is_err());
}
#[test]
fn test_ambiguous_pod() {
let params = Params::default();
let vd_set = &*MOCK_VD_SET;
let mut builder = MainPodBuilder::new(&params, vd_set);
let _sum_of_stmt_1 = builder.pub_op(Operation::sum_of(11, 1, 10));
let _sum_of_stmt_2 = builder.pub_op(Operation::sum_of(10, 9, 1));
let _eq_stmt = builder.pub_op(Operation::eq(10, 10));
let prover = MockProver {};
let pod = builder.prove(&prover, &params).unwrap();
println!("{pod}");
let request = parse(
r#"
REQUEST(
SumOf(?a, ?b, ?c)
Equal(?a, 10)
)
"#,
&params,
&[],
)
.unwrap();
let bindings = request.request.exact_match_pod(&*pod.pod).unwrap();
assert_eq!(*bindings.get("a").unwrap(), 10.into());
assert_eq!(*bindings.get("b").unwrap(), 9.into());
assert_eq!(*bindings.get("c").unwrap(), 1.into());
}
}

View file

@ -267,22 +267,12 @@ mod tests {
let params = middleware::Params::default();
let vd_set = &*MOCK_VD_SET;
let (gov_id_builder, pay_stub_builder, sanction_list_builder) =
zu_kyc_sign_pod_builders(&params);
let (gov_id_builder, pay_stub_builder) = zu_kyc_sign_pod_builders(&params);
let signer = Signer(SecretKey(1u32.into()));
let gov_id_pod = gov_id_builder.sign(&signer).unwrap();
let signer = Signer(SecretKey(2u32.into()));
let pay_stub_pod = pay_stub_builder.sign(&signer).unwrap();
let signer = Signer(SecretKey(3u32.into()));
let sanction_list_pod = sanction_list_builder.sign(&signer).unwrap();
let kyc_builder = zu_kyc_pod_builder(
&params,
vd_set,
&gov_id_pod,
&pay_stub_pod,
&sanction_list_pod,
)
.unwrap();
let kyc_builder = zu_kyc_pod_builder(&params, vd_set, &gov_id_pod, &pay_stub_pod).unwrap();
let prover = MockProver {};
let kyc_pod = kyc_builder.prove(&prover, &params).unwrap();
@ -300,21 +290,12 @@ mod tests {
vds.push(rec_main_pod_circuit_data(&params).1.verifier_only.clone());
let vd_set = VDSet::new(params.max_depth_mt_vds, &vds).unwrap();
let (gov_id_builder, pay_stub_builder, sanction_list_builder) =
zu_kyc_sign_pod_builders(&params);
let (gov_id_builder, pay_stub_builder) = zu_kyc_sign_pod_builders(&params);
let signer = Signer(SecretKey(1u32.into()));
let gov_id_pod = gov_id_builder.sign(&signer)?;
let signer = Signer(SecretKey(2u32.into()));
let pay_stub_pod = pay_stub_builder.sign(&signer)?;
let signer = Signer(SecretKey(3u32.into()));
let sanction_list_pod = sanction_list_builder.sign(&signer)?;
let kyc_builder = zu_kyc_pod_builder(
&params,
&vd_set,
&gov_id_pod,
&pay_stub_pod,
&sanction_list_pod,
)?;
let kyc_builder = zu_kyc_pod_builder(&params, &vd_set, &gov_id_pod, &pay_stub_pod)?;
let prover = Prover {};
let kyc_pod = kyc_builder.prove(&prover, &params)?;