diff --git a/README.md b/README.md index fd7ae016..486bd2ff 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cli.js b/cli.js index 4482f3e4..c3c801a6 100755 --- a/cli.js +++ b/cli.js @@ -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 }, diff --git a/smart_contract_tests/test/smart_contracts.test.js b/smart_contract_tests/test/smart_contracts.test.js index d24f8bcf..5f6467ae 100644 --- a/smart_contract_tests/test/smart_contracts.test.js +++ b/smart_contract_tests/test/smart_contracts.test.js @@ -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( @@ -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]]; @@ -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"); diff --git a/templates/verifier_groth16.sol.ejs b/templates/verifier_groth16.sol.ejs index fda549e3..43d1bb14 100644 --- a/templates/verifier_groth16.sol.ejs +++ b/templates/verifier_groth16.sol.ejs @@ -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)))