本文分析Truora-Service服务端java,目标是梳理出OracleCore合约查找创建机制、事件监听机制、url访问机制、数据回写机制, 以后创建自己的预言机服务器就了如指掌了。
代码下载路径: git clone GitHub - WeBankBlockchain/Truora-Service
1 配置文件
项目配置文件build.gradle中的配置有这些内容:
implementation 'org.fisco-bcos:solcJ:0.6.10.0'
implementation 'org.fisco-bcos.java-sdk:fisco-bcos-java-sdk-v3:3.0.1'
2 OracleCore合约查找创建机制
服务启动阶段, 执行:
Bcos3ClientConfig 自动装配,读取配置文件conf/application-fiscobcos3.yml
读出platform: fiscobcos3, 装配类EventRegisters,按照chainId分类读取groupId。
Bcos3EventRunner init()
检查eventRegisters.startOracleCore==true?
true就开始执行 oracleWorker.init(eventRegister),调用父类的init():
AbstractContractWorker init()
initContract()
loadOrDeployContract() : 读取数据库中记录查找ORACLE_CORE类型,
找到记录,读取合约地址,检查有效性:--->
this.isContractAddressValid(eventRegister, contractAddress) 这里面直接用OracleCore类加载地址生成合约实例。
没找到记录,表示不存在合约,开始部署, 部署成功后写入数据库
OracleCoreWorker deployContract()
3 OracleCore合约事件监听
紧接着上一步合约地址初始化oracleWorker.init(eventRegister),开始监听事件初始化
oracleCoreEventCallback.init(oracleWorker);
Bcos3EventCallback init()
registerEventListener();
读取数据库历史记录,找到最后一条记录的区块号,+1后赋值给from,表明监听开始的区块号。
subcriber.subscribeEvent(eventSubParams,this); 组装监听事件参数,注册,自己就是回调处理对象,实现了处理方法onReceiveLog
onReceiveLog() 收到事件,异步处理,交给
AbstractContractWorker processBatchLogs() 处理一批log,
processSingleLog(eventLog); 处理一条log
处理逻辑:检查数据库中是否已经存在requestId,存在表示处理过这条事件,丢弃。
调用 OracleCoreWorker processLog() 去链下获取各种资源并回写链上
sourceCrawlerFactory.handle()
URLCrawler handle() 发起http调用。
fulfill() 查询得到的结果写回预言机合约,写前先查询合约中是否存在了requestId结果,目的是防止重复写入。
直接用OracleCore类调用方法fulfillRequest完成交易,最后检查交易结果,写入
执行结果写入History数据库
this.updateReqHistory(requestId, reqStatus, errorMsg, result);
4 url访问机制
关键的数据源爬虫工厂类:SourceCrawlerFactory
设计思想是一个简单的插件化机制,便于在扩展更多的的资源获取方式时,对接到事件回调流程里。
* 即:EventWorker收到链上预言机事件,则调用这里的接口,传入事件里和数据源有关的参数
* Factory根据名称等信息,确定实例化哪一个bean去调用外部资源,如“UrlCrawler”去获取,并返回结果,供worker去上链
* 初步拟定进出的参数都是简单的字符串,可以打成json格式
* * 如{name="Url",url="http://www.xyz.com"},
* 只需要包含有name,则可以定位到bean,然后json的解析可以由bean自由发挥。
*规则: bean的名字必须是 [XXX]Crawler, 如URLCrawler,注解要指明名字: @Service("URLCrawler") ,可选:@Scope("prototype")
实际上是在 URLCrawler handle() 发起http调用。
两个调用方法,最终调用到具体的爬虫实现类.handle().
public String handle(String inputStr)
public String handle(String name, String inputStr)
爬虫实现类:
HashUrlCrawler:通过url访问外部网页,将返回的资源内容全部打成hash。关键是通过cryptoSuite.hash(httpResponse)。
LocalRandomCrawler:一个本地生成伪随机数的桩,主要用来做测试。
URLCrawler:通过url访问外部网页,返回的结果。
使用httpService.getHttpResultAndParse(httpUrl, format, path);
// url = "https://www.random.org/integers/?num=100&min=1&max=100&col=1&base=10&format=plain&rnd=new"
// url = "plain(https://www.random.org/integers/?num=100&min=1&max=100&col=1&base=10&format=plain&rnd=new)[1]";
// url = "json(https://api.exchangerate-api.com/v4/latest/CNY).rates.JPY";
format格式:
-
json-- 返回结果解析成json,然后按照附加的字段读取对应的数据。详细的请看JsonPath说明
com.jayway.jsonpath
Class JsonPath
java.lang.Object
com.jayway.jsonpath.JsonPath
public class JsonPath
extends java.lang.Object
JsonPath is to JSON what XPATH is to XML, a simple way to extract parts of a given document. JsonPath is available in many programming languages such as Javascript, Python and PHP.
JsonPath allows you to compile a json path string to use it many times or to compile and apply in one single on demand operation.
Given the Json document:
String json =
"{
"store":{
"book":[
{
"category":"reference",
"author":"Nigel Rees",
"title":"Sayings of the Century",
"price":8.95
},
{
"category":"fiction",
"author":"Evelyn Waugh",
"title":"Sword of Honour",
"price":12.99
}
],
"bicycle":{
"color":"red",
"price":19.95
}
}
}";
A JsonPath can be compiled and used as shown:
JsonPath path = JsonPath.compile("$.store.book[1]"); List<Object> books = path.read(json);
Or:
List<Object> authors = JsonPath.read(json, "$.store.book[*].author")
If the json path returns a single value (is definite):
String author = JsonPath.read(json, "$.store.book[1].author")
Gradle: com.jayway.jsonpath:json-path:2.5.0 (json-path-2.5.0.jar)
-
plain-- 返回结果解析成数组,url后面的[1]指明了元素index,直接读取index元素数据。
-
url-- 无需解析,直接返回结果。
5 数据回写机制
服务器端执行完监听事件请求,得到的外界URL返回数据,这时必须要把这个数据写入到OracleCore合约中,之后链上合约才能读取到数据,正常运行相关功能。
OracleCoreWorker fulfill() 工作流程:
读取OracleCore合约地址,
加载OracleCore合约地址,生成oracleCore实例。
先查询合约中是否存在了requestId结果,已经存在结果(可能是被处理过了,或者超时了)就不再重复写入。这一步目的是防止重复写入,减少一个错误的交易。
直接用OracleCore类调用方法fulfillRequest完成交易,得到收据receipt。
检查交易结果:成功, 输出log信息;
失败,区分交易错误和处理错误两种类型,分别输出log信息。