Skip to content

Commit

Permalink
chore(Improve zkProgram structure): add TSDocs
Browse files Browse the repository at this point in the history
  • Loading branch information
teddyjfpender committed Aug 14, 2023
1 parent e4e3cf7 commit cd393e4
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 31 deletions.
86 changes: 56 additions & 30 deletions packages/provable-programs/src/AttestSingleCredentialProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,83 @@ import { Field, Experimental, Bool, MerkleMapWitness } from 'snarkyjs';
import { CredentialPresentation, SignedClaim, stringToField } from '@herald-sdk/data-model';
import { PublicInputArgs } from './utils/types';

// NOTE: This ZkProgram only works for one field on a claim
// TODO: add more ZkPrograms that work for multiple fields on a claim - use a merge function that takes
// a proof as an input and proves something else about another field on the claim recursively -- rollup style
/**
* Handles comparison operations for a given claim value against a provided value.
*
* @param claimValue - The claim value to be compared.
* @param operation - The operation to perform (e.g., 'lt', 'lte', 'eq', 'gte', 'gt').
* @param value - The value against which the claim value is compared.
*
* @returns A Boolean indicating the result of the comparison.
* @throws {Error} Throws an error if an invalid operation is provided.
*/
function handleOperation(claimValue: Field, operation: Field, value: Field): Bool | undefined {
switch (operation) {
case stringToField('lt'):
return claimValue.lessThan(value);
case stringToField('lte'):
return claimValue.lessThanOrEqual(value);
case stringToField('eq'):
return claimValue.equals(value);
case stringToField('gte'):
return claimValue.greaterThanOrEqual(value);
case stringToField('gt'):
return claimValue.greaterThan(value);
default:
return undefined;
}
}

/**
* A ZkProgram for attesting a single property of a credential.
* @remarks
* Currently, this ZkProgram only supports one field of a claim. Future iterations should provide support for multiple fields.
* Use-case includes combining multiple proofs to attest various fields in a rollup style.
*
* @public
*/
export const AttestSingleCredentialProperty = Experimental.ZkProgram({
publicInput: PublicInputArgs,

methods: {
attest: {
privateInputs: [MerkleMapWitness, Field, SignedClaim, CredentialPresentation],

/**
* Attestation method to validate and prove claims.
* @param publicInputs - The public inputs for the zk proof.
* @param claim - A witness for the MerkleMap.
* @param claimValue - The actual value of the claim.
* @param signedClaim - A claim signed by the issuer.
* @param credentialPresentation - A presentation of the credential, signed by the subject.
* @remarks
* The method checks the validity of the signatures and verifies the match of computed roots. It also infers value based on given operation rules.
*/

method(publicInputs: PublicInputArgs, claim: MerkleMapWitness, claimValue: Field, signedClaim: SignedClaim, credentialPresentation: CredentialPresentation) {
// check the Claim root is signed by the expected issuer
signedClaim.signatureIssuer.verify(
publicInputs.issuerPubKey,
signedClaim.claimRoot.toFields(),
).assertTrue();
).assertTrue('Failed to verify claim root signature by issuer.');
// check the presentation is signed by the expected subject
credentialPresentation.signatureSubject.verify(
publicInputs.subjectPubKey,
signedClaim.signatureIssuer.toFields(),
).assertTrue();
).assertTrue('Failed to verify presentation signature by subject.');

const [computedRoot, _] = claim.computeRootAndKey(claimValue);
computedRoot?.equals(signedClaim.claimRoot).assertTrue();

let inferredValue: Bool | undefined;
// assumption is the claimValue is Field representation of a number (e.g. if the claim is "age" and age is 18 then the claimValue is Field(18))
// this should allow to perform comparisons on the claimValue
// this should also work for checking if a string is equivalent to another string
switch (publicInputs.provingRule.operation) {
case stringToField('lt'):
inferredValue = claimValue.lessThan(Field.from(publicInputs.provingRule.value));
break;
case stringToField('lte'):
inferredValue = claimValue.lessThanOrEqual(Field.from(publicInputs.provingRule.value));
break;
case stringToField('eq'):
inferredValue = claimValue.equals(Field.from(publicInputs.provingRule.value));
break;
case stringToField('gte'):
inferredValue = claimValue.greaterThanOrEqual(Field.from(publicInputs.provingRule.value));
break;
case stringToField('gt'):
inferredValue = claimValue.greaterThan(Field.from(publicInputs.provingRule.value));
break;
}

inferredValue?.assertTrue();
computedRoot?.equals(signedClaim.claimRoot).assertTrue('Computed root does not match signed claim root.');

let inferredValue = handleOperation(claimValue, publicInputs.provingRule.operation, Field.from(publicInputs.provingRule.value));
inferredValue?.assertTrue('No valid operation matched for the given input.');
},
},
},
});

/**
* Class representation for the Attestation Proof derived from the ZkProgram `AttestSingleCredentialProperty`.
* @public
*/
export class AttestationProof extends Experimental.ZkProgram.Proof(AttestSingleCredentialProperty){};
1 change: 1 addition & 0 deletions packages/provable-programs/src/utils/zkPrograms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ export const ZkProgramsDetails: ZkPDetails = {
return { attestationProof: proof };
},
compile: AttestSingleCredentialProperty.compile
// TODO: add cache for compiled program artifacts
}
}
68 changes: 67 additions & 1 deletion packages/provable-programs/test/ZkPrograms.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,77 @@
import { Field, MerkleMapWitness, PrivateKey, PublicKey } from "snarkyjs";
import { AttestSingleCredentialProperty } from "../src";
import { Claim, ClaimType, CredentialPresentation, Rule, SignedClaim, constructClaim, constructSignedClaim } from "@herald-sdk/data-model";

function create(claims: {[key: string]: ClaimType}, issuerPrvKey: PrivateKey | string): {claim: Claim, signedClaim: SignedClaim} {
if (typeof issuerPrvKey === "string") {
issuerPrvKey = PrivateKey.fromBase58(issuerPrvKey);
}
// constructs a Claim (MerkleMap) from a dictionary of claims
const claim = constructClaim(claims);
// construct a signed claim from a claim and a private key
const signedClaim = constructSignedClaim(claim, issuerPrvKey);
return {claim, signedClaim}
}

describe("ZkPrograms", () => {
describe("AttestSingleCredentialProperty", () => {
let subjectPrvKey: PrivateKey
let issuerPrvKey: PrivateKey
let wrongSubjectPrvKey: PrivateKey
let claims: {[key: string]: ClaimType}
let credential: {claim: Claim, signedClaim: SignedClaim}
let rule: Rule
let challenge: { issuerPubKey: PublicKey, subjectPubKey: PublicKey, provingRule: Rule }
let property: string
let operation: string
let value: number
let compiledArtifacts: { verificationKey: string }

beforeAll(async () => {
subjectPrvKey = PrivateKey.random();
issuerPrvKey = PrivateKey.random();
wrongSubjectPrvKey = PrivateKey.random();
claims = {
age: 21,
subject: subjectPrvKey.toPublicKey()
};

credential = create(claims, issuerPrvKey);

property = "age";
operation = 'gte';
value = 18;
rule = new Rule(property, operation, value);

const issuerPubKey = issuerPrvKey.toPublicKey();
const subjectPubKey = subjectPrvKey.toPublicKey();
challenge = { issuerPubKey, subjectPubKey, provingRule: rule };
compiledArtifacts = await AttestSingleCredentialProperty.compile();
});
/*
it("should verify a valid credential", async () => {
const { claim, signedClaim } = credential;
const credentialPresentation = new CredentialPresentation(signedClaim, subjectPrvKey);
const result = await AttestSingleCredentialProperty.attest(challenge, claim, claims.age, signedClaim, credentialPresentation);
expect(result).toBeTruthy();
});*/

it("should fail to verify a signature from not the true subject", async () => {
const { claim, signedClaim } = credential;
const credentialPresentation = new CredentialPresentation(signedClaim, wrongSubjectPrvKey);
const claimWitness = claim.getWitness(property) as MerkleMapWitness;
const claimValue = claim.getField(property) as Field;
console.log("verificationKey", compiledArtifacts.verificationKey)
await expect(
AttestSingleCredentialProperty.attest(challenge, claimWitness, claimValue, signedClaim, credentialPresentation)
).rejects.toThrowError("Failed to verify presentation signature by subject.");
});
/*
it("should deterministically create the same verification key for the same ZkProgram", async () => {
const program = await AttestSingleCredentialProperty.compile();
const program2 = await AttestSingleCredentialProperty.compile();
console.log("program.verificationKey", program.verificationKey)
expect(program.verificationKey).toEqual(program2.verificationKey);
});
*/

});

0 comments on commit cd393e4

Please sign in to comment.