/* 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 { Empty, Eq, ProvableInstance, 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 { Unconstrained } from '../../provable/types/unconstrained.js'; import { ZkappConstants } from '../v1/constants.js'; export { StateValues, GenericStatePreconditions, StatePreconditions, StateDefinition, StateUpdates, StateLayout, GenericStateUpdates, StateMask, StateReader, State, }; const { MAX_ZKAPP_STATE_FIELDS } = ZkappConstants; // TODO IMMEDIATELY: This representation doesn't actually work, because if you specify a state // element in a custom state layout that doesn't satisfy the StateElement type, // typescript will just replace the state element types in the layout with `any`. // Fucking typescript. type StateElement> = Provable & Empty; // type StateElementInstance = E extends StateElement ? T : never; // TODO: custom state layouts need to specify the order of their keys type CustomStateLayout = { [name: string]: Provable & Empty }; type StateLayout = 'GenericState' | CustomStateLayout; const CustomStateLayout = { project( Layout: StateIn, f: (key: keyof StateIn, value: StateIn[typeof key]) => StateOut[typeof key] ): StateOut { const entriesIn = Object.entries(Layout) as [keyof StateIn, Provable][]; const entriesOut = entriesIn.map(([key, T]) => [key, f(key, T as StateIn[typeof key])]); return Object.fromEntries(entriesOut); }, // mapToArray( // 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; // } }; type StateDefinition = State extends 'GenericState' ? 'GenericState' : { Layout: State } & Provable<{ [name in keyof State]: ProvableInstance; }>; const StateDefinition = { split2( definition: StateDefinition, value1: State extends 'GenericState' ? Generic1 : Custom1, value2: State extends 'GenericState' ? Generic2 : Custom2, generic: (x1: Generic1, x2: Generic2) => void, custom: (layout: CustomStateLayout, x1: Custom1, x2: Custom2) => void ) { if (definition === 'GenericState') { return generic(value1 as Generic1, value2 as Generic2); } else { return custom(definition.Layout, value1 as Custom1, value2 as Custom2); } }, map( definition: StateDefinition, value: State extends 'GenericState' ? GenericIn : CustomIn, generic: (x: GenericIn) => GenericOut, custom: (layout: CustomStateLayout, x: CustomIn) => CustomOut ): State extends 'GenericState' ? GenericOut : CustomOut { if (definition === 'GenericState') { return generic(value as GenericIn) as State extends 'GenericState' ? GenericOut : CustomOut; } else { return custom(definition.Layout, value as CustomIn) as State extends 'GenericState' ? GenericOut : CustomOut; } }, map2< State extends StateLayout, GenericIn1, CustomIn1, GenericIn2, CustomIn2, GenericOut, CustomOut, >( definition: StateDefinition, value1: State extends 'GenericState' ? GenericIn1 : CustomIn1, value2: State extends 'GenericState' ? GenericIn2 : CustomIn2, generic: (x1: GenericIn1, x2: GenericIn2) => GenericOut, custom: (layout: CustomStateLayout, x1: CustomIn1, x2: CustomIn2) => CustomOut ): State extends 'GenericState' ? GenericOut : CustomOut { if (definition === 'GenericState') { return generic(value1 as GenericIn1, value2 as GenericIn2) as State extends 'GenericState' ? GenericOut : CustomOut; } else { return custom( definition.Layout, value1 as CustomIn1, value2 as CustomIn2 ) as State extends 'GenericState' ? GenericOut : CustomOut; } }, project( definition: StateDefinition, generic: () => Generic, custom: (layout: CustomStateLayout) => Custom ): State extends 'GenericState' ? Generic : Custom { return StateDefinition.map(definition, undefined, generic, custom); }, convert( definition: StateDefinition, value: State extends 'GenericState' ? Generic : Custom, generic: (x: Generic) => Out, custom: (layout: CustomStateLayout, x: Custom) => Out ): Out { return StateDefinition.map(definition, value, generic, custom); }, }; // TODO: allow for explicit ordering/mapping of state field indices function State(Layout: State): StateDefinition { // 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(): number { return sizeInFields; }, toFields(x: { [name in keyof State]: ProvableInstance; }): Field[] { const fields = []; for (const key in Layout) { fields.push(...Layout[key].toFields(x[key])); } return fields; }, toAuxiliary(x?: { [name in keyof State]: ProvableInstance; }): any[] { const aux = []; for (const key in Layout) { aux.push(Layout[key].toAuxiliary(x !== undefined ? x[key] : undefined)); } return aux; }, fromFields( _fields: Field[], _aux: any[] ): { [name in keyof State]: ProvableInstance } { throw new Error('TODO'); }, toValue(x: { [name in keyof State]: ProvableInstance }): { [name in keyof State]: ProvableInstance; } { return x; }, fromValue(x: { [name in keyof State]: ProvableInstance; }): { [name in keyof State]: ProvableInstance } { return x; }, check(_x: { [name in keyof State]: ProvableInstance; }): void { throw new Error('TODO'); }, } as StateDefinition; // TODO: ^ get rid of the type-cast here (typescript's error message here is very unhelpful) } type StatePreconditions = State extends 'GenericState' ? GenericStatePreconditions : { [name in keyof State]: Precondition.Equals< ProvableInstance & Eq> >; }; const StatePreconditions = { empty(State: StateDefinition): StatePreconditions { return StateDefinition.project( State, GenericStatePreconditions.empty, (Layout: CustomStateLayout) => CustomStateLayout.project(Layout, (_key, T) => Precondition.Equals.disabled(T.empty())) ); }, toGeneric( State: StateDefinition, statePreconditions: StatePreconditions ): StatePreconditions<'GenericState'> { return StateDefinition.convert( State, statePreconditions, (x: GenericStatePreconditions) => x as StatePreconditions<'GenericState'>, ( Layout: CustomStateLayout, preconditions: { [name in keyof State]: Precondition.Equals< ProvableInstance & Eq> >; } ) => { // const fieldPreconditions = CustomStateLayout.mapToArray>( // 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) as [keyof State, StateElement][]; 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: StatePreconditions<'GenericState'>, State: StateDefinition ): StatePreconditions { return StateDefinition.project( State, () => statePreconditions, (Layout: CustomStateLayout) => { // 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: StateDefinition, preconditions: StatePreconditions ): Precondition.Equals[] { return [...StatePreconditions.toGeneric(State, preconditions).preconditions]; }, }; type StateUpdates = State extends 'GenericState' ? GenericStateUpdates : { [name in keyof State]?: ProvableInstance | Update>; }; const StateUpdates = { empty(State: StateDefinition): StateUpdates { return StateDefinition.project(State, GenericStateUpdates.empty, (Layout: CustomStateLayout) => CustomStateLayout.project(Layout, (_key, T) => Update.disabled(T.empty())) ); }, anyValuesAreSet(stateUpdates: StateUpdates): Bool { const updates: Update[] = stateUpdates instanceof GenericStateUpdates ? stateUpdates.updates : Object.values(stateUpdates); return Bool.anyTrue(updates.map((update) => update.set)); }, toGeneric( State: StateDefinition, stateUpdates: StateUpdates ): StateUpdates<'GenericState'> { return StateDefinition.convert( State, stateUpdates, (x: GenericStateUpdates) => x as StateUpdates<'GenericState'>, ( Layout: CustomStateLayout, updates: { [name in keyof State]?: | ProvableInstance | Update>; } ) => { const entries = Object.entries(Layout) as [keyof State, Provable & Empty][]; 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: StateUpdates<'GenericState'>, State: StateDefinition ): StateUpdates { return StateDefinition.project( State, () => stateUpdates, (Layout: CustomStateLayout) => { // 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: StateDefinition, updates: StateUpdates ): Update[] { return [...StateUpdates.toGeneric(State, updates).updates]; }, }; type StateValues = State extends 'GenericState' ? GenericStateValues : { [name in keyof State]: ProvableInstance }; const StateValues = { empty(State: StateDefinition): StateValues { return StateDefinition.project(State, GenericStateValues.empty, (Layout: CustomStateLayout) => CustomStateLayout.project(Layout, (_key, T) => T.empty()) ); }, toGeneric( State: StateDefinition, stateValues: StateValues ): StateValues<'GenericState'> { return StateDefinition.convert( State, stateValues, (x: GenericStateValues) => x as StateValues<'GenericState'>, ( Layout: CustomStateLayout, updates: { [name in keyof State]?: ProvableInstance } ) => { const entries = Object.entries(Layout) as [keyof State, Provable][]; const fieldValues = entries.flatMap(([key, T]) => { const value = updates[key]; return T.toFields(value); }); return new GenericStateValues(fieldValues); } ); }, fromGeneric( stateValues: StateValues<'GenericState'>, State: StateDefinition ): StateValues { return StateDefinition.project( State, () => stateValues, (Layout: CustomStateLayout) => { // 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: StateDefinition, stateValues: StateValues, statePreconditions: StatePreconditions ): void { 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: StateDefinition, stateValues: StateValues, stateUpdates: StateUpdates ): StateValues { 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): { [name in keyof State]: ProvableInstance } => { const result = { ...values }; for (const key in Layout) { const update = updates[key as keyof State]; if (update !== undefined) { const updateValue = update instanceof Update ? update : new Update(new Bool(true), update); if (updateValue.set.toBoolean()) { result[key as keyof State] = updateValue.value; } } } return result; } ); }, }; type StateMask = State extends 'GenericState' ? GenericStateMask : { [name in keyof State]?: ProvableInstance }; const StateMask = { create(State: StateDefinition): StateMask { return StateDefinition.project(State, GenericStateMask.empty, () => ({})); }, }; type StateReader = State extends 'GenericState' ? GenericStateReader : { [name in keyof State]: State[name] extends Provable ? () => U : never; }; const StateReader = { create( State: StateDefinition, stateValues: Unconstrained>, stateMask: Unconstrained> ): StateReader { if (State === 'GenericState') { const values = stateValues as Unconstrained; const mask = stateMask as Unconstrained; return new GenericStateReader(values, mask) as StateReader; } else { const values = stateValues as Unconstrained<{ [name in keyof State]: ProvableInstance; }>; const mask = stateMask as Unconstrained<{ [name in keyof State]?: ProvableInstance; }>; return CustomStateLayout.project(State.Layout, (key, T) => (): ProvableInstance => { return Provable.witness(T, () => { const value = values.get()[key as keyof State]; mask.get()[key as keyof State] = value; return value; }); }) as StateReader; } }, }; class StateFieldsArray { constructor( private fieldElements: T[], empty: () => T ) { 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(): T[] { return [...this.fieldElements]; } } class GenericStateValues extends StateFieldsArray { constructor(values: Field[]) { super(values, Field.empty); } get values(): Field[] { return this.fields; } get(index: number): Field { if (index >= MAX_ZKAPP_STATE_FIELDS) throw new Error('zkapp state index out of bounds'); return this.fields[index]; } map(f: (x: Field, i: number) => Field): GenericStateValues { return new GenericStateValues(this.values.map(f)); } static empty(): GenericStateValues { return new GenericStateValues([]); } } class GenericStatePreconditions extends StateFieldsArray> { constructor(preconditions: Precondition.Equals[]) { super(preconditions, () => Precondition.Equals.disabled(Field.empty())); } get preconditions(): Precondition.Equals[] { return this.fields; } static empty(): GenericStatePreconditions { return new GenericStatePreconditions([]); } } class GenericStateUpdates extends StateFieldsArray> { constructor(updates: Update[]) { super(updates, () => Update.disabled(Field.empty())); } get updates(): Update[] { return this.fields; } static empty(): GenericStateUpdates { return new GenericStateUpdates([]); } } class GenericStateMask extends StateFieldsArray { constructor() { super([], () => undefined); } set(index: number, value: Field): void { if (index >= MAX_ZKAPP_STATE_FIELDS) throw new Error('zkapp state index out of bounds'); this.fields[index] = value; } static empty(): GenericStateMask { return new GenericStateMask(); } } class GenericStateReader { constructor( private values: Unconstrained, private mask: Unconstrained ) {} read(index: number): Field { return Provable.witness(Field, () => { const value = this.values.get().get(index); this.mask.get().set(index, value); return value; }); } }