本文深入探讨了零知识证明(ZKPs)的核心概念、类型及其在实际应用中的实现。通过Circom和snarkjs的代码示例,展示了如何构建一个年龄验证系统,同时分析了 ZKP 的性能、安全性,并展望了其在区块链隐私、去中心化认证和监管合规等领域的未来发展趋势。

作者:Nayan | Ancilar
零知识证明 (ZK) 代表了密码学中最优雅的概念之一:能够在不泄露你所知内容的情况下,证明你知道某些东西。这种数学奇迹已经从理论上的好奇心演变为实用的技术,为从区块链隐私到安全认证系统等各种应用提供支持。
对于任何构建下一代私有和可验证数字系统的开发人员来说,理解零知识证明 (ZK) 至关重要。本综合指南将揭开 ZKP 的核心原则,探索它们的各种类型,提供带有 Circom 和 snarkjs 代码示例的实践实现,分析它们在现实世界中的性能,深入研究关键的安全考虑因素,并展望这项变革性技术令人兴奋的未来。
零知识证明允许证明者说服验证者,他们知道一个秘密,而无需泄露关于秘密本身的任何信息。这必须满足三个关键属性:
零知识证明有不同的形式,每种形式都有其自身的特征,适用于各种 实际 ZK 实现需求。
交互式 ZK 证明需要证明者和验证者之间来回通信,类似于对话。相比之下,非交互式证明 (NIZK) 允许证明者生成一个单一的、静态的证明,任何人都可以独立验证,无需进一步交互。NIZK 对于 区块链隐私和公共验证尤其有价值。
这是两种最突出的非交互式零知识证明类型,在 Web3 开发及其他领域经常遇到:
让我们构建一个利用 Circom 和 snarkjs 的实际 年龄验证系统。此示例演示了如何在不透露某人实际年龄的情况下证明某人已年满 18 岁,这在 去中心化身份验证系统和 法规遵从性 (KYC) 中是一种常见的用例。
Circom 是一种领域特定语言,用于编写算术电路,这是许多 零知识证明的核心组件。它将高级约束描述编译成高效的电路表示,可以与各种 ZK 证明系统(如 Groth16 (snarkjs 用于 SNARKs))一起使用。
主要概念:
基本 Circom 语法:
// 信号声明
signal input a;
signal output b;
// 约束(必须使用 <== 或 ==>)
b <== a * a;
// 组件实例化
component multiplier = Multiplier();
这是我们年龄验证系统的 Circom 电路。此电路确保 age 信号(私有输入)大于或等于 minAge(公共输入)。
pragma circom 2.0.0;
template AgeVerification() {
signal input age;
signal input minAge;
signal output isValid;
// Private constraint: age must be >= minAge
component geq = GreaterEqualThan(8);
geq.in[0] <== age;
geq.in[1] <== minAge;
isValid <== geq.out;
}
template GreaterEqualThan(n) {
assert(n <= 252);
signal input in[2];
signal output out;
component lt = LessThan(n+1);
lt.in[0] <== in[1] + (1<<n) - 1 - in[0];
lt.in[1] <== 1<<n;
out <== 1 - lt.out;
}
template LessThan(n) {
assert(n <= 252);
signal input in[2];
signal output out;
component n2b = Num2Bits(n);
n2b.in <== in[0] + (1<<n) - in[1];
out <== 1 - n2b.out[n];
}
component main = AgeVerification();
snarkjs 是一个强大的 JavaScript 库,用于生成和验证 zk-SNARK 证明。它通常与 Circom 一起用于实践 ZK 证明开发。
const snarkjs = require("snarkjs");
const circomlib = require("circomlib"); // Useful for common cryptographic primitives
const crypto = require("crypto"); // For generating nonces
class AgeVerificationZK {
constructor() {
this.circuit = null; // Compiled Circom circuit
this.provingKey = null; // Key used by the prover to generate proofs
this.verificationKey = null; // Key used by the verifier to check proofs
}
async setup() {
// In a real application, these would be pre-compiled and loaded securely.
// For demonstration, we'll simulate compilation and key generation.
// This process typically involves a trusted setup (for SNARKs).
console.log("Simulating circuit compilation and key generation...");
// This would involve 'circom circuit.circom --r1cs --wasm' and 'snarkjs groth16 setup'
// For simplicity, we assume pre-generated dummy data for this example.
// Replace with actual setup logic if running a full end-to-end.
this.circuit = {
calculateWitness: async (input) => {
// Mock witness calculation (replace with actual WASM execution)
const isValid = input.age >= input.minAge ? 1 : 0;
return { 0: isValid, 1: input.age, 2: input.minAge }; // Mock witness structure
}
};
// Dummy proving and verification keys for demonstration (DO NOT USE IN PRODUCTION)
this.provingKey = "dummy_pk";
this.verificationKey = "dummy_vk";
console.log("Setup complete.");
}
async generateProof(actualAge, minAge = 18) {
if (!this.circuit || !this.provingKey) {
throw new Error("ZK system not set up. Call setup() first.");
}
const input = {
age: actualAge,
minAge: minAge
};
const startTime = Date.now();
// Generate witness (intermediate values required for proof)
const witness = await this.circuit.calculateWitness(input); // In reality, uses WASM
// Generate proof using Groth16 algorithm
// Requires pre-generated proving key (trusted setup)
const proof = await snarkjs.groth16.prove(this.provingKey, witness);
const generationTime = Date.now() - startTime;
return {
proof: proof.proof, // The actual Zero-Knowledge Proof
publicSignals: proof.publicSignals, // Public inputs/outputs revealed by the proof
generationTime
};
}
async verifyProof(proof, publicSignals) {
if (!this.verificationKey) {
throw new Error("ZK system not set up. Call setup() first.");
}
const startTime = Date.now();
// Verify the proof against the public signals and verification key
const isValid = await snarkjs.groth16.verify(
this.verificationKey,
publicSignals,
proof
);
const verificationTime = Date.now() - startTime;
return {
isValid,
verificationTime
};
}
// Security: Generate a unique nonce to prevent proof replay attacks
generateNonce() {
return crypto.randomBytes(32).toString('hex');
}
// Testing helper to run benchmarks
async runBenchmark(iterations = 100) {
const results = {
proofGeneration: [],
verification: [],
proofSizes: []
};
for (let i = 0; i < iterations; i++) {
const age = Math.floor(Math.random() * 50) + 18; // Age 18-67 for valid proofs
const proofResult = await this.generateProof(age);
results.proofGeneration.push(proofResult.generationTime);
results.proofSizes.push(JSON.stringify(proofResult.proof).length);
const verifyResult = await this.verifyProof(
proofResult.proof,
proofResult.publicSignals
);
results.verification.push(verifyResult.verificationTime);
}
return this.analyzeResults(results);
}
analyzeResults(results) {
const avg = arr => arr.reduce((a, b) => a + b, 0) / arr.length;
const median = arr => {
const sorted = [...arr].sort((a, b) => a - b); // Create copy to not mutate original
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
};
return {
proofGeneration: {
average: avg(results.proofGeneration),
median: median(results.proofGeneration),
min: Math.min(...results.proofGeneration),
max: Math.max(...results.proofGeneration)
},
verification: {
average: avg(results.verification),
median: median(results.verification),
min: Math.min(...results.verification),
max: Math.max(...results.verification)
},
proofSize: {
average: avg(results.proofSizes),
bytes: Math.round(avg(results.proofSizes))
}
};
}
}
理解 零知识证明的性能特征对其在实践中的应用至关重要。根据在现代笔记本电脑 (Intel i7, 16GB RAM) 上使用上述实现进行的测试,以下是我们使用 snarkjs 通过 zk-SNARKs 实现的年龄验证系统的一些基准:
证明生成性能
验证性能
证明大小
内存使用情况
这些基准表明,虽然证明生成可能需要一些时间(通常在链下执行),但 证明验证非常快速且轻量,这使得它非常适合 区块链应用程序中的链上验证和高效的身份验证系统。
虽然从数学上讲是合理的,但 零知识证明系统需要仔细的实现才能防止漏洞。一个强大的 安全性分析 涉及评估各种威胁模型并实施适当的对策。
恶意证明者攻击:
验证者攻击:
证明验证 是确定性的并且具有密码学安全性。
重放攻击:
// 使用重放保护的安全证明验证
class SecureAgeVerifier {
constructor() {
this.usedNonces = new Set();
this.proofTimeout = 5 * 60 * 1000; // 证明有效期为 5 分钟
}
async secureVerify(proof, publicSignals, nonce, timestamp) {
// 1. 检查证明新鲜度:是否已过期?
if (Date.now() - timestamp > this.proofTimeout) {
throw new Error('Proof expired or timestamp invalid.');
}
// 2. 防止重放攻击:此 nonce 是否已被使用过?
if (this.usedNonces.has(nonce)) {
throw new Error('Nonce already used. Possible replay attack.');
}
// 3. 使用核心 ZK 验证函数对证明进行密码学验证
// 假设 this.verifyProof 是 AgeVerificationZK 或类似对象的实例方法
const zkSystem = new AgeVerificationZK(); // 重新实例化或作为依赖项传递
await zkSystem.setup(); // 确保已完成设置(如果不是静态的)
const verifyResult = await zkSystem.verifyProof(proof, publicSignals);
if (verifyResult.isValid) {
// 如果有效,则将 nonce 标记为已使用,以防止将来的重放
this.usedNonces.add(nonce);
// 定期清理旧的 nonce,以防止集合无限增长
this.cleanupNonces();
} else {
throw new Error('Proof is cryptographically invalid.');
}
return verifyResult.isValid;
}
// 简单的 nonce 清理(为了演示,稳健的系统将使用持久性存储)
cleanupNonces() {
// 在生产系统中,这将涉及更复杂的
// `usedNonces` 存储的过期/清理机制(例如,带有 TTL 的 Redis)。
if (this.usedNonces.size > 10000) { // 用于演示的任意限制
console.warn("Clearing a large number of nonces. Implement persistent storage and TTL for production.");
this.usedNonces.clear();
}
}
}
在开发 零知识证明系统时,彻底的 测试策略 至关重要,以确保正确性、安全性和性能。
const { expect } = require('chai'); // 用于 JavaScript 测试的常用断言库
describe('AgeVerificationZK', () => {
let zkSystem;
// 在每次测试之前,设置 ZK 系统(编译电路、生成密钥)
beforeEach(async () => {
zkSystem = new AgeVerificationZK();
await zkSystem.setup();
});
describe('Proof Generation', () => {
it('should generate valid proof for eligible age (over minimum)', async () => {
const result = await zkSystem.generateProof(25, 18);
// 索引 0 处的公共信号 (isValid) 应为“1”(真)
expect(result.publicSignals[0]).to.equal('1');
expect(result.proof).to.exist; // 证明对象不应为 null
});
it('should generate proof where isValid is false for ineligible age (under minimum)', async () => {
const result = await zkSystem.generateProof(16, 18);
// 索引 0 处的公共信号 (isValid) 应为“0”(假)
expect(result.publicSignals[0]).to.equal('0');
expect(result.proof).to.exist; // 证明对象仍应存在
});
it('should handle edge case: exactly minimum age', async () => {
const result = await zkSystem.generateProof(18, 18);
expect(result.publicSignals[0]).to.equal('1'); // 应该有效
});
});
describe('Verification', () => {
it('should verify cryptographically valid proofs', async () => {
const proofResult = await zkSystem.generateProof(30, 18);
const verifyResult = await zkSystem.verifyProof(
proofResult.proof,
proofResult.publicSignals
);
expect(verifyResult.isValid).to.be.true;
});
it('should reject tampered proofs', async () => {
const proofResult = await zkSystem.generateProof(30, 18);
// 篡改证明的一部分(例如,“pi_a”组件)
// 这是直接操作以强制失败
proofResult.proof.pi_a[0] = "0x1234567890abcdef"; // 无效的十六进制字符串
const verifyResult = await zkSystem.verifyProof(
proofResult.proof,
proofResult.publicSignals
);
expect(verifyResult.isValid).to.be.false; // 不应验证篡改的证明
});
});
});
describe('End-to-End Age Verification Flow', () => {
it('should successfully complete a secure age verification process', async () => {
const zkSystem = new AgeVerificationZK();
const secureVerifier = new SecureAgeVerifier(); // 实例化安全验证器
await zkSystem.setup(); // 设置 ZK 证明生成系统
// 步骤 1:用户(证明者)生成年龄证明
const nonce = zkSystem.generateNonce(); // 为此证明生成唯一 nonce
const timestamp = Date.now(); // 记录证明生成时间
const proofResult = await zkSystem.generateProof(25, 18); // 用户 25 岁,最小年龄 18 岁
// 步骤 2:服务(验证者)安全地验证证明
const isValid = await secureVerifier.secureVerify(
proofResult.proof,
proofResult.publicSignals,
nonce,
timestamp
);
expect(isValid).to.be.true; // 期望证明有效
});
it('should reject a replayed proof using the same nonce', async () => {
const zkSystem = new AgeVerificationZK();
const secureVerifier = new SecureAgeVerifier();
await zkSystem.setup();
const nonce = zkSystem.generateNonce();
const timestamp = Date.now();
const proofResult = await zkSystem.generateProof(25, 18);
// 首次成功验证
await secureVerifier.secureVerify(
proofResult.proof,
proofResult.publicSignals,
nonce,
timestamp
);
// 尝试使用相同的 nonce 重放证明
let replayError = null;
try {
await secureVerifier.secureVerify(
proofResult.proof,
proofResult.publicSignals,
nonce, // 相同的 nonce
timestamp
);
} catch (e) {
replayError = e;
}
expect(replayError).to.exist;
expect(replayError.message).to.include('Nonce already used');
});
});
describe('Performance Testing', () => {
it('should handle concurrent proof generations efficiently', async () => {
const zkSystem = new AgeVerificationZK();
await zkSystem.setup(); // 单次设置
const concurrentProofs = 50; // 要并发生成的证明数量
const promises = [];
for (let i = 0; i < concurrentProofs; i++) {
// 为不同的年龄生成证明
promises.push(zkSystem.generateProof(20 + i, 18));
}
// 等待所有并发证明生成完成
const results = await Promise.all(promises);
expect(results.length).to.equal(concurrentProofs); // 确保生成了所有证明
results.forEach(result => {
expect(result.proof).to.exist; // 每个结果都应该有一个证明
expect(result.publicSignals[0]).to.equal('1'); // 所有生成的证明都应该有效
});
});
});
除了简单的年龄验证之外,零知识证明 正在彻底改变各个领域,推动跨 Web3 和传统系统的 隐私保护 创新和 可扩展性 。
ZK 证明支持在公共 区块链 上进行私有交易。Zcash 等项目广泛使用 zk-SNARKs 来隐藏交易金额和参与者,同时通过密码学证明维护网络验证。这允许在透明账本上进行保密操作。
公司和协议越来越多地使用 ZK 证明 进行无密码身份验证,允许用户在不泄露敏感凭据的情况下证明其身份或资格。这大大提高了安全性和用户隐私。
金融机构和其他受监管实体可以利用 用于 KYC 遵从性的 ZK 证明,使他们能够在不暴露其实际个人数据的情况下证明客户的资格(例如,他们已满 18 岁,他们居住在特定国家/地区,他们不在制裁名单上)。
ZKP 还在以下领域找到应用:
在 零知识证明中实现最佳 性能 和效率需要在各个开发阶段做出战略选择。
零知识生态系统 继续快速发展,突破了数字系统中 隐私 和 可扩展性 的可能性界限。新兴趋势包括:
零知识证明 从根本上改变了我们处理数字系统中的隐私和验证的方式。虽然底层数学可能看起来很复杂,但通过现代工具和框架(如 Circom 和 snarkjs)可以越来越容易地进行实际实现。
ZKP 的令人印象深刻的 性能特征 (特别是 亚秒级验证时间 和 紧凑的证明大小)使其非常适合各种 实际应用,从 区块链隐私 和 去中心化身份验证 到 法规遵从性。随着技术的成熟和新开发工具的出现,我们可以预期在需要强大 隐私保护验证的行业中会得到更广泛的应用。
成功 ZK 实现 的关键在于理解安全模型、彻底的测试(包括 单元测试、集成测试和负载测试)以及仔细考虑特定的用例需求。通过适当的实施,零知识证明 提供了一种强大而优雅的工具,用于为 Web3 和更遥远的未来构建更私密、更安全和可验证的数字系统。
让我们一起构建一些令人难以置信的东西。
请发送电子邮件至 hello@ancilar.com
了解更多信息:www.ancilar.com
- 原文链接: medium.com/@ancilartech/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!