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

Fix smart contracts not verifying proofs #1302

Merged
merged 14 commits into from
Dec 11, 2023
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
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
97 changes: 97 additions & 0 deletions src/tests/fake-proof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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 () => {
const brokenProof = await RecursiveProgram.verifyReal(proof);
assert(await verify(brokenProof, programVk.data));
}, '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 rejects proof
mitschabaude marked this conversation as resolved.
Show resolved Hide resolved
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 🎉');