作者:林冠巨集 / 指尖下的幽靈
前序:
路印協議
功能非常之多及強大,本文只做入門級別的分析。
理論部分請細看其白皮書,github.com/Loopring/wh…
實際程式碼部分:github.com/Loopring/re…
目錄
- 路印協議
- 一般應用於
- 作用
- 模組組成部分
- 交易流程
- 程式碼核心業務邏輯
relay
原始碼概述
路印協議
- 簡稱
Loopring
- 和
0x
、Kyber
一樣,是區塊鏈應用去中心化交易協議
之一,協議明確了使用它來進行買賣交易的行為務必要按照它規定的模式來進行。 - 從程式的角度去描述的話,它是一份由
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的私鑰加密,也就是簽名後發給中繼點軟體 --- 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
的是在訂單建立的時候,由客戶端設定的 -
環路數學符號
環路礦工撮合多筆訂單,以等同或優於使用者開出的匯率滿足部分或全部訂單數額
。它的表示式就是: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 的餘額不足,會採取縮減訂單數額。
- 一旦足夠的資金存入地址,訂單會自動恢復至原始數額。而取消訂單則需要單向手動操作且不可撤銷。
- 上面的存入地址中的地址指的是,使用者在區塊鏈中的賬戶地址。
程式碼呼叫邏輯
是:relay
把miner
的環路資料,和第一點一樣,通過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
複製程式碼
-
OrderManager
和AccountManager
中註冊的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.GatewayNewOrder
由IPFS
分發OrderManager
裡面的eventemitter.NewOrder
- 由
gateway.go
接收到GatewayNewOrder
之後分發。 - 客戶端呼叫
WalletService
的 APISubmitOrder
後觸發
- 由
-
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(ðBlockNumber); 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
則和其所在公鏈互動。