Chainlink_oracle 预言机 定义
定义:智能合约无法获取区块链以外的数据,它是一个沙盒,外部API提供的数据和任何其他链下资源都无法获取。那么,预言机就充当这个角色:将真实世界的数据输送到智能合约中
例子1:用户在质押平台质押了代币,那么平台就需要知道代币的价格,而价格无法在链上获取,需要从外部输入。
例子2:保险公司说如果明天下雨,则赔偿给你,而明天下不下雨这件事,需要从外部输入
由于区块链的节点之间,执行程序的结果都是相同的,因此不存在随机这个东西,否则无法达到共识。比如:生成随机数的函数,这是不存在的,只能是伪随机数。并且合约主动获取链下数据存在API不稳定、URL更新等一系列问题,因此必须有一个第三方合约来主动给合约来提供链下数据
工作流程 中心化预言机
运行一个中心化节点,然后提供数据给合约。
问题:单点失败。如果整个区块链的信息依赖于某个节点,如果这个节点挂了,那么整个区块链数据就无法获取了
去中心化预言机
多个数据节点形成去中心预言机网络,每个节点都会收集数据,达成共识(聚合)后输入到区块链上的智能合约。达成公式:比如取中位数、平均数等
技术上,避免了单点失败风险
数据上,通过网络对多个数据源进行验证
chainlink就搞了一个这样的去中心化预言机网络,提供喂价、随机数等等,目前chainlink的共识机制是取中位数
喂价 原理
业务流程图:数据提供商和预言机节点是不一样的,数据提供商只负责收集数据,预言机节点会和其他节点聚合数据,达成共识
使用 获得币对地址:https://docs.chain.link/data-feeds/price-feeds/addresses
remix 使用solidity获得goerli测试网中币对的价格
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // SPDX-License-Identifier: MIT pragma solidity 0.8.21; // Proxy的接口 import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; contract PriceFeed{ AggregatorV3Interface internal priceFeed; constructor(){ priceFeed = AggregatorV3Interface(0x779877A7B0D9E8603169DdbD7836e478b4624789); } function getPrice() public view returns (int256){ // latestRoundData() returns // uint80 roundId, // 价格的更新是一轮一轮的,每次更新+1 // int256 answer, // 价格数据 // uint256 startedAt, // 这个价格什么时候开始更新 // uint256 updatedAt, // 这个价格什么时候结束 // uint80 answeredInRound // 这个价格在第几轮更新 (,int256 answer,,,) = priceFeed.latestRoundData(); return answer; // BTC/ETH价格 = 15962257405504495000 } }
hardhat
npm init -yes
npm install —save-dev hardhat
npx hardhat init:选择空的
安装依赖:
npm install —save-dev @chainlink/contracts
npm install —save-dev chai-bn
pm install —save-dev @nomicfoundation/hardhat-toolbox
DataFeedDemo.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // SPDX-License-Identifier: MIT pragma solidity 0.8.19; // Proxy的接口 import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; contract DataFeedDemo{ AggregatorV3Interface internal priceFeed; constructor(){ // https://docs.chain.link/data-feeds/price-feeds/addresses priceFeed = AggregatorV3Interface(0x779877A7B0D9E8603169DdbD7836e478b4624789); } function getPrice() public view returns (int256){ // latestRoundData() returns // uint80 roundId, // 价格的更新是一轮一轮的,每次更新+1 // int256 answer, // 价格数据 // uint256 startedAt, // 这个价格什么时候开始更新 // uint256 updatedAt, // 这个价格什么时候结束 // uint80 answeredInRound // 这个价格在第几轮更新 (,int256 answer,,,) = priceFeed.latestRoundData(); return answer; // BTC/ETH价格 = 15962257405504495000 } }
deployDataFeed.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const { ethers } = require ("hardhat" ) async function deployDataFeed ( ){ const DataFeed = await ethers.getContractFactory ("DataFeedDemo" ) const DataFeedContract = await DataFeed .deploy () await DataFeedContract .waitForDeployment () console .log ("the contract is deployed successfully" ) let dataFeedContractAddr = DataFeedContract .target console .log ("address of the contract is:" + dataFeedContractAddr) } deployDataFeed ().then (()=> { process.exit (0 ) }).catch (err => { console .log (err) })
DataFeedDemo.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const { ethers } = require ("hardhat" ) const { expect } = require ("chai" )describe ("Data Feed Demo test" , function ( ){ it ("check if the price data return by oracle is greater than 0" , async function ( ){ const DataFeed = await ethers.getContractFactory ("DataFeedDemo" ) const DataFeedContract = await DataFeed .deploy () await DataFeedContract .waitForDeployment () console .log ("the contract is deployed successfully" ) const result = await DataFeedContract .getPrice () console .log ("the price is :" + ethers.formatUnits (result, "ether" )) expect (result).to .be .greaterThan ("0" ) }) })
hardhat.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 require ("@nomicfoundation/hardhat-toolbox" )const privatekey = "xxx" const goerliapi = "https://eth-goerli.g.alchemy.com/v2/xxx" module .exports = { solidity : "0.8.19" , networks : { goerli : { accounts : [privatekey], url : goerliapi, } } };
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { "name" : "datafeeddemo" , "version" : "1.0.0" , "description" : "" , "main" : "index.js" , "scripts" : { "test" : "echo \"Error: no test specified\" && exit 1" }, "keywords" : [], "author" : "" , "license" : "ISC" , "devDependencies" : { "@chainlink/contracts" : "^0.6.1" , "@nomicfoundation/hardhat-toolbox" : "^3.0.0" , "chai-bn" : "^0.3.1" , "hardhat" : "^2.17.1" } }
VRF 原理
可验证随机数(VRF)定义
可证明性:验证者要查看chainlink没有作弊
独特性:一个种子对应一个输出,chainlink无法选择一个对自己有利的输出
伪随机性:数学算法
随机数算法VRF(90年代提出的密码学算法,论文内容)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 1.密钥生成函数(Key Gen) G(r) = (PK,SK) PK: public key SK: secret key # chainlink生成一个新的账户,用私钥来操作,把公钥写在合约变量中 2.随机数生达函数(Evaluate) E(SK,seed)=>(Randomness,Proof) seed: RNG的种子 Randomness: 随机数 Proof: 证明 3.验证函数(Verify) V(PK,seed,Randomness,Proff) => (True or false)
技术架构
用户调用自己创建的Consumer合约的函数请求随机数,用户创建的合约需要实现RequestRandomWords()
和FulfillRandomWords()
Consumer合约进一步调用Coordinator函数请求随机数
将PreSeed写入Event log
预言机读取Event log中的Preseed和blockhash
预言机通过VRF生成随机数和Proof
预言机将rc和proof写入Coordinator
Coordinator进行验证&将随机数写入Consumer合约
使用 步骤
注册一个VRF,订阅它,告诉chainlink我们要用它的服务,并且可以查看随机数情况:https://vrf.chain.link/
将consumer合约(用户合约)加入到订阅
consumer合约请求随机数
consumer合约接收随机数
合约地址:https://docs.chain.link/vrf/v2/direct-funding/supported-networks和https://docs.chain.link/vrf/v2/subscription/supported-networks
等待我们实现的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 pragma solidity ^0.8.0; abstract contract VRFConsumerBaseV2 { error OnlyCoordinatorCanFulfill(address have, address want); address private immutable vrfCoordinator; constructor(address _vrfCoordinator) { vrfCoordinator = _vrfCoordinator; } // 在我们的consumer合约需要实现这个方法 function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal virtual; // Coordinator会回调这个方法进行写入 function rawFulfillRandomWords(uint256 requestId, uint256[] memory randomWords) external { // 查看调用者是不是vrfCoordinator if (msg.sender != vrfCoordinator) { revert OnlyCoordinatorCanFulfill(msg.sender, vrfCoordinator); } fulfillRandomWords(requestId, randomWords); } }
remix 获取subId
完成订阅,然后可以调用requestRandomWords()
申请随机数
需要稍微等一下,我们才能查看到s_randomWords随机数数据(在此之前也可以可以查询到s_requestId)
成功
随机数很大,实际应用中可以取模操作使他变小
坑:uint32 callbackGasLimit = 5_200_00;
设置太高或者太低都会失败。并且,如果你的link太少,Coordinator在回调的时候,无法成功,因此需要在页面中添加更多的link
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 // SPDX-License-Identifier: MIT pragma solidity 0.8.21; import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; contract ChainlinkVRFDemo is VRFConsumerBaseV2{ // 1.获取Coordinator的接口 VRFCoordinatorV2Interface COORDINATOR; address vrfCoordinatorAddr = 0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D; // 2.获取keyHash,服务质量 bytes32 keyHash = 0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15; // 3.订阅ID号 uint64 s_subId; // 4.我们认为3个区块之后获取VRF,实验方便起见,选3 uint16 requestConfirmations = 3; // 5.gas回调设置,太小会导致无法写入随机数 uint32 callbackGasLimit = 5_200_00; // 太高或太小都会fail // 6.我们请求4个随机数 uint32 numWords = 4; address owner; // 7.第几轮的随机数 uint256 public s_requestId; // 8.随机数结果 uint256[] public s_randomWords; constructor(uint64 subId) VRFConsumerBaseV2(vrfCoordinatorAddr){ COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinatorAddr); s_subId = subId; owner = msg.sender; } function requestRandomWords() public { // 我们给Coordinator发送请求,Coordinator又调用预言机节点返回随机数,预言机节点需要花费gas // 因此gas不仅仅是调用者花费gas,预言机节点也会花费gas,因此做一点限制 require(msg.sender == owner); // bytes32 keyHash, 其实就是公钥信息。不同的keyHash代表我支付不同的gas费用,比如我想要更好更快的,则选择质量更高的keyHash,服务商会提供你更好的服务 // uint64 subId, 订阅ID号 // uint16 requestConfirmations, 认为多少个区块之后能成功获取VRF,L1一般是12 // uint32 callbackGasLimit, VRF调用我们的fulfillrandomWords()时使用的gas上限 // uint32 numWords 请求多少个随机数,目前上限是500 // 返回一个requestID,告诉你是哪次请求的 // 9.请求随机数 s_requestId = COORDINATOR.requestRandomWords(keyHash, s_subId, requestConfirmations, callbackGasLimit, numWords); } function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override{ // 10.接收随机数 s_randomWords = randomWords; } }
hardhat mock合约地址:https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol
npm install —save-dev hardhat
npx hardhat init选择空的
依赖
npm install —save-dev @chainlink/contracts
npm install —save-dev hardhat-deploy
npm install —save ethers@5.7
npm install —save-dev chai
npm install —save-dev ethereum-waffle:给chai加了一些方法
yarn add @nomiclabs/hardhat-waffle -D
npm install —save @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers:原因
https://github.com/wighawag/hardhat-deploy
if you use ethers.js
we recommend you also install hardhat-deploy-ethers
which add extra features to access deployments as ethers contract.
Since hardhat-deploy-ethers
is a fork of @nomiclabs/hardhat-ethers
and that other plugin might have a hardcoded dependency on @nomiclabs/hardhat-ethers
the best way to install hardhat-deploy-ethers
and ensure compatibility is the following:
1 npm install --save-dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers ethers
注意,下面的代码存在包版本错误的问题,并没有跑起来(deploy可以跑,test跑不了),主要理解思路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 // SPDX-License-Identifier: MIT pragma solidity 0.8.19; import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; contract ChainlinkVRFDemo is VRFConsumerBaseV2{ // 1.获取Coordinator的接口 VRFCoordinatorV2Interface COORDINATOR; // 使用本地网络Mock address vrfCoordinatorAddr; // 2.获取keyHash,服务质量 bytes32 keyHash = 0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15; // 3.订阅ID号 uint64 s_subId; // 4.我们认为3个区块之后获取VRF,实验方便起见,选3 uint16 requestConfirmations = 3; // 5.gas回调设置,太小会导致无法写入随机数 uint32 callbackGasLimit = 5_200_00; // 太高或太小都会fail // 6.我们请求4个随机数 uint32 numWords = 4; address owner; // 7.第几轮的随机数 uint256 public s_requestId; // 8.随机数结果 uint256[] public s_randomWords; constructor(address _vrfCoordinatorAddr, uint64 subId) VRFConsumerBaseV2(vrfCoordinatorAddr){ COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinatorAddr); s_subId = subId; owner = msg.sender; vrfCoordinatorAddr = _vrfCoordinatorAddr; } function requestRandomWords() public { // 我们给Coordinator发送请求,Coordinator又调用预言机节点返回随机数,预言机节点需要花费gas // 因此gas不仅仅是调用者花费gas,预言机节点也会花费gas,因此做一点限制 require(msg.sender == owner); // bytes32 keyHash, 其实就是公钥信息。不同的keyHash代表我支付不同的gas费用,比如我想要更好更快的,则选择质量更高的keyHash,服务商会提供你更好的服务 // uint64 subId, 订阅ID号 // uint16 requestConfirmations, 认为多少个区块之后能成功获取VRF,L1一般是12 // uint32 callbackGasLimit, VRF调用我们的fulfillrandomWords()时使用的gas上限 // uint32 numWords 请求多少个随机数,目前上限是500 // 返回一个requestID,告诉你是哪次请求的 // 9.请求随机数 s_requestId = COORDINATOR.requestRandomWords(keyHash, s_subId, requestConfirmations, callbackGasLimit, numWords); } function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override{ // 10.接收随机数 s_randomWords = randomWords; } }
1 2 3 pragma solidity 0.8.19; import "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol";
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const {getNamedAccounts, deployments} = require ("hardhat" )const baseFee = "10000000000000000" ; const gasPriceLink = "1000000000" ; module .exports = async ({getNamedAccounts, deployments}) =>{ const {deploy} = deployments const {deployer} = await getNamedAccounts () await deploy ("VRFCoordinatorV2Mock" ,{ from : deployer, args : [baseFee, gasPriceLink], log : true , }) } module .exports .tags = ["mock" ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const {getNamedAccounts, deployments} = require ("hardhat" )module .exports = async ({getNamedAccounts, deployments}) =>{ const {deploy} = deployments const {deployer} = await getNamedAccounts () let vrfCoordinatorAddr let subId let vrfCoordinator = await ethers.getContract ("VRFCoordinatorV2Mock" ) vrfCoordinatorAddr = vrfCoordinator.target const tx = await vrfCoordinator.createSubscription () const txReceipt = await tx.wait (1 ) subId = txReceipt.logs [0 ].topics [1 ] await vrfCoordinator.fundSubscription (subId,"10000000000000000000" ) await deploy ("ChainlinkVRFDemo" ,{ from : deployer, args : [vrfCoordinatorAddr, subId], log : true , }) } module .exports .tags = ["vrf" ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 const { expect,assert } = require ("chai" )const { deployments, ethers } = require ("hardhat" )describe ("test VRFConsumer" , async function ( ){ let vrfCoordinator let vrfConsumer beforeEach (async () =>{ await deployments.fixture (["mock" ,"vrf" ]) vrfCoordinator = await ethers.getContract ("VRFCoordinatorV2Mock" ) vrfConsumer = await ethers.getContract ("ChainlinkVRFDemo" ) }) it ("check if we can request random number" , async () =>{ expect (vrfConsumer.requestRandomWords ()).to .emit ( vrfCoordinator, "RandomWordsRequested" ) }) it ("check if we can receive random number" , async () =>{ console .log ("test1" ) await vrfConsumer.requestRandomWords () console .log ("test2" ) const requestId = vrfConsumer.requestId () let vrfConsumerAddr = vrfConsumer.target await vrfCoordinator.fulfillRandomWords (requestId,vrfConsumerAddr) const randomWords0 = await vrfConsumer.s_randomWords (0 ) const randomWords1 = await vrfConsumer.s_randomWords (1 ) const randomWords2 = await vrfConsumer.s_randomWords (2 ) assert (randomWords0.gt (ethers.constant .Zero ),"first random wrong" ) assert (randomWords1.gt (ethers.constant .Zero ),"second random wrong" ) assert (randomWords2.gt (ethers.constant .Zero ),"third random wrong" ) }) })
1 2 3 4 5 6 7 8 9 10 11 12 13 require ("@nomiclabs/hardhat-ethers" )require ("hardhat-deploy" )require ("@nomiclabs/hardhat-waffle" )module .exports = { solidity : "0.8.19" , namedAccounts : { deployer : { default : 0 } } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { "name" : "vrfdemo" , "version" : "1.0.0" , "description" : "" , "main" : "index.js" , "scripts" : { "test" : "echo \"Error: no test specified\" && exit 1" } , "keywords" : [ ] , "author" : "" , "license" : "ISC" , "devDependencies" : { "@chainlink/contracts" : "^0.6.1" , "@nomicfoundation/hardhat-chai-matchers" : "^2.0.1" , "@nomicfoundation/hardhat-ethers" : "^3.0.2" , "@nomiclabs/hardhat-waffle" : "^2.0.6" , "chai" : "^4.3.7" , "ethereum-waffle" : "^4.0.10" , "hardhat" : "^2.17.1" , "hardhat-deploy" : "^0.11.34" } , "dependencies" : { "@nomiclabs/hardhat-ethers" : "npm:hardhat-deploy-ethers" , "ethers" : "^6.7.0" } }
Keepers 原理 合约自动化执行
如果是如下的两种方式,存在一定的问题
chainlink的方案
流程
Oracle检查CheckUpkeep,判断是否满足条件
如果CheckUpkeep条件不满足,则下个区块继续检查
如果CheckUpkeep条件满足,oracle 则调用keepers注册合约
Keepers注册合约调用用户合约performUpkeep
使用 remix 网站:https://automation.chain.link/goerli/new
完成注册
成功实验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 // array [1000, 1000, 1000, ...] // function => array [1000, 900, 1000, 900, ...] // checkupkeep: update => performUpkeep // checkupkeep: not updated => next round check // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/automation/interfaces/KeeperCompatibleInterface.sol"; contract KeepersDemo is KeeperCompatibleInterface{ uint256 public constant SIZE = 10; uint256 public constant INITIAL_BALANCE = 1000; uint256[SIZE] public balances; constructor(){ for(uint256 i = 0; i < SIZE; i++){ balances[i] = INITIAL_BALANCE; } } function withdraw(uint256 amount,uint256[] memory indexs) public { for(uint256 i = 0; i < indexs.length; i++){ balances[indexs[i]] -= amount; } } // chainlink预言机在链下执行 // 应该把绝大部分逻辑写在这里面 // performUpkeep()里面的操作应该尽可能的少,以此来减少gas消耗 // 可以通过bytes 信息告诉performUpkeep function checkUpkeep(bytes calldata) external view override returns (bool upkeepNeeded, bytes memory performData){ upkeepNeeded = false; for(uint256 i = 0; i < SIZE; i++){ if(balances[i] < INITIAL_BALANCE){ upkeepNeeded = true; break; } } // 如果performUpkeep()需要checkUpkeep()执行输出的一些数据,则可以在performData操作 return (upkeepNeeded,""); } function performUpkeep(bytes calldata) external { for(uint256 i = 0; i < SIZE; i++){ if(balances[i] < INITIAL_BALANCE){ balances[i] = INITIAL_BALANCE; } } } }