diff --git a/src/init.cpp b/src/init.cpp index 97b29a8fb..b362f1450 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1439,6 +1439,7 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) if (GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) StartTorControl(threadGroup, scheduler); + InitRespendFilter(); StartNode(threadGroup, scheduler); // Monitor the chain, and alert if we get blocks much quicker or slower than expected diff --git a/src/main.cpp b/src/main.cpp index 3cf9db618..eae3b38a8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -561,6 +561,14 @@ void FindNextBlocksToDownload(NodeId nodeid, unsigned int count, std::vector= nLimit*10*1000) + return true; + dCount += nSize; + return false; +} + +static bool RespendRelayExceeded(const CTransaction& doubleSpend) +{ + // 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 true; + } + + LogPrint("mempool", "Rate limit dRespendCount: %g => %g\n", dRespendCount, dRespendCount+nSize); + + return false; +} + bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState &state, const CTransaction &tx, bool fLimitFree, bool* pfMissingInputs, bool fOverrideMempoolLimit, bool fRejectAbsurdFee, std::vector& vHashTxnToUncache) @@ -1066,13 +1109,26 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState &state, const C return state.Invalid(false, REJECT_ALREADY_KNOWN, "txn-already-in-mempool"); // Check for conflicts with in-memory transactions + bool fRespend = false; + COutPoint relayForOutpoint; + set setConflicts; { LOCK(pool.cs); // protect pool.mapNextTx BOOST_FOREACH(const CTxIn &txin, tx.vin) { + COutPoint outpoint = txin.prevout; + // A respend is a tx that conflicts with a member of the pool if (pool.mapNextTx.count(txin.prevout)) { + fRespend = true; + // Relay only one tx per respent outpoint, but not if tx is equivalent to pool member + if (!doubleSpendFilter.contains(outpoint) && !tx.IsEquivalentTo(*pool.mapNextTx[outpoint].ptx)) + { + relayForOutpoint = outpoint; + break; + } + /* const CTransaction *ptxConflicting = pool.mapNextTx[txin.prevout].ptx; if (!setConflicts.count(ptxConflicting->GetHash())) { @@ -1105,8 +1161,12 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState &state, const C setConflicts.insert(ptxConflicting->GetHash()); } + */ + } } + if (fRespend && (relayForOutpoint.IsNull() || RespendRelayExceeded(tx))) + return false; } { @@ -1215,22 +1275,15 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState &state, const C // be annoying or make others' transactions take longer to confirm. if (fLimitFree && nModifiedFees < ::minRelayTxFee.GetFee(nSize)) { - static CCriticalSection csFreeLimiter; static double dFreeCount; - static int64_t nLastTime; - int64_t nNow = GetTime(); + static int64_t nLastFreeTime; + static int64_t nFreeLimit = GetArg("-limitfreerelay", DEFAULT_LIMITFREERELAY); - LOCK(csFreeLimiter); - - // 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", DEFAULT_LIMITFREERELAY) * 10 * 1000) + if (RateLimitExceeded(dFreeCount, nLastFreeTime, nFreeLimit, nSize)) return state.DoS(0, false, REJECT_INSUFFICIENTFEE, "rate limited free transaction"); LogPrint("mempool", "Rate limit dFreeCount: %g => %g\n", dFreeCount, dFreeCount+nSize); - dFreeCount += nSize; } if (fRejectAbsurdFee && nFees > ::minRelayTxFee.GetFee(nSize) * 10000) @@ -1434,8 +1487,19 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState &state, const C } pool.RemoveStaged(allConflicting); - // Store transaction in memory - pool.addUnchecked(hash, entry, setAncestors, !IsInitialBlockDownload()); + if (fRespend) + { + // Clear the filter on average every MAX_DOUBLE_SPEND_BLOOM insertions + if (insecure_rand()%MAX_DOUBLESPEND_BLOOM == 0) + doubleSpendFilter.clear(); + doubleSpendFilter.insert(relayForOutpoint); + RelayTransaction(tx); + } + else + { + // Store transaction in memory + pool.addUnchecked(hash, entry, !IsInitialBlockDownload()); + } // Add memory address index if (fAddressIndex) { @@ -1455,9 +1519,9 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState &state, const C } } - SyncWithWallets(tx, NULL); + SyncWithWallets(tx, NULL, fRespend); - return true; + return !fRespend; } bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransaction &tx, bool fLimitFree, @@ -3149,7 +3213,7 @@ bool static DisconnectTip(CValidationState& state, const Consensus::Params& cons // Let wallets know transactions went from 1-confirmed to // 0-confirmed or conflicted: BOOST_FOREACH(const CTransaction &tx, block.vtx) { - SyncWithWallets(tx, NULL); + SyncWithWallets(tx, NULL, false); } return true; } @@ -3208,11 +3272,11 @@ bool static ConnectTip(CValidationState& state, const CChainParams& chainparams, // Tell wallet about transactions that went from mempool // to conflicted: BOOST_FOREACH(const CTransaction &tx, txConflicted) { - SyncWithWallets(tx, NULL); + SyncWithWallets(tx, NULL, false); } // ... and about transactions that got confirmed: BOOST_FOREACH(const CTransaction &tx, pblock->vtx) { - SyncWithWallets(tx, pblock); + SyncWithWallets(tx, pblock, false); } int64_t nTime6 = GetTimeMicros(); nTimePostConnect += nTime6 - nTime5; nTimeTotal += nTime6 - nTime1; diff --git a/src/main.h b/src/main.h index d2998c841..3438510a9 100644 --- a/src/main.h +++ b/src/main.h @@ -169,6 +169,9 @@ extern CBlockIndex *pindexBestHeader; /** Minimum disk space required - used in CheckDiskSpace() */ static const uint64_t nMinDiskSpace = 52428800; +/** Initialize respend bloom filter **/ +void InitRespendFilter(); + /** Pruning-related variables and constants */ /** True if any block files have ever been pruned. */ extern bool fHavePruned; diff --git a/src/primitives/transaction.cpp b/src/primitives/transaction.cpp index a75c5f452..c3577dad4 100644 --- a/src/primitives/transaction.cpp +++ b/src/primitives/transaction.cpp @@ -89,6 +89,15 @@ CTransaction& CTransaction::operator=(const CTransaction &tx) { return *this; } +bool CTransaction::IsEquivalentTo(const CTransaction& tx) const +{ + CMutableTransaction tx1 = *this; + CMutableTransaction tx2 = tx; + for (unsigned int i = 0; i < tx1.vin.size(); i++) tx1.vin[i].scriptSig = CScript(); + for (unsigned int i = 0; i < tx2.vin.size(); i++) tx2.vin[i].scriptSig = CScript(); + return CTransaction(tx1) == CTransaction(tx2); +} + CAmount CTransaction::GetValueOut() const { CAmount nValueOut = 0; diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index 9a71ff72e..50fdc0d6f 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -272,6 +272,9 @@ public: return hash; } + // True if only scriptSigs are different + bool IsEquivalentTo(const CTransaction& tx) const; + // Return sum of txouts. CAmount GetValueOut() const; // GetValueIn() is a method on CCoinsViewCache, because diff --git a/src/qt/transactionrecord.cpp b/src/qt/transactionrecord.cpp index f9626e2e9..041e93982 100644 --- a/src/qt/transactionrecord.cpp +++ b/src/qt/transactionrecord.cpp @@ -240,7 +240,7 @@ void TransactionRecord::updateStatus(const CWalletTx &wtx) if (status.depth < 0) { status.status = TransactionStatus::Conflicted; - status.hasConflicting = !(wtx.GetConflicts().empty()); + status.hasConflicting = !(wtx.GetConflicts(false).empty()); } else if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0) { @@ -249,7 +249,7 @@ void TransactionRecord::updateStatus(const CWalletTx &wtx) else if (status.depth == 0) { status.status = TransactionStatus::Unconfirmed; - status.hasConflicting = !(wtx.GetConflicts().empty()); + status.hasConflicting = !(wtx.GetConflicts(false).empty()); if (wtx.isAbandoned()) status.status = TransactionStatus::Abandoned; } diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 97a335a99..8782678eb 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -684,7 +684,6 @@ void CTxMemPool::removeForReorg(const CCoinsViewCache *pcoins, unsigned int nMem void CTxMemPool::removeConflicts(const CTransaction &tx, std::list& removed) { // Remove transactions which depend on inputs of tx, recursively - list result; LOCK(cs); BOOST_FOREACH(const CTxIn &txin, tx.vin) { std::map::iterator it = mapNextTx.find(txin.prevout); diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp index 81f3b775f..7cc1b3ce3 100644 --- a/src/validationinterface.cpp +++ b/src/validationinterface.cpp @@ -14,7 +14,7 @@ CMainSignals& GetMainSignals() void RegisterValidationInterface(CValidationInterface* pwalletIn) { g_signals.UpdatedBlockTip.connect(boost::bind(&CValidationInterface::UpdatedBlockTip, pwalletIn, _1)); - g_signals.SyncTransaction.connect(boost::bind(&CValidationInterface::SyncTransaction, pwalletIn, _1, _2)); + g_signals.SyncTransaction.connect(boost::bind(&CValidationInterface::SyncTransaction, pwalletIn, _1, _2, _3)); g_signals.UpdatedTransaction.connect(boost::bind(&CValidationInterface::UpdatedTransaction, pwalletIn, _1)); g_signals.SetBestChain.connect(boost::bind(&CValidationInterface::SetBestChain, pwalletIn, _1)); g_signals.Inventory.connect(boost::bind(&CValidationInterface::Inventory, pwalletIn, _1)); @@ -32,7 +32,7 @@ void UnregisterValidationInterface(CValidationInterface* pwalletIn) { g_signals.Inventory.disconnect(boost::bind(&CValidationInterface::Inventory, pwalletIn, _1)); g_signals.SetBestChain.disconnect(boost::bind(&CValidationInterface::SetBestChain, pwalletIn, _1)); g_signals.UpdatedTransaction.disconnect(boost::bind(&CValidationInterface::UpdatedTransaction, pwalletIn, _1)); - g_signals.SyncTransaction.disconnect(boost::bind(&CValidationInterface::SyncTransaction, pwalletIn, _1, _2)); + g_signals.SyncTransaction.disconnect(boost::bind(&CValidationInterface::SyncTransaction, pwalletIn, _1, _2, _3)); g_signals.UpdatedBlockTip.disconnect(boost::bind(&CValidationInterface::UpdatedBlockTip, pwalletIn, _1)); } @@ -48,6 +48,6 @@ void UnregisterAllValidationInterfaces() { g_signals.UpdatedBlockTip.disconnect_all_slots(); } -void SyncWithWallets(const CTransaction &tx, const CBlock *pblock) { - g_signals.SyncTransaction(tx, pblock); +void SyncWithWallets(const CTransaction &tx, const CBlock *pblock, bool fRespend) { + g_signals.SyncTransaction(tx, pblock, fRespend); } diff --git a/src/validationinterface.h b/src/validationinterface.h index 4da145473..211bcf0c7 100644 --- a/src/validationinterface.h +++ b/src/validationinterface.h @@ -27,12 +27,12 @@ void UnregisterValidationInterface(CValidationInterface* pwalletIn); /** Unregister all wallets from core */ void UnregisterAllValidationInterfaces(); /** Push an updated transaction to all registered wallets */ -void SyncWithWallets(const CTransaction& tx, const CBlock* pblock = NULL); +void SyncWithWallets(const CTransaction& tx, const CBlock* pblock = NULL, bool fRespend = false); class CValidationInterface { protected: virtual void UpdatedBlockTip(const CBlockIndex *pindex) {} - virtual void SyncTransaction(const CTransaction &tx, const CBlock *pblock) {} + virtual void SyncTransaction(const CTransaction &tx, const CBlock *pblock, bool fRespend) {} virtual void SetBestChain(const CBlockLocator &locator) {} virtual void UpdatedTransaction(const uint256 &hash) {} virtual void Inventory(const uint256 &hash) {} @@ -48,8 +48,8 @@ protected: struct CMainSignals { /** Notifies listeners of updated block chain tip */ boost::signals2::signal UpdatedBlockTip; - /** Notifies listeners of updated transaction data (transaction, and optionally the block it is found in. */ - boost::signals2::signal SyncTransaction; + /** Notifies listeners of updated transaction data (transaction, optionally the block it is found in, and whether this is a known respend. */ + boost::signals2::signal SyncTransaction; /** Notifies listeners of an updated transaction without new data (for now: a coinbase potentially becoming visible). */ boost::signals2::signal UpdatedTransaction; /** Notifies listeners of a new active block chain. */ diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 2a68252c5..b9d7ded1e 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -95,7 +95,7 @@ void WalletTxToJSON(const CWalletTx& wtx, UniValue& entry) conflicts.push_back(conflict.GetHex()); entry.push_back(Pair("walletconflicts", conflicts)); UniValue respends; - BOOST_FOREACH(const uint256& respend, wtx.GetConflicts()) + 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())); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index a56e99e5e..977346a42 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -391,7 +391,7 @@ bool CWallet::SetMaxVersion(int nVersion) return true; } -set CWallet::GetConflicts(const uint256& txid) const +set CWallet::GetConflicts(const uint256& txid, bool includeEquivalent) const { set result; AssertLockHeld(cs_wallet); @@ -409,7 +409,8 @@ set CWallet::GetConflicts(const uint256& txid) const continue; // No conflict if zero or one spends range = mapTxSpends.equal_range(txin.prevout); 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; } @@ -1237,7 +1238,7 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFromLoadWallet, CWalletD * pblock is optional, but should be provided if the transaction is known to be in a block. * If fUpdate is true, existing transactions will be updated. */ -bool CWallet::AddToWalletIfInvolvingMe(const CTransaction& tx, const CBlock* pblock, bool fUpdate) +bool CWallet::AddToWalletIfInvolvingMe(const CTransaction& tx, const CBlock* pblock, bool fUpdate, bool fRespend) { { AssertLockHeld(cs_wallet); @@ -1257,7 +1258,15 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransaction& tx, const CBlock* pbl bool fExisted = mapWallet.count(tx.GetHash()) != 0; if (fExisted && !fUpdate) return false; - if (fExisted || IsMine(tx) || IsFromMe(tx)) + + bool fIsConflicting = IsConflicting(tx); + // Don't add respends that pay us, unless they conflict with us. Prevents resource exhaustion. + if (!fIsConflicting && fRespend) return false; + + if (fIsConflicting) + nConflictsReceived++; + + if (fExisted || IsMine(tx) || IsFromMe(tx) || fIsConflicting) { CWalletTx wtx(this,tx); @@ -1392,24 +1401,21 @@ void CWallet::MarkConflicted(const uint256& hashBlock, const uint256& hashTx) } } -void CWallet::SyncTransaction(const CTransaction& tx, const CBlock* pblock) +void void CWallet::SyncTransaction(const CTransaction& tx, const CBlock* pblock, bool fRespend) { + LOCK2(cs_main, cs_wallet); + if (!pblock) { + // wallets need to refund inputs when disconnecting coinstake + if (tx.IsCoinStake()) { + if (IsFromMe(tx)) { + DisableTransaction(tx); + return; + } + } + } - - LOCK2(cs_main, cs_wallet); - - if (!pblock) { - // wallets need to refund inputs when disconnecting coinstake - if (tx.IsCoinStake()) { - if (IsFromMe(tx)) { - DisableTransaction(tx); - return; - } - } - } - - if (!AddToWalletIfInvolvingMe(tx, pblock, true)) + if (!AddToWalletIfInvolvingMe(tx, pblock, true, fRespend)) return; // Not one of ours // If a transaction changes 'conflicted' state, that changes the balance @@ -1438,6 +1444,14 @@ isminetype CWallet::IsMine(const CTxIn &txin) const return ISMINE_NO; } +bool CWallet::IsConflicting(const CTransaction& tx) const +{ + BOOST_FOREACH(const CTxIn& txin, tx.vin) + if (mapTxSpends.count(txin.prevout)) + return true; + return false; +} + CAmount CWallet::GetDebit(const CTxIn &txin, const isminefilter& filter) const { { @@ -1774,7 +1788,7 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate) ReadBlockFromDisk(block, pindex, Params().GetConsensus()); BOOST_FOREACH(CTransaction& tx, block.vtx) { - if (AddToWalletIfInvolvingMe(tx, &block, fUpdate)) + if (AddToWalletIfInvolvingMe(tx, &block, fUpdate, false)) ret++; } pindex = chainActive.Next(pindex); @@ -1805,7 +1819,7 @@ void CWallet::ReacceptWalletTransactions() int nDepth = wtx.GetDepthInMainChain(); - if (!wtx.IsCoinBase() && !wtx.IsCoinStake() && (nDepth == 0 && !wtx.isAbandoned())) { + if (!wtx.IsCoinBase() && !wtx.IsCoinStake() && (nDepth == 0 && !wtx.isAbandoned() && (IsMine(wtx) || IsFromMe(wtx)))) { mapSorted.insert(std::make_pair(wtx.nOrderPos, &wtx)); } } @@ -1834,13 +1848,13 @@ bool CWalletTx::RelayWalletTransaction() return false; } -set CWalletTx::GetConflicts() const +set CWalletTx::GetConflicts(bool includeEquivalent) const { set result; if (pwallet != NULL) { uint256 myHash = GetHash(); - result = pwallet->GetConflicts(myHash); + result = pwallet->GetConflicts(myHash, includeEquivalent); result.erase(myHash); } return result; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index f87feb20c..63ecbc7fd 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -414,7 +414,7 @@ public: bool RelayWalletTransaction(); - std::set GetConflicts() const; + std::set GetConflicts(bool includeEquivalent=true) const; }; @@ -668,8 +668,8 @@ public: void MarkDirty(); bool AddToWallet(const CWalletTx& wtxIn, bool fFromLoadWallet, CWalletDB* pwalletdb); - void SyncTransaction(const CTransaction& tx, const CBlock* pblock); - bool AddToWalletIfInvolvingMe(const CTransaction& tx, const CBlock* pblock, bool fUpdate); + void SyncTransaction(const CTransaction& tx, const CBlock* pblock, bool fRespend); + bool AddToWalletIfInvolvingMe(const CTransaction& tx, const CBlock* pblock, bool fUpdate, bool fRespend); int ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate = false); void ReacceptWalletTransactions(); void ResendWalletTransactions(int64_t nBestBlockTime); @@ -737,6 +737,7 @@ public: bool IsMine(const CTransaction& tx) const; /** should probably be renamed to IsRelevantToMe */ bool IsFromMe(const CTransaction& tx) const; + bool IsConflicting(const CTransaction& tx) const; CAmount GetDebit(const CTransaction& tx, const isminefilter& filter) const; CAmount GetCredit(const CTransaction& tx, const isminefilter& filter) const; CAmount GetChange(const CTransaction& tx) const; @@ -789,7 +790,7 @@ public: void DisableTransaction(const CTransaction &tx); //! Get wallet transactions that conflict with given transaction (spend same outputs) - std::set GetConflicts(const uint256& txid) const; + std::set GetConflicts(const uint256& txid, bool includeEquivalent) const; //! Flush wallet (bitdb flush) void Flush(bool shutdown=false);