WEB3DEV Español

Delia Viloria T
Delia Viloria T

Posted on

Ahorro de gas al implementar circuitos en contratos inteligentes

Image description

O cómo al analizar la interacción con circuitos se revelan oportunidades de optimización de contratos inteligentes.

He estado trabajando en un Hackathon de un proyecto personal, Block Qualified, y me he encontrado con una estrategia interesante de ahorro de gas: todo tiene que ver con la forma en que un contrato inteligente interactúa con el mismo circuito de conocimiento cero.

Primero, necesitamos un poco de contexto. Block Qualified fue una idea simple que tuve para incorporar una prueba de conocimiento cero en un proyecto Hackathon, y se trata de tomar exámenes directamente en la cadena. Permite que una parte, llamémoslo el emisor de credenciales, defina una prueba, básicamente un examen que evalúa algún tipo de conocimiento, en la blockchain. Cualquier persona puede intentar resolver esta prueba, demostrando que posee los conocimientos/calificaciones necesarios. Y todo esto se hace usando pruebas de conocimiento cero para que los solucionadores nunca revelen sus soluciones, sólo su conocimiento sobre ellas.

No nos centraremos en cómo los exámenes se definen como circuitos de conocimiento cero. Ese es el tema para otro artículo. Por ahora, pensemos en los requisitos de alto nivel de una aplicación como esta. ¿Qué necesita una aplicación descentralizada (DApp) para resolver las pruebas? Puede ser algo como:

  • Permitir a los usuarios resolver una prueba

  • Permitir que los usuarios publiquen su solución en el blockchain

  • Evitar que los usuarios maliciosos hagan trampa

La forma en que se cumplen estos requisitos es utilizando pruebas de conocimiento cero. El usuario, o solucionador, no publica sus soluciones directamente en el blockchain, ya que esto las haría públicas, ¡cualquiera podría hacer fraude! En su lugar, proporcionan una prueba de conocimiento de la solución. No vamos a hablar de cómo los solucionadores proporcionan sus soluciones, pero por ahora, puede consultar cómo se definen los circuitos correspondientes.

¿Pero qué pasa si simplemente copio la transacción de resolución de otra persona y la reclamo como mía? Si los circuitos de conocimiento cero solo sirven para proporcionar una prueba de conocimiento, ¡Allí tenemos un problema! Para resolver esto, podemos usar algo similar a una sal criptográfica. La implementación real se parece a lo siguiente:

pragma circom 2.0.0;

template Test() {
  [...]
  signal input salt;
  [...]
  // Adicione sinais ocultos para garantir que a adulteração do sal invalide a prova de sarcasmo
  signal saltSquare;
  saltSquare <== salt * salt;
}
Enter fullscreen mode Exit fullscreen mode

Al introducir esta sal en nuestro circuito y usarla para formar una señal oculta, garantizamos que la misma solución con dos sales diferentes resultará en dos pruebas diferentes. Ahora solo tenemos que asegurarnos de que cada sal se use sólo una sola vez. Podemos hacer esto configurándolo como una entrada pública del circuito y anulándolo dentro del contrato inteligente. Esto puede parecer algo así:

pragma solidity ^0.8.7;

contract TestCreator {
    mapping (uint256 => bool) public usedSalts;

    function solveTest( uint256 testId, uint256 salt, solutionProof proof ) external {
        require(!usedSalts[salt], "Salt was already used");

        require(verifier(proof, salt), "Invalid proof");

        usedSalts[salt] = true;
    }
Enter fullscreen mode Exit fullscreen mode

Una vez que una prueba es verificada, su sal correspondiente es anulada. Si alguien intenta copiar esta transacción, se revertirá: la sal ya fue usada. ¡Entonces nadie será capaz de hacer fraude!

Esta fue la implementación original: permite a los usuarios resolver las pruebas y evita que los fraudulentos obtengan credenciales. Pero hay una manera más barata de hacer eso e implica pensar en el papel de esa sal. Siéntete libre de detenerte aquí y pensar en lo que podría ser.

Entonces, ¿qué papel juega exactamente esta sal? La idea, como hemos visto, es que la misma solución, usando dos sales diferentes, nos lleva a dos pruebas diferentes. Rastreamos estas sales para verificar esencialmente si una prueba es una prueba nueva o si ha sido reutilizada.

Con esta construcción, no establecemos ningún otro requisito sobre el valor de esta sal, aparte del hecho de que no debe haber sido utilizado antes. Dado que estas sales son un uint256 (es decir, un número entre 0 y 2 ** 256-1), podemos simplemente generar un número entero aleatorio, verificar si no se usó antes y generar nuestras pruebas de solución con él. Bastante simple.

(En realidad, es un poco más complejo, ya que el valor de la sal debe pertenecer al campo escalar SNARK, con un límite superior de ~2*254, pero esta es la idea básica).

Pero no tener restricciones en la forma en que esa sal se genera nos está costando gas. ¿Cómo exactamente puede ser eso?

Bueno, vamos a pensar en la sal de nuevo. Tenemos que asegurarnos de que será un valor diferente para cada prueba, para evitar fraude. Por el momento, estamos transfiriendo esta carga al usuario (o el DApp que usarían para interactuar con el protocolo), y el contrato inteligente solo verifica que cumplieron. La solución consiste en hacer lo contrario: el contrato inteligente genera de forma determinística la sal a ser utilizada y luego verifica si el usuario lo ha cumplido.

Por lo tanto, necesitamos generar una sal de manera determinista y asegurarnos de que sea diferente para cada transacción. Podríamos hacerlo con una función hash, pero si lo pensamos bien, no necesitamos que sea diferente para cada transacción, sino para cada solucionador. Después de todo, lo que queremos es evitar que otros usuarios hagan fraude. ¿Por qué no simplemente usar la dirección? Esto puede parecer algo así:

pragma solidity ^0.8.7;

contract TestCreator {
    function solveTest( uint256 testId, address recipient, solutionProof proof ) external {
        uint salt = uint(uint160(recipient));

        require(verifier(proof, salt), "Invalid proof");
    }
}
Enter fullscreen mode Exit fullscreen mode

Podemos estar seguros de que dos solucionadores diferentes nunca tendrán la misma sal y, por lo tanto, el fraude se vuelve inviable. El usuario simplemente tiene que generar sus pruebas usando su dirección (convertida en un número entero) como sal.

Simplemente al utilizar la dirección como sal dentro del contrato inteligente, nos ahorramos el costo SSTORE de 20.000 de gas para valores diferentes de cero donde anteriormente había un cero. El ahorro real del costo de gas de la implementación completa es de casi 40.000 gas, todo gracias a un simple cambio de perspectiva en relación a la forma en que el contrato inteligente interactúa con el sistema de prueba.

Una consecuencia inesperada: retransmisión de transacciones

Debes haber notado que el último fragmento de código introdujo un nuevo parámetro en la función solveTest: la dirección del destinatario. Podemos obtener la misma lógica eliminando este parámetro y usando msg.Sender cuando sea necesario. Pero como la dirección del destinatario de la credencial ya está incrustada en la prueba (por el uso de la dirección como sal), podemos soportar fácilmente la retransmisión de transacciones.

Los usuarios pueden simplemente resolver una prueba, generar la prueba correspondiente usando su dirección como sal y enviar esa prueba para un retransmisor de transacción. Como el destinatario está incorporado en la propia prueba, el retransmisor no puede alterarlo sin invalidar la prueba. Esto significa que la prueba que el usuario ha generado solo se puede utilizar para otorgar la credencial a sí mismo - hacer fraude no es posible ¡incluso cuando tú tienes la transacción de resolución completa delante de ti!

Por lo tanto, no solo ahorramos casi 40.000 gas con esta implementación, sino que ¡también podemos configurar retransmisores de transacciones para que los usuarios ni siquiera tengan que preocuparse por los costos de gas!

Este artículo es una traducción de Deenz realizada por Delia Viloria T. Puedes encontrar el artículo original aquí.

Discussion (0)