github.com/ZuluSpl0it/Sia@v1.3.7/modules/wallet/transactions_test.go (about)

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