wtxmgr: add transaction replacement and double spend tests

In this commit, we add a set of double spend tests to ensure that we can
properly detect and handle them. At this point, we do not do this, but a
follow up commit will address this.
This commit is contained in:
Wilmer Paulino 2018-06-08 18:52:25 -07:00
parent 64b5b448f5
commit 15cec7d90d
No known key found for this signature in database
GPG key ID: 6DF57B9F9514972F

View file

@ -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)
}
})
}