跳到主要内容

链锁式 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 转换为地址,因为那是赢家的地址。

嘭,它完成了 🚀

你现在知道如何玩这个游戏了。在下一个教程中,我们将为此创建一个用户界面,并学习如何使用代码本身来跟踪这些事件。

让我们开始吧 🚀🚀