From cd393e4e9064d3f64aade637d23cbd45437a1ccf Mon Sep 17 00:00:00 2001 From: Teddy Date: Mon, 14 Aug 2023 22:40:15 +0100 Subject: [PATCH] chore(Improve zkProgram structure): add TSDocs --- .../src/AttestSingleCredentialProperty.ts | 86 ++++++++++++------- .../provable-programs/src/utils/zkPrograms.ts | 1 + .../provable-programs/test/ZkPrograms.test.ts | 68 ++++++++++++++- 3 files changed, 124 insertions(+), 31 deletions(-) diff --git a/packages/provable-programs/src/AttestSingleCredentialProperty.ts b/packages/provable-programs/src/AttestSingleCredentialProperty.ts index a150f46..f3f24f7 100644 --- a/packages/provable-programs/src/AttestSingleCredentialProperty.ts +++ b/packages/provable-programs/src/AttestSingleCredentialProperty.ts @@ -2,9 +2,41 @@ 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, @@ -12,47 +44,41 @@ export const AttestSingleCredentialProperty = Experimental.ZkProgram({ 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){}; \ No newline at end of file diff --git a/packages/provable-programs/src/utils/zkPrograms.ts b/packages/provable-programs/src/utils/zkPrograms.ts index 65f347c..6945310 100644 --- a/packages/provable-programs/src/utils/zkPrograms.ts +++ b/packages/provable-programs/src/utils/zkPrograms.ts @@ -9,5 +9,6 @@ export const ZkProgramsDetails: ZkPDetails = { return { attestationProof: proof }; }, compile: AttestSingleCredentialProperty.compile + // TODO: add cache for compiled program artifacts } } \ No newline at end of file diff --git a/packages/provable-programs/test/ZkPrograms.test.ts b/packages/provable-programs/test/ZkPrograms.test.ts index eae056c..c44efef 100644 --- a/packages/provable-programs/test/ZkPrograms.test.ts +++ b/packages/provable-programs/test/ZkPrograms.test.ts @@ -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); }); +*/ + }); \ No newline at end of file