{"content":{"title":"编写零知识 dApp","body":"> * 原文链接：https://medium.com/@yujiangtham/writing-a-zero-knowledge-dapp-fd7f936e2d43\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/6868) \r\n\r\n\r\n\r\n这篇文章将介绍如何将零知识电路集成到智能合约中，然后再集成到 dApp 中。\r\n\r\n![img](https://img.learnblockchain.cn/pics/20231108154537.png)\r\n欢迎来到零知识的世界\r\n\r\n## 介绍\r\n\r\n零知识（ZK）密码学世界在过去几个月里一直备受关注。随着新的 [zkRollups](https://learnblockchain.cn/tags/zkRollup) 和 [zkEVM](https://learnblockchain.cn/tags/zkEVM) 的宣布，加密世界已经将注意力转向零知识证明的能力，以提供解决隐私和可扩展性问题的优雅解决方案（有关详情，请参阅我[之前的文章](https://medium.com/@yujiangtham/what-are-zero-knowledge-proofs-and-why-do-they-matter-b580e8de71e0)）。\r\n\r\n这文章假设你已经了解 [Solidity](https://decert.me/tutorial/solidity/intro/) 和 Typescript（react/next.js），旨在帮助填补如何编写 ZK 电路（在 [Circom](https://iden3.io/circom) 中）以及如何将其集成到 next.js 应用程序部分。[基本有限域知识](https://medium.com/@yujiangtham/lets-dissect-a-zksnark-part-1-a82fc092f58a)也是有用的。\r\n\r\n本文的主要目标不是解释如何在 [Circom](https://learnblockchain.cn/tags/circom) 中编写电路，而是概述从在 Circom 中编写代码到某人可以在其 dApp 中使用的一系列步骤。\r\n\r\n## 我们将构建什么\r\n\r\n我们将构建一个简单的 [dApp](https://learnblockchain.cn/tags/DApp)，用于检查你提交的两个数字是否介于 0 和 5 之间，并且它们不相等。你将在浏览器中生成一个 ZK 证明，并仅提交证明以进行交易，因此互联网上甚至合约本身都不会知道提交的两个数字，只知道它们符合上述约束。\r\n\r\n在我们开始构建之前，让我们回顾一些内容，以便我们站在坚实的基础上…\r\n\r\n## 什么是 ZK 电路？\r\n\r\nZK 电路是一个程序，给定一些输入，输出一个证明，可以轻松验证电路内运行的每一步计算都是正确的。\r\n\r\n## 如何编写 ZK 电路？\r\n\r\n有许多种方法可以创建 ZK 电路，其中一些是高级领域特定语言，例如 [Noir](https://aztec.network/noir/) 和 [Circom](https://iden3.io/circom)，它们允许你编写一个将编译为电路的程序。或者，还有一些底层包，比如 [Halo2](https://halo2.dev/en-US)，你将在其中指定表中的值将落在哪里，该表代表你的电路。\r\n\r\n今天，我们将使用 Circom 和 [SnarkJS](https://learnblockchain.cn/tags/snarkjs)，因为它们都是相对广泛使用的，目前可以在浏览器中生成证明。\r\n\r\n## 需要知道的知识\r\n\r\n在 Circom 和大多数其他领域特定语言中编写代码意味着你需要熟悉处理域元素和编写约束。\r\n\r\n### 域元素（Field elements）\r\n\r\n在 Circom 中，只有一种数据类型，即域元素。对于本教程的目的，你可以将域元素视为基本上是模大素数的任何数字，该素数基于所使用的椭圆曲线。Circom 中的默认曲线是 BN128。\r\n\r\n### 约束\r\n\r\n电路可以接受来自生成证明实体的任意输入，因此这些输入必须在电路内受到约束，以便输入、输出和中间值落在一些可接受的值集内或与另一个变量保持某种关系。约束的两个示例可能是确保一个输入是另一个输入的平方，或者输入不等于输出。我们将讨论如何在代码示例中编写这些内容。\r\n\r\n### 一般流程\r\n\r\n我们将要涵盖的过程的一般流程，在这个图中可视化了：\r\n\r\n![img](https://img.learnblockchain.cn/pics/20231108154546.png)\r\n\r\n> 创建零知识 dApp 的整体流程。突出显示的框是开发者或用户输入。\r\n\r\n## 让我们开始吧\r\n\r\n我相信你已经厌倦了介绍和背景知识，所以让我们直接开始。我将根据上面的一般流程图将其分解为三个部分，以便更容易跟踪。电路部分讨论了在 Circom 中编写 ZK 电路的所有内容，然后我们将其导出到合约部分，该部分与前端部分进行交互。\r\n\r\n我们将使用的证明系统是 Plonk，它允许我们拥有一个通用的可信设置，而不必为每个电路生成额外的随机性。它比 Groth16 证明系统慢，但对我们现在想要的东西已经足够了。\r\n\r\n这是本文代码库的链接：https://github.com/ytham/zk_example_dapp \r\n\r\n## 设置\r\n\r\n在继续之前，我们需要安装以下软件包：\r\n\r\n- [Node.js](https://nodejs.org/en/download/)\r\n- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install/)\r\n- [Foundry](https://github.com/foundry-rs/foundry)\r\n- [Circom](https://docs.circom.io/getting-started/installation/)\r\n- [SnarkJS](https://www.npmjs.com/package/snarkjs)\r\n- [Metamask](https://metamask.io/)\r\n\r\n让我们首先创建项目文件夹。我们将使用以下设置使用命令创建一个 next.js 应用程序：\r\n\r\n> ```\r\n> yarn create next-app\r\n> ```\r\n\r\n![img](https://img.learnblockchain.cn/pics/20231108154553.png)\r\n\r\n我们新的 next.js 存储库的设置\r\n\r\n完成后，`cd` 进入文件夹，然后运行以下命令以添加以下软件包：\r\n\r\n> ```\r\n> yarn add circomlib snarkjs wagmi ethers@^5 axios @mantine/core @mantine/hooks @mantine/notifications @emotion/react\r\n> ```\r\n\r\n## 电路\r\n\r\n![电路](https://img.learnblockchain.cn/pics/20231108154557.png)\r\n\r\n> 电路部分突出显示\r\n\r\n### 设置\r\n\r\n确保你在项目文件夹中，并创建一个名为 `circuits` 的文件夹，然后在 `circuits` 文件夹内创建一个名为 `build` 的文件夹。我们将在本节中使用 `circuits` 文件夹。\r\n\r\n### 编写电路\r\n\r\n如上所述，我们将编写一个电路，该电路接受两个输入，确保它们都介于 0 和 5 之间，并确保它们彼此不相等。然后它将相乘并输出这两个值。我在下面的 circom 文件中添加了内部注释：\r\n\r\n***注意：*** 顶部写着 `// file: <filename>`的行实际上不是文件的一部分，它只是告诉你文件相对于项目根目录的位置。\r\n\r\n```\r\n// file: /circuits/simple_multiplier.circom\r\n\r\npragma circom 2.1.3;\r\n\r\ninclude \"../node_modules/circomlib/circuits/comparators.circom\";\r\n\r\ntemplate SimpleMultiplier() {\r\n    // Private input signals\r\n    signal input in[2];\r\n\r\n    // Output signal (public)\r\n    signal output out;\r\n\r\n    // Create a constraint here saying that our two input signals cannot\r\n    // equal each other.\r\n    component isz = IsZero();\r\n    isz.in <== in[0] - in[1];\r\n\r\n    // The IsZero component returns 1 if the input is 0, or 0 otherwise.\r\n    isz.out === 0;\r\n\r\n    // Define the greater than and less than components that we'll define \r\n    // inside the for loop below.\r\n    component gte[2];\r\n    component lte[2];\r\n    \r\n    // We loop through the two signals to compare them.\r\n    for (var i = 0; i < 2; i++) {\r\n        // Both the LessEqThan and GreaterEqThan components take number of \r\n        // bits as an input. In this case, we want to ensure our inputs are \r\n        // [0,5], which requires 3 bits (101).\r\n        lte[i] = LessEqThan(3);\r\n\r\n        // We put our circuit's input signal as the input signal to the \r\n        // LessEqThan component and compare it against 5.\r\n        lte[i].in[0] <== in[i];\r\n        lte[i].in[1] <== 5;\r\n\r\n        // The LessEqThan component outputs a 1 if the evaluation is true, \r\n        // 0 otherwise, so we create this equality constraint.\r\n        lte[i].out === 1;\r\n\r\n        // We do the same with GreaterEqThan, and also require 3 bits since\r\n        // the range of inputs is still [0,5].\r\n        gte[i] = GreaterEqThan(3);\r\n\r\n        // Compare our input with 0 \r\n        gte[i].in[0] <== in[i];\r\n        gte[i].in[1] <== 0;\r\n\r\n        // The GreaterEqThan component outputs a 1 if the evaluation is true, \r\n        // 0 otherwise, so we create this equality constraint.\r\n        gte[i].out === 1;\r\n    }\r\n\r\n    // Write a * b into c and then constrain c to be equal to a * b.\r\n    out <== in[0] * in[1];\r\n}\r\n\r\ncomponent main = SimpleMultiplier();\r\n```\r\n\r\n### 编译为中间表示\r\n\r\n完成电路后，我们将将其编译为称为 R1CS（Rank-1 Constraint System）的中间表示。有关 R1CS 的其他信息可以在[先前的文章](https://medium.com/@yujiangtham/lets-dissect-a-zksnark-part-2-5f92f1d7d2e9)中找到。在 `circuits` 文件夹中运行以下命令：\r\n\r\n> ```\r\n> circom simple_multiplier.circom --r1cs --wasm --sym -o build\r\n> ```\r\n\r\n这将在 `circuits/build` 文件夹中输出 R1CS、WASM 和符号，并显示电路数据，包括约束数量。\r\n\r\n### Powers of Tau 可信设置文件\r\n\r\n已生成了许多 Powers of Tau 可信设置，我们可以根据我们的电路大小（约束数量）使用。为了减少证明时间，你将希望使用最接近你电路大小的 Powers of Tau。你可以在此存储库中找到可信设置文件：\r\n\r\n> [*https://github.com/iden3/snarkjs#7-prepare-phase-2*](https://github.com/iden3/snarkjs#7-prepare-phase-2)\r\n\r\n让我们继续使用最小的一个（`powersOfTau28_hez_final_08.ptau`），它支持最多 256 个约束，因为我们的电路有约 14 个约束。\r\n\r\n### 生成证明密钥（proving key）\r\n\r\n现在，从 `circuits` 目录，运行以下命令来生成我们将使用来生成证明的证明密钥，该证明密钥使用了 R1CS 和 ptau 文件：\r\n\r\n> ```\r\n> snarkjs plonk setup build/simple_multiplier.r1cs ptau/powersOfTau28_hez_final_08.ptau build/proving_key.zkey\r\n> ```\r\n\r\n## 合约\r\n\r\n![合约](https://img.learnblockchain.cn/pics/20231108154604.png)\r\n\r\n>  合约部分突出显示\r\n\r\n### 设置\r\n\r\n在项目根目录中创建一个名为 `contracts` 的新目录。`cd contracts` 然后使用以下 Foundry 命令在 `contracts` 文件夹内创建一个新项目：\r\n\r\n> ```\r\n> forge init --no-commit\r\n> ```\r\n\r\n删除 `script`、`src` 和 `test` 文件夹中生成的起始文件。\r\n\r\n在 `contracts` 文件夹中也添加一个 `.env` 文件，其中你将添加要从中部署的钱包的私钥（确保此钱包中有一些 GoerliETH，你可以从[Goerli PoW faucet](https://goerli-faucet.pk910.de/)获取）。你还需要具有 Alchemy 的帐户（或其他选择的 RPC 提供者），并从 Alchemy 仪表板输入你的 RPC url：\r\n\r\n```\r\n// file: /contracts/.env\r\n\r\nGOERLI_RPC_URL=https://eth-goerli.g.alchemy.com/v2/<YOUR_GOERLI_API_KEY>\r\nPRIVATE_KEY=<YOUR_PRIVATE_KEY>\r\n```\r\n\r\n还将以下内容添加到 foundry.toml 文件中：\r\n\r\n```\r\n// file: /contracts/foundry.toml\r\n\r\n[profile.default]\r\nsrc = 'src'\r\nout = 'out'\r\nlibs = ['lib']\r\n\r\n# Add this \r\n[rpc_endpoints]\r\ngoerli = \"${GOERLI_RPC_URL}\"\r\n```\r\n\r\n### 导出智能合约验证者\r\n\r\n我们可以使用以下 SnarkJS 命令从项目根目录生成验证者智能合约：\r\n\r\n> ```\r\n> snarkjs zkey export solidityverifier circuits/build/proving_key.zkey contracts/src/PlonkVerifier.sol\r\n> ```\r\n\r\n### 编写智能合约\r\n\r\n编写以下合约，该合约利用我们上面导出的 `PlonkVerifier.sol` 文件。合约仅基于 PlonkVerifier 的结果输出一个布尔值 true 或 false，但你可以想象编写一些铸造 NFT、转移代币、部署另一个合约或任何你能想到的其他内容。为简洁起见，我省略了测试。\r\n\r\n```\r\n// file: /contracts/src/SimpleMultiplier.sol\r\n\r\n// SPDX-License-Identifier: UNLICENSED\r\npragma solidity ^0.8.13;\r\n\r\n// Interface to PlonkVerifier.sol\r\ninterface IPlonkVerifier {\r\n    function verifyProof(bytes memory proof, uint[] memory pubSignals) external view returns (bool);\r\n}\r\n\r\ncontract SimpleMultiplier {\r\n    address public s_plonkVerifierAddress;\r\n\r\n    event ProofResult(bool result);\r\n\r\n    constructor(address plonkVerifierAddress) {\r\n        s_plonkVerifierAddress = plonkVerifierAddress;\r\n    }\r\n\r\n    // ZK proof is generated in the browser and submitted as a transaction w/ the proof as bytes.\r\n    function submitProof(bytes memory proof, uint256[] memory pubSignals) public returns (bool) {\r\n        bool result = IPlonkVerifier(s_plonkVerifierAddress).verifyProof(proof, pubSignals);\r\n        emit ProofResult(result);\r\n        return result;\r\n    }\r\n}\r\n```\r\n\r\n在 `contracts` 文件夹中运行以下命令构建合约：\r\n\r\n> ```\r\n> forge build\r\n> ```\r\n\r\n然后，返回到项目根目录，然后在 `src/lib` 文件夹中创建一个名为 `abi` 的文件夹，并将 json 输出复制到该文件夹中：\r\n\r\n> ```\r\n> mkdir -p src/lib/abi\r\n> cp contracts/out/SimpleMultiplier.sol/SimpleMultiplier.json src/lib/abi/.\r\n> ```\r\n\r\n### 部署合约\r\n\r\n然后，使用部署脚本部署合约：\r\n\r\n```\r\n// file: /contracts/scripts/SimpleMultiplier.s.sol\r\n\r\n// SPDX-License-Identifier: UNLICENSED\r\npragma solidity ^0.8.13;\r\n\r\nimport \"forge-std/Script.sol\";\r\nimport \"../src/PlonkVerifier.sol\";\r\nimport \"../src/SimpleMultiplier.sol\";\r\n\r\ncontract SimpleMultiplierScript is Script {\r\n    function setUp() public {}\r\n\r\n    function run() public {\r\n        uint256 deployerPrivateKey = vm.envUint(\"PRIVATE_KEY\");\r\n        vm.startBroadcast(deployerPrivateKey);\r\n\r\n        PlonkVerifier pv = new PlonkVerifier();\r\n        SimpleMultiplier sm = new SimpleMultiplier(address(pv));\r\n\r\n        vm.stopBroadcast();\r\n    }\r\n}\r\n```\r\n\r\n并使用以下命令运行部署脚本：\r\n\r\n> ```\r\n> forge script script/SimpleMultiplier.s.sol SimpleMultiplierScript --broadcast --verify --rpc-url goerli\r\n> ```\r\n\r\n你将看到两个合约已部署。第一个是 `PlonkVerifier` 合约，第二个是 `SimpleMultiplier` 合约。我们只需要 `SimpleMultiplier` 合约的地址。让我们保存这个地址以便稍后在前端中使用：\r\n\r\n```\r\n// file: /src/shared/addresses.ts\r\n\r\nexport const Addresses = {\r\n  SIMPLE_MULTIPLIER_ADDR: \"<YOUR_DEPLOYED_CONTRACT_ADDR>\" as `0x${string}`,\r\n}\r\n```\r\n\r\n太棒了！现在我们已经部署了我们的验证者和合约，并准备好继续在浏览器中构建用户界面！\r\n\r\n## 前端\r\n\r\n![前端](https://img.learnblockchain.cn/pics/20231108154611.png)\r\n\r\n> 前端部分突出显示\r\n\r\n### 设置\r\n\r\n我们将通过 [Github](https://github.com/) 部署我们的 dApp 到 [Vercel](https://vercel.com/)，因此请确保在继续之前你在这两个地方都有帐户。\r\n\r\n### 创建前端\r\n\r\n我们将通过创建或修改以下文件来构建前端界面。为简洁起见，我在各个页面中使用了许多项，而不是为它们创建单独的组件。我也没有处理各种每种错误情况。这不是一个 next.js/react 教程，因此我假设读者已经有了基本的了解。\r\n\r\n我们使用 Wagmi 包连接到区块链，并将整个应用程序包装在 `WagmiConfig` 中：\r\n\r\n```typescript\r\n// file: /src/pages/_app.tsx\r\n\r\nimport '@/styles/globals.css'\r\nimport { WagmiConfig, createClient, configureChains, goerli } from 'wagmi'\r\nimport { publicProvider } from 'wagmi/providers/public'\r\nimport type { AppProps } from 'next/app'\r\nimport { MantineProvider } from '@mantine/core'\r\nimport { Notifications } from '@mantine/notifications';\r\n\r\n// We'll just be using Goerli testnet for now\r\nconst { chains, provider, webSocketProvider } = configureChains(\r\n  [goerli],\r\n  [publicProvider()],\r\n)\r\n \r\nconst client = createClient({\r\n  autoConnect: true,\r\n  provider,\r\n  webSocketProvider,\r\n})\r\n\r\nexport default function App({ Component, pageProps }: AppProps) {\r\n  // We'll be using Wagmi sending our transaction and Mantine for CSS \r\n  // and notifications\r\n  return (\r\n    <WagmiConfig client={client}>\r\n      <MantineProvider withNormalizeCSS>\r\n        <Notifications />\r\n        <Component {...pageProps} />\r\n      </MantineProvider>\r\n    </WagmiConfig>\r\n  )\r\n}\r\n```\r\n\r\n我们的 `index.tsx` 文件包含一个 `ConnectWalletButton` 和两个输入字段，用户可以在其中输入他们的数字 [0,5]。当用户按下两个输入字段的提交按钮时，它会将一个 POST 请求发送到后端，其中包含输入以生成证明。生成证明后，它将该证明数据作为交易提交到区块链。\r\n\r\n理想情况下，我们希望将其分成许多不同的组件文件，但为简单起见，它都在一个文件中：\r\n\r\n```typescript\r\n// file: /src/pages/index.tsx\r\n\r\nimport Head from 'next/head'\r\nimport Link from 'next/link';\r\nimport { useState } from 'react';\r\nimport { Stack, Text, Title, Grid, Input, Button, Group, Space } from '@mantine/core'\r\nimport axios, { AxiosRequestConfig } from 'axios';\r\nimport { useAccount } from 'wagmi';\r\nimport { notifications } from \"@mantine/notifications\";\r\nimport { ConnectWalletButton } from '@/components/ConnectWalletButton';\r\nimport { executeTransaction } from '@/lib/executeTransaction';\r\n\r\nexport default function Home() {\r\n  const [input0, setInput0] = useState(\"\");\r\n  const [input1, setInput1] = useState(\"\");\r\n  const { isConnected } = useAccount();\r\n  \r\n  const handleGenerateProofSendTransaction = async (e: any) => {\r\n    e.preventDefault();\r\n    \r\n    // We will send an HTTP request with our inputs to our next.js backend to \r\n    // request a proof to be generated.\r\n    const data = {\r\n      input0,\r\n      input1,\r\n    }\r\n    const config: AxiosRequestConfig = {\r\n      headers: {\r\n        \"Content-Type\": \"application/json\",\r\n      }\r\n    }\r\n\r\n    // Send the HTTP request\r\n    try {\r\n      const res = await axios.post(\"/api/generate_proof\", data, config);\r\n      notifications.show({\r\n        message: \"Proof generated successfully! Submitting transaction...\",\r\n        color: \"green\",\r\n      });\r\n\r\n      // Split out the proof and public signals from the response data\r\n      const { proof, publicSignals } = res.data;\r\n\r\n      // Write the transaction\r\n      const txResult = await executeTransaction(proof, publicSignals);\r\n      const txHash = txResult.transactionHash;\r\n\r\n      notifications.show({\r\n        message: `Transaction succeeded! Tx Hash: ${txHash}`,\r\n        color: \"green\",\r\n        autoClose: false,\r\n      });\r\n    } catch (err: any) {\r\n      const statusCode = err?.response?.status;\r\n      const errorMsg = err?.response?.data?.error;\r\n      notifications.show({\r\n        message: `Error ${statusCode}: ${errorMsg}`,\r\n        color: \"red\",\r\n      });\r\n    }\r\n  }\r\n\r\n  // Only allow submit if the user first connects their wallet\r\n  const renderSubmitButton = () => {\r\n    if (!isConnected) {\r\n      return <ConnectWalletButton />\r\n    }\r\n    return (\r\n      <Button type=\"submit\">Generate Proof & Send Transaction</Button>\r\n    )\r\n  }\r\n\r\n  return (\r\n    <>\r\n      <Head>\r\n        <title>ZK Simple Multiplier</title>\r\n        <link rel=\"icon\" href=\"/favicon.ico\" />\r\n      </Head>\r\n      <Stack justify=\"center\" align=\"center\" w=\"100vw\" h=\"100vh\" spacing={0}>\r\n        <Stack align=\"center\" spacing={0}>\r\n          <Group w=\"96vw\" h=\"10vh\" position=\"apart\" align=\"center\">\r\n            <Title order={3}>\r\n              ZK Simple Multiplier\r\n            </Title>\r\n            <ConnectWalletButton />\r\n          </Group>\r\n          <Grid align=\"center\" justify=\"center\" mih=\"80vh\">\r\n            <Grid.Col sm={8} md={6} lg={4}>\r\n              <Text>\r\n                {\"Input two numbers between 0 and 5, inclusive. The two numbers must \\\r\n                not be equal. We'll generate a ZK proof locally in the browser, and \\\r\n                only the proof will be sent to the blockchain so that no one \\\r\n                watching the blockchain will know the two numbers.\"}\r\n              </Text>\r\n              <Space h={20} />\r\n              <form onSubmit={handleGenerateProofSendTransaction}>\r\n                <Stack spacing=\"sm\">\r\n                  <Input.Wrapper label=\"Input 0\">\r\n                    <Input \r\n                      placeholder=\"Number between 0 and 5\" \r\n                      value={input0} \r\n                      onChange={(e) => setInput0(e.currentTarget.value)}\r\n                    />\r\n                  </Input.Wrapper>\r\n                  <Input.Wrapper label=\"Input 1\">\r\n                  <Input \r\n                      placeholder=\"Number between 0 and 5\" \r\n                      value={input1} \r\n                      onChange={(e) => setInput1(e.currentTarget.value)}\r\n                    />\r\n                  </Input.Wrapper>\r\n                  <Space h={10} />\r\n                  { renderSubmitButton() }\r\n                </Stack>\r\n              </form>\r\n            </Grid.Col>\r\n          </Grid>\r\n          <Group w=\"96vw\" h=\"10vh\" position=\"center\" align=\"center\">\r\n            <Link href=\"https://medium.com/@yujiangtham/writing-a-zero-knowledge-dapp-fd7f936e2d43\">\r\n              <Text>\r\n                Created using this tutorial!\r\n              </Text>\r\n            </Link>\r\n          </Group>\r\n        </Stack>\r\n      </Stack>\r\n    </>\r\n  )\r\n}\r\n```\r\n\r\n对于 ConnectWalletButton，我们使用 Metamask 来连接它，因为它是最受欢迎的，但你也可以使用其他任何你喜欢的钱包，并使用适当的[Wagmi 连接器](https://wagmi.sh/react/connectors/injected)。\r\n\r\n```typescript\r\n// file: /src/components/ConnectWalletButton.tsx\r\n\r\nimport { Button } from \"@mantine/core\"\r\nimport { disconnect } from \"@wagmi/core\";\r\nimport { useAccount, useConnect, useEnsName } from 'wagmi'\r\nimport { InjectedConnector } from 'wagmi/connectors/injected'\r\n\r\nexport const ConnectWalletButton = () => {\r\n  const { address, isConnected } = useAccount();\r\n  const { data: ensName } = useEnsName({ address });\r\n  const { connect } = useConnect({\r\n    connector: new InjectedConnector(),\r\n  });\r\n\r\n  const handleClick = () => {\r\n    if (isConnected) {\r\n      disconnect();\r\n    } else {\r\n      connect();\r\n    }\r\n  }\r\n\r\n  const renderConnectText = () => {\r\n    if (isConnected) {\r\n      const start = address?.slice(0,6);\r\n      const end = address?.slice(address.length-4, address.length);\r\n      return `${start}...${end}`;\r\n    } else {\r\n      return \"Connect Wallet\";\r\n    }\r\n  }\r\n  \r\n  return (\r\n    <Button onClick={handleClick}>\r\n      { renderConnectText() }\r\n    </Button>\r\n  )\r\n}\r\n```\r\n\r\n一旦我们从后端收到输入，我们解析输入，然后调用我们的 `generateProof` 库函数（我们将在下一节中实现）：\r\n\r\n```typescript\r\n// file: /src/pages/api/generate_proof.ts\r\n\r\nimport { generateProof } from '@/lib/generateProof';\r\nimport type { NextApiRequest, NextApiResponse } from 'next'\r\n\r\nexport default async function handler(\r\n  req: NextApiRequest,\r\n  res: NextApiResponse,\r\n) {\r\n  const body = req?.body;\r\n  if (body === undefined) {\r\n    return res.status(403).json({error: \"Request has no body\"});\r\n  }\r\n  console.log(body);\r\n\r\n  const input0 = parseInt(body.input0);\r\n  const input1 = parseInt(body.input1);\r\n\r\n  if (input0 === undefined || Number.isNaN(input0) \r\n    || input1 === undefined || Number.isNaN(input1)) {\r\n    return res.status(403).json({error: \"Invalid inputs\"});\r\n  }\r\n  const proof = await generateProof(input0, input1);\r\n\r\n  if (proof.proof === \"\") {\r\n    return res.status(403).json({error: \"Proving failed\"});\r\n  }\r\n\r\n  res.setHeader(\"Content-Type\", \"text/json\");\r\n  res.status(200).json(proof);\r\n}\r\n```\r\n\r\n### 计算见证并生成证明\r\n\r\n见证计算和证明生成在以下文件中使用 `snarkjs.plonk.fullProve` 一步完成。然后将数据转换为 Solidity calldata blob，进一步拆分为 `proof` 和 `publicSignals`。\r\n\r\n```typescript\r\n// file: /src/lib/generateProof.ts\r\n\r\nimport path from \"path\";\r\n// @ts-ignore\r\nimport * as snarkjs from 'snarkjs';\r\n\r\nexport const generateProof = async (input0: number, input1: number): Promise<any> => {\r\n  console.log(`Generating vote proof with inputs: ${input0}, ${input1}`);\r\n  \r\n  // We need to have the naming scheme and shape of the inputs match the .circom file\r\n  const inputs = {\r\n    in: [input0, input1],\r\n  }\r\n\r\n  // Paths to the .wasm file and proving key\r\n  const wasmPath = path.join(process.cwd(), 'circuits/build/simple_multiplier_js/simple_multiplier.wasm');\r\n  const provingKeyPath = path.join(process.cwd(), 'circuits/build/proving_key.zkey')\r\n\r\n  try {\r\n    // Generate a proof of the circuit and create a structure for the output signals\r\n    const { proof, publicSignals } = await snarkjs.plonk.fullProve(inputs, wasmPath, provingKeyPath);\r\n\r\n    // Convert the data into Solidity calldata that can be sent as a transaction\r\n    const calldataBlob = await snarkjs.plonk.exportSolidityCallData(proof, publicSignals);\r\n    const calldata = calldataBlob.split(',');\r\n\r\n    console.log(calldata);\r\n\r\n    return {\r\n      proof: calldata[0], \r\n      publicSignals: JSON.parse(calldata[1]),\r\n    }\r\n  } catch (err) {\r\n    console.log(`Error:`, err)\r\n    return {\r\n      proof: \"\", \r\n      publicSignals: [],\r\n    }\r\n  }\r\n}\r\n```\r\n\r\n### 提交交易\r\n\r\n交易在此文件中提交：\r\n\r\n```typescript\r\n// file: /src/lib/executeTransaction.ts\r\n\r\nimport { Addresses } from '@/shared/addresses';\r\nimport { TransactionReceipt } from '@ethersproject/abstract-provider';\r\nimport { prepareWriteContract, writeContract } from '@wagmi/core';\r\n\r\nexport const executeTransaction = async (proof: any, publicSignals: Array<string>): Promise<TransactionReceipt> => {\r\n  const abiPath = require('./abi/SimpleMultiplier.json');\r\n\r\n  // Prepare the transaction data\r\n  const config = await prepareWriteContract({\r\n    address: Addresses.SIMPLE_MULTIPLIER_ADDR,\r\n    abi: abiPath.abi,\r\n    functionName: 'submitProof',\r\n    args: [proof, publicSignals]\r\n  });\r\n\r\n  // Execute the transaction\r\n  const writeResult = await writeContract(config);\r\n\r\n  // Wait for the transaction block to be mined\r\n  const txResult = await writeResult.wait();\r\n  return txResult;\r\n}\r\n```\r\n\r\n### 使用结果更新 UI\r\n\r\n交易结果在 `await txResult.wait()` 的输出中给出。在这里，我们只是向用户发送了一个通知，但你可以以最适合你的方式更新 UI 信息。\r\n\r\n### 与你的应用程序交互\r\n\r\n你可以通过在项目根目录中运行 `yarn dev` 来运行本地服务器进行尝试。\r\n\r\n此外，你还可以部署到 [Vercel](https://vercel.com/)，使你的 dApp 可供任何人在网络上使用。首先，创建一个新的 Github 存储库，并提交所有更改并将文件推送到该存储库。转到 Vercel，然后添加一个新项目：\r\n\r\n![img](https://img.learnblockchain.cn/pics/20231108154621.png)\r\n\r\n> 在 Vercel 中添加一个新项目\r\n\r\n选择要导入的 git 存储库：\r\n\r\n![img](https://img.learnblockchain.cn/pics/20231108154625.png)\r\n\r\n> 要导入的 git 存储库\r\n\r\n确保框架预设为 Next.js，然后点击部署。\r\n\r\n![img](https://img.learnblockchain.cn/pics/20231108154630.png)\r\n\r\n等待几分钟以便项目构建，然后你应该会得到一个链接，你可以使用它或将其发送给你的朋友。我的部署在这里：\r\n\r\n> [*https://zk-example-dapp.vercel.app/*](https://zk-example-dapp.vercel.app/)\r\n\r\n来试试吧！转到你在Vercel上部署的页面并试试吧！\r\n\r\n## 总结\r\n\r\n### 电路\r\n\r\n1. 编写circom电路\r\n2. 编译电路：\r\n   `circom circuit.circom --r1cs --wasm --sym`\r\n3. 下载powers of tau可信设置文件\r\n4. 运行Plonk设置以获取验证密钥：\r\n   `snarkjs plonk setup circuit.r1cs ptau_file.ptau proving_key.zkey`\r\n\r\n### 合约\r\n\r\n1. 导出验证器智能合约\r\n   `snarkjs zkey export solidityverifier proving_key.zkey verifier.sol`\r\n2. 将验证器集成到你的Solidity项目中\r\n\r\n### 前端\r\n\r\n1. 获取用户输入\r\n2. 在一步中计算见证并生成证明\r\n   `await snarkjs.plonk.fullProve({ inputs }, wasmPath, provingKeyPath);`\r\n3. 使用证明提交交易给验证器合约\r\n\r\n\r\n\r\n希望本文对你的zk dApp开发之旅有所帮助。欢迎你在[Twitter](https://twitter.com/DarkForestHam)上关注我！干杯！\r\n\r\n\r\n---\r\n\r\n本翻译由 [DeCert.me](https://decert.me/) 协助支持， 在 DeCert 构建可信履历，为自己码一个未来。"},"author":{"user":"https://learnblockchain.cn/people/412","address":null},"history":"bafkreih73gktsfaqmb33cmtcy7fdwiz7ucsmxfsakqccfr3lzxouvfoagq","timestamp":1699437704,"version":1}