Números aleatorios en Solidity con Chainlink VRF

Resumen

Generar números aleatorios en Solidity es un reto técnico porque la blockchain no tiene reloj propio ni fuentes de entropía confiables. La solución pasa por usar un oráculo como Chainlink VRF, que entrega valores verificables desde fuera de la red. Aquí verás cómo integrarlo en un contrato de Tic Tac Toe para decidir qué jugador inicia la partida.

Por qué Solidity no puede generar números aleatorios confiables

La máquina virtual de Ethereum es determinista, así que cualquier intento de generar aleatoriedad usando block.timestamp o blockhash puede ser manipulado por validadores o atacantes. Por eso se recurre a fuentes externas que firmen criptográficamente el valor entregado.

¿Qué es un oráculo en blockchain? Es un servicio que conecta un contrato inteligente con datos del mundo exterior. Chainlink es uno de los más usados y permite recibir números aleatorios verificables on-chain.

En el ejemplo se trabaja sobre la testnet de Sepolia y con el modelo de suscripción de Chainlink, que permite financiar varias solicitudes desde una misma cuenta sin pagar token por llamada directa [00:50].

Cómo se hereda de VRFConsumerBase para pedir aleatoriedad

La integración requiere dos referencias importadas desde Chainlink: el contrato Coordinator, que ya vive en la red y entrega los números, y VRFConsumerBase, del cual heredamos para recibir la respuesta asíncrona [02:30].

El flujo se divide en dos partes:

  • Una función que dispara la solicitud y devuelve un requestId.
  • Una función override que captura la respuesta cuando el coordinador la entrega.

Esa separación existe porque la respuesta no es instantánea: depende de la cantidad de confirmaciones configuradas, equivalentes a bloques minados [05:10].

Qué datos guardar en el constructor

Dos valores quedan fijos al desplegar el contrato y por eso se almacenan en el constructor:

  1. La dirección del Coordinator, que no cambia mientras uses la misma red.
  2. El subscriptionId, declarado como uint64 porque ese es el tamaño que exige Chainlink.

Como VRFConsumerBase tiene un constructor con parámetros, hay que pasarle explícitamente la dirección del coordinador al heredar. Solidity lo exige siempre que el contrato padre reciba argumentos [04:20].

Cómo funciona requestRandomWords y sus parámetros

La llamada que dispara la generación es requestRandomWords, y recibe cinco parámetros que conviene entender uno a uno:

  • keyHash: identifica la red y el nivel de gas. Cambia según la testnet o mainnet usada.
  • subscriptionId: el ID guardado en el constructor.
  • requestConfirmations: cuántos bloques esperar antes de recibir el número. El valor por defecto es 3.
  • callbackGasLimit: tope de gas para la respuesta. En el ejemplo se usan 100.000 unidades.
  • numWords: cuántos números aleatorios pedir en una sola solicitud.

Si necesitaras inicializar varias partidas a la vez, podrías pedir un array completo de números en una sola llamada en lugar de hacer múltiples requests [08:40].

¿Cuánto tarda Chainlink VRF en responder? Depende del tiempo de bloque y de las confirmaciones configuradas. Con 3 confirmaciones y bloques de 12 segundos, son unos 36 segundos antes de recibir la respuesta.

Cómo asociar la respuesta del oráculo a la partida correcta

Cada solicitud devuelve un requestId único. Si tu contrato gestiona muchas partidas en paralelo, necesitas saber a cuál pertenece cada respuesta. La solución elegante es un mapping que asocia requestId con el ID de la partida.

Así, cuando llega la respuesta a la función fulfillRandomWords, basta con consultar el mapping para encontrar la partida correspondiente y aplicar la lógica que decide quién empieza [11:00].

El truco para asignar el primer jugador

En lugar de reescribir toda la lógica del Tic Tac Toe, se reutiliza el campo últimoTurno. La regla del juego ya impide que el último jugador en mover repita turno, así que setear ese valor equivale a forzar al otro a empezar.

La decisión se toma con paridad:

  • Si el número aleatorio es par, el último turno se asigna al jugador uno y empieza el dos.
  • Si es impar, ocurre lo contrario.

Como Chainlink devuelve números muy grandes, se usa el operador módulo para acotar el rango. Recuerda que el módulo entrega el resto de la división, así que para detectar paridad comparas random % 2 == 0 [12:30].

Cómo evitar que alguien juegue antes de recibir el número

Durante los segundos que tarda Chainlink en responder, un jugador podría intentar mover. Para bloquear ese hueco se agrega una restricción en la función de jugar: la partida solo acepta movimientos si últimoTurno es distinto de la dirección cero.

Como por defecto las direcciones se inicializan en address(0), esa simple comparación garantiza que la aleatoriedad ya se resolvió antes de permitir el primer movimiento [14:10].

Con esto el contrato queda listo para compilar y desplegarse en Sepolia. ¿Te animas a probarlo y contarme si lograste financiar tu suscripción de Chainlink desde la primera vez?