decred.org/dcrdex@v1.0.5/client/core/simnet_trade.go (about) 1 //go:build harness && !nolgpl 2 3 package core 4 5 // The asset and dcrdex harnesses should be running before executing this test. 6 // 7 // The ./run script rebuilds the dcrdex binary with dex.testLockTimeTaker=1m 8 // and dex.testLockTimeMaker=2m before running the binary, making it possible 9 // for this test to wait for swap locktimes to expire and ensure that refundable 10 // swaps are actually refunded when the swap locktimes expire. 11 // 12 // Some errors you might encounter (especially after running this test 13 // multiple times): 14 // - error placing order rpc error: 36: coin locked 15 // likely that the DEX has not revoked a previously failed match that locked 16 // the coin that was about to be reused, waiting a couple seconds before retrying 17 // should eliminate the error. Otherwise, clear the dcrdex db and restart the 18 // dcrdex harness 19 // - error placing order not enough to cover requested funds 20 // use the affected asset harness to send funds to the affected wallet 21 // - occasional issue with fee payment confirmation 22 // restart dcr-harness and dcrdex-harness. stop dcrdex before dcr harness 23 24 import ( 25 "context" 26 "errors" 27 "fmt" 28 "os" 29 "os/exec" 30 "path/filepath" 31 "strconv" 32 "strings" 33 "sync" 34 "sync/atomic" 35 "time" 36 37 "decred.org/dcrdex/client/asset" 38 "decred.org/dcrdex/client/asset/bch" 39 "decred.org/dcrdex/client/asset/btc" 40 "decred.org/dcrdex/client/asset/dash" 41 "decred.org/dcrdex/client/asset/dcr" 42 "decred.org/dcrdex/client/asset/dgb" 43 "decred.org/dcrdex/client/asset/doge" 44 "decred.org/dcrdex/client/asset/eth" 45 "decred.org/dcrdex/client/asset/firo" 46 "decred.org/dcrdex/client/asset/ltc" 47 "decred.org/dcrdex/client/asset/polygon" 48 "decred.org/dcrdex/client/asset/zcl" 49 "decred.org/dcrdex/client/asset/zec" 50 "decred.org/dcrdex/client/comms" 51 "decred.org/dcrdex/dex" 52 "decred.org/dcrdex/dex/calc" 53 "decred.org/dcrdex/dex/config" 54 "decred.org/dcrdex/dex/msgjson" 55 dexbtc "decred.org/dcrdex/dex/networks/btc" 56 dexdgb "decred.org/dcrdex/dex/networks/dgb" 57 dexdoge "decred.org/dcrdex/dex/networks/doge" 58 dexeth "decred.org/dcrdex/dex/networks/eth" 59 "decred.org/dcrdex/dex/order" 60 "github.com/btcsuite/btcd/chaincfg/chainhash" 61 "github.com/btcsuite/btcd/wire" 62 "golang.org/x/sync/errgroup" 63 ) 64 65 var ( 66 dexHost = "127.0.0.1:17273" 67 68 homeDir = os.Getenv("HOME") 69 dextestDir = filepath.Join(homeDir, "dextest") 70 ethAlphaIPCFile = filepath.Join(dextestDir, "eth", "alpha", "node", "geth.ipc") 71 polygonAlphaIPCFile = filepath.Join(dextestDir, "polygon", "alpha", "bor", "bor.ipc") 72 73 ethUsdcID, _ = dex.BipSymbolID("usdc.eth") 74 polygonUsdcID, _ = dex.BipSymbolID("usdc.polygon") 75 ) 76 77 type SimWalletType int 78 79 const ( 80 WTCoreClone SimWalletType = iota + 1 81 WTSPVNative 82 WTElectrum 83 ) 84 85 // SimClient is the configuration for the client's wallets. 86 type SimClient struct { 87 BaseWalletType SimWalletType 88 QuoteWalletType SimWalletType 89 BaseNode string 90 QuoteNode string 91 } 92 93 type assetConfig struct { 94 id uint32 95 symbol string 96 conversionFactor uint64 97 valFmt func(any) string 98 isToken bool 99 } 100 101 var testLookup = map[string]func(s *simulationTest) error{ 102 "success": testTradeSuccess, 103 "nomakerswap": testNoMakerSwap, 104 "notakerswap": testNoTakerSwap, 105 "nomakerredeem": testNoMakerRedeem, 106 "makerghost": testMakerGhostingAfterTakerRedeem, 107 "orderstatus": testOrderStatusReconciliation, 108 "resendpending": testResendPendingRequests, 109 } 110 111 func SimTests() []string { 112 // Use this slice instead of the generating from the testLookup map so that 113 // the order doesn't change. 114 return []string{ 115 "success", 116 "nomakerswap", 117 "notakerswap", 118 "nomakerredeem", 119 "makerghost", 120 "orderstatus", 121 "resendpending", 122 } 123 } 124 125 // SimulationConfig is the test configuration. 126 type SimulationConfig struct { 127 BaseSymbol string 128 QuoteSymbol string 129 RegistrationAsset string 130 Client1 *SimClient 131 Client2 *SimClient 132 Tests []string 133 Logger dex.Logger 134 RunOnce bool 135 } 136 137 type simulationTest struct { 138 ctx context.Context 139 cancel context.CancelFunc 140 log dex.Logger 141 base *assetConfig 142 quote *assetConfig 143 regAsset uint32 144 marketName string 145 lotSize uint64 146 rateStep uint64 147 client1 *simulationClient 148 client2 *simulationClient 149 clients []*simulationClient 150 client1IsMaker bool 151 } 152 153 func (s *simulationTest) waitALittleBit() { 154 sleep := 4 * time.Second 155 if s.client1.BaseWalletType == WTElectrum || s.client1.QuoteWalletType == WTElectrum || 156 s.client2.BaseWalletType == WTElectrum || s.client2.QuoteWalletType == WTElectrum { 157 sleep = 7 * time.Second 158 } 159 s.log.Infof("Waiting a little bit, %s.", sleep) 160 time.Sleep(sleep * sleepFactor) 161 } 162 163 // RunSimulationTest runs one or more simulations tests, based on the provided 164 // SimulationConfig. 165 func RunSimulationTest(cfg *SimulationConfig) error { 166 if cfg.Client1.BaseWalletType == WTCoreClone && cfg.Client2.BaseWalletType == WTCoreClone && 167 cfg.Client1.BaseNode == cfg.Client2.BaseNode { 168 return fmt.Errorf("the %s RPC wallets for both clients are the same", cfg.BaseSymbol) 169 } 170 171 if cfg.Client1.QuoteWalletType == WTCoreClone && cfg.Client2.QuoteWalletType == WTCoreClone && 172 cfg.Client1.QuoteNode == cfg.Client2.QuoteNode { 173 return fmt.Errorf("the %s RPC wallets for both clients are the same", cfg.QuoteSymbol) 174 } 175 176 // No alpha wallets allowed until we smarten up the balance checks, I guess. 177 if cfg.Client1.BaseNode == "alpha" || cfg.Client1.QuoteNode == "alpha" || 178 cfg.Client2.BaseNode == "alpha" || cfg.Client2.QuoteNode == "alpha" { 179 return fmt.Errorf("no alpha nodes allowed") 180 } 181 182 baseID, ok := dex.BipSymbolID(cfg.BaseSymbol) 183 if !ok { 184 return fmt.Errorf("base asset %q not known", cfg.BaseSymbol) 185 } 186 baseUnitInfo, err := asset.UnitInfo(baseID) 187 if err != nil { 188 return fmt.Errorf("no unit info for %q", cfg.BaseSymbol) 189 } 190 quoteID, ok := dex.BipSymbolID(cfg.QuoteSymbol) 191 if !ok { 192 return fmt.Errorf("base asset %q not known", cfg.BaseSymbol) 193 } 194 quoteUnitInfo, err := asset.UnitInfo(quoteID) 195 if err != nil { 196 return fmt.Errorf("no unit info for %q", cfg.QuoteSymbol) 197 } 198 regAsset := baseID 199 if cfg.RegistrationAsset == cfg.QuoteSymbol { 200 regAsset = quoteID 201 } 202 valFormatter := func(valFmt func(uint64) string) func(any) string { 203 return func(vi any) string { 204 var vu uint64 205 var negative bool 206 switch vt := vi.(type) { 207 case uint64: 208 vu = vt 209 case int64: 210 if negative = vt < 0; negative { 211 vu = uint64(-vt) 212 } else { 213 vu = uint64(vt) 214 } 215 } 216 if negative { 217 return "-" + valFmt(vu) 218 } 219 return valFmt(vu) 220 } 221 } 222 223 s := &simulationTest{ 224 log: cfg.Logger, 225 base: &assetConfig{ 226 id: baseID, 227 symbol: cfg.BaseSymbol, 228 conversionFactor: baseUnitInfo.Conventional.ConversionFactor, 229 valFmt: valFormatter(baseUnitInfo.ConventionalString), 230 isToken: asset.TokenInfo(baseID) != nil, 231 }, 232 quote: &assetConfig{ 233 id: quoteID, 234 symbol: cfg.QuoteSymbol, 235 conversionFactor: quoteUnitInfo.Conventional.ConversionFactor, 236 valFmt: valFormatter(quoteUnitInfo.ConventionalString), 237 isToken: asset.TokenInfo(quoteID) != nil, 238 }, 239 regAsset: regAsset, 240 marketName: marketName(baseID, quoteID), 241 } 242 243 if err := s.setup(cfg.Client1, cfg.Client2); err != nil { 244 return fmt.Errorf("setup error: %w", err) 245 } 246 spacer := ` 247 248 ******************************************************************************* 249 ` 250 for _, testName := range cfg.Tests { 251 s.log.Infof("%s\nRunning test %q with client 1 as maker on market %q.%s", spacer, testName, s.marketName, spacer) 252 f, ok := testLookup[testName] 253 if !ok { 254 return fmt.Errorf("no test named %q", testName) 255 } 256 s.client1IsMaker = true 257 if err := f(s); err != nil { 258 return fmt.Errorf("test %q failed with client 1 as maker: %w", testName, err) 259 } 260 s.log.Infof("%s\nSUCCESS!! Test %q with client 1 as maker on market %q PASSED!%s", spacer, testName, s.marketName, spacer) 261 s.client1IsMaker = false 262 if !cfg.RunOnce && testName != "orderstatus" { 263 s.log.Infof("%s\nRunning test %q with client 2 as maker on market %q.%s", spacer, testName, s.marketName, spacer) 264 if err := f(s); err != nil { 265 return fmt.Errorf("test %q failed with client 2 as maker: %w", testName, err) 266 } 267 s.log.Infof("%s\nSUCCESS!! Test %q with client 2 as maker on market %q PASSED!%s", spacer, testName, s.marketName, spacer) 268 } 269 } 270 271 s.cancel() 272 for _, c := range s.clients { 273 c.wg.Wait() 274 c.log.Infof("Client %s done.", c.name) 275 } 276 277 return nil 278 } 279 280 func (s *simulationTest) startClients() error { 281 for _, c := range s.clients { 282 err := c.init(s.ctx) 283 if err != nil { 284 return err 285 } 286 walletNames := [2]string{} 287 var i int 288 for bipID := range c.wallets { 289 walletNames[i] = unbip(bipID) 290 i++ 291 } 292 c.log.Infof("Setting up client %s with wallets %s and %s.", c.name, walletNames[0], walletNames[1]) 293 294 c.wg.Add(1) 295 go func() { 296 defer c.wg.Done() 297 c.core.Run(s.ctx) 298 }() 299 <-c.core.Ready() 300 301 // init app 302 _, err = c.core.InitializeClient(c.appPass, nil) 303 if err != nil { 304 return err 305 } 306 c.log.Infof("Core initialized.") 307 308 c.core.Login(c.appPass) 309 310 createWallet := func(pass []byte, fund bool, form *WalletForm) error { 311 err = c.core.CreateWallet(c.appPass, pass, form) 312 if err != nil { 313 return err 314 } 315 c.log.Infof("Connected %s wallet (fund = %v).", unbip(form.AssetID), fund) 316 hctrl := newHarnessCtrl(form.AssetID) 317 // eth needs the headers to be new in order to 318 // count itself synced, so mining a few blocks here. 319 hctrl.mineBlocks(s.ctx, 1) 320 c.log.Infof("Waiting for %s wallet to sync.", unbip(form.AssetID)) 321 synced := make(chan error) 322 go func() { 323 tStart := time.Now() 324 for time.Since(tStart) < time.Second*30 { 325 if c.core.WalletState(form.AssetID).Synced { 326 synced <- nil 327 return 328 } 329 time.Sleep(time.Second) 330 } 331 synced <- fmt.Errorf("wallet never synced") 332 }() 333 if err := <-synced; err != nil { 334 return fmt.Errorf("wallet never synced") 335 } 336 c.log.Infof("Client %s %s wallet.", c.name, unbip(form.AssetID)) 337 if fund { 338 // Fund new wallet. 339 c.log.Infof("Client %s funding synced %s wallet.", c.name, unbip(form.AssetID)) 340 address := c.core.WalletState(form.AssetID).Address 341 amts := []int{10, 18, 5, 7, 1, 15, 3, 25} 342 if err := hctrl.fund(s.ctx, address, amts); err != nil { 343 return fmt.Errorf("fund error: %w", err) 344 } 345 mined: 346 for { 347 hctrl.mineBlocks(s.ctx, 2) 348 bal, err := c.core.AssetBalance(form.AssetID) 349 if err != nil { 350 return fmt.Errorf("error getting balance for %s: %w", unbip(form.AssetID), err) 351 } 352 if bal.Available > 0 { 353 break mined 354 } 355 s.log.Infof("Waiting for %s funding tx to be mined", unbip(form.AssetID)) 356 select { 357 case <-time.After(time.Second * 2): 358 case <-s.ctx.Done(): 359 return s.ctx.Err() 360 } 361 } 362 363 // Tip change after block filtering scan takes the wallet time. 364 time.Sleep(2 * time.Second * sleepFactor) 365 366 w, _ := c.core.wallet(form.AssetID) 367 approved := make(chan struct{}) 368 if approver, is := w.Wallet.(asset.TokenApprover); is { 369 const contractVer = 0 370 if _, err := approver.ApproveToken(contractVer, func() { 371 close(approved) 372 }); err != nil { 373 return fmt.Errorf("error approving %s token: %w", unbip(form.AssetID), err) 374 } 375 out: 376 for { 377 s.log.Infof("Mining more blocks to get approval tx confirmed") 378 hctrl.mineBlocks(s.ctx, 2) 379 select { 380 case <-approved: 381 c.log.Infof("%s approved", unbip(form.AssetID)) 382 break out 383 case <-time.After(time.Second * 5): 384 case <-s.ctx.Done(): 385 return s.ctx.Err() 386 } 387 } 388 } 389 } 390 391 return nil 392 } 393 394 // connect wallets 395 for assetID, wallet := range c.wallets { 396 os.RemoveAll(c.core.assetDataDirectory(assetID)) 397 398 c.log.Infof("Creating %s %s-type wallet. config = %+v.", dex.BipIDSymbol(assetID), wallet.walletType, wallet.config) 399 400 pw := wallet.pass 401 402 if wallet.parent != nil { 403 if err := createWallet(pw, wallet.fund, wallet.parent); err != nil { 404 return fmt.Errorf("error creating parent %s wallet: %w", dex.BipIDSymbol(wallet.parent.AssetID), err) 405 } 406 // For degen assets, the pass is only for the parent. 407 pw = nil 408 } 409 410 if err := createWallet(pw, wallet.fund, &WalletForm{ 411 AssetID: assetID, 412 Config: wallet.config, 413 Type: wallet.walletType, 414 }); err != nil { 415 return err 416 } 417 418 } 419 420 // Tip change after block filtering scan takes the wallet time, even 421 // longer for Electrum. 422 sleep := 2 * time.Second 423 if c.BaseWalletType == WTElectrum || c.QuoteWalletType == WTElectrum { 424 sleep = 6 * time.Second 425 } 426 time.Sleep(sleep * sleepFactor) 427 428 err = s.registerDEX(c) 429 if err != nil { 430 return err 431 } 432 } 433 434 dc, _, err := s.client1.core.dex(dexHost) 435 if err != nil { 436 return err 437 } 438 439 mktCfg := dc.marketConfig(s.marketName) 440 441 if mktCfg == nil { 442 return fmt.Errorf("market %s not found", s.marketName) 443 } 444 445 s.lotSize = mktCfg.LotSize 446 s.rateStep = mktCfg.RateStep 447 448 return nil 449 } 450 451 func (s *simulationTest) setup(cl1, cl2 *SimClient) (err error) { 452 s.client1, err = s.newClient("1", cl1) 453 if err != nil { 454 return err 455 } 456 s.client2, err = s.newClient("2", cl2) 457 if err != nil { 458 return err 459 } 460 s.clients = []*simulationClient{s.client1, s.client2} 461 s.ctx, s.cancel = context.WithCancel(context.Background()) 462 err = s.startClients() 463 if err != nil { 464 return fmt.Errorf("error starting clients: %w", err) 465 } 466 s.client1.replaceConns() 467 s.client2.replaceConns() 468 return nil 469 } 470 471 var sleepFactor time.Duration = 1 472 473 // TestTradeSuccess runs a simple trade test and ensures that the resulting 474 // trades are completed successfully. 475 func testTradeSuccess(s *simulationTest) error { 476 var qty, rate uint64 = 2 * s.lotSize, 150 * s.rateStep 477 s.client1.isSeller, s.client2.isSeller = true, false 478 return s.simpleTradeTest(qty, rate, order.MatchConfirmed) 479 } 480 481 // TestNoMakerSwap runs a simple trade test and ensures that the resulting 482 // trades fail because of the Maker not sending their init swap tx. 483 func testNoMakerSwap(s *simulationTest) error { 484 var qty, rate uint64 = 1 * s.lotSize, 100 * s.rateStep 485 s.client1.isSeller, s.client2.isSeller = false, true 486 return s.simpleTradeTest(qty, rate, order.NewlyMatched) 487 } 488 489 // TestNoTakerSwap runs a simple trade test and ensures that the resulting 490 // trades fail because of the Taker not sending their init swap tx. 491 // Also ensures that Maker's funds are refunded after locktime expires. 492 func testNoTakerSwap(s *simulationTest) error { 493 var qty, rate uint64 = 3 * s.lotSize, 200 * s.rateStep 494 s.client1.isSeller, s.client2.isSeller = true, false 495 return s.simpleTradeTest(qty, rate, order.MakerSwapCast) 496 } 497 498 // TestNoMakerRedeem runs a simple trade test and ensures that the resulting 499 // trades fail because of Maker not redeeming Taker's swap. 500 // Also ensures that both Maker and Taker's funds are refunded after their 501 // respective swap locktime expires. 502 // A scenario where Maker actually redeemed Taker's swap but did not notify 503 // Taker is handled in TestMakerGhostingAfterTakerRedeem which ensures that 504 // Taker auto-finds Maker's redeem and completes the trade by redeeming Maker's 505 // swap. 506 func testNoMakerRedeem(s *simulationTest) error { 507 var qty, rate uint64 = 1 * s.lotSize, 250 * s.rateStep 508 s.client1.isSeller, s.client2.isSeller = true, false 509 510 enable := func(client *simulationClient) { 511 client.enableWallets() 512 s.client1.filteredConn.requestFilter.Store(func(string) error { return nil }) 513 s.client2.filteredConn.requestFilter.Store(func(string) error { return nil }) 514 } 515 var killed uint32 516 preFilter1 := func(route string) error { 517 if route == msgjson.InitRoute && atomic.CompareAndSwapUint32(&killed, 0, 1) { 518 s.client1.disableWallets() 519 time.AfterFunc(s.client1.core.lockTimeTaker, func() { enable(s.client1) }) 520 } 521 return nil 522 } 523 preFilter2 := func(route string) error { 524 if route == msgjson.InitRoute && atomic.CompareAndSwapUint32(&killed, 0, 1) { 525 s.client2.disableWallets() 526 time.AfterFunc(s.client1.core.lockTimeTaker, func() { enable(s.client2) }) 527 } 528 return nil 529 } 530 531 s.client1.filteredConn.requestFilter.Store(preFilter1) 532 s.client2.filteredConn.requestFilter.Store(preFilter2) 533 534 return s.simpleTradeTest(qty, rate, order.TakerSwapCast) 535 } 536 537 // TestMakerGhostingAfterTakerRedeem places simple orders for clients 1 and 2, 538 // negotiates the resulting trades smoothly till TakerSwapCast, then Maker goes 539 // AWOL after redeeming taker's swap without notifying Taker. This test ensures 540 // that Taker auto-finds Maker's redeem, extracts the secret key and redeems 541 // Maker's swap to complete the trade. 542 // A scenario where Maker actually did NOT redeem Taker's swap is handled in 543 // testNoMakerRedeem which ensures that both parties are able to refund their 544 // swaps. 545 // TODO: What happens if FindRedemption encounters a refund instead of a redeem? 546 func testMakerGhostingAfterTakerRedeem(s *simulationTest) error { 547 var qty, rate uint64 = 1 * s.lotSize, 250 * s.rateStep 548 s.client1.isSeller, s.client2.isSeller = true, false 549 550 var bits uint8 551 anErr := errors.New("intentional error from test") 552 // Prevent the first redeemer, who must be maker, from sending 553 // redeem info to the server. 554 preFilter1 := func(route string) error { 555 if route == msgjson.RedeemRoute { 556 if bits == 0 { 557 bits = 0b1 558 } 559 if bits&0b1 != 0 { 560 return anErr 561 } 562 } 563 return nil 564 } 565 preFilter2 := func(route string) error { 566 if route == msgjson.RedeemRoute { 567 if bits == 0 { 568 bits = 0b10 569 } 570 if bits&0b10 != 0 { 571 return anErr 572 } 573 } 574 return nil 575 } 576 577 s.client1.filteredConn.requestFilter.Store(preFilter1) 578 s.client2.filteredConn.requestFilter.Store(preFilter2) 579 defer s.client1.filteredConn.requestFilter.Store(func(string) error { return nil }) 580 defer s.client2.filteredConn.requestFilter.Store(func(string) error { return nil }) 581 582 return s.simpleTradeTest(qty, rate, order.MatchConfirmed) 583 } 584 585 // TestOrderStatusReconciliation simulates a few conditions that could cause a 586 // client to record a wrong status for an order, especially where the client 587 // considers an order as active when it no longer is. The expectation is that 588 // the client can infer the correct order status for such orders and update 589 // accordingly. The following scenarios are simulated and tested: 590 // 591 // Order 1: 592 // - Standing order, preimage not revealed, "missed" revoke_order note. 593 // - Expect order status to stay at Epoch status before going AWOL and to 594 // become Revoked after re-connecting the DEX. Locked coins should be 595 // returned. 596 // 597 // Order 2: 598 // - Non-standing order, preimage revealed, "missed" nomatch or match request 599 // (if matched). 600 // - Expect order status to stay at Epoch status before going AWOL and to 601 // become Executed after re-connecting the DEX, even if the order was 602 // matched and the matches got revoked due to client inaction. 603 // 604 // Order 3: 605 // - Standing order, partially matched, booked, revoked due to inaction on a 606 // match. 607 // - Expect order status to be Booked before going AWOL and to become Revoked 608 // after re-connecting the DEX. Locked coins should be returned. 609 func testOrderStatusReconciliation(s *simulationTest) error { 610 for _, client := range s.clients { 611 if err := s.updateBalances(client); err != nil { 612 return err 613 } 614 client.expectBalanceDiffs = nil // not interested in balance checks for this test case 615 } 616 617 waiter, ctx := errgroup.WithContext(context.Background()) 618 619 s.client1.isSeller, s.client2.isSeller = false, true 620 621 // Record client 2's locked balance before placing trades 622 // to determine the amount locked for the placed trades. 623 c2Balance, err := s.client2.core.AssetBalance(s.base.id) // client 2 is seller 624 if err != nil { 625 return fmt.Errorf("client 2 pre-trade balance error %w", err) 626 } 627 preTradeLockedBalance := c2Balance.Locked 628 s.log.Infof("Client 2 %s available balance is %v.", s.base.symbol, s.base.valFmt(c2Balance.Available)) 629 630 rate := 100 * s.rateStep 631 632 s.log.Infof("%s\n", ` 633 Placing an order for client 1, qty=3*lotSize, rate=100*rateStep 634 This order should get matched to either or both of these client 2 635 sell orders: 636 - Order 2: immediate limit order, qty=2*lotSize, rate=100*rateStep, 637 may not get matched if Order 3 below is matched first. 638 - Order 3: standing limit order, qty=4*lotSize, rate=100*rateStep, 639 will always be partially matched (3*lotSize matched or 640 1*lotSize matched, if Order 2 is matched first).`) 641 waiter.Go(func() error { 642 _, err := s.placeOrder(s.client1, 3*s.lotSize, rate, false) 643 if err != nil { 644 return fmt.Errorf("client 1 place order error: %v", err) 645 } 646 return nil 647 }) 648 649 // forgetClient2Order deletes the passed order id from client 2's 650 // dc.trade map, ensuring that all requests and notifications for 651 // the order are not processed. 652 c2dc := s.client2.dc() 653 c2ForgottenOrders := make(map[order.OrderID]*trackedTrade) 654 forgetClient2Order := func(oid order.OrderID) { 655 s.log.Infof("Forcing client 2 to forget order %s.", oid) 656 c2dc.tradeMtx.Lock() 657 defer c2dc.tradeMtx.Unlock() 658 tracker, found := c2dc.trades[oid] 659 if !found { 660 return 661 } 662 delete(c2dc.trades, oid) 663 c2ForgottenOrders[oid] = tracker 664 } 665 666 // Expected order statuses before and after client 2 goes AWOL. 667 c2OrdersBefore := make(map[order.OrderID]order.OrderStatus) 668 c2OrdersAfter := make(map[order.OrderID]order.OrderStatus) 669 var statusMtx sync.Mutex 670 recordBeforeAfterStatuses := func(oid order.OrderID, beforeStatus, afterStatus order.OrderStatus) { 671 statusMtx.Lock() 672 defer statusMtx.Unlock() 673 c2OrdersBefore[oid] = beforeStatus 674 c2OrdersAfter[oid] = afterStatus 675 } 676 677 s.log.Infof("%s\n", ` 678 Placing Client 2 Order 1: 679 - Standing order, preimage not revealed, "missed" revoke_order note. 680 - Expect order status to stay at Epoch status before going AWOL and 681 to become Revoked after re-connecting the DEX. Locked coins should 682 be returned.`) 683 684 waiter.Go(func() error { 685 // standing limit order, qty and rate doesn't matter, preimage 686 // miss prevents this order from getting matched. 687 orderID, err := s.placeOrder(s.client2, 1*s.lotSize, rate, false) 688 if err != nil { 689 return fmt.Errorf("client 2 place order error: %v", err) 690 } 691 oid, err := order.IDFromHex(orderID) 692 if err != nil { 693 return fmt.Errorf("client 2 place order error: %v", err) 694 } 695 // Foil preimage reveal by "forgetting" this order. 696 // Also prevents processing revoke_order notes for this order. 697 forgetClient2Order(oid) 698 recordBeforeAfterStatuses(oid, order.OrderStatusEpoch, order.OrderStatusRevoked) 699 return nil 700 }) 701 702 s.log.Infof("%s\n", ` 703 Placing Client 2 Order 2: 704 - Non-standing order, preimage revealed, "missed" nomatch or match 705 request (if matched). 706 - Expect order status to stay at Epoch status before going AWOL and 707 to become Executed after re-connecting the DEX, even if the order 708 was matched and the matches got revoked due to client inaction. No 709 attempt is made to cause match revocation anyways.`) 710 711 waiter.Go(func() error { 712 notes := s.client2.startNotificationReader(ctx) 713 // immediate limit order, use qty=2*lotSize, rate=300*rateStep to be 714 // potentially matched by client 1's order above. 715 orderID, err := s.placeOrder(s.client2, 2*s.lotSize, rate*3, true) 716 if err != nil { 717 return fmt.Errorf("client 2 place order error: %v", err) 718 } 719 tracker, err := s.client2.findOrder(orderID) 720 if err != nil { 721 return fmt.Errorf("client 2 place order error: %v", err) 722 } 723 oid := tracker.ID() 724 // Wait a max of 2 epochs for preimage to be sent for this order. 725 twoEpochs := 2 * time.Duration(tracker.epochLen()) * time.Millisecond 726 s.client2.log.Infof("Client 2 waiting %v for preimage reveal, order %s", twoEpochs, tracker.token()) 727 preimageRevealed := notes.find(ctx, twoEpochs, func(n Notification) bool { 728 orderNote, isOrderNote := n.(*OrderNote) 729 if isOrderNote && n.Topic() == TopicPreimageSent && orderNote.Order.ID.String() == orderID { 730 forgetClient2Order(oid) 731 return true 732 } 733 return false 734 }) 735 if !preimageRevealed { 736 return fmt.Errorf("preimage not revealed for order %s after %s", tracker.token(), twoEpochs) 737 } 738 recordBeforeAfterStatuses(oid, order.OrderStatusEpoch, order.OrderStatusExecuted) 739 return nil 740 }) 741 742 s.log.Infof("%s\n", ` 743 Client 2 placing Order 3: 744 - Standing order, partially matched, booked, revoked due to inaction on 745 a match. 746 - Expect order status to be Booked before going AWOL and to become 747 Revoked after re-connecting the DEX. Locked coins should be returned.`) 748 749 waiter.Go(func() error { 750 notes := s.client2.startNotificationReader(ctx) 751 // standing limit order, use qty=4*lotSize, rate=100*rateStep to be 752 // partially matched by client 1's order above. 753 orderID, err := s.placeOrder(s.client2, 4*s.lotSize, rate, false) 754 if err != nil { 755 return fmt.Errorf("client 2 place order error: %v", err) 756 } 757 tracker, err := s.client2.findOrder(orderID) 758 if err != nil { 759 return fmt.Errorf("client 2 place order error: %v", err) 760 } 761 // Wait a max of 2 epochs for preimage to be sent for this order. 762 twoEpochs := 2 * time.Duration(tracker.epochLen()) * time.Millisecond 763 s.client2.log.Infof("Client 2 waiting %v for preimage reveal, order %s", twoEpochs, tracker.token()) 764 preimageRevealed := notes.find(ctx, twoEpochs, func(n Notification) bool { 765 orderNote, isOrderNote := n.(*OrderNote) 766 return isOrderNote && n.Topic() == TopicPreimageSent && orderNote.Order.ID.String() == orderID 767 }) 768 if !preimageRevealed { 769 return fmt.Errorf("preimage not revealed for order %s after %s", tracker.token(), twoEpochs) 770 } 771 // Preimage sent, matches will be made soon. Lock wallets to prevent 772 // client from sending swap when this order is matched. Particularly 773 // important if we're matched as maker. 774 s.client2.disableWallets() 775 oid := tracker.ID() 776 // Wait 1 minute for order to receive match request. 777 maxMatchDuration := time.Minute 778 s.client2.log.Infof("Client 2 waiting %v for order %s to be partially matched", maxMatchDuration, tracker.token()) 779 matched := notes.find(ctx, maxMatchDuration, func(n Notification) bool { 780 orderNote, isOrderNote := n.(*OrderNote) 781 isMatchedTopic := n.Topic() == TopicBuyMatchesMade || n.Topic() == TopicSellMatchesMade 782 return isOrderNote && isMatchedTopic && orderNote.Order.ID.String() == orderID 783 }) 784 if !matched { 785 return fmt.Errorf("order %s not matched after %s", tracker.token(), maxMatchDuration) 786 } 787 if tracker.Trade().Remaining() == 0 { 788 return fmt.Errorf("order %s fully matched instead of partially", tracker.token()) 789 } 790 if ctx.Err() != nil { 791 return nil // return here if some other goroutine errored 792 } 793 tracker.mtx.RLock() 794 // Partially matched, let's ditch the first match to trigger order 795 // revocation due to match inaction. 796 var isTaker bool 797 for _, match := range tracker.matches { 798 match.swapErr = fmt.Errorf("ditch match") 799 isTaker = match.Side == order.Taker 800 break // only interested in first match 801 } 802 tracker.mtx.RUnlock() 803 if isTaker { 804 // Monitor the match till MakerSwapCast, mine a couple blocks for 805 // maker's swap and ditch the match just when we're required to send 806 // counter-swap. 807 // Keep the order active to enable receiving audit request when Maker 808 // sends swap. 809 err = s.monitorTrackedTrade(s.client2, tracker, order.MakerSwapCast) 810 if err != nil { 811 return err 812 } 813 } 814 // Match will get revoked after lastEvent+bTimeout. 815 forgetClient2Order(oid) // ensure revoke_match request is "missed" 816 recordBeforeAfterStatuses(oid, order.OrderStatusBooked, order.OrderStatusRevoked) 817 return nil 818 }) 819 820 // Wait for orders to be placed and forgotten or partly negotiated. 821 if err := waiter.Wait(); err != nil { 822 return err 823 } 824 825 s.log.Info("Orders placed and monitored to desired states.") 826 827 // Confirm that the order statuses are what we expect before triggering 828 // a authDEX->connect status recovery. 829 c2dc.tradeMtx.RLock() 830 for oid, expectStatus := range c2OrdersBefore { 831 tracker, found := c2ForgottenOrders[oid] 832 if !found { 833 tracker, found = c2dc.trades[oid] 834 } 835 if !found { 836 return fmt.Errorf("missing client 2 order %v", oid) 837 } 838 if tracker.metaData.Status != expectStatus { 839 return fmt.Errorf("expected pre-recovery status %v for client 2 order %v, got %v", 840 expectStatus, oid, tracker.metaData.Status) 841 } 842 s.client2.log.Infof("Client 2 order %v in expected pre-recovery status %v.", oid, expectStatus) 843 } 844 c2dc.tradeMtx.RUnlock() 845 846 // Check trade-locked amount before disconnecting. 847 c2Balance, err = s.client2.core.AssetBalance(s.base.id) // client 2 is seller 848 if err != nil { 849 return fmt.Errorf("client 2 pre-disconnect balance error %w", err) 850 } 851 s.client2.log.Infof("Client 2 %s available balance before disconnecting is %v.", s.base.symbol, s.base.valFmt(c2Balance.Available)) 852 totalLockedByTrades := c2Balance.Locked - preTradeLockedBalance 853 preDisconnectLockedBalance := c2Balance.Locked // should reduce after funds are returned 854 preDisconnectAvailableBal := c2Balance.Available // should increase after funds are returned 855 856 // Disconnect the DEX and allow some time for DEX to update order statuses. 857 s.client2.log.Info("Disconnecting client 2 from the DEX server.") 858 c2dc.connMaster.Disconnect() 859 // Disconnection is asynchronous, wait for confirmation of DEX disconnection. 860 disconnectTimeout := 10 * sleepFactor * time.Second 861 disconnected := s.client2.notes.find(context.Background(), disconnectTimeout, func(n Notification) bool { 862 connNote, ok := n.(*ConnEventNote) 863 return ok && connNote.Host == dexHost && connNote.ConnectionStatus != comms.Connected 864 }) 865 if !disconnected { 866 return fmt.Errorf("client 2 dex not disconnected after %v", disconnectTimeout) 867 } 868 869 s.client2.enableWallets() 870 // Disconnect the wallets, they'll be reconnected when Login is called below. 871 // Login->connectWallets will error for btc spv wallets if the wallet is not 872 // first disconnected. 873 s.client2.disconnectWallets() 874 875 // Allow some time for orders to be revoked due to inaction, and 876 // for requests pending on the server to expire (usually bTimeout). 877 bTimeout := time.Millisecond * time.Duration(c2dc.cfg.BroadcastTimeout) 878 disconnectPeriod := 2 * bTimeout 879 s.client2.log.Infof("Waiting %v before reconnecting client 2 to DEX.", disconnectPeriod) 880 time.Sleep(disconnectPeriod) 881 882 s.client2.log.Info("Reconnecting client 2 to DEX to trigger order status reconciliation.") 883 884 // Use core.initialize to restore client 2 orders from db, and login 885 // to trigger dex authentication. 886 // TODO: cannot do this anymore with built-in wallets 887 err = s.client2.core.initialize() 888 if err != nil { 889 return fmt.Errorf("client 2 login error: %w", err) 890 } 891 err = s.client2.core.Login(s.client2.appPass) 892 if err != nil { 893 return fmt.Errorf("client 2 login error: %w", err) 894 } 895 896 c2dc = s.client2.dc() 897 c2dc.tradeMtx.RLock() 898 for oid, expectStatus := range c2OrdersAfter { 899 tracker, found := c2dc.trades[oid] 900 if !found { 901 return fmt.Errorf("client 2 order %v not found after re-initializing core", oid) 902 } 903 if tracker.metaData.Status != expectStatus { 904 return fmt.Errorf("status not updated for client 2 order %v, expected %v, got %v", 905 oid, expectStatus, tracker.metaData.Status) 906 } 907 s.client2.log.Infof("Client 2 order %v in expected post-recovery status %v.", oid, expectStatus) 908 } 909 c2dc.tradeMtx.RUnlock() 910 911 // Wait a bit for tick cycle to trigger inactive trade retirement and funds unlocking. 912 halfBTimeout := time.Millisecond * time.Duration(c2dc.cfg.BroadcastTimeout/2) 913 time.Sleep(halfBTimeout) 914 915 c2Balance, err = s.client2.core.AssetBalance(s.base.id) // client 2 is seller 916 if err != nil { 917 return fmt.Errorf("client 2 post-reconnect balance error %w", err) 918 } 919 s.client2.log.Infof("Client 2 %s available balance after reconnecting to DEX is %v.", s.base.symbol, s.base.valFmt(c2Balance.Available)) 920 if c2Balance.Available != preDisconnectAvailableBal+totalLockedByTrades { 921 return fmt.Errorf("client 2 locked funds not returned: locked before trading %v, locked after trading %v, "+ 922 "locked after reconnect %v", preTradeLockedBalance, preDisconnectLockedBalance, c2Balance.Locked) 923 } 924 if c2Balance.Locked != preDisconnectLockedBalance-totalLockedByTrades { 925 return fmt.Errorf("client 2 locked funds not returned: locked before trading %v, locked after trading %v, "+ 926 "locked after reconnect %v", preTradeLockedBalance, preDisconnectLockedBalance, c2Balance.Locked) 927 } 928 s.client2.enableWallets() 929 930 for _, c := range s.clients { 931 if err := c.mineMedian(context.TODO(), s.quote.id); err != nil { 932 return err 933 } 934 if err := c.mineMedian(context.TODO(), s.base.id); err != nil { 935 return err 936 } 937 } 938 939 s.waitALittleBit() 940 941 return nil 942 } 943 944 // TestResendPendingRequests runs a simple trade test, simulates init/redeem 945 // request errors during trade negotiation and ensures that failed requests 946 // are retried and the trades complete successfully. 947 func testResendPendingRequests(s *simulationTest) error { 948 var qty, rate uint64 = 1 * s.lotSize, 250 * s.rateStep 949 s.client1.isSeller, s.client2.isSeller = true, false 950 951 anErr := errors.New("intentional error from test") 952 var bits uint8 953 // Fail every first try of init and redeem. Second try will be 954 // passed on to the real comms. 955 preFilter1 := func(route string) error { 956 if route == msgjson.InitRoute { 957 if bits&0b1 == 0 { 958 bits |= 0b1 959 return anErr 960 } 961 } 962 if route == msgjson.RedeemRoute { 963 if bits&0b10 == 0 { 964 bits |= 0b10 965 return anErr 966 } 967 } 968 return nil 969 } 970 971 preFilter2 := func(route string) error { 972 if route == msgjson.InitRoute { 973 if bits&0b100 == 0 { 974 bits |= 0b100 975 return anErr 976 } 977 } 978 if route == msgjson.RedeemRoute { 979 if bits&0b1000 == 0 { 980 bits |= 0b1000 981 return anErr 982 } 983 } 984 return nil 985 } 986 987 s.client1.filteredConn.requestFilter.Store(preFilter1) 988 s.client2.filteredConn.requestFilter.Store(preFilter2) 989 defer s.client1.filteredConn.requestFilter.Store(func(string) error { return nil }) 990 defer s.client2.filteredConn.requestFilter.Store(func(string) error { return nil }) 991 992 return s.simpleTradeTest(qty, rate, order.MatchConfirmed) 993 } 994 995 // simpleTradeTest uses client1 and client2 to place similar orders but on 996 // either sides that get matched and monitors the resulting trades up till the 997 // specified final status. 998 // Also checks that the changes to the clients wallets balances are within 999 // expected range. 1000 func (s *simulationTest) simpleTradeTest(qty, rate uint64, finalStatus order.MatchStatus) error { 1001 if s.client1.isSeller && s.client2.isSeller { 1002 return fmt.Errorf("Both client 1 and 2 cannot be sellers") 1003 } 1004 1005 c1OrderID, c2OrderID, err := s.placeTestOrders(qty, rate) 1006 if err != nil { 1007 return err 1008 } 1009 1010 if finalStatus == order.NewlyMatched { 1011 // Kill the wallets to prevent Maker from sending swap as soon as the 1012 // orders are matched. 1013 for _, client := range s.clients { 1014 client.disableWallets() 1015 defer client.enableWallets() 1016 } 1017 1018 s.log.Info("Disabled wallets to prevent order status from moving past order.NewlyMatched") 1019 } 1020 1021 if finalStatus == order.MakerSwapCast { 1022 // Kill the wallets to prevent Taker from sending swap as soon 1023 // as the orders are matched. 1024 name := s.client1.name 1025 if s.client1IsMaker { 1026 name = s.client2.name 1027 s.client2.disableWallets() 1028 defer s.client2.enableWallets() 1029 } else { 1030 s.client1.disableWallets() 1031 defer s.client1.enableWallets() 1032 } 1033 s.log.Infof("Disabled client %s wallets to prevent order status from moving past order.MakerSwapCast.", name) 1034 } 1035 1036 // NOTE: Client 1 is always maker the first time this test is run and 1037 // taker the second when running the same test twice. 1038 monitorTrades, ctx := errgroup.WithContext(context.Background()) 1039 monitorTrades.Go(func() error { 1040 return s.monitorOrderMatchingAndTradeNeg(ctx, s.client1, c1OrderID, finalStatus) 1041 }) 1042 monitorTrades.Go(func() error { 1043 return s.monitorOrderMatchingAndTradeNeg(ctx, s.client2, c2OrderID, finalStatus) 1044 }) 1045 if err = monitorTrades.Wait(); err != nil { 1046 return err 1047 } 1048 1049 // Allow some time for balance changes to be properly reported. 1050 // There is usually a split-second window where a locked output 1051 // has been spent but the spending tx is still in mempool. This 1052 // will cause the txout to be included in the wallets locked 1053 // balance, causing a higher than actual balance report. 1054 s.waitALittleBit() 1055 1056 for _, client := range s.clients { 1057 if err = s.assertBalanceChanges(client, false); err != nil { 1058 return err 1059 } 1060 } 1061 1062 // Check if any refunds are necessary and wait to ensure the refunds 1063 // are completed. 1064 if finalStatus < order.MatchConfirmed { 1065 refundsWaiter, ctx := errgroup.WithContext(context.Background()) 1066 refundsWaiter.Go(func() error { 1067 return s.checkAndWaitForRefunds(ctx, s.client1, c1OrderID) 1068 }) 1069 refundsWaiter.Go(func() error { 1070 return s.checkAndWaitForRefunds(ctx, s.client2, c2OrderID) 1071 }) 1072 if err = refundsWaiter.Wait(); err != nil { 1073 return err 1074 } 1075 } 1076 1077 s.log.Infof("Trades ended at %s.", finalStatus) 1078 return nil 1079 } 1080 1081 func (s *simulationTest) placeTestOrders(qty, rate uint64) (string, string, error) { 1082 for _, client := range s.clients { 1083 if err := s.updateBalances(client); err != nil { 1084 return "", "", fmt.Errorf("client %s balance update error: %v", client.name, err) 1085 } 1086 // Reset the expected balance changes for this client, to be updated 1087 // later in the monitorTrackedTrade function as swaps and redeems are 1088 // executed. 1089 client.expectBalanceDiffs = map[uint32]int64{s.base.id: 0, s.quote.id: 0} 1090 } 1091 1092 var ( 1093 c1OrderID, c2OrderID string 1094 err error 1095 ) 1096 placeOrderC1 := func() error { 1097 c1OrderID, err = s.placeOrder(s.client1, qty, rate, false) 1098 if err != nil { 1099 return fmt.Errorf("client1 place %s order error: %v", sellString(s.client1.isSeller), err) 1100 } 1101 return nil 1102 } 1103 1104 placeOrderC2 := func() error { 1105 c2OrderID, err = s.placeOrder(s.client2, qty, rate, false) 1106 if err != nil { 1107 return fmt.Errorf("client2 place %s order error: %v", sellString(s.client2.isSeller), err) 1108 } 1109 return nil 1110 } 1111 1112 // The client to have their order booked first becomes the maker of a 1113 // trade. Here the second time a trade is run for the same 1114 // simulationTest make and taker will be swapped allowing testing for 1115 // both sides at different stages of failure to act and their resolution. 1116 var ( 1117 client *simulationClient 1118 orderID string 1119 ) 1120 if s.client1IsMaker { 1121 if err = placeOrderC1(); err != nil { 1122 return "", "", err 1123 } 1124 orderID = c1OrderID 1125 client = s.client1 1126 } else { 1127 if err = placeOrderC2(); err != nil { 1128 return "", "", err 1129 } 1130 orderID = c2OrderID 1131 client = s.client2 1132 } 1133 1134 tracker, err := client.findOrder(orderID) 1135 if err != nil { 1136 return "", "", err 1137 } 1138 // Wait the epoch duration for this order to get booked. 1139 epochDur := time.Duration(tracker.epochLen()) * time.Millisecond 1140 time.Sleep(epochDur) 1141 1142 if s.client1IsMaker { 1143 if err = placeOrderC2(); err != nil { 1144 return "", "", err 1145 } 1146 } else { 1147 if err = placeOrderC1(); err != nil { 1148 return "", "", err 1149 } 1150 } 1151 1152 return c1OrderID, c2OrderID, nil 1153 } 1154 1155 func (s *simulationTest) monitorOrderMatchingAndTradeNeg(ctx context.Context, client *simulationClient, orderID string, finalStatus order.MatchStatus) error { 1156 errs := newErrorSet("[client %s] ", client.name) 1157 1158 client.log.Infof("Client %s starting to monitor order %s.", client.name, orderID) 1159 1160 tracker, err := client.findOrder(orderID) 1161 if err != nil { 1162 return errs.addErr(err) 1163 } 1164 1165 // Wait up to 2 times the epoch duration for this order to get matched. 1166 maxMatchDuration := 2 * time.Duration(tracker.epochLen()) * time.Millisecond 1167 client.log.Infof("Client %s Waiting up to %v for matches on order %s.", client.name, maxMatchDuration, tracker.token()) 1168 matched := client.notes.find(ctx, maxMatchDuration, func(n Notification) bool { 1169 orderNote, isOrderNote := n.(*OrderNote) 1170 isMatchedTopic := n.Topic() == TopicBuyMatchesMade || n.Topic() == TopicSellMatchesMade 1171 return isOrderNote && isMatchedTopic && orderNote.Order.ID.String() == orderID 1172 }) 1173 if ctx.Err() != nil { // context canceled 1174 return nil 1175 } 1176 if !matched { 1177 return errs.add("order %s not matched after %s", tracker.token(), maxMatchDuration) 1178 } 1179 1180 recordBalanceChanges := func(isSwap bool, qty, rate uint64) { 1181 amt := int64(qty) 1182 a := s.base 1183 if client.isSeller != isSwap { 1184 // use quote amt for seller redeem and buyer swap 1185 amt = int64(calc.BaseToQuote(rate, qty)) 1186 a = s.quote 1187 } 1188 if isSwap { 1189 amt *= -1 1190 } 1191 client.log.Infof("Updated %s balance diff with %s.", a.symbol, a.valFmt(amt)) 1192 client.expectBalanceDiffs[a.id] += amt 1193 } 1194 1195 tracker.mtx.RLock() 1196 client.log.Infof("Client %s %d match(es) received for order %s.", client.name, len(tracker.matches), tracker.token()) 1197 for _, match := range tracker.matches { 1198 client.log.Infof("Client %s is %s on match %s, amount %s %s.", client.name, match.Side.String(), 1199 token(match.MatchID.Bytes()), s.base.valFmt(match.Quantity), s.base.symbol) 1200 if match.Side == order.Taker { 1201 if finalStatus >= order.TakerSwapCast { 1202 recordBalanceChanges(true, match.Quantity, match.Rate) 1203 } 1204 if finalStatus >= order.MatchComplete { 1205 recordBalanceChanges(false, match.Quantity, match.Rate) 1206 } 1207 } else { // maker 1208 if finalStatus >= order.MakerSwapCast { 1209 recordBalanceChanges(true, match.Quantity, match.Rate) 1210 } 1211 if finalStatus >= order.MakerRedeemed { 1212 recordBalanceChanges(false, match.Quantity, match.Rate) 1213 } 1214 } 1215 } 1216 tracker.mtx.RUnlock() 1217 1218 return s.monitorTrackedTrade(client, tracker, finalStatus) 1219 } 1220 1221 func (s *simulationTest) monitorTrackedTrade(client *simulationClient, tracker *trackedTrade, finalStatus order.MatchStatus) error { 1222 // run a repeated check for match status changes to mine blocks as necessary. 1223 maxTradeDuration := 4 * time.Minute 1224 var waitedForOtherSideMakerInit, waitedForOtherSideTakerInit bool 1225 1226 tryUntil(s.ctx, maxTradeDuration, func() bool { 1227 1228 var completedTrades int 1229 mineAssets := make(map[uint32]uint32) 1230 var waitForOtherSideMakerInit, waitForOtherSideTakerInit bool 1231 var thisSide uint32 1232 // Don't spam. 1233 time.Sleep(time.Second * 2 * sleepFactor) 1234 tracker.mtx.Lock() 1235 for _, match := range tracker.matches { 1236 side, status := match.Side, match.Status 1237 client.psMTX.Lock() 1238 lastStatus := client.processedStatus[match.MatchID] 1239 client.psMTX.Unlock() 1240 if status >= finalStatus && lastStatus >= finalStatus { 1241 match.swapErr = fmt.Errorf("take no further action") 1242 completedTrades++ 1243 continue 1244 } 1245 if status != lastStatus { 1246 client.log.Infof("Match %s: NOW =====> %s.", match.MatchID, status) 1247 client.psMTX.Lock() 1248 client.processedStatus[match.MatchID] = status 1249 client.psMTX.Unlock() 1250 } 1251 1252 logIt := func(swapOrRedeem string, assetID, nBlocks uint32) { 1253 var actor order.MatchSide 1254 if swapOrRedeem == "redeem" { 1255 actor = side // this client 1256 } else if side == order.Maker { 1257 actor = order.Taker // counter-party 1258 } else { 1259 actor = order.Maker 1260 } 1261 client.log.Infof("Mining %d %s blocks for %s's %s, match %s.", nBlocks, unbip(assetID), 1262 actor, swapOrRedeem, token(match.MatchID.Bytes())) 1263 } 1264 1265 if (side == order.Maker && status <= order.MakerSwapCast && finalStatus >= order.MakerSwapCast) || 1266 (side == order.Taker && status <= order.TakerSwapCast && finalStatus >= order.TakerSwapCast) { 1267 // If the other side is geth, we need to give it 1268 // time to confirm the swap in order to populate 1269 // swap fees. 1270 if !waitedForOtherSideMakerInit && accountBIPs[tracker.wallets.toWallet.AssetID] { 1271 thisSide = tracker.wallets.fromWallet.AssetID 1272 waitForOtherSideMakerInit = true 1273 } 1274 if !waitedForOtherSideTakerInit && status > order.MakerSwapCast && 1275 accountBIPs[tracker.wallets.toWallet.AssetID] { 1276 thisSide = tracker.wallets.fromWallet.AssetID 1277 waitForOtherSideTakerInit = true 1278 } 1279 // Progress from asset. 1280 nBlocks := tracker.metaData.FromSwapConf 1281 if accountBIPs[tracker.wallets.fromWallet.AssetID] { 1282 nBlocks = 8 1283 } 1284 assetID := tracker.wallets.fromWallet.AssetID 1285 mineAssets[assetID] = nBlocks 1286 logIt("swap", assetID, nBlocks) 1287 } 1288 if (side == order.Maker && status > order.TakerSwapCast && finalStatus > order.TakerSwapCast) || 1289 (side == order.Taker && status > order.MakerRedeemed && finalStatus > order.MakerRedeemed) { 1290 if !waitedForOtherSideMakerInit && accountBIPs[tracker.wallets.fromWallet.AssetID] { 1291 thisSide = tracker.wallets.toWallet.AssetID 1292 waitForOtherSideMakerInit = true 1293 } 1294 if !waitedForOtherSideTakerInit && status > order.MakerSwapCast && 1295 accountBIPs[tracker.wallets.fromWallet.AssetID] { 1296 thisSide = tracker.wallets.toWallet.AssetID 1297 waitForOtherSideTakerInit = true 1298 } 1299 // Progress to asset. 1300 nBlocks := tracker.metaData.ToSwapConf 1301 if accountBIPs[tracker.wallets.toWallet.AssetID] { 1302 nBlocks = 8 1303 } 1304 assetID := tracker.wallets.toWallet.AssetID 1305 mineAssets[assetID] = nBlocks 1306 logIt("redeem", assetID, nBlocks) 1307 } 1308 } 1309 1310 finish := completedTrades == len(tracker.matches) 1311 // Do not hold the lock while mining as this hinders trades. 1312 tracker.mtx.Unlock() 1313 1314 // Geth light client takes time for the best block to be updated 1315 // after mining. Sleeping to get confs registered for the init 1316 // before the match can be confirmed if the utxo side completes 1317 // too fast. 1318 if waitForOtherSideMakerInit || waitForOtherSideTakerInit { 1319 client.log.Infof("Not mining asset %d so geth can find init confirms.", thisSide) 1320 delete(mineAssets, thisSide) 1321 } 1322 mine := func(assetID, nBlocks uint32) { 1323 err := newHarnessCtrl(assetID).mineBlocks(s.ctx, nBlocks) 1324 if err != nil { 1325 client.log.Infof("%s mine error %v.", unbip(assetID), err) // return err??? 1326 } 1327 } 1328 for assetID, swapConf := range mineAssets { 1329 mine(assetID, swapConf) 1330 } 1331 if waitForOtherSideMakerInit || waitForOtherSideTakerInit { 1332 client.log.Info("Sleeping 8 seconds for geth to update other side's init best block.") 1333 time.Sleep(time.Second * 8) 1334 if waitForOtherSideMakerInit { 1335 waitedForOtherSideMakerInit = true 1336 } 1337 if waitForOtherSideTakerInit { 1338 waitedForOtherSideTakerInit = true 1339 } 1340 } 1341 return finish 1342 }) 1343 if s.ctx.Err() != nil { // context canceled 1344 return nil 1345 } 1346 1347 var incompleteTrades int 1348 tracker.mtx.RLock() 1349 for _, match := range tracker.matches { 1350 if match.Status < finalStatus { 1351 incompleteTrades++ 1352 client.log.Infof("Incomplete trade: order %s, match %s, status %s, side %s.", tracker.token(), 1353 token(match.MatchID[:]), match.Status, match.Side) 1354 } else { 1355 client.log.Infof("Trade for order %s, match %s monitored successfully till %s, side %s.", tracker.token(), 1356 token(match.MatchID[:]), match.Status, match.Side) 1357 } 1358 } 1359 tracker.mtx.RUnlock() 1360 if incompleteTrades > 0 { 1361 return fmt.Errorf("client %s reported %d incomplete trades for order %s after %s", 1362 client.name, incompleteTrades, tracker.token(), maxTradeDuration) 1363 } 1364 1365 return nil 1366 } 1367 1368 // swaps cannot be refunded until the MedianTimePast is greater than 1369 // the swap locktime. The MedianTimePast is calculated by taking the 1370 // timestamps of the last 11 blocks and finding the median. Mining 6 1371 // blocks on the chain a second from now will ensure that the 1372 // MedianTimePast will be greater than the furthest swap locktime, 1373 // thereby lifting the time lock on all these swaps. 1374 func (client *simulationClient) mineMedian(ctx context.Context, assetID uint32) error { 1375 time.Sleep(sleepFactor * time.Second) 1376 if err := newHarnessCtrl(assetID).mineBlocks(ctx, 6); err != nil { 1377 return fmt.Errorf("client %s: error mining 6 %s blocks for swap refunds: %v", 1378 client.name, unbip(assetID), err) 1379 } 1380 client.log.Infof("Mined 6 blocks for assetID %d to expire swap locktimes.", assetID) 1381 return nil 1382 } 1383 1384 func (s *simulationTest) checkAndWaitForRefunds(ctx context.Context, client *simulationClient, orderID string) error { 1385 // check if client has pending refunds 1386 client.log.Infof("Checking if refunds are necessary for client %s.", client.name) 1387 refundAmts := map[uint32]int64{s.base.id: 0, s.quote.id: 0} 1388 var furthestLockTime time.Time 1389 1390 hasRefundableSwap := func(match *matchTracker) bool { 1391 sentSwap := match.MetaData.Proof.ContractData != nil 1392 noRedeems := match.Status < order.MakerRedeemed 1393 return sentSwap && noRedeems 1394 } 1395 1396 tracker, err := client.findOrder(orderID) 1397 if err != nil { 1398 return err 1399 } 1400 1401 if tracker == nil { 1402 return nil 1403 } 1404 tracker.mtx.RLock() 1405 for _, match := range tracker.matches { 1406 if !hasRefundableSwap(match) { 1407 continue 1408 } 1409 1410 swapAmt := match.Quantity 1411 if !client.isSeller { 1412 swapAmt = calc.BaseToQuote(match.Rate, match.Quantity) 1413 } 1414 refundAmts[tracker.wallets.fromWallet.AssetID] += int64(swapAmt) 1415 1416 matchTime := match.matchTime() 1417 swapLockTime := matchTime.Add(tracker.lockTimeTaker) 1418 if match.Side == order.Maker { 1419 swapLockTime = matchTime.Add(tracker.lockTimeMaker) 1420 } 1421 if swapLockTime.After(furthestLockTime) { 1422 furthestLockTime = swapLockTime 1423 } 1424 } 1425 tracker.mtx.RUnlock() 1426 1427 if ctx.Err() != nil { // context canceled 1428 return nil 1429 } 1430 if furthestLockTime.IsZero() { 1431 client.log.Infof("No refunds necessary for client %s.", client.name) 1432 return nil 1433 } 1434 1435 client.log.Infof("Found refundable swaps worth %s %s and %s %s.", s.base.valFmt(refundAmts[s.base.id]), 1436 s.base.symbol, s.quote.valFmt(refundAmts[s.quote.id]), s.quote.symbol) 1437 1438 // wait for refunds to be executed 1439 now := time.Now() 1440 if furthestLockTime.After(now) { 1441 wait := furthestLockTime.Sub(now) 1442 client.log.Infof("Waiting until the longest timelock expires at %v before checking "+ 1443 "client %s wallet balances for expected refunds.", wait, client.name) 1444 select { 1445 case <-ctx.Done(): 1446 return nil 1447 case <-time.After(wait): 1448 } 1449 } 1450 1451 if refundAmts[s.quote.id] > 0 { 1452 if err := client.mineMedian(ctx, s.quote.id); err != nil { 1453 return err 1454 } 1455 } 1456 if refundAmts[s.base.id] > 0 { 1457 if err := client.mineMedian(ctx, s.base.id); err != nil { 1458 return err 1459 } 1460 } 1461 1462 // allow up to 30 seconds for core to get around to refunding the swaps 1463 var notRefundedSwaps int 1464 refundWaitTimeout := 60 * time.Second 1465 refundedSwaps := tryUntil(ctx, refundWaitTimeout, func() bool { 1466 tracker.mtx.RLock() 1467 defer tracker.mtx.RUnlock() 1468 notRefundedSwaps = 0 1469 for _, match := range tracker.matches { 1470 if hasRefundableSwap(match) && match.MetaData.Proof.RefundCoin == nil { 1471 notRefundedSwaps++ 1472 } 1473 } 1474 return notRefundedSwaps == 0 1475 }) 1476 if ctx.Err() != nil { // context canceled 1477 return nil 1478 } 1479 if !refundedSwaps { 1480 return fmt.Errorf("client %s reported %d unrefunded swaps after %s", 1481 client.name, notRefundedSwaps, refundWaitTimeout) 1482 } 1483 1484 // swaps refunded, mine some blocks to get the refund txs confirmed and 1485 // confirm that balance changes are as expected. 1486 for assetID, expectedBalanceDiff := range refundAmts { 1487 if expectedBalanceDiff > 0 { 1488 if err := newHarnessCtrl(assetID).mineBlocks(ctx, 1); err != nil { 1489 return fmt.Errorf("%s mine error %v", unbip(assetID), err) 1490 } 1491 } 1492 } 1493 s.waitALittleBit() 1494 1495 client.expectBalanceDiffs = refundAmts 1496 err = s.assertBalanceChanges(client, true) 1497 if err == nil { 1498 client.log.Infof("Successfully refunded swaps worth %s %s and %s %s.", s.base.valFmt(refundAmts[s.base.id]), 1499 s.base.symbol, s.quote.valFmt(refundAmts[s.quote.id]), s.quote.symbol) 1500 } 1501 return err 1502 } 1503 1504 func tryUntil(ctx context.Context, tryDuration time.Duration, tryFn func() bool) bool { 1505 expire := time.NewTimer(tryDuration) 1506 tick := time.NewTicker(250 * time.Millisecond) 1507 defer func() { 1508 expire.Stop() 1509 tick.Stop() 1510 }() 1511 for { 1512 select { 1513 case <-ctx.Done(): 1514 return false 1515 case <-expire.C: 1516 return false 1517 case <-tick.C: 1518 if tryFn() { 1519 return true 1520 } 1521 } 1522 } 1523 } 1524 1525 /************************************ 1526 HELPER TYPES, FUNCTIONS AND METHODS 1527 ************************************/ 1528 1529 type harnessCtrl struct { 1530 dir, fundCmd, fundStr string 1531 } 1532 1533 func (hc *harnessCtrl) run(ctx context.Context, cmd string, args ...string) error { 1534 command := exec.CommandContext(ctx, cmd, args...) 1535 command.Dir = hc.dir 1536 r, err := command.CombinedOutput() 1537 if err != nil { 1538 return fmt.Errorf("exec error: running %q from directory %q, err = %q, output = %q", 1539 command, command.Dir, err, string(r)) 1540 } 1541 return nil 1542 } 1543 1544 func (hc *harnessCtrl) mineBlocks(ctx context.Context, n uint32) error { 1545 return hc.run(ctx, "./mine-alpha", fmt.Sprintf("%d", n)) 1546 } 1547 1548 func (hc *harnessCtrl) fund(ctx context.Context, address string, amts []int) error { 1549 for _, amt := range amts { 1550 fs := fmt.Sprintf(hc.fundStr, address, amt) 1551 strs := strings.Split(fs, "_") 1552 err := hc.run(ctx, hc.fundCmd, strs...) 1553 if err != nil { 1554 return err 1555 } 1556 } 1557 return nil 1558 } 1559 1560 func newHarnessCtrl(assetID uint32) *harnessCtrl { 1561 symbolParts := strings.Split(dex.BipIDSymbol(assetID), ".") 1562 baseChainSymbol := symbolParts[0] 1563 if len(symbolParts) == 2 { 1564 baseChainSymbol = symbolParts[1] 1565 } 1566 switch assetID { 1567 case dcr.BipID, btc.BipID, ltc.BipID, bch.BipID, doge.BipID, firo.BipID, zec.BipID, zcl.BipID, dgb.BipID, dash.BipID: 1568 return &harnessCtrl{ 1569 dir: filepath.Join(dextestDir, baseChainSymbol, "harness-ctl"), 1570 fundCmd: "./alpha", 1571 fundStr: "sendtoaddress_%s_%d", 1572 } 1573 case eth.BipID, polygon.BipID: 1574 // Sending with values of .1 eth. 1575 return &harnessCtrl{ 1576 dir: filepath.Join(dextestDir, baseChainSymbol, "harness-ctl"), 1577 fundCmd: "./sendtoaddress", 1578 fundStr: "%s_%d", 1579 } 1580 case ethUsdcID, polygonUsdcID: 1581 return &harnessCtrl{ 1582 dir: filepath.Join(dextestDir, baseChainSymbol, "harness-ctl"), 1583 fundCmd: "./sendUSDC", 1584 fundStr: "%s_%d", 1585 } 1586 } 1587 panic(fmt.Sprintf("unknown asset %d for harness control", assetID)) 1588 } 1589 1590 type tWallet struct { 1591 pass []byte 1592 config map[string]string 1593 walletType string // type is a keyword 1594 fund bool 1595 hc *harnessCtrl 1596 parent *WalletForm 1597 } 1598 1599 var cloneTypes = map[uint32]string{ 1600 0: "bitcoindRPC", 1601 2: "litecoindRPC", 1602 20: "digibytedRPC", 1603 145: "bitcoindRPC", // yes, same as btc 1604 3: "dogecoindRPC", 1605 136: "firodRPC", 1606 133: "zcashdRPC", 1607 147: "zclassicdRPC", 1608 5: "dashdRPC", 1609 } 1610 1611 // accountBIPs is a map of account based assets. Used in fee estimation. 1612 var accountBIPs = map[uint32]bool{ 1613 eth.BipID: true, 1614 ethUsdcID: true, 1615 polygon.BipID: true, 1616 polygonUsdcID: true, 1617 } 1618 1619 func dcrWallet(wt SimWalletType, node string) (*tWallet, error) { 1620 switch wt { 1621 case WTSPVNative: 1622 return &tWallet{ 1623 walletType: "SPV", 1624 fund: true, 1625 }, nil 1626 case WTCoreClone: 1627 default: 1628 return nil, fmt.Errorf("invalid wallet type: %v", wt) 1629 } 1630 1631 cfg, err := config.Parse(filepath.Join(dextestDir, "dcr", node, fmt.Sprintf("%s.conf", node))) 1632 if err != nil { 1633 return nil, err 1634 } 1635 cfg["account"] = "default" 1636 return &tWallet{ 1637 walletType: "dcrwalletRPC", 1638 pass: []byte("abc"), 1639 config: cfg, 1640 }, nil 1641 } 1642 1643 func btcWallet(wt SimWalletType, node string) (*tWallet, error) { 1644 return btcCloneWallet(btc.BipID, node, wt) 1645 } 1646 1647 func ltcWallet(wt SimWalletType, node string) (*tWallet, error) { 1648 return btcCloneWallet(ltc.BipID, node, wt) 1649 } 1650 1651 func bchWallet(wt SimWalletType, node string) (*tWallet, error) { 1652 return btcCloneWallet(bch.BipID, node, wt) 1653 } 1654 1655 func firoWallet(wt SimWalletType, node string) (*tWallet, error) { 1656 return btcCloneWallet(firo.BipID, node, wt) 1657 } 1658 1659 func ethWallet() (*tWallet, error) { 1660 return &tWallet{ 1661 fund: true, 1662 walletType: "rpc", 1663 config: map[string]string{"providers": ethAlphaIPCFile}, 1664 }, nil 1665 } 1666 1667 func polygonWallet() (*tWallet, error) { 1668 return &tWallet{ 1669 fund: true, 1670 walletType: "rpc", 1671 config: map[string]string{"providers": polygonAlphaIPCFile}, 1672 }, nil 1673 } 1674 1675 func usdcWallet() (*tWallet, error) { 1676 return &tWallet{ 1677 fund: true, 1678 walletType: "token", 1679 parent: &WalletForm{ 1680 Type: "rpc", 1681 AssetID: eth.BipID, 1682 Config: map[string]string{"providers": ethAlphaIPCFile}, 1683 }, 1684 }, nil 1685 } 1686 1687 func polyUsdcWallet() (*tWallet, error) { 1688 return &tWallet{ 1689 fund: true, 1690 walletType: "token", 1691 parent: &WalletForm{ 1692 Type: "rpc", 1693 AssetID: polygon.BipID, 1694 Config: map[string]string{"providers": polygonAlphaIPCFile}, 1695 }, 1696 }, nil 1697 } 1698 1699 func btcCloneWallet(assetID uint32, node string, wt SimWalletType) (*tWallet, error) { 1700 switch wt { 1701 case WTSPVNative: 1702 return &tWallet{ 1703 walletType: "SPV", 1704 fund: true, 1705 }, nil 1706 case WTElectrum: 1707 // dex/testing/btc/electrum.sh 1708 cfg, err := config.Parse(filepath.Join(dextestDir, "electrum", dex.BipIDSymbol(assetID), "client-config.ini")) 1709 if err != nil { 1710 return nil, err 1711 } 1712 return &tWallet{ 1713 walletType: "electrumRPC", 1714 pass: []byte("abc"), 1715 config: cfg, 1716 fund: true, 1717 }, nil 1718 case WTCoreClone: 1719 default: 1720 return nil, fmt.Errorf("invalid wallet type: %v", wt) 1721 } 1722 1723 rpcWalletType, ok := cloneTypes[assetID] 1724 if !ok { 1725 return nil, fmt.Errorf("invalid wallet type %v for asset %v", wt, assetID) 1726 } 1727 1728 parentNode := node 1729 pass := []byte("abc") 1730 if node == "gamma" || node == "delta" || assetID == zec.BipID || assetID == zcl.BipID { 1731 if assetID != dash.BipID { 1732 pass = nil 1733 } 1734 } 1735 1736 switch assetID { 1737 case doge.BipID, zec.BipID, zcl.BipID, firo.BipID: 1738 // dogecoind, zcashd and firod don't support > 1 wallet, so gamma and delta 1739 // have their own nodes. 1740 default: 1741 switch node { 1742 case "gamma": 1743 parentNode = "alpha" 1744 case "delta": 1745 parentNode = "beta" 1746 } 1747 } 1748 1749 cfg, err := config.Parse(filepath.Join(dextestDir, dex.BipIDSymbol(assetID), parentNode, parentNode+".conf")) 1750 if err != nil { 1751 return nil, err 1752 } 1753 1754 if parentNode != node { 1755 cfg["walletname"] = node 1756 } 1757 1758 // doge fees are slightly higher than others. Leaving this as 0 will 1759 // apply bitcoin limits. 1760 switch assetID { 1761 case doge.BipID: 1762 cfg["fallbackfee"] = fmt.Sprintf("%f", dexdoge.DefaultFee*1000/1e8) 1763 cfg["feeratelimit"] = fmt.Sprintf("%f", dexdoge.DefaultFeeRateLimit*1000/1e8) 1764 case dgb.BipID: 1765 cfg["fallbackfee"] = fmt.Sprintf("%f", dexdgb.DefaultFee*1000/1e8) 1766 cfg["feeratelimit"] = fmt.Sprintf("%f", dexdgb.DefaultFeeRateLimit*1000/1e8) 1767 } 1768 1769 return &tWallet{ 1770 walletType: rpcWalletType, 1771 pass: pass, 1772 config: cfg, 1773 }, nil 1774 } 1775 1776 func dogeWallet(node string) (*tWallet, error) { 1777 return btcCloneWallet(doge.BipID, node, WTCoreClone) 1778 } 1779 1780 func dashWallet(node string) (*tWallet, error) { 1781 return btcCloneWallet(dash.BipID, node, WTCoreClone) 1782 } 1783 1784 func dgbWallet(node string) (*tWallet, error) { 1785 return btcCloneWallet(dgb.BipID, node, WTCoreClone) 1786 } 1787 1788 func zecWallet(node string) (*tWallet, error) { 1789 if node == "alpha" { 1790 return nil, errors.New("cannot use alpha wallet on Zcash") 1791 } 1792 cfg, err := config.Parse(filepath.Join(dextestDir, "zec", node, node+".conf")) 1793 if err != nil { 1794 return nil, err 1795 } 1796 return &tWallet{ 1797 walletType: "zcashdRPC", 1798 config: cfg, 1799 }, nil 1800 } 1801 1802 func zclWallet(node string) (*tWallet, error) { 1803 return btcCloneWallet(zcl.BipID, node, WTCoreClone) 1804 } 1805 1806 func (s *simulationTest) newClient(name string, cl *SimClient) (*simulationClient, error) { 1807 wallets := make(map[uint32]*tWallet, 2) 1808 addWallet := func(assetID uint32, wt SimWalletType, node string) error { 1809 var tw *tWallet 1810 var err error 1811 switch assetID { 1812 case dcr.BipID: 1813 tw, err = dcrWallet(wt, node) 1814 case btc.BipID: 1815 tw, err = btcWallet(wt, node) 1816 case eth.BipID: 1817 tw, err = ethWallet() 1818 case ethUsdcID: 1819 tw, err = usdcWallet() 1820 case polygon.BipID: 1821 tw, err = polygonWallet() 1822 case polygonUsdcID: 1823 tw, err = polyUsdcWallet() 1824 case ltc.BipID: 1825 tw, err = ltcWallet(wt, node) 1826 case bch.BipID: 1827 tw, err = bchWallet(wt, node) 1828 case doge.BipID: 1829 tw, err = dogeWallet(node) 1830 case dgb.BipID: 1831 tw, err = dgbWallet(node) 1832 case dash.BipID: 1833 tw, err = dashWallet(node) 1834 case firo.BipID: 1835 tw, err = firoWallet(wt, node) 1836 case zec.BipID: 1837 tw, err = zecWallet(node) 1838 case zcl.BipID: 1839 tw, err = zclWallet(node) 1840 default: 1841 return fmt.Errorf("no method to create wallet for asset %d", assetID) 1842 } 1843 if err != nil { 1844 return err 1845 } 1846 wallets[assetID] = tw 1847 return nil 1848 } 1849 if err := addWallet(s.base.id, cl.BaseWalletType, cl.BaseNode); err != nil { 1850 return nil, err 1851 } 1852 if err := addWallet(s.quote.id, cl.QuoteWalletType, cl.QuoteNode); err != nil { 1853 return nil, err 1854 } 1855 return &simulationClient{ 1856 SimClient: cl, 1857 name: name, 1858 log: s.log.SubLogger(name), 1859 appPass: []byte(fmt.Sprintf("client-%s", name)), 1860 wallets: wallets, 1861 processedStatus: make(map[order.MatchID]order.MatchStatus), 1862 }, nil 1863 } 1864 1865 type simulationClient struct { 1866 *SimClient 1867 name string 1868 log dex.Logger 1869 wg sync.WaitGroup 1870 core *Core 1871 notes *notificationReader 1872 1873 psMTX sync.Mutex 1874 processedStatus map[order.MatchID]order.MatchStatus 1875 1876 appPass []byte 1877 wallets map[uint32]*tWallet 1878 balances map[uint32]uint64 1879 isSeller bool 1880 // Update after each test run to perform post-test balance 1881 // change validation. Set to nil to NOT perform balance checks. 1882 expectBalanceDiffs map[uint32]int64 1883 lastOrder []byte 1884 filteredConn *tConn 1885 } 1886 1887 var clientCounter uint32 1888 1889 func (client *simulationClient) init(ctx context.Context) error { 1890 tmpDir, _ := os.MkdirTemp("", "") 1891 cNum := atomic.AddUint32(&clientCounter, 1) 1892 var err error 1893 client.core, err = New(&Config{ 1894 DBPath: filepath.Join(tmpDir, "dex.db"), 1895 Net: dex.Regtest, 1896 Logger: dex.StdOutLogger("CORE:"+strconv.Itoa(int(cNum)), client.log.Level()), 1897 }) 1898 if err != nil { 1899 return err 1900 } 1901 1902 client.notes = client.startNotificationReader(ctx) 1903 return nil 1904 } 1905 1906 func (client *simulationClient) replaceConns() { 1907 // Put the real comms in a fake comms we can induce request failures 1908 // with. 1909 client.core.connMtx.Lock() 1910 client.filteredConn = &tConn{ 1911 WsConn: client.core.conns[dexHost].WsConn, 1912 } 1913 client.core.conns[dexHost].WsConn = client.filteredConn 1914 client.core.connMtx.Unlock() 1915 } 1916 1917 func (s *simulationTest) registerDEX(client *simulationClient) error { 1918 dc := client.dc() 1919 if dc != nil { 1920 dc.connMaster.Disconnect() 1921 client.core.connMtx.Lock() 1922 delete(client.core.conns, dc.acct.host) 1923 client.core.connMtx.Unlock() 1924 } 1925 1926 dexConf, err := client.core.GetDEXConfig(dexHost, nil) 1927 if err != nil { 1928 return err 1929 } 1930 1931 feeAssetSymbol := dex.BipIDSymbol(s.regAsset) 1932 1933 bondAsset := dexConf.BondAssets[feeAssetSymbol] 1934 if bondAsset == nil { 1935 return fmt.Errorf("%s not supported for fees!", feeAssetSymbol) 1936 } 1937 postBondRes, err := client.core.PostBond(&PostBondForm{ 1938 Addr: dexHost, 1939 AppPass: client.appPass, 1940 Asset: &s.regAsset, 1941 Bond: bondAsset.Amt, 1942 LockTime: uint64(time.Now().Add(time.Hour * 24 * 30 * 5).Unix()), 1943 }) 1944 if err != nil { 1945 return err 1946 } 1947 client.log.Infof("Sent registration fee to DEX %s.", dexHost) 1948 1949 // Mine block(s) to mark fee as paid. 1950 1951 done := make(chan struct{}) 1952 go func() { 1953 for { 1954 select { 1955 case <-time.After(time.Second * 5): 1956 err = newHarnessCtrl(s.regAsset).mineBlocks(s.ctx, 1) 1957 if err != nil { 1958 client.log.Errorf("error mining fee blocks: %v", err) 1959 } 1960 case <-done: 1961 return 1962 } 1963 } 1964 }() 1965 1966 client.log.Infof("Mined %d %s blocks for fee payment confirmation.", postBondRes.ReqConfirms, feeAssetSymbol) 1967 1968 // Wait up to bTimeout+12 seconds for fee payment. notify_fee times out 1969 // after bTimeout+10 seconds. 1970 feeTimeout := time.Millisecond*time.Duration(client.dc().cfg.BroadcastTimeout) + 12*time.Second 1971 client.log.Infof("Waiting %v for bond confirmation notice.", feeTimeout) 1972 feePaid := client.notes.find(s.ctx, feeTimeout, func(n Notification) bool { 1973 return n.Type() == NoteTypeBondPost && n.Topic() == TopicBondConfirmed 1974 }) 1975 close(done) 1976 if !feePaid { 1977 return fmt.Errorf("fee payment not confirmed after %s", feeTimeout) 1978 } 1979 1980 client.log.Infof("Fee payment confirmed for client %s.", client.name) 1981 return nil 1982 } 1983 1984 type notificationReader struct { 1985 sync.Mutex 1986 feed <-chan Notification 1987 notes []Notification 1988 } 1989 1990 // startNotificationReader opens a new channel for receiving Core notifications 1991 // and starts a goroutine to monitor the channel for new notifications to prevent 1992 // the channel from blocking. Notifications received are added to a notes slice 1993 // to be read by consumers subsequently. 1994 // If multiple concurrent processes require access to Core notifications, each 1995 // should start a notificationReader to ensure that desired notifications are 1996 // received. 1997 func (client *simulationClient) startNotificationReader(ctx context.Context) *notificationReader { 1998 n := ¬ificationReader{ 1999 feed: client.core.NotificationFeed().C, 2000 } 2001 2002 // keep notification channel constantly drained to avoid 2003 // 'blocking notification channel' error logs. 2004 go func() { 2005 for { 2006 select { 2007 case note := <-n.feed: 2008 n.Lock() 2009 n.notes = append(n.notes, note) 2010 n.Unlock() 2011 2012 case <-ctx.Done(): 2013 return 2014 } 2015 } 2016 }() 2017 2018 return n 2019 } 2020 2021 // read returns the notifications saved by this notification reader as a slice 2022 // of Notification objects. The notifications slice is cleared to accept new 2023 // notifications. 2024 func (n *notificationReader) readNotifications() []Notification { 2025 n.Lock() 2026 defer n.Unlock() 2027 notifications := n.notes 2028 n.notes = nil // mark as "read" 2029 return notifications 2030 } 2031 2032 // find repeatedly checks the client.notifications slice for a particular 2033 // notification until the notification is found or the specified waitDuration 2034 // elapses. Clears the notifications slice. 2035 func (n *notificationReader) find(ctx context.Context, waitDuration time.Duration, check func(Notification) bool) bool { 2036 return tryUntil(ctx, waitDuration, func() bool { 2037 notifications := n.readNotifications() 2038 for _, n := range notifications { 2039 if check(n) { 2040 return true 2041 } 2042 } 2043 return false 2044 }) 2045 } 2046 2047 func (s *simulationTest) placeOrder(client *simulationClient, qty, rate uint64, tifNow bool) (string, error) { 2048 dc := client.dc() 2049 mkt := dc.marketConfig(s.marketName) 2050 if mkt == nil { 2051 return "", fmt.Errorf("no %s market found", s.marketName) 2052 } 2053 2054 tradeForm := &TradeForm{ 2055 Host: dexHost, 2056 Base: s.base.id, 2057 Quote: s.quote.id, 2058 IsLimit: true, 2059 Sell: client.isSeller, 2060 Qty: qty, 2061 Rate: rate, 2062 TifNow: tifNow, 2063 } 2064 2065 ord, err := client.core.Trade(client.appPass, tradeForm) 2066 if err != nil { 2067 return "", err 2068 } 2069 2070 client.lastOrder = ord.ID 2071 2072 r := calc.ConventionalRateAlt(rate, s.base.conversionFactor, s.quote.conversionFactor) 2073 2074 client.log.Infof("Client %s placed order %sing %s %s at %f %s/%s (%s).", client.name, sellString(client.isSeller), 2075 s.base.valFmt(qty), s.base.symbol, r, s.quote.symbol, s.base.symbol, ord.ID[:8]) 2076 return ord.ID.String(), nil 2077 } 2078 2079 func (s *simulationTest) updateBalances(client *simulationClient) error { 2080 client.log.Infof("Updating balances for client %s.", client.name) 2081 client.balances = make(map[uint32]uint64, len(client.wallets)) 2082 setBalance := func(a *assetConfig) error { 2083 if parent := client.wallets[a.id].parent; parent != nil { 2084 parentSymbol := dex.BipIDSymbol(parent.AssetID) 2085 parentBalance, err := client.core.AssetBalance(parent.AssetID) 2086 if err != nil { 2087 return fmt.Errorf("error getting parent %s balance: %w", parentSymbol, err) 2088 } 2089 s.log.Infof("Client %s parent %s balance: available %s, immature %s, locked %s.", client.name, parentSymbol, 2090 a.valFmt(parentBalance.Available), a.valFmt(parentBalance.Immature), a.valFmt(parentBalance.Locked)) 2091 } 2092 2093 balances, err := client.core.AssetBalance(a.id) 2094 if err != nil { 2095 return err 2096 } 2097 client.balances[a.id] = balances.Available + balances.Immature + balances.Locked 2098 client.log.Infof("Client %s %s balance: available %s, immature %s, locked %s.", client.name, a.symbol, 2099 a.valFmt(balances.Available), a.valFmt(balances.Immature), a.valFmt(balances.Locked)) 2100 return nil 2101 } 2102 if err := setBalance(s.base); err != nil { 2103 return err 2104 } 2105 return setBalance(s.quote) 2106 } 2107 2108 func (s *simulationTest) assertBalanceChanges(client *simulationClient, isRefund bool) error { 2109 if client.expectBalanceDiffs == nil { 2110 return errors.New("balance diff is nil") 2111 } 2112 2113 ord, err := client.core.Order(client.lastOrder) 2114 if err != nil { 2115 return errors.New("last order not found") 2116 } 2117 2118 var baseFees, quoteFees int64 2119 if fees := ord.FeesPaid; fees != nil && !isRefund { 2120 if ord.Sell { 2121 baseFees = int64(fees.Swap) 2122 quoteFees = int64(fees.Redemption) 2123 } else { 2124 quoteFees = int64(fees.Swap) 2125 baseFees = int64(fees.Redemption) 2126 } 2127 } 2128 2129 // Account assets require a refund fee in addition to the swap amount. 2130 if isRefund { 2131 // NOTE: Gas price may be higher if the eth harness has 2132 // had a lot of use. The minimum is the gas tip cap. 2133 ethRefundFees := int64(dexeth.RefundGas(0 /*version*/)) * dexeth.MinGasTipCap 2134 2135 msgTx := wire.NewMsgTx(0) 2136 prevOut := wire.NewOutPoint(&chainhash.Hash{}, 0) 2137 txIn := wire.NewTxIn(prevOut, []byte{}, nil) 2138 msgTx.AddTxIn(txIn) 2139 size := dexbtc.MsgTxVBytes(msgTx) //wut? btc only 2140 // tx fee is 1sat/vByte on simnet. utxoRefundFees is 293 sats. 2141 // TODO: even simnet rate on other assets like dgb and doge isn't 1... 2142 utxoRefundFees := int64(size + dexbtc.RefundSigScriptSize + dexbtc.P2PKHOutputSize) 2143 // TODO: segwit proper fee rate, but need to get segwit flag: 2144 // size := btc.calcTxSize(msgTx) 2145 // if btc.segwit { 2146 // witnessVBytes := uint64((dexbtc.RefundSigScriptSize + 2 + 3) / 4) 2147 // size += witnessVBytes + dexbtc.P2WPKHOutputSize 2148 // } else { 2149 // size += dexbtc.RefundSigScriptSize + dexbtc.P2PKHOutputSize 2150 // } 2151 if ord.Sell { 2152 if accountBIPs[s.base.id] { 2153 baseFees = ethRefundFees 2154 } else { 2155 baseFees = utxoRefundFees 2156 } 2157 } else { 2158 if accountBIPs[s.quote.id] { 2159 quoteFees = ethRefundFees 2160 } else { 2161 quoteFees = utxoRefundFees 2162 } 2163 } 2164 } 2165 2166 defer func() { 2167 // Clear after assertion so that the next assertion is only performed 2168 // if the expected balance changes are explicitly set. 2169 client.expectBalanceDiffs = nil 2170 }() 2171 prevBalances := client.balances 2172 err = s.updateBalances(client) 2173 if err != nil { 2174 return err 2175 } 2176 2177 checkDiff := func(a *assetConfig, expDiff, fees int64) error { 2178 // actual diff will likely be less than expected because of tx fees 2179 // TODO: account for actual fee(s) or use a more realistic fee estimate. 2180 expVal := expDiff 2181 if !a.isToken { 2182 expVal -= fees 2183 } 2184 2185 minExpectedDiff, maxExpectedDiff := int64(float64(expVal)*0.95), int64(float64(expVal)*1.05) 2186 2187 // diffs can be negative 2188 if minExpectedDiff > maxExpectedDiff { 2189 minExpectedDiff, maxExpectedDiff = maxExpectedDiff, minExpectedDiff 2190 } 2191 2192 balanceDiff := int64(client.balances[a.id]) - int64(prevBalances[a.id]) 2193 if balanceDiff < minExpectedDiff || balanceDiff > maxExpectedDiff { 2194 return fmt.Errorf("%s balance change not in expected range %s - %s, got %s", a.symbol, 2195 a.valFmt(minExpectedDiff), a.valFmt(maxExpectedDiff), a.valFmt(balanceDiff)) 2196 } 2197 client.log.Infof("%s balance change %s is in expected range of %s - %s.", a.symbol, 2198 a.valFmt(balanceDiff), a.valFmt(minExpectedDiff), a.valFmt(maxExpectedDiff)) 2199 return nil 2200 } 2201 2202 if err := checkDiff(s.base, client.expectBalanceDiffs[s.base.id], baseFees); err != nil { 2203 return err 2204 } 2205 return checkDiff(s.quote, client.expectBalanceDiffs[s.quote.id], quoteFees) 2206 } 2207 2208 func (client *simulationClient) dc() *dexConnection { 2209 client.core.connMtx.RLock() 2210 defer client.core.connMtx.RUnlock() 2211 return client.core.conns[dexHost] 2212 } 2213 2214 func (client *simulationClient) findOrder(orderID string) (*trackedTrade, error) { 2215 oid, err := order.IDFromHex(orderID) 2216 if err != nil { 2217 return nil, fmt.Errorf("error parsing order id %s -> %v", orderID, err) 2218 } 2219 tracker, _ := client.dc().findOrder(oid) 2220 return tracker, nil 2221 } 2222 2223 func (client *simulationClient) disableWallets() { 2224 client.log.Infof("Disabling wallets for client %s.", client.name) 2225 client.core.walletMtx.Lock() 2226 for _, wallet := range client.core.wallets { 2227 wallet.setDisabled(true) 2228 } 2229 client.core.walletMtx.Unlock() 2230 } 2231 2232 func (client *simulationClient) enableWallets() { 2233 client.log.Infof("Enabling wallets for client %s.", client.name) 2234 client.core.walletMtx.Lock() 2235 for _, wallet := range client.core.wallets { 2236 wallet.setDisabled(false) 2237 } 2238 client.core.walletMtx.Unlock() 2239 } 2240 2241 func (client *simulationClient) disconnectWallets() { 2242 client.log.Infof("Disconnecting wallets for client %s.", client.name) 2243 client.core.walletMtx.Lock() 2244 for _, wallet := range client.core.wallets { 2245 wallet.Disconnect() 2246 } 2247 client.core.walletMtx.Unlock() 2248 } 2249 2250 var _ comms.WsConn = (*tConn)(nil) 2251 2252 type tConn struct { 2253 comms.WsConn 2254 requestFilter atomic.Value // func(route string) error 2255 } 2256 2257 func (tc *tConn) Request(msg *msgjson.Message, respHandler func(*msgjson.Message)) error { 2258 return tc.RequestWithTimeout(msg, respHandler, time.Minute, func() {}) 2259 } 2260 2261 func (tc *tConn) RequestWithTimeout(msg *msgjson.Message, respHandler func(*msgjson.Message), expireTime time.Duration, expire func()) error { 2262 if fi := tc.requestFilter.Load(); fi != nil { 2263 if err := fi.(func(string) error)(msg.Route); err != nil { 2264 return err 2265 } 2266 } 2267 return tc.WsConn.RequestWithTimeout(msg, respHandler, expireTime, expire) 2268 } 2269 2270 func init() { 2271 if race { 2272 sleepFactor = 3 2273 } 2274 }