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  }