首页 > 世链号 > 撸一个预言机(Oracle)服务,真香
币小葱  

撸一个预言机(Oracle)服务,真香

摘要:Oracle 后端服务整体包含事件订阅模块、查询模块和回调模块

一、文章结构

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

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

•在上篇中,我们实现了一个通用的 Oracle 合约,其主要有一个接收用户请求的 Query 方法;回调用户合约的 Response 方法和一个供 Oracle 后端服务订阅的 QueryInfo 事件。

•本篇是中篇,主要使用 go 语言开发实现 Oracle 的后端服务。

文中的 Oracle 服务完整代码地址:https://github.com/six-days/ethereum-oracle-service

二、服务架构

Oracle 后端服务整体包含事件订阅模块查询模块回调模块,架构如下图所示。

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

服务开启后,首先会通过以太坊 ws 协议的 jsonrpc,在区块链上注册事件订阅,订阅成功后开启一个 for 循环,接收并处理事件消息。

代码如下所示。

 // start monitor oracle contract eventfunc (e *EventWatch) Start() { if err := e.subscribeEvent(); err != nil { return } e.dealEvent()} func (e *EventWatch) dealEvent() { for { select { case err := <-e.Subscription.Err(): logs.Error("[dealEvent] Subscription err: ", err) e.subscribeEvent() case vLog := <-e.EventChan: // 处理查询请求并回调 go e.dealQuery(vLog) } }} 

三、事件订阅

事件订阅必须使用 ws 协议的 jsonrpc,http 协议的 jsonprc 无法订阅事件。

事件订阅的核心是通过 ethclient 的 SubscribeFilterLogs 方法,其中 query 参数是订阅的过滤条件。其中

Addresses 是 Oracle 合约地址;•Topics 参数是过滤主题,是一个二维数组,这里我们的主题只指定了事件的名称。

代码如下所示。

 * 
 func (e *EventWatch) subscribeEvent() error { query := ethereum.FilterQuery{ Addresses: []common.Address{ common.HexToAddress(e.Config.OracleContractAddress), }, Topics: [][]common.Hash{ {e.OracleABI.Events[OracelEventName].ID()}, }, } events := make(chan types.Log) sub, err := e.Client.SubscribeFilterLogs(context.Background(), query, events) if err != nil { logs.Error("[SubscribeEvent]fail to subscribe event:", err) return err } e.EventChan = events e.Subscription = sub return nil} 

四、查询模块

1、日志解析

事件日志解析我们用 go-ethereum 的 abi 模块的 Unpack 方法,将日志解析为我们定义好的结构体。

代码如下所示。

 type OracleQueryInfo struct { QueryId [32]byte Requester common.Address Fee *big.Int CallbackAddr common.Address CallbackFUN string QueryData []byte Raw types.Log // Blockchain specific contextual infos} type QueryRequest struct { URL string `json:"url,omitempty"` ResponseParams []string `json:"responseParams,omitempty"`} func (e *EventWatch) dealQuery(vLog types.Log) error { queryInfo := &OracleQueryInfo;{} err := e.OracleABI.Unpack(queryInfo, OracelEventName, vLog.Data) if err != nil { return fmt.Errorf("[dealQuery] unpack event log failed:%v", err) } reqData := &QueryRequest;{} if err = json.Unmarshal(queryInfo.QueryData, reqData); err != nil { return fmt.Errorf("[dealQuery] unmarshal query data failed:%v", err) }} 

2、查询请求

查询请求比较简单,就是根据用户提供的 url 发送请求。代码如下所示。

 // sendQueryRequest 根据客户端指定的查询地址发送请求 func (e *EventWatch) sendQueryRequest(reqData *QueryRequest, resParamType string) (interface{}, error) { req, err := http.NewRequest("GET", reqData.URL, nil) if err != nil { return nil, fmt.Errorf("[sendQueryRequest] NewRequest failed: %v", err) } res, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("[sendQueryRequest] http get request failed: %v", err) } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { return nil, fmt.Errorf("[sendQueryRequest] read response data failed: %v", err) } logs.Trace("[sendQueryRequest] get ", reqData.URL, " response is: ", string(body)) queryRes, err := ParseResponeData(body, reqData.ResponseParams, resParamType) if err != nil { return nil, err } return queryRes, nil} 

查询可能失败,这里需要增加失败重试机制,代码比较简单,就不写出来了。

3、结果解析

这里使用 go-simplejson 库将查询结果进行 json 解析,并且提取用户指定所需要的字段,将字段转换为用户合约中回调方法接收的数据类型。

 * 
 // ParseResponeData 解析链下获取到的数据,提取用户所需要的字段,并转换为对应的数据类型 func ParseResponeData(repData []byte, keys []string, resParamType string) (interface{}, error) { resData, err := simplejson.NewJson(repData) if err != nil { return nil, fmt.Errorf("[ParseResponeData] unmarshal response data failed:%v", err) } for_, paramName := range keys { resData = resData.Get(paramName) } if resData == nil { return nil, fmt.Errorf("[ParseResponeData] response data not exist request key:%v", keys) } var resValue interface{} var coverErr error switch resParamType { case "uint256": resUint64Value, coverErr := resData.Uint64() if coverErr == nil { resValue = big.NewInt(int64(resUint64Value)) } case "bytes": resValue, coverErr = resData.Bytes() default: return nil, fmt.Errorf("[ParseResponeData] unsupport response data type %s", resParamType) } if coverErr != nil { return nil, fmt.Errorf("[ParseResponeData] response data type %s error:%v", resParamType, err) } return resValue, nil} 

五、回调模块

回调模块相对比较简单,首先将 Oracle 合约实例化了一个 BoundContract 对象,然后调用 Transact 方法发送交易。其中第一个参数是使用私钥实例化的一个 TransactOpts 对象。

在 TransactOpts 对象中可以配置 nonce、gasLimit、gasPrice 等值,如果不指定,Transact 方法会自己补充上。除此之外,Transact 方法也会调用 TransactOpts 对象的 Signer 方法对消息进行签名。

Transact 方法源码详见:https://github.com/six-days/go-ethereum/blob/master/accounts/abi/bind/base.go

回调模块代码如下所示。

 * 
 // sendQueryResponse 将查询到的结果发送给客户端合约指定方法 func (e *EventWatch) sendQueryResponse(res interface{}, stateCode uint64, queryInfo *OracleQueryInfo, resParamType string) error { in := []interface{}{ queryInfo.QueryId, queryInfo.CallbackAddr, queryInfo.CallbackFUN, stateCode, res, } var responseName string switch resParamType { case "bytes": responseName = OracelResponseBytesName case "uint256": responseName = OracelResponseUint256Name default: return fmt.Errorf("[SendQueryResponse] unsupport response data type") } transaction, err := e.BoundContract.Transact(e.TransactOpts, responseName, in...) if err != nil { return fmt.Errorf("[SendQueryResponse] Transact failed: %v", err) } logs.Trace("[SendQueryResponse] call back tx:", transaction.Hash().Hex()) return nil} 

回调也可能失败,服务对 sendQueryResponse 方法的调用也增加了失败重试机制。

六、可以优化的地方

至此,我们的 V1 版的 Oracle 服务已开发完成,服务已能满足基本需求,但还有一些方面需要进一步优化,我这里列出了三点。

1、Nonce 托管

在回调模块中,调用合约时,我们并没有指定发起交易账号的 Nonce 值,而是由 Transact 方法在每次发起交易时,动态计算。这就会限制我们交易的并发。

在高并发的情况下,肯定会出现多笔交易 Nonce 值相同的情况,后发起交易覆盖前交易,造成前交易失败。

针对这种情况,我的思路是对 Nonce 进行托管:

•在缓存(内存或 redis 等)中维护账号对应的 Nonce•每次发起交易时,从缓存中获取,每获取一次,缓存中的 Nonce 累加 1•缓存中的 Nonce 定期和链上进行校对和同步•对于可能出现的空洞情况,使用空交易填补

2、Gas 优化

这段时间以太坊网络比较拥堵,导致手续费居高不下。对于我们 Oracle 服务来说,节省 Gas 是很重要的一个优化方向。

这里我的思路是可以从以下几个方面优化:

•引入动态 GasPrice,可以从 https://ethgasstation.info 网站中获取实时的 GasPrice•指定 GasLimit,防止由于合约问题消耗过多 Gas•余额检查,防止由于余额不足造成交易失败,浪费了手续费•接收回调数据的用户合约方法尽量简单,分离业务逻辑

3、支持 http 协议 jsonrpc

有的网络节点没有开启 ws 服务,而使用 http 协议的网络 jsonrpc 又无法直接订阅事件。这时可以采取迂回策略,模拟事件订阅,具体思路如下:

•开启网络区块监控•监控到有新区块产生,查询区块中的日志•如果有我们 Oracle 合约产生的查询日志,则进入后续的查询和回调流程

大家对于优化有其他思路或疑问,欢迎留言探讨。

下篇中,我将以一个抽奖合约为示例,介绍如何使用我们开发的 Oracle 服务来对抽奖合约提供一个随机数。

本文作者:六天


来源链接:mp.weixin.qq.com

免责声明
世链财经作为开放的信息发布平台,所有资讯仅代表作者个人观点,与世链财经无关。如文章、图片、音频或视频出现侵权、违规及其他不当言论,请提供相关材料,发送到:2785592653@qq.com。
风险提示:本站所提供的资讯不代表任何投资暗示。投资有风险,入市须谨慎。
世链粉丝群:提供最新热点新闻,空投糖果、红包等福利,微信:juu3644。