UI to alert of respend attempt affecting wallet.

Respend transactions that conflict with transactions already in the
wallet are added to it.  They are not displayed unless they also involve
the wallet, or get into a block.  If they do not involve the wallet,
they continue not to affect balance.

Transactions that involve the wallet, and have conflicting non-equivalent
transactions, are highlighted in red.  When the conflict first occurs, a
modal dialog is thrown.

CWallet::SyncMetaData is changed to sync only to equivalent transactions.

When a conflict is added to the wallet, counter nConflictsReceived is
incremented.  This acts like a change in active block height for the
purpose of triggering UI updates.
This commit is contained in:
Tom Harding 2014-06-26 18:31:05 -07:00
parent d640a3ceab
commit ada5a067c7
9 changed files with 87 additions and 21 deletions

View file

@ -23,6 +23,10 @@ static const int STATUSBAR_ICONSIZE = 16;
#define COLOR_NEGATIVE QColor(255, 0, 0) #define COLOR_NEGATIVE QColor(255, 0, 0)
/* Transaction list -- bare address (without label) */ /* Transaction list -- bare address (without label) */
#define COLOR_BAREADDRESS QColor(140, 140, 140) #define COLOR_BAREADDRESS QColor(140, 140, 140)
/* Transaction list -- has conflicting transactions */
#define COLOR_HASCONFLICTING Qt::white;
/* Transaction list -- has conflicting transactions - background */
#define COLOR_HASCONFLICTING_BG QColor(192, 0, 0)
/* Tooltips longer than this (in characters) are converted into rich text, /* Tooltips longer than this (in characters) are converted into rich text,
so that they can be word-wrapped. so that they can be word-wrapped.

View file

@ -24,7 +24,7 @@ TransactionFilterProxy::TransactionFilterProxy(QObject *parent) :
typeFilter(ALL_TYPES), typeFilter(ALL_TYPES),
minAmount(0), minAmount(0),
limitRows(-1), limitRows(-1),
showInactive(true) showInactive(false)
{ {
} }
@ -39,7 +39,7 @@ bool TransactionFilterProxy::filterAcceptsRow(int sourceRow, const QModelIndex &
qint64 amount = llabs(index.data(TransactionTableModel::AmountRole).toLongLong()); qint64 amount = llabs(index.data(TransactionTableModel::AmountRole).toLongLong());
int status = index.data(TransactionTableModel::StatusRole).toInt(); int status = index.data(TransactionTableModel::StatusRole).toInt();
if(!showInactive && status == TransactionStatus::Conflicted) if(!showInactive && status == TransactionStatus::Conflicted && type == TransactionRecord::Other)
return false; return false;
if(!(TYPE(type) & typeFilter)) if(!(TYPE(type) & typeFilter))
return false; return false;

View file

@ -170,6 +170,8 @@ void TransactionRecord::updateStatus(const CWalletTx &wtx)
status.depth = wtx.GetDepthInMainChain(); status.depth = wtx.GetDepthInMainChain();
status.cur_num_blocks = chainActive.Height(); status.cur_num_blocks = chainActive.Height();
status.hasConflicting = false;
if (!IsFinalTx(wtx, chainActive.Height() + 1)) if (!IsFinalTx(wtx, chainActive.Height() + 1))
{ {
if (wtx.nLockTime < LOCKTIME_THRESHOLD) if (wtx.nLockTime < LOCKTIME_THRESHOLD)
@ -213,6 +215,7 @@ void TransactionRecord::updateStatus(const CWalletTx &wtx)
if (status.depth < 0) if (status.depth < 0)
{ {
status.status = TransactionStatus::Conflicted; status.status = TransactionStatus::Conflicted;
status.hasConflicting = !(wtx.GetConflicts(false).empty());
} }
else if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0) else if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0)
{ {
@ -221,6 +224,7 @@ void TransactionRecord::updateStatus(const CWalletTx &wtx)
else if (status.depth == 0) else if (status.depth == 0)
{ {
status.status = TransactionStatus::Unconfirmed; status.status = TransactionStatus::Unconfirmed;
status.hasConflicting = !(wtx.GetConflicts(false).empty());
} }
else if (status.depth < RecommendedNumConfirmations) else if (status.depth < RecommendedNumConfirmations)
{ {
@ -231,13 +235,13 @@ void TransactionRecord::updateStatus(const CWalletTx &wtx)
status.status = TransactionStatus::Confirmed; status.status = TransactionStatus::Confirmed;
} }
} }
} }
bool TransactionRecord::statusUpdateNeeded() bool TransactionRecord::statusUpdateNeeded(int64_t nConflictsReceived)
{ {
AssertLockHeld(cs_main); AssertLockHeld(cs_main);
return status.cur_num_blocks != chainActive.Height(); return (status.cur_num_blocks != chainActive.Height() ||
status.cur_num_conflicts != nConflictsReceived);
} }
QString TransactionRecord::getTxID() const QString TransactionRecord::getTxID() const

View file

@ -20,7 +20,8 @@ class TransactionStatus
public: public:
TransactionStatus(): TransactionStatus():
countsForBalance(false), sortKey(""), countsForBalance(false), sortKey(""),
matures_in(0), status(Offline), depth(0), open_for(0), cur_num_blocks(-1) matures_in(0), status(Offline), hasConflicting(false), depth(0), open_for(0), cur_num_blocks(-1),
cur_num_conflicts(-1)
{ } { }
enum Status { enum Status {
@ -51,6 +52,10 @@ public:
/** @name Reported status /** @name Reported status
@{*/ @{*/
Status status; Status status;
// Has conflicting transactions spending same prevout
bool hasConflicting;
qint64 depth; qint64 depth;
qint64 open_for; /**< Timestamp if status==OpenUntilDate, otherwise number qint64 open_for; /**< Timestamp if status==OpenUntilDate, otherwise number
of additional blocks that need to be mined before of additional blocks that need to be mined before
@ -59,6 +64,10 @@ public:
/** Current number of blocks (to know whether cached status is still valid) */ /** Current number of blocks (to know whether cached status is still valid) */
int cur_num_blocks; int cur_num_blocks;
/** Number of conflicts received into wallet as of last status update */
int64_t cur_num_conflicts;
}; };
/** UI model for a transaction. A core transaction can be represented by multiple UI transactions if it has /** UI model for a transaction. A core transaction can be represented by multiple UI transactions if it has
@ -133,7 +142,7 @@ public:
/** Return whether a status update is needed. /** Return whether a status update is needed.
*/ */
bool statusUpdateNeeded(); bool statusUpdateNeeded(int64_t nConflictsReceived);
}; };
#endif // TRANSACTIONRECORD_H #endif // TRANSACTIONRECORD_H

View file

@ -168,8 +168,7 @@ public:
parent->endRemoveRows(); parent->endRemoveRows();
break; break;
case CT_UPDATED: case CT_UPDATED:
// Miscellaneous updates -- nothing to do, status update will take care of this, and is only computed for emit parent->dataChanged(parent->index(lowerIndex, parent->Status), parent->index(upperIndex-1, parent->Amount));
// visible transactions.
break; break;
} }
} }
@ -190,20 +189,21 @@ public:
// stuck if the core is holding the locks for a longer time - for // stuck if the core is holding the locks for a longer time - for
// example, during a wallet rescan. // example, during a wallet rescan.
// //
// If a status update is needed (blocks came in since last check), // If a status update is needed (blocks or conflicts came in since last check),
// update the status of this transaction from the wallet. Otherwise, // update the status of this transaction from the wallet. Otherwise,
// simply re-use the cached status. // simply re-use the cached status.
TRY_LOCK(cs_main, lockMain); TRY_LOCK(cs_main, lockMain);
if(lockMain) if(lockMain)
{ {
TRY_LOCK(wallet->cs_wallet, lockWallet); TRY_LOCK(wallet->cs_wallet, lockWallet);
if(lockWallet && rec->statusUpdateNeeded()) if(lockWallet && rec->statusUpdateNeeded(wallet->nConflictsReceived))
{ {
std::map<uint256, CWalletTx>::iterator mi = wallet->mapWallet.find(rec->hash); std::map<uint256, CWalletTx>::iterator mi = wallet->mapWallet.find(rec->hash);
if(mi != wallet->mapWallet.end()) if(mi != wallet->mapWallet.end())
{ {
rec->updateStatus(mi->second); rec->updateStatus(mi->second);
rec->status.cur_num_conflicts = wallet->nConflictsReceived;
} }
} }
} }
@ -363,6 +363,8 @@ QString TransactionTableModel::formatTxType(const TransactionRecord *wtx) const
return tr("Payment to yourself"); return tr("Payment to yourself");
case TransactionRecord::Generated: case TransactionRecord::Generated:
return tr("Mined"); return tr("Mined");
case TransactionRecord::Other:
return tr("Other");
default: default:
return QString(); return QString();
} }
@ -535,7 +537,13 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const
return formatTooltip(rec); return formatTooltip(rec);
case Qt::TextAlignmentRole: case Qt::TextAlignmentRole:
return column_alignments[index.column()]; return column_alignments[index.column()];
case Qt::BackgroundColorRole:
if (rec->status.hasConflicting)
return COLOR_HASCONFLICTING_BG;
break;
case Qt::ForegroundRole: case Qt::ForegroundRole:
if (rec->status.hasConflicting)
return COLOR_HASCONFLICTING;
// Non-confirmed (but not immature) as transactions are grey // Non-confirmed (but not immature) as transactions are grey
if(!rec->status.countsForBalance && rec->status.status != TransactionStatus::Immature) if(!rec->status.countsForBalance && rec->status.status != TransactionStatus::Immature)
{ {

View file

@ -138,6 +138,14 @@ void WalletModel::checkBalanceChanged()
void WalletModel::updateTransaction(const QString &hash, int status) void WalletModel::updateTransaction(const QString &hash, int status)
{ {
if (status == CT_GOT_CONFLICT)
{
emit message(tr("Conflict Received"),
tr("WARNING: Transaction may never be confirmed. Its input was seen being spent by another transaction on the network. Wait for confirmation!"),
CClientUIInterface::MSG_WARNING);
return;
}
if(transactionTableModel) if(transactionTableModel)
transactionTableModel->updateTransaction(hash, status); transactionTableModel->updateTransaction(hash, status);

View file

@ -21,7 +21,8 @@ enum ChangeType
{ {
CT_NEW, CT_NEW,
CT_UPDATED, CT_UPDATED,
CT_DELETED CT_DELETED,
CT_GOT_CONFLICT
}; };
/** Signals for UI communication. */ /** Signals for UI communication. */

View file

@ -256,7 +256,7 @@ bool CWallet::SetMaxVersion(int nVersion)
return true; return true;
} }
set<uint256> CWallet::GetConflicts(const uint256& txid) const set<uint256> CWallet::GetConflicts(const uint256& txid, bool includeEquivalent) const
{ {
set<uint256> result; set<uint256> result;
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
@ -274,6 +274,7 @@ set<uint256> CWallet::GetConflicts(const uint256& txid) const
continue; // No conflict if zero or one spends continue; // No conflict if zero or one spends
range = mapTxSpends.equal_range(txin.prevout); range = mapTxSpends.equal_range(txin.prevout);
for (TxSpends::const_iterator it = range.first; it != range.second; ++it) for (TxSpends::const_iterator it = range.first; it != range.second; ++it)
if (includeEquivalent || !wtx.IsEquivalentTo(mapWallet.at(it->second)))
result.insert(it->second); result.insert(it->second);
} }
return result; return result;
@ -303,6 +304,7 @@ void CWallet::SyncMetaData(pair<TxSpends::iterator, TxSpends::iterator> range)
const uint256& hash = it->second; const uint256& hash = it->second;
CWalletTx* copyTo = &mapWallet[hash]; CWalletTx* copyTo = &mapWallet[hash];
if (copyFrom == copyTo) continue; if (copyFrom == copyTo) continue;
if (!copyFrom->IsEquivalentTo(*copyTo)) continue;
copyTo->mapValue = copyFrom->mapValue; copyTo->mapValue = copyFrom->mapValue;
copyTo->vOrderForm = copyFrom->vOrderForm; copyTo->vOrderForm = copyFrom->vOrderForm;
// fTimeReceivedIsTxTime not copied on purpose // fTimeReceivedIsTxTime not copied on purpose
@ -588,6 +590,20 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFromLoadWallet)
// Notify UI of new or updated transaction // Notify UI of new or updated transaction
NotifyTransactionChanged(this, hash, fInsertedNew ? CT_NEW : CT_UPDATED); NotifyTransactionChanged(this, hash, fInsertedNew ? CT_NEW : CT_UPDATED);
// Notifications for existing transactions that now have conflicts with this one
if (fInsertedNew)
{
BOOST_FOREACH(const uint256& conflictHash, wtxIn.GetConflicts(false))
{
CWalletTx& txConflict = mapWallet[conflictHash];
NotifyTransactionChanged(this, conflictHash, CT_UPDATED); //Updates UI table
if (IsFromMe(txConflict) || IsMine(txConflict))
{
NotifyTransactionChanged(this, conflictHash, CT_GOT_CONFLICT); //Throws dialog
}
}
}
// notify an external script when a wallet transaction comes in or is updated // notify an external script when a wallet transaction comes in or is updated
std::string strCmd = GetArg("-walletnotify", ""); std::string strCmd = GetArg("-walletnotify", "");
@ -610,7 +626,12 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransaction& tx, const CBlock* pbl
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
bool fExisted = mapWallet.count(tx.GetHash()); bool fExisted = mapWallet.count(tx.GetHash());
if (fExisted && !fUpdate) return false; if (fExisted && !fUpdate) return false;
if (fExisted || IsMine(tx) || IsFromMe(tx))
bool fIsConflicting = IsConflicting(tx);
if (fIsConflicting)
nConflictsReceived++;
if (fExisted || IsMine(tx) || IsFromMe(tx) || fIsConflicting)
{ {
CWalletTx wtx(this,tx); CWalletTx wtx(this,tx);
// Get merkle branch if transaction was found in a block // Get merkle branch if transaction was found in a block
@ -896,7 +917,7 @@ void CWallet::ReacceptWalletTransactions()
int nDepth = wtx.GetDepthInMainChain(); int nDepth = wtx.GetDepthInMainChain();
if (!wtx.IsCoinBase() && nDepth < 0) if (!wtx.IsCoinBase() && nDepth < 0 && (IsMine(wtx) || IsFromMe(wtx)))
{ {
// Try to add to memory pool // Try to add to memory pool
LOCK(mempool.cs); LOCK(mempool.cs);
@ -916,13 +937,13 @@ void CWalletTx::RelayWalletTransaction()
} }
} }
set<uint256> CWalletTx::GetConflicts() const set<uint256> CWalletTx::GetConflicts(bool includeEquivalent) const
{ {
set<uint256> result; set<uint256> result;
if (pwallet != NULL) if (pwallet != NULL)
{ {
uint256 myHash = GetHash(); uint256 myHash = GetHash();
result = pwallet->GetConflicts(myHash); result = pwallet->GetConflicts(myHash, includeEquivalent);
result.erase(myHash); result.erase(myHash);
} }
return result; return result;

View file

@ -141,6 +141,9 @@ public:
MasterKeyMap mapMasterKeys; MasterKeyMap mapMasterKeys;
unsigned int nMasterKeyMaxID; unsigned int nMasterKeyMaxID;
// Increment to cause UI refresh, similar to new block
int64_t nConflictsReceived;
CWallet() CWallet()
{ {
SetNull(); SetNull();
@ -163,6 +166,7 @@ public:
nNextResend = 0; nNextResend = 0;
nLastResend = 0; nLastResend = 0;
nTimeFirstKey = 0; nTimeFirstKey = 0;
nConflictsReceived = 0;
} }
std::map<uint256, CWalletTx> mapWallet; std::map<uint256, CWalletTx> mapWallet;
@ -305,6 +309,13 @@ public:
{ {
return (GetDebit(tx) > 0); return (GetDebit(tx) > 0);
} }
bool IsConflicting(const CTransaction& tx) const
{
BOOST_FOREACH(const CTxIn& txin, tx.vin)
if (mapTxSpends.count(txin.prevout))
return true;
return false;
}
int64_t GetDebit(const CTransaction& tx) const int64_t GetDebit(const CTransaction& tx) const
{ {
int64_t nDebit = 0; int64_t nDebit = 0;
@ -377,7 +388,7 @@ public:
int GetVersion() { LOCK(cs_wallet); return nWalletVersion; } int GetVersion() { LOCK(cs_wallet); return nWalletVersion; }
// Get wallet transactions that conflict with given transaction (spend same outputs) // Get wallet transactions that conflict with given transaction (spend same outputs)
std::set<uint256> GetConflicts(const uint256& txid) const; std::set<uint256> GetConflicts(const uint256& txid, bool includeEquivalent) const;
/** Address book entry changed. /** Address book entry changed.
* @note called with lock cs_wallet held. * @note called with lock cs_wallet held.
@ -699,7 +710,7 @@ public:
void RelayWalletTransaction(); void RelayWalletTransaction();
std::set<uint256> GetConflicts() const; std::set<uint256> GetConflicts(bool includeEquivalent=true) const;
}; };