diff --git a/src/wallet/coinselection.h b/src/wallet/coinselection.h --- a/src/wallet/coinselection.h +++ b/src/wallet/coinselection.h @@ -53,4 +53,8 @@ std::set &out_set, Amount &value_ret, Amount not_input_fees); +// Original coin selection algorithm as a fallback +bool KnapsackSolver(const Amount nTargetValue, std::vector &vCoins, + std::set &setCoinsRet, Amount &nValueRet); + #endif // BITCOIN_COINSELECTION_H diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp --- a/src/wallet/coinselection.cpp +++ b/src/wallet/coinselection.cpp @@ -202,3 +202,130 @@ return true; } + +static void ApproximateBestSubset(const std::vector &vValue, + const Amount &nTotalLower, + const Amount &nTargetValue, + std::vector &vfBest, Amount &nBest, + int iterations = 1000) { + std::vector vfIncluded; + + vfBest.assign(vValue.size(), true); + nBest = nTotalLower; + + FastRandomContext insecure_rand; + + for (int nRep = 0; nRep < iterations && nBest != nTargetValue; nRep++) { + vfIncluded.assign(vValue.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++) { + // 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; + vfIncluded[i] = true; + if (nTotal >= nTargetValue) { + fReachedTarget = true; + if (nTotal < nBest) { + nBest = nTotal; + vfBest = vfIncluded; + } + + nTotal -= vValue[i].txout.nValue; + vfIncluded[i] = false; + } + } + } + } + } +} + +bool KnapsackSolver(const Amount nTargetValue, std::vector &vCoins, + std::set &setCoinsRet, Amount &nValueRet) { + setCoinsRet.clear(); + nValueRet = Amount::zero(); + + // List of values less than target + boost::optional coinLowestLarger; + std::vector vValue; + Amount nTotalLower = Amount::zero(); + + random_shuffle(vCoins.begin(), vCoins.end(), GetRandInt); + + for (const CInputCoin &coin : vCoins) { + if (coin.txout.nValue == nTargetValue) { + setCoinsRet.insert(coin); + nValueRet += coin.txout.nValue; + 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; + } + } + + if (nTotalLower == nTargetValue) { + for (const auto &input : vValue) { + setCoinsRet.insert(input); + nValueRet += input.txout.nValue; + } + return true; + } + + if (nTotalLower < nTargetValue) { + if (!coinLowestLarger) { + return false; + } + setCoinsRet.insert(coinLowestLarger.get()); + nValueRet += coinLowestLarger->txout.nValue; + return true; + } + + // Solve subset sum by stochastic approximation + std::sort(vValue.begin(), vValue.end(), descending); + std::vector vfBest; + Amount nBest; + + ApproximateBestSubset(vValue, nTotalLower, nTargetValue, vfBest, nBest); + if (nBest != nTargetValue && nTotalLower >= nTargetValue + MIN_CHANGE) { + ApproximateBestSubset(vValue, 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 && + ((nBest != nTargetValue && nBest < nTargetValue + MIN_CHANGE) || + coinLowestLarger->txout.nValue <= nBest)) { + setCoinsRet.insert(coinLowestLarger.get()); + nValueRet += coinLowestLarger->txout.nValue; + } else { + for (size_t i = 0; i < vValue.size(); i++) { + if (vfBest[i]) { + setCoinsRet.insert(vValue[i]); + nValueRet += vValue[i].txout.nValue; + } + } + + if (LogAcceptCategory(BCLog::SELECTCOINS)) { + LogPrint(BCLog::SELECTCOINS, "SelectCoins() best subset: "); + for (size_t i = 0; i < vValue.size(); i++) { + if (vfBest[i]) { + LogPrint(BCLog::SELECTCOINS, "%s ", + FormatMoney(vValue[i].txout.nValue)); + } + } + LogPrint(BCLog::SELECTCOINS, "total %s\n", FormatMoney(nBest)); + } + } + + return true; +} diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2515,48 +2515,6 @@ return ptx->vout[n]; } -static void ApproximateBestSubset(const std::vector &vValue, - const Amount &nTotalLower, - const Amount &nTargetValue, - std::vector &vfBest, Amount &nBest, - int iterations = 1000) { - std::vector vfIncluded; - - vfBest.assign(vValue.size(), true); - nBest = nTotalLower; - - FastRandomContext insecure_rand; - - for (int nRep = 0; nRep < iterations && nBest != nTargetValue; nRep++) { - vfIncluded.assign(vValue.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++) { - // 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; - vfIncluded[i] = true; - if (nTotal >= nTargetValue) { - fReachedTarget = true; - if (nTotal < nBest) { - nBest = nTotal; - vfBest = vfIncluded; - } - - nTotal -= vValue[i].txout.nValue; - vfIncluded[i] = false; - } - } - } - } - } -} - bool CWallet::OutputEligibleForSpending( const COutput &output, const CoinEligibilityFilter &eligibilty_filter) const { @@ -2586,93 +2544,16 @@ setCoinsRet.clear(); nValueRet = Amount::zero(); - // List of values less than target - boost::optional coinLowestLarger; - std::vector vValue; - Amount nTotalLower = Amount::zero(); - - random_shuffle(vCoins.begin(), vCoins.end(), GetRandInt); - + std::vector utxo_pool; for (const COutput &output : vCoins) { if (!OutputEligibleForSpending(output, eligibilty_filter)) { continue; } CInputCoin coin = CInputCoin(output.tx->tx, output.i); - - if (coin.txout.nValue == nTargetValue) { - setCoinsRet.insert(coin); - nValueRet += coin.txout.nValue; - 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; - } - } - - if (nTotalLower == nTargetValue) { - for (unsigned int i = 0; i < vValue.size(); ++i) { - setCoinsRet.insert(vValue[i]); - nValueRet += vValue[i].txout.nValue; - } - - return true; + utxo_pool.push_back(coin); } - - if (nTotalLower < nTargetValue) { - if (!coinLowestLarger) { - return false; - } - - setCoinsRet.insert(coinLowestLarger.get()); - nValueRet += coinLowestLarger->txout.nValue; - return true; - } - - // Solve subset sum by stochastic approximation - std::sort(vValue.begin(), vValue.end(), CompareValueOnly()); - std::reverse(vValue.begin(), vValue.end()); - std::vector vfBest; - Amount nBest; - - ApproximateBestSubset(vValue, nTotalLower, nTargetValue, vfBest, nBest); - if (nBest != nTargetValue && nTotalLower >= nTargetValue + MIN_CHANGE) { - ApproximateBestSubset(vValue, 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 && - ((nBest != nTargetValue && nBest < nTargetValue + MIN_CHANGE) || - coinLowestLarger->txout.nValue <= nBest)) { - setCoinsRet.insert(coinLowestLarger.get()); - nValueRet += coinLowestLarger->txout.nValue; - } else { - for (unsigned int i = 0; i < vValue.size(); i++) { - if (vfBest[i]) { - setCoinsRet.insert(vValue[i]); - nValueRet += vValue[i].txout.nValue; - } - } - - if (LogAcceptCategory(BCLog::SELECTCOINS)) { - LogPrint(BCLog::SELECTCOINS, "SelectCoins() best subset: "); - for (size_t i = 0; i < vValue.size(); i++) { - if (vfBest[i]) { - LogPrint(BCLog::SELECTCOINS, "%s ", - FormatMoney(vValue[i].txout.nValue)); - } - } - LogPrint(BCLog::SELECTCOINS, "total %s\n", FormatMoney(nBest)); - } - } - - return true; + return KnapsackSolver(nTargetValue, utxo_pool, setCoinsRet, nValueRet); } bool CWallet::SelectCoins(const std::vector &vAvailableCoins,