You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Chainlink测试网随机数返回0问题及主网风险与方案咨询

问题描述

我正在开发一款基于Chainlink订阅服务的NFT随机数应用,将智能合约添加为consumer后,在测试网中偶尔会出现返回0而非有效随机数的情况(订阅已充值、合约已添加为消费者)。我想了解:

  1. 该问题是否会在主网出现?
  2. 曾尝试用assert判断随机数非0,但导致钱包gas估算异常,现寻求安全的零值拦截方案。
  3. 如何保障随机数请求成功?

附BSC测试网合约地址:0x74623BaE2c3AcC39Db224c236229dc4d5aD1F64d

相关合约代码:

function closeSaleGetWinner()
    external
    nonReentrant
    onlyRole(OPERATOR_ROLE)
{
    requestRandomWords();

    require(
        isDrawLive,
        "There's not a live draw neither ticket sale is opened"
    );
    uint256[6] memory numberOfMatches;
    uint256 drawPrice;

    // set the total pool price to BUSD smart contract balance

    drawPrice =
        checkBUSDContractBalance() *
        allDraws[currentDraw].poolPercentage; // use 1 * 10**18 for use the whole pool

    // close ticket sale and get random winner number from s_randomWords

    isDrawLive = false;

    //assert(s_randomWords[0]!=0);


    allDraws[currentDraw].winnerNumber = s_slicedRandomWords;
    //unchecked {
    uint256 j;
    Ticket storage _ticket;
    uint256 _match;
    // check and update matched numbers per ticket on allTickets structure
    for (uint256 i = 0; i < drawToTickets[currentDraw].length; i++) {
        j = drawToTickets[currentDraw][i];
        _ticket = allTickets[j];
        _match = checkWinner(_ticket);
        _ticket.matchedNumbers = _match;
        numberOfMatches[_match] = numberOfMatches[_match] + 1;
    }

    // after storing number of winners with [_match] matches calculate win per group

    // it's time to overwrite reward variable with
    // 1st the part of the price from the total pool for #i number of matches
    // 2nd divide e
    for (uint256 i = 0; i < 5; i++) {
        allDraws[currentDraw].reward[i] =
            allDraws[currentDraw].reward[i] *
            drawPrice;
        if (numberOfMatches[i + 1] > 0) {
            allDraws[currentDraw].reward[i] = SafeMath.div(
                allDraws[currentDraw].reward[i],
                numberOfMatches[i + 1]
            );
        } else {
            allDraws[currentDraw].reward[i] = 0;
        }
    }

    // once stored delete random generated number for further checks
    delete (s_randomWords);
    delete (s_slicedRandomWords);
}

/*
compares the ticket number with the winner number of the draw and returns a value
representing matched number between 0 to 5 (6 values)
*/

function checkWinner(Ticket storage t) internal view returns (uint256) {
    uint256 _match = 0;

    // we go and compare digit by digit storing number of consecutive matches and stopping when
    // there are no more coincidences

    uint256[5] memory ticketNumber = t.number;
    uint256[5] memory winnerNumber = allDraws[_drawIdCounter.current() - 1]
        .winnerNumber;

    for (uint256 i = 0; i < 5; i++) {
        // If there exists any distinct
        // lastTicketDigit, then return No
        if (ticketNumber[i] == winnerNumber[i]) {
            _match = _match + 1;
        } else return _match;
    }
    return _match;
}



function buyRandomTicket() public nonReentrant {
    
    uint256[] storage myTickets = accounts[msg.sender].myTicketsHistory;
    require(isDrawLive, "Sorry there's no live draw by now");

    require(!Address.isContract(msg.sender), "only EOA allowed");

    address user = address(msg.sender);
    uint256 _value = allDraws[currentDraw].ticketPrice;

    // check balance and user allowance

    uint256 busdUserBalance = BUSD.balanceOf(msg.sender);
    require(busdUserBalance >= _value, "Not enough balance");

    uint256 allowance = BUSD.allowance(msg.sender, address(this));
    require(allowance >= _value, "Check the BUSD allowance");

    uint256 devsReward = _value.mul(5).div(100);

    uint256 otherReward = _value.mul(20).div(100);

    // get payment

    BUSD.safeTransferFrom(msg.sender, address(this), _value);

    requestRandomWords();

    BUSD.safeTransfer(w1, devsReward);

    BUSD.safeTransfer(w2, devsReward);

    BUSD.safeTransfer(w3, otherReward);

    /*pay referral*/
    if (accounts[msg.sender].referrer != address(0))
        payReferral(_value, address(msg.sender));

    Ticket memory randomTicket = Ticket(
        user,
        currentDraw,
        _ticketIdCounter.current(),
        s_slicedRandomWords,
        false,
        0
    );
    //require(s_randomWords[0]!=0);

    uint256[] storage drawTickets = drawToTickets[currentDraw];
    // add to the mapping to store tickets sold for a draw
    drawTickets.push(randomTicket.ticketID);
    myTickets.push(randomTicket.ticketID);
    allTickets.push(randomTicket);

    safeMint(user);

    // set to 0 random storage variable for further checks
    delete (s_randomWords);
    delete (s_slicedRandomWords);
}

/* CHAINLINK FUNCTIONS */

// Assumes the subscription is funded sufficiently.
function requestRandomWords() internal {
    // Will revert if subscription is not set and funded.
    s_requestId = COORDINATOR.requestRandomWords(
        keyHash,
        s_subscriptionId,
        requestConfirmations,
        callbackGasLimit,
        numWords
    );
}

function fulfillRandomWords(
    uint256, /* requestId */
    uint256[] memory randomWords
) internal override {
    uint256 _lastDigit;
    s_randomWords = randomWords;
    uint256 aux = randomWords[0];
    for (uint256 i = 0; i < 5; i++) {
        _lastDigit = aux % 10;
        s_slicedRandomWords[i] = _lastDigit;
        aux = aux / 10;
    }
}

问题解答

一、主网是否会出现零值随机数?

Chainlink VRF返回0的情况在主网中极低概率出现,但并非完全不可能:

  • 测试网偶尔出现零值大概率是节点测试环境的偶发异常,主网节点的稳定性和可靠性远高于测试网,返回0的概率可忽略不计。
  • 但从合约安全角度,必须假设0是合法的随机值(尽管概率极低),做好兜底处理——因为Chainlink VRF的随机数基于密码学安全源,0本身属于有效输出范围。

二、安全的零值拦截方案(避免gas估算异常)

使用assert会导致gas估算异常,因为assert失败会消耗所有剩余gas,钱包无法预判触发逻辑。核心问题是你的代码存在异步逻辑错误:调用requestRandomWords后直接使用s_slicedRandomWords,但Chainlink VRF是异步回调模式,此时随机数还未生成,直接用初始值0才是测试网零值的主要原因。推荐以下方案:

方案1:拆分业务流程,等待回调完成后执行逻辑

将发起请求和使用随机数的逻辑拆分,先完成转账、分佣等不依赖随机数的操作,等待fulfillRandomWords回调后再生成ticket或计算中奖:

// 新增存储待处理请求的映射
mapping(uint256 => address) public s_pendingTicketRequests;

function buyRandomTicket() public nonReentrant {
    // ... 原有校验逻辑 ...

    BUSD.safeTransferFrom(msg.sender, address(this), _value);

    // 发起随机数请求并记录请求ID和用户
    uint256 requestId = COORDINATOR.requestRandomWords(
        keyHash,
        s_subscriptionId,
        requestConfirmations,
        callbackGasLimit,
        numWords
    );
    s_pendingTicketRequests[requestId] = msg.sender;

    // 完成分佣、推荐奖励逻辑
    BUSD.safeTransfer(w1, devsReward);
    BUSD.safeTransfer(w2, devsReward);
    BUSD.safeTransfer(w3, otherReward);
    if (accounts[msg.sender].referrer != address(0))
        payReferral(_value, address(msg.sender));
}

function fulfillRandomWords(
    uint256 requestId,
    uint256[] memory randomWords
) internal override {
    // 拦截零值随机数,用require替代assert,仅回滚当前回调交易
    require(randomWords[0] != 0, "Invalid random number");

    address user = s_pendingTicketRequests[requestId];
    delete s_pendingTicketRequests[requestId];

    // 处理随机数生成ticket
    uint256 _lastDigit;
    uint256 aux = randomWords[0];
    uint256[5] memory slicedRandom;
    for (uint256 i = 0; i < 5; i++) {
        _lastDigit = aux % 10;
        slicedRandom[i] = _lastDigit;
        aux = aux / 10;
    }

    Ticket memory randomTicket = Ticket(
        user,
        currentDraw,
        _ticketIdCounter.current(),
        slicedRandom,
        false,
        0
    );
    // ... 存储ticket、mint NFT逻辑 ...
    _ticketIdCounter.increment();
}

方案2:在回调中添加重试机制

如果遇到零值或无效随机数,自动重试请求(限制重试次数避免无限循环):

mapping(uint256 => uint256) public s_requestRetryCount;

function fulfillRandomWords(
    uint256 requestId,
    uint256[] memory randomWords
) internal override {
    if (randomWords[0] == 0) {
        // 限制最多重试3次
        if (s_requestRetryCount[requestId] < 3) {
            s_requestRetryCount[requestId]++;
            requestRandomWords();
            return;
        }
        // 超过重试次数触发事件通知管理员
        emit RandomRequestFailed(requestId);
        return;
    }

    // 正常处理随机数逻辑
    uint256 _lastDigit;
    s_randomWords = randomWords;
    uint256 aux = randomWords[0];
    for (uint256 i = 0; i < 5; i++) {
        _lastDigit = aux % 10;
        s_slicedRandomWords[i] = _lastDigit;
        aux = aux / 10;
    }
}

三、保障随机数请求成功的措施

  1. 设置合理的回调gas限制:callbackGasLimit要足够覆盖fulfillRandomWords的执行逻辑,建议测试时估算所需gas后设置1.5-2倍余量,避免因gas不足导致回调失败。
  2. 监控订阅余额:确保订阅账户始终有足够LINK余额,主网可通过自动化脚本监控余额,低于阈值时自动充值。
  3. 记录请求状态:用映射记录每个请求ID的状态(pending/complete/failed),方便排查问题和触发应急处理。
  4. 选择官方推荐节点:主网优先使用Chainlink官方推荐的VRF节点,避免第三方不稳定节点。
  5. 添加超时处理:针对长时间未回调的请求,允许管理员手动触发重试或应急逻辑。

内容的提问来源于stack exchange,提问作者eac

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 20:01:11