UNPKG

mina-attestations

Version:
520 lines (471 loc) 14.9 kB
import { Bool, Field, Provable, PublicKey } from 'o1js'; import { ProvableType } from './o1js-missing.ts'; import { assert, assertHasProperty } from './util.ts'; import { NestedProvable } from './nested.ts'; import { DynamicArray } from './dynamic/dynamic-array.ts'; import { DynamicRecord, extractProperty } from './dynamic/dynamic-record.ts'; import { hashDynamicWithPrefix } from './dynamic/dynamic-hash.ts'; import type { Input, RootValue, RootType } from './program-spec.ts'; import type { CredentialSpec, CredentialType } from './credential.ts'; import type { NativeWitness } from './credential-native.ts'; import { Imported, type ImportedWitness } from './credential-imported.ts'; import { Numeric, type NumericMaximum, numericMaximumType, } from './dynamic/gadgets-numeric.ts'; export { Node, Operation }; export { type CredentialNode, type InputToNode, root }; const Operation = { owner: { type: 'owner' } as Node<PublicKey>, constant<Data>(data: Data): Node<Data> { return { type: 'constant', data }; }, issuer, issuerPublicKey, verificationKeyHash, publicInput, property, record, equals, equalsOneOf, lessThan, lessThanEq, add, sub, mul, div, and, or, not, hash, hashWithPrefix, ifThenElse, compute, }; type CredentialNode<Data = any, Witness = WitnessAny> = { type: 'credential'; credentialKey: string; credentialType: CredentialType; // phantom data data?: Data; witness?: Witness; }; type Node<Data = any> = | { type: 'constant'; data: Data } | { type: 'root'; input: Record<string, Input> } // operations to extract credential information | { type: 'owner' } | CredentialNode<Data> | { type: 'issuer'; credentialKey: string } | { type: 'issuerPublicKey'; credentialKey: string } | { type: 'verificationKeyHash'; credentialKey: string } | { type: 'publicInput'; credentialKey: string } // | { type: 'property'; key: string; inner: Node } | { type: 'record'; data: Record<string, Node> } | { type: 'equals'; left: Node; right: Node } | { type: 'equalsOneOf'; input: Node; options: Node[] | Node<any[]> | Node<DynamicArray>; } | { type: 'lessThan'; left: Node<Numeric>; right: Node<Numeric> } | { type: 'lessThanEq'; left: Node<Numeric>; right: Node<Numeric> } | { type: 'add'; left: Node<Numeric>; right: Node<Numeric> } | { type: 'sub'; left: Node<Numeric>; right: Node<Numeric> } | { type: 'mul'; left: Node<Numeric>; right: Node<Numeric> } | { type: 'div'; left: Node<Numeric>; right: Node<Numeric> } | { type: 'and'; inputs: Node<Bool>[] } | { type: 'or'; left: Node<Bool>; right: Node<Bool> } | { type: 'not'; inner: Node<Bool> } | { type: 'hash'; inputs: Node[]; prefix?: string } | { type: 'ifThenElse'; condition: Node<Bool>; thenNode: Node; elseNode: Node; } | { type: 'compute'; inputs: readonly Node[]; computation: (...inputs: any[]) => any; outputType: ProvableType; }; type GetData<T extends Input> = T extends Input<infer Data> ? Data : never; type InputToNode<T extends Input> = T extends CredentialSpec< infer Witness, infer Data > ? CredentialNode<Data, Witness> : Node<GetData<T>>; const Node = { eval: evalNode, evalType: evalNodeType, }; function evalNode<Data>(root: RootValue, node: Node<Data>): Data { switch (node.type) { case 'constant': return node.data; case 'root': return root as Data; // credential operations case 'owner': return root.owner as Data; case 'credential': return assertCredential(root, node).data; case 'issuer': return assertCredential(root, node).issuer as Data; case 'issuerPublicKey': return assertNativeCredential(root, node).witness.issuer as Data; case 'verificationKeyHash': return assertImportedCredential(root, node).witness.vk.hash as Data; case 'publicInput': return assertImportedCredential(root, node).witness.proof .publicInput as Data; case 'property': { let inner = evalNode<unknown>(root, node.inner); return extractProperty(inner, node.key) as Data; } case 'record': { let result: Record<string, any> = {}; for (let key in node.data) { result[key] = evalNode(root, node.data[key]!); } return result as any; } 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 'equalsOneOf': { let input = evalNode(root, node.input); let type = NestedProvable.get(NestedProvable.fromValue(input)); let options: any[] | DynamicArray; if (Array.isArray(node.options)) { options = node.options.map((i) => evalNode(root, i)); } else { options = evalNode<any[] | DynamicArray>(root, node.options); } if (options instanceof DynamicArray.Base) { let bool = options.reduce(Bool, Bool(false), (acc, o) => acc.or(Provable.equal(type, input, o)) ); return bool as Data; } let bools = options.map((o) => Provable.equal(type, input, o)); return bools.reduce(Bool.or) as Data; } case 'lessThan': { let left = evalNode(root, node.left); let right = evalNode(root, node.right); return Numeric.lessThan(left, right) as Data; } case 'lessThanEq': { let left = evalNode(root, node.left); let right = evalNode(root, node.right); return Numeric.lessThanOrEqual(left, right) as Data; } case 'add': { let left = evalNode(root, node.left); let right = evalNode(root, node.right); return Numeric.add(left, right) as Data; } case 'sub': { let left = evalNode(root, node.left); let right = evalNode(root, node.right); return Numeric.subtract(left, right) as Data; } case 'mul': { let left = evalNode(root, node.left); let right = evalNode(root, node.right); return Numeric.multiply(left, right) as Data; } case 'div': { let left = evalNode(root, node.left); let right = evalNode(root, node.right); return Numeric.divide(left, right) as Data; } case 'and': { let inputs = node.inputs.map((i) => evalNode(root, i)); return inputs.reduce(Bool.and) as Data; } case 'or': { let left = evalNode(root, node.left); let right = evalNode(root, node.right); return left.or(right) as Data; } case 'not': { let inner = evalNode(root, node.inner); return inner.not() as Data; } case 'hash': { let inputs = node.inputs.map((i) => evalNode(root, i)); return hashDynamicWithPrefix(node.prefix, ...inputs) as Data; } case 'ifThenElse': { let condition = evalNode(root, node.condition); let thenNode = evalNode(root, node.thenNode); let elseNode = evalNode(root, node.elseNode); let result = Provable.if(condition, thenNode, elseNode); return result as Data; } case 'compute': { const computationInputs = node.inputs.map((input) => evalNode(root, input) ); return node.computation(...computationInputs); } } } function evalNodeType(rootType: RootType, node: Node): NestedProvable { switch (node.type) { case 'constant': return ProvableType.fromValue(node.data); case 'root': return rootType as NestedProvable; // credential operations case 'owner': return PublicKey; case 'credential': return assertCredentialType(rootType, node).data; case 'issuer': return Field; case 'issuerPublicKey': return PublicKey; case 'verificationKeyHash': return Field; case 'publicInput': { let spec = assertCredentialType(rootType, node); return Imported.publicInputType(spec); } 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 = innerValue[node.key]; return ProvableType.fromValue(value); } // case 2: inner is a record of provable types return inner[node.key] as any; } case 'equals': case 'equalsOneOf': case 'lessThan': case 'lessThanEq': case 'and': case 'or': case 'not': return Bool; case 'hash': return Field; case 'add': case 'sub': case 'mul': case 'div': let leftType = evalNodeType(rootType, node.left); let rightType = evalNodeType(rootType, node.right); return numericMaximumType(leftType, rightType); case 'ifThenElse': let thenType = evalNodeType(rootType, node.thenNode); return thenType; // the two must be the same case 'record': { let result: Record<string, NestedProvable> = {}; for (let key in node.data) { result[key] = evalNodeType(rootType, node.data[key]!); } return result; } case 'compute': { return node.outputType; } } } // Node constructors function root<Inputs extends Record<string, Input>>( inputs: Inputs ): Node<{ [K in keyof Inputs]: Node<GetData<Inputs[K]>> }> { return { type: 'root', input: inputs }; } function property<K extends string, Data extends { [key in K]: any }>( node: Node<Data | DynamicRecord<Data>>, key: K ): Node<Data[K]> { return { type: 'property', key, inner: node as Node<any> }; } function record<Nodes extends Record<string, Node>>( nodes: Nodes ): Node<{ [K in keyof Nodes]: Nodes[K] extends Node<infer Data> ? Data : never; }> { return { type: 'record', data: nodes }; } function equals<Data>(left: Node<Data>, right: Node<Data>): Node<Bool> { return { type: 'equals', left, right }; } function equalsOneOf<Data>( input: Node<Data>, options: Node<Data>[] | Node<Data[]> | Node<DynamicArray<Data>> ): Node<Bool> { return { type: 'equalsOneOf', input, options }; } function lessThan<Left extends Numeric, Right extends Numeric>( left: Node<Left>, right: Node<Right> ): Node<Bool> { return { type: 'lessThan', left, right }; } function lessThanEq<Left extends Numeric, Right extends Numeric>( left: Node<Left>, right: Node<Right> ): Node<Bool> { return { type: 'lessThanEq', left, right }; } function add<Left extends Numeric, Right extends Numeric>( left: Node<Left>, right: Node<Right> ): Node<NumericMaximum<Left | Right>> { return { type: 'add', left, right }; } function sub<Left extends Numeric, Right extends Numeric>( left: Node<Left>, right: Node<Right> ): Node<NumericMaximum<Left | Right>> { return { type: 'sub', left, right }; } function mul<Left extends Numeric, Right extends Numeric>( left: Node<Left>, right: Node<Right> ): Node<NumericMaximum<Left | Right>> { return { type: 'mul', left, right }; } function div<Left extends Numeric, Right extends Numeric>( left: Node<Left>, right: Node<Right> ): Node<NumericMaximum<Left | Right>> { return { type: 'div', left, right }; } function and(...inputs: Node<Bool>[]): Node<Bool> { return { type: 'and', inputs }; } function or(left: Node<Bool>, right: Node<Bool>): Node<Bool> { return { type: 'or', left, right }; } function not(inner: Node<Bool>): Node<Bool> { return { type: 'not', inner }; } function hash(...inputs: Node[]): Node<Field> { return { type: 'hash', inputs }; } function hashWithPrefix(prefix: string, ...inputs: Node[]): Node<Field> { return { type: 'hash', inputs, prefix }; } function ifThenElse<Data>( condition: Node<Bool>, thenNode: Node<Data>, elseNode: Node<Data> ): Node<Data> { return { type: 'ifThenElse', condition, thenNode, elseNode }; } function compute<Inputs extends readonly Node[], Output>( inputs: [...Inputs], outputType: ProvableType<Output>, computation: ( ...args: { [K in keyof Inputs]: Inputs[K] extends Node<infer T> ? T : never; } ) => Output ): Node<Output> { return { type: 'compute', inputs: inputs, computation: computation as (inputs: any[]) => Output, outputType, }; } // credential operations function issuer(credential: CredentialNode): Node<Field> { return { type: 'issuer', credentialKey: credential.credentialKey }; } function issuerPublicKey({ credentialType, credentialKey, }: CredentialNode<any, NativeWitness>): Node<PublicKey> { assert( credentialType === 'native', '`issuerPublicKey` is only available on signed credentials' ); return { type: 'issuerPublicKey', credentialKey }; } function verificationKeyHash({ credentialType, credentialKey, }: CredentialNode<any, ImportedWitness>): Node<Field> { assert( credentialType === 'imported', '`verificationKeyHash` is only available on imported credentials' ); return { type: 'verificationKeyHash', credentialKey }; } function publicInput<Input>({ credentialType, credentialKey, }: CredentialNode<any, ImportedWitness<Input>>): Node<Input> { assert( credentialType === 'imported', '`publicInput` is only available on imported credentials' ); return { type: 'publicInput', credentialKey }; } type WitnessAny = NativeWitness | ImportedWitness | undefined; type CredentialOutput<Data = any, Witness extends WitnessAny = WitnessAny> = { data: Data; issuer: Field; witness: Witness; }; type CredentialOutputNative<Data = any> = CredentialOutput<Data, NativeWitness>; type CredentialOutputImported<Data = any> = CredentialOutput< Data, ImportedWitness >; type CredentialNodeType = | 'credential' | 'issuer' | 'issuerPublicKey' | 'verificationKeyHash' | 'publicInput'; function assertCredential<Data>( root: RootValue, credential: Node<Data> & { type: CredentialNodeType } ) { assertHasProperty(root, credential.credentialKey); return root[credential.credentialKey] as CredentialOutput<Data>; } function assertCredentialType<Data>( rootType: Record<string, NestedProvable | CredentialSpec>, credential: Node<Data> & { type: CredentialNodeType } ) { assertHasProperty(rootType, credential.credentialKey); return rootType[credential.credentialKey] as CredentialSpec; } function assertNativeCredential<Data>( root: RootValue, credential: Node<Data> & { type: CredentialNodeType } ) { let cred = assertCredential(root, credential); assert(cred.witness?.type === 'native'); return cred as CredentialOutputNative<Data>; } function assertImportedCredential<Data>( root: RootValue, credential: Node<Data> & { type: CredentialNodeType } ) { let cred = assertCredential(root, credential); assert(cred.witness?.type === 'imported'); return cred as CredentialOutputImported<Data>; }