Una buena revisión de código no es sólo una lectura completa del código, además de una especie de genialidad inalcanzable. Se trata de encontrar formas de pensar repetidamente en cada aspecto del sistema, construir un modelo mental del código y luego usar ese proceso para encontrar errores estúpidos, errores clásicos y errores profundos.
Una buena revisión de código funciona desde muchas perspectivas diferentes y alterna repetidamente desde niveles altos hasta los detalles más pequeños.
Me he centrado en la seguridad de los contratos inteligentes durante tres años y las revisiones de código son una gran parte de mi trabajo. Este es mi sistema para sacarles el máximo partido.
Las revisiones del código interno siguen siendo la forma más eficaz de encontrar errores. Idealmente, sus pruebas unitarias y pruebas de bifurcación pueden hacer el trabajo fácil de asegurarse de que el código "funcione".
La seguridad del contrato es un gradiente, no un valor booleano. Las pruebas difusas, las pruebas invariantes y las pruebas formales son capas fantásticas que aumentan las probabilidades de que el código sea correcto, pero el mayor avance en el gradiente de seguridad proviene de una buena revisión del código.
La corrección es increíblemente importante en los contratos inteligentes.
La mayoría de las otras industrias no tienen decenas o cientos de millones de dólares transferibles instantáneamente a una persona que encuentra y explota un error.
Si estás volando un avión: sí, el mundo es aleatorio y sí, hay muchos aviones, y sí, las fallas de hardware son un problema. Pero no es como si hubiera 10.000 tipos en París, Europa del Este, Corea del Norte, San Francisco y la naturaleza canadiense leyendo su código fuente, con cables conectados a su sistema de control de vuelo y cambiando agresivamente el clima, los relojes, el diseño del aeropuerto y irradiando radiación a su avión para poner su sistema en el estado específico donde el código comete un error. Los contratos inteligentes viven en un entorno inherentemente adverso.
Y no son sólo los malvados humanos: están todos los sistemas automatizados acechando en el bosque oscuro, constantemente hurgando en el código y listos para atacar la señal de dinero gratis.
No basta con que el código funcione. No basta con que funcione frente a la aleatoriedad. Tiene que funcionar frente a un enemigo adversario que busca una manera de cambiar el mundo que lo rodea para explotarlo. El código debe ser perfectamente correcto en todas las circunstancias. Es difícil para los humanos lograr un código perfecto en el primer intento.
La segunda razón por la que hacemos revisiones de código es que queremos mantenerlo simple.
A largo plazo, la simplicidad es lo que mantendrá alejados los errores y acelerará el desarrollo. Cada pizca de complejidad que queda en el sistema supone una carga de tiempo para cada parte del desarrollo de software futuro que hagamos, y la complejidad añade riesgo a todo lo que construyamos en el futuro.
La simplicidad no es algo que viene gratis. No es algo fácil. No se puede simplemente agitar una varita mágica y hacer realidad la simplicidad. Es algo que requiere tiempo, trabajo y algo de creatividad. La simplicidad es un problema multidimensional. A veces es necesario hacer concesiones entre diferentes enfoques hacia la simplicidad.
Debido a que la simplicidad es tan difícil, queremos opiniones, comentarios y perfección de varias personas del equipo sobre el código. Lo simple suele ser el resultado de un proceso colaborativo.
Si se tratara de una empresa web2, seleccionar la más pequeña línea de código de manera que no cambie el comportamiento sería una acción antisocial. Aquí, en nuestro código de Solidity, queremos perseguir tanta perfección como cada uno de nosotros pueda aportar.
La tercera razón por la que hacemos revisiones de código es que queremos compartir conocimientos con el equipo.
Contamos con un equipo de cuatro desarrolladores de contratos inteligentes. Cuando la persona que escribe el código realiza una revisión profunda y luego dos personas más también hacen una revisión profunda, tres cuartas partes de nuestro equipo ahora comprenden profundamente este nuevo código. Cuando el equipo comprende profundamente el código base, hay una diferencia importante tanto en la dinámica del equipo como en la capacidad de escribir código que funcione con el resto del sistema.
Estas revisiones de código también comparten técnicas de codificación y preocupaciones de seguridad en todo el equipo. Y el aprendizaje es en ambos sentidos. Puedo aprender tanto de lo que leo en las revisiones de código como de lo que la gente encuentra en las revisiones de mi código.
Por ejemplo, la semana pasada estaba revisando un código y vi la forma en que el desarrollador manejó la necesidad de bajar información desde un nivel superior en una jerarquía de herencia a un código base inferior. Más tarde esa semana utilicé lo que aprendí al manejar diferentes tipos de grupos de Curve a partir de estrategias que comparten el mismo código base.
Una revisión de código no consiste en leer el código una vez, comentar lo que ve y luego declararlo aprobado.
Retrocedamos un segundo y miremos lo que estamos tratando de hacer. Estamos tratando de encontrar todos los errores. Me gusta pensar que los errores se dividen en tres categorías.
El problema central es cómo encontrar errores profundos, sutiles y engañosos. Esto a menudo requiere construir un mejor modelo mental del código en su cabeza que el que tenía incluso la persona que escribió el sistema. ¿Cómo se llega a eso, especialmente desde un arranque en frío?
La solución es realizar bucles repetidos a través del código. Muchos, muchos, muchos repasan el código cada vez buscando un problema diferente, y cada vez construyen una imagen más clara, un modelo mental más rico del código dentro de su cabeza. Es realmente en la representación mental donde se encuentran los errores realmente difíciles.
Entonces el objetivo es:
Lo primero que hago es realizar una lectura de primer paso. Esto es para tener una idea de la estructura general del código para poder navegar bien en pasadas posteriores.
Ahora me encanta el papel para reseñas. Así que imprimo el código y, a menudo, elimino los comentarios del código en la primera lectura.
Luego escribo en mi papel cualquier pregunta que tenga, cualquier cosa que se vea mal o fea y las formas en que creo que el código podría estar roto. A menudo me equivoco en estas cosas; soy un poco optimista en cuanto a que las cosas se rompan.
Después de terminar mi primera lectura, reviso mis comentarios y verifico que realmente son problemas. Seguiré adelante y escribiré los reales en los comentarios de relaciones públicas en este momento. (Comenzar a trabajar detalladamente aquí es otro paso para desarrollar mi conocimiento del sistema).
El resto de mi proceso funciona a partir de un documento de lista de verificación de cosas a considerar/verificar. Lo bueno es que esto te obliga a pensar en el sistema desde diferentes aspectos. Y cuando simplemente se lee el código, es fácil olvidarse de buscar las cosas que no están ahí. Las listas de verificación ayudan a ver estos problemas de ausencia.
Nuestra lista de verificación es específica de nuestra propia base de código y estilo de código que queremos.
A medida que avanza en el uso de la lista de verificación, aumenta su conocimiento de las compilaciones del sistema y se detectan errores estúpidos y clásicos.
Algunas de las cosas en nuestra propia lista de verificación no son en realidad cosas que deban ser ciertas para que el código pase la revisión. Son cosas que si no son ciertas entonces debemos examinarlas con mucho cuidado y tener cuidado con el peligro. Por ejemplo, tenemos un elemento de la lista de verificación para "no utiliza Ethereum sin procesar" y casi todos nuestros contratos no lo hacen. Pero si tenemos que usar Ethereum sin procesar, entonces sabemos que debemos prestar especial atención a los posibles peligros y pensar realmente en lo que puede salir mal con esto.
Las listas de verificación son la mejor manera de encontrar errores comunes en su código.
Luego, el siguiente enfoque es pensar en las invariantes del sistema.
Las invariantes son cosas que siempre deben ser ciertas para que el sistema sea bueno. Es una forma tremendamente útil de pensar en el sistema y luego comprobar que en realidad siempre funciona como esperamos.
Puede dividirlas en "cosas que deben ser ciertas antes de que se ejecute este código", "cosas que deben ser ciertas después de que se ejecute este código" y "reglas sobre las relaciones de diferentes variables de estado".
Entonces, una vez que escribo estas invariantes, vuelvo a revisar el código y verifico que realmente se cumplen.
Las invariantes de estado son particularmente útiles: alguna parte del estado siempre debe tener una cierta relación con otra parte del estado para que el sistema sea bueno. Por ejemplo, digamos que si suma los saldos de cada cuenta, debería ser igual al saldo total en el sistema. Una buena forma de crear invariantes de estado es recorrer la lista de variables que no son de configuración en su contrato y, para cada una, pensar cómo deberían relacionarse entre sí.
El pensamiento basado en invariantes es probablemente el arma secreta más subestimada para revisar código y encontrar errores.
El siguiente paso es pensar en atacar los contratos.
Pasar a pensar en su código desde el punto de vista de un atacante es un poderoso cambio de perspectiva mental: como desarrollador, es pensar en cómo funcionará el código, en lugar de en qué situaciones puede ponerlo para romperlo.
En lugar de simplemente "pensar en atacar los contratos", la mejor manera que he encontrado para hacerlo es simplemente comenzar a escribir ideas de ataque primero sin siquiera ver si son válidas o no. Simplemente envío spam sobre cosas que podrían salir mal y su posible impacto en el peor de los casos.
Luego, después de haber escrito un montón de ataques, escribo por qué no se pueden realizar cada uno, nuevamente, todos a la vez sin verificar el código. Como paso final, reviso y verifico, en el código, que lo que pensé sobre lo que bloquearía estos ataques es cierto. La perspectiva que cambia del pensamiento de "ataque" a "defensa" y luego a "ataque" es útil y en este paso, como en todos los demás, estoy construyendo mi modelo mental del código.
El código de implementación, la configuración, el monitoreo requerido y las acciones de gobierno son una parte tan importante del sistema final como lo es el código. Verifique que todo aquí sea correcto, incluida la verificación manual de que todas las direcciones sean correctas.
Simulaciones de batalla: pruebas de bifurcación
Por último, hay mucho valor en unos pocos minutos de prueba de cordura. Es sorprendente lo que encuentras simplemente jugando con un sistema. El código a menudo se escribe para pasar las pruebas, pero puede hacer cosas incorrectas cuando se usa con números u orden de operaciones diferentes a los de las pruebas. Esto también garantiza que la acción de implementación y gobernanza dé como resultado un sistema que funcione.
Por lo general, registro las pruebas de bifurcación que hago aquí, las limpio un poco y luego las guardo para usarlas nuevamente una vez que el código se haya implementado.
Cuando se ha escrito el primer borrador del código, me gusta hacer una revisión informal de primer paso, que no se parece en nada a todo este proceso. Esto nos permite realizar correcciones de diseño antes de desarrollar los conjuntos de pruebas y realmente agregar calidad al sistema.
Entonces, nuestras revisiones internas reales ocurren después de que se escribe el código y el propietario de este conjunto de códigos está satisfecho con el código y las pruebas. Primero, el propietario hace su propia revisión y luego etiqueta a los otros dos miembros del equipo para que realicen sus revisiones.
El propietario del código siempre debe realizar la primera revisión. Actualmente son la persona que entiende este código mejor que nadie en todo el universo, por lo que al forzar algunas perspectivas nuevas, tienen bastantes posibilidades de encontrar un error. En segundo lugar, esto permite al propietario obtener información inmediata sobre lo buena que es su reseña, ya que si el propietario no se da cuenta del error, es de esperar que los otros dos revisores lo hagan.
Quiere realizar nuestro proceso de revisión interna completo antes de enviar el código a los auditores. De esta manera, la auditoría externa es un control de su proceso interno. Si un auditor encuentra un error importante, entonces debe regresar y descubrir cómo cambiar su proceso de revisión interna para que este tipo de error no vuelva a aparecer.
La clave para encontrar los errores más difíciles es observar el sistema desde muchas perspectivas diferentes y en muchos niveles diferentes. ¡Y use una lista de verificación para los errores fáciles!