gitlab.com/jokerrs1/Sia@v1.3.2/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 := newWallet(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 testdir := build.TempDir(modules.WalletDir, t.Name()) 146 g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir)) 147 if err != nil { 148 t.Fatal(err) 149 } 150 cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir)) 151 if err != nil { 152 t.Fatal(err) 153 } 154 tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir)) 155 if err != nil { 156 t.Fatal(err) 157 } 158 159 wdir := filepath.Join(testdir, modules.WalletDir) 160 _, err = New(cs, nil, wdir) 161 if err != errNilTpool { 162 t.Error(err) 163 } 164 _, err = New(nil, tp, wdir) 165 if err != errNilConsensusSet { 166 t.Error(err) 167 } 168 _, err = New(nil, nil, wdir) 169 if err != errNilConsensusSet { 170 t.Error(err) 171 } 172 } 173 174 // TestAllAddresses checks that AllAddresses returns all of the wallet's 175 // addresses in sorted order. 176 func TestAllAddresses(t *testing.T) { 177 wt, err := createBlankWalletTester(t.Name()) 178 if err != nil { 179 t.Fatal(err) 180 } 181 defer wt.closeWt() 182 183 wt.wallet.keys[types.UnlockHash{1}] = spendableKey{} 184 wt.wallet.keys[types.UnlockHash{5}] = spendableKey{} 185 wt.wallet.keys[types.UnlockHash{0}] = spendableKey{} 186 wt.wallet.keys[types.UnlockHash{2}] = spendableKey{} 187 wt.wallet.keys[types.UnlockHash{4}] = spendableKey{} 188 wt.wallet.keys[types.UnlockHash{3}] = spendableKey{} 189 addrs := wt.wallet.AllAddresses() 190 for i := range addrs { 191 if addrs[i][0] != byte(i) { 192 t.Error("address sorting failed:", i, addrs[i][0]) 193 } 194 } 195 } 196 197 // TestCloseWallet tries to close the wallet. 198 func TestCloseWallet(t *testing.T) { 199 if testing.Short() { 200 t.Skip() 201 } 202 testdir := build.TempDir(modules.WalletDir, t.Name()) 203 g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir)) 204 if err != nil { 205 t.Fatal(err) 206 } 207 cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir)) 208 if err != nil { 209 t.Fatal(err) 210 } 211 tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir)) 212 if err != nil { 213 t.Fatal(err) 214 } 215 wdir := filepath.Join(testdir, modules.WalletDir) 216 w, err := New(cs, tp, wdir) 217 if err != nil { 218 t.Fatal(err) 219 } 220 if err := w.Close(); err != nil { 221 t.Fatal(err) 222 } 223 } 224 225 // TestRescanning verifies that calling Rescanning during a scan operation 226 // returns true, and false otherwise. 227 func TestRescanning(t *testing.T) { 228 if testing.Short() { 229 t.SkipNow() 230 } 231 wt, err := createWalletTester(t.Name(), &modules.ProductionDependencies{}) 232 if err != nil { 233 t.Fatal(err) 234 } 235 defer wt.closeWt() 236 237 // A fresh wallet should not be rescanning. 238 if wt.wallet.Rescanning() { 239 t.Fatal("fresh wallet should not report that a scan is underway") 240 } 241 242 // lock the wallet 243 wt.wallet.Lock() 244 245 // spawn an unlock goroutine 246 errChan := make(chan error) 247 go func() { 248 // acquire the write lock so that Unlock acquires the trymutex, but 249 // cannot proceed further 250 wt.wallet.mu.Lock() 251 errChan <- wt.wallet.Unlock(wt.walletMasterKey) 252 }() 253 254 // wait for goroutine to start, after which Rescanning should return true 255 time.Sleep(time.Millisecond * 10) 256 if !wt.wallet.Rescanning() { 257 t.Fatal("wallet should report that a scan is underway") 258 } 259 260 // release the mutex and allow the call to complete 261 wt.wallet.mu.Unlock() 262 if err := <-errChan; err != nil { 263 t.Fatal("unlock failed:", err) 264 } 265 266 // Rescanning should now return false again 267 if wt.wallet.Rescanning() { 268 t.Fatal("wallet should not report that a scan is underway") 269 } 270 } 271 272 // TestFutureAddressGeneration checks if the right amount of future addresses 273 // is generated after calling NextAddress() or locking + unlocking the wallet. 274 func TestLookaheadGeneration(t *testing.T) { 275 if testing.Short() { 276 t.SkipNow() 277 } 278 wt, err := createWalletTester(t.Name(), &modules.ProductionDependencies{}) 279 if err != nil { 280 t.Fatal(err) 281 } 282 defer wt.closeWt() 283 284 // Check if number of future keys is correct 285 wt.wallet.mu.RLock() 286 progress, err := dbGetPrimarySeedProgress(wt.wallet.dbTx) 287 wt.wallet.mu.RUnlock() 288 if err != nil { 289 t.Fatal("Couldn't fetch primary seed from db") 290 } 291 292 actualKeys := uint64(len(wt.wallet.lookahead)) 293 expectedKeys := maxLookahead(progress) 294 if actualKeys != expectedKeys { 295 t.Errorf("expected len(lookahead) == %d but was %d", actualKeys, expectedKeys) 296 } 297 298 // Generate some more keys 299 for i := 0; i < 100; i++ { 300 wt.wallet.NextAddress() 301 } 302 303 // Lock and unlock 304 wt.wallet.Lock() 305 wt.wallet.Unlock(wt.walletMasterKey) 306 307 wt.wallet.mu.RLock() 308 progress, err = dbGetPrimarySeedProgress(wt.wallet.dbTx) 309 wt.wallet.mu.RUnlock() 310 if err != nil { 311 t.Fatal("Couldn't fetch primary seed from db") 312 } 313 314 actualKeys = uint64(len(wt.wallet.lookahead)) 315 expectedKeys = maxLookahead(progress) 316 if actualKeys != expectedKeys { 317 t.Errorf("expected len(lookahead) == %d but was %d", actualKeys, expectedKeys) 318 } 319 320 wt.wallet.mu.RLock() 321 defer wt.wallet.mu.RUnlock() 322 for i := range wt.wallet.keys { 323 _, exists := wt.wallet.lookahead[i] 324 if exists { 325 t.Fatal("wallet keys contained a key which is also present in lookahead") 326 } 327 } 328 } 329 330 // TestAdvanceLookaheadNoRescan tests if a transaction to multiple lookahead addresses 331 // is handled correctly without forcing a wallet rescan. 332 func TestAdvanceLookaheadNoRescan(t *testing.T) { 333 if testing.Short() { 334 t.SkipNow() 335 } 336 wt, err := createWalletTester(t.Name(), &modules.ProductionDependencies{}) 337 if err != nil { 338 t.Fatal(err) 339 } 340 defer wt.closeWt() 341 342 builder := wt.wallet.StartTransaction() 343 payout := types.ZeroCurrency 344 345 // Get the current progress 346 wt.wallet.mu.RLock() 347 progress, err := dbGetPrimarySeedProgress(wt.wallet.dbTx) 348 wt.wallet.mu.RUnlock() 349 if err != nil { 350 t.Fatal("Couldn't fetch primary seed from db") 351 } 352 353 // choose 10 keys in the lookahead and remember them 354 var receivingAddresses []types.UnlockHash 355 for _, sk := range generateKeys(wt.wallet.primarySeed, progress, 10) { 356 sco := types.SiacoinOutput{ 357 UnlockHash: sk.UnlockConditions.UnlockHash(), 358 Value: types.NewCurrency64(1e3), 359 } 360 361 builder.AddSiacoinOutput(sco) 362 payout = payout.Add(sco.Value) 363 receivingAddresses = append(receivingAddresses, sk.UnlockConditions.UnlockHash()) 364 } 365 366 err = builder.FundSiacoins(payout) 367 if err != nil { 368 t.Fatal(err) 369 } 370 371 tSet, err := builder.Sign(true) 372 if err != nil { 373 t.Fatal(err) 374 } 375 376 err = wt.tpool.AcceptTransactionSet(tSet) 377 if err != nil { 378 t.Fatal(err) 379 } 380 381 _, err = wt.miner.AddBlock() 382 if err != nil { 383 t.Fatal(err) 384 } 385 386 // Check if the receiving addresses were moved from future keys to keys 387 wt.wallet.mu.RLock() 388 defer wt.wallet.mu.RUnlock() 389 for _, uh := range receivingAddresses { 390 _, exists := wt.wallet.lookahead[uh] 391 if exists { 392 t.Fatal("UnlockHash still exists in wallet lookahead") 393 } 394 395 _, exists = wt.wallet.keys[uh] 396 if !exists { 397 t.Fatal("UnlockHash not in map of spendable keys") 398 } 399 } 400 } 401 402 // TestAdvanceLookaheadNoRescan tests if a transaction to multiple lookahead addresses 403 // is handled correctly forcing a wallet rescan. 404 func TestAdvanceLookaheadForceRescan(t *testing.T) { 405 if testing.Short() { 406 t.SkipNow() 407 } 408 wt, err := createWalletTester(t.Name(), &modules.ProductionDependencies{}) 409 if err != nil { 410 t.Fatal(err) 411 } 412 defer wt.closeWt() 413 414 // Mine blocks without payouts so that the balance stabilizes 415 for i := types.BlockHeight(0); i < types.MaturityDelay; i++ { 416 wt.addBlockNoPayout() 417 } 418 419 // Get the current progress and balance 420 wt.wallet.mu.RLock() 421 progress, err := dbGetPrimarySeedProgress(wt.wallet.dbTx) 422 wt.wallet.mu.RUnlock() 423 if err != nil { 424 t.Fatal("Couldn't fetch primary seed from db") 425 } 426 startBal, _, _ := wt.wallet.ConfirmedBalance() 427 428 // Send coins to an address with a high seed index, just outside the 429 // lookahead range. It will not be initially detected, but later the 430 // rescan should find it. 431 highIndex := progress + uint64(len(wt.wallet.lookahead)) + 5 432 farAddr := generateSpendableKey(wt.wallet.primarySeed, highIndex).UnlockConditions.UnlockHash() 433 farPayout := types.SiacoinPrecision.Mul64(8888) 434 435 builder := wt.wallet.StartTransaction() 436 builder.AddSiacoinOutput(types.SiacoinOutput{ 437 UnlockHash: farAddr, 438 Value: farPayout, 439 }) 440 err = builder.FundSiacoins(farPayout) 441 if err != nil { 442 t.Fatal(err) 443 } 444 445 txnSet, err := builder.Sign(true) 446 if err != nil { 447 t.Fatal(err) 448 } 449 450 err = wt.tpool.AcceptTransactionSet(txnSet) 451 if err != nil { 452 t.Fatal(err) 453 } 454 wt.addBlockNoPayout() 455 newBal, _, _ := wt.wallet.ConfirmedBalance() 456 if !startBal.Sub(newBal).Equals(farPayout) { 457 t.Fatal("wallet should not recognize coins sent to very high seed index") 458 } 459 460 builder = wt.wallet.StartTransaction() 461 var payout types.Currency 462 463 // choose 10 keys in the lookahead and remember them 464 var receivingAddresses []types.UnlockHash 465 for uh, index := range wt.wallet.lookahead { 466 // Only choose keys that force a rescan 467 if index < progress+lookaheadRescanThreshold { 468 continue 469 } 470 sco := types.SiacoinOutput{ 471 UnlockHash: uh, 472 Value: types.SiacoinPrecision.Mul64(1000), 473 } 474 builder.AddSiacoinOutput(sco) 475 payout = payout.Add(sco.Value) 476 receivingAddresses = append(receivingAddresses, uh) 477 478 if len(receivingAddresses) >= 10 { 479 break 480 } 481 } 482 483 err = builder.FundSiacoins(payout) 484 if err != nil { 485 t.Fatal(err) 486 } 487 488 txnSet, err = builder.Sign(true) 489 if err != nil { 490 t.Fatal(err) 491 } 492 493 err = wt.tpool.AcceptTransactionSet(txnSet) 494 if err != nil { 495 t.Fatal(err) 496 } 497 wt.addBlockNoPayout() 498 499 // Allow the wallet rescan to finish 500 time.Sleep(time.Second * 2) 501 502 // Check that high seed index txn was discovered in the rescan 503 rescanBal, _, _ := wt.wallet.ConfirmedBalance() 504 if !rescanBal.Equals(startBal) { 505 t.Fatal("wallet did not discover txn after rescan") 506 } 507 508 // Check if the receiving addresses were moved from future keys to keys 509 wt.wallet.mu.RLock() 510 defer wt.wallet.mu.RUnlock() 511 for _, uh := range receivingAddresses { 512 _, exists := wt.wallet.lookahead[uh] 513 if exists { 514 t.Fatal("UnlockHash still exists in wallet lookahead") 515 } 516 517 _, exists = wt.wallet.keys[uh] 518 if !exists { 519 t.Fatal("UnlockHash not in map of spendable keys") 520 } 521 } 522 } 523 524 // TestDistantWallets tests if two wallets that use the same seed stay 525 // synchronized. 526 func TestDistantWallets(t *testing.T) { 527 if testing.Short() { 528 t.SkipNow() 529 } 530 wt, err := createWalletTester(t.Name(), &modules.ProductionDependencies{}) 531 if err != nil { 532 t.Fatal(err) 533 } 534 defer wt.closeWt() 535 536 // Create another wallet with the same seed. 537 w2, err := New(wt.cs, wt.tpool, build.TempDir(modules.WalletDir, t.Name()+"2", modules.WalletDir)) 538 if err != nil { 539 t.Fatal(err) 540 } 541 err = w2.InitFromSeed(crypto.TwofishKey{}, wt.wallet.primarySeed) 542 if err != nil { 543 t.Fatal(err) 544 } 545 err = w2.Unlock(crypto.TwofishKey(crypto.HashObject(wt.wallet.primarySeed))) 546 if err != nil { 547 t.Fatal(err) 548 } 549 550 // Use the first wallet. 551 for i := uint64(0); i < lookaheadBuffer/2; i++ { 552 _, err = wt.wallet.SendSiacoins(types.SiacoinPrecision, types.UnlockHash{}) 553 if err != nil { 554 t.Fatal(err) 555 } 556 wt.addBlockNoPayout() 557 } 558 559 // The second wallet's balance should update accordingly. 560 w1bal, _, _ := wt.wallet.ConfirmedBalance() 561 w2bal, _, _ := w2.ConfirmedBalance() 562 563 if !w1bal.Equals(w2bal) { 564 t.Fatal("balances do not match:", w1bal, w2bal) 565 } 566 567 // Send coins to an address with a very high seed index, outside the 568 // lookahead range. w2 should not detect it. 569 tbuilder := wt.wallet.StartTransaction() 570 farAddr := generateSpendableKey(wt.wallet.primarySeed, lookaheadBuffer*10).UnlockConditions.UnlockHash() 571 value := types.SiacoinPrecision.Mul64(1e3) 572 tbuilder.AddSiacoinOutput(types.SiacoinOutput{ 573 UnlockHash: farAddr, 574 Value: value, 575 }) 576 err = tbuilder.FundSiacoins(value) 577 if err != nil { 578 t.Fatal(err) 579 } 580 txnSet, err := tbuilder.Sign(true) 581 if err != nil { 582 t.Fatal(err) 583 } 584 err = wt.tpool.AcceptTransactionSet(txnSet) 585 if err != nil { 586 t.Fatal(err) 587 } 588 wt.addBlockNoPayout() 589 590 if newBal, _, _ := w2.ConfirmedBalance(); !newBal.Equals(w2bal.Sub(value)) { 591 t.Fatal("wallet should not recognize coins sent to very high seed index") 592 } 593 }