Circuitos zkSNARKs paso a paso
Published: 2023-03-31 18:43:15

---
title: Circuitos zkSNARKs paso a paso
published: true
description: 
tags: 
#cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3oe5dyodpdi6l5hjv5zf.png
---

ZK es una tecnología que trae nuevos beneficios al blockchain. Tanto para escalar como para privacidad. En este workshop vamos a hacer un primer circuito ZK que nos servirá para aprender los conocimientos básicos sobre el tema. Haremos un juego llamado ZK Sudoku, enviaremos una prueba que resolvimos el rompecabezas a un smart contract y este nos dará un token como premio por hacerlo.

## Antes de comenzar

Para este tutorial ocuparás [NodeJs](https://nodejs.org/es/) que recomiendo descargarlo en Linux via [NVM](https://github.com/nvm-sh/nvm#installing-and-updating), y también necesitarás [Metamask](https://metamask.io/) u otra wallet compatible con fondos en Goerli que puedes obtener desde un [faucet](https://goerlifaucet.com/).

## Obtener fondos en Scroll Testnet

Puedes ir al [bridge de Scroll](https://scroll.io/alpha/bridge) para mover fondos de Goerli testnet a Scroll Alpha. Coloca tu wallet en Goerli y desde ahí mueve fondos a Scroll L2.

![Scroll Bridge](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iwgkh4csk0r2l5snydjk.png)

💡 Este post muestra como lanzar el proyecto en Scroll. Mas adelante mostraremos los cambios necesarios para hacerlo en otras chains.

## Ceremonia Inicial

Esta ceremonia inicializa los _Parámetros Globales_ que son necesarios para asegurar la computación de la generación de pruebas. La ceremonia nos devolvera la _llave de comprobación_ (PK) y la _llave de verificación_ (VK). Estlo lo haremos a partir de un circuito escrito en el lenguaje Zokrates.

Comenzamos creando el siguiente archivo html.

`generateVerifier.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
</head>
<body>
  <textarea cols="40" rows="5" id="_source">def main(private field a, field b)
{
  assert(a * a == b);
  return;
}</textarea>
  <input type="button" value="Generate Verifier" onclick="_generate()"></input>
  <h3>Verifier</h3>
  <textarea cols="40" rows="5" id="verifier"></textarea>
  <br>
  <h3>Prover key</h3>
  <textarea cols="40" rows="5" id="pk"></textarea>
  <br>
</body>
</html>

<script src="https://unpkg.com/zokrates-js@latest/umd.min.js"></script>
<script>
  function _generate()
  {
    zokrates.initialize().then((zokratesProvider) => {
      var source = document.getElementById("_source").value
      const artifacts = zokratesProvider.compile(source);
      const keypair = zokratesProvider.setup(artifacts.program);
      const verifier = zokratesProvider.exportSolidityVerifier(keypair.vk);

      document.getElementById("verifier").textContent=verifier;
      document.getElementById("pk").textContent='{"pk":[' + keypair.pk + ']}';
    });
  }
</script>
```

Ahora lo corremos en un servidor web. Puede ser cualquier servidor de archivos estáticos. En este caso vamos a usar `lite-server`.

Para instalar el servidor corremos lo siguiente.

```bash
npm install -g lite-server
```

Y esto para levantar el servidor.

```bash
lite-server
```

Accedemos al servidor web desde la url [http://localhost:3000/generateVerifier.html](http://localhost:3000/generateVerifier.html).

Una vez lista la interfaz, colocamos el circuito en el primer cuadro de texto. Y luego hacemos clic en "Generate Verifier".

```js
// Sudoku of format

// | a11 | a12 || b11 | b12 |
// --------------------------
// | a21 | a22 || b21 | b22 |
// ==========================
// | c11 | c12 || d11 | d12 |
// --------------------------
// | c21 | c22 || d21 | d22 |

// We use a naive encoding of the values as `[1, 2, 3, 4]` and rely on if-else statements to detect duplicates

def countDuplicates(field e11, field e12, field e21, field e22) -> field {
    field mut duplicates = e11 == e12 ? 1 : 0;
    duplicates = duplicates + (e11 == e21 ? 1 : 0);
    duplicates = duplicates + (e11 == e22 ? 1 : 0);
    duplicates = duplicates + (e12 == e21 ? 1 : 0);
    duplicates = duplicates + (e12 == e22 ? 1 : 0);
    duplicates = duplicates + (e21 == e22 ? 1 : 0);
    return duplicates;
}

// returns 0 for x in (1..4)
def validateInput(field x) -> bool {
    return (x - 1) * (x - 2) * (x - 3) * (x - 4) == 0;
}

// variables naming: box'row''column'
def main(field a21, field b11, field b22, field c11, field c22, field d21, private field a11, private field a12, private field a22, private field b12, private field b21, private field c12, private field c21, private field d11, private field d12, private field d22) -> bool {

    // validate inputs
    assert(validateInput(a11));
    assert(validateInput(a12));
    assert(validateInput(a21));
    assert(validateInput(a22));

    assert(validateInput(b11));
    assert(validateInput(b12));
    assert(validateInput(b21));
    assert(validateInput(b22));

    assert(validateInput(c11));
    assert(validateInput(c12));
    assert(validateInput(c21));
    assert(validateInput(c22));

    assert(validateInput(d11));
    assert(validateInput(d12));
    assert(validateInput(d21));
    assert(validateInput(d22));

    field mut duplicates = 0; // globally counts duplicate entries in boxes, rows and columns

    // check box correctness

    // no duplicates
    duplicates = duplicates + countDuplicates(a11, a12, a21, a22);
    duplicates = duplicates + countDuplicates(b11, b12, b21, b22);
    duplicates = duplicates + countDuplicates(c11, c12, c21, c22);
    duplicates = duplicates + countDuplicates(d11, d12, d21, d22);

    // check row correctness

    duplicates = duplicates + countDuplicates(a11, a12, b11, b12);
    duplicates = duplicates + countDuplicates(a21, a22, b21, b22);
    duplicates = duplicates + countDuplicates(c11, c12, d11, d12);
    duplicates = duplicates + countDuplicates(c21, c22, d21, d22);

    // check column correctness

    duplicates = duplicates + countDuplicates(a11, a21, c11, c21);
    duplicates = duplicates + countDuplicates(a12, a22, c12, c22);
    duplicates = duplicates + countDuplicates(b11, b21, d11, d21);
    duplicates = duplicates + countDuplicates(b12, b22, d12, d22);

    // the solution is correct if and only if there are no duplicates
    assert(duplicates == 0);
    return true;
}
```

La llave de comprobación, será generada en formato json. En una de las cajas posteriores en la UI. Colocamos esta llave en el archivo `assets/pk.json` en la misma carpeta de proyecto. También guardemos el circuito de Zokrates en el archivo `assets/sudoku.zok`.

## El smart contract de tokens

La llave de verificación, o VK, nos es devuelta incrustada en un contrato inteligente en Solidity.

Vamos a modificar el contrato generado para que nos otorgue un token por completar la respuesta.

Para eso vamos a hacer lo siguientes cambios en el contrato.

Primero importamos las librerías de OpenZeppelin necesarias para lanzar tokens ERC20. Y luego el contrato que hereda del contrato verificador que fue generado anteriormente. Para hacer esto puedes ya sea importar el contrato verificador como un nuevo archivo o colocar este código luego del contrato verificador generado.

```solidity
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

// SPDX-License-Identifier: MIT

contract ZKSudokuToken is Verifier, ERC20 {

    constructor() ERC20("ZK Sudoku Token", "ZKST") {
    }

    function mintWithProof(Proof memory proof, uint[7] memory input) public
    {
        require(verifyTx(proof, input));
        _mint(msg.sender, 10 ether);
    }
}
```

Lanza el smart contract y agrega el ABI en el archivo `assets/VerifierABI.json`.

## El frontend

Agregamos el archivo HTML del frontend que contiene todo el UI.

```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>
  <input type="input" value="1" id="a11" size="2" disabled></input>
  <input type="input" value="2" id="a12" size="2" disabled></input>
  |
  <input type="input" value="" id="b11" size="2"></input>
  <input type="input" value="4" id="b12" size="2" disabled></input><br>
  <input type="input" value="3" id="a21" size="2" disabled></input>
  <input type="input" value="" id="a22" size="2" ></input>
  |
  <input type="input" value="1" id="b21" size="2" disabled></input>
  <input type="input" value="2" id="b22" size="2" disabled></input><br>
  ----------------------------------<br>
  <input type="input" value="2" id="c11" size="2" disabled></input>
  <input type="input" value="1" id="c12" size="2" disabled></input>
  |
  <input type="input" value="4" id="d11" size="2" disabled></input>
  <input type="input" value="3" id="d12" size="2" disabled></input><br>
  <input type="input" value="4" id="c21" size="2" disabled></input>
  <input type="input" value="3" id="c22" size="2" disabled></input>
  |
  <input type="input" value="2" id="d21" size="2" disabled></input>
  <input type="input" value="" id="d22" size="2"></input><br>
  <input type="button" value="Generate Proof" onclick="_verify()"></input>
  <input type="button" value="Mint" onclick="_mint()"></input>
  <br>
  <p id="proof"></p>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/web3/1.3.5/web3.min.js"></script>
  <script src="https://unpkg.com/zokrates-js@latest/umd.min.js"></script>
  <script type="text/javascript" src="blockchain_stuff.js"></script>
</body>
</html>

<script>
  function _verify()
  {
    verify()
  }
  function _mint()
  {
    mintWithProof()
  }
</script>
```

Finalmente agregamos el archivo `blockchain_stuff.js` que contiene la lógica del blockchain. Solo asegúrate de actualizar `MY_CONTRACT_ADDRESS` con el address del contrato que recién lanzamos.

```js
const NETWORK_ID = 534353

const MY_CONTRACT_ADDRESS = "0x31379Cff7f2846fa34DA42265F1C50B827653DE9"
const MY_CONTRACT_ABI_PATH = "./assets/VerifierABI.json"
const PK_PATH = "./assets/pk.json"
const ZOK_PATH = "./assets/sudoku.zok"
var my_contract
var zokratesProvider

var accounts
var web3

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() {
  //zokratesProvider = await zokrates.initialize()
  //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 () {
          my_contract = await getContract(web3, MY_CONTRACT_ADDRESS, MY_CONTRACT_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 Scroll Alpha";
      }
    });
  };
  awaitWeb3();
}

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

loadDapp()

const onContractInitCallback = async () => {
  // Now the contracts are initialized
}

const onWalletConnectedCallback = async () => {
  // Now the account is initialized
}

async function getProof()
{
  a11 = document.getElementById("a11").value
  a12 = document.getElementById("a12").value
  a21 = document.getElementById("a21").value
  a22 = document.getElementById("a22").value

  b11 = document.getElementById("b11").value
  b12 = document.getElementById("b12").value
  b21 = document.getElementById("b21").value
  b22 = document.getElementById("b22").value

  c11 = document.getElementById("c11").value
  c12 = document.getElementById("c12").value
  c21 = document.getElementById("c21").value
  c22 = document.getElementById("c22").value

  d11 = document.getElementById("d11").value
  d12 = document.getElementById("d12").value
  d21 = document.getElementById("d21").value
  d22 = document.getElementById("d22").value

  zokratesProvider = await zokrates.initialize()

  const response = await fetch(ZOK_PATH);
  const source = await response.text();
  
  const artifacts = zokratesProvider.compile(source);
  const { witness, output } = zokratesProvider.computeWitness(artifacts, [a21, b11, b22, c11, c22, d21, a11, a12, a22, b12, b21, c12, c21, d11, d12, d22]);
  
  pkFile = await fetch(PK_PATH)
  pkJson = await pkFile.json()
  pk = pkJson.pk
  
  const proof = zokratesProvider.utils.formatProof(zokratesProvider.generateProof(
    artifacts.program,
    witness,
    pk
    ));
    
  return proof
}

const verify = async () => {
  var proof = await getProof()

  document.getElementById("proof").textContent="Proof: " + JSON.stringify(proof);

  var verificationResult = await my_contract.methods.verifyTx(proof[0], proof[1]).call()
  if(verificationResult)
  {
    alert("Success!");
  }
}

const mintWithProof = async () => {
  var proof = await getProof()
  const result = await my_contract.methods.mintWithProof(proof[0], proof[1])
  .send({ from: accounts[0], gas: 0, value: 0 })
  .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)
  });
}
```

💡 En este caso estamos usando el `NETWORK_ID` 534353 que representa a Scroll Alpha. Si no estás usando Scroll cambialo al que estés utilizando.

Ahora podemos correr la interfaz web.

```bash
lite-server
```

Completamos el sudoku y hacemos click en "Mint".

![Mint](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8sgu69v1vx9engklwmci.png)

Una vez confirmada la transacción podemos ver el token premio en Metamask si agregamos el contrato lanzado como asset ERC20.

**¡Gracias por ver este tutorial!**

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.