diff --git a/src/bindings b/src/bindings index 83f8520624..3503101051 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 83f85206241c2fabd2be360acc5347bc104da452 +Subproject commit 35031010512993426bf226dbd6f1048fa1da09cc diff --git a/src/examples/zkapps/dex/upgradability.ts b/src/examples/zkapps/dex/upgradability.ts index 78c5c68379..d6cf13d6ad 100644 --- a/src/examples/zkapps/dex/upgradability.ts +++ b/src/examples/zkapps/dex/upgradability.ts @@ -185,7 +185,7 @@ async function atomicActionsTest({ withVesting }: { withVesting: boolean }) { }); await tx.prove(); await expect(tx.sign([feePayerKey, keys.dex]).send()).rejects.toThrow( - /Update_not_permitted_delegate/ + /Cannot update field 'delegate'/ ); /** diff --git a/src/lib/mina.ts b/src/lib/mina.ts index 46cacdc568..22c83fe9bb 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -43,6 +43,8 @@ import { type ActionStates, type NetworkConstants, } from './mina/mina-instance.js'; +import { SimpleLedger } from './mina/transaction-logic/ledger.js'; +import { assert } from './gadgets/common.js'; export { createTransaction, @@ -395,12 +397,10 @@ function LocalBlockchain({ if (enforceTransactionLimits) verifyTransactionLimits(txn.transaction); - for (const update of txn.transaction.accountUpdates) { - let accountJson = ledger.getAccount( - Ml.fromPublicKey(update.body.publicKey), - Ml.constFromField(update.body.tokenId) - ); + // create an ad-hoc ledger to record changes to accounts within the transaction + let simpleLedger = SimpleLedger.create(); + 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() @@ -411,9 +411,23 @@ function LocalBlockchain({ ); } - if (accountJson) { - let account = Account.fromJSON(accountJson); + let account = simpleLedger.load(update.body); + // 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); + simpleLedger.store(storedAccount); + account = storedAccount; + } + } + + // TODO: verify account update even if the account doesn't exist yet, using a default initial account + if (account !== undefined) { await verifyAccountUpdate( account, update, @@ -421,6 +435,7 @@ function LocalBlockchain({ this.proofsEnabled, this.getNetworkId() ); + simpleLedger.apply(update); } } @@ -1239,7 +1254,12 @@ async function verifyAccountUpdate( publicOutput: [], }; - let verificationKey = account.zkapp?.verificationKey?.data!; + let verificationKey = account.zkapp?.verificationKey?.data; + assert( + verificationKey !== undefined, + 'Account does not have a verification key' + ); + isValidProof = await verify(proof, verificationKey); if (!isValidProof) { throw Error( @@ -1247,7 +1267,7 @@ async function verifyAccountUpdate( ); } } catch (error) { - errorTrace += '\n\n' + (error as Error).message; + errorTrace += '\n\n' + (error as Error).stack; isValidProof = false; } } @@ -1261,7 +1281,7 @@ async function verifyAccountUpdate( networkId ); } catch (error) { - errorTrace += '\n\n' + (error as Error).message; + errorTrace += '\n\n' + (error as Error).stack; isValidSignature = false; } } @@ -1289,7 +1309,7 @@ async function verifyAccountUpdate( if (!verified) { throw Error( `Transaction verification failed: Cannot update field '${field}' because permission for this field is '${p}', but the required authorization was not provided or is invalid. - ${errorTrace !== '' ? 'Error trace: ' + errorTrace : ''}` + ${errorTrace !== '' ? 'Error trace: ' + errorTrace : ''}\n\n` ); } } diff --git a/src/lib/mina/account.ts b/src/lib/mina/account.ts index 1e66331cbc..1eb38eb214 100644 --- a/src/lib/mina/account.ts +++ b/src/lib/mina/account.ts @@ -13,12 +13,23 @@ import { jsLayout } from '../../bindings/mina-transaction/gen/js-layout.js'; import { ProvableExtended } from '../circuit_value.js'; export { FetchedAccount, Account, PartialAccount }; -export { accountQuery, parseFetchedAccount, fillPartialAccount }; +export { newAccount, accountQuery, parseFetchedAccount, fillPartialAccount }; type AuthRequired = Types.Json.AuthRequired; type Account = Types.Account; const Account = Types.Account; +function newAccount(accountId: { + publicKey: PublicKey; + tokenId?: Field; +}): Account { + let account = Account.empty(); + account.publicKey = accountId.publicKey; + account.tokenId = accountId.tokenId ?? Types.TokenId.empty(); + account.permissions = Permissions.initial(); + return account; +} + type PartialAccount = Omit, 'zkapp'> & { zkapp?: Partial; }; diff --git a/src/lib/mina/transaction-logic/apply.ts b/src/lib/mina/transaction-logic/apply.ts new file mode 100644 index 0000000000..52e38f2065 --- /dev/null +++ b/src/lib/mina/transaction-logic/apply.ts @@ -0,0 +1,30 @@ +/** + * Apply transactions to a ledger of accounts. + */ +import { type AccountUpdate } from '../../account_update.js'; +import { Account } from '../account.js'; + +export { applyAccountUpdate }; + +/** + * Apply a single account update to update an account. + * + * TODO: + * - This must receive and return some context global to the transaction, to check validity + * - Should operate on the value / bigint type, not the provable type + */ +function applyAccountUpdate(account: Account, update: AccountUpdate): Account { + account.publicKey.assertEquals(update.publicKey); + account.tokenId.assertEquals(update.tokenId, 'token id mismatch'); + + // clone account (TODO: do this efficiently) + let json = Account.toJSON(account); + account = Account.fromJSON(json); + + // update permissions + if (update.update.permissions.isSome.toBoolean()) { + account.permissions = update.update.permissions.value; + } + + return account; +} diff --git a/src/lib/mina/transaction-logic/ledger.ts b/src/lib/mina/transaction-logic/ledger.ts new file mode 100644 index 0000000000..266054ca50 --- /dev/null +++ b/src/lib/mina/transaction-logic/ledger.ts @@ -0,0 +1,60 @@ +/** + * A ledger of accounts - simple model of a local blockchain. + */ +import { PublicKey } from '../../signature.js'; +import { type AccountUpdate, TokenId } from '../../account_update.js'; +import { Account, newAccount } from '../account.js'; +import { Field } from '../../field.js'; +import { applyAccountUpdate } from './apply.js'; + +export { SimpleLedger }; + +class SimpleLedger { + accounts: Map; + + constructor() { + this.accounts = new Map(); + } + + static create(): SimpleLedger { + return new SimpleLedger(); + } + + exists({ publicKey, tokenId = TokenId.default }: InputAccountId): boolean { + return this.accounts.has(accountId({ publicKey, tokenId })); + } + + store(account: Account): void { + this.accounts.set(accountId(account), account); + } + + load({ + publicKey, + tokenId = TokenId.default, + }: InputAccountId): Account | undefined { + let id = accountId({ publicKey, tokenId }); + let account = this.accounts.get(id); + return account; + } + + apply(update: AccountUpdate): void { + let id = accountId(update.body); + let account = this.accounts.get(id); + account ??= newAccount(update.body); + + let updated = applyAccountUpdate(account, update); + this.accounts.set(id, updated); + } +} + +type AccountId = { publicKey: PublicKey; tokenId: Field }; +type InputAccountId = { publicKey: PublicKey; tokenId?: Field }; + +function accountId(account: AccountId): bigint { + let id = account.publicKey.x.toBigInt(); + id <<= 1n; + id |= BigInt(account.publicKey.isOdd.toBoolean()); + id <<= BigInt(Field.sizeInBits); + id |= account.tokenId.toBigInt(); + return id; +} diff --git a/src/mina b/src/mina index b1b443ffdc..a5c7f667a5 160000 --- a/src/mina +++ b/src/mina @@ -1 +1 @@ -Subproject commit b1b443ffdc15ffd8569f2c244ecdeb5029c35097 +Subproject commit a5c7f667a5008c15243f28921505c3930a4fdf35