Skip to content

Commit

Permalink
Merge pull request #1302 from o1-labs/fix/any-proof-verifies
Browse files Browse the repository at this point in the history
Fix smart contracts not verifying proofs
  • Loading branch information
mitschabaude committed Dec 11, 2023
2 parents 74d1b1b + 515b1c0 commit 3978a97
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- `this.network.x.getAndAssertEquals()` is now `this.network.x.getAndRequireEquals()` https://github.com/o1-labs/o1js/pull/1265
- `Provable.constraintSystem()` and `{ZkProgram,SmartContract}.analyzeMethods()` return a `print()` method for pretty-printing the constraint system https://github.com/o1-labs/o1js/pull/1240

### Fixed

- Fix missing recursive verification of proofs in smart contracts https://github.com/o1-labs/o1js/pull/1302

## [0.14.2](https://github.com/o1-labs/o1js/compare/26363465d...1ad7333e9e)

### Breaking changes
Expand Down
1 change: 1 addition & 0 deletions run-ci-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ case $TEST_TYPE in
./run src/examples/simple_zkapp.ts --bundle
./run src/examples/zkapps/reducer/reducer_composite.ts --bundle
./run src/examples/zkapps/composability.ts --bundle
./run src/tests/fake-proof.ts
;;

"Voting integration tests")
Expand Down
6 changes: 5 additions & 1 deletion src/lib/circuit_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
import { Provable } from './provable.js';
import { assert } from './errors.js';
import { inCheckedComputation } from './provable-context.js';
import { Proof } from './proof_system.js';

// external API
export {
Expand Down Expand Up @@ -597,10 +598,13 @@ function cloneCircuitValue<T>(obj: T): T {
) as any as T;
if (ArrayBuffer.isView(obj)) return new (obj.constructor as any)(obj);

// o1js primitives aren't cloned
// o1js primitives and proofs aren't cloned
if (isPrimitive(obj)) {
return obj;
}
if (obj instanceof Proof) {
return obj;
}

// cloning strategy that works for plain objects AND classes whose constructor only assigns properties
let propertyDescriptors: Record<string, PropertyDescriptor> = {};
Expand Down
102 changes: 94 additions & 8 deletions src/lib/proof_system.unit-test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,106 @@
import { Field } from './core.js';
import { Field, Bool } from './core.js';
import { Struct } from './circuit_value.js';
import { UInt64 } from './int.js';
import { ZkProgram } from './proof_system.js';
import {
CompiledTag,
Empty,
Proof,
ZkProgram,
picklesRuleFromFunction,
sortMethodArguments,
} from './proof_system.js';
import { expect } from 'expect';
import { Pickles, ProvablePure, Snarky } from '../snarky.js';
import { AnyFunction } from './util/types.js';
import { snarkContext } from './provable-context.js';
import { it } from 'node:test';
import { Provable } from './provable.js';
import { bool, equivalentAsync, field, record } from './testing/equivalent.js';
import { FieldConst, FieldVar } from './field.js';

const EmptyProgram = ZkProgram({
name: 'empty',
publicInput: Field,
methods: {
run: {
privateInputs: [],
method: (publicInput: Field) => {},
},
},
methods: { run: { privateInputs: [], method: (_) => {} } },
});

class EmptyProof extends ZkProgram.Proof(EmptyProgram) {}

// unit-test zkprogram creation helpers:
// -) sortMethodArguments
// -) picklesRuleFromFunction

it('pickles rule creation', async () => {
// a rule that verifies a proof conditionally, and returns the proof's input as output
function main(proof: EmptyProof, shouldVerify: Bool) {
proof.verifyIf(shouldVerify);
return proof.publicInput;
}
let privateInputs = [EmptyProof, Bool];

// collect method interface
let methodIntf = sortMethodArguments('mock', 'main', privateInputs, Proof);

expect(methodIntf).toEqual({
methodName: 'main',
witnessArgs: [Bool],
proofArgs: [EmptyProof],
allArgs: [
{ type: 'proof', index: 0 },
{ type: 'witness', index: 0 },
],
genericArgs: [],
});

// store compiled tag
CompiledTag.store(EmptyProgram, 'mock tag');

// create pickles rule
let rule: Pickles.Rule = picklesRuleFromFunction(
Empty as ProvablePure<any>,
Field as ProvablePure<any>,
main as AnyFunction,
{ name: 'mock' },
methodIntf,
[]
);

await equivalentAsync(
{ from: [field, bool], to: record({ field, bool }) },
{ runs: 5 }
)(
(field, bool) => ({ field, bool }),
async (field, bool) => {
let dummy = await EmptyProof.dummy(field, undefined, 0);
let field_: FieldConst = [0, 0n];
let bool_: FieldConst = [0, 0n];

Provable.runAndCheck(() => {
// put witnesses in snark context
snarkContext.get().witnesses = [dummy, bool];

// call pickles rule
let {
publicOutput: [, publicOutput],
shouldVerify: [, shouldVerify],
} = rule.main([0]);

// `publicOutput` and `shouldVerify` are as expected
Snarky.field.assertEqual(publicOutput, dummy.publicInput.value);
Snarky.field.assertEqual(shouldVerify, bool.value);

Provable.asProver(() => {
field_ = Snarky.field.readVar(publicOutput);
bool_ = Snarky.field.readVar(shouldVerify);
});
});

return { field: Field(field_), bool: Bool(FieldVar.constant(bool_)) };
}
);
});

// regression tests for some zkprograms
const emptyMethodsMetadata = EmptyProgram.analyzeMethods();
expect(emptyMethodsMetadata.run).toEqual(
expect.objectContaining({
Expand Down
7 changes: 1 addition & 6 deletions src/lib/testing/equivalent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { test, Random } from '../testing/property.js';
import { Provable } from '../provable.js';
import { deepEqual } from 'node:assert/strict';
import { Bool, Field } from '../core.js';
import { AnyFunction, Tuple } from '../util/types.js';

export {
equivalent,
Expand Down Expand Up @@ -433,12 +434,6 @@ function throwError(message?: string): any {
throw Error(message);
}

// helper types

type AnyFunction = (...args: any) => any;

type Tuple<T> = [] | [T, ...T[]];

// infer input types from specs

type Param1<In extends OrUnion<any, any>> = In extends {
Expand Down
4 changes: 3 additions & 1 deletion src/lib/util/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { assert } from '../errors.js';

export { Tuple, TupleN, AnyTuple, TupleMap };
export { AnyFunction, Tuple, TupleN, AnyTuple, TupleMap };

type AnyFunction = (...args: any) => any;

type Tuple<T> = [T, ...T[]] | [];
type AnyTuple = Tuple<any>;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/zkapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
FlexibleProvablePure,
InferProvable,
provable,
Struct,
toConstant,
} from './circuit_value.js';
import { Provable, getBlindingValue, memoizationContext } from './provable.js';
Expand Down Expand Up @@ -196,7 +195,8 @@ function wrapMethod(
let id = memoizationContext.enter({ ...context, blindingValue });
let result: unknown;
try {
result = method.apply(this, actualArgs.map(cloneCircuitValue));
let clonedArgs = actualArgs.map(cloneCircuitValue);
result = method.apply(this, clonedArgs);
} finally {
memoizationContext.leave(id);
}
Expand Down
96 changes: 96 additions & 0 deletions src/tests/fake-proof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
Mina,
PrivateKey,
SmartContract,
UInt64,
method,
ZkProgram,
verify,
} from 'o1js';
import assert from 'assert';

const RealProgram = ZkProgram({
name: 'real',
methods: {
make: {
privateInputs: [UInt64],
method(value: UInt64) {
let expected = UInt64.from(34);
value.assertEquals(expected);
},
},
},
});

const FakeProgram = ZkProgram({
name: 'fake',
methods: {
make: { privateInputs: [UInt64], method(_: UInt64) {} },
},
});

class RealProof extends ZkProgram.Proof(RealProgram) {}

const RecursiveProgram = ZkProgram({
name: 'broken',
methods: {
verifyReal: {
privateInputs: [RealProof],
method(proof: RealProof) {
proof.verify();
},
},
},
});

class RecursiveContract extends SmartContract {
@method verifyReal(proof: RealProof) {
proof.verify();
}
}

Mina.setActiveInstance(Mina.LocalBlockchain());
let publicKey = PrivateKey.random().toPublicKey();
let zkApp = new RecursiveContract(publicKey);

await RealProgram.compile();
await FakeProgram.compile();
let { verificationKey: contractVk } = await RecursiveContract.compile();
let { verificationKey: programVk } = await RecursiveProgram.compile();

// proof that should be rejected
const fakeProof = await FakeProgram.make(UInt64.from(99999));
const dummyProof = await RealProof.dummy(undefined, undefined, 0);

for (let proof of [fakeProof, dummyProof]) {
// zkprogram rejects proof
await assert.rejects(async () => {
await RecursiveProgram.verifyReal(proof);
}, 'recursive program rejects fake proof');

// contract rejects proof
await assert.rejects(async () => {
let tx = await Mina.transaction(() => zkApp.verifyReal(proof));
await tx.prove();
}, 'recursive contract rejects fake proof');
}

// proof that should be accepted
const realProof = await RealProgram.make(UInt64.from(34));

// zkprogram accepts proof
const brokenProof = await RecursiveProgram.verifyReal(realProof);
assert(
await verify(brokenProof, programVk.data),
'recursive program accepts real proof'
);

// contract accepts proof
let tx = await Mina.transaction(() => zkApp.verifyReal(realProof));
let [contractProof] = await tx.prove();
assert(
await verify(contractProof!, contractVk.data),
'recursive contract accepts real proof'
);

console.log('fake proof test passed 🎉');

0 comments on commit 3978a97

Please sign in to comment.