Skip to content

Yotoha0303/vrf_Random

Repository files navigation

vrf_Random

很多以太坊上的应用都需要用到随机数,例如NFT随机抽取tokenId、抽盲盒、gamefi战斗中随机分胜负等等。但是由于以太坊上所有数据都是公开透明(public)且确定性(deterministic)的,它没法像其他编程语言一样给开发者提供生成随机数的方法。这一讲我们将介绍链上(哈希函数)和链下(chainlink预言机)随机数生成的两种方法,并利用它们做一款tokenId随机铸造的NFT

1)、链上随机数生成

我们可以将一些链上的全局变量作为种子,利用keccak256()哈希函数来获取伪随机数。这是因为哈希函数具有灵敏性和均一性,可以得到“看似”随机的结果。下面的getRandomOnchain()函数利用全局变量block.timestampmsg.senderblockhash(block.number-1)作为种子来获取随机数:

/** 
    * 链上伪随机数生成
    * 利用keccak256()打包一些链上的全局变量/自定义变量
    * 返回时转换成uint256类型
    */
    function getRandomOnchain() public view returns(uint256){
        // remix运行blockhash会报错
        bytes32 randomBytes = keccak256(abi.encodePacked(block.timestamp, msg.sender, blockhash(block.number-1)));
        
        return uint256(randomBytes);
    }

注意: ,这个方法并不安全:

  • 首先,block.timestampmsg.senderblockhash(block.number-1)这些变量都是公开的,使用者可以预测出用这些种子生成出的随机数,并挑出他们想要的随机数执行合约。
  • 其次,矿工可以操纵blockhashblock.timestamp,使得生成的随机数符合他的利益。

尽管如此,由于这种方法是最便捷的链上随机数生成方法,大量项目方依靠它来生成不安全的随机数,包括知名的项目meebitsloots等。当然,这些项目也无一例外的被攻击了:攻击者可以铸造任何他们想要的稀有NFT,而非随机抽取。

2)、链下随机数生成

我们可以在链下生成随机数,然后通过预言机把随机数上传到链上。Chainlink提供VRF(可验证随机函数)服务,链上开发者可以支付LINK代币来获取随机数。 Chainlink VRF有两个版本,第二个版本需要官网注册并预付费,比第一个版本多许多操作,需要花费更多的gas,但取消订阅后可以拿回剩余的Link,这里介绍第二个版本Chainlink VRF V2

3)、Chainlink VRF使用步骤

我们将用一个简单的合约介绍使用Chainlink VRF的步骤。RandomNumberConsumer合约可以向VRF请求随机数,并存储在状态变量randomWords中。

1. 申请Subscription并转入Link代币’

在Chainlink VRF网站这里上创建一个Subscription,其中邮箱和项目名都是选填

创建完成后往Subscription中转入一些Link代币。测试网的LINK代币可以从LINK水龙头领取。

2. 用户合约继承VRFConsumerBaseV2

为了使用VRF获取随机数,合约需要继承VRFConsumerBaseV2合约,并在构造函数中初始化VRFCoordinatorV2InterfaceSubscription Id

注意: 不同链对应不同的参数,在这里查询。

教程中我们使用Sepolia测试网。

旧版V1

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

//旧版
contract RandomNumberConsumer is VRFConsumerBaseV2{

    //请求随机数需要调用VRFCoordinatorV2Interface接口
    VRFCoordinatorV2Interface COORDINATOR;
    
    // 申请后的subId
    uint64 subId;

    //存放得到的 requestId 和 随机数
    uint256 public requestId;
    uint256[] public randomWords;
    
    /**
     * 使用chainlink VRF,构造函数需要继承 VRFConsumerBaseV2
     * 不同链参数填的不一样
     * 具体可以看:https://docs.chain.link/vrf/v2/subscription/supported-networks
     * 网络: Sepolia测试网
     * Chainlink VRF Coordinator 地址: 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
     * LINK 代币地址: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709
     * 30 gwei Key Hash: 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c
     * Minimum Confirmations 最小确认块数 : 3 (数字大安全性高,一般填12)
     * callbackGasLimit gas限制 : 最大 2,500,000
     * Maximum Random Values 一次可以得到的随机数个数 : 最大 500          
     */
    address vrfCoordinator = 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625;
    bytes32 keyHash = 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;
    uint16 requestConfirmations = 3;
    uint32 callbackGasLimit = 200_000;
    uint32 numWords = 3;
    
    constructor(uint64 s_subId) VRFConsumerBaseV2(vrfCoordinator){
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        subId = s_subId;
    }

新版V2

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.21;

import "./ERC721.sol";
import "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";

contract Random2 is ERC721, VRFConsumerBaseV2Plus {
    uint256 public totalSupply = 100;
    uint256[100] public ids;
    uint256 public mintCount;

    event RequestSent(uint256 requestId, uint32 numWords);
    event RequestFulfilled(uint256 requestId, uint256[] randomWords);

    struct RequestStatus {
        bool fulfilled;
        bool exists;
        uint256[] randomWords;
    }
    mapping(uint256 => RequestStatus) public s_requests;
    mapping(uint256 => address) public requestToSender;

    uint256 public s_subscriptionsId;
    uint256[] public requestIds;
    uint256 public lastRequestId;

    bytes32 public keyHash =
        0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae;
    uint32 public callbackGasLimit = 100000;

    uint16 public requestConfirmations = 3;
    uint32 public numWords = 2;

    address COORDINATOR = 0x9DdfaCa8183c41ad55329BdeeD9F6A8d53168B1B;

    constructor(
        uint256 subscriptionId
    ) VRFConsumerBaseV2Plus(COORDINATOR) ERC721("ETH Random", "ETH") {
        s_subscriptionsId = subscriptionId;
    }

    //请求随机数
    function requestRandomWords() public {
        uint256 requestId = COORDINATOR.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: keyHash,
                subId: s_subscriptionsId,
                requestConfirmations: requestConfirmations,
                callbackGasLimit: callbackGasLimit,
                numWords: numWords,
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: true})
                )
            })
        );

        s_requests[requestId] = RequestStatus({
            randomWords: new uint256[](0),
            exists: true,
            fulfilled: false
        });
        requestIds.push(requestId);
        lastRequestId = requestId;

        //存储随机数提供者的信息
        requestToSender[requestId] = msg.sender;

        emit RequestSent(requestId, numWords);
    }

    /** 
    * 输入uint256数字,返回一个可以mint的tokenId
    * 算法过程可理解为:totalSupply个空杯子(0初始化的ids)排成一排,每个杯子旁边放一个球,编号为[0, totalSupply - 1]。
      每次从场上随机拿走一个球(球可能在杯子旁边,这是初始状态;也可能是在杯子里,说明杯子旁边的球已经被拿走过,则此时新的球从末尾被放到了杯子里)
      再把末尾的一个球(依然是可能在杯子里也可能在杯子旁边)放进被拿走的球的杯子里,循环totalSupply次。相比传统的随机排列,省去了初始化ids[]的gas。
    */
    function pickRandomUniqueId(
        uint256 random
    ) private returns (uint256 tokenId) {
        uint256 len = totalSupply - mintCount++;    //可以铸造的tokenId数量
        require(len > 0, "mint close");
        uint256 randomIndex = random % len;     //获取链上随机数

        //随机数取模,得到tokenId,作为数组下标,同时记录value为len-1,如果取模得到的值已存在,则tokenId取该数组下标的value
        tokenId = ids[randomIndex] != 0 ? ids[randomIndex] : randomIndex;
        ids[randomIndex] = ids[len - 1] == 0 ? len - 1 : ids[len - 1];
        ids[len - 1] = 0;
    }

    //铸造NFT
    function mintRandomOnchain() public {
        uint256 _tokenId = pickRandomUniqueId(getRandomOnchain());
        _mint(msg.sender, _tokenId);
    }

    //将当前链上的随机数信息进行打包
    function getRandomOnchain() public view returns (uint256) {
        bytes32 randomBytes = keccak256(
            abi.encodePacked(
                blockhash(block.number - 1),
                msg.sender,
                block.timestamp
            )
        );
        return uint256(randomBytes);
    }

    function fulfillRandomWords(
        uint256 _requestsId,
        uint256[] calldata _randomWords
    ) internal override {
        require(s_requests[_requestsId].exists, "request not found");
        s_requests[_requestsId].fulfilled = true;
        s_requests[_requestsId].randomWords = _randomWords;

        //获取提供者
        address sender = requestToSender[_requestsId];

        uint256 tokenId = pickRandomUniqueId(_randomWords[0]);
        _mint(sender, tokenId);

        emit RequestFulfilled(_requestsId, _randomWords);
    }

    function getRequestStatus(
        uint256 _requestsId
    ) external view returns (bool fulfilled, uint256[] memory randomWords) {
        require(s_requests[_requestsId].exists, "request not found");
        RequestStatus memory request = s_requests[_requestsId];
        return (request.fulfilled, request.randomWords);
    }
}

2. 用户合约申请随机数

用户可以调用从VRFCoordinatorV2Interface接口合约中的requestRandomWords函数申请随机数,并返回申请标识符requestId。这个申请会传递给VRF合约。

注意: 合约部署后,需要把合约加入到SubscriptionConsumers中,才能发送申请。

/** 
     * 向VRF合约申请随机数 (旧版)
     */
    function requestRandomWords() external {
        requestId = COORDINATOR.requestRandomWords(
            keyHash,
            subId,
            requestConfirmations,
            callbackGasLimit,
            numWords
        );
    }
//请求随机数(新版)
    function requestRandomWords() public {
        uint256 requestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: keyHash,
                subId: s_subscriptionsId,
                requestConfirmations: requestConfirmations,
                callbackGasLimit: callbackGasLimit,
                numWords: numWords,
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: true})
                )
            })
        );

        s_requests[requestId] = RequestStatus({
            randomWords: new uint256[](0),
            exists: true,
            fulfilled: false
        });
        requestIds.push(requestId);
        lastRequestId = requestId;

        //存储随机数提供者的信息
        requestToSender[requestId] = msg.sender;

        emit RequestSent(requestId, numWords);
    }

3. Chainlink节点链下生成随机数和[数字签名],并发送给VRF合约

4. VRF合约验证签名有效性

5. 用户合约接收并使用随机数

VRF合约验证签名有效之后,会自动调用用户合约的回退函数fulfillRandomness(),将链下生成的随机数发送过来。用户要把消耗随机数的逻辑写在这里。

注意: 用户申请随机数时调用的requestRandomness()VRF合约返回随机数时调用的回退函数fulfillRandomness()是两笔交易,调用者分别是用户合约和VRF合约,后者比前者晚几分钟(不同链延迟不一样)。

/**
     * VRF合约的回调函数,验证随机数有效之后会自动被调用
     * 消耗随机数的逻辑写在这里
     */
    function fulfillRandomWords(uint256 requestId, uint256[] memory s_randomWords) internal override {
        randomWords = s_randomWords;
    }
4)、tokenId随机铸造的NFT

这一节,我们将利用链上和链下随机数来做一款tokenId随机铸造的NFTRandom合约继承ERC721VRFConsumerBaseV2合约。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/ERC721.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
//旧版
contract Random is ERC721, VRFConsumerBaseV2{
状态变量
  • NFT
    

    相关

    • totalSupplyNFT总供给。
    • ids:数组,用于计算可供minttokenId,见pickRandomUniqueId()函数。
    • mintCount:已经mint的数量。
  • Chainlink VRF
    

    相关

    • COORDINATOR:调用VRFCoordinatorV2Interface接口
    • vrfCoordinator:VRF合约地址
    • keyHash:VRF唯一标识符。
    • requestConfirmations:确认块数
    • callbackGasLimitVRF手续费。
    • numWords:请求的随机数个数
    • subId:申请的Subscription Id
    • requestId:申请标识符
    • requestToSender:记录申请VRF用于铸造的用户地址。
// NFT相关
    uint256 public totalSupply = 100; // 总供给
    uint256[100] public ids; // 用于计算可供mint的tokenId
    uint256 public mintCount; // 已mint数量

    // chainlink VRF参数
    
    //VRFCoordinatorV2Interface
    VRFCoordinatorV2Interface COORDINATOR;
    
    /**
     * 使用chainlink VRF,构造函数需要继承 VRFConsumerBaseV2
     * 不同链参数填的不一样
     * 网络: Sepolia测试网
     * Chainlink VRF Coordinator 地址: 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
     * LINK 代币地址: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709
     * 30 gwei Key Hash: 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c
     * Minimum Confirmations 最小确认块数 : 3 (数字大安全性高,一般填12)
     * callbackGasLimit gas限制 : 最大 2,500,000
     * Maximum Random Values 一次可以得到的随机数个数 : 最大 500          
     */
    address vrfCoordinator = 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625;
    bytes32 keyHash = 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;
    uint16 requestConfirmations = 3;
    uint32 callbackGasLimit = 1_000_000;
    uint32 numWords = 1;
    uint64 subId;
    uint256 public requestId;
    
    // 记录VRF申请标识对应的mint地址
    mapping(uint256 => address) public requestToSender;
构造函数

初始化继承的VRFConsumerBaseV2ERC721合约的相关变量。

constructor(uint64 s_subId) 
        VRFConsumerBaseV2(vrfCoordinator)
        ERC721("WTF Random", "WTF"){
            COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
            subId = s_subId;
    }
其他函数

除了构造函数以外,合约里还定义了5个函数。

  • pickRandomUniqueId():输入随机数,获取可供minttokenId
  • getRandomOnchain():获取链上随机数(不安全)。
  • mintRandomOnchain():利用链上随机数铸造NFT,调用了getRandomOnchain()pickRandomUniqueId()
  • mintRandomVRF():申请Chainlink VRF用于铸造随机数。由于使用随机数铸造的逻辑在回调函数fulfillRandomness(),而回调函数的调用者是VRF合约,而非铸造NFT的用户,这里必须利用requestToSender状态变量记录VRF申请标识符对应的用户地址。
  • fulfillRandomWords()VRF的回调函数,由VRF合约在验证随机数真实性后自动调用,用返回的链下随机数铸造NFT
/** 
    * 输入uint256数字,返回一个可以mint的tokenId
    * 算法过程可理解为:totalSupply个空杯子(0初始化的ids)排成一排,每个杯子旁边放一个球,编号为[0, totalSupply - 1]。
    每次从场上随机拿走一个球(球可能在杯子旁边,这是初始状态;也可能是在杯子里,说明杯子旁边的球已经被拿走过,则此时新的球从末尾被放到了杯子里)
    再把末尾的一个球(依然是可能在杯子里也可能在杯子旁边)放进被拿走的球的杯子里,循环totalSupply次。相比传统的随机排列,省去了初始化ids[]的gas。
    */
    function pickRandomUniqueId(uint256 random) private returns (uint256 tokenId) {
        //先计算减法,再计算++, 关注(a++,++a)区别
        uint256 len = totalSupply - mintCount++; // 可mint数量
        require(len > 0, "mint close"); // 所有tokenId被mint完了
        uint256 randomIndex = random % len; // 获取链上随机数

        //随机数取模,得到tokenId,作为数组下标,同时记录value为len-1,如果取模得到的值已存在,则tokenId取该数组下标的value
        tokenId = ids[randomIndex] != 0 ? ids[randomIndex] : randomIndex; // 获取tokenId
        ids[randomIndex] = ids[len - 1] == 0 ? len - 1 : ids[len - 1]; // 更新ids 列表
        ids[len - 1] = 0; // 删除最后一个元素,能返还gas
    }

    /** 
    * 链上伪随机数生成
    * keccak256(abi.encodePacked()中填上一些链上的全局变量/自定义变量
    * 返回时转换成uint256类型
    */
    function getRandomOnchain() public view returns(uint256){
        /*
         * 本例链上随机只依赖区块哈希,调用者地址,和区块时间,
         * 想提高随机性可以再增加一些属性比如nonce等,但是不能根本上解决安全问题
         */
        bytes32 randomBytes = keccak256(abi.encodePacked(blockhash(block.number-1), msg.sender, block.timestamp));
        return uint256(randomBytes);
    }

    // 利用链上伪随机数铸造NFT
    function mintRandomOnchain() public {
        uint256 _tokenId = pickRandomUniqueId(getRandomOnchain()); // 利用链上随机数生成tokenId
        _mint(msg.sender, _tokenId);
    }

    /** 
     * 调用VRF获取随机数,并mintNFT
     * 要调用requestRandomness()函数获取,消耗随机数的逻辑写在VRF的回调函数fulfillRandomness()中
     * 调用前,需要在Subscriptions中转入足够的Link
     */
    function mintRandomVRF() public {
        // 调用requestRandomness获取随机数
        requestId = COORDINATOR.requestRandomWords(
            keyHash,
            subId,
            requestConfirmations,
            callbackGasLimit,
            numWords
        );
        requestToSender[requestId] = msg.sender;
    }

    /**
     * VRF的回调函数,由VRF Coordinator调用
     * 消耗随机数的逻辑写在本函数中
     */
    function fulfillRandomWords(uint256 requestId, uint256[] memory s_randomWords) internal override{
        address sender = requestToSender[requestId]; // 从requestToSender中获取minter用户地址
        uint256 tokenId = pickRandomUniqueId(s_randomWords[0]); // 利用VRF返回的随机数生成tokenId
        _mint(sender, tokenId);
    }

新版RandomNumberConsumer2

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.21;

import "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";

contract RandomNumberConsumer2 is VRFConsumerBaseV2Plus {
    uint256 immutable s_subscriptionId;
    bytes32 immutable s_keyHash;
    uint32 constant CALLBACK_GAS_LIMIT = 100000;
    uint16 constant REQUEST_CONFIRMATIONS = 3;
    uint32 constant NUM_WORDS = 2;

    uint256[] public s_randomWords;
    uint256 public s_requestId;

    // bytes32 public keyHash =
    //     0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae;

    // address vrfCoordinator = 0x9DdfaCa8183c41ad55329BdeeD9F6A8d53168B1B;

    event ReturnedRandomness(uint256[] randomWords);

    constructor(
        uint256 subscriptionId,
        address vrfCoordinator,
        bytes32 keyHash
    ) VRFConsumerBaseV2Plus(vrfCoordinator) {
        s_keyHash = keyHash;
        s_subscriptionId = subscriptionId;
    }

    // constructor(uint256 subscriptionId) VRFConsumerBaseV2Plus(vrfCoordinator) {
    //     s_subscriptionId = subscriptionId;
    // }

    function requestRandomWords() external onlyOwner {
        s_requestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: s_keyHash,
                subId: s_subscriptionId,
                requestConfirmations: REQUEST_CONFIRMATIONS,
                callbackGasLimit: CALLBACK_GAS_LIMIT,
                numWords: NUM_WORDS,
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
                )
            })
        );
    }

    function fulfillRandomWords(
        uint256 /* requestId */,
        uint256[] calldata randomWords
    ) internal override {
        s_randomWords = randomWords;
        emit ReturnedRandomness(randomWords);
    }
}

5)、remix验证

1. 在Chainlink VRF上申请Subscription

2. 利用Chainlink水龙头获取测试网的LINKETH

3. 在Subscription中转入LINK代币

4. 在Sepolia测试网部署Random合约

5. 利用链上随机数铸造NFT

remix界面中,点击左侧橙色函数mintRandomOnchain。在弹出的小狐狸钱包中点击确认,利用链上随机数铸造交易就开始了

6. 在Consumers中添加合约地址

将合约加入到SubscriptionConsumers

  1. 利用Chainlink VRF链下随机数铸造NFT

同理,在remix界面中,点击左侧橙色函数mintRandomVRF/requestRandomWords,在弹出的小狐狸钱包中点击确认,利用Chainlink VRF链下随机数铸造交易就开始了

注意: 采用VRF铸造NFT时,发起交易和铸造成功不在同一个区块

8. 验证NFT已被铸造

9. 取消订阅

当合约不使用后可以在Chainlink VRF上取消订阅,取出剩余的LINK代币

6)、总结

Solidity中生成随机数没有其他编程语言那么容易。这一讲我们将介绍链上(哈希函数)和链下(chainlink预言机)随机数生成的两种方法,并利用它们做一款tokenId随机铸造的NFT。这两种方法各有利弊:使用链上随机数高效,但是不安全;而链下随机数生成依赖于第三方提供的预言机服务,比较安全,但是没那么简单经济。项目方要根据业务场景来选择适合自己的方案。

除此以外,还有一些组织在尝试RNG(Random Number Generation)的新鲜方式,如randao就提出以DAO的模式来提供一个on-chaintrue randomness的服务

About

仅仅只是用于测试的项目,它是用来获取和生产区块链上的随机数的!

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published