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 }