o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
321 lines • 13.7 kB
JavaScript
import { AccountUpdate, TokenId } from './account-update.js';
import * as Mina from './mina.js';
import { fetchAccount, networkConfig } from './fetch.js';
import { Provable } from '../../provable/provable.js';
import { Field } from '../../provable/wrapped.js';
import { ProvableType, } from '../../provable/types/provable-intf.js';
import { ensureConsistentPrecondition } from './precondition.js';
import { Bool } from '../../provable/wrapped.js';
// external API
export { State, state, declareState };
// internal API
export { assertStatePrecondition, cleanStatePrecondition, getLayout };
function State(defaultValue) {
return createState(defaultValue);
}
/**
* A decorator to use within a zkapp to indicate what will be stored on-chain.
* For example, if you want to store a field element `some_state` in a zkapp,
* you can use the following in the declaration of your zkapp:
*
* ```
* @state(Field) some_state = State<Field>();
* ```
*
*/
function state(type) {
let stateType = ProvableType.get(type);
return function (target, key, _descriptor) {
const ZkappClass = target.constructor;
if (reservedPropNames.has(key)) {
throw Error(`Property name ${key} is reserved.`);
}
let sc = smartContracts.get(ZkappClass);
if (sc === undefined) {
sc = { states: [], layout: undefined };
smartContracts.set(ZkappClass, sc);
}
sc.states.push([key, stateType]);
Object.defineProperty(target, key, {
get() {
return this._?.[key];
},
set(v) {
if (v._contract !== undefined)
throw Error('A State should only be assigned once to a SmartContract');
if (this._?.[key])
throw Error('A @state should only be assigned once');
v._contract = {
key,
stateType: stateType,
instance: this,
class: ZkappClass,
wasConstrained: false,
wasRead: false,
cachedVariable: undefined,
};
(this._ ??= {})[key] = v;
},
});
};
}
/**
* `declareState` can be used in place of the `@state` decorator to declare on-chain state on a SmartContract.
* It should be placed _after_ the class declaration.
* Here is an example of declaring a state property `x` of type `Field`.
* ```ts
* class MyContract extends SmartContract {
* x = State<Field>();
* // ...
* }
* declareState(MyContract, { x: Field });
* ```
*
* If you're using pure JS, it's _not_ possible to use the built-in class field syntax,
* i.e. the following will _not_ work:
*
* ```js
* // THIS IS WRONG IN JS!
* class MyContract extends SmartContract {
* x = State();
* }
* declareState(MyContract, { x: Field });
* ```
*
* Instead, add a constructor where you assign the property:
* ```js
* class MyContract extends SmartContract {
* constructor(x) {
* super();
* this.x = State();
* }
* }
* declareState(MyContract, { x: Field });
* ```
*/
function declareState(SmartContract, states) {
for (let key in states) {
let CircuitValue = states[key];
state(CircuitValue)(SmartContract.prototype, key);
}
}
function createState(defaultValue) {
return {
_contract: undefined,
defaultValue,
set(state) {
if (this._contract === undefined)
throw Error('set can only be called when the State is assigned to a SmartContract @state.');
let layout = getLayoutPosition(this._contract);
let stateAsFields = this._contract.stateType.toFields(state);
let accountUpdate = this._contract.instance.self;
stateAsFields.forEach((x, i) => {
let appStateSlot = accountUpdate.body.update.appState[layout.offset + i];
AccountUpdate.setValue(appStateSlot, x);
});
},
requireEquals(state) {
if (this._contract === undefined)
throw Error('requireEquals can only be called when the State is assigned to a SmartContract @state.');
let layout = getLayoutPosition(this._contract);
let stateAsFields = this._contract.stateType.toFields(state);
let accountUpdate = this._contract.instance.self;
stateAsFields.forEach((x, i) => {
let precondition = accountUpdate.body.preconditions.account.state[layout.offset + i];
ensureConsistentPrecondition(precondition, Bool(true), x, this._contract?.key);
AccountUpdate.assertEquals(precondition, x);
});
this._contract.wasConstrained = true;
},
requireEqualsIf(condition, state) {
if (this._contract === undefined)
throw Error('requireEqualsIf can only be called when the State is assigned to a SmartContract @state.');
let layout = getLayoutPosition(this._contract);
let stateAsFields = this._contract.stateType.toFields(state);
let accountUpdate = this._contract.instance.self;
stateAsFields.forEach((stateField, i) => {
let value = Provable.if(condition, stateField, Field(0));
ensureConsistentPrecondition(accountUpdate.body.preconditions.account.state[layout.offset + i], condition, value, this._contract?.key);
let state = accountUpdate.body.preconditions.account.state[layout.offset + i];
state.isSome = condition;
state.value = value;
});
this._contract.wasConstrained = true;
},
requireNothing() {
if (this._contract === undefined)
throw Error('requireNothing can only be called when the State is assigned to a SmartContract @state.');
// TODO: this should ideally reset any previous precondition,
// by setting each relevant state field to { isSome: false, value: Field(0) }
this._contract.wasConstrained = true;
},
get() {
if (this._contract === undefined)
throw Error('get can only be called when the State is assigned to a SmartContract @state.');
// inside the circuit, we have to cache variables, so there's only one unique variable per on-chain state.
// if we'd return a fresh variable every time, developers could easily end up linking just *one* of them to the precondition,
// while using an unconstrained variable elsewhere, which would create a loophole in the proof.
if (this._contract.cachedVariable !== undefined &&
// `inCheckedComputation() === true` here always implies being inside a wrapped smart contract method,
// which will ensure that the cache is cleaned up before & after each method run.
Provable.inCheckedComputation()) {
this._contract.wasRead = true;
return this._contract.cachedVariable;
}
let layout = getLayoutPosition(this._contract);
let contract = this._contract;
let inProver_ = Provable.inProver();
let stateFieldsType = Provable.Array(Field, layout.length);
let stateAsFields = Provable.witness(stateFieldsType, () => {
let account;
try {
account = Mina.getAccount(contract.instance.address, contract.instance.self.body.tokenId);
}
catch (err) {
// TODO: there should also be a reasonable error here
if (inProver_) {
throw err;
}
let message = `${contract.key}.get() failed, either:\n` +
`1. We can't find this zkapp account in the ledger\n` +
`2. Because the zkapp account was not found in the cache. ` +
`Try calling \`await fetchAccount(zkappAddress)\` first.\n` +
`If none of these are the case, then please reach out on Discord at #zkapp-developers and/or open an issue to tell us!`;
if (err.message) {
err.message = message + `\n\n${err.message}`;
throw err;
}
else {
throw Error(message);
}
}
if (account.zkapp?.appState === undefined) {
// if the account is not a zkapp account, let the default state be all zeroes
return Array(layout.length).fill(Field(0));
}
else {
let stateAsFields = [];
for (let i = 0; i < layout.length; ++i) {
stateAsFields.push(account.zkapp.appState[layout.offset + i]);
}
return stateAsFields;
}
});
let state = this._contract.stateType.fromFields(stateAsFields);
if (Provable.inCheckedComputation())
this._contract.stateType.check?.(state);
this._contract.wasRead = true;
this._contract.cachedVariable = state;
return state;
},
getAndRequireEquals() {
let state = this.get();
this.requireEquals(state);
return state;
},
async fetch() {
if (this._contract === undefined)
throw Error('fetch can only be called when the State is assigned to a SmartContract @state.');
let layout = getLayoutPosition(this._contract);
let address = this._contract.instance.address;
let tokenId = this._contract.instance.tokenId;
let account;
if (networkConfig.minaEndpoint === '') {
account = Mina.getAccount(address, tokenId);
}
else {
({ account } = await fetchAccount({
publicKey: address,
tokenId: TokenId.toBase58(tokenId),
}));
}
if (account === undefined)
return undefined;
let stateAsFields;
if (account.zkapp?.appState === undefined) {
stateAsFields = Array(layout.length).fill(Field(0));
}
else {
stateAsFields = [];
for (let i = 0; i < layout.length; i++) {
stateAsFields.push(account.zkapp.appState[layout.offset + i]);
}
}
return this._contract.stateType.fromFields(stateAsFields);
},
fromAppState(appState) {
if (this._contract === undefined)
throw Error('fromAppState() can only be called when the State is assigned to a SmartContract @state.');
let layout = getLayoutPosition(this._contract);
let stateAsFields = [];
for (let i = 0; i < layout.length; ++i) {
stateAsFields.push(appState[layout.offset + i]);
}
return this._contract.stateType.fromFields(stateAsFields);
},
};
}
function getLayoutPosition({ key, class: contractClass }) {
let layout = getLayout(contractClass);
let stateLayout = layout.get(key);
if (stateLayout === undefined) {
throw new Error(`state ${key} not found`);
}
return stateLayout;
}
function getLayout(scClass) {
let sc = smartContracts.get(scClass);
if (sc === undefined)
return new Map();
if (sc.layout === undefined) {
let layout = new Map();
sc.layout = layout;
let offset = 0;
sc.states.forEach(([key, stateType]) => {
let length = stateType.sizeInFields();
layout.set(key, { offset, length });
offset += length;
});
if (offset > 8) {
throw Error(`Found ${offset} on-chain state field elements on ${scClass.name}. Currently, only a total of 8 field elements of state are supported.`);
}
}
return sc.layout;
}
// per-smart contract class context for keeping track of state layout
const smartContracts = new WeakMap();
const reservedPropNames = new Set(['_methods', '_']);
function assertStatePrecondition(sc) {
try {
for (let [key, context] of getStateContexts(sc)) {
// check if every state that was read was also constrained
if (!context?.wasRead || context.wasConstrained)
continue;
// we accessed a precondition field but not constrained it explicitly - throw an error
let errorMessage = `You used \`this.${key}.get()\` without adding a precondition that links it to the actual on-chain state.
Consider adding this line to your code:
this.${key}.requireEquals(this.${key}.get());`;
throw Error(errorMessage);
}
}
finally {
cleanStatePrecondition(sc);
}
}
function cleanStatePrecondition(sc) {
for (let [, context] of getStateContexts(sc)) {
if (context === undefined)
continue;
context.wasRead = false;
context.wasConstrained = false;
context.cachedVariable = undefined;
}
}
function getStateContexts(sc) {
let scClass = sc.constructor;
let scInfo = smartContracts.get(scClass);
if (scInfo === undefined)
return [];
return scInfo.states.map(([key]) => [key, sc[key]?._contract]);
}
//# sourceMappingURL=state.js.map