Solidity语言 - 如何在智能合约中使用Chainlink VRF - Quicknode

  • QuickNode
  • 发布于 2024-05-30 10:49
  • 阅读 22

本文详细介绍了如何在智能合约中使用Chainlink VRF生成随机数。文章涵盖了Chainlink VRF的原理、请求随机数的方法,并通过构建一个自定义的抽奖智能合约进行了系统的示范。还提供了关于测试、部署合约和创建Chainlink VRF订阅的步骤,有助于开发者理解如何在其项目中实现该功能。

重要通知

本指南包含对 Goerli 测试网的引用,而该网络已不再积极维护。尽管与该链相关的特定步骤可能不适用,但总体过程可能对其他链仍然有效。我们建议你探索当前可用的替代方案。如果你想查看本指南的更新版本,请 告知我们!

概述

在区块链上生成随机性可能比较困难,因为智能合约是确定性的,并且可能被预测。然而, Chainlink 解决了这个问题。本文将教你更多关于 Chainlink VRF 的知识以及它是如何工作的。此外,你还将了解两种不同的 Chainlink VRF 方法——直接资金方法和订阅方法。稍后在文章中,你将使用 Remix.IDE 部署一个自定义彩票智能合约,并使用 Chainlink VRF 生成一个随机数。

你需要准备的内容

你将要做的事情

  • 了解 Chainlink VRF 和不同的请求方法
  • 创建并部署一个使用 Chainlink VRF 的自定义彩票智能合约
  • 在 Goerli 测试网上使用 Remix.IDE 测试彩票智能合约

什么是 Chainlink VRF?

Chainlink VRF 是一个公平且可验证的随机数生成器 (RNG),允许智能合约在不危及安全的情况下访问随机值。对于每个请求,Chainlink VRF 将返回一个或多个随机值以及链上的加密证明。

你可以使用 Chainlink VRF 为你的智能合约构建可证明的公平和可验证的随机数。潜在的用例包括:

  • 区块链游戏/元宇宙

- PVP 匹配:创建可证明公平的匹配系统,让用户知道不会发生操控。

- 不可预测的游戏:为游戏添加可证明的公平随机性(例如,游戏中的随机奖品)

  • NFT

- 随机选出社区或在线活动的获胜者

  • DeFi

- 在投入的池中选出获胜者

  • 消费者广告与奖励

- 随机选出在线活动的获胜者

- 随机选出忠诚度会员俱乐部的获胜者

  • 公平概率(例如,在线博彩)

- 在在线博彩游戏、战利品盒等中随机化概率。

  • 安全性

- 在身份验证流程中添加无法预测的随机性

这些只是一些例子。利用你的创造力和经验来尝试新的想法吧!

Chainlink VRF 如何工作?

正如我们所讨论的,Chainlink VRF 帮助你获取和返回来自智能合约的随机数据。但是,在你可以使用它之前,你需要使用 LINK 代币为你的账户提供资金,LINK 代币是 Chainlink 的原生和实用代币。如果你想在主网部署合约,你需要从交易所获取 LINK 代币。

然而,为了开发目的,你可以在 Goerli 或 Mumbai 测试网络上使用测试网 LINK 代币(你可以在这个 饮水机 上获得一些)。下一步将是导入 Chainlink VRF 实现,然后在你的合约中相应使用它们。

一旦 Chainlink VRF 被纳入你的智能合约,每当请求随机性时,Chainlink 将在后台执行以下操作:

一个 Chainlink 节点将使用当前区块中的一些数据来编程随机性。

生成随机性后,它将向 VRF 合约发送加密证明。这在随机性公开显示在区块链上之前必须获得批准。

所有这些链上和链下的通信都在相对较短的时间范围内发生。

请求随机性的两种方法

Chainlink VRF 提供两种请求随机性的方法:

  • 订阅模型

要使用此方法,你必须创建一个订阅账户,并使用 LINK 代币为该账户提供资金。用户可以在此后连接多个合约,而无需分别为每个合约提供资金。

该方法允许你通过一个订阅为多个消费合约提供资金。当消费合约请求随机值时,请求将被满足,交易费用将被计算。然后余额将相应扣除。

  • 直接资金

在此方法中,一旦用户请求随机值,消费合约将使用 LINK 代币支付。因此,你必须直接为你的消费合约提供资金,并确保有足够的 LINK 代币用于随机请求的支付。此外,你还可以通过在智能合约中添加额外逻辑,将 LINK 成本传递给用户。

如何在订阅模型与直接资金模型之间选择合适的方法

每种模型都有其优点,没有一种比另一种更好。这完全取决于你合约和 DApp 的具体用例。关于这一点,你可以考虑以下事实,以做出更好的选择:

  • 如果你的 DApp 需要定期随机请求,订阅模型会更好且更具成本效益。另一方面,处理一次性请求时,直接资助的方法是更好的选择。
  • 如果你有多个 VRF 消费合约,订阅模型更好。
  • 如果你关注优化请求的 Gas 价格,订阅方法更可取。
  • 不像直接资金方法,订阅可以在单个请求中返回多个随机词。

使用 Chainlink VRF 创建自定义彩票合约

到目前为止,我们已经建立并维护了对 Chainlink VRF 工作原理的足够概念理解。因此,我们将继续构建一个彩票合约,尝试从开发视角探讨我们讨论的所有内容。

在 Remix.IDE 中打开一个新的空白工作区,然后创建一个名为 game.sol 的 Solidity 文件。

步骤 1:导入依赖项

由于我们将与 Chainlink 接口进行交互,因此我们必须在合约中导入并继承三个依赖项:VRFCoordinatorV2InterfaceVRFConsumerBaseV2ConfirmedOwner 合约。

VRFCoordinatorV2Interface 是一个可以请求随机性并管理订阅的功能依赖项。VRFConsumerBaseV2 是与消费者合约进行有效通信的工具。然后 ConfirmedOwner 依赖于安全性,它印证了所有者并设置远离操控者。

打开 game.sol 文件并将以下代码添加到顶部:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

import "https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/ConfirmedOwner.sol";

步骤 2:设定状态变量

接下来,我们将创建一个名为 Game 的合约,并声明智能合约逻辑(即事件、结构体、映射和状态变量)。在 game.sol 中的导入语句下添加以下代码:

contract Game is VRFConsumerBaseV2, ConfirmedOwner {
    event RequestSent(uint256 requestId, uint32 numWords);
    event RequestFulfilled(uint256 requestId, uint256[] randomWords);

    struct RequestStatus {
        bool fulfilled; // 请求是否已成功完成
        bool exists; // requestId 是否存在
        uint256[] randomWords;
    }
    mapping(uint256 => RequestStatus)
        public s_requests; /* requestId --> requestStatus */
    VRFCoordinatorV2Interface COORDINATOR;

    // 你的订阅 ID。
    uint64 s_subscriptionId;

    // 过去的请求 ID。
    uint256[] public requestIds;
    uint256 public lastRequestId;

    bytes32 immutable keyHash;
    address public immutable linkToken;

    uint32 callbackGasLimit = 150000;

    uint16 requestConfirmations = 3;
    uint32 numWords = 1;
    uint public randomWordsNum;

前两个初始事件(即 RequestSent 和 RequestFulfilled)在用户请求随机值时记录;一个记录发送的值,另一个记录满足的值。

然后有一个结构体(即 RequestStatus),用于记录关于请求的状态的数据,无论它是否已被满足或根本存在。创建这个结构体后,我们需要跟踪它,因此需要映射。

随后,我们将 keyHashlinkToken 引入合约。其他部分设置 gaslimit、我们期望的随机词数量和请求确认。

此外,我们还需要为我们的彩票合约创建状态变量。

在你之前粘贴的代码下添加以下代码到 game.sol

address[] public players;
uint maxPlayers;

bool public gameStarted;
uint public entryfee;
uint public gameId;

address public recentWinner;

event GameStarted(uint gameId, uint maxPlayers, uint entryfee);
event PlayerJoined(uint gameId, address player);
event GameEnded(uint gameId, address winner);

首先,我们创建了一个 players 数组并将其可见性设置为 public。然后,我们创建了一个无符号整数以表示最大玩家数 maxPlayers。彩票必须在每个人可以参与之前启动,因此我们创建了一个布尔数据类型 gameStarted,以跟踪游戏状态。

每个参与者将支付一个 entryfee,每个游戏将拥有一个唯一的 gameId。然后我们创建了一个地址数据类型,用于存储 recentWinner

这些事件跟踪游戏何时开始、结束,以及每当有玩家加入时。你可以使用 eth_getLogs 方法查找这些事件的日志。

步骤 3:在构造函数中初始化

我们最初导入了一些依赖项,这将在构造函数中初始化。首先,我们设置一些构造函数参数(例如,subscriptionId、_\linkToken),这些参数指的是我们的合约用于资金支持的订阅 ID 和我们部署网络的 LINK 代币地址。

我们将 VRFConsumerBaseV2 初始化并调用 ConfirmedOwner 函数以设置 msg.sender 为所有者。设置好我们的 COORDINATOR 变量与 VRFCoordinatorV2Interface 后,设置 s_subscriptionId。请注意,要注意你正在部署的网络并根据正确的值获取相应的内容。最后,我们设置 keyHash、你愿意为请求支付的最大 Gas 价格(以 Wei 为单位)、linkToken 地址,以及我们的 gameStarted 变量设置为 false。

在本教程中,我们将使用 Goerli 测试网。更重要的是,我们将游戏的开始状态设置为 false,直到所有者启动它。

在之前你粘贴的代码下添加以下代码到 game.sol

constructor(
    uint64 subscriptionId,
    address _linkToken
)
    VRFConsumerBaseV2(0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D)
    ConfirmedOwner(msg.sender)
{
    COORDINATOR = VRFCoordinatorV2Interface(
        0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D
    );
    s_subscriptionId = subscriptionId;

    keyHash = 0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15; // 我们已经设置过这个
    linkToken = _linkToken;

    gameStarted = false;
}

步骤 4:startGame 函数

在你之前粘贴的代码下添加以下代码到 game.sol

function startGame(uint _maxPlayers, uint _entryfee) public {
    require(!gameStarted, "游戏已经开始了");

    players = new address[](0);

    maxPlayers = _maxPlayers;

    gameStarted = true;

    entryfee = _entryfee; // 以 wei 为单位的入场费(18 个零)
    gameId += 1;

    emit GameStarted(gameId, maxPlayers, entryfee);
}

我们以上创建了一个 startGame 函数并将其设为 public。然后,我们重置 players 数组和最大玩家数。

当游戏开始时,我们将 gameID 增加 1,并将 gameStarted 更新为 true。

步骤 5:joinGame 函数

在你之前粘贴的代码下添加以下代码到 game.sol

function joinGame() public payable {
    require(gameStarted, "游戏尚未启动");
    require(players.length < maxPlayers, "游戏已经满员!");

    require(
        msg.value == entryfee,
        "金额应等于入场费"
    );
    players.push(msg.sender);

    emit PlayerJoined(gameId, msg.sender);

    if (players.length == maxPlayers) {
        getRandomWinner();
    }
}

在上述的 joinGame 函数中,我们必须设置三个检查条件。大致参与者不能加入:

  • 游戏尚未开始
  • 玩家人数已满
  • 参与者未支付入场费

一旦玩家通过验证,参与者将被添加到玩家数组中。我们然后检查玩家数是否小于设置的 maxPlayers。一旦最后一名玩家加入游戏 (players.length == maxPlayers)getRandomWinner 函数将被调用以选出随机获胜者。

步骤 6:pickWinner 函数

在你之前粘贴的代码下添加以下代码到 game.sol

function getRandomWinner() internal returns (address) {
    uint256 requestId = requestRandomWords();

    uint256 winnerIndex = randomWordsNum % players.length;

    recentWinner = players[winnerIndex];

    (bool success, ) = recentWinner.call{value: address(this).balance}("");
    require(success, "无法发送以太");
    gameStarted = false;

    emit GameEnded(gameId, recentWinner);
    return recentWinner;
}

getRandomWinner 函数中,我们将 requestId 指向 requestRandomWords。然后我们使用玩家数组和 randomWordsNum 进行取模运算以生成随机性。

其成功后,我们调用转账给 recentWinner 并返回其地址以供公开可见。

步骤 7:设置获取随机性的逻辑

你必须实现这三个主要的 Chainlink 函数以生成随机数:requestRandomWords 函数、fulfillRandomWords 函数和 getRequestStatus 函数。让我们花些时间了解这些函数如何运作以生成随机性。

当我们调用 requestRandomWords 函数时,Chainlink oracle 会发起一个请求以生成随机数。

该请求包含一个 requestId 变量,将数据传递给 VRF 协调器,例如我们的订阅 id、请求确认限额、回调 Gas 限额以及我们想要的随机数数量。完成后,oracle 将返回一个 ID(例如,requestId)。合约还将在一个映射(例如,s_requests)中存储返回值。

那么返回的 requestId 有什么用途?它在回调函数 fulfillRandomWords 中使用,其中它接受一个 requestId(在请求随机词功能调用完成时)和一个随机词数组(即数字)。请注意,在此回调函数中,你可以实施逻辑以修改返回的随机数或更新状态变量等。

function requestRandomWords() public onlyOwner returns (uint256 requestId) {
    requestId = COORDINATOR.requestRandomWords(
        keyHash,
        s_subscriptionId,
        requestConfirmations,
        callbackGasLimit,
        numWords
    );
    s_requests[requestId] = RequestStatus({
        randomWords: new uint256[](0),
        exists: true,
        fulfilled: false
    });
    requestIds.push(requestId);
    lastRequestId = requestId;
    emit RequestSent(requestId, numWords);
    return requestId; // requestID 是 uint。
}

function fulfillRandomWords(
    uint256 _requestId,
    uint256[] memory _randomWords
) internal override {
    require(s_requests[_requestId].exists, "请求未找到");
    s_requests[_requestId].fulfilled = true;
    s_requests[_requestId].randomWords = _randomWords;
    randomWordsNum = _randomWords[0]; // 将数组索引设置为变量,更方便处理
    emit RequestFulfilled(_requestId, _randomWords);
}

// 检查随机数请求状态的函数
function getRequestStatus(
    uint256 _requestId
) external view returns (bool fulfilled, uint256[] memory randomWords) {
    require(s_requests[_requestId].exists, "请求未找到");
    RequestStatus memory request = s_requests[_requestId];
    return (request.fulfilled, request.randomWords);
}

请注意,你可以通过更新 requestRandomWords 函数中的 numWords 变量从一个 VRF 请求中一次请求多个随机值。此外,根据当前的测试网条件,可能需要几分钟的时间获取请求的随机值回到你的合约。请访问 vrf.chain.link 查看你的订阅的未处理请求列表。

最后,game.sol 中的完整代码应如下所示:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

import "https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/ConfirmedOwner.sol";

contract Game is VRFConsumerBaseV2, ConfirmedOwner {
    event RequestSent(uint256 requestId, uint32 numWords);
    event RequestFulfilled(uint256 requestId, uint256[] randomWords);

    struct RequestStatus {
        bool fulfilled; // 请求是否已成功完成
        bool exists; // requestId 是否存在
        uint256[] randomWords;
    }
    mapping(uint256 => RequestStatus)
        public s_requests; /* requestId --> requestStatus */
    VRFCoordinatorV2Interface COORDINATOR;

    // 你的订阅 ID。
    uint64 s_subscriptionId;

    // 过去的请求 ID。
    uint256[] public requestIds;
    uint256 public lastRequestId;

    bytes32 immutable keyHash;
    address public immutable linkToken;

    uint32 callbackGasLimit = 150000;

    uint16 requestConfirmations = 3;
    uint32 numWords = 1;
    uint public randomWordsNum;

    address[] public players;
    uint maxPlayers;

    bool public gameStarted;
    uint public entryfee;
    uint public gameId;

    address public recentWinner;

    event GameStarted(uint gameId, uint maxPlayers, uint entryfee);
    event PlayerJoined(uint gameId, address player);
    event GameEnded(uint gameId, address winner);

    constructor(
        uint64 subscriptionId,
        address _linkToken
    )
        VRFConsumerBaseV2(0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D)
        ConfirmedOwner(msg.sender)
    {
        COORDINATOR = VRFCoordinatorV2Interface(
            0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D
        );
        s_subscriptionId = subscriptionId;

        keyHash = 0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15; // 我们已经设置过这个
        linkToken = _linkToken;

        gameStarted = false;
    }

    receive() external payable {}

    function startGame(uint _maxPlayers, uint _entryfee) public {
        require(!gameStarted, "游戏已经开始了");

        players = new address[](0);

        maxPlayers = _maxPlayers;

        gameStarted = true;

        entryfee = _entryfee; // 以 wei 为单位的入场费(18 个零)
        gameId += 1;

        emit GameStarted(gameId, maxPlayers, entryfee);
    }

    function joinGame() public payable {
        require(gameStarted, "游戏尚未启动");
        require(players.length < maxPlayers, "游戏已经满员!");

        require(
            msg.value == entryfee,
            "金额应等于入场费"
        );
        players.push(msg.sender);

        emit PlayerJoined(gameId, msg.sender);

        if (players.length == maxPlayers) {
            getRandomWinner();
        }
    }

    function getRandomWinner() internal returns (address) {
        uint256 requestId = requestRandomWords();

        uint256 winnerIndex = randomWordsNum % players.length;

        recentWinner = players[winnerIndex];

        (bool success, ) = recentWinner.call{value: address(this).balance}("");
        require(success, "无法发送以太");
        gameStarted = false;

        emit GameEnded(gameId, recentWinner);
        return recentWinner;
    }

    function requestRandomWords() public onlyOwner returns (uint256 requestId) {
        requestId = COORDINATOR.requestRandomWords(
            keyHash,
            s_subscriptionId,
            requestConfirmations,
            callbackGasLimit,
            numWords
        );
        s_requests[requestId] = RequestStatus({
            randomWords: new uint256[](0),
            exists: true,
            fulfilled: false
        });
        requestIds.push(requestId);
        lastRequestId = requestId;
        emit RequestSent(requestId, numWords);
        return requestId; // requestID 是 uint。
    }

    function fulfillRandomWords(
        uint256 _requestId,
        uint256[] memory _randomWords
    ) internal override {
        require(s_requests[_requestId].exists, "请求未找到");
        s_requests[_requestId].fulfilled = true;
        s_requests[_requestId].randomWords = _randomWords;
        randomWordsNum = _randomWords[0]; // 将数组索引设置为变量,更方便处理
        emit RequestFulfilled(_requestId, _randomWords);
    }

    // 检查随机数请求状态的函数
    function getRequestStatus(
        uint256 _requestId
    ) external view returns (bool fulfilled, uint256[] memory randomWords) {
        require(s_requests[_requestId].exists, "请求未找到");
        RequestStatus memory request = s_requests[_requestId];
        return (request.fulfilled, request.randomWords);
    }
}

设置 Chainlink VRF 订阅

现在游戏合约已经创建,我们可以进行创建 Chainlink 订阅的下一步。然而,在我们这样做之前,请确保你在 Goerli 测试网上有一些 ETH。你可以在 QuickNode 饮水机 获取一些。

然后,访问 Chainlink 订阅 页面以创建一个订阅。连接你的 Metamask 钱包,然后单击 创建订阅 按钮。最后一步是在你的 MetaMask 钱包中签署交易(确保你在 Goerli 测试网上)。

下一阶段是为你的 Chainlink 订阅提供资金。点击 添加资金 并添加 10 个 LINK 代币。完成后,你的仪表盘应如下所示:

创建参与合约

我们需要编写并部署一个合约,作为彩票参与者。该合约的主要逻辑是存入资金并调用 joinGame 函数。

创建一个名为 GameParticipant.sol 的文件,然后将以下代码添加到文件中:

pragma solidity 0.8.0;

contract joinGameContract {
    constructor() payable {}

    receive() external payable {}

    function joinGame(address _addr) public payable {
        (bool success, ) = _addr.call{value: address(this).balance}(
            abi.encodeWithSignature("joinGame()")
        );

        require(success, "交易失败");
    }
}

该合约调用 joinGame 函数,进行转账调用,并包含一个 receive 回退函数以接收以太。

测试彩票合约

此时,我们将开始通过部署彩票合约(例如,game.sol)来进行测试,但在此之前,我们必须先编译智能合约。在 Solidity 编译器选项卡中,选择 game.sol 文件并单击编译按钮。然后对 GameParticipant.sol 重复此操作。

请注意,如果启用了自动编译选项,你无需再次编译合约。

然后,一旦你的合约已经编译并准备部署,导航到 Remix.IDE 的 DEPLOY & RUN TRANSACTIONS 选项,并在环境下拉菜单中选择 Injected Provider 选项。然后,在合约下拉菜单中选择 Game 合约(例如,game.sol)。

你必须将你在 Chainlink 订阅仪表盘上找到的订阅 ID 输入构造函数参数中,还要输入 LINK 代币地址:0x326C977E6efc84E512bB9C30f76E30c160eD06FB

部署成功后,你必须将合约地址添加为消费者,并在你的 MetaMask 钱包中签署交易。

下一步是与 startGame 函数进行交互。将玩家数量设置为 2,将入场费设置为 1 wei。你需要在你的 MetaMask 钱包中签署此交易。

接下来,我们需要部署 joinGameContract 合约(即 GameParticipant.sol)。在 Remix.IDE 中选择该合约,然后转到 DEPLOY & RUN TRANSACTIONS 选项卡以部署合约。

在你签署交易并部署合约后,在 Remix.IDE 中的值字段中输入 1 wei,然后调用 joinGame 函数。你需要将 game.sol 合约的地址作为参数传递,并在 MetaMask 中签署该交易。

一旦 GameParticipant 合约加入彩票,返回 game.sol 合约并调用 joinGame 函数,将 1 wei 传入你的交易中。

一旦最大玩家数已加入,合约将向 Chainlink VRF 发送请求以检索随机数并返回获胜者的地址。要查看获胜者,请单击 recentWinner 变量以读取其状态。

结论

恭喜你!你刚刚学习了 Chainlink VRF 以及如何从智能合约请求随机数。Chainlink 代表了区块链技术的重大进步,因为它为开发人员提供了可证明的公平随机数生成器。

Chainlink VRF 的用例不仅限于在本文章中我们所做的彩票随机获胜者;你可以利用它在你未来可能构建的任何合约中生成随机性。

我们希望看到你创造的内容!请在 DiscordTwitter 与我们分享你的想法。如果你对本指南有任何反馈或问题,我们希望听到你的声音!

  • 原文链接: quicknode.com/guides/eth...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。