# 磁力兑换实现
# 数据结构
磁力兑换是磁力合约的使用形式,磁力合约将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,这样订单就撮合完成了。