Skip to content
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

Merged
merged 6 commits into from
Dec 11, 2019
Merged

Conversation

chappjc
Copy link
Member

@chappjc chappjc commented Nov 20, 2019

This implements two pieces of the MarketTunnel interface:

  • MidGap
  • CoinLocked

server/market/market.go Outdated Show resolved Hide resolved
server/market/market.go Outdated Show resolved Hide resolved
Comment on lines +165 to +168
// 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.
Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

@chappjc chappjc Dec 3, 2019

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.

Copy link
Member Author

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 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.

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
Copy link
Member Author

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) {
Copy link
Member Author

@chappjc chappjc Dec 3, 2019

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.

Copy link
Member

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.

Comment on lines +193 to +237
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)
}
Copy link
Member Author

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)
Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member Author

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.

@chappjc
Copy link
Member Author

chappjc commented Dec 3, 2019

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).

@chappjc chappjc marked this pull request as ready for review December 3, 2019 15:23
Copy link
Member

@buck54321 buck54321 left a 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.

server/book/book.go Outdated Show resolved Hide resolved
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)
Copy link
Member

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?

server/db/driver/pg/internal/orders.go Outdated Show resolved Hide resolved
server/market/market.go Show resolved Hide resolved
type LockableAsset struct {
*asset.Asset
coinlock.CoinLocker // should be *coinlock.AssetCoinLocker
}
Copy link
Member

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)
Copy link
Member

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?

Copy link
Member Author

@chappjc chappjc Dec 5, 2019

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
Copy link
Member

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.

Copy link
Member Author

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 {
Copy link
Member

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.

@chappjc
Copy link
Member Author

chappjc commented Dec 5, 2019

A book bugfix is in progress. Do not re-review yet.

@chappjc
Copy link
Member Author

chappjc commented Dec 5, 2019

Bug fix in #97 is required to be merged first.

@chappjc
Copy link
Member Author

chappjc commented Dec 6, 2019

PR #97 with the book bug fix is merged and this is rebased. Tests working now. Also added the tests for ActiveOrderCoins, and changed it so it is not limited to standing limit orders but any active orders even those with epoch status (although that doesn't make a lot of sense on startup when the epoch is empty). The details of shutdown and start up will need to be considered more carefully in the future before this detail is resolved.


var _ (CoinLocker) = (*swapLocker)(nil)

type coinIDKey string
Copy link
Member Author

@chappjc chappjc Dec 6, 2019

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.

Copy link
Member

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.

Comment on lines +61 to +67

// 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)

Copy link
Member Author

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.

Copy link
Member

@buck54321 buck54321 left a 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.

Comment on lines +553 to +555
// Since Swapper.Negotiate is called asynchronously, we must lock coins
// with the Swapper first. The swapper will unlock the coins.
m.swapper.LockOrdersCoins(swapOrders)
Copy link
Member

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?

Copy link
Member Author

@chappjc chappjc Dec 11, 2019

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.

chappjc and others added 4 commits December 11, 2019 14:11
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).
@chappjc chappjc merged commit cc08b9b into decred:master Dec 11, 2019
@chappjc chappjc deleted the markettunnel branch December 11, 2019 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants