图片来源:Mauro Sbicego on Unsplash
零知识证明使*证明者(Prover)能够向验证者(Verifier)*证明某物的知识而不暴露该知识。例如,如果我们想证明我们已经解决了一个谜题,而又不透露解决方案,我们可以使用零知识证明。
零知识证明与智能合约有什么关系?
想象一个名为 “Sudoku(数独)”的Solidity智能合约,运行在 EVM 区块链上。它有一个公共的二维数组,代表棋盘的初始状态。
译者注:数独是源自18世纪瑞士的一种数学游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次
Sudoku合约 有一个公共函数,接受一个二维数组,根据初始状态和数独规则进行检查,如果解决方案是正确的,则铸造一个NFT。
contract Sudoku { // Initial board state uint8[][] public initialState; function answer(uint[][] memory solution) public { // 检查 `solution` 匹配初始状态 `initialState` ... // 检查 `solution` 匹配 数独 规则 // (1-9 in squares & lines) ... // 如果匹配铸造 NFT ... } }
这就可以工作了,对吧?但是等等,棋盘有多大呢?要知道棋盘越大,函数中的循环迭代次数就越多,函数就越贵。这没法很好地扩容。
另外,当我可以复制第一个成功的答案时,我为什么要花所有的精力去解决它呢?只要有一个人在链上提交了答案,解决方案就会公开,因为每笔交易的调用数据都是公开的。我会通过复制第一个成功的解决方案来铸造一个NFT!
零知识证明使证明者能够向验证者证明对某一事物的了解,而不透露该实际事物。
ZK密码学使我们能够创建一个 Solidity 智能合约来作为一个验证者,而不知道任何关于解决方案的信息。证明者(我们)可以生成一个链外证明将其发布给验证者(智能合约),证明该谜题已被解开,而不泄露解决方案。
了解了零知识证明与智能合约的关系,开始看看真实的案例,编写一个 ZK 电路。
Circom 介绍
Circom 是一种新型的特定领域语言,用于定义算术电路,它可用于生成零知识证明。
作为开发者,我们可以用Circom写一个ZK电路,检查输入是否在链下解决了难题。然后我们可以用这个电路来生成一个证明,证明这个谜题被找到了。我们也可以用这个电路来生成一个Verifier智能合约,检查电路吐出的任何证明。同样,由于零知识密码学的存在,这个证明不包含任何关于解决方案的信息。这意味着在链上发布的信息并没有揭示任何有关它的信息。它唯一揭示的是解决方案是否被解决了。
第一个ZK电路
本攻略的源代码可*在GitHub*。这段代码仅用于演示目的。请不要在生产中使用它。
我们想证明我们知道下面这个二次方程的根(x):
x^2 + x + 5 = 11
高中数学学生知道我们可以用二次方程手动计算出x。我们想向智能合约证明,我们知道它,而不给出值。
第1步 – 设置你的环境
我们在本攻略中使用yarn
和Hardhat 以太坊开发环境。使用设置指南创建一个新的Hardhat项目。
接下来,安装hardhat-circom
包:
yarn add hardhat-circom
最后,删除所有安装时附带的合约、脚本和测试。
第二步 – 编写电路
在代码库的根目录下创建一个名为circuits
的目录。
在circuits
中创建一个名为quadratic.circom
的新文件,并粘贴以下几行代码:
pragma circom 2.0.0; // The circom compiler version // Simple function to square a value function sqr(x) { return x*x; } // The main component for the circuit // This checks that the input (x) does indeed solve the equation // x^2 + x + 5 = 11 // Answer: x = 2 template Quadratic () { // input x (private by default) signal input x; // output, the right side of the equals sign signal output right; // Square x for x^2 var first = sqr(x); // x for x var second = x; // 5 for 5 var third = 5; // Add them all together to get the right side of the equation var sum = first + second + third; // Assign the right side of the equation to the output signal // <== is an assignment + constraint right <== sum; // Constrain the right to be 11 // === is a constraint right === 11; } // Entry point into the circuit component main = Quadratic();
这个电路是链外组件,它私下检查一个解决方案并创建一个证明。大部分的语法是不言自明的,但有一些新的术语:”信号(Signals)”和 “约束(Constraints)”。
信号是输入和输出值,但也可以是电路中的中间值。可以把它们看作是电路中的垫脚石。
约束对系统中的每一个垫脚石强制执行一个条件。===
是一个约束的例子,而<==
是一个约束和赋值的例子。
第三步 – 配置Hardhat配置
Hardhat 还不知道如何编译或运行电路。让我们修改一下hardhat.config.js
文件,使其看起来像这样:
require("@nomicfoundation/hardhat-toolbox"); require("hardhat-circom"); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: { compilers: [ { version: "0.6.11", }, ], }, circom: { inputBasePath: "./circuits", ptau: "https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_15.ptau", circuits: [ { name: "quadratic", }, ], }, };
我们添加的新部分是require("hardhat-circom")
和exports
中的circom
部分。
第四步 – 测试电路
在test
目录下创建一个名为quadratic.test.js
的新文件,并粘贴以下内容:
const hre = require("hardhat"); const { ethers } = require("hardhat"); const { assert, expect } = require("chai"); const snarkjs = require("snarkjs"); describe("quadratic circuit", () => { let circuit; const sampleInput = { x: "2", }; const sanityCheck = true; before(async () => { circuit = await hre.circuitTest.setup("quadratic"); }); describe('circuit tests', () => { it("produces a witness with valid constraints", async () => { const witness = await circuit.calculateWitness(sampleInput, sanityCheck); await circuit.checkConstraints(witness); }); it("has expected witness values", async () => { const witness = await circuit.calculateLabeledWitness( sampleInput, sanityCheck ); assert.propertyVal(witness, "main.x", sampleInput.x); assert.propertyVal(witness, "main.right", "11"); }); it("has the correct output", async () => { const expected = { right: 11 }; const witness = await circuit.calculateWitness(sampleInput, sanityCheck); await circuit.assertOut(witness, expected); }); it("fails if the input is wrong", async () => { await expect(circuit.calculateWitness({x: 3}, sanityCheck)).to.be.rejectedWith(Error); }); }) });
我们在这里的三个测试用例用来测试:
- 当给定输入时,约束是有效的。
- 见证值是正确的。
- 给出正确的值时,输出是正确的。
- 当给定一个不正确的值时,电路会失败。
运行yarn hardhat test
来运行测试。
好吧,你可能在想:”这很好,但现在只是在链外测试电路。我们如何用它来生成一个证明,让智能合约来验证它?”好问题。
第五步 — 生成验证者智能合约
Circom 非常棒,因为它不用手工编写验证者智能合约,而是用我们已经写好的电路为我们生成一个验证者!
要做到这一点,我们需要给它一些有效的输入。在circuits
中创建另一个名为quadratic.json
的文件,并粘贴以下内容:
{ "x": 2 }
然后,在代码库的根目录,运行以下内容:
yarn hardhat circom --deterministic --debug --verbose
这将生成一堆有趣的文件,其中最重要的是,在contracts
中的一个文件,叫做QuadraticVerifier.sol
。
第六步 – 测试验证者智能合约
在我们的quadratic.test.js
文件中,在circuit tests
下,添加一个新的describe
代码块,内容如下:
... describe('verifier tests', () => { let verifier; let jsonCalldata; beforeEach(async () => { // Create and deploy the Verifier contract const VerifierFactory = await ethers.getContractFactory("contracts/QuadraticVerifier.sol:Verifier"); verifier = await VerifierFactory.deploy(); // Generate the proof using snarkjs const {proof, publicSignals} = await snarkjs.groth16.fullProve(sampleInput, "circuits/quadratic.wasm", "circuits/quadratic.zkey"); // Construct the raw calldata to be sent to the verifier contract const rawcalldata = await snarkjs.groth16.exportSolidityCallData(proof, publicSignals); jsonCalldata = JSON.parse("["+rawcalldata+"]") }) it("proves the proof", async () => { assert.isTrue(await verifier.verifyProof(jsonCalldata[0], jsonCalldata[1], jsonCalldata[2], jsonCalldata[3])); }) it("fails to prove if the public signals are wrong", async () => { assert.isFalse(await verifier.verifyProof(jsonCalldata[0], jsonCalldata[1], jsonCalldata[2], [12])) }) }) ...
这些测试检查向提交得验证者智能合约Proof的正确和失败的情况。 通过 yarn hardhat test
来运行测试。
祝贺你!你完成了一个的ZK电路和 ZK 智能合约验证者!
接下来的步骤
为了继续这个例子,还可以尝试以下:
- 将验证者合约部署到测试网,在链外生成证明,并使用Remix进行测试。
- 创建一个与验证者关联的合约,在证明找到解决方案时,铸造 NFT。
- 创建一个与所有这些部分互动的前端。
- 编写能进行更复杂计算的电路。
- 为circom-playground资源库贡献力量,增加新的例子和 PR 🙂
进一步阅读:
评论前必须登录!
注册