changeset 2716:ac431543ca42 draft

Refactor SelectCoinsMinConf() and add unit tests. AvailableCoins() makes a vector of available outputs which is then passed to SelectCoinsMinConf(). This allows unit tests to test the coin selection algorithm without having the whole blockchain available.
author Chris Moore <dooglus@gmail.com>
date Mon, 27 Feb 2012 04:19:32 -0800
parents 33d5155f1da3
children 28ec4e934953
files src/test/wallet_tests.cpp src/wallet.cpp src/wallet.h
diffstat 3 files changed, 343 insertions(+), 48 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/src/test/wallet_tests.cpp
@@ -0,0 +1,256 @@
+#include <boost/test/unit_test.hpp>
+
+#include "main.h"
+#include "wallet.h"
+
+// how many times to run all the tests to have a chance to catch errors that only show up with particular random shuffles
+#define RUN_TESTS 100
+
+// enables tests which currently fail, due to code which tries too hard to avoid sub-cent change
+// #define STRICT
+
+using namespace std;
+
+BOOST_AUTO_TEST_SUITE(wallet_tests)
+
+static CWallet wallet;
+static vector<COutput> vCoins;
+
+static void add_coin(int64 nValue, int nAge = 6*24, bool fIsFromMe = false, int nInput=0)
+{
+    static int i;
+    CTransaction* tx = new CTransaction;
+    tx->nLockTime = i++;        // so all transactions get different hashes
+    tx->vout.resize(nInput+1);
+    tx->vout[nInput].nValue = nValue;
+    CWalletTx* wtx = new CWalletTx(&wallet, *tx);
+    delete tx;
+    if (fIsFromMe)
+    {
+        // IsFromMe() returns (GetDebit() > 0), and GetDebit() is 0 if vin.empty(),
+        // so stop vin being empty, and cache a non-zero Debit to fake out IsFromMe()
+        wtx->vin.resize(1);
+        wtx->fDebitCached = true;
+        wtx->nDebitCached = 1;
+    }
+    COutput output(wtx, nInput, nAge);
+    vCoins.push_back(output);
+}
+
+static void empty_wallet(void)
+{
+    BOOST_FOREACH(COutput output, vCoins)
+        delete output.tx;
+    vCoins.clear();
+}
+
+BOOST_AUTO_TEST_CASE(coin_selection_tests)
+{
+    static set<pair<const CWalletTx*,unsigned int> > setCoinsRet;
+    static int64 nValueRet;
+
+    // test multiple times to allow for differences in the shuffle order
+    for (int i = 0; i < RUN_TESTS; i++)
+    {
+        empty_wallet();
+
+        // with an empty wallet we can't even pay one cent
+        BOOST_CHECK(!wallet.SelectCoinsMinConf( 1 * CENT, 1, 6, vCoins, setCoinsRet, nValueRet));
+
+        add_coin(1*CENT, 4);        // add a new 1 cent coin
+
+        // with a new 1 cent coin, we still can't find a mature 1 cent
+        BOOST_CHECK(!wallet.SelectCoinsMinConf( 1 * CENT, 1, 6, vCoins, setCoinsRet, nValueRet));
+
+        // but we can find a new 1 cent
+        BOOST_CHECK( wallet.SelectCoinsMinConf( 1 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK_EQUAL(nValueRet, 1 * CENT);
+
+        add_coin(2*CENT);           // add a mature 2 cent coin
+
+        // we can't make 3 cents of mature coins
+        BOOST_CHECK(!wallet.SelectCoinsMinConf( 3 * CENT, 1, 6, vCoins, setCoinsRet, nValueRet));
+
+        // we can make 3 cents of new  coins
+        BOOST_CHECK( wallet.SelectCoinsMinConf( 3 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK_EQUAL(nValueRet, 3 * CENT);
+
+        add_coin(5*CENT);           // add a mature 5 cent coin,
+        add_coin(10*CENT, 3, true); // a new 10 cent coin sent from one of our own addresses
+        add_coin(20*CENT);          // and a mature 20 cent coin
+
+        // now we have new: 1+10=11 (of which 10 was self-sent), and mature: 2+5+20=27.  total = 38
+
+        // we can't make 38 cents only if we disallow new coins:
+        BOOST_CHECK(!wallet.SelectCoinsMinConf(38 * CENT, 1, 6, vCoins, setCoinsRet, nValueRet));
+        // we can't even make 37 cents if we don't allow new coins even if they're from us
+        BOOST_CHECK(!wallet.SelectCoinsMinConf(38 * CENT, 6, 6, vCoins, setCoinsRet, nValueRet));
+        // but we can make 37 cents if we accept new coins from ourself
+        BOOST_CHECK( wallet.SelectCoinsMinConf(37 * CENT, 1, 6, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK_EQUAL(nValueRet, 37 * CENT);
+        // and we can make 38 cents if we accept all new coins
+        BOOST_CHECK( wallet.SelectCoinsMinConf(38 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK_EQUAL(nValueRet, 38 * CENT);
+
+        // try making 34 cents from 1,2,5,10,20 - we can't do it exactly
+        BOOST_CHECK( wallet.SelectCoinsMinConf(34 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK_GT(nValueRet, 34 * CENT);         // but should get more than 34 cents
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 3);     // the best should be 20+10+5.  it's incredibly unlikely the 1 or 2 got included (but possible)
+
+        // when we try making 7 cents, the smaller coins (1,2,5) are enough.  I'd hope to see just 2+5, but 1 gets included too
+        BOOST_CHECK( wallet.SelectCoinsMinConf( 7 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+#ifdef STRICT
+        BOOST_CHECK_EQUAL(nValueRet, 7 * CENT);
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 2);
+#else
+        BOOST_CHECK(nValueRet == 7 * CENT || nValueRet == 8 * CENT);
+        BOOST_CHECK(setCoinsRet.size() == 2 || setCoinsRet.size() == 3);
+#endif
+
+        // when we try making 8 cents, the smaller coins (1,2,5) are exactly enough.
+        BOOST_CHECK( wallet.SelectCoinsMinConf( 8 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK(nValueRet == 8 * CENT);
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 3);
+
+        // when we try making 9 cents, no subset of smaller coins is enough, and we get the next bigger coin (10)
+        BOOST_CHECK( wallet.SelectCoinsMinConf( 9 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK_EQUAL(nValueRet, 10 * CENT);
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 1);
+
+        // now clear out the wallet and start again to test chosing between subsets of smaller coins and the next biggest coin
+        empty_wallet();
+
+        add_coin( 6*CENT);
+        add_coin( 7*CENT);
+        add_coin( 8*CENT);
+        add_coin(20*CENT);
+        add_coin(30*CENT); // now we have 6+7+8+20+30 = 71 cents total
+
+        // check that we have 71 and not 72
+        BOOST_CHECK( wallet.SelectCoinsMinConf(71 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK(!wallet.SelectCoinsMinConf(72 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+
+        // now try making 16 cents.  the best smaller coins can do is 6+7+8 = 21; not as good at the next biggest coin, 20
+        BOOST_CHECK( wallet.SelectCoinsMinConf(16 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK_EQUAL(nValueRet, 20 * CENT); // we should get 20 in one coin
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 1);
+
+        add_coin( 5*CENT); // now we have 5+6+7+8+20+30 = 75 cents total
+
+        // now if we try making 16 cents again, the smaller coins can make 5+6+7 = 18 cents, better than the next biggest coin, 20
+        BOOST_CHECK( wallet.SelectCoinsMinConf(16 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK_EQUAL(nValueRet, 18 * CENT); // we should get 18 in 3 coins
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 3);
+
+        add_coin( 18*CENT); // now we have 5+6+7+8+18+20+30
+
+        // and now if we try making 16 cents again, the smaller coins can make 5+6+7 = 18 cents, the same as the next biggest coin, 18
+        BOOST_CHECK( wallet.SelectCoinsMinConf(16 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK_EQUAL(nValueRet, 18 * CENT);  // we should get 18 in 1 coin
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 1); // because in the event of a tie, the biggest coin wins
+
+        // now try making 11 cents.  we hope for 5+6 but get 5+7 due to fear of sub-cent change
+        BOOST_CHECK( wallet.SelectCoinsMinConf(11 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+#ifdef STRICT
+        BOOST_CHECK_EQUAL(nValueRet, 11 * CENT);
+#else
+        BOOST_CHECK(nValueRet == 11 * CENT || nValueRet == 12 * CENT);
+#endif
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 2);
+
+        // check that the smallest bigger coin is used
+        add_coin( 1*COIN);
+        add_coin( 2*COIN);
+        add_coin( 3*COIN);
+        add_coin( 4*COIN); // now we have 5+6+7+8+18+20+30+100+200+300+400 = 1094 cents
+        BOOST_CHECK( wallet.SelectCoinsMinConf(95 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK_EQUAL(nValueRet, 1 * COIN);  // we should get 1 bitcoin in 1 coin
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 1);
+
+        BOOST_CHECK( wallet.SelectCoinsMinConf(195 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK_EQUAL(nValueRet, 2 * COIN);  // we should get 2 bitcoins in 1 coin
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 1);
+
+        // empty the wallet and start again, now with fractions of a cent, to test sub-cent change avoidance
+        empty_wallet();
+        add_coin(0.1*CENT);
+        add_coin(0.2*CENT);
+        add_coin(0.3*CENT);
+        add_coin(0.4*CENT);
+        add_coin(0.5*CENT);
+
+        // try making 1 cent from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 = 1.5 cents
+        // we'll get sub-cent change whatever happens, so can expect 1.0 exactly
+        BOOST_CHECK( wallet.SelectCoinsMinConf(1 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK_EQUAL(nValueRet, 1 * CENT);
+
+        // but if we add a bigger coin, making it possible to avoid sub-cent change, things change:
+        add_coin(1111*CENT);
+
+        // try making 1 cent from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 1111 = 1112.5 cents
+        BOOST_CHECK( wallet.SelectCoinsMinConf(1 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+#ifdef STRICT
+        BOOST_CHECK_EQUAL(nValueRet, 1 * CENT); // we hope for the exact amount
+#else
+        BOOST_CHECK(nValueRet == 1 * CENT || nValueRet == 1111 * CENT); // but get a single 1111 cent coin
+#endif
+
+        // if we add more sub-cent coins:
+        add_coin(0.6*CENT);
+        add_coin(0.7*CENT);
+
+        // and try again to make 1.0 cents, the new coins will be used to make 2.0 cents - leaving 1.0 cent change
+        BOOST_CHECK( wallet.SelectCoinsMinConf(1 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+#ifdef STRICT
+        BOOST_CHECK_EQUAL(nValueRet, 1 * CENT); // we hope for the exact amount
+#else
+        BOOST_CHECK(nValueRet == 1 * CENT || nValueRet == 2 * CENT); // but get a bunch of sub-cent coins totalling 2 cents
+        BOOST_CHECK_GE(setCoinsRet.size(), 4); // 0.7 + 0.6 + 0.5 + 0.2 = 2.0 for example
+#endif
+
+        // run the 'mtgox' test (see http://blockexplorer.com/tx/29a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf)
+        // they tried to consolidate 10 50k coins into one 500k coin, and ended up with 50k in change
+        empty_wallet();
+        for (int i = 0; i < 20; i++)
+            add_coin(50000 * COIN);
+
+        BOOST_CHECK( wallet.SelectCoinsMinConf(500000 * COIN, 1, 1, vCoins, setCoinsRet, nValueRet));
+#ifdef STRICT
+        BOOST_CHECK_EQUAL(nValueRet, 500000 * COIN); // we hope for the exact amount
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 10); // in ten coins
+#else
+        BOOST_CHECK(nValueRet == 500000 * COIN || nValueRet == 550000 * COIN); // but get 50k too much
+        BOOST_CHECK(setCoinsRet.size() == 10 || setCoinsRet.size() == 11); // in 11 coins
+#endif
+
+        // if there's not enough in the smaller coins to make at least 1 cent change (0.5+0.6+0.7 < 1.0+1.0),
+        // we need to try finding an exact subset anyway
+
+        // sometimes it will fail, and so we use the next biggest coin:
+        empty_wallet();
+        add_coin(0.5 * CENT);
+        add_coin(0.6 * CENT);
+        add_coin(0.7 * CENT);
+        add_coin(1111 * CENT);
+        BOOST_CHECK( wallet.SelectCoinsMinConf(1 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+        BOOST_CHECK_EQUAL(nValueRet, 1111 * CENT); // we get the bigger coin
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 1);
+
+        // but sometimes it's possible, and we use an exact subset (0.4 + 0.6 = 1.0)
+        empty_wallet();
+        add_coin(0.4 * CENT);
+        add_coin(0.6 * CENT);
+        add_coin(0.8 * CENT);
+        add_coin(1111 * CENT);
+        BOOST_CHECK( wallet.SelectCoinsMinConf(1 * CENT, 1, 1, vCoins, setCoinsRet, nValueRet));
+#ifdef STRICT
+        BOOST_CHECK_EQUAL(nValueRet, 1 * CENT); // we hope for the exact amount
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 2); // in two coins 0.4+0.6
+#else
+        BOOST_CHECK_EQUAL(nValueRet, 1111 * CENT); // but since value of smaller coins < target+cent, we get the bigger coin again
+        BOOST_CHECK_EQUAL(setCoinsRet.size(), 1);
+#endif
+    }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
--- a/src/wallet.cpp
+++ b/src/wallet.cpp
@@ -889,7 +889,32 @@
     return nTotal;
 }
 
-bool CWallet::SelectCoinsMinConf(int64 nTargetValue, int nConfMine, int nConfTheirs, set<pair<const CWalletTx*,unsigned int> >& setCoinsRet, int64& nValueRet) const
+// populate vCoins with vector of spendable (age, (value, (transaction, output_number))) outputs
+void CWallet::AvailableCoins(vector<COutput>& vCoins) const
+{
+    vCoins.clear();
+
+    {
+        LOCK(cs_wallet);
+        for (map<uint256, CWalletTx>::const_iterator it = mapWallet.begin(); it != mapWallet.end(); ++it)
+        {
+            const CWalletTx* pcoin = &(*it).second;
+
+            if (!pcoin->IsFinal() || !pcoin->IsConfirmed())
+                continue;
+
+            if (pcoin->IsCoinBase() && pcoin->GetBlocksToMaturity() > 0)
+                continue;
+
+            for (int i = 0; i < pcoin->vout.size(); i++)
+                if (!(pcoin->IsSpent(i)) && IsMine(pcoin->vout[i]) && pcoin->vout[i].nValue > 0)
+                    vCoins.push_back(COutput(pcoin, i, pcoin->GetDepthInMainChain()));
+        }
+    }
+}
+
+bool CWallet::SelectCoinsMinConf(int64 nTargetValue, int nConfMine, int nConfTheirs, vector<COutput> vCoins,
+                                 set<pair<const CWalletTx*,unsigned int> >& setCoinsRet, int64& nValueRet) const
 {
     setCoinsRet.clear();
     nValueRet = 0;
@@ -901,54 +926,32 @@
     vector<pair<int64, pair<const CWalletTx*,unsigned int> > > vValue;
     int64 nTotalLower = 0;
 
+    BOOST_FOREACH(COutput output, vCoins)
     {
-       LOCK(cs_wallet);
-       vector<const CWalletTx*> vCoins;
-       vCoins.reserve(mapWallet.size());
-       for (map<uint256, CWalletTx>::const_iterator it = mapWallet.begin(); it != mapWallet.end(); ++it)
-           vCoins.push_back(&(*it).second);
-       random_shuffle(vCoins.begin(), vCoins.end(), GetRandInt);
+        const CWalletTx *pcoin = output.tx;
 
-       BOOST_FOREACH(const CWalletTx* pcoin, vCoins)
-       {
-            if (!pcoin->IsFinal() || !pcoin->IsConfirmed())
-                continue;
+        if (output.nDepth < (pcoin->IsFromMe() ? nConfMine : nConfTheirs))
+            continue;
 
-            if (pcoin->IsCoinBase() && pcoin->GetBlocksToMaturity() > 0)
-                continue;
+        int i = output.i;
+        int64 n = pcoin->vout[i].nValue;
 
-            int nDepth = pcoin->GetDepthInMainChain();
-            if (nDepth < (pcoin->IsFromMe() ? nConfMine : nConfTheirs))
-                continue;
+        pair<int64,pair<const CWalletTx*,unsigned int> > coin = make_pair(n,make_pair(pcoin, i));
 
-            for (unsigned int i = 0; i < pcoin->vout.size(); i++)
-            {
-                if (pcoin->IsSpent(i) || !IsMine(pcoin->vout[i]))
-                    continue;
-
-                int64 n = pcoin->vout[i].nValue;
-
-                if (n <= 0)
-                    continue;
-
-                pair<int64,pair<const CWalletTx*,unsigned int> > 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;
-                }
-            }
+        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;
         }
     }
 
@@ -1023,12 +1026,14 @@
                 nValueRet += vValue[i].first;
             }
 
+#ifdef DEBUG
         //// debug print
         printf("SelectCoins() best subset: ");
         for (unsigned int i = 0; i < vValue.size(); i++)
             if (vfBest[i])
                 printf("%s ", FormatMoney(vValue[i].first).c_str());
         printf("total %s\n", FormatMoney(nBest).c_str());
+#endif
     }
 
     return true;
@@ -1036,9 +1041,12 @@
 
 bool CWallet::SelectCoins(int64 nTargetValue, set<pair<const CWalletTx*,unsigned int> >& setCoinsRet, int64& nValueRet) const
 {
-    return (SelectCoinsMinConf(nTargetValue, 1, 6, setCoinsRet, nValueRet) ||
-            SelectCoinsMinConf(nTargetValue, 1, 1, setCoinsRet, nValueRet) ||
-            SelectCoinsMinConf(nTargetValue, 0, 1, setCoinsRet, nValueRet));
+    vector<COutput> vCoins;
+    AvailableCoins(vCoins);
+
+    return (SelectCoinsMinConf(nTargetValue, 1, 6, vCoins, setCoinsRet, nValueRet) ||
+            SelectCoinsMinConf(nTargetValue, 1, 1, vCoins, setCoinsRet, nValueRet) ||
+            SelectCoinsMinConf(nTargetValue, 0, 1, vCoins, setCoinsRet, nValueRet));
 }
 
 
--- a/src/wallet.h
+++ b/src/wallet.h
@@ -14,6 +14,7 @@
 class CWalletTx;
 class CReserveKey;
 class CWalletDB;
+class COutput;
 
 /** (client) version numbers for particular wallet features */
 enum WalletFeature
@@ -60,7 +61,7 @@
 class CWallet : public CCryptoKeyStore
 {
 private:
-    bool SelectCoinsMinConf(int64 nTargetValue, int nConfMine, int nConfTheirs, std::set<std::pair<const CWalletTx*,unsigned int> >& setCoinsRet, int64& nValueRet) const;
+    void AvailableCoins(std::vector<COutput>& vCoins) const;
     bool SelectCoins(int64 nTargetValue, std::set<std::pair<const CWalletTx*,unsigned int> >& setCoinsRet, int64& nValueRet) const;
 
     CWalletDB *pwalletdbEncryption;
@@ -112,6 +113,8 @@
     // check whether we are allowed to upgrade (or already support) to the named feature
     bool CanSupportFeature(enum WalletFeature wf) { return nWalletMaxVersion >= wf; }
 
+    bool SelectCoinsMinConf(int64 nTargetValue, int nConfMine, int nConfTheirs, std::vector<COutput> vCoins, std::set<std::pair<const CWalletTx*,unsigned int> >& setCoinsRet, int64& nValueRet) const;
+
     // keystore implementation
     // Generate a new key
     CPubKey GenerateNewKey();
@@ -602,6 +605,34 @@
 };
 
 
+
+
+class COutput
+{
+public:
+    const CWalletTx *tx;
+    int i;
+    int nDepth;
+
+    COutput(const CWalletTx *txIn, int iIn, int nDepthIn)
+    {
+        tx = txIn; i = iIn; nDepth = nDepthIn;
+    }
+
+    std::string ToString() const
+    {
+        return strprintf("COutput(%s, %d, %d) [%s]", tx->GetHash().ToString().substr(0,10).c_str(), i, nDepth, FormatMoney(tx->vout[i].nValue).c_str());
+    }
+
+    void print() const
+    {
+        printf("%s\n", ToString().c_str());
+    }
+};
+
+
+
+
 /** Private key that includes an expiration date in case it never gets used. */
 class CWalletKey
 {