4、链上随机数基础:Solidity中的随机数问题、blockhash与timestamp的局限性、预言机随机数
随机数在NFT里有多重要?说白了,稀有度就是靠随机数撑起来的。你想想看,如果随机数能被预测,那稀有NFT就全被科学家抢走了,普通玩家还玩什么?
但问题来了——区块链是确定性的。每个节点执行同样的代码,得到同样的结果。这跟随机天生就是矛盾的。我刚开始做NFT时,就踩过这个坑,今天好好聊聊。
4.1 Solidity里的随机数到底难在哪?
在传统开发里,随机数太简单了。Python里一个random.randint()就搞定。但在Solidity里,你没法用系统时间、鼠标移动这些熵源。所有输入都是公开的、可预测的。
这就导致了一个核心矛盾:
- 完全去中心化:每个节点必须得到相同结果
- 真正的随机性:需要不可预测的熵源
这两个目标,天然就是冲突的。嗯,这里要注意,不是Solidity设计得不好,而是区块链的底层逻辑决定的。
核心结论:Solidity本身没有原生的安全随机数生成器。所有链上随机方案,本质上都是「伪随机」或「外部引入随机性」。
4.2 blockhash与timestamp:新手最容易掉进去的坑
很多新手会这么写:
// ❌ 危险示例!千万别这么用
function unsafeRandom() public view returns (uint256) {
return uint256(keccak256(abi.encodePacked(
block.timestamp,
blockhash(block.number - 1),
msg.sender
)));
}
看起来挺像回事的,对吧?但我告诉你,这个方案在真正的NFT铸造场景里,基本等于没有随机性。
4.2.1 block.timestamp的问题
矿工可以操纵时间戳。虽然不能改太多,但±15秒的调整空间是有的。在抢NFT铸造的场景里,15秒足够让矿工算出对自己有利的随机数了。
我记得有个项目,就是用timestamp做随机数。结果科学家们直接跟矿工勾结,每次铸造都拿到最好的稀有度。项目方亏惨了。
4.2.2 blockhash的问题
blockhash(block.number) 返回的是当前区块的哈希。但问题是——这个值在同一个区块内是固定的。而且Solidity官方文档明确说了:只能访问最近256个区块的hash。
更致命的是:
- 如果你用
blockhash(block.number),它永远返回0 - 如果你用
blockhash(block.number - 1),矿工可以提前计算
⚠️ 避坑指南:我曾经在一个测试网项目里用了blockhash,本地跑得好好的,一上主网就全废了。查了两天才发现是block.number的边界问题。从那以后,我再也不在正式项目里用blockhash做随机数了。
4.3 为什么这些方案不安全?
说白了,问题出在「可预测性」上。我们用一个表格来对比:
| 随机源 | 可预测性 | 矿工可操纵 | 实际安全性 |
|---|---|---|---|
| block.timestamp | 高 | 是(±15秒) | ❌ 极低 |
| blockhash | 中 | 是(同一区块内) | ❌ 低 |
| block.difficulty | 中 | 部分可操纵 | ❌ 低 |
| msg.sender + nonce | 低 | 否 | ⚠️ 中等(可被front-run) |
你想想看,如果矿工能预测随机数,他就可以:
- 先计算哪个区块位置能拿到好随机数
- 把自己的交易安排在那个位置
- 拿到稀有NFT后转手卖掉
这就是典型的MEV攻击。我见过一个PFP项目,就因为用了timestamp做随机数,结果前100个铸造的全是科学家,项目直接凉了。
4.4 预言机随机数:目前最靠谱的方案
既然链上搞不定,那就从链外引入随机性。这就是预言机(Oracle)的思路。
4.4.1 Chainlink VRF 的工作原理
Chainlink VRF(可验证随机函数)是目前最主流的方案。它的流程是这样的:
- 你的合约向Chainlink发起随机数请求
- Chainlink的节点在链下生成随机数
- 同时生成一个密码学证明
- 随机数和证明一起上链
- 你的合约验证证明,确认随机数没被篡改
关键点在于:Chainlink的随机数是在链下生成的,矿工看不到也改不了。而且有密码学证明,Chainlink自己也没法作弊。
💡 个人经验:我建议所有NFT项目都用Chainlink VRF。虽然每次请求要花一点LINK代币,但跟被科学家薅羊毛的损失比起来,这点成本完全可以忽略。
4.4.2 代码示例:集成Chainlink VRF
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
contract RandomNFT is VRFConsumerBaseV2 {
VRFCoordinatorV2Interface COORDINATOR;
// 存储随机数
uint256[] public randomResults;
uint256 public requestId;
// 构造函数:初始化VRF
constructor(address coordinatorAddress) VRFConsumerBaseV2(coordinatorAddress) {
COORDINATOR = VRFCoordinatorV2Interface(coordinatorAddress);
}
// 请求随机数
function requestRandomNumber() external {
requestId = COORDINATOR.requestRandomWords(
0x...subscriptionId..., // 你的订阅ID
0x...gasLane..., // gas上限
100000, // callback gas
1, // 请求数量
1 // 每个请求返回几个随机数
);
}
// Chainlink回调函数
function fulfillRandomWords(
uint256 _requestId,
uint256[] memory _randomWords
) internal override {
randomResults.push(_randomWords[0]);
}
}
这段代码看着复杂,其实核心就两步:请求和接收。中间的所有密码学验证,Chainlink都帮你处理好了。
4.4.3 其他预言机方案
除了Chainlink,还有一些选择:
- Provable(原Oraclize):老牌预言机,但Gas费偏高
- API3:去中心化API网络,支持随机数
- DIY方案:自己搭一个随机数服务,但需要维护节点
我个人还是推荐Chainlink。生态最成熟,文档最全,出问题也好找人问。
4.5 实战中的选择建议
说了这么多,到底该怎么选?我总结一下:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 高价值NFT(稀有度影响价格) | Chainlink VRF | 安全、可验证、抗MEV |
| 测试网/原型 | blockhash + 时间戳 | 简单、免费、够用 |
| 低价值游戏道具 | 链上伪随机(如线性同余) | Gas费低、速度块 |
| 需要高频随机数 | 自定义预言机 | 成本可控、延迟低 |
⚠️ 重要提醒:千万不要在生产环境里只用blockhash或timestamp做随机数。我见过太多项目因此翻车了。省那点LINK代币,最后赔得更多。
4.6 小结
随机数在Solidity里是个老大难问题。blockhash和timestamp看着方便,但漏洞太多。预言机方案虽然要花钱,但安全可靠。
我的建议很简单:
- 做原型测试,随便用什么都行
- 上主网,老老实实用Chainlink VRF
- 别想着自己发明轮子,密码学的东西太容易出错了
下一章我们会讲具体的稀有度算法实现。到时候你会看到,随机数只是第一步,怎么把随机数转化成有意义的稀有度属性,才是真正的技术活。
💡 课后思考:如果你做一个盲盒NFT项目,用户铸造时不知道里面是什么。你会怎么设计随机数方案,既保证公平,又让用户体验流畅?