关于区块链:在conflux上从0到1实现合约喂价Blockchain-Oracle

5次阅读

共计 5266 个字符,预计需要花费 14 分钟才能阅读完成。

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

背景

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

总结

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

正文完
 0