淺析 <路印協議--Loopring> 及總體分析 Relay 源碼

做者:林冠宏 / 指尖下的幽靈node

前序:git

路印協議功能很是之多及強大,本文只作入門級別的分析。github

理論部分請細看其白皮書,<a href="https://github.com/Loopring/whitepaper" >https://github.com/Loopring/whitepaper</a>golang

實際代碼部分:<a href="https://github.com/Loopring/relay">https://github.com/Loopring/relay</a>web


目錄

  • 路印協議
  • 通常應用於
  • 做用
  • 模塊組成部分
  • 交易流程
  • 代碼核心業務邏輯
  • relay源碼概述

路印協議

  • 簡稱Loopring
  • 0xKyber 同樣,是區塊鏈應用去中心化交易協議之一,協議明確了使用它來進行買賣交易的行爲務必要按照它規定的模式來進行。
  • 從程序的角度去描述的話,它是一份由Go語言編寫的可應用於和區塊鏈相關的開源軟件。
  • 且外,請注意它不是區塊鏈應用中的智能合約,讀者注意區分二者概念。

通常應用於

  • 虛擬貨幣交易所,交易所有下面例子
    • MtGox
    • Bitfinex
    • 火幣網
    • OKEX
    • ...

做用

  • 解決中心化交易存在的一系列問題
    • 缺少安全
      • 交易所保存用戶私鑰,黑客攻擊後竊走。
      • 體現須要交易所批准,想象下若是交易所人員攜款跑路或忽然倒閉
    • 缺少透明度
      • 用戶買賣由中心化交易所代替執行,內部具體流程保密
      • 用戶資產可能被用做第三方投資
    • 缺少流動性
      • 交易量多的交易所容易形成市場壟斷
      • 即便出過嚴重事故,卻仍然因佔巨大市場份額而其餘用戶不得不繼續在該所交易
  • 優化現有區中心話交易的一些問題
    • 缺少統一標準
    • 流動性差
      • 訂單廣播網絡範圍小
      • 訂單表成交後更新速度慢
    • 性能問題
      • 致使高額的執行代碼支付費用
      • 挖坑延遲
      • 更改/取消訂單代價高

模塊組成部分

  • 支持向路印網絡發送請求的錢包軟件
    • APP
    • WEB
  • 路印中繼軟件 -- Relay
  • 路印區塊鏈智能合約 -- LPSC
  • 路印中繼網,由多個運行了路印中繼軟件的網絡節點組成
  • 路印同盟鏈,佈置了LPSC的區塊鏈

交易流程

對照上圖共6大步驟的說明及其代碼核心業務邏輯

1.協議受權

  • 用戶 Y 想交易代幣,所以,受權 LPSC 出售數額爲 9 的代幣 B。此操做不會凍結用戶的代幣。訂單處理期間,用戶依然能夠自由支配代幣。
  • 代碼調用邏輯是:錢包向某區塊鏈,例如以太坊的公有鏈發起json-rpc請求,根據請求中的合約地址address合約ABI信息找到對應的LPSC合約後,再根據methodName找到對應的的接口方法,這些接口方法固然是遵循ERC20標準的。請求受權出售Y帳戶9個B代幣。

2. 訂單建立

  • 錢包APP或網頁應用中,顯示由網絡中介,例第三方API接口https://api.coinmarketcap.com提供 代幣 B 和代幣 C 之間的當前匯率和訂單表。用戶根據這些信息,設置好本身的買賣代幣及其相關數量,例如:賣10ETH,買50EOS。而後建立好這個訂單請求,訂單中還有其餘信息。最後訂單被用戶Y的私鑰加密,也就是簽名後發給中繼點軟件 --- relayredis

  • 代碼調用邏輯是:錢包客戶端能夠採用Http請求調用第三方API接口或使用其它方式來獲取ticker--24小時市場變化統計數據和各代幣的價格信息以後,再經過UI界面組合顯示訂單表和匯率。用戶設置好本身的訂單信息後和簽名後,經過josn-rpc請求向relay發起訂單請求。算法

  • 訂單簽名步驟sql

    • <a href="https://github.com/Loopring/loopring.js/wiki/%E8%B7%AF%E5%8D%B0%E5%8D%8F%E8%AE%AEv1.0.0%E8%AE%A2%E5%8D%95%E7%BB%93%E6%9E%84%E5%92%8C%E6%95%B0%E5%AD%97%E7%AD%BE%E5%90%8D">文檔</a>
    • 使用Keccak-256 算法對這個字節數組作散列計算獲得訂單的Hash
    • Secp256k1簽名算法對獲得的Hash進行簽名獲得Sig
    • Sig的0-32 位轉換成16進制字符串賦值給Order的r
    • Sig的32-64 位轉換成16進制字符串賦值給Order的s
    • Sig的64位加上27轉換成整型Order的v

3.訂單廣播

  • 錢包向單個或多箇中繼發送訂單及其簽名,中繼隨之更新轄下公共訂單表。路印協議不限制訂單表架構,容許「先到先得」模式;中繼能夠自行選擇訂單表設計。數據庫

  • 代碼調用邏輯是:客戶端向單個或多個relay發送order request後,relay接收到訂單後,再各自向已知的其它relay進行廣播,廣播的技術點在relay源碼中的gateway部分能夠看出使用的是IPFS--點對點的分佈式版本文件系統技術。那麼這些relay點它們組成的就是上面所說的路印中繼網。隨後各relay進行各自的訂單表refresh,這就保證了統一。表的設計是能夠自定義的,例如字段,數據庫引擎的選擇等。json

4.流動性共享

  • 這部分已經附屬解析到第三點中的互相廣播部分。
  • 此外,補充兩點
    • 節點有權選擇是否及如何交流,咱們能夠經過修改源碼來進行各類限制
    • 這部分有個核心點--接收廣播後的表更新算法設計,如何達到高速處理杜絕偏差回滾

5.環路撮合(訂單配對)

  • 環路礦工撮合多筆訂單,以等同或優於用戶開出的匯率知足部分或所有訂單數額。路印協議之因此可以保證任何交易對之間的高流動性,很大程度上得益於環路礦工。若是成交匯率高於用戶 Y 的出價,環路中全部訂單皆可共享箇中利潤。而做爲報酬,環路礦工能夠選擇收取部分利潤(分潤,同時向用戶支付 LRx),或收取原定的LRx 手續費。

  • 原定手續費LRx 的是在訂單建立的時候,由客戶端設置的

  • 環路數學符號

    • 環路礦工撮合多筆訂單,以等同或優於用戶開出的匯率知足部分或所有訂單數額。它的表達式就是:Ri->j * Rj->i >= 1
    • 此外,對於某訂單中,部分被交易的。例如賣10A買2B,結果賣出了4A,那麼默認必然是買入了 (2/5)B。由於。訂單兌換率恆定 除非訂單徹底成交:Ri->j * Rj->i = 1,不然部分賣買出的比例兌換率等同於原始的兌換率。10/2=4/y
  • 代碼調用邏輯是:miner部分的代碼,和relay在同一個項目中。在relay處理完訂單以後,miner會去去訂單表拿取訂單進行撮合。造成最優環,也就是訂單成功配對,miner這層會進行對應的數學運算。

6. 驗證及結算

  • 這部分是LPSC處理的。
    • LPSC 接收訂單環路後會進行多項檢查,驗證環路礦工提供的數據,例如各方簽名。
    • 決定訂單環路是否能夠部分或所有結清(取決於環路訂單的成交匯率和用戶錢包中的代幣餘額)。
    • 若是各項檢查達標,LPSC會經過原子操做將代幣轉至用戶,同時向環路礦工和錢包支付手續費。
    • LPSC 若是發現用戶 Y 的餘額不足,會採起縮減訂單數額。
    • 一旦足夠的資金存入地址,訂單會自動恢復至原始數額。而取消訂單則須要單向手動操做且不可撤銷。
    • 上面的存入地址中的地址指的是,用戶在區塊鏈中的帳戶地址。
  • 代碼調用邏輯是:relayminer的環路數據,和第一點同樣,經過json-rpc請求到公鏈中的LPSC合約,讓它進行處理。

relay源碼概述

就我所分析的最新的relay源碼,它內部目前是基於ETH公有鏈做爲第一個開發區塊鏈平臺。內部採用裏以太坊Go源碼包不少的方法結構體,json-rpc目前調用的命令最多的都是Geth的。

多是考慮到ETH的成熟和普及程度,因此選擇ETH做爲第一個開發區塊鏈平臺。但路印協議並非爲ETH量身定作的,它能夠在知足條件的多條異構區塊鏈上得以實施。後續估計會考慮在EOS,ETC等公有鏈上上進行開發。

程序的入口

採用了cli模式,即提供了本地命令行查詢。也提供了外部的API。

--relay
--|--cmd
--|--|--lrc
--|--|--|--main.go

func main() {
    app := utils.NewApp()
    app.Action = startNode // 啓動一箇中繼節點
    ...
}

節點的初始化與啓動

func startNode(ctx *cli.Context) error {

	globalConfig := utils.SetGlobalConfig(ctx) // 讀取配置文件並初始化
	// 日誌系統初始化
	// 對系統中斷和程序被殺死事件信號的註冊
	n = node.NewNode(logger, globalConfig) // 初始化節點
	//...
	n.Start() // 啓動節點
	//...
	return nil
}

配置文件位置在

--relay
--|--config
--|--|--relay.toml
--|--|--其它

relay.toml 內部可配置的項很是多,例如硬存儲數據庫MySQL配置信息的設置等。

初始化節點,各部分的的介紹請看下面代碼的註釋

func NewNode(logger *zap.Logger, globalConfig *config.GlobalConfig) *Node {
    // ...
    // register
    n.registerMysql() // lgh:初始化數據庫引擎句柄和建立對應的表格,使用了 gorm 框架
    cache.NewCache(n.globalConfig.Redis) // lgh:初始化Redis,內存存儲三方框架
    
    util.Initialize(n.globalConfig.Market) // lgh:設置從 json 文件導入代幣信息,和市場
    n.registerMarketCap() // lgh: 初始化貨幣市值信息,去網絡同步
    
    n.registerAccessor()  // lgh: 初始化指定合約的ABI和經過json-rpc請求eth_call去以太坊獲取它們的地址,以及啓動了定時任務同步本地區塊數目,僅數目
    
    n.registerUserManager() // lgh: 初始化用戶白名單相關操做,內存緩存部分基於 go-cache 庫,以及啓動了定時任務更新白名單列表
    
    n.registerOrderManager() // lgh: 初始化訂單相關配置,含內存緩存-redis,以及系列的訂單事件監聽者,如cancel,submit,newOrder 等
    n.registerAccountManager() // lgh: 初始化帳號管理實例的一些簡單參數。內部主要是和訂單管理者同樣,擁有用戶交易動做事件監聽者,例如轉帳,確認等
    n.registerGateway() // lgh:初始化了系列的過濾規則,包含訂單請求規則等。以及 GatewayNewOrder 新訂單事件的訂閱
    n.registerCrypto(nil) // lgh: 初始化加密器,目前主要是Keccak-256
    
    if "relay" == globalConfig.Mode {
    	n.registerRelayNode()
    } else if "miner" == globalConfig.Mode {
    	n.registerMineNode()
    } else {
    	n.registerMineNode()
    	n.registerRelayNode()
    }
    
    return n
}

func (n *Node) registerRelayNode() {
    n.relayNode = &RelayNode{}
    n.registerExtractor()
    n.registerTransactionManager() // lgh:事務管理器
    n.registerTrendManager()   // lgh: 趨勢數據管理器,市場變化趨勢信息
    n.registerTickerCollector() // lgh: 負責統計24小時市場變化統計數據。目前支持的平臺有OKEX,幣安
    n.registerWalletService() // lgh: 初始化錢包服務實例
    n.registerJsonRpcService()// lgh: 初始化 json-rpc 端口和綁定錢包WalletServiceHandler,start 的時候啓動服務
    n.registerWebsocketService() // lgh: 初始化 webSocket
    n.registerSocketIOService()
    txmanager.NewTxView(n.rdsService)
}

func (n *Node) registerMineNode() {
    n.mineNode = &MineNode{}
    ks := keystore.NewKeyStore(n.globalConfig.Keystore.Keydir, keystore.StandardScryptN, keystore.StandardScryptP)
    n.registerCrypto(ks)
    n.registerMiner()
}

從上面的各個register點入手分析。有以下結論

  • 總體來講,relay的內部代碼的通信模式是基於:事件訂閱--事件接收--事件處理 的。
  • relay 採用的硬存儲數據庫是分佈式數據庫Mysql,代碼中使用了gorm框架。在registerMysql 作了表格的建立等工做
  • 內存存儲方面有兩套
    • 基於 Redis
    • 基於 go-cache
  • 在導入代幣信息,和市值信息的部分存在一個問題點:配置文件中的市場市值數據獲取的第三方接口coinmarketcap已經在其官網發表了聲明,v1版本的API將於本年11月30日下線,因此,relay這裏默認的配置文件中下面的須要改成v2版本的。

[market_cap]
        base_url = "https://api.coinmarketcap.com/v1/ticker/?limit=0&convert=%s"
        currency = "USD"
        duration = 5
        is_sync = false
  • OrderManagerAccountManager 中註冊的Event 事件,主要被觸發的點在socketio.go 中,對應上面談到的gateway模塊中負責接收IPFS通信的廣播。在接收完後,纔會再分發下去,進行觸發事件處理。

    --relay
    --|--gateway
    --|--|--socketio.go
    
    func (so *SocketIOServiceImpl) broadcastTrades(input eventemitter.EventData) (err error) {
        // ...
        v.Emit(eventKeyTrades+EventPostfixRes, respMap[fillKey])
        // ...
    }
  • 新訂單事件的觸發步驟分兩層

    • gateway.go 裏面的eventemitter.GatewayNewOrderIPFS分發
    • OrderManager 裏面的 eventemitter.NewOrder
      • gateway.go接收到GatewayNewOrder以後分發。
      • 客戶端調用WalletService 的 API SubmitOrder 後觸發
  • relay節點模式有3種

    • 單啓動 relay 中繼節點

    • 單啓動 miner 礦工節點

    • 雙啓動,這是默認的形式

      if "relay" == globalConfig.Mode {
      	n.registerRelayNode()
      } else if "miner" == globalConfig.Mode {
      	n.registerMineNode()
      } else {
      	n.registerMineNode()
      	n.registerRelayNode()
      }
  • relay--中繼節點 提供了給客戶端的API主要是WalletService錢包的。前綴方法名是: loopring

    • 支持 json-rpc 的格式調用

    • 只是Http-GET & POST 的形式調用

      func (j *JsonrpcServiceImpl) Start() {
          handler := rpc.NewServer()
          if err := handler.RegisterName("loopring", j.walletService); err != nil {
          	fmt.Println(err)
          	return
          }
          var (
          	listener net.Listener
          	err      error
          )
          if listener, err = net.Listen("tcp", ":"+j.port); err != nil {
          	return
          }
          //httpServer := rpc.NewHTTPServer([]string{"*"}, handler)
          httpServer := &http.Server{Handler: newCorsHandler(handler, []string{"*"})}
          //httpServer.Handler = newCorsHandler(handler, []string{"*"})
          go httpServer.Serve(listener)
          log.Info(fmt.Sprintf("HTTP endpoint opened on " + j.port))
          return
      }
  • Miner--礦工節點,主要提供了訂單環路撮合的功能,可配置有以下的部分。

    [miner]
        ringMaxLength = 4  // 最大的環個數
        name = "miner1"
        rate_ratio_cvs_threshold = 1000000000000000
        subsidy = 1.0
        walletSplit = 0.8
        minGasLimit = 1000000000
        maxGasLimit = 100000000000 // 郵費最大值
        feeReceipt = "0x750aD4351bB728ceC7d639A9511F9D6488f1E259"
        [[miner.normal_miners]]
            address = "0x750aD4351bB728ceC7d639A9511F9D6488f1E259"
            maxPendingTtl = 40
            maxPendingCount = 20
            gasPriceLimit = 10000000000
        [miner.TimingMatcher]
        		round_orders_count=2
        		duration = 10000  // 觸發一次撮合動做的毫秒數
        		delayed_number = 10000
        		max_cache_rounds_length = 1000
        		lag_for_clean_submit_cache_blocks = 200
        		reserved_submit_time = 45
        		max_sumit_failed_count = 3
    • 礦工節點的啓動分兩部分:

      • 匹配者,負責訂單撮合
      • 提交者,負責訂單結果的提交與其餘處理
      func (minerInstance *Miner) Start() {
          minerInstance.matcher.Start()
          minerInstance.submitter.start()
      }
    • miner 本身擁有一個計費者。在匹配者matcher定時從ordermanager中拉取n條order數據進行匹配成環,若是成環則經過調用evaluator進行費用估計,而後提交到submitter進行提交到以太坊

      evaluator := miner.NewEvaluator(n.marketCapProvider, n.globalConfig.Miner)
    • 匹配者 matcher.Start()

      func (matcher *TimingMatcher) Start() {
      	matcher.listenSubmitEvent() // lgh: 註冊且監聽 Miner_RingSubmitResult 事件,提交成功或失敗或unknown 後,都從內存緩存中刪除該環
      	matcher.listenOrderReady() // lgh: 定時器,每隔十秒,進行以太坊,即Geth同步的區塊數和 relay 本地數據庫fork是false的區塊數進行對比,來控制匹配這 matcher 是否準備好,可以進行匹配
      	matcher.listenTimingRound() // lgh: 開始定時進行環的撮合,受上面的 orderReady 影響
      	matcher.cleanMissedCache() // lgh: 清除上一次程序退出前的錯誤內存緩存
      }
      • Geth同步的區塊數和 relay 本地數據庫fork是false的區塊數進行對比
      if err = ethaccessor.BlockNumber(&ethBlockNumber); nil == err {
      		var block *dao.Block
      		// s.db.Order("create_time desc").Where("fork = ?", false).First(&block).Error
      		if block, err = matcher.db.FindLatestBlock(); nil == err { block.BlockNumber, ethBlockNumber.Int64())
      			if ethBlockNumber.Int64() > (block.BlockNumber + matcher.lagBlocks) {
      				matcher.isOrdersReady = false
      			} else {
      				matcher.isOrdersReady = true
      			}
      		}
      	}
      	...
      • matcher.isOrdersReady 控制撮合的開始
      if !matcher.isOrdersReady {
      		return
      	}
      	...
      	m.match()
      	...
      • TimingMatcher.match 方法是整個訂單撮合的核心。在其成功撮合後,會發送eventemitter.Miner_NewRing 新環事件,告訴訂閱者,撮合成功
    • 提交者 submitter.start()。提交者,主要有一個很核心的步驟: 訂閱後並監聽 Miner_NewRing 事件,而後提交到以太坊,再更新本地環數據表。代碼以下

      // listenNewRings()
      txHash, status, err1 := submitter.submitRing(ringState) // 提交到以太坊
      	...
      	submitter.submitResult(...) // 觸發本地的 update
      func (submitter *RingSubmitter) submitRing(...) {
      	...
      	if nil == err {
      		txHashStr := "0x"
      		//  ethaccessor.SignAndSendTransaction 提交函數
      		txHashStr, err = ethaccessor.SignAndSendTransaction(ringSubmitInfo.Miner, ringSubmitInfo.ProtocolAddress, ringSubmitInfo.ProtocolGas, ringSubmitInfo.ProtocolGasPrice, nil, ringSubmitInfo.ProtocolData, false)
      		...
      		txHash = common.HexToHash(txHashStr)
      	} 
      	...
      }

至此,咱們有了一個總體的概念。對照上面的交易流程圖。從客戶端發起訂單,都relay處理後,最後提交給區塊鏈(例以太坊公鏈),到最終的交易完成。relay 源碼內的各個模塊是各司其責的。

Relay錢包路印協議之間的橋接,向上和錢包對接,向下和Miner對接。給錢包提供API,給Miner提供訂單,內部維護訂單池。

miner一方面撮合訂單,另外一方面和LPSC交互。而LPSC則和其所在公鏈交互。

相關文章
相關標籤/搜索