各位观众老爷们,晚上好!我是今天的主讲人,江湖人称“代码搬运工”,今天咱们聊点刺激的——如何在浏览器里玩转零知识证明,特别是用 snarkjs
这个神器。
开场白:啥是零知识证明?
想象一下,你想证明你是个老司机,但又不想暴露你的驾照信息,甚至不想让别人知道你到底有没有驾照,这就是零知识证明的精髓:证明“我知道”,但不泄露“我知道什么”。
更学术点说,零知识证明 (Zero-Knowledge Proof, ZKP) 是一种密码学协议,允许一方(证明者)向另一方(验证者)证明某个陈述是真实的,而无需透露除陈述本身之外的任何信息。
为什么要在浏览器里搞 ZKP?
你可能会问,这玩意儿听起来高大上,跟咱们前端程序员有啥关系?关系大了!
- 隐私保护: 在用户数据敏感的场景下,比如身份验证、投票、支付等,ZKP 可以保护用户隐私,避免数据泄露。
- 计算外包: 你可以将复杂的计算放到浏览器里进行,然后用 ZKP 证明计算结果的正确性,而无需信任服务器。
- 链上验证: 在区块链应用中,ZKP 可以让链上验证更高效,降低交易成本。
snarkjs
:前端 ZKP 的瑞士军刀
snarkjs
是一个 JavaScript 库,专门用于生成和验证 ZK-SNARKs (Succinct Non-interactive ARguments of Knowledge)。它提供了以下核心功能:
- 电路描述: 使用 Circom 语言描述你的计算逻辑。
- 电路编译: 将 Circom 电路编译成 R1CS (Rank-1 Constraint System) 形式。
- 可信设置: 生成 proving key 和 verifying key,这是 ZK-SNARKs 的关键。
- 证明生成: 使用 proving key 和输入数据生成证明。
- 证明验证: 使用 verifying key 和公共输入数据验证证明。
浏览器集成:手把手教你撸代码
废话不多说,咱们直接上代码,看看如何在浏览器里使用 snarkjs
。
1. 环境搭建
首先,你需要安装 snarkjs
和相关依赖。这里我们使用 npm 或者 yarn:
npm install snarkjs circomlib --save
# 或者
yarn add snarkjs circomlib
由于 snarkjs
依赖一些 Node.js 的模块,我们需要使用 webpack 或者 Parcel 等打包工具将代码打包成浏览器可用的 bundle。 这里以 webpack 为例:
npm install webpack webpack-cli --save-dev
创建一个 webpack.config.js
文件:
const path = require('path');
module.exports = {
entry: './src/index.js', //你的入口文件
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
library: 'zkpLib', // 暴露给全局的变量名,方便在HTML中调用
libraryTarget: 'umd', // 使用通用模块定义
globalObject: 'this', // 兼容不同的环境
},
mode: 'development', // 设置为开发模式
devtool: 'inline-source-map', // 方便调试
resolve: {
fallback: {
"fs": false,
"tls": false,
"net": false,
"path": false,
"zlib": false,
"http": false,
"https": false,
"stream": false,
"crypto": false, // 避免crypto报错
},
},
};
创建一个 src/index.js
作为入口文件,我们将在其中编写 ZKP 相关的代码。
2. 定义 Circom 电路
Circom 是一种领域特定语言 (DSL),用于描述算术电路。咱们先来一个简单的例子:证明你知道一个数的平方根。
创建一个名为 sqrt.circom
的文件:
pragma circom 2.0.0;
template Sqrt() {
signal input in;
signal input sqrt;
signal output out;
in === sqrt * sqrt;
out <== in;
}
component main {public [in]} = Sqrt();
这段代码定义了一个名为 Sqrt
的模板,它接受一个输入 in
和一个输入 sqrt
,并验证 in
是否等于 sqrt
的平方。out
只是将 in
赋值过去,没什么实际意义,仅仅是为了满足 Circom 语法的要求,电路必须有输出。 component main {public [in]} = Sqrt();
定义了主电路,并将 in
设置为公共输入。
3. 编译 Circom 电路
使用 snarkjs
提供的命令行工具编译 Circom 电路:
snarkjs circom compile sqrt.circom -o sqrt.json
这条命令会将 sqrt.circom
编译成 sqrt.json
,其中包含了电路的 R1CS 描述。
4. 生成 proving key 和 verifying key
这一步是 ZK-SNARKs 的关键,我们需要生成 proving key 和 verifying key。这里我们使用 snarkjs powersOfTau
命令生成 powers of tau 文件,然后使用 snarkjs groth16 setup
命令生成 keys。
首先,生成 powers of tau 文件:
snarkjs powersOfTau new bn128 12 pot12.ptau
snarkjs powersOfTau contribute pot12.ptau pot12_1.ptau --name="First contribution" -v
snarkjs powersOfTau prepare pot12_1.ptau pot12_final.ptau
然后,生成 proving key 和 verifying key:
snarkjs groth16 setup sqrt.r1cs pot12_final.ptau proving_key.json verifying_key.json
5. 在浏览器中使用 snarkjs
现在,我们可以在浏览器中使用 snarkjs
生成和验证证明了。
在 src/index.js
中编写以下代码:
import * as snarkjs from 'snarkjs';
async function generateProof(input) {
// 导入电路描述、proving key 和 verifying key
const circuit = require('./sqrt.json');
const provingKey = require('./proving_key.json');
// 计算 witness
const witness = await circuit.calculateWitness(input);
// 生成证明
const { proof, publicSignals } = await snarkjs.groth16.prove(provingKey, witness);
return { proof, publicSignals };
}
async function verifyProof(proof, publicSignals) {
// 导入 verifying key
const verifyingKey = require('./verifying_key.json');
// 验证证明
const verificationKey = {
protocol: 'groth16',
curve: 'bn128',
vk_alpha_1: verifyingKey.alpha_1,
vk_beta_2: verifyingKey.beta_2,
vk_gamma_2: verifyingKey.gamma_2,
vk_delta_2: verifyingKey.delta_2,
vk_alphabeta_11: verifyingKey.alphabeta_11,
vk_gamma_abc: verifyingKey.gamma_abc,
};
const verified = await snarkjs.groth16.verify(verificationKey, publicSignals, proof);
return verified;
}
// 暴露函数给全局
window.generateProof = generateProof;
window.verifyProof = verifyProof;
6. 创建 HTML 文件
创建一个 index.html
文件,用于调用 JavaScript 函数:
<!DOCTYPE html>
<html>
<head>
<title>snarkjs Example</title>
</head>
<body>
<h1>snarkjs Example</h1>
<button onclick="testZKP()">Run ZKP</button>
<div id="result"></div>
<script src="./dist/bundle.js"></script>
<script>
async function testZKP() {
const input = { in: "9" }; // 输入要证明的数的平方
const sqrt_val = 3;
// 修改电路以接受sqrt作为输入
const witness = { in: input.in, sqrt: sqrt_val };
const { proof, publicSignals } = await window.generateProof(witness);
const verified = await window.verifyProof(proof, publicSignals);
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = `Proof Verification Result: ${verified}`;
}
</script>
</body>
</html>
7. 打包和运行
使用 webpack 打包代码:
npx webpack
在浏览器中打开 index.html
,点击按钮,你就可以看到证明验证的结果了。
代码解释
generateProof(input)
函数:- 导入编译后的电路描述、proving key。
- 根据输入计算 witness。这里需要注意的是,witness 包含了所有的输入信号,包括公共输入和私有输入。
- 使用
snarkjs.groth16.prove()
函数生成证明。 - 返回 proof 和 publicSignals。
verifyProof(proof, publicSignals)
函数:- 导入 verifying key。
- 使用
snarkjs.groth16.verify()
函数验证证明。 - 返回验证结果。
注意事项
- 可信设置: ZK-SNARKs 的安全性依赖于可信设置的安全性。在生产环境中,需要使用 MPC (Multi-Party Computation) 等技术来生成 proving key 和 verifying key,以避免单点故障。
- 电路设计: 电路设计是 ZK-SNARKs 的关键。一个好的电路设计可以提高证明效率,降低计算成本。
- 性能优化: ZK-SNARKs 的计算复杂度较高,需要进行性能优化。可以使用 WebAssembly 等技术来提高计算速度。
- 数据类型: 确保输入数据的类型与电路定义中的类型一致。
snarkjs
通常使用字符串来表示大整数。 - 文件路径: 确保文件路径正确,特别是电路描述、proving key 和 verifying key 的路径。
- 浏览器兼容性:
snarkjs
在不同的浏览器中可能存在兼容性问题,需要进行测试。 - 内存限制: 浏览器环境的内存有限,需要注意控制电路的复杂度,避免内存溢出。
表格总结
| 步骤 | 命令/代码 | 描述
| 依赖 | snarkjs
, circomlib
, webpack
| snarkjs
是 ZKP 核心库,circomlib
提供常用电路组件,webpack
用于打包。 咱们今天就先聊到这儿,希望大家有所收获! 下次再见!