如何创建 零知识证明并在Solidity 合约中验证
图片来源: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写一个ZK电路,检查输入是否在链下解决了难题。然后我们可以用这个电路来生成一个证明,证明这个谜题被找到了。我们也可以用这个电路来生成一个Verifier智能合约,检查电路吐出的任何证明。同样,由于零知识密码学的存在,这个证明不包含任何关于解决方案的信息。这意味着在链上发布的信息并没有揭示任何有关它的信息。它唯一揭示的是解决方案是否被解决了。
本攻略的源代码可在GitHub。这段代码仅用于演示目的。请不要在生产中使用它。
我们想证明我们知道下面这个二次方程的根(x):
x^2 + x + 5 = 11
高中数学学生知道我们可以用二次方程手动计算出x。我们想向智能合约证明,我们知道它,而不给出值。
我们在本攻略中使用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.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 :)
进一步阅读:
本攻略的源代码可在GitHub上。要了解更多关于智能合约安全和智能合约审计的信息,请访问 Cyfrin.io。
感谢 Chaintool 对本翻译的支持, Chaintool 是一个为区块链开发者准备的开源工具箱。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!