-
Notifications
You must be signed in to change notification settings - Fork 107
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
Expose sideloaded verification keys #1606
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
d29b5cb
Updated bindings to support sl vks
rpanic 2189f4b
Implemented JS API for sideloaded keys
rpanic c2f6891
Added two examples using DynamicProofs
rpanic 40c927d
Merge branch 'main' into feature/side-loaded-keys3
rpanic 4fff643
Changelog
rpanic 095fb28
Formatting
rpanic 964ef71
Zkprogram refactoring
rpanic c32ca2f
Fixed review comments
rpanic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule bindings
updated
15 files
+1 −1 | MINA_COMMIT | |
+89,616 −89,541 | compiled/node_bindings/o1js_node.bc.cjs | |
+1 −1 | compiled/node_bindings/o1js_node.bc.map | |
+284 −284 | compiled/node_bindings/plonk_wasm.cjs | |
+142 −142 | compiled/node_bindings/plonk_wasm.d.cts | |
+ − | compiled/node_bindings/plonk_wasm_bg.wasm | |
+122 −122 | compiled/node_bindings/plonk_wasm_bg.wasm.d.ts | |
+21 −21 | compiled/web_bindings/o1js_web.bc.js | |
+264 −264 | compiled/web_bindings/plonk_wasm.d.ts | |
+284 −284 | compiled/web_bindings/plonk_wasm.js | |
+ − | compiled/web_bindings/plonk_wasm_bg.wasm | |
+122 −122 | compiled/web_bindings/plonk_wasm_bg.wasm.d.ts | |
+2 −1 | crypto/constants.ts | |
+76 −0 | ocaml/lib/pickles_bindings.ml | |
+23 −1 | ocaml/lib/pickles_bindings.mli |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import { | ||
DynamicProof, | ||
Field, | ||
MerkleTree, | ||
MerkleWitness, | ||
Proof, | ||
SelfProof, | ||
Struct, | ||
VerificationKey, | ||
ZkProgram, | ||
verify, | ||
} from 'o1js'; | ||
|
||
/** | ||
* This example showcases how DynamicProofs can be used along with a merkletree that stores | ||
* the verification keys that can be used to verify it. | ||
* The MainProgram has two methods, addSideloadedProgram that adds a given verification key | ||
* to the tree, and validateUsingTree that uses a given tree leaf to verify a given child-proof | ||
* using the verification tree stored under that leaf. | ||
*/ | ||
|
||
const sideloadedProgram = ZkProgram({ | ||
name: 'childProgram', | ||
publicInput: Field, | ||
publicOutput: Field, | ||
methods: { | ||
compute: { | ||
privateInputs: [Field], | ||
async method(publicInput: Field, privateInput: Field) { | ||
return publicInput.add(privateInput); | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
class SideloadedProgramProof extends DynamicProof<Field, Field> { | ||
static publicInputType = Field; | ||
static publicOutputType = Field; | ||
static maxProofsVerified = 0 as const; | ||
} | ||
|
||
const tree = new MerkleTree(64); | ||
class MerkleTreeWitness extends MerkleWitness(64) {} | ||
|
||
class MainProgramState extends Struct({ | ||
treeRoot: Field, | ||
state: Field, | ||
}) {} | ||
|
||
const mainProgram = ZkProgram({ | ||
name: 'mainProgram', | ||
publicInput: MainProgramState, | ||
publicOutput: MainProgramState, | ||
methods: { | ||
addSideloadedProgram: { | ||
privateInputs: [VerificationKey, MerkleTreeWitness], | ||
async method( | ||
publicInput: MainProgramState, | ||
vk: VerificationKey, | ||
merkleWitness: MerkleTreeWitness | ||
) { | ||
// In practice, this method would be guarded via some access control mechanism | ||
const currentRoot = merkleWitness.calculateRoot(Field(0)); | ||
publicInput.treeRoot.assertEquals( | ||
currentRoot, | ||
'Provided merklewitness not correct or leaf not empty' | ||
); | ||
const newRoot = merkleWitness.calculateRoot(vk.hash); | ||
|
||
return new MainProgramState({ | ||
state: publicInput.state, | ||
treeRoot: newRoot, | ||
}); | ||
}, | ||
}, | ||
validateUsingTree: { | ||
privateInputs: [ | ||
SelfProof, | ||
VerificationKey, | ||
MerkleTreeWitness, | ||
SideloadedProgramProof, | ||
], | ||
async method( | ||
publicInput: MainProgramState, | ||
previous: Proof<MainProgramState, MainProgramState>, | ||
vk: VerificationKey, | ||
merkleWitness: MerkleTreeWitness, | ||
proof: SideloadedProgramProof | ||
) { | ||
// Verify previous program state | ||
previous.publicOutput.state.assertEquals(publicInput.state); | ||
previous.publicOutput.treeRoot.assertEquals(publicInput.treeRoot); | ||
|
||
// Verify inclusion of vk inside the tree | ||
const computedRoot = merkleWitness.calculateRoot(vk.hash); | ||
publicInput.treeRoot.assertEquals( | ||
computedRoot, | ||
'Tree witness with provided vk not correct' | ||
); | ||
|
||
proof.verify(vk); | ||
|
||
// Compute new state | ||
proof.publicInput.assertEquals(publicInput.state); | ||
const newState = proof.publicOutput; | ||
return new MainProgramState({ | ||
treeRoot: publicInput.treeRoot, | ||
state: newState, | ||
}); | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
console.log('Compiling circuits...'); | ||
const programVk = (await sideloadedProgram.compile()).verificationKey; | ||
const mainVk = (await mainProgram.compile()).verificationKey; | ||
|
||
console.log('Proving deployment of sideloaded key'); | ||
const rootBefore = tree.getRoot(); | ||
tree.setLeaf(1n, programVk.hash); | ||
const witness = new MerkleTreeWitness(tree.getWitness(1n)); | ||
|
||
const proof1 = await mainProgram.addSideloadedProgram( | ||
new MainProgramState({ | ||
treeRoot: rootBefore, | ||
state: Field(0), | ||
}), | ||
programVk, | ||
witness | ||
); | ||
|
||
console.log('Proving child program execution'); | ||
const childProof = await sideloadedProgram.compute(Field(0), Field(10)); | ||
|
||
console.log('Proving verification inside main program'); | ||
const proof2 = await mainProgram.validateUsingTree( | ||
proof1.publicOutput, | ||
proof1, | ||
programVk, | ||
witness, | ||
SideloadedProgramProof.fromProof(childProof) | ||
); | ||
|
||
const validProof2 = await verify(proof2, mainVk); | ||
console.log('ok?', validProof2); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { | ||
ZkProgram, | ||
Field, | ||
DynamicProof, | ||
Proof, | ||
VerificationKey, | ||
Undefined, | ||
verify, | ||
} from 'o1js'; | ||
|
||
/** | ||
* This example showcases mutual recursion (A -> B -> A) through two circuits that respectively | ||
* add or multiply a given publicInput. | ||
* Every multiplication or addition step consumes a previous proof from the other circuit to verify prior state. | ||
*/ | ||
|
||
class DynamicMultiplyProof extends DynamicProof<Undefined, Field> { | ||
static publicInputType = Undefined; | ||
static publicOutputType = Field; | ||
static maxProofsVerified = 1 as const; | ||
} | ||
|
||
const add = ZkProgram({ | ||
name: 'add', | ||
publicInput: Undefined, | ||
publicOutput: Field, | ||
methods: { | ||
performAddition: { | ||
privateInputs: [Field, DynamicMultiplyProof, VerificationKey], | ||
async method( | ||
field: Field, | ||
proof: DynamicMultiplyProof, | ||
vk: VerificationKey | ||
) { | ||
// TODO The incoming verification key isn't constrained in any way, therefore a malicious prover | ||
// can inject any vk they like which could lead to security issues. In practice, there would always | ||
// be some sort of access control to limit the set of possible vks used. | ||
|
||
const multiplyResult = proof.publicOutput; | ||
// Skip verification in case the input is 0, as that is our base-case | ||
proof.verifyIf(vk, multiplyResult.equals(Field(0)).not()); | ||
|
||
const additionResult = multiplyResult.add(field); | ||
return additionResult; | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
const AddProof = ZkProgram.Proof(add); | ||
|
||
const multiply = ZkProgram({ | ||
name: 'multiply', | ||
publicInput: Undefined, | ||
publicOutput: Field, | ||
methods: { | ||
performMultiplication: { | ||
privateInputs: [Field, AddProof], | ||
async method(field: Field, addProof: Proof<Undefined, Field>) { | ||
addProof.verify(); | ||
const multiplicationResult = addProof.publicOutput.mul(field); | ||
return multiplicationResult; | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
console.log('Compiling circuits...'); | ||
const addVk = (await add.compile()).verificationKey; | ||
const multiplyVk = (await multiply.compile()).verificationKey; | ||
|
||
console.log('Proving basecase'); | ||
const dummyProof = await DynamicMultiplyProof.dummy(undefined, Field(0), 1); | ||
const baseCase = await add.performAddition(Field(5), dummyProof, multiplyVk); | ||
|
||
const validBaseCase = await verify(baseCase, addVk); | ||
console.log('ok?', validBaseCase); | ||
|
||
console.log('Proving first multiplication'); | ||
const multiply1 = await multiply.performMultiplication(Field(3), baseCase); | ||
|
||
const validMultiplication = await verify(multiply1, multiplyVk); | ||
console.log('ok?', validMultiplication); | ||
|
||
console.log('Proving second (recursive) addition'); | ||
const add2 = await add.performAddition( | ||
Field(4), | ||
DynamicMultiplyProof.fromProof(multiply1), | ||
multiplyVk | ||
); | ||
|
||
const validAddition = await verify(add2, addVk); | ||
console.log('ok?', validAddition); | ||
|
||
console.log('Result (should be 19):', add2.publicOutput.toBigInt()); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this example makes me a bit uncomfortable, since
add
isn't proving thatDynamicMultiplyProof
came from the multiply program, and that might not be obvious to users seeing this example.I think adding a TODO comment where you highlight this caveat would be enough to prevent this pattern accidentally ending up in a user app and creating a vulnerability