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