随机性是现代应用的核心需求。从NFT的随机属性分配、GameFi中的战斗结果、DeFi协议的抽奖机制,到DAO的随机抽样审计——几乎所有需要”公平”场景都离不开随机数。但在区块链这个确定性世界中,实现真正的随机性是一个公认的技术难题。
本文将深入分析为什么区块链不适合直接生成随机数,介绍主流的技术解决方案,并提供完整的代码实现示例。
区块链随机性的技术挑战
确定性与可验证性的矛盾
区块链的核心特性是确定性:给定相同的输入,节点必须产生相同的输出。这与随机数的本质需求——不可预测性——形成了根本矛盾。

以太坊这样的公有链还有额外的挑战:
- 透明性:所有链上数据对所有参与者可见,包括合约状态、交易内容
- 可验证性:任何计算结果都必须能被所有节点独立验证
- 抗审查性:恶意行为者无法通过阻止某些交易来影响结果
这些特性使得传统的随机数生成方式(如时间戳作为种子)在区块链上完全失效——攻击者可以通过控制交易顺序、甚至操纵区块提议者来影响结果。
“随机数”攻击类型
理解攻击向量是设计安全随机系统的前提。
区块操控攻击:矿工或验证者可以选择性打包交易、决定交易顺序、甚至推迟打包某个区块来操控使用区块哈希、时间戳作为随机源的结果。
提前运算攻击:一旦交易被广播,攻击者可以观察到交易内容(包括之前提交的承诺值),然后决定是否让交易上链。这种攻击在Fomo3D类游戏中造成过巨大损失——最后一个购买者需要”等待”一个区块时间,在高gas费诱惑下,矿工可以优先打包自己的交易。
前端运行攻击:游戏合约中的随机数一旦可以被预测,MEV机器人可以抢先执行交易套利。
Commit-Reveal 方案
方案原理
Commit-Reveal是最经典的链上随机数生成方案,分两阶段执行:
提交阶段(Commit):用户提交一个哈希值 H(secret, nonce),这个哈希锁定了一个随机数,但在揭示之前无法被反推。
揭示阶段(Reveal):用户提交实际的secret和nonce,合约验证哈希匹配后,使用这个值作为随机源。
这个方案的关键洞察是:用户无法在提交后再修改承诺值,因为哈希已经确定。
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract CommitRevealRandom {
// 提交记录:地址 -> 承诺哈希
mapping(address => bytes32) public commitments;
// 揭示记录:地址 -> 实际随机数
mapping(address => uint256) public reveals;
// 提交阶段的时间窗口
uint256 public commitDeadline;
uint256 public revealDeadline;
// 最终随机数
uint256 public finalRandom;
bool public randomGenerated;
event Committed(address indexed user, bytes32 commitment);
event Revealed(address indexed user, uint256 randomValue);
constructor(uint256 _commitDuration, uint256 _revealDuration) {
commitDeadline = block.timestamp + _commitDuration;
revealDeadline = commitDeadline + _revealDuration;
}
// 阶段1:提交承诺
function commit(bytes32 commitment) external {
require(block.timestamp < commitDeadline, "Commit phase ended");
require(commitments[msg.sender] == bytes32(0), "Already committed");
commitments[msg.sender] = commitment;
emit Committed(msg.sender, commitment);
}
// 阶段2:揭示随机数
function reveal(uint256 secret, uint256 nonce) external {
require(block.timestamp >= commitDeadline, "Commit phase not ended");
require(block.timestamp < revealDeadline, "Reveal phase ended");
require(commitments[msg.sender] != bytes32(0), "No commitment found");
// 验证承诺
bytes32 computedHash = keccak256(abi.encodePacked(secret, nonce));
require(computedHash == commitments[msg.sender], "Invalid commitment");
// 使用secret和nonce生成个人随机数
uint256 personalRandom = uint256(keccak256(abi.encodePacked(secret, nonce, block.timestamp)));
reveals[msg.sender] = personalRandom;
emit Revealed(msg.sender, personalRandom);
}
// 生成最终随机数:使用所有参与者揭示值的异或
function generateFinalRandom() external {
require(block.timestamp >= revealDeadline, "Reveal phase not ended");
require(!randomGenerated, "Random already generated");
uint256 seed = uint256(blockhash(block.number - 1));
// 对所有揭示值进行异或操作
address[] memory users = getAllUsers();
for (uint256 i = 0; i < users.length; i++) {
seed ^= reveals[users[i]];
}
finalRandom = seed;
randomGenerated = true;
}
// 获取所有提交过的用户(简化版本,实际需要存储用户列表)
function getAllUsers() internal view returns (address[] memory) {
// 实际实现中应该维护一个用户数组
return new address[](0);
}
// 使用最终随机数进行公平选择
function fairSelect(address[] memory candidates) external view returns (address) {
require(randomGenerated, "Random not generated");
require(candidates.length > 0, "No candidates");
return candidates[finalRandom % candidates.length];
}
}
Commit-Reveal的局限性
虽然Commit-Reveal方案简单有效,但存在明显的局限性:
- 两阶段交易:用户需要提交两次交易,增加了交互复杂度
- 最后一刻攻击:在揭示阶段,矿工仍然可以选择性地打包揭示交易
- 参与率问题:如果部分用户在揭示阶段不行动(忘记或故意),方案可能失败
- 串通攻击:多个参与者可以串通,在揭示阶段协调彼此的输入
增强版本:Commit-Reveal + 区块依赖
为了提高安全性,可以将Commit-Reveal与区块属性结合:
solidity
contract EnhancedCommitReveal {
struct Commitment {
bytes32 hash;
uint256 commitBlock;
bool revealed;
uint256 revealValue;
}
mapping(address => Commitment) public commitments;
uint256 public revealPhaseEnd;
uint256 public finalRandom;
constructor(uint256 _revealPhaseDuration) {
revealPhaseEnd = block.timestamp + _revealPhaseDuration;
}
function commit(bytes32 _commitment) external {
require(commitments[msg.sender].hash == bytes32(0), "Already committed");
commitments[msg.sender] = Commitment({
hash: _commitment,
commitBlock: block.number,
revealed: false,
revealValue: 0
});
}
function reveal(uint256 secret, uint256 nonce) external {
Commitment storage c = commitments[msg.sender];
require(c.hash != bytes32(0), "No commitment");
require(!c.revealed, "Already revealed");
// 验证承诺
require(
keccak256(abi.encodePacked(secret, nonce)) == c.hash,
"Invalid secret"
);
// 组合多个不可预测因素
c.revealValue = uint256(keccak256(abi.encodePacked(
secret,
nonce,
c.commitBlock,
block.number,
blockhash(c.commitBlock),
blockhash(block.number - 1)
)));
c.revealed = true;
}
function generateRandom() external {
require(block.timestamp >= revealPhaseEnd, "Phase not ended");
uint256 seed = 0;
address[] memory users = getCommittedUsers();
// 使用提交者的reveal值和区块哈希
for (uint256 i = 0; i < users.length; i++) {
Commitment storage c = commitments[users[i]];
if (c.revealed) {
seed ^= c.revealValue;
}
// 未揭示的用户,其commitBlock也有贡献
seed ^= c.commitBlock * 31337;
}
// 加入额外的区块属性
seed ^= uint256(blockhash(block.number - 1));
seed ^= uint256(block.coinbase);
finalRandom = uint256(keccak256(abi.encodePacked(seed)));
}
function getCommittedUsers() internal view returns (address[] memory) {
// 实际实现需要维护用户列表
return new address[](0);
}
}
Chainlink VRF 方案
VRF的核心原理
Chainlink Verifiable Random Function (VRF) 提供了链上可验证的随机数生成服务,是目前最广泛使用的去中心化随机数解决方案。
VRF的工作原理基于密码学:
- Chainlink节点使用私钥对输入数据(包括用户提供的种子)进行签名
- 合约通过公钥验证签名的有效性
- 签名结果经过哈希处理,转换为可用的随机数
由于节点的私钥不可见,任何人都无法预测VRF输出。合约可以在链上验证VRF证明的有效性,确保随机数的真实性。
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
contract VRFRandomConsumer is VRFConsumerBaseV2 {
VRFCoordinatorV2Interface public immutable vrfCoordinator;
// Chainlink VRF配置
uint64 public immutable subscriptionId;
bytes32 public immutable keyHash;
uint32 public constant callbackGasLimit = 100000;
uint16 public constant requestConfirmations = 3;
uint32 public constant numWords = 2;
// 请求记录
mapping(uint256 => address) public requestToSender;
mapping(uint256 => uint256) public requestToSeed;
// 随机数结果
uint256[] public randomResults;
// VRF请求事件
event RandomWordsRequested(uint256 requestId, address requester);
event RandomWordsReceived(uint256 requestId, uint256[] randomWords);
constructor(
address _vrfCoordinator,
uint64 _subscriptionId,
bytes32 _keyHash
) VRFConsumerBaseV2(_vrfCoordinator) {
vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator);
subscriptionId = _subscriptionId;
keyHash = _keyHash;
}
// 请求随机数
function requestRandomWords() external returns (uint256 requestId) {
// 使用区块属性和用户地址作为种子,增加熵
uint256 seed = uint256(
keccak256(abi.encode(
block.timestamp,
block.difficulty,
msg.sender,
gasleft()
))
);
// 将发送者与请求ID关联,便于后续处理
requestToSender[requestId] = msg.sender;
requestToSeed[requestId] = seed;
requestId = vrfCoordinator.requestRandomWords(
keyHash,
subscriptionId,
requestConfirmations,
callbackGasLimit,
numWords
);
emit RandomWordsRequested(requestId, msg.sender);
}
// Chainlink VRF回调函数
function fulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
) internal override {
require(requestToSender[requestId] == msg.sender, "Wrong request");
// 保存随机数结果
for (uint256 i = 0; i < randomWords.length; i++) {
randomResults.push(randomWords[i]);
}
emit RandomWordsReceived(requestId, randomWords);
}
// 使用随机数进行公平选择
function fairSelectWithVRF(
uint256 requestId,
address[] memory candidates
) external returns (address) {
require(candidates.length > 0, "Empty candidates");
uint256 randomValue = randomResults[requestId % randomResults.length];
return candidates[randomValue % candidates.length];
}
}
订阅者ID与资金管理
在实际部署中,你需要:
- 在 Chainlink VRF Portal 上创建订阅者ID
- 为订阅者充值LINK代币(用于支付Oracle费用)
- 将消费者合约添加到订阅者
solidity
// 订阅管理器:允许合约加入VRF订阅
contract VRFSubscriptionManager {
VRFCoordinatorV2Interface public immutable vrfCoordinator;
uint64 public immutable subscriptionId;
// 授权的消费者合约列表
mapping(address => bool) public authorizedConsumers;
event ConsumerAuthorized(address consumer);
event ConsumerUnauthorized(address consumer);
constructor(address _vrfCoordinator, uint64 _subscriptionId) {
vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator);
subscriptionId = _subscriptionId;
}
// 添加消费者合约
function addConsumer(address consumer) external {
authorizedConsumers[consumer] = true;
vrfCoordinator.addConsumer(subscriptionId, consumer);
emit ConsumerAuthorized(consumer);
}
// 移除消费者合约
function removeConsumer(address consumer) external {
authorizedConsumers[consumer] = false;
vrfCoordinator.removeConsumer(subscriptionId, consumer);
emit ConsumerUnauthorized(consumer);
}
}
链下签名方案
BLS 签名聚合
一种更高级的方案是使用链下签名聚合。多个签名者对同一消息签名,其签名可以聚合为一个签名。聚合签名的哈希值可以作为高质量的随机数。
这种方案的优势:
- 完全链下计算,Gas效率高
- 可实现阈值的去中心化(需要N-of-M个签名者参与)
- 签名过程实时,延迟低
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/**
* @title BLS Signature Based Randomness
* @notice 使用BLS签名聚合生成链上随机数
*/
contract BLSSignatureRandom {
// 签名者集合
address[] public signers;
mapping(address => bool) public isSigner;
// 每个轮次的签名收集
struct SigningRound {
bytes32 message;
uint256 threshold;
mapping(address => bytes) signatures;
mapping(address => bool) hasSigned;
uint256 signersCount;
bytes32 finalRandom;
bool completed;
}
mapping(uint256 => SigningRound) public rounds;
uint256 public currentRound;
// 模拟BLS配对验证(实际使用需要预编译或库)
function verifySignature(
bytes memory publicKey,
bytes memory message,
bytes memory signature
) internal pure returns (bool) {
// 实际实现中应使用BLS12-381配对函数
// 这里使用简化的占位实现
return signature.length == 96;
}
// 发起新的签名轮次
function startNewRound(bytes32 message) external {
require(isSigner[msg.sender], "Not a signer");
currentRound++;
SigningRound storage round = rounds[currentRound];
round.message = message;
round.threshold = (signers.length * 2) / 3 + 1; // 2/3阈值
round.completed = false;
// 签名者自动参与第一轮
submitSignature(round.message);
}
// 提交签名
function submitSignature(bytes32 message) public {
require(isSigner[msg.sender], "Not a signer");
SigningRound storage round = rounds[currentRound];
require(!round.hasSigned[msg.sender], "Already signed");
// 模拟签名过程(实际需要链下签名)
bytes memory signature = abi.encodePacked(
keccak256(abi.encodePacked(message, msg.sender, block.timestamp))
);
round.signatures[msg.sender] = signature;
round.hasSigned[msg.sender] = true;
round.signersCount++;
// 检查是否达到阈值
if (round.signersCount >= round.threshold) {
finalizeRound();
}
}
// 完成轮次,生成最终随机数
function finalizeRound() internal {
SigningRound storage round = rounds[currentRound];
require(!round.completed, "Already completed");
// 聚合所有签名
bytes32 aggregatedSignature = bytes32(0);
for (uint256 i = 0; i < signers.length; i++) {
address signer = signers[i];
if (round.hasSigned[signer]) {
aggregatedSignature ^= bytes32(round.signatures[signer]);
}
}
// 生成最终随机数
round.finalRandom = uint256(keccak256(abi.encodePacked(
round.message,
aggregatedSignature,
blockhash(block.number - 1)
)));
round.completed = true;
}
// 获取当前轮次的随机数
function getCurrentRandom() external view returns (uint256) {
return rounds[currentRound].finalRandom;
}
// 管理签名者
function addSigner(address signer) external {
require(!isSigner[signer], "Already a signer");
signers.push(signer);
isSigner[signer] = true;
}
function removeSigner(address signer) external {
require(isSigner[signer], "Not a signer");
isSigner[signer] = false;
}
}
RANDAO + VDF 方案
RANDAO的原理
RANDAO是一种简单的链上随机数协议:多个参与者各自提交随机数,所有随机数被汇总(通常通过异或运算)生成最终随机数。
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract RandaoRandom {
// 参与者的承诺
mapping(address => bytes32) public commitments;
// 揭示阶段
mapping(address => uint256) public reveals;
// RANDAO参数
uint256 public constant REVEAL_PERIOD = 50; // 区块数
uint256 public commitmentCount;
uint256 public revealCount;
uint256 public finalRandom;
bool public randomLocked;
uint256 public commitEndBlock;
uint256 public revealEndBlock;
event CommitmentMade(address indexed participant, bytes32 commitment);
event RevealMade(address indexed participant, uint256 revealedValue);
event RandomGenerated(uint256 finalValue);
constructor() {
commitEndBlock = block.number + REVEAL_PERIOD;
revealEndBlock = commitEndBlock + REVEAL_PERIOD;
}
// 阶段1:提交承诺
function commit(bytes32 commitment) external {
require(block.number < commitEndBlock, "Commit period ended");
require(commitments[msg.sender] == bytes32(0), "Already committed");
commitments[msg.sender] = commitment;
commitmentCount++;
emit CommitmentMade(msg.sender, commitment);
}
// 阶段2:揭示
function reveal(uint256 secret) external {
require(block.number >= commitEndBlock, "Commit period not ended");
require(block.number < revealEndBlock, "Reveal period ended");
require(commitments[msg.sender] != bytes32(0), "No commitment");
require(reveals[msg.sender] == 0, "Already revealed");
// 验证承诺
bytes32 computedHash = keccak256(abi.encodePacked(secret, msg.sender));
require(computedHash == commitments[msg.sender], "Invalid secret");
reveals[msg.sender] = secret;
revealCount++;
emit RevealMade(msg.sender, secret);
}
// 生成最终随机数
function finalize() external {
require(block.number >= revealEndBlock, "Reveal period not ended");
require(!randomLocked, "Already finalized");
uint256 seed = uint256(blockhash(block.number - 1));
// 异或所有揭示值
address[] memory participants = getParticipants();
for (uint256 i = 0; i < participants.length; i++) {
if (reveals[participants[i]] != 0) {
seed ^= reveals[participants[i]];
}
}
finalRandom = uint256(keccak256(abi.encodePacked(seed)));
randomLocked = true;
emit RandomGenerated(finalRandom);
}
// 获取所有参与者
function getParticipants() internal view returns (address[] memory) {
// 实际实现需要维护参与者列表
return new address[](0);
}
}
RANDAO的局限性
- 最后揭示者攻击:最后一个揭示者可以看到所有其他值,选择性揭示以影响结果
- 不揭示惩罚:参与者可以不揭示,破坏随机数生成
RANDAO + VDF 增强
VDF(Verifiable Delay Function)可以解决最后揭示者攻击。VDF需要一定的计算时间才能得出结果,这个时间窗口使得任何人(包括最后一个揭示者)都无法在结果出来后反向操控。
实用场景:NFT随机属性分配
让我们通过一个实际案例来展示随机数的应用——NFT的属性随机分配:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
contract RandomNFTAttributes is VRFConsumerBaseV2 {
// 属性定义
enum Rarity { Common, Uncommon, Rare, Epic, Legendary }
struct AttributeSet {
Rarity rarity;
uint256 power;
uint256 speed;
uint256 intelligence;
}
// NFT属性
mapping(uint256 => AttributeSet) public tokenAttributes;
// VRF配置
uint64 public subscriptionId;
bytes32 public keyHash;
// 请求映射
mapping(uint256 => uint256) public requestToTokenId;
mapping(uint256 => uint256) public requestToVRFRequestId;
// Rarity概率(百分比 x 100)
uint256[] public rarityChances = [
5000, // Common: 50%
3000, // Uncommon: 30%
1500, // Rare: 15%
400, // Epic: 4%
100 // Legendary: 1%
];
constructor(
address vrfCoordinator,
uint64 _subscriptionId,
bytes32 _keyHash
) VRFConsumerBaseV2(vrfCoordinator) {
subscriptionId = _subscriptionId;
keyHash = _keyHash;
}
function mintWithRandomAttributes(address to) external returns (uint256 tokenId) {
tokenId = totalSupply++; // 简化实现
// 请求VRF随机数
uint256 vrfRequestId = vrfCoordinator.requestRandomWords(
keyHash,
subscriptionId,
3, // requestConfirmations
100000, // callbackGasLimit
1 // numWords
);
requestToTokenId[vrfRequestId] = tokenId;
return tokenId;
}
function fulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
) internal override {
uint256 tokenId = requestToTokenId[requestId];
uint256 random = randomWords[0];
// 根据随机数决定稀有度
Rarity rarity = determineRarity(random);
// 根据稀有度决定属性范围
tokenAttributes[tokenId] = AttributeSet({
rarity: rarity,
power: generateAttribute(random, getMinPower(rarity), getMaxPower(rarity)),
speed: generateAttribute(random + 1, getMinSpeed(rarity), getMaxSpeed(rarity)),
intelligence: generateAttribute(random + 2, getMinInt(rarity), getMaxInt(rarity))
});
}
function determineRarity(uint256 random) internal view returns (Rarity) {
uint256 roll = random % 10000;
uint256 cumulative = 0;
for (uint256 i = 0; i < rarityChances.length; i++) {
cumulative += rarityChances[i];
if (roll < cumulative) {
return Rarity(i);
}
}
return Rarity.Common;
}
function generateAttribute(
uint256 seed,
uint256 min,
uint256 max
) internal pure returns (uint256) {
return min + (seed % (max - min + 1));
}
// 稀有度属性范围
function getMinPower(Rarity r) internal pure returns (uint256) {
if (r == Rarity.Common) return 10;
if (r == Rarity.Uncommon) return 30;
if (r == Rarity.Rare) return 50;
if (r == Rarity.Epic) return 70;
return 90;
}
function getMaxPower(Rarity r) internal pure returns (uint256) {
if (r == Rarity.Common) return 29;
if (r == Rarity.Uncommon) return 49;
if (r == Rarity.Rare) return 69;
if (r == Rarity.Epic) return 89;
return 100;
}
// 类似方法用于 speed 和 intelligence
function getMinSpeed(Rarity r) internal pure returns (uint256) {
if (r == Rarity.Common) return 5;
if (r == Rarity.Uncommon) return 20;
if (r == Rarity.Rare) return 40;
if (r == Rarity.Epic) return 60;
return 80;
}
function getMaxSpeed(Rarity r) internal pure returns (uint256) {
if (r == Rarity.Common) return 19;
if (r == Rarity.Uncommon) return 39;
if (r == Rarity.Rare) return 59;
if (r == Rarity.Epic) return 79;
return 100;
}
function getMinInt(Rarity r) internal pure returns (uint256) {
if (r == Rarity.Common) return 1;
if (r == Rarity.Uncommon) return 15;
if (r == Rarity.Rare) return 35;
if (r == Rarity.Epic) return 55;
return 75;
}
function getMaxInt(Rarity r) internal pure returns (uint256) {
if (r == Rarity.Common) return 14;
if (r == Rarity.Uncommon) return 34;
if (r == Rarity.Rare) return 54;
if (r == Rarity.Epic) return 74;
return 100;
}
}
方案选型指南
| 方案 | 安全性 | 成本 | 去中心化程度 | 适用场景 |
|---|---|---|---|---|
| Block Hash | 低 | 极低 | 依赖区块提议者 | 实验/内部工具 |
| Commit-Reveal | 中 | 低 | 无需信任 | 小规模应用 |
| Chainlink VRF | 高 | 中 | 完全去中心化 | 生产环境首选 |
| BLS聚合签名 | 高 | 低 | 取决于签名者 | 高频随机需求 |
| RANDAO | 中高 | 低 | 无需信任 | 协议内生随机 |
总结
区块链随机数生成是一个需要根据具体场景权衡的问题。对于大多数生产级应用,Chainlink VRF是当前最可靠的选择,兼顾了安全性、去中心化和开发便利性。
如果你的项目有特殊需求:
- 成本敏感:Commit-Reveal或RANDAO是更低Gas的替代方案
- 极高频率:考虑链下BLS签名聚合方案
- 组合使用:常见做法是VRF作为主随机源,同时在最终计算中加入区块哈希等链上因素作为额外熵
无论如何,请记住一个核心原则:永远不要单独使用区块哈希或时间戳作为唯一随机源。
相关阅读:

发表回复