o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
690 lines (630 loc) • 25.1 kB
text/typescript
import { Bool, Field } from '../../provable/wrapped.js';
import { circuitValueEquals, cloneCircuitValue } from '../../provable/types/struct.js';
import { Provable } from '../../provable/provable.js';
import { activeInstance as Mina } from './mina-instance.js';
import type { AccountUpdate } from './account-update.js';
import { Int64, UInt32, UInt64 } from '../../provable/int.js';
import { Layout } from '../../../bindings/mina-transaction/gen/v1/transaction.js';
import { jsLayout } from '../../../bindings/mina-transaction/gen/v1/js-layout.js';
import { emptyReceiptChainHash, TokenSymbol } from '../../provable/crypto/poseidon.js';
import { PublicKey } from '../../provable/crypto/signature.js';
import {
ActionState,
Actions,
ZkappUri,
} from '../../../bindings/mina-transaction/v1/transaction-leaves.js';
import type { Types } from '../../../bindings/mina-transaction/v1/types.js';
import type { Permissions } from './account-update.js';
import { ZkappStateLength } from './mina-instance.js';
import { assertInternal } from '../../util/errors.js';
export {
preconditions,
Account,
Network,
CurrentSlot,
assertPreconditionInvariants,
cleanPreconditionsCache,
ensureConsistentPrecondition,
AccountValue,
NetworkValue,
getAccountPreconditions,
Preconditions,
OrIgnore,
ClosedInterval,
};
type AccountUpdateBody = Types.AccountUpdate['body'];
/**
* Preconditions for the network and accounts
*/
type Preconditions = AccountUpdateBody['preconditions'];
/**
* Either check a value or ignore it.
*
* Used within [[ AccountPredicate ]]s and [[ ProtocolStatePredicate ]]s.
*/
type OrIgnore<T> = { isSome: Bool; value: T };
/**
* An interval representing all the values between `lower` and `upper` inclusive
* of both the `lower` and `upper` values.
*
* @typeParam A something with an ordering where one can quantify a lower and
* upper bound.
*/
type ClosedInterval<T> = { lower: T; upper: T };
type NetworkPrecondition = Preconditions['network'];
let NetworkPrecondition = {
ignoreAll(): NetworkPrecondition {
let stakingEpochData = {
ledger: { hash: ignore(Field(0)), totalCurrency: ignore(uint64()) },
seed: ignore(Field(0)),
startCheckpoint: ignore(Field(0)),
lockCheckpoint: ignore(Field(0)),
epochLength: ignore(uint32()),
};
let nextEpochData = cloneCircuitValue(stakingEpochData);
return {
snarkedLedgerHash: ignore(Field(0)),
blockchainLength: ignore(uint32()),
minWindowDensity: ignore(uint32()),
totalCurrency: ignore(uint64()),
globalSlotSinceGenesis: ignore(uint32()),
stakingEpochData,
nextEpochData,
};
},
};
/**
* Ignores a `dummy`
*
* @param dummy The value to ignore
* @returns Always an ignored value regardless of the input.
*/
function ignore<T>(dummy: T): OrIgnore<T> {
return { isSome: Bool(false), value: dummy };
}
/**
* Ranges between all uint32 values
*/
const uint32 = () => ({ lower: UInt32.from(0), upper: UInt32.MAXINT() });
/**
* Ranges between all uint64 values
*/
const uint64 = () => ({ lower: UInt64.from(0), upper: UInt64.MAXINT() });
type AccountPrecondition = Preconditions['account'];
const AccountPrecondition = {
ignoreAll(): AccountPrecondition {
let appState: Array<OrIgnore<Field>> = [];
for (let i = 0; i < ZkappStateLength; ++i) {
appState.push(ignore(Field(0)));
}
return {
balance: ignore(uint64()),
nonce: ignore(uint32()),
receiptChainHash: ignore(Field(0)),
delegate: ignore(PublicKey.empty()),
state: appState,
actionState: ignore(Actions.emptyActionState()),
provedState: ignore(Bool(false)),
isNew: ignore(Bool(false)),
};
},
};
type GlobalSlotPrecondition = Preconditions['validWhile'];
const GlobalSlotPrecondition = {
ignoreAll(): GlobalSlotPrecondition {
return ignore(uint32());
},
};
const Preconditions = {
ignoreAll(): Preconditions {
return {
account: AccountPrecondition.ignoreAll(),
network: NetworkPrecondition.ignoreAll(),
validWhile: GlobalSlotPrecondition.ignoreAll(),
};
},
};
function preconditions(accountUpdate: AccountUpdate, isSelf: boolean) {
initializePreconditions(accountUpdate, isSelf);
return {
account: Account(accountUpdate),
network: Network(accountUpdate),
currentSlot: CurrentSlot(accountUpdate),
};
}
// note: please keep the two precondition implementations separate
// so we can add customized fields easily
function Network(accountUpdate: AccountUpdate): Network {
let layout = jsLayout.AccountUpdate.entries.body.entries.preconditions.entries.network;
let context = getPreconditionContextExn(accountUpdate);
let network: RawNetwork = preconditionClass(layout as Layout, 'network', accountUpdate, context);
let timestamp = {
get() {
let slot = network.globalSlotSinceGenesis.get();
return globalSlotToTimestamp(slot);
},
getAndRequireEquals() {
let slot = network.globalSlotSinceGenesis.getAndRequireEquals();
return globalSlotToTimestamp(slot);
},
requireEquals(value: UInt64) {
let { genesisTimestamp, slotTime } = Mina.getNetworkConstants();
let slot = timestampToGlobalSlot(
value,
`Timestamp precondition unsatisfied: the timestamp can only equal numbers of the form ${genesisTimestamp} + k*${slotTime},\n` +
`i.e., the genesis timestamp plus an integer number of slots.`
);
return network.globalSlotSinceGenesis.requireEquals(slot);
},
requireEqualsIf(condition: Bool, value: UInt64) {
let { genesisTimestamp, slotTime } = Mina.getNetworkConstants();
let slot = timestampToGlobalSlot(
value,
`Timestamp precondition unsatisfied: the timestamp can only equal numbers of the form ${genesisTimestamp} + k*${slotTime},\n` +
`i.e., the genesis timestamp plus an integer number of slots.`
);
return network.globalSlotSinceGenesis.requireEqualsIf(condition, slot);
},
requireBetween(lower: UInt64, upper: UInt64) {
let [slotLower, slotUpper] = timestampToGlobalSlotRange(lower, upper);
return network.globalSlotSinceGenesis.requireBetween(slotLower, slotUpper);
},
requireNothing() {
return network.globalSlotSinceGenesis.requireNothing();
},
};
return { ...network, timestamp };
}
function Account(accountUpdate: AccountUpdate): Account {
let layout = jsLayout.AccountUpdate.entries.body.entries.preconditions.entries.account;
let context = getPreconditionContextExn(accountUpdate);
let identity = (x: any) => x;
let update: Update = {
delegate: {
...preconditionSubclass(accountUpdate, 'account.delegate', PublicKey, context),
...updateSubclass(accountUpdate, 'delegate', identity),
},
verificationKey: updateSubclass(accountUpdate, 'verificationKey', identity),
permissions: updateSubclass(accountUpdate, 'permissions', identity),
zkappUri: updateSubclass(accountUpdate, 'zkappUri', ZkappUri.fromJSON),
tokenSymbol: updateSubclass(accountUpdate, 'tokenSymbol', TokenSymbol.from),
timing: updateSubclass(accountUpdate, 'timing', identity),
votingFor: updateSubclass(accountUpdate, 'votingFor', identity),
};
return {
...preconditionClass(layout as Layout, 'account', accountUpdate, context),
...update,
};
}
function updateSubclass<K extends keyof Update>(
accountUpdate: AccountUpdate,
key: K,
transform: (value: UpdateValue[K]) => UpdateValueOriginal[K]
) {
return {
set(value: UpdateValue[K]) {
accountUpdate.body.update[key].isSome = Bool(true);
accountUpdate.body.update[key].value = transform(value);
},
};
}
function CurrentSlot(accountUpdate: AccountUpdate): CurrentSlot {
let context = getPreconditionContextExn(accountUpdate);
return {
requireBetween(lower: UInt32, upper: UInt32) {
context.constrained.add('validWhile');
let property: RangeCondition<UInt32> = accountUpdate.body.preconditions.validWhile;
ensureConsistentPrecondition(property, Bool(true), { lower, upper }, 'validWhile');
property.isSome = Bool(true);
property.value.lower = lower;
property.value.upper = upper;
},
};
}
let unimplementedPreconditions: LongKey[] = [
// unimplemented because its not checked in the protocol
'network.stakingEpochData.seed',
'network.nextEpochData.seed',
];
let baseMap = { UInt64, UInt32, Field, Bool, PublicKey, ActionState };
function getProvableType(layout: { type: string; checkedTypeName?: string }) {
let typeName = layout.checkedTypeName ?? layout.type;
let type = baseMap[typeName as keyof typeof baseMap];
assertInternal(type !== undefined, `Unknown precondition base type ${typeName}`);
return type;
}
function preconditionClass(
layout: Layout,
baseKey: any,
accountUpdate: AccountUpdate,
context: PreconditionContext
): any {
if (layout.type === 'option') {
// range condition
if (layout.optionType === 'closedInterval') {
let baseType = getProvableType(layout.inner.entries.lower);
return preconditionSubClassWithRange(accountUpdate, baseKey, baseType, context);
}
// value condition
else if (layout.optionType === 'flaggedOption') {
let baseType = getProvableType(layout.inner);
return preconditionSubclass(accountUpdate, baseKey, baseType, context);
}
} else if (layout.type === 'array') {
return {}; // not applicable yet, TODO if we implement state
} else if (layout.type === 'object') {
// for each field, create a recursive object
return Object.fromEntries(
layout.keys.map((key) => {
let value = layout.entries[key];
return [key, preconditionClass(value, `${baseKey}.${key}`, accountUpdate, context)];
})
);
} else throw Error('bug');
}
function preconditionSubClassWithRange<K extends LongKey, U extends FlatPreconditionValue[K]>(
accountUpdate: AccountUpdate,
longKey: K,
fieldType: Provable<U>,
context: PreconditionContext
) {
return {
...preconditionSubclass(accountUpdate, longKey, fieldType as any, context),
requireBetween(lower: any, upper: any) {
context.constrained.add(longKey);
let property: RangeCondition<any> = getPath(accountUpdate.body.preconditions, longKey);
let newValue = { lower, upper };
ensureConsistentPrecondition(property, Bool(true), newValue, longKey);
property.isSome = Bool(true);
property.value = newValue;
},
};
}
function defaultLower(fieldType: any) {
assertInternal(fieldType === UInt32 || fieldType === UInt64);
return (fieldType as typeof UInt32 | typeof UInt64).zero;
}
function defaultUpper(fieldType: any) {
assertInternal(fieldType === UInt32 || fieldType === UInt64);
return (fieldType as typeof UInt32 | typeof UInt64).MAXINT();
}
function preconditionSubclass<K extends LongKey, U extends FlatPreconditionValue[K]>(
accountUpdate: AccountUpdate,
longKey: K,
fieldType: Provable<U> & { empty(): U },
context: PreconditionContext
) {
if (fieldType === undefined) {
throw Error(`this.${longKey}: fieldType undefined`);
}
let obj = {
get() {
if (unimplementedPreconditions.includes(longKey)) {
let self = context.isSelf ? 'this' : 'accountUpdate';
throw Error(`${self}.${longKey}.get() is not implemented yet.`);
}
let { read, vars } = context;
read.add(longKey);
return (vars[longKey] ??= getVariable(accountUpdate, longKey, fieldType)) as U;
},
getAndRequireEquals() {
let value = obj.get();
obj.requireEquals(value);
return value;
},
requireEquals(value: U) {
context.constrained.add(longKey);
let property = getPath(accountUpdate.body.preconditions, longKey) as AnyCondition<U>;
if ('isSome' in property) {
let isInterval = 'lower' in property.value && 'upper' in property.value;
let newValue = isInterval ? { lower: value, upper: value } : value;
ensureConsistentPrecondition(property, Bool(true), newValue, longKey);
property.isSome = Bool(true);
property.value = newValue;
} else {
setPath(accountUpdate.body.preconditions, longKey, value);
}
},
requireEqualsIf(condition: Bool, value: U) {
context.constrained.add(longKey);
let property = getPath(accountUpdate.body.preconditions, longKey) as AnyCondition<U>;
assertInternal('isSome' in property);
if ('lower' in property.value && 'upper' in property.value) {
let lower = Provable.if(condition, fieldType, value, defaultLower(fieldType) as U);
let upper = Provable.if(condition, fieldType, value, defaultUpper(fieldType) as U);
ensureConsistentPrecondition(property, condition, { lower, upper }, longKey);
property.isSome = condition;
property.value.lower = lower;
property.value.upper = upper;
} else {
let newValue = Provable.if(condition, fieldType, value, fieldType.empty());
ensureConsistentPrecondition(property, condition, newValue, longKey);
property.isSome = condition;
property.value = newValue;
}
},
requireNothing() {
let property = getPath(accountUpdate.body.preconditions, longKey) as AnyCondition<U>;
if ('isSome' in property) {
property.isSome = Bool(false);
if ('lower' in property.value && 'upper' in property.value) {
property.value.lower = defaultLower(fieldType) as U;
property.value.upper = defaultUpper(fieldType) as U;
} else {
property.value = fieldType.empty();
}
}
context.constrained.add(longKey);
},
};
return obj;
}
function getVariable<K extends LongKey, U extends FlatPreconditionValue[K]>(
accountUpdate: AccountUpdate,
longKey: K,
fieldType: Provable<U>
): U {
return Provable.witness(fieldType, () => {
let [accountOrNetwork, ...rest] = longKey.split('.');
let key = rest.join('.');
let value: U;
if (accountOrNetwork === 'account') {
let account = getAccountPreconditions(accountUpdate.body);
value = account[key as keyof AccountValue] as U;
} else if (accountOrNetwork === 'network') {
let networkState = Mina.getNetworkState();
value = getPath(networkState, key);
} else if (accountOrNetwork === 'validWhile') {
let networkState = Mina.getNetworkState();
value = networkState.globalSlotSinceGenesis as U;
} else {
throw Error('impossible');
}
return value;
});
}
function globalSlotToTimestamp(slot: UInt32) {
let { genesisTimestamp, slotTime } = Mina.getNetworkConstants();
return UInt64.from(slot).mul(slotTime).add(genesisTimestamp);
}
function timestampToGlobalSlot(timestamp: UInt64, message: string) {
let { genesisTimestamp, slotTime } = Mina.getNetworkConstants();
let { quotient: slot, rest } = timestamp.sub(genesisTimestamp).divMod(slotTime);
rest.value.assertEquals(Field(0), message);
return slot.toUInt32();
}
function timestampToGlobalSlotRange(
tsLower: UInt64,
tsUpper: UInt64
): [lower: UInt32, upper: UInt32] {
// we need `slotLower <= current slot <= slotUpper` to imply `tsLower <= current timestamp <= tsUpper`
// so we have to make the range smaller -- round up `tsLower` and round down `tsUpper`
// also, we should clamp to the UInt32 max range [0, 2**32-1]
let { genesisTimestamp, slotTime } = Mina.getNetworkConstants();
let tsLowerInt = Int64.from(tsLower).sub(genesisTimestamp).add(slotTime).sub(1);
let lowerCapped = Provable.if<UInt64>(
tsLowerInt.isPositive(),
UInt64,
tsLowerInt.magnitude,
UInt64.from(0)
);
let slotLower = lowerCapped.div(slotTime).toUInt32Clamped();
// unsafe `sub` means the error in case tsUpper underflows slot 0 is ugly, but should not be relevant in practice
let slotUpper = tsUpper.sub(genesisTimestamp).div(slotTime).toUInt32Clamped();
return [slotLower, slotUpper];
}
function getAccountPreconditions(body: { publicKey: PublicKey; tokenId?: Field }): AccountValue {
let { publicKey, tokenId } = body;
let hasAccount = Mina.hasAccount(publicKey, tokenId);
if (!hasAccount) {
return {
balance: UInt64.zero,
nonce: UInt32.zero,
receiptChainHash: emptyReceiptChainHash(),
actionState: Actions.emptyActionState(),
delegate: publicKey,
provedState: Bool(false),
isNew: Bool(true),
};
}
let account = Mina.getAccount(publicKey, tokenId);
return {
balance: account.balance,
nonce: account.nonce,
receiptChainHash: account.receiptChainHash,
actionState: account.zkapp?.actionState?.[0] ?? Actions.emptyActionState(),
delegate: account.delegate ?? account.publicKey,
provedState: account.zkapp?.provedState ?? Bool(false),
isNew: Bool(false),
};
}
// per account update context for checking invariants on precondition construction
type PreconditionContext = {
isSelf: boolean;
vars: Partial<FlatPreconditionValue>;
read: Set<LongKey>;
constrained: Set<LongKey>;
};
function initializePreconditions(accountUpdate: AccountUpdate, isSelf: boolean) {
preconditionContexts.set(accountUpdate, {
read: new Set(),
constrained: new Set(),
vars: {},
isSelf,
});
}
function cleanPreconditionsCache(accountUpdate: AccountUpdate) {
let context = preconditionContexts.get(accountUpdate);
if (context !== undefined) context.vars = {};
}
function assertPreconditionInvariants(accountUpdate: AccountUpdate) {
let context = getPreconditionContextExn(accountUpdate);
let self = context.isSelf ? 'this' : 'accountUpdate';
let dummyPreconditions = Preconditions.ignoreAll();
for (let preconditionPath of context.read) {
// check if every precondition that was read was also constrained
if (context.constrained.has(preconditionPath)) continue;
// check if the precondition was modified manually, which is also a valid way of avoiding an error
let precondition = getPath(accountUpdate.body.preconditions, preconditionPath);
let dummy = getPath(dummyPreconditions, preconditionPath);
if (!circuitValueEquals(precondition, dummy)) continue;
// we accessed a precondition field but not constrained it explicitly - throw an error
let hasRequireBetween = isRangeCondition(precondition);
let shortPath = preconditionPath.split('.').pop();
let errorMessage = `You used \`${self}.${preconditionPath}.get()\` without adding a precondition that links it to the actual ${shortPath}.
Consider adding this line to your code:
${self}.${preconditionPath}.requireEquals(${self}.${preconditionPath}.get());${
hasRequireBetween
? `
You can also add more flexible preconditions with \`${self}.${preconditionPath}.requireBetween(...)\`.`
: ''
}`;
throw Error(errorMessage);
}
}
function getPreconditionContextExn(accountUpdate: AccountUpdate) {
let c = preconditionContexts.get(accountUpdate);
if (c === undefined) throw Error('bug: precondition context not found');
return c;
}
/**
* Asserts that a precondition is not already set or that it matches the new values.
*
* This function checks if a precondition is already set for a given property and compares it
* with new values. If the precondition is not set, it allows the new values. If it's already set,
* it ensures consistency with the existing precondition.
*
* @param property - The property object containing the precondition information.
* @param newIsSome - A boolean or CircuitValue indicating whether the new precondition should exist.
* @param value - The new value for the precondition. Can be a simple value or an object with 'lower' and 'upper' properties for range preconditions.
* @param name - The name of the precondition for error messages.
*
* @throws {Error} Throws an error with a detailed message if attempting to set an inconsistent precondition.
* @todo It would be nice to have the input parameter types more specific, but it's hard to do with the current implementation.
*/
function ensureConsistentPrecondition(property: any, newIsSome: any, value: any, name: any) {
if (!property.isSome.isConstant() || property.isSome.toBoolean()) {
let errorMessage = `
Precondition Error: Precondition Error: Attempting to set a precondition that is already set for '${name}'.
'${name}' represents the field or value you're trying to set a precondition for.
Preconditions must be set only once to avoid overwriting previous assertions.
For example, do not use 'requireBetween()' or 'requireEquals()' multiple times on the same field.
Recommendation:
Ensure that preconditions for '${name}' are set in a single place and are not overwritten. If you need to update a precondition,
consider refactoring your code to consolidate all assertions for '${name}' before setting the precondition.
Example of Correct Usage:
// Incorrect Usage:
timestamp.requireBetween(newUInt32(0n), newUInt32(2n));
timestamp.requireBetween(newUInt32(1n), newUInt32(3n));
// Correct Usage:
timestamp.requireBetween(new UInt32(1n), new UInt32(2n));
`;
property.isSome.assertEquals(newIsSome, errorMessage);
if ('lower' in property.value && 'upper' in property.value) {
property.value.lower.assertEquals(value.lower, errorMessage);
property.value.upper.assertEquals(value.lower, errorMessage);
} else {
property.value.assertEquals(value, errorMessage);
}
}
}
const preconditionContexts = new WeakMap<AccountUpdate, PreconditionContext>();
// exported types
type NetworkValue = PreconditionBaseTypes<NetworkPrecondition>;
type RawNetwork = PreconditionClassType<NetworkPrecondition>;
type Network = RawNetwork & {
timestamp: PreconditionSubclassRangeType<UInt64>;
};
// TODO: should we add account.state?
// then can just use circuitArray(Field, 8) as the type
type AccountPreconditionNoState = Omit<Preconditions['account'], 'state'>;
type AccountValue = PreconditionBaseTypes<AccountPreconditionNoState>;
type Account = PreconditionClassType<AccountPreconditionNoState> & Update;
type CurrentSlotPrecondition = Preconditions['validWhile'];
type CurrentSlot = {
requireBetween(lower: UInt32, upper: UInt32): void;
};
type PreconditionBaseTypes<T> = {
[K in keyof T]: T[K] extends RangeCondition<infer U>
? U
: T[K] extends FlaggedOptionCondition<infer U>
? U
: T[K] extends Field
? Field
: PreconditionBaseTypes<T[K]>;
};
type PreconditionSubclassType<U> = {
get(): U;
getAndRequireEquals(): U;
requireEquals(value: U): void;
requireEqualsIf(condition: Bool, value: U): void;
requireNothing(): void;
};
type PreconditionSubclassRangeType<U> = PreconditionSubclassType<U> & {
requireBetween(lower: U, upper: U): void;
};
type PreconditionClassType<T> = {
[K in keyof T]: T[K] extends RangeCondition<infer U>
? PreconditionSubclassRangeType<U>
: T[K] extends FlaggedOptionCondition<infer U>
? PreconditionSubclassType<U>
: T[K] extends Field
? PreconditionSubclassType<Field>
: PreconditionClassType<T[K]>;
};
// update
type Update_ = Omit<AccountUpdate['body']['update'], 'appState'>;
type Update = {
[K in keyof Update_]: { set(value: UpdateValue[K]): void };
};
type UpdateValueOriginal = {
[K in keyof Update_]: Update_[K]['value'];
};
type UpdateValue = {
[K in keyof Update_]: K extends 'zkappUri' | 'tokenSymbol'
? string
: K extends 'permissions'
? Permissions
: Update_[K]['value'];
};
// TS magic for computing flattened precondition types
type JoinEntries<K, P> = K extends string
? P extends [string, unknown, unknown]
? [`${K}${P[0] extends '' ? '' : '.'}${P[0]}`, P[1], P[2]]
: never
: never;
type PreconditionFlatEntry<T> = T extends RangeCondition<infer V>
? ['', T, V]
: T extends FlaggedOptionCondition<infer U>
? ['', T, U]
: { [K in keyof T]: JoinEntries<K, PreconditionFlatEntry<T[K]>> }[keyof T];
type FlatPreconditionValue = {
[S in PreconditionFlatEntry<NetworkPrecondition> as `network.${S[0]}`]: S[2];
} & {
[S in PreconditionFlatEntry<AccountPreconditionNoState> as `account.${S[0]}`]: S[2];
} & { validWhile: PreconditionFlatEntry<CurrentSlotPrecondition>[2] };
type LongKey = keyof FlatPreconditionValue;
// types for the two kinds of conditions
type RangeCondition<T> = { isSome: Bool; value: { lower: T; upper: T } };
type FlaggedOptionCondition<T> = { isSome: Bool; value: T };
type AnyCondition<T> = RangeCondition<T> | FlaggedOptionCondition<T>;
function isRangeCondition<T extends object>(
condition: AnyCondition<T>
): condition is RangeCondition<T> {
return 'isSome' in condition && 'lower' in condition.value;
}
// helper. getPath({a: {b: 'x'}}, 'a.b') === 'x'
// TODO: would be awesome to type this
function getPath(obj: any, path: string) {
let pathArray = path.split('.').reverse();
while (pathArray.length > 0) {
let key = pathArray.pop();
obj = obj[key as any];
}
return obj;
}
function setPath(obj: any, path: string, value: any) {
let pathArray = path.split('.');
let key = pathArray.pop()!;
getPath(obj, pathArray.join('.'))[key] = value;
}