概述
在之前的文章《Chainlink生成随机数的方法一》中我们介绍了在Chainlink中产生随机数的一般方法。本文我们介绍Chainlink生成随机数的另一种方法。读者可能会有疑惑——简单的产生一个随机数为什么还有不同的方法?这里我先给出结论——总体来讲两种方法底层逻辑相同,区别仅在于使用场景。比如:在方法一中我们需要为自己写的智能合约中预先转入Link,通过转移Link来触发随机数的请求。方法二(即我们本文将要介绍)中,其将Link统一管理,然后授权智能合约来转移Link。这样可以集中管理应用而不用为每个应用单独设置管理地址及账户。
框架
框架图
组件
相比于方法一,此处少了Wrapper组件,这是因为Link的转移发生在向智能合约返回结果时。
VRF Coordinator(链上)
Coodinator主要做三件事:1. 接收到智能合约的请求后发射日志事件; 2. 收到Service的响应数据后计算费用并扣除Link;3. 验证数据,并将验证通过的数据回调给调用方。
VRF Service(链下)
Service订阅Coordinator的日志事件,并从日志中解析出请求参数,然后向外部发送请求并收集数据,收集到数据后通过RPC方式将数据发送给Coordinator。
流程
与预言机交互时需要遵守一定的规范,一般我们通过引入、继承预言机的相关接口来实现,比如在下面代码示例中我们引入了VRFCoordinatorV2Interface接口,通过调用该接口中的方法来发起请求。另外智能合约需要实现“fullfill”方法来接收数据,我们通过继承VRFConsumerBaseV2抽象合约,并实现其fulfillRandomWords方法。
- 先在Chainlink创建一个subscription,并向subscription中转入一定数量的Link;
- 创建一个发起随机数请求的合约,(在下面示例代码中我们调用VRFCoordinatorV2Interface接口的requestRandomWords方法来发起调用),并将合约添加到subscription;
- 调用合约方法发起随机数请求到VRF Coordinator;
- VRF Coordinator收到请求后发射日志事件;
- VRF Service收到VRF Coordinator发射的日志事件后,解析日志数据并生成响应数据;
- VRF Service以RPC调用的形式向以太坊发起交易,用于调用VRF Coordinator的方法,并将响应数据以参数的形式传递到VRF Coordinator;
- VRF Coordinator计算GAS费用将将其转换为Link予以扣除;
- VRF Coordinator调用智能合约的fulfillRandomWords方法向其返回数据;
代码示例
// SPDX-License-Identifier: MIT
// An example of a consumer contract that relies on a subscription for funding.
pragma solidity ^0.8.19;
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.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/
*/
contract VRFv2Consumer is VRFConsumerBaseV2, ConfirmedOwner {
event RequestSent(uint256 requestId, uint32 numWords);
event RequestFulfilled(uint256 requestId, uint256[] randomWords);
struct RequestStatus {
bool fulfilled; // whether the request has been successfully fulfilled
bool exists; // whether a requestId exists
uint256[] randomWords;
}
mapping(uint256 => RequestStatus)
public s_requests; /* requestId --> requestStatus */
VRFCoordinatorV2Interface COORDINATOR;
uint64 s_subscriptionId;
// past requests Id.
uint256[] public requestIds;
uint256 public lastRequestId;
// The gas lane to use, which specifies the maximum gas price to bump to.
// For a list of available gas lanes on each network,
// see https://docs.chain.link/docs/vrf/v2/subscription/supported-networks/#configurations
bytes32 keyHash =
0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;
// Depends on the number of requested values that you want sent to the
// fulfillRandomWords() function. Storing each word costs about 20,000 gas,
// so 100,000 is a safe default for this example contract. 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 callbackGasLimit = 100000;
// The default is 3, but you can set this higher.
uint16 requestConfirmations = 3;
// For this example, retrieve 2 random values in one request.
// Cannot exceed VRFCoordinatorV2.MAX_NUM_WORDS.
uint32 numWords = 2;
/**
* COORDINATOR FOR SEPOLIA: 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
*/
address coordinator;
constructor(
uint64 _subscriptionId,
address _coordinator
)
VRFConsumerBaseV2(_coordinator)
ConfirmedOwner(msg.sender)
{
COORDINATOR = VRFCoordinatorV2Interface(_coordinator);
s_subscriptionId = _subscriptionId;
}
// Assumes the subscription is funded sufficiently.
function requestRandomWords()
external
onlyOwner
returns (uint256 requestId)
{
// Will revert if subscription is not set and funded.
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;
}
function fulfillRandomWords(
uint256 _requestId,
uint256[] memory _randomWords
) internal override {
require(s_requests[_requestId].exists, "request not found");
s_requests[_requestId].fulfilled = true;
s_requests[_requestId].randomWords = _randomWords;
emit RequestFulfilled(_requestId, _randomWords);
}
function getRequestStatus(
uint256 _requestId
) external view returns (bool fulfilled, uint256[] memory randomWords) {
require(s_requests[_requestId].exists, "request not found");
RequestStatus memory request = s_requests[_requestId];
return (request.fulfilled, request.randomWords);
}
}
部署
部署代码时需要提供subscriptionId(每个注册完成的subscription都会生成唯一ID),Coordinator地址,不同网络该地址不同,Chainlink支持的网络看这里。比如:我使用的是Sepolia网络,其对应的Coordinator地址是0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
智能合约部署完成后,将其添加到自己注册的subscription中(进入自己subscription详情页,点击“Add consumer”后填入智能合约的地址)。
一切就绪后,就可以执行合约的requestRandomWords方法来发起请求,稍等片刻后就可以看到结果。