DEV Community

Cover image for [Aztec Noir + Scroll] Crea ZK DApps fácil
Ahmed Castro
Ahmed Castro

Posted on

[Aztec Noir + Scroll] Crea ZK DApps fácil

ZK es una tecnología muy prometedora que permite escalabilidad y privacidad on-chain. En los últimos meses, las herramientas de desarrollo de ZK han mejorado significativamente. Esto significa que ahora es posible para nosotros, los desarrolladores, crear ZK DApps con una mejor experiencia de usuario. En este tutorial, crearemos una ZK DApp muy sencilla con privacidad habilitada, donde demostraremos que X y Y son números diferentes pero sin revelar X.

En esta guía, utilizaremos Scroll Sepolia Testnet para la verificación on-chain, ya que es más económico y fácil de usar. Si deseas utilizar otra chain, te explicaré los cambios exactos necesarios. Además, utilizaremos Noir como un lenguaje de circuitos DSL. Noir ofrece un buen soporte de WASM (necesario para generar pruebas desde el Browser) y soporte ECDSA (necesario para anonimizar, por ejemplo, una cuenta de Metamask). Estoy muy emocionado por el soporte ECDSA de Noir, así que lo cubriremos en una futura guía. Por eso, recuerda suscribirte a este blog y también en YouTube.

tl;dr?

También puedes probar el demo o ver el repo en github.

Antes de iniciar

Para este tutorial necesitarás Metamask, o cualquier otra wallet de tu preferencia, con fondos en Scroll Sepolia que puedes obtener desde la Sepolia faucet y luego los puedes bridgear a L2 usando el Scroll Sepolia bridge. También puedes usar una Scroll Sepolia Faucet para recibir fondos directamente en L2.

Step 1. Instalar Nargo

Para crear circuitos con Noir, necesitarás Nargo. En esta guía, utilizaremos Nargo v17, así que asegúrate de instalar esa versión específica. Puedes instalarla ejecutando los siguientes comandos.

En Linux:

mkdir -p $HOME/.nargo/bin && \
curl -o $HOME/.nargo/bin/nargo-x86_64-unknown-linux-gnu.tar.gz -L https://github.com/noir-lang/noir/releases/download/v0.17.0/nargo-x86_64-unknown-linux-gnu.tar.gz && \
tar -xvf $HOME/.nargo/bin/nargo-x86_64-unknown-linux-gnu.tar.gz -C $HOME/.nargo/bin/ && \
echo 'export PATH=$PATH:$HOME/.nargo/bin' >> ~/.bashrc && \
source ~/.bashrc
Enter fullscreen mode Exit fullscreen mode

En MAC:

mkdir -p $HOME/.nargo/bin && \
curl -o $HOME/.nargo/bin/nargo-x86_64-apple-darwin.tar.gz -L https://github.com/noir-lang/noir/releases/download/v0.17.0/nargo-x86_64-apple-darwin.tar.gz && \
tar -xvf $HOME/.nargo/bin/nargo-x86_64-apple-darwin.tar.gz -C $HOME/.nargo/bin/ && \
echo '\nexport PATH=$PATH:$HOME/.nargo/bin' >> ~/.zshrc && \
source ~/.zshrc
Enter fullscreen mode Exit fullscreen mode

Para más alternativas visita la documentación oficial.

Step 2. Lanza el verificador

Crea un circuito de ejemplo, compílalo, genera el verificador en Solidity y luego regresa al directorio original.

nargo new circuit
cd circuit
nargo compile
cd ..
Enter fullscreen mode Exit fullscreen mode

El verificador en Solidity debería estar ahora ubicado en circuit/contract/circuit/plonk_vk.sol. Lánzalo y luego lanza el siguiente contrato pasando la dirección del verificador que acabas de lanzar como parámetro en el constructor.

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

interface IUltraVerifier {
  function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool);
}

contract VerificationCounter
{
    uint public verifyCount;

    IUltraVerifier ultraVerifier;
    constructor(address ultraVerifierAddress)
    {
        ultraVerifier = IUltraVerifier(ultraVerifierAddress);
    }

    function sendProof(bytes calldata _proof, bytes32[] calldata _publicInputs) public
    {
        ultraVerifier.verify(_proof, _publicInputs);
        verifyCount+=1;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3. El frontend

Comencemos configurando nuestro archivo package.json e instalando las dependencias ejecutando npm install.

package.json

{
  "name": "verification-counter",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "vite --open",
    "build": "vite build",
    "preview": "vite preview"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@noir-lang/backend_barretenberg": "^0.17.0",
    "@noir-lang/noir_js": "^0.17.0"
  },
  "devDependencies": {
    "rollup-plugin-copy": "^3.5.0",
    "vite": "^4.5.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

A continuación, configuramos el archivo de configuración de Vite necesario para ejecutar el servidor web.

vite.config.js

import { defineConfig } from 'vite';
import copy from 'rollup-plugin-copy';
import fs from 'fs';
import path from 'path';

const wasmContentTypePlugin = {
  name: 'wasm-content-type-plugin',
  configureServer(server) {
    server.middlewares.use(async (req, res, next) => {
      if (req.url.endsWith('.wasm')) {
        res.setHeader('Content-Type', 'application/wasm');
        const newPath = req.url.replace('deps', 'dist');
        const targetPath = path.join(__dirname, newPath);
        const wasmContent = fs.readFileSync(targetPath);
        return res.end(wasmContent);
      }
      next();
    });
  },
};

export default defineConfig(({ command }) => {
  if (command === 'serve') {
    return {
      plugins: [
        copy({
          targets: [{ src: 'node_modules/**/*.wasm', dest: 'node_modules/.vite/dist' }],
          copySync: true,
          hook: 'buildStart',
        }),
        command === 'serve' ? wasmContentTypePlugin : [],
      ],
    };
  }

  return {};
});
Enter fullscreen mode Exit fullscreen mode

Todas las interacciones en el frontend se realizarán a través del siguiente archivo HTML.

index.html

<!DOCTYPE html>
<body>
  <h1>Aztec Noir - Scroll Demo</h1>
  <h2><i>Prove that X and Y are not equal</i></h2>
  <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>

  x: <input type="input"  value="" id="_x"></input>
  y: <input type="input"  value="" id="_y"></input>
  <input type="button" value="Send Poof" onclick="_sendProof()"></input>

  <p id="public_input"></p>
  <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://cdn.jsdelivr.net/gh/ethereumjs/browser-builds/dist/ethereumjs-tx/ethereumjs-tx-1.3.3.min.js"></script>
  <script type="module" src="/app.js"></script>
</body>

<script>
  function _sendProof()
  {
    _x = document.getElementById("_x").value
    _y = document.getElementById("_y").value
    sendProof(_x, _y)
  }
</script>

</html>
Enter fullscreen mode Exit fullscreen mode

Y toda la lógica de web3 y zk estará en el siguiente archivo JavaScript.

Ten en cuenta que debes cambiar VERIFIERCOUTNERADDRESS por el contrato VerifierCounter que acabas de lanzar. Además, si deseas utilizar este frontend en una cadena diferente a Scroll Sepolia, solo tienes que cambiar la variable NETWORK_ID de 534351 al ID de la cadena que desees.

app.js

import { BarretenbergBackend } from '@noir-lang/backend_barretenberg';
import { Noir } from '@noir-lang/noir_js';
import circuit from './circuit/target/circuit.json';

const NETWORK_ID = 534351

const MY_CONTRACT_ADDRESS = "VERIFIERCOUTNERADDRESS"
const MY_CONTRACT_ABI_PATH = "./json_abi/VerificationCounter.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: Please connect to 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();
  var 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 Sepolia";
      }
    });
  };
  awaitWeb3();
}

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

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

const onWalletConnectedCallback = async () => {
}

document.addEventListener('DOMContentLoaded', async () => {
    loadDapp()
});

const sendProof = async (x, y) => {
    const backend = new BarretenbergBackend(circuit);
    const noir = new Noir(circuit, backend);
    const input = { x: x, y: y };
    document.getElementById("web3_message").textContent="Generating proof... ⌛"
    var proof = await noir.generateFinalProof(input);
    document.getElementById("web3_message").textContent="Generating proof... ✅"
    proof = "0x" + ethereumjs.Buffer.Buffer.from(proof.proof).toString('hex')
    y = ethereumjs.Buffer.Buffer.from([y]).toString('hex')
    y = "0x" + "0".repeat(64-y.length) + y

    document.getElementById("public_input").textContent = "public input: " + y
    document.getElementById("proof").textContent = "proof: " + proof

    const result = await my_contract.methods.sendProof(proof, [y])
    .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)
    });
}
window.sendProof=sendProof;
Enter fullscreen mode Exit fullscreen mode

Por último, no olvides agregar tu ABI JSON en el siguiente archivo.

json_abi/VerificationCounter.json

[
    {
        "inputs": [
            {
                "internalType": "bytes",
                "name": "_proof",
                "type": "bytes"
            },
            {
                "internalType": "bytes32[]",
                "name": "_publicInputs",
                "type": "bytes32[]"
            }
        ],
        "name": "sendProof",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "address",
                "name": "ultraVerifierAddress",
                "type": "address"
            }
        ],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [],
        "name": "verifyCount",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    }
]
Enter fullscreen mode Exit fullscreen mode

Deberías estar listo para lanzar el servidor ahora.

Step 4. ¡Envía pruebas!

Inicia el servidor ejecutando npm start. Ahora podrás enviar pruebas siempre que x e y no sean iguales. El contrato contará cada prueba exitosa que se envíe.

Noir WASM Proving on Scroll Sepolia Testnet

Para mas información, visita la documentación oficial de Noir.

¡Gracias por ver este tutorial!

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

Top comments (0)