Criptografía 101: Pruebas de Conocimiento Cero (Parte 3)
Este artículo forma parte de una serie más larga sobre criptografía. Si este es el primer artículo con el que te encuentras, te recomiendo encarecidamente comenzar desde el principio de la serie.
¡Con la teoría detrás de nosotros, es hora de ser más prácticos!
En el artículo de hoy, me gustaría centrarme en algunos ejemplos simples de afirmaciones que podemos probar utilizando un zkSNARK como Plonk. Así que, esencialmente, estaremos construyendo algunos circuitos aritméticos.
Y no veo mejor manera de empezar esto que ¡revisitar las pruebas de rango! Vamos directo a la acción.
Pruebas de Rango Revisitadas
Durante nuestra no tan breve mirada a Bulletproofs, vimos lo difícil que era construir una prueba de rango desde cero. Pero ahora, tenemos algunas nuevas herramientas a nuestra disposición. Y como prometimos, veremos cómo podemos construir un circuito aritmético para representar la afirmación:
Hay una representación válida de N bits para un número
¡Construir tal circuito nos permite probar el conocimiento de dicho número usando Plonk! Así que intentemos expresar esto en ecuaciones, de nuevo.
Realmente, este es el mismo sistema que describimos antes. Por lo tanto, la siguiente descripción será algo condensada — ¡recomiendo la explicación anterior para una mayor atención al detalle!
Si nuestro número puede ser representado con bits, esto significa que hay alguna representación binaria válida para él:
Y a su vez, estos números deberían satisfacer esta ecuación:
También sabemos que todas nuestras entradas estarán en un campo finito de tamaño :
Tanto como los serán entradas a nuestro circuito — por lo tanto, necesitamos alguna manera de probar que los valores son bits (ya sea o ). Y podemos hacer esto con la siguiente comprobación:
Para cada uno de los N bits.
Resta
¡Genial! Ya tenemos el sistema... Pero ¿cómo lo convertimos en un circuito aritmético? En particular, necesitamos poder representar la resta, pero solo podemos usar compuertas de suma y multiplicación. ¿Qué hacer entonces?

Bueno, cuando trabajamos con campos finitos, existe el concepto de inversos aditivos! Piensa en restar como lo mismo que sumar . Pero, por supuesto, no es un elemento de nuestro campo:
¿Cómo nos ayuda esto, entonces? Verás, así como dijimos que la suma se envuelve hasta el principio del conjunto cuando el resultado es mayor que , también es cierto que la resta se envuelve hasta el final del conjunto cuando el resultado es menor que .
Realmente, lo que sucede es que podemos mapear al conjunto utilizando la operación módulo:
También puedes ver esto expresado como . Esto se llama una relación de congruencia, y se lee " es congruente con módulo ".
Hay mucho que decir sobre las relaciones de congruencia, que también definen clases de congruencia — pero evitaremos entrar en esos temas hoy. Puedes leer sobre ello aquí si estás interesado.
En conclusión, sumar tiene exactamente el mismo efecto que restar !
Esto funciona para cualquier grupo aditivo.
Juntándolo Todo
Ahora que sabemos cómo restar, ¡podemos crear nuestro circuito!
Para empezar, debemos verificar que es un bit. Esto se puede hacer de esta manera:

Por supuesto, si sumamos todas las salidas de estas verificaciones, el valor resultante también debería ser .
Luego, necesitamos representar la suma de cada bit multiplicado por la potencia correspondiente de . Una vez que obtengamos el resultado, restar también debería dar . Y como podrías adivinar, restar es lo mismo que sumar , que por supuesto, es el resultado de multiplicar y .
Para mantener las cosas manejables, digamos que . Para esta cantidad de bits, nuestro circuito se vería así:

¡Y ahí lo tienes! Este circuito debería dar como salida si un número y su representación binaria , , y se introducen en él. Por supuesto, no queremos revelar ninguno de ellos — así que serán la afirmación privada. Todas las demás entradas pueden considerarse parte del testigo — solo un montón de valores que permiten una evaluación eficiente del circuito.
Si nos referimos a nuestra nomenclatura del artículo anterior, entonces la información secreta y el testigo serían:
A partir de aquí, todo lo que hacemos es aplicar Plonk como se presentó en el artículo anterior, ¡y entonces tenemos un zkSNARK para probar que está en el rango de a . ¡Genial!
Pertenencia a Conjuntos
El ejemplo anterior introdujo un par de ideas interesantes, como cómo restar con compuertas. E implícitamente, también usamos algunos números mágicos como entradas, para evitar cálculos excesivos.
Sin embargo, estos no son los únicos trucos que podemos usar al construir circuitos — como veremos en un momento. Pasemos a nuestro siguiente ejemplo para ver qué nos espera.
Imagina que tenemos un conjunto de valores :
Lo que intentaremos hacer es probar el conocimiento de un valor que pertenece a dicho conjunto. Necesitaremos alguna afirmación matemática que represente este hecho. Esta es bastante fácil — aquí, simplemente escribiré la ecuación para ti:
Si es, de hecho, parte del conjunto, entonces uno de los términos será cero, haciendo que todo el producto dé como resultado . De lo contrario, el valor de será algún valor positivo no cero.
Es decir, los son las raíces de .
Construir un circuito para este polinomio no es difícil. Probablemente implicaría proporcionar los valores como el testigo, para un cálculo más rápido, y para que el protocolo tenga algún sentido. Es decir, ¿de qué sirve probar el conocimiento de un valor en un conjunto, si el verificador no sabe de qué conjunto están hablando?
Sin embargo, parece haber un problema aquí: si es público, entonces todo el mundo puede leerlo. Así que en teoría, cualquiera podría probar el conocimiento de algún en el conjunto. Construir una prueba de conocimiento cero en ese escenario no tiene mucho sentido...
Ocultando el Conjunto
Arreglar esto no es difícil: para hacer el conjunto secreto, podemos hashear cada elemento, y hacer que los valores hasheados sean públicos en su lugar. El conjunto ahora se ve así:
Ahora estamos hablando
Con este ajuste, entonces conocer el conjunto no revela nada sobre los valores reales en , que permanecen secretos. Nuestra ecuación también necesita una ligera modificación:
Por supuesto, la función de hash debe hashear en nuestro campo finito, para que podamos usar estos valores en nuestro circuito.
Maravilloso. Espléndido. Hasta que... Notamos que necesitamos incluir la función de hash en nuestro circuito.

Está bien, respira. No es tan malo. Por supuesto, si tuviéramos que implementar un circuito para una función de hash nosotros mismos, sería un verdadero dolor de cabeza.
Afortunadamente, algunas personas ya han pensado en esto, y proporcionan funciones de hash como paquetes en algunos Lenguajes de Dominio Específico (DSL) populares para construir circuitos, como Circom y Noir.
Por ejemplo, está Poseidon. Puedes encontrar su implementación aquí.
Pensemos en la función de hash como una caja, que incluiremos en nuestro circuito, como un componente. Con esto en mente, nuestro circuito se vería así:

¡Impresionante! Todo lo que queda es ejecutar esto a través de Plonk — ya sabes, calculando el rastro, codificándolo, todas esas cosas buenas que ya discutimos en el capítulo anterior de la serie.
Verificación de Cero
Tener un componente de hash reutilizable para incluir en nuestros circuitos es ciertamente una buena idea. Uno puede preguntarse si hay otras acciones o verificaciones que podríamos querer reutilizar de manera similar — siendo así un buen objetivo para este tipo de proceso de caja negra.
¡Muy similar al proceso de crear componentes de computadora!
Una idea muy simple para tal componente, es construir algo que verifique si algún valor es cero o no. Es decir, una función que hace lo siguiente:
Quiero decir, podría ser útil... ¿Quién sabe, verdad? ¡Verificar que algunos cálculos den en un circuito grande podría ser necesario! Además, no hay un
if (x == 0)
directo disponible para usar.
Construyamos tal componente. Aquí hay una idea para ello:

Una inspección más detallada paso a paso revela lo que está haciendo este circuito: las primeras tres compuertas calculan la expresión:
Si es , entonces su inverso también será cero, y la expresión dará . Si no es , entonces por definición sabemos que , y por lo tanto, la expresión devolverá .
Esta es la razón por la que la salida de este componente no está en la última compuerta, sino en la tercera. Entonces, ¿por qué hay una cuarta compuerta? ¿Qué propósito tiene?
Para responder a la pregunta, te preguntaré: ¿no has notado algo fuera de lo común en este circuito? Míralo de nuevo.
Pronto te darás cuenta de que esta es la primera vez que usamos un inverso modular. Es una entrada a nuestro circuito — simplemente no podemos calcular inversos modulares solo con compuertas de suma y multiplicación. Y así, el valor del inverso modular debe ser proporcionado.
Recuerda que en Plonk, solo verificamos que el rastro de cálculo tenga sentido — está perfectamente bien que se proporcione un inverso como entrada, ¡siempre que nos aseguremos de que el valor sea correcto!
La idea es que la cuarta compuerta existe como un medio para verificar que el inverso modular reclamado es de hecho correcto. Llamemos al inverso modular reclamado . En total, hay cuatro escenarios posibles para analizar, que puedes corroborar tú mismo siguiendo el circuito:
- : No importa qué inverso proporcionemos, la salida será . Realmente no nos importa si el inverso está bien o no, lo cual es consistente con que la última compuerta siempre dé .
- : El inverso es correcto, y la salida es . La última compuerta también debería dar , lo que significa que el inverso es correcto.
- : El inverso es incorrecto. La salida será (que es incorrecta), y además, la última compuerta no dará . ¡Debido a que la última restricción no se cumple, esto significa que algo en las entradas está mal!
- : El inverso es incorrecto, y en este caso, la salida no será , sino algún otro elemento en el campo finito. Debido a esto, la última compuerta ciertamente no dará — y esto significa que el inverso proporcionado no es correcto.
¡Y ahí lo tienes! Nuestro circuito verificador de cero está listo.
Tener estos elementos simples que realizan tareas simples puede ser enormemente útil a medida que construimos circuitos cada vez más grandes. Permite cierto grado de componibilidad en el proceso, ¡lo que puede acelerar dramáticamente la creación de tus circuitos!
Resumen
Estos son solo algunos ejemplos de las cosas que puedes hacer con circuitos aritméticos: dado que son un modelo de cálculo tan general, puedes construir casi cualquier tipo de afirmación.
Es cierto que crear el circuito en sí mismo podría ser un poco desafiante si tuviéramos que unir muchas compuertas nosotros mismos. Por eso nunca lo hacemos realmente.
Usamos otros tipos de representaciones, que pueden ser conceptualizadas como circuitos.
Circom, que es un DSL popular para construir circuitos, no te obliga a escribir compuertas. En su lugar, se basa en una técnica llamada aritmetización para transformar un conjunto de restricciones en la forma adecuada para zkSNARKs.
Finalmente, los SNARKs tienen muchos beneficios (tamaños de prueba pequeños, tiempos de verificación cortos), pero también hay algunos aspectos que generan cierto grado de preocupación (por ejemplo, Plonk requiere una configuración de confianza). Por esta razón (y otras), esta sigue siendo un área de investigación muy activa. ¡Quién sabe qué podríamos ver en el futuro!
Hablando de configuraciones de confianza e idealmente evitándolas, hay otro tipo de prueba sucinta de conocimiento que no lo requiere — es transparente. Así que en el próximo capítulo de nuestra aventura, exploraremos el mundo de los STARKs, el hermano menor de los SNARKs que trae algunas propiedades interesantes a la mesa. ¡Hasta la próxima!