Solidity随机数生成:打造区块链上真·安全的随机魔法

Solidity里一个超级硬核的主题——安全的随机数生成!在区块链上搞随机数可不是闹着玩的,比如抽奖、游戏、NFT分发,随机数不安全,分分钟被黑客算计,钱包直接空!以太坊的区块链是确定性环境,生成真随机数得费点心思。这篇干货会用大白话把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.timestampblock.difficultymsg.sendernonce生成伪随机数。
    • keccak256生成256位哈希,转换为uint256
    • pickWinner从玩家数组中选随机赢家。
  • 问题
    • 可预测性:矿工可操控block.timestampblock.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测试网)。

测试

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随机数生成的硬核魔法吧!

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
天涯学馆
天涯学馆
0x9d6d...50d5
资深大厂程序员,12年开发经验,致力于探索前沿技术!