Interfaces con privacidad en Solidity y zk-WASM

Ahmed Castro - Jun 25 - - Dev Community

Los usuarios de blockchain necesitan privacidad en sus finanzas, identidad, redes sociales y más. Pero web3 es transparente y público. Entonces, ¿cómo pueden los usuarios proteger su anonimato en un ambiente con estas características?

La clave es crear pruebas de computación en un lugar donde únicamente el usuario tiene acceso, donde los datos del usuario estén seguros. Ese lugar es precisamente el navegador, antes que los datos del usuarios toquen el internet. A esto le llamamos generación de pruebas a nivel de cliente, "client side proving", o "browser proving" en inglés.

diagrama aplicacion zk

Para mantener los parámetros privados, estos nunca deben salir de nuestro navegador

Conozcamos, con un ejemplo práctico y sencillo, cómo crear interfaces que hagan uso de zk-wasm, la tecnología que hace esto posible.

Dependencias

Para este ejemplo usaremos circom, si no lo tienes instalado puedes hacerlo con los comandos a continuación.

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

1. Crea un circuito

Haremos un ejemplo muy sencillo: generar una prueba de computación de una multiplicación a*b=c pero manteniendo privados a y b. Si te interesa un ejemplo más avanzado con un caso de uso real visita mi artículo anterior.

Circom nos permite crear circuitos, que permiten generar pruebas de ejecución ofuscando los parámetros.

Comienza creando el circuito a continuación.

myCircuit.circom

pragma circom 2.0.0;

template Multiplier() {
    signal input a;
    signal input b;
    signal output c;
    c <== a*b;
 }

 component main = Multiplier();
Enter fullscreen mode Exit fullscreen mode

Ahora compílalo y genera los artefactos que usaremos más adelante.

circom myCircuit.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 myCircuit.r1cs pot12_final.ptau myCircuit_0000.zkey
snarkjs zkey contribute myCircuit_0000.zkey myCircuit_0001.zkey --name="1st Contributor Name" -v
snarkjs zkey export verificationkey myCircuit_0001.zkey verification_key.json
Enter fullscreen mode Exit fullscreen mode

2. Lanza los contratos

El siguiente comando generará un contrato verificador en el archivo de verifier.sol. Lánzalo en un blockchain de preferencia. Este contrato contiene la función verifyProof() que recibe por parámetro una prueba de computación hecha con nuestro circuito y devuelve true si la prueba es correcta.

Nota: Este contrato es compatible con L1 EVMs, L2 optimistas, pero en términos de L2 ZK, actualmente únicamente es compatible con Scroll.

snarkjs zkey export solidityverifier myCircuit_0001.zkey verifier.sol
Enter fullscreen mode Exit fullscreen mode

Ahora lanza el siguiente contrato de lógica personalizada pasando como parámetro de constructor el address del contrato verificador que lanzamos anteriormente. En este contrato puedes agregar la lógica en solidity que desees, por ejemplo el conteo de votos de un sistema de votación o la recepción o envío de tokens ERC20 en un sistema DeFi anónimo. En este ejemplo únicamente almacenaremos el resultado de la multiplicación que hicimos en nuestro circuito.

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

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

contract CircomCustomLogic {
    ICircomVerifier circomVerifier;
    uint public publicInput;

    constructor(address circomVeriferAddress) {
        circomVerifier = ICircomVerifier(circomVeriferAddress);
    }

    function sendProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[1] calldata _pubSignals) public {
        // ZK verification
        circomVerifier.verifyProof(_pA, _pB, _pC, _pubSignals);

        // Your custom logic
        publicInput = _pubSignals[0];
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Construye un frontend

Ahora crea la siguiente estructura de archivos:

js/
  blockchain_stuff.js
  snarkjs.min.js
json_abi/
  MyContract.json
zk_artifacts/
  myCircuit_final.zkey
  myCircuit.wasm
  verification_key.json
index.html
Enter fullscreen mode Exit fullscreen mode
  • js/snarkjs.min.js: descarga este archivo que contiene la librería de snark.js
  • json_abi/MyContract.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.
  • zk_artifacts: coloca en esta carpeta los artefactos generados anteriormente. Nota: Cambia el nombre de myCircuit_0002.zkey por myCircuit_final.zkey
  • index.html y js/blockchain_stuff.js los detallo a continuación

El archivo HTML a continuación describe la interfaz gráfica donde colocaremos los números a multiplicar. En un ambiente de producción recomendaría usar un frontend framework como react, vue o angular. Este ejemplo es con fines didácticos.

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>
  <p id="contract_state"></p>
  <input type="input"  value="" id="a"></input>
  <input type="input"  value="" id="b"></input>
  <input type="button" value="Send Proof" onclick="_sendProof()"></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/blockchain_stuff.js"></script>
  <script type="text/javascript" src="js/snarkjs.min.js"></script>
</body>
</html>

<script>
  function _sendProof()
  {
    a = document.getElementById("a").value
    b = document.getElementById("b").value
    sendProof(a, b)
  }
</script>
Enter fullscreen mode Exit fullscreen mode

Nuestro archivo de javascript contiene tanto la lógica de generación de pruebas zk usando la librería snark.js como la lógica de blockchain usando la librería web3.js. En un ambiente de producción recomendaría usar typescript y no javascript vainilla, este ejemplo es con fines didácticos.

js/blockchain_stuff.js

const NETWORK_ID = 534351

const MY_CONTRACT_ADDRESS = "0xFdAFc996a60bC5fEB307AAF81b1eD0A34a954F06"
const MY_CONTRACT_ABI_PATH = "./json_abi/MyContract.json"
var my_contract

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() {
  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 Testnet";
      }
    });
  };
  awaitWeb3();
}

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

loadDapp()

const onContractInitCallback = async () => {
  var publicInput = await my_contract.methods.publicInput().call()
  var contract_state = "Public input: " + publicInput
  document.getElementById("contract_state").textContent = contract_state;
}

const onWalletConnectedCallback = async () => {
}


//// Functions ////

const sendProof = async (a, b) => {
  document.getElementById("web3_message").textContent="Generating proof...";

  const { proof, publicSignals } = await snarkjs.groth16.fullProve( { a: a, b: b}, "../zk_artifacts/myCircuit.wasm", "../zk_artifacts/myCircuit_final.zkey");

  const vkey = await fetch("../zk_artifacts/verification_key.json").then( function(res) {
    return res.json();
  });

  const res = await snarkjs.groth16.verify(vkey, publicSignals, proof);

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

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

  const result = await my_contract.methods.sendProof(pA, pB, pC, publicSignals)
  .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

4. Prueba la aplicación

Antes de probar, debes adaptar las variables NETWORK_ID y MY_CONTRACT_ADDRESS en js/blockchain_stuff.js. NETWORK_ID es el identificador único de la chain que estés usando, en este ejemplo estoy usando 534351 que representa a Scroll Sepolia Testnet, si deseas usar otro te recomiendo buscar el identificador en chainlist. Por otro coloca el address del contrato CircomCustomLogic que recién lanzaste en la variable MY_CONTRACT_ADDRESS.

Ahora estás listo para probar la aplicación en cualquier servidor web. Usualmente yo uso lite-server para desarrollar, así lo instalas y levantas un servidor, solo asegúrate de estar en la carpeta del proyecto:

npm install -g lite-server #para instalar
lite-server #para levantar el servidor
Enter fullscreen mode Exit fullscreen mode

ejemplo de circuito multiplicador zk
Una vez todo esté listo, tu aplicación debería de verse de esta manera

¿Qué más necesito aprender para desarrollar una zkDapp?

Para desarrollar una aplicación descentralizada y anónima necesitarás una combinación de conocimientos en Circom, Solidity y programación web. Dependiendo de los casos de uso también ocuparás un poco de programación backend en el caso de necesitar un Relayer. Estaré creando guías sobre estos temas así que te invito a que te suscribas.

También te dejo un par de materiales de aprendizaje como siguiente paso:

¡Gracias por leer esta guía!

Sígueme en dev.to y en Youtube para todo lo relacionado al desarrollo en Blockchain en Español.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .