Logotipo de Zephyrnet

[Espejo] Programas de aritmética cuadrática: de cero a héroe

Fecha:

Vitalik Buterin a través de Blog de Vitalik Buterin

Este es un espejo de la publicación en https://medium.com/@VitalikButerin/programas-de-aritmética-cuadrática-de-cero-al-héroe-f6d558cea649

Últimamente ha habido mucho interés en la tecnología detrás de los zk-SNARK, y la gente está cada vez más tratando de desmitificar algo que muchos han llegado a llamar “matemáticas lunares” debido a su percibida complejidad pura e indescifrable. De hecho, los zk-SNARK son bastante difíciles de entender, especialmente debido a la gran cantidad de partes móviles que deben unirse para que todo funcione, pero si desglosamos la tecnología pieza por pieza, comprenderla se vuelve más simple.

El propósito de esta publicación no es servir como una introducción completa a zk-SNARK; asume como conocimiento previo que (i) sabes qué son los zk-SNARK y qué hacen, y (ii) sabes suficientes matemáticas para poder razonar sobre cosas como polinomios (si la afirmación �(�)+�(�) =(�+�)(�) , donde � y � son polinomios, te parece natural y obvio, entonces estás en el nivel correcto). Más bien, la publicación profundiza en la maquinaria detrás de la tecnología e intenta explicar lo mejor posible la primera mitad del proceso, como lo dibuja aquí el investigador de zk-SNARK, Eran Tromer:

Los pasos aquí se pueden dividir en dos mitades. En primer lugar, los zk-SNARK no se pueden aplicar directamente a ningún problema computacional; más bien, hay que convertir el problema en la “forma” correcta para que funcione. La forma se llama “programa aritmético cuadrático” (QAP), y transformar el código de una función en uno de estos no es en sí mismo nada trivial. Junto con el proceso para convertir el código de una función en un QAP, hay otro proceso que se puede ejecutar para que, si tiene una entrada al código, pueda crear una solución correspondiente (a veces llamada "testigo" del QAP). Después de esto, hay otro proceso bastante complejo para crear la “prueba de conocimiento cero” real para este testigo, y un proceso separado para verificar una prueba que otra persona le pasa a usted, pero estos son detalles que están fuera del alcance de esta publicación. .

El ejemplo que elegiremos es sencillo: demostrar que conoces la solución de una ecuación cúbica: �3+�+5=35 (pista: la respuesta es 3). Este problema es lo suficientemente simple como para que el QAP resultante no sea tan grande como para resultar intimidante, pero no lo suficientemente trivial como para que se pueda ver toda la maquinaria entrar en juego.

Escribamos nuestra función de la siguiente manera:

def qeval(x):
    y = x**3
    return x + y + 5

El sencillo lenguaje de programación de propósito especial que estamos usando aquí admite aritmética básica (+, −, ⋅, /), exponenciación de potencia constante (�7 pero no ��) y asignación de variables, que es lo suficientemente poderosa como para que teóricamente puedas hacerlo. cualquier cálculo dentro de él (siempre que el número de pasos computacionales esté limitado; no se permiten bucles). Tenga en cuenta que el módulo (%) y los operadores de comparación (<, >, ≤, ≥) NO son compatibles, ya que no existe una manera eficiente de hacer módulo o comparación directamente en aritmética de grupos cíclicos finitos (agradezca esto; si hubiera una manera hacer cualquiera de las dos cosas, entonces la criptografía de curva elíptica se rompería más rápido de lo que se puede decir "búsqueda binaria" y "teorema del resto chino").

Puede extender el lenguaje a módulos y comparaciones proporcionando descomposiciones de bits (por ejemplo, 13=23+22+1) como entradas auxiliares, demostrando la exactitud de esas descomposiciones y haciendo los cálculos en circuitos binarios; en aritmética de campos finitos, hacer comprobaciones de igualdad (==) también es factible y, de hecho, un poco más fácil, pero ambos son detalles en los que no entraremos en detalles ahora. Podemos extender el lenguaje para admitir condicionales (por ejemplo, si �<5:�=7; de lo contrario: �=9) convirtiéndolos a una forma aritmética: �=7⋅(�<5)+9⋅(�≥5 ) aunque tenga en cuenta que sería necesario ejecutar ambas “rutas” del condicional, y si tiene muchos condicionales anidados, esto puede generar una gran cantidad de gastos generales.

Repasemos ahora este proceso paso a paso. Si desea hacer esto usted mismo para cualquier fragmento de código, le implementó un compilador aquí (Solo con fines educativos; ¡todavía no estoy listo para crear QAP para zk-SNARK del mundo real!).

Aplastamiento

El primer paso es un procedimiento de "aplanamiento", en el que convertimos el código original, que puede contener declaraciones y expresiones arbitrariamente complejas, en una secuencia de declaraciones que tienen dos formas: �=� (donde � puede ser una variable o un número ) y �=� (��) � (donde �� puede ser +, −, ⋅, / y � y � pueden ser variables, números o subexpresiones en sí mismas). Puedes pensar en cada una de estas afirmaciones como si fueran puertas lógicas en un circuito. El resultado del proceso de aplanamiento del código anterior es el siguiente:

sym_1 = x * x
y = sym_1 * x
sym_2 = y + x
~out = sym_2 + 5

Si lees el código original y el código aquí, podrás ver con bastante facilidad que los dos son equivalentes.

Puertas a R1CS

Ahora, convertimos esto en algo llamado sistema de restricciones de rango 1 (R1CS). Un R1CS es una secuencia de grupos de tres vectores (�, �, �), y la solución de un R1CS es un vector �, donde � debe satisfacer la ecuación �.�⋅�.�−�.�=0, donde . representa el producto escalar; en términos más simples, si “juntamos” � y �, multiplicamos los dos valores en las mismas posiciones, y luego tomamos la suma de estos productos, luego hacemos lo mismo con � y � y luego con � y � , entonces el tercer resultado es igual al producto de los dos primeros resultados. Por ejemplo, este es un R1CS satisfecho:

Pero en lugar de tener una sola restricción, tendremos muchas restricciones: una para cada puerta lógica. Existe una forma estándar de convertir una puerta lógica en un triple (�,�,�) dependiendo de cuál sea la operación (+, −, ⋅ o /) y si los argumentos son variables o números. La longitud de cada vector es igual al número total de variables en el sistema, incluida una variable ficticia ~una en el primer índice que representa el número 1, las variables de entrada, una variable ficticia ~out que representa la salida, y luego todas las variables intermedias (���1 y ���2 arriba); los vectores generalmente serán muy escasos y solo llenarán los espacios correspondientes a las variables que se ven afectadas por alguna puerta lógica en particular.

Primero, proporcionaremos el mapeo de variables que usaremos:

'~one', 'x', '~out', 'sym_1', 'y', 'sym_2'

El vector de solución constará de asignaciones para todas estas variables, en ese orden.

Ahora, daremos el triple (�,�,�) para la primera puerta:

a = [0, 1, 0, 0, 0, 0]
b = [0, 1, 0, 0, 0, 0]
c = [0, 0, 0, 1, 0, 0]

Puede ver que si el vector solución contiene 3 en la segunda posición y 9 en la cuarta posición, entonces, independientemente del resto del contenido del vector solución, la verificación del producto escalar se reducirá a 3⋅3=9, y así pasará. Si el vector solución tiene −3 en la segunda posición y 9 en la cuarta posición, la verificación también pasará; de hecho, si el vector de solución tiene 7 en la segunda posición y 49 en la cuarta posición, entonces esa verificación aún pasará; el propósito de esta primera verificación es verificar la coherencia de las entradas y salidas de la primera puerta únicamente.

Ahora pasemos a la segunda puerta:

a = [0, 0, 0, 1, 0, 0]
b = [0, 1, 0, 0, 0, 0]
c = [0, 0, 0, 0, 1, 0]

De manera similar a la primera verificación del producto escalar, aquí estamos verificando que ���1⋅�=�.

Ahora, la tercera puerta:

a = [0, 1, 0, 0, 1, 0]
b = [1, 0, 0, 0, 0, 0]
c = [0, 0, 0, 0, 0, 1]

Aquí, el patrón es algo diferente: se multiplica el primer elemento en el vector solución por el segundo elemento, luego por el quinto elemento, se suman los dos resultados y se verifica si la suma es igual al sexto elemento. Debido a que el primer elemento en el vector solución es siempre uno, esto es solo una verificación de suma, verificando que la salida sea igual a la suma de las dos entradas.

Finalmente, la cuarta puerta:

a = [5, 0, 0, 0, 0, 1]
b = [1, 0, 0, 0, 0, 0]
c = [0, 0, 1, 0, 0, 0]

Aquí, estamos evaluando la última verificación, ~out =���2+5. La verificación del producto escalar funciona tomando el sexto elemento en el vector de solución, sumando cinco veces el primer elemento (recordatorio: el primer elemento es 1, por lo que esto efectivamente significa sumar 5) y comparándolo con el tercer elemento, que es donde almacenar la variable de salida.

Y ahí tenemos nuestro R1CS con cuatro restricciones. El testigo es simplemente la asignación de todas las variables, incluidas las de entrada, de salida y las internas:

[1, 3, 35, 9, 27, 30]

Puede calcular esto usted mismo simplemente "ejecutando" el código aplanado anterior, comenzando con la asignación de la variable de entrada �=3 e ingresando los valores de todas las variables intermedias y la salida a medida que las calcula.

El R1CS completo en conjunto es:

A
[0, 1, 0, 0, 0, 0]
[0, 0, 0, 1, 0, 0]
[0, 1, 0, 0, 1, 0]
[5, 0, 0, 0, 0, 1]

B
[0, 1, 0, 0, 0, 0]
[0, 1, 0, 0, 0, 0]
[1, 0, 0, 0, 0, 0]
[1, 0, 0, 0, 0, 0]

C
[0, 0, 0, 1, 0, 0]
[0, 0, 0, 0, 1, 0]
[0, 0, 0, 0, 0, 1]
[0, 0, 1, 0, 0, 0]

R1CS a QAP

El siguiente paso es tomar este R1CS y convertirlo al formato QAP, que implementa exactamente la misma lógica excepto que usa polinomios en lugar de productos escalares. Hacemos esto de la siguiente manera. Pasamos de cuatro grupos de tres vectores de longitud seis a seis grupos de tres polinomios de grado 3, donde evaluamos los polinomios en cada coordenada x representa una de las restricciones. Es decir, si evaluamos los polinomios en �=1, obtenemos nuestro primer conjunto de vectores, si evaluamos los polinomios en �=2, entonces obtenemos nuestro segundo conjunto de vectores, y así sucesivamente.

Podemos hacer esta transformación usando algo llamado Interpolación de Lagrange. El problema que resuelve una interpolación de Lagrange es el siguiente: si tienes un conjunto de puntos (es decir, (�,�) pares de coordenadas), entonces hacer una interpolación de Lagrange en esos puntos te da un polinomio que pasa por todos esos puntos. Hacemos esto descomponiendo el problema: para cada coordenada, creamos un polinomio que tiene la coordenada deseada en esa coordenada y una coordenada de 0 en todas las demás coordenadas que nos interesan, y luego obtenemos la coordenada final. Como resultado sumamos todos los polinomios.

Hagamos un ejemplo. Supongamos que queremos un polinomio que pase por (1,3), (2,2) y (3,4). Comenzamos haciendo un polinomio que pase por (1,3), (2,0) y (3,0). Resulta que hacer un polinomio que “sobresalga” en �=1 y sea cero en los otros puntos de interés es fácil; simplemente hacemos:

(x - 2) * (x - 3)

Que se ve así:

Ahora, sólo necesitamos “reescalarlo” para que la altura en x=1 sea la correcta:

(x - 2) * (x - 3) * 3 / ((1 - 2) * (1 - 3))

Esto nos da:

1.5 * x**2 - 7.5 * x + 9

Luego hacemos lo mismo con los otros dos puntos y obtenemos otros dos polinomios de apariencia similar, excepto que “sobresalen” en �=2 y �=3 en lugar de �=1. Sumamos los tres y obtenemos:

1.5 * x**2 - 5.5 * x + 7

Con exactamente las coordenadas que queremos. El algoritmo descrito anteriormente toma �(�3) tiempo, ya que hay � puntos y cada punto requiere �(�2) tiempo para multiplicar los polinomios; con un poco de pensamiento, esto se puede reducir a �(�2) tiempo, y con mucho más pensamiento, utilizando algoritmos rápidos de transformada de Fourier y similares, se puede reducir aún más: una optimización crucial cuando se usan funciones en zk. -Los SNARK en la práctica suelen tener miles de puertas.

Ahora, usemos la interpolación de Lagrange para transformar nuestro R1CS. Lo que vamos a hacer es tomar el primer valor de cada vector �, usar la interpolación de Lagrange para hacer un polinomio a partir de eso (donde evaluar el polinomio en � obtiene el primer valor del �ésimo vector �), repetir el proceso para el primer valor de cada vector � y �, y luego repita ese proceso para el segundo valor, el tercero, valores, etc. Para mayor comodidad, proporcionaré las respuestas ahora mismo:

A polynomials
[-5.0, 9.166, -5.0, 0.833]
[8.0, -11.333, 5.0, -0.666]
[0.0, 0.0, 0.0, 0.0]
[-6.0, 9.5, -4.0, 0.5]
[4.0, -7.0, 3.5, -0.5]
[-1.0, 1.833, -1.0, 0.166]

B polynomials
[3.0, -5.166, 2.5, -0.333]
[-2.0, 5.166, -2.5, 0.333]
[0.0, 0.0, 0.0, 0.0]
[0.0, 0.0, 0.0, 0.0]
[0.0, 0.0, 0.0, 0.0]
[0.0, 0.0, 0.0, 0.0]

C polynomials
[0.0, 0.0, 0.0, 0.0]
[0.0, 0.0, 0.0, 0.0]
[-1.0, 1.833, -1.0, 0.166]
[4.0, -4.333, 1.5, -0.166]
[-6.0, 9.5, -4.0, 0.5]
[4.0, -7.0, 3.5, -0.5]

Los coeficientes están en orden ascendente, por lo que el primer polinomio anterior es en realidad 0.833⋅�3—5⋅�2+9.166⋅�−5. Este conjunto de polinomios (más un polinomio Z que explicaré más adelante) constituye los parámetros para esta instancia QAP en particular. Tenga en cuenta que todo el trabajo hasta este punto debe realizarse solo una vez para cada función que intenta utilizar zk-SNARKs para verificar; Una vez generados los parámetros QAP, se pueden reutilizar.

Intentemos evaluar todos estos polinomios en �=1. Evaluar un polinomio en �=1 simplemente significa sumar todos los coeficientes (como 1�=1 para todo �), por lo que no es difícil. Obtenemos:

A results at x=1
0
1
0
0
0
0

B results at x=1
0
1
0
0
0
0

C results at x=1
0
0
0
1
0
0

Y he aquí, lo que tenemos aquí es exactamente lo mismo que el conjunto de tres vectores para la primera puerta lógica que creamos arriba.

Comprobando el QAP

Ahora bien, ¿cuál es el punto de esta loca transformación? La respuesta es que en lugar de verificar las restricciones en el R1CS individualmente, ahora podemos verificar todas las restricciones al mismo tiempo haciendo la verificación del producto escalar sobre los polinomios.

Debido a que en este caso la verificación del producto escalar es una serie de sumas y multiplicaciones de polinomios, el resultado en sí será un polinomio. Si el polinomio resultante, evaluado en cada coordenada � que usamos anteriormente para representar una puerta lógica, es igual a cero, entonces eso significa que todas las comprobaciones pasan; Si el polinomio resultante evaluado en al menos una de las coordenadas � que representan una puerta lógica da un valor distinto de cero, entonces eso significa que los valores que entran y salen de esa puerta lógica son inconsistentes (es decir, la puerta es �=�⋅�� �1 pero los valores proporcionados podrían ser �=2,���1=2 y �=5).

Tenga en cuenta que el polinomio resultante no tiene por qué ser cero y, de hecho, en la mayoría de los casos no lo será; podría tener cualquier comportamiento en los puntos que no corresponden a ninguna puerta lógica, siempre que el resultado sea cero en todos los puntos que do corresponden a alguna puerta. Para verificar la corrección, en realidad no evaluamos el polinomio �=�.�⋅�.�−�.� en cada punto correspondiente a una puerta; en su lugar, dividimos � por otro polinomio, �, y comprobamos que � divide uniformemente a �, es decir, que la división �/� no deja resto.

� se define como (�−1)⋅(�−2)⋅(�−3)… – el polinomio más simple que es igual a cero en todos los puntos que corresponden a puertas lógicas. Es un hecho elemental del álgebra que cualquier un polinomio que es igual a cero en todos estos puntos tiene que ser múltiplo de este polinomio mínimo, y si un polinomio es múltiplo de � entonces su evaluación en cualquiera de esos puntos será cero; esta equivalencia facilita mucho nuestro trabajo.

Ahora, hagamos la verificación del producto escalar con los polinomios anteriores. Primero, los polinomios intermedios:

A . s = [43.0, -73.333, 38.5, -5.166]
B . s = [-3.0, 10.333, -5.0, 0.666]
C . s = [-41.0, 71.666, -24.5, 2.833]

Ahora, �.�⋅�.�—�.�:

t = [-88.0, 592.666, -1063.777, 805.833, -294.777, 51.5, -3.444]

Ahora, el polinomio mínimo �=(�−1)⋅(�−2)⋅(�−3)⋅(�−4):

Z = [24, -50, 35, -10, 1]

Y si dividimos el resultado anterior por �, obtenemos:

h = t / Z = [-3.666, 17.055, -3.444]

Sin resto.

Y así tenemos la solución para el QAP. Si intentamos falsificar cualquiera de las variables en la solución R1CS de la que derivamos esta solución QAP (digamos, establecer la última en 31 en lugar de 30), obtenemos un polinomio � que no pasa una de las comprobaciones (en ese caso en particular). caso, el resultado en �=3 sería igual a −1 en lugar de 0), y además � no sería múltiplo de �; más bien, dividir �/� daría un resto de [−5.0,8.833,−4.5,0.666].

Tenga en cuenta que lo anterior es una simplificación; “En el mundo real”, la suma, multiplicación, resta y división no ocurrirán con números regulares, sino con elementos de campo finitos: un tipo espeluznante de aritmética que es autoconsistente, por lo que todas las leyes algebraicas que conocemos y amamos aún es cierto, pero donde todas las respuestas son elementos de algún conjunto de tamaño finito, generalmente números enteros dentro del rango de 0 a �−1 para algún �. Por ejemplo, si �=13, entonces 1/2=7 (y 7⋅2=1),3⋅5=2, y así sucesivamente. El uso de aritmética de campos finitos elimina la necesidad de preocuparse por los errores de redondeo y permite que el sistema funcione bien con curvas elípticas, que terminan siendo necesarias para el resto de la maquinaria zk-SNARK que hace que el protocolo zk-SNARK sea realmente seguro.

Un agradecimiento especial a Eran Tromer por ayudarme a explicarme muchos detalles sobre el funcionamiento interno de zk-SNARK.

punto_img

Información más reciente

punto_img