返回 Expert 笔记
Expert Day 231

ZK 应用 — 身份(Semaphore / World ID / Sismo)

三大 ZK 身份协议架构、commitment + nullifier 在身份场景的复用

2026-12-18
Phase 4 - ZK电路开发实战 (Day 223-243)
ZKSemaphoreWorldIDSismoidentityanonymous-signaling

日期: 2026-12-18 方向: ZK工程 / 电路开发 阶段: Phase 4 - ZK电路开发实战 (Day 223-243) 标签: #ZK #Semaphore #WorldID #Sismo #identity #anonymous-signaling


今日目标

类型内容
学习三大 ZK 身份协议架构、commitment + nullifier 在身份场景的复用
实操部署 Semaphore demo(创建 identity → join group → broadcast signal)
产出semaphore_demo/(合约 + JS SDK 调用 + 完整流程)

背景与定位

为什么 ZK 身份是 Killer App?

  • 传统身份:要么完全公开(钱包地址)要么完全私有(KYC)。
  • ZK 身份:「证明我是 X 类人」(成年/真人/某 DAO 成员)但不暴露具体是谁。
  • 核心三大场景:
    1. Proof of Personhood — 反女巫、UBI(World ID, BrightID)
    2. Anonymous Membership — 匿名投票、whistleblowing(Semaphore)
    3. Selective Disclosure — 选择性披露属性(Sismo, Polygon ID)

三大协议架构对比

维度SemaphoreWorld IDSismo
维护者PSE (Ethereum Foundation)Worldcoin / Tools for HumanitySismo Labs
身份来源自创建 (任意 secret)虹膜扫描 (Orb)链上行为聚合
反女巫由 group 维护者保证生物识别(一人一证)aggregated badges
协议 ZKCircom + Groth16Semaphore-basedPythia (Hydra-S2)
HashPoseidonPoseidonPoseidon
树深度203020
隐私强度极强极强(生物 ID 不上链)中等(badges 公开)
主要场景匿名投票/聊天/空投UBI / 真人证明链上 reputation

Semaphore 深度解析

核心数据结构

Identity = (trapdoor, nullifier)  ← 用户秘密
Commitment = Poseidon(trapdoor, nullifier, 0)
Group = Merkle tree of commitments
ExternalNullifier = epoch / topic / scope
NullifierHash = Poseidon(nullifier, externalNullifier)
Signal = anything (vote, message)
SignalHash = keccak256(signal) >> 8

电路(简化)

Public:
  root              — group merkle root
  externalNullifier — context (e.g., poll ID)
  nullifierHash     — derived
  signalHash        — bound to message

Private:
  trapdoor, nullifier
  pathElements[20], pathIndices[20]

Constraints:
  commitment = Poseidon(trapdoor, nullifier, 0)
  MerkleProof(commitment, pathElements, pathIndices) === root
  nullifierHash === Poseidon(nullifier, externalNullifier)
  signalHash * signalHash === signalHash * signalHash  // bind signal

nullifierHash 让 (identity, externalNullifier) 唯一——同一人在同一 poll 只能投一次,但跨 poll 不可关联。


完整 Semaphore Demo 代码

1. 创建 Identity

// scripts/create_identity.ts
import { Identity } from "@semaphore-protocol/identity";
import * as fs from "fs";

const identity = new Identity();

console.log("commitment :", identity.commitment.toString());
console.log("trapdoor   :", identity.trapdoor.toString());
console.log("nullifier  :", identity.nullifier.toString());

fs.writeFileSync("identity.json", JSON.stringify({
    trapdoor: identity.trapdoor.toString(),
    nullifier: identity.nullifier.toString(),
}));

2. 部署 Group 合约

// contracts/AnonymousVoting.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@semaphore-protocol/contracts/interfaces/ISemaphore.sol";

contract AnonymousVoting {
    ISemaphore public semaphore;
    uint256 public groupId;
    uint256 public pollId;          // externalNullifier
    address public coordinator;

    event ProposalCreated(uint256 pollId);
    event Voted(uint256 indexed pollId, uint256 vote, uint256 nullifierHash);

    constructor(address _semaphore, uint256 _groupId) {
        semaphore = ISemaphore(_semaphore);
        groupId = _groupId;
        coordinator = msg.sender;
    }

    function joinGroup(uint256 commitment) external {
        // typically restricted; here open for demo
        semaphore.addMember(groupId, commitment);
    }

    function createPoll() external {
        require(msg.sender == coordinator, "only coord");
        pollId++;
        emit ProposalCreated(pollId);
    }

    function vote(
        uint256 vote,                    // YES=1 / NO=0
        uint256 merkleTreeRoot,
        uint256 nullifierHash,
        uint256[8] calldata proof
    ) external {
        semaphore.verifyProof(
            groupId,
            merkleTreeRoot,
            vote,                        // signal
            nullifierHash,
            pollId,                      // externalNullifier
            proof
        );
        emit Voted(pollId, vote, nullifierHash);
    }
}

3. JS 投票脚本

// scripts/vote.ts
import { Identity } from "@semaphore-protocol/identity";
import { Group } from "@semaphore-protocol/group";
import { generateProof } from "@semaphore-protocol/proof";
import { ethers } from "hardhat";
import * as fs from "fs";

async function main() {
    const json = JSON.parse(fs.readFileSync("identity.json", "utf8"));
    const identity = new Identity(json.trapdoor + ":" + json.nullifier);

    // load group state from chain (omitted: subscribe to MemberAdded events)
    const groupId = 1;
    const group = new Group(groupId, 20);
    const memberCommits: bigint[] = await fetchAllMembers();
    memberCommits.forEach(c => group.addMember(c));

    const voteValue = 1n;     // YES
    const pollId = 1n;        // externalNullifier

    const { proof, merkleTreeRoot, nullifierHash } = await generateProof(
        identity,
        group,
        pollId,                    // externalNullifier
        voteValue,                 // signal
        { wasmFilePath: "./node_modules/@semaphore-protocol/proof/snark-artifacts/semaphore.wasm",
          zkeyFilePath: "./node_modules/@semaphore-protocol/proof/snark-artifacts/semaphore.zkey" }
    );

    const voting = await ethers.getContractAt("AnonymousVoting", process.env.VOTING!);
    const tx = await voting.vote(voteValue, merkleTreeRoot, nullifierHash, proof);
    const r = await tx.wait();
    console.log("voted, gas =", r!.gasUsed.toString());
}

main();

4. 部署脚本

// scripts/deploy.ts
import { ethers } from "hardhat";

async function main() {
    // Semaphore deployed addresses (mainnet/testnet)
    const SEMAPHORE = "0x3889927F0B5Eb1a02C6E2C20b39a1Bd4EAd76131"; // sepolia

    const Voting = await ethers.getContractFactory("AnonymousVoting");
    const voting = await Voting.deploy(SEMAPHORE, 1 /* groupId */);
    await voting.waitForDeployment();
    console.log("Voting:", await voting.getAddress());
}

main();

真实合约地址 / Real Contract Addresses

协议网络地址
Semaphore (V3)Ethereum mainnet0x4B62B05F2cD60adAec5060daF73Ee5Ed01b3712b
Semaphore (V4)Sepolia0x...(最新见 docs.semaphore.pse.dev)
World IDOptimism0x57f928158C3EE7CDad1e4D8642503c4D0201f611
World IDPolygon0x515f06B36E6D3b707eAecBdeD18d8B384944c87f
Sismo HubMainnet0x44a8e4F9bCa67E0CB4533ee2d05D77ABe7c80Bd9

真实数据 / Real Data

Semaphore proof

  • proof size: 256 bytes (Groth16 + 8 public)
  • verify gas: ~250,000
  • prove time(浏览器): ~3 sec
  • 约束数:~12,000

World ID

  • 注册用户:~5M+ orbs verified(截至 2024)
  • 应用:Worldcoin grants(每周 ~3 WLD UBI)、Discord/Reddit 反 sybil
  • 协议:Semaphore + 「世界级别 group」(所有 Orb 验证用户)

Sismo

  • 累计 badges minted:~500k
  • 主要 use case:DAO 空投定向(如 Lens)、协议 boosted user
  • 已停止主要运营(2024 后转向 Sismo Connect)

安全教训 / Security Lessons

漏洞 1:Semaphore V1 trusted setup

2020 年 Semaphore V1 ceremony 出现某个 contributor 没有正确销毁 toxic waste 的怀疑。V2 重做 ceremony,扩大参与者到 50+。教训:trusted setup 透明度直接影响协议可信度。

漏洞 2:World ID 隐私争议

虹膜数据虽不上链,但 Worldcoin 中心化服务器存储模板。批评者:「ZK 在前端,集权在后端」。Tools for Humanity 后续推出 Personal Custody 让用户本地存储。

漏洞 3:MACI v1 underconstrained

Privacy & Scaling Explorations 的 MACI v1(基于 Semaphore)2022 年审计发现:投票电路某约束允许 prover 提交多次相同 nullifier。修复:v1.1 加强约束。

漏洞 4:Nullifier 范围攻击

如果 externalNullifier 选错(如对所有 polls 用同一个),nullifierHash 会跨 poll 被关联,破坏匿名性。Semaphore docs 强烈建议每个 poll 用独立 externalNullifier。


生产经验

  • 离线 group 同步:Semaphore 链上只存 root,client 必须从事件重建 leaves。大群(100k+ members)同步可能慢,需要后端服务(如 zk-kit Bandada)。
  • proof 在浏览器 vs 后端:3 秒可接受;如要支持手机端,考虑 server-side prover(牺牲一点信任)。
  • gas 优化:Semaphore V4 把 verifier gas 从 ~280k 降到 ~210k(custom verifier),重要因为投票场景每用户都要 verify 一次。

关键速查

// SDK 调用速查
import { Identity } from "@semaphore-protocol/identity";
import { Group } from "@semaphore-protocol/group";
import { generateProof, verifyProof } from "@semaphore-protocol/proof";

const identity = new Identity();
const group = new Group(groupId, treeDepth);
group.addMember(identity.commitment);

const { proof, merkleTreeRoot, nullifierHash } =
    await generateProof(identity, group, externalNullifier, signal, snarkArtifacts);

await verifyProof(proof, treeDepth);

面试题

  1. Q: Semaphore 的核心 ZK 证明在证明什么? A: 证明 (1) 我知道某个 trapdoor/nullifier 对应一个 commitment;(2) 这个 commitment 在 Group 的 Merkle tree 中;(3) 同时输出 Poseidon(nullifier, externalNullifier) 作为 nullifierHash 防双投。整个过程不暴露 trapdoor/nullifier。

  2. Q: World ID 与 Semaphore 是什么关系? A: World ID 是 Semaphore 协议的特化应用:群组就是「所有通过 Orb 虹膜验证的人」。技术栈复用 Semaphore,但身份准入由 Tools for Humanity 中心化运营 Orb 设备控制。

  3. Q: 为什么 nullifier 要和 externalNullifier 一起 hash? A: 让同一身份在不同上下文(不同 poll)产生不同 nullifierHash,避免跨 poll 关联(保护匿名性),同时在同一 poll 内防双投。这是 ZK 身份协议的精妙设计。

  4. Q: World ID 的「proof of personhood」是真正的 ZK 吗? A: 链上部分是 ZK(与 Semaphore 同),但身份获取(虹膜扫描)依赖中心化 Orb 设备。批评者称为「ZK on-chain, trust off-chain」。


明日预告

Day 232 — ZK 应用:投票 / MACI。Vitalik 倡导的 Minimal Anti-Collusion Infrastructure,解决「贿选」问题。