UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

1,592 lines (1,467 loc) 67.4 kB
import { cloneCircuitValue, FlexibleProvable, StructNoJson, } from '../provable/types/struct.js'; import { provable, provablePure } from '../provable/types/provable-derivers.js'; import { memoizationContext, memoizeWitness, Provable, } from '../provable/provable.js'; import { Field, Bool } from '../provable/wrapped.js'; import { Pickles, Test } from '../../snarky.js'; import { jsLayout } from '../../bindings/mina-transaction/gen/js-layout.js'; import { Types, toJSONEssential, } from '../../bindings/mina-transaction/types.js'; import { PrivateKey, PublicKey } from '../provable/crypto/signature.js'; import { UInt64, UInt32, Int64, Sign } from '../provable/int.js'; import type { SmartContract } from './zkapp.js'; import { Preconditions, Account, Network, CurrentSlot, preconditions, OrIgnore, ClosedInterval, getAccountPreconditions, } from './precondition.js'; import { dummyBase64Proof, Empty, Proof, Prover, } from '../proof-system/zkprogram.js'; import { Memo } from '../../mina-signer/src/memo.js'; import { Events as BaseEvents, Actions as BaseActions, } from '../../bindings/mina-transaction/transaction-leaves.js'; import { TokenId as Base58TokenId } from './base58-encodings.js'; import { hashWithPrefix, packToFields, Poseidon, } from '../provable/crypto/poseidon.js'; import { mocks, prefixes, protocolVersions, } from '../../bindings/crypto/constants.js'; import { MlArray } from '../ml/base.js'; import { Signature, signFieldElement, zkAppBodyPrefix, } from '../../mina-signer/src/signature.js'; import { MlFieldConstArray } from '../ml/fields.js'; import { accountUpdatesToCallForest, CallForest, callForestHashGeneric, transactionCommitments, } from '../../mina-signer/src/sign-zkapp-command.js'; import { currentTransaction } from './transaction-context.js'; import { isSmartContract } from './smart-contract-base.js'; import { activeInstance } from './mina-instance.js'; import { emptyHash, genericHash, MerkleList, MerkleListBase, } from '../provable/merkle-list.js'; import { Hashed } from '../provable/packed.js'; import { accountUpdateLayout, smartContractContext, } from './smart-contract-context.js'; import { assert } from '../util/assert.js'; import { RandomId } from '../provable/types/auxiliary.js'; import { From } from '../../bindings/lib/provable-generic.js'; // external API export { AccountUpdate, Permissions, ZkappPublicInput, TransactionVersion, AccountUpdateForest, AccountUpdateTree, }; // internal API export { SetOrKeep, Permission, Preconditions, Body, Authorization, FeePayerUnsigned, ZkappCommand, addMissingSignatures, addMissingProofs, Events, Actions, TokenId, CallForest, zkAppProver, dummySignature, LazyProof, AccountUpdateTreeBase, AccountUpdateLayout, hashAccountUpdate, HashedAccountUpdate, }; const TransactionVersion = { current: () => UInt32.from(protocolVersions.txnVersion), }; type ZkappProverData = { transaction: ZkappCommand; accountUpdate: AccountUpdate; index: number; }; let zkAppProver = Prover<ZkappProverData>(); type AuthRequired = Types.Json.AuthRequired; type AccountUpdateBody = Types.AccountUpdate['body']; type Update = AccountUpdateBody['update']; type MayUseToken = AccountUpdateBody['mayUseToken']; type Events = BaseEvents; const Events = { ...BaseEvents, pushEvent(events: Events, event: Field[]): Events { events = BaseEvents.pushEvent(events, event); Provable.asProver(() => { // make sure unconstrained data is stored as constants events.data[0] = events.data[0].map((e) => Field(Field.toBigint(e))); }); return events; }, }; type Actions = BaseActions; const Actions = { ...BaseActions, pushEvent(actions: Actions, action: Field[]): Actions { actions = BaseActions.pushEvent(actions, action); Provable.asProver(() => { // make sure unconstrained data is stored as constants actions.data[0] = actions.data[0].map((e) => Field(Field.toBigint(e))); }); return actions; }, }; /** * Either set a value or keep it the same. */ type SetOrKeep<T> = { isSome: Bool; value: T }; const True = () => Bool(true); const False = () => Bool(false); /** * One specific permission value. * * A {@link Permission} tells one specific permission for our zkapp how it * should behave when presented with requested modifications. * * Use static factory methods on this class to use a specific behavior. See * documentation on those methods to learn more. */ type Permission = Types.AuthRequired; class VerificationKeyPermission { constructor(public auth: Permission, public txnVersion: UInt32) {} // TODO this class could be made incompatible with a plain object (breaking change) // private _ = undefined; static withCurrentVersion(perm: Permission) { return new VerificationKeyPermission(perm, TransactionVersion.current()); } } let Permission = { /** * Modification is impossible. */ impossible: (): Permission => ({ constant: True(), signatureNecessary: True(), signatureSufficient: False(), }), /** * Modification is always permitted */ none: (): Permission => ({ constant: True(), signatureNecessary: False(), signatureSufficient: True(), }), /** * Modification is permitted by zkapp proofs only */ proof: (): Permission => ({ constant: False(), signatureNecessary: False(), signatureSufficient: False(), }), /** * Modification is permitted by signatures only, using the private key of the zkapp account */ signature: (): Permission => ({ constant: False(), signatureNecessary: True(), signatureSufficient: True(), }), /** * Modification is permitted by zkapp proofs or signatures */ proofOrSignature: (): Permission => ({ constant: False(), signatureNecessary: False(), signatureSufficient: True(), }), /** * Special Verification key permissions. * * The difference to normal permissions is that `Permission.proof` and `Permission.impossible` are replaced by less restrictive permissions: * - `impossible` is replaced by `impossibleDuringCurrentVersion` * - `proof` is replaced by `proofDuringCurrentVersion` * * The issue is that a future hardfork which changes the proof system could mean that old verification keys can no longer * be used to verify proofs in the new proof system, and the zkApp would have to be redeployed to adapt the verification key. * * Having either `impossible` or `proof` would mean that these zkApps can't be upgraded after this hypothetical hardfork, and would become unusable. * * Such a future hardfork would manifest as an increment in the "transaction version" of zkApps, which you can check with {@link TransactionVersion.current()}. * * The `impossibleDuringCurrentVersion` and `proofDuringCurrentVersion` have an additional `txnVersion` field. * These permissions follow the same semantics of not upgradable, or only upgradable with proofs, * _as long as_ the current transaction version is the same as the one on the permission. * * Once the current transaction version is higher than the one on the permission, the permission is treated as `signature`, * and the zkApp can be redeployed with a signature of the original account owner. */ VerificationKey: { /** * Modification is impossible, as long as the network accepts the current {@link TransactionVersion}. * * After a hardfork that increments the transaction version, the permission is treated as `signature`. */ impossibleDuringCurrentVersion: () => VerificationKeyPermission.withCurrentVersion(Permission.impossible()), /** * Modification is always permitted */ none: () => VerificationKeyPermission.withCurrentVersion(Permission.none()), /** * Modification is permitted by zkapp proofs only; as long as the network accepts the current {@link TransactionVersion}. * * After a hardfork that increments the transaction version, the permission is treated as `signature`. */ proofDuringCurrentVersion: () => VerificationKeyPermission.withCurrentVersion(Permission.proof()), /** * Modification is permitted by signatures only, using the private key of the zkapp account */ signature: () => VerificationKeyPermission.withCurrentVersion(Permission.signature()), /** * Modification is permitted by zkapp proofs or signatures */ proofOrSignature: () => VerificationKeyPermission.withCurrentVersion( Permission.proofOrSignature() ), }, }; // TODO: we could replace the interface below if we could bridge annotations from OCaml type Permissions_ = Update['permissions']['value']; /** * Permissions specify how specific aspects of the zkapp account are allowed * to be modified. All fields are denominated by a {@link Permission}. */ interface Permissions extends Permissions_ { /** * The {@link Permission} corresponding to the 8 state fields associated with * an account. */ editState: Permission; /** * The {@link Permission} corresponding to the ability to send transactions * from this account. */ send: Permission; /** * The {@link Permission} corresponding to the ability to receive transactions * to this account. */ receive: Permission; /** * The {@link Permission} corresponding to the ability to set the delegate * field of the account. */ setDelegate: Permission; /** * The {@link Permission} corresponding to the ability to set the permissions * field of the account. */ setPermissions: Permission; /** * The {@link Permission} corresponding to the ability to set the verification * key associated with the circuit tied to this account. Effectively * "upgradeability" of the smart contract. */ setVerificationKey: VerificationKeyPermission; /** * The {@link Permission} corresponding to the ability to set the zkapp uri * typically pointing to the source code of the smart contract. Usually this * should be changed whenever the {@link Permissions.setVerificationKey} is * changed. Effectively "upgradeability" of the smart contract. */ setZkappUri: Permission; /** * The {@link Permission} corresponding to the ability to emit actions to the account. */ editActionState: Permission; /** * The {@link Permission} corresponding to the ability to set the token symbol * for this account. */ setTokenSymbol: Permission; // TODO: doccomments incrementNonce: Permission; setVotingFor: Permission; setTiming: Permission; /** * Permission to control the ability to include _any_ account update for this * account in a transaction. Note that this is more restrictive than all other * permissions combined. For normal accounts it can safely be set to `none`, * but for token contracts this has to be more restrictive, to prevent * unauthorized token interactions -- for example, it could be * `proofOrSignature`. */ access: Permission; } let Permissions = { ...Permission, /** * Default permissions are: * * {@link Permissions.editState} = {@link Permission.proof} * * {@link Permissions.send} = {@link Permission.signature} * * {@link Permissions.receive} = {@link Permission.none} * * {@link Permissions.setDelegate} = {@link Permission.signature} * * {@link Permissions.setPermissions} = {@link Permission.signature} * * {@link Permissions.setVerificationKey} = {@link Permission.signature} * * {@link Permissions.setZkappUri} = {@link Permission.signature} * * {@link Permissions.editActionState} = {@link Permission.proof} * * {@link Permissions.setTokenSymbol} = {@link Permission.signature} * */ default: (): Permissions => ({ editState: Permission.proof(), send: Permission.proof(), receive: Permission.none(), setDelegate: Permission.signature(), setPermissions: Permission.signature(), setVerificationKey: Permission.VerificationKey.signature(), setZkappUri: Permission.signature(), editActionState: Permission.proof(), setTokenSymbol: Permission.signature(), incrementNonce: Permission.signature(), setVotingFor: Permission.signature(), setTiming: Permission.signature(), access: Permission.none(), }), initial: (): Permissions => ({ editState: Permission.signature(), send: Permission.signature(), receive: Permission.none(), setDelegate: Permission.signature(), setPermissions: Permission.signature(), setVerificationKey: Permission.VerificationKey.signature(), setZkappUri: Permission.signature(), editActionState: Permission.signature(), setTokenSymbol: Permission.signature(), incrementNonce: Permission.signature(), setVotingFor: Permission.signature(), setTiming: Permission.signature(), access: Permission.none(), }), dummy: (): Permissions => ({ editState: Permission.none(), send: Permission.none(), receive: Permission.none(), access: Permission.none(), setDelegate: Permission.none(), setPermissions: Permission.none(), setVerificationKey: Permission.VerificationKey.none(), setZkappUri: Permission.none(), editActionState: Permission.none(), setTokenSymbol: Permission.none(), incrementNonce: Permission.none(), setVotingFor: Permission.none(), setTiming: Permission.none(), }), allImpossible: (): Permissions => ({ editState: Permission.impossible(), send: Permission.impossible(), receive: Permission.impossible(), access: Permission.impossible(), setDelegate: Permission.impossible(), setPermissions: Permission.impossible(), setVerificationKey: Permission.VerificationKey.impossibleDuringCurrentVersion(), setZkappUri: Permission.impossible(), editActionState: Permission.impossible(), setTokenSymbol: Permission.impossible(), incrementNonce: Permission.impossible(), setVotingFor: Permission.impossible(), setTiming: Permission.impossible(), }), fromString: (permission: AuthRequired): Permission => { switch (permission) { case 'None': return Permission.none(); case 'Either': return Permission.proofOrSignature(); case 'Proof': return Permission.proof(); case 'Signature': return Permission.signature(); case 'Impossible': return Permission.impossible(); default: throw Error( `Cannot parse invalid permission. ${permission} does not exist.` ); } }, fromJSON: ( permissions: NonNullable< Types.Json.AccountUpdate['body']['update']['permissions'] > ): Permissions => { return Object.fromEntries( Object.entries(permissions).map(([k, v]) => [ k, Permissions.fromString(typeof v === 'string' ? v : v.auth), ]) ) as unknown as Permissions; }, }; // TODO: get docstrings from OCaml and delete this interface /** * The body of describing how some [[ AccountUpdate ]] should change. */ interface Body extends AccountUpdateBody { /** * The address for this body. */ publicKey: PublicKey; /** * Specify {@link Update}s to tweakable pieces of the account record backing * this address in the ledger. */ update: Update; /** * The TokenId for this account. */ tokenId: Field; /** * By what {@link Int64} should the balance of this account change. All * balanceChanges must balance by the end of smart contract execution. */ balanceChange: { magnitude: UInt64; sgn: Sign }; /** * Recent events that have been emitted from this account. * Events can be collected by archive nodes. * * [Check out our documentation about * Events!](https://docs.minaprotocol.com/zkapps/advanced-o1js/events) */ events: Events; /** * Recent {@link Action}s emitted from this account. * Actions can be collected by archive nodes and used in combination with * a {@link Reducer}. * * [Check out our documentation about * Actions!](https://docs.minaprotocol.com/zkapps/advanced-o1js/actions-and-reducer) */ actions: Events; /** * The type of call. */ mayUseToken: MayUseToken; callData: Field; callDepth: number; /** * A list of {@link Preconditions} that need to be fulfilled in order for * the {@link AccountUpdate} to be valid. */ preconditions: Preconditions; /** * Defines if a full commitment is required for this transaction. */ useFullCommitment: Bool; /** * Defines if the fee for creating this account should be paid out of this * account's balance change. * * This must only be true if the balance change is larger than the account * creation fee and the token ID is the default. */ implicitAccountCreationFee: Bool; /** * Defines if the nonce should be incremented with this {@link AccountUpdate}. */ incrementNonce: Bool; /** * Defines the type of authorization that is needed for this {@link * AccountUpdate}. * * A authorization can be one of three types: None, Proof or Signature */ authorizationKind: AccountUpdateBody['authorizationKind']; } const Body = { /** * A body that doesn't change the underlying account record */ keepAll( publicKey: PublicKey, tokenId?: Field, mayUseToken?: MayUseToken ): Body { let { body } = Types.AccountUpdate.empty(); body.publicKey = publicKey; if (tokenId) { body.tokenId = tokenId; body.mayUseToken = Provable.if( tokenId.equals(TokenId.default), AccountUpdate.MayUseToken.type, AccountUpdate.MayUseToken.No, AccountUpdate.MayUseToken.ParentsOwnToken ); } if (mayUseToken) { body.mayUseToken = mayUseToken; } return body; }, dummy(): Body { return Types.AccountUpdate.empty().body; }, }; type FeePayer = Types.ZkappCommand['feePayer']; type FeePayerBody = FeePayer['body']; const FeePayerBody = { keepAll(publicKey: PublicKey, nonce: UInt32): FeePayerBody { return { publicKey, nonce, fee: UInt64.zero, validUntil: undefined, }; }, }; type FeePayerUnsigned = FeePayer & { lazyAuthorization?: LazySignature | undefined; }; type Control = Types.AccountUpdate['authorization']; type LazyNone = { kind: 'lazy-none'; }; type LazySignature = { kind: 'lazy-signature' }; type LazyProof = { kind: 'lazy-proof'; methodName: string; args: any[]; previousProofs: Pickles.Proof[]; ZkappClass: typeof SmartContract; memoized: { fields: Field[]; aux: any[] }[]; blindingValue: Field; }; const AccountId = provable({ tokenOwner: PublicKey, parentTokenId: Field }); const TokenId = { ...Types.TokenId, ...Base58TokenId, get default() { return Field(1); }, derive(tokenOwner: PublicKey, parentTokenId = Field(1)): Field { let input = AccountId.toInput({ tokenOwner, parentTokenId }); return hashWithPrefix(prefixes.deriveTokenId, packToFields(input)); }, }; /** * An {@link AccountUpdate} is a set of instructions for the Mina network. * It includes {@link Preconditions} and a list of state updates, which need to * be authorized by either a {@link Signature} or {@link Proof}. */ class AccountUpdate implements Types.AccountUpdate { id: number; /** * A human-readable label for the account update, indicating how that update * was created. Can be modified by applications to add richer information. */ label: string = ''; body: Body; authorization: Control; lazyAuthorization: LazySignature | LazyProof | LazyNone | undefined = undefined; account: Account; network: Network; currentSlot: CurrentSlot; private isSelf: boolean; static Actions = Actions; static Events = Events; constructor(body: Body, authorization?: Control); constructor(body: Body, authorization: Control = {}, isSelf = false) { this.id = Math.random(); this.body = body; this.authorization = authorization; let { account, network, currentSlot } = preconditions(this, isSelf); this.account = account; this.network = network; this.currentSlot = currentSlot; this.isSelf = isSelf; } /** * Clones the {@link AccountUpdate}. */ static clone(accountUpdate: AccountUpdate) { let body = cloneCircuitValue(accountUpdate.body); let authorization = cloneCircuitValue(accountUpdate.authorization); let cloned: AccountUpdate = new (AccountUpdate as any)( body, authorization, accountUpdate.isSelf ); cloned.lazyAuthorization = accountUpdate.lazyAuthorization; cloned.id = accountUpdate.id; cloned.label = accountUpdate.label; return cloned; } get tokenId() { return this.body.tokenId; } send({ to, amount, }: { to: PublicKey | AccountUpdate | SmartContract; amount: number | bigint | UInt64; }) { let receiver: AccountUpdate; if (to instanceof AccountUpdate) { receiver = to; receiver.body.tokenId.assertEquals(this.body.tokenId); } else if (isSmartContract(to)) { receiver = to.self; receiver.body.tokenId.assertEquals(this.body.tokenId); } else { receiver = AccountUpdate.defaultAccountUpdate(to, this.body.tokenId); receiver.label = `${this.label ?? 'Unlabeled'}.send()`; this.approve(receiver); } // Sub the amount from the sender's account this.body.balanceChange = Int64.fromObject(this.body.balanceChange).sub( amount ); // Add the amount to the receiver's account receiver.body.balanceChange = Int64.fromObject( receiver.body.balanceChange ).add(amount); return receiver; } /** * 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(child: AccountUpdate | AccountUpdateTree | AccountUpdateForest) { if (child instanceof AccountUpdateForest) { accountUpdateLayout()?.setChildren(this, child); return; } if (child instanceof AccountUpdate) { child.body.callDepth = this.body.callDepth + 1; } accountUpdateLayout()?.disattach(child); accountUpdateLayout()?.pushChild(this, child); } get balance() { let accountUpdate = this; return { addInPlace(x: Int64 | UInt32 | UInt64 | string | number | bigint) { let { magnitude, sgn } = accountUpdate.body.balanceChange; accountUpdate.body.balanceChange = new Int64(magnitude, sgn).add(x); }, subInPlace(x: Int64 | UInt32 | UInt64 | string | number | bigint) { let { magnitude, sgn } = accountUpdate.body.balanceChange; accountUpdate.body.balanceChange = new Int64(magnitude, sgn).sub(x); }, }; } get balanceChange() { return Int64.fromObject(this.body.balanceChange); } set balanceChange(x: Int64) { this.body.balanceChange = x; } get update(): Update { return this.body.update; } static setValue<T>(maybeValue: SetOrKeep<T>, value: T) { maybeValue.isSome = Bool(true); maybeValue.value = value; } /** * Constrain a property to lie between lower and upper bounds. * * @param property The property to constrain * @param lower The lower bound * @param upper The upper bound * * Example: To constrain the account balance of a SmartContract to lie between * 0 and 20 MINA, you can use * * ```ts * \@method onlyRunsWhenBalanceIsLow() { * let lower = UInt64.zero; * let upper = UInt64.from(20e9); * AccountUpdate.assertBetween(this.self.body.preconditions.account.balance, lower, upper); * // ... * } * ``` */ static assertBetween<T>( property: OrIgnore<ClosedInterval<T>>, lower: T, upper: T ) { property.isSome = Bool(true); property.value.lower = lower; property.value.upper = upper; } // TODO: assertGreaterThan, assertLowerThan? /** * Fix a property to a certain value. * * @param property The property to constrain * @param value The value it is fixed to * * Example: To fix the account nonce of a SmartContract to 0, you can use * * ```ts * \@method onlyRunsWhenNonceIsZero() { * AccountUpdate.assertEquals(this.self.body.preconditions.account.nonce, UInt32.zero); * // ... * } * ``` */ static assertEquals<T extends object>( property: OrIgnore<ClosedInterval<T> | T>, value: T ) { property.isSome = Bool(true); if ('lower' in property.value && 'upper' in property.value) { property.value.lower = value; property.value.upper = value; } else { property.value = value; } } get publicKey(): PublicKey { return this.body.publicKey; } /** * Use this command if this account update should be signed by the account * owner, instead of not having any authorization. * * If you use this and are not relying on a wallet to sign your transaction, * then you should use the following code before sending your transaction: * * ```ts * let tx = await Mina.transaction(...); // create transaction as usual, using `requireSignature()` somewhere * tx.sign([privateKey]); // pass the private key of this account to `sign()`! * ``` * * Note that an account's {@link Permissions} determine which updates have to * be (can be) authorized by a signature. */ requireSignature() { let { nonce, isSameAsFeePayer } = AccountUpdate.getSigningInfo(this); // if this account is the same as the fee payer, we use the "full commitment" for replay protection this.body.useFullCommitment = isSameAsFeePayer; this.body.implicitAccountCreationFee = Bool(false); // otherwise, we increment the nonce let doIncrementNonce = isSameAsFeePayer.not(); this.body.incrementNonce = doIncrementNonce; // in this case, we also have to set a nonce precondition let lower = Provable.if(doIncrementNonce, UInt32, nonce, UInt32.zero); let upper = Provable.if(doIncrementNonce, UInt32, nonce, UInt32.MAXINT()); this.body.preconditions.account.nonce.isSome = doIncrementNonce; this.body.preconditions.account.nonce.value.lower = lower; this.body.preconditions.account.nonce.value.upper = upper; // set lazy signature Authorization.setLazySignature(this); } static signFeePayerInPlace(feePayer: FeePayerUnsigned) { feePayer.body.nonce = this.getNonce(feePayer); feePayer.authorization = dummySignature(); feePayer.lazyAuthorization = { kind: 'lazy-signature' }; } static getNonce(accountUpdate: AccountUpdate | FeePayerUnsigned) { return AccountUpdate.getSigningInfo(accountUpdate).nonce; } private static signingInfo = provable({ isSameAsFeePayer: Bool, nonce: UInt32, }); private static getSigningInfo( accountUpdate: AccountUpdate | FeePayerUnsigned ) { return memoizeWitness(AccountUpdate.signingInfo, () => AccountUpdate.getSigningInfoUnchecked(accountUpdate) ); } private static getSigningInfoUnchecked( update: AccountUpdate | FeePayerUnsigned ) { let publicKey = update.body.publicKey; let tokenId = update instanceof AccountUpdate ? update.body.tokenId : TokenId.default; let nonce = Number(getAccountPreconditions(update.body).nonce.toString()); // if the fee payer is the same account update as this one, we have to start // the nonce predicate at one higher, bc the fee payer already increases its // nonce let isFeePayer = currentTransaction()?.sender?.equals(publicKey); let isSameAsFeePayer = !!isFeePayer ?.and(tokenId.equals(TokenId.default)) .toBoolean(); 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 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), }; } toJSON() { return Types.AccountUpdate.toJSON(this); } static toJSON(a: AccountUpdate) { return Types.AccountUpdate.toJSON(a); } static fromJSON(json: Types.Json.AccountUpdate) { let accountUpdate = Types.AccountUpdate.fromJSON(json); return new AccountUpdate(accountUpdate.body, accountUpdate.authorization); } hash(): Field { let input = Types.AccountUpdate.toInput(this); return hashWithPrefix( zkAppBodyPrefix(activeInstance.getNetworkId()), packToFields(input) ); } toPublicInput({ accountUpdates, }: { accountUpdates: AccountUpdate[]; }): ZkappPublicInput { let accountUpdate = this.hash(); // 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, activeInstance.getNetworkId() ); return { accountUpdate, calls }; } toPrettyLayout() { let node = accountUpdateLayout()?.get(this); assert(node !== undefined, 'AccountUpdate not found in layout'); node.children.print(); } 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 }); } /** * @deprecated Use {@link AccountUpdate.default} instead. */ static defaultAccountUpdate(address: PublicKey, tokenId?: Field) { return AccountUpdate.default(address, tokenId); } /** * Create an account update from a public key and an optional token id. * * **Important**: This method is different from `AccountUpdate.create()`, in that it really just creates the account update object. * It does not attach the update to the current transaction or smart contract. * Use this method for lower-level operations with account updates. */ static default(address: PublicKey, tokenId?: Field) { return new AccountUpdate(Body.keepAll(address, tokenId)); } static dummy() { let dummy = new AccountUpdate(Body.dummy()); dummy.label = 'Dummy'; return dummy; } isDummy() { return this.body.publicKey.isEmpty(); } static defaultFeePayer(address: PublicKey, nonce: UInt32): FeePayerUnsigned { let body = FeePayerBody.keepAll(address, nonce); return { body, authorization: dummySignature(), lazyAuthorization: { kind: 'lazy-signature' }, }; } static dummyFeePayer(): FeePayerUnsigned { let body = FeePayerBody.keepAll(PublicKey.empty(), UInt32.zero); return { body, authorization: dummySignature() }; } /** * Creates an account update. If this is inside a transaction, the account * update becomes part of the transaction. If this is inside a smart contract * method, the account update will not only become part of the transaction, * but also becomes available for the smart contract to modify, in a way that * becomes part of the proof. */ static create(publicKey: PublicKey, tokenId?: Field) { let accountUpdate = AccountUpdate.defaultAccountUpdate(publicKey, tokenId); let insideContract = smartContractContext.get(); if (insideContract) { let self = insideContract.this.self; self.approve(accountUpdate); accountUpdate.label = `${ self.label || 'Unlabeled' } > AccountUpdate.create()`; } else { currentTransaction()?.layout.pushTopLevel(accountUpdate); accountUpdate.label = `Mina.transaction() > AccountUpdate.create()`; } return accountUpdate; } /** * Create an account update that is added to the transaction only if a condition is met. * * See {@link AccountUpdate.create} for more information. In this method, you can pass in * a condition that determines whether the account update should be added to the transaction. */ static createIf(condition: Bool, publicKey: PublicKey, tokenId?: Field) { return AccountUpdate.create( // if the condition is false, we use an empty public key, which causes the account update to be ignored // as a dummy when building the transaction Provable.if(condition, publicKey, PublicKey.empty()), tokenId ); } /** * Attach account update to the current transaction * -- if in a smart contract, to its children */ static attachToTransaction(accountUpdate: AccountUpdate) { let insideContract = smartContractContext.get(); if (insideContract) { let selfUpdate = insideContract.this.self; // avoid redundant attaching & cycle in account update structure, happens // when calling attachToTransaction(this.self) inside a @method // TODO avoid account update cycles more generally if (selfUpdate === accountUpdate) return; insideContract.this.self.approve(accountUpdate); } else { if (!currentTransaction.has()) return; currentTransaction.get().layout.pushTopLevel(accountUpdate); } } /** * Disattach an account update from where it's currently located in the transaction */ static unlink(accountUpdate: AccountUpdate) { accountUpdateLayout()?.disattach(accountUpdate); } /** * Creates an account update, like {@link AccountUpdate.create}, but also * makes sure this account update will be authorized with a signature. * * If you use this and are not relying on a wallet to sign your transaction, * then you should use the following code before sending your transaction: * * ```ts * let tx = await Mina.transaction(...); // create transaction as usual, using `createSigned()` somewhere * tx.sign([privateKey]); // pass the private key of this account to `sign()`! * ``` * * Note that an account's {@link Permissions} determine which updates have to * be (can be) authorized by a signature. */ static createSigned(publicKey: PublicKey, tokenId?: Field) { let accountUpdate = AccountUpdate.create(publicKey, tokenId); accountUpdate.label = accountUpdate.label.replace( '.create()', '.createSigned()' ); accountUpdate.requireSignature(); return accountUpdate; } /** * Use this method to pay the account creation fee for another account (or, multiple accounts using the optional second argument). * * Beware that you _don't_ need to specify the account that is created! * Instead, the protocol will automatically identify that accounts need to be created, * and require that the net balance change of the transaction covers the account creation fee. * * @param feePayer the address of the account that pays the fee * @param numberOfAccounts the number of new accounts to fund (default: 1) * @returns they {@link AccountUpdate} for the account which pays the fee */ static fundNewAccount(feePayer: PublicKey, numberOfAccounts = 1) { let accountUpdate = AccountUpdate.createSigned(feePayer); accountUpdate.label = 'AccountUpdate.fundNewAccount()'; let fee = activeInstance.getNetworkConstants().accountCreationFee; fee = fee.mul(numberOfAccounts); accountUpdate.balance.subInPlace(fee); return accountUpdate; } // static methods that implement Provable<AccountUpdate> static sizeInFields = Types.AccountUpdate.sizeInFields; static toFields = Types.AccountUpdate.toFields; static toAuxiliary(a?: AccountUpdate) { let aux = Types.AccountUpdate.toAuxiliary(a); let lazyAuthorization = a && a.lazyAuthorization; let id = a?.id ?? Math.random(); let label = a?.label ?? ''; return [{ lazyAuthorization, id, label }, aux]; } static toInput = Types.AccountUpdate.toInput; static empty() { return AccountUpdate.dummy(); } static check = Types.AccountUpdate.check; static fromFields(fields: Field[], [other, aux]: any[]): AccountUpdate { let accountUpdate = Types.AccountUpdate.fromFields(fields, aux); return Object.assign( new AccountUpdate(accountUpdate.body, accountUpdate.authorization), other ); } static toValue = Types.AccountUpdate.toValue; static fromValue( value: From<typeof Types.AccountUpdate> | AccountUpdate ): AccountUpdate { if (value instanceof AccountUpdate) return value; let accountUpdate = Types.AccountUpdate.fromValue(value); return new AccountUpdate(accountUpdate.body, accountUpdate.authorization); } static witness<T>( type: FlexibleProvable<T>, compute: () => Promise<{ accountUpdate: AccountUpdate; result: T }>, { skipCheck = false } = {} ) { // construct the circuit type for a accountUpdate + other result let accountUpdateType = skipCheck ? { ...provable(AccountUpdate), check() {} } : AccountUpdate; let combinedType = provable({ accountUpdate: accountUpdateType, result: type as any, }); return Provable.witnessAsync(combinedType, compute); } static get MayUseToken() { return { type: provablePure({ parentsOwnToken: Bool, inheritFromParent: Bool }), No: { parentsOwnToken: Bool(false), inheritFromParent: Bool(false) }, ParentsOwnToken: { parentsOwnToken: Bool(true), inheritFromParent: Bool(false), }, InheritFromParent: { parentsOwnToken: Bool(false), inheritFromParent: Bool(true), }, isNo({ body: { mayUseToken: { parentsOwnToken, inheritFromParent }, }, }: AccountUpdate) { return parentsOwnToken.or(inheritFromParent).not(); }, isParentsOwnToken(a: AccountUpdate) { return a.body.mayUseToken.parentsOwnToken; }, isInheritFromParent(a: AccountUpdate) { return a.body.mayUseToken.inheritFromParent; }, }; } /** * Returns a JSON representation of only the fields that differ from the * default {@link AccountUpdate}. */ toPretty() { function short(s: string) { return '..' + s.slice(-4); } let jsonUpdate: Partial<Types.Json.AccountUpdate> = toJSONEssential( jsLayout.AccountUpdate as any, this ); let body: Partial<Types.Json.AccountUpdate['body']> = jsonUpdate.body as any; delete body.callData; body.publicKey = short(body.publicKey!); if (body.balanceChange?.magnitude === '0') delete body.balanceChange; if (body.tokenId === TokenId.toBase58(TokenId.default)) { delete body.tokenId; } else { body.tokenId = short(body.tokenId!); } if (body.callDepth === 0) delete body.callDepth; if (body.incrementNonce === false) delete body.incrementNonce; if (body.useFullCommitment === false) delete body.useFullCommitment; if (body.implicitAccountCreationFee === false) delete body.implicitAccountCreationFee; if (body.events?.length === 0) delete body.events; if (body.actions?.length === 0) delete body.actions; if (body.preconditions?.account) { body.preconditions.account = JSON.stringify( body.preconditions.account ) as any; } if (body.preconditions?.network) { body.preconditions.network = JSON.stringify( body.preconditions.network ) as any; } if (body.preconditions?.validWhile) { body.preconditions.validWhile = JSON.stringify( body.preconditions.validWhile ) as any; } if (jsonUpdate.authorization?.proof) { jsonUpdate.authorization.proof = short(jsonUpdate.authorization.proof); } if (jsonUpdate.authorization?.signature) { jsonUpdate.authorization.signature = short( jsonUpdate.authorization.signature ); } if (body.update?.verificationKey) { body.update.verificationKey = JSON.stringify({ data: short(body.update.verificationKey.data), hash: short(body.update.verificationKey.hash), }) as any; } for (let key of ['permissions', 'appState', 'timing'] as const) { if (body.update?.[key]) { body.update[key] = JSON.stringify(body.update[key]) as any; } } for (let key of ['events', 'actions'] as const) { if (body[key]) { body[key] = JSON.stringify(body[key]) as any; } } if (body.authorizationKind?.isProved === false) { delete (body as any).authorizationKind?.verificationKeyHash; } if ( body.authorizationKind?.isProved === false && body.authorizationKind?.isSigned === false ) { delete (body as any).authorizationKind; } if ( jsonUpdate.authorization !== undefined || body.authorizationKind?.isProved === true || body.authorizationKind?.isSigned === true ) { (body as any).authorization = jsonUpdate.authorization; } body.mayUseToken = { parentsOwnToken: this.body.mayUseToken.parentsOwnToken.toBoolean(), inheritFromParent: this.body.mayUseToken.inheritFromParent.toBoolean(), }; let pretty: any = { ...body }; let withId = false; if (withId) pretty = { id: Math.floor(this.id * 1000), ...pretty }; if (this.label) pretty = { label: this.label, ...pretty }; return pretty; } } // call forest stuff function hashAccountUpdate(update: AccountUpdate) { return genericHash( AccountUpdate, zkAppBodyPrefix(activeInstance.getNetworkId()), update ); } class HashedAccountUpdate extends Hashed.create( AccountUpdate, hashAccountUpdate ) {} type AccountUpdateTreeBase = { id: number; accountUpdate: Hashed<AccountUpdate>; children: AccountUpdateForestBase; }; type AccountUpdateForestBase = MerkleListBase<AccountUpdateTreeBase>; const AccountUpdateTreeBase = StructNoJson({ id: RandomId, accountUpdate: HashedAccountUpdate.provable, children: MerkleListBase<AccountUpdateTreeBase>(), }); /** * Class which represents a forest (list of trees) of account updates, * in a compressed way which allows iterating and selectively witnessing the account updates. * * The (recursive) type signature is: * ``` * type AccountUpdateForest = MerkleList<AccountUpdateTree>; * type AccountUpdateTree = { * accountUpdate: Hashed<AccountUpdate>; * children: AccountUpdateForest; * }; * ``` */ class AccountUpdateForest extends MerkleList.create( AccountUpdateTreeBase, merkleListHash ) { 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; } private static fromSimpleForest( simpleForest: CallForest<AccountUpdate> ): 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.fromReverse(nodes); } // 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); }); }); } } /** * 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<AccountUpdate>; * children: AccountUpdateForest; * }; * type AccountUpdateForest = MerkleList<AccountUpdateTree>; * ``` */ 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) ); } else { this.children.push(update); } } // 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: AccountUpdateTreeBase) { return hashCons(forestHash, hashNode(tree)); } function hashNode(tree: AccountUpdateTreeBase) { return Poseidon.hashWithPrefix(prefixes.accountUpdateNode, [ tree.accountUpdate.hash, tree.children.hash, ]); } function hashCons(forestHash: Field, nodeHash: Field) { return Poseidon.hashWithPrefix(prefixes.accountUpdateCons, [ nodeHash, forestHash, ]); } /** * `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 UnfinishedTree = { id: number; isDummy: Bool; // `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 UnfinishedForestMutable = UnfinishedForest & { final?: undefined; mutable: UnfinishedTree[]; }; class UnfinishedForest { final?: AccountUpdateForest; mutable?: UnfinishedTree[]; isFinal(): this is UnfinishedForestFinal { return this.final !== undefined; } isMutable(): this is UnfinishedForestMutable { return this.mutable !== undefined; } constructor(mutable?: UnfinishedTree[], final?: AccountUpdateForest) { assert( (final === undefined) !== (mutable === undefined), 'final or mutable' ); this.final = final; this.mutable = mutable; } static empty(): UnfinishedForestMutable { return new UnfinishedForest([]) as any; } private setFinal(final: AccountUpdateForest): UnfinishedForestFinal { return Object.assign(this, { final, mutable: undefined }); } finalize(): AccountUpdateForest { if (this.isFinal()) return this.final; assert(this.isMutable(), 'final or mutable'); 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; } witnessHash(): UnfinishedForestFinal { let final = Provable.witness(AccountUpdateForest.provable, () => this.finalize() ); return this.setFinal(final); } 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(), 'Cann