Merge pull request #509 from wpaulino/double-spend-bug-fix

wtxmgr: fix double spend bug
This commit is contained in:
Olaoluwa Osuntokun 2018-07-23 20:28:34 -07:00 committed by GitHub
commit a4d9da433f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 536 additions and 187 deletions

View file

@ -1214,12 +1214,18 @@ func (it *unminedCreditIterator) reposition(txHash *chainhash.Hash, index uint32
//
// [0:32] Transaction hash (32 bytes)
// putRawUnminedInput maintains a list of unmined transaction hashes that have
// spent an outpoint. Each entry in the bucket is keyed by the outpoint being
// spent.
func putRawUnminedInput(ns walletdb.ReadWriteBucket, k, v []byte) error {
err := ns.NestedReadWriteBucket(bucketUnminedInputs).Put(k, v)
spendTxHashes := ns.NestedReadBucket(bucketUnminedInputs).Get(k)
spendTxHashes = append(spendTxHashes, v...)
err := ns.NestedReadWriteBucket(bucketUnminedInputs).Put(k, spendTxHashes)
if err != nil {
str := "failed to put unmined input"
return storeError(ErrDatabase, str, err)
}
return nil
}
@ -1227,6 +1233,26 @@ func existsRawUnminedInput(ns walletdb.ReadBucket, k []byte) (v []byte) {
return ns.NestedReadBucket(bucketUnminedInputs).Get(k)
}
// fetchUnminedInputSpendTxHashes fetches the list of unmined transactions that
// spend the serialized outpoint.
func fetchUnminedInputSpendTxHashes(ns walletdb.ReadBucket, k []byte) []chainhash.Hash {
rawSpendTxHashes := ns.NestedReadBucket(bucketUnminedInputs).Get(k)
if rawSpendTxHashes == nil {
return nil
}
// Each transaction hash is 32 bytes.
spendTxHashes := make([]chainhash.Hash, 0, len(rawSpendTxHashes)/32)
for len(rawSpendTxHashes) > 0 {
var spendTxHash chainhash.Hash
copy(spendTxHash[:], rawSpendTxHashes[:32])
spendTxHashes = append(spendTxHashes, spendTxHash)
rawSpendTxHashes = rawSpendTxHashes[32:]
}
return spendTxHashes
}
func deleteRawUnminedInput(ns walletdb.ReadWriteBucket, k []byte) error {
err := ns.NestedReadWriteBucket(bucketUnminedInputs).Delete(k)
if err != nil {

View file

@ -167,72 +167,67 @@ func Create(ns walletdb.ReadWriteBucket) error {
return createStore(ns)
}
// moveMinedTx moves a transaction record from the unmined buckets to block
// buckets.
func (s *Store) moveMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord, recKey, recVal []byte, block *BlockMeta) error {
log.Infof("Marking unconfirmed transaction %v mined in block %d",
&rec.Hash, block.Height)
// updateMinedBalance updates the mined balance within the store, if changed,
// after processing the given transaction record.
func (s *Store) updateMinedBalance(ns walletdb.ReadWriteBucket, rec *TxRecord,
block *BlockMeta) error {
// Insert block record as needed.
blockKey, blockVal := existsBlockRecord(ns, block.Height)
var err error
if blockVal == nil {
blockVal = valueBlockRecord(block, &rec.Hash)
} else {
blockVal, err = appendRawBlockRecord(blockVal, &rec.Hash)
if err != nil {
return err
}
}
err = putRawBlockRecord(ns, blockKey, blockVal)
if err != nil {
return err
}
err = putRawTxRecord(ns, recKey, recVal)
if err != nil {
return err
}
// Fetch the mined balance in case we need to update it.
minedBalance, err := fetchMinedBalance(ns)
if err != nil {
return err
}
// For all transaction inputs, remove the previous output marker from the
// unmined inputs bucket. For any mined transactions with unspent credits
// spent by this transaction, mark each spent, remove from the unspents map,
// and insert a debit record for the spent credit.
debitIncidence := indexedIncidence{
incidence: incidence{txHash: rec.Hash, block: block.Block},
// index set for each rec input below.
// Add a debit record for each unspent credit spent by this transaction.
// The index is set in each iteration below.
spender := indexedIncidence{
incidence: incidence{
txHash: rec.Hash,
block: block.Block,
},
}
newMinedBalance := minedBalance
for i, input := range rec.MsgTx.TxIn {
unspentKey, credKey := existsUnspent(ns, &input.PreviousOutPoint)
err = deleteRawUnminedInput(ns, unspentKey)
if err != nil {
return err
}
if credKey == nil {
// Debits for unmined transactions are not explicitly
// tracked. Instead, all previous outputs spent by any
// unmined transaction are added to a map for quick
// lookups when it must be checked whether a mined
// output is unspent or not.
//
// Tracking individual debits for unmined transactions
// could be added later to simplify (and increase
// performance of) determining some details that need
// the previous outputs (e.g. determining a fee), but at
// the moment that is not done (and a db lookup is used
// for those cases instead). There is also a good
// chance that all unmined transaction handling will
// move entirely to the db rather than being handled in
// memory for atomicity reasons, so the simplist
// implementation is currently used.
continue
}
debitIncidence.index = uint32(i)
amt, err := spendCredit(ns, credKey, &debitIncidence)
// If this output is relevant to us, we'll mark the it as spent
// and remove its amount from the store.
spender.index = uint32(i)
amt, err := spendCredit(ns, credKey, &spender)
if err != nil {
return err
}
minedBalance -= amt
err = deleteRawUnspent(ns, unspentKey)
err = putDebit(
ns, &rec.Hash, uint32(i), amt, &block.Block, credKey,
)
if err != nil {
return err
}
if err := deleteRawUnspent(ns, unspentKey); err != nil {
return err
}
err = putDebit(ns, &rec.Hash, uint32(i), amt, &block.Block, credKey)
if err != nil {
return err
}
newMinedBalance -= amt
}
// For each output of the record that is marked as a credit, if the
@ -246,6 +241,7 @@ func (s *Store) moveMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord, recKey,
block: block.Block,
spentBy: indexedIncidence{index: ^uint32(0)},
}
it := makeUnminedCreditIterator(ns, &rec.Hash)
for it.next() {
// TODO: This should use the raw apis. The credit value (it.cv)
@ -260,12 +256,12 @@ func (s *Store) moveMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord, recKey,
if err != nil {
return err
}
cred.outPoint.Index = index
cred.amount = amount
cred.change = change
err = putUnspentCredit(ns, &cred)
if err != nil {
if err := putUnspentCredit(ns, &cred); err != nil {
return err
}
err = putUnspent(ns, &cred.outPoint, &block.Block)
@ -273,35 +269,29 @@ func (s *Store) moveMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord, recKey,
return err
}
// reposition cursor before deleting, since the above puts have
// invalidated the cursor.
it.reposition(&rec.Hash, index)
// Avoid cursor deletion until bolt issue #620 is resolved.
// err = it.delete()
// if err != nil {
// return err
// }
minedBalance += amount
newMinedBalance += amount
}
if it.err != nil {
return it.err
}
// Delete all possible credits outside of the iteration since the cursor
// deletion is broken.
for i := 0; i < len(rec.MsgTx.TxOut); i++ {
k := canonicalOutPoint(&rec.Hash, uint32(i))
err = deleteRawUnminedCredit(ns, k)
if err != nil {
return err
}
// Update the balance if it has changed.
if newMinedBalance != minedBalance {
return putMinedBalance(ns, newMinedBalance)
}
err = putMinedBalance(ns, minedBalance)
if err != nil {
return err
return nil
}
// deleteUnminedTx deletes an unmined transaction from the store.
//
// NOTE: This should only be used once the transaction has been mined.
func (s *Store) deleteUnminedTx(ns walletdb.ReadWriteBucket, rec *TxRecord) error {
for i := range rec.MsgTx.TxOut {
k := canonicalOutPoint(&rec.Hash, uint32(i))
if err := deleteRawUnminedCredit(ns, k); err != nil {
return err
}
}
return deleteRawUnmined(ns, rec.Hash[:])
@ -331,98 +321,23 @@ func (s *Store) RemoveUnminedTx(ns walletdb.ReadWriteBucket, rec *TxRecord) erro
}
// insertMinedTx inserts a new transaction record for a mined transaction into
// the database. It is expected that the exact transation does not already
// exist in the unmined buckets, but unmined double spends (including mutations)
// are removed.
func (s *Store) insertMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord, block *BlockMeta) error {
// Fetch the mined balance in case we need to update it.
minedBalance, err := fetchMinedBalance(ns)
if err != nil {
return err
}
// the database under the confirmed bucket. It guarantees that, if the
// tranasction was previously unconfirmed, then it will take care of cleaning up
// the unconfirmed state. All other unconfirmed double spend attempts will be
// removed as well.
func (s *Store) insertMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord,
block *BlockMeta) error {
// Add a debit record for each unspent credit spent by this tx.
spender := indexedIncidence{
incidence: incidence{
txHash: rec.Hash,
block: block.Block,
},
// index set for each iteration below
}
for i, input := range rec.MsgTx.TxIn {
unspentKey, credKey := existsUnspent(ns, &input.PreviousOutPoint)
if credKey == nil {
// Debits for unmined transactions are not explicitly
// tracked. Instead, all previous outputs spent by any
// unmined transaction are added to a map for quick
// lookups when it must be checked whether a mined
// output is unspent or not.
//
// Tracking individual debits for unmined transactions
// could be added later to simplify (and increase
// performance of) determining some details that need
// the previous outputs (e.g. determining a fee), but at
// the moment that is not done (and a db lookup is used
// for those cases instead). There is also a good
// chance that all unmined transaction handling will
// move entirely to the db rather than being handled in
// memory for atomicity reasons, so the simplist
// implementation is currently used.
continue
}
spender.index = uint32(i)
amt, err := spendCredit(ns, credKey, &spender)
if err != nil {
return err
}
err = putDebit(ns, &rec.Hash, uint32(i), amt, &block.Block,
credKey)
if err != nil {
return err
}
minedBalance -= amt
err = deleteRawUnspent(ns, unspentKey)
if err != nil {
return err
}
}
// TODO only update if we actually modified the
// mined balance.
err = putMinedBalance(ns, minedBalance)
if err != nil {
// If a transaction record for this hash and block already exists, we
// can exit early.
if _, v := existsTxRecord(ns, &rec.Hash, &block.Block); v != nil {
return nil
}
// If a transaction record for this tx hash and block already exist,
// there is nothing left to do.
k, v := existsTxRecord(ns, &rec.Hash, &block.Block)
if v != nil {
return nil
}
// If the exact tx (not a double spend) is already included but
// unconfirmed, move it to a block.
v = existsRawUnmined(ns, rec.Hash[:])
if v != nil {
return s.moveMinedTx(ns, rec, k, v, block)
}
// As there may be unconfirmed transactions that are invalidated by this
// transaction (either being duplicates, or double spends), remove them
// from the unconfirmed set. This also handles removing unconfirmed
// transaction spend chains if any other unconfirmed transactions spend
// outputs of the removed double spend.
err = s.removeDoubleSpends(ns, rec)
if err != nil {
return err
}
// If a block record does not yet exist for any transactions from this
// block, insert the record. Otherwise, update it by adding the
// transaction hash to the set of transactions from this block.
// block, insert a block record first. Otherwise, update it by adding
// the transaction hash to the set of transactions from this block.
var err error
blockKey, blockValue := existsBlockRecord(ns, block.Height)
if blockValue == nil {
err = putBlockRecord(ns, block, &rec.Hash)
@ -436,13 +351,33 @@ func (s *Store) insertMinedTx(ns walletdb.ReadWriteBucket, rec *TxRecord, block
if err != nil {
return err
}
err = putTxRecord(ns, rec, &block.Block)
if err != nil {
if err := putTxRecord(ns, rec, &block.Block); err != nil {
return err
}
return nil
// Determine if this transaction has affected our balance, and if so,
// update it.
if err := s.updateMinedBalance(ns, rec, block); err != nil {
return err
}
// If this transaction previously existed within the store as unmined,
// we'll need to remove it from the unmined bucket.
if v := existsRawUnmined(ns, rec.Hash[:]); v != nil {
log.Infof("Marking unconfirmed transaction %v mined in block %d",
&rec.Hash, block.Height)
if err := s.deleteUnminedTx(ns, rec); err != nil {
return err
}
}
// As there may be unconfirmed transactions that are invalidated by this
// transaction (either being duplicates, or double spends), remove them
// from the unconfirmed set. This also handles removing unconfirmed
// transaction spend chains if any other unconfirmed transactions spend
// outputs of the removed double spend.
return s.removeDoubleSpends(ns, rec)
}
// AddCredit marks a transaction record as containing a transaction output
@ -729,11 +664,21 @@ func (s *Store) rollback(ns walletdb.ReadWriteBucket, height int32) error {
for _, op := range coinBaseCredits {
opKey := canonicalOutPoint(&op.Hash, op.Index)
unminedKey := existsRawUnminedInput(ns, opKey)
if unminedKey != nil {
unminedVal := existsRawUnmined(ns, unminedKey)
unminedSpendTxHashKeys := fetchUnminedInputSpendTxHashes(ns, opKey)
for _, unminedSpendTxHashKey := range unminedSpendTxHashKeys {
unminedVal := existsRawUnmined(ns, unminedSpendTxHashKey[:])
// If the spending transaction spends multiple outputs
// from the same transaction, we'll find duplicate
// entries within the store, so it's possible we're
// unable to find it if the conflicts have already been
// removed in a previous iteration.
if unminedVal == nil {
continue
}
var unminedRec TxRecord
copy(unminedRec.Hash[:], unminedKey) // Silly but need an array
unminedRec.Hash = unminedSpendTxHashKey
err = readRawTxRecord(&unminedRec.Hash, unminedVal, &unminedRec)
if err != nil {
return err

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

View file

@ -53,25 +53,37 @@ func (s *Store) removeDoubleSpends(ns walletdb.ReadWriteBucket, rec *TxRecord) e
for _, input := range rec.MsgTx.TxIn {
prevOut := &input.PreviousOutPoint
prevOutKey := canonicalOutPoint(&prevOut.Hash, prevOut.Index)
doubleSpendHash := existsRawUnminedInput(ns, prevOutKey)
if doubleSpendHash != nil {
doubleSpendHashes := fetchUnminedInputSpendTxHashes(ns, prevOutKey)
for _, doubleSpendHash := range doubleSpendHashes {
doubleSpendVal := existsRawUnmined(ns, doubleSpendHash[:])
// If the spending transaction spends multiple outputs
// from the same transaction, we'll find duplicate
// entries within the store, so it's possible we're
// unable to find it if the conflicts have already been
// removed in a previous iteration.
if doubleSpendVal == nil {
continue
}
var doubleSpend TxRecord
doubleSpendVal := existsRawUnmined(ns, doubleSpendHash)
copy(doubleSpend.Hash[:], doubleSpendHash) // Silly but need an array
err := readRawTxRecord(&doubleSpend.Hash, doubleSpendVal,
&doubleSpend)
doubleSpend.Hash = doubleSpendHash
err := readRawTxRecord(
&doubleSpend.Hash, doubleSpendVal, &doubleSpend,
)
if err != nil {
return err
}
log.Debugf("Removing double spending transaction %v",
doubleSpend.Hash)
err = s.removeConflict(ns, &doubleSpend)
if err != nil {
if err := s.removeConflict(ns, &doubleSpend); err != nil {
return err
}
}
}
return nil
}
@ -83,14 +95,23 @@ func (s *Store) removeConflict(ns walletdb.ReadWriteBucket, rec *TxRecord) error
// For each potential credit for this record, each spender (if any) must
// be recursively removed as well. Once the spenders are removed, the
// credit is deleted.
numOuts := uint32(len(rec.MsgTx.TxOut))
for i := uint32(0); i < numOuts; i++ {
k := canonicalOutPoint(&rec.Hash, i)
spenderHash := existsRawUnminedInput(ns, k)
if spenderHash != nil {
for i := range rec.MsgTx.TxOut {
k := canonicalOutPoint(&rec.Hash, uint32(i))
spenderHashes := fetchUnminedInputSpendTxHashes(ns, k)
for _, spenderHash := range spenderHashes {
spenderVal := existsRawUnmined(ns, spenderHash[:])
// If the spending transaction spends multiple outputs
// from the same transaction, we'll find duplicate
// entries within the store, so it's possible we're
// unable to find it if the conflicts have already been
// removed in a previous iteration.
if spenderVal == nil {
continue
}
var spender TxRecord
spenderVal := existsRawUnmined(ns, spenderHash)
copy(spender.Hash[:], spenderHash) // Silly but need an array
spender.Hash = spenderHash
err := readRawTxRecord(&spender.Hash, spenderVal, &spender)
if err != nil {
return err
@ -98,13 +119,11 @@ func (s *Store) removeConflict(ns walletdb.ReadWriteBucket, rec *TxRecord) error
log.Debugf("Transaction %v is part of a removed conflict "+
"chain -- removing as well", spender.Hash)
err = s.removeConflict(ns, &spender)
if err != nil {
if err := s.removeConflict(ns, &spender); err != nil {
return err
}
}
err := deleteRawUnminedCredit(ns, k)
if err != nil {
if err := deleteRawUnminedCredit(ns, k); err != nil {
return err
}
}
@ -115,8 +134,7 @@ func (s *Store) removeConflict(ns walletdb.ReadWriteBucket, rec *TxRecord) error
for _, input := range rec.MsgTx.TxIn {
prevOut := &input.PreviousOutPoint
k := canonicalOutPoint(&prevOut.Hash, prevOut.Index)
err := deleteRawUnminedInput(ns, k)
if err != nil {
if err := deleteRawUnminedInput(ns, k); err != nil {
return err
}
}