github.com/Synthesix/Sia@v1.3.3-0.20180413141344-f863baeed3ca/modules/wallet/transactions_test.go (about)

     1  package wallet
     2  
     3  import (
     4  	"path/filepath"
     5  	"testing"
     6  
     7  	"github.com/Synthesix/Sia/modules"
     8  	"github.com/Synthesix/Sia/types"
     9  )
    10  
    11  // TestIntegrationTransactions checks that the transaction history is being
    12  // correctly recorded and extended.
    13  func TestIntegrationTransactions(t *testing.T) {
    14  	if testing.Short() {
    15  		t.SkipNow()
    16  	}
    17  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
    18  	if err != nil {
    19  		t.Fatal(err)
    20  	}
    21  	defer wt.closeWt()
    22  
    23  	// Creating the wallet tester results in blocks being mined until the miner
    24  	// has money, which means types.MaturityDelay+1 blocks are created, and
    25  	// each block is going to have a transaction (the miner payout) going to
    26  	// the wallet.
    27  	txns, err := wt.wallet.Transactions(0, 100)
    28  	if err != nil {
    29  		t.Fatal(err)
    30  	}
    31  	if len(txns) != int(types.MaturityDelay+1) {
    32  		t.Error("unexpected transaction history length")
    33  	}
    34  	sentValue := types.NewCurrency64(5000)
    35  	_, err = wt.wallet.SendSiacoins(sentValue, types.UnlockHash{})
    36  	if err != nil {
    37  		t.Fatal(err)
    38  	}
    39  	// No more confirmed transactions have been added.
    40  	txns, err = wt.wallet.Transactions(0, 100)
    41  	if err != nil {
    42  		t.Fatal(err)
    43  	}
    44  	if len(txns) != int(types.MaturityDelay+1) {
    45  		t.Error("unexpected transaction history length")
    46  	}
    47  	// Two transactions added to unconfirmed pool - 1 to fund the exact output,
    48  	// and 1 to hold the exact output.
    49  	if len(wt.wallet.UnconfirmedTransactions()) != 2 {
    50  		t.Error("was expecting 4 unconfirmed transactions")
    51  	}
    52  
    53  	b, _ := wt.miner.FindBlock()
    54  	err = wt.cs.AcceptBlock(b)
    55  	if err != nil {
    56  		t.Fatal(err)
    57  	}
    58  	// A confirmed transaction was added for the miner payout, and the 2
    59  	// transactions that were previously unconfirmed.
    60  	txns, err = wt.wallet.Transactions(0, 100)
    61  	if err != nil {
    62  		t.Fatal(err)
    63  	}
    64  	if len(txns) != int(types.MaturityDelay+2+2) {
    65  		t.Errorf("unexpected transaction history length: expected %v, got %v", types.MaturityDelay+2+2, len(txns))
    66  	}
    67  
    68  	// Try getting a partial history for just the previous block.
    69  	txns, err = wt.wallet.Transactions(types.MaturityDelay+2, types.MaturityDelay+2)
    70  	if err != nil {
    71  		t.Fatal(err)
    72  	}
    73  	// The partial should include one transaction for a block, and 2 for the
    74  	// send that occurred.
    75  	if len(txns) != 3 {
    76  		t.Errorf("unexpected transaction history length: expected %v, got %v", 3, len(txns))
    77  	}
    78  }
    79  
    80  // TestTransactionsSingleTxn checks if it is possible to find a txn that was
    81  // appended to the processed transactions and is also the only txn for a
    82  // certain block height.
    83  func TestTransactionsSingleTxn(t *testing.T) {
    84  	if testing.Short() {
    85  		t.SkipNow()
    86  	}
    87  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
    88  	if err != nil {
    89  		t.Fatal(err)
    90  	}
    91  	defer wt.closeWt()
    92  
    93  	// Creating the wallet tester results in blocks being mined until the miner
    94  	// has money, which means types.MaturityDelay+1 blocks are created, and
    95  	// each block is going to have a transaction (the miner payout) going to
    96  	// the wallet.
    97  	txns, err := wt.wallet.Transactions(0, 100)
    98  	if err != nil {
    99  		t.Fatal(err)
   100  	}
   101  	if len(txns) != int(types.MaturityDelay+1) {
   102  		t.Error("unexpected transaction history length")
   103  	}
   104  
   105  	// Create a processed txn for a future block height. It whould be the last
   106  	// txn in the database and the only txn with that height.
   107  	height := wt.cs.Height() + 1
   108  	pt := modules.ProcessedTransaction{
   109  		ConfirmationHeight: height,
   110  	}
   111  	wt.wallet.mu.Lock()
   112  	if err := dbAppendProcessedTransaction(wt.wallet.dbTx, pt); err != nil {
   113  		t.Fatal(err)
   114  	}
   115  
   116  	// Set the consensus height to height. Otherwise Transactions will return
   117  	// an error. We can't just mine a block since that would create new
   118  	// transactions.
   119  	if err := dbPutConsensusHeight(wt.wallet.dbTx, height); err != nil {
   120  		t.Fatal(err)
   121  	}
   122  	wt.wallet.mu.Unlock()
   123  
   124  	// Search for the previously appended txn
   125  	txns, err = wt.wallet.Transactions(height, height)
   126  	if err != nil {
   127  		t.Fatal(err)
   128  	}
   129  
   130  	// We should find exactly 1 txn
   131  	if len(txns) != 1 {
   132  		t.Errorf("Found %v txns but should be 1", len(txns))
   133  	}
   134  }
   135  
   136  // TestIntegrationTransaction checks that individually queried transactions
   137  // contain the correct values.
   138  func TestIntegrationTransaction(t *testing.T) {
   139  	if testing.Short() {
   140  		t.SkipNow()
   141  	}
   142  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
   143  	if err != nil {
   144  		t.Fatal(err)
   145  	}
   146  	defer wt.closeWt()
   147  
   148  	_, exists := wt.wallet.Transaction(types.TransactionID{})
   149  	if exists {
   150  		t.Error("able to query a nonexisting transction")
   151  	}
   152  
   153  	// test sending siacoins
   154  	sentValue := types.NewCurrency64(5000)
   155  	sendTxns, err := wt.wallet.SendSiacoins(sentValue, types.UnlockHash{})
   156  	if err != nil {
   157  		t.Fatal(err)
   158  	}
   159  	_, err = wt.miner.AddBlock()
   160  	if err != nil {
   161  		t.Fatal(err)
   162  	}
   163  
   164  	// sendTxns[0] is the set-up transaction, sendTxns[1] contains the sentValue output
   165  	txn, exists := wt.wallet.Transaction(sendTxns[1].ID())
   166  	if !exists {
   167  		t.Fatal("unable to query transaction")
   168  	}
   169  	if txn.TransactionID != sendTxns[1].ID() {
   170  		t.Error("wrong transaction was fetched")
   171  	} else if len(txn.Inputs) != 1 || len(txn.Outputs) != 2 {
   172  		t.Error("expected 1 input and 2 outputs, got", len(txn.Inputs), len(txn.Outputs))
   173  	} else if !txn.Outputs[0].Value.Equals(sentValue) {
   174  		t.Errorf("expected first output to equal %v, got %v", sentValue, txn.Outputs[0].Value)
   175  	} else if exp := txn.Inputs[0].Value.Sub(sentValue); !txn.Outputs[1].Value.Equals(exp) {
   176  		t.Errorf("expected first output to equal %v, got %v", exp, txn.Outputs[1].Value)
   177  	}
   178  
   179  	// test sending siafunds
   180  	err = wt.wallet.LoadSiagKeys(wt.walletMasterKey, []string{"../../types/siag0of1of1.siakey"})
   181  	if err != nil {
   182  		t.Error(err)
   183  	}
   184  	sentValue = types.NewCurrency64(12)
   185  	sendTxns, err = wt.wallet.SendSiafunds(sentValue, types.UnlockHash{})
   186  	if err != nil {
   187  		t.Fatal(err)
   188  	}
   189  	_, err = wt.miner.AddBlock()
   190  	if err != nil {
   191  		t.Fatal(err)
   192  	}
   193  
   194  	txn, exists = wt.wallet.Transaction(sendTxns[1].ID())
   195  	if !exists {
   196  		t.Fatal("unable to query transaction")
   197  	}
   198  	if len(txn.Inputs) != 1 || len(txn.Outputs) != 3 {
   199  		t.Error("expected 1 input and 3 outputs, got", len(txn.Inputs), len(txn.Outputs))
   200  	} else if !txn.Outputs[1].Value.Equals(sentValue) {
   201  		t.Errorf("expected first output to equal %v, got %v", sentValue, txn.Outputs[1].Value)
   202  	} else if exp := txn.Inputs[0].Value.Sub(sentValue); !txn.Outputs[2].Value.Equals(exp) {
   203  		t.Errorf("expected first output to equal %v, got %v", exp, txn.Outputs[2].Value)
   204  	}
   205  }
   206  
   207  // TestProcessedTxnIndexCompatCode checks if the compatibility code for the
   208  // bucketProcessedTxnIndex works as expected
   209  func TestProcessedTxnIndexCompatCode(t *testing.T) {
   210  	if testing.Short() {
   211  		t.SkipNow()
   212  	}
   213  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
   214  	if err != nil {
   215  		t.Fatal(err)
   216  	}
   217  	defer wt.closeWt()
   218  
   219  	// Mine blocks to get lots of processed transactions
   220  	for i := 0; i < 1000; i++ {
   221  		if _, err := wt.miner.AddBlock(); err != nil {
   222  			t.Fatal(err)
   223  		}
   224  	}
   225  
   226  	// The wallet tester mined blocks. Therefore the bucket shouldn't be
   227  	// empty.
   228  	wt.wallet.mu.Lock()
   229  	wt.wallet.syncDB()
   230  	expectedTxns := wt.wallet.dbTx.Bucket(bucketProcessedTxnIndex).Stats().KeyN
   231  	if expectedTxns == 0 {
   232  		t.Fatal("bucketProcessedTxnIndex shouldn't be empty")
   233  	}
   234  
   235  	// Delete the bucket
   236  	if err := wt.wallet.dbTx.DeleteBucket(bucketProcessedTxnIndex); err != nil {
   237  		t.Fatalf("Failed to empty bucket: %v", err)
   238  	}
   239  
   240  	// Bucket shouldn't exist
   241  	if wt.wallet.dbTx.Bucket(bucketProcessedTxnIndex) != nil {
   242  		t.Fatal("bucketProcessedTxnIndex should be empty")
   243  	}
   244  	wt.wallet.mu.Unlock()
   245  
   246  	// Close the wallet
   247  	if err := wt.wallet.Close(); err != nil {
   248  		t.Fatalf("Failed to close wallet: %v", err)
   249  	}
   250  
   251  	// Restart wallet
   252  	wallet, err := New(wt.cs, wt.tpool, filepath.Join(wt.persistDir, modules.WalletDir))
   253  	if err != nil {
   254  		t.Fatalf("Failed to restart wallet: %v", err)
   255  	}
   256  	wt.wallet = wallet
   257  
   258  	// Bucket should exist
   259  	wt.wallet.mu.Lock()
   260  	defer wt.wallet.mu.Unlock()
   261  	if wt.wallet.dbTx.Bucket(bucketProcessedTxnIndex) == nil {
   262  		t.Fatal("bucketProcessedTxnIndex should exist")
   263  	}
   264  
   265  	// Check if bucket has expected size
   266  	wt.wallet.syncDB()
   267  	numTxns := wt.wallet.dbTx.Bucket(bucketProcessedTxnIndex).Stats().KeyN
   268  	if expectedTxns != numTxns {
   269  		t.Errorf("Bucket should have %v entries but had %v", expectedTxns, numTxns)
   270  	}
   271  }
   272  
   273  // TestIntegrationAddressTransactions checks grabbing the history for a single
   274  // address.
   275  func TestIntegrationAddressTransactions(t *testing.T) {
   276  	if testing.Short() {
   277  		t.SkipNow()
   278  	}
   279  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
   280  	if err != nil {
   281  		t.Fatal(err)
   282  	}
   283  	defer wt.closeWt()
   284  
   285  	// Grab an address and send it money.
   286  	uc, err := wt.wallet.NextAddress()
   287  	addr := uc.UnlockHash()
   288  	if err != nil {
   289  		t.Fatal(err)
   290  	}
   291  	_, err = wt.wallet.SendSiacoins(types.NewCurrency64(5005), addr)
   292  	if err != nil {
   293  		t.Fatal(err)
   294  	}
   295  
   296  	// Check the confirmed balance of the address.
   297  	addrHist := wt.wallet.AddressTransactions(addr)
   298  	if len(addrHist) != 0 {
   299  		t.Error("address should be empty - no confirmed transactions")
   300  	}
   301  	if len(wt.wallet.AddressUnconfirmedTransactions(addr)) == 0 {
   302  		t.Error("addresses unconfirmed transactions should not be empty")
   303  	}
   304  	b, _ := wt.miner.FindBlock()
   305  	err = wt.cs.AcceptBlock(b)
   306  	if err != nil {
   307  		t.Fatal(err)
   308  	}
   309  	addrHist = wt.wallet.AddressTransactions(addr)
   310  	if len(addrHist) == 0 {
   311  		t.Error("address history should have some transactions")
   312  	}
   313  	if len(wt.wallet.AddressUnconfirmedTransactions(addr)) != 0 {
   314  		t.Error("addresses unconfirmed transactions should be empty")
   315  	}
   316  }
   317  
   318  // TestAddressTransactionRevertedBlock checks grabbing the history for a
   319  // address after its block was reverted
   320  func TestAddressTransactionRevertedBlock(t *testing.T) {
   321  	if testing.Short() {
   322  		t.SkipNow()
   323  	}
   324  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
   325  	if err != nil {
   326  		t.Fatal(err)
   327  	}
   328  	defer wt.closeWt()
   329  
   330  	// Grab an address and send it money.
   331  	uc, err := wt.wallet.NextAddress()
   332  	addr := uc.UnlockHash()
   333  	if err != nil {
   334  		t.Fatal(err)
   335  	}
   336  	_, err = wt.wallet.SendSiacoins(types.NewCurrency64(5005), addr)
   337  	if err != nil {
   338  		t.Fatal(err)
   339  	}
   340  
   341  	b, _ := wt.miner.FindBlock()
   342  	err = wt.cs.AcceptBlock(b)
   343  	if err != nil {
   344  		t.Fatal(err)
   345  	}
   346  
   347  	addrHist := wt.wallet.AddressTransactions(addr)
   348  	if len(addrHist) == 0 {
   349  		t.Error("address history should have some transactions")
   350  	}
   351  	if len(wt.wallet.AddressUnconfirmedTransactions(addr)) != 0 {
   352  		t.Error("addresses unconfirmed transactions should be empty")
   353  	}
   354  
   355  	// Revert the block
   356  	wt.wallet.mu.Lock()
   357  	if err := wt.wallet.revertHistory(wt.wallet.dbTx, []types.Block{b}); err != nil {
   358  		t.Fatal(err)
   359  	}
   360  	wt.wallet.mu.Unlock()
   361  
   362  	addrHist = wt.wallet.AddressTransactions(addr)
   363  	if len(addrHist) > 0 {
   364  		t.Error("address history should should be empty")
   365  	}
   366  	if len(wt.wallet.AddressUnconfirmedTransactions(addr)) > 0 {
   367  		t.Error("addresses unconfirmed transactions should have some transactions")
   368  	}
   369  }
   370  
   371  // TestTransactionInputOutputIDs verifies that ProcessedTransaction's inputs
   372  // and outputs have a valid ID field.
   373  func TestTransactionInputOutputIDs(t *testing.T) {
   374  	if testing.Short() {
   375  		t.SkipNow()
   376  	}
   377  	wt, err := createWalletTester(t.Name(), modules.ProdDependencies)
   378  	if err != nil {
   379  		t.Fatal(err)
   380  	}
   381  	defer wt.closeWt()
   382  
   383  	// mine a few blocks to create miner payouts
   384  	for i := 0; i < 5; i++ {
   385  		_, err = wt.miner.AddBlock()
   386  		if err != nil {
   387  			t.Fatal(err)
   388  		}
   389  	}
   390  
   391  	// create some siacoin outputs
   392  	uc, err := wt.wallet.NextAddress()
   393  	addr := uc.UnlockHash()
   394  	if err != nil {
   395  		t.Fatal(err)
   396  	}
   397  	_, err = wt.wallet.SendSiacoins(types.NewCurrency64(5005), addr)
   398  	if err != nil {
   399  		t.Fatal(err)
   400  	}
   401  	_, err = wt.miner.AddBlock()
   402  	if err != nil {
   403  		t.Fatal(err)
   404  	}
   405  
   406  	// verify the miner payouts and siacoin outputs/inputs have correct IDs
   407  	txns, err := wt.wallet.Transactions(0, 1000)
   408  	if err != nil {
   409  		t.Fatal(err)
   410  	}
   411  
   412  	outputIDs := make(map[types.OutputID]struct{})
   413  	for _, txn := range txns {
   414  		block, _ := wt.cs.BlockAtHeight(txn.ConfirmationHeight)
   415  		for i, output := range txn.Outputs {
   416  			outputIDs[output.ID] = struct{}{}
   417  			if output.FundType == types.SpecifierMinerPayout {
   418  				if output.ID != types.OutputID(block.MinerPayoutID(uint64(i))) {
   419  					t.Fatal("miner payout had incorrect output ID")
   420  				}
   421  			}
   422  			if output.FundType == types.SpecifierSiacoinOutput {
   423  				if output.ID != types.OutputID(txn.Transaction.SiacoinOutputID(uint64(i))) {
   424  					t.Fatal("siacoin output had incorrect output ID")
   425  				}
   426  			}
   427  		}
   428  		for _, input := range txn.Inputs {
   429  			if _, exists := outputIDs[input.ParentID]; !exists {
   430  				t.Fatal("input has ParentID that points to a nonexistent output:", input.ParentID)
   431  			}
   432  		}
   433  	}
   434  }
   435  
   436  // BenchmarkAddressTransactions benchmarks the AddressTransactions method,
   437  // using the near-worst-case scenario of 10,000 transactions to search through
   438  // with only a single relevant transaction.
   439  func BenchmarkAddressTransactions(b *testing.B) {
   440  	wt, err := createWalletTester(b.Name(), modules.ProdDependencies)
   441  	if err != nil {
   442  		b.Fatal(err)
   443  	}
   444  	// add a bunch of fake transactions to the db
   445  	//
   446  	// NOTE: this is somewhat brittle, but the alternative (generating
   447  	// authentic transactions) is prohibitively slow.
   448  	wt.wallet.mu.Lock()
   449  	for i := 0; i < 10000; i++ {
   450  		err := dbAppendProcessedTransaction(wt.wallet.dbTx, modules.ProcessedTransaction{
   451  			TransactionID: types.TransactionID{1},
   452  		})
   453  		if err != nil {
   454  			b.Fatal(err)
   455  		}
   456  	}
   457  	// add a single relevant transaction
   458  	searchAddr := types.UnlockHash{1}
   459  	err = dbAppendProcessedTransaction(wt.wallet.dbTx, modules.ProcessedTransaction{
   460  		TransactionID: types.TransactionID{1},
   461  		Inputs: []modules.ProcessedInput{{
   462  			RelatedAddress: searchAddr,
   463  		}},
   464  	})
   465  	if err != nil {
   466  		b.Fatal(err)
   467  	}
   468  	wt.wallet.syncDB()
   469  	wt.wallet.mu.Unlock()
   470  
   471  	b.ResetTimer()
   472  	b.Run("indexed", func(b *testing.B) {
   473  		for i := 0; i < b.N; i++ {
   474  			txns := wt.wallet.AddressTransactions(searchAddr)
   475  			if len(txns) != 1 {
   476  				b.Fatal(len(txns))
   477  			}
   478  		}
   479  	})
   480  	b.Run("indexed-nosync", func(b *testing.B) {
   481  		wt.wallet.db.NoSync = true
   482  		for i := 0; i < b.N; i++ {
   483  			txns := wt.wallet.AddressTransactions(searchAddr)
   484  			if len(txns) != 1 {
   485  				b.Fatal(len(txns))
   486  			}
   487  		}
   488  		wt.wallet.db.NoSync = false
   489  	})
   490  	b.Run("unindexed", func(b *testing.B) {
   491  		for i := 0; i < b.N; i++ {
   492  			wt.wallet.mu.Lock()
   493  			wt.wallet.syncDB()
   494  			var pts []modules.ProcessedTransaction
   495  			it := dbProcessedTransactionsIterator(wt.wallet.dbTx)
   496  			for it.next() {
   497  				pt := it.value()
   498  				relevant := false
   499  				for _, input := range pt.Inputs {
   500  					relevant = relevant || input.RelatedAddress == searchAddr
   501  				}
   502  				for _, output := range pt.Outputs {
   503  					relevant = relevant || output.RelatedAddress == searchAddr
   504  				}
   505  				if relevant {
   506  					pts = append(pts, pt)
   507  				}
   508  			}
   509  			_ = pts
   510  			wt.wallet.mu.Unlock()
   511  		}
   512  	})
   513  }