decred.org/dcrdex@v1.0.5/client/asset/btc/livetest/livetest.go (about) 1 package livetest 2 3 // Regnet tests expect the BTC test harness to be running. 4 // 5 // Sim harness info: 6 // The harness has three wallets, alpha, beta, and gamma. 7 // All three wallets have confirmed UTXOs. 8 // The beta wallet has only coinbase outputs. 9 // The alpha wallet has coinbase outputs too, but has sent some to the gamma 10 // wallet, so also has some change outputs. 11 // The gamma wallet has regular transaction outputs of varying size and 12 // confirmation count. Value:Confirmations = 13 // 10:8, 18:7, 5:6, 7:5, 1:4, 15:3, 3:2, 25:1 14 15 import ( 16 "bytes" 17 "context" 18 "crypto/sha256" 19 "errors" 20 "fmt" 21 "math/rand" 22 "os" 23 "os/exec" 24 "os/user" 25 "path/filepath" 26 "strings" 27 "sync/atomic" 28 "testing" 29 "time" 30 31 "decred.org/dcrdex/client/asset" 32 "decred.org/dcrdex/dex" 33 "decred.org/dcrdex/dex/config" 34 "decred.org/dcrdex/dex/wait" 35 ) 36 37 var tLogger dex.Logger 38 39 type WalletConstructor func(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) 40 41 func tBackend(ctx context.Context, t *testing.T, cfg *Config, dir string, walletName *WalletName, blkFunc func(string)) *connectedWallet { 42 t.Helper() 43 user, err := user.Current() 44 if err != nil { 45 t.Fatalf("error getting current user: %v", err) 46 } 47 48 fileName := walletName.Filename 49 if fileName == "" { 50 fileName = walletName.Node + ".conf" 51 } 52 53 cfgPath := filepath.Join(user.HomeDir, "dextest", cfg.Asset.Symbol, walletName.Node, fileName) 54 settings, err := config.Parse(cfgPath) 55 if err != nil { 56 t.Fatalf("error reading config options: %v", err) 57 } 58 settings["walletname"] = walletName.Name 59 if cfg.SplitTx { 60 settings["txsplit"] = "1" 61 } 62 63 reportName := fmt.Sprintf("%s:%s-%s", cfg.Asset.Symbol, walletName.Node, walletName.Name) 64 65 notes := make(chan asset.WalletNotification, 1) 66 walletCfg := &asset.WalletConfig{ 67 Type: walletName.WalletType, 68 Settings: settings, 69 Emit: asset.NewWalletEmitter(notes, cfg.Asset.ID, tLogger), 70 PeersChange: func(num uint32, err error) { 71 fmt.Printf("peer count = %d, err = %v", num, err) 72 }, 73 DataDir: dir, 74 } 75 76 w, err := cfg.NewWallet(walletCfg, tLogger.SubLogger(walletName.Node+"."+walletName.Name), dex.Regtest) 77 if err != nil { 78 t.Fatalf("error creating backend: %v", err) 79 } 80 81 cm := dex.NewConnectionMaster(w) 82 err = cm.Connect(ctx) 83 if err != nil { 84 t.Fatalf("error connecting backend: %v", err) 85 } 86 87 go func() { 88 for { 89 select { 90 case ni := <-notes: 91 switch ni.(type) { 92 case *asset.TipChangeNote: 93 blkFunc(reportName) 94 } 95 case <-ctx.Done(): 96 return 97 } 98 } 99 }() 100 101 return &connectedWallet{w, cm} 102 } 103 104 type connectedWallet struct { 105 asset.Wallet 106 cxn *dex.ConnectionMaster 107 } 108 109 type testRig struct { 110 t *testing.T 111 symbol string 112 firstWallet *connectedWallet 113 secondWallet *connectedWallet 114 } 115 116 func (rig *testRig) close() { 117 closeConn := func(cm *dex.ConnectionMaster) { 118 closed := make(chan struct{}) 119 go func() { 120 cm.Disconnect() 121 close(closed) 122 }() 123 select { 124 case <-closed: 125 case <-time.NewTimer(time.Second * 30).C: 126 rig.t.Fatalf("failed to disconnect") 127 } 128 } 129 closeConn(rig.firstWallet.cxn) 130 closeConn(rig.secondWallet.cxn) 131 } 132 133 func (rig *testRig) mineAlpha() error { 134 var tmuxWindow string 135 switch rig.symbol { 136 case "zec", "firo", "doge": 137 tmuxWindow = rig.symbol + "-harness:4" 138 default: 139 tmuxWindow = rig.symbol + "-harness:2" 140 } 141 return exec.Command("tmux", "send-keys", "-t", tmuxWindow, "./mine-alpha 1", "C-m").Run() 142 } 143 144 func randBytes(l int) []byte { 145 b := make([]byte, l) 146 rand.Read(b) 147 return b 148 } 149 150 type WalletName struct { 151 Node string 152 Name string 153 // WalletType is optional 154 WalletType string 155 // Filename is optional. If specified, it will be used instead of 156 // [node].conf. 157 Filename string 158 } 159 160 type Config struct { 161 NewWallet WalletConstructor 162 LotSize uint64 163 Asset *dex.Asset 164 SplitTx bool 165 SPV bool 166 FirstWallet *WalletName 167 SecondWallet *WalletName 168 } 169 170 func Run(t *testing.T, cfg *Config) { 171 tLogger = dex.StdOutLogger("TEST", dex.LevelDebug) 172 tCtx, shutdown := context.WithCancel(context.Background()) 173 defer shutdown() 174 175 tStart := time.Now() 176 177 walletPassword := []byte("abc") 178 179 if cfg.FirstWallet == nil { 180 cfg.FirstWallet = &WalletName{Node: "alpha"} 181 } 182 183 if cfg.SecondWallet == nil { 184 cfg.SecondWallet = &WalletName{ 185 Node: "alpha", 186 Name: "gamma", 187 } 188 } 189 190 var blockReported uint32 191 blkFunc := func(name string) { 192 atomic.StoreUint32(&blockReported, 1) 193 tLogger.Infof("%s has reported a new block", name) 194 } 195 196 rig := &testRig{ 197 t: t, 198 symbol: cfg.Asset.Symbol, 199 } 200 201 var expConfs uint32 202 blockWait := time.Second * 5 203 if cfg.SPV { 204 blockWait = time.Second * 6 205 } 206 mine := func() { 207 if cfg.SPV { // broadcast with spv client takes a bit longer 208 time.Sleep(blockWait) 209 } 210 rig.mineAlpha() 211 expConfs++ 212 time.Sleep(blockWait) 213 } 214 215 tmpDir, err := os.MkdirTemp("", "") 216 if err != nil { 217 t.Fatalf("Error creating temporary directory: %v", err) 218 } 219 defer os.RemoveAll(tmpDir) 220 firstDir := filepath.Join(tmpDir, "first") 221 secondDir := filepath.Join(tmpDir, "second") 222 223 t.Log("Setting up alpha/beta/gamma wallet backends...") 224 rig.firstWallet = tBackend(tCtx, t, cfg, firstDir, cfg.FirstWallet, blkFunc) 225 rig.secondWallet = tBackend(tCtx, t, cfg, secondDir, cfg.SecondWallet, blkFunc) 226 defer rig.close() 227 228 // Unlocks a wallet for use. 229 unlock := func(w *connectedWallet, name string) { 230 a, isAuthenticator := w.Wallet.(asset.Authenticator) 231 if isAuthenticator { 232 err := a.Unlock(walletPassword) 233 if err != nil && !strings.Contains(err.Error(), "running with an unencrypted wallet, but walletpassphrase was called") { 234 t.Fatalf("error unlocking %s wallet: %v", name, err) 235 } 236 } 237 } 238 239 unlock(rig.firstWallet, cfg.FirstWallet.Name) 240 unlock(rig.secondWallet, cfg.SecondWallet.Name) 241 242 var lots uint64 = 2 243 contractValue := lots * cfg.LotSize 244 245 tLogger.Info("Wallets configured") 246 247 inUTXOs := func(utxo asset.Coin, utxos []asset.Coin) bool { 248 for _, u := range utxos { 249 if bytes.Equal(u.ID(), utxo.ID()) { 250 return true 251 } 252 } 253 return false 254 } 255 256 // Check available amount. 257 checkAmt := func(name string, wallet *connectedWallet) { 258 bal, err := wallet.Balance() 259 if err != nil { 260 t.Fatalf("error getting available balance: %v", err) 261 } 262 tLogger.Infof("%s %f available, %f immature, %f locked", 263 name, float64(bal.Available)/1e8, float64(bal.Immature)/1e8, float64(bal.Locked)/1e8) 264 } 265 checkAmt("first", rig.firstWallet) 266 checkAmt("second", rig.secondWallet) 267 268 ord := &asset.Order{ 269 Version: 0, 270 Value: contractValue * 3, 271 MaxSwapCount: lots * 3, 272 MaxFeeRate: cfg.Asset.MaxFeeRate, 273 // Redeem vars omitted. 274 } 275 setOrderValue := func(v uint64) { 276 ord.Value = v 277 ord.MaxSwapCount = v / cfg.LotSize 278 } 279 280 tLogger.Info("Testing FundOrder") 281 282 // Gamma should only have 10 BTC utxos, so calling fund for less should only 283 // return 1 utxo. 284 utxos, _, _, err := rig.secondWallet.FundOrder(ord) 285 if err != nil { 286 t.Fatalf("Funding error: %v", err) 287 } 288 utxo := utxos[0] 289 290 // UTXOs should be locked 291 utxos, _, _, _ = rig.secondWallet.FundOrder(ord) 292 if inUTXOs(utxo, utxos) { 293 t.Fatalf("received locked output") 294 } 295 rig.secondWallet.ReturnCoins([]asset.Coin{utxo}) 296 rig.secondWallet.ReturnCoins(utxos) 297 // Make sure we get the first utxo back with Fund. 298 utxos, _, _, _ = rig.secondWallet.FundOrder(ord) 299 if !cfg.SplitTx && !inUTXOs(utxo, utxos) { 300 t.Fatalf("unlocked output not returned") 301 } 302 rig.secondWallet.ReturnCoins(utxos) 303 304 // Get a separate set of UTXOs for each contract. 305 setOrderValue(contractValue) 306 utxos1, _, _, err := rig.secondWallet.FundOrder(ord) 307 if err != nil { 308 t.Fatalf("error funding first contract: %v", err) 309 } 310 // Get a separate set of UTXOs for each contract. 311 setOrderValue(contractValue * 2) 312 utxos2, _, _, err := rig.secondWallet.FundOrder(ord) 313 if err != nil { 314 t.Fatalf("error funding second contract: %v", err) 315 } 316 317 // For some reason, SwapConfirmations fails with a split tx in SPV mode in 318 // this test. It works fine in practice, so figuring this out is a TODO. 319 if cfg.SplitTx && cfg.SPV { 320 time.Sleep(blockWait) 321 rig.mineAlpha() 322 time.Sleep(blockWait) 323 } 324 325 address, err := rig.firstWallet.DepositAddress() 326 if err != nil { 327 t.Fatalf("error getting alpha address: %v", err) 328 } 329 330 secretKey1 := randBytes(32) 331 keyHash1 := sha256.Sum256(secretKey1) 332 secretKey2 := randBytes(32) 333 keyHash2 := sha256.Sum256(secretKey2) 334 lockTime := time.Now().Add(time.Hour * 8).UTC() 335 // Have gamma send a swap contract to the alpha address. 336 contract1 := &asset.Contract{ 337 Address: address, 338 Value: contractValue, 339 SecretHash: keyHash1[:], 340 LockTime: uint64(lockTime.Unix()), 341 } 342 contract2 := &asset.Contract{ 343 Address: address, 344 Value: contractValue * 2, 345 SecretHash: keyHash2[:], 346 LockTime: uint64(lockTime.Unix()), 347 } 348 swaps := &asset.Swaps{ 349 Inputs: append(utxos1, utxos2...), 350 Contracts: []*asset.Contract{contract1, contract2}, 351 FeeRate: cfg.Asset.MaxFeeRate, 352 } 353 354 tLogger.Info("Testing Swap") 355 356 receipts, _, _, err := rig.secondWallet.Swap(swaps) 357 if err != nil { 358 t.Fatalf("error sending swap transaction: %v", err) 359 } 360 if len(receipts) != 2 { 361 t.Fatalf("expected 1 receipt, got %d", len(receipts)) 362 } 363 364 tLogger.Infof("Sent %d swaps", len(receipts)) 365 for i, r := range receipts { 366 tLogger.Infof(" Swap # %d: %s", i+1, r.Coin()) 367 } 368 369 // Don't check zero confs for SPV. Core deals with the failures until the 370 // tx is mined. 371 372 if cfg.SPV { 373 mine() 374 } 375 376 confCoin := receipts[0].Coin() 377 confContract := receipts[0].Contract() 378 checkConfs := func(minN uint32, expSpent bool) { 379 t.Helper() 380 confs, spent, err := rig.secondWallet.SwapConfirmations(context.Background(), confCoin.ID(), confContract, tStart) 381 if err != nil { 382 if minN > 0 || !errors.Is(err, asset.CoinNotFoundError) { 383 t.Fatalf("error getting %d confs: %v", minN, err) 384 } 385 } 386 if confs < minN { 387 t.Fatalf("expected %d confs, got %d", minN, confs) 388 } 389 if spent != expSpent { 390 t.Fatalf("checkConfs: expected spent = %t, got %t", expSpent, spent) 391 } 392 } 393 394 checkConfs(expConfs, false) 395 396 latencyQ := wait.NewTickerQueue(time.Millisecond * 500) 397 go latencyQ.Run(tCtx) 398 399 getAuditInfo := func(coinID, contract []byte) *asset.AuditInfo { 400 c := make(chan *asset.AuditInfo, 1) 401 latencyQ.Wait(&wait.Waiter{ 402 Expiration: time.Now().Add(time.Second * 10), 403 TryFunc: func() wait.TryDirective { 404 ai, err := rig.firstWallet.AuditContract(coinID, contract, nil, false) // no TxData because server gets that for us in practice! 405 if err != nil { 406 if strings.Contains(err.Error(), "error finding unspent contract") { 407 return wait.TryAgain 408 } 409 c <- nil 410 t.Fatalf("error auditing contract: %v", err) 411 } 412 c <- ai 413 return wait.DontTryAgain 414 }, 415 ExpireFunc: func() { 416 t.Fatalf("makeRedemption -> AuditContract timed out") 417 }, 418 }) 419 420 // Alpha should be able to redeem. 421 return <-c 422 } 423 424 makeRedemption := func(swapVal uint64, receipt asset.Receipt, secret []byte) *asset.Redemption { 425 t.Helper() 426 427 ai := getAuditInfo(receipt.Coin().ID(), receipt.Contract()) 428 auditCoin := ai.Coin 429 if ai.Recipient != address { 430 t.Fatalf("wrong address. %s != %s", ai.Recipient, address) 431 } 432 if auditCoin.Value() != swapVal { 433 t.Fatalf("wrong contract value. wanted %d, got %d", swapVal, auditCoin.Value()) 434 } 435 confs, spent, err := rig.firstWallet.SwapConfirmations(tCtx, receipt.Coin().ID(), receipt.Contract(), tStart) 436 if err != nil { 437 t.Fatalf("error getting confirmations: %v", err) 438 } 439 if confs < expConfs { 440 t.Fatalf("unexpected number of confirmations. wanted %d, got %d", expConfs, confs) 441 } 442 if spent { 443 t.Fatalf("makeRedemption: expected unspent, got spent") 444 } 445 if ai.Expiration.Equal(lockTime) { 446 t.Fatalf("wrong lock time. wanted %s, got %s", lockTime, ai.Expiration) 447 } 448 return &asset.Redemption{ 449 Spends: ai, 450 Secret: secret, 451 } 452 } 453 454 tLogger.Info("Testing AuditContract") 455 456 redemptions := []*asset.Redemption{ 457 makeRedemption(contractValue, receipts[0], secretKey1), 458 makeRedemption(contractValue*2, receipts[1], secretKey2), 459 } 460 461 tLogger.Info("Testing Redeem") 462 463 _, _, _, err = rig.firstWallet.Redeem(&asset.RedeemForm{ 464 Redemptions: redemptions, 465 }) 466 if err != nil { 467 t.Fatalf("redemption error: %v", err) 468 } 469 470 // Find the redemption 471 472 // Only do the mempool zero-conf redemption check when not spv. 473 if cfg.SPV { 474 mine() 475 } 476 477 swapReceipt := receipts[0] 478 ctx, cancelFind := context.WithDeadline(tCtx, time.Now().Add(time.Second*30)) 479 defer cancelFind() 480 481 tLogger.Info("Testing FindRedemption") 482 483 found := make(chan struct{}) 484 latencyQ.Wait(&wait.Waiter{ 485 Expiration: time.Now().Add(time.Second * 10), 486 TryFunc: func() wait.TryDirective { 487 ctx, cancel := context.WithTimeout(tCtx, time.Second) 488 defer cancel() 489 _, _, err = rig.secondWallet.FindRedemption(ctx, swapReceipt.Coin().ID(), nil) 490 if err != nil { 491 return wait.TryAgain 492 } 493 close(found) 494 return wait.DontTryAgain 495 }, 496 ExpireFunc: func() { t.Fatalf("mempool FindRedemption timed out") }, 497 }) 498 <-found 499 500 // Mine a block and find the redemption again. 501 mine() 502 if atomic.LoadUint32(&blockReported) == 0 { 503 t.Fatalf("no block reported") 504 } 505 // Check that there is 1 confirmation on the swap 506 checkConfs(expConfs, true) 507 _, checkKey, err := rig.secondWallet.FindRedemption(ctx, swapReceipt.Coin().ID(), nil) 508 if err != nil { 509 t.Fatalf("error finding confirmed redemption: %v", err) 510 } 511 if !bytes.Equal(checkKey, secretKey1) { 512 t.Fatalf("findRedemption (unconfirmed) key mismatch. %x != %x", checkKey, secretKey1) 513 } 514 515 // Now send another one with lockTime = now and try to refund it. 516 secretKey := randBytes(32) 517 keyHash := sha256.Sum256(secretKey) 518 lockTime = time.Now().Add(-8 * time.Hour) 519 520 // Have gamma send a swap contract to the alpha address. 521 setOrderValue(contractValue) 522 utxos, _, _, _ = rig.secondWallet.FundOrder(ord) 523 contract := &asset.Contract{ 524 Address: address, 525 Value: contractValue, 526 SecretHash: keyHash[:], 527 LockTime: uint64(lockTime.Unix()), 528 } 529 swaps = &asset.Swaps{ 530 Inputs: utxos, 531 Contracts: []*asset.Contract{contract}, 532 FeeRate: cfg.Asset.MaxFeeRate, 533 } 534 535 tLogger.Info("Testing Refund") 536 537 receipts, _, _, err = rig.secondWallet.Swap(swaps) 538 if err != nil { 539 t.Fatalf("error sending swap transaction: %v", err) 540 } 541 542 if len(receipts) != 1 { 543 t.Fatalf("expected 1 receipt, got %d", len(receipts)) 544 } 545 swapReceipt = receipts[0] 546 547 // SPV doesn't recognize ownership of the swap output, so we need to mine 548 // the transaction in order to establish spent status. In theory, we could 549 // just yolo and refund regardless of spent status. 550 if cfg.SPV { 551 mine() 552 } 553 554 const defaultFee = 100 555 coinID, err := rig.secondWallet.Refund(swapReceipt.Coin().ID(), swapReceipt.Contract(), 100) 556 if err != nil { 557 t.Fatalf("refund error: %v", err) 558 } 559 c, _ := asset.DecodeCoinID(cfg.Asset.ID, coinID) 560 tLogger.Infof("Refunded with %s", c) 561 562 // Test Send. 563 tLogger.Info("Testing Send") 564 coin, err := rig.secondWallet.Send(address, cfg.LotSize, defaultFee) 565 if err != nil { 566 t.Fatalf("error sending: %v", err) 567 } 568 if coin.Value() != cfg.LotSize { 569 t.Fatalf("Expected %d got %d", cfg.LotSize, coin.Value()) 570 } 571 tLogger.Infof("Sent with %s", coin.String()) 572 573 // Test Withdraw. 574 withdrawer, _ := rig.secondWallet.Wallet.(asset.Withdrawer) 575 tLogger.Info("Testing Withdraw") 576 coin, err = withdrawer.Withdraw(address, cfg.LotSize, defaultFee) 577 if err != nil { 578 t.Fatalf("error withdrawing: %v", err) 579 } 580 if coin.Value() >= cfg.LotSize { 581 t.Fatalf("Expected less than %d got %d", cfg.LotSize, coin.Value()) 582 } 583 tLogger.Infof("Withdrew with %s", coin.String()) 584 585 if cfg.SPV { 586 mine() 587 } 588 }