撸一个预言机(Oracle)服务,真香!—上篇

本文将通过上、中、下三篇文章带领大家一步步开发实现一个自己中心化的Oracle服务,并通过抽奖合约演示如何使用

# 一、文章结构 本文将通过上、中、下三篇文章带领大家一步步开发实现一个中心化的Oracle服务,并通过一个抽奖合约演示如何使用我们的Oracle服务。文章内容安排如下: - 上篇:**Oracle简介及合约实现** - 中篇:[使用go语言开发Oracle服务](https://learnblockchain.cn/article/1157) - 下篇:[抽奖合约调用Oracle服务示例](https://learnblockchain.cn/article/1162) # 一、Oracle简介 Oracle(预言机)是链接链上与链下的桥梁,能够将链下数据推送给链上。正是由于Oracle的存在,使得区块链从封闭走向开放,充满无限可能。 如需了解Oracle基础知识,这里推荐阅读孙孝虎的[《什么是区块链预言机(BlockChain Oracle)》](https://learnblockchain.cn/2019/03/11/f2266ccd563b) Oracle服务分为中心化和去中心化,其核心区别是对数据的获取和审核上。去中心化的Oracle服务会有一套机制能够保障推送给用户合约的数据是可信的。而无论是中心化还是去中心化,用户合约调用Oracle合约和Oracle服务将获取到的结果数据推送给用户合约的底层逻辑都是一样的。 一个完整的**中心化Oracle服务**请求流程为: 1. 用户合约调用Oracle合约的查询方法 2. Oracle合约接收到用户查询请求后将相关数据写入Event事件中 3. Oracle服务(后台服务)通过订阅Oracle合约的Event事件,获取到用户的请求 4. Oracle服务根据用户请求获取外部数据 5. Oracle服务调用Oracle合约响应方法,传入获取的外部数据 6. Oracle合约响应方法调用用户合约的回调方法,将数据传递给用户合约 7. 用户合约收到Oracle合约传递的数据,继续自己的业务。 整体流程如下图所示。 ![Chainlinkfundamental2.png](https://img.learnblockchain.cn/attachments/2020/02/gmj50Fo25e4a8895e7549.png) > 图片来源于文章《Chainlink预言机基本原理》:https://learnblockchain.cn/article/587 # 三、Oracle合约 通过上面对Oracle服务流程的分析,总结到一个Oracle合约至少需要包含两个方法和一个事件: - 能够接收用户合约请求的方法 - 能够回调用户合约的方法 - 能够供Oracle服务订阅的用户请求事件 接下来,我将实现一个通用的Oracle合约。 ## 1. 能够接收用户合约请求的方法 ``` /** * @dev 接收客户端请求 * @param queryId 请求id,回调时原值返回 * @param callbackAddr 回调的合约地址 * @param callbackFUN 回调合约的方法及参数,如getResponse(bytes32,uint64,uint256/bytes), * 其中getResponse表示回调方法名,可自定义; * bytes32类型参数指请求id,回调时会原值返回; * uint64类型参数表示oracle服务状态码,1表示成功,0表示失败; * 第三个参数表示Oracle服务回调支持uint256/bytes两种类型的参数 * @param queryData 请求数据,json格式,如{"url":"https://ethgasstation.info/api/ethgasAPI.json","responseParams":["fast"]} * @return bool true请求成功,false请求失败 */ function query(bytes32 queryId, address callbackAddr, string calldata callbackFUN, bytes calldata queryData) external payable returns(bool) { require(msg.value >= MIN_FEE, "Insufficient handling fee!"); require(bytes(callbackFUN).length > 0, "Invalid callbackFUN!"); require(queryData.length > 0, "Invalid queryData!"); // 记录日志 emit QueryInfo(queryId, msg.sender, msg.value, callbackAddr, callbackFUN, queryData); return true; } ``` 需要说明的地方: - 用户合约会多次请求Oracle服务,获取数据, `queryId `请求ID参数可以让用户合约对请求做标识。 - 让用户传`callbackAddr`回调地址参数,而不是直接通过`msg.sender`获取调用者地址,是考虑到调用Oracle合约(付费方)和接收数据方有可能不是一个地址。 - 对于用户请求的数据类型,本文目前实现了uint256和bytes两种类型的回调。 - 考虑到通用性,用户请求的数据来源由用户自定义。如果是一个专类的Oracle服务(如只提供随机数服务),可以不需要请求数据字段。 - 考虑到节省用户的请求费用,加之本身就是一个中心化的Oracle服务,不存在作弊问题,因此`query`方法并没有更改任何状态变量,用户请求数据直接写入到日志中。 ## 2. 能够回调用户合约的方法 ``` /** * @dev 将查询得到的结果(bytes类型)发送给客户端 * @param queryId 查询请求id * @param callbackAddr 回调的合约地址 * @param callbackFUN 回调合约的方法及参数 * @param stateCode 查询结果状态码,1表示查询成功,0表示失败 * @param respData 查询结果 * @return bool true请求成功,false请求失败 */ function responseBytes(bytes32 queryId, address callbackAddr, string calldata callbackFUN, uint64 stateCode, bytes calldata respData) payable external isOwner returns(bool) { require(address(this).balance > CALLBACK_GAS, "Insufficient balance!"); (bool success,) = callbackAddr.call.gas(CALLBACK_GAS)(abi.encodeWithSignature(callbackFUN, queryId, stateCode, respData)); require(success,"call back failed!"); } /** * @dev 将查询得到的结果(uint256类型)发送给客户端 */ function responseUint256(bytes32 queryId, address callbackAddr, string calldata callbackFUN, uint64 stateCode, uint256 respData) payable external isOwner returns(bool) { require(address(this).balance > CALLBACK_GAS, "Insufficient balance!"); (bool success,) = callbackAddr.call.gas(CALLBACK_GAS)(abi.encodeWithSignature(callbackFUN, queryId, stateCode, respData)); require(success); } ``` ## 3. 能够供Oracle服务订阅的用户请求事件 事件将用户请求的相关参数都记录下来,Oracle服务通过订阅该事件,一旦有用户请求时,Oracle服务就能够获取到用户的请求数据。 ``` // 查询事件,oracle后端服务会订阅该事件 event QueryInfo(bytes32 queryId, address requester, uint fee, address callbackAddr, string callbackFUN, bytes queryData); ``` > 完整代码地址:[https://github.com/six-days/ethereum-contracts/blob/master/oracle/Oracle.sol](https://github.com/six-days/ethereum-contracts/blob/master/oracle/Oracle.sol) 下篇我们将介绍Oracle服务(后端服务)如何订阅查询事件以及将获取到的数据返回给合约,具体实现代码将以golang语言给出。

一、文章结构

本文将通过上、中、下三篇文章带领大家一步步开发实现一个中心化的Oracle服务,并通过一个抽奖合约演示如何使用我们的Oracle服务。文章内容安排如下:

  • 上篇:Oracle简介及合约实现
  • 中篇:使用go语言开发Oracle服务
  • 下篇:抽奖合约调用Oracle服务示例

一、Oracle简介

Oracle(预言机)是链接链上与链下的桥梁,能够将链下数据推送给链上。正是由于Oracle的存在,使得区块链从封闭走向开放,充满无限可能。

如需了解Oracle基础知识,这里推荐阅读孙孝虎的《什么是区块链预言机(BlockChain Oracle)》

Oracle服务分为中心化和去中心化,其核心区别是对数据的获取和审核上。去中心化的Oracle服务会有一套机制能够保障推送给用户合约的数据是可信的。而无论是中心化还是去中心化,用户合约调用Oracle合约和Oracle服务将获取到的结果数据推送给用户合约的底层逻辑都是一样的。

一个完整的中心化Oracle服务请求流程为:

  1. 用户合约调用Oracle合约的查询方法
  2. Oracle合约接收到用户查询请求后将相关数据写入Event事件中
  3. Oracle服务(后台服务)通过订阅Oracle合约的Event事件,获取到用户的请求
  4. Oracle服务根据用户请求获取外部数据
  5. Oracle服务调用Oracle合约响应方法,传入获取的外部数据
  6. Oracle合约响应方法调用用户合约的回调方法,将数据传递给用户合约
  7. 用户合约收到Oracle合约传递的数据,继续自己的业务。

整体流程如下图所示。

撸一个预言机(Oracle)服务,真香!—上篇插图

图片来源于文章《Chainlink预言机基本原理》:https://learnblockchain.cn/article/587

三、Oracle合约

通过上面对Oracle服务流程的分析,总结到一个Oracle合约至少需要包含两个方法和一个事件:

  • 能够接收用户合约请求的方法
  • 能够回调用户合约的方法
  • 能够供Oracle服务订阅的用户请求事件

接下来,我将实现一个通用的Oracle合约。

1. 能够接收用户合约请求的方法

 /**
 * @dev 接收客户端请求
 * @param queryId 请求id,回调时原值返回
 * @param callbackAddr 回调的合约地址
 * @param callbackFUN 回调合约的方法及参数,如getResponse(bytes32,uint64,uint256/bytes),
 *        其中getResponse表示回调方法名,可自定义;
 *        bytes32类型参数指请求id,回调时会原值返回;
 *        uint64类型参数表示oracle服务状态码,1表示成功,0表示失败;
 *        第三个参数表示Oracle服务回调支持uint256/bytes两种类型的参数
 * @param queryData 请求数据,json格式,如{"url":"https://ethgasstation.info/api/ethgasAPI.json","responseParams":["fast"]}
 * @return bool true请求成功,false请求失败
 */
function query(bytes32 queryId, address callbackAddr, string calldata callbackFUN, bytes calldata queryData) external payable returns(bool) {
    require(msg.value >= MIN_FEE, "Insufficient handling fee!");
    require(bytes(callbackFUN).length > 0, "Invalid callbackFUN!");
    require(queryData.length > 0, "Invalid queryData!");
    // 记录日志
    emit QueryInfo(queryId, msg.sender, msg.value, callbackAddr, callbackFUN, queryData);
    return true;
}

需要说明的地方:

  • 用户合约会多次请求Oracle服务,获取数据, queryId请求ID参数可以让用户合约对请求做标识。
  • 让用户传callbackAddr回调地址参数,而不是直接通过msg.sender获取调用者地址,是考虑到调用Oracle合约(付费方)和接收数据方有可能不是一个地址。
  • 对于用户请求的数据类型,本文目前实现了uint256和bytes两种类型的回调。
    • 考虑到通用性,用户请求的数据来源由用户自定义。如果是一个专类的Oracle服务(如只提供随机数服务),可以不需要请求数据字段。
  • 考虑到节省用户的请求费用,加之本身就是一个中心化的Oracle服务,不存在作弊问题,因此query方法并没有更改任何状态变量,用户请求数据直接写入到日志中。

2. 能够回调用户合约的方法

/**
 * @dev 将查询得到的结果(bytes类型)发送给客户端
 * @param queryId 查询请求id
 * @param callbackAddr 回调的合约地址
 * @param callbackFUN 回调合约的方法及参数
 * @param stateCode 查询结果状态码,1表示查询成功,0表示失败
 * @param respData 查询结果
 * @return bool true请求成功,false请求失败
 */
function responseBytes(bytes32 queryId, address callbackAddr, string calldata callbackFUN, uint64 stateCode, bytes calldata respData) payable external isOwner returns(bool) {
    require(address(this).balance > CALLBACK_GAS, "Insufficient balance!");
    (bool success,) = callbackAddr.call.gas(CALLBACK_GAS)(abi.encodeWithSignature(callbackFUN, queryId, stateCode, respData));
    require(success,"call back failed!");
}

/**
 * @dev 将查询得到的结果(uint256类型)发送给客户端
 */
function responseUint256(bytes32 queryId, address callbackAddr, string calldata callbackFUN, uint64 stateCode, uint256 respData) payable external isOwner returns(bool) {
    require(address(this).balance > CALLBACK_GAS, "Insufficient balance!");
    (bool success,) = callbackAddr.call.gas(CALLBACK_GAS)(abi.encodeWithSignature(callbackFUN, queryId, stateCode, respData));
    require(success);
}

3. 能够供Oracle服务订阅的用户请求事件

事件将用户请求的相关参数都记录下来,Oracle服务通过订阅该事件,一旦有用户请求时,Oracle服务就能够获取到用户的请求数据。

// 查询事件,oracle后端服务会订阅该事件
event QueryInfo(bytes32 queryId, address requester, uint fee, address callbackAddr, string callbackFUN, bytes queryData);

完整代码地址:https://github.com/six-days/ethereum-contracts/blob/master/oracle/Oracle.sol

下篇我们将介绍Oracle服务(后端服务)如何订阅查询事件以及将获取到的数据返回给合约,具体实现代码将以golang语言给出。

区块链技术网。

  • 发表于 2020-06-16 22:01
  • 阅读 ( 2518 )
  • 学分 ( 301 )
  • 分类:以太坊

评论