Q7: 在区块链上创建随机数有哪些挑战?
欢迎来到《Solidity面试修炼之道》专栏💎。
专栏核心理念:
核心 Slogan💸💸:从面试题到实战精通,你的 Web3 开发进阶指南。
一句话介绍🔬🔬: 150+ 道面试题 × 103 篇深度解析 = 你的 Solidity 修炼秘籍。
- ✅ 名称有深度和系统性
- ✅ "修炼"体现进阶过程
- ✅ 适合中文技术社区
- ✅ 记忆度高,易于传播
- ✅ 全场景适用
Q7: 在区块链上创建随机数有哪些挑战?
简答:
区块链是确定性和公开的,矿工/验证者可以预测或操纵随机数来源(如区块哈希、时间戳),使得生成真正的随机数非常困难。
详细分析:
在区块链上生成随机数面临两个根本性挑战:
-
确定性:区块链的核心特性是所有节点必须能够独立验证交易结果。这意味着给定相同的输入,智能合约必须产生相同的输出。真正的随机性与这一要求相矛盾。
-
公开性:区块链上的所有数据都是公开的,包括区块哈希、时间戳、交易数据等。攻击者可以在提交交易前看到这些值,从而预测"随机"结果。
常见的不安全随机数来源:
block.timestamp:矿工可以在一定范围内操纵block.difficulty/block.prevrandao:可预测或可操纵blockhash():只能访问最近 256 个区块,且可预测tx.origin或msg.sender:攻击者可控
安全的解决方案:
- Chainlink VRF:使用可验证随机函数,提供可证明的随机性
- Commit-Reveal 方案:两阶段提交,防止预测
- 多方计算:结合多个不可控来源
- 预言机:从链下获取随机数
代码示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;/*** @title RandomnessExamples* @notice 演示区块链上随机数的挑战和解决方案*/// ❌ 不安全的随机数生成方法
contract UnsafeRandomness {/*** @notice 使用 block.timestamp - 不安全!* @dev 矿工可以操纵时间戳(约 15 秒范围内)*/function unsafeRandomWithTimestamp() public view returns (uint256) {// ❌ 矿工可以选择有利的时间戳return uint256(keccak256(abi.encodePacked(block.timestamp)));}/*** @notice 使用 blockhash - 不安全!* @dev 可预测,且只能访问最近 256 个区块*/function unsafeRandomWithBlockhash() public view returns (uint256) {// ❌ 攻击者可以等待有利的区块哈希return uint256(blockhash(block.number - 1));}/*** @notice 使用 msg.sender - 不安全!* @dev 攻击者完全控制自己的地址*/function unsafeRandomWithSender() public view returns (uint256) {// ❌ 攻击者可以尝试多个地址直到获得有利结果return uint256(keccak256(abi.encodePacked(msg.sender)));}/*** @notice 组合多个来源 - 仍然不安全!* @dev 虽然更复杂,但仍可预测和操纵*/function unsafeRandomCombined() public view returns (uint256) {// ❌ 所有输入都是公开或可操纵的return uint256(keccak256(abi.encodePacked(block.timestamp,block.difficulty,msg.sender)));}/*** @notice 演示攻击场景:彩票*/function unsafeLottery() public payable returns (bool) {require(msg.value == 0.1 ether, "Must send 0.1 ETH");// ❌ 不安全的随机数uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp,msg.sender))) % 100;// 如果随机数 < 50,用户获胜if (random < 50) {payable(msg.sender).transfer(0.2 ether);return true;}return false;}
}/*** @title AttackUnsafeLottery* @notice 演示如何攻击不安全的随机数*/
contract AttackUnsafeLottery {UnsafeRandomness public target;constructor(address _target) {target = UnsafeRandomness(_target);}/*** @notice 攻击:只在能赢时才参与*/function attack() public payable {// 预测随机数uint256 predictedRandom = uint256(keccak256(abi.encodePacked(block.timestamp,address(this)))) % 100;// 只在能赢时才调用if (predictedRandom < 50) {target.unsafeLottery{value: 0.1 ether}();}}
}// ✅ 安全方案 1:Commit-Reveal 模式
contract CommitRevealLottery {struct Commitment {bytes32 commit;uint256 blockNumber;bool revealed;}mapping(address => Commitment) public commitments;uint256 public revealDeadline;/*** @notice 第一阶段:提交承诺* @param _commitment 承诺哈希 = keccak256(abi.encodePacked(secret, address))*/function commit(bytes32 _commitment) public payable {require(msg.value == 0.1 ether, "Must send 0.1 ETH");require(commitments[msg.sender].commit == bytes32(0), "Already committed");commitments[msg.sender] = Commitment({commit: _commitment,blockNumber: block.number,revealed: false});}/*** @notice 第二阶段:揭示秘密* @param _secret 原始秘密*/function reveal(uint256 _secret) public {Commitment storage c = commitments[msg.sender];require(c.commit != bytes32(0), "No commitment");require(!c.revealed, "Already revealed");require(block.number > c.blockNumber + 1, "Too early");// 验证承诺bytes32 hash = keccak256(abi.encodePacked(_secret, msg.sender));require(hash == c.commit, "Invalid secret");c.revealed = true;// 使用秘密和未来区块哈希生成随机数uint256 random = uint256(keccak256(abi.encodePacked(_secret,blockhash(c.blockNumber + 1)))) % 100;if (random < 50) {payable(msg.sender).transfer(0.2 ether);}}
}// ✅ 安全方案 2:Chainlink VRF 接口示例
interface VRFCoordinatorV2Interface {function requestRandomWords(bytes32 keyHash,uint64 subId,uint16 minimumRequestConfirmations,uint32 callbackGasLimit,uint32 numWords) external returns (uint256 requestId);
}contract ChainlinkVRFLottery {VRFCoordinatorV2Interface public vrfCoordinator;bytes32 public keyHash;uint64 public subscriptionId;mapping(uint256 => address) public requestIdToPlayer;constructor(address _vrfCoordinator,bytes32 _keyHash,uint64 _subscriptionId) {vrfCoordinator = VRFCoordinatorV2Interface(_vrfCoordinator);keyHash = _keyHash;subscriptionId = _subscriptionId;}/*** @notice 参与彩票:请求随机数*/function enterLottery() public payable returns (uint256) {require(msg.value == 0.1 ether, "Must send 0.1 ETH");// 请求随机数uint256 requestId = vrfCoordinator.requestRandomWords(keyHash,subscriptionId,3, // 确认数100000, // callback gas limit1 // 随机数数量);requestIdToPlayer[requestId] = msg.sender;return requestId;}/*** @notice Chainlink VRF 回调函数* @dev 由 VRF Coordinator 调用*/function fulfillRandomWords(uint256 requestId,uint256[] memory randomWords) internal {address player = requestIdToPlayer[requestId];uint256 random = randomWords[0] % 100;if (random < 50) {payable(player).transfer(0.2 ether);}}
}
理论补充:
随机数攻击的类型:
- 预测攻击:攻击者在交易前计算随机数,只在有利时才提交交易
- 操纵攻击:矿工选择性地包含或排除交易,或操纵区块参数
- 重放攻击:在多个区块中尝试相同的操作,直到获得有利结果
Commit-Reveal 方案的工作原理:
- Commit 阶段:用户提交
hash(secret + address),不透露秘密 - 等待期:等待至少一个区块,确保区块哈希不可预测
- Reveal 阶段:用户揭示秘密,合约验证并使用
secret + blockhash生成随机数
Chainlink VRF 的优势:
- 使用密码学证明保证随机性
- 链下生成,链上验证
- 防止预测和操纵
- 可验证的公平性
实际应用建议:
- 低价值场景:可以使用 commit-reveal
- 高价值场景:必须使用 Chainlink VRF 或类似方案
- 游戏:考虑使用链下随机数 + 零知识证明
- 抽奖:使用多方随机数贡献
相关问题:
- Q9: 荷兰式拍卖和英式拍卖有什么区别?
- Q37: 什么是提交-揭示方案,什么时候会使用它?

