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 = {