gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/wallet/wallet_test.go (about) 1 package wallet 2 3 import ( 4 "path/filepath" 5 "testing" 6 "time" 7 8 "gitlab.com/SiaPrime/SiaPrime/build" 9 "gitlab.com/SiaPrime/SiaPrime/crypto" 10 "gitlab.com/SiaPrime/SiaPrime/modules" 11 "gitlab.com/SiaPrime/SiaPrime/modules/consensus" 12 "gitlab.com/SiaPrime/SiaPrime/modules/gateway" 13 "gitlab.com/SiaPrime/SiaPrime/modules/miner" 14 "gitlab.com/SiaPrime/SiaPrime/modules/transactionpool" 15 "gitlab.com/SiaPrime/SiaPrime/types" 16 ) 17 18 // A Wallet tester contains a ConsensusTester and has a bunch of helpful 19 // functions for facilitating wallet integration testing. 20 type walletTester struct { 21 cs modules.ConsensusSet 22 gateway modules.Gateway 23 tpool modules.TransactionPool 24 miner modules.TestMiner 25 wallet *Wallet 26 27 walletMasterKey crypto.CipherKey 28 29 persistDir string 30 } 31 32 // createWalletTester takes a testing.T and creates a WalletTester. 33 func createWalletTester(name string, deps modules.Dependencies) (*walletTester, error) { 34 // Create the modules 35 testdir := build.TempDir(modules.WalletDir, name) 36 g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir)) 37 if err != nil { 38 return nil, err 39 } 40 cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir)) 41 if err != nil { 42 return nil, err 43 } 44 tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir)) 45 if err != nil { 46 return nil, err 47 } 48 w, err := NewCustomWallet(cs, tp, filepath.Join(testdir, modules.WalletDir), deps) 49 if err != nil { 50 return nil, err 51 } 52 masterKey := crypto.GenerateSiaKey(crypto.TypeDefaultWallet) 53 _, err = w.Encrypt(masterKey) 54 if err != nil { 55 return nil, err 56 } 57 err = w.Unlock(masterKey) 58 if err != nil { 59 return nil, err 60 } 61 m, err := miner.New(cs, tp, w, filepath.Join(testdir, modules.WalletDir)) 62 if err != nil { 63 return nil, err 64 } 65 66 // Assemble all components into a wallet tester. 67 wt := &walletTester{ 68 cs: cs, 69 gateway: g, 70 tpool: tp, 71 miner: m, 72 wallet: w, 73 74 walletMasterKey: masterKey, 75 76 persistDir: testdir, 77 } 78 79 // Mine blocks until there is money in the wallet. 80 for i := types.BlockHeight(0); i <= types.MaturityDelay; i++ { 81 b, _ := wt.miner.FindBlock() 82 err := wt.cs.AcceptBlock(b) 83 if err != nil { 84 return nil, err 85 } 86 } 87 return wt, nil 88 } 89 90 // createBlankWalletTester creates a wallet tester that has not mined any 91 // blocks or encrypted the wallet. 92 func createBlankWalletTester(name string) (*walletTester, error) { 93 // Create the modules 94 testdir := build.TempDir(modules.WalletDir, name) 95 g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir)) 96 if err != nil { 97 return nil, err 98 } 99 cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir)) 100 if err != nil { 101 return nil, err 102 } 103 tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir)) 104 if err != nil { 105 return nil, err 106 } 107 w, err := New(cs, tp, filepath.Join(testdir, modules.WalletDir)) 108 if err != nil { 109 return nil, err 110 } 111 m, err := miner.New(cs, tp, w, filepath.Join(testdir, modules.MinerDir)) 112 if err != nil { 113 return nil, err 114 } 115 116 // Assemble all components into a wallet tester. 117 wt := &walletTester{ 118 gateway: g, 119 cs: cs, 120 tpool: tp, 121 miner: m, 122 wallet: w, 123 124 persistDir: testdir, 125 } 126 return wt, nil 127 } 128 129 // closeWt closes all of the modules in the wallet tester. 130 func (wt *walletTester) closeWt() error { 131 errs := []error{ 132 wt.gateway.Close(), 133 wt.cs.Close(), 134 wt.tpool.Close(), 135 wt.miner.Close(), 136 wt.wallet.Close(), 137 } 138 return build.JoinErrors(errs, "; ") 139 } 140 141 // TestNilInputs tries starting the wallet using nil inputs. 142 func TestNilInputs(t *testing.T) { 143 if testing.Short() { 144 t.SkipNow() 145 } 146 testdir := build.TempDir(modules.WalletDir, t.Name()) 147 g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir)) 148 if err != nil { 149 t.Fatal(err) 150 } 151 cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir)) 152 if err != nil { 153 t.Fatal(err) 154 } 155 tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir)) 156 if err != nil { 157 t.Fatal(err) 158 } 159 160 wdir := filepath.Join(testdir, modules.WalletDir) 161 _, err = New(cs, nil, wdir) 162 if err != errNilTpool { 163 t.Error(err) 164 } 165 _, err = New(nil, tp, wdir) 166 if err != errNilConsensusSet { 167 t.Error(err) 168 } 169 _, err = New(nil, nil, wdir) 170 if err != errNilConsensusSet { 171 t.Error(err) 172 } 173 } 174 175 // TestAllAddresses checks that AllAddresses returns all of the wallet's 176 // addresses in sorted order. 177 func TestAllAddresses(t *testing.T) { 178 if testing.Short() { 179 t.Skip() 180 } 181 wt, err := createBlankWalletTester(t.Name()) 182 if err != nil { 183 t.Fatal(err) 184 } 185 defer wt.closeWt() 186 187 wt.wallet.keys[types.UnlockHash{1}] = spendableKey{} 188 wt.wallet.keys[types.UnlockHash{5}] = spendableKey{} 189 wt.wallet.keys[types.UnlockHash{0}] = spendableKey{} 190 wt.wallet.keys[types.UnlockHash{2}] = spendableKey{} 191 wt.wallet.keys[types.UnlockHash{4}] = spendableKey{} 192 wt.wallet.keys[types.UnlockHash{3}] = spendableKey{} 193 addrs, err := wt.wallet.AllAddresses() 194 if err != nil { 195 t.Fatal(err) 196 } 197 for i := range addrs { 198 if addrs[i][0] != byte(i) { 199 t.Error("address sorting failed:", i, addrs[i][0]) 200 } 201 } 202 } 203 204 // TestCloseWallet tries to close the wallet. 205 func TestCloseWallet(t *testing.T) { 206 if testing.Short() { 207 t.Skip() 208 } 209 testdir := build.TempDir(modules.WalletDir, t.Name()) 210 g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir)) 211 if err != nil { 212 t.Fatal(err) 213 } 214 cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir)) 215 if err != nil { 216 t.Fatal(err) 217 } 218 tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir)) 219 if err != nil { 220 t.Fatal(err) 221 } 222 wdir := filepath.Join(testdir, modules.WalletDir) 223 w, err := New(cs, tp, wdir) 224 if err != nil { 225 t.Fatal(err) 226 } 227 if err := w.Close(); err != nil { 228 t.Fatal(err) 229 } 230 } 231 232 // TestRescanning verifies that calling Rescanning during a scan operation 233 // returns true, and false otherwise. 234 func TestRescanning(t *testing.T) { 235 if testing.Short() { 236 t.SkipNow() 237 } 238 wt, err := createWalletTester(t.Name(), modules.ProdDependencies) 239 if err != nil { 240 t.Fatal(err) 241 } 242 defer wt.closeWt() 243 244 // A fresh wallet should not be rescanning. 245 rescanning, err := wt.wallet.Rescanning() 246 if err != nil { 247 t.Fatal(err) 248 } 249 if rescanning { 250 t.Fatal("fresh wallet should not report that a scan is underway") 251 } 252 253 // lock the wallet 254 wt.wallet.Lock() 255 256 // spawn an unlock goroutine 257 errChan := make(chan error) 258 go func() { 259 // acquire the write lock so that Unlock acquires the trymutex, but 260 // cannot proceed further 261 wt.wallet.mu.Lock() 262 errChan <- wt.wallet.Unlock(wt.walletMasterKey) 263 }() 264 265 // wait for goroutine to start, after which Rescanning should return true 266 time.Sleep(time.Millisecond * 10) 267 rescanning, err = wt.wallet.Rescanning() 268 if err != nil { 269 t.Fatal(err) 270 } 271 if !rescanning { 272 t.Fatal("wallet should report that a scan is underway") 273 } 274 275 // release the mutex and allow the call to complete 276 wt.wallet.mu.Unlock() 277 if err := <-errChan; err != nil { 278 t.Fatal("unlock failed:", err) 279 } 280 281 // Rescanning should now return false again 282 rescanning, err = wt.wallet.Rescanning() 283 if err != nil { 284 t.Fatal(err) 285 } 286 if rescanning { 287 t.Fatal("wallet should not report that a scan is underway") 288 } 289 } 290 291 // TestFutureAddressGeneration checks if the right amount of future addresses 292 // is generated after calling NextAddress() or locking + unlocking the wallet. 293 func TestLookaheadGeneration(t *testing.T) { 294 if testing.Short() { 295 t.SkipNow() 296 } 297 wt, err := createWalletTester(t.Name(), modules.ProdDependencies) 298 if err != nil { 299 t.Fatal(err) 300 } 301 defer wt.closeWt() 302 303 // Check if number of future keys is correct 304 wt.wallet.mu.RLock() 305 progress, err := dbGetPrimarySeedProgress(wt.wallet.dbTx) 306 wt.wallet.mu.RUnlock() 307 if err != nil { 308 t.Fatal("Couldn't fetch primary seed from db") 309 } 310 311 actualKeys := uint64(len(wt.wallet.lookahead)) 312 expectedKeys := maxLookahead(progress) 313 if actualKeys != expectedKeys { 314 t.Errorf("expected len(lookahead) == %d but was %d", actualKeys, expectedKeys) 315 } 316 317 // Generate some more keys 318 for i := 0; i < 100; i++ { 319 wt.wallet.NextAddress() 320 } 321 322 // Lock and unlock 323 wt.wallet.Lock() 324 wt.wallet.Unlock(wt.walletMasterKey) 325 326 wt.wallet.mu.RLock() 327 progress, err = dbGetPrimarySeedProgress(wt.wallet.dbTx) 328 wt.wallet.mu.RUnlock() 329 if err != nil { 330 t.Fatal("Couldn't fetch primary seed from db") 331 } 332 333 actualKeys = uint64(len(wt.wallet.lookahead)) 334 expectedKeys = maxLookahead(progress) 335 if actualKeys != expectedKeys { 336 t.Errorf("expected len(lookahead) == %d but was %d", actualKeys, expectedKeys) 337 } 338 339 wt.wallet.mu.RLock() 340 defer wt.wallet.mu.RUnlock() 341 for i := range wt.wallet.keys { 342 _, exists := wt.wallet.lookahead[i] 343 if exists { 344 t.Fatal("wallet keys contained a key which is also present in lookahead") 345 } 346 } 347 } 348 349 // TestAdvanceLookaheadNoRescan tests if a transaction to multiple lookahead addresses 350 // is handled correctly without forcing a wallet rescan. 351 func TestAdvanceLookaheadNoRescan(t *testing.T) { 352 if testing.Short() { 353 t.SkipNow() 354 } 355 wt, err := createWalletTester(t.Name(), modules.ProdDependencies) 356 if err != nil { 357 t.Fatal(err) 358 } 359 defer wt.closeWt() 360 361 builder, err := wt.wallet.StartTransaction() 362 if err != nil { 363 t.Fatal(err) 364 } 365 payout := types.ZeroCurrency 366 367 // Get the current progress 368 wt.wallet.mu.RLock() 369 progress, err := dbGetPrimarySeedProgress(wt.wallet.dbTx) 370 wt.wallet.mu.RUnlock() 371 if err != nil { 372 t.Fatal("Couldn't fetch primary seed from db") 373 } 374 375 // choose 10 keys in the lookahead and remember them 376 var receivingAddresses []types.UnlockHash 377 for _, sk := range generateKeys(wt.wallet.primarySeed, progress, 10) { 378 sco := types.SiacoinOutput{ 379 UnlockHash: sk.UnlockConditions.UnlockHash(), 380 Value: types.NewCurrency64(1e3), 381 } 382 383 builder.AddSiacoinOutput(sco) 384 payout = payout.Add(sco.Value) 385 receivingAddresses = append(receivingAddresses, sk.UnlockConditions.UnlockHash()) 386 } 387 388 err = builder.FundSiacoins(payout) 389 if err != nil { 390 t.Fatal(err) 391 } 392 393 tSet, err := builder.Sign(true) 394 if err != nil { 395 t.Fatal(err) 396 } 397 398 err = wt.tpool.AcceptTransactionSet(tSet) 399 if err != nil { 400 t.Fatal(err) 401 } 402 403 _, err = wt.miner.AddBlock() 404 if err != nil { 405 t.Fatal(err) 406 } 407 408 // Check if the receiving addresses were moved from future keys to keys 409 wt.wallet.mu.RLock() 410 defer wt.wallet.mu.RUnlock() 411 for _, uh := range receivingAddresses { 412 _, exists := wt.wallet.lookahead[uh] 413 if exists { 414 t.Fatal("UnlockHash still exists in wallet lookahead") 415 } 416 417 _, exists = wt.wallet.keys[uh] 418 if !exists { 419 t.Fatal("UnlockHash not in map of spendable keys") 420 } 421 } 422 } 423 424 // TestAdvanceLookaheadNoRescan tests if a transaction to multiple lookahead addresses 425 // is handled correctly forcing a wallet rescan. 426 func TestAdvanceLookaheadForceRescan(t *testing.T) { 427 if testing.Short() { 428 t.SkipNow() 429 } 430 wt, err := createWalletTester(t.Name(), modules.ProdDependencies) 431 if err != nil { 432 t.Fatal(err) 433 } 434 defer wt.closeWt() 435 436 // Mine blocks without payouts so that the balance stabilizes 437 for i := types.BlockHeight(0); i < types.MaturityDelay; i++ { 438 wt.addBlockNoPayout() 439 } 440 441 // Get the current progress and balance 442 wt.wallet.mu.RLock() 443 progress, err := dbGetPrimarySeedProgress(wt.wallet.dbTx) 444 wt.wallet.mu.RUnlock() 445 if err != nil { 446 t.Fatal("Couldn't fetch primary seed from db") 447 } 448 startBal, _, _, err := wt.wallet.ConfirmedBalance() 449 if err != nil { 450 t.Fatal(err) 451 } 452 453 // Send coins to an address with a high seed index, just outside the 454 // lookahead range. It will not be initially detected, but later the 455 // rescan should find it. 456 highIndex := progress + uint64(len(wt.wallet.lookahead)) + 5 457 farAddr := generateSpendableKey(wt.wallet.primarySeed, highIndex).UnlockConditions.UnlockHash() 458 farPayout := types.SiacoinPrecision.Mul64(8888) 459 460 builder, err := wt.wallet.StartTransaction() 461 if err != nil { 462 t.Fatal(err) 463 } 464 builder.AddSiacoinOutput(types.SiacoinOutput{ 465 UnlockHash: farAddr, 466 Value: farPayout, 467 }) 468 err = builder.FundSiacoins(farPayout) 469 if err != nil { 470 t.Fatal(err) 471 } 472 473 txnSet, err := builder.Sign(true) 474 if err != nil { 475 t.Fatal(err) 476 } 477 478 err = wt.tpool.AcceptTransactionSet(txnSet) 479 if err != nil { 480 t.Fatal(err) 481 } 482 wt.addBlockNoPayout() 483 newBal, _, _, err := wt.wallet.ConfirmedBalance() 484 if err != nil { 485 t.Fatal(err) 486 } 487 if !startBal.Sub(newBal).Equals(farPayout) { 488 t.Fatal("wallet should not recognize coins sent to very high seed index") 489 } 490 491 builder, err = wt.wallet.StartTransaction() 492 if err != nil { 493 t.Fatal(err) 494 } 495 var payout types.Currency 496 497 // choose 10 keys in the lookahead and remember them 498 var receivingAddresses []types.UnlockHash 499 for uh, index := range wt.wallet.lookahead { 500 // Only choose keys that force a rescan 501 if index < progress+lookaheadRescanThreshold { 502 continue 503 } 504 sco := types.SiacoinOutput{ 505 UnlockHash: uh, 506 Value: types.SiacoinPrecision.Mul64(1000), 507 } 508 builder.AddSiacoinOutput(sco) 509 payout = payout.Add(sco.Value) 510 receivingAddresses = append(receivingAddresses, uh) 511 512 if len(receivingAddresses) >= 10 { 513 break 514 } 515 } 516 517 err = builder.FundSiacoins(payout) 518 if err != nil { 519 t.Fatal(err) 520 } 521 522 txnSet, err = builder.Sign(true) 523 if err != nil { 524 t.Fatal(err) 525 } 526 527 err = wt.tpool.AcceptTransactionSet(txnSet) 528 if err != nil { 529 t.Fatal(err) 530 } 531 wt.addBlockNoPayout() 532 533 // Allow the wallet rescan to finish 534 time.Sleep(time.Second * 2) 535 536 // Check that high seed index txn was discovered in the rescan 537 rescanBal, _, _, err := wt.wallet.ConfirmedBalance() 538 if err != nil { 539 t.Fatal(err) 540 } 541 if !rescanBal.Equals(startBal) { 542 t.Fatal("wallet did not discover txn after rescan") 543 } 544 545 // Check if the receiving addresses were moved from future keys to keys 546 wt.wallet.mu.RLock() 547 defer wt.wallet.mu.RUnlock() 548 for _, uh := range receivingAddresses { 549 _, exists := wt.wallet.lookahead[uh] 550 if exists { 551 t.Fatal("UnlockHash still exists in wallet lookahead") 552 } 553 554 _, exists = wt.wallet.keys[uh] 555 if !exists { 556 t.Fatal("UnlockHash not in map of spendable keys") 557 } 558 } 559 } 560 561 // TestDistantWallets tests if two wallets that use the same seed stay 562 // synchronized. 563 func TestDistantWallets(t *testing.T) { 564 if testing.Short() { 565 t.SkipNow() 566 } 567 wt, err := createWalletTester(t.Name(), modules.ProdDependencies) 568 if err != nil { 569 t.Fatal(err) 570 } 571 defer wt.closeWt() 572 573 // Create another wallet with the same seed. 574 w2, err := New(wt.cs, wt.tpool, build.TempDir(modules.WalletDir, t.Name()+"2", modules.WalletDir)) 575 if err != nil { 576 t.Fatal(err) 577 } 578 err = w2.InitFromSeed(nil, wt.wallet.primarySeed) 579 if err != nil { 580 t.Fatal(err) 581 } 582 sk := crypto.NewWalletKey(crypto.HashObject(wt.wallet.primarySeed)) 583 err = w2.Unlock(sk) 584 if err != nil { 585 t.Fatal(err) 586 } 587 588 // Use the first wallet. 589 for i := uint64(0); i < lookaheadBuffer/2; i++ { 590 _, err = wt.wallet.SendSiacoins(types.SiacoinPrecision, types.UnlockHash{}) 591 if err != nil { 592 t.Fatal(err) 593 } 594 wt.addBlockNoPayout() 595 } 596 597 // The second wallet's balance should update accordingly. 598 w1bal, _, _, err := wt.wallet.ConfirmedBalance() 599 if err != nil { 600 t.Fatal(err) 601 } 602 w2bal, _, _, err := w2.ConfirmedBalance() 603 if err != nil { 604 t.Fatal(err) 605 } 606 607 if !w1bal.Equals(w2bal) { 608 t.Fatal("balances do not match:", w1bal, w2bal) 609 } 610 611 // Send coins to an address with a very high seed index, outside the 612 // lookahead range. w2 should not detect it. 613 tbuilder, err := wt.wallet.StartTransaction() 614 if err != nil { 615 t.Fatal(err) 616 } 617 farAddr := generateSpendableKey(wt.wallet.primarySeed, lookaheadBuffer*10).UnlockConditions.UnlockHash() 618 value := types.SiacoinPrecision.Mul64(1e3) 619 tbuilder.AddSiacoinOutput(types.SiacoinOutput{ 620 UnlockHash: farAddr, 621 Value: value, 622 }) 623 err = tbuilder.FundSiacoins(value) 624 if err != nil { 625 t.Fatal(err) 626 } 627 txnSet, err := tbuilder.Sign(true) 628 if err != nil { 629 t.Fatal(err) 630 } 631 err = wt.tpool.AcceptTransactionSet(txnSet) 632 if err != nil { 633 t.Fatal(err) 634 } 635 wt.addBlockNoPayout() 636 637 if newBal, _, _, err := w2.ConfirmedBalance(); !newBal.Equals(w2bal.Sub(value)) { 638 if err != nil { 639 t.Fatal(err) 640 } 641 t.Fatal("wallet should not recognize coins sent to very high seed index") 642 } 643 }