diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a54469555..ea097597d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ This project adheres to https://github.com/o1-labs/o1js/pull/2436 - Improved the runtime table API with a `RuntimeTable` class with better readability https://github.com/o1-labs/o1js/pull/2402 +- Internal `Mina.LocalBlockchain` type exported via `Mina`. https://github.com/o1-labs/o1js/pull/2538 ### Fixed diff --git a/src/examples/zkapps/hello-world/run.ts b/src/examples/zkapps/hello-world/run.ts index e708f7446e..07abf16ba5 100644 --- a/src/examples/zkapps/hello-world/run.ts +++ b/src/examples/zkapps/hello-world/run.ts @@ -63,7 +63,7 @@ try { await txn.prove(); await txn.sign([feePayer1.key]).send(); } catch (err: any) { - handleError(err, 'Account_delegate_precondition_unsatisfied'); + handleError(err, 'delegate precondition'); } if (!correctlyFails) { diff --git a/src/lib/mina/v1/ledger/ledger.ts b/src/lib/mina/v1/ledger/ledger.ts new file mode 100644 index 0000000000..807aa3bdbb --- /dev/null +++ b/src/lib/mina/v1/ledger/ledger.ts @@ -0,0 +1,140 @@ +import { PublicKey } from '../../../provable/crypto/signature.js'; +import { Int64, UInt64 } from '../../../provable/int.js'; +import { Field } from '../../../provable/wrapped.js'; +import { AccountUpdate as AccountUpdateV2 } from '../../v2/account-update.js'; +import { Account as AccountV2 } from '../../v2/account.js'; +import { ZkappFeePayment } from '../../v2/transaction.js'; +import { ChainView } from '../../v2/views.js'; +import { + AccountUpdateApplyResult, + ApplyState, + checkAndApplyAccountUpdate, + checkAndApplyFeePayment, +} from '../../v2/zkapp-logic.js'; +import { TokenId, ZkappCommand } from '../account-update.js'; +import { Account as AccountV1, newAccount } from '../account.js'; + +export const DefaultTokenId = 1n; + +export type AccountId = { publicKey: PublicKey; tokenId?: Field }; + +export class LocalLedger { + _nextLocation: bigint; + _accounts: Map; + _locations: Map; // keyed by "pubkey|tokenId" + + constructor() { + this._nextLocation = 1n; + this._accounts = new Map(); + this._locations = new Map(); + } + + static create(): LocalLedger { + return new LocalLedger(); + } + + _key(publicKey: PublicKey, tokenId: Field) { + return `${publicKey.toBase58()}|${tokenId}`; + } + + saveAccount(publicKey: PublicKey, account: AccountV1) { + const location = (() => { + const key = this._key(publicKey, TokenId.default); + const existing = this._locations.get(key); + if (existing === undefined) throw new Error('account with public key does not exist'); + return existing; + })(); + this._accounts.set(location, account); + } + + addAccount(publicKey: PublicKey, balance: bigint | number | string): void { + const accountId = { publicKey, tokenId: TokenId.default }; + const location = (() => { + const key = this._key(publicKey, TokenId.default); + const existing = this._locations.get(key); + if (existing !== undefined) throw new Error('account with public key already exists'); + const newLocation = this._nextLocation; + this._nextLocation += 1n; + return newLocation; + })(); + + const account = newAccount(accountId); + account.balance = UInt64.from(balance); + const key = this._key(publicKey, TokenId.default); + this._locations.set(key, location); + this._accounts.set(location, account); + } + + getAccount(publicKey: PublicKey, tokenId: Field = TokenId.default): AccountV1 | undefined { + const key = this._key(publicKey, tokenId); + const location = this._locations.get(key); + if (location === undefined) return undefined; + return this._accounts.get(location); + } + + getOrCreateAccount(publicKey: PublicKey, tokenId: Field = TokenId.default): AccountV1 { + const account = this.getAccount(publicKey, tokenId); + if (account != undefined) { + return account; + } + + this.addAccount(publicKey, 0); + return this.getAccount(publicKey, tokenId)!; + } + + applyTransaction(transaction: ZkappCommand, fee: UInt64, networkState: ChainView): void { + const feePayerAccount = this.getAccount(transaction.feePayer.body.publicKey); + if (!feePayerAccount) { + throw new Error('fee payer account not found'); + } + + const feePayerAccountV2 = AccountV2.fromV1(feePayerAccount); + const feePayerUpdate = checkAndApplyFeePayment( + networkState, + feePayerAccountV2, + new ZkappFeePayment({ + publicKey: feePayerAccount.publicKey, + nonce: feePayerAccount.nonce, + fee, + validUntil: transaction.feePayer.body.validUntil, + }) + ); + if (feePayerUpdate.status == 'Failed') { + throw new Error(`failed to apply fee payment with errors: ${feePayerUpdate.errors}`); + } + this.saveAccount(feePayerAccount.publicKey, feePayerUpdate.updatedAccount.toV1()); + + let feeExcessState: ApplyState = { status: 'Alive', value: Int64.zero }; + + for (const update of transaction.accountUpdates) { + const { body, authorization } = update; + if (body.authorizationKind.isProved.toBoolean() && !authorization.proof) { + throw Error( + `The actual authorization does not match the expected authorization kind. Did you forget to invoke \`await tx.prove()\`?` + ); + } + const account = this.getOrCreateAccount(body.publicKey, body.tokenId); + const accountUpdateV2 = AccountUpdateV2.fromInternalRepr(update.body); + const accountV2 = AccountV2.fromV1(account); + try { + const applied: AccountUpdateApplyResult = checkAndApplyAccountUpdate( + networkState, + accountV2, + accountUpdateV2, + feeExcessState + ); + if (applied.status == 'Failed') { + throw new Error(`Failed to apply account update with errors: ${applied.errors}`); + } + this.saveAccount(body.publicKey, applied.updatedAccount.toV1()); + feeExcessState = applied.updatedFeeExcessState; + } catch (e) { + console.error(e); + console.log(JSON.stringify(account, null, 2)); + console.log(JSON.stringify(accountV2, null, 2)); + console.log(JSON.stringify(update, null, 2)); + throw e; + } + } + } +} diff --git a/src/lib/mina/v1/local-blockchain.ts b/src/lib/mina/v1/local-blockchain.ts index 270f9e25d5..65139df1f3 100644 --- a/src/lib/mina/v1/local-blockchain.ts +++ b/src/lib/mina/v1/local-blockchain.ts @@ -1,16 +1,15 @@ -import { Ledger, Test, initializeBindings } from '../../../bindings.js'; -import { Types, TypesBigint } from '../../../bindings/mina-transaction/v1/types.js'; +import { Test, initializeBindings } from '../../../bindings.js'; +import { TypesBigint } from '../../../bindings/mina-transaction/v1/types.js'; import { transactionCommitments } from '../../../mina-signer/src/sign-zkapp-command.js'; import { NetworkId } from '../../../mina-signer/src/types.js'; -import { Ml } from '../../ml/conversion.js'; import { PrivateKey, PublicKey } from '../../provable/crypto/signature.js'; import { UInt32, UInt64 } from '../../provable/int.js'; import { Field } from '../../provable/wrapped.js'; -import { prettifyStacktrace } from '../../util/errors.js'; import { TupleN } from '../../util/types.js'; import { Actions, Authorization, TokenId, ZkappCommand } from './account-update.js'; import { Account } from './account.js'; import { invalidTransactionError } from './errors.js'; +import { LocalLedger } from './ledger/ledger.js'; import { Mina, defaultNetworkConstants, @@ -71,24 +70,16 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits const slotTime = 3 * 60 * 1000; const startTime = Date.now(); const genesisTimestamp = UInt64.from(startTime); - const ledger = Ledger.create(); + const ledger = LocalLedger.create(); let networkState = defaultNetworkState(); - function addAccount(publicKey: PublicKey, balance: string) { - try { - ledger.addAccount(Ml.fromPublicKey(publicKey), balance); - } catch (error) { - throw prettifyStacktrace(error); - } - } - let testAccounts = [] as never as TupleN; for (let i = 0; i < 10; ++i) { let MINA = 10n ** 9n; const largeValue = 1000n * MINA; const testAccount = TestPublicKey.random(); - addAccount(testAccount, largeValue.toString()); + ledger.addAccount(testAccount, largeValue.toString()); testAccounts.push(testAccount); } @@ -109,14 +100,14 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits return UInt32.from(Math.ceil((new Date().valueOf() - startTime) / slotTime)); }, hasAccount(publicKey: PublicKey, tokenId: Field = TokenId.default) { - return !!ledger.getAccount(Ml.fromPublicKey(publicKey), Ml.constFromField(tokenId)); + return !!ledger.getAccount(publicKey, tokenId); }, getAccount(publicKey: PublicKey, tokenId: Field = TokenId.default): Account { - let accountJson = ledger.getAccount(Ml.fromPublicKey(publicKey), Ml.constFromField(tokenId)); - if (accountJson === undefined) { + const account = ledger.getAccount(publicKey, tokenId); + if (account === undefined) { throw new Error(reportGetAccountError(publicKey.toBase58(), TokenId.toBase58(tokenId))); } - return Types.Account.fromJSON(accountJson); + return account; }, getNetworkState() { return networkState; @@ -137,7 +128,7 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits for (const update of txn.transaction.accountUpdates) { let authIsProof = !!update.authorization.proof; let kindIsProof = update.body.authorizationKind.isProved.toBoolean(); - // checks and edge case where a proof is expected, but the developer forgot to invoke await tx.prove() + // checks an edge case where a proof is expected, but the developer forgot to invoke await tx.prove() // this resulted in an assertion OCaml error, which didn't contain any useful information if (kindIsProof && !authIsProof) { throw Error( @@ -149,12 +140,8 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits // the first time we encounter an account, use it from the persistent ledger if (account === undefined) { - let accountJson = ledger.getAccount( - Ml.fromPublicKey(update.body.publicKey), - Ml.constFromField(update.body.tokenId) - ); - if (accountJson !== undefined) { - let storedAccount = Account.fromJSON(accountJson); + let storedAccount = ledger.getAccount(update.body.publicKey, update.body.tokenId); + if (storedAccount !== undefined) { simpleLedger.store(storedAccount); account = storedAccount; } @@ -178,10 +165,10 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits let status: PendingTransactionStatus = 'pending'; const errors: string[] = []; try { - ledger.applyJsonTransaction( - JSON.stringify(zkappCommandJson), - defaultNetworkConstants.accountCreationFee.toString(), - JSON.stringify(networkState) + ledger.applyTransaction( + txn.transaction, + defaultNetworkConstants.accountCreationFee, + networkState ); } catch (err: any) { status = 'rejected'; @@ -310,13 +297,6 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits }); }); }, - applyJsonTransaction(json: string) { - return ledger.applyJsonTransaction( - json, - defaultNetworkConstants.accountCreationFee.toString(), - JSON.stringify(networkState) - ); - }, async fetchEvents(publicKey: PublicKey, tokenId: Field = TokenId.default) { // Return events in reverse chronological order (latest events at the beginning) const reversedEvents = ( @@ -363,7 +343,7 @@ async function LocalBlockchain({ proofsEnabled = true, enforceTransactionLimits } return currentActions.slice(startIndex, endIndex); }, - addAccount, + addAccount: ledger.addAccount.bind(ledger), /** * An array of 10 test accounts that have been pre-filled with * 30000000000 units of currency. diff --git a/src/lib/mina/v2/account-update.unit-test.ts b/src/lib/mina/v2/account-update.unit-test.ts index b4dd928a42..dacad02004 100644 --- a/src/lib/mina/v2/account-update.unit-test.ts +++ b/src/lib/mina/v2/account-update.unit-test.ts @@ -1,15 +1,8 @@ -import { AccountUpdate, Authorized, GenericData } from './account-update.js'; -import { AccountId, AccountTiming } from './account.js'; -import { AccountUpdateAuthorizationKind } from './authorization.js'; -import { TokenId, Update } from './core.js'; -import { Precondition } from './preconditions.js'; -import { GenericStatePreconditions, GenericStateUpdates } from './state.js'; -import { AccountUpdate as V1AccountUpdateImpl } from '../v1/account-update.js'; -import { VerificationKey } from '../../proof-system/verification-key.js'; -import { Bool } from '../../provable/bool.js'; -import { Field } from '../../provable/field.js'; -import { UInt32, UInt64, Int64, Sign } from '../../provable/int.js'; -import { PrivateKey } from '../../provable/crypto/signature.js'; +import { expect } from 'expect'; +import { jsLayout as layoutV1 } from '../../../bindings/mina-transaction/gen/v1/js-layout.js'; +import * as ValuesV1 from '../../../bindings/mina-transaction/gen/v1/transaction-bigint.js'; +import * as JsonV1 from '../../../bindings/mina-transaction/gen/v1/transaction-json.js'; +import * as TypesV1 from '../../../bindings/mina-transaction/gen/v1/transaction.js'; import { Actions as V1Actions, Events as V1Events, @@ -17,26 +10,33 @@ import { TokenSymbol as V1TokenSymbol, ZkappUri as V1ZkappUri, } from '../../../bindings/mina-transaction/v1/transaction-leaves.js'; -import * as TypesV1 from '../../../bindings/mina-transaction/gen/v1/transaction.js'; -import * as ValuesV1 from '../../../bindings/mina-transaction/gen/v1/transaction-bigint.js'; -import * as JsonV1 from '../../../bindings/mina-transaction/gen/v1/transaction-json.js'; -import { jsLayout as layoutV1 } from '../../../bindings/mina-transaction/gen/v1/js-layout.js'; -import { expect } from 'expect'; +import { VerificationKey } from '../../proof-system/verification-key.js'; +import { Bool } from '../../provable/bool.js'; +import { PrivateKey } from '../../provable/crypto/signature.js'; +import { Field } from '../../provable/field.js'; +import { Int64, Sign, UInt32, UInt64 } from '../../provable/int.js'; +import { AccountUpdate as V1AccountUpdateImpl } from '../v1/account-update.js'; +import { AccountUpdate, Authorized, GenericData } from './account-update.js'; +import { AccountId, AccountTiming } from './account.js'; +import { AccountUpdateAuthorizationKind } from './authorization.js'; +import { TokenId, Update } from './core.js'; +import { Precondition } from './preconditions.js'; +import { GenericStatePreconditions, GenericStateUpdates } from './state.js'; +import { + Signature, + signFieldElement, + zkAppBodyPrefix, +} from '../../../mina-signer/src/signature.js'; import { ZkappConstants } from '../v1/constants.js'; import { testV1V2ClassEquivalence, testV1V2ValueEquivalence, testV2Encoding, } from './test/utils.js'; -import { - Signature, - signFieldElement, - zkAppBodyPrefix, -} from '../../../mina-signer/src/signature.js'; import { Types } from '../../../bindings/mina-transaction/v1/types.js'; -import { packToFields, hashWithPrefix } from '../../../lib/provable/crypto/poseidon.js'; +import { hashWithPrefix, packToFields } from '../../../lib/provable/crypto/poseidon.js'; function testHashEquality(v1: TypesV1.AccountUpdate, v2: Authorized) { expect(TypesV1.AccountUpdate.toInput(v1)).toEqual(v2.toInput()); diff --git a/src/lib/mina/v2/account.ts b/src/lib/mina/v2/account.ts index f721692558..232311b13e 100644 --- a/src/lib/mina/v2/account.ts +++ b/src/lib/mina/v2/account.ts @@ -1,16 +1,17 @@ -import { Permissions } from './permissions.js'; -import { StateDefinition, StateLayout, StateValues } from './state.js'; +import { TokenSymbol } from '../../../lib/provable/crypto/poseidon.js'; import { VerificationKey } from '../../proof-system/verification-key.js'; import { Bool } from '../../provable/bool.js'; +import { PublicKey } from '../../provable/crypto/signature.js'; import { Field } from '../../provable/field.js'; -import { UInt64, UInt32 } from '../../provable/int.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 { TokenSymbol } from '../../../lib/provable/crypto/poseidon.js'; +import { Account as AccountV1 } from '../v1/account.js'; import { TokenId, ZkappUri } from './core.js'; +import { Permissions } from './permissions.js'; +import { GenericStateValues, StateDefinition, StateLayout, StateValues } from './state.js'; -export { AccountId, AccountTiming, AccountIdSet, Account, AccountIdMap }; +export { Account, AccountId, AccountIdMap, AccountIdSet, AccountTiming }; function accountIdKeys(accountId: AccountId): { publicKey: string; @@ -267,239 +268,6 @@ class Account { }; } - /* - checkAndApplyFeePayment( - feePayment: ZkappFeePayment - ): - | { status: 'Applied'; updatedAccount: Account } - | { status: 'Failed'; errors: Error[] } { - const errors: Error[] = []; - - if (this.accountId.tokenId.equals(TokenId.MINA).not().toBoolean()) - errors.push(new Error('cannot pay zkapp fee with a non-mina account')); - - if (this.accountId.publicKey.equals(feePayment.publicKey).not().toBoolean()) - errors.push( - new Error('fee payment public key does not match account public key') - ); - - if (this.nonce.equals(feePayment.nonce).not().toBoolean()) - errors.push(new Error('invalid account nonce')); - - if (this.balance.lessThan(feePayment.fee).toBoolean()) - errors.push( - new Error( - 'account does not have enough balance to pay the required fee' - ) - ); - - // TODO: validWhile (probably checked elsewhere) - - if (errors.length === 0) { - const updatedAccount = new Account(this.State, false, { - ...this, - balance: this.balance.sub(feePayment.fee), - nonce: this.nonce.add(UInt32.one), - }); - return { status: 'Applied', updatedAccount }; - } else { - return { status: 'Failed', errors }; - } - } - - // TODO: replay checks (probably live on the AccountUpdate itself, but needs to be called near this) - checkAndApplyUpdate( - update: AccountUpdate - ): - | { status: 'Applied'; updatedAccount: Account } - | { status: 'Failed'; errors: Error[] } { - const errors: Error[] = []; - - if (this.accountId.equals(update.accountId).not().toBoolean()) - errors.push( - new Error( - 'account id in account update does not match actual account id' - ) - ); - - // TODO: check verificationKeyHash - // TODO: check mayUseToken (somewhere, maybe not here) - - // CHECK PRECONDITIONS - - 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) - function checkPrecondition( - preconditionName: string, - constraint: { isSatisfied(x: T): Bool; toStringHuman(): string }, - value: T - ): void { - if (constraint.isSatisfied(value).not().toBoolean()) - errors.push(preconditionError(preconditionName, constraint, value)); - } - - checkPrecondition( - 'balance', - update.preconditions.account.balance, - this.balance - ); - checkPrecondition( - 'nonce', - update.preconditions.account.nonce, - this.nonce - ); - checkPrecondition( - 'receiptChainHash', - update.preconditions.account.receiptChainHash, - this.receiptChainHash - ); - if (this.delegate !== null) - checkPrecondition( - 'delegate', - update.preconditions.account.delegate, - this.delegate - ); - checkPrecondition( - 'isProven', - update.preconditions.account.isProven, - this.zkapp.isProven - ); - - StateValues.checkPreconditions( - this.State, - this.zkapp.state, - update.preconditions.account.state - ); - - const actionState = this.zkapp?.actionState ?? []; - const actionStateSatisfied = Bool.anyTrue( - actionState.map((s) => - update.preconditions.account.actionState.isSatisfied(s) - ) - ); - if (actionStateSatisfied.not().toBoolean()) - errors.push( - preconditionError( - 'actionState', - update.preconditions.account.actionState, - actionState - ) - ); - - // TODO: updates.preconditions.account.isNew - - // TODO: network (probably checked elsewhere) - // TODO: validWhile (probably checked elsewhere) - - // CHECK PERMISSIONS - - 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', this.permissions.access, true); - checkPermission('send', this.permissions.send, update.balanceChange.isNegative().toBoolean()); - checkPermission('receive', this.permissions.receive, update.balanceChange.isPositive().toBoolean()); - checkPermission('incrementNonce', this.permissions.incrementNonce, update.incrementNonce.toBoolean()); - checkPermission('setDelegate', this.permissions.setDelegate, update.delegateUpdate.set.toBoolean()); - checkPermission('setPermissions', this.permissions.setPermissions, update.permissionsUpdate.set.toBoolean()); - checkPermission('setVerificationKey', this.permissions.setVerificationKey.auth, update.verificationKeyUpdate.set.toBoolean()); - checkPermission('setZkappUri', this.permissions.setZkappUri, update.zkappUriUpdate.set.toBoolean()); - checkPermission('setTokenSymbol', this.permissions.setTokenSymbol, update.tokenSymbolUpdate.set.toBoolean()); - checkPermission('setVotingFor', this.permissions.setVotingFor, update.votingForUpdate.set.toBoolean()); - checkPermission('setTiming', this.permissions.setTiming, update.timingUpdate.set.toBoolean()); - checkPermission('editActionState', this.permissions.editActionState, update.pushActions.data.length > 0); - checkPermission('editState', this.permissions.editState, StateUpdates.anyValuesAreSet(update.stateUpdates).toBoolean()); - - // APPLY UPDATES - - // TODO: account for implicitAccountCreationFee here - let updatedBalance: UInt64 = this.balance; - // TODO: why is Int64 not comparable? - // if(update.balanceChange.lessThan(Int64.create(this.balance, Sign.minusOne)).toBoolean()) - if ( - update.balanceChange.isNegative().toBoolean() && - update.balanceChange.magnitude.greaterThan(this.balance).toBoolean() - ) { - errors.push( - new Error( - `insufficient balance for balanceChange (balance = ${this.balance}, balanceChange = -${update.balanceChange.magnitude})` - ) - ); - } else { - // TODO: check for overflows? - const isPos = update.balanceChange.isPositive().toBoolean(); - const amount = update.balanceChange.magnitude; - updatedBalance = isPos - ? this.balance.add(amount) - : this.balance.sub(amount); - } - - // TODO: pushEvents - // TODO: pushActions - - if (errors.length === 0) { - function applyUpdate(update: Update, value: T): T { - return update.set.toBoolean() ? update.value : value; - } - - const allStateUpdated = Bool.allTrue( - StateUpdates.toFieldUpdates(this.State, update.stateUpdates).map( - (update) => update.set - ) - ); - - const updatedAccount = new Account(this.State, false, { - ...this, - balance: updatedBalance, - tokenSymbol: applyUpdate(update.tokenSymbolUpdate, this.tokenSymbol), - nonce: update.incrementNonce.toBoolean() - ? this.nonce.add(UInt32.one) - : this.nonce, - delegate: applyUpdate(update.delegateUpdate, this.delegate), - votingFor: applyUpdate(update.votingForUpdate, this.votingFor), - timing: applyUpdate(update.timingUpdate, this.timing), - permissions: applyUpdate(update.permissionsUpdate, this.permissions), - zkapp: { - state: StateValues.applyUpdates( - this.State, - this.zkapp.state, - update.stateUpdates - ), - verificationKey: applyUpdate( - update.verificationKeyUpdate, - this.zkapp.verificationKey - ), - // actionState: TODO, - isProven: this.zkapp.isProven.or(allStateUpdated), - zkappUri: applyUpdate(update.zkappUriUpdate, this.zkapp.zkappUri), - }, - }); - - return { status: 'Applied', updatedAccount }; - } else { - return { status: 'Failed', errors }; - } - } - */ - toGeneric(): Account { return new Account<'GenericState'>('GenericState', this.isNew, { ...this, @@ -518,7 +286,7 @@ class Account { ...account, zkapp: { ...account.zkapp, - state: StateValues.fromGeneric(account.zkapp.state, State), + state: StateValues.fromGeneric(account.zkapp?.state ?? {}, State), }, }); } @@ -536,4 +304,103 @@ class Account { permissions: Permissions.defaults(), }); } + + static fromV1(account: AccountV1): Account<'GenericState'> { + const { + publicKey, + tokenId: tokenIdValue, + tokenSymbol: tokenSymbolValue, + delegate: delegateValue, + timing: timingValue, + permissions: permissionsValue, + zkapp: zkappValue, + ...rest + } = account; + + const tokenId = new TokenId(tokenIdValue); + const accountId = new AccountId(publicKey, tokenId); + const tokenSymbol = new TokenSymbol(tokenSymbolValue); + const delegate = delegateValue ?? null; + const timing = new AccountTiming(timingValue); + const permissions = Permissions.fromInternalRepr(permissionsValue); + + const zkapp = (() => { + if (!zkappValue) { + return undefined; + } + + const { + appState, + verificationKey: verificationKeyValue, + provedState, + zkappUri: zkAppUriValue, + ...rest + } = zkappValue; + if (!verificationKeyValue) { + return undefined; + } + + const verificationKey = new VerificationKey(verificationKeyValue); + const state = new GenericStateValues(appState); + const zkappUri = new ZkappUri(zkAppUriValue); + + return { + state, + verificationKey, + zkappUri, + isProven: provedState, + ...rest, + }; + })(); + + return new Account<'GenericState'>('GenericState', false, { + accountId, + tokenSymbol, + delegate, + timing, + permissions, + zkapp, + ...rest, + }); + } + + toV1(): AccountV1 { + const { accountId, tokenSymbol, delegate, zkapp: zkappValue, timing, ...rest } = this; + + const zkapp = (() => { + if (!zkappValue) { + return undefined; + } + + const { state, verificationKey, zkappUri, isProven, ...rest } = zkappValue; + try { + return { + appState: state.values, + verificationKey: { + data: verificationKey.data, + hash: verificationKey.hash, + }, + zkappUri: zkappUri.data, + provedState: isProven, + zkappVersion: UInt32.empty(), + lastActionSlot: UInt32.empty(), + ...rest, + }; + } catch(e) { + console.error(e); + console.log(state); + throw e + } + })(); + + return { + publicKey: accountId.publicKey, + tokenId: accountId.tokenId.value, + tokenSymbol: tokenSymbol.symbol, + timing: { isTimed: Bool.empty(), ...timing }, + delegate: delegate ?? undefined, + zkapp: zkapp, + ...rest, + }; + } } diff --git a/src/lib/mina/v2/state.ts b/src/lib/mina/v2/state.ts index c28ab6beed..b93780db67 100644 --- a/src/lib/mina/v2/state.ts +++ b/src/lib/mina/v2/state.ts @@ -11,25 +11,25 @@ // TODO: there is a lot of duplication here on the generic representation that we can reduce -import { Empty, Eq, ProvableInstance, Update } from './core.js'; -import { Precondition } from './preconditions.js'; import { Bool } from '../../provable/bool.js'; import { Field } from '../../provable/field.js'; import { Provable } from '../../provable/provable.js'; import { Unconstrained } from '../../provable/types/unconstrained.js'; import { ZkappConstants } from '../v1/constants.js'; +import { Empty, Eq, ProvableInstance, Update } from './core.js'; +import { Precondition } from './preconditions.js'; export { - StateValues, GenericStatePreconditions, - StatePreconditions, + GenericStateUpdates, + State, StateDefinition, - StateUpdates, StateLayout, - GenericStateUpdates, StateMask, + StatePreconditions, StateReader, - State, + StateUpdates, + StateValues }; const { MAX_ZKAPP_STATE_FIELDS } = ZkappConstants; @@ -53,16 +53,6 @@ const CustomStateLayout = { const entriesOut = entriesIn.map(([key, T]) => [key, f(key, T as StateIn[typeof key])]); return Object.fromEntries(entriesOut); }, - - // mapToArray( - // Layout: State, - // f: (key: keyof State, value: State[typeof key]) => Out - // ): Out[] { - // const out: Out[] = []; - // const keys = Object.keys(Layout) as (keyof State)[]; - // keys.forEach((key) => out.push(f(key, Layout[key]))); - // return out; - // } }; type StateDefinition = State extends 'GenericState' @@ -608,7 +598,7 @@ class StateFieldsArray { } } -class GenericStateValues extends StateFieldsArray { +export class GenericStateValues extends StateFieldsArray { constructor(values: Field[]) { super(values, Field.empty); } diff --git a/src/lib/mina/v2/zkapp-logic.ts b/src/lib/mina/v2/zkapp-logic.ts index 5b4dbe6d18..871c178bc6 100644 --- a/src/lib/mina/v2/zkapp-logic.ts +++ b/src/lib/mina/v2/zkapp-logic.ts @@ -1,28 +1,33 @@ +import { Bool } from '../../provable/bool.js'; +import { PublicKey } from '../../provable/crypto/signature.js'; +import { Field } from '../../provable/field.js'; +import { Int64, Sign, UInt32, UInt64 } from '../../provable/int.js'; +import { ZkappConstants } from '../v1/constants.js'; 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, + Preconditions, } 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 }; +export { ApplyState, checkAndApplyAccountUpdate, checkAndApplyFeePayment }; type ApplyResult = ({ status: 'Applied' } & T) | { status: 'Failed'; errors: Error[] }; type ApplyState = { status: 'Alive'; value: T } | { status: 'Dead' }; +export const InitialApplyState: ApplyState = { + status: 'Alive', + value: Int64.zero, +}; + function updateApplyState( applyState: ApplyState, errors: Error[], @@ -42,13 +47,6 @@ function updateApplyState( } } -// 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( chain: ChainView, account: Account, @@ -90,7 +88,7 @@ function checkPreconditions( if (account.delegate !== null) checkPrecondition('delegate', preconditions.account.delegate, account.delegate); checkPrecondition('isProven', preconditions.account.isProven, account.zkapp.isProven); - checkPrecondition('isNew', preconditions.account.isNew, new Bool(account.isNew.get())); + // checkPrecondition('isNew', preconditions.account.isNew, new Bool(account.isNew.get())); StateValues.checkPreconditions(account.State, account.zkapp.state, preconditions.account.state); @@ -254,16 +252,12 @@ function applyUpdates( Sign.minusOne ); - feeExcessState = updateApplyState( - feeExcessState, - errors, - (feeExcess) => - tryAddInt64(feeExcess, accountCreationFee) ?? - new Error('fee excess underflowed due when subtracting the account creation fee') + feeExcessState = updateApplyState(feeExcessState, errors, (feeExcess) => + feeExcess.add(accountCreationFee) ); if (update.implicitAccountCreationFee.toBoolean()) { - const balanceChangeWithoutCreationFee = tryAddInt64(actualBalanceChange, accountCreationFee); + const balanceChangeWithoutCreationFee = actualBalanceChange.add(accountCreationFee); if (balanceChangeWithoutCreationFee === null) { errors.push( new Error('balance change underflowed when subtracting the account creation fee') @@ -275,14 +269,10 @@ function applyUpdates( } const balanceSigned = Int64.create(account.balance, Sign.one); - const updatedBalanceSigned = tryAddInt64(balanceSigned, actualBalanceChange); + const updatedBalanceSigned = balanceSigned.add(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()) { + if (updatedBalanceSigned.isNegative().toBoolean()) { errors.push(new Error('account balance was negative after applying balance change')); } else { updatedBalance = updatedBalanceSigned.magnitude; @@ -329,6 +319,8 @@ function checkAccountTiming( errors.push(new Error('account has an insufficient minimum balance after applying update')); } +export type AccountUpdateApplyResult = ReturnType; + // 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 @@ -347,12 +339,12 @@ function checkAndApplyAccountUpdate( 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})` - ) - ); + // 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)