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
|
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) {
|
func TestCoinbases(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -1364,3 +1376,351 @@ func TestRemoveUnminedTx(t *testing.T) {
|
||||||
len(unminedTxns))
|
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…
Add table
Reference in a new issue