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