DEV Community

Ahmed Castro
Ahmed Castro

Posted on • Edited on

Tu primer circuito zkSNARK [ZK ESP Semana2]

Image description

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 NFT como premio por hacerlo.

Antes de comenzar

Para este tutorial ocuparás NodeJs que recomiendo descargarlo en Linux via NVM, y también necesitarás Metamask u otra wallet compatible con fondos en Goerli que puedes obtener desde un faucet.

El Zokrates Playground

Una buena manera de iniciar escribiendo circuitos zkSNARKs es por medio del Playground de Zokrates. Desde la ventana de tu browser puedes escribir circuitos y probar enviarle parámetros.

Te invito a que inicies con estos circuitos sencillos:

Circuito que valida que dos números sean diferentes

def main(field a, field b) -> bool {
    assert(a != b);
    return true;
}
Enter fullscreen mode Exit fullscreen mode

Circuito que valida a * b + c == d

def main(field a, field b, field c, field d) -> bool {
    assert(a * b + c == d);
    return true;
}
Enter fullscreen mode Exit fullscreen mode

Zokrates vía WASM

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

<!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>
Enter fullscreen mode Exit fullscreen mode

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.

npm install -g lite-server
Enter fullscreen mode Exit fullscreen mode

Y esto para levantar el servidor.

lite-server
Enter fullscreen mode Exit fullscreen mode

Accedemos al servidor web desde la url 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".

// 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 duplicates == 0;
}
Enter fullscreen mode Exit fullscreen mode

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 NFTs

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 NFT por completar la respuesta.

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

Primero importamos las librerías de OpenZeppelin necesarias. Por un lado las del ERC721 para hacer NFTs y el de Counters para los IDs de los NFTs. 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.

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

// SPDX-License-Identifier: MIT

contract ZKSudokuNFT is Verifier, ERC721 {
    address URISetter;
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
    string URI = "https://raw.githubusercontent.com/Turupawn/ZokratesSudokuExample/master/assets/metadata.json";
    constructor() ERC721("ZK Sudoku Token", "ZKST") {
        URISetter = msg.sender;
    }

    function setURI(string memory _URI) public
    {
        require(msg.sender == URISetter);
        URI = _URI;
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
        return URI;
    }

    function mintWithProof(Proof memory proof, uint[7] memory input) public
    {
        require(verifyTx(proof, input));
        _mint(msg.sender, _tokenIds.current());
        _tokenIds.increment();
    }
}
Enter fullscreen mode Exit fullscreen mode

💡 Puedes observer que en este contrato establecimos la metadata URI que apunta hacia un nombre, descripción e imágen específica para todos los NFTs minteados. Tú puedes cambiarlo a tu gusto.

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.

index.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 NFT" onclick="_mintNFT()"></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 _mintNFT()
  {
    mintWithProof()
  }
</script>
Enter fullscreen mode Exit fullscreen mode

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.

blockchain_stuff.js

const NETWORK_ID = 5

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 Goerli";
      }
    });
  };
  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)
  });
}
Enter fullscreen mode Exit fullscreen mode

💡 En este caso estamos usando el NETWORK_ID 5 que representa a Goerli. Si no estás usando Goerli cambialo al que estés utilizando.

Ahora podemos correr la interfaz web.

lite-server
Enter fullscreen mode Exit fullscreen mode

Completamos el sudoku y hacemos click en "Mint NFT".

Mint NFT

Una vez confirmada la transacción podemos verlo Opensea.

ZK Proof NFT OpenSea

Para la siguiente semana

En la tercer semana haremos circuitos usando el lenguaje de Aztec Noir una nueva tecnología similar a Zokrates que pone en nuestras manos una manera diferente de escribir circuitos. Hablaremos de las novedades de Aztec y escribiremos circuitos usando este lenguaje similar a Rust.

Para la siguiente semana únicamente necesitaremos traer npm instalado. Te recomiendo instalar la versión 18.16.0 pues es la que mejor me ha funcionado:

Top comments (0)