24

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

 3 years ago
source link: https://learnblockchain.cn/article/1150
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

一、文章结构

本文将通过上、中、下三篇文章带领大家一步步开发实现一个中心化的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合约传递的数据,继续自己的业务。

整体流程如下图所示。

rmyEBfj.png!web

图片来源于文章《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语言给出。

一、文章结构

本文将通过上、中、下三篇文章带领大家一步步开发实现一个中心化的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合约传递的数据,继续自己的业务。

整体流程如下图所示。

rmyEBfj.png!web

图片来源于文章《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语言给出。

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 16分钟前
  • 阅读 ( 3 )
  • 学分 ( 0 )
  • 分类:以太坊

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK