UNPKG

mina-attestations

Version:
507 lines 19.7 kB
import { DynamicProof, FeatureFlags, Field, Poseidon, Provable, ProvableType, Signature, Struct, TokenId, Unconstrained, verify, } from 'o1js'; import { isCredentialSpec, } from "./program-spec.js"; import { createProgram } from "./program.js"; import { credentialMatchesSpec, hashCredential, } from "./credential.js"; import { assert, zip } from "./util.js"; import { hashContext, computeHttpsContext, computeZkAppContext, serializeInputContext, deserializeHttpsContext, deserializeZkAppContext, } from "./context.js"; import { NestedProvable } from "./nested.js"; import { serializeSpec, deserializeSpec } from "./serialize-spec.js"; import { deserializeNestedProvableValue, deserializeProvable, serializeProvable, serializeProvableField, serializeSimplyNestedProvableValue, } from "./serialize-provable.js"; import { PresentationRequestSchema, PresentationSchema, } from "./validation.js"; import { TypeBuilder } from "./provable-type-builder.js"; // external API export { PresentationRequest, HttpsRequest, ZkAppRequest, Presentation, ProvablePresentation, }; // internal export { pickCredentials }; const PresentationRequest = { https(spec, claims, context) { // generate random nonce on "the server" let serverNonce = Field.random(); return HttpsRequest({ spec, claims, program: createProgram(spec), inputContext: { type: 'https', action: context.action, serverNonce }, }); }, httpsFromCompiled(compiled, claims, context) { let serverNonce = Field.random(); return HttpsRequest({ spec: compiled.spec, claims, program: compiled.program, verificationKey: compiled.verificationKey, inputContext: { type: 'https', action: context.action, serverNonce }, }); }, zkApp(spec, claims, context) { return ZkAppRequest({ spec, claims, program: createProgram(spec), inputContext: { type: 'zk-app', verifierIdentity: { publicKey: context.publicKey, tokenId: context.tokenId ?? TokenId.default, network: context.network ?? 'devnet', }, action: context.methodName, serverNonce: context.nonce?.value ?? Field(0), }, }); }, zkAppFromCompiled(compiled, claims, context) { return ZkAppRequest({ spec: compiled.spec, claims, program: compiled.program, verificationKey: compiled.verificationKey, inputContext: { type: 'zk-app', verifierIdentity: { publicKey: context.publicKey, tokenId: context.tokenId ?? TokenId.default, network: context.network ?? 'devnet', }, action: context.methodName, serverNonce: context.nonce?.value ?? Field(0), }, }); }, noContext(spec, claims) { return { type: 'no-context', spec, claims, inputContext: undefined, deriveContext: () => Field(0), }; }, toJSON(request) { let json = { type: request.type, spec: serializeSpec(request.spec), claims: serializeSimplyNestedProvableValue(request.claims), inputContext: serializeInputContext(request.inputContext), }; return JSON.stringify(json); }, fromJSON(expectedType, json) { let raw = JSON.parse(json); let parsed = PresentationRequestSchema.parse(raw); let request = requestFromJson(parsed); assert(request.type === expectedType, `Expected ${expectedType} request, got ${request.type}`); return request; }, }; function requestFromJson(request) { let spec = deserializeSpec(request.spec); let claims = deserializeNestedProvableValue(request.claims); switch (request.type) { case 'no-context': return PresentationRequest.noContext(spec, claims); case 'zk-app': { const inputContext = deserializeZkAppContext(request.inputContext); return ZkAppRequest({ spec, claims, inputContext }); } case 'https': { const inputContext = deserializeHttpsContext(request.inputContext); return HttpsRequest({ spec, claims, inputContext }); } default: throw Error(`Invalid presentation request type: ${request.type}`); } } const Presentation = { async precompile(spec) { let program = createProgram(spec); let verificationKey = await program.compile(); let maxProofsVerified = await program.program.maxProofsVerified(); // TODO this is extra work and should be exposed on ZkProgram let featureFlags = await FeatureFlags.fromZkProgram(program.program); let compiled = { claimsType: program.claimsType, outputClaimType: program.outputClaimType, tagName: program.program.name, verificationKey, maxProofsVerified, featureFlags, }; class Presentation_ extends ProvablePresentation { compiledRequest() { return compiled; } static from(input) { return this.provable.fromValue(input); } static get provable() { return super.provable; } } return { spec, program, verificationKey, ProvablePresentation: Presentation_, }; }, async compile(request) { let program = request.program ?? createProgram(request.spec); let verificationKey = await program.compile(); return { ...request, program, verificationKey }; }, /** * Create a presentation, given the request, context, and credentials. * * The first argument is the private key of the credential's owner, which is needed to sign credentials. */ create: createPresentation, /** * Prepare a presentation, given the request, context, and credentials * * This way creating the presentation doesn't require the private key of the owner but * instead lets the wallet to handle the signing process */ prepare: preparePresentation, /** * Finalize presentation given request, signature, and prepared data from preparePresentation */ finalize: finalizePresentation, /** * Verify a presentation against a request and context. * * Returns the verified output claim of the proof, to be consumed by application-specific logic. */ verify: verifyPresentation, /** * Serialize a presentation to JSON. */ toJSON, /** * Deserialize a presentation from JSON. */ fromJSON, }; async function preparePresentation({ request, context: walletContext, credentials, }) { // find credentials let { credentialsUsed, credentialsAndSpecs } = pickCredentials(request.spec, credentials); // compile the program let compiled = await Presentation.precompile(request.spec); // generate random client nonce let clientNonce = Field.random(); // derive context let context = request.deriveContext(request.inputContext, walletContext, { clientNonce, vkHash: compiled.verificationKey.hash, claims: hashClaims(request.claims), }); // prepare fields to sign let credHashes = credentialsAndSpecs.map(({ credential }) => hashCredential(credential)); let issuers = credentialsAndSpecs.map(({ spec, witness }) => spec.issuer(witness)); // data that is going to be signed by the wallet const fieldsToSign = [context, ...zip(credHashes, issuers).flat()]; return { context, messageFields: fieldsToSign.map((f) => f.toString()), credentialsUsed, serverNonce: request.inputContext?.serverNonce ?? Field(0), clientNonce, compiledRequest: compiled, }; } async function finalizePresentation(request, ownerSignature, preparedData) { // create the presentation proof let proof = await preparedData.compiledRequest.program.run({ context: preparedData.context, claims: request.claims, ownerSignature, credentials: preparedData.credentialsUsed, }); let { proof: proofBase64, maxProofsVerified } = proof.toJSON(); return { version: 'v0', claims: request.claims, outputClaim: proof.publicOutput, serverNonce: preparedData.serverNonce, clientNonce: preparedData.clientNonce, proof: { maxProofsVerified, proof: proofBase64 }, }; } async function createPresentation(ownerKey, params) { const prepared = await preparePresentation(params); const ownerSignature = Signature.create(ownerKey, prepared.messageFields.map(Field.from)); return finalizePresentation(params.request, ownerSignature, prepared); } async function verifyPresentation(request, presentation, context) { // make sure request is compiled let { program, verificationKey } = await Presentation.compile(request); // rederive context let contextHash = request.deriveContext(request.inputContext, context, { clientNonce: presentation.clientNonce, vkHash: verificationKey.hash, claims: hashClaims(request.claims), }); // assert the correct claims were used, and claims match the proof public inputs let { proof, outputClaim } = presentation; let claimType = NestedProvable.get(NestedProvable.fromValue(request.claims)); let claims = request.claims; Provable.assertEqual(claimType, presentation.claims, claims); // reconstruct proof object let inputType = program.program.publicInputType; let outputType = program.program.publicOutputType; let publicInputFields = inputType.toFields({ context: contextHash, claims: claims, }); let publicOutputFields = outputType.toFields(outputClaim); let jsonProof = { publicInput: publicInputFields.map((f) => f.toString()), publicOutput: publicOutputFields.map((f) => f.toString()), proof: proof.proof, maxProofsVerified: proof.maxProofsVerified, }; // verify the proof against our verification key let ok = await verify(jsonProof, verificationKey); assert(ok, 'Invalid proof'); // return the verified outputClaim return outputClaim; } // json function toJSON(presentation) { let json = { version: presentation.version, claims: serializeSimplyNestedProvableValue(presentation.claims), outputClaim: serializeProvable(presentation.outputClaim), serverNonce: serializeProvableField(presentation.serverNonce), clientNonce: serializeProvableField(presentation.clientNonce), proof: presentation.proof, }; return JSON.stringify(json); } function fromJSON(presentationJson) { let parsed = JSON.parse(presentationJson); let presentation = PresentationSchema.parse(parsed); assert(presentation.version === 'v0', `Unsupported presentation version: ${presentation.version}`); return { version: presentation.version, claims: deserializeNestedProvableValue(presentation.claims), outputClaim: deserializeProvable(presentation.outputClaim), serverNonce: deserializeProvable(presentation.serverNonce), clientNonce: deserializeProvable(presentation.clientNonce), proof: presentation.proof, }; } // helper function pickCredentials(spec, [...credentials]) { let credentialsNeeded = Object.entries(spec.inputs).filter((c) => isCredentialSpec(c[1])); let credentialsUsed = {}; let credentialsStillNeeded = []; // an attached `key` signals that the caller knows where to use the credential // in that case, we don't perform additional filtering for (let [key, spec] of credentialsNeeded) { let i = credentials.findIndex((c) => c.key === key); if (i === -1) { credentialsStillNeeded.push([key, spec]); continue; } else { credentialsUsed[key] = credentials[i]; credentials.splice(i, 1); } } for (let credential of credentials) { if (credentialsStillNeeded.length === 0) break; // can we use this credential for one of the remaining slots? let j = credentialsStillNeeded.findIndex(([, spec]) => { let matches = credentialMatchesSpec(spec, credential); // console.log('matches', matches, spec, credential); return matches; }); if (j === -1) continue; let [slot] = credentialsStillNeeded.splice(j, 1); let [key] = slot; credentialsUsed[key] = credential; } assert(credentialsStillNeeded.length === 0, `Missing credentials: ${credentialsStillNeeded .map(([key]) => `"${key}"`) .join(', ')}`); let credentialsAndSpecs = credentialsNeeded.map(([key, spec]) => ({ ...credentialsUsed[key], spec, })); return { credentialsUsed, credentialsAndSpecs }; } function HttpsRequest(request) { return { type: 'https', ...request, deriveContext(inputContext, walletContext, derivedContext) { const context = computeHttpsContext({ ...inputContext, ...walletContext, ...derivedContext, }); return hashContext(context); }, }; } function ZkAppRequest(request) { return { type: 'zk-app', ...request, deriveContext(inputContext, _walletContext, derivedContext) { const context = computeZkAppContext({ ...inputContext, ...derivedContext, }); return hashContext(context); }, }; } function hashClaims(claims) { let claimsType = NestedProvable.fromValue(claims); let claimsFields = Struct(claimsType).toFields(claims); return Poseidon.hash(claimsFields); } function hashClaimsFromType(claimsType, claims) { let claimsFields = ProvableType.get(claimsType).toFields(claims); return Poseidon.hash(claimsFields); } // in-circuit verification and provable type /** * Presentation that can be verified inside a zkApp. * * Create a subclass for your presentation as follows: * * ```ts * let compiled = await Presentation.precompile(spec); * class ProvablePresentation extends compiled.ProvablePresentation {} * ``` */ class ProvablePresentation { // properties created from a presentation claims; outputClaim; clientNonce; serverNonce; proof; constructor(input) { this.claims = input.claims; this.outputClaim = input.outputClaim; this.clientNonce = input.clientNonce; this.serverNonce = input.serverNonce; this.proof = input.proof; } // static properties derived from precompiling the request compiledRequest() { throw Error('Must be implemented in subclass'); } /** * Verify presentation in a provable context. * * Input is the zkApp which this presentation is verified in. * * Pass in the public key, token id and current method of your zkapp to make sure * you don't accept presentations that were intended for a different context. * * Optionally, you can further restrict context by passing in the network and nonce. */ verify(context) { // input/output types let compiled = this.compiledRequest(); let { claimsType, outputClaimType } = compiled; let { claims, outputClaim } = this; // rederive context let fullContext = computeZkAppContext({ type: 'zk-app', verifierIdentity: { publicKey: context.publicKey, tokenId: context.tokenId, network: context.network ?? 'devnet', }, action: context.methodName, serverNonce: context.nonce?.value ?? Field(0), clientNonce: this.clientNonce, vkHash: compiled.verificationKey.hash, claims: hashClaimsFromType(claimsType, claims), }); let contextHash = hashContext(fullContext); // reconstruct proof class // TODO there should be DynamicProof.fromProgram() class PresentationProof extends DynamicProof { static publicInputType = NestedProvable.get({ context: Field, claims: claimsType, }); static publicOutputType = ProvableType.get(outputClaimType); static maxProofsVerified = compiled.maxProofsVerified; static featureFlags = compiled.featureFlags; static tag() { return { name: compiled.tagName }; } } // witness proof and mark it to be verified let presentationProof = Provable.witness(PresentationProof, () => { return { proof: DynamicProof._proofFromBase64(this.proof.get(), compiled.maxProofsVerified), maxProofsVerified: compiled.maxProofsVerified, publicInput: { context: contextHash.toConstant(), claims: Provable.toConstant(claimsType, claims), }, publicOutput: Provable.toConstant(outputClaimType, this.outputClaim), }; }); presentationProof.declare(); presentationProof.verify(compiled.verificationKey); // check public inputs Provable.assertEqual(claimsType, presentationProof.publicInput.claims, claims); presentationProof.publicInput.context.assertEquals(contextHash); Provable.assertEqual(outputClaimType, presentationProof.publicOutput, outputClaim); // return the verified claims return { claims, outputClaim }; } // provable type representation static get provable() { let This = this; let { claimsType, outputClaimType, maxProofsVerified } = this.prototype.compiledRequest(); return TypeBuilder.shape({ claims: claimsType, outputClaim: outputClaimType, clientNonce: Field, serverNonce: Unconstrained.withEmpty(0n), proof: Unconstrained.withEmpty(''), }) .forClass(This) .mapValue({ there(p) { return { version: 'v0', claims: ProvableType.get(claimsType).fromValue(p.claims), outputClaim: ProvableType.get(outputClaimType).fromValue(p.outputClaim), clientNonce: Field(p.clientNonce), serverNonce: Field(p.serverNonce), proof: { proof: p.proof, maxProofsVerified }, }; }, back(p) { return { claims: ProvableType.get(claimsType).toValue(p.claims), outputClaim: ProvableType.get(outputClaimType).toValue(p.outputClaim), clientNonce: p.clientNonce.toBigInt(), serverNonce: p.serverNonce.toBigInt(), proof: p.proof.proof, }; }, distinguish(p) { return p instanceof This; }, }) .build(); } } //# sourceMappingURL=presentation.js.map