mina-attestations
Version:
Private Attestations on Mina
507 lines • 19.8 kB
JavaScript
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, deserializeProvableValue, serializeProvableValue, 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: serializeProvableValue(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: deserializeProvableValue(presentation.outputClaim),
serverNonce: deserializeProvableValue(presentation.serverNonce),
clientNonce: deserializeProvableValue(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