decred.org/dcrdex@v1.0.5/server/market/market_test.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package market
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/hex"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"math/rand"
    14  	"runtime"
    15  	"strings"
    16  	"sync"
    17  	"sync/atomic"
    18  	"testing"
    19  	"time"
    20  
    21  	"decred.org/dcrdex/dex"
    22  	"decred.org/dcrdex/dex/calc"
    23  	"decred.org/dcrdex/dex/candles"
    24  	"decred.org/dcrdex/dex/msgjson"
    25  	"decred.org/dcrdex/dex/order"
    26  	"decred.org/dcrdex/dex/order/test"
    27  	"decred.org/dcrdex/server/account"
    28  	"decred.org/dcrdex/server/asset"
    29  	"decred.org/dcrdex/server/coinlock"
    30  	"decred.org/dcrdex/server/db"
    31  	"decred.org/dcrdex/server/matcher"
    32  	"decred.org/dcrdex/server/swap"
    33  )
    34  
    35  type TArchivist struct {
    36  	mtx                  sync.Mutex
    37  	poisonEpochOrder     order.Order
    38  	orderWithKnownCommit order.OrderID
    39  	commitForKnownOrder  order.Commitment
    40  	bookedOrders         []*order.LimitOrder
    41  	canceledOrders       []*order.LimitOrder
    42  	archivedCancels      []*order.CancelOrder
    43  	epochInserted        chan struct{}
    44  	revoked              order.Order
    45  }
    46  
    47  func (ta *TArchivist) Close() error           { return nil }
    48  func (ta *TArchivist) LastErr() error         { return nil }
    49  func (ta *TArchivist) Fatal() <-chan struct{} { return nil }
    50  func (ta *TArchivist) Order(oid order.OrderID, base, quote uint32) (order.Order, order.OrderStatus, error) {
    51  	return nil, order.OrderStatusUnknown, errors.New("boom")
    52  }
    53  func (ta *TArchivist) BookOrders(base, quote uint32) ([]*order.LimitOrder, error) {
    54  	ta.mtx.Lock()
    55  	defer ta.mtx.Unlock()
    56  	return ta.bookedOrders, nil
    57  }
    58  func (ta *TArchivist) EpochOrders(base, quote uint32) ([]order.Order, error) {
    59  	return nil, nil
    60  }
    61  func (ta *TArchivist) MarketMatches(base, quote uint32) ([]*db.MatchDataWithCoins, error) {
    62  	return nil, nil
    63  }
    64  func (ta *TArchivist) FlushBook(base, quote uint32) (sells, buys []order.OrderID, err error) {
    65  	ta.mtx.Lock()
    66  	defer ta.mtx.Unlock()
    67  	for _, lo := range ta.bookedOrders {
    68  		if lo.Sell {
    69  			sells = append(sells, lo.ID())
    70  		} else {
    71  			buys = append(buys, lo.ID())
    72  		}
    73  	}
    74  	ta.bookedOrders = nil
    75  	return
    76  }
    77  func (ta *TArchivist) NewArchivedCancel(ord *order.CancelOrder, epochID, epochDur int64) error {
    78  	if ta.archivedCancels != nil {
    79  		ta.archivedCancels = append(ta.archivedCancels, ord)
    80  	}
    81  	return nil
    82  }
    83  func (ta *TArchivist) ActiveOrderCoins(base, quote uint32) (baseCoins, quoteCoins map[order.OrderID][]order.CoinID, err error) {
    84  	return make(map[order.OrderID][]order.CoinID), make(map[order.OrderID][]order.CoinID), nil
    85  }
    86  func (ta *TArchivist) UserOrders(ctx context.Context, aid account.AccountID, base, quote uint32) ([]order.Order, []order.OrderStatus, error) {
    87  	return nil, nil, errors.New("boom")
    88  }
    89  func (ta *TArchivist) UserOrderStatuses(aid account.AccountID, base, quote uint32, oids []order.OrderID) ([]*db.OrderStatus, error) {
    90  	return nil, errors.New("boom")
    91  }
    92  func (ta *TArchivist) ActiveUserOrderStatuses(aid account.AccountID) ([]*db.OrderStatus, error) {
    93  	return nil, errors.New("boom")
    94  }
    95  func (ta *TArchivist) OrderWithCommit(ctx context.Context, commit order.Commitment) (found bool, oid order.OrderID, err error) {
    96  	ta.mtx.Lock()
    97  	defer ta.mtx.Unlock()
    98  	if commit == ta.commitForKnownOrder {
    99  		return true, ta.orderWithKnownCommit, nil
   100  	}
   101  	return
   102  }
   103  func (ta *TArchivist) failOnCommitWithOrder(ord order.Order) {
   104  	ta.mtx.Lock()
   105  	ta.commitForKnownOrder = ord.Commitment()
   106  	ta.orderWithKnownCommit = ord.ID()
   107  	ta.mtx.Unlock()
   108  }
   109  func (ta *TArchivist) CompletedUserOrders(aid account.AccountID, N int) (oids []order.OrderID, compTimes []int64, err error) {
   110  	return nil, nil, nil
   111  }
   112  func (ta *TArchivist) ExecutedCancelsForUser(aid account.AccountID, N int) ([]*db.CancelRecord, error) {
   113  	return nil, nil
   114  }
   115  func (ta *TArchivist) OrderStatus(order.Order) (order.OrderStatus, order.OrderType, int64, error) {
   116  	return order.OrderStatusUnknown, order.UnknownOrderType, -1, errors.New("boom")
   117  }
   118  func (ta *TArchivist) NewEpochOrder(ord order.Order, epochIdx, epochDur int64, epochGap int32) error {
   119  	ta.mtx.Lock()
   120  	defer ta.mtx.Unlock()
   121  	if ta.poisonEpochOrder != nil && ord.ID() == ta.poisonEpochOrder.ID() {
   122  		return errors.New("barf")
   123  	}
   124  	return nil
   125  }
   126  func (ta *TArchivist) StorePreimage(ord order.Order, pi order.Preimage) error { return nil }
   127  func (ta *TArchivist) failOnEpochOrder(ord order.Order) {
   128  	ta.mtx.Lock()
   129  	ta.poisonEpochOrder = ord
   130  	ta.mtx.Unlock()
   131  }
   132  func (ta *TArchivist) InsertEpoch(ed *db.EpochResults) error {
   133  	if ta.epochInserted != nil { // the test wants to know
   134  		ta.epochInserted <- struct{}{}
   135  	}
   136  	return nil
   137  }
   138  func (ta *TArchivist) LastEpochRate(base, quote uint32) (rate uint64, err error) {
   139  	return 1, nil
   140  }
   141  func (ta *TArchivist) BookOrder(lo *order.LimitOrder) error {
   142  	ta.mtx.Lock()
   143  	defer ta.mtx.Unlock()
   144  	// Note that the other storage functions like ExecuteOrder and CancelOrder
   145  	// do not change this order slice.
   146  	ta.bookedOrders = append(ta.bookedOrders, lo)
   147  	return nil
   148  }
   149  func (ta *TArchivist) ExecuteOrder(ord order.Order) error { return nil }
   150  func (ta *TArchivist) CancelOrder(lo *order.LimitOrder) error {
   151  	if ta.canceledOrders != nil {
   152  		ta.canceledOrders = append(ta.canceledOrders, lo)
   153  	}
   154  	return nil
   155  }
   156  func (ta *TArchivist) RevokeOrder(ord order.Order) (order.OrderID, time.Time, error) {
   157  	ta.revoked = ord
   158  	return ord.ID(), time.Now(), nil
   159  }
   160  func (ta *TArchivist) RevokeOrderUncounted(order.Order) (order.OrderID, time.Time, error) {
   161  	return order.OrderID{}, time.Now(), nil
   162  }
   163  func (ta *TArchivist) SetOrderCompleteTime(ord order.Order, compTime int64) error { return nil }
   164  func (ta *TArchivist) FailCancelOrder(*order.CancelOrder) error                   { return nil }
   165  func (ta *TArchivist) UpdateOrderFilled(*order.LimitOrder) error                  { return nil }
   166  func (ta *TArchivist) UpdateOrderStatus(order.Order, order.OrderStatus) error     { return nil }
   167  
   168  // SwapArchiver for Swapper
   169  func (ta *TArchivist) ActiveSwaps() ([]*db.SwapDataFull, error) { return nil, nil }
   170  func (ta *TArchivist) InsertMatch(match *order.Match) error     { return nil }
   171  func (ta *TArchivist) MatchByID(mid order.MatchID, base, quote uint32) (*db.MatchData, error) {
   172  	return nil, nil
   173  }
   174  func (ta *TArchivist) UserMatches(aid account.AccountID, base, quote uint32) ([]*db.MatchData, error) {
   175  	return nil, nil
   176  }
   177  func (ta *TArchivist) CompletedAndAtFaultMatchStats(aid account.AccountID, lastN int) ([]*db.MatchOutcome, error) {
   178  	return nil, nil
   179  }
   180  func (ta *TArchivist) PreimageStats(user account.AccountID, lastN int) ([]*db.PreimageResult, error) {
   181  	return nil, nil
   182  }
   183  func (ta *TArchivist) ForgiveMatchFail(order.MatchID) (bool, error) { return false, nil }
   184  func (ta *TArchivist) AllActiveUserMatches(account.AccountID) ([]*db.MatchData, error) {
   185  	return nil, nil
   186  }
   187  func (ta *TArchivist) MatchStatuses(aid account.AccountID, base, quote uint32, matchIDs []order.MatchID) ([]*db.MatchStatus, error) {
   188  	return nil, nil
   189  }
   190  func (ta *TArchivist) SwapData(mid db.MarketMatchID) (order.MatchStatus, *db.SwapData, error) {
   191  	return 0, nil, nil
   192  }
   193  func (ta *TArchivist) SaveMatchAckSigA(mid db.MarketMatchID, sig []byte) error { return nil }
   194  func (ta *TArchivist) SaveMatchAckSigB(mid db.MarketMatchID, sig []byte) error { return nil }
   195  
   196  // Contract data.
   197  func (ta *TArchivist) SaveContractA(mid db.MarketMatchID, contract []byte, coinID []byte, timestamp int64) error {
   198  	return nil
   199  }
   200  func (ta *TArchivist) SaveAuditAckSigB(mid db.MarketMatchID, sig []byte) error { return nil }
   201  func (ta *TArchivist) SaveContractB(mid db.MarketMatchID, contract []byte, coinID []byte, timestamp int64) error {
   202  	return nil
   203  }
   204  func (ta *TArchivist) SaveAuditAckSigA(mid db.MarketMatchID, sig []byte) error { return nil }
   205  
   206  // Redeem data.
   207  func (ta *TArchivist) SaveRedeemA(mid db.MarketMatchID, coinID, secret []byte, timestamp int64) error {
   208  	return nil
   209  }
   210  func (ta *TArchivist) SaveRedeemAckSigB(mid db.MarketMatchID, sig []byte) error {
   211  	return nil
   212  }
   213  func (ta *TArchivist) SaveRedeemB(mid db.MarketMatchID, coinID []byte, timestamp int64) error {
   214  	return nil
   215  }
   216  func (ta *TArchivist) SetMatchInactive(mid db.MarketMatchID, forgive bool) error { return nil }
   217  func (ta *TArchivist) LoadEpochStats(uint32, uint32, []*candles.Cache) error     { return nil }
   218  
   219  type TCollector struct{}
   220  
   221  var collectorSpot = &msgjson.Spot{
   222  	Stamp: rand.Uint64(),
   223  }
   224  
   225  func (tc *TCollector) ReportEpoch(base, quote uint32, epochIdx uint64, stats *matcher.MatchCycleStats) (*msgjson.Spot, error) {
   226  	return collectorSpot, nil
   227  }
   228  
   229  type tFeeFetcher struct {
   230  	maxFeeRate uint64
   231  }
   232  
   233  func (*tFeeFetcher) FeeRate(context.Context) uint64 {
   234  	return 10
   235  }
   236  
   237  func (f *tFeeFetcher) MaxFeeRate() uint64 {
   238  	return f.maxFeeRate
   239  }
   240  
   241  func (f *tFeeFetcher) LastRate() uint64 {
   242  	return 10
   243  }
   244  
   245  func (f *tFeeFetcher) SwapFeeRate(context.Context) uint64 {
   246  	return 10
   247  }
   248  
   249  type tBalancer struct {
   250  	reqs map[string]int
   251  }
   252  
   253  func newTBalancer() *tBalancer {
   254  	return &tBalancer{make(map[string]int)}
   255  }
   256  
   257  func (b *tBalancer) CheckBalance(acctAddr string, assetID, redeemAssetID uint32, qty, lots uint64, redeems int) bool {
   258  	b.reqs[acctAddr]++
   259  	return true
   260  }
   261  
   262  func randomOrderID() order.OrderID {
   263  	pk := randomBytes(order.OrderIDSize)
   264  	var id order.OrderID
   265  	copy(id[:], pk)
   266  	return id
   267  }
   268  
   269  const (
   270  	tUserTier, tUserScore, tMaxScore = int64(1), int32(30), int32(60)
   271  )
   272  
   273  var parcelLimit = float64(calcParcelLimit(tUserTier, tUserScore, tMaxScore))
   274  
   275  func newTestMarket(opts ...any) (*Market, *TArchivist, *TAuth, func(), error) {
   276  	// The DEX will make MasterCoinLockers for each asset.
   277  	masterLockerBase := coinlock.NewMasterCoinLocker()
   278  	bookLockerBase := masterLockerBase.Book()
   279  	swapLockerBase := masterLockerBase.Swap()
   280  
   281  	masterLockerQuote := coinlock.NewMasterCoinLocker()
   282  	bookLockerQuote := masterLockerQuote.Book()
   283  	swapLockerQuote := masterLockerQuote.Swap()
   284  
   285  	epochDurationMSec := uint64(500) // 0.5 sec epoch duration
   286  	storage := &TArchivist{}
   287  	var balancer Balancer
   288  
   289  	baseAsset, quoteAsset := assetDCR, assetBTC
   290  
   291  	for _, opt := range opts {
   292  		switch optT := opt.(type) {
   293  		case *TArchivist:
   294  			storage = optT
   295  		case [2]*asset.BackedAsset:
   296  			baseAsset, quoteAsset = optT[0], optT[1]
   297  			if baseAsset.ID == assetETH.ID || baseAsset.ID == assetMATIC.ID {
   298  				bookLockerBase = nil
   299  			}
   300  			if quoteAsset.ID == assetETH.ID || quoteAsset.ID == assetMATIC.ID {
   301  				bookLockerQuote = nil
   302  			}
   303  		case *tBalancer:
   304  			balancer = optT
   305  		}
   306  
   307  	}
   308  
   309  	authMgr := &TAuth{
   310  		sends:            make([]*msgjson.Message, 0),
   311  		preimagesByMsgID: make(map[uint64]order.Preimage),
   312  		preimagesByOrdID: make(map[string]order.Preimage),
   313  	}
   314  
   315  	var swapDone func(ord order.Order, match *order.Match, fail bool)
   316  	swapperCfg := &swap.Config{
   317  		Assets: map[uint32]*swap.SwapperAsset{
   318  			assetDCR.ID:   {BackedAsset: assetDCR, Locker: swapLockerBase},
   319  			assetBTC.ID:   {BackedAsset: assetBTC, Locker: swapLockerQuote},
   320  			assetETH.ID:   {BackedAsset: assetETH},
   321  			assetMATIC.ID: {BackedAsset: assetMATIC},
   322  		},
   323  		Storage:          storage,
   324  		AuthManager:      authMgr,
   325  		BroadcastTimeout: 10 * time.Second,
   326  		TxWaitExpiration: 5 * time.Second,
   327  		LockTimeTaker:    dex.LockTimeTaker(dex.Testnet),
   328  		LockTimeMaker:    dex.LockTimeMaker(dex.Testnet),
   329  		SwapDone: func(ord order.Order, match *order.Match, fail bool) {
   330  			swapDone(ord, match, fail)
   331  		},
   332  	}
   333  	swapper, err := swap.NewSwapper(swapperCfg)
   334  	if err != nil {
   335  		panic(err.Error())
   336  	}
   337  
   338  	mbBuffer := 1.1
   339  	mktInfo, err := dex.NewMarketInfo(baseAsset.ID, quoteAsset.ID,
   340  		dcrLotSize, btcRateStep, epochDurationMSec, mbBuffer)
   341  	if err != nil {
   342  		return nil, nil, nil, func() {}, fmt.Errorf("dex.NewMarketInfo() failure: %w", err)
   343  	}
   344  
   345  	mkt, err := NewMarket(&Config{
   346  		MarketInfo:      mktInfo,
   347  		Storage:         storage,
   348  		Swapper:         swapper,
   349  		AuthManager:     authMgr,
   350  		FeeFetcherBase:  &tFeeFetcher{baseAsset.MaxFeeRate},
   351  		CoinLockerBase:  bookLockerBase,
   352  		FeeFetcherQuote: &tFeeFetcher{quoteAsset.MaxFeeRate},
   353  		CoinLockerQuote: bookLockerQuote,
   354  		DataCollector:   new(TCollector),
   355  		Balancer:        balancer,
   356  		CheckParcelLimit: func(_ account.AccountID, f MarketParcelCalculator) bool {
   357  			parcels := f(0)
   358  			return parcels <= parcelLimit
   359  		},
   360  	})
   361  	if err != nil {
   362  		return nil, nil, nil, func() {}, fmt.Errorf("Failed to create test market: %w", err)
   363  	}
   364  
   365  	swapDone = mkt.SwapDone
   366  
   367  	ssw := dex.NewStartStopWaiter(swapper)
   368  	ssw.Start(testCtx)
   369  	cleanup := func() {
   370  		ssw.Stop()
   371  		ssw.WaitForShutdown()
   372  	}
   373  
   374  	return mkt, storage, authMgr, cleanup, nil
   375  }
   376  
   377  func TestMarket_NewMarket_BookOrders(t *testing.T) {
   378  	mkt, storage, _, cleanup, err := newTestMarket()
   379  	if err != nil {
   380  		t.Fatalf("newTestMarket failure: %v", err)
   381  	}
   382  
   383  	// With no book orders in the DB, the market should have an empty book after
   384  	// construction.
   385  	_, buys, sells := mkt.Book()
   386  	if len(buys) > 0 || len(sells) > 0 {
   387  		cleanup()
   388  		t.Fatalf("Fresh market had %d buys and %d sells, expected none.",
   389  			len(buys), len(sells))
   390  	}
   391  	cleanup()
   392  
   393  	rnd.Seed(12)
   394  
   395  	randCoinDCR := func() []byte {
   396  		coinID := make([]byte, 36)
   397  		rnd.Read(coinID[:])
   398  		return coinID
   399  	}
   400  
   401  	// Now store some book orders to verify NewMarket sees them.
   402  	loBuy := makeLO(buyer3, mkRate3(0.8, 1.0), randLots(10), order.StandingTiF)
   403  	loBuy.FillAmt = mkt.marketInfo.LotSize // partial fill to cover utxo check alt. path
   404  	loSell := makeLO(seller3, mkRate3(1.0, 1.2), randLots(10)+1, order.StandingTiF)
   405  	fundingCoinDCR := randCoinDCR()
   406  	loSell.Coins = []order.CoinID{fundingCoinDCR}
   407  	// let VerifyUnspentCoin find this coin as unspent
   408  	oRig.dcr.addUTXO(&msgjson.Coin{ID: fundingCoinDCR}, 1234)
   409  
   410  	_ = storage.BookOrder(loBuy)  // the stub does not error
   411  	_ = storage.BookOrder(loSell) // the stub does not error
   412  
   413  	mkt, storage, _, cleanup, err = newTestMarket(storage)
   414  	if err != nil {
   415  		t.Fatalf("newTestMarket failure: %v", err)
   416  	}
   417  	defer cleanup()
   418  
   419  	_, buys, sells = mkt.Book()
   420  	if len(buys) != 1 || len(sells) != 1 {
   421  		t.Fatalf("Fresh market had %d buys and %d sells, expected 1 buy, 1 sell.",
   422  			len(buys), len(sells))
   423  	}
   424  	if buys[0].ID() != loBuy.ID() {
   425  		t.Errorf("booked buy order has incorrect ID. Expected %v, got %v",
   426  			loBuy.ID(), buys[0].ID())
   427  	}
   428  	if sells[0].ID() != loSell.ID() {
   429  		t.Errorf("booked sell order has incorrect ID. Expected %v, got %v",
   430  			loSell.ID(), sells[0].ID())
   431  	}
   432  
   433  	// PurgeBook should clear the in memory book and those in storage.
   434  	mkt.PurgeBook()
   435  	_, buys, sells = mkt.Book()
   436  	if len(buys) > 0 || len(sells) > 0 {
   437  		t.Fatalf("purged market had %d buys and %d sells, expected none.",
   438  			len(buys), len(sells))
   439  	}
   440  
   441  	los, _ := storage.BookOrders(mkt.marketInfo.Base, mkt.marketInfo.Quote)
   442  	if len(los) != 0 {
   443  		t.Errorf("stored book orders were not flushed")
   444  	}
   445  
   446  }
   447  
   448  func TestMarket_Book(t *testing.T) {
   449  	mkt, storage, auth, cleanup, err := newTestMarket()
   450  	if err != nil {
   451  		t.Fatalf("newTestMarket failure: %v", err)
   452  	}
   453  	defer cleanup()
   454  
   455  	rnd.Seed(0)
   456  
   457  	// Fill the book.
   458  	for i := 0; i < 8; i++ {
   459  		// Buys
   460  		lo := makeLO(buyer3, mkRate3(0.8, 1.0), randLots(10), order.StandingTiF)
   461  		if !mkt.book.Insert(lo) {
   462  			t.Fatalf("Failed to Insert order into book.")
   463  		}
   464  		//t.Logf("Inserted buy order  (rate=%10d, quantity=%d) onto book.", lo.Rate, lo.Quantity)
   465  
   466  		// Sells
   467  		lo = makeLO(seller3, mkRate3(1.0, 1.2), randLots(10), order.StandingTiF)
   468  		if !mkt.book.Insert(lo) {
   469  			t.Fatalf("Failed to Insert order into book.")
   470  		}
   471  		//t.Logf("Inserted sell order (rate=%10d, quantity=%d) onto book.", lo.Rate, lo.Quantity)
   472  	}
   473  
   474  	bestBuy, bestSell := mkt.book.Best()
   475  
   476  	marketRate := mkt.MidGap()
   477  	mktRateWant := (bestBuy.Rate + bestSell.Rate) / 2
   478  	if marketRate != mktRateWant {
   479  		t.Errorf("Market rate expected %d, got %d", mktRateWant, mktRateWant)
   480  	}
   481  
   482  	_, buys, sells := mkt.Book()
   483  	if buys[0] != bestBuy {
   484  		t.Errorf("Incorrect best buy order. Got %v, expected %v",
   485  			buys[0], bestBuy)
   486  	}
   487  	if sells[0] != bestSell {
   488  		t.Errorf("Incorrect best sell order. Got %v, expected %v",
   489  			sells[0], bestSell)
   490  	}
   491  
   492  	// unbook something not on the book
   493  	if mkt.Unbook(makeLO(buyer3, 100, 1, order.StandingTiF)) {
   494  		t.Fatalf("unbooked and order that was not on the book")
   495  	}
   496  
   497  	// unbook the best buy order
   498  	feed := mkt.OrderFeed()
   499  
   500  	if !mkt.Unbook(bestBuy) {
   501  		t.Fatalf("Failed to unbook order")
   502  	}
   503  
   504  	sig := <-feed
   505  	if sig.action != unbookAction {
   506  		t.Fatalf("did not receive unbookAction signal")
   507  	}
   508  	sigData, ok := sig.data.(sigDataUnbookedOrder)
   509  	if !ok {
   510  		t.Fatalf("incorrect sigdata type")
   511  	}
   512  	if sigData.epochIdx != -1 {
   513  		t.Fatalf("expected epoch index -1, got %d", sigData.epochIdx)
   514  	}
   515  	loUnbooked, ok := sigData.order.(*order.LimitOrder)
   516  	if !ok {
   517  		t.Fatalf("incorrect unbooked order type")
   518  	}
   519  	if loUnbooked.ID() != bestBuy.ID() {
   520  		t.Errorf("unbooked order %v, wanted %v", loUnbooked.ID(), bestBuy.ID())
   521  	}
   522  
   523  	if auth.canceledOrder != bestBuy.ID() {
   524  		t.Errorf("revoke not recorded with auth manager")
   525  	}
   526  
   527  	if storage.revoked.ID() != bestBuy.ID() {
   528  		t.Errorf("revoke not recorded in storage")
   529  	}
   530  
   531  	if lockedCoins, _ := mkt.coinsLocked(bestBuy); lockedCoins != nil {
   532  		t.Errorf("unbooked order still has locked coins: %v", lockedCoins)
   533  	}
   534  
   535  	bestBuy2, _ := mkt.book.Best()
   536  	if bestBuy2 == bestBuy {
   537  		t.Errorf("failed to unbook order")
   538  	}
   539  
   540  }
   541  
   542  func TestMarket_Suspend(t *testing.T) {
   543  	// Create the market.
   544  	mkt, _, _, cleanup, err := newTestMarket()
   545  	if err != nil {
   546  		t.Fatalf("newTestMarket failure: %v", err)
   547  		cleanup()
   548  		return
   549  	}
   550  	defer cleanup()
   551  	epochDurationMSec := int64(mkt.EpochDuration())
   552  
   553  	// Suspend before market start.
   554  	finalIdx, _ := mkt.Suspend(time.Now(), false)
   555  	if finalIdx != -1 {
   556  		t.Fatalf("not running market should not allow suspend")
   557  	}
   558  
   559  	ctx, cancel := context.WithCancel(context.Background())
   560  	defer cancel()
   561  
   562  	startEpochIdx := 2 + time.Now().UnixMilli()/epochDurationMSec
   563  	startEpochTime := time.UnixMilli(startEpochIdx * epochDurationMSec)
   564  	midPrevEpochTime := startEpochTime.Add(time.Duration(-epochDurationMSec/2) * time.Millisecond)
   565  
   566  	// ~----|-------|-------|-------|
   567  	// ^now ^prev   ^start  ^next
   568  
   569  	var wg sync.WaitGroup
   570  	wg.Add(1)
   571  	go func() {
   572  		defer wg.Done()
   573  		mkt.Start(ctx, startEpochIdx)
   574  	}()
   575  
   576  	feed := mkt.OrderFeed()
   577  	go func() {
   578  		for range feed {
   579  		}
   580  	}()
   581  
   582  	// Wait until half way through the epoch prior to start, when we know Run is
   583  	// running but the market hasn't started yet.
   584  	<-time.After(time.Until(midPrevEpochTime))
   585  
   586  	// This tests the case where m.activeEpochIdx == 0 but start is scheduled.
   587  	// The suspend (final) epoch should be the one just prior to startEpochIdx.
   588  	persist := true
   589  	finalIdx, finalTime := mkt.Suspend(time.Now(), persist)
   590  	if finalIdx != startEpochIdx-1 {
   591  		t.Fatalf("finalIdx = %d, wanted %d", finalIdx, startEpochIdx-1)
   592  	}
   593  	if !startEpochTime.Equal(finalTime) {
   594  		t.Errorf("got finalTime = %v, wanted %v", finalTime, startEpochTime)
   595  	}
   596  
   597  	if mkt.suspendEpochIdx != finalIdx {
   598  		t.Errorf("got suspendEpochIdx = %d, wanted = %d", mkt.suspendEpochIdx, finalIdx)
   599  	}
   600  
   601  	// Set a new suspend time, in the future this time.
   602  	nextEpochIdx := startEpochIdx + 1
   603  	nextEpochTime := time.UnixMilli(nextEpochIdx * epochDurationMSec)
   604  
   605  	// Just before second epoch start.
   606  	finalIdx, finalTime = mkt.Suspend(nextEpochTime.Add(-1*time.Millisecond), persist)
   607  	if finalIdx != nextEpochIdx-1 {
   608  		t.Fatalf("finalIdx = %d, wanted %d", finalIdx, nextEpochIdx-1)
   609  	}
   610  	if !nextEpochTime.Equal(finalTime) {
   611  		t.Errorf("got finalTime = %v, wanted %v", finalTime, nextEpochTime)
   612  	}
   613  
   614  	if mkt.suspendEpochIdx != finalIdx {
   615  		t.Errorf("got suspendEpochIdx = %d, wanted = %d", mkt.suspendEpochIdx, finalIdx)
   616  	}
   617  
   618  	// Exactly at second epoch start, with same result.
   619  	finalIdx, finalTime = mkt.Suspend(nextEpochTime, persist)
   620  	if finalIdx != nextEpochIdx-1 {
   621  		t.Fatalf("finalIdx = %d, wanted %d", finalIdx, nextEpochIdx-1)
   622  	}
   623  	if !nextEpochTime.Equal(finalTime) {
   624  		t.Errorf("got finalTime = %v, wanted %v", finalTime, nextEpochTime)
   625  	}
   626  
   627  	if mkt.suspendEpochIdx != finalIdx {
   628  		t.Errorf("got suspendEpochIdx = %d, wanted = %d", mkt.suspendEpochIdx, finalIdx)
   629  	}
   630  
   631  	mkt.waitForEpochOpen()
   632  
   633  	// should be running
   634  	if !mkt.Running() {
   635  		t.Fatal("the market should have be running")
   636  	}
   637  
   638  	// Wait until after suspend time.
   639  	<-time.After(time.Until(finalTime.Add(20 * time.Millisecond)))
   640  
   641  	// should be stopped
   642  	if mkt.Running() {
   643  		t.Fatal("the market should have been suspended")
   644  	}
   645  
   646  	wg.Wait()
   647  	mkt.FeedDone(feed)
   648  
   649  	// Start up again (consumer resumes the Market manually)
   650  	startEpochIdx = 1 + time.Now().UnixMilli()/epochDurationMSec
   651  	startEpochTime = time.UnixMilli(startEpochIdx * epochDurationMSec)
   652  
   653  	wg.Add(1)
   654  	go func() {
   655  		defer wg.Done()
   656  		mkt.Start(ctx, startEpochIdx)
   657  	}()
   658  
   659  	feed = mkt.OrderFeed()
   660  	go func() {
   661  		for range feed {
   662  		}
   663  	}()
   664  
   665  	mkt.waitForEpochOpen()
   666  
   667  	// should be running
   668  	if !mkt.Running() {
   669  		t.Fatal("the market should have be running")
   670  	}
   671  
   672  	// Suspend asap.
   673  	_, finalTime = mkt.SuspendASAP(persist)
   674  	<-time.After(time.Until(finalTime.Add(40 * time.Millisecond)))
   675  
   676  	// Should be stopped
   677  	if mkt.Running() {
   678  		t.Fatal("the market should have been suspended")
   679  	}
   680  
   681  	cancel()
   682  	wg.Wait()
   683  	mkt.FeedDone(feed)
   684  }
   685  
   686  func TestMarket_Suspend_Persist(t *testing.T) {
   687  	// Create the market.
   688  	mkt, storage, _, cleanup, err := newTestMarket()
   689  	if err != nil {
   690  		t.Fatalf("newTestMarket failure: %v", err)
   691  		cleanup()
   692  		return
   693  	}
   694  	defer cleanup()
   695  	epochDurationMSec := int64(mkt.EpochDuration())
   696  
   697  	ctx, cancel := context.WithCancel(context.Background())
   698  	defer cancel()
   699  
   700  	startEpochIdx := 2 + time.Now().UnixMilli()/epochDurationMSec
   701  	//startEpochTime := time.UnixMilli(startEpochIdx * epochDurationMSec)
   702  
   703  	// ~----|-------|-------|-------|
   704  	// ^now ^prev   ^start  ^next
   705  
   706  	var wg sync.WaitGroup
   707  	wg.Add(1)
   708  	go func() {
   709  		defer wg.Done()
   710  		mkt.Start(ctx, startEpochIdx)
   711  	}()
   712  
   713  	startFeedRecv := func(feed <-chan *updateSignal) {
   714  		go func() {
   715  			for range feed {
   716  			}
   717  		}()
   718  	}
   719  
   720  	// Wait until after original start time.
   721  	mkt.waitForEpochOpen()
   722  
   723  	if !mkt.Running() {
   724  		t.Fatal("the market should be running")
   725  	}
   726  
   727  	lo := makeLO(seller3, mkRate3(0.8, 1.0), randLots(10), order.StandingTiF)
   728  	ok := mkt.book.Insert(lo)
   729  	if !ok {
   730  		t.Fatalf("Failed to insert an order into Market's Book")
   731  	}
   732  	_ = storage.BookOrder(lo)
   733  
   734  	// Suspend asap with no resume.  The epoch with the limit order will be
   735  	// processed and then the market will suspend.
   736  	//wantClosedFeed = true // allow the feed receiver goroutine to return w/o error
   737  	persist := true
   738  	_, finalTime := mkt.SuspendASAP(persist)
   739  	<-time.After(time.Until(finalTime.Add(40 * time.Millisecond)))
   740  
   741  	// Wait for Run to return.
   742  	wg.Wait()
   743  
   744  	// Should be stopped
   745  	if mkt.Running() {
   746  		t.Fatal("the market should have been suspended")
   747  	}
   748  
   749  	// Verify the order is still there.
   750  	los, _ := storage.BookOrders(mkt.marketInfo.Base, mkt.marketInfo.Quote)
   751  	if len(los) == 0 {
   752  		t.Errorf("stored book orders were flushed")
   753  	}
   754  
   755  	_, buys, sells := mkt.Book()
   756  	if len(buys) != 0 {
   757  		t.Errorf("buy side of book not empty")
   758  	}
   759  	if len(sells) != 1 {
   760  		t.Errorf("sell side of book not equal to 1")
   761  	}
   762  
   763  	// Start it up again.
   764  	feed := mkt.OrderFeed()
   765  	startEpochIdx = 1 + time.Now().UnixMilli()/epochDurationMSec
   766  	//startEpochTime = time.UnixMilli(startEpochIdx * epochDurationMSec)
   767  	wg.Add(1)
   768  	go func() {
   769  		defer wg.Done()
   770  		mkt.Start(ctx, startEpochIdx)
   771  	}()
   772  
   773  	startFeedRecv(feed)
   774  
   775  	mkt.waitForEpochOpen()
   776  
   777  	if !mkt.Running() {
   778  		t.Fatal("the market should be running")
   779  	}
   780  
   781  	persist = false
   782  	_, finalTime = mkt.SuspendASAP(persist)
   783  	<-time.After(time.Until(finalTime.Add(40 * time.Millisecond)))
   784  
   785  	// Wait for Run to return.
   786  	wg.Wait()
   787  	mkt.FeedDone(feed)
   788  
   789  	// Should be stopped
   790  	if mkt.Running() {
   791  		t.Fatal("the market should have been suspended")
   792  	}
   793  
   794  	// Verify the order is gone.
   795  	los, _ = storage.BookOrders(mkt.marketInfo.Base, mkt.marketInfo.Quote)
   796  	if len(los) != 0 {
   797  		t.Errorf("stored book orders were not flushed")
   798  	}
   799  
   800  	_, buys, sells = mkt.Book()
   801  	if len(buys) != 0 {
   802  		t.Errorf("buy side of book not empty")
   803  	}
   804  	if len(sells) != 0 {
   805  		t.Errorf("sell side of book not empty")
   806  	}
   807  
   808  	if t.Failed() {
   809  		cancel()
   810  		wg.Wait()
   811  	}
   812  }
   813  
   814  func TestMarket_Run(t *testing.T) {
   815  	// This test exercises the Market's main loop, which cycles the epochs and
   816  	// queues (or not) incoming orders.
   817  
   818  	// Create the market.
   819  	mkt, storage, auth, cleanup, err := newTestMarket()
   820  	if err != nil {
   821  		t.Fatalf("newTestMarket failure: %v", err)
   822  		cleanup()
   823  		return
   824  	}
   825  	epochDurationMSec := int64(mkt.EpochDuration())
   826  	// This test wants to know when epoch order matching booking is done.
   827  	storage.epochInserted = make(chan struct{}, 1)
   828  	// and when handlePreimage is done.
   829  	auth.handlePreimageDone = make(chan struct{}, 1)
   830  
   831  	ctx, cancel := context.WithCancel(context.Background())
   832  	defer cancel()
   833  
   834  	// Check that start is delayed by an unsynced backend. Tell the Market to
   835  	// start
   836  	atomic.StoreUint32(&oRig.dcr.synced, 0)
   837  	nowEpochIdx := time.Now().UnixMilli()/epochDurationMSec + 1
   838  
   839  	unsyncedEpochIdx := nowEpochIdx + 1
   840  	unsyncedEpochTime := time.UnixMilli(unsyncedEpochIdx * epochDurationMSec)
   841  
   842  	startEpochIdx := unsyncedEpochIdx + 1
   843  	startEpochTime := time.UnixMilli(startEpochIdx * epochDurationMSec)
   844  
   845  	var wg sync.WaitGroup
   846  	wg.Add(1)
   847  	go func() {
   848  		defer wg.Done()
   849  		mkt.Start(ctx, unsyncedEpochIdx)
   850  	}()
   851  
   852  	// Make an order for the first epoch.
   853  	clientTimeMSec := startEpochIdx*epochDurationMSec + 10 // 10 ms after epoch start
   854  	lots := 1
   855  	qty := uint64(dcrLotSize * lots)
   856  	rate := uint64(1000) * dcrRateStep
   857  	aid := test.NextAccount()
   858  	pi := test.RandomPreimage()
   859  	commit := pi.Commit()
   860  	limit := &msgjson.LimitOrder{
   861  		Prefix: msgjson.Prefix{
   862  			AccountID:  aid[:],
   863  			Base:       dcrID,
   864  			Quote:      btcID,
   865  			OrderType:  msgjson.LimitOrderNum,
   866  			ClientTime: uint64(clientTimeMSec),
   867  			Commit:     commit[:],
   868  		},
   869  		Trade: msgjson.Trade{
   870  			Side:     msgjson.SellOrderNum,
   871  			Quantity: qty,
   872  			Coins:    []*msgjson.Coin{},
   873  			Address:  btcAddr,
   874  		},
   875  		Rate: rate,
   876  		TiF:  msgjson.StandingOrderNum,
   877  	}
   878  
   879  	newLimit := func() *order.LimitOrder {
   880  		return &order.LimitOrder{
   881  			P: order.Prefix{
   882  				AccountID:  aid,
   883  				BaseAsset:  limit.Base,
   884  				QuoteAsset: limit.Quote,
   885  				OrderType:  order.LimitOrderType,
   886  				ClientTime: time.UnixMilli(clientTimeMSec),
   887  				Commit:     commit,
   888  			},
   889  			T: order.Trade{
   890  				Coins:    []order.CoinID{},
   891  				Sell:     true,
   892  				Quantity: limit.Quantity,
   893  				Address:  limit.Address,
   894  			},
   895  			Rate:  limit.Rate,
   896  			Force: order.StandingTiF,
   897  		}
   898  	}
   899  
   900  	parcelQty := uint64(dcrLotSize)
   901  	maxMakerQty := parcelQty * uint64(parcelLimit)
   902  	maxTakerQty := maxMakerQty / 2
   903  
   904  	var msgID uint64
   905  	nextMsgID := func() uint64 { msgID++; return msgID }
   906  	newOR := func() *orderRecord {
   907  		return &orderRecord{
   908  			msgID: nextMsgID(),
   909  			req:   limit,
   910  			order: newLimit(),
   911  		}
   912  	}
   913  
   914  	storMsgPI := func(id uint64, pi order.Preimage) {
   915  		auth.piMtx.Lock()
   916  		auth.preimagesByMsgID[id] = pi
   917  		auth.piMtx.Unlock()
   918  	}
   919  
   920  	oRecord := newOR()
   921  	storMsgPI(oRecord.msgID, pi)
   922  	//auth.Send will update preimagesByOrderID
   923  
   924  	// Submit order before market starts running
   925  	err = mkt.SubmitOrder(oRecord)
   926  	if err == nil {
   927  		t.Error("order successfully submitted to stopped market")
   928  	}
   929  	if !errors.Is(err, ErrMarketNotRunning) {
   930  		t.Fatalf(`expected ErrMarketNotRunning ("%v"), got "%v"`, ErrMarketNotRunning, err)
   931  	}
   932  
   933  	mktStatus := mkt.Status()
   934  	if mktStatus.Running {
   935  		t.Fatalf("Market should not be running yet")
   936  	}
   937  
   938  	halfEpoch := time.Duration(epochDurationMSec/2) * time.Millisecond
   939  
   940  	<-time.After(time.Until(unsyncedEpochTime.Add(halfEpoch)))
   941  
   942  	if mkt.Running() {
   943  		t.Errorf("market running with an unsynced backend")
   944  	}
   945  
   946  	atomic.StoreUint32(&oRig.dcr.synced, 1)
   947  
   948  	<-time.After(time.Until(startEpochTime.Add(halfEpoch)))
   949  	<-storage.epochInserted
   950  
   951  	if !mkt.Running() {
   952  		t.Errorf("market not running after backend sync finished")
   953  	}
   954  
   955  	// Submit again.
   956  	limit.Quantity = dcrLotSize
   957  
   958  	oRecord = newOR()
   959  	storMsgPI(oRecord.msgID, pi)
   960  	err = mkt.SubmitOrder(oRecord)
   961  	if err != nil {
   962  		t.Fatal(err)
   963  	}
   964  
   965  	// Let the epoch cycle and the fake client respond with its preimage
   966  	// (handlePreimageResp done)...
   967  	<-auth.handlePreimageDone
   968  	// and for matching to complete (in processReadyEpoch).
   969  	<-storage.epochInserted
   970  
   971  	// Submit an immediate taker sell (taker) over user taker limit
   972  
   973  	piSell := test.RandomPreimage()
   974  	commitSell := piSell.Commit()
   975  	oRecordSell := newOR()
   976  	limit.Commit = commitSell[:]
   977  	loSell := oRecordSell.order.(*order.LimitOrder)
   978  	loSell.P.Commit = commitSell
   979  	loSell.Force = order.ImmediateTiF // likely taker
   980  	loSell.Quantity = maxTakerQty     // one lot already booked
   981  
   982  	storMsgPI(oRecordSell.msgID, pi)
   983  	err = mkt.SubmitOrder(oRecordSell)
   984  	if err == nil {
   985  		t.Fatal("should have rejected too large likely-taker")
   986  	}
   987  
   988  	// Submit a taker buy that is over user taker limit
   989  	// loSell := oRecord.order.(*order.LimitOrder)
   990  	piBuy := test.RandomPreimage()
   991  	commitBuy := piBuy.Commit()
   992  	oRecordBuy := newOR()
   993  	limit.Commit = commitBuy[:]
   994  	loBuy := oRecordBuy.order.(*order.LimitOrder)
   995  	loBuy.P.Commit = commitBuy
   996  	loBuy.Sell = false
   997  	loBuy.Quantity = maxTakerQty // One lot already booked
   998  	// rate matches with the booked sell = likely taker
   999  
  1000  	storMsgPI(oRecordBuy.msgID, piBuy)
  1001  	err = mkt.SubmitOrder(oRecordBuy)
  1002  	if err == nil {
  1003  		t.Fatal("should have rejected too large likely-taker")
  1004  	}
  1005  
  1006  	// Submit a likely taker with an acceptable limit
  1007  	loSell.Quantity = maxTakerQty - dcrLotSize // the limit
  1008  
  1009  	storMsgPI(oRecordSell.msgID, piSell)
  1010  	err = mkt.SubmitOrder(oRecordSell)
  1011  	if err != nil {
  1012  		t.Fatalf("should have allowed that likely-taker: %v", err)
  1013  	}
  1014  
  1015  	// Another in the same epoch will push over the limit
  1016  	loBuy.Quantity = dcrLotSize // just one lot
  1017  	storMsgPI(oRecordBuy.msgID, pi)
  1018  	err = mkt.SubmitOrder(oRecordBuy)
  1019  	if err == nil {
  1020  		t.Fatalf("should have rejected too likely-taker that pushed the limit with existing epoch status takers")
  1021  	}
  1022  
  1023  	// Submit a valid cancel order.
  1024  	loID := oRecord.order.ID()
  1025  	piCo := test.RandomPreimage()
  1026  	commit = piCo.Commit()
  1027  	cancelTime := time.Now().UnixMilli()
  1028  	cancelMsg := &msgjson.CancelOrder{
  1029  		Prefix: msgjson.Prefix{
  1030  			AccountID:  aid[:],
  1031  			Base:       dcrID,
  1032  			Quote:      btcID,
  1033  			OrderType:  msgjson.CancelOrderNum,
  1034  			ClientTime: uint64(cancelTime),
  1035  			Commit:     commit[:],
  1036  		},
  1037  		TargetID: loID[:],
  1038  	}
  1039  
  1040  	newCancel := func() *order.CancelOrder {
  1041  		return &order.CancelOrder{
  1042  			P: order.Prefix{
  1043  				AccountID:  aid,
  1044  				BaseAsset:  limit.Base,
  1045  				QuoteAsset: limit.Quote,
  1046  				OrderType:  order.CancelOrderType,
  1047  				ClientTime: time.UnixMilli(cancelTime),
  1048  				Commit:     commit,
  1049  			},
  1050  			TargetOrderID: loID,
  1051  		}
  1052  	}
  1053  	co := newCancel()
  1054  
  1055  	coRecord := orderRecord{
  1056  		msgID: nextMsgID(),
  1057  		req:   cancelMsg,
  1058  		order: co,
  1059  	}
  1060  
  1061  	// Cancel order w/o permission to cancel target order (the limit order from
  1062  	// above that is now booked)
  1063  	cancelTime++
  1064  	otherAccount := test.NextAccount()
  1065  	cancelMsg.ClientTime = uint64(cancelTime)
  1066  	cancelMsg.AccountID = otherAccount[:]
  1067  	coWrongAccount := newCancel()
  1068  	piBadCo := test.RandomPreimage()
  1069  	commitBadCo := piBadCo.Commit()
  1070  	coWrongAccount.Commit = commitBadCo
  1071  	coWrongAccount.AccountID = otherAccount
  1072  	coWrongAccount.ClientTime = time.UnixMilli(cancelTime)
  1073  	cancelMsg.Commit = commitBadCo[:]
  1074  	coRecordWrongAccount := orderRecord{
  1075  		msgID: nextMsgID(),
  1076  		req:   cancelMsg,
  1077  		order: coWrongAccount,
  1078  	}
  1079  
  1080  	// Submit the invalid cancel order first because it would be caught by the
  1081  	// duplicate check if we do it after the valid one is submitted.
  1082  	storMsgPI(coRecordWrongAccount.msgID, piBadCo)
  1083  	err = mkt.SubmitOrder(&coRecordWrongAccount)
  1084  	if err == nil {
  1085  		t.Errorf("An invalid order was processed, but it should not have been.")
  1086  	} else if !errors.Is(err, ErrCancelNotPermitted) {
  1087  		t.Errorf(`expected ErrCancelNotPermitted ("%v"), got "%v"`, ErrCancelNotPermitted, err)
  1088  	}
  1089  
  1090  	// Valid cancel order
  1091  	storMsgPI(coRecord.msgID, piCo)
  1092  	err = mkt.SubmitOrder(&coRecord)
  1093  	if err != nil {
  1094  		t.Fatalf("Failed to submit order: %v", err)
  1095  	}
  1096  
  1097  	// Duplicate cancel order
  1098  	piCoDup := test.RandomPreimage()
  1099  	commit = piCoDup.Commit()
  1100  	cancelTime++
  1101  	cancelMsg.ClientTime = uint64(cancelTime)
  1102  	cancelMsg.Commit = commit[:]
  1103  	coDup := newCancel()
  1104  	coDup.Commit = commit
  1105  	coDup.ClientTime = time.UnixMilli(cancelTime)
  1106  	coRecordDup := orderRecord{
  1107  		msgID: nextMsgID(),
  1108  		req:   cancelMsg,
  1109  		order: coDup,
  1110  	}
  1111  	storMsgPI(coRecordDup.msgID, piCoDup)
  1112  	err = mkt.SubmitOrder(&coRecordDup)
  1113  	if err == nil {
  1114  		t.Errorf("An duplicate cancel order was processed, but it should not have been.")
  1115  	} else if !errors.Is(err, ErrDuplicateCancelOrder) {
  1116  		t.Errorf(`expected ErrDuplicateCancelOrder ("%v"), got "%v"`, ErrDuplicateCancelOrder, err)
  1117  	}
  1118  
  1119  	// Let the epoch cycle and the fake client respond with its preimage
  1120  	// (handlePreimageResp done)..
  1121  	<-auth.handlePreimageDone
  1122  	// and for matching to complete (in processReadyEpoch).
  1123  	<-storage.epochInserted
  1124  
  1125  	cancel()
  1126  	wg.Wait()
  1127  	cleanup()
  1128  
  1129  	// Test duplicate order (commitment) with a new Market.
  1130  	mkt, storage, auth, cleanup, err = newTestMarket()
  1131  	if err != nil {
  1132  		t.Fatalf("newTestMarket failure: %v", err)
  1133  	}
  1134  	storage.epochInserted = make(chan struct{}, 1)
  1135  	auth.handlePreimageDone = make(chan struct{}, 1)
  1136  
  1137  	ctx, cancel = context.WithCancel(context.Background())
  1138  	defer cancel()
  1139  	wg.Add(1)
  1140  	go func() {
  1141  		defer wg.Done()
  1142  		mkt.Run(ctx)
  1143  	}()
  1144  	mkt.waitForEpochOpen()
  1145  
  1146  	// fresh oRecord
  1147  	oRecord = newOR()
  1148  	storMsgPI(oRecord.msgID, pi)
  1149  	err = mkt.SubmitOrder(oRecord)
  1150  	if err != nil {
  1151  		t.Error(err)
  1152  	}
  1153  
  1154  	// Submit another order with the same Commitment in the same Epoch.
  1155  	oRecord = newOR()
  1156  	storMsgPI(oRecord.msgID, pi)
  1157  	err = mkt.SubmitOrder(oRecord)
  1158  	if err == nil {
  1159  		t.Errorf("A duplicate order was processed, but it should not have been.")
  1160  	} else if !errors.Is(err, ErrInvalidCommitment) {
  1161  		t.Errorf(`expected ErrInvalidCommitment ("%v"), got "%v"`, ErrInvalidCommitment, err)
  1162  	}
  1163  
  1164  	// Send an order with a bad lot size.
  1165  	oRecord = newOR()
  1166  	oRecord.order.(*order.LimitOrder).Quantity += mkt.marketInfo.LotSize / 2
  1167  	storMsgPI(oRecord.msgID, pi)
  1168  	err = mkt.SubmitOrder(oRecord)
  1169  	if err == nil {
  1170  		t.Errorf("An invalid order was processed, but it should not have been.")
  1171  	} else if !errors.Is(err, ErrInvalidOrder) {
  1172  		t.Errorf(`expected ErrInvalidOrder ("%v"), got "%v"`, ErrInvalidOrder, err)
  1173  	}
  1174  
  1175  	// Rate too low
  1176  	oRecord = newOR()
  1177  	mkt.minimumRate = oRecord.order.(*order.LimitOrder).Rate + 1
  1178  	storMsgPI(oRecord.msgID, pi)
  1179  	if err = mkt.SubmitOrder(oRecord); !errors.Is(err, ErrInvalidRate) {
  1180  		t.Errorf("An invalid rate was accepted, but it should not have been.")
  1181  	}
  1182  	mkt.minimumRate = 0
  1183  
  1184  	// Let the epoch cycle and the fake client respond with its preimage
  1185  	// (handlePreimageResp done)..
  1186  	<-auth.handlePreimageDone
  1187  	// and for matching to complete (in processReadyEpoch).
  1188  	<-storage.epochInserted
  1189  
  1190  	// Submit an order with a Commitment known to the DB.
  1191  	// NOTE: disabled since the OrderWithCommit check in Market.processOrder is disabled too.
  1192  	// oRecord = newOR()
  1193  	// oRecord.order.SetTime(time.Now()) // This will register a different order ID with the DB in the next statement.
  1194  	// storage.failOnCommitWithOrder(oRecord.order)
  1195  	// storMsgPI(oRecord.msgID, pi)
  1196  	// err = mkt.SubmitOrder(oRecord) // Will re-stamp the order, but the commit will be the same.
  1197  	// if err == nil {
  1198  	// 	t.Errorf("A duplicate order was processed, but it should not have been.")
  1199  	// } else if !errors.Is(err, ErrInvalidCommitment) {
  1200  	// 	t.Errorf(`expected ErrInvalidCommitment ("%v"), got "%v"`, ErrInvalidCommitment, err)
  1201  	// }
  1202  
  1203  	// Submit an order with a zero commit.
  1204  	oRecord = newOR()
  1205  	oRecord.order.(*order.LimitOrder).Commit = order.Commitment{}
  1206  	storMsgPI(oRecord.msgID, pi)
  1207  	err = mkt.SubmitOrder(oRecord)
  1208  	if err == nil {
  1209  		t.Errorf("An order with a zero Commitment was processed, but it should not have been.")
  1210  	} else if !errors.Is(err, ErrInvalidCommitment) {
  1211  		t.Errorf(`expected ErrInvalidCommitment ("%v"), got "%v"`, ErrInvalidCommitment, err)
  1212  	}
  1213  
  1214  	// Submit an order that breaks storage somehow.
  1215  	// tweak the order's commitment+preimage so it's not a dup.
  1216  	oRecord = newOR()
  1217  	pi = test.RandomPreimage()
  1218  	commit = pi.Commit()
  1219  	lo := oRecord.order.(*order.LimitOrder)
  1220  	lo.Commit = commit
  1221  	limit.Commit = commit[:] // oRecord.req
  1222  	storMsgPI(oRecord.msgID, pi)
  1223  	storage.failOnEpochOrder(lo) // force storage to fail on this order
  1224  	if err = mkt.SubmitOrder(oRecord); !errors.Is(err, ErrInternalServer) {
  1225  		t.Errorf(`expected ErrInternalServer ("%v"), got "%v"`, ErrInternalServer, err)
  1226  	}
  1227  
  1228  	// NOTE: The Market is now stopping on its own because of the storage failure.
  1229  
  1230  	wg.Wait()
  1231  	cleanup()
  1232  }
  1233  
  1234  func TestMarket_enqueueEpoch(t *testing.T) {
  1235  	// This tests processing of a closed epoch by prepEpoch (for preimage
  1236  	// collection) and processReadyEpoch (for sending the expected book and
  1237  	// unbook messages to book subscribers registered via OrderFeed) via
  1238  	// enqueueEpoch and the epochPump.
  1239  
  1240  	mkt, _, auth, cleanup, err := newTestMarket()
  1241  	if err != nil {
  1242  		t.Fatalf("Failed to create test market: %v", err)
  1243  		return
  1244  	}
  1245  	defer cleanup()
  1246  
  1247  	rnd.Seed(0) // deterministic random data
  1248  
  1249  	// Fill the book. Preimages not needed for these.
  1250  	for i := 0; i < 8; i++ {
  1251  		// Buys
  1252  		lo := makeLO(buyer3, mkRate3(0.8, 1.0), randLots(10), order.StandingTiF)
  1253  		if !mkt.book.Insert(lo) {
  1254  			t.Fatalf("Failed to Insert order into book.")
  1255  		}
  1256  		//t.Logf("Inserted buy order (rate=%10d, quantity=%d) onto book.", lo.Rate, lo.Quantity)
  1257  
  1258  		// Sells
  1259  		lo = makeLO(seller3, mkRate3(1.0, 1.2), randLots(10), order.StandingTiF)
  1260  		if !mkt.book.Insert(lo) {
  1261  			t.Fatalf("Failed to Insert order into book.")
  1262  		}
  1263  		//t.Logf("Inserted sell order (rate=%10d, quantity=%d) onto book.", lo.Rate, lo.Quantity)
  1264  	}
  1265  
  1266  	bestBuy, bestSell := mkt.book.Best()
  1267  	bestBuyRate := bestBuy.Rate
  1268  	bestBuyQuant := bestBuy.Quantity * 3 // tweak for new shuffle seed without changing csum
  1269  	bestSellID := bestSell.ID()
  1270  
  1271  	var epochIdx, epochDur int64 = 123413513, int64(mkt.marketInfo.EpochDuration)
  1272  	eq := NewEpoch(epochIdx, epochDur)
  1273  	eID := order.EpochID{Idx: uint64(epochIdx), Dur: uint64(epochDur)}
  1274  	lo, loPI := makeLORevealed(seller3, bestBuyRate-dcrRateStep, bestBuyQuant, order.StandingTiF)
  1275  	co, coPI := makeCORevealed(buyer3, bestSellID)
  1276  	eq.Insert(lo)
  1277  	eq.Insert(co)
  1278  
  1279  	cSum, _ := hex.DecodeString("4859aa186630c2b135074037a8db42f240bbbe81c1361d8783aa605ed3f0cf90")
  1280  	seed, _ := hex.DecodeString("e061777b09170c80ce7049439bef0d69649f361ed16b500b5e53b80920813c54")
  1281  	mp := &order.MatchProof{
  1282  		Epoch:     eID,
  1283  		Preimages: []order.Preimage{loPI, coPI},
  1284  		Misses:    nil,
  1285  		CSum:      cSum,
  1286  		Seed:      seed,
  1287  	}
  1288  
  1289  	// Test with a missed preimage.
  1290  	eq2 := NewEpoch(epochIdx, epochDur)
  1291  	co2, co2PI := makeCORevealed(buyer3, randomOrderID())
  1292  	lo2, _ := makeLORevealed(seller3, bestBuyRate-dcrRateStep, bestBuyQuant, order.ImmediateTiF)
  1293  	eq2.Insert(co2)
  1294  	eq2.Insert(lo2) // lo2 will not be in preimage map (miss)
  1295  
  1296  	cSum2, _ := hex.DecodeString("a64ee6372a49f9465910ca0b556818dbc765f3c7fa21d5f40ab25bf4b73f45ed") // includes both commitments, including the miss
  1297  	seed2, _ := hex.DecodeString("aba75140b1f6edf26955a97e1b09d7b17abdc9c0b099fc73d9729501652fbf66") // includes only the provided preimage
  1298  	mp2 := &order.MatchProof{
  1299  		Epoch:     eID,
  1300  		Preimages: []order.Preimage{co2PI},
  1301  		Misses:    []order.Order{lo2},
  1302  		CSum:      cSum2,
  1303  		Seed:      seed2,
  1304  	}
  1305  
  1306  	auth.piMtx.Lock()
  1307  	auth.preimagesByOrdID[lo.UID()] = loPI
  1308  	auth.preimagesByOrdID[co.UID()] = coPI
  1309  	auth.preimagesByOrdID[co2.UID()] = co2PI
  1310  	// No lo2 (miss)
  1311  	auth.piMtx.Unlock()
  1312  
  1313  	var bookSignals []*updateSignal
  1314  	var mtx sync.Mutex
  1315  	// intercept what would go to an OrderFeed() chan of Run were running.
  1316  	notifyChan := make(chan *updateSignal, 32)
  1317  	defer close(notifyChan) // quit bookSignals receiver, but not necessary
  1318  	go func() {
  1319  		for up := range notifyChan {
  1320  			//fmt.Println("received signal", up.action)
  1321  			mtx.Lock()
  1322  			bookSignals = append(bookSignals, up)
  1323  			mtx.Unlock()
  1324  		}
  1325  	}()
  1326  
  1327  	var wg sync.WaitGroup
  1328  	defer wg.Wait() // wait for the following epoch pipeline goroutines
  1329  
  1330  	ctx, cancel := context.WithCancel(context.Background())
  1331  	defer cancel() // stop the following epoch pipeline goroutines
  1332  
  1333  	// This test does not start the entire market, so manually start the epoch
  1334  	// queue pump, and a goroutine to receive ready (preimage collection
  1335  	// completed) epochs and start matching, etc.
  1336  	ePump := newEpochPump()
  1337  	wg.Add(1)
  1338  	go func() {
  1339  		defer wg.Done()
  1340  		ePump.Run(ctx)
  1341  	}()
  1342  
  1343  	goForIt := make(chan struct{}, 1)
  1344  
  1345  	wg.Add(1)
  1346  	go func() {
  1347  		defer close(goForIt)
  1348  		defer wg.Done()
  1349  		for ep := range ePump.ready {
  1350  			t.Logf("processReadyEpoch: %d orders revealed\n", len(ep.ordersRevealed))
  1351  
  1352  			// prepEpoch has completed preimage collection.
  1353  			mkt.processReadyEpoch(ep, notifyChan) // notify is async!
  1354  			goForIt <- struct{}{}
  1355  		}
  1356  	}()
  1357  
  1358  	// MatchProof for empty epoch queue.
  1359  	mp0 := &order.MatchProof{
  1360  		Epoch: eID,
  1361  		// everything else is nil
  1362  	}
  1363  
  1364  	tests := []struct {
  1365  		name                string
  1366  		epoch               *EpochQueue
  1367  		expectedBookSignals []*updateSignal
  1368  	}{
  1369  		{
  1370  			"ok book unbook",
  1371  			eq,
  1372  			[]*updateSignal{
  1373  				{matchProofAction, sigDataMatchProof{mp}},
  1374  				{bookAction, sigDataBookedOrder{lo, epochIdx}},
  1375  				{unbookAction, sigDataUnbookedOrder{bestBuy, epochIdx}},
  1376  				{unbookAction, sigDataUnbookedOrder{bestSell, epochIdx}},
  1377  				{epochReportAction, sigDataEpochReport{epochIdx, epochDur, nil, nil, 10, 10, nil}},
  1378  			},
  1379  		},
  1380  		{
  1381  			"ok no matches or book updates, one miss",
  1382  			eq2,
  1383  			[]*updateSignal{
  1384  				{matchProofAction, sigDataMatchProof{mp2}},
  1385  				{epochReportAction, sigDataEpochReport{epochIdx, epochDur, nil, nil, 10, 10, nil}},
  1386  			},
  1387  		},
  1388  		{
  1389  			"ok empty queue",
  1390  			NewEpoch(epochIdx, epochDur),
  1391  			[]*updateSignal{
  1392  				{matchProofAction, sigDataMatchProof{mp0}},
  1393  				{epochReportAction, sigDataEpochReport{epochIdx, epochDur, nil, nil, 10, 10, nil}},
  1394  			},
  1395  		},
  1396  	}
  1397  	for _, tt := range tests {
  1398  		t.Run(tt.name, func(t *testing.T) {
  1399  			mkt.enqueueEpoch(ePump, tt.epoch)
  1400  			// Wait for processReadyEpoch, which sends on buffered (async) book
  1401  			// order feed channels.
  1402  			<-goForIt
  1403  			// Preimage collection has completed, but notifications are asynchronous.
  1404  			runtime.Gosched()                  // defer to the notify goroutine in (*Market).Run, somewhat redundant with the following sleep
  1405  			time.Sleep(250 * time.Millisecond) // let the test goroutine receive the signals on notifyChan, updating bookSignals
  1406  			// TODO: if this sleep becomes a problem, a receive(expectedNotes int) function might be needed
  1407  			mtx.Lock()
  1408  			defer mtx.Unlock() // inside this closure
  1409  			defer func() { bookSignals = []*updateSignal{} }()
  1410  			if len(bookSignals) != len(tt.expectedBookSignals) {
  1411  				t.Fatalf("expected %d book update signals, got %d",
  1412  					len(tt.expectedBookSignals), len(bookSignals))
  1413  			}
  1414  			for i, s := range bookSignals {
  1415  				exp := tt.expectedBookSignals[i]
  1416  				if exp.action != s.action {
  1417  					t.Errorf("Book signal #%d has action %d, expected %d",
  1418  						i, s.action, exp.action)
  1419  				}
  1420  
  1421  				switch sigData := s.data.(type) {
  1422  				case sigDataMatchProof:
  1423  					mp := sigData.matchProof
  1424  					wantMp := exp.data.(sigDataMatchProof).matchProof
  1425  					if !bytes.Equal(wantMp.CSum, mp.CSum) {
  1426  						t.Errorf("Book signal #%d (action %v), has CSum %x, expected %x",
  1427  							i, s.action, mp.CSum, wantMp.CSum)
  1428  					}
  1429  					if !bytes.Equal(wantMp.Seed, mp.Seed) {
  1430  						t.Errorf("Book signal #%d (action %v), has Seed %x, expected %x",
  1431  							i, s.action, mp.Seed, wantMp.Seed)
  1432  					}
  1433  					if wantMp.Epoch.Idx != mp.Epoch.Idx {
  1434  						t.Errorf("Book signal #%d (action %v), has Epoch Idx %d, expected %d",
  1435  							i, s.action, mp.Epoch.Idx, wantMp.Epoch.Idx)
  1436  					}
  1437  					if wantMp.Epoch.Dur != mp.Epoch.Dur {
  1438  						t.Errorf("Book signal #%d (action %v), has Epoch Dur %d, expected %d",
  1439  							i, s.action, mp.Epoch.Dur, wantMp.Epoch.Dur)
  1440  					}
  1441  					if len(wantMp.Preimages) != len(mp.Preimages) {
  1442  						t.Errorf("Book signal #%d (action %v), has %d Preimages, expected %d",
  1443  							i, s.action, len(mp.Preimages), len(wantMp.Preimages))
  1444  						continue
  1445  					}
  1446  					for ii := range wantMp.Preimages {
  1447  						if wantMp.Preimages[ii] != mp.Preimages[ii] {
  1448  							t.Errorf("Book signal #%d (action %v), has #%d Preimage %x, expected %x",
  1449  								i, s.action, ii, mp.Preimages[ii], wantMp.Preimages[ii])
  1450  						}
  1451  					}
  1452  					if len(wantMp.Misses) != len(mp.Misses) {
  1453  						t.Errorf("Book signal #%d (action %v), has %d Misses, expected %d",
  1454  							i, s.action, len(mp.Misses), len(wantMp.Misses))
  1455  						continue
  1456  					}
  1457  					for ii := range wantMp.Misses {
  1458  						if wantMp.Misses[ii].ID() != mp.Misses[ii].ID() {
  1459  							t.Errorf("Book signal #%d (action %v), has #%d missed Order %v, expected %v",
  1460  								i, s.action, ii, mp.Misses[ii].ID(), wantMp.Misses[ii].ID())
  1461  						}
  1462  					}
  1463  
  1464  				case sigDataBookedOrder:
  1465  					wantOrd := exp.data.(sigDataBookedOrder).order
  1466  					if wantOrd.ID() != sigData.order.ID() {
  1467  						t.Errorf("Book signal #%d (action %v) has order %v, expected %v",
  1468  							i, s.action, sigData.order.ID(), wantOrd.ID())
  1469  					}
  1470  
  1471  				case sigDataUnbookedOrder:
  1472  					wantOrd := exp.data.(sigDataUnbookedOrder).order
  1473  					if wantOrd.ID() != sigData.order.ID() {
  1474  						t.Errorf("Unbook signal #%d (action %v) has order %v, expected %v",
  1475  							i, s.action, sigData.order.ID(), wantOrd.ID())
  1476  					}
  1477  
  1478  				case sigDataNewEpoch:
  1479  					wantIdx := exp.data.(sigDataNewEpoch).idx
  1480  					if wantIdx != sigData.idx {
  1481  						t.Errorf("new epoch signal #%d (action %v) has epoch index %d, expected %d",
  1482  							i, s.action, sigData.idx, wantIdx)
  1483  					}
  1484  
  1485  				case sigDataEpochReport:
  1486  					expSig := exp.data.(sigDataEpochReport)
  1487  					if expSig.epochIdx != sigData.epochIdx {
  1488  						t.Errorf("epoch report signal #%d (action %v) has epoch index %d, expected %d",
  1489  							i, s.action, sigData.epochIdx, expSig.epochIdx)
  1490  					}
  1491  					if expSig.epochDur != sigData.epochDur {
  1492  						t.Errorf("epoch report signal #%d (action %v) has epoch duration %d, expected %d",
  1493  							i, s.action, sigData.epochDur, expSig.epochDur)
  1494  					}
  1495  				}
  1496  
  1497  			}
  1498  		})
  1499  	}
  1500  
  1501  	cancel()
  1502  }
  1503  
  1504  func TestMarket_Cancelable(t *testing.T) {
  1505  	// Create the market.
  1506  	mkt, storage, auth, cleanup, err := newTestMarket()
  1507  	if err != nil {
  1508  		t.Fatalf("newTestMarket failure: %v", err)
  1509  		return
  1510  	}
  1511  	defer cleanup()
  1512  	// This test wants to know when epoch order matching booking is done.
  1513  	storage.epochInserted = make(chan struct{}, 1)
  1514  	// and when handlePreimage is done.
  1515  	auth.handlePreimageDone = make(chan struct{}, 1)
  1516  
  1517  	epochDurationMSec := int64(mkt.EpochDuration())
  1518  	startEpochIdx := 1 + time.Now().UnixMilli()/epochDurationMSec
  1519  	ctx, cancel := context.WithCancel(context.Background())
  1520  	var wg sync.WaitGroup
  1521  	wg.Add(1)
  1522  	go func() {
  1523  		defer wg.Done()
  1524  		mkt.Start(ctx, startEpochIdx)
  1525  	}()
  1526  
  1527  	// Make an order for the first epoch.
  1528  	clientTimeMSec := startEpochIdx*epochDurationMSec + 10 // 10 ms after epoch start
  1529  	lots := dex.PerTierBaseParcelLimit
  1530  	qty := uint64(dcrLotSize * lots)
  1531  	rate := uint64(1000) * dcrRateStep
  1532  	aid := test.NextAccount()
  1533  	pi := test.RandomPreimage()
  1534  	commit := pi.Commit()
  1535  	limitMsg := &msgjson.LimitOrder{
  1536  		Prefix: msgjson.Prefix{
  1537  			AccountID:  aid[:],
  1538  			Base:       dcrID,
  1539  			Quote:      btcID,
  1540  			OrderType:  msgjson.LimitOrderNum,
  1541  			ClientTime: uint64(clientTimeMSec),
  1542  			Commit:     commit[:],
  1543  		},
  1544  		Trade: msgjson.Trade{
  1545  			Side:     msgjson.SellOrderNum,
  1546  			Quantity: qty,
  1547  			Coins:    []*msgjson.Coin{},
  1548  			Address:  btcAddr,
  1549  		},
  1550  		Rate: rate,
  1551  		TiF:  msgjson.StandingOrderNum,
  1552  	}
  1553  
  1554  	newLimit := func() *order.LimitOrder {
  1555  		return &order.LimitOrder{
  1556  			P: order.Prefix{
  1557  				AccountID:  aid,
  1558  				BaseAsset:  limitMsg.Base,
  1559  				QuoteAsset: limitMsg.Quote,
  1560  				OrderType:  order.LimitOrderType,
  1561  				ClientTime: time.UnixMilli(clientTimeMSec),
  1562  				Commit:     commit,
  1563  			},
  1564  			T: order.Trade{
  1565  				Coins:    []order.CoinID{},
  1566  				Sell:     true,
  1567  				Quantity: limitMsg.Quantity,
  1568  				Address:  limitMsg.Address,
  1569  			},
  1570  			Rate:  limitMsg.Rate,
  1571  			Force: order.StandingTiF,
  1572  		}
  1573  	}
  1574  	lo := newLimit()
  1575  
  1576  	oRecord := orderRecord{
  1577  		msgID: 1,
  1578  		req:   limitMsg,
  1579  		order: lo,
  1580  	}
  1581  
  1582  	auth.piMtx.Lock()
  1583  	auth.preimagesByMsgID[oRecord.msgID] = pi
  1584  	auth.piMtx.Unlock()
  1585  
  1586  	// Wait for the start of the epoch to submit the order.
  1587  	mkt.waitForEpochOpen()
  1588  
  1589  	if mkt.Cancelable(order.OrderID{}) {
  1590  		t.Errorf("Cancelable reported bogus order as is cancelable, " +
  1591  			"but it wasn't even submitted.")
  1592  	}
  1593  
  1594  	// Submit the standing limit order into the current epoch.
  1595  	err = mkt.SubmitOrder(&oRecord)
  1596  	if err != nil {
  1597  		t.Fatal(err)
  1598  	}
  1599  
  1600  	if !mkt.Cancelable(lo.ID()) {
  1601  		t.Errorf("Cancelable failed to report order %v as cancelable, "+
  1602  			"but it was in the epoch queue", lo)
  1603  	}
  1604  
  1605  	// Let the epoch cycle and the fake client respond with its preimage
  1606  	// (handlePreimageResp done)..
  1607  	<-auth.handlePreimageDone
  1608  	// and for matching to complete (in processReadyEpoch).
  1609  	<-storage.epochInserted
  1610  
  1611  	if !mkt.Cancelable(lo.ID()) {
  1612  		t.Errorf("Cancelable failed to report order %v as cancelable, "+
  1613  			"but it should have been booked.", lo)
  1614  	}
  1615  
  1616  	mkt.bookMtx.Lock()
  1617  	_, ok := mkt.book.Remove(lo.ID())
  1618  	mkt.bookMtx.Unlock()
  1619  	if !ok {
  1620  		t.Errorf("Failed to remove order %v from the book.", lo)
  1621  	}
  1622  
  1623  	if mkt.Cancelable(lo.ID()) {
  1624  		t.Errorf("Cancelable reported order %v as is cancelable, "+
  1625  			"but it was removed from the Book.", lo)
  1626  	}
  1627  
  1628  	cancel()
  1629  	wg.Wait()
  1630  }
  1631  
  1632  func TestMarket_handlePreimageResp(t *testing.T) {
  1633  	randomCommit := func() (com order.Commitment) {
  1634  		rnd.Read(com[:])
  1635  		return
  1636  	}
  1637  
  1638  	newOrder := func() (*order.LimitOrder, order.Preimage) {
  1639  		qty := uint64(dcrLotSize * 10)
  1640  		rate := uint64(1000) * dcrRateStep
  1641  		return makeLORevealed(seller3, rate, qty, order.StandingTiF)
  1642  	}
  1643  
  1644  	authMgr := &TAuth{}
  1645  	mkt := &Market{
  1646  		auth:    authMgr,
  1647  		storage: &TArchivist{},
  1648  	}
  1649  
  1650  	piMsg := &msgjson.PreimageResponse{
  1651  		Preimage: msgjson.Bytes{},
  1652  	}
  1653  	msg, _ := msgjson.NewResponse(5, piMsg, nil)
  1654  
  1655  	runAndReceive := func(msg *msgjson.Message, dat *piData) *order.Preimage {
  1656  		var wg sync.WaitGroup
  1657  		wg.Add(1)
  1658  		go func() {
  1659  			mkt.handlePreimageResp(msg, dat)
  1660  			wg.Done()
  1661  		}()
  1662  		piRes := <-dat.preimage
  1663  		wg.Wait()
  1664  		return piRes
  1665  	}
  1666  
  1667  	// 1. bad Message.Type: RPCParseError
  1668  	msg.Type = msgjson.Request // should be Response
  1669  	lo, pi := newOrder()
  1670  	dat := &piData{lo, make(chan *order.Preimage)}
  1671  	piRes := runAndReceive(msg, dat)
  1672  	if piRes != nil {
  1673  		t.Errorf("Expected <nil> preimage, got %v", piRes)
  1674  	}
  1675  
  1676  	// Inspect the servers rpc error response message.
  1677  	respMsg := authMgr.getSend()
  1678  	if respMsg == nil {
  1679  		t.Fatalf("no error response")
  1680  	}
  1681  	resp, _ := respMsg.Response()
  1682  	msgErr := resp.Error
  1683  	// Code 1, Message about parsing response and invalid type (1 is not response)
  1684  	if msgErr.Code != msgjson.RPCParseError {
  1685  		t.Errorf("Expected error code %d, got %d", msgjson.RPCParseError, msgErr.Code)
  1686  	}
  1687  	wantMsgPrefix := "error parsing preimage notification response"
  1688  	if !strings.Contains(msgErr.Message, wantMsgPrefix) {
  1689  		t.Errorf("Expected error message %q, got %q", wantMsgPrefix, msgErr.Message)
  1690  	}
  1691  
  1692  	// 2. empty preimage from client: InvalidPreimage
  1693  	msg, _ = msgjson.NewResponse(5, piMsg, nil)
  1694  	//lo, pi := newOrder()
  1695  	dat = &piData{lo, make(chan *order.Preimage)}
  1696  	piRes = runAndReceive(msg, dat)
  1697  	if piRes != nil {
  1698  		t.Errorf("Expected <nil> preimage, got %v", piRes)
  1699  	}
  1700  
  1701  	respMsg = authMgr.getSend()
  1702  	if respMsg == nil {
  1703  		t.Fatalf("no error response")
  1704  	}
  1705  	resp, _ = respMsg.Response()
  1706  	msgErr = resp.Error
  1707  	// 30 invalid preimage length (0 byes)
  1708  	if msgErr.Code != msgjson.InvalidPreimage {
  1709  		t.Errorf("Expected error code %d, got %d", msgjson.InvalidPreimage, msgErr.Code)
  1710  	}
  1711  	if !strings.Contains(msgErr.Message, "invalid preimage length") {
  1712  		t.Errorf("Expected error message %q, got %q",
  1713  			"invalid preimage length (0 bytes)",
  1714  			msgErr.Message)
  1715  	}
  1716  
  1717  	// 3. correct preimage length, commitment mismatch
  1718  	//lo, pi := newOrder()
  1719  	lo.Commit = randomCommit() // break the commitment
  1720  	dat = &piData{
  1721  		ord:      lo,
  1722  		preimage: make(chan *order.Preimage),
  1723  	}
  1724  	piMsg = &msgjson.PreimageResponse{
  1725  		Preimage: pi[:],
  1726  	}
  1727  
  1728  	msg, _ = msgjson.NewResponse(5, piMsg, nil)
  1729  	piRes = runAndReceive(msg, dat)
  1730  	if piRes != nil {
  1731  		t.Errorf("Expected <nil> preimage, got %v", piRes)
  1732  	}
  1733  
  1734  	respMsg = authMgr.getSend()
  1735  	if respMsg == nil {
  1736  		t.Fatalf("no error response")
  1737  	}
  1738  	resp, _ = respMsg.Response()
  1739  	msgErr = resp.Error
  1740  	// 30 invalid preimage length (0 byes)
  1741  	if msgErr.Code != msgjson.PreimageCommitmentMismatch {
  1742  		t.Errorf("Expected error code %d, got %d",
  1743  			msgjson.PreimageCommitmentMismatch, msgErr.Code)
  1744  	}
  1745  	if !strings.Contains(msgErr.Message, "does not match order commitment") {
  1746  		t.Errorf("Expected error message of the form %q, got %q",
  1747  			"preimage hash {hash} does not match order commitment {commit}",
  1748  			msgErr.Message)
  1749  	}
  1750  
  1751  	// 4. correct preimage and commit
  1752  	lo.Commit = pi.Commit() // fix the commitment
  1753  	dat = &piData{
  1754  		ord:      lo,
  1755  		preimage: make(chan *order.Preimage),
  1756  	}
  1757  	piMsg = &msgjson.PreimageResponse{
  1758  		Preimage: pi[:],
  1759  	}
  1760  
  1761  	piRes = runAndReceive(msg, dat)
  1762  	if piRes == nil {
  1763  		t.Errorf("Expected preimage %x, got <nil>", pi)
  1764  	} else if *piRes != pi {
  1765  		t.Errorf("Expected preimage %x, got %x", pi, *piRes)
  1766  	}
  1767  
  1768  	// no response this time (no error)
  1769  	respMsg = authMgr.getSend()
  1770  	if respMsg != nil {
  1771  		t.Fatalf("got error response: %d %q", respMsg.Type, string(respMsg.Payload))
  1772  	}
  1773  
  1774  	// 5. client classified server request as invalid: InvalidRequestError
  1775  	msg, _ = msgjson.NewResponse(5, nil, msgjson.NewError(msgjson.InvalidRequestError, "invalid request"))
  1776  	lo, pi = newOrder()
  1777  	dat = &piData{lo, make(chan *order.Preimage)}
  1778  	piRes = runAndReceive(msg, dat)
  1779  	if piRes != nil {
  1780  		t.Errorf("Expected <nil> preimage, got %v", piRes)
  1781  	}
  1782  
  1783  	// Inspect the servers rpc error response message.
  1784  	respMsg = authMgr.getSend()
  1785  	if respMsg != nil {
  1786  		t.Fatalf("server is not expected to respond with anything")
  1787  	}
  1788  
  1789  	// 6. payload is not msgjson.PreimageResponse, unmarshal still succeeds, but PI is nil
  1790  	notaPiMsg := new(msgjson.OrderBookSubscription)
  1791  	msg, _ = msgjson.NewResponse(5, notaPiMsg, nil)
  1792  	dat = &piData{lo, make(chan *order.Preimage)}
  1793  	piRes = runAndReceive(msg, dat)
  1794  	if piRes != nil {
  1795  		t.Errorf("Expected <nil> preimage, got %v", piRes)
  1796  	}
  1797  
  1798  	respMsg = authMgr.getSend()
  1799  	if respMsg == nil {
  1800  		t.Fatalf("no error response")
  1801  	}
  1802  	resp, _ = respMsg.Response()
  1803  	msgErr = resp.Error
  1804  	// 30 invalid preimage length (0 byes)
  1805  	if msgErr.Code != msgjson.InvalidPreimage {
  1806  		t.Errorf("Expected error code %d, got %d", msgjson.InvalidPreimage, msgErr.Code)
  1807  	}
  1808  	if !strings.Contains(msgErr.Message, "invalid preimage length") {
  1809  		t.Errorf("Expected error message %q, got %q",
  1810  			"invalid preimage length (0 bytes)",
  1811  			msgErr.Message)
  1812  	}
  1813  
  1814  	// 7. payload unmarshal error
  1815  	msg, _ = msgjson.NewResponse(5, piMsg, nil)
  1816  	msg.Payload = json.RawMessage(`{"result":1}`) // ResponsePayload with invalid Result
  1817  	dat = &piData{lo, make(chan *order.Preimage)}
  1818  	piRes = runAndReceive(msg, dat)
  1819  	if piRes != nil {
  1820  		t.Errorf("Expected <nil> preimage, got %v", piRes)
  1821  	}
  1822  
  1823  	respMsg = authMgr.getSend()
  1824  	if respMsg == nil {
  1825  		t.Fatalf("no error response")
  1826  	}
  1827  	resp, _ = respMsg.Response()
  1828  	msgErr = resp.Error
  1829  	// Code 1, Message about parsing response payload and invalid type (1 is not response)
  1830  	if msgErr.Code != msgjson.RPCParseError {
  1831  		t.Errorf("Expected error code %d, got %d", msgjson.RPCParseError, msgErr.Code)
  1832  	}
  1833  	// wrapped json.UnmarshalFieldError
  1834  	wantMsgPrefix = "error parsing preimage response payload result"
  1835  	if !strings.Contains(msgErr.Message, wantMsgPrefix) {
  1836  		t.Errorf("Expected error message %q, got %q", wantMsgPrefix, msgErr.Message)
  1837  	}
  1838  }
  1839  
  1840  func TestMarket_CancelWhileSuspended(t *testing.T) {
  1841  	mkt, storage, auth, cleanup, err := newTestMarket()
  1842  	defer cleanup()
  1843  	if err != nil {
  1844  		t.Fatalf("newTestMarket failure: %v", err)
  1845  		return
  1846  	}
  1847  
  1848  	auth.handleMatchDone = make(chan *msgjson.Message, 1)
  1849  	storage.archivedCancels = make([]*order.CancelOrder, 0, 1)
  1850  	storage.canceledOrders = make([]*order.LimitOrder, 0, 1)
  1851  
  1852  	ctx, cancel := context.WithCancel(context.Background())
  1853  	defer cancel()
  1854  
  1855  	// Insert a limit order into the book before the market has started
  1856  	lo := makeLO(buyer3, mkRate3(1.0, 1.2), 1, order.StandingTiF)
  1857  	mkt.book.Insert(lo)
  1858  	if err != nil {
  1859  		t.Fatalf("Failed to Insert order into book.")
  1860  	}
  1861  
  1862  	// Start the market
  1863  	epochDurationMSec := int64(mkt.EpochDuration())
  1864  	startEpochIdx := 2 + time.Now().UnixMilli()/epochDurationMSec
  1865  	startEpochTime := time.UnixMilli(startEpochIdx * epochDurationMSec)
  1866  	go mkt.Start(ctx, startEpochIdx)
  1867  	<-time.After(time.Until(startEpochTime.Add(50 * time.Millisecond)))
  1868  	if !mkt.Running() {
  1869  		t.Fatal("market should be running")
  1870  	}
  1871  
  1872  	// Suspend the market, persisting the existing orders
  1873  	_, finalTime := mkt.Suspend(time.Now(), true)
  1874  	<-time.After(time.Until(finalTime.Add(50 * time.Millisecond)))
  1875  	if mkt.Running() {
  1876  		t.Fatal("market should not be running")
  1877  	}
  1878  
  1879  	if mkt.book.BuyCount() != 1 {
  1880  		t.Fatalf("There should be an order in the book.")
  1881  	}
  1882  
  1883  	// Submit a valid cancel order.
  1884  	loID := lo.ID()
  1885  	piCo := test.RandomPreimage()
  1886  	commit := piCo.Commit()
  1887  	cancelTime := time.Now().UnixMilli()
  1888  	aid := buyer3.Acct
  1889  	cancelMsg := &msgjson.CancelOrder{
  1890  		Prefix: msgjson.Prefix{
  1891  			AccountID:  aid[:],
  1892  			Base:       dcrID,
  1893  			Quote:      btcID,
  1894  			OrderType:  msgjson.CancelOrderNum,
  1895  			ClientTime: uint64(cancelTime),
  1896  			Commit:     commit[:],
  1897  		},
  1898  		TargetID: loID[:],
  1899  	}
  1900  	newCancel := func() *order.CancelOrder {
  1901  		return &order.CancelOrder{
  1902  			P: order.Prefix{
  1903  				AccountID:  aid,
  1904  				BaseAsset:  lo.Base(),
  1905  				QuoteAsset: lo.Quote(),
  1906  				OrderType:  order.CancelOrderType,
  1907  				ClientTime: time.UnixMilli(cancelTime),
  1908  				Commit:     commit,
  1909  			},
  1910  			TargetOrderID: loID,
  1911  		}
  1912  	}
  1913  	co := newCancel()
  1914  	coRecord := orderRecord{
  1915  		msgID: 1,
  1916  		req:   cancelMsg,
  1917  		order: co,
  1918  	}
  1919  	err = mkt.SubmitOrder(&coRecord)
  1920  	if err != nil {
  1921  		t.Fatalf("Error submitting cancel order: %v", err)
  1922  	}
  1923  
  1924  	if mkt.book.BuyCount() != 0 {
  1925  		t.Fatalf("Did not remove order from book.")
  1926  	}
  1927  
  1928  	// Make sure that the cancel order was archived, and the limit order was
  1929  	// canceled.
  1930  	if len(storage.archivedCancels) != 1 {
  1931  		t.Fatalf("1 cancel order should be archived but there are %v", len(storage.archivedCancels))
  1932  	}
  1933  	if !bytes.Equal(storage.archivedCancels[0].ID().Bytes(), co.ID().Bytes()) {
  1934  		t.Fatalf("Archived cancel order's ID does not match expected")
  1935  	}
  1936  	if len(storage.canceledOrders) != 1 {
  1937  		t.Fatalf("1 cancel order should be archived but there are %v", len(storage.archivedCancels))
  1938  	}
  1939  	if !bytes.Equal(storage.canceledOrders[0].ID().Bytes(), lo.ID().Bytes()) {
  1940  		t.Fatalf("Cacneled limit order's ID does not match expected")
  1941  	}
  1942  
  1943  	// Make sure that we responded to the order request
  1944  	if len(auth.sends) != 1 {
  1945  		t.Fatalf("There should be 1 send, a response to the order request.")
  1946  	}
  1947  	msg := auth.sends[0]
  1948  	response := new(msgjson.OrderResult)
  1949  	msg.UnmarshalResult(response)
  1950  	if !bytes.Equal(response.OrderID, co.ID().Bytes()) {
  1951  		t.Fatalf("order response sent for the incorrect order ID")
  1952  	}
  1953  
  1954  	// Make sure that we sent the match request to the client.
  1955  	msg = <-auth.handleMatchDone
  1956  	var matches []*msgjson.Match
  1957  	err = json.Unmarshal(msg.Payload, &matches)
  1958  	if err != nil {
  1959  		t.Fatalf("failed to unmarshal match messages")
  1960  	}
  1961  	if len(matches) != 2 {
  1962  		t.Fatalf("There should be 2 payloads, one for maker and taker match each: %v", len(matches))
  1963  	}
  1964  	var taker, maker bool
  1965  	if matches[0].Side == uint8(order.Maker) || matches[1].Side == uint8(order.Maker) {
  1966  		maker = true
  1967  	}
  1968  	if matches[0].Side == uint8(order.Taker) || matches[1].Side == uint8(order.Taker) {
  1969  		taker = true
  1970  	}
  1971  	if !taker || !maker {
  1972  		t.Fatalf("There should be 2 payloads, one for maker and taker match each")
  1973  	}
  1974  }
  1975  
  1976  func TestMarket_NewMarket_AccountBased(t *testing.T) {
  1977  	testAccountAssets(t, true, false)
  1978  	testAccountAssets(t, false, true)
  1979  	testAccountAssets(t, true, true)
  1980  }
  1981  
  1982  func testAccountAssets(t *testing.T, base, quote bool) {
  1983  	storage := &TArchivist{}
  1984  	balancer := newTBalancer()
  1985  	const numPerSide = 10
  1986  	ords := make([]*order.LimitOrder, 0, numPerSide*2)
  1987  
  1988  	baseAsset, quoteAsset := assetDCR, assetBTC
  1989  	if base {
  1990  		baseAsset = assetETH
  1991  	}
  1992  	if quote {
  1993  		quoteAsset = assetMATIC
  1994  	}
  1995  
  1996  	for i := 0; i < numPerSide*2; i++ {
  1997  		writer := test.RandomWriter()
  1998  		writer.Market = &test.Market{
  1999  			Base:    baseAsset.ID,
  2000  			Quote:   quoteAsset.ID,
  2001  			LotSize: dcrLotSize,
  2002  		}
  2003  		writer.Sell = i%2 == 0
  2004  		ord := makeLO(writer, mkRate3(0.8, 1.0), randLots(10), order.StandingTiF)
  2005  		if (ord.Sell && base) || (!ord.Sell && quote) { // eth-funded order needs a account address coin.
  2006  			ord.Coins = []order.CoinID{[]byte(test.RandomAddress())}
  2007  		}
  2008  		ords = append(ords, ord)
  2009  		storage.BookOrder(ord)
  2010  	}
  2011  
  2012  	_, _, _, cleanup, err := newTestMarket(storage, balancer, [2]*asset.BackedAsset{baseAsset, quoteAsset})
  2013  	if err != nil {
  2014  		t.Fatalf("newTestMarket failure: %v", err)
  2015  	}
  2016  	defer cleanup()
  2017  
  2018  	for _, lo := range ords {
  2019  		if base && balancer.reqs[lo.BaseAccount()] == 0 {
  2020  			t.Fatalf("base balance not requested for order")
  2021  		}
  2022  		if quote && balancer.reqs[lo.QuoteAccount()] == 0 {
  2023  			t.Fatalf("quote balance not requested for order")
  2024  		}
  2025  	}
  2026  }
  2027  
  2028  func TestMarket_AccountPending(t *testing.T) {
  2029  	storage := &TArchivist{}
  2030  	writer := test.RandomWriter()
  2031  	writer.Market = &test.Market{
  2032  		Base:    assetETH.ID,
  2033  		Quote:   assetMATIC.ID,
  2034  		LotSize: dcrLotSize,
  2035  	}
  2036  
  2037  	const rate = btcRateStep * 100
  2038  	const sellLots = 10
  2039  	const buyLots = 20
  2040  	ethAddr := test.RandomAddress()
  2041  	maticAddr := test.RandomAddress()
  2042  
  2043  	writer.Sell = true
  2044  	lo := makeLO(writer, rate, sellLots, order.StandingTiF)
  2045  	lo.Coins = []order.CoinID{[]byte(ethAddr)}
  2046  	lo.Address = maticAddr
  2047  	storage.BookOrder(lo)
  2048  
  2049  	writer.Sell = false
  2050  	lo = makeLO(writer, rate, buyLots, order.StandingTiF)
  2051  	lo.Coins = []order.CoinID{[]byte(maticAddr)}
  2052  	lo.Address = ethAddr
  2053  	storage.BookOrder(lo)
  2054  
  2055  	mkt, _, _, cleanup, err := newTestMarket(storage, newTBalancer(), [2]*asset.BackedAsset{assetETH, assetMATIC})
  2056  	if err != nil {
  2057  		t.Fatalf("newTestMarket failure: %v", err)
  2058  	}
  2059  	defer cleanup()
  2060  
  2061  	checkPending := func(tag string, addr string, assetID uint32, expQty, expLots uint64, expRedeems int) {
  2062  		t.Helper()
  2063  		qty, lots, redeems := mkt.AccountPending(addr, assetID)
  2064  		if qty != expQty {
  2065  			t.Fatalf("%s: wrong quantity: wanted %d, got %d", tag, expQty, qty)
  2066  		}
  2067  		if lots != expLots {
  2068  			t.Fatalf("%s: wrong lots: wanted %d, got %d", tag, expLots, lots)
  2069  		}
  2070  		if redeems != expRedeems {
  2071  			t.Fatalf("%s: wrong redeems: wanted %d, got %d", tag, expRedeems, redeems)
  2072  		}
  2073  	}
  2074  
  2075  	checkPending("booked-only-eth", ethAddr, assetETH.ID, sellLots*dcrLotSize, sellLots, buyLots)
  2076  
  2077  	quoteQty := calc.BaseToQuote(rate, buyLots*dcrLotSize)
  2078  	checkPending("booked-only-matic", maticAddr, assetMATIC.ID, quoteQty, buyLots, sellLots)
  2079  
  2080  	const epochSellLots = 5
  2081  	writer.Sell = true
  2082  	lo = makeLO(writer, rate, epochSellLots, order.StandingTiF)
  2083  	lo.Coins = []order.CoinID{[]byte(ethAddr)}
  2084  	lo.Address = maticAddr
  2085  	mkt.epochOrders[lo.ID()] = lo
  2086  	const totalSellLots = sellLots + epochSellLots
  2087  	checkPending("with-epoch-sell-eth", ethAddr, assetETH.ID, totalSellLots*dcrLotSize, totalSellLots, buyLots)
  2088  	checkPending("with-epoch-sell-matic", maticAddr, assetMATIC.ID, quoteQty, buyLots, totalSellLots)
  2089  
  2090  	// Market buy order.
  2091  	midGap := mkt.MidGap()
  2092  	mktBuyQty := quoteQty + calc.BaseToQuote(midGap, dcrLotSize/2)
  2093  	writer.Sell = false
  2094  	mo := makeMO(writer, 0)
  2095  	mo.Quantity = mktBuyQty
  2096  	mo.Coins = []order.CoinID{[]byte(maticAddr)}
  2097  	mo.Address = ethAddr
  2098  	mkt.epochOrders[mo.ID()] = mo
  2099  	redeems := int(totalSellLots)
  2100  	totalBuyLots := buyLots + calc.QuoteToBase(midGap, mktBuyQty)/dcrLotSize
  2101  	totalQty := quoteQty + mktBuyQty
  2102  	checkPending("with-epoch-market-buy-matic", maticAddr, assetMATIC.ID, totalQty, totalBuyLots, redeems)
  2103  	checkPending("with-epoch-market-buy-eth", ethAddr, assetETH.ID, totalSellLots*dcrLotSize, totalSellLots, int(totalBuyLots))
  2104  }