本文详细介绍了如何在智能合约中使用Chainlink VRF生成随机数。文章涵盖了Chainlink VRF的原理、请求随机数的方法,并通过构建一个自定义的抽奖智能合约进行了系统的示范。还提供了关于测试、部署合约和创建Chainlink VRF订阅的步骤,有助于开发者理解如何在其项目中实现该功能。
重要通知
本指南包含对 Goerli 测试网的引用,而该网络已不再积极维护。尽管与该链相关的特定步骤可能不适用,但总体过程可能对其他链仍然有效。我们建议你探索当前可用的替代方案。如果你想查看本指南的更新版本,请 告知我们!
在区块链上生成随机性可能比较困难,因为智能合约是确定性的,并且可能被预测。然而, Chainlink 解决了这个问题。本文将教你更多关于 Chainlink VRF 的知识以及它是如何工作的。此外,你还将了解两种不同的 Chainlink VRF 方法——直接资金方法和订阅方法。稍后在文章中,你将使用 Remix.IDE 部署一个自定义彩票智能合约,并使用 Chainlink VRF 生成一个随机数。
你需要准备的内容
基本理解 Solidity
MetaMask 钱包
Goerli 测试网的 ETH (你可以在 QuickNode 饮水机 获得一些)
测试网 LINK 代币
你将要做的事情
Chainlink VRF 是一个公平且可验证的随机数生成器 (RNG),允许智能合约在不危及安全的情况下访问随机值。对于每个请求,Chainlink VRF 将返回一个或多个随机值以及链上的加密证明。
你可以使用 Chainlink VRF 为你的智能合约构建可证明的公平和可验证的随机数。潜在的用例包括:
- PVP 匹配:创建可证明公平的匹配系统,让用户知道不会发生操控。
- 不可预测的游戏:为游戏添加可证明的公平随机性(例如,游戏中的随机奖品)
- 随机选出社区或在线活动的获胜者
- 在投入的池中选出获胜者
- 随机选出在线活动的获胜者
- 随机选出忠诚度会员俱乐部的获胜者
- 在在线博彩游戏、战利品盒等中随机化概率。
- 在身份验证流程中添加无法预测的随机性
这些只是一些例子。利用你的创造力和经验来尝试新的想法吧!
正如我们所讨论的,Chainlink VRF 帮助你获取和返回来自智能合约的随机数据。但是,在你可以使用它之前,你需要使用 LINK 代币为你的账户提供资金,LINK 代币是 Chainlink 的原生和实用代币。如果你想在主网部署合约,你需要从交易所获取 LINK 代币。
然而,为了开发目的,你可以在 Goerli 或 Mumbai 测试网络上使用测试网 LINK 代币(你可以在这个 饮水机 上获得一些)。下一步将是导入 Chainlink VRF 实现,然后在你的合约中相应使用它们。
一旦 Chainlink VRF 被纳入你的智能合约,每当请求随机性时,Chainlink 将在后台执行以下操作:
一个 Chainlink 节点将使用当前区块中的一些数据来编程随机性。
生成随机性后,它将向 VRF 合约发送加密证明。这在随机性公开显示在区块链上之前必须获得批准。
所有这些链上和链下的通信都在相对较短的时间范围内发生。
Chainlink VRF 提供两种请求随机性的方法:
要使用此方法,你必须创建一个订阅账户,并使用 LINK 代币为该账户提供资金。用户可以在此后连接多个合约,而无需分别为每个合约提供资金。
该方法允许你通过一个订阅为多个消费合约提供资金。当消费合约请求随机值时,请求将被满足,交易费用将被计算。然后余额将相应扣除。
在此方法中,一旦用户请求随机值,消费合约将使用 LINK 代币支付。因此,你必须直接为你的消费合约提供资金,并确保有足够的 LINK 代币用于随机请求的支付。此外,你还可以通过在智能合约中添加额外逻辑,将 LINK 成本传递给用户。
每种模型都有其优点,没有一种比另一种更好。这完全取决于你合约和 DApp 的具体用例。关于这一点,你可以考虑以下事实,以做出更好的选择:
到目前为止,我们已经建立并维护了对 Chainlink VRF 工作原理的足够概念理解。因此,我们将继续构建一个彩票合约,尝试从开发视角探讨我们讨论的所有内容。
在 Remix.IDE 中打开一个新的空白工作区,然后创建一个名为 game.sol 的 Solidity 文件。
由于我们将与 Chainlink 接口进行交互,因此我们必须在合约中导入并继承三个依赖项:VRFCoordinatorV2Interface、VRFConsumerBaseV2 和 ConfirmedOwner 合约。
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";
接下来,我们将创建一个名为 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),用于记录关于请求的状态的数据,无论它是否已被满足或根本存在。创建这个结构体后,我们需要跟踪它,因此需要映射。
随后,我们将 keyHash 和 linkToken 引入合约。其他部分设置 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 方法查找这些事件的日志。
我们最初导入了一些依赖项,这将在构造函数中初始化。首先,我们设置一些构造函数参数(例如,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;
}
在你之前粘贴的代码下添加以下代码到 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。
在你之前粘贴的代码下添加以下代码到 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 函数将被调用以选出随机获胜者。
在你之前粘贴的代码下添加以下代码到 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 并返回其地址以供公开可见。
你必须实现这三个主要的 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 订阅的下一步。然而,在我们这样做之前,请确保你在 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 的用例不仅限于在本文章中我们所做的彩票随机获胜者;你可以利用它在你未来可能构建的任何合约中生成随机性。
我们希望看到你创造的内容!请在 Discord 或 Twitter 与我们分享你的想法。如果你对本指南有任何反馈或问题,我们希望听到你的声音!
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!