Solidity随机数生成:打造区块链上真·安全的随机魔法
Solidity里一个超级硬核的主题——安全的随机数生成!在区块链上搞随机数可不是闹着玩的,比如抽奖、游戏、NFT分发,随机数不安全,分分钟被黑客算计,钱包直接空!以太坊的区块链是确定性环境,生成真随机数得费点心思。这篇干货会用大白话把Solidity里安全的随机数生成技巧讲得明明白白,从基础的伪随机到Chainlink VRF、预言机,再到多方计算,配合OpenZeppelin和Hardhat测试,带你一步步实现稳如老狗的随机数方案。每种方法都配代码和分析,重点是硬核知识点,废话少说,直接上技术细节,帮你把随机数整得又安全又靠谱!
随机数生成的核心概念
先搞清楚几个关键点:
- 区块链的确定性:以太坊是确定性环境,所有节点必须对同一输入产生相同输出,随机数不能依赖本地熵。
- 伪随机数:用链上数据(如块高、时间戳、msg.sender)生成,看似随机但可被矿工操控。
- 真随机数:需要外部可信随机源(如Chainlink VRF)或多方计算。
- 安全风险:
- 可预测性:用块高或时间戳,矿工可操控结果。
- 重放攻击:随机数生成逻辑暴露,攻击者可重复调用。
- 链下泄露:链下生成随机数,未加密传输可能被拦截。
- 解决方案:
- Chainlink VRF:提供可验证随机数,安全且去中心化。
- 预言机:引入链下随机源,需信任预言机。
- 多方计算:多方生成随机种子,降低单点风险。
- 工具:
- Solidity 0.8.x:自带溢出/下溢检查,安全可靠。
- OpenZeppelin:提供安全的数学库和访问控制。
- Hardhat:测试和调试随机数逻辑。
- Chainlink:VRF和预言机服务。
咱们用Solidity 0.8.20,结合Chainlink、OpenZeppelin和Hardhat,逐步实现安全的随机数生成方案。
环境准备
用Hardhat搭建开发环境,写和测试合约。
mkdir random-number-demo
cd random-number-demo
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts @chainlink/contracts
npm install ethers
初始化Hardhat:
npx hardhat init
选择TypeScript项目,安装依赖:
npm install --save-dev ts-node typescript @types/node @types/mocha
目录结构:
random-number-demo/
├── contracts/
│ ├── PseudoRandom.sol
│ ├── ChainlinkVRF.sol
│ ├── OracleRandom.sol
│ ├── MultiPartyRandom.sol
├── scripts/
│ ├── deploy.ts
├── test/
│ ├── RandomNumber.test.ts
├── hardhat.config.ts
├── tsconfig.json
├── package.json
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./"
},
"include": ["hardhat.config.ts", "scripts", "test"]
}
hardhat.config.ts:
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
hardhat: {
chainId: 1337,
},
sepolia: {
url: "https://sepolia.infura.io/v3/YOUR_INFURA_KEY",
accounts: ["YOUR_PRIVATE_KEY"]
}
}
};
export default config;
- Chainlink:测试需要Sepolia测试网,申请Infura API和LINK代币。
- 跑本地节点:
npx hardhat node
伪随机数(不安全)
先看伪随机数,简单但有风险。
合约代码
contracts/PseudoRandom.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
contract PseudoRandom is Ownable {
uint256 public nonce;
constructor() Ownable() {
nonce = 0;
}
function getRandomNumber() public returns (uint256) {
nonce++;
return uint256(keccak256(abi.encodePacked(block.timestamp, block.difficulty, msg.sender, nonce)));
}
function pickWinner(address[] memory players) public onlyOwner returns (address) {
uint256 random = getRandomNumber();
return players[random % players.length];
}
}
解析
- 逻辑:
- 用
block.timestamp、block.difficulty、msg.sender和nonce生成伪随机数。 keccak256生成256位哈希,转换为uint256。pickWinner从玩家数组中选随机赢家。
- 用
- 问题:
- 可预测性:矿工可操控
block.timestamp和block.difficulty。 - 重放攻击:同一块内,攻击者可重复调用预测结果。
- nonce:增加不可预测性,但仍不足。
- 可预测性:矿工可操控
- 适用场景:低安全性需求,如简单游戏测试。
测试
test/RandomNumber.test.ts:
import { ethers } from "hardhat";
import { expect } from "chai";
import { PseudoRandom } from "../typechain-types";
describe("PseudoRandom", function () {
let random: PseudoRandom;
let owner: any, addr1: any;
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
const RandomFactory = await ethers.getContractFactory("PseudoRandom");
random = await RandomFactory.deploy();
await random.deployed();
});
it("should generate random number", async function () {
const randomNumber = await random.getRandomNumber();
expect(randomNumber).to.be.a("BigNumber");
});
it("should pick a winner", async function () {
const players = [owner.address, addr1.address];
const winner = await random.pickWinner(players);
expect([owner.address, addr1.address]).to.include(winner);
});
it("should restrict pickWinner to owner", async function () {
const players = [owner.address, addr1.address];
await expect(random.connect(addr1).pickWinner(players)).to.be.revertedWith("Ownable: caller is not the owner");
});
});
跑测试:
npx hardhat test
- 结果:生成随机数并选出赢家,但安全性低。
Chainlink VRF(可验证随机数)
Chainlink VRF提供安全、去中心化的随机数。
合约代码
contracts/ChainlinkVRF.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
contract ChainlinkVRF is Ownable, VRFConsumerBaseV2 {
VRFCoordinatorV2Interface COORDINATOR;
uint64 subscriptionId;
address vrfCoordinator = 0x7a1BaC17Ccc5b313516C5E16fb24f7659aA5ebed; // Sepolia
bytes32 keyHash = 0x4b09e658ed251bcafeebbc69400383d49f344ace09b9576fe248bb02c003fe9f;
uint32 callbackGasLimit = 100000;
uint16 requestConfirmations = 3;
uint32 numWords = 1;
mapping(uint256 => address) public requestToSender;
uint256[] public randomWords;
constructor(uint64 _subscriptionId) Ownable() VRFConsumerBaseV2(vrfCoordinator) {
COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
subscriptionId = _subscriptionId;
}
function requestRandomNumber() public onlyOwner returns (uint256 requestId) {
requestId = COORDINATOR.requestRandomWords(
keyHash,
subscriptionId,
requestConfirmations,
callbackGasLimit,
numWords
);
requestToSender[requestId] = msg.sender;
return requestId;
}
function fulfillRandomWords(uint256 requestId, uint256[] memory _randomWords) internal override {
randomWords = _randomWords;
}
function pickWinner(address[] memory players) public onlyOwner returns (address) {
require(randomWords.length > 0, "No random number available");
uint256 random = randomWords[0];
return players[random % players.length];
}
}
解析
- 逻辑:
- 继承
VRFConsumerBaseV2,连接Chainlink VRF。 requestRandomNumber:向VRF Coordinator请求随机数,需LINK代币。fulfillRandomWords:回调函数,接收随机数。pickWinner:用随机数选择赢家。
- 继承
- 参数:
vrfCoordinator:Sepolia测试网的VRF Coordinator地址。keyHash:Gas Lane,决定Gas价格。subscriptionId:Chainlink订阅ID,需在Chainlink官网创建。callbackGasLimit:回调函数Gas上限。requestConfirmations:确认块数,确保安全性。numWords:请求的随机数数量。
- 安全特性:
- VRF提供可验证随机数,防止篡改。
onlyOwner限制调用。
- 准备:
- 在Chainlink官网创建VRF订阅,获取
subscriptionId。 - 向订阅账户转入LINK代币(Sepolia测试网)。
- 在Chainlink官网创建VRF订阅,获取
测试
test/RandomNumber.test.ts(添加):
import { ethers } from "hardhat";
import { expect } from "chai";
import { ChainlinkVRF } from "../typechain-types";
describe("ChainlinkVRF", function () {
let random: ChainlinkVRF;
let owner: any, addr1: any;
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
const RandomFactory = await ethers.getContractFactory("ChainlinkVRF");
random = await RandomFactory.deploy(123); // Mock subscriptionId
await random.deployed();
});
it("should request random number", async function () {
await expect(random.requestRandomNumber()).to.emit(random, "RequestSent");
});
it("should pick winner after receiving random number", async function () {
// Mock VRF fulfillment
await random.requestRandomNumber();
// Simulate callback (local testing requires mock VRFCoordinator)
const players = [owner.address, addr1.address];
const winner = await random.pickWinner(players);
expect([owner.address, addr1.address]).to.include(winner);
});
});
- 解析:
- 本地测试需模拟VRF Coordinator(可用Chainlink的MockVRFCoordinator)。
- 部署到Sepolia测试网,验证真实VRF功能。
- 注意:确保订阅账户有足够LINK代币。
预言机随机数
用Chainlink预言机引入链下随机数。
contracts/OracleRandom.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract OracleRandom is Ownable {
AggregatorV3Interface internal oracle;
uint256 public randomNumber;
constructor(address _oracle) Ownable() {
oracle = AggregatorV3Interface(_oracle);
}
function getRandomNumber() public onlyOwner {
(, int256 answer,,,) = oracle.latestRoundData();
randomNumber = uint256(answer);
}
function pickWinner(address[] memory players) public onlyOwner returns (address) {
require(randomNumber > 0, "No random number");
return players[randomNumber % players.length];
}
}
解析
- 逻辑:
- 用Chainlink数据源(如价格Feed)模拟随机数。
getRandomNumber:从预言机获取数据(如价格)。pickWinner:用数据选择赢家。
- 问题:
- 数据源(如价格)并非真随机,可能被预测。
- 需信任预言机提供者。
- 适用场景:对随机性要求不高,需快速实现。
测试
test/RandomNumber.test.ts(添加):
import { OracleRandom } from "../typechain-types";
describe("OracleRandom", function () {
let random: OracleRandom;
let owner: any, addr1: any;
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
const RandomFactory = await ethers.getContractFactory("OracleRandom");
random = await RandomFactory.deploy("0x694AA1769357215DE4FAC081bf1f309aDC325306"); // Sepolia ETH/USD
await random.deployed();
});
it("should get random number from oracle", async function () {
await random.getRandomNumber();
expect(await random.randomNumber()).to.be.gt(0);
});
it("should pick winner", async function () {
await random.getRandomNumber();
const players = [owner.address, addr1.address];
const winner = await random.pickWinner(players);
expect([owner.address, addr1.address]).to.include(winner);
});
});
- 结果:从预言机获取数据,生成伪随机数,适合简单场景。
多方计算随机数
通过多方提交种子,生成随机数。
contracts/MultiPartyRandom.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
contract MultiPartyRandom is Ownable {
mapping(address => bytes32) public commitments;
mapping(address => uint256) public reveals;
address[] public participants;
uint256 public revealCount;
uint256 public randomNumber;
function commit(bytes32 commitment) public {
require(commitments[msg.sender] == bytes32(0), "Already committed");
commitments[msg.sender] = commitment;
participants.push(msg.sender);
}
function reveal(uint256 seed, bytes32 salt) public {
require(commitments[msg.sender] != bytes32(0), "No commitment");
require(keccak256(abi.encodePacked(seed, salt)) == commitments[msg.sender], "Invalid reveal");
reveals[msg.sender] = seed;
revealCount++;
}
function generateRandom() public onlyOwner {
require(revealCount == participants.length, "Not all revealed");
uint256 result = 0;
for (uint256 i = 0; i < participants.length; i++) {
result ^= reveals[participants[i]];
}
randomNumber = result;
}
function pickWinner(address[] memory players) public onlyOwner returns (address) {
require(randomNumber > 0, "No random number");
return players[randomNumber % players.length];
}
}
解析
- 逻辑:
- commit:参与者提交哈希(种子+盐)。
- reveal:揭示种子和盐,验证哈希。
- generateRandom:用所有种子异或生成随机数。
- pickWinner:用随机数选赢家。
- 安全特性:
- 提交-揭示机制防止提前泄露。
- 多方种子降低单点风险。
- 问题:
- 需足够参与者。
- 最后揭示者可能不提交,需超时机制。
测试
test/RandomNumber.test.ts(添加):
import { MultiPartyRandom } from "../typechain-types";
describe("MultiPartyRandom", function () {
let random: MultiPartyRandom;
let owner: any, addr1: any, addr2: any;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
const RandomFactory = await ethers.getContractFactory("MultiPartyRandom");
random = await RandomFactory.deploy();
await random.deployed();
});
it("should generate random number with multi-party", async function () {
const seed1 = 123;
const salt1 = ethers.utils.randomBytes(32);
const commitment1 = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["uint256", "bytes32"], [seed1, salt1]));
await random.connect(addr1).commit(commitment1);
const seed2 = 456;
const salt2 = ethers.utils.randomBytes(32);
const commitment2 = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["uint256", "bytes32"], [seed2, salt2]));
await random.connect(addr2).commit(commitment2);
await random.connect(addr1).reveal(seed1, salt1);
await random.connect(addr2).reveal(seed2, salt2);
await random.generateRandom();
expect(await random.randomNumber()).to.be.gt(0);
const players = [owner.address, addr1.address, addr2.address];
const winner = await random.pickWinner(players);
expect(players).to.include(winner);
});
});
- 结果:多方生成随机数,安全性依赖参与者数量。
部署脚本
scripts/deploy.ts:
import { ethers } from "hardhat";
async function main() {
const [owner] = await ethers.getSigners();
const PseudoRandomFactory = await ethers.getContractFactory("PseudoRandom");
const pseudoRandom = await PseudoRandomFactory.deploy();
await pseudoRandom.deployed();
console.log(`PseudoRandom deployed to: ${pseudoRandom.address}`);
const ChainlinkVRFFactory = await ethers.getContractFactory("ChainlinkVRF");
const chainlinkVRF = await ChainlinkVRFFactory.deploy(123); // Mock subscriptionId
await chainlinkVRF.deployed();
console.log(`ChainlinkVRF deployed to: ${chainlinkVRF.address}`);
const OracleRandomFactory = await ethers.getContractFactory("OracleRandom");
const oracleRandom = await OracleRandomFactory.deploy("0x694AA1769357215DE4FAC081bf1f309aDC325306");
await oracleRandom.deployed();
console.log(`OracleRandom deployed to: ${oracleRandom.address}`);
const MultiPartyRandomFactory = await ethers.getContractFactory("MultiPartyRandom");
const multiPartyRandom = await MultiPartyRandomFactory.deploy();
await multiPartyRandom.deployed();
console.log(`MultiPartyRandom deployed to: ${multiPartyRandom.address}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
跑部署:
npx hardhat run scripts/deploy.ts --network hardhat
对比分析
- 伪随机:
- Gas:~50k(生成随机数)。
- 安全性:低,易被矿工操控。
- 适用:测试或低安全场景。
- Chainlink VRF:
- Gas:~200k(请求+回调)。
- 安全性:高,可验证随机数。
- 适用:高安全场景,如NFT、抽奖。
- 预言机:
- Gas:~100k(获取数据)。
- 安全性:中等,依赖预言机。
- 适用:简单场景。
- 多方计算:
- Gas:~150k(多方提交+生成)。
- 安全性:较高,依赖参与者。
- 适用:去中心化场景。
跑代码,体验Solidity随机数生成的硬核魔法吧!
版权声明
本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。
区块链技术网


发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。