在這個教程中,我們将學習如何開發一個基于以太坊的零知識身份證明DApp,學習如何開發Circom零知識電路、如何生成并方法Solidity零知識驗證智能合約,以及如何利用Javascript在鍊下生成零知識證據,并在教程最後提供完整的源代碼下載下傳。
區塊鍊開發教程連結: 以太坊 | 比特币 EOS Tendermint Hyperledger Fabric Omni/USDT Ripple
1、零知識身份證明DApp概述
我們将開發一個零知識應用來證明一個使用者屬于特定的群組而無需透露使用者的具體資訊,使用流程如下圖所示:

我們的開發過程分為以下幾個步驟:
- 開發零知識電路
- 生成用于驗證零知識電路的Solidity庫
- 開發智能合約并內建上述Solidity庫
- 本地生成證據并在鍊上進行驗證
2、零知識證明以太坊DApp開發環境搭建
就像你不需要完全了解HTTP協定也可以開發web應用一樣,已經有很多工具可以幫助開發基于零知識的DApp而無需密碼學或數學基礎。
我推薦如下的開發語言和工具鍊:
- JavaScript/TypeScript:應用采用javascript/typescript開發,因為這兩者在以太坊生态中得到很好的支援
- Solidity: 智能合約用Solidity開發,因為它很成熟并且社群很好
- Truffle:使用Truffle作為智能合約開發和部署架構
- Circom:使用Circom來開發零知識證明電路
3、CIRCOM零知識電路開發:判斷私鑰是否比對公鑰集
我們的目标是建立一個電路,該電路可以判别輸入的私鑰是否對應于輸入的公鑰集合之一。該電路的僞代碼如下:
// Note that a private key is a scalar value (int)
// whereas a public key is a point in space (Tuple[int, int])
const zk_identity = (private_key, public_keys) => {
// derive_public_from_private is a function that
// returns a public key given a private key
derived_public_key = derive_public_from_private(private_key)
for (let pk in public_keys):
if derived_public_key === pk:
return true
return false
}
我們現在要開始用circom編寫零知識電路了。circom的文法可以查閱其
官方文檔。
首先建立項目檔案夾并安裝必要的依賴包:
npm install circom circomlib snarkjs websnark
mkdir contracts
mkdir circuits
mkdir -p build/circuits
touch circuits/circuit.circom
現在編寫電路檔案circuit.circom,首先引入(incluude)必要的基礎電路并定義PublicKey模闆:
include "../node_modules/circomlib/circuits/bitify.circom";
include "../node_modules/circomlib/circuits/escalarmulfix.circom";
include "../node_modules/circomlib/circuits/comparators.circom";
template PublicKey() {
// Note: private key needs to be hashed, and then pruned
// to make sure its compatible with the babyJubJub curve
signal private input in;
signal output out[2];
component privBits = Num2Bits(253);
privBits.in <== in;
var BASE8 = [
5299619240641551281634865583518297030282874472190772894086521144482721001553,
16950150798460657717958625567821834550301663161624707787222815936182638968203
];
component mulFix = EscalarMulFix(253, BASE8);
for (var i = 0; i < 253; i++) {
mulFix.e[i] <== privBits.out[i];
}
out[0] <== mulFix.out[0];
out[1] <== mulFix.out[1];
}
PublicKey模闆的作用是在babyJubJub曲線上找出私鑰(電路輸入)對應的公鑰(電路輸出)。注意在上面的電路中,我們将輸入私鑰聲明為私有信号,是以在生成的證據中不會包含任何可以重構該輸入私鑰的資訊。
一旦完成上述的基礎子產品,現在就可以建構我們的零知識證明電路的主邏輯了 —— 驗證指定的使用者是否屬于一個群組:
include ...
template PublicKey() {
...
}
template ZkIdentity(groupSize) {
// Public Keys in the smart contract
// Note: this assumes that the publicKeys
// are all unique
signal input publicKeys[groupSize][2];
// Prover's private key
signal private input privateKey;
// Prover's derived public key
component publicKey = PublicKey();
publicKey.in <== privateKey;
// Make sure that derived public key needs to
// matche to at least one public key in the
// smart contract to validate their identity
var sum = 0;
// Create a component to check if two values are
// equal
component equals[groupSize][2];
for (var i = 0; i < groupSize; i++) {
// Helper component to check if two
// values are equal
// We don't want to use ===
// as that will fail immediately if
// the predicate doesn't hold true
equals[i][0] = IsEqual();
equals[i][1] = IsEqual();
equals[i][0].in[0] <== publicKeys[i][0];
equals[i][0].in[1] <== publicKey.out[0];
equals[i][1].in[0] <== publicKeys[i][1];
equals[i][1].in[1] <== publicKey.out[1];
sum += equals[i][0].out;
sum += equals[i][1].out;
}
// equals[i][j].out will return 1 if the values are equal
// and 0 if the values are not equal
// Therefore, if the derived public key (a point in space)
// matches a public keys listed in the smart contract, the sum of
// all the equals[i][j].out should be equal to 2
sum === 2;
}
// Main entry point
component main = ZkIdentity(2);
現在我們編譯、設定并生成該電路的Solidity驗證器:
$(npm bin)/circom circuits/circuit.circom -o build/circuits/circuit.json
# snarkjs setup might take a few seconds
$(npm bin)/snarkjs setup --protocol groth -c build/circuits/circuit.json --pk build/circuits/provingKey.json --vk build/circuits/verifyingKey.json
# Generate solidity lib to verify proof
$(npm bin)/snarkjs generateverifier --pk build/circuits/provingKey.json --vk build/circuits/verifyingKey.json -v contracts/Verifier.sol
# You should now have a new "Verifier.sol" in your contracts directory
# $ ls contracts
# Migrations.sol Verifier.sol
注意我們使用groth協定生成證明密鑰和驗證密鑰,因為我們希望使用websnark來生成證據,因為websnark要比snarkjs性能好的多。
一旦完成上面的環節,我們就已經實作了零知識證明邏輯。下面的部分我們将介紹如何使用生成的Solidity零知識驗證合約。
4、Solidity零知識驗證合約
在完成零知識電路的設定之後,會生成一個名為Verifier.sol的solidity庫。如果你檢視這個檔案的内容,就會看到其中包含如下的函數:
...
function verifyProof(
uint[2] memory a,
uint[2][2] memory b,
uint[2] memory c,
uint[4] memory input
) public view returns (bool r) {
Proof memory proof;
proof.A = Pairing.G1Point(a[0], a[1]);
proof.B = Pairing.G2Point([b[0][0], b[0][1]], [b[1][0], b[1][1]]);
proof.C = Pairing.G1Point(c[0], c[1]);
uint[] memory inputValues = new uint[](input.length);
for(uint i = 0; i < input.length; i++){
inputValues[i] = input[i];
}
if (verify(inputValues, proof) == 0) {
return true;
} else {
return false;
}
}
...
這是用于驗證零知識證據有效性的輔助函數。verifyProof函數接收4個參數,但是我們隻關心其中表示電路公共輸入的input參數,我們将使用它在智能合約代碼中驗證使用者的身份。讓我們看一下具體的實作代碼:
pragma solidity 0.5.11;
import "./Verifier.sol";
contract ZkIdentity is Verifier {
address public owner;
uint256[2][2] public publicKeys;
constructor() public {
owner = msg.sender;
publicKeys = [
[
11588997684490517626294634429607198421449322964619894214090255452938985192043,
15263799208273363060537485776371352256460743310329028590780329826273136298011
],
[
3554016859368109379302439886604355056694273932204896584100714954675075151666,
17802713187051641282792755605644920157679664448965917618898436110214540390950
]
];
}
function isInGroup(
uint256[2] memory a,
uint256[2][2] memory b,
uint256[2] memory c,
uint256[4] memory input // public inputs
) public view returns (bool) {
if (
input[0] != publicKeys[0][0] &&
input[1] != publicKeys[0][1] &&
input[2] != publicKeys[1][0] &&
input[3] != publicKeys[1][1]
) {
revert("Supplied public keys do not match contracts");
}
return verifyProof(a, b, c, input);
}
}
我們建立一個新的合約ZkIdentity.sol,它繼承自生成的Verifier.sol,有一個包含2個成員公鑰的初始群組,以及一個名為isInGroup的函數,該函數首先驗證電路的公開輸入信号與智能合約中的群組一緻,然後傳回對輸入證據的驗證結果。
邏輯并不複雜,不過的确也滿足了我們的目标:驗證一個使用者屬于特定的群組而無需透露使用者是誰。
在繼續下面的部分之前,需要先部署合約到鍊上。
5、用JavaScript生成零知識證據并與智能合約互動
一旦我們完成了零知識電路并實作了智能合約邏輯,就可以生成證據并調用智能合約的isInGroup方法進行驗證了。
下面的僞代碼展示了如何生成證據并利用智能合約進行驗證,你可以通路
這裡檢視完整的js代碼:
// Assuming below already exists
const provingKey // provingKey.json
const circuit // zero-knowledge circuit we wrote
const zkIdentityContract // Zk-Identity contract instance
const privateKey // Private key that corresponds to one of the public key in the smart contract
const publicKeys = [
[
11588997684490517626294634429607198421449322964619894214090255452938985192043n,
15263799208273363060537485776371352256460743310329028590780329826273136298011n
],
[
3554016859368109379302439886604355056694273932204896584100714954675075151666n,
17802713187051641282792755605644920157679664448965917618898436110214540390950n
]
]
const circuitInputs = {
privateKey,
publicKeys
}
const witness = circuit.calculateWitness(circuitInputs)
const proof = groth16GenProof(witness, provingKey)
const isInGroup = zkIdentityContract.isInGroup(
proof.a,
proof.b,
proof.c,
witness.publicSignals
)
運作js代碼就可以證明你屬于一個群組而無需透露你是誰!
教程的完整代碼
下載下傳位址原文連結:
零知識證明DApp開發實踐 — 彙智網