Escriba para buscar · Esc para cerrar

Escribir el protocolo dos veces

Hay una clase de error que ningún conjunto de pruebas atrapa, por minucioso que sea. No porque las pruebas sean malas, sino porque las pruebas y el código los escribió la misma persona, desde el mismo entendimiento, y coinciden en algo que está mal.

El error que coincide con su prueba

Normalmente, probar pregunta: ¿hace el código lo que espero? Usted escribe la expectativa como una aserción y la máquina la comprueba. Eso encuentra errores en el código y se pierde los errores en la expectativa.

Si leyó mal la especificación —tomó un valor amortiguado donde la fórmula pedía uno sin amortiguar, supuso que una operación cubre una unidad cuando puede cubrir muchas— escribirá la prueba con la misma lectura errónea. La prueba pasa. La cobertura es completa. El error sigue ahí, y desde dentro es invisible.

Cada prueba que usted escribe es una copia de su entendimiento. Donde el entendimiento está mal, la copia está mal en el mismo sitio, y las dos coinciden entre sí para siempre.

Pruebas diferenciales

La salida es construir una segunda implementación que no comparta las suposiciones de la primera, ejecutar ambas con las mismas entradas y comparar. Donde discrepen, al menos una está mal, y hay que volver a la especificación para averiguar cuál.

La independencia es todo el asunto, y es fácil perderla:

  • Escríbala desde la especificación, no desde el código. Leer el código primero importa sus suposiciones, y lo que se obtiene es una traducción cara en vez de una comprobación.
  • Use otro lenguaje. Otra aritmética, otro comportamiento ante desbordamientos, otros modismos: los desajustes que esto produce son informativos, no molestos.
  • Deje que sea lenta. La referencia no se va a desplegar. La claridad gana a la eficiencia; escriba la fórmula tal como la enuncia el documento.
  • Idealmente, otra persona. No siempre es posible. Cuando no lo sea, ponga tiempo entre ambas: una especificación leída de nuevo se lee distinta.

Qué encuentra en la práctica

Dos ejemplos de este proyecto, ambos hallados por comparación y ninguno por las pruebas:

  • Un par de valores mezclados. El cálculo del margen en una comprobación de seguridad usaba cifras sin amortiguar mientras la magnitud que protegía sí usaba el amortiguador. Todas las pruebas unitarias pasaban, porque calculaban igual. La referencia, escrita directamente desde la fórmula, dio otro número en un fondo pequeño, y en un fondo pequeño el umbral de seguridad podía romperse.
  • Un caso indefinido. El precio de emisión estaba especificado para una unidad, y las operaciones pueden cubrir muchísimas a la vez. El contrato y las pruebas asumieron en silencio el caso pequeño. Leído al pie de la letra, un depósito grande se habría llevado una fase entera al precio de apertura.

Ninguno es exótico. Ambos son el resultado corriente de que una sola mente escriba los dos lados.

Dónde sigue siendo útil la referencia

No es un ejercicio de una sola vez. Una vez existe, la referencia sigue rindiendo:

  • Las pruebas de propiedades pueden lanzar miles de entradas aleatorias a ambas y comparar: un fuzzer con un oráculo acoplado, en lugar de uno que solo busca caídas.
  • Los cambios se comprueban contra ella, así que una refactorización que altere el comportamiento en silencio aparece como una discrepancia.
  • Es legible para quien no lee Solidity, lo que abre la revisión a más ojos.
  • Cuando las dos discrepan y la especificación es ambigua, eso es un defecto de la especificación, encontrado antes de que nadie desplegara nada.

El truco relacionado: ataque sus propias pruebas

Una segunda pregunta es si las pruebas están mirando siquiera. Las pruebas de mutación la responden: se inyecta un defecto deliberado en el código compilado —invertir una comparación, cambiar una constante— y se ejecuta el conjunto. Si sigue pasando, ese defecto está en una zona que nada observa.

Es incómodo de una forma útil. La cobertura dice qué líneas se ejecutaron; la mutación dice si algo se habría dado cuenta de que esas líneas estaban mal. No es lo mismo, y solo lo segundo es una propiedad de las pruebas.

Lo que cuesta

Una implementación de referencia de un protocolo de este tamaño son días, no meses, precisamente porque se le permite ser lenta y sencilla. Frente al coste de encontrar un error de umbral después del despliegue, la cuenta no está reñida.

Common questions

¿Es lo mismo que la verificación formal?

No. La verificación formal demuestra que unas propiedades se cumplen para todas las entradas; las pruebas diferenciales comparan dos implementaciones en las entradas que usted prueba. La verificación es más fuerte y mucho más cara. Responden preguntas distintas y no son alternativas.

¿Sirve si la misma persona escribe ambas?

Menos que dos personas, más que nada, sobre todo con tiempo de por medio y con la especificación como fuente en vez del código. Los dos ejemplos de arriba se encontraron exactamente así.

¿Y si la equivocada es la referencia?

Ocurre, y sigue siendo una ganancia: una discrepancia le devuelve a la especificación, y sale sabiendo qué lectura es la correcta en lugar de suponerlo.

Cómo se hizo aquí

El protocolo de Assetrix se implementó dos veces: una en Solidity como contrato, y otra en Python desde el libro blanco, deliberadamente sin leer antes el contrato. Ambas se comparan con las mismas entradas, y los dos errores descritos arriba salieron de esa comparación.

El conjunto son 481 pruebas locales más 8 contra un fork de Arbitrum One en vivo, y él mismo se comprueba con pruebas de mutación contra el bytecode compilado. Nada de esto sustituye la revisión por personas que no lo escribieron: eso todavía no ha ocurrido, y la página de abajo lo dice con claridad.