ZK-App Client Implementation with Semaphore

ChiHaoLu
SWF Lab
Published in
15 min readDec 31, 2022

Author: ChiHaoLu(chihaolu.eth) @ swfLAB

本文僅供各為作為筆記服用,不具教學功能。適合已經基本理解何謂 ZKP 且會撰寫 Circom、Solidity 等語言的人閱讀。

Semaphore

Quick Look the Core Design

本篇文章主要透過 Semaphore 搭建 ZKP 身分應用,並介紹如何撰寫銜接電路以及合約的前端,這樣用戶們就不用自己產 Proof 與智能合約互動了,可以透過我們(項目方、Operator)提供的前端(客戶端)操作來證明即可。

Semaphore 是一個去中心化的匿名登入與身分證明系統。想法是在合約中有一個 Merkle tree 的 membership proof,可以不公開密碼也能證明自己是某個群體中(某顆 merkle tree)的成員(merkle tree 的其中一個 leaf)。

Semaphore 可以讓用戶在智能合約上註冊一個 Idendity(身分),並且廣播(Broadcast)他們的訊號(Signal),特點在於:

  1. 可以匿名的證明他們的身分存在某一個群體之中
  2. 公開地儲存(Broadcast)任意字串(Signal)在合約之中,這之中有類似於 nonce 的 External Nullifier 存在,如果在 External Nullifier 的情況下,有對同一個字串做 double-signalling 那就是非法的

實際上有 nullfier 和 path index 就已經足夠證明了,還需要 External Nullifier 是因為其作為 Nonce 作用,能讓一個 Identity 可以多次的在同一輪(Merkle Tree)中對同一個 Signal 進行廣播。

在 External Nullifier 不同的情況下,就會生成不同的nullifier hash。

當使用者註冊了他們的 identity,Semaphore 會產生一組 EdDSA 公私鑰對 pubKey/privKeypubKey/privKey,並且將 publickeypublickey 和兩個隨機秘密值哈希之後送去給合約,儲存在 Merkle Tree 中(就是加入一個 Group)。這個 Hash 就是 identityCommitmentidentityCommitment,而兩個隨機秘密值則為 identityNullifieridentityNullifier 和 identityTrapdooridentityTrapdoor。

接下來使用者就能用他們的 privKeyprivKey 來計算公鑰 pubKeypubKey,再用 pubKeypubKey 產生該 merkle tree 的 merkle proof。

在此 circuit 中只要公開該 merkle tree 的 root,其他使用者即可知道此使用者是這個 group 的成員

ZKP 的部分則是在驗證公私鑰對以及驗證公鑰是某個 merkle tree 的 leaf。用戶能夠在 Semaphore 提供的前端(client 端、鏈下)產生他們在某個 group 中的 proof。將 proof 送到合約中時就可以驗證這個 proof 是否 valid。

零知識證明可以讓用戶證明他們知曉某個已定義的資訊,卻不用把資訊本身揭露。

Glossary

Image Source: To Mixers and Beyond: presenting Semaphore, a privacy gadget built on Ethereum
const private_key = crypto.randomBytes(32).toString('hex');
const prvKey = Buffer.from(private_key, 'hex');
const pubKey = eddsa.prv2pub(prvKey);
const identity_nullifier = '0x' + crypto.randomBytes(31).toString('hex');
const identity_trapdoor = '0x' + crypto.randomBytes(31).toString('hex');
const identity_commitment = pedersenHash([
bigInt(circomlib.babyJub.mulPointEscalar(pubKey, 8)[0]),
bigInt(identity_nullifier),
bigInt(identity_trapdoor)
]);

每個 Identity 由兩個隱私部分組成(都是隨機產生的,且不紀錄在以太坊的智慧合約中):

  1. 31 Bytes 的 nullifier
  2. 31 Bytes 的 trapdoor

每個 Identity 會對應兩個公開資訊:

  1. Commitment 會存在合約中
  2. nullifier_hash:用來證明某個 Identity 對應的 commitment 存在 merkle tree 上。

每個 Identity 主要的兩個 Operations 分別為 Enroll(Deposit) 和 Boardcast(Withdral)。

當我們想要搭建一個 Dapp 在 Semaphore 上的時候,要在 Merkle Tree 中註冊一個 Identity 其實同時可以做為用戶註冊我們 Dapp 的行為,舉例來說在 DeFi 應用上可能是 Staking 或者 Depositing。

而當用戶想要領取金錢時(證明他在某個 Merkle Tree 中),也就是對應 Boardcast。

Deposit/Enroll

insertIdendity() 可以讓用戶的 Identity Commitment 被 insert 進 Merkle Tree 之中:

function insertIdentity(uint256 identity_commitment) public {
insert(id_tree_index, identity_commitment);
uint256 root = tree_roots[id_tree_index];
root_history[root] = true;
}

Identity 對應的 commitment 會新增到一個 merkle 樹(Group)上,同時新的 merkle 樹根會記錄在 root_history 的 mapping 中。

Withdral/Boardcast

當用戶已經在合約中有他們的 Identity 就可以發訊號(Signal)了。要發訊號時必須先提供其 Identity 在 Tree 上的證明(能計算出commitment):

function broadcastSignal(
bytes memory signal,
uint[2] memory a,
uint[2][2] memory b,
uint[2] memory c,
uint[4] memory input // (root, nullifiers_hash, signal_hash, external_nullifier)
) public isValidSignalAndProof(signal, a, b, c, input)
{
uint nullifiers_hash = input[1];
signals[current_signal_index++] = signal;
nullifier_hash_history[nullifiers_hash] = true;
emit SignalBroadcast(signal, nullifiers_hash, input[3]);
}
  • signal 為用戶想要傳送的資訊(字串)
  • a, b, c 為 ZKP 的 proof
  • input 是 ZKP 的 circuit public input。通常這個包括輸入包含 Merkle Root、nullifier hash、signal hash 和 External Nullifier。

進到 boardcastSignal() 前最先的驗證為 isValidSignalAndProof,會依序驗證以下內容:

  1. 廣播前的 Merkle Root 在 Contract 中被確實存著
  2. 這個 External Nullifier 沒有被使用過(為了避免 double-signaling)
  3. Signal Hash 是合法的
  4. External Nullifier 是正確的
  5. Proof 和 Input 送到 verifier 驗證通過

在 Proof 通過驗證之後,Signal 就會被記錄下來(同時對應的 nullifier hash 也會被記錄下來),如果 External Nullifier不變的話,所有 Identity 只能傳送一次 Signal。

System Design and Client

Design View

這個部分會使用 semaphore-protocol/boilerplate 作為範例使用,基本上最重要的內容就在 ./apps/web-app/src/pages 之中的三個檔案。

  • index.tsx: 生成 New Identity,也就是產生新的三個 Trapdoor(private, known only by user)、Nullifier(private, known only by user)、Commitment(public)
  • groups.tsx:加入 Group,也就是加入 Merkle Tree
  • proofs.tsx:廣播訊號,同時進行驗證

基本上需要用到的物件都來自於 Semaphore 官方套件:

import { Group } from "@semaphore-protocol/group"
import { Identity } from "@semaphore-protocol/identity"
import { generateProof, packToSolidityProof } from "@semaphore-protocol/proof"

主要是使用 subgraph 來聆聽當前區塊鏈的資訊(例如去 Fetch 合約中有的 Group 之類的)。

New Identity(index.tsx)

    useEffect(() => {
const identityString = localStorage.getItem("identity")

if (identityString) {
const identity = new Identity(identityString)

setIdentity(identity)

setLogs("Your Semaphore identity was retrieved from the browser cache 👌🏽")
} else {
setLogs("Create your Semaphore identity 👆🏽")
}
}, [])

const createIdentity = useCallback(async () => {
const identity = new Identity()

setIdentity(identity)

localStorage.setItem("identity", identity.toString())

setLogs("Your new Semaphore identity was just created 🎉")
}, [])

Enter Group(groups.tsx)

   const joinGroup = useCallback(async () => {
if (!_identity) {
return
}

const username = window.prompt("Please enter your username:")

if (username) {
setLoading.on()
setLogs(`Joining the Greeter group...`)

const { status } = await fetch("api/join", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
identityCommitment: _identity.commitment.toString(),
username
})
})

if (status === 200) {
addUser({ identityCommitment: _identity.commitment.toString(), username })

setLogs(`You joined the Greeter group event 🎉 Greet anonymously!`)
} else {
setLogs("Some error occurred, please try again!")
}

setLoading.off()
}
}, [_identity])

Broadcast Signal(proofs.tsx)

    const sendFeedback = useCallback(async () => {
if (!_identity) {
return
}

const options = ["zkConditionerRound1+1", "zkConditionerRound1-1"]
const feedback = prompt(
'Please vote with below options without quotes: \n1. "zkConditionerRound1+1" \n2. "zkConditionerRound1-1"'
)
if (feedback === null) {
alert("You can input null opinion!")
return
} else {
if (!options.includes(feedback?.toString())) {
alert('Your opinion is not "zkConditionerRound1+1" or "zkConditionerRound1-1"!')
return
}
}

if (feedback) {
setLoading.on()
setLogs(`Posting your anonymous vote...`)

try {
const group = new Group()
const feedbackHash = solidityKeccak256(["string"], [feedback])

const subgraph = new Subgraph("goerli")
const { members } = await subgraph.getGroup(env.GROUP_ID, { members: true })

group.addMembers(members)

const { proof, publicSignals } = await generateProof(_identity, group, env.GROUP_ID, feedbackHash)
const solidityProof = packToSolidityProof(proof)

const { status } = await fetch("api/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
feedback,
merkleRoot: publicSignals.merkleRoot,
nullifierHash: publicSignals.nullifierHash,
solidityProof
})
})

if (status === 200) {
addFeedback(feedback)

setLogs(`Your vote was posted 🎉`)
} else {
setLogs("Some error occurred, please try again!")
}
} catch (error) {
console.error(error)

setLogs("Some error occurred, please try again!")
} finally {
setLoading.off()
}
}
}, [_identity])

--

--

ChiHaoLu
SWF Lab

Multitasking Master & Mr.MurMur | Blockchain Dev. @ imToken Labs | chihaolu.me | Advisory Services - https://forms.gle/mVGKQwPQEUP37fLYA