o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
1,273 lines (1,272 loc) • 58.4 kB
JavaScript
var _a, _b;
import { cloneCircuitValue, StructNoJson } from '../../provable/types/struct.js';
import { provable, provableExtends, provablePure } from '../../provable/types/provable-derivers.js';
import { memoizationContext, memoizeWitness, Provable } from '../../provable/provable.js';
import { Field, Bool } from '../../provable/wrapped.js';
import { Pickles } from '../../../bindings.js';
import { jsLayout } from '../../../bindings/mina-transaction/gen/v1/js-layout.js';
import { Types, toJSONEssential } from '../../../bindings/mina-transaction/v1/types.js';
import { PublicKey } from '../../provable/crypto/signature.js';
import { UInt64, UInt32, Int64 } from '../../provable/int.js';
import { Preconditions, preconditions, getAccountPreconditions, } from './precondition.js';
import { dummyBase64Proof, Prover } from '../../proof-system/zkprogram.js';
import { Memo } from '../../../mina-signer/src/memo.js';
import { Events as BaseEvents, Actions as BaseActions, MayUseToken as BaseMayUseToken, } from '../../../bindings/mina-transaction/v1/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 { Signature, signFieldElement, zkAppBodyPrefix, } from '../../../mina-signer/src/signature.js';
import { MlFieldConstArray } from '../../ml/fields.js';
import { accountUpdatesToCallForest, 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';
// external API
export { AccountUpdate, Permissions, ZkappPublicInput, TransactionVersion, AccountUpdateForest, AccountUpdateTree, };
// internal API
export { Permission, Preconditions, Body, Authorization, ZkappCommand, addMissingSignatures, addMissingProofs, Events, Actions, TokenId, zkAppProver, dummySignature, AccountUpdateTreeBase, AccountUpdateLayout, hashAccountUpdate, HashedAccountUpdate, };
const TransactionVersion = {
current: () => UInt32.from(protocolVersions.txnVersion),
};
let zkAppProver = Prover();
const MayUseToken = {
type: BaseMayUseToken,
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 }, }, }) => parentsOwnToken.or(inheritFromParent).not(),
isParentsOwnToken: (a) => a.body.mayUseToken.parentsOwnToken,
isInheritFromParent: (a) => a.body.mayUseToken.inheritFromParent,
};
const Events = {
...BaseEvents,
pushEvent(events, event) {
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;
},
};
const Actions = {
...BaseActions,
pushEvent(actions, action) {
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;
},
};
const True = () => Bool(true);
const False = () => Bool(false);
class VerificationKeyPermission {
constructor(auth, txnVersion) {
this.auth = auth;
this.txnVersion = txnVersion;
}
// TODO this class could be made incompatible with a plain object (breaking change)
// private _ = undefined;
static withCurrentVersion(perm) {
return new VerificationKeyPermission(perm, TransactionVersion.current());
}
}
let Permission = {
/**
* Modification is impossible.
*/
impossible: () => ({
constant: True(),
signatureNecessary: True(),
signatureSufficient: False(),
}),
/**
* Modification is always permitted
*/
none: () => ({
constant: True(),
signatureNecessary: False(),
signatureSufficient: True(),
}),
/**
* Modification is permitted by zkapp proofs only
*/
proof: () => ({
constant: False(),
signatureNecessary: False(),
signatureSufficient: False(),
}),
/**
* Modification is permitted by signatures only, using the private key of the zkapp account
*/
signature: () => ({
constant: False(),
signatureNecessary: True(),
signatureSufficient: True(),
}),
/**
* Modification is permitted by zkapp proofs or signatures
*/
proofOrSignature: () => ({
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()),
},
};
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: () => ({
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: () => ({
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: () => ({
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: () => ({
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) => {
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) => {
return Object.fromEntries(Object.entries(permissions).map(([k, v]) => [
k,
Permissions.fromString(typeof v === 'string' ? v : v.auth),
]));
},
};
const Body = {
/**
* A body that doesn't change the underlying account record
*/
keepAll(publicKey, tokenId, mayUseToken) {
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() {
return Types.AccountUpdate.empty().body;
},
};
const FeePayerBody = {
keepAll(publicKey, nonce) {
return {
publicKey,
nonce,
fee: UInt64.zero,
validUntil: undefined,
};
},
};
const AccountId = provable({ tokenOwner: PublicKey, parentTokenId: Field });
const TokenId = {
...Types.TokenId,
...Base58TokenId,
get default() {
return Field(1);
},
derive(tokenOwner, parentTokenId = Field(1)) {
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 {
constructor(body, authorization = {}, isSelf = false) {
/**
* A human-readable label for the account update, indicating how that update
* was created. Can be modified by applications to add richer information.
*/
this.label = '';
this.lazyAuthorization = undefined;
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) {
let body = cloneCircuitValue(accountUpdate.body);
let authorization = cloneCircuitValue(accountUpdate.authorization);
let cloned = new AccountUpdate(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, }) {
let receiver;
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.default(to, this.body.tokenId);
receiver.label = `${this.label ?? 'Unlabeled'}.send()`;
this.approve(receiver);
}
// Sub the amount from the sender's account
this.body.balanceChange = this.body.balanceChange.sub(amount);
// Add the amount to the receiver's account
receiver.body.balanceChange = 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) {
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) {
accountUpdate.body.balanceChange = accountUpdate.body.balanceChange.add(x);
},
subInPlace(x) {
accountUpdate.body.balanceChange = accountUpdate.body.balanceChange.sub(x);
},
};
}
get balanceChange() {
return this.body.balanceChange;
}
set balanceChange(x) {
this.body.balanceChange = x;
}
get update() {
return this.body.update;
}
static setValue(maybeValue, value) {
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(property, lower, upper) {
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(property, value) {
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() {
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) {
feePayer.body.nonce = this.getNonce(feePayer);
feePayer.authorization = dummySignature();
feePayer.lazyAuthorization = { kind: 'lazy-signature' };
}
static getNonce(accountUpdate) {
return AccountUpdate.getSigningInfo(accountUpdate).nonce;
}
static getSigningInfo(accountUpdate) {
return memoizeWitness(AccountUpdate.signingInfo, () => AccountUpdate.getSigningInfoUnchecked(accountUpdate));
}
static getSigningInfoUnchecked(update) {
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, (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) {
return Types.AccountUpdate.toJSON(a);
}
static fromJSON(json) {
let accountUpdate = Types.AccountUpdate.fromJSON(json);
return new AccountUpdate(accountUpdate.body, accountUpdate.authorization);
}
hash() {
let input = Types.AccountUpdate.toInput(this);
return hashWithPrefix(zkAppBodyPrefix(activeInstance.getNetworkId()), packToFields(input));
}
toPublicInput({ accountUpdates }) {
let accountUpdate = this.hash();
// collect this update's descendants
let descendants = [];
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() {
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 });
}
/**
* 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, tokenId) {
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, nonce) {
let body = FeePayerBody.keepAll(address, nonce);
return {
body,
authorization: dummySignature(),
lazyAuthorization: { kind: 'lazy-signature' },
};
}
static dummyFeePayer() {
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, tokenId) {
let accountUpdate = AccountUpdate.default(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, publicKey, tokenId) {
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) {
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) {
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, tokenId) {
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, 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 toAuxiliary(a) {
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 empty() {
return AccountUpdate.dummy();
}
static fromFields(fields, [other, aux]) {
let accountUpdate = Types.AccountUpdate.fromFields(fields, aux);
return Object.assign(new AccountUpdate(accountUpdate.body, accountUpdate.authorization), other);
}
static fromValue(value) {
if (value instanceof AccountUpdate)
return value;
let accountUpdate = Types.AccountUpdate.fromValue(value);
return new AccountUpdate(accountUpdate.body, accountUpdate.authorization);
}
/**
* This function acts as the `check()` method on an `AccountUpdate` that is sent to the Mina node as part of a transaction.
*
* Background: the Mina node performs most necessary validity checks on account updates, both in- and outside of circuits.
* To save constraints, we don't repeat these checks in zkApps in places where we can be sure the checked account updates
* will be part of a transaction.
*
* However, there are a few checks skipped by the Mina node, that could cause vulnerabilities in zkApps if
* not checked in the zkApp proof itself. Adding these extra checks is the purpose of this function.
*/
static clientSideOnlyChecks(au) {
// canonical int64 representation of the balance change
Int64.check(au.body.balanceChange);
}
static witness(resultType, compute, { skipCheck = false } = {}) {
// construct the circuit type for a accountUpdate + other result
let accountUpdate = skipCheck
? {
...provable(AccountUpdate),
check: AccountUpdate.clientSideOnlyChecks,
}
: AccountUpdate;
let combinedType = provable({ accountUpdate, result: resultType });
return Provable.witnessAsync(combinedType, compute);
}
/**
* Returns a JSON representation of only the fields that differ from the
* default {@link AccountUpdate}.
*/
toPretty() {
function short(s) {
return '..' + s.slice(-4);
}
let jsonUpdate = toJSONEssential(jsLayout.AccountUpdate, this);
let body = jsonUpdate.body;
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);
}
if (body.preconditions?.network) {
body.preconditions.network = JSON.stringify(body.preconditions.network);
}
if (body.preconditions?.validWhile) {
body.preconditions.validWhile = JSON.stringify(body.preconditions.validWhile);
}
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),
});
}
for (let key of ['permissions', 'appState', 'timing']) {
if (body.update?.[key]) {
body.update[key] = JSON.stringify(body.update[key]);
}
}
for (let key of ['events', 'actions']) {
if (body[key]) {
body[key] = JSON.stringify(body[key]);
}
}
if (body.authorizationKind?.isProved === false) {
delete body.authorizationKind?.verificationKeyHash;
}
if (body.authorizationKind?.isProved === false && body.authorizationKind?.isSigned === false) {
delete body.authorizationKind;
}
if (jsonUpdate.authorization !== undefined ||
body.authorizationKind?.isProved === true ||
body.authorizationKind?.isSigned === true) {
body.authorization = jsonUpdate.authorization;
}
body.mayUseToken = {
parentsOwnToken: this.body.mayUseToken.parentsOwnToken.toBoolean(),
inheritFromParent: this.body.mayUseToken.inheritFromParent.toBoolean(),
};
let pretty = { ...body };
let withId = false;
if (withId)
pretty = { id: Math.floor(this.id * 1000), ...pretty };
if (this.label)
pretty = { label: this.label, ...pretty };
return pretty;
}
}
AccountUpdate.Actions = Actions;
AccountUpdate.Events = Events;
AccountUpdate.signingInfo = provable({
isSameAsFeePayer: Bool,
nonce: UInt32,
});
// static methods that implement Provable<AccountUpdate>
AccountUpdate.sizeInFields = Types.AccountUpdate.sizeInFields;
AccountUpdate.toFields = Types.AccountUpdate.toFields;
AccountUpdate.toInput = Types.AccountUpdate.toInput;
AccountUpdate.check = Types.AccountUpdate.check;
AccountUpdate.toValue = Types.AccountUpdate.toValue;
AccountUpdate.MayUseToken = MayUseToken;
// call forest stuff
function hashAccountUpdate(update) {
return genericHash(AccountUpdate, zkAppBodyPrefix(activeInstance.getNetworkId()), update);
}
class HashedAccountUpdate extends Hashed.create(AccountUpdate, hashAccountUpdate) {
}
const AccountUpdateTreeBase = StructNoJson({
id: RandomId,
accountUpdate: HashedAccountUpdate,
children: MerkleListBase(),
});
/**
* 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 (_b = MerkleList.create(AccountUpdateTreeBase, merkleListHash)) {
push(update) {
return super.push(update instanceof AccountUpdate ? AccountUpdateTree.from(update) : update);
}
pushIf(condition, update) {
return super.pushIf(condition, update instanceof AccountUpdate ? AccountUpdateTree.from(update) : update);
}
static fromFlatArray(updates) {
let simpleForest = accountUpdatesToCallForest(updates);
return this.fromSimpleForest(simpleForest);
}
toFlatArray(mutate = true, depth = 0) {
return _a.toFlatArray(this, mutate, depth);
}
static toFlatArray(forest, mutate = true, depth = 0) {
let flat = [];
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;
}
static fromSimpleForest(simpleForest) {
let nodes = simpleForest.map((node) => {
let accountUpdate = HashedAccountUpdate.hash(node.accountUpdate);
let children = _a.fromSimpleForest(node.children);
return { accountUpdate, children, id: node.accountUpdate.id };
});
return _a.fromReverse(nodes);
}
// TODO this comes from paranoia and might be removed later
static assertConstant(forest) {
Provable.asProver(() => {
forest.data.get().forEach(({ element: tree }) => {
assert(Provable.isConstant(AccountUpdate, tree.accountUpdate.value.get()), 'account update not constant');
_a.assertConstant(tree.children);
});
});
}
// fix static methods
static empty() {
return _a.provable.empty();
}
static from(array) {
return new _a(super.from(array));
}
static fromReverse(array) {
return new _a(super.fromReverse(array));
}
}
_a = AccountUpdateForest;
AccountUpdateForest.provable = provableExtends(_a, Reflect.get(_b, "provable", _a));
/**
* 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,
children: AccountUpdateForest,
}) {
/**
* Create a tree of account updates which only consists of a root.
*/
static from(update, hash) {
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, hash) {
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, aux) {
return new AccountUpdateTree(super.fromFields(fields, aux));
}
static empty() {
return new AccountUpdateTree(super.empty());
}
}
// how to hash a forest
function merkleListHash(forestHash, tree) {
return hashCons(forestHash, hashNode(tree));
}
function hashNode(tree) {
return Poseidon.hashWithPrefix(prefixes.accountUpdateNode, [
tree.accountUpdate.hash,
tree.children.hash,
]);
}
function hashCons(forestHash, nodeHash) {
return Poseidon.hashWithPrefix(prefixes.accountUpdateCons, [nodeHash, forestHash]);
}
class UnfinishedForest {
isFinal() {
return this.final !== undefined;
}
isMutable() {
return this.mutable !== undefined;
}
constructor(mutable, final) {
assert((final === undefined) !== (mutable === undefined), 'final or mutable');
this.final = final;
this.mutable = mutable;
}
static empty() {
return new UnfinishedForest([]);
}
setFinal(final) {
return Object.assign(this, { final, mutable: undefined });
}
finalize() {
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() {
let final = Provable.witness(AccountUpdateForest, () => this.finalize());
return this.setFinal(final);
}
push(node) {
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);
}
remove(node) {
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) {
if (this.isMutable()) {
assert(this.mutable.length === 0, 'Replacing a mutable forest that has existing children might be a mistake.');
}
return this.setFinal(new AccountUpdateForest(forest));
}
static fromForest(forest) {
return UnfinishedForest.empty().setToForest(forest);
}
toFlatArray(mutate = true, depth = 0) {
if (this.isFinal())
return this.final.toFlatArray(mutate, depth);
assert(this.isMutable(), 'final or mutable');
let flatUpdates = [];
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;
}
toConstantInPlace() {
if (this.isFinal()) {
this.final.hash = this.final.hash.toConstant();
return;
}
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();
}
}
print() {
let indent = 0;
let layout = '';
let toPretty = (a) => {
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 || '<no 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) {
if (update instanceof AccountUpdate) {
return {
mutable: update,
id: update.id,
isDummy: update.isDummy(),
children: UnfinishedForest.empty(),
};
}
return {
final: update.accountUpdate,
id: update.id,
isDummy: Bool(false),
children: UnfinishedForest.fromForest(update.children),
};
},
setTo(node, update) {
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),
});
}
},
finalize(node) {
let accountUpdate = node.final ?? HashedAccountUpdate.hash(node.mutable);
let children = node.children.finalize();
return { accountUpdate, id: node.id, isDummy: node.isDummy, children };
},
isUnfinished(input) {
return 'final' in input || 'mutable' in input;
},
};
class AccountUpdateLayout {
constructor(root) {
this.map = new Map();
root ??= AccountUpdate.dummy();
let rootTree = {
mutable: root,
id: root.id,
isDummy: Bool(false),
children: UnfinishedForest.empty(),
};
this.map.set(root.id, rootTree);
this.root = rootTree;
}
get(update) {
return this.map.get(update.id);
}
getOrCreate(update) {
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, child) {
let parentNode = this.getOrCreate(parent);
let childNode = this.getOrCreate(child);
parentNode.children.push(childNode);
}
pushTopLevel(child) {
this.pushChild(this.root, child);
}
setChildren(parent, children) {
let parentNode = this.getOrCreate(parent);
parentNode.children.setToForest(children);
}
setTopLevel(children) {
this.setChildren(this.root, children);
}
disattach(update) {
let node = this.get(update);
node?.siblings?.remove(node);
return node;
}
finalizeAndRemove(update) {
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 }) {
return this.root.children.toFlatArray(mutate);
}
forEachPredecessor(update, callback) {
let updates = this.toFlatList({ mutate: false });
for (let otherUpdate of updates) {
if (otherUpdate.id === update.id)
return;
callback(otherUpdate);
}
}
toConstantInPlace() {
this.root.children.toConstantInPlace();
}
}
const ZkappCommand = {
toPretty(transaction) {
let feePayer = ZkappCommand.toJSON(transaction).feePayer;
feePayer.body.publicKey = '..' + feePayer.body.publicKey.slice(-4);
feePayer.body.authorization = '..' + feePayer.authorization.slice(-4);
if (feePayer.body.validUntil === null)
delete feePayer.body.validUntil;
return [
{ label: 'feePayer', ...feePayer.body },
...transaction.accountUpdates.map((a) => a.toPretty()),
];