DEV Community

Ahmed Castro
Ahmed Castro

Posted on • Updated on

Votos anónimos usando ZK [ZK ES Semana 4]

En esta guía, crearemos un sistema de votación on-chain seguro y anónimo utilizando el lenguaje Noir de Aztec. Generaremos un merkle tree off-chain que contendrá los votantes autorizados. Votaremos en un contrato inteligente de Solidity que solo permitirá votos si verifica que el voto fué emitido por un miembro del merkle tree.

Antes de comenzar

Para este tutorial necesitarás Metamask o cualquier otra billetera evm con fondos en Sepolia que puedes conseguir desde el faucet. También necesitarás nargo v0.6.0. Que puedes encontrar las instrucciones de cómo instalarlo desde el sitio web de Noir.

Paso 1: Hashea las hojas del merkle tree

Cada usuario será representado por una hoja en el merkle Tree. Pero el contenido de este árbol será público y es importante proteger las llaves privadas de los usuarios. Para lograr la privacidad y seguridad, cada usuario transformará sus llaves privadas en llaves públicas mediante una función hash, de manera que el merkle tree pueda ser almacenado on-chain sin exponer información sensible.

Iniciemos creando un nuevo proyecto:

nargo new zk_voting
cd zk_voting
Enter fullscreen mode Exit fullscreen mode

Ahora cambiamos el circuito default por nuestro generador de hojas.

src/main.nr

use dep::std;

fn main(priv_key : Field, secret : Field) {
    let note_commitment = std::hash::pedersen([priv_key, secret]);

    std::println("Public leaf:");
    std::println(note_commitment);
}
Enter fullscreen mode Exit fullscreen mode

Para verificar que todo está en orden y para crear un par de archivos extra ejecutamos el siguiente comando.

nargo check
Enter fullscreen mode Exit fullscreen mode

Ejecutaremos este circuito tres veces, una vez por cada clave privada de los usuarios. Utilizaremos 1, 2 y 3 como claves privadas, y 9 como un valor secreto. Este valor secreto ayudará a hacer las pruebas más seguras.

Para ejecutar el cicuito, estableceremos los parámetros en el archivo Prover.toml y luego ejecutaremos el comando prove utilizando --show-output para habilitar la visualización de los logs de consola:

Prover.toml

priv_key = "1"
secret = "9"
Enter fullscreen mode Exit fullscreen mode
nargo prove p --show-output
Enter fullscreen mode Exit fullscreen mode

Después de ejecutar el circuito tres veces (una vez para cada llave privada), obtendrás los nodos hoja resultantes del merkle tree que han sido hasheados.

0x1053e87bed5f2a4f5144b386c5403701212f4b3c21cb14683b6ebdfed16c854d
0x1eff4379fbc748b53a07ce5961c24eccdfeb8f17154490d2e41c6096a9519329
0x0e0adf2416341b3d868d88043127be8e6965f3758ce6a80d11a494fe21b0cdff
Enter fullscreen mode Exit fullscreen mode

Step 2: Construye el merkle tree

Una vez que tengamos los nodos hoja, generaremos el árbol de merkle, rama por rama, hasta llegar a la raíz. Por ejemplo, se realiza un hash entre la llave pública del usuario 1 y el usuario 2, luego se realiza un hash de esa rama con el usuario 3 para obtener la raíz. Este proceso se repite hasta construir todo el árbol.

src/main.nr

use dep::std;

fn main(index : Field, left_leaf : Field, right_path : [Field; 1]) {
    let root = std::merkle::compute_root_from_leaf(left_leaf, index, right_path);
    std::println(root);
}
Enter fullscreen mode Exit fullscreen mode

Por ejemplo para construir la rama intermedia entre el votador 1 y 2 usamos los siguientes parametros.

index = "0"
left_leaf = "0x1053e87bed5f2a4f5144b386c5403701212f4b3c21cb14683b6ebdfed16c854d"
right_path = ["0x1eff4379fbc748b53a07ce5961c24eccdfeb8f17154490d2e41c6096a9519329"]

Enter fullscreen mode Exit fullscreen mode

Una vez que hayas ejecutado el circuito anterior para cada rama y la raíz final, el árbol de merkle resultante será el siguiente:

└─ 0x2959dc1151fec22361702924ec0edcc117824d77c275044462e8a91fd1d707a5
   ├─ 0x0f76fbce0c43859cb1aaceedf9948995a1671979f522cb49886a9180a93cc3e1
   │  ├─ 0x1053e87bed5f2a4f5144b386c5403701212f4b3c21cb14683b6ebdfed16c854d
   │  └─ 0x1eff4379fbc748b53a07ce5961c24eccdfeb8f17154490d2e41c6096a9519329
   └─ 0x0e0adf2416341b3d868d88043127be8e6965f3758ce6a80d11a494fe21b0cdff
      └─ 0x0e0adf2416341b3d868d88043127be8e6965f3758ce6a80d11a494fe21b0cdff
Enter fullscreen mode Exit fullscreen mode

Paso 3: Verifica un voto

Para generar un voto, deberás proporcionar tanto la prueba de membrecía al merkle tree como las variables de votación que se pasarás como parámetro al contrato inteligente.

src/main.nr

use dep::std;

fn main(root : pub Field,
  index : Field,
  hash_path : [Field; 2],
  secret: Field, priv_key: Field,
  proposalId: pub Field,
  vote: pub Field) -> pub Field
{
    let note_commitment = std::hash::pedersen([priv_key, secret]);
    let nullifier = std::hash::pedersen([root, priv_key, proposalId]);

    let is_member = std::merkle::check_membership(root, note_commitment[0], index, hash_path);
    assert(is_member == 1);

    nullifier[0]
}
Enter fullscreen mode Exit fullscreen mode

Antes de generar la prueba, generamos el smart contract verificador corriendo lo siguiente.

nargo codegen-verifier
Enter fullscreen mode Exit fullscreen mode

Ahora lanzamos el contrato con nombre UltraVerifier.

Luego importamos el smart contract verificador en el siguiente contrato verificador de votos y lo lanzamos pasando por parámetro el merkle root y el address del contrato verificador que recién lanzamos.

//SPDX-License-Identifier: MIT
pragma solidity >=0.8.19;

contract zkVoting {

    struct Proposal {
        string description;
        uint deadline;
        uint forVotes;
        uint againstVotes;
    }

    UltraVerifier ultraVerifier;

    bytes32 merkleRoot;
    uint proposalCount;
    mapping (uint proposalId => Proposal) public proposals;
    mapping (bytes32 nullifier => bool isNullified) public nullifiers;

    constructor(bytes32 _merkleRoot, address ultraVerifierAddress) {
        merkleRoot = _merkleRoot;
        ultraVerifier = UltraVerifier(ultraVerifierAddress);
    }

    function propose(string memory description, uint deadline) public {
        proposals[proposalCount] = Proposal(description, deadline, 0, 0);
        proposalCount += 1;
    }

    function castVote(bytes calldata _proof, bytes32[] calldata _publicInputs) public {
        require(ultraVerifier.verify(_proof, _publicInputs) ==  true, "Invalid proof");
        bytes32 merkleRootPublicInput = _publicInputs[0];
        uint proposalId = uint(_publicInputs[1]);
        uint vote = uint(_publicInputs[2]);
        bytes32 nullifier = _publicInputs[3];

        require(block.timestamp < proposals[proposalId].deadline, "Voting period is over");
        require(merkleRoot == merkleRootPublicInput, "Invalid merke root");
        require(!nullifiers[nullifier], "Vote already casted");

        nullifiers[nullifier] = true;

        if(vote == 1)
            proposals[proposalId].forVotes += 1;
        else if (vote == 2)
            proposals[proposalId].againstVotes += 1;
    }
}
Enter fullscreen mode Exit fullscreen mode

Puedes crear una nueva propuesta on-chain ejecutando la función propose pasando por parámetro el nombre de la propuesta y el deadline que representa el timestamp máximo para emitir votos. Esto creará propuesta con id 0.

Ahora podemos votar siempre y cuando se tengamos las llaves secretas 1, 2 o 3.

Por ejemplo podríamos generar un voto con los siguientes parámetros en el archivo Prover.toml. Donde votamos a favor de la primera propuesta, o sea la propuesta con id 0. En el smart contract definimos que 1 representa un voto a favor y 0 en contra.

Prover.toml

hash_path = ["0x1eff4379fbc748b53a07ce5961c24eccdfeb8f17154490d2e41c6096a9519329", "0x0e0adf2416341b3d868d88043127be8e6965f3758ce6a80d11a494fe21b0cdff"]
index = "0"
priv_key = "1"
proposalId = "0"
root = "0x2959dc1151fec22361702924ec0edcc117824d77c275044462e8a91fd1d707a5"
secret = "9"
vote = "1"
Enter fullscreen mode Exit fullscreen mode

Ejecutamos el comando siguiente para generar una prueba.

nargo prove p
Enter fullscreen mode Exit fullscreen mode

Luego pasar los siguientes parámetros en el smart contract para enviar un voto on-chain.

Proof que la puedes encontrar en el archivo proofs/p.proof:

0x288eaef700174c5422d28af8bcfacf026a5006bda0581bd6b4c82b2744a12c8d0a8db1b1f8417ed8887ab6b3d0bc0ace81db7d0ff974c54b8af13456534ea566291e719e9c1f2c4d907e21ebd0b79fb5b68ddbeab6fa111707aaf9db0f9b3e9416ddf9e599cee1249e4f7e348bea27dde30e2c2264c1fea8668343d4d09e57371c7216da157adffe541adbe3bc5bb68306ca2ebee0067e818244a5a2d816b86a28136ff7765fc47abf876bad9e105e9d58df3f1371d2f86091478ebb870c24b907236843df6bce1cc1bfaa139ab1fefc1270a6c905985be506eeb8e1708a2b580cba2d24a1f43daad1f44e48eb77ef6d6829e2f9289240bb51404dfd221ad49723f154de83eca3b3626ffae700f95a823315159cfd028a6a870fbe4284d351f813a0494c43928fe87514e90413a0e2d67e6a3404297834910f0df15de1e0566724e0bc05d055a73751992a9195aba4b1957d201b97d59b4a62e515f25ce7627f2b81ac780dc2208da1975b8030b605cf7b3ec2d9069cde4e49ee15c9e059cd2b2f42962fbadedd7b6c0e0ebb643497834ae93837e3b2a253faa4146d3c229b6c305fbc31667a0dc5e27dae91c3b9fed66da4f173c4a622d50a1c9984899f1b5002374df34f2b26e8378cdbd4ba9f3c8f55881181c4dce221b8e55019e4a9e16f1ae7180d99eab0a729d3f55d46477d0b6e62e7348018b5e6fadd21dd1f4ab100072dccfc7920da2aa406bfe38c6438ff49bd8f586c64581e650532b22d9c16e91991e4bed48eec89bbacda343f3b936d444ade5dc00e6e6f24737ba891e7a06e20ca03b19c65672a535d94a7ddc054ca3f39a9a213951809611bdc81f5fb3b74047c0c591c9e5e2cdc550f9ff62a6cc023be6a59e0a79b0f01f2b232497c36be118ad6589fdc9483edb3defb2ff98ce33c501afb798ae741e92b0afd9ef462981009ef57f6402b93d2c109d378aac8fa4b4c476f6002c23feaa038d0bbabd67e1430072a272d60be721f48b84ec0896644aed86037f86505de857210850d8d250646bc1d2b592496b9ce9532c3101f9b8b502443e5723d79465c10d9917f7a3703fb99201015db091e4277a0b679d35f0c988afbea347c9fd68a283ca41f78bb282dd413843517865f0b5282f925ecc33558d538664d1ec519286e0a9f0b1cca1cbbe59ae99e7c7ae2336f031b812750c737db73fd8a92484d4a234d897d56c41503a05f33a160c5fc4d2623787f1dca151a9f990a1057750c950933b14fbc0d2082e4528dff1d5f8f16fa368cb5d051e2ac00822c2b582dc0857de914eeb0522b162e22cca454596eab7bca7783fe5dfec979ef2fd11c4e048472f99b756ef22f60c267275458196aa17af575adeef431965b550329483ca9669632840772231b10782731259b7a5101e69058f18d4269ffdb87ac16b4bcfc921f89ca68a0fe11dfdb80409df108c5a79095692432e3e11d68bfb7e14995803db8bdae092770132107e0852e6fe31b62a68e96f4261cfb3cc114bee2d6a53d5f552772899b8b1025eb70611e9ffad793ad9989e52a85c1677697715651edc91c28f00083569609be44efe62238d6f4c02ccb48402d0c52db9c42283139f01fd68ba1811c06ed02652329779604c13e0de369535dba83fad4c5c487926fcbcb77393e407f941729a997a422da8847bb111e7d7cdfb3850986465c6ee52c3538ab27da9e1b57a309164f4331228d0fab4bc35d64b6d3197ebf59db58707970dd83b54e2fad30a4008d247beaf4e06bd7de54c3e4125625f3f4b963bf31c1b4cfb41fbe00a7e9ff00134ed427d2c7c682cf3cc287e8403afee1070e0ed6e41823fff9a71c1e0ef80c04820c214773783a3cf049fdd6392788ad116729c42c871f2b4fde5c419afe1b8a4197101d35331a24228de5442d8676e8bbe16dd48d8b6b97f4a6795a3f53263159c72ad8ba9423bd8e946d35ce45204b86f0fc6af45ed8be1f2cd9b7dda30098649d5ce100f52ea3f178ae6b70dafe486f0982b42a43b32aa8b5828b8c57282e13c37b415be9afdf365c7803700a8d6d745776fa09a5f1f3ef883a5d0d252e4612c272abbe15dbb51034d8355c4d4a23e1b2ab0cc0a6ab7368965d83d1da26a24f9e5ca23a4d8523286f13948690665c40cc53edbcc19c65c644362cb074270b9cd5d2c3b835ab8f163be2e03894c230f9276e48ff94ec7e7cea6e149752109a8e780a40af4184c40b37fb931ce8f9cfefad8794ebe31478ef2c17c7b3ca22da6c24298ba7be1afa7469512e20b947ea4b95349428a220c9b35ffbd53333149467a5282448c19ba67aa6c1a47125621ce45f20f1adb447f4de44514ab40f09dbf95e962630fcaf3eacec1f8297dd0ae434e0638a5f60cbfd638a72016fd214930023b82d9a64737e47ea78afd6b8ae0d959a016cb18dc09c287b2552b50b134bd02da3757a0fa66c5be43971c5b7b709f7c8320f083690d0335b6df8c80d1dc6b83f39a4453ea8ddc89c537192fbd25b7bf6cc5816d57470503c4c49b10b2f6cb0c681a587a992b7bb2634e361f5a1ddda4bd4e5730d801b1f65139e4aec2a34b339d4ddec7beb7b5aaf15ae941ace555dacb88faf2dc04507d8b854fb4f1e7503afe9a06ee11bcea41b57c6bc0b545983f969b39b273437e2fb42a0b96b239fcf0798a6662b86374c63fad7f1f41fc5612ded9e780f649ae6cf6ddca9590954f3f27b86e32a6aa1780d10609dea9751434e562709ec19abfce395422f5a225f0de059e27d4f0d5e3c7576bdf98b97ae1343051cc7cd71d6ddfb06235c9028240f64061d1c5c5a489b1f4e565a96cd0f450ea5ebcd9ddb9797f3f105136d286dd91278a081b75e877b4f0fbec0e65ba0104512142c3874273cd0472dab3e207ab8ad5c196a8fbbc5f5bf11560dc76c27cfb708cba70acb2ec10608bab3781d806f6d45f781e1882d9f52f4217e82783d8faf0cbd58f16aef43c74130aa8e26748a0279e6143d4be0846ca4b04707aacdb6ba57e4a125491c67bcfeed37e4
Enter fullscreen mode Exit fullscreen mode

Public params que puedes encontrar en el archivo Verifier.toml (toma en cuenta que debes usar el formato siguiente, donde pasas un arreglo que contiene los parámetros en el orden que están en el circuito: root, proposalId, vote, return):

["0x2959dc1151fec22361702924ec0edcc117824d77c275044462e8a91fd1d707a5","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000001","0x0d45003dbf41a4cf37055cb9a03552f20a9aa750b9a746aa0c6c724f74333b4c"]
Enter fullscreen mode Exit fullscreen mode

Recursos adicionales

Para la siguiente semana

En la 5ta semana de este workshop daremos un vistazo en general sobre temas de actualidad en ZK. Y también haremos un ejercicio práctico donde veremos cómo funciona Halo2, la tecnología base que usa Scroll y la PSE para escalar Ethereum.

Para la siguiente semana asegúrate de traer instalado lo siguiente:

  • Rust
  • Dependencias:
    • Si estás usando Linux también trae instalado CMake y los build essencials

Top comments (0)