作者:林冠宏 / 指尖下的幽灵
前序:
路印协议 功能非常之多及强大,本文只做入门级别的分析。
理论部分请细看其白皮书,
实际代码部分:
- 路印协议
- 一般应用于
- 作用
- 模块组成部分
- 交易流程
- 代码核心业务逻辑
-
relay 源码概述
路印协议
- 简称
Loopring
- 和
0x 、Kyber 一样,是区块链应用去中心化交易协议 之一,协议明确了使用它来进行买卖交易的行为务必要按照它规定的模式来进行。
- 从程序的角度去描述的话,它是一份由
Go语言 编写的可应用于和区块链相关的开源软件。
- 且外,请注意它不是区块链应用中的
智能合约 ,读者注意区分两者概念。
- 虚拟货币交易所,
交易所 有下面例子
- MtGox
- Bitfinex
- 火币网
- OKEX
- ...
- 解决中心化交易存在的一系列问题
- 缺乏安全
- 交易所保存用户私钥,黑客攻击后窃走。
- 体现需要交易所批准,想象下如果交易所人员携款跑路或突然倒闭
- 缺乏透明度
- 用户买卖由中心化交易所代替执行,内部具体流程保密
- 用户资产可能被用作第三方投资
- 缺乏流动性
- 交易量多的交易所容易造成市场垄断
- 即使出过严重事故,却仍然因占巨大市场份额而其他用户不得不继续在该所交易
- 优化现有区中心话交易的一些问题
- 缺乏统一标准
- 流动性差
- 性能问题
- 导致高额的执行代码支付费用
- 挖坑延迟
- 更改/取消订单代价高
- 支持向路印网络发送请求的钱包软件
- 路印中继软件 --
Relay
- 路印区块链智能合约 --
LPSC
- 路印中继网,由多个运行了
路印中继软件 的网络节点组成
- 路印同盟链,布置了
LPSC 的区块链

说明及其代码核心业务逻辑
- 用户 Y 想交易代币,因此,授权 LPSC 出售数额为 9 的代币 B。此操作不会冻结用户的代币。订单处理期间,用户依然可以自由支配代币。
-
代码调用逻辑 是:钱包向某区块链,例如以太坊的公有链 发起json-rpc请求 ,根据请求中的合约地址address 和合约ABI 信息找到对应的LPSC合约后,再根据methodName 找到对应的的接口方法,这些接口方法当然是遵循ERC20标准的。请求授权出售Y账户9个B代币。
钱包向单个或多个中继发送订单及其签名,中继随之更新辖下公共订单表。路印协议不限制订单表架构,允许“先到先得”模式;中继可以自行选择订单表设计。
代码调用逻辑 是:客户端向单个或多个relay 发送order request 后,relay 接收到订单后,再各自向已知的其它relay 进行广播,广播的技术点在relay 源码中的gateway 部分可以看出使用的是IPFS--点对点的分布式版本文件系统 技术。那么这些relay 点它们组成的就是上面所说的路印中继网 。随后各relay 进行各自的订单表refresh ,这就保证了统一。表的设计是可以自定义的,例如字段,数据库引擎的选择等。
- 这部分已经附属解析到第三点中的互相广播部分。
- 此外,补充两点
- 节点有权选择是否及如何交流,我们可以通过修改源码来进行各种限制
- 这部分有个核心点--接收广播后的表更新算法设计,如何达到
高速处理 和杜绝误差回滚
- 环路矿工撮合多笔订单,以等同或优于用户开出的汇率满足部分或全部订单数额。路印协议之所以能够保证任何交易对之间的高流动性,很大程度上得益于环路矿工。如果成交汇率高于用户 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 这层会进行对应的数学运算。
- 这部分是
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 // 启动一个中继节点
...
}
<h4 id="节点的初始化与启动">节点的初始化与启动
<pre class="golang">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 做了表格的创建等工作
- 内存存储方面有两套
- 在导入代币信息,和市值信息的部分存在一个
问题点 :配置文件中的市场市值 数据获取的第三方接口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 通讯的广播。在接收完后,才会再分发下去,进行触发事件处理。
```golang
--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--中继节点 提供了给客户端的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--矿工节点 ,主要提供了订单环路撮合的功能,可配置有如下的部分。golang [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 进行提交到以太坊golang evaluator := miner.NewEvaluator(n.marketCapProvider,n.globalConfig.Miner)
- 匹配者
matcher.Start() golang 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 事件,然后提交到以太坊 ,再更新本地环数据表 。代码如下golang // 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 则和其所在公链交互。 (编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|