🌀Huracán: El proyecto educativo para ingenieros aprendiendo ZK
Published: 2024-07-05 17:06:48

---
title: 🌀Huracán: El proyecto educativo para ingenieros aprendiendo ZK
published: true
description: 
tags: 
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2024-06-26 16:56 +0000
---

Aprender sobre ZK hoy no es una tarea fácil. Es una tecnología nueva, sin mucha documentación. Huracán nace de mi propia necesidad de aprender sobre ZK de una manera práctica, orientada a desarrolladores e ingenieros.

Huracán es un proyecto totalmente funcional, capaz de hacer transacciones privadas en Ethereum y blockchains EVM. Está basado en proyectos de privacidad actualmente en funcionamiento, pero con el código mínimo para facilitar el proceso de aprendizaje. Vamos a cubrir cómo esta tecnología puede adaptarse a nuevos casos de uso y futuras regulaciones. Además, al final del artículo comparto qué se necesita para llevar este proyecto de pruebas en testnet a uso real en producción.

Al terminar esta guía vas a poder ir a otros proyectos de la misma naturaleza entender cómo están construidos.

¿Prefieres ver el código completo? [Dirígete a Github](https://github.com/Turupawn/Huracan) para encontrar la totalidad del código en esta guíua.

_Cómo está construído Huracán:_

* Circuito hecho con Circom
* Hasheo con Poseidon
* Contratos de lógica de depósito y retiro en solidity
* Construcción de merkle trees en js y solidity, verificación en Circom
* Frontend con html y js vainilla
* web3.js para interacción con web3 y snarkjs para probar en el navegador (zk-WASM)
* Relayer con ethers.js 6 y express para preservar el anonimato de los usuarios

## Tabla de contenido

1. [Cómo funciona Huracán](#1-cómo-funciona-huracán)
1. [El circuito](#2-el-circuito)
1. [Los contratos](#3-los-contratos)
1. [El frontend](#4-el-frontend)
1. [El relayer](#5-el-relayer)
1. [¿Cómo llevar Huracán a producción?](#6-cómo-llevar-huracán-a-producción)
1. [Ideas para profundizar](#7-ideas-para-profundizar)

## 1. Cómo funciona Huracán

Huracán es una herramienta DeFi que protege la identidad de sus usuarios utilizando la técnica conocida como _pruebas de inclusión anónima_ para realizar lo que comúnmente llamamos _mixer_. Este sistema es capaz de demostrar que un usuario ha depositado ether en un contrato sin demostrar cual de todos fué.

![Depositando en Huracán](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/if5yhd2ef1vwr0bujxkz.png)
<p><small><em>Cada usuario que deposita Ether en Huracán es colocado como una hoja en un merkle tree dentro del contrato</em></small></p>

Para lograr esto, necesitamos un smart contract donde se depositarán los fondos y al hacerlo generará un merkle tree donde cada hoja representa un depositante. Adicionalmente, ocuparemos un circuito que generará las pruebas de inclusión que mantendrán al usuario anónimo al momento de retirar los fondos. Y también un relayer que ejecutará la transacción en nombre del usuario anónimo para proteger su privacidad.

![Retiro de Huracán](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/igrb94o5cidddo3c8s5z.png)
<p><small><em>Los usuarios pueden luego retirar sus fondos demostrando que parte del merkle tree sin revelar cuál hoja les pertenece</em></small></p>

A continuación el código, la explicación breve y los materiales de apoyo necesarios para construir y lanzar tu propio proyecto con privacidad.

## 2. El circuito

_Material de apoyo: [Smart Contracts privados con Solidity y Circom](https://dev.to/turupawn/smart-contracts-privados-con-solidity-y-circom-3a8h)_

El circuito se encarga si eres parte del merkle tree, es decir, que eres uno de los depositantes sin revelar cuál eres pues mantienes los parámetros privados pero generas una prueba de inclusión que es capaz de ser verificada por un smart contract. ¿Cuáles parámetros privados? Durante el depósito, hasheamos una llave privada y un nulificador para crear así una nueva hoja en el árbol. La llave privada es un parámetro privado que nos servirá para luego demostrar que somos nosotros los dueños de esa hora. El nulificador es otro parámetros cuyo hash será pasado al contrato de solidity al momento de redimir los fondos, esto para prevenir que un usuario retire fondos 2 veces seguidas (double spend). El resto de parámetros privados son la ayuda que ocupa el circuito para reconstruir el árbol y revisar que somos parte de él.

Iniciamos instalando la librería `circomlib` que contiene los circuitos de poseidon que estaremos utilizando en este tutorial.

```bash
git clone https://github.com/iden3/circomlib.git
```

Ahora creamos nuestro circuito `proveWithdrawal` que prueba que hemos depositado en el contrato sin revelar quién somos.

`proveWithdrawal.circom`
```js
pragma circom 2.0.0;

include "circomlib/circuits/poseidon.circom";

template switchPosition() {
    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];
}

template commitmentHasher() {
    signal input privateKey;
    signal input nullifier;
    signal output commitment;
    signal output nullifierHash;
    component commitmentHashComponent;
    commitmentHashComponent = Poseidon(2);
    commitmentHashComponent.inputs[0] <== privateKey;
    commitmentHashComponent.inputs[1] <== nullifier;
    commitment <== commitmentHashComponent.out;
    component nullifierHashComponent;
    nullifierHashComponent = Poseidon(1);
    nullifierHashComponent.inputs[0] <== nullifier;
    nullifierHash <== nullifierHashComponent.out;
}

template proveWithdrawal(levels) {
    signal input root;
    signal input recipient;
    signal input privateKey;
    signal input nullifier;
    signal input pathElements[levels];
    signal input pathIndices[levels];
    signal output nullifierHash;

    signal leaf;
    component commitmentHasherComponent;
    commitmentHasherComponent = commitmentHasher();
    commitmentHasherComponent.privateKey <== privateKey;
    commitmentHasherComponent.nullifier <== nullifier;
    leaf <== commitmentHasherComponent.commitment;
    nullifierHash <== commitmentHasherComponent.nullifierHash;

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

    signal computedPath[levels];

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

        hashers[i] = Poseidon(2);
        hashers[i].inputs[0] <== selectors[i].out[0];
        hashers[i].inputs[1] <== selectors[i].out[1];
        computedPath[i] <== hashers[i].out;
    }
    root === computedPath[levels - 1];
}

component main {public [root, recipient]} = proveWithdrawal(2);
```

Para compilar el circuito necesitamos tener instalado ambos circom y snarkjs. Si no lo tienes instalado usa la guía de instalación de Circom.

{% collapsible Guía de instalación de Circom %}

Coloca estos comandos para instalar circom y snarkjs.

```bash
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
git clone https://github.com/iden3/circom.git
cd circom
cargo build --release
cargo install --path circom
npm install -g snarkjs
```

{% endcollapsible %}

Ejecuta los comandos para generar la trusted setup y generar los archivos artefactos que estaremos usando más adelante en el frontend.

```bash
circom proveWithdrawal.circom --r1cs --wasm --sym
snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v
snarkjs groth16 setup proveWithdrawal.r1cs pot12_final.ptau proveWithdrawal_0000.zkey
snarkjs zkey contribute proveWithdrawal_0000.zkey proveWithdrawal_0001.zkey --name="1st Contributor Name" -v
snarkjs zkey export verificationkey proveWithdrawal_0001.zkey verification_key.json
```

Ahora podemos generar el contrato verificador en `verfier.sol`.

```bash
snarkjs zkey export solidityverifier proveWithdrawal_0001.zkey verifier.sol
```

## 3. Los contratos

_Material de apoyo: [Solidity en 15 minutos](https://dev.to/turupawn/como-lanzar-un-dex-paso-a-pasox-2cnm)_

Los contratos son la garantía transparente que todo ha funcionado de manera correcta. Nos permite llevar un conteo de cuánto se ha depositado y también verifican que las pruebas sean válidas para así liberar los fondos. Es importante que todo lo que ocurre en los smart contracts es público, es la parte de nuestro sistema que no es anónima.

Haremos uso de tres contratos. El primero es el contrato verificador que recién generamos en el archivo `verifier.sol`, lánzalo ahora.

El segundo contrato es el de poseidon, si estás en Scroll Sepolia simplemente usa el que yo ya lancé en `0x52f28FEC91a076aCc395A8c730dCa6440B6D9519`. Si quieres usar otra blockchain descolapsa y sigue los pasos:

{% collapsible Lanza el contrato de poseidon %}

La versión de poseidon que usamos en nuestro circuito y contrato deben ser exactamente compatibles. Por lo tanto usamos la versión en `circomlibjs` tal y como lo muestro, solo asegúrate de colocar tu llave privada y url RPC en `TULLAVEPRIVADA` y `TUURLRPC`.

```bash
git clone https://github.com/iden3/circomlibjs.git
node --input-type=module --eval "import { writeFileSync } from 'fs'; import('./circomlibjs/src/poseidon_gencontract.js').then(({ createCode }) => { const output = createCode(2); writeFileSync('poseidonBytecode', output); })"
cast send --rpc-url TUURLRPC --private-key TULLAVEPRIVADA --create $(cat bytecode)
```
{% endcollapsible %}

Ahora lanza el contrato `Huracan` pasando como parámetro la dirección del contrato verificador y de poseidon.

```js
// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

interface IPoseidon {
    function poseidon(uint[2] memory inputs) external returns(uint[1] memory output);
}

interface ICircomVerifier {
    function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[3] calldata _pubSignals) external view returns (bool);
}

contract Huracan {
    ICircomVerifier circomVerifier;
    uint nextIndex;
    uint public constant LEVELS = 2;
    uint public constant MAX_SIZE = 4;
    uint public NOTE_VALUE = 0.001 ether;
    uint[] public filledSubtrees = new uint[](LEVELS);
    uint[] public emptySubtrees = new uint[](LEVELS);
    address POSEIDON_ADDRESS;
    uint public root;

    mapping(uint => uint) public commitments;
    mapping(uint => bool) public nullifiers;

    event Deposit(uint index, uint commitment);

    constructor(address poseidonAddress, address circomVeriferAddress) {
        POSEIDON_ADDRESS = poseidonAddress;
        circomVerifier = ICircomVerifier(circomVeriferAddress);

        for (uint32 i = 1; i < LEVELS; i++) {
            emptySubtrees[i] = IPoseidon(POSEIDON_ADDRESS).poseidon([
                emptySubtrees[i-1],
                0
            ])[0];
        }
    }

    function deposit(uint commitment) public payable {
        require(msg.value == NOTE_VALUE, "Invalid value sent");
        require(nextIndex != MAX_SIZE, "Merkle tree is full. No more leaves can be added");
        uint currentIndex = nextIndex;
        uint currentLevelHash = commitment;
        uint left;
        uint right;

        for (uint32 i = 0; i < LEVELS; i++) {
            if (currentIndex % 2 == 0) {
                left = currentLevelHash;
                right = emptySubtrees[i];
                filledSubtrees[i] = currentLevelHash;
            } else {
                left = filledSubtrees[i];
                right = currentLevelHash;
            }
            currentLevelHash = IPoseidon(POSEIDON_ADDRESS).poseidon([left, right])[0];
            currentIndex /= 2;
        }

        root = currentLevelHash;
        emit Deposit(nextIndex, commitment);
        commitments[nextIndex] = commitment;
        nextIndex = nextIndex + 1;
    }

    function withdraw(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[3] calldata _pubSignals) public {
        circomVerifier.verifyProof(_pA, _pB, _pC, _pubSignals);
        uint nullifierHash = _pubSignals[0];
        uint rootPublicInput = _pubSignals[1];
        address recipient = address(uint160(_pubSignals[2]));

        require(root == rootPublicInput, "Invalid merke root");
        require(!nullifiers[nullifierHash], "Vote already casted");

        nullifiers[nullifierHash] = true;

        (bool sent, bytes memory data) = recipient.call{value: NOTE_VALUE}("");
        require(sent, "Failed to send Ether");
        data;
    }
}
```

## 4. El frontend

_Material de apoyo: [Interfaces con privacidad en Solidity y zk-WASM](https://dev.to/turupawn/interfaces-con-privacidad-en-solidity-y-zk-wasm-1amg)_

El frontend es la interfaz gráfica con la que estaremos interactuando. En esta demostración estaremos usando html y js vainilla para que los desarrolladores podamos adaptarla a cualquier frontend framework que estemos usando. Una característica muy importante del frontend es que debe ser capaz de producir las pruebas zk sin soltar información privada en el internet. Por eso es importante la tecnología zk-WASM que permite de una manera eficiente construir pruebas en nuestro navegador.

Ahora crea la siguiente estructura de archivos:

```
js/
  blockchain_stuff.js
  snarkjs.min.js
json_abi/
  Huracan.json
  Poseidon.json
zk_artifacts/
  myCircuit_final.zkey
  myCircuit.wasm
index.html
```

* `js/snarkjs.min.js`: descarga este archivo que contiene la librería de snark.js
* `json_abi/Huracan.json`: el ABI del contrato CircomCustomLogic que recién lanzamos, por ejemplo en Remix, lo puedes hacer dando clic en el botón "ABI" en la pestaña de compilación.
* `json_abi/Poseidon.json`: coloca [esto](https://gist.github.com/Turupawn/b89999eb8b00d7507908d6fbf6aa7f0b)
* `zk_artifacts`: coloca en esta carpeta los artefactos generados anteriormente. Nota: Cambia el nombre de myCircuit_0002.zkey por myCircuit_final.zkey
* `index.html`, `js/blockchain_stuff.js` y `js/zk_stuff.js` los detallo a continuación

El archivo de HTML contiene la interfaz necesaria para que los usuarios interactén con Huracán.

`index.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
</head>
<body>
  <input id="connect_button" type="button" value="Connect" onclick="connectWallet()" style="display: none"></input>
  <p id="account_address" style="display: none"></p>
  <p id="web3_message"></p>
  <p id="contract_state"></p>
  <input type="input"  value="" id="depositPrivateKey" placeholder="private key"></input>
  <input type="input"  value="" id="depositNullifier" placeholder="nullifier"></input>
  <input type="button" value="Deposit" onclick="_deposit()"></input>
  <br>
  <input type="input"  value="" id="withdrawPrivateKey" placeholder="private key"></input>
  <input type="input"  value="" id="withdrawNullifier" placeholder="nullifier"></input>
  <input type="input"  value="" id="withdrawRecipient" placeholder="recipient"></input>
  <input type="button" value="Withdraw" onclick="_withdraw()"></input>
  <br>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/web3/1.3.5/web3.min.js"></script>
  <script type="text/javascript" src="js/zk_stuff.js"></script>
  <script type="text/javascript" src="js/blockchain_stuff.js"></script>
  <script type="text/javascript" src="js/snarkjs.min.js"></script>
</body>
</html>

<script>
  function _deposit()
  {
    depositPrivateKey = document.getElementById("depositPrivateKey").value
    depositNullifier = document.getElementById("depositNullifier").value
    deposit(depositPrivateKey, depositNullifier)
  }

  function _withdraw()
  {
    withdrawPrivateKey = document.getElementById("withdrawPrivateKey").value
    withdrawNullifier = document.getElementById("withdrawNullifier").value
    withdrawRecipient = document.getElementById("withdrawRecipient").value
    withdraw(withdrawPrivateKey, withdrawNullifier, withdrawRecipient)
  }
</script>
```

Ahora colocamos toda lo logica relacionada a web3, es decir la conexión a la wallet en el browser, lectura del estado y llamado de funciones.

`js/blockchain_stuff.js`
```js
const NETWORK_ID = 534351

const HURACAN_ADDRESS = "0x8BD32BDC921f5239c0f5d9eaf093B49A67C3b9d0"
const HURACAN_ABI_PATH = "./json_abi/Huracan.json"

const POSEIDON_ADDRESS = "0x52f28FEC91a076aCc395A8c730dCa6440B6D9519"
const POSEIDON_ABI_PATH = "./json_abi/Poseidon.json"

const RELAYER_URL = "http://localhost:8080"

var huracanContract
var poseidonContract

var accounts
var web3
let leaves

function metamaskReloadCallback() {
  window.ethereum.on('accountsChanged', (accounts) => {
    document.getElementById("web3_message").textContent="Se cambió el account, refrescando...";
    window.location.reload()
  })
  window.ethereum.on('networkChanged', (accounts) => {
    document.getElementById("web3_message").textContent="Se el network, refrescando...";
    window.location.reload()
  })
}

const getWeb3 = async () => {
  return new Promise((resolve, reject) => {
    if(document.readyState=="complete")
    {
      if (window.ethereum) {
        const web3 = new Web3(window.ethereum)
        window.location.reload()
        resolve(web3)
      } else {
        reject("must install MetaMask")
        document.getElementById("web3_message").textContent="Error: Porfavor conéctate a Metamask";
      }
    }else
    {
      window.addEventListener("load", async () => {
        if (window.ethereum) {
          const web3 = new Web3(window.ethereum)
          resolve(web3)
        } else {
          reject("must install MetaMask")
          document.getElementById("web3_message").textContent="Error: Please install Metamask";
        }
      });
    }
  });
};

const getContract = async (web3, address, abi_path) => {
  const response = await fetch(abi_path);
  const data = await response.json();

  const netId = await web3.eth.net.getId();
  contract = new web3.eth.Contract(
    data,
    address
    );
  return contract
}

async function loadDapp() {
  metamaskReloadCallback()
  document.getElementById("web3_message").textContent="Please connect to Metamask"
  var awaitWeb3 = async function () {
    web3 = await getWeb3()
    web3.eth.net.getId((err, netId) => {
      if (netId == NETWORK_ID) {
        var awaitContract = async function () {
          huracanContract = await getContract(web3, HURACAN_ADDRESS, HURACAN_ABI_PATH)
          poseidonContract = await getContract(web3, POSEIDON_ADDRESS, POSEIDON_ABI_PATH)
          document.getElementById("web3_message").textContent="You are connected to Metamask"
          onContractInitCallback()
          web3.eth.getAccounts(function(err, _accounts){
            accounts = _accounts
            if (err != null)
            {
              console.error("An error occurred: "+err)
            } else if (accounts.length > 0)
            {
              onWalletConnectedCallback()
              document.getElementById("account_address").style.display = "block"
            } else
            {
              document.getElementById("connect_button").style.display = "block"
            }
          });
        };
        awaitContract();
      } else {
        document.getElementById("web3_message").textContent="Please connect to Goerli";
      }
    });
  };
  awaitWeb3();
}

async function connectWallet() {
  await window.ethereum.request({ method: "eth_requestAccounts" })
  accounts = await web3.eth.getAccounts()
  onWalletConnectedCallback()
}

loadDapp()

const onContractInitCallback = async () => {
  document.getElementById("web3_message").textContent="Reading merkle tree data...";
  leaves = []
  let i =0
  let maxSize = await huracanContract.methods.MAX_SIZE().call()
  for(let i=0; i<maxSize; i++)
  {
    leaves.push(await huracanContract.methods.commitments(i).call())
  }
  document.getElementById("web3_message").textContent="All ready!";
}

const onWalletConnectedCallback = async () => {
}


//// Functions ////

const deposit = async (depositPrivateKey, depositNullifier) => {
  let commitment = await poseidonContract.methods.poseidon([depositPrivateKey,depositNullifier]).call()

  let value = await huracanContract.methods.NOTE_VALUE().call()

  document.getElementById("web3_message").textContent="Please confirm transaction.";

  const result = await huracanContract.methods.deposit(commitment)
    .send({ from: accounts[0], gas: 0, value: value })
    .on('transactionHash', function(hash){
      document.getElementById("web3_message").textContent="Executing...";
    })
  .on('receipt', function(receipt){
    document.getElementById("web3_message").textContent="Success.";    })
  .catch((revertReason) => {
    console.log("ERROR! Transaction reverted: " + revertReason.receipt.transactionHash)
  });
}

const withdraw = async (privateKey, nullifier, recipient) => {
  document.getElementById("web3_message").textContent="Generating proof...";

  let commitment = await poseidonContract.methods.poseidon([privateKey,nullifier]).call()

  let index = null
  for(let i=0; i<leaves.length;i++)
  {
    if(commitment == leaves[i])
    {
      index = i
    }
  }

  if(index == null)
  {
    console.log("Commitment not found in merkle tree")
    return
  }

  let root = await huracanContract.methods.root().call()
  let proof = await getWithdrawalProof(index, privateKey, nullifier, recipient, root)

  await sendProofToRelayer(proof.pA, proof.pB, proof.pC, proof.publicSignals)
}

const sendProofToRelayer = async (pA, pB, pC, publicSignals) => {
  fetch(RELAYER_URL + "/relay?pA=" + pA + "&pB=" + pB + "&pC=" + pC + "&publicSignals=" + publicSignals)
  .then(res => res.json())
  .then(out =>
    console.log(out))
  .catch();
}
```

Finalmente el archivo que contiene la logica relacionada a ZK. Este archivo se encarga de generar las pruebas ZK.

`js/zk_stuff.js`
```js
async function getMerklePath(leaves) {
  if (leaves.length === 0) {
    throw new Error('Leaves array is empty');
  }

  let layers = [leaves];

  // Build the Merkle tree
  while (layers[layers.length - 1].length > 1) {
    const currentLayer = layers[layers.length - 1];
    const nextLayer = [];

    for (let i = 0; i < currentLayer.length; i += 2) {
      const left = currentLayer[i];
      const right = currentLayer[i + 1] ? currentLayer[i + 1] : left; // Handle odd number of nodes
      nextLayer.push(await poseidonContract.methods.poseidon([left,right]).call())
    }
    layers.push(nextLayer);
  }

  const root = layers[layers.length - 1][0];

  function getPath(leafIndex) {
    let pathElements = [];
    let pathIndices = [];
    let currentIndex = leafIndex;

    for (let i = 0; i < layers.length - 1; i++) {
      const currentLayer = layers[i];
      const isLeftNode = currentIndex % 2 === 0;
      const siblingIndex = isLeftNode ? currentIndex + 1 : currentIndex - 1;

      pathIndices.push(isLeftNode ? 0 : 1);
      pathElements.push(siblingIndex < currentLayer.length ? currentLayer[siblingIndex] : currentLayer[currentIndex]);

      currentIndex = Math.floor(currentIndex / 2);
    }

    return {
      PathElements: pathElements,
      PathIndices: pathIndices
    };
  }

  // You can get the path for any leaf index by calling getPath(leafIndex)
  return {
    getMerklePathForLeaf: getPath,
    root: root
  };
}

function addressToUint(address) {
  const hexString = address.replace(/^0x/, '');
  const uint = BigInt('0x' + hexString);
  return uint;
}

async function getWithdrawalProof(index, privateKey, nullifier, recipient, root) {
  let merklePath = await getMerklePath(leaves)
  let pathElements = merklePath.getMerklePathForLeaf(index).PathElements;
  let pathIndices = merklePath.getMerklePathForLeaf(index).PathIndices;
  let proverParams = {
    "privateKey": privateKey,
    "nullifier": nullifier,
    "recipient": addressToUint(recipient),
    "root": root,
    "pathElements": pathElements,
    "pathIndices": pathIndices
  }

  const { proof, publicSignals } = await snarkjs.groth16.fullProve( 
    proverParams, 
    "../zk_artifacts/proveWithdrawal.wasm", "../zk_artifacts/proveWithdrawal_final.zkey"
  );

  let pA = proof.pi_a
  pA.pop()
  let pB = proof.pi_b
  pB.pop()
  let pC = proof.pi_c
  pC.pop()

  document.getElementById("web3_message").textContent="Proof generated please confirm transaction.";

  return {
    pA: pA,
    pB: pB,
    pC: pC,
    publicSignals: publicSignals
  }
}
```

### 5. El relayer

¿De qué sirve usar pruebas de anonimato zk si al final nosotros mismos la ejecutamos? Si hacemos esto perderíamos la privacidad porque en Ethereum todo es público. Por eso ocupamos a un relayer. Un intermediario que ejecuta la transacción on-chain a nombre de del usuario anónimo.

Iniciamos creando el archivo del backend.

`relayer.mjs`
```js
import fs from "fs"
import cors from "cors"
import express from "express"
import { ethers } from 'ethers';

const app = express()
app.use(cors())

const JSON_CONTRACT_PATH = "./json_abi/Huracan.json"
const CHAIN_ID = "534351"
const PORT = 8080
var contract
var provider
var signer

const { RPC_URL, HURACAN_ADDRESS, RELAYER_PRIVATE_KEY, RELAYER_ADDRESS } = process.env;

const loadContract = async (data) => {
  data = JSON.parse(data);
  contract = new ethers.Contract(HURACAN_ADDRESS, data, signer);
}

async function initAPI() {
  provider = new ethers.JsonRpcProvider(RPC_URL);
  signer = new ethers.Wallet(RELAYER_PRIVATE_KEY, provider);

  fs.readFile(JSON_CONTRACT_PATH, 'utf8', function (err,data) {
    if (err) {
      return console.log(err);
    }
    loadContract(data)
  });

  app.listen(PORT, () => {
    console.log(`Listening to port ${PORT}`)
  })
}

async function relayMessage(pA, pB, pC, publicSignals)
{
  console.log(pA)
  console.log(pB)
  console.log(pC)
  console.log(publicSignals)
  const transaction = {
    from: RELAYER_ADDRESS,
    to: HURACAN_ADDRESS,
    value: '0',
    gasPrice: "700000000", // 0.7 gwei
    nonce: await provider.getTransactionCount(RELAYER_ADDRESS),
    chainId: CHAIN_ID,
    data: contract.interface.encodeFunctionData(
      "withdraw",[pA, pB, pC, publicSignals]
    )
  };
  const signedTransaction = await signer.populateTransaction(transaction);
  const transactionResponse = await signer.sendTransaction(signedTransaction);
  console.log('🎉 The hash of your transaction is:', transactionResponse.hash);
}

app.get('/relay', (req, res) => {
  console.log(req)
  var pA = req.query["pA"].split(',')
  var pBTemp = req.query["pB"].split(',')
  const pB = [
    [pBTemp[0], pBTemp[1]],
    [pBTemp[2], pBTemp[3]]
  ];
  var pC = req.query["pC"].split(',')
  var publicSignals = req.query["publicSignals"].split(',')

  relayMessage(pA, pB, pC, publicSignals)

  res.setHeader('Content-Type', 'application/json');
  res.send({
    "message": "the proof was relayed"
  })
})

initAPI()
```

Instala la librería de coors para puedas correr el relayer de manera local.

```
npm install cors
```

Ahora lanza el servidor reemplazando `TUURLRPC`, `TUHURACANADDRESS`, `TULLAVEPRIVADA`, `TUADDRESS` en el comandos a continuación.

```bash
RPC_URL=TUURLRPC HURACAN_ADDRESS=TUHURACANADDRESS RELAYER_PRIVATE_KEY=TULLAVEPRIVADA RELAYER_ADDRESS=TUADDRESS node relayer.mjs
```

Ahora estás listo para depositar y retirar fondos en Huracán desde la interfaz web.

![Huracán web](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m0gfsc81pgjfcyt32gyu.png)

## 6. ¿Cómo llevar Huracán a producción?

**a. Guarda el histórico de raíces on-chain**

Al solo estar guardando la raíz más reciente, la prueba generada debe usar esta. Esto significa que si justo después de generar una prueba de retiro alguien hace un depósito, y por consiguiente modifica la raíz, la prueba generada será inválida y se tendrá que generar una nueva.

_Cambios necesarios:_ Guardar todo el histórico de las raíces on chain en, por ejemplo un mapping `mapping(uint id => uint root) public roots;` y al momento de generar una prueba usar la más reciente. Si alguien hace un depósito y cambia la raíz no habrá problema pues la verificación se hará ente cualquier raíz que haya sido guardada históricamente usando una función, por ejemplo `isKnownRoot(uint root)`.

**b. Indexa el merkle tree en un lugar accesible**

Para generar una prueba de inclusión, ocupamos leer el estado actual del árbol. Actualmente lo leemos de la variable `commitments` pero este proceso es lento y requiere muchas llamadas RPC si el tamaño del árbol es grande.

_Cambios necesarios:_ Almacenar e indexar en un lugar accesible la totalidad del árbol. Pienso que el lugar ideal para esto es un subgraph.

**c. Incentiva al relayer**

Es necesario ofrecer una recompensa al relayer pues él es quien paga las comisiones de transacción on-chain.

_Cambios necesarios:_ Al momento de generar la prueba, otorga un porcentaje de la note al relayer. Puedes hacer esto agregándo un parámetro extra en los circuitos, por ejemplo `signal input fee;` y en solidity enviar ese valor al `msg.sender` o a quien el relayer determine.

**d. Usa librerías adecuadas**

En la webapp, en vez de html y js vainilla deberías usar un frontend framework como react, angular o vue para ofrecer una mejor experiencia a los usuarios y desarrolladores.

En el relayer, en vez de express se debería de usar un backend más robusto y hostearlo en una máquina equipada para soportar un número de transacciones alto y con mecanismos anti-DoS, con un firewall adecuado ya que los fondos del relayer para usar como gas son un motivo para hackear el servidor.

**e. Define el tamaño del merkle tree**

Este ejemplo funciona para 4 depositantes, tendrás que reflejar los cambios en el circuito y contrato para hacerlo funcionar.

_Cambios necesarios:_ Comienza cámbiando [la cantidad de niveles en el circuito](https://github.com/Turupawn/Huracan/blob/master/circuits/proveWithdrawal.circom#L67) actualmente está establecida en 2 pues es la cantidad necesaria para un árbol con 4 hojas. Además actualiza las constantes `LEVELS` y `MAX_SIZE` [en el contrato](https://github.com/Turupawn/Huracan/blob/master/contracts/Huracan.sol#L16). Si tu árbol es muy grande, puedes ahorrar gas en el lanzamiento hardcodeando los valores default de un árbol vacío [en vez de usar un ciclo](https://github.com/Turupawn/Huracan/blob/master/contracts/Huracan.sol#L33) como lo he mostrado.

**f. Recuerda, muy importante, todo lo que hemos usado está en etapas experimentales**

Los circuitos y contratos en esta guía no están debidamente auditados, al igual que las librerías que se usan. Como por ejemplo Poseidon, que es una nueva función de hasheo que es muy prometedora y que usamos en vez del tradicional Pedersen.

Y también recuerda, en este tutorial no hemos hecho una trusted setup segura. Para hacer esto es recomendado hacer una ceremonia abierta, con tiempo suficiente para abrir la participación.

## 7. Ideas para profundizar

**a. Pruebas de exclusión**

De la misma manera que en este ejemplo manejamos pruebas de inclusión, podemos hacer pruebas de exclusión que _demuestren que que no somos parte de un grupo blacklisteado_. Esto puede ayudar a ser compatibles con futuras regulaciones que determinen los estados.

**b. Usar ERC20s en vez de Ether**

En vez de usar la ether como moneda nativa en Solidity, podemos usar un ERC20 específico. Los cambios únicamente se harán en los contratos de solidity y webapp. Pues los circuitos pueden quedar exactamente igual.

**c. Experimentar con Re-staking**

Una vez integres ERC20s me parece que una buen paso para continuar es experimentar con la generación de ganancias pasivas usando LSTs.

**d. ¡Piensa en otros casos de uso!**

Las pruebas de inclusión anónimas tienen muchos casos de uso, inclusive fuera de DeFi. Piensa cómo puedes implementar lo que has aprendido en sistemas de votos y gobernanza, redes sociales, videojuegos, etc...

**¡Gracias por leer esta guía!**

Sígueme en dev.to y en [Youtube](https://www.youtube.com/channel/UCNRB4tgwp09z4391JRjEsRA) para todo lo relacionado al desarrollo en Blockchain en Español.