# 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请求流程

  1. Dapp合约调用VRFV2WrappercalculateRequestPrice函数来估算完成随机数生成所需的总交易成本

  2. Dapp合约调用WinkMidtransferAndCall函数,以支付Wrapper所计算的请求价格。该方法发送Wink代币,并执行VRFV2WrapperonTokenTransfer逻辑。

  3. VRFV2WrapperonTokenTransfer逻辑触发VRFCoordinatorV2requestRandomWords函数以请求随机数。

  4. VRFCoordinatorV2合约emitRandomWordsRequested事件。

  5. VRF节点捕捉该事件,并等待指定数量的区块确认,然后把随机值和证明通过函数fulfillRandomWords返回VRFCoordinatorV2合约。

  6. VRFCoordinatorV2在链上验证证明,然后回调VRFV2WrapperfulfillRandomWords函数。

  7. 最后,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