Mergulho no mundo dos contratos inteligentes com o protocolo de Zero-knowledge
Com base na Wikipedia, a definição de prova ou protocolo de Zero Knowledge Proof (ZKP) é a seguinte:
… prova de zero-knowledge ou protocolo de zero-knowledge é um método pelo qual uma parte (quem prova) pode comprovar para a outra parte (o verificador) que uma determinada declaração é verdadeira, enquanto quem prova evita transmitir qualquer informação adicional além do fato de que a declaração é de fato verdadeira. A essência das provas de zero-knowledge é que é trivial provar que alguém possui conhecimento de certas informações simplesmente revelando-as; o desafio é provar tal posse sem revelar a informação em si ou qualquer informação adicional.
A tecnologia da ZKP pode ser amplamente utilizada em muitos campos diferentes como por exemplo, votação anônima ou transferência de dinheiro anônima que são difíceis de resolver em um banco de dados público como a blockchain.
Tornado Cash é um mixer de moedas que você pode usar para tornar anônimas suas transações na Ethereum. Por causa da lógica da blockchain, cada transação é pública. Se você tem algum ETH em sua conta, não pode transferi-lo anonimamente, pois qualquer pessoa pode acompanhar seu histórico de transações na blockchain. Os mixers de moedas, como o Tornado Cash, podem resolver este problema de privacidade quebrando a ligação on-chain entre o endereço de origem e o endereço de destino, usando o ZKP.
Se você quiser tornar anônima uma de suas transações, terá que depositar uma pequena quantia de ETH (ou token ERC20) no contrato da Tornado Cash (ex.: 1 ETH). Depois de algum tempo, você pode retirar este 1 ETH com uma conta diferente. O truque é que ninguém pode criar uma ligação entre a conta do depositante e a conta de saque. Se centenas de contas depositam 1 ETH de um lado e as outras centenas de contas retiram 1 ETH do outro lado, então ninguém poderá seguir o caminho por onde o dinheiro se move. O desafio técnico é que as transações de contratos inteligentes também são públicas como qualquer outra transação na rede Ethereum. Este é o ponto em que a ZKP será relevante.
Quando você deposita seu 1 ETH no contrato, você tem que assumir um "compromisso". Este compromisso é armazenado pelo contrato inteligente. Quando você retira 1 ETH do outro lado, você tem que fornecer um "nullifier" (anulador) e uma prova de zero-knowledge. O anulador é uma identificação única que está em conexão com o compromisso e o ZKP prova a conexão, mas ninguém sabe qual anulador é atribuído a qual compromisso (exceto o proprietário da conta do depositante/retirada).
Novamente: Podemos provar que um dos compromissos é atribuído ao nosso anulador, sem revelar o nosso compromisso.
Os anuladores são rastreados pelo contrato inteligente, assim podemos retirar apenas um ETH depositado com um anulador.
Parece fácil? Não é! :) Vamos às profundezas da tecnologia. Mas antes de tudo, temos que entender outra coisa complicada, a árvore Merkle.
fonte: https://en.wikipedia.org/wiki/Merkle_tree
Árvores Merkle são árvores de hash, onde as folhas são os elementos e cada nó é um hash dos nós filhos. A raiz da árvore é a raiz Merkle, que representa todo o conjunto de elementos. Se você acrescenta, remove ou muda qualquer elemento (folha) na árvore, a raiz Merkle muda. A raiz Merkle é um identificador único do conjunto de elementos. Mas como podemos utilizá-la?
Existe uma outra coisa chamada prova Merkle. Se eu tenho uma raiz Merkle, você pode me enviar uma prova Merkle que comprove que um elemento está no conjunto que é representado pela raiz. A figura abaixo mostra como isto está funcionando. Se você quiser me provar que HK está no conjunto, você tem que me enviar os hashes HL, HIJ, HMNOP, e HABCDEFGH. Usando esses hashes, posso calcular a raiz Merkle. Se a raiz for a mesma que a minha raiz, então HK está no conjunto. Onde podemos utilizar isso?
Um exemplo simples é o whitelisting. Imagine um contrato inteligente que tem um método que só pode ser chamado pelos usuários da lista branca (whitelist). O problema é que existem 1000 contas listadas na lista branca. Como você pode armazená-las no contrato inteligente? A maneira simples é armazenar cada conta no mapping, mas é muito caro. Uma solução mais barata é construir uma árvore Merkle, e armazenar somente a raiz Merkle (1 hash versus 1000 não é ruim). Se alguém quiser chamar o método, tem que dar uma prova Merkle (neste caso é uma lista de 10 hashes) que pode ser facilmente validada pelo contrato inteligente.
Novamente: Uma árvore Merkle é usada para representar um conjunto de elementos com um hash (a raiz Merkle). A existência de um elemento pode ser comprovada pela prova Merkle.
A próxima coisa que temos que entender é a própria prova zero-knowledge. Com o ZKP, você pode provar que sabe algo sem revelar o que sabe. Para gerar um ZKP, você precisa de um circuito. Um circuito é algo como um pequeno programa que tem inputs*e *outputs públicos, e inputs privados. Estes inputs privados são o conhecimento que você não revela para a verificação, é por isso que é chamado de prova de zero-knowledge (conhecimento zero). Com ZKP, podemos provar que o output pode ser gerado a partir dos inputs com o circuito dado.
Um circuito simples tem este aspecto:
pragma circom 2.0.0;
include "node_modules/circomlib/circuits/bitify.circom";
include "node_modules/circomlib/circuits/pedersen.circom";
template Main() {
signal input nullifier;
signal output nullifierHash;
component nullifierHasher = Pedersen(248);
component nullifierBits = Num2Bits(248);
nullifierBits.in <== nullifier;
for (var i = 0; i < 248; i++) {
nullifierHasher.in[i] <== nullifierBits.out[i];
}
nullifierHash <== nullifierHasher.out[0];
}
component main = Main();
Usando este circuito, podemos provar que conhecemos a fonte do hash dado. Este circuito tem um input (o anulador) e um output (o hash do anulador). A acessibilidade padrão dos inputs é privada, e os outputs*são sempre públicos. Este circuito utiliza 2 bibliotecas do Circomlib. Circomlib é um conjunto de circuitos úteis. A primeira biblioteca é bitlify que contém métodos de manipulação de bits, e a segunda é pedersen que contém o *hasher Pedersen. O hasher Pedersen é um método de hashing que pode ser executado eficientemente em circuitos ZKP. No corpo do template Principal, nós completamos o hasher e calculamos o hash. (Para mais informações sobre a linguagem circom, por favor, veja documentação circom)
Para gerar a prova de conhecimento zero, você precisará de uma chave de prova. Esta é a parte mais sensível do ZKP porque utilizando os dados da fonte que são usados para gerar a chave de prova, qualquer um poderia gerar provas falsas. Estes dados da fonte são chamados de "resíduos tóxicos" que têm que ser descartados. Por causa disso, há uma "cerimônia" para gerar a chave de prova. A cerimônia tem muitos membros e cada membro contribui para a chave de prova. Apenas um membro não malicioso é suficiente para gerar uma chave de prova válida. Usando os inputs privados, os inputs *públicos e a chave de prova, o sistema ZKP pode executar o circuito e gerar a Prova e os outputs.
Há uma chave de validação para a chave de prova que pode ser usada para a validação. O sistema de validação utiliza os inputs públicos, os outputs, e a chave de validação para validar a prova.
Snarkjs é uma ferramenta completa para gerar a chave de prova e a chave de verificação pela cerimônia, gerar a prova e validá-la. Ele também pode gerar um contrato inteligente para a verificação, que pode ser usado por qualquer outro contrato para validar a prova de zero-knowledge. Para mais informações, por favor, veja documentação snarkjs .
Agora, temos tudo para entender como funciona o Tornado Cash (TC). Quando você deposita 1 ETH no contrato do TC, você tem que fornecer um hash de compromisso. Este hash de compromisso será armazenado em uma árvore Merkle. Quando você retira este 1 ETH com uma conta diferente, você tem que fornecer 2 provas de zero-knowledge. A primeira comprova que a árvore Merkel contém seu compromisso. Esta prova é uma prova zero-knowledge de uma prova Merkle. Mas isto não é suficiente, porque você deve ser autorizado a retirar este 1 ETH apenas uma vez. Por causa disso, você tem que fornecer um anulador que é único para o compromisso. O contrato armazena este anulador, isto assegura que você não poderá retirar o dinheiro depositado mais de uma vez.
A unicidade do anulador é assegurada pelo método de geração de compromisso. O compromisso é gerado a partir do anulador e de um segredo pelo hashing. Se você mudar o anulador, então o compromisso mudará, portanto, um anulador pode ser usado para apenas um compromisso. Devido à natureza unidirecional do hashing, não é possível vincular o compromisso e o anulador, mas podemos gerar um ZKP para ele.
Depois da teoria, vamos ver como é o circuito de retirada do TC:
include "../node_modules/circomlib/circuits/bitify.circom";
include "../node_modules/circomlib/circuits/pedersen.circom";
include "merkleTree.circom";
// computa Pedersen(anulador + segredo)
template CommitmentHasher() {
signal input nullifier;
signal input secret;
signal output commitment;
signal output nullifierHash;
component commitmentHasher = Pedersen(496);
component nullifierHasher = Pedersen(248);
component nullifierBits = Num2Bits(248);
component secretBits = Num2Bits(248);
nullifierBits.in <== nullifier;
secretBits.in <== secret;
for (var i = 0; i < 248; i++) {
nullifierHasher.in[i] <== nullifierBits.out[i];
commitmentHasher.in[i] <== nullifierBits.out[i];
commitmentHasher.in[i + 248] <== secretBits.out[i];
}
commitment <== commitmentHasher.out[0];
nullifierHash <== nullifierHasher.out[0];
}
// Verifica que o compromisso que corresponde a determinado segredo e anulador está incluído na árvore merkle dos depósitos
template Withdraw(levels) {
signal input root;
signal input nullifierHash;
signal private input nullifier;
signal private input secret;
signal private input pathElements[levels];
signal private input pathIndices[levels];
component hasher = CommitmentHasher();
hasher.nullifier <== nullifier;
hasher.secret <== secret;
hasher.nullifierHash === nullifierHash;
component tree = MerkleTreeChecker(levels);
tree.leaf <== hasher.commitment;
tree.root <== root;
for (var i = 0; i < levels; i++) {
tree.pathElements[i] <== pathElements[i];
tree.pathIndices[i] <== pathIndices[i];
}
}
component main = Withdraw(20);
O primeiro template é o CommitmentHasher. Ele possui dois inputs, o anulador e o segredo que são dois números aleatórios de 248 bits. O template calcula o hash do anulador e o hash do compromisso que é um hash do anulador e do segredo como eu escrevi anteriormente.
O segundo template é o próprio Withdraw. Ele tem 2 inputs públicos, a raiz Merkle, e o nullifierHash. A raiz Merkle é necessária para verificar a prova Merkle, e o nullifierHash é necessário para que o contrato inteligente possa armazená-lo. Os parâmetros de input privados são o anulador, o segredo, e o pathElements e pathIndices da prova Merkle. O circuito verifica o nullifier, gerando o compromisso a partir dele e do segredo e também verifica a prova Merkle dada. Se tudo estiver bem, será gerada a prova zero-knowledge que pode ser validada pelo contrato inteligente TC.
Pode-se encontrar os contratos inteligentes na pasta de contratos no repositório. O Verifier é gerado a partir do circuito. É usado pelo contrato Tornado para verificar o ZKP para o hash do anulador e a raiz Merkle dados.
A maneira mais fácil de usar o contrato é com a interface de linha de comando. É escrita em JavaScript e o seu código fonte é relativamente simples. Você pode facilmente encontrar o local onde os parâmetros e o ZKP são gerados e usados para chamar o contrato inteligente.
A prova zero-knowledge é relativamente nova no mundo das criptomoedas. A matemática por trás dela é bem complexa e difícil de entender, mas ferramentas como snarkjs
e circom
facilitam o uso. Espero que esse artigo te ajude a entender essa tecnologia “mágica” e que você possa usar o ZKP em seu próximo projeto.
Feliz codificação…
Agradecimentos a Anupam Chugh
Este artigo foi escrito por Laszlo Fazekas, traduzido por Fátima Lima e você pode ler o original aqui.
Latest comments (0)