diff --git a/wtxmgr/tx_test.go b/wtxmgr/tx_test.go index 388436c..56873fc 100644 --- a/wtxmgr/tx_test.go +++ b/wtxmgr/tx_test.go @@ -651,6 +651,18 @@ func spendOutput(txHash *chainhash.Hash, index uint32, outputValues ...int64) *w return &tx } +func spendOutputs(outputs []wire.OutPoint, outputValues ...int64) *wire.MsgTx { + tx := &wire.MsgTx{} + for _, output := range outputs { + tx.TxIn = append(tx.TxIn, &wire.TxIn{PreviousOutPoint: output}) + } + for _, value := range outputValues { + tx.TxOut = append(tx.TxOut, &wire.TxOut{Value: value}) + } + + return tx +} + func TestCoinbases(t *testing.T) { t.Parallel() @@ -1364,3 +1376,351 @@ func TestRemoveUnminedTx(t *testing.T) { len(unminedTxns)) } } + +// commitDBTx is a helper function that allows us to perform multiple operations +// on a specific database's bucket as a single atomic operation. +func commitDBTx(t *testing.T, store *Store, db walletdb.DB, + f func(walletdb.ReadWriteBucket)) { + + dbTx, err := db.BeginReadWriteTx() + if err != nil { + t.Fatal(err) + } + defer dbTx.Commit() + + ns := dbTx.ReadWriteBucket(namespaceKey) + + f(ns) +} + +// testInsertDoubleSpendTx is a helper test which double spends an output. The +// boolean parameter indicates whether the first spending transaction should be +// the one confirmed. This test ensures that when a double spend occurs and both +// spending transactions are present in the mempool, if one of them confirms, +// then the remaining conflicting transactions within the mempool should be +// removed from the wallet's store. +func testInsertMempoolDoubleSpendTx(t *testing.T, first bool) { + store, db, teardown, err := testStore() + defer teardown() + if err != nil { + t.Fatal(err) + } + + // In order to reproduce real-world scenarios, we'll use a new database + // transaction for each interaction with the wallet. + // + // We'll start off the test by creating a new coinbase output at height + // 100 and inserting it into the store. + b100 := BlockMeta{ + Block: Block{Height: 100}, + Time: time.Now(), + } + cb := newCoinBase(1e8) + cbRec, err := NewTxRecordFromMsgTx(cb, b100.Time) + if err != nil { + t.Fatal(err) + } + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + if err := store.InsertTx(ns, cbRec, &b100); err != nil { + t.Fatal(err) + } + err := store.AddCredit(ns, cbRec, &b100, 0, false) + if err != nil { + t.Fatal(err) + } + }) + + // Then, we'll create two spends from the same coinbase output, in order + // to replicate a double spend scenario. + firstSpend := spendOutput(&cbRec.Hash, 0, 5e7, 5e7) + firstSpendRec, err := NewTxRecordFromMsgTx(firstSpend, time.Now()) + if err != nil { + t.Fatal(err) + } + secondSpend := spendOutput(&cbRec.Hash, 0, 4e7, 6e7) + secondSpendRec, err := NewTxRecordFromMsgTx(secondSpend, time.Now()) + if err != nil { + t.Fatal(err) + } + + // We'll insert both of them into the store without confirming them. + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + if err := store.InsertTx(ns, firstSpendRec, nil); err != nil { + t.Fatal(err) + } + err := store.AddCredit(ns, firstSpendRec, nil, 0, false) + if err != nil { + t.Fatal(err) + } + }) + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + if err := store.InsertTx(ns, secondSpendRec, nil); err != nil { + t.Fatal(err) + } + err := store.AddCredit(ns, secondSpendRec, nil, 0, false) + if err != nil { + t.Fatal(err) + } + }) + + // Ensure that both spends are found within the unconfirmed transactions + // in the wallet's store. + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + unminedTxs, err := store.UnminedTxs(ns) + if err != nil { + t.Fatal(err) + } + if len(unminedTxs) != 2 { + t.Fatalf("expected 2 unmined txs, got %v", + len(unminedTxs)) + } + }) + + // Then, we'll confirm either the first or second spend, depending on + // the boolean passed, with a height deep enough that allows us to + // succesfully spend the coinbase output. + coinbaseMaturity := int32(chaincfg.TestNet3Params.CoinbaseMaturity) + bMaturity := BlockMeta{ + Block: Block{Height: b100.Height + coinbaseMaturity}, + Time: time.Now(), + } + + var confirmedSpendRec *wtxmgr.TxRecord + if first { + confirmedSpendRec = firstSpendRec + } else { + confirmedSpendRec = secondSpendRec + } + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + err := store.InsertTx(ns, confirmedSpendRec, &bMaturity) + if err != nil { + t.Fatal(err) + } + err = store.AddCredit( + ns, confirmedSpendRec, &bMaturity, 0, false, + ) + if err != nil { + t.Fatal(err) + } + }) + + // This should now trigger the store to remove any other pending double + // spends for this coinbase output, as we've already seen the confirmed + // one. Therefore, we shouldn't see any other unconfirmed transactions + // within it. We also ensure that the transaction that confirmed and is + // now listed as a UTXO within the wallet is the correct one. + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + unminedTxs, err := store.UnminedTxs(ns) + if err != nil { + t.Fatal(err) + } + if len(unminedTxs) != 0 { + t.Fatalf("expected 0 unmined txs, got %v", + len(unminedTxs)) + } + + minedTxs, err := store.UnspentOutputs(ns) + if err != nil { + t.Fatal(err) + } + if len(minedTxs) != 1 { + t.Fatalf("expected 1 mined tx, got %v", len(minedTxs)) + } + if !minedTxs[0].Hash.IsEqual(&confirmedSpendRec.Hash) { + t.Fatalf("expected confirmed tx hash %v, got %v", + confirmedSpendRec.Hash, minedTxs[0].Hash) + } + }) +} + +// TestInsertMempoolDoubleSpendConfirmedFirstTx ensures that when a double spend +// occurs and both spending transactions are present in the mempool, if the +// first spend seen is confirmed, then the second spend transaction within the +// mempool should be removed from the wallet's store. +func TestInsertMempoolDoubleSpendConfirmedFirstTx(t *testing.T) { + t.Parallel() + testInsertMempoolDoubleSpendTx(t, true) +} + +// TestInsertMempoolDoubleSpendConfirmedFirstTx ensures that when a double spend +// occurs and both spending transactions are present in the mempool, if the +// second spend seen is confirmed, then the first spend transaction within the +// mempool should be removed from the wallet's store. +func TestInsertMempoolDoubleSpendConfirmSecondTx(t *testing.T) { + t.Parallel() + testInsertMempoolDoubleSpendTx(t, false) +} + +// TestInsertConfirmedDoubleSpendTx tests that when one or more double spends +// occur and a spending transaction confirms that was not known to the wallet, +// then the unconfirmed double spends within the mempool should be removed from +// the wallet's store. +func TestInsertConfirmedDoubleSpendTx(t *testing.T) { + t.Parallel() + + store, db, teardown, err := testStore() + defer teardown() + if err != nil { + t.Fatal(err) + } + + // In order to reproduce real-world scenarios, we'll use a new database + // transaction for each interaction with the wallet. + // + // We'll start off the test by creating a new coinbase output at height + // 100 and inserting it into the store. + b100 := BlockMeta{ + Block: Block{Height: 100}, + Time: time.Now(), + } + cb1 := newCoinBase(1e8) + cbRec1, err := NewTxRecordFromMsgTx(cb1, b100.Time) + if err != nil { + t.Fatal(err) + } + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + if err := store.InsertTx(ns, cbRec1, &b100); err != nil { + t.Fatal(err) + } + err := store.AddCredit(ns, cbRec1, &b100, 0, false) + if err != nil { + t.Fatal(err) + } + }) + + // Then, we'll create three spends from the same coinbase output. The + // first two will remain unconfirmed, while the last should confirm and + // remove the remaining unconfirmed from the wallet's store. + firstSpend1 := spendOutput(&cbRec1.Hash, 0, 5e7) + firstSpendRec1, err := NewTxRecordFromMsgTx(firstSpend1, time.Now()) + if err != nil { + t.Fatal(err) + } + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + if err := store.InsertTx(ns, firstSpendRec1, nil); err != nil { + t.Fatal(err) + } + err := store.AddCredit(ns, firstSpendRec1, nil, 0, false) + if err != nil { + t.Fatal(err) + } + }) + + secondSpend1 := spendOutput(&cbRec1.Hash, 0, 4e7) + secondSpendRec1, err := NewTxRecordFromMsgTx(secondSpend1, time.Now()) + if err != nil { + t.Fatal(err) + } + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + if err := store.InsertTx(ns, secondSpendRec1, nil); err != nil { + t.Fatal(err) + } + err := store.AddCredit(ns, secondSpendRec1, nil, 0, false) + if err != nil { + t.Fatal(err) + } + }) + + // We'll also create another output and have one unconfirmed and one + // confirmed spending transaction also spend it. + cb2 := newCoinBase(2e8) + cbRec2, err := NewTxRecordFromMsgTx(cb2, b100.Time) + if err != nil { + t.Fatal(err) + } + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + if err := store.InsertTx(ns, cbRec2, &b100); err != nil { + t.Fatal(err) + } + err := store.AddCredit(ns, cbRec2, &b100, 0, false) + if err != nil { + t.Fatal(err) + } + }) + + firstSpend2 := spendOutput(&cbRec2.Hash, 0, 5e7) + firstSpendRec2, err := NewTxRecordFromMsgTx(firstSpend2, time.Now()) + if err != nil { + t.Fatal(err) + } + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + if err := store.InsertTx(ns, firstSpendRec2, nil); err != nil { + t.Fatal(err) + } + err := store.AddCredit(ns, firstSpendRec2, nil, 0, false) + if err != nil { + t.Fatal(err) + } + }) + + // At this point, we should see all unconfirmed transactions within the + // store. + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + unminedTxs, err := store.UnminedTxs(ns) + if err != nil { + t.Fatal(err) + } + if len(unminedTxs) != 3 { + t.Fatalf("expected 3 unmined txs, got %d", + len(unminedTxs)) + } + }) + + // Then, we'll insert the confirmed spend at a height deep enough that + // allows us to successfully spend the coinbase outputs. + coinbaseMaturity := int32(chaincfg.TestNet3Params.CoinbaseMaturity) + bMaturity := BlockMeta{ + Block: Block{Height: b100.Height + coinbaseMaturity}, + Time: time.Now(), + } + outputsToSpend := []wire.OutPoint{ + {Hash: cbRec1.Hash, Index: 0}, + {Hash: cbRec2.Hash, Index: 0}, + } + confirmedSpend := spendOutputs(outputsToSpend, 3e7) + confirmedSpendRec, err := NewTxRecordFromMsgTx( + confirmedSpend, bMaturity.Time, + ) + if err != nil { + t.Fatal(err) + } + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + err := store.InsertTx(ns, confirmedSpendRec, &bMaturity) + if err != nil { + t.Fatal(err) + } + err = store.AddCredit( + ns, confirmedSpendRec, &bMaturity, 0, false, + ) + if err != nil { + t.Fatal(err) + } + }) + + // Now that the confirmed spend exists within the store, we should no + // longer see the unconfirmed spends within it. We also ensure that the + // transaction that confirmed and is now listed as a UTXO within the + // wallet is the correct one. + commitDBTx(t, store, db, func(ns walletdb.ReadWriteBucket) { + unminedTxs, err := store.UnminedTxs(ns) + if err != nil { + t.Fatal(err) + } + if len(unminedTxs) != 0 { + t.Fatalf("expected 0 unmined txs, got %v", + len(unminedTxs)) + } + + minedTxs, err := store.UnspentOutputs(ns) + if err != nil { + t.Fatal(err) + } + if len(minedTxs) != 1 { + t.Fatalf("expected 1 mined tx, got %v", len(minedTxs)) + } + if !minedTxs[0].Hash.IsEqual(&confirmedSpendRec.Hash) { + t.Fatalf("expected confirmed tx hash %v, got %v", + confirmedSpend, minedTxs[0].Hash) + } + }) +}