Keep track of explicit wallet conflicts instead of using mempool

This commit is contained in:
Pieter Wuille 2015-11-26 18:42:07 +01:00
parent 5d5ef3a4cf
commit 9ac63d6d30
6 changed files with 124 additions and 26 deletions

View file

@ -215,6 +215,24 @@ of just announcing the hash. In a reorganization, all new headers are sent,
instead of just the new tip. This can often prevent an extra roundtrip before instead of just the new tip. This can often prevent an extra roundtrip before
the actual block is downloaded. the actual block is downloaded.
Negative confirmations and conflict detection
---------------------------------------------
The wallet will now report a negative number for confirmations that indicates
how deep in the block chain the conflict is found. For example, if a transaction
A has 5 confirmations and spends the same input as a wallet transaction B, B
will be reported as having -5 confirmations. If another wallet transaction C
spends an output from B, it will also be reported as having -5 confirmations.
To detect conflicts with historical transactions in the chain a one-time
`-rescan` may be needed.
Unlike earlier versions, unconfirmed but non-conflicting transactions will never
get a negative confirmation count. They are not treated as spendable unless
they're coming from ourself (change) and accepted into our local mempool,
however. The new "trusted" field in the `listtransactions` RPC output
indicates whether outputs of an unconfirmed transaction are considered
spendable.
0.12.0 Change log 0.12.0 Change log
================= =================

View file

@ -136,7 +136,7 @@ class TxnMallTest(BitcoinTestFramework):
tx2 = self.nodes[0].gettransaction(txid2) tx2 = self.nodes[0].gettransaction(txid2)
# Verify expected confirmations # Verify expected confirmations
assert_equal(tx1["confirmations"], -1) assert_equal(tx1["confirmations"], -2)
assert_equal(tx1_clone["confirmations"], 2) assert_equal(tx1_clone["confirmations"], 2)
assert_equal(tx2["confirmations"], 1) assert_equal(tx2["confirmations"], 1)

View file

@ -99,7 +99,7 @@ class TxnMallTest(BitcoinTestFramework):
# Now give doublespend and its parents to miner: # Now give doublespend and its parents to miner:
self.nodes[2].sendrawtransaction(fund_foo_tx["hex"]) self.nodes[2].sendrawtransaction(fund_foo_tx["hex"])
self.nodes[2].sendrawtransaction(fund_bar_tx["hex"]) self.nodes[2].sendrawtransaction(fund_bar_tx["hex"])
self.nodes[2].sendrawtransaction(doublespend["hex"]) doublespend_txid = self.nodes[2].sendrawtransaction(doublespend["hex"])
# ... mine a block... # ... mine a block...
self.nodes[2].generate(1) self.nodes[2].generate(1)
@ -107,14 +107,15 @@ class TxnMallTest(BitcoinTestFramework):
connect_nodes(self.nodes[1], 2) connect_nodes(self.nodes[1], 2)
self.nodes[2].generate(1) # Mine another block to make sure we sync self.nodes[2].generate(1) # Mine another block to make sure we sync
sync_blocks(self.nodes) sync_blocks(self.nodes)
assert_equal(self.nodes[0].gettransaction(doublespend_txid)["confirmations"], 2)
# Re-fetch transaction info: # Re-fetch transaction info:
tx1 = self.nodes[0].gettransaction(txid1) tx1 = self.nodes[0].gettransaction(txid1)
tx2 = self.nodes[0].gettransaction(txid2) tx2 = self.nodes[0].gettransaction(txid2)
# Both transactions should be conflicted # Both transactions should be conflicted
assert_equal(tx1["confirmations"], -1) assert_equal(tx1["confirmations"], -2)
assert_equal(tx2["confirmations"], -1) assert_equal(tx2["confirmations"], -2)
# Node0's total balance should be starting balance, plus 100BTC for # Node0's total balance should be starting balance, plus 100BTC for
# two more matured blocks, minus 1240 for the double-spend, plus fees (which are # two more matured blocks, minus 1240 for the double-spend, plus fees (which are

View file

@ -65,6 +65,8 @@ void WalletTxToJSON(const CWalletTx& wtx, UniValue& entry)
entry.push_back(Pair("blockhash", wtx.hashBlock.GetHex())); entry.push_back(Pair("blockhash", wtx.hashBlock.GetHex()));
entry.push_back(Pair("blockindex", wtx.nIndex)); entry.push_back(Pair("blockindex", wtx.nIndex));
entry.push_back(Pair("blocktime", mapBlockIndex[wtx.hashBlock]->GetBlockTime())); entry.push_back(Pair("blocktime", mapBlockIndex[wtx.hashBlock]->GetBlockTime()));
} else {
entry.push_back(Pair("trusted", wtx.IsTrusted()));
} }
uint256 hash = wtx.GetHash(); uint256 hash = wtx.GetHash();
entry.push_back(Pair("txid", hash.GetHex())); entry.push_back(Pair("txid", hash.GetHex()));
@ -1421,7 +1423,9 @@ UniValue listtransactions(const UniValue& params, bool fHelp)
" \"fee\": x.xxx, (numeric) The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the \n" " \"fee\": x.xxx, (numeric) The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the \n"
" 'send' category of transactions.\n" " 'send' category of transactions.\n"
" \"confirmations\": n, (numeric) The number of confirmations for the transaction. Available for 'send' and \n" " \"confirmations\": n, (numeric) The number of confirmations for the transaction. Available for 'send' and \n"
" 'receive' category of transactions.\n" " 'receive' category of transactions. Negative confirmations indicate the\n"
" transation conflicts with the block chain\n"
" \"trusted\": xxx (bool) Whether we consider the outputs of this unconfirmed transaction safe to spend.\n"
" \"blockhash\": \"hashvalue\", (string) The block hash containing the transaction. Available for 'send' and 'receive'\n" " \"blockhash\": \"hashvalue\", (string) The block hash containing the transaction. Available for 'send' and 'receive'\n"
" category of transactions.\n" " category of transactions.\n"
" \"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"

View file

@ -608,6 +608,14 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFromLoadWallet, CWalletD
wtx.BindWallet(this); wtx.BindWallet(this);
wtxOrdered.insert(make_pair(wtx.nOrderPos, TxPair(&wtx, (CAccountingEntry*)0))); wtxOrdered.insert(make_pair(wtx.nOrderPos, TxPair(&wtx, (CAccountingEntry*)0)));
AddToSpends(hash); AddToSpends(hash);
BOOST_FOREACH(const CTxIn& txin, wtx.vin) {
if (mapWallet.count(txin.prevout.hash)) {
CWalletTx& prevtx = mapWallet[txin.prevout.hash];
if (prevtx.nIndex == -1 && !prevtx.hashBlock.IsNull()) {
MarkConflicted(prevtx.hashBlock, wtx.GetHash());
}
}
}
} }
else else
{ {
@ -727,6 +735,20 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransaction& tx, const CBlock* pbl
{ {
{ {
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
if (pblock) {
BOOST_FOREACH(const CTxIn& txin, tx.vin) {
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(txin.prevout);
while (range.first != range.second) {
if (range.first->second != tx.GetHash()) {
LogPrintf("Transaction %s (in block %s) conflicts with wallet transaction %s (both spend %s:%i)\n", tx.GetHash().ToString(), pblock->GetHash().ToString(), range.first->second.ToString(), range.first->first.hash.ToString(), range.first->first.n);
MarkConflicted(pblock->GetHash(), range.first->second);
}
range.first++;
}
}
}
bool fExisted = mapWallet.count(tx.GetHash()) != 0; bool fExisted = mapWallet.count(tx.GetHash()) != 0;
if (fExisted && !fUpdate) return false; if (fExisted && !fUpdate) return false;
if (fExisted || IsMine(tx) || IsFromMe(tx)) if (fExisted || IsMine(tx) || IsFromMe(tx))
@ -747,9 +769,57 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransaction& tx, const CBlock* pbl
return false; return false;
} }
void CWallet::MarkConflicted(const uint256& hashBlock, const uint256& hashTx)
{
LOCK2(cs_main, cs_wallet);
CBlockIndex* pindex;
assert(mapBlockIndex.count(hashBlock));
pindex = mapBlockIndex[hashBlock];
int conflictconfirms = 0;
if (chainActive.Contains(pindex)) {
conflictconfirms = -(chainActive.Height() - pindex->nHeight + 1);
}
assert(conflictconfirms < 0);
// Do not flush the wallet here for performance reasons
CWalletDB walletdb(strWalletFile, "r+", false);
std::deque<uint256> todo;
std::set<uint256> done;
todo.push_back(hashTx);
while (!todo.empty()) {
uint256 now = todo.front();
todo.pop_front();
done.insert(now);
assert(mapWallet.count(now));
CWalletTx& wtx = mapWallet[now];
int currentconfirm = wtx.GetDepthInMainChain();
if (conflictconfirms < currentconfirm) {
// Block is 'more conflicted' than current confirm; update.
// Mark transaction as conflicted with this block.
wtx.nIndex = -1;
wtx.hashBlock = hashBlock;
wtx.MarkDirty();
wtx.WriteToDisk(&walletdb);
// Iterate over all its outputs, and mark transactions in the wallet that spend them conflicted too
TxSpends::const_iterator iter = mapTxSpends.lower_bound(COutPoint(now, 0));
while (iter != mapTxSpends.end() && iter->first.hash == now) {
if (!done.count(iter->second)) {
todo.push_back(iter->second);
}
iter++;
}
}
}
}
void CWallet::SyncTransaction(const CTransaction& tx, const CBlock* pblock) void CWallet::SyncTransaction(const CTransaction& tx, const CBlock* pblock)
{ {
LOCK2(cs_main, cs_wallet); LOCK2(cs_main, cs_wallet);
if (!AddToWalletIfInvolvingMe(tx, pblock, true)) if (!AddToWalletIfInvolvingMe(tx, pblock, true))
return; // Not one of ours return; // Not one of ours
@ -1089,7 +1159,7 @@ void CWallet::ReacceptWalletTransactions()
int nDepth = wtx.GetDepthInMainChain(); int nDepth = wtx.GetDepthInMainChain();
if (!wtx.IsCoinBase() && nDepth < 0) { if (!wtx.IsCoinBase() && nDepth == 0) {
mapSorted.insert(std::make_pair(wtx.nOrderPos, &wtx)); mapSorted.insert(std::make_pair(wtx.nOrderPos, &wtx));
} }
} }
@ -1303,6 +1373,14 @@ bool CWalletTx::IsTrusted() const
if (!bSpendZeroConfChange || !IsFromMe(ISMINE_ALL)) // using wtx's cached debit if (!bSpendZeroConfChange || !IsFromMe(ISMINE_ALL)) // using wtx's cached debit
return false; return false;
// Don't trust unconfirmed transactions from us unless they are in the mempool.
{
LOCK(mempool.cs);
if (!mempool.exists(GetHash())) {
return false;
}
}
// Trusted if all inputs are from us and are in the mempool: // Trusted if all inputs are from us and are in the mempool:
BOOST_FOREACH(const CTxIn& txin, vin) BOOST_FOREACH(const CTxIn& txin, vin)
{ {
@ -1879,6 +1957,7 @@ bool CWallet::CreateTransaction(const vector<CRecipient>& vecSend, CWalletTx& wt
//a chance at a free transaction. //a chance at a free transaction.
//But mempool inputs might still be in the mempool, so their age stays 0 //But mempool inputs might still be in the mempool, so their age stays 0
int age = pcoin.first->GetDepthInMainChain(); int age = pcoin.first->GetDepthInMainChain();
assert(age >= 0);
if (age != 0) if (age != 0)
age += 1; age += 1;
dPriority += (double)nCredit * age; dPriority += (double)nCredit * age;
@ -2814,9 +2893,9 @@ int CMerkleTx::SetMerkleBranch(const CBlock& block)
return chainActive.Height() - pindex->nHeight + 1; return chainActive.Height() - pindex->nHeight + 1;
} }
int CMerkleTx::GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet) const int CMerkleTx::GetDepthInMainChain(const CBlockIndex* &pindexRet) const
{ {
if (hashBlock.IsNull() || nIndex == -1) if (hashBlock.IsNull())
return 0; return 0;
AssertLockHeld(cs_main); AssertLockHeld(cs_main);
@ -2829,17 +2908,7 @@ int CMerkleTx::GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet) const
return 0; return 0;
pindexRet = pindex; pindexRet = pindex;
return chainActive.Height() - pindex->nHeight + 1; return ((nIndex == -1) ? (-1) : 1) * (chainActive.Height() - pindex->nHeight + 1);
}
int CMerkleTx::GetDepthInMainChain(const CBlockIndex* &pindexRet) const
{
AssertLockHeld(cs_main);
int nResult = GetDepthInMainChainINTERNAL(pindexRet);
if (nResult == 0 && !mempool.exists(GetHash()))
return -1; // Not in chain, not in mempool
return nResult;
} }
int CMerkleTx::GetBlocksToMaturity() const int CMerkleTx::GetBlocksToMaturity() const

View file

@ -156,11 +156,14 @@ struct COutputEntry
/** A transaction with a merkle branch linking it to the block chain. */ /** A transaction with a merkle branch linking it to the block chain. */
class CMerkleTx : public CTransaction class CMerkleTx : public CTransaction
{ {
private:
int GetDepthInMainChainINTERNAL(const CBlockIndex* &pindexRet) const;
public: public:
uint256 hashBlock; uint256 hashBlock;
/* An nIndex == -1 means that hashBlock (in nonzero) refers to the earliest
* block in the chain we know this or any in-wallet dependency conflicts
* with. Older clients interpret nIndex == -1 as unconfirmed for backward
* compatibility.
*/
int nIndex; int nIndex;
CMerkleTx() CMerkleTx()
@ -193,16 +196,15 @@ public:
int SetMerkleBranch(const CBlock& block); int SetMerkleBranch(const CBlock& block);
/** /**
* Return depth of transaction in blockchain: * Return depth of transaction in blockchain:
* -1 : not in blockchain, and not in memory pool (conflicted transaction) * <0 : conflicts with a transaction this deep in the blockchain
* 0 : in memory pool, waiting to be included in a block * 0 : in memory pool, waiting to be included in a block
* >=1 : this many blocks deep in the main chain * >=1 : this many blocks deep in the main chain
*/ */
int GetDepthInMainChain(const CBlockIndex* &pindexRet) const; int GetDepthInMainChain(const CBlockIndex* &pindexRet) const;
int GetDepthInMainChain() const { const CBlockIndex *pindexRet; return GetDepthInMainChain(pindexRet); } int GetDepthInMainChain() const { const CBlockIndex *pindexRet; return GetDepthInMainChain(pindexRet); }
bool IsInMainChain() const { const CBlockIndex *pindexRet; return GetDepthInMainChainINTERNAL(pindexRet) > 0; } bool IsInMainChain() const { const CBlockIndex *pindexRet; return GetDepthInMainChain(pindexRet) > 0; }
int GetBlocksToMaturity() const; int GetBlocksToMaturity() const;
bool AcceptToMemoryPool(bool fLimitFree=true, bool fRejectAbsurdFee=true); bool AcceptToMemoryPool(bool fLimitFree=true, bool fRejectAbsurdFee=true);
}; };
@ -481,6 +483,10 @@ private:
void AddToSpends(const COutPoint& outpoint, const uint256& wtxid); void AddToSpends(const COutPoint& outpoint, const uint256& wtxid);
void AddToSpends(const uint256& wtxid); void AddToSpends(const uint256& wtxid);
/* Mark a transaction (and its in-wallet descendants) as conflicting with a particular block. */
void MarkConflicted(const uint256& hashBlock, const uint256& hashTx);
void SyncMetaData(std::pair<TxSpends::iterator, TxSpends::iterator>); void SyncMetaData(std::pair<TxSpends::iterator, TxSpends::iterator>);
public: public: