随机数在软件设计中有很广泛的应用场景,尤其是在游戏中、菠菜、彩票等业务。但在区块链产生一个真正随机的数字确不容易,主要原因是区块链的共识机制需要所有节点达成一致,这使得智能合约中不可能存在真正随机的数字,否则各节点无法达成一致。本文主要介绍预言机生成随机数的原理,以及对比Chainlink生成随机数的两种方法。
众所周知 智能合约的自动执行、智能合约外部数据的获取都需要预言机的支持。预言机一般分为链上和链下两部分,其中链上部分相当于一个桥接器。它接收智能合约的请求,同时将请求发送到预言机的链下部分,链下部分根据请求收集数据相应数据,收集到数据后通过方法调用的形式将数据响应给预言机的链上部分,链上部分再将数据反馈到智能合约。
在Chainlink中智能合约向预言机发送请求是通过转移Link来触发的,Link是一个 ERC677 token,它继承了ERC20 token标准的功能,与ERC20 token不同的是,转移Link时可以携带额外的数据,Link的接收方可以定制逻辑,每当收到Link则解析数据并自动触发相关逻辑。下面通过获取随机数的例子来说明具体工作流程。
注:从上图我们可以看到,我们首先需要向智能合约中转入Link,以备触发预言机的逻辑。
Wrapper定义接收Link的接口,并实现相关逻辑——将智能合约的请求转发给Coordinator
Coodinator主要做两件事:1. 接收到Wrapper的请求后发射日志事件; 2. 收到Service的响应数据后验证,再将验证通过的数据回调给Wrapper。
Service订阅Coordinator的日志事件,并从日志中解析出请求参数,然后向外部发送请求并收集数据,收集到数据后通过RPC方式将数据发送给Coordinator。
与预言机交互时需要遵守一定的规范,一般我们通过继承预言机的接口来实现,比如“生成随机数”时我们需要继承VRFV2WrapperConsumerBase,该抽象合约中封装了相关方法。智能合约需要实现该抽象合约的fullfill方法来接收数据。以下是获取数据的流程:
// SPDX-License-Identifier: MIT
// An example of a consumer contract that directly pays for each request.
pragma solidity ^0.8.19;
import "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import "@chainlink/contracts/src/v0.8/vrf/VRFV2WrapperConsumerBase.sol";
contract VRFv2DirectFundingConsumer is
VRFV2WrapperConsumerBase,
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 */
uint256[] public requestIds;
uint256 public lastRequestId;
uint32 callbackGasLimit = 100000;
uint16 requestConfirmations = 3;
uint32 numWords = 2;
address linkAddress;
constructor(address _linkAddress, address _wrapperAddress)
ConfirmedOwner(msg.sender)
VRFV2WrapperConsumerBase(_linkAddress, _wrapperAddress)
{
linkAddress = _linkAddress;
}
function requestRandomWords()
external
onlyOwner
returns (uint256 requestId)
{
requestId = requestRandomness(
callbackGasLimit,
requestConfirmations,
numWords
);
s_requests[requestId] = RequestStatus({
paid: VRF_V2_WRAPPER.calculateRequestPrice(callbackGasLimit),
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"
);
}
}
部署合约时需要指定Link和Wrapper的地址。Sepolia网络为例,Link地址为:0x779877A7B0D9E8603169DdbD7836e478b4624789,Wrapper地址为:0xab18414CD93297B0d12ac29E63Ca20f515b3DB46。Chainlink支持的网络从这里可以看到。