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:
parent
64b5b448f5
commit
15cec7d90d
1 changed files with 360 additions and 0 deletions
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue