4

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

 2 years ago
source link: https://segmentfault.com/a/1190000040918523
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.
neoserver,ios ssh client

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

发布于 今天 14:55

这是我独立完成的一个大需求,给项目合约进行喂价。写这篇文章的初衷是想一边记录自己的成长,也同时一边帮后浪总结经验。

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_timse

            print("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 = 5   

            print(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 r

def 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 = contract.encodeABI(fn_name="putPrice", args=arguments)
        
        # get Nonce

        currentConfluxStatus = 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()

感觉还有挺多没有总结到位,包括架构,工程上的,细节上的。其实做的时候感觉踩了好多好多坑,但是总结起来也没多少。代码还没完全整理好。先发总结。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK