Circom es un lenguaje de programación para crear Rank 1 Constraint Systems (R1CS) y poblar el vector witness del R1CS.
El formato R1CS es de interés debido a la utilidad de ese formato para construir SNARKs, particularmente Groth16. Con los SNARKs, habilitamos la computación verificable, lo que nos permite probar la corrección de un cálculo. Al verificar, la parte interesada gasta menos esfuerzo computacional para confirmar la corrección del que necesitaría para realizar el cálculo por sí misma. También es posible generar la prueba sin revelar los datos subyacentes, y en este caso, nos referimos a ellos como zkSNARKs.
La primera parte de nuestro libro de ZK se centró en probar la validez de un witness para un R1CS dado. Este recurso se centra en cómo generar programáticamente un R1CS y cómo diseñarlos para modelar algoritmos realistas, como una máquina virtual o funciones hash criptográficas.
Requisitos previos
Esperamos que el lector ya esté familiarizado con los siguientes capítulos de nuestro libro de ZK:
- https://rareskills.io/post/p-vs-np
- https://rareskills.io/post/arithmetic-circuit
- https://rareskills.io/post/finite-fields
- https://rareskills.io/post/rank-1-constraint-system
Vamos a asumir que el lector sabe qué es un R1CS y qué representa. Esto se explica completamente en los cuatro capítulos anteriores.
No es necesario comprender completamente las matemáticas detrás de ZK para usar Circom, pero hay algunos principios que deben comprenderse por completo, o de lo contrario Circom no tendrá sentido.
No obstante, si el lector se toma en serio hacer carrera en ZK, aprender los fundamentos de ZK es esencial. Para ello, recomendamos encarecidamente leer las dos primeras secciones del libro de ZK y construir el sistema de pruebas Groth16 desde cero para consolidar el aprendizaje.
Sin embargo, si el objetivo del lector es comprender rápidamente las aplicaciones ZK, le recomendamos leer los cuatro capítulos enumerados anteriormente y luego utilizar este recurso.
Por qué existe Circom
Circom fue creado para abordar dos problemas principales en el desarrollo de sistemas de restricciones para SNARKs.
- Diseñar manualmente sistemas de restricciones es tedioso y propenso a errores, especialmente cuando se trata de restricciones a gran escala o repetitivas.
- Poblar el witness es igualmente desafiante y requiere el cálculo manual de valores intermedios que, de otro modo, podrían derivarse programáticamente.
Por lo tanto, Circom 1) simplifica el diseño de restricciones y 2) automatiza la población del witness.
1. Diseñar el sistema de restricciones es tedioso
Las tareas de diseñar manualmente un conjunto de restricciones (correctas) y luego traducirlas a R1CS son tediosas y propensas a errores. Circom fue creado para hacer esta tarea menos desafiante y tediosa generando programáticamente las restricciones.
Por ejemplo, para decir que el valor x solo puede tener los valores , podemos expresar eso con la restricción
Sin embargo, un R1CS solo puede tener una multiplicación no constante por restricción, por lo que debemos dividir la restricción anterior en dos restricciones:
Para sistemas pequeños, esta traducción manual es manejable. Sin embargo, sería extremadamente molesto hacerlo a mano si necesitáramos crear esta restricción para 100 o incluso 1000 variables. Si tenemos miles de restricciones muy similares, sería preferible crear un “template” para las restricciones y generarlas en un bucle for. Circom nos permite crear estas restricciones programáticamente.
Por ejemplo, supongamos que quisiéramos restringir 1,000 variables para que tengan los valores . Circom puede generar estos valores en un bucle de la siguiente manera:
template Constrain1000Example() {
signal input in[1000];
for (var i = 0; i < 1000; i++) {
0 === in[i] * (in[i] - 1);
}
}
component main = Constrain1000Example();
Explicaremos la sintaxis más adelante en otros capítulos, pero la idea central es que definimos una restricción 0 === in[i] * (in[i] - 1) y la repetimos 1000 veces.
2. Poblar el witness es tedioso
El witness en el contexto de ZK es una asignación a las variables que satisface todas las restricciones en un circuito aritmético.
Como vimos en el artículo sobre circuitos aritméticos, probar que un número es menor que otro requiere convertir ambos números a binario, ya que “mayor que” no tiene sentido en un campo finito dado que los números dan la vuelta.
Expresar el número en binario, asumiendo que cabe en cuatro bits, requiere que satisfaga las siguientes restricciones:
Aquí, es el bit menos significativo, y es el bit más significativo. El probador debe suministrar , que son los bits binarios de , junto con el propio .
En este caso, probar que es un número de cuatro bits se ha vuelto cinco veces más tedioso porque, además de , también tenemos que proporcionar los valores binarios de , a pesar de que pueden derivarse de manera determinista y directa. Circom automatiza este proceso y nos permite escribir código para poblar variables en el witness basándonos en otras variables. Por ejemplo, para poblar las variables binarias, podríamos escribir el siguiente código de Circom (el siguiente código carece de algunas características de seguridad necesarias — por favor no lo copies a ciegas):
b_0 <-- x & 1; // get the first bit of x via bitmask
b_1 <-- (x >> 1) & 1; // get the second bit of x
b_2 <-- (x >> 2) & 1; // get the third bit of x
b_3 <-- (x >> 3) & 1; // get the fourth bit of x
El código anterior genera el witness pero no crea las restricciones en nuestra fórmula:
El circuito anterior traducido a Circom sería (la sintaxis se explicará más adelante):
template BinaryConstraint() {
// assign the values to b_0,...,b_3
x === b_0 + 2*b_1 + 4*b_2 + 8*b_3;
0 === b_0*(b_0 - 1);
0 === b_1*(b_1 - 1);
0 === b_2*(b_2 - 1);
0 === b_3*(b_3 - 1);
}
Una gran ventaja de Circom es que su código se asemeja a las matemáticas de los circuitos aritméticos, por lo que es fácil traducir un sistema de ecuaciones a Circom.
La idea es que en lugar de suministrar al circuito, solo suministramos . Circom calculará los valores binarios por nosotros y luego completará las restricciones con los valores calculados.
Además de automatizar la generación de restricciones, Circom mejora el proceso de poblar el witness a través de su operador de “asignar y restringir”, <==.
La ventaja de asignar y restringir con <== en Circom
Circom simplifica aún más la población del witness a través de su operador de “asignar y restringir” <==. Supongamos que tenemos la restricción:
z === x * y
Si suministramos los valores para x y y, sería un poco molesto tener que suministrar también el valor para z porque z solo tiene una solución posible.
Con Circom, usamos <== de la siguiente manera:
z <== x * y
Con esto, la variable z ya no necesita ser proporcionada como un input, ya que Circom la puebla por nosotros, y su valor quedará bloqueado en para el resto del circuito.
Por lo tanto, Circom ahorra al usuario la molestia de proporcionar explícitamente un valor para cada elemento en el witness, lo cual es uno de los principales atractivos de la conveniencia de Circom.
Circom es tanto un DSL como un lenguaje de programación
La mayor fuente de confusión al programar en Circom es que es tanto un lenguaje de programación (similar a Javascript) como un DSL que se compila en un R1CS. En ese sentido, se parece un poco a Solidity. Solidity puede afectar el estado subyacente de la blockchain transfiriendo Ether, pero también puede comportarse como un lenguaje de programación normal. La porción de “lenguaje de programación” de Circom sirve para ayudar con la población automática del witness, como se describió anteriormente. Sin embargo, para el recién llegado, no siempre está claro qué partes de Circom afectan al R1CS subyacente.
Por ejemplo, el siguiente es un código válido de Circom que calcula la potencia de un número:
function power(base, exp) {
return base ** exp;
}
template Power() {
signal input base;
signal input exp;
signal output out;
out <-- power(base, exp);
}
component main = Power();
/* INPUT = {
"base": "3",
"exp": "2"
} */
Sin embargo, el código anterior no genera ninguna restricción (por lo que no sería útil para probar nada). Como aprenderemos más adelante, el operador <-- tiene el único propósito de generar el witness, no de generar las restricciones.
Por qué aprender Circom
Como uno de los lenguajes específicos de dominio (DSLs) más antiguos para ZK, Circom cuenta con la mayor cantidad de librerías y proyectos disponibles de los que puedes aprender y está probado en batalla.
Creemos que aprender los DSLs de ZK más modernos, como Halo2 y Plonky3, será mucho más fácil si primero enseñamos Circom, así que estamos haciendo eso.
Para ver por qué, aquí está el código para calcular la secuencia de Fibonacci en Halo2 y el código para calcular Fibonacci en Plonky3. Un vistazo superficial a los ejemplos debería convencer al lector de que esos DSLs podrían no ser el mejor lugar para que empiece un principiante. Aquí está el código de Circom para probar que out es el enésimo número correcto de Fibonnaci. Es mucho más fácil de entender en comparación:
pragma circom 2.1.6;
// proves `out` is the nth
// fibonnaci number
template Fibonacci(n) {
var offset = n + 1;
assert(n > 2);
signal fib[offset];
signal output out;
fib[0] <== 0;
fib[1] <== 1;
for (var i = 2; i < offset; i++) {
fib[i] <== fib[i-1] + fib[i - 2];
}
out <== fib[n];
}
// 5th fibonnaci number is 5
// 0 1 1 2 3 5
component main = Fibonacci(5);
En contraste, Circom tiene una curva de aprendizaje relativamente simple para los principiantes que se sumergen en el desarrollo de ZK.
¿Acaso Noir, Cairo y Leo no abstraen la necesidad de aprender a escribir restricciones?
Puedes escribir contratos inteligentes en blockchains ZK o layer 2s utilizando lenguajes similares a Rust, como Noir, Cairo y Leo, que están diseñados para “ocultar” la generación de restricciones al programador. Si tu objetivo es simplemente escribir aplicaciones para estas blockchains, aprender cómo funcionan las restricciones ZK a nivel interno no es estrictamente necesario.
Sin embargo, ten en cuenta que todo programador serio de Solidity tiene una comprensión decente de cómo funciona la Ethereum Virtual Machine (EVM) y puede escribir código assembly básico. Saber qué está sucediendo detrás de escena te ayudará a escribir código más eficiente, y este recurso logra ese objetivo.
Además, hay muchos errores que surgen en estos entornos de ejecución debido al modelo de ejecución ZK subyacente. Entender qué es realmente privado, qué limitaciones pueden existir en el flujo de control, los errores comunes al usar campos, o ganar la capacidad de usar de forma segura funciones sin restricciones en Noir o restricciones personalizadas en o1js requieren de una comprensión de bajo nivel.
El objetivo de esta serie
No obstante, los lenguajes ZK de alto nivel no vuelven obsoleta la escritura de restricciones; de hecho, aumentan la demanda de expertos que realmente entienden cómo funcionan. El propósito de este recurso es incorporar a desarrolladores y auditores de seguridad más avanzados para que puedan desarrollar y asegurar los entornos subyacentes de blockchain, máquina virtual y compiladores que utilizan estos lenguajes ZK de alto nivel.
Cómo está estructurado este recurso
Este recurso se divide en dos partes principales:
-
La primera parte enseña la sintaxis de Circom. Específicamente, enseñamos cómo escribir restricciones y programar Circom para que pueble la mayoría de los valores del witness por nosotros.
-
La segunda parte de este recurso enseña cómo diseñar restricciones para aplicaciones ZK en general. Usaremos Circom para los ejemplos, pero el contenido se aplica a otros DSLs de ZK, como Halo2 o Plonky3.
También tocaremos temas de seguridad en aplicaciones ZK a lo largo del contenido.
El aprendizaje no solo se logra estudiando, sino practicando
Muchos de los capítulos incluyen ejercicios explícitos o algún código sin terminar que se “deja como ejercicio para el lector”. Tu viaje de aprendizaje será mucho más efectivo si resuelves esos problemas. Diseñamos esos problemas para que sirvan como un repaso de lo que acabas de leer para reforzar el aprendizaje. No requieren de ninguna “intuición” o “astucia” especial para resolverse si comprendes correctamente el recurso escrito. Nuestra esperanza es que los ejercicios al final resulten algo “obvios” después de leer el material (si no es así, ¡por favor abre un issue o envía un pull request en el repositorio de los ejercicios!).
Instalación de Circom
Las instrucciones para instalar Circom están aquí: https://docs.circom.io/getting-started/installation/#installing-dependencies
También hay un IDE en línea para Circom aquí: https://zkrepl.dev/
Apéndice: Plonk vs Groth16 para Circom
Para los lectores familiarizados con el sistema de pruebas Plonk, vale la pena señalar que escribimos el mismo circuito tanto para los sistemas probadores de Plonk como para el sistema probador de Groth16.
Groth16 permite un número ilimitado de operaciones de suma por restricción, pero solo una multiplicación no constante (ten en cuenta que un Rank 1 Constraint System tiene una multiplicación por fila). En contraste, Plonk solo permite una multiplicación o una suma por restricción, y no ambas. La limitación de una multiplicación por restricción se hará evidente a medida que exploremos Circom.
Sin embargo, los circuitos de Circom que son compatibles con Groth16 también funcionarán con Plonk. La librería snarkjs que usa los Rank 1 Constraints Systems como un input los traduce a un sistema de restricciones de Plonk si el desarrollador así lo desea.
Circom es, por lo tanto, agnóstico sobre si el sistema de pruebas subyacente previsto es Groth16 o Plonk. Siempre que el circuito sea compatible con Groth16, también puede ser compatible con Plonk sin cambios adicionales por parte del desarrollador.
Autoría y créditos
Calnix escribió la primera parte de este libro e influyó significativamente en su estructura general. Por favor, sigue a Calnix en X y tal vez envíale un agradecimiento.
Agradecemos a Veridise, Privacy Scaling Explorations, Marco Besier de zkSecurity y Chainlight por sus útiles revisiones de este trabajo.