diff --git a/package.json b/package.json index d7a92f9..9b512a8 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,8 @@ "types": "build/src/index.d.ts", "scripts": { "build": "tsc", - "buildw": "tsc --watch", - "coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage", + "watch": "tsc --watch", "format": "prettier --write --ignore-unknown **/*", - "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", - "testw": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch", "extension:dev": "vite build --config browser-extension/vite.config.js --watch", "extension:build": "vite build --config browser-extension/vite.config.js" }, diff --git a/spec/rfc.md b/spec/rfc.md index 3fd76b2..18b22f1 100644 --- a/spec/rfc.md +++ b/spec/rfc.md @@ -86,6 +86,7 @@ The flow begins by defining a ZK program using `credentialProgram.create` this d A `claim` is then constructed, a `claim` is private data which the user wants to attest to. This `claim` can be turned into a `Credential` which could even be used later to attest to the data in the `claim`. The `Credential` can then be used to generate a proof based on public params such as minAge in this specific example and the `credentialProgram` generated earlier. + ```javascript import { Credential } from '@custom-sdk/credentials'; import credentialProgram from '@custom-sdk/credentialProgram'; @@ -94,18 +95,19 @@ import { PrivateKey, PublicKey } from '@custom-sdk'; // Define the VerifyComplexAge program using modular operations from the library const VerifyComplexAgeProgramConfig = { - name: 'VerifyComplexAge', - input: { minAge: 'number', maxAge: 'number' }, - output: { valid: 'boolean' }, - logic: Operations.and([ - Operations.greaterThanOrEqual('credential.age', 'input.minAge'), - Operations.lessThanOrEqual('credential.age', 'input.maxAge') - ]), - attestationType: 'signature' + name: 'VerifyComplexAge', + input: { minAge: 'number', maxAge: 'number' }, + output: { valid: 'boolean' }, + logic: Operations.and([ + Operations.greaterThanOrEqual('credential.age', 'input.minAge'), + Operations.lessThanOrEqual('credential.age', 'input.maxAge'), + ]), }; // Create the VerifyComplexAge program instance -const VerifyComplexAge = credentialProgram.create(VerifyComplexAgeProgramConfig); +const VerifyComplexAge = credentialProgram.create( + VerifyComplexAgeProgramConfig +); // Generate issuer private and public keys const issuerPrvKey = PrivateKey.random(); @@ -116,14 +118,22 @@ const subjectPrvKey = PrivateKey.random(); const subjectPubKey = subjectPrvKey.toPublicKey().toBase58(); // Convert to string format for use in the challenge // Construct a claim about the subject, including a signature by the issuer -const claimsObject = { age: 21, subject: subjectPubKey, signature: issuerPrvKey.sign('age:21') }; +const claimsObject = { + age: 21, + subject: subjectPubKey, + signature: issuerPrvKey.sign('age:21'), +}; const claims = JSON.stringify(claimsObject); // Construct a credential using the claim const credential = Credential.create(claims); // Prove the credential satisfies the challenge using the VerifyComplexAge program -const proofResponse = await credential.prove('age', { minAge: 18, maxAge: 65, issuerPublicKey: issuerPubKey }, VerifyComplexAge); +const proofResponse = await credential.prove( + 'age', + { minAge: 18, maxAge: 65, issuerPublicKey: issuerPubKey }, + VerifyComplexAge +); console.log(proofResponse); ``` @@ -136,16 +146,16 @@ The API should be initialized within the wallet provider context. It could use a ```javascript interface MinaWallet { - attestation: AttestationAPI; + attestation: AttestationAPI; } interface AttestationAPI { - initialize(config: AttestationConfig): Promise; + initialize(config: AttestationConfig): Promise; } interface AttestationConfig { - apiKey: string; - endpoint: string; + apiKey: string; + endpoint: string; } ``` @@ -155,20 +165,20 @@ This API allows users to create credentials about various data types locally. ```javascript interface AttestationAPI { - create(params: LocalCredentialParams): Promise; + create(params: LocalCredentialParams): Promise; } interface LocalCredentialParams { - claims: { [key: string]: any }; // Claims about the subject + claims: { [key: string]: any }; // Claims about the subject } interface CredentialResponse { - credentialId: string; - credential: string; // Encoded credential - nullifier?: string; // Unique identifier for the credential - nullifierKey?: string; // Key associated with the nullifier - nullifierProof?: string; // Proof that the nullifierKey was derived as expected - expiration?: number; // Expiration time if set + credentialId: string; + credential: string; // Encoded credential + nullifier?: string; // Unique identifier for the credential + nullifierKey?: string; // Key associated with the nullifier + nullifierProof?: string; // Proof that the nullifierKey was derived as expected + expiration?: number; // Expiration time if set } ``` @@ -178,41 +188,41 @@ This API is used to define a Zero-Knowledge (ZK) program that specifies the logi ```javascript interface CredentialProgramInput { - [key: string]: 'number' | 'string' | 'boolean'; + [key: string]: 'number' | 'string' | 'boolean'; } interface CredentialProgramOutput { - [key: string]: 'boolean'; + [key: string]: 'boolean'; } interface CredentialProgramConfig { - name: string; - input: CredentialProgramInput; - output: CredentialProgramOutput; - logic: OperationNode; // Updated to use a static object + name: string; + input: CredentialProgramInput; + output: CredentialProgramOutput; + logic: OperationNode; // Updated to use a static object } interface OperationNode { - operation: string; - inputs?: (OperationNode | any)[]; // The inputs can be either another operation node or a static value - [key: string]: any; // Allow for additional properties specific to the operation + operation: string; + inputs?: (OperationNode | any)[]; // The inputs can be either another operation node or a static value + [key: string]: any; // Allow for additional properties specific to the operation } interface CredentialProgram { - create(config: CredentialProgramConfig): CredentialVerificationInstance; + create(config: CredentialProgramConfig): CredentialVerificationInstance; } interface CredentialVerificationInstance { - name: string; - input: CredentialProgramInput; - output: CredentialProgramOutput; - logic: OperationNode; + name: string; + input: CredentialProgramInput; + output: CredentialProgramOutput; + logic: OperationNode; } ``` When generating a new ZK Program, - the `create` must know how to parse the `CredentialVerificationInstance` logic and generate input compatible with our custom DSL (see below custom DSL definition). +the `create` must know how to parse the `CredentialVerificationInstance` logic and generate input compatible with our custom DSL (see below custom DSL definition). ### `credentialProgram.Operations` API @@ -221,7 +231,7 @@ Every credential program receives an `OperationNode`, an operation node is an ob ```json { "operation": "and", - "inputs": [ + "inputs": [ { "operation": "greaterThan", "firstInput": 18, @@ -229,7 +239,7 @@ Every credential program receives an `OperationNode`, an operation node is an ob }, { "operation": "lessThan", - "firstInput": 51, + "firstInput": 51, "secondInput": "credential.maxAge", }, ... @@ -242,44 +252,44 @@ However, writing such a verbose expression is complex, hard to debug, and prone ```javascript // Define boolean operations const Operations = { - greaterThanOrEqual: (firstInput, secondInput) => ({ - operation: 'greaterThanOrEqual', - firstInput, - secondInput - }), - lessThanOrEqual: (firstInput, secondInput) => ({ - operation: 'lessThanOrEqual', - firstInput, - secondInput - }), - and: (inputs) => ({ - operation: 'and', - inputs - }), - or: (inputs) => ({ - operation: 'or', - inputs - }), - equals: (firstInput, secondInput) => ({ - operation: 'equals', - firstInput, - secondInput - }), - hash: (input) => ({ - operation: 'hash', - input - }), - verifySignature: (message, signature, publicKey) => ({ - operation: 'verifySignature', - message, - signature, - publicKey - }), - verifyProof: (inputs) => ({ - operation: 'verifyProof', - inputs - }) - // Add more operations as needed + greaterThanOrEqual: (firstInput, secondInput) => ({ + operation: 'greaterThanOrEqual', + firstInput, + secondInput, + }), + lessThanOrEqual: (firstInput, secondInput) => ({ + operation: 'lessThanOrEqual', + firstInput, + secondInput, + }), + and: (inputs) => ({ + operation: 'and', + inputs, + }), + or: (inputs) => ({ + operation: 'or', + inputs, + }), + equals: (firstInput, secondInput) => ({ + operation: 'equals', + firstInput, + secondInput, + }), + hash: (input) => ({ + operation: 'hash', + input, + }), + verifySignature: (message, signature, publicKey) => ({ + operation: 'verifySignature', + message, + signature, + publicKey, + }), + verifyProof: (inputs) => ({ + operation: 'verifyProof', + inputs, + }), + // Add more operations as needed }; export default Operations; @@ -289,10 +299,14 @@ Users can now use these methods to compose complex logic. ```javascript const operationsJSON = Operations.and([ - Operations.greaterThanOrEqual('credential.age', 18), - Operations.lessThanOrEqual('credential.age', 65), - Operations.verifySignature('credential.message', 'credential.signature', 'credential.publicKey') -]) + Operations.greaterThanOrEqual('credential.age', 18), + Operations.lessThanOrEqual('credential.age', 65), + Operations.verifySignature( + 'credential.message', + 'credential.signature', + 'credential.publicKey' + ), +]); ``` ### `Credential.create` API @@ -301,13 +315,13 @@ This API is used to create a credential from a set of claims. ```javascript interface CredentialAPI { - create(claims: string): Credential; + create(claims: string): Credential; } interface Credential { - claims: { [key: string]: any }; - issuerPublicKey: string; - signature: string; + claims: { [key: string]: any }; + issuerPublicKey: string; + signature: string; } ``` @@ -317,21 +331,25 @@ This API is used to generate a cryptographic proof based on a credential, a set ```javascript interface ProofAPI { - prove(claimKey: string, publicParams: object, credentialVerificationInstance: CredentialVerificationInstance): Promise; + prove( + claimKey: string, + publicParams: object, + credentialVerificationInstance: CredentialVerificationInstance + ): Promise; } interface ProofResponse { - proof: object; - proofId: string; - valid: boolean; - publicParams: object; + proof: object; + proofId: string; + valid: boolean; + publicParams: object; } interface CredentialVerificationInstance { - name: string; - input: { [key: string]: 'number' | 'string' | 'boolean' }; - output: { [key: string]: 'number' | 'string' | 'boolean' }; - logic: (input: { [key: string]: any }) => { [key: string]: any }; + name: string; + input: { [key: string]: 'number' | 'string' | 'boolean' }; + output: { [key: string]: 'number' | 'string' | 'boolean' }; + logic: (input: { [key: string]: any }) => { [key: string]: any }; } ``` @@ -341,16 +359,16 @@ This API allows users to compose multiple proofs into a composite proof. ```javascript interface AttestationAPI { - composeAttestation(params: ComposeParams): Promise; + composeAttestation(params: ComposeParams): Promise; } interface ComposeParams { - attestationIds: string[]; // List of attestation IDs to be composed + attestationIds: string[]; // List of attestation IDs to be composed } interface ComposeResponse { - compositeAttestationId: string; - compositeProof: string; // Composite cryptographic proof + compositeAttestationId: string; + compositeProof: string; // Composite cryptographic proof } ``` @@ -367,6 +385,7 @@ A nullifier is a cryptographic element used to mark a proof as consumed. Once a The nullifier implementation involves generating a unique identifier for each proof, which will be stored and checked during verification. Here’s a detailed approach to implementing nullifiers within the Wallet Provider Private Attestation API: 1. **Nullifier Generation**: + - During proof generation, a nullifier will be created using a deterministic function based on the credential’s unique identifier and claims. This ensures that the nullifier is unique to each proof. - The nullifier should be coupled with a `nullifierKey`, which is generated by hashing the nullifier with a zkApp-specific hash and an auto-generated salt. This ensures that the nullifierKey is specific to the application. - The salt will be auto-generated for each proof to ensure uniqueness and enhance security. @@ -375,70 +394,84 @@ The nullifier implementation involves generating a unique identifier for each pr - The wallet should warn users if a nullifier key is being requested with the same zkApp-specific hash, as this could link different user activities. 2. **Nullifier Storage**: + - Once a proof is used, its nullifier is recorded in a secure storage system. This storage can be on-chain or off-chain, depending on the application requirements and security considerations. - The storage system must ensure that nullifiers are immutable and tamper-proof. 3. **Verification Process**: + - During the verification of a proof, the nullifier, the nullifierKey, and the salt are checked against the recorded nullifiers to ensure they have not been used before. - If the nullifier is found in the storage, the proof is rejected to prevent reuse. - This verification step ensures that each proof remains unique and prevents replay attacks. ```javascript interface VerifyParams { - proofId: string; - verificationKey: string; // Verification key - nullifier: string; // Nullifier to be checked - nullifierKey: string; // Key associated with the nullifier - salt: string; // Salt used during proof generation - credentialId: string; // Unique identifier for the credential - claims: any; // Claims used to generate the nullifier + proofId: string; + verificationKey: string; // Verification key + nullifier: string; // Nullifier to be checked + nullifierKey: string; // Key associated with the nullifier + salt: string; // Salt used during proof generation + credentialId: string; // Unique identifier for the credential + claims: any; // Claims used to generate the nullifier } interface VerifyResponse { - valid: boolean; - message: string; + valid: boolean; + message: string; } async function verifyProof(params: VerifyParams): Promise { - // Reconstruct the nullifier using the credentialId and claims - const expectedNullifier = generateNullifier(params.credentialId, params.claims); - if (expectedNullifier !== params.nullifier) { - return { valid: false, message: "Invalid nullifier." }; - } - - // Verify that the nullifier and nullifierKey were derived using the salt - const expectedNullifierKey = generateNullifierKey(expectedNullifier, params.salt); - if (expectedNullifierKey !== params.nullifierKey) { - return { valid: false, message: "Invalid nullifier key." }; - } - - const nullifierExists = await checkNullifier(params.nullifier, params.nullifierKey); - if (nullifierExists) { - return { valid: false, message: "Proof has already been used." }; - } - - // Additional verification logic here - return { valid: true, message: "Proof is valid." }; + // Reconstruct the nullifier using the credentialId and claims + const expectedNullifier = generateNullifier( + params.credentialId, + params.claims + ); + if (expectedNullifier !== params.nullifier) { + return { valid: false, message: 'Invalid nullifier.' }; + } + + // Verify that the nullifier and nullifierKey were derived using the salt + const expectedNullifierKey = generateNullifierKey( + expectedNullifier, + params.salt + ); + if (expectedNullifierKey !== params.nullifierKey) { + return { valid: false, message: 'Invalid nullifier key.' }; + } + + const nullifierExists = await checkNullifier( + params.nullifier, + params.nullifierKey + ); + if (nullifierExists) { + return { valid: false, message: 'Proof has already been used.' }; + } + + // Additional verification logic here + return { valid: true, message: 'Proof is valid.' }; } - async function checkNullifier(nullifier: string, nullifierKey: string): Promise { - // Check the storage for the nullifier and nullifierKey - // Return true if nullifier exists, false otherwise + async function checkNullifier( + nullifier: string, + nullifierKey: string + ): Promise { + // Check the storage for the nullifier and nullifierKey + // Return true if nullifier exists, false otherwise } function generateNullifierKey(nullifier: string, salt: string): string { - // Generate the nullifierKey using the nullifier and salt - // This is a placeholder implementation and should use a secure hash function - return someHashFunction(nullifier + salt); + // Generate the nullifierKey using the nullifier and salt + // This is a placeholder implementation and should use a secure hash function + return someHashFunction(nullifier + salt); } function generateNullifier(credentialId: string, claims: any): string { - // Generate the nullifier using a secure hash function - return someHashFunction(credentialId + JSON.stringify(claims)); + // Generate the nullifier using a secure hash function + return someHashFunction(credentialId + JSON.stringify(claims)); } function generateSalt(): string { - return randomBytes(16).toString('hex'); // Generate a 16-byte salt and convert to hex + return randomBytes(16).toString('hex'); // Generate a 16-byte salt and convert to hex } ``` @@ -466,22 +499,22 @@ The command language needs to be simple yet expressive enough to handle typical #### Command Language Syntax 1. **Set Input**: Assign values to input variables. - ```plaintext - SET INPUT variable value; - ``` + ```plaintext + SET INPUT variable value; + ``` 2. **Set Output**: Assign values to output variables. - ```plaintext - SET OUTPUT variable value; - ``` + ```plaintext + SET OUTPUT variable value; + ``` 3. **Conditional Statements**: Perform conditional logic. - ```plaintext - IF variable1 operator variable2 THEN - commands - ELSE - commands - ENDIF; - ``` - - **Operators**: `==`, `!=`, `<`, `>`, `<=`, `>=` + ```plaintext + IF variable1 operator variable2 THEN + commands + ELSE + commands + ENDIF; + ``` + - **Operators**: `==`, `!=`, `<`, `>`, `<=`, `>=` ### Example Program in Command Language @@ -490,20 +523,28 @@ Here is the `VerifyAge` program defined via our Attestation API: ```javascript // Define the VerifyAge program const VerifyAge = credentialProgram.create({ - name: 'VerifyAge', - input: { age: 'number', minAge: 'number' }, - output: { valid: 'boolean' }, - logic: (input) => { - return { valid: input.age >= input.minAge }; - } + name: 'VerifyAge', + input: { age: 'number', minAge: 'number' }, + output: { valid: 'boolean' }, + logic: (input) => { + return { valid: input.age >= input.minAge }; + }, }); ``` ```javascript -const claimsObject = { age: 21, subject: subjectPubKey, signature: issuerPrvKey.sign('age:21') }; +const claimsObject = { + age: 21, + subject: subjectPubKey, + signature: issuerPrvKey.sign('age:21'), +}; const claims = JSON.stringify(claimsObject); const credential = Credential.create(claims); -const proofResponse = await credential.prove('age', { minAge: 18, issuerPublicKey: issuerPubKey }, VerifyAge); +const proofResponse = await credential.prove( + 'age', + { minAge: 18, issuerPublicKey: issuerPubKey }, + VerifyAge +); ``` Here is the `VerifyAge` program outputted in the command language, the command language is compiled at the Attestation API level: diff --git a/src/credential-types.ts b/src/credential-types.ts new file mode 100644 index 0000000..9bcd288 --- /dev/null +++ b/src/credential-types.ts @@ -0,0 +1,115 @@ +export type { Credential }; + +// ****** EXAMPLE WALLET PROVIDER ATTESTATION API ****** + +type MinaWallet = { + attestation: AttestationAPI; +}; + +type AttestationAPI = { + initialize(config: AttestationConfig): Promise; +}; + +type AttestationConfig = { + apiKey: string; + endpoint: string; +}; + +// ****** CREDENTIAL CREATION API ****** + +type CredentialAttestationAPI = { + create(params: LocalCredentialParams): Promise; +}; + +type LocalCredentialParams = { + claims: { [key: string]: any }; // Claims about the subject +}; + +type CredentialResponse = { + credentialId: string; + credential: string; // Encoded credential + nullifier?: string; // Unique identifier for the credential + nullifierKey?: string; // Key associated with the nullifier + nullifierProof?: string; // Proof that the nullifierKey was derived as expected + expiration?: number; // Expiration time if set +}; + +// ****** CREDENTIALPROGRAM.CREATE API ****** + +type CredentialProgramInput = { + [key: string]: 'number' | 'string' | 'boolean'; +}; + +type CredentialProgramOutput = { + [key: string]: 'boolean'; +}; + +type CredentialProgramConfig = { + name: string; + input: CredentialProgramInput; + output: CredentialProgramOutput; + logic: OperationNode; +}; + +type OperationNode = { + operation: string; + inputs?: (OperationNode | any)[]; // The inputs can be either another operation node or a static value + [key: string]: any; // Allow for additional properties specific to the operation +}; + +type CredentialProgram = { + create(config: CredentialProgramConfig): CredentialVerificationInstance; +}; + +type CredentialVerificationInstance = { + name: string; + input: CredentialProgramInput; + output: CredentialProgramOutput; + logic: OperationNode; +}; + +// TODO: decide credentialProgram.Operations API + +// ****** CREDENTIAL.CREATE API ****** + +type CredentialAPI = { + create(claims: string): Credential; +}; + +type Credential = { + claims: { [key: string]: any }; + issuerPublicKey: string; + signature: string; +}; + +// ****** CREDENTIAL.PROVE API ****** + +type ProofAPI = { + prove( + claimKey: string, + publicParams: object, + credentialVerificationInstance: CredentialVerificationInstance + ): Promise; +}; + +type ProofResponse = { + proof: object; + proofId: string; + valid: boolean; + publicParams: object; +}; + +// ****** PROOF COMPOSITION API ****** + +type ProofAttestationAPI = { + composeAttestation(params: ComposeParams): Promise; +}; + +type ComposeParams = { + attestationIds: string[]; // List of attestation IDs to be composed +}; + +type ComposeResponse = { + compositeAttestationId: string; + compositeProof: string; // Composite cryptographic proof +}; diff --git a/src/nested.ts b/src/nested.ts new file mode 100644 index 0000000..34e229d --- /dev/null +++ b/src/nested.ts @@ -0,0 +1,51 @@ +/** + * Allows us to represent nested Provable types, to save us from always having to + * wrap types in `Struct` and similar. + */ +import { Provable, type ProvablePure, Struct } from 'o1js'; +import { type ProvablePureType, ProvableType } from './o1js-missing.ts'; + +export { NestedProvable }; + +export type { + NestedProvablePure, + NestedProvableFor, + NestedProvablePureFor, + InferNestedProvable, +}; + +const NestedProvable = { + get: ((type: NestedProvableFor): Provable => { + return ProvableType.isProvableType(type) + ? ProvableType.get(type) + : Struct(type); + }) as { + (type: NestedProvablePureFor): ProvablePure; + (type: NestedProvableFor): Provable; + (type: NestedProvablePure): ProvablePure; + (type: NestedProvable): Provable; + }, +}; + +type NestedProvable = ProvableType | { [key: string]: NestedProvable }; +type NestedProvablePure = + | ProvablePureType + | { [key: string]: NestedProvablePure }; + +type NestedProvableFor = + | ProvableType + | { [K in keyof T & string]: NestedProvableFor }; + +type NestedProvablePureFor = + | ProvablePureType + | { [K in keyof T & string]: NestedProvablePureFor }; + +type InferNestedProvable = A extends NestedProvableFor + ? T + : A extends ProvableType + ? T + : A extends Record + ? { + [K in keyof A]: InferNestedProvable; + } + : never; diff --git a/src/o1js-missing.ts b/src/o1js-missing.ts new file mode 100644 index 0000000..0d50764 --- /dev/null +++ b/src/o1js-missing.ts @@ -0,0 +1,104 @@ +/** + * This file exports types and functions that actually should be exported from o1js + */ +import { + Field, + type InferProvable, + Provable, + type ProvablePure, + Undefined, +} from 'o1js'; +import { assert, assertHasProperty, hasProperty } from './util.ts'; + +export { + ProvableType, + assertPure, + type ProvablePureType, + type InferProvableType, +}; + +const ProvableType = { + get>(type: A): ToProvable { + return ( + (typeof type === 'object' || typeof type === 'function') && + type !== null && + 'provable' in type + ? type.provable + : type + ) as ToProvable; + }, + + // TODO o1js should make sure this is possible for _all_ provable types + fromValue(value: T): Provable { + if (value === undefined) return Undefined as any; + assertHasProperty( + value, + 'constructor', + 'Encountered provable value without a constructor: Cannot obtain provable type.' + ); + let constructor = value.constructor; + let type = ProvableType.get(constructor); + assertIsProvable(type); + return type; + }, + + synthesize(type_: ProvableType): T { + let type = ProvableType.get(type_); + let fields = Array.from({ length: type.sizeInFields() }, (_, i) => + Field(0) + ); + return type.fromFields(fields, type.toAuxiliary()); + }, + + isProvableType(type: unknown): type is ProvableType { + let type_ = ProvableType.get(type); + return hasProperty(type_, 'toFields') && hasProperty(type_, 'fromFields'); + }, +}; + +function assertPure(type_: Provable): asserts type_ is ProvablePure; +function assertPure( + type: ProvableType +): asserts type is ProvablePureType; +function assertPure( + type: ProvableType +): asserts type is ProvablePureType { + let aux = ProvableType.get(type).toAuxiliary(); + assert( + lengthRecursive(aux) === 0, + 'Expected pure provable type to have no auxiliary fields' + ); +} + +type NestedArray = any[] | NestedArray[]; + +function lengthRecursive(array: NestedArray): number { + let length = 0; + for (let i = 0; i < array.length; i++) { + length += lengthRecursive(array[i]); + } + return length; +} + +function assertIsProvable(type: unknown): asserts type is Provable { + assertHasProperty( + type, + 'toFields', + 'Expected provable type to have a toFields method' + ); + assertHasProperty( + type, + 'fromFields', + 'Expected provable type to have a fromFields method' + ); +} + +type WithProvable = { provable: A } | A; +type ProvableType = WithProvable>; +type ProvablePureType = WithProvable>; +type ToProvable> = A extends { + provable: infer P; +} + ? P + : A; +type InferProvableType = InferProvable>; diff --git a/src/program-config.ts b/src/program-config.ts index 0abbf06..47115b6 100644 --- a/src/program-config.ts +++ b/src/program-config.ts @@ -3,59 +3,105 @@ import { Bool, Bytes, Field, + PrivateKey, Provable, PublicKey, Signature, Struct, Undefined, VerificationKey, - type InferProvable, type ProvablePure, } from 'o1js'; -import type { Tuple } from './types.ts'; +import type { ExcludeFromRecord } from './types.ts'; +import { + assertPure, + type InferProvableType, + type ProvablePureType, + ProvableType, +} from './o1js-missing.ts'; +import { assertHasProperty } from './util.ts'; +import { + type InferNestedProvable, + NestedProvable, + type NestedProvableFor, + type NestedProvablePure, + type NestedProvablePureFor, +} from './nested.ts'; /** * TODO: program spec must be serializable * - can be done by defining an enum of supported base types */ -export type { Node }; -export { Spec, Attestation, Operation, Input, GetData }; +export type { PublicInputs, UserInputs }; +export { + Spec, + Node, + Attestation, + Operation, + Input, + publicInputTypes, + publicOutputType, + privateInputTypes, + splitUserInputs, + verifyAttestations, + recombineDataInputs, +}; -type Spec = Tuple> = { +type Spec< + Data = any, + Inputs extends Record = Record +> = { inputs: Inputs; - logic: OutputNode; + logic: Required>; }; /** * Specify a ZkProgram that verifies and selectively discloses data */ -function Spec>( +function Spec>( + inputs: Inputs, + spec: (inputs: { + [K in keyof Inputs]: Node>; + }) => { + assert?: Node; + data: Node; + } +): Spec; + +// variant without data output +function Spec>( inputs: Inputs, - spec: ( - ...inputs: { - [K in keyof Inputs]: Node>; - } & any[] - ) => OutputNode + spec: (inputs: { + [K in keyof Inputs]: Node>; + }) => { + assert?: Node; + } +): Spec; + +// implementation +function Spec>( + inputs: Inputs, + spec: (inputs: { + [K in keyof Inputs]: Node>; + }) => OutputNode ): Spec { - return { inputs, logic: spec(...(inputs as any)) }; + let rootNode = root(inputs); + let inputNodes: { + [K in keyof Inputs]: Node>; + } = {} as any; + for (let key in inputs) { + inputNodes[key] = property(rootNode, key) as any; + } + let logic = spec(inputNodes); + let assert = logic.assert ?? Node.constant(Bool(true)); + let data: Node = logic.data ?? (Node.constant(undefined) as any); + + return { inputs, logic: { assert, data } }; } const Undefined_: ProvablePure = Undefined; -// TODO export from o1js -const ProvableType = { - get>(type: A): ToProvable { - return ( - (typeof type === 'object' || typeof type === 'function') && - type !== null && - 'provable' in type - ? type.provable - : type - ) as ToProvable; - }, -}; - /** * An attestation is: * - a string fully identifying the attestation type @@ -65,10 +111,11 @@ const ProvableType = { * - a function `verify(publicInput: Public, privateInput: Private, data: Data)` that asserts the attestation is valid */ type Attestation = { - type: Id; + type: 'attestation'; + id: Id; public: ProvablePureType; private: ProvableType; - data: ProvablePureType; + data: NestedProvablePureFor; verify(publicInput: Public, privateInput: Private, data: Data): void; }; @@ -78,30 +125,31 @@ function defineAttestation< PublicType extends ProvablePureType, PrivateType extends ProvableType >(config: { - type: Id; + id: Id; public: PublicType; private: PrivateType; - verify( + verify( publicInput: InferProvableType, privateInput: InferProvableType, dataType: DataType, - data: InferProvableType + data: InferNestedProvable ): void; -}): ( - data: DataType -) => Attestation< - Id, - InferProvableType, - InferProvableType, - InferProvableType -> { - return function attestation(dataType) { +}) { + return function attestation( + dataType: DataType + ): Attestation< + Id, + InferProvableType, + InferProvableType, + InferNestedProvable + > { return { - type: config.type, + type: 'attestation', + id: config.id, public: config.public, private: config.private, - data: dataType, + data: dataType as any, verify(publicInput, privateInput, data) { return config.verify(publicInput, privateInput, dataType, data); }, @@ -111,7 +159,7 @@ function defineAttestation< // dummy attestation with no proof attached const ANone = defineAttestation({ - type: 'attestation-none', + id: 'none', public: Undefined_, private: Undefined_, verify() { @@ -121,21 +169,26 @@ const ANone = defineAttestation({ // native signature const ASignature = defineAttestation({ - type: 'attestation-signature', + id: 'native-signature', public: PublicKey, // issuer public key private: Signature, // verify the signature verify(issuerPk, signature, type, data) { - let ok = signature.verify(issuerPk, ProvableType.get(type).toFields(data)); + let ok = signature.verify( + issuerPk, + NestedProvable.get(type).toFields(data) + ); assert(ok, 'Invalid signature'); }, }); // TODO recursive proof const AProof = defineAttestation({ - type: 'attestation-proof', - public: Field as ProvablePure, // the verification key hash (TODO: make this a `VerificationKey` when o1js supports it) + id: 'proof', + // TODO include hash of public inputs of the inner proof + // TODO maybe names could be issuer, credential + public: Field, // the verification key hash (TODO: make this a `VerificationKey` when o1js supports it) private: Struct({ vk: VerificationKey, // the verification key proof: Undefined_, // the proof, TODO: make this a `DynamicProof` when o1js supports it, or by refactoring our provable type representation @@ -168,14 +221,23 @@ const Operation = { and, }; +type Constant = { + type: 'constant'; + data: ProvableType; + value: Data; +}; +type Public = { type: 'public'; data: NestedProvablePureFor }; +type Private = { type: 'private'; data: NestedProvableFor }; + type Input = | Attestation - | { type: 'constant'; data: ProvableType; value: Data } - | { type: 'public'; data: ProvablePureType } - | { type: 'private'; data: ProvableType }; + | Constant + | Public + | Private; type Node = - | Input + | { type: 'constant'; data: Data } + | { type: 'root'; input: Record } | { type: 'property'; key: string; inner: Node } | { type: 'equals'; left: Node; right: Node } | { type: 'and'; left: Node; right: Node }; @@ -185,25 +247,99 @@ type OutputNode = { data?: Node; }; -type GetData = T extends Node ? Data : never; +const Node = { + eval: evalNode, + evalType: evalNodeType, + + constant(data: Data): Node { + return { type: 'constant', data }; + }, +}; + +function evalNode(root: object, node: Node): Data { + switch (node.type) { + case 'constant': + return node.data; + case 'root': + return root as any; + case 'property': { + let inner = evalNode(root, node.inner); + assertHasProperty(inner, node.key); + return inner[node.key] as Data; + } + case 'equals': { + let left = evalNode(root, node.left); + let right = evalNode(root, node.right); + let bool = Provable.equal(ProvableType.fromValue(left), left, right); + return bool as Data; + } + case 'and': { + let left = evalNode(root, node.left); + let right = evalNode(root, node.right); + return left.and(right) as Data; + } + } +} + +function evalNodeType( + rootType: NestedProvable, + node: Node +): NestedProvable { + switch (node.type) { + case 'constant': + return ProvableType.fromValue(node.data); + case 'root': + return rootType; + case 'property': { + // TODO would be nice to get inner types of structs more easily + let inner = evalNodeType(rootType, node.inner); + + // case 1: inner is a provable type + if (ProvableType.isProvableType(inner)) { + let innerValue = ProvableType.synthesize(inner); + assertHasProperty(innerValue, node.key); + let value: Data = innerValue[node.key] as any; + return ProvableType.fromValue(value); + } + // case 2: inner is a record of provable types + return inner[node.key] as any; + } + case 'equals': { + return Bool as any; + } + case 'and': { + return Bool as any; + } + } +} + +type GetData = T extends Input ? Data : never; function constant( data: DataType, value: InferProvableType -): Input> { +): Constant> { return { type: 'constant', data, value }; } -function publicParameter( +function publicParameter( data: DataType -): Input> { - return { type: 'public', data }; +): Public> { + return { type: 'public', data: data as any }; } -function privateParameter( +function privateParameter( data: DataType -): Input> { - return { type: 'private', data }; +): Private> { + return { type: 'private', data: data as any }; +} + +// Node constructors + +function root>( + inputs: Inputs +): Node<{ [K in keyof Inputs]: Node> }> { + return { type: 'root', input: inputs }; } function property( @@ -227,34 +363,252 @@ function and(left: Node, right: Node): Node { const isMain = import.meta.filename === process.argv[1]; if (isMain) { const Bytes32 = Bytes(32); - const InputData = Struct({ age: Field, name: Bytes32 }); + const InputData = { age: Field, name: Bytes32 }; const spec = Spec( - [ - Attestation.signature(InputData), - Input.public(Field), - Input.constant(Bytes32, Bytes32.fromString('Alice')), - ], - (data, targetAge, targetName) => ({ + { + signedData: Attestation.signature(InputData), + targetAge: Input.public(Field), + targetName: Input.constant(Bytes32, Bytes32.fromString('Alice')), + }, + ({ signedData, targetAge, targetName }) => ({ assert: Operation.and( - Operation.equals(Operation.property(data, 'age'), targetAge), - Operation.equals(Operation.property(data, 'name'), targetName) + Operation.equals(Operation.property(signedData, 'age'), targetAge), + Operation.equals(Operation.property(signedData, 'name'), targetName) ), - data: Operation.property(data, 'age'), + data: Operation.property(signedData, 'age'), }) ); + console.log(spec.logic); + + // create user inputs + let data = { age: Field(18), name: Bytes32.fromString('Alice') }; + let signedData = createAttestation(InputData, data); + + let userInputs: UserInputs = { + signedData, + targetAge: Field(18), + }; - console.log(spec); + // evaluate the logic at input + let { privateInput, publicInput } = splitUserInputs(spec, userInputs); + let root = recombineDataInputs(spec, publicInput, privateInput); + let assert = Node.eval(root, spec.logic.assert); + let output = Node.eval(root, spec.logic.data); + Provable.log({ publicInput, privateInput, root, assert, output }); + + // public inputs, extracted at the type level + type specPublicInputs = PublicInputs; + + // private inputs, extracted at the type level + type specPrivateInputs = PrivateInputs; + + function createAttestation(type: NestedProvableFor, data: Data) { + let issuer = PrivateKey.randomKeypair(); + let signature = Signature.create( + issuer.privateKey, + NestedProvable.get(type).toFields(data) + ); + return { public: issuer.publicKey, private: signature, data }; + } } -// TODO these types should be in o1js +function publicInputTypes({ + inputs, +}: S): Record { + let result: Record = {}; + + Object.entries(inputs).forEach(([key, input]) => { + if (input.type === 'attestation') { + result[key] = input.public; + } + if (input.type === 'public') { + result[key] = input.data; + } + }); + return result; +} + +function privateInputTypes({ + inputs, +}: S): Record { + let result: Record = {}; + + Object.entries(inputs).forEach(([key, input]) => { + if (input.type === 'attestation') { + result[key] = { private: input.private, data: input.data }; + } + if (input.type === 'private') { + result[key] = input.data; + } + }); + return result; +} -type WithProvable = { provable: A } | A; -type ProvableType = WithProvable>; -type ProvablePureType = WithProvable>; -type ToProvable> = A extends { - provable: infer P; +function publicOutputType(spec: S): ProvablePure { + let root = dataInputTypes(spec); + let outputTypeNested = Node.evalType(root, spec.logic.data); + let outputType = NestedProvable.get(outputTypeNested); + assertPure(outputType); + return outputType; } - ? P - : A; -type InferProvableType = InferProvable>; + +function dataInputTypes({ inputs }: S): NestedProvable { + let result: Record = {}; + Object.entries(inputs).forEach(([key, input]) => { + result[key] = input.data; + }); + return result; +} + +function splitUserInputs( + spec: S, + userInputs: Record +): { + publicInput: PublicInputs; + privateInput: PrivateInputs; +}; +function splitUserInputs( + spec: S, + userInputs: Record +): { publicInput: Record; privateInput: Record } { + let publicInput: Record = {}; + let privateInput: Record = {}; + + Object.entries(spec.inputs).forEach(([key, input]) => { + if (input.type === 'attestation') { + publicInput[key] = userInputs[key].public; + privateInput[key] = { + private: userInputs[key].private, + data: userInputs[key].data, + }; + } + if (input.type === 'public') { + publicInput[key] = userInputs[key]; + } + if (input.type === 'private') { + privateInput[key] = userInputs[key]; + } + if (input.type === 'constant') { + // do nothing + } + }); + return { publicInput, privateInput }; +} + +function verifyAttestations( + spec: S, + publicInputs: Record, + privateInputs: Record +) { + Object.entries(spec.inputs).forEach(([key, input]) => { + if (input.type === 'attestation') { + let publicInput = publicInputs[key]; + let { private: privateInput, data } = privateInputs[key]; + console.log('verifying', key, input.id); + input.verify(publicInput, privateInput, data); + } + }); +} + +function recombineDataInputs( + spec: S, + publicInputs: Record, + privateInputs: Record +): DataInputs; +function recombineDataInputs( + spec: S, + publicInputs: Record, + privateInputs: Record +): Record { + let result: Record = {}; + + Object.entries(spec.inputs).forEach(([key, input]) => { + if (input.type === 'attestation') { + result[key] = privateInputs[key].data; + } + if (input.type === 'public') { + result[key] = publicInputs[key]; + } + if (input.type === 'private') { + result[key] = privateInputs[key]; + } + if (input.type === 'constant') { + result[key] = input.value; + } + }); + return result; +} + +type PublicInputs> = ExcludeFromRecord< + MapToPublic, + never +>; + +type PrivateInputs> = ExcludeFromRecord< + MapToPrivate, + never +>; + +type UserInputs> = ExcludeFromRecord< + MapToUserInput, + never +>; + +type DataInputs> = ExcludeFromRecord< + MapToDataInput, + never +>; + +type MapToPublic> = { + [K in keyof T]: ToPublic; +}; + +type MapToPrivate> = { + [K in keyof T]: ToPrivate; +}; + +type MapToUserInput> = { + [K in keyof T]: ToUserInput; +}; + +type MapToDataInput> = { + [K in keyof T]: ToDataInput; +}; + +type ToPublic = T extends Attestation< + string, + infer Public, + any, + any +> + ? Public + : T extends Public + ? Data + : never; + +type ToPrivate = T extends Attestation< + string, + any, + infer Private, + infer Data +> + ? { private: Private; data: Data } + : T extends Private + ? Data + : never; + +type ToUserInput = T extends Attestation< + string, + infer Public, + infer Private, + infer Data +> + ? { public: Public; private: Private; data: Data } + : T extends Public + ? Data + : T extends Private + ? Data + : never; + +type ToDataInput = T extends Input ? Data : never; diff --git a/src/program.ts b/src/program.ts index 5458ee2..03d42e5 100644 --- a/src/program.ts +++ b/src/program.ts @@ -1,70 +1,131 @@ -import { Proof, Field } from 'o1js'; import { - type GetData, + Proof, + Field, + PublicKey, + PrivateKey, + Signature, + VerificationKey, + ZkProgram, + Provable, +} from 'o1js'; +import { Attestation, Input, + Node, Operation, + privateInputTypes, + publicInputTypes, + publicOutputType, + recombineDataInputs, Spec, + splitUserInputs, + verifyAttestations, + type PublicInputs, + type UserInputs, } from './program-config.ts'; -import { Tuple } from './types.ts'; +import { NestedProvable, type NestedProvableFor } from './nested.ts'; export { createProgram }; -type TODO = any; - -type Program> = { - compile(): Promise<{ verificationKey: { data: string; hash: Field } }>; +type Program> = { + compile(): Promise; - run(input: { [K in keyof Inputs]: GetData }): Promise< - Proof - >; + run(input: UserInputs): Promise, Data>>; }; function createProgram( spec: S ): Program, S['inputs']> { - throw Error('Not implemented'); + // 1. split spec inputs into public and private inputs + let PublicInput = NestedProvable.get(publicInputTypes(spec)); + let PublicOutput = publicOutputType(spec); + let PrivateInput = NestedProvable.get(privateInputTypes(spec)); + + let program = ZkProgram({ + name: `todo`, // we should create a name deterministically derived from the spec, e.g. `credential-${hash(spec)}` + publicInput: PublicInput, + publicOutput: PublicOutput, + methods: { + run: { + privateInputs: [PrivateInput], + method(publicInput, privateInput) { + verifyAttestations(spec, publicInput, privateInput); + + let root = recombineDataInputs(spec, publicInput, privateInput); + let assertion = Node.eval(root, spec.logic.assert); + let output = Node.eval(root, spec.logic.data); + assertion.assertTrue('Program assertion failed!'); + return output; + }, + }, + }, + }); + + return { + async compile() { + const result = await program.compile(); + return result.verificationKey; + }, + async run(input) { + let { publicInput, privateInput } = splitUserInputs(spec, input); + let result = await program.run(publicInput, privateInput); + return result as any; + }, + }; } // inline test const isMain = import.meta.filename === process.argv[1]; if (isMain) { - let { Bytes, Struct } = await import('o1js'); + let { Bytes } = await import('o1js'); const Bytes32 = Bytes(32); - const InputData = Struct({ age: Field, name: Bytes32 }); + const InputData = { age: Field, name: Bytes32 }; + // TODO always include owner pk and verify signature on it const spec = Spec( - [ - Attestation.signature(InputData), - Input.public(Field), - Input.constant(Bytes32, Bytes32.fromString('Alice')), - ], - (data, targetAge, targetName) => ({ + { + signedData: Attestation.signature(InputData), + targetAge: Input.public(Field), + targetName: Input.constant(Bytes32, Bytes32.fromString('Alice')), + }, + ({ signedData, targetAge, targetName }) => ({ assert: Operation.and( - Operation.equals(Operation.property(data, 'age'), targetAge), - Operation.equals(Operation.property(data, 'name'), targetName) + Operation.equals(Operation.property(signedData, 'age'), targetAge), + Operation.equals(Operation.property(signedData, 'name'), targetName) ), - data: Operation.property(data, 'age'), + data: Operation.property(signedData, 'age'), }) ); + function createAttestation(type: NestedProvableFor, data: Data) { + let issuer = PrivateKey.randomKeypair(); + let signature = Signature.create( + issuer.privateKey, + NestedProvable.get(type).toFields(data) + ); + return { public: issuer.publicKey, private: signature, data }; + } + + let data = { age: Field(18), name: Bytes32.fromString('Alice') }; + let signedData = createAttestation(InputData, data); + let program = createProgram(spec); + await program.compile(); - async function notExecuted() { - // input types are inferred from spec - // TODO leverage `From<>` type to pass in inputs directly as numbers / strings etc - let result = await program.run([ - { age: Field(42), name: Bytes32.fromString('Alice') }, - Field(18), - Bytes32.fromString('Alice'), - ]); - - // output types are inferred from spec - // TODO infer result.publicInput - result.publicOutput satisfies Field; - } + // input types are inferred from spec + // TODO leverage `From<>` type to pass in inputs directly as numbers / strings etc + let proof = await program.run({ signedData, targetAge: Field(18) }); + + Provable.log({ + publicInput: proof.publicInput, + publicOutput: proof.publicOutput, + }); + + // proof types are inferred from spec + proof.publicInput satisfies { signedData: PublicKey; targetAge: Field }; + proof.publicOutput satisfies Field; } // helper diff --git a/src/types.ts b/src/types.ts index 8c882b2..fc6df26 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,117 +1,36 @@ -export type { Credential, Tuple }; - -type Tuple = [T, ...T[]] | []; - -// ****** EXAMPLE WALLET PROVIDER ATTESTATION API ****** - -type MinaWallet = { - attestation: AttestationAPI; -}; - -type AttestationAPI = { - initialize(config: AttestationConfig): Promise; -}; - -type AttestationConfig = { - apiKey: string; - endpoint: string; -}; - -// ****** CREDENTIAL CREATION API ****** - -type CredentialAttestationAPI = { - create(params: LocalCredentialParams): Promise; -}; - -type LocalCredentialParams = { - claims: { [key: string]: any }; // Claims about the subject -}; - -type CredentialResponse = { - credentialId: string; - credential: string; // Encoded credential - nullifier?: string; // Unique identifier for the credential - nullifierKey?: string; // Key associated with the nullifier - nullifierProof?: string; // Proof that the nullifierKey was derived as expected - expiration?: number; // Expiration time if set -}; - -// ****** CREDENTIALPROGRAM.CREATE API ****** - -type CredentialProgramInput = { - [key: string]: 'number' | 'string' | 'boolean'; -}; - -type CredentialProgramOutput = { - [key: string]: 'boolean'; -}; - -type CredentialProgramConfig = { - name: string; - input: CredentialProgramInput; - output: CredentialProgramOutput; - logic: OperationNode; +export type { + Tuple, + Flatten, + FilterTuple, + ExcludeFromTuple, + ExcludeFromRecord, }; -type OperationNode = { - operation: string; - inputs?: (OperationNode | any)[]; // The inputs can be either another operation node or a static value - [key: string]: any; // Allow for additional properties specific to the operation -}; - -type CredentialProgram = { - create(config: CredentialProgramConfig): CredentialVerificationInstance; -}; - -type CredentialVerificationInstance = { - name: string; - input: CredentialProgramInput; - output: CredentialProgramOutput; - logic: OperationNode; -}; - -// TODO: decide credentialProgram.Operations API - -// ****** CREDENTIAL.CREATE API ****** - -type CredentialAPI = { - create(claims: string): Credential; -}; - -type Credential = { - claims: { [key: string]: any }; - issuerPublicKey: string; - signature: string; -}; - -// ****** CREDENTIAL.PROVE API ****** - -type ProofAPI = { - prove( - claimKey: string, - publicParams: object, - credentialVerificationInstance: CredentialVerificationInstance - ): Promise; -}; - -type ProofResponse = { - proof: object; - proofId: string; - valid: boolean; - publicParams: object; -}; - -// ****** PROOF COMPOSITION API ****** - -type ProofAttestationAPI = { - composeAttestation(params: ComposeParams): Promise; -}; - -type ComposeParams = { - attestationIds: string[]; // List of attestation IDs to be composed -}; +type Tuple = [T, ...T[]] | []; -type ComposeResponse = { - compositeAttestationId: string; - compositeProof: string; // Composite cryptographic proof +type Flatten = T extends [] + ? [] + : T extends [infer T0] + ? [...Flatten] + : T extends [infer T0, ...infer Ts] + ? [...Flatten, ...Flatten] + : [T]; + +type ExcludeFromTuple = T extends [ + infer F, + ...infer R +] + ? [F] extends [E] + ? ExcludeFromTuple + : [F, ...ExcludeFromTuple] + : []; + +type FilterTuple = T extends [infer F, ...infer R] + ? [F] extends [E] + ? [F, ...FilterTuple] + : FilterTuple + : []; + +type ExcludeFromRecord = { + [P in keyof T as T[P] extends E ? never : P]: T[P]; }; diff --git a/src/util.ts b/src/util.ts index c320a3f..5b382b9 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,7 +1,29 @@ -export { assert }; +export { assert, assertHasProperty, hasProperty }; function assert(condition: boolean, message?: string): asserts condition { if (!condition) { throw Error(message ?? 'Assertion failed'); } } + +function assertHasProperty( + obj: unknown, + key: K, + message?: string +): asserts obj is Record { + assert( + ((typeof obj === 'object' && obj !== null) || typeof obj === 'function') && + key in obj, + message ?? `Expected object to have property ${key}` + ); +} + +function hasProperty( + obj: unknown, + key: K +): obj is Record { + return ( + ((typeof obj === 'object' && obj !== null) || typeof obj === 'function') && + key in obj + ); +}