Privote: ZK Kanıtı Tabanlı Özel Çok Seçenekli Oylama Dapp'i Kodlama

Bu eğitimde, ZK tabanlı özel bir oylama sistemi olan Privote'u uyguluyoruz; bu sistemde oylama sona erene kadar seçmenların tercihleri kimse tarafından görülemez.

8 dk okuma
Privote: ZK Kanıtı Tabanlı Özel Çok Seçenekli Oylama Dapp'i Kodlama

Kayıtlı bir seçmen olarak belirli bir konu veya aday üzerinde oy kullandığınızı hayal edin. Harici bir taraf oyunuzu satmanızı istiyor; başka bir deyişle, bazı teşvikler karşılığında kendi tercihiniz yerine onların tercihine oy vermenizi talep ediyor. Bu talebe boyun eğerseniz, genellikle bir blockchain işlem makbuzu aracılığıyla oyunuzun kanıtını sunmanız gerekecek. Oyunuzu satmaya meyilliyseniz bu kulağa cazip bir fırsat gibi gelebilir; ancak bu durum tüm oy verme sürecinin bütünlüğünü zedeler.

Seçmenlerin tercih ettikleri aday veya konular için oy kullanmasını sağlarken seçimlerini gizleyerek başkalarının görmesini nasıl engelleyebiliriz? Yanıt tek bir kelimede gizli: Şifreleme.

Bu eğitimde, blockchain üzerinde şifrelemeyi kullanan basit bir oylama uygulaması olan Privote'u nasıl geliştireceğimizi inceleyeceğiz. Zamanı geldiğinde oylar şifresi çözülerek sayılır ve sonuçlar seçmenlerin anonimliği korunarak blockchain'e geri yayımlanır. Sonuçlar, sıfır bilgi (ZK) kanıtı aracılığıyla doğrulanır. Mükemmel olmasa da bu örnek, temel kavramları açıkça ortaya koymaktadır.

Blockchain oylamasını derinlemesine anlamak için lütfen Vitalik Buterin'in blog yazısına başvurun.

Devam etmeden önce, Privote dapp'inin ana bileşenlerine ilişkin bir özet sunalım:

  • Akıllı Sözleşme: Bu sistem, birden fazla oyu (oy pusulasını) ve başlıklar, mevcut seçenekler ile şifrelenmiş veriler gibi ilgili bilgileri depolar.

  • Güvenilir Taraf: Oyları şifreleyen, sonuçları hesaplayan ve bunları blockchain'de yayımlayan zincir dışı bir sunucu. Bu sunucuya "Güvenilir Taraf" denmesinin bir nedeni vardır: Yanlış hesaplama yaparak ya da mesajları sansürleyerek hatalı sonuçlar yayımlayamaz.

  • Frontend: Kullanıcıların cüzdanlarını bağladığı ve oy gönderdiği bir platform. "Yönetici"nin kullanılan oy sayısını görüntüleyip sonuçları manuel olarak yayımlayabileceği ayrı bir sayfa da bulunacaktır.

Privote'u başarıyla oluşturmak için iki zorluğun üstesinden gelmemiz gerekiyor:

  • Herhangi bir seçmenin oyunu şifrelemesine olanak tanırken güvenilir tarafın bu oyu çözebilmesini nasıl sağlarız?

  • Güvenilir tarafın güvenilir olduğunu ve hesaplamanın ZK kanıtını sunabildiğini nasıl garanti ederiz; peki tam olarak neyi kanıtlıyoruz?

Zorluk 1: Herhangi bir seçmenin oyunu şifrelemesine olanak tanırken güvenilir tarafın bu oyu çözebilmesini sağlamak

Bunu çözmek için asimetrik şifrelemeyi benimseyebiliriz. Verileri şifrelemek ve çözmek için aynı anahtarı kullanan simetrik şifrelemeden farklı olarak asimetrik şifreleme, biri şifrelemek diğeri çözmek için olmak üzere iki farklı anahtar kullanır. ElGamal, RSA, DSA ve eliptik eğri teknikleri dahil olmak üzere birden fazla asimetrik şifreleme algoritması mevcuttur. Bu eğitimde, eliptik eğri tekniklerini, özellikle de Diffie-Hellman anahtar değişimini(ECDH)](https://www.upguard.com/blog/diffie-hellman)(ECDH)) kullanacağız.

ECDH'de hem güvenilir taraf hem de her seçmen kendi anahtar çiftlerine (bir açık anahtar ve bir özel anahtar) sahiptir. Sıfır bilgi kanıtlarına uygunluğu nedeniyle burada kullanılan Baby Jubjub eğrisi gibi eliptik eğri seçimi, kriptografik işlemlerin güvenliğini ve verimliliğini etkiler. Baby Jubjub eğrisi y^2 = x^3 + 168698 x 2 + x denklemiyle tanımlanır ve anahtar çiftleri bu eğri üzerindeki belirli nokta çiftleri olarak oluşturulur:

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,
  };
};
Kopyala

generateKeyPair fonksiyonunda önce güvenli bir rastgele özel anahtar oluşturuyorum, ardından eğri üzerinde önceden tanımlanmış bir nokta (genellikle taban nokta veya üreteç nokta olarak adlandırılır) üzerinde belirli bir matematiksel işlem (skaler çarpım) gerçekleştirerek karşılık gelen açık anahtarı hesaplıyorum. Kodda bu nokta Base8 olarak adlandırılmıştır. Bu çarpımın sonucu, X ve Y koordinatlarıyla gösterilen eğri üzerindeki başka bir noktadır. Ardından nokta, packPoint fonksiyonuyla kullanım kolaylığı sağlamak amacıyla bigint formatına dönüştürülür.

Şifresi çözülmüş seçimle oy kullanma

Seçmen, kendi özel anahtarını güvenilir tarafın açık anahtarıyla birleştirerek bir şifre oluşturur. Bu şifre, oyu şifrelemek için kullanılır. Akıllı sözleşme daha sonra şifrelenmiş oyu ve seçmenin açık anahtarını birlikte kaydeder.

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;
};
Kopyala

Oyları çözme ve sonuçları hesaplama

Güvenilir taraf, verilerin şifresini çözmek için kendi özel anahtarını ve her seçmenin açık anahtarını kullanarak bir şifre oluşturabilir.

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;
};
Kopyala

generateKeyPair fonksiyonunda, açık anahtarı packPoint fonksiyonuyla kolay kullanım için biçimlendirdik. Yukarıdaki kodda, unpackPoint ile onu orijinal formuna geri dönüştürüyoruz; çünkü özel anahtarı ve seçmenin açık anahtarını birleştirerek paylaşılan bir gizli anahtar oluşturmamız gerekiyor. Bu paylaşılan gizli anahtar, oyun şifresini çözmek için kullanılır.

Her oyun şifresi çözüldükten sonra sonuçları hesaplamak oldukça kolaydır! Oylar dizisinde döngü oluşturarak belirli bir seçenek için oyları biriktirmeniz yeterlidir.

Zorluk 2: Güvenilir tarafın güvenilir olduğunu ve hesaplamanın ZK kanıtını sunabildiğini garanti etmek. Peki tam olarak neyi kanıtlıyoruz?

ZK kanıtlarına aşina değilseniz, bunların oldukça karmaşık ancak bir o kadar da büyüleyici bir teknoloji olduğunu belirtelim. ZK kanıtları, bir tarafın değerin kendisi hakkında herhangi bir bilgi açıklamadan başka bir tarafa belirli bir değeri bildiğini kanıtlayabildiği bir yöntemdir. Özünde, ayrıntıları paylaşmadan bir şeyin doğruluğunu onaylamanın bir yoludur. Üstelik herkes bu kanıtın doğru olup olmadığını doğrulayabilir.

Uygulamamız için ideal olarak güvenilir taraf, şifrelenmiş oyları alıp şifrelerini çözebildiğini ve her seçeneğin aldığı oy sayısını hesaplayabildiğini kanıtlamalıdır. Bu durumda "gizli" anahtar, güvenilir tarafın özel anahtarıdır. Basitlik adına, yalnızca akıllı sözleşmede taahhüt edilen açık anahtarla eşleşen bir özel anahtarın sahipliğini kanıtlıyoruz. Daha önce belirtildiği gibi, oylar şifreyi oluşturmak için bu anahtarı kullanarak oyu şifreler.

Bu amaçla, Ethereum üzerinde zkSNARK'lar için bir araç kutusu olan ZoKrates'ı kullanacağız. ZoKrates, programınızı üst düzey bir dilde belirtmekten hesaplama kanıtları oluşturmaya ve bu kanıtları Solidity'de doğrulamaya kadar dapp'inizde doğrulanabilir hesaplamayı kullanmanıza yardımcı olur.

Kullandığım basit ZK programı şudur:

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;
}
Kopyala

Argüman listesinde görebileceğiniz gibi, pk bir genel argüman, sk ise özel bir argümandır. context de geneldir. Yalnızca kanıtlayıcı (güvenilir taraf) sk'yı bilir; ancak pk, akıllı sözleşmede yer aldığı için herkes tarafından bilinir. context, anahtar çifti oluşturmak için seçtiğimiz eğriyle ilgili parametrelerdir.

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)
};
Kopyala

Yukarıdaki kod parçacığında, bir kanıt oluşturmak için ZoKrates JS SDK'sını kullanıyorum. Akış, ZoKrates belgelerinde belgelenenle benzerdir. Amacımız ZoKrates programını kullanarak bir kanıt oluşturmaktır; bu nedenle computeWitness fonksiyonuna üç argüman iletiyoruz: pk (genel argüman), sk (özel argüman) ve context. Bunlar bir tanık oluşturmak için kullanılır ve bu tanık daha sonra kanıt oluşturmak için kullanılır.

Argümanın şu bölümünü merak ediyor olabilirsiniz:

"8",
    "168700",
    "168696",
    "168698",
    "1",
    "0",
    "1",
  "16540640123574156134436876038791482806971768689494387082833631921987005038935",
    "20819045374670962167435360035096875258406992893633759881276124905556507972311"
Kopyala

context, uygulama genelinde kullandığımız belirli Baby JubJub eğrisiyle ilgili verileri sağlar; zira birçok eğri varyasyonu kullanılabilir. Doğrulama anahtarı, kanıtlama anahtarı ve ZoKrates programı gibi dosyalar ayrı bir dizinde tutulur. Bunları almak için aşağıdaki fonksiyonu kullanıyorum:

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,
  };
};
Kopyala

Aşağıdaki kod parçacığı, sözleşmenin güvenilir tarafın hesapladığı sonuçları kabul ettiği sözleşmeden alınmıştır. Sözleşme, yalnızca doğrulama başarılı olduğunda sonucu kabul eder. Dışarıdan kişiler, pk'yı (sözleşmede yer alan ve herkese açık olan) ve kanıtı doğrulayıcı fonksiyona ileterek bunu doğrulayabilir:

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

     // accept results
Kopyala

Sırada ne var?

Bu blog yazısında, oylama süreci sona erene kadar seçmenlerin tercihlerini gizli tutan ZK tabanlı özel bir oylama sistemi olan Privote'u geliştirdik. Oyları şifrelemek için eliptik eğri tekniklerini kullandık ve bunların güvenli bir şekilde nasıl çözüleceğini ve sayılacağını gösterdik. Ayrıca özel bilgileri açıklamadan oylama sürecinin bütünlüğünü doğrulamak için ZoKrates kullanarak sıfır bilgi kanıtlarını uyguladık.

Bu eğitim, blockchain üzerinde güvenli ve özel oylama sistemleri oluşturmaya ilişkin temel bir anlayış sunmaktadır. İkinci bölümde, frontend'i oluşturacak ve kullanıcıların MetaMask SDK kullanarak nasıl bağlanıp oy kullanabileceğini göstereceğiz.

Yapay zeka tarafından çevrildi. Hatalar içerebilir. Lütfen her zaman bilgileri doğrulayın.

Çeviriyi değerlendir