WEB3DEV

Cover image for Uma introdução à zk-merkle-tree, uma biblioteca JavaScript para votação anônima na Ethereum usando o zkSNARK
Panegali
Panegali

Posted on

Uma introdução à zk-merkle-tree, uma biblioteca JavaScript para votação anônima na Ethereum usando o zkSNARK

O anonimato na votação é um dos requisitos básicos, mas em uma rede pública como uma blockchain, não é trivial implementá-lo. Felizmente, a tecnologia de prova de conhecimento zero (ZKP) torna isso possível, mas, infelizmente, ZKP é uma tecnologia muito complexa. É por isso que construí zk-merkle-tree, uma biblioteca JavaScript que oculta quase todas as complexidades do zkSNARK.

Existem outras bibliotecas como Semaphore, que resolvem esse problema. A vantagem da zk-merkle-tree é que ela é muito simples. Também tenho um projeto de exemplo, onde você pode ver como pode usá-la em seu próprio projeto.

Neste artigo, mostrarei passo a passo como construí esta biblioteca com base no código-fonte do aplicativo misturador de moedas, Tornado Cash, portanto, este artigo não será apenas uma introdução à biblioteca, mas um tutorial de como você pode criar uma aplicação geral baseada no zkSNARK.

Tenho artigos anteriores sobre prova de conhecimento zero e zkSNARK. É altamente recomendável lê-los antes deste artigo, porque este artigo é baseado neles.

A primeira coisa sobre a qual quero falar é o hashing. No mundo Ethereum, estamos usando variações do algoritmo SHA (SHA-256, Keccak-256, etc.) para hashing. Infelizmente, o cálculo da ZKP usando esses algoritmos é caro, então temos que usar hashing compatível com ZK. Existem alguns algoritmos compatíveis com ZK, como MiMC e Pedersen que são usados ​​pelo Tornado Cash (e também zk-merkle-tree), ou Poseidon que é usado pela Semaphore. Esses hashes podem ser calculados de forma barata em circuitos ZK, mas são um pouco mais caros na blockchain do que as variações SHA.

Tornado Cash e zk-merkle-tree estão usando hashes MiMC para construir a árvore de Merkle. Circonlibjs contém uma implementação dela e um gerador de contrato inteligente que você pode usar para implantá-la na blockchain.

const MiMCSponge = new ethers.ContractFactory(
 mimcSpongecontract.abi,
 mimcSpongecontract.createCode(SEED, 220),
 signers[0]
)
mimcsponge = await MiMCSponge.deploy()
Enter fullscreen mode Exit fullscreen mode

O código acima é de zktree_test.ts e usa ethers para implantar o contrato de hash MiMC na blockchain.

O hasher no lado JS pode ser gerado pelo seguinte:

mimc = await buildMimcSponge();
Enter fullscreen mode Exit fullscreen mode

Você pode testar seu contrato por este código:

const res = await mimcsponge["MiMCSponge"](1, 2, 3);
const res2 = mimc.hash(1, 2, 3);
assert.equal(res.xL.toString(), mimc.F.toString(res2.xL));
assert.equal(res.xR.toString(), mimc.F.toString(res2.xR));
Enter fullscreen mode Exit fullscreen mode

A primeira linha calcula o hash usando o contrato inteligente e a segunda linha também calcula pela função JS. A única parte interessante é a conversão de número para string por F.toString. Na zk-SNARK, todo cálculo é feito em um campo finito, então temos que usar mimc.F.toString para a conversão.

Agora podemos calcular hashes MiMC compatíveis com ZK, então tudo está pronto para construir a árvore de Merkle. A implementação do Solidity da árvore de Merkle é totalmente importada do TornadoCash:

// baseado em https://github.com/tornadocash/tornado-core/blob/master/contracts/MerkleTreeWithHistory.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "hardhat/console.sol";

interface IHasher {
   function MiMCSponge(
       uint256 in_xL,
       uint256 in_xR,
       uint256 k
   ) external pure returns (uint256 xL, uint256 xR);
}

contract MerkleTreeWithHistory {
   uint256 public constant FIELD_SIZE =
       21888242871839275222246405745257275088548364400416034343698204186575808495617;
   uint256 public constant ZERO_VALUE =
       21663839004416932945382355908790599225266501822907911457504978515578255421292; // = keccak256("tornado") % FIELD_SIZE
   IHasher public immutable hasher;

   uint32 public levels;

   // as seguintes variáveis são tornadas públicas para facilitar os testes e a depuração e não devem ser acessadas em código normal

   // filledSubtrees e roots poderiam ser bytes32[tamanho], mas o uso de mapeamentos os tornam mais barato porque removem a verificação da faixa de índice em cada interação
   mapping(uint256 => bytes32) public filledSubtrees;
   mapping(uint256 => bytes32) public roots;
   uint32 public constant ROOT_HISTORY_SIZE = 30;
   uint32 public currentRootIndex = 0;
   uint32 public nextIndex = 0;

   constructor(uint32 _levels, IHasher _hasher) {
       require(_levels > 0, "_levels devem ser maiores que zero");
       require(_levels < 32, "_levels deve ser inferior a 32");
       levels = _levels;
       hasher = _hasher;

       for (uint32 i = 0; i < _levels; i++) {
           filledSubtrees[i] = zeros(i);
       }

       roots[0] = zeros(_levels - 1);
   }

   /**
   @dev Hash 2 folhas da árvore, returna MiMC(_left, _right)
   */
   function hashLeftRight(
       uint256 _left,
       uint256 _right
   ) public view returns (bytes32) {
       require(
           _left < FIELD_SIZE,
           "_left deve estar dentro do campo"
       );
       require(
           _right < FIELD_SIZE,
           "_right deve estar dentro do campo"
       );
       uint256 R = _left;
       uint256 C = 0;
       (R, C) = hasher.MiMCSponge(R, C, 0);
       R = addmod(R, _right, FIELD_SIZE);
       (R, C) = hasher.MiMCSponge(R, C, 0);
       return bytes32(R);
   }

   function _insert(bytes32 _leaf) internal returns (uint32 index) {
       uint32 _nextIndex = nextIndex;
       require(
           _nextIndex != uint32(2) ** levels,
           "A árvore de Merkle está cheia. Não é possível acrescentar mais folhas."
       );
       uint32 currentIndex = _nextIndex;
       bytes32 currentLevelHash = _leaf;
       bytes32 left;
       bytes32 right;

       for (uint32 i = 0; i < levels; i++) {
           if (currentIndex % 2 == 0) {
               left = currentLevelHash;
               right = zeros(i);
               filledSubtrees[i] = currentLevelHash;
           } else {
               left = filledSubtrees[i];
               right = currentLevelHash;
           }
           currentLevelHash = hashLeftRight(uint256(left), uint256(right));
           currentIndex /= 2;
       }

       uint32 newRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
       currentRootIndex = newRootIndex;
       roots[newRootIndex] = currentLevelHash;
       nextIndex = _nextIndex + 1;
       return _nextIndex;
   }

   /**
   @dev Se a raiz está presente no histórico da raiz
   */
   function isKnownRoot(bytes32 _root) public view returns (bool) {
       if (_root == 0) {
           return false;
       }
       uint32 _currentRootIndex = currentRootIndex;
       uint32 i = _currentRootIndex;
       do {
           if (_root == roots[i]) {
               return true;
           }
           if (i == 0) {
               i = ROOT_HISTORY_SIZE;
           }
           i--;
       } while (i != _currentRootIndex);
       return false;
   }

   /**
   @dev Retorna a última raiz
   */
   function getLastRoot() public view returns (bytes32) {
       return roots[currentRootIndex];
   }

   /// @dev fornece Zero (Vazio) elementos para um MerkleTree MiMC. Até 32 níveis
   function zeros(uint256 i) public pure returns (bytes32) {
       if (i == 0)
           return
               bytes32(
                   0x2fe54c60d3acabf3343a35b6eba15db4821b340f76e741e2249685ed4899af6c
               );
       else if (i == 1)
           return
               bytes32(
                   0x256a6135777eee2fd26f54b8b7037a25439d5235caee224154186d2b8a52e31d
               );
       else if (i == 2)
           return
               bytes32(
                   0x1151949895e82ab19924de92c40a3d6f7bcb60d92b00504b8199613683f0c200
               );
       else if (i == 3)
           return
               bytes32(
                   0x20121ee811489ff8d61f09fb89e313f14959a0f28bb428a20dba6b0b068b3bdb
               );
       else if (i == 4)
           return
               bytes32(
                   0x0a89ca6ffa14cc462cfedb842c30ed221a50a3d6bf022a6a57dc82ab24c157c9
               );
       else if (i == 5)
           return
               bytes32(
                   0x24ca05c2b5cd42e890d6be94c68d0689f4f21c9cec9c0f13fe41d566dfb54959
               );
       else if (i == 6)
           return
               bytes32(
                   0x1ccb97c932565a92c60156bdba2d08f3bf1377464e025cee765679e604a7315c
               );
       else if (i == 7)
           return
               bytes32(
                   0x19156fbd7d1a8bf5cba8909367de1b624534ebab4f0f79e003bccdd1b182bdb4
               );
       else if (i == 8)
           return
               bytes32(
                   0x261af8c1f0912e465744641409f622d466c3920ac6e5ff37e36604cb11dfff80
               );
       else if (i == 9)
           return
               bytes32(
                   0x0058459724ff6ca5a1652fcbc3e82b93895cf08e975b19beab3f54c217d1c007
               );
       else if (i == 10)
           return
               bytes32(
                   0x1f04ef20dee48d39984d8eabe768a70eafa6310ad20849d4573c3c40c2ad1e30
               );
       else if (i == 11)
           return
               bytes32(
                   0x1bea3dec5dab51567ce7e200a30f7ba6d4276aeaa53e2686f962a46c66d511e5
               );
       else if (i == 12)
           return
               bytes32(
                   0x0ee0f941e2da4b9e31c3ca97a40d8fa9ce68d97c084177071b3cb46cd3372f0f
               );
       else if (i == 13)
           return
               bytes32(
                   0x1ca9503e8935884501bbaf20be14eb4c46b89772c97b96e3b2ebf3a36a948bbd
               );
       else if (i == 14)
           return
               bytes32(
                   0x133a80e30697cd55d8f7d4b0965b7be24057ba5dc3da898ee2187232446cb108
               );
       else if (i == 15)
           return
               bytes32(
                   0x13e6d8fc88839ed76e182c2a779af5b2c0da9dd18c90427a644f7e148a6253b6
               );
       else if (i == 16)
           return
               bytes32(
                   0x1eb16b057a477f4bc8f572ea6bee39561098f78f15bfb3699dcbb7bd8db61854
               );
       else if (i == 17)
           return
               bytes32(
                   0x0da2cb16a1ceaabf1c16b838f7a9e3f2a3a3088d9e0a6debaa748114620696ea
               );
       else if (i == 18)
           return
               bytes32(
                   0x24a3b3d822420b14b5d8cb6c28a574f01e98ea9e940551d2ebd75cee12649f9d
               );
       else if (i == 19)
           return
               bytes32(
                   0x198622acbd783d1b0d9064105b1fc8e4d8889de95c4c519b3f635809fe6afc05
               );
       else if (i == 20)
           return
               bytes32(
                   0x29d7ed391256ccc3ea596c86e933b89ff339d25ea8ddced975ae2fe30b5296d4
               );
       else if (i == 21)
           return
               bytes32(
                   0x19be59f2f0413ce78c0c3703a3a5451b1d7f39629fa33abd11548a76065b2967
               );
       else if (i == 22)
           return
               bytes32(
                   0x1ff3f61797e538b70e619310d33f2a063e7eb59104e112e95738da1254dc3453
               );
       else if (i == 23)
           return
               bytes32(
                   0x10c16ae9959cf8358980d9dd9616e48228737310a10e2b6b731c1a548f036c48
               );
       else if (i == 24)
           return
               bytes32(
                   0x0ba433a63174a90ac20992e75e3095496812b652685b5e1a2eae0b1bf4e8fcd1
               );
       else if (i == 25)
           return
               bytes32(
                   0x019ddb9df2bc98d987d0dfeca9d2b643deafab8f7036562e627c3667266a044c
               );
       else if (i == 26)
           return
               bytes32(
                   0x2d3c88b23175c5a5565db928414c66d1912b11acf974b2e644caaac04739ce99
               );
       else if (i == 27)
           return
               bytes32(
                   0x2eab55f6ae4e66e32c5189eed5c470840863445760f5ed7e7b69b2a62600f354
               );
       else if (i == 28)
           return
               bytes32(
                   0x002df37a2642621802383cf952bf4dd1f32e05433beeb1fd41031fb7eace979d
               );
       else if (i == 29)
           return
               bytes32(
                   0x104aeb41435db66c3e62feccc1d6f5d98d0a0ed75d1374db457cf462e3a1f427
               );
       else if (i == 30)
           return
               bytes32(
                   0x1f3c6fd858e9a7d4b0d1f38e256a09d81d5a5e3c963987e2d4b814cfab7c6ebb
               );
       else if (i == 31)
           return
               bytes32(
                   0x2c7a07d20dff79d01fecedc1134284a8d08436606c93693b67e333f671bf69cc
               );
       else revert("Índice fora dos limites");
   }
}
Enter fullscreen mode Exit fullscreen mode

O contrato tem 2 parâmetros iniciais. O nível da árvore e o endereço do contrato hasher MiMC.

O método privado hashLeftRight calcula o hash MiMC do par de elementos.

O método _insert insere um elemento e calcula a nova raiz de Merkle. A árvore armazena o histórico das últimas 30 raízes porque as transações Ethereum não são em tempo real. É possível que você envie sua prova de Merkle para a blockchain, mas antes de sua transação, alguém insere um novo elemento que altera a raiz de Merkle. Por causa do histórico, sua prova de Merkle ainda será válida, porque a raiz que você usou para sua prova está no histórico. Essa verificação de raiz é feita pelo método isKnownRoot.

Agora temos uma árvore de Merkle compatível com ZK, onde podemos armazenar os compromissos. Vejamos como podemos calcular uma prova de Merkle para nosso compromisso:

// calcula a raiz de Merkle a partir de elementos e um caminho para o elemento dado
export function calculateMerkleRootAndPath(mimc: any, levels: number, elements: any[], element?: any) {
   const capacity = 2 ** levels
   if (elements.length > capacity) throw new Error('A árvore está cheia')

   const zeros = generateZeros(mimc, levels);
   let layers = []
   layers[0] = elements.slice()
   for (let level = 1; level <= levels; level++) {
       layers[level] = []
       for (let i = 0; i < Math.ceil(layers[level - 1].length / 2); i++) {
           layers[level][i] = calculateHash(
               mimc,
               layers[level - 1][i * 2],
               i * 2 + 1 < layers[level - 1].length ? layers[level - 1][i * 2 + 1] : zeros[level - 1],
           )
       }
   }

   const root = layers[levels].length > 0 ? layers[levels][0] : zeros[levels - 1]

   let pathElements = []
   let pathIndices = []

   if (element) {
       const bne = BigNumber.from(element)
       let index = layers[0].findIndex(e => BigNumber.from(e).eq(bne))
       for (let level = 0; level < levels; level++) {
           pathIndices[level] = index % 2
           pathElements[level] = (index ^ 1) < layers[level].length ? layers[level][index ^ 1] : zeros[level]
           index >>= 1
       }
   }

   return {
       root: root.toString(),
       pathElements: pathElements.map((v) => v.toString()),
       pathIndices: pathIndices.map((v) => v.toString())
   }
}
Enter fullscreen mode Exit fullscreen mode

Este método é do zktree.ts e calcula a prova de Merkle para o elemento fornecido. Para o cálculo da raiz de Merkle, precisamos dos elementos adicionados anteriormente e construímos a árvore de Merkle. Podemos ler essas informações da blockchain porque toda operação de inserção emite um evento Commit. O código abaixo coleta esses eventos e calcula a prova de Merkle a partir deles:

export async function calculateMerkleRootAndPathFromEvents(mimc: any, address: any, provider: any, levels: number, element: any) {
   const abi = [
       "event Commit(bytes32 indexed commitment,uint32 leafIndex,uint256 timestamp)"
   ];
   const contract = new Contract(address, abi, provider)
   const events = await contract.queryFilter(contract.filters.Commit())
   let commitments = []
   for (let event of events) {
       commitments.push(BigNumber.from(event.args.commitment))
   }
   return calculateMerkleRootAndPath(mimc, levels, commitments, element)
}
Enter fullscreen mode Exit fullscreen mode

Agora temos uma árvore de Merkle na blockchain, podemos inserir elementos nela e podemos gerar provas de Merkle no lado do cliente, então vamos ver a parte ZK:

// baseado em https://github.com/tornadocash/tornado-core/blob/master/circuits/merkleTree.circom
pragma circom 2.0.0;

include "../node_modules/circomlib/circuits/mimcsponge.circom";

// Calcula MiMC([esquerda, direita])
template HashLeftRight() {
   signal input left;
   signal input right;
   signal output hash;

   component hasher = MiMCSponge(2, 220, 1);
   hasher.ins[0] <== left;
   hasher.ins[1] <== right;
   hasher.k <== 0;
   hash <== hasher.outs[0];
}

// se s == 0 returna [in[0], in[1]]
// se s == 1 returna [in[1], in[0]]
template DualMux() {
   signal input in[2];
   signal input s;
   signal output out[2];

   s * (1 - s) === 0;
   out[0] <== (in[1] - in[0])*s + in[0];
   out[1] <== (in[0] - in[1])*s + in[1];
}

// Verifica se a prova de merkle está correta para determinada raiz de merkle e uma folha
// A entrada pathIndices é uma matriz de seletores 0/1 informando se determinado pathElement está no lado esquerdo ou direito do caminho de merkle
template MerkleTreeChecker(levels) {
   signal input leaf;
   signal input pathElements[levels];
   signal input pathIndices[levels];
   signal output root;

   component selectors[levels];
   component hashers[levels];

   for (var i = 0; i < levels; i++) {
       selectors[i] = DualMux();
       selectors[i].in[0] <== i == 0 ? leaf : hashers[i - 1].hash;
       selectors[i].in[1] <== pathElements[i];
       selectors[i].s <== pathIndices[i];

       hashers[i] = HashLeftRight();
       hashers[i].left <== selectors[i].out[0];
       hashers[i].right <== selectors[i].out[1];
   }

   root <== hashers[levels - 1].hash;
}
Enter fullscreen mode Exit fullscreen mode

O circuito do validador de prova de Merkle também é copiado da fonte do Tornado Cash, como o próprio contrato da árvore de Merkle. O circuito faz iterações nos elementos de caminho e calcula a raiz de Merkle. Quando o eleitor vota, ele não precisa enviar a prova de Merkle para o contrato inteligente, apenas a raiz de Merkle, e a prova ZK de que ela pode calcular com sucesso a raiz usando este circuito e a prova de Merkle que é conhecida apenas por ela (esta é a parte privada do ZKP).

A última etapa é o método de geração de compromisso/anulador. Para garantir que um eleitor vote apenas uma vez, ele precisa enviar um anulador para a blockchain, que é registrado pelo contrato inteligente e pode ser usado apenas uma vez. O anulador deve ser exclusivo e atribuído ao compromisso. Assim, um compromisso tem apenas um anulador, mas por causa do ZKP, ninguém sabe (apenas o eleitor) qual anulador está atribuído a qual compromisso. O anulador é gerado pelo MiMC em zk-merkle-tree:

export async function generateCommitment() {
   const mimc = await buildMimcSponge();
   const nullifier = BigNumber.from(crypto.randomBytes(31)).toString()
   const secret = BigNumber.from(crypto.randomBytes(31)).toString()
   const commitment = mimc.F.toString(mimc.multiHash([nullifier, secret]))
   const nullifierHash = mimc.F.toString(mimc.multiHash([nullifier]))
   return {
       nullifier: nullifier,
       secret: secret,
       commitment: commitment,
       nullifierHash: nullifierHash
   }
}
Enter fullscreen mode Exit fullscreen mode

Como você pode ver, o compromisso é um hash do anulador e um segredo. A parte pública é o hash do anulador e o hash de compromisso, o segredo é privado. É fácil ver que, sem o segredo, ninguém pode vincular o compromisso ao anulador.

Vamos ver todo o circuito do validador:

pragma circom 2.0.0;

include "CommitmentHasher.circom";
include "MerkleTreeChecker.circom";

template Verifier(levels) {
   signal input nullifier;
   signal input secret;
   signal input pathElements[levels];
   signal input pathIndices[levels];
   signal output nullifierHash;
   signal output root;

   component commitmentHasher = CommitmentHasher();
   component merkleTreeChecker = MerkleTreeChecker(levels);

   commitmentHasher.nullifier <== nullifier;
   commitmentHasher.secret <== secret;

   merkleTreeChecker.leaf <== commitmentHasher.commitment;
   for (var i = 0; i < levels; i++) {
       merkleTreeChecker.pathElements[i] <== pathElements[i];
       merkleTreeChecker.pathIndices[i] <== pathIndices[i];
   }

   nullifierHash <== commitmentHasher.nullifierHash;
   root <== merkleTreeChecker.root;
}

component main = Verifier(20);
Enter fullscreen mode Exit fullscreen mode

O circuito é relativamente simples. O CommitmentHasher valida o compromisso e os hashes do anulador usando o anulador e o segredo como parâmetros privados. Se o compromisso for válido, ele valida a prova de Merkle fornecida na próxima etapa.

Agora temos tudo o que precisamos, então vamos juntar as coisas:

export async function calculateMerkleRootAndZKProof(address: any, provider: any, levels: number, commitment: any, zkey: any) {
   const mimc = await buildMimcSponge();
   const rootAndPath = await calculateMerkleRootAndPathFromEvents(mimc, address, provider, levels, commitment.commitment);
   const { proof, publicSignals } = await snarkjs.groth16.fullProve(
       {
           nullifier: commitment.nullifier, secret: commitment.secret,
           pathElements: rootAndPath.pathElements, pathIndices: rootAndPath.pathIndices
       },
       getVerifierWASM(),
       zkey);
   const cd = convertCallData(await snarkjs.groth16.exportSolidityCallData(proof, publicSignals));
   return {
       nullifierHash: publicSignals[0],
       root: publicSignals[1],
       proof_a: cd.a,
       proof_b: cd.b,
       proof_c: cd.c
   }
}
Enter fullscreen mode Exit fullscreen mode

Depois que o eleitor gera o compromisso, ele o envia para o contrato inteligente que o armazena na árvore de Merkle. Na fase de votação, ela usa o método calculateMerkleRootAndZKProof que calcula a prova de Merkle a partir dos eventos Commit que são emitidos pelo contrato inteligente. Este método calcula a prova de conhecimento zero e a converte na forma correta para o contrato inteligente do validador.

O contrato inteligente Validator é gerado a partir do circuito Validator pelo snarkjs. Pode ser feito pelo seguinte comando:

npx snarkjs zkey export solidityverifier build/Verifier.zkey contracts/Verifier.sol
Enter fullscreen mode Exit fullscreen mode

Para obter mais informações, verifique o script prepare.sh e meu artigo anterior sobre o assunto.

O contrato ZKTree completo é assim:

// baseado em https://github.com/tornadocash/tornado-core/blob/master/contracts/Tornado.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "./MerkleTreeWithHistory.sol";

interface IVerifier {
   function verifyProof(
       uint[2] memory a,
       uint[2][2] memory b,
       uint[2] memory c,
       uint[2] memory input
   ) external pure returns (bool r);
}

contract ZKTree is MerkleTreeWithHistory {
   mapping(bytes32 => bool) public nullifiers;
   mapping(bytes32 => bool) public commitments;

   IVerifier public immutable verifier;

   event Commit(
       bytes32 indexed commitment,
       uint32 leafIndex,
       uint256 timestamp
   );

   constructor(
       uint32 _levels,
       IHasher _hasher,
       IVerifier _verifier
   ) MerkleTreeWithHistory(_levels, _hasher) {
       verifier = _verifier;
   }

   function _commit(bytes32 _commitment) internal {
       require(!commitments[_commitment], "O compromisso foi submetido");

       commitments[_commitment] = true;
       uint32 insertedIndex = _insert(_commitment);
       emit Commit(_commitment, insertedIndex, block.timestamp);
   }

   function _nullify(
       bytes32 _nullifier,
       bytes32 _root,
       uint[2] memory _proof_a,
       uint[2][2] memory _proof_b,
       uint[2] memory _proof_c
   ) internal {
       require(!nullifiers[_nullifier], "O anulador foi submetido");
       require(isKnownRoot(_root), "Não consegue encontrar sua raiz de merkle");
       require(
           verifier.verifyProof(
               _proof_a,
               _proof_b,
               _proof_c,
               [uint256(_nullifier), uint256(_root)]
           ),
           "Prova inválida"
       );

       nullifiers[_nullifier] = true;       
   }
}
Enter fullscreen mode Exit fullscreen mode

O contrato é herdado do contrato MerkleTree e possui 3 parâmetros iniciais. O nível e o hasher para a árvore de Merkle e o endereço do contrato Verifier que é gerado pelo snarkjs do circuito Verifier.

O método _commit insere o compromisso na árvore de Merkle e emite o evento Commit para o cálculo da raiz de Merkle.

O método _nullify verifica se o anulador não é usado (um eleitor pode votar apenas uma vez), verifica a raiz pelo método isKnownRoot e verifica a prova de conhecimento zero.

Os métodos _commit e _nullify são abstratos, então você não pode usar o contrato ZKTree diretamente, você tem que herdar seu próprio contrato dele. Este contrato de nível superior será responsável pela validação do eleitor e registro dos votos.

Você pode encontrar uma implementação mínima no projeto zktree-vote (tenho um artigo completo sobre isso).

Em resumo, a implementação e o processo de votação são os seguintes:

  • Você tem que implementar o contrato ZKTree e implantá-lo. Seu contrato será responsável pelo registro de eleitor e voto.
  • O eleitor gera um compromisso pelo método generateCommitment e envia o compromisso para o contrato inteligente na fase de registro.
  • Na fase de votação, o eleitor gera a prova de zero conhecimento pelo método calculateMerkleRootAndZKProof e a envia com o anulador para o contrato inteligente, que a verifica e registra o voto.

Isso é tudo. Espero que este artigo ajude você a entender como as provas de conhecimento zero podem ser usadas com JavaScript e Solidity (não apenas para votação).

Para obter mais informações, verifique o repositório zk-merkle-tree e o repositório zktree-vote, que é um exemplo de implementação da biblioteca.


Artigo escrito por Laszlo Fazekas e traduzido por Marcelo Panegali.

Top comments (0)