Solidity里一个超级硬核的主题——安全的随机数生成!在区块链上搞随机数可不是闹着玩的,比如抽奖、游戏、NFT分发,随机数不安全,分分钟被黑客算计,钱包直接空!以太坊的区块链是确定性环境,生成真随机数得费点心思。这篇干货会用大白话把Solidity里安全的随机数生成技巧讲得明明白白,从基础的伪随
Solidity里一个超级硬核的主题——安全的随机数生成!在区块链上搞随机数可不是闹着玩的,比如抽奖、游戏、NFT分发,随机数不安全,分分钟被黑客算计,钱包直接空!以太坊的区块链是确定性环境,生成真随机数得费点心思。这篇干货会用大白话把Solidity里安全的随机数生成技巧讲得明明白白,从基础的伪随机到Chainlink VRF、预言机,再到多方计算,配合OpenZeppelin和Hardhat测试,带你一步步实现稳如老狗的随机数方案。每种方法都配代码和分析,重点是硬核知识点,废话少说,直接上技术细节,帮你把随机数整得又安全又靠谱!
先搞清楚几个关键点:
咱们用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;
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。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提供安全、去中心化的随机数。
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:请求的随机数数量。onlyOwner限制调用。subscriptionId。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);
});
});
用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];
}
}
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];
}
}
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
跑代码,体验Solidity随机数生成的硬核魔法吧!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!