# 磁力兑换实现

# 数据结构

磁力兑换是磁力合约的使用形式,磁力合约将BUTXO的资产交换能力进一步放大和标准化,通过引入矩阵交易对的模式,可以在一笔交易内就完成资产间矩阵置换,多资产匹配和原子互换更加容易实现。
磁力兑换的核心步骤是链上撮合,事实上目前市面上常见的去中心化交易协议,例如0x,都是链下撮合,链上结算。这是由于以太坊性能不足而妥协的方案。而得益于高速侧链Vapor,MOV能够实现链上撮合,实现真正的去中心化。
简单的说,撮合就是获取用户的订单,并且匹配最优价格,最后把撮合成功后的对应资产转入双方的账户中。
首先从数据结构入手,为了实现链上撮合,定义了几种数据结构:

type MovUtxo struct {
	SourceID       *bc.Hash
	SourcePos      uint64
	Amount         uint64
	ControlProgram []byte
}

MovUtxo是一种专门用于MOV交易的UTXO,相比普通的UTXO,做了一部分简化,把验证相关的字段删去,只保留交易相关的内容。

type Order struct {
	FromAssetID      *bc.AssetID
	ToAssetID        *bc.AssetID
	Utxo             *MovUtxo
	RatioNumerator   int64
	RatioDenominator int64
}

定义了订单的格式FromAssetID,ToAssetID分别定义了要交易的资产,RatioNumerator,RatioDenominator定义了两种资产兑换的价格

type OrderBook struct {
	movStore database.MovStore
	dbOrders *sync.Map
	arrivalAddOrders *sync.Map
	arrivalDelOrders *sync.Map
}

定义了订单簿的格式,将存储在levelDB中的订单载入到内存中,以满足高速匹配的要求。dbOrders记录当前内存中的挂单,如果全部被撮合完了,则需要从movStore数据库中取,arrivalAddOrders,arrivalDelOrders分别是交易池中最新的挂单和撤单。

# 合约内容

contract MagneticContract(requestedAsset: Asset,
                           ratioNumerator: Integer,
                           ratioDenominator: Integer,
                           sellerProgram: Program,
                           standardProgram: Program,
                           sellerKey: PublicKey) locks valueAmount of valueAsset {
  clause partialTrade(exchangeAmount: Amount) {
   define actualAmount: Integer = exchangeAmount * ratioDenominator / ratioNumerator
   verify actualAmount > 0 && actualAmount < valueAmount
   define receiveAmount: Integer = exchangeAmount * 999 / 1000
   lock receiveAmount of requestedAsset with sellerProgram
   lock valueAmount-actualAmount of valueAsset with standardProgram
   unlock actualAmount of valueAsset
  }
  clause fullTrade() {
   define requestedAmount: Integer = valueAmount * ratioNumerator / ratioDenominator
   define requestedAmount: Integer = requestedAmount * 999 / 1000
   verify requestedAmount > 0
   lock requestedAmount of requestedAsset with sellerProgram
   unlock valueAmount of valueAsset
  }
  clause cancel(sellerSig: Signature) {
   verify checkTxSig(sellerKey, sellerSig)
   unlock valueAmount of valueAsset
  }
 }

磁力合约在MOV协议中,主要负责两件事情:第一是在共识节点打包交易的时候负责将可匹配的UTXO对生成交易传递给链内核层打包入块;第二是再验证区块的时候验证区块内交易的匹配逻辑是否符合规则,防止共识节点再匹配过程中进行作恶。用户所有的挂单和交易匹配均是链上的合约行为。
对于挂单合约的设计,磁力合约的本质目的是锁定任意数量的资产A,愿意以某种特定的汇率兑换资产B。合约的内部保存四个常量:期望兑换的资产B的ID、期望兑换的汇率、挂单用户的公钥、挂单用户接受资产B 的地址。合约可以通过三种模式解决:

  • 全部解决:合约中的所有资产A都被兑换成了资产B并且转入挂单用户的地址
  • 部分解决:合约中部分资产A被兑换成了资产B并转入挂单用户的地址,剩余资产A通过递归合约的模式重新锁回合约本身(新生成的UTXO)
  • 取消挂单:挂单用户通过私钥签名将合约中的 资产A都转回自己的地址

磁力合约状态转换图

# 撮合细节

当用户挂单后,挂单会进入交易池中,等待被撮合,撮合节点获取所有的挂单,尝试进行撮合,启动的在vapor/application/mov/mov_core.go

func (m *Core) BeforeProposalBlock(txs []*types.Tx, blockHeight uint64, gasLeft int64, isTimeout func() bool) ([]*types.Tx, error) {
	……
	orderBook, err := buildOrderBook(m.movStore, txs)
	……
	……
	matchEngine := match.NewEngine(orderBook, match.NewDefaultFeeStrategy(), rewardProgram)
	tradePairIterator := database.NewTradePairIterator(m.movStore)
	matchCollector := newMatchTxCollector(matchEngine, tradePairIterator, gasLeft, isTimeout)
	return matchCollector.result()
}

然后我们获取交易池中和之前数据库中所有的交易,构造出一个订单簿。然后使用NewEngine方法调起撮合引擎,然后使用newMatchTxCollector来并发撮合流程vapor/application/mov/match/engine.go
首先是HasMatchedTx,来检查是否存在能够匹配的订单

func (e *Engine) HasMatchedTx(tradePairs ...*common.TradePair) bool {
	if err := validateTradePairs(tradePairs); err != nil {
		return false
	}

	orders := e.orderBook.PeekOrders(tradePairs)
	if len(orders) == 0 {
		return false
	}

	return IsMatched(orders)
}

先验证一下是否存在这个交易对,然后获取这个交易对的最优订单,在使用IsMatched方法来检查是否存在可以匹配的订单。

func IsMatched(orders []*common.Order) bool {
   sortedOrders := sortOrders(orders)
   if len(sortedOrders) == 0 {
      return false
   }

   product := big.NewRat(1, 1)
   for _, order := range orders {
      product.Mul(product, big.NewRat(order.RatioNumerator, order.RatioDenominator))
   }
   one := big.NewRat(1, 1)
   return product.Cmp(one) <= 0
}

sMatched方法取出其中第一笔交易获取它的汇率,再初始化一个汇率倒数1,将汇率倒数再和下一笔交易的汇率倒数进行相乘,然后比较两个值大小,如果大于等于0,则说明有订单可以匹配,如果小于0则说明没有。

匹配细节

如果isMatched返回true值,说明存在可以撮合的订单,我们调用NextMatchedTx,首先构建匹配交易,再从orderbook中删除已匹配的交易,如果是部分匹配,则把剩下的订单再放入orderbook中。
真正的实现链上资产成交的函数buildMatchTx

func (e *Engine) buildMatchTx(orders []*common.Order) (*types.Tx, error) {
	txData := &types.TxData{Version: 1}
	……
	receivedAmounts, priceDiffs := CalcReceivedAmount(orders)
	allocatedAssets := e.feeStrategy.Allocate(receivedAmounts, priceDiffs)
	if err := addMatchTxOutput(txData, orders, receivedAmounts, allocatedAssets); err != nil {
		return nil, err
	}

	if err := e.addMatchTxFeeOutput(txData, allocatedAssets.Fees); err != nil {
		return nil, err
	}

	byteData, err := txData.MarshalText()
	……

	txData.SerializedSize = uint64(len(byteData))
	return types.NewTx(*txData), nil
}

找出订单对应的utxo,计算交易双方能够获得的资产数量,设置手续费,然后调用 addMatchTxOutput构造交易后的output。
链上资产成交的函数buildMatchTx

func (e *Engine) buildMatchTx(orders []*common.Order) (*types.Tx, error) {
	txData := &types.TxData{Version: 1}
	for _, order := range orders {
		input := types.NewSpendInput(nil, *order.Utxo.SourceID, *order.FromAssetID, order.Utxo.Amount, order.Utxo.SourcePos, order.Utxo.ControlProgram)
		txData.Inputs = append(txData.Inputs, input)
	}

	receivedAmounts, priceDiffs := CalcReceivedAmount(orders)
	allocatedAssets := e.feeStrategy.Allocate(receivedAmounts, priceDiffs)
	if err := addMatchTxOutput(txData, orders, receivedAmounts, allocatedAssets); err != nil {
		return nil, err
	}

	if err := e.addMatchTxFeeOutput(txData, allocatedAssets.Fees); err != nil {
		return nil, err
	}

	byteData, err := txData.MarshalText()
	if err != nil {
		return nil, err
	}

	txData.SerializedSize = uint64(len(byteData))
	return types.NewTx(*txData), nil
}

计算解锁合约需要的各个参数,包括需要给对方的资产数量,自己接受的资产数量,是否是部分交易等等,然后再调用setMatchTxArguments来设置磁力合约中不同的入口来真正解锁链上的UTXO,这样订单就撮合完成了。

Last Updated: 10/23/2020, 3:30:56 PM