跳至內容

Privote:基於 ZK 證明的私密多選投票 Dapp 開發

在本教學中,我們實作一個基於 ZK 的私密投票系統 Privote,在投票結束之前,任何人都無法看到投票者的選擇。

閱讀時間 8 分鐘
Privote:基於 ZK 證明的私密多選投票 Dapp 開發

想像您是一位已登記的選民,正在針對特定議題或候選人進行投票。某個外部人士想要購買您的選票,換句話說,他們希望您以換取某些利益為條件,投票給他們屬意的選項,而非您自己的選擇。如果您屈服於他們的要求,他們將需要您的投票證明——通常是透過區塊鏈交易收據。雖然對於有意出售選票的人而言,這看似是個絕佳機會,但這將嚴重損害整個投票流程的公正性。

我們要如何讓選民能夠自由投票給自己屬意的候選人或議題,同時隱藏其選擇,使任何人都無法得知?答案就在一個詞:加密(Encryption)。

在本教學中,我們將探討如何開發一個簡單的投票應用程式——Privote——它在區塊鏈上運用加密技術。當時機到來時,選票將被解密並計票,結果在維護選民匿名性的前提下發布回區塊鏈。結果透過零知識(ZK)證明進行驗證。雖然並非完美,但此範例清楚闡明了其背後的核心概念。

如需深入了解區塊鏈投票,請參閱 Vitalik Buterin 的部落格文章。

在繼續之前,以下是 Privote dapp 主要組成部分的說明:

  • 智能合約:此系統儲存多筆投票(選票)及相關資料,例如標題、可選選項和加密資料。

  • 受信任方:一個鏈下伺服器,負責解密選票、計算結果並將其發布至區塊鏈。此伺服器被稱為「受信任方」是有原因的:它無法發布不正確的結果,無論是因計算錯誤或審查訊息所致。

  • 前端:一個供使用者連接錢包並提交選票的平台。此外,還會有一個獨立頁面供「管理員」查看已投票數並手動公布結果。

為了成功建構 Privote,我們需要解決兩個挑戰:

  • 如何讓任何選民都能加密其選票,同時允許受信任方進行解密?

  • 如何確保受信任方是可靠的,並能提供計算過程的 ZK 證明——但我們究竟在證明什麼?

挑戰一:讓任何選民都能加密其選票,同時允許受信任方進行解密

為了解決這個問題,我們可以採用非對稱加密。與使用相同金鑰進行加密和解密的對稱加密不同,非對稱加密使用兩把不同的金鑰,一把用於加密,另一把用於解密。目前存在多種非對稱加密演算法,包括 ElGamal、RSA、DSA 以及橢圓曲線技術。在本教學中,我們將使用橢圓曲線技術,具體而言是 Diffie-Hellman 金鑰交換(ECDH)](https://www.upguard.com/blog/diffie-hellman)(ECDH))。

在 ECDH 中,受信任方和每位選民都擁有各自的金鑰對(一把公鑰和一把私鑰)。橢圓曲線的選擇,例如此處因其適用於零知識證明而採用的 Baby Jubjub 曲線,會影響密碼學運算的安全性與效率;Baby Jubjub 曲線由方程式 y^2 = x^3 + 168698 x 2 + x 定義,金鑰對則以此曲線上特定的點對形式生成:

import {
  packPoint,
  unpackPoint,
  mulPointEscalar,
  Point,
} from "@zk-kit/baby-jubjub";
import crypto from "crypto";

// generates babyjubjub key pair that is compatible with our zokrates implementation

export const generateKeyPair = () => {
// X and Y coordinates of the base point 
  const Base8: Point<bigint> = [
    16540640123574156134436876038791482806971768689494387082833631921987005038935n,
    20819045374670962167435360035096875258406992893633759881276124905556507972311n,
  ];

  const privateKey = BigInt(`0x${crypto.randomBytes(32).toString("hex")}`);

  const publicKey = packPoint(mulPointEscalar(Base8, privateKey));

  return {
    privateKey,
    publicKey,
  };
};
複製

在 generateKeyPair 函式中,我首先建立一個安全的隨機私鑰,然後透過對曲線上預定義點(通常稱為基點或生成點)執行特定的數學運算(純量乘法)來計算對應的公鑰。在程式碼中,此點被命名為 Base8。此乘法的結果是曲線上的另一個點,以其 X 和 Y 座標表示。隨後,該點透過 packPoint 函式被格式化為 bigint,以便於使用。

以加密選擇進行投票

選民透過將其私鑰與受信任方的公鑰結合來建立密文(cipher)。此密文用於加密選票。智能合約隨後將加密後的選票與選民的公鑰一併儲存。

export const encryptVote = async (vote: string) => {
  const { publicKey: trustedPartypublicKey } = config;
  const { privateKey, publicKey } = generateKeyPair();
  const encryptedVote = await encryptData(
    vote,
    privateKey,
    publicKey,
    BigInt(trustedPartypublicKey)
  );

  return {
    encryptedVote,
    publicKey: publicKey.toString(),
  };
};

export const encryptData = (
  data: string,
  privateKey: bigint,
  voterPublicKey: bigint,
  trustedPartyKey: bigint
) => {
  // Compute the shared secret by multiplying the trusted party's public key by the private key.
  const unpackedKey = unpackPoint(trustedPartyKey);
  if (!unpackedKey) {
    throw new Error("Invalid key");
  }
  const sharedSecretPoint = mulPointEscalar(unpackedKey, privateKey);

  let sharedSecret = sharedSecretPoint[0].toString(16);

  // Pad the shared secret with zeros to make it 32 bytes long.
  while (sharedSecret.length < 64) {
    sharedSecret = "0" + sharedSecret;
  }

  const cipher = crypto.createCipheriv(
    "aes-256-ctr",
    Buffer.from(sharedSecret, "hex"), // Use the first 32 bytes of the shared secret as the key.
    Buffer.alloc(16, 0) // Use a zero-filled buffer as the IV.
  );
  let encrypted = cipher.update(data, "utf8", "hex");
  encrypted += cipher.final("hex");

  return encrypted;
};
複製

解密選票並計算結果

受信任方可以使用其私鑰和每位選民的公鑰建立密文,以解密資料。

export const publishResults = async (id: number) => {
	.....
	.....

  const decryptedVotes = votes.map((vote) => {
    const { encryptedVote, voter } = vote;
    return decryptVote(encryptedVote, voter);
  });

  const results = calculateResult(decryptedVotes);
// next send the calculate results to the blockchain with the zk proof 
	 ......
	 ......
};

export const decryptVote = (encryptedVote: string, voterPublicKey: string) => {
  const { privateKey } = config;
  const decryptedVote = decryptData(
    encryptedVote,
    BigInt(privateKey),
    BigInt(voterPublicKey)
  );

  return decryptedVote;
};

export const decryptData = (
  encryptedData: string,
  privateKey: bigint,
  trustedPartyPublicKey: bigint
) => {
  const unpackedKey = unpackPoint(trustedPartyPublicKey);
  if (!unpackedKey) {
    throw new Error("Invalid key");
  }

  const sharedSecretPoint = mulPointEscalar(unpackedKey, privateKey);

  let sharedSecret = sharedSecretPoint[0].toString(16);

  // Pad the shared secret with zeros to make it 32 bytes long.
  while (sharedSecret.length < 64) {
    sharedSecret = "0" + sharedSecret;
  }

  const decipher = crypto.createDecipheriv(
    "aes-256-ctr",
    Buffer.from(sharedSecret, "hex"), // Use the first 32 bytes of the shared secret as the key.
    Buffer.alloc(16, 0) // Use a zero-filled buffer as the IV.
  );
  let decrypted = decipher.update(encryptedData, "hex", "utf8");
  decrypted += decipher.final("utf8");
  return decrypted;
};
複製

在 generateKeyPair 函式中,我們使用 packPoint 函式將公鑰格式化以便於使用。在上述程式碼中,我們透過 unpackPoint 將其轉換回原始形式,因為我們需要透過結合私鑰和選民的公鑰來建立共享密鑰(shared secret)。此共享密鑰用於解密選票。

解密每張選票後,計算結果便輕而易舉!只需遍歷選票陣列,並累計特定選項的票數即可。

挑戰二:確保受信任方是可靠的,並能提供計算過程的 ZK 證明。但我們究竟在證明什麼?

如果您對 ZK 證明不熟悉,它是一項相當複雜卻令人著迷的技術。ZK 證明是一種方法,讓一方能夠向另一方證明自己知道某個值,而無需透露關於該值本身的任何資訊。本質上,這是一種在不分享細節的情況下驗證某事真實性的方式。此外,任何人都可以驗證該證明是否正確。

對於我們的應用程式,理想情況下,受信任方應證明他們能夠取得加密選票、解密它們,並計算每個選項獲得的票數。在這種情況下,「秘密」金鑰就是受信任方的私鑰。為了簡化起見,我們僅證明對私鑰的所有權,且該私鑰與智能合約中提交的公鑰相匹配。如前所述,選票使用該金鑰生成密文以加密選票。

為此,我們將使用 ZoKrates,這是一個用於以太坊 zkSNARKs 的工具箱。它幫助您在 dapp 中運用可驗證計算,從以高階語言指定您的程式,到生成計算證明,再到在 Solidity 中驗證這些證明。

以下是我使用的簡單 ZK 程式:

import "ecc/proofOfOwnership" as proofOfOwnership;
from "ecc/babyjubjubParams" import BabyJubJubParams;

def main(field[2] pk, private field sk, BabyJubJubParams context) -> bool {
  bool resp = proofOfOwnership(pk, sk,context);
  return resp;
}
複製

如您在參數列表中所見,pk 是公開參數,sk 是私有參數,context 也是公開的。只有證明者——即受信任方——知道 sk,但 pk 對任何人都是公開的,因為它存在於智能合約中。context 是與我們選擇用於生成金鑰對的曲線相關的參數。

import { initialize } from "zokrates-js";

export const generateProof = async (
  privateKey: string,
  publicKey: string[]
) => {

  const provider = await initialize();

  const artifacts = await getZKP();

  const program = Uint8Array.from(Buffer.from(artifacts.program, "hex"));

  const { witness, output } = provider.computeWitness(program, [
    ...publicKey.flat(),
    privateKey,
    "8",
    "168700",
    "168696",
    "168698",
    "1",
    "0",
    "1",
  "16540640123574156134436876038791482806971768689494387082833631921987005038935",
    "20819045374670962167435360035096875258406992893633759881276124905556507972311",
  ]);

  const provingKey = Uint8Array.from(
    Buffer.from(artifacts.provingKey, "hex")
  );

  const proof = provider.generateProof(
    program,
    witness,
    provingKey
  );

  return provider.utils.formatProof(proof)
};
複製

在上述程式碼片段中,我使用 ZoKrates JS SDK 來生成證明。其流程與 ZoKrates 文件中所記載的相似。我們的目標是使用 ZoKrates 程式生成證明,因此我們向 computeWitness 函式傳遞三個參數:pk(公開參數)、sk(私有參數)和 context。這些參數用於建立見證(witness),然後再用於生成證明。

您可能對以下這部分參數感到疑惑:

"8",
    "168700",
    "168696",
    "168698",
    "1",
    "0",
    "1",
  "16540640123574156134436876038791482806971768689494387082833631921987005038935",
    "20819045374670962167435360035096875258406992893633759881276124905556507972311"
複製

context 提供了與我們在整個應用程式中使用的特定 Baby JubJub 曲線相關的資料,因為可以使用許多不同的曲線變體。驗證金鑰、證明金鑰和 ZoKrates 程式等檔案存放在獨立的目錄中。我使用以下函式來取得這些檔案:

const getZKP = async () => {
  const zkPath = path.join(process.cwd(), "src", "services", "zkp");
  const program = (await readFile(path.join(zkPath, "./out"))).toString("hex");
  const verificationKey = JSON.parse(
    (await readFile(path.join(zkPath, "./verification.key"))).toString()
  );
  const provingKey = (
    await readFile(path.join(zkPath, "./proving.key"))
  ).toString("hex");

  return {
    program,
    provingKey,
    verificationKey,
  };
};
複製

以下程式碼片段來自合約,其中合約接受受信任方計算的結果。只有在驗證成功的情況下,合約才會接受該結果。外部人員可以透過將 pk(公開且存在於合約中)和證明傳遞給驗證函式來進行驗證:

        if (!isVerified) {
            revert("Invalid proof");
        }

     // accept results
複製

下一步是什麼?

在本篇文章中,我們開發了 Privote,一個基於 ZK 的私密投票系統,在投票期間結束前保持選民選擇的機密性。我們使用橢圓曲線技術加密選票,並示範了如何安全地解密和計票。我們也使用 ZoKrates 實作了零知識證明,以在不揭露私密資訊的情況下驗證投票流程的完整性。

本教學提供了在區塊鏈上建立安全且私密的投票系統的基礎概念。在第二部分中,我們將建構前端,並向您展示使用者如何透過 MetaMask SDK 連接並投票。

AI 翻譯。可能包含錯誤。請務必核實資訊。

評價此翻譯

本文作者: