Merge pull request #3883 from dgenr8/first_double_spend

Relay and alert user to double spends
This commit is contained in:
Gavin Andresen 2014-06-30 08:35:12 -04:00
commit 8ceb28afc3
21 changed files with 296 additions and 38 deletions

View File

@ -139,6 +139,9 @@ Execute command when the best block changes (%s in cmd is replaced by block hash
\fB\-walletnotify=\fR<cmd> \fB\-walletnotify=\fR<cmd>
Execute command when a wallet transaction changes (%s in cmd is replaced by TxID) Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)
.TP .TP
\fB\-respendnotify=\fR<cmd>
Execute command when a network tx respends wallet tx input (%s=respend TxID, %t=wallet TxID)
.TP
\fB\-alertnotify=\fR<cmd> \fB\-alertnotify=\fR<cmd>
Execute command when a relevant alert is received (%s in cmd is replaced by message) Execute command when a relevant alert is received (%s in cmd is replaced by message)
.TP .TP

View File

@ -19,3 +19,49 @@ estimate.
Statistics used to estimate fees and priorities are saved in the Statistics used to estimate fees and priorities are saved in the
data directory in the 'fee_estimates.dat' file just before data directory in the 'fee_estimates.dat' file just before
program shutdown, and are read in at startup. program shutdown, and are read in at startup.
Double-Spend Relay and Alerts
=============================
VERY IMPORTANT: *It has never been safe, and remains unsafe, to rely*
*on unconfirmed transactions.*
Relay
-----
When an attempt is seen on the network to spend the same unspent funds
more than once, it is no longer ignored. Instead, it is broadcast, to
serve as an alert. This broadcast is subject to protections against
denial-of-service attacks.
Wallets and other bitcoin services should alert their users to
double-spends that affect them. Merchants and other users may have
enough time to withhold goods or services when payment becomes
uncertain, until confirmation.
Bitcoin Core Wallet Alerts
--------------------------
The Bitcoin Core wallet now makes respend attempts visible in several
ways.
If you are online, and a respend affecting one of your wallet
transactions is seen, a notification is immediately issued to the
command registered with `-respendnotify=<cmd>`. Additionally, if
using the GUI:
- An alert box is immediately displayed.
- The affected wallet transaction is highlighted in red until it is
confirmed (and it may never be confirmed).
A `respendsobserved` array is added to `gettransaction`, `listtransactions`,
and `listsinceblock` RPC results.
Warning
-------
*If you rely on an unconfirmed transaction, these change do VERY*
*LITTLE to protect you from a malicious double-spend, because:*
- You may learn about the respend too late to avoid doing whatever
you were being paid for
- Using other relay rules, a double-spender can craft his crime to
resist broadcast
- Miners can choose which conflicting spend to confirm, and some
miners may not confirm the first acceptable spend they see

View File

@ -94,6 +94,13 @@ bool CBloomFilter::contains(const uint256& hash) const
return contains(data); return contains(data);
} }
void CBloomFilter::clear()
{
vData.assign(vData.size(),0);
isFull = false;
isEmpty = true;
}
bool CBloomFilter::IsWithinSizeConstraints() const bool CBloomFilter::IsWithinSizeConstraints() const
{ {
return vData.size() <= MAX_BLOOM_FILTER_SIZE && nHashFuncs <= MAX_HASH_FUNCS; return vData.size() <= MAX_BLOOM_FILTER_SIZE && nHashFuncs <= MAX_HASH_FUNCS;

View File

@ -78,6 +78,8 @@ public:
bool contains(const COutPoint& outpoint) const; bool contains(const COutPoint& outpoint) const;
bool contains(const uint256& hash) const; bool contains(const uint256& hash) const;
void clear();
// True if the size is <= MAX_BLOOM_FILTER_SIZE and the number of hash functions is <= MAX_HASH_FUNCS // True if the size is <= MAX_BLOOM_FILTER_SIZE and the number of hash functions is <= MAX_HASH_FUNCS
// (catch a filter which was just deserialized which was too big) // (catch a filter which was just deserialized which was too big)
bool IsWithinSizeConstraints() const; bool IsWithinSizeConstraints() const;

View File

@ -119,6 +119,22 @@ CTransaction& CTransaction::operator=(const CTransaction &tx) {
return *this; return *this;
} }
bool CTransaction::IsEquivalentTo(const CTransaction& tx) const
{
if (nVersion != tx.nVersion ||
nLockTime != tx.nLockTime ||
vin.size() != tx.vin.size() ||
vout != tx.vout)
return false;
for (unsigned int i = 0; i < vin.size(); i++)
{
if (vin[i].nSequence != tx.vin[i].nSequence ||
vin[i].prevout != tx.vin[i].prevout)
return false;
}
return true;
}
int64_t CTransaction::GetValueOut() const int64_t CTransaction::GetValueOut() const
{ {
int64_t nValueOut = 0; int64_t nValueOut = 0;

View File

@ -256,6 +256,9 @@ public:
return hash; return hash;
} }
// True if only scriptSigs are different
bool IsEquivalentTo(const CTransaction& tx) const;
// Return sum of txouts. // Return sum of txouts.
int64_t GetValueOut() const; int64_t GetValueOut() const;
// GetValueIn() is a method on CCoinsViewCache, because // GetValueIn() is a method on CCoinsViewCache, because

View File

@ -260,6 +260,7 @@ std::string HelpMessage(HelpMessageMode mode)
strUsage += " -upgradewallet " + _("Upgrade wallet to latest format") + " " + _("on startup") + "\n"; strUsage += " -upgradewallet " + _("Upgrade wallet to latest format") + " " + _("on startup") + "\n";
strUsage += " -wallet=<file> " + _("Specify wallet file (within data directory)") + " " + _("(default: wallet.dat)") + "\n"; strUsage += " -wallet=<file> " + _("Specify wallet file (within data directory)") + " " + _("(default: wallet.dat)") + "\n";
strUsage += " -walletnotify=<cmd> " + _("Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)") + "\n"; strUsage += " -walletnotify=<cmd> " + _("Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)") + "\n";
strUsage += " -respendnotify=<cmd> " + _("Execute command when a network tx respends wallet tx input (%s=respend TxID, %t=wallet TxID)") + "\n";
strUsage += " -zapwallettxes=<mode> " + _("Delete all wallet transactions and only recover those part of the blockchain through -rescan on startup") + "\n"; strUsage += " -zapwallettxes=<mode> " + _("Delete all wallet transactions and only recover those part of the blockchain through -rescan on startup") + "\n";
strUsage += " " + _("(default: 1, 1 = keep tx meta data e.g. account owner and payment request information, 2 = drop tx meta data)") + "\n"; strUsage += " " + _("(default: 1, 1 = keep tx meta data e.g. account owner and payment request information, 2 = drop tx meta data)") + "\n";
#endif #endif
@ -1175,6 +1176,7 @@ bool AppInit2(boost::thread_group& threadGroup)
LogPrintf("mapAddressBook.size() = %u\n", pwalletMain ? pwalletMain->mapAddressBook.size() : 0); LogPrintf("mapAddressBook.size() = %u\n", pwalletMain ? pwalletMain->mapAddressBook.size() : 0);
#endif #endif
RegisterInternalSignals();
StartNode(threadGroup); StartNode(threadGroup);
if (fServer) if (fServer)
StartRPCThreads(); StartRPCThreads();

View File

@ -7,6 +7,7 @@
#include "addrman.h" #include "addrman.h"
#include "alert.h" #include "alert.h"
#include "bloom.h"
#include "chainparams.h" #include "chainparams.h"
#include "checkpoints.h" #include "checkpoints.h"
#include "checkqueue.h" #include "checkqueue.h"
@ -124,6 +125,10 @@ namespace {
} // anon namespace } // anon namespace
// Forward reference functions defined here:
static const unsigned int MAX_DOUBLESPEND_BLOOM = 1000;
static void RelayDoubleSpend(const COutPoint& outPoint, const CTransaction& doubleSpend, bool fInBlock, CBloomFilter& filter);
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
// //
// dispatching functions // dispatching functions
@ -146,10 +151,25 @@ struct CMainSignals {
boost::signals2::signal<void (const uint256 &)> Inventory; boost::signals2::signal<void (const uint256 &)> Inventory;
// Tells listeners to broadcast their data. // Tells listeners to broadcast their data.
boost::signals2::signal<void ()> Broadcast; boost::signals2::signal<void ()> Broadcast;
// Notifies listeners of detection of a double-spent transaction. Arguments are outpoint that is
// double-spent, first transaction seen, double-spend transaction, and whether the second double-spend
// transaction was first seen in a block.
// Note: only notifies if the previous transaction is in the memory pool; if previous transction was in a block,
// then the double-spend simply fails when we try to lookup the inputs in the current UTXO set.
boost::signals2::signal<void (const COutPoint&, const CTransaction&, bool)> DetectedDoubleSpend;
} g_signals; } g_signals;
} // anon namespace } // anon namespace
void RegisterInternalSignals() {
static CBloomFilter doubleSpendFilter;
seed_insecure_rand();
doubleSpendFilter = CBloomFilter(MAX_DOUBLESPEND_BLOOM, 0.01, insecure_rand(), BLOOM_UPDATE_NONE);
g_signals.DetectedDoubleSpend.connect(boost::bind(RelayDoubleSpend, _1, _2, _3, doubleSpendFilter));
}
void RegisterWallet(CWalletInterface* pwalletIn) { void RegisterWallet(CWalletInterface* pwalletIn) {
g_signals.SyncTransaction.connect(boost::bind(&CWalletInterface::SyncTransaction, pwalletIn, _1, _2)); g_signals.SyncTransaction.connect(boost::bind(&CWalletInterface::SyncTransaction, pwalletIn, _1, _2));
g_signals.EraseTransaction.connect(boost::bind(&CWalletInterface::EraseFromWallet, pwalletIn, _1)); g_signals.EraseTransaction.connect(boost::bind(&CWalletInterface::EraseFromWallet, pwalletIn, _1));
@ -872,6 +892,21 @@ int64_t GetMinFee(const CTransaction& tx, unsigned int nBytes, bool fAllowFree,
return nMinFee; return nMinFee;
} }
// Exponentially limit the rate of nSize flow to nLimit. nLimit unit is thousands-per-minute.
bool RateLimitExceeded(double& dCount, int64_t& nLastTime, int64_t nLimit, unsigned int nSize)
{
static CCriticalSection csLimiter;
int64_t nNow = GetTime();
LOCK(csLimiter);
dCount *= pow(1.0 - 1.0/600.0, (double)(nNow - nLastTime));
nLastTime = nNow;
if (dCount >= nLimit*10*1000)
return true;
dCount += nSize;
return false;
}
bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransaction &tx, bool fLimitFree, bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransaction &tx, bool fLimitFree,
bool* pfMissingInputs, bool fRejectInsaneFee) bool* pfMissingInputs, bool fRejectInsaneFee)
@ -906,9 +941,10 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
for (unsigned int i = 0; i < tx.vin.size(); i++) for (unsigned int i = 0; i < tx.vin.size(); i++)
{ {
COutPoint outpoint = tx.vin[i].prevout; COutPoint outpoint = tx.vin[i].prevout;
if (pool.mapNextTx.count(outpoint)) // Does tx conflict with a member of the pool, and is it not equivalent to that member?
if (pool.mapNextTx.count(outpoint) && !tx.IsEquivalentTo(*pool.mapNextTx[outpoint].ptx))
{ {
// Disable replacement feature for now g_signals.DetectedDoubleSpend(outpoint, tx, false);
return false; return false;
} }
} }
@ -980,23 +1016,15 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
// be annoying or make others' transactions take longer to confirm. // be annoying or make others' transactions take longer to confirm.
if (fLimitFree && nFees < CTransaction::minRelayTxFee.GetFee(nSize)) if (fLimitFree && nFees < CTransaction::minRelayTxFee.GetFee(nSize))
{ {
static CCriticalSection csFreeLimiter;
static double dFreeCount; static double dFreeCount;
static int64_t nLastTime; static int64_t nLastFreeTime;
int64_t nNow = GetTime(); static int64_t nFreeLimit = GetArg("-limitfreerelay", 15);
LOCK(csFreeLimiter); if (RateLimitExceeded(dFreeCount, nLastFreeTime, nFreeLimit, nSize))
// Use an exponentially decaying ~10-minute window:
dFreeCount *= pow(1.0 - 1.0/600.0, (double)(nNow - nLastTime));
nLastTime = nNow;
// -limitfreerelay unit is thousand-bytes-per-minute
// At default rate it would take over a month to fill 1GB
if (dFreeCount >= GetArg("-limitfreerelay", 15)*10*1000)
return state.DoS(0, error("AcceptToMemoryPool : free transaction rejected by rate limiter"), return state.DoS(0, error("AcceptToMemoryPool : free transaction rejected by rate limiter"),
REJECT_INSUFFICIENTFEE, "insufficient priority"); REJECT_INSUFFICIENTFEE, "insufficient priority");
LogPrint("mempool", "Rate limit dFreeCount: %g => %g\n", dFreeCount, dFreeCount+nSize); LogPrint("mempool", "Rate limit dFreeCount: %g => %g\n", dFreeCount, dFreeCount+nSize);
dFreeCount += nSize;
} }
if (fRejectInsaneFee && nFees > CTransaction::minRelayTxFee.GetFee(nSize) * 10000) if (fRejectInsaneFee && nFees > CTransaction::minRelayTxFee.GetFee(nSize) * 10000)
@ -1019,6 +1047,48 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
return true; return true;
} }
static void RelayDoubleSpend(const COutPoint& outPoint, const CTransaction& doubleSpend, bool fInBlock, CBloomFilter& filter)
{
// Relaying double-spend attempts to our peers lets them detect when
// somebody might be trying to cheat them. However, blindly relaying
// every double-spend across the entire network gives attackers
// a denial-of-service attack: just generate a stream of double-spends
// re-spending the same (limited) set of outpoints owned by the attacker.
// So, we use a bloom filter and only relay (at most) the first double
// spend for each outpoint. False-positives ("we have already relayed")
// are OK, because if the peer doesn't hear about the double-spend
// from us they are very likely to hear about it from another peer, since
// each peer uses a different, randomized bloom filter.
if (fInBlock || filter.contains(outPoint)) return;
// Apply an independent rate limit to double-spend relays
static double dRespendCount;
static int64_t nLastRespendTime;
static int64_t nRespendLimit = GetArg("-limitrespendrelay", 100);
unsigned int nSize = ::GetSerializeSize(doubleSpend, SER_NETWORK, PROTOCOL_VERSION);
if (RateLimitExceeded(dRespendCount, nLastRespendTime, nRespendLimit, nSize))
{
LogPrint("mempool", "Double-spend relay rejected by rate limiter\n");
return;
}
LogPrint("mempool", "Rate limit dRespendCount: %g => %g\n", dRespendCount, dRespendCount+nSize);
// Clear the filter on average every MAX_DOUBLE_SPEND_BLOOM
// insertions
if (insecure_rand()%MAX_DOUBLESPEND_BLOOM == 0)
filter.clear();
filter.insert(outPoint);
RelayTransaction(doubleSpend);
// Share conflict with wallet
g_signals.SyncTransaction(doubleSpend, NULL);
}
int CMerkleTx::GetDepthInMainChainINTERNAL(CBlockIndex* &pindexRet) const int CMerkleTx::GetDepthInMainChainINTERNAL(CBlockIndex* &pindexRet) const
{ {

View File

@ -108,6 +108,9 @@ struct CNodeStateStats;
struct CBlockTemplate; struct CBlockTemplate;
/** Set up internal signal handlers **/
void RegisterInternalSignals();
/** Register a wallet to receive updates from core */ /** Register a wallet to receive updates from core */
void RegisterWallet(CWalletInterface* pwalletIn); void RegisterWallet(CWalletInterface* pwalletIn);
/** Unregister a wallet from core */ /** Unregister a wallet from core */

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

@ -19,9 +19,17 @@ class TransactionStatus
{ {
public: public:
TransactionStatus(): TransactionStatus():
countsForBalance(false), sortKey(""), countsForBalance(false),
matures_in(0), status(Offline), depth(0), open_for(0), cur_num_blocks(-1) sortKey(""),
{ } matures_in(0),
status(Offline),
hasConflicting(false),
depth(0),
open_for(0),
cur_num_blocks(-1),
cur_num_conflicts(-1)
{
}
enum Status { enum Status {
Confirmed, /**< Have 6 or more confirmations (normal tx) or fully mature (mined tx) **/ Confirmed, /**< Have 6 or more confirmations (normal tx) or fully mature (mined tx) **/
@ -51,6 +59,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 +71,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 +149,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

@ -58,6 +58,10 @@ void WalletTxToJSON(const CWalletTx& wtx, Object& entry)
BOOST_FOREACH(const uint256& conflict, wtx.GetConflicts()) BOOST_FOREACH(const uint256& conflict, wtx.GetConflicts())
conflicts.push_back(conflict.GetHex()); conflicts.push_back(conflict.GetHex());
entry.push_back(Pair("walletconflicts", conflicts)); entry.push_back(Pair("walletconflicts", conflicts));
Array respends;
BOOST_FOREACH(const uint256& respend, wtx.GetConflicts(false))
respends.push_back(respend.GetHex());
entry.push_back(Pair("respendsobserved", respends));
entry.push_back(Pair("time", wtx.GetTxTime())); entry.push_back(Pair("time", wtx.GetTxTime()));
entry.push_back(Pair("timereceived", (int64_t)wtx.nTimeReceived)); entry.push_back(Pair("timereceived", (int64_t)wtx.nTimeReceived));
BOOST_FOREACH(const PAIRTYPE(string,string)& item, wtx.mapValue) BOOST_FOREACH(const PAIRTYPE(string,string)& item, wtx.mapValue)
@ -1211,6 +1215,12 @@ Value listtransactions(const Array& params, bool fHelp)
" \"blockindex\": n, (numeric) The block index containing the transaction. Available for 'send' and 'receive'\n" " \"blockindex\": n, (numeric) The block index containing the transaction. Available for 'send' and 'receive'\n"
" category of transactions.\n" " category of transactions.\n"
" \"txid\": \"transactionid\", (string) The transaction id. Available for 'send' and 'receive' category of transactions.\n" " \"txid\": \"transactionid\", (string) The transaction id. Available for 'send' and 'receive' category of transactions.\n"
" \"walletconflicts\" : [\n"
" \"conflictid\", (string) Ids of transactions, including equivalent clones, that re-spend a txid input.\n"
" ],\n"
" \"respendsobserved\" : [\n"
" \"respendid\", (string) Ids of transactions, NOT equivalent clones, that re-spend a txid input. \"Double-spends.\"\n"
" ],\n"
" \"time\": xxx, (numeric) The transaction time in seconds since epoch (midnight Jan 1 1970 GMT).\n" " \"time\": xxx, (numeric) The transaction time in seconds since epoch (midnight Jan 1 1970 GMT).\n"
" \"timereceived\": xxx, (numeric) The time received in seconds since epoch (midnight Jan 1 1970 GMT). Available \n" " \"timereceived\": xxx, (numeric) The time received in seconds since epoch (midnight Jan 1 1970 GMT). Available \n"
" for 'send' and 'receive' category of transactions.\n" " for 'send' and 'receive' category of transactions.\n"
@ -1376,6 +1386,12 @@ Value listsinceblock(const Array& params, bool fHelp)
" \"blockindex\": n, (numeric) The block index containing the transaction. Available for 'send' and 'receive' category of transactions.\n" " \"blockindex\": n, (numeric) The block index containing the transaction. Available for 'send' and 'receive' category of transactions.\n"
" \"blocktime\": xxx, (numeric) The block time in seconds since epoch (1 Jan 1970 GMT).\n" " \"blocktime\": xxx, (numeric) The block time in seconds since epoch (1 Jan 1970 GMT).\n"
" \"txid\": \"transactionid\", (string) The transaction id. Available for 'send' and 'receive' category of transactions.\n" " \"txid\": \"transactionid\", (string) The transaction id. Available for 'send' and 'receive' category of transactions.\n"
" \"walletconflicts\" : [\n"
" \"conflictid\", (string) Ids of transactions, including equivalent clones, that re-spend a txid input.\n"
" ],\n"
" \"respendsobserved\" : [\n"
" \"respendid\", (string) Ids of transactions, NOT equivalent clones, that re-spend a txid input. \"Double-spends.\"\n"
" ],\n"
" \"time\": xxx, (numeric) The transaction time in seconds since epoch (Jan 1 1970 GMT).\n" " \"time\": xxx, (numeric) The transaction time in seconds since epoch (Jan 1 1970 GMT).\n"
" \"timereceived\": xxx, (numeric) The time received in seconds since epoch (Jan 1 1970 GMT). Available for 'send' and 'receive' category of transactions.\n" " \"timereceived\": xxx, (numeric) The time received in seconds since epoch (Jan 1 1970 GMT). Available for 'send' and 'receive' category of transactions.\n"
" \"comment\": \"...\", (string) If a comment is associated with the transaction.\n" " \"comment\": \"...\", (string) If a comment is associated with the transaction.\n"
@ -1448,6 +1464,12 @@ Value gettransaction(const Array& params, bool fHelp)
" \"blockindex\" : xx, (numeric) The block index\n" " \"blockindex\" : xx, (numeric) The block index\n"
" \"blocktime\" : ttt, (numeric) The time in seconds since epoch (1 Jan 1970 GMT)\n" " \"blocktime\" : ttt, (numeric) The time in seconds since epoch (1 Jan 1970 GMT)\n"
" \"txid\" : \"transactionid\", (string) The transaction id.\n" " \"txid\" : \"transactionid\", (string) The transaction id.\n"
" \"walletconflicts\" : [\n"
" \"conflictid\", (string) Ids of transactions, including equivalent clones, that re-spend a txid input.\n"
" ],\n"
" \"respendsobserved\" : [\n"
" \"respendid\", (string) Ids of transactions, NOT equivalent clones, that re-spend a txid input. \"Double-spends.\"\n"
" ],\n"
" \"time\" : ttt, (numeric) The transaction time in seconds since epoch (1 Jan 1970 GMT)\n" " \"time\" : ttt, (numeric) The transaction time in seconds since epoch (1 Jan 1970 GMT)\n"
" \"timereceived\" : ttt, (numeric) The time received in seconds since epoch (1 Jan 1970 GMT)\n" " \"timereceived\" : ttt, (numeric) The time received in seconds since epoch (1 Jan 1970 GMT)\n"
" \"details\" : [\n" " \"details\" : [\n"

View File

@ -45,6 +45,10 @@ BOOST_AUTO_TEST_CASE(bloom_create_insert_serialize)
expected[i] = (char)vch[i]; expected[i] = (char)vch[i];
BOOST_CHECK_EQUAL_COLLECTIONS(stream.begin(), stream.end(), expected.begin(), expected.end()); BOOST_CHECK_EQUAL_COLLECTIONS(stream.begin(), stream.end(), expected.begin(), expected.end());
BOOST_CHECK_MESSAGE( filter.contains(ParseHex("99108ad8ed9bb6274d3980bab5a85c048f0950c8")), "BloomFilter doesn't contain just-inserted object!");
filter.clear();
BOOST_CHECK_MESSAGE( !filter.contains(ParseHex("99108ad8ed9bb6274d3980bab5a85c048f0950c8")), "BloomFilter should be empty!");
} }
BOOST_AUTO_TEST_CASE(bloom_create_insert_serialize_with_tweak) BOOST_AUTO_TEST_CASE(bloom_create_insert_serialize_with_tweak)

View File

@ -415,7 +415,6 @@ void CTxMemPool::remove(const CTransaction &tx, std::list<CTransaction>& removed
void CTxMemPool::removeConflicts(const CTransaction &tx, std::list<CTransaction>& removed) void CTxMemPool::removeConflicts(const CTransaction &tx, std::list<CTransaction>& removed)
{ {
// Remove transactions which depend on inputs of tx, recursively // Remove transactions which depend on inputs of tx, recursively
list<CTransaction> result;
LOCK(cs); LOCK(cs);
BOOST_FOREACH(const CTxIn &txin, tx.vin) { BOOST_FOREACH(const CTxIn &txin, tx.vin) {
std::map<COutPoint, CInPoint>::iterator it = mapNextTx.find(txin.prevout); std::map<COutPoint, CInPoint>::iterator it = mapNextTx.find(txin.prevout);

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,7 +274,8 @@ 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)
result.insert(it->second); if (includeEquivalent || !wtx.IsEquivalentTo(mapWallet.at(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,28 @@ 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
// external respend notify
std::string strCmd = GetArg("-respendnotify", "");
if (!strCmd.empty())
{
boost::replace_all(strCmd, "%s", wtxIn.GetHash().GetHex());
boost::replace_all(strCmd, "%t", conflictHash.GetHex());
boost::thread t(runCommand, strCmd); // thread runs free
}
}
}
}
// 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 +634,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 +925,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 +945,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;
}; };