From 1d0bdde7de395aa39688f674b7897d7b668c3bf5 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 2 May 2024 08:56:44 +0200 Subject: [PATCH 01/97] move existing reducer code into a new folder --- src/index.ts | 8 +- src/lib/mina/actions/reducer.ts | 317 ++++++++++++++++++++++++++++++++ src/lib/mina/zkapp.ts | 306 +----------------------------- 3 files changed, 321 insertions(+), 310 deletions(-) create mode 100644 src/lib/mina/actions/reducer.ts diff --git a/src/index.ts b/src/index.ts index 42c4614ef6..85ac939a9e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -59,12 +59,8 @@ export { type PendingTransactionPromise, } from './lib/mina/transaction.js'; export type { DeployArgs } from './lib/mina/zkapp.js'; -export { - SmartContract, - method, - declareMethods, - Reducer, -} from './lib/mina/zkapp.js'; +export { SmartContract, method, declareMethods } from './lib/mina/zkapp.js'; +export { Reducer } from './lib/mina/actions/reducer.js'; export { state, State, declareState } from './lib/mina/state.js'; export type { JsonProof } from './lib/proof-system/zkprogram.js'; diff --git a/src/lib/mina/actions/reducer.ts b/src/lib/mina/actions/reducer.ts new file mode 100644 index 0000000000..47ead7d9b7 --- /dev/null +++ b/src/lib/mina/actions/reducer.ts @@ -0,0 +1,317 @@ +import { Field } from '../../provable/wrapped.js'; +import { Actions } from '../account-update.js'; +import { + FlexibleProvablePure, + InferProvable, + provable, +} from '../../provable/types/struct.js'; +import { Provable } from '../../provable/provable.js'; +import { ProvableHashable } from '../../provable/crypto/poseidon.js'; +import * as Mina from '../mina.js'; +import { ProvablePure } from '../../provable/types/provable-intf.js'; +import { MerkleList } from '../../provable/merkle-list.js'; +import type { SmartContract } from '../zkapp.js'; + +export { Reducer, getReducer }; + +const Reducer: (< + T extends FlexibleProvablePure, + A extends InferProvable = InferProvable +>(reducer: { + actionType: T; +}) => ReducerReturn) & { + initialActionState: Field; +} = Object.defineProperty( + function (reducer: any) { + // we lie about the return value here, and instead overwrite this.reducer with + // a getter, so we can get access to `this` inside functions on this.reducer (see constructor) + return reducer; + }, + 'initialActionState', + { get: Actions.emptyActionState } +) as any; + +type Reducer = { + actionType: FlexibleProvablePure; +}; + +type ReducerReturn = { + /** + * Dispatches an {@link Action}. Similar to normal {@link Event}s, + * {@link Action}s can be stored by archive nodes and later reduced within a {@link SmartContract} method + * to change the state of the contract accordingly + * + * ```ts + * this.reducer.dispatch(Field(1)); // emits one action + * ``` + * + * */ + dispatch(action: Action): void; + /** + * Reduces a list of {@link Action}s, similar to `Array.reduce()`. + * + * ```ts + * let pendingActions = this.reducer.getActions({ + * fromActionState: actionState, + * }); + * + * let newState = this.reducer.reduce( + * pendingActions, + * Field, // the state type + * (state: Field, _action: Field) => { + * return state.add(1); + * }, + * initialState // initial state + * ); + * ``` + * + */ + reduce( + actions: MerkleList>, + stateType: Provable, + reduce: (state: State, action: Action) => State, + initial: State, + options?: { + maxUpdatesWithActions?: number; + maxActionsPerUpdate?: number; + skipActionStatePrecondition?: boolean; + } + ): State; + /** + * Perform circuit logic for every {@link Action} in the list. + * + * This is a wrapper around {@link reduce} for when you don't need `state`. + */ + forEach( + actions: MerkleList>, + reduce: (action: Action) => void, + options?: { + maxUpdatesWithActions?: number; + maxActionsPerUpdate?: number; + skipActionStatePrecondition?: boolean; + } + ): void; + /** + * Fetches the list of previously emitted {@link Action}s by this {@link SmartContract}. + * ```ts + * let pendingActions = this.reducer.getActions({ + * fromActionState: actionState, + * }); + * ``` + * + * The final action state can be accessed on `pendingActions.hash`. + * ```ts + * let endActionState = pendingActions.hash; + * ``` + * + * If the optional `endActionState` is provided, the list of actions will be fetched up to that state. + * In that case, `pendingActions.hash` is guaranteed to equal `endActionState`. + */ + getActions({ + fromActionState, + endActionState, + }?: { + fromActionState?: Field; + endActionState?: Field; + }): MerkleList>; + /** + * Fetches the list of previously emitted {@link Action}s by zkapp {@link SmartContract}. + * ```ts + * let pendingActions = await zkapp.reducer.fetchActions({ + * fromActionState: actionState, + * }); + * ``` + */ + fetchActions({ + fromActionState, + endActionState, + }?: { + fromActionState?: Field; + endActionState?: Field; + }): Promise; +}; + +function getReducer(contract: SmartContract): ReducerReturn { + let reducer: Reducer = ((contract as any)._ ??= {}).reducer; + if (reducer === undefined) + throw Error( + 'You are trying to use a reducer without having declared its type.\n' + + `Make sure to add a property \`reducer\` on ${contract.constructor.name}, for example: +class ${contract.constructor.name} extends SmartContract { + reducer = { actionType: Field }; +}` + ); + return { + dispatch(action: A) { + let accountUpdate = contract.self; + let eventFields = reducer.actionType.toFields(action); + accountUpdate.body.actions = Actions.pushEvent( + accountUpdate.body.actions, + eventFields + ); + }, + + reduce( + actionLists: MerkleList>, + stateType: Provable, + reduce: (state: S, action: A) => S, + state: S, + { + maxUpdatesWithActions = 32, + maxActionsPerUpdate = 1, + skipActionStatePrecondition = false, + } = {} + ): S { + Provable.asProver(() => { + if (actionLists.data.get().length > maxUpdatesWithActions) { + throw Error( + `reducer.reduce: Exceeded the maximum number of lists of actions, ${maxUpdatesWithActions}. + Use the optional \`maxUpdatesWithActions\` argument to increase this number.` + ); + } + }); + + if (!skipActionStatePrecondition) { + // the actionList.hash is the hash of all actions in that list, appended to the previous hash (the previous list of historical actions) + // this must equal one of the action states as preconditions to build a chain to that we only use actions that were dispatched between the current on chain action state and the initialActionState + contract.account.actionState.requireEquals(actionLists.hash); + } + + const listIter = actionLists.startIterating(); + + for (let i = 0; i < maxUpdatesWithActions; i++) { + let { element: merkleActions, isDummy } = listIter.Unsafe.next(); + let actionIter = merkleActions.startIterating(); + let newState = state; + + if (maxActionsPerUpdate === 1) { + // special case with less work, because the only action is a dummy iff merkleActions is a dummy + let action = Provable.witness( + reducer.actionType, + () => + actionIter.data.get()[0]?.element ?? + actionIter.innerProvable.empty() + ); + let emptyHash = actionIter.Constructor.emptyHash; + let finalHash = actionIter.nextHash(emptyHash, action); + finalHash = Provable.if(isDummy, emptyHash, finalHash); + + // note: this asserts nothing in the isDummy case, because `actionIter.hash` is not well-defined + // but it doesn't matter because we're also skipping all state and action state updates in that case + actionIter.hash.assertEquals(finalHash); + + newState = reduce(newState, action); + } else { + for (let j = 0; j < maxActionsPerUpdate; j++) { + let { element: action, isDummy } = actionIter.Unsafe.next(); + newState = Provable.if( + isDummy, + stateType, + newState, + reduce(newState, action) + ); + } + // note: this asserts nothing about the iterated actions if `MerkleActions` is a dummy + // which doesn't matter because we're also skipping all state and action state updates in that case + actionIter.assertAtEnd(); + } + + state = Provable.if(isDummy, stateType, state, newState); + } + + // important: we check that by iterating, we actually reached the claimed final action state + listIter.assertAtEnd(); + + return state; + }, + + forEach( + actionLists: MerkleList>, + callback: (action: A) => void, + config + ) { + const stateType = provable(null); + this.reduce( + actionLists, + stateType, + (_, action) => { + callback(action); + return null; + }, + null, + config + ); + }, + + getActions(config?: { + fromActionState?: Field; + endActionState?: Field; + }): MerkleList> { + const Action = reducer.actionType; + const emptyHash = Actions.empty().hash; + const nextHash = (hash: Field, action: A) => + Actions.pushEvent({ hash, data: [] }, Action.toFields(action)).hash; + + class ActionList extends MerkleList.create( + Action as unknown as ProvableHashable, + nextHash, + emptyHash + ) {} + + class MerkleActions extends MerkleList.create( + ActionList.provable, + (hash: Field, actions: ActionList) => + Actions.updateSequenceState(hash, actions.hash), + // if no "start" action hash was specified, this means we are fetching the entire history of actions, which started from the empty action state hash + // otherwise we are only fetching a part of the history, which starts at `fromActionState` + // TODO does this show that `emptyHash` should be part of the instance, not the class? that would make the provable representation bigger though + config?.fromActionState ?? Actions.emptyActionState() + ) {} + + let actions = Provable.witness(MerkleActions.provable, () => { + let actionFields = Mina.getActions( + contract.address, + config, + contract.tokenId + ); + // convert string-Fields back into the original action type + let actions = actionFields.map((event) => + event.actions.map((action) => + (reducer.actionType as ProvablePure).fromFields( + action.map(Field) + ) + ) + ); + return MerkleActions.from( + actions.map((a) => ActionList.fromReverse(a)) + ); + }); + // note that we don't have to assert anything about the initial action state here, + // because it is taken directly and not witnessed + if (config?.endActionState !== undefined) { + actions.hash.assertEquals(config.endActionState); + } + return actions; + }, + + async fetchActions(config?: { + fromActionState?: Field; + endActionState?: Field; + }): Promise { + let result = await Mina.fetchActions( + contract.address, + config, + contract.tokenId + ); + if ('error' in result) { + throw Error(JSON.stringify(result)); + } + return result.map((event) => + // putting our string-Fields back into the original action type + event.actions.map((action) => + (reducer.actionType as ProvablePure).fromFields(action.map(Field)) + ) + ); + }, + }; +} diff --git a/src/lib/mina/zkapp.ts b/src/lib/mina/zkapp.ts index cea652cffb..be1a1ed9af 100644 --- a/src/lib/mina/zkapp.ts +++ b/src/lib/mina/zkapp.ts @@ -74,10 +74,10 @@ import { } from './smart-contract-context.js'; import { assertPromise } from '../util/assert.js'; import { ProvablePure } from '../provable/types/provable-intf.js'; -import { MerkleList } from '../provable/merkle-list.js'; +import { getReducer, Reducer } from './actions/reducer.js'; // external API -export { SmartContract, method, DeployArgs, declareMethods, Reducer }; +export { SmartContract, method, DeployArgs, declareMethods }; const reservedPropNames = new Set(['_methods', '_']); type AsyncFunction = (...args: any) => Promise; @@ -1162,291 +1162,6 @@ super.init(); } } -type Reducer = { - actionType: FlexibleProvablePure; -}; - -type ReducerReturn = { - /** - * Dispatches an {@link Action}. Similar to normal {@link Event}s, - * {@link Action}s can be stored by archive nodes and later reduced within a {@link SmartContract} method - * to change the state of the contract accordingly - * - * ```ts - * this.reducer.dispatch(Field(1)); // emits one action - * ``` - * - * */ - dispatch(action: Action): void; - /** - * Reduces a list of {@link Action}s, similar to `Array.reduce()`. - * - * ```ts - * let pendingActions = this.reducer.getActions({ - * fromActionState: actionState, - * }); - * - * let newState = this.reducer.reduce( - * pendingActions, - * Field, // the state type - * (state: Field, _action: Field) => { - * return state.add(1); - * }, - * initialState // initial state - * ); - * ``` - * - */ - reduce( - actions: MerkleList>, - stateType: Provable, - reduce: (state: State, action: Action) => State, - initial: State, - options?: { - maxUpdatesWithActions?: number; - maxActionsPerUpdate?: number; - skipActionStatePrecondition?: boolean; - } - ): State; - /** - * Perform circuit logic for every {@link Action} in the list. - * - * This is a wrapper around {@link reduce} for when you don't need `state`. - */ - forEach( - actions: MerkleList>, - reduce: (action: Action) => void, - options?: { - maxUpdatesWithActions?: number; - maxActionsPerUpdate?: number; - skipActionStatePrecondition?: boolean; - } - ): void; - /** - * Fetches the list of previously emitted {@link Action}s by this {@link SmartContract}. - * ```ts - * let pendingActions = this.reducer.getActions({ - * fromActionState: actionState, - * }); - * ``` - * - * The final action state can be accessed on `pendingActions.hash`. - * ```ts - * let endActionState = pendingActions.hash; - * ``` - * - * If the optional `endActionState` is provided, the list of actions will be fetched up to that state. - * In that case, `pendingActions.hash` is guaranteed to equal `endActionState`. - */ - getActions({ - fromActionState, - endActionState, - }?: { - fromActionState?: Field; - endActionState?: Field; - }): MerkleList>; - /** - * Fetches the list of previously emitted {@link Action}s by zkapp {@link SmartContract}. - * ```ts - * let pendingActions = await zkapp.reducer.fetchActions({ - * fromActionState: actionState, - * }); - * ``` - */ - fetchActions({ - fromActionState, - endActionState, - }?: { - fromActionState?: Field; - endActionState?: Field; - }): Promise; -}; - -function getReducer(contract: SmartContract): ReducerReturn { - let reducer: Reducer = ((contract as any)._ ??= {}).reducer; - if (reducer === undefined) - throw Error( - 'You are trying to use a reducer without having declared its type.\n' + - `Make sure to add a property \`reducer\` on ${contract.constructor.name}, for example: -class ${contract.constructor.name} extends SmartContract { - reducer = { actionType: Field }; -}` - ); - return { - dispatch(action: A) { - let accountUpdate = contract.self; - let eventFields = reducer.actionType.toFields(action); - accountUpdate.body.actions = Actions.pushEvent( - accountUpdate.body.actions, - eventFields - ); - }, - - reduce( - actionLists: MerkleList>, - stateType: Provable, - reduce: (state: S, action: A) => S, - state: S, - { - maxUpdatesWithActions = 32, - maxActionsPerUpdate = 1, - skipActionStatePrecondition = false, - } = {} - ): S { - Provable.asProver(() => { - if (actionLists.data.get().length > maxUpdatesWithActions) { - throw Error( - `reducer.reduce: Exceeded the maximum number of lists of actions, ${maxUpdatesWithActions}. - Use the optional \`maxUpdatesWithActions\` argument to increase this number.` - ); - } - }); - - if (!skipActionStatePrecondition) { - // the actionList.hash is the hash of all actions in that list, appended to the previous hash (the previous list of historical actions) - // this must equal one of the action states as preconditions to build a chain to that we only use actions that were dispatched between the current on chain action state and the initialActionState - contract.account.actionState.requireEquals(actionLists.hash); - } - - const listIter = actionLists.startIterating(); - - for (let i = 0; i < maxUpdatesWithActions; i++) { - let { element: merkleActions, isDummy } = listIter.Unsafe.next(); - let actionIter = merkleActions.startIterating(); - let newState = state; - - if (maxActionsPerUpdate === 1) { - // special case with less work, because the only action is a dummy iff merkleActions is a dummy - let action = Provable.witness( - reducer.actionType, - () => - actionIter.data.get()[0]?.element ?? - actionIter.innerProvable.empty() - ); - let emptyHash = actionIter.Constructor.emptyHash; - let finalHash = actionIter.nextHash(emptyHash, action); - finalHash = Provable.if(isDummy, emptyHash, finalHash); - - // note: this asserts nothing in the isDummy case, because `actionIter.hash` is not well-defined - // but it doesn't matter because we're also skipping all state and action state updates in that case - actionIter.hash.assertEquals(finalHash); - - newState = reduce(newState, action); - } else { - for (let j = 0; j < maxActionsPerUpdate; j++) { - let { element: action, isDummy } = actionIter.Unsafe.next(); - newState = Provable.if( - isDummy, - stateType, - newState, - reduce(newState, action) - ); - } - // note: this asserts nothing about the iterated actions if `MerkleActions` is a dummy - // which doesn't matter because we're also skipping all state and action state updates in that case - actionIter.assertAtEnd(); - } - - state = Provable.if(isDummy, stateType, state, newState); - } - - // important: we check that by iterating, we actually reached the claimed final action state - listIter.assertAtEnd(); - - return state; - }, - - forEach( - actionLists: MerkleList>, - callback: (action: A) => void, - config - ) { - const stateType = provable(null); - this.reduce( - actionLists, - stateType, - (_, action) => { - callback(action); - return null; - }, - null, - config - ); - }, - - getActions(config?: { - fromActionState?: Field; - endActionState?: Field; - }): MerkleList> { - const Action = reducer.actionType; - const emptyHash = Actions.empty().hash; - const nextHash = (hash: Field, action: A) => - Actions.pushEvent({ hash, data: [] }, Action.toFields(action)).hash; - - class ActionList extends MerkleList.create( - Action as unknown as ProvableHashable, - nextHash, - emptyHash - ) {} - - class MerkleActions extends MerkleList.create( - ActionList.provable, - (hash: Field, actions: ActionList) => - Actions.updateSequenceState(hash, actions.hash), - // if no "start" action hash was specified, this means we are fetching the entire history of actions, which started from the empty action state hash - // otherwise we are only fetching a part of the history, which starts at `fromActionState` - // TODO does this show that `emptyHash` should be part of the instance, not the class? that would make the provable representation bigger though - config?.fromActionState ?? Actions.emptyActionState() - ) {} - - let actions = Provable.witness(MerkleActions.provable, () => { - let actionFields = Mina.getActions( - contract.address, - config, - contract.tokenId - ); - // convert string-Fields back into the original action type - let actions = actionFields.map((event) => - event.actions.map((action) => - (reducer.actionType as ProvablePure).fromFields( - action.map(Field) - ) - ) - ); - return MerkleActions.from( - actions.map((a) => ActionList.fromReverse(a)) - ); - }); - // note that we don't have to assert anything about the initial action state here, - // because it is taken directly and not witnessed - if (config?.endActionState !== undefined) { - actions.hash.assertEquals(config.endActionState); - } - return actions; - }, - - async fetchActions(config?: { - fromActionState?: Field; - endActionState?: Field; - }): Promise { - let result = await Mina.fetchActions( - contract.address, - config, - contract.tokenId - ); - if ('error' in result) { - throw Error(JSON.stringify(result)); - } - return result.map((event) => - // putting our string-Fields back into the original action type - event.actions.map((action) => - (reducer.actionType as ProvablePure).fromFields(action.map(Field)) - ) - ); - }, - }; -} - function selfAccountUpdate(zkapp: SmartContract, methodName?: string) { let body = Body.keepAll(zkapp.address, zkapp.tokenId); let update = new (AccountUpdate as any)(body, {}, true) as AccountUpdate; @@ -1510,23 +1225,6 @@ function declareMethods( } } -const Reducer: (< - T extends FlexibleProvablePure, - A extends InferProvable = InferProvable ->(reducer: { - actionType: T; -}) => ReducerReturn) & { - initialActionState: Field; -} = Object.defineProperty( - function (reducer: any) { - // we lie about the return value here, and instead overwrite this.reducer with - // a getter, so we can get access to `this` inside functions on this.reducer (see constructor) - return reducer; - }, - 'initialActionState', - { get: Actions.emptyActionState } -) as any; - const ProofAuthorization = { setKind( { body, id }: AccountUpdate, From 941639d8549734f73aa29397da545dbd25b2bc0e Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 2 May 2024 10:54:42 +0200 Subject: [PATCH 02/97] remove `provable` reexport --- src/bindings | 2 +- src/index.ts | 6 +++++- src/lib/mina/account-update.ts | 3 +-- src/lib/mina/actions/reducer.ts | 2 +- src/lib/mina/zkapp.ts | 6 +----- src/lib/proof-system/zkprogram.ts | 3 +-- src/lib/provable/gadgets/elliptic-curve.ts | 2 +- src/lib/provable/string.ts | 3 ++- src/lib/provable/test/arithmetic.unit-test.ts | 2 +- src/lib/provable/test/struct.unit-test.ts | 3 ++- src/lib/provable/test/test-utils.ts | 2 +- src/lib/provable/types/struct.ts | 2 -- src/lib/testing/equivalent.ts | 2 +- 13 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/bindings b/src/bindings index 51933ebdcc..bbeda07b4b 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 51933ebdcc57604f3ba621c10a7e148b0587ab25 +Subproject commit bbeda07b4b2bd1d31592b8758c74f404dc58f34d diff --git a/src/index.ts b/src/index.ts index 85ac939a9e..0909b3ac92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,7 +32,11 @@ export type { FlexibleProvablePure, InferProvable, } from './lib/provable/types/struct.js'; -export { provable, provablePure, Struct } from './lib/provable/types/struct.js'; +export { + provable, + provablePure, +} from './lib/provable/types/provable-derivers.js'; +export { Struct } from './lib/provable/types/struct.js'; export { Unconstrained } from './lib/provable/types/unconstrained.js'; export { Provable } from './lib/provable/provable.js'; export { diff --git a/src/lib/mina/account-update.ts b/src/lib/mina/account-update.ts index cfb21532da..d3f539ad52 100644 --- a/src/lib/mina/account-update.ts +++ b/src/lib/mina/account-update.ts @@ -1,10 +1,9 @@ import { cloneCircuitValue, FlexibleProvable, - provable, - provablePure, StructNoJson, } from '../provable/types/struct.js'; +import { provable, provablePure } from '../provable/types/provable-derivers.js'; import { memoizationContext, memoizeWitness, diff --git a/src/lib/mina/actions/reducer.ts b/src/lib/mina/actions/reducer.ts index 47ead7d9b7..fab297dab7 100644 --- a/src/lib/mina/actions/reducer.ts +++ b/src/lib/mina/actions/reducer.ts @@ -3,8 +3,8 @@ import { Actions } from '../account-update.js'; import { FlexibleProvablePure, InferProvable, - provable, } from '../../provable/types/struct.js'; +import { provable } from '../../provable/types/provable-derivers.js'; import { Provable } from '../../provable/provable.js'; import { ProvableHashable } from '../../provable/crypto/poseidon.js'; import * as Mina from '../mina.js'; diff --git a/src/lib/mina/zkapp.ts b/src/lib/mina/zkapp.ts index be1a1ed9af..fbfd5ff0e6 100644 --- a/src/lib/mina/zkapp.ts +++ b/src/lib/mina/zkapp.ts @@ -7,7 +7,6 @@ import { Body, Events, Permissions, - Actions, TokenId, ZkappCommand, zkAppProver, @@ -20,8 +19,6 @@ import { import { cloneCircuitValue, FlexibleProvablePure, - InferProvable, - provable, } from '../provable/types/struct.js'; import { Provable, @@ -32,7 +29,6 @@ import * as Encoding from '../../bindings/lib/encoding.js'; import { HashInput, Poseidon, - ProvableHashable, hashConstant, isHashable, packToFields, @@ -47,7 +43,6 @@ import { analyzeMethod, compileProgram, Empty, - emptyValue, getPreviousProofsForProver, methodArgumentsToConstant, methodArgumentTypesAndValues, @@ -75,6 +70,7 @@ import { import { assertPromise } from '../util/assert.js'; import { ProvablePure } from '../provable/types/provable-intf.js'; import { getReducer, Reducer } from './actions/reducer.js'; +import { provable } from '../provable/types/provable-derivers.js'; // external API export { SmartContract, method, DeployArgs, declareMethods }; diff --git a/src/lib/proof-system/zkprogram.ts b/src/lib/proof-system/zkprogram.ts index 43857b9cb2..7ecbd7ebd2 100644 --- a/src/lib/proof-system/zkprogram.ts +++ b/src/lib/proof-system/zkprogram.ts @@ -18,9 +18,8 @@ import { InferProvable, ProvablePureExtended, Struct, - provable, - provablePure, } from '../provable/types/struct.js'; +import { provable, provablePure } from '../provable/types/provable-derivers.js'; import { Provable } from '../provable/provable.js'; import { assert, prettifyStacktracePromise } from '../util/errors.js'; import { snarkContext } from '../provable/core/provable-context.js'; diff --git a/src/lib/provable/gadgets/elliptic-curve.ts b/src/lib/provable/gadgets/elliptic-curve.ts index 5265cbff47..c8db5b8e03 100644 --- a/src/lib/provable/gadgets/elliptic-curve.ts +++ b/src/lib/provable/gadgets/elliptic-curve.ts @@ -15,7 +15,7 @@ import { affineDouble, } from '../../../bindings/crypto/elliptic-curve.js'; import { Bool } from '../bool.js'; -import { provable } from '../types/struct.js'; +import { provable } from '../types/provable-derivers.js'; import { assertPositiveInteger } from '../../../bindings/crypto/non-negative.js'; import { arrayGet, assertNotVectorEquals } from './basic.js'; import { sliceField3 } from './bit-slices.js'; diff --git a/src/lib/provable/string.ts b/src/lib/provable/string.ts index 87c085573e..2a52e33cdf 100644 --- a/src/lib/provable/string.ts +++ b/src/lib/provable/string.ts @@ -2,7 +2,8 @@ import { Bool, Field } from './wrapped.js'; import { Provable } from './provable.js'; import { Poseidon } from './crypto/poseidon.js'; import { Gadgets } from './gadgets/gadgets.js'; -import { Struct, provable } from './types/struct.js'; +import { Struct } from './types/struct.js'; +import { provable } from './types/provable-derivers.js'; export { Character, CircuitString }; diff --git a/src/lib/provable/test/arithmetic.unit-test.ts b/src/lib/provable/test/arithmetic.unit-test.ts index 579dce8236..ec02667fc3 100644 --- a/src/lib/provable/test/arithmetic.unit-test.ts +++ b/src/lib/provable/test/arithmetic.unit-test.ts @@ -7,7 +7,7 @@ import { } from '../../testing/equivalent.js'; import { Field } from '../wrapped.js'; import { Gadgets } from '../gadgets/gadgets.js'; -import { provable } from '../types/struct.js'; +import { provable } from '../types/provable-derivers.js'; import { assert } from '../gadgets/common.js'; let Arithmetic = ZkProgram({ diff --git a/src/lib/provable/test/struct.unit-test.ts b/src/lib/provable/test/struct.unit-test.ts index ab48c0dddf..5fae21d659 100644 --- a/src/lib/provable/test/struct.unit-test.ts +++ b/src/lib/provable/test/struct.unit-test.ts @@ -1,4 +1,5 @@ -import { provable, Struct } from '../types/struct.js'; +import { Struct } from '../types/struct.js'; +import { provable } from '../types/provable-derivers.js'; import { Unconstrained } from '../types/unconstrained.js'; import { UInt32 } from '../int.js'; import { PrivateKey, PublicKey } from '../crypto/signature.js'; diff --git a/src/lib/provable/test/test-utils.ts b/src/lib/provable/test/test-utils.ts index 208fdccc68..6ff8535330 100644 --- a/src/lib/provable/test/test-utils.ts +++ b/src/lib/provable/test/test-utils.ts @@ -6,7 +6,7 @@ import { assert } from '../gadgets/common.js'; import { Bytes } from '../wrapped-classes.js'; import { CurveAffine } from '../../../bindings/crypto/elliptic-curve.js'; import { simpleMapToCurve } from '../gadgets/elliptic-curve.js'; -import { provable } from '../types/struct.js'; +import { provable } from '../types/provable-derivers.js'; export { foreignField, diff --git a/src/lib/provable/types/struct.ts b/src/lib/provable/types/struct.ts index c251509bb5..6afe43b326 100644 --- a/src/lib/provable/types/struct.ts +++ b/src/lib/provable/types/struct.ts @@ -21,8 +21,6 @@ import { From, InferValue } from '../../../bindings/lib/provable-generic.js'; export { ProvableExtended, ProvablePureExtended, - provable, - provablePure, Struct, FlexibleProvable, FlexibleProvablePure, diff --git a/src/lib/testing/equivalent.ts b/src/lib/testing/equivalent.ts index 745532b000..58eeefc9fb 100644 --- a/src/lib/testing/equivalent.ts +++ b/src/lib/testing/equivalent.ts @@ -6,7 +6,7 @@ import { Provable } from '../provable/provable.js'; import { deepEqual } from 'node:assert/strict'; import { Bool, Field } from '../provable/wrapped.js'; import { AnyFunction, Tuple } from '../util/types.js'; -import { provable } from '../provable/types/struct.js'; +import { provable } from '../provable/types/provable-derivers.js'; import { assert } from '../provable/gadgets/common.js'; import { synchronousRunners } from '../provable/core/provable-context.js'; From 00cbfda15c59fc2f102bea79d62f96c387cc0ed8 Mon Sep 17 00:00:00 2001 From: i Date: Thu, 2 May 2024 11:13:29 +0200 Subject: [PATCH 03/97] fix --- src/mina-signer/mina-signer.ts | 5 ++--- src/mina-signer/src/rosetta.ts | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/mina-signer/mina-signer.ts b/src/mina-signer/mina-signer.ts index b5de51d847..d6b9793a43 100644 --- a/src/mina-signer/mina-signer.ts +++ b/src/mina-signer/mina-signer.ts @@ -460,9 +460,8 @@ class Client { * @param privateKey The private key used to sign the transaction * @returns A string with the resulting payload for /construction/combine. */ - rosettaCombinePayload(signingPayload: string, privateKey: Json.PrivateKey) { - let parsedPayload = JSON.parse(signingPayload); - return JSON.stringify(Rosetta.rosettaCombinePayload(parsedPayload, privateKey, this.network)); + rosettaCombinePayload(signingPayload: Rosetta.UnsignedPayload, privateKey: Json.PrivateKey) { + return Rosetta.rosettaCombinePayload(signingPayload, privateKey, this.network); } /** diff --git a/src/mina-signer/src/rosetta.ts b/src/mina-signer/src/rosetta.ts index 012b23fcb1..cdd82bc4f0 100644 --- a/src/mina-signer/src/rosetta.ts +++ b/src/mina-signer/src/rosetta.ts @@ -192,7 +192,7 @@ function rosettaCombinePayload( network: NetworkId ) { let signature = signTransaction( - unsignedPayload.unsigned_transaction, + JSON.parse(unsignedPayload.unsigned_transaction), privateKey, network ); @@ -282,7 +282,7 @@ function rosettaTransactionToSignedCommand({ } type UnsignedPayload = { - unsigned_transaction: UnsignedTransaction; + unsigned_transaction: string; payloads: unknown[]; }; From e3e758b5e8fbd2e8a38b6a4fbf1b2896841ccca6 Mon Sep 17 00:00:00 2001 From: i Date: Thu, 2 May 2024 11:15:08 +0200 Subject: [PATCH 04/97] update version --- src/mina-signer/package-lock.json | 4 ++-- src/mina-signer/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mina-signer/package-lock.json b/src/mina-signer/package-lock.json index ce9e20b7ed..ce6ed4b280 100644 --- a/src/mina-signer/package-lock.json +++ b/src/mina-signer/package-lock.json @@ -1,12 +1,12 @@ { "name": "mina-signer", - "version": "3.0.6", + "version": "3.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mina-signer", - "version": "3.0.6", + "version": "3.0.7", "license": "Apache-2.0", "dependencies": { "blakejs": "^1.2.1", diff --git a/src/mina-signer/package.json b/src/mina-signer/package.json index 39013917fd..a3fadbd44b 100644 --- a/src/mina-signer/package.json +++ b/src/mina-signer/package.json @@ -1,7 +1,7 @@ { "name": "mina-signer", "description": "Node API for signing transactions on various networks for Mina Protocol", - "version": "3.0.6", + "version": "3.0.7", "type": "module", "scripts": { "build": "tsc -p ../../tsconfig.mina-signer.json", From 693f06b1f83c55529048da73489df53543b2eaa5 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 2 May 2024 13:07:45 +0200 Subject: [PATCH 05/97] for each method on merkle list --- src/lib/provable/merkle-list.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/lib/provable/merkle-list.ts b/src/lib/provable/merkle-list.ts index 6871573682..bb1173837e 100644 --- a/src/lib/provable/merkle-list.ts +++ b/src/lib/provable/merkle-list.ts @@ -191,6 +191,27 @@ class MerkleList implements MerkleListBase { return new this.Constructor({ hash: this.hash, data }); } + /** + * Iterate through the list in a fixed number of steps any apply a given callback on each element. + * + * Proves that the iteration traverses the entire list. + * Once past the last element, dummy elements will be passed to the callback. + * + * Note: There are no guarantees about the contents of dummy elements, so the callback is expected + * to handle the `isDummy` flag separately. + */ + forEach( + length: number, + callback: (element: T, isDummy: Bool, i: number) => void + ) { + let iter = this.startIterating(); + for (let i = 0; i < length; i++) { + let { element, isDummy } = iter.Unsafe.next(); + callback(element, isDummy, i); + } + iter.assertAtEnd(); + } + startIterating(): MerkleListIterator { let merkleArray = MerkleListIterator.createFromList(this.Constructor); return merkleArray.startIterating(this); From bf24822739dd546bf5190c522c57f146fa87db02 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 2 May 2024 13:23:26 +0200 Subject: [PATCH 06/97] scaffold out merkle map rollup --- src/lib/mina/actions/merkle-map-rollup.ts | 117 ++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/lib/mina/actions/merkle-map-rollup.ts diff --git a/src/lib/mina/actions/merkle-map-rollup.ts b/src/lib/mina/actions/merkle-map-rollup.ts new file mode 100644 index 0000000000..6416632f06 --- /dev/null +++ b/src/lib/mina/actions/merkle-map-rollup.ts @@ -0,0 +1,117 @@ +import { ZkProgram } from '../../proof-system/zkprogram.js'; +import { Field, Bool } from '../../provable/wrapped.js'; +import { Unconstrained } from '../../provable/types/unconstrained.js'; +import { MerkleList, MerkleListIterator } from '../../provable/merkle-list.js'; +import { Actions } from '../../../bindings/mina-transaction/transaction-leaves.js'; +import { MerkleTree } from '../../provable/merkle-tree.js'; +import { Struct } from '../../provable/types/struct.js'; +import { SelfProof } from '../../proof-system/zkprogram.js'; +import { Provable } from '../../provable/provable.js'; + +// our action type +class MerkleLeaf extends Struct({ key: Field, value: Field }) {} + +class ActionList extends MerkleList.create( + MerkleLeaf, + (hash: Field, action: MerkleLeaf) => + Actions.pushEvent({ hash, data: [] }, MerkleLeaf.toFields(action)).hash, + Actions.empty().hash +) {} + +class ActionIterator extends MerkleListIterator.create( + ActionList.provable, + (hash: Field, actions: ActionList) => + Actions.updateSequenceState(hash, actions.hash), + // we don't have to care about the initial hash here because we will just step forward + Actions.emptyActionState() +) {} + +class MerkleMapState extends Struct({ + // this should just be a MerkleTree type that carries the full tree as aux data + merkleRoot: Field, + // TODO: make zkprogram support auxiliary data in public inputs + // actionState: ActionIterator.provable, + actionState: Field, +}) {} + +/** + * This function represents a proof that we can go from MerkleMapState A -> B + * One call of `merkleUpdateBatch()` either + * - creates an initial proof A -> B (this is the `isRecursive: false` case) + * - or, takes an existing proof A -> B, adds its own logic to prove B -> B', so that the output is a proof A -> B' + */ +const merkleUpdateBatch = + (maxUpdatesPerBatch: number, maxActionsPerUpdate: number) => + async ( + stateA: MerkleMapState, + actions: ActionIterator, + tree: Unconstrained, + isRecursive: Bool, + recursiveProof: SelfProof + ): Promise => { + // in the non-recursive case, this skips verifying the proof so we can pass in a dummy proof + recursiveProof.verifyIf(isRecursive); + + // in the recursive case, the recursive proof's initial state has to match this proof's initial state + stateA = Provable.if( + isRecursive, + MerkleMapState, + stateA, + recursiveProof.publicInput + ); + Provable.assertEqual(MerkleMapState, recursiveProof.publicInput, stateA); + + // the state we start with + let stateB = Provable.if( + isRecursive, + MerkleMapState, + recursiveProof.publicOutput, + stateA + ); + // this would be unnecessary if the iterator could just be the public input + actions.currentHash.assertEquals(stateB.actionState); + let root = stateB.merkleRoot; + + // update merkle root for each action + for (let i = 0; i < maxUpdatesPerBatch; i++) { + let actionsList = actions.next(); + + actionsList.forEach(maxActionsPerUpdate, ({ key, value }, isDummy) => { + // TODO update root, using key, value and the tree + }); + } + + return { + merkleRoot: root, + actionState: actions.currentHash, + }; + }; + +function MerkleMapRollup({ + maxUpdatesPerBatch = 10, + maxActionsPerUpdate = 5, +} = {}) { + let merkleMapRollup = ZkProgram({ + name: 'merkle-map-rollup', + publicInput: MerkleMapState, + publicOutput: MerkleMapState, + methods: { + nextBatch: { + privateInputs: [ + // the actions to process + ActionIterator.provable, + // the merkle tree to update + Unconstrained.provable, + // flag to set whether this is a recursive call + Bool, + // recursive proof for A -> B + SelfProof, + ], + method: merkleUpdateBatch(maxUpdatesPerBatch, maxActionsPerUpdate), + }, + }, + }); + + // TODO: convenient interface for doing the entire chain of proofs + return merkleMapRollup; +} From 1f2bf0d12e30d028c6b9933b34ffcd106ab6d516 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 2 May 2024 13:32:05 +0200 Subject: [PATCH 07/97] some code golf --- src/lib/mina/actions/merkle-map-rollup.ts | 39 ++++++++++++----------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/lib/mina/actions/merkle-map-rollup.ts b/src/lib/mina/actions/merkle-map-rollup.ts index 6416632f06..7b5fe11cf0 100644 --- a/src/lib/mina/actions/merkle-map-rollup.ts +++ b/src/lib/mina/actions/merkle-map-rollup.ts @@ -7,6 +7,7 @@ import { MerkleTree } from '../../provable/merkle-tree.js'; import { Struct } from '../../provable/types/struct.js'; import { SelfProof } from '../../proof-system/zkprogram.js'; import { Provable } from '../../provable/provable.js'; +import { AnyTuple } from '../../util/types.js'; // our action type class MerkleLeaf extends Struct({ key: Field, value: Field }) {} @@ -40,15 +41,28 @@ class MerkleMapState extends Struct({ * - creates an initial proof A -> B (this is the `isRecursive: false` case) * - or, takes an existing proof A -> B, adds its own logic to prove B -> B', so that the output is a proof A -> B' */ -const merkleUpdateBatch = - (maxUpdatesPerBatch: number, maxActionsPerUpdate: number) => - async ( +const merkleUpdateBatch = ( + maxUpdatesPerBatch: number, + maxActionsPerUpdate: number +) => ({ + privateInputs: [ + // the actions to process + ActionIterator.provable, + // the merkle tree to update + Unconstrained.provable, + // flag to set whether this is a recursive call + Bool, + // recursive proof for A -> B + SelfProof, + ] satisfies AnyTuple, + + async method( stateA: MerkleMapState, actions: ActionIterator, tree: Unconstrained, isRecursive: Bool, recursiveProof: SelfProof - ): Promise => { + ): Promise { // in the non-recursive case, this skips verifying the proof so we can pass in a dummy proof recursiveProof.verifyIf(isRecursive); @@ -85,7 +99,8 @@ const merkleUpdateBatch = merkleRoot: root, actionState: actions.currentHash, }; - }; + }, +}); function MerkleMapRollup({ maxUpdatesPerBatch = 10, @@ -96,19 +111,7 @@ function MerkleMapRollup({ publicInput: MerkleMapState, publicOutput: MerkleMapState, methods: { - nextBatch: { - privateInputs: [ - // the actions to process - ActionIterator.provable, - // the merkle tree to update - Unconstrained.provable, - // flag to set whether this is a recursive call - Bool, - // recursive proof for A -> B - SelfProof, - ], - method: merkleUpdateBatch(maxUpdatesPerBatch, maxActionsPerUpdate), - }, + nextBatch: merkleUpdateBatch(maxUpdatesPerBatch, maxActionsPerUpdate), }, }); From bd676ce2d8b0dc8da1e40a6153c006b7c41279e4 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 2 May 2024 14:10:57 +0200 Subject: [PATCH 08/97] cloneable merkle tree --- src/lib/provable/merkle-tree.ts | 13 ++++++++++++- src/lib/provable/types/struct.ts | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/lib/provable/merkle-tree.ts b/src/lib/provable/merkle-tree.ts index d818d9af6e..708a412e0b 100644 --- a/src/lib/provable/merkle-tree.ts +++ b/src/lib/provable/merkle-tree.ts @@ -42,6 +42,17 @@ class MerkleTree { } } + /** + * Return a new MerkleTree with the same contents as this one. + */ + clone() { + let newTree = new MerkleTree(this.height); + for (let [level, nodes] of Object.entries(this.nodes)) { + newTree.nodes[level as any as number] = { ...nodes }; + } + return newTree; + } + /** * Returns a node which lives at a given index and level. * @param level Level of the node. @@ -149,7 +160,7 @@ class MerkleTree { } /** - * The {@link BaseMerkleWitness} class defines a circuit-compatible base class for [Merkle Witness'](https://computersciencewiki.org/index.php/Merkle_proof). + * The {@link BaseMerkleWitness} class defines a circuit-compatible base class for [Merkle Witness](https://computersciencewiki.org/index.php/Merkle_proof). */ class BaseMerkleWitness extends CircuitValue { static height: number; diff --git a/src/lib/provable/types/struct.ts b/src/lib/provable/types/struct.ts index 6afe43b326..ef192a5154 100644 --- a/src/lib/provable/types/struct.ts +++ b/src/lib/provable/types/struct.ts @@ -293,6 +293,9 @@ function cloneCircuitValue(obj: T): T { if (obj.constructor !== undefined && 'clone' in obj.constructor) { return (obj as any).constructor.clone(obj); } + if ('clone' in obj && typeof obj.clone === 'function') { + return (obj as any).clone(obj); + } // built-in JS datatypes with custom cloning strategies if (Array.isArray(obj)) return obj.map(cloneCircuitValue) as any as T; From d29d490a06eae52ed3e5afeb1636120573c9ed86 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 2 May 2024 15:35:09 +0200 Subject: [PATCH 09/97] get leaf of merkle tree --- src/lib/provable/merkle-tree.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib/provable/merkle-tree.ts b/src/lib/provable/merkle-tree.ts index 708a412e0b..090d244471 100644 --- a/src/lib/provable/merkle-tree.ts +++ b/src/lib/provable/merkle-tree.ts @@ -63,6 +63,15 @@ class MerkleTree { return this.nodes[level]?.[index.toString()] ?? this.zeroes[level]; } + /** + * Returns a leaf at a given index. + * @param index Index of the leaf. + * @returns The data of the leaf. + */ + getLeaf(key: bigint) { + return this.getNode(0, key); + } + /** * Returns the root of the [Merkle Tree](https://en.wikipedia.org/wiki/Merkle_tree). * @returns The root of the Merkle Tree. From d9c382404d49e1a60869662a5f220e8369404574 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 2 May 2024 15:35:52 +0200 Subject: [PATCH 10/97] finish v0 of merkle map rollup --- src/lib/mina/actions/merkle-map-rollup.ts | 123 +++++++++++++++++++--- 1 file changed, 111 insertions(+), 12 deletions(-) diff --git a/src/lib/mina/actions/merkle-map-rollup.ts b/src/lib/mina/actions/merkle-map-rollup.ts index 7b5fe11cf0..5bd77b2d8c 100644 --- a/src/lib/mina/actions/merkle-map-rollup.ts +++ b/src/lib/mina/actions/merkle-map-rollup.ts @@ -3,11 +3,14 @@ import { Field, Bool } from '../../provable/wrapped.js'; import { Unconstrained } from '../../provable/types/unconstrained.js'; import { MerkleList, MerkleListIterator } from '../../provable/merkle-list.js'; import { Actions } from '../../../bindings/mina-transaction/transaction-leaves.js'; -import { MerkleTree } from '../../provable/merkle-tree.js'; +import { MerkleTree, MerkleWitness } from '../../provable/merkle-tree.js'; import { Struct } from '../../provable/types/struct.js'; import { SelfProof } from '../../proof-system/zkprogram.js'; import { Provable } from '../../provable/provable.js'; import { AnyTuple } from '../../util/types.js'; +import { assert } from '../../provable/gadgets/common.js'; + +export { MerkleMapRollup }; // our action type class MerkleLeaf extends Struct({ key: Field, value: Field }) {} @@ -29,12 +32,17 @@ class ActionIterator extends MerkleListIterator.create( class MerkleMapState extends Struct({ // this should just be a MerkleTree type that carries the full tree as aux data - merkleRoot: Field, + root: Field, // TODO: make zkprogram support auxiliary data in public inputs // actionState: ActionIterator.provable, actionState: Field, }) {} +const TREE_HEIGHT = 256; +class MerkleMapWitness extends MerkleWitness(TREE_HEIGHT) {} + +// TODO: it would be nice to abstract the logic for proving a chain of state transition proofs + /** * This function represents a proof that we can go from MerkleMapState A -> B * One call of `merkleUpdateBatch()` either @@ -84,21 +92,43 @@ const merkleUpdateBatch = ( ); // this would be unnecessary if the iterator could just be the public input actions.currentHash.assertEquals(stateB.actionState); - let root = stateB.merkleRoot; + let root = stateB.root; // update merkle root for each action for (let i = 0; i < maxUpdatesPerBatch; i++) { - let actionsList = actions.next(); + actions.next().forEach(maxActionsPerUpdate, ({ key, value }, isDummy) => { + // merkle witness + let witness = Provable.witness( + MerkleMapWitness, + () => new MerkleMapWitness(tree.get().getWitness(key.toBigInt())) + ); + + // previous value at the key + let previousValue = Provable.witness(Field, () => + tree.get().getLeaf(key.toBigInt()) + ); + + // prove that the witness is correct, by comparing the implied root and key + // note: this just works if the (key, value) is a (0,0) dummy, because the value at the 0 key will always be 0 + witness.calculateIndex().assertEquals(key); + witness.calculateRoot(previousValue).assertEquals(root); + + // store new value in at the key + let newRoot = witness.calculateRoot(value); - actionsList.forEach(maxActionsPerUpdate, ({ key, value }, isDummy) => { - // TODO update root, using key, value and the tree + // update the tree, outside the circuit (this should all be part of a better merkle tree API) + Provable.asProver(() => { + // ignore dummy value + if (isDummy.toBoolean()) return; + tree.get().setLeaf(key.toBigInt(), value); + }); + + // update root + root = Provable.if(isDummy, root, newRoot); }); } - return { - merkleRoot: root, - actionState: actions.currentHash, - }; + return { root, actionState: actions.currentHash }; }, }); @@ -115,6 +145,75 @@ function MerkleMapRollup({ }, }); - // TODO: convenient interface for doing the entire chain of proofs - return merkleMapRollup; + let MerkleRollupProof = ZkProgram.Proof(merkleMapRollup); + + let isCompiled = true; + + return { + async compile() { + if (isCompiled) return; + let result = await merkleMapRollup.compile(); + isCompiled = true; + return result; + }, + + async prove(actions: MerkleList>, tree: MerkleTree) { + assert(tree.height === TREE_HEIGHT, 'Tree height must match'); + await this.compile(); + + let n = actions.data.get().length; + let nBatches = Math.ceil(n / maxUpdatesPerBatch); + + // if there are no actions, we still need to create a valid proof for the empty transition + if (n === 0) nBatches = 1; + + // input state + let iterator = actions.startIterating(); + + let inputState = new MerkleMapState({ + root: tree.getRoot(), + actionState: iterator.currentHash, + }); + + // dummy proof + console.time('dummy'); + let dummyState = MerkleMapState.empty(); + let dummy = await MerkleRollupProof.dummy(dummyState, dummyState, 1); + console.timeEnd('dummy'); + + // base proof + console.time('batch 0'); + let proof = await merkleMapRollup.nextBatch( + inputState, + iterator, + Unconstrained.from(tree), + Bool(false), + dummy + ); + console.timeEnd('batch 0'); + + // recursive proofs + for (let i = 1; i < nBatches; i++) { + // update iterator (would be nice if the method call would just return the updated one) + iterator.currentHash = proof.publicOutput.actionState; + for (let j = 0; j < maxUpdatesPerBatch; j++) { + iterator._updateIndex('next'); + } + + console.time(`batch ${i}`); + proof = await merkleMapRollup.nextBatch( + inputState, + iterator, + Unconstrained.from(tree), + Bool(true), + proof + ); + console.timeEnd(`batch ${i}`); + } + + return proof; + }, + + program: merkleMapRollup, + }; } From c325b99ca55b0daaea5cf61ac952c553c59af94f Mon Sep 17 00:00:00 2001 From: rpanic Date: Thu, 2 May 2024 16:45:03 +0200 Subject: [PATCH 11/97] Fixed bug when using sideloaded keys in SmartContracts --- src/index.ts | 1 + src/lib/proof-system/sideloaded.unit-test.ts | 17 ++++++++++++++++- src/lib/proof-system/zkprogram.ts | 1 + src/lib/provable/types/struct.ts | 4 ++-- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 42c4614ef6..7678fe5dcc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -69,6 +69,7 @@ export { state, State, declareState } from './lib/mina/state.js'; export type { JsonProof } from './lib/proof-system/zkprogram.js'; export { + type ProofBase, Proof, DynamicProof, SelfProof, diff --git a/src/lib/proof-system/sideloaded.unit-test.ts b/src/lib/proof-system/sideloaded.unit-test.ts index 5f397b443d..88bb40fca8 100644 --- a/src/lib/proof-system/sideloaded.unit-test.ts +++ b/src/lib/proof-system/sideloaded.unit-test.ts @@ -5,7 +5,7 @@ import { Void, ZkProgram, } from './zkprogram.js'; -import { Field, Struct } from '../../index.js'; +import { Field, SmartContract, Struct, method } from '../../index.js'; import { it, describe, before } from 'node:test'; import { expect } from 'expect'; @@ -124,6 +124,17 @@ const sideloadedProgram2 = ZkProgram({ }, }); +export class SideloadedSmartContract extends SmartContract { + @method async setValue( + value: Field, + proof: SampleSideloadedProof, + vk: VerificationKey + ) { + proof.verify(vk); + proof.publicInput.assertEquals(value); + } +} + describe('sideloaded', async () => { let program1Vk = (await program1.compile()).verificationKey; let program2Vk = (await program2.compile()).verificationKey; @@ -211,4 +222,8 @@ describe('sideloaded', async () => { expect(tag1).not.toStrictEqual(tag2); }); + + it('should compile with SmartContracts', async () => { + await SideloadedSmartContract.compile(); + }); }); diff --git a/src/lib/proof-system/zkprogram.ts b/src/lib/proof-system/zkprogram.ts index 43857b9cb2..d834e38d82 100644 --- a/src/lib/proof-system/zkprogram.ts +++ b/src/lib/proof-system/zkprogram.ts @@ -44,6 +44,7 @@ import { prefixes } from '../../bindings/crypto/constants.js'; // public API export { + ProofBase, Proof, DynamicProof, SelfProof, diff --git a/src/lib/provable/types/struct.ts b/src/lib/provable/types/struct.ts index c251509bb5..9cce8f932e 100644 --- a/src/lib/provable/types/struct.ts +++ b/src/lib/provable/types/struct.ts @@ -13,7 +13,7 @@ import type { IsPure, } from './provable-derivers.js'; import { Provable } from '../provable.js'; -import { Proof } from '../../proof-system/zkprogram.js'; +import { DynamicProof, Proof } from '../../proof-system/zkprogram.js'; import { ProvablePure } from './provable-intf.js'; import { From, InferValue } from '../../../bindings/lib/provable-generic.js'; @@ -310,7 +310,7 @@ function cloneCircuitValue(obj: T): T { if (isPrimitive(obj)) { return obj; } - if (obj instanceof Proof) { + if (obj instanceof Proof || obj instanceof DynamicProof) { return obj; } From 50f55a011219f885dc13d49a41393ab40bef98af Mon Sep 17 00:00:00 2001 From: i Date: Thu, 2 May 2024 16:59:02 +0200 Subject: [PATCH 12/97] fix tests --- src/mina-signer/tests/rosetta.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mina-signer/tests/rosetta.test.ts b/src/mina-signer/tests/rosetta.test.ts index 2527f526ea..338746882c 100644 --- a/src/mina-signer/tests/rosetta.test.ts +++ b/src/mina-signer/tests/rosetta.test.ts @@ -101,7 +101,7 @@ describe('Rosetta', () => { }); it('generates valid combine payload', () => { - const combinePayload = client.rosettaCombinePayload(JSON.stringify(rosettaUnsignedPayload), privateKey); + const combinePayload = client.rosettaCombinePayload(rosettaUnsignedPayload, privateKey); const expectedCombinePayload = { network_identifier: { blockchain: 'mina', network: 'mainnet' }, unsigned_transaction: rosettaUnsignedTxn, @@ -118,6 +118,6 @@ describe('Rosetta', () => { } ] }; - expect(combinePayload).toBe(JSON.stringify(expectedCombinePayload)); + expect(combinePayload).toBe(expectedCombinePayload); }); }); From c5700ee9ad4dc4362ecf8ba3d9c18781d4c863e5 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 2 May 2024 19:13:03 +0200 Subject: [PATCH 13/97] add outside proof logic --- src/lib/mina/actions/merkle-map-rollup.ts | 32 +++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/lib/mina/actions/merkle-map-rollup.ts b/src/lib/mina/actions/merkle-map-rollup.ts index 5bd77b2d8c..0aaee2a2b8 100644 --- a/src/lib/mina/actions/merkle-map-rollup.ts +++ b/src/lib/mina/actions/merkle-map-rollup.ts @@ -157,7 +157,7 @@ function MerkleMapRollup({ return result; }, - async prove(actions: MerkleList>, tree: MerkleTree) { + async prove(tree: MerkleTree, actions: MerkleList>) { assert(tree.height === TREE_HEIGHT, 'Tree height must match'); await this.compile(); @@ -211,9 +211,37 @@ function MerkleMapRollup({ console.timeEnd(`batch ${i}`); } - return proof; + // do the same updates outside the circuit + tree = merkleUpdateOutside(actions, tree, { + maxUpdatesPerBatch, + maxActionsPerUpdate, + }); + + return { proof, tree }; }, program: merkleMapRollup, }; } + +// very annoying: we have to repeat the merkle updates outside the circuit + +function merkleUpdateOutside( + actions: MerkleList>, + tree: MerkleTree, + { maxUpdatesPerBatch = 10, maxActionsPerUpdate = 5 } = {} +) { + tree = tree.clone(); + + actions.forEach(maxUpdatesPerBatch, (actionsList, isDummy) => { + if (isDummy.toBoolean()) return; + + actionsList.forEach(maxActionsPerUpdate, ({ key, value }, isDummy) => { + if (isDummy.toBoolean()) return; + + tree.setLeaf(key.toBigInt(), value); + }); + }); + + return tree; +} From a1439c3577b80c53539ec6c8cde89d3fcf8ec7d1 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 11:09:12 +0200 Subject: [PATCH 14/97] some comments with efficiency ideas --- src/lib/mina/actions/merkle-map-rollup.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/lib/mina/actions/merkle-map-rollup.ts b/src/lib/mina/actions/merkle-map-rollup.ts index 0aaee2a2b8..5c2d03b9c8 100644 --- a/src/lib/mina/actions/merkle-map-rollup.ts +++ b/src/lib/mina/actions/merkle-map-rollup.ts @@ -13,6 +13,11 @@ import { assert } from '../../provable/gadgets/common.js'; export { MerkleMapRollup }; // our action type +// TODO downside of not including the full state data in an action is that we have to mess with events or another data source separately +// TODO we could store the full data, and the [key, value] as the _last two field elements_. then we could prove a custom poseidon hash +// TODO where you can provide a prehash of the full data, and only hash the last two elements in provable code +// TOOD -- proving that the key and value are correct, but saving the work of hashing the full data +// TODO and then the size of the full data could be up to 100 elements class MerkleLeaf extends Struct({ key: Field, value: Field }) {} class ActionList extends MerkleList.create( @@ -94,6 +99,8 @@ const merkleUpdateBatch = ( actions.currentHash.assertEquals(stateB.actionState); let root = stateB.root; + // TODO: would be more efficient to linearize the actions first and then iterate over them, + // so we don't do the merkle lookup `maxActionsPerUpdate` times every time // update merkle root for each action for (let i = 0; i < maxUpdatesPerBatch; i++) { actions.next().forEach(maxActionsPerUpdate, ({ key, value }, isDummy) => { From 5c45a7d6380b775c7890cf5dc5e49fa42fdd03da Mon Sep 17 00:00:00 2001 From: i Date: Fri, 3 May 2024 11:24:35 +0200 Subject: [PATCH 15/97] fix test --- src/mina-signer/tests/rosetta.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mina-signer/tests/rosetta.test.ts b/src/mina-signer/tests/rosetta.test.ts index 338746882c..2c298c7583 100644 --- a/src/mina-signer/tests/rosetta.test.ts +++ b/src/mina-signer/tests/rosetta.test.ts @@ -8,7 +8,7 @@ describe('Rosetta', () => { const rosettaUnsignedTxn: UnsignedTransaction = { "randomOracleInput": "0000000333E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E000002570242F000000000008000000000000000C00000007FFFFFFFC00000000000000000000000000000000000000000000000000000000000000000000E0000000000000000014D677000000000", "signerInput": { "prefix": ["33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E", "33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E", "33E1F14C6155B706D4EA12CF70685B8DCD3342A8B36A27CC3EB61B5871F9219E"], "suffix": ["0000000000000007FFFFFFFC00000006000000000000000200000000001E8480", "0000000003800000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000000000000000000001DCD65000000000"] }, "payment": { "to": "B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X", "from": "B62qqQomCgjaKhayN79wWqDNsSJKFaZjrkuCp8Kcrt36ubXb14XHU2X", "fee": "1000000", "token": "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf", "nonce": "1", "memo": null, "amount": "1000000000", "valid_until": null }, "stakeDelegation": null }; const rosettaUnsignedPayload = { - unsigned_transaction: rosettaUnsignedTxn, + unsigned_transaction: JSON.stringify(rosettaUnsignedTxn), payloads: [ { account_identifier: { From 8fc54e0f4dd000262f41f0d4603e769cdcbacbe3 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 12:17:35 +0200 Subject: [PATCH 16/97] serialization and custom efficient hashing --- .../actions/offchain-state-serialization.ts | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/lib/mina/actions/offchain-state-serialization.ts diff --git a/src/lib/mina/actions/offchain-state-serialization.ts b/src/lib/mina/actions/offchain-state-serialization.ts new file mode 100644 index 0000000000..a85b47c9a2 --- /dev/null +++ b/src/lib/mina/actions/offchain-state-serialization.ts @@ -0,0 +1,81 @@ +/** + * This defines a custom way to serialize various kinds of offchain state into an action. + * + * There is a special trick of including Merkle map (keyHash, valueHash) pairs _at the end_ of each action. + * Thanks to the properties of Poseidon, this enables us to compute the action hash cheaply + * if we only need to prove that (key, value) are part of it. + */ + +import { ProvablePure } from 'src/lib/provable/types/provable-intf.js'; +import { + Poseidon, + ProvableHashable, + hashWithPrefix, +} from '../../provable/crypto/poseidon.js'; +import { Field } from '../../provable/wrapped.js'; +import { assert } from '../../provable/gadgets/common.js'; +import { prefixes } from '../../../bindings/crypto/constants.js'; + +export { fromAction, toAction, pushActionCustom, merkleLeafFromAction }; + +type Action = [...Field[], Field, Field]; +type Actionable = ProvableHashable & ProvablePure; + +function toAction( + keyType: Actionable, + valueType: Actionable, + key: K, + value: V +): Action { + let combinedSize = keyType.sizeInFields() + valueType.sizeInFields(); + let padding = combinedSize % 2 === 0 ? [] : [Field(0)]; + + let keyHash = Poseidon.hashPacked(keyType, key); + let valueHash = Poseidon.hashPacked(valueType, value); + return [ + ...keyType.toFields(key), + ...valueType.toFields(value), + ...padding, + keyHash, + valueHash, + ]; +} + +function fromAction( + keyType: Actionable, + valueType: Actionable, + action: Action +): { key: K; value: V } { + let keySize = keyType.sizeInFields(); + let valueSize = valueType.sizeInFields(); + let paddingSize = (keySize + valueSize) % 2 === 0 ? 0 : 1; + assert( + action.length === keySize + valueSize + paddingSize + 2, + 'invalid action size' + ); + + let key = keyType.fromFields(action.slice(0, keySize)); + keyType.check(key); + + let value = valueType.fromFields(action.slice(keySize, keySize + valueSize)); + valueType.check(value); + + return { key, value }; +} + +/** + * A custom method to hash an action which only hashes the keyHash and valueHash inside the circuit. + * Therefore, it only proves that the keyHash and valueHash are part of the action, and nothing about + * the rest of the action. + */ +function pushActionCustom(actionsHash: Field, action: Action) { + let eventHash = hashWithPrefix(prefixes.event, action); + let hash = hashWithPrefix(prefixes.sequenceEvents, [actionsHash, eventHash]); + return hash; +} + +function merkleLeafFromAction(action: Action) { + assert(action.length >= 2, 'invalid action size'); + let [key, value] = action.slice(-2); + return { key, value }; +} From f5256c7c51864d1af16ddef76090d90bfb3830ba Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 14:09:00 +0200 Subject: [PATCH 17/97] finish merkle leaf serialization --- src/lib/mina/actions/merkle-map-rollup.ts | 16 +--- .../actions/offchain-state-serialization.ts | 91 ++++++++++++++++--- 2 files changed, 79 insertions(+), 28 deletions(-) diff --git a/src/lib/mina/actions/merkle-map-rollup.ts b/src/lib/mina/actions/merkle-map-rollup.ts index 5c2d03b9c8..900a590406 100644 --- a/src/lib/mina/actions/merkle-map-rollup.ts +++ b/src/lib/mina/actions/merkle-map-rollup.ts @@ -9,24 +9,10 @@ import { SelfProof } from '../../proof-system/zkprogram.js'; import { Provable } from '../../provable/provable.js'; import { AnyTuple } from '../../util/types.js'; import { assert } from '../../provable/gadgets/common.js'; +import { ActionList, MerkleLeaf } from './offchain-state-serialization.js'; export { MerkleMapRollup }; -// our action type -// TODO downside of not including the full state data in an action is that we have to mess with events or another data source separately -// TODO we could store the full data, and the [key, value] as the _last two field elements_. then we could prove a custom poseidon hash -// TODO where you can provide a prehash of the full data, and only hash the last two elements in provable code -// TOOD -- proving that the key and value are correct, but saving the work of hashing the full data -// TODO and then the size of the full data could be up to 100 elements -class MerkleLeaf extends Struct({ key: Field, value: Field }) {} - -class ActionList extends MerkleList.create( - MerkleLeaf, - (hash: Field, action: MerkleLeaf) => - Actions.pushEvent({ hash, data: [] }, MerkleLeaf.toFields(action)).hash, - Actions.empty().hash -) {} - class ActionIterator extends MerkleListIterator.create( ActionList.provable, (hash: Field, actions: ActionList) => diff --git a/src/lib/mina/actions/offchain-state-serialization.ts b/src/lib/mina/actions/offchain-state-serialization.ts index a85b47c9a2..488b849e83 100644 --- a/src/lib/mina/actions/offchain-state-serialization.ts +++ b/src/lib/mina/actions/offchain-state-serialization.ts @@ -6,17 +6,25 @@ * if we only need to prove that (key, value) are part of it. */ -import { ProvablePure } from 'src/lib/provable/types/provable-intf.js'; +import { ProvablePure } from '../../provable/types/provable-intf.js'; import { Poseidon, ProvableHashable, hashWithPrefix, + salt, } from '../../provable/crypto/poseidon.js'; import { Field } from '../../provable/wrapped.js'; import { assert } from '../../provable/gadgets/common.js'; import { prefixes } from '../../../bindings/crypto/constants.js'; +import { Struct } from '../../provable/types/struct.js'; +import { Unconstrained } from '../../provable/types/unconstrained.js'; +import { MerkleList } from '../../provable/merkle-list.js'; +import * as Mina from '../mina.js'; +import { PublicKey } from '../../provable/crypto/signature.js'; +import { Provable } from 'src/lib/provable/provable.js'; +import { Actions } from '../account-update.js'; -export { fromAction, toAction, pushActionCustom, merkleLeafFromAction }; +export { fromAction, toAction, MerkleLeaf, ActionList, fetchMerkleLeaves }; type Action = [...Field[], Field, Field]; type Actionable = ProvableHashable & ProvablePure; @@ -64,18 +72,75 @@ function fromAction( } /** - * A custom method to hash an action which only hashes the keyHash and valueHash inside the circuit. - * Therefore, it only proves that the keyHash and valueHash are part of the action, and nothing about - * the rest of the action. + * This represents a custom kind of action which includes a Merkle map key and value in its serialization, + * and doesn't represent the rest of the action's field elements in provable code. */ -function pushActionCustom(actionsHash: Field, action: Action) { - let eventHash = hashWithPrefix(prefixes.event, action); - let hash = hashWithPrefix(prefixes.sequenceEvents, [actionsHash, eventHash]); - return hash; +class MerkleLeaf extends Struct({ + key: Field, + value: Field, + prefix: Unconstrained.provable as Provable>, +}) { + static fromAction(action: Field[]) { + assert(action.length >= 2, 'invalid action size'); + let [key, value] = action.slice(-2); + let prefix = Unconstrained.from(action.slice(0, -2)); + return new MerkleLeaf({ key, value, prefix }); + } + + /** + * A custom method to hash an action which only hashes the key and value in provable code. + * Therefore, it only proves that the key and value are part of the action, and nothing about + * the rest of the action. + */ + static hash(action: MerkleLeaf) { + let preHashState = Provable.witnessFields(3, () => { + let prefix = action.prefix.get(); + let init = salt(prefixes.event) as [Field, Field, Field]; + return Poseidon.update(init, prefix); + }); + return Poseidon.update(preHashState, [action.key, action.value])[0]; + } } -function merkleLeafFromAction(action: Action) { - assert(action.length >= 2, 'invalid action size'); - let [key, value] = action.slice(-2); - return { key, value }; +function pushAction(actionsHash: Field, action: MerkleLeaf) { + return hashWithPrefix(prefixes.sequenceEvents, [ + actionsHash, + MerkleLeaf.hash(action), + ]); +} + +class ActionList extends MerkleList.create( + MerkleLeaf, + pushAction, + Actions.empty().hash +) {} + +async function fetchMerkleLeaves( + contract: { address: PublicKey; tokenId: Field }, + config?: { + fromActionState?: Field; + endActionState?: Field; + } +): Promise>> { + class MerkleActions extends MerkleList.create( + ActionList.provable, + (hash: Field, actions: ActionList) => + Actions.updateSequenceState(hash, actions.hash), + // if no "start" action hash was specified, this means we are fetching the entire history of actions, which started from the empty action state hash + // otherwise we are only fetching a part of the history, which starts at `fromActionState` + config?.fromActionState ?? Actions.emptyActionState() + ) {} + + let result = await Mina.fetchActions( + contract.address, + config, + contract.tokenId + ); + if ('error' in result) throw Error(JSON.stringify(result)); + + // convert string-Fields back into the original action type + let merkleLeafs = result.map((event) => + event.actions.map((action) => MerkleLeaf.fromAction(action.map(Field))) + ); + return MerkleActions.from(merkleLeafs.map((a) => ActionList.fromReverse(a))); } From 5a6604c3dadb4992dbcedf5cd3d18a03f81dcf94 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 14:25:23 +0200 Subject: [PATCH 18/97] rename --- ...map-rollup.ts => offchain-state-rollup.ts} | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) rename src/lib/mina/actions/{merkle-map-rollup.ts => offchain-state-rollup.ts} (94%) diff --git a/src/lib/mina/actions/merkle-map-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts similarity index 94% rename from src/lib/mina/actions/merkle-map-rollup.ts rename to src/lib/mina/actions/offchain-state-rollup.ts index 900a590406..dd78d2a16f 100644 --- a/src/lib/mina/actions/merkle-map-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -11,7 +11,7 @@ import { AnyTuple } from '../../util/types.js'; import { assert } from '../../provable/gadgets/common.js'; import { ActionList, MerkleLeaf } from './offchain-state-serialization.js'; -export { MerkleMapRollup }; +export { OffchainStateRollup }; class ActionIterator extends MerkleListIterator.create( ActionList.provable, @@ -125,11 +125,11 @@ const merkleUpdateBatch = ( }, }); -function MerkleMapRollup({ +function OffchainStateRollup({ maxUpdatesPerBatch = 10, maxActionsPerUpdate = 5, } = {}) { - let merkleMapRollup = ZkProgram({ + let offchainStateRollup = ZkProgram({ name: 'merkle-map-rollup', publicInput: MerkleMapState, publicOutput: MerkleMapState, @@ -138,14 +138,16 @@ function MerkleMapRollup({ }, }); - let MerkleRollupProof = ZkProgram.Proof(merkleMapRollup); + let RollupProof = ZkProgram.Proof(offchainStateRollup); let isCompiled = true; return { + Proof: RollupProof, + async compile() { if (isCompiled) return; - let result = await merkleMapRollup.compile(); + let result = await offchainStateRollup.compile(); isCompiled = true; return result; }, @@ -171,12 +173,12 @@ function MerkleMapRollup({ // dummy proof console.time('dummy'); let dummyState = MerkleMapState.empty(); - let dummy = await MerkleRollupProof.dummy(dummyState, dummyState, 1); + let dummy = await RollupProof.dummy(dummyState, dummyState, 1); console.timeEnd('dummy'); // base proof console.time('batch 0'); - let proof = await merkleMapRollup.nextBatch( + let proof = await offchainStateRollup.nextBatch( inputState, iterator, Unconstrained.from(tree), @@ -194,7 +196,7 @@ function MerkleMapRollup({ } console.time(`batch ${i}`); - proof = await merkleMapRollup.nextBatch( + proof = await offchainStateRollup.nextBatch( inputState, iterator, Unconstrained.from(tree), @@ -213,7 +215,7 @@ function MerkleMapRollup({ return { proof, tree }; }, - program: merkleMapRollup, + program: offchainStateRollup, }; } From 5b6d1497cf8a8f8cb680f2773a92ef070db550ed Mon Sep 17 00:00:00 2001 From: i Date: Fri, 3 May 2024 15:00:58 +0200 Subject: [PATCH 19/97] fix test --- src/mina-signer/tests/rosetta.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mina-signer/tests/rosetta.test.ts b/src/mina-signer/tests/rosetta.test.ts index 2c298c7583..8b8d691822 100644 --- a/src/mina-signer/tests/rosetta.test.ts +++ b/src/mina-signer/tests/rosetta.test.ts @@ -104,7 +104,7 @@ describe('Rosetta', () => { const combinePayload = client.rosettaCombinePayload(rosettaUnsignedPayload, privateKey); const expectedCombinePayload = { network_identifier: { blockchain: 'mina', network: 'mainnet' }, - unsigned_transaction: rosettaUnsignedTxn, + unsigned_transaction: JSON.stringify(rosettaUnsignedTxn), signatures: [ { hex_bytes: mainnetSignatureHex, @@ -118,6 +118,6 @@ describe('Rosetta', () => { } ] }; - expect(combinePayload).toBe(expectedCombinePayload); + expect(JSON.stringify(combinePayload)).toBe(JSON.stringify(expectedCombinePayload)); }); }); From 23d926dc87af75710790b399b6c4f7d7de57b37f Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 15:06:12 +0200 Subject: [PATCH 20/97] start typing out stuff --- .../actions/offchain-contract.unit-test.ts | 16 ++++++ .../actions/offchain-state-serialization.ts | 11 +++- src/lib/mina/actions/offchain-state.ts | 54 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/lib/mina/actions/offchain-contract.unit-test.ts create mode 100644 src/lib/mina/actions/offchain-state.ts diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts new file mode 100644 index 0000000000..5e9083298a --- /dev/null +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -0,0 +1,16 @@ +import { OffchainState } from './offchain-state.js'; +import { PublicKey } from '../../provable/crypto/signature.js'; +import { UInt64 } from '../../provable/int.js'; + +const state = OffchainState({ + accounts: OffchainState.Map(PublicKey, UInt64), + totalSupply: OffchainState.Field(UInt64), +}); + +let pk = PublicKey.empty(); + +state.fields.totalSupply.set(UInt64.from(100)); +state.fields.accounts.set(pk, UInt64.from(50)); + +let supply = await state.fields.totalSupply.get(); +let balance = await state.fields.accounts.get(pk); diff --git a/src/lib/mina/actions/offchain-state-serialization.ts b/src/lib/mina/actions/offchain-state-serialization.ts index 488b849e83..867db66fda 100644 --- a/src/lib/mina/actions/offchain-state-serialization.ts +++ b/src/lib/mina/actions/offchain-state-serialization.ts @@ -21,10 +21,17 @@ import { Unconstrained } from '../../provable/types/unconstrained.js'; import { MerkleList } from '../../provable/merkle-list.js'; import * as Mina from '../mina.js'; import { PublicKey } from '../../provable/crypto/signature.js'; -import { Provable } from 'src/lib/provable/provable.js'; +import { Provable } from '../../provable/provable.js'; import { Actions } from '../account-update.js'; -export { fromAction, toAction, MerkleLeaf, ActionList, fetchMerkleLeaves }; +export { + fromAction, + toAction, + MerkleLeaf, + ActionList, + fetchMerkleLeaves, + Actionable, +}; type Action = [...Field[], Field, Field]; type Actionable = ProvableHashable & ProvablePure; diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts new file mode 100644 index 0000000000..d77af43965 --- /dev/null +++ b/src/lib/mina/actions/offchain-state.ts @@ -0,0 +1,54 @@ +import { InferProvable } from '../../provable/types/struct.js'; +import { Actionable } from './offchain-state-serialization.js'; + +export { OffchainState }; + +type Any = Actionable; + +function OffchainField(type: T) { + return { kind: 'offchain-field' as const, type }; +} +type OffchainField = { + get(): Promise; + set(value: T): void; +}; + +function OffchainMap(key: K, value: V) { + return { kind: 'offchain-map' as const, keyType: key, valueType: value }; +} +type OffchainMap = { + get(key: K): Promise; + set(key: K, value: V): void; +}; + +type OffchainStateKind = + | { kind: 'offchain-field'; type: Any } + | { kind: 'offchain-map'; keyType: Any; valueType: Any }; + +type OffchainStateIntf = Kind extends { + kind: 'offchain-field'; + type: infer T; +} + ? OffchainField> + : Kind extends { + kind: 'offchain-map'; + keyType: infer K; + valueType: infer V; + } + ? OffchainMap, InferProvable> + : never; + +function OffchainState< + const Config extends { [key: string]: OffchainStateKind } +>( + config: Config +): { + readonly fields: { + [K in keyof Config]: OffchainStateIntf; + }; +} { + throw new Error('Not implemented'); +} + +OffchainState.Map = OffchainMap; +OffchainState.Field = OffchainField; From f39e013b50e7ddb6ff2ca2a9f72b356dce1fb5f9 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 16:05:01 +0200 Subject: [PATCH 21/97] nice option type --- src/lib/provable/option.ts | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/lib/provable/option.ts diff --git a/src/lib/provable/option.ts b/src/lib/provable/option.ts new file mode 100644 index 0000000000..eae3416560 --- /dev/null +++ b/src/lib/provable/option.ts @@ -0,0 +1,45 @@ +import { emptyValue } from '../proof-system/zkprogram.js'; +import { Provable } from './provable.js'; +import { Struct } from './types/struct.js'; +import { Bool } from './wrapped.js'; + +export { Option }; + +type Option = { isSome: Bool; value: T } & { + orElse(defaultValue: T | V): T; +}; + +function Option( + type: Provable +): Provable< + Option, + // TODO make V | undefined the value type + { isSome: boolean; value: V } +> & { + from(value?: T): Option; +} { + const Super = Struct({ isSome: Bool, value: type }); + return class Option_ extends Super { + orElse(defaultValue: T | V): T { + return Provable.if( + this.isSome, + type, + this.value, + type.fromValue(defaultValue) + ); + } + + static from(value?: V | T) { + return value === undefined + ? new Option_({ isSome: Bool(false), value: emptyValue(type) }) + : new Option_({ isSome: Bool(true), value: type.fromValue(value) }); + } + + static fromFields(fields: any[], aux?: any): Option_ { + return new Option_(Super.fromFields(fields, aux)); + } + static fromValue(value: { isSome: boolean | Bool; value: V | T }) { + return new Option_(Super.fromValue(value)); + } + }; +} From ca2dc19dcf7a6118b61b37d2acaf5c72571f5412 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 16:05:21 +0200 Subject: [PATCH 22/97] more api scaffolding --- src/lib/mina/actions/offchain-state-rollup.ts | 2 +- src/lib/mina/actions/offchain-state.ts | 103 ++++++++++++++---- 2 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index dd78d2a16f..ee22a18d74 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -11,7 +11,7 @@ import { AnyTuple } from '../../util/types.js'; import { assert } from '../../provable/gadgets/common.js'; import { ActionList, MerkleLeaf } from './offchain-state-serialization.js'; -export { OffchainStateRollup }; +export { OffchainStateRollup, MerkleMapState }; class ActionIterator extends MerkleListIterator.create( ActionList.provable, diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index d77af43965..169f4fd7a4 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -1,24 +1,98 @@ import { InferProvable } from '../../provable/types/struct.js'; import { Actionable } from './offchain-state-serialization.js'; +import { PublicKey } from '../../provable/crypto/signature.js'; +import { Field } from '../../provable/field.js'; +import { Proof } from '../../proof-system/zkprogram.js'; +import { MerkleMapState } from './offchain-state-rollup.js'; +import { Option } from '../../provable/option.js'; +import { InferValue } from '../../../bindings/lib/provable-generic.js'; export { OffchainState }; +type OffchainState = { + /** + * The individual fields of the offchain state. + * + * ```ts + * const state = OffchainState({ totalSupply: OffchainState.Field(UInt64) }); + * + * state.fields.totalSupply.set(UInt64.from(100)); + * + * let supply = await state.fields.totalSupply.get(); + * ``` + */ + readonly fields: { + [K in keyof Config]: OffchainStateIntf; + }; + + /** + * Set the contract that this offchain state is connected with. + * + * Note: This declares two _onchain_ state fields on the contract, + * which it uses to keep commitments to the offchain state and processed actions. + * + * This means that the contract has only 6 remaining onchain state fields available. + * + * It also sets the reducer for this contract, so you can't use another reducer with this contract. + */ + setContract(contract: Contract): void; + + /** + * Create a proof that the offchain state is in a valid state. + */ + createSettlementProof(): Promise>; + + /** + * The custom proof class for state settlement proofs, that have to be passed into the settling method. + */ + Proof: typeof Proof; + + /** + * Settle the offchain state. + */ + settle(proof: Proof): Promise; +}; + +function OffchainState< + const Config extends { [key: string]: OffchainStateKind } +>(config: Config): OffchainState { + throw new Error('Not implemented'); +} + +OffchainState.Map = OffchainMap; +OffchainState.Field = OffchainField; + +// helpers + type Any = Actionable; +type Contract = { address: PublicKey; tokenId: Field }; function OffchainField(type: T) { return { kind: 'offchain-field' as const, type }; } -type OffchainField = { +type OffchainField = { + /** + * Get the value of the field. + */ get(): Promise; - set(value: T): void; + /** + * Set the value of the field. + */ + set(value: T | TValue): void; }; function OffchainMap(key: K, value: V) { return { kind: 'offchain-map' as const, keyType: key, valueType: value }; } -type OffchainMap = { - get(key: K): Promise; - set(key: K, value: V): void; +type OffchainMap = { + /** + * Get the value for this key, or none if it doesn't exist. + */ + get(key: K): Promise>; + /** + * Set the value for this key. + */ + set(key: K, value: V | VValue): void; }; type OffchainStateKind = @@ -29,26 +103,11 @@ type OffchainStateIntf = Kind extends { kind: 'offchain-field'; type: infer T; } - ? OffchainField> + ? OffchainField, InferValue> : Kind extends { kind: 'offchain-map'; keyType: infer K; valueType: infer V; } - ? OffchainMap, InferProvable> + ? OffchainMap, InferProvable, InferValue> : never; - -function OffchainState< - const Config extends { [key: string]: OffchainStateKind } ->( - config: Config -): { - readonly fields: { - [K in keyof Config]: OffchainStateIntf; - }; -} { - throw new Error('Not implemented'); -} - -OffchainState.Map = OffchainMap; -OffchainState.Field = OffchainField; From 6021181bbb7ce192a4fb77087984e2d5278f11a6 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 16:23:11 +0200 Subject: [PATCH 23/97] example offchain state interaction --- .../actions/offchain-contract.unit-test.ts | 107 +++++++++++++++++- src/lib/mina/actions/offchain-state.ts | 19 +++- 2 files changed, 119 insertions(+), 7 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 5e9083298a..7d24300ff7 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -1,16 +1,113 @@ import { OffchainState } from './offchain-state.js'; import { PublicKey } from '../../provable/crypto/signature.js'; import { UInt64 } from '../../provable/int.js'; +import { SmartContract, method } from '../zkapp.js'; +import { assert } from '../../provable/gadgets/common.js'; +import { AccountUpdate, Mina } from '../../../index.js'; const state = OffchainState({ accounts: OffchainState.Map(PublicKey, UInt64), totalSupply: OffchainState.Field(UInt64), }); -let pk = PublicKey.empty(); +class StateProof extends state.Proof {} -state.fields.totalSupply.set(UInt64.from(100)); -state.fields.accounts.set(pk, UInt64.from(50)); +// example contract that interacts with offchain state -let supply = await state.fields.totalSupply.get(); -let balance = await state.fields.accounts.get(pk); +class ExampleContract extends SmartContract { + @method + async createAccount(address: PublicKey, amountToMint: UInt64) { + state.fields.accounts.set(address, amountToMint); + + let totalSupply = await state.fields.totalSupply.get(); + state.fields.totalSupply.set(totalSupply.add(amountToMint)); + } + + @method + async transfer(from: PublicKey, to: PublicKey, amount: UInt64) { + let fromOption = await state.fields.accounts.get(from); + assert(fromOption.isSome, 'sender account exists'); + + let toBalance = (await state.fields.accounts.get(to)).orElse(0n); + + // (this also checks that the sender has enough balance) + state.fields.accounts.set(from, fromOption.value.sub(amount)); + state.fields.accounts.set(to, toBalance.add(amount)); + } + + @method.returns(UInt64) + async getSupply() { + return await state.fields.totalSupply.get(); + } + + @method.returns(UInt64) + async getBalance(address: PublicKey) { + return (await state.fields.accounts.get(address)).orElse(0n); + } + + @method + async settle(proof: StateProof) { + await state.settle(proof); + } +} +state.setContractClass(ExampleContract); + +// test code below + +// setup + +const Local = await Mina.LocalBlockchain(); +Mina.setActiveInstance(Local); + +let [sender, receiver, contractAccount] = Local.testAccounts; +let contract = new ExampleContract(contractAccount); +state.setContractAccount(contract); + +await ExampleContract.compile(); + +// deploy and create first account + +await Mina.transaction(sender, async () => { + AccountUpdate.fundNewAccount(sender); + await contract.deploy(); + await contract.createAccount(sender, UInt64.from(1000)); +}) + .sign([sender.key]) + .prove() + .send(); + +// settle + +let proof = await state.createSettlementProof(); +await Mina.transaction(sender, async () => { + await contract.settle(proof); +}) + .sign([sender.key]) + .prove() + .send(); + +// transfer + +await Mina.transaction(sender, async () => { + await contract.transfer(sender, receiver, UInt64.from(100)); +}) + .sign([sender.key]) + .prove() + .send(); + +// settle + +proof = await state.createSettlementProof(); +await Mina.transaction(sender, async () => { + await contract.settle(proof); +}) + .sign([sender.key]) + .prove() + .send(); + +// check balance and supply +let balance = await contract.getBalance(receiver); +let supply = await contract.getSupply(); + +console.log('balance', balance.toString()); +console.log('supply', supply.toString()); diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 169f4fd7a4..4e58ecb2e6 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -6,6 +6,7 @@ import { Proof } from '../../proof-system/zkprogram.js'; import { MerkleMapState } from './offchain-state-rollup.js'; import { Option } from '../../provable/option.js'; import { InferValue } from '../../../bindings/lib/provable-generic.js'; +import { SmartContract } from '../zkapp.js'; export { OffchainState }; @@ -26,7 +27,7 @@ type OffchainState = { }; /** - * Set the contract that this offchain state is connected with. + * Set the contract class that this offchain state appliues to. * * Note: This declares two _onchain_ state fields on the contract, * which it uses to keep commitments to the offchain state and processed actions. @@ -35,7 +36,21 @@ type OffchainState = { * * It also sets the reducer for this contract, so you can't use another reducer with this contract. */ - setContract(contract: Contract): void; + setContractClass(contract: typeof SmartContract): void; + + /** + * Set the contract that this offchain state is connected with. + * + * This tells the offchain state about the account to fetch data from. + */ + setContractAccount(contract: Contract): void; + + /** + * Compile the offchain state ZkProgram. + * + * Note: If this is not done explicitly, it will be done before creating the first proof automatically. + */ + compile(): Promise; /** * Create a proof that the offchain state is in a valid state. From d442abf916c89ba7711352677229b5fbee03e4ce Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 16:23:26 +0200 Subject: [PATCH 24/97] bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index bbeda07b4b..03241cd44f 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit bbeda07b4b2bd1d31592b8758c74f404dc58f34d +Subproject commit 03241cd44f895249fc28dda7bd133479f2ef0de2 From cd1e402770515f292bc07523f17eff388c92f2fd Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 16:33:03 +0200 Subject: [PATCH 25/97] minor --- src/lib/mina/actions/offchain-contract.unit-test.ts | 8 ++++---- src/lib/provable/option.ts | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 7d24300ff7..a7ceaae9ec 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -2,7 +2,6 @@ import { OffchainState } from './offchain-state.js'; import { PublicKey } from '../../provable/crypto/signature.js'; import { UInt64 } from '../../provable/int.js'; import { SmartContract, method } from '../zkapp.js'; -import { assert } from '../../provable/gadgets/common.js'; import { AccountUpdate, Mina } from '../../../index.js'; const state = OffchainState({ @@ -26,12 +25,13 @@ class ExampleContract extends SmartContract { @method async transfer(from: PublicKey, to: PublicKey, amount: UInt64) { let fromOption = await state.fields.accounts.get(from); - assert(fromOption.isSome, 'sender account exists'); + let fromBalance = fromOption.assertSome('sender account exists'); - let toBalance = (await state.fields.accounts.get(to)).orElse(0n); + let toOption = await state.fields.accounts.get(to); + let toBalance = toOption.orElse(0n); // (this also checks that the sender has enough balance) - state.fields.accounts.set(from, fromOption.value.sub(amount)); + state.fields.accounts.set(from, fromBalance.sub(amount)); state.fields.accounts.set(to, toBalance.add(amount)); } diff --git a/src/lib/provable/option.ts b/src/lib/provable/option.ts index eae3416560..e207658271 100644 --- a/src/lib/provable/option.ts +++ b/src/lib/provable/option.ts @@ -6,6 +6,7 @@ import { Bool } from './wrapped.js'; export { Option }; type Option = { isSome: Bool; value: T } & { + assertSome(message?: string): T; orElse(defaultValue: T | V): T; }; @@ -29,6 +30,11 @@ function Option( ); } + assertSome(message?: string): T { + this.isSome.assertTrue(message); + return this.value; + } + static from(value?: V | T) { return value === undefined ? new Option_({ isSome: Bool(false), value: emptyValue(type) }) From 98fb7662fd0fc7bef98b741700d2cf9dc7e4839f Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 16:45:55 +0200 Subject: [PATCH 26/97] start implementation --- src/lib/mina/actions/offchain-state.ts | 74 ++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 4e58ecb2e6..b553e6acc2 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -3,10 +3,14 @@ import { Actionable } from './offchain-state-serialization.js'; import { PublicKey } from '../../provable/crypto/signature.js'; import { Field } from '../../provable/field.js'; import { Proof } from '../../proof-system/zkprogram.js'; -import { MerkleMapState } from './offchain-state-rollup.js'; +import { + MerkleMapState, + OffchainStateRollup, +} from './offchain-state-rollup.js'; import { Option } from '../../provable/option.js'; import { InferValue } from '../../../bindings/lib/provable-generic.js'; import { SmartContract } from '../zkapp.js'; +import { assert } from 'src/lib/provable/gadgets/common.js'; export { OffchainState }; @@ -71,13 +75,77 @@ type OffchainState = { function OffchainState< const Config extends { [key: string]: OffchainStateKind } >(config: Config): OffchainState { - throw new Error('Not implemented'); + // setup internal state of this "class" + let internal = { + _contract: undefined as Contract | undefined, + + get contract() { + assert( + internal._contract === undefined, + 'Must call `setContractAccount()` first' + ); + return internal._contract; + }, + }; + + const notImplemented = (): any => assert(false, 'Not implemented'); + + let rollup = OffchainStateRollup(); + + function field(index: number, type: Any) { + return { + get: notImplemented, + set: notImplemented, + }; + } + + function map(index: number, keyType: Any, valueType: Any) { + return { + get: notImplemented, + set: notImplemented, + }; + } + + return { + setContractClass(contract) { + notImplemented(); + }, + + setContractAccount(contract) { + internal._contract = contract; + }, + + async compile() { + await rollup.compile(); + }, + + async createSettlementProof() { + return notImplemented(); + }, + + Proof: rollup.Proof, + + async settle(proof) { + notImplemented(); + }, + + fields: Object.fromEntries( + Object.entries(config).map(([key, kind], i) => [ + key, + kind.kind === 'offchain-field' + ? field(i, kind.type) + : kind.kind === 'offchain-map' + ? map(i, kind.keyType, kind.valueType) + : notImplemented(), + ]) + ) as any, + }; } OffchainState.Map = OffchainMap; OffchainState.Field = OffchainField; -// helpers +// type helpers type Any = Actionable; type Contract = { address: PublicKey; tokenId: Field }; From 65645b30683075b2e055057897d348743546ef07 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 16:47:54 +0200 Subject: [PATCH 27/97] minor --- .../actions/offchain-contract.unit-test.ts | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index a7ceaae9ec..9c44646fe1 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -79,18 +79,24 @@ await Mina.transaction(sender, async () => { // settle let proof = await state.createSettlementProof(); -await Mina.transaction(sender, async () => { - await contract.settle(proof); -}) + +await Mina.transaction(sender, () => contract.settle(proof)) .sign([sender.key]) .prove() .send(); +// check balance and supply +let balance = await contract.getBalance(receiver); +let supply = await contract.getSupply(); + +console.log('balance', balance.toString()); +console.log('supply', supply.toString()); + // transfer -await Mina.transaction(sender, async () => { - await contract.transfer(sender, receiver, UInt64.from(100)); -}) +await Mina.transaction(sender, () => + contract.transfer(sender, receiver, UInt64.from(100)) +) .sign([sender.key]) .prove() .send(); @@ -98,16 +104,15 @@ await Mina.transaction(sender, async () => { // settle proof = await state.createSettlementProof(); -await Mina.transaction(sender, async () => { - await contract.settle(proof); -}) + +await Mina.transaction(sender, () => contract.settle(proof)) .sign([sender.key]) .prove() .send(); // check balance and supply -let balance = await contract.getBalance(receiver); -let supply = await contract.getSupply(); +let balance2 = await contract.getBalance(receiver); +let supply2 = await contract.getSupply(); -console.log('balance', balance.toString()); -console.log('supply', supply.toString()); +console.log('balance', balance2.toString()); +console.log('supply', supply2.toString()); From d19e3d0deb1b10e905c8c1eb0976540e8c3ee584 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 16:48:24 +0200 Subject: [PATCH 28/97] fix --- src/lib/mina/actions/offchain-state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index b553e6acc2..6f7962d922 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -10,7 +10,7 @@ import { import { Option } from '../../provable/option.js'; import { InferValue } from '../../../bindings/lib/provable-generic.js'; import { SmartContract } from '../zkapp.js'; -import { assert } from 'src/lib/provable/gadgets/common.js'; +import { assert } from '../../provable/gadgets/common.js'; export { OffchainState }; From ecbce96c68f685ea894e3e4a68c7112689253b2c Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 20:19:20 +0200 Subject: [PATCH 29/97] small fix --- src/lib/mina/actions/offchain-contract.unit-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 9c44646fe1..cc67015f6e 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -72,7 +72,7 @@ await Mina.transaction(sender, async () => { await contract.deploy(); await contract.createAccount(sender, UInt64.from(1000)); }) - .sign([sender.key]) + .sign([sender.key, contractAccount.key]) .prove() .send(); From 246d2d6ba16983223b4f84ab73f5ddb51a35f11c Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 20:27:57 +0200 Subject: [PATCH 30/97] small fixes --- src/lib/mina/actions/offchain-state-rollup.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index ee22a18d74..cf76e7c508 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -62,17 +62,24 @@ const merkleUpdateBatch = ( isRecursive: Bool, recursiveProof: SelfProof ): Promise { + // clone tree so we don't modify it + tree = Unconstrained.witness(() => tree.get().clone()); + // in the non-recursive case, this skips verifying the proof so we can pass in a dummy proof recursiveProof.verifyIf(isRecursive); // in the recursive case, the recursive proof's initial state has to match this proof's initial state - stateA = Provable.if( - isRecursive, + // TODO maybe a dedicated `assertEqualIf()` is more efficient and readable + Provable.assertEqual( MerkleMapState, - stateA, - recursiveProof.publicInput + recursiveProof.publicInput, + Provable.if( + isRecursive, + MerkleMapState, + stateA, + recursiveProof.publicInput + ) ); - Provable.assertEqual(MerkleMapState, recursiveProof.publicInput, stateA); // the state we start with let stateB = Provable.if( From 9018c931b94c623f69ba3bd3c8599e468eee9747 Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 3 May 2024 20:29:47 +0200 Subject: [PATCH 31/97] probably fixes --- src/lib/mina/actions/offchain-state-rollup.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index cf76e7c508..8092974e43 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -62,9 +62,6 @@ const merkleUpdateBatch = ( isRecursive: Bool, recursiveProof: SelfProof ): Promise { - // clone tree so we don't modify it - tree = Unconstrained.witness(() => tree.get().clone()); - // in the non-recursive case, this skips verifying the proof so we can pass in a dummy proof recursiveProof.verifyIf(isRecursive); @@ -162,6 +159,8 @@ function OffchainStateRollup({ async prove(tree: MerkleTree, actions: MerkleList>) { assert(tree.height === TREE_HEIGHT, 'Tree height must match'); await this.compile(); + // clone the tree so we don't modify the input + tree = tree.clone(); let n = actions.data.get().length; let nBatches = Math.ceil(n / maxUpdatesPerBatch); @@ -213,12 +212,6 @@ function OffchainStateRollup({ console.timeEnd(`batch ${i}`); } - // do the same updates outside the circuit - tree = merkleUpdateOutside(actions, tree, { - maxUpdatesPerBatch, - maxActionsPerUpdate, - }); - return { proof, tree }; }, @@ -226,7 +219,7 @@ function OffchainStateRollup({ }; } -// very annoying: we have to repeat the merkle updates outside the circuit +// TODO: do we have to repeat the merkle updates outside the circuit? function merkleUpdateOutside( actions: MerkleList>, From ebf3e4716caaadce77fc411d93219152faa70715 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 6 May 2024 12:16:04 +0200 Subject: [PATCH 32/97] + update API --- .../mina/actions/offchain-contract.unit-test.ts | 16 +++++++++++++--- src/lib/mina/actions/offchain-state.ts | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index cc67015f6e..808e6f4a3d 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -18,6 +18,8 @@ class ExampleContract extends SmartContract { async createAccount(address: PublicKey, amountToMint: UInt64) { state.fields.accounts.set(address, amountToMint); + // TODO `totalSupply` easily gets into a wrong state here on concurrent calls. + // and using `.update()` doesn't help either let totalSupply = await state.fields.totalSupply.get(); state.fields.totalSupply.set(totalSupply.add(amountToMint)); } @@ -30,9 +32,17 @@ class ExampleContract extends SmartContract { let toOption = await state.fields.accounts.get(to); let toBalance = toOption.orElse(0n); - // (this also checks that the sender has enough balance) - state.fields.accounts.set(from, fromBalance.sub(amount)); - state.fields.accounts.set(to, toBalance.add(amount)); + // TODO we use `update` here so that previous balances can't be overridden + // but this still includes a trivial double-spend opportunity, because the updates are not rejected atomically: + // if the `to` update gets accepted but the `from` update fails, it's a double-spend + state.fields.accounts.update(from, { + from: fromBalance, + to: fromBalance.sub(amount), + }); + state.fields.accounts.update(to, { + from: toBalance, + to: toBalance.add(amount), + }); } @method.returns(UInt64) diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 6f7962d922..d391b58d35 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -162,6 +162,13 @@ type OffchainField = { * Set the value of the field. */ set(value: T | TValue): void; + /** + * Update the value of the field, while requiring a specific previous value. + * + * If the previous value does not match, the update will not be applied. + * If no previous value is present, the `from` value is ignored and the update applied unconditionally. + */ + update(update: { from: T | TValue; to: T | TValue }): void; }; function OffchainMap(key: K, value: V) { @@ -176,6 +183,13 @@ type OffchainMap = { * Set the value for this key. */ set(key: K, value: V | VValue): void; + /** + * Update the value of the field, while requiring a specific previous value. + * + * If the previous value does not match, the update will not be applied. + * If no previous value is present, the `from` value is ignored and the update applied unconditionally. + */ + update(key: K, update: { from: V | VValue; to: V | VValue }): void; }; type OffchainStateKind = From 1d8a9611fe3f164c2c8722559ca25299f42570f4 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 6 May 2024 18:08:06 +0200 Subject: [PATCH 33/97] add failing unit test for dynamic call --- src/lib/mina/test/dynamic-call.unit-test.ts | 90 +++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/lib/mina/test/dynamic-call.unit-test.ts diff --git a/src/lib/mina/test/dynamic-call.unit-test.ts b/src/lib/mina/test/dynamic-call.unit-test.ts new file mode 100644 index 0000000000..eb4381297b --- /dev/null +++ b/src/lib/mina/test/dynamic-call.unit-test.ts @@ -0,0 +1,90 @@ +import { Constructor } from 'src/bindings/lib/provable-generic.js'; +import { + Bool, + UInt64, + SmartContract, + method, + PublicKey, + Mina, +} from '../../../index.js'; + +/** + * Tests that we can call a subcontract dynamically based on the address + * as long as its signature matches the signature our contract was compiled against. + */ + +type Subcontract = SmartContract & { + submethod(a: UInt64, b: UInt64): Promise; +}; + +// two implementations with same signature of the called method, but different provable logic + +class SubcontractA extends SmartContract implements Subcontract { + @method.returns(Bool) + async submethod(a: UInt64, b: UInt64): Promise { + return a.greaterThan(b); + } +} + +class SubcontractB extends SmartContract implements Subcontract { + @method.returns(Bool) + async submethod(a: UInt64, b: UInt64): Promise { + return a.mul(b).equals(UInt64.from(42)); + } +} + +// caller contract that calls the subcontract + +class Caller extends SmartContract { + Subcontract: Constructor = SubcontractA; + + @method + async call(a: UInt64, b: UInt64, address: PublicKey) { + const subcontract = new this.Subcontract(address); + await subcontract.submethod(a, b); + } +} + +// test + +// setup + +let Local = await Mina.LocalBlockchain({ proofsEnabled: true }); +Mina.setActiveInstance(Local); + +let [sender, callerAccount, aAccount, bAccount] = Local.testAccounts; + +await SubcontractA.compile(); +await SubcontractB.compile(); +await Caller.compile(); + +let caller = new Caller(callerAccount); +let a = new SubcontractA(aAccount); +let b = new SubcontractB(bAccount); + +await Mina.transaction(sender, async () => { + await caller.deploy(); + await a.deploy(); + await b.deploy(); +}) + .sign([callerAccount.key, aAccount.key, bAccount.key, sender.key]) + .send(); + +// subcontract A call + +let x = UInt64.from(10); +let y = UInt64.from(5); + +await Mina.transaction(sender, () => caller.call(x, y, aAccount)) + .prove() + .sign([sender.key]) + .send(); + +// subcontract B call + +caller.Subcontract = SubcontractB; + +await Mina.transaction(sender, () => caller.call(x, y, bAccount)) + .prove() + .sign([sender.key]) + .send(); From 2a237e386bb38944ab3c5c984c7c4078686b88d5 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 6 May 2024 18:31:03 +0200 Subject: [PATCH 34/97] fix some debugging code --- src/lib/mina/zkapp.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/lib/mina/zkapp.ts b/src/lib/mina/zkapp.ts index cea652cffb..786b81a17d 100644 --- a/src/lib/mina/zkapp.ts +++ b/src/lib/mina/zkapp.ts @@ -1612,13 +1612,23 @@ function diffRecursive( let { transaction, index, accountUpdate: input } = inputData; diff(transaction, index, prover.toPretty(), input.toPretty()); // TODO - let inputChildren = accountUpdateLayout()!.get(input)!.children.mutable!; - let proverChildren = accountUpdateLayout()!.get(prover)!.children.mutable!; + let proverChildren = accountUpdateLayout()?.get(prover)?.children.mutable; + if (proverChildren === undefined) return; + + // collect input children + let inputChildren: AccountUpdate[] = []; + let callDepth = input.body.callDepth; + for (let i = index; i < transaction.accountUpdates.length; i++) { + let update = transaction.accountUpdates[i]; + if (update.body.callDepth <= callDepth) break; + if (update.body.callDepth === callDepth + 1) inputChildren.push(update); + } + let nChildren = inputChildren.length; for (let i = 0; i < nChildren; i++) { - let inputChild = inputChildren[i].mutable; + let inputChild = inputChildren[i]; let child = proverChildren[i].mutable; - if (!inputChild || !child) return; + if (!child) return; diffRecursive(child, { transaction, index, accountUpdate: inputChild }); } } From 5a9c0b8e6fe8413d87d646b0d626e01e0b17a922 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 6 May 2024 21:10:23 +0200 Subject: [PATCH 35/97] fix test --- src/lib/mina/test/dynamic-call.unit-test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/lib/mina/test/dynamic-call.unit-test.ts b/src/lib/mina/test/dynamic-call.unit-test.ts index eb4381297b..006bf9ef5e 100644 --- a/src/lib/mina/test/dynamic-call.unit-test.ts +++ b/src/lib/mina/test/dynamic-call.unit-test.ts @@ -1,4 +1,3 @@ -import { Constructor } from 'src/bindings/lib/provable-generic.js'; import { Bool, UInt64, @@ -35,12 +34,12 @@ class SubcontractB extends SmartContract implements Subcontract { // caller contract that calls the subcontract -class Caller extends SmartContract { - Subcontract: Constructor = SubcontractA; +let Subcontract: new (...args: any) => Subcontract = SubcontractA; +class Caller extends SmartContract { @method async call(a: UInt64, b: UInt64, address: PublicKey) { - const subcontract = new this.Subcontract(address); + const subcontract = new Subcontract(address); await subcontract.submethod(a, b); } } @@ -82,7 +81,7 @@ await Mina.transaction(sender, () => caller.call(x, y, aAccount)) // subcontract B call -caller.Subcontract = SubcontractB; +Subcontract = SubcontractB; await Mina.transaction(sender, () => caller.call(x, y, bAccount)) .prove() From 8d7a2e06587269eede8219de120ea43fd36a6390 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 6 May 2024 21:18:16 +0200 Subject: [PATCH 36/97] make test prettier --- src/lib/mina/test/dynamic-call.unit-test.ts | 23 ++++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/lib/mina/test/dynamic-call.unit-test.ts b/src/lib/mina/test/dynamic-call.unit-test.ts index 006bf9ef5e..51238215ea 100644 --- a/src/lib/mina/test/dynamic-call.unit-test.ts +++ b/src/lib/mina/test/dynamic-call.unit-test.ts @@ -1,3 +1,9 @@ +/** + * Tests that shows we can call a subcontract dynamically based on the address, + * as long as its signature matches the signature our contract was compiled against. + * + * In other words, the exact implementation/constraints of zkApp methods we call are not hard-coded in the caller contract. + */ import { Bool, UInt64, @@ -7,11 +13,6 @@ import { Mina, } from '../../../index.js'; -/** - * Tests that we can call a subcontract dynamically based on the address - * as long as its signature matches the signature our contract was compiled against. - */ - type Subcontract = SmartContract & { submethod(a: UInt64, b: UInt64): Promise; }; @@ -34,17 +35,19 @@ class SubcontractB extends SmartContract implements Subcontract { // caller contract that calls the subcontract -let Subcontract: new (...args: any) => Subcontract = SubcontractA; - class Caller extends SmartContract { @method async call(a: UInt64, b: UInt64, address: PublicKey) { - const subcontract = new Subcontract(address); + const subcontract = new Caller.Subcontract(address); await subcontract.submethod(a, b); } + + // subcontract to call. this property is changed below + // TODO: having to set this property is a hack, it would be nice to + static Subcontract: new (...args: any) => Subcontract = SubcontractA; } -// test +// TEST BELOW // setup @@ -81,7 +84,7 @@ await Mina.transaction(sender, () => caller.call(x, y, aAccount)) // subcontract B call -Subcontract = SubcontractB; +Caller.Subcontract = SubcontractB; await Mina.transaction(sender, () => caller.call(x, y, bAccount)) .prove() From 271ec6fb2eb72dde895f0459b7bdf226d21f6af4 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Mon, 6 May 2024 22:32:53 +0200 Subject: [PATCH 37/97] Update src/lib/mina/test/dynamic-call.unit-test.ts --- src/lib/mina/test/dynamic-call.unit-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/mina/test/dynamic-call.unit-test.ts b/src/lib/mina/test/dynamic-call.unit-test.ts index 51238215ea..97ef19d9d3 100644 --- a/src/lib/mina/test/dynamic-call.unit-test.ts +++ b/src/lib/mina/test/dynamic-call.unit-test.ts @@ -43,7 +43,7 @@ class Caller extends SmartContract { } // subcontract to call. this property is changed below - // TODO: having to set this property is a hack, it would be nice to + // TODO: having to set this property is a hack, it would be nice to pass the contract as parameter static Subcontract: new (...args: any) => Subcontract = SubcontractA; } From 6428ab707cad20ead26a0cdb948b7c4633b5e923 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 8 May 2024 10:28:06 +0200 Subject: [PATCH 38/97] add better-named wrappers for vk permissions --- src/lib/mina/account-update.ts | 97 +++++++++++++++++++++++++++------- src/lib/mina/precondition.ts | 3 ++ 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/src/lib/mina/account-update.ts b/src/lib/mina/account-update.ts index cfb21532da..cae627548f 100644 --- a/src/lib/mina/account-update.ts +++ b/src/lib/mina/account-update.ts @@ -152,6 +152,18 @@ const False = () => Bool(false); * documentation on those methods to learn more. */ type Permission = Types.AuthRequired; + +class VerificationKeyPermission { + constructor(public auth: Permission, public txnVersion: UInt32) {} + + // TODO this class could be made incompatible with a plain object (breaking change) + // private _ = undefined; + + static withCurrentTxnVersion(perm: Permission) { + return new VerificationKeyPermission(perm, TransactionVersion.current()); + } +} + let Permission = { /** * Modification is impossible. @@ -197,6 +209,65 @@ let Permission = { signatureNecessary: False(), signatureSufficient: True(), }), + + /** + * Special Verification key permissions. + * + * The difference to normal permissions is that `Permission.proof` and `Permission.impossible` are replaced by less restrictive permissions: + * - `impossible` is replaced by `impossibleUntilHardfork` + * - `proof` is replaced by `proofUntilHardfork` + * + * The issue is that a future hardfork which changes the proof system could mean that old verification keys can no longer + * be used to verify proofs in the new proof system, and the zkApp would have to be redeployed to adapt the verification key to the new proof system. + * + * Having either `impossible` or `proof` would mean that these zkApps can't be upgraded after this hypothetical hardfork, and would become unusable. + * + * A future hardfork would manifest as an increment in the "transaction version" of zkApps, which you can check with {@link TransactionVersion.current()}. + * + * The `impossibleUntilHardfork` and `proofUntilHardfork` have an additional `txnVersion` field. + * These permissions follow the same semantics of not upgradable, or only upgradable with proofs, + * _as long as_ the current transaction version is the same as the one one the permission. + * + * If the current transaction version is higher than the one on the permission, the permission is treated as `signature`, + * and the zkApp can be redeployed with a signature of the original account owner. + */ + VerificationKey: { + /** + * Modification is impossible, until the next hardfork. + * + * After a hardfork which changes the {@link TransactionVersion}, the permission is treated as `signature`. + */ + impossibleUntilHardfork: () => + VerificationKeyPermission.withCurrentTxnVersion(Permission.impossible()), + + /** + * Modification is always permitted + */ + none: () => + VerificationKeyPermission.withCurrentTxnVersion(Permission.none()), + + /** + * Modification is permitted by zkapp proofs only; but only until the next hardfork. + * + * After a hardfork which changes the {@link TransactionVersion}, the permission is treated as `signature`. + */ + proofUntilHardfork: () => + VerificationKeyPermission.withCurrentTxnVersion(Permission.proof()), + + /** + * Modification is permitted by signatures only, using the private key of the zkapp account + */ + signature: () => + VerificationKeyPermission.withCurrentTxnVersion(Permission.signature()), + + /** + * Modification is permitted by zkapp proofs or signatures + */ + proofOrSignature: () => + VerificationKeyPermission.withCurrentTxnVersion( + Permission.proofOrSignature() + ), + }, }; // TODO: we could replace the interface below if we could bridge annotations from OCaml @@ -242,10 +313,7 @@ interface Permissions extends Permissions_ { * key associated with the circuit tied to this account. Effectively * "upgradeability" of the smart contract. */ - setVerificationKey: { - auth: Permission; - txnVersion: UInt32; - }; + setVerificationKey: VerificationKeyPermission; /** * The {@link Permission} corresponding to the ability to set the zkapp uri @@ -283,6 +351,7 @@ interface Permissions extends Permissions_ { } let Permissions = { ...Permission, + /** * Default permissions are: * @@ -311,10 +380,7 @@ let Permissions = { receive: Permission.none(), setDelegate: Permission.signature(), setPermissions: Permission.signature(), - setVerificationKey: { - auth: Permission.signature(), - txnVersion: TransactionVersion.current(), - }, + setVerificationKey: Permission.VerificationKey.signature(), setZkappUri: Permission.signature(), editActionState: Permission.proof(), setTokenSymbol: Permission.signature(), @@ -330,10 +396,7 @@ let Permissions = { receive: Permission.none(), setDelegate: Permission.signature(), setPermissions: Permission.signature(), - setVerificationKey: { - auth: Permission.signature(), - txnVersion: TransactionVersion.current(), - }, + setVerificationKey: Permission.VerificationKey.signature(), setZkappUri: Permission.signature(), editActionState: Permission.signature(), setTokenSymbol: Permission.signature(), @@ -350,10 +413,7 @@ let Permissions = { access: Permission.none(), setDelegate: Permission.none(), setPermissions: Permission.none(), - setVerificationKey: { - auth: Permission.signature(), - txnVersion: TransactionVersion.current(), - }, + setVerificationKey: Permission.VerificationKey.none(), setZkappUri: Permission.none(), editActionState: Permission.none(), setTokenSymbol: Permission.none(), @@ -369,10 +429,7 @@ let Permissions = { access: Permission.impossible(), setDelegate: Permission.impossible(), setPermissions: Permission.impossible(), - setVerificationKey: { - auth: Permission.signature(), - txnVersion: TransactionVersion.current(), - }, + setVerificationKey: Permission.VerificationKey.impossibleUntilHardfork(), setZkappUri: Permission.impossible(), editActionState: Permission.impossible(), setTokenSymbol: Permission.impossible(), diff --git a/src/lib/mina/precondition.ts b/src/lib/mina/precondition.ts index 87cf5e0dfc..f276748493 100644 --- a/src/lib/mina/precondition.ts +++ b/src/lib/mina/precondition.ts @@ -19,6 +19,7 @@ import { ZkappUri, } from '../../bindings/mina-transaction/transaction-leaves.js'; import type { Types } from '../../bindings/mina-transaction/types.js'; +import type { Permissions } from './account-update.js'; import { ZkappStateLength } from './mina-instance.js'; export { @@ -615,6 +616,8 @@ type UpdateValueOriginal = { type UpdateValue = { [K in keyof Update_]: K extends 'zkappUri' | 'tokenSymbol' ? string + : K extends 'permissions' + ? Permissions : Update_[K]['value']; }; From 228168aefd4dc47179d54fcc6fb7b3b3aef2603f Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 8 May 2024 10:28:12 +0200 Subject: [PATCH 39/97] adapt all examples --- src/examples/zkapps/dex/erc20.ts | 6 +----- src/examples/zkapps/dex/upgradability.ts | 14 ++------------ src/examples/zkapps/voting/dummy-contract.ts | 5 ----- src/examples/zkapps/voting/membership.ts | 6 +----- src/examples/zkapps/voting/voting.ts | 6 +----- src/examples/zkapps/zkapp-self-update.ts | 6 +----- 6 files changed, 6 insertions(+), 37 deletions(-) diff --git a/src/examples/zkapps/dex/erc20.ts b/src/examples/zkapps/dex/erc20.ts index 586b301faa..a5f6e290ed 100644 --- a/src/examples/zkapps/dex/erc20.ts +++ b/src/examples/zkapps/dex/erc20.ts @@ -13,7 +13,6 @@ import { TokenContract, AccountUpdateForest, Struct, - TransactionVersion, } from 'o1js'; export { Erc20Like, TrivialCoin }; @@ -79,10 +78,7 @@ class TrivialCoin extends TokenContract implements Erc20Like { // make account non-upgradable forever this.account.permissions.set({ ...Permissions.default(), - setVerificationKey: { - auth: Permissions.impossible(), - txnVersion: TransactionVersion.current(), - }, + setVerificationKey: Permissions.VerificationKey.impossibleUntilHardfork(), setPermissions: Permissions.impossible(), access: Permissions.proofOrSignature(), }); diff --git a/src/examples/zkapps/dex/upgradability.ts b/src/examples/zkapps/dex/upgradability.ts index 4034f2e4a8..52e0debecc 100644 --- a/src/examples/zkapps/dex/upgradability.ts +++ b/src/examples/zkapps/dex/upgradability.ts @@ -1,12 +1,5 @@ import { expect } from 'expect'; -import { - AccountUpdate, - Mina, - Permissions, - PrivateKey, - UInt64, - TransactionVersion, -} from 'o1js'; +import { AccountUpdate, Mina, Permissions, PrivateKey, UInt64 } from 'o1js'; import { getProfiler } from '../../utils/profiler.js'; import { TokenContract, addresses, createDex, keys, tokenIds } from './dex.js'; @@ -446,10 +439,7 @@ async function upgradeabilityTests({ withVesting }: { withVesting: boolean }) { let update = AccountUpdate.createSigned(addresses.dex); update.account.permissions.set({ ...Permissions.initial(), - setVerificationKey: { - auth: Permissions.impossible(), - txnVersion: TransactionVersion.current(), - }, + setVerificationKey: Permissions.VerificationKey.impossibleUntilHardfork(), }); }); await tx.prove(); diff --git a/src/examples/zkapps/voting/dummy-contract.ts b/src/examples/zkapps/voting/dummy-contract.ts index 006cacc15a..87aaf9e05e 100644 --- a/src/examples/zkapps/voting/dummy-contract.ts +++ b/src/examples/zkapps/voting/dummy-contract.ts @@ -6,7 +6,6 @@ import { method, DeployArgs, Permissions, - TransactionVersion, } from 'o1js'; export class DummyContract extends SmartContract { @@ -19,10 +18,6 @@ export class DummyContract extends SmartContract { editState: Permissions.proofOrSignature(), editActionState: Permissions.proofOrSignature(), setPermissions: Permissions.proofOrSignature(), - setVerificationKey: { - auth: Permissions.signature(), - txnVersion: TransactionVersion.current(), - }, incrementNonce: Permissions.proofOrSignature(), }); this.sum.set(Field(0)); diff --git a/src/examples/zkapps/voting/membership.ts b/src/examples/zkapps/voting/membership.ts index 178d911826..3210fb39cb 100644 --- a/src/examples/zkapps/voting/membership.ts +++ b/src/examples/zkapps/voting/membership.ts @@ -11,7 +11,6 @@ import { provablePure, AccountUpdate, Provable, - TransactionVersion, } from 'o1js'; import { Member } from './member.js'; import { ParticipantPreconditions } from './preconditions.js'; @@ -76,10 +75,7 @@ export class Membership_ extends SmartContract { editState: Permissions.proofOrSignature(), editActionState: Permissions.proofOrSignature(), setPermissions: Permissions.proofOrSignature(), - setVerificationKey: { - auth: Permissions.proofOrSignature(), - txnVersion: TransactionVersion.current(), - }, + setVerificationKey: Permissions.VerificationKey.proofOrSignature(), incrementNonce: Permissions.proofOrSignature(), }); } diff --git a/src/examples/zkapps/voting/voting.ts b/src/examples/zkapps/voting/voting.ts index 3a90071bf4..f68157cdfd 100644 --- a/src/examples/zkapps/voting/voting.ts +++ b/src/examples/zkapps/voting/voting.ts @@ -11,7 +11,6 @@ import { provablePure, AccountUpdate, Provable, - TransactionVersion, } from 'o1js'; import { Member } from './member.js'; @@ -103,10 +102,7 @@ export class Voting_ extends SmartContract { editState: Permissions.proofOrSignature(), editActionState: Permissions.proofOrSignature(), incrementNonce: Permissions.proofOrSignature(), - setVerificationKey: { - auth: Permissions.none(), - txnVersion: TransactionVersion.current(), - }, + setVerificationKey: Permissions.VerificationKey.none(), setPermissions: Permissions.proofOrSignature(), }); this.accumulatedVotes.set(Reducer.initialActionState); diff --git a/src/examples/zkapps/zkapp-self-update.ts b/src/examples/zkapps/zkapp-self-update.ts index 53c27eb7bc..38e2cc3693 100644 --- a/src/examples/zkapps/zkapp-self-update.ts +++ b/src/examples/zkapps/zkapp-self-update.ts @@ -9,7 +9,6 @@ import { Mina, AccountUpdate, Provable, - TransactionVersion, } from 'o1js'; class SelfUpdater extends SmartContract { @@ -17,10 +16,7 @@ class SelfUpdater extends SmartContract { super.init(); this.account.permissions.set({ ...Permissions.default(), - setVerificationKey: { - auth: Permissions.proof(), - txnVersion: TransactionVersion.current(), - }, + setVerificationKey: Permissions.VerificationKey.proofUntilHardfork(), }); } From b85dc0cbf1c41bd062e8a453e0ca132c0f330d6e Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 9 May 2024 08:18:05 +0200 Subject: [PATCH 40/97] change naming to "during current version" --- src/examples/zkapps/dex/erc20.ts | 3 ++- src/examples/zkapps/dex/upgradability.ts | 3 ++- src/examples/zkapps/zkapp-self-update.ts | 3 ++- src/lib/mina/account-update.ts | 30 ++++++++++++------------ 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/examples/zkapps/dex/erc20.ts b/src/examples/zkapps/dex/erc20.ts index a5f6e290ed..9438bb09b3 100644 --- a/src/examples/zkapps/dex/erc20.ts +++ b/src/examples/zkapps/dex/erc20.ts @@ -78,7 +78,8 @@ class TrivialCoin extends TokenContract implements Erc20Like { // make account non-upgradable forever this.account.permissions.set({ ...Permissions.default(), - setVerificationKey: Permissions.VerificationKey.impossibleUntilHardfork(), + setVerificationKey: + Permissions.VerificationKey.impossibleDuringCurrentVersion(), setPermissions: Permissions.impossible(), access: Permissions.proofOrSignature(), }); diff --git a/src/examples/zkapps/dex/upgradability.ts b/src/examples/zkapps/dex/upgradability.ts index 52e0debecc..1e1f8e8560 100644 --- a/src/examples/zkapps/dex/upgradability.ts +++ b/src/examples/zkapps/dex/upgradability.ts @@ -439,7 +439,8 @@ async function upgradeabilityTests({ withVesting }: { withVesting: boolean }) { let update = AccountUpdate.createSigned(addresses.dex); update.account.permissions.set({ ...Permissions.initial(), - setVerificationKey: Permissions.VerificationKey.impossibleUntilHardfork(), + setVerificationKey: + Permissions.VerificationKey.impossibleDuringCurrentVersion(), }); }); await tx.prove(); diff --git a/src/examples/zkapps/zkapp-self-update.ts b/src/examples/zkapps/zkapp-self-update.ts index 38e2cc3693..e0ef735e8a 100644 --- a/src/examples/zkapps/zkapp-self-update.ts +++ b/src/examples/zkapps/zkapp-self-update.ts @@ -16,7 +16,8 @@ class SelfUpdater extends SmartContract { super.init(); this.account.permissions.set({ ...Permissions.default(), - setVerificationKey: Permissions.VerificationKey.proofUntilHardfork(), + setVerificationKey: + Permissions.VerificationKey.proofDuringCurrentVersion(), }); } diff --git a/src/lib/mina/account-update.ts b/src/lib/mina/account-update.ts index cae627548f..6a38942e73 100644 --- a/src/lib/mina/account-update.ts +++ b/src/lib/mina/account-update.ts @@ -159,7 +159,7 @@ class VerificationKeyPermission { // TODO this class could be made incompatible with a plain object (breaking change) // private _ = undefined; - static withCurrentTxnVersion(perm: Permission) { + static withCurrentVersion(perm: Permission) { return new VerificationKeyPermission(perm, TransactionVersion.current()); } } @@ -214,21 +214,21 @@ let Permission = { * Special Verification key permissions. * * The difference to normal permissions is that `Permission.proof` and `Permission.impossible` are replaced by less restrictive permissions: - * - `impossible` is replaced by `impossibleUntilHardfork` - * - `proof` is replaced by `proofUntilHardfork` + * - `impossible` is replaced by `impossibleDuringCurrentVersion` + * - `proof` is replaced by `proofDuringCurrentVersion` * * The issue is that a future hardfork which changes the proof system could mean that old verification keys can no longer * be used to verify proofs in the new proof system, and the zkApp would have to be redeployed to adapt the verification key to the new proof system. * * Having either `impossible` or `proof` would mean that these zkApps can't be upgraded after this hypothetical hardfork, and would become unusable. * - * A future hardfork would manifest as an increment in the "transaction version" of zkApps, which you can check with {@link TransactionVersion.current()}. + * Such a future hardfork would manifest as an increment in the "transaction version" of zkApps, which you can check with {@link TransactionVersion.current()}. * - * The `impossibleUntilHardfork` and `proofUntilHardfork` have an additional `txnVersion` field. + * The `impossibleDuringCurrentVersion` and `proofDuringCurrentVersion` have an additional `txnVersion` field. * These permissions follow the same semantics of not upgradable, or only upgradable with proofs, * _as long as_ the current transaction version is the same as the one one the permission. * - * If the current transaction version is higher than the one on the permission, the permission is treated as `signature`, + * Once the current transaction version is higher than the one on the permission, the permission is treated as `signature`, * and the zkApp can be redeployed with a signature of the original account owner. */ VerificationKey: { @@ -237,34 +237,33 @@ let Permission = { * * After a hardfork which changes the {@link TransactionVersion}, the permission is treated as `signature`. */ - impossibleUntilHardfork: () => - VerificationKeyPermission.withCurrentTxnVersion(Permission.impossible()), + impossibleDuringCurrentVersion: () => + VerificationKeyPermission.withCurrentVersion(Permission.impossible()), /** * Modification is always permitted */ - none: () => - VerificationKeyPermission.withCurrentTxnVersion(Permission.none()), + none: () => VerificationKeyPermission.withCurrentVersion(Permission.none()), /** * Modification is permitted by zkapp proofs only; but only until the next hardfork. * * After a hardfork which changes the {@link TransactionVersion}, the permission is treated as `signature`. */ - proofUntilHardfork: () => - VerificationKeyPermission.withCurrentTxnVersion(Permission.proof()), + proofDuringCurrentVersion: () => + VerificationKeyPermission.withCurrentVersion(Permission.proof()), /** * Modification is permitted by signatures only, using the private key of the zkapp account */ signature: () => - VerificationKeyPermission.withCurrentTxnVersion(Permission.signature()), + VerificationKeyPermission.withCurrentVersion(Permission.signature()), /** * Modification is permitted by zkapp proofs or signatures */ proofOrSignature: () => - VerificationKeyPermission.withCurrentTxnVersion( + VerificationKeyPermission.withCurrentVersion( Permission.proofOrSignature() ), }, @@ -429,7 +428,8 @@ let Permissions = { access: Permission.impossible(), setDelegate: Permission.impossible(), setPermissions: Permission.impossible(), - setVerificationKey: Permission.VerificationKey.impossibleUntilHardfork(), + setVerificationKey: + Permission.VerificationKey.impossibleDuringCurrentVersion(), setZkappUri: Permission.impossible(), editActionState: Permission.impossible(), setTokenSymbol: Permission.impossible(), From d664b6bd15f46af9b3f5684da7dc901db316c349 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 9 May 2024 08:23:51 +0200 Subject: [PATCH 41/97] comment tweaks --- src/lib/mina/account-update.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/mina/account-update.ts b/src/lib/mina/account-update.ts index 6a38942e73..79a92a7582 100644 --- a/src/lib/mina/account-update.ts +++ b/src/lib/mina/account-update.ts @@ -218,7 +218,7 @@ let Permission = { * - `proof` is replaced by `proofDuringCurrentVersion` * * The issue is that a future hardfork which changes the proof system could mean that old verification keys can no longer - * be used to verify proofs in the new proof system, and the zkApp would have to be redeployed to adapt the verification key to the new proof system. + * be used to verify proofs in the new proof system, and the zkApp would have to be redeployed to adapt the verification key. * * Having either `impossible` or `proof` would mean that these zkApps can't be upgraded after this hypothetical hardfork, and would become unusable. * @@ -226,16 +226,16 @@ let Permission = { * * The `impossibleDuringCurrentVersion` and `proofDuringCurrentVersion` have an additional `txnVersion` field. * These permissions follow the same semantics of not upgradable, or only upgradable with proofs, - * _as long as_ the current transaction version is the same as the one one the permission. + * _as long as_ the current transaction version is the same as the one on the permission. * * Once the current transaction version is higher than the one on the permission, the permission is treated as `signature`, * and the zkApp can be redeployed with a signature of the original account owner. */ VerificationKey: { /** - * Modification is impossible, until the next hardfork. + * Modification is impossible, as long as the network accepts the current {@link TransactionVersion}. * - * After a hardfork which changes the {@link TransactionVersion}, the permission is treated as `signature`. + * After a hardfork that increments the transaction version, the permission is treated as `signature`. */ impossibleDuringCurrentVersion: () => VerificationKeyPermission.withCurrentVersion(Permission.impossible()), @@ -246,9 +246,9 @@ let Permission = { none: () => VerificationKeyPermission.withCurrentVersion(Permission.none()), /** - * Modification is permitted by zkapp proofs only; but only until the next hardfork. + * Modification is permitted by zkapp proofs only; as long as the network accepts the current {@link TransactionVersion}. * - * After a hardfork which changes the {@link TransactionVersion}, the permission is treated as `signature`. + * After a hardfork that increments the transaction version, the permission is treated as `signature`. */ proofDuringCurrentVersion: () => VerificationKeyPermission.withCurrentVersion(Permission.proof()), From 045c90b87c75238f2adc4fd2c8763202f9beb05f Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 9 May 2024 08:29:34 +0200 Subject: [PATCH 42/97] changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b99530dcb..7847fcdb57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased](https://github.com/o1-labs/o1js/compare/4a17de857...HEAD) +### Added + +- `Permissions.VerificationKey`, a namespace for verification key permissions https://github.com/o1-labs/o1js/pull/1639 + - Includes more accurate names for the `impossible` and `proof` permissions for verification keys, which are now called `impossibleDuringCurrentVersion` and `proofDuringCurrentVersion` respectively. + ### Fixes - Fix absolute imports which prevented compilation in some TS projects that used o1js https://github.com/o1-labs/o1js/pull/1628 From 7bc958d924031de5ea42860907f7405eb7bc8e1f Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 09:46:39 +0200 Subject: [PATCH 43/97] implement set() --- .../actions/offchain-state-serialization.ts | 66 +++++++++++----- src/lib/mina/actions/offchain-state.ts | 75 +++++++++++++++---- src/lib/mina/actions/reducer.ts | 2 +- src/lib/mina/state.ts | 2 +- src/lib/provable/crypto/poseidon.ts | 2 +- 5 files changed, 113 insertions(+), 34 deletions(-) diff --git a/src/lib/mina/actions/offchain-state-serialization.ts b/src/lib/mina/actions/offchain-state-serialization.ts index 867db66fda..dbbf16fda9 100644 --- a/src/lib/mina/actions/offchain-state-serialization.ts +++ b/src/lib/mina/actions/offchain-state-serialization.ts @@ -11,6 +11,7 @@ import { Poseidon, ProvableHashable, hashWithPrefix, + packToFields, salt, } from '../../provable/crypto/poseidon.js'; import { Field } from '../../provable/wrapped.js'; @@ -23,6 +24,7 @@ import * as Mina from '../mina.js'; import { PublicKey } from '../../provable/crypto/signature.js'; import { Provable } from '../../provable/provable.js'; import { Actions } from '../account-update.js'; +import { TupleN } from '../../util/types.js'; export { fromAction, @@ -34,21 +36,23 @@ export { }; type Action = [...Field[], Field, Field]; -type Actionable = ProvableHashable & ProvablePure; +type Actionable = ProvableHashable & ProvablePure; -function toAction( - keyType: Actionable, +function toAction | undefined>( + prefix: Field, + keyType: KeyType, valueType: Actionable, - key: K, + key: KeyType extends undefined ? undefined : K, value: V ): Action { - let combinedSize = keyType.sizeInFields() + valueType.sizeInFields(); + let keySize = keyType?.sizeInFields() ?? 0; + let combinedSize = 2 + keySize + valueType.sizeInFields(); let padding = combinedSize % 2 === 0 ? [] : [Field(0)]; - let keyHash = Poseidon.hashPacked(keyType, key); + let keyHash = hashPackedWithPrefix([prefix, Field(keySize)], keyType, key); let valueHash = Poseidon.hashPacked(valueType, value); return [ - ...keyType.toFields(key), + ...(keyType?.toFields(key as K) ?? []), ...valueType.toFields(value), ...padding, keyHash, @@ -56,26 +60,54 @@ function toAction( ]; } -function fromAction( - keyType: Actionable, +function fromAction | undefined>( + keyType: KeyType, valueType: Actionable, action: Action -): { key: K; value: V } { - let keySize = keyType.sizeInFields(); +): { + prefix: [Field, Field]; + key: KeyType extends undefined ? undefined : K; + value: V; +} { + let keySize = keyType?.sizeInFields() ?? 0; let valueSize = valueType.sizeInFields(); - let paddingSize = (keySize + valueSize) % 2 === 0 ? 0 : 1; + let paddingSize = (2 + keySize + valueSize) % 2 === 0 ? 0 : 1; assert( - action.length === keySize + valueSize + paddingSize + 2, + action.length === 2 + keySize + valueSize + paddingSize + 2, 'invalid action size' ); + let prefix = TupleN.fromArray(2, action.slice(0, 2)); + let key: K | undefined = undefined; - let key = keyType.fromFields(action.slice(0, keySize)); - keyType.check(key); + if (keyType !== undefined) { + key = keyType.fromFields(action.slice(2, 2 + keySize)); + keyType.check(key); + } - let value = valueType.fromFields(action.slice(keySize, keySize + valueSize)); + let value = valueType.fromFields( + action.slice(2 + keySize, 2 + keySize + valueSize) + ); valueType.check(value); - return { key, value }; + return { prefix, key: key as any, value }; +} + +function hashPackedWithPrefix | undefined>( + prefix: [Field, Field], + type: Type, + value: Type extends undefined ? undefined : T +) { + // hash constant prefix + let state = Poseidon.initialState(); + state = Poseidon.update(state, prefix); + + // hash value if a type was passed in + if (type !== undefined) { + let input = type.toInput(value as T); + let packed = packToFields(input); + state = Poseidon.update(state, packed); + } + return state[0]; } /** diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index d391b58d35..ab650b27bc 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -1,7 +1,7 @@ import { InferProvable } from '../../provable/types/struct.js'; -import { Actionable } from './offchain-state-serialization.js'; +import { Actionable, toAction } from './offchain-state-serialization.js'; import { PublicKey } from '../../provable/crypto/signature.js'; -import { Field } from '../../provable/field.js'; +import { Field } from '../../provable/wrapped.js'; import { Proof } from '../../proof-system/zkprogram.js'; import { MerkleMapState, @@ -11,6 +11,8 @@ import { Option } from '../../provable/option.js'; import { InferValue } from '../../../bindings/lib/provable-generic.js'; import { SmartContract } from '../zkapp.js'; import { assert } from '../../provable/gadgets/common.js'; +import { declareState } from '../state.js'; +import { Actions } from '../account-update.js'; export { OffchainState }; @@ -45,9 +47,9 @@ type OffchainState = { /** * Set the contract that this offchain state is connected with. * - * This tells the offchain state about the account to fetch data from. + * This tells the offchain state about the account to fetch data from and modify. */ - setContractAccount(contract: Contract): void; + setContractAccount(contract: SmartContract): void; /** * Compile the offchain state ZkProgram. @@ -77,11 +79,11 @@ function OffchainState< >(config: Config): OffchainState { // setup internal state of this "class" let internal = { - _contract: undefined as Contract | undefined, + _contract: undefined as SmartContract | undefined, get contract() { assert( - internal._contract === undefined, + internal._contract !== undefined, 'Must call `setContractAccount()` first' ); return internal._contract; @@ -92,23 +94,70 @@ function OffchainState< let rollup = OffchainStateRollup(); - function field(index: number, type: Any) { + function field( + index: number, + type: Actionable + ): OffchainField { + const prefix = Field(index); + + function selfToAction(value: T | TValue): Field[] { + return toAction( + prefix, + undefined, + type, + undefined, + type.fromValue(value) + ); + } + return { + set(value) { + // serialize into action + let action = selfToAction(value); + + // push action on account update + let update = internal.contract.self; + Actions.pushEvent(update.body.actions, action); + }, + update: notImplemented, get: notImplemented, - set: notImplemented, }; } - function map(index: number, keyType: Any, valueType: Any) { + function map( + index: number, + keyType: Actionable, + valueType: Actionable + ): OffchainMap { + const prefix = Field(index); + + function selfToAction(key: K, value: V | VValue): Field[] { + return toAction( + prefix, + keyType, + valueType, + key, + valueType.fromValue(value) + ); + } + return { + set(key, value) { + // serialize into action + let action = selfToAction(key, value); + + // push action on account update + let update = internal.contract.self; + Actions.pushEvent(update.body.actions, action); + }, + update: notImplemented, get: notImplemented, - set: notImplemented, }; } return { setContractClass(contract) { - notImplemented(); + declareState(contract, { stateRoot: Field, actionState: Field }); }, setContractAccount(contract) { @@ -134,9 +183,7 @@ function OffchainState< key, kind.kind === 'offchain-field' ? field(i, kind.type) - : kind.kind === 'offchain-map' - ? map(i, kind.keyType, kind.valueType) - : notImplemented(), + : map(i, kind.keyType, kind.valueType), ]) ) as any, }; diff --git a/src/lib/mina/actions/reducer.ts b/src/lib/mina/actions/reducer.ts index fab297dab7..8be11506fb 100644 --- a/src/lib/mina/actions/reducer.ts +++ b/src/lib/mina/actions/reducer.ts @@ -138,7 +138,7 @@ function getReducer(contract: SmartContract): ReducerReturn { 'You are trying to use a reducer without having declared its type.\n' + `Make sure to add a property \`reducer\` on ${contract.constructor.name}, for example: class ${contract.constructor.name} extends SmartContract { - reducer = { actionType: Field }; + reducer = Reducer({ actionType: Field }); }` ); return { diff --git a/src/lib/mina/state.ts b/src/lib/mina/state.ts index 2501c6dd3b..920269ac8a 100644 --- a/src/lib/mina/state.ts +++ b/src/lib/mina/state.ts @@ -162,7 +162,7 @@ function state(stateType: FlexibleProvablePure) { */ function declareState( SmartContract: T, - states: Record> + states: Record> ) { for (let key in states) { let CircuitValue = states[key]; diff --git a/src/lib/provable/crypto/poseidon.ts b/src/lib/provable/crypto/poseidon.ts index 49a514282f..c3425f0f66 100644 --- a/src/lib/provable/crypto/poseidon.ts +++ b/src/lib/provable/crypto/poseidon.ts @@ -28,7 +28,7 @@ export { }; type Hashable = { toInput: (x: T) => HashInput; empty: () => T }; -type ProvableHashable = Provable & Hashable; +type ProvableHashable = Provable & Hashable; class Sponge { #sponge: unknown; From 80d66abe6426a67fe2fd6457e39591453b4b06bc Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 09:50:16 +0200 Subject: [PATCH 44/97] don't use update() for now --- .../actions/offchain-contract.unit-test.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 808e6f4a3d..5776ecb577 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -35,14 +35,16 @@ class ExampleContract extends SmartContract { // TODO we use `update` here so that previous balances can't be overridden // but this still includes a trivial double-spend opportunity, because the updates are not rejected atomically: // if the `to` update gets accepted but the `from` update fails, it's a double-spend - state.fields.accounts.update(from, { - from: fromBalance, - to: fromBalance.sub(amount), - }); - state.fields.accounts.update(to, { - from: toBalance, - to: toBalance.add(amount), - }); + state.fields.accounts.set(from, fromBalance.sub(amount)); + // state.fields.accounts.update(from, { + // from: fromBalance, + // to: fromBalance.sub(amount), + // }); + state.fields.accounts.set(to, toBalance.add(amount)); + // state.fields.accounts.update(to, { + // from: toBalance, + // to: toBalance.add(amount), + // }); } @method.returns(UInt64) From bc2893d3cbfe965906fb697ea5d4599bc1e974e7 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 13:02:16 +0200 Subject: [PATCH 45/97] change serialization and implement get() --- .../actions/offchain-contract.unit-test.ts | 2 +- .../actions/offchain-state-serialization.ts | 95 +++++++----- src/lib/mina/actions/offchain-state.ts | 140 +++++++++++++----- 3 files changed, 166 insertions(+), 71 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 5776ecb577..3f37a6df32 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -73,7 +73,7 @@ Mina.setActiveInstance(Local); let [sender, receiver, contractAccount] = Local.testAccounts; let contract = new ExampleContract(contractAccount); -state.setContractAccount(contract); +state.setContractInstance(contract); await ExampleContract.compile(); diff --git a/src/lib/mina/actions/offchain-state-serialization.ts b/src/lib/mina/actions/offchain-state-serialization.ts index dbbf16fda9..47b67fb8f8 100644 --- a/src/lib/mina/actions/offchain-state-serialization.ts +++ b/src/lib/mina/actions/offchain-state-serialization.ts @@ -24,20 +24,31 @@ import * as Mina from '../mina.js'; import { PublicKey } from '../../provable/crypto/signature.js'; import { Provable } from '../../provable/provable.js'; import { Actions } from '../account-update.js'; -import { TupleN } from '../../util/types.js'; +import { MerkleMap } from '../../provable/merkle-map.js'; export { - fromAction, + toKeyHash, toAction, + fromActionWithoutHashes, MerkleLeaf, ActionList, fetchMerkleLeaves, + fetchMerkleMap, Actionable, }; type Action = [...Field[], Field, Field]; type Actionable = ProvableHashable & ProvablePure; +function toKeyHash | undefined>( + prefix: Field, + keyType: KeyType, + key: KeyType extends undefined ? undefined : K +): Field { + let keySize = keyType?.sizeInFields() ?? 0; + return hashPackedWithPrefix([prefix, Field(keySize)], keyType, key); +} + function toAction | undefined>( prefix: Field, keyType: KeyType, @@ -45,51 +56,26 @@ function toAction | undefined>( key: KeyType extends undefined ? undefined : K, value: V ): Action { - let keySize = keyType?.sizeInFields() ?? 0; - let combinedSize = 2 + keySize + valueType.sizeInFields(); + let combinedSize = valueType.sizeInFields(); let padding = combinedSize % 2 === 0 ? [] : [Field(0)]; - let keyHash = hashPackedWithPrefix([prefix, Field(keySize)], keyType, key); + let keyHash = hashPackedWithPrefix([prefix, Field(0)], keyType, key); let valueHash = Poseidon.hashPacked(valueType, value); - return [ - ...(keyType?.toFields(key as K) ?? []), - ...valueType.toFields(value), - ...padding, - keyHash, - valueHash, - ]; + return [...valueType.toFields(value), ...padding, keyHash, valueHash]; } -function fromAction | undefined>( - keyType: KeyType, +function fromActionWithoutHashes( valueType: Actionable, - action: Action -): { - prefix: [Field, Field]; - key: KeyType extends undefined ? undefined : K; - value: V; -} { - let keySize = keyType?.sizeInFields() ?? 0; + action: Field[] +): V { let valueSize = valueType.sizeInFields(); - let paddingSize = (2 + keySize + valueSize) % 2 === 0 ? 0 : 1; - assert( - action.length === 2 + keySize + valueSize + paddingSize + 2, - 'invalid action size' - ); - let prefix = TupleN.fromArray(2, action.slice(0, 2)); - let key: K | undefined = undefined; - - if (keyType !== undefined) { - key = keyType.fromFields(action.slice(2, 2 + keySize)); - keyType.check(key); - } + let paddingSize = valueSize % 2 === 0 ? 0 : 1; + assert(action.length === valueSize + paddingSize, 'invalid action size'); - let value = valueType.fromFields( - action.slice(2 + keySize, 2 + keySize + valueSize) - ); + let value = valueType.fromFields(action.slice(0, valueSize)); valueType.check(value); - return { prefix, key: key as any, value }; + return value; } function hashPackedWithPrefix | undefined>( @@ -183,3 +169,38 @@ async function fetchMerkleLeaves( ); return MerkleActions.from(merkleLeafs.map((a) => ActionList.fromReverse(a))); } + +/** + * Recreate Merkle tree from fetched actions. + * + * We also deserialize a keyHash -> value map from the leaves. + */ +async function fetchMerkleMap( + contract: { address: PublicKey; tokenId: Field }, + endActionState?: Field +): Promise<{ merkleMap: MerkleMap; valueMap: Map }> { + let result = await Mina.fetchActions( + contract.address, + { endActionState }, + contract.tokenId + ); + if ('error' in result) throw Error(JSON.stringify(result)); + + let leaves = result + .map((event) => + event.actions + .map((action) => MerkleLeaf.fromAction(action.map(Field))) + .reverse() + ) + .flat(); + + let merkleMap = new MerkleMap(); + let valueMap = new Map(); + + for (let leaf of leaves) { + merkleMap.set(leaf.key, leaf.value); + valueMap.set(leaf.key.toBigInt(), leaf.prefix.get()); + } + + return { merkleMap, valueMap }; +} diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index ab650b27bc..385f17d399 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -1,6 +1,11 @@ import { InferProvable } from '../../provable/types/struct.js'; -import { Actionable, toAction } from './offchain-state-serialization.js'; -import { PublicKey } from '../../provable/crypto/signature.js'; +import { + Actionable, + fetchMerkleMap, + fromActionWithoutHashes, + toAction, + toKeyHash, +} from './offchain-state-serialization.js'; import { Field } from '../../provable/wrapped.js'; import { Proof } from '../../proof-system/zkprogram.js'; import { @@ -11,8 +16,11 @@ import { Option } from '../../provable/option.js'; import { InferValue } from '../../../bindings/lib/provable-generic.js'; import { SmartContract } from '../zkapp.js'; import { assert } from '../../provable/gadgets/common.js'; -import { declareState } from '../state.js'; +import { State, declareState } from '../state.js'; import { Actions } from '../account-update.js'; +import { MerkleMap, MerkleMapWitness } from '../../provable/merkle-map.js'; +import { Provable } from '../../provable/provable.js'; +import { Poseidon } from '../../provable/crypto/poseidon.js'; export { OffchainState }; @@ -47,9 +55,9 @@ type OffchainState = { /** * Set the contract that this offchain state is connected with. * - * This tells the offchain state about the account to fetch data from and modify. + * This tells the offchain state about the account to fetch data from and modify, and lets it handle actions and onchain state. */ - setContractAccount(contract: SmartContract): void; + setContractInstance(contract: SmartContract): void; /** * Compile the offchain state ZkProgram. @@ -74,12 +82,19 @@ type OffchainState = { settle(proof: Proof): Promise; }; +type OffchainStateContract = SmartContract & { + stateRoot: State; + actionState: State; +}; + function OffchainState< const Config extends { [key: string]: OffchainStateKind } >(config: Config): OffchainState { // setup internal state of this "class" let internal = { - _contract: undefined as SmartContract | undefined, + _contract: undefined as OffchainStateContract | undefined, + _merkleMap: undefined as MerkleMap | undefined, + _valueMap: undefined as Map | undefined, get contract() { assert( @@ -89,38 +104,97 @@ function OffchainState< return internal._contract; }, }; + const onchainActionState = async () => { + let actionState = await internal.contract.actionState.fetch(); + assert(actionState !== undefined, 'Could not fetch action state'); + return actionState; + }; + + const merkleMaps = async () => { + if (internal._merkleMap !== undefined && internal._valueMap !== undefined) { + return { merkleMap: internal._merkleMap, valueMap: internal._valueMap }; + } + let actionState = await onchainActionState(); + let { merkleMap, valueMap } = await fetchMerkleMap( + internal.contract, + actionState + ); + internal._merkleMap = merkleMap; + internal._valueMap = valueMap; + return { merkleMap, valueMap }; + }; const notImplemented = (): any => assert(false, 'Not implemented'); let rollup = OffchainStateRollup(); + /** + * generic get which works for both fields and maps + */ + async function get(key: Field, valueType: Actionable) { + // get onchain merkle root + let stateRoot = internal.contract.stateRoot.getAndRequireEquals(); + + // witness the actual value + const optionType = Option(valueType); + let value = await Provable.witnessAsync(optionType, async () => { + let { valueMap } = await merkleMaps(); + let valueFields = valueMap.get(key.toBigInt()); + if (valueFields === undefined) { + return optionType.from(); + } + let value = fromActionWithoutHashes(valueType, valueFields); + return optionType.from(value); + }); + + // witness a merkle witness + let witness = await Provable.witnessAsync(MerkleMapWitness, async () => { + let { merkleMap } = await merkleMaps(); + return merkleMap.getWitness(key); + }); + + // anchor the value against the onchain root and passed in key + // we also allow the value to be missing, in which case the map must contain the 0 element + let valueHash = Provable.if( + value.isSome, + Poseidon.hashPacked(valueType, value.value), + Field(0) + ); + let [actualRoot, actualKey] = witness.computeRootAndKey(valueHash); + key.assertEquals(actualKey, 'key mismatch'); + stateRoot.assertEquals(actualRoot, 'root mismatch'); + + return value; + } + function field( index: number, type: Actionable ): OffchainField { const prefix = Field(index); - function selfToAction(value: T | TValue): Field[] { - return toAction( - prefix, - undefined, - type, - undefined, - type.fromValue(value) - ); - } - return { set(value) { // serialize into action - let action = selfToAction(value); + let action = toAction( + prefix, + undefined, + type, + undefined, + type.fromValue(value) + ); // push action on account update let update = internal.contract.self; Actions.pushEvent(update.body.actions, action); }, update: notImplemented, - get: notImplemented, + async get() { + let key = toKeyHash(prefix, undefined, undefined); + let optionValue = await get(key, type); + // for fields that are not in the map, we return the default value -- similar to onchain state + return optionValue.orElse(type.empty()); + }, }; } @@ -131,27 +205,26 @@ function OffchainState< ): OffchainMap { const prefix = Field(index); - function selfToAction(key: K, value: V | VValue): Field[] { - return toAction( - prefix, - keyType, - valueType, - key, - valueType.fromValue(value) - ); - } - return { set(key, value) { // serialize into action - let action = selfToAction(key, value); + let action = toAction( + prefix, + keyType, + valueType, + key, + valueType.fromValue(value) + ); // push action on account update let update = internal.contract.self; Actions.pushEvent(update.body.actions, action); }, update: notImplemented, - get: notImplemented, + async get(key) { + let keyHash = toKeyHash(prefix, keyType, key); + return await get(keyHash, valueType); + }, }; } @@ -160,8 +233,10 @@ function OffchainState< declareState(contract, { stateRoot: Field, actionState: Field }); }, - setContractAccount(contract) { - internal._contract = contract; + setContractInstance(contract) { + (contract as any).actionState = State(); + (contract as any).stateRoot = State(); + internal._contract = contract as any; }, async compile() { @@ -195,7 +270,6 @@ OffchainState.Field = OffchainField; // type helpers type Any = Actionable; -type Contract = { address: PublicKey; tokenId: Field }; function OffchainField(type: T) { return { kind: 'offchain-field' as const, type }; From e00fb471fc196c96e5720a9307978c9ee001fbef Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 13:43:09 +0200 Subject: [PATCH 46/97] settling --- .../actions/offchain-contract.unit-test.ts | 1 + src/lib/mina/actions/offchain-state-rollup.ts | 2 +- .../actions/offchain-state-serialization.ts | 1 + src/lib/mina/actions/offchain-state.ts | 48 +++++++++++++++---- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 3f37a6df32..cb8d881f33 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -75,6 +75,7 @@ let [sender, receiver, contractAccount] = Local.testAccounts; let contract = new ExampleContract(contractAccount); state.setContractInstance(contract); +await state.compile(); await ExampleContract.compile(); // deploy and create first account diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index 8092974e43..614631a030 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -144,7 +144,7 @@ function OffchainStateRollup({ let RollupProof = ZkProgram.Proof(offchainStateRollup); - let isCompiled = true; + let isCompiled = false; return { Proof: RollupProof, diff --git a/src/lib/mina/actions/offchain-state-serialization.ts b/src/lib/mina/actions/offchain-state-serialization.ts index 47b67fb8f8..8a22d45f0b 100644 --- a/src/lib/mina/actions/offchain-state-serialization.ts +++ b/src/lib/mina/actions/offchain-state-serialization.ts @@ -170,6 +170,7 @@ async function fetchMerkleLeaves( return MerkleActions.from(merkleLeafs.map((a) => ActionList.fromReverse(a))); } +// TODO this should be `updateMerkleMap`, and we should call it on every get() and settle() /** * Recreate Merkle tree from fetched actions. * diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 385f17d399..7fbe3d6299 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -1,6 +1,7 @@ import { InferProvable } from '../../provable/types/struct.js'; import { Actionable, + fetchMerkleLeaves, fetchMerkleMap, fromActionWithoutHashes, toAction, @@ -83,8 +84,7 @@ type OffchainState = { }; type OffchainStateContract = SmartContract & { - stateRoot: State; - actionState: State; + offchainState: State; }; function OffchainState< @@ -105,7 +105,8 @@ function OffchainState< }, }; const onchainActionState = async () => { - let actionState = await internal.contract.actionState.fetch(); + let actionState = (await internal.contract.offchainState.fetch()) + ?.actionState; assert(actionState !== undefined, 'Could not fetch action state'); return actionState; }; @@ -133,7 +134,7 @@ function OffchainState< */ async function get(key: Field, valueType: Actionable) { // get onchain merkle root - let stateRoot = internal.contract.stateRoot.getAndRequireEquals(); + let stateRoot = internal.contract.offchainState.getAndRequireEquals().root; // witness the actual value const optionType = Option(valueType); @@ -230,12 +231,11 @@ function OffchainState< return { setContractClass(contract) { - declareState(contract, { stateRoot: Field, actionState: Field }); + declareState(contract, { offchainState: MerkleMapState }); }, setContractInstance(contract) { - (contract as any).actionState = State(); - (contract as any).stateRoot = State(); + (contract as any).offchainState = State(); internal._contract = contract as any; }, @@ -244,13 +244,43 @@ function OffchainState< }, async createSettlementProof() { - return notImplemented(); + let { merkleMap } = await merkleMaps(); + + // fetch pending actions + let actionState = await onchainActionState(); + let actions = await fetchMerkleLeaves(internal.contract, { + fromActionState: actionState, + }); + + let result = await rollup.prove(merkleMap.tree, actions); + + // update internal merkle maps as well + // TODO make this not insanely recompute everything + let { merkleMap: newMerkleMap, valueMap: newValueMap } = + await fetchMerkleMap(internal.contract); + internal._merkleMap = newMerkleMap; + internal._valueMap = newValueMap; + + return result.proof; }, Proof: rollup.Proof, async settle(proof) { - notImplemented(); + // verify the proof + proof.verify(); + + // check that proof moves state forward from the one currently storedö + let state = internal.contract.offchainState.getAndRequireEquals(); + Provable.assertEqual(MerkleMapState, state, proof.publicInput); + + // require that proof uses the correct pending actions + internal.contract.account.actionState.requireEquals( + proof.publicOutput.actionState + ); + + // update the state + internal.contract.offchainState.set(proof.publicOutput); }, fields: Object.fromEntries( From bd966c645cf2bd842db62a4b2e6b635d746a9ede Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 16:25:04 +0200 Subject: [PATCH 47/97] support onchain state default values --- src/lib/mina/state.ts | 19 ++++++++++++++----- src/lib/mina/zkapp.ts | 19 ++++++++++++++++++- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/lib/mina/state.ts b/src/lib/mina/state.ts index 920269ac8a..799e46bcd0 100644 --- a/src/lib/mina/state.ts +++ b/src/lib/mina/state.ts @@ -12,7 +12,12 @@ import { ProvablePure } from '../provable/types/provable-intf.js'; // external API export { State, state, declareState }; // internal API -export { assertStatePrecondition, cleanStatePrecondition }; +export { + assertStatePrecondition, + cleanStatePrecondition, + getLayout, + InternalStateType, +}; /** * Gettable and settable state that can be checked for equality. @@ -70,8 +75,8 @@ type State = { */ fromAppState(appState: Field[]): A; }; -function State(): State { - return createState(); +function State(defaultValue?: A): State { + return createState(defaultValue); } /** @@ -181,11 +186,15 @@ type StateAttachedContract = { cachedVariable?: A; }; -type InternalStateType = State & { _contract?: StateAttachedContract }; +type InternalStateType = State & { + _contract?: StateAttachedContract; + defaultValue?: A; +}; -function createState(): InternalStateType { +function createState(defaultValue?: T): InternalStateType { return { _contract: undefined as StateAttachedContract | undefined, + defaultValue, set(state: T) { if (this._contract === undefined) diff --git a/src/lib/mina/zkapp.ts b/src/lib/mina/zkapp.ts index fbfd5ff0e6..cbba6f8e94 100644 --- a/src/lib/mina/zkapp.ts +++ b/src/lib/mina/zkapp.ts @@ -51,7 +51,12 @@ import { sortMethodArguments, } from '../proof-system/zkprogram.js'; import { PublicKey } from '../provable/crypto/signature.js'; -import { assertStatePrecondition, cleanStatePrecondition } from './state.js'; +import { + InternalStateType, + assertStatePrecondition, + cleanStatePrecondition, + getLayout, +} from './state.js'; import { inAnalyze, inCheckedComputation, @@ -766,9 +771,21 @@ super.init(); // let accountUpdate = this.newSelf(); // this would emulate the behaviour of init() being a @method this.account.provedState.requireEquals(Bool(false)); let accountUpdate = this.self; + + // set all state fields to 0 for (let i = 0; i < ZkappStateLength; i++) { AccountUpdate.setValue(accountUpdate.body.update.appState[i], Field(0)); } + + // for all explicitly declared states, set them to their default value + let stateKeys = getLayout(this.constructor as typeof SmartContract).keys(); + for (let key of stateKeys) { + let state = this[key as keyof this] as InternalStateType | undefined; + if (state !== undefined && state.defaultValue !== undefined) { + state.set(state.defaultValue); + } + } + AccountUpdate.attachToTransaction(accountUpdate); } From 4f525bab0d7b1510144cd98684e37b83939a9acb Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 16:27:05 +0200 Subject: [PATCH 48/97] set state fields explicitly --- .../actions/offchain-contract.unit-test.ts | 72 +++++++++++++------ src/lib/mina/actions/offchain-state-rollup.ts | 42 +++++++---- src/lib/mina/actions/offchain-state.ts | 43 +++++------ 3 files changed, 94 insertions(+), 63 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index cb8d881f33..379651736f 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -1,46 +1,52 @@ -import { OffchainState } from './offchain-state.js'; +import { OffchainState, OffchainStateCommitments } from './offchain-state.js'; import { PublicKey } from '../../provable/crypto/signature.js'; import { UInt64 } from '../../provable/int.js'; import { SmartContract, method } from '../zkapp.js'; -import { AccountUpdate, Mina } from '../../../index.js'; +import { Mina, State, state } from '../../../index.js'; -const state = OffchainState({ +const offchainState = OffchainState({ accounts: OffchainState.Map(PublicKey, UInt64), totalSupply: OffchainState.Field(UInt64), }); -class StateProof extends state.Proof {} +class StateProof extends offchainState.Proof {} // example contract that interacts with offchain state class ExampleContract extends SmartContract { + // TODO could have sugar for this like + // @OffchainState.commitment offchainState = OffchainState.Commitment(); + @state(OffchainStateCommitments) offchainState = State( + OffchainStateCommitments.empty() + ); + @method async createAccount(address: PublicKey, amountToMint: UInt64) { - state.fields.accounts.set(address, amountToMint); + offchainState.fields.accounts.set(address, amountToMint); // TODO `totalSupply` easily gets into a wrong state here on concurrent calls. // and using `.update()` doesn't help either - let totalSupply = await state.fields.totalSupply.get(); - state.fields.totalSupply.set(totalSupply.add(amountToMint)); + let totalSupply = await offchainState.fields.totalSupply.get(); + offchainState.fields.totalSupply.set(totalSupply.add(amountToMint)); } @method async transfer(from: PublicKey, to: PublicKey, amount: UInt64) { - let fromOption = await state.fields.accounts.get(from); + let fromOption = await offchainState.fields.accounts.get(from); let fromBalance = fromOption.assertSome('sender account exists'); - let toOption = await state.fields.accounts.get(to); + let toOption = await offchainState.fields.accounts.get(to); let toBalance = toOption.orElse(0n); // TODO we use `update` here so that previous balances can't be overridden // but this still includes a trivial double-spend opportunity, because the updates are not rejected atomically: // if the `to` update gets accepted but the `from` update fails, it's a double-spend - state.fields.accounts.set(from, fromBalance.sub(amount)); + offchainState.fields.accounts.set(from, fromBalance.sub(amount)); // state.fields.accounts.update(from, { // from: fromBalance, // to: fromBalance.sub(amount), // }); - state.fields.accounts.set(to, toBalance.add(amount)); + offchainState.fields.accounts.set(to, toBalance.add(amount)); // state.fields.accounts.update(to, { // from: toBalance, // to: toBalance.add(amount), @@ -49,20 +55,19 @@ class ExampleContract extends SmartContract { @method.returns(UInt64) async getSupply() { - return await state.fields.totalSupply.get(); + return await offchainState.fields.totalSupply.get(); } @method.returns(UInt64) async getBalance(address: PublicKey) { - return (await state.fields.accounts.get(address)).orElse(0n); + return (await offchainState.fields.accounts.get(address)).orElse(0n); } @method async settle(proof: StateProof) { - await state.settle(proof); + await offchainState.settle(proof); } } -state.setContractClass(ExampleContract); // test code below @@ -73,30 +78,49 @@ Mina.setActiveInstance(Local); let [sender, receiver, contractAccount] = Local.testAccounts; let contract = new ExampleContract(contractAccount); -state.setContractInstance(contract); +offchainState.setContractInstance(contract); -await state.compile(); +console.time('compile'); +await offchainState.compile(); +console.timeEnd('compile'); +console.time('compile contract'); await ExampleContract.compile(); +console.timeEnd('compile contract'); // deploy and create first account +console.time('deploy'); await Mina.transaction(sender, async () => { - AccountUpdate.fundNewAccount(sender); await contract.deploy(); - await contract.createAccount(sender, UInt64.from(1000)); }) .sign([sender.key, contractAccount.key]) .prove() .send(); +console.timeEnd('deploy'); + +// create first account + +console.time('create account'); +await Mina.transaction(sender, async () => { + await contract.createAccount(sender, UInt64.from(1000)); +}) + .sign([sender.key]) + .prove() + .send(); +console.timeEnd('create account'); // settle -let proof = await state.createSettlementProof(); +console.time('settlement proof 1'); +let proof = await offchainState.createSettlementProof(); +console.timeEnd('settlement proof 1'); +console.time('settle 1'); await Mina.transaction(sender, () => contract.settle(proof)) .sign([sender.key]) .prove() .send(); +console.timeEnd('settle 1'); // check balance and supply let balance = await contract.getBalance(receiver); @@ -107,21 +131,27 @@ console.log('supply', supply.toString()); // transfer +console.time('transfer'); await Mina.transaction(sender, () => contract.transfer(sender, receiver, UInt64.from(100)) ) .sign([sender.key]) .prove() .send(); +console.timeEnd('transfer'); // settle -proof = await state.createSettlementProof(); +console.time('settlement proof 2'); +proof = await offchainState.createSettlementProof(); +console.timeEnd('settlement proof 2'); +console.time('settle 2'); await Mina.transaction(sender, () => contract.settle(proof)) .sign([sender.key]) .prove() .send(); +console.timeEnd('settle 2'); // check balance and supply let balance2 = await contract.getBalance(receiver); diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index 614631a030..7104ea86ed 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -10,8 +10,9 @@ import { Provable } from '../../provable/provable.js'; import { AnyTuple } from '../../util/types.js'; import { assert } from '../../provable/gadgets/common.js'; import { ActionList, MerkleLeaf } from './offchain-state-serialization.js'; +import { MerkleMap } from '../../provable/merkle-map.js'; -export { OffchainStateRollup, MerkleMapState }; +export { OffchainStateRollup, OffchainStateCommitments }; class ActionIterator extends MerkleListIterator.create( ActionList.provable, @@ -21,13 +22,21 @@ class ActionIterator extends MerkleListIterator.create( Actions.emptyActionState() ) {} -class MerkleMapState extends Struct({ +class OffchainStateCommitments extends Struct({ // this should just be a MerkleTree type that carries the full tree as aux data root: Field, // TODO: make zkprogram support auxiliary data in public inputs // actionState: ActionIterator.provable, actionState: Field, -}) {} +}) { + static empty() { + let emptyMerkleRoot = new MerkleMap().getRoot(); + return new OffchainStateCommitments({ + root: emptyMerkleRoot, + actionState: Actions.emptyActionState(), + }); + } +} const TREE_HEIGHT = 256; class MerkleMapWitness extends MerkleWitness(TREE_HEIGHT) {} @@ -56,23 +65,26 @@ const merkleUpdateBatch = ( ] satisfies AnyTuple, async method( - stateA: MerkleMapState, + stateA: OffchainStateCommitments, actions: ActionIterator, tree: Unconstrained, isRecursive: Bool, - recursiveProof: SelfProof - ): Promise { + recursiveProof: SelfProof< + OffchainStateCommitments, + OffchainStateCommitments + > + ): Promise { // in the non-recursive case, this skips verifying the proof so we can pass in a dummy proof recursiveProof.verifyIf(isRecursive); // in the recursive case, the recursive proof's initial state has to match this proof's initial state // TODO maybe a dedicated `assertEqualIf()` is more efficient and readable Provable.assertEqual( - MerkleMapState, + OffchainStateCommitments, recursiveProof.publicInput, Provable.if( isRecursive, - MerkleMapState, + OffchainStateCommitments, stateA, recursiveProof.publicInput ) @@ -81,7 +93,7 @@ const merkleUpdateBatch = ( // the state we start with let stateB = Provable.if( isRecursive, - MerkleMapState, + OffchainStateCommitments, recursiveProof.publicOutput, stateA ); @@ -130,13 +142,13 @@ const merkleUpdateBatch = ( }); function OffchainStateRollup({ - maxUpdatesPerBatch = 10, - maxActionsPerUpdate = 5, + maxUpdatesPerBatch = 2, + maxActionsPerUpdate = 2, } = {}) { let offchainStateRollup = ZkProgram({ name: 'merkle-map-rollup', - publicInput: MerkleMapState, - publicOutput: MerkleMapState, + publicInput: OffchainStateCommitments, + publicOutput: OffchainStateCommitments, methods: { nextBatch: merkleUpdateBatch(maxUpdatesPerBatch, maxActionsPerUpdate), }, @@ -171,14 +183,14 @@ function OffchainStateRollup({ // input state let iterator = actions.startIterating(); - let inputState = new MerkleMapState({ + let inputState = new OffchainStateCommitments({ root: tree.getRoot(), actionState: iterator.currentHash, }); // dummy proof console.time('dummy'); - let dummyState = MerkleMapState.empty(); + let dummyState = OffchainStateCommitments.empty(); let dummy = await RollupProof.dummy(dummyState, dummyState, 1); console.timeEnd('dummy'); diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 7fbe3d6299..8457ade150 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -10,20 +10,20 @@ import { import { Field } from '../../provable/wrapped.js'; import { Proof } from '../../proof-system/zkprogram.js'; import { - MerkleMapState, + OffchainStateCommitments, OffchainStateRollup, } from './offchain-state-rollup.js'; import { Option } from '../../provable/option.js'; import { InferValue } from '../../../bindings/lib/provable-generic.js'; import { SmartContract } from '../zkapp.js'; import { assert } from '../../provable/gadgets/common.js'; -import { State, declareState } from '../state.js'; +import { State } from '../state.js'; import { Actions } from '../account-update.js'; import { MerkleMap, MerkleMapWitness } from '../../provable/merkle-map.js'; import { Provable } from '../../provable/provable.js'; import { Poseidon } from '../../provable/crypto/poseidon.js'; -export { OffchainState }; +export { OffchainState, OffchainStateCommitments }; type OffchainState = { /** @@ -41,24 +41,14 @@ type OffchainState = { [K in keyof Config]: OffchainStateIntf; }; - /** - * Set the contract class that this offchain state appliues to. - * - * Note: This declares two _onchain_ state fields on the contract, - * which it uses to keep commitments to the offchain state and processed actions. - * - * This means that the contract has only 6 remaining onchain state fields available. - * - * It also sets the reducer for this contract, so you can't use another reducer with this contract. - */ - setContractClass(contract: typeof SmartContract): void; - /** * Set the contract that this offchain state is connected with. * * This tells the offchain state about the account to fetch data from and modify, and lets it handle actions and onchain state. */ - setContractInstance(contract: SmartContract): void; + setContractInstance( + contract: SmartContract & { offchainState: State } + ): void; /** * Compile the offchain state ZkProgram. @@ -70,21 +60,25 @@ type OffchainState = { /** * Create a proof that the offchain state is in a valid state. */ - createSettlementProof(): Promise>; + createSettlementProof(): Promise< + Proof + >; /** * The custom proof class for state settlement proofs, that have to be passed into the settling method. */ - Proof: typeof Proof; + Proof: typeof Proof; /** * Settle the offchain state. */ - settle(proof: Proof): Promise; + settle( + proof: Proof + ): Promise; }; type OffchainStateContract = SmartContract & { - offchainState: State; + offchainState: State; }; function OffchainState< @@ -230,13 +224,8 @@ function OffchainState< } return { - setContractClass(contract) { - declareState(contract, { offchainState: MerkleMapState }); - }, - setContractInstance(contract) { - (contract as any).offchainState = State(); - internal._contract = contract as any; + internal._contract = contract; }, async compile() { @@ -272,7 +261,7 @@ function OffchainState< // check that proof moves state forward from the one currently storedö let state = internal.contract.offchainState.getAndRequireEquals(); - Provable.assertEqual(MerkleMapState, state, proof.publicInput); + Provable.assertEqual(OffchainStateCommitments, state, proof.publicInput); // require that proof uses the correct pending actions internal.contract.account.actionState.requireEquals( From 5ead7a528188e0a6d879db095f8bc960132ba66e Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 16:49:57 +0200 Subject: [PATCH 49/97] fix how we get contract --- src/lib/mina/actions/offchain-state.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 8457ade150..6f77e980fc 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -22,6 +22,7 @@ import { Actions } from '../account-update.js'; import { MerkleMap, MerkleMapWitness } from '../../provable/merkle-map.js'; import { Provable } from '../../provable/provable.js'; import { Poseidon } from '../../provable/crypto/poseidon.js'; +import { smartContractContext } from '../smart-contract-context.js'; export { OffchainState, OffchainStateCommitments }; @@ -123,12 +124,23 @@ function OffchainState< let rollup = OffchainStateRollup(); + function contract() { + let ctx = smartContractContext.get(); + assert(ctx !== null, 'get() must be called within a contract method'); + assert( + ctx.this.constructor === internal.contract.constructor, + 'Offchain state methods can only be called on the contract that you called setContractInstance() on' + ); + return ctx.this as OffchainStateContract; + } + /** * generic get which works for both fields and maps */ async function get(key: Field, valueType: Actionable) { // get onchain merkle root - let stateRoot = internal.contract.offchainState.getAndRequireEquals().root; + + let stateRoot = contract().offchainState.getAndRequireEquals().root; // witness the actual value const optionType = Option(valueType); @@ -180,7 +192,7 @@ function OffchainState< ); // push action on account update - let update = internal.contract.self; + let update = contract().self; Actions.pushEvent(update.body.actions, action); }, update: notImplemented, @@ -212,7 +224,7 @@ function OffchainState< ); // push action on account update - let update = internal.contract.self; + let update = contract().self; Actions.pushEvent(update.body.actions, action); }, update: notImplemented, @@ -260,16 +272,16 @@ function OffchainState< proof.verify(); // check that proof moves state forward from the one currently storedö - let state = internal.contract.offchainState.getAndRequireEquals(); + let state = contract().offchainState.getAndRequireEquals(); Provable.assertEqual(OffchainStateCommitments, state, proof.publicInput); // require that proof uses the correct pending actions - internal.contract.account.actionState.requireEquals( + contract().account.actionState.requireEquals( proof.publicOutput.actionState ); // update the state - internal.contract.offchainState.set(proof.publicOutput); + contract().offchainState.set(proof.publicOutput); }, fields: Object.fromEntries( From 0c516555d2c1b215f21efcd488c2618b658ba5b3 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 16:50:22 +0200 Subject: [PATCH 50/97] improve unconstrained + empty --- .../actions/offchain-state-serialization.ts | 2 +- src/lib/provable/types/unconstrained.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/lib/mina/actions/offchain-state-serialization.ts b/src/lib/mina/actions/offchain-state-serialization.ts index 8a22d45f0b..3f52dbb689 100644 --- a/src/lib/mina/actions/offchain-state-serialization.ts +++ b/src/lib/mina/actions/offchain-state-serialization.ts @@ -103,7 +103,7 @@ function hashPackedWithPrefix | undefined>( class MerkleLeaf extends Struct({ key: Field, value: Field, - prefix: Unconstrained.provable as Provable>, + prefix: Unconstrained.provableWithEmpty([]), }) { static fromAction(action: Field[]) { assert(action.length >= 2, 'invalid action size'); diff --git a/src/lib/provable/types/unconstrained.ts b/src/lib/provable/types/unconstrained.ts index 89fb648b8d..03782310a1 100644 --- a/src/lib/provable/types/unconstrained.ts +++ b/src/lib/provable/types/unconstrained.ts @@ -115,6 +115,7 @@ and Provable.asProver() blocks, which execute outside the proof. fields?: Field[]; packed?: [Field, number][]; }; + empty: () => Unconstrained; } = { sizeInFields: () => 0, toFields: () => [], @@ -124,5 +125,21 @@ and Provable.asProver() blocks, which execute outside the proof. toValue: (t) => t, fromValue: (t) => t, toInput: () => ({}), + empty: (): any => { + throw Error('There is no default empty value for Unconstrained.'); + }, }; + + static provableWithEmpty(empty: T): Provable> & { + toInput: (x: Unconstrained) => { + fields?: Field[]; + packed?: [Field, number][]; + }; + empty: () => Unconstrained; + } { + return { + ...Unconstrained.provable, + empty: () => Unconstrained.from(empty), + }; + } } From 75c6dd2b6e037308938d6032b0beb97681a8db00 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 17:24:43 +0200 Subject: [PATCH 51/97] refactor to two separate program methods for now --- src/lib/mina/actions/offchain-state-rollup.ts | 220 +++++++++--------- 1 file changed, 116 insertions(+), 104 deletions(-) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index 7104ea86ed..6274668189 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -1,5 +1,5 @@ -import { ZkProgram } from '../../proof-system/zkprogram.js'; -import { Field, Bool } from '../../provable/wrapped.js'; +import { Proof, ZkProgram } from '../../proof-system/zkprogram.js'; +import { Field } from '../../provable/wrapped.js'; import { Unconstrained } from '../../provable/types/unconstrained.js'; import { MerkleList, MerkleListIterator } from '../../provable/merkle-list.js'; import { Actions } from '../../../bindings/mina-transaction/transaction-leaves.js'; @@ -7,7 +7,6 @@ import { MerkleTree, MerkleWitness } from '../../provable/merkle-tree.js'; import { Struct } from '../../provable/types/struct.js'; import { SelfProof } from '../../proof-system/zkprogram.js'; import { Provable } from '../../provable/provable.js'; -import { AnyTuple } from '../../util/types.js'; import { assert } from '../../provable/gadgets/common.js'; import { ActionList, MerkleLeaf } from './offchain-state-serialization.js'; import { MerkleMap } from '../../provable/merkle-map.js'; @@ -44,103 +43,66 @@ class MerkleMapWitness extends MerkleWitness(TREE_HEIGHT) {} // TODO: it would be nice to abstract the logic for proving a chain of state transition proofs /** - * This function represents a proof that we can go from MerkleMapState A -> B - * One call of `merkleUpdateBatch()` either - * - creates an initial proof A -> B (this is the `isRecursive: false` case) - * - or, takes an existing proof A -> B, adds its own logic to prove B -> B', so that the output is a proof A -> B' + * Common logic for the proof that we can go from MerkleMapState A -> B */ -const merkleUpdateBatch = ( - maxUpdatesPerBatch: number, - maxActionsPerUpdate: number -) => ({ - privateInputs: [ - // the actions to process - ActionIterator.provable, - // the merkle tree to update - Unconstrained.provable, - // flag to set whether this is a recursive call - Bool, - // recursive proof for A -> B - SelfProof, - ] satisfies AnyTuple, - - async method( - stateA: OffchainStateCommitments, - actions: ActionIterator, - tree: Unconstrained, - isRecursive: Bool, - recursiveProof: SelfProof< - OffchainStateCommitments, - OffchainStateCommitments - > - ): Promise { - // in the non-recursive case, this skips verifying the proof so we can pass in a dummy proof - recursiveProof.verifyIf(isRecursive); - - // in the recursive case, the recursive proof's initial state has to match this proof's initial state - // TODO maybe a dedicated `assertEqualIf()` is more efficient and readable - Provable.assertEqual( - OffchainStateCommitments, - recursiveProof.publicInput, - Provable.if( - isRecursive, - OffchainStateCommitments, - stateA, - recursiveProof.publicInput - ) - ); - - // the state we start with - let stateB = Provable.if( - isRecursive, - OffchainStateCommitments, - recursiveProof.publicOutput, - stateA - ); - // this would be unnecessary if the iterator could just be the public input - actions.currentHash.assertEquals(stateB.actionState); - let root = stateB.root; - - // TODO: would be more efficient to linearize the actions first and then iterate over them, - // so we don't do the merkle lookup `maxActionsPerUpdate` times every time - // update merkle root for each action - for (let i = 0; i < maxUpdatesPerBatch; i++) { - actions.next().forEach(maxActionsPerUpdate, ({ key, value }, isDummy) => { - // merkle witness - let witness = Provable.witness( - MerkleMapWitness, - () => new MerkleMapWitness(tree.get().getWitness(key.toBigInt())) - ); - - // previous value at the key - let previousValue = Provable.witness(Field, () => - tree.get().getLeaf(key.toBigInt()) - ); +function merkleUpdateBatch( + { + maxUpdatesPerBatch, + maxActionsPerUpdate, + }: { + maxUpdatesPerBatch: number; + maxActionsPerUpdate: number; + }, + stateA: OffchainStateCommitments, + actions: ActionIterator, + tree: Unconstrained +): OffchainStateCommitments { + // this would be unnecessary if the iterator could just be the public input + actions.currentHash.assertEquals(stateA.actionState); + let root = stateA.root; + + // TODO: would be more efficient to linearize the actions first and then iterate over them, + // so we don't do the merkle lookup `maxActionsPerUpdate` times every time + // update merkle root for each action + for (let i = 0; i < maxUpdatesPerBatch; i++) { + actions.next().forEach(maxActionsPerUpdate, ({ key, value }, isDummy) => { + // merkle witness + let witness = Provable.witness( + MerkleMapWitness, + () => new MerkleMapWitness(tree.get().getWitness(key.toBigInt())) + ); - // prove that the witness is correct, by comparing the implied root and key - // note: this just works if the (key, value) is a (0,0) dummy, because the value at the 0 key will always be 0 - witness.calculateIndex().assertEquals(key); - witness.calculateRoot(previousValue).assertEquals(root); + // previous value at the key + let previousValue = Provable.witness(Field, () => + tree.get().getLeaf(key.toBigInt()) + ); - // store new value in at the key - let newRoot = witness.calculateRoot(value); + // prove that the witness is correct, by comparing the implied root and key + // note: this just works if the (key, value) is a (0,0) dummy, because the value at the 0 key will always be 0 + witness.calculateIndex().assertEquals(key); + witness.calculateRoot(previousValue).assertEquals(root); - // update the tree, outside the circuit (this should all be part of a better merkle tree API) - Provable.asProver(() => { - // ignore dummy value - if (isDummy.toBoolean()) return; - tree.get().setLeaf(key.toBigInt(), value); - }); + // store new value in at the key + let newRoot = witness.calculateRoot(value); - // update root - root = Provable.if(isDummy, root, newRoot); + // update the tree, outside the circuit (this should all be part of a better merkle tree API) + Provable.asProver(() => { + // ignore dummy value + if (isDummy.toBoolean()) return; + tree.get().setLeaf(key.toBigInt(), value); }); - } - return { root, actionState: actions.currentHash }; - }, -}); + // update root + root = Provable.if(isDummy, root, newRoot); + }); + } + + return { root, actionState: actions.currentHash }; +} +/** + * This program represents a proof that we can go from MerkleMapState A -> B + */ function OffchainStateRollup({ maxUpdatesPerBatch = 2, maxActionsPerUpdate = 2, @@ -150,7 +112,66 @@ function OffchainStateRollup({ publicInput: OffchainStateCommitments, publicOutput: OffchainStateCommitments, methods: { - nextBatch: merkleUpdateBatch(maxUpdatesPerBatch, maxActionsPerUpdate), + /** + * `firstBatch()` creates the initial proof A -> B + */ + firstBatch: { + // [actions, tree] + privateInputs: [ActionIterator.provable, Unconstrained.provable], + + async method( + stateA: OffchainStateCommitments, + actions: ActionIterator, + tree: Unconstrained + ): Promise { + return merkleUpdateBatch( + { maxUpdatesPerBatch, maxActionsPerUpdate }, + stateA, + actions, + tree + ); + }, + }, + /** + * `nextBatch()` takes an existing proof A -> B, adds its own logic to prove B -> B', so that the output is a proof A -> B' + */ + nextBatch: { + // [actions, tree, proof] + privateInputs: [ + ActionIterator.provable, + Unconstrained.provable, + SelfProof, + ], + + async method( + stateA: OffchainStateCommitments, + actions: ActionIterator, + tree: Unconstrained, + recursiveProof: Proof< + OffchainStateCommitments, + OffchainStateCommitments + > + ): Promise { + recursiveProof.verify(); + + // in the recursive case, the recursive proof's initial state has to match this proof's initial state + Provable.assertEqual( + OffchainStateCommitments, + recursiveProof.publicInput, + stateA + ); + + // the state we start with + let stateB = recursiveProof.publicOutput; + + return merkleUpdateBatch( + { maxUpdatesPerBatch, maxActionsPerUpdate }, + stateB, + actions, + tree + ); + }, + }, }, }); @@ -188,20 +209,12 @@ function OffchainStateRollup({ actionState: iterator.currentHash, }); - // dummy proof - console.time('dummy'); - let dummyState = OffchainStateCommitments.empty(); - let dummy = await RollupProof.dummy(dummyState, dummyState, 1); - console.timeEnd('dummy'); - // base proof console.time('batch 0'); - let proof = await offchainStateRollup.nextBatch( + let proof = await offchainStateRollup.firstBatch( inputState, iterator, - Unconstrained.from(tree), - Bool(false), - dummy + Unconstrained.from(tree) ); console.timeEnd('batch 0'); @@ -218,7 +231,6 @@ function OffchainStateRollup({ inputState, iterator, Unconstrained.from(tree), - Bool(true), proof ); console.timeEnd(`batch ${i}`); From 1e5b5a319ebcd481408ec79b73e80b5f85d40076 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 17:40:46 +0200 Subject: [PATCH 52/97] disable proofs --- .../actions/offchain-contract.unit-test.ts | 21 +++++++++++-------- src/lib/mina/actions/offchain-state-rollup.ts | 17 ++++++++++++++- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 379651736f..42d166b96d 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -72,20 +72,23 @@ class ExampleContract extends SmartContract { // test code below // setup +const proofsEnabled = false; -const Local = await Mina.LocalBlockchain(); +const Local = await Mina.LocalBlockchain({ proofsEnabled }); Mina.setActiveInstance(Local); let [sender, receiver, contractAccount] = Local.testAccounts; let contract = new ExampleContract(contractAccount); offchainState.setContractInstance(contract); -console.time('compile'); -await offchainState.compile(); -console.timeEnd('compile'); -console.time('compile contract'); -await ExampleContract.compile(); -console.timeEnd('compile contract'); +if (proofsEnabled) { + console.time('compile'); + await offchainState.compile(); + console.timeEnd('compile'); + console.time('compile contract'); + await ExampleContract.compile(); + console.timeEnd('compile contract'); +} // deploy and create first account @@ -123,10 +126,10 @@ await Mina.transaction(sender, () => contract.settle(proof)) console.timeEnd('settle 1'); // check balance and supply -let balance = await contract.getBalance(receiver); let supply = await contract.getSupply(); -console.log('balance', balance.toString()); +console.log('balance (sender)', (await contract.getBalance(sender)).toString()); +console.log('balance (recv)', (await contract.getBalance(receiver)).toString()); console.log('supply', supply.toString()); // transfer diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index 6274668189..160b08f17f 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -10,6 +10,7 @@ import { Provable } from '../../provable/provable.js'; import { assert } from '../../provable/gadgets/common.js'; import { ActionList, MerkleLeaf } from './offchain-state-serialization.js'; import { MerkleMap } from '../../provable/merkle-map.js'; +import { getProofsEnabled } from '../mina.js'; export { OffchainStateRollup, OffchainStateCommitments }; @@ -191,7 +192,7 @@ function OffchainStateRollup({ async prove(tree: MerkleTree, actions: MerkleList>) { assert(tree.height === TREE_HEIGHT, 'Tree height must match'); - await this.compile(); + if (getProofsEnabled()) await this.compile(); // clone the tree so we don't modify the input tree = tree.clone(); @@ -209,6 +210,20 @@ function OffchainStateRollup({ actionState: iterator.currentHash, }); + // if proofs are disabled, create a dummy proof and final state, and return + if (!getProofsEnabled()) { + tree = merkleUpdateOutside(actions, tree, { + maxUpdatesPerBatch, + maxActionsPerUpdate, + }); + let finalState = new OffchainStateCommitments({ + root: tree.getRoot(), + actionState: iterator.hash, + }); + let proof = await RollupProof.dummy(inputState, finalState, 2, 15); + return { proof, tree }; + } + // base proof console.time('batch 0'); let proof = await offchainStateRollup.firstBatch( From e9df9870ea1151811c96df0d6c28867e254233d1 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 17:52:58 +0200 Subject: [PATCH 53/97] suprisingly non mutating api? --- src/lib/mina/actions/offchain-state.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 6f77e980fc..cf58050594 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -193,7 +193,7 @@ function OffchainState< // push action on account update let update = contract().self; - Actions.pushEvent(update.body.actions, action); + update.body.actions = Actions.pushEvent(update.body.actions, action); }, update: notImplemented, async get() { @@ -225,7 +225,7 @@ function OffchainState< // push action on account update let update = contract().self; - Actions.pushEvent(update.body.actions, action); + update.body.actions = Actions.pushEvent(update.body.actions, action); }, update: notImplemented, async get(key) { From fc7e633f98d4da14ce8335c0c7dbfb0695476f22 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 18:34:43 +0200 Subject: [PATCH 54/97] use merkle tree, not map (set leaf is different) --- .../mina/actions/offchain-contract.unit-test.ts | 6 +++--- .../mina/actions/offchain-state-serialization.ts | 11 +++++------ src/lib/mina/actions/offchain-state.ts | 15 +++++++++------ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 42d166b96d..e4d9417659 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -72,7 +72,7 @@ class ExampleContract extends SmartContract { // test code below // setup -const proofsEnabled = false; +const proofsEnabled = true; const Local = await Mina.LocalBlockchain({ proofsEnabled }); Mina.setActiveInstance(Local); @@ -157,8 +157,8 @@ await Mina.transaction(sender, () => contract.settle(proof)) console.timeEnd('settle 2'); // check balance and supply -let balance2 = await contract.getBalance(receiver); let supply2 = await contract.getSupply(); -console.log('balance', balance2.toString()); +console.log('balance (sender)', (await contract.getBalance(sender)).toString()); +console.log('balance (recv)', (await contract.getBalance(receiver)).toString()); console.log('supply', supply2.toString()); diff --git a/src/lib/mina/actions/offchain-state-serialization.ts b/src/lib/mina/actions/offchain-state-serialization.ts index 3f52dbb689..cb6a26655d 100644 --- a/src/lib/mina/actions/offchain-state-serialization.ts +++ b/src/lib/mina/actions/offchain-state-serialization.ts @@ -24,7 +24,7 @@ import * as Mina from '../mina.js'; import { PublicKey } from '../../provable/crypto/signature.js'; import { Provable } from '../../provable/provable.js'; import { Actions } from '../account-update.js'; -import { MerkleMap } from '../../provable/merkle-map.js'; +import { MerkleTree } from '../../provable/merkle-tree.js'; export { toKeyHash, @@ -45,8 +45,7 @@ function toKeyHash | undefined>( keyType: KeyType, key: KeyType extends undefined ? undefined : K ): Field { - let keySize = keyType?.sizeInFields() ?? 0; - return hashPackedWithPrefix([prefix, Field(keySize)], keyType, key); + return hashPackedWithPrefix([prefix, Field(0)], keyType, key); } function toAction | undefined>( @@ -179,7 +178,7 @@ async function fetchMerkleLeaves( async function fetchMerkleMap( contract: { address: PublicKey; tokenId: Field }, endActionState?: Field -): Promise<{ merkleMap: MerkleMap; valueMap: Map }> { +): Promise<{ merkleMap: MerkleTree; valueMap: Map }> { let result = await Mina.fetchActions( contract.address, { endActionState }, @@ -195,11 +194,11 @@ async function fetchMerkleMap( ) .flat(); - let merkleMap = new MerkleMap(); + let merkleMap = new MerkleTree(256); let valueMap = new Map(); for (let leaf of leaves) { - merkleMap.set(leaf.key, leaf.value); + merkleMap.setLeaf(leaf.key.toBigInt(), leaf.value); valueMap.set(leaf.key.toBigInt(), leaf.prefix.get()); } diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index cf58050594..4653b2bc47 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -19,10 +19,10 @@ import { SmartContract } from '../zkapp.js'; import { assert } from '../../provable/gadgets/common.js'; import { State } from '../state.js'; import { Actions } from '../account-update.js'; -import { MerkleMap, MerkleMapWitness } from '../../provable/merkle-map.js'; import { Provable } from '../../provable/provable.js'; import { Poseidon } from '../../provable/crypto/poseidon.js'; import { smartContractContext } from '../smart-contract-context.js'; +import { MerkleTree, MerkleWitness } from '../../provable/merkle-tree.js'; export { OffchainState, OffchainStateCommitments }; @@ -82,13 +82,15 @@ type OffchainStateContract = SmartContract & { offchainState: State; }; +const MerkleWitness256 = MerkleWitness(256); + function OffchainState< const Config extends { [key: string]: OffchainStateKind } >(config: Config): OffchainState { // setup internal state of this "class" let internal = { _contract: undefined as OffchainStateContract | undefined, - _merkleMap: undefined as MerkleMap | undefined, + _merkleMap: undefined as MerkleTree | undefined, _valueMap: undefined as Map | undefined, get contract() { @@ -155,9 +157,9 @@ function OffchainState< }); // witness a merkle witness - let witness = await Provable.witnessAsync(MerkleMapWitness, async () => { + let witness = await Provable.witnessAsync(MerkleWitness256, async () => { let { merkleMap } = await merkleMaps(); - return merkleMap.getWitness(key); + return new MerkleWitness256(merkleMap.getWitness(key.toBigInt())); }); // anchor the value against the onchain root and passed in key @@ -167,7 +169,8 @@ function OffchainState< Poseidon.hashPacked(valueType, value.value), Field(0) ); - let [actualRoot, actualKey] = witness.computeRootAndKey(valueHash); + let actualKey = witness.calculateIndex(); + let actualRoot = witness.calculateRoot(valueHash); key.assertEquals(actualKey, 'key mismatch'); stateRoot.assertEquals(actualRoot, 'root mismatch'); @@ -253,7 +256,7 @@ function OffchainState< fromActionState: actionState, }); - let result = await rollup.prove(merkleMap.tree, actions); + let result = await rollup.prove(merkleMap, actions); // update internal merkle maps as well // TODO make this not insanely recompute everything From d7502aad5417c4be8cd6c8350b5b565be97d6494 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 18:42:05 +0200 Subject: [PATCH 55/97] make unit test more test-like --- .../actions/offchain-contract.unit-test.ts | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index e4d9417659..547f4e57ff 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -3,6 +3,7 @@ import { PublicKey } from '../../provable/crypto/signature.js'; import { UInt64 } from '../../provable/int.js'; import { SmartContract, method } from '../zkapp.js'; import { Mina, State, state } from '../../../index.js'; +import assert from 'assert'; const offchainState = OffchainState({ accounts: OffchainState.Map(PublicKey, UInt64), @@ -38,19 +39,15 @@ class ExampleContract extends SmartContract { let toOption = await offchainState.fields.accounts.get(to); let toBalance = toOption.orElse(0n); - // TODO we use `update` here so that previous balances can't be overridden - // but this still includes a trivial double-spend opportunity, because the updates are not rejected atomically: - // if the `to` update gets accepted but the `from` update fails, it's a double-spend + /** + * FIXME using `set()` here is completely insecure, a sender can easily double-spend by sending multiple transactions, + * which will all use the same initial balance. + * Even using a naive version of `update()` would give a double-spend opportunity, because the updates are not rejected atomically: + * if the `to` update gets accepted but the `from` update fails, it's a double-spend + * => properly implementing this needs a version of `update()` that rejects all state actions in one update if any of them fails! + */ offchainState.fields.accounts.set(from, fromBalance.sub(amount)); - // state.fields.accounts.update(from, { - // from: fromBalance, - // to: fromBalance.sub(amount), - // }); offchainState.fields.accounts.set(to, toBalance.add(amount)); - // state.fields.accounts.update(to, { - // from: toBalance, - // to: toBalance.add(amount), - // }); } @method.returns(UInt64) @@ -82,9 +79,9 @@ let contract = new ExampleContract(contractAccount); offchainState.setContractInstance(contract); if (proofsEnabled) { - console.time('compile'); + console.time('compile program'); await offchainState.compile(); - console.timeEnd('compile'); + console.timeEnd('compile program'); console.time('compile contract'); await ExampleContract.compile(); console.timeEnd('compile contract'); @@ -126,11 +123,7 @@ await Mina.transaction(sender, () => contract.settle(proof)) console.timeEnd('settle 1'); // check balance and supply -let supply = await contract.getSupply(); - -console.log('balance (sender)', (await contract.getBalance(sender)).toString()); -console.log('balance (recv)', (await contract.getBalance(receiver)).toString()); -console.log('supply', supply.toString()); +await checkAgainstSupply(1000n); // transfer @@ -157,8 +150,18 @@ await Mina.transaction(sender, () => contract.settle(proof)) console.timeEnd('settle 2'); // check balance and supply -let supply2 = await contract.getSupply(); +await checkAgainstSupply(1000n); + +// test helper + +async function checkAgainstSupply(expectedSupply: bigint) { + let supply = (await contract.getSupply()).toBigInt(); + assert.strictEqual(supply, expectedSupply); -console.log('balance (sender)', (await contract.getBalance(sender)).toString()); -console.log('balance (recv)', (await contract.getBalance(receiver)).toString()); -console.log('supply', supply2.toString()); + let balanceSender = (await contract.getBalance(sender)).toBigInt(); + let balanceReceiver = (await contract.getBalance(receiver)).toBigInt(); + + console.log('balance (sender)', balanceSender); + console.log('balance (recv)', balanceReceiver); + assert.strictEqual(balanceSender + balanceReceiver, supply); +} From cda0e1f2489123e5acde0bdcd0ec64f6baddfa83 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 13 May 2024 18:49:46 +0200 Subject: [PATCH 56/97] fixup contracts without state --- src/lib/mina/state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/mina/state.ts b/src/lib/mina/state.ts index 799e46bcd0..d6c96980e5 100644 --- a/src/lib/mina/state.ts +++ b/src/lib/mina/state.ts @@ -372,7 +372,7 @@ function getLayoutPosition({ function getLayout(scClass: typeof SmartContract) { let sc = smartContracts.get(scClass); - if (sc === undefined) throw Error('bug'); + if (sc === undefined) return new Map(); if (sc.layout === undefined) { let layout = new Map(); sc.layout = layout; From 44d5616e72362af290b36a346582e6067ac98a5a Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 12:12:08 +0200 Subject: [PATCH 57/97] some boyscouting --- src/lib/mina/actions/offchain-state-rollup.ts | 7 +++---- .../mina/actions/offchain-state-serialization.ts | 4 ++-- src/lib/mina/actions/offchain-state.ts | 16 +++++++++------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index 160b08f17f..077cbd9fb3 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -44,7 +44,7 @@ class MerkleMapWitness extends MerkleWitness(TREE_HEIGHT) {} // TODO: it would be nice to abstract the logic for proving a chain of state transition proofs /** - * Common logic for the proof that we can go from MerkleMapState A -> B + * Common logic for the proof that we can go from OffchainStateCommitments A -> B */ function merkleUpdateBatch( { @@ -102,7 +102,7 @@ function merkleUpdateBatch( } /** - * This program represents a proof that we can go from MerkleMapState A -> B + * This program represents a proof that we can go from OffchainStateCommitments A -> B */ function OffchainStateRollup({ maxUpdatesPerBatch = 2, @@ -182,6 +182,7 @@ function OffchainStateRollup({ return { Proof: RollupProof, + program: offchainStateRollup, async compile() { if (isCompiled) return; @@ -253,8 +254,6 @@ function OffchainStateRollup({ return { proof, tree }; }, - - program: offchainStateRollup, }; } diff --git a/src/lib/mina/actions/offchain-state-serialization.ts b/src/lib/mina/actions/offchain-state-serialization.ts index cb6a26655d..455a1e7f08 100644 --- a/src/lib/mina/actions/offchain-state-serialization.ts +++ b/src/lib/mina/actions/offchain-state-serialization.ts @@ -55,8 +55,8 @@ function toAction | undefined>( key: KeyType extends undefined ? undefined : K, value: V ): Action { - let combinedSize = valueType.sizeInFields(); - let padding = combinedSize % 2 === 0 ? [] : [Field(0)]; + let valueSize = valueType.sizeInFields(); + let padding = valueSize % 2 === 0 ? [] : [Field(0)]; let keyHash = hashPackedWithPrefix([prefix, Field(0)], keyType, key); let valueHash = Poseidon.hashPacked(valueType, value); diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 4653b2bc47..9c8bb07d43 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -53,13 +53,11 @@ type OffchainState = { /** * Compile the offchain state ZkProgram. - * - * Note: If this is not done explicitly, it will be done before creating the first proof automatically. */ compile(): Promise; /** - * Create a proof that the offchain state is in a valid state. + * Create a proof that updates the commitments to offchain state: Merkle root and action state. */ createSettlementProof(): Promise< Proof @@ -128,10 +126,13 @@ function OffchainState< function contract() { let ctx = smartContractContext.get(); - assert(ctx !== null, 'get() must be called within a contract method'); + assert( + ctx !== null, + 'Offchain state methods must be called within a contract method' + ); assert( ctx.this.constructor === internal.contract.constructor, - 'Offchain state methods can only be called on the contract that you called setContractInstance() on' + 'Offchain state methods can only be called on the same contract that you called setContractInstance() on' ); return ctx.this as OffchainStateContract; } @@ -141,7 +142,6 @@ function OffchainState< */ async function get(key: Field, valueType: Actionable) { // get onchain merkle root - let stateRoot = contract().offchainState.getAndRequireEquals().root; // witness the actual value @@ -260,6 +260,8 @@ function OffchainState< // update internal merkle maps as well // TODO make this not insanely recompute everything + // - take new tree from `result` + // - update value map in `prove()`, or separately based on `actions` let { merkleMap: newMerkleMap, valueMap: newValueMap } = await fetchMerkleMap(internal.contract); internal._merkleMap = newMerkleMap; @@ -274,7 +276,7 @@ function OffchainState< // verify the proof proof.verify(); - // check that proof moves state forward from the one currently storedö + // check that proof moves state forward from the one currently stored let state = contract().offchainState.getAndRequireEquals(); Provable.assertEqual(OffchainStateCommitments, state, proof.publicInput); From 0df6f4eb7f00f385be62ebe5d32c1c161886bc9e Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 12:16:18 +0200 Subject: [PATCH 58/97] export option type --- src/index.ts | 1 + src/lib/mina/actions/offchain-state.ts | 2 +- src/lib/provable/option.ts | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 374e629dad..f58de89974 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,6 +52,7 @@ export { Gadgets } from './lib/provable/gadgets/gadgets.js'; export { Types } from './bindings/mina-transaction/types.js'; export { MerkleList, MerkleListIterator } from './lib/provable/merkle-list.js'; +export { Option } from './lib/provable/option.js'; export * as Mina from './lib/mina/mina.js'; export { diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 9c8bb07d43..afe1d5005e 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -150,7 +150,7 @@ function OffchainState< let { valueMap } = await merkleMaps(); let valueFields = valueMap.get(key.toBigInt()); if (valueFields === undefined) { - return optionType.from(); + return optionType.none(); } let value = fromActionWithoutHashes(valueType, valueFields); return optionType.from(value); diff --git a/src/lib/provable/option.ts b/src/lib/provable/option.ts index e207658271..c5d540d908 100644 --- a/src/lib/provable/option.ts +++ b/src/lib/provable/option.ts @@ -18,6 +18,7 @@ function Option( { isSome: boolean; value: V } > & { from(value?: T): Option; + none(): Option; } { const Super = Struct({ isSome: Bool, value: type }); return class Option_ extends Super { @@ -40,6 +41,9 @@ function Option( ? new Option_({ isSome: Bool(false), value: emptyValue(type) }) : new Option_({ isSome: Bool(true), value: type.fromValue(value) }); } + static none() { + return Option_.from(undefined); + } static fromFields(fields: any[], aux?: any): Option_ { return new Option_(Super.fromFields(fields, aux)); From 744251ece929b4b48573046ef4902690ea4bc1e3 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 12:57:18 +0200 Subject: [PATCH 59/97] option docs, initial state example --- src/examples/simple-zkapp.ts | 3 +-- src/lib/provable/option.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/examples/simple-zkapp.ts b/src/examples/simple-zkapp.ts index 337b3a4762..349dfbd3d4 100644 --- a/src/examples/simple-zkapp.ts +++ b/src/examples/simple-zkapp.ts @@ -18,14 +18,13 @@ const doProofs = true; const beforeGenesis = UInt64.from(Date.now()); class SimpleZkapp extends SmartContract { - @state(Field) x = State(); + @state(Field) x = State(initialState); events = { update: Field, payout: UInt64, payoutReceiver: PublicKey }; @method async init() { super.init(); - this.x.set(initialState); } @method.returns(Field) diff --git a/src/lib/provable/option.ts b/src/lib/provable/option.ts index c5d540d908..e214f99ae4 100644 --- a/src/lib/provable/option.ts +++ b/src/lib/provable/option.ts @@ -10,6 +10,22 @@ type Option = { isSome: Bool; value: T } & { orElse(defaultValue: T | V): T; }; +/** + * Define an optional version of a provable type. + * + * @example + * ```ts + * class OptionUInt64 extends Option(UInt64) {} + * + * // create an optional UInt64 + * let some = OptionUInt64.from(5n); + * let none = OptionUInt64.none(); + * + * // get back a UInt64 + * let five: UInt64 = some.assertSome('must have a value'); + * let zero: UInt64 = none.orElse(0n); // specify a default value + * ``` + */ function Option( type: Provable ): Provable< From 28a4e69f927a21a431da38ead5db6f9ec0fb1d0b Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 12:58:23 +0200 Subject: [PATCH 60/97] changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7847fcdb57..3215c7dd3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,9 +19,20 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added +- `Option` for defining an optional version of any provable type https://github.com/o1-labs/o1js/pull/1630 +- `MerkleTree.clone()` and `MerkleTree.getLeaf()`, new convenience methods for merkle trees https://github.com/o1-labs/o1js/pull/1630 +- `MerkleList.forEach()`, a simple and safe way for iterating over a `MerkleList` +- `Unconstrained.provableWithEmpty()` to create an unconstrained provable type with a known `empty()` value https://github.com/o1-labs/o1js/pull/1630 - `Permissions.VerificationKey`, a namespace for verification key permissions https://github.com/o1-labs/o1js/pull/1639 - Includes more accurate names for the `impossible` and `proof` permissions for verification keys, which are now called `impossibleDuringCurrentVersion` and `proofDuringCurrentVersion` respectively. +### Changed + +- `State()` now optionally accepts an initial value as input parameter https://github.com/o1-labs/o1js/pull/1630 + - Example: `@state(Field) x = State(Field(1));` + - Initial values will be set in the default `init()` method + - You no longer need a custom `init()` method to set initial values + ### Fixes - Fix absolute imports which prevented compilation in some TS projects that used o1js https://github.com/o1-labs/o1js/pull/1628 From 467f0a06c4bc0a960a8f197a6ea88277e556d9d5 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 13:47:37 +0200 Subject: [PATCH 61/97] increase updates per batch in rollup --- src/lib/mina/actions/offchain-state-rollup.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index 077cbd9fb3..b362703fb7 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -105,7 +105,9 @@ function merkleUpdateBatch( * This program represents a proof that we can go from OffchainStateCommitments A -> B */ function OffchainStateRollup({ - maxUpdatesPerBatch = 2, + // 1 actions uses about 7.5k constraints + // we can fit 7 * 7.5k = 52.5k constraints in one method next to the proof verification + maxUpdatesPerBatch = 3, maxActionsPerUpdate = 2, } = {}) { let offchainStateRollup = ZkProgram({ From 7346f0c06bce060a0d45b0e384399707134f0042 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 16:11:36 +0200 Subject: [PATCH 62/97] better assertAtEnd error message --- src/lib/provable/merkle-list.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/lib/provable/merkle-list.ts b/src/lib/provable/merkle-list.ts index bb1173837e..6144b42e53 100644 --- a/src/lib/provable/merkle-list.ts +++ b/src/lib/provable/merkle-list.ts @@ -209,7 +209,9 @@ class MerkleList implements MerkleListBase { let { element, isDummy } = iter.Unsafe.next(); callback(element, isDummy, i); } - iter.assertAtEnd(); + iter.assertAtEnd( + `Expected MerkleList to have at most ${length} elements, but it has more.` + ); } startIterating(): MerkleListIterator { @@ -394,8 +396,11 @@ class MerkleListIterator implements MerkleListIteratorBase { this.currentHash = Provable.if(condition, this.hash, this.currentHash); } - assertAtEnd() { - return this.currentHash.assertEquals(this.hash); + assertAtEnd(message?: string) { + return this.currentHash.assertEquals( + this.hash, + message ?? 'Merkle list iterator is not at the end' + ); } isAtStart() { From da7737319cb41ffa735d22970c7661c10a217a0c Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 16:12:48 +0200 Subject: [PATCH 63/97] linearize actions before processing, to avoid spending constraints on dummies --- src/lib/mina/actions/offchain-state-rollup.ts | 159 ++++++++++-------- 1 file changed, 92 insertions(+), 67 deletions(-) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index b362703fb7..8e10b1fbad 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -48,10 +48,10 @@ class MerkleMapWitness extends MerkleWitness(TREE_HEIGHT) {} */ function merkleUpdateBatch( { - maxUpdatesPerBatch, + maxActionsPerBatch, maxActionsPerUpdate, }: { - maxUpdatesPerBatch: number; + maxActionsPerBatch: number; maxActionsPerUpdate: number; }, stateA: OffchainStateCommitments, @@ -62,41 +62,47 @@ function merkleUpdateBatch( actions.currentHash.assertEquals(stateA.actionState); let root = stateA.root; - // TODO: would be more efficient to linearize the actions first and then iterate over them, - // so we don't do the merkle lookup `maxActionsPerUpdate` times every time - // update merkle root for each action - for (let i = 0; i < maxUpdatesPerBatch; i++) { - actions.next().forEach(maxActionsPerUpdate, ({ key, value }, isDummy) => { - // merkle witness - let witness = Provable.witness( - MerkleMapWitness, - () => new MerkleMapWitness(tree.get().getWitness(key.toBigInt())) - ); - - // previous value at the key - let previousValue = Provable.witness(Field, () => - tree.get().getLeaf(key.toBigInt()) - ); + // linearize actions into a flat MerkleList, so we don't process an insane amount of dummy actions + let linearActions = ActionList.empty(); - // prove that the witness is correct, by comparing the implied root and key - // note: this just works if the (key, value) is a (0,0) dummy, because the value at the 0 key will always be 0 - witness.calculateIndex().assertEquals(key); - witness.calculateRoot(previousValue).assertEquals(root); - - // store new value in at the key - let newRoot = witness.calculateRoot(value); - - // update the tree, outside the circuit (this should all be part of a better merkle tree API) - Provable.asProver(() => { - // ignore dummy value - if (isDummy.toBoolean()) return; - tree.get().setLeaf(key.toBigInt(), value); - }); - - // update root - root = Provable.if(isDummy, root, newRoot); + for (let i = 0; i < maxActionsPerBatch; i++) { + actions.next().forEach(maxActionsPerUpdate, (action, isDummy) => { + linearActions.pushIf(isDummy.not(), action); }); } + actions.assertAtEnd(); + + // update merkle root for each action + linearActions.forEach(maxActionsPerBatch, ({ key, value }, isDummy) => { + // merkle witness + let witness = Provable.witness( + MerkleMapWitness, + () => new MerkleMapWitness(tree.get().getWitness(key.toBigInt())) + ); + + // previous value at the key + let previousValue = Provable.witness(Field, () => + tree.get().getLeaf(key.toBigInt()) + ); + + // prove that the witness is correct, by comparing the implied root and key + // note: this just works if the (key, value) is a (0,0) dummy, because the value at the 0 key will always be 0 + witness.calculateIndex().assertEquals(key); + witness.calculateRoot(previousValue).assertEquals(root); + + // store new value in at the key + let newRoot = witness.calculateRoot(value); + + // update the tree, outside the circuit (this should all be part of a better merkle tree API) + Provable.asProver(() => { + // ignore dummy value + if (isDummy.toBoolean()) return; + tree.get().setLeaf(key.toBigInt(), value); + }); + + // update root + root = Provable.if(isDummy, root, newRoot); + }); return { root, actionState: actions.currentHash }; } @@ -105,10 +111,13 @@ function merkleUpdateBatch( * This program represents a proof that we can go from OffchainStateCommitments A -> B */ function OffchainStateRollup({ - // 1 actions uses about 7.5k constraints - // we can fit 7 * 7.5k = 52.5k constraints in one method next to the proof verification - maxUpdatesPerBatch = 3, - maxActionsPerUpdate = 2, + // 1 action uses about 7.5k constraints + // we can fit at most 7 * 7.5k = 52.5k constraints in one method next to proof verification + // => we use `maxActionsPerBatch = 6` to safely stay below the constraint limit + // the second parameter `maxActionsPerUpdate` only weakly affects # constraints, but has to be <= `maxActionsPerBatch` + // => so we set it to the same value + maxActionsPerBatch = 6, + maxActionsPerUpdate = 6, } = {}) { let offchainStateRollup = ZkProgram({ name: 'merkle-map-rollup', @@ -128,7 +137,7 @@ function OffchainStateRollup({ tree: Unconstrained ): Promise { return merkleUpdateBatch( - { maxUpdatesPerBatch, maxActionsPerUpdate }, + { maxActionsPerBatch, maxActionsPerUpdate }, stateA, actions, tree @@ -168,7 +177,7 @@ function OffchainStateRollup({ let stateB = recursiveProof.publicOutput; return merkleUpdateBatch( - { maxUpdatesPerBatch, maxActionsPerUpdate }, + { maxActionsPerBatch, maxActionsPerUpdate }, stateB, actions, tree @@ -199,15 +208,8 @@ function OffchainStateRollup({ // clone the tree so we don't modify the input tree = tree.clone(); - let n = actions.data.get().length; - let nBatches = Math.ceil(n / maxUpdatesPerBatch); - - // if there are no actions, we still need to create a valid proof for the empty transition - if (n === 0) nBatches = 1; - // input state let iterator = actions.startIterating(); - let inputState = new OffchainStateCommitments({ root: tree.getRoot(), actionState: iterator.currentHash, @@ -215,10 +217,7 @@ function OffchainStateRollup({ // if proofs are disabled, create a dummy proof and final state, and return if (!getProofsEnabled()) { - tree = merkleUpdateOutside(actions, tree, { - maxUpdatesPerBatch, - maxActionsPerUpdate, - }); + tree = merkleUpdateOutside(actions, tree); let finalState = new OffchainStateCommitments({ root: tree.getRoot(), actionState: iterator.hash, @@ -229,25 +228,23 @@ function OffchainStateRollup({ // base proof console.time('batch 0'); + let slice = sliceActions(iterator, maxActionsPerBatch); let proof = await offchainStateRollup.firstBatch( inputState, - iterator, + slice, Unconstrained.from(tree) ); console.timeEnd('batch 0'); // recursive proofs - for (let i = 1; i < nBatches; i++) { - // update iterator (would be nice if the method call would just return the updated one) - iterator.currentHash = proof.publicOutput.actionState; - for (let j = 0; j < maxUpdatesPerBatch; j++) { - iterator._updateIndex('next'); - } + for (let i = 1; ; i++) { + if (iterator.isAtEnd().toBoolean()) break; console.time(`batch ${i}`); + let slice = sliceActions(iterator, maxActionsPerBatch); proof = await offchainStateRollup.nextBatch( inputState, - iterator, + slice, Unconstrained.from(tree), proof ); @@ -259,21 +256,49 @@ function OffchainStateRollup({ }; } +// from a nested list of actions, create a slice (iterator) starting at `index` that has at most `batchSize` actions in it\ +// also moves the original iterator forward to start after the slice +function sliceActions(actions: ActionIterator, batchSize: number) { + class ActionListsList extends MerkleList.create( + ActionList.provable, + (hash: Field, actions: ActionList) => + Actions.updateSequenceState(hash, actions.hash), + actions.currentHash + ) {} + + let slice = ActionListsList.empty(); + let totalSize = 0; + + while (true) { + // stop if we reach the end of the list + if (actions.isAtEnd().toBoolean()) break; + + let nextList = actions.data.get()[actions._index('next')].element; + let nextSize = nextList.data.get().length; + assert( + nextSize <= batchSize, + 'Actions in one update exceed maximum batch size' + ); + if (totalSize + nextSize > batchSize) break; + + let nextMerkleList = actions.next(); + slice.push(nextMerkleList); + totalSize += nextSize; + } + + return slice.startIterating(); +} + // TODO: do we have to repeat the merkle updates outside the circuit? function merkleUpdateOutside( actions: MerkleList>, - tree: MerkleTree, - { maxUpdatesPerBatch = 10, maxActionsPerUpdate = 5 } = {} + tree: MerkleTree ) { tree = tree.clone(); - actions.forEach(maxUpdatesPerBatch, (actionsList, isDummy) => { - if (isDummy.toBoolean()) return; - - actionsList.forEach(maxActionsPerUpdate, ({ key, value }, isDummy) => { - if (isDummy.toBoolean()) return; - + actions.data.get().forEach(({ element: actionsList }) => { + actionsList.data.get().forEach(({ element: { key, value } }) => { tree.setLeaf(key.toBigInt(), value); }); }); From 0ba482c90a273b936b6fd94866eb03f2ab5c035a Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 16:57:03 +0200 Subject: [PATCH 64/97] add two more fields to merkle leaves --- .../actions/offchain-state-serialization.ts | 64 +++++++++++++++---- src/lib/mina/actions/offchain-state.ts | 18 +++--- 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/src/lib/mina/actions/offchain-state-serialization.ts b/src/lib/mina/actions/offchain-state-serialization.ts index 455a1e7f08..277a42afab 100644 --- a/src/lib/mina/actions/offchain-state-serialization.ts +++ b/src/lib/mina/actions/offchain-state-serialization.ts @@ -14,7 +14,7 @@ import { packToFields, salt, } from '../../provable/crypto/poseidon.js'; -import { Field } from '../../provable/wrapped.js'; +import { Field, Bool } from '../../provable/wrapped.js'; import { assert } from '../../provable/gadgets/common.js'; import { prefixes } from '../../../bindings/crypto/constants.js'; import { Struct } from '../../provable/types/struct.js'; @@ -48,19 +48,41 @@ function toKeyHash | undefined>( return hashPackedWithPrefix([prefix, Field(0)], keyType, key); } -function toAction | undefined>( - prefix: Field, - keyType: KeyType, - valueType: Actionable, - key: KeyType extends undefined ? undefined : K, - value: V -): Action { +function toAction | undefined>({ + prefix, + keyType, + valueType, + key, + value, + previousValue, +}: { + prefix: Field; + keyType: KeyType; + valueType: Actionable; + key: KeyType extends undefined ? undefined : K; + value: V; + previousValue?: V; +}): Action { let valueSize = valueType.sizeInFields(); let padding = valueSize % 2 === 0 ? [] : [Field(0)]; let keyHash = hashPackedWithPrefix([prefix, Field(0)], keyType, key); + + let usesPreviousValue = Bool(previousValue !== undefined).toField(); + let previousValueHash = + previousValue !== undefined + ? Poseidon.hashPacked(valueType, previousValue) + : Field(0); let valueHash = Poseidon.hashPacked(valueType, value); - return [...valueType.toFields(value), ...padding, keyHash, valueHash]; + + return [ + ...valueType.toFields(value), + ...padding, + usesPreviousValue, + previousValueHash, + keyHash, + valueHash, + ]; } function fromActionWithoutHashes( @@ -102,13 +124,22 @@ function hashPackedWithPrefix | undefined>( class MerkleLeaf extends Struct({ key: Field, value: Field, + usesPreviousValue: Bool, + previousValue: Field, prefix: Unconstrained.provableWithEmpty([]), }) { static fromAction(action: Field[]) { - assert(action.length >= 2, 'invalid action size'); - let [key, value] = action.slice(-2); - let prefix = Unconstrained.from(action.slice(0, -2)); - return new MerkleLeaf({ key, value, prefix }); + assert(action.length >= 4, 'invalid action size'); + let [usesPreviousValue_, previousValue, key, value] = action.slice(-4); + let usesPreviousValue = usesPreviousValue_.assertBool(); + let prefix = Unconstrained.from(action.slice(0, -4)); + return new MerkleLeaf({ + usesPreviousValue, + previousValue, + key, + value, + prefix, + }); } /** @@ -122,7 +153,12 @@ class MerkleLeaf extends Struct({ let init = salt(prefixes.event) as [Field, Field, Field]; return Poseidon.update(init, prefix); }); - return Poseidon.update(preHashState, [action.key, action.value])[0]; + return Poseidon.update(preHashState, [ + action.usesPreviousValue.toField(), + action.previousValue, + action.key, + action.value, + ])[0]; } } diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index afe1d5005e..c0cfc0f0d8 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -186,13 +186,13 @@ function OffchainState< return { set(value) { // serialize into action - let action = toAction( + let action = toAction({ prefix, - undefined, - type, - undefined, - type.fromValue(value) - ); + keyType: undefined, + valueType: type, + key: undefined, + value: type.fromValue(value), + }); // push action on account update let update = contract().self; @@ -218,13 +218,13 @@ function OffchainState< return { set(key, value) { // serialize into action - let action = toAction( + let action = toAction({ prefix, keyType, valueType, key, - valueType.fromValue(value) - ); + value: valueType.fromValue(value), + }); // push action on account update let update = contract().self; From 4b1a34f9041b5900102ba1d6bf5fb9b915c13fe7 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 18:29:15 +0200 Subject: [PATCH 65/97] restructure rollup proof to commit merkle tree updates atomically, at checkpoints --- src/lib/mina/actions/offchain-state-rollup.ts | 75 +++++++++++++++---- .../actions/offchain-state-serialization.ts | 38 ++++++++++ 2 files changed, 98 insertions(+), 15 deletions(-) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index 8e10b1fbad..93b312d316 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -1,5 +1,5 @@ import { Proof, ZkProgram } from '../../proof-system/zkprogram.js'; -import { Field } from '../../provable/wrapped.js'; +import { Bool, Field } from '../../provable/wrapped.js'; import { Unconstrained } from '../../provable/types/unconstrained.js'; import { MerkleList, MerkleListIterator } from '../../provable/merkle-list.js'; import { Actions } from '../../../bindings/mina-transaction/transaction-leaves.js'; @@ -8,7 +8,12 @@ import { Struct } from '../../provable/types/struct.js'; import { SelfProof } from '../../proof-system/zkprogram.js'; import { Provable } from '../../provable/provable.js'; import { assert } from '../../provable/gadgets/common.js'; -import { ActionList, MerkleLeaf } from './offchain-state-serialization.js'; +import { + ActionList, + LinearizedAction, + LinearizedActionList, + MerkleLeaf, +} from './offchain-state-serialization.js'; import { MerkleMap } from '../../provable/merkle-map.js'; import { getProofsEnabled } from '../mina.js'; @@ -60,24 +65,51 @@ function merkleUpdateBatch( ): OffchainStateCommitments { // this would be unnecessary if the iterator could just be the public input actions.currentHash.assertEquals(stateA.actionState); - let root = stateA.root; // linearize actions into a flat MerkleList, so we don't process an insane amount of dummy actions - let linearActions = ActionList.empty(); + let linearActions = LinearizedActionList.empty(); for (let i = 0; i < maxActionsPerBatch; i++) { - actions.next().forEach(maxActionsPerUpdate, (action, isDummy) => { - linearActions.pushIf(isDummy.not(), action); - }); + let inner = actions.next().startIterating(); + let isAtEnd = Bool(false); + for (let i = 0; i < maxActionsPerUpdate; i++) { + let { element: action, isDummy } = inner.Unsafe.next(); + let isCheckPoint = inner.isAtEnd(); + [isAtEnd, isCheckPoint] = [ + isAtEnd.or(isCheckPoint), + isCheckPoint.and(isAtEnd.not()), + ]; + linearActions.pushIf( + isDummy.not(), + new LinearizedAction({ action, isCheckPoint }) + ); + } + inner.assertAtEnd( + `Expected at most ${maxActionsPerUpdate} actions per account update.` + ); } actions.assertAtEnd(); - // update merkle root for each action - linearActions.forEach(maxActionsPerBatch, ({ key, value }, isDummy) => { + // update merkle root at once for the actions of each account update + let root = stateA.root; + let intermediateRoot = root; + + type TreeUpdate = { key: Field; value: Field }; + let intermediateUpdates: TreeUpdate[] = []; + let intermediateTree = Unconstrained.witness(() => tree.get().clone()); + + linearActions.forEach(maxActionsPerBatch, (element, isDummy) => { + let { + action: { key, value }, + isCheckPoint, + } = element; + // Provable.log({ key, value, isEndOfUpdate, isDummy }); + // merkle witness let witness = Provable.witness( MerkleMapWitness, - () => new MerkleMapWitness(tree.get().getWitness(key.toBigInt())) + () => + new MerkleMapWitness(intermediateTree.get().getWitness(key.toBigInt())) ); // previous value at the key @@ -88,20 +120,33 @@ function merkleUpdateBatch( // prove that the witness is correct, by comparing the implied root and key // note: this just works if the (key, value) is a (0,0) dummy, because the value at the 0 key will always be 0 witness.calculateIndex().assertEquals(key); - witness.calculateRoot(previousValue).assertEquals(root); + witness.calculateRoot(previousValue).assertEquals(intermediateRoot); // store new value in at the key let newRoot = witness.calculateRoot(value); + // update root + intermediateRoot = Provable.if(isDummy, intermediateRoot, newRoot); + root = Provable.if(isCheckPoint, intermediateRoot, root); + // intermediateRoot = Provable.if(isCheckPoint, root, intermediateRoot); + // update the tree, outside the circuit (this should all be part of a better merkle tree API) Provable.asProver(() => { // ignore dummy value if (isDummy.toBoolean()) return; - tree.get().setLeaf(key.toBigInt(), value); - }); - // update root - root = Provable.if(isDummy, root, newRoot); + intermediateTree.get().setLeaf(key.toBigInt(), value); + intermediateUpdates.push({ key, value }); + + let isEnd = isCheckPoint.toBoolean(); + + if (isEnd) { + intermediateUpdates.forEach(({ key, value }) => { + tree.get().setLeaf(key.toBigInt(), value); + }); + intermediateUpdates = []; + } + }); }); return { root, actionState: actions.currentHash }; diff --git a/src/lib/mina/actions/offchain-state-serialization.ts b/src/lib/mina/actions/offchain-state-serialization.ts index 277a42afab..83ddafeaea 100644 --- a/src/lib/mina/actions/offchain-state-serialization.ts +++ b/src/lib/mina/actions/offchain-state-serialization.ts @@ -31,6 +31,8 @@ export { toAction, fromActionWithoutHashes, MerkleLeaf, + LinearizedAction, + LinearizedActionList, ActionList, fetchMerkleLeaves, fetchMerkleMap, @@ -175,6 +177,42 @@ class ActionList extends MerkleList.create( Actions.empty().hash ) {} +class LinearizedAction extends Struct({ + action: MerkleLeaf, + /** + * Whether this action is the last in an account update. + * In a linearized sequence of actions, this value determines the points at which we commit an atomic update to the Merkle tree. + */ + isCheckPoint: Bool, +}) { + /** + * A custom method to hash an action which only hashes the key and value in provable code. + * Therefore, it only proves that the key and value are part of the action, and nothing about + * the rest of the action. + */ + static hash({ action, isCheckPoint }: LinearizedAction) { + let preHashState = Provable.witnessFields(3, () => { + let prefix = action.prefix.get(); + let init = salt(prefixes.event) as [Field, Field, Field]; + return Poseidon.update(init, prefix); + }); + return Poseidon.update(preHashState, [ + // pack two bools into 1 field + action.usesPreviousValue.toField().add(isCheckPoint.toField().mul(2)), + action.previousValue, + action.key, + action.value, + ])[0]; + } +} + +class LinearizedActionList extends MerkleList.create( + LinearizedAction, + (hash: Field, action: LinearizedAction) => + Poseidon.hash([hash, LinearizedAction.hash(action)]), + Actions.empty().hash +) {} + async function fetchMerkleLeaves( contract: { address: PublicKey; tokenId: Field }, config?: { From 5651b2b9b21fc7aadace907f04c63114abe24d17 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 18:49:51 +0200 Subject: [PATCH 66/97] bool.implies() --- src/lib/provable/bool.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/lib/provable/bool.ts b/src/lib/provable/bool.ts index 31c9080f09..3e3a74024c 100644 --- a/src/lib/provable/bool.ts +++ b/src/lib/provable/bool.ts @@ -91,6 +91,22 @@ class Bool { return this.not().and(new Bool(y).not()).not(); } + /** + * Whether this Bool implies another Bool `y`. + * + * This is the same as `x.not().or(y)`: if `x` is true, then `y` must be true for the implication to be true. + * + * @example + * ```ts + * let isZero = x.equals(0); + * let lessThan10 = x.lessThan(10); + * assert(isZero.implies(lessThan10), 'x = 0 implies x < 10'); + * ``` + */ + implies(y: Bool | boolean): Bool { + return this.not().or(y); + } + /** * Proves that this {@link Bool} is equal to `y`. * @param y a {@link Bool}. From 65f256058101608e472b730b67dc10fe10511ca6 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 18:51:54 +0200 Subject: [PATCH 67/97] support actions with preconditions in rollup --- src/lib/mina/actions/offchain-state-rollup.ts | 51 ++++++++++++------- src/lib/provable/types/unconstrained.ts | 2 +- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index 93b312d316..020d3e753e 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -98,12 +98,12 @@ function merkleUpdateBatch( let intermediateUpdates: TreeUpdate[] = []; let intermediateTree = Unconstrained.witness(() => tree.get().clone()); + let isValidUpdate = Bool(true); + linearActions.forEach(maxActionsPerBatch, (element, isDummy) => { - let { - action: { key, value }, - isCheckPoint, - } = element; - // Provable.log({ key, value, isEndOfUpdate, isDummy }); + let { action, isCheckPoint } = element; + let { key, value, usesPreviousValue, previousValue } = action; + // Provable.log({ key, value, isCheckPoint, isDummy }); // merkle witness let witness = Provable.witness( @@ -113,22 +113,32 @@ function merkleUpdateBatch( ); // previous value at the key - let previousValue = Provable.witness(Field, () => + let actualPreviousValue = Provable.witness(Field, () => tree.get().getLeaf(key.toBigInt()) ); - // prove that the witness is correct, by comparing the implied root and key + // prove that the witness and `actualPreviousValue` is correct, by comparing the implied root and key // note: this just works if the (key, value) is a (0,0) dummy, because the value at the 0 key will always be 0 witness.calculateIndex().assertEquals(key); - witness.calculateRoot(previousValue).assertEquals(intermediateRoot); + witness.calculateRoot(actualPreviousValue).assertEquals(intermediateRoot); + + // if an expected previous value was provided, check whether it matches the actual previous value + // otherwise, the entire update in invalidated + let matchesPreviousValue = actualPreviousValue.equals(previousValue); + let isValidAction = usesPreviousValue.implies(matchesPreviousValue); + isValidUpdate = isValidUpdate.and(isValidAction); // store new value in at the key let newRoot = witness.calculateRoot(value); - // update root + // update intermediate root if this wasn't a dummy action intermediateRoot = Provable.if(isDummy, intermediateRoot, newRoot); - root = Provable.if(isCheckPoint, intermediateRoot, root); - // intermediateRoot = Provable.if(isCheckPoint, root, intermediateRoot); + + // at checkpoints, update the root, if the entire update was valid + root = Provable.if(isCheckPoint.and(isValidUpdate), intermediateRoot, root); + // at checkpoints, reset intermediate values + isValidUpdate = Provable.if(isCheckPoint, Bool(true), isValidUpdate); + intermediateRoot = Provable.if(isCheckPoint, root, intermediateRoot); // update the tree, outside the circuit (this should all be part of a better merkle tree API) Provable.asProver(() => { @@ -138,13 +148,18 @@ function merkleUpdateBatch( intermediateTree.get().setLeaf(key.toBigInt(), value); intermediateUpdates.push({ key, value }); - let isEnd = isCheckPoint.toBoolean(); - - if (isEnd) { - intermediateUpdates.forEach(({ key, value }) => { - tree.get().setLeaf(key.toBigInt(), value); - }); - intermediateUpdates = []; + if (isCheckPoint.toBoolean()) { + // if the update was valid, apply the intermediate updates to the actual tree + if (isValidUpdate.toBoolean()) { + intermediateUpdates.forEach(({ key, value }) => { + tree.get().setLeaf(key.toBigInt(), value); + }); + intermediateUpdates = []; + } + // otherwise, we have to roll back the intermediate tree (TODO: inefficient) + else { + intermediateTree.set(tree.get().clone()); + } } }); }); diff --git a/src/lib/provable/types/unconstrained.ts b/src/lib/provable/types/unconstrained.ts index 03782310a1..dcf5b1c833 100644 --- a/src/lib/provable/types/unconstrained.ts +++ b/src/lib/provable/types/unconstrained.ts @@ -93,7 +93,7 @@ and Provable.asProver() blocks, which execute outside the proof. /** * Create an `Unconstrained` from a witness computation. */ - static witness(compute: () => T) { + static witness(compute: () => T): Unconstrained { return witness( Unconstrained.provable, () => new Unconstrained(true, compute()) From 6f7059ca3064984bdedf5cd78ca83de3d8c055a0 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 20:25:30 +0200 Subject: [PATCH 68/97] implement update --- src/lib/mina/actions/offchain-state.ts | 36 ++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index c0cfc0f0d8..b7e8636ab8 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -198,7 +198,23 @@ function OffchainState< let update = contract().self; update.body.actions = Actions.pushEvent(update.body.actions, action); }, - update: notImplemented, + + update({ from, to }) { + // serialize into action + let action = toAction({ + prefix, + keyType: undefined, + valueType: type, + key: undefined, + value: type.fromValue(to), + previousValue: type.fromValue(from), + }); + + // push action on account update + let update = contract().self; + update.body.actions = Actions.pushEvent(update.body.actions, action); + }, + async get() { let key = toKeyHash(prefix, undefined, undefined); let optionValue = await get(key, type); @@ -230,7 +246,23 @@ function OffchainState< let update = contract().self; update.body.actions = Actions.pushEvent(update.body.actions, action); }, - update: notImplemented, + + update(key, { from, to }) { + // serialize into action + let action = toAction({ + prefix, + keyType, + valueType, + key, + value: valueType.fromValue(to), + previousValue: valueType.fromValue(from), + }); + + // push action on account update + let update = contract().self; + update.body.actions = Actions.pushEvent(update.body.actions, action); + }, + async get(key) { let keyHash = toKeyHash(prefix, keyType, key); return await get(keyHash, valueType); From 39bb623088358b9e43d98454b795c3e07dd85fc9 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 20:31:21 +0200 Subject: [PATCH 69/97] some fixes, happy path not working yet --- .../actions/offchain-contract.unit-test.ts | 27 ++++++++++++------- src/lib/mina/actions/offchain-state-rollup.ts | 17 +++++++++--- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 547f4e57ff..4b4e4a034d 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -25,10 +25,15 @@ class ExampleContract extends SmartContract { async createAccount(address: PublicKey, amountToMint: UInt64) { offchainState.fields.accounts.set(address, amountToMint); - // TODO `totalSupply` easily gets into a wrong state here on concurrent calls. - // and using `.update()` doesn't help either + // TODO using `update()` on the total supply means that this method + // can only be called once every settling cycle let totalSupply = await offchainState.fields.totalSupply.get(); offchainState.fields.totalSupply.set(totalSupply.add(amountToMint)); + // TODO this fails, because `from` does not yield the correct value hash if the field was not set before + // offchainState.fields.totalSupply.update({ + // from: totalSupply, + // to: totalSupply.add(amountToMint), + // }); } @method @@ -40,14 +45,18 @@ class ExampleContract extends SmartContract { let toBalance = toOption.orElse(0n); /** - * FIXME using `set()` here is completely insecure, a sender can easily double-spend by sending multiple transactions, - * which will all use the same initial balance. - * Even using a naive version of `update()` would give a double-spend opportunity, because the updates are not rejected atomically: - * if the `to` update gets accepted but the `from` update fails, it's a double-spend - * => properly implementing this needs a version of `update()` that rejects all state actions in one update if any of them fails! + * Update both accounts atomically. + * + * This is safe, because both updates will only be accepted if both previous balances are still correct. */ - offchainState.fields.accounts.set(from, fromBalance.sub(amount)); - offchainState.fields.accounts.set(to, toBalance.add(amount)); + offchainState.fields.accounts.update(from, { + from: fromBalance, + to: fromBalance.sub(amount), + }); + offchainState.fields.accounts.update(to, { + from: toBalance, + to: toBalance.add(amount), + }); } @method.returns(UInt64) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index 020d3e753e..d41d039c0a 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -103,7 +103,6 @@ function merkleUpdateBatch( linearActions.forEach(maxActionsPerBatch, (element, isDummy) => { let { action, isCheckPoint } = element; let { key, value, usesPreviousValue, previousValue } = action; - // Provable.log({ key, value, isCheckPoint, isDummy }); // merkle witness let witness = Provable.witness( @@ -114,8 +113,17 @@ function merkleUpdateBatch( // previous value at the key let actualPreviousValue = Provable.witness(Field, () => - tree.get().getLeaf(key.toBigInt()) + intermediateTree.get().getLeaf(key.toBigInt()) ); + Provable.log({ + key, + value, + isCheckPoint, + isDummy, + usesPreviousValue, + previousValue, + actualPreviousValue, + }); // prove that the witness and `actualPreviousValue` is correct, by comparing the implied root and key // note: this just works if the (key, value) is a (0,0) dummy, because the value at the 0 key will always be 0 @@ -137,6 +145,7 @@ function merkleUpdateBatch( // at checkpoints, update the root, if the entire update was valid root = Provable.if(isCheckPoint.and(isValidUpdate), intermediateRoot, root); // at checkpoints, reset intermediate values + let wasValidUpdate = isValidUpdate; isValidUpdate = Provable.if(isCheckPoint, Bool(true), isValidUpdate); intermediateRoot = Provable.if(isCheckPoint, root, intermediateRoot); @@ -150,16 +159,16 @@ function merkleUpdateBatch( if (isCheckPoint.toBoolean()) { // if the update was valid, apply the intermediate updates to the actual tree - if (isValidUpdate.toBoolean()) { + if (wasValidUpdate.toBoolean()) { intermediateUpdates.forEach(({ key, value }) => { tree.get().setLeaf(key.toBigInt(), value); }); - intermediateUpdates = []; } // otherwise, we have to roll back the intermediate tree (TODO: inefficient) else { intermediateTree.set(tree.get().clone()); } + intermediateUpdates = []; } }); }); From 889ca27901e2ab4a6d698c2d7feb3ba19ec204ed Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 20:59:24 +0200 Subject: [PATCH 70/97] update from has to be an option! working happy path --- .../mina/actions/offchain-contract.unit-test.ts | 4 ++-- .../mina/actions/offchain-state-serialization.ts | 9 +++++++-- src/lib/mina/actions/offchain-state.ts | 16 ++++++++-------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 4b4e4a034d..6b6b30c7f4 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -50,11 +50,11 @@ class ExampleContract extends SmartContract { * This is safe, because both updates will only be accepted if both previous balances are still correct. */ offchainState.fields.accounts.update(from, { - from: fromBalance, + from: fromOption, to: fromBalance.sub(amount), }); offchainState.fields.accounts.update(to, { - from: toBalance, + from: toOption, to: toBalance.add(amount), }); } diff --git a/src/lib/mina/actions/offchain-state-serialization.ts b/src/lib/mina/actions/offchain-state-serialization.ts index 83ddafeaea..4f79843fa7 100644 --- a/src/lib/mina/actions/offchain-state-serialization.ts +++ b/src/lib/mina/actions/offchain-state-serialization.ts @@ -25,6 +25,7 @@ import { PublicKey } from '../../provable/crypto/signature.js'; import { Provable } from '../../provable/provable.js'; import { Actions } from '../account-update.js'; import { MerkleTree } from '../../provable/merkle-tree.js'; +import { Option } from '../../provable/option.js'; export { toKeyHash, @@ -63,7 +64,7 @@ function toAction | undefined>({ valueType: Actionable; key: KeyType extends undefined ? undefined : K; value: V; - previousValue?: V; + previousValue?: Option; }): Action { let valueSize = valueType.sizeInFields(); let padding = valueSize % 2 === 0 ? [] : [Field(0)]; @@ -73,7 +74,11 @@ function toAction | undefined>({ let usesPreviousValue = Bool(previousValue !== undefined).toField(); let previousValueHash = previousValue !== undefined - ? Poseidon.hashPacked(valueType, previousValue) + ? Provable.if( + previousValue.isSome, + Poseidon.hashPacked(valueType, previousValue.value), + Field(0) + ) : Field(0); let valueHash = Poseidon.hashPacked(valueType, value); diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index b7e8636ab8..2fad29dde6 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -120,8 +120,6 @@ function OffchainState< return { merkleMap, valueMap }; }; - const notImplemented = (): any => assert(false, 'Not implemented'); - let rollup = OffchainStateRollup(); function contract() { @@ -207,7 +205,7 @@ function OffchainState< valueType: type, key: undefined, value: type.fromValue(to), - previousValue: type.fromValue(from), + previousValue: from, }); // push action on account update @@ -255,7 +253,7 @@ function OffchainState< valueType, key, value: valueType.fromValue(to), - previousValue: valueType.fromValue(from), + previousValue: from, }); // push action on account update @@ -355,9 +353,10 @@ type OffchainField = { * Update the value of the field, while requiring a specific previous value. * * If the previous value does not match, the update will not be applied. - * If no previous value is present, the `from` value is ignored and the update applied unconditionally. + * + * Note that the previous value is an option: to require that the field was not set before, use `Option.none()`. */ - update(update: { from: T | TValue; to: T | TValue }): void; + update(update: { from: Option; to: T | TValue }): void; }; function OffchainMap(key: K, value: V) { @@ -376,9 +375,10 @@ type OffchainMap = { * Update the value of the field, while requiring a specific previous value. * * If the previous value does not match, the update will not be applied. - * If no previous value is present, the `from` value is ignored and the update applied unconditionally. + * + * Note that the previous value is an option: to require that the field was not set before, use `Option.none()`. */ - update(key: K, update: { from: V | VValue; to: V | VValue }): void; + update(key: K, update: { from: Option; to: V | VValue }): void; }; type OffchainStateKind = From 9a85efc323f86881a75d194205173def01a0f9fc Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 21:07:12 +0200 Subject: [PATCH 71/97] make field.get return an option as well --- .../mina/actions/offchain-contract.unit-test.ts | 16 ++++++++-------- src/lib/mina/actions/offchain-state-rollup.ts | 9 --------- src/lib/mina/actions/offchain-state.ts | 12 +++++++----- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 6b6b30c7f4..2e9dfdc3a2 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -27,13 +27,13 @@ class ExampleContract extends SmartContract { // TODO using `update()` on the total supply means that this method // can only be called once every settling cycle - let totalSupply = await offchainState.fields.totalSupply.get(); - offchainState.fields.totalSupply.set(totalSupply.add(amountToMint)); - // TODO this fails, because `from` does not yield the correct value hash if the field was not set before - // offchainState.fields.totalSupply.update({ - // from: totalSupply, - // to: totalSupply.add(amountToMint), - // }); + let totalSupplyOption = await offchainState.fields.totalSupply.get(); + let totalSupply = totalSupplyOption.orElse(0n); + + offchainState.fields.totalSupply.update({ + from: totalSupplyOption, + to: totalSupply.add(amountToMint), + }); } @method @@ -61,7 +61,7 @@ class ExampleContract extends SmartContract { @method.returns(UInt64) async getSupply() { - return await offchainState.fields.totalSupply.get(); + return (await offchainState.fields.totalSupply.get()).orElse(0n); } @method.returns(UInt64) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index d41d039c0a..d934ba38f0 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -115,15 +115,6 @@ function merkleUpdateBatch( let actualPreviousValue = Provable.witness(Field, () => intermediateTree.get().getLeaf(key.toBigInt()) ); - Provable.log({ - key, - value, - isCheckPoint, - isDummy, - usesPreviousValue, - previousValue, - actualPreviousValue, - }); // prove that the witness and `actualPreviousValue` is correct, by comparing the implied root and key // note: this just works if the (key, value) is a (0,0) dummy, because the value at the 0 key will always be 0 diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 2fad29dde6..1de44ffeff 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -215,9 +215,7 @@ function OffchainState< async get() { let key = toKeyHash(prefix, undefined, undefined); - let optionValue = await get(key, type); - // for fields that are not in the map, we return the default value -- similar to onchain state - return optionValue.orElse(type.empty()); + return await get(key, type); }, }; } @@ -342,13 +340,15 @@ function OffchainField(type: T) { } type OffchainField = { /** - * Get the value of the field. + * Get the value of the field, or none if it doesn't exist yet. */ - get(): Promise; + get(): Promise>; + /** * Set the value of the field. */ set(value: T | TValue): void; + /** * Update the value of the field, while requiring a specific previous value. * @@ -367,10 +367,12 @@ type OffchainMap = { * Get the value for this key, or none if it doesn't exist. */ get(key: K): Promise>; + /** * Set the value for this key. */ set(key: K, value: V | VValue): void; + /** * Update the value of the field, while requiring a specific previous value. * From f2f62d445ceb0ec2ef36a05691b37037891dc6be Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 22:35:31 +0200 Subject: [PATCH 72/97] make option infer from the class so that it works --- src/lib/provable/option.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/lib/provable/option.ts b/src/lib/provable/option.ts index e214f99ae4..bf6bf12424 100644 --- a/src/lib/provable/option.ts +++ b/src/lib/provable/option.ts @@ -1,15 +1,18 @@ +import { InferValue } from '../../bindings/lib/provable-generic.js'; import { emptyValue } from '../proof-system/zkprogram.js'; import { Provable } from './provable.js'; -import { Struct } from './types/struct.js'; +import { InferProvable, Struct } from './types/struct.js'; import { Bool } from './wrapped.js'; -export { Option }; +export { Option, OptionOrValue }; type Option = { isSome: Bool; value: T } & { assertSome(message?: string): T; orElse(defaultValue: T | V): T; }; +type OptionOrValue = Option | { isSome: boolean; value: V }; + /** * Define an optional version of a provable type. * @@ -26,17 +29,20 @@ type Option = { isSome: Bool; value: T } & { * let zero: UInt64 = none.orElse(0n); // specify a default value * ``` */ -function Option( - type: Provable +function Option>( + type: A ): Provable< - Option, + Option, InferValue>, // TODO make V | undefined the value type - { isSome: boolean; value: V } + { isSome: boolean; value: InferValue } > & { - from(value?: T): Option; - none(): Option; + from(value?: InferProvable): Option, InferValue>; + none(): Option, InferValue>; } { - const Super = Struct({ isSome: Bool, value: type }); + type T = InferProvable; + type V = InferValue; + + const Super = Struct({ isSome: Bool, value: type as Provable }); return class Option_ extends Super { orElse(defaultValue: T | V): T { return Provable.if( From b213035838ab5089bf2f88304e0d72d914d3f843 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 22:35:39 +0200 Subject: [PATCH 73/97] unconstrained tweak --- src/lib/provable/types/unconstrained.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/provable/types/unconstrained.ts b/src/lib/provable/types/unconstrained.ts index dcf5b1c833..a6b4baf519 100644 --- a/src/lib/provable/types/unconstrained.ts +++ b/src/lib/provable/types/unconstrained.ts @@ -110,7 +110,7 @@ and Provable.asProver() blocks, which execute outside the proof. }); } - static provable: Provable> & { + static provable: Provable, Unconstrained> & { toInput: (x: Unconstrained) => { fields?: Field[]; packed?: [Field, number][]; @@ -130,7 +130,10 @@ and Provable.asProver() blocks, which execute outside the proof. }, }; - static provableWithEmpty(empty: T): Provable> & { + static provableWithEmpty(empty: T): Provable< + Unconstrained, + Unconstrained + > & { toInput: (x: Unconstrained) => { fields?: Field[]; packed?: [Field, number][]; From 742d66227aa67a95c7ac5a8d6da0db02693dfb70 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 22:50:49 +0200 Subject: [PATCH 74/97] figure out option type --- src/lib/provable/option.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/provable/option.ts b/src/lib/provable/option.ts index bf6bf12424..8f51f1cb12 100644 --- a/src/lib/provable/option.ts +++ b/src/lib/provable/option.ts @@ -1,4 +1,4 @@ -import { InferValue } from '../../bindings/lib/provable-generic.js'; +import { From, InferValue } from '../../bindings/lib/provable-generic.js'; import { emptyValue } from '../proof-system/zkprogram.js'; import { Provable } from './provable.js'; import { InferProvable, Struct } from './types/struct.js'; @@ -11,7 +11,7 @@ type Option = { isSome: Bool; value: T } & { orElse(defaultValue: T | V): T; }; -type OptionOrValue = Option | { isSome: boolean; value: V }; +type OptionOrValue = { isSome: boolean | Bool; value: V | T }; /** * Define an optional version of a provable type. @@ -36,6 +36,9 @@ function Option>( // TODO make V | undefined the value type { isSome: boolean; value: InferValue } > & { + fromValue: ( + value: From<{ isSome: typeof Bool; value: A }> + ) => Option, InferValue>; from(value?: InferProvable): Option, InferValue>; none(): Option, InferValue>; } { From 956b66b0f93d867800fe0a864382fee30c14e47d Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 22:59:44 +0200 Subject: [PATCH 75/97] more flexible option input type --- src/lib/mina/actions/offchain-state.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 1de44ffeff..2c1058352f 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -13,7 +13,7 @@ import { OffchainStateCommitments, OffchainStateRollup, } from './offchain-state-rollup.js'; -import { Option } from '../../provable/option.js'; +import { Option, OptionOrValue } from '../../provable/option.js'; import { InferValue } from '../../../bindings/lib/provable-generic.js'; import { SmartContract } from '../zkapp.js'; import { assert } from '../../provable/gadgets/common.js'; @@ -180,6 +180,7 @@ function OffchainState< type: Actionable ): OffchainField { const prefix = Field(index); + let optionType = Option(type); return { set(value) { @@ -205,7 +206,7 @@ function OffchainState< valueType: type, key: undefined, value: type.fromValue(to), - previousValue: from, + previousValue: optionType.fromValue(from), }); // push action on account update @@ -223,9 +224,10 @@ function OffchainState< function map( index: number, keyType: Actionable, - valueType: Actionable + valueType: Actionable ): OffchainMap { const prefix = Field(index); + let optionType = Option(valueType); return { set(key, value) { @@ -251,7 +253,7 @@ function OffchainState< valueType, key, value: valueType.fromValue(to), - previousValue: from, + previousValue: optionType.fromValue(from), }); // push action on account update @@ -342,7 +344,7 @@ type OffchainField = { /** * Get the value of the field, or none if it doesn't exist yet. */ - get(): Promise>; + get(): Promise>; /** * Set the value of the field. @@ -356,7 +358,7 @@ type OffchainField = { * * Note that the previous value is an option: to require that the field was not set before, use `Option.none()`. */ - update(update: { from: Option; to: T | TValue }): void; + update(update: { from: OptionOrValue; to: T | TValue }): void; }; function OffchainMap(key: K, value: V) { @@ -380,7 +382,10 @@ type OffchainMap = { * * Note that the previous value is an option: to require that the field was not set before, use `Option.none()`. */ - update(key: K, update: { from: Option; to: V | VValue }): void; + update( + key: K, + update: { from: OptionOrValue; to: V | VValue } + ): void; }; type OffchainStateKind = From a8d0b79c2bc99330259f4a114a0a92f87a311dfe Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 23:02:55 +0200 Subject: [PATCH 76/97] proper out-of-snark merkle update logic --- .../actions/offchain-contract.unit-test.ts | 14 ++++- .../actions/offchain-state-serialization.ts | 62 +++++++++++++++---- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 2e9dfdc3a2..cd0ce1d359 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -111,7 +111,11 @@ console.timeEnd('deploy'); console.time('create account'); await Mina.transaction(sender, async () => { + // first call (should succeed) await contract.createAccount(sender, UInt64.from(1000)); + + // second call (should fail) + await contract.createAccount(sender, UInt64.from(2000)); }) .sign([sender.key]) .prove() @@ -137,9 +141,13 @@ await checkAgainstSupply(1000n); // transfer console.time('transfer'); -await Mina.transaction(sender, () => - contract.transfer(sender, receiver, UInt64.from(100)) -) +await Mina.transaction(sender, async () => { + // first call (should succeed) + await contract.transfer(sender, receiver, UInt64.from(100)); + + // second call (should fail) + await contract.transfer(sender, receiver, UInt64.from(200)); +}) .sign([sender.key]) .prove() .send(); diff --git a/src/lib/mina/actions/offchain-state-serialization.ts b/src/lib/mina/actions/offchain-state-serialization.ts index 4f79843fa7..61d7d3d032 100644 --- a/src/lib/mina/actions/offchain-state-serialization.ts +++ b/src/lib/mina/actions/offchain-state-serialization.ts @@ -37,6 +37,7 @@ export { ActionList, fetchMerkleLeaves, fetchMerkleMap, + updateMerkleMap, Actionable, }; @@ -265,21 +266,60 @@ async function fetchMerkleMap( ); if ('error' in result) throw Error(JSON.stringify(result)); - let leaves = result - .map((event) => - event.actions - .map((action) => MerkleLeaf.fromAction(action.map(Field))) - .reverse() - ) - .flat(); + let leaves = result.map((event) => + event.actions + .map((action) => MerkleLeaf.fromAction(action.map(Field))) + .reverse() + ); let merkleMap = new MerkleTree(256); let valueMap = new Map(); - for (let leaf of leaves) { - merkleMap.setLeaf(leaf.key.toBigInt(), leaf.value); - valueMap.set(leaf.key.toBigInt(), leaf.prefix.get()); - } + updateMerkleMap(leaves, merkleMap, valueMap); return { merkleMap, valueMap }; } + +function updateMerkleMap( + updates: MerkleLeaf[][], + tree: MerkleTree, + valueMap?: Map +) { + let intermediateTree = tree.clone(); + + for (let leaves of updates) { + let isValidUpdate = true; + let updates: { key: bigint; value: bigint; fullValue: Field[] }[] = []; + + for (let leaf of leaves) { + let { key, value, usesPreviousValue, previousValue, prefix } = + MerkleLeaf.toValue(leaf); + + // the update is invalid if there is an unsatisfied precondition + let isValidAction = + !usesPreviousValue || + intermediateTree.getLeaf(key).toBigInt() === previousValue; + + if (!isValidAction) { + isValidUpdate = false; + + break; + } + + // update the intermediate tree, save updates for final tree + intermediateTree.setLeaf(key, Field(value)); + updates.push({ key, value, fullValue: prefix.get() }); + } + + if (isValidUpdate) { + // if the update was valid, we can commit the updates + for (let { key, value, fullValue } of updates) { + tree.setLeaf(key, Field(value)); + if (valueMap) valueMap.set(key, fullValue); + } + } else { + // if the update was invalid, we have to roll back the intermediate tree + intermediateTree = tree.clone(); + } + } +} From bda7dcedafba79db18448a816ae775aae551f030 Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 23:25:21 +0200 Subject: [PATCH 77/97] fix proofs disabled case --- src/lib/mina/actions/offchain-state-rollup.ts | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index d934ba38f0..236c24a1fc 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -13,6 +13,7 @@ import { LinearizedAction, LinearizedActionList, MerkleLeaf, + updateMerkleMap, } from './offchain-state-serialization.js'; import { MerkleMap } from '../../provable/merkle-map.js'; import { getProofsEnabled } from '../mina.js'; @@ -94,8 +95,7 @@ function merkleUpdateBatch( let root = stateA.root; let intermediateRoot = root; - type TreeUpdate = { key: Field; value: Field }; - let intermediateUpdates: TreeUpdate[] = []; + let intermediateUpdates: { key: Field; value: Field }[] = []; let intermediateTree = Unconstrained.witness(() => tree.get().clone()); let isValidUpdate = Bool(true); @@ -277,7 +277,22 @@ function OffchainStateRollup({ // if proofs are disabled, create a dummy proof and final state, and return if (!getProofsEnabled()) { - tree = merkleUpdateOutside(actions, tree); + // convert actions to nested array + let actionsList = actions.data + .get() + .map(({ element: actionsList }) => + actionsList.data + .get() + .map(({ element }) => element) + // TODO reverse needed because of bad internal merkle list representation + .reverse() + ) + // TODO reverse needed because of bad internal merkle list representation + .reverse(); + + // update the tree outside the circuit + updateMerkleMap(actionsList, tree); + let finalState = new OffchainStateCommitments({ root: tree.getRoot(), actionState: iterator.hash, @@ -316,7 +331,7 @@ function OffchainStateRollup({ }; } -// from a nested list of actions, create a slice (iterator) starting at `index` that has at most `batchSize` actions in it\ +// from a nested list of actions, create a slice (iterator) starting at `index` that has at most `batchSize` actions in it. // also moves the original iterator forward to start after the slice function sliceActions(actions: ActionIterator, batchSize: number) { class ActionListsList extends MerkleList.create( @@ -348,20 +363,3 @@ function sliceActions(actions: ActionIterator, batchSize: number) { return slice.startIterating(); } - -// TODO: do we have to repeat the merkle updates outside the circuit? - -function merkleUpdateOutside( - actions: MerkleList>, - tree: MerkleTree -) { - tree = tree.clone(); - - actions.data.get().forEach(({ element: actionsList }) => { - actionsList.data.get().forEach(({ element: { key, value } }) => { - tree.setLeaf(key.toBigInt(), value); - }); - }); - - return tree; -} From cd9a68ab05ad3a1f5aa46067729dabf42ca81b9c Mon Sep 17 00:00:00 2001 From: Gregor Date: Tue, 14 May 2024 23:26:19 +0200 Subject: [PATCH 78/97] remove debug logs --- src/lib/mina/actions/offchain-state-rollup.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index 236c24a1fc..ce5b47e813 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -302,20 +302,17 @@ function OffchainStateRollup({ } // base proof - console.time('batch 0'); let slice = sliceActions(iterator, maxActionsPerBatch); let proof = await offchainStateRollup.firstBatch( inputState, slice, Unconstrained.from(tree) ); - console.timeEnd('batch 0'); // recursive proofs for (let i = 1; ; i++) { if (iterator.isAtEnd().toBoolean()) break; - console.time(`batch ${i}`); let slice = sliceActions(iterator, maxActionsPerBatch); proof = await offchainStateRollup.nextBatch( inputState, @@ -323,7 +320,6 @@ function OffchainStateRollup({ Unconstrained.from(tree), proof ); - console.timeEnd(`batch ${i}`); } return { proof, tree }; From 49cf11a1264f229846d1e211b48bdba55271dabf Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 May 2024 10:35:10 +0200 Subject: [PATCH 79/97] set -> overwrite plus scary doccomments --- .../actions/offchain-contract.unit-test.ts | 2 +- src/lib/mina/actions/offchain-state.ts | 38 ++++++++++++------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index cd0ce1d359..908369760e 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -23,7 +23,7 @@ class ExampleContract extends SmartContract { @method async createAccount(address: PublicKey, amountToMint: UInt64) { - offchainState.fields.accounts.set(address, amountToMint); + offchainState.fields.accounts.overwrite(address, amountToMint); // TODO using `update()` on the total supply means that this method // can only be called once every settling cycle diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 2c1058352f..56e6254dc8 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -183,7 +183,7 @@ function OffchainState< let optionType = Option(type); return { - set(value) { + overwrite(value) { // serialize into action let action = toAction({ prefix, @@ -230,7 +230,7 @@ function OffchainState< let optionType = Option(valueType); return { - set(key, value) { + overwrite(key, value) { // serialize into action let action = toAction({ prefix, @@ -346,19 +346,24 @@ type OffchainField = { */ get(): Promise>; - /** - * Set the value of the field. - */ - set(value: T | TValue): void; - /** * Update the value of the field, while requiring a specific previous value. * * If the previous value does not match, the update will not be applied. * - * Note that the previous value is an option: to require that the field was not set before, use `Option.none()`. + * Note that the previous value is an option: to require that the field was not set before, use `Option(type).none()` or `undefined`. */ update(update: { from: OptionOrValue; to: T | TValue }): void; + + /** + * Set the value of the field to the given value, without taking into account the previous value. + * + * **Warning**: if this is performed by multiple zkapp calls concurrently (between one call to `settle()` and the next), + * calls that are applied later will simply overwrite and ignore whatever changes were made by earlier calls. + * + * This behaviour can imply a security risk in many applications, so use `overwrite()` with caution. + */ + overwrite(value: T | TValue): void; }; function OffchainMap(key: K, value: V) { @@ -370,22 +375,27 @@ type OffchainMap = { */ get(key: K): Promise>; - /** - * Set the value for this key. - */ - set(key: K, value: V | VValue): void; - /** * Update the value of the field, while requiring a specific previous value. * * If the previous value does not match, the update will not be applied. * - * Note that the previous value is an option: to require that the field was not set before, use `Option.none()`. + * Note that the previous value is an option: to require that the field was not set before, use `Option(type).none()` or `undefined`. */ update( key: K, update: { from: OptionOrValue; to: V | VValue } ): void; + + /** + * Set the value for this key to the given value, without taking into account the previous value. + * + * **Warning**: if the same key is modified by multiple zkapp calls concurrently (between one call to `settle()` and the next), + * calls that are applied later will simply overwrite and ignore whatever changes were made by earlier calls. + * + * This behaviour can imply a security risk in many applications, so use `overwrite()` with caution. + */ + overwrite(key: K, value: V | VValue): void; }; type OffchainStateKind = From 2799332966aa9a49f0920b3203393ea2efe9cdae Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 May 2024 10:49:03 +0200 Subject: [PATCH 80/97] fix method.returns inference --- src/lib/mina/zkapp.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lib/mina/zkapp.ts b/src/lib/mina/zkapp.ts index c4ecefb141..84e73cc286 100644 --- a/src/lib/mina/zkapp.ts +++ b/src/lib/mina/zkapp.ts @@ -19,6 +19,7 @@ import { import { cloneCircuitValue, FlexibleProvablePure, + InferProvable, } from '../provable/types/struct.js'; import { Provable, @@ -170,12 +171,14 @@ function method( * } * ``` */ -method.returns = function ( - returnType: Provable -) { +method.returns = function < + K extends string, + T extends SmartContract, + R extends Provable +>(returnType: R) { return function decorateMethod( target: T & { - [k in K]: (...args: any) => Promise; + [k in K]: (...args: any) => Promise>; }, methodName: K & string & keyof T, descriptor: PropertyDescriptor From d6bd723d3b54a54de43a5715a06b012b0eb098a0 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 May 2024 10:52:15 +0200 Subject: [PATCH 81/97] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3215c7dd3d..0988b8eab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Fixes - Fix absolute imports which prevented compilation in some TS projects that used o1js https://github.com/o1-labs/o1js/pull/1628 +- Fix type inference for `method.returns(Type)`, to require a matching return signature https://github.com/o1-labs/o1js/pull/1653 ## [1.1.0](https://github.com/o1-labs/o1js/compare/1ad7333e9e...4a17de857) - 2024-04-30 From c18fbe66f512cd13d74b0265c3627f4a4c5e7916 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 May 2024 11:42:14 +0200 Subject: [PATCH 82/97] export offchain state, add doccomments --- src/index.ts | 14 ++++++ src/lib/mina/actions/offchain-state-rollup.ts | 8 ++++ src/lib/mina/actions/offchain-state.ts | 47 +++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/src/index.ts b/src/index.ts index f58de89974..0252a528bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -128,6 +128,7 @@ export { setNumberOfWorkers } from './lib/proof-system/workers.js'; // experimental APIs import { memoizeWitness } from './lib/provable/provable.js'; +import * as OffchainState_ from './lib/mina/actions/offchain-state.js'; export { Experimental }; const Experimental_ = { @@ -140,6 +141,19 @@ const Experimental_ = { */ namespace Experimental { export let memoizeWitness = Experimental_.memoizeWitness; + + // offchain state + export let OffchainState = OffchainState_.OffchainState; + + /** + * Commitments that keep track of the current state of an offchain Merkle tree constructed from actions. + * Intended to be stored on-chain. + * + * Fields: + * - `root`: The root of the current Merkle tree + * - `actionState`: The hash pointing to the list of actions that have been applied to form the current Merkle tree + */ + export class OffchainStateCommitments extends OffchainState_.OffchainStateCommitments {} } Error.stackTraceLimit = 100000; diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index ce5b47e813..985eb2816b 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -28,6 +28,14 @@ class ActionIterator extends MerkleListIterator.create( Actions.emptyActionState() ) {} +/** + * Commitments that keep track of the current state of an offchain Merkle tree constructed from actions. + * Intended to be stored on-chain. + * + * Fields: + * - `root`: The root of the current Merkle tree + * - `actionState`: The hash pointing to the list of actions that have been applied to form the current Merkle tree + */ class OffchainStateCommitments extends Struct({ // this should just be a MerkleTree type that carries the full tree as aux data root: Field, diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 56e6254dc8..c0582b4e35 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -70,6 +70,24 @@ type OffchainState = { /** * Settle the offchain state. + * + * Use this in a contract method as follows: + * + * @example + * ```ts + * class StateProof extends offchainState.Proof {} + * + * // ... + * + * class MyContract extends SmartContract { + * \@method + * async settle(proof: StateProof) { + * await offchainState.settle(proof); + * } + * } + * ``` + * + * The `StateProof` can be created by calling `offchainState.createSettlementProof()`. */ settle( proof: Proof @@ -82,6 +100,35 @@ type OffchainStateContract = SmartContract & { const MerkleWitness256 = MerkleWitness(256); +/** + * Offchain state for a `SmartContract`. + * + * ```ts + * // declare your offchain state + * + * const offchainState = OffchainState({ + * accounts: OffchainState.Map(PublicKey, UInt64), + * totalSupply: OffchainState.Field(UInt64), + * }); + * + * // use it in a contract, by adding an onchain state field of type `OffchainStateCommitments` + * + * class MyContract extends SmartContract { + * \@state(OffchainStateCommitments) offchainState = State( + * OffchainStateCommitments.empty() + * ); + * + * // ... + * } + * + * // set the contract instance + * + * let contract = new MyContract(address); + * offchainState.setContractInstance(contract); + * ``` + * + * See the individual methods on `offchainState` for more information on usage. + */ function OffchainState< const Config extends { [key: string]: OffchainStateKind } >(config: Config): OffchainState { From 39307c1d03b29dffc1c47343e609d6260988cf46 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 May 2024 12:01:42 +0200 Subject: [PATCH 83/97] failing, more complex test --- .../actions/offchain-contract.unit-test.ts | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 908369760e..92279b937d 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -1,10 +1,19 @@ -import { OffchainState, OffchainStateCommitments } from './offchain-state.js'; -import { PublicKey } from '../../provable/crypto/signature.js'; -import { UInt64 } from '../../provable/int.js'; -import { SmartContract, method } from '../zkapp.js'; -import { Mina, State, state } from '../../../index.js'; +import { + SmartContract, + method, + Mina, + State, + state, + PublicKey, + UInt64, + Experimental, +} from '../../../index.js'; import assert from 'assert'; +const proofsEnabled = true; + +const { OffchainState, OffchainStateCommitments } = Experimental; + const offchainState = OffchainState({ accounts: OffchainState.Map(PublicKey, UInt64), totalSupply: OffchainState.Field(UInt64), @@ -78,12 +87,11 @@ class ExampleContract extends SmartContract { // test code below // setup -const proofsEnabled = true; const Local = await Mina.LocalBlockchain({ proofsEnabled }); Mina.setActiveInstance(Local); -let [sender, receiver, contractAccount] = Local.testAccounts; +let [sender, receiver, contractAccount, other] = Local.testAccounts; let contract = new ExampleContract(contractAccount); offchainState.setContractInstance(contract); @@ -96,6 +104,8 @@ if (proofsEnabled) { console.timeEnd('compile contract'); } +Local.setProofsEnabled(false); + // deploy and create first account console.time('deploy'); @@ -136,7 +146,7 @@ await Mina.transaction(sender, () => contract.settle(proof)) console.timeEnd('settle 1'); // check balance and supply -await checkAgainstSupply(1000n); +await check({ expectedSupply: 1000n, expectedSenderBalance: 1000n }); // transfer @@ -145,16 +155,33 @@ await Mina.transaction(sender, async () => { // first call (should succeed) await contract.transfer(sender, receiver, UInt64.from(100)); - // second call (should fail) + // more calls that should all fail + // (these are enough to need two proof steps during settlement) await contract.transfer(sender, receiver, UInt64.from(200)); + await contract.transfer(sender, receiver, UInt64.from(300)); + await contract.transfer(sender, receiver, UInt64.from(400)); + await contract.transfer(sender, receiver, UInt64.from(500)); }) .sign([sender.key]) .prove() .send(); console.timeEnd('transfer'); +// create another account + +console.time('create account 2'); +await Mina.transaction(sender, async () => { + await contract.createAccount(other, UInt64.from(500)); +}) + .sign([sender.key]) + .prove() + .send(); +console.timeEnd('create account 2'); + // settle +Local.setProofsEnabled(true); + console.time('settlement proof 2'); proof = await offchainState.createSettlementProof(); console.timeEnd('settlement proof 2'); @@ -167,11 +194,17 @@ await Mina.transaction(sender, () => contract.settle(proof)) console.timeEnd('settle 2'); // check balance and supply -await checkAgainstSupply(1000n); +await check({ expectedSupply: 1500n, expectedSenderBalance: 900n }); // test helper -async function checkAgainstSupply(expectedSupply: bigint) { +async function check({ + expectedSupply, + expectedSenderBalance, +}: { + expectedSupply: bigint; + expectedSenderBalance: bigint; +}) { let supply = (await contract.getSupply()).toBigInt(); assert.strictEqual(supply, expectedSupply); @@ -181,4 +214,5 @@ async function checkAgainstSupply(expectedSupply: bigint) { console.log('balance (sender)', balanceSender); console.log('balance (recv)', balanceReceiver); assert.strictEqual(balanceSender + balanceReceiver, supply); + assert.strictEqual(balanceSender, expectedSenderBalance); } From fc646d3d4a8a9ad1425f4620b1e54110040596ab Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 May 2024 12:53:14 +0200 Subject: [PATCH 84/97] fix: convert field elements to constants when moving out of circuit --- src/lib/mina/actions/offchain-state-rollup.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/lib/mina/actions/offchain-state-rollup.ts b/src/lib/mina/actions/offchain-state-rollup.ts index 985eb2816b..f4cfa6d009 100644 --- a/src/lib/mina/actions/offchain-state-rollup.ts +++ b/src/lib/mina/actions/offchain-state-rollup.ts @@ -126,8 +126,10 @@ function merkleUpdateBatch( // prove that the witness and `actualPreviousValue` is correct, by comparing the implied root and key // note: this just works if the (key, value) is a (0,0) dummy, because the value at the 0 key will always be 0 - witness.calculateIndex().assertEquals(key); - witness.calculateRoot(actualPreviousValue).assertEquals(intermediateRoot); + witness.calculateIndex().assertEquals(key, 'key mismatch'); + witness + .calculateRoot(actualPreviousValue) + .assertEquals(intermediateRoot, 'root mismatch'); // if an expected previous value was provided, check whether it matches the actual previous value // otherwise, the entire update in invalidated @@ -153,14 +155,14 @@ function merkleUpdateBatch( // ignore dummy value if (isDummy.toBoolean()) return; - intermediateTree.get().setLeaf(key.toBigInt(), value); + intermediateTree.get().setLeaf(key.toBigInt(), value.toConstant()); intermediateUpdates.push({ key, value }); if (isCheckPoint.toBoolean()) { // if the update was valid, apply the intermediate updates to the actual tree if (wasValidUpdate.toBoolean()) { intermediateUpdates.forEach(({ key, value }) => { - tree.get().setLeaf(key.toBigInt(), value); + tree.get().setLeaf(key.toBigInt(), value.toConstant()); }); } // otherwise, we have to roll back the intermediate tree (TODO: inefficient) @@ -306,7 +308,7 @@ function OffchainStateRollup({ actionState: iterator.hash, }); let proof = await RollupProof.dummy(inputState, finalState, 2, 15); - return { proof, tree }; + return { proof, tree, nProofs: 0 }; } // base proof @@ -318,8 +320,10 @@ function OffchainStateRollup({ ); // recursive proofs + let nProofs = 1; for (let i = 1; ; i++) { if (iterator.isAtEnd().toBoolean()) break; + nProofs++; let slice = sliceActions(iterator, maxActionsPerBatch); proof = await offchainStateRollup.nextBatch( @@ -330,7 +334,7 @@ function OffchainStateRollup({ ); } - return { proof, tree }; + return { proof, tree, nProofs }; }, }; } From eafddc6943da9365657606e0104dcae0dd9087fb Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 May 2024 13:02:54 +0200 Subject: [PATCH 85/97] change and fix test --- .../actions/offchain-contract.unit-test.ts | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 92279b937d..50a860dcdf 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -104,8 +104,6 @@ if (proofsEnabled) { console.timeEnd('compile contract'); } -Local.setProofsEnabled(false); - // deploy and create first account console.time('deploy'); @@ -148,39 +146,38 @@ console.timeEnd('settle 1'); // check balance and supply await check({ expectedSupply: 1000n, expectedSenderBalance: 1000n }); -// transfer +// transfer (should succeed) console.time('transfer'); await Mina.transaction(sender, async () => { - // first call (should succeed) await contract.transfer(sender, receiver, UInt64.from(100)); - - // more calls that should all fail - // (these are enough to need two proof steps during settlement) - await contract.transfer(sender, receiver, UInt64.from(200)); - await contract.transfer(sender, receiver, UInt64.from(300)); - await contract.transfer(sender, receiver, UInt64.from(400)); - await contract.transfer(sender, receiver, UInt64.from(500)); }) .sign([sender.key]) .prove() .send(); console.timeEnd('transfer'); -// create another account +console.time('more transfers'); +Local.setProofsEnabled(false); // we run these without proofs to save time -console.time('create account 2'); await Mina.transaction(sender, async () => { - await contract.createAccount(other, UInt64.from(500)); + // more transfers that should fail + // (these are enough to need two proof steps during settlement) + await contract.transfer(sender, receiver, UInt64.from(200)); + await contract.transfer(sender, receiver, UInt64.from(300)); + await contract.transfer(sender, receiver, UInt64.from(400)); + await contract.transfer(sender, receiver, UInt64.from(500)); + + // create another account (should succeed) + await contract.createAccount(other, UInt64.from(555)); }) .sign([sender.key]) .prove() .send(); -console.timeEnd('create account 2'); +console.timeEnd('more transfers'); // settle - -Local.setProofsEnabled(true); +Local.setProofsEnabled(proofsEnabled); console.time('settlement proof 2'); proof = await offchainState.createSettlementProof(); @@ -194,7 +191,7 @@ await Mina.transaction(sender, () => contract.settle(proof)) console.timeEnd('settle 2'); // check balance and supply -await check({ expectedSupply: 1500n, expectedSenderBalance: 900n }); +await check({ expectedSupply: 1555n, expectedSenderBalance: 900n }); // test helper @@ -210,9 +207,10 @@ async function check({ let balanceSender = (await contract.getBalance(sender)).toBigInt(); let balanceReceiver = (await contract.getBalance(receiver)).toBigInt(); + let balanceOther = (await contract.getBalance(other)).toBigInt(); console.log('balance (sender)', balanceSender); console.log('balance (recv)', balanceReceiver); - assert.strictEqual(balanceSender + balanceReceiver, supply); + assert.strictEqual(balanceSender + balanceReceiver + balanceOther, supply); assert.strictEqual(balanceSender, expectedSenderBalance); } From 5ae2efdade698aabef825aa0e0382b41f2e305de Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 May 2024 13:45:36 +0200 Subject: [PATCH 86/97] further tweak option type --- src/lib/provable/option.ts | 65 ++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/src/lib/provable/option.ts b/src/lib/provable/option.ts index 8f51f1cb12..b4b34e0190 100644 --- a/src/lib/provable/option.ts +++ b/src/lib/provable/option.ts @@ -1,7 +1,8 @@ -import { From, InferValue } from '../../bindings/lib/provable-generic.js'; +import { InferValue } from '../../bindings/lib/provable-generic.js'; import { emptyValue } from '../proof-system/zkprogram.js'; import { Provable } from './provable.js'; import { InferProvable, Struct } from './types/struct.js'; +import { provable } from './types/provable-derivers.js'; import { Bool } from './wrapped.js'; export { Option, OptionOrValue }; @@ -11,7 +12,11 @@ type Option = { isSome: Bool; value: T } & { orElse(defaultValue: T | V): T; }; -type OptionOrValue = { isSome: boolean | Bool; value: V | T }; +type OptionOrValue = + | { isSome: boolean | Bool; value: T | V } + | T + | V + | undefined; /** * Define an optional version of a provable type. @@ -33,26 +38,55 @@ function Option>( type: A ): Provable< Option, InferValue>, - // TODO make V | undefined the value type - { isSome: boolean; value: InferValue } + InferValue | undefined > & { - fromValue: ( - value: From<{ isSome: typeof Bool; value: A }> - ) => Option, InferValue>; - from(value?: InferProvable): Option, InferValue>; + fromValue( + value: + | { isSome: boolean | Bool; value: InferProvable | InferValue } + | InferProvable + | InferValue + | undefined + ): Option, InferValue>; + from( + value?: InferProvable | InferValue + ): Option, InferValue>; none(): Option, InferValue>; } { type T = InferProvable; type V = InferValue; + let strictType: Provable = type; - const Super = Struct({ isSome: Bool, value: type as Provable }); + // construct a provable with a JS type of `T | undefined` + const PlainOption: Provable< + { isSome: Bool; value: T }, + { isSome: boolean; value: V } + > = provable({ isSome: Bool, value: strictType }); + + const RawOption = { + ...PlainOption, + + toValue({ isSome, value }: { isSome: Bool; value: T }) { + return isSome.toBoolean() ? strictType.toValue(value) : undefined; + }, + + fromValue(value: OptionOrValue) { + if (value === undefined) + return { isSome: Bool(false), value: emptyValue(strictType) }; + // TODO: this isn't 100% robust. We would need recursive type validation on any nested objects to make it work + if (typeof value === 'object' && 'isSome' in value) + return PlainOption.fromValue(value as any); + return { isSome: Bool(true), value: strictType.fromValue(value) }; + }, + }; + + const Super = Struct(RawOption); return class Option_ extends Super { orElse(defaultValue: T | V): T { return Provable.if( this.isSome, - type, + strictType, this.value, - type.fromValue(defaultValue) + strictType.fromValue(defaultValue) ); } @@ -63,8 +97,11 @@ function Option>( static from(value?: V | T) { return value === undefined - ? new Option_({ isSome: Bool(false), value: emptyValue(type) }) - : new Option_({ isSome: Bool(true), value: type.fromValue(value) }); + ? new Option_({ isSome: Bool(false), value: emptyValue(strictType) }) + : new Option_({ + isSome: Bool(true), + value: strictType.fromValue(value), + }); } static none() { return Option_.from(undefined); @@ -73,7 +110,7 @@ function Option>( static fromFields(fields: any[], aux?: any): Option_ { return new Option_(Super.fromFields(fields, aux)); } - static fromValue(value: { isSome: boolean | Bool; value: V | T }) { + static fromValue(value: OptionOrValue) { return new Option_(Super.fromValue(value)); } }; From 036d798655c867f8202b4f312e9f77a16461d01d Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 May 2024 13:46:02 +0200 Subject: [PATCH 87/97] tweak test --- src/lib/mina/actions/offchain-contract.unit-test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lib/mina/actions/offchain-contract.unit-test.ts b/src/lib/mina/actions/offchain-contract.unit-test.ts index 50a860dcdf..882e86efc4 100644 --- a/src/lib/mina/actions/offchain-contract.unit-test.ts +++ b/src/lib/mina/actions/offchain-contract.unit-test.ts @@ -32,7 +32,11 @@ class ExampleContract extends SmartContract { @method async createAccount(address: PublicKey, amountToMint: UInt64) { - offchainState.fields.accounts.overwrite(address, amountToMint); + // setting `from` to `undefined` means that the account must not exist yet + offchainState.fields.accounts.update(address, { + from: undefined, + to: amountToMint, + }); // TODO using `update()` on the total supply means that this method // can only be called once every settling cycle @@ -166,10 +170,12 @@ await Mina.transaction(sender, async () => { await contract.transfer(sender, receiver, UInt64.from(200)); await contract.transfer(sender, receiver, UInt64.from(300)); await contract.transfer(sender, receiver, UInt64.from(400)); - await contract.transfer(sender, receiver, UInt64.from(500)); // create another account (should succeed) await contract.createAccount(other, UInt64.from(555)); + + // create existing account again (should fail) + await contract.createAccount(receiver, UInt64.from(333)); }) .sign([sender.key]) .prove() From 41244454e3a49b11cb999c491fcd4bd07b5d2de9 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 May 2024 13:48:37 +0200 Subject: [PATCH 88/97] minor comment --- src/lib/provable/option.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/provable/option.ts b/src/lib/provable/option.ts index b4b34e0190..372bcd5f6c 100644 --- a/src/lib/provable/option.ts +++ b/src/lib/provable/option.ts @@ -74,7 +74,7 @@ function Option>( return { isSome: Bool(false), value: emptyValue(strictType) }; // TODO: this isn't 100% robust. We would need recursive type validation on any nested objects to make it work if (typeof value === 'object' && 'isSome' in value) - return PlainOption.fromValue(value as any); + return PlainOption.fromValue(value as any); // type-cast here is ok, matches implementation return { isSome: Bool(true), value: strictType.fromValue(value) }; }, }; From 6a10121623f650a19324cf62c8f5db5210b8c66b Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 May 2024 13:49:40 +0200 Subject: [PATCH 89/97] 1.2.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9bbd0480be..97fb5f146c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "o1js", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "o1js", - "version": "1.1.0", + "version": "1.2.0", "license": "Apache-2.0", "dependencies": { "blakejs": "1.2.1", diff --git a/package.json b/package.json index 347810c150..8ad4033e20 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "o1js", "description": "TypeScript framework for zk-SNARKs and zkApps", - "version": "1.1.0", + "version": "1.2.0", "license": "Apache-2.0", "homepage": "https://github.com/o1-labs/o1js/", "keywords": [ From b958aa119e1a76e133df738a52ea6aef83923432 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 May 2024 13:56:13 +0200 Subject: [PATCH 90/97] changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3215c7dd3d..6bbe615548 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm _Security_ in case of vulnerabilities. --> -## [Unreleased](https://github.com/o1-labs/o1js/compare/4a17de857...HEAD) +## [Unreleased](https://github.com/o1-labs/o1js/compare/6a1012162...HEAD) + +## [1.2.0](https://github.com/o1-labs/o1js/compare/4a17de857...6a1012162) - 2024-05-14 ### Added +- **Offchain storage MVP** exported under `Experimental.OffchainStorage` https://github.com/o1-labs/o1js/pull/1630 https://github.com/o1-labs/o1js/pull/1652 + - allows you to store any number of fields and key-value maps on your zkApp + - implemented using actions which define an offchain Merkle tree - `Option` for defining an optional version of any provable type https://github.com/o1-labs/o1js/pull/1630 - `MerkleTree.clone()` and `MerkleTree.getLeaf()`, new convenience methods for merkle trees https://github.com/o1-labs/o1js/pull/1630 - `MerkleList.forEach()`, a simple and safe way for iterating over a `MerkleList` From 0d10ff2e97a76a52afd24bf3138d908961673740 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 15 May 2024 14:01:42 +0200 Subject: [PATCH 91/97] changelog fix --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bbe615548..29d1dcc956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added -- **Offchain storage MVP** exported under `Experimental.OffchainStorage` https://github.com/o1-labs/o1js/pull/1630 https://github.com/o1-labs/o1js/pull/1652 +- **Offchain state MVP** exported under `Experimental.OffchainState` https://github.com/o1-labs/o1js/pull/1630 https://github.com/o1-labs/o1js/pull/1652 - allows you to store any number of fields and key-value maps on your zkApp - implemented using actions which define an offchain Merkle tree - `Option` for defining an optional version of any provable type https://github.com/o1-labs/o1js/pull/1630 From 32955a8e17d04343e9531eef402626727bf6c500 Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 15 May 2024 15:44:55 +0200 Subject: [PATCH 92/97] fix changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edef6f20df..de4d0dd104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased](https://github.com/o1-labs/o1js/compare/6a1012162...HEAD) +### Fixes + +- Fix type inference for `method.returns(Type)`, to require a matching return signature https://github.com/o1-labs/o1js/pull/1653 + ## [1.2.0](https://github.com/o1-labs/o1js/compare/4a17de857...6a1012162) - 2024-05-14 ### Added @@ -41,7 +45,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Fixes - Fix absolute imports which prevented compilation in some TS projects that used o1js https://github.com/o1-labs/o1js/pull/1628 -- Fix type inference for `method.returns(Type)`, to require a matching return signature https://github.com/o1-labs/o1js/pull/1653 ## [1.1.0](https://github.com/o1-labs/o1js/compare/1ad7333e9e...4a17de857) - 2024-04-30 From 40abf08828a349ce1d02a548a59fe44e3c41df0d Mon Sep 17 00:00:00 2001 From: Serhii Shymkiv Date: Wed, 15 May 2024 20:14:19 +0300 Subject: [PATCH 93/97] Update zkApp CLI links to NPM. --- CONTRIBUTING.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 850880db1c..46a6d9a97b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ To ensure consistency within the o1js ecosystem and ease review and use by our t - `npm install ` works and is all that is needed to use the package. - o1js must be listed as a [peer dependency](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#peerdependencies). - If applicable, the package must work both on the web and in NodeJS. -- The package is created using the [zkApp CLI](https://github.com/o1-labs/zkapp-cli) (recommended). +- The package is created using the [zkApp CLI](https://www.npmjs.com/package/zkapp-cli) (recommended). If you did not create the package using the zkApp CLI, follow these guidelines for code consistency: - Use TypeScript, and export types from `d.ts` files. We suggest that you base your tsconfig on the [tsconfig.json](./tsconfig.json) that o1js uses. - Code must be auto-formatted with [prettier](https://prettier.io/). We encourage you to use [.prettierrc.cjs](./.prettierrc.cjs), the same prettier config as o1js. diff --git a/README.md b/README.md index 637a852691..20a280ceae 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The easiest way to write zk programs is using o1js. o1js is a TypeScript library for [zk-SNARKs](https://minaprotocol.com/blog/what-are-zk-snarks) and zkApps. You can use o1js to write zk smart contracts based on zero-knowledge proofs for the Mina Protocol. -o1js is automatically included when you create a project using the [Mina zkApp CLI](https://github.com/o1-labs/zkapp-cli). +o1js is automatically included when you create a project using the [zkApp CLI](https://www.npmjs.com/package/zkapp-cli). ## Learn More From 56c9c4801d797509603a70187dce137302eab0f5 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 May 2024 13:03:19 +0200 Subject: [PATCH 94/97] add missing empty() on Group and Scalar --- src/lib/provable/group.ts | 8 ++++---- src/lib/provable/scalar.ts | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/lib/provable/group.ts b/src/lib/provable/group.ts index 10a30e3fc7..5584e7d59d 100644 --- a/src/lib/provable/group.ts +++ b/src/lib/provable/group.ts @@ -352,6 +352,10 @@ class Group { fields: [x.x, x.y], }; } + + static empty() { + return Group.zero; + } } // internal helpers @@ -360,10 +364,6 @@ function isConstant(g: Group) { return g.x.isConstant() && g.y.isConstant(); } -function toTuple(g: Group): [0, FieldVar, FieldVar] { - return [0, g.x.value, g.y.value]; -} - function toProjective(g: Group) { return Pallas.fromAffine({ x: g.x.toBigInt(), diff --git a/src/lib/provable/scalar.ts b/src/lib/provable/scalar.ts index 2277b304d3..fe4602fbd1 100644 --- a/src/lib/provable/scalar.ts +++ b/src/lib/provable/scalar.ts @@ -317,6 +317,10 @@ class Scalar implements ShiftedScalar { static fromJSON(x: string) { return Scalar.from(SignableFq.fromJSON(x)); } + + static empty() { + return Scalar.from(0n); + } } // internal helpers From a569bc9a6a2b4f3056bdc15475e1b586f8b86c36 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 May 2024 13:13:19 +0200 Subject: [PATCH 95/97] cover empty() in unit test --- src/lib/provable/test/struct.unit-test.ts | 24 ++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/lib/provable/test/struct.unit-test.ts b/src/lib/provable/test/struct.unit-test.ts index 5fae21d659..5554aafc81 100644 --- a/src/lib/provable/test/struct.unit-test.ts +++ b/src/lib/provable/test/struct.unit-test.ts @@ -18,6 +18,8 @@ import { Bool } from '../bool.js'; import assert from 'assert/strict'; import { FieldType } from '../core/fieldvar.js'; import { From } from '../../../bindings/lib/provable-generic.js'; +import { Group } from '../group.js'; +import { modifiedField } from '../types/fields.js'; let type = provable({ nested: { a: Number, b: Boolean }, @@ -85,6 +87,26 @@ expect(jsValue).toEqual({ expect(type.fromValue(jsValue)).toEqual(value); +// empty +let empty = type.empty(); +expect(empty).toEqual({ + nested: { a: 0, b: false }, + other: '', + pk: PublicKey.empty(), + bool: new Bool(false), + uint: [UInt32.zero, UInt32.zero], +}); + +// empty with Group +expect(provable({ value: Group }).empty()).toEqual({ value: Group.zero }); + +// fails with a clear error on input without an empty method +const FieldWithoutEmpty = modifiedField({}); +delete (FieldWithoutEmpty as any).empty; +expect(() => provable({ value: FieldWithoutEmpty }).empty()).toThrow( + 'Expected `empty()` method on anonymous type object' +); + // check await Provable.runAndCheck(() => { type.check(value); @@ -120,7 +142,7 @@ class MyStructPure extends Struct({ uint: [UInt32, UInt32], }) {} -// Struct.from() works on both js and provable inputs +// Struct.fromValue() works on both js and provable inputs let myStructInput = { nested: { a: Field(1), b: 2n }, From 5e6e760015dac02f0e348a6e5f121cc6dcaa07b0 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 May 2024 13:32:30 +0200 Subject: [PATCH 96/97] bindings --- src/bindings | 2 +- src/lib/provable/types/struct.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/bindings b/src/bindings index 03241cd44f..55481e7444 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 03241cd44f895249fc28dda7bd133479f2ef0de2 +Subproject commit 55481e7444b48cd452375bc8a23a5a5fc42264b7 diff --git a/src/lib/provable/types/struct.ts b/src/lib/provable/types/struct.ts index 587b2efa3f..60e9d23c30 100644 --- a/src/lib/provable/types/struct.ts +++ b/src/lib/provable/types/struct.ts @@ -1,7 +1,6 @@ import { Field, Bool, Scalar, Group } from '../wrapped.js'; import { provable, - provablePure, provableTuple, HashInput, NonMethods, From 6a9d4d3c0f2ada3537263b803a06382515a09143 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 16 May 2024 17:01:05 +0200 Subject: [PATCH 97/97] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de4d0dd104..cbb7280cde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Fixes - Fix type inference for `method.returns(Type)`, to require a matching return signature https://github.com/o1-labs/o1js/pull/1653 +- Fix `Struct.empty()` returning a garbage object when one of the base types doesn't support `empty()` https://github.com/o1-labs/o1js/pull/1657 ## [1.2.0](https://github.com/o1-labs/o1js/compare/4a17de857...6a1012162) - 2024-05-14