Skip to content

Commit 8ceb28a

Browse files
committed
Merge pull request #3883 from dgenr8/first_double_spend
Relay and alert user to double spends
2 parents 6fba25e + 7a19efe commit 8ceb28a

21 files changed

+296
-38
lines changed

contrib/debian/manpages/bitcoin-qt.1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ Execute command when the best block changes (%s in cmd is replaced by block hash
139139
\fB\-walletnotify=\fR<cmd>
140140
Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)
141141
.TP
142+
\fB\-respendnotify=\fR<cmd>
143+
Execute command when a network tx respends wallet tx input (%s=respend TxID, %t=wallet TxID)
144+
.TP
142145
\fB\-alertnotify=\fR<cmd>
143146
Execute command when a relevant alert is received (%s in cmd is replaced by message)
144147
.TP

doc/release-notes.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,49 @@ estimate.
1919
Statistics used to estimate fees and priorities are saved in the
2020
data directory in the 'fee_estimates.dat' file just before
2121
program shutdown, and are read in at startup.
22+
23+
Double-Spend Relay and Alerts
24+
=============================
25+
VERY IMPORTANT: *It has never been safe, and remains unsafe, to rely*
26+
*on unconfirmed transactions.*
27+
28+
Relay
29+
-----
30+
When an attempt is seen on the network to spend the same unspent funds
31+
more than once, it is no longer ignored. Instead, it is broadcast, to
32+
serve as an alert. This broadcast is subject to protections against
33+
denial-of-service attacks.
34+
35+
Wallets and other bitcoin services should alert their users to
36+
double-spends that affect them. Merchants and other users may have
37+
enough time to withhold goods or services when payment becomes
38+
uncertain, until confirmation.
39+
40+
Bitcoin Core Wallet Alerts
41+
--------------------------
42+
The Bitcoin Core wallet now makes respend attempts visible in several
43+
ways.
44+
45+
If you are online, and a respend affecting one of your wallet
46+
transactions is seen, a notification is immediately issued to the
47+
command registered with `-respendnotify=<cmd>`. Additionally, if
48+
using the GUI:
49+
- An alert box is immediately displayed.
50+
- The affected wallet transaction is highlighted in red until it is
51+
confirmed (and it may never be confirmed).
52+
53+
A `respendsobserved` array is added to `gettransaction`, `listtransactions`,
54+
and `listsinceblock` RPC results.
55+
56+
Warning
57+
-------
58+
*If you rely on an unconfirmed transaction, these change do VERY*
59+
*LITTLE to protect you from a malicious double-spend, because:*
60+
61+
- You may learn about the respend too late to avoid doing whatever
62+
you were being paid for
63+
- Using other relay rules, a double-spender can craft his crime to
64+
resist broadcast
65+
- Miners can choose which conflicting spend to confirm, and some
66+
miners may not confirm the first acceptable spend they see
67+

src/bloom.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ bool CBloomFilter::contains(const uint256& hash) const
9494
return contains(data);
9595
}
9696

97+
void CBloomFilter::clear()
98+
{
99+
vData.assign(vData.size(),0);
100+
isFull = false;
101+
isEmpty = true;
102+
}
103+
97104
bool CBloomFilter::IsWithinSizeConstraints() const
98105
{
99106
return vData.size() <= MAX_BLOOM_FILTER_SIZE && nHashFuncs <= MAX_HASH_FUNCS;

src/bloom.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ class CBloomFilter
7878
bool contains(const COutPoint& outpoint) const;
7979
bool contains(const uint256& hash) const;
8080

81+
void clear();
82+
8183
// True if the size is <= MAX_BLOOM_FILTER_SIZE and the number of hash functions is <= MAX_HASH_FUNCS
8284
// (catch a filter which was just deserialized which was too big)
8385
bool IsWithinSizeConstraints() const;

src/core.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,22 @@ CTransaction& CTransaction::operator=(const CTransaction &tx) {
119119
return *this;
120120
}
121121

122+
bool CTransaction::IsEquivalentTo(const CTransaction& tx) const
123+
{
124+
if (nVersion != tx.nVersion ||
125+
nLockTime != tx.nLockTime ||
126+
vin.size() != tx.vin.size() ||
127+
vout != tx.vout)
128+
return false;
129+
for (unsigned int i = 0; i < vin.size(); i++)
130+
{
131+
if (vin[i].nSequence != tx.vin[i].nSequence ||
132+
vin[i].prevout != tx.vin[i].prevout)
133+
return false;
134+
}
135+
return true;
136+
}
137+
122138
int64_t CTransaction::GetValueOut() const
123139
{
124140
int64_t nValueOut = 0;

src/core.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,9 @@ class CTransaction
256256
return hash;
257257
}
258258

259+
// True if only scriptSigs are different
260+
bool IsEquivalentTo(const CTransaction& tx) const;
261+
259262
// Return sum of txouts.
260263
int64_t GetValueOut() const;
261264
// GetValueIn() is a method on CCoinsViewCache, because

src/init.cpp

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

1179+
RegisterInternalSignals();
11781180
StartNode(threadGroup);
11791181
if (fServer)
11801182
StartRPCThreads();

src/main.cpp

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include "addrman.h"
99
#include "alert.h"
10+
#include "bloom.h"
1011
#include "chainparams.h"
1112
#include "checkpoints.h"
1213
#include "checkqueue.h"
@@ -124,6 +125,10 @@ namespace {
124125

125126
} // anon namespace
126127

128+
// Forward reference functions defined here:
129+
static const unsigned int MAX_DOUBLESPEND_BLOOM = 1000;
130+
static void RelayDoubleSpend(const COutPoint& outPoint, const CTransaction& doubleSpend, bool fInBlock, CBloomFilter& filter);
131+
127132
//////////////////////////////////////////////////////////////////////////////
128133
//
129134
// dispatching functions
@@ -146,10 +151,25 @@ struct CMainSignals {
146151
boost::signals2::signal<void (const uint256 &)> Inventory;
147152
// Tells listeners to broadcast their data.
148153
boost::signals2::signal<void ()> Broadcast;
154+
// Notifies listeners of detection of a double-spent transaction. Arguments are outpoint that is
155+
// double-spent, first transaction seen, double-spend transaction, and whether the second double-spend
156+
// transaction was first seen in a block.
157+
// Note: only notifies if the previous transaction is in the memory pool; if previous transction was in a block,
158+
// then the double-spend simply fails when we try to lookup the inputs in the current UTXO set.
159+
boost::signals2::signal<void (const COutPoint&, const CTransaction&, bool)> DetectedDoubleSpend;
149160
} g_signals;
150161

151162
} // anon namespace
152163

164+
void RegisterInternalSignals() {
165+
static CBloomFilter doubleSpendFilter;
166+
seed_insecure_rand();
167+
doubleSpendFilter = CBloomFilter(MAX_DOUBLESPEND_BLOOM, 0.01, insecure_rand(), BLOOM_UPDATE_NONE);
168+
169+
g_signals.DetectedDoubleSpend.connect(boost::bind(RelayDoubleSpend, _1, _2, _3, doubleSpendFilter));
170+
}
171+
172+
153173
void RegisterWallet(CWalletInterface* pwalletIn) {
154174
g_signals.SyncTransaction.connect(boost::bind(&CWalletInterface::SyncTransaction, pwalletIn, _1, _2));
155175
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,
872892
return nMinFee;
873893
}
874894

895+
// Exponentially limit the rate of nSize flow to nLimit. nLimit unit is thousands-per-minute.
896+
bool RateLimitExceeded(double& dCount, int64_t& nLastTime, int64_t nLimit, unsigned int nSize)
897+
{
898+
static CCriticalSection csLimiter;
899+
int64_t nNow = GetTime();
900+
901+
LOCK(csLimiter);
902+
903+
dCount *= pow(1.0 - 1.0/600.0, (double)(nNow - nLastTime));
904+
nLastTime = nNow;
905+
if (dCount >= nLimit*10*1000)
906+
return true;
907+
dCount += nSize;
908+
return false;
909+
}
875910

876911
bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransaction &tx, bool fLimitFree,
877912
bool* pfMissingInputs, bool fRejectInsaneFee)
@@ -906,9 +941,10 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
906941
for (unsigned int i = 0; i < tx.vin.size(); i++)
907942
{
908943
COutPoint outpoint = tx.vin[i].prevout;
909-
if (pool.mapNextTx.count(outpoint))
944+
// Does tx conflict with a member of the pool, and is it not equivalent to that member?
945+
if (pool.mapNextTx.count(outpoint) && !tx.IsEquivalentTo(*pool.mapNextTx[outpoint].ptx))
910946
{
911-
// Disable replacement feature for now
947+
g_signals.DetectedDoubleSpend(outpoint, tx, false);
912948
return false;
913949
}
914950
}
@@ -980,23 +1016,15 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
9801016
// be annoying or make others' transactions take longer to confirm.
9811017
if (fLimitFree && nFees < CTransaction::minRelayTxFee.GetFee(nSize))
9821018
{
983-
static CCriticalSection csFreeLimiter;
9841019
static double dFreeCount;
985-
static int64_t nLastTime;
986-
int64_t nNow = GetTime();
987-
988-
LOCK(csFreeLimiter);
1020+
static int64_t nLastFreeTime;
1021+
static int64_t nFreeLimit = GetArg("-limitfreerelay", 15);
9891022

990-
// Use an exponentially decaying ~10-minute window:
991-
dFreeCount *= pow(1.0 - 1.0/600.0, (double)(nNow - nLastTime));
992-
nLastTime = nNow;
993-
// -limitfreerelay unit is thousand-bytes-per-minute
994-
// At default rate it would take over a month to fill 1GB
995-
if (dFreeCount >= GetArg("-limitfreerelay", 15)*10*1000)
1023+
if (RateLimitExceeded(dFreeCount, nLastFreeTime, nFreeLimit, nSize))
9961024
return state.DoS(0, error("AcceptToMemoryPool : free transaction rejected by rate limiter"),
9971025
REJECT_INSUFFICIENTFEE, "insufficient priority");
1026+
9981027
LogPrint("mempool", "Rate limit dFreeCount: %g => %g\n", dFreeCount, dFreeCount+nSize);
999-
dFreeCount += nSize;
10001028
}
10011029

10021030
if (fRejectInsaneFee && nFees > CTransaction::minRelayTxFee.GetFee(nSize) * 10000)
@@ -1019,6 +1047,48 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
10191047
return true;
10201048
}
10211049

1050+
static void RelayDoubleSpend(const COutPoint& outPoint, const CTransaction& doubleSpend, bool fInBlock, CBloomFilter& filter)
1051+
{
1052+
// Relaying double-spend attempts to our peers lets them detect when
1053+
// somebody might be trying to cheat them. However, blindly relaying
1054+
// every double-spend across the entire network gives attackers
1055+
// a denial-of-service attack: just generate a stream of double-spends
1056+
// re-spending the same (limited) set of outpoints owned by the attacker.
1057+
// So, we use a bloom filter and only relay (at most) the first double
1058+
// spend for each outpoint. False-positives ("we have already relayed")
1059+
// are OK, because if the peer doesn't hear about the double-spend
1060+
// from us they are very likely to hear about it from another peer, since
1061+
// each peer uses a different, randomized bloom filter.
1062+
1063+
if (fInBlock || filter.contains(outPoint)) return;
1064+
1065+
// Apply an independent rate limit to double-spend relays
1066+
static double dRespendCount;
1067+
static int64_t nLastRespendTime;
1068+
static int64_t nRespendLimit = GetArg("-limitrespendrelay", 100);
1069+
unsigned int nSize = ::GetSerializeSize(doubleSpend, SER_NETWORK, PROTOCOL_VERSION);
1070+
1071+
if (RateLimitExceeded(dRespendCount, nLastRespendTime, nRespendLimit, nSize))
1072+
{
1073+
LogPrint("mempool", "Double-spend relay rejected by rate limiter\n");
1074+
return;
1075+
}
1076+
1077+
LogPrint("mempool", "Rate limit dRespendCount: %g => %g\n", dRespendCount, dRespendCount+nSize);
1078+
1079+
// Clear the filter on average every MAX_DOUBLE_SPEND_BLOOM
1080+
// insertions
1081+
if (insecure_rand()%MAX_DOUBLESPEND_BLOOM == 0)
1082+
filter.clear();
1083+
1084+
filter.insert(outPoint);
1085+
1086+
RelayTransaction(doubleSpend);
1087+
1088+
// Share conflict with wallet
1089+
g_signals.SyncTransaction(doubleSpend, NULL);
1090+
}
1091+
10221092

10231093
int CMerkleTx::GetDepthInMainChainINTERNAL(CBlockIndex* &pindexRet) const
10241094
{

src/main.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ struct CNodeStateStats;
108108

109109
struct CBlockTemplate;
110110

111+
/** Set up internal signal handlers **/
112+
void RegisterInternalSignals();
113+
111114
/** Register a wallet to receive updates from core */
112115
void RegisterWallet(CWalletInterface* pwalletIn);
113116
/** Unregister a wallet from core */

src/qt/guiconstants.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ static const int STATUSBAR_ICONSIZE = 16;
2323
#define COLOR_NEGATIVE QColor(255, 0, 0)
2424
/* Transaction list -- bare address (without label) */
2525
#define COLOR_BAREADDRESS QColor(140, 140, 140)
26+
/* Transaction list -- has conflicting transactions */
27+
#define COLOR_HASCONFLICTING Qt::white;
28+
/* Transaction list -- has conflicting transactions - background */
29+
#define COLOR_HASCONFLICTING_BG QColor(192, 0, 0)
2630

2731
/* Tooltips longer than this (in characters) are converted into rich text,
2832
so that they can be word-wrapped.

0 commit comments

Comments
 (0)