Skip to content

Commit

Permalink
Merge pull request #267 from o1-labs/feature/enforce-state-precondition
Browse files Browse the repository at this point in the history
Enforce state preconditions
  • Loading branch information
mitschabaude committed Jun 28, 2022
2 parents 5e3ad0a + 76a6227 commit 1068560
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 1 deletion.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/examples/simple_zkapp.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Expand Down
1 change: 1 addition & 0 deletions src/examples/simple_zkapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
1 change: 1 addition & 0 deletions src/examples/simple_zkapp_berkeley.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
1 change: 1 addition & 0 deletions src/examples/simple_zkapp_with_proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Expand Down
62 changes: 61 additions & 1 deletion src/lib/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -17,6 +20,7 @@ type State<A> = {
set(a: A): void;
fetch(): Promise<A | undefined>;
assertEquals(a: A): void;
assertNothing(): void;
};
function State<A>(): State<A> {
return createState<A>();
Expand Down Expand Up @@ -64,8 +68,11 @@ function state<A>(stateType: AsFieldElements<A>) {
stateType: stateType,
instance: this,
class: ZkappClass,
wasConstrained: false,
wasRead: false,
cachedVariable: undefined,
};
(this._ ?? (this._ = {}))[key] = v;
(this._ ??= {})[key] = v;
},
});
};
Expand Down Expand Up @@ -121,6 +128,9 @@ type StateAttachedContract<A> = {
stateType: AsFieldElements<A>;
instance: SmartContract;
class: typeof SmartContract;
wasRead: boolean;
wasConstrained: boolean;
cachedVariable?: A;
};

type InternalStateType<A> = State<A> & { _contract?: StateAttachedContract<A> };
Expand Down Expand Up @@ -156,13 +166,26 @@ function createState<T>(): InternalStateType<T> {
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() {
if (this._contract === undefined)
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[];
Expand Down Expand Up @@ -209,6 +232,8 @@ function createState<T>(): InternalStateType<T> {
}
let state = this._contract.stateType.ofFields(stateAsFields);
this._contract.stateType.check?.(state);
this._contract.wasRead = true;
this._contract.cachedVariable = state;
return state;
},

Expand Down Expand Up @@ -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,
{
Expand All @@ -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<unknown> | 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]);
}
4 changes: 4 additions & 0 deletions src/lib/zkapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
compileProgram,
Proof,
} from './proof_system';
import { assertStatePrecondition, cleanStatePrecondition } from './state';

export { deploy, DeployArgs, signFeePayer, declareMethods };

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 = {
Expand Down

0 comments on commit 1068560

Please sign in to comment.