通过代码实践 Chainlink 预言机网络的搭建,并使用预置或自定义的适配器实现智能合约与外部世界的数据交互。

原文标题:《Chainlink 去中心化预言机桥接区块链与现实世界》(Bridging Blockchain to the Real World using Chainlink)
撰文:Fang Gong

Chainlink 是一个去中心化的预言机网络,它可以让区块链中的智能合约安全地 访问外部世界的数据。在这个文章中,我们将探索 chainlink 预言机网络的搭建,并学习如何使用预置或自定义的适配器实现智能合约与外部世界数据的桥接。

智能合约被锁定在区块链里,与外部世界隔离开来。然而在许多应用中,智能合约的运行需要依赖于外部真实世界的信息。

以 Ocean 协议为例:只有当提供的数据被证明是可以使用时,数据提供商才可以得到代币奖励。因此一个可以桥接区块链和现实世界的预言机(Oracle)网络 就非常必要了。

Chainlink 是一个去中心化的 Oracle 网络,它可以让区块链中的智能合约安全地访问外部世界的数据:

[译]Chainlink 去中心化预言机桥接区块链与现实世界

在这个教程中,我们将探索 chainlink 网络的搭建以及其适配器的使用方法,我们 在 Kovan 测试链搭建了一个用于概念验证的演示环境,所有的代码可以从 这里 1 下载。我们使用 truffle v5.0.3 和 Node.js v8.11.1。

Chainlink 网络的主要组成部分如下:

  • Chainlink 预言机合约:预言机智能合约部署在区块链网络上,它接收来自合约的 Link 代币支付并向 Chainlink 节点分发事件
  • Chainlink 节点:Chainlink 节点是运行在区块链和外部数据源之间的链下中间件, 它提供真实世界的数据,执行来自请求器合约的请求
  • Chainlink 适配器:适配器是应用相关的软件,它负责与数据源交互并执行特定的任务。chainlink 适配器可以部署在 serverless 平台,例如 amazon lambda 或 Google cloud functions

[译]Chainlink 去中心化预言机桥接区块链与现实世界

值得指出的是,每个来自请求器合约的请求都必须包含一个任务 ID,用来唯一的标识 一个特定的工作流水线。Chainlink 节点依赖于任务 ID 来识别与数据源交互所需的适配器 以及处理数据所需的工作流。

在这一部分,我们使用 Chainlinkg 预置的适配器来展示如何集成 Chainlink 并向其 提交请求。

在项目根目录,执行如下命令安装 chainlink 包:

    $ npm install github:smartcontractkit/chainlink --save

另外,Chainlink 官方最近增加了一个新的 NPM 包用于 Chainlink 合约,可以如下 命令安装:

    $ npm install chainlink.js — save

2.2 在 Kovan 测试链部署请求器合约

要访问 Chainlink 的预言机合约,需要构造一个用于发送 Link 代币并提交请求的请求器合约。

我们创建了一个请求器合约示例,可以在这里下载。

constructor() public {    
       // Set the address for the LINK token in Kovan network.    
       setLinkToken(0xa36085F69e2889c224210F603D836748e7dC0088);   
       // Set the address of the Oracle contract in Kovan network. 
       setOracle(0x2f90A6D021db21e1B2A077c5a37B3C7E75D15b7e); }...  

       /*  
       * Create a request and send it to default Oracle contract  
       */  
      function createRequest(    
      bytes32_jobId,    
      string_url,    
      string_path,    
      int256_times
      )    

      public    
      onlyOwner    
      returns (bytes32 requestId)  
      {    
      // create request instance    
      Chainlink.Request memory req = newRequest(_jobId, this, this.fulfill.selector);    
      // fill in the pass-in parameters  

       req.add("url",_url);    
       req.add("path",_path);    
       req.addInt("times",_times);    
       // send request & payment to Chainlink oracle   
       requestId = chainlinkRequestTo(getOracle(), req, ORACLE_PAYMENT);           
       // emit event message    
       emit requestCreated(msg.sender,_jobId, requestId);  }  

请求器合约中的关键函数是 createRequest 函数,它创建请求并设置必要的参数:

  • Job Id:特定作业流水线的唯一标识符。可以在这里查看内置适配器的完整清单:https://docs.chain.link/docs/addresses-and-job-specs
  • URL:可以返回 JSON 数据的 Web API 的访问端结点
  • path:JSON 数据字段选择路径,用来声明使用数据中的哪一部分
  • times:数据倍乘系数。该操作将浮点数转换为整数,因为 solidity 智能合约仅接受整数

2.3 在 Kovan 测试链部署请求器合约

执行如下命令在以太坊 Kovan 测试链部署请求器合约:

   $ truffle migrate --network kovan    
    ...   Deploying 'OceanRequester'   
    --------------------------   
    > transaction hash: 0x6e228163e73828c58c8287fec72c551289516a1d8e9300aab5dcc99d848f6146   
    >  Blocks: 0            Seconds: 16   
    > contract address:    0x04E4b02EA2662F5BF0189912e6092d317d6388F3   
    > account:             0x0E364EB0Ad6EB5a4fC30FC3D2C2aE8EBe75F245c   
    > balance:             2.703082875853937168   
    > gas used:            1439461   
    > gas price:           10 gwei   
    > value sent:          0 ETH   
    > total cost:          0.01439461 ETH   

    > Saving artifacts  

    -------------------------------------  

   > Total cost:          0.01439461 ETH  

Chainlink 官方提供了一些代币 faucet。在 Kovan 测试链上可以访问 https://kovan.chain.link/ 获取一些测试用的 LINK 代币。

只需要输入合约地址或钱包地址,Chainlink 的 faucet 就会转 100 个 LINK 代币进去:

[译]Chainlink 去中心化预言机桥接区块链与现实世界

2.5 从合约请求数据

我们创建了一个 JavaScript 脚本来与请求器合约交互,以便创建并提交请求给 Chainlink 网络。可以在这里下载 JavaScript 脚本。

    contract("OceanRequester", (accounts) => {  
    const LinkToken = artifacts.require("LinkToken.sol");  
    const OceanRequester = artifacts.require("OceanRequester.sol");  
    const jobId = web3.utils.toHex("2c6578f488c843588954be403aba2deb");  
    const url = "https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms;=USD,EUR,JPY";  
    const path = "USD"; 
    const times = 100;  
    let link, ocean;  

    beforeEach(async () => {  

        link = await LinkToken.at("0xa36085F69e2889c224210F603D836748e7dC0088");    
        ocean = await OceanRequester.at("0x04E4b02EA2662F5BF0189912e6092d317d6388F3");  

    });  

   describe("query the initial token balance", () => {
       it("create a request and send to Chainlink", async () => {      
       let tx = await ocean.createRequest(jobId, url, path, times);      
       request = h.decodeRunRequest(tx.receipt.rawLogs[3]);      
       ...      
       data = await ocean.getRequestResult(request.id)      
       console.log("Request is fulfilled. data := " + data)      
       ...   
       });   
    });
 });  

上面的代码中,关键参数已经加粗显式。任务 ID「2c6578f488c843588954be403aba2deb」 标识了用于从 URL 提取 JSON 数据、拷贝指定字段值并转换化为 SOlidity 支持的 uint256 类型的 Chainlink 适配器。

例如,返回的 JSON 格式数据看起来像这样:

    {USD":142.33,"EUR":126.69,"JPY":15765.39}

path 参数设置为 USD 表示该字段的值需要提供给请求器合约。

我们可以运行该脚本像 Chainlinkg 网络提交请求并从指定的 URL 提取数据。Chainlinkg 节点大概需要 2 秒钟来执行该请求,其中包含区块确认的时间。

[译]Chainlink 去中心化预言机桥接区块链与现实世界

前面的部分看起来干净简洁。但是,Chainlink 内置的适配器很有限,不能 满足各种区块链应用的要求。因此,需要为不同的应用场景创建定制的适配器。

在这一部分,我们学习如何开发自己的适配器,并学习如何将其嵌入 Chainlink 体系中。可以在这里 2 找到一些外部适配器 的参考实现,或者查看这里的指南 [3]。

下图展示了包含外部适配器的 Chainlink 网络架构:

[译]Chainlink 去中心化预言机桥接区块链与现实世界

区块链开发者需要完成以下工作:

  • 将预言机合约部署到区块链网络
  • 开发定制适配器并部署到 AWS lambda 或 GCP functions,提供用于交互的 URL 端结点
  • 运行一个新的 CHainlink 节点并在该节点的配置中注册定制的适配器的 URL 端结点
  • 在 Chainlink 节点中为该任务创建一个任务描述,以便其监听预言机合约并触发正确 的工作流水线
  • 在链上预言机合约中注册新的 CHainlink 节点
  • 创建一个新的请求器合约来提交请求

下面我们逐步来实现。

3.1 在 Kovan 测试链部署预言机合约

在我们的概念验证系统中,需要一个预言机合约与 Chainlinkg 节点交互。为此,我们 在 Kovan 测试链上部署这个合约:https://github.com/oceanprotocol/Nautilus/blob/master/4-chainlink

    3_oracle_migration.js
    =====================   
    Deploying 'Oracle'   
    ------------------   
    > transaction hash:    0xd281b18c4be0be9b2bdbfed4bae090aab5c86027564f048785b1f971cf0b6f2c   
    > Blocks: 0            Seconds: 8   
    > contract address:    0x698EFB00F79E858724633e297d5188705512e506   
    > account:             0x0E364EB0Ad6EB5a4fC30FC3D2C2aE8EBe75F245c   
    > balance:             2.262907885853937168   
    > gas used:            1311430   
    > gas price:           10 gwei   
    > value sent:          0 ETH   
    > total cost:          0.0131143 ETH   
    > Saving artifacts  
     -------------------------------------   
    > Total cost:           0.0131143 ETH  

3.2 创建一个新的外部适配器

在这个概念验证系统中,我们使用一个由 OracleFinder 开发的外部适配器 CryptoCompareExternalAdapter。对应更一般性的应用,Thomas Hodges 创建了一个用 NodeJS 开发的外部适配器模板:https://github.com/thodges-gh/CL-EA-NodeJS-Template

    $ git clone https://github.com/OracleFinder/CryptoCompareExternalAdapter.git
    $ cd CryptoCompareExternalAdapter/
    $ npm install
    $ zip -r chainlink-cloud-adapter.zip .

压缩文件 chainlink-cloud-adapter.zip 创建后就可以部署了。作为示例,我们将 这个外部适配器部署到 Google Cloud Functions。在登录之后,参考下图创建一个 新的函数并上传 chainlink-cloud-adapter.zip:

[译]Chainlink 去中心化预言机桥接区块链与现实世界

为这个外部适配器生成的 URL 访问端结点需要提供给 chainlink 节点:

    https://us-central1-macro-mercury-234919.cloudfunctions.net/coinmarketcap-adapter

现在使用 Google Cloud Functions 的控制台,我们可以测试适配器以确保它可以 正常运行:

[译]Chainlink 去中心化预言机桥接区块链与现实世界

现在,外部适配器已经在 Google Cloud 平台运行起来,它等待执行来自 Chainlink 节点的请求。

我们需要运行一个新的 chainlink 节点,以便可以访问外部适配器,步骤如下:

安装 Parity 并接入 Kovan 网络:

    $ docker pull parity/parity:stable
    $ mkdir ~/.parity-kovan
    $ docker run -h eth --name eth -p 8546:8546 \ 
             -v ~/.parity-kovan:/home/parity/.local/share/io.parity.ethereum/ \ 
             -it parity/parity:stable --chain=kovan \ 
             --ws-interface=all --ws-origins="all" --light \
             --base-path /home/parity/.local/share/io.parity.ethereum/

创建 Chainlink 节点的管理账号

    $ docker pull smartcontract/chainlink:latest  
    $ mkdir -p ~/.chainlink-kovan/tls  
    $ openssl req -x509 -out ~/.chainlink-kovan/tls/server.crt -keyout ~/.chainlink-kovan/tls/server.key \  
    -newkey rsa:2048 -nodes -sha256 \  
    -subj '/CN=localhost' -extensions EXT -config <( \  
    printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")  

创建 Chainlink 节点的配置信息

 echo "ROOT=/chainlink
 LOG_LEVEL=debug
 ETH_URL=ws://eth:8546 ETH_CHAIN_ID=42
 MIN_OUTGOING_CONFIRMATIONS=2
 MIN_INCOMING_CONFIRMATIONS=0
 LINK_CONTRACT_ADDRESS=0xa36085F69e2889c224210F603D836748e7dC0088
 TLS_CERT_PATH=/chainlink/tls/server.crt
 TLS_KEY_PATH=/chainlink/tls/server.key
 ALLOW_ORIGINS=*" > .env

运行 chainlink 节点

    $ docker run --link eth -p 6689:6689 -v ~/.chainlink-kovan:/chainlink -it --env-file=.env smartcontract/chainlink n

访问 https://localhost:6689 打开 Chainlink 节点的 GUI 配置界面,使用 前面创建的管理账号登入,仪表盘看起来像这样:

[译]Chainlink 去中心化预言机桥接区块链与现实世界

在 chainlink 节点中注册外部适配器

在 Bridges 选项卡,我们需要创建一个新的桥接器并填写桥接 url:

[译]Chainlink 去中心化预言机桥接区块链与现实世界

结果看起来是这样:

[译]Chainlink 去中心化预言机桥接区块链与现实世界

3.4 为外部适配器创建任务描述

在 chainlink 节点上,很重要的一个步骤是创建一个新的任务描述,参考:https://docs.chain.link/docs/job-specifications

有了任务描述,Chainlink 节点可以监听来自预言机合约的事件消息并触发 在任务描述中定义的流水线。我们的任务描述:

    {  
        "initiators": [   
            {      
            "type": "RunLog",      
            "params": { "address": "0x698efb00f79e858724633e297d5188705512e506" }  
            } 
         ],  

      "tasks": [    
      {      
           "type": "coinmarketcap",     
         "confirmations": 0,      
          "params": {}  
      },   
      {     
         "type": "Copy",     
         "params": {}   
       },   
       {      
             "type": "Multiply", 
            "params": { "times": 100 }  
       },    
       {     "type": "EthUint256" },  
       {    "type": "EthTx" }  ]  
    }  

initiators 用来设置触发 chainlink 节点的合约地址, tasks 定义了该任务的 作业流水线。

新的 Chainlink 节点必须要在之前部署的预言机合约中注册,这样它才能接受 请求并执行任务。

可以在 Chainlink 节点的配置页面找到新的 chainlink 节点的账户地址:

[译]Chainlink 去中心化预言机桥接区块链与现实世界

我们使用一个 JavaScript 文件来注册该 Chainlink 节点:

    const Web3 = require('web3')
    const web3 = new Web3(new Web3.providers.HttpProvider('https://kovan.infura.io/'))
    const h = require("chainlink-test-helpers");const scale = 1e18;  

    contract("Oracle", (accounts) => {  

      const Oracle = artifacts.require("Oracle.sol");  
      const chainlinkNode ='0x79B80f3b6B06FD5516146af22E10df26dfDc5455';  
      let oracle;  

      beforeEach(async () => {    
            oracle = await Oracle.at("0x698EFB00F79E858724633e297d5188705512e506");  
      });  

      describe("should register chainlink node", () => {   
            it("register chainlink node", async () => {      
                 await oracle.setFulfillmentPermission(chainlinkNode, true)      
                 let status = await oracle.getAuthorizationStatus(chainlinkNode)      
                 console.log("Chainlink node's status is := " + status)  
                 }); 
             });
         });

使用 truffle 运行上述脚本:

    $ truffle test test/Oracle.Test.js --network kovan
    Using network 'kovan'   Contract: Oracle       
        should register chainlink node
    Chainlink node's status is := true

状态为 true 表示 chainlink 节点已经注册成功,并被授予执行任务的权限。

3.6 创建请求器合约以提交请求

为了向外部适配器提交请求,我们创建一个请求器合约 contracts/requestGCP.sol 来 测试整个工作流。

    function createRequest(    
        bytes32_jobId,    
        string_coin,    
        string_market  )  

        public   
        onlyOwner    
        returns (bytes32 requestId)  
    {  
        // create request instance    
        Chainlink.Request memory req = newRequest(_jobId, this, this.fulfill.selector);   
        // fill in the pass-in parameters    
        req.add("endpoint", "price");    
        req.add("fsym",_coin);    
        req.add("tsyms",_market);    
        req.add("copyPath",_market);    
        // send request & payment to Chainlink oracle (Requester Contract sends the payment)    
        requestId = chainlinkRequestTo(getOracle(), req, ORACLE_PAYMENT);   
        // emit event message    
        emit requestCreated(msg.sender,_jobId, requestId); 

     }  

同样部署到 Kovan 测试链:

    4_requestGCP_migration.js  
    =========================   
    Replacing 'requestGCP'  
       ----------------------  
       > transaction hash:    0x978974b43d843606c42ce15c87fcc560a5c625497bf074f5ec0f337347438fdf   
       > Blocks: 0            Seconds: 16   
       > contract address:    0x6f73E784253aD72F0BA4164101860992dFC17Fe1  
       > account:             0x0E364EB0Ad6EB5a4fC30FC3D2C2aE8EBe75F245c   
       > balance:             2.248942845853937168   
       > gas used:            1396504   
       > gas price:           10 gwei   
       > value sent:          0 ETH   
       > total cost:          0.01396504 ETH   
       > Saving artifacts  

      ------------------------------------   

       > Total cost:          0.01396504 ETH  

利用 faucet 充值一些 link 代币:https://kovan.chain.link/

现在,我们可以请求外部适配器来访问链下数据。可以使用如下脚本:

    const Web3 = require('web3')  
    const web3 = new Web3(new Web3.providers.WebsocketProvider('ws://eth:8546'))  

    const h = require("chainlink-test-helpers");
    const scale = 1e18;  

    contract("requestGCP", (accounts) => {  
    const LinkToken = artifacts.require("LinkToken.sol");  
    const RequestGCP = artifacts.require("requestGCP.sol");  
    const jobId = web3.utils.toHex("80c7e6908e714bf4a73170c287b9a18c");  
    const coin = "ETH"  const market = "USD";  
    const defaultAccount =0x0e364eb0ad6eb5a4fc30fc3d2c2ae8ebe75f245c;  
    let link, ocean;  

      beforeEach(async () => {    
            link = await LinkToken.at("0xa36085F69e2889c224210F603D836748e7dC0088");    
            ocean = await RequestGCP.at("0x6f73E784253aD72F0BA4164101860992dFC17Fe1"); 
     });  

      describe("should request data and receive callback", () => {   
          let request;    ...   
           it("create a request and send to Chainlink", async () => {     
             let tx = await ocean.createRequest(jobId, coin, market);      
              request = h.decodeRunRequest(tx.receipt.rawLogs[3]);      
             console.log("request has been sent. request id :=" + request.id)  

              let data = 0      
              let timer = 0     
              while(data == 0){        
                  data = await ocean.getRequestResult(request.id)       
                   if(data != 0) {          
                  console.log("Request is fulfilled. data := " + data)        }        
                 wait(1000)        
                  timer = timer + 1        
                  console.log("waiting for " + timer + " second")    
                    }   
            });  
       });
   });  

用 truffle 运行该脚本:

    truffle test test/requestGCP.Test.js --network kovan

运行了大约 10 秒钟,外部适配器完成该任务:

[译]Chainlink 去中心化预言机桥接区块链与现实世界

可以在 chainlink 节点的交易历史中找到该交易:

[译]Chainlink 去中心化预言机桥接区块链与现实世界

也可以在 Google cloud functions 的仪表盘中找到该交易:

[译]Chainlink 去中心化预言机桥接区块链与现实世界

4、结语

Chainlink 是一个重要的桥接区块链与现实世界的去中心化预言机网络。许多 区块链应用可能都需要通过 chainlink 网络来访问现实世界中的数据流。

References

[1] 这里 :
https://github.com/oceanprotocol/Nautilus/tree/master/4-chainlink

[2] 这里 :
https://chainlinkadapters.com/

[3] 指南 :
https://chainlinkadapters.com/guides

[4] Bridging Blockchain to the Real World using Chainlink:
https://blog.oceanprotocol.com/bridging-blockchain-to-the-real-world-using-chainlink-afcf19cfbb73

来源链接:blog.oceanprotocol.com