在conflux上从0到1实现合约喂价(Blockchain Oracle)

这是我独立完成的一个大需求,给项目合约进行喂价。写这篇文章的初衷是想一边记录自己的成长,也同时一边帮后浪总结经验。
背景
Conflux现在没有好用的Blockchain Oracle服务,只有一个等待上线的Witnet。我们担心TriangleDao上线了,Witnet还没上线。经过讨论,团队决定不用别人的服务了,自己编写Oracle喂价服务。得益于我在stafi protocol同哥对我的超级严格的指导,我对于这种脚本服务的编写目前还是很自信的。很多问题(比如http重连等问题),已经在我的考虑之内了。
架构
与smartbch的秀宏哥和TriangleDao的XD讨论了一下,如果只需要合约自我喂价的话,其实整个架构可以设计得很简单:只需要2个进程,一个负责获取从binance/okex获取cfx的最新报价,一个进程负责把最新报价写入链上。当然,还需要写一个简单的sdk,让同事调用合约,获取报价即可。
难点
整体架构分为2个部分,一个是读、一个是写入。一个稳健的Oracle服务两个方面都不能出错。

1.【endpoint容错】我觉得无法保证biannce或者okex的API一定不会出错,或者endpoint崩溃,需要换一个endpoint。所以这里需要进行一个endpoint容错。
2.【数据库的插入报错】我认为还是要把数据读取的记录插入到数据库,这样方便日后的调试甚至是数据恢复,我设置了一个status字段。用来表示记录的状态。
3.【网络堵塞,请求超时】网络的环境有时候可能会不稳定,这里一定要做容错。目前假如服务器的网络环境不稳定,我暂时没有任何办法。解决方案:其实最好就是分布式部署,多节点容灾。
目前写了2个关键函数,getAvgPrice和getAvgPrice。
def getRemotePrice(self, symbol, price_dimansion):binance_res, binance_avg_price, binance_avg_time = self.binanceHolder.getAvgPrice(symbol, price_dimansion)print("binance finish")okex_res, okex_avg_price, okex_avg_time = self.okexHolder.getAvgPrice(symbol, price_dimansion)

Binance获取price有同步和异步两种方法,根据需求,我这里需要一个同步堵塞的方法。
class BinanceHolder():def __init__(self) -> None: self.client = Client(api_key, api_secret)# async def init(self): #self.client = await AsyncClient.create(api_key, api_secret)def getAvgPrice(self, symbol, price_dimansion): try: avg_price = self.client.?get_avg_price(symbol='CFXUSDT')print("biancne getavg price: ", avg_price)binance_avg_time = int(avg_price['mins']) binance_avg_price = int( float(avg_price['price']) * price_dimansion)#{'mins': 5, 'price': '0.32856984'} # binance_res, binance_avg_price, binance_avg_timseprint("binance_avg_price, binance_avg_time : ", binance_avg_price, binance_avg_time) return True, binance_avg_price, binance_avg_time

Okex也一样,采用同步堵塞的方法
class OkexHolder():def __init__(self) -> None: self.spotAPI = spot.SpotAPI(api_key, secret_key, passphrase, False)def getAvgPrice(self, symbol, price_dimansion):try: result = self.spotAPI.get_deal(instrument_id="CFX-USDT", limit='')# {"time": "2021-10-21T18:59:19.640Z", "timestamp": "2021-10-21T18:59:19.640Z", # "trade_id": "6977672", "price": "0.33506", "size": "32.531486", "side": "sell"} firstResult = result[0] print(firstResult["price"])# okex_res, okex_avg_price, okex_avg_time okex_avg_price = int( float(firstResult["price"]) * price_dimansion ) okex_avg_time = 5print(okex_avg_price, okex_avg_time) return True, okex_avg_price, okex_avg_time except: traceback.print_exc() return False, 0, 0

这两个部分都需要一个error and retry的函数,用来检测错误重启。不断重试。
写 再看写,我需要把数据写入链上。用合约记录price的状态。我认为状态只跟4个因素有关:“price, price_dimension, symbol和source"。因为solidity没有办法存储浮点数,我必须把price乘以一个10的N次方,变成一个大数存入合约里。关于function,对于合约来说功能只有两个:putPrice 和 getPrice。所以第一版本的合约如下:
pragma solidity >=0.6.11; import "./Ownable.sol"; contract triangleOracle is Ownable {// 16 + 16 + 16 + 16 = 64 bytes struct PriceOracle { uint128 price_dimension; // 16 bytes uint128 price; // 16 bytes bytes16 symbol; // 16 bytes string source; // 16 bytes } PriceOracle latestPrice; event PutLatestTokenPrice(uint128 price, string source, bytes16 symbol, uint128 price_dimension); function putPrice(uint128 price, string memory source, bytes16 symbol, uint128 price_dimension) public onlyOwner { latestPrice = PriceOracle ({ price: price, source: source, symbol: symbol, price_dimension: price_dimension }); emit PutLatestTokenPrice(price, source, symbol, price_dimension); }function getPrice() public returns (uint128 price, string memory source, bytes16 symbol, uint128 price_dimension) { return (latestPrice.price, latestPrice.source, latestPrice.symbol, latestPrice.price_dimension); }}

写入链上数据,最需要考虑的是,如果让交易最低成本被矿工快速打包。如果矿工没有打包,那么链上数据的更新就会有延迟。首先,我们要知道gas可以通过 cfx_estimateGasAndCollateral 来估算。gas指的是,矿工最多只能执行的计算次数。这个是为了防止恶意执行逻辑或者死循环。在 conflux上,最终矿工费等于 gasUsed * gasPrice。所以要设置好gas和gas price这两个参数。
还有几个参数也是要注意的,storageLimit,epochHeight和nonce。这几个也是非常关键的参数,是否能够被成功打包的关键。
首先,conflux的gas price很低,一般设置为0x5~0x1。我设置成0x5。
其次,gas需要去请求链上的gas来估算我们需要的gas,函数是estimateGasAndCollateral。
def gasEstimated(parameters): r = c.cfx.estimateGasAndCollateral(parameters, "latest_state") return r# // Result # { #"jsonrpc": "2.0", #"result": { #"gasLimit": "0x6d60", #"gasUsed": "0x5208", #"storageCollateralized": "0x80" #}, #"id": 1 # }

我采取gas = gasUsed + 0x100来定下gas的数值,同样,storageLimit这个参数也是storageCollateralized + 0x20来定下。
parameters["storageLimit"] = result["storageCollateralized"] + 0x20 parameters["gas"] = result["gasUsed"] + 0x100

最后,写入合约。
def gasEstimated(parameters): r = c.cfx.estimateGasAndCollateral(parameters, "latest_state") return rdef send_contract_call(contract_address, user_testnet_address,contract_abi, private_key, arguments): try: # initiate an contract instance with abi, bytecode, or address contract = c.contract(contract_address, contract_abi) data = https://www.it610.com/article/contract.encodeABI(fn_name="putPrice", args=arguments)# get NoncecurrentConfluxStatus = c.cfx.getStatus() CurrentNonce =c.cfx.getNextNonce(user_testnet_address)parameters = { 'from': user_testnet_address, 'to': contract_address, 'data': data, 'nonce': CurrentNonce, 'gasPrice': 0x5 }result = gasEstimated(parameters)parameters["storageLimit"] = result["storageCollateralized"] + 0x20 parameters["gas"] = result["gasUsed"] + 0x100 parameters["chainId"] = 1 parameters["epochHeight"] = currentConfluxStatus["epochNumber"]# populate tx with other parameters for example: chainId, epochHeight, storageLimit # then sign it with account signed_tx = Account.sign_transaction(parameters, private_key)print(signed_tx.hash.hex()) print(signed_tx.rawTransaction.hex()) c.cfx.sendRawTransaction(signed_tx.rawTransaction.hex())except: traceback.print_exc()

总结 【在conflux上从0到1实现合约喂价(Blockchain Oracle)】感觉还有挺多没有总结到位,包括架构,工程上的,细节上的。其实做的时候感觉踩了好多好多坑,但是总结起来也没多少。代码还没完全整理好。先发总结。

    推荐阅读