UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

392 lines (352 loc) 13.7 kB
import { AccountUpdate } from './account-update.js'; import { Account } from './account.js'; import { AuthorizationLevel } from './authorization.js'; import { Update } from './core.js'; import { Permissions } from './permissions.js'; import { Preconditions, EpochDataPreconditions, EpochLedgerPreconditions, } from './preconditions.js'; import { StateLayout, StateUpdates, StateValues } from './state.js'; import { ZkappFeePayment } from './transaction.js'; import { ChainView, EpochData, EpochLedgerData } from './views.js'; import { Bool } from '../../provable/bool.js'; import { Field } from '../../provable/field.js'; import { Int64, Sign, UInt64, UInt32 } from '../../provable/int.js'; import { PublicKey } from '../../provable/crypto/signature.js'; import { ZkappConstants } from '../v1/constants.js'; export { checkAndApplyAccountUpdate, checkAndApplyFeePayment, ApplyState }; type ApplyResult<T> = ({ status: 'Applied' } & T) | { status: 'Failed'; errors: Error[] }; type ApplyState<T> = { status: 'Alive'; value: T } | { status: 'Dead' }; function updateApplyState<T>( applyState: ApplyState<T>, errors: Error[], f: (x: T) => T | Error ): ApplyState<T> { switch (applyState.status) { case 'Alive': const result = f(applyState.value); if (result instanceof Error) { errors.push(result); return { status: 'Dead' }; } else { return { status: 'Alive', value: result }; } case 'Dead': return applyState; } } // TODO: make this function checked-friendly, and move this function into the Int64 type directly function tryAddInt64(a: Int64, b: Int64): Int64 | null { if (a.sgn.equals(b.sgn).toBoolean() && a.magnitude.lessThan(b.magnitude).toBoolean()) return null; return a.add(b); } function checkPreconditions<State extends StateLayout>( chain: ChainView, account: Account<State>, preconditions: Preconditions<State>, errors: Error[] ): void { function preconditionError( preconditionName: string, constraint: { toStringHuman(): string }, value: unknown ): Error { return new Error( `${preconditionName} precondition failed: ${value} does not satisfy "${constraint.toStringHuman()}"` ); } // WARNING: failing to specify the type parameter on this function exhibits unsound behavior // (thanks typescript) // I think you can do something to fix this with NoInfer, but a first attempt at that seemed // to break it even more. function checkPrecondition<T>( preconditionName: string, constraint: { isSatisfied(x: T): Bool; toStringHuman(): string }, value: T ): void { if (constraint.isSatisfied(value).not().toBoolean()) errors.push(preconditionError(preconditionName, constraint, value)); } // ACCOUNT PRECONDITIONS checkPrecondition<UInt64>('balance', preconditions.account.balance, account.balance); checkPrecondition<UInt32>('nonce', preconditions.account.nonce, account.nonce); checkPrecondition<Field>( 'receiptChainHash', preconditions.account.receiptChainHash, account.receiptChainHash ); if (account.delegate !== null) checkPrecondition<PublicKey>('delegate', preconditions.account.delegate, account.delegate); checkPrecondition<Bool>('isProven', preconditions.account.isProven, account.zkapp.isProven); checkPrecondition<Bool>('isNew', preconditions.account.isNew, new Bool(account.isNew.get())); StateValues.checkPreconditions(account.State, account.zkapp.state, preconditions.account.state); const actionState = account.zkapp?.actionState ?? []; const actionStateSatisfied = Bool.anyTrue( actionState.map((s) => preconditions.account.actionState.isSatisfied(s)) ); if (actionStateSatisfied.not().toBoolean()) errors.push(preconditionError('actionState', preconditions.account.actionState, actionState)); // NETWORK PRECONDITIONS checkPrecondition('validWhile', preconditions.validWhile, chain.globalSlotSinceGenesis); checkPrecondition( 'snarkedLedgerHash', preconditions.network.snarkedLedgerHash, chain.snarkedLedgerHash ); checkPrecondition( 'blockchainLength', preconditions.network.blockchainLength, chain.blockchainLength ); checkPrecondition( 'minWindowDensity', preconditions.network.minWindowDensity, chain.minWindowDensity ); checkPrecondition('totalCurrency', preconditions.network.totalCurrency, chain.totalCurrency); checkPrecondition( 'globalSlotSinceGenesis', preconditions.network.globalSlotSinceGenesis, chain.globalSlotSinceGenesis ); function checkEpochLedgerPreconditions( name: string, epochLedgerPreconditions: EpochLedgerPreconditions, epochLedgerData: EpochLedgerData ) { checkPrecondition(`${name}.hash`, epochLedgerPreconditions.hash, epochLedgerData.hash); checkPrecondition( `${name}.totalCurrency`, epochLedgerPreconditions.totalCurrency, epochLedgerData.totalCurrency ); } function checkEpochDataPreconditions( name: string, epochDataPreconditions: EpochDataPreconditions, epochData: EpochData ): void { checkPrecondition(`${name}.seed`, epochDataPreconditions.seed, epochData.seed); checkPrecondition( `${name}.startCheckpoint`, epochDataPreconditions.startCheckpoint, epochData.startCheckpoint ); checkPrecondition( `${name}.lockCheckpoint`, epochDataPreconditions.lockCheckpoint, epochData.lockCheckpoint ); checkPrecondition( `${name}.epochLength`, epochDataPreconditions.epochLength, epochData.epochLength ); checkEpochLedgerPreconditions( `${name}.ledger`, epochDataPreconditions.ledger, epochData.ledger ); } checkEpochDataPreconditions( 'stakingEpochData', preconditions.network.stakingEpochData, chain.stakingEpochData ); checkEpochDataPreconditions( 'nextEpochData', preconditions.network.nextEpochData, chain.nextEpochData ); } function checkPermissions<State extends StateLayout, Event, Action>( permissions: Permissions, update: AccountUpdate<State, Event, Action>, errors: Error[] ): void { function checkPermission( permissionName: string, requiredAuthLevel: AuthorizationLevel, actionIsPerformed: boolean ): void { if (actionIsPerformed && !requiredAuthLevel.isSatisfied(update.authorizationKind)) errors.push( new Error( `${permissionName} permission was violated: account update has authorization kind ${update.authorizationKind.identifier()}, but required auth level is ${requiredAuthLevel.identifier()}` ) ); } checkPermission('access', permissions.access, true); checkPermission('send', permissions.send, update.balanceChange.isNegative().toBoolean()); checkPermission('receive', permissions.receive, update.balanceChange.isPositive().toBoolean()); checkPermission('incrementNonce', permissions.incrementNonce, update.incrementNonce.toBoolean()); checkPermission('setDelegate', permissions.setDelegate, update.delegateUpdate.set.toBoolean()); checkPermission( 'setPermissions', permissions.setPermissions, update.permissionsUpdate.set.toBoolean() ); checkPermission( 'setVerificationKey', permissions.setVerificationKey.auth, update.verificationKeyUpdate.set.toBoolean() ); checkPermission('setZkappUri', permissions.setZkappUri, update.zkappUriUpdate.set.toBoolean()); checkPermission( 'setTokenSymbol', permissions.setTokenSymbol, update.tokenSymbolUpdate.set.toBoolean() ); checkPermission('setVotingFor', permissions.setVotingFor, update.votingForUpdate.set.toBoolean()); checkPermission('setTiming', permissions.setTiming, update.timingUpdate.set.toBoolean()); checkPermission( 'editActionState', permissions.editActionState, update.pushActions.data.length > 0 ); checkPermission( 'editState', permissions.editState, StateUpdates.anyValuesAreSet(update.stateUpdates).toBoolean() ); } function applyUpdates<State extends StateLayout, Event, Action>( account: Account<State>, update: AccountUpdate<State, Event, Action>, feeExcessState: ApplyState<Int64>, errors: Error[] ): { updatedFeeExcessState: ApplyState<Int64>; updatedAccount: Account<State>; } { function applyUpdate<T>(update: Update<T>, value: T): T { return update.set.toBoolean() ? update.value : value; } let actualBalanceChange: Int64 = update.balanceChange; if (account.isNew.get()) { const accountCreationFee = Int64.create( UInt64.from(ZkappConstants.ACCOUNT_CREATION_FEE), Sign.minusOne ); feeExcessState = updateApplyState( feeExcessState, errors, (feeExcess) => tryAddInt64(feeExcess, accountCreationFee) ?? new Error('fee excess underflowed due when subtracting the account creation fee') ); if (update.implicitAccountCreationFee.toBoolean()) { const balanceChangeWithoutCreationFee = tryAddInt64(actualBalanceChange, accountCreationFee); if (balanceChangeWithoutCreationFee === null) { errors.push( new Error('balance change underflowed when subtracting the account creation fee') ); } else { actualBalanceChange = balanceChangeWithoutCreationFee; } } } const balanceSigned = Int64.create(account.balance, Sign.one); const updatedBalanceSigned = tryAddInt64(balanceSigned, actualBalanceChange); let updatedBalance = account.balance; if (updatedBalanceSigned === null) { errors.push( new Error('account balance overflowed or underflowed when applying balance change') ); } else if (updatedBalanceSigned.isNegative().toBoolean()) { errors.push(new Error('account balance was negative after applying balance change')); } else { updatedBalance = updatedBalanceSigned.magnitude; } const allStateUpdated = Bool.allTrue( StateUpdates.toFieldUpdates(account.State, update.stateUpdates).map((update) => update.set) ); const updatedAccount = new Account(account.State, false, { ...account, balance: updatedBalance, tokenSymbol: applyUpdate(update.tokenSymbolUpdate, account.tokenSymbol), nonce: update.incrementNonce.toBoolean() ? account.nonce.add(UInt32.one) : account.nonce, delegate: applyUpdate(update.delegateUpdate, account.delegate), votingFor: applyUpdate(update.votingForUpdate, account.votingFor), timing: applyUpdate(update.timingUpdate, account.timing), permissions: applyUpdate(update.permissionsUpdate, account.permissions), zkapp: { state: StateValues.applyUpdates(account.State, account.zkapp.state, update.stateUpdates), verificationKey: applyUpdate(update.verificationKeyUpdate, account.zkapp.verificationKey), actionState: /* TODO */ [ new Field(0), new Field(0), new Field(0), new Field(0), new Field(0), ], isProven: account.zkapp.isProven.or(allStateUpdated), zkappUri: applyUpdate(update.zkappUriUpdate, account.zkapp.zkappUri), }, }); return { updatedFeeExcessState: feeExcessState, updatedAccount }; } function checkAccountTiming<State extends StateLayout>( account: Account<State>, globalSlot: UInt32, errors: Error[] ): void { const minimumBalance = account.timing.minimumBalanceAtSlot(globalSlot); if (!account.balance.greaterThanOrEqual(minimumBalance).toBoolean()) errors.push(new Error('account has an insufficient minimum balance after applying update')); } // TODO: It's a good idea to have a check somewhere which asserts an account is valid before trying // applying account updates (eg: the account balance already meets the minimum requirement of // the account timing). This will help prevent other mistakes that occur before applying an // account update. function checkAndApplyAccountUpdate<State extends StateLayout, Event, Action>( chain: ChainView, account: Account<State>, update: AccountUpdate<State, Event, Action>, feeExcessState: ApplyState<Int64> ): ApplyResult<{ updatedFeeExcessState: ApplyState<Int64>; updatedAccount: Account<State>; }> { const errors: Error[] = []; if (!account.accountId.equals(update.accountId).toBoolean()) errors.push(new Error('account id in account update does not match actual account id')); if (!account.zkapp.verificationKey.hash.equals(update.verificationKeyHash).toBoolean()) errors.push( new Error( `account verification key does not match account update's verification key (account has ${account.zkapp.verificationKey.hash}, account update referenced ${update.verificationKeyHash})` ) ); // TODO: check mayUseToken (somewhere, maybe not here) checkPreconditions(chain, account, update.preconditions, errors); checkPermissions(account.permissions, update, errors); const { updatedFeeExcessState, updatedAccount } = applyUpdates( account, update, feeExcessState, errors ); checkAccountTiming(updatedAccount, chain.globalSlotSinceGenesis, errors); if (errors.length === 0) { return { status: 'Applied', updatedFeeExcessState, updatedAccount }; } else { return { status: 'Failed', errors }; } } function checkAndApplyFeePayment( chain: ChainView, account: Account, feePayment: ZkappFeePayment ): ApplyResult<{ updatedAccount: Account }> { const result = checkAndApplyAccountUpdate(chain, account, feePayment.toAccountUpdate(), { status: 'Alive', value: Int64.zero, }); if (result.status === 'Applied') { return { status: 'Applied', updatedAccount: result.updatedAccount }; } else { return result; } }