diff --git a/.github/workflows/live-tests.yml b/.github/workflows/live-tests.yml index d6c3f72ff4..958c9bacfd 100644 --- a/.github/workflows/live-tests.yml +++ b/.github/workflows/live-tests.yml @@ -14,7 +14,7 @@ on: jobs: main-branch: - timeout-minutes: 25 + timeout-minutes: 45 runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.base_ref == 'main') services: @@ -40,7 +40,7 @@ jobs: mina-branch-name: o1js-main berkeley-branch: - timeout-minutes: 25 + timeout-minutes: 45 runs-on: ubuntu-latest if: github.ref == 'refs/heads/berkeley' || (github.event_name == 'pull_request' && github.base_ref == 'berkeley') services: @@ -66,7 +66,7 @@ jobs: mina-branch-name: berkeley develop-branch: - timeout-minutes: 25 + timeout-minutes: 45 runs-on: ubuntu-latest if: github.ref == 'refs/heads/develop' || (github.event_name == 'pull_request' && github.base_ref == 'develop') services: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ca42cd21a..d6b9b5cbf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased](https://github.com/o1-labs/o1js/compare/834a44002...HEAD) +### Breaking changes + +- Remove `AccountUpdate.children` and `AccountUpdate.parent` properties https://github.com/o1-labs/o1js/pull/1402 + - Also removes the optional `AccountUpdatesLayout` argument to `approve()` + - Adds `AccountUpdateTree` and `AccountUpdateForest`, new classes that represent a layout of account updates explicitly + - Both of the new types are now accepted as inputs to `approve()` + - `accountUpdate.extractTree()` to obtain the tree associated with an account update in the current transaction context. + ### Added - `MerkleList` to enable provable operations on a dynamically-sized list https://github.com/o1-labs/o1js/pull/1398 diff --git a/src/examples/zkapps/token_with_proofs.ts b/src/examples/zkapps/token_with_proofs.ts index b685c8610c..556354db00 100644 --- a/src/examples/zkapps/token_with_proofs.ts +++ b/src/examples/zkapps/token_with_proofs.ts @@ -56,10 +56,8 @@ class TokenContract extends SmartContract { receiverAddress: PublicKey, callback: Experimental.Callback ) { - let senderAccountUpdate = this.approve( - callback, - AccountUpdate.Layout.AnyChildren - ); + // TODO use token contract methods for approve + let senderAccountUpdate = this.approve(callback) as AccountUpdate; let amount = UInt64.from(1_000); let negativeAmount = Int64.fromObject( senderAccountUpdate.body.balanceChange diff --git a/src/index.ts b/src/index.ts index 3d3e7ca924..97240671ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,6 +76,7 @@ export { ZkappPublicInput, TransactionVersion, AccountUpdateForest, + AccountUpdateTree, } from './lib/account_update.js'; export { TokenAccountUpdateIterator } from './lib/mina/token/forest-iterator.js'; diff --git a/src/lib/account_update.ts b/src/lib/account_update.ts index 8595b762a6..bc5ec31f9c 100644 --- a/src/lib/account_update.ts +++ b/src/lib/account_update.ts @@ -3,8 +3,7 @@ import { FlexibleProvable, provable, provablePure, - Struct, - Unconstrained, + StructNoJson, } from './circuit_value.js'; import { memoizationContext, memoizeWitness, Provable } from './provable.js'; import { Field, Bool } from './core.js'; @@ -35,32 +34,37 @@ import { Actions, } from '../bindings/mina-transaction/transaction-leaves.js'; import { TokenId as Base58TokenId } from './base58-encodings.js'; -import { - hashWithPrefix, - packToFields, - Poseidon, - ProvableHashable, -} from './hash.js'; +import { hashWithPrefix, packToFields, Poseidon } from './hash.js'; import { mocks, prefixes, protocolVersions, } from '../bindings/crypto/constants.js'; -import { Context } from './global-context.js'; import { MlArray } from './ml/base.js'; import { Signature, signFieldElement } from '../mina-signer/src/signature.js'; import { MlFieldConstArray } from './ml/fields.js'; -import { transactionCommitments } from '../mina-signer/src/sign-zkapp-command.js'; +import { + accountUpdatesToCallForest, + CallForest, + callForestHashGeneric, + transactionCommitments, +} from '../mina-signer/src/sign-zkapp-command.js'; import { currentTransaction } from './mina/transaction-context.js'; import { isSmartContract } from './mina/smart-contract-base.js'; import { activeInstance } from './mina/mina-instance.js'; import { + emptyHash, genericHash, MerkleList, MerkleListBase, - withHashes, } from './provable-types/merkle-list.js'; import { Hashed } from './provable-types/packed.js'; +import { + accountUpdateLayout, + smartContractContext, +} from './mina/smart-contract-context.js'; +import { assert } from './util/assert.js'; +import { RandomId } from './provable-types/auxiliary.js'; // external API export { @@ -69,10 +73,10 @@ export { ZkappPublicInput, TransactionVersion, AccountUpdateForest, + AccountUpdateTree, }; // internal API export { - smartContractContext, SetOrKeep, Permission, Preconditions, @@ -82,40 +86,25 @@ export { ZkappCommand, addMissingSignatures, addMissingProofs, - ZkappStateLength, Events, Actions, TokenId, Token, CallForest, createChildAccountUpdate, - AccountUpdatesLayout, zkAppProver, - SmartContractContext, dummySignature, LazyProof, - AccountUpdateTree, - UnfinishedForest, + AccountUpdateTreeBase, + AccountUpdateLayout, hashAccountUpdate, HashedAccountUpdate, }; -const ZkappStateLength = 8; - const TransactionVersion = { current: () => UInt32.from(protocolVersions.txnVersion), }; -type SmartContractContext = { - this: SmartContract; - methodCallDepth: number; - selfUpdate: AccountUpdate; - selfCalls: UnfinishedForest; -}; -let smartContractContext = Context.create({ - default: null, -}); - type ZkappProverData = { transaction: ZkappCommand; accountUpdate: AccountUpdate; @@ -620,18 +609,6 @@ class AccountUpdate implements Types.AccountUpdate { account: Account; network: Network; currentSlot: CurrentSlot; - children: { - callsType: - | { type: 'None' } - | { type: 'Witness' } - | { type: 'Equals'; value: Field } - | { type: 'WitnessEquals'; value: Field }; - accountUpdates: AccountUpdate[]; - } = { - callsType: { type: 'None' }, - accountUpdates: [], - }; - parent: AccountUpdate | undefined = undefined; private isSelf: boolean; @@ -661,13 +638,8 @@ class AccountUpdate implements Types.AccountUpdate { accountUpdate.isSelf ); cloned.lazyAuthorization = accountUpdate.lazyAuthorization; - cloned.children.callsType = accountUpdate.children.callsType; - cloned.children.accountUpdates = accountUpdate.children.accountUpdates.map( - AccountUpdate.clone - ); cloned.id = accountUpdate.id; cloned.label = accountUpdate.label; - cloned.parent = accountUpdate.parent; return cloned; } @@ -690,7 +662,7 @@ class AccountUpdate implements Types.AccountUpdate { } if (accountLike instanceof PublicKey) { accountLike = AccountUpdate.defaultAccountUpdate(accountLike, id); - makeChildAccountUpdate(thisAccountUpdate, accountLike); + thisAccountUpdate.approve(accountLike); } if (!accountLike.label) accountLike.label = `${ @@ -814,34 +786,25 @@ class AccountUpdate implements Types.AccountUpdate { } /** - * Makes an {@link AccountUpdate} a child-{@link AccountUpdate} of this and - * approves it. + * Makes another {@link AccountUpdate} a child of this one. + * + * The parent-child relationship means that the child becomes part of the "statement" + * of the parent, and goes into the commitment that is authorized by either a signature + * or a proof. + * + * For a proof in particular, child account updates are contained in the public input + * of the proof that authorizes the parent account update. */ - approve( - childUpdate: AccountUpdate, - layout: AccountUpdatesLayout = AccountUpdate.Layout.NoChildren - ) { - makeChildAccountUpdate(this, childUpdate); - AccountUpdate.witnessChildren(childUpdate, layout, { skipCheck: true }); - - // TODO: this is not as general as approve suggests - let insideContract = smartContractContext.get(); - if (insideContract && insideContract.selfUpdate.id === this.id) { - UnfinishedForest.push(insideContract.selfCalls, childUpdate); + approve(child: AccountUpdate | AccountUpdateTree | AccountUpdateForest) { + if (child instanceof AccountUpdateForest) { + accountUpdateLayout()?.setChildren(this, child); + return; } - } - - /** - * Makes an {@link AccountUpdate} a child-{@link AccountUpdate} of this. - */ - adopt(childUpdate: AccountUpdate) { - makeChildAccountUpdate(this, childUpdate); - - // TODO: this is not as general as adopt suggests - let insideContract = smartContractContext.get(); - if (insideContract && insideContract.selfUpdate.id === this.id) { - UnfinishedForest.push(insideContract.selfCalls, childUpdate); + if (child instanceof AccountUpdate) { + child.body.callDepth = this.body.callDepth + 1; } + accountUpdateLayout()?.disattach(child); + accountUpdateLayout()?.pushChild(this, child); } get balance() { @@ -1020,17 +983,14 @@ class AccountUpdate implements Types.AccountUpdate { if (isSameAsFeePayer) nonce++; // now, we check how often this account update already updated its nonce in // this tx, and increase nonce from `getAccount` by that amount - CallForest.forEachPredecessor( - currentTransaction.get().accountUpdates, - update as AccountUpdate, - (otherUpdate) => { - let shouldIncreaseNonce = otherUpdate.publicKey - .equals(publicKey) - .and(otherUpdate.tokenId.equals(tokenId)) - .and(otherUpdate.body.incrementNonce); - if (shouldIncreaseNonce.toBoolean()) nonce++; - } - ); + let layout = currentTransaction()?.layout; + layout?.forEachPredecessor(update as AccountUpdate, (otherUpdate) => { + let shouldIncreaseNonce = otherUpdate.publicKey + .equals(publicKey) + .and(otherUpdate.tokenId.equals(tokenId)) + .and(otherUpdate.body.incrementNonce); + if (shouldIncreaseNonce.toBoolean()) nonce++; + }); return { nonce: UInt32.from(nonce), isSameAsFeePayer: Bool(isSameAsFeePayer), @@ -1065,34 +1025,49 @@ class AccountUpdate implements Types.AccountUpdate { } } - toPublicInput(): ZkappPublicInput { + toPublicInput({ + accountUpdates, + }: { + accountUpdates: AccountUpdate[]; + }): ZkappPublicInput { let accountUpdate = this.hash(); - let calls = CallForest.hashChildren(this); + + // collect this update's descendants + let descendants: AccountUpdate[] = []; + let callDepth = this.body.callDepth; + let i = accountUpdates.findIndex((a) => a.id === this.id); + assert(i !== -1, 'Account update not found in transaction'); + for (i++; i < accountUpdates.length; i++) { + let update = accountUpdates[i]; + if (update.body.callDepth <= callDepth) break; + descendants.push(update); + } + + // call forest hash + let forest = accountUpdatesToCallForest(descendants, callDepth + 1); + let calls = callForestHashGeneric( + forest, + (a) => a.hash(), + Poseidon.hashWithPrefix, + emptyHash + ); return { accountUpdate, calls }; } toPrettyLayout() { - let indent = 0; - let layout = ''; - let i = 0; - - let print = (a: AccountUpdate) => { - layout += - ' '.repeat(indent) + - `AccountUpdate(${i}, ${a.label || ''}, ${ - a.children.callsType.type - })` + - '\n'; - i++; - indent += 2; - for (let child of a.children.accountUpdates) { - print(child); - } - indent -= 2; - }; + let node = accountUpdateLayout()?.get(this); + assert(node !== undefined, 'AccountUpdate not found in layout'); + node.children.print(); + } - print(this); - return layout; + extractTree(): AccountUpdateTree { + let layout = accountUpdateLayout(); + let hash = layout?.get(this)?.final?.hash; + let id = this.id; + let children = + layout?.finalizeAndRemove(this) ?? AccountUpdateForest.empty(); + let accountUpdate = HashedAccountUpdate.hash(this, hash); + return new AccountUpdateTree({ accountUpdate, id, children }); } static defaultAccountUpdate(address: PublicKey, tokenId?: Field) { @@ -1138,8 +1113,8 @@ class AccountUpdate implements Types.AccountUpdate { self.label || 'Unlabeled' } > AccountUpdate.create()`; } else { - currentTransaction()?.accountUpdates.push(accountUpdate); - accountUpdate.label = `Mina.transaction > AccountUpdate.create()`; + currentTransaction()?.layout.pushTopLevel(accountUpdate); + accountUpdate.label = `Mina.transaction() > AccountUpdate.create()`; } return accountUpdate; } @@ -1158,30 +1133,14 @@ class AccountUpdate implements Types.AccountUpdate { insideContract.this.self.approve(accountUpdate); } else { if (!currentTransaction.has()) return; - let updates = currentTransaction.get().accountUpdates; - if (!updates.find((update) => update.id === accountUpdate.id)) { - updates.push(accountUpdate); - } + currentTransaction.get().layout.pushTopLevel(accountUpdate); } } /** * Disattach an account update from where it's currently located in the transaction */ static unlink(accountUpdate: AccountUpdate) { - // TODO duplicate logic - let insideContract = smartContractContext.get(); - if (insideContract) { - UnfinishedForest.remove(insideContract.selfCalls, accountUpdate); - } - let siblings = - accountUpdate.parent?.children.accountUpdates ?? - currentTransaction()?.accountUpdates; - if (siblings === undefined) return; - let i = siblings?.findIndex((update) => update.id === accountUpdate.id); - if (i !== undefined && i !== -1) { - siblings!.splice(i, 1); - } - accountUpdate.parent === undefined; + accountUpdateLayout()?.disattach(accountUpdate); } /** @@ -1266,21 +1225,10 @@ class AccountUpdate implements Types.AccountUpdate { static toFields = Types.AccountUpdate.toFields; static toAuxiliary(a?: AccountUpdate) { let aux = Types.AccountUpdate.toAuxiliary(a); - let children: AccountUpdate['children'] = { - callsType: { type: 'None' }, - accountUpdates: [], - }; let lazyAuthorization = a && a.lazyAuthorization; - if (a) { - children.callsType = a.children.callsType; - children.accountUpdates = a.children.accountUpdates.map( - AccountUpdate.clone - ); - } - let parent = a?.parent; let id = a?.id ?? Math.random(); let label = a?.label ?? ''; - return [{ lazyAuthorization, children, parent, id, label }, aux]; + return [{ lazyAuthorization, id, label }, aux]; } static toInput = Types.AccountUpdate.toInput; static empty() { @@ -1311,111 +1259,6 @@ class AccountUpdate implements Types.AccountUpdate { return Provable.witness(combinedType, compute); } - static witnessChildren( - accountUpdate: AccountUpdate, - childLayout: AccountUpdatesLayout, - options?: { skipCheck: boolean } - ) { - // just witness children's hash if childLayout === null - if (childLayout === AccountUpdate.Layout.AnyChildren) { - accountUpdate.children.callsType = { type: 'Witness' }; - return; - } - if (childLayout === AccountUpdate.Layout.NoDelegation) { - accountUpdate.children.callsType = { type: 'Witness' }; - accountUpdate.body.mayUseToken.parentsOwnToken.assertFalse(); - accountUpdate.body.mayUseToken.inheritFromParent.assertFalse(); - return; - } - accountUpdate.children.callsType = { type: 'None' }; - let childArray: AccountUpdatesLayout[] = - typeof childLayout === 'number' - ? Array(childLayout).fill(AccountUpdate.Layout.NoChildren) - : childLayout; - let n = childArray.length; - for (let i = 0; i < n; i++) { - accountUpdate.children.accountUpdates[i] = AccountUpdate.witnessTree( - provable(null), - childArray[i], - () => ({ - accountUpdate: - accountUpdate.children.accountUpdates[i] ?? AccountUpdate.dummy(), - result: null, - }), - options - ).accountUpdate; - } - if (n === 0) { - accountUpdate.children.callsType = { - type: 'Equals', - value: CallForest.emptyHash(), - }; - } - } - - /** - * Like AccountUpdate.witness, but lets you specify a layout for the - * accountUpdate's children, which also get witnessed - */ - static witnessTree( - resultType: FlexibleProvable, - childLayout: AccountUpdatesLayout, - compute: () => { - accountUpdate: AccountUpdate; - result: T; - }, - options?: { skipCheck: boolean } - ) { - // witness the root accountUpdate - let { accountUpdate, result } = AccountUpdate.witness( - resultType, - compute, - options - ); - // witness child account updates - AccountUpdate.witnessChildren(accountUpdate, childLayout, options); - return { accountUpdate, result }; - } - - /** - * Describes the children of an account update, which are laid out in a tree. - * - * The tree layout is described recursively by using a combination of `AccountUpdate.Layout.NoChildren`, `AccountUpdate.Layout.StaticChildren(...)` and `AccountUpdate.Layout.AnyChildren`. - * - `NoChildren` means an account update that can't have children - * - `AnyChildren` means an account update can have an arbitrary amount of children, which means you can't access those children in your circuit (because the circuit is static). - * - `StaticChildren` means the account update must have a certain static amount of children and expects as arguments a description of each of those children. - * As a shortcut, you can also pass `StaticChildren` a number, which means it has that amount of children but no grandchildren. - * - * This is best understood by examples: - * - * ```ts - * let { NoChildren, AnyChildren, StaticChildren } = AccounUpdate.Layout; - * - * NoChildren // an account update with no children - * AnyChildren // an account update with arbitrary children - * StaticChildren(NoChildren) // an account update with 1 child, which doesn't have children itself - * StaticChildren(1) // shortcut for StaticChildren(NoChildren) - * StaticChildren(2) // shortcut for StaticChildren(NoChildren, NoChildren) - * StaticChildren(0) // equivalent to NoChildren - * - * // an update with 2 children, of which one has arbitrary children and the other has exactly 1 descendant - * StaticChildren(AnyChildren, StaticChildren(1)) - * ``` - */ - static Layout = { - StaticChildren: ((...args: any[]) => { - if (args.length === 1 && typeof args[0] === 'number') return args[0]; - if (args.length === 0) return 0; - return args; - }) as { - (n: number): AccountUpdatesLayout; - (...args: AccountUpdatesLayout[]): AccountUpdatesLayout; - }, - NoChildren: 0, - AnyChildren: 'AnyChildren' as const, - NoDelegation: 'NoDelegation' as const, - }; - static get MayUseToken() { return { type: provablePure({ parentsOwnToken: Bool, inheritFromParent: Bool }), @@ -1541,12 +1384,6 @@ class AccountUpdate implements Types.AccountUpdate { } } -type AccountUpdatesLayout = - | number - | 'AnyChildren' - | 'NoDelegation' - | AccountUpdatesLayout[]; - // call forest stuff function hashAccountUpdate(update: AccountUpdate) { @@ -1558,13 +1395,17 @@ class HashedAccountUpdate extends Hashed.create( hashAccountUpdate ) {} -type AccountUpdateTree = { +type AccountUpdateTreeBase = { + id: number; accountUpdate: Hashed; - calls: MerkleListBase; + children: AccountUpdateForestBase; }; -const AccountUpdateTree: ProvableHashable = Struct({ +type AccountUpdateForestBase = MerkleListBase; + +const AccountUpdateTreeBase = StructNoJson({ + id: RandomId, accountUpdate: HashedAccountUpdate.provable, - calls: MerkleListBase(), + children: MerkleListBase(), }); /** @@ -1576,55 +1417,124 @@ const AccountUpdateTree: ProvableHashable = Struct({ * type AccountUpdateForest = MerkleList; * type AccountUpdateTree = { * accountUpdate: Hashed; - * calls: AccountUpdateForest; + * children: AccountUpdateForest; * }; * ``` */ class AccountUpdateForest extends MerkleList.create( - AccountUpdateTree, + AccountUpdateTreeBase, merkleListHash ) { - static fromArray( - updates: AccountUpdate[], - { skipDummies = false } = {} - ): AccountUpdateForest { - if (skipDummies) return AccountUpdateForest.fromArraySkipDummies(updates); + static fromFlatArray(updates: AccountUpdate[]): AccountUpdateForest { + let simpleForest = accountUpdatesToCallForest(updates); + return this.fromSimpleForest(simpleForest); + } + static toFlatArray( + forest: AccountUpdateForestBase, + mutate = true, + depth = 0 + ) { + let flat: AccountUpdate[] = []; + for (let { element: tree } of forest.data.get()) { + let update = tree.accountUpdate.value.get(); + if (mutate) update.body.callDepth = depth; + flat.push(update); + flat.push(...this.toFlatArray(tree.children, mutate, depth + 1)); + } + return flat; + } - let nodes = updates.map((update) => { - let accountUpdate = HashedAccountUpdate.hash(update); - let calls = AccountUpdateForest.fromArray(update.children.accountUpdates); - return { accountUpdate, calls }; + private static fromSimpleForest( + simpleForest: CallForest + ): AccountUpdateForest { + let nodes = simpleForest.map((node) => { + let accountUpdate = HashedAccountUpdate.hash(node.accountUpdate); + let children = AccountUpdateForest.fromSimpleForest(node.children); + return { accountUpdate, children, id: node.accountUpdate.id }; }); return AccountUpdateForest.from(nodes); } - private static fromArraySkipDummies( - updates: AccountUpdate[] - ): AccountUpdateForest { - let forest = AccountUpdateForest.empty(); + // TODO this comes from paranoia and might be removed later + static assertConstant(forest: AccountUpdateForestBase) { + Provable.asProver(() => { + forest.data.get().forEach(({ element: tree }) => { + assert( + Provable.isConstant(AccountUpdate, tree.accountUpdate.value.get()), + 'account update not constant' + ); + AccountUpdateForest.assertConstant(tree.children); + }); + }); + } +} - for (let update of [...updates].reverse()) { - let accountUpdate = HashedAccountUpdate.hash(update); - let calls = AccountUpdateForest.fromArraySkipDummies( - update.children.accountUpdates +/** + * Class which represents a tree of account updates, + * in a compressed way which allows iterating and selectively witnessing the account updates. + * + * The (recursive) type signature is: + * ``` + * type AccountUpdateTree = { + * accountUpdate: Hashed; + * children: AccountUpdateForest; + * }; + * type AccountUpdateForest = MerkleList; + * ``` + */ +class AccountUpdateTree extends StructNoJson({ + id: RandomId, + accountUpdate: HashedAccountUpdate.provable, + children: AccountUpdateForest.provable, +}) { + /** + * Create a tree of account updates which only consists of a root. + */ + static from(update: AccountUpdate | AccountUpdateTree, hash?: Field) { + if (update instanceof AccountUpdateTree) return update; + return new AccountUpdateTree({ + accountUpdate: HashedAccountUpdate.hash(update, hash), + id: update.id, + children: AccountUpdateForest.empty(), + }); + } + + /** + * Add an {@link AccountUpdate} or {@link AccountUpdateTree} to the children of this tree's root. + * + * See {@link AccountUpdate.approve}. + */ + approve(update: AccountUpdate | AccountUpdateTree, hash?: Field) { + accountUpdateLayout()?.disattach(update); + if (update instanceof AccountUpdate) { + this.children.pushIf( + update.isDummy().not(), + AccountUpdateTree.from(update, hash) ); - forest.pushIf(update.isDummy().not(), { accountUpdate, calls }); + } else { + this.children.push(update); } + } - return forest; + // fix Struct type + static fromFields(fields: Field[], aux: any) { + return new AccountUpdateTree(super.fromFields(fields, aux)); + } + static empty() { + return new AccountUpdateTree(super.empty()); } } // how to hash a forest -function merkleListHash(forestHash: Field, tree: AccountUpdateTree) { +function merkleListHash(forestHash: Field, tree: AccountUpdateTreeBase) { return hashCons(forestHash, hashNode(tree)); } -function hashNode(tree: AccountUpdateTree) { +function hashNode(tree: AccountUpdateTreeBase) { return Poseidon.hashWithPrefix(prefixes.accountUpdateNode, [ tree.accountUpdate.hash, - tree.calls.hash, + tree.children.hash, ]); } function hashCons(forestHash: Field, nodeHash: Field) { @@ -1635,240 +1545,359 @@ function hashCons(forestHash: Field, nodeHash: Field) { } /** - * Structure for constructing the forest of child account updates, from a circuit. + * `UnfinishedForest` / `UnfinishedTree` are structures for constructing the forest of child account updates from a circuit. * * The circuit can mutate account updates and change their array of children, so here we can't hash * everything immediately. Instead, we maintain a structure consisting of either hashes or full account * updates that can be hashed into a final call forest at the end. + * + * `UnfinishedForest` and `UnfinishedTree` behave like a tagged enum type: + * ``` + * type UnfinishedForest = + * | Mutable of UnfinishedTree[] + * | Final of AccountUpdateForest; + * + * type UnfinishedTree = ( + * | Mutable of AccountUpdate + * | Final of HashedAccountUpdate + * ) & { children: UnfinishedForest, ... } + * ``` */ -type UnfinishedForest = HashOrValue; - type UnfinishedTree = { - accountUpdate: HashOrValue; + id: number; isDummy: Bool; - calls: UnfinishedForest; + // `children` must be readonly since it's referenced in each child's siblings + readonly children: UnfinishedForest; + siblings?: UnfinishedForest; +} & ( + | { final: HashedAccountUpdate; mutable?: undefined } + | { final?: undefined; mutable: AccountUpdate } +); + +type UnfinishedForestFinal = UnfinishedForest & { + final: AccountUpdateForest; + mutable?: undefined; }; -type HashOrValue = - | { readonly useHash: true; hash: Field; readonly value: T } - | { readonly useHash: false; value: T }; - -const UnfinishedForest = { - empty(): UnfinishedForest { - return { useHash: false, value: [] }; - }, - - witnessHash(forest: UnfinishedForest): UnfinishedForest { - let hash = Provable.witness(Field, () => { - return UnfinishedForest.finalize(forest).hash; - }); - return { useHash: true, hash, value: forest.value }; - }, - - fromArray(updates: AccountUpdate[], useHash = false): UnfinishedForest { - if (useHash) { - let forest = UnfinishedForest.empty(); - Provable.asProver(() => (forest = UnfinishedForest.fromArray(updates))); - return UnfinishedForest.witnessHash(forest); - } +type UnfinishedForestMutable = UnfinishedForest & { + final?: undefined; + mutable: UnfinishedTree[]; +}; - let nodes = updates.map((update): UnfinishedTree => { - return { - accountUpdate: { useHash: false, value: update }, - isDummy: update.isDummy(), - calls: UnfinishedForest.fromArray(update.children.accountUpdates), - }; - }); - return { useHash: false, value: nodes }; - }, +class UnfinishedForest { + final?: AccountUpdateForest; + mutable?: UnfinishedTree[]; - push( - forest: UnfinishedForest, - accountUpdate: AccountUpdate, - calls?: UnfinishedForest - ) { - forest.value.push({ - accountUpdate: { useHash: false, value: accountUpdate }, - isDummy: accountUpdate.isDummy(), - calls: calls ?? UnfinishedForest.empty(), - }); - }, + isFinal(): this is UnfinishedForestFinal { + return this.final !== undefined; + } + isMutable(): this is UnfinishedForestMutable { + return this.mutable !== undefined; + } - remove(forest: UnfinishedForest, accountUpdate: AccountUpdate) { - // find account update by .id - let index = forest.value.findIndex( - (node) => node.accountUpdate.value.id === accountUpdate.id + constructor(mutable?: UnfinishedTree[], final?: AccountUpdateForest) { + assert( + (final === undefined) !== (mutable === undefined), + 'final or mutable' ); + this.final = final; + this.mutable = mutable; + } - // nothing to do if it's not there - if (index === -1) return; + static empty(): UnfinishedForestMutable { + return new UnfinishedForest([]) as any; + } - // remove it - forest.value.splice(index, 1); - }, + private setFinal(final: AccountUpdateForest): UnfinishedForestFinal { + return Object.assign(this, { final, mutable: undefined }); + } - finalize(forest: UnfinishedForest): AccountUpdateForest { - if (forest.useHash) { - let data = Unconstrained.witness(() => - UnfinishedForest.finalize({ ...forest, useHash: false }).data.get() - ); - return new AccountUpdateForest({ hash: forest.hash, data }); - } + finalize(): AccountUpdateForest { + if (this.isFinal()) return this.final; + assert(this.isMutable(), 'final or mutable'); - // not using the hash means we calculate it in-circuit - let nodes = forest.value.map(toTree); + let nodes = this.mutable.map(UnfinishedTree.finalize); let finalForest = AccountUpdateForest.empty(); for (let { isDummy, ...tree } of [...nodes].reverse()) { finalForest.pushIf(isDummy.not(), tree); } + this.setFinal(finalForest); return finalForest; - }, -}; + } -function toTree(node: UnfinishedTree): AccountUpdateTree & { isDummy: Bool } { - let accountUpdate = node.accountUpdate.useHash - ? new HashedAccountUpdate( - node.accountUpdate.hash, - Unconstrained.from(node.accountUpdate.value) - ) - : HashedAccountUpdate.hash(node.accountUpdate.value); + witnessHash(): UnfinishedForestFinal { + let final = Provable.witness(AccountUpdateForest.provable, () => + this.finalize() + ); + return this.setFinal(final); + } - let calls = UnfinishedForest.finalize(node.calls); - return { accountUpdate, isDummy: node.isDummy, calls }; -} + push(node: UnfinishedTree) { + if (node.siblings === this) return; + assert( + node.siblings === undefined, + 'Cannot push node that already has a parent.' + ); + node.siblings = this; + assert(this.isMutable(), 'Cannot push to an immutable forest'); + this.mutable.push(node); + } -const CallForest = { - // similar to Mina_base.ZkappCommand.Call_forest.to_account_updates_list - // takes a list of accountUpdates, which each can have children, so they form a "forest" (list of trees) - // returns a flattened list, with `accountUpdate.body.callDepth` specifying positions in the forest - // also removes any "dummy" accountUpdates - toFlatList( - forest: AccountUpdate[], - mutate = true, - depth = 0 - ): AccountUpdate[] { - let accountUpdates = []; - for (let accountUpdate of forest) { - if (accountUpdate.isDummy().toBoolean()) continue; - if (mutate) accountUpdate.body.callDepth = depth; - let children = accountUpdate.children.accountUpdates; - accountUpdates.push( - accountUpdate, - ...CallForest.toFlatList(children, mutate, depth + 1) + remove(node: UnfinishedTree) { + assert(this.isMutable(), 'Cannot remove from an immutable forest'); + // find by .id + let index = this.mutable.findIndex((n) => n.id === node.id); + + // nothing to do if it's not there + if (index === -1) return; + + // remove it + node.siblings = undefined; + this.mutable.splice(index, 1); + } + + setToForest(forest: AccountUpdateForestBase) { + if (this.isMutable()) { + assert( + this.mutable.length === 0, + 'Replacing a mutable forest that has existing children might be a mistake.' ); } - return accountUpdates; - }, + return this.setFinal(new AccountUpdateForest(forest)); + } - // Mina_base.Zkapp_command.Digest.Forest.empty - emptyHash() { - return Field(0); - }, + static fromForest(forest: AccountUpdateForestBase) { + return UnfinishedForest.empty().setToForest(forest); + } - // similar to Mina_base.Zkapp_command.Call_forest.accumulate_hashes - // hashes a accountUpdate's children (and their children, and ...) to compute - // the `calls` field of ZkappPublicInput - hashChildren(update: AccountUpdate): Field { - if (!Provable.inCheckedComputation()) { - return CallForest.hashChildrenBase(update); + toFlatArray(mutate = true, depth = 0): AccountUpdate[] { + if (this.isFinal()) + return AccountUpdateForest.toFlatArray(this.final, mutate, depth); + assert(this.isMutable(), 'final or mutable'); + let flatUpdates: AccountUpdate[] = []; + for (let node of this.mutable) { + if (node.isDummy.toBoolean()) continue; + let update = node.mutable ?? node.final.value.get(); + if (mutate) update.body.callDepth = depth; + let children = node.children.toFlatArray(mutate, depth + 1); + flatUpdates.push(update, ...children); } + return flatUpdates; + } - let { callsType } = update.children; - // compute hash outside the circuit if callsType is "Witness" - // i.e., allowing accountUpdates with arbitrary children - if (callsType.type === 'Witness') { - return Provable.witness(Field, () => CallForest.hashChildrenBase(update)); + toConstantInPlace() { + if (this.isFinal()) { + this.final.hash = this.final.hash.toConstant(); + return; } - if (callsType.type === 'WitnessEquals') { - return callsType.value; + assert(this.isMutable(), 'final or mutable'); + for (let node of this.mutable) { + if (node.mutable !== undefined) { + node.mutable = Provable.toConstant(AccountUpdate, node.mutable); + } else { + node.final.hash = node.final.hash.toConstant(); + } + node.isDummy = Provable.toConstant(Bool, node.isDummy); + node.children.toConstantInPlace(); } - let calls = CallForest.hashChildrenBase(update); - if (callsType.type === 'Equals') { - calls.assertEquals(callsType.value); + } + + print() { + let indent = 0; + let layout = ''; + + let toPretty = (a: UnfinishedForest) => { + if (a.isFinal()) { + layout += ' '.repeat(indent) + ' ( finalized forest )\n'; + return; + } + assert(a.isMutable(), 'final or mutable'); + indent += 2; + for (let tree of a.mutable) { + let label = tree.mutable?.label || ''; + if (tree.final !== undefined) { + Provable.asProver(() => (label = tree.final!.value.get().label)); + } + layout += ' '.repeat(indent) + `( ${label} )` + '\n'; + toPretty(tree.children); + } + indent -= 2; + }; + + toPretty(this); + console.log(layout); + } +} + +const UnfinishedTree = { + create(update: AccountUpdate | AccountUpdateTree): UnfinishedTree { + if (update instanceof AccountUpdate) { + return { + mutable: update, + id: update.id, + isDummy: update.isDummy(), + children: UnfinishedForest.empty(), + }; } - return calls; + return { + final: update.accountUpdate, + id: update.id, + isDummy: Bool(false), + children: UnfinishedForest.fromForest(update.children), + }; }, - hashChildrenBase({ children }: AccountUpdate) { - let stackHash = CallForest.emptyHash(); - for (let accountUpdate of [...children.accountUpdates].reverse()) { - let calls = CallForest.hashChildren(accountUpdate); - let nodeHash = hashWithPrefix(prefixes.accountUpdateNode, [ - accountUpdate.hash(), - calls, - ]); - let newHash = hashWithPrefix(prefixes.accountUpdateCons, [ - nodeHash, - stackHash, - ]); - // skip accountUpdate if it's a dummy - stackHash = Provable.if(accountUpdate.isDummy(), stackHash, newHash); + setTo(node: UnfinishedTree, update: AccountUpdate | AccountUpdateTree) { + if (update instanceof AccountUpdate) { + if (node.final !== undefined) { + Object.assign(node, { + mutable: update, + final: undefined, + children: UnfinishedForest.empty(), + }); + } + } else if (node.mutable !== undefined) { + Object.assign(node, { + mutable: undefined, + final: update.accountUpdate, + children: UnfinishedForest.fromForest(update.children), + }); } - return stackHash; }, - computeCallDepth(update: AccountUpdate) { - for (let callDepth = 0; ; callDepth++) { - if (update.parent === undefined) return callDepth; - update = update.parent; - } + finalize(node: UnfinishedTree): AccountUpdateTreeBase & { isDummy: Bool } { + let accountUpdate = node.final ?? HashedAccountUpdate.hash(node.mutable); + let children = node.children.finalize(); + return { accountUpdate, id: node.id, isDummy: node.isDummy, children }; }, - map(updates: AccountUpdate[], map: (update: AccountUpdate) => AccountUpdate) { - let newUpdates: AccountUpdate[] = []; - for (let update of updates) { - let newUpdate = map(update); - newUpdate.children.accountUpdates = CallForest.map( - update.children.accountUpdates, - map - ); - newUpdates.push(newUpdate); - } - return newUpdates; + isUnfinished( + input: AccountUpdate | AccountUpdateTree | UnfinishedTree + ): input is UnfinishedTree { + return 'final' in input || 'mutable' in input; }, +}; - forEach(updates: AccountUpdate[], callback: (update: AccountUpdate) => void) { - for (let update of updates) { - callback(update); - CallForest.forEach(update.children.accountUpdates, callback); +class AccountUpdateLayout { + readonly map: Map; + readonly root: UnfinishedTree; + final?: AccountUpdateForest; + + constructor(root?: AccountUpdate) { + this.map = new Map(); + root ??= AccountUpdate.dummy(); + let rootTree: UnfinishedTree = { + mutable: root, + id: root.id, + isDummy: Bool(false), + children: UnfinishedForest.empty(), + }; + this.map.set(root.id, rootTree); + this.root = rootTree; + } + + get(update: AccountUpdate | AccountUpdateTree) { + return this.map.get(update.id); + } + + private getOrCreate( + update: AccountUpdate | AccountUpdateTree | UnfinishedTree + ): UnfinishedTree { + if (UnfinishedTree.isUnfinished(update)) { + if (!this.map.has(update.id)) { + this.map.set(update.id, update); + } + return update; } - }, + let node = this.map.get(update.id); + + if (node !== undefined) { + // might have to change node + UnfinishedTree.setTo(node, update); + return node; + } + + node = UnfinishedTree.create(update); + this.map.set(update.id, node); + return node; + } + + pushChild( + parent: AccountUpdate | UnfinishedTree, + child: AccountUpdate | AccountUpdateTree + ) { + let parentNode = this.getOrCreate(parent); + let childNode = this.getOrCreate(child); + parentNode.children.push(childNode); + } + + pushTopLevel(child: AccountUpdate) { + this.pushChild(this.root, child); + } + + setChildren( + parent: AccountUpdate | UnfinishedTree, + children: AccountUpdateForest + ) { + let parentNode = this.getOrCreate(parent); + parentNode.children.setToForest(children); + } + + setTopLevel(children: AccountUpdateForest) { + this.setChildren(this.root, children); + } + + disattach(update: AccountUpdate | AccountUpdateTree) { + let node = this.get(update); + node?.siblings?.remove(node); + return node; + } + + finalizeAndRemove(update: AccountUpdate | AccountUpdateTree) { + let node = this.get(update); + if (node === undefined) return; + this.disattach(update); + return node.children.finalize(); + } + + finalizeChildren() { + let final = this.root.children.finalize(); + this.final = final; + AccountUpdateForest.assertConstant(final); + return final; + } + + toFlatList({ mutate }: { mutate: boolean }) { + return this.root.children.toFlatArray(mutate); + } forEachPredecessor( - updates: AccountUpdate[], update: AccountUpdate, callback: (update: AccountUpdate) => void ) { - let isPredecessor = true; - CallForest.forEach(updates, (otherUpdate) => { - if (otherUpdate.id === update.id) isPredecessor = false; - if (isPredecessor) callback(otherUpdate); - }); - }, -}; + let updates = this.toFlatList({ mutate: false }); + for (let otherUpdate of updates) { + if (otherUpdate.id === update.id) return; + callback(otherUpdate); + } + } + toConstantInPlace() { + this.root.children.toConstantInPlace(); + } +} + +// TODO remove function createChildAccountUpdate( parent: AccountUpdate, childAddress: PublicKey, tokenId?: Field ) { let child = AccountUpdate.defaultAccountUpdate(childAddress, tokenId); - makeChildAccountUpdate(parent, child); - return child; -} -function makeChildAccountUpdate(parent: AccountUpdate, child: AccountUpdate) { child.body.callDepth = parent.body.callDepth + 1; - let wasChildAlready = parent.children.accountUpdates.find( - (update) => update.id === child.id - ); - // add to our children if not already here - if (!wasChildAlready) { - parent.children.accountUpdates.push(child); - // remove the child from the top level list / its current parent - AccountUpdate.unlink(child); - } - child.parent = parent; + AccountUpdate.unlink(child); + return child; } // authorization @@ -2133,7 +2162,7 @@ async function createZkappProof( }: LazyProof, { transaction, accountUpdate, index }: ZkappProverData ): Promise> { - let publicInput = accountUpdate.toPublicInput(); + let publicInput = accountUpdate.toPublicInput(transaction); let publicInputFields = MlFieldConstArray.to( ZkappPublicInput.toFields(publicInput) ); diff --git a/src/lib/account_update.unit-test.ts b/src/lib/account_update.unit-test.ts index a280b22712..845cd25a48 100644 --- a/src/lib/account_update.unit-test.ts +++ b/src/lib/account_update.unit-test.ts @@ -71,9 +71,12 @@ function createAccountUpdate() { let otherAddress = PrivateKey.random().toPublicKey(); let accountUpdate = AccountUpdate.create(address); - accountUpdate.approve(AccountUpdate.create(otherAddress)); + let otherUpdate = AccountUpdate.create(otherAddress); + accountUpdate.approve(otherUpdate); - let publicInput = accountUpdate.toPublicInput(); + let publicInput = accountUpdate.toPublicInput({ + accountUpdates: [accountUpdate, otherUpdate], + }); // create transaction JSON with the same accountUpdate structure, for ocaml version let tx = await Mina.transaction(() => { diff --git a/src/lib/circuit_value.ts b/src/lib/circuit_value.ts index a24e58e83a..d1d24e9959 100644 --- a/src/lib/circuit_value.ts +++ b/src/lib/circuit_value.ts @@ -32,6 +32,7 @@ export { Struct, FlexibleProvable, FlexibleProvablePure, + Unconstrained, }; // internal API @@ -45,7 +46,7 @@ export { HashInput, InferJson, InferredProvable, - Unconstrained, + StructNoJson, }; type ProvableExtension = { @@ -477,6 +478,24 @@ function Struct< return Struct_ as any; } +function StructNoJson< + A, + T extends InferProvable = InferProvable, + Pure extends boolean = IsPure +>( + type: A +): (new (value: T) => T) & { _isStruct: true } & (Pure extends true + ? ProvablePure + : Provable) & { + toInput: (x: T) => { + fields?: Field[] | undefined; + packed?: [Field, number][] | undefined; + }; + empty: () => T; + } { + return Struct(type) satisfies Provable as any; +} + /** * Container which holds an unconstrained value. This can be used to pass values * between the out-of-circuit blocks in provable code. @@ -547,6 +566,14 @@ and Provable.asProver() blocks, which execute outside the proof. /** * Create an `Unconstrained` with the given `value`. + * + * Note: If `T` contains provable types, `Unconstrained.from` is an anti-pattern, + * because it stores witnesses in a space that's intended to be used outside the proof. + * Something like the following should be used instead: + * + * ```ts + * let xWrapped = Unconstrained.witness(() => Provable.toConstant(type, x)); + * ``` */ static from(value: T) { return new Unconstrained(true, value); diff --git a/src/lib/mina.ts b/src/lib/mina.ts index 22c83fe9bb..b00075b40f 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -15,6 +15,7 @@ import { Actions, Events, dummySignature, + AccountUpdateLayout, } from './account_update.js'; import * as Fetch from './fetch.js'; import { assertPreconditionInvariants, NetworkValue } from './precondition.js'; @@ -181,7 +182,7 @@ function createTransaction( let transactionId = currentTransaction.enter({ sender, - accountUpdates: [], + layout: new AccountUpdateLayout(), fetchMode, isFinalRunOutsideCircuit, numberOfRuns, @@ -203,9 +204,7 @@ function createTransaction( f(); Provable.asProver(() => { let tx = currentTransaction.get(); - tx.accountUpdates = CallForest.map(tx.accountUpdates, (a) => - toConstant(AccountUpdate, a) - ); + tx.layout.toConstantInPlace(); }); }); } else { @@ -221,10 +220,10 @@ function createTransaction( currentTransaction.leave(transactionId); throw err; } - let accountUpdates = currentTransaction.get().accountUpdates; - // TODO: I'll be back - // CallForest.addCallers(accountUpdates); - accountUpdates = CallForest.toFlatList(accountUpdates); + + let accountUpdates = currentTransaction + .get() + .layout.toFlatList({ mutate: true }); try { // check that on-chain values weren't used without setting a precondition @@ -428,9 +427,11 @@ function LocalBlockchain({ // TODO: verify account update even if the account doesn't exist yet, using a default initial account if (account !== undefined) { + let publicInput = update.toPublicInput(txn.transaction); await verifyAccountUpdate( account, update, + publicInput, commitments, this.proofsEnabled, this.getNetworkId() @@ -1163,6 +1164,7 @@ function defaultNetworkState(): NetworkValue { async function verifyAccountUpdate( account: Account, accountUpdate: AccountUpdate, + publicInput: ZkappPublicInput, transactionCommitments: { commitment: bigint; fullCommitment: bigint }, proofsEnabled: boolean, networkId: NetworkId @@ -1244,7 +1246,6 @@ async function verifyAccountUpdate( if (accountUpdate.authorization.proof && proofsEnabled) { try { - let publicInput = accountUpdate.toPublicInput(); let publicInputFields = ZkappPublicInput.toFields(publicInput); let proof: JsonProof = { diff --git a/src/lib/mina/account-update-layout.unit-test.ts b/src/lib/mina/account-update-layout.unit-test.ts new file mode 100644 index 0000000000..7fd21e50b0 --- /dev/null +++ b/src/lib/mina/account-update-layout.unit-test.ts @@ -0,0 +1,64 @@ +import { Mina } from '../../index.js'; +import { AccountUpdate, AccountUpdateTree } from '../account_update.js'; +import { UInt64 } from '../int.js'; +import { SmartContract, method } from '../zkapp.js'; + +// smart contract which creates an account update that has a child of its own + +class NestedCall extends SmartContract { + @method deposit() { + let payerUpdate = AccountUpdate.createSigned(this.sender); + payerUpdate.send({ to: this.address, amount: UInt64.one }); + } + + @method depositUsingTree() { + let payerUpdate = AccountUpdate.createSigned(this.sender); + let receiverUpdate = AccountUpdate.create(this.address); + payerUpdate.send({ to: receiverUpdate, amount: UInt64.one }); + + let tree = AccountUpdateTree.from(payerUpdate); + tree.approve(receiverUpdate); + this.approve(tree); + } +} + +// setup + +let Local = Mina.LocalBlockchain({ proofsEnabled: true }); +Mina.setActiveInstance(Local); + +let [ + { publicKey: sender, privateKey: senderKey }, + { publicKey: zkappAddress, privateKey: zkappKey }, +] = Local.testAccounts; + +await NestedCall.compile(); +let zkapp = new NestedCall(zkappAddress); + +// deploy zkapp + +await (await Mina.transaction(sender, () => zkapp.deploy())) + .sign([zkappKey, senderKey]) + .send(); + +// deposit call + +let balanceBefore = Mina.getBalance(zkappAddress); + +let depositTx = await Mina.transaction(sender, () => zkapp.deposit()); +console.log(depositTx.toPretty()); +await depositTx.prove(); +await depositTx.sign([senderKey]).send(); + +Mina.getBalance(zkappAddress).assertEquals(balanceBefore.add(1)); + +// deposit call using tree + +balanceBefore = balanceBefore.add(1); + +depositTx = await Mina.transaction(sender, () => zkapp.depositUsingTree()); +console.log(depositTx.toPretty()); +await depositTx.prove(); +await depositTx.sign([senderKey]).send(); + +Mina.getBalance(zkappAddress).assertEquals(balanceBefore.add(1)); diff --git a/src/lib/mina/smart-contract-context.ts b/src/lib/mina/smart-contract-context.ts new file mode 100644 index 0000000000..96f93c4e8c --- /dev/null +++ b/src/lib/mina/smart-contract-context.ts @@ -0,0 +1,25 @@ +import type { SmartContract } from '../zkapp.js'; +import type { AccountUpdate, AccountUpdateLayout } from '../account_update.js'; +import { Context } from '../global-context.js'; +import { currentTransaction } from './transaction-context.js'; + +export { smartContractContext, SmartContractContext, accountUpdateLayout }; + +type SmartContractContext = { + this: SmartContract; + selfUpdate: AccountUpdate; + selfLayout: AccountUpdateLayout; +}; +let smartContractContext = Context.create({ + default: null, +}); + +function accountUpdateLayout() { + // in a smart contract, return the layout currently created in the contract call + let layout = smartContractContext.get()?.selfLayout; + + // if not in a smart contract but in a transaction, return the layout of the transaction + layout ??= currentTransaction()?.layout; + + return layout; +} diff --git a/src/lib/mina/token/forest-iterator.ts b/src/lib/mina/token/forest-iterator.ts index 441ae00951..62b717e94a 100644 --- a/src/lib/mina/token/forest-iterator.ts +++ b/src/lib/mina/token/forest-iterator.ts @@ -1,7 +1,7 @@ import { AccountUpdate, AccountUpdateForest, - AccountUpdateTree, + AccountUpdateTreeBase, TokenId, } from '../../account_update.js'; import { Field } from '../../core.js'; @@ -57,7 +57,7 @@ class TokenAccountUpdateIterator { selfToken: Field; constructor( - forest: MerkleListIterator, + forest: MerkleListIterator, mayUseToken: MayUseToken, selfToken: Field ) { @@ -87,8 +87,8 @@ class TokenAccountUpdateIterator { */ next() { // get next account update from the current forest (might be a dummy) - let { accountUpdate, calls } = this.currentLayer.forest.next(); - let childForest = AccountUpdateIterator.startIterating(calls); + let { accountUpdate, children } = this.currentLayer.forest.next(); + let childForest = AccountUpdateIterator.startIterating(children); let childLayer = { forest: childForest, mayUseToken: MayUseToken.InheritFromParent, diff --git a/src/lib/mina/token/forest-iterator.unit-test.ts b/src/lib/mina/token/forest-iterator.unit-test.ts index 06c9981847..c9a92117a8 100644 --- a/src/lib/mina/token/forest-iterator.unit-test.ts +++ b/src/lib/mina/token/forest-iterator.unit-test.ts @@ -4,7 +4,6 @@ import { TokenAccountUpdateIterator } from './forest-iterator.js'; import { AccountUpdate, AccountUpdateForest, - CallForest, TokenId, hashAccountUpdate, } from '../../account_update.js'; @@ -13,7 +12,6 @@ import { Pickles } from '../../../snarky.js'; import { accountUpdatesToCallForest, callForestHash, - CallForest as SimpleCallForest, } from '../../../mina-signer/src/sign-zkapp-command.js'; import assert from 'assert'; import { Field, Bool } from '../../core.js'; @@ -60,40 +58,19 @@ test.custom({ timeBudget: 1000 })( let forestBigint = accountUpdatesToCallForest(flatUpdatesBigint); let expectedHash = callForestHash(forestBigint); - // convert to o1js-style list of nested `AccountUpdate`s let flatUpdates = flatUpdatesBigint.map(accountUpdateFromBigint); - let updates = callForestToNestedArray( - accountUpdatesToCallForest(flatUpdates) - ); - - let forest = AccountUpdateForest.fromArray(updates); + let forest = AccountUpdateForest.fromFlatArray(flatUpdates); forest.hash.assertEquals(expectedHash); } ); -// can recover flat account updates from nested updates -// this is here to assert that we compute `updates` correctly in the other tests - -test(flatAccountUpdates, (flatUpdates) => { - let updates = callForestToNestedArray( - accountUpdatesToCallForest(flatUpdates) - ); - let flatUpdates2 = CallForest.toFlatList(updates, false); - let n = flatUpdates.length; - for (let i = 0; i < n; i++) { - assert.deepStrictEqual(flatUpdates2[i], flatUpdates[i]); - } -}); - // traverses the top level of a call forest in correct order // i.e., CallForestArray works test.custom({ timeBudget: 1000 })(flatAccountUpdates, (flatUpdates) => { // prepare call forest from flat account updates - let updates = callForestToNestedArray( - accountUpdatesToCallForest(flatUpdates) - ); - let forest = AccountUpdateForest.fromArray(updates).startIterating(); + let forest = AccountUpdateForest.fromFlatArray(flatUpdates).startIterating(); + let updates = flatUpdates.filter((u) => u.body.callDepth === 0); // step through top-level by calling forest.next() repeatedly let n = updates.length; @@ -116,10 +93,7 @@ test.custom({ timeBudget: 5000 })(flatAccountUpdates, (flatUpdates) => { let tokenId = TokenId.default; // prepare forest iterator from flat account updates - let updates = callForestToNestedArray( - accountUpdatesToCallForest(flatUpdates) - ); - let forest = AccountUpdateForest.fromArray(updates); + let forest = AccountUpdateForest.fromFlatArray(flatUpdates); let forestIterator = TokenAccountUpdateIterator.create(forest, tokenId); // step through forest iterator and compare against expected updates @@ -154,28 +128,26 @@ test.custom({ timeBudget: 5000 })( }); let tokenId = TokenId.derive(tokenOwner); - // prepare forest iterator from flat account updates - let updates = callForestToNestedArray( - accountUpdatesToCallForest(flatUpdates) - ); - // make all top-level updates inaccessible - updates.forEach((u, i) => { - if (i % 3 === 0) { - u.body.mayUseToken = AccountUpdate.MayUseToken.No; - } else if (i % 3 === 1) { - u.body.mayUseToken = AccountUpdate.MayUseToken.InheritFromParent; - } else { - u.body.publicKey = tokenOwner; - u.body.tokenId = TokenId.default; - } - }); + flatUpdates + .filter((u) => u.body.callDepth === 0) + .forEach((u, i) => { + if (i % 3 === 0) { + u.body.mayUseToken = AccountUpdate.MayUseToken.No; + } else if (i % 3 === 1) { + u.body.mayUseToken = AccountUpdate.MayUseToken.InheritFromParent; + } else { + u.body.publicKey = tokenOwner; + u.body.tokenId = TokenId.default; + } + }); - let forest = AccountUpdateForest.fromArray(updates); + // prepare forest iterator from flat account updates + let forest = AccountUpdateForest.fromFlatArray(flatUpdates); let forestIterator = TokenAccountUpdateIterator.create(forest, tokenId); // step through forest iterator and compare against expected updates - let expectedUpdates = updates; + let expectedUpdates = flatUpdates.filter((u) => u.body.callDepth === 0); let n = flatUpdates.length; for (let i = 0; i < n; i++) { @@ -214,10 +186,7 @@ test.custom({ timeBudget: 5000 })( }); // prepare forest iterator from flat account updates - let updates = callForestToNestedArray( - accountUpdatesToCallForest(flatUpdates) - ); - let forest = AccountUpdateForest.fromArray(updates); + let forest = AccountUpdateForest.fromFlatArray(flatUpdates); let forestIterator = TokenAccountUpdateIterator.create(forest, tokenId); // step through forest iterator and compare against expected updates @@ -239,15 +208,6 @@ function accountUpdateFromBigint(a: TypesBigint.AccountUpdate): AccountUpdate { return AccountUpdate.fromJSON(TypesBigint.AccountUpdate.toJSON(a)); } -function callForestToNestedArray( - forest: SimpleCallForest -): AccountUpdate[] { - return forest.map(({ accountUpdate, children }) => { - accountUpdate.children.accountUpdates = callForestToNestedArray(children); - return accountUpdate; - }); -} - function assertEqual(actual: AccountUpdate, expected: AccountUpdate) { let actualHash = hashAccountUpdate(actual).toBigInt(); let expectedHash = hashAccountUpdate(expected).toBigInt(); diff --git a/src/lib/mina/token/token-contract.ts b/src/lib/mina/token/token-contract.ts index 765fd184f9..38a2a64843 100644 --- a/src/lib/mina/token/token-contract.ts +++ b/src/lib/mina/token/token-contract.ts @@ -5,11 +5,8 @@ import { PublicKey } from '../../signature.js'; import { AccountUpdate, AccountUpdateForest, - UnfinishedForest, AccountUpdateTree, - HashedAccountUpdate, Permissions, - smartContractContext, } from '../../account_update.js'; import { DeployArgs, SmartContract } from '../../zkapp.js'; import { TokenAccountUpdateIterator } from './forest-iterator.js'; @@ -50,33 +47,23 @@ abstract class TokenContract extends SmartContract { updates: AccountUpdateForest, callback: (update: AccountUpdate, usesToken: Bool) => void ) { - let forest = TokenAccountUpdateIterator.create(updates, this.token.id); + let iterator = TokenAccountUpdateIterator.create(updates, this.token.id); // iterate through the forest and apply user-defined logc for (let i = 0; i < MAX_ACCOUNT_UPDATES; i++) { - let { accountUpdate, usesThisToken } = forest.next(); + let { accountUpdate, usesThisToken } = iterator.next(); callback(accountUpdate, usesThisToken); } // prove that we checked all updates - forest.assertFinished( + iterator.assertFinished( `Number of account updates to approve exceed ` + `the supported limit of ${MAX_ACCOUNT_UPDATES}.\n` ); // skip hashing our child account updates in the method wrapper // since we just did that in the loop above - this.self.children.callsType = { - type: 'WitnessEquals', - value: updates.hash, - }; - - // make top-level updates our children - Provable.asProver(() => { - updates.data.get().forEach((update) => { - this.self.adopt(update.element.accountUpdate.value.get()); - }); - }); + this.approve(updates); } /** @@ -100,16 +87,16 @@ abstract class TokenContract extends SmartContract { /** * Approve a single account update (with arbitrarily many children). */ - approveAccountUpdate(accountUpdate: AccountUpdate) { - let forest = finalizeAccountUpdates([accountUpdate]); + approveAccountUpdate(accountUpdate: AccountUpdate | AccountUpdateTree) { + let forest = toForest([accountUpdate]); this.approveBase(forest); } /** * Approve a list of account updates (with arbitrarily many children). */ - approveAccountUpdates(accountUpdates: AccountUpdate[]) { - let forest = finalizeAccountUpdates(accountUpdates); + approveAccountUpdates(accountUpdates: (AccountUpdate | AccountUpdateTree)[]) { + let forest = toForest(accountUpdates); this.approveBase(forest); } @@ -138,32 +125,16 @@ abstract class TokenContract extends SmartContract { from.balanceChange = Int64.from(amount).neg(); to.balanceChange = Int64.from(amount); - let forest = finalizeAccountUpdates([from, to]); + let forest = toForest([from, to]); this.approveBase(forest); } } -function finalizeAccountUpdates(updates: AccountUpdate[]): AccountUpdateForest { - let trees = updates.map(finalizeAccountUpdate); +function toForest( + updates: (AccountUpdate | AccountUpdateTree)[] +): AccountUpdateForest { + let trees = updates.map((a) => + a instanceof AccountUpdate ? a.extractTree() : a + ); return AccountUpdateForest.from(trees); } - -function finalizeAccountUpdate(update: AccountUpdate): AccountUpdateTree { - let calls: AccountUpdateForest; - - let insideContract = smartContractContext.get(); - if (insideContract) { - let node = insideContract.selfCalls.value.find( - (c) => c.accountUpdate.value.id === update.id - ); - if (node !== undefined) { - calls = UnfinishedForest.finalize(node.calls); - } - } - calls ??= AccountUpdateForest.fromArray(update.children.accountUpdates, { - skipDummies: true, - }); - AccountUpdate.unlink(update); - - return { accountUpdate: HashedAccountUpdate.hash(update), calls }; -} diff --git a/src/lib/mina/token/token-contract.unit-test.ts b/src/lib/mina/token/token-contract.unit-test.ts index 6bac61c881..bcbe2bc882 100644 --- a/src/lib/mina/token/token-contract.unit-test.ts +++ b/src/lib/mina/token/token-contract.unit-test.ts @@ -37,9 +37,11 @@ class ExampleTokenContract extends TokenContract { let Local = Mina.LocalBlockchain({ proofsEnabled: false }); Mina.setActiveInstance(Local); -let { publicKey: sender, privateKey: senderKey } = Local.testAccounts[0]; -let { publicKey: tokenAddress, privateKey: tokenKey } = Local.testAccounts[1]; -let { publicKey: otherAddress } = Local.testAccounts[2]; +let [ + { publicKey: sender, privateKey: senderKey }, + { publicKey: tokenAddress, privateKey: tokenKey }, + { publicKey: otherAddress, privateKey: otherKey }, +] = Local.testAccounts; let token = new ExampleTokenContract(tokenAddress); let tokenId = token.token.id; @@ -70,17 +72,33 @@ update1.body.mayUseToken = AccountUpdate.MayUseToken.ParentsOwnToken; let update2 = AccountUpdate.create(otherAddress); update2.body.mayUseToken = AccountUpdate.MayUseToken.InheritFromParent; +update2.body.callDepth = 1; let update3 = AccountUpdate.create(otherAddress, tokenId); update3.body.mayUseToken = AccountUpdate.MayUseToken.InheritFromParent; update3.balanceChange = Int64.one; +update3.body.callDepth = 2; -update1.adopt(update2); -update2.adopt(update3); - -let forest = AccountUpdateForest.fromArray([update1]); +let forest = AccountUpdateForest.fromFlatArray([update1, update2, update3]); await assert.rejects( () => Mina.transaction(sender, () => token.approveBase(forest)), /Field\.assertEquals\(\): 1 != 0/ ); + +// succeeds to approve deep account update tree with zero balance sum +let update4 = AccountUpdate.createSigned(otherAddress, tokenId); +update4.body.mayUseToken = AccountUpdate.MayUseToken.InheritFromParent; +update4.balanceChange = Int64.minusOne; +update4.body.callDepth = 2; + +forest = AccountUpdateForest.fromFlatArray([ + update1, + update2, + update3, + update4, +]); + +let approveTx = await Mina.transaction(sender, () => token.approveBase(forest)); +await approveTx.prove(); +await approveTx.sign([senderKey, otherKey]).send(); diff --git a/src/lib/mina/transaction-context.ts b/src/lib/mina/transaction-context.ts index a3bb040b1c..d981384085 100644 --- a/src/lib/mina/transaction-context.ts +++ b/src/lib/mina/transaction-context.ts @@ -1,4 +1,4 @@ -import type { AccountUpdate } from '../account_update.js'; +import type { AccountUpdateLayout } from '../account_update.js'; import type { PublicKey } from '../signature.js'; import { Context } from '../global-context.js'; @@ -7,7 +7,7 @@ export { currentTransaction, CurrentTransaction, FetchMode }; type FetchMode = 'fetch' | 'cached' | 'test'; type CurrentTransaction = { sender?: PublicKey; - accountUpdates: AccountUpdate[]; + layout: AccountUpdateLayout; fetchMode: FetchMode; isFinalRunOutsideCircuit: boolean; numberOfRuns: 0 | 1 | undefined; diff --git a/src/lib/mina/transaction-logic/ledger.ts b/src/lib/mina/transaction-logic/ledger.ts index 266054ca50..daf7d3cb0d 100644 --- a/src/lib/mina/transaction-logic/ledger.ts +++ b/src/lib/mina/transaction-logic/ledger.ts @@ -2,10 +2,11 @@ * A ledger of accounts - simple model of a local blockchain. */ import { PublicKey } from '../../signature.js'; -import { type AccountUpdate, TokenId } from '../../account_update.js'; +import type { AccountUpdate } from '../../account_update.js'; import { Account, newAccount } from '../account.js'; import { Field } from '../../field.js'; import { applyAccountUpdate } from './apply.js'; +import { Types } from '../../../bindings/mina-transaction/types.js'; export { SimpleLedger }; @@ -20,7 +21,10 @@ class SimpleLedger { return new SimpleLedger(); } - exists({ publicKey, tokenId = TokenId.default }: InputAccountId): boolean { + exists({ + publicKey, + tokenId = Types.TokenId.empty(), + }: InputAccountId): boolean { return this.accounts.has(accountId({ publicKey, tokenId })); } @@ -30,7 +34,7 @@ class SimpleLedger { load({ publicKey, - tokenId = TokenId.default, + tokenId = Types.TokenId.empty(), }: InputAccountId): Account | undefined { let id = accountId({ publicKey, tokenId }); let account = this.accounts.get(id); diff --git a/src/lib/provable-types/auxiliary.ts b/src/lib/provable-types/auxiliary.ts new file mode 100644 index 0000000000..46ee535526 --- /dev/null +++ b/src/lib/provable-types/auxiliary.ts @@ -0,0 +1,13 @@ +import type { ProvableHashable } from '../hash.js'; + +export { RandomId }; + +const RandomId: ProvableHashable = { + sizeInFields: () => 0, + toFields: () => [], + toAuxiliary: (v = Math.random()) => [v], + fromFields: (_, [v]) => v, + check: () => {}, + toInput: () => ({}), + empty: () => Math.random(), +}; diff --git a/src/lib/provable-types/merkle-list.ts b/src/lib/provable-types/merkle-list.ts index 6e949c050e..745b47ee9a 100644 --- a/src/lib/provable-types/merkle-list.ts +++ b/src/lib/provable-types/merkle-list.ts @@ -26,6 +26,12 @@ type WithHash = { previousHash: Field; element: T }; function WithHash(type: ProvableHashable): ProvableHashable> { return Struct({ previousHash: Field, element: type }); } +function toConstant(type: Provable, node: WithHash): WithHash { + return { + previousHash: node.previousHash.toConstant(), + element: Provable.toConstant(type, node.element), + }; +} /** * Common base type for {@link MerkleList} and {@link MerkleListIterator} @@ -88,7 +94,10 @@ class MerkleList implements MerkleListBase { push(element: T) { let previousHash = this.hash; this.hash = this.nextHash(previousHash, element); - this.data.updateAsProver((data) => [{ previousHash, element }, ...data]); + this.data.updateAsProver((data) => [ + toConstant(this.innerProvable, { previousHash, element }), + ...data, + ]); } /** @@ -102,7 +111,9 @@ class MerkleList implements MerkleListBase { previousHash ); this.data.updateAsProver((data) => - condition.toBoolean() ? [{ previousHash, element }, ...data] : data + condition.toBoolean() + ? [toConstant(this.innerProvable, { previousHash, element }), ...data] + : data ); } @@ -162,11 +173,12 @@ class MerkleList implements MerkleListBase { let element = this.pop(); // if the condition is false, we restore the original state - this.data.updateAsProver((data) => - condition.toBoolean() + this.data.updateAsProver((data) => { + let node = { previousHash: this.hash, element }; + return condition.toBoolean() ? data - : [{ previousHash: this.hash, element }, ...data] - ); + : [toConstant(this.innerProvable, node), ...data]; + }); this.hash = Provable.if(condition, this.hash, originalHash); return element; @@ -204,10 +216,10 @@ class MerkleList implements MerkleListBase { from: (array: T[]) => MerkleList; provable: ProvableHashable>; } { - return class MerkleList_ extends MerkleList { + class MerkleListTBase extends MerkleList { static _innerProvable = type; - static _provable = provableFromClass(MerkleList_, { + static _provable = provableFromClass(MerkleListTBase, { hash: Field, data: Unconstrained.provable, }) as ProvableHashable>; @@ -221,13 +233,22 @@ class MerkleList implements MerkleListBase { static from(array: T[]): MerkleList { let { hash, data } = withHashes(array, nextHash, emptyHash_); - return new this({ data: Unconstrained.from(data), hash }); + let unconstrained = Unconstrained.witness(() => + data.map((x) => toConstant(type, x)) + ); + return new this({ data: unconstrained, hash }); } static get provable(): ProvableHashable> { assert(this._provable !== undefined, 'MerkleList not initialized'); return this._provable; } + } + // override `instanceof` for subclasses + return class MerkleListT extends MerkleListTBase { + static [Symbol.hasInstance](x: any): boolean { + return x instanceof MerkleListTBase; + } }; } @@ -411,7 +432,10 @@ class MerkleListIterator implements MerkleListIteratorBase { static from(array: T[]): MerkleListIterator { let { hash, data } = withHashes(array, nextHash, emptyHash_); - return this.startIterating({ data: Unconstrained.from(data), hash }); + let unconstrained = Unconstrained.witness(() => + data.map((x) => toConstant(type, x)) + ); + return this.startIterating({ data: unconstrained, hash }); } static startIterating({ diff --git a/src/lib/provable-types/packed.ts b/src/lib/provable-types/packed.ts index eb0a04184e..c770b28145 100644 --- a/src/lib/provable-types/packed.ts +++ b/src/lib/provable-types/packed.ts @@ -52,7 +52,9 @@ class Packed { /** * Create a packed representation of `type`. You can then use `PackedType.pack(x)` to pack a value. */ - static create(type: ProvableExtended): typeof Packed { + static create(type: ProvableExtended): typeof Packed & { + provable: ProvableHashable>; + } { // compute size of packed representation let input = type.toInput(type.empty()); let packedSize = countFields(input); @@ -67,6 +69,11 @@ class Packed { static empty(): Packed { return Packed_.pack(type.empty()); } + + static get provable() { + assert(this._provable !== undefined, 'Packed not initialized'); + return this._provable; + } }; } @@ -79,9 +86,13 @@ class Packed { * Pack a value. */ static pack(x: T): Packed { - let input = this.innerProvable.toInput(x); + let type = this.innerProvable; + let input = type.toInput(x); let packed = packToFields(input); - return new this(packed, Unconstrained.from(x)); + let unconstrained = Unconstrained.witness(() => + Provable.toConstant(type, x) + ); + return new this(packed, unconstrained); } /** @@ -114,10 +125,6 @@ class Packed { return this.constructor as typeof Packed; } - static get provable(): ProvableHashable> { - assert(this._provable !== undefined, 'Packed not initialized'); - return this._provable; - } static get innerProvable(): ProvableExtended { assert(this._innerProvable !== undefined, 'Packed not initialized'); return this._innerProvable; @@ -177,7 +184,9 @@ class Hashed { static create( type: ProvableHashable, hash?: (t: T) => Field - ): typeof Hashed { + ): typeof Hashed & { + provable: ProvableHashable>; + } { let _hash = hash ?? ((t: T) => Poseidon.hashPacked(type, t)); let dummyHash = _hash(type.empty()); @@ -194,6 +203,11 @@ class Hashed { static empty(): Hashed { return new this(dummyHash, Unconstrained.from(type.empty())); } + + static get provable() { + assert(this._provable !== undefined, 'Hashed not initialized'); + return this._provable; + } }; } @@ -208,10 +222,19 @@ class Hashed { /** * Wrap a value, and represent it by its hash in provable code. + * + * ```ts + * let hashed = HashedType.hash(value); + * ``` + * + * Optionally, if you already have the hash, you can pass it in and avoid recomputing it. */ - static hash(value: T): Hashed { - let hash = this._hash(value); - return new this(hash, Unconstrained.from(value)); + static hash(value: T, hash?: Field): Hashed { + hash ??= this._hash(value); + let unconstrained = Unconstrained.witness(() => + Provable.toConstant(this.innerProvable, value) + ); + return new this(hash, unconstrained); } /** @@ -241,10 +264,6 @@ class Hashed { return this.constructor as typeof Hashed; } - static get provable(): ProvableHashable> { - assert(this._provable !== undefined, 'Hashed not initialized'); - return this._provable; - } static get innerProvable(): ProvableHashable { assert(this._innerProvable !== undefined, 'Hashed not initialized'); return this._innerProvable; diff --git a/src/lib/provable.ts b/src/lib/provable.ts index 961f723451..6d1eac028a 100644 --- a/src/lib/provable.ts +++ b/src/lib/provable.ts @@ -196,6 +196,16 @@ const Provable = { * ``` */ inCheckedComputation, + + /** + * Returns a constant version of a provable type. + */ + toConstant(type: Provable, value: T) { + return type.fromFields( + type.toFields(value).map((x) => x.toConstant()), + type.toAuxiliary(value) + ); + }, }; function witness = FlexibleProvable>( diff --git a/src/lib/testing/constraint-system.ts b/src/lib/testing/constraint-system.ts index 1f8ad9ba52..064b49d863 100644 --- a/src/lib/testing/constraint-system.ts +++ b/src/lib/testing/constraint-system.ts @@ -19,7 +19,7 @@ export { not, and, or, - satisfies, + fulfills, equals, contains, allConstant, @@ -186,7 +186,7 @@ function or(...tests: ConstraintSystemTest[]): ConstraintSystemTest { /** * General test */ -function satisfies( +function fulfills( label: string, run: (cs: Gate[], inputs: TypeAndValue[]) => boolean ): ConstraintSystemTest { @@ -276,10 +276,7 @@ function ifNotAllConstant(test: ConstraintSystemTest): ConstraintSystemTest { /** * Test whether constraint system is empty. */ -const isEmpty = satisfies( - 'constraint system is empty', - (cs) => cs.length === 0 -); +const isEmpty = fulfills('constraint system is empty', (cs) => cs.length === 0); /** * Modifies a test so that it runs on the constraint system with generic gates filtered out. diff --git a/src/lib/token.test.ts b/src/lib/token.test.ts index 5f8502f902..e6c2ee1f5b 100644 --- a/src/lib/token.test.ts +++ b/src/lib/token.test.ts @@ -12,7 +12,6 @@ import { Permissions, VerificationKey, Field, - Experimental, Int64, TokenId, } from 'o1js'; @@ -84,38 +83,31 @@ class TokenContract extends SmartContract { this.totalAmountInCirculation.set(newTotalAmountInCirculation); } - @method approveTransferCallback( + @method approveTransfer( senderAddress: PublicKey, receiverAddress: PublicKey, amount: UInt64, - callback: Experimental.Callback + senderAccountUpdate: AccountUpdate ) { - let layout = AccountUpdate.Layout.NoChildren; // Allow only 1 accountUpdate with no children - let senderAccountUpdate = this.approve(callback, layout); - let negativeAmount = Int64.fromObject( - senderAccountUpdate.body.balanceChange - ); + this.approve(senderAccountUpdate); + let negativeAmount = senderAccountUpdate.balanceChange; negativeAmount.assertEquals(Int64.from(amount).neg()); let tokenId = this.token.id; senderAccountUpdate.body.tokenId.assertEquals(tokenId); senderAccountUpdate.body.publicKey.assertEquals(senderAddress); - let receiverAccountUpdate = Experimental.createChildAccountUpdate( - this.self, - receiverAddress, - tokenId - ); + let receiverAccountUpdate = AccountUpdate.create(receiverAddress, tokenId); receiverAccountUpdate.balance.addInPlace(amount); } } class ZkAppB extends SmartContract { - @method approveZkapp(amount: UInt64) { + @method approveSend(amount: UInt64) { this.balance.subInPlace(amount); } } class ZkAppC extends SmartContract { - @method approveZkapp(amount: UInt64) { + @method approveSend(amount: UInt64) { this.balance.subInPlace(amount); } @@ -535,16 +527,13 @@ describe('Token', () => { await tx.sign([feePayerKey, tokenZkappKey]).send(); tx = await Mina.transaction(feePayer, () => { - let approveSendingCallback = Experimental.Callback.create( - zkAppB, - 'approveZkapp', - [UInt64.from(10_000)] - ); - tokenZkapp.approveTransferCallback( + zkAppB.approveSend(UInt64.from(10_000)); + + tokenZkapp.approveTransfer( zkAppBAddress, zkAppCAddress, UInt64.from(10_000), - approveSendingCallback + zkAppB.self ); }); await tx.prove(); @@ -570,16 +559,12 @@ describe('Token', () => { await expect(() => Mina.transaction(feePayer, () => { - let approveSendingCallback = Experimental.Callback.create( - zkAppC, - 'approveIncorrectLayout', - [UInt64.from(10_000)] - ); - tokenZkapp.approveTransferCallback( + zkAppC.approveIncorrectLayout(UInt64.from(10_000)); + tokenZkapp.approveTransfer( zkAppBAddress, zkAppCAddress, UInt64.from(10_000), - approveSendingCallback + zkAppC.self ); }) ).rejects.toThrow(); diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 466076c7a0..3ac45d7307 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -2,23 +2,20 @@ import { Gate, Pickles, ProvablePure } from '../snarky.js'; import { Field, Bool } from './core.js'; import { AccountUpdate, - AccountUpdatesLayout, Authorization, Body, Events, Permissions, Actions, SetOrKeep, - smartContractContext, TokenId, ZkappCommand, zkAppProver, ZkappPublicInput, - ZkappStateLength, - SmartContractContext, LazyProof, - CallForest, - UnfinishedForest, + AccountUpdateForest, + AccountUpdateLayout, + AccountUpdateTree, } from './account_update.js'; import { cloneCircuitValue, @@ -54,6 +51,7 @@ import { PrivateKey, PublicKey } from './signature.js'; import { assertStatePrecondition, cleanStatePrecondition } from './state.js'; import { inAnalyze, + inCheckedComputation, inCompile, inProver, snarkContext, @@ -61,6 +59,12 @@ import { import { Cache } from './proof-system/cache.js'; import { assert } from './gadgets/common.js'; import { SmartContractBase } from './mina/smart-contract-base.js'; +import { ZkappStateLength } from './mina/mina-instance.js'; +import { + SmartContractContext, + accountUpdateLayout, + smartContractContext, +} from './mina/smart-contract-context.js'; // external API export { @@ -165,13 +169,10 @@ function wrapMethod( let insideContract = smartContractContext.get(); if (!insideContract) { - const context: SmartContractContext = { - this: this, - methodCallDepth: 0, - selfUpdate: selfAccountUpdate(this, methodName), - selfCalls: UnfinishedForest.empty(), - }; - let id = smartContractContext.enter(context); + const { id, context } = SmartContractContext.enter( + this, + selfAccountUpdate(this, methodName) + ); try { if (inCompile() || inProver() || inAnalyze()) { // important to run this with a fresh accountUpdate everytime, otherwise compile messes up our circuits @@ -179,7 +180,8 @@ function wrapMethod( let proverData = inProver() ? zkAppProver.getData() : undefined; let txId = Mina.currentTransaction.enter({ sender: proverData?.transaction.feePayer.body.publicKey, - accountUpdates: [], + // TODO could pass an update with the fee payer's content here? probably not bc it's not accessed + layout: new AccountUpdateLayout(), fetchMode: inProver() ? 'cached' : 'test', isFinalRunOutsideCircuit: false, numberOfRuns: undefined, @@ -194,11 +196,11 @@ function wrapMethod( let blindingValue = Provable.witness(Field, getBlindingValue); // it's also good if we prove that we use the same blinding value across the method // that's why we pass the variable (not the constant) into a new context - let context = memoizationContext() ?? { + let memoCtx = memoizationContext() ?? { memoized: [], currentIndex: 0, }; - let id = memoizationContext.enter({ ...context, blindingValue }); + let id = memoizationContext.enter({ ...memoCtx, blindingValue }); let result: unknown; try { let clonedArgs = actualArgs.map(cloneCircuitValue); @@ -218,7 +220,8 @@ function wrapMethod( ProofAuthorization.setKind(accountUpdate); debugPublicInput(accountUpdate); - checkPublicInput(publicInput, accountUpdate); + let calls = context.selfLayout.finalizeChildren(); + checkPublicInput(publicInput, accountUpdate, calls); // check the self accountUpdate right after calling the method // TODO: this needs to be done in a unified way for all account updates that are created @@ -242,7 +245,6 @@ function wrapMethod( // called smart contract at the top level, in a transaction! // => attach ours to the current list of account updates let accountUpdate = context.selfUpdate; - Mina.currentTransaction()?.accountUpdates.push(accountUpdate); // first, clone to protect against the method modifying arguments! // TODO: double-check that this works on all possible inputs, e.g. CircuitValue, o1js primitives @@ -296,9 +298,24 @@ function wrapMethod( memoized, blindingValue, }, - Mina.currentTransaction()!.accountUpdates + Mina.currentTransaction.get().layout ); } + + // transfer layout from the smart contract context to the transaction + if (inCheckedComputation()) { + Provable.asProver(() => { + accountUpdate = Provable.toConstant(AccountUpdate, accountUpdate); + context.selfLayout.toConstantInPlace(); + }); + } + let txLayout = Mina.currentTransaction.get().layout; + txLayout.pushTopLevel(accountUpdate); + txLayout.setChildren( + accountUpdate, + context.selfLayout.finalizeChildren() + ); + return result; } } finally { @@ -308,14 +325,11 @@ function wrapMethod( // if we're here, this method was called inside _another_ smart contract method let parentAccountUpdate = insideContract.this.self; - let methodCallDepth = insideContract.methodCallDepth; - let innerContext: SmartContractContext = { - this: this, - methodCallDepth: methodCallDepth + 1, - selfUpdate: selfAccountUpdate(this, methodName), - selfCalls: UnfinishedForest.empty(), - }; - let id = smartContractContext.enter(innerContext); + + let { id, context: innerContext } = SmartContractContext.enter( + this, + selfAccountUpdate(this, methodName) + ); try { // if the call result is not undefined but there's no known returnType, the returnType was probably not annotated properly, // so we have to explain to the user how to do that @@ -342,7 +356,6 @@ function wrapMethod( let constantBlindingValue = blindingValue.toConstant(); let accountUpdate = this.self; accountUpdate.body.callDepth = parentAccountUpdate.body.callDepth + 1; - accountUpdate.parent = parentAccountUpdate; let memoContext = { memoized: [], @@ -390,15 +403,27 @@ function wrapMethod( memoized, blindingValue: constantBlindingValue, }, - Mina.currentTransaction()!.accountUpdates + Mina.currentTransaction()?.layout ?? new AccountUpdateLayout() ); } - return { accountUpdate, result: result ?? null }; + // extract callee's account update layout + let children = innerContext.selfLayout.finalizeChildren(); + + return { + accountUpdate, + result: { result: result ?? null, children }, + }; }; // we have to run the called contract inside a witness block, to not affect the caller's circuit - let { accountUpdate, result } = AccountUpdate.witness( - returnType ?? provable(null), + let { + accountUpdate, + result: { result, children }, + } = AccountUpdate.witness<{ result: any; children: AccountUpdateForest }>( + provable({ + result: returnType ?? provable(null), + children: AccountUpdateForest.provable, + }), runCalledContract, { skipCheck: true } ); @@ -410,16 +435,9 @@ function wrapMethod( // connect accountUpdate to our own. outside Provable.witness so compile knows the right structure when hashing children accountUpdate.body.callDepth = parentAccountUpdate.body.callDepth + 1; - accountUpdate.parent = parentAccountUpdate; - // beware: we don't include the callee's children in the caller circuit - // nothing is asserted about them -- it's the callee's task to check their children - accountUpdate.children.callsType = { type: 'Witness' }; - parentAccountUpdate.children.accountUpdates.push(accountUpdate); - UnfinishedForest.push( - insideContract.selfCalls, - accountUpdate, - UnfinishedForest.fromArray(accountUpdate.children.accountUpdates, true) - ); + + insideContract.selfLayout.pushTopLevel(accountUpdate); + insideContract.selfLayout.setChildren(accountUpdate, children); // assert that we really called the right zkapp accountUpdate.body.publicKey.assertEquals(this.address); @@ -448,11 +466,11 @@ function wrapMethod( function checkPublicInput( { accountUpdate, calls }: ZkappPublicInput, - self: AccountUpdate + self: AccountUpdate, + selfCalls: AccountUpdateForest ) { - let otherInput = self.toPublicInput(); - accountUpdate.assertEquals(otherInput.accountUpdate); - calls.assertEquals(otherInput.calls); + accountUpdate.assertEquals(self.hash()); + calls.assertEquals(selfCalls.hash); } /** @@ -827,7 +845,7 @@ super.init(); this.#executionState = { transactionId, accountUpdate }; return accountUpdate; } - // same as this.self, but explicitly creates a _new_ account update + /** * Same as `SmartContract.self` but explicitly creates a new {@link AccountUpdate}. */ @@ -909,22 +927,22 @@ super.init(); * * Under the hood, "approving" just means that the account update is made a child of the zkApp in the * tree of account updates that forms the transaction. - * The second parameter `layout` allows you to also make assertions about the approved update's _own_ children, - * by specifying a certain expected layout of children. See {@link AccountUpdate.Layout}. * * @param updateOrCallback - * @param layout * @returns The account update that was approved (needed when passing in a Callback) */ approve( - updateOrCallback: AccountUpdate | Callback, - layout?: AccountUpdatesLayout + updateOrCallback: + | AccountUpdate + | AccountUpdateTree + | AccountUpdateForest + | Callback ) { let accountUpdate = - updateOrCallback instanceof AccountUpdate - ? updateOrCallback - : Provable.witness(AccountUpdate, () => updateOrCallback.accountUpdate); - this.self.approve(accountUpdate, layout); + updateOrCallback instanceof Callback + ? Provable.witness(AccountUpdate, () => updateOrCallback.accountUpdate) + : updateOrCallback; + this.self.approve(accountUpdate); return accountUpdate; } @@ -1453,6 +1471,18 @@ type ExecutionState = { accountUpdate: AccountUpdate; }; +const SmartContractContext = { + enter(self: SmartContract, selfUpdate: AccountUpdate) { + let context: SmartContractContext = { + this: self, + selfUpdate, + selfLayout: new AccountUpdateLayout(selfUpdate), + }; + let id = smartContractContext.enter(context); + return { id, context }; + }, +}; + type DeployArgs = | { verificationKey?: { data: string; hash: string | Field }; @@ -1518,7 +1548,10 @@ const Reducer: (< ) as any; const ProofAuthorization = { - setKind({ body, id }: AccountUpdate, priorAccountUpdates?: AccountUpdate[]) { + setKind( + { body, id }: AccountUpdate, + priorAccountUpdates?: AccountUpdateLayout + ) { body.authorizationKind.isSigned = Bool(false); body.authorizationKind.isProved = Bool(true); let hash = Provable.witness(Field, () => { @@ -1526,17 +1559,16 @@ const ProofAuthorization = { let isProver = proverData !== undefined; assert( isProver || priorAccountUpdates !== undefined, - 'Called `setProofAuthorizationKind()` outside the prover without passing in `priorAccountUpdates`.' + 'Called `setKind()` outside the prover without passing in `priorAccountUpdates`.' ); let myAccountUpdateId = isProver ? proverData.accountUpdate.id : id; - priorAccountUpdates ??= proverData.transaction.accountUpdates; - priorAccountUpdates = priorAccountUpdates.filter( + let priorAccountUpdatesFlat = priorAccountUpdates?.toFlatList({ + mutate: false, + }); + priorAccountUpdatesFlat ??= proverData.transaction.accountUpdates; + priorAccountUpdatesFlat = priorAccountUpdatesFlat.filter( (a) => a.id !== myAccountUpdateId ); - let priorAccountUpdatesFlat = CallForest.toFlatList( - priorAccountUpdates, - false - ); let accountUpdate = [...priorAccountUpdatesFlat] .reverse() .find((body_) => @@ -1560,7 +1592,7 @@ const ProofAuthorization = { setLazyProof( accountUpdate: AccountUpdate, proof: Omit, - priorAccountUpdates: AccountUpdate[] + priorAccountUpdates: AccountUpdateLayout ) { this.setKind(accountUpdate, priorAccountUpdates); accountUpdate.authorization = {}; @@ -1599,10 +1631,13 @@ function diffRecursive( ) { let { transaction, index, accountUpdate: input } = inputData; diff(transaction, index, prover.toPretty(), input.toPretty()); - let nChildren = input.children.accountUpdates.length; + // TODO + let inputChildren = accountUpdateLayout()!.get(input)!.children.mutable!; + let proverChildren = accountUpdateLayout()!.get(prover)!.children.mutable!; + let nChildren = inputChildren.length; for (let i = 0; i < nChildren; i++) { - let inputChild = input.children.accountUpdates[i]; - let child = prover.children.accountUpdates[i]; + let inputChild = inputChildren[i].mutable; + let child = proverChildren[i].mutable; if (!inputChild || !child) return; diffRecursive(child, { transaction, index, accountUpdate: inputChild }); } diff --git a/src/mina-signer/src/sign-zkapp-command.ts b/src/mina-signer/src/sign-zkapp-command.ts index 004a3f0940..0dc1f79a95 100644 --- a/src/mina-signer/src/sign-zkapp-command.ts +++ b/src/mina-signer/src/sign-zkapp-command.ts @@ -28,6 +28,7 @@ export { verifyAccountUpdateSignature, accountUpdatesToCallForest, callForestHash, + callForestHashGeneric, accountUpdateHash, feePayerHash, createFeePayer, @@ -156,11 +157,25 @@ function accountUpdateHash(update: AccountUpdate) { return hashWithPrefix(prefixes.body, fields); } -function callForestHash(forest: CallForest): Field { - let stackHash = 0n; +function callForestHash(forest: CallForest): bigint { + return callForestHashGeneric(forest, accountUpdateHash, hashWithPrefix, 0n); +} + +function callForestHashGeneric( + forest: CallForest, + hash: (a: A) => F, + hashWithPrefix: (prefix: string, input: F[]) => F, + emptyHash: F +): F { + let stackHash = emptyHash; for (let callTree of [...forest].reverse()) { - let calls = callForestHash(callTree.children); - let treeHash = accountUpdateHash(callTree.accountUpdate); + let calls = callForestHashGeneric( + callTree.children, + hash, + hashWithPrefix, + emptyHash + ); + let treeHash = hash(callTree.accountUpdate); let nodeHash = hashWithPrefix(prefixes.accountUpdateNode, [ treeHash, calls, diff --git a/tests/vk-regression/vk-regression.json b/tests/vk-regression/vk-regression.json index 201702e8d8..6e8a9a738d 100644 --- a/tests/vk-regression/vk-regression.json +++ b/tests/vk-regression/vk-regression.json @@ -1,22 +1,22 @@ { "Voting_": { - "digest": "27e70f037e04461a113b08cf9633feee59f84edeccb059022ced1a98ded95064", + "digest": "232c8378880bbb68117f00d34e442aa804f42255a6f02d343b738b8171d6ce65", "methods": { "voterRegistration": { - "rows": 1262, - "digest": "c826896d08315d78c96d09a638934600" + "rows": 1259, + "digest": "de76523858f6497e67b359668a14f51d" }, "candidateRegistration": { - "rows": 1262, - "digest": "c4e91618c623fbf3fcc8e489a50a7d46" + "rows": 1259, + "digest": "737730be005f7d071d339e036353ef7b" }, "approveRegistrations": { - "rows": 1150, - "digest": "d8fab66c623d2361b20328162c4119b1" + "rows": 1148, + "digest": "08b40e8d2a9ea7369c371afd2e4e1f54" }, "vote": { - "rows": 1676, - "digest": "31ca7fde9282e5de63e94112ac57cf51" + "rows": 1673, + "digest": "37fdfc514d42b21cbe6f0b914bd88efa" }, "countVotes": { "rows": 5796, @@ -24,16 +24,16 @@ } }, "verificationKey": { - "data": "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", - "hash": "18877603486899771012708643218593061070120605011629548276683277334885785617595" + "data": "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", + "hash": "15776151091008092402671254671490268418790844901048023495826534500844684898367" } }, "Membership_": { - "digest": "ceb4d74a167be69adf0ef8ee382ab94b1f719baef157b436124b5899fe2ccb1", + "digest": "35aa3fb2f5114cb29b575c70c3cef15e6538c66074876997d0005cd51925cf9", "methods": { "addEntry": { - "rows": 1355, - "digest": "112d1f76c4f742d7442c9c2bfa1276ee" + "rows": 1353, + "digest": "9c4e2d1faf08ecf2ab6d46e29d3e7063" }, "isMember": { "rows": 469, @@ -45,8 +45,8 @@ } }, "verificationKey": { - "data": "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", - "hash": "5535930703920638657421952138351450446443212191906585261070337225336596376335" + "data": "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", + "hash": "26245886907208238580957987655381478268353243870285421747846360000722576368055" } }, "HelloWorld": { @@ -63,15 +63,15 @@ } }, "TokenContract": { - "digest": "1355cefd6d9405bd490d1cdc5138cd1f4c0a874d6c08ccff7577bf1db33ac2f3", + "digest": "13926c08b6e9f7e6dd936edc1d019db122882c357b6e5f3baf416e06530c608a", "methods": { "init": { "rows": 655, - "digest": "5ab91ce22c35bb2dba025b823eb7aa4e" + "digest": "d535a589774b32cd36e2d6c3707afd30" }, "init2": { "rows": 652, - "digest": "7abe7b8feb9e908aa0a128c57fef92ab" + "digest": "4a781d299f945218e93f3d9235549373" }, "approveBase": { "rows": 13244, @@ -79,37 +79,37 @@ } }, "verificationKey": { - "data": "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", - "hash": "28008044568252732309294943203765202925095616783374137364924282422136908799323" + "data": "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", + "hash": "8425314086810278038786818130066444259495740778554070932241514249764148251692" } }, "Dex": { - "digest": "f4d4a511ccaa3e2f4d793387ac561fbe109e700f60dd2ac0c269be720ba0e61", + "digest": "1adce234400fe0b0ea8b1e625256538660d9ed6e15767e2ac78bb7a59d83a3af", "methods": { "supplyLiquidityBase": { - "rows": 2884, - "digest": "c39cfd5031cb9e938f9748f4c3504b25" + "rows": 2882, + "digest": "9930f9a0b82eff6247cded6430c6356f" }, "swapX": { - "rows": 1565, - "digest": "0d084e4bebf31426806d8d616879afa8" + "rows": 1563, + "digest": "e6c2a260178af42268a1020fc8e6113d" }, "swapY": { - "rows": 1565, - "digest": "20e2671dea607a4f0d1508faa4483dbe" + "rows": 1563, + "digest": "8a3959ec3394716f1979cf013a9a4ced" }, "burnLiquidity": { "rows": 718, - "digest": "d205382392651e933604c5d0e5ddeceb" + "digest": "6c406099fe2d2493bd216f9bbe3ba934" }, "transfer": { "rows": 1044, - "digest": "837b98775353a242e8f57bcbaddf11d8" + "digest": "1df1d01485d388ee364156f940214d23" } }, "verificationKey": { - "data": "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", - "hash": "6889182109222827677998879306887098403215976687219856364080055862647496337052" + "data": "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", + "hash": "23594323045578615602880853374590447788338441806100547122393736875331781522763" } }, "Group Primitive": {