Demostraremos una exploración paso a paso de una zk-dapp básica diseñada para verificar sumas. Esta aplicación permite a los usuarios demostrar que la suma de dos números, X e Y, es igual a Z sin revelar los números reales en la blockchain. Aunque resolver este problema no requiere necesariamente pruebas de conocimiento cero (zero-knowledge proofs), las utilizaremos en este ejemplo para mantener la simplicidad y mejorar la comprensión.
Sumerjámonos en el código, o salta para ejecutar la dapp.
Comenzaremos clonando el repositorio noir-zk-fullstack-example localmente;
git clone https://github.com/RareSkills/noir-zk-fullstack-example.git
NOTA: Para entender el código de manera efectiva, es necesario tener conocimientos prácticos de noir y typescript.
Instalación de dependencias
Ya tenemos versiones específicas indicadas en el archivo package.json. Para instalar, ejecuta:
npm install
No uses yarn ya que es incapaz de recuperar las versiones específicas de NPM que se requieren.
Despliegue (Backend)
Para construir el proyecto y desplegar los contratos localmente, es necesario iniciar una EVM de desarrollo local en http://localhost:8545. Para lograr esto, primero cambiamos el nombre del archivo .env.example a .env, luego abrimos una nueva terminal para ejecutar el siguiente comando:
npx hardhat node
Tienes la flexibilidad de elegir diferentes redes para ejecutar. Para hacerlo, necesitas hacer los siguientes ajustes: en primer lugar, modifica el contenido del archivo .env añadiendo la clave privada del desplegador (deployer) y la clave API de Alchemy. Posteriormente, navega al archivo hardhat.config.ts e incluye una nueva configuración de red.
Una vez hecho esto, puedes desplegar usando la variable de entorno NETWORK para especificar la red deseada. Por ejemplo NETWORK=mumbai npm run build o NETWORK=sepoia npm run build. Para el propósito de esta dapp, estaremos desplegando localmente usando este comando:
NETWORK=localhost npm run build
Ejecutar el comando mencionado anteriormente desencadena la ejecución de cuatro comandos adicionales en el orden mencionado:
- hardhat run scripts/genContract.ts
- hardhat compile
- hardhat run --network ${NETWORK} scripts/deploy.ts
- next build
¿Qué sucede cuando se ejecutan?
-
hardhat run scripts/genContract.ts
import { NoirServer } from '../utils/noir/noirServer'; async function main() { const noir = new NoirServer();await noir.compile(); noir.getSmartContract() process.exit(); } // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. main().catch(error => { console.error(error); process.exitCode = 1; });La ejecución del script
genContract.tsllama al método compile() de la clase NoirServer, el cual compila el circuito de noir escrito en el directorio./circuits/srcy también genera la ACIR (Abstract Circuit Intermediate Representation). También inicializa this.prover y this.verifier llamando a setup_generic_prover_and_verifier de barretenberg con la ACIR generada.async compile() { // I'm running on the server so I can use the file system initialiseResolver((id: any) => { try { const code = fs.readFileSync(`circuits/src/${id}`, { encoding: 'utf8' }) as string; return code } catch (err) { console.error(err); throw err; } }); const compiled_noir = compile({ entry_point: 'main.nr', }); this.compiled = compiled_noir; this.acir = acir_read_bytes(this.compiled.circuit); [this.prover, this.verifier] = await setup_generic_prover_and_verifier(this.acir); };Adicionalmente, se llama al método getSmartContract, el cual genera un contrato en solidity en
./contract/plonk_vk.sol. Este contrato se crea como parte del proceso de ejecución.getSmartContract() { const sc = this.verifier.SmartContract(); // The user must have a folder called 'contract' in the root directory. If not, we create it. if (!fs.existsSync(path.join(__dirname, '../../contract'))) { console.log('Contract folder does not exist. Creating...'); fs.mkdirSync(path.join(__dirname, '../../contract')); } // If the user already has a file called 'plonk_vk.sol' in the 'contract' folder, we delete it. if (fs.existsSync(path.join(__dirname, '../../contract/plonk_vk.sol'))) { fs.unlinkSync(path.join(__dirname, '../../contract/plonk_vk.sol')); } // We write the contract to a file called 'plonk_vk.sol' in the 'contract' folder. fs.writeFileSync(path.join(__dirname, '../../contract/plonk_vk.sol'), sc, { flag: 'w', }); return sc; } -
hardhat compile
Este comando compila el/los contrato(s) ubicados en el directorio
./contract. En este caso específico, compila el contrato plonk_vk.sol. -
hardhat run --network ${NETWORK} scripts/deploy.ts
import { writeFileSync } from 'fs'; import { ethers } from 'hardhat'; async function main() { // Deploy the verifier contractconst Verifier = await ethers.getContractFactory('TurboVerifier'); const verifier = await Verifier.deploy(); // Get the address of the deployed verifier contract const verifierAddr = await verifier.deployed(); // Create a config object const config = { chainId: ethers.provider.network.chainId, verifier: verifierAddr.address, }; // Print the config console.log('Deployed at', config); writeFileSync('utils/addresses.json', JSON.stringify(config), { flag: 'w' }); process.exit(); } // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. main().catch(error => { console.error(error); process.exitCode = 1; });Este script despliega plonk_vk.sol en la red asignada a la variable de entorno NETWORK y escribe la dirección desplegada en
./utils/addresses.json. -
next build
Genera una versión optimizada de nuestra aplicación para producción.
Calcular Prueba (Frontend)
Para iniciar un servidor de desarrollo, ejecuta el siguiente comando:
npm run dev
Navega a http://localhost:3000 en tu navegador web. Conecta tu billetera MetaMask a la dapp y cambia tu red de MetaMask a la red Localhost. Luego, proporciona dos valores de entrada (input) y haz clic en el botón Calculate proof. Esto iniciará el cálculo de la prueba y también la verificará onchain.
Dentro del directorio ./components, el archivo components.tsx contiene dos funciones notables que manejan esas acciones:
- calculateProof
- verifyProof
El código está comentado para su comprensión.
1. calculateProof
// Calculates proof
const calculateProof = async () => {
// only launch if we do have an acir to calculate the proof from
// set a pending state to show a spinner
setPending(true);
if (input.x == "" || input.y == "") {
toast.error('Fields can not be empty!');
setPending(false);
} else {
// launching a new worker for the proof calculation
const worker = new Worker(new URL('../utils/prover.ts', import.meta.url));
// handling the response from the worker
worker.onmessage = e => {
if (e.data instanceof Error) {
toast.error('Error while calculating proof');
setPending(false);
} else {
toast.success('Proof calculated');
setProof(e.data);
setPending(false);
}
};
// sending the acir and input to the worker
worker.postMessage({ input });
}
};
Primero comprueba si los campos de input no están vacíos. Si los campos de input contienen valores, procede a enviar esas entradas al worker recién creado y también llama a la función onmessage en el archivo ./utils/prover.ts.
// @ts-ignore
import { NoirBrowser } from '../utils/noir/noirBrowser';
// // Add an event listener for the message event
onmessage = async event => {
try {
const { input } = event.data;
const hexInputObj = Object.entries(input).reduce((newObj, [key, value]) => {
newObj[key] = (value as number).toString(16).padStart(2, '0');
return newObj;
}, {});
const noir = new NoirBrowser();
await noir.compile();
const proof = await noir.createProof({ input: hexInputObj })
console.log(hexInputObj)
postMessage(proof);
} catch (er) {
console.log(er);
postMessage(er);
} finally {
close();
}
};
Para crear la prueba, se invoca la función create_proof de la biblioteca barretenberg. Acepta tres argumentos: el objeto this.prover, acir, y input.
async createProof({input} : {input: any}) {
const proof = await create_proof(this.prover, this.acir, input);
return proof;
}
1. verifyProof
const verifyProof = async () => {
// only launch if we do have an acir and a proof to verify
if (proof) {
// launching a new worker for the verification
const worker = new Worker(new URL('../utils/verifier.ts', import.meta.url));
console.log('worker launched');
// handling the response from the worker
worker.onmessage = async e => {
if (e.data instanceof Error) {
toast.error('Error while verifying proof');
} else {
toast.success('Proof verified');
// Verifies proof on-chain
const ethers = new Ethers();
const ver = await ethers.contract.verify(proof);
if (ver) {
toast.success('Proof verified on-chain!');
setVerification(true);
} else {
toast.error('Proof failed on-chain verification');
setVerification(false);
}
}
};
// sending the acir and proof to the worker
worker.postMessage({ proof });
}
};
Primero comprueba si hay una prueba disponible para su verificación. Si existe una prueba, lanza un nuevo worker. Luego, la prueba se envía al worker, y se llama a la función onmessage en el archivo ./utils/verifier.ts para procesar la prueba entrante llamando a la función verify_proof de barretenberg.
async verifyProof({proof} : {proof: any}) {
const verification = await verify_proof(this.verifier, proof);
return verification;
}
Si todo el proceso funciona como está previsto, la función debería devolver true.
Ejecutar la dApp
1. Clonar el repositorio localmente
git clone https://github.com/RareSkills/noir-zk-fullstack-example.git
2. Instalar dependencias
npm install
3. Iniciar una EVM de desarrollo local en http://localhost:8545
cambia el nombre del archivo .env.example a .env, luego abre una nueva terminal para ejecutar el siguiente comando:
npx hardhat node
4. Construir el proyecto
NETWORK=localhost npm run build
5. Iniciar el servidor de desarrollo
npm run dev
Abre http://localhost:3000 en tu navegador web. Conecta tu billetera MetaMask a la dapp y cambia tu red de MetaMask a la red Localhost.
Si no puedes encontrarla, abre la configuración de redes de MetaMask, añade una nueva red y configúrala con los siguientes detalles:
- Network name
- Localhost 8545
- New RPC URL
- http://localhost:8545
- Chain ID - 1337 - Currency Symbol - ETH
Guarda la configuración de la red, cambia tu billetera MetaMask a la red Localhost y prueba la dApp :)
Conclusión
Debido al desarrollo activo de Noir, el proyecto experimenta frecuentemente actualizaciones y mejoras. En consecuencia, no es raro que las últimas versiones de varios paquetes presenten incompatibilidades entre sí.
Mantenerse al día con las últimas notas de la versión (release notes) y las discusiones de la comunidad es crucial para evitar posibles problemas causados por incompatibilidades de versiones y garantizar una experiencia de desarrollo fluida.
Aprende Más
Para más información sobre programación de conocimiento cero, consulta nuestro curso de conocimiento cero. Para desarrollo avanzado de contratos inteligentes, consulta nuestro Solidity Bootcamp.
Publicado originalmente el 28 de mayo de 2023