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  }