Skip to content

Commit

Permalink
Automatically remove old tickets and expired transactions
Browse files Browse the repository at this point in the history
The wallet had no way to remove expired transactions from the internal
memory pool. Tickets that were lower than the current stake difficulty
were also never removed. Automatic pruning of these transactions on a
per block basis has been added, allowing the wallet to recover and
respend these funds. The function removeConflict has also been
renamed to removeUnconfirmed to better describe its activity.
  • Loading branch information
cjepson committed Feb 25, 2016
1 parent ddb741a commit 04a75c0
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 6 deletions.
10 changes: 10 additions & 0 deletions wallet/chainntfns.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,16 @@ func (w *Wallet) connectBlock(b wtxmgr.BlockMeta) {

w.Stop()
}

// Prune all expired transactions and all stake tickets that no longer
// meet the minimum stake difficulty.
stakeDifficultyInfo := w.GetStakeDifficulty()
err = w.TxStore.PruneUnconfirmed(bs.Height,
stakeDifficultyInfo.StakeDifficulty)
if err != nil {
log.Errorf("Failed to prune unconfirmed transactions when "+
"connecting block height %v: %v", bs.Height, err.Error())
}
}

// disconnectBlock handles a chain server reorganize by rolling back all
Expand Down
79 changes: 78 additions & 1 deletion wtxmgr/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,83 @@ func (s *Store) getBlockHash(ns walletdb.Bucket, height int32) (chainhash.Hash,
return br.Block.Hash, nil
}

// PruneUnconfirmed prunes old stake tickets that are below the current stake
// difficulty or any unconfirmed transaction which is expired.
func (s *Store) PruneUnconfirmed(height int32, stakeDiff int64) error {
s.mutex.Lock()
defer s.mutex.Unlock()

if s.isClosed {
str := "tx manager is closed"
return storeError(ErrIsClosed, str, nil)
}

var unconfTxRs []*TxRecord
var uTxRstoRemove []*TxRecord

// Open an update transaction in the database and
// remove all of the tagged transactions.
err := scopedUpdate(s.namespace, func(ns walletdb.Bucket) error {
errDb := ns.Bucket(bucketUnmined).ForEach(func(k, v []byte) error {
// TODO: Parsing transactions from the db may be a little
// expensive. It's possible the caller only wants the
// serialized transactions.
var txHash chainhash.Hash
errLocal := readRawUnminedHash(k, &txHash)
if errLocal != nil {
return errLocal
}

var rec TxRecord
errLocal = readRawTxRecord(&txHash, v, &rec)
if errLocal != nil {
return errLocal
}

unconfTxRs = append(unconfTxRs, &rec)

return nil
})
if errDb != nil {
return errDb
}

for _, uTxR := range unconfTxRs {
// Tag all transactions that are expired.
if uTxR.MsgTx.Expiry <= uint32(height) &&
uTxR.MsgTx.Expiry != wire.NoExpiryValue {
log.Debugf("Tagging expired tx %v for removal (expiry %v, "+
"height %v)", uTxR.Hash, uTxR.MsgTx.Expiry, height)
uTxRstoRemove = append(uTxRstoRemove, uTxR)
}

// Tag all stake tickets which are below
// network difficulty.
if uTxR.TxType == stake.TxTypeSStx {
if uTxR.MsgTx.TxOut[0].Value < stakeDiff {
log.Debugf("Tagging low diff ticket %v for removal "+
"(stake %v, target %v)", uTxR.Hash,
uTxR.MsgTx.TxOut[0].Value, stakeDiff)
uTxRstoRemove = append(uTxRstoRemove, uTxR)
}
}
}

for _, uTxR := range uTxRstoRemove {
errLocal := s.removeUnconfirmed(ns, uTxR)
if errLocal != nil {
return errLocal
}
}
return nil
})
if err != nil {
return err
}

return nil
}

// moveMinedTx moves a transaction record from the unmined buckets to block
// buckets.
func (s *Store) moveMinedTx(ns walletdb.Bucket, rec *TxRecord, recKey,
Expand Down Expand Up @@ -1422,7 +1499,7 @@ func (s *Store) rollback(ns walletdb.Bucket, height int32) error {

log.Debugf("Transaction %v spends a removed coinbase "+
"output -- removing as well", unminedRec.Hash)
err = s.removeConflict(ns, &unminedRec)
err = s.removeUnconfirmed(ns, &unminedRec)
if err != nil {
return err
}
Expand Down
12 changes: 7 additions & 5 deletions wtxmgr/unconfirmed.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (s *Store) removeDoubleSpends(ns walletdb.Bucket, rec *TxRecord) error {

log.Debugf("Removing double spending transaction %v",
doubleSpend.Hash)
err = s.removeConflict(ns, &doubleSpend)
err = s.removeUnconfirmed(ns, &doubleSpend)
if err != nil {
return err
}
Expand All @@ -87,11 +87,13 @@ func (s *Store) removeDoubleSpends(ns walletdb.Bucket, rec *TxRecord) error {
return nil
}

// removeConflict removes an unmined transaction record and all spend chains
// removeUnconfirmed removes an unmined transaction record and all spend chains
// deriving from it from the store. This is designed to remove transactions
// that would otherwise result in double spend conflicts if left in the store,
// and to remove transactions that spend coinbase transactions on reorgs.
func (s *Store) removeConflict(ns walletdb.Bucket, rec *TxRecord) error {
// and to remove transactions that spend coinbase transactions on reorgs. It
// can also be used to remove old tickets that do not meet the network difficulty
// and expired transactions.
func (s *Store) removeUnconfirmed(ns walletdb.Bucket, 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.
Expand All @@ -110,7 +112,7 @@ func (s *Store) removeConflict(ns walletdb.Bucket, rec *TxRecord) error {

log.Debugf("Transaction %v is part of a removed conflict "+
"chain -- removing as well", spender.Hash)
err = s.removeConflict(ns, &spender)
err = s.removeUnconfirmed(ns, &spender)
if err != nil {
return err
}
Expand Down

0 comments on commit 04a75c0

Please sign in to comment.