Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relay and alert user to double spends #3883

Merged
merged 6 commits into from Jun 30, 2014
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions contrib/debian/manpages/bitcoin-qt.1
Expand Up @@ -139,6 +139,9 @@ Execute command when the best block changes (%s in cmd is replaced by block hash
\fB\-walletnotify=\fR<cmd>
Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)
.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>
Execute command when a relevant alert is received (%s in cmd is replaced by message)
.TP
Expand Down
46 changes: 46 additions & 0 deletions doc/release-notes.md
Expand Up @@ -19,3 +19,49 @@ estimate.
Statistics used to estimate fees and priorities are saved in the
data directory in the 'fee_estimates.dat' file just before
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 confirmg the first acceptable spend they see

7 changes: 7 additions & 0 deletions src/bloom.cpp
Expand Up @@ -94,6 +94,13 @@ bool CBloomFilter::contains(const uint256& hash) const
return contains(data);
}

void CBloomFilter::clear()
{
vData.assign(vData.size(),0);
isFull = false;
isEmpty = true;
}

bool CBloomFilter::IsWithinSizeConstraints() const
{
return vData.size() <= MAX_BLOOM_FILTER_SIZE && nHashFuncs <= MAX_HASH_FUNCS;
Expand Down
2 changes: 2 additions & 0 deletions src/bloom.h
Expand Up @@ -78,6 +78,8 @@ class CBloomFilter
bool contains(const COutPoint& outpoint) 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
// (catch a filter which was just deserialized which was too big)
bool IsWithinSizeConstraints() const;
Expand Down
16 changes: 16 additions & 0 deletions src/core.cpp
Expand Up @@ -119,6 +119,22 @@ CTransaction& CTransaction::operator=(const CTransaction &tx) {
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 nValueOut = 0;
Expand Down
3 changes: 3 additions & 0 deletions src/core.h
Expand Up @@ -256,6 +256,9 @@ class CTransaction
return hash;
}

// True if only scriptSigs are different
bool IsEquivalentTo(const CTransaction& tx) const;

// Return sum of txouts.
int64_t GetValueOut() const;
// GetValueIn() is a method on CCoinsViewCache, because
Expand Down
2 changes: 2 additions & 0 deletions src/init.cpp
Expand Up @@ -260,6 +260,7 @@ std::string HelpMessage(HelpMessageMode mode)
strUsage += " -upgradewallet " + _("Upgrade wallet to latest format") + " " + _("on startup") + "\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 += " -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 += " " + _("(default: 1, 1 = keep tx meta data e.g. account owner and payment request information, 2 = drop tx meta data)") + "\n";
#endif
Expand Down Expand Up @@ -1175,6 +1176,7 @@ bool AppInit2(boost::thread_group& threadGroup)
LogPrintf("mapAddressBook.size() = %u\n", pwalletMain ? pwalletMain->mapAddressBook.size() : 0);
#endif

RegisterInternalSignals();
StartNode(threadGroup);
if (fServer)
StartRPCThreads();
Expand Down
100 changes: 86 additions & 14 deletions src/main.cpp
Expand Up @@ -7,6 +7,7 @@

#include "addrman.h"
#include "alert.h"
#include "bloom.h"
#include "chainparams.h"
#include "checkpoints.h"
#include "checkqueue.h"
Expand Down Expand Up @@ -122,6 +123,10 @@ namespace {
map<uint256, pair<NodeId, list<uint256>::iterator> > mapBlocksToDownload;
}

// 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
Expand All @@ -143,9 +148,24 @@ struct CMainSignals {
boost::signals2::signal<void (const uint256 &)> Inventory;
// Tells listeners to broadcast their data.
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;
}

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) {
g_signals.SyncTransaction.connect(boost::bind(&CWalletInterface::SyncTransaction, pwalletIn, _1, _2));
g_signals.EraseTransaction.connect(boost::bind(&CWalletInterface::EraseFromWallet, pwalletIn, _1));
Expand Down Expand Up @@ -824,6 +844,22 @@ int64_t GetMinFee(const CTransaction& tx, unsigned int nBytes, bool fAllowFree,
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);

// Use an exponentially decaying ~10-minute window:
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* pfMissingInputs, bool fRejectInsaneFee)
Expand Down Expand Up @@ -858,9 +894,10 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
for (unsigned int i = 0; i < tx.vin.size(); i++)
{
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;
}
}
Expand Down Expand Up @@ -932,23 +969,15 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
// be annoying or make others' transactions take longer to confirm.
if (fLimitFree && nFees < CTransaction::minRelayTxFee.GetFee(nSize))
{
static CCriticalSection csFreeLimiter;
static double dFreeCount;
static int64_t nLastTime;
int64_t nNow = GetTime();

LOCK(csFreeLimiter);
static int64_t nLastFreeTime;
static int64_t nFreeLimit = GetArg("-limitfreerelay", 15);

// 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)
if (RateLimitExceeded(dFreeCount, nLastFreeTime, nFreeLimit, nSize))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Indentation

return state.DoS(0, error("AcceptToMemoryPool : free transaction rejected by rate limiter"),
REJECT_INSUFFICIENTFEE, "insufficient priority");

LogPrint("mempool", "Rate limit dFreeCount: %g => %g\n", dFreeCount, dFreeCount+nSize);
dFreeCount += nSize;
}

if (fRejectInsaneFee && nFees > CTransaction::minRelayTxFee.GetFee(nSize) * 10000)
Expand All @@ -971,6 +1000,49 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa
return true;
}

static void
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could this not be 2 lines?

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
{
Expand Down
3 changes: 3 additions & 0 deletions src/main.h
Expand Up @@ -108,6 +108,9 @@ struct CNodeStateStats;

struct CBlockTemplate;

/** Set up internal signal handlers **/
void RegisterInternalSignals();

/** Register a wallet to receive updates from core */
void RegisterWallet(CWalletInterface* pwalletIn);
/** Unregister a wallet from core */
Expand Down
4 changes: 4 additions & 0 deletions src/qt/guiconstants.h
Expand Up @@ -23,6 +23,10 @@ static const int STATUSBAR_ICONSIZE = 16;
#define COLOR_NEGATIVE QColor(255, 0, 0)
/* Transaction list -- bare address (without label) */
#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,
so that they can be word-wrapped.
Expand Down
4 changes: 2 additions & 2 deletions src/qt/transactionfilterproxy.cpp
Expand Up @@ -24,7 +24,7 @@ TransactionFilterProxy::TransactionFilterProxy(QObject *parent) :
typeFilter(ALL_TYPES),
minAmount(0),
limitRows(-1),
showInactive(true)
showInactive(false)
{
}

Expand All @@ -39,7 +39,7 @@ bool TransactionFilterProxy::filterAcceptsRow(int sourceRow, const QModelIndex &
qint64 amount = llabs(index.data(TransactionTableModel::AmountRole).toLongLong());
int status = index.data(TransactionTableModel::StatusRole).toInt();

if(!showInactive && status == TransactionStatus::Conflicted)
if(!showInactive && status == TransactionStatus::Conflicted && type == TransactionRecord::Other)
return false;
if(!(TYPE(type) & typeFilter))
return false;
Expand Down
10 changes: 7 additions & 3 deletions src/qt/transactionrecord.cpp
Expand Up @@ -170,6 +170,8 @@ void TransactionRecord::updateStatus(const CWalletTx &wtx)
status.depth = wtx.GetDepthInMainChain();
status.cur_num_blocks = chainActive.Height();

status.hasConflicting = false;

if (!IsFinalTx(wtx, chainActive.Height() + 1))
{
if (wtx.nLockTime < LOCKTIME_THRESHOLD)
Expand Down Expand Up @@ -213,6 +215,7 @@ void TransactionRecord::updateStatus(const CWalletTx &wtx)
if (status.depth < 0)
{
status.status = TransactionStatus::Conflicted;
status.hasConflicting = !(wtx.GetConflicts(false).empty());
}
else if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0)
{
Expand All @@ -221,6 +224,7 @@ void TransactionRecord::updateStatus(const CWalletTx &wtx)
else if (status.depth == 0)
{
status.status = TransactionStatus::Unconfirmed;
status.hasConflicting = !(wtx.GetConflicts(false).empty());
}
else if (status.depth < RecommendedNumConfirmations)
{
Expand All @@ -231,13 +235,13 @@ void TransactionRecord::updateStatus(const CWalletTx &wtx)
status.status = TransactionStatus::Confirmed;
}
}

}

bool TransactionRecord::statusUpdateNeeded()
bool TransactionRecord::statusUpdateNeeded(int64_t nConflictsReceived)
{
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
Expand Down