Logo
Nuevos usuarios de RPC: 35% de descuento el primer mesVer oferta
OnFinality Learn
Integración y Desarrollo12 min de lectura

Pruebas de conocimiento cero (ZKPs): guía práctica de implementación y verificación

Aprende cómo funcionan las pruebas de conocimiento cero en la práctica, desde el diseño de circuitos hasta la verificación en cadena, con código ejecutable y procedimientos de diagnóstico.

TL;DR

Las pruebas de conocimiento cero (ZKPs) permiten que un probador convenza a un verificador de que una afirmación es verdadera sin revelar el secreto subyacente. Este artículo explica el modelo mental central y luego recorre un flujo de trabajo de implementación reproducible: definir un circuito, generar una prueba fuera de la cadena y verificarla en la cadena o en un servicio backend. Encontrarás código ejecutable para la generación y verificación de pruebas, un procedimiento de diagnóstico para fallos comunes y orientación sobre cuándo las ZKPs son la herramienta adecuada frente a cuándo bastan compromisos más simples. El enfoque está en la metodología y la verificación, no en respaldar un sistema de prueba o proveedor de endpoints específico.

Alcance y supuestos para una implementación de ZKP

Esta guía asume que eres un desarrollador familiarizado con criptografía básica (funciones hash, firmas de clave pública) y tienes acceso a un endpoint RPC de blockchain para la verificación en cadena. No necesitas experiencia previa con circuitos aritméticos. El objetivo es pasar de la definición abstracta —un probador convence a un verificador de que una afirmación es verdadera sin revelar por qué— a un pipeline funcional que puedas probar y medir.

Usaremos un flujo de trabajo simplificado de zk-SNARK con Circom y snarkjs, que son ampliamente utilizados para verificadores compatibles con Ethereum. El mismo modelo mental se aplica a otros sistemas de prueba (STARKs, Bulletproofs, Halo2), aunque las herramientas y los tamaños de prueba difieren. Todo el código usa marcadores de posición como RPC_URL y PRIVATE_KEY; nunca confirmes claves reales.

  • Node.js 18+ y npm instalados
  • Familiaridad básica con JavaScript y herramientas de línea de comandos
  • Un endpoint RPC para la cadena de destino (usa /en/api-service para comparar opciones)
  • Una cuenta de testnet con ETH de prueba para el despliegue de contratos

El modelo mental: probador, verificador y el testigo

Una prueba de conocimiento cero tiene tres roles: el probador, el verificador y la afirmación. La afirmación es una declaración pública, como “Conozco una entrada secreta que produce este valor público al aplicarle hash”. El testigo es la entrada secreta. El probador ejecuta un cálculo que produce una prueba; el verificador comprueba la prueba contra la afirmación pública. La prueba debe satisfacer completitud (las afirmaciones verdaderas se aceptan), solidez (las afirmaciones falsas se rechazan) y conocimiento cero (la prueba no revela nada sobre el testigo).

En contextos de blockchain, el verificador suele ser un contrato inteligente. El contrato no vuelve a ejecutar el cálculo; solo verifica una prueba sucinta. Por eso las ZKPs permiten la computación verificable: puedes demostrar que un cálculo fuera de la cadena se realizó correctamente sin obligar a cada nodo a repetirlo. La contrapartida es que generar pruebas es computacionalmente costoso, mientras que la verificación es barata.

De la afirmación al circuito: definición de restricciones

Un circuito es un conjunto de restricciones aritméticas sobre un campo finito. Cada restricción es una ecuación que debe cumplirse para el testigo. Por ejemplo, para demostrar el conocimiento de una preimagen x tal que hash(x) = h, implementarías la función hash como un circuito y restringirías su salida para que sea igual a la h pública. El probador proporciona x; el circuito verifica el hash internamente.

El diseño de circuitos es donde la mayoría de los proyectos ZKP invierten su esfuerzo. Las restricciones son costosas: más restricciones implican claves de prueba más grandes y una generación de pruebas más lenta. Debes minimizar las restricciones utilizando funciones hash eficientes (por ejemplo, Poseidon en lugar de SHA-256 dentro del circuito) y evitando comprobaciones de rango innecesarias. Las entradas públicas son los valores que ve el verificador; todo lo demás es privado.

// example.circom
pragma circom 2.0.0;

include "circomlib/circuits/poseidon.circom";

template PreimageProof() {
    signal input preimage;
    signal input hash;
    component hasher = Poseidon(1);
    hasher.inputs[0] <== preimage;
    hash === hasher.out;
}

component main = PreimageProof();

Compilación del circuito y generación de una configuración de confianza

Después de escribir el circuito, lo compilas a un Sistema de Restricciones de Rango 1 (R1CS) y a un generador de testigos WebAssembly. Luego ejecutas una configuración de confianza para producir claves de prueba y verificación. Para desarrollo, una ceremonia local de Powers of Tau es suficiente; para producción, debes usar una ceremonia multipartita o un sistema de prueba transparente para evitar un único punto de residuo tóxico.

Los comandos a continuación asumen que tienes circom y snarkjs instalados globalmente. Los archivos de salida son circuit.r1cs, circuit.wasm y circuit.zkey. Mantén el archivo .zkey seguro; es necesario para la generación de pruebas.

circom example.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 example.r1cs pot12_final.ptau circuit_0000.zkey
snarkjs zkey contribute circuit_0000.zkey circuit_final.zkey --name="My contribution" -v
snarkjs zkey export verificationkey circuit_final.zkey verification_key.json

Generación de una prueba fuera de la cadena con JavaScript

La generación de pruebas toma el testigo (entradas privadas) y las entradas públicas, ejecuta el circuito y produce una prueba y señales públicas. El siguiente código usa snarkjs en Node.js. Lee el calculador de testigos desde el WASM compilado, calcula el testigo y genera una prueba Groth16. La prueba es un objeto JSON que se puede enviar a un contrato verificador o a un servicio backend.

Ten en cuenta que la generación de pruebas puede tardar de segundos a minutos según el tamaño del circuito. Para circuitos grandes, es posible que necesites un servicio de prueba dedicado o una máquina con memoria suficiente. El ejemplo usa un circuito pequeño para fines de demostración.

const snarkjs = require("snarkjs");
const fs = require("fs");

async function generateProof() {
  const wasm = "example_js/example.wasm";
  const zkey = "circuit_final.zkey";
  const input = { preimage: 12345, hash: "0x..." }; // replace with actual Poseidon hash
  const { proof, publicSignals } = await snarkjs.groth16.fullProve(input, wasm, zkey);
  fs.writeFileSync("proof.json", JSON.stringify(proof, null, 2));
  fs.writeFileSync("public.json", JSON.stringify(publicSignals, null, 2));
  console.log("Proof generated");
}

generateProof().catch(console.error);

Verificación de la prueba en cadena con un verificador en Solidity

snarkjs puede exportar un contrato verificador en Solidity a partir de la clave de verificación. El contrato expone una función verifyProof que toma la prueba y las señales públicas y devuelve un booleano. Despliegas este contrato en tu cadena de destino y lo llamas desde tu aplicación. El costo de gas de la verificación suele ser de unos pocos cientos de miles de gas, dependiendo del sistema de prueba y del número de entradas públicas.

El siguiente script usa ethers.js para llamar al contrato verificador. Reemplaza RPC_URL, PRIVATE_KEY y VERIFIER_ADDRESS con tus valores. La prueba y las señales públicas se leen de los archivos generados en el paso anterior.

const { ethers } = require("ethers");
const fs = require("fs");

async function verifyOnChain() {
  const provider = new ethers.JsonRpcProvider("RPC_URL");
  const wallet = new ethers.Wallet("PRIVATE_KEY", provider);
  const verifierAddress = "VERIFIER_ADDRESS";
  const abi = ["function verifyProof(uint256[2] a, uint256[2][2] b, uint256[2] c, uint256[] publicSignals) public view returns (bool)"];
  const verifier = new ethers.Contract(verifierAddress, abi, wallet);
  const proof = JSON.parse(fs.readFileSync("proof.json"));
  const publicSignals = JSON.parse(fs.readFileSync("public.json"));
  const isValid = await verifier.verifyProof(proof.pi_a, proof.pi_b, proof.pi_c, publicSignals);
  console.log("Proof valid:", isValid);
}

verifyOnChain().catch(console.error);

Resultado esperado de la verificación y cómo interpretarlo

Cuando la prueba es válida, el contrato verificador devuelve true y la transacción (si envías una) tiene éxito. Si la prueba no es válida, el contrato devuelve false o revierte. Debes probar ambos casos: genera una prueba con el testigo correcto y luego intenta la verificación con una señal pública manipulada. Esta última debería fallar, confirmando la solidez.

Para la verificación fuera de la cadena, snarkjs proporciona un comando verify que comprueba la prueba contra la clave de verificación. Esto es útil para depurar antes del despliegue. La salida esperada es “OK” para una prueba válida y un error para una inválida.

snarkjs groth16 verify verification_key.json public.json proof.json

Procedimiento de diagnóstico para fallos comunes de ZKP

La mayoría de los fallos de ZKP se dividen en tres categorías: errores de generación de testigos, errores de generación de pruebas y discrepancias de verificación. Comienza comprobando que el testigo satisface todas las restricciones. Ejecuta el calculador de testigos directamente e inspecciona la salida. Si el testigo es correcto, asegúrate de que la clave de prueba coincida con el circuito; una discrepancia entre el .zkey y el .r1cs hará que la generación de pruebas falle o produzca pruebas inválidas.

Si la verificación en cadena falla pero la verificación fuera de la cadena tiene éxito, comprueba que las señales públicas estén codificadas correctamente para el verificador en Solidity. Las pruebas Groth16 usan puntos de curva elíptica que deben pasarse como arreglos uint256; un error común es intercambiar el orden de las coordenadas o usar cadenas decimales en lugar de hexadecimales. También verifica que el contrato verificador se exportó desde la misma clave de verificación.

  • Error de testigo: restricción no satisfecha—verifica los tipos de entrada y el módulo del campo.
  • Error de generación de prueba: falta .zkey o circuito incorrecto—regenera las claves.
  • Discrepancia de verificación: orden o codificación de las señales públicas—compara con la salida de snarkjs.
  • Fallo en la estimación de gas: formato de prueba incompatible con el verificador—vuelve a exportar el contrato.

Limitaciones y compensaciones de las ZKPs

Las ZKPs son potentes pero no gratuitas. El tiempo de prueba y la memoria crecen con el tamaño del circuito; los cálculos complejos (por ejemplo, pruebas de Merkle grandes o zkEVM) pueden requerir hardware especializado. Las configuraciones de confianza introducen una suposición de confianza a menos que uses sistemas transparentes. La verificación de pruebas es barata en cadena, pero la prueba en sí debe transmitirse y almacenarse, lo que cuesta gas de calldata.

Las pruebas de conocimiento cero tampoco resuelven la disponibilidad de datos: si el testigo no está disponible, la prueba puede ser válida pero los datos subyacentes no se pueden recuperar. Para muchas aplicaciones, un esquema de compromiso más simple (por ejemplo, un hash) es suficiente y mucho más económico. Usa ZKPs cuando necesites demostrar una propiedad de datos privados sin revelarla, o cuando necesites una verificación sucinta de un cálculo pesado.

Próximos pasos: integración de ZKPs en tu stack

Una vez que tengas un pipeline de pruebas funcional, el siguiente paso es integrarlo con el backend de tu aplicación y los contratos inteligentes. Considera dónde se generan las pruebas (del lado del cliente, del lado del servidor o un servicio de prueba) y cómo manejarás los fallos. Para la verificación en cadena, necesitarás un endpoint RPC confiable; puedes explorar /en/api-service para opciones gestionadas o /en/dedicated-node para infraestructura dedicada. Si vas a desplegar en una red específica, consulta /en/networks para las cadenas compatibles.

Para uso en producción, audita tu circuito y el contrato verificador, y considera una verificación formal de las restricciones. Mide el tiempo de prueba y el gas de verificación en tu hardware y cadena de destino; estos números varían ampliamente. Puedes usar el procedimiento de tabla de resultados a continuación para registrar tus propias mediciones.

  • Mide el tiempo de prueba de tu circuito en hardware representativo.
  • Registra el costo de gas de verificación por prueba en tu cadena de destino.
  • Prueba con testigos inválidos para confirmar la solidez.
  • Documenta la ceremonia de configuración de confianza y la gestión de claves.

Procedimiento de medición reproducible para tu pipeline de ZKP

Para comparar sistemas de prueba o diseños de circuitos, ejecuta un benchmark controlado. Usa un circuito y un testigo fijos, y mide lo siguiente en la misma máquina: tiempo de generación de testigos, tiempo de generación de pruebas, tamaño de la prueba y gas de verificación en cadena. Ejecuta cada medición al menos cinco veces y reporta la mediana. No confíes en benchmarks publicados por otros equipos; tus restricciones y hardware serán diferentes.

Crea una tabla de resultados como la siguiente y complétala con tus propias mediciones. La tabla es una plantilla; no inventes números. Para las mediciones de gas, usa un fork local o una testnet y registra el gas utilizado por la transacción verifyProof. Para el tamaño de la prueba, registra la longitud en bytes del JSON de la prueba.

Nunca te preocupes por la infraestructura nuevamente

OnFinality elimina la carga pesada de DevOps para que puedas construir de forma más inteligente y rápida.

Comenzar