您的位置:首页 > 运维架构

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

2018-07-08 09:33 501 查看

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

前序:

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

理论部分请细看其白皮书,https://github.com/Loopring/whitepaper

实际代码部分:https://github.com/Loopring/relay

目录

  • 路印协议
  • 一般应用于
  • 作用
  • 模块组成部分
  • 交易流程
  • 代码核心业务逻辑
  • 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
      通讯的广播。在接收完后,才会再分发下去,进行触发事件处理。
      ```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
      中继节点
    • 单启动
      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
    则和其所在公链交互。

    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: