Las Crónicas de ZK: Circuitos (Parte 2)
Si recuerdas, un par de artículos atrás introdujimos el problema de satisfacibilidad de circuitos (ambas versiones, CSAT y #SAT), lo cual nos llevó directamente a una poderosa extensión de los circuitos tradicionales en forma de circuitos aritméticos.
Aunque estos problemas eran interesantes en concepto, probablemente aún se sentían un poco raros. Y en ese entonces, afirmamos que una alternativa más intuitiva para nuestros propósitos era de alguna manera probar la correctitud de una evaluación de un circuito — es decir, probar que evaluar un circuito en un conjunto particular de entradas produce un resultado dado.
Nuestro objetivo para hoy será presentar un mecanismo para hacer exactamente eso.
Tengo que avisarte: esto va a ser un poco más complejo que los artículos anteriores. Sin embargo, nos llevará a lo que se sientirá como nuestro primer sistema de pruebas verdaderamente general.
¡El primero de muchos!
¡Pero no te preocupes demasiado! Como siempre, lo haremos paso a paso, desglosando las partes importantes.
Hay mucho que cubrir, así que toma tu café y prepárate — ¡esta será una entrega larga!
Probando la Evaluación
¡Muy bien entonces! Dado que ya hemos aprendido sobre circuitos aritméticos, podemos saltar directamente al problema en cuestión.
Dado algún circuito aritmético , nuestro objetivo es probar que para alguna entrada , la salida del circuito tiene algún valor :
Quiero que nos tomemos un momento para analizar cómo abordar esto.
Aún no tenemos pistas claras. Con #SAT, la suma del hipercubo hizo obvia la conexión con la verificación de la suma, pero no tenemos la misma suerte acá. Vamos a necesitar otro ángulo de ataque.
Tal vez deberíamos repasar nuestro conocimiento hasta ahora, y eso nos puede dar una dirección a seguir. Veamos: sabemos que los circuitos aritméticos están compuestos de compuertas de suma y multiplicación. Y cada vez que alimentas alguna entrada a un circuito, fluye hacia adelante a través de los cables y hacia las compuertas (recuerdemos, es un DAG), que realizan operaciones simples.
Aquí va una pregunta rápida: ¿qué pasa si una compuerta está mal calculada?
Hasta ahora estamos asumiendo el "camino feliz", pero ¿qué pasaría si ese no fuera el caso? Cualquier cálculo incorrecto de una sola compuerta causaría un efecto dominó a través del circuito, haciendo que todo el cálculo sea inválido.
A la inversa, también podemos decir que para que el resultado de todo el circuito sea correcto, cada compuerta debe haber sido correctamente calculada.
Sí, hay una pequeña posibilidad de que el resultado accidentalmente se mantenga correcto debido a la aritmética modular. Pero con un campo de tamaño adecuado, esta probabilidad es despreciable, así que podemos ignorarla con seguridad.

Esto nos da una observación extremadamente poderosa: podemos descomponer el problema de verificar la evaluación del circuito en verificar la evaluación correcta de compuertas individuales.
¡Y de ahí viene principalmente el poder de los circuitos como modelo de cómputo! Las compuertas son como los bloques de Lego de la computación verificable: puedes verificar su corrección, y puedes encadenarlas juntas para formar estructuras más ricas!

¡Bien! Esto parece un punto de partida prometedor. Lo que necesitamos ahora es una forma de usar este nuevo conocimiento.
Información Relevante
Para construir un sistema de pruebas, un verificador debería poder sondear estas compuertas para verificar si están correctamente calculadas. Esto generalmente se hace a través de una estrategia de codificación: el cómputo en una compuerta se expresa de una forma que el verificador puede usar por su cuenta.
Entonces, ¿cómo hacemos eso?
Bueno, hay al menos tres cosas que un verificador necesita para verificar exitosamente si un cálculo de compuerta es válido o no:
- Las entradas
- La salida
- El tipo de la compuerta (ya sea suma o multiplicación)
Si un verificador puede obtener estas cosas, ¡simplemente puede calcular la salida esperada y comparar con el resultado proporcionado!
Esa es la teoría, al menos — pero ¿cómo codificaríamos esta información?
Codificación
La respuesta podría no ser sorprendente en este punto: ¡polinomios!
¡Siempre son polinomios!
No hay una sola forma de hacer esto. Diferentes sistemas de pruebas usan diferentes técnicas — y esa es parte de dónde vienen las diferencias clave entre los sistemas de pruebas modernos.
Quiero que nos vayamos acercando de a poco a este tema de la codificación, con un ejemplo. Veamos qué podemos hacer con un circuito pequeño:

Aquí hay algunas ideas. Primero, querríamos codificar los valores de los cables. Tenemos un total de cables, así que podríamos crear un polinomio tal que:
Notemos que esas son evaluaciones — diremos un poco más sobre cómo obtener el polinomio real más adelante.
Luego, necesitaríamos codificar la información de las compuertas. Para hacer esto, podemos usar polinomios indicadores para cada compuerta: uno para compuertas de suma, y uno para compuertas de multiplicación.
De manera similar a las bases de Lagrange, podemos elegir una compuerta , y crear un polinomio que devuelve solo cuando:
- Los valores de , , y corresponden a los cables correctos para la compuerta, con siendo la entrada izquierda, y siendo la entrada derecha, y siendo la salida.
- La compuerta es una compuerta de suma.
Lo mismo se puede hacer con compuertas de multiplicación, y un polinomio .
Con esto, podemos representar la afirmación "la compuerta está correctamente evaluada" con esta expresión de abajo:
El papel de y es desactivar las verificaciones si no corresponden a la compuerta correcta. Por supuesto, deberíamos asegurarnos de que y no sean iguales a al mismo tiempo.
¡Ok, pausa! Eso probablemente fue mucha información.
Tómate un momento para dejar que todo esto se asimile. Este tipo de estrategias de codificación serán muy importantes a medida que avancemos más en la serie, ¡así que es una buena idea familiarizarse con ellas ahora!
En serio, tómate tu tiempo. Si te adelantas, lo sabré.

¿Bien? ¡Ok, sigamos adelante!
Codificación de compuertas, listo.
Desafortunadamente... Hay un problema muy evidente con esto: para verificar la correctitud de una evaluación de circuito, un verificador necesitaría verificar cada compuerta individual. Para circuitos grandes (donde el tamaño es grande), esto significaría un tiempo de verificación de .
Y esto es bastante malo, porque no es mejor que simplemente ejecutar el circuito — ¡y toda la esencia de la computación verificable es permitir que los verificadores corran más rápido que eso!
Hemos dado con un gran obstáculo en nuestros planes... ¿Podemos hacer algo mejor?
Circuitos Estructurados
¡A veces podemos!
El secreto está en darse cuenta de que no todos los circuitos son iguales. De hecho, todos los circuitos tienen una estructura general diferente. Y en algunos casos, podemos explotarla para lograr un mejor rendimiento.
En particular, estamos interesados en circuitos donde podemos identificar capas claras: grupos de compuertas que no están directamente conectadas entre sí (son independientes), y en su lugar dependen solo de una capa anterior.

Llamamos al número de capas la profundidad () del circuito, y el número de cables en cada capa representa el ancho () de la capa. Para circuitos con ancho relativamente uniforme, el tamaño total del circuito es aproximadamente .
Ahora... ¿Qué pasaría si pudiéramos mostrar que una capa completa está correctamente calculada de una sola vez?

Si podemos lograr esto, podemos estar en camino a ahorros de tiempo considerables — especialmente para circuitos poco profundos, cuya profundidad es mucho menor que su ancho.
Algunos protocolos de hecho aprovechan esta estructura. Tal es el caso de nuestro próximo tema: el protocolo GKR.
El Protocolo GKR
GKR son las iniciales de los creadores de la técnica: Goldwasser, Kalai y Rothblum. Varias técnicas en criptografía han adoptado esta forma de nomenclatura.
Esto que veremos ahora combinará prácticamente todo lo que hemos aprendido hasta ahora: circuitos estructurados, codificación polinomial, extensiones multilineales y verificaciones de suma.
Por experiencia personal, mi sugerencia es no apresurarse con esto. Vuelve a los conceptos anteriores si es necesario. Este artículo no se va a ninguna parte — ¡eso te lo puedo asegurar!
Diseño del Circuito
El protocolo GKR asume una estructura muy específica para el circuito. No solo tiene capas, sino que cada capa tiene un número predeterminado de compuertas.
Etiquetemos cada capa con un índice . Comenzando desde la capa de salida con un índice de , y contando hacia atrás hacia las entradas, cada capa tendrá un total de compuertas.

¡Nota que es un valor asociado con cada capa!
Nada es involuntario sobre esta elección. Pero ¿por qué exactamente forzaríamos a las capas a tener un número tan extrañamente específico de compuertas?
La razón es una que ya hemos explotado en el pasado: ¡indexación!
Esencialmente, podemos etiquetar cada compuerta (o equivalentemente, cada cable) de cualquier capa dada usando nuestro confiable hipercubo booleano. Codificamos los valores para cada compuerta como evaluaciones de alguna función :
Donde devuelve el valor en el cable en la capa ( es una cadena binaria de bits que indexa el cable).
Y ya sabes qué significa un hipercubo booleano: ¡viene una verificación de suma!
Conectando las Capas
Por supuesto, la parte que aún nos falta es esa codificación de operación de compuerta de la que hablamos antes. Y para obtener esto, necesitaremos hacer una cosa más: especificar cómo están conectadas las compuertas.
Esto se conoce como el predicado de cableado. Es la única pieza faltante en el rompecabezas de hoy, y una vez que tengamos esto, finalmente podremos conectar todo.
¡Literalmente!
Para esto, definamos los polinomios y , que describen este predicado de cableado:
Estos, como se indicó anteriormente, tomarán tres etiquetas de compuerta , , y — las dos primeras de la capa de entrada, y la última de la capa de salida —, y devolverán solo cuando:
- Las compuertas en la combinación están verdaderamente conectadas en el circuito, lo que significa que la compuerta tiene entradas y .
- La compuerta más a la derecha () es del tipo correcto (suma o multiplicación).

De manera similar, para compuertas de multiplicación.
Mientras que codifica los valores de los cables en la capa , , describen qué compuertas se conectan a cuáles, y sus tipos. Juntos, constituyen una descripción completa del circuito, ¡o una codificación completa!
Todo lo que queda, como se insinuó anteriormente, es transformar esto de alguna manera en una verificación de suma.
Construyendo la Verificación de Suma
Si recuerdas, el protocolo de verificación de la suma tenía una estructura recursiva muy agradable: el problema original se reducía a una versión más pequeña del mismo problema, finalmente teniendo algo que es casi trivial de verificar.
Entonces, ¿qué tal si intentamos algo en esa línea?
La idea es simple: comenzar en la capa de salida, con algunos valores de salida reclamados. Allí, intentaremos transformar la afirmación sobre las salidas, en una afirmación sobre los valores de la penúltima capa. Enjuaga y repite, y después de pasos, llegamos a una afirmación sobre las entradas, ¡que deberían ser conocidas por el verificador!
Simple, ¿verdad?

¡Manténganse firmes, soldados!
Para realizar este paso de reducción, necesitaremos verificar algo sobre cada capa. Y aquí es donde nuestra codificación se vuelve relevante, todo gracias a este pequeño truco aquí:
Antes de que tu cabeza explote, intentemos explicar qué está haciendo la expresión:
- Estamos sumando sobre todos los índices de compuerta posibles en la capa , que son las posibles entradas en la capa (la capa de salida en este paso)
- Luego, estamos preguntando: ¿es esta una compuerta de suma o una compuerta de multiplicación? Recuerda, y devuelven o , y solo se activarán cuando exista una compuerta con entradas y , y salida en la capa, y cuando la compuerta sea del tipo especificado.
- Por último, y se multiplican por las operaciones deseadas.
El punto clave de esto es que a pesar de sumar sobre un hipercubo booleano, solo una de las combinaciones posibles activará o — así que toda la suma se reduce a solo un término.
Por ejemplo: si el cable en la capa obtiene entradas de los cables y en la capa a través de una compuerta de suma, entonces tenemos que:
- ¡Toda la suma se reduce a solo !
Con esto, ¡las similitudes con la verificación de suma son mucho más evidentes!
¿Ese polinomio loco dentro de la suma? Simplemente llamémoslo :
Y con él, podríamos fijar a un valor particular, y aplicar la verificación de suma para verificar:
Nota que estamos fijando para que el predicado de cableado tenga sentido.
Como puedes ver, en cada capa, necesitamos valores de la siguiente capa para construir este polinomio que estamos usando. A menos que el verificador esté mirando la capa de entrada, ¡no conocerían estos valores! En su lugar, el probador los proporcionará — ¡así que esta verificación de suma reduce el problema a una afirmación sobre la siguiente capa!
Por lo tanto, solo necesitamos aplicar esto veces (la profundidad del circuito), y estaríamos prácticamente listos.
Los Desafíos Restantes
¡Ok, genial! Esto se está formando bastante bien. Pero para avanzar, necesitaremos abordar dos problemas restantes, que se vuelven claros una vez que miramos más de cerca la verificación de suma que necesitamos realizar en cualquier capa individual.
Entonces, recapitulemos brevemente cómo procede el protocolo de verificación de la suma.
- Primero, el probador envía un resultado reclamado y un polinomio univariado al verificador, elaborado a partir del original.
- El verificador realiza una verificación simple, y luego selecciona un desafío aleatorio en el que evalúan . También envían este valor al probador.
- El probador luego procede a probar que es el nuevo resultado reclamado — ¡y lo hace volviendo a (1.)!
El proceso se repite hasta que se agotan todas las variables del original, y el verificador cierra el proceso realizando una única consulta de oráculo. Por supuesto, en nuestro caso, es el para cada capa.
¡Para la explicación completa, puedes volver a este artículo!
Ahora, este mecanismo tiene dos problemas:
- Necesitamos poder evaluar en un punto seleccionado aleatoriamente de un campo finito grande... ¡Pero por definición, este polinomio está definido sobre un hipercubo booleano!
- La consulta de oráculo final en realidad puede ser calculada por el verificador, siempre que conozcan dos valores especiales: la evaluación de en la capa anterior en los puntos seleccionados aleatoriamente. ¡Pero estos valores no son conocidos por el verificador!
Si logramos resolver estos problemas, entonces tendríamos un sistema de pruebas completamente funcional. Abordémoslos uno a la vez.
Yendo Multilineal

Entonces, necesitamos aplicar el protocolo de verificación de la suma en esa expresión larga que codifica el predicado de cableado:
Durante la verificación de suma, necesitamos poder seleccionar valores aleatorios seleccionados de un campo finito para cada componente de las entradas. ¡Eso significa que necesitaremos extender el dominio de esos polinomios!
Si bien hay múltiples formas de hacer esto, hay una forma elegantemente simple y eficiente de hacerlo: extensiones multilineales (MLEs). Si tomamos MLEs de cada polinomio involucrado, obtenemos una expresión que es adecuada para nuestras necesidades:
Oh, por cierto, denotamos la extensión multilineal con el símbolo de tilde (), así que es la extensión multilineal de W (definida sobre un dominio más restringido, como un hipercubo booleano).
¡Con respecto a la igualdad anterior, siéntete libre de verificar si se cumple si quieres!
¡Y con esto, podemos realizar una verificación de suma en cada capa!
Reduciendo Evaluaciones
Por último, recuerda que la consulta de oráculo al final de cada verificación de suma (para cada capa) requiere dos evaluaciones de la capa anterior, en alguna entrada seleccionada aleatoriamente:
Ahora, si tuviéramos que realizar dos verificaciones cada vez, el número de veces que necesitaríamos ejecutar la verificación de suma completa crecería exponencialmente, haciendo que nuestra estrategia sea inutilizable. ¡Por lo tanto, el toque final en nuestro mecanismo es intentar combinar esas dos verificaciones en una sola!
¡Esto será una herramienta útil en otros contextos, así que enlazaré de vuelta a esta sección en el futuro. ¡No te preocupes!
Podemos usar un truco muy inteligente para esto. Definamos:
Esto es simplemente una línea. Y ya sabemos que una línea está definida por dos puntos. Podemos establecerlos como , y .
Ahora esta línea codifica ambos valores.
Con esto, definiremos un pequeño subprotocolo. Así es como funciona:
- El probador envía un polinomio univariado de grado como máximo , reclamado que es igual a:
- El verificador luego verifica que , y . Esto valida que al menos, este polinomio reclamado codifica los valores requeridos.
- Finalmente, el verificador elige algún punto aleatorio , y con esto calculan:
Entonces, ¿qué está pasando aquí? El probador esencialmente está proporcionando una forma para que el verificador evalúe de manera "segura" para la verificación de suma de la siguiente capa:
¡Nota que esta verificación no corresponde a seleccionar una capa aleatoria en la siguiente capa! En su lugar, usamos algún valor aleatorio que probablemente no estará en un índice de compuerta válido, sino en algún otro punto!
La idea clave es que el probador no conoce de antemano. Debido a esto, si es en realidad incorrecto (llamémoslo ), sabemos gracias al lema de Schwartz-Zippel que es muy probable que:
¡Esto hará la vida muy difícil para el probador, que tiene que continuar con la siguiente ronda de verificación de suma con una suma reclamada incorrecta, para la cual no podrían usar los polinomios de cable reales!
Con estas dos consideraciones en mente, ¡el protocolo está completamente definido!
El flujo va así:
- El probador codifica las capas del circuito como polinomios, y calcula sus extensiones multilineales.
- Comenzando en la última capa, el probador y el verificador se involucran en una ronda de verificación de suma, usando el valor de salida como la afirmación inicial.
- El probador reduce las dos evaluaciones requeridas de a una sola a través de , que envían al verificador. El verificador luego calcula la afirmación para la siguiente ronda de verificación de suma.
- Enjuaga y repite hasta que se alcanza la capa de entrada, momento en el cual el verificador puede cerrar las verificaciones por su cuenta (¡no se necesita acceso al oráculo!).
Análisis
Como siempre, sin embargo, necesitamos evaluar algunos aspectos clave sobre el protocolo para determinar si tiene sentido o no: a saber, completitud, solidez y análisis de costos.
Sé que ustedes podrían estar cansados en este punto. Esto ya es una bomba de conocimiento tal como está.
tl;dr: GKR es sólido con error despreciable, el verificador corre aproximadamente en , pero el probador es costoso en . Si quieres los detalles, sigue leyendo. ¡De lo contrario, salta al resumen!
Solidez
La completitud no necesita mucha explicación: siempre que el probador sea honesto, todas las expresiones que presentamos deberían funcionar. Sin embargo, la solidez sí requiere un poco más de trabajo para demostrarse.
¡Básicamente como siempre!
En cada capa , el probador podría mentir sobre:
- Los polinomios enviados durante la verificación de suma. Sabemos que estos tienen grado , así que usando el lema de Schwartz-Zippel, hay una probabilidad de de que el probador tenga suerte sobre la elección aleatoria del verificador — esencialmente la misma lógica que usamos aquí.
- La reducción de línea . Debido a que es como máximo un polinomio de grado , podemos usar nuevamente el lema de Schwartz-Zippel, y calcular la probabilidad de una coincidencia aleatoria sobre la elección de como máximo .
Una vez atrapado en una mentira en una capa dada, el probador debe continuar a la siguiente capa con una afirmación incorrecta. Necesitarían tener suerte en cada capa subsiguiente para evitar la detección. Lo que significa que para el circuito completo, el error de solidez total es la suma de los errores en cada capa.
Esto resulta en . ¿Qué podemos hacer con este valor? Esencialmente, ¡que podemos hacer este error arbitrariamente pequeño con una selección adecuada de ! En otras palabras, cuanto mayor sea el tamaño de , menor será el error de solidez.
¡Así que el protocolo GKR es sólido!
Costos
El otro aspecto importante a discutir son los costos computacionales para el protocolo completo. Esto determinará si el protocolo tiene alguna posibilidad de ser útil.
Para explicar los costos, sin embargo, necesitaríamos un par de lemas e ideas que aún no hemos presentado. No creo que sea sabio seguir empaquetando contenido en este único artículo, y podríamos tener la oportunidad de hablar sobre esto en el futuro. Así que en su lugar, nuevamente, resumiré los costos para ustedes:
- El probador corre en tiempo .
- El verificador, simplificando un poco, corre en , donde es el costo de evaluar la MLE de la capa de entrada.
- Los costos totales de comunicación son elementos de campo.
¡Por supuesto, es importante entender de dónde vienen estos números — pero prometo que continuaremos mejorando nuestra lógica de estimación a medida que avancemos más en la serie!
Lo más importante ahora es evaluar las ganancias de tiempo de verificación al aplicar el protocolo GKR. Nota que el probador es realmente lento: el tiempo cúbico significa que su tiempo de ejecución crecerá bastante incluso para circuitos no tan grandes.
El verificador, sin embargo, se beneficia dramáticamente cuando los circuitos son poco profundos, lo que significa que su profundidad es pequeña.
Solo para lanzar algunos números: para un circuito de tamaño (aproximadamente 1 millón de compuertas) y profundidad , el probador correrá en aproximadamente operaciones (lo cual es extremadamente costoso), ¡pero el tiempo de ejecución del verificador será solo aproximadamente , que es mucho más pequeño!
¡Y el número de elementos de campo que se comunican es aún menor: solo !
Aunque esto suena bien en principio, la verdad es que los costos del probador de GKR siguen siendo prohibitivamente altos para circuitos grandes. Esto se convierte en un cuello de botella para su aplicabilidad, especialmente debido a su naturaleza interactiva.
Por esta razón, las implementaciones prácticas se enfocan en ciertas optimizaciones, o pasos de preprocesamiento que reducen los costos del probador a un nivel más manejable — pero eso, mis amigos, será una historia para otro momento.
Resumen
Ay por Dios. Eso si que fue un montón.
¡Pero lo hicimos!

¡Acabamos de ver nuestro primer protocolo de verificación de circuitos verdaderamente general! Y a pesar de sus limitaciones de tiempo de ejecución, GKR revela algo súper importante: que la verificación rápida de cómputos arbitrarios es posible.
Este es un paso crucial en nuestro viaje hacia protocolos más prácticos. Nuestro objetivo de ahora en adelante será encontrar mejores formas de codificar y verificar cómputos, pero al menos ahora sabemos que se puede hacer.
Creo que este es un momento poderoso en nuestro viaje. Te invito a tomarte un momento para apreciar cuánto hemos hecho con solo un par de herramientas. ¡Solo imagina en qué cosas locas nos meteremos más adelante!
Hablando de eso, nos hemos enfocado únicamente en circuitos como nuestros modelos de cómputo general por ahora. No sé tú, pero para mí, escribir un circuito no se siente como la cosa más natural de hacer al escribir un programa. Normalmente pensamos en cómputo en otros términos.
Entonces, ¿eso significa que nuestros esfuerzos son fundamentalmente defectuosos? ¡Esta es una pregunta que intentaremos responder en nuestro próximo encuentro!
¡Nos vemos pronto!

¿Te resultó útil este contenido?
Apoya a Frank Mangone enviando un café. Todos los ingresos van directamente al autor.
