diff --git a/CHANGELOG.md b/CHANGELOG.md index b271fbf1aa..b62053c16e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/o1-labs/snarkyjs/compare/2375f08...HEAD) +### Added + +- Implement recursion + - RFC: https://github.com/o1-labs/snarkyjs/issues/89 + - Enable smart contract methods to take previous proofs as argument + - Add new primitive `ZkProgram` which represents a collection of circuits that produce instances of the same proof. + Like smart contracts, ZkPrograms can produce execution proofs and merge in previous proofs, but they are more general and suitable for roll-up-type systems. + - Supported numbers of merged proofs are 0, 1 and 2 + - PRs: https://github.com/o1-labs/snarkyjs/pull/245 https://github.com/o1-labs/snarkyjs/pull/250 https://github.com/o1-labs/snarkyjs/pull/261 + +### Changed + +- BREAKING CHANGE: Make on-chain state consistent with other preconditions - throw an error when state is not explicitly constrained https://github.com/o1-labs/snarkyjs/pull/267 + ## [0.4.3](https://github.com/o1-labs/snarkyjs/compare/e66f08d...2375f08) ### Added diff --git a/src/examples/simple_zkapp.mjs b/src/examples/simple_zkapp.mjs index 6b8e1a27c5..d2d9a043c7 100644 --- a/src/examples/simple_zkapp.mjs +++ b/src/examples/simple_zkapp.mjs @@ -25,6 +25,7 @@ class SimpleZkapp extends SmartContract { update(y) { this.account.balance.assertEquals(this.account.balance.get()); let x = this.x.get(); + this.x.assertEquals(x); this.x.set(x.add(y)); } } diff --git a/src/examples/simple_zkapp.ts b/src/examples/simple_zkapp.ts index 68e70300da..63a055c50d 100644 --- a/src/examples/simple_zkapp.ts +++ b/src/examples/simple_zkapp.ts @@ -32,6 +32,7 @@ class SimpleZkapp extends SmartContract { @method update(y: Field) { let x = this.x.get(); + this.x.assertEquals(x); this.x.set(x.add(y)); } diff --git a/src/examples/simple_zkapp_berkeley.ts b/src/examples/simple_zkapp_berkeley.ts index 079f07496a..7039784ba5 100644 --- a/src/examples/simple_zkapp_berkeley.ts +++ b/src/examples/simple_zkapp_berkeley.ts @@ -35,6 +35,7 @@ class SimpleZkapp extends SmartContract { @method update(y: Field) { let x = this.x.get(); + this.x.assertEquals(x); y.assertGt(0); this.x.set(x.add(y)); } diff --git a/src/examples/simple_zkapp_with_proof.ts b/src/examples/simple_zkapp_with_proof.ts index 0de1e5e81c..bf2a1f283b 100644 --- a/src/examples/simple_zkapp_with_proof.ts +++ b/src/examples/simple_zkapp_with_proof.ts @@ -41,6 +41,7 @@ class NotSoSimpleZkapp extends SmartContract { oldProof.verify(); trivialProof.verify(); let x = this.x.get(); + this.x.assertEquals(x); this.x.set(x.add(y)); } } diff --git a/src/lib/state.ts b/src/lib/state.ts index 079fa52a99..d3ba1582da 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -7,7 +7,10 @@ import { Account, fetchAccount } from './fetch'; import * as GlobalContext from './global-context'; import { SmartContract } from './zkapp'; +// external API export { State, state, declareState }; +// internal API +export { assertStatePrecondition, cleanStatePrecondition }; /** * Gettable and settable state that can be checked for equality. @@ -17,6 +20,7 @@ type State = { set(a: A): void; fetch(): Promise; assertEquals(a: A): void; + assertNothing(): void; }; function State(): State { return createState(); @@ -64,8 +68,11 @@ function state(stateType: AsFieldElements) { stateType: stateType, instance: this, class: ZkappClass, + wasConstrained: false, + wasRead: false, + cachedVariable: undefined, }; - (this._ ?? (this._ = {}))[key] = v; + (this._ ??= {})[key] = v; }, }); }; @@ -121,6 +128,9 @@ type StateAttachedContract = { stateType: AsFieldElements; instance: SmartContract; class: typeof SmartContract; + wasRead: boolean; + wasConstrained: boolean; + cachedVariable?: A; }; type InternalStateType = State & { _contract?: StateAttachedContract }; @@ -156,6 +166,15 @@ function createState(): InternalStateType { x ); }); + this._contract.wasConstrained = true; + }, + + assertNothing() { + if (this._contract === undefined) + throw Error( + 'assertNothing can only be called when the State is assigned to a SmartContract @state.' + ); + this._contract.wasConstrained = true; }, get() { @@ -163,6 +182,10 @@ function createState(): InternalStateType { throw Error( 'get can only be called when the State is assigned to a SmartContract @state.' ); + if (this._contract.cachedVariable !== undefined) { + this._contract.wasRead = true; + return this._contract.cachedVariable; + } let layout = getLayoutPosition(this._contract); let address: PublicKey = this._contract.instance.address; let stateAsFields: Field[]; @@ -209,6 +232,8 @@ function createState(): InternalStateType { } let state = this._contract.stateType.ofFields(stateAsFields); this._contract.stateType.check?.(state); + this._contract.wasRead = true; + this._contract.cachedVariable = state; return state; }, @@ -267,6 +292,7 @@ function getLayout(scClass: typeof SmartContract) { return sc.layout; } +// per-smart contract class context for keeping track of state layout const smartContracts = new WeakMap< typeof SmartContract, { @@ -276,3 +302,37 @@ const smartContracts = new WeakMap< >(); const reservedPropNames = new Set(['_methods', '_']); + +function assertStatePrecondition(sc: SmartContract) { + try { + for (let [key, context] of getStateContexts(sc)) { + // check if every state that was read was also contrained + if (!context?.wasRead || context.wasConstrained) continue; + // we accessed a precondition field but not constrained it explicitly - throw an error + let errorMessage = `You used \`this.${key}.get()\` without adding a precondition that links it to the actual on-chain state. +Consider adding this line to your code: +this.${key}.assertEquals(this.${key}.get());`; + throw Error(errorMessage); + } + } finally { + cleanStatePrecondition(sc); + } +} + +function cleanStatePrecondition(sc: SmartContract) { + for (let [, context] of getStateContexts(sc)) { + if (context === undefined) continue; + context.wasRead = false; + context.wasConstrained = false; + context.cachedVariable = undefined; + } +} + +function getStateContexts( + sc: SmartContract +): [string, StateAttachedContract | undefined][] { + let scClass = sc.constructor as typeof SmartContract; + let scInfo = smartContracts.get(scClass); + if (scInfo === undefined) return []; + return scInfo.states.map(([key]) => [key, (sc as any)[key]?._contract]); +} diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index 3e8e3ab1bd..020122b935 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -24,6 +24,7 @@ import { compileProgram, Proof, } from './proof_system'; +import { assertStatePrecondition, cleanStatePrecondition } from './state'; export { deploy, DeployArgs, signFeePayer, declareMethods }; @@ -82,6 +83,7 @@ function wrapMethod( methodIntf: MethodInterface ) { return function wrappedMethod(this: SmartContract, ...actualArgs: any[]) { + cleanStatePrecondition(this); if (inCheckedComputation() || Mina.currentTransaction === undefined) { if (inCheckedComputation()) { // inside prover / compile, the method is always called with the public input as first argument @@ -100,6 +102,7 @@ function wrapMethod( // TODO: this needs to be done in a unified way for all parties that are created assertPreconditionInvariants(this.self); cleanPreconditionsCache(this.self); + assertStatePrecondition(this); return result; } else { // in a transaction, also add a lazy proof to the self party @@ -109,6 +112,7 @@ function wrapMethod( // TODO: double-check that this works on all possible inputs, e.g. CircuitValue, snarkyjs primitives let clonedArgs = cloneCircuitValue(actualArgs); let result = method.apply(this, actualArgs); + assertStatePrecondition(this); let auth = this.self.authorization; if (!('kind' in auth || 'proof' in auth || 'signature' in auth)) { this.self.authorization = {