一、链上随机数为啥经常“不靠谱”
很多做链上应用的开发者,不管是做抽奖、NFT mint还是游戏对战,都会用到随机数,但新手很容易踩坑——直接用链上当前的块哈希、时间戳来当随机数,结果最后发现随机数是“可被操控”的。举个真实例子:某早期链上抽奖项目,用block.timestamp(当前块时间戳)生成中奖号码,结果矿工提前调整了块的时间戳,把中奖号改成了自己想要的数字,最后项目亏了十几万。本质上,链上的公开数据,只要是被区块生产者(比如以太坊矿工)能掌控或篡改的,都不能当可信随机数。
比如块哈希,它是每个区块的唯一哈希值,看似随机,但矿工可以选择“挖不挖这个块”——如果这个块的哈希用来生成的随机数,会让矿工亏损,他就会放弃挖这个块,等下一个块生成,这样就跳过了那个“坏”随机数,甚至人为控制出奖结果。时间戳更夸张,矿工可以把块的时间戳调整到下一分钟,差几分钟就能改一次随机数,完全没法用在需要公平的场景。
二、靠谱的链上随机数:VRF和块哈希的适用场景
2.1 可验证随机函数(VRF):高安全需求的首选
可验证随机函数,说白了就是“带公证的扔骰子”:你找一个大家都信任的第三方(比如Chainlink的节点),这个第三方有一个只有自己知道的“秘密种子”,他扔骰子的时候,不仅给你结果,还给你一个“验证证书”。这个证书是用他的秘密种子算出来的,链上所有人都能公开验证——既证明了结果没被篡改,也证明公证人没法抵赖自己的操作。
VRF的核心优势是三个:不可预测(生成前没人知道结果)、可公开验证(任何人都能查证书真伪)、不可篡改(一旦生成就没法改)。所以它的适用场景都是高安全要求的:比如高价值的NFT白名单、百万级别的链上抽奖、游戏里的稀有装备掉落、DeFi里的随机清算(比如随机选择要清算的用户),这些场景容不得半点作弊。
不过VRF也有缺点:它需要依赖第三方节点提供服务(比如Chainlink VRF),会消耗更多的Gas费,比用块哈希贵一点,但对于高价值应用来说,这点Gas费完全值得。下面用Solidity写一个简单的VRF示例,技术栈是Solidity 0.8.20 + Chainlink VRF v2.5:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// 导入Chainlink VRF核心合约和接口
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/vrf/interfaces/VRFCoordinatorV2Interface.sol";
// 继承VRF消费者基类,实现随机数获取逻辑
contract HighValueRaffle is VRFConsumerBaseV2 {
// VRF协调者地址(以太坊主网用这个,测试网换对应地址即可)
VRFCoordinatorV2Interface public immutable COORDINATOR;
// Chainlink官网创建的订阅ID,替换成自己的
uint64 public immutable subscriptionId;
// Gas上限(支付给节点的Gas费用)
uint32 public constant CALLBACK_GAS_LIMIT = 100000;
// 等待区块确认数,越少越快但风险略高,3个是常用值
uint16 public constant REQUEST_CONFIRMATIONS = 3;
// 需要生成的随机数个数,这里只需要1个
uint32 public constant NUM_WORDS = 1;
// 存储请求ID和对应的发起者,防止重复请求
mapping(uint256 => address) public requestToUser;
// 存储生成的可信随机数
uint256 public lotteryRandom;
// 标记是否已经生成随机数,防止重复开奖
bool public isRandomReady;
// 构造函数初始化VRF配置
constructor(address _vrfCoordinator, uint64 _subId) VRFConsumerBaseV2(_vrfCoordinator) {
COORDINATOR = VRFCoordinatorV2Interface(_vrfCoordinator);
subscriptionId = _subId;
}
// 用户发起随机数请求的函数,任何人都能调用
function requestRandom() external returns (uint256 reqId) {
require(!isRandomReady, "Lottery already drawn");
// 向Chainlink节点请求随机数,传入参数:Gas价格、订阅ID、确认数、Gas上限、随机数个数
reqId = COORDINATOR.requestRandomWords(
0x47e179ec197488593b187f80a00eb0da91f1b9d0b8a7b1d8f4c3e6d8a2b3c4d,
subscriptionId,
REQUEST_CONFIRMATIONS,
CALLBACK_GAS_LIMIT,
NUM_WORDS
);
requestToUser[reqId] = msg.sender;
}
// VRF节点返回随机数后的回调函数,只能由协调者调用
function fulfillRandomWords(uint256, uint256[] memory randomWords) internal override {
require(!isRandomReady, "Random already set");
lotteryRandom = randomWords[0];
isRandomReady = true;
}
// 获取中奖号码,这里取随机数模100,得到0-99的公平号码
function getWinningNumber() public view returns (uint256) {
require(isRandomReady, "Wait for random number");
return lotteryRandom % 100;
}
}
2.2 块哈希方案:低安全需求的轻量选择
如果你的应用是低价值的,比如小程序里的积分抽奖、测试网的Demo、游戏里的非核心随机操作(比如谁先开局),那用块哈希就够了——它不需要依赖第三方,Gas费极低,几乎不用额外成本。
不过块哈希的缺点很明显:安全性差,只能用最近256个块的哈希(超过256个块的哈希会被链上清除,返回空),而且前1-2个块的哈希,矿工还是有小概率操控(比如放弃坏块等下一个),所以只能用在“就算作弊也不会有大损失”的场景。下面是Solidity写的块哈希示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// 低价值抽奖用块哈希生成随机数,无需第三方依赖
contract LowValueRaffle {
// 存储前一个块的高度,取它的哈希来生成随机数
uint256 public previousBlock;
// 存储生成的随机数
uint256 public raffleRandom;
// 标记是否开奖
bool public isDrawed;
constructor() {
// 用前一个块的哈希,避免当前块哈希未生成的问题
previousBlock = block.number - 1;
}
// 开奖函数,只能调用一次
function draw() external {
require(!isDrawed, "Already drew");
// 获取块哈希,注意:只有最近256个块的哈希有效
bytes32 hash = blockhash(previousBlock);
require(hash != bytes32(0), "Block too old, can't get hash");
// 取模100得到0-99的中奖号
raffleRandom = uint256(hash) % 100;
isDrawed = true;
}
// 获取中奖号码
function getWinNum() public view returns (uint256) {
require(isDrawed, "Not drawn yet");
return raffleRandom;
}
}
三、预言机随机数被提前预测的规避方法
很多开发者会用预言机(比如Chainlink)获取随机数,但有个容易忽略的风险:预言机生成的随机数,在“生成”和“你使用”之间的这段时间,会被链上的其他人提前看到,然后操控交易套利——比如你用预言机的随机数发NFT,有人看到随机数后立刻 mint 所有稀有NFT,这就是“提前预测”的问题。
3.1 延迟揭示机制
最简单的规避方法:设置“延迟使用随机数”。比如你发起请求的时候,告诉预言机“3个块之后再把随机数给我”,预言机就会把随机数存3个块,过了时间再返回给你,没人能提前拿到这个数,自然没法套利。
3.2 用VRF替代普通预言机随机数
VRF的随机数是不可预测的,因为只有公证节点有秘密种子,别人没法提前算出结果,从根源上避免了提前预测的问题,比延迟揭示更彻底,适合高价值场景。
3.3 阈值签名预言机
如果对安全性要求极高,可以用多个节点一起生成随机数(比如Chainlink的阈值VRF),多个节点的秘密种子组合成最终结果,单个节点没法控制随机数,彻底杜绝了单点操控的风险。
四、总结
做链上应用选随机数方案,核心看你的场景价值:如果是高价值的NFT、抽奖、DeFi,必须用VRF,哪怕Gas费高一点;如果是低价值的小功能,用块哈希就够了,成本极低;如果用预言机的话,一定要注意“提前预测”的风险,要么用延迟揭示,要么直接换VRF。另外,所有用随机数的场景,一定要避开时间戳、当前块哈希这些可被操控的链上数据,避免像早期项目一样踩坑。
评论
围绕“链上随机数生成不可信问题深度分析:可验证随机函数与块哈希方案的适用场景及预言机随机数源被提前预测的规避方法”参与讨论