o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
1,046 lines (941 loc) • 37.2 kB
text/typescript
import { AccountId, AccountTiming } from './account.js';
import {
AccountUpdateAuthorization,
AccountUpdateAuthorizationEnvironment,
AccountUpdateAuthorizationKind,
AccountUpdateAuthorizationKindIdentifier,
AccountUpdateAuthorizationKindWithZkappContext,
} from './authorization.js';
import { Option, TokenId, Update, ZkappUri, mapUndefined } from './core.js';
import { Permissions, PermissionsDescription } from './permissions.js';
import { Preconditions, PreconditionsDescription } from './preconditions.js';
import { GenericStateUpdates, StateDefinition, StateLayout, StateUpdates } from './state.js';
import { Pickles } from '../../../bindings.js';
import { Bool } from '../../provable/bool.js';
import { Field } from '../../provable/field.js';
import { Int64, UInt64 } from '../../provable/int.js';
import { Proof } from '../../proof-system/zkprogram.js';
import {
emptyHashWithPrefix,
hashWithPrefix,
packToFields,
TokenSymbol,
} from '../../provable/crypto/poseidon.js';
import { PublicKey } from '../../provable/crypto/signature.js';
import { HashInput } from '../../provable/types/provable-derivers.js';
import { Provable } from '../../provable/types/provable-intf.js';
import { mocks, prefixes } from '../../../bindings/crypto/constants.js';
import * as Bindings from '../../../bindings/mina-transaction/v2/index.js';
import * as PoseidonBigint from '../../../mina-signer/src/poseidon-bigint.js';
import {
Signature,
signFieldElement,
zkAppBodyPrefix,
} from '../../../mina-signer/src/signature.js';
import { NetworkId } from '../../../mina-signer/src/types.js';
import { Struct } from '../../provable/types/struct.js';
import { VerificationKey } from '../../../lib/proof-system/verification-key.js';
// TODO: make private abstractions over many fields (eg new apis for Update and Constraint.*)
// TODO: replay checks
export {
AccountUpdate,
Authorized,
GenericData,
AccountUpdateTree,
AccountUpdateTreeDescription,
ContextFreeAccountUpdateDescription,
ContextFreeAccountUpdate,
DynamicProvable,
AccountUpdateCommitment,
CommittedList,
EventsHashConfig,
ActionsHashConfig,
};
class AccountUpdateCommitment extends Struct({
accountUpdateCommitment: Field,
}) {
constructor(accountUpdateCommitment: Field) {
super({ accountUpdateCommitment });
}
}
// TODO: move elsewhere
type DynamicProvable<T> =
| Provable<T>
| {
toFields(x: T): Field[];
toAuxiliary(x: T): any[];
fromFieldsDynamic(fields: Field[], aux: any[]): { value: T; fieldsConsumed: number };
};
// TODO: move elsewhere
const GenericData: DynamicProvable<Field[]> = {
toFields(x: Field[]): Field[] {
return x;
},
toAuxiliary(x: Field[]): any[] {
return [x.length];
},
fromFieldsDynamic(fields: Field[], aux: any[]): { value: Field[]; fieldsConsumed: number } {
const [_len] = aux;
let len = _len ?? fields.length;
return { value: fields.slice(0, len), fieldsConsumed: len };
},
};
// TODO: move elsewhere
/*
interface Hashable {
hash(): Field;
}
*/
// TODO: move elsewhere
interface HashableDataConfig<Item> {
readonly emptyPrefix: string;
readonly consPrefix: string;
hash(item: Item): Field;
}
function EventsHashConfig<T>(T: DynamicProvable<T>): HashableDataConfig<T> {
return {
emptyPrefix: 'MinaZkappEventsEmpty',
consPrefix: prefixes.events,
hash(x: T): Field {
const fields = T.toFields(x);
return hashWithPrefix(prefixes.event, fields);
},
};
}
function ActionsHashConfig<T>(T: DynamicProvable<T>): HashableDataConfig<T> {
return {
emptyPrefix: 'MinaZkappActionsEmpty',
consPrefix: prefixes.sequenceEvents,
hash(x: T): Field {
const fields = T.toFields(x);
return hashWithPrefix(prefixes.event, fields);
},
};
}
// TODO: move elsewhere
class CommittedList<Item> {
readonly Item: DynamicProvable<Item>;
readonly data: Item[];
readonly hash: Field;
constructor({ Item, data, hash }: { Item: DynamicProvable<Item>; data: Item[]; hash: Field }) {
this.Item = Item;
this.data = data;
this.hash = hash;
}
toInternalRepr(): { data: Field[][]; hash: Field } {
return {
data: this.data.map(this.Item.toFields),
hash: this.hash,
};
}
// IMPORTANT: It is the callers responsibility to ensure the commitment will compute the same
// after mapping the list (this function does not check this for you at runtime).
mapUnsafe<B>(NewItem: DynamicProvable<B>, f: (a: Item) => B): CommittedList<B> {
return new CommittedList({
Item: NewItem,
data: this.data.map(f),
hash: this.hash,
});
}
static hashList<Item>(config: HashableDataConfig<Item>, items: Item[]): Field {
let hash = emptyHashWithPrefix(config.emptyPrefix);
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i];
hash = hashWithPrefix(config.consPrefix, [hash, config.hash(item)]);
}
return hash;
}
static from<Item>(
Item: DynamicProvable<Item>,
config: HashableDataConfig<Item>,
value: undefined | Item[] | CommittedList<Item> | Bindings.Leaves.CommittedList
): CommittedList<Item> {
if (value instanceof CommittedList) return value;
let items: Item[];
//let hash;
if (value === undefined) {
items = [];
} else if (value instanceof Array) {
items = value;
} else {
// TODO: think about this a bit more... we don't have the aux data here, so we should do
// something to restrict the types
if ('fromFields' in Item) {
items = value.data.map((fields) => Item.fromFields(fields, []));
} else {
items = value.data.map((fields) => {
const { value: result, fieldsConsumed } = Item.fromFieldsDynamic(fields, []);
if (fieldsConsumed !== fields.length)
throw new Error('expected all fields to be consumed when casting dynamic item');
return result;
});
}
//hash = value.hash;
}
//hash = hash ?? CommittedList.hashList(config, items);
return new CommittedList({
Item,
data: items,
hash: CommittedList.hashList(config, items),
});
}
}
interface MayUseToken {
parentsOwnToken: Bool;
inheritFromParent: Bool;
}
type AccountUpdateTreeDescription<RootDescription, Child> = RootDescription & {
// TODO: support using commitments?
children?: AccountUpdateTree<Child>[];
};
// TODO: CONSIDER -- merge this logic into AccountUpdate
// class AccountUpdateTree<AccountUpdateType> {
class AccountUpdateTree<Root, Child = Root> {
constructor(
public rootAccountUpdate: Root,
public children: AccountUpdateTree<Child, Child>[]
) {}
// depth first traversal (parents before children)
static forEachNode<T>(
tree: AccountUpdateTree<T>,
depth: number,
f: (accountUpdate: T, depth: number) => void
): void {
f(tree.rootAccountUpdate, depth);
tree.children.forEach((child) => AccountUpdateTree.forEachNode(child, depth + 1, f));
}
// inverted depth first traversal (children before parents)
static forEachNodeInverted<T>(
tree: AccountUpdateTree<T>,
depth: number,
f: (accountUpdate: T, depth: number) => void
): void {
tree.children.forEach((child) => AccountUpdateTree.forEachNodeInverted(child, depth + 1, f));
f(tree.rootAccountUpdate, depth);
}
static reduce<T, R>(tree: AccountUpdateTree<T>, f: (accountUpdate: T, childValues: R[]) => R): R {
const childValues = tree.children.map((child) => AccountUpdateTree.reduce(child, f));
return f(tree.rootAccountUpdate, childValues);
}
// TODO: delete (realized I didn't need it part way through writing)
// // build context as we descend the tree, map, then reduce as we ascend
// static mapReduceWithContext<T, Ctx, R>(
// tree: AccountUpdateTree<T>,
// context: Ctx,
// mapContext: (ctx: Ctx, index: number) => Ctx,
// map: (ctx: Ctx, accountUpdate: T) => R,
// reduce: (values: R[]) => R
// ): R {
// if(tree.children.length === 0) {
// return map(context, tree.rootAccountUpdate);
// } else {
// const reducedValues = tree.children.map((child, index) =>
// AccountUpdateTree.mapReduceWithContext(
// child,
// mapContext(context, index),
// mapContext,
// map,
// reduce
// );
// );
// return reduce(reducedValues);
// }
// }
// TODO: refactor the type parameter interfaces
static mapRoot<RootIn, RootOut, Child>(
tree: AccountUpdateTree<RootIn, Child>,
f: (accountUpdate: RootIn) => RootOut
): AccountUpdateTree<RootOut, Child> {
const newAccountUpdate = f(tree.rootAccountUpdate);
return new AccountUpdateTree(newAccountUpdate, tree.children);
}
static async map<A, B>(
tree: AccountUpdateTree<A>,
f: (accountUpdate: A) => Promise<B>
): Promise<AccountUpdateTree<B>> {
const newAccountUpdate = await f(tree.rootAccountUpdate);
const newChildren = await AccountUpdateTree.mapForest(tree.children, f);
return new AccountUpdateTree(newAccountUpdate, newChildren);
}
static mapForest<A, B>(
forest: AccountUpdateTree<A>[],
f: (a: A) => Promise<B>
): Promise<AccountUpdateTree<B>[]> {
return Promise.all(forest.map((tree) => AccountUpdateTree.map(tree, f)));
}
// TODO: I think this can be safely made polymorphic over the account update state, actions, and events representations
// TODO: Field, not bigint
static hash(tree: AccountUpdateTree<AccountUpdate>, networkId: NetworkId): bigint {
// TODO: is it ok to do this and ignore the toValue encodings entirely?
const accountUpdateFieldInput = tree.rootAccountUpdate.toInput();
const accountUpdateBigintInput = {
fields: accountUpdateFieldInput.fields?.map((f: Field) => f.toBigInt()),
packed: accountUpdateFieldInput.packed?.map(([f, n]: [Field, number]): [bigint, number] => [
f.toBigInt(),
n,
]),
};
// TODO: negotiate between this implementation and AccountUpdate#hash to figure out what is correct
// TODO NOW: ^ this was done, but we need to update this function to share code still
const accountUpdateCommitment = PoseidonBigint.hashWithPrefix(
zkAppBodyPrefix(networkId),
PoseidonBigint.packToFields(accountUpdateBigintInput)
);
const childrenCommitment = AccountUpdateTree.hashForest(networkId, tree.children);
return PoseidonBigint.hashWithPrefix(prefixes.accountUpdateNode, [
accountUpdateCommitment,
childrenCommitment,
]);
}
// TODO: Field, not bigint
static hashForest(networkId: NetworkId, forest: AccountUpdateTree<AccountUpdate>[]): bigint {
const consHash = (acc: bigint, tree: AccountUpdateTree<AccountUpdate>) =>
PoseidonBigint.hashWithPrefix(prefixes.accountUpdateCons, [
AccountUpdateTree.hash(tree, networkId),
acc,
]);
return [...forest].reverse().reduce(consHash, 0n);
}
static unrollForest<AccountUpdateType, Return>(
forest: AccountUpdateTree<AccountUpdateType>[],
f: (accountUpdate: AccountUpdateType, depth: number) => Return
): Return[] {
const seq: Return[] = [];
forest.forEach((tree) =>
AccountUpdateTree.forEachNode(tree, 0, (accountUpdate, depth) =>
seq.push(f(accountUpdate, depth))
)
);
return seq;
}
static sizeInFields(): number {
return AccountUpdate.sizeInFields();
}
static toFields(x: AccountUpdateTree<AccountUpdate>): Field[] {
return AccountUpdate.toFields(x.rootAccountUpdate);
}
static toAuxiliary(x?: AccountUpdateTree<AccountUpdate>): any[] {
return [AccountUpdate.toAuxiliary(x?.rootAccountUpdate), x?.children ?? []];
}
static fromFields(fields: Field[], aux: any[]): AccountUpdateTree<AccountUpdate> {
return new AccountUpdateTree(AccountUpdate.fromFields(fields, aux[0]), aux[1]);
}
static toValue(x: AccountUpdateTree<AccountUpdate>): AccountUpdateTree<AccountUpdate> {
return x;
}
static fromValue(x: AccountUpdateTree<AccountUpdate>): AccountUpdateTree<AccountUpdate> {
return x;
}
static check(_x: AccountUpdateTree<AccountUpdate>): void {
// TODO
}
static from<RootDescription, Root, Child>(
descr: AccountUpdateTreeDescription<RootDescription, Child>,
createAccountUpdate: (descr: RootDescription) => Root
): AccountUpdateTree<Root, Child> {
return new AccountUpdateTree(createAccountUpdate(descr), descr.children ?? []);
}
}
interface ContextFreeAccountUpdateDescription<
State extends StateLayout = 'GenericState',
Event = Field[],
Action = Field[],
> {
// TODO: accept identifiers for authorization kind
authorizationKind: AccountUpdateAuthorizationKindIdentifier | AccountUpdateAuthorizationKind;
preconditions?: PreconditionsDescription<State> | Preconditions<State>;
balanceChange?: Int64;
incrementNonce?: Bool;
useFullCommitment?: Bool;
implicitAccountCreationFee?: Bool;
mayUseToken?: MayUseToken;
pushEvents?: Event[] | CommittedList<Event>;
pushActions?: Action[] | CommittedList<Action>;
setState?: StateUpdates<State>;
setPermissions?: PermissionsDescription | Permissions | Update<Permissions>;
setDelegate?: PublicKey | Update<PublicKey>;
setVerificationKey?: VerificationKey | Update<VerificationKey>;
setZkappUri?: string | ZkappUri | Update<ZkappUri>;
setTokenSymbol?: string | TokenSymbol | Update<TokenSymbol>;
setTiming?: AccountTiming | Update<AccountTiming>;
setVotingFor?: Field | Update<Field>;
}
// in a ZkModule context: ContextFreeAccountUpdate is an AccountUpdate without an account id and call data
class ContextFreeAccountUpdate<
State extends StateLayout = 'GenericState',
Event = Field[],
Action = Field[],
> {
readonly State: StateDefinition<State>;
authorizationKind: AccountUpdateAuthorizationKind;
preconditions: Preconditions<State>;
balanceChange: Int64;
incrementNonce: Bool;
useFullCommitment: Bool;
implicitAccountCreationFee: Bool;
mayUseToken: MayUseToken;
pushEvents: CommittedList<Event>;
pushActions: CommittedList<Action>;
// TODO: standardize on these being set* for *Update, don't do both
stateUpdates: StateUpdates<State>;
permissionsUpdate: Update<Permissions>;
delegateUpdate: Update<PublicKey>;
verificationKeyUpdate: Update<VerificationKey>;
zkappUriUpdate: Update<ZkappUri>;
tokenSymbolUpdate: Update<TokenSymbol>;
timingUpdate: Update<AccountTiming>;
votingForUpdate: Update<Field>;
constructor(
State: StateDefinition<State>,
Event: DynamicProvable<Event>,
Action: DynamicProvable<Action>,
descr:
| ContextFreeAccountUpdateDescription<State, Event, Action>
| ContextFreeAccountUpdate<State, Event, Action>
) {
function castUpdate<A, B>(
value: undefined | A | Update<B>,
defaultValue: B,
f: (a: A) => B
): Update<B> {
if (value instanceof Update) {
return value;
} else {
return Update.from(mapUndefined(value, f), defaultValue);
}
}
this.State = State;
this.authorizationKind = AccountUpdateAuthorizationKind.from(descr.authorizationKind);
this.preconditions =
mapUndefined(descr.preconditions, (x) => Preconditions.from(State, x)) ??
Preconditions.emptyPoly(State);
this.balanceChange = descr.balanceChange ?? Int64.create(UInt64.zero);
this.incrementNonce = descr.incrementNonce ?? new Bool(false);
this.useFullCommitment = descr.useFullCommitment ?? new Bool(false);
this.implicitAccountCreationFee = descr.implicitAccountCreationFee ?? new Bool(false);
this.mayUseToken = descr.mayUseToken ?? {
parentsOwnToken: new Bool(false),
inheritFromParent: new Bool(false),
};
this.pushEvents = CommittedList.from(Event, EventsHashConfig(Event), descr.pushEvents);
this.pushActions = CommittedList.from(Action, ActionsHashConfig(Action), descr.pushActions);
if (descr instanceof ContextFreeAccountUpdate) {
this.stateUpdates = descr.stateUpdates;
this.permissionsUpdate = descr.permissionsUpdate;
this.delegateUpdate = descr.delegateUpdate;
this.verificationKeyUpdate = descr.verificationKeyUpdate;
this.zkappUriUpdate = descr.zkappUriUpdate;
this.tokenSymbolUpdate = descr.tokenSymbolUpdate;
this.timingUpdate = descr.timingUpdate;
this.votingForUpdate = descr.votingForUpdate;
} else {
this.stateUpdates = descr.setState ?? StateUpdates.empty(State);
this.permissionsUpdate = castUpdate(
descr.setPermissions,
Permissions.empty(),
Permissions.from
);
this.delegateUpdate = Update.from(descr.setDelegate, PublicKey.empty());
this.verificationKeyUpdate = Update.from(descr.setVerificationKey, VerificationKey.empty());
this.zkappUriUpdate = castUpdate(descr.setZkappUri, ZkappUri.empty(), ZkappUri.from);
this.tokenSymbolUpdate = castUpdate(
descr.setTokenSymbol,
TokenSymbol.empty(),
TokenSymbol.from
);
this.timingUpdate = Update.from(descr.setTiming, AccountTiming.empty());
this.votingForUpdate = Update.from(descr.setVotingFor, Field.empty());
}
}
toGeneric(): ContextFreeAccountUpdate {
return ContextFreeAccountUpdate.generic({
authorizationKind: this.authorizationKind,
preconditions: this.preconditions.toGeneric(),
balanceChange: this.balanceChange,
incrementNonce: this.incrementNonce,
useFullCommitment: this.useFullCommitment,
implicitAccountCreationFee: this.implicitAccountCreationFee,
mayUseToken: this.mayUseToken,
pushEvents: this.pushEvents.mapUnsafe(GenericData, this.pushEvents.Item.toFields),
pushActions: this.pushActions.mapUnsafe(GenericData, this.pushActions.Item.toFields),
setState: StateUpdates.toGeneric(this.State, this.stateUpdates),
setPermissions: this.permissionsUpdate,
setDelegate: this.delegateUpdate,
setVerificationKey: this.verificationKeyUpdate,
setZkappUri: this.zkappUriUpdate,
setTokenSymbol: this.tokenSymbolUpdate,
setTiming: this.timingUpdate,
setVotingFor: this.votingForUpdate,
});
}
static fromGeneric<State extends StateLayout, Event, Action>(
x: ContextFreeAccountUpdate,
State: StateDefinition<State>,
Event: DynamicProvable<Event>,
Action: DynamicProvable<Action>
): ContextFreeAccountUpdate<State, Event, Action> {
// TODO: this method is broken because we aren't storing aux data in the generic format...
return new ContextFreeAccountUpdate(State, Event, Action, {
authorizationKind: x.authorizationKind,
preconditions: Preconditions.fromGeneric(x.preconditions, State),
balanceChange: x.balanceChange,
incrementNonce: x.incrementNonce,
useFullCommitment: x.useFullCommitment,
implicitAccountCreationFee: x.implicitAccountCreationFee,
mayUseToken: x.mayUseToken,
pushEvents: x.pushEvents.mapUnsafe(Event, (fields) =>
// TODO: this is really unsafe, make it safe
'fromFieldsDynamic' in Event
? Event.fromFieldsDynamic(fields, []).value
: Event.fromFields(fields, [])
),
pushActions: x.pushActions.mapUnsafe(Action, (fields) =>
// TODO: this is really unsafe, make it safe
'fromFieldsDynamic' in Action
? Action.fromFieldsDynamic(fields, []).value
: Action.fromFields(fields, [])
),
setState: StateUpdates.fromGeneric(x.stateUpdates, State),
setPermissions: x.permissionsUpdate,
setDelegate: x.delegateUpdate,
setVerificationKey: x.verificationKeyUpdate,
setZkappUri: x.zkappUriUpdate,
setTokenSymbol: x.tokenSymbolUpdate,
setTiming: x.timingUpdate,
setVotingFor: x.votingForUpdate,
});
}
static generic(descr: ContextFreeAccountUpdateDescription): ContextFreeAccountUpdate {
return new ContextFreeAccountUpdate('GenericState', GenericData, GenericData, descr);
}
static emptyPoly<State extends StateLayout, Event, Action>(
State: StateDefinition<State>,
Event: DynamicProvable<Event>,
Action: DynamicProvable<Action>
) {
return new ContextFreeAccountUpdate(State, Event, Action, {
authorizationKind: AccountUpdateAuthorizationKind.None(),
});
}
static empty(): ContextFreeAccountUpdate {
return ContextFreeAccountUpdate.emptyPoly('GenericState', GenericData, GenericData);
}
static from<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>(
State: StateDefinition<State>,
Event: DynamicProvable<Event>,
Action: DynamicProvable<Action>,
x:
| ContextFreeAccountUpdateDescription<State, Event, Action>
| ContextFreeAccountUpdate<State, Event, Action>
| undefined
): ContextFreeAccountUpdate<State, Event, Action> {
if (x instanceof ContextFreeAccountUpdate) return x;
if (x === undefined) return ContextFreeAccountUpdate.emptyPoly(State, Event, Action);
return new ContextFreeAccountUpdate(State, Event, Action, x);
}
}
type AccountUpdateDescription<State extends StateLayout, Event = Field[], Action = Field[]> = (
| {
update: ContextFreeAccountUpdate<State, Event, Action>;
proof?: Proof<undefined, AccountUpdateCommitment>;
}
| ContextFreeAccountUpdateDescription<State, Event, Action>
) & {
accountId: AccountId;
verificationKeyHash?: Field;
callData?: Field;
};
class AccountUpdate<
State extends StateLayout = 'GenericState',
Event = Field[],
Action = Field[],
> extends ContextFreeAccountUpdate<State, Event, Action> {
accountId: AccountId;
verificationKeyHash: Field;
callData: Field;
// TODO: this probable shouldn't live here, but somewhere else
proof: 'NoProofRequired' | 'ProofPending' | Proof<undefined, AccountUpdateCommitment>;
// TODO: circuit friendly representation (we really don't want to toBoolean() in the constructor here...)
// proof: {pending: true, isRequired: Bool} | Proof<undefined, AccountUpdateCommitment>;
constructor(
State: StateDefinition<State>,
Event: DynamicProvable<Event>,
Action: DynamicProvable<Action>,
descr: AccountUpdateDescription<State, Event, Action>
) {
// TODO NOW: THIS PATTERN IS BROKEN (we are casting and update into a description, which only works because the missing fields are all optional)
const superInput = 'update' in descr ? descr.update : descr;
super(State, Event, Action, superInput);
if (this.authorizationKind.isProved.toBoolean()) {
this.proof = 'proof' in descr && descr.proof !== undefined ? descr.proof : 'ProofPending';
} else {
if ('proof' in descr && descr.proof !== undefined) {
throw new Error(
'proof was provided when constructing an AccountUpdate that does not require a proof'
);
}
this.proof = 'NoProofRequired';
}
this.accountId = descr.accountId;
this.verificationKeyHash =
descr.verificationKeyHash ?? new Field(mocks.dummyVerificationKeyHash);
this.callData = descr.callData ?? new Field(0);
}
get authorizationKindWithZkappContext(): AccountUpdateAuthorizationKindWithZkappContext {
return new AccountUpdateAuthorizationKindWithZkappContext(
this.authorizationKind,
this.verificationKeyHash
);
}
toInternalRepr(callDepth: number): Bindings.Layout.AccountUpdateBody {
return {
authorizationKind: this.authorizationKindWithZkappContext,
publicKey: this.accountId.publicKey,
tokenId: this.accountId.tokenId.value,
callData: this.callData,
callDepth: callDepth,
balanceChange: this.balanceChange,
incrementNonce: this.incrementNonce,
useFullCommitment: this.useFullCommitment,
implicitAccountCreationFee: this.implicitAccountCreationFee,
mayUseToken: this.mayUseToken,
events: this.pushEvents.toInternalRepr(),
actions: this.pushActions.toInternalRepr(),
preconditions: this.preconditions.toInternalRepr(),
update: {
appState: StateUpdates.toFieldUpdates(this.State, this.stateUpdates).map((update) =>
update.toOption()
),
delegate: this.delegateUpdate.toOption(),
verificationKey: Option.map(this.verificationKeyUpdate.toOption(), (data) =>
data instanceof VerificationKey ? new VerificationKey(data) : data
),
permissions: this.permissionsUpdate.toOption(),
zkappUri: this.zkappUriUpdate.toOption(),
tokenSymbol: this.tokenSymbolUpdate.toOption(),
timing: this.timingUpdate.toOption(),
votingFor: this.votingForUpdate.toOption(),
},
};
}
toInput(): HashInput {
return Bindings.Layout.AccountUpdateBody.toInput(this.toInternalRepr(0));
}
commit(networkId: NetworkId): AccountUpdateCommitment {
const commitment = hashWithPrefix(zkAppBodyPrefix(networkId), packToFields(this.toInput()));
return new AccountUpdateCommitment(commitment);
}
toGeneric(): AccountUpdate {
return new AccountUpdate('GenericState', GenericData, GenericData, {
update: super.toGeneric(),
proof: this.proof instanceof Proof ? this.proof : undefined,
accountId: this.accountId,
verificationKeyHash: this.verificationKeyHash,
callData: this.callData,
});
}
static fromGeneric<State extends StateLayout, Event, Action>(
x: AccountUpdate,
State: StateDefinition<State>,
Event: DynamicProvable<Event>,
Action: DynamicProvable<Action>
): AccountUpdate<State, Event, Action> {
return new AccountUpdate(State, Event, Action, {
update: ContextFreeAccountUpdate.fromGeneric(x, State, Event, Action),
proof: x.proof instanceof Proof ? x.proof : undefined,
accountId: x.accountId,
verificationKeyHash: x.verificationKeyHash,
callData: x.callData,
});
}
async authorize(
authEnv: AccountUpdateAuthorizationEnvironment
): Promise<Authorized<State, Event, Action>> {
let proof = null;
let signature = null;
switch (this.proof) {
case 'NoProofRequired':
if (this.authorizationKind.isProved.toBoolean()) {
throw new Error(
`account update proof was marked as not required, but authorization kind was ${this.authorizationKind.identifier()}`
);
} else {
break;
}
case 'ProofPending':
if (this.authorizationKind.isProved.toBoolean()) {
throw new Error(
`account update proof is still pending; a proof must be generated and assigned to an account update before calling authorize`
);
} else {
console.warn(
`account update is marked to required a proof, but the authorization kind is ${this.authorizationKind.identifier()} (and the proof is still pending)`
);
break;
}
default:
if (this.authorizationKind.isProved.toBoolean()) {
proof = Pickles.proofToBase64Transaction(this.proof.proof);
} else {
console.warn(
`account update has a proof, but no proof is required by authorization kind ${this.authorizationKind.identifier()}, so it will not be included`
);
}
}
if (this.authorizationKind.isSigned.toBoolean()) {
let txnCommitment;
if (this.useFullCommitment.toBoolean()) {
if (authEnv.fullTransactionCommitment === undefined) {
throw new Error(
'unable to authorize account update: useFullCommitment is true, but not full transaction commitment was provided in authorization environment'
);
}
txnCommitment = authEnv.fullTransactionCommitment;
} else {
txnCommitment = authEnv.accountUpdateForestCommitment;
}
const privateKey = await authEnv.getPrivateKey(this.accountId.publicKey);
const sig = signFieldElement(txnCommitment, privateKey.toBigInt(), authEnv.networkId);
signature = Signature.toBase58(sig);
}
return new Authorized({ proof, signature }, this);
}
static create(x: AccountUpdateDescription<'GenericState', Field[], Field[]>): AccountUpdate {
return new AccountUpdate('GenericState', GenericData, GenericData, x);
}
static fromInternalRepr(x: Bindings.Layout.AccountUpdateBody): AccountUpdate {
return new AccountUpdate('GenericState', GenericData, GenericData, {
accountId: new AccountId(x.publicKey, new TokenId(x.tokenId)),
verificationKeyHash: x.authorizationKind.verificationKeyHash,
authorizationKind: new AccountUpdateAuthorizationKind(x.authorizationKind),
callData: x.callData,
balanceChange: Int64.create(x.balanceChange.magnitude, x.balanceChange.sgn),
incrementNonce: x.incrementNonce,
useFullCommitment: x.useFullCommitment,
implicitAccountCreationFee: x.implicitAccountCreationFee,
mayUseToken: x.mayUseToken,
pushEvents: CommittedList.from(GenericData, EventsHashConfig(GenericData), x.events),
pushActions: CommittedList.from(GenericData, ActionsHashConfig(GenericData), x.actions),
preconditions: Preconditions.fromInternalRepr(x.preconditions),
setState: new GenericStateUpdates(x.update.appState.map(Update.fromOption)),
setDelegate: Update.fromOption(x.update.delegate),
setVerificationKey: Update.fromOption(x.update.verificationKey),
setPermissions: Update.fromOption(
Option.map(x.update.permissions, Permissions.fromInternalRepr)
),
setZkappUri: Update.fromOption(Option.map(x.update.zkappUri, (uri) => new ZkappUri(uri))),
setTokenSymbol: Update.fromOption(
Option.map(x.update.tokenSymbol, (symbol) => new TokenSymbol(symbol))
),
setTiming: Update.fromOption(
Option.map(x.update.timing, (timing) => new AccountTiming(timing))
),
setVotingFor: Update.fromOption(x.update.votingFor),
});
}
static sizeInFields(): number {
return Bindings.Layout.AccountUpdateBody.sizeInFields();
}
static toFields(x: AccountUpdate): Field[] {
return Bindings.Layout.AccountUpdateBody.toFields(x.toInternalRepr(0));
}
static toAuxiliary(x?: AccountUpdate, callDepth?: number): any[] {
return Bindings.Layout.AccountUpdateBody.toAuxiliary(x?.toInternalRepr(callDepth ?? 0));
}
static fromFields(fields: Field[], aux: any[]): AccountUpdate {
return AccountUpdate.fromInternalRepr(
Bindings.Layout.AccountUpdateBody.fromFields(fields, aux)
);
}
static toValue(x: AccountUpdate): AccountUpdate {
return x;
}
static fromValue(x: AccountUpdate): AccountUpdate {
return x;
}
static check(_x: AccountUpdate): void {
// TODO
}
static empty(): AccountUpdate {
return new AccountUpdate('GenericState', GenericData, GenericData, {
accountId: AccountId.empty(),
callData: Field.empty(),
update: ContextFreeAccountUpdate.empty(),
});
}
}
// TODO NOW: un-namespace this
/*
type ContextFreeDescription<
State extends StateLayout = 'GenericState',
Event = Field[],
Action = Field[]
> = ContextFreeAccountUpdateDescription<State, Event, Action>;
type ContextFree<
State extends StateLayout = 'GenericState',
Event = Field[],
Action = Field[]
> = ContextFreeAccountUpdate<State, Event, Action>;
const ContextFree = ContextFreeAccountUpdate;
*/
// TODO: can we enforce that Authorized account updates are immutable?
class Authorized<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]> {
authorization: AccountUpdateAuthorization;
private update: AccountUpdate<State, Event, Action>;
constructor(
authorization: AccountUpdateAuthorization,
update: AccountUpdate<State, Event, Action>
) {
this.authorization = authorization;
this.update = update;
}
toAccountUpdate(): AccountUpdate<State, Event, Action> {
return this.update;
}
get State(): StateDefinition<State> {
return this.update.State;
}
get authorizationKind(): AccountUpdateAuthorizationKind {
return this.update.authorizationKind;
}
get accountId(): AccountId {
return this.update.accountId;
}
get verificationKeyHash(): Field {
return this.update.verificationKeyHash;
}
get callData(): Field {
return this.update.callData;
}
get preconditions(): Preconditions<State> {
return this.update.preconditions;
}
get balanceChange(): Int64 {
return this.update.balanceChange;
}
get incrementNonce(): Bool {
return this.update.incrementNonce;
}
get useFullCommitment(): Bool {
return this.update.useFullCommitment;
}
get implicitAccountCreationFee(): Bool {
return this.update.implicitAccountCreationFee;
}
get mayUseToken(): MayUseToken {
return this.update.mayUseToken;
}
get pushEvents(): CommittedList<Event> {
return this.update.pushEvents;
}
get pushActions(): CommittedList<Action> {
return this.update.pushActions;
}
get stateUpdates(): StateUpdates<State> {
return this.update.stateUpdates;
}
get permissionsUpdate(): Update<Permissions> {
return this.update.permissionsUpdate;
}
get delegateUpdate(): Update<PublicKey> {
return this.update.delegateUpdate;
}
get verificationKeyUpdate(): Update<VerificationKey> {
return this.update.verificationKeyUpdate;
}
get zkappUriUpdate(): Update<ZkappUri> {
return this.update.zkappUriUpdate;
}
get tokenSymbolUpdate(): Update<TokenSymbol> {
return this.update.tokenSymbolUpdate;
}
get timingUpdate(): Update<AccountTiming> {
return this.update.timingUpdate;
}
get votingForUpdate(): Update<Field> {
return this.update.votingForUpdate;
}
get authorizationKindWithZkappContext(): AccountUpdateAuthorizationKindWithZkappContext {
return this.update.authorizationKindWithZkappContext;
}
hash(netId: NetworkId): Field {
let input = Bindings.Layout.ZkappAccountUpdate.toInput(this.toInternalRepr(0));
return hashWithPrefix(zkAppBodyPrefix(netId), packToFields(input));
}
toInternalRepr(callDepth: number): Bindings.Layout.ZkappAccountUpdate {
return {
authorization: {
proof: this.authorization.proof === null ? undefined : this.authorization.proof,
signature: this.authorization.signature === null ? undefined : this.authorization.signature,
},
body: this.update.toInternalRepr(callDepth),
};
}
static fromInternalRepr(x: Bindings.Layout.ZkappAccountUpdate): Authorized {
return new Authorized(
{
// when the internal representation is returned from the previous version when casting from fields,
// (if there is no proof or authorization, values are set to false rather than to undefined)
proof: (x.authorization.proof as any) !== false ? (x.authorization.proof ?? null) : null,
signature:
(x.authorization.proof as any) !== false ? (x.authorization.signature ?? null) : null,
},
AccountUpdate.fromInternalRepr(x.body)
);
}
toJSON(callDepth: number): any {
return Authorized.toJSON(this, callDepth);
}
toInput(): HashInput {
return Authorized.toInput(this);
}
toFields(): Field[] {
return Authorized.toFields(this);
}
static empty(): Authorized {
return new Authorized({ proof: null, signature: null }, AccountUpdate.empty());
}
static sizeInFields(): number {
return Bindings.Layout.ZkappAccountUpdate.sizeInFields();
}
static toJSON<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>(
x: Authorized<State, Event, Action>,
callDepth: number
): any {
return Bindings.Layout.ZkappAccountUpdate.toJSON(x.toInternalRepr(callDepth));
}
static toInput<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>(
x: Authorized<State, Event, Action>
): HashInput {
return Bindings.Layout.ZkappAccountUpdate.toInput(x.toInternalRepr(0));
}
static toFields<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>(
x: Authorized<State, Event, Action>
): Field[] {
return Bindings.Layout.ZkappAccountUpdate.toFields(x.toInternalRepr(0));
}
static toAuxiliary<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>(
x?: Authorized<State, Event, Action>,
callDepth?: number
): any[] {
return Bindings.Layout.ZkappAccountUpdate.toAuxiliary(x?.toInternalRepr(callDepth ?? 0));
}
static fromFields(fields: Field[], aux: any[]): Authorized {
return Authorized.fromInternalRepr(Bindings.Layout.ZkappAccountUpdate.fromFields(fields, aux));
}
static toValue<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>(
x: Authorized<State, Event, Action>
): Authorized<State, Event, Action> {
return x;
}
static fromValue<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>(
x: Authorized<State, Event, Action>
): Authorized<State, Event, Action> {
return x;
}
static check<State extends StateLayout = 'GenericState', Event = Field[], Action = Field[]>(
_x: Authorized<State, Event, Action>
) {
throw new Error('TODO');
}
}