decred.org/dcrwallet/v3@v3.1.0/wallet/udb/tx_test.go (about) 1 // Copyright (c) 2013-2015 The btcsuite developers 2 // Copyright (c) 2019 The Decred developers 3 // Use of this source code is governed by an ISC 4 // license that can be found in the LICENSE file. 5 6 package udb 7 8 import ( 9 "context" 10 "testing" 11 "time" 12 13 _ "decred.org/dcrwallet/v3/wallet/drivers/bdb" 14 "decred.org/dcrwallet/v3/wallet/walletdb" 15 "github.com/decred/dcrd/chaincfg/chainhash" 16 "github.com/decred/dcrd/dcrutil/v4" 17 "github.com/decred/dcrd/wire" 18 ) 19 20 func TestInsertsCreditsDebitsRollbacks(t *testing.T) { 21 ctx := context.Background() 22 db, _, s, _, teardown, err := cloneDB(ctx, "inserts_credits_debits_rollbacks.kv") 23 defer teardown() 24 if err != nil { 25 t.Fatal(err) 26 } 27 28 g := makeBlockGenerator() 29 b1H := g.generate(dcrutil.BlockValid) 30 b1Hash := b1H.BlockHash() 31 b2H := g.generate(dcrutil.BlockValid) 32 b2Hash := b2H.BlockHash() 33 b3H := g.generate(dcrutil.BlockValid) 34 headerData := makeHeaderDataSlice(b1H, b2H, b3H) 35 filters := emptyFilters(3) 36 37 tx1 := wire.MsgTx{TxOut: []*wire.TxOut{{Value: 2e8}}} 38 tx1Rec, err := NewTxRecordFromMsgTx(&tx1, time.Time{}) 39 if err != nil { 40 t.Fatal(err) 41 } 42 43 sTx1 := wire.MsgTx{ 44 TxIn: []*wire.TxIn{{ 45 PreviousOutPoint: wire.OutPoint{ 46 Hash: tx1.TxHash(), 47 Index: 0, 48 Tree: wire.TxTreeRegular, 49 }, 50 ValueIn: tx1Rec.MsgTx.TxOut[0].Value, 51 BlockHeight: b2H.Height, 52 BlockIndex: 0, 53 }}, 54 TxOut: []*wire.TxOut{{Value: tx1Rec.MsgTx.TxOut[0].Value}}, 55 } 56 sTx1Rec, err := NewTxRecordFromMsgTx(&sTx1, time.Time{}) 57 if err != nil { 58 t.Fatal(err) 59 } 60 61 tx2 := wire.MsgTx{TxOut: []*wire.TxOut{{Value: 3e8}}} 62 tx2Rec, err := NewTxRecordFromMsgTx(&tx2, time.Time{}) 63 if err != nil { 64 t.Fatal(err) 65 } 66 67 sTx2 := wire.MsgTx{ 68 TxIn: []*wire.TxIn{{ 69 PreviousOutPoint: wire.OutPoint{ 70 Hash: tx2.TxHash(), 71 Index: 0, 72 Tree: wire.TxTreeRegular, 73 }, 74 ValueIn: tx2Rec.MsgTx.TxOut[0].Value, 75 BlockHeight: b3H.Height, 76 BlockIndex: 0, 77 }}, 78 TxOut: []*wire.TxOut{{Value: tx2Rec.MsgTx.TxOut[0].Value}}, 79 } 80 sTx2Rec, err := NewTxRecordFromMsgTx(&sTx2, time.Time{}) 81 if err != nil { 82 t.Fatal(err) 83 } 84 85 err = walletdb.Update(ctx, db, func(dbtx walletdb.ReadWriteTx) error { 86 err = insertMainChainHeaders(s, dbtx, headerData, filters) 87 if err != nil { 88 return err 89 } 90 return nil 91 }) 92 if err != nil { 93 t.Fatal(err) 94 } 95 96 defaultAccount := uint32(0) 97 tests := []struct { 98 name string 99 f func(*Store, walletdb.ReadWriteTx) (*Store, error) 100 bal, unc dcrutil.Amount 101 unspents map[wire.OutPoint]struct{} 102 unmined map[chainhash.Hash]struct{} 103 }{ 104 { 105 name: "new store", 106 f: func(s *Store, dbtx walletdb.ReadWriteTx) (*Store, error) { 107 return s, nil 108 }, 109 bal: 0, 110 unc: 0, 111 unspents: map[wire.OutPoint]struct{}{}, 112 unmined: map[chainhash.Hash]struct{}{}, 113 }, 114 { 115 name: "txout insert", 116 f: func(s *Store, dbtx walletdb.ReadWriteTx) (*Store, error) { 117 err = s.InsertMemPoolTx(dbtx, tx1Rec) 118 if err != nil { 119 return nil, err 120 } 121 122 err = s.AddCredit(dbtx, tx1Rec, nil, 0, false, defaultAccount) 123 return s, err 124 }, 125 bal: 0, 126 unc: dcrutil.Amount(tx1Rec.MsgTx.TxOut[0].Value), 127 unspents: map[wire.OutPoint]struct{}{ 128 { 129 Hash: tx1Rec.Hash, 130 Index: 0, 131 Tree: wire.TxTreeRegular, 132 }: {}, 133 }, 134 unmined: map[chainhash.Hash]struct{}{ 135 tx1Rec.Hash: {}, 136 }, 137 }, 138 { 139 name: "insert duplicate unconfirmed", 140 f: func(s *Store, dbtx walletdb.ReadWriteTx) (*Store, error) { 141 err = s.InsertMemPoolTx(dbtx, tx1Rec) 142 return s, err 143 }, 144 bal: 0, 145 unc: dcrutil.Amount(tx1Rec.MsgTx.TxOut[0].Value), 146 unspents: map[wire.OutPoint]struct{}{ 147 { 148 Hash: tx1Rec.Hash, 149 Index: 0, 150 Tree: wire.TxTreeRegular, 151 }: {}, 152 }, 153 unmined: map[chainhash.Hash]struct{}{ 154 tx1Rec.Hash: {}, 155 }, 156 }, 157 { 158 name: "confirmed txout insert", 159 f: func(s *Store, dbtx walletdb.ReadWriteTx) (*Store, error) { 160 err = s.InsertMinedTx(dbtx, tx1Rec, &b1Hash) 161 return s, err 162 }, 163 bal: dcrutil.Amount(tx1Rec.MsgTx.TxOut[0].Value), 164 unc: 0, 165 unspents: map[wire.OutPoint]struct{}{ 166 { 167 Hash: tx1Rec.Hash, 168 Index: 0, 169 Tree: wire.TxTreeRegular, 170 }: {}, 171 }, 172 unmined: map[chainhash.Hash]struct{}{}, 173 }, 174 { 175 name: "rollback confirmed credit", 176 f: func(s *Store, dbtx walletdb.ReadWriteTx) (*Store, error) { 177 err := s.Rollback(dbtx, int32(b1H.Height)) 178 return s, err 179 }, 180 bal: 0, 181 unc: dcrutil.Amount(tx1Rec.MsgTx.TxOut[0].Value), 182 unspents: map[wire.OutPoint]struct{}{ 183 { 184 Hash: tx1Rec.Hash, 185 Index: 0, 186 Tree: wire.TxTreeRegular, 187 }: {}, 188 }, 189 unmined: map[chainhash.Hash]struct{}{ 190 tx1Rec.Hash: {}, 191 }, 192 }, 193 { 194 name: "insert duplicate confirmed", 195 f: func(s *Store, dbtx walletdb.ReadWriteTx) (*Store, error) { 196 err = insertMainChainHeaders(s, dbtx, headerData, filters) 197 if err != nil { 198 return nil, err 199 } 200 201 err = s.InsertMinedTx(dbtx, tx1Rec, &b1Hash) 202 return s, nil 203 }, 204 bal: dcrutil.Amount(tx1Rec.MsgTx.TxOut[0].Value), 205 unc: 0, 206 unspents: map[wire.OutPoint]struct{}{ 207 { 208 Hash: tx1Rec.Hash, 209 Index: 0, 210 Tree: wire.TxTreeRegular, 211 }: {}, 212 }, 213 unmined: map[chainhash.Hash]struct{}{}, 214 }, 215 { 216 name: "insert confirmed double spend", 217 f: func(s *Store, dbtx walletdb.ReadWriteTx) (*Store, error) { 218 err = s.InsertMinedTx(dbtx, sTx1Rec, &b2Hash) 219 if err != nil { 220 return nil, err 221 } 222 223 err = s.InsertMinedTx(dbtx, sTx1Rec, &b2Hash) 224 return s, err 225 }, 226 bal: 0, 227 unc: 0, 228 unspents: map[wire.OutPoint]struct{}{}, 229 unmined: map[chainhash.Hash]struct{}{}, 230 }, 231 { 232 name: "rollback after spending tx", 233 f: func(s *Store, dbtx walletdb.ReadWriteTx) (*Store, error) { 234 err := s.Rollback(dbtx, int32(b2H.Height)) 235 return s, err 236 }, 237 bal: 0, 238 unc: 0, 239 unspents: map[wire.OutPoint]struct{}{}, 240 unmined: map[chainhash.Hash]struct{}{ 241 sTx1Rec.Hash: {}, 242 }, 243 }, 244 { 245 name: "insert unconfirmed debit", 246 f: func(s *Store, dbtx walletdb.ReadWriteTx) (*Store, error) { 247 err = s.InsertMemPoolTx(dbtx, sTx2Rec) 248 return s, err 249 }, 250 bal: 0, 251 unc: 0, 252 unspents: map[wire.OutPoint]struct{}{}, 253 unmined: map[chainhash.Hash]struct{}{ 254 sTx1Rec.Hash: {}, 255 sTx2Rec.Hash: {}, 256 }, 257 }, 258 } 259 260 for _, test := range tests { 261 err := walletdb.Update(ctx, db, func(dbtx walletdb.ReadWriteTx) error { 262 tmpStore, err := test.f(s, dbtx) 263 if err != nil { 264 t.Fatalf("%s: got error: %v", test.name, err) 265 } 266 267 s := tmpStore 268 bal, err := s.AccountBalance(dbtx, 1, defaultAccount) 269 if err != nil { 270 t.Fatalf("%s: Confirmed Balance failed: %v", test.name, err) 271 } 272 if bal.Spendable != test.bal { 273 t.Fatalf("%s: balance mismatch: expected: %d, got: %v", 274 test.name, test.bal, bal.Spendable) 275 } 276 unc, err := s.AccountBalance(dbtx, 1, defaultAccount) 277 if err != nil { 278 t.Fatalf("%s: Unconfirmed Balance failed: %v", test.name, err) 279 } 280 if unc.Unconfirmed != test.unc { 281 t.Fatalf("%s: unconfirmed balance mismatch: expected %d, got %d", 282 test.name, test.unc, unc) 283 } 284 285 // Check that unspent outputs match expected. 286 unspent, err := s.UnspentOutputs(dbtx) 287 if err != nil { 288 t.Fatalf("%s: failed to fetch unspent outputs: %v", test.name, err) 289 } 290 for _, cred := range unspent { 291 if _, ok := test.unspents[cred.OutPoint]; !ok { 292 t.Errorf("%s: unexpected unspent output: %v", 293 test.name, cred.OutPoint) 294 } 295 delete(test.unspents, cred.OutPoint) 296 } 297 if len(test.unspents) != 0 { 298 t.Fatalf("%s: missing expected unspent output(s)", test.name) 299 } 300 301 // Check that unmined txs match expected. 302 unmined, err := s.UnminedTxs(dbtx) 303 if err != nil { 304 t.Fatalf("%s: cannot load unmined transactions: %v", 305 test.name, err) 306 } 307 for _, tx := range unmined { 308 if _, ok := test.unmined[tx.Hash]; !ok { 309 t.Fatalf("%s: unexpected unmined tx: %v", 310 test.name, tx.Hash) 311 } 312 delete(test.unmined, tx.Hash) 313 } 314 if len(test.unmined) != 0 { 315 t.Fatalf("%s: missing expected unmined tx(s)", test.name) 316 } 317 318 return nil 319 }) 320 if err != nil { 321 t.Fatal(err) 322 } 323 } 324 } 325 326 func newCoinBase(outputValues ...int64) *wire.MsgTx { 327 tx := wire.MsgTx{ 328 TxIn: []*wire.TxIn{ 329 { 330 PreviousOutPoint: wire.OutPoint{Index: ^uint32(0)}, 331 }, 332 }, 333 } 334 for _, val := range outputValues { 335 tx.TxOut = append(tx.TxOut, &wire.TxOut{Value: val}) 336 } 337 return &tx 338 } 339 340 func spendOutput(txHash *chainhash.Hash, index uint32, tree int8, outputValues ...int64) *wire.MsgTx { 341 tx := wire.MsgTx{ 342 TxIn: []*wire.TxIn{ 343 { 344 PreviousOutPoint: wire.OutPoint{Hash: *txHash, Index: index, Tree: tree}, 345 }, 346 }, 347 } 348 for _, val := range outputValues { 349 tx.TxOut = append(tx.TxOut, &wire.TxOut{Value: val}) 350 } 351 return &tx 352 } 353 354 func TestCoinbases(t *testing.T) { 355 ctx := context.Background() 356 db, _, s, _, teardown, err := cloneDB(ctx, "coinbases.kv") 357 defer teardown() 358 if err != nil { 359 t.Fatal(err) 360 } 361 362 cb := newCoinBase(20e8, 10e8, 30e8) 363 cbRec, err := NewTxRecordFromMsgTx(cb, time.Time{}) 364 if err != nil { 365 t.Fatal(err) 366 } 367 368 defaultAccount := uint32(0) 369 g := makeBlockGenerator() 370 b1H := g.generate(dcrutil.BlockValid) 371 b1Hash := b1H.BlockHash() 372 b1Meta := makeBlockMeta(b1H) 373 headers := []*wire.BlockHeader{b1H} 374 375 // Generate enough blocks for tests. 376 for idx := 0; idx < 18; idx++ { 377 bh := g.generate(dcrutil.BlockValid) 378 headers = append(headers, bh) 379 } 380 381 headerData := makeHeaderDataSlice(headers...) 382 filters := emptyFilters(18) 383 384 err = walletdb.Update(ctx, db, func(dbtx walletdb.ReadWriteTx) error { 385 err = insertMainChainHeaders(s, dbtx, headerData[0:1], filters[0:1]) 386 if err != nil { 387 t.Fatal(err) 388 } 389 390 // Insert coinbase and mark outputs 0 and 2 as credits. 391 err = s.InsertMinedTx(dbtx, cbRec, &b1Hash) 392 if err != nil { 393 t.Fatal(err) 394 } 395 396 err = s.AddCredit(dbtx, cbRec, b1Meta, 0, false, defaultAccount) 397 if err != nil { 398 t.Fatal(err) 399 } 400 401 err = s.AddCredit(dbtx, cbRec, b1Meta, 2, false, defaultAccount) 402 if err != nil { 403 t.Fatal(err) 404 } 405 406 type coinbaseTest struct { 407 immature dcrutil.Amount 408 spendable dcrutil.Amount 409 } 410 411 testMaturity := func(tests []coinbaseTest) error { 412 for i, tst := range tests { 413 bal, err := s.AccountBalance(dbtx, 0, defaultAccount) 414 if err != nil { 415 t.Fatalf("Coinbase test %d: Store.Balance failed: %v", i, err) 416 } 417 418 if bal.ImmatureCoinbaseRewards != tst.immature { 419 t.Fatalf("Coinbase test %d: Got %v immature coinbase, Expected %v", 420 i, bal.ImmatureCoinbaseRewards, tst.immature) 421 } 422 423 if bal.ImmatureCoinbaseRewards != tst.immature { 424 t.Fatalf("Coinbase test %d: Got %v spendable balance, Expected %v", 425 i, bal.Spendable, tst.spendable) 426 } 427 } 428 429 return nil 430 } 431 432 expectedImmature := []coinbaseTest{ 433 { 434 immature: dcrutil.Amount(50e8), 435 spendable: dcrutil.Amount(0), 436 }, 437 } 438 439 // At Block 1, 16 blocks from testnet coinbase maturity . 440 err := testMaturity(expectedImmature) 441 if err != nil { 442 t.Fatal(err) 443 } 444 445 // Extend chain by 6 blocks. 446 err = insertMainChainHeaders(s, dbtx, headerData[1:7], filters[1:7]) 447 if err != nil { 448 t.Fatal(err) 449 } 450 451 // At Block 7, 10 blocks from testnet coinbase maturity. 452 err = testMaturity(expectedImmature) 453 if err != nil { 454 t.Fatal(err) 455 } 456 457 // Extend chain by 6 blocks. 458 err = insertMainChainHeaders(s, dbtx, headerData[7:13], filters[7:13]) 459 if err != nil { 460 t.Fatal(err) 461 } 462 463 // At Block 13, 4 blocks from testnet coinbase maturity. 464 err = testMaturity(expectedImmature) 465 if err != nil { 466 t.Fatal(err) 467 } 468 469 expectedMature := []coinbaseTest{ 470 { 471 immature: dcrutil.Amount(0), 472 spendable: dcrutil.Amount(50e8), 473 }, 474 } 475 476 // Extend chain by 3 blocks. The coinbase should still be immature since 477 // it is still a block away from maturity. 478 err = insertMainChainHeaders(s, dbtx, 479 headerData[13:16], filters[13:16]) 480 if err != nil { 481 t.Fatal(err) 482 } 483 484 // At Block 16, 1 block from testnet coinbase maturity. 485 err = testMaturity(expectedImmature) 486 if err != nil { 487 t.Fatal(err) 488 } 489 490 // Extend chain by 1 block. 491 err = insertMainChainHeaders(s, dbtx, 492 headerData[16:17], filters[16:17]) 493 if err != nil { 494 t.Fatal(err) 495 } 496 497 // At Block 17, testnet coinbase maturity reached. The coinbase should 498 // be available to spend. 499 err = testMaturity(expectedMature) 500 if err != nil { 501 t.Fatal(err) 502 } 503 504 // Spend an output from the coinbase. This should deduct the amount 505 // spent by the tx from the matured coinbase amount. 506 spenderA := spendOutput(&cbRec.Hash, 0, 0, 5e8, 15e8) 507 spenderARec, err := NewTxRecordFromMsgTx(spenderA, time.Now()) 508 if err != nil { 509 t.Fatal(err) 510 } 511 512 b17H := headers[16] 513 b17Hash := b17H.BlockHash() 514 err = s.InsertMinedTx(dbtx, spenderARec, &b17Hash) 515 if err != nil { 516 t.Fatal(err) 517 } 518 519 expectedMatureRemainder := []coinbaseTest{ 520 { 521 immature: dcrutil.Amount(0), 522 spendable: dcrutil.Amount(30e8), 523 }, 524 } 525 526 err = testMaturity(expectedMatureRemainder) 527 if err != nil { 528 t.Fatal(err) 529 } 530 531 // Reorg out the block that matured the coinbase and spends part of the 532 // coinbase. The immature coinbase should be deducted by the amount 533 // being spent by the tx. 534 err = s.Rollback(dbtx, int32(b17H.Height)) 535 if err != nil { 536 t.Fatal(err) 537 } 538 539 expectedReorgImmature := []coinbaseTest{ 540 { 541 immature: dcrutil.Amount(30e8), 542 spendable: dcrutil.Amount(0), 543 }, 544 } 545 546 err = testMaturity(expectedReorgImmature) 547 if err != nil { 548 t.Fatal(err) 549 } 550 551 // Reorg out the block that contained the coinbase. Since the block 552 // with the coinbase is no longer part of the chain there should not be 553 // any mature or immature amounts reported. 554 err = s.Rollback(dbtx, int32(b1H.Height)) 555 if err != nil { 556 t.Fatal(err) 557 } 558 559 expectedReorgToFirstBlock := []coinbaseTest{ 560 { 561 immature: dcrutil.Amount(0), 562 spendable: dcrutil.Amount(0), 563 }, 564 } 565 566 err = testMaturity(expectedReorgToFirstBlock) 567 if err != nil { 568 t.Fatal(err) 569 } 570 571 return nil 572 }) 573 if err != nil { 574 t.Fatal(err) 575 } 576 }