# WINkLink 随机数服务
# 介绍
VRF(Verifiable Random Function),即可验证的随机函数,其可生成安全可靠的随机数。 随机数由用户提供的seed、nonce(VRFCoordinator合约的私有状态)、请求所在区块hash 和 随机数生成节点的私钥共同决定,随机数节点不可作弊。且该随机数在返回给用户Dapp之前经过了验证,从而保证了该随机数的安全性。
随机数生成流程如下:
- 由用户合约在链上发出生成随机数的请求;
- 节点监听到该请求后,会在链下生成随机数和证明,然后在链上合约中响应;
- 链上合约对生成的随机数进行验证并通过后,以回调函数反馈到用户Dapp。
它可以用于任何需要可靠随机数的应用程序:
- 区块链游戏和NFTs
- 职责和资源的随机分配(例如随机分配法官审理案件)
- 为共识机制选择代表性样本
WINkLink VRF解决方案包含了以下链上和链下的组件:
- VRF Coordinator(链上合约):用于与VRF服务进行交互的合约。当发出随机数请求时,它会触发一个事件,然后通过VRF服务验证随机数以及关于其生成方式的证明。
- VRF Wrapper(链上合约):封装了VRF Coordinator,提供了接口以便用户Dapp调用。
- VRF 服务(链下节点):此链下组件通过订阅VRFCoordinator事件日志来监听随机数请求,并基于区块hash和随机数生成一个随机数,然后向VRFCoordinator发起一个交易,其中包括随机数和关于其生成方式的证明
本文介绍如何部署和使用VRF合约。
# VRF请求流程
Dapp合约调用
VRFV2Wrapper
的calculateRequestPrice
函数来估算完成随机数生成所需的总交易成本Dapp合约调用
WinkMid
的transferAndCall
函数,以支付Wrapper所计算的请求价格。该方法发送Wink代币,并执行VRFV2Wrapper
的onTokenTransfer
逻辑。VRFV2Wrapper
的onTokenTransfer
逻辑触发VRFCoordinatorV2
的requestRandomWords
函数以请求随机数。VRFCoordinatorV2
合约emitRandomWordsRequested
事件。VRF节点捕捉该事件,并等待指定数量的区块确认,然后把随机值和证明通过函数
fulfillRandomWords
返回VRFCoordinatorV2
合约。VRFCoordinatorV2
在链上验证证明,然后回调VRFV2Wrapper
的fulfillRandomWords
函数。最后,
VRFV2Wrapper
回调Dapp合约完成请求。
# 准备工作
WINkLink 的维护者需要对 TRON 有一定的了解,熟悉智能合约部署和调用流程。 建议参考 官方文档 (opens new window) 。
完成节点账号申请,建议参考节点账号准备文档 (opens new window) 。
# VRFCoordinatorV2 合约
VRFCoordinatorV2 合约是部署在 TRON 公链上的预言机合约。主要功能如下
- 接收消费者合约(Consumer Contract)的数据请求,触发 Event Log
- 数据请求发送时会附带 WIN 转账作为使用费用
- 接受 WINkLink 节点所提交的随机数和证明
- VRFCoordinator收到合约后会对随机数进行验证
- 对数据请求的 WIN 代币费用进行结算,提取收益
部署 VRFCoordinatorV2 合约时需要在构造函数提供相关参数:
constructor(
address wink,
address blockhashStore,
address winkMid
)
_blockHashStore
为BlockHashStore合约地址,_win
为WIN代币地址, _winkMid
为WinkMid合约地址。
# VRFV2Wrapper 合约
VRFV2Wrapper封装了与VRFCoordinatorV2的交互,作为dapp与VRFCoordinatorV2的中间层,供Dapp直接调用。
配置参数
keyHash
: 节点keyhash
maxNumWords
: 单次请求词数限制,当前设置为10
# 为节点账户授权
节点账户需要授权才能向 VRFCoordinatorV2 合约提交数据,否则会报错 。
需要使用 VRFCoordinatorV2 合约的 owner 执行如下合约调用,将节点账户添加到白名单:
function registerProvingKey(address oracle, uint256[2] calldata publicProvingKey) external onlyOwner
其中_oracle
为注册节点的地址,用于接收Dapp应用对其支付的WIN代币,_publicProvingKey
为注册节点用于生成随机数的公钥。
示例调用例如 registerProvingKey(TYmwSFuFuiDZCtYsRFKCNr25byeqHH7Esb,['6273228386041830135141271310112248407537170435188969735053134748570771583756',67273502359025519559461602732298865784327759914690240925031700564257821594585'])
。
# Dapp合约
当编写新的Dapp合约时,需遵循以下规则:
- a) 引入VRFV2WrapperConsumerBase.sol
// SPDX-License-Identifier: MIT
// An example of a consumer contract that directly pays for each request.
pragma solidity ^0.8.7;
import "./VRFV2WrapperConsumerBase.sol";
contract VRFv2DirectFundingConsumer is VRFV2WrapperConsumerBase{}
- b) 合约需实现vrf回调函数
fulfillRandomWords
,在这里你可以编写获取随机数结果后的业务处理逻辑.
function fulfillRandomWords(
uint256 _requestId,
uint256[] memory _randomWords
)
- c) 调用
requestRandomness
发起vrf请求。
function requestRandomWords()
external
onlyOwner
returns (uint256 requestId)
{
requestId = requestRandomness(
msg.sender,
callbackGasLimit,
requestConfirmations,
numWords
);
s_requests[requestId] = RequestStatus({
paid: VRF_V2_WRAPPER.calculateRequestPrice(callbackGasLimit, numWords),
randomWords: new uint256[](0),
fulfilled: false
});
requestIds.push(requestId);
lastRequestId = requestId;
emit RequestSent(requestId, numWords);
return requestId;
}
示例Dapp合约
部署实例合约VRFv2DirectFundingConsumer.sol。
构造函数参数:
_winkAddress
:wink代币合约地址
_winkMid
: winkMid合约地址
_wrapper
:VRFV2Wrapper合约地址
_numWords
: 单次请求随机词数量
/// SPDX-License-Identifier: MIT
// An example of a consumer contract that directly pays for each request.
pragma solidity ^0.8.7;
import "./ConfirmedOwner.sol";
import "./VRFV2WrapperConsumerBase.sol";
/**
* 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 VRFv2DirectFundingConsumer is
VRFV2WrapperConsumerBase,
ConfirmedOwner
{
address winkAddress;
event RequestSent(uint256 requestId, uint32 numWords);
event RequestFulfilled(
uint256 requestId,
uint256[] randomWords,
uint256 payment
);
struct RequestStatus {
uint256 paid; // amount paid in wink
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 callbackGasLimit = 0;
// 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 VRFV2Wrapper.getConfig().maxNumWords.
uint32 numWords;
constructor(
address _winkAddress,
address _winkMid,
address _wrapper,
uint32 _numWords
)
ConfirmedOwner(msg.sender)
VRFV2WrapperConsumerBase(_winkMid, _wrapper) {
winkAddress = _winkAddress;
numWords = _numWords;
}
function requestRandomWords()
external
onlyOwner
returns (uint256 requestId)
{
requestId = requestRandomness(
msg.sender,
callbackGasLimit,
requestConfirmations,
numWords
);
s_requests[requestId] = RequestStatus({
paid: VRF_V2_WRAPPER.calculateRequestPrice(callbackGasLimit, numWords),
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);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ConfirmedOwnerWithProposal.sol";
/**
* @title The ConfirmedOwner contract
* @notice A contract with helpers for basic contract ownership.
*/
contract ConfirmedOwner is ConfirmedOwnerWithProposal {
constructor(address newOwner) ConfirmedOwnerWithProposal(newOwner, address(0)) {}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./OwnableInterface.sol";
/**
* @title The ConfirmedOwner contract
* @notice A contract with helpers for basic contract ownership.
*/
contract ConfirmedOwnerWithProposal is OwnableInterface {
address private s_owner;
address private s_pendingOwner;
event OwnershipTransferRequested(address indexed from, address indexed to);
event OwnershipTransferred(address indexed from, address indexed to);
constructor(address newOwner, address pendingOwner) {
require(newOwner != address(0), "Cannot set owner to zero");
s_owner = newOwner;
if (pendingOwner != address(0)) {
_transferOwnership(pendingOwner);
}
}
/**
* @notice Allows an owner to begin transferring ownership to a new address,
* pending.
*/
function transferOwnership(address to) public override onlyOwner {
_transferOwnership(to);
}
/**
* @notice Allows an ownership transfer to be completed by the recipient.
*/
function acceptOwnership() external override {
require(msg.sender == s_pendingOwner, "Must be proposed owner");
address oldOwner = s_owner;
s_owner = msg.sender;
s_pendingOwner = address(0);
emit OwnershipTransferred(oldOwner, msg.sender);
}
/**
* @notice Get the current owner
*/
function owner() public view override returns (address) {
return s_owner;
}
/**
* @notice validate, transfer ownership, and emit relevant events
*/
function _transferOwnership(address to) private {
require(to != msg.sender, "Cannot transfer to self");
s_pendingOwner = to;
emit OwnershipTransferRequested(s_owner, to);
}
/**
* @notice validate access
*/
function _validateOwnership() internal view {
require(msg.sender == s_owner, "Only callable by owner");
}
/**
* @notice Reverts if called by anyone other than the contract owner.
*/
modifier onlyOwner() {
_validateOwnership();
_;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface OwnableInterface {
function owner() external returns (address);
function transferOwnership(address recipient) external;
function acceptOwnership() external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./TRC20Interface.sol";
import "./VRFV2WrapperInterface.sol";
/** *******************************************************************************
* @notice Interface for contracts using VRF randomness through the VRF V2 wrapper
* ********************************************************************************
* @dev PURPOSE
*
* @dev Create VRF V2 requests without the need for subscription management. Rather than creating
* @dev and funding a VRF V2 subscription, a user can use this wrapper to create one off requests,
* @dev paying up front rather than at fulfillment.
*
* @dev Since the price is determined using the gas price of the request transaction rather than
* @dev the fulfillment transaction, the wrapper charges an additional premium on callback gas
* @dev usage, in addition to some extra overhead costs associated with the VRFV2Wrapper contract.
* *****************************************************************************
* @dev USAGE
*
* @dev Calling contracts must inherit from VRFV2WrapperConsumerBase. The consumer must be funded
* @dev with enough WINK to make the request, otherwise requests will revert. To request randomness,
* @dev call the 'requestRandomness' function with the desired VRF parameters. This function handles
* @dev paying for the request based on the current pricing.
*
* @dev Consumers must implement the fullfillRandomWords function, which will be called during
* @dev fulfillment with the randomness result.
*/
abstract contract VRFV2WrapperConsumerBase {
WinkMid internal immutable WINK_MID;
VRFV2WrapperInterface internal immutable VRF_V2_WRAPPER;
/**
* @param _winkMid is the address of WinkMid
* @param _vrfV2Wrapper is the address of the VRFV2Wrapper contract
*/
constructor(address _winkMid, address _vrfV2Wrapper) {
WINK_MID = WinkMid(_winkMid);
VRF_V2_WRAPPER = VRFV2WrapperInterface(_vrfV2Wrapper);
}
/**
* @dev Requests randomness from the VRF V2 wrapper.
*
* @param _callbackGasLimit is the gas limit that should be used when calling the consumer's
* fulfillRandomWords function.
* @param _requestConfirmations is the number of confirmations to wait before fulfilling the
* request. A higher number of confirmations increases security by reducing the likelihood
* that a chain re-org changes a published randomness outcome.
* @param _numWords is the number of random words to request.
*
* @return requestId is the VRF V2 request ID of the newly created randomness request.
*/
function requestRandomness(
address _from,
uint32 _callbackGasLimit,
uint16 _requestConfirmations,
uint32 _numWords
) internal returns (uint256 requestId) {
WINK_MID.transferAndCall(
_from,
address(VRF_V2_WRAPPER),
VRF_V2_WRAPPER.calculateRequestPrice(_callbackGasLimit, _numWords),
abi.encode(_callbackGasLimit, _requestConfirmations, _numWords)
);
return VRF_V2_WRAPPER.lastRequestId();
}
/**
* @notice fulfillRandomWords handles the VRF V2 wrapper response. The consuming contract must
* @notice implement it.
*
* @param _requestId is the VRF V2 request ID.
* @param _randomWords is the randomness result.
*/
function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal virtual;
function rawFulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) external {
require(msg.sender == address(VRF_V2_WRAPPER), "only VRF V2 wrapper can fulfill");
fulfillRandomWords(_requestId, _randomWords);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface VRFV2WrapperInterface {
/**
* @return the request ID of the most recent VRF V2 request made by this wrapper. This should only
* be relied option within the same transaction that the request was made.
*/
function lastRequestId() external view returns (uint256);
/**
* @notice Calculates the price of a VRF request with the given callbackGasLimit at the current
* @notice block.
*
* @dev This function relies on the transaction gas price which is not automatically set during
* @dev simulation. To estimate the price at a specific gas price, use the estimatePrice function.
*
* @param _callbackGasLimit is the gas limit used to estimate the price.
*/
function calculateRequestPrice(uint32 _callbackGasLimit, uint32 _numWords) external view returns (uint64);
// /**
// * @notice Estimates the price of a VRF request with a specific gas limit and gas price.
// *
// * @dev This is a convenience function that can be called in simulation to better understand
// * @dev pricing.
// *
// * @param _callbackGasLimit is the gas limit used to estimate the price.
// * @param _requestGasPriceWei is the gas price in wei used for the estimation.
// */
// function estimateRequestPrice(uint32 _callbackGasLimit, uint256 _requestGasPriceWei) external view returns (uint256);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
abstract contract TRC20Interface {
function totalSupply() public view virtual returns (uint);
function balanceOf(address guy) public view virtual returns (uint);
function allowance(address src, address guy) public view virtual returns (uint);
function approve(address guy, uint wad) public virtual returns (bool);
function transfer(address dst, uint wad) public virtual returns (bool);
function transferFrom(address src, address dst, uint wad) public virtual returns (bool);
event Transfer(address indexed from, address indexed to, uint tokens);
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}
abstract contract WinkMid {
function setToken(address tokenAddress) public virtual;
function transferAndCall(address from, address to, uint64 tokens, bytes calldata _data) public virtual returns (bool success);
function balanceOf(address guy) public view virtual returns (uint);
function transferFrom(address src, address dst, uint wad) public virtual returns (bool);
function allowance(address src, address guy) public view virtual returns (uint);
}
# Tron Nile网VRF合约
为方便开发者, Nile 测试网已经部署了 WinkMid
合约,封装了 Nile 测试网 WIN
代币。
开发者可直接使用该合约地址,无需额外部署。 Nile 测试网同时提供了水龙头地址可以领取测试 TRX 和 WIN 代币。
Item | Value |
---|---|
WIN Token | TNDSHKGBmgRx9mDYA9CnxPx55nu672yQw2 |
WinkMid | TJpkay8rJXUWhvS2uL5AmMwFspQdHCX1rw |
BlockHashStore | TBpTbK9KQzagrN7eMKFr5QM2pgZf6FN7KA |
VRFCoordinatorV2 | TMvQdnsahiJRiJpA7YgcpLUdKFi2LswPrb |
VRFV2Wrapper | TJSP3zzmEH84y2W8hjfTgpqwhQsdWwn3N5 |
Fee | 10 WIN |
测试网水龙头: https://nileex.io/join/getJoinPage (opens new window)
# Tron主网VRF合约
Item | Value |
---|---|
WIN Token | TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7 |
WinkMid | TSG1B8DKDGY5sRFXwQ6xJofVr75DCFUA64 |
BlockHashStore | TRGmef4qUdNJ4xTEL96hToGuMTNst57aS1 |
VRFCoordinatorV2 | TD7hF84Xwf8Cu2zscmqxrgiGaEBziZhXqf |
VRFV2Wrapper | TYMSMoitSkxuKUF1oiZp2fse4MEgsM86WT |
Fee | 10 WIN |
← 术语表