-
Notifications
You must be signed in to change notification settings - Fork 92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
market: implement MarketTunnel #85
Conversation
68fdf22
to
da167e8
Compare
2fb0eb8
to
79236f6
Compare
// TODO!!! The orders must be updated to track the current backing coins, | ||
// not just the original backing coins. This is critical for partially | ||
// filled orders where each fill creates change that must then be tracked as | ||
// the new backing coins. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a big TODO. We discussed this in matrix recently, but in summary, the Order needs to know the coins that are currently used to back the order. This can change because of partial fills.
Say an order is partially filled and it stays on the books. Now say the swap succeeds and the backing coins are spent. From this swap that partially filled the order, there was change. That change would need to be tracked now as it would be used to back the order with the new/reduced remaining amount.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should try to track the change output with the coin locker. The changes in #91 left a TxID
method to hopefully deal with change outputs. See the use of (MarketTunnel).TxMonitored
by the OrderRouter
. I was guessing that the "dex-monitored" change output can be checked by asking the Swapper
whether it is currently tracking the transaction ID. The Swapper
now saves the Coins
involved in the swap, so it should be straightforward to implement.
Implementation of coin tracking through the locker would necessitate the user submitting the change coin information with the init
and redeem
route payloads, and probably some messy edge cases. Notably, right now, the exchange wallet interface from #72 and #92 don't even give the user information about the coin output. It's tracked internally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So it seems the swapStatus.coins needs to be used in the Swapper's coin locker? I think it will be clear to you what needs to be done there when you get a look at the swap.go changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it stands, I think this PR is going to fail to lock or double lock partial filled orders with several active matches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes in #91 left a
TxID
method to hopefully deal with change outputs. See the use of(MarketTunnel).TxMonitored
by theOrderRouter
. I was guessing that the "dex-monitored" change output can be checked by asking theSwapper
whether it is currently tracking the transaction ID. TheSwapper
now saves theCoins
involved in the swap, so it should be straightforward to implement.
Is the following diff what you had in mind? https://github.com/decred/dcrdex/pull/95/files#diff-5f1d20749025f318cd1a8ba0915ebd37R418-R456
func (ac *AssetCoinLocker) LockCoins(orderCoins map[order.OrderID][]CoinID) { | ||
ac.coinMtx.Lock() | ||
for oid, coins := range orderCoins { | ||
ac.lockedCoinsByOrder[oid] = coins |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this would need to be an append if the set of coins tracked by a given order changes.
} | ||
|
||
// UnlockOrderCoins unlocks any coins backing order. | ||
func (ac *AssetCoinLocker) UnlockOrderCoins(oid order.OrderID) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unlocking by order ID may prove to be inappropriate for the same reasons mentioned before, at least for the Swapper's CoinLocker. Unlocking by CoinID may be needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May prove to be a nice feature, but not a priority, IMO. Locking only the coins that have been spent is possible with no changes to the current backend interface though.
type DEXCoinLocker struct { | ||
masterLocks map[uint32]*MasterCoinLocker | ||
} | ||
|
||
func NewDEXCoinLocker(assets []uint32) *DEXCoinLocker { | ||
masterLocks := make(map[uint32]*MasterCoinLocker, len(assets)) | ||
for _, asset := range assets { | ||
masterLocks[asset] = NewMasterCoinLocker() | ||
} | ||
|
||
return &DEXCoinLocker{masterLocks} | ||
} | ||
|
||
func (c *DEXCoinLocker) CoinLocked(asset uint32, coin string) bool { | ||
locker := c.masterLocks[asset] | ||
if locker == nil { | ||
panic(fmt.Sprintf("unknown asset %d", asset)) | ||
} | ||
|
||
return locker.CoinLocked(CoinID(coin)) | ||
} | ||
|
||
func (c *DEXCoinLocker) OrderCoinsLocked(asset uint32, oid order.OrderID) []CoinID { | ||
locker := c.masterLocks[asset] | ||
if locker == nil { | ||
panic(fmt.Sprintf("unknown asset %d", asset)) | ||
} | ||
|
||
return locker.OrderCoinsLocked(oid) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These may or may not be used by the highest level DEX component, hence not commented, but kept here to facilitate the next steps.
makers = matchSet.Makers | ||
} else if o.Force == order.ImmediateTiF { | ||
// There was no match and TiF is Immediate. Fail. | ||
failed = append(failed, q) | ||
break | ||
} | ||
|
||
// Either matched or standing unmatched => passed. | ||
passed = append(passed, q) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was a bug that passed only applies to matched orders, but not newly-booked/standing unbooked orders.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of right now, it doesn't look like we are using the passed
group anywhere. Do you anticipate that we will need it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't had to use it. Will remove.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, going to remove in #95 since it touches Matcher too.
There is the massive TODO regarding the potentially changing CoinID set for an order as it is partially filled, but I think that is slightly beyond the scope of this PR. The main thing in this PR is the coinlocker types that provide a shared underlying coin locking system between book and swapper (and markets). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The coin locker API and the scope of the coin handling were a little larger than I expected, but it seems to be tuned in to the DEX's needs. Just a couple things that need attention, but otherwise looks good here.
makers = matchSet.Makers | ||
} else if o.Force == order.ImmediateTiF { | ||
// There was no match and TiF is Immediate. Fail. | ||
failed = append(failed, q) | ||
break | ||
} | ||
|
||
// Either matched or standing unmatched => passed. | ||
passed = append(passed, q) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of right now, it doesn't look like we are using the passed
group anywhere. Do you anticipate that we will need it?
type LockableAsset struct { | ||
*asset.Asset | ||
coinlock.CoinLocker // should be *coinlock.AssetCoinLocker | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is gonna get interesting. #72 and #92 move most of asset.Asset
to the dex
module, and creates a BackedAsset
in the server/asset
module. It'll all need to come together somehow.
I'm also considering proposing to rename DEXAsset
to AssetBackend
or similar, which I think is more descriptive.
@@ -327,6 +425,20 @@ func (m *Market) processOrder(rec *orderRecord, epoch *EpochQueue, errChan chan< | |||
return | |||
} | |||
|
|||
// Ensure that the received order does not use locked coins. | |||
lockedCoins := m.coinsLocked(ord) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These coins are being checked in both the order router and the market. Is that the intention?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the epoch processing loop in Market.runEpochs
runs concurrently with the order router handlers registered with the AuthManager
, and coins aren't locked until runEpochs
does Market.processOrder
, there needs to be a check somewhere in the synchronous processing pipeline of runEpochs
/processOrder
. I think we could eliminate the CoinLocked
call in (*OrderRouter).checkPrefixTrade
, but the early extra check seemed fine.
// storage, since active orders and active matches are tracked there. | ||
OutpointLocked([]byte) bool | ||
CoinLocked(coinID order.CoinID, assetID uint32) bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could also pass the DEXCoinLocker
directly to the order book router, if that makes things easier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK either way. But see what you make of my comment regarding the extra lock check in #85 (comment)
// CoinLocked checks if a coin is locked. The asset is specified since we should | ||
// not assume that a CoinID for one asset cannot be made to match another | ||
// asset's CoinID. | ||
func (m *Market) CoinLocked(asset uint32, coin coinlock.CoinID) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made a comment in the order router too, but if it's easier to pass the DEXCoinLocker
to the order router, this method could be dropped from MarketTunnel
.
A |
Bug fix in #97 is required to be merged first. |
PR #97 with the book bug fix is merged and this is rebased. Tests working now. Also added the tests for |
|
||
var _ (CoinLocker) = (*swapLocker)(nil) | ||
|
||
type coinIDKey string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is kinda icky, so I'm open to other ideas for a map key, although since there's no restriction on CoinID length in bytes, I'm at a loss for ideas... except perhaps a hash of the slice, which would then be a fixed length.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm okay with strings. I know they take a performance hit in map look ups, but I'm guessing it's not significant enough to warrant the complexity of extra IDs.
|
||
// Deep copy the coin ID (a slice) since the backing buffer may be | ||
// reused. | ||
bc := make([]byte, cLen) | ||
copy(bc, b[1:cLen+1]) | ||
c = append(c, bc) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was subtle and hard to catch, but the lib/pq driver did hit it and the backing buffers were reused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebase is going to run into the LockedAsset
/BackedAsset
thing, but this is otherwise good to go.
// Since Swapper.Negotiate is called asynchronously, we must lock coins | ||
// with the Swapper first. The swapper will unlock the coins. | ||
m.swapper.LockOrdersCoins(swapOrders) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I follow. Why can't the Swapper lock the coins asynchronously?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe what could happen is a new order could arrive that is backed by coins that should be locked by the swapper, but since execution of go m.swapper.Negotiate
is at the mercy by the Go scheduler, the new order may get accepted by the m.coinsLocked(ord)
check in processOrder
if the locking happens inside Negotiate
.
market: Add and test MidGap, improve tests. market: CoinLocker, and Market.CoinLocked swap: LockableAsset in coins map market: update Market tests with CoinLocked, ActiveOrderCoins, and Swapper config changes with LockableAsset set cancel order ServerTime in match tests CI: Add structcheck and replace gofmt with goimports. matcher: add doneOK output doneOK = passed - booked Fix passed not including unmatched standing orders. swap: LockOrdersCoins separates orders by locked asset coins internally. Add unlockOrderCoins to unlock any order.Order's coins. Unlock coins in Swapper.revoke and in processBlock for completed matches.
SelectActiveOrderCoinIDs -> SelectOrderCoinIDs Restrict SelectOrderCoinIDs to market and limit orders (no cancel).
d298d52
to
e94de6c
Compare
This implements two pieces of the
MarketTunnel
interface
:MidGap
CoinLocked