去中心化Oracle-Chainlink使用

我们构建一个DeFi dapp,需要从链外的API中获取一些数据,比如让您的用户能够获取ETH交易对的价格。JavaScript 应用程序或者其他的中心化应用程序可以轻松获取此类信息,比如向Binance 公共 API(或任何其他公开提供价格信息的服务)发出请求。但是,智能合约不能直接访问链外的数据。所以我们想做的是从去中心化的预言机网络(DON)和去中心化的数据源中获取我们的数据。

Chainlink 是一个去中心化预言机网络(DON)的框架,是一种跨多个预言机从多个来源获取数据的方法。这个 DON 以去中心化的方式聚合数据,并将其放在区块链上的智能合约中供我们读取。因此,我们所要做的就是从 Chainlink 网络不断为我们的合约读取数据!

使用 Chainlink 数据馈送是一种在这种去中心化环境中更便宜、更准确、更安全地从现实世界收集数据的方法。由于数据来自多个来源,因此多个人可以参与生态系统,它**甚至比运行集中式预言机更便宜。并且可以极大程度的减少错误集中,**Chainlink 网络使用一种称为链下报告的系统来就链下数据达成共识,并将经过加密验证的单一交易中的数据报告回链上供用户使用。

然后,您可以使用它制作SynthetixAaveCompound等协议!

目前chainlink为我们提供了几种服务:

  • DATA FEEDS快速获取链上交易对价格

  • USING RANDOMNESS 获取可信赖的随机数

  • CONNECT TO ANY API 调用第三方API

  • CHAINLINK KEEPERS 为用户提供去中心化的节点网络

Oracle数据交互流程:

post image

1.DATA FEEDS 数据馈送

  • 功能简介

    Data Feeds 是将您的智能合约与资产的真实市场价格联系起来的最快方式。例如,Data Feeds 的一个用途是使智能合约能够在一次调用中检索资产的最新定价数据。您可以使用智能合约来获取 EVM 链上的资产价格,比如ETH/USD交易对

  • 实际开发

    对于我们合约来说,我们需要通过和另外一个Oracle合约进行交互。对于交互的代码框架我们可以从Chainlink GitHub repository 进行导入。

    在程序中导入AggregatorV3Interface.sol,这样你就可以创建一个AggregatorV3Interface实例,并且使用它提供的接口。需要注意的是我们构建函数的时候需要传入Oracle的合约地址,你可以通过链上Feeds Registry或者chainlink文档Data Feeds Contract Addresses中提供的地址。

post image

因为在智能合约中浮点数是不被允许的,因为浮点数的精度对于价格数据来说是致命的,所以latestRoundData.pricefeed 获取到的是一个整数数据。所以我们需要通过 decimals()来取得真实的位数。

当然Data Feeds还支持检索前一轮 ID 的价格数据的函数:getRoundData(roundId)

function getHistoricalPrice(uint80 roundId) public view returns (int256) {
        (
            uint80 id, 
            int price,
            uint startedAt,
            uint timeStamp,
            uint80 answeredInRound
        ) = priceFeed.getRoundData(roundId);
        require(timeStamp > 0, "Round not complete");
        return price;
    }

我们还可以用Chainlink Feed Registry 来直接获取交易对的数据。Chainlink Feed Registry 是资产到 Feed 的链上映射。它使您可以直接从资产地址查询 Chainlink 数据提要,而无需提前知道合约地址。它们使智能合约能够通过单个合约在单个调用中获取资产的最新价格。

我们可以使用DenominationsSolidity 库来获取以太坊地址的货币标识符,这样我们就不需要去搜索对应的TOKEN。

import "@chainlink/contracts/src/v0.8/interfaces/FeedRegistryInterface.sol";
import "@chainlink/contracts/src/v0.8/Denominations.sol";

/**
     * Returns the ETH / USD price
     */
    function getEthUsdPrice() public view returns (int) {
        (
            uint80 roundID,
            int price,
            uint startedAt,
            uint timeStamp,
            uint80 answeredInRound
        ) = registry.latestRoundData(Denominations.ETH, Denominations.USD);
        return price;
    }

2.USING RANDOMNESS 获取随机数

在我们的合同中,我们一直在处理伪随机性。我们的keccak256功能是创建智能合约随机数的好方法。但是我们知道在区块链上的随机数是不可能真正随机的。虽然我们可以用一些全局变量来生成随机数,来使得随机数变得更难预测。

uint(keccak256(abi.encodePacked(msg.sender, block.difficulty, block.timestamp)));

然而,即使是这些数字也具有可预测性:

  • msg.sender 发送者钱包地址:发件人知道

  • block.difficulty 区块难度:直接受矿工影响

  • block.timestamp 时间戳:是可预测的

    在区块链之外获得随机性的另外一种方法是使用链外 API 调用返回随机数的服务。但是,如果该服务出现故障、被贿赂、被黑客入侵或其他原因,您可能会取回一个损坏的随机数。而且这种方式是一种中心化的方式。所以chainlink为我们提供了使用去中心化的Chainlink VRF ,这无疑是一种更好的方案。

  • 功能简介

    Chainlink VRF(可验证随机函数)是为智能合约设计的可证明公平且可验证的随机源。智能合约开发人员可以使用 Chainlink VRF 作为防篡改随机数生成器(RNG),为依赖不可预测结果的任何应用程序构建可靠的智能合约:

    • 区块链游戏和 NFT

    • 随机分配职责和资源(例如随机分配法官处理案件)

    • 为共识机制选择代表性样本

      当然,要使用预言机,我们必须支付一点 gas,也称为LILNK代币。LINK 代币专门设计用于与预言机合作并确保 Chainlink 预言机网络的安全性。每当我们按照基本请求模型提出请求时,我们的合约必须支付由我们使用的特定预言机服务定义的一定数量的 LINK (每种服务有不同的gas 费用)。

      总结来说,智能合约通过指定用于唯一标识 Chainlink 预言机的哈希来请求随机性。Chainlink 节点使用该哈希值和自己的密钥生成一个随机数,然后将其连同加密证明一起返回给链上合约。链上合约(称为VRF Coordinator)采用随机数和证明,并使用预言机的公钥进行验证。

      交互流程:

post image
  • 实际开发

    在合约中我们需要继承VRFConsumerBase 合约在构造函数中提供chainlink VRF的合约地址以及link的TOKEN地址。在继承合约的构造函数中提供 VRF对应的Key Hash 以及gasfee。这些地址都可以在chainlink的官方文档中获取,详细链接请参考下面的API参考。

post image

其中 requestRandomness 用于发起获取随机数事件,它会返回一个单一请求的requestID 。

fulfillRandomness 由 VRFCoordinator 在收到有效的 VRF 证明时调用。重写此函数以对链联 VRF 生成的随机数执行操作。

当然我们也可以用mapping将返回的随机数和requestId 或者address对应:

mapping(bytes32 => uint256) public requestIdToRandomNumber;

function getRandomNumber() public returns (bytes32 requestId) {
    require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK - fill contract with faucet");
    return requestRandomness(keyHash, fee);
}

function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
    requestIdToRandomNumber[requestId] = randomness;
}
  • API参考

https://docs.chain.link/docs/vrf-contracts/

3.CONNECT TO ANY API 调用第三方API

  • 功能简介

    Chainlink 的去中心化预言机网络为智能合约提供了推送和拉取数据的能力,这使智能合约能够通过去中心化预言机网络访问*任何外部数据源。*促进了链上和链下应用程序之间的互操作性。

  • 实际开发

    要使用 API 响应,智能合约应继承自ChainlinkClient. 该合约公开了一个名为Chainlink.Request的结构,智能合约应该使用它来构建 API 请求。该请求应包括预言机地址、作业 ID、费用、适配器参数和回调函数签名。

    目前,任何返回值都必须在 32 字节以内,如果该值大于需要发出多个请求。

    首先我们需要继承ChainlinkClient合约,并且在构造函数中给与预言机地址、作业 ID、费用参数。如果目标区块链的 LINK 地址尚未公开,请将构造函数中的setPublicChainlinkToken替换为setChainlinkToken(_address),其中_address是对应的 LINK 代币合约。oracle关键字是指合约进行 API 调用的特定 Chainlink 节点,而jobId是该节点要运行的特定ID。每个ID都是唯一的,并返回不同类型的数据。

    pragma solidity ^0.8.7;
    import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol";
    contract APIConsumer is ChainlinkClient {
        using Chainlink for Chainlink.Request;
      
        uint256 public volume;
        address private oracle;
        bytes32 private jobId;
        uint256 private fee;
        
        constructor() {
            setPublicChainlinkToken();
            oracle = 0xc57B33452b4F7BB189bB5AfaE9cc4aBa1f7a4FD8;
            jobId = "d5270d1c311941d0b08bead21fea7747";
            fee = 0.1 * 10 ** 18; // (Varies by network and job)
        }
    

    然后我们需要构造请求,并且重写回调处理函数:

    function requestVolumeData() public returns (bytes32 requestId) 
        {
            Chainlink.Request memory request = buildChainlinkRequest(jobId, address(this), this.fulfill.selector);
            
            // Set the URL to perform the GET request on
            request.add("get", "https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD");
            
            request.add("path", "RAW.ETH.USD.VOLUME24HOUR");
            
            // Multiply the result by 1000000000000000000 to remove decimals
            int timesAmount = 10**18;
            request.addInt("times", timesAmount);
            
            // Sends the request
            return sendChainlinkRequestTo(oracle, request, fee);
        }
        
        /**
         * Receive the response in the form of uint256
         */ 
        function fulfill(bytes32 _requestId, uint256 _volume) public recordChainlinkFulfillment(_requestId)
        {
            volume = _volume;
        }
    

    其中在我们add完GET请求后,需要add API响应中返回数据的路径,它使用JSONPath来确定数据的位置,比如例中就需要处理返回JSON中 RAW.ETH.USD.VOLUME24HOUR的数据。最后我们需要重写fulfill()来处理返回的数据。

    上面的代码示例从 oracle 响应中返回一个无符号整数,但有多种数据类型可用,例如:

    • uint256 - 无符号整数

    • int256 - 有符号整数

    • bool - 真或假值

    • bytes32 - 字符串和字节值

    如果需要返回字符串,请使用bytes32. 这是转换bytes32string. 目前,任何返回值都必须在 32 字节以内,如果该值大于则需要进行多个请求。

    特定作业返回的数据类型取决于它支持的任务。确保使用支持我们的合约需要使用的数据类型的预言机进行请求。

    同理HttpPost 可以add相应的body以及http头

    req.add("post", "http://post.example.com");
    req.add("queryParams", "firstKey=firstVal&secondKey=secondVal");
    req.add("extPath", "price/BTC/USD"); 
    

    参数

    • postPOST: 接受一个包含要向其发出请求的 URL 的字符串。

    • headers: 将包含键作为字符串和值作为字符串数组的对象。

    • queryParams: 为 URL 的查询参数获取一个字符串或字符串数​​组。

    • extPath: 将斜杠分隔的字符串或字符串数​​组附加到作业的 URL。

    • body:将用作请求中的数据的 JSON 正文(作为字符串)。

    json解析

    核心适配器遍历path指定并返回在该结果中找到的值。如果从HttpGet或HttpPost适配器返回 JSON 数据,则必须使用此适配器来解析响应。

    参数

    • path: 接受一个字符串数组,每个字符串是在字符串化 JSON 结果或单个点分隔字符串中解析出的下一个键。

      对于字符串化的 JSON:

      {"RAW": {"ETH": {"USD": {"LASTMARKET": "_someValue"}}}}
      

    我们可以将json解析到我们定义的数组当中:

    string[] memory path = new stringUnsupported embed;
    path[0] = "RAW";
    path[1] = "ETH";
    path[2] = "USD";
    path[3] = "LASTMARKET";
    req.addStringArray("path", path);
    

    或者对json中单个节点进行解析:

    req.add("path", "RAW.ETH.USD.LASTMARKET");
    

    最后我们会在fulfill()中回调得到我们解析后构造得json数据,当然对于数组我们也可以指定解析得数组偏移:

    req.add("path", "3.standardId");
    

    在之前提到过合约中是不支持浮点类型得数据得,所以对于有浮点类型得数据,在请求中我们可以run.addInt("times", 100);来为浮点数做一个乘基:

    参数

    • times:将输入乘以的数字。

    服务Sleep

    当然对于服务得并发任务来说,我们也可以控制节点得调用时间:req.addUint("until", now + 1 hours);核心适配器将在给定的持续时间内暂停当前任务管道。您必须设置ENABLE_EXPERIMENTAL_ADAPTERS=true才能使用睡眠适配器。

    参数

    • until:作业应该停止睡眠并在管道中的下一个任务处恢复的 UNIX 时间戳。

    在不指定 URL 的情况下选择 Oracle 作业

    如果您的合约正在调用公共 API 端点,则可能已经存在一个 Oracle 作业。这可能意味着您不需要将 URL 或其他适配器参数添加到请求中,因为作业已经配置为返回所需的数据。这使得智能合约代码更加简洁。例如使用预先配置的现有作业来发出请求以获取数据,就可以不用add URL信息:

        /**
         * Initial request
         */
        function requestElectionWinner() public returns (bytes32 requestId) 
        {
            Chainlink.Request memory request = buildChainlinkRequest(jobId, address(this), this.fulfill.selector);
            request.add("date", "2021-11-02");
            request.add("raceId", "12111");
            request.add("statePostal", "FL");
            request.add("resultsType", "l"); // Replace with l for live results.  
            return sendChainlinkRequestTo(oracle, request, fee);
        }
    
        /**
         * Callback function
         */
        function fulfill(bytes32 _requestId, uint256 _voteCount, bytes32 _firstName, bytes32 _lastName, bytes32 _candidateId, bytes32 _party, bytes[] memory _candidates) public recordChainlinkFulfillment(_requestId)
        {
            voteCount = _voteCount;
            firstName = _firstName;
            lastName = _lastName;
            candidateId = _candidateId;
            party = _party;
            candidates = _candidates;
            emit WinnerFound(firstName, lastName, voteCount, candidateId, party);
        }
    

    选择 Oracle 作业

    Chainlink 推动了各种预言机数据服务的推出,这些服务允许 dApp 通过提供商拥有的节点来访问自外部数据源的丰富数据。我们可以从数据提供者节点列表中找到我们需要得特定功能的oracle节点。

  • API参考

    https://docs.chain.link/docs/chainlink-framework/

功能简介

智能合约不能在任意时间或任意条件下触发或启动自己的功能。只有当交易由另一个账户(例如用户、预言机或合约)发起时,才会发生状态更改。(中心化的应用程序可以使用线程或者定时器来进行一些操作,比如定时的数据处理,数据上报等)。那当我们部署的合约需要实现这些功能就会比较麻烦。除非我们在部署完合约之后,在写一套用于维护的工具。

为了解决这个问题,区块链项目可以:

  • 创建高度可靠的基础设施触发智能合约功能。这是集中式的,而且构建和维护通常很昂贵。

  • 外包给第三方。此选项也是集中式的,并且会产生单点故障。

  • 使用公开市场解决方案。这个选项是去中心化的,但伴随着复杂的激励调整、竞争机器人的潜力会增加执行成本以及难以确保可靠性。

Chainlink Keeper网络也为我们提供了这个解决方案。Chainlink Keepers 为用户提供去中心化的节点网络,这些节点被激励(合约注册了节点服务需要支付一定的费用)执行所有已注册的工作(或维护),而不会相互竞争。网络有几个好处:

  • 为开发人员提供超可靠、去中心化的智能合约自动化

  • 提供可扩展的计算,允许开发人员以更低的成本构建更高级的 dApp

  • 灵活性和可编程性

那么我们来说一下Chainlink Keeper Network :

Chainlink Keeper Network 为智能合约提供选项,以最小化信任和去中心化的方式外包定期维护任务。该网络旨在为 Keeper 生态系统内的激励和治理提供协议。

网络中有三个主要参与者:

  • 客户端合约:需要外部实体来为其维护任务提供服务的智能合约。

  • Keepers:执行注册维护的 Chainlink 节点。

  • Registry:任何人都可以通过它创建和管理维护,节点运营商可以执行维护的合约。

下图描述了 Keeper 网络的架构。它负责管理网络上的参与者并补偿 Keepers 以执行成功的维护。客户可以注册维护,节点运营商可以注册为守护者。

post image
  • 实际开发

需要我们部署的合约支持chainlink keepers协议功能,我们只需要为我们的合约提供两个公开的合约接口,用于第三方的维护程序进行调用编程。

首先我们需要导入KeeperCompatible.sol头文件,并且继承KeeperCompatibleInterface:

pragma solidity ^0.7.0;

// KeeperCompatible.sol imports the functions from both ./KeeperBase.sol and
// ./interfaces/KeeperCompatibleInterface.sol
import "@chainlink/contracts/src/v0.7/KeeperCompatible.sol";

contract Counter is KeeperCompatibleInterface {
}

KeeperCompatibleInterface为我们提供了两个接口checkUpkeepperformUpkeep

Keeper 节点运行此方法是eth_call为了确定您的合约是否需要完成一些工作。如果您的链下模拟checkUpkeep确认满足您的预定义条件,则 Keeper 将向区块链广播执行performUpkeep方法。

function checkUpkeep(bytes calldata checkData)external returns (
    bool upkeepNeeded,
    bytes memory performData
  );

参数

checkData 检查维护时传递给合同的数据。在维护注册中指定,因此注册的维护始终相同。

upkeepNeeded:指示 Keeper 是否应该呼叫performUpkeep

performDataperformUpkeep如果需要维护,Keeper 应该调用的字节。如果您想对数据进行编码以便稍后解码,请尝试abi.encode.

区块上会根据upkeepNeeded的返回值来确定是否执行performUpkeep ,如果需要执行就将返回的 performData传递给performUpkeep。由performUpkeep中对数据进行处理。

function performUpkeep(bytes calldata performData) external;

当然我们可以用我们可以用枚举的方式在函数中实现各种维护的业务:

post image
  • API参考

https://docs.chain.link/docs/chainlink-keepers/compatible-contracts/

我们可以运营 Chainlink 节点成为 Chainlink 网络的一部分,帮助开发人员构建混合智能合约,让他们能够访问真实世界的数据和服务。

具体操作请参考官方文档-Node Operators:

https://docs.chain.link/chainlink-nodes/