链锁式 VRFs
简介
在处理计算机时,由于计算机的确定性,随机性是一个重要但难以处理的问题。在谈到区块链时更是如此,因为计算机不仅是确定性的,而且是透明的。因此,可信的随机数不能在 Solidity 中原生生成,因为随机性将在链上计算,这对所有矿工和用户来说是公共信息。
所以我们可以使用一些 web2 技术来生成随机性,然后在链上使用它们。
什么是 oracle?
- oracle 从外部世界向区块链的智能合约发送数据,反之亦然。
 - 然后,智能合约可以使用这些数据来做出决定并改变其状态。
 - 它们充当了区块链和外部世界之间的桥梁。
 - 但需要注意的是,区块链 oracle 本身不是数据源,它的工作是查询、验证和认证外部数据,然后再将其传递给智能合约。
 
今天我们将学习其中 一个名为 Chainlink VRF 的 oracle。
介绍
- Chainlink VRF 是用于生成随机值的 oracle。
 - 这些值通过密码学证明进行验证。
 - 这些证明证明了结果没有被 oracle 操作员、用户、矿工等篡改或操纵。
 - 证明被公布在链上,以便它们可以被验证。
 - 在验证成功后,它们会被要求随机性的智能合约所使用。
 
官方的 Chainlink 文档将 VRF 描述为
Chainlink VRF(可验证的随机函数)是为智能合约设计的一个可证明的公平和可验证的随机性来源。智能合约开发者可以使用 Chainlink VRF 作为防篡改的随机数发生器(RNG),为任何依赖不可预测结果的应用构建可靠的智能合约。
它是如何工作的?
- 
Chainlink 有两个合约,我们主要关注的是 VRFConsumerBase.sol 和 VRFCoordinator。
 - 
VRFConsumerBase 是调用 VRF 协调员的合约,它最终负责发布随机性。
 - 
我们将继承 VRFConsumerBase,并将使用其中的两个函数。
- requestRandomness,它对随机性提出初始请求。
 - fulfillRandomness,这是一个接收并对经过验证的随机性进行处理的函数。
 
 - 
如果你看一下这个图,你就可以理解这个流程,RandomGameWinner 合约将继承 VRFConsumerBase 合约,并将在 VRFConsumerBase 中调用 requestRandomness 函数。
 - 
在调用该函数时,对随机性的请求开始了,VRFConsumerBase 进一步调用 VRFCoordinator 合约,该合约负责从外部世界获取随机性的回报。
 - 
在 VRFCoordinator 获得随机性后,它调用 VRFConsumerBase 中的 fullFillRandomness 函数,然后进一步选择获胜者。
 - 
注意重要的部分是,尽管你调用了 requireRandomness 函数,但你在 fullFillRandomness 函数中获得了随机性。
 
前提条件
- 你已经完成了 Etherscan 的验证级别
 - 你已经完成了第 2 层教程
 
要求
- 我们今天将建立一个抽奖游戏
 - 每个游戏都会有一个最大的玩家人数和一个参赛费。
 - 在最大数量的玩家进入游戏后,将随机选出一名获胜者
 - 获胜者将获得 maxplayers*entryfee 数额的乙醚,以赢得游戏。
 
构建
首先,在你的电脑中创建一个名为 RandomWinnerGame 的文件夹。
为了构建智能合约,我们将使用 Hardhat。Hardhat 是一个 Ethereum 开发环境和框架,为 Solidity 的全栈开发而设计。简单地说,你可以编写你的智能合约,部署它们,运行测试,并调试你的代码。
初始化
要设置一个 Hardhat 项目,请打开终端并在 RandomWinnerGame 文件夹内执行这些命令
mkdir hardhat-tutorial
cd hardhat-tutorial
npm init --yes
npm install --save-dev hardhat
npx hardhat
在同一个终端,现在安装@openzeppelin/contracts,因为我们将导入 Openzeppelin 的 Contracts
npm install @openzeppelin/contracts
最后,我们将安装 Chainlink 合约,以使用 Chainlink VRF
npm install -save @chainlink/contracts
在合约目录下创建文件 RandomWinnerGame.sol,并粘贴以下代码。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
contract RandomWinnerGame is VRFConsumerBase, Ownable {
    //Chainlink variables
    // The amount of LINK to send with the request
    uint256 public fee;
    // ID of public key against which randomness is generated
    bytes32 public keyHash;
    // Address of the players
    address[] public players;
    //Max number of players in one game
    uint8 maxPlayers;
    // Variable to indicate if the game has started or not
    bool public gameStarted;
    // the fees for entering the game
    uint256 entryFee;
    // current game id
    uint256 public gameId;
    // emitted when the game starts
    event GameStarted(uint256 gameId, uint8 maxPlayers, uint256 entryFee);
    // emitted when someone joins a game
    event PlayerJoined(uint256 gameId, address player);
    // emitted when the game ends
    event GameEnded(uint256 gameId, address winner,bytes32 requestId);
   /**
   * constructor inherits a VRFConsumerBase and initiates the values for keyHash, fee and gameStarted
   * @param vrfCoordinator address of VRFCoordinator contract
   * @param linkToken address of LINK token contract
   * @param vrfFee the amount of LINK to send with the request
   * @param vrfKeyHash ID of public key against which randomness is generated
   */
    constructor(
        address vrfCoordinator, address linkToken,
        bytes32 vrfKeyHash, uint256 vrfFee
    )
    VRFConsumerBase(vrfCoordinator, linkToken) {
        keyHash = vrfKeyHash;
        fee = vrfFee;
        gameStarted = false;
    }
    /**
    * startGame starts the game by setting appropriate values for all the variables
    */
    function startGame(uint8 _maxPlayers, uint256 _entryFee) public onlyOwner {
        // Check if there is a game already running
        require(!gameStarted, "Game is currently running");
        // empty the players array
        delete players;
        // set the max players for this game
        maxPlayers = _maxPlayers;
        // set the game started to true
        gameStarted = true;
        // setup the entryFee for the game
        entryFee = _entryFee;
        gameId += 1;
        emit GameStarted(gameId, maxPlayers, entryFee);
    }
    /**
    joinGame is called when a player wants to enter the game
     */
    function joinGame() public payable {
        // Check if a game is already running
        require(gameStarted, "Game has not been started yet");
        // Check if the value sent by the user matches the entryFee
        require(msg.value == entryFee, "Value sent is not equal to entryFee");
        // Check if there is still some space left in the game to add another player
        require(players.length < maxPlayers, "Game is full");
        // add the sender to the players list
        players.push(msg.sender);
        emit PlayerJoined(gameId, msg.sender);
        // If the list is full start the winner selection process
        if(players.length == maxPlayers) {
            getRandomWinner();
        }
    }
    /**
    * fulfillRandomness is called by VRFCoordinator when it receives a valid VRF proof.
    * This function is overrided to act upon the random number generated by Chainlink VRF.
    * @param requestId  this ID is unique for the request we sent to the VRF Coordinator
    * @param randomness this is a random unit256 generated and returned to us by the VRF Coordinator
   */
    function fulfillRandomness(bytes32 requestId, uint256 randomness) internal virtual override  {
        // We want out winnerIndex to be in the length from 0 to players.length-1
        // For this we mod it with the player.length value
        uint256 winnerIndex = randomness % players.length;
        // get the address of the winner from the players array
        address winner = players[winnerIndex];
        // send the ether in the contract to the winner
        (bool sent,) = winner.call{value: address(this).balance}("");
        require(sent, "Failed to send Ether");
        // Emit that the game has ended
        emit GameEnded(gameId, winner,requestId);
        // set the gameStarted variable to false
        gameStarted = false;
    }
    /**
    * getRandomWinner is called to start the process of selecting a random winner
    */
    function getRandomWinner() private returns (bytes32 requestId) {
        // LINK is an internal interface for Link token found within the VRFConsumerBase
        // Here we use the balanceOF method from that interface to make sure that our
        // contract has enough link so that we can request the VRFCoordinator for randomness
        require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK");
        // Make a request to the VRF coordinator.
        // requestRandomness is a function within the VRFConsumerBase
        // it starts the process of randomness generation
        return requestRandomness(keyHash, fee);
    }
     // Function to receive Ether. msg.data must be empty
    receive() external payable {}
    // Fallback function is called when msg.data is not empty
    fallback() external payable {}
}
构造函数接收以下参数。
- vrfCoordinator,是 VRFCoordinator 合约的地址。
 - linkToken 是链接令牌的地址,它是链式链接获取其付款的令牌。
 - vrfFee 是发送随机性请求所需的链接令牌的金额
 - vrfKeyHash 是生成随机性所依据的公钥的 ID。这个值负责为我们的随机性请求生成一个唯一的 ID,称为 requestId。
 
(所有这些值都是由 Chainlink 提供给我们的。)
startGame 方法
startGame 方法是 onlyOwner,这意味着它只能由所有者调用。它用于启动游戏,在这个函数被调用后,玩家可以进入游戏,直到达到极限。它还会发出 GameStarted 事件。
joinGame 方 法
当用户想进入一个游戏时,这个函数将被调用。如果达到 maxPlayers 限制,它将调用 getRandomWinner 函数。
getRandomWinner 方法
这个函数首先检查我们的合约在请求随机性之前是否有 Link token,因为链路合约以 Link token 的形式请求费用。然后这个函数调用我们从 VRFConsumerBase 继承的 requestRandomness,并开始生成随机数的过程。
fulfillRandomness 方法
这个函数从 VRFConsumerBase 继承而来。当 VRFCoordinator 从外部世界接收到随机性之后,它就被调用。在接收到随机性(可以是 uint256 范围内的任何数字)后,我们使用 mod operaator 将其范围从 0 减少到 player.length-1。
这为我们选择了一个索引,我们用这个索引从玩家数组中检索出赢家。它将合约中所有的乙醚发送给赢家,并发出一个 GameEnded 事件。
安装配置环境变量
npm i dotenv
QUICKNODE_HTTP_URL="add-quicknode-http-provider-url-here"
PRIVATE_KEY="add-the-private-key-here"
POLYGONSCAN_KEY="polygonscan-api-key-token-here"
最后,与 Etherscan 类似,Polygon 网络也有 Polygonscan。这两个区块探索器都是由同一个团队开发的,工作方式基本上完全相同。为了通过 Hardhat 在 Polygonscan 上自动进行合约验证,我们需要一个 Polygonscan 的 API 密钥。前往 PolygonScan 并注册。在 "账户概览 "页面上,点击 API Key,添加一个新的 API Key,并复制 API Key Token。把这个放在下面的 POLYGONSCAN_KEY 中。
修改 hardhat.config.js 配置
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config({ path: ".env" });
const QUICKNODE_HTTP_URL = process.env.QUICKNODE_HTTP_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const POLYGONSCAN_KEY = process.env.POLYGONSCAN_KEY;
module.exports = {
  solidity: "0.8.4",
  networks: {
    mumbai: {
      url: QUICKNODE_HTTP_URL,
      accounts: [PRIVATE_KEY],
    },
  },
  etherscan: {
    apiKey: {
      polygonMumbai: POLYGONSCAN_KEY,
    },
  },
};
修改常量配置
constants/index.js
const { ethers, BigNumber } = require("hardhat");
const LINK_TOKEN = "0x326C977E6efc84E512bB9C30f76E30c160eD06FB";
const VRF_COORDINATOR = "0x8C7382F9D8f56b33781fE506E897a4F1e2d17255";
const KEY_HASH =
  "0x6e75b569a01ef56d18cab6a8e71e6600d6ce853834d4a5748b720d06f878b3a4";
const FEE = ethers.utils.parseEther("0.0001");
module.exports = { LINK_TOKEN, VRF_COORDINATOR, KEY_HASH, FEE };
我们从这里得到的数值是链家已经提供给我们的。
让我们把合约部署到孟 买网络。在 scripts 文件夹下创建一个新文件,或者替换现有的默认文件,命名为 deploy.js。
修改部署代码
const { ethers } = require("hardhat");
require("dotenv").config({ path: ".env" });
const { FEE, VRF_COORDINATOR, LINK_TOKEN, KEY_HASH } = require("../constants");
async function main() {
  /*
 A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts,
 so randomWinnerGame here is a factory for instances of our RandomWinnerGame contract.
 */
  const randomWinnerGame = await ethers.getContractFactory("RandomWinnerGame");
  // deploy the contract
  const deployedRandomWinnerGameContract = await randomWinnerGame.deploy(
    VRF_COORDINATOR,
    LINK_TOKEN,
    KEY_HASH,
    FEE
  );
  await deployedRandomWinnerGameContract.deployed();
  // print the address of the deployed contract
  console.log(
    "Verify Contract Address:",
    deployedRandomWinnerGameContract.address
  );
  console.log("Sleeping.....");
  // Wait for etherscan to notice that the contract has been deployed
  await sleep(30000);
  // Verify the contract after deploying
  await hre.run("verify:verify", {
    address: deployedRandomWinnerGameContract.address,
    constructorArguments: [VRF_COORDINATOR, LINK_TOKEN, KEY_HASH, FEE],
  });
}
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
// Call the main function and catch if there is any error
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
编译部署
npx hardhat compile
npx hardhat run scripts/deploy.js --network mumbai
它应该已经打印了一个到孟买 polygonscan 的链接,你的合约现在已经验证了。点击 polygonscan 链接,在那里与你的合约互动。现在让我们在 polygonscan 上玩游戏吧。
在你的终端,他们应该已经打印了一个链接到你的合约,如果没有,然后去孟买多边形扫描和搜索你的合约地址,它应该被验证。
我们现在将用一些链式链接来资助这个合约,这样我们就可以要求随机性,去Polygon Faucet,从下拉菜单中选择链接,然后输入你的合约地址
现在,通过点击连接到 Web3,将你的钱包连接到孟买多边形扫描。 确保你的账户有一些孟买多边形代币
然后在 startGame 函数中输入一些数值,并点击 Write 按钮
现在你可以让你的地址加入游戏了 注意:我在这里输入的数值是 10WEI,因为这是我指定的报名费的数值,但是因为加入游戏接受乙醚而不是 WEI,我必须将 10WEI 转换成乙醚。你也可以用 eth 转换器将你的参赛费转换成乙 醚。
现在刷新页面并连接一个新的钱包,其中有一些 Matic,这样你就可以让另一个玩家加入。
如果你现在进入你的事件标签并不断刷新(VRFCoordinator 调用 fullFillRandomness 函数需要一些时间,因为它必须从外部世界获得数据),在某一时刻你将能够看到一个事件,上面写着 GameEnded
从下拉菜单中把 GameEnded 事件中的第一个值用 Hex 转换为地址,因为那是赢家的地址。
嘭,它完成了 🚀
你现在知道如何玩这个游戏了。在下一个教程中,我们将为此创建一个用户界面,并学习如何使用代码本身来跟踪这些事件。
让我们开始吧 🚀🚀