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 }