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
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
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 ..
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;
}
}
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"
}
}
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 {};
});
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>
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;
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"
}
]
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.
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)