Skip to content
Open
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,14 @@ await wtnsCalculate(input, wasmFile, wtns, {memorySize: 0});



### G2 point ordering in the Solidity verifier

The exported Groth16 verifier expects the proof's `B` component in EIP-197
order, which is the reverse of `proof.json`'s. `zkey export soliditycalldata`
performs that swap for you, so its output can be passed to `verifyProof`
directly. If you build the call by hand, see the note in the generated
verifier above its `// B` block.

## Further resources
- [Announcing the Perpetual Powers of Tau Ceremony to benefit all zk-SNARK projects](https://medium.com/coinmonks/announcing-the-perpetual-powers-of-tau-ceremony-to-benefit-all-zk-snark-projects-c3da86af8377)
- [Scalable Multi-party Computation for zk-SNARK Parameters in
Expand Down
17 changes: 15 additions & 2 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,26 @@ const commands = [
},
{
cmd: "zkey export solidityverifier [circuit_final.zkey] [verifier.sol]",
description: "Creates a verifier in solidity",
description: "Creates a verifier in solidity. Expects _pB in EIP-197 order; see soliditycalldata",
longDescription: "Creates a verifier in solidity.\n\n" +
"The generated contract's verifyProof expects the proof's B component\n" +
"in EIP-197 order ([x_im, x_re, y_im, y_re]) -- the reverse of\n" +
"proof.json's; see the note above the contract's `// B` block. Use\n" +
"'zkey export soliditycalldata' to produce arguments in that order.",
alias: ["zkesv", "generateverifier -vk|verificationkey -v|verifier"],
action: zkeyExportSolidityVerifier
},
{
cmd: "zkey export soliditycalldata [public.json] [proof.json]",
description: "Generates call parameters ready to be called.",
description: "Generates call parameters ready to be called (proof's B already in EIP-197 order)",
longDescription: "Generates call parameters ready to be called.\n\n" +
"The proof's B component is emitted in EIP-197 order\n" +
"([x_im, x_re, y_im, y_re]), which is the reverse of proof.json's and\n" +
"is what the exported verifier expects -- so this output can be\n" +
"passed to verifyProof directly. Building the call by hand from\n" +
"proof.json without swapping each G2 coordinate pair yields an\n" +
"off-curve point, and verifyProof then returns false for a proof\n" +
"that verifies fine off-chain.",
alias: ["zkesc", "generatecall -pub|public -p|proof"],
action: zkeyExportSolidityCalldata
},
Expand Down
39 changes: 39 additions & 0 deletions smart_contract_tests/test/smart_contracts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ describe("Smart contracts test suite", function () {
)).to.be.equal(true);
});

it("Groth16 exportSolidityCallData output verifies on-chain", async () => {
expect(await groth16VerifyViaExportedCallData(
path.join("../test", "groth16", "circuit.r1cs"),
path.join("../test", "groth16", "witness.wtns")
)).to.be.equal(true);
});

it("Groth16 smart contract 1 aliased input", async () => {
expect(
await groth16VerifyAliased(
Expand Down Expand Up @@ -92,6 +99,10 @@ describe("Smart contracts test suite", function () {
const { proof: proof, publicSignals: publicInputs } = await snarkjs.groth16.prove(zkeyFilename, wtnsFilename);

const proofA = [proof.pi_a[0], proof.pi_a[1]];
// G2 coordinates are swapped to EIP-197 order here; the verifier
// expects _pB im-first. exportSolidityCallData does the same swap --
// see the groth16 exportSolidityCallData test below, which pins that
// exporter and verifier agree.
const proofB = [[proof.pi_b[0][1], proof.pi_b[0][0]], [proof.pi_b[1][1], proof.pi_b[1][0]]];
const proofC = [proof.pi_c[0], proof.pi_c[1]];

Expand All @@ -109,6 +120,34 @@ describe("Smart contracts test suite", function () {
return await verifierContract.verifyProof(proofA, proofB, proofC, publicInputs);
}

// Pins the contract between `zkey export soliditycalldata` and the exported
// verifier. Nothing else covers it: every other test builds the calldata by
// hand, so both could drift from each other and stay green. The G2 ordering
// convention -- verification-key constants im-first inside the contract, the
// proof's B swapped by the caller -- is only correct if these two agree, and
// that agreement is an ABI: verifiers are deployed immutably, so changing
// either side desynchronises every deployment that already exists.
async function groth16VerifyViaExportedCallData(r1csFilename, wtnsFilename) {
const solidityVerifierFilename = path.join("contracts", "groth16.sol");
const zkeyFilename = { type: "mem" };

await snarkjs.zKey.newZKey(r1csFilename, ptauFilename, zkeyFilename);
const { proof, publicSignals } = await snarkjs.groth16.prove(zkeyFilename, wtnsFilename);

const verifierCode = await snarkjs.zKey.exportSolidityVerifier(zkeyFilename, templates);
fs.writeFileSync(solidityVerifierFilename, verifierCode, "utf-8");
await run("compile");

const VerifierFactory = await ethers.getContractFactory("Groth16Verifier");
verifierContract = await VerifierFactory.deploy();

// Take the arguments exactly as the CLI would emit them.
const callData = await snarkjs.groth16.exportSolidityCallData(proof, publicSignals);
const [a, b, c, inputs] = JSON.parse(`[${callData}]`);

return await verifierContract.verifyProof(a, b, c, inputs);
}

async function groth16VerifyAliased(r1csFilename, wtnsFilename) {
const solidityVerifierFilename = path.join("contracts", "groth16.sol");

Expand Down
18 changes: 18 additions & 0 deletions templates/verifier_groth16.sol.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,24 @@ contract Groth16Verifier {
mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q))

// B
//
// NOTE ON G2 ORDERING. `_pB` is expected to ALREADY be in
// EIP-197 order, i.e. [x_im, x_re, y_im, y_re], and is passed
// through unchanged here. The verification-key constants
// above (betax1/betax2/...) are likewise emitted im-first.
//
// The swap therefore happens on the CALLER side, not in this
// contract. `snarkjs zkey export soliditycalldata` performs
// it for you (see src/groth16_exportsoliditycalldata.js). If
// you build calldata by hand from a proof.json, you must swap
// each G2 coordinate pair yourself:
//
// B = [[pi_b[0][1], pi_b[0][0]],
// [pi_b[1][1], pi_b[1][0]]]
//
// Passing proof.json's natural order straight through yields
// an off-curve point; the pairing precompile then fails and
// verifyProof returns false for an otherwise valid proof.
mstore(add(_pPairing, 64), calldataload(pB))
mstore(add(_pPairing, 96), calldataload(add(pB, 32)))
mstore(add(_pPairing, 128), calldataload(add(pB, 64)))
Expand Down