Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enforce state preconditions #267

Merged
merged 5 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() {
Copy link
Contributor

@MartinMinkov MartinMinkov Jun 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick question, when is assertNothing intended to be used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No specific use case I had in mind, I just wanted to have an escape hatch to get rid of the error if you have some reason to use .get() without having a precondition

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great error message :D

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