如何创建一个 ZK 智能合约

如何创建 零知识证明并在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 是一种新型的特定领域语言,用于定义算术电路,它可用于生成零知识证明。

作为开发者,我们可以用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);
    });
  })
});

我们在这里的三个测试用例用来测试:

  1. 当给定输入时,约束是有效的。

  2. 见证值是正确的。

  3. 给出正确的值时,输出是正确的。

  4. 当给定一个不正确的值时,电路会失败。

运行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 智能合约验证者!

接下来的步骤

为了继续这个例子,还可以尝试以下:

  1. 将验证者合约部署到测试网,在链外生成证明,并使用Remix进行测试。

  2. 创建一个与验证者关联的合约,在证明找到解决方案时,铸造 NFT。

  3. 创建一个与所有这些部分互动的前端。

  4. 编写能进行更复杂计算的电路。

  5. circom-playground资源库贡献力量,增加新的例子和 PR :)

进一步阅读:

本攻略的源代码可在GitHub上。要了解更多关于智能合约安全和智能合约审计的信息,请访问 Cyfrin.io


感谢 Chaintool 对本翻译的支持, Chaintool 是一个为区块链开发者准备的开源工具箱。

点赞 1
收藏 7
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO