gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/wallet/transactions_test.go (about)

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