10分钟智能合约:进阶实战-4.4 随机数问题

50 阅读5分钟

欢迎订阅专栏10分钟智能合约:进阶实战

随机数问题:为什么区块链上的随机数难以安全生成

随机数问题 是指智能合约在需要不可预测的随机数时,由于区块链的确定性、公开性以及区块数据的可预测性,导致开发者错误地使用链上公开变量(如 blockhashblock.timestamp)作为随机数源,从而使随机数可被矿工/验证者操控或攻击者提前计算,最终造成应用(如彩票、NFT 铸造、游戏)被操纵,资金损失。

核心悖论:区块链要求所有节点计算结果确定一致,而随机数本质上是不确定的。两者之间存在根本冲突。


1. 链上“随机数”的不可信来源

开发者常误用的伪随机数来源及其风险:

随机数来源风险描述
blockhash(block.number-1)矿工可轻微调整区块参数使得 hash 符合预期(如最后一位为 0)。且只能获取最近 256 个区块的 hash,超出返回 0。
block.timestamp矿工可在一定范围内调整时间戳(通常 ±15 秒)。攻击者可预测/选择对自己有利的时间戳。
block.difficulty / block.number公开且可预测。
msg.sender / tx.origin公开,可被攻击者提前计算。
gasleft()攻击者可通过控制 Gas 来影响结果。
以上变量的 keccak256 组合仍然可预测,因为所有输入都是公开的。

本质:这些变量在交易被打包前就是已知的(对矿工而言)或可暴力枚举的。矿工有动机选择对自己有利的结果(例如在彩票游戏中,矿工可以丢弃导致自己输的交易,只打包赢的交易)。


2. 典型漏洞示例

2.1 使用 blockhash 作为随机数

contract BadRandom {
    uint256 private seed = 123456;

    function guess(uint256 guess) external returns (bool) {
        uint256 random = uint256(blockhash(block.number - 1)) % 100;
        return guess == random;
    }
}
  • 攻击:矿工可以尝试不同的交易排序和 nonce,直到 blockhash 使其猜中的结果;或者用户在本地提前计算下一区块的 hash,直接提交正确的 guess

2.2 使用 keccak256(abi.encodePacked(block.timestamp, msg.sender))

function mintNFT() external payable {
    uint256 rand = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 10000;
    // 根据 rand 决定 NFT 稀有度
}
  • 攻击:攻击者可以在自己的合约中模拟调用 mintNFT,预先计算出 rand,如果结果不合意就取消交易,否则发送。

2.3 依赖未来区块的 blockhash

function random() external view returns (uint256) {
    return uint256(blockhash(block.number + 10));  // 错误:未来的区块 hash 未知,且 block.number+10 超出范围
}
  • 后果blockhash(block.number) 返回 0,blockhash(block.number+delta) 在 Solidity 中直接返回 0(因为参数必须是小于当前区块号 256 的过去区块号)。

3. 攻击手法

攻击类型描述
矿工预知/操控矿工可以在打包交易时选择性包含或排除交易,或微调区块时间戳/coinbase,使随机数对自己有利。
用户前端预测用户通过估算当前区块参数,提前计算出随机数,只提交有利的交易。
交易回滚攻击者在合约中调用随机函数,如果结果不满意,使用 assert(false)revert 让交易失败,直到成功。
Front-running监听 mempool 中用户的随机数相关交易,计算结果,然后抢先发包套利。

4. 安全解决方案

方案描述适用场景
Chainlink VRF(推荐)去中心化预言机提供可验证的随机数,由链下生成并提交链上,附密码学证明,无法篡改。所有需要不可预测随机数的场景(彩票、NFT 稀有度、游戏)。
Commit-Reveal 方案用户先提交随机数的哈希(承诺),后再提交原值(揭示)。可防止他人提前得知,但无法防止用户自己反悔(可通过经济惩罚缓解)。拍卖、投票等需要隐藏出价场景。
RANDAO多参与者各自提交随机数种子,组合成最终随机数。需要足够数量的诚实参与者。DAO、去中心化随机数生成器。
blockhash + 外部熵结合区块数据和链下可信源(如将来某高度后的某个公开事件),但仍是确定性的。对安全性要求不高的场景。
Signidice两个玩家各自签名,交互生成随机数,适用于两方游戏。PvP 游戏。

最佳实践:对于生产级的 DApp,直接使用 Chainlink VRF。它是当前业界标准,经过了大量安全审计和实战检验。

Chainlink VRF 示例:

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

contract RandomConsumer is VRFConsumerBaseV2 {
    VRFCoordinatorV2Interface COORDINATOR;
    uint64 s_subscriptionId;
    bytes32 s_keyHash = 0x...;
    uint32 callbackGasLimit = 100000;

    function requestRandomWords() external {
        COORDINATOR.requestRandomWords(
            s_keyHash,
            s_subscriptionId,
            3,        // 确认数
            callbackGasLimit,
            1         // 请求一个随机数
        );
    }

    function fulfillRandomWords(uint256, uint256[] memory randomWords) internal override {
        uint256 random = randomWords[0] % 10000;
        // 安全使用随机数
    }
}

5. 总结

  • 绝不要使用 blockhashblock.timestampblock.difficultygasleft 等链上公开变量作为不可预测随机数的唯一来源。
  • 矿工/验证者 有能力影响这些变量的值,或选择性打包交易。
  • 使用专业随机数预言机(如 Chainlink VRF)是唯一安全且去中心化的方案。
  • 如果你必须自己实现,考虑 Commit-Reveal + 经济惩罚,但这是复杂且有风险的。

审计要点:检查所有声称“随机数”的生成逻辑,确认其不可被矿工操控、不可被用户提前预测、不可被回滚攻击。