# Generalized Predicate Proofs for Verifiable Credentials This design enables a credential holder to prove complex logical statements over multiple attributes without revealing the underlying values. The verifier validates the proof and obtains the boolean output of the logical expression, which indicates whether the policy is satisfied. This is particularly useful for privacy preserving identity systems where users must prove eligibility conditions such as: - `age >= 18` - `employment_years >= 2 AND (50000 <= annual_income_eur <= 100000 OR account_balance_eur >= 10000)` - `age >= 18 AND residency_country IN {"Netherlands","Belgium","Germany"}` The construction supports arbitrary logical composition while keeping all claims private. # Inputs The proving system receives three inputs: 1. List of claim values (private) 2. List of predicates 3. Logical expression combining predicate results # 1. Claim Values Claims are the private attributes stored inside the credential. These values are never revealed to the verifier. Example: ``` claims = { date_of_birth: "1990-03-20", country: "Netherlands", annual_income_eur: 52000 } ``` So the claim values are `("1990-03-20", "Netherlands", 52000)`. # 2. Predicates Predicates are boolean conditions evaluated over claim values. Each predicate evaluates to either `true` or `false`. Examples: - `"1990-03-20" <= "2008-04-04"` - `annual_income_eur >= 50000` - `country == "Netherlands"` Each predicate can be represented as: ``` predicate = (claim_index, operator, constant) ``` - `claim_index`: index to the claim value - `operator`: comparison operator - `constant`: comparison value Supported predicate operators: - `<=` - `>=` - `==` From a theoretical perspective, `<` and `=` are sufficient to construct all comparison operators. In this design, we include `<=` and `>=` for simplicity and optimization (fewer constraints). Examples of rewrites: - `<` can be rewritten as `<= (value - 1)` - `>` can be rewritten as `>= (value + 1)` - `!=` can be expressed as `NOT (==)` using logical composition Restricting the operator set simplifies circuit construction and reduces constraint count. Each predicate operates on exactly one claim value and compares it against a constant. Relations involving multiple claims can be expressed through logical composition. ## Example Predicate List Credential reference: ``` claims = { date_of_birth: "2003-06-15", country: "Germany", annual_income_eur: 52000 } ``` Predicate list: ``` P0: date_of_birth <= "2008-04-04" P1: annual_income_eur >= 50000 P2: country == "Netherlands" ``` Checking against the credential reference: ``` P0: "2003-06-15" <= "2008-04-04" -> true P1: 52000 >= 50000 -> true P2: "Germany" == "Netherlands" -> false ``` After evaluation: ``` P0 → true P1 → true P2 → false ``` # 3. Logical Expression The logical expression combines predicate results using boolean operators. Supported logical operators: - AND - OR - NOT From a theoretical perspective, `AND` and `NOT` are functionally complete, so all logical operators can be expressed using only those two. In this design, `OR` is included explicitly for simplicity and optimization (fewer constraints). # Why Postfix Notation is Used Although users naturally write expressions in infix notation, postfix notation is used inside the circuit for several reasons: 1. No parentheses required 2. No operator precedence handling inside circuit 3. Deterministic evaluation order 4. Stack based evaluation 5. No branching logic 6. Reduced circuit complexity Infix notation requires parsing rules such as: - precedence handling - parentheses matching - recursive evaluation These operations are inefficient in zero knowledge circuits because they require conditional branching and dynamic control flow. Postfix notation removes all ambiguity and allows simple linear evaluation. # Infix and Postfix Examples Infix: ``` A AND B ``` Postfix: ``` A B AND ``` Complex example: Infix: ``` (A AND B) OR (C AND D) ``` Postfix: ``` A B AND C D AND OR ``` # Conversion from Infix to Postfix The conversion from infix to postfix is performed outside the circuit using the Shunting Yard algorithm, adapted to handle logical expressions and operators. The circuit therefore evaluates only the postfix logical expression over predicate outputs, avoiding parsing and operator precedence handling inside the circuit. The system assumes the provided postfix expression correctly represents the intended policy. Since the postfix expression is a public input, the verifier can independently validate that it is well formed and corresponds to the expected policy before verifying the proof. Example conversion: Infix: `(date_of_birth <= "2008-04-04" AND annual_income_eur >= 50000) OR country == "Netherlands"` Postfix: ``` P0 P1 AND P2 OR ``` # Evaluation Flow The proving process consists of two conceptual steps. ## Step 1: Predicate Evaluation Claims: ``` date_of_birth = "2003-06-15" annual_income_eur = 52000 country = "Germany" ``` Predicates: ``` P0: date_of_birth <= "2008-04-04" → true P1: annual_income_eur >= 50000 → true P2: country == "Netherlands" → false ``` Predicate results: ``` [ true, true, false ] ``` ## Step 2: Logical Expression Evaluation Policy: `(date_of_birth <= "2008-04-04" AND annual_income_eur >= 50000) OR country == "Netherlands"` Postfix: ``` P0 P1 AND P2 OR ``` Evaluation steps (processing the postfix expression from left to right, in a stack based way): ``` 1. Read P0 -> push true 2. Read P1 -> push true 3. Read AND -> pop true, true; push true 4. Read P2 -> push false 5. Read OR -> pop false, true; push true ``` Final result: ``` true ``` Only the final boolean value is revealed. # Formal Model Claims: $C = (c_0, c_1, \ldots, c_n)$ Predicates: $P_i = (j_i, \mathsf{op}_i, k_i), \quad i \in \{0, \ldots, m\}, \quad j_i \in \{0, \ldots, n\}, \quad \mathsf{op}_i \in \{\le, \ge, =\}$ where $j_i$ is the claim index, and $k_i$ is the predicate constant. Predicate evaluation: $r_i = \mathsf{op}_i(c_{j_i}, k_i) \in \{0,1\}, \quad i \in \{0, \ldots, m\}$ Predicate results: $R = (r_0, r_1, \ldots, r_m)$ Logical expression (postfix notation): $L = (\ell_0, \ell_1, \ldots, \ell_t)$ $\ell_q \in \{r \mid r \in R\} \cup \{\mathrm{AND}, \mathrm{OR}, \mathrm{NOT}\}, \quad q \in \{0, \ldots, t\}$ The prover generates a zero knowledge proof that: $b = E(L), \quad b \in \{0,1\}$ without revealing $C$. # Privacy Guarantees The verifier learns: - Logical expression (policy) - Predicate constants - Final boolean result The verifier does not learn: - Claim values - Individual predicate results - Intermediate evaluation steps # Derived Predicate Patterns: Range, Membership, and Non Membership Common predicate patterns such as range checks, membership, and non membership can be expressed through logical composition of basic predicates. ## Range Proofs Range proofs allow proving that a value lies within an interval. Example: `30000 <= annual_income_eur <= 60000` Predicates: `P0: annual_income_eur >= 30000` `P1: annual_income_eur <= 60000` Logical expression: ``` P0 P1 AND ``` This proves the value lies in the range. ## Membership Proofs Membership in a set: $country \in {X, Y, Z}$ Predicates: `P0: country == X` `P1: country == Y` `P2: country == Z` Logical expression: ``` P0 P1 OR P2 OR ``` This proves the value belongs to the set without revealing which element. ## Non Membership Proofs Non membership: $country \notin {X, Y}$ Predicates: `P0: country == X` `P1: country == Y` Logical expression: ``` P0 NOT P1 NOT AND ``` This proves the value is not in the forbidden set. ## Combining Range and Membership Example policy: "User is millennial and located in one of these EU countries: NL, BE, or DE" Using ISO 8601 date-of-birth notation (`YYYY-MM-DD`): Predicates: `P0: date_of_birth >= "1981-01-01"` `P1: date_of_birth <= "1996-12-31"` `P2: country == NL` `P3: country == BE` `P4: country == DE` Logical expression: ``` P0 P1 AND P2 P3 OR P4 OR AND ``` This proves: - user is millennial - user country is one of these EU countries: NL, BE, or DE without revealing either value. # Example: Age Verification This example shows the complete flow for proving adulthood (>= 18) using a date of birth claim. Note: Today's date for this example is `2026-04-04`. The cutoff date is `2008-04-04`, calculated as today's date minus 18 years. Given credential: ``` claims = { date_of_birth: "1990-03-20" // ... other credential claims } ``` Predicate definition: ``` P0: date_of_birth <= cutoff_date ``` Single-predicate expression (no logical operator needed): ``` P0 ``` Evaluation: ``` "1990-03-20" <= "2008-04-04" -> true ``` Result: ``` true ``` This verifies the holder is `>= 18` on `2026-04-04`, without revealing the exact age. When only one predicate is used, generalized predicates still apply: the expression can be just that predicate token, with no `AND`, `OR`, or `NOT` required.