淺析 及整體分析 Relay 原始碼

林冠巨集發表於2019-03-02

作者:林冠巨集 / 指尖下的幽靈

前序:

路印協議功能非常之多及強大,本文只做入門級別的分析。

理論部分請細看其白皮書,github.com/Loopring/wh…

實際程式碼部分:github.com/Loopring/re…


目錄

  • 路印協議
  • 一般應用於
  • 作用
  • 模組組成部分
  • 交易流程
  • 程式碼核心業務邏輯
  • relay原始碼概述

路印協議

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

一般應用於

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

作用

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

模組組成部分

  • 支援向路印網路傳送請求的錢包軟體
    • APP
    • WEB
  • 路印中繼軟體 — Relay
  • 路印區塊鏈智慧合約 — LPSC
  • 路印中繼網,由多個執行了路印中繼軟體的網路節點組成
  • 路印同盟鏈,佈置了LPSC的區塊鏈

交易流程

淺析  及整體分析 Relay 原始碼

對照上圖共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的私鑰加密,也就是簽名後發給中繼點軟體 --- relay

  • 程式碼呼叫邏輯是:錢包客戶端可以採用Http請求呼叫第三方API介面或使用其它方式來獲取ticker--24小時市場變化統計資料和各代幣的價格資訊之後,再通過UI介面組合顯示訂單表和匯率。使用者設定好自己的訂單資訊後和簽名後,通過josn-rpc請求向relay發起訂單請求。

  • 訂單簽名步驟

    • 文件
    • 使用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,這就保證了統一。表的設計是可以自定義的,例如欄位,資料庫引擎的選擇等。

4.流動性共享

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

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

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

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

  • 環路數學符號

    • 淺析  及整體分析 Relay 原始碼
    • 環路礦工撮合多筆訂單,以等同或優於使用者開出的匯率滿足部分或全部訂單數額。它的表示式就是: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版本的。
淺析  及整體分析 Relay 原始碼
[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則和其所在公鏈互動。

相關文章