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 }