UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

688 lines (616 loc) 24 kB
/* 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<T extends Eq<T>> = Provable<T> & Empty<T>; // type StateElementInstance<E> = E extends StateElement<infer T> ? T : never; // TODO: custom state layouts need to specify the order of their keys type CustomStateLayout = { [name: string]: Provable<any> & Empty<any> }; type StateLayout = 'GenericState' | CustomStateLayout; const CustomStateLayout = { project<StateIn extends CustomStateLayout, StateOut extends { [name in keyof StateIn]: unknown }>( Layout: StateIn, f: (key: keyof StateIn, value: StateIn[typeof key]) => StateOut[typeof key] ): StateOut { const entriesIn = Object.entries(Layout) as [keyof StateIn, Provable<any>][]; const entriesOut = entriesIn.map(([key, T]) => [key, f(key, T as StateIn[typeof key])]); 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; // } }; type StateDefinition<State extends StateLayout> = State extends 'GenericState' ? 'GenericState' : { Layout: State } & Provable<{ [name in keyof State]: ProvableInstance<State[name]>; }>; const StateDefinition = { split2<State extends StateLayout, Generic1, Custom1, Generic2, Custom2>( definition: StateDefinition<State>, 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<State extends StateLayout, GenericIn, CustomIn, GenericOut, CustomOut>( definition: StateDefinition<State>, 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<State>, 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<State extends StateLayout, Generic, Custom>( definition: StateDefinition<State>, generic: () => Generic, custom: (layout: CustomStateLayout) => Custom ): State extends 'GenericState' ? Generic : Custom { return StateDefinition.map(definition, undefined, generic, custom); }, convert<State extends StateLayout, Generic, Custom, Out>( definition: StateDefinition<State>, 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<State extends CustomStateLayout>(Layout: State): StateDefinition<State> { // 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<State[name]>; }): Field[] { const fields = []; for (const key in Layout) { fields.push(...Layout[key].toFields(x[key])); } return fields; }, toAuxiliary(x?: { [name in keyof State]: ProvableInstance<State[name]>; }): 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<State[name]> } { throw new Error('TODO'); }, toValue(x: { [name in keyof State]: ProvableInstance<State[name]> }): { [name in keyof State]: ProvableInstance<State[name]>; } { return x; }, fromValue(x: { [name in keyof State]: ProvableInstance<State[name]>; }): { [name in keyof State]: ProvableInstance<State[name]> } { return x; }, check(_x: { [name in keyof State]: ProvableInstance<State[name]>; }): void { throw new Error('TODO'); }, } as StateDefinition<State>; // TODO: ^ get rid of the type-cast here (typescript's error message here is very unhelpful) } type StatePreconditions<State extends StateLayout> = State extends 'GenericState' ? GenericStatePreconditions : { [name in keyof State]: Precondition.Equals< ProvableInstance<State[name]> & Eq<ProvableInstance<State[name]>> >; }; const StatePreconditions = { empty<State extends StateLayout>(State: StateDefinition<State>): StatePreconditions<State> { return StateDefinition.project( State, GenericStatePreconditions.empty, (Layout: CustomStateLayout) => CustomStateLayout.project(Layout, (_key, T) => Precondition.Equals.disabled(T.empty())) ); }, toGeneric<State extends StateLayout>( State: StateDefinition<State>, statePreconditions: StatePreconditions<State> ): StatePreconditions<'GenericState'> { return StateDefinition.convert( State, statePreconditions, (x: GenericStatePreconditions) => x as StatePreconditions<'GenericState'>, ( Layout: CustomStateLayout, preconditions: { [name in keyof State]: Precondition.Equals< ProvableInstance<State[name]> & Eq<ProvableInstance<State[name]>> >; } ) => { // 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) as [keyof State, StateElement<any>][]; 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<State extends StateLayout>( statePreconditions: StatePreconditions<'GenericState'>, State: StateDefinition<State> ): StatePreconditions<State> { 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 extends StateLayout>( State: StateDefinition<State>, preconditions: StatePreconditions<State> ): Precondition.Equals<Field>[] { return [...StatePreconditions.toGeneric(State, preconditions).preconditions]; }, }; type StateUpdates<State extends StateLayout> = State extends 'GenericState' ? GenericStateUpdates : { [name in keyof State]?: ProvableInstance<State[name]> | Update<ProvableInstance<State[name]>>; }; const StateUpdates = { empty<State extends StateLayout>(State: StateDefinition<State>): StateUpdates<State> { return StateDefinition.project(State, GenericStateUpdates.empty, (Layout: CustomStateLayout) => CustomStateLayout.project(Layout, (_key, T) => Update.disabled(T.empty())) ); }, anyValuesAreSet<State extends StateLayout>(stateUpdates: StateUpdates<State>): Bool { const updates: Update<unknown>[] = stateUpdates instanceof GenericStateUpdates ? stateUpdates.updates : Object.values(stateUpdates); return Bool.anyTrue(updates.map((update) => update.set)); }, toGeneric<State extends StateLayout>( State: StateDefinition<State>, stateUpdates: StateUpdates<State> ): StateUpdates<'GenericState'> { return StateDefinition.convert( State, stateUpdates, (x: GenericStateUpdates) => x as StateUpdates<'GenericState'>, ( Layout: CustomStateLayout, updates: { [name in keyof State]?: | ProvableInstance<State[name]> | Update<ProvableInstance<State[name]>>; } ) => { const entries = Object.entries(Layout) as [keyof State, Provable<any> & Empty<any>][]; 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<State extends StateLayout>( stateUpdates: StateUpdates<'GenericState'>, State: StateDefinition<State> ): StateUpdates<State> { 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 extends StateLayout>( State: StateDefinition<State>, updates: StateUpdates<State> ): Update<Field>[] { return [...StateUpdates.toGeneric(State, updates).updates]; }, }; type StateValues<State extends StateLayout> = State extends 'GenericState' ? GenericStateValues : { [name in keyof State]: ProvableInstance<State[name]> }; const StateValues = { empty<State extends StateLayout>(State: StateDefinition<State>): StateValues<State> { return StateDefinition.project(State, GenericStateValues.empty, (Layout: CustomStateLayout) => CustomStateLayout.project(Layout, (_key, T) => T.empty()) ); }, toGeneric<State extends StateLayout>( State: StateDefinition<State>, stateValues: StateValues<State> ): StateValues<'GenericState'> { return StateDefinition.convert( State, stateValues, (x: GenericStateValues) => x as StateValues<'GenericState'>, ( Layout: CustomStateLayout, updates: { [name in keyof State]?: ProvableInstance<State[name]> } ) => { const entries = Object.entries(Layout) as [keyof State, Provable<any>][]; const fieldValues = entries.flatMap(([key, T]) => { const value = updates[key]; return T.toFields(value); }); return new GenericStateValues(fieldValues); } ); }, fromGeneric<State extends StateLayout>( stateValues: StateValues<'GenericState'>, State: StateDefinition<State> ): StateValues<State> { 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 extends StateLayout>( State: StateDefinition<State>, stateValues: StateValues<State>, statePreconditions: StatePreconditions<State> ): 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 extends StateLayout>( State: StateDefinition<State>, stateValues: StateValues<State>, stateUpdates: StateUpdates<State> ): StateValues<State> { 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<State[name]> } => { 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 StateLayout> = State extends 'GenericState' ? GenericStateMask : { [name in keyof State]?: ProvableInstance<State[name]> }; const StateMask = { create<State extends StateLayout>(State: StateDefinition<State>): StateMask<State> { return StateDefinition.project(State, GenericStateMask.empty, () => ({})); }, }; type StateReader<State extends StateLayout> = State extends 'GenericState' ? GenericStateReader : { [name in keyof State]: State[name] extends Provable<infer U> ? () => U : never; }; const StateReader = { create<State extends StateLayout>( State: StateDefinition<State>, stateValues: Unconstrained<StateValues<State>>, stateMask: Unconstrained<StateMask<State>> ): StateReader<State> { if (State === 'GenericState') { const values = stateValues as Unconstrained<GenericStateValues>; const mask = stateMask as Unconstrained<GenericStateMask>; return new GenericStateReader(values, mask) as StateReader<State>; } else { const values = stateValues as Unconstrained<{ [name in keyof State]: ProvableInstance<State[name]>; }>; const mask = stateMask as Unconstrained<{ [name in keyof State]?: ProvableInstance<State[name]>; }>; return CustomStateLayout.project(State.Layout, (key, T) => (): ProvableInstance<typeof T> => { return Provable.witness(T, () => { const value = values.get()[key as keyof State]; mask.get()[key as keyof State] = value; return value; }); }) as StateReader<State>; } }, }; class StateFieldsArray<T> { 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<Field> { 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<Precondition.Equals<Field>> { constructor(preconditions: Precondition.Equals<Field>[]) { super(preconditions, () => Precondition.Equals.disabled(Field.empty())); } get preconditions(): Precondition.Equals<Field>[] { return this.fields; } static empty(): GenericStatePreconditions { return new GenericStatePreconditions([]); } } class GenericStateUpdates extends StateFieldsArray<Update<Field>> { constructor(updates: Update<Field>[]) { super(updates, () => Update.disabled(Field.empty())); } get updates(): Update<Field>[] { return this.fields; } static empty(): GenericStateUpdates { return new GenericStateUpdates([]); } } class GenericStateMask extends StateFieldsArray<Field | undefined> { 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<GenericStateValues>, private mask: Unconstrained<GenericStateMask> ) {} read(index: number): Field { return Provable.witness(Field, () => { const value = this.values.get().get(index); this.mask.get().set(index, value); return value; }); } }