From 335e878be8f30ae1f7a23fbd3686fdd80c600282 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Thu, 17 Mar 2011 22:51:59 +0100 Subject: [PATCH 1/2] Spent per txout Change some internal data structures to keep track of spentness of each wallet transaction output separately, to support partially-spent transactions: * an update to the data structures (vfSpent in CWalletTx instead of fSpent) * a backward-compatible update to the wallet disk format. Old clients reading back an updated wallet will ignore partially spent transactions when creating new ones, and may report a wrong balance, though. * some helper functions (CWalletTx: IsSpent, MarkSpent, MarkDirty to reset cached values, GetAvailableCredit which only counts unredeemed outputs) --- main.cpp | 66 ++++++++++++++--------------- main.h | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 147 insertions(+), 43 deletions(-) diff --git a/main.cpp b/main.cpp index bfc45af2..6741d558 100644 --- a/main.cpp +++ b/main.cpp @@ -136,11 +136,7 @@ bool AddToWallet(const CWalletTx& wtxIn) wtx.fFromMe = wtxIn.fFromMe; fUpdated = true; } - if (wtxIn.fSpent && wtxIn.fSpent != wtx.fSpent) - { - wtx.fSpent = wtxIn.fSpent; - fUpdated = true; - } + fUpdated |= wtx.UpdateSpent(wtxIn.vfSpent); } //// debug print @@ -221,10 +217,10 @@ void WalletUpdateSpent(const COutPoint& prevout) if (mi != mapWallet.end()) { CWalletTx& wtx = (*mi).second; - if (!wtx.fSpent && wtx.vout[prevout.n].IsMine()) + if (!wtx.IsSpent(prevout.n) && wtx.vout[prevout.n].IsMine()) { printf("WalletUpdateSpent found spent coin %sbc %s\n", FormatMoney(wtx.GetCredit()).c_str(), wtx.GetHash().ToString().c_str()); - wtx.fSpent = true; + wtx.MarkSpent(prevout.n); wtx.WriteToDisk(); vWalletUpdated.push_back(prevout.hash); } @@ -939,34 +935,34 @@ void ReacceptWalletTransactions() foreach(PAIRTYPE(const uint256, CWalletTx)& item, mapWallet) { CWalletTx& wtx = item.second; - if (wtx.fSpent && wtx.IsCoinBase()) + if (wtx.IsCoinBase() && wtx.IsSpent(0)) continue; CTxIndex txindex; + bool fUpdated = false; if (txdb.ReadTxIndex(wtx.GetHash(), txindex)) { // Update fSpent if a tx got spent somewhere else by a copy of wallet.dat - if (!wtx.fSpent) + if (txindex.vSpent.size() != wtx.vout.size()) { - if (txindex.vSpent.size() != wtx.vout.size()) - { - printf("ERROR: ReacceptWalletTransactions() : txindex.vSpent.size() %d != wtx.vout.size() %d\n", txindex.vSpent.size(), wtx.vout.size()); - continue; - } - for (int i = 0; i < txindex.vSpent.size(); i++) - { - if (!txindex.vSpent[i].IsNull() && wtx.vout[i].IsMine()) - { - wtx.fSpent = true; - vMissingTx.push_back(txindex.vSpent[i]); - } - } - if (wtx.fSpent) + printf("ERROR: ReacceptWalletTransactions() : txindex.vSpent.size() %d != wtx.vout.size() %d\n", txindex.vSpent.size(), wtx.vout.size()); + continue; + } + for (int i = 0; i < txindex.vSpent.size(); i++) + { + if (!txindex.vSpent[i].IsNull() && wtx.vout[i].IsMine()) { - printf("ReacceptWalletTransactions found spent coin %sbc %s\n", FormatMoney(wtx.GetCredit()).c_str(), wtx.GetHash().ToString().c_str()); - wtx.WriteToDisk(); + wtx.MarkSpent(i); + fUpdated = true; + vMissingTx.push_back(txindex.vSpent[i]); } } + if (fUpdated) + { + printf("ReacceptWalletTransactions found spent coin %sbc %s\n", FormatMoney(wtx.GetCredit()).c_str(), wtx.GetHash().ToString().c_str()); + wtx.MarkDirty(); + wtx.WriteToDisk(); + } } else { @@ -3732,9 +3728,9 @@ int64 GetBalance() for (map::iterator it = mapWallet.begin(); it != mapWallet.end(); ++it) { CWalletTx* pcoin = &(*it).second; - if (!pcoin->IsFinal() || pcoin->fSpent || !pcoin->IsConfirmed()) + if (!pcoin->IsFinal() || !pcoin->IsConfirmed()) continue; - nTotal += pcoin->GetCredit(); + nTotal += pcoin->GetAvailableCredit(); } } @@ -3763,14 +3759,17 @@ bool SelectCoinsMinConf(int64 nTargetValue, int nConfMine, int nConfTheirs, set< foreach(CWalletTx* pcoin, vCoins) { - if (!pcoin->IsFinal() || pcoin->fSpent || !pcoin->IsConfirmed()) + if (!pcoin->IsFinal() || !pcoin->IsConfirmed()) + continue; + + if (pcoin->IsCoinBase() && pcoin->GetBlocksToMaturity() > 0) continue; int nDepth = pcoin->GetDepthInMainChain(); if (nDepth < (pcoin->IsFromMe() ? nConfMine : nConfTheirs)) continue; - int64 n = pcoin->GetCredit(); + int64 n = pcoin->GetAvailableCredit(); if (n <= 0) continue; if (n == nTargetValue) @@ -4017,12 +4016,11 @@ bool CommitTransaction(CWalletTx& wtxNew, CReserveKey& reservekey) // Mark old coins as spent set setCoins; foreach(const CTxIn& txin, wtxNew.vin) - setCoins.insert(&mapWallet[txin.prevout.hash]); - foreach(CWalletTx* pcoin, setCoins) { - pcoin->fSpent = true; - pcoin->WriteToDisk(); - vWalletUpdated.push_back(pcoin->GetHash()); + CWalletTx &pcoin = mapWallet[txin.prevout.hash]; + pcoin.MarkSpent(txin.prevout.n); + pcoin.WriteToDisk(); + vWalletUpdated.push_back(pcoin.GetHash()); } } diff --git a/main.h b/main.h index e9d0c003..0711d4ba 100644 --- a/main.h +++ b/main.h @@ -738,6 +738,7 @@ public: fMerkleVerified = false; } + IMPLEMENT_SERIALIZE ( nSerSize += SerReadWrite(s, *(CTransaction*)this, nType, nVersion, ser_action); @@ -774,15 +775,17 @@ public: unsigned int fTimeReceivedIsTxTime; unsigned int nTimeReceived; // time received by this node char fFromMe; - char fSpent; string strFromAccount; + vector vfSpent; // memory only mutable char fDebitCached; mutable char fCreditCached; + mutable char fAvailableCreditCached; mutable char fChangeCached; mutable int64 nDebitCached; mutable int64 nCreditCached; + mutable int64 nAvailableCreditCached; mutable int64 nChangeCached; // memory only UI hints @@ -814,13 +817,15 @@ public: fTimeReceivedIsTxTime = false; nTimeReceived = 0; fFromMe = false; - fSpent = false; strFromAccount.clear(); + vfSpent.clear(); fDebitCached = false; fCreditCached = false; + fAvailableCreditCached = false; fChangeCached = false; nDebitCached = 0; nCreditCached = 0; + nAvailableCreditCached = 0; nChangeCached = 0; nTimeDisplayed = 0; nLinesDisplayed = 0; @@ -832,22 +837,96 @@ public: CWalletTx* pthis = const_cast(this); if (fRead) pthis->Init(); - nSerSize += SerReadWrite(s, *(CMerkleTx*)this, nType, nVersion, ser_action); - READWRITE(vtxPrev); + char fSpent = false; - pthis->mapValue["fromaccount"] = pthis->strFromAccount; - READWRITE(mapValue); - pthis->strFromAccount = pthis->mapValue["fromaccount"]; - pthis->mapValue.erase("fromaccount"); - pthis->mapValue.erase("version"); + if (!fRead) + { + pthis->mapValue["fromaccount"] = pthis->strFromAccount; + + string str; + foreach(char f, vfSpent) + { + str += (f ? '1' : '0'); + if (f) + fSpent = true; + } + pthis->mapValue["spent"] = str; + } + nSerSize += SerReadWrite(s, *(CMerkleTx*)this, nType, nVersion,ser_action); + READWRITE(vtxPrev); + READWRITE(mapValue); READWRITE(vOrderForm); READWRITE(fTimeReceivedIsTxTime); READWRITE(nTimeReceived); READWRITE(fFromMe); READWRITE(fSpent); + + if (fRead) + { + pthis->strFromAccount = pthis->mapValue["fromaccount"]; + + if (mapValue.count("spent")) + foreach(char c, pthis->mapValue["spent"]) + pthis->vfSpent.push_back(c != '0'); + else + pthis->vfSpent.assign(vout.size(), fSpent); + } + + pthis->mapValue.erase("fromaccount"); + pthis->mapValue.erase("version"); + pthis->mapValue.erase("spent"); ) + // marks certain txout's as spent + // returns true if any update took place + bool UpdateSpent(const vector& vfNewSpent) + { + bool fReturn = false; + for (int i=0; i < vfNewSpent.size(); i++) + { + if (i == vfSpent.size()) + break; + + if (vfNewSpent[i] && !vfSpent[i]) + { + vfSpent[i] = true; + fReturn = true; + fAvailableCreditCached = false; + } + } + return fReturn; + } + + void MarkDirty() + { + fCreditCached = false; + fAvailableCreditCached = false; + fDebitCached = false; + fChangeCached = false; + } + + void MarkSpent(unsigned int nOut) + { + if (nOut >= vout.size()) + throw runtime_error("CWalletTx::MarkSpent() : nOut out of range"); + vfSpent.resize(vout.size()); + if (!vfSpent[nOut]) + { + vfSpent[nOut] = true; + fAvailableCreditCached = false; + } + } + + bool IsSpent(unsigned int nOut) const + { + if (nOut >= vout.size()) + throw runtime_error("CWalletTx::IsSpent() : nOut out of range"); + if (nOut >= vfSpent.size()) + return false; + return (!!vfSpent[nOut]); + } + int64 GetDebit() const { if (vin.empty()) @@ -873,6 +952,33 @@ public: return nCreditCached; } + int64 GetAvailableCredit(bool fUseCache=true) const + { + // Must wait until coinbase is safely deep enough in the chain before valuing it + if (IsCoinBase() && GetBlocksToMaturity() > 0) + return 0; + + if (fUseCache && fAvailableCreditCached) + return nAvailableCreditCached; + + int64 nCredit = 0; + for (int i = 0; i < vout.size(); i++) + { + if (!IsSpent(i)) + { + const CTxOut &txout = vout[i]; + nCredit += txout.GetCredit(); + if (!MoneyRange(nCredit)) + throw runtime_error("CWalletTx::GetAvailableCredit() : value out of range"); + } + } + + nAvailableCreditCached = nCredit; + fAvailableCreditCached = true; + return nCredit; + } + + int64 GetChange() const { if (fChangeCached) From aca3f961dbce34e460dc02d37969b2035d5b2260 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Thu, 17 Mar 2011 22:54:20 +0100 Subject: [PATCH 2/2] select transaction outputs separately Update to SelectCoins and CreateTransaction to select source transaction outputs separately instead of per whole transaction. --- main.cpp | 108 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 62 insertions(+), 46 deletions(-) diff --git a/main.cpp b/main.cpp index 6741d558..529dff85 100644 --- a/main.cpp +++ b/main.cpp @@ -3739,14 +3739,16 @@ int64 GetBalance() } -bool SelectCoinsMinConf(int64 nTargetValue, int nConfMine, int nConfTheirs, set& setCoinsRet) +bool SelectCoinsMinConf(int64 nTargetValue, int nConfMine, int nConfTheirs, set >& setCoinsRet, int64& nValueRet) { setCoinsRet.clear(); + nValueRet = 0; // List of values less than target - int64 nLowestLarger = INT64_MAX; - CWalletTx* pcoinLowestLarger = NULL; - vector > vValue; + pair > coinLowestLarger; + coinLowestLarger.first = INT64_MAX; + coinLowestLarger.second.first = NULL; + vector > > vValue; int64 nTotalLower = 0; CRITICAL_BLOCK(cs_mapWallet) @@ -3769,23 +3771,33 @@ bool SelectCoinsMinConf(int64 nTargetValue, int nConfMine, int nConfTheirs, set< if (nDepth < (pcoin->IsFromMe() ? nConfMine : nConfTheirs)) continue; - int64 n = pcoin->GetAvailableCredit(); - if (n <= 0) - continue; - if (n == nTargetValue) - { - setCoinsRet.insert(pcoin); - return true; - } - else if (n < nTargetValue + CENT) + for (int i = 0; i < pcoin->vout.size(); i++) { - vValue.push_back(make_pair(n, pcoin)); - nTotalLower += n; - } - else if (n < nLowestLarger) - { - nLowestLarger = n; - pcoinLowestLarger = pcoin; + if (pcoin->IsSpent(i) || !pcoin->vout[i].IsMine()) + continue; + + int64 n = pcoin->vout[i].nValue; + + if (n <= 0) + continue; + + pair > coin = make_pair(n,make_pair(pcoin,i)); + + if (n == nTargetValue) + { + setCoinsRet.insert(coin.second); + nValueRet += coin.first; + return true; + } + else if (n < nTargetValue + CENT) + { + vValue.push_back(coin); + nTotalLower += n; + } + else if (n < coinLowestLarger.first) + { + coinLowestLarger = coin; + } } } } @@ -3793,15 +3805,19 @@ bool SelectCoinsMinConf(int64 nTargetValue, int nConfMine, int nConfTheirs, set< if (nTotalLower == nTargetValue || nTotalLower == nTargetValue + CENT) { for (int i = 0; i < vValue.size(); ++i) + { setCoinsRet.insert(vValue[i].second); + nValueRet += vValue[i].first; + } return true; } - if (nTotalLower < nTargetValue + (pcoinLowestLarger ? CENT : 0)) + if (nTotalLower < nTargetValue + (coinLowestLarger.second.first ? CENT : 0)) { - if (pcoinLowestLarger == NULL) + if (coinLowestLarger.second.first == NULL) return false; - setCoinsRet.insert(pcoinLowestLarger); + setCoinsRet.insert(coinLowestLarger.second); + nValueRet += coinLowestLarger.first; return true; } @@ -3844,13 +3860,18 @@ bool SelectCoinsMinConf(int64 nTargetValue, int nConfMine, int nConfTheirs, set< } // If the next larger is still closer, return it - if (pcoinLowestLarger && nLowestLarger - nTargetValue <= nBest - nTargetValue) - setCoinsRet.insert(pcoinLowestLarger); - else + if (coinLowestLarger.second.first && coinLowestLarger.first - nTargetValue <= nBest - nTargetValue) { + setCoinsRet.insert(coinLowestLarger.second); + nValueRet += coinLowestLarger.first; + } + else { for (int i = 0; i < vValue.size(); i++) if (vfBest[i]) + { setCoinsRet.insert(vValue[i].second); + nValueRet += vValue[i].first; + } //// debug print printf("SelectCoins() best subset: "); @@ -3863,11 +3884,11 @@ bool SelectCoinsMinConf(int64 nTargetValue, int nConfMine, int nConfTheirs, set< return true; } -bool SelectCoins(int64 nTargetValue, set& setCoinsRet) +bool SelectCoins(int64 nTargetValue, set >& setCoinsRet, int64& nValueRet) { - return (SelectCoinsMinConf(nTargetValue, 1, 6, setCoinsRet) || - SelectCoinsMinConf(nTargetValue, 1, 1, setCoinsRet) || - SelectCoinsMinConf(nTargetValue, 0, 1, setCoinsRet)); + return (SelectCoinsMinConf(nTargetValue, 1, 6, setCoinsRet, nValueRet) || + SelectCoinsMinConf(nTargetValue, 1, 1, setCoinsRet, nValueRet) || + SelectCoinsMinConf(nTargetValue, 0, 1, setCoinsRet, nValueRet)); } @@ -3905,15 +3926,14 @@ bool CreateTransaction(const vector >& vecSend, CWalletTx& wtxNew.vout.push_back(CTxOut(s.second, s.first)); // Choose coins to use - set setCoins; - if (!SelectCoins(nTotalValue, setCoins)) - return false; + set > setCoins; int64 nValueIn = 0; - foreach(CWalletTx* pcoin, setCoins) + if (!SelectCoins(nTotalValue, setCoins, nValueIn)) + return false; + foreach(PAIRTYPE(CWalletTx*, unsigned int) pcoin, setCoins) { - int64 nCredit = pcoin->GetCredit(); - nValueIn += nCredit; - dPriority += (double)nCredit * pcoin->GetDepthInMainChain(); + int64 nCredit = pcoin.first->vout[pcoin.second].nValue; + dPriority += (double)nCredit * pcoin.first->GetDepthInMainChain(); } // Fill a vout back to self with any change @@ -3946,18 +3966,14 @@ bool CreateTransaction(const vector >& vecSend, CWalletTx& reservekey.ReturnKey(); // Fill vin - foreach(CWalletTx* pcoin, setCoins) - for (int nOut = 0; nOut < pcoin->vout.size(); nOut++) - if (pcoin->vout[nOut].IsMine()) - wtxNew.vin.push_back(CTxIn(pcoin->GetHash(), nOut)); + foreach(const PAIRTYPE(CWalletTx*,unsigned int)& coin, setCoins) + wtxNew.vin.push_back(CTxIn(coin.first->GetHash(),coin.second)); // Sign int nIn = 0; - foreach(CWalletTx* pcoin, setCoins) - for (int nOut = 0; nOut < pcoin->vout.size(); nOut++) - if (pcoin->vout[nOut].IsMine()) - if (!SignSignature(*pcoin, wtxNew, nIn++)) - return false; + foreach(const PAIRTYPE(CWalletTx*,unsigned int)& coin, setCoins) + if (!SignSignature(*coin.first, wtxNew, nIn++)) + return false; // Limit size unsigned int nBytes = ::GetSerializeSize(*(CTransaction*)&wtxNew, SER_NETWORK);