欢迎订阅专栏:10分钟智能合约:进阶实战
随机数问题:为什么区块链上的随机数难以安全生成
随机数问题 是指智能合约在需要不可预测的随机数时,由于区块链的确定性、公开性以及区块数据的可预测性,导致开发者错误地使用链上公开变量(如 blockhash、block.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. 总结
- 绝不要使用
blockhash、block.timestamp、block.difficulty、gasleft等链上公开变量作为不可预测随机数的唯一来源。 - 矿工/验证者 有能力影响这些变量的值,或选择性打包交易。
- 使用专业随机数预言机(如 Chainlink VRF)是唯一安全且去中心化的方案。
- 如果你必须自己实现,考虑 Commit-Reveal + 经济惩罚,但这是复杂且有风险的。
审计要点:检查所有声称“随机数”的生成逻辑,确认其不可被矿工操控、不可被用户提前预测、不可被回滚攻击。