链上随机数生成

核心知识点讲解

区块链随机数的挑战

区块链的确定性和透明性使得生成安全的随机数变得困难:

  • 确定性:相同的输入总是产生相同的输出
  • 透明性:所有交易和状态对网络中的所有节点可见
  • 可预测性:攻击者可以预测或操纵随机数生成过程

链上随机数生成方法

1. 伪随机数生成

基于区块信息的伪随机数:

  • 使用 block.timestampblock.numberblock.difficulty 等区块变量
  • 结合用户输入或交易哈希
  • 优点:简单直接,不需要外部依赖
  • 缺点:可预测性高,容易被操纵

2. 承诺-揭示模式

使用多轮交互生成随机数:

  • 参与者提交加密后的随机数(承诺)
  • 所有参与者提交后,揭示各自的随机数
  • 组合所有揭示的随机数生成最终结果
  • 优点:安全性较高
  • 缺点:需要多轮交互,实现复杂

3. 预言机解决方案

使用外部预言机提供随机数:

  • Chainlink VRF:Verifiable Random Function,可验证的随机函数
  • API3 QRNG:Quantum Random Number Generator,量子随机数生成器
  • 优点:安全性高,不可预测
  • 缺点:需要支付预言机费用,有一定的延迟
  1. 请求随机数:智能合约向 Chainlink VRF 发送请求
  2. 生成随机数:Chainlink 节点使用 VRF 生成随机数和证明
  3. 验证随机数:智能合约验证证明的有效性
  4. 使用随机数:验证通过后,智能合约使用生成的随机数

实用案例分析

伪随机数实现

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

contract PseudoRandom {
    uint256 private nonce;
    
    function getRandomNumber() public returns (uint256) {
        // 使用区块信息和nonce生成伪随机数
        uint256 random = uint256(keccak256(abi.encodePacked(
            block.timestamp,
            block.difficulty,
            msg.sender,
            nonce
        )));
        nonce++;
        return random;
    }
    
    // 注意:此方法不安全,仅用于演示
    function getRandomInRange(uint256 min, uint256 max) public returns (uint256) {
        uint256 random = getRandomNumber();
        return min + (random % (max - min + 1));
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract RandomNumberConsumer is VRFConsumerBaseV2 {
    VRFCoordinatorV2Interface COORDINATOR;
    
    // 测试网络的VRF协调器地址
    address vrfCoordinator = 0x7a1BaC17Ccc5b313516C5E16fb24f7659aA5ebed;
    
    // 测试网络的LINK代币地址
    address linkToken = 0x326C977E6efc84E512bB9C30f76E30c160eD06FB;
    
    // 订阅ID,需要在Chainlink VRF界面创建
    uint64 subscriptionId;
    
    // 燃气限额
    uint32 callbackGasLimit = 100000;
    
    // 请求确认数
    uint16 requestConfirmations = 3;
    
    // 一次请求的随机数数量
    uint32 numWords = 1;
    
    // 存储生成的随机数
    uint256[] public randomWords;
    
    constructor(uint64 _subscriptionId) VRFConsumerBaseV2(vrfCoordinator) {
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        subscriptionId = _subscriptionId;
    }
    
    // 请求随机数
    function requestRandomWords() external {
        // 向VRF协调器发送随机数请求
        uint256 requestId = COORDINATOR.requestRandomWords(
            subscriptionId,
            requestConfirmations,
            callbackGasLimit,
            numWords,
            1 // 额外参数
        );
    }
    
    // VRF协调器的回调函数
    function fulfillRandomWords(
        uint256 /* requestId */,
        uint256[] memory _randomWords
    ) internal override {
        randomWords = _randomWords;
    }
    
    // 获取随机数范围
    function getRandomInRange(uint256 min, uint256 max) public view returns (uint256) {
        require(randomWords.length > 0, "No random words yet");
        uint256 random = randomWords[0];
        return min + (random % (max - min + 1));
    }
}

随机数应用场景

1. 彩票合约

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

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

contract Lottery is VRFConsumerBaseV2 {
    VRFCoordinatorV2Interface COORDINATOR;
    address vrfCoordinator = 0x7a1BaC17Ccc5b313516C5E16fb24f7659aA5ebed;
    address linkToken = 0x326C977E6efc84E512bB9C30f76E30c160eD06FB;
    uint64 subscriptionId;
    uint32 callbackGasLimit = 100000;
    uint16 requestConfirmations = 3;
    uint32 numWords = 1;
    
    address[] public participants;
    address public winner;
    bool public lotteryClosed;
    
    constructor(uint64 _subscriptionId) VRFConsumerBaseV2(vrfCoordinator) {
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        subscriptionId = _subscriptionId;
    }
    
    function enter() public payable {
        require(msg.value > 0, "Must send ETH to enter");
        require(!lotteryClosed, "Lottery is closed");
        participants.push(msg.sender);
    }
    
    function closeLottery() public {
        require(!lotteryClosed, "Lottery is already closed");
        lotteryClosed = true;
        requestRandomWords();
    }
    
    function requestRandomWords() internal {
        COORDINATOR.requestRandomWords(
            subscriptionId,
            requestConfirmations,
            callbackGasLimit,
            numWords,
            1
        );
    }
    
    function fulfillRandomWords(
        uint256 /* requestId */,
        uint256[] memory _randomWords
    ) internal override {
        uint256 winnerIndex = _randomWords[0] % participants.length;
        winner = participants[winnerIndex];
        payable(winner).transfer(address(this).balance);
    }
    
    function getParticipantsCount() public view returns (uint256) {
        return participants.length;
    }
}

2. NFT随机属性生成

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

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

contract RandomNFT is ERC721, VRFConsumerBaseV2 {
    VRFCoordinatorV2Interface COORDINATOR;
    address vrfCoordinator = 0x7a1BaC17Ccc5b313516C5E16fb24f7659aA5ebed;
    address linkToken = 0x326C977E6efc84E512bB9C30f76E30c160eD06FB;
    uint64 subscriptionId;
    uint32 callbackGasLimit = 100000;
    uint16 requestConfirmations = 3;
    uint32 numWords = 3; // 生成3个随机数用于3个属性
    
    uint256 public tokenCounter;
    mapping(uint256 => uint256[]) public tokenAttributes;
    mapping(uint256 => uint256) public requestIdToTokenId;
    
    constructor(uint64 _subscriptionId) 
        ERC721("RandomNFT", "RNFT") 
        VRFConsumerBaseV2(vrfCoordinator)
    {
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        subscriptionId = _subscriptionId;
    }
    
    function mint() public {
        uint256 tokenId = tokenCounter;
        _safeMint(msg.sender, tokenId);
        
        uint256 requestId = COORDINATOR.requestRandomWords(
            subscriptionId,
            requestConfirmations,
            callbackGasLimit,
            numWords,
            tokenId
        );
        
        requestIdToTokenId[requestId] = tokenId;
        tokenCounter++;
    }
    
    function fulfillRandomWords(
        uint256 requestId,
        uint256[] memory _randomWords
    ) internal override {
        uint256 tokenId = requestIdToTokenId[requestId];
        
        // 生成3个属性值(0-99)
        uint256[] memory attributes = new uint256[](3);
        attributes[0] = _randomWords[0] % 100;
        attributes[1] = _randomWords[1] % 100;
        attributes[2] = _randomWords[2] % 100;
        
        tokenAttributes[tokenId] = attributes;
    }
    
    function getTokenAttributes(uint256 tokenId) public view returns (uint256[] memory) {
        return tokenAttributes[tokenId];
    }
}

实践练习

  1. 部署伪随机数合约

    • 部署上面的 PseudoRandom 合约
    • 调用 getRandomNumber 方法生成随机数
    • 分析其安全性问题
  2. 使用 Chainlink VRF

    • 在 Chainlink 网站上创建 VRF 订阅
    • 部署 RandomNumberConsumer 合约
    • 为合约充值 LINK 代币
    • 调用 requestRandomWords 方法
    • 等待回调并获取随机数
  3. 开发彩票合约

    • 部署 Lottery 合约
    • 多个账户参与彩票
    • 关闭彩票并等待随机数生成
    • 验证 winner 地址和奖金发放
  4. 创建随机属性 NFT

    • 部署 RandomNFT 合约
    • 铸造 NFT
    • 查看生成的随机属性

总结

链上随机数生成是Web3开发中的一个重要挑战,因为区块链的确定性和透明性特性。在选择随机数生成方法时,需要根据应用场景的安全性要求进行权衡:

  • 伪随机数:适合对安全性要求不高的场景,如游戏内的非关键随机事件
  • 承诺-揭示模式:适合需要多方参与的场景,如DAO投票
  • Chainlink VRF:适合对安全性要求高的场景,如彩票、NFT minting、DeFi协议

Chainlink VRF 是目前最安全可靠的链上随机数解决方案,它通过密码学证明确保随机数的不可预测性和可验证性。虽然使用 Chainlink VRF 需要支付一定的费用,但对于需要高安全性的应用来说,这是值得的投资。

通过本集的学习,你应该能够理解链上随机数生成的挑战和解决方案,为开发需要随机数的Web3应用做好准备。

« 上一篇 智能合约升级 下一篇 » 时间锁和权限管理