写点什么

web3 chainlink 预言机喂价、VRF

作者:1_bit
  • 2022 年 10 月 11 日
    广东
  • 本文字数:6538 字

    阅读完需:约 21 分钟

一、chainlink 使用准备

1.1 什么是预言机

在智能合约中,无法获取区块链意外的数据,或外部数据不可靠,从而产生了预言机。预言机可使外部数据可信并保持唯一性。


在使用 chainlink 之前要注意,(2022 年 9 月 29 日) 之前很多资料所述在测试网需要使用 Rinkeby 网络,但由于 Ethereum 的协议更改:Rinkeby、Ropsten 和 Kovan 测试网络可能无法可靠地工作,很快就会被弃用。所以在此我们需要使用 Goerli 测试网络(当然 chainlink 也支持其他网络):


1.2 获取预言机测试权

在 Goerli 测试网络下,需要获取 Link 以及对应的 eth 测试权 ,获取方式如下。


首先导入 Link 的代权 ,导入步骤为:


  • 1.点击导入代权



  • 2.点击后在如下页面中输入合约地 0x326C977E6efc84E512bB9C30f76E30c160eD06FB 以及 LINK



  • 3.最后进行添加即可,添加之后转到 https://faucets.chain.link/,随后在页面上点击连接钱包,出现如下界面:



在此需要有一个推特账号并且有发布一条推特,最后点击验证后即可获取测试权 。

二、 获取交易对价格

使用 chainlink 预言机在 solidity 中需要引入 chainlink 的 sol 文件:


import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
复制代码


随后需要在 solidity 中创建一个 AggregatorV3Interface 对象:


AggregatorV3Interface internal priceFeed;
复制代码


可以创建一个构造函数用来对这个 reserveFeed 初始化:


constructor() {    reserveFeed = AggregatorV3Interface(0x779877A7B0D9E8603169DdbD7836e478b4624789);}
复制代码


其中 AggregatorV3Interface 中所填入的地址为你需要对应数据的地址,这个地址可以在 chainlink 文档中查询不同价格的地址。接下来创建一个方法,通过 priceFeed 调取最新 BTC / ETH 价格的价格:


priceFeed.latestRoundData();
复制代码


由于这个 latestRoundData 方法返回多个参数,在此我们仅获取价格即可,所有函数写成:


function getLatestPrice() public view returns (int) {    (        /*uint80 roundID*/,        int price,        /*uint startedAt*/,        /*uint timeStamp*/,        /*uint80 answeredInRound*/    ) = priceFeed.latestRoundData();    return price;}
复制代码


调取后即可得到最新价格:



以上返回的数值是一个 18 位的小数,也就是 14.615977000000000000 完整合约代码如下(参考 chainlink 官方示例):


// SPDX-License-Identifier: MITpragma solidity ^0.8.7;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract PriceConsumerV3 {
AggregatorV3Interface internal priceFeed;
/** * Network: Goerli * Aggregator: ETH/USD * Address: 0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e */ constructor() { priceFeed = AggregatorV3Interface(0x779877A7B0D9E8603169DdbD7836e478b4624789); }
/** * Returns the latest price */ function getLatestPrice() public view returns (int) { ( /*uint80 roundID*/, int price, /*uint startedAt*/, /*uint timeStamp*/, /*uint80 answeredInRound*/ ) = priceFeed.latestRoundData(); return price; }}
复制代码


对于以上所述的地址就等于一个接口,这个接口可以通过 chainlink 的 doc 进行查看,链接为:https://docs.chain.link/docs/data-feeds/price-feeds/addresses/


打开链接后,page 拉到下面,可以找到对应的测试网地址:



点击地址进入,选择合约,可以查看当前合约:



接下来还可以直接点击 read Contract 查看相关读取的方法,找到我们所使用的的 latestRoundData 为我们所调用的方法:



点击 query 就等于我们调用了该方法:



之后将会出现最新的返回结果:



其中参数:


  • roundid 表示第几轮价格(价格是一轮轮更新的)

  • answer 表示当前价格

  • startedAt 什么时候开始更新的价格

  • updatedAt 什么时候开始更新结束的

  • answeredlnRound 是第几轮更新的当前价格


三、VRF 确定性的随机数使用准备

3.1 创建订阅

VRF 是 chainlink 提供的可验证随机数,在以往的随机数中,每个节点生成的随机数是不一样的,此时所有节点的某个结果不一致,是在区块链中是不允许的,chainlink 的 VRF 即可解决这个问题。


在此我们只探讨 VRF 的使用方法,并不验证 VRF 的可验证性。


使用 VRF 分为以下几个步骤(之后将会详细讲解):


  1. 创建一个订阅,有了订阅后才可以使用 VRF (相当于注册个账号,方便接下来的使用)

  2. 创建订阅后将会有一个 订阅 ID,

  3. 获取 订阅 ID 获取到 ID 后,部署合约通过 订阅 ID 初始化(构造函数完成)VRF 的接口

  4. 部署合约后将合约地址通过网页指定调用随机数的合约


由于我们使用搞得是 goerli 测试网络,在 chainlink 文档中找到 goerli 网络的 VRF 订阅地址,doc 链接为:https://docs.chain.link/docs/vrf/v2/subscription/supported-networks/



找到 goerli 网络的 VRF 订阅链接 https://vrf.chain.link/goerli 通过地址进行链接订阅。


接着打开 goerli 的订阅链接,在 web 页 中点击 Create Subscription 对 开始创建订阅:



点击创建订阅后需要通过钱包账户进行链接,本文中使用小狐狸 metamask,点击按钮连接钱包:



接着会获取到你钱包地址,点击 Create subscription 创建订阅:



此时会发生一个交易,要注意你的钱包提示(有些同学可能会不直接弹出钱包,在此需要点击钱包后会弹出交易窗),点击确认交易(我点太快了):



随后稍等一会,将会出现已订阅提示,并且会出现一个 transaction 交易,可以点击查看:



点击查看交易后可以看到对应的交易信息(当然你可以不看,可以稍等片刻后交易完成):



接着会出现一个窗口,叫你往里面存一点 LINK,这个 LINK 是用于生成随机数的花费,你可以选择 10 个就可以了,做个测试嘛,不需要太多,10 个够十来次了(LINK 之前在前几节有说怎么获取):



随后小狐狸会弹出交易窗,交易完毕后即设置随机数生成花费成功:



接下来就需要添加你的合约账户了,在这里你还可以看到这个 ID,这个 ID 很重要,注意:



此时我们只需要记录下这个订阅 ID 后就可以去写代码了,这个页面先保留,别急着叉掉。

3.2 VRF 获取真随机数合约编写

在合约中使用 VRF 需要 import 引入对应的合约,合约所在位置可以通过 chainlink 的 GitHub 进行查看,地址为:https://github.com/smartcontractkit/chainlink/blob/develop/contracts/



在此处我们一共要 import 两个合约:


  • chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol

  • chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol


这些合约可以在这个项目的 src 查看,由于我们是 0.8 版本,所以之后查看将会在 v0.8 目录下查看,以下是对应不同版本的 合约:



我们选择 v0.8 版本:



我们往下拉,找到 VRF 相关的合约内容,在此 就是所需引入的 VRF 第一个合约,这个合约是需要咱们继承后重写接收预言机下所产生的随机数接口(之后将会详细讲解):



接着还有一个合约在 interface 目录下:



此时需要进入到 interface 才能查看此合约代码:



接下来所实现 VRF 的步骤为:


  1. 创建一个类继承 VRFConsumerBaseV2 合约,重写方法

  2. 引入 VRF 的 interface ,在继承的 VRFConsumerBaseV2 合约下使用 VRFCoordinatorV2Interface 申请随机数(对随机数进行获取)


接着我们创建一个合约后,在合约中使用 import 引入这两个合约(示例引用 chainlink 官方示例):


// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.7;
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
复制代码


注意,引入合约路径须在前面加一个“@”。


接着创建一个合约名为 ChainLinkVRFDemo 继承自 VRFConsumerBaseV2:


contract ChainLinkVRFDemo is VRFConsumerBaseV2{
}
复制代码


接着编写一个构造方法,这个构造方法需要调用 父合约 构造函数,传入 VRF 的调用地址,在此我们可以查看 goerli 测试网络下的地址,查看链接为:https://docs.chain.link/docs/vrf/v2/subscription/supported-networks/#avalanche-mainnet



此时构造函数代码如下:


address vrfCoordinatorAddr=0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D;//调用地址constructor()VRFConsumerBaseV2(vrfCoordinatorAddr){}
复制代码


由于接下来我们还需要使用对应的订阅 ID,通过这个订阅 ID 对预言机发送请求获取随机数,所以此时我们得需要设置这个订阅号,那么在构造函数时传入参数,记录订阅号 ID。此时新建一个变量 subId,修改构造函数接收订阅 ID:


uint64 subId;//订阅号constructor(uint64 _subId)VRFConsumerBaseV2(vrfCoordinatorAddr){    subId=_subId;}
复制代码


由于我们在创建另一个 import 的合约 VRFCoordinatorV2Interface 获取随机数时也需要对应的知道 VRF 的调用地址,所以此时咱们直接在构造函数中对其对象进行创建,并设定地址:


VRFCoordinatorV2Interface VRFInterface;//当然要声明一个 VRFCoordinatorV2Interface 类型的变量,接着在构造函数中实例化即可constructor(uint64 _subId)VRFConsumerBaseV2(vrfCoordinatorAddr){    VRFInterface=VRFCoordinatorV2Interface(vrfCoordinatorAddr);    subId=_subId;}
复制代码


此时你若编译肯定会报错,那是因为你还未重写一个接收方法,这个方法叫做 fulfillRandomWords,咱们在 GitHub 的 VRFConsumerBaseV2.sol 源码中应该可以看得到:



这个方法需要重写,作用是当我们请求了随机数后,将会返回对应的随机数(也就是参数 randomWords),我们需要在这个方法中编写代码将这个随机数进行存储。可能看到这里你会疑惑,直接调用请求不就可以获取了?为什么还要这个方法?而且这个方法是 internal 修饰的,是内部的方法呀。


在此我们并不讨论 chainlink VRF 随机数生成后返回的原力,咱们只需要知道在调用随机数请求后,chainlink 的节点开始生成随机数,并经过一些列的操作(投票随机数)最后得出最终结果,这个结果将通过调用咱们这个合约中的一个接口进行随机数的“返回”。


可能你还会说,这个接口不是 internal ?外部怎么调用?其实最开始我也并不理解 VRF 这个接口的实现逻辑,在我查看他代码时发现,fulfillRandomWords 这个方法在 rawFulfillRandomWords 中进行了调用,你看下图:



此时 rawFulfillRandomWords 方法是 external 进行修饰的,并且接收两个参数,其中我肯定的告诉你 randomWords 就是我们请求后的随机数。


那么此时你应该明白了大体逻辑:在我们继承了 VRFConsumerBaseV2 合约后,并不需要重写 rawFulfillRandomWords 方法,只需要重写 fulfillRandomWords 方法,在其内部编写将 randomWords 存储到何处(哪个变量接收)即可,这样就得到了随机数。


现在你应该明白了吧。那么接下来我们直接从 GitHub 中复制这个需要重写的 fulfillRandomWords,用一个变量接收 randomWords 即可:


//存储随机数uint256[] public s_randomWords;//接收方法function fulfillRandomWords(uint256 requestID,uint256[] memory randomWords) internal override{    s_randomWords=randomWords;} 
复制代码


记得,一定要 把 virtual 改成了 override。


接下来咱们开始编写请求方法吧,请求方法也超击简单,通过 另外一个 import 的合约 VRFCoordinatorV2Interface 的对象调用 requestRandomWords 方法,并且传入参数即可。


我们可以在 Github 上的合约代码内看到这个接口:



这个方法接收 5 个参数,分别是:


  • keyHash:一个 hash 值,不同的 hash 值表示你这个“生成随机数的快慢”

  • subId:订阅 ID(已获得)

  • requestConfirmations:多少个交易后表示成功,越小值越快

  • callbackGasLimit:gas limit 上限官方示例给出的值是 100000

  • numWords:请求多少个随机数,上限是 500


这样一看是不是就觉得很简单了?


其中的 keyHash 是有官方给出的,查看文档: https://docs.chain.link/docs/vrf/v2/subscription/supported-networks/#avalanche-mainnet



上图中的 30 gwei key hash 就是这个 hash 值了,在这里由于是测试网只有一个,这个 hash 的作用就是给得越高,当网络繁忙时,你的“交易”会比其他的快,低一点慢 高一点快。


你可以看到其他网络中的 gwei key hash 的值有不同的选择:



剩下的参数我们就知道怎么创建了,一个是 requestConfirmations 就给个 3,表示 3 个确认后我们就认为交易 success 了、callbackGasLimit 默认给官方文档的 100000、numWords 请求随机数就请求 3 个即可;那么此时我们先创建一个方法叫做 requestRandomWords(必须这个名,至于为啥还在了解),并且使用在构造函数中所创建的 VRFInterface 对象对接口 requestRandomWords 进行调用,并且传入对应的参数:


//选择不同的 gwei  可以快和慢,低一点慢 高一点快bytes32 keyHash=0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15;//认为多少个交易以后交易就成功了,一般是12uint16 requestConfirmations=3;//gas limit上限uint32 callbackGasLimit=100000;//请求多少个随机数,上限是500uint32 numWords=3;
function requestRandomWords() external{ VRFInterface.requestRandomWords( keyHash, subId, requestConfirmations, callbackGasLimit, numWords );}
复制代码


是不是这样就 ok 了?不过我们需要,这个请求随机数的方法是一个交易,会存在花费,我们需要对应的为其设置只能合约者可以调用,那么添加 require:



在这里还要提一嘴,这个 requestRandomWords 方法会返回一个值 requestID,表示你这个随机数是第几轮产生的,你也可以进行接收:



整理一下后最终整个合约代码如下:


// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.7;
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
contract ChainLinkVRFDemo is VRFConsumerBaseV2{ VRFCoordinatorV2Interface VRFInterface; uint64 subId;//订阅号 address owner; address vrfCoordinatorAddr=0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D; //选择不同的 gwei 可以快和慢,低一点慢 高一点快 bytes32 keyHash=0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15; //认为多少个交易以后交易就成功了,一般是12 uint16 requestConfirmations=3; //gas limit上限 uint32 callbackGasLimit=100000; //请求多少个随机数,上限是500 uint32 numWords=3; //存储随机数 uint256[] public s_randomWords; //第几次请求的数据 uint256 public requestID; constructor(uint64 _subId)VRFConsumerBaseV2(vrfCoordinatorAddr){ VRFInterface=VRFCoordinatorV2Interface(vrfCoordinatorAddr); subId=_subId; }
//接收方法 function fulfillRandomWords(uint256 requestID,uint256[] memory randomWords) internal override{ s_randomWords=randomWords; } function requestRandomWords() external{ require(msg.sender==owner); requestID=VRFInterface.requestRandomWords( keyHash, subId, requestConfirmations, callbackGasLimit, numWords ); }}
复制代码


我们开始部署合约,选择 Metamask 的网络,传入之前我生成的那个订阅 ID 号(请忽略一下地址不同,因为之前敲错代码了后又重新贴图了一次):



点击部署后将会有一个交易产生:



确定支付后可以对其查看:



回到之前订阅成功的页面,点击 添加合约地址:



复制自己的合约地址:



添加合约:



接着确认小狐狸的交易:




接着进行等待交易完成(你的可能和我的不一样)。



接着点击请求随机数方法:



接着会弹出交易提示,我们点击确认:



接着我们可以看到一个随机数的请求:



等 Pending 编程 success 即可完成随机数获取。



success 之后,点击合约中的存储状态变量,输入索引获取随机数,由于我们设置的是 3 个随机数,所以索引为 0、1、2:



发布于: 刚刚阅读数: 2
用户头像

1_bit

关注

还未添加个人签名 2021.02.05 加入

InfoQ签约作者 CSDN博客专家、博客之星TOP5、新星导师、第二季新星评委、博客新星评审团 蓝桥签约作者 动漫系列编程作者 2021年火爆C站的大话教程作者 目前正在深入学习画漫画,之后将会自制动漫视频。

评论

发布
暂无评论
web3 chainlink 预言机喂价、VRF_智能合约_1_bit_InfoQ写作社区