wtxmgr: store multiple spending tx hashes for outpoints

In this commit, we modify the way we store spending transaction hashes
for unconfirmed spends. Now, rather than only keeping track of one
possible unconfirmed spend, we track multiple in order to ensure we
properly handle transaction replacements, like in the case of RBF,
double spends, etc. With this in, the double spent tests recently added
should now pass.
This commit is contained in:
Wilmer Paulino 2018-06-08 18:59:57 -07:00
parent ff646086b7
commit aa826c64cf
No known key found for this signature in database
GPG key ID: 6DF57B9F9514972F
3 changed files with 80 additions and 26 deletions

View file

@ -1214,12 +1214,18 @@ func (it *unminedCreditIterator) reposition(txHash *chainhash.Hash, index uint32
// //
// [0:32] Transaction hash (32 bytes) // [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 { 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 { if err != nil {
str := "failed to put unmined input" str := "failed to put unmined input"
return storeError(ErrDatabase, str, err) return storeError(ErrDatabase, str, err)
} }
return nil return nil
} }
@ -1227,6 +1233,26 @@ func existsRawUnminedInput(ns walletdb.ReadBucket, k []byte) (v []byte) {
return ns.NestedReadBucket(bucketUnminedInputs).Get(k) 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 { func deleteRawUnminedInput(ns walletdb.ReadWriteBucket, k []byte) error {
err := ns.NestedReadWriteBucket(bucketUnminedInputs).Delete(k) err := ns.NestedReadWriteBucket(bucketUnminedInputs).Delete(k)
if err != nil { if err != nil {

View file

@ -664,11 +664,21 @@ func (s *Store) rollback(ns walletdb.ReadWriteBucket, height int32) error {
for _, op := range coinBaseCredits { for _, op := range coinBaseCredits {
opKey := canonicalOutPoint(&op.Hash, op.Index) opKey := canonicalOutPoint(&op.Hash, op.Index)
unminedKey := existsRawUnminedInput(ns, opKey) unminedSpendTxHashKeys := fetchUnminedInputSpendTxHashes(ns, opKey)
if unminedKey != nil { for _, unminedSpendTxHashKey := range unminedSpendTxHashKeys {
unminedVal := existsRawUnmined(ns, unminedKey) 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 var unminedRec TxRecord
copy(unminedRec.Hash[:], unminedKey) // Silly but need an array unminedRec.Hash = unminedSpendTxHashKey
err = readRawTxRecord(&unminedRec.Hash, unminedVal, &unminedRec) err = readRawTxRecord(&unminedRec.Hash, unminedVal, &unminedRec)
if err != nil { if err != nil {
return err return err

View file

@ -53,25 +53,37 @@ func (s *Store) removeDoubleSpends(ns walletdb.ReadWriteBucket, rec *TxRecord) e
for _, input := range rec.MsgTx.TxIn { for _, input := range rec.MsgTx.TxIn {
prevOut := &input.PreviousOutPoint prevOut := &input.PreviousOutPoint
prevOutKey := canonicalOutPoint(&prevOut.Hash, prevOut.Index) 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 var doubleSpend TxRecord
doubleSpendVal := existsRawUnmined(ns, doubleSpendHash) doubleSpend.Hash = doubleSpendHash
copy(doubleSpend.Hash[:], doubleSpendHash) // Silly but need an array err := readRawTxRecord(
err := readRawTxRecord(&doubleSpend.Hash, doubleSpendVal, &doubleSpend.Hash, doubleSpendVal, &doubleSpend,
&doubleSpend) )
if err != nil { if err != nil {
return err return err
} }
log.Debugf("Removing double spending transaction %v", log.Debugf("Removing double spending transaction %v",
doubleSpend.Hash) doubleSpend.Hash)
err = s.removeConflict(ns, &doubleSpend) if err := s.removeConflict(ns, &doubleSpend); err != nil {
if err != nil {
return err return err
} }
} }
} }
return nil 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 // For each potential credit for this record, each spender (if any) must
// be recursively removed as well. Once the spenders are removed, the // be recursively removed as well. Once the spenders are removed, the
// credit is deleted. // credit is deleted.
numOuts := uint32(len(rec.MsgTx.TxOut)) for i := range rec.MsgTx.TxOut {
for i := uint32(0); i < numOuts; i++ { k := canonicalOutPoint(&rec.Hash, uint32(i))
k := canonicalOutPoint(&rec.Hash, i) spenderHashes := fetchUnminedInputSpendTxHashes(ns, k)
spenderHash := existsRawUnminedInput(ns, k) for _, spenderHash := range spenderHashes {
if spenderHash != nil { 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 var spender TxRecord
spenderVal := existsRawUnmined(ns, spenderHash) spender.Hash = spenderHash
copy(spender.Hash[:], spenderHash) // Silly but need an array
err := readRawTxRecord(&spender.Hash, spenderVal, &spender) err := readRawTxRecord(&spender.Hash, spenderVal, &spender)
if err != nil { if err != nil {
return err 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 "+ log.Debugf("Transaction %v is part of a removed conflict "+
"chain -- removing as well", spender.Hash) "chain -- removing as well", spender.Hash)
err = s.removeConflict(ns, &spender) if err := s.removeConflict(ns, &spender); err != nil {
if err != nil {
return err return err
} }
} }
err := deleteRawUnminedCredit(ns, k) if err := deleteRawUnminedCredit(ns, k); err != nil {
if err != nil {
return err return err
} }
} }
@ -115,8 +134,7 @@ func (s *Store) removeConflict(ns walletdb.ReadWriteBucket, rec *TxRecord) error
for _, input := range rec.MsgTx.TxIn { for _, input := range rec.MsgTx.TxIn {
prevOut := &input.PreviousOutPoint prevOut := &input.PreviousOutPoint
k := canonicalOutPoint(&prevOut.Hash, prevOut.Index) k := canonicalOutPoint(&prevOut.Hash, prevOut.Index)
err := deleteRawUnminedInput(ns, k) if err := deleteRawUnminedInput(ns, k); err != nil {
if err != nil {
return err return err
} }
} }