UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

556 lines (505 loc) 18.2 kB
import { AccountUpdate, AccountUpdateCommitment, AccountUpdateTree, AccountUpdateTreeDescription, ContextFreeAccountUpdateDescription, ContextFreeAccountUpdate, DynamicProvable, } from '../account-update.js'; import { AccountUpdateAuthorizationKind } from '../authorization.js'; import { Account, AccountId } from '../account.js'; import { mapObject, ProvableTuple, ProvableTupleInstances } from '../core.js'; import { getCallerFrame } from '../errors.js'; import { StateDefinition, StateMask, StateLayout, StateReader, StateValues } from '../state.js'; import { checkAndApplyAccountUpdate } from '../zkapp-logic.js'; import { ZkappCommandContext } from '../transaction.js'; import { Cache } from '../../../proof-system/cache.js'; import { Method as ZkProgramMethod, Proof, ZkProgram } from '../../../proof-system/zkprogram.js'; import { Bool } from '../../../provable/bool.js'; import { Field } from '../../../provable/field.js'; import { UInt32, UInt64 } from '../../../provable/int.js'; import { Provable } from '../../../provable/provable.js'; import { PublicKey } from '../../../provable/crypto/signature.js'; import { Unconstrained } from '../../../provable/types/unconstrained.js'; import { VerificationKey } from '../../../proof-system/verification-key.js'; import { ZkappConstants } from '../../v1/constants.js'; import { MinaAmount } from '../currency.js'; export { MinaProgramEnv, MinaProgramMethodReturn, MinaProgramMethodImpl, MinaProgramMethodProver, MinaProgramDescription, MinaProgram, }; class MinaProgramEnv<State extends StateLayout> { private expectedPreconditions: Unconstrained<{ balance?: MinaAmount; nonce?: UInt32; receiptChainHash?: Field; delegate?: PublicKey; state: StateMask<State>; actionState?: Field; isProven?: Bool; }>; constructor( public State: StateDefinition<State>, private account: Unconstrained<Account<State>>, // TODO: we can actually remove this since the verification key will always be set on an // account before we call a method on it private verificationKey: Unconstrained<VerificationKey> ) { this.expectedPreconditions = Unconstrained.from({ state: StateMask.create(State), }); } get accountId(): AccountId { return Provable.witness(AccountId, () => this.account.get().accountId); } get accountVerificationKeyHash(): Field { return Provable.witness(Field, () => this.account.get().zkapp.verificationKey.hash); } get programVerificationKey(): VerificationKey { return Provable.witness(VerificationKey, () => this.verificationKey.get()); } get balance(): MinaAmount { return Provable.witness(UInt64, () => { const balance = this.account.get().balance; this.expectedPreconditions.get().balance = balance; return balance; }); } get nonce(): UInt32 { return Provable.witness(UInt32, () => { const nonce = this.account.get().nonce; this.expectedPreconditions.get().nonce = nonce; return nonce; }); } get receiptChainHash(): Field { return Provable.witness(Field, () => { const receiptChainHash = this.account.get().receiptChainHash; this.expectedPreconditions.get().receiptChainHash = receiptChainHash; return receiptChainHash; }); } get delegate(): PublicKey { return Provable.witness(PublicKey, () => { const delegate = this.account.get().delegate ?? this.account.get().accountId.publicKey; this.expectedPreconditions.get().delegate = delegate; return delegate; }); } get state(): StateReader<State> { const accountState = Provable.witness(Unconstrained<StateValues<State>>, () => { return Unconstrained.from(this.account.get().zkapp.state); }); const accountStateMask = Provable.witness(Unconstrained<StateMask<State>>, () => { return Unconstrained.from(this.expectedPreconditions.get().state); }); return StateReader.create(this.State, accountState, accountStateMask); } // only returns the most recent action state for an account get actionState(): Field { return Provable.witness(Field, () => { const actionState = this.account.get().zkapp.actionState[ZkappConstants.ACCOUNT_ACTION_STATE_BUFFER_SIZE - 1]; this.expectedPreconditions.get().actionState = actionState; return actionState; }); } get isProven(): Bool { return Provable.witness(Bool, () => { const isProven = this.account.get().zkapp.isProven; this.expectedPreconditions.get().isProven = isProven; return isProven; }); } static sizeInFields(): number { return 0; } static toFields<State extends StateLayout>(_x: MinaProgramEnv<State>): Field[] { return []; } static toAuxiliary<State extends StateLayout>(x?: MinaProgramEnv<State>): any[] { // if(x === undefined) throw new Error('invalid call to MinaProgram#toAuxiliary'); // eww... how do I handle the undefined MinaProgramEnv situation? return [x?.account, x?.verificationKey]; } static fromFields(_fields: Field[], aux: any[]): MinaProgramEnv<'GenericState'> { return new MinaProgramEnv('GenericState', aux[0], aux[1]); } static toValue<State extends StateLayout>(x: MinaProgramEnv<State>): MinaProgramEnv<State> { return x; } static fromValue<State extends StateLayout>(x: MinaProgramEnv<State>): MinaProgramEnv<State> { return x; } static check<State extends StateLayout>(_x: MinaProgramEnv<State>) { // TODO NOW //throw new Error('TODO'); } } type MinaProgramMethodReturn< State extends StateLayout = 'GenericState', Event = Field[], Action = Field[] > = | Omit< AccountUpdateTreeDescription< ContextFreeAccountUpdateDescription<State, Event, Action>, AccountUpdate >, 'authorizationKind' > | ContextFreeAccountUpdate<State, Event, Action>; type MinaProgramMethodImpl< State extends StateLayout, Event, Action, PrivateInputs extends ProvableTuple > = { privateInputs: PrivateInputs; method( env: MinaProgramEnv<State>, ...args: ProvableTupleInstances<PrivateInputs> ): Promise<MinaProgramMethodReturn<State, Event, Action>>; }; // TODO: return the tree, not the proof and the single update type MinaProgramMethodProver< State extends StateLayout, Event, Action, PrivateInputs extends ProvableTuple > = ( env: ZkappCommandContext, accountId: AccountId, ...args: ProvableTupleInstances<PrivateInputs> ) => Promise<AccountUpdateTree<AccountUpdate<State, Event, Action>, AccountUpdate>>; interface MinaProgramDescription< State extends StateLayout, Event, Action, MethodPrivateInputs extends { [key: string]: ProvableTuple } > { name: string; State: StateDefinition<State>; Event: DynamicProvable<Event>; Action: DynamicProvable<Action>; methods: { [key in keyof MethodPrivateInputs]: MinaProgramMethodImpl< State, Event, Action, MethodPrivateInputs[key] >; }; } // TODO: use ZkProgram types to help construct this type MinaProgram< State extends StateLayout, Event, Action, MethodPrivateInputs extends { [key: string]: ProvableTuple } > = { name: string; State: StateDefinition<State>; Event: DynamicProvable<Event>; Action: DynamicProvable<Action>; compile(options?: { cache?: Cache; forceRecompile?: boolean }): Promise<{ verificationKey: { data: string; hash: Field; }; }>; } & { [key in keyof MethodPrivateInputs]: MinaProgramMethodProver< State, Event, Action, MethodPrivateInputs[key] >; }; // TODO really need to fix the types here... function zkProgramMethod< State extends StateLayout, Event, Action, PrivateInputs extends ProvableTuple >( State: StateDefinition<State>, Event: DynamicProvable<Event>, Action: DynamicProvable<Action>, impl: MinaProgramMethodImpl<State, Event, Action, PrivateInputs> ): ZkProgramMethod< undefined, AccountUpdateCommitment, { privateInputs: [Provable<MinaProgramEnv<State>>, ...PrivateInputs]; auxiliaryOutput: typeof AccountUpdateTree<AccountUpdate>; } > { return { privateInputs: [MinaProgramEnv, ...impl.privateInputs], auxiliaryOutput: AccountUpdateTree, // async method(env: MinaProgramEnv<State>, ...inputs: ProvableTupleInstances<PrivateInputs>) { async method( ...[env, ...inputs]: [MinaProgramEnv<State>, ...ProvableTupleInstances<PrivateInputs>] ) { const describedUpdate = await impl.method(env, ...inputs); let describedUpdate2; if (describedUpdate instanceof ContextFreeAccountUpdate) { // TODO: is it ok that we allow signature and proof as an option here, but don't let the description return such an authorization kind? if (!describedUpdate.authorizationKind.isProved.toBoolean()) { throw new Error('TODO: error message'); } describedUpdate2 = describedUpdate; } else { describedUpdate2 = { ...describedUpdate, authorizationKind: AccountUpdateAuthorizationKind.Proof(), }; } const callData = /* TODO */ new Field(0); const updateTree = AccountUpdateTree.from( { ...describedUpdate2, accountId: env.accountId, // TODO: take the verification key from the account state after the virtual update application verificationKeyHash: env.programVerificationKey.hash, callData, }, // TODO: return the specialized version... (descr) => new AccountUpdate(State, Event, Action, descr) ); // const freeUpdate = ContextFreeAccountUpdate.from(State, Event, Action, describedUpdate2); // const update = new AccountUpdate(State, Event, Action, { // accountId: env.accountId, // verificationKeyHash: env.verificationKeyHash, // callData, // update: freeUpdate, // }) if (Provable.inProver()) { // env.checkAndApplyUpdateAsProver(update); } // TODO: return update as auxiliary output return { publicOutput: updateTree.rootAccountUpdate.commit('testnet' /* TODO */), auxiliaryOutput: AccountUpdateTree.mapRoot(updateTree, (accountUpdate) => accountUpdate.toGeneric() ), }; }, } as unknown as ZkProgramMethod< undefined, AccountUpdateCommitment, { privateInputs: [Provable<MinaProgramEnv<State>>, ...PrivateInputs]; auxiliaryOutput: typeof AccountUpdateTree<AccountUpdate>; } >; } function proverMethod< State extends StateLayout, Event, Action, PrivateInputs extends ProvableTuple >( State: StateDefinition<State>, Event: DynamicProvable<Event>, Action: DynamicProvable<Action>, getVerificationKey: () => VerificationKey, rawProver: ( env: MinaProgramEnv<State>, ...inputs: ProvableTupleInstances<PrivateInputs> ) => Promise<{ proof: Proof<undefined, AccountUpdateCommitment>; auxiliaryOutput: AccountUpdateTree<AccountUpdate>; }>, _impl: MinaProgramMethodImpl<State, Event, Action, PrivateInputs> ): MinaProgramMethodProver<State, Event, Action, PrivateInputs> { // TODO HORRIBLE HACK: // In order to circumvent the lack of support for nested program calls, some hard assumptions are // made within this function which will only work if certain rules are followed when prover // methods are invoked externally. // // We perform shallow evaluation on the roots of account update trees returned by method // invocations. This requires that all child updates were manually applied before invoking the // method call. Importantly, with this restriction, methods cannot actually generate new // children, the children must be passed in as private inputs and constrained accordingly. // Unproven update arguments which are not at the root of the tree returned by a method must be // manually applied to the ledger in the correct order. return async ( ctx: ZkappCommandContext, accountId: AccountId, ...inputs: ProvableTupleInstances<PrivateInputs> ) => { const callSite = getCallerFrame(); const verificationKey = getVerificationKey(); const genericAccount = ctx.ledger.getAccount(accountId) ?? Account.empty(accountId); // TODO: This conversion is safe only under the assumption that the account is new or the // verification key matches the current program's verification key. Assert this is true, // or throw an error. const account: Account<State> = Account.fromGeneric(genericAccount, State); const env = new MinaProgramEnv( account.State, Unconstrained.from(account), Unconstrained.from(verificationKey) ); const { proof, auxiliaryOutput: genericAccountUpdateTree } = await rawProver(env, ...inputs); genericAccountUpdateTree.rootAccountUpdate.proof = proof; // TODO: We currently throw an error here if there are any children, until we solve the // problems around account update tracing and not adding duplicate child updates // to the root (when calling this prover method). if (genericAccountUpdateTree.children.length !== 0) throw new Error('TODO: support nested account updates'); // TODO HACK: Currently, the rawProver is only able to return the generic state representation, // so we must convert it again for the return value. const accountUpdateTree = AccountUpdateTree.mapRoot(genericAccountUpdateTree, (accountUpdate) => AccountUpdate.fromGeneric(accountUpdate, State, Event, Action) ); // apply only the root update and not the children (see above for details) const applied = checkAndApplyAccountUpdate( ctx.chain, account, accountUpdateTree.rootAccountUpdate, ctx.feeExcessState ); let errors: Error[]; switch (applied.status) { case 'Applied': ctx.ledger.setAccount(applied.updatedAccount.toGeneric()); ctx.feeExcessState = applied.updatedFeeExcessState; errors = []; break; case 'Failed': errors = applied.errors; break; } const trace = { accountId, callSite, errors, // TODO (for now, we throw an error above if there are children) childTraces: [], }; ctx.unsafeAddWithoutApplying(genericAccountUpdateTree, trace); // TODO: do we need to clone the accountUpdate here so that we have fresh variables? return accountUpdateTree; }; } function MinaProgram< State extends StateLayout, Event, Action, MethodPrivateInputs extends { [key: string]: ProvableTuple } >( descr: MinaProgramDescription<State, Event, Action, MethodPrivateInputs> ): MinaProgram<State, Event, Action, MethodPrivateInputs> { const programMethods = mapObject< { [key in keyof MethodPrivateInputs]: MinaProgramMethodImpl< State, Event, Action, MethodPrivateInputs[key] >; }, { [key in keyof MethodPrivateInputs]: ZkProgramMethod< undefined, AccountUpdateCommitment, { privateInputs: [Provable<MinaProgramEnv<State>>, ...MethodPrivateInputs[key]]; auxiliaryOutput: typeof AccountUpdateTree<AccountUpdate>; } >; } >( descr.methods, <Key extends keyof MethodPrivateInputs>( key: Key ): ZkProgramMethod< undefined, AccountUpdateCommitment, { privateInputs: [Provable<MinaProgramEnv<State>>, ...MethodPrivateInputs[Key]]; auxiliaryOutput: typeof AccountUpdateTree<AccountUpdate>; } > => zkProgramMethod(descr.State, descr.Event, descr.Action, descr.methods[key]) ); const Program = ZkProgram< { publicInput: undefined; publicOutput: typeof AccountUpdateCommitment; methods: { [key in keyof MethodPrivateInputs]: { privateInputs: [Provable<MinaProgramEnv<State>>, ...MethodPrivateInputs[key]]; auxiliaryOutput: typeof AccountUpdateTree; }; }; }, { [key in keyof MethodPrivateInputs]: { // method(...privateInputs: [MinaProgramEnv<State>, ...({ [I in keyof MethodPrivateInputs[key]]: InferProvable<MethodPrivateInputs[key][I]>} & any[]) ]): Promise<{publicOutput: AccountUpdateCommitment, auxiliaryOutput: AccountUpdate}> method(...privateInputs: any[]): Promise<any>; }; // [key in keyof MethodPrivateInputs]: ZkProgramMethod< // any, // any, // any // // undefined, // // typeof AccountUpdateCommitment, // // { // // privateInputs: [Provable<MinaProgramEnv<State>>, ...MethodPrivateInputs[key]], // // auxiliaryOutput: typeof AccountUpdate // // } // > } >({ name: descr.name, publicInput: undefined, publicOutput: AccountUpdateCommitment, methods: programMethods as any /* TODO */, }); // TODO: proper verification key caching let verificationKey: VerificationKey | null = null; function getVerificationKey() { if (verificationKey === null) { throw new Error('You must compile a MinaProgram before calling any of methods on it.'); } return verificationKey; } // TODO: this is wrong -- we need to check and interact with options, not just forward them. // A proper fix here is probably to refactor the compile interface for ZkProgram... this cache pattern is odd. async function compile(options?: { cache?: Cache; forceRecompile?: boolean; proofsEnabled?: boolean; }): Promise<{ verificationKey: { data: string; hash: Field }; }> { const compiledProgram = await Program.compile(options); verificationKey = new VerificationKey(compiledProgram.verificationKey); return compiledProgram; } const proverMethods = mapObject(descr.methods, (key: keyof MethodPrivateInputs) => proverMethod( descr.State, descr.Event, descr.Action, getVerificationKey, Program[key] as any /* TODO */, descr.methods[key] ) ); return { name: descr.name, State: descr.State, Event: descr.Event, Action: descr.Action, compile, ...proverMethods, }; }