乐闻世界logo
搜索文章和话题

Solidity 中如何处理随机数生成?有哪些安全方案?

3月6日 23:05

在 Solidity 中生成随机数是一个复杂的问题,因为区块链是确定性的环境,无法直接生成真正随机的数。以下是各种随机数生成方案及其安全性分析。

1. 不安全的随机数生成方式

使用区块哈希(不安全)

solidity
contract InsecureRandom { // 危险:矿工可以操纵区块哈希 function getRandomNumber() public view returns (uint256) { return uint256(keccak256(abi.encodePacked(blockhash(block.number - 1)))); } // 危险:矿工可以操纵时间戳 function getRandomFromTimestamp() public view returns (uint256) { return uint256(keccak256(abi.encodePacked(block.timestamp))); } // 危险:所有参数都可被矿工操纵 function getRandomInsecure() public view returns (uint256) { return uint256(keccak256(abi.encodePacked( block.timestamp, block.difficulty, msg.sender ))); } }

为什么这些方式不安全?

  • 矿工可以操纵 blockhashblock.timestampblock.difficulty
  • 攻击者可以预测结果并选择性提交交易
  • 所有链上数据都是公开可见的

2. Commit-Reveal 方案

这是一种两阶段提交方案,可以防止前置交易攻击。

solidity
contract CommitReveal { struct Commit { bytes32 commitHash; uint256 revealDeadline; bool revealed; } mapping(address => Commit) public commits; // 第一阶段:提交哈希 function commit(bytes32 _commitHash) external { require(commits[msg.sender].commitHash == bytes32(0), "Already committed"); commits[msg.sender] = Commit({ commitHash: _commitHash, revealDeadline: block.number + 10, revealed: false }); } // 第二阶段:揭示原始值 function reveal(uint256 _secret, uint256 _guess) external { Commit storage userCommit = commits[msg.sender]; require(userCommit.commitHash != bytes32(0), "No commit found"); require(!userCommit.revealed, "Already revealed"); require(block.number <= userCommit.revealDeadline, "Reveal period ended"); // 验证哈希 bytes32 revealHash = keccak256(abi.encodePacked(_secret, _guess)); require(revealHash == userCommit.commitHash, "Invalid reveal"); userCommit.revealed = true; // 生成随机数 uint256 randomNumber = uint256(keccak256(abi.encodePacked( _secret, blockhash(userCommit.revealDeadline) ))); // 使用随机数... } // 计算提交哈希的辅助函数 function getCommitHash(uint256 _secret, uint256 _guess) external pure returns (bytes32) { return keccak256(abi.encodePacked(_secret, _guess)); } }

Chainlink VRF 是目前最安全的链上随机数解决方案。

solidity
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; contract RandomNumberConsumer is VRFConsumerBaseV2 { VRFCoordinatorV2Interface COORDINATOR; // 订阅 ID uint64 s_subscriptionId; // 使用的 Gas 通道 bytes32 keyHash; // 回调 Gas 限制 uint32 callbackGasLimit = 100000; // 请求的确认数 uint16 requestConfirmations = 3; // 请求的随机数数量 uint32 numWords = 1; // 存储请求 ID 和对应的游戏数据 mapping(uint256 => address) public requestToGame; mapping(uint256 => uint256[]) public requestIdToRandomWords; event RandomWordsRequested(uint256 requestId, address game); event RandomWordsFulfilled(uint256 requestId, uint256[] randomWords); constructor( uint64 subscriptionId, address vrfCoordinator, bytes32 _keyHash ) VRFConsumerBaseV2(vrfCoordinator) { COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator); s_subscriptionId = subscriptionId; keyHash = _keyHash; } // 请求随机数 function requestRandomWords() external returns (uint256 requestId) { requestId = COORDINATOR.requestRandomWords( keyHash, s_subscriptionId, requestConfirmations, callbackGasLimit, numWords ); requestToGame[requestId] = msg.sender; emit RandomWordsRequested(requestId, msg.sender); } // Chainlink 回调函数 function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { requestIdToRandomWords[requestId] = randomWords; emit RandomWordsFulfilled(requestId, randomWords); // 使用随机数处理游戏逻辑 address game = requestToGame[requestId]; // ... } }

4. 使用预言机(Oracle)

通过预言机获取链外随机数。

solidity
interface IOracle { function requestRandomNumber() external returns (uint256 requestId); function getRandomNumber(uint256 requestId) external view returns (uint256); } contract OracleRandom { IOracle public oracle; mapping(uint256 => bool) public usedRandomNumbers; event RandomNumberRequested(uint256 requestId); event RandomNumberReceived(uint256 randomNumber); constructor(address _oracle) { oracle = IOracle(_oracle); } function requestRandom() external returns (uint256 requestId) { requestId = oracle.requestRandomNumber(); emit RandomNumberRequested(requestId); return requestId; } function useRandom(uint256 requestId) external returns (uint256) { uint256 randomNumber = oracle.getRandomNumber(requestId); require(!usedRandomNumbers[randomNumber], "Random number already used"); usedRandomNumbers[randomNumber] = true; emit RandomNumberReceived(randomNumber); return randomNumber; } }

5. 多方计算(MPC)方案

通过多个参与方共同生成随机数。

solidity
contract MPCRandom { struct Contribution { bytes32 hash; uint256 value; bool revealed; } mapping(address => Contribution) public contributions; address[] public participants; uint256 public revealDeadline; bool public randomGenerated; uint256 public randomNumber; event ContributionSubmitted(address participant); event RandomNumberGenerated(uint256 randomNumber); modifier onlyParticipant() { require(isParticipant(msg.sender), "Not a participant"); _; } function isParticipant(address _addr) public view returns (bool) { for (uint i = 0; i < participants.length; i++) { if (participants[i] == _addr) return true; } return false; } // 第一阶段:提交贡献的哈希 function submitHash(bytes32 _hash) external onlyParticipant { require(block.number < revealDeadline, "Submission period ended"); require(contributions[msg.sender].hash == bytes32(0), "Already submitted"); contributions[msg.sender].hash = _hash; emit ContributionSubmitted(msg.sender); } // 第二阶段:揭示贡献值 function reveal(uint256 _value, uint256 _nonce) external onlyParticipant { require(block.number >= revealDeadline, "Reveal not started"); require(!contributions[msg.sender].revealed, "Already revealed"); bytes32 expectedHash = keccak256(abi.encodePacked(_value, _nonce)); require(expectedHash == contributions[msg.sender].hash, "Invalid reveal"); contributions[msg.sender].value = _value; contributions[msg.sender].revealed = true; } // 生成随机数 function generateRandom() external { require(block.number > revealDeadline + 10, "Wait for all reveals"); require(!randomGenerated, "Random already generated"); uint256 combined = 0; for (uint i = 0; i < participants.length; i++) { if (contributions[participants[i]].revealed) { combined ^= contributions[participants[i]].value; } } randomNumber = uint256(keccak256(abi.encodePacked(combined, blockhash(block.number - 1)))); randomGenerated = true; emit RandomNumberGenerated(randomNumber); } }

6. 随机数使用场景

NFT 随机铸造

solidity
contract NFTRandomMint is VRFConsumerBaseV2 { // ... VRF 设置 ... struct MintRequest { address minter; uint256 tokenId; bool fulfilled; } mapping(uint256 => MintRequest) public mintRequests; uint256 public nextTokenId; function requestMint() external returns (uint256 requestId) { requestId = requestRandomWords(); mintRequests[requestId] = MintRequest({ minter: msg.sender, tokenId: nextTokenId++, fulfilled: false }); } function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { MintRequest storage request = mintRequests[requestId]; request.fulfilled = true; // 使用随机数确定 NFT 属性 uint256 randomness = randomWords[0]; uint256 rarity = randomness % 100; // 0-99 // 铸造 NFT _safeMint(request.minter, request.tokenId); // 设置属性 if (rarity < 5) { _setTokenRarity(request.tokenId, "Legendary"); } else if (rarity < 20) { _setTokenRarity(request.tokenId, "Epic"); } else if (rarity < 50) { _setTokenRarity(request.tokenId, "Rare"); } else { _setTokenRarity(request.tokenId, "Common"); } } }

游戏随机结果

solidity
contract DiceGame is VRFConsumerBaseV2 { struct Game { address player; uint256 bet; uint256 predictedNumber; bool fulfilled; uint256 result; } mapping(uint256 => Game) public games; event GameStarted(uint256 requestId, address player, uint256 bet); event GameFinished(uint256 requestId, uint256 result, bool won); function play(uint256 _predictedNumber) external payable returns (uint256 requestId) { require(msg.value >= 0.01 ether, "Minimum bet is 0.01 ETH"); require(_predictedNumber >= 1 && _predictedNumber <= 6, "Predict 1-6"); requestId = requestRandomWords(); games[requestId] = Game({ player: msg.sender, bet: msg.value, predictedNumber: _predictedNumber, fulfilled: false, result: 0 }); emit GameStarted(requestId, msg.sender, msg.value); } function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { Game storage game = games[requestId]; game.fulfilled = true; // 生成 1-6 的骰子结果 game.result = (randomWords[0] % 6) + 1; bool won = (game.result == game.predictedNumber); if (won) { // 支付奖金(6倍赔率) payable(game.player).transfer(game.bet * 6); } emit GameFinished(requestId, game.result, won); } }

7. 方案对比

方案安全性成本延迟适用场景
区块哈希免费即时测试环境
Commit-Reveal简单游戏
Chainlink VRF中等中等生产环境
预言机中等中等特定需求
MPC高价值场景

8. 最佳实践

  1. 生产环境使用 Chainlink VRF:最安全、最可靠的解决方案
  2. 避免使用链上数据生成随机数:所有链上数据都可被操纵
  3. 实现重试机制:处理 VRF 回调失败的情况
  4. 限制随机数使用范围:避免重复使用同一随机数
  5. 添加时间锁:给用户提供退出时间
solidity
// 安全的随机数使用模式 contract SafeRandomUsage { uint256 public constant MAX_RANDOM = 10000; mapping(uint256 => bool) public usedRandoms; function useRandom(uint256 _random) internal returns (uint256) { uint256 normalized = _random % MAX_RANDOM; require(!usedRandoms[normalized], "Random already used"); usedRandoms[normalized] = true; return normalized; } }

标签:Solidity