参考文档
Remix: https://remix.ethereum.org/
领取 LINK 测试币 :https://faucets.chain.link/bnb-chain-testnet
VRF 订阅账号:https://vrf.chain.link/
ChainLink VRF 2.5官网:https://vrf.chain.link/bnb-chain-testnet
ChainLink VRF 合约源码:https://github.com/smartcontractkit/chainlink/tree/contracts-v1.3.0/contracts/src/v0.8/vrf/dev
目录
引言
在区块链开发中,我们常常需要安全、可信且不可预测的随机数。无论是链上游戏、NFT 空投、还是去中心化抽奖应用,“随机性” 都是公平性和用户信任的核心。而 Chainlink 的 VRF(Verifiable Random Function)2.5,正是为此设计的一种链上可验证的随机数生成服务。
本文将从原理出发,结合开发者流程,为你详细讲解 Chainlink VRF 2.5 的工作机制、使用步骤和代码实践。
一、什么是 Chainlink VRF?
Chainlink VRF 是 Chainlink 提供的 可验证随机数生成器,全称为 Verifiable Random Function。它能在链上提供:
- 不可预测性:任何人在请求发出前无法预测结果。
- 可验证性:结果附带签名,任何人都能验证是否被篡改。
- 去信任化:不依赖中心化服务器,结果来自去中心化的 Chainlink 节点。
VRF 2.5 是什么?
VRF 2.5 是 VRF v2 的升级版本:
- 优化费用结构
- 支持 订阅模式 和 direct funding(直接付费)模式
- 支持更多链(BNB Chain、Polygon、Arbitrum 等)
- 接口更友好,集成更简单
二、Chainlink VRF 的核心工作机制
Chainlink VRF 的本质是:加密生成 + 签名验证
。流程如下:
1. 用户合约发起请求
智能合约向 Chainlink 的 VRF Coordinator 发起请求(需要支付 LINK),包含:
- 请求者地址
- keyHash(节点密钥标识)
- subscriptionId(订阅账户 ID)
- gas 限制与回调参数等
2. Chainlink 节点生成随机数
Chainlink VRF 节点接收请求后,生成如下内容:
- 使用私钥签名某个随机值
- 生成一个 VRF proof(证明)和随机数结果
此过程完全由 Chainlink 节点处理,用户无法预测其结果。
3. 节点将结果返回 VRF Coordinator
Chainlink 节点将生成的随机数及其证明提交至链上的 VRF Coordinator。
4. Coordinator 验证随机数签名
VRF Coordinator 合约在链上验证随机数是否由 Chainlink 节点合法生成。
- 验证签名是否匹配 keyHash
- 校验随机数生成过程是否符合 VRF 加密标准
5. 用户合约接收并处理随机数
验证通过后,VRF Coordinator 会调用用户合约的 fulfillRandomWords() 函数,将最终的随机数数组发送回去。
你可以在此回调中执行各种逻辑,如:彩票开奖、NFT mint 随机分配、游戏中生成掉落奖励等。
三、使用 Chainlink VRF v2.5 的完整开发流程
使用 VRF v2.5 的两种方式
✅ 订阅模式(subscription):先充值 LINK 到订阅账户,每次请求消耗订阅里的余额
✅ direct funding(直接付费):每次请求时直接支付 LINK 或原生代币,无需提前充值
下面我们用两种模式都实战一遍。
(一)使用 subscription 模式
3.1 创建订阅
创建订阅账号 传送🚪
输入电子邮箱(可选)和项目名称(可选)来创建订阅。
创建一个订阅账户的第一步(点击Create Subscription) 交易请求,点击确认。
创建一个订阅账户的第二步,会收到确认交易,你也可以点开看。
3.2 申请订阅 ID
创建订阅 ID 传送🚪
点击订阅,然后输入电子邮箱(可选)和项目名称(可选)来创建订阅。
创建一个订阅账户的第一步(点击Create Subscription) 交易请求,点击确认。
创建一个订阅账户的第二步,会收到确认交易,你也可以点开看。
创建一个订阅账户的最后一步,请求签名。
3.2 查看订阅
创建好订阅之后,你可以看到在这个面板上有刚刚创建的订阅账户。
3.3 查看 Subscription lD
点进这个订阅账户,可以查看到 Subscription lD。把这个ID 复制下来,等会儿我们合约要用到。
3.4 申请 LINK 代币和订阅 ID
如果是测试网,需要申请测试 LINK 代币和订阅 ID。申请测试LINK代币点击传送🚪我这里把接收LINK测试币的钱包地址复制进去了。然后根据提示领取就可以了,成功之后,我们可以去BSC浏览器查看,账户地址,看是否有LINK测试币。
3.4.1 MetaMask里添加代币
为方便起见,我们在MetaMask里面添加代币。找到在BNB链上的 Chainlink 代币地址,复制。传送🚪
点击 MetaMask,连接的是BSC测试网络,找到添加代币。
我这里选择的是BNB Smart Chain Testnet 网络,然后代币地址复制进来,我这里已经添加了。
3.4.2 查看 LINK 代币
3.5 获取 keyHash 和 VRF Coordinator
访问 Chainlink VRF 页面,根据目标网络,我这里用的是BSC的测试网,点击传送🚪 复制这两个参数,接下来我们的合约需要用到。
3.6 合约介绍
合约简介:
这是一个掷 20 面骰子的链上小游戏合约,使用 Chainlink VRF v2.5 提供的真随机数,让用户掷出一个 1~20 的随机数,并根据点数返回一个对应的「家族」名称(比如《冰与火之歌》里的 Stark、Lannister 等)。
源码在这里,大家可以参考。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {VRFConsumerBaseV2Plus} from "@chainlink/contracts@1.4.0/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts@1.4.0/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
// 使用随机数模拟掷20面骰子的Chainlink VRF消费者合约
contract VRFD20 is VRFConsumerBaseV2Plus {
// 表示骰子正在掷的状态
uint256 private constant ROLL_IN_PROGRESS = 42;
// 你的订阅ID
uint256 public s_subscriptionId;
// BNB Chain Testnet 网络的VRF协调者地址
address public vrfCoordinator = 0xDA3b641D438362C440Ac5458c57e00a712b66700;
// 使用的gas通道,指定最大gas价格
bytes32 public s_keyHash =
0x8596b430971ac45bdf6088665b9ad8e8630c9d5049ab54b14dff711bee7c0e26;
// 回调函数的gas限制,存储每个随机数约需20000 gas
uint32 public callbackGasLimit = 50000;
// 请求确认数,默认为3
uint16 public requestConfirmations = 3;
// 请求的随机数数量,最大不超过 VRFCoordinatorV2_5.MAX_NUM_WORDS
uint32 public numWords = 1;
// 映射:请求ID到掷骰者地址
mapping(uint256 => address) private s_rollers;
// 映射:掷骰者地址到VRF结果
mapping(address => uint256) private s_results;
// 事件:骰子已掷出
event DiceRolled(uint256 indexed requestId, address indexed roller);
// 事件:骰子结果已返回
event DiceLanded(uint256 indexed requestId, uint256 indexed result);
// 构造函数,继承VRFConsumerBaseV2Plus
constructor(uint256 subscriptionId) VRFConsumerBaseV2Plus(vrfCoordinator) {
s_subscriptionId = subscriptionId;
}
// 请求随机数,模拟掷骰子
function rollDice(
address roller
) public onlyOwner returns (uint256 requestId) {
// 确保未掷过骰子
require(s_results[roller] == 0, "Already rolled");
// 请求随机数
requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: s_keyHash,
subId: s_subscriptionId,
requestConfirmations: requestConfirmations,
callbackGasLimit: callbackGasLimit,
numWords: numWords,
extraArgs: VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
)
})
);
s_rollers[requestId] = roller;
s_results[roller] = ROLL_IN_PROGRESS;
emit DiceRolled(requestId, roller);
}
// VRF协调者回调函数,返回随机数
function fulfillRandomWords(
uint256 requestId,
uint256[] calldata randomWords
) internal override {
// 计算20面骰子结果
uint256 d20Value = (randomWords[0] % 20) + 1;
s_results[s_rollers[requestId]] = d20Value;
emit DiceLanded(requestId, d20Value);
}
// 获取玩家的家族名称
function house(address player) public view returns (string memory) {
// 确保已掷骰子
require(s_results[player] != 0, "Dice not rolled");
// 确保掷骰完成
require(s_results[player] != ROLL_IN_PROGRESS, "Roll in progress");
return _getHouseName(s_results[player]);
}
// 根据ID获取家族名称
function _getHouseName(uint256 id) private pure returns (string memory) {
string[20] memory houseNames = [
"Targaryen",
"Lannister",
"Stark",
"Tyrell",
"Baratheon",
"Martell",
"Tully",
"Bolton",
"Greyjoy",
"Arryn",
"Frey",
"Mormont",
"Tarley",
"Dayne",
"Umber",
"Valeryon",
"Manderly",
"Clegane",
"Glover",
"Karstark"
];
return houseNames[id - 1];
}
}
代码说明
合约继承
:合约继承 VRFConsumerBaseV2Plus,这是 Chainlink 提供的基合约,用于处理 VRF 请求和回调
构造函数
:初始化 VRF 协调器地址和订阅 ID
请求随机数
:requestRandomNumber 函数向 VRF 协调器发送请求,指定 keyHash、订阅 ID、gas 限制等参数
接收随机数
:fulfillRandomWords 是回调函数,由 VRF 协调器调用,将生成的随机数存储在 randomWords 变量中
注意事项
网络选择:确保 VRF 协调器地址和 keyHash 与目标网络匹配
LINK 余额:订阅账户需有足够的 LINK 代币支付费用
Gas 优化:根据回调逻辑的复杂性调整 callbackGasLimit,避免请求失败
安全考虑:限制 requestRandomNumber 的调用权限,防止未经授权的请求
📦 主要变量解释
🌱 典型流程
1️⃣ owner 调用 rollDice(address roller) → 请求随机数
2️⃣ Chainlink VRF 返回随机数 → 调用 fulfillRandomWords
3️⃣ 玩家调用 house(address) → 得到自己的家族名
3.7 部署并测试
现在我们来正式开始吧。我这里是使用 Remix 来开发和部署的。打开remix,新建合约。刚刚创建了订阅账户,要修改vrfCoordinator 和 s_keyHash 这两个参数的值。
3.7.1 部署 VRFD20.sol 合约
输入 Subscription lD,然后部署和测试合约。
3.7.2 复制合约地址
部署成功之后,复制这个合约地址,我们准备给它添加进我们订阅账户里面的消费者。
3.7.3 添加消费者
点击 Add consumer ,把合约添加为消费者。
点击确认交易。
3.7.4 添加 Fund subscription
成功之后,添加 Link 代币做为费用支出,(因为每次请求随机数都要消耗 Link 代币) 点击 Fund subscription 。
点击确认交易。
3.7.5 查看消费者界面
添加 Link 代币成功之后,你会看到消费者界面。
3.8 测试合约
现在我们来掷色子,请求一下随机数:
现在验证结果,我们能看到已经获取到了家族的名称,证明随机数请求成功!
(二)使用 direct funding 模式
3.1 合约介绍
这是一个示例合约,用于演示如何在 BNB Chain 测试网上使用 Chainlink VRF v2.5 Wrapper(支持原生代币或 LINK 支付)请求链上随机数。
⚠️ 注意:合约中的很多值是硬编码的,官方也提醒不要直接用于生产环境。
源码在这里,大家可以参考。
// SPDX-License-Identifier: MIT
// An example of a consumer contract that directly pays for each request.
pragma solidity 0.8.19;
import {ConfirmedOwner} from "@chainlink/contracts@1.4.0/src/v0.8/shared/access/ConfirmedOwner.sol";
import {LinkTokenInterface} from "@chainlink/contracts@1.4.0/src/v0.8/shared/interfaces/LinkTokenInterface.sol";
import {VRFV2PlusWrapperConsumerBase} from "@chainlink/contracts@1.4.0/src/v0.8/vrf/dev/VRFV2PlusWrapperConsumerBase.sol";
import {VRFV2PlusClient} from "@chainlink/contracts@1.4.0/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
/**
* Request testnet LINK and ETH here: https://faucets.chain.link/
* Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/
*/
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
contract DirectFundingConsumer is VRFV2PlusWrapperConsumerBase, ConfirmedOwner {
event RequestSent(uint256 requestId, uint32 numWords);
event RequestFulfilled(
uint256 requestId,
uint256[] randomWords,
uint256 payment
);
struct RequestStatus {
uint256 paid; // amount paid in link
bool fulfilled; // whether the request has been successfully fulfilled
uint256[] randomWords;
}
mapping(uint256 => RequestStatus)
public s_requests; /* requestId --> requestStatus */
// past requests Id.
uint256[] public requestIds;
uint256 public lastRequestId;
// Depends on the number of requested values that you want sent to the
// fulfillRandomWords() function. Test and adjust
// this limit based on the network that you select, the size of the request,
// and the processing of the callback request in the fulfillRandomWords()
// function.
uint32 public callbackGasLimit = 100000;
// The default is 3, but you can set this higher.
uint16 public requestConfirmations = 3;
// For this example, retrieve 2 random values in one request.
// Cannot exceed VRFV2Wrapper.getConfig().maxNumWords.
uint32 public numWords = 2;
// Address LINK - hardcoded for BNB Chain Testnet
address public linkAddress = 0x84b9B910527Ad5C03A9Ca831909E21e236EA7b06;
// address WRAPPER - hardcoded for BNB Chain Testnet
address public wrapperAddress = 0x471506e6ADED0b9811D05B8cAc8Db25eE839Ac94;
constructor()
ConfirmedOwner(msg.sender)
VRFV2PlusWrapperConsumerBase(wrapperAddress)
{}
function requestRandomWords(
bool enableNativePayment
) external onlyOwner returns (uint256) {
bytes memory extraArgs = VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({nativePayment: enableNativePayment})
);
uint256 requestId;
uint256 reqPrice;
if (enableNativePayment) {
(requestId, reqPrice) = requestRandomnessPayInNative(
callbackGasLimit,
requestConfirmations,
numWords,
extraArgs
);
} else {
(requestId, reqPrice) = requestRandomness(
callbackGasLimit,
requestConfirmations,
numWords,
extraArgs
);
}
s_requests[requestId] = RequestStatus({
paid: reqPrice,
randomWords: new uint256[](0),
fulfilled: false
});
requestIds.push(requestId);
lastRequestId = requestId;
emit RequestSent(requestId, numWords);
return requestId;
}
function fulfillRandomWords(
uint256 _requestId,
uint256[] memory _randomWords
) internal override {
require(s_requests[_requestId].paid > 0, "request not found");
s_requests[_requestId].fulfilled = true;
s_requests[_requestId].randomWords = _randomWords;
emit RequestFulfilled(
_requestId,
_randomWords,
s_requests[_requestId].paid
);
}
function getRequestStatus(
uint256 _requestId
)
external
view
returns (uint256 paid, bool fulfilled, uint256[] memory randomWords)
{
require(s_requests[_requestId].paid > 0, "request not found");
RequestStatus memory request = s_requests[_requestId];
return (request.paid, request.fulfilled, request.randomWords);
}
/**
* Allow withdraw of Link tokens from the contract
*/
function withdrawLink() public onlyOwner {
LinkTokenInterface link = LinkTokenInterface(linkAddress);
require(
link.transfer(msg.sender, link.balanceOf(address(this))),
"Unable to transfer"
);
}
/// @notice withdrawNative withdraws the amount specified in amount to the owner
/// @param amount the amount to withdraw, in wei
function withdrawNative(uint256 amount) external onlyOwner {
(bool success, ) = payable(owner()).call{value: amount}("");
// solhint-disable-next-line gas-custom-errors
require(success, "withdrawNative failed");
}
event Received(address, uint256);
receive() external payable {
emit Received(msg.sender, msg.value);
}
}
代码说明
核心功能
requestRandomWords
功能
:发起一次随机数请求。
参数
:enableNativePayment 是否启用原生代币(BNB)支付。
流程
:
1、构造 extraArgs(标记支付方式),
根据支付方式调用:requestRandomness(用 LINK 支付)或者requestRandomnessPayInNative(用原生代币支付)方法。
2、记录 requestId 和费用。
3、更新 s_requests 和 requestIds。
4、发出 RequestSent 事件。
fulfillRandomWords
- VRF Wrapper 回调函数,返回随机数。
- 根据 requestId 更新状态,设置 fulfilled=true 并存储随机数。
- 发出 RequestFulfilled 事件。
getRequestStatus
- 外部可调用,查看请求状态。
- 返回 paid、fulfilled 和 randomWords。
📦 主要变量解释
3.2 获取 LINK Token 和 VRF Wrapper
访问 Chainlink VRF 页面,根据目标网络,我这里用的是BSC的测试网,点击传送🚪然后复制这两个参数,接下来我们的合约需要用到。
把合约里的参数进行修改,注意我这里用的是BSC测试网。
3.3 部署 DirectFundingConsumer.sol 合约
3.4 给合约转 LINK 测试币
输入合约地址和 LINK 币数量,然后点击继续,为合约充 LINK 代币。
发送成功,可以查看到交易记录。
3.5 测试合约
对于 enableNativePayment,如果使用 BNB 支付请求,请填写 true,如果使用测试网 LINK 支付,请填写 false。点击 transact 将随机值的请求发送到 Chainlink VRF。MetaMask 打开并要求您确认交易。交易批准后,Chainlink VRF 会处理你的请求。Chainlink VRF 执行请求,并在 fulfillRandomWords() 函数的回调中将随机值返回给你的合约。此时,新的键 requestId 将添加到映射 s_requests 中。根据当前的测试网条件,回调可能需要几分钟时间才能将请求的随机值返回给您的合约。
获取请求的请求 ID,请调用 lastRequestId()。在 oracle 将随机值返回给您的合约后,映射 s_requests 将更新。接收到的随机值存储在 s_requests[_requestId].randomWords 中。调用 getRequestStatus() 并指定 requestId 以显示随机单词。
总结
在链上开发中,安全、可信且可验证的随机数是游戏、公平抽奖、NFT 分配等核心场景不可或缺的基础。Chainlink VRF(Verifiable Random Function)2.5 正是为此而生,凭借其不可预测性、可验证性和去信任化,让链上应用真正做到公平透明。
本篇内容:
- 从原理出发,理解了 VRF 的核心机制:用户合约发起请求 → Chainlink 节点生成随机数和证明 → VRF Coordinator 验证 → 回调到合约。
- 实战演示了 VRF v2.5 的两种使用方式:
○ 订阅模式(subscription):适合频繁调用,先充值 LINK 到订阅账户,统一支付。
○ direct funding 模式:适合偶尔调用,每次直接支付 LINK 或原生代币,无需预充值。 - 通过两个合约示例:
○ VRFD20 合约:掷 20 面骰子,返回家族名。
○ DirectFundingConsumer 合约:演示如何在 BNB Chain 测试网请求随机数并管理请求状态。
通过 VRF 2.5,我们不仅能实现真正链上可验证的随机数,也能大幅提升用户对项目的信任和公平性的认可。希望本文能帮助你更好地理解和实践 Chainlink VRF,让你的区块链项目更专业、更安全 。