我们将逐步探索一个旨在验证加法的基本 zk-dapp。该应用程序允许用户在不向区块链泄露实际数字的情况下,证明两个数字 X 和 Y 的总和等于 Z。尽管解决这个问题并不一定需要零知识证明,但在本示例中我们将使用它们以保持简单性并增强理解。
让我们深入研究代码,或者跳过直接运行 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
您可以灵活选择不同的网络来运行。为此,您需要进行以下调整:首先,通过添加部署者的私钥和 Alchemy 的 API 密钥来修改 .env 文件的内容。之后,导航到 hardhat.config.ts 文件并包含一个新的网络配置。
完成后,您可以使用 NETWORK 环境变量来指定所需的网络进行部署。例如 NETWORK=mumbai npm run build 或 NETWORK=sepoia npm run build。对于此 dapp,我们将使用以下命令在本地进行部署:
NETWORK=localhost npm run build
执行上述命令会按所述顺序触发另外四个命令的执行:
- hardhat run scripts/genContract.ts
- hardhat compile
- hardhat run --network ${NETWORK} scripts/deploy.ts
- next build
执行这些命令时会发生什么?
-
hardhat run scripts/genContract.ts
import { NoirServer } from '../utils/noir/noirServer'; async function main() { const noir = new NoirServer();await noir.compile(); noir.getSmartContract() process.exit(); } // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. main().catch(error => { console.error(error); process.exitCode = 1; });执行
genContract.ts脚本会调用 NoirServer 类的 compile 方法,该方法会编译写在./circuits/src目录中的 Noir 电路,并生成 ACIR(抽象电路中间表示)。此外,通过使用生成的 ACIR 调用 barretenberg 的 setup_generic_prover_and_verifier 来初始化 this.prover 和 this.verifier。async compile() { // I'm running on the server so I can use the file system 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(); // The user must have a folder called 'contract' in the root directory. If not, we create it. if (!fs.existsSync(path.join(__dirname, '../../contract'))) { console.log('Contract folder does not exist. Creating...'); fs.mkdirSync(path.join(__dirname, '../../contract')); } // If the user already has a file called 'plonk_vk.sol' in the 'contract' folder, we delete it. if (fs.existsSync(path.join(__dirname, '../../contract/plonk_vk.sol'))) { fs.unlinkSync(path.join(__dirname, '../../contract/plonk_vk.sol')); } // We write the contract to a file called 'plonk_vk.sol' in the 'contract' folder. fs.writeFileSync(path.join(__dirname, '../../contract/plonk_vk.sol'), sc, { flag: 'w', }); return sc; } -
hardhat compile
此命令会编译位于
./contract目录中的合约。在本特定示例中,它会编译 plonk_vk.sol 合约。 -
hardhat run --network ${NETWORK} scripts/deploy.ts
import { writeFileSync } from 'fs'; import { ethers } from 'hardhat'; async function main() { // Deploy the verifier contractconst Verifier = await ethers.getContractFactory('TurboVerifier'); const verifier = await Verifier.deploy(); // Get the address of the deployed verifier contract const verifierAddr = await verifier.deployed(); // Create a config object const config = { chainId: ethers.provider.network.chainId, verifier: verifierAddr.address, }; // Print the config console.log('Deployed at', config); writeFileSync('utils/addresses.json', JSON.stringify(config), { flag: 'w' }); process.exit(); } // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. main().catch(error => { console.error(error); process.exitCode = 1; });此脚本将 plonk_vk.sol 部署到分配给 NETWORK 环境变量的网络,并将部署后的地址写入
./utils/addresses.json。 -
next build
为生产环境生成应用程序的优化版本。
计算证明(前端)
要启动开发服务器,请执行以下命令:
npm run dev
在您的网络浏览器上导航至 http://localhost:3000。将您的 MetaMask 钱包连接到该 dapp,并将您的 MetaMask 网络切换为 Localhost 网络。然后,提供两个输入值并点击 Calculate proof 按钮。这将启动证明计算并在链上进行验证。
在 ./components 目录中,components.tsx 文件包含两个处理这些操作值得注意的函数:
- calculateProof
- verifyProof
代码已添加注释以便理解。
1. calculateProof
// Calculates proof
const calculateProof = async () => {
// only launch if we do have an acir to calculate the proof from
// set a pending state to show a spinner
setPending(true);
if (input.x == "" || input.y == "") {
toast.error('Fields can not be empty!');
setPending(false);
} else {
// launching a new worker for the proof calculation
const worker = new Worker(new URL('../utils/prover.ts', import.meta.url));
// handling the response from the worker
worker.onmessage = e => {
if (e.data instanceof Error) {
toast.error('Error while calculating proof');
setPending(false);
} else {
toast.success('Proof calculated');
setProof(e.data);
setPending(false);
}
};
// sending the acir and input to the worker
worker.postMessage({ input });
}
};
它首先检查输入字段是否不为空。如果输入字段包含值,它将继续将这些输入发送到新创建的 worker,并在 ./utils/prover.ts 文件中调用 onmessage 函数。
// @ts-ignore
import { NoirBrowser } from '../utils/noir/noirBrowser';
// // Add an event listener for the message event
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;
}
1. verifyProof
const verifyProof = async () => {
// only launch if we do have an acir and a proof to verify
if (proof) {
// launching a new worker for the verification
const worker = new Worker(new URL('../utils/verifier.ts', import.meta.url));
console.log('worker launched');
// handling the response from the worker
worker.onmessage = async e => {
if (e.data instanceof Error) {
toast.error('Error while verifying proof');
} else {
toast.success('Proof verified');
// Verifies proof on-chain
const ethers = new Ethers();
const ver = await ethers.contract.verify(proof);
if (ver) {
toast.success('Proof verified on-chain!');
setVerification(true);
} else {
toast.error('Proof failed on-chain verification');
setVerification(false);
}
}
};
// sending the acir and proof to the 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。
运行 dApp
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 网络设置,添加一个新网络并使用以下详细信息进行配置:
- Network name
- Localhost 8545
- New RPC URL
- http://localhost:8545
- Chain ID - 1337 - Currency Symbol - ETH
保存网络配置,将您的 MetaMask 钱包切换到 Localhost 网络并测试该 dApp :)
结论
由于 Noir 处于活跃开发阶段,该项目经常会有更新和改进。因此,各种包的最新版本之间出现不兼容的情况并不少见。
关注最新的发布说明和社区讨论至关重要,以避免由版本不兼容引起的潜在问题并确保无缝的开发体验。
了解更多
要了解有关零知识编程的更多信息,请参阅我们的零知识课程。对于高级智能合约开发,请参阅我们的 Solidity Bootcamp。
最初发布于 2023 年 5 月 28 日