[Aztec Noir + Scroll] Crea ZK DApps fácil
Published: 2023-11-08 00:02:45

---
title: [Aztec Noir + Scroll] Crea ZK DApps fácil
published: true
description: 
tags: 
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xsiiu4wb8lazarq16pbo.png
# Use a ratio of 100:42 for best results.
# published_at: 2023-11-07 18:53 +0000
---

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](https://funny-pasca-841f19.netlify.app/) o ver el [repo en github](https://github.com/Turupawn/NoirScrollDemo/).

## Antes de iniciar

Para este tutorial necesitarás [Metamask](https://metamask.io/), o cualquier otra wallet de tu preferencia, con fondos en Scroll Sepolia que puedes obtener desde la [Sepolia faucet](https://sepoliafaucet.com/) y luego los puedes bridgear a L2 usando el [Scroll Sepolia bridge](https://scroll.io/bridge). También puedes usar una [Scroll Sepolia Faucet](https://docs.scroll.io/en/user-guide/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:

```bash
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
```

En MAC:

```bash
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
```

Para más alternativas visita la [documentación oficial](https://noir-lang.org/getting_started/nargo_installation).

## Step 2. Lanza el verificador

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

```bash
nargo new circuit
cd circuit
nargo compile
cd ..
```

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.

```solidity
// 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;
    }
}
```

## Step 3. El frontend

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

`package.json`
```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"
  }
}
```

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

`vite.config.js`
```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 {};
});
```

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

`index.html`

```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>
```

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`
```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;
```

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

`json_abi/VerificationCounter.json`
```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"
	}
]
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fswzb7ogf0a72573qsxb.png)


Para mas información, visita la [documentación oficial de Noir](https://noir-lang.org/noir_js/getting_started/tiny_noir_app/).

**¡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.