# 编写零知识 dApp

## 什么是 ZK 电路？

ZK 电路是一个程序，给定一些输入，输出一个证明，可以轻松验证电路内运行的每一步计算都是正确的。

## 设置

``yarn create next-app``

``yarn add circomlib snarkjs wagmi ethers@^5 axios @mantine/core @mantine/hooks @mantine/notifications @emotion/react``

## 电路

### 编写电路

``````// file: /circuits/simple_multiplier.circom

pragma circom 2.1.3;

include "../node_modules/circomlib/circuits/comparators.circom";

template SimpleMultiplier() {
// Private input signals
signal input in[2];

// Output signal (public)
signal output out;

// Create a constraint here saying that our two input signals cannot
// equal each other.
component isz = IsZero();
isz.in &lt;== in[0] - in[1];

// The IsZero component returns 1 if the input is 0, or 0 otherwise.
isz.out === 0;

// Define the greater than and less than components that we'll define
// inside the for loop below.
component gte[2];
component lte[2];

// We loop through the two signals to compare them.
for (var i = 0; i &lt; 2; i++) {
// Both the LessEqThan and GreaterEqThan components take number of
// bits as an input. In this case, we want to ensure our inputs are
// [0,5], which requires 3 bits (101).
lte[i] = LessEqThan(3);

// We put our circuit's input signal as the input signal to the
// LessEqThan component and compare it against 5.
lte[i].in[0] &lt;== in[i];
lte[i].in[1] &lt;== 5;

// The LessEqThan component outputs a 1 if the evaluation is true,
// 0 otherwise, so we create this equality constraint.
lte[i].out === 1;

// We do the same with GreaterEqThan, and also require 3 bits since
// the range of inputs is still [0,5].
gte[i] = GreaterEqThan(3);

// Compare our input with 0
gte[i].in[0] &lt;== in[i];
gte[i].in[1] &lt;== 0;

// The GreaterEqThan component outputs a 1 if the evaluation is true,
// 0 otherwise, so we create this equality constraint.
gte[i].out === 1;
}

// Write a * b into c and then constrain c to be equal to a * b.
out &lt;== in[0] * in[1];
}

component main = SimpleMultiplier();``````

### 编译为中间表示

``circom simple_multiplier.circom --r1cs --wasm --sym -o build``

### Powers of Tau 可信设置文件

https://github.com/iden3/snarkjs#7-prepare-phase-2

### 生成证明密钥（proving key）

``snarkjs plonk setup build/simple_multiplier.r1cs ptau/powersOfTau28_hez_final_08.ptau build/proving_key.zkey``

## 合约

### 设置

``forge init --no-commit``

`contracts` 文件夹中也添加一个 `.env` 文件，其中你将添加要从中部署的钱包的私钥（确保此钱包中有一些 GoerliETH，你可以从Goerli PoW faucet获取）。你还需要具有 Alchemy 的帐户（或其他选择的 RPC 提供者），并从 Alchemy 仪表板输入你的 RPC url：

``````// file: /contracts/.env

GOERLI_RPC_URL=https://eth-goerli.g.alchemy.com/v2/&lt;YOUR_GOERLI_API_KEY>
PRIVATE_KEY=&lt;YOUR_PRIVATE_KEY>``````

``````// file: /contracts/foundry.toml

[profile.default]
src = 'src'
out = 'out'
libs = ['lib']

[rpc_endpoints]
goerli = "\${GOERLI_RPC_URL}"``````

### 导出智能合约验证者

``snarkjs zkey export solidityverifier circuits/build/proving_key.zkey contracts/src/PlonkVerifier.sol``

### 编写智能合约

``````// file: /contracts/src/SimpleMultiplier.sol

pragma solidity ^0.8.13;

// Interface to PlonkVerifier.sol
interface IPlonkVerifier {
function verifyProof(bytes memory proof, uint[] memory pubSignals) external view returns (bool);
}

contract SimpleMultiplier {

event ProofResult(bool result);

}

// ZK proof is generated in the browser and submitted as a transaction w/ the proof as bytes.
function submitProof(bytes memory proof, uint256[] memory pubSignals) public returns (bool) {
emit ProofResult(result);
return result;
}
}``````

`contracts` 文件夹中运行以下命令构建合约：

``forge build``

``````mkdir -p src/lib/abi
cp contracts/out/SimpleMultiplier.sol/SimpleMultiplier.json src/lib/abi/.``````

### 部署合约

``````// file: /contracts/scripts/SimpleMultiplier.s.sol

pragma solidity ^0.8.13;

import "forge-std/Script.sol";
import "../src/PlonkVerifier.sol";
import "../src/SimpleMultiplier.sol";

contract SimpleMultiplierScript is Script {
function setUp() public {}

function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");

PlonkVerifier pv = new PlonkVerifier();

}
}``````

``forge script script/SimpleMultiplier.s.sol SimpleMultiplierScript --broadcast --verify --rpc-url goerli``

``````// file: /src/shared/addresses.ts

}``````

## 前端

### 创建前端

``````// file: /src/pages/_app.tsx

import '@/styles/globals.css'
import { WagmiConfig, createClient, configureChains, goerli } from 'wagmi'
import { publicProvider } from 'wagmi/providers/public'
import type { AppProps } from 'next/app'
import { MantineProvider } from '@mantine/core'

// We'll just be using Goerli testnet for now
const { chains, provider, webSocketProvider } = configureChains(
[goerli],
[publicProvider()],
)

const client = createClient({
autoConnect: true,
provider,
webSocketProvider,
})

export default function App({ Component, pageProps }: AppProps) {
// We'll be using Wagmi sending our transaction and Mantine for CSS
return (
&lt;WagmiConfig client={client}>
&lt;MantineProvider withNormalizeCSS>
&lt;Component {...pageProps} />
&lt;/MantineProvider>
&lt;/WagmiConfig>
)
}``````

``````// file: /src/pages/index.tsx

import { useState } from 'react';
import { Stack, Text, Title, Grid, Input, Button, Group, Space } from '@mantine/core'
import axios, { AxiosRequestConfig } from 'axios';
import { useAccount } from 'wagmi';
import { ConnectWalletButton } from '@/components/ConnectWalletButton';
import { executeTransaction } from '@/lib/executeTransaction';

export default function Home() {
const [input0, setInput0] = useState("");
const [input1, setInput1] = useState("");
const { isConnected } = useAccount();

const handleGenerateProofSendTransaction = async (e: any) => {
e.preventDefault();

// We will send an HTTP request with our inputs to our next.js backend to
// request a proof to be generated.
const data = {
input0,
input1,
}
const config: AxiosRequestConfig = {
"Content-Type": "application/json",
}
}

// Send the HTTP request
try {
const res = await axios.post("/api/generate_proof", data, config);
message: "Proof generated successfully! Submitting transaction...",
color: "green",
});

// Split out the proof and public signals from the response data
const { proof, publicSignals } = res.data;

// Write the transaction
const txResult = await executeTransaction(proof, publicSignals);
const txHash = txResult.transactionHash;

message: `Transaction succeeded! Tx Hash: \${txHash}`,
color: "green",
autoClose: false,
});
} catch (err: any) {
const statusCode = err?.response?.status;
const errorMsg = err?.response?.data?.error;
message: `Error \${statusCode}: \${errorMsg}`,
color: "red",
});
}
}

// Only allow submit if the user first connects their wallet
const renderSubmitButton = () => {
if (!isConnected) {
return &lt;ConnectWalletButton />
}
return (
&lt;Button type="submit">Generate Proof & Send Transaction&lt;/Button>
)
}

return (
&lt;>
&lt;title>ZK Simple Multiplier&lt;/title>
&lt;Stack justify="center" align="center" w="100vw" h="100vh" spacing={0}>
&lt;Stack align="center" spacing={0}>
&lt;Group w="96vw" h="10vh" position="apart" align="center">
&lt;Title order={3}>
ZK Simple Multiplier
&lt;/Title>
&lt;ConnectWalletButton />
&lt;/Group>
&lt;Grid align="center" justify="center" mih="80vh">
&lt;Grid.Col sm={8} md={6} lg={4}>
&lt;Text>
{"Input two numbers between 0 and 5, inclusive. The two numbers must \
not be equal. We'll generate a ZK proof locally in the browser, and \
only the proof will be sent to the blockchain so that no one \
watching the blockchain will know the two numbers."}
&lt;/Text>
&lt;Space h={20} />
&lt;form onSubmit={handleGenerateProofSendTransaction}>
&lt;Stack spacing="sm">
&lt;Input.Wrapper label="Input 0">
&lt;Input
placeholder="Number between 0 and 5"
value={input0}
onChange={(e) => setInput0(e.currentTarget.value)}
/>
&lt;/Input.Wrapper>
&lt;Input.Wrapper label="Input 1">
&lt;Input
placeholder="Number between 0 and 5"
value={input1}
onChange={(e) => setInput1(e.currentTarget.value)}
/>
&lt;/Input.Wrapper>
&lt;Space h={10} />
{ renderSubmitButton() }
&lt;/Stack>
&lt;/form>
&lt;/Grid.Col>
&lt;/Grid>
&lt;Group w="96vw" h="10vh" position="center" align="center">
&lt;Text>
Created using this tutorial!
&lt;/Text>
&lt;/Group>
&lt;/Stack>
&lt;/Stack>
&lt;/>
)
}``````

``````// file: /src/components/ConnectWalletButton.tsx

import { Button } from "@mantine/core"
import { disconnect } from "@wagmi/core";
import { useAccount, useConnect, useEnsName } from 'wagmi'
import { InjectedConnector } from 'wagmi/connectors/injected'

export const ConnectWalletButton = () => {
const { address, isConnected } = useAccount();
const { data: ensName } = useEnsName({ address });
const { connect } = useConnect({
connector: new InjectedConnector(),
});

const handleClick = () => {
if (isConnected) {
disconnect();
} else {
connect();
}
}

const renderConnectText = () => {
if (isConnected) {
return `\${start}...\${end}`;
} else {
return "Connect Wallet";
}
}

return (
&lt;Button onClick={handleClick}>
{ renderConnectText() }
&lt;/Button>
)
}``````

``````// file: /src/pages/api/generate_proof.ts

import { generateProof } from '@/lib/generateProof';
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const body = req?.body;
if (body === undefined) {
return res.status(403).json({error: "Request has no body"});
}
console.log(body);

const input0 = parseInt(body.input0);
const input1 = parseInt(body.input1);

if (input0 === undefined || Number.isNaN(input0)
|| input1 === undefined || Number.isNaN(input1)) {
return res.status(403).json({error: "Invalid inputs"});
}
const proof = await generateProof(input0, input1);

if (proof.proof === "") {
return res.status(403).json({error: "Proving failed"});
}

res.status(200).json(proof);
}``````

### 计算见证并生成证明

``````// file: /src/lib/generateProof.ts

import path from "path";
// @ts-ignore
import * as snarkjs from 'snarkjs';

export const generateProof = async (input0: number, input1: number): Promise&lt;any> => {
console.log(`Generating vote proof with inputs: \${input0}, \${input1}`);

// We need to have the naming scheme and shape of the inputs match the .circom file
const inputs = {
in: [input0, input1],
}

// Paths to the .wasm file and proving key
const wasmPath = path.join(process.cwd(), 'circuits/build/simple_multiplier_js/simple_multiplier.wasm');
const provingKeyPath = path.join(process.cwd(), 'circuits/build/proving_key.zkey')

try {
// Generate a proof of the circuit and create a structure for the output signals
const { proof, publicSignals } = await snarkjs.plonk.fullProve(inputs, wasmPath, provingKeyPath);

// Convert the data into Solidity calldata that can be sent as a transaction
const calldataBlob = await snarkjs.plonk.exportSolidityCallData(proof, publicSignals);
const calldata = calldataBlob.split(',');

console.log(calldata);

return {
proof: calldata[0],
publicSignals: JSON.parse(calldata[1]),
}
} catch (err) {
console.log(`Error:`, err)
return {
proof: "",
publicSignals: [],
}
}
}``````

### 提交交易

``````// file: /src/lib/executeTransaction.ts

import { TransactionReceipt } from '@ethersproject/abstract-provider';
import { prepareWriteContract, writeContract } from '@wagmi/core';

export const executeTransaction = async (proof: any, publicSignals: Array&lt;string>): Promise&lt;TransactionReceipt> => {
const abiPath = require('./abi/SimpleMultiplier.json');

// Prepare the transaction data
const config = await prepareWriteContract({
abi: abiPath.abi,
functionName: 'submitProof',
args: [proof, publicSignals]
});

// Execute the transaction
const writeResult = await writeContract(config);

// Wait for the transaction block to be mined
const txResult = await writeResult.wait();
return txResult;
}``````

### 与你的应用程序交互

https://zk-example-dapp.vercel.app/

## 总结

### 电路

1. 编写circom电路
2. 编译电路： `circom circuit.circom --r1cs --wasm --sym`
3. 下载powers of tau可信设置文件
4. 运行Plonk设置以获取验证密钥： `snarkjs plonk setup circuit.r1cs ptau_file.ptau proving_key.zkey`

### 合约

1. 导出验证器智能合约 `snarkjs zkey export solidityverifier proving_key.zkey verifier.sol`
2. 将验证器集成到你的Solidity项目中

### 前端

1. 获取用户输入
2. 在一步中计算见证并生成证明 `await snarkjs.plonk.fullProve({ inputs }, wasmPath, provingKeyPath);`
3. 使用证明提交交易给验证器合约

0x9e64...7c84