Red de conocimiento informático - Material del sitio web - Cómo escribir un anillo de números primos en Python

Cómo escribir un anillo de números primos en Python

El objetivo principal de este artículo es mostrarles a todos cómo implementar el algoritmo STARK utilizando el lenguaje Python.

STARKs (Argumentos de conocimiento transparentes escalables) es una técnica para crear una prueba en la que f(x)=y, donde f puede tardar mucho en calcularse, pero esta prueba se puede verificar muy rápidamente. . STARK es "doble escala": para un cálculo que toma t pasos, se necesitarán aproximadamente O(t * log(t)) pasos para completar la prueba, que es probablemente la situación óptima, y ​​esto requiere ~O(log2 (t )) pasos para verificar, que es mucho más rápido que el cálculo original para valores moderadamente grandes de T. Los STARK también tienen propiedades de "prueba de conocimiento cero" que preservan la privacidad. Aunque le aplicamos este tipo de caso de uso para completar la función de retraso verificable, este tipo de propiedad no es necesaria, por lo que no debemos preocuparnos.

Antes que nada, por favor dame algunas aclaraciones:

Este código no ha sido revisado completamente en casos de uso reales, no se puede garantizar.

Esto; parte del código aún no es ideal (escrito en Python)

La "realidad" de STARK favorece razones de eficiencia específicas de la aplicación para usar campos binarios en lugar de campos primos; sin embargo, también realizan Sí, el; El código escrito aquí es legal y utilizable.

No existe una forma real de utilizar STARK. Es una arquitectura criptográfica y matemática muy amplia, con diferentes configuraciones para diferentes aplicaciones e investigación continua para reducir la complejidad del probador y del verificador y al mismo tiempo mejorar la usabilidad.

Este artículo espera que todo el mundo pueda conocer cómo opera la aritmética modular y el campo primo,

y combinarlo con los conceptos de polinomios, interpolación y valoración.

¡Ahora vamos a descubrirlo juntos!

MIMC

La siguiente es la visualización de funciones de STARK:

def mimc(inp, pasos, round_constants): start_time = time.time() para i en rango (pasos-1): inp = (inp**3 round_constants, 337, 85, inv=True) [46, 169, 29, 149, 126, 262, 140, 93] gt; poly_utils. PrimeField(337) gt; gt; [f.eval_poly_at([46, 169, 29, 149, 126, 262, 140, 93], f.exp(85, i)) para i en el rango (8 )] [3, 1, 4, 1, 5, 9, 2, 6]

La transformada de Fourier tomará [x[0] .... x[n-1]] como entrada, y su El objetivo es generar x[0] x[1] … x[n-1] como primer elemento, x[0] x[1] * 2 … x[n-1] * w**(n -1) como segundo elemento, y así sucesivamente; la Transformada Rápida de Fourier puede lograr esto dividiendo los datos por la mitad, haciendo una FFT en ambos lados y luego combinando los resultados.

La figura anterior es una explicación de cómo se somete la información a las operaciones FFT. Observe cómo FFT hace dos copias de los datos y las pega hasta obtener un elemento.

Ahora, juntemos todas las piezas y veamos cómo se ve todo: def mk_mimc_proof(inp, pasos, round_constants), que genera una prueba del resultado de la ejecución de la función MIMC dada la entrada es el número de pasos.

Primero, algunas funciones de aserción:

# Calcular el conjunto de coordenadas x xs = get_power_cycle(root_of_unity, modulus) column = [] for i in range(len(xs)//4): x_poly = f . lagrange_interp_4( [xs[i len(xs)*j//4] para j en el rango(4)], [valores[i len(valores)*j//4] para j en el rango(4)], ) columna .append(f.eval_poly_at(x_poly, special_x))

El factor de expansión es la trayectoria calculada (el conjunto de "valores intermedios" sobre los que se ejecuta la función MIMC) que estiraremos.

m2 = merkelize(columna) # Selecciona pseudoaleatoriamente los índices y para muestrear # (m2[1] es la raíz Merkle de la columna) ys = get_pseudorandom_indices(m2[1], len(columna), 40) # Calcular las ramas de Merkle para los valores en el polinomio y las ramas de la columna = [] para y en ys: Branches.append([mk_branch(m2, y)] [mk_branch(m, y (len(xs) ) // 4) * j) for j in range(4)])

Necesitamos que el número de pasos multiplicado por el factor de expansión sea como máximo 2^32, porque cuando k gt; no tenemos 2^k veces la raíz unitaria.

computational_trace_polynomial = inv_fft(computational_trace, modulus, subroot) p_evaluaciones = fft(computational_trace_polynomial, modulus, root_of_unity)

Nuestro primer cálculo será obtener la trayectoria computacional es decir, todo Compute; valores intermedios de entrada a salida.

asertar pasos lt;= 2**32 // extension_factor afirmar is_a_power_of_2(steps) and is_a_power_of_2(len(round_constants)) afirmar len(round_constants) lt; Comenzamos convirtiendo la trayectoria calculada en un polinomio, "anotando" valores continuos en la trayectoria de potencias sucesivas de la raíz unitaria g (donde g^pasos = 1), y luego procedemos al conjunto más grande, es decir , potencias sucesivas de la raíz unitaria g2 , donde g2^steps * 8 = 1 (tenga en cuenta que g2^8 = g) evaluación polinómica.

# Generar el seguimiento computacional computational_trace = [inp] para i in range(steps-1): computational_trace.append((computational_trace[-1]**3 round_constants[i len(round_constants)]) módulo ) salida = computational_trace[-1]

Negro: el poder de g1. Púrpura: Potencias de g2. Naranja: 1. Puedes pensar en las raíces unitarias consecutivas como un círculo dispuesto de esta manera. "Colocamos" la trayectoria calculada a lo largo de las potencias de g1 y luego la ampliamos para calcular el valor del mismo polinomio en valores intermedios (es decir, potencias de g2).

Podemos convertir las constantes cíclicas de MIMC en polinomios. Debido a que estas cadenas de constantes cíclicas ocurren con mucha frecuencia (en nuestras pruebas, cada 64 pasos), resulta que forman un polinomio de orden 64, y su expresión y expansión se pueden calcular fácilmente desde afuera:

skips2 = pasos // len(round_constants) constantes_mini_polynomial = fft(round_constants, modulus, f.exp(subroot, skips2), inv=True) constantes_polynomial = [0 si i skip2 else constantes_mini_polynomial[i/ /skips2] para i en el rango( pasos)] constantes_mini_extension = fft(constants_mini_polynomial, modulus, f.exp(root_of_unity, skips2))

Supongamos que hay 8192 pasos y 64 constantes cíclicas. Esto es lo que queremos hacer: estamos haciendo una FFT para calcular la constante del bucle en función de g1128. Luego agregamos muchos ceros en el medio para completar la función de g1. Dado que g1128 realiza un bucle aproximadamente cada 64 pasos, sabemos que g1 hará lo mismo. Solo contamos los 512 pasos de esta expansión porque sabemos que esta expansión se repetirá cada 512 pasos. Ahora, calculamos C(P(x)) como lo hicimos en el caso de Fibonacci, excepto que esta vez estamos calculando. Tenga en cuenta que no estamos calculando polinomios en forma de coeficientes, sino que estamos calculando en base a potencias sucesivas de mayor; -ordenar raíces unitarias.

c_of_p necesita satisfacer Q(x) = C(P(x), P(g1*x), K(x)) = P(g1*x) – P(x)**3 – K(x); el objetivo es que para cualquier x que pongamos en la trayectoria (excepto el último paso, porque después del último paso no hay pasos), el siguiente valor de la trayectoria sea igual al anterior. uno, más la constante del bucle. A diferencia del ejemplo de Fibonacci en la Parte 1, donde si un paso de cálculo estaba en el vector k, el siguiente sería el vector k 1, anotamos potencias sucesivas de la raíz unitaria de bajo orden ( g1 ) para calcular la trayectoria, por lo que si un paso de cálculo es en x = g1i, el siguiente paso será en g1i 1 = g1i * g1 = x * g1. Por lo tanto, para cada potencia de la raíz unitaria de orden inferior ( g1 ), esperamos terminar con P(x*g1) = P(x)**3 K(x), o P(x*g1) – P (x )**3 – K(x) = Q(x) = 0.

Por lo tanto, Q(x) será igual a cero en todas las potencias consecutivas de la raíz unitaria de orden inferior g (excepto la última).

# Crea el polinomio compuesto tal que # C(P(x), P(g1*x), K(x)) = P(g1*x) - P(x)**3 - K(x) c_of_p_evaluaciones = [(p_evaluaciones[(i factor_extensión)precisión] - f.exp(p_evaluaciones[i], 3) - constantes_mini_extensión[i len(constantes_mini_extensión)]) módulo para i en rango(precisión)] print(' Polinomio C(P, K) calculado')

Existe un teorema algebraico que demuestra que si Q(x) es igual a cero en todas estas coordenadas x, entonces el producto del polinomio mínimo será igual a cero en todas estas coordenadas x :Z(x) = (x – x_1) * (x – x_2) * … * (x – x_n). Al demostrar que en cualquier coordenada única, Q(x) es igual a cero, queremos demostrar que esto es difícil porque verificar dicha prueba lleva más tiempo que ejecutar el cálculo original, usaremos una forma indirecta para demostrar que Q(x) es el producto de Z(x). ¿Y qué haremos? Demostrando D(x) = Q(x) / Z(x) y usando FRI para demostrar que en realidad es un polinomio, no una fracción.

Elegimos una disposición específica de raíces unitarias de bajo y alto orden porque resulta que calcular Z(x) y dividirlo por Z(x) también es muy sencillo: la expresión para Z es dos términos parte de.

Cabe señalar que el numerador y el denominador de Z se calculan directamente, y luego la división por Z se convierte en multiplicación utilizando el método de inversión modular por lotes, y luego Q (X) se multiplica punto por punto. a través de la inversa del valor Z(X) x). Cabe señalar que para las potencias de raíces unitarias de orden inferior, excepto para la última, se puede obtener Z(x) = 0, por lo que este cálculo se interrumpirá si se incluye su cálculo inverso. Esto es muy desafortunado, aunque solucionaremos este vacío simplemente modificando la verificación aleatoria y el algoritmo FRI, por lo que incluso si cometemos un error de cálculo, no importa.

Debido a que Z(x) se puede expresar de manera sucinta, también obtenemos otro beneficio: el verificador puede calcular rápidamente Z(x) para cualquier x especial y no requiere ningún cálculo previo. Para el probador, podemos aceptar que tiene que tratar con polinomios de tamaño igual al número de pasos, pero no queremos que el verificador haga lo mismo porque queremos que el proceso de verificación sea lo suficientemente conciso.

# Calcular D(x) = Q(x) / Z(x) # Z(x) = (x^steps - 1) / (x - x_atlast_step) z_num_evaluaciones = [xs[(i * precisión] - 1 para i en rango(precisión)] z_num_inv = f.multi_inv(z_num_evaluaciones) z_den_evaluaciones = [xs[i] - último_paso_posición para i en rango(precisión)] d_evaluaciones = [cp * zd * módulo zni para cp, zd , zni in zip(c_of_p_evaluaciones, z_den_evaluaciones, z_num_inv)] print('Polinomio D calculado')

En varios puntos aleatorios, realice la detección de conceptos D(x) * Z(x) = Q (x), para que se puedan verificar las restricciones de transferencia y cada paso de cálculo sea un resultado válido del paso anterior. Pero también queremos verificar las restricciones límite, donde las entradas y salidas del cálculo son lo que dice el probador. Solo requiere que el probador proporcione los valores de P (1), D (1), P (último_paso) y D (último_paso), que son muy frágiles sin prueba, esos valores son todos; en el mismo polinomio. Entonces, usamos un truco de división polinomial similar:

# Calcular interpolante de ((1, entrada), (x_atlast_step, salida)) interpolant = f.lagrange_interp_2([1, last_step_position], [inp, salida ]) i_evaluaciones = [f.eval_poly_at(interpolant, x) para x en xs] zeropoly2 = f.mul_polys([-1, 1], [-last_step_position, 1]) inv_z2_evaluaciones = f.multi_inv([f.eval_poly_at(cociente , x) para x en xs]) # B = (P - I) / Z2 b_evaluaciones = [((p - i) * invq) módulo para p, i, invq en zip(p_evaluaciones, i_evaluaciones, inv_z2_evaluaciones)] print( 'Polinomio B calculado')

Entonces, nuestro argumento es el siguiente. El probador quiere demostrar que P(1) == entrada y P(last_step) == salida. Si usamos I(x) como interpolación, entonces es la línea que cruza los puntos brillantes (1, entrada) y (último_paso, salida), por lo que P(x) – I(x) será igual a cero en este punto brillante. lugar. Por lo tanto, probaremos que P(x) – I(x) es el producto de P(x) – I(x), y lo demostramos aumentando el cociente.

Púrpura: Calcula el polinomio de trayectoria (P).

Verde: Interpolación (I) (observe cómo se construye la interpolación, es igual a la entrada en x = 1 (debería ser el primer paso para calcular la trayectoria) y es igual a la salida en x = g^(pasos- 1) (debe ser el último paso de cálculo de la trayectoria). Rojo: P-I Amarillo: Polinomio mínimo igual a 0 en x = 1 y x=g^(pasos-1) (es decir, Z2): (P – I) < /Z2. p>

Ahora, veamos cómo combinar las raíces de Merkle de P, D y B.

Ahora, necesitamos demostrar que P, D y B son en realidad polinomios, y son el orden máximo correcto. Pero la prueba FRI es grande y costosa, y no queremos tener tres pruebas FRI, por lo que calculamos la combinación lineal pseudoaleatoria de P, D y B, y hacemos la prueba FRI. basado en esto:

# Calcula sus raíces Merkle mtree = merkelize([pval.to_bytes(32, 'big') dval.to_bytes(32, 'big') bval.to_bytes(32, 'big' ) for pval, dval, bval in zip(p_evaluaciones, d_evaluaciones, b_evaluaciones)]) print('Raíz hash calculada')

A menos que los tres polinomios tengan el orden inferior correcto, es casi imposible tener un combinación lineal elegida al azar, por lo que esto es suficiente.

Queremos demostrar que el orden de D es menor que 2 * pasos, y el grado de P y B es menor que los pasos, por lo que en realidad usamos aleatorio. P, P * xpasos, B, Bpasos y combinaciones aleatorias de D, y se puede ver que esta parte de la combinación es menor que 2 * pasos

Ahora, verifiquemos todas las combinaciones polinómicas.

Primero obtenemos muchos índices aleatorios y luego proporcionamos polinomios para las ramas de Merkle en estos índices:

k1 = int.from_bytes(blake(mtree[1] b'\x01'), 'big' ) k2 = int.from_bytes(blake(mtree[1] b'\x02'), 'grande') k3 = int.from_bytes(blake(mtree[1] b'\x03'), 'grande') k4 = int .from_bytes(blake(mtree[1] b'\x04'), 'big') # Calcula la combinación lineal Ni siquiera nos molestamos en calcularla # en forma de coeficiente, simplemente calculamos las evaluaciones root_of_unity_to_the_steps = f.exp( raíz_de_unidad, pasos) potencias = [1] para i en rango(1, precisión): potencias.append(potencias[-1] * módulo raíz_de_unidad_para_los_pasos) l_evaluaciones = [(d_evaluaciones[i] p_evaluaciones[i] * k1 p_evaluaciones[i] * k2 * potencias[i] b_evaluaciones[i] * k3 b_evaluaciones[i] * potencias[i] * k4) módulo para i en rango(precisión)]

La función get_pseudorandom_indices responderá [0.. .precision- 1] Índice aleatorio en el rango y el parámetro exclusion_multiples_of no proporciona el valor de un parámetro múltiple específico. Esto asegura que no tomamos muestras a lo largo de la trayectoria de cálculo original; de lo contrario, obtendremos una respuesta incorrecta.

La prueba consiste en una prueba de orden inferior de un conjunto de raíces de Merkle, ramas comprobadas al azar y combinaciones lineales aleatorias:

# Haga algunas comprobaciones al azar del árbol de Merkle en coordenadas pseudoaleatorias, excluyendo # múltiplos de `extension_factor` sucursales = [] muestras = spot_check_security_factor posiciones = get_pseudorandom_indices(l_mtree[1], precisión, muestras, excluir_multiples_of=extension_factor) para pos en posiciones: sucursales.append(mk_branch(mtree, pos )) sucursales.append(mk_branch(mtree, (pos skips) precisión)) sucursales.append(mk_branch(l_mtree, pos)) print('Muestras de comprobaciones puntuales calculadas')

La parte más larga del La prueba completa son las ramas del árbol de Merkel, y FRI demuestra que está compuesto por más ramas. Este es el resultado sustantivo del verificador:

o = [mtree[1], l_mtree[1], sucursales, prove_low_grado(l_evaluaciones, root_of_unity, pasos * 2, módulo, excluir_multiples_of=extension_factor)]

En cada posición, el probador debe proporcionar una prueba de Merkle para que el verificador pueda verificar la prueba de Merkle y verificar C(P(x), P(g1*x), K(x )) = Z( x) * D(x) y B(x) * Z2(x) I(x) = P(x) (recordatorio: para x que no está en la órbita calculada inicial, Z(x) no será cero, por lo que C( P(x), P(g1*x), K(x) no será cero). El verificador también verificará que la combinación lineal sea correcta y luego llamará.

para i, pos en enumerar(posiciones): x = f.exp(G2, pos) x_to_the_steps = f.exp(x, pasos) mbranch1 = verificar_branch(m_root, pos, sucursales[i*3 ]) mbranch2 = verificar_rama(m_root, (pos skips)precisión, ramas[i*3 1]) l_of_x = verificar_rama(l_root, pos, ramas[i*3 2], salida_as_int=True) p_of_x = int.from_bytes(mbranch1[ :32], 'grande') p_of_g1x = int.from_bytes(mbranch2[:32], 'grande') d_of_x = int.from_bytes(mbranch1[32:64], 'grande') b_of_x = int.from_bytes(mbranch1[64 :], 'big') zvalue = f.div(f.exp(x, pasos) - 1, x - last_step_position) k_of_x = f.eval_poly_at(constants_mini_polynomial, f.exp(x, skips2)) # Verifique las restricciones de transición Q (x) = Z(x) * D(x) afirmar (p_of_g1x - p_of_x ** 3 - k_of_x - zvalue * d_of_x) módulo == 0 # Verificar las restricciones de límite B(x) * Z2(x) I(x) = P(x) interpolante = f.lagrange_interp_2([1, último_paso_posición], [entrada, salida]) zeropoly2 = f.mul_polys([-1, 1], [-último_paso_posición, 1]) afirmar (p_of_x - b_of_x * f. eval_poly_at(zeropoly2, x) - f.eval_poly_at(interpolant, x)) módulo == 0 # Verificar la exactitud de la combinación lineal afirmar (l_of_x - d_of_x - k1 * p_of_x - k2 * p_of_x * x_to_the_steps - k3 * b_of_x - k4 * b_of_x * x_to_the_steps) módulo == 0

En realidad, no se ha realizado con éxito; el análisis de confiabilidad a través de verificaciones polinomiales y la cantidad de verificaciones aleatorias requeridas para FRI ha demostrado ser muy complicado. Pero eso es todo código, y al menos no tienes que preocuparte por hacer optimizaciones locas. Cuando ejecuto el código anterior, obtenemos una prueba STARK, que es entre 300 y 400 veces más costosa de probar (por ejemplo, un cálculo MIMC que tarda 0,2 segundos tarda 60 segundos en probarse). Esto permite que una máquina de 4 núcleos calcule STARK en MIMC más rápido que calcular MIMC al revés.

Dicho esto, en Python esto sería relativamente ineficiente de implementar y demostraría tener diferentes ratios de tiempo de ejecución. Al mismo tiempo, también vale la pena señalar que el costo de prueba STARK de MIMC es muy bajo porque MIMC es casi perfectamente computable y su forma matemática es muy simple. Para los cálculos promedio, que implican menos cálculos explícitos (por ejemplo, verificar si un número es mayor o menor que otro), el costo computacional puede ser mayor, entre 10.000 y 50.000 veces.