UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

269 lines 12.4 kB
import { AccountUpdateAuthorizationKind, } from './authorization.js'; import { AccountUpdate, AccountUpdateTree, Authorized, GenericData } from './account-update.js'; import { Account, AccountId, AccountIdSet } from './account.js'; import { TokenId } from './core.js'; import { getCallerFrame, ZkappCommandErrorTrace } from './errors.js'; import { Precondition } from './preconditions.js'; import { checkAndApplyAccountUpdate, checkAndApplyFeePayment } from './zkapp-logic.js'; import { Bool } from '../../provable/bool.js'; import { Field } from '../../provable/field.js'; import { Int64, Sign, UInt32 } from '../../provable/int.js'; import { mocks } from '../../../bindings/crypto/constants.js'; import * as BindingsLayout from '../../../bindings/mina-transaction/gen/v2/js-layout.js'; import { Memo } from '../../../mina-signer/src/memo.js'; import { hashWithPrefix, prefixes } from '../../../mina-signer/src/poseidon-bigint.js'; import { Signature, signFieldElement } from '../../../mina-signer/src/signature.js'; export { ZkappCommand, ZkappFeePayment, ZkappCommandContext, AuthorizedZkappCommand, createZkappCommand, }; class ZkappFeePayment { constructor(descr) { this.__type = 'ZkappCommand'; this.publicKey = descr.publicKey; this.fee = descr.fee; this.validUntil = descr.validUntil; this.nonce = descr.nonce; } authorize({ networkId, privateKey, fullTransactionCommitment, }) { let signature = signFieldElement(fullTransactionCommitment, privateKey.toBigInt(), networkId); return new AuthorizedZkappFeePayment(this, Signature.toBase58(signature)); } toAccountUpdate() { return new AccountUpdate('GenericState', GenericData, GenericData, { authorizationKind: AccountUpdateAuthorizationKind.Signature(), verificationKeyHash: new Field(mocks.dummyVerificationKeyHash), callData: new Field(0), accountId: new AccountId(this.publicKey, TokenId.MINA), balanceChange: Int64.create(this.fee, Sign.minusOne), incrementNonce: new Bool(true), useFullCommitment: new Bool(true), implicitAccountCreationFee: new Bool(true), preconditions: { account: { nonce: this.nonce, }, network: { globalSlotSinceGenesis: Precondition.InRange.betweenInclusive(UInt32.zero, this.validUntil ?? UInt32.MAXINT()), }, }, }); } toDummyAuthorizedAccountUpdate() { return new Authorized({ signature: '', proof: null }, this.toAccountUpdate()); } toInternalRepr() { return { publicKey: this.publicKey, fee: this.fee, validUntil: this.validUntil, nonce: this.nonce, }; } toJSON() { return ZkappFeePayment.toJSON(this); } static toJSON(x) { return BindingsLayout.FeePayerBody.toJSON(x.toInternalRepr()); } } class AuthorizedZkappFeePayment { constructor(body, signature) { this.body = body; this.signature = signature; } toInternalRepr() { return { body: this.body.toInternalRepr(), authorization: this.signature, }; } } class ZkappCommand { constructor(descr) { // TODO: put this on everything (in this case, we really need it to disambiguate the Description format) this.__type = 'ZkappCommand'; this.feePayment = descr.feePayment; this.accountUpdateForest = descr.accountUpdates.map((update) => update instanceof AccountUpdateTree ? update : new AccountUpdateTree(update, [])); // TODO: we probably want an explicit memo type instead to help enforce these rules early and not surprise the user when their memo changes slightly later this.memo = Memo.fromString(descr.memo ?? ''); } commitments(networkId) { const feePayerCommitment = this.feePayment.toDummyAuthorizedAccountUpdate().hash(networkId); const accountUpdateForestCommitment = AccountUpdateTree.hashForest(networkId, this.accountUpdateForest); const memoCommitment = Memo.hash(this.memo); const fullTransactionCommitment = hashWithPrefix(prefixes.accountUpdateCons, [ memoCommitment, feePayerCommitment.toBigInt(), accountUpdateForestCommitment, ]); return { accountUpdateForestCommitment, fullTransactionCommitment }; } async authorize(authEnv) { const feePayerPrivateKey = await authEnv.getPrivateKey(this.feePayment.publicKey); const commitments = this.commitments(authEnv.networkId); const authorizedFeePayment = this.feePayment.authorize({ networkId: authEnv.networkId, privateKey: feePayerPrivateKey, fullTransactionCommitment: commitments.fullTransactionCommitment, }); const accountUpdateAuthEnv = { ...authEnv, ...commitments, }; const authorizedAccountUpdateForest = await AccountUpdateTree.mapForest(this.accountUpdateForest, (accountUpdate) => accountUpdate.authorize(accountUpdateAuthEnv)); return new AuthorizedZkappCommand({ feePayment: authorizedFeePayment, accountUpdateForest: authorizedAccountUpdateForest, memo: this.memo, }); } } class AuthorizedZkappCommand { constructor({ feePayment, accountUpdateForest, memo, }) { this.__type = 'AuthorizedZkappCommand'; this.feePayment = feePayment; this.accountUpdateForest = accountUpdateForest; // TODO: here we have to assume the Memo is already encoded correctly, but what we really want is a Memo type... this.memo = memo; } toInternalRepr() { return { feePayer: this.feePayment.toInternalRepr(), accountUpdates: AccountUpdateTree.unrollForest(this.accountUpdateForest, (update, depth) => update.toInternalRepr(depth)), memo: Memo.toBase58(this.memo), }; } toJSON() { return AuthorizedZkappCommand.toJSON(this); } static toJSON(x) { return BindingsLayout.ZkappCommand.toJSON(x.toInternalRepr()); } } // NB: this is really more of an environment than a context, but this naming convention helps to // disambiguate the transaction environment from the mina program environment class ZkappCommandContext { constructor(ledger, chain, failedAccounts, globalSlot) { this.ledger = ledger; this.chain = chain; this.failedAccounts = failedAccounts; this.globalSlot = globalSlot; this.feeExcessState = { status: 'Alive', value: Int64.zero }; this.accountUpdateForest = []; this.accountUpdateForestTrace = []; } add(x) { const callSite = getCallerFrame(); const accountUpdateTree = x instanceof AccountUpdateTree ? x : new AccountUpdateTree(x, []); const genericAccountUpdateTree = AccountUpdateTree.mapRoot(accountUpdateTree, (accountUpdate) => accountUpdate.toGeneric()); const trace = AccountUpdateTree.reduce(genericAccountUpdateTree, (accountUpdate, childTraces) => { let errors; if (!this.failedAccounts.has(accountUpdate.accountId)) { const account = this.ledger.getAccount(accountUpdate.accountId) ?? Account.empty(accountUpdate.accountId); const applied = checkAndApplyAccountUpdate(this.chain, account, accountUpdate, this.feeExcessState); switch (applied.status) { case 'Applied': errors = []; this.ledger.setAccount(applied.updatedAccount); this.feeExcessState = applied.updatedFeeExcessState; break; case 'Failed': errors = applied.errors; break; } } else { errors = [ // TODO: this should be a warning new Error('skipping account update because a previous account update failed when accessing the same account'), ]; } return { accountId: accountUpdate.accountId, callSite, errors, childTraces, }; }); this.accountUpdateForest.push(genericAccountUpdateTree); this.accountUpdateForestTrace.push(trace); } // only to be used when an account update tree has already been applied to the ledger view unsafeAddWithoutApplying(x, trace) { const accountUpdateTree = x instanceof AccountUpdateTree ? x : new AccountUpdateTree(x, []); const genericAccountUpdateTree = AccountUpdateTree.mapRoot(accountUpdateTree, (accountUpdate) => accountUpdate.toGeneric()); this.accountUpdateForest.push(genericAccountUpdateTree); // TODO: check that the trace shape matches the account update shape this.accountUpdateForestTrace.push(trace); } finalize() { const errors = []; if (this.feeExcessState.status === 'Dead') { errors.push(new Error('fee excess could not be computed due to other errors')); } else if (!this.feeExcessState.value.equals(Int64.zero).toBoolean()) { errors.push(new Error('fee excess does not equal 0 (this transaction is attempting to either burn or mint new Mina tokens, which is disallowed)')); } return { accountUpdateForest: [...this.accountUpdateForest], accountUpdateForestTrace: [...this.accountUpdateForestTrace], generalErrors: errors, }; } } // IMPORTANT TODO: Currently, if a zkapp command fails in the virtual application, any successful // account updates are still applied to the provided ledger view. We should // probably make the ledger view interface immutable, or clone it every time we // create a new zkapp command, to help avoid unexpected behavior externally. async function createUnsignedZkappCommand(ledger, chain, { feePayer, fee, validUntil, }, f) { // TODO const globalSlot = UInt32.zero; const failedAccounts = new AccountIdSet(); let feePaymentErrors = []; let feePayment = null; const feePayerId = new AccountId(feePayer, TokenId.MINA); const feePayerAccount = ledger.getAccount(feePayerId); if (feePayerAccount !== null) { feePayment = new ZkappFeePayment({ publicKey: feePayer, nonce: feePayerAccount.nonce, fee, validUntil, }); const applied = checkAndApplyFeePayment(chain, feePayerAccount, feePayment); switch (applied.status) { case 'Applied': ledger.setAccount(applied.updatedAccount); break; case 'Failed': feePaymentErrors = applied.errors; failedAccounts.add(feePayerAccount.accountId); break; } } else { feePaymentErrors = [new Error('zkapp fee payer account not found')]; failedAccounts.add(feePayerId); } const ctx = new ZkappCommandContext(ledger, chain, failedAccounts, globalSlot); await f(ctx); const { accountUpdateForest, accountUpdateForestTrace, generalErrors } = ctx.finalize(); const errorTrace = new ZkappCommandErrorTrace(generalErrors, feePaymentErrors, accountUpdateForestTrace); if (!errorTrace.hasErrors()) { // should never be true if we hit this branch if (feePayment === null) throw new Error('internal error'); return new ZkappCommand({ feePayment, accountUpdates: accountUpdateForest, }); } else { console.log(errorTrace.generateReport()); throw new Error('errors were encountered while creating a ZkappCommand (an error report is available in the logs)'); } } async function createZkappCommand(ledger, chain, authEnv, feePayment, f) { const unsignedCmd = await createUnsignedZkappCommand(ledger, chain, feePayment, f); return unsignedCmd.authorize(authEnv); } //# sourceMappingURL=transaction.js.map