El propósito del desarrollo de software es lograr que funcione según lo previsto. Se requieren múltiples iteraciones de prueba para lograrlo, y el tiempo entre iteraciones es de importancia crítica cuando se intenta completar la tarea en cuestión lo antes posible.
En Origin, utilizamos pruebas unitarias y de bifurcación para el desarrollo. La ejecución de pruebas de bifurcación puede ser tediosamente lenta, pero hemos logrado reducir el tiempo de ejecución de las pruebas de bifurcación (en algunos casos) en un factor de 4. A través de este artículo, explicaré cómo puedes mejorar la velocidad de tus pruebas de bifurcación para que pueda hacer que su código funcione según lo previsto en poco tiempo.
Normalmente, el uso de una prueba de bifurcación o una prueba unitaria depende de la arquitectura del protocolo que se está desarrollando. Algunos equipos solo usan pruebas unitarias y otros, tanto pruebas unitarias como de bifurcación. En Origin, nuestros contratos interactúan con varios protocolos de terceros (Curve, Uniswap, Balancer, Convex…) y los errores en la integración pueden causar pérdidas catastróficas de fondos.
Utilizando pruebas unitarias, nos burlamos del ABI del protocolo de terceros y probamos que nuestros contratos se integren correctamente mediante programación. Pero probar el comportamiento de un protocolo de terceros con todas las complejidades y varios estados (legítimos o manipulados) en los que puede encontrarse el protocolo es donde brillan las pruebas de bifurcación. Y para algunos ingenieros de nuestro equipo, el desarrollo basado en pruebas mediante pruebas de bifurcación es el modo de desarrollo principal. Reducir el tiempo de “cambio de código” al tiempo de “ejecución de prueba” es esencial.
En Origin, utilizamos Hardhat para nuestra pila de desarrollo de Solidity. Es importante que comprendamos los distintos pasos que componen una ejecución de prueba de bifurcación para reducir el tiempo del ciclo de desarrollo. Para simplificar, ejecutaremos solo una prueba de bifurcación a la vez, que suele ser el enfoque cuando se trabaja en un contrato inteligente.
El enfoque más lento es ejecutar las pruebas de bifurcación con la bifurcación de nodos del último bloque. De esta manera, cada vez que se ejecutan pruebas se elige una altura de bloque diferente y el casco no puede realizar ningún almacenamiento en caché leyendo las ranuras de almacenamiento del proveedor de la red principal. Esto se refleja en la lenta velocidad de ejecución de 101 segundos.
Esta prueba de bifurcación en particular envía fondos a una de nuestras estrategias de extracción de liquidez que despliega activos en un grupo/pool de Balancer. Esto provoca una gran cantidad de lecturas de ranuras de almacenamiento y es la causa principal de la parte lenta de "prueba" de la ejecución (parte naranja de la barra de arriba).
La fruta más fácil y obvia es fijar la altura del bloque para que Hardhat pueda ahorrar una gran cantidad de tiempo utilizando un caché local de lecturas de ranuras de almacenamiento. El tiempo de compilación no se ve afectado por esta optimización, pero sí todos los demás pasos. Reduciendo en gran medida el tiempo del ciclo a 27 segundos (esto se aplica a todas las ejecuciones de la prueba que no sean las primeras, donde el caché del casco ya está calentado).
El enfoque anterior mejora significativamente la situación, pero aún está lejos de la velocidad de las pruebas unitarias y, a veces, todavía puede parecer frustrantemente lento. Sólo hemos cambiado un contrato. ¿Por qué necesitamos volver a ejecutar las mismas implementaciones y posteriores a la implementación cuando, idealmente, solo es necesario cambiar el código de bytes de 1 (o más) contratos de Solidity? Dado que, por supuesto, no cambiaremos el diseño o el estado de las ranuras de almacenamiento entre compilaciones. Ingrese a las implementaciones en caliente.
Hot-deploy implementa el contrato que se está modificando y recupera su código de bytes. Utilizando instantáneas del conjunto de pruebas de casco, encuentra la versión anterior de ese contrato en un estado antes de que se ejecutaran las pruebas y reemplaza su código de bytes con el recién compilado. Además de eso, ejecuta los dispositivos de prueba y la prueba en sí, omitiendo todos los pasos (posteriores) a la implementación.
El tiempo del ciclo ahora se reduce a sólo 7 segundos. Este cambio en el ciclo de desarrollo ha ayudado a Origin Protocol a mejorar en gran medida la productividad y la velocidad a la que podemos desarrollar funciones.
Desafortunadamente, las implementaciones en caliente no se pueden activar como un conmutador (al menos no todavía) y requieren cierta configuración. La forma en que hemos implementado la solución en Origin es la siguiente:
Un requisito importante es que un nodo de casco independiente se ejecute por separado del entorno de prueba de la bifurcación. Puede que no sea inmediatamente obvio, pero la ejecución de prueba de bifurcación creará otro tiempo de ejecución de nodo y utilizará el otro nodo en ejecución independiente como proveedor. El nodo independiente ejecutará todas las implementaciones (posteriores), y el nodo de prueba de bifurcación compilará los cambios, ejecutará dispositivos de prueba reemplazando cualquier código de bytes de contrato requerido y, finalmente, ejecutará la prueba.
Es bueno tener en cuenta los cambios en los archivos de implementación, las ranuras de almacenamiento del contrato y su estado de configuración. Debe considerar que la ejecución de dispositivos de prueba requiere un reinicio del nodo independiente.
A veces podemos sentirnos tentados a utilizar pruebas unitarias para partes de la plataforma que se integran con protocolos de terceros solo para poder desarrollarnos más rápido... a costa de la seguridad adicional que puede proporcionar una prueba de bifurcación. Para los desarrolladores de Origin, las implementaciones en caliente son un paso importante para cerrar la brecha de tiempo del ciclo y tratar de tener nuestro pastel y comérselo también. Espero que otros prueben esto y espero que usted pueda beneficiarse de este enfoque para reducir los tiempos del ciclo de desarrollo.