tutoriales.com

Gestionando la Aleatoriedad On-Chain: Desarrollando Contratos Inteligentes con Verifiable Random Functions (VRF) de Chainlink

Este tutorial te guiará a través del proceso de integrar funciones de aleatoriedad verificable (VRF) de Chainlink en tus contratos inteligentes de Solidity. Descubrirás cómo la aleatoriedad on-chain segura es crucial para aplicaciones descentralizadas justas, desde juegos hasta NFTs y sorteos, y cómo implementarla paso a paso.

Intermedio20 min de lectura9 views
Reportar error

🚀 Introducción a la Aleatoriedad Verificable On-Chain

En el mundo de las aplicaciones descentralizadas (dApps), la aleatoriedad es un componente fundamental para una amplia gama de casos de uso. Desde la creación de características únicas para NFTs generativos hasta la determinación de ganadores en sorteos o la lógica de combate en juegos blockchain, la capacidad de introducir un elemento de azar es crucial para la equidad y la imprevisibilidad. Sin embargo, generar aleatoriedad on-chain de manera segura y confiable presenta desafíos significativos.

¿Por qué la aleatoriedad es un desafío en Blockchain? 🤯

Las blockchains son sistemas deterministas. Esto significa que cada nodo de la red debe poder replicar el mismo resultado de una transacción para verificar su validez. Si la aleatoriedad dependiera de factores internos del contrato o de datos fácilmente manipulables (como block.timestamp o block.difficulty), un atacante podría predecir o influir en el resultado, comprometiendo la seguridad y la justicia de la aplicación. Esto se conoce como el problema de la aleatoriedad predecible o manipulable.

⚠️ Advertencia: Nunca utilices `block.timestamp`, `block.difficulty`, `blockhash` (obsoleto o con alcance limitado) o cualquier otro dato fácilmente predecible o manipulable por un minero/validador como fuente de aleatoriedad en tus contratos inteligentes. Son inseguros y explotables.

La solución: Funciones de Aleatoriedad Verificable (VRF) ✨

Aquí es donde entran en juego las Verifiable Random Functions (VRF). Chainlink VRF es un servicio de oráculo descentralizado que proporciona números aleatorios verificables y a prueba de manipulaciones a los contratos inteligentes. Funciona generando pruebas criptográficas que demuestran que el número aleatorio fue generado correctamente y sin sesgos, y que no fue manipulado. Estas pruebas se verifican on-chain, asegurando la integridad del proceso.

🎯 ¿Cómo funciona Chainlink VRF? Una mirada a fondo

Chainlink VRF opera utilizando un sistema de criptografía de curva elíptica para generar números aleatorios junto con una prueba criptográfica que verifica su autenticidad.

🔥 Importante: La característica clave de Chainlink VRF es que la aleatoriedad es *verificable* on-chain. Esto elimina la confianza en un tercero centralizado y garantiza que el número aleatorio es genuino y no ha sido alterado.

El proceso paso a paso 👣

  1. Solicitud del Contrato Inteligente: Tu contrato inteligente solicita un número aleatorio a Chainlink VRF. Para ello, llama a la función requestRandomWords en el contrato VRFCoordinatorV2Interface de Chainlink, pasando un keyHash, la tarifa de gas, un número de palabras aleatorias y un callbackGasLimit.
  2. Generación Off-Chain: Un nodo Chainlink VRF recibe la solicitud. Utiliza su clave privada para generar un número aleatorio y una prueba criptográfica. La aleatoriedad se deriva de una combinación de su clave secreta y un seed generado a partir del requestId de la solicitud y el blockhash del bloque donde se generó la respuesta.
  3. Transacción de Cumplimiento: El nodo Chainlink envía una transacción de vuelta a la blockchain. Esta transacción contiene el número aleatorio generado y la prueba criptográfica. Llama a la función rawFulfillRandomWords en el contrato VRFCoordinatorV2.
  4. Verificación On-Chain: El contrato VRFCoordinatorV2 verifica la prueba criptográfica utilizando la clave pública del nodo Chainlink. Si la prueba es válida, el número aleatorio se considera auténtico.
  5. Entrega al Contrato Solicitante: Finalmente, el contrato VRFCoordinatorV2 llama a la función fulfillRandomWords en tu contrato inteligente, pasándole el requestId original y los números aleatorios generados. Tu contrato implementa esta función para recibir y utilizar el valor aleatorio.
Tu Contrato Inteligente VRFCoordinatorV2 6. Verifica Prueba Criptográfica Nodo Chainlink VRF (Off-chain) 4. Genera Número + Prueba 1. requestRandomWords 2. Solicitud al Nodo 5. rawFulfillRandomWords 7. fulfillRandomWords

🛠️ Requisitos previos para la implementación

Antes de sumergirnos en el código, asegúrate de tener lo siguiente:

  • Conocimientos básicos de Solidity y desarrollo de contratos inteligentes.
  • Un entorno de desarrollo Solidity: Remix IDE, Hardhat, Truffle, etc.
  • MetaMask o una billetera compatible para interactuar con la red de prueba (testnet).
  • Fondos de prueba (ETH) en una testnet: Sepolia es la testnet recomendada por Ethereum.
  • Tokens LINK de prueba en la misma testnet: Necesarios para pagar las tarifas del servicio VRF de Chainlink.

Configuración de Chainlink VRF 📌

Para interactuar con Chainlink VRF, necesitarás algunos parámetros específicos para la red de prueba que estés utilizando. Estos se obtienen de la documentación oficial de Chainlink VRF. Para Sepolia, necesitarás:

  • VRF_COORDINATOR_V2: La dirección del contrato VRFCoordinatorV2 en Sepolia.
  • KEY_HASH: Un keyHash es el ID del tipo de clave pública del nodo Chainlink VRF que utilizará la red Chainlink para cumplir tu solicitud. Esto impacta el costo de gas del cumplimiento. Selecciona uno adecuado de la documentación.
  • SUBSCRIPTION_ID: Si utilizas el modelo de financiación de suscripción (recomendado), necesitarás una suscripción. Crearemos una en el siguiente paso. Si usas financiación directa, el proceso es ligeramente diferente y más complejo.
  • MINIMUM_CONSUMER_GAS_LIMIT: Un límite de gas mínimo recomendado para la función fulfillRandomWords en tu contrato.
💡 Consejo: Siempre consulta la documentación oficial de Chainlink para obtener las últimas direcciones y parámetros de configuración para la red que estés utilizando.

📝 Creando y Financiando una Suscripción VRF (Modelo de Suscripción)

El modelo de suscripción es la forma más sencilla y recomendada de financiar tus solicitudes de VRF. Un contrato de SubscriptionManager retiene tokens LINK y los distribuye entre los contratos consumidores (tus contratos inteligentes) según sus solicitudes.

Pasos para crear y financiar una suscripción:

  1. Visita la UI de Chainlink VRF: Navega a la interfaz de usuario de Chainlink VRF para la red de prueba Sepolia.
  2. Conecta tu billetera: Asegúrate de que MetaMask esté configurado para la red Sepolia.
  3. Crea una nueva suscripción: Haz clic en 'Create new subscription'. Esto generará un Subscription ID único para ti. Anótalo, lo necesitarás más adelante en tu contrato.
  4. Financia la suscripción: Una vez creada, selecciona tu nueva suscripción y haz clic en 'Add funds'. Envía una cantidad suficiente de tokens LINK (por ejemplo, 1 LINK) desde tu billetera a la dirección de tu suscripción. Esto garantizará que tus solicitudes de VRF puedan ser cumplidas.
  5. Añade tu contrato como consumidor: Cuando despliegues tu contrato inteligente, necesitarás añadir su dirección como un 'Consumer' a tu suscripción. Lo haremos después de desplegar nuestro contrato.
¡Suscripción lista!

✍️ Desarrollando el Contrato Inteligente Solicitante

Ahora vamos a escribir el contrato Solidity que solicitará y recibirá los números aleatorios de Chainlink VRF. Usaremos Hardhat para este ejemplo.

1. Inicializar el proyecto Hardhat

Si aún no tienes un proyecto Hardhat, créalo:

npx hardhat init

Selecciona 'Create a basic sample project'.

2. Instalar el contrato VRFCoordinatorV2Interface

Necesitamos instalar los contratos de interfaz de Chainlink VRF a través de npm para poder importarlos en nuestro contrato.

npm install @chainlink/contracts

3. Crear el contrato RandomNumberConsumer.sol

Crea un nuevo archivo contracts/RandomNumberConsumer.sol y añade el siguiente código. Este contrato será un Consumer de VRF. Heredará de VRFConsumerBaseV2 para manejar la lógica de la suscripción y el callback.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

contract RandomNumberConsumer is VRFConsumerBaseV2 {

    VRFCoordinatorV2Interface COORDINATOR;

    // Guarda la ID de la suscripción Chainlink VRF que usamos para financiar las solicitudes.
    uint64 s_subscriptionId;

    // keyHash es el ID del tipo de clave pública del nodo VRF.
    // Consulta la documentación de Chainlink VRF para la red que usas.
    bytes32 s_keyHash;

    // El límite de gas para el callback de fulfillRandomWords.
    // Cuanto más alto, más operaciones puede realizar tu contrato en el callback.
    uint32 s_callbackGasLimit;

    // El precio de la tarifa de gas para el cumplimiento, ajustado por Chainlink.
    uint32 s_requestConfirmations;

    // El número de palabras aleatorias a solicitar.
    uint32 numWords;

    // Almacena las últimas palabras aleatorias generadas.
    uint256[] public s_randomWords;

    // El ID de la última solicitud de aleatoriedad.
    uint256 public s_requestId;

    // Evento para emitir cuando se reciben palabras aleatorias.
    event RequestSent(uint256 requestId, uint32 numWords);
    event RequestFulfilled(uint256 requestId, uint256[] randomWords);

    constructor(
        uint64 subscriptionId,
        address vrfCoordinator,
        bytes32 keyHash,
        uint32 callbackGasLimit,
        uint32 requestConfirmations,
        uint32 wordsToRequest
    ) VRFConsumerBaseV2(vrfCoordinator) {
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        s_subscriptionId = subscriptionId;
        s_keyHash = keyHash;
        s_callbackGasLimit = callbackGasLimit;
        s_requestConfirmations = requestConfirmations;
        numWords = wordsToRequest;
    }

    // Solicita palabras aleatorias del Chainlink VRFCoordinator.
    function requestRandomWords() public returns (uint256 requestId) {
        // Will revert if subscription is not funded or if there are any other errors.
        s_requestId = COORDINATOR.requestRandomWords(
            s_keyHash,
            s_subscriptionId,
            s_requestConfirmations,
            s_callbackGasLimit,
            numWords
        );
        emit RequestSent(s_requestId, numWords);
        return s_requestId;
    }

    // Callback que recibe las palabras aleatorias del Chainlink VRFCoordinator.
    // Solo puede ser llamado por el VRFCoordinator.
    function fulfillRandomWords(
        uint256 requestId,
        uint256[] memory randomWords
    ) internal override {
        require(s_requestId == requestId, "Request ID mismatch");
        s_randomWords = randomWords;
        emit RequestFulfilled(requestId, randomWords);
    }

    // Para propósitos de depuración, permite al propietario ver el saldo de LINK de la suscripción.
    function getSubscriptionBalance() public view returns (uint256) {
        // Esto solo es válido si el contrato es también el propietario de la suscripción.
        // En este ejemplo, el contrato es un consumidor, no el propietario.
        // Para ver el saldo real de la suscripción, usa la UI de Chainlink VRF.
        return COORDINATOR.getSubscription(s_subscriptionId).balance;
    }
}

Explicación del Código 📖

  • import: Importa las interfaces necesarias de los contratos Chainlink.
  • VRFCoordinatorV2Interface COORDINATOR: Una instancia del contrato VRFCoordinatorV2 de Chainlink, usado para interactuar con el servicio VRF.
  • s_subscriptionId: La ID de la suscripción VRF que creaste y financiaste.
  • s_keyHash: El keyHash de VRF para la red Sepolia.
  • s_callbackGasLimit: El límite de gas para la función fulfillRandomWords. Asegúrate de que sea lo suficientemente alto para cubrir todas las operaciones que tu contrato realizará al recibir el número aleatorio.
  • s_requestConfirmations: El número de confirmaciones de bloque que el oráculo Chainlink esperará antes de responder. Un valor más alto (3 es común) aumenta la seguridad a expensas de la latencia.
  • numWords: Cuántos números aleatorios quieres recibir en una sola solicitud.
  • s_randomWords: Un array público para almacenar los números aleatorios generados.
  • constructor: Inicializa las variables con los parámetros específicos de Chainlink VRF y la suscripción.
  • requestRandomWords(): Esta es la función que tu contrato llamará para solicitar aleatoriedad. Internamente, llama a requestRandomWords en el VRFCoordinatorV2 de Chainlink.
  • fulfillRandomWords(): Esta es una función internal override que tu contrato debe implementar. El VRFCoordinatorV2 la llamará para entregarte los números aleatorios generados. Aquí es donde tu lógica para usar los números aleatorios debe ir. Es crucial que esta función sea eficiente en gas, ya que el callbackGasLimit la restringe.

4. Compilar el contrato

npx hardhat compile

5. Script de despliegue deploy.js

Crea un archivo scripts/deploy.js con el siguiente contenido. Asegúrate de reemplazar los placeholders con tus valores reales.

const { ethers } = require("hardhat");

async function main() {
    // Parámetros de Sepolia para Chainlink VRF V2. Consulta la documentación oficial de Chainlink.
    const VRF_COORDINATOR_V2 = "0x8103525829D64A7822501a5eA4F8bC75D859664D"; // VRF Coordinator para Sepolia
    const KEY_HASH = "0x474e34a077df58807dbe9c96d3c009b23bc3c605ce5fa17bb22698aef2794eed"; // Key Hash para Sepolia
    const CALLBACK_GAS_LIMIT = 500000; // Un límite de gas generoso para la función fulfillRandomWords
    const REQUEST_CONFIRMATIONS = 3; // Número de confirmaciones de bloque a esperar
    const WORDS_TO_REQUEST = 1; // Número de palabras aleatorias a generar por solicitud

    // ⚠️ Reemplaza con tu Subscription ID real de Chainlink VRF UI
    const SUBSCRIPTION_ID = YOUR_SUBSCRIPTION_ID; // EJEMPLO: 1234

    if (!SUBSCRIPTION_ID) {
        throw new Error("Por favor, establece tu SUBSCRIPTION_ID. Visita https://vrf.chain.link/sepolia");
    }

    console.log("Desplegando contrato RandomNumberConsumer...");

    const RandomNumberConsumer = await ethers.getContractFactory("RandomNumberConsumer");
    const randomNumberConsumer = await RandomNumberConsumer.deploy(
        SUBSCRIPTION_ID,
        VRF_COORDINATOR_V2,
        KEY_HASH,
        CALLBACK_GAS_LIMIT,
        REQUEST_CONFIRMATIONS,
        WORDS_TO_REQUEST
    );

    await randomNumberConsumer.deployed();

    console.log(`RandomNumberConsumer desplegado en: ${randomNumberConsumer.address}`);

    // 📌 IMPORTANTE: Después de desplegar, añade la dirección del contrato como 'Consumer'
    // en tu suscripción en la UI de Chainlink VRF (https://vrf.chain.link/sepolia).
    // La dirección es: ${randomNumberConsumer.address}

}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});
⚠️ Advertencia: Asegúrate de que `YOUR_SUBSCRIPTION_ID` se reemplaza con la ID numérica de tu suscripción real. No olvides añadir la dirección de tu contrato desplegado como 'Consumer' en la UI de Chainlink VRF después de desplegar.

6. Desplegar el contrato

Para desplegar en Sepolia, asegúrate de tener configurado Sepolia en tu hardhat.config.js y de que tu variable de entorno ALCHEMY_API_KEY o INFURA_API_KEY y PRIVATE_KEY estén configuradas.

// hardhat.config.js (ejemplo)
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY;
const PRIVATE_KEY = process.env.PRIVATE_KEY;

module.exports = {
  solidity: "0.8.19",
  networks: {
    sepolia: {
      url: `https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
      accounts: [PRIVATE_KEY],
    },
  },
};

Despliega el contrato:

npx hardhat run scripts/deploy.js --network sepolia

Una vez desplegado, la consola te dará la dirección de tu contrato. Copia esta dirección.

✅ Añadir el Contrato como Consumidor en Chainlink VRF UI

Ahora que tu contrato está desplegado, vuelve a la UI de Chainlink VRF, selecciona tu suscripción y haz clic en 'Add consumer'. Pega la dirección de tu contrato desplegado y confírmalo. Esto permite que tu contrato utilice los fondos de tu suscripción para sus solicitudes de VRF.

Chainlink VRF UI Seleccionar Suscripción Botón: Añadir Consumidor Campo: Dirección Contrato Botón: Confirmar

🧪 Interactuando con el Contrato y Solicitando Aleatoriedad

Ahora que todo está configurado, podemos interactuar con nuestro contrato para solicitar y recibir números aleatorios.

1. Solicitar números aleatorios

Puedes interactuar con tu contrato desplegado a través de Etherscan (si el contrato ha sido verificado) o directamente con un script de Hardhat.

Ejemplo de script para solicitar y leer:

Crea un nuevo script scripts/interact.js:

const { ethers } = require("hardhat");

async function main() {
    // Reemplaza con la dirección de tu contrato desplegado
    const CONTRACT_ADDRESS = "YOUR_DEPLOYED_CONTRACT_ADDRESS"; 

    if (!CONTRACT_ADDRESS) {
        throw new Error("Por favor, establece la dirección de tu contrato desplegado.");
    }

    const randomNumberConsumer = await ethers.getContractAt("RandomNumberConsumer", CONTRACT_ADDRESS);

    console.log("Solicitando números aleatorios...");
    const tx = await randomNumberConsumer.requestRandomWords();
    const receipt = await tx.wait();
    console.log("Solicitud de números aleatorios enviada. Transaction Hash:", receipt.transactionHash);

    // Espera un poco a que el oráculo de Chainlink procese la solicitud y envíe el callback.
    // Esto puede tardar de unos segundos a un par de minutos dependiendo de la congestión de la red.
    console.log("Esperando a que el oráculo de Chainlink cumpla la solicitud...");

    // Puedes configurar un listener para el evento RequestFulfilled si estás en un script de larga ejecución
    // o simplemente consultar el estado del contrato después de un tiempo.

    let randomWords = await randomNumberConsumer.s_randomWords();
    while (randomWords.length === 0) {
        await new Promise(resolve => setTimeout(resolve, 5000)); // Espera 5 segundos
        randomWords = await randomNumberConsumer.s_randomWords();
        console.log("Aún esperando palabras aleatorias...");
    }

    console.log("Números aleatorios recibidos:", randomWords.map(word => word.toString()));

    // Para propósitos de demostración, imprimimos el saldo de LINK de la suscripción (solo disponible si el contrato es dueño de la suscripción)
    // Para el saldo real, consulta la UI de Chainlink VRF.
    // const subscriptionBalance = await randomNumberConsumer.getSubscriptionBalance();
    // console.log("Saldo de LINK de la suscripción (desde el contrato):", ethers.utils.formatEther(subscriptionBalance));
}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});

Ejecuta el script de interacción:

npx hardhat run scripts/interact.js --network sepolia

Este script primero solicitará un número aleatorio. Luego, esperará (consultando cada 5 segundos) hasta que la función fulfillRandomWords en tu contrato sea llamada por el VRFCoordinatorV2 y tu variable s_randomWords se actualice con el resultado.

💡 Consejo: Observa los eventos en tu consola o en Etherscan. Verás un evento `RequestSent` cuando tu contrato haga la solicitud y, después de un tiempo, un evento `RequestFulfilled` cuando Chainlink VRF entregue la aleatoriedad.

Consideraciones sobre el gas y los límites de callback ⛽

Cuando solicitas aleatoriedad, debes especificar un callbackGasLimit. Este es el límite máximo de gas que el VRFCoordinatorV2 usará para ejecutar tu función fulfillRandomWords. Es crucial que todas las operaciones dentro de fulfillRandomWords se mantengan dentro de este límite. Si excede el límite, la transacción de cumplimiento fallará, pero tu suscripción seguirá siendo cargada por la solicitud.

📌 Nota: Es una buena práctica mantener la lógica dentro de `fulfillRandomWords` lo más simple posible, posiblemente almacenando el resultado y emitiendo un evento, para que otras funciones puedan reaccionar a la aleatoriedad más tarde.

💡 Casos de uso de VRF en dApps

La aleatoriedad segura y verificable abre un mundo de posibilidades para las dApps. Algunos ejemplos incluyen:

  • Juegos Blockchain (GameFi): Determinación de recompensas aleatorias, asignación de atributos a personajes/ítems, resultados de batallas, generación de mazmorras.
  • NFTs Generativos: Creación de rasgos únicos o rarezas para NFTs en el momento de la acuñación.
  • Sorteos y Loterías: Selección de ganadores de forma justa y transparente.
  • Asignación de Recursos: Distribución equitativa de recursos escasos o roles en DAOs.
  • Muestreo Estadístico: Para protocolos que necesitan seleccionar un subconjunto aleatorio de usuarios o datos.
Juegos: Recompensas aleatorias, atributos de objetos.
NFTs: Rasgos únicos y rareza.
Sorteos: Selección de ganadores justa.
DAOs: Asignación equitativa de roles/recursos.

⚠️ Mejores Prácticas y Seguridad

  • Gas fulfillRandomWords: Diseña tu función fulfillRandomWords para ser lo más eficiente posible en gas. Evita operaciones complejas o que consuman mucho gas dentro de ella.
  • Manejo de Reentradas: Aunque VRF no es directamente susceptible a reentradas en el sentido clásico, asegúrate de que cualquier lógica que se active después de recibir la aleatoriedad esté protegida contra reentradas si interactúa con otros contratos.
  • Verificación de requestId: Siempre verifica que el requestId en fulfillRandomWords coincide con el requestId de tu solicitud original. Esto asegura que estás procesando la respuesta correcta.
  • Control de Acceso: Restringe quién puede llamar a requestRandomWords en tu contrato si no quieres que sea público.
  • Financiación de Suscripción: Monitorea el saldo de tu suscripción LINK. Si se agota, las solicitudes de VRF fallarán.
  • Manejo de Errores: Aunque VRF es robusto, considera qué sucede si una solicitud falla (por ejemplo, por falta de LINK o callbackGasLimit insuficiente). Podrías querer un mecanismo de fallback o reintento.

Conclusión ✨

La integración de Chainlink VRF en tus contratos inteligentes es un paso crucial para construir aplicaciones descentralizadas justas, transparentes e impredecibles. Al aprovechar la aleatoriedad verificable, puedes desbloquear nuevos casos de uso y mejorar la seguridad y la confianza en tus dApps. Espero que este tutorial te haya proporcionado una base sólida para empezar a usar Chainlink VRF en tus propios proyectos Solidity.

¿Qué pasa si mi solicitud VRF falla?Si tu solicitud VRF falla (por ejemplo, por falta de LINK en tu suscripción o por un `callbackGasLimit` insuficiente), la transacción de `requestRandomWords` puede revertir directamente, o la transacción de cumplimiento del oráculo puede fallar. En el segundo caso, se te cobrará por la solicitud, pero tu contrato no recibirá el número aleatorio. Es importante monitorear tus transacciones y los eventos para detectar estos fallos.
¿Puedo solicitar múltiples números aleatorios a la vez?Sí, puedes especificar `numWords` en el constructor o en la llamada a `requestRandomWords` para solicitar múltiples números aleatorios en una sola solicitud. Estos se te entregarán como un array en la función `fulfillRandomWords`.
¿Hay alternativas a Chainlink VRF?Existen otras soluciones de aleatoriedad en el espacio blockchain, como drand o soluciones basadas en *commit-reveal*. Sin embargo, Chainlink VRF es una de las opciones más utilizadas y seguras debido a su enfoque en la verificabilidad criptográfica on-chain y su amplia adopción.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!