diff --git a/src/bench/coin_selection.cpp b/src/bench/coin_selection.cpp index 2acc2fb9c..31d359d78 100644 --- a/src/bench/coin_selection.cpp +++ b/src/bench/coin_selection.cpp @@ -1,111 +1,122 @@ // Copyright (c) 2012-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include static void addCoin(const Amount nValue, const CWallet &wallet, - std::vector &vCoins) { + std::vector &groups) { int nInput = 0; static int nextLockTime = 0; CMutableTransaction tx; // so all transactions get different hashes tx.nLockTime = nextLockTime++; tx.vout.resize(nInput + 1); tx.vout[nInput].nValue = nValue; CWalletTx *wtx = new CWalletTx(&wallet, MakeTransactionRef(std::move(tx))); int nAge = 6 * 24; COutput output(wtx, nInput, nAge, true /* spendable */, true /* solvable */, true /* safe */); - vCoins.push_back(output); + groups.emplace_back(output.GetInputCoin(), 0, false, 0, 0); } // Simple benchmark for wallet coin selection. Note that it maybe be necessary // to build up more complicated scenarios in order to get meaningful // measurements of performance. From laanwj, "Wallet coin selection is probably // the hardest, as you need a wider selection of scenarios, just testing the // same one over and over isn't too useful. Generating random isn't useful // either for measurements." // (https://github.com/bitcoin/bitcoin/issues/7883#issuecomment-224807484) static void CoinSelection(benchmark::State &state) { SelectParams(CBaseChainParams::REGTEST); const CWallet wallet(Params(), "dummy", CWalletDBWrapper::CreateDummy()); LOCK(wallet.cs_wallet); // Add coins. - std::vector vCoins; + std::vector groups; for (int i = 0; i < 1000; ++i) { - addCoin(1000 * COIN, wallet, vCoins); + addCoin(1000 * COIN, wallet, groups); } - addCoin(3 * COIN, wallet, vCoins); + addCoin(3 * COIN, wallet, groups); const CoinEligibilityFilter filter_standard(1, 6, 0); const CoinSelectionParams coin_selection_params( true, 34, 148, CFeeRate(Amount::zero()), 0); while (state.KeepRunning()) { std::set setCoinsRet; Amount nValueRet; bool bnb_used; bool success = wallet.SelectCoinsMinConf( - 1003 * COIN, filter_standard, vCoins, setCoinsRet, nValueRet, + 1003 * COIN, filter_standard, groups, setCoinsRet, nValueRet, coin_selection_params, bnb_used); assert(success); assert(nValueRet == 1003 * COIN); assert(setCoinsRet.size() == 2); } } typedef std::set CoinSet; +std::vector> wtxn; // Copied from src/wallet/test/coinselector_tests.cpp -static void add_coin(const Amount nValue, int nInput, - std::vector &set) { +static void add_coin(const CWallet &wallet, const Amount nValue, int nInput, + std::vector &set) { CMutableTransaction tx; tx.vout.resize(nInput + 1); tx.vout[nInput].nValue = nValue; - set.emplace_back(MakeTransactionRef(tx), nInput); + auto wtx = + std::make_unique(&wallet, MakeTransactionRef(std::move(tx))); + set.emplace_back( + COutput(wtx.get(), nInput, 0, true, true, true).GetInputCoin(), 0, true, + 0, 0); + wtxn.emplace_back(std::move(wtx)); } // Copied from src/wallet/test/coinselector_tests.cpp -static Amount make_hard_case(int utxos, std::vector &utxo_pool) { +static Amount make_hard_case(const CWallet &wallet, int utxos, + std::vector &utxo_pool) { utxo_pool.clear(); Amount target = Amount::zero(); for (int i = 0; i < utxos; ++i) { const Amount base = (int64_t(1) << (utxos + i)) * SATOSHI; target += base; - add_coin(base, 2 * i, utxo_pool); - add_coin(base + (int64_t(1) << (utxos - 1 - i)) * SATOSHI, 2 * i + 1, - utxo_pool); + add_coin(wallet, base, 2 * i, utxo_pool); + add_coin(wallet, base + (int64_t(1) << (utxos - 1 - i)) * SATOSHI, + 2 * i + 1, utxo_pool); } return target; } static void BnBExhaustion(benchmark::State &state) { + SelectParams(CBaseChainParams::REGTEST); + const CWallet wallet(Params(), "dummy", CWalletDBWrapper::CreateDummy()); + LOCK(wallet.cs_wallet); + // Setup - std::vector utxo_pool; + std::vector utxo_pool; CoinSet selection; Amount value_ret = Amount::zero(); Amount not_input_fees = Amount::zero(); while (state.KeepRunning()) { // Benchmark - Amount target = make_hard_case(17, utxo_pool); + Amount target = make_hard_case(wallet, 17, utxo_pool); // Should exhaust SelectCoinsBnB(utxo_pool, target, Amount::zero(), selection, value_ret, not_input_fees); // Cleanup utxo_pool.clear(); selection.clear(); } } BENCHMARK(CoinSelection, 650); BENCHMARK(BnBExhaustion, 650); diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp index 2fe7ab79a..0527ef059 100644 --- a/src/wallet/coinselection.cpp +++ b/src/wallet/coinselection.cpp @@ -1,377 +1,379 @@ // Copyright (c) 2017 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include // Descending order comparator struct { - bool operator()(const CInputCoin &a, const CInputCoin &b) const { + bool operator()(const OutputGroup &a, const OutputGroup &b) const { return a.effective_value > b.effective_value; } } descending; /** * This is the Branch and Bound Coin Selection algorithm designed by Murch. It * searches for an input set that can pay for the spending target and does not * exceed the spending target by more than the cost of creating and spending a * change output. The algorithm uses a depth-first search on a binary tree. In * the binary tree, each node corresponds to the inclusion or the omission of a * UTXO. UTXOs are sorted by their effective values and the trees is explored * deterministically per the inclusion branch first. At each node, the algorithm * checks whether the selection is within the target range. While the selection * has not reached the target range, more UTXOs are included. When a selection's * value exceeds the target range, the complete subtree deriving from this * selection can be omitted. At that point, the last included UTXO is deselected * and the corresponding omission branch explored instead. The search ends after * the complete tree has been searched or after a limited number of tries. * * The search continues to search for better solutions after one solution has * been found. The best solution is chosen by minimizing the waste metric. The * waste metric is defined as the cost to spend the current inputs at the given * fee rate minus the long term expected cost to spend the inputs, plus the * amount the selection exceeds the spending target: * * waste = selectionTotal - target + inputs × (currentFeeRate - longTermFeeRate) * * The algorithm uses two additional optimizations. A lookahead keeps track of * the total value of the unexplored UTXOs. A subtree is not explored if the * lookahead indicates that the target range cannot be reached. Further, it is * unnecessary to test equivalent combinations. This allows us to skip testing * the inclusion of UTXOs that match the effective value and waste of an omitted * predecessor. * * The Branch and Bound algorithm is described in detail in Murch's Master * Thesis: * https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf * * @param const std::vector& utxo_pool The set of UTXOs that we are * choosing from. These UTXOs will be sorted in descending order by effective * value and the CInputCoins' values are their effective values. * @param const Amount& target_value This is the value that we want to select. * It is the lower bound of the range. * @param const Amount& cost_of_change This is the cost of creating and * spending a change output. This plus target_value is the upper bound of the * range. * @param std::set& out_set -> This is an output parameter for the * set of CInputCoins that have been selected. * @param Amount& value_ret -> This is an output parameter for the total value * of the CInputCoins that were selected. * @param Amount not_input_fees -> The fees that need to be paid for the * outputs and fixed size overhead (version, locktime, marker and flag) */ static const size_t TOTAL_TRIES = 100000; -bool SelectCoinsBnB(std::vector &utxo_pool, +bool SelectCoinsBnB(std::vector &utxo_pool, const Amount &target_value, const Amount &cost_of_change, std::set &out_set, Amount &value_ret, const Amount not_input_fees) { out_set.clear(); Amount curr_value = Amount::zero(); // select the utxo at this index std::vector curr_selection; curr_selection.reserve(utxo_pool.size()); Amount actual_target = not_input_fees + target_value; // Calculate curr_available_value Amount curr_available_value = Amount::zero(); - for (const CInputCoin &utxo : utxo_pool) { + for (const OutputGroup &utxo : utxo_pool) { // Assert that this utxo is not negative. It should never be negative, // effective value calculation should have removed it assert(utxo.effective_value > Amount::zero()); curr_available_value += utxo.effective_value; } if (curr_available_value < actual_target) { return false; } // Sort the utxo_pool std::sort(utxo_pool.begin(), utxo_pool.end(), descending); Amount curr_waste = Amount::zero(); std::vector best_selection; Amount best_waste = MAX_MONEY; // Depth First search loop for choosing the UTXOs for (size_t i = 0; i < TOTAL_TRIES; ++i) { // Conditions for starting a backtrack bool backtrack = false; if (curr_value + curr_available_value < actual_target || // Cannot possibly reach target with the amount // remaining in the curr_available_value. curr_value > actual_target + cost_of_change || // Selected value is out of range, go back // and try other branch (curr_waste > best_waste && (utxo_pool.at(0).fee - utxo_pool.at(0).long_term_fee) > Amount::zero())) { // Don't select things which we know will be more wasteful if the // waste is increasing backtrack = true; } // Selected value is within range else if (curr_value >= actual_target) { // This is the excess value which is added to the waste for the // below comparison. Adding another UTXO after this check could // bring the waste down if the long term fee is higher than the // current fee. However we are not going to explore that because // this optimization for the waste is only done when we have hit our // target value. Adding any more UTXOs will be just burning the // UTXO; it will go entirely to fees. Thus we aren't going to // explore any more UTXOs to avoid burning money like that. curr_waste += (curr_value - actual_target); if (curr_waste <= best_waste) { best_selection = curr_selection; best_selection.resize(utxo_pool.size()); best_waste = curr_waste; } // Remove the excess value as we will be selecting different coins // now curr_waste -= (curr_value - actual_target); backtrack = true; } // Backtracking, moving backwards if (backtrack) { // Walk backwards to find the last included UTXO that still needs to // have its omission branch traversed. while (!curr_selection.empty() && !curr_selection.back()) { curr_selection.pop_back(); curr_available_value += utxo_pool.at(curr_selection.size()).effective_value; } if (curr_selection.empty()) { // We have walked back to the first utxo and no branch is // untraversed. All solutions searched break; } // Output was included on previous iterations, try excluding now. curr_selection.back() = false; - CInputCoin &utxo = utxo_pool.at(curr_selection.size() - 1); + OutputGroup &utxo = utxo_pool.at(curr_selection.size() - 1); curr_value -= utxo.effective_value; curr_waste -= utxo.fee - utxo.long_term_fee; } // Moving forwards, continuing down this branch else { - CInputCoin &utxo = utxo_pool.at(curr_selection.size()); + OutputGroup &utxo = utxo_pool.at(curr_selection.size()); // Remove this utxo from the curr_available_value utxo amount curr_available_value -= utxo.effective_value; // Avoid searching a branch if the previous UTXO has the same value // and same waste and was excluded. Since the ratio of fee to long // term fee is the same, we only need to check if one of those // values match in order to know that the waste is the same. if (!curr_selection.empty() && !curr_selection.back() && utxo.effective_value == utxo_pool.at(curr_selection.size() - 1).effective_value && utxo.fee == utxo_pool.at(curr_selection.size() - 1).fee) { curr_selection.push_back(false); } else { // Inclusion branch first (Largest First Exploration) curr_selection.push_back(true); curr_value += utxo.effective_value; curr_waste += utxo.fee - utxo.long_term_fee; } } } // Check for solution if (best_selection.empty()) { return false; } // Set output set value_ret = Amount::zero(); for (size_t i = 0; i < best_selection.size(); ++i) { if (best_selection.at(i)) { - out_set.insert(utxo_pool.at(i)); - value_ret += utxo_pool.at(i).txout.nValue; + util::insert(out_set, utxo_pool.at(i).m_outputs); + value_ret += utxo_pool.at(i).m_value; } } return true; } -static void ApproximateBestSubset(const std::vector &vValue, +static void ApproximateBestSubset(const std::vector &groups, const Amount &nTotalLower, const Amount &nTargetValue, std::vector &vfBest, Amount &nBest, int iterations = 1000) { std::vector vfIncluded; - vfBest.assign(vValue.size(), true); + vfBest.assign(groups.size(), true); nBest = nTotalLower; FastRandomContext insecure_rand; for (int nRep = 0; nRep < iterations && nBest != nTargetValue; nRep++) { - vfIncluded.assign(vValue.size(), false); + vfIncluded.assign(groups.size(), false); Amount nTotal = Amount::zero(); bool fReachedTarget = false; for (int nPass = 0; nPass < 2 && !fReachedTarget; nPass++) { - for (size_t i = 0; i < vValue.size(); i++) { + for (size_t i = 0; i < groups.size(); i++) { // The solver here uses a randomized algorithm, the randomness // serves no real security purpose but is just needed to prevent // degenerate behavior and it is important that the rng is fast. // We do not use a constant random sequence, because there may // be some privacy improvement by making the selection random. if (nPass == 0 ? insecure_rand.randbool() : !vfIncluded[i]) { - nTotal += vValue[i].txout.nValue; + nTotal += groups[i].m_value; vfIncluded[i] = true; if (nTotal >= nTargetValue) { fReachedTarget = true; if (nTotal < nBest) { nBest = nTotal; vfBest = vfIncluded; } - nTotal -= vValue[i].txout.nValue; + nTotal -= groups[i].m_value; vfIncluded[i] = false; } } } } } } -bool KnapsackSolver(const Amount nTargetValue, std::vector &vCoins, +bool KnapsackSolver(const Amount nTargetValue, std::vector &groups, std::set &setCoinsRet, Amount &nValueRet) { setCoinsRet.clear(); nValueRet = Amount::zero(); // List of values less than target - boost::optional coinLowestLarger; - std::vector vValue; + boost::optional lowest_larger; + std::vector applicable_groups; Amount nTotalLower = Amount::zero(); - random_shuffle(vCoins.begin(), vCoins.end(), GetRandInt); + random_shuffle(groups.begin(), groups.end(), GetRandInt); - for (const CInputCoin &coin : vCoins) { - if (coin.txout.nValue == nTargetValue) { - setCoinsRet.insert(coin); - nValueRet += coin.txout.nValue; + for (const OutputGroup &group : groups) { + if (group.m_value == nTargetValue) { + util::insert(setCoinsRet, group.m_outputs); + nValueRet += group.m_value; return true; - } else if (coin.txout.nValue < nTargetValue + MIN_CHANGE) { - vValue.push_back(coin); - nTotalLower += coin.txout.nValue; - } else if (!coinLowestLarger || - coin.txout.nValue < coinLowestLarger->txout.nValue) { - coinLowestLarger = coin; + } else if (group.m_value < nTargetValue + MIN_CHANGE) { + applicable_groups.push_back(group); + nTotalLower += group.m_value; + } else if (!lowest_larger || group.m_value < lowest_larger->m_value) { + lowest_larger = group; } } if (nTotalLower == nTargetValue) { - for (const auto &input : vValue) { - setCoinsRet.insert(input); - nValueRet += input.txout.nValue; + for (const auto &group : applicable_groups) { + util::insert(setCoinsRet, group.m_outputs); + nValueRet += group.m_value; } return true; } if (nTotalLower < nTargetValue) { - if (!coinLowestLarger) { + if (!lowest_larger) { return false; } - setCoinsRet.insert(coinLowestLarger.get()); - nValueRet += coinLowestLarger->txout.nValue; + util::insert(setCoinsRet, lowest_larger->m_outputs); + nValueRet += lowest_larger->m_value; return true; } // Solve subset sum by stochastic approximation - std::sort(vValue.begin(), vValue.end(), descending); + std::sort(applicable_groups.begin(), applicable_groups.end(), descending); std::vector vfBest; Amount nBest; - ApproximateBestSubset(vValue, nTotalLower, nTargetValue, vfBest, nBest); + ApproximateBestSubset(applicable_groups, nTotalLower, nTargetValue, vfBest, + nBest); if (nBest != nTargetValue && nTotalLower >= nTargetValue + MIN_CHANGE) { - ApproximateBestSubset(vValue, nTotalLower, nTargetValue + MIN_CHANGE, - vfBest, nBest); + ApproximateBestSubset(applicable_groups, nTotalLower, + nTargetValue + MIN_CHANGE, vfBest, nBest); } // If we have a bigger coin and (either the stochastic approximation didn't // find a good solution, or the next bigger coin is closer), return the // bigger coin - if (coinLowestLarger && + if (lowest_larger && ((nBest != nTargetValue && nBest < nTargetValue + MIN_CHANGE) || - coinLowestLarger->txout.nValue <= nBest)) { - setCoinsRet.insert(coinLowestLarger.get()); - nValueRet += coinLowestLarger->txout.nValue; + lowest_larger->m_value <= nBest)) { + util::insert(setCoinsRet, lowest_larger->m_outputs); + nValueRet += lowest_larger->m_value; } else { - for (size_t i = 0; i < vValue.size(); i++) { + for (size_t i = 0; i < applicable_groups.size(); i++) { if (vfBest[i]) { - setCoinsRet.insert(vValue[i]); - nValueRet += vValue[i].txout.nValue; + util::insert(setCoinsRet, applicable_groups[i].m_outputs); + nValueRet += applicable_groups[i].m_value; } } if (LogAcceptCategory(BCLog::SELECTCOINS)) { + /* Continued */ LogPrint(BCLog::SELECTCOINS, "SelectCoins() best subset: "); - for (size_t i = 0; i < vValue.size(); i++) { + for (size_t i = 0; i < applicable_groups.size(); i++) { if (vfBest[i]) { + /* Continued */ LogPrint(BCLog::SELECTCOINS, "%s ", - FormatMoney(vValue[i].txout.nValue)); + FormatMoney(applicable_groups[i].m_value)); } } LogPrint(BCLog::SELECTCOINS, "total %s\n", FormatMoney(nBest)); } } return true; } /****************************************************************************** OutputGroup ******************************************************************************/ void OutputGroup::Insert(const CInputCoin &output, int depth, bool from_me, size_t ancestors, size_t descendants) { m_outputs.push_back(output); m_from_me &= from_me; m_value += output.effective_value; m_depth = std::min(m_depth, depth); // m_ancestors is currently the max ancestor count for all coins in the // group; however, this is not ideal, as a wallet will consider e.g. thirty // 2-ancestor coins as having two ancestors, when in reality it has 60 // ancestors. m_ancestors = std::max(m_ancestors, ancestors); // m_descendants is the count as seen from the top ancestor, not the // descendants as seen from the coin itself; thus, this value is accurate m_descendants = std::max(m_descendants, descendants); effective_value = m_value; } std::vector::iterator OutputGroup::Discard(const CInputCoin &output) { auto it = m_outputs.begin(); while (it != m_outputs.end() && it->outpoint != output.outpoint) { ++it; } if (it == m_outputs.end()) { return it; } m_value -= output.effective_value; effective_value -= output.effective_value; return m_outputs.erase(it); } bool OutputGroup::EligibleForSpending( const CoinEligibilityFilter &eligibility_filter) const { return m_depth >= (m_from_me ? eligibility_filter.conf_mine : eligibility_filter.conf_theirs) && m_ancestors <= eligibility_filter.max_ancestors && m_descendants <= eligibility_filter.max_descendants; } diff --git a/src/wallet/coinselection.h b/src/wallet/coinselection.h index 2b4a82b5a..21569589c 100644 --- a/src/wallet/coinselection.h +++ b/src/wallet/coinselection.h @@ -1,115 +1,115 @@ // Copyright (c) 2017 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_WALLET_COINSELECTION_H #define BITCOIN_WALLET_COINSELECTION_H #include #include #include //! target minimum change amount static const Amount MIN_CHANGE = CENT; //! final minimum change amount after paying for fees static const Amount MIN_FINAL_CHANGE = MIN_CHANGE / 2; class CInputCoin { public: CInputCoin(const CTransactionRef &tx, unsigned int i) { if (!tx) { throw std::invalid_argument("tx should not be null"); } if (i >= tx->vout.size()) { throw std::out_of_range("The output index is out of range"); } outpoint = COutPoint(tx->GetId(), i); txout = tx->vout[i]; effective_value = txout.nValue; } CInputCoin(const CTransactionRef &tx, unsigned int i, int input_bytes) : CInputCoin(tx, i) { m_input_bytes = input_bytes; } COutPoint outpoint; CTxOut txout; Amount effective_value; Amount fee = Amount::zero(); Amount long_term_fee = Amount::zero(); /** * Pre-computed estimated size of this output as a fully-signed input in a * transaction. Can be -1 if it could not be calculated. */ int m_input_bytes{-1}; bool operator<(const CInputCoin &rhs) const { return outpoint < rhs.outpoint; } bool operator!=(const CInputCoin &rhs) const { return outpoint != rhs.outpoint; } bool operator==(const CInputCoin &rhs) const { return outpoint == rhs.outpoint; } }; struct CoinEligibilityFilter { const int conf_mine; const int conf_theirs; const uint64_t max_ancestors; const uint64_t max_descendants; CoinEligibilityFilter(int conf_mine_, int conf_theirs_, uint64_t max_ancestors_) : conf_mine(conf_mine_), conf_theirs(conf_theirs_), max_ancestors(max_ancestors_), max_descendants(max_ancestors_) {} CoinEligibilityFilter(int conf_mine_, int conf_theirs_, uint64_t max_ancestors_, uint64_t max_descendants_) : conf_mine(conf_mine_), conf_theirs(conf_theirs_), max_ancestors(max_ancestors_), max_descendants(max_descendants_) {} }; struct OutputGroup { std::vector m_outputs; bool m_from_me{true}; Amount m_value = Amount::zero(); int m_depth{999}; size_t m_ancestors{0}; size_t m_descendants{0}; Amount effective_value = Amount::zero(); Amount fee = Amount::zero(); Amount long_term_fee = Amount::zero(); OutputGroup() {} OutputGroup(std::vector &&outputs, bool from_me, Amount value, int depth, size_t ancestors, size_t descendants) : m_outputs(std::move(outputs)), m_from_me(from_me), m_value(value), m_depth(depth), m_ancestors(ancestors), m_descendants(descendants) {} OutputGroup(const CInputCoin &output, int depth, bool from_me, size_t ancestors, size_t descendants) : OutputGroup() { Insert(output, depth, from_me, ancestors, descendants); } void Insert(const CInputCoin &output, int depth, bool from_me, size_t ancestors, size_t descendants); std::vector::iterator Discard(const CInputCoin &output); bool EligibleForSpending(const CoinEligibilityFilter &eligibility_filter) const; }; -bool SelectCoinsBnB(std::vector &utxo_pool, +bool SelectCoinsBnB(std::vector &utxo_pool, const Amount &target_value, const Amount &cost_of_change, std::set &out_set, Amount &value_ret, const Amount not_input_fees); // Original coin selection algorithm as a fallback -bool KnapsackSolver(const Amount nTargetValue, std::vector &vCoins, +bool KnapsackSolver(const Amount nTargetValue, std::vector &groups, std::set &setCoinsRet, Amount &nValueRet); #endif // BITCOIN_WALLET_COINSELECTION_H diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp index 66759e6f4..0a7bf5e1f 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -1,724 +1,749 @@ // Copyright (c) 2017 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include // For Params #include #include #include #include #include #include #include #include #include BOOST_FIXTURE_TEST_SUITE(coinselector_tests, WalletTestingSetup) // 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 // some tests fail 1% of the time due to bad luck. // we repeat those tests this many times and only complain if all iterations of // the test fail #define RANDOM_REPEATS 5 std::vector> wtxn; typedef std::set CoinSet; static std::vector vCoins; static Amount balance = Amount::zero(); CoinEligibilityFilter filter_standard(1, 6, 0); CoinEligibilityFilter filter_confirmed(1, 1, 0); CoinEligibilityFilter filter_standard_extra(6, 6, 0); CoinSelectionParams coin_selection_params(false, 0, 0, CFeeRate(Amount::zero()), 0); static void add_coin(const Amount nValue, int nInput, std::vector &set) { CMutableTransaction tx; tx.vout.resize(nInput + 1); tx.vout[nInput].nValue = nValue; set.emplace_back(MakeTransactionRef(tx), nInput); } static void add_coin(const Amount nValue, int nInput, CoinSet &set) { CMutableTransaction tx; tx.vout.resize(nInput + 1); tx.vout[nInput].nValue = nValue; set.emplace(MakeTransactionRef(tx), nInput); } static void add_coin(CWallet &wallet, const Amount nValue, int nAge = 6 * 24, bool fIsFromMe = false, int nInput = 0) { balance += nValue; static int nextLockTime = 0; CMutableTransaction tx; // so all transactions get different hashes tx.nLockTime = nextLockTime++; tx.vout.resize(nInput + 1); tx.vout[nInput].nValue = nValue; 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() tx.vin.resize(1); } std::unique_ptr wtx( new CWalletTx(&wallet, MakeTransactionRef(std::move(tx)))); if (fIsFromMe) { wtx->fDebitCached = true; wtx->nDebitCached = SATOSHI; } COutput output(wtx.get(), nInput, nAge, true /* spendable */, true /* solvable */, true /* safe */); vCoins.push_back(output); wallet.AddToWallet(*wtx.get()); wtxn.emplace_back(std::move(wtx)); } static void empty_wallet(void) { vCoins.clear(); wtxn.clear(); balance = Amount::zero(); } static bool equal_sets(CoinSet a, CoinSet b) { std::pair ret = mismatch(a.begin(), a.end(), b.begin()); return ret.first == a.end() && ret.second == b.end(); } static Amount make_hard_case(int utxos, std::vector &utxo_pool) { utxo_pool.clear(); Amount target = Amount::zero(); for (int i = 0; i < utxos; ++i) { const Amount base = (int64_t(1) << (utxos + i)) * SATOSHI; target += base; add_coin(base, 2 * i, utxo_pool); add_coin(base + (int64_t(1) << (utxos - 1 - i)) * SATOSHI, 2 * i + 1, utxo_pool); } return target; } +inline std::vector & +GroupCoins(const std::vector &coins) { + static std::vector static_groups; + static_groups.clear(); + for (auto &coin : coins) { + static_groups.emplace_back(coin, 0, true, 0, 0); + } + return static_groups; +} + +inline std::vector &GroupCoins(const std::vector &coins) { + static std::vector static_groups; + static_groups.clear(); + for (auto &coin : coins) { + // HACK: we can't figure out the is_me flag so we use the conditions + // defined below; perhaps set safe to false for !fIsFromMe in add_coin() + const bool is_me = + coin.tx->fDebitCached && coin.tx->nDebitCached == SATOSHI; + static_groups.emplace_back(coin.GetInputCoin(), coin.nDepth, is_me, 0, + 0); + } + return static_groups; +} + // Branch and bound coin selection tests BOOST_AUTO_TEST_CASE(bnb_search_test) { LOCK(m_wallet.cs_wallet); // Setup std::vector utxo_pool; CoinSet selection; CoinSet actual_selection; Amount value_ret = Amount::zero(); Amount not_input_fees = Amount::zero(); ///////////////////////// // Known Outcome tests // ///////////////////////// BOOST_TEST_MESSAGE("Testing known outcomes"); // Empty utxo pool - BOOST_CHECK(!SelectCoinsBnB(utxo_pool, 1 * CENT, CENT / 2, selection, - value_ret, not_input_fees)); + BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, CENT / 2, + selection, value_ret, not_input_fees)); selection.clear(); // Add utxos add_coin(1 * CENT, 1, utxo_pool); add_coin(2 * CENT, 2, utxo_pool); add_coin(3 * CENT, 3, utxo_pool); add_coin(4 * CENT, 4, utxo_pool); // Select 1 Cent add_coin(1 * CENT, 1, actual_selection); - BOOST_CHECK(SelectCoinsBnB(utxo_pool, 1 * CENT, CENT / 2, selection, - value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, CENT / 2, + selection, value_ret, not_input_fees)); BOOST_CHECK(equal_sets(selection, actual_selection)); actual_selection.clear(); selection.clear(); // Select 2 Cent add_coin(2 * CENT, 2, actual_selection); - BOOST_CHECK(SelectCoinsBnB(utxo_pool, 2 * CENT, CENT / 2, selection, - value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 2 * CENT, CENT / 2, + selection, value_ret, not_input_fees)); BOOST_CHECK(equal_sets(selection, actual_selection)); actual_selection.clear(); selection.clear(); // Select 5 Cent add_coin(3 * CENT, 3, actual_selection); add_coin(2 * CENT, 2, actual_selection); - BOOST_CHECK(SelectCoinsBnB(utxo_pool, 5 * CENT, CENT / 2, selection, - value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 5 * CENT, CENT / 2, + selection, value_ret, not_input_fees)); BOOST_CHECK(equal_sets(selection, actual_selection)); actual_selection.clear(); selection.clear(); // Select 11 Cent, not possible - BOOST_CHECK(!SelectCoinsBnB(utxo_pool, 11 * CENT, CENT / 2, selection, - value_ret, not_input_fees)); + BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 11 * CENT, CENT / 2, + selection, value_ret, not_input_fees)); actual_selection.clear(); selection.clear(); // Select 10 Cent add_coin(5 * CENT, 5, utxo_pool); add_coin(4 * CENT, 4, actual_selection); add_coin(3 * CENT, 3, actual_selection); add_coin(2 * CENT, 2, actual_selection); add_coin(1 * CENT, 1, actual_selection); - BOOST_CHECK(SelectCoinsBnB(utxo_pool, 10 * CENT, CENT / 2, selection, - value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, CENT / 2, + selection, value_ret, not_input_fees)); BOOST_CHECK(equal_sets(selection, actual_selection)); actual_selection.clear(); selection.clear(); // Negative effective value // Select 10 Cent but have 1 Cent not be possible because too small add_coin(5 * CENT, 5, actual_selection); add_coin(3 * CENT, 3, actual_selection); add_coin(2 * CENT, 2, actual_selection); - BOOST_CHECK(SelectCoinsBnB(utxo_pool, 10 * CENT, 5000 * SATOSHI, selection, - value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 5000 * SATOSHI, + selection, value_ret, not_input_fees)); // Select 0.25 Cent, not possible - BOOST_CHECK(!SelectCoinsBnB(utxo_pool, CENT / 4, CENT / 2, selection, - value_ret, not_input_fees)); + BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), CENT / 4, CENT / 2, + selection, value_ret, not_input_fees)); actual_selection.clear(); selection.clear(); // Iteration exhaustion test Amount target = make_hard_case(17, utxo_pool); // Should exhaust - BOOST_CHECK(!SelectCoinsBnB(utxo_pool, target, Amount::zero(), selection, - value_ret, not_input_fees)); + BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), target, Amount::zero(), + selection, value_ret, not_input_fees)); target = make_hard_case(14, utxo_pool); // Should not exhaust - BOOST_CHECK(SelectCoinsBnB(utxo_pool, target, Amount::zero(), selection, - value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), target, Amount::zero(), + selection, value_ret, not_input_fees)); // Test same value early bailout optimization add_coin(7 * CENT, 7, actual_selection); add_coin(7 * CENT, 7, actual_selection); add_coin(7 * CENT, 7, actual_selection); add_coin(7 * CENT, 7, actual_selection); add_coin(2 * CENT, 7, actual_selection); add_coin(7 * CENT, 7, utxo_pool); add_coin(7 * CENT, 7, utxo_pool); add_coin(7 * CENT, 7, utxo_pool); add_coin(7 * CENT, 7, utxo_pool); add_coin(2 * CENT, 7, utxo_pool); for (int i = 0; i < 50000; ++i) { add_coin(5 * CENT, 7, utxo_pool); } - BOOST_CHECK(SelectCoinsBnB(utxo_pool, 30 * CENT, 5000 * SATOSHI, selection, - value_ret, not_input_fees)); + BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 30 * CENT, 5000 * SATOSHI, + selection, value_ret, not_input_fees)); //////////////////// // Behavior tests // //////////////////// // Select 1 Cent with pool of only greater than 5 Cent utxo_pool.clear(); for (int i = 5; i <= 20; ++i) { add_coin(i * CENT, i, utxo_pool); } // Run 100 times, to make sure it is never finding a solution for (int i = 0; i < 100; ++i) { - BOOST_CHECK(!SelectCoinsBnB(utxo_pool, 1 * CENT, 2 * CENT, selection, - value_ret, not_input_fees)); + BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 2 * CENT, + selection, value_ret, not_input_fees)); } // Make sure that effective value is working in SelectCoinsMinConf when BnB // is used CoinSelectionParams coin_selection_params_bnb(true, 0, 0, CFeeRate(3000 * SATOSHI), 0); CoinSet setCoinsRet; Amount nValueRet; bool bnb_used; empty_wallet(); add_coin(m_wallet, SATOSHI); // Make sure that it has a negative effective value. The next check should // assert if this somehow got through. Otherwise it will fail vCoins.at(0).nInputBytes = 40; BOOST_CHECK(!m_wallet.SelectCoinsMinConf( - 1 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, + 1 * CENT, filter_standard, GroupCoins(vCoins), setCoinsRet, nValueRet, coin_selection_params_bnb, bnb_used)); // Make sure that we aren't using BnB when there are preset inputs empty_wallet(); add_coin(m_wallet, 5 * CENT); add_coin(m_wallet, 3 * CENT); add_coin(m_wallet, 2 * CENT); CCoinControl coin_control; coin_control.fAllowOtherInputs = true; coin_control.Select(COutPoint(vCoins.at(0).tx->GetId(), vCoins.at(0).i)); BOOST_CHECK(m_wallet.SelectCoins(vCoins, 10 * CENT, setCoinsRet, nValueRet, coin_control, coin_selection_params_bnb, bnb_used)); BOOST_CHECK(!bnb_used); BOOST_CHECK(!coin_selection_params_bnb.use_bnb); } BOOST_AUTO_TEST_CASE(knapsack_solver_test) { CWallet testWallet(Params(), "dummy", CWalletDBWrapper::CreateDummy()); CoinSet setCoinsRet, setCoinsRet2; Amount nValueRet; bool bnb_used; LOCK(testWallet.cs_wallet); // 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(!testWallet.SelectCoinsMinConf( - 1 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 1 * CENT, filter_standard, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); + // add a new 1 cent coin add_coin(testWallet, 1 * CENT, 4); // with a new 1 cent coin, we still can't find a mature 1 cent BOOST_CHECK(!testWallet.SelectCoinsMinConf( - 1 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 1 * CENT, filter_standard, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // but we can find a new 1 cent BOOST_CHECK(testWallet.SelectCoinsMinConf( - 1 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 1 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK_EQUAL(nValueRet, 1 * CENT); // add a mature 2 cent coin add_coin(testWallet, 2 * CENT); // we can't make 3 cents of mature coins BOOST_CHECK(!testWallet.SelectCoinsMinConf( - 3 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 3 * CENT, filter_standard, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // we can make 3 cents of new coins BOOST_CHECK(testWallet.SelectCoinsMinConf( - 3 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 3 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK_EQUAL(nValueRet, 3 * CENT); // add a mature 5 cent coin, add_coin(testWallet, 5 * CENT); // a new 10 cent coin sent from one of our own addresses add_coin(testWallet, 10 * CENT, 3, true); // and a mature 20 cent coin add_coin(testWallet, 20 * CENT); // 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(!testWallet.SelectCoinsMinConf( - 38 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 38 * CENT, filter_standard, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // we can't even make 37 cents if we don't allow new coins even if // they're from us BOOST_CHECK(!testWallet.SelectCoinsMinConf( - 38 * CENT, filter_standard_extra, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 38 * CENT, filter_standard_extra, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // but we can make 37 cents if we accept new coins from ourself BOOST_CHECK(testWallet.SelectCoinsMinConf( - 37 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 37 * CENT, filter_standard, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK_EQUAL(nValueRet, 37 * CENT); // and we can make 38 cents if we accept all new coins BOOST_CHECK(testWallet.SelectCoinsMinConf( - 38 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 38 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK_EQUAL(nValueRet, 38 * CENT); // try making 34 cents from 1,2,5,10,20 - we can't do it exactly BOOST_CHECK(testWallet.SelectCoinsMinConf( - 34 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 34 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // but 35 cents is closest BOOST_CHECK_EQUAL(nValueRet, 35 * CENT); // the best should be 20+10+5. it's incredibly unlikely the 1 or 2 got // included (but possible) BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U); // when we try making 7 cents, the smaller coins (1,2,5) are enough. We // should see just 2+5 BOOST_CHECK(testWallet.SelectCoinsMinConf( - 7 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 7 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK_EQUAL(nValueRet, 7 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); // when we try making 8 cents, the smaller coins (1,2,5) are exactly // enough. BOOST_CHECK(testWallet.SelectCoinsMinConf( - 8 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 8 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK(nValueRet == 8 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U); // when we try making 9 cents, no subset of smaller coins is enough, and // we get the next bigger coin (10) BOOST_CHECK(testWallet.SelectCoinsMinConf( - 9 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 9 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK_EQUAL(nValueRet, 10 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); // now clear out the wallet and start again to test choosing between // subsets of smaller coins and the next biggest coin empty_wallet(); add_coin(testWallet, 6 * CENT); add_coin(testWallet, 7 * CENT); add_coin(testWallet, 8 * CENT); add_coin(testWallet, 20 * CENT); // now we have 6+7+8+20+30 = 71 cents total add_coin(testWallet, 30 * CENT); // check that we have 71 and not 72 BOOST_CHECK(testWallet.SelectCoinsMinConf( - 71 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 71 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK(!testWallet.SelectCoinsMinConf( - 72 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 72 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // 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(testWallet.SelectCoinsMinConf( - 16 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 16 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // we should get 20 in one coin BOOST_CHECK_EQUAL(nValueRet, 20 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); // now we have 5+6+7+8+20+30 = 75 cents total add_coin(testWallet, 5 * CENT); // 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(testWallet.SelectCoinsMinConf( - 16 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 16 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // we should get 18 in 3 coins BOOST_CHECK_EQUAL(nValueRet, 18 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U); // now we have 5+6+7+8+18+20+30 add_coin(testWallet, 18 * CENT); // 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(testWallet.SelectCoinsMinConf( - 16 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 16 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // we should get 18 in 1 coin BOOST_CHECK_EQUAL(nValueRet, 18 * CENT); // because in the event of a tie, the biggest coin wins BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); // now try making 11 cents. we should get 5+6 BOOST_CHECK(testWallet.SelectCoinsMinConf( - 11 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 11 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK_EQUAL(nValueRet, 11 * CENT); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); // check that the smallest bigger coin is used add_coin(testWallet, 1 * COIN); add_coin(testWallet, 2 * COIN); add_coin(testWallet, 3 * COIN); // now we have 5+6+7+8+18+20+30+100+200+300+400 = 1094 cents add_coin(testWallet, 4 * COIN); BOOST_CHECK(testWallet.SelectCoinsMinConf( - 95 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 95 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // we should get 1 BCH in 1 coin BOOST_CHECK_EQUAL(nValueRet, 1 * COIN); BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); BOOST_CHECK(testWallet.SelectCoinsMinConf( - 195 * CENT, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 195 * CENT, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // we should get 2 BCH in 1 coin BOOST_CHECK_EQUAL(nValueRet, 2 * COIN); BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); // empty the wallet and start again, now with fractions of a cent, to // test small change avoidance empty_wallet(); add_coin(testWallet, 1 * MIN_CHANGE / 10); add_coin(testWallet, 2 * MIN_CHANGE / 10); add_coin(testWallet, 3 * MIN_CHANGE / 10); add_coin(testWallet, 4 * MIN_CHANGE / 10); add_coin(testWallet, 5 * MIN_CHANGE / 10); - // try making 1 * MIN_CHANGE from the 1.5 * MIN_CHANGE we'll get change - // smaller than MIN_CHANGE whatever happens, so can expect MIN_CHANGE - // exactly + // try making 1 * MIN_CHANGE from the 1.5 * MIN_CHANGE + // we'll get change smaller than MIN_CHANGE whatever happens, so can + // expect MIN_CHANGE exactly BOOST_CHECK(testWallet.SelectCoinsMinConf( - MIN_CHANGE, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + MIN_CHANGE, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE); // but if we add a bigger coin, small change is avoided add_coin(testWallet, 1111 * MIN_CHANGE); // try making 1 from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 1111 = 1112.5 BOOST_CHECK(testWallet.SelectCoinsMinConf( - 1 * MIN_CHANGE, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 1 * MIN_CHANGE, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // we should get the exact amount BOOST_CHECK_EQUAL(nValueRet, 1 * MIN_CHANGE); // if we add more small coins: add_coin(testWallet, 6 * MIN_CHANGE / 10); add_coin(testWallet, 7 * MIN_CHANGE / 10); // and try again to make 1.0 * MIN_CHANGE BOOST_CHECK(testWallet.SelectCoinsMinConf( - 1 * MIN_CHANGE, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 1 * MIN_CHANGE, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // we should get the exact amount BOOST_CHECK_EQUAL(nValueRet, 1 * MIN_CHANGE); // 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 j = 0; j < 20; j++) { add_coin(testWallet, 50000 * COIN); } BOOST_CHECK(testWallet.SelectCoinsMinConf( - 500000 * COIN, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 500000 * COIN, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // we should get the exact amount BOOST_CHECK_EQUAL(nValueRet, 500000 * COIN); // in ten coins BOOST_CHECK_EQUAL(setCoinsRet.size(), 10U); // if there's not enough in the smaller coins to make at least 1 * // MIN_CHANGE 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(testWallet, 5 * MIN_CHANGE / 10); add_coin(testWallet, 6 * MIN_CHANGE / 10); add_coin(testWallet, 7 * MIN_CHANGE / 10); add_coin(testWallet, 1111 * MIN_CHANGE); BOOST_CHECK(testWallet.SelectCoinsMinConf( - 1 * MIN_CHANGE, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 1 * MIN_CHANGE, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // we get the bigger coin BOOST_CHECK_EQUAL(nValueRet, 1111 * MIN_CHANGE); BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); // but sometimes it's possible, and we use an exact subset (0.4 + 0.6 = // 1.0) empty_wallet(); add_coin(testWallet, 4 * MIN_CHANGE / 10); add_coin(testWallet, 6 * MIN_CHANGE / 10); add_coin(testWallet, 8 * MIN_CHANGE / 10); add_coin(testWallet, 1111 * MIN_CHANGE); BOOST_CHECK(testWallet.SelectCoinsMinConf( - MIN_CHANGE, filter_confirmed, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + MIN_CHANGE, filter_confirmed, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); // we should get the exact amount BOOST_CHECK_EQUAL(nValueRet, MIN_CHANGE); // in two coins 0.4+0.6 BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); // test avoiding small change empty_wallet(); add_coin(testWallet, 5 * MIN_CHANGE / 100); add_coin(testWallet, 1 * MIN_CHANGE); add_coin(testWallet, 100 * MIN_CHANGE); // trying to make 100.01 from these three coins BOOST_CHECK(testWallet.SelectCoinsMinConf( - 10001 * MIN_CHANGE / 100, filter_confirmed, vCoins, setCoinsRet, - nValueRet, coin_selection_params, bnb_used)); + 10001 * MIN_CHANGE / 100, filter_confirmed, GroupCoins(vCoins), + setCoinsRet, nValueRet, coin_selection_params, bnb_used)); // we should get all coins BOOST_CHECK_EQUAL(nValueRet, 10105 * MIN_CHANGE / 100); BOOST_CHECK_EQUAL(setCoinsRet.size(), 3U); // but if we try to make 99.9, we should take the bigger of the two // small coins to avoid small change BOOST_CHECK(testWallet.SelectCoinsMinConf( - 9990 * MIN_CHANGE / 100, filter_confirmed, vCoins, setCoinsRet, - nValueRet, coin_selection_params, bnb_used)); + 9990 * MIN_CHANGE / 100, filter_confirmed, GroupCoins(vCoins), + setCoinsRet, nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK_EQUAL(nValueRet, 101 * MIN_CHANGE); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); // test with many inputs for (Amount amt = 1500 * SATOSHI; amt < COIN; amt = 10 * amt) { empty_wallet(); // Create 676 inputs (= (old MAX_STANDARD_TX_SIZE == 100000) / 148 // bytes per input) for (uint16_t j = 0; j < 676; j++) { add_coin(testWallet, amt); } BOOST_CHECK(testWallet.SelectCoinsMinConf( - 2000 * SATOSHI, filter_confirmed, vCoins, setCoinsRet, - nValueRet, coin_selection_params, bnb_used)); + 2000 * SATOSHI, filter_confirmed, GroupCoins(vCoins), + setCoinsRet, nValueRet, coin_selection_params, bnb_used)); if (amt - 2000 * SATOSHI < MIN_CHANGE) { // needs more than one input: uint16_t returnSize = std::ceil( - double(2000 + (MIN_CHANGE / SATOSHI)) / (amt / SATOSHI)); + (2000.0 + (MIN_CHANGE / SATOSHI)) / (amt / SATOSHI)); Amount returnValue = returnSize * amt; BOOST_CHECK_EQUAL(nValueRet, returnValue); BOOST_CHECK_EQUAL(setCoinsRet.size(), returnSize); } else { // one input is sufficient: BOOST_CHECK_EQUAL(nValueRet, amt); BOOST_CHECK_EQUAL(setCoinsRet.size(), 1U); } } // test randomness { empty_wallet(); for (int i2 = 0; i2 < 100; i2++) { add_coin(testWallet, COIN); } // picking 50 from 100 coins doesn't depend on the shuffle, but does // depend on randomness in the stochastic approximation code BOOST_CHECK(testWallet.SelectCoinsMinConf( - 50 * COIN, filter_standard, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 50 * COIN, filter_standard, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK(testWallet.SelectCoinsMinConf( - 50 * COIN, filter_standard, vCoins, setCoinsRet2, nValueRet, - coin_selection_params, bnb_used)); + 50 * COIN, filter_standard, GroupCoins(vCoins), setCoinsRet2, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK(!equal_sets(setCoinsRet, setCoinsRet2)); int fails = 0; for (int j = 0; j < RANDOM_REPEATS; j++) { // selecting 1 from 100 identical coins depends on the shuffle; // this test will fail 1% of the time run the test // RANDOM_REPEATS times and only complain if all of them fail BOOST_CHECK(testWallet.SelectCoinsMinConf( - COIN, filter_standard, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + COIN, filter_standard, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK(testWallet.SelectCoinsMinConf( - COIN, filter_standard, vCoins, setCoinsRet2, nValueRet, - coin_selection_params, bnb_used)); + COIN, filter_standard, GroupCoins(vCoins), setCoinsRet2, + nValueRet, coin_selection_params, bnb_used)); if (equal_sets(setCoinsRet, setCoinsRet2)) { fails++; } } BOOST_CHECK_NE(fails, RANDOM_REPEATS); // add 75 cents in small change. not enough to make 90 cents, then // try making 90 cents. there are multiple competing "smallest // bigger" coins, one of which should be picked at random add_coin(testWallet, 5 * CENT); add_coin(testWallet, 10 * CENT); add_coin(testWallet, 15 * CENT); add_coin(testWallet, 20 * CENT); add_coin(testWallet, 25 * CENT); fails = 0; for (int j = 0; j < RANDOM_REPEATS; j++) { // selecting 1 from 100 identical coins depends on the shuffle; // this test will fail 1% of the time run the test // RANDOM_REPEATS times and only complain if all of them fail BOOST_CHECK(testWallet.SelectCoinsMinConf( - 90 * CENT, filter_standard, vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + 90 * CENT, filter_standard, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK(testWallet.SelectCoinsMinConf( - 90 * CENT, filter_standard, vCoins, setCoinsRet2, nValueRet, - coin_selection_params, bnb_used)); + 90 * CENT, filter_standard, GroupCoins(vCoins), + setCoinsRet2, nValueRet, coin_selection_params, bnb_used)); if (equal_sets(setCoinsRet, setCoinsRet2)) { fails++; } } BOOST_CHECK_NE(fails, RANDOM_REPEATS); } } empty_wallet(); } BOOST_AUTO_TEST_CASE(ApproximateBestSubset) { CoinSet setCoinsRet; Amount nValueRet; bool bnb_used; LOCK(m_wallet.cs_wallet); empty_wallet(); // Test vValue sort order for (int i = 0; i < 1000; i++) { add_coin(m_wallet, 1000 * COIN); } add_coin(m_wallet, 3 * COIN); - BOOST_CHECK(m_wallet.SelectCoinsMinConf(1003 * COIN, filter_standard, - vCoins, setCoinsRet, nValueRet, - coin_selection_params, bnb_used)); + BOOST_CHECK(m_wallet.SelectCoinsMinConf( + 1003 * COIN, filter_standard, GroupCoins(vCoins), setCoinsRet, + nValueRet, coin_selection_params, bnb_used)); BOOST_CHECK_EQUAL(nValueRet, 1003 * COIN); BOOST_CHECK_EQUAL(setCoinsRet.size(), 2U); empty_wallet(); } // Tests that with the ideal conditions, the coin selector will always be able // to find a solution that can pay the target value BOOST_AUTO_TEST_CASE(SelectCoins_test) { CWallet testWallet(Params(), "dummy", CWalletDBWrapper::CreateDummy()); // Random generator stuff std::default_random_engine generator; std::exponential_distribution distribution(100); FastRandomContext rand; // Run this test 100 times for (int i = 0; i < 100; ++i) { empty_wallet(); // Make a wallet with 1000 exponentially distributed random inputs for (int j = 0; j < 1000; ++j) { add_coin(testWallet, int64_t(10000000 * distribution(generator)) * SATOSHI); } // Generate a random fee rate in the range of 100 - 400 CFeeRate rate(int64_t(rand.randrange(300) + 100) * SATOSHI); // Generate a random target value between 1000 and wallet balance Amount target = int64_t(rand.randrange(balance / SATOSHI - 1000) + 1000) * SATOSHI; // Perform selection CoinSelectionParams coin_selection_params_knapsack( false, 34, 148, CFeeRate(Amount::zero()), 0); CoinSelectionParams coin_selection_params_bnb( true, 34, 148, CFeeRate(Amount::zero()), 0); CoinSet out_set; Amount out_value = Amount::zero(); - bool bnb_used; + bool bnb_used = false; BOOST_CHECK(testWallet.SelectCoinsMinConf( - target, filter_standard, vCoins, out_set, out_value, - coin_selection_params_bnb, bnb_used) || + target, filter_standard, GroupCoins(vCoins), out_set, + out_value, coin_selection_params_bnb, bnb_used) || testWallet.SelectCoinsMinConf( - target, filter_standard, vCoins, out_set, out_value, - coin_selection_params_knapsack, bnb_used)); + target, filter_standard, GroupCoins(vCoins), out_set, + out_value, coin_selection_params_knapsack, bnb_used)); BOOST_CHECK_GE(out_value, target); } } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 54cb7e959..18006e465 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1,4683 +1,4699 @@ // Copyright (c) 2009-2010 Satoshi Nakamoto // Copyright (c) 2009-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for IsDeprecatedRPCEnabled #include #include