做者:林冠宏 / 指尖下的幽靈node
前序:git
路印協議
功能很是之多及強大,本文只作入門級別的分析。github
理論部分請細看其白皮書,github.com/Loopring/wh…golang
實際代碼部分:github.com/Loopring/re…web
relay
源碼概述路印協議
Loopring
0x
、Kyber
同樣,是區塊鏈應用去中心化交易協議
之一,協議明確了使用它來進行買賣交易的行爲務必要按照它規定的模式來進行。Go語言
編寫的可應用於和區塊鏈相關的開源軟件。智能合約
,讀者注意區分二者概念。交易所
有下面例子
Relay
LPSC
路印中繼軟件
的網絡節點組成LPSC
的區塊鏈說明
及其代碼核心業務邏輯
代碼調用邏輯
是:錢包向某區塊鏈,例如以太坊的公有鏈
發起json-rpc請求
,根據請求中的合約地址address
和合約ABI
信息找到對應的LPSC合約後,再根據methodName
找到對應的的接口方法,這些接口方法固然是遵循ERC20標準的。請求受權出售Y帳戶9個B代幣。錢包APP或網頁應用中,顯示由網絡中介,例第三方API接口https://api.coinmarketcap.com
提供 代幣 B 和代幣 C 之間的當前匯率和訂單表。用戶根據這些信息,設置好本身的買賣代幣及其相關數量,例如:賣10ETH
,買50EOS
。而後建立好這個訂單請求,訂單中還有其餘信息。最後訂單被用戶Y的私鑰加密,也就是簽名後發給中繼點軟件 --- relay
redis
代碼調用邏輯
是:錢包客戶端能夠採用Http請求調用第三方API接口或使用其它方式來獲取ticker--24小時市場變化統計數據
和各代幣的價格信息以後,再經過UI界面組合顯示訂單表和匯率。用戶設置好本身的訂單信息後和簽名後,經過josn-rpc
請求向relay
發起訂單請求。算法
訂單簽名步驟sql
Keccak-256
算法對這個字節數組作散列計算獲得訂單的Hash錢包向單個或多箇中繼發送訂單及其簽名,中繼隨之更新轄下公共訂單表。路印協議不限制訂單表架構,容許「先到先得」模式;中繼能夠自行選擇訂單表設計。數據庫
代碼調用邏輯
是:客戶端向單個或多個relay
發送order request
後,relay
接收到訂單後,再各自向已知的其它relay
進行廣播,廣播的技術點在relay
源碼中的gateway
部分能夠看出使用的是IPFS--點對點的分佈式版本文件系統
技術。那麼這些relay
點它們組成的就是上面所說的路印中繼網
。隨後各relay
進行各自的訂單表refresh
,這就保證了統一。表的設計是能夠自定義的,例如字段,數據庫引擎的選擇等。json
高速處理
和杜絕偏差回滾
環路礦工撮合多筆訂單,以等同或優於用戶開出的匯率知足部分或所有訂單數額。路印協議之因此可以保證任何交易對之間的高流動性,很大程度上得益於環路礦工。若是成交匯率高於用戶 Y 的出價,環路中全部訂單皆可共享箇中利潤。而做爲報酬,環路礦工能夠選擇收取部分利潤(分潤,同時向用戶支付 LRx),或收取原定的LRx 手續費。
原定手續費LRx
的是在訂單建立的時候,由客戶端設置的
環路數學符號
環路礦工撮合多筆訂單,以等同或優於用戶開出的匯率知足部分或所有訂單數額
。它的表達式就是:Ri->j * Rj->i >= 1原始的兌換率
。10/2=4/y代碼調用邏輯
是:miner
部分的代碼,和relay
在同一個項目中。在relay
處理完訂單以後,miner
會去去訂單表拿取訂單進行撮合。造成最優環,也就是訂單成功配對,miner
這層會進行對應的數學運算。
LPSC
處理的。
原子操做
將代幣轉至用戶,同時向環路礦工和錢包支付手續費。代碼調用邏輯
是: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
的 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(ð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
則和其所在公鏈交互。