本文详细介绍了如何使用Noir和Next.js构建一个基础的零知识(zk)DApp,展示了如何验证两个数字的和,而不透露实际数字。文章提供了清晰的步骤指导,包括代码片段、依赖安装、合约部署和前端验证,适合希望学习zk-DApp开发的读者。
我们将逐步演示一个基本的 zk-dapp,旨在验证加法。该应用程序使用户能够证明两个数字 X 和 Y 的和等于 Z,而无需在区块链上披露实际数字。虽然解决这个问题不一定需要零知识证明,但我们将在这个示例中利用它们,以保持简单并增强理解。
让我们 dive 进入代码,或者跳过运行 dapp。
我们将开始克隆 noir-zk-fullstack-example 仓库到本地:
git clone https://github.com/RareSkills/noir-zk-fullstack-example.git
注意:为了有效理解代码,必须具备 noir 和 typescript 的基本知识。
我们在 package.json
文件中已经列出了特定的版本。要安装,请运行:
npm install
请勿使用 yarn,因为它无法检索所需的特定 NPM 版本。
为了本地构建项目并部署合约,有必要在 http://localhost:8545 启动本地开发 EVM。为此,我们首先将文件 .env.example 的名称更改为 .env,然后打开一个新终端执行以下命令:
npx hardhat node
你可以选择不同的网络进行运行。为此,你需要做以下调整:首先,修改 .env 文件的内容,添加部署者的私钥和 Alchemy 的 API 密钥。之后,导航至 hardhat.config.ts
文件并包括新的网络配置。
完成后,你可以使用 NETWORK 环境变量来指定所需的网络进行部署。例如 NETWORK=mumbai npm run build
或 NETWORK=sepoia npm run build
。对于这个 dapp,我们将使用以下命令本地部署:
NETWORK=localhost npm run build
执行上述命令会依次触发四个额外命令的执行:
这些命令执行后会发生什么?
import { NoirServer } from ‘../utils/noir/noirServer’;
async function main() {
const noir = new NoirServer();
await noir.compile();
noir.getSmartContract();
process.exit();
}
// 我们推荐这种模式,以便在任何地方使用 async/await,并妥善处理错误。
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
执行 genContract.ts
脚本会调用 NoirServer 类的 compile() 方法,它编译位于 ./circuits/src
目录中的 noir 电路,并同时生成 ACIR(Abstract Circuit Intermediate Representation)。同时,通过调用 barretenberg 的 setup_generic_prover_and_verifier 函数,它会初始化 this.prover 和 this.verifier。
async compile() {
// 我在服务器上运行,因此可以使用文件系统
initialiseResolver((id: any) => {
try {
const code = fs.readFileSync(`circuits/src/${id}`, { encoding: ‘utf8’ }) as string;
return code;
} catch (err) {
console.error(err);
throw err;
}
});
const compiled_noir = compile({
entry_point: 'main.nr',
});
this.compiled = compiled_noir;
this.acir = acir_read_bytes(this.compiled.circuit);
[this.prover, this.verifier] = await setup_generic_prover_and_verifier(this.acir);
};
此外,还会调用 getSmartContract 方法,该方法在 ./contract/plonk_vk.sol
生成一个 Solidity 合约。此合约是在执行过程中创建的。
getSmartContract() {
const sc = this.verifier.SmartContract();
// 用户的根目录下必须有一个叫 'contract' 的文件夹。如果没有,我们将创建它。
if (!fs.existsSync(path.join(__dirname, '../../contract'))) {
console.log('Contract folder does not exist. Creating...');
fs.mkdirSync(path.join(__dirname, '../../contract'));
}
// 如果用户已经在 'contract' 文件夹中有一个名为 'plonk_vk.sol' 的文件,我们将删除它。
if (fs.existsSync(path.join(__dirname, '../../contract/plonk_vk.sol'))) {
fs.unlinkSync(path.join(__dirname, '../../contract/plonk_vk.sol'));
}
// 我们将合约写入 'contract' 文件夹中的 'plonk_vk.sol' 文件。
fs.writeFileSync(path.join(__dirname, '../../contract/plonk_vk.sol'), sc, {
flag: 'w',
});
return sc;
}
此命令编译位于 ./contract
目录中的合约。在这个特定的情况下,它编译 plonk_vk.sol 合约。
import { writeFileSync } from 'fs';
import { ethers } from 'hardhat';
async function main() {
// 部署验证合约
const Verifier = await ethers.getContractFactory('TurboVerifier');
const verifier = await Verifier.deploy();
// 获取部署的验证合约的地址
const verifierAddr = await verifier.deployed();
// 创建一个配置对象
const config = {
chainId: ethers.provider.network.chainId,
verifier: verifierAddr.address,
};
// 打印配置
console.log('Deployed at', config);
writeFileSync('utils/addresses.json', JSON.stringify(config), { flag: 'w' });
process.exit();
}
// 我们推荐这种模式,以便在任何地方使用 async/await,并妥善处理错误。
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
这个脚本将 plonk_vk.sol 合约部署到指定的 NETWORK 环境变量指向的网络,并将部署地址写入 ./utils/addresses.json
。
生成我们应用程序的生产优化版本。
要启动开发服务器,请执行以下命令:
npm run dev
在你的网络浏览器中导航至 http://localhost:3000。连接你的 MetaMask 钱包到 dapp,并将你的 MetaMask 网络切换到 Localhost 网络。然后,提供两个输入值,并点击计算证明按钮。这将启动证明计算并在链上进行验证。
在 ./components
目录中,components.tsx
文件包含两个处理这些操作的重要函数:
代码已加注释以便理解。
// 计算证明
const calculateProof = async () => {
// 仅在我们有可用于计算证明的 acir 时发起
// 设置待处理状态以显示旋转加载器
setPending(true);
if (input.x == "" || input.y == "") {
toast.error('字段不能为空!');
setPending(false);
} else {
// 为证明计算启动新的 worker
const worker = new Worker(new URL('../utils/prover.ts', import.meta.url));
// 处理来自 worker 的响应
worker.onmessage = e => {
if (e.data instanceof Error) {
toast.error('计算证明时出错');
setPending(false);
} else {
toast.success('证明计算完成');
setProof(e.data);
setPending(false);
}
};
// 将 acir 和输入发送给 worker
worker.postMessage({ input });
}
};
它首先检查输入字段是否为空。如果输入字段包含值,则继续将这些输入发送到新创建的 worker,并调用在 ./utils/prover.ts
文件中的 onmessage 函数。
// @ts-ignore
import { NoirBrowser } from '../utils/noir/noirBrowser';
// 为消息事件添加事件监听器
onmessage = async event => {
try {
const { input } = event.data;
const hexInputObj = Object.entries(input).reduce((newObj, [key, value]) => {
newObj[key] = (value as number).toString(16).padStart(2, '0');
return newObj;
}, {});
const noir = new NoirBrowser();
await noir.compile();
const proof = await noir.createProof({ input: hexInputObj });
console.log(hexInputObj);
postMessage(proof);
} catch (er) {
console.log(er);
postMessage(er);
} finally {
close();
}
};
为了创建证明,调用了 barretenberg 库中的 create_proof 函数。它接受三个参数:this.prover 对象、acir 和 input。
async createProof({input} : {input: any}) {
const proof = await create_proof(this.prover, this.acir, input);
return proof;
}
const verifyProof = async () => {
// 仅在我们有 acir 和要验证的证明时发起
if (proof) {
// 启动新的 worker 进行验证
const worker = new Worker(new URL('../utils/verifier.ts', import.meta.url));
console.log('worker 启动');
// 处理来自 worker 的响应
worker.onmessage = async e => {
if (e.data instanceof Error) {
toast.error('验证证明时出错');
} else {
toast.success('证明已验证');
// 在链上验证证明
const ethers = new Ethers();
const ver = await ethers.contract.verify(proof);
if (ver) {
toast.success('证明已在链上验证成功!');
setVerification(true);
} else {
toast.error('证明在链上验证失败');
setVerification(false);
}
}
};
// 将 acir 和证明发送给 worker
worker.postMessage({ proof });
}
};
它首先检查是否有可用于验证的证明。如果存在证明,将启动新的 worker。然后,将证明发送到 worker,并调用 ./utils/verifier.ts
文件中的 onmessage 函数来处理传入的证明,通过调用 barretenberg 的 verify_proof 函数。
async verifyProof({proof} : {proof: any}) {
const verification = await verify_proof(this.verifier, proof);
return verification;
}
如果整个过程按预期工作,则该函数应该返回 true。
1. 本地克隆仓库
git clone https://github.com/RareSkills/noir-zk-fullstack-example.git
2. 安装依赖
npm install
3. 在 http://localhost:8545 启动一个本地开发 EVM
将文件 .env.example 的名称更改为 .env 然后打开新终端执行以下命令:
npx hardhat node
4. 构建项目
NETWORK=localhost npm run build
5. 启动开发服务器
npm run dev
在你的网络浏览器中打开 http://localhost:3000。连接你的 MetaMask 钱包至 dapp 并将你的 MetaMask 网络切换到 Localhost 网络。如果找不到该选项,请打开 MetaMask 网络设置,添加新网络并使用以下详细信息进行配置:
- 网络名称
- Localhost 8545
- 新 RPC URL
- http://localhost:8545
- 链 ID - 1337 - 货币符号 - ETH
保存网络配置,切换你的 MetaMask 钱包至 Localhost 网络并测试 dApp 🙂
由于 Noir 的积极开发,项目频繁进行更新和改进。因此,最新版本的各种软件包之间出现不兼容的情况并不少见。
保持对最新发布说明和社区讨论的关注,至关重要,以避免因版本不兼容而造成潜在问题,并确保顺利的开发体验。
有关零知识编程的更多信息,请查看我们的零知识课程。有关高级智能合约开发,请查看我们的Solidity Bootcamp。
- 原文链接: rareskills.io/post/zk-ad...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!