浅析 <路印协议--Loopring> 及整体分析 Relay 源码

作者:林冠宏 / 指尖下的幽灵

前序:

路印协议功能非常之多及强大,本文只做入门级别的分析。

理论部分请细看其白皮书,

实际代码部分:

<h2 id="目录">目录

  • 路印协议
  • 一般应用于
  • 作用
  • 模块组成部分
  • 交易流程
  • 代码核心业务逻辑
  • relay源码概述

<h3 id="路印协议">路印协议

  • 简称Loopring
  • 0xKyber 一样,是区块链应用去中心化交易协议之一,协议明确了使用它来进行买卖交易的行为务必要按照它规定的模式来进行。
  • 从程序的角度去描述的话,它是一份由Go语言编写的可应用于和区块链相关的开源软件。
  • 且外,请注意它不是区块链应用中的智能合约,读者注意区分两者概念。

  • 虚拟货币交易所,交易所有下面例子
    • MtGox
    • Bitfinex
    • 火币网
    • OKEX
    • ...

  • 解决中心化交易存在的一系列问题
    • 缺乏安全
      • 交易所保存用户私钥,黑客攻击后窃走。
      • 体现需要交易所批准,想象下如果交易所人员携款跑路或突然倒闭
    • 缺乏透明度
      • 用户买卖由中心化交易所代替执行,内部具体流程保密
      • 用户资产可能被用作第三方投资
    • 缺乏流动性
      • 交易量多的交易所容易造成市场垄断
      • 即使出过严重事故,却仍然因占巨大市场份额而其他用户不得不继续在该所交易
  • 优化现有区中心话交易的一些问题
    • 缺乏统一标准
    • 流动性差
      • 订单广播网络范围小
      • 订单表成交后更新速度慢
    • 性能问题
      • 导致高额的执行代码支付费用
      • 挖坑延迟
      • 更改/取消订单代价高

  • 支持向路印网络发送请求的钱包软件
    • APP
    • WEB
  • 路印中继软件 -- 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 的余额不足,会采取缩减订单数额。
    • 一旦足够的资金存入地址,订单会自动恢复至原始数额。而取消订单则需要单向手动操作且不可撤销。
    • 上面的存入地址中的地址指的是,用户在区块链中的账户地址。
  • 代码调用逻辑是: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 // 启动一个中继节点
...
}


<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 做了表格的创建等工作
  • 内存存储方面有两套
    • 基于 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通讯的广播。在接收完后,才会再分发下去,进行触发事件处理。 ```golang --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--矿工节点,主要提供了订单环路撮合的功能,可配置有如下的部分。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(&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 事件,然后提交到以太坊,再更新本地环数据表。代码如下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则和其所在公链交互。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


类型转换 1、int转string 2、string转int 3、string转float 4、用户结构类型转换
package main import s &quot;strings&quot; import &quot;fmt&quot; var p = fmt.Println func main() { p(&quot;Contains: &quot;, s.Contains(&quot;test&quo
类使用:实现一个people中有一个sayhi的方法调用功能,代码如下: 接口使用:实现上面功能,代码如下:
html代码: beego代码:
1、读取文件信息: 2、读取文件夹下的所有文件: 3、写入文件信息 4、删除文件,成功返回true,失败返回false
配置环境:Windows7+推荐IDE:LiteIDEGO下载地址:http://www.golangtc.com/downloadBeego开发文档地址:http://beego.me/docs/intro/ 安装步骤: 一、GO环境安装 二、配置系统变量 三、Beego安装 一、GO环境安装 根
golang获取程序运行路径:
Golang的文档和社区资源:为什么它可以帮助开发人员快速上手?
Golang:AI 开发者的实用工具
Golang的标准库:为什么它可以大幅度提高开发效率?
Golang的部署和运维:如何将应用程序部署到生产环境中?
高性能AI开发:Golang的优势所在
本篇文章和大家了解一下go语言开发优雅得关闭协程的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。1.简介本文将介绍首先为什么需要主...
这篇文章主要介绍了Go关闭goroutine协程的方法,具有一定借鉴价值,需要的朋友可以参考下。下面就和我一起来看看吧。1.简介本文将介绍首先为什么需要主动关闭gor...
本篇文章和大家了解一下go关闭GracefulShutdown服务的几种方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。目录Shutdown方法Regi...
这篇文章主要介绍了Go语言如何实现LRU算法的核心思想和实现过程,具有一定借鉴价值,需要的朋友可以参考下。下面就和我一起来看看吧。GO实现Redis的LRU例子常
今天小编给大家分享的是Go简单实现多租户数据库隔离的方法,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会...
这篇“Linux系统中怎么安装NSQ的Go语言客户端”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希
本文小编为大家详细介绍“怎么在Go语言中实现锁机制”,内容详细,步骤清晰,细节处理妥当,希望这篇“怎么在Go语言中实现锁机制”文章能帮助大家解决疑惑,下面...
今天小编给大家分享一下Go语言中interface类型怎么使用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考