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 }