o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
394 lines • 16.6 kB
JavaScript
/* ZkApp State
* -----------
*
* ZkApp State is typed using a StateLayout type variable, which is a mapped-type mapping from state
* names to Provable type implementations. A top-level StateDefinition is defined by zkApp
* developers and passed when constructing AccountUpdates (and related structures). The
* StatePreconditions and StateDefinition types define the internal representation of State values
* in preconditions and updates within AccountUpdates. There is also a GenericState representation
* which maps to the standard field array representation which is used by the protocol.
*/
// TODO: there is a lot of duplication here on the generic representation that we can reduce
import { Update } from './core.js';
import { Precondition } from './preconditions.js';
import { Bool } from '../../provable/bool.js';
import { Field } from '../../provable/field.js';
import { Provable } from '../../provable/provable.js';
import { ZkappConstants } from '../v1/constants.js';
export { StateValues, GenericStatePreconditions, StatePreconditions, StateDefinition, StateUpdates, GenericStateUpdates, StateMask, StateReader, State, };
const { MAX_ZKAPP_STATE_FIELDS } = ZkappConstants;
const CustomStateLayout = {
project(Layout, f) {
const entriesIn = Object.entries(Layout);
const entriesOut = entriesIn.map(([key, T]) => [key, f(key, T)]);
return Object.fromEntries(entriesOut);
},
// mapToArray<State extends CustomStateLayout, Out>(
// Layout: State,
// f: (key: keyof State, value: State[typeof key]) => Out
// ): Out[] {
// const out: Out[] = [];
// const keys = Object.keys(Layout) as (keyof State)[];
// keys.forEach((key) => out.push(f(key, Layout[key])));
// return out;
// }
};
const StateDefinition = {
split2(definition, value1, value2, generic, custom) {
if (definition === 'GenericState') {
return generic(value1, value2);
}
else {
return custom(definition.Layout, value1, value2);
}
},
map(definition, value, generic, custom) {
if (definition === 'GenericState') {
return generic(value);
}
else {
return custom(definition.Layout, value);
}
},
map2(definition, value1, value2, generic, custom) {
if (definition === 'GenericState') {
return generic(value1, value2);
}
else {
return custom(definition.Layout, value1, value2);
}
},
project(definition, generic, custom) {
return StateDefinition.map(definition, undefined, generic, custom);
},
convert(definition, value, generic, custom) {
return StateDefinition.map(definition, value, generic, custom);
},
};
// TODO: allow for explicit ordering/mapping of state field indices
function State(Layout) {
// TODO: proxy provable definition out of Struct with helper
// class StateDef extends Struct(Layout) {}
// TODO: check sizeInFields
const sizeInFields = Object.values(Layout)
.map((T) => T.sizeInFields())
.reduce((a, b) => a + b, 0);
return {
Layout,
sizeInFields() {
return sizeInFields;
},
toFields(x) {
const fields = [];
for (const key in Layout) {
fields.push(...Layout[key].toFields(x[key]));
}
return fields;
},
toAuxiliary(x) {
const aux = [];
for (const key in Layout) {
aux.push(Layout[key].toAuxiliary(x !== undefined ? x[key] : undefined));
}
return aux;
},
fromFields(_fields, _aux) {
throw new Error('TODO');
},
toValue(x) {
return x;
},
fromValue(x) {
return x;
},
check(_x) {
throw new Error('TODO');
},
};
// TODO: ^ get rid of the type-cast here (typescript's error message here is very unhelpful)
}
const StatePreconditions = {
empty(State) {
return StateDefinition.project(State, GenericStatePreconditions.empty, (Layout) => CustomStateLayout.project(Layout, (_key, T) => Precondition.Equals.disabled(T.empty())));
},
toGeneric(State, statePreconditions) {
return StateDefinition.convert(State, statePreconditions, (x) => x, (Layout, preconditions) => {
// const fieldPreconditions = CustomStateLayout.mapToArray<typeof Layout, Precondition.Equals<Field>>(
// Layout,
// (key: keyof State, T) => {
// const precondition = preconditions[key];
// const fields = T.toFields(precondition.value);
// return fields.map((field) => new Precondition.Equals(precondition.isEnabled, field));
// }
// ).flat();
const entries = Object.entries(Layout);
const fieldPreconditions = entries.flatMap(([key, T]) => {
const precondition = preconditions[key];
const fields = T.toFields(precondition.value);
return fields.map((field) => new Precondition.Equals(precondition.isEnabled, field));
});
return new GenericStatePreconditions(fieldPreconditions);
});
},
fromGeneric(statePreconditions, State) {
return StateDefinition.project(State, () => statePreconditions, (Layout) => {
// NB: this relies on the order of map being deterministic
// TODO: make the order of custom state layout keys deterministic (lol)
let i = 0;
return CustomStateLayout.project(Layout, (_key, T) => {
const fieldPreconditions = statePreconditions.preconditions.slice(i, i + T.sizeInFields());
i += T.sizeInFields();
if (fieldPreconditions.length === 0)
throw new Error('invalid state element field length');
const isEnabled = fieldPreconditions[0].isEnabled;
const allPreconditionsShareEnablement = Bool.allTrue(fieldPreconditions.map((precondition) => precondition.isEnabled.equals(isEnabled)));
if (allPreconditionsShareEnablement.not().toBoolean())
throw new Error('state field preconditions mapping to the same state field element were not all enabled/disabled equally');
const fields = fieldPreconditions.map((precondition) => precondition.value);
const value = T.fromFields(fields, /* TODO */ []);
return new Precondition.Equals(isEnabled, value);
});
});
},
toFieldPreconditions(State, preconditions) {
return [...StatePreconditions.toGeneric(State, preconditions).preconditions];
},
};
const StateUpdates = {
empty(State) {
return StateDefinition.project(State, GenericStateUpdates.empty, (Layout) => CustomStateLayout.project(Layout, (_key, T) => Update.disabled(T.empty())));
},
anyValuesAreSet(stateUpdates) {
const updates = stateUpdates instanceof GenericStateUpdates
? stateUpdates.updates
: Object.values(stateUpdates);
return Bool.anyTrue(updates.map((update) => update.set));
},
toGeneric(State, stateUpdates) {
return StateDefinition.convert(State, stateUpdates, (x) => x, (Layout, updates) => {
const entries = Object.entries(Layout);
const fieldUpdates = entries.flatMap(([key, T]) => {
const update = updates[key];
const update2 = update === undefined
? new Update(new Bool(false), T.empty())
: update instanceof Update
? update
: new Update(new Bool(true), update);
const fields = T.toFields(update2.value);
return fields.map((field) => new Update(update2.set, field));
});
return new GenericStateUpdates(fieldUpdates);
});
},
fromGeneric(stateUpdates, State) {
return StateDefinition.project(State, () => stateUpdates, (Layout) => {
// NB: this relies on the order of map being deterministic
// TODO: make the order of custom state layout keys deterministic (lol)
let i = 0;
return CustomStateLayout.project(Layout, (_key, T) => {
const fieldUpdates = stateUpdates.updates.slice(i, i + T.sizeInFields());
i += T.sizeInFields();
if (fieldUpdates.length === 0)
throw new Error('invalid state element field length');
const set = fieldUpdates[0].set;
const allUpdatesShareEnablement = Bool.allTrue(fieldUpdates.map((precondition) => precondition.set.equals(set)));
if (allUpdatesShareEnablement.not().toBoolean())
throw new Error('state field preconditions mapping to the same state field element were not all enabled/disabled equally');
const fields = fieldUpdates.map((precondition) => precondition.value);
const value = T.fromFields(fields, /* TODO */ []);
return new Update(set, value);
});
});
},
toFieldUpdates(State, updates) {
return [...StateUpdates.toGeneric(State, updates).updates];
},
};
const StateValues = {
empty(State) {
return StateDefinition.project(State, GenericStateValues.empty, (Layout) => CustomStateLayout.project(Layout, (_key, T) => T.empty()));
},
toGeneric(State, stateValues) {
return StateDefinition.convert(State, stateValues, (x) => x, (Layout, updates) => {
const entries = Object.entries(Layout);
const fieldValues = entries.flatMap(([key, T]) => {
const value = updates[key];
return T.toFields(value);
});
return new GenericStateValues(fieldValues);
});
},
fromGeneric(stateValues, State) {
return StateDefinition.project(State, () => stateValues, (Layout) => {
// NB: this relies on the order of map being deterministic
// TODO: make the order of custom state layout keys deterministic (lol)
let i = 0;
return CustomStateLayout.project(Layout, (_key, T) => {
const fields = stateValues.values.slice(i, i + T.sizeInFields());
i += T.sizeInFields();
return T.fromFields(fields, /* TODO */ []);
});
});
},
checkPreconditions(State, stateValues, statePreconditions) {
StateDefinition.split2(State, stateValues, statePreconditions, (values, preconditions) => {
for (const i in values.values) {
if (preconditions.preconditions[i].isSatisfied(values.values[i]).not().toBoolean())
throw new Error(`precondition for state field ${i} not satisfied`);
}
}, () => {
// TODO: evaluate these directly on the custom state representation and give meaningful errors
StateValues.checkPreconditions('GenericState', StateValues.toGeneric(State, stateValues), StatePreconditions.toGeneric(State, statePreconditions));
});
// if(State === 'GenericState') {
// // unsafely narrow types manually since typescript can't
// const state = (values as GenericStateValues).values;
// const statePreconditions = preconditions as GenericStatePreconditions;
// if(state.length !== MAX_ZKAPP_STATE_FIELDS)
// throw new Error('internal error: invalid number of generic state field values');
// if(state.length !== statePreconditions.preconditions.length)
// throw new Error('internal error: invalid number of generic state field preconditions');
// for(const i in state) {
// if(statePreconditions.preconditions[i].isSatisfied(state[i]).not().toBoolean())
// throw new Error(`precondition for state field ${i} not satisfied`);
// }
// } else {
// // TODO: evaluate these directly on the custom state representation and give meaningful errors
// StateValues.checkPreconditions(
// 'GenericState',
// StateValues.toGeneric(State, values),
// StatePreconditions.toGeneric(State, preconditions)
// );
// }
},
applyUpdates(State, stateValues, stateUpdates) {
return StateDefinition.map2(State, stateValues, stateUpdates, (values, updates) => values.map((value, i) => {
const update = updates.updates[i];
return update.set.toBoolean() ? update.value : value;
}), (Layout, values, updates) => {
const result = { ...values };
for (const key in Layout) {
const update = updates[key];
if (update !== undefined) {
const updateValue = update instanceof Update ? update : new Update(new Bool(true), update);
if (updateValue.set.toBoolean()) {
result[key] = updateValue.value;
}
}
}
return result;
});
},
};
const StateMask = {
create(State) {
return StateDefinition.project(State, GenericStateMask.empty, () => ({}));
},
};
const StateReader = {
create(State, stateValues, stateMask) {
if (State === 'GenericState') {
const values = stateValues;
const mask = stateMask;
return new GenericStateReader(values, mask);
}
else {
const values = stateValues;
const mask = stateMask;
return CustomStateLayout.project(State.Layout, (key, T) => () => {
return Provable.witness(T, () => {
const value = values.get()[key];
mask.get()[key] = value;
return value;
});
});
}
},
};
class StateFieldsArray {
constructor(fieldElements, empty) {
this.fieldElements = fieldElements;
if (this.fieldElements.length > MAX_ZKAPP_STATE_FIELDS) {
throw new Error('exceeded maximum number of state elements');
}
if (this.fieldElements.length < MAX_ZKAPP_STATE_FIELDS) {
for (let i = this.fieldElements.length; i < MAX_ZKAPP_STATE_FIELDS; i++) {
this.fieldElements.push(empty());
}
}
if (this.fieldElements.length !== MAX_ZKAPP_STATE_FIELDS) {
throw new Error('internal error: invariant broken');
}
}
get fields() {
return [...this.fieldElements];
}
}
class GenericStateValues extends StateFieldsArray {
constructor(values) {
super(values, Field.empty);
}
get values() {
return this.fields;
}
get(index) {
if (index >= MAX_ZKAPP_STATE_FIELDS)
throw new Error('zkapp state index out of bounds');
return this.fields[index];
}
map(f) {
return new GenericStateValues(this.values.map(f));
}
static empty() {
return new GenericStateValues([]);
}
}
class GenericStatePreconditions extends StateFieldsArray {
constructor(preconditions) {
super(preconditions, () => Precondition.Equals.disabled(Field.empty()));
}
get preconditions() {
return this.fields;
}
static empty() {
return new GenericStatePreconditions([]);
}
}
class GenericStateUpdates extends StateFieldsArray {
constructor(updates) {
super(updates, () => Update.disabled(Field.empty()));
}
get updates() {
return this.fields;
}
static empty() {
return new GenericStateUpdates([]);
}
}
class GenericStateMask extends StateFieldsArray {
constructor() {
super([], () => undefined);
}
set(index, value) {
if (index >= MAX_ZKAPP_STATE_FIELDS)
throw new Error('zkapp state index out of bounds');
this.fields[index] = value;
}
static empty() {
return new GenericStateMask();
}
}
class GenericStateReader {
constructor(values, mask) {
this.values = values;
this.mask = mask;
}
read(index) {
return Provable.witness(Field, () => {
const value = this.values.get().get(index);
this.mask.get().set(index, value);
return value;
});
}
}
//# sourceMappingURL=state.js.map