Número aleatorio
La aleatoriedad es complicada en la blockchain porque la blockchain es determinista, pero la aleatoriedad requiere no determinismo (de lo contrario, se vuelve predecible). Este artículo asume que el usuario ya tiene cierta familiaridad con solidity, especialmente con las operaciones block.number(), block.hash() y las firmas digitales.
Si utilizas datos como block.timestamp o el blockhash anterior, entonces alguien puede usar un smart contract para predecir si una transacción resultará en el resultado deseado o no. Capture the ether tiene una secuencia de hacks para ponerte a prueba en esto.
Si necesitas números aleatorios, hay algunos patrones de diseño que puedes considerar.
Commit Reveal
Aunque las transacciones de blockchain son perfectamente deterministas en el momento de la ejecución, no pueden predecir el futuro. Específicamente, el blockhash futuro no se puede predecir (con una advertencia que se describe a continuación).
Funciona de la siguiente manera: una transacción compromete que 20 bloques en el futuro desde el bloque actual n, cualquiera que sea el blockhash en 20 + n, ese blockhash será el número aleatorio. Dado que no se puede predecir el blockhash del bloque n + 2, donde n es el bloque actual, entonces el bloque n + 20 se considera aleatorio.
Se te permite una mirada retrospectiva (lookback) de 256 bloques para obtener la función hash, por lo que el usuario debe iniciar una segunda transacción en algún momento entre el bloque n + 20 y n + 276. La segunda transacción es la revelación (reveal). Por supuesto, el usuario puede ver si el número aleatorio salió a su favor o no, por lo que la aplicación debe estar configurada de tal manera que el usuario esté incentivado a enviar la segunda transacción si le resulta favorable.
Por ejemplo, un lanzamiento de moneda justo pagaría al usuario si, digamos, el blockhash fuera un número par. El usuario no haría la segunda transacción si viera que el blockhash es impar, pero de todos modos no gana nada. Para ganar, el blockhash debe ser par, y solo entonces se molestará en enviar la segunda transacción.
Este esquema puede ser alterado por los productores de bloques. Aunque los productores de bloques no pueden forzar un valor hash exacto, pueden reordenar las transacciones hasta que el hash del bloque produzca un número par (o algo más que les sea favorable).
Puedes defenderte de este comportamiento haciendo que el usuario comprometa el hash de un número secreto en el bloque n. En el bloque n + 20, el usuario revela la preimagen del hash y la preimagen se concatena con el blockhash. La concatenación de esos dos valores se somete a hash, y ese hash se utiliza como el número aleatorio.
El productor de bloques no puede conocer la preimagen del hash, por lo que no puede alterarlo. Pero aún así no es a prueba de productores de bloques.
Si el productor de bloques está jugando a la lotería, puede comprometer un número secreto conocido y luego alterar el blockhash de tal manera que el resultado final sea favorable.
Ahora que Ethereum ha pasado a proof of stake, este ataque es más difícil de llevar a cabo, porque el productor malicioso debe ser el productor de bloques en el bloque exacto de la revelación.
Pero si quieres estar seguro contra productores de bloques maliciosos, deberías usar Chainlink VRF (descrito a continuación).
Un atacante financieramente equipado puede explotar este esquema si está lo suficientemente motivado. Supongamos que pagamos a un jugador si el blockhash es par. El atacante puede inundar la red con transacciones de alto gas para evitar la transacción de revelación entre los bloques 20 y 276. Recuerda que el blockhash produce cero si el lookback es mayor a 256. Esto sería muy costoso para el atacante, pero sigue siendo un vector de ataque posible.
Chainlink VRF
Ya se ha escrito mucho en línea sobre cómo generar números aleatorios con Chainlink VRF (Verifiable Random Function). Su documentación es muy buena y fácil de seguir. Pero aquí explicamos cómo funciona en pocas palabras.
El smart contract que quiere un número aleatorio llama al smart contract de Chainlink solicitando un número aleatorio (y pagando algo de LINK para cubrir el costo).
Chainlink aceptará la solicitud, esperará un número especificado de bloques y llamará de vuelta (callback) al contrato que solicitó el número aleatorio. El algoritmo de Chainlink para generar el número aleatorio es transparente, por lo que cualquiera puede validar que fue creado de manera justa.
Chainlink no puede hacer esto en una sola transacción, o un jugador malicioso podría revertir la transacción si obtiene un resultado que no le gusta.
Debido a la posibilidad de una reorganización de la cadena (chain reorganization), la aplicación debe especificar que el callback ocurra más adelante en el futuro para casos de uso de alto valor.
Chainlink inicia la segunda transacción, lo que ahorra al usuario el problema de autorizar una segunda transacción. Sin embargo, hay un precio literal por esta conveniencia, ya que no solo el usuario debe pagar el costo del gas, sino que ellos (o la aplicación) también deben pagar con el token LINK para usar el servicio.
Firma offchain
Tengo que dar crédito a una conversación que tuve con gaspack.xyz, a quienes se les ocurrió el núcleo de esta idea.
Un problema obvio de UX con las soluciones anteriores es que requieren algún tipo de retraso, y potencialmente dos transacciones. En un juego blockchain, es posible que los jugadores no aprecien el retraso.
¿Cómo puedes hacer esto sin crear un smart contract vulnerable?
Una forma semidescentralizada de obtener números aleatorios es hacer que un generador de números aleatorios offchain cree y firme criptográficamente un número aleatorio, el remitente (sender) y un número de bloque futuro.
Ese número aleatorio se concatenará con el blockhash futuro, y a la cadena resultante se le aplicará una función hash. Esto produce el número aleatorio. El smart contract responsable de distribuir la recompensa verifica la firma con el remitente y el número de bloque.
Incluso si el generador de números aleatorios offchain no es perfectamente aleatorio, o es incluso un poco malicioso, no puede predecir los blockhashes futuros, y no sabe con qué se concatenará su número aleatorio. Debido a que la firma solo es válida en un bloque particular, el jugador no puede esperar a un bloque favorable antes de tirar los dados.
Hay tres formas en las que este esquema puede ser débil:
- El minero controla tanto el generador de números aleatorios como es el productor de bloques en el momento especificado. Siempre que el productor de bloques y el generador de números aleatorios no conspiren, este esquema es seguro.
- El número aleatorio produce el mismo número una y otra vez. En este caso, el minero puede predecir fácilmente cómo debería alterar el hash.
- Es necesario que haya una forma de evitar que los jugadores obtengan varios números aleatorios para que puedan intentarlo hasta que los resultados sean favorables. Esto necesariamente requiere algún tipo de control offchain que probablemente será menos transparente y seguro.
Es posible mitigar estos inconvenientes hasta cierto punto descentralizando aún más al productor de números aleatorios. Por ejemplo, podrías usar los hashes de bloques en la red bitcoin como fuente de números aleatorios. Esto hará que sea transparente si está ocurriendo algo sospechoso.
Publicado originalmente el 2 de diciembre de 2022